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]