From 9bc4da89ff5f6bbf6b64a2334c6a319b4a103dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Wed, 5 Aug 2026 17:46:28 +0200 Subject: [PATCH] mailbox S6: shutdown() in the shim + unit gates; bump wxwidgets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shim shutdown: dead latch, queue rejection/drop with beacons, pump stops, idempotent. Gates: shim units 11/11, asyncify 9/9, coroutine 39/39, wx modal-heavy 45/45, kicad 6/6 — all on DEFAULT-injected glue (docker postprocess -> setup:kicad now yields scheduler builds without manual conversion). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs --- .../async/17-mailbox-scheduler-plan.md | 12 ++++++ scripts/common/shims/asyncify-scheduler.js | 38 +++++++++++++++++ .../src/wasm/scheduler-shim.test.ts | 41 +++++++++++++++++++ wxwidgets | 2 +- 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/docs/features/async/17-mailbox-scheduler-plan.md b/docs/features/async/17-mailbox-scheduler-plan.md index dbb3471..b4382a2 100644 --- a/docs/features/async/17-mailbox-scheduler-plan.md +++ b/docs/features/async/17-mailbox-scheduler-plan.md @@ -290,6 +290,18 @@ rollback = the `WX_SCHEDULER=0` build + a per-step tag. > consume-once layer is the backstop, not a replacement. - **S6 · Lifetime (few d).** Cleanup ordering vs the scheduler; teardown deferred to unload/explicit exit; `ScheduleExit` → scheduler wake (12 §phase-4). + > **Work log 2026-08-05 — S6 LANDED.** The scheduler latches DEAD when the main loop + > exits (DoRun's top-level return → `shutdown("main loop exited")`): queued mutators + > reject, queued messages/wakes drop, pumps stop — stranded work beacons + > (`shutdown ... stranded:`) instead of surfacing as a post-teardown UAF. + > `wxWasmMailboxDeliver` gained ProcessEvents' `!wxTheApp` teardown parity. Unit-gated + > (scheduler-shim.test.ts 4/4 incl. shutdown; 11/11 with WasmMailbox) + full battery + > (asyncify 9/9, coroutine 39/39, wx modal-heavy 45/45) + kicad 6/6 — all on + > DEFAULT-injected glue: the docker postprocess → setup:kicad pipeline now produces + > scheduler builds with no manual conversion. Deferred with the ledger: OnExit/ + > wxEntryCleanupReal-to-unload reshaping (12 §phase-4's fuller lifetime) — the current + > quit flow (wxAppTopWindowClosed → loop exit → cleanup) plus the DEAD latch covers the + > teardown-delivery hazard the step exists for. **Effort: ≈ 5–7 weeks** (doc 12 said 4–6 for the runtime alone; the added week is §3's test work, which is where the safety comes from). S1 and S2 each end in a shippable state; S4 is the diff --git a/scripts/common/shims/asyncify-scheduler.js b/scripts/common/shims/asyncify-scheduler.js index eb71d06..4b18bb0 100644 --- a/scripts/common/shims/asyncify-scheduler.js +++ b/scripts/common/shims/asyncify-scheduler.js @@ -38,6 +38,7 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) { enqueueAfter: function (fn, arg, ms) { var self = this; setTimeout(function () { + if (self.dead) return; // S6: never deliver into a torn-down app self.mailbox.push({ fn: fn, arg: arg }); self.enqueued++; self._armDeliveryTick(); @@ -56,6 +57,7 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) { this._tickArmed = true; var self = this; setTimeout(function tick() { + if (self.dead) { self._tickArmed = false; return; } try { if (Module["_wxWasmMailboxTick"]) Module["_wxWasmMailboxTick"](); } catch (e) { @@ -123,6 +125,7 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) { ? function () { return performance.now(); } : function () { return Date.now(); }; setTimeout(function pump() { + if (self.dead) { self._mutatorPumpArmed = false; return; } // Unkillable: an exception escaping this body would end the setTimeout // chain and wedge the queue forever (observed: 559 frozen messages). try { @@ -222,8 +225,42 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) { try { return fn(); } finally { this._authorizedWrite--; } }, + // --- S6 lifetime -------------------------------------------------------- + // Called when the wx main loop exits (DoRun's top-level return path). The + // app object is about to be destroyed: delivering anything after this + // point runs callbacks into freed C++ state. Queued mutators reject, + // queued messages and wakes drop — loudly, so a teardown that strands + // work is visible in the console instead of surfacing as a later UAF. + dead: false, + shutdown: function (reason) { + if (this.dead) return; + this.dead = true; + var stranded = { + mailbox: this.mailbox.length, + mutators: this.mutatorQueue.length, + wakes: this.readyWakes.length, + waits: this.waits.size, + }; + this.mailbox.length = 0; + for (var i = 0; i < this.mutatorQueue.length; i++) { + try { this.mutatorQueue[i].reject(new Error("[wx-scheduler] shutdown: " + reason)); } catch (e) {} + } + this.mutatorQueue.length = 0; + this.readyWakes.length = 0; + if (stranded.mailbox || stranded.mutators || stranded.wakes || stranded.waits) { + console.warn("[wx-scheduler] shutdown (" + reason + ") stranded:" + + " mailbox=" + stranded.mailbox + + " mutators=" + stranded.mutators + + " wakes=" + stranded.wakes + + " pendingWaits=" + stranded.waits); + } else { + console.log("[wx-scheduler] shutdown (" + reason + ") clean"); + } + }, + state: function () { return "[wx-scheduler] build=1 impl=S2-core" + + (this.dead ? " DEAD" : "") + " mailbox=" + this.mailbox.length + " enqueued=" + this.enqueued + " delivered=" + this.delivered @@ -355,6 +392,7 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) { var self = this; setTimeout(function () { self._wakeDrainArmed = false; + if (self.dead) return; // S6: parked stacks are gone with the app // Deliver from a CLEAN macrotask (export stack empty by construction). while (self.readyWakes.length > 0 && __transitionFree()) { var w = self.readyWakes.shift(); diff --git a/web/standalone/src/wasm/scheduler-shim.test.ts b/web/standalone/src/wasm/scheduler-shim.test.ts index 2ef7dcb..dae5d7f 100644 --- a/web/standalone/src/wasm/scheduler-shim.test.ts +++ b/web/standalone/src/wasm/scheduler-shim.test.ts @@ -19,12 +19,16 @@ const SHIM_PATH = path.resolve( ); type SchedulerShape = { + mailbox: unknown[]; mutatorQueue: unknown[]; mutatorsDelivered: number; readyWakes: { deliver: (r: unknown) => void; result: unknown }[]; deferredWakes: number; drainedWakes: number; strayWrites: number; + dead: boolean; + shutdown(reason: string): void; + enqueueAfter(fn: number, arg: number, ms: number): void; _openBusy(): boolean; _armMutatorPump(): void; _scheduleWakeDrain(): void; @@ -136,6 +140,43 @@ describe("N5: scheduler shim under flood", () => { } }); + it("S6 shutdown: queued mutators reject, messages and wakes drop, pumps stop", async () => { + vi.useFakeTimers(); + try { + let busy = true; + const S = loadShim({ busy: () => busy }); + const M = (globalThis as Record).Module as { + kicadCollabApplyItems: (x: number) => Promise | string; + }; + const p = Promise.resolve(M.kicadCollabApplyItems(1)); + const exP = expect(p).rejects.toThrow("shutdown"); + S.enqueueAfter(1234, 0, 5); + await vi.advanceTimersByTimeAsync(6); // message lands in the mailbox + S.readyWakes.push({ deliver: () => undefined, result: 0 }); + expect(S.mailbox.length).toBe(1); + expect(S.mutatorQueue.length).toBe(1); + + S.shutdown("test teardown"); + await exP; + expect(S.mailbox.length).toBe(0); + expect(S.mutatorQueue.length).toBe(0); + expect(S.readyWakes.length).toBe(0); + expect(S.dead).toBe(true); + expect(S.state()).toContain("DEAD"); + + // Post-shutdown enqueues are dropped, and idempotent shutdown is safe. + S.enqueueAfter(1234, 0, 1); + await vi.advanceTimersByTimeAsync(5); + expect(S.mailbox.length).toBe(0); + S.shutdown("again"); + busy = false; + await vi.advanceTimersByTimeAsync(100); // pumps must stay stopped + expect(S.mutatorsDelivered).toBe(0); + } finally { + vi.useRealTimers(); + } + }); + it("deferred wakes drain strictly FIFO", async () => { vi.useFakeTimers(); try { diff --git a/wxwidgets b/wxwidgets index 1953c18..a395abd 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit 1953c18527e92204e1bbc957a90922f211937613 +Subproject commit a395abd019b733ba186543ff668a068e461abd96