From 2bc2beca916c2d978f86962c66a4cfc3ef80cc02 Mon Sep 17 00:00:00 2001
From: David Karapetyan <dkarapetyan@gmail.com>
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 <boekewurm+postgres@gmail.com>
---
 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)

