Thread
-
Re: [PATCH] pg_surgery: Fix WAL corruption from concurrent heap_force_kill
Fabrízio de Royes Mello <fabriziomello@gmail.com> — 2026-05-05T15:11:54Z
On Mon, Apr 20, 2026 at 5:20 PM Fabrízio de Royes Mello < fabriziomello@gmail.com> wrote: > > Hi hackers, > > I found a bug in pg_surgery's heap_force_kill that can produce WAL records with incorrect CRC checksums when multiple sessions operate on heap pages that share the same visibility map page. > > When heap_force_kill kills a tuple on an all-visible page, it calls visibilitymap_clear() to clear the VM bits, then later calls log_newpage_buffer(vmbuf) to write a full-page image of the VM page to WAL. However, visibilitymap_clear() releases the exclusive lock on the VM buffer before returning -- that's its normal API contract. The subsequent log_newpage_buffer(vmbuf) then writes the FPI without holding any content lock on the VM buffer. > > XLogInsert reads the page content twice via the XLogRecData chain pointer to shared memory: once to compute the CRC in XLogRecordAssemble, and once to copy the data to WAL buffers in CopyXLogRecordToWAL. If a concurrent backend modifies the VM page between those two reads (e.g., another heap_force_kill session clearing a different bit on the same VM page), the CRC will not match the written bytes. > > Fix it by re-acquiring the VM buffer exclusive lock immediately after visibilitymap_clear() returns, and release it after log_newpage_buffer() completes. The lock is only acquired when RelationNeedsWAL() is true, since unlogged relations skip the FPI write entirely. > > The patch includes a TAP test that uses injection points to deterministically reproduce the race condition. Three injection points are used: > * "heap-force-kill-vm-pin" in heap_surgery.c (outside the critical section) to initialize DSM shared memory for the wait machinery. > * "heap-force-kill-before-vm-wal" in heap_surgery.c (inside the critical section, between the heap FPI and VM FPI writes) as a synchronization barrier. > * "wal-insert-after-crc" in xloginsert.c (inside XLogInsert, between XLogRecordAssemble and XLogInsertRecord) to pause after CRC computation but before the data copy. > > The test pauses session 1 inside XLogInsert after the VM FPI's CRC is computed, runs a concurrent heap_force_kill from session 2 on the same VM page, then wakes session 1. Without the fix, session 2 modifies the VM page between CRC and copy, and pg_walinspect reports "incorrect resource manager data checksum". With the fix, session 2 blocks on the VM buffer lock and no corruption occurs. > > The "wal-insert-after-crc" injection point in xloginsert.c uses INJECTION_POINT_CACHED (pre-loaded by the caller before the critical section), so it only fires when explicitly loaded and adds no overhead to the normal WAL write path. > Rebased version. -- Fabrízio de Royes Mello
-
Re: [PATCH] pg_surgery: Fix WAL corruption from concurrent heap_force_kill
Zsolt Parragi <zsolt.parragi@percona.com> — 2026-05-05T20:42:43Z
Hello! I verified both the patch, and that the test case catches the bug without the patch, the overall change seems good to me. + if (did_modify_vm) + INJECTION_POINT_CACHED("heap-force-kill-before-vm-wal", NULL); + if (did_modify_vm && RelationNeedsWAL(rel)) + { log_newpage_buffer(vmbuf, false); + LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK); + } Is the additional if intentional here? Based on the name it seems like it could be simply part of the next if, currently it also fires for unlogged tables. +# Give session 2 time to reach the VM buffer lock (or complete if +# unfixed). We cannot reliably detect blocking from Perl, so just +# sleep briefly. +use Time::HiRes qw(usleep); +usleep(500_000); Wouldn't this be possibly unstable on CI? The following seems to work for me, both for detecting the issue in the unpatched version and to result in a quicker continuation in the patched version: (~1.55sec total execution time compared to ~2sec original) (with the patch, s2 wait for the loc, without the patch it finishes and becomes idle) my $s2 = $node->background_psql('postgres'); my $s2_pid = $s2->query_safe(q{SELECT pg_backend_pid()}); chomp $s2_pid; $s2->query_until(qr/starting_s2/, q(\echo starting_s2 SELECT heap_force_kill('test_vm'::regclass, ARRAY['(1,1)']::tid[]); \echo s2_done )); use Time::HiRes qw(usleep); my $observed = ''; my $deadline = time() + 10; while (time() < $deadline) { $observed = $node->safe_psql('postgres', qq{ SELECT format('%s/%s/%s', coalesce(wait_event_type, 'NULL'), coalesce(wait_event, 'NULL'), coalesce(state, 'NULL')) FROM pg_stat_activity WHERE pid = $s2_pid }); last if $observed =~ m{^Buffer/BufferExclusive/} || $observed =~ m{/idle$}; usleep(50_000); } -
Re: [PATCH] pg_surgery: Fix WAL corruption from concurrent heap_force_kill
Fabrízio de Royes Mello <fabriziomello@gmail.com> — 2026-05-06T20:27:25Z
On Tue, May 5, 2026 at 5:42 PM Zsolt Parragi <zsolt.parragi@percona.com> wrote: > > Hello! > > I verified both the patch, and that the test case catches the bug > without the patch, the overall change seems good to me. > Thanks for your review. > + if (did_modify_vm) > + INJECTION_POINT_CACHED("heap-force-kill-before-vm-wal", NULL); > + > if (did_modify_vm && RelationNeedsWAL(rel)) > + { > log_newpage_buffer(vmbuf, false); > + LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK); > + } > > Is the additional if intentional here? Based on the name it seems like > it could be simply part of the next if, currently it also fires for > unlogged tables. > My bad it is a leftover. Fixed. > +# Give session 2 time to reach the VM buffer lock (or complete if > +# unfixed). We cannot reliably detect blocking from Perl, so just > +# sleep briefly. > +use Time::HiRes qw(usleep); > +usleep(500_000); > > Wouldn't this be possibly unstable on CI? > Yes it should be somewhat flaky. TBH, I'm not sure if the test will be pushed with the fix, but anyway let's improve it. > The following seems to work for me, both for detecting the issue in > the unpatched version and to result in a quicker continuation in the > patched version: (~1.55sec total execution time compared to ~2sec > original) > > (with the patch, s2 wait for the loc, without the patch it finishes > and becomes idle) > > my $s2 = $node->background_psql('postgres'); > my $s2_pid = $s2->query_safe(q{SELECT pg_backend_pid()}); > chomp $s2_pid; > > $s2->query_until(qr/starting_s2/, > q(\echo starting_s2 > SELECT heap_force_kill('test_vm'::regclass, ARRAY['(1,1)']::tid[]); > \echo s2_done > )); > > use Time::HiRes qw(usleep); > my $observed = ''; > my $deadline = time() + 10; > while (time() < $deadline) { > $observed = $node->safe_psql('postgres', qq{ > SELECT format('%s/%s/%s', > coalesce(wait_event_type, 'NULL'), > coalesce(wait_event, 'NULL'), > coalesce(state, 'NULL')) > FROM pg_stat_activity WHERE pid = $s2_pid > }); > last if $observed =~ m{^Buffer/BufferExclusive/} > || $observed =~ m{/idle$}; > usleep(50_000); > } Thanks, see the newly attached version (v2) with proposed changes. Regards, -- Fabrízio de Royes Mello On behalf of PlanetScale -
Re: [PATCH] pg_surgery: Fix WAL corruption from concurrent heap_force_kill
Melanie Plageman <melanieplageman@gmail.com> — 2026-05-06T23:54:36Z
On Wed, May 6, 2026 at 4:27 PM Fabrízio de Royes Mello <fabriziomello@gmail.com> wrote: > > On Tue, May 5, 2026 at 5:42 PM Zsolt Parragi <zsolt.parragi@percona.com> wrote: > > > > I verified both the patch, and that the test case catches the bug > > without the patch, the overall change seems good to me. I haven't reviewed the test, but I think heap_force_common() has bigger problems than what your fix fixes. Definitely the lock on the vmbuffer needs to be held not just while clearing the VM but also while emitting WAL for those changes. But, actually, even more than that, the changes to the heap and the accompanying changes to the VM page should be logged in the same WAL record. It is forbidden for PD_ALL_VISIBLE to be clear while the VM is set -- which is the state we are in after replaying the WAL record clearing PD_ALL_VISIBLE before replaying the one clearing the VM bits. You need all the changes to happen atomically in one record. This is briefly discussed as part of a larger bug fix I am working on to correctly WAL log VM clears [1]. I propose a fix there that emits a single WAL record for the heap and VM changes while holding a lock on both pages. - Melanie [1] https://www.postgresql.org/message-id/flat/CAAKRu_Z_KAAZAHtuorifR2MRc_MkEcHf-C_t4b9HZaHpa3nriw%40mail.gmail.com#61d104a35fc95fa67708c060ec61d59b