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
WebAssemblynamespace 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
DataCountparsing 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 thetable.*instruction family. - Fixed-width SIMD: the
v128type 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_indirectvalidate 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 blockingatomic.wait/atomic.notifyagent semantics are not (see below). - Exception handling:
try_tablewithcatch/catch_ref,throw, andthrow_refexecute with real unwinding, and the host installsWebAssembly.TagandWebAssembly.Exception(includingException.getArgandException.is). The legacyrethrowopcode is the one piece not carried. - GC (struct and array subset):
struct.new/struct.get,array.new/array.getand their variants, andref.castexecute. 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 BigIntmemory.growwork. - 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/dataoffsets) validates and executes. - Shared-memory objects:
new WebAssembly.Memory({ shared: true })returns aSharedArrayBuffer, 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.bufferwith 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.notifyagent model, and the legacy exceptionrethrowopcode, are not implemented. - The full GC surface: beyond the struct/array/
ref.castsubset above, the remaining typed-reference and cast forms are not complete. - JS Promise Integration:
WebAssembly.Suspending/promisingexist in stub form only; suspension does not fully work. - Streaming/async depth:
compileStreaming/instantiateStreamingare 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.