← All posts

Sandboxing AI Agents with Wasm: Are the Security Guarantees of WASI Strong Enough?

Posted on November 11th, 2026 by Burak Yalçınkaya
Last updated on November 11th, 2026
Posted in News

Sandboxing AI Agents with Wasm: Are the Security Guarantees of WASI Strong Enough?

Excerpt: AI agents need reliable security boundaries, but how much can we trust Wasm to provide them? We put WASI filesystem operations to the test with AI-assisted differential fuzzing and uncovered inconsistencies across Wasm engines in enforcing file permissions, exposing gaps in the security guarantees developers rely on.

WebAssembly is increasingly being used as a lightweight sandbox for running untrusted code, including code generated or used by AI agents. A Wasm module runs within a constrained execution environment: it cannot directly issue host system calls or access arbitrary host memory, and its interaction with the outside world is mediated by the interfaces the host exposes. WASI (the WebAssembly System Interface) standardizes many of these interfaces, providing controlled access to resources such as the filesystem, network, and clocks.

An agent you do not fully trust is exactly the kind of thing you want to box in: let it read the files it needs, write only where it should, and reach nothing else. But a sandbox is only as strong as the primitives under it. If "this file is read-only" does not actually hold, every policy you build on top of it is quietly wrong. So the guarantees WASI actually enforces are worth testing rather than assuming.

We tested WASI's filesystem interface the way we have been testing a lot of things lately: with an AI coding agent driving the work.

The setup

We asked Claude Code to build a differential fuzzer for WASI and run it against two independent engines: Wasmtime, a widely used runtime from the Bytecode Alliance, and WasmEdge, an independent runtime hosted by the CNCF and written in C++. We've used the same technique to test Wasm runtimes before. An earlier campaign uncovered a null-reference bug in WAMR (WebAssembly Micro Runtime). If you want to learn more about the process, read our blog post.

Differential fuzzing suits an AI-driven project especially well for one reason: it does not require trusting the model's understanding of the WASI standard. When two independent implementations disagree on the same input, the divergence gives us a specific case to investigate. It does not necessarily tell us which implementation is wrong, or whether the specification allows both behaviors. Claude's job is to build a harness that explores as many scenarios as it can, run each one through both engines, and report divergences. Determining what those divergences mean comes afterward, and that is where we step in.

Differential fuzzing harness: random sequences of WASI filesystem calls replayed identically against Wasmtime and WasmEdge, comparing results after every call

The harness works at the level of individual WASI calls. Claude wrote a generator that emits random sequences of filesystem operations with fuzzed arguments: opening files with different flags and permissions, writing bytes, seeking, renaming files, and creating directories. Each sequence is fed to a small WebAssembly carrier module that calls the WASI host functions from the guest (Wasm) side, and a separate host-side adapter wires that module up to each engine. The harness preopens a matching sandboxed directory tree for each engine, replays each generated sequence identically against both, and after every call compares three things: the returned error code, any bytes the call wrote back into memory, and the state of the directory tree on disk.

The generator aims at the cases a sandbox is meant to reject, not just the ones that succeed: handles opened with narrowed rights and then written to, and paths built to escape the preopened directory (.. sequences, absolute paths, symlinks pointing outside). Path escape was the campaign's highest-priority target, since one succeeding on either engine would be an outright sandbox escape. Neither engine exhibited a path escape in our tests. The divergence appeared in cases involving narrowed file permissions.

The finding

The fuzzer found a case where the two engines disagree. The test case involves a preopened directory with read and write access. The guest Wasm program opens a file in that directory with the "create" flag but requests read-only access, then attempts to write to it. On WasmEdge, the returned file descriptor is read-only, so the write operation fails. On Wasmtime, however, requesting file creation also results in a writable handle, even when the guest requests read-only access. The guest can therefore write to the file despite its requested access mode. In the most striking case, requesting no access flags at all still produces a handle with read and write access.

Both engines blocked path traversal and symlink escapes, and we did not find a way to escape the preopened directory. The issue is narrower: when a directory is writable, the requested access mode of an individual file may not be enforced as expected. It is about controlling access to individual files inside a writable directory, the finer-grained control that per-handle rights seem to promise.

The WASI spec is not precise enough about how these flags are supposed to work when used together. Because the requested access mode is read-only, one would expect the write attempt to fail. On the other hand, one could argue that the "create" flag also implies write access, which is effectively Wasmtime's behavior.

The behavior is an open question even for Wasmtime's own maintainers. One of them opened an issue on the WASI spec asking how these flags should interact, noting there is no obviously correct answer. Until the spec settles this, you cannot assume a file is read-only inside a writable directory, at least not on every engine.

WASI is designed to provide a portable interface. But when implementations disagree on security-relevant behavior, a program's security can depend on which engine happens to run it. A careful developer aware of this can defend against it, but that is the wrong place for the responsibility to sit. A primitive that only holds when every guest author knows its quirks and codes around them is not much of a primitive.

How this becomes a privilege escalation

The Wasm ecosystem is moving toward composition: independently developed modules and components that can be published, reused, and connected across trust boundaries. The component model exists to let independently authored components interoperate. In that kind of world, fine-grained control over capabilities matters more and more, because you are constantly handing pieces of your own access to code you did not write.

Suppose a sandboxing environment runs untrusted Wasm components, such as AI agents or their plugins, alongside a trusted component that manages file access for them. The trusted component opens a file using the create flag while requesting read-only access, then passes the resulting handle to an untrusted component. It relies on the runtime to enforce the requested access mode.

On Wasmtime, the engine does not enforce it. The handle is writable, so the untrusted component, which has no filesystem access of its own, can overwrite a file it was only supposed to read. It has gained a capability it was never granted and could not have obtained on its own: a privilege escalation across the boundary between the two components.

Where this leaves us

So, is WASI safe enough to sandbox AI agents? In our tests, the outer boundary held: we found no path that escaped the sandbox. But the finer-grained guarantees you would actually build a security policy on are another matter. An automated fuzzer found this gap by probing exactly the cases a sandbox is meant to reject. An AI agent running untrusted code could probe the same edge cases, discover this gap, and exploit it to access capabilities it was never meant to have. If your security model relies on fine-grained capabilities, verify the exact engine and version you ship.

But the deeper problem goes beyond one bug. A single bug can be fixed, but an underspecified primitive cannot be reliably implemented in the first place. Two engines can behave differently without either clearly violating the specification, leaving developers unable to reason confidently about their sandbox across implementations. The problem is not necessarily that Wasmtime chose the wrong behavior. It is that the specification does not define what the right behavior is. Security policies are only as strong as the guarantees provided by the primitives they rely on. If we want to build trustworthy sandboxes, those primitives need to be defined with mathematical precision.

Fuzzing can expose disagreements between implementations, but it cannot establish which implementation is correct when the specification provides no ground truth. A formal specification would give us a precise account of what they should do, and formal verification could establish whether each implementation conforms to it. LLMs are making formal verification more practical by improving the process of constructing proofs, while machine-checked proofs provide the assurance that the results are correct. We should use that progress to tackle the problem at its source: formally specify the guarantees a sandbox promises, then verify that the engines implementing them actually deliver those guarantees.