Skip to content

A7 + #66: component faults are loud on host stream/future ops - #76

Merged
lann merged 1 commit into
mainfrom
stream-trap-retire-66
Aug 11, 2026
Merged

A7 + #66: component faults are loud on host stream/future ops#76
lann merged 1 commit into
mainfrom
stream-trap-retire-66

Conversation

@lann

@lann lann commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Closes #66 (follow-up 1 from the #54/#63 investigation), under contract amendment A7.

The defect class

A guest trap poisons its component instance — the enter/leave bracket stays broken (definitions.py Store.tick line 597), mayEnter is false forever — so live stream/future ends in the poisoned handle table can never rendezvous again. Empirically (repro in the issue): a host writeAll parked on such an end hung forever; the export call rejected loudly but nothing walked back to the parked peer. Worse, the repro exposed that a second same-direction host op could "rendezvous" against the first op's own parked buffer and resolve as if a peer took the data (write resolving 1 with no peer in existence) — silent corruption, not just a hang.

The fix

  • Retirement walk (task/streams.ts retireInstanceAsyncEnds): at poison time, every live CopyEnd in the dead table records a failure against its shared object (cause = the trap) and is dropped via dropSharedForTeardown — host ends and healthy guest peers get the spec-shaped DROPPED notification; a side parked by the doomed guest itself is retired silently (notifying it queues a phantom event into the corpse's waitables; a later tick then asserts — review B2).
  • Every bracket-break site hooks the walk, via an injection seam (setOnInstancePoisoned; a scheduler→streams import would make CopyEnd extends Waitable evaluation-order-sensitive): boundary.ts poison() (sync lift, entered set), Store.tick, Thread.resumeWith, and both FACT cross-component catches (callee side — review B1: a composed callee's trap otherwise unwound to hooks that only walk the caller's chain).
  • Layered delivery (the StreamProducerError design, inverse direction): exec settles DROPPED-shaped (definitions.py parity); the conventions layer rejects with PeerTrappedError (cause chains to the trap; writes carry progress) — with the precision that an op which genuinely completed before the trap keeps its result, and the fault surfaces on the export call and the handle's next op. Future awaits brand instead of reporting a clean-drop DroppedError.
  • Import abandonment: a trapping host import drops the lifted stream/future args it abandoned (releaseAsyncArgsdropForTeardown), so their peers settle with the truthful short count. The err-value path deliberately retains handles (a fallible import returning err is a normal outcome).
  • One in-flight op per host end per direction: a second same-direction op throws TypeError synchronously — the host-side spelling of the CopyEnd busy trap. Read-while-write-parked stays legal.
  • Negligence still hangs: an embedder that lowers a host end and never acts keeps the documented honest hang; only component faults changed.

Contract numbering repair

main carried two different "amendment A5"s (#63's stream round-trips in the Status header, and #71's wasi-shims parking kernel inline-only). The parking kernel is renumbered to A6 with a proper Status-header entry; this change takes A7.

Fixture + tests

stream-pass grows consume-then-trap, open-then-trap, and future-then-trap (gate-sequenced so the host parks before the guest dies). trap_retire_test.ts pins: parked-write rejection with progress, data-then-reject reads, post-trap pre-op rejection, future branding, import abandonment (pure shape → short count; mixed shape → branding wins), clean-EOS non-branding, both double-park guards.

Review + gates

Two reviewer rounds; blockers B1 (FACT sites) and B2 (phantom-event via plain drop() in releaseAsyncArgs) fixed, advisories addressed (future coverage, comment precision, documented activity-arm asymmetry). Gates on the rebased tree (17 commits absorbed, incl. the parking kernel): build, test-rust, test-runtime (370), test-wasi-shims (50), test-ct-runner, test-bundle, examples, test-translate, conformance (1254/0 — the linking directory exercises the FACT hooks; no stale xfails), sched-seeds, test-ports, test-webrtc, shells, browsers, websocket-conformance (55/0), smoke-tls, smoke-c0 legs 2+4 (legs 1/3 lack the experiment-mosh checkout here, pre-existing).

A guest trap poisons its instance (the enter/leave bracket stays broken,
per definitions.py Store.tick line 597) — so live stream/future ends in
the poisoned handle table can never rendezvous again. Before this, a
host operation parked on such an end hung forever; one started
afterwards could "succeed" against the corpse (a copy nothing will ever
read); and a second same-direction host op could rendezvous against the
FIRST op's own parked buffer (observed: write resolving 1 with no peer
in existence — the #66 repro).

The retirement walk (task/streams.ts retireInstanceAsyncEnds): at
poison time, every live CopyEnd in the dead table records a failure
against its shared object (cause = the trap) and is dropped via
dropSharedForTeardown, whose parked-side discipline notifies host ends
and healthy guest peers (DROPPED) but silently retires a side parked by
the doomed guest itself — notifying it would queue a phantom event into
the corpse's waitables and a later tick would assert (review B2).

Hooked at every bracket-break site, through an injection seam
(scheduler.ts setOnInstancePoisoned — streams.ts cannot be imported
from the scheduler without making `CopyEnd extends Waitable`
evaluation-order-sensitive): exec/boundary.ts poison() (sync lift,
entered set), Store.tick and Thread.resumeWith (trap during a resumed
thread), and both FACT cross-component catches (callee side — review
B1: a composed callee's trap otherwise unwound to hooks that walk only
the caller's chain).

Layered delivery, mirroring the StreamProducerError design in the
inverse direction: the exec layer still settles DROPPED-shaped
(definitions.py parity); the conventions layer rejects with
PeerTrappedError (cause chains to the trap; write ops carry `progress`)
— pre-op via throwIfFailed, post-await via throwIfPeerTrapped, with the
precision that an operation which genuinely COMPLETED before the trap
keeps its result (a full write, a read that copied data); Future awaits
brand instead of reporting a clean-drop DroppedError. A trapping host
IMPORT drops the lifted stream/future args it abandoned
(releaseAsyncArgs -> dropForTeardown), so their peers settle with the
truthful short count; the err-VALUE path deliberately retains handles.

One in-flight operation per host end per direction: a second write
while one is parked (or second read, or second future op) throws a
TypeError synchronously — the host-side spelling of the CopyEnd busy
trap. Read-while-write-parked stays legal (different ends).

Contract: embedder-api.md amendment A7, plus a numbering repair — main
carried two different "amendment A5"s (#63's stream round-trips and
#71's wasi-shims parking kernel, which never joined the Status header);
the parking kernel is renumbered to A6 with a header entry, this change
takes A7.

Fixture: stream-pass grows consume-then-trap, open-then-trap and
future-then-trap (gate-sequenced so the host can park before the guest
dies) + the async-spawn feature. Tests: trap_retire_test.ts pins the
parked-write rejection, data-then-reject reads, post-trap pre-op
rejection, future branding, import abandonment (pure shape: short
count; mixed shape: branding wins), clean-EOS non-branding, and both
double-park guards.

Reviewed (two rounds): B1/B2 fixed as above; advisory A6-arm asymmetry
documented at dropForTeardown; future coverage added (A1); test-4
comment states the pure-shape rationale (A3).

Consumer lockfiles (ports/webrtc, ports/websocket): mechanical catch-up
adding the @deltic/translator workspace link from #61.

Gates (on the rebased tree, 17 commits absorbed incl. the parking
kernel and the iroh-exam retirement): build, test-rust, test-runtime
(370), test-wasi-shims (50), test-ct-runner, test-bundle, examples,
test-translate, conformance (1254/0, linking directory exercises the
FACT hooks, no stale xfails), sched-seeds (1, 4242), test-ports,
test-webrtc, shells (sm-pinned; jsc-pinned arch-skipped), browsers
(chromium, firefox), websocket-conformance (55/0), smoke-tls, smoke-c0
legs 2+4 (legs 1/3: missing experiment-mosh checkout, pre-existing).

Closes #66.
@lann
lann enabled auto-merge August 11, 2026 00:17
@lann
lann merged commit 9ac2a84 into main Aug 11, 2026
4 checks passed
@lannbot
lannbot deleted the stream-trap-retire-66 branch August 23, 2026 16:51
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.

host streams: a guest trap strands parked host read/write promises

1 participant