From 220acccaf8ed7c09cfa9ed0f5e882d22b76ebc48 Mon Sep 17 00:00:00 2001 From: Shihao Zhong 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)