Skip to content

WASI Deep Dive

WASI does not just expose system calls — it redesigns how a program is granted access to system resources. Instead of the POSIX model where a process inherits ambient authority from its user ID, WASI uses capability-based security: a module can only use a resource if the runtime explicitly passes a capability for it.

In a traditional Unix process, opening /etc/passwd succeeds as long as the process’s UID has read permission. There is no sandboxing at the application boundary.

In WASI the module has no ambient access at all. The runtime acts as a gatekeeper:

  • To read from the filesystem, the host must pass a pre-opened directory file descriptor via --dir.
  • To access an environment variable, the host must allow it via --env.
  • By default, a WASI module has zero filesystem access, zero network access, and zero environment variable access.

This means you can safely run untrusted .wasm binaries — the worst a compromised module can do is limited to exactly what it was handed.

WASI has gone through two major revisions:

Preview 1 (0.1)Preview 2 (0.2)
StatusStable, widely supportedStable since early 2024
Interface styleFlat C-like imports (fd_write, path_open)Component Model WIT interfaces
Target triplewasm32-wasi / wasm32-wasip1wasm32-wasip2
NetworkingNot in core (vendor extensions)wasi:sockets in core
ToolchainRust stable, WASI SDKcargo component, wasm-tools

For most production work today, Preview 1 (wasm32-wasip1) is the safe default. Preview 2 is the future, and tooling is maturing quickly.

Add the target once, then build normally:

Terminal window
# Install the WASI target
rustup target add wasm32-wasip1
# Build a release binary
cargo build --target wasm32-wasip1 --release
# Output: target/wasm32-wasip1/release/my_app.wasm

A minimal Rust main.rs for WASI:

fn main() {
println!("Hello from WASI!");
let args: Vec<String> = std::env::args().collect();
println!("Args: {:?}", args);
}

println! maps to the WASI fd_write import on file descriptor 1 (stdout). std::env::args() uses the WASI args_get / args_sizes_get imports. Both are part of Preview 1.

When wasmtime (or any WASI runtime) executes a .wasm binary it looks for an exported function named _start. This is the WASI equivalent of main().

The Rust compiler generates _start automatically when you compile with a main function and the wasm32-wasip1 target. You can verify this with wasm-objdump:

Terminal window
wasm-objdump -x target/wasm32-wasip1/release/my_app.wasm | grep export
# ...
# - func[N] <_start> -> "_start"
flowchart TD
  RT["wasmtime / wasmer\n(runtime)"]
  subgraph "Capability grants"
    D["--dir /data\n(pre-opened fd)"]
    E["--env KEY=VAL"]
    A["argv via --"]
  end
  RT -->|"pre-opens"| D
  RT -->|"injects"| E
  RT -->|"passes"| A
  D --> M["Wasm Module\n(_start)"]
  E --> M
  A --> M
Capability grants flow into the module at startup
In WASI, how does a module gain access to a directory on the host filesystem?
Which Rust target triple compiles to WASI Preview 1?
What is the WASI entry-point function that runtimes look for?
What is the main security advantage of WASI capability-based security over POSIX ambient authority?