fix(async): fiber resume guard — the prod board-load trap, red/green

Companion to kicad f0ce20ef64 (libcontext swap_suspended guard), which this
pins. The v0.1.20 diagnostics decoded the crash that survived v0.1.13–19:
TOOL_MANAGER Resume()s a coroutine whose body is asyncify-parked inside
handleSleep, the swap rewinds the stale fiber suspension, and the runtime is
poisoned. Full chain of evidence in docs/features/async/16-fiber-resume-guard.md
(+ round-3 addendum in 15-timer-park-repro.md).

- wasm/bindings/fiber_park.h + kicadTestFiberPark{Start,Prime,Poke,State}
  exports (pcbnew + merged kicad_editor): stages Call→yield→legitimate
  resume→sleep park→mid-park Resume, the exact prod state machine. The
  first yield matters: it primes a real (then stale) suspension, matching
  long-lived tool loops rather than a first-slice park.
- tests/kicad/fiber-resume-park.spec.ts: asserts the healthy contract on
  polled state only (embind returns across fiber swaps are unwind
  placeholders). RED on the unguarded build — fiber/sleep buffer
  cross-restores, a jump-ghost beacon, the parked body zombified. GREEN with
  the guard: mid-park poke refused ([collab-fcontext] jump-refused beacon),
  park completes, post-yield resume works, no trap signatures.
- Regression sweep green: timer-park-repro, collab-load-fuzz, load-pcb,
  pcbnew-collab, collab-undo, eeschema-collab (19 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
This commit is contained in:
Gergő Törcsvári 2026-07-31 23:37:05 +02:00
commit e561507898
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
7 changed files with 441 additions and 2 deletions

View file

@ -119,7 +119,37 @@ The lever + spec stay as the regression gate for the fix regardless: they
deterministically create and verify the concurrent-park window that all three
shipped fixes were blind to.
## Candidate real fix (only after a red)
## Round 3 — v0.1.20 PROD crash decoded (2026-07-31 late, log console-export-2026-7-31_22-33-27.log)
The crash reproduced in prod with the diagnostics live, and they discriminate
cleanly: ZERO `concurrent-park`, ZERO `aliased-wake-live` (this spec's
collision class is NOT the prod mechanism — the two green rounds were a true
negative), exactly one benign `overlapped-wake … over null` 2 ms after
`open:settled`, traps 218 ms later. Decoding the trap stack against the
shipped runtime source:
**Root cause: a KiCad coroutine is Resume()d while its body is
asyncify-parked inside handleSleep.** libcontext-on-emscripten-fibers and
handleSleep are two independent suspension protocols sharing one context: a
fiber suspended by `fiber_swap` has valid rewind data at `fiber+20`; a fiber
whose body parked via handleSleep does NOT (its live state is in the sleep's
allocateData buffer, invisible to the fiber machinery — and to TOOL_MANAGER).
Resume → `fiber_swap` into it → `finishContextSwitch` rewinds the STALE
`fiber+20` → rewind-path mismatch → "unreachable executed"; every subsequent
entry (the GAL timer's async_call cascade among them) reads poisoned asyncify
state → "index out of bounds". Impossible on native (no parking without
yielding), unreachable in this spec (warm fibers never park internally), and
untouched by all three shipped fixes.
Next: a `kicadTestSetFiberPark(ms)`-style lever (coroutine body that
`emscripten_sleep()`s + a Resume poked mid-park) as spec cycle 5 — expected
deterministically RED with exactly the prod signature — then the fix: track
"asyncify park in flight" per fiber and defer Resume/swap-into until the
park completes (wasm layer; `kicadCollabFiberBusy` from drift-trio #10b is
the precedent).
## Candidate real fix for the timer half (superseded by round 3 for the prod
crash, still sound hygiene)
Deliver timer notifies from the main-loop chain: the JS timer callback only
marks the timer due and wakes the yield; the loop dispatches due timers after