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:
parent
f86ef9e433
commit
e561507898
7 changed files with 441 additions and 2 deletions
127
wasm/bindings/fiber_park.h
Normal file
127
wasm/bindings/fiber_park.h
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Test-only repro lever for the DECODED production board-load trap
|
||||
* (docs/features/async/15-timer-park-repro.md round 3, 2026-07-31):
|
||||
* resuming a KiCad coroutine while its body is asyncify-parked inside
|
||||
* handleSleep.
|
||||
*
|
||||
* Two suspension protocols share one context on wasm. A coroutine suspended
|
||||
* by a real yield (fiber_swap) has valid rewind data in its fiber struct; a
|
||||
* coroutine whose body parked via handleSleep (lib-bridge wait,
|
||||
* emscripten_sleep) does NOT — its live state is in the sleep's buffer,
|
||||
* invisible to the fiber machinery and to TOOL_MANAGER. Resume() then swaps
|
||||
* into the STALE fiber data: finishContextSwitch → doRewind → "unreachable
|
||||
* executed", and every later entry reads poisoned Asyncify state ("index out
|
||||
* of bounds"). Impossible natively — a coroutine cannot be suspended without
|
||||
* yielding.
|
||||
*
|
||||
* The lever stages the prod state machine exactly:
|
||||
* start(parkMs): Call() a coroutine that immediately KiYield()s — this
|
||||
* writes VALID suspension data once and clears the fresh-entry path, the
|
||||
* state every long-lived tool loop is in.
|
||||
* prime(): Resume() it legitimately — the body then emscripten_sleep()s
|
||||
* (the internal park; prime's Resume ghost-returns per the epoch
|
||||
* machinery) and afterwards KiYield()s again.
|
||||
* poke(): Resume() DURING the sleep — the fatal prod operation. Unfixed
|
||||
* runtime: rewinds the stale suspension → the exact prod trap. Fixed
|
||||
* runtime: the jump is refused (null INVOCATION_ARGS, same contract as
|
||||
* jump-ghost) and the body completes undisturbed; a later poke() after
|
||||
* the second KiYield resumes it for real.
|
||||
*
|
||||
* Production is inert: nothing runs unless start() is called.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <tool/coroutine.h>
|
||||
|
||||
namespace pcbjam_fiber_park
|
||||
{
|
||||
|
||||
struct State
|
||||
{
|
||||
// 0 idle · 1 yielded-once (primed suspension) · 2 in the sleep park ·
|
||||
// 3 woke, yielded again · 4 resumed past second yield · 5 body returned
|
||||
int phase = 0;
|
||||
int parkMs = 0;
|
||||
int pokes = 0;
|
||||
};
|
||||
|
||||
inline State& state()
|
||||
{
|
||||
static State s_state;
|
||||
return s_state;
|
||||
}
|
||||
|
||||
inline COROUTINE<int, int>*& co()
|
||||
{
|
||||
static COROUTINE<int, int>* s_co = nullptr;
|
||||
return s_co;
|
||||
}
|
||||
|
||||
inline int fiberBody( int )
|
||||
{
|
||||
state().phase = 1;
|
||||
co()->KiYield();
|
||||
|
||||
state().phase = 2;
|
||||
if( state().parkMs > 0 )
|
||||
emscripten_sleep( state().parkMs );
|
||||
|
||||
state().phase = 3;
|
||||
co()->KiYield();
|
||||
|
||||
state().phase = 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Call() + first KiYield: coroutine now has VALID fiber suspension data. */
|
||||
inline bool start( int aParkMs )
|
||||
{
|
||||
if( co() && co()->Running() )
|
||||
return false; // one in flight; the spec drives one cycle at a time
|
||||
|
||||
delete co();
|
||||
state() = State();
|
||||
state().parkMs = aParkMs;
|
||||
co() = new COROUTINE<int, int>( fiberBody );
|
||||
co()->Call( 0 );
|
||||
return state().phase == 1;
|
||||
}
|
||||
|
||||
/** Legitimate Resume into the primed yield; the body then parks. Ghost-returns. */
|
||||
inline bool prime()
|
||||
{
|
||||
if( !co() )
|
||||
return false;
|
||||
|
||||
return co()->Resume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume() regardless of the body's suspension state — what TOOL_MANAGER does
|
||||
* on the next event, unaware the body is asyncify-parked. Counted so the spec
|
||||
* can correlate pokes with phases.
|
||||
*/
|
||||
inline bool poke()
|
||||
{
|
||||
if( !co() )
|
||||
return false;
|
||||
|
||||
++state().pokes;
|
||||
return co()->Resume();
|
||||
}
|
||||
|
||||
inline std::string stateJson()
|
||||
{
|
||||
char buf[112];
|
||||
snprintf( buf, sizeof( buf ),
|
||||
"{\"phase\":%d,\"pokes\":%d,\"parkMs\":%d,\"running\":%s}",
|
||||
state().phase, state().pokes, state().parkMs,
|
||||
( co() && co()->Running() ) ? "true" : "false" );
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace pcbjam_fiber_park
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
#include "pcbjam_libs_reload.h"
|
||||
#include "open_gate.h"
|
||||
#include "timer_park.h"
|
||||
#include "fiber_park.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
|
|
@ -188,6 +189,28 @@ static std::string kicadTestTimerParkState()
|
|||
return pcbjam_timer_park::stateJson();
|
||||
}
|
||||
|
||||
// Test-only (fiber-resume-park repro, fiber_park.h): Resume() into an
|
||||
// asyncify-parked coroutine — the decoded prod board-load trap.
|
||||
static bool kicadTestFiberParkStart( int aParkMs )
|
||||
{
|
||||
return pcbjam_fiber_park::start( aParkMs );
|
||||
}
|
||||
|
||||
static bool kicadTestFiberParkPrime()
|
||||
{
|
||||
return pcbjam_fiber_park::prime();
|
||||
}
|
||||
|
||||
static bool kicadTestFiberParkPoke()
|
||||
{
|
||||
return pcbjam_fiber_park::poke();
|
||||
}
|
||||
|
||||
static std::string kicadTestFiberParkState()
|
||||
{
|
||||
return pcbjam_fiber_park::stateJson();
|
||||
}
|
||||
|
||||
|
||||
// Canvas-only chrome toggle (features/mobile): hide/show every AUI pane
|
||||
// except the central draw canvas, plus the menubar and status bar, so the GAL
|
||||
|
|
@ -544,6 +567,10 @@ EMSCRIPTEN_BINDINGS(kicad_editor) {
|
|||
function("kicadTestSetOpenPark", &kicadTestSetOpenPark);
|
||||
function("kicadTestArmTimerPark", &kicadTestArmTimerPark);
|
||||
function("kicadTestTimerParkState", &kicadTestTimerParkState);
|
||||
function("kicadTestFiberParkStart", &kicadTestFiberParkStart);
|
||||
function("kicadTestFiberParkPrime", &kicadTestFiberParkPrime);
|
||||
function("kicadTestFiberParkPoke", &kicadTestFiberParkPoke);
|
||||
function("kicadTestFiberParkState", &kicadTestFiberParkState);
|
||||
|
||||
// Canvas-only mobile mode (features/mobile).
|
||||
function("kicadSetChrome", &kicadSetChrome);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
#include "collab_presence_core.h"
|
||||
#include "open_gate.h"
|
||||
#include "timer_park.h"
|
||||
#include "fiber_park.h"
|
||||
#include "collab_presence_style.h"
|
||||
#include "pcbjam_theme.h"
|
||||
#include "pcbjam_libs_reload.h"
|
||||
|
|
@ -137,6 +138,28 @@ std::string kicadTestTimerParkState()
|
|||
return pcbjam_timer_park::stateJson();
|
||||
}
|
||||
|
||||
// Test-only (fiber-resume-park repro, fiber_park.h): Resume() into an
|
||||
// asyncify-parked coroutine — the decoded prod board-load trap.
|
||||
bool kicadTestFiberParkStart( int aParkMs )
|
||||
{
|
||||
return pcbjam_fiber_park::start( aParkMs );
|
||||
}
|
||||
|
||||
bool kicadTestFiberParkPrime()
|
||||
{
|
||||
return pcbjam_fiber_park::prime();
|
||||
}
|
||||
|
||||
bool kicadTestFiberParkPoke()
|
||||
{
|
||||
return pcbjam_fiber_park::poke();
|
||||
}
|
||||
|
||||
std::string kicadTestFiberParkState()
|
||||
{
|
||||
return pcbjam_fiber_park::stateJson();
|
||||
}
|
||||
|
||||
// Read-only viewer lock (read-only-viewer): flips the process-global
|
||||
// PCBJAM_READ_ONLY flag consumed by TOOL_MANAGER (view-only action allowlist)
|
||||
// and the selection tools (nothing selectable), and mirrors it onto the
|
||||
|
|
@ -2401,6 +2424,10 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
|||
function("kicadTestSetOpenPark", &kicadTestSetOpenPark);
|
||||
function("kicadTestArmTimerPark", &kicadTestArmTimerPark);
|
||||
function("kicadTestTimerParkState", &kicadTestTimerParkState);
|
||||
function("kicadTestFiberParkStart", &kicadTestFiberParkStart);
|
||||
function("kicadTestFiberParkPrime", &kicadTestFiberParkPrime);
|
||||
function("kicadTestFiberParkPoke", &kicadTestFiberParkPoke);
|
||||
function("kicadTestFiberParkState", &kicadTestFiberParkState);
|
||||
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
|
||||
// Read-only viewer lock (read-only-viewer).
|
||||
function("kicadSetReadOnly", &kicadSetReadOnly);
|
||||
|
|
|
|||
Loading…
Reference in a new issue