wasi_file_nif (wasm v0.1.0)

View Source

Optional native file access that closes the WASI path race.

Changing the C means running it under a sanitizer; the sanitizer section of docs/wasi.md says how, and why AddressSanitizer alone is not enough for it.

You never call this; wasi_fs picks it when it built. Call wasi_fs:backend/0 to find out whether you have it. wasi_path resolves a path and then opens it, and a component can be swapped for a symlink in between. filelib:safe_relative_path/2 closes every lexical and static symlink escape, but Erlang exposes neither openat nor O_NOFOLLOW, so the race itself cannot be closed from Erlang.

This module walks a path one component at a time with openat(..., O_NOFOLLOW) relative to the previously opened directory, so each name is resolved exactly once, by the kernel, and no symlink is followed at any depth.

Optional by construction

If the shared object is missing or fails to load, available/0 returns false and wasi_path falls back to the pure-Erlang resolver with the race documented. Building this project never requires a C compiler.

Scope

Six functions, all file I/O. No WebAssembly semantics cross the boundary: capability decisions, rights masking and errno mapping stay in Erlang. Every call is O(1) or bounded by an explicit length, and all of them run on dirty I/O schedulers because they block.

Summary

Functions

Whether the native backend loaded. false means the fallback is in use.

Flush this file to disk.

Set an open descriptor's times, as two 64-bit second/nanosecond pairs.

Open RelPath beneath Dir, one component at a time.

Open a preopen root by name, once.

The two-place operations, rename and link.

Act on a name inside a directory, without resolving that name twice.

Functions

available()

-spec available() -> boolean().

Whether the native backend loaded. false means the fallback is in use.

close(H)

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

fstat(H)

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

fsync(H)

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

Flush this file to disk.

Answers ok for a descriptor that cannot be synced, such as a pipe. A guest that calls fd_sync defensively should not be failed for holding something that has nothing to flush.

ftruncate(H, Len)

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

futimes(H, Spec)

-spec futimes(term(), binary()) -> ok | {error, atom()}.

Set an open descriptor's times, as two 64-bit second/nanosecond pairs.

futimens, because an open file has no name to walk: fd_filestat_set_times cannot go through the path operations the way its path_* counterpart does.

open_at(Dir, RelPath, Flags, Follow)

-spec open_at(term(), binary(), integer(), 0 | 1) -> {ok, term()} | {error, atom()}.

Open RelPath beneath Dir, one component at a time.

Dir is either a handle from a previous call or, for a preopen, the host root as a string. Flags are raw open(2) flags.

Follow is 1 when the final component may be a symlink that gets followed and 0 when it may not, which is what LOOKUPFLAGS_SYMLINK_FOLLOW decides. Components before the last are always followed: that is what a path means.

open_dir(Path)

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

Open a preopen root by name, once.

The only place a path is resolved by name rather than component by component, and it happens once per preopen rather than once per open, which is what anchoring means.

path_op2(Dir1, Rel1, Dir2, Rel2, Op)

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

The two-place operations, rename and link.

Each end is resolved against its own directory. Resolving only one would let a module move a file out of the sandbox by naming the destination carelessly.

path_op(Dir, Rel, Op, Arg)

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

Act on a name inside a directory, without resolving that name twice.

The walk stops one component short and the operation is a single *at call against a descriptor nobody can substitute, which is what closes the window between checking a path and acting on it. Op is a small integer because choosing it is wasi_fs's business and no WebAssembly meaning crosses this boundary.

pread(H, Offset, Len)

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

pwrite(H, Offset, Data)

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

readdir(H)

-spec readdir(term()) -> {ok, [binary()]} | {error, integer()}.