Skip to content

WASM sandbox: threat model

The synapse sandbox commands run an untrusted WebAssembly tool under a capability manifest the operator approved. This page states precisely what is contained, by which mechanism, and what is deliberately out of scope — so an operator granting a capability knows exactly what they are granting.

The honest frame: this is a capability-limited execution sandbox built on wasmtime, not a multi-tenant hostile-code isolation boundary like a microVM. It contains a tool that might be buggy or over-eager; it is not a claim to withstand a determined attacker exploiting a wasmtime CVE. Where the line falls is written below, not implied.

What is denied, and by what mechanism

Each guarantee is enforced by a mechanism, and each has an adversarial test in tests/test_wasm_sandbox_escapes.py that drives a hostile module past it.

Vector Mechanism Failure mode
Arbitrary host syscalls The linker defines only the WASI preview1 set. Any other import (env::system, a raw host function) is undefined and the module cannot instantiate. Structural — the module fails to link (EXIT_ERROR, "unknown import").
Network The functions that would open a socket (sock_connect, sock_open) are not defined at all; sock_recv/sock_send act only on an already-open fd that no import can create, and the WASI config grants no network. Structural — no import opens an outbound connection.
Filesystem outside grants Only the manifest's filesystem grants become WASI preopens; with no grant the tool starts with no fd to any path, and a parent directory is never implicitly reachable. Structural — the tool holds fds to exactly the granted directories.
Write through a read-only grant Write access is opt-in per grant; a read-only grant derives a read-only preopen (DirPerms.READ_ONLY). Structural — no writable fd exists for a read-only grant.
Symlink-swapped preopen / escape outside the workspace Each grant's host path is canonicalised before the preopen: a path that resolves through a symlink, or is not an existing directory, is refused, and the resolved real path is what is preopened and recorded — a link swapped in after the manifest was authored cannot redirect the sandbox. An operator may also pass --workspace-root DIR (repeatable): every preopen must then resolve at or below an approved root, so a genuine but out-of-workspace directory is refused too. Fail-closed — the run is refused (EXIT_ERROR, "sandbox path refused") before the tool executes; synapse sandbox validate --check-paths [--workspace-root DIR] reports the same verdict without running anything.
Unbounded memory The store's memory limit is set to the manifest's memory_bytes; a memory.grow past it is refused (returns −1). Runtime limit — the allocation never happens.
Unbounded compute Fuel metering is on; the tool is charged per instruction against the manifest's fuel. Runtime limit — EXIT_OUT_OF_FUEL.
Wall-clock runaway An epoch-interruption timer trips after wall_clock_ms, interrupting even a tight loop that burns no fuel. Runtime limit — EXIT_EPOCH_DEADLINE.

Attestation

Every run produces a bounded RunReceipt — tool id, content digest, input/output digests (never the bytes), exit token, fuel used, granted capabilities, and any containment reason. synapse sandbox run --attest DB appends it to a durable event store as a sandbox_run event, so an auditor can later prove which tool ran under which grants and how it exited through the same synapse event-query and replay path as any coordination event — without the tool's inputs or outputs ever entering the log.

The gate before execution

A tool runs only when both hold: its module digest matches the approved manifest's content_digest (a swapped binary is refused), and the operator passed --approve (the capability grant is never implicit). synapse sandbox test pre-flights the digest match and the entrypoint export without executing anything.

Out of scope — stated plainly

  • Side channels. Timing, cache, and Spectre-class leakage between the guest and host are not addressed; do not run a tool you must isolate from host secrets at that level.
  • wasmtime host-escape CVEs. The boundary is only as strong as the underlying runtime. Keep the [wasm] extra current; a runtime CVE is a runtime problem this layer cannot paper over.
  • Resource exhaustion of the host process. Fuel and memory bound the guest; they do not bound the host orchestrator's own bookkeeping if you launch thousands of runs concurrently. Rate-limit at the caller.
  • Covert use of granted capabilities. A tool granted a writable directory may write anything it likes there; the grant is the trust boundary. Grant the narrowest scope that lets the tool do its job.

For the operator

Grant the least: the narrowest filesystem scope, the smallest fuel and memory that complete the job, the shortest wall clock. Read the receipt — a run that exited on a limit is telling you the grant was wrong or the tool misbehaved. Attest runs you may need to audit. The sandbox contains an honest tool having a bad day; the manifest is where you decide how much a dishonest one could ever reach.