From 9628f4de6c5fadc73fa424f5a2e215b9aaa122cd Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Wed, 19 Aug 2026 15:45:43 -0400 Subject: [PATCH] GiST: Invalidate killed items consistently. GiST neglected to invalidate its killedItems[] array on a rescan. As a result, it was just about possible for the wrong tuples from the wrong index page to be LP_DEAD-marked on a rescan. The scan mistakenly believed that the previous rescan's killedItems[] were for this rescan's curBlkno, causing index corruption. To fix, bring GiST in line with nbtree and hash: call gistkillitems from both gistrescan and gistendscan (the existing gistgettuple caller still handles the common case where we need to LP_DEAD-mark before moving on to the next page). That way the scan's pending killedItems[] are passed to gistkillitems while they still describe items from curBlkno. When gistkillitems runs, it'll invalidate the array in passing (and won't needlessly miss out on an opportunity to LP_DEAD-mark eligible index tuples). Back branches just get minimal hardening: we invalidate killedItems[] at the places where the master branch gets new calls to gistkillitems (and we invalidate curBlkno and curPageLSN on a rescan). The test that proved corruption on master didn't result in corruption on any stable branch, though only because, without commit 9c9ddf109, we'd clobber curPageLSN without also updating curBlkno -- which accidentally prevented it. Relying on gistkillitems to not LP_DEAD-mark by passing it a curBlkno whose curPageLSN was taken from an entirely different page seems like a very bad idea, which is why this issue is being treated as a bug affecting all stable branches. Author: Peter Geoghegan Reviewed-By: Andrey Borodin Discussion: https://postgr.es/m/CAH2-WzmwEThnQf17Ju+t0N9_KJLsEQSXzYrFnaS2=s4KnGGrqw@mail.gmail.com Backpatch-through: 14 --- src/backend/access/gist/gistget.c | 4 ++++ src/backend/access/gist/gistscan.c | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c index c8f7e781c6c..49ba2d901d8 100644 --- a/src/backend/access/gist/gistget.c +++ b/src/backend/access/gist/gistget.c @@ -425,7 +425,10 @@ gistScanPage(IndexScanDesc scan, GISTSearchItem *pageItem, * killed tuple as not passing the qual. */ if (scan->ignore_killed_tuples && ItemIdIsDead(iid)) + { + Assert(GistPageIsLeaf(page)); continue; + } it = (IndexTuple) PageGetItem(page, iid); @@ -722,6 +725,7 @@ gistgettuple(IndexScanDesc scan, ScanDirection dir) CHECK_FOR_INTERRUPTS(); /* save current item BlockNumber for next gistkillitems() call */ + Assert(so->numKilled == 0); so->curBlkno = item->blkno; /* diff --git a/src/backend/access/gist/gistscan.c b/src/backend/access/gist/gistscan.c index 61e92cf0f5d..f62880a0e81 100644 --- a/src/backend/access/gist/gistscan.c +++ b/src/backend/access/gist/gistscan.c @@ -133,7 +133,12 @@ gistrescan(IndexScanDesc scan, ScanKey key, int nkeys, int i; MemoryContext oldCxt; + /* invalidate any killed items still pending */ + so->numKilled = 0; + /* rescan an existing indexscan --- reset state */ + so->curBlkno = InvalidBlockNumber; + so->curPageLSN = InvalidXLogRecPtr; /* * The first time through, we create the search queue in the scanCxt. @@ -350,6 +355,9 @@ gistendscan(IndexScanDesc scan) { GISTScanOpaque so = (GISTScanOpaque) scan->opaque; + /* invalidate any killed items still pending */ + so->numKilled = 0; + /* * freeGISTstate is enough to clean up everything made by gistbeginscan, * as well as the queueCxt if there is a separate context for it.