Claude Code found a long standing race condition bug in multi-level page deletion (deletion that removes a subtree including at least one internal page, in addition to a leaf page). We aren't sufficiently careful about a concurrent page split in the half-dead leaf page's left sibling, at the point where we delete an internal target page (an ancestor of that half-dead page). Such a concurrent split can change the half-dead page's left sibling link during a window when no lock is held on any leaf page. When that happens, the WAL record describing how to change the half-dead leaf page (as part of deleting the target internal page) will change the half-dead page's left link field to its pre-split sibling's block number, not to its new left sibling block number, which is wrong -- wal_consistency_checking fails. This bug dates all the way back to the page deletion overhaul introduced in Postgres 9.4.
The bug is probably very unlikely to affect users. Original execution correctly doesn't change the half-dead page's left sibling link at all. The reason we need to change its left sibling during recovery is because its buffer is REGBUF_WILL_INIT for efficiency reasons. So not only does this bug require a concurrent split at just the right/wrong time, with a multi-level page deletion (both of which are unlikely in general): even after that happens, the resulting corruption on standbys only persists until the half-dead page is marked fully deleted -- which typically happens immediately after the corruption is introduced. Attached v1-0001-* patch fixes the bug. v1-0002-* is the test case that Claude Code authored to prove the bug exists. I doubt that the test case is worth committing. -- Peter Geoghegan
v1-0002-Add-test-for-stale-left-link-logged-by-btree-page.patch
Description: Binary data
v1-0001-Re-read-sibling-links-during-nbtree-page-unlink.patch
Description: Binary data
