From 0709bdf828689eba84233a46a35a829a24a13f33 Mon Sep 17 00:00:00 2001
From: Jakub Wartak <jakub.wartak@enterprisedb.com>
Date: Thu, 23 Apr 2026 14:39:10 +0200
Subject: [PATCH v5b 6/6] amcheck: report corruption when index points to
 non-existent heap tuple (take 2)

1. Kill the false positive "index points to non-existent heap tuple" related to valid LP_DEAD tuples.
2. Add detection for beyond EOF blocks and out-of-range offsets.
3. Make it possible for indexed TID pointing at a true LP_UNUSED slot to be detected (makes it possible to detect dangling index tuples).

Author: Jakub Wartak <jakub.wartak@enterprisedb.com>
Discussion: https://www.postgresql.org/message-id/flat/432626F9-65DF-4F0D-B345-26CFC3E2CFAC@yandex-team.ru
---
 contrib/amcheck/verify_nbtree.c | 82 ++++++++++++++++++++++++++++++---
 1 file changed, 75 insertions(+), 7 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 32763de4262..f6f2f9983b7 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -3061,16 +3061,84 @@ bt_verify_index_tuple_points_to_heap(BtreeCheckState *state, IndexTuple itup,
 											  SnapshotAny, slot);
 		if (!found)
 		{
+			BlockNumber blkno = ItemPointerGetBlockNumber(tid);
+			OffsetNumber offnum = ItemPointerGetOffsetNumber(tid);
+			Buffer      buffer;
+			Page        page;
+
 			ExecDropSingleTupleTableSlot(slot);
-			ereport(ERROR,
-					(errcode(ERRCODE_INDEX_CORRUPTED),
-					 errmsg("index tuple in index \"%s\" points to non-existent heap tuple in table \"%s\"",
+
+			/*
+			 * Technically duplicate error as the same check for EOF in bt_target_page_check()
+			 * and we could avoid this, but add this as another sanity check before reading
+			 * the buffer itself. Also AccessShareLock prevents truncate.
+			 */
+			if (blkno >= RelationGetNumberOfBlocks(state->heaprel)) {
+				ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED),
+					errmsg("index tuple in index \"%s\" points to heap tuple in table \"%s\" that is beyond EOF",
+					RelationGetRelationName(state->rel),
+					RelationGetRelationName(state->heaprel)),
+					errdetail_internal("Index tid=(%u,%u) points to heap tid=(%u,%u) that is beyond EOF of heap.",
+					targetblock, offset,
+					ItemPointerGetBlockNumber(tid),
+					ItemPointerGetOffsetNumber(tid))));
+			}
+			else
+			{
+				/*
+				* heap_fetch() might have returned false for
+				* - offsets past the end of the page
+				* - for any item whose line pointer is not LP_NORMAL (LP_DEAD, LP_UNUSED,
+				*   LP_REDIRECT).
+				*
+				* There's also SELECT performing it's opportunistic pruning that could
+				* produce LP_UNUSED, but only on HOT-chain intermediate tuples and those
+				* should not be referenced by btree index directly.
+				*
+				* To avoid false-positive corruption reports, pin and share-lock the
+				* heap buffer and inspect the line pointer directly.
+				*/
+
+				buffer = ReadBufferExtended(state->heaprel, MAIN_FORKNUM, blkno,
+								RBM_NORMAL, state->checkstrategy);
+				LockBuffer(buffer, BUFFER_LOCK_SHARE);
+				page = BufferGetPage(buffer);
+
+				if (offnum < FirstOffsetNumber || offnum > PageGetMaxOffsetNumber(page))
+				{
+					ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED),
+						errmsg("index tuple in index \"%s\" points to heap tuple in table \"%s\" with illegal offset",
+						RelationGetRelationName(state->rel),
+						RelationGetRelationName(state->heaprel)),
+						errdetail_internal("Index tid=(%u,%u) points to heap tid=(%u,%u) with illegal offset",
+						targetblock, offset,
+						ItemPointerGetBlockNumber(tid),
+						ItemPointerGetOffsetNumber(tid))));
+				}
+				else
+				{
+					/*
+					 * If we detect index tuple pointing to LP_UNUSED flag in the heap entry,
+					 * we can assume it's corruption (dangling index pointer). The other flags
+					 * such as LP_DEAD or LP_REDIRECT are fine and can be hit as we progress.
+					 */
+					ItemId lp = PageGetItemId(page, offnum);
+					if (lp->lp_flags == LP_UNUSED)
+					{
+						ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED),
+							errmsg("index tuple in index \"%s\" points to heap tuple marked unused in table \"%s\"",
 							RelationGetRelationName(state->rel),
 							RelationGetRelationName(state->heaprel)),
-					 errdetail_internal("Index tid=(%u,%u) points to heap tid=(%u,%u) that no longer exists.",
-									   targetblock, offset,
-									   ItemPointerGetBlockNumber(tid),
-									   ItemPointerGetOffsetNumber(tid))));
+							errdetail_internal("Index tid=(%u,%u) points to heap tid=(%u,%u) with LP_UNUSED flag.",
+							targetblock, offset,
+							ItemPointerGetBlockNumber(tid),
+							ItemPointerGetOffsetNumber(tid))));
+					}
+				}
+				UnlockReleaseBuffer(buffer);
+			}
+
+			return;
 		}
 
 		/* Skip dead tuples (not visible to our snapshot) */
-- 
2.43.0

