From bb02eba534112594bc44a4b4c4fe9acb7ac7d656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Mon, 10 Aug 2026 06:38:04 -0700 Subject: [PATCH] pg_stat_statements: Fix buffer overflow with query normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since commit 62d712ecfd94, pg_stat_statements has been underestimating the size of the result buffer possible for a normalized query, in cases where the query includes many squashable lists, causing the normalized query to write past the allocated area. The allocated buffer size forgot to account for the comment appended in a squashable list, "/*, ... */". Instead of trying to track down precisely how much space we need, fix by switch to using an expansible StringInfo. This not only fixes the bug, but it also makes the code simpler to follow. Author: Álvaro Herrera Reported-by: Sajeeb Lohani with TrendAI Zero Day Initiative Reported-by: Yuelin Wang <3020001251@tju.edu.cn> Diagnosed-by: Michaël Paquier Backpatch-through: 18 Security: CVE-2026-14676 Discussion: https://postgr.es/m/19528-7290dd7e6f7dcc22@postgresql.org --- .../pg_stat_statements/pg_stat_statements.c | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c index 92315627916..d1d8dd34f25 100644 --- a/contrib/pg_stat_statements/pg_stat_statements.c +++ b/contrib/pg_stat_statements/pg_stat_statements.c @@ -2813,17 +2813,22 @@ static char * generate_normalized_query(const JumbleState *jstate, const char *query, int query_loc, int *query_len_p) { - char *norm_query; + StringInfoData norm_query; int query_len = *query_len_p; - int norm_query_buflen, /* Space allowed for norm_query */ - len_to_wrt, /* Length (in bytes) to write */ + int len_to_wrt, /* Length (in bytes) to write */ quer_loc = 0, /* Source query byte location */ - n_quer_loc = 0, /* Normalized query byte location */ last_off = 0, /* Offset from start for previous tok */ last_tok_len = 0; /* Length (in bytes) of that tok */ int num_constants_replaced = 0; LocationLen *locs = NULL; + /* + * Our output buffer is an expansible StringInfo, but avoid enlarging it + * in most cases by reserving extra space for each constant location. + */ + Assert(jstate->clocations_count > 0); + initStringInfoExt(&norm_query, query_len + jstate->clocations_count * 10); + /* * Determine constants' lengths (core system only gives us locations), and * return a sorted copy of jstate's LocationLen data with lengths filled @@ -2831,18 +2836,6 @@ generate_normalized_query(const JumbleState *jstate, const char *query, */ locs = ComputeConstantLengths(jstate, query, query_loc); - /* - * Allow for $n symbols to be longer than the constants they replace. - * Constants must take at least one byte in text form, while a $n symbol - * certainly isn't more than 11 bytes, even if n reaches INT_MAX. We - * could refine that limit based on the max value of n for the current - * query, but it hardly seems worth any extra effort to do so. - */ - norm_query_buflen = query_len + jstate->clocations_count * 10; - - /* Allocate result buffer */ - norm_query = palloc(norm_query_buflen + 1); - for (int i = 0; i < jstate->clocations_count; i++) { int off, /* Offset from start for cur tok */ @@ -2872,17 +2865,16 @@ generate_normalized_query(const JumbleState *jstate, const char *query, len_to_wrt = off - last_off; len_to_wrt -= last_tok_len; Assert(len_to_wrt >= 0); - memcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt); - n_quer_loc += len_to_wrt; + appendBinaryStringInfo(&norm_query, query + quer_loc, len_to_wrt); /* * And insert a param symbol in place of the constant token; and, if * we have a squashable list, insert a placeholder comment starting * from the list's second value. */ - n_quer_loc += sprintf(norm_query + n_quer_loc, "$%d%s", - num_constants_replaced + 1 + jstate->highest_extern_param_id, - locs[i].squashed ? " /*, ... */" : ""); + appendStringInfo(&norm_query, "$%d%s", + num_constants_replaced + 1 + jstate->highest_extern_param_id, + locs[i].squashed ? " /*, ... */" : ""); num_constants_replaced++; /* move forward */ @@ -2902,12 +2894,8 @@ generate_normalized_query(const JumbleState *jstate, const char *query, len_to_wrt = query_len - quer_loc; Assert(len_to_wrt >= 0); - memcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt); - n_quer_loc += len_to_wrt; + appendBinaryStringInfo(&norm_query, query + quer_loc, len_to_wrt); - Assert(n_quer_loc <= norm_query_buflen); - norm_query[n_quer_loc] = '\0'; - - *query_len_p = n_quer_loc; - return norm_query; + *query_len_p = norm_query.len; + return norm_query.data; }