From 220acccaf8ed7c09cfa9ed0f5e882d22b76ebc48 Mon Sep 17 00:00:00 2001
From: Shihao Zhong <zhong950419@gmail.com>
Date: Sun, 30 Aug 2026 23:33:11 -0400
Subject: [PATCH] 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.
---
 contrib/pageinspect/btreefuncs.c       | 17 +++++++++++--
 contrib/pageinspect/expected/btree.out |  4 +++
 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      |  3 +++
 contrib/pageinspect/sql/gist.sql       |  3 +++
 contrib/pageinspect/sql/hash.sql       |  3 +++
 9 files changed, 82 insertions(+), 8 deletions(-)

diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c
index 9917663593b..acecd00eae5 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);
diff --git a/contrib/pageinspect/expected/btree.out b/contrib/pageinspect/expected/btree.out
index 0aa5d73322f..439568b69ee 100644
--- a/contrib/pageinspect/expected/btree.out
+++ b/contrib/pageinspect/expected/btree.out
@@ -211,6 +211,10 @@ 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
 \set VERBOSITY default
 -- Tests with all-zero pages.
 SHOW block_size \gset
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..e3695ce2565 100644
--- a/contrib/pageinspect/sql/btree.sql
+++ b/contrib/pageinspect/sql/btree.sql
@@ -53,6 +53,9 @@ 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));
 \set VERBOSITY default
 
 -- Tests with all-zero pages.
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)

