wasi_path (wasm v0.1.0)
View SourceCapability-scoped path resolution.
This is the part of WASI that decides whether a sandbox is real, so read it before you trust one. A module names a path relative to a preopened directory, and this module decides which host path, if any, that is allowed to mean.
Four escape routes have to be closed, and all four are tested in wasi_SUITE:
- absolute paths (
/etc/passwd) - parent traversal (
../../etc/passwd) - traversal that only escapes partway through (
sub/../../outside) - symlinks pointing out of the sandbox, including absolute ones and symlinked directories used as a prefix
filelib:safe_relative_path/2 already handles all four, including the
symlink cases, so it is used rather than reimplemented: a hand-rolled path
sanitiser is exactly the kind of code that looks right and is not.
Residual risk you carry. There is a time-of-check to time-of-use window
between resolving a path and opening it: someone with write access to the
sandbox directory can replace a component with a symlink in between.
Re-verifying the opened path afterwards narrows the window without closing it,
because Erlang's file module exposes neither openat nor O_NOFOLLOW. Do not
point a preopen at a directory an untrusted party can write to concurrently.
Summary
Functions
-spec resolve(file:filename_all(), binary()) -> {ok, file:filename_all()} | {error, non_neg_integer()}.
Resolve a guest-supplied path against a preopened host directory.
-spec resolve(file:filename_all(), binary(), boolean()) -> {ok, file:filename_all()} | {error, non_neg_integer()}.
As resolve/2. MustExist additionally requires the target to be
present, which distinguishes "you may not name that" from "it is not there".
-spec resolve(file:filename_all(), binary(), boolean(), follow | nofollow) -> {ok, file:filename_all()} | {error, non_neg_integer()}.
As resolve/3, saying whether the last component may be resolved.
nofollow is what lets this backend name a symlink. filelib:safe_relative_path/2
resolves links in the course of deciding a path is safe, which is what makes it
safe without openat, and it means the resolved path is the link's target: a
readlink through it answers EINVAL about a regular file, and an unlink
removes the wrong thing.
So for nofollow only the parent is resolved, and the last component is
appended by name. Containment still holds and holds for the same reason: the
parent is inside the root because safe_relative_path/2 said so, and a bare
name with no separator in it cannot leave the directory it is in.
. and .. are resolved whole. They name directories, so there is no final
component to decline to follow, and appending them by name is exactly the
traversal this must not do by hand.
-spec verify_within(file:filename_all(), file:filename_all()) -> ok | {error, non_neg_integer()}.
Re-check that a path is still inside the root.
Called after opening, to narrow the time-of-check to time-of-use window described above. It cannot eliminate it.