Skip to content

A resource that outlives its connection touches nobody - #79

Merged
lann merged 1 commit into
mainfrom
stale-resource-epochs
Aug 15, 2026
Merged

A resource that outlives its connection touches nobody#79
lann merged 1 commit into
mainfrom
stale-resource-epochs

Conversation

@lannbot

@lannbot lannbot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Part of #78 — the epoch guard (the conformance case stays open there).

ConnectionHandle is a slab slot noq reuses once a connection fully drains. Resources held raw handles and looked entries up per call, so a resource kept past its connection's death acted on the slot's next occupant. Observed live (from wosh's freeze drill): the drop-implied reset(0) of a dead attachment's send stream, held until the next resume attached, landed on the reused slot and killed stream 0 of every freshly resumed connection — a permanent resume loop.

The guard:

  • every ConnEntry carries an epoch from a never-reused counter;
  • every resource minted against a connection — connection, send-stream, recv-stream, the DialGuard, the accept queue's entries — is stamped with its mint epoch;
  • resource-side lookups go through entry_for(handle, epoch): a stale resource observes "connection gone" (methods fail error.closed, state()closed, wait-closednone, reset/stop/close no-op) instead of the stranger in the slot;
  • accept() skips queue entries whose connection died unclaimed — which also stops a reused slot being delivered to two acceptors;
  • pump-side lookups (noq events, datagram dispatch) name the current occupant by construction and stay unstamped.

This also retires every expect("connection entry") — each was one conns.remove away from a guest trap.

Verified: just check (fmt, clippy, validate-wit, tests) and the full cross-host just matrix. End to end: wosh's freeze drills — a native SIGSTOP repro and a headless-browser gate — loop forever without this and pass with it (wosh#63 carries the gates).

Connection handles are slab slots: noq frees one once its connection
is fully drained, and the next connection reuses it. Resources held
raw handles and looked entries up per call, so a resource kept past
its connection's death answered for -- and acted on -- the slot's
next occupant. Observed from wosh as a resume loop: the previous
attachment's send-stream resource, dropped at the next attach, fired
its drop-implied reset(0) into the reused slot and killed stream 0 of
the freshly resumed connection, every time.

Every ConnEntry now carries an epoch from a never-reused counter, and
everything minted against a connection -- the connection resource,
its streams, the dial guard, the accept queue's entries -- is stamped
with the epoch it was minted under. Resource-side lookups go through
entry_for, which yields the entry only while the slot still holds the
SAME connection: a stale resource observes 'gone' (methods fail
closed, state() reports closed, wait-closed resolves none, reset/
stop/close drop to no-ops) instead of the stranger in the slot.
accept() skips queue entries whose connection died unclaimed, which
also stops a reused slot being handed to two acceptors. Pump-side
lookups (noq events, datagram dispatch) speak of the current occupant
by construction and stay unstamped.

Verified by just check and the full cross-host matrix; end to end by
wosh's freeze drills (native SIGSTOP repro and the browser gate),
which loop forever without this and pass with it.

Part of #78 (the conformance case for slot reuse remains open there).
@lann
lann merged commit f270e51 into main Aug 15, 2026
1 check passed
@lann
lann deleted the stale-resource-epochs branch August 15, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants