Sandboxing and limits
A wazy guest starts with nothing of its own: no files, no network, no stdio. Every capability
below is something you hand it. The two ABIs are configured through different surfaces — WASI 0.1
through ModuleConfig methods on a core module, WASI 0.2 through the wasip2.Config struct you
pass to wasip2.With — and the defaults are not the same, so every row names its ABI.
| Capability | ABI | Default | Granted by |
|---|---|---|---|
| Filesystem | 0.1 | nothing visible | WithFSConfig mounts |
| Filesystem | 0.2 | nothing visible | Config.FS mounts |
| stdout, stderr, stdin | 0.1 | discarded / empty | WithStdout, WithStderr, WithStdin |
| stdout, stderr, stdin | 0.2 | discarded / empty | Config.Stdout, Config.Stderr, Config.Stdin |
| Environment | 0.1 | empty | WithEnv |
| Environment | 0.2 | empty | Config.Env |
| Arguments | 0.1 | empty, no argv[0] | WithArgs |
| Arguments | 0.2 | argv[0] only | Config.Args appends after it |
| Wall clock | 0.1 | a fake clock, frozen | WithSysWalltime |
| Wall clock | 0.2 | the real time.Now |
already on; Config.WallClock replaces it |
| Randomness | 0.1 | deterministic | WithRandSource |
| Randomness | 0.2 | real crypto/rand |
already on; not configurable |
| TCP / UDP | 0.2 | unregistered | Config.AllowTCP / Config.AllowUDP |
| Outgoing HTTP | 0.2 | unregistered | Config.EnableHTTP |
Cyan marks a capability that is already on before you ask; grey marks one that is not there until you hand it over.
Arguments are the row that differs most between the two. WASI 0.1 defaults to no arguments at all,
argv[0] included — WithArgs sets the whole vector, and the runtime synthesizes nothing. WASI 0.2
always prepends a synthetic argv[0] of "wazy", because wasi:cli/environment.get-arguments
returns the full argv and guests following the Unix convention skip the first element;
Config.Args holds only what comes after it.
The frozen clock is the WASI 0.1 default, and it is the one that surprises people. It is deliberate: a guest that needs the real time has to be given it, and a guest that reads the clock for fingerprinting gets nothing useful.
The component path does not inherit that default, or the deterministic random source. wasip2.With
registers wasi:clocks and wasi:random unconditionally, backed by time.Now and crypto/rand:
WallClock is the only clock you can replace, and wasip2.Config has no randomness field at all.
The way to deny a component those interfaces is not to register WASI on it — every WASI interface a
component imports that you did not register is an unregistered trap stub that fails loud, naming
the interface and function, the first time the guest calls it.
Filesystem
Section titled “Filesystem”Mounts are explicit, and read-only is a first-class option:
fs := wazy.NewFSConfig(). WithReadOnlyDirMount("./assets", "/assets"). // guest sees /assets, cannot write WithDirMount("./scratch", "/tmp"). // read-write WithFSMount(embeddedFS, "/lib") // an fs.FS, e.g. go:embedWithFSMount takes any fs.FS, so an embed.FS works and nothing touches the real filesystem.
WithSysFSMount is the escape hatch for a custom sys.FS implementation.
There is no .. escape. Both the core and the component path resolve a guest path with the same
path.Clean + fs.ValidPath rule and reject anything that leaves the descriptor it is resolved
against — ../../.. and rooted paths alike — before the path reaches the filesystem. There is no
way to turn that off: to give a guest a second directory, add a second mount.
Memory
Section titled “Memory”r := wazy.NewRuntimeWithConfig(ctx, wazy.NewRuntimeConfig(). WithMemoryLimitPages(256)) // 256 * 64 KiB = 16 MiB, hard capA module whose declared minimum exceeds the limit is rejected at compile time. A declared
maximum over it is clamped to the limit instead, so the module still loads and simply cannot grow
as far as it asked; a memory.grow past the limit fails at run time, returning -1, which is a value
the guest must handle.
This bounds a memory declared with an i64 index type as well — otherwise a module could reach past
your only general memory ceiling by doing nothing more than declaring i64, which is what
clang --target=wasm64 and Rust’s memory64 target emit. WithMemory64LimitPages sets that ceiling
independently, above or below this one, and wins whichever order the two are configured in; it is
the only way to allow a 64-bit memory past four gibibytes, since WithMemoryLimitPages itself tops
out at 65536 pages.
WithMemoryCapacityReservePages trades address space for speed: reserving capacity up front makes
in-capacity memory.grow a 109 ns operation instead of a reallocation. Out-of-capacity, shared and
imported memories keep the safe path.
Wall-clock limits
Section titled “Wall-clock limits”A context deadline kills even a tight compiled loop, if you ask for it:
r := wazy.NewRuntimeWithConfig(ctx, wazy.NewRuntimeConfig(). WithCloseOnContextDone(true))
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)defer cancel()
_, err := fn.Call(ctx) // returns once the deadline passes, even mid-loopThe compiler decrements a reserved register and takes a predicted-not-taken branch at every function entry and every loop back-edge, rather than calling back into Go. When the counter runs out, one trip through Go does the authoritative check and yields to the scheduler. Real compute pays about +18% with the option on, where wazero pays +1057%.
The worst case is a near-empty compute kernel (a bare spin loop), which has no body to dilute
the per-back-edge decrement against and pays +44%. A host-call-dense loop, which used to
be the expensive shape, now pays +2%. There is no knob; if your workload is a tight
compute kernel and you do not need Kill() to interrupt a stuck guest, leave the option off.
What wazy does not do
Section titled “What wazy does not do”- No fuel metering. There is no instruction budget. Wall time is the lever; if you need kernel-enforced CPU quotas per tenant, you want a container or a microVM.
- No
wasi-threadsspawn. Shared memory and atomics are available viaexperimental.CoreFeaturesThreads, but a guest cannot spawn OS threads. - No side-channel hardening. Sandboxing here is memory-safety and capability isolation, not protection against a co-resident guest measuring timing.
Trapping and errors
Section titled “Trapping and errors”A guest trap — out-of-bounds access, an unreachable, a failed memory.grow the guest did not
handle — surfaces as an error from Call, with the guest stack in the message. It does not take
your process down. A host function that panics becomes a trap the same way.
Closing the runtime closes everything it created:
r := wazy.NewRuntime(ctx)defer r.Close(ctx)