From a9eb9f17d120060a88a817689ee71f3556bdc258 Mon Sep 17 00:00:00 2001
From: Virender Singla <vksingla@google.com>
Date: Mon, 31 Aug 2026 09:22:00 +0000
Subject: [PATCH] Fix missing tts_tid assignment in ExecForceStoreHeapTuple

When storing a HeapTuple into a BufferHeapTupleTableSlot in
ExecForceStoreHeapTuple(), ExecClearTuple() invalidated slot->tts_tid,
but it was never updated with the tuple's physical ItemPointer (t_self).

For queries using the index scan reorder queue (such as KNN GiST scans
with lossy distance recheck) combined with row locking (e.g. FOR UPDATE),
the invalid TID was passed to heap_lock_tuple(), causing relation file
extension and "attempted to lock invisible tuple" errors.

Fix by assigning slot->tts_tid from tuple->t_self.

Backpatch-through: 14
---
 src/backend/executor/execTuples.c  |  1 +
 src/test/regress/expected/gist.out | 23 +++++++++++++++++++++++
 src/test/regress/sql/gist.sql      | 16 ++++++++++++++++
 3 files changed, 40 insertions(+)

diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c
index b8e8f52c64c..695b87f97e2 100644
--- a/src/backend/executor/execTuples.c
+++ b/src/backend/executor/execTuples.c
@@ -1765,6 +1765,7 @@ ExecForceStoreHeapTuple(HeapTuple tuple,
 		slot->tts_flags &= ~TTS_FLAG_EMPTY;
 		oldContext = MemoryContextSwitchTo(slot->tts_mcxt);
 		bslot->base.tuple = heap_copytuple(tuple);
+		slot->tts_tid = tuple->t_self;
 		slot->tts_flags |= TTS_FLAG_SHOULDFREE;
 		MemoryContextSwitchTo(oldContext);
 
diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out
index ac79f94aa80..280c2a88c88 100644
--- a/src/test/regress/expected/gist.out
+++ b/src/test/regress/expected/gist.out
@@ -463,3 +463,26 @@ create index gist_tbl_box_index on gist_tbl using gist (b);
 insert into gist_tbl
   select box(point(0.05*i, 0.05*i)) from generate_series(0,10) as i;
 drop table gist_tbl;
+-- Test KNN scan with lossy distance recheck, reorder queue, and row locking
+CREATE TABLE gist_reorder_tbl (id int4 PRIMARY KEY, v circle);
+INSERT INTO gist_reorder_tbl (id, v)
+  SELECT i, circle(point(i, i), 0.5) FROM generate_series(1, 100) i;
+CREATE INDEX gist_reorder_idx ON gist_reorder_tbl USING gist (v circle_ops);
+-- Verify that ctid is properly preserved when popped from reorder queue
+SELECT ctid != '(4294967295,0)'::tid AS valid_tid FROM gist_reorder_tbl
+  ORDER BY v <-> point(50, 50) LIMIT 1;
+ valid_tid 
+-----------
+ t
+(1 row)
+
+-- Verify that row locking succeeds without attempting to lock invisible tuple
+BEGIN;
+SELECT id FROM gist_reorder_tbl ORDER BY v <-> point(50, 50) LIMIT 1 FOR UPDATE;
+ id 
+----
+ 50
+(1 row)
+
+COMMIT;
+DROP TABLE gist_reorder_tbl;
diff --git a/src/test/regress/sql/gist.sql b/src/test/regress/sql/gist.sql
index 57dcc082450..c23912da9cf 100644
--- a/src/test/regress/sql/gist.sql
+++ b/src/test/regress/sql/gist.sql
@@ -236,3 +236,19 @@ create index gist_tbl_box_index on gist_tbl using gist (b);
 insert into gist_tbl
   select box(point(0.05*i, 0.05*i)) from generate_series(0,10) as i;
 drop table gist_tbl;
+
+-- Test KNN scan with lossy distance recheck, reorder queue, and row locking
+CREATE TABLE gist_reorder_tbl (id int4 PRIMARY KEY, v circle);
+INSERT INTO gist_reorder_tbl (id, v)
+  SELECT i, circle(point(i, i), 0.5) FROM generate_series(1, 100) i;
+CREATE INDEX gist_reorder_idx ON gist_reorder_tbl USING gist (v circle_ops);
+
+-- Verify that ctid is properly preserved when popped from reorder queue
+SELECT ctid != '(4294967295,0)'::tid AS valid_tid FROM gist_reorder_tbl
+  ORDER BY v <-> point(50, 50) LIMIT 1;
+
+-- Verify that row locking succeeds without attempting to lock invisible tuple
+BEGIN;
+SELECT id FROM gist_reorder_tbl ORDER BY v <-> point(50, 50) LIMIT 1 FOR UPDATE;
+COMMIT;
+DROP TABLE gist_reorder_tbl;
-- 
2.55.0.897.gb25b4bd76c-goog

