BUG #19865: WAL summary reader wraps block numbers for oversized chunk counts

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: imchifan(at)mail(dot)com
Subject: BUG #19865: WAL summary reader wraps block numbers for oversized chunk counts
Date: 2026-11-11 05:36:09
Message-ID: 19865-b30f75a4530db87d@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: 19865
Logged by: Faye
Email address: imchifan(at)mail(dot)com
PostgreSQL version: Unsupported/Unknown
Operating system: Linux/amd64
Description:

pg_wal_summary_contents() accepts a checksum-valid WAL summary containing
65,537 chunks. When chunks 0 and 65,536 both contain offset 7, it returns
block 7 twice. The second entry mathematically represents block 2^32+7,
which is outside the 32-bit BlockNumber domain, but the reader wraps it to
7.

The reader should reject this summary because its declared chunk domain
represents block numbers that BlockNumber cannot express. It should not
return aliased block references.

How to repeat:

```python
#!/usr/bin/env python3
import shlex
import struct
import subprocess

SUMMARY_NAME = "00000001FFFFFFF000000000FFFFFFF000000001.summary"
TABLE_NAME = "wal_summary_test"

def crc32c(data: bytes) -> int:
crc = 0xFFFFFFFF
for byte in data:
crc ^= byte
for _ in range(8):
crc = (crc >> 1) ^ (0x82F63B78 if crc & 1 else 0)
return crc ^ 0xFFFFFFFF

def make_payload() -> bytes:
# One relation entry with chunks 0 and 65536 both encoding offset 7.
# The latter block number is 2^32 + 7 and wraps to 7 in the reader.
body = bytearray(struct.pack("<I", 0x652B137B))
body += struct.pack("<IIIIII", 1663, 1, 12345, 0, 0xFFFFFFFF, 65537)
sizes = [0] * 65537
sizes[0] = 1
sizes[-1] = 1
body += struct.pack("<" + "H" * len(sizes), *sizes)
body += struct.pack("<HH", 7, 7)
body += bytes(24) # all-zero sentinel entry
body += struct.pack("<I", crc32c(body))
return bytes(body)

def run_sql(sql: str, *options: str) -> str:
result = subprocess.run(
["psql", "-X", "-v", "ON_ERROR_STOP=1", *options],
input=sql,
text=True,
capture_output=True,
check=True,
)
return result.stdout

payload_hex = make_payload().hex()
setup = f"""
CREATE TABLE {TABLE_NAME}(lo oid);
INSERT INTO {TABLE_NAME}
SELECT lo_from_bytea(0, decode('{payload_hex}', 'hex'));
SELECT lo_export(lo, current_setting('data_directory') ||
'/pg_wal/summaries/{SUMMARY_NAME}')
FROM {TABLE_NAME};
"""
test = """
SELECT count(*) AS rows_returned,
count(DISTINCT relblocknumber) AS distinct_blocks,
min(relblocknumber) AS min_block,
max(relblocknumber) AS max_block
FROM pg_wal_summary_contents(1, 'FFFFFFF0/00000000', 'FFFFFFF0/00000001')
WHERE relfilenode = 12345 AND NOT is_limit_block;
"""

run_sql(setup)
try:
print(run_sql(test, "-A", "-t"), end="")
finally:
data_directory = run_sql(
"SHOW data_directory;", "-A", "-t"
).strip()
summary_path = f"{data_directory}/pg_wal/summaries/{SUMMARY_NAME}"
remove_command = "rm -f " + shlex.quote(summary_path)
cleanup = f"""
COPY (SELECT 1) TO PROGRAM '{remove_command.replace("'", "''")}';
SELECT lo_unlink(lo) FROM {TABLE_NAME};
DROP TABLE {TABLE_NAME};
"""
run_sql(cleanup)
```

Run it as a role permitted to call lo_export(), COPY TO PROGRAM, and
pg_wal_summary_contents():

```
python3 reproducer.py
```

I get:

```
2|1|7|7
```

The query therefore reports two rows but only one distinct block number;
both the minimum and maximum are 7. The serialized entries represent blocks
7 and 2^32+7, so the latter has been wrapped rather than rejected.

The issue was reproduced on PostgreSQL 20devel on Linux/amd64.
BlockRefTableReaderNextRelation() limits nchunks by allocation size, while
BlockRefTableReaderGetBlocks() computes chunkno * 65536 + chunkoffset in the
32-bit BlockNumber domain. This occurs when reading a malformed,
checksum-valid WAL summary whose declared chunk domain extends beyond the
range representable by BlockNumber.

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-11-11 05:39:16 BUG #19866: acldefault() emits invalid UTF-8 for a high-bit "char" value
Previous Message PG Bug reporting form 2026-11-11 05:33:01 BUG #19864: pg_get_publication_tables() gets cache lookup error after concurrent DROP PUBLICATION