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

Reply via email to