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