Skip to content

lift atomicity: gate host entry on jspi hop quiescence; retire poisoned late-settles - #82

Merged
lann merged 1 commit into
mainfrom
resume-deferral
Aug 11, 2026
Merged

lift atomicity: gate host entry on jspi hop quiescence; retire poisoned late-settles#82
lann merged 1 commit into
mainfrom
resume-deferral

Conversation

@lann

@lann lann commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Consumer-found (wosh, minutes after bumping past #71): in jspi mode the promising-entry HOP — core returned, result lift pending, bracket already released — admitted a second host call whose guest turn reused the un-lifted return area. Trap: list too long lifting the mosh engine's tick (list<list<u8>>), then an assert cascade off the poisoned instance's late settles. canon_lift (sync options) runs core+lift atomically; the hop window has no reference counterpart.

  • Hop-quiescence gate (exec/boundary.ts): host calls defer while the instance has a hop-parked activation (awaiting ∧ no owning SuspensionPoint — hasRunnableWork's own discriminator). Genuine JSPI suspensions keep the documented interleaving divergence; plain mode's synchronous fast path is untouched.
  • Poisoned late-settle retirement (task/thread.ts, scheduler.ts): a poisoned instance's parked segments retire quietly instead of assert-cascading; the original trap stays the loud one. Invoke-path poison now routes through notifyInstancePoisoned so the marker is recorded at every bracket-break site.
  • Deterministic pin: hop_atomicity_test.ts + fixtures/hop-atomicity.wat (tick/clobber; jspi via a suspending()-marked import the guest never calls — the exact wosh mechanism). Fails pre-fix with the wild symptom; no timing dependence.

Gates: test-runtime 382/0, sched-seeds both, conformance 1254/0, wasi-shims 52/0, smoke-c0 4/4, websocket 55/55. Consumer: the wosh bundled real-traffic reproducer green 3/3 (failed first-try pre-fix).

…ed late-settles

In jspi mode a lifted export's entry is promising-wrapped, so even a
guest turn that completes synchronously settles a microtask later (pin
(j)) — a HOP between the guest's core return and the host-side result
lift. The invoke wrapper releases the reentrance bracket when the first
segment parks (including hop-parks), so a second host call could enter
and run a full guest turn against memory the pending lift was about to
read. definitions.py canon_lift (sync options) runs core + lift
atomically inside one bracket; the hop window has no counterpart there.

Found by the wosh consumer within minutes of bumping past the parking
kernel (whose marked wasi imports auto-detect plain sync components
into jspi mode): the mosh engine's tick -> list<list<u8>> lift read a
concurrent feed-keys turn's reallocated memory — Trap: list too long —
poisoning the instance; every later settle of the instance's parked
threads then died on resumeWith's enterability assert, burying the
real trap under an assert cascade.

Fix, two halves:

- exec/boundary.ts: THE HOP-QUIESCENCE GATE — a host call into a
  jspi-mode instance defers (awaitHopQuiescence) while the instance
  has a hop-parked activation: an awaiting thread with no owning
  SuspensionPoint, the same (b)/(c) discriminator hasRunnableWork
  uses. Genuine JSPI suspensions keep the documented interleaving
  divergence (host-import re-entry relies on it); plain mode keeps its
  synchronous fast path untouched. Progress is guaranteed — hops
  settle on the engine's schedule independent of other activations.

- task/thread.ts + scheduler.ts: poisoned-instance late settles are
  RETIRED quietly instead of assert-cascading (poisonedInstances
  WeakSet beside the #66 seam; boundary's invoke-path poison now
  routes through notifyInstancePoisoned so the marker is recorded).
  The abandoned call's driver reports via its deadlock trap naming
  the export; the ORIGINAL trap stays the loud one.

Regression pin: runtime/tests/jspi/hop_atomicity_test.ts over
fixtures/hop-atomicity.wat — tick() -> list<list<u8>> plus clobber()
that overwrites the return area, flipped into jspi mode by a
suspending()-marked import the guest never calls (the exact wosh
mechanism; asserts !planNeedsSuspension so the provenance stays
honest). Deterministic: no timing. Fails pre-fix with the wild
symptom (list too long), passes post-fix incl. a self-heal round 2.

Gates: test-runtime 382/0, sched-seeds (1, 4242), conformance 1254/0,
test-wasi-shims 52/0, smoke-c0 4/4, websocket-conformance 55/55.
Consumer verification: the wosh browser-pump reproducer (bundled,
real mosh traffic) green 3/3 (failed first-try pre-fix).
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.

1 participant