From 2bc2beca916c2d978f86962c66a4cfc3ef80cc02 Mon Sep 17 00:00:00 2001 From: David Karapetyan Date: Fri, 31 Jul 2026 11:17:34 +0200 Subject: [PATCH v4] Fix XLogReader mishandling of oversized multi-page records. XLogRecordAssemble() refuses records larger than XLogRecordMaxSize, but the reader only checked a minimum xl_tot_len. A crafted multi-page record with xl_tot_len near UINT32_MAX could pass contrecord length checks, overflow allocate_recordbuf()'s size math, and corrupt memory during reassembly (or hit related asserts under cassert). This patch avoids allocating memory for split-header records if the size of the record is definitely too large, avoiding "allocation too large" and size overflow -related issues. The 039_end_of_wal.pl test is adjusted with the new limits in mind, and a test case for too-large reclength has been added. Co-authored-by: Matthias van de Meent --- src/backend/access/transam/xlogreader.c | 42 +++++++++++++++++++++++-- src/test/recovery/t/039_end_of_wal.pl | 32 +++++++++++++------ 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 946907a2507..3064734076f 100644 --- a/src/backend/access/transam/xlogreader.c +++ b/src/backend/access/transam/xlogreader.c @@ -191,9 +191,16 @@ XLogReaderFree(XLogReaderState *state) static void allocate_recordbuf(XLogReaderState *state, uint32 reclength) { - uint32 newSize = reclength; + uint32 newSize; - newSize += XLOG_BLCKSZ - (newSize % XLOG_BLCKSZ); + /* + * The caller must have made sure reclength is valid and within the + * XLogRecordMaxSize limit; this guarantees that our calculations cannot + * overflow or otherwise exceed memory system limits. + */ + Assert(reclength <= XLogRecordMaxSize); + + newSize = TYPEALIGN(XLOG_BLCKSZ, reclength); newSize = Max(newSize, 5 * Max(BLCKSZ, XLOG_BLCKSZ)); if (state->readRecordBuf) @@ -673,6 +680,21 @@ restart: (uint32) SizeOfXLogRecord, total_len); goto err; } + + /* + * If it's too large we shouldn't try to reconstruct the corrupted + * record; it could cause all kinds of issues through overflow, + * oversized allocations, and OOMs. + */ + 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 +1170,22 @@ ValidXLogRecordHeader(XLogReaderState *state, XLogRecPtr RecPtr, (uint32) SizeOfXLogRecord, record->xl_tot_len); return false; } + + /* + * Symmetric with XLogRecordAssemble(): the reader must not attempt to + * reassemble or decode a record larger than XLogRecordMaxSize. Without + * this bound, a crafted multi-page xl_tot_len near UINT32_MAX can make + * allocate_recordbuf()'s size math overflow and corrupt memory during + * reassembly. + */ + 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..286117e2111 100644 --- a/src/test/recovery/t/039_end_of_wal.pl +++ b/src/test/recovery/t/039_end_of_wal.pl @@ -163,6 +163,18 @@ 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 +193,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 +228,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 +240,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 +255,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 +270,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 +289,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 +305,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 +330,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 +341,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 +360,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), -- 2.50.1 (Apple Git-155)