Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/browser-demos-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ jobs:
working-directory: apps/browser-demos
run: npx playwright install --with-deps chromium firefox webkit

- name: Run cross-origin isolation smoke tests
- name: Run browser smoke tests
working-directory: apps/browser-demos
run: |
npx playwright test test/coi.spec.ts \
npx playwright test test/coi.spec.ts test/wasm-trap-signal.spec.ts \
--project=chromium \
--project=firefox \
--project=webkit
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
2 changes: 2 additions & 0 deletions apps/browser-demos/public/trap-signal-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html>
<title>Wasm trap signal test</title>
157 changes: 157 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,157 @@
import { expect, test } from "@playwright/test";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { SIGFPE, SIGILL, SIGSEGV } from "../../../host/src/trap-signals";

const __dirname = dirname(fileURLToPath(import.meta.url));
const trapSignalsModulePath = resolve(
__dirname,
"../../../host/src/trap-signals.ts",
);

// Browser engines do not all isolate synthetic Wasm traps well enough for a
// stable E2E test. The host tests exercise real traps; this suite verifies that
// the classifier Kandelo ships to browsers maps representative engine messages.
const TRAP_MESSAGE_CASES = [
{
name: "v8 memory-oob",
message: "RuntimeError: memory access out of bounds",
expectedSignum: SIGSEGV,
expectedSignalName: "SIGSEGV",
},
{
name: "jsc memory-oob",
message: "RuntimeError: Out of bounds memory access",
expectedSignum: SIGSEGV,
expectedSignalName: "SIGSEGV",
},
{
name: "spidermonkey generic bounds",
message: "RuntimeError: index out of bounds",
expectedSignum: SIGSEGV,
expectedSignalName: "SIGSEGV",
},
{
name: "table index bounds",
message: "RuntimeError: table index is out of bounds",
expectedSignum: SIGSEGV,
expectedSignalName: "SIGSEGV",
},
{
name: "call_indirect bounds",
message: "RuntimeError: Out of bounds call_indirect",
expectedSignum: SIGSEGV,
expectedSignalName: "SIGSEGV",
},
{
name: "divide by zero",
message: "RuntimeError: divide by zero",
expectedSignum: SIGFPE,
expectedSignalName: "SIGFPE",
},
{
name: "integer divide by zero",
message: "RuntimeError: integer divide by zero",
expectedSignum: SIGFPE,
expectedSignalName: "SIGFPE",
},
{
name: "integer overflow",
message: "RuntimeError: integer overflow",
expectedSignum: SIGFPE,
expectedSignalName: "SIGFPE",
},
{
name: "unreachable",
message: "RuntimeError: unreachable",
expectedSignum: SIGILL,
expectedSignalName: "SIGILL",
},
{
name: "unreachable executed",
message: "RuntimeError: unreachable executed",
expectedSignum: SIGILL,
expectedSignalName: "SIGILL",
},
{
name: "indirect type mismatch",
message: "RuntimeError: indirect call type mismatch",
expectedSignum: SIGILL,
expectedSignalName: "SIGILL",
},
{
name: "null indirect function",
message: "RuntimeError: null function or function signature mismatch",
expectedSignum: SIGILL,
expectedSignalName: "SIGILL",
},
{
name: "call_indirect signature mismatch",
message: "RuntimeError: call_indirect to a signature that does not match",
expectedSignum: SIGILL,
expectedSignalName: "SIGILL",
},
{
name: "call_indirect null table entry",
message: "RuntimeError: call_indirect to a null table entry",
expectedSignum: SIGILL,
expectedSignalName: "SIGILL",
},
{
name: "stack overflow",
message: "RangeError: Maximum call stack size exceeded",
expectedSignum: SIGSEGV,
expectedSignalName: "SIGSEGV",
},
{
name: "stack exhausted",
message: "RuntimeError: call stack exhausted",
expectedSignum: SIGSEGV,
expectedSignalName: "SIGSEGV",
},
{
name: "loader compile error",
message: "CompileError: WebAssembly.compile(): expected magic word",
expectedSignum: null,
expectedSignalName: null,
},
{
name: "ABI mismatch",
message: "ABI version mismatch: program=1 kernel=2",
expectedSignum: null,
expectedSignalName: null,
},
] as const;

for (const trapCase of TRAP_MESSAGE_CASES) {
test(`@trap-signal classifies ${trapCase.name} in the browser`, async ({
page,
baseURL,
browserName,
}) => {
expect(baseURL).toBeTruthy();
const moduleUrl = new URL(`/@fs/${trapSignalsModulePath}`, baseURL).href;

await page.goto(new URL("/trap-signal-test.html", baseURL).href);
const classification = await page.evaluate(
async ({ moduleUrl, message }) => {
const trapSignals = await import(/* @vite-ignore */ moduleUrl);
return trapSignals.classifyWasmCrashSignal(message) as {
signum: number;
signalName: string;
} | null;
},
{ moduleUrl, message: trapCase.message },
);

if (trapCase.expectedSignum === null) {
expect(classification, `${browserName} ${trapCase.name}`).toBeNull();
return;
}

expect(classification, `${browserName} ${trapCase.name}`).toMatchObject({
signum: trapCase.expectedSignum,
signalName: trapCase.expectedSignalName,
});
});
}
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;
}
25 changes: 13 additions & 12 deletions examples/run-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const nanoWasm = tryResolveBinary("programs/nano.wasm");
const tclshWasm = tryResolveBinary("programs/tcl.wasm");
const testfixtureWasm = tryResolveBinary("programs/sqlite/testfixture.wasm");
const mysqltestWasm = tryResolveBinary("programs/mariadb/mysqltest.wasm");
const echoWasm = tryResolveBinary("programs/echo.wasm") ?? resolve(repoRoot, "examples/echo.wasm");

// GNU coreutils multi-call binary supports all of these as argv[0]
const coreutilsNames = [
Expand All @@ -73,9 +74,9 @@ const coreutilsNames = [
// Values may be null when a program isn't fetched/built locally.
// Consumers filter out null entries before use.
const builtinPrograms: Record<string, string | null> = {
"echo": resolve(repoRoot, "examples/echo.wasm"),
"/bin/echo": resolve(repoRoot, "examples/echo.wasm"),
"/usr/bin/echo": resolve(repoRoot, "examples/echo.wasm"),
"echo": echoWasm,
"/bin/echo": echoWasm,
"/usr/bin/echo": echoWasm,
"sh": dashWasm,
"/bin/sh": dashWasm,
"dash": dashWasm,
Expand Down Expand Up @@ -257,12 +258,6 @@ function resolveProgram(path: string): ArrayBuffer | null {
if (mapped) {
return loadBytes(mapped);
}
// execlp() searches the inherited host/dev-shell PATH. In CI that can
// resolve tools like gencat to /nix/store/.../bin/gencat; never load that
// host ELF as a guest program.
if (path.endsWith("/gencat")) {
return loadBytes(resolve(repoRoot, "examples/gencat.wasm"));
}
const kernelCwd = process.env.KERNEL_CWD || process.cwd();
const candidates = [
path,
Expand All @@ -280,6 +275,14 @@ function resolveProgram(path: string): ArrayBuffer | null {
return null;
}

function guestEnv(): string[] {
const kernelPath = process.env.KERNEL_PATH ?? "/usr/local/bin:/usr/bin:/bin";
const inherited = Object.entries(process.env)
.filter(([k, v]) => v !== undefined && k !== "PATH")
.map(([k, v]) => `${k}=${v}`);
return [...inherited, `PATH=${kernelPath}`];
}

async function main() {
const name = process.argv[2];
if (!name) {
Expand Down Expand Up @@ -340,9 +343,7 @@ async function main() {
const timeoutMs = parseInt(process.env.TIMEOUT || "30000", 10);
const exitPromise = host.spawn(loadBytes(programPath), processArgv, {
env: [
...Object.entries(process.env)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => `${k}=${v}`),
...guestEnv(),
...gitEnv,
],
cwd: process.env.KERNEL_CWD || process.cwd(),
Expand Down
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
Loading
Loading