Publish a handover connection only after releasing its mutex - #183
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughThe patch adds public connection-count APIs and changes worker handover synchronization. It also adds dedicated detached HTTP and WebSocket cleanup paths, with guarded slot release during normal cleanup and shutdown. ChangesConnection lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Fixes a use-after-free of a connection's mutex that aborts the relay. Pre-existing and identical upstream; unchanged by the recent handover-list work.
The bug
swapListruns on a thread-pool thread and holdshttp_conn._mutacross its whole body, including the finalhandover_list.insert. That insert is the publish: it makes the connection visible to the event-loop thread, which can snapshot the handover list and release the connection (releaseHandoverreturns theHTTPConnto its pool or destroys it) at any moment afterwards.handover_list.insertreleases the list mutex beforeswapList's deferred_mut.unlockruns. So:Nothing synchronizes those:
processSignalnever takes_mut. Reaching the window needs the loop already awake on some other connection's signal, since this connection's own signal is sent afterswapListreturns. With several thread-pool threads that is routine.Reproduced, not inferred
Compiled a probe into
HTTPConnPool.releasethat readsconn._mut.stateand reports when the pool takes ownership of a still-locked mutex, then widened the window with a 2ms delay between the insert and the unlock to make the interleaving reliable. Load was 2000 interleaved WebSocket upgrades andConnection: closerequests.The panic lands exactly where the analysis predicts, in
Mutex.unlock:"switch on corrupt value" is the mutex memory no longer holding a valid
State, because theHTTPConnwas recycled underneath it. In ReleaseSafe that is an abort, so a remote peer driving handover churn can restart the relay; in ReleaseFast (the release build) there is no check and it is a silent write into recycled memory.Honest limit on this evidence: the 2ms delay is what makes it reproducible. It proves the mechanism and the consequence, not the natural rate. An earlier run without the delay produced zero hits, but its load generator was too slow to create meaningful handover churn, so that is not evidence of rarity either.
The fix
Publishing is now the last thing that touches the connection, and happens after
_mutis released. The locked region moves intoswapListLocked;swapListreleases the mutex and only then inserts intohandover_list. The caller's followingloop.signal()deliberately touches neitherconnnorhttp_conn, so it stays safe.The gap this opens, between the unlock and the insert, is harmless: the connection is in no list and its state is already
.handover, which is exactly whatrun()'s.recvhandler checks for and skips.Also here
Corrects a comment I misplaced in #181. It was meant for
disown()but a first-match replacement put it onswapList, where its reasoning ("both callers: run()'s parse-error path and accept()'s errdefer") describesdisown's callers, notswapList's. Both switches now carry the reasoning that actually applies to them.Verification
zig build test65/65, handover-leak test 6/6, protocol 45/45, concurrency 2/2 at 200 connections,verify-vendored-httpz.shclean, zero panics or corruption in any relay log.Review follow-up: the first fix was incomplete
Both reviews independently found that moving the handover publish fixed only one of two paths, and my own repro confirmed it.
swapListalso publishes.keepalive(and.request) from inside the critical section, and the event-loop thread reaches those throughdisown()andcloseList(), neither of which takes_mut. So the same use-after-free survived.Reproduced it the same way, with
http_keepalive_timeout_s = 0so every keepalive conn expires on insert: 57 probe hits and the relay aborted, same panic, now from the.keepalivecall site.Hoisting the keepalive insert the way the handover one was hoisted is not a valid fix, and both reviews said so independently.
run()'s.recvskips a.handoverconn but not a.keepaliveone, so a gap with.keepalivestate and no list membership would let a pipelined request drivekeepalive_list.removeon a non-member, nullingheadandtailand wiping the live list. That trades a rare use-after-free for a reachable list wipe.So the wait goes where every free funnels through:
HTTPConnPool.releasetakes and releases_mutbefore recycling, which makes it impossible for a call site to miss. I know that matters because I first fenced onlycloseListand the relay still crashed throughdisown.Verified with both windows widened and mixed load including pipelined parse errors, which is what drives the
disownpath:closeListonlyrelease()The 403 is the fence doing its job under a deliberately widened window; it is logged at debug rather than error, since the wait makes it an expected and handled interleaving rather than a fault.
One result in this series was invalid and I am not counting it: an intermediate run reported a crash, but the build had failed on a shadowed
iobinding and the test had silently run the previous binary. The runs above are gated on the build succeeding.Also corrected here, per both reviews: my "degrades to a stale list entry" rationale on the impossible
.handoverarms was false.List.removeon a non-member rewrites the live list's head and tail from stale links, which is the connection-loss and core-pinning bug fixed in #181, not a benign stale entry. Those arms now log and touch no list, which is what actually degrades safely.Re-verified on the final tree:
zig build test65/65, handover-leak 6/6, protocol 45/45, concurrency 2/2, vendor integrity clean, zero panics.