Found by the adversarial correctness review of main @ 396a216 (driver track, finding R1-F5). Not confirmed by a test — the function is not exported and the ordering argument below is derived from promise hop counts, not measured on an engine.
Shape
With export E's callback activation hop-parked and E's driver parked in the deadlock probe (P3: tagAwait(t).then(() => true), promise + 3 microtask hops to wake), a concurrent call F on the same instance parks in awaitHopQuiescence (t.awaiting.then(...) → Promise.race, promise + 2 hops). F wakes first and its store.serviceSettled() (boundary.ts ~1875) dispatches E's tail: resumeWith → E's callback wasm runs synchronously inside F's entry wrapper — no driver depth, no driver on the stack.
If that callback traps, the Trap rejects F (which never entered the guest), and E's own driver later reports a generic "deadlock detected". resumeWith's poison marker is set, so the instance IS correctly poisoned — only the attribution is wrong. But guest code also runs under what is effectively a fourth, undocumented driver, outside the one-driver-per-store rule (docs/architecture.md §6).
Authorities
- arch §6 one-driver rule;
runtime/src/task/thread.ts design note ("the abandoned call's own driver reports, via its deadlock trap naming the export").
- definitions.py
canon_lift: a trap unwinds the lifting call.
What would settle it
E2E: a callback-ABI guest with a fast-path-hop import (async-probe / cancel-import block-for under jspi). Call export E, then on the same microtask tick call export F of the same instance; make E's next callback trap (stream-pass's consume-then-trap is the closest existing participant). Assert F's rejection is not E's trap. If confirmed, the fix direction is for awaitHopQuiescence to wait rather than service — hand the tail to whichever driver is live or to the settlement pump.
Found by the adversarial correctness review of
main@ 396a216 (driver track, finding R1-F5). Not confirmed by a test — the function is not exported and the ordering argument below is derived from promise hop counts, not measured on an engine.Shape
With export E's callback activation hop-parked and E's driver parked in the deadlock probe (P3:
tagAwait(t).then(() => true), promise + 3 microtask hops to wake), a concurrent call F on the same instance parks inawaitHopQuiescence(t.awaiting.then(...)→Promise.race, promise + 2 hops). F wakes first and itsstore.serviceSettled()(boundary.ts ~1875) dispatches E's tail:resumeWith→ E's callback wasm runs synchronously inside F's entry wrapper — no driver depth, no driver on the stack.If that callback traps, the Trap rejects F (which never entered the guest), and E's own driver later reports a generic "deadlock detected".
resumeWith's poison marker is set, so the instance IS correctly poisoned — only the attribution is wrong. But guest code also runs under what is effectively a fourth, undocumented driver, outside the one-driver-per-store rule (docs/architecture.md §6).Authorities
runtime/src/task/thread.tsdesign note ("the abandoned call's own driver reports, via its deadlock trap naming the export").canon_lift: a trap unwinds the lifting call.What would settle it
E2E: a callback-ABI guest with a fast-path-hop import (
async-probe/cancel-importblock-forunder jspi). Call export E, then on the same microtask tick call export F of the same instance; make E's next callback trap (stream-pass'sconsume-then-trapis the closest existing participant). Assert F's rejection is not E's trap. If confirmed, the fix direction is forawaitHopQuiescenceto wait rather than service — hand the tail to whichever driver is live or to the settlement pump.