Files
wasmtime/wasi-common/src/hostcalls_impl/fs_helpers.rs
Jakub Konka cd1e54487c Merge wasi-common into wasmtime
This commit merges [CraneStation/wasi-common] repo as a subdir of
this repo while preserving **all** of git history. There is an
initiative to pull `wasi-common` into [CraneStation/wasmtime], and
[CraneStation/wasmtime] becoming a monorepo. This came about for
several reasons with a common theme of convenience, namely,
having a monorepo:
1. cleans up the problem of dependencies (as we have seen first
   hand with dependabot enabled, it can cause some grief)
2. completely removes the problem of syncing the closely dependent
   repos (e.g., updating `wasi-common` with say a bugfix generally
   implies creating a "sync" commit for pulling in the changes into
   the "parent" repo, in this case, `wasmtime`)
3. mainly for the two reasons above, makes publishing to crates.io
   easier
4. hopefully streamlines the process of getting the community
   involved in contributing to `wasi-common` as now everything
   is one place

[CraneStation/wasi-common]: https://github.com/CraneStation/wasi-common
[CraneStation/wasmtime]: https://github.com/CraneStation/wasmtime
2019-11-07 15:45:58 +01:00

214 lines
9.0 KiB
Rust

#![allow(non_camel_case_types)]
use crate::sys::host_impl;
use crate::sys::hostcalls_impl::fs_helpers::*;
use crate::{fdentry::FdEntry, wasi, Error, Result};
use std::fs::File;
use std::path::{Component, Path};
#[derive(Debug)]
pub(crate) struct PathGet {
dirfd: File,
path: String,
}
impl PathGet {
pub(crate) fn dirfd(&self) -> &File {
&self.dirfd
}
pub(crate) fn path(&self) -> &str {
&self.path
}
}
/// Normalizes a path to ensure that the target path is located under the directory provided.
///
/// This is a workaround for not having Capsicum support in the OS.
pub(crate) fn path_get(
fe: &FdEntry,
rights_base: wasi::__wasi_rights_t,
rights_inheriting: wasi::__wasi_rights_t,
dirflags: wasi::__wasi_lookupflags_t,
path: &str,
needs_final_component: bool,
) -> Result<PathGet> {
const MAX_SYMLINK_EXPANSIONS: usize = 128;
if path.contains('\0') {
// if contains NUL, return EILSEQ
return Err(Error::EILSEQ);
}
if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY {
// if `dirfd` doesn't refer to a directory, return `ENOTDIR`.
return Err(Error::ENOTDIR);
}
let dirfd = fe
.as_descriptor(rights_base, rights_inheriting)?
.as_file()?
.try_clone()?;
// Stack of directory file descriptors. Index 0 always corresponds with the directory provided
// to this function. Entering a directory causes a file descriptor to be pushed, while handling
// ".." entries causes an entry to be popped. Index 0 cannot be popped, as this would imply
// escaping the base directory.
let mut dir_stack = vec![dirfd];
// Stack of paths left to process. This is initially the `path` argument to this function, but
// any symlinks we encounter are processed by pushing them on the stack.
let mut path_stack = vec![path.to_owned()];
// Track the number of symlinks we've expanded, so we can return `ELOOP` after too many.
let mut symlink_expansions = 0;
// TODO: rewrite this using a custom posix path type, with a component iterator that respects
// trailing slashes. This version does way too much allocation, and is way too fiddly.
loop {
match path_stack.pop() {
Some(cur_path) => {
log::debug!("path_get cur_path = {:?}", cur_path);
let ends_with_slash = cur_path.ends_with('/');
let mut components = Path::new(&cur_path).components();
let head = match components.next() {
None => return Err(Error::ENOENT),
Some(p) => p,
};
let tail = components.as_path();
if tail.components().next().is_some() {
let mut tail = host_impl::path_from_host(tail.as_os_str())?;
if ends_with_slash {
tail.push('/');
}
path_stack.push(tail);
}
log::debug!("path_get path_stack = {:?}", path_stack);
match head {
Component::Prefix(_) | Component::RootDir => {
// path is absolute!
return Err(Error::ENOTCAPABLE);
}
Component::CurDir => {
// "." so skip
}
Component::ParentDir => {
// ".." so pop a dir
let _ = dir_stack.pop().ok_or(Error::ENOTCAPABLE)?;
// we're not allowed to pop past the original directory
if dir_stack.is_empty() {
return Err(Error::ENOTCAPABLE);
}
}
Component::Normal(head) => {
let mut head = host_impl::path_from_host(head)?;
if ends_with_slash {
// preserve trailing slash
head.push('/');
}
if !path_stack.is_empty() || (ends_with_slash && !needs_final_component) {
match openat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) {
Ok(new_dir) => {
dir_stack.push(new_dir);
}
Err(e) => {
match e.as_wasi_errno() {
wasi::__WASI_ELOOP
| wasi::__WASI_EMLINK
| wasi::__WASI_ENOTDIR =>
// Check to see if it was a symlink. Linux indicates
// this with ENOTDIR because of the O_DIRECTORY flag.
{
// attempt symlink expansion
let mut link_path = readlinkat(
dir_stack.last().ok_or(Error::ENOTCAPABLE)?,
&head,
)?;
symlink_expansions += 1;
if symlink_expansions > MAX_SYMLINK_EXPANSIONS {
return Err(Error::ELOOP);
}
if head.ends_with('/') {
link_path.push('/');
}
log::debug!(
"attempted symlink expansion link_path={:?}",
link_path
);
path_stack.push(link_path);
}
_ => {
return Err(e);
}
}
}
}
continue;
} else if ends_with_slash
|| (dirflags & wasi::__WASI_LOOKUP_SYMLINK_FOLLOW) != 0
{
// if there's a trailing slash, or if `LOOKUP_SYMLINK_FOLLOW` is set, attempt
// symlink expansion
match readlinkat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) {
Ok(mut link_path) => {
symlink_expansions += 1;
if symlink_expansions > MAX_SYMLINK_EXPANSIONS {
return Err(Error::ELOOP);
}
if head.ends_with('/') {
link_path.push('/');
}
log::debug!(
"attempted symlink expansion link_path={:?}",
link_path
);
path_stack.push(link_path);
continue;
}
Err(e) => {
if e.as_wasi_errno() != wasi::__WASI_EINVAL
&& e.as_wasi_errno() != wasi::__WASI_ENOENT
// this handles the cases when trying to link to
// a destination that already exists, and the target
// path contains a slash
&& e.as_wasi_errno() != wasi::__WASI_ENOTDIR
{
return Err(e);
}
}
}
}
// not a symlink, so we're done;
return Ok(PathGet {
dirfd: dir_stack.pop().ok_or(Error::ENOTCAPABLE)?,
path: head,
});
}
}
}
None => {
// no further components to process. means we've hit a case like "." or "a/..", or if the
// input path has trailing slashes and `needs_final_component` is not set
return Ok(PathGet {
dirfd: dir_stack.pop().ok_or(Error::ENOTCAPABLE)?,
path: String::from("."),
});
}
}
}
}