| name | wasmtime-wasi-sandbox |
| description | Wasmtime WASI sandbox for agent-executed code — capability-based security, component model, WASI Preview 2. Isolate untrusted code from the host filesystem/network. Sources: bytecodealliance/wasmtime (Apache-2.0). |
/wasmtime-wasi-sandbox
When to Use
- Running untrusted or agent-generated code with capability-based isolation
- Sandboxing plugins/extensions that should not access the host filesystem or network
- Embedding a portable execution environment in a Rust or Node.js host
- Multi-tenant code execution where each tenant gets a separate WASM instance
Do NOT use for
- Trusted internal scripts (no isolation benefit)
- GPU-intensive workloads (WASM has no GPU access)
- Long-running persistent processes (WASM instances are typically stateless per call)
Capability-based security model
WASI is capability-based: a WASM module gets ONLY the capabilities explicitly granted.
Default: no filesystem, no network, no env vars, no clock access.
Grant per instantiation:
preopened directories → specific filesystem paths only
inherit_stdin/stdout → explicit opt-in
env vars → only named vars passed through
network → not in WASI Preview 1; restricted in Preview 2
Agent use case: give untrusted code a /tmp/sandbox dir only.
Never grant access to /home, ~/.ssh, /etc, or source code dirs.
Node.js host (wasmtime-nodejs)
import { Wasmtime } from 'wasmtime';
async function runSandboxed(wasmBytes: Buffer, userInput: string): Promise<string> {
const engine = new Wasmtime.Engine();
const module = await Wasmtime.Module.fromBuffer(engine, wasmBytes);
const linker = new Wasmtime.Linker(engine);
const wasi = new Wasmtime.Wasi({
args: ["program", userInput],
env: {},
preopenedDirs: {
"/sandbox": "/tmp/wasm-sandbox",
},
inheritStdin: false,
inheritStdout: true,
inheritStderr: true,
});
wasi.addToLinker(linker);
const store = new Wasmtime.Store(engine);
const instance = await linker.instantiate(store, module);
const run = instance.getFunc(store, "_start");
run.call(store);
return wasi.getStdout();
}
Rust host embedding (wasmtime crate)
use wasmtime::*;
use wasmtime_wasi::{WasiCtxBuilder, WasiView};
fn run_sandboxed(wasm_bytes: &[u8], input: &str) -> anyhow::Result<String> {
let engine = Engine::default();
let module = Module::from_binary(&engine, wasm_bytes)?;
let wasi = WasiCtxBuilder::new()
.inherit_stdout()
.inherit_stderr()
.preopened_dir(
wasmtime_wasi::Dir::open_ambient_dir("/tmp/sandbox", ambient_authority())?,
"/sandbox",
)?
.build();
let mut store = Store::new(&engine, wasi);
let mut linker: Linker<wasmtime_wasi::WasiCtx> = Linker::new(&engine);
wasmtime_wasi::add_to_linker_sync(&mut linker, |cx| cx)?;
let instance = linker.instantiate(&mut store, &module)?;
let start = instance.get_typed_func::<(), ()>(&mut store, )?;
start.(& store, ())?;
(.())
}
Resource limits (CPU + memory)
let mut config = Config::new();
config
.consume_fuel(true)
.max_wasm_stack(512 * 1024);
let engine = Engine::new(&config)?;
let mut store = Store::new(&engine, wasi);
store.set_fuel(10_000_000)?;
let mut limits = StoreLimitsBuilder::new()
.memory_size(16 * 64 * 1024)
.build();
store.limiter(|state| &mut limits);
Component Model (WASI Preview 2)
cargo component build --release
Anti-Fake-Pass Checklist
❌ Granting preopened "/" (root) directory → defeats the entire sandbox
❌ No fuel/resource limits → untrusted code can spin CPU infinitely
❌ Sharing WASM store across untrusted tenants → stores share mutable state; one per tenant
❌ No memory limit → malicious WASM allocates until host OOM
❌ Using WASM for GPU workloads → WASM has no GPU; use native or CUDA
❌ Assuming WASI Preview 1 has network isolation by default → it does, but Preview 2 sockets must also be explicitly restricted