pcbjam/tests/apps/standalone/jspi-stack/index.html
Viktor Vaczi 3f09a46ff5 jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.

scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.

Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.

Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.

Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00

166 lines
5 KiB
HTML

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>jspi-stack harness</title></head>
<body>
<!--
Browser port of driver.mjs — the red/green shadow-stack battery
(emscripten #27364). Runs every mode/variant combo sequentially and emits
one line per combo:
[JSPI_STACK] SCENARIO <mode>/<variant> corruptA=<n> corruptB=<n> corruptNested=<n> verdict=<RED|GREEN|UNEXPECTED>
then [JSPI_STACK] DONE. RED must observe corruption with mitigation OFF
(proves the harness can still see the bug); GREEN legs must be
corruption-free. Parsed by tests/jspi/jspi-stack.spec.ts.
-->
<script type="module">
const DEPTH = 24;
const STOMP_DEPTH = 48;
const REGION_BYTES = 256 * 1024;
async function runScenario(mode, variant) {
const { default: factory } = await import(
variant === 'pthread' ? './stack_test_pt.mjs' : './stack_test.mjs');
// --- gate plumbing (fresh per run; the instance reads the global) ------
const gates = new Map();
let gateHook = null;
globalThis.__jspiGate = (id) => {
const base = new Promise((res) => gates.set(id, res));
return gateHook ? gateHook(id, base) : base;
};
const release = (id) => {
const r = gates.get(id);
if (!r) throw new Error(`no gate armed for id ${id}`);
gates.delete(id);
r(0);
};
const m = await factory();
const centralBase = m.stackSave();
// --- mitigations (same discipline as driver.mjs) -----------------------
const entryTop = new Map();
const actSp = new Map();
const regions = new Map();
const snapshots = new Map();
function startActivation(id) {
if (mode === 'green-region') {
const base = regions.get(id) ?? m._malloc(REGION_BYTES);
regions.set(id, base);
const top = base + REGION_BYTES;
const saved = m.stackSave();
m.stackRestore(top);
entryTop.set(id, top);
const p = m._activation(id, DEPTH);
m.stackRestore(saved);
return finishActivation(id, p);
}
entryTop.set(id, m.stackSave());
const p = m._activation(id, DEPTH);
return finishActivation(id, p);
}
function finishActivation(id, p) {
return p.then((v) => {
m.stackRestore(centralBase);
if (mode === 'green-region') {
const base = regions.get(id);
if (base) { m._free(base); regions.delete(id); }
}
return v;
});
}
if (mode === 'green-copy') {
gateHook = (id, base) => {
const sp = m.stackSave();
const top = entryTop.get(id);
snapshots.set(id, new Uint8Array(m.HEAPU8.buffer, sp, top - sp).slice());
return base.then((v) => {
new Uint8Array(m.HEAPU8.buffer).set(snapshots.get(id), sp);
snapshots.delete(id);
m.stackRestore(sp);
return v;
});
};
} else if (mode === 'green-region') {
gateHook = (id, base) => {
actSp.set(id, m.stackSave());
return base.then((v) => {
m.stackRestore(actSp.get(id));
return v;
});
};
}
// red: gateHook stays null — raw JSPI, no discipline.
async function interleavedRound() {
const pA = startActivation(1);
const pB = startActivation(2);
release(1);
const corruptA = await pA;
if (mode === 'red') {
m._stomp(STOMP_DEPTH);
} else {
m.stackRestore(centralBase);
m._stomp(STOMP_DEPTH);
}
release(2);
const corruptB = await pB;
return { corruptA, corruptB };
}
async function nestedCase() {
const pA = startActivation(3);
let corruptInner = -1;
const innerDone = (async () => {
const pB = startActivation(4);
release(4);
corruptInner = await pB;
})();
release(3);
const corruptOuter = await pA;
await innerDone;
return { corruptOuter, corruptInner };
}
let totalA = 0, totalB = 0, totalNested = 0;
if (variant === 'pthread') m._start_churn();
const ROUNDS = mode === 'red' ? 1 : 3;
for (let i = 0; i < ROUNDS; i++) {
const { corruptA, corruptB } = await interleavedRound();
totalA += corruptA; totalB += corruptB;
}
const { corruptOuter, corruptInner } = await nestedCase();
totalNested = corruptOuter + corruptInner;
if (variant === 'pthread') m._stop_churn();
const corrupted = totalA + totalB + totalNested > 0;
const verdict =
mode === 'red' ? (corrupted ? 'RED' : 'UNEXPECTED')
: (corrupted ? 'UNEXPECTED' : 'GREEN');
console.log(
`[JSPI_STACK] SCENARIO ${mode}/${variant} corruptA=${totalA} ` +
`corruptB=${totalB} corruptNested=${totalNested} verdict=${verdict}`);
}
const COMBOS = [
['red', 'single'],
['green-copy', 'single'],
['green-region', 'single'],
['green-copy', 'pthread'],
['green-region', 'pthread'],
];
try {
for (const [mode, variant] of COMBOS) {
await runScenario(mode, variant);
}
console.log('[JSPI_STACK] DONE');
} catch (e) {
console.log('[JSPI_STACK] FATAL ' + e);
}
</script>
</body>
</html>