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>
|