From 4290894e69504b7f8543ed1ed0a302602b2628d9 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Wed, 23 Sep 2026 16:42:27 -0400
Subject: [PATCH v1 08/11] Add RBM_ZERO_ON_MISSING and read the visibility map
 with it

Add a buffer read mode, RBM_ZERO_ON_MISSING: like RBM_NORMAL for a page that
exists (a corrupt page errors), but if the block is past the fork's EOF the
fork is extended with zeroed pages during recovery rather than failing.

Use this mode when reading the VM in normal operation and recovery. The
preceding changes register VM blocks for DML clears even when they are
no-ops on the primary, and WAL-log corruption repairs. Users can select
zero_damaged_pages = 'vm' to zero a damaged VM page without also allowing
damaged main-fork pages to be zeroed.

XXX: VM truncation doesn't always register every VM block it modifies.
Look into this more.
---
 src/backend/access/heap/heapam_xlog.c   | 39 +++++++++++++++++++------
 src/backend/access/heap/visibilitymap.c | 14 +++++----
 src/backend/storage/buffer/bufmgr.c     |  3 +-
 src/include/storage/bufmgr.h            |  5 ++++
 4 files changed, 46 insertions(+), 15 deletions(-)

diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c
index 5fa1de09cfb..b0ca1acf2b8 100644
--- a/src/backend/access/heap/heapam_xlog.c
+++ b/src/backend/access/heap/heapam_xlog.c
@@ -53,11 +53,18 @@ heap_xlog_vm_clear(XLogReaderState *record,
 	 * read it. These will either apply an FPI or indicate that we should
 	 * clear the requested bits ourselves.
 	 */
-	if (XLogReadBufferForRedo(record, wal_vm_block_id,
-							  &vmbuffer) == BLK_NEEDS_REDO)
+	if (XLogReadBufferForRedoExtended(record, wal_vm_block_id,
+									  RBM_ZERO_ON_MISSING, false,
+									  &vmbuffer) == BLK_NEEDS_REDO)
 	{
+		Page		vmpage = BufferGetPage(vmbuffer);
+
+		/* initialize the page if it was extended as zeros */
+		if (PageIsNew(vmpage))
+			PageInit(vmpage, BLCKSZ, 0);
+
 		if (visibilitymap_clear(target_locator, heap_blkno, vmbuffer, flags))
-			PageSetLSN(BufferGetPage(vmbuffer), lsn);
+			PageSetLSN(vmpage, lsn);
 	}
 	if (BufferIsValid(vmbuffer))
 		UnlockReleaseBuffer(vmbuffer);
@@ -273,7 +280,7 @@ heap_xlog_prune_freeze(XLogReaderState *record)
 	 */
 	if ((vmflags & VISIBILITYMAP_VALID_BITS) &&
 		XLogReadBufferForRedoExtended(record, 1,
-									  RBM_ZERO_ON_ERROR,
+									  RBM_ZERO_ON_MISSING,
 									  false,
 									  &vmbuffer) == BLK_NEEDS_REDO)
 	{
@@ -727,7 +734,7 @@ heap_xlog_multi_insert(XLogReaderState *record)
 	 * heap_xlog_prune_freeze()).
 	 */
 	if ((xlrec->flags & XLH_INSERT_ALL_FROZEN_SET) &&
-		XLogReadBufferForRedoExtended(record, 1, RBM_ZERO_ON_ERROR, false,
+		XLogReadBufferForRedoExtended(record, 1, RBM_ZERO_ON_MISSING, false,
 									  &vmbuffer) == BLK_NEEDS_REDO)
 	{
 		Page		vmpage = BufferGetPage(vmbuffer);
@@ -812,9 +819,16 @@ heap_xlog_update(XLogReaderState *record, bool hot_update)
 
 		Assert(xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED);
 
-		if (XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_VM_NEW,
-								  &vmbuffer_new) == BLK_NEEDS_REDO)
+		if (XLogReadBufferForRedoExtended(record, HEAP_UPDATE_BLKREF_VM_NEW,
+										  RBM_ZERO_ON_MISSING, false,
+										  &vmbuffer_new) == BLK_NEEDS_REDO)
 		{
+			Page		vmpage = BufferGetPage(vmbuffer_new);
+
+			/* initialize the page if it was extended as zeros */
+			if (PageIsNew(vmpage))
+				PageInit(vmpage, BLCKSZ, 0);
+
 			/*
 			 * If both the old and new heap pages were all-visible and their
 			 * VM bits are on the same VM page, that single VM page is
@@ -850,9 +864,16 @@ heap_xlog_update(XLogReaderState *record, bool hot_update)
 
 		Assert(xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED);
 
-		if (XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_VM_OLD,
-								  &vmbuffer_old) == BLK_NEEDS_REDO)
+		if (XLogReadBufferForRedoExtended(record, HEAP_UPDATE_BLKREF_VM_OLD,
+										  RBM_ZERO_ON_MISSING, false,
+										  &vmbuffer_old) == BLK_NEEDS_REDO)
 		{
+			Page		vmpage = BufferGetPage(vmbuffer_old);
+
+			/* initialize the page if it was extended as zeros */
+			if (PageIsNew(vmpage))
+				PageInit(vmpage, BLCKSZ, 0);
+
 			if (visibilitymap_clear(rlocator, oldblk, vmbuffer_old,
 									VISIBILITYMAP_VALID_BITS))
 				PageSetLSN(BufferGetPage(vmbuffer_old), lsn);
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index fe5ce437e1b..73ca76462a9 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -592,9 +592,13 @@ vm_readbuf(Relation rel, BlockNumber blkno, bool extend)
 	}
 
 	/*
-	 * For reading we use ZERO_ON_ERROR mode, and initialize the page if
-	 * necessary. It's always safe to clear bits, so it's better to clear
-	 * corrupt pages than error out.
+	 * For reading we use ZERO_ON_MISSING mode, and initialize the page if
+	 * necessary. A block past the fork's end never reaches the read below: it
+	 * is either extended by vm_extend() or reported as missing. A corrupt
+	 * existing page throws an error rather than being automatically zeroed.
+	 * zero_damaged_pages can override this for the VM fork. DML changes and
+	 * corruption repairs register the VM in WAL, but truncation still has a
+	 * separate tail-clear path; see visibilitymap_prepare_truncate().
 	 *
 	 * We use the same path below to initialize pages when extending the
 	 * relation, as a concurrent extension can end up with vm_extend()
@@ -609,7 +613,7 @@ vm_readbuf(Relation rel, BlockNumber blkno, bool extend)
 	}
 	else
 		buf = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, blkno,
-								 RBM_ZERO_ON_ERROR, NULL);
+								 RBM_ZERO_ON_MISSING, NULL);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -649,7 +653,7 @@ vm_extend(Relation rel, BlockNumber vm_nblocks)
 							  EB_CREATE_FORK_IF_NEEDED |
 							  EB_CLEAR_SIZE_CACHE,
 							  vm_nblocks,
-							  RBM_ZERO_ON_ERROR);
+							  RBM_ZERO_ON_MISSING);
 
 	/*
 	 * Send a shared-inval message to force other backends to close any smgr
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 27539b564f0..4609b1b86b9 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -928,7 +928,8 @@ ReadBuffer(Relation reln, BlockNumber blockNum)
  * RBM_ZERO_AND_CLEANUP_LOCK is the same as RBM_ZERO_AND_LOCK, but acquires
  * a cleanup-strength lock on the page.
  *
- * RBM_NORMAL_NO_LOG mode is treated the same as RBM_NORMAL here.
+ * RBM_NORMAL_NO_LOG and RBM_ZERO_ON_MISSING modes are treated the same as
+ * RBM_NORMAL here.
  *
  * If strategy is not NULL, a nondefault buffer access strategy is used.
  * See buffer/README for details.
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 7d6106ffd7e..39c27c4d811 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -51,6 +51,11 @@ typedef enum
 	RBM_ZERO_ON_ERROR,			/* Read, but return an all-zeros page on error */
 	RBM_NORMAL_NO_LOG,			/* Don't log page as invalid during WAL
 								 * replay; otherwise same as RBM_NORMAL */
+	RBM_ZERO_ON_MISSING,		/* During WAL replay, extend the fork with
+								 * zeroed pages if the block doesn't exist and
+								 * accept an all-zeroes page, rather than
+								 * treating either as invalid; otherwise same
+								 * as RBM_NORMAL */
 } ReadBufferMode;
 
 /*
-- 
2.43.0

