WASI Preview 2 and the Component Model: What Capability-Based Isolation Actually Prevents
The Problem
WASI Preview 1 (2019) gave WebAssembly modules a POSIX-like interface to the outside world. A module could call path_open, fd_read, fd_write, poll_oneoff, and a handful of other POSIX-derived functions through an fd table the host populated at instantiation. The key claim was capability-based: instead of ambient filesystem access, the host gave the module pre-opened directory handles, and the module could only operate within those subtrees. No pre-opened handle for /etc meant no reads from /etc.
This was genuine progress over unrestricted POSIX access, but it had structural limitations that matter for security. The capability was coarse-grained — a pre-opened directory handle allowed the module to do everything that handle permitted: read, write, create, delete, traverse, stat, watch. There was no way to grant read-only access to a directory subtree without also granting write access through the same handle. The capability was also temporally ambient — once granted at instantiation, it remained in scope for the entire lifetime of the module execution. There was no mechanism to revoke it, narrow it mid-execution, or limit it to specific call sites. And critically, the interface was monolithic: there was one WASI namespace, and a module either imported functions from it or it didn’t. You could not compose two modules in a way that gave each component exactly the capabilities it needed without sharing the whole namespace.
WASI Preview 2 (stabilized in Wasmtime 18.0, released February 2024) introduces the Component Model — a type-safe, composable binary format for WASM modules. Components are not modules in the WASI P1 sense. A component is a WASM binary that explicitly declares its imports and exports using WIT (WebAssembly Interface Types), a typed interface definition language. Every capability a component uses — filesystem access, outbound HTTP, TCP sockets, environment variables, random number generation, wall-clock time — must be expressed as a named WIT import. If the WIT import is not declared, the linker will not provide the function, and the compiled binary has no call site for it. There is no ambient access to anything.
The security implications of this design are deep but also bounded. Understanding both sides — what the interface-level capability model genuinely enforces, and where it provides no protection at all — is the difference between deploying WASI P2 correctly and deploying it with a false sense of isolation.
WASI P1 vs. P2 Interface Model
In WASI P1, a module’s interface with the host was a flat table of function imports, all in the wasi_snapshot_preview1 namespace:
(module
(import "wasi_snapshot_preview1" "fd_read" (func $fd_read (param i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "path_open" (func $path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "sock_recv" (func $sock_recv (param i32 i32 i32 i32 i32) (result i32)))
)
The host WASM runtime provided implementations of those functions. Whether the module actually should have network access was enforced by the host embedding — Wasmtime would refuse to link sock_recv if the embedder didn’t provide it, but this enforcement was entirely runtime-side. The module binary itself expressed no preference about what capabilities it needed versus what it was prepared to receive.
In WASI P2, a component’s imports and exports are expressed in WIT. The WIT compiler (wasm-tools component) embeds the typed interface directly into the component binary as a custom section. When the runtime instantiates the component, it must satisfy every declared import exactly — the right type signature, the right version. If the host doesn’t provide an import, instantiation fails. If the host provides an import the component didn’t declare, the component cannot call it. The component binary is its own specification.
WIT Interfaces and What They Control
A WIT interface declaration looks like this:
// wasi:filesystem/types@0.2.0
interface types {
type descriptor = resource;
descriptor-stat: func(self: borrow<descriptor>) -> result<descriptor-stat, error-code>;
read-via-stream: func(self: borrow<descriptor>, offset: filesize) -> result<tuple<input-stream, bool>, error-code>;
write-via-stream: func(self: borrow<descriptor>, offset: filesize) -> result<output-stream, error-code>;
open-at: func(
self: borrow<descriptor>,
path-flags: path-flags,
path: string,
open-flags: open-flags,
flags: descriptor-flags,
) -> result<descriptor, error-code>;
// ... 20+ more methods
}
Each import in a component world corresponds to one of these WIT interfaces. The complete set of standard WASI P2 interfaces includes:
wasi:filesystem/types@0.2.0andwasi:filesystem/preopens@0.2.0— filesystem operations and pre-opened directory handleswasi:sockets/tcp@0.2.0,wasi:sockets/udp@0.2.0,wasi:sockets/ip-name-lookup@0.2.0— TCP and UDP socket operations and DNSwasi:http/outgoing-handler@0.2.0— outbound HTTP requestswasi:http/incoming-handler@0.2.0— the export interface for HTTP request handlerswasi:cli/environment@0.2.0— reading environment variableswasi:cli/stdin@0.2.0,wasi:cli/stdout@0.2.0,wasi:cli/stderr@0.2.0— standard I/O streamswasi:random/random@0.2.0— cryptographically secure random byteswasi:clocks/wall-clock@0.2.0,wasi:clocks/monotonic-clock@0.2.0— time access
A component that doesn’t import wasi:sockets/tcp has no TCP socket functions in its binary. Not “the runtime will refuse the call” — the functions do not exist in the component’s code. There is no call instruction to intercept.
Concrete Example: HTTP Handler Without Filesystem Access
Consider an HTTP request handler implemented in Rust using the wasi crate’s component bindings. The WIT world declaration:
// http-handler.wit
package myorg:http-handler@1.0.0;
world http-handler {
// Only import what we actually need for time and random
import wasi:clocks/monotonic-clock@0.2.0;
import wasi:random/random@0.2.0;
// Export the HTTP handler interface — this is how the runtime calls us
export wasi:http/incoming-handler@0.2.0;
// Notably absent:
// - wasi:filesystem/types — no filesystem access
// - wasi:filesystem/preopens — no pre-opened directories
// - wasi:sockets/tcp — no outbound TCP
// - wasi:http/outgoing-handler — no outbound HTTP requests
// - wasi:cli/environment — no environment variable reads
}
The Rust component implementation:
// src/lib.rs
use wasi::http::types::{
IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam,
};
wasi::http::proxy::export!(HttpHandler);
struct HttpHandler;
impl wasi::exports::http::incoming_handler::Guest for HttpHandler {
fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
let method = request.method();
let path = request.path_with_query().unwrap_or_default();
let response = OutgoingResponse::new(wasi::http::types::Fields::new());
response.set_status_code(200).unwrap();
let body = OutgoingBody::new(response.body().unwrap(), None);
let stream = body.write().unwrap();
stream.write_all(
format!("method={method:?} path={path}").as_bytes()
).unwrap();
ResponseOutparam::set(response_out, Ok(response));
}
}
Now try to add a filesystem read to this component:
// This does NOT compile when targeting wasm32-wasip2 with this WIT world
fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
// Attempt to read a file
let config = std::fs::read_to_string("/etc/config.toml").unwrap();
// ^ compile error: wasi:filesystem/types not in the world
// The Rust standard library's fs module calls wasi:filesystem under the hood.
// If the world doesn't import wasi:filesystem, the linker fails.
}
When you build this component against the http-handler.wit world using cargo component build, the Rust toolchain generates bindings only for the interfaces declared in the world. std::fs::read_to_string compiles to WASM instructions that call the WASI filesystem import. Because wasi:filesystem/types is not in the world, the WIT linker (wasm-tools component link) cannot satisfy that import and refuses to produce a valid component binary. The error occurs at link time, not at runtime:
error: component validation error: import `wasi:filesystem/types` is required
by the module but is not provided by the world `http-handler`
This is not a runtime sandbox check. The component binary that would be produced is simply invalid — it references an interface that its declared world does not provide. The enforcement happens at the type system level, before any code runs.
When you attempt to instantiate a component whose imports are not satisfied by the host, Wasmtime reports the failure at instantiation time, not during execution:
Error: component imports instance `wasi:filesystem/types@0.2.0`, but a
matching implementation was not found in the linker
There is no runtime path where a component without a filesystem import can access the filesystem. The capability is absent at the interface level.
Runtimes That Implement the Component Model
Three production runtimes implement WASI P2 components:
Wasmtime (Bytecode Alliance, Rust): The reference implementation. wasmtime run --component executes components; wasmtime serve --component serves HTTP components. The wasmtime::component::Linker API in embedding code controls exactly which WASI interfaces are linked into each component instance.
Fermyon Spin: A serverless framework purpose-built on WASI P2. Spin’s spin.toml manifest declares per-component capability grants. The framework’s component loader enforces those grants against the component’s WIT imports at deployment time, not just at runtime instantiation.
jco: A JavaScript/Node.js implementation of the Component Model. Used primarily for running WASI P2 components in server-side JavaScript environments and for transpiling WASM components to ES modules. jco run component.wasm and jco serve implement the same component model semantics as Wasmtime.
What WASI P2 Actually Prevents
The capability model enforces these properties:
Filesystem access without a filesystem capability: A component that does not import wasi:filesystem/types and wasi:filesystem/preopens has no mechanism to open files, read directories, or access path metadata. This is enforced at the interface level — there is no filesystem call site in the component binary.
Outbound network access without socket or HTTP capabilities: A component that does not import wasi:sockets/tcp, wasi:sockets/udp, or wasi:http/outgoing-handler cannot initiate network connections. No ambient socket access exists. This is a significant departure from POSIX, where any process can call connect() unless explicitly blocked.
Environment variable access without explicit grant: wasi:cli/environment must be imported. A component without it cannot read environment variables, which are a common source of credential leakage in container environments.
Capability scope at instantiation: When a filesystem capability is granted, it is scoped to specific pre-opened directories at component instantiation. A malicious component with wasi:filesystem granted for /tmp/workspace cannot access /etc, /home, or /var — not because of OS-level path permission checks, but because the WASI filesystem implementation the host provides only exposes the pre-opened handles it was configured with.
What WASI P2 Does NOT Prevent
JIT miscompilation bugs (CVE-2023-26114 class): The component model’s capability restrictions apply to the WASI interface layer. A JIT miscompilation bug in Cranelift or another WASM backend operates below the WASI layer — it generates incorrect native machine code from valid WASM instructions. When a Cranelift bug causes a bounds check to validate the wrong address, the resulting out-of-bounds memory access occurs in native code, after the WASI capability check has already been satisfied. No WIT interface declaration prevents this. A component with no capabilities at all can trigger a JIT miscompilation that reads Wasmtime’s own heap memory. The WASI P2 capability model provides zero protection against this class of vulnerability.
Host kernel bugs reachable through runtime syscalls: Wasmtime is a userspace process. It makes host syscalls — mmap, mprotect, read, write, futex, clone, epoll_wait — to do its work. Those syscalls reach the host kernel. If the kernel has a bug reachable through those call paths (CVE-2022-0847 Dirty Pipe via splice+write, any recent nf_tables or io_uring vulnerability), the Wasmtime process is as exposed as any other userspace process. A component running inside that Wasmtime process is completely compromised when the Wasmtime process is compromised.
Side-channel attacks: Timing side channels, cache side channels (Spectre), and microarchitectural attacks do not go through the WASI interface. A component that performs constant-time RSA-based computations and measures execution time leaks information regardless of its WIT world declaration. WASI P2 provides no Spectre mitigations beyond what the runtime and hardware implement independently.
Supply chain attacks on the component itself: WASI P2 verifies that a component only uses the capabilities it declared — it does not verify what the component does with those capabilities. A malicious component that legitimately imports wasi:http/outgoing-handler with access to https://api.external.com can exfiltrate arbitrary data to that endpoint. The capability model restricts which interfaces exist; it says nothing about the semantics of how those interfaces are used.
Component composition attacks: When a component composes with another component (imports from it via WIT), the outer component can pass arbitrary data to the inner component through typed function calls. A malicious inner component can misuse its own granted capabilities in response to inputs from the outer component. Composing a trusted outer component with an untrusted inner component is not safe unless the inner component’s WIT interface is fully validated for semantics, not just type correctness.
The Residual Shared Kernel Problem
WASI P2 components run inside a Wasmtime process, which runs on a Linux kernel. The isolation boundary that WASI P2 creates is between the component and the WASI API — it prevents the component from calling filesystem functions it wasn’t granted. But it does not create a new OS-level isolation boundary. The Wasmtime process has the same kernel exposure as any other process on the same host.
Consider the Dirty Pipe scenario. CVE-2022-0847 allows unprivileged overwrite of read-only page-cache pages through the pipe splicing mechanism. An attacker who has shell access to any container on the same Kubernetes node can overwrite bytes in the Wasmtime binary itself — not by escaping the WASM sandbox, but by using splice and write against the host kernel’s page cache. The next time Wasmtime executes those modified bytes, the attacker’s code runs with all the capabilities of the Wasmtime process, including direct read-write access to every component’s linear memory.
The WASI capability model is completely bypassed because the attack path never touches the WASI interface. WASI P2 prevents a WASM component from calling open("/etc/passwd") — it does not prevent a kernel vulnerability from giving an attacker write access to the Wasmtime binary on the same node.
This means WASI P2’s capability model must be understood as interface-level enforcement only. For threats that originate at or below the runtime process level, kernel-level and hypervisor-level controls are still required.
Threat Model
Component without filesystem capability accessing files: The component binary does not contain a call to any WASI filesystem function. Wasmtime’s linker verifies this at instantiation. No OS-level policy enforcement is involved — the interface simply does not exist in the component. This is enforced unconditionally and correctly.
Component with scoped filesystem capability attempting path traversal: A component with wasi:filesystem pre-opened to /tmp/workspace cannot access /etc/shadow or ../../etc/passwd through the WASI interface. The WASI filesystem implementation translates all relative paths against the pre-opened handle. The OS filesystem is never consulted for paths outside the scoped subtree — the traversal fails at the WASI layer before any kernel call is made.
JIT miscompilation (CVE-2023-26114 class): A crafted WASM binary triggers a Cranelift code generation bug that produces machine code performing an out-of-bounds memory access. This access occurs at the native code level, after WASM bytecode validation and after WASI capability checking. The WASM instruction set is valid; the bounds check is present in the bytecode; Cranelift’s JIT generates an instruction that evaluates the check against the wrong address. The capability model is irrelevant. The component does not need any granted capability to trigger this — the miscompilation applies to any memory operation, regardless of the component’s WIT world.
Host kernel attack via Wasmtime process syscalls: The Wasmtime process on a kernel with an exploitable bug in mmap, mprotect, io_uring, or pipe handling is exploitable by anything that can trigger those syscalls in the Wasmtime process — which includes the runtime’s own internal operations, not just explicit WASI calls. An attacker who achieves remote code execution in any co-located container and exploits a kernel vulnerability gains control of the Wasmtime process. From there they have full read-write access to every component’s linear memory, regardless of that component’s WASI capability configuration.
Supply chain via wasi:http exfiltration: A malicious WASM component is distributed through a package registry. It declares only wasi:http/outgoing-handler with an allowed endpoint of https://api.legitimate-service.com. The operator, seeing a minimal WIT world with no filesystem or socket access, treats the component as low-risk. At runtime, the component reads data from its input (passed through legitimate HTTP requests handled by the wrapping application), encodes it as query parameters, and sends outbound requests to the allowed endpoint. The allowed endpoint is controlled by the attacker. The capability model permitted this because the capability was legitimately granted — the failure was in the scope of the grant, not the enforcement of it.
Malicious composed sub-component: An application is built by composing an outer component (trusted, reviewed) with an inner component (a third-party library implementing a protocol parser). The inner component’s WIT interface accepts a parse(bytes) function call that returns a parsed result. A malicious inner component that has also been granted wasi:filesystem by the outer composition misuses the parse() call to write the bytes it receives to a file, exfiltrating data received by the outer component. The outer component’s WIT world is clean; the vulnerability is in what the inner component does with its own legitimately granted capabilities when invoked with data from the outer component.
Hardening Configuration
1. Minimal Capability Grant with Wasmtime CLI
# Run a component with NO filesystem, socket, or environment access
# The component only gets wasi:clocks and wasi:random by default
wasmtime run \
--component \
my-handler.wasm
# Run with filesystem access restricted to a single read-only directory
# The path before :: is the host path; after :: is the guest path
wasmtime run \
--component \
--dir /data::/data:readonly \
my-handler.wasm
# Run an HTTP handler component
# wasmtime serve grants wasi:http/incoming-handler and wasi:http/outgoing-handler
# but NOT wasi:filesystem or wasi:sockets unless explicitly added
wasmtime serve \
--component \
my-http-handler.wasm
# Add filesystem access to an HTTP handler component
wasmtime serve \
--component \
--dir /tmp/cache::/cache \
my-http-handler.wasm
# Inspect what WASI imports a component declares before running it
wasm-tools component wit my-handler.wasm
The wasm-tools component wit command produces the WIT interfaces embedded in the component binary. Run this as part of any deployment pipeline to verify that a component declares only the capabilities your policy permits. A component that unexpectedly declares wasi:sockets/tcp or wasi:cli/environment should be investigated before deployment.
2. WIT Interface Definition — Minimal Capability Set
// http-handler.wit — HTTP handler with no persistent state access
package myorg:http-handler@1.0.0;
world http-handler {
// Required for incoming request handling
import wasi:clocks/monotonic-clock@0.2.0;
import wasi:random/random@0.2.0;
// The export that Wasmtime's serve subcommand invokes
export wasi:http/incoming-handler@0.2.0;
// The linker enforces absence of anything not declared here.
// A component binary that calls wasi:filesystem, wasi:sockets,
// wasi:cli/environment, or wasi:http/outgoing-handler will fail
// to link against this world.
}
The corresponding Rust component implementation using cargo-component:
// Cargo.toml
[package]
name = "http-handler"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasi = { version = "0.13", features = ["macros", "http"] }
[package.metadata.component]
package = "myorg:http-handler@1.0.0"
// src/lib.rs
use wasi::exports::http::incoming_handler::Guest;
use wasi::http::types::{
Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam,
};
wasi::http::proxy::export!(Handler);
struct Handler;
impl Guest for Handler {
fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
let headers = request.headers();
let content_type = headers
.get(&"content-type".to_string())
.into_iter()
.flatten()
.next()
.and_then(|b| String::from_utf8(b).ok())
.unwrap_or_else(|| "none".to_string());
let response = OutgoingResponse::new(Fields::new());
response.set_status_code(200).unwrap();
let body = response.body().unwrap();
let stream = body.write().unwrap();
let output = format!("content-type: {content_type}\n");
stream.blocking_write_and_flush(output.as_bytes()).unwrap();
drop(stream);
OutgoingBody::finish(body, None).unwrap();
ResponseOutparam::set(response_out, Ok(response));
}
}
Attempting to add std::fs::read_to_string() to this handler produces a link-time failure:
$ cargo component build --release
Compiling http-handler v0.1.0
error: component validation error at offset 0x...
import `wasi:filesystem/types@0.2.0` is not listed in world `http-handler`
required by `wasi:filesystem/preopens@0.2.0`
note: consider adding `import wasi:filesystem/types@0.2.0` to the world
The standard library’s fs module in the Rust WASI target uses wasi:filesystem internally. Because the world declaration doesn’t include that interface, the component cannot be built — the capability restriction is enforced before the binary is produced.
3. Fermyon Spin with Restricted Component Capabilities
# spin.toml
spin_manifest_version = 2
[application]
name = "api-service"
version = "0.1.0"
# HTTP trigger — all requests to /api/... go to this component
[[trigger.http]]
route = "/api/..."
component = "api-handler"
[component.api-handler]
source = "target/wasm32-wasip2/release/api_handler.wasm"
# Outbound HTTP: only these hostnames are permitted.
# The component imports wasi:http/outgoing-handler — requests to any
# hostname not listed here fail at the Spin HTTP client layer with
# an error before the request leaves the host.
allowed_outbound_hosts = ["https://api.external.com"]
# key_value_stores: not listed = no access
# If the component calls spin:key-value/store.open, Spin returns an error.
# key_value_stores = ["default"]
# sqlite_databases: not listed = no access
# sqlite_databases = ["default"]
# variables: explicit list of which application variables the component
# can read. Empty list = no variable access.
# variables = ["database_url"]
# Filesystem: components in Spin run in an ephemeral directory scoped to
# the component. No host filesystem access is provided unless files are
# listed here. This component has no files listed, so no host filesystem
# access is possible through the WASI filesystem interface.
Spin enforces these capability grants at the component loader level. When api-handler attempts to open a key-value store that isn’t in its key_value_stores list, Spin’s host implementation of the spin:key-value WIT interface returns EACCES before any storage operation occurs. When an outbound HTTP request targets a hostname not in allowed_outbound_hosts, Spin’s outbound HTTP implementation refuses the request — the component’s wasi:http/outgoing-handler import is linked, but the host implementation of that interface enforces the allowlist.
This is a second layer of capability restriction on top of the WIT interface layer. The component can have wasi:http/outgoing-handler in its WIT world (link-time capability) while Spin further restricts which endpoints it can reach at runtime (host-implementation enforcement). Both layers are required — the WIT interface layer alone does not restrict where HTTP requests go.
4. Defense in Depth: Seccomp for the Wasmtime Process
WASM components do not make syscalls directly — only the Wasmtime runtime process does. The component model’s capability restrictions apply at the WASI interface level, not the syscall level. Seccomp filters apply at the syscall level, not the WASI interface level. The two mechanisms enforce different threat models and neither is a substitute for the other.
Apply a seccomp profile to the container running Wasmtime to reduce the kernel attack surface available to the Wasmtime process itself:
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["splice", "tee", "vmsplice"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1,
"comment": "Dirty Pipe (CVE-2022-0847) uses splice; block pipe splicing operations"
},
{
"names": ["perf_event_open"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1,
"comment": "Block hardware performance counter access; side-channel risk"
},
{
"names": ["bpf"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1,
"comment": "Block BPF program loading; process memory tracing via bpf_probe_read_user"
},
{
"names": ["ptrace"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1,
"comment": "Block ptrace; prevents process memory inspection by co-tenant"
},
{
"names": ["personality"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1
},
{
"names": ["keyctl", "add_key", "request_key"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1,
"comment": "Block kernel keyring access"
}
]
}
This profile is applied to the container, not to the WASM component. There is no mechanism to apply seccomp to a WASM component directly — components do not make syscalls. The profile restricts what the Wasmtime process itself can do with the kernel.
Apply the seccomp profile in Kubernetes:
apiVersion: v1
kind: Pod
metadata:
name: wasm-handler
annotations:
seccomp.security.alpha.kubernetes.io/pod: localhost/wasmtime-profile.json
spec:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: wasmtime-profile.json
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
containers:
- name: handler
image: myregistry.io/wasm-handler:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
limits:
memory: "128Mi"
cpu: "500m"
The readOnlyRootFilesystem: true setting prevents the container process from writing to its filesystem — which matters because even if the WASM component has no wasi:filesystem capability, the Wasmtime process itself could write to the container filesystem if it were exploited. The seccomp profile and the read-only filesystem are controls on the Wasmtime process, which is not governed by the WASI capability model.
5. Wasmtime Version and CVE Tracking
The WASI P2 capability model’s security depends entirely on the correctness of the runtime that enforces it. Wasmtime has its own CVE stream, independent of base container images or OS packages. Staying current on Wasmtime is not optional.
# Check current Wasmtime version
wasmtime --version
# wasmtime 23.0.2 (2f8b9a64f 2024-12-04)
# Notable CVEs in Wasmtime's history:
# CVE-2023-26114 — Cranelift miscompilation, out-of-bounds read on x86_64
# Patched in: 6.0.1 (March 2023)
# Affected: Wasmtime <= 6.0.0
#
# CVE-2023-50711 — incorrect bounds check in 64-bit memory operations
# Patched in: 14.0.4, 15.0.1, 16.0.0 (December 2023)
# Affected: Wasmtime 14.0.0 - 14.0.3, 15.0.0
#
# Monitor: https://github.com/bytecodealliance/wasmtime/security/advisories
# RSS feed: https://github.com/bytecodealliance/wasmtime/security/advisories.atom
For Rust projects that embed Wasmtime, configure Dependabot to track the wasmtime crate:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
allow:
- dependency-name: "wasmtime"
- dependency-name: "wasmtime-*"
- dependency-name: "cranelift-*"
reviewers:
- "security-team"
commit-message:
prefix: "security(wasm)"
Restricting the Dependabot config to Wasmtime and Cranelift crates specifically ensures security updates surface quickly without the noise of all dependency updates. Treat any Wasmtime patch release as a potential security fix — the project’s release cadence is monthly major versions, with patch releases for security issues outside that cadence.
6. Component Signature Verification
The component model enforces what a component declares. It does not enforce that the component you received is the component you expected. Signature verification closes this gap.
# Sign a WASM component with cosign (keyless or key-based)
cosign sign-blob \
--key cosign.key \
--output-signature component.wasm.sig \
component.wasm
# Verify the signature before loading into Wasmtime
cosign verify-blob \
--key cosign.pub \
--signature component.wasm.sig \
component.wasm
# Sign a Spin application pushed to an OCI registry
spin registry push \
--signing-key cosign.key \
myregistry.io/myapp:1.0.0
# Verify the WIT interfaces declared in a component before running
# This is a policy check, not a signature check — inspect declared capabilities
wasm-tools component wit component.wasm | grep -E "import|export"
For CI pipelines that produce WASM components:
# .github/workflows/build-component.yml
- name: Build WASM component
run: cargo component build --release
- name: Inspect component capabilities
run: |
wasm-tools component wit \
target/wasm32-wasip2/release/api_handler.wasm \
> component-wit.txt
# Fail if the component declares unexpected capabilities
if grep -q "wasi:sockets/tcp" component-wit.txt; then
echo "ERROR: component declares unexpected TCP socket capability"
exit 1
fi
if grep -q "wasi:cli/environment" component-wit.txt; then
echo "ERROR: component declares unexpected environment variable access"
exit 1
fi
- name: Sign component
run: |
cosign sign-blob \
--key "${{ secrets.COSIGN_KEY }}" \
--output-signature target/wasm32-wasip2/release/api_handler.wasm.sig \
target/wasm32-wasip2/release/api_handler.wasm
- name: Attest build provenance
uses: actions/attest-build-provenance@v1
with:
subject-path: target/wasm32-wasip2/release/api_handler.wasm
The capability inspection step in CI creates an auditable record of what interfaces each component version declared. Combined with signature verification at deployment time, this closes the gap between the WASI P2 runtime enforcement (correct for the component that arrives) and the supply chain question (is the component that arrives the component that was audited).
Expected Behaviour
Instantiation failure for missing capability: When a component imports wasi:filesystem/types@0.2.0 but the host linker does not provide it, Wasmtime fails at instantiation:
Error: component imports instance `wasi:filesystem/types@0.2.0`, but a
matching implementation was not found in the linker
This error occurs before any WASM code executes. There is no runtime trap, no panic in the component, no signal — the component simply does not start.
Runtime trap for granted capability with scoped access violation: A component with a filesystem capability pre-opened to /tmp/workspace that attempts to open a path outside that scope via a relative traversal receives an error from the WASI filesystem implementation, not a process-level trap:
Error: failed to open path: Not permitted (wasi_filesystem:0x48)
The error is returned through the WASI API’s result type — the component sees Err(ErrorCode::NotPermitted) and can handle it programmatically. The process continues running.
wasm-tools component wit output for a minimal HTTP handler:
package myorg:http-handler@1.0.0;
world http-handler {
import wasi:clocks/monotonic-clock@0.2.0;
import wasi:random/random@0.2.0;
export wasi:http/incoming-handler@0.2.0;
}
A clean inspection output with no filesystem, socket, or environment imports is the expected baseline for any HTTP handler component that does not require persistent state.
Seccomp profile in the process hierarchy: The seccomp filter applies to the container’s PID namespace — specifically to the Wasmtime process (PID 1 in a minimal container, or a child process if an init process is present). WASM components are not separate processes; they are execution contexts within the Wasmtime process. The seccomp filter does not apply “inside” the component in any meaningful sense — there is no inside at the syscall level. The filter applies to every syscall the Wasmtime process makes, regardless of which component’s execution triggered the internal Wasmtime code that issued the syscall.
Trade-offs
Minimal capability grants require ongoing analysis: Every component deployment requires an audit of what capabilities it genuinely needs. A component developer adding a new feature — say, caching responses to a temporary file — must update both the WIT world and the deployment manifest. Over-restriction during development produces frustrating build failures; under-restriction in production recreates the ambient access problems WASI P2 was designed to eliminate. The operational discipline required to maintain minimal grants is real and non-trivial.
WASI P2 vs P1 migration: Many existing WASM libraries, bindings generators, and tools target WASI P1. The WASI P1 interface (wasi_snapshot_preview1) is not the same binary interface as WASI P2 component imports. wasm-tools component new can wrap a WASI P1 module into a component by adapting the P1 imports to P2 interfaces, but the result still has the WASI P1 capability granularity internally. A genuine WASI P2 component with fine-grained WIT interface restrictions requires rewriting against the P2 bindings. The toolchain (cargo-component, wit-bindgen, jco) is production-ready for Rust and JavaScript; for other languages the toolchain maturity varies.
Seccomp on Wasmtime process and WASM sandbox JIT bugs: A tight seccomp profile on the Wasmtime process reduces the kernel attack surface for threats that originate outside the WASM sandbox — co-tenant kernel exploits, process introspection. It does not reduce the attack surface for JIT miscompilation bugs that operate entirely within the Wasmtime process’s address space using normal memory operations. A Cranelift bug that reads adjacent heap memory does not need splice, bpf, or ptrace — it uses normal load instructions against memory the Wasmtime process already owns. Seccomp and the WASI capability model protect against different threat classes and neither is a superset of the other.
Defense-in-depth (WASI P2 + seccomp + gVisor): Running Wasmtime inside gVisor’s user-kernel adds a layer that intercepts host syscalls before they reach the Linux kernel — providing protection against kernel CVEs that neither the WASI capability model nor a seccomp profile can address. The operational cost is real: gVisor adds 3-15% latency for most syscall-heavy workloads, has incomplete syscall coverage (some syscalls produce ENOSYS in gVisor that succeed on Linux), and requires separate configuration for the gVisor runtime class in Kubernetes. The layered approach is correct for high-value multi-tenant deployments; it is over-engineered for a single-tenant workload with a strong seccomp profile already applied.
Failure Modes
Granting a wide filesystem path because it is easier: The most common deployment mistake. Instead of auditing exactly which paths a component needs, an operator pre-opens / or /app to silence build failures. This recreates POSIX ambient filesystem access inside the WASI capability model. The capability model is formally satisfied — the component was granted a filesystem capability — but the security property is entirely lost.
Treating WASI P2 capability restrictions as OS-level isolation: WASI P2 prevents a component from calling WASI filesystem functions it wasn’t granted. It does not create a kernel namespace boundary. A component cannot call open("/etc/passwd") through WASI without the filesystem capability — but if the Wasmtime process is compromised through a JIT bug or a kernel exploit, the attacker reads /etc/passwd through the Wasmtime process’s normal filesystem access, not through the WASI interface at all. WASI P2 isolation is an API-level guarantee, not an OS-level boundary.
Not updating Wasmtime separately from base container images: Container base image updates (Ubuntu, Alpine, Debian) do not include Wasmtime — Wasmtime is installed separately, typically in the Dockerfile. A base image update that brings in OpenSSL, glibc, and kernel headers does not update Wasmtime. CVE tracking must include the Wasmtime crate or binary explicitly. The bytecodealliance/wasmtime GitHub security advisory feed is the authoritative source; it is not aggregated into most Linux distribution CVE feeds.
Assuming component composition is safe because both components are signed: Signature verification establishes that a component binary has not been tampered with. It does not establish what the component does with its legitimately granted capabilities. A signed, audited inner component that has wasi:filesystem granted for /tmp can write arbitrary data to /tmp when its parse() WIT function is called with attacker-controlled input from the outer component. The trust model for composed components requires auditing the security behavior of each component’s WIT implementation, not just verifying that the binaries are signed.
Confusing allowed_outbound_hosts scope with network capability absence: In Spin’s spin.toml, allowed_outbound_hosts = [] means no outbound HTTP connections are permitted by Spin’s policy layer. But if the component also imports wasi:sockets/tcp and the Spin version being used provides a TCP socket implementation, the TCP socket access is controlled by a different mechanism. Check both the component’s WIT imports (via wasm-tools component wit) and the runtime’s capability grant configuration independently.