Hi Peter, Yes, I agree that this is real corruption.
The race also seems to have been introduced by fd83c83d0. Before that change this path used ginTraverseLock(), which explicitly rechecks GinPageIsLeaf() after the share-to-exclusive relock. Restoring that call should fix the race without changing the intended locking model. This looks strikingly similar to BUG #16792 [0]. That report involved PostgreSQL 11 under heavy update and autovacuum load. GIN scans returned rows that did not match the indexed value, REINDEX fixed the problem temporarily, and it returned after some weeks. Heikki eventually established that the index contained an extra entry for an old key and developed a version of the GIN amcheck code while investigating it, but AFAICS the underlying cause was not found. A stale posting-tree TID surviving VACUUM and later referring to a recycled heap line pointer would explain the extra match in that report. The old case also had the correct new key missing, so I cannot say that it was necessarily the same bug. But the version, workload and symptoms seem close enough to make the connection worth noting. The current gin_index_check() would probably not detect the corruption shown by this test. In a nearby thread I propose index-all-keys-match, which probably could find this. Thanks for finding this! Best regards, Andrey Borodin. [0] https://www.postgresql.org/message-id/flat/16792-b1913b6b4e098331%40postgresql.org
