--- id: pg-fix-forwarder-hardening name: Forwarder TOCTOU restructure + hang-shaped arms (review 004 items 4+6) status: pending depends_on: [] scope: broad risk: high impact: component level: implementation tags: [review-004-fixes, postgres-engine, forwarder] --- ## Description Forwarder surgery that should ride its own session, per `docs/reviews/004-pre-release-general-review.md` §"Findings recorded, not fixed" items 4 and 6 (the review bundles them as the forwarder-hardening session). Line refs are as of the review; the inline fixes may have shifted them slightly — re-locate on pickup. **(a) Register/unregister TOCTOU (item 4).** The registry refcount and the LISTEN/UNLISTEN command emission are not one critical section (`forwarder.rs:131-176` vs `438-482`): interleaved register/unregister across tasks can leave a live refcount with the server-side LISTEN cleared (both queued commands share a generation, so no stale check saves it) until the next reconnect re-issue. Fix shape: a decide-and-queue restructure inside the forwarder — the register/unregister decision and the derived command enqueue must be atomic with respect to each other (decide under the registry guard; queue the command *with* the refcount state it was derived from). **(b) Generation-verify/LISTEN failure parks forever (item 6, arm 1).** A failed generation-verify/LISTEN batch is treated as connection death and parks on `poll_task.await` forever even though the session may be alive (`forwarder.rs:698-702/803-808`). **(c) Shutdown can leak the listener session (item 6, arm 2).** `close()`/`Drop` can leak the listener session past shutdown when the serve loop is awaiting a command on a black-holed connection (the `run_command` await sits outside the shutdown `select`, `:781`; no timeout on this connection). Treatment family for (b)+(c): timeouts/bounded waits on the listener path. **Preserve while restructuring** (the gate will check these): the two POC-pinned deadlock pitfalls stay structurally excluded; the reconnect invariants from `pg-fix-forwarder-reconnect` (the retry core, the fanout release guard, receivers-terminal-iff-loop-gone) stay green; the register-failure unregister fix from review 004 (inline fix 3) stays; the poison-recovery lock idiom from review 004 (inline fix 4) stays. Use the existing test-seam vocabulary (the reconnect-config slot, the connector injection, the generation tests) for the new pins. ## Acceptance Criteria - [ ] The TOCTOU is closed: register/unregister interleavings across tasks cannot leave a live refcount with the server-side LISTEN cleared — pinned by a test that interleaves the two and asserts the server-side LISTEN state matches the refcount - [ ] A failed generation-verify/LISTEN batch no longer parks forever: bounded wait with an honest error or reconnect — pinned by test - [ ] `close()`/`Drop` reclaims the listener session even when the serve loop is stuck awaiting on a dead connection — pinned by test (black-holed connection seam) - [ ] The preserved postures listed above re-verified green (existing forwarder pins: reconnect survival, no-replay, shutdown close, stale-UNLISTEN generation tests, the register-failure unregister pin) - [ ] `cargo test -p alkstore-postgres` (harness server), clippy `-D warnings`, fmt clean; workspace gates green server-less ## References - docs/reviews/004-pre-release-general-review.md §"Findings recorded, not fixed" items 4 and 6 (the findings, the fix shapes, the own-session scoping) - docs/architecture/decisions/004-postgres-driver.md (the forwarder's owned failure modes, the two POC-pinned deadlock pitfalls) - tasks/pg-fix-forwarder-reconnect.md (the retry core, the fanout guard, the test-seam vocabulary) - tasks/pg-fix-stale-unlisten.md (the generation/staleness machinery the TOCTOU restructure must compose with) ## Notes > To be filled by implementation agent ## Summary > To be filled on completion