From 07d93fbe2dbe5efb6d1de6fd6fb9d96053ac0244 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 24 Jul 2026 04:33:48 -0700 Subject: [PATCH] feat: run AArch64 hello world in browser --- .gitignore | 1 + Cargo.lock | 253 +++++++++++++++++++++++++++++- Cargo.toml | 3 + PLAN.md | 4 +- README.md | 8 +- crates/browser-runtime/Cargo.toml | 20 +++ crates/browser-runtime/src/lib.rs | 208 ++++++++++++++++++++++++ crates/linux-runtime/src/lib.rs | 119 +++++++++++++- crates/runtime-core/src/lib.rs | 5 + deny.toml | 1 + docs/architecture.md | 12 +- docs/licensing.md | 2 + tools/wasm-bindgen/Cargo.toml | 14 ++ tools/wasm-bindgen/src/main.rs | 20 +++ web/index.html | 28 +++- web/package.json | 1 + web/scripts/generate-probes.mjs | 43 ++++- web/src/main.ts | 31 +++- web/src/probe.ts | 12 +- web/src/probe.worker.ts | 39 ++++- web/src/styles.css | 20 +++ web/tests/probe.spec.ts | 14 +- 22 files changed, 824 insertions(+), 34 deletions(-) create mode 100644 crates/browser-runtime/Cargo.toml create mode 100644 crates/browser-runtime/src/lib.rs create mode 100644 tools/wasm-bindgen/Cargo.toml create mode 100644 tools/wasm-bindgen/src/main.rs diff --git a/.gitignore b/.gitignore index bda8d7d..e9c7c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ /web/playwright-report/ /web/test-results/ /web/public/*.wasm +/web/src/generated/ .DS_Store diff --git a/Cargo.lock b/Cargo.lock index 937b3a4..5b46e6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,18 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "binarrow-aarch64" version = "0.1.0" @@ -27,6 +39,16 @@ dependencies = [ "sleigh-runtime", ] +[[package]] +name = "binarrow-browser-runtime" +version = "0.1.0" +dependencies = [ + "binarrow-host-api", + "binarrow-linux-runtime", + "binarrow-loader", + "wasm-bindgen", +] + [[package]] name = "binarrow-cli" version = "0.1.0" @@ -88,12 +110,19 @@ dependencies = [ name = "binarrow-runtime-core" version = "0.1.0" +[[package]] +name = "binarrow-wasm-bindgen-tool" +version = "0.1.0" +dependencies = [ + "wasm-bindgen-cli-support", +] + [[package]] name = "binarrow-wasm-probe" version = "0.1.0" dependencies = [ - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", ] [[package]] @@ -114,12 +143,49 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "foldhash" version = "0.2.0" @@ -140,6 +206,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -151,6 +239,21 @@ dependencies = [ "serde_core", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +dependencies = [ + "rayon", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -158,11 +261,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "serde", "serde_core", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "js-sys" version = "0.3.103" @@ -173,6 +282,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -185,6 +300,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + [[package]] name = "memchr" version = "2.8.3" @@ -235,6 +356,32 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + [[package]] name = "rustversion" version = "1.0.23" @@ -254,6 +401,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -276,6 +424,19 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sleigh-compile" version = "0.3.0" @@ -301,6 +462,12 @@ dependencies = [ "sleigh-parse", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "syn" version = "2.0.119" @@ -335,6 +502,35 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walrus" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bfa49767bb3a9e1afb02aa95bbcbde8d82f2db4ca377afae94d688f14f62378" +dependencies = [ + "anyhow", + "gimli", + "id-arena", + "leb128", + "log", + "rayon", + "walrus-macro", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", +] + +[[package]] +name = "walrus-macro" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9b0525d7ea6e5f906aca581a172e5c91b4c595290dfa8ad4a2bc9ffef33b44" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -357,6 +553,24 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-cli-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c3e3bac5bcdc2a15ba22862e2cb7c0d1beddf9d46dd320bec1f6ae82f2da53" +dependencies = [ + "anyhow", + "base64", + "leb128", + "log", + "rustc-demangle", + "serde", + "serde_json", + "walrus", + "wasm-bindgen-shared", + "wasmparser 0.245.1", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.126" @@ -389,6 +603,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + [[package]] name = "wasm-encoder" version = "0.254.0" @@ -396,7 +620,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.254.0", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags", + "hashbrown 0.16.1", + "indexmap", + "semver", + "serde", ] [[package]] @@ -406,7 +643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" dependencies = [ "bitflags", - "hashbrown", + "hashbrown 0.17.1", "indexmap", "semver", "serde", @@ -437,3 +674,9 @@ dependencies = [ "quote", "syn 2.0.119", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 88a9a00..310c353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/aarch64", + "crates/browser-runtime", "crates/cli", "crates/elf", "crates/guest-memory", @@ -10,6 +11,7 @@ members = [ "crates/loader", "crates/runtime-core", "crates/wasm-probe", + "tools/wasm-bindgen", ] exclude = ["experiments/icicle", "experiments/icicle-wasm", "fuzz"] resolver = "3" @@ -27,6 +29,7 @@ object = { version = "0.39.1", default-features = false, features = ["elf", "rea pcode = { version = "=0.2.0", git = "https://github.com/icicle-emu/icicle-emu", rev = "6f53b6016e63ddcff80a105b8e5ad37caf7a6ed3" } sleigh-compile = { version = "=0.3.0", git = "https://github.com/icicle-emu/icicle-emu", rev = "6f53b6016e63ddcff80a105b8e5ad37caf7a6ed3", default-features = false } sleigh-runtime = { version = "=0.1.0", git = "https://github.com/icicle-emu/icicle-emu", rev = "6f53b6016e63ddcff80a105b8e5ad37caf7a6ed3" } +wasm-bindgen = "=0.2.126" wasm-encoder = "0.254.0" [workspace.lints.rust] diff --git a/PLAN.md b/PLAN.md index cf47b93..e26e03d 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,7 +1,7 @@ # AArch64 ELF-to-WebAssembly Browser Runtime ## Engineering Build Plan and Agent Handoff -**Status:** Phase 1 in progress; native static AArch64 hello-world executes through Linux `write` and `exit` +**Status:** Phase 1 in progress; static AArch64 hello-world executes through Linux `write` and `exit` in native and browser Workers **Primary implementation language:** Rust **Initial browser target:** Google Chrome **Guest architecture:** AArch64, little-endian, Linux userspace @@ -1843,7 +1843,7 @@ The next agent should begin here. 29. Repeat in the browser. 30. Add a regression test and trace fixture. -Items 21–28 are complete with a generated static assembly ELF fixture and the native `binarrow run` host. The browser execution and trace checkpoints remain. +Items 21–30 are complete. The same generated static assembly ELF fixture executes through the project-owned loader, SLEIGH interpreter, Linux runtime, and terminal host in both the native CLI and a Chromium Worker; the browser regression verifies its output, exit status, resource counters, and syscall trace. Do not begin the full web IDE before item 30 passes. diff --git a/README.md b/README.md index 4b06313..5b821d6 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,17 @@ The project has completed the Phase 0 feasibility stage described in [PLAN.md](P - `crates/linux-abi`: minimal AArch64 Linux syscall numbers and errno return encoding. - `crates/host-api`: browser-independent terminal service traits. - `crates/linux-runtime`: bounded `write`/`exit` dispatch and the static-process execution loop. +- `crates/browser-runtime`: wasm-bindgen bridge that runs the same static process inside a Worker. - `crates/cli`: the native `binarrow inspect ` and `binarrow run ` commands. - `crates/wasm-probe`: generation of the Memory64 WebAssembly module used by browser tests. - `web`: a Worker-based Chrome feature report and Playwright smoke test. - `experiments/icicle`: the pinned, isolated AArch64 decode/interpreter feasibility probe. - `experiments/icicle-wasm`: a pinned P-code semantic probe compiled to raw browser Wasm. - `third_party/ghidra-aarch64`: the pinned Apache-2.0 AArch64 SLEIGH source closure with license, notice, and provenance. +- `tools/wasm-bindgen`: pinned, repository-local generation of browser bindings. - `docs/decisions`: architecture decision records, including the selective Icicle adoption decision. -The browser gate dynamically instantiates Memory64, executes the normalized P-code semantics corresponding to an AArch64 `mov`/`add` sequence, and proves that a Promise-bearing JSPI import suspends and resumes Wasm. See [the browser feasibility report](docs/research/browser-feasibility-report.md) for the exact verified environment and diagnostics. +The browser gate dynamically instantiates Memory64, proves that a Promise-bearing JSPI import suspends and resumes Wasm, and now runs a complete static AArch64 hello-world ELF through the production Rust loader, SLEIGH interpreter, Linux runtime, and terminal boundary in a Worker. See [the browser feasibility report](docs/research/browser-feasibility-report.md) for the original capability environment and diagnostics. ## Prerequisites @@ -57,13 +59,13 @@ Run an ELF through the native interpreter and propagate its guest exit status: cargo run -p binarrow-cli -- run path/to/aarch64-static.elf [guest arguments...] ``` -The runnable instruction/syscall profile is deliberately narrow: the current static fixture uses `movz` and `svc`, writes only to stdout or stderr, and exits. Invalid terminal descriptors and guest buffers return Linux errno values; instruction, memory, and combined-output limits are enforced before host side effects. +The runnable instruction/syscall profile is deliberately narrow: the current static fixture uses `movz` and `svc`, writes only to stdout or stderr, and exits. Invalid terminal descriptors and guest buffers return Linux errno values; instruction, syscall, memory, and combined-output limits are enforced before host side effects. The browser build generates its Memory64, JSPI, and P-code `.wasm` probes before starting Vite. Generated artifacts are not committed. ## Scope -The native hello-world checkpoint is complete: the loader and SLEIGH interpreter execute a static AArch64 ELF through `write` and `exit`, route bytes through an explicit host terminal trait, enforce output and instruction budgets, and propagate the guest exit code from the CLI. The normalized instruction subset remains intentionally small and traps cleanly on unsupported semantics. The next checkpoint is running the same ELF/runtime path in the browser Worker and recording a syscall trace fixture. See [PLAN.md](PLAN.md) for the roadmap and [docs/architecture.md](docs/architecture.md) for the current boundaries. +The native-and-browser hello-world checkpoint is complete: both hosts run the same project-owned loader, SLEIGH interpreter, Linux `write`/`exit` dispatcher, resource limits, and syscall trace. Chromium verifies the guest output, exit status, instruction count, syscall count, and output-byte count end to end. The normalized instruction subset remains intentionally small and traps cleanly on unsupported semantics. The next checkpoint is expanding control-flow, memory, and syscall support far enough to run a compiler-produced static program rather than the handcrafted assembly fixture. See [PLAN.md](PLAN.md) for the roadmap and [docs/architecture.md](docs/architecture.md) for the current boundaries. ## License diff --git a/crates/browser-runtime/Cargo.toml b/crates/browser-runtime/Cargo.toml new file mode 100644 index 0000000..e99b4c1 --- /dev/null +++ b/crates/browser-runtime/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "binarrow-browser-runtime" +description = "WebAssembly bridge for running binarrow inside a browser Worker" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +binarrow-host-api = { path = "../host-api", version = "0.1.0" } +binarrow-linux-runtime = { path = "../linux-runtime", version = "0.1.0" } +binarrow-loader = { path = "../loader", version = "0.1.0" } +wasm-bindgen.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[lints] +workspace = true diff --git a/crates/browser-runtime/src/lib.rs b/crates/browser-runtime/src/lib.rs new file mode 100644 index 0000000..c8c989d --- /dev/null +++ b/crates/browser-runtime/src/lib.rs @@ -0,0 +1,208 @@ +//! WebAssembly entry point for the browser Worker execution checkpoint. + +use core::convert::Infallible; + +use binarrow_host_api::{HostTerminal, TerminalStream}; +use binarrow_linux_runtime::Process; +use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; +use wasm_bindgen::prelude::*; + +const MESSAGE: &[u8] = b"hello, world\n"; +const MESSAGE_LENGTH: u16 = 13; + +/// Browser-safe result returned after the embedded static ELF terminates. +#[wasm_bindgen] +pub struct BrowserExecution { + stdout: String, + stderr: String, + trace: String, + exit_code: u8, + executed_instructions: u64, + dispatched_syscalls: u64, + output_bytes: u64, +} + +#[wasm_bindgen] +impl BrowserExecution { + #[wasm_bindgen(getter)] + #[must_use] + pub fn stdout(&self) -> String { + self.stdout.clone() + } + + #[wasm_bindgen(getter)] + #[must_use] + pub fn stderr(&self) -> String { + self.stderr.clone() + } + + #[wasm_bindgen(getter)] + #[must_use] + pub fn trace(&self) -> String { + self.trace.clone() + } + + #[wasm_bindgen(getter)] + #[must_use] + pub fn exit_code(&self) -> u8 { + self.exit_code + } + + #[wasm_bindgen(getter)] + #[must_use] + pub fn executed_instructions(&self) -> u64 { + self.executed_instructions + } + + #[wasm_bindgen(getter)] + #[must_use] + pub fn dispatched_syscalls(&self) -> u64 { + self.dispatched_syscalls + } + + #[wasm_bindgen(getter)] + #[must_use] + pub fn output_bytes(&self) -> u64 { + self.output_bytes + } +} + +/// Load and execute the deterministic static `AArch64` hello-world fixture. +/// +/// # Errors +/// +/// Returns a JavaScript error when loading, SLEIGH initialization, execution, +/// or UTF-8 conversion fails. +#[wasm_bindgen] +pub fn run_hello_world() -> Result { + execute_hello_world().map_err(|message| JsValue::from_str(&message)) +} + +fn execute_hello_world() -> Result { + let image = load_process( + &hello_elf(), + &ProcessParameters { + argv: vec![b"/hello".to_vec()], + envp: Vec::new(), + random_bytes: [0x42; 16], + credentials: Credentials::default(), + }, + ProcessConfig::default(), + ) + .map_err(|error| error.to_string())?; + let mut process = Process::new(image).map_err(|error| error.to_string())?; + let mut terminal = CapturedTerminal::default(); + let result = process + .run(&mut terminal) + .map_err(|error| error.to_string())?; + let trace = process + .trace() + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + + Ok(BrowserExecution { + stdout: String::from_utf8(terminal.stdout).map_err(|error| error.to_string())?, + stderr: String::from_utf8(terminal.stderr).map_err(|error| error.to_string())?, + trace, + exit_code: result.exit_code, + executed_instructions: result.executed_instructions, + dispatched_syscalls: result.dispatched_syscalls, + output_bytes: result.output_bytes, + }) +} + +#[derive(Default)] +struct CapturedTerminal { + stdout: Vec, + stderr: Vec, +} + +impl HostTerminal for CapturedTerminal { + type Error = Infallible; + + fn write(&mut self, stream: TerminalStream, bytes: &[u8]) -> Result<(), Self::Error> { + match stream { + TerminalStream::StandardOutput => self.stdout.extend_from_slice(bytes), + TerminalStream::StandardError => self.stderr.extend_from_slice(bytes), + } + Ok(()) + } +} + +fn hello_elf() -> Vec { + let mut bytes = vec![0; 0x130]; + bytes[..16].copy_from_slice(&[0x7f, b'E', b'L', b'F', 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + write_u16(&mut bytes, 16, 2); + write_u16(&mut bytes, 18, 183); + write_u32(&mut bytes, 20, 1); + write_u64(&mut bytes, 24, 0x1100); + write_u64(&mut bytes, 32, 64); + write_u16(&mut bytes, 52, 64); + write_u16(&mut bytes, 54, 56); + write_u16(&mut bytes, 56, 1); + + write_u32(&mut bytes, 64, 1); + write_u32(&mut bytes, 68, 5); + write_u64(&mut bytes, 72, 0); + write_u64(&mut bytes, 80, 0x1000); + write_u64(&mut bytes, 88, 0x1000); + write_u64(&mut bytes, 96, 0x130); + write_u64(&mut bytes, 104, 0x130); + write_u64(&mut bytes, 112, 0x1000); + + let code = [ + move_wide(0, 1), + move_wide(1, 0x1120), + move_wide(2, MESSAGE_LENGTH), + move_wide(8, 64), + 0xd400_0001, + move_wide(0, 0), + move_wide(8, 93), + 0xd400_0001, + ]; + for (index, instruction) in code.into_iter().enumerate() { + let offset = 0x100 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes()); + } + bytes[0x120..0x120 + MESSAGE.len()].copy_from_slice(MESSAGE); + bytes +} + +const fn move_wide(register: u8, immediate: u16) -> u32 { + 0xd280_0000 | ((immediate as u32) << 5) | (register as u32) +} + +fn write_u16(bytes: &mut [u8], offset: usize, value: u16) { + bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); +} + +fn write_u32(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn write_u64(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +} + +#[cfg(test)] +mod tests { + use super::{MESSAGE, execute_hello_world}; + + #[test] + fn executes_the_browser_fixture_on_the_native_test_host() { + let result = execute_hello_world().unwrap(); + + assert_eq!(result.stdout.as_bytes(), MESSAGE); + assert!(result.stderr.is_empty()); + assert_eq!(result.exit_code, 0); + assert_eq!(result.executed_instructions, 8); + assert_eq!(result.dispatched_syscalls, 2); + assert_eq!(result.output_bytes, MESSAGE.len() as u64); + assert_eq!( + result.trace, + "write(fd=1, buf=0x1120, count=13) = 13\nexit(status=0) -> exit 0" + ); + } +} diff --git a/crates/linux-runtime/src/lib.rs b/crates/linux-runtime/src/lib.rs index ae4831a..b523965 100644 --- a/crates/linux-runtime/src/lib.rs +++ b/crates/linux-runtime/src/lib.rs @@ -21,6 +21,42 @@ pub struct ExecutionResult { pub output_bytes: u64, } +/// Observable result of one completed Linux syscall dispatch. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SyscallOutcome { + Returned(u64), + Exited(u8), +} + +/// Stable trace record captured at the architecture/Linux boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SyscallEvent { + pub number: u64, + pub arguments: [u64; 6], + pub outcome: SyscallOutcome, +} + +impl fmt::Display for SyscallEvent { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match Syscall::from_number(self.number) { + Some(Syscall::Write) => write!( + formatter, + "write(fd={}, buf={:#x}, count={})", + self.arguments[0], self.arguments[1], self.arguments[2], + )?, + Some(Syscall::Exit) => write!(formatter, "exit(status={})", self.arguments[0])?, + Some(Syscall::ExitGroup) => { + write!(formatter, "exit_group(status={})", self.arguments[0])?; + } + None => write!(formatter, "syscall({})", self.number)?, + } + match self.outcome { + SyscallOutcome::Returned(value) => write!(formatter, " = {value}"), + SyscallOutcome::Exited(code) => write!(formatter, " -> exit {code}"), + } + } +} + /// A failure outside normal Linux syscall return handling. #[derive(Debug)] pub enum ExecutionError { @@ -53,6 +89,7 @@ pub struct Process { executed_instructions: u64, dispatched_syscalls: u64, output_bytes: u64, + trace: Vec, } impl Process { @@ -71,6 +108,7 @@ impl Process { executed_instructions: 0, dispatched_syscalls: 0, output_bytes: 0, + trace: Vec::new(), }) } @@ -84,6 +122,11 @@ impl Process { &self.memory } + #[must_use] + pub fn trace(&self) -> &[SyscallEvent] { + &self.trace + } + /// Run until `exit`/`exit_group`, a host failure, or a project resource trap. /// /// Unsupported syscall numbers return `-ENOSYS` in `x0` and execution @@ -108,6 +151,11 @@ impl Process { .run_until_supervisor_call(&mut self.state, &self.memory, remaining) .map_err(ExecutionError::Trap)?; self.executed_instructions += stop.executed_instructions; + if self.dispatched_syscalls >= self.limits.syscall_budget { + return Err(ExecutionError::Trap(Trap::ResourceLimit( + ResourceLimit::Syscalls, + ))); + } self.dispatched_syscalls += 1; if stop.supervisor_call.immediate != 0 { @@ -116,17 +164,39 @@ impl Process { )); } - match Syscall::from_number(stop.supervisor_call.syscall_number) { - Some(Syscall::Write) => self.dispatch_write(terminal)?, + let number = stop.supervisor_call.syscall_number; + let arguments = self.syscall_arguments(); + match Syscall::from_number(number) { + Some(Syscall::Write) => { + self.dispatch_write(terminal)?; + self.trace.push(SyscallEvent { + number, + arguments, + outcome: SyscallOutcome::Returned(self.register(0)), + }); + } Some(Syscall::Exit | Syscall::ExitGroup) => { + let exit_code = self.register(0).to_le_bytes()[0]; + self.trace.push(SyscallEvent { + number, + arguments, + outcome: SyscallOutcome::Exited(exit_code), + }); return Ok(ExecutionResult { - exit_code: self.register(0).to_le_bytes()[0], + exit_code, executed_instructions: self.executed_instructions, dispatched_syscalls: self.dispatched_syscalls, output_bytes: self.output_bytes, }); } - None => self.set_return(Errno::NoSystemCall.return_value()), + None => { + self.set_return(Errno::NoSystemCall.return_value()); + self.trace.push(SyscallEvent { + number, + arguments, + outcome: SyscallOutcome::Returned(self.register(0)), + }); + } } } } @@ -180,6 +250,12 @@ impl Process { .set_x(0, value) .expect("Linux return register is valid"); } + + fn syscall_arguments(&self) -> [u64; 6] { + core::array::from_fn(|index| { + self.register(u8::try_from(index).expect("six syscall arguments fit u8")) + }) + } } #[cfg(test)] @@ -191,7 +267,7 @@ mod tests { use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; use binarrow_runtime_core::{ResourceLimit, Trap}; - use super::{ExecutionError, Process}; + use super::{ExecutionError, Process, SyscallEvent, SyscallOutcome}; const MESSAGE: &[u8] = b"hello, world\n"; const MESSAGE_LENGTH: u16 = 13; @@ -229,6 +305,21 @@ mod tests { assert_eq!(result.output_bytes, MESSAGE.len() as u64); assert_eq!(terminal.standard_output, MESSAGE); assert!(terminal.standard_error.is_empty()); + assert_eq!( + process.trace(), + [ + SyscallEvent { + number: 64, + arguments: [1, u64::from(MESSAGE_ADDRESS), 13, 0, 0, 0], + outcome: SyscallOutcome::Returned(13), + }, + SyscallEvent { + number: 93, + arguments: [0, u64::from(MESSAGE_ADDRESS), 13, 0, 0, 0], + outcome: SyscallOutcome::Exited(0), + }, + ] + ); } #[test] @@ -248,6 +339,24 @@ mod tests { assert!(terminal.standard_output.is_empty()); } + #[test] + fn syscall_limit_bounds_dispatch_and_trace_growth() { + let mut config = ProcessConfig::default(); + config.limits.syscall_budget = 1; + let image = load_hello(config, 1, MESSAGE_ADDRESS); + let mut process = Process::new(image).unwrap(); + let mut terminal = RecordingTerminal::default(); + + let error = process.run(&mut terminal).unwrap_err(); + + assert!(matches!( + error, + ExecutionError::Trap(Trap::ResourceLimit(ResourceLimit::Syscalls)) + )); + assert_eq!(terminal.standard_output, MESSAGE); + assert_eq!(process.trace().len(), 1); + } + #[test] fn invalid_write_arguments_return_errno_without_host_side_effects() { for (file_descriptor, address, errno) in [ diff --git a/crates/runtime-core/src/lib.rs b/crates/runtime-core/src/lib.rs index 0a8e825..df44de0 100644 --- a/crates/runtime-core/src/lib.rs +++ b/crates/runtime-core/src/lib.rs @@ -58,6 +58,8 @@ pub struct ResourceLimits { pub max_memory_bytes: u64, /// Maximum guest instructions before yielding or terminating. pub instruction_budget: u64, + /// Maximum Linux syscalls dispatched before termination. + pub syscall_budget: u64, /// Maximum bytes accepted from stdout and stderr together. pub max_output_bytes: u64, /// Maximum simultaneously open guest descriptors. @@ -69,6 +71,7 @@ impl Default for ResourceLimits { Self { max_memory_bytes: 256 * 1024 * 1024, instruction_budget: 10_000_000, + syscall_budget: 100_000, max_output_bytes: 4 * 1024 * 1024, max_open_files: 256, } @@ -103,6 +106,7 @@ pub enum MemoryAccess { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ResourceLimit { Instructions, + Syscalls, Memory, Output, OpenFiles, @@ -128,6 +132,7 @@ mod tests { assert!(limits.max_memory_bytes > 0); assert!(limits.instruction_budget > 0); + assert!(limits.syscall_budget > 0); assert!(limits.max_output_bytes > 0); assert!(limits.max_open_files > 0); } diff --git a/deny.toml b/deny.toml index d39ca70..3831ca9 100644 --- a/deny.toml +++ b/deny.toml @@ -17,6 +17,7 @@ allow = [ "Apache-2.0 WITH LLVM-exception", "MIT", "Unicode-3.0", + "Zlib", ] [[licenses.clarify]] diff --git a/docs/architecture.md b/docs/architecture.md index caf36be..52dac3c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -64,7 +64,11 @@ Defines host services without choosing a native or browser implementation. The f ### `binarrow-linux-runtime` -Consumes a loaded process image, owns its architectural execution state, and repeatedly runs the interpreter to structured supervisor-call stops. The first dispatcher implements AArch64 Linux `write`, `exit`, and `exit_group`; unsupported calls return `ENOSYS`, invalid write arguments return `EBADF`/`EFAULT`, and output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. +Consumes a loaded process image, owns its architectural execution state, and repeatedly runs the interpreter to structured supervisor-call stops. The first dispatcher implements AArch64 Linux `write`, `exit`, and `exit_group`; unsupported calls return `ENOSYS`, invalid write arguments return `EBADF`/`EFAULT`, and output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. Every completed dispatch also appends a project-owned trace event whose arguments are captured before return-register mutation; an explicit syscall budget bounds trace growth. + +### `binarrow-browser-runtime` + +Provides the narrow wasm-bindgen boundary used by the Worker. Its current checkpoint embeds the same deterministic static hello-world ELF shape used by native tests, invokes the production loader/runtime, captures the terminal host trait, and exposes output, exit status, counters, and formatted syscall events. No AArch64 or Linux behavior is reimplemented in TypeScript. ### `binarrow-cli` @@ -74,10 +78,14 @@ Owns native filesystem/terminal access and stable human-readable rendering. Its Owns generated WebAssembly feature fixtures. Keeping the bytes generated by `wasm-encoder` makes feature probes reviewable and prevents checked-in binary drift. +### Browser build tooling + +`tools/wasm-bindgen` pins the binding generator as an ordinary workspace dependency and is invoked by `web/scripts/generate-probes.mjs`. Browser-runtime Wasm and generated JavaScript/TypeScript bindings are build outputs rather than committed artifacts. This keeps local and CI builds on the same wasm-bindgen revision without requiring a globally installed CLI. + ## Planned next boundaries The Icicle feasibility spike is isolated under `experiments/icicle`; it is not a dependency of `runtime-core`. [ADR-0001](decisions/0001-adopt-selected-icicle-components.md) selects Icicle's SLEIGH/P-code/CPU interpreter components behind project-owned IR and memory adapters while excluding its native JIT and host-coupled runtime boundary. `experiments/icicle-wasm` separately validates Icicle's lightweight `pcode` crate in raw `wasm32-unknown-unknown`. The production `binarrow-aarch64` crate pins `pcode`, `sleigh-runtime`, and the filesystem-free portion of `sleigh-compile` at that same revision and keeps their types private. The required AHash/getrandom browser backend is selected in the workspace's target configuration; `icicle-cpu` and its native VM/JIT remain outside the production graph. -The next production boundary is browser execution of the same static hello-world fixture. The Worker should supply the terminal host trait, surface output and the exit result to the page, preserve the existing resource limits, and add a stable syscall trace without duplicating Linux behavior in JavaScript. +The next production boundary is a compiler-produced static program. That requires expanding normalized AArch64 control-flow/load-store semantics and adding the concrete startup syscalls observed from the chosen musl fixture, beginning with `brk` and anonymous `mmap` where its trace demonstrates they are needed. diff --git a/docs/licensing.md b/docs/licensing.md index 25ea174..5f2ef25 100644 --- a/docs/licensing.md +++ b/docs/licensing.md @@ -25,8 +25,10 @@ Before adopting a foundational emulator, decoder, ABI source, runtime, or toolch | `object` | Parse ELF format, architecture, and image-kind metadata | MIT OR Apache-2.0 | Rust dependency with only `elf`, `read_core`, and `std` features | | `libfuzzer-sys` | Exercise the ELF byte parser with generated inputs | (MIT OR Apache-2.0) AND NCSA | Isolated fuzz-workspace dependency with its own policy file | | `wasm-feature-detect` | Canonical browser feature probes | Apache-2.0 | JavaScript dependency | +| `wasm-bindgen` and `wasm-bindgen-cli-support` | Rust browser ABI and reproducible JavaScript/TypeScript glue generation | MIT OR Apache-2.0 | Rust runtime and repository-local build-tool dependencies | | Vite, TypeScript, Playwright | Build, type-check, and test the probe | MIT / Apache-2.0 | Development dependencies | | `unicode-ident` | Unicode identifier tables used by the wasm-bindgen macro toolchain | (MIT OR Apache-2.0) AND Unicode-3.0 | Transitive Rust dependency | +| `foldhash` | Hashing used transitively by wasmparser/walrus in the binding generator | Zlib | Transitive Rust build-tool dependency | | Icicle `pcode`, `sleigh-runtime`, and `sleigh-compile` | Production AArch64 decode/lift input behind project-owned state and operations | MIT OR Apache-2.0 | Pinned Git dependencies; no copied Rust source | | Other selected Icicle components | Native CPU/interpreter feasibility and future semantic reference | MIT OR Apache-2.0 | Pinned isolated experiment; not in the production graph | | Ghidra AArch64 language data | Production AArch64 decode and semantics specification | Apache-2.0 | Pinned vendored source closure with retained `LICENSE`, `NOTICE`, certification manifest, and provenance README | diff --git a/tools/wasm-bindgen/Cargo.toml b/tools/wasm-bindgen/Cargo.toml new file mode 100644 index 0000000..2717405 --- /dev/null +++ b/tools/wasm-bindgen/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "binarrow-wasm-bindgen-tool" +description = "Pinned wasm-bindgen glue generator for binarrow web builds" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +wasm-bindgen-cli-support = "=0.2.126" + +[lints] +workspace = true diff --git a/tools/wasm-bindgen/src/main.rs b/tools/wasm-bindgen/src/main.rs new file mode 100644 index 0000000..620d887 --- /dev/null +++ b/tools/wasm-bindgen/src/main.rs @@ -0,0 +1,20 @@ +use std::{env, error::Error, path::PathBuf}; + +use wasm_bindgen_cli_support::Bindgen; + +fn main() -> Result<(), Box> { + let mut arguments = env::args_os().skip(1); + let input = PathBuf::from(arguments.next().ok_or("missing input WebAssembly path")?); + let output = PathBuf::from(arguments.next().ok_or("missing output directory")?); + if arguments.next().is_some() { + return Err("expected exactly an input WebAssembly path and output directory".into()); + } + + let mut bindgen = Bindgen::new(); + bindgen + .input_path(input) + .out_name("binarrow_browser_runtime") + .typescript(true); + bindgen.web(true)?.generate(output)?; + Ok(()) +} diff --git a/web/index.html b/web/index.html index 55cd8c2..b325732 100644 --- a/web/index.html +++ b/web/index.html @@ -12,15 +12,31 @@
-

Phase 0 feasibility

-

binarrow browser probe

+

Phase 1 execution checkpoint

+

binarrow browser runtime

- This Worker checks the WebAssembly capabilities required by the planned - AArch64 userspace runtime. Memory64 is tested by instantiating a module - generated from Rust with wasm-encoder; JSPI and the - browser-portable P-code path are exercised end to end. + This Worker loads a static AArch64 ELF, executes its real SLEIGH + semantics, dispatches Linux system calls in Rust, and reports the guest + terminal and trace alongside the required WebAssembly capabilities.

Starting probe Worker…

+
+

Static AArch64 execution

+

+        
+
Exit code
+
—
+
Instructions
+
—
+
System calls
+
—
+
Output bytes
+
—
+
+

System call trace

+

+      
+

WebAssembly capabilities

diff --git a/web/package.json b/web/package.json index 9454ce6..8cc37d9 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "dev": "vite --host 127.0.0.1", "generate:probes": "node scripts/generate-probes.mjs", "prebuild": "pnpm generate:probes", + "precheck": "pnpm generate:probes", "pretest": "pnpm generate:probes", "test": "playwright test" }, diff --git a/web/scripts/generate-probes.mjs b/web/scripts/generate-probes.mjs index 55c3b82..3fede85 100644 --- a/web/scripts/generate-probes.mjs +++ b/web/scripts/generate-probes.mjs @@ -1,4 +1,4 @@ -import { copyFileSync, mkdirSync } from "node:fs"; +import { copyFileSync, mkdirSync, rmSync } from "node:fs"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; @@ -14,8 +14,19 @@ const semanticArtifact = fileURLToPath( import.meta.url, ), ); +const browserRuntimeArtifact = fileURLToPath( + new URL( + "../../target/wasm32-unknown-unknown/release/binarrow_browser_runtime.wasm", + import.meta.url, + ), +); +const browserBindingsDirectory = fileURLToPath( + new URL("../src/generated/", import.meta.url), +); mkdirSync(publicDirectory, { recursive: true }); +rmSync(browserBindingsDirectory, { recursive: true, force: true }); +mkdirSync(browserBindingsDirectory, { recursive: true }); execFileSync( "cargo", @@ -50,3 +61,33 @@ copyFileSync( semanticArtifact, fileURLToPath(new URL("../public/icicle-semantic-probe.wasm", import.meta.url)), ); + +execFileSync( + "cargo", + [ + "build", + "--quiet", + "--locked", + "--release", + "--target", + "wasm32-unknown-unknown", + "-p", + "binarrow-browser-runtime", + ], + { cwd: repositoryDirectory, stdio: "inherit" }, +); + +execFileSync( + "cargo", + [ + "run", + "--quiet", + "--locked", + "-p", + "binarrow-wasm-bindgen-tool", + "--", + browserRuntimeArtifact, + browserBindingsDirectory, + ], + { cwd: repositoryDirectory, stdio: "inherit" }, +); diff --git a/web/src/main.ts b/web/src/main.ts index 1c7b2b5..4849bb7 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -4,9 +4,24 @@ import type { ProbeMessage } from "./probe"; const status = document.querySelector("#status"); const report = document.querySelector("#report"); +const terminal = document.querySelector("#terminal"); +const syscallTrace = document.querySelector("#syscall-trace"); +const exitCode = document.querySelector("#exit-code"); +const instructionCount = document.querySelector("#instruction-count"); +const syscallCount = document.querySelector("#syscall-count"); +const outputCount = document.querySelector("#output-count"); -if (!status || !report) { - throw new Error("feature report markup is incomplete"); +if ( + !status || + !report || + !terminal || + !syscallTrace || + !exitCode || + !instructionCount || + !syscallCount || + !outputCount +) { + throw new Error("runtime report markup is incomplete"); } const worker = new Worker(new URL("./probe.worker.ts", import.meta.url), { @@ -20,9 +35,19 @@ worker.addEventListener("message", (event: MessageEvent) => { return; } - status.textContent = "Feature report ready"; + status.textContent = "Browser runtime ready"; status.dataset.state = "ready"; + terminal.textContent = event.data.execution.stdout; + if (event.data.execution.stderr) { + terminal.textContent += event.data.execution.stderr; + } + syscallTrace.textContent = event.data.execution.trace; + exitCode.textContent = String(event.data.execution.exitCode); + instructionCount.textContent = String(event.data.execution.executedInstructions); + syscallCount.textContent = String(event.data.execution.dispatchedSyscalls); + outputCount.textContent = String(event.data.execution.outputBytes); + for (const result of event.data.results) { const term = document.createElement("dt"); term.textContent = result.name; diff --git a/web/src/probe.ts b/web/src/probe.ts index 4b04d61..fee89b4 100644 --- a/web/src/probe.ts +++ b/web/src/probe.ts @@ -18,6 +18,16 @@ export interface FeatureResult { detail?: string; } +export interface ExecutionReport { + stdout: string; + stderr: string; + trace: string; + exitCode: number; + executedInstructions: bigint; + dispatchedSyscalls: bigint; + outputBytes: bigint; +} + export type ProbeMessage = - | { kind: "report"; results: FeatureResult[] } + | { kind: "report"; results: FeatureResult[]; execution: ExecutionReport } | { kind: "error"; message: string }; diff --git a/web/src/probe.worker.ts b/web/src/probe.worker.ts index b004efe..915165d 100644 --- a/web/src/probe.worker.ts +++ b/web/src/probe.worker.ts @@ -6,8 +6,16 @@ import { tailCall, typedFunctionReferences, } from "wasm-feature-detect"; +import initBrowserRuntime, { + run_hello_world, +} from "./generated/binarrow_browser_runtime.js"; -import type { FeatureName, FeatureResult, ProbeMessage } from "./probe"; +import type { + ExecutionReport, + FeatureName, + FeatureResult, + ProbeMessage, +} from "./probe"; interface Memory64Exports extends WebAssembly.Exports { memory: WebAssembly.Memory; @@ -124,9 +132,32 @@ async function createReport(): Promise { ]); } -createReport() - .then((results) => { - const message: ProbeMessage = { kind: "report", results }; +async function executeStaticHelloWorld(): Promise { + await initBrowserRuntime({ + module_or_path: new URL( + "./generated/binarrow_browser_runtime_bg.wasm", + import.meta.url, + ), + }); + const result = run_hello_world(); + try { + return { + stdout: result.stdout, + stderr: result.stderr, + trace: result.trace, + exitCode: result.exit_code, + executedInstructions: result.executed_instructions, + dispatchedSyscalls: result.dispatched_syscalls, + outputBytes: result.output_bytes, + }; + } finally { + result.free(); + } +} + +Promise.all([createReport(), executeStaticHelloWorld()]) + .then(([results, execution]) => { + const message: ProbeMessage = { kind: "report", results, execution }; self.postMessage(message); }) .catch((error) => { diff --git a/web/src/styles.css b/web/src/styles.css index e6875bf..591d48d 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -31,6 +31,14 @@ h1 { line-height: 0.95; } +h2 { + margin: 2.5rem 0 0.8rem; + font-family: system-ui, sans-serif; + font-size: 1rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + .lede { max-width: 47rem; margin: 2rem 0; @@ -61,6 +69,18 @@ dl { border-top: 1px solid #aaa38c; } +pre { + min-height: 1.4rem; + margin: 0; + padding: 1rem; + overflow-x: auto; + border: 1px solid #aaa38c; + background: #171713; + color: #e8e3d2; + line-height: 1.5; + white-space: pre-wrap; +} + dt, dd { margin: 0; diff --git a/web/tests/probe.spec.ts b/web/tests/probe.spec.ts index ee63023..6e6d742 100644 --- a/web/tests/probe.spec.ts +++ b/web/tests/probe.spec.ts @@ -1,12 +1,22 @@ import { expect, test } from "@playwright/test"; -test("executes the required Phase 0 proofs in a Worker", async ({ +test("executes a static AArch64 hello-world ELF in a Worker", async ({ browser, page, }) => { await page.goto("/"); - await expect(page.getByRole("status")).toHaveText("Feature report ready"); + await expect(page.getByRole("status")).toHaveText("Browser runtime ready"); + await expect(page.getByLabel("Guest terminal output")).toHaveText( + "hello, world", + ); + await expect(page.locator("#exit-code")).toHaveText("0"); + await expect(page.locator("#instruction-count")).toHaveText("8"); + await expect(page.locator("#syscall-count")).toHaveText("2"); + await expect(page.locator("#output-count")).toHaveText("13"); + await expect(page.getByLabel("System call trace")).toHaveText( + "write(fd=1, buf=0x1120, count=13) = 13\nexit(status=0) -> exit 0", + ); for (const requiredFeature of [ "Memory64", "AArch64 P-code semantics", -- 2.54.0