wasi_fs (wasm v0.1.0)

View Source

File access for WASI, over whichever backend is available.

Call backend/0 if you need to know which one you got, because the weaker one carries a documented race. There are two implementations of the same six operations:

  • native, via wasi_file_nif, which walks each path component with openat(..., O_NOFOLLOW) so a name is never resolved twice and no symlink is ever followed. This closes the time-of-check-to-time-of-use window.
  • fallback, via file plus wasi_path, which refuses every escape it can detect lexically and through filelib:safe_relative_path/2, but resolves the path and opens it as two separate steps.

The backend is chosen once, at open time, and recorded in the handle, so a descriptor is always used with the backend that produced it.

A native handle is safe to share between processes. Each one carries its own lock, so a close cannot land between another operation testing the descriptor and using it; without that, closing while a read was in flight returned the descriptor number to the kernel, the next open anywhere in the VM was handed it back, and the read in flight read whatever that now was.

You do not choose between them. wasi_preview1 asks for a path beneath a preopen and gets a handle back; which mechanism enforced the boundary is this module's business.

Summary

Types

An open file, tagged with the backend that opened it.

A directory that guest paths are resolved beneath.

Functions

Which backend is in use. fallback still refuses every detectable escape.

Release a root. A preopen holds one for the life of the instance.

List the directory a root names, rather than one beneath it.

Open Guest beneath the preopened host directory Root.

Open a preopen root, once, for every path that will be resolved under it.

Set a name's access and modification times, in nanoseconds.

As set_times/4, following the final component when asked.

As set_times/4, for an open descriptor rather than a name.

Stat a name without following it, which is what a link's own stat is.

As stat/2, following the final component when asked.

Everything the WASI filestat holds, for an open descriptor.

Create a link holding Target verbatim. The target is never resolved.

Flush to disk, answering ok for anything with nothing to flush.

Cut the file to Len bytes, or extend it with zeroes.

Types

handle()

-nominal handle() :: {native, term()} | {fallback, file:io_device(), string()}.

An open file, tagged with the backend that opened it.

root()

-nominal root() :: {native, term()} | {fallback, string()}.

A directory that guest paths are resolved beneath.

On the native backend it is the directory itself, opened once, and every guest path is resolved relative to that descriptor. Naming the root by path on each open would leave it to be resolved again every time, so replacing or renaming it between two opens would silently move the sandbox; anchoring means operations continue against the directory that was opened, and a swapped child is what gets refused.

Functions

backend()

-spec backend() -> native | fallback.

Which backend is in use. fallback still refuses every detectable escape.

close/1

-spec close(handle()) -> ok.

forget/1

-spec forget(root()) -> ok.

Release a root. A preopen holds one for the life of the instance.

link(From, A, To, B)

-spec link(root(), binary(), root(), binary()) -> ok | {error, non_neg_integer()}.

list/1

-spec list(root()) -> {ok, [binary()]} | {error, non_neg_integer()}.

List the directory a root names, rather than one beneath it.

What fd_readdir needs: the descriptor already names the directory, and on the native backend it is the directory, so there is no name to resolve. . and .. are filtered out here so both backends answer the same thing; the caller puts them back, because a reader that counts entries expects them first.

list_dir(Root, Guest)

-spec list_dir(file:filename_all(), binary()) -> {ok, [binary()]} | {error, non_neg_integer()}.

mkdir/2

-spec mkdir(root(), binary()) -> ok | {error, non_neg_integer()}.

open/3

-spec open(root(), binary(), [read | write | create | truncate | exclusive]) ->
              {ok, handle()} | {error, non_neg_integer()}.

Open Guest beneath the preopened host directory Root.

Returns a WASI errno on failure, already mapped, so callers do not have to know which backend produced it.

pread/3

-spec pread(handle(), non_neg_integer(), non_neg_integer()) ->
               {ok, binary()} | eof | {error, non_neg_integer()}.

preopen(Host)

-spec preopen(file:filename_all()) -> {ok, root()} | {error, non_neg_integer()}.

Open a preopen root, once, for every path that will be resolved under it.

pwrite/3

-spec pwrite(handle(), non_neg_integer(), binary()) ->
                {ok, non_neg_integer()} | {error, non_neg_integer()}.

readlink/2

-spec readlink(root(), binary()) -> {ok, binary()} | {error, non_neg_integer()}.

rename(From, A, To, B)

-spec rename(root(), binary(), root(), binary()) -> ok | {error, non_neg_integer()}.

rmdir/2

-spec rmdir(root(), binary()) -> ok | {error, non_neg_integer()}.

set_times(Root, Guest, Atime, Mtime)

-spec set_times(root(), binary(), non_neg_integer() | omit, non_neg_integer() | omit) ->
                   ok | {error, non_neg_integer()}.

Set a name's access and modification times, in nanoseconds.

omit for either leaves that stamp alone, which is what the WASI fstflags bits ask for when they name only one of the two. The final component is not followed, so this sets the times on a symlink itself rather than on its target, which is what path_filestat_set_times without a follow flag means.

set_times/5

-spec set_times(root(), binary(), non_neg_integer() | omit, non_neg_integer() | omit, follow | nofollow) ->
                   ok | {error, non_neg_integer()}.

As set_times/4, following the final component when asked.

path_filestat_set_times takes the same lookup flag path_filestat_get does, and means the same thing by it: without it the symlink's own times are set, with it the target's.

The fallback follows either way. file:write_file_info/3 has no lstat counterpart, so on that backend a symlink's own times cannot be set at all, which is the same class of limitation as its path resolution.

set_times_fd/3

-spec set_times_fd(handle(), non_neg_integer() | omit, non_neg_integer() | omit) ->
                      ok | {error, non_neg_integer()}.

As set_times/4, for an open descriptor rather than a name.

size/1

-spec size(handle()) -> {ok, non_neg_integer()} | {error, non_neg_integer()}.

stat(Root, Guest)

-spec stat(root(), binary()) -> {ok, map()} | {error, non_neg_integer()}.

Stat a name without following it, which is what a link's own stat is.

stat/3

-spec stat(root(), binary(), follow | nofollow) -> {ok, map()} | {error, non_neg_integer()}.

As stat/2, following the final component when asked.

path_filestat_get takes a lookup flag saying whether it is asking about a symlink or about what it points at, and the two answers differ in every field. Ignoring it reported the link for both, which upstream catches three ways: path_exists, symlink_filestat and fd_filestat_set.

Only the final component is a choice. Everything before it is followed either way, because that is what a path means.

stat_fd/1

-spec stat_fd(handle()) -> {ok, map()} | {error, non_neg_integer()}.

Everything the WASI filestat holds, for an open descriptor.

The same map stat/2 answers for a name. fd_filestat_get used to report only the size and the descriptor's own filetype and zero for the rest, which is what made two files in a directory share an inode.

The fallback has no fstat, so it stats the path the descriptor was opened from. That is a second resolution and therefore a window, which is the whole reason the native backend exists; it is the fallback's existing bargain and not a new one.

symlink/3

-spec symlink(root(), binary(), binary()) -> ok | {error, non_neg_integer()}.

Create a link holding Target verbatim. The target is never resolved.

sync/1

-spec sync(handle()) -> ok | {error, non_neg_integer()}.

Flush to disk, answering ok for anything with nothing to flush.

truncate/2

-spec truncate(handle(), non_neg_integer()) -> ok | {error, non_neg_integer()}.

Cut the file to Len bytes, or extend it with zeroes.

unlink/2

-spec unlink(root(), binary()) -> ok | {error, non_neg_integer()}.