From 30a24e0b94c56a282a352c0883808cbf80201c23 Mon Sep 17 00:00:00 2001 From: Zhong ShiHao Date: Fri, 4 Sep 2026 23:18:12 -0400 Subject: [PATCH] pg_surgery: bound the page's max offset by MaxHeapTuplesPerPage A corrupt pd_lower can push PageGetMaxOffsetNumber() past MaxHeapTuplesPerPage and overrun the include_this_tid[] stack array in heap_force_kill()/heap_force_freeze(). Skip such a block. --- contrib/pg_surgery/expected/heap_surgery.out | 26 ++++++++++++++++++++ contrib/pg_surgery/heap_surgery.c | 13 ++++++++++ contrib/pg_surgery/sql/heap_surgery.sql | 8 ++++++ 3 files changed, 47 insertions(+) diff --git a/contrib/pg_surgery/expected/heap_surgery.out b/contrib/pg_surgery/expected/heap_surgery.out index 42586137d88..8de874400ed 100644 --- a/contrib/pg_surgery/expected/heap_surgery.out +++ b/contrib/pg_surgery/expected/heap_surgery.out @@ -84,6 +84,32 @@ NOTICE: skipping tid (0, 6) for relation "htab" because the item number is out (1 row) +-- the highest offset number a page can hold must be accepted +create temp table htab4(); +insert into htab4 select from generate_series(1, 291); +select heap_force_freeze('htab4'::regclass, ARRAY['(0, 291)']::tid[]); + heap_force_freeze +------------------- + +(1 row) + +select ctid from htab4 where xmin = 2; + ctid +--------- + (0,291) +(1 row) + +select heap_force_kill('htab4'::regclass, ARRAY['(0, 291)']::tid[]); + heap_force_kill +----------------- + +(1 row) + +select ctid from htab4 where xmin = 2; + ctid +------ +(0 rows) + -- set up a new table with a redirected line pointer -- use a temp table so that vacuum behavior doesn't depend on global xmin create temp table htab2(a int); diff --git a/contrib/pg_surgery/heap_surgery.c b/contrib/pg_surgery/heap_surgery.c index 51f3f3c49eb..9b46d3a2c65 100644 --- a/contrib/pg_surgery/heap_surgery.c +++ b/contrib/pg_surgery/heap_surgery.c @@ -184,6 +184,19 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) maxoffset = PageGetMaxOffsetNumber(page); + if (maxoffset > MaxHeapTuplesPerPage) + { + UnlockReleaseBuffer(buf); + + /* Update the current_start_ptr before moving to the next page. */ + curr_start_ptr = next_start_ptr; + + ereport(NOTICE, + (errmsg("skipping block %u for relation \"%s\" because the page header is invalid", + blkno, RelationGetRelationName(rel)))); + continue; + } + /* * Figure out which TIDs we are going to process and which ones we are * going to skip. diff --git a/contrib/pg_surgery/sql/heap_surgery.sql b/contrib/pg_surgery/sql/heap_surgery.sql index c4e933da13a..0fca0131246 100644 --- a/contrib/pg_surgery/sql/heap_surgery.sql +++ b/contrib/pg_surgery/sql/heap_surgery.sql @@ -37,6 +37,14 @@ select ctid, xmax from htab where xmin = 2; -- out-of-range TIDs should be skipped select heap_force_freeze('htab'::regclass, ARRAY['(0, 0)', '(0, 6)']::tid[]); +-- the highest offset number a page can hold must be accepted +create temp table htab4(); +insert into htab4 select from generate_series(1, 291); +select heap_force_freeze('htab4'::regclass, ARRAY['(0, 291)']::tid[]); +select ctid from htab4 where xmin = 2; +select heap_force_kill('htab4'::regclass, ARRAY['(0, 291)']::tid[]); +select ctid from htab4 where xmin = 2; + -- set up a new table with a redirected line pointer -- use a temp table so that vacuum behavior doesn't depend on global xmin create temp table htab2(a int); -- 2.37.1 (Apple Git-137.1)