postgres.git / summary / log / commit / refs
commit 9628f4de6c5fadc73fa424f5a2e215b9aaa122cd
Author: Peter Geoghegan <pg@bowt.ie>
Date: Wed Aug 19 19:45:43 2026 +0000
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 <pg@bowt.ie>
Reviewed-By: Andrey Borodin <x4mmm@yandex-team.ru>
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.
[parent: 0bace7af3dff]