From 22cd32b7b29ab3157a2a28b46b7899c92cca6888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Tue, 21 Jul 2026 12:45:51 +0200 Subject: [PATCH] fix(drift-trio): pin fiber slot across asyncify parks (#10b layer 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symbolized (HOIST_KEEP_NAMES=1): the trap is on the asyncify REWIND re-entering the fiber — stack-local COROUTINE+body were destroyed when Call() returned early on a park, so the rewind called through freed objects (latent UB in the ORIGINAL fire-and-forget runOnFiber too). Heap-pinned FiberSlot + explicit done flag + fiber-tail re-drain. Layer 2 (rewind interplay) still open — fuzz stays fixme'd; pageerror stacks now captured in fuzz artifacts. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi --- tests/kicad/drift-trio-fuzz.spec.ts | 4 +- wasm/bindings/collab_common.h | 83 +++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/tests/kicad/drift-trio-fuzz.spec.ts b/tests/kicad/drift-trio-fuzz.spec.ts index f4fc78d..99bb876 100644 --- a/tests/kicad/drift-trio-fuzz.spec.ts +++ b/tests/kicad/drift-trio-fuzz.spec.ts @@ -438,7 +438,9 @@ for (const [cfg, label, mkActions, seedReg] of [ const t = m.text(); if (/collab|drift|parse|error|abort/i.test(t)) consoles[tabLabel]!.push(t.slice(0, 400)); }); - page.on("pageerror", (e) => consoles[tabLabel]!.push(`PAGEERROR ${e.message}`)); + page.on("pageerror", (e) => + consoles[tabLabel]!.push(`PAGEERROR ${e.message}\nSTACK ${(e.stack ?? "").slice(0, 4000)}`), + ); } const actions = mkActions(); const reg = seedReg(); diff --git a/wasm/bindings/collab_common.h b/wasm/bindings/collab_common.h index b99d024..7057571 100644 --- a/wasm/bindings/collab_common.h +++ b/wasm/bindings/collab_common.h @@ -61,32 +61,95 @@ inline bool& fiberBusy() return busy; } +/* The in-flight body. HEAP-allocated and pinned for the body's whole life: + * when a body PARKS (asyncify suspension inside commit.Push), COROUTINE::Call + * RETURNS EARLY — the later asyncify rewind re-enters the fiber through the + * SAME callable at the SAME addresses (dynCall_vi → fcontext_entry → + * callerStub → the wrapper). Stack-local cor/body (the original runOnFiber + * AND the first serialized version) were destroyed on that early return, so + * the rewind called through freed objects — "table index is out of bounds" + * at rewind, memory corruption downstream (finding #10b's symbolized stack). + * `done` is the ONLY completion signal; Call() returning is not. */ +struct FiberSlot +{ + COROUTINE* cor = nullptr; + std::function* body = nullptr; + bool done = false; +}; + +inline FiberSlot& activeFiberSlot() +{ + static FiberSlot s; + return s; +} + +inline wxEvtHandler*& fiberHandler() +{ + static wxEvtHandler* h = nullptr; + return h; +} + +inline void drainFibers(); + +inline void reapFiber() +{ + FiberSlot& slot = activeFiberSlot(); + delete slot.cor; + delete slot.body; + slot.cor = nullptr; + slot.body = nullptr; + slot.done = false; + fiberBusy() = false; +} + inline void drainFibers() { + FiberSlot& slot = activeFiberSlot(); + if( fiberBusy() ) - return; // the running drain's while-loop covers the rest + { + if( !slot.done ) + return; // parked body still in flight — its tail re-drains + + reapFiber(); // completed via rewind since the last drain + } auto& q = fiberQueue(); while( !q.empty() ) { fiberBusy() = true; - - std::function body = std::move( q.front() ); + slot.done = false; + slot.body = new std::function( std::move( q.front() ) ); q.pop_front(); - COROUTINE cor( [&body]( int ) -> int - { - body(); - return 0; - } ); - cor.Call( 0 ); - fiberBusy() = false; + slot.cor = new COROUTINE( []( int ) -> int + { + FiberSlot& sl = activeFiberSlot(); + ( *sl.body )(); + sl.done = true; + + // If we parked, no drain is pending by the time the rewind + // completes — schedule the reap + next body from the fiber tail + // (CallAfter only queues; safe here). + if( wxEvtHandler* h = fiberHandler() ) + h->CallAfter( []() { drainFibers(); } ); + + return 0; + } ); + + slot.cor->Call( 0 ); + + if( !slot.done ) + return; // parked — cor/body stay pinned for the rewind + + reapFiber(); } } inline void runOnFiber( wxEvtHandler* aHandler, std::function aBody ) { + fiberHandler() = aHandler; fiberQueue().push_back( std::move( aBody ) ); aHandler->CallAfter( []() { drainFibers(); } ); }