amcheck: Fix parent key check in gin_index_check()
authorTomas Vondra <[email protected]>
Tue, 17 Jun 2025 13:46:26 +0000 (15:46 +0200)
committerTomas Vondra <[email protected]>
Tue, 17 Jun 2025 13:46:29 +0000 (15:46 +0200)
The checks introduced by commit 14ffaece0fb5 did not get the parent key
checks quite right, missing some data corruption cases. In particular:

* The "rightlink" check was not working as intended, because rightlink
  is a BlockNumber, and InvalidBlockNumber is 0xFFFFFFFF, so

    !GinPageGetOpaque(page)->rightlink

  almost always evaluates to false (except for rightlink=0). So in most
  cases parenttup was left NULL, preventing any checks against parent.

* Use GinGetDownlink() to retrieve child blkno to avoid triggering
  Assert, same as the core GIN code.

Issues reported by Arseniy Mukhin, along with a proposed patch. Review
by Andrey M. Borodin, cleanup and improvements by me.

Author: Arseniy Mukhin <[email protected]>
Reviewed-by: Andrey M. Borodin <[email protected]>
Discussion: https://postgr.es/m/CAE7r3MJ611B9TE=YqBBncewp7-k64VWs+sjk7XF6fJUX77uFBA@mail.gmail.com

contrib/amcheck/t/006_verify_gin.pl
contrib/amcheck/verify_gin.c

index 7fdde170e06fb75e4739dd3e697ddf8a89b66dd3..308e53b2f75d0c6629ad940643018061818748f5 100644 (file)
@@ -34,6 +34,8 @@ $node->safe_psql(
 invalid_entry_order_leaf_page_test();
 invalid_entry_order_inner_page_test();
 invalid_entry_columns_order_test();
+inconsistent_with_parent_key__parent_key_corrupted_test();
+inconsistent_with_parent_key__child_key_corrupted_test();
 
 sub invalid_entry_order_leaf_page_test
 {
@@ -159,6 +161,82 @@ sub invalid_entry_columns_order_test
    like($stderr, qr/$expected/);
 }
 
+sub inconsistent_with_parent_key__parent_key_corrupted_test
+{
+   my $relname = "test";
+   my $indexname = "test_gin_idx";
+
+   # fill the table until we have a split
+   $node->safe_psql(
+       'postgres', qq(
+       DROP TABLE IF EXISTS $relname;
+       CREATE TABLE $relname (a text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'llllllllll' || random_string($filler_size) ||'}')::text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'mmmmmmmmmm' || random_string($filler_size) ||'}')::text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'nnnnnnnnnn' || random_string($filler_size) ||'}')::text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'xxxxxxxxxx' || random_string($filler_size) ||'}')::text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'yyyyyyyyyy' || random_string($filler_size) ||'}')::text[]);
+       CREATE INDEX $indexname ON $relname USING gin (a);
+   ));
+   my $relpath = relation_filepath($indexname);
+
+   $node->stop;
+
+   my $blkno = 1;  # root
+
+   # we have nnnnnnnnnn... as parent key in the root, so replace it with something smaller then child's keys
+   string_replace_block(
+       $relpath,
+       'nnnnnnnnnn',
+       'aaaaaaaaaa',
+       $blkno
+   );
+
+   $node->start;
+
+   my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT gin_index_check('$indexname')));
+   my $expected = "index \"$indexname\" has inconsistent records on page 3 offset 3";
+   like($stderr, qr/$expected/);
+}
+
+sub inconsistent_with_parent_key__child_key_corrupted_test
+{
+   my $relname = "test";
+   my $indexname = "test_gin_idx";
+
+   # fill the table until we have a split
+   $node->safe_psql(
+       'postgres', qq(
+       DROP TABLE IF EXISTS $relname;
+       CREATE TABLE $relname (a text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'llllllllll' || random_string($filler_size) ||'}')::text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'mmmmmmmmmm' || random_string($filler_size) ||'}')::text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'nnnnnnnnnn' || random_string($filler_size) ||'}')::text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'xxxxxxxxxx' || random_string($filler_size) ||'}')::text[]);
+       INSERT INTO $relname (a) VALUES (('{' || 'yyyyyyyyyy' || random_string($filler_size) ||'}')::text[]);
+       CREATE INDEX $indexname ON $relname USING gin (a);
+    ));
+   my $relpath = relation_filepath($indexname);
+
+   $node->stop;
+
+   my $blkno = 3;  # leaf
+
+   # we have nnnnnnnnnn... as parent key in the root, so replace child key with something bigger
+   string_replace_block(
+       $relpath,
+       'nnnnnnnnnn',
+       'pppppppppp',
+       $blkno
+   );
+
+   $node->start;
+
+   my ($result, $stdout, $stderr) = $node->psql('postgres', qq(SELECT gin_index_check('$indexname')));
+   my $expected = "index \"$indexname\" has inconsistent records on page 3 offset 3";
+   like($stderr, qr/$expected/);
+}
+
 # Returns the filesystem path for the named relation.
 sub relation_filepath
 {
index 3f7994a6bc698d366ea7c97939df26342fe7c662..fb17e4613c6eb2f7b531bd4a6fc5c2cbb60fbe0d 100644 (file)
@@ -608,10 +608,10 @@ gin_check_parent_keys_consistency(Relation rel,
                ptr = (GinScanItem *) palloc(sizeof(GinScanItem));
                ptr->depth = stack->depth + 1;
                /* last tuple in layer has no high key */
-               if (i != maxoff && !GinPageGetOpaque(page)->rightlink)
-                   ptr->parenttup = CopyIndexTuple(idxtuple);
-               else
+               if (i == maxoff && rightlink == InvalidBlockNumber)
                    ptr->parenttup = NULL;
+               else
+                   ptr->parenttup = CopyIndexTuple(idxtuple);
                ptr->parentblk = stack->blkno;
                ptr->blkno = GinGetDownlink(idxtuple);
                ptr->next = stack->next;
@@ -748,7 +748,7 @@ gin_refind_parent(Relation rel, BlockNumber parentblkno,
        ItemId      p_iid = PageGetItemIdCareful(rel, parentblkno, parentpage, o);
        IndexTuple  itup = (IndexTuple) PageGetItem(parentpage, p_iid);
 
-       if (ItemPointerGetBlockNumber(&(itup->t_tid)) == childblkno)
+       if (GinGetDownlink(itup) == childblkno)
        {
            /* Found it! Make copy and return it */
            result = CopyIndexTuple(itup);