Run the accept teardown once, and guard the shutdown union read - #184
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 hardens HTTP and WebSocket connection ownership, handover cleanup, event-loop reclamation, shutdown protocol handling, slot accounting, and connection-pool recycling. ChangesConnection lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant WebSocketProcessing
participant closeWebsocket
participant EventLoop
WebSocketProcessing->>closeWebsocket: Queue closed WebSocket wrapper
closeWebsocket->>EventLoop: Signal the loop
EventLoop->>EventLoop: Reclaim wrapper on loop thread
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 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 |
Two remaining teardown defects in the vendored epoll worker. Both pre-existing and identical upstream.
1.
accept()runs its whole teardown twice on amonitorReadfailureThree errdefers are live when
loop.monitorRead(conn)fails, and they unwind LIFO, so all three run for one connection:disown()already does the full teardown (list removal, slot release,http_conn_pool.release,conn_mem_pool.destroy), so the two earlier errdefers are pure duplication once the conn owns itself.std.heap.MemoryPool.destroyprepends to a free list, so a double destroy links a node to itself and two latercreate()calls hand the sameConnto two different connections. The doubleclose()can land on an fd another thread just opened.Reproduced by fault injection, since the trigger is
epoll_ctlfailing (ENOMEM/ENOSPCagainstfs.epoll.max_user_watches), which is not directly attacker-controlled. Injecting amonitorReadfailure every 5th accept and logging each errdefer gave exactly the predicted sequence, and the relay aborted with a core dump:The fix is a
conn_owns_teardownflag set once the conn owns both its socket and its memory; the two earlier errdefers become no-ops from that point. Failures before that point still get the original cleanup, which is what they are there for.Same injection against the fix, 8 failures:
errdefer-A0,errdefer-B0,errdefer-C8, no panics, relay alive.2.
shutdownListreadsc.protocol.httpwith no union guardThis one turned out to be already unreachable, and I am recording why rather than just adding the guard.
The reported precondition was a
processSignalsnapshot leaving a stale.websocketnode ashandover_list's head. That requiredList.removeon a snapshot node, and after the handover-list and mutex fixes there is nohandover_list.removeanywhere in the file. The list now has exactly one mutation site, the insert inswapList, always with a.httpconn, plus the snapshot clear. Verified statically, and empirically across 12 shutdown-under-load rounds with upgrades in flight: zero sightings, zero panics, 12 clean shutdowns.Guarded anyway, because that safety is a non-local invariant and the failure mode is severe: in ReleaseSafe the union read panics, and in ReleaseFast it reinterprets a
*ws.HandlerConnas an*HTTPConn, soposix.close()closes whatever integer lands at that offset.server.deinit()runs before storage teardown, so a wild close there can land on the LMDB fd before the final sync. Skipping leaks one fd in a process that is already exiting, which beats either outcome.Verification
zig build test65/65, handover-leak 6/6, protocol 45/45, concurrency 2/2 at 200 connections,verify-vendored-httpz.shclean, zero panics in any relay log.Both fault-injection rigs were removed before the final build; the committed tree contains no probes.
Review follow-up
Both reviews came back with no blockers. Two things worth recording.
Applied:
self.len += 1sat seven lines above the ownership flag, and the two pre-flag errdefers release neither the slot nor the list entry. Anytryadded in that gap would have leaked a worker slot permanently, and past the insert would have left a freedConnin the liverequest_list. The increment now sits immediately afterrequest_list.insert(conn)and directly above the flag, with nothing fallible between them and a comment saying they must stay adjacent. Both reviews arrived at this independently. Re-verified afterwards, including the idle-reclaim test that reads slot reclamation back off the metrics gauge: 6/6.Deliberately not applied here: the same unguarded
c.protocol.httpread remains incollectTimedOut,closeListanddisown. Both reviews verified statically that none of them can currently see a.websocketconn, so this is consistency rather than a live bug, and the right remedy differs from the one used here.shutdownListruns during process teardown, so log-and-skip costs one leaked fd in an exiting process; those three run on every timeout sweep in a live relay, where skipping would leak a slot and an fd for the process lifetime, which is the accept-stall failure this project already fixed once. Picking the right degradation there deserves its own reproduction rather than riding along on this PR. Tracked separately.Also confirmed by review, and worth stating because it inverts the usual intuition: the vendored
posix.closeis.BADF => unreachable, so the double close aborts in ReleaseSafe but is silent UB in ReleaseFast, which is what the release artifacts are built with. The loudly-crashing build was the safer one.Summary by CodeRabbit
New Features
Bug Fixes