test(kicad): poisoned-attribution lever — the laundering scenario, red/green

kicadTestFiberParkStartSecond/PokeSecond: a second coroutine started while
the first body is asyncify-parked reproduces the misattributed jump that
launders the parked fiber past the C++ guard (the v0.1.21 prod bypass).
Spec scenario 2 stages it and asserts the JS stale-rewind guard quarantines
the laundered resume (exactly one fiber-resume-refused beacon), the parked
body completes undisturbed, and both coroutines finish cleanly.

Doc: async/16 rounds 2 + WSOD section.

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-08-01 10:05:42 +02:00
commit 5333099810
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
5 changed files with 204 additions and 3 deletions

View file

@ -47,6 +47,13 @@ struct State
int phase = 0;
int parkMs = 0;
int pokes = 0;
// Second coroutine (poisoned-attribution scenario): 0 idle · 1 yielded ·
// 2 completed. Starting it while the FIRST body is asyncify-parked makes
// libcontext attribute the jump's old side to that parked fiber
// (g_current_context is stale), writing a fresh suspension into its
// struct — the exact laundering that let the prod resume bypass the
// swap_suspended guard.
int phase2 = 0;
};
inline State& state()
@ -114,13 +121,59 @@ inline bool poke()
return co()->Resume();
}
inline COROUTINE<int, int>*& co2()
{
static COROUTINE<int, int>* s_co2 = nullptr;
return s_co2;
}
inline int fiberBody2( int )
{
state().phase2 = 1;
co2()->KiYield();
state().phase2 = 2;
return 0;
}
/**
* Start a SECOND coroutine while the first body is asyncify-parked. Because
* g_current_context still points at the parked fiber, libcontext attributes
* this jump's old side to it: the swap writes a fresh (foreign) suspension
* into the PARKED fiber's struct and re-marks it swap_suspended the
* laundering that lets a later Resume bypass the C++ guard. The JS
* stale-rewind guard (handlesleep.js) must still quarantine it.
*/
inline bool startSecond()
{
if( !co() )
return false; // scenario needs the first coroutine in flight
if( co2() && co2()->Running() )
return false;
delete co2();
state().phase2 = 0;
co2() = new COROUTINE<int, int>( fiberBody2 );
co2()->Call( 0 );
return state().phase2 == 1;
}
/** Resume the second coroutine past its yield (cleanup / completion). */
inline bool pokeSecond()
{
if( !co2() )
return false;
return co2()->Resume();
}
inline std::string stateJson()
{
char buf[112];
char buf[144];
snprintf( buf, sizeof( buf ),
"{\"phase\":%d,\"pokes\":%d,\"parkMs\":%d,\"running\":%s}",
"{\"phase\":%d,\"pokes\":%d,\"parkMs\":%d,\"running\":%s,\"phase2\":%d}",
state().phase, state().pokes, state().parkMs,
( co() && co()->Running() ) ? "true" : "false" );
( co() && co()->Running() ) ? "true" : "false", state().phase2 );
return buf;
}

View file

@ -211,6 +211,16 @@ static std::string kicadTestFiberParkState()
return pcbjam_fiber_park::stateJson();
}
static bool kicadTestFiberParkStartSecond()
{
return pcbjam_fiber_park::startSecond();
}
static bool kicadTestFiberParkPokeSecond()
{
return pcbjam_fiber_park::pokeSecond();
}
// 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
@ -571,6 +581,8 @@ EMSCRIPTEN_BINDINGS(kicad_editor) {
function("kicadTestFiberParkPrime", &kicadTestFiberParkPrime);
function("kicadTestFiberParkPoke", &kicadTestFiberParkPoke);
function("kicadTestFiberParkState", &kicadTestFiberParkState);
function("kicadTestFiberParkStartSecond", &kicadTestFiberParkStartSecond);
function("kicadTestFiberParkPokeSecond", &kicadTestFiberParkPokeSecond);
// Canvas-only mobile mode (features/mobile).
function("kicadSetChrome", &kicadSetChrome);

View file

@ -160,6 +160,16 @@ std::string kicadTestFiberParkState()
return pcbjam_fiber_park::stateJson();
}
bool kicadTestFiberParkStartSecond()
{
return pcbjam_fiber_park::startSecond();
}
bool kicadTestFiberParkPokeSecond()
{
return pcbjam_fiber_park::pokeSecond();
}
// 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
@ -2428,6 +2438,8 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
function("kicadTestFiberParkPrime", &kicadTestFiberParkPrime);
function("kicadTestFiberParkPoke", &kicadTestFiberParkPoke);
function("kicadTestFiberParkState", &kicadTestFiberParkState);
function("kicadTestFiberParkStartSecond", &kicadTestFiberParkStartSecond);
function("kicadTestFiberParkPokeSecond", &kicadTestFiberParkPokeSecond);
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
// Read-only viewer lock (read-only-viewer).
function("kicadSetReadOnly", &kicadSetReadOnly);