From 8d86317cf30cdd15044f7834e5141216c5f1bb1e Mon Sep 17 00:00:00 2001
From: snppg <slpmcf@gmail.com>
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

