ConnectionHandle is a slab slot: noq frees it once a connection is fully drained, and the next accepted or dialed connection reuses it. Every resource (connection, send-stream, recv-stream) holds a raw handle and looks the entry up per call — so a resource that outlives its connection starts answering for (and acting on!) whatever connection now occupies the slot.
The sharpest consequence, observed live from wosh: SendStreamRes's drop-implied reset(0) fired at a reused slot and reset stream 0 of a stranger's fresh connection. The victim was a tunnel-resume loop: every resumed connection was killed at birth by the previous attachment's dying stream resource (wosh holds the old attachment's send stream until the next attach replaces it — legal per the WIT). Reproduction: freeze a wosh client mid-session (SIGSTOP, 45s), let the listener's connection idle out, thaw; every resume then died with Reset(0) on the hello read, forever.
Same family, not yet observed but reachable:
accept() hands out the slot's next occupant when a queued-but-unclaimed connection dies and the slot is reused — one connection delivered to two acceptors (the queue also double-counts in accept_backlog).
- Every
expect("connection entry") lookup was one conns.remove away from a guest trap (entries are only ever overwritten today, so the panic was latent).
Fix shape (PR to follow): stamp every ConnEntry with a never-reused epoch, stamp every resource (and DialGuard, and the accept-queue entries) with the epoch it was minted under, and gate every resource-side lookup on it. A stale resource observes "connection gone": methods fail error.closed, state() reports closed, wait-closed resolves (none), drops do nothing. Pump-side lookups (noq events, datagram dispatch) speak of the current occupant by construction and stay unstamped.
ConnectionHandleis a slab slot: noq frees it once a connection is fully drained, and the next accepted or dialed connection reuses it. Every resource (connection,send-stream,recv-stream) holds a raw handle and looks the entry up per call — so a resource that outlives its connection starts answering for (and acting on!) whatever connection now occupies the slot.The sharpest consequence, observed live from wosh:
SendStreamRes's drop-impliedreset(0)fired at a reused slot and reset stream 0 of a stranger's fresh connection. The victim was a tunnel-resume loop: every resumed connection was killed at birth by the previous attachment's dying stream resource (wosh holds the old attachment's send stream until the next attach replaces it — legal per the WIT). Reproduction: freeze a wosh client mid-session (SIGSTOP, 45s), let the listener's connection idle out, thaw; every resume then died withReset(0)on the hello read, forever.Same family, not yet observed but reachable:
accept()hands out the slot's next occupant when a queued-but-unclaimed connection dies and the slot is reused — one connection delivered to two acceptors (the queue also double-counts inaccept_backlog).expect("connection entry")lookup was oneconns.removeaway from a guest trap (entries are only ever overwritten today, so the panic was latent).Fix shape (PR to follow): stamp every
ConnEntrywith a never-reused epoch, stamp every resource (andDialGuard, and the accept-queue entries) with the epoch it was minted under, and gate every resource-side lookup on it. A stale resource observes "connection gone": methods failerror.closed,state()reportsclosed,wait-closedresolves (none), drops do nothing. Pump-side lookups (noq events, datagram dispatch) speak of the current occupant by construction and stay unstamped.