06 — WebAssembly Runtime
Overview
WebAssembly (WASM) is a binary instruction format and execution model designed as a portable compilation target for programming languages. Standardized by the W3C in 2019, it was initially created to solve a specific problem: running near-native-speed code in web browsers without relying on JavaScript performance. It has since evolved into a general-purpose portable execution environment, running in server-side runtimes, edge computing platforms, serverless functions, and embedded devices. WebAssembly's design — a sandboxed virtual machine with deterministic execution, compact binary encoding, and a capability-based security model — makes it one of the most carefully designed runtime specifications since the JVM.
Prerequisites
- Understanding of virtual machines and bytecode interpretation (see
29-runtime-systems/01-jvm-architecture.md) - Familiarity with JIT compilation concepts (see
29-runtime-systems/02-jit-compilation.md) - Basic understanding of memory models and stack machines
- Some exposure to compilation (see
30-compilers-and-linkers/01-compilation-pipeline.md)
Historical Context
The Problem WebAssembly Was Designed to Solve
By 2013, web browsers were running increasingly complex applications: games, video editors, office suites. JavaScript's JIT compilers (V8's Crankshaft, SpiderMonkey's IonMonkey) delivered impressive performance for idiomatic JavaScript, but code compiled from C++ to JavaScript via Emscripten (using the asm.js subset) still had unpredictable performance due to JavaScript's dynamic semantics.
Mozilla engineers proposed asm.js in 2013: a strict subset of JavaScript with type annotations that JIT compilers could optimize to near-native speeds. Google, Microsoft, Apple, and Mozilla then collaborated on a new design — one that abandoned JavaScript syntax entirely and specified a proper binary format with a formal semantic model.
WebAssembly 1.0 shipped in all four major browsers (Chrome 57, Firefox 52, Safari 11, Edge 16) by late 2017. The W3C ratified it as a full recommendation in December 2019. The specification lives at https://webassembly.github.io/spec/.
Design Goals
WebAssembly's design goals are stated explicitly in its specification and are worth examining as engineering decisions:
- Safe: Execution is sandboxed. WASM code cannot access host memory, make system calls, or escape its execution environment without explicit grants from the host.
- Fast: Near-native execution speed through efficient compilation. The binary format is designed to be decoded and compiled faster than JavaScript can be parsed and compiled.
- Portable: One binary runs on x86-64, ARM64, RISC-V, or any architecture a WASM runtime supports. No recompilation for different targets.
- Compact: Binary format is smaller than equivalent JavaScript and compresses well. A compiled C++ function is typically 2–5x smaller as WASM than as minified JavaScript.
- Deterministic: Given identical inputs, WASM executes identically on all conforming runtimes. (NaN bit patterns and floating-point rounding are fully specified.)
- Language-neutral: Any language that can compile to WASM binary is a first-class WASM language: C, C++, Rust, Go, Swift, AssemblyScript, Kotlin.
WASM Virtual Machine Model
WebAssembly is a stack machine. Instructions pop operands from and push results onto a typed value stack.
WASM Execution Model:
Source: (i32.add (i32.const 40) (i32.const 2))
In binary (WAT text format):
i32.const 40 ;; push 40
i32.const 2 ;; push 2
i32.add ;; pop 40, pop 2, push 42
Value stack state:
Before: []
After const 40: [40]
After const 2: [40, 2]
After add: [42]
Linear Memory
A WASM module has a single flat byte array called linear memory. It is addressed by 32-bit indices (WASM 1.0; WASM 64-bit memory proposal extends this). Every memory access is bounds-checked: if an instruction accesses memory[index] where index >= memory.size, a trap (runtime exception) is raised immediately.
Linear memory is the only place WASM code can store mutable data structures. It is separate from the value stack (which only holds scalar values) and from host memory.
WASM Module Memory Layout:
+--------+--------+--------+--------+-----+
| 0x0000 | 0x0001 | 0x0002 | ...... | end |
+--------+--------+--------+--------+-----+
<---------- linear memory ----------------->
^ allocated in pages of 65,536 bytes (64 KB)
^ maximum size declared in module header
^ cannot alias host memory (complete isolation)
Host memory: (separate address space)
+------------------------+
| JavaScript heap |
| DOM objects |
| Host runtime objects |
+------------------------+
Memory can be grown at runtime with the memory.grow instruction, which returns the previous size in pages or -1 on failure. The module declares an optional maximum; the host enforces it.
Function Table and Indirect Calls
WASM has no raw function pointers. Indirect calls (equivalent to calling through a function pointer in C) go through a function table: an array of function references. The call_indirect instruction takes a table index and a type signature, and traps if the element at that index does not match the expected signature. This prevents type confusion attacks that would be possible with raw pointer arithmetic.
Global Variables and Module Instances
A WASM module is analogous to an object file: it declares imports (functions, memory, globals, tables the host must provide) and exports (functions, memory, globals the host can call). When a module is instantiated, the host provides concrete values for all imports, producing a module instance with its own linear memory and global state. Multiple instances of the same module share no state.
WASM Security Model
WebAssembly's security model is capability-based:
- WASM code has no ambient authority. It cannot open files, make network connections, read environment variables, or call operating system functions unless the host explicitly provides those capabilities as imported functions.
- All memory access is within the linear memory sandbox. Overflows and out-of-bounds accesses trap rather than corrupt adjacent memory.
- There are no unmanaged pointers. The value stack and local variables are typed and cannot hold raw memory addresses (only
i32/i64values used as indices into linear memory). - No shared mutable state between module instances by default (the threads proposal introduces SharedArrayBuffer-like shared memory, which is opt-in).
Security boundary:
WASM Module
+------------------------------------------+
| Linear memory (isolated byte array) |
| Value stack (typed scalars only) |
| Function table (type-checked calls only) |
| |
| Imported functions: |
| fd_write(fd, iovs, iovs_len, nwritten) | <- host provides this
| clock_time_get(id, precision, time) | <- host provides this
+------------------------------------------+
| ^
calls imported returns values
functions only to module
| |
+------v---------------------+------------+
| Host Runtime (JS engine / WASI runtime) |
| Validates all calls, enforces caps |
+------------------------------------------+
WASI: WebAssembly System Interface
Running WASM outside a browser requires OS capabilities (filesystem, networking, clocks, random number generation). WASI (WebAssembly System Interface) defines a standardized set of imported functions for these capabilities, designed around the principle of capability-based security: a WASM module is granted only the specific file descriptors, environment variables, and permissions it needs, not ambient access to the entire filesystem.
WASI 0.1 defined POSIX-like functions (fd_read, fd_write, path_open, etc.) as WebAssembly imports. WASI 0.2 (2024) introduces the Component Model, replacing the flat function import/export model with a typed interface definition language (WIT — WASM Interface Types).
WASM Binary Format
A .wasm file is a sequence of sections. Each section has a type byte, a byte-length, and content. The sections in order:
.wasm binary structure:
Magic: 0x00 0x61 0x73 0x6D ("\0asm")
Version: 0x01 0x00 0x00 0x00 (version 1)
[section type byte] [byte length (LEB128)] [content]
Type section (0x01): Function type signatures: (i32, i32) -> i32
Import section (0x02): Imports: module "wasi_snapshot_preview1" fn "fd_write"
Function section (0x03): Maps function index to type index
Table section (0x04): Function table declaration
Memory section (0x05): Linear memory size (initial, max pages)
Global section (0x06): Global variable declarations + init expressions
Export section (0x07): Exported names: "main" -> function index 5
Start section (0x08): Optional: function to call on instantiation
Element section (0x09): Function table initializers
Code section (0x0A): Function bodies (locals + instructions)
Data section (0x0B): Static data to copy into linear memory at init
Integers encoded as LEB128 (variable-length, space-efficient)
Floating point: IEEE 754 binary32/binary64, little-endian
The binary format is designed for fast single-pass decoding. Streaming compilation is possible: a browser can begin compiling earlier sections while still downloading later ones.
WASM in Browsers
All four major browser engines implement WASM with a two-tier compilation strategy:
Tier 1: Fast Baseline Compiler
V8 uses Liftoff, SpiderMonkey uses Baseline. These compilers do a single pass over WASM bytecode and emit machine code directly with minimal optimization. The goal is to minimize time-to-execution: a Liftoff compile is typically 5–10x faster than optimized compile. The resulting code is 2–3x slower than fully optimized code but runs immediately.
Tier 2: Optimizing JIT
V8 uses TurboFan (shared with JavaScript JIT), SpiderMonkey uses IonMonkey/Warp. These run in background threads while Liftoff code is executing. They apply register allocation, inlining, loop invariant code motion, and other classic optimizations. When the optimized version is ready, execution transparently switches to it.
Browser WASM compilation pipeline:
.wasm bytes arrive (streaming)
|
v
Validation (type checking, bytecode verification)
|
+----------> Liftoff (fast baseline)
| |
| Machine code (2-3x slower than optimal)
| |
| [execute immediately]
| |
v v
TurboFan (background) [running]
| |
Optimized code <----- (swap when ready)
|
[execute faster]
JavaScriptCore (Safari/WebKit) similarly uses BBQ (fast tier) and OMG (optimizing tier, based on B3).
Memory Safety in Browsers
Browser WASM runtimes place the linear memory allocation at a random offset from the process base (ASLR), and guard pages surround it. A WASM bounds-check trap results in a clean JavaScript exception, not a process crash or exploitable memory corruption.
WASM Outside the Browser: WASI Runtimes
Wasmtime
Wasmtime (Bytecode Alliance, Mozilla/Fastly/Intel) is the reference WASI runtime written in Rust. It uses Cranelift (also Rust) as its JIT compiler backend. Wasmtime provides:
- A command-line tool (
wasmtime run foo.wasm) - An embedding API for Rust, C, Python, Go, .NET
- WASI preview 1 and preview 2 support
- Fuel-based execution metering (halt a module after N instructions)
Wasmer
Wasmer (Wasmer Inc.) is an alternative runtime that supports multiple compiler backends: Cranelift, LLVM (for maximum optimization), and Singlepass (fastest startup, for serverless). Wasmer supports cross-compilation and packaging WASM modules as standalone executables for Linux, macOS, and Windows.
WasmEdge
WasmEdge (CNCF project, Second State) targets edge and cloud-native use cases. It has extensions for networking (WASI-socket), AI inference (TensorFlow, PyTorch via host bindings), and runs inside containers.
Edge Computing: Cloudflare Workers and Fastly
Cloudflare Workers (2017) and Fastly Compute@Edge (2019) were the first production serverless platforms built on WASM as the execution substrate.
Traditional serverless (AWS Lambda):
Request → Cold start → Node.js/Python runtime init → Function execute
Cold start: 100ms – 2000ms (JVM worse)
WASM serverless (Cloudflare Workers):
Request → WASM module instantiation → Function execute
Cold start: < 1ms (no OS process, no VM, module is pre-compiled)
Isolation: V8 isolates (not containers) — 1 isolate per tenant
Cloudflare Workers uses V8 isolates with WASM support to run 100,000+ tenant functions on a single machine with strict isolation and sub-millisecond cold starts. The key insight: WASM modules are pre-validated and pre-compiled at deploy time, so instantiation at request time is nearly free.
Fastly's Compute@Edge compiles WASM ahead-of-time at deploy time using an LLVM-based compiler, producing native code that runs at wire speed in their PoP (point of presence) network.
WASM and Rust
Rust has become the dominant systems programming language targeting WASM, for structural reasons:
- Rust's ownership model eliminates garbage collection — no GC pauses in WASM modules
- Rust's
wasm32-unknown-unknownandwasm32-wasitargets are tier 1 (fully supported) wasm-bindgen(for browser) andwit-bindgen(for WASI) generate bindings automatically- Rust's minimal runtime produces compact
.wasmbinaries (10–100 KB for many utilities) wasm-packprovides a complete toolchain for building and publishing browser WASM packages
C and C++ are also well-supported via Emscripten and the wasi-sdk. Go's WASM support is functional but produces larger binaries due to the Go runtime being included. AssemblyScript (TypeScript-like language that compiles directly to WASM) provides a gentler on-ramp for JavaScript developers.
The Component Model and WIT
WASM 1.0's import/export model is limited: only scalar numeric types can cross the WASM/host boundary. Passing strings, arrays, or structs requires manual linear memory manipulation — error-prone and language-specific.
The WASM Component Model (stabilizing in 2024) adds:
- WIT (WASM Interface Types): An IDL (interface definition language) for describing component APIs with rich types: strings, lists, records, variants (tagged unions), options, results.
- Component linking: Components can depend on other components, with the toolchain generating glue code for type adaptation.
- Language interoperability: A Rust component can call a Python component without either knowing the other's implementation language.
WIT interface example:
// calculator.wit
package example:calculator@1.0.0;
interface ops {
add: func(a: f64, b: f64) -> f64;
divide: func(a: f64, b: f64) -> result<f64, string>;
}
world calculator {
export ops;
}
wit-bindgen generates Rust/C/Python bindings from WIT files. This brings WASM closer to a universal binary interface (ABI) for composable software.
Debugging Notes
- WASM traps: Traps (out-of-bounds memory access, integer divide by zero, unreachable instruction, indirect call type mismatch) produce a runtime exception. In browsers, check the browser devtools Sources panel — WASM modules appear as decompiled WAT (text format) with source maps if the
.wasmincludes DWARF debug sections. - Source maps:
wasm-pack build --debugorcargo build --target wasm32-unknown-unknownwith debug symbols embeds DWARF information. Chrome DevTools can step through original Rust/C source lines. - Memory leaks in WASM: WASM's linear memory does not shrink automatically (no
memory.shrink). MonitorWebAssembly.Memory.buffer.byteLength. Use custom allocators (wee_alloc, dlmalloc) with explicitfree()and validate with address sanitizer during testing. - Wasmtime CLI debugging:
wasmtime --wasm-features all --cranelift-debug-verifier foo.wasmenables additional validation.WASMTIME_LOG=cranelift=debugdumps JIT compilation traces. - Performance profiling: Firefox's profiler supports WASM sampling. Chrome's Performance panel shows WASM in the flame chart. Use
console.time/console.timeEndaround WASM call boundaries to measure host-guest transition overhead.
Security Implications
- Spectre/Meltdown: WASM's performance relies on JIT compilation to native code. WASM running in browsers is subject to speculative execution side-channel attacks. Browser vendors mitigate this by reducing timer precision (
performance.now()resolution reduced to 1ms), adding COOP/COEP headers for SharedArrayBuffer, and site isolation (separate processes per origin). - WASM supply chain: WASM modules distributed via registries (npm, WAPM, bytecodealliance.github.io) can contain malicious code. Validate module signatures. The Component Model's capability system limits what an untrusted component can do, but its host-provided capabilities must be granted carefully.
- Linear memory as attack surface: A WASM module with a bug (buffer overflow within linear memory) can corrupt its own heap but cannot escape to host memory. This confines damage to within the module's data — a significant security improvement over native code.
- WASI capability leakage: WASI grants capabilities as file descriptors. A module granted access to
/var/logshould not receive the root filesystem. Use--dir /var/log(wasmtime CLI) or the Preopened Directories API to provide minimal, scoped filesystem access.
Performance Implications
- Near-native throughput: WASM compiled with TurboFan (V8) achieves within 10–20% of native performance for compute-intensive workloads (image processing, cryptography, compression). Memory-bound workloads may suffer from the bounds-check overhead on every array access (typically 1 additional comparison per load/store).
- Call overhead: Every call from JavaScript to WASM (or WASM to imported host function) involves a type-checked boundary crossing: ~5–20ns overhead. Minimize cross-boundary call frequency for hot paths; batch operations across the boundary.
- Binary size: WASM modules for complex applications can be large (several MB for C++ codebases). Use
wasm-opt(Binaryen) post-processing to reduce size 20–40%. Enablelz4or Brotli compression for WASM delivery over HTTP. - Startup time: Module compilation is proportional to binary size. Large modules (2 MB+) may take 50–200ms to compile in a browser. Use
WebAssembly.instantiateStreaming()for concurrent download+compile. Server-side, pre-compile to a cache withwasmtime compile.
Failure Modes
- Trap on integer overflow: Unlike most CPUs, WASM
i32.trunc_f64_straps on overflow rather than wrapping. Code ported from C that relies on UB truncation behavior must be audited. - Stack overflow: WASM has a call stack depth limit (typically 1000–65536 frames depending on runtime). Deep recursion in WASM will trap. Use iterative algorithms or trampolining for deep recursion.
- Memory exhaustion:
memory.growreturning -1 is a normal failure mode. WASM code must handle allocation failures; many C codebases abort on malloc failure, which produces a WASM trap. Test under constrained memory limits. - Determinism violations: WASM specifies NaN bit patterns for most operations, but certain edge cases (NaN propagation in SIMD, rounding in FMA on some CPUs) can produce non-deterministic results. Do not rely on bit-exact floating-point reproducibility across runtimes without explicit verification.
Modern Usage (2024–2025)
- WASI 0.2 (Component Model): Released early 2024, enabling composable WASM components with typed interfaces. Tooling (cargo-component, jco) has stabilized.
- WASM in Kubernetes:
containerdwith therunwasishim can run WASM modules as Kubernetes pods, using WasmEdge or Wasmtime as the runtime. The WASM containers start in <5ms vs. 500ms+ for OCI containers. - WASM for plugin systems: Applications embedding WASM as a plugin runtime: Envoy (WASM filter extensions), Zed (editor plugins in WASM), Shopify Functions (checkout customization in WASM), Istio (WASM extensions).
- AI inference in WASM: WASI-NN provides a portable neural network inference API. WasmEdge integrates TensorFlow Lite and PyTorch for edge inference without needing a GPU driver in the WASM module.
Future Directions
- WASM threads: The threads proposal (SharedArrayBuffer +
i32.atomic.wait) enables multi-threaded WASM modules sharing memory. Usable in Chrome with COOP/COEP headers. Enables parallelism within a WASM module. - GC proposal: Enables WASM to use the host runtime's garbage collector for managed language types (Java, Kotlin, Dart objects). Dart 3.3 and Kotlin WASM use this to produce smaller binaries by not embedding a GC.
- WASI 0.3 and beyond: Continued standardization of WASI interfaces: HTTP, sockets, key-value stores, SQL databases — making WASM modules that are fully portable across cloud providers.
- Quantum-safe WASM: WASM's determinism and sandboxing make it an attractive substrate for running post-quantum cryptographic algorithms in a controlled environment.
Exercises
-
First WASM binary: Write a Fibonacci function in Rust, compile to
wasm32-unknown-unknown, and call it from JavaScript usingWebAssembly.instantiateStreaming(). Measure performance against a pure JavaScript implementation for n=40. -
WASI program: Write a WASI program (Rust) that reads a file path from the command line, counts word frequencies, and writes results to stdout. Run it with
wasmtime --dir . word-count.wasm input.txt. Verify it cannot access files outside the granted directory. -
Memory inspection: In a browser, load a WASM module that uses linear memory. Use
new Uint8Array(wasmInstance.exports.memory.buffer)in DevTools to inspect the raw bytes of a data structure. Correlate with the source-level struct layout. -
Optimize with wasm-opt: Compile a non-trivial C program with Emscripten. Run
wasm-opt -O3on the output. Compare binary sizes and benchmark performance. What percentage improvement do you observe? -
Edge deployment: Deploy a Rust function as a Cloudflare Worker using WASM. Measure cold start latency from five geographically distributed locations using
curltiming. Compare with a cold-started AWS Lambda (same logic, Python runtime).
References
- WebAssembly Specification. https://webassembly.github.io/spec/core/
- Haas, A., et al. (2017). "Bringing the Web Up to Speed with WebAssembly." PLDI 2017.
- WASI Design Documentation. https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md
- Bytecode Alliance. https://bytecodealliance.org
- Wasmtime documentation. https://docs.wasmtime.dev
- Lin Clark. "An Illustrated Introduction to the WASM Component Model." https://bytecodealliance.org/articles/webassembly-component-model
- Cloudflare Workers documentation. https://developers.cloudflare.com/workers/
- Rossberg, A. "WebAssembly Reference Manual." https://webassembly.github.io/spec/core/bikeshed/
- Anderson, T. "WebAssembly: Neither Web nor Assembly." Strange Loop 2019.