Fix race in ReplicationSlotRelease for ephemeral slots

First seen: 2026-05-27 11:50:16+00:00 · Messages: 5 · Participants: 3

Latest Update

2026-06-01 · claude-opus-4-6

Fix Race in ReplicationSlotRelease for Ephemeral Slots

Core Problem

The bug exists in ReplicationSlotRelease(), a function responsible for releasing a backend's hold on a replication slot. The function has a critical use-after-free-style race condition specific to ephemeral slots — slots that are automatically dropped when released (used for transient logical replication operations like pg_create_logical_replication_slot() with the temporary flag).

The Race Condition in Detail

The execution flow in ReplicationSlotRelease() for ephemeral slots is:

  1. Call ReplicationSlotDropAcquired() — this marks the slot's shared memory entry as free in the ReplicationSlotCtl->replication_slots[] array.
  2. Execute common post-release cleanup code that dereferences the now-freed slot pointer to update shared memory fields like active_proc (set to NULL) and effective_xmin (potentially reset).

Between steps 1 and 2, another backend can immediately allocate the same slot array entry for a completely new, unrelated replication slot. The original backend then blindly writes to that memory, corrupting the new slot's state:

This is a classic TOCTOU (time-of-check-time-of-use) problem in shared memory slot management. The slot array is a fixed-size shared memory structure (max_replication_slots entries), and entries are reused via a linear scan for free entries — making reuse of recently freed entries likely under load.

Architectural Significance

Replication slots are critical infrastructure: they prevent WAL removal and catalog cleanup that consumers need. Corrupting a slot's effective_xmin could allow premature vacuum of rows still needed by a logical subscriber, causing data loss or replication failures. Corrupting active_proc violates mutual exclusion, potentially allowing concurrent access to slot state that assumes single-writer semantics.

Proposed Solution

The fix is structurally simple: wrap the post-drop shared memory cleanup code in a conditional that only executes for non-ephemeral slots. For ephemeral slots, once ReplicationSlotDropAcquired() returns, the function must not touch the slot's shared memory state at all because the slot no longer conceptually exists.

if (!slot_was_ephemeral)
{
    /* Safe to update shared memory — slot still exists */
    SpinLockAcquire(&slot->mutex);
    slot->active_pid = 0;
    SpinLockRelease(&slot->mutex);
    /* ... effective_xmin updates ... */
}

This is correct because:

Backpatching Considerations

Fujii confirmed this should be backpatched to all supported branches, indicating the bug has existed since ephemeral slots were introduced. The fix is minimal and low-risk, making it appropriate for stable branch inclusion.

Testing Debate

A secondary discussion arose about whether to add a regression test:

The decision was deferred to Fujii as the committer.

Key Design Insight

The fundamental issue is that ReplicationSlotRelease() conflated two distinct operations into one code path:

  1. Release ownership of a slot that continues to exist (persistent/temporary slots)
  2. Destroy a slot that should cease to exist (ephemeral slots)

These have fundamentally different post-conditions regarding shared memory validity. The fix correctly separates these paths. A more defensive future approach might be to NULL out the local slot pointer immediately after the drop call to make any subsequent dereference a clear programming error (crash rather than silent corruption).