From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Mon, 3 Aug 2026 13:18:00 +0000
Subject: [PATCH v2] pg_surgery: Fix infinite loop on large tid arrays

heap_force_common() tracked the current position in the caller-supplied
tid[] using OffsetNumber.  That type is a uint16, so when the array
held more than 65535 entries the updated index wrapped and the outer
loop never reached ntids.  A SQL call with a sufficiently large tid[]
could then run until cancelled.

Fix by tracking the tid[] position with int instead of OffsetNumber.
A regress case based on the report is included.

Bug: #19607
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Reported-by: Yuelin Wang <1217816127@qq.com>
Discussion: https://postgr.es/m/19607-2f256a66481c514b@postgresql.org
Backpatch-through: 14

---
diff --git a/contrib/pg_surgery/expected/heap_surgery.out b/contrib/pg_surgery/expected/heap_surgery.out
index df7d13b0908..8997caf3ee3 100644
--- a/contrib/pg_surgery/expected/heap_surgery.out
+++ b/contrib/pg_surgery/expected/heap_surgery.out
@@ -134,6 +134,23 @@ select heap_force_kill('htab2'::regclass, ARRAY['(0, 3)']::tid[]);
  
 (1 row)
 
+-- a tid[] larger than 65535 entries must still finish
+create temp table htab3(a int);
+insert into htab3 values (1);
+select heap_force_kill(
+	'htab3'::regclass,
+	array(select '(0,1)'::tid from generate_series(1, 65536)));
+ heap_force_kill 
+-----------------
+ 
+(1 row)
+
+select count(*) from htab3;
+ count 
+-------
+     0
+(1 row)
+
 -- materialized view.
 -- note that we don't commit the transaction, so autovacuum can't interfere.
 begin;
diff --git a/contrib/pg_surgery/heap_surgery.c b/contrib/pg_surgery/heap_surgery.c
index 181b7d1e210..51f3f3c49eb 100644
--- a/contrib/pg_surgery/heap_surgery.c
+++ b/contrib/pg_surgery/heap_surgery.c
@@ -44,7 +44,7 @@ static Datum heap_force_common(FunctionCallInfo fcinfo,
 							   HeapTupleForceOption heap_force_opt);
 static void sanity_check_tid_array(ArrayType *ta, int *ntids);
 static BlockNumber find_tids_one_page(ItemPointer tids, int ntids,
-									  OffsetNumber *next_start_ptr);
+									  int *next_start_ptr);
 
 /*-------------------------------------------------------------------------
  * heap_force_kill()
@@ -91,7 +91,7 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt)
 	int			ntids,
 				nblocks;
 	Relation	rel;
-	OffsetNumber curr_start_ptr,
+	int			curr_start_ptr,
 				next_start_ptr;
 	bool		include_this_tid[MaxHeapTuplesPerPage];
 
@@ -413,7 +413,7 @@ sanity_check_tid_array(ArrayType *ta, int *ntids)
  * ------------------------------------------------------------------------
  */
 static BlockNumber
-find_tids_one_page(ItemPointer tids, int ntids, OffsetNumber *next_start_ptr)
+find_tids_one_page(ItemPointer tids, int ntids, int *next_start_ptr)
 {
 	int			i;
 	BlockNumber prev_blkno,
diff --git a/contrib/pg_surgery/sql/heap_surgery.sql b/contrib/pg_surgery/sql/heap_surgery.sql
index 6526b27535d..ff4474bddd7 100644
--- a/contrib/pg_surgery/sql/heap_surgery.sql
+++ b/contrib/pg_surgery/sql/heap_surgery.sql
@@ -65,6 +65,14 @@ select heap_force_kill('htab2'::regclass, ARRAY[NULL]::tid[]);
 -- but we should be able to kill the one tuple we have
 select heap_force_kill('htab2'::regclass, ARRAY['(0, 3)']::tid[]);
 
+-- a tid[] larger than 65535 entries must still finish
+create temp table htab3(a int);
+insert into htab3 values (1);
+select heap_force_kill(
+	'htab3'::regclass,
+	array(select '(0,1)'::tid from generate_series(1, 65536)));
+select count(*) from htab3;
+
 -- materialized view.
 -- note that we don't commit the transaction, so autovacuum can't interfere.
 begin;
