Sandboxing third-party code with WebAssembly linear memory and capability imports
Running untrusted third-party code close to users needs isolation without process overhead. WebAssembly linear memory plus capability-based imports gives a bounded sandbox with controlled host access.
14 Apr 2026, 00:00 UTC

Running untrusted or third-party code close to users requires isolation without paying for a full OS process per tenant. WebAssembly gives a portable sandbox with predictable performance, but the isolation only works if you treat linear memory as a hard boundary and imports as capabilities. That is the practical engineering decision: compile to portable bytecode, instantiate with an explicit import surface, and let the module operate only inside its own linear memory.
The problem: plugins at the edge need isolation, not processes
Edge and browser-adjacent workloads often need to execute code supplied by partners: image transforms, parsers, scoring functions. Spinning a process per request is expensive. Running in the same process is unsafe. WebAssembly modules are sandboxed by design, but the host still decides what is reachable.
Linear memory is a contiguous, bounds-checked byte array owned by the module. The module cannot access host memory directly. Imports and exports are the only crossing points. Using that model as a capability system keeps the trust surface small.
How isolation is enforced
Modules are portable bytecode with explicit interfaces
A WebAssembly module is compiled from WAT or a language toolchain to .wasm bytecode. Instantiation requires all imports to be satisfied. If an import is missing, instantiation fails. Exports are the only functions, memories, and globals the host can call.
Check a module before deployment with a standard tool. Run in a terminal where wabt is installed:
wasm-objdump -x module.wasmRequired permissions: read access to the file. Expected check: the output lists Import and Export sections. Risk: a module with unexpected imports will fail to instantiate unless the host provides them.
Linear memory provides a bounded address space
Memory in WebAssembly is a fixed number of 64KiB pages. Access outside the current size traps. Growth is explicit via memory.grow and may be denied by the host. Bulk memory operations are proposal-dependent and may not be enabled by default in all runtimes.
Because the module cannot dereference host pointers, all data must be copied into and out of linear memory. That copy cost is the price of isolation.
Host bindings are capabilities, not services
An import like env.log or env.random is a capability you choose to grant. The module cannot discover other host APIs. A minimal import surface means fewer ways to escape the sandbox.
Version assumption: core WebAssembly with bulk memory and reference types is widely supported in modern browsers and Node.js, but exact proposal support varies. Check the runtime documentation for enabled proposals before relying on them.
Worked example: buffer processing with a minimal boundary
Consider a module that receives a byte buffer, processes it entirely inside its own memory, and returns a result via an exported function. The host allocates memory, copies input in, calls the export, then reads back.
Illustrative steps, not a tested implementation:
- Author module source in WAT with one memory and one exported function, e.g., process. No imports except an optional log capability.
- Compile to bytecode:
wat2wasm --enable-bulk-memory module.wat -o module.wasmRun where wabt is installed. Required permissions: write to output directory. Expected check: module.wasm exists and is non-empty. Risk: compilation flags change available instructions.
Practical verification: create a minimal module with one import and one export, instantiate it, and confirm that removing the import causes instantiation to fail. Inspect memory size before and after a call to observe growth behavior in your runtime.
Trade-off and limitation
Manual memory management and copying dominate cost. Host-module data transfer requires explicit copies. There is no native file system, threads, or OS sockets unless the host provides them via imports, which re-expands the trust surface.
Memory growth is unbounded by default unless the host caps it. Garbage collection proposals change how data moves between host and module and are version-sensitive.
Actionable closing
Start with one isolated computation and define the smallest import surface needed. Measure host-module data transfer size and frequency before scaling. Iterate on the boundary: move work inside the module to reduce copies, and remove imports that are not essential. Verify proposal support in each target runtime and test instantiation failure modes explicitly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.