From 3bd4475e117a30219b28239709741ac60bf7aec2 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Wed, 23 Sep 2026 10:53:09 -0400
Subject: [PATCH v2 1/3] Clear divergent visibility map bits during heap redo

When replaying a heap record that clears PD_ALL_VISIBLE but did not
register a VM buffer because the VM was already clear on the primary,
clear the VM bit anyway.

The VM can diverge across a cluster, e.g. via CREATE DATABASE STRATEGY
WAL_LOG, and a standby must not keep a set VM bit over a page whose
PD_ALL_VISIBLE is clear. You can still get torn pages because you didn't
log an FPI on the primary, however the VM is usually read with
RBM_ZERO_ON_ERROR, so it is considered sufficient. Only backpatch to 19
because <= 18 already have a fallback.

Backpatch-through: 19
---
 src/backend/access/heap/heapam_xlog.c | 49 ++++++++++++++++++++++-----
 1 file changed, 40 insertions(+), 9 deletions(-)

diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c
index fae3b477c09..c65804a6256 100644
--- a/src/backend/access/heap/heapam_xlog.c
+++ b/src/backend/access/heap/heapam_xlog.c
@@ -22,6 +22,33 @@
 #include "storage/freespace.h"
 #include "storage/standby.h"
 
+/*
+ * Clear visibility map bits for a heap block when the WAL record clearing it
+ * did not register the VM block. This handles cases where the VM is
+ * out-of-sync between the primary and standby (for instance, CREATE DATABASE
+ * STRATEGY WAL_LOG historically could cause this).
+ *
+ * This is not fully resilient: the VM page is modified without a full-page
+ * image, so a torn write during a crash could leave it inconsistent until the
+ * page is next repaired. That is considered acceptable since the VM is zeroed
+ * on error when reading it.
+ */
+static void
+heap_xlog_vm_clear_unregistered(Relation reln, BlockNumber heap_blkno,
+								uint8 flags)
+{
+	Buffer		vmbuffer = InvalidBuffer;
+
+	if (visibilitymap_get_status(reln, heap_blkno, &vmbuffer) & flags)
+	{
+		LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE);
+		visibilitymap_clear(reln, heap_blkno, vmbuffer, flags);
+		UnlockReleaseBuffer(vmbuffer);
+	}
+	else if (BufferIsValid(vmbuffer))
+		ReleaseBuffer(vmbuffer);
+}
+
 /*
  * Clear visibility map bits for a single heap block during heap redo.
  *
@@ -46,22 +73,26 @@ heap_xlog_vm_clear(XLogReaderState *record,
 	Relation	reln = CreateFakeRelcacheEntry(target_locator);
 	Buffer		vmbuffer = InvalidBuffer;
 
+	if (!XLogRecHasBlockRef(record, wal_vm_block_id))
+	{
+		heap_xlog_vm_clear_unregistered(reln, heap_blkno, flags);
+		FreeFakeRelcacheEntry(reln);
+		return;
+	}
+
 	/*
 	 * If the vmbuffer was registered, use the recovery-specific routines to
 	 * read it. These will either apply an FPI or indicate that we should
 	 * clear the requested bits ourselves.
 	 */
-	if (XLogRecHasBlockRef(record, wal_vm_block_id))
+	if (XLogReadBufferForRedo(record, wal_vm_block_id,
+							  &vmbuffer) == BLK_NEEDS_REDO)
 	{
-		if (XLogReadBufferForRedo(record, wal_vm_block_id,
-								  &vmbuffer) == BLK_NEEDS_REDO)
-		{
-			if (visibilitymap_clear(reln, heap_blkno, vmbuffer, flags))
-				PageSetLSN(BufferGetPage(vmbuffer), lsn);
-		}
-		if (BufferIsValid(vmbuffer))
-			UnlockReleaseBuffer(vmbuffer);
+		if (visibilitymap_clear(reln, heap_blkno, vmbuffer, flags))
+			PageSetLSN(BufferGetPage(vmbuffer), lsn);
 	}
+	if (BufferIsValid(vmbuffer))
+		UnlockReleaseBuffer(vmbuffer);
 
 	FreeFakeRelcacheEntry(reln);
 }
-- 
2.43.0

