Zoom

Zoom is Cruft's WebAssembly implementation: its own binary parser, validator, and interpreter, plus the host bridge that installs the WebAssembly namespace. It runs core MVP modules and a broad set of proposals (multi-value, SIMD, reference types, atomics, exceptions, tail calls, and more) that real npm packages need, under a per-call execution-fuel bound. This page covers what runs today, the memory-safety guarantee, and that bound.

Zoom is Cruft's WebAssembly implementation: its own binary parser, validator, and interpreter, with a JavaScript host bridge installing the WebAssembly namespace. This page describes the architecture, what is implemented today, and the distance to full parity. V8/Node are used as the empirical parity target, never a source for the implementation.

Architecture: engine and bridge

Zoom splits along the same engine/host seam as the rest of the runtime:

  • The engine is the WebAssembly engine proper: LEB128 decoding, the binary parser over the module sections, validation, numeric semantics, and an interpreter. It knows nothing about JavaScript.
  • The host bridge installs the JavaScript-facing WebAssembly namespace and mediates between JS values and engine state, instantiating modules, wiring imports (including JS functions), and exposing memory to JavaScript.

The namespace surface installed today is the full constructor-shaped set that package feature-detection probes for: Module, Instance, Memory, Table, Global, Tag, Exception, the three error classes (CompileError, LinkError, RuntimeError), compile / instantiate / validate, and the streaming entry points compileStreaming / instantiateStreaming (correct-shaped, synchronously wrapped).

What is implemented

The engine is an MVP-plus interpreter: the WebAssembly core MVP plus a broad set of post-MVP proposals, chosen by what real npm payloads need and confirmed by running them.

  • All core module sections: type, import, function, table, memory, global, export, start, element, code, data, plus DataCount parsing and ordering (a section modern toolchains emit and older parsers choke on).
  • Scalar types and MVP execution: i32/i64/f32/f64, control flow, locals/globals, direct and indirect calls, the numeric instruction set, linear-memory loads/stores, memory.size/memory.grow.
  • Selected post-MVP opcodes: sign extension, non-trapping float-to-int conversions, bulk memory operations, passive data segments.
  • Multi-value: multiple return values and typed (multi-value) block signatures validate, instantiate, and execute; a ()->(i32,i32) function returns [1, 2] through its real path.
  • Reference types: externref (host-reference identity round-trips, f(o) === o), ref.null/ref.func, and the table.* instruction family.
  • Fixed-width SIMD: the v128 type and the fixed-width SIMD instruction set (v128.const, i32x4.splat, lane extraction, etc.) validate and execute.
  • Relaxed SIMD: the relaxed-SIMD set (relaxed madd/nmadd, relaxed dot, q15mulr, and the rest) validates and executes.
  • Tail calls: return_call/return_call_indirect validate and execute.
  • Atomics: atomic load, store, read-modify-write, compare-exchange, and fence execute over a shared Memory. The non-blocking model is complete; the blocking atomic.wait/atomic.notify agent semantics are not (see below).
  • Exception handling: try_table with catch/catch_ref, throw, and throw_ref execute with real unwinding, and the host installs WebAssembly.Tag and WebAssembly.Exception (including Exception.getArg and Exception.is). The legacy rethrow opcode is the one piece not carried.
  • GC (struct and array subset): struct.new/struct.get, array.new/array.get and their variants, and ref.cast execute. The full GC proposal (every typed-reference and cast form) is not complete.
  • memory64: 64-bit memories (new WebAssembly.Memory({ address: "i64" })), i64 addressing, and BigInt memory.grow work.
  • Multi-memory: instructions carry an explicit memory index, so a module with more than one linear memory validates and executes.
  • Extended constant expressions: arithmetic in initializer expressions (global/element/data offsets) validates and executes.
  • Shared-memory objects: new WebAssembly.Memory({ shared: true }) returns a SharedArrayBuffer, and the atomic opcodes above run against it.
  • Host integration: JS function imports, and a partial WASI preview1 surface (enough for file preopen/read probe paths), with per-instance WASI state so each instance has its own file-descriptor table.
  • Memory exposure: WebAssembly.Memory.buffer with boundary-synced semantics sufficient for current package probes (not yet V8-grade zero-copy alias/detach behavior, an approximation).

The wins that matter are empirical, per the project's standards: real npm modules execute through their real paths, xxhash-wasm runs, and the Vite/OXC WASM import path passes (it was the motivation for the DataCount work). The selection principle is visible there: proposals get implemented when a production payload demands them, lowest-tier first.

What is not yet implemented

  • A WASM JIT: the engine is interpreter-only. It parses and interprets WebAssembly; it does not compile to native code. Correctness comes first, and a WASM JIT is not part of the engine today.
  • Blocking atomics and legacy rethrow: the atomic.wait/atomic.notify agent model, and the legacy exception rethrow opcode, are not implemented.
  • The full GC surface: beyond the struct/array/ref.cast subset above, the remaining typed-reference and cast forms are not complete.
  • JS Promise Integration: WebAssembly.Suspending/promising exist in stub form only; suspension does not fully work.
  • Streaming/async depth: compileStreaming/instantiateStreaming are a correct-shaped synchronous implementation wrapped in promises, without spec-grade async timing.
  • Exhaustive validation: the operand-stack and control-flow type-check is complete for the shipped opcode set, but full spec-grade coverage of every proposal's limits and edge cases is still in progress.

Memory safety and the execution bound

The security properties that matter for running untrusted .wasm hold up well, and the memory-safety result is strong. Validation is mandatory and inseparable from parsing: the only way to obtain a module is through the parser, which runs a full set of validation passes (including a complete operand-stack and control-flow type-check of every function body) before returning, and instantiation takes only an already-parsed module, so there is no path that reaches the interpreter with an unvalidated module. Every linear-memory load and store bounds-checks the effective address against the current memory size, and the base + offset and addr + size arithmetic uses checked addition, so the classic offset-overflow bypass is closed. The parser and interpreter contain zero unsafe. The net effect, with no raw-pointer path anywhere in the engine: a malformed or hostile module cannot achieve an out-of-bounds read or write in the host process; the worst it can do is trap.

The execution bound is a fuel budget, not a wall-clock timeout. The interpreter charges fuel per instruction and traps when it runs out, so a module whose exported function is a bare loop br 0 does not hang the host: it runs to the budget and throws WebAssembly: execution fuel exhausted, and the host thread continues. The default is 10,000,000 instructions per top-level call, reset for each call and overridable with CRUFT_WASM_EXECUTION_FUEL. Because it counts instructions rather than time, the trap point is deterministic across machines, and because the budget is a ceiling beneath the language, a module cannot raise its own.

One boundary detail: WebAssembly is not present inside a Compartment (typeof WebAssembly there is undefined), so a compartment timeout_ms does not apply to WASM. Run untrusted modules in a worker: the fuel budget already stops an infinite loop from wedging the thread, and the worker adds the usual heap and lifecycle isolation. memory.grow is capped at the memory's declared maximum and grows lazily rather than reserving the 4 GiB ceiling up front.

Using it today

Practical guidance:

  • Packages shipping MVP-era or conservatively-compiled WASM (hash libraries, codecs, parsers compiled with older or compatibility-minded toolchains) are the supported case, and the diff-prod/npm lanes are the evidence base for which ones. Modern toolchain output that leans on multi-value, reference types, SIMD, atomics, exceptions, or tail calls is now also supported (see "What is implemented").
  • Payloads using a not-yet-implemented feature (blocking atomic.wait, the full GC surface, working JS Promise Integration) are refused by the parser or validator rather than mis-executed. For the features that are implemented, the guarantee is the same as for the MVP core: if it loads, it is meant to be right.
  • Parity and feature claims route to the WASM verification lanes; this page intentionally states shapes, not pass rates.