v5b-0006-amcheck-report-corruption-when-index-points-to-n.patch
text/x-patch
Filename: v5b-0006-amcheck-report-corruption-when-index-points-to-n.patch
Type: text/x-patch
Part: 5
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch 0006
Subject: amcheck: report corruption when index points to non-existent heap tuple (take 2)
| File | + | − |
|---|---|---|
| contrib/amcheck/verify_nbtree.c | 75 | 7 |
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