From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Yuriy Grigoryev Date: Fri, 4 Sep 2026 14:20:00 +0700 Subject: [PATCH] Reject WAL block images whose hole does not fit in the page. DecodeXLogRecord() already rejects a BKPIMAGE_HAS_HOLE image when hole_offset or hole_length is zero, or when bimg_len is BLCKSZ. It does not check that the hole lies inside the page. hole_offset and hole_length are uint16 values taken from the WAL record, so a compressed image can claim a hole that starts near the end of the page and extends well past BLCKSZ. RestoreBlockImage() then uses those fields as memcpy/MemSet lengths into a BLCKSZ buffer, and as BLCKSZ - hole_length for the decompressor's output capacity. A hole that does not fit causes out-of-bounds writes and size_t underflow. Extend the existing HAS_HOLE cross-check with hole_offset <= BLCKSZ and hole_length <= BLCKSZ - hole_offset, using subtraction so the two untrusted fields are never added together. Re-check the same bound at the start of RestoreBlockImage() before decompression or memcpy. This is not reachable from WAL produced by a healthy PostgreSQL instance. It requires a corrupt or crafted record that still has a plausible structure. Discussion: https://postgr.es/m/19599-8859c3822a831331@postgresql.org --- src/backend/access/transam/xlogreader.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 5c26d33a603..b7d082d8f8b 100644 --- a/src/backend/access/transam/xlogreader.c +++ b/src/backend/access/transam/xlogreader.c @@ -1824,13 +1824,18 @@ DecodeXLogRecord(XLogReaderState *state, datatotal += blk->bimg_len; /* - * cross-check that hole_offset > 0, hole_length > 0 and - * bimg_len < BLCKSZ if the HAS_HOLE flag is set. + * cross-check that hole_offset > 0, hole_length > 0, + * bimg_len < BLCKSZ, and the hole fits in the page if the + * HAS_HOLE flag is set. Compare hole_length with + * BLCKSZ - hole_offset so the two untrusted fields are never + * added together. */ if ((blk->bimg_info & BKPIMAGE_HAS_HOLE) && (blk->hole_offset == 0 || blk->hole_length == 0 || - blk->bimg_len == BLCKSZ)) + blk->bimg_len == BLCKSZ || + blk->hole_offset > BLCKSZ || + blk->hole_length > BLCKSZ - blk->hole_offset)) { report_invalid_record(state, "BKPIMAGE_HAS_HOLE set, but hole offset %u length %u block image length %u at %X/%X", @@ -2099,6 +2104,21 @@ RestoreBlockImage(XLogReaderState *record, uint8 block_id, char *page) bkpb = &record->record->blocks[block_id]; ptr = bkpb->bkp_image; + /* + * The hole must fit in the page. DecodeXLogRecord() already enforces + * this; re-check here before using the values as memcpy/MemSet lengths + * or as the decompressor output capacity. + */ + if (bkpb->hole_offset > BLCKSZ || + bkpb->hole_length > BLCKSZ - bkpb->hole_offset) + { + report_invalid_record(record, + "could not restore image at %X/%X with invalid state, block %d", + LSN_FORMAT_ARGS(record->ReadRecPtr), + block_id); + return false; + } + if (BKPIMAGE_COMPRESSED(bkpb->bimg_info)) { /* If a backup block image is compressed, decompress it */ -- 2.39.2