Improving handling of oversized records in xlogreader.c
authorMichael Paquier <michael@paquier.xyz>
Tue, 4 Aug 2026 21:53:10 +0000 (06:53 +0900)
committerMichael Paquier <michael@paquier.xyz>
Tue, 4 Aug 2026 21:53:10 +0000 (06:53 +0900)
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
src/test/recovery/t/039_end_of_wal.pl

index 946907a2507d30648ac9b0fcd98513b325d2fff2..4bfe8607f4e1091db96c8a9984408cc2878e8295 100644 (file)
@@ -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,
index f46d089a0fbc1d727f0be7f956c7b12e1d6cdcd1..5fe8835e094126125544103af14d3f1913640631 100644 (file)
@@ -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),