From a8e41a30b323447b023f0cf5a3f26a3f51a9c80f Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Wed, 29 Apr 2026 12:08:26 -0400
Subject: [PATCH v1_PGMASTER 7/7] xlog: Verify all block references are used
 during replay

It's unclear if this makes sense to actually commit. It requires that
blocks read during recovery go through XLogReadBufferForRedoExtended()
and has a special case for handling XLOG_FPI_FOR_HINT when full page
writes are disabled.

Author: Andres Freund <andres@anarazel.de>
Co-authored-by: Melanie Plageman <melanieplageman@gmail.com>
---
 src/backend/access/transam/xlog.c         | 10 ++++++++++
 src/backend/access/transam/xlogreader.c   |  4 ++++
 src/backend/access/transam/xlogrecovery.c |  9 +++++++++
 src/backend/access/transam/xlogutils.c    |  6 ++++++
 src/include/access/xlogreader.h           |  4 ++++
 5 files changed, 33 insertions(+)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 18d5dee06e0..0731439df90 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -9085,6 +9085,16 @@ xlog_redo(XLogReaderState *record)
 			{
 				if (info == XLOG_FPI)
 					elog(ERROR, "XLOG_FPI record did not contain a full-page image");
+
+#ifdef USE_ASSERT_CHECKING
+
+				/*
+				 * If full_page_writes are disabled, we don't want to error
+				 * out because we didn't read the block.
+				 */
+				if (info == XLOG_FPI_FOR_HINT)
+					record->record->blocks[block_id].used_read = true;
+#endif
 				continue;
 			}
 
diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c
index 8849610db00..63747aec566 100644
--- a/src/backend/access/transam/xlogreader.c
+++ b/src/backend/access/transam/xlogreader.c
@@ -1794,6 +1794,10 @@ DecodeXLogRecord(XLogReaderState *state,
 
 			blk = &decoded->blocks[block_id];
 			blk->in_use = true;
+#ifdef USE_ASSERT_CHECKING
+			blk->used_read = false;
+#endif
+
 			blk->apply_image = false;
 
 			COPY_HEADER_FIELD(&fork_flags, sizeof(uint8));
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index c236e2b7969..4e1d50f642d 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -1962,6 +1962,15 @@ ApplyWalRecord(XLogReaderState *xlogreader, XLogRecord *record, TimeLineID *repl
 	/* Now apply the WAL record itself */
 	GetRmgr(record->xl_rmid).rm_redo(xlogreader);
 
+#ifdef USE_ASSERT_CHECKING
+	for (int block_id = 0; block_id <= xlogreader->record->max_block_id; block_id++)
+	{
+		DecodedBkpBlock *blk = &xlogreader->record->blocks[block_id];
+
+		Assert(!blk->in_use || blk->used_read);
+	}
+#endif
+
 	/*
 	 * After redo, check whether the backup pages associated with the WAL
 	 * record are consistent with the existing pages. This check is done only
diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c
index 5fbe39133b8..979d6465159 100644
--- a/src/backend/access/transam/xlogutils.c
+++ b/src/backend/access/transam/xlogutils.c
@@ -359,6 +359,12 @@ XLogReadBufferForRedoExtended(XLogReaderState *record,
 			 block_id);
 	}
 
+#ifdef USE_ASSERT_CHECKING
+	/* Shouldn't have multiple read references to block */
+	Assert(!record->record->blocks[block_id].used_read);
+	record->record->blocks[block_id].used_read = true;
+#endif
+
 	/*
 	 * Make sure that if the block is marked with WILL_INIT, the caller is
 	 * going to initialize it. And vice versa.
diff --git a/src/include/access/xlogreader.h b/src/include/access/xlogreader.h
index 97eae2c1dab..fd2119005e8 100644
--- a/src/include/access/xlogreader.h
+++ b/src/include/access/xlogreader.h
@@ -145,6 +145,10 @@ typedef struct
 	bool		has_data;
 	char	   *data;
 	uint16		data_len;
+
+#ifdef USE_ASSERT_CHECKING
+	bool		used_read;
+#endif
 } DecodedBkpBlock;
 
 /*
-- 
2.43.0

