postgres.git / summary / log / commit / refs
commit 18992dc9d98022a43831da8e1868abf3ce5cd66a
Author: Michael Paquier <michael@paquier.xyz>
Date: Tue Aug 04 21:53:10 2026 +0000
Improving handling of oversized records in xlogreader.c
XLogRecordAssemble() refuses records larger than XLogRecordMaxSize when
generating records, but the reader-side only checked a minimal number
for xl_tot_len.
A crafted multi-page record with xl_tot_len near UINT32_MAX could bypass
the contrecord length checks, overflow allocate_recordbuf()'s size math,
and corrupt memory during reassembly (or hit related asserts under
cassert).
xlogreader.c is updated to check that the received record is never
larger than XLogRecordMaxSize, when first receiving the first bytes of
xl_tot_len, then again when validating the record header.
WAL is a source of data trusted by the server, with CRC32 checksums
providing a level of protection before replaying the records if data is
corrupted. These limits could be internally reached only with crafted
WAL records, which is something that analyzers (named as LLMs) like
complaining about these days.
039_end_of_wal.pl is adjusted according to the new maximum limit, a test
for XLogRecordMaxSize is added.
Author: David Karapetyan <dkarapetyan@gmail.com>
Author: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CALmTjZBddzeGVU2o1JNZb3mPvV68PqDskWPAUQBVb-35yVkiwA@mail.gmail.com
src/backend/access/transam/xlogreader.c | 33 +++++++++++++++++++++++++++++++--
src/test/recovery/t/039_end_of_wal.pl | 33 +++++++++++++++++++++++----------
2 files changed, 54 insertions(+), 12 deletions(-)
diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c
index 946907a2507..4bfe8607f4e 100644
--- a/src/backend/access/transam/xlogreader.c
+++ b/src/backend/access/transam/xlogreader.c
@@ -185,15 +185,20 @@ XLogReaderFree(XLogReaderState *state)
* with. (That is enough for all "normal" records, but very large commit or
* abort records might need more space.)
*
+ * The caller must make sure that "reclength" is valid and within the
+ * XLogRecordMaxSize limit.
+ *
* Note: This routine should *never* be called for xl_tot_len until the header
* of the record has been fully validated.
*/
static void
allocate_recordbuf(XLogReaderState *state, uint32 reclength)
{
- uint32 newSize = reclength;
+ uint32 newSize;
+
+ Assert(reclength <= XLogRecordMaxSize);
- newSize += XLOG_BLCKSZ - (newSize % XLOG_BLCKSZ);
+ newSize = TYPEALIGN(XLOG_BLCKSZ, reclength);
newSize = Max(newSize, 5 * Max(BLCKSZ, XLOG_BLCKSZ));
if (state->readRecordBuf)
@@ -673,6 +678,21 @@ restart:
(uint32) SizeOfXLogRecord, total_len);
goto err;
}
+
+ /*
+ * If the record length exceeds the maximum allowed size, don't try to
+ * reconstruct it. The backend enforces the same limit in
+ * XLogRecordAssemble().
+ */
+ if (total_len > XLogRecordMaxSize)
+ {
+ report_invalid_record(state,
+ "invalid record length at %X/%08X: expected at most %u, got %u",
+ LSN_FORMAT_ARGS(RecPtr),
+ XLogRecordMaxSize, total_len);
+ goto err;
+ }
+
/* We'll validate the header once we have the next page. */
gotheader = false;
}
@@ -1148,6 +1168,15 @@ ValidXLogRecordHeader(XLogReaderState *state, XLogRecPtr RecPtr,
(uint32) SizeOfXLogRecord, record->xl_tot_len);
return false;
}
+
+ if (record->xl_tot_len > XLogRecordMaxSize)
+ {
+ report_invalid_record(state,
+ "invalid record length at %X/%08X: expected at most %u, got %u",
+ LSN_FORMAT_ARGS(RecPtr),
+ XLogRecordMaxSize, record->xl_tot_len);
+ return false;
+ }
if (!RmgrIdIsValid(record->xl_rmid))
{
report_invalid_record(state,
diff --git a/src/test/recovery/t/039_end_of_wal.pl b/src/test/recovery/t/039_end_of_wal.pl
index f46d089a0fb..5fe8835e094 100644
--- a/src/test/recovery/t/039_end_of_wal.pl
+++ b/src/test/recovery/t/039_end_of_wal.pl
@@ -163,6 +163,19 @@ ok( $node->log_contains(
$log_size),
"xl_tot_len short");
+# xl_tot_len is > XLogRecordMaxSize (presumably recycled garbage).
+$node->emit_wal(0);
+$end_lsn = $node->advance_wal_out_of_record_splitting_zone($WAL_BLOCK_SIZE);
+$node->stop('immediate');
+$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
+ build_record_header(1024 * 1024 * 1024));
+$log_size = -s $node->logfile;
+$node->start;
+ok( $node->log_contains(
+ "invalid record length at .*: expected at most 1069547520, got 1073741824",
+ $log_size),
+ "xl_tot_len short");
+
# xl_tot_len in final position, not big enough to span into a new page but
# also not eligible for regular record header validation
$node->emit_wal(0);
@@ -181,7 +194,7 @@ $node->emit_wal(0);
$end_lsn = $node->advance_wal_out_of_record_splitting_zone($WAL_BLOCK_SIZE);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 0, 0xdeadbeef));
+ build_record_header(2 * 1024 * 1024, 0, 0xdeadbeef));
$log_size = -s $node->logfile;
$node->start;
ok( $node->log_contains(
@@ -216,7 +229,7 @@ $prev_lsn = $node->advance_wal_out_of_record_splitting_zone($WAL_BLOCK_SIZE);
$end_lsn = $node->emit_wal(0);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 0, $prev_lsn));
+ build_record_header(2 * 1024 * 1024, 0, $prev_lsn));
$log_size = -s $node->logfile;
$node->start;
ok($node->log_contains("invalid magic number 0000 .* LSN .*", $log_size),
@@ -228,7 +241,7 @@ $prev_lsn = $node->advance_wal_out_of_record_splitting_zone($WAL_BLOCK_SIZE);
$end_lsn = $node->emit_wal(0);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 0, $prev_lsn));
+ build_record_header(2 * 1024 * 1024, 0, $prev_lsn));
$node->write_wal($TLI, start_of_next_page($end_lsn),
$WAL_SEGMENT_SIZE, build_page_header(0xcafe, 0, 1, 0));
$log_size = -s $node->logfile;
@@ -243,7 +256,7 @@ $prev_lsn = $node->advance_wal_out_of_record_splitting_zone($WAL_BLOCK_SIZE);
$end_lsn = $node->emit_wal(0);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 0, $prev_lsn));
+ build_record_header(2 * 1024 * 1024, 0, $prev_lsn));
$node->write_wal($TLI, start_of_next_page($end_lsn),
$WAL_SEGMENT_SIZE, build_page_header($XLP_PAGE_MAGIC, 0, 1, 0xbaaaaaad));
$log_size = -s $node->logfile;
@@ -258,7 +271,7 @@ $prev_lsn = $node->advance_wal_out_of_record_splitting_zone($WAL_BLOCK_SIZE);
$end_lsn = $node->emit_wal(0);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 42, $prev_lsn));
+ build_record_header(2 * 1024 * 1024, 42, $prev_lsn));
$node->write_wal(
$TLI,
start_of_next_page($end_lsn),
@@ -277,7 +290,7 @@ $prev_lsn = $node->advance_wal_out_of_record_splitting_zone($WAL_BLOCK_SIZE);
$end_lsn = $node->emit_wal(0);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 42, $prev_lsn));
+ build_record_header(2 * 1024 * 1024, 42, $prev_lsn));
$node->write_wal($TLI, start_of_next_page($end_lsn),
$WAL_SEGMENT_SIZE,
build_page_header($XLP_PAGE_MAGIC, 0, 1, start_of_next_page($end_lsn)));
@@ -293,7 +306,7 @@ $prev_lsn = $node->advance_wal_out_of_record_splitting_zone($WAL_BLOCK_SIZE);
$end_lsn = $node->emit_wal(0);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 42, $prev_lsn));
+ build_record_header(2 * 1024 * 1024, 42, $prev_lsn));
$node->write_wal(
$TLI,
start_of_next_page($end_lsn),
@@ -318,7 +331,7 @@ $node->emit_wal(0);
$end_lsn = $node->advance_wal_to_record_splitting_zone($WAL_BLOCK_SIZE);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 0, 0xdeadbeef));
+ build_record_header(2 * 1024 * 1024, 0, 0xdeadbeef));
$log_size = -s $node->logfile;
$node->start;
ok($node->log_contains("invalid magic number 0000 .* LSN .*", $log_size),
@@ -329,7 +342,7 @@ $node->emit_wal(0);
$end_lsn = $node->advance_wal_to_record_splitting_zone($WAL_BLOCK_SIZE);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 0, 0xdeadbeef));
+ build_record_header(2 * 1024 * 1024, 0, 0xdeadbeef));
$node->write_wal(
$TLI,
start_of_next_page($end_lsn),
@@ -348,7 +361,7 @@ $node->emit_wal(0);
$end_lsn = $node->advance_wal_to_record_splitting_zone($WAL_BLOCK_SIZE);
$node->stop('immediate');
$node->write_wal($TLI, $end_lsn, $WAL_SEGMENT_SIZE,
- build_record_header(2 * 1024 * 1024 * 1024, 0, 0xdeadbeef));
+ build_record_header(2 * 1024 * 1024, 0, 0xdeadbeef));
$node->write_wal(
$TLI,
start_of_next_page($end_lsn),
[parent: 3f2fdd150eb0]