From 07ffe07830bf027307de75d1414f48f42e9e224b Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 10 Nov 2025 09:00:00 -0600 Subject: [PATCH 1/3] Check for CREATE privilege on the schema in CREATE STATISTICS. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This omission allowed table owners to create statistics in any schema, potentially leading to unexpected naming conflicts. For ALTER TABLE commands that require re-creating statistics objects, skip this check in case the user has since lost CREATE on the schema. The addition of a second parameter to CreateStatistics() breaks ABI compatibility, but we are unaware of any impacted third-party code. Reported-by: Jelte Fennema-Nio Author: Jelte Fennema-Nio Co-authored-by: Nathan Bossart Reviewed-by: Noah Misch Reviewed-by: Álvaro Herrera Security: CVE-2025-12817 Backpatch-through: 13 --- src/backend/commands/statscmds.c | 17 +++++++++++- src/backend/commands/tablecmds.c | 2 +- src/backend/tcop/utility.c | 2 +- src/include/commands/defrem.h | 2 +- src/test/regress/expected/stats_ext.out | 36 +++++++++++++++++++++++++ src/test/regress/sql/stats_ext.sql | 33 +++++++++++++++++++++++ 6 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c index d3220849f6a..db0e2a5f74c 100644 --- a/src/backend/commands/statscmds.c +++ b/src/backend/commands/statscmds.c @@ -65,7 +65,7 @@ compare_int16(const void *a, const void *b) * CREATE STATISTICS */ ObjectAddress -CreateStatistics(CreateStatsStmt *stmt) +CreateStatistics(CreateStatsStmt *stmt, bool check_rights) { int16 attnums[STATS_MAX_DIMENSIONS]; int nattnums = 0; @@ -176,6 +176,21 @@ CreateStatistics(CreateStatsStmt *stmt) } namestrcpy(&stxname, namestr); + /* + * Check we have creation rights in target namespace. Skip check if + * caller doesn't want it. + */ + if (check_rights) + { + AclResult aclresult; + + aclresult = object_aclcheck(NamespaceRelationId, namespaceId, + GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_SCHEMA, + get_namespace_name(namespaceId)); + } + /* * Deal with the possibility that the statistics object already exists. */ diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 3548786fe41..b8b69336b89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -11038,7 +11038,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel, Assert(stmt->transformed); HOLD_DISPATCH(); - address = CreateStatistics(stmt); + address = CreateStatistics(stmt, !is_rebuild); RESUME_DISPATCH(); return address; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 08756e59c2a..021c69ad031 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2538,7 +2538,7 @@ ProcessUtilitySlow(ParseState *pstate, /* Run parse analysis ... */ stmt = transformStatsStmt(relid, stmt, queryString); - address = CreateStatistics(stmt); + address = CreateStatistics(stmt, true); } break; diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index 4b00ff7cdc7..7af15a37f52 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -87,7 +87,7 @@ extern void RemoveOperatorById(Oid operOid); extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt); /* commands/statscmds.c */ -extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt); +extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights); extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt); extern void RemoveStatisticsById(Oid statsOid); extern void RemoveStatisticsDataById(Oid statsOid, bool inh); diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out index cd6a5e61e67..80bf1733e89 100644 --- a/src/test/regress/expected/stats_ext.out +++ b/src/test/regress/expected/stats_ext.out @@ -3336,6 +3336,40 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x s_expr | {1} (2 rows) +-- CREATE STATISTICS checks for CREATE on the schema +RESET SESSION AUTHORIZATION; +CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT); +CREATE SCHEMA sts_sch2; +GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch1 +CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch2 +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch2 +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1; +GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch1 +CREATE STATISTICS sts_sch2.pass1 ON a, b FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.pass2 ON a, b FROM sts_sch1.tbl; +-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1, sts_sch2 FROM regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT; -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); @@ -3345,6 +3379,8 @@ DROP SCHEMA tststats CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to table tststats.priv_test_tbl drop cascades to view tststats.priv_test_view +DROP SCHEMA sts_sch1, sts_sch2 CASCADE; +NOTICE: drop cascades to table sts_sch1.tbl DROP USER regress_stats_user1; -- test analyze with extended statistics CREATE TABLE tbl_issue1293 (col1 int, col2 int); diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql index db9196b3918..5ac9587c8fa 100644 --- a/src/test/regress/sql/stats_ext.sql +++ b/src/test/regress/sql/stats_ext.sql @@ -1698,12 +1698,45 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*); +-- CREATE STATISTICS checks for CREATE on the schema +RESET SESSION AUTHORIZATION; +CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT); +CREATE SCHEMA sts_sch2; +GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1; +GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.pass1 ON a, b FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.pass2 ON a, b FROM sts_sch1.tbl; + +-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1, sts_sch2 FROM regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT; + -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); RESET SESSION AUTHORIZATION; DROP TABLE stats_ext_tbl; DROP SCHEMA tststats CASCADE; +DROP SCHEMA sts_sch1, sts_sch2 CASCADE; DROP USER regress_stats_user1; -- test analyze with extended statistics From d1c5ccc96e3b84aafecb9341024da53824bbc3f9 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Mon, 10 Nov 2025 06:03:04 -0800 Subject: [PATCH 2/3] libpq: Prevent some overflows of int/size_t MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several functions could overflow their size calculations, when presented with very large inputs from remote and/or untrusted locations, and then allocate buffers that were too small to hold the intended contents. Switch from int to size_t where appropriate, and check for overflow conditions when the inputs could have plausibly originated outside of the libpq trust boundary. (Overflows from within the trust boundary are still possible, but these will be fixed separately.) A version of add_size() is ported from the backend to assist with code that performs more complicated concatenation. Reported-by: Aleksey Solovev (Positive Technologies) Reviewed-by: Noah Misch Reviewed-by: Álvaro Herrera Security: CVE-2025-12818 Backpatch-through: 13 --- src/interfaces/libpq/fe-connect.c | 17 ++++- src/interfaces/libpq/fe-exec.c | 103 +++++++++++++++++++++++----- src/interfaces/libpq/fe-print.c | 61 ++++++++++++++-- src/interfaces/libpq/fe-protocol3.c | 67 +++++++++++++++--- src/interfaces/libpq/libpq-int.h | 11 ++- 5 files changed, 225 insertions(+), 34 deletions(-) diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 135af368f84..54858d15689 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -1075,7 +1076,7 @@ parse_comma_separated_list(char **startptr, bool *more) char *p; char *s = *startptr; char *e; - int len; + size_t len; /* * Search for the end of the current element; a comma or end-of-string @@ -5594,7 +5595,21 @@ ldapServiceLookup(const char *purl, PQconninfoOption *options, /* concatenate values into a single string with newline terminators */ size = 1; /* for the trailing null */ for (i = 0; values[i] != NULL; i++) + { + if (values[i]->bv_len >= INT_MAX || + size > (INT_MAX - (values[i]->bv_len + 1))) + { + libpq_append_error(errorMessage, + "connection info string size exceeds the maximum allowed (%d)", + INT_MAX); + ldap_value_free_len(values); + ldap_unbind(ld); + return 3; + } + size += values[i]->bv_len + 1; + } + if ((result = malloc(size)) == NULL) { libpq_append_error(errorMessage, "out of memory"); diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index 2cf897cef04..e723bdf925c 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -532,7 +532,7 @@ PQsetvalue(PGresult *res, int tup_num, int field_num, char *value, int len) } else { - attval->value = (char *) pqResultAlloc(res, len + 1, true); + attval->value = (char *) pqResultAlloc(res, (size_t) len + 1, true); if (!attval->value) goto fail; attval->len = len; @@ -624,8 +624,13 @@ pqResultAlloc(PGresult *res, size_t nBytes, bool isBinary) */ if (nBytes >= PGRESULT_SEP_ALLOC_THRESHOLD) { - size_t alloc_size = nBytes + PGRESULT_BLOCK_OVERHEAD; + size_t alloc_size; + /* Don't wrap around with overly large requests. */ + if (nBytes > SIZE_MAX - PGRESULT_BLOCK_OVERHEAD) + return NULL; + + alloc_size = nBytes + PGRESULT_BLOCK_OVERHEAD; block = (PGresult_data *) malloc(alloc_size); if (!block) return NULL; @@ -1292,7 +1297,7 @@ pqRowProcessor(PGconn *conn, const char **errmsgp) bool isbinary = (res->attDescs[i].format != 0); char *val; - val = (char *) pqResultAlloc(res, clen + 1, isbinary); + val = (char *) pqResultAlloc(res, (size_t) clen + 1, isbinary); if (val == NULL) goto fail; @@ -4166,6 +4171,27 @@ PQescapeString(char *to, const char *from, size_t length) } +/* + * Frontend version of the backend's add_size(), intended to be API-compatible + * with the pg_add_*_overflow() helpers. Stores the result into *dst on success. + * Returns true instead if the addition overflows. + * + * TODO: move to common/int.h + */ +static bool +add_size_overflow(size_t s1, size_t s2, size_t *dst) +{ + size_t result; + + result = s1 + s2; + if (result < s1 || result < s2) + return true; + + *dst = result; + return false; +} + + /* * Escape arbitrary strings. If as_ident is true, we escape the result * as an identifier; if false, as a literal. The result is returned in @@ -4178,9 +4204,9 @@ PQescapeInternal(PGconn *conn, const char *str, size_t len, bool as_ident) const char *s; char *result; char *rp; - int num_quotes = 0; /* single or double, depending on as_ident */ - int num_backslashes = 0; - size_t input_len = strlen(str); + size_t num_quotes = 0; /* single or double, depending on as_ident */ + size_t num_backslashes = 0; + size_t input_len = strnlen(str, len); size_t result_size; char quote_char = as_ident ? '"' : '\''; bool validated_mb = false; @@ -4245,10 +4271,21 @@ PQescapeInternal(PGconn *conn, const char *str, size_t len, bool as_ident) } } - /* Allocate output buffer. */ - result_size = input_len + num_quotes + 3; /* two quotes, plus a NUL */ + /* + * Allocate output buffer. Protect against overflow, in case the caller + * has allocated a large fraction of the available size_t. + */ + if (add_size_overflow(input_len, num_quotes, &result_size) || + add_size_overflow(result_size, 3, &result_size)) /* two quotes plus a NUL */ + goto overflow; + if (!as_ident && num_backslashes > 0) - result_size += num_backslashes + 2; + { + if (add_size_overflow(result_size, num_backslashes, &result_size) || + add_size_overflow(result_size, 2, &result_size)) /* for " E" prefix */ + goto overflow; + } + result = rp = (char *) malloc(result_size); if (rp == NULL) { @@ -4321,6 +4358,12 @@ PQescapeInternal(PGconn *conn, const char *str, size_t len, bool as_ident) *rp = '\0'; return result; + +overflow: + libpq_append_conn_error(conn, + "escaped string size exceeds the maximum allowed (%zu)", + SIZE_MAX); + return NULL; } char * @@ -4386,16 +4429,25 @@ PQescapeByteaInternal(PGconn *conn, unsigned char *result; size_t i; size_t len; - size_t bslash_len = (std_strings ? 1 : 2); + const size_t bslash_len = (std_strings ? 1 : 2); /* - * empty string has 1 char ('\0') + * Calculate the escaped length, watching for overflow as we do with + * PQescapeInternal(). The following code relies on a small constant + * bslash_len so that small additions and multiplications don't need their + * own overflow checks. + * + * Start with the empty string, which has 1 char ('\0'). */ len = 1; if (use_hex) { - len += bslash_len + 1 + 2 * from_length; + /* We prepend "\x" and double each input character. */ + if (add_size_overflow(len, bslash_len + 1, &len) || + add_size_overflow(len, from_length, &len) || + add_size_overflow(len, from_length, &len)) + goto overflow; } else { @@ -4403,13 +4455,25 @@ PQescapeByteaInternal(PGconn *conn, for (i = from_length; i > 0; i--, vp++) { if (*vp < 0x20 || *vp > 0x7e) - len += bslash_len + 3; + { + if (add_size_overflow(len, bslash_len + 3, &len)) /* octal "\ooo" */ + goto overflow; + } else if (*vp == '\'') - len += 2; + { + if (add_size_overflow(len, 2, &len)) /* double each quote */ + goto overflow; + } else if (*vp == '\\') - len += bslash_len + bslash_len; + { + if (add_size_overflow(len, bslash_len * 2, &len)) /* double each backslash */ + goto overflow; + } else - len++; + { + if (add_size_overflow(len, 1, &len)) + goto overflow; + } } } @@ -4470,6 +4534,13 @@ PQescapeByteaInternal(PGconn *conn, *rp = '\0'; return result; + +overflow: + if (conn) + libpq_append_conn_error(conn, + "escaped bytea size exceeds the maximum allowed (%zu)", + SIZE_MAX); + return NULL; } unsigned char * diff --git a/src/interfaces/libpq/fe-print.c b/src/interfaces/libpq/fe-print.c index 40620b47e94..4b9dd7da087 100644 --- a/src/interfaces/libpq/fe-print.c +++ b/src/interfaces/libpq/fe-print.c @@ -107,6 +107,16 @@ PQprint(FILE *fout, const PGresult *res, const PQprintOpt *po) } screen_size; #endif + /* + * Quick sanity check on po->fieldSep, since we make heavy use of int + * math throughout. + */ + if (fs_len < strlen(po->fieldSep)) + { + fprintf(stderr, libpq_gettext("overlong field separator\n")); + goto exit; + } + nTups = PQntuples(res); fieldNames = (const char **) calloc(nFields, sizeof(char *)); fieldNotNum = (unsigned char *) calloc(nFields, 1); @@ -402,7 +412,7 @@ do_field(const PQprintOpt *po, const PGresult *res, { if (plen > fieldMax[j]) fieldMax[j] = plen; - if (!(fields[i * nFields + j] = (char *) malloc(plen + 1))) + if (!(fields[i * nFields + j] = (char *) malloc((size_t) plen + 1))) { fprintf(stderr, libpq_gettext("out of memory\n")); return false; @@ -452,6 +462,27 @@ do_field(const PQprintOpt *po, const PGresult *res, } +/* + * Frontend version of the backend's add_size(), intended to be API-compatible + * with the pg_add_*_overflow() helpers. Stores the result into *dst on success. + * Returns true instead if the addition overflows. + * + * TODO: move to common/int.h + */ +static bool +add_size_overflow(size_t s1, size_t s2, size_t *dst) +{ + size_t result; + + result = s1 + s2; + if (result < s1 || result < s2) + return true; + + *dst = result; + return false; +} + + static char * do_header(FILE *fout, const PQprintOpt *po, const int nFields, int *fieldMax, const char **fieldNames, unsigned char *fieldNotNum, @@ -464,15 +495,31 @@ do_header(FILE *fout, const PQprintOpt *po, const int nFields, int *fieldMax, fputs("", fout); else { - int tot = 0; + size_t tot = 0; int n = 0; char *p = NULL; + /* Calculate the border size, checking for overflow. */ for (; n < nFields; n++) - tot += fieldMax[n] + fs_len + (po->standard ? 2 : 0); + { + /* Field plus separator, plus 2 extra '-' in standard format. */ + if (add_size_overflow(tot, fieldMax[n], &tot) || + add_size_overflow(tot, fs_len, &tot) || + (po->standard && add_size_overflow(tot, 2, &tot))) + goto overflow; + } if (po->standard) - tot += fs_len * 2 + 2; - border = malloc(tot + 1); + { + /* An extra separator at the front and back. */ + if (add_size_overflow(tot, fs_len, &tot) || + add_size_overflow(tot, fs_len, &tot) || + add_size_overflow(tot, 2, &tot)) + goto overflow; + } + if (add_size_overflow(tot, 1, &tot)) /* terminator */ + goto overflow; + + border = malloc(tot); if (!border) { fprintf(stderr, libpq_gettext("out of memory\n")); @@ -535,6 +582,10 @@ do_header(FILE *fout, const PQprintOpt *po, const int nFields, int *fieldMax, else fprintf(fout, "\n%s\n", border); return border; + +overflow: + fprintf(stderr, libpq_gettext("header size exceeds the maximum allowed\n")); + return NULL; } diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index dbb64161a33..82d0516e138 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -25,6 +25,7 @@ #include #include +#include #ifdef WIN32 #include "win32.h" @@ -64,8 +65,8 @@ static int getReadyForQuery(PGconn *conn); static void saveCdbStatMsg(PGresult *result, char *data, int len); static void reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding); -static int build_startup_packet(const PGconn *conn, char *packet, - const PQEnvironmentOption *options); +static size_t build_startup_packet(const PGconn *conn, char *packet, + const PQEnvironmentOption *options); /* @@ -1385,8 +1386,21 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding) * scridx[] respectively. */ - /* we need a safe allocation size... */ + /* + * We need a safe allocation size. + * + * The only caller of reportErrorPosition() is pqBuildErrorMessage3(); it + * gets its query from either a PQresultErrorField() or a PGcmdQueueEntry, + * both of which must have fit into conn->inBuffer/outBuffer. So slen fits + * inside an int, but we can't assume that (slen * sizeof(int)) fits + * inside a size_t. + */ slen = strlen(wquery) + 1; + if (slen > SIZE_MAX / sizeof(int)) + { + free(wquery); + return; + } qidx = (int *) malloc(slen * sizeof(int)); if (qidx == NULL) @@ -2443,15 +2457,43 @@ pqBuildStartupPacket3(PGconn *conn, int *packetlen, const PQEnvironmentOption *options) { char *startpacket; + size_t len; + + len = build_startup_packet(conn, NULL, options); + if (len == 0 || len > INT_MAX) + return NULL; - *packetlen = build_startup_packet(conn, NULL, options); + *packetlen = len; startpacket = (char *) malloc(*packetlen); if (!startpacket) return NULL; - *packetlen = build_startup_packet(conn, startpacket, options); + + len = build_startup_packet(conn, startpacket, options); + Assert(*packetlen == len); + return startpacket; } +/* + * Frontend version of the backend's add_size(), intended to be API-compatible + * with the pg_add_*_overflow() helpers. Stores the result into *dst on success. + * Returns true instead if the addition overflows. + * + * TODO: move to common/int.h + */ +static bool +add_size_overflow(size_t s1, size_t s2, size_t *dst) +{ + size_t result; + + result = s1 + s2; + if (result < s1 || result < s2) + return true; + + *dst = result; + return false; +} + /* * Build a startup packet given a filled-in PGconn structure. * @@ -2459,13 +2501,13 @@ pqBuildStartupPacket3(PGconn *conn, int *packetlen, * To avoid duplicate logic, this routine is called twice: the first time * (with packet == NULL) just counts the space needed, the second time * (with packet == allocated space) fills it in. Return value is the number - * of bytes used. + * of bytes used, or zero in the unlikely event of size_t overflow. */ -static int +static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options) { - int packet_len = 0; + size_t packet_len = 0; const PQEnvironmentOption *next_eo; const char *val; @@ -2484,10 +2526,12 @@ build_startup_packet(const PGconn *conn, char *packet, do { \ if (packet) \ strcpy(packet + packet_len, optname); \ - packet_len += strlen(optname) + 1; \ + if (add_size_overflow(packet_len, strlen(optname) + 1, &packet_len)) \ + return 0; \ if (packet) \ strcpy(packet + packet_len, optval); \ - packet_len += strlen(optval) + 1; \ + if (add_size_overflow(packet_len, strlen(optval) + 1, &packet_len)) \ + return 0; \ } while(0) if (conn->pguser && conn->pguser[0]) @@ -2535,7 +2579,8 @@ build_startup_packet(const PGconn *conn, char *packet, /* Add trailing terminator */ if (packet) packet[packet_len] = '\0'; - packet_len++; + if (add_size_overflow(packet_len, 1, &packet_len)) + return 0; return packet_len; } diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index fabf6bab94d..908cc5da0b9 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -558,7 +558,16 @@ struct pg_conn pg_prng_state prng_state; /* prng state for load balancing connections */ - /* Buffer for data received from backend and not yet processed */ + /* + * Buffer for data received from backend and not yet processed. + * + * NB: We rely on a maximum inBufSize/outBufSize of INT_MAX (and therefore + * an INT_MAX upper bound on the size of any and all packet contents) to + * avoid overflow; for example in reportErrorPosition(). Changing the type + * would require not only an adjustment to the overflow protection in + * pqCheck{In,Out}BufferSpace(), but also a careful audit of all libpq + * code that uses ints during size calculations. + */ char *inBuffer; /* currently allocated buffer */ int inBufSize; /* allocated size of buffer */ int inStart; /* offset to first unconsumed data in buffer */ From 326b3cee72dc3b1a22c6e91e89b2c83af33e8014 Mon Sep 17 00:00:00 2001 From: reshke Date: Sun, 31 May 2026 16:01:26 +0000 Subject: [PATCH 3/3] Adapt CBDB regression test suite for 07ffe0783 --- .../regress/expected/stats_ext_optimizer.out | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/test/regress/expected/stats_ext_optimizer.out b/src/test/regress/expected/stats_ext_optimizer.out index 97513665ae1..cf187147fb9 100644 --- a/src/test/regress/expected/stats_ext_optimizer.out +++ b/src/test/regress/expected/stats_ext_optimizer.out @@ -3371,6 +3371,40 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x s_expr | {1} (2 rows) +-- CREATE STATISTICS checks for CREATE on the schema +RESET SESSION AUTHORIZATION; +CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT); +CREATE SCHEMA sts_sch2; +GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch1 +CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch2 +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch2 +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1; +GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch1 +CREATE STATISTICS sts_sch2.pass1 ON a, b FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.pass2 ON a, b FROM sts_sch1.tbl; +-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1, sts_sch2 FROM regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT; -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); @@ -3380,6 +3414,8 @@ DROP SCHEMA tststats CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to table tststats.priv_test_tbl drop cascades to view tststats.priv_test_view +DROP SCHEMA sts_sch1, sts_sch2 CASCADE; +NOTICE: drop cascades to table sts_sch1.tbl DROP USER regress_stats_user1; -- test analyze with extended statistics CREATE TABLE tbl_issue1293 (col1 int, col2 int);