Hi, gin_index_check() compares adjacent separator keys on posting-tree internal pages with ItemPointerCompare(...) < 0, so it reports out-of-order keys but misses duplicates.
Equal separators should not occur: each PostingItem key is the child page's right bound (dataPrepareDownlink), and leaf TIDs are strictly increasing, so siblings must be strictly ascending. dataLocateItem also treats equal keys as an exact match to that downlink. Entry-tree checks already reject equals (>= 0); posting trees should do the same. The attached patch changes the comparison to <= 0 and adds a TAP test that corrupts two adjacent keys to be equal. Best regards, Stepan Neretin.
From 8d86317cf30cdd15044f7834e5141216c5f1bb1e Mon Sep 17 00:00:00 2001 From: snppg <[email protected]> Date: Wed, 15 Jul 2026 18:58:47 +0700 Subject: [PATCH] amcheck: Detect duplicate PostingItem keys in GIN posting trees gin_index_check() used a strict < comparison for adjacent separator keys, so equal keys were silently accepted. GIN posting-tree separators must be strictly ascending (each key is a child right bound), matching the entry-tree checks that already reject equals. --- contrib/amcheck/t/006_verify_gin.pl | 30 +++++++++++++++++++++++++++++ contrib/amcheck/verify_gin.c | 3 ++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/contrib/amcheck/t/006_verify_gin.pl b/contrib/amcheck/t/006_verify_gin.pl index 93571f1d2ab..d720a3b3f13 100644 --- a/contrib/amcheck/t/006_verify_gin.pl +++ b/contrib/amcheck/t/006_verify_gin.pl @@ -37,6 +37,7 @@ invalid_entry_columns_order_test(); inconsistent_with_parent_key__parent_key_corrupted_test(); inconsistent_with_parent_key__child_key_corrupted_test(); inconsistent_with_parent_key__parent_key_corrupted_posting_tree_test(); +equal_posting_tree_internal_keys_test(); sub invalid_entry_order_leaf_page_test { @@ -252,6 +253,35 @@ sub inconsistent_with_parent_key__parent_key_corrupted_posting_tree_test like($stderr, qr/$expected/); } +# Equal adjacent PostingItem keys on posting-tree root (block 2). +sub equal_posting_tree_internal_keys_test +{ + my $indexname = "test_gin_idx"; + + $node->safe_psql( + 'postgres', qq( + DROP TABLE IF EXISTS test; + CREATE TABLE test (a text[]); + INSERT INTO test (a) SELECT ('{aaaaa}') FROM generate_series(1, 30000); + CREATE INDEX $indexname ON test USING gin (a); + )); + my $relpath = relation_filepath($indexname); + + $node->stop; + + # PostingItems start at offset 32; set first key equal to second. + my $find = qr/\A(.{32})(.{4})(.{6})(.{4})(.{6})/s; + my $replace = '$1$2$5$4$5'; + string_replace_block($relpath, $find, $replace, 2); + + $node->start; + + my ($result, $stdout, $stderr) = + $node->psql('postgres', qq(SELECT gin_index_check('$indexname'))); + like($stderr, + qr/index "$indexname" has wrong tuple order in posting tree, block 2, offset 2/); +} + # Returns the filesystem path for the named relation. sub relation_filepath diff --git a/contrib/amcheck/verify_gin.c b/contrib/amcheck/verify_gin.c index fa06689ed5b..9b01b215b5c 100644 --- a/contrib/amcheck/verify_gin.c +++ b/contrib/amcheck/verify_gin.c @@ -334,7 +334,8 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting { PostingItem *previous_posting_item = GinDataPageGetPostingItem(page, i - 1); - if (ItemPointerCompare(&posting_item->key, &previous_posting_item->key) < 0) + /* Reject out-of-order and duplicate separator keys. */ + if (ItemPointerCompare(&posting_item->key, &previous_posting_item->key) <= 0) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" has wrong tuple order in posting tree, block %u, offset %u", -- 2.55.0
