WebAssembly and Portable Sandboxed Execution

WebAssembly is a compact bytecode designed to run safely and fast anywhere — in browsers and outside them.

Learning Objectives

Explain why WebAssembly exists; compare WAT and the binary format; describe modules, functions, tables, memories, globals and exports.

Explain types and validation, stack-machine semantics, linear memory, loads and stores, memory pages and growth, imports/exports, instantiation, host functions and WASI.

Compare browser and standalone hosts and compilation strategies; explain traps and bounds checks, sandboxing and its limits, resource limits, JS↔Wasm interop, compilation targets, debugging, performance, SIMD, threads and atomics, capability review, portability, and how Wasm compares with JVM bytecode and native code.

Plain-Language Explanation

WebAssembly is a compact bytecode designed to run safely and fast anywhere — in browsers and outside them. A Wasm module is validated before it runs, can only touch its own block of memory, and can only do what its host explicitly gives it (imported functions). That makes it a good format for running code you did not write.

Precise Technical Explanation

Why WebAssembly exists Near-native performance for compiled languages in browsers, safely; a portable compilation target; a sandboxed plugin format outside browsers.

Text format (WAT) versus binary format .wat is a readable S-expression syntax; .wasm is the compact binary that engines load. Tools (wabt's wat2wasm, wasm-tools) convert between them.

Modules, functions, tables, memories, globals, and exports A module declares types, imports, functions, tables (of function references for indirect calls), memories, globals, exports, a start function and data/element segments.

Types and validation Every function's body is type-checked by an abstract stack interpretation before running: each instruction's operand and result types must match, blocks must leave the declared types. Validation is required and linear-time.

Stack machine semantics Instructions pop operands and push results (local.get 0; local.get 1; i32.add), with *structured* control flow (block, loop, if and labelled br) instead of arbitrary jumps — which makes validation and compilation simple.

A contiguous, byte-addressable array (ArrayBuffer in JS), separate from the engine's own memory and from the call stack.

Loads and stores i32.load offset=… align=… reads from linear memory; every access is bounds-checked (explicitly or with guard pages).

Memory pages and growth Memory size is counted in 64 KiB pages; memory.grow adds pages up to an optional maximum.

Imports and exports Imports are the module's only way to affect the outside world: functions, memories, tables and globals provided by the host at instantiation.

Compile (validate + compile code) once; instantiate many times with different imports.

Imported functions implemented by the host (JavaScript functions, WASI calls).

WASI overview and version/interface caveats The WebAssembly System Interface provides capability-based OS-like APIs (files through pre-opened directories, clocks, random). WASI preview 1 is widely implemented; preview 2 (the component model with WIT interfaces) is newer — check which your runtime supports.

Browser host versus standalone runtime Browsers embed Wasm in their JS engines; standalone runtimes (Wasmtime, Wasmer, WasmEdge, WAMR) run it in servers, edge platforms and devices.

Compilation and execution strategies Interpreters (WAMR's, wasm3), baseline compilers (V8 Liftoff), optimising compilers (TurboFan, Cranelift), AOT compilation (Wasmtime's compile).

Traps and bounds checking Out-of-bounds memory access, division by zero, unreachable, invalid indirect calls trap: execution stops and the host receives an error (a JS WebAssembly.RuntimeError).

Sandboxing model and its limits Wasm code cannot access memory outside its linear memory or call anything not imported. Limits: imported functions are as dangerous as you make them; memory-safety bugs *inside* a module (a C buffer overflow) still corrupt that module's own memory; side channels (Spectre) and resource exhaustion need host measures.

Hosts cap memory (maximum pages), execution time (fuel/epoch interruption in Wasmtime), stack depth and table sizes.

JavaScript ↔ WebAssembly interoperability Exported functions are callable from JS; numbers pass directly; strings and objects pass through linear memory (or reference types/GC proposals); glue generators (wasm-bindgen, Emscripten) handle it.

C/C++/Rust compilation targets clang --target=wasm32-wasi, Emscripten, rustc --target wasm32-unknown-unknown/wasm32-wasip1, Go and others.

Debug symbols and source maps DWARF in custom sections and source maps let browser devtools show original sources.

Predictable, often near native for compute; costs at boundaries (JS↔Wasm calls, copying data), bounds checks, and the absence of some native features.

SIMD and threads overview 128-bit SIMD instructions are standard; threads use shared memories and Web Workers (browser) or host threads.

Memory sharing and atomics Shared linear memory with atomic instructions and wait/notify; in browsers requires cross-origin isolation.

Security review of imported capabilities Review every import as an attack surface: least privilege, validate inputs from the module, never pass raw host pointers.

Portability and deployment One .wasm runs on every conforming engine; WASI versions and proposals (GC, exceptions, threads) vary in support.

WebAssembly versus JVM bytecode and native code Like JVM bytecode: portable, validated, stack-based. Unlike it: no built-in object model or GC requirement, structured control flow, linear memory, designed for C/C++/Rust. Unlike native code: sandboxed and portable, compiled by the host.

Illustrative WAT example Stack effects: local.get 0 pushes the first parameter, local.get 1 the second, i32.add pops two i32s and pushes their sum, which is the function's result. A real build needs a WAT assembler or toolchain to produce the binary; the example below writes those exact bytes by hand instead.

(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add
  )
)

Why It Matters in Real Systems

Wasm powers in-browser compute (image/video codecs, CAD, games), plugin systems (Envoy, databases, editors), edge computing and smart contracts — wherever untrusted code must run fast and safely.

Key Terms

Module's byte array, bounds-checked Type-checking a module before running Abort of execution on an error Import / host function Capability provided by the host Capability-based system interface 64 KiB unit of linear memory

Diagram, Execution Trace or State Transition

Text equivalent: the engine validates and compiles a module, instantiates it with host-provided imports (creating its memory, tables and globals), exposes exports to the host, and turns runtime faults into traps reported to the host.

flowchart LR
  B[.wasm bytes] --> V[Validate: types, structured control] --> C[Compile: baseline / optimising] --> I[Instantiate with imports]
  I --> M[Linear memory, tables, globals]
  I --> X[Exports callable by host]
  X -->|out-of-bounds, unreachable| T[Trap → host error]

Syntax or Instruction Format

Binary: \0asm magic, version 1, then sections (id, size, contents): type (1), import (2), function (3), memory (5), export (7), code (10). Instructions are opcodes with LEB128 immediates (0x20 local.get, 0x6a i32.add, 0x28 i32.load, 0x0b end).

Complete Example

Four modules assembled byte by byte and run by Node.js 24.18's WebAssembly engine: the specification's add, a module that fails validation, a memory module that traps and grows, and a host import (the exact error message texts are V8's):

const sec = (id, bytes) => [id, bytes.length, ...bytes];                 // all sections here are < 128 bytes
const name = (s) => [s.length, ...[...s].map((c) => c.charCodeAt(0))];
const module = (...sections) => new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...sections.flat()]);
const I32 = 0x7f;

// 1. (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)
const addBytes = module(
  sec(1, [0x01, 0x60, 0x02, I32, I32, 0x01, I32]),                        // type: (i32, i32) -> i32
  sec(3, [0x01, 0x00]),                                                   // one function of type 0
  sec(7, [0x01, ...name("add"), 0x00, 0x00]),                             // export "add" = function 0
  sec(10, [0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b]),        // body: local.get 0, local.get 1, i32.add, end
);
console.log("add module:", addBytes.length, "bytes:", Buffer.from(addBytes).toString("hex"));
const { add } = new WebAssembly.Instance(new WebAssembly.Module(addBytes)).exports;
console.log("add(20, 22) =", add(20, 22), "| add(2147483647, 1) =", add(2147483647, 1), "(i32 wraps)");

// 2. the same body with only one operand before i32.add
…
add module: 41 bytes: 0061736d0100000001070160027f7f017f030201000707010361646400000a09010700200020016a0b
add(20, 22) = 42 | add(2147483647, 1) = -2147483648 (i32 wraps)
validate(bad): false
CompileError: WebAssembly.Module(): Compiling function #0 failed: not enough arguments on the stack for i32.add (need 2, got 1) @+28
load(0) = 42 | memory: 65536 bytes
RuntimeError: memory access out of bounds
grow(1) returned previous pages: 1 | now 131072 bytes | load(65536) = 0
host function called with 42
TypeError: WebAssembly.Instance(): Import #0 "env": module is not an object or function

Line-by-Line Explanation

Module 1: 41 bytes — magic and version, then type, function, export and code sections; the code body is exactly local.get 0; local.get 1; i32.add; end. i32.add wraps on overflow (two's complement).

Module 2: the body pushes one value then i32.add needs two — validation fails before any code runs (WebAssembly.validate returns false; compiling throws CompileError).

Module 3: the host writes 42 into linear memory and the module reads it; loading at 65536 (one page) is out of bounds and traps with RuntimeError; after grow(1) the same address is valid (fresh memory reads 0).

Module 4: the module can only reach the outside world through env.log; instantiating without providing it fails immediately (V8 reports a TypeError because the import object has no env; a wrong signature would give a LinkError) — capabilities are explicit.

Expected Output

As shown; error message texts are engine-specific.

Step-by-Step Runtime Behavior

add(20, 22): JS-to-Wasm call → operand stack [] → local.get 0 [20] → local.get 1 [20, 22] → i32.add [42] → end returns 42 to JS.

Common Misconceptions

"Wasm is only for browsers." Standalone runtimes run it on servers, edges and devices.

"Sandboxed means the module's code is safe." It cannot escape its memory, but bugs inside it can still corrupt its own data and abuse whatever imports it has.

"Wasm is always faster than JavaScript." Boundary crossings and data copying can dominate.

Common Bugs and Failure Modes

Forgetting to provide imports, mismatched import signatures, reading stale views of memory after grow (old ArrayBuffers detach), out-of-bounds traps from pointer arithmetic bugs, excessive JS↔Wasm calls in tight loops.

Corrected or Safer Patterns

Recreate typed-array views after growth, batch work inside Wasm, validate pointers and lengths passed across the boundary, set memory maximums and execution limits in the host.

Debugging and Inspection Techniques

wasm-tools print/wasm2wat to disassemble, DevTools' Wasm debugging with DWARF, WebAssembly.validate, runtime fuel/epoch logs.

Performance Considerations

Baseline compilers give fast start-up; optimising tiers give peak speed; SIMD and threads help compute; minimise boundary crossings.

Security Considerations

Review imports as capabilities, cap memory and CPU, keep host functions minimal, treat module output as untrusted data, and remember side channels and in-module memory corruption.

Portability and Version Caveats

Core Wasm 1.0/2.0 features are portable; proposals (GC, exceptions, threads, component model) and WASI versions vary by runtime and version.

Real-World Use Cases

Figma, Google Earth and Photoshop on the web, Envoy/Istio filters, database UDFs, edge functions (Cloudflare, Fastly), blockchain VMs, plugin systems in editors and games.

Best Practices

Expose the smallest set of imports, set limits, validate untrusted modules once and cache compiled modules, keep data in linear memory to avoid copying.

Exercises

E1 · Beginner. What does i32.add do with 2147483647 + 1?

Answer: Wraps to −2147483648 (two's-complement 32-bit arithmetic), as the real run shows.

E2 · Intermediate. Why did load(65536) trap before grow but not after?

Answer: One page is 65,536 bytes (valid addresses 0–65,535 for a byte; a 4-byte load at 65,536 is out of bounds); after growing to two pages it is in bounds.

E3 · Advanced challenge. Design the import set for a Wasm image-filter plugin in a desktop app.

Answer: No file, network or clock imports; the host copies pixels into linear memory and calls an exported apply(ptr, len, width, height); optionally a log(ptr, len) import with length limits. Set a memory maximum and an execution time limit; validate the plugin's returned buffer bounds.

Quiz

? What happens when Wasm code reads outside its linear memory?
+ It traps; the host receives a runtime error. :: Every access is bounds-checked.
- It reads host memory. :: The sandbox prevents that.
- It returns 0. :: It traps.
- The page is added automatically. :: Growth is explicit with memory.grow.

? When does WebAssembly validation happen?
+ Before execution, when the module is compiled. :: Invalid modules never run.
- After each instruction. :: That would be a runtime check.
- Only in debug builds. :: Always.
- Never; Wasm is trusted. :: Validation is mandatory.

? Which are true of WebAssembly's sandbox? (select all that apply)
+ A module can only call functions it imports :: Capabilities are explicit.
+ Memory accesses are bounds-checked :: Within linear memory.
- Memory-safety bugs inside the module are impossible :: They can corrupt the module's own memory.
+ The host should still limit memory and CPU :: Resource exhaustion is possible.

Summary and Key Takeaways

WebAssembly is a validated, structured stack bytecode with linear memory and explicit imports, compiled by each host with its own strategy and trapping on faults. Its sandbox is strong at the memory boundary; capabilities and resources remain the host's responsibility.

Further Reading

*WebAssembly Core Specification* (W3C), Structure, Validation and Execution chapters.

Haas et al., "Bringing the Web up to Speed with WebAssembly" (PLDI 2017).

WASI documentation (wasi.dev); Bytecode Alliance, Wasmtime security documentation.