Hi Salma, While testing v3, I noticed a crash in _bt_killitems() triggered by concurrent bt_merge().
The problem ----------- An index scan reads leaf page L, drops its content lock (keeping the pin), and visits the heap. Finding dead tuples, it records them in so->killedItems[]. Concurrently, bt_merge() acquires BT_WRITE on L, moves all tuples to R, and calls BTPageSetMergedAway(), which sets pd_lower to 32 and marks L as BTP_MERGED_AWAY. When the scanner calls _bt_killitems() in !so->dropPin mode, it re-acquires BT_READ and proceeds without checking page flags. PageGetMaxOffsetNumber() returns (32 - 24) / 4 = 2, because the 8-byte safemergexid at bytes 24..31 overlaps with pd_linp[0..1]. The function then interprets safemergexid bits as ItemIdData, causing Assert(ItemIdHasStorage) failures in debug builds or SIGSEGV in production. The so->dropPin path is not affected -- it checks LSN first and gives up if the page was modified. The !so->dropPin path assumes the pin keeps page contents stable, which holds for standard VACUUM (cleanup lock required) but not for bt_merge() (regular BT_WRITE by design). Proposed fix ------------ Add a check for P_ISMERGEDAWAY after acquiring the lock: + /* + * bt_merge() can convert a page to BTP_MERGED_AWAY while we hold a + * pin but no lock. The original tuples are gone; give up on hinting. + */ + if (P_ISMERGEDAWAY(opaque)) + goto unlock_page; Abandoning LP_DEAD hints is harmless -- a subsequent VACUUM will clean up the dead tuples. Any thoughts on this? Best regards, Yao Feng
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 23950f949e..769e1be64a 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -28,6 +28,7 @@
#include "storage/subsystems.h"
#include "utils/datum.h"
#include "utils/lsyscache.h"
+#include "utils/injection_point.h"
#include "utils/rel.h"
@@ -204,6 +205,8 @@ _bt_killitems(IndexScanDesc scan)
Assert(BTScanPosIsValid(so->currPos));
Assert(scan->heapRelation != NULL); /* can't be a bitmap index scan */
+ INJECTION_POINT("before_bt_killitems", NULL);
+
/* Always invalidate so->killedItems[] before leaving so->currPos */
so->numKilled = 0;
test_killitems_crash.sh
Description: Bourne shell script
From 4caf156985236ef0fa8d4fe4574d6bc559f250b9 Mon Sep 17 00:00:00 2001 From: Yao Feng <[email protected]> Date: Tue, 22 Sep 2026 08:42:04 +0000 Subject: [PATCH] nbtree: Guard _bt_killitems against concurrent page merges Avoid crashes in _bt_killitems() by returning early if the page was concurrently merged away (P_ISMERGEDAWAY). --- src/backend/access/nbtree/nbtutils.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c index 23950f949e..b9c110bea5 100644 --- a/src/backend/access/nbtree/nbtutils.c +++ b/src/backend/access/nbtree/nbtutils.c @@ -254,6 +254,14 @@ _bt_killitems(IndexScanDesc scan) page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); + + /* + * bt_merge() can convert a page to BTP_MERGED_AWAY while we hold a + * pin but no lock. The original tuples are gone; give up on hinting. + */ + if (P_ISMERGEDAWAY(opaque)) + goto unlock_page; + minoff = P_FIRSTDATAKEY(opaque); maxoff = PageGetMaxOffsetNumber(page); -- 2.53.0
