Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/browser-demos/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ export default defineConfig({
},
{
name: "firefox",
testMatch: "coi.spec.ts",
testMatch: ["coi.spec.ts", "wasm-trap-signal.spec.ts"],
use: { browserName: "firefox" },
},
{
name: "webkit",
testMatch: ["coi.spec.ts", "kandelo-webkit-smoke.spec.ts"],
testMatch: [
"coi.spec.ts",
"kandelo-webkit-smoke.spec.ts",
"wasm-trap-signal.spec.ts",
],
use: { browserName: "webkit" },
},
],
Expand Down
94 changes: 94 additions & 0 deletions apps/browser-demos/test/wasm-trap-signal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { expect, test } from "@playwright/test";
import {
classifyWasmCrashSignal,
SIGFPE,
SIGILL,
SIGSEGV,
} from "../../../host/src/trap-signals";

const TRAP_MODULES = [
{
name: "memory-oob",
expectedSignum: SIGSEGV,
bytes: [
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 5, 3,
1, 0, 1, 7, 7, 1, 3, 114, 117, 110, 0, 0, 10, 12, 1, 10, 0, 65,
128, 128, 4, 40, 2, 0, 26, 11,
],
},
{
name: "divide-by-zero",
expectedSignum: SIGFPE,
bytes: [
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 7,
1, 3, 114, 117, 110, 0, 0, 10, 10, 1, 8, 0, 65, 1, 65, 0, 109, 26,
11,
],
},
{
name: "unreachable",
expectedSignum: SIGILL,
bytes: [
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 7,
1, 3, 114, 117, 110, 0, 0, 10, 5, 1, 3, 0, 0, 11,
],
},
{
name: "indirect-null-table-entry",
expectedSignum: SIGILL,
bytes: [
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 4, 4,
1, 112, 0, 1, 7, 7, 1, 3, 114, 117, 110, 0, 0, 10, 9, 1, 7, 0, 65,
0, 17, 0, 0, 11,
],
},
{
name: "indirect-table-oob",
expectedSignum: SIGSEGV,
bytes: [
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2,
1, 0, 4, 4, 1, 112, 0, 1, 7, 7, 1, 3, 114, 117, 110, 0,
0, 10, 9, 1, 7, 0, 65, 1, 17, 0, 0, 11,
],
},
] as const;

test("Wasm trap messages classify to POSIX signals in this browser engine", async ({
page,
browserName,
}) => {
await page.setContent("<!doctype html><title>wasm trap signal test</title>");

const messages = await page.evaluate(async (fixtures) => {
const results: Record<string, string> = {};
for (const fixture of fixtures) {
const { instance } = await WebAssembly.instantiate(new Uint8Array(fixture.bytes));
const run = instance.exports.run;
if (typeof run !== "function") {
throw new Error(`fixture ${fixture.name} did not export run()`);
}
try {
run();
results[fixture.name] = "NO_TRAP";
} catch (err) {
if (err instanceof Error) {
results[fixture.name] = `${err.name}: ${err.message}`;
} else {
results[fixture.name] = String(err);
}
}
}
return results;
}, TRAP_MODULES);

for (const fixture of TRAP_MODULES) {
const message = messages[fixture.name];
expect(message, `${browserName} ${fixture.name}`).toBeDefined();
expect(message, `${browserName} ${fixture.name}`).not.toBe("NO_TRAP");
const classification = classifyWasmCrashSignal(message);
expect(
classification?.signum,
`${browserName} ${fixture.name}: ${message}`,
).toBe(fixture.expectedSignum);
}
});
15 changes: 15 additions & 0 deletions examples/divzero_trap_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Deliberately trigger a Wasm integer divide-by-zero trap.
*/
#include <stdio.h>

int main(void)
{
fprintf(stderr, "before-divzero\n");
fflush(stderr);
volatile int numerator = 1;
volatile int denominator = 0;
volatile int value = numerator / denominator;
fprintf(stderr, "after-divzero-SHOULD-NEVER-REACH:%d\n", value);
return 1;
}
16 changes: 16 additions & 0 deletions examples/oob_trap_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Deliberately dereference an address outside the configured wasm32
* linear-memory maximum so the engine raises a memory OOB trap.
*/
#include <stdint.h>
#include <stdio.h>

int main(void)
{
fprintf(stderr, "before-oob\n");
fflush(stderr);
volatile uint32_t *p = (volatile uint32_t *)(uintptr_t)0x7fffffffU;
volatile uint32_t value = *p;
fprintf(stderr, "after-oob-SHOULD-NEVER-REACH:%u\n", value);
return 1;
}
4 changes: 2 additions & 2 deletions examples/wasm_trap_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
* promise never resolves and tests time out.
*
* With the fix, this program terminates promptly and `spawn()`
* resolves. Expected exit code is 0 because worker-main treats
* `unreachable` as the normal `_Exit` exit pattern.
* resolves. An arbitrary user-space `unreachable` is classified as
* SIGILL; only the known kernel_exit path is treated as normal exit.
*/
#include <stdio.h>

Expand Down
46 changes: 31 additions & 15 deletions host/src/browser-kernel-worker-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ import { TlsNetworkBackend } from "./networking/tls-network-backend";
import { patchWasmForThread } from "./worker-main";
import { detectPtrWidth, extractHeapBase } from "./constants";
import { ThreadExitCoordinator } from "./thread-exit-coordinator";
import {
classifiedSignalOrFallback,
classifiedTrapExitStatus,
signalExitStatus,
SIGSEGV,
} from "./trap-signals";
import type {
CentralizedWorkerInitMessage,
CentralizedThreadInitMessage,
Expand Down Expand Up @@ -737,7 +743,7 @@ function installProcessWorkerListeners(
pid: number,
): void {
let exited = false;
const finalize = (status: number, source: string) => {
const finalize = (status: number, source: string, crashSignum?: number) => {
if (exited) return;
exited = true;
if (processes.get(pid)?.worker !== worker) return; // already replaced (e.g. by exec)
Expand All @@ -747,19 +753,20 @@ function installProcessWorkerListeners(
} else {
console.warn(message);
}
handleExit(pid, status);
handleExit(pid, status, crashSignum);
};

// Status conventions match the Node host (PR #410):
// 139 = 128 + SIGSEGV (11) — uncaught wasm trap, worker died without
// a SYS_exit_group. Matches a Linux child killed by SIGSEGV.
// -1 — worker-main caught an instantiation/init error and posted
// {type:"error"}. Same convention as Node-side handleSpawn.
// Status conventions match the Node host:
// 128+signal — classified Wasm trap, or generic SIGSEGV when a worker
// dies without a trap reason. Matches Linux signal-death status.
// -1 — worker-main caught an unclassified instantiation/init error
// and posted {type:"error"}.
// m.status — worker-main posted {type:"exit"}, normal exit path.
worker.on("error", (err: Error) => {
if (intentionallyTerminated.has(worker as object)) return;
console.error(`[kernel-worker] Worker error pid=${pid}:`, err.message);
finalize(128 + 11, "worker.onerror");
const signum = classifiedSignalOrFallback(err);
finalize(signalExitStatus(signum), "worker.onerror", signum);
});
worker.on("exit", (code: number) => {
// BrowserWorkerHandle synthesizes an "exit" event when the underlying
Expand All @@ -773,7 +780,7 @@ function installProcessWorkerListeners(
// finalized via the "error" handler above, so this is a no-op there.
// If "exit" fires without a prior "error" (worker died via some path
// we don't yet model), still treat it as a crash.
finalize(128 + 11, "worker exit event");
finalize(signalExitStatus(SIGSEGV), "worker exit event", SIGSEGV);
});
worker.on("message", (msg: unknown) => {
const m = msg as { type?: string; message?: string; pid?: number; status?: number };
Expand All @@ -789,7 +796,8 @@ function installProcessWorkerListeners(
`[process-worker] ${m.message ?? "unknown error"}\n`,
);
post({ type: "stderr", pid, data: errBytes });
finalize(-1, "worker-main error message");
const signum = classifiedSignalOrFallback(m.message);
finalize(classifiedTrapExitStatus(m.message) ?? -1, "worker-main error message", signum);
} else if (m.type === "exit") {
finalize(m.status ?? 0, "worker-main exit message");
}
Expand Down Expand Up @@ -1189,11 +1197,19 @@ function handleThreadExit(pid: number, channelOffset: number): boolean {
return threadExits.requestExit(pid, channelOffset);
}

function handleExit(pid: number, exitStatus: number): void {
void finishProcessExit(pid, exitStatus);
function signalFromExitStatus(exitStatus: number): number | null {
return exitStatus >= 128 ? (exitStatus - 128) & 0x7f : null;
}

function handleExit(pid: number, exitStatus: number, crashSignum?: number): void {
void finishProcessExit(pid, exitStatus, crashSignum);
}

async function finishProcessExit(pid: number, exitStatus: number): Promise<void> {
async function finishProcessExit(
pid: number,
exitStatus: number,
crashSignum: number = signalFromExitStatus(exitStatus) ?? SIGSEGV,
): Promise<void> {
if (processTeardowns.has(pid)) return;

const info = processes.get(pid);
Expand All @@ -1207,7 +1223,7 @@ async function finishProcessExit(pid: number, exitStatus: number): Promise<void>
threadedProcessPids.delete(pid);

const teardown = (async () => {
// Synthesize a SIGSEGV-style reap *before* `deactivateProcess` in
// Synthesize a signal-style reap *before* `deactivateProcess` in
// case the worker died without sending SYS_EXIT_GROUP (uncaught
// wasm trap -> onerror, worker-main `{type:"error"}` -> finalize(-1),
// externally terminated Worker -> "exit" event). Without this, a
Expand All @@ -1216,7 +1232,7 @@ async function finishProcessExit(pid: number, exitStatus: number): Promise<void>
// `hostReaped`: when the kernel already processed a clean
// SYS_EXIT_GROUP for this pid, this is a no-op. Mirrors
// `finalizeProcessWorker` in host/src/node-kernel-worker-entry.ts.
try { kernelWorker.notifyHostProcessCrashed(pid); } catch { /* best-effort */ }
try { kernelWorker.notifyHostProcessCrashed(pid, crashSignum); } catch { /* best-effort */ }
// Check if this is a "top-level" process or a fork child
// For now, always deactivate — the main thread tracks exit promises
kernelWorker.deactivateProcess(pid);
Expand Down
Loading
Loading