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
166 lines
5 KiB
HTML
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>
|