BUG #19528: Assert failure in generate_normalized_query() via Squashed Array Literals
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
This thread has been committed, so CI has stopped here. Anything below is the last result it produced.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t248592psql -h localhost -U postgresBuilt from patchset v2 (message #2), August 10, 2026 at 06:47 AM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t248592_2 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t248592_2 && git checkout t248592_2Patchset v2 (message #2) is on t248592_2
The following bug has been logged on the website:
Bug reference: 19528
Logged by: Yuelin Wang
Email address: 3020001251@tju.edu.cn
PostgreSQL version: 19beta1
Operating system: Linux (Ubuntu 24.04, x86_64)
Description:
The buffer is allocated at line 2841 with a per-location budget of 10 bytes:
```c
norm_query_buflen = query_len + jstate->clocations_count * 10;
```
The comment explains the budget: a `$n` placeholder is at most 11 bytes and
the original constant is at least 1 byte, so net expansion is at most 10
bytes per location. This holds for non-squashed constants.
For squashed array elements, the sprintf at line 2883 writes `"$N /*, ...
*/"` instead of just `"$N"`:
```c
n_quer_loc += sprintf(norm_query + n_quer_loc, "$%d%s",
num_constants_replaced + 1 +
jstate->highest_extern_param_id,
locs[i].squashed ? " /*, ... */" : "");
```
`" /*, ... */"` is 11 bytes, so a squashed entry writes `"$N"` (2 bytes)
plus `" /*, ... */"` (11 bytes) = 13 bytes to replace a 1-byte constant, a
net expansion of 12 bytes. The budget of 10 bytes is exceeded by 2 bytes per
squashed location. With N squashed array elements the buffer overflows by 2N
bytes.
Any `ARRAY[a,b]` literal records its second element as a squashed
`clocations` entry. Ten such arrays produce a 20-byte overflow, which is
sufficient to trigger the Assert at line 2908:
```
TRAP: failed Assert("n_quer_loc <= norm_query_buflen"),
File: "pg_stat_statements.c", Line: 2908
```
In a Release build (assertions disabled) the overflow is a true heap write
past the `palloc` allocation, corrupting adjacent allocator metadata.
### Reproduction
`pg_stat_statements` must be listed in `shared_preload_libraries`. The
triggering role holds no special privileges beyond `LOGIN`.
```sql
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE ROLE vuln_004_lowpriv LOGIN PASSWORD 'vuln004';
GRANT pg_read_all_stats TO vuln_004_lowpriv;
SET ROLE vuln_004_lowpriv;
SELECT ARRAY[1,2], ARRAY[1,2], ARRAY[1,2], ARRAY[1,2], ARRAY[1,2],
ARRAY[1,2], ARRAY[1,2], ARRAY[1,2], ARRAY[1,2], ARRAY[1,2];
```
### Observed Output
psql output:
```
SET
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost
```
Server log (PostgreSQL 19beta1, cassert build, 2026-06-19):
```
TRAP: failed Assert("n_quer_loc <= norm_query_buflen"), File:
"pg_stat_statements.c", Line: 2908, PID: 2671635
postgres: yuelinwang postgres [local]
SELECT(ExceptionalCondition+0x103)[0x5602a92f55b8]
/data/.../pg_stat_statements.so(+0x655b)[0x7d147957e55b]
/data/.../pg_stat_statements.so(+0x9ef7)[0x7d1479581ef7]
/data/.../pg_stat_statements.so(+0xb3ba)[0x7d14795833ba]
postgres: yuelinwang postgres [local]
SELECT(parse_analyze_fixedparams+0x120)[...]
postgres: yuelinwang postgres [local] SELECT(PostgresMain+0x1142)[...]
LOG: client backend (PID 2671635) was terminated by signal 6: Aborted
DETAIL: Failed process was running: SELECT ARRAY[1,2], ARRAY[1,2],
ARRAY[1,2], ARRAY[1,2], ARRAY[1,2],
ARRAY[1,2], ARRAY[1,2], ARRAY[1,2], ARRAY[1,2], ARRAY[1,2];
LOG: terminating any other active server processes
LOG: all server processes terminated; reinitializing
LOG: database system was interrupted; last known up at 2026-06-19 12:34:57
+08
LOG: database system was not properly shut down; automatic recovery in
progress
LOG: database system is ready to accept connections
```
### Expected vs Actual
| Step | Expected | Actual |
|---|---|---|
| SELECT with 10 ARRAY[1,2] literals | returns one row of 10 arrays |
backend crashes (SIGABRT signal 6) |
| Server log | nothing | `TRAP: failed Assert("n_quer_loc <=
norm_query_buflen"), File: "pg_stat_statements.c", Line: 2908` |
| Server state | unaffected | crash recovery triggered, postmaster
reinitializes |
| Connection | stays open | `server closed the connection unexpectedly` |
### Fix
Account for the extra 11 bytes appended when a squashed suffix is written.
One correct approach:
```c
norm_query_buflen = query_len + jstate->clocations_count *
(10 + (jstate->has_squashed_lists ? 11 : 0));
```
A simpler conservative fix that covers the worst case (`"$2147483648 /*, ...
*/"` = 22 bytes replacing a 1-byte constant, net 21 bytes):
```c
norm_query_buflen = query_len + jstate->clocations_count * 22;
```
On Sat, Jun 20, 2026 at 7:53 PM PG Bug reporting form
<noreply@postgresql.org> wrote:
`" /*, ... */"` is 11 bytes, so a squashed entry writes `"$N"` (2 bytes)
plus `" /*, ... */"` (11 bytes) = 13 bytes to replace a 1-byte constant, a
net expansion of 12 bytes. The budget of 10 bytes is exceeded by 2 bytes per
squashed location. With N squashed array elements the buffer overflows by 2N
bytes.
This analysis doesn't seem correct to me.
For "ARRAY[1,2]", the recorded location covers the entire list text,
"1,2" (3 bytes), which is replaced with "$n /*, ... */". For a
single-digit placeholder, "$1 /*, ... */" is 13 bytes, so replacing
"1,2" results in a net growth of 10 bytes.
That still fits within the existing budget. The budget is first
exceeded when the placeholder number reaches two digits:
"$10 /*, ... */" is 14 bytes, giving a net growth of 11 bytes.
Therefore, the smallest reproducer is 10 squashed "ARRAY[1,2]"
entries, where the shortfall seems only 1 byte overall, not 2 bytes per
squashed location.
### Fix
Account for the extra 11 bytes appended when a squashed suffix is written.
One correct approach:```c
norm_query_buflen = query_len + jstate->clocations_count *
(10 + (jstate->has_squashed_lists ? 11 : 0));
```A simpler conservative fix that covers the worst case (`"$2147483648 /*, ...
*/"` = 22 bytes replacing a 1-byte constant, net 21 bytes):```c
norm_query_buflen = query_len + jstate->clocations_count * 22;
```
How about keeping the current "clocations_count * 10" budget for
ordinary replacements, counting squashed entries separately, and
adding only the extra space they may need, for example:
clocations_count * 10 + squashed_count * 9
The existing "+10" already covers ordinary placeholders. For squashed
entries, the worst-case additional overhead is 9 bytes, from replacing
the shortest possible source text "1,2" (3 bytes) with the longest
possible placeholder "$2147483647 /*, ... */" (22 bytes), for a total
growth of 19 bytes. Attached patch implements this approach.
Thoughts?
Regards,
--
Fujii Masao
The analysis looks correct to me; clocations_count * 10 + squashed_count * 9 tightly covers the worst case and is cleaner than the alternatives.
王跃林
3020001251@tju.edu.cn
Original:
From:Fujii Masao <masao.fujii@gmail.com>Date:2026-06-20 23:18:19(中国 (GMT+08:00))To:3020001251<3020001251@tju.edu.cn> , pgsql-bugs<pgsql-bugs@lists.postgresql.org>Cc:Subject:Re: BUG #19528: Assert failure in generate_normalized_query() via Squashed Array LiteralsOn Sat, Jun 20, 2026 at 7:53 PM PG Bug reporting form
<noreply@postgresql.org> wrote:
`" /*, ... */"` is 11 bytes, so a squashed entry writes `"$N"` (2 bytes)
plus `" /*, ... */"` (11 bytes) = 13 bytes to replace a 1-byte constant, a
net expansion of 12 bytes. The budget of 10 bytes is exceeded by 2 bytes per
squashed location. With N squashed array elements the buffer overflows by 2N
bytes.
This analysis doesn't seem correct to me.
For "ARRAY[1,2]", the recorded location covers the entire list text,
"1,2" (3 bytes), which is replaced with "$n /*, ... */". For a
single-digit placeholder, "$1 /*, ... */" is 13 bytes, so replacing
"1,2" results in a net growth of 10 bytes.
That still fits within the existing budget. The budget is first
exceeded when the placeholder number reaches two digits:
"$10 /*, ... */" is 14 bytes, giving a net growth of 11 bytes.
Therefore, the smallest reproducer is 10 squashed "ARRAY[1,2]"
entries, where the shortfall seems only 1 byte overall, not 2 bytes per
squashed location.
### Fix
Account for the extra 11 bytes appended when a squashed suffix is written.
One correct approach:```c
norm_query_buflen = query_len + jstate->clocations_count *
(10 + (jstate->has_squashed_lists ? 11 : 0));
```A simpler conservative fix that covers the worst case (`"$2147483648 /*, ...
*/"` = 22 bytes replacing a 1-byte constant, net 21 bytes):```c
norm_query_buflen = query_len + jstate->clocations_count * 22;
```
How about keeping the current "clocations_count * 10" budget for
ordinary replacements, counting squashed entries separately, and
adding only the extra space they may need, for example:
clocations_count * 10 + squashed_count * 9
The existing "+10" already covers ordinary placeholders. For squashed
entries, the worst-case additional overhead is 9 bytes, from replacing
the shortest possible source text "1,2" (3 bytes) with the longest
possible placeholder "$2147483647 /*, ... */" (22 bytes), for a total
growth of 19 bytes. Attached patch implements this approach.
Thoughts?
Regards,
--
Fujii Masao