diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index f012e99c9d7..a91082efb9c 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -787,6 +787,14 @@ ReplicationSlotRelease(void) * decoding be disabled. */ ReplicationSlotDropAcquired(is_logical); + + /* + * The slot's array entry is now free and may be reused by another + * backend at any moment. This injection point lets tests open that + * window deterministically and verify we never touch the (now stale) + * slot pointer afterwards. + */ + INJECTION_POINT("ephemeral-slot-release-after-drop", NULL); } /* diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl index 97d11f98b59..fa5a0222dcd 100644 --- a/src/test/recovery/t/006_logical_decoding.pl +++ b/src/test/recovery/t/006_logical_decoding.pl @@ -275,6 +275,80 @@ is( $node_primary->safe_psql( qq(Check that reset timestamp is later after resetting stats for slot '$stats_test_slot1' again.) ); +# Regression test for the race in ReplicationSlotRelease() when releasing an +# ephemeral slot. Releasing an ephemeral slot first drops it via +# ReplicationSlotDropAcquired(), which frees the slot's array entry for reuse. +# ReplicationSlotRelease() must not touch that (now stale) slot pointer +# afterwards, or it can corrupt an unrelated slot that grabbed the freed entry. +# This needs an injection point, so it only runs on builds that support them. +SKIP: +{ + skip "Injection points not supported by this build", 2 + if $ENV{enable_injection_points} ne 'yes'; + skip "Extension injection_points not installed", 2 + unless $node_primary->check_extension('injection_points'); + + $node_primary->safe_psql('postgres', q(CREATE EXTENSION injection_points)); + + # Freeze a backend right after it drops an ephemeral slot, i.e. once the + # slot's array entry has been freed and is available for reuse. + $node_primary->safe_psql('postgres', + q{SELECT injection_points_attach('ephemeral-slot-release-after-drop', 'wait')} + ); + + # Create a logical slot with a non-existent output plugin. The slot is + # created as RS_EPHEMERAL; the bogus plugin makes creation ERROR out, and + # the error path calls ReplicationSlotRelease(), which drops the ephemeral + # slot and then blocks on the injection point. on_error_stop => 0 keeps the + # session alive past the ERROR so it can still report 'done_release'. + my $dropper = + $node_primary->background_psql('postgres', on_error_stop => 0); + $dropper->query_until( + qr/start_drop/, q( +\echo start_drop +SELECT pg_create_logical_replication_slot('test_slot_dropped', 'no_such_plugin', false); +\echo done_release +)); + + # Wait until the backend is parked on the injection point, with the array + # entry of 'test_slot_dropped' freed and up for grabs. + $node_primary->wait_for_event('client backend', + 'ephemeral-slot-release-after-drop'); + + # A second backend creates a new persistent slot, which reuses the just + # freed array entry -- the very entry the frozen backend still points at. + $node_primary->safe_psql('postgres', + q{SELECT pg_create_physical_replication_slot('test_slot_created', true, false)} + ); + + my $before = $node_primary->safe_psql('postgres', + q{SELECT inactive_since FROM pg_replication_slots WHERE slot_name = 'test_slot_created'} + ); + + # Let the frozen backend finish releasing. With the bug it now scribbles on + # the reused entry; with the fix it leaves it untouched. + $node_primary->safe_psql('postgres', + q{SELECT injection_points_wakeup('ephemeral-slot-release-after-drop')}); + $dropper->query_until(qr/done_release/, ''); + + my $after = $node_primary->safe_psql('postgres', + q{SELECT inactive_since FROM pg_replication_slots WHERE slot_name = 'test_slot_created'} + ); + + is($after, $before, + 'releasing an ephemeral slot must not modify a slot that reused its entry' + ); + + my $slot = $node_primary->safe_psql('postgres', + q{SELECT slot_name, slot_type FROM pg_replication_slots WHERE slot_name = 'test_slot_created'} + ); + is($slot, "test_slot_created|physical", 'reused slot is intact'); + + $dropper->quit; + $node_primary->safe_psql('postgres', + q{SELECT injection_points_detach('ephemeral-slot-release-after-drop')}); +} + # done with the node $node_primary->stop;