From 62158812f1401c5ad291302bdc8bf4fb38353dc6 Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Thu, 2 Jul 2026 12:06:27 +0800 Subject: [PATCH v1] Escape CR/LF in invalid global object name errors Commit b380a56a3 added checks to reject CR and LF in database, role, and tablespace names, but the error messages still included the raw rejected name. This could make client output and server log entries confusing, since LF splits the message across physical lines and CR can overwrite the beginning of the line. Add a common helper to escape CR and LF in names used for line-oriented error/report output, and use it for the new database, role, and tablespace validation errors. Also use it in pg_upgrade when reporting old-cluster database, role, and tablespace names that contain CR or LF. Author: Chao Li --- src/backend/commands/dbcommands.c | 7 ++- src/backend/commands/tablespace.c | 7 ++- src/backend/commands/user.c | 7 ++- src/bin/pg_upgrade/check.c | 12 +++- src/common/string.c | 56 +++++++++++++++++++ src/include/common/string.h | 1 + .../expected/alter_system_table.out | 3 +- .../unsafe_tests/expected/rolenames.out | 3 +- src/test/regress/expected/tablespace.out | 3 +- 9 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c index f0819d15ab7..3072a043d99 100644 --- a/src/backend/commands/dbcommands.c +++ b/src/backend/commands/dbcommands.c @@ -49,6 +49,7 @@ #include "commands/seclabel.h" #include "commands/tablespace.h" #include "common/file_perm.h" +#include "common/string.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "pgstat.h" @@ -748,7 +749,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) if (strpbrk(dbname, "\n\r")) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("database name \"%s\" contains a newline or carriage return character", dbname))); + errmsg("database name \"%s\" contains a newline or carriage return character", + pg_escape_name_for_error(dbname)))); /* Extract options from the statement node tree */ foreach(option, stmt->options) @@ -1929,7 +1931,8 @@ RenameDatabase(const char *oldname, const char *newname) if (strpbrk(newname, "\n\r")) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("database name \"%s\" contains a newline or carriage return character", newname))); + errmsg("database name \"%s\" contains a newline or carriage return character", + pg_escape_name_for_error(newname)))); /* * Look up the target database's OID, and get exclusive lock on it. We diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c index d91fcf0facf..7c925de1fd6 100644 --- a/src/backend/commands/tablespace.c +++ b/src/backend/commands/tablespace.c @@ -67,6 +67,7 @@ #include "commands/seclabel.h" #include "commands/tablespace.h" #include "common/file_perm.h" +#include "common/string.h" #include "miscadmin.h" #include "postmaster/bgwriter.h" #include "storage/fd.h" @@ -247,7 +248,8 @@ CreateTableSpace(CreateTableSpaceStmt *stmt) if (strpbrk(stmt->tablespacename, "\n\r")) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("tablespace name \"%s\" contains a newline or carriage return character", stmt->tablespacename))); + errmsg("tablespace name \"%s\" contains a newline or carriage return character", + pg_escape_name_for_error(stmt->tablespacename)))); in_place = allow_in_place_tablespaces && strlen(location) == 0; @@ -982,7 +984,8 @@ RenameTableSpace(const char *oldname, const char *newname) if (strpbrk(newname, "\n\r")) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("tablespace name \"%s\" contains a newline or carriage return character", newname))); + errmsg("tablespace name \"%s\" contains a newline or carriage return character", + pg_escape_name_for_error(newname)))); /* * If built with appropriate switch, whine when regression-testing diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index be11c49f919..5deda77ab9b 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -30,6 +30,7 @@ #include "commands/defrem.h" #include "commands/seclabel.h" #include "commands/user.h" +#include "common/string.h" #include "libpq/crypt.h" #include "miscadmin.h" #include "port/pg_bitutils.h" @@ -175,7 +176,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) if (strpbrk(stmt->role, "\n\r")) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("role name \"%s\" contains a newline or carriage return character", stmt->role))); + errmsg("role name \"%s\" contains a newline or carriage return character", + pg_escape_name_for_error(stmt->role)))); /* The defaults can vary depending on the original statement type */ switch (stmt->stmt_type) @@ -1358,7 +1360,8 @@ RenameRole(const char *oldname, const char *newname) if (strpbrk(newname, "\n\r")) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("role name \"%s\" contains a newline or carriage return character", newname))); + errmsg("role name \"%s\" contains a newline or carriage return character", + pg_escape_name_for_error(newname)))); rel = table_open(AuthIdRelationId, RowExclusiveLock); dsc = RelationGetDescr(rel); diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 7e34e15a9d9..59a3c3e4ba0 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -12,10 +12,11 @@ #include "catalog/pg_am_d.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_class_d.h" +#include "common/string.h" +#include "common/unicode_version.h" #include "fe_utils/string_utils.h" #include "mb/pg_wchar.h" #include "pg_upgrade.h" -#include "common/unicode_version.h" static void check_new_cluster_is_empty(void); static void check_is_install_user(ClusterInfo *cluster); @@ -2621,10 +2622,17 @@ check_old_cluster_global_names(ClusterInfo *cluster) /* If name has \n or \r, then report it. */ if (strpbrk(objname, "\n\r")) { + char *escaped_objname; + if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL) pg_fatal("could not open file \"%s\": %m", output_path); - fprintf(script, "%d : %s name = \"%s\"\n", ++count, objtype, objname); + escaped_objname = pg_escape_name_for_error(objname); + if (escaped_objname == NULL) + pg_fatal("out of memory"); + fprintf(script, "%d : %s name = \"%s\"\n", ++count, objtype, + escaped_objname); + pg_free(escaped_objname); } } diff --git a/src/common/string.c b/src/common/string.c index 41c74a1502d..25938211965 100644 --- a/src/common/string.c +++ b/src/common/string.c @@ -125,6 +125,62 @@ pg_clean_ascii(const char *str, int alloc_flags) } +/* + * pg_escape_name_for_error -- Escape a name for line-oriented error reports + * + * Makes a newly allocated copy of the string passed in, which must be + * '\0'-terminated. In the backend, the copy is palloc'd; in the frontend, it + * is malloc'd. + * + * This preserves valid non-ASCII bytes, but escapes characters that would make + * a name span multiple physical lines. + */ +char * +pg_escape_name_for_error(const char *str) +{ + size_t dstlen; + char *dst; + const char *p; + size_t i = 0; + + /* + * In the worst case, every byte is either '\n' or '\r' and becomes two + * bytes, plus a null terminator. + */ + dstlen = strlen(str) * 2 + 1; + +#ifdef FRONTEND + dst = malloc(dstlen); +#else + dst = palloc(dstlen); +#endif + + if (!dst) + return NULL; + + for (p = str; *p != '\0'; p++) + { + switch (*p) + { + case '\n': + dst[i++] = '\\'; + dst[i++] = 'n'; + break; + case '\r': + dst[i++] = '\\'; + dst[i++] = 'r'; + break; + default: + dst[i++] = *p; + break; + } + } + + dst[i] = '\0'; + return dst; +} + + /* * pg_is_ascii -- Check if string is made only of ASCII characters */ diff --git a/src/include/common/string.h b/src/include/common/string.h index 2a7c31ea74e..5da2d3572b7 100644 --- a/src/include/common/string.h +++ b/src/include/common/string.h @@ -28,6 +28,7 @@ extern bool pg_str_endswith(const char *str, const char *end); extern int strtoint(const char *pg_restrict str, char **pg_restrict endptr, int base); extern char *pg_clean_ascii(const char *str, int alloc_flags); +extern char *pg_escape_name_for_error(const char *str); extern int pg_strip_crlf(char *str); extern bool pg_is_ascii(const char *str); diff --git a/src/test/modules/unsafe_tests/expected/alter_system_table.out b/src/test/modules/unsafe_tests/expected/alter_system_table.out index 9ea6061f4ae..c03bbcfd3b1 100644 --- a/src/test/modules/unsafe_tests/expected/alter_system_table.out +++ b/src/test/modules/unsafe_tests/expected/alter_system_table.out @@ -67,8 +67,7 @@ DETAIL: The prefix "pg_" is reserved for system tablespaces. -- contains \n\r tablespace name CREATE TABLESPACE "invalid name" LOCATION '/no/such/location'; -ERROR: tablespace name "invalid -name" contains a newline or carriage return character +ERROR: tablespace name "invalid\nname" contains a newline or carriage return character -- triggers CREATE FUNCTION tf1() RETURNS trigger LANGUAGE plpgsql diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out index 9dfb8475e16..d7f4d8d485a 100644 --- a/src/test/modules/unsafe_tests/expected/rolenames.out +++ b/src/test/modules/unsafe_tests/expected/rolenames.out @@ -104,8 +104,7 @@ ERROR: role name "pg_abcdef" is reserved DETAIL: Role names starting with "pg_" are reserved. CREATE ROLE "invalid rolename"; -- error -ERROR: role name "invalid -rolename" contains a newline or carriage return character +ERROR: role name "invalid\nrolename" contains a newline or carriage return character CREATE ROLE regress_testrol0 SUPERUSER LOGIN; CREATE ROLE regress_testrolx SUPERUSER LOGIN; CREATE ROLE regress_testrol2 SUPERUSER; diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..ac14a021b8f 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -963,8 +963,7 @@ NOTICE: no matching relations in tablespace "regress_tblspace_renamed" found -- Should fail, contains \n in name ALTER TABLESPACE regress_tblspace_renamed RENAME TO "invalid name"; -ERROR: tablespace name "invalid -name" contains a newline or carriage return character +ERROR: tablespace name "invalid\nname" contains a newline or carriage return character -- Should succeed DROP TABLESPACE regress_tblspace_renamed; DROP SCHEMA testschema CASCADE; -- 2.50.1 (Apple Git-155)