fix(drift-trio): pin fiber slot across asyncify parks (#10b layer 1)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
This commit is contained in:
Gergő Törcsvári 2026-07-21 12:45:51 +02:00
commit 22cd32b7b2
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
2 changed files with 76 additions and 11 deletions

View file

@ -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();

View file

@ -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<int, int>* cor = nullptr;
std::function<void()>* 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<void()> body = std::move( q.front() );
slot.done = false;
slot.body = new std::function<void()>( std::move( q.front() ) );
q.pop_front();
COROUTINE<int, int> cor( [&body]( int ) -> int
{
body();
return 0;
} );
cor.Call( 0 );
fiberBusy() = false;
slot.cor = new COROUTINE<int, int>( []( 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<void()> aBody )
{
fiberHandler() = aHandler;
fiberQueue().push_back( std::move( aBody ) );
aHandler->CallAfter( []() { drainFibers(); } );
}