From c5824d62c4d7cd24452f83a776fbfab8460a883a Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Wed, 16 Sep 2026 11:48:05 -0400
Subject: [PATCH v1] Register VM buffer on no-op clears so redo fixes a
 divergent VM

Since ed62d26caca, heap WAL records clearing PD_ALL_VISIBLE (insert,
multi_insert, delete, update) only registered the VM buffer when clearing
its bits changed the VM page.

With CREATE DATABASE using strategy WAL_LOG and copying from a template, it's
possible to be in a scenario where the VM is clear on the primary and set on
the standby. Skipping clearing the VM during redo would then lead to data
corruption.

Though the reported CREATE DATABASE issue should be independently fixed,
harden the VM clear paths against VM divergence by always registering the VM
buffer when having to clear even PD_ALL_VISIBLE.

Tuple locking isn't fixed because it is pre-existing and hardening it in this
way would negatively impact performance significantly.
---
 src/backend/access/heap/heapam.c | 59 ++++++++++++++++++++++++++------
 1 file changed, 49 insertions(+), 10 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 9ebb1b35d37..46815f52305 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -64,6 +64,7 @@ static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf,
 								  Buffer vmbuffer_new, HeapTuple oldtup,
 								  HeapTuple newtup, HeapTuple old_key_tuple,
 								  bool all_visible_cleared, bool new_all_visible_cleared,
+								  bool vmbuffer_old_modified, bool vmbuffer_new_modified,
 								  bool walLogical);
 #ifdef USE_ASSERT_CHECKING
 static void check_lock_if_inplace_updateable_rel(Relation relation,
@@ -2172,8 +2173,14 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
 		/* filtering by origin on a row level is much more efficient */
 		XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);
 
-		if (vmbuffer_modified)
-			XLogRegisterBuffer(HEAP_INSERT_BLKREF_VM, vmbuffer, 0);
+		/*
+		 * Register the VM buffer even if its bits were already clear, so redo
+		 * clears PD_ALL_VISIBLE's VM bits; the VM can be out-of-sync across a
+		 * cluster.
+		 */
+		if (clear_all_visible)
+			XLogRegisterBuffer(HEAP_INSERT_BLKREF_VM, vmbuffer,
+							   vmbuffer_modified ? 0 : REGBUF_NO_CHANGE);
 
 		recptr = XLogInsert(RM_HEAP_ID, info);
 
@@ -2611,8 +2618,16 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
 			XLogRegisterData(xlrec, tupledata - scratch.data);
 			XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_HEAP, buffer,
 							   REGBUF_STANDARD | bufflags);
-			if (all_frozen_set || vmbuffer_modified)
-				XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_VM, vmbuffer, 0);
+
+			/*
+			 * Register the VM buffer when setting it all-frozen, or so redo
+			 * clears PD_ALL_VISIBLE's VM bits even if they were already clear
+			 * here (the VM can be out-of-sync across a cluster).
+			 */
+			if (all_frozen_set || clear_all_visible)
+				XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_VM, vmbuffer,
+								   (all_frozen_set || vmbuffer_modified) ?
+								   0 : REGBUF_NO_CHANGE);
 
 			XLogRegisterBufData(HEAP_MULTI_INSERT_BLKREF_HEAP, tupledata,
 								totaldatalen);
@@ -3142,8 +3157,14 @@ heap_delete(Relation relation, const ItemPointerData *tid,
 		/* filtering by origin on a row level is much more efficient */
 		XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);
 
-		if (vmbuffer_modified)
-			XLogRegisterBuffer(HEAP_DELETE_BLKREF_VM, vmbuffer, 0);
+		/*
+		 * Register the VM buffer even if its bits were already clear, so redo
+		 * clears PD_ALL_VISIBLE's VM bits; the VM can be out-of-sync across a
+		 * cluster.
+		 */
+		if (clear_all_visible)
+			XLogRegisterBuffer(HEAP_DELETE_BLKREF_VM, vmbuffer,
+							   vmbuffer_modified ? 0 : REGBUF_NO_CHANGE);
 
 		recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_DELETE);
 
@@ -4274,14 +4295,25 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 			log_heap_new_cid(relation, heaptup);
 		}
 
+		/*
+		 * When both heap pages are all-visible and share a VM page, that page
+		 * is registered once as VM_NEW; pass the old slot as invalid to avoid
+		 * registering the same buffer twice.
+		 */
 		recptr = log_heap_update(relation, buffer,
-								 vmbuffer_modified ? vmbuffer : InvalidBuffer,
+								 (clear_all_visible &&
+								  !(clear_all_visible_new &&
+									vmbuffer == vmbuffer_new)) ?
+								 vmbuffer : InvalidBuffer,
 								 newbuf,
-								 vmbuffer_new_modified ? vmbuffer_new : InvalidBuffer,
+								 clear_all_visible_new ?
+								 vmbuffer_new : InvalidBuffer,
 								 &oldtup, heaptup,
 								 old_key_tuple,
 								 clear_all_visible,
 								 clear_all_visible_new,
+								 vmbuffer_modified,
+								 vmbuffer_new_modified,
 								 walLogical);
 		if (newbuf != buffer)
 		{
@@ -9021,6 +9053,7 @@ log_heap_update(Relation reln, Buffer oldbuf, Buffer vmbuffer_old,
 				HeapTuple oldtup, HeapTuple newtup,
 				HeapTuple old_key_tuple,
 				bool all_visible_cleared, bool new_all_visible_cleared,
+				bool vmbuffer_old_modified, bool vmbuffer_new_modified,
 				bool walLogical)
 {
 	xl_heap_update xlrec;
@@ -9233,15 +9266,21 @@ log_heap_update(Relation reln, Buffer oldbuf, Buffer vmbuffer_old,
 	 * same VM page and both their VM bits were cleared, the caller passes
 	 * only vmbuffer_new (mirroring the heap page convention where block 0 =
 	 * new is always registered).
+	 *
+	 * A buffer is registered even if its bits were already clear, so redo
+	 * clears PD_ALL_VISIBLE's VM bits (the VM can be out-of-sync across a
+	 * cluster); use REGBUF_NO_CHANGE when the page was not modified.
 	 */
 	Assert((BufferIsInvalid(vmbuffer_old) && BufferIsInvalid(vmbuffer_new)) ||
 		   (vmbuffer_old != vmbuffer_new));
 
 	if (BufferIsValid(vmbuffer_new))
-		XLogRegisterBuffer(HEAP_UPDATE_BLKREF_VM_NEW, vmbuffer_new, 0);
+		XLogRegisterBuffer(HEAP_UPDATE_BLKREF_VM_NEW, vmbuffer_new,
+						   vmbuffer_new_modified ? 0 : REGBUF_NO_CHANGE);
 
 	if (BufferIsValid(vmbuffer_old))
-		XLogRegisterBuffer(HEAP_UPDATE_BLKREF_VM_OLD, vmbuffer_old, 0);
+		XLogRegisterBuffer(HEAP_UPDATE_BLKREF_VM_OLD, vmbuffer_old,
+						   vmbuffer_old_modified ? 0 : REGBUF_NO_CHANGE);
 
 	/* filtering by origin on a row level is much more efficient */
 	XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);
-- 
2.43.0

