wasi_preview1 (wasm v0.3.0)

View Source

WASI Preview 1, as an Erlang host interface.

Build an import map here when you want to choose exactly what a WASI program may reach. This is not an embedded WASI runtime: each syscall is an ordinary Erlang host function, so you can inspect it, trace it, replace it or refuse it, and the capability decisions are made in Erlang rather than inside somebody else's C library.

Grant capabilities explicitly, because nothing is ambient

Wasi = #{ stdout => group_leader(),
          dirs   => [{<<"/data">>, "/srv/app/data", read}],
          env    => #{<<"MODE">> => <<"production">>},
          args   => [<<"prog">>, <<"--flag">>],
          clocks => [monotonic],
          random => strong,
          net    => #{connect => [{tcp, <<"10.0.0.0/8">>, 443}]} },
{ok, Imports} = wasi_preview1:imports(Wasi).

Leave a key out and the module does not have that capability: the syscall returns ENOTCAPABLE. No dirs means no filesystem at all, not one rooted at the current directory. No net means no network at all, not a network restricted to somewhere sensible. No env means environ_get reports zero variables rather than leaking the host's. That is the opposite of the usual default, and it is the point: a module gets what you gave it and nothing else.

wasi_net parses the network grant, and wasi_sock runs the sockets behind it. docs/security.md says what a grant does not cover.

ENOTCAPABLE stays distinct from EACCES throughout, so the module (and whoever is debugging it) can tell "you were not granted this" from "the host operating system refused".

Path resolution, which is where sandboxes actually fail, is in wasi_path.

Where things are

2,400 lines, one clause per syscall. The banners group them by the part of the capability model they belong to:

you wantlook at
the import set handed to an instanceimports/1, in %%% api
the descriptor table, and what an fd is%%% file I/O, and #wasi{} in include/wasi.hrl
preopened directories and their numbering%%% preopens
a path syscall, and the sandbox it goes through%%% path syscalls, then wasi_path
sockets%%% sockets, %%% sockets: the extension, then wasi_sock
proc_exit and the trap it becomes%%% process
clocks, randomness, args and environmenttheir own banners

Two conventions run through every clause. An iovec is a {ptr, len} pair read out of guest memory, so a syscall reads the vector before it reads the data. And every return is an errno, a number, never an Erlang error: the guest is entitled to see EBADF and carry on. The distinction between ENOTCAPABLE and EACCES is the capability model speaking and is explained above.

Summary

Functions

Close every descriptor this instance still holds.

Pull the exit status out of the error proc_exit produces.

Build the import map, under a module name you choose. Pass wasi_unstable to bind the same implementation for an older toolchain.

The snapshot hook for this import module.

Functions

close_all(Inst)

-spec close_all(term()) -> ok.

Close every descriptor this instance still holds.

Registered as an instance cleanup, so wasm:destroy/1 runs it. A guest that exits without closing its files and sockets is ordinary, and dropping the handles only makes them unreachable: the operating system resources stay until the owning process exits. Preopens are closed here too. Refusing to close one is a rule about the guest asking, not about teardown.

default_config()

-spec default_config() -> map().

exit_code/1

-spec exit_code(term()) -> {ok, integer()} | error.

Pull the exit status out of the error proc_exit produces.

imports(Config)

-spec imports(map()) -> map().

imports(Config0, ModuleName)

-spec imports(map(), binary()) -> map().

Build the import map, under a module name you choose. Pass wasi_unstable to bind the same implementation for an older toolchain.

snapshot_hook()

-spec snapshot_hook() -> map().

The snapshot hook for this import module.

Pass it as snapshot_hooks => #{~"wasi_snapshot_preview1" => Hook} at instantiation. Without it a capture is refused, because a module in bindings with no hook is a module nobody has vouched for.

What it can and cannot see

It sees live descriptors, and that is all. A capture is eligible while exactly the baseline is open: the stdio entries and the preopens this configuration created, nothing opened since and nothing closed since. That is a claim that can be checked.

What it cannot see is a descriptor number the guest copied into a local variable, which is an integer in linear memory indistinguishable from any other. So the guarantee is "no descriptor outside the reconstructible baseline is still open", and not "the guest is not holding a number". Serialising a live file or socket stays out of scope, and this hook does not pretend otherwise.

A guest that opened a file during initialisation and kept it is not snapshottable, and is told so by name.