From cb6ecce48eba9fd1fce3992d3ae31b1f55012e24 Mon Sep 17 00:00:00 2001
From: Shihao Zhong <zhong950419@gmail.com>
Date: Sun, 30 Aug 2026 23:33:11 -0400
Subject: [PATCH v3 1/2] pageinspect: validate line pointers before using them

The GiST, B-tree and hash page_items() functions could read past the
page image when given a corrupt page.  Verify each line pointer first.
bt_page_items() also raises an error for a pivot tuple marked LP_DEAD
instead of failing an assertion.

Co-authored-by: Greg Burd <gregburd@protonmail.com>
---
 contrib/pageinspect/btreefuncs.c       | 28 ++++++++++++++++++---
 contrib/pageinspect/expected/btree.out | 13 ++++++++++
 contrib/pageinspect/expected/gist.out  |  4 +++
 contrib/pageinspect/expected/hash.out  |  4 +++
 contrib/pageinspect/gistfuncs.c        | 35 +++++++++++++++++++++++---
 contrib/pageinspect/hashfuncs.c        | 17 +++++++++++--
 contrib/pageinspect/sql/btree.sql      | 11 ++++++++
 contrib/pageinspect/sql/gist.sql       |  3 +++
 contrib/pageinspect/sql/hash.sql       |  3 +++
 9 files changed, 109 insertions(+), 9 deletions(-)

diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c
index 9917663593b..9c461322758 100644
--- a/contrib/pageinspect/btreefuncs.c
+++ b/contrib/pageinspect/btreefuncs.c
@@ -500,11 +500,24 @@ bt_page_print_tuples(ua_page_items *uargs)
 
 	id = PageGetItemId(page, offset);
 
-	if (!ItemIdIsValid(id))
-		elog(ERROR, "invalid ItemId");
+	/* Check that the line pointer and tuple lie within the page. */
+	if (!ItemIdHasStorage(id) ||
+		ItemIdGetOffset(id) != MAXALIGN(ItemIdGetOffset(id)) ||
+		ItemIdGetLength(id) < sizeof(IndexTupleData) ||
+		ItemIdGetOffset(id) + ItemIdGetLength(id) > BLCKSZ)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid line pointer at offset %u in btree page",
+						offset)));
 
 	itup = (IndexTuple) PageGetItem(page, id);
 
+	if (IndexTupleSize(itup) > ItemIdGetLength(id))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid index tuple length at offset %u in btree page",
+						offset)));
+
 	j = 0;
 	memset(nulls, 0, sizeof(nulls));
 	values[j++] = Int16GetDatum(offset);
@@ -572,7 +585,16 @@ bt_page_print_tuples(ua_page_items *uargs)
 		values[j++] = BoolGetDatum(ItemIdIsDead(id));
 	else
 	{
-		Assert(!ItemIdIsDead(id));
+		/*
+		 * LP_DEAD is never set on a pivot tuple in a valid page, but this
+		 * function accepts an arbitrary page image, so report a corrupt one
+		 * rather than tripping an assertion.
+		 */
+		if (ItemIdIsDead(id))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg("invalid pivot tuple at offset %u: line pointer marked dead",
+							offset)));
 		nulls[j++] = true;
 	}
 
diff --git a/contrib/pageinspect/expected/btree.out b/contrib/pageinspect/expected/btree.out
index 0aa5d73322f..4fe0eb9f894 100644
--- a/contrib/pageinspect/expected/btree.out
+++ b/contrib/pageinspect/expected/btree.out
@@ -211,6 +211,18 @@ SELECT bt_page_items(get_raw_page('test1', 0));
 ERROR:  input page is not a valid btree page
 SELECT bt_page_items(get_raw_page('test1_a_brin', 0));
 ERROR:  input page is not a valid btree page
+-- A corrupt line pointer must be reported, not read out of bounds.  All-ones is
+-- an invalid (out-of-range, unaligned) line pointer on any architecture.
+SELECT bt_page_items(set_byte(set_byte(set_byte(set_byte(get_raw_page('test1_a_idx', 1), 24, 255), 25, 255), 26, 255), 27, 255));
+ERROR:  invalid line pointer at offset 1 in btree page
+-- LP_DEAD is never set on a pivot tuple, so a page that has it is corrupt.
+-- Block 1 is the first leaf of a multi-page index, and its first item is the
+-- high key.  Byte 26 holds the lp_flags of that line pointer.
+CREATE TABLE test2 (a int);
+INSERT INTO test2 SELECT g FROM generate_series(1, 1000) g;
+CREATE INDEX test2_a_idx ON test2 (a);
+SELECT bt_page_items(set_byte(get_raw_page('test2_a_idx', 1), 26, 245));
+ERROR:  invalid pivot tuple at offset 1: line pointer marked dead
 \set VERBOSITY default
 -- Tests with all-zero pages.
 SHOW block_size \gset
@@ -219,3 +231,4 @@ SELECT bt_page_items(decode(repeat('00', :block_size), 'hex'));
 bt_page_items | 
 
 DROP TABLE test1;
+DROP TABLE test2;
diff --git a/contrib/pageinspect/expected/gist.out b/contrib/pageinspect/expected/gist.out
index 8502f9efb41..de2cf55aae1 100644
--- a/contrib/pageinspect/expected/gist.out
+++ b/contrib/pageinspect/expected/gist.out
@@ -80,6 +80,10 @@ SELECT gist_page_items_bytea(get_raw_page('test_gist', 0));
 ERROR:  input page is not a valid GiST page
 SELECT gist_page_items_bytea(get_raw_page('test_gist_btree', 0));
 ERROR:  input page is not a valid GiST page
+-- A corrupt line pointer must be reported, not read out of bounds.  All-ones is
+-- an invalid (out-of-range, unaligned) line pointer on any architecture.
+SELECT gist_page_items_bytea(set_byte(set_byte(set_byte(set_byte(get_raw_page('test_gist_idx', 0), 24, 255), 25, 255), 26, 255), 27, 255));
+ERROR:  invalid line pointer at offset 1 in GiST page
 \set VERBOSITY default
 -- Tests with all-zero pages.
 SHOW block_size \gset
diff --git a/contrib/pageinspect/expected/hash.out b/contrib/pageinspect/expected/hash.out
index ea387a68143..bedb746b9ed 100644
--- a/contrib/pageinspect/expected/hash.out
+++ b/contrib/pageinspect/expected/hash.out
@@ -193,6 +193,10 @@ SELECT hash_page_stats(get_raw_page('test_hash', 0));
 ERROR:  input page is not a valid hash page
 SELECT hash_page_type(get_raw_page('test_hash', 0));
 ERROR:  input page is not a valid hash page
+-- A corrupt line pointer must be reported, not read out of bounds.  All-ones is
+-- an invalid (out-of-range, unaligned) line pointer on any architecture.
+SELECT hash_page_items(set_byte(set_byte(set_byte(set_byte(get_raw_page('test_hash_a_idx', 3), 24, 255), 25, 255), 26, 255), 27, 255));
+ERROR:  invalid line pointer at offset 1 in hash page
 \set VERBOSITY default
 -- Tests with all-zero pages.
 SHOW block_size \gset
diff --git a/contrib/pageinspect/gistfuncs.c b/contrib/pageinspect/gistfuncs.c
index 8f127d41ec4..975ae24f73a 100644
--- a/contrib/pageinspect/gistfuncs.c
+++ b/contrib/pageinspect/gistfuncs.c
@@ -169,10 +169,24 @@ gist_page_items_bytea(PG_FUNCTION_ARGS)
 
 		id = PageGetItemId(page, offset);
 
-		if (!ItemIdIsValid(id))
-			elog(ERROR, "invalid ItemId");
+		/* Check that the line pointer and tuple lie within the page. */
+		if (!ItemIdHasStorage(id) ||
+			ItemIdGetOffset(id) != MAXALIGN(ItemIdGetOffset(id)) ||
+			ItemIdGetLength(id) < sizeof(IndexTupleData) ||
+			ItemIdGetOffset(id) + ItemIdGetLength(id) > BLCKSZ)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("invalid line pointer at offset %u in GiST page",
+							offset)));
 
 		itup = (IndexTuple) PageGetItem(page, id);
+
+		if (IndexTupleSize(itup) > ItemIdGetLength(id))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("invalid index tuple length at offset %u in GiST page",
+							offset)));
+
 		tuple_len = IndexTupleSize(itup);
 
 		memset(nulls, 0, sizeof(nulls));
@@ -274,11 +288,24 @@ gist_page_items(PG_FUNCTION_ARGS)
 
 		id = PageGetItemId(page, offset);
 
-		if (!ItemIdIsValid(id))
-			elog(ERROR, "invalid ItemId");
+		/* Check that the line pointer and tuple lie within the page. */
+		if (!ItemIdHasStorage(id) ||
+			ItemIdGetOffset(id) != MAXALIGN(ItemIdGetOffset(id)) ||
+			ItemIdGetLength(id) < sizeof(IndexTupleData) ||
+			ItemIdGetOffset(id) + ItemIdGetLength(id) > BLCKSZ)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("invalid line pointer at offset %u in GiST page",
+							offset)));
 
 		itup = (IndexTuple) PageGetItem(page, id);
 
+		if (IndexTupleSize(itup) > ItemIdGetLength(id))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("invalid index tuple length at offset %u in GiST page",
+							offset)));
+
 		index_deform_tuple(itup, tupdesc,
 						   itup_values, itup_isnull);
 
diff --git a/contrib/pageinspect/hashfuncs.c b/contrib/pageinspect/hashfuncs.c
index 30870329cc9..ddd4d8691f1 100644
--- a/contrib/pageinspect/hashfuncs.c
+++ b/contrib/pageinspect/hashfuncs.c
@@ -356,11 +356,24 @@ hash_page_items(PG_FUNCTION_ARGS)
 
 		id = PageGetItemId(uargs->page, uargs->offset);
 
-		if (!ItemIdIsValid(id))
-			elog(ERROR, "invalid ItemId");
+		/* Check that the line pointer and tuple lie within the page. */
+		if (!ItemIdHasStorage(id) ||
+			ItemIdGetOffset(id) != MAXALIGN(ItemIdGetOffset(id)) ||
+			ItemIdGetLength(id) < sizeof(IndexTupleData) ||
+			ItemIdGetOffset(id) + ItemIdGetLength(id) > BLCKSZ)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("invalid line pointer at offset %u in hash page",
+							uargs->offset)));
 
 		itup = (IndexTuple) PageGetItem(uargs->page, id);
 
+		if (IndexInfoFindDataOffset(itup->t_info) + sizeof(uint32) > ItemIdGetLength(id))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("invalid index tuple length at offset %u in hash page",
+							uargs->offset)));
+
 		j = 0;
 		values[j++] = Int32GetDatum((int32) uargs->offset);
 		values[j++] = PointerGetDatum(&itup->t_tid);
diff --git a/contrib/pageinspect/sql/btree.sql b/contrib/pageinspect/sql/btree.sql
index 102ebdefe3c..6111642ca58 100644
--- a/contrib/pageinspect/sql/btree.sql
+++ b/contrib/pageinspect/sql/btree.sql
@@ -53,6 +53,16 @@ SELECT bt_page_items('aaa'::bytea);
 CREATE INDEX test1_a_brin ON test1 USING brin(a);
 SELECT bt_page_items(get_raw_page('test1', 0));
 SELECT bt_page_items(get_raw_page('test1_a_brin', 0));
+-- A corrupt line pointer must be reported, not read out of bounds.  All-ones is
+-- an invalid (out-of-range, unaligned) line pointer on any architecture.
+SELECT bt_page_items(set_byte(set_byte(set_byte(set_byte(get_raw_page('test1_a_idx', 1), 24, 255), 25, 255), 26, 255), 27, 255));
+-- LP_DEAD is never set on a pivot tuple, so a page that has it is corrupt.
+-- Block 1 is the first leaf of a multi-page index, and its first item is the
+-- high key.  Byte 26 holds the lp_flags of that line pointer.
+CREATE TABLE test2 (a int);
+INSERT INTO test2 SELECT g FROM generate_series(1, 1000) g;
+CREATE INDEX test2_a_idx ON test2 (a);
+SELECT bt_page_items(set_byte(get_raw_page('test2_a_idx', 1), 26, 245));
 \set VERBOSITY default
 
 -- Tests with all-zero pages.
@@ -60,3 +70,4 @@ SHOW block_size \gset
 SELECT bt_page_items(decode(repeat('00', :block_size), 'hex'));
 
 DROP TABLE test1;
+DROP TABLE test2;
diff --git a/contrib/pageinspect/sql/gist.sql b/contrib/pageinspect/sql/gist.sql
index 85bc44b8000..9dd79ff9450 100644
--- a/contrib/pageinspect/sql/gist.sql
+++ b/contrib/pageinspect/sql/gist.sql
@@ -34,6 +34,9 @@ SELECT gist_page_opaque_info('aaa'::bytea);
 SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist', 0));
 SELECT gist_page_items_bytea(get_raw_page('test_gist', 0));
 SELECT gist_page_items_bytea(get_raw_page('test_gist_btree', 0));
+-- A corrupt line pointer must be reported, not read out of bounds.  All-ones is
+-- an invalid (out-of-range, unaligned) line pointer on any architecture.
+SELECT gist_page_items_bytea(set_byte(set_byte(set_byte(set_byte(get_raw_page('test_gist_idx', 0), 24, 255), 25, 255), 26, 255), 27, 255));
 \set VERBOSITY default
 
 -- Tests with all-zero pages.
diff --git a/contrib/pageinspect/sql/hash.sql b/contrib/pageinspect/sql/hash.sql
index e4b9e975f8a..b5a3f2767a7 100644
--- a/contrib/pageinspect/sql/hash.sql
+++ b/contrib/pageinspect/sql/hash.sql
@@ -100,6 +100,9 @@ SELECT hash_metapage_info(get_raw_page('test_hash', 0));
 SELECT hash_page_items(get_raw_page('test_hash', 0));
 SELECT hash_page_stats(get_raw_page('test_hash', 0));
 SELECT hash_page_type(get_raw_page('test_hash', 0));
+-- A corrupt line pointer must be reported, not read out of bounds.  All-ones is
+-- an invalid (out-of-range, unaligned) line pointer on any architecture.
+SELECT hash_page_items(set_byte(set_byte(set_byte(set_byte(get_raw_page('test_hash_a_idx', 3), 24, 255), 25, 255), 26, 255), 27, 255));
 \set VERBOSITY default
 
 -- Tests with all-zero pages.
-- 
2.37.1 (Apple Git-137.1)

