diff --git a/lib/wasi/src/lib.rs b/lib/wasi/src/lib.rs index 2ebf294b3..51371480f 100644 --- a/lib/wasi/src/lib.rs +++ b/lib/wasi/src/lib.rs @@ -1,7 +1,5 @@ #![deny(unused_imports, unused_variables, unused_unsafe, unreachable_patterns)] -#[macro_use] -extern crate log; #[cfg(target = "windows")] extern crate winapi; diff --git a/lib/wasi/src/state.rs b/lib/wasi/src/state.rs index bcb34d333..3eda2e888 100644 --- a/lib/wasi/src/state.rs +++ b/lib/wasi/src/state.rs @@ -9,7 +9,7 @@ use std::{ cell::Cell, fs, io::{self, Read, Seek, Write}, - path::PathBuf, + path::{Path, PathBuf}, time::SystemTime, }; use wasmer_runtime_core::debug; @@ -81,6 +81,7 @@ impl Seek for WasiFile { } } +/// A file that Wasi knows about that may or may not be open #[derive(Debug)] pub struct InodeVal { pub stat: __wasi_filestat_t, @@ -136,7 +137,10 @@ impl InodeVal { #[derive(Debug)] pub enum Kind { File { - handle: WasiFile, + /// the open file, if it's open + handle: Option, + /// the path to the file + path: PathBuf, }, Dir { /// Parent directory @@ -148,7 +152,9 @@ pub enum Kind { entries: HashMap, }, Symlink { - forwarded: Inode, + forwarded: Option, + /// This is required because, at the very least, symlinks can be deleted and we'll need to check that + path: PathBuf, }, Buffer { buffer: Vec, @@ -311,6 +317,7 @@ impl WasiFs { }) } + /* #[allow(dead_code)] fn filestat_inode( &self, @@ -318,8 +325,13 @@ impl WasiFs { flags: __wasi_lookupflags_t, ) -> Result<__wasi_filestat_t, __wasi_errno_t> { let inode_val = &self.inodes[inode]; - if let (true, Kind::Symlink { mut forwarded }) = - (flags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, &inode_val.kind) + if let ( + true, + Kind::Symlink { + mut forwarded, + path, + }, + ) = (flags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, &inode_val.kind) { // Time to follow the symlink. let mut counter = 0; @@ -355,6 +367,118 @@ impl WasiFs { self.filestat_inode(inode, flags) } + */ + + /// gets a host file from a base directory and a path + /// this function ensures the fs remains sandboxed + pub fn get_inode_at_path( + &mut self, + base: __wasi_fd_t, + path: &str, + ) -> Result { + let base_dir = self.get_fd(base)?; + let path: &Path = Path::new(path); + + let mut symlinks_followed = 0; + let mut cur_inode = base_dir.inode; + // TODO: rights checks + 'path_iter: for component in path.components() { + // for each component traverse file structure + // loading inodes as necessary + 'symlink_resolution: loop { + match &mut self.inodes[cur_inode].kind { + Kind::Buffer { .. } => unimplemented!("state::get_inode_at_path for buffers"), + Kind::Dir { + ref mut entries, + ref path, + ref parent, + .. + } => { + if component.as_os_str().to_string_lossy() == ".." { + if let Some(p) = parent { + cur_inode = *p; + continue 'path_iter; + } else { + // TODO: be smart here with multiple preopened directories + return Err(__WASI_EACCES); + } + } + if let Some(entry) = + entries.get(component.as_os_str().to_string_lossy().as_ref()) + { + cur_inode = *entry; + } else { + let file = { + let mut cd = path.clone(); + cd.push(component); + cd + }; + // TODO: verify this returns successfully when given a non-symlink + let metadata = file.symlink_metadata().ok().ok_or(__WASI_EEXIST)?; + let file_type = metadata.file_type(); + + let kind = if file_type.is_dir() { + // load DIR + Kind::Dir { + parent: Some(cur_inode), + path: file.clone(), + entries: Default::default(), + } + } else if file_type.is_file() { + // load file + Kind::File { + handle: None, + path: file.clone(), + } + } else if file_type.is_symlink() { + // use a stack and load symlinks? + // load symlink + Kind::Symlink { + forwarded: None, + path: file.clone(), + } + } else { + unimplemented!("state::get_inode_at_path unknown file type: not file, directory, or symlink"); + }; + + let new_inode = self.inodes.insert(InodeVal { + stat: get_stat_for_kind(&kind).ok_or(__WASI_EIO)?, + is_preopened: false, + name: file.to_string_lossy().to_string(), + kind, + }); + *self.inode_counter.get_mut() += 1; + + cur_inode = new_inode; + } + } + Kind::File { .. } => { + return Err(__WASI_ENOTDIR); + } + Kind::Symlink { forwarded, path } => { + if symlinks_followed > MAX_SYMLINKS { + return Err(__WASI_EMLINK); + } + if let Some(fwd) = forwarded { + cur_inode = *fwd; + } else { + // load the symlink + let _link = path.read_link().ok().ok_or(__WASI_EIO)?; + } + symlinks_followed += 1; + continue 'symlink_resolution; + } + } + break 'symlink_resolution; + } + } + + Ok(cur_inode) + } + + pub fn get_fd(&self, fd: __wasi_fd_t) -> Result<&Fd, __wasi_errno_t> { + self.fd_map.get(&fd).ok_or(__WASI_EBADF) + } pub fn filestat_fd(&self, fd: __wasi_fd_t) -> Result<__wasi_filestat_t, __wasi_errno_t> { let fd = self.fd_map.get(&fd).ok_or(__WASI_EBADF)?; @@ -414,11 +538,15 @@ impl WasiFs { let inode = &mut self.inodes[fd.inode]; match &mut inode.kind { - Kind::File { handle } => handle.flush().map_err(|_| __WASI_EIO)?, + Kind::File { + handle: Some(handle), + .. + } => handle.flush().map_err(|_| __WASI_EIO)?, // TODO: verify this behavior Kind::Dir { .. } => return Err(__WASI_EISDIR), Kind::Symlink { .. } => unimplemented!(), Kind::Buffer { .. } => (), + _ => return Err(__WASI_EIO), } } } @@ -477,35 +605,36 @@ pub fn host_file_type_to_wasi_file_type(file_type: fs::FileType) -> __wasi_filet pub fn get_stat_for_kind(kind: &Kind) -> Option<__wasi_filestat_t> { match kind { - Kind::File { handle } => match handle { - WasiFile::HostFile(hf) => { - let md = hf.metadata().ok()?; + Kind::File { handle, path } => { + let md = match handle { + Some(WasiFile::HostFile(hf)) => hf.metadata().ok()?, + None => path.metadata().ok()?, + }; - Some(__wasi_filestat_t { - st_filetype: host_file_type_to_wasi_file_type(md.file_type()), - st_size: md.len(), - st_atim: md - .accessed() - .ok()? - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_nanos() as u64, - st_mtim: md - .modified() - .ok()? - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_nanos() as u64, - st_ctim: md - .created() - .ok() - .and_then(|ct| ct.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|ct| ct.as_nanos() as u64) - .unwrap_or(0), - ..__wasi_filestat_t::default() - }) - } - }, + Some(__wasi_filestat_t { + st_filetype: host_file_type_to_wasi_file_type(md.file_type()), + st_size: md.len(), + st_atim: md + .accessed() + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_nanos() as u64, + st_mtim: md + .modified() + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_nanos() as u64, + st_ctim: md + .created() + .ok() + .and_then(|ct| ct.duration_since(SystemTime::UNIX_EPOCH).ok()) + .map(|ct| ct.as_nanos() as u64) + .unwrap_or(0), + ..__wasi_filestat_t::default() + }) + } Kind::Dir { path, .. } => { let md = path.metadata().ok()?; Some(__wasi_filestat_t { diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index 8151a8c22..c03f8d96b 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -648,9 +648,13 @@ pub fn fd_pwrite( let inode = &mut state.fs.inodes[fd_entry.inode]; let bytes_written = match &mut inode.kind { - Kind::File { handle } => { - handle.seek(::std::io::SeekFrom::Start(offset as u64)); - wasi_try!(write_bytes(handle, memory, iovs_arr_cell)) + Kind::File { handle, .. } => { + if let Some(handle) = handle { + handle.seek(::std::io::SeekFrom::Start(offset as u64)); + wasi_try!(write_bytes(handle, memory, iovs_arr_cell)) + } else { + return __WASI_EINVAL; + } } Kind::Dir { .. } => { // TODO: verify @@ -736,9 +740,13 @@ pub fn fd_read( let inode = &mut state.fs.inodes[fd_entry.inode]; let bytes_read = match &mut inode.kind { - Kind::File { handle } => { - handle.seek(::std::io::SeekFrom::Start(offset as u64)); - wasi_try!(read_bytes(handle, memory, iovs_arr_cell)) + Kind::File { handle, .. } => { + if let Some(handle) = handle { + handle.seek(::std::io::SeekFrom::Start(offset as u64)); + wasi_try!(read_bytes(handle, memory, iovs_arr_cell)) + } else { + return __WASI_EINVAL; + } } Kind::Dir { .. } => { // TODO: verify @@ -1011,10 +1019,13 @@ pub fn fd_write( let inode = &mut state.fs.inodes[fd_entry.inode]; let bytes_written = match &mut inode.kind { - Kind::File { handle } => { - handle.seek(::std::io::SeekFrom::Start(offset as u64)); - - wasi_try!(write_bytes(handle, memory, iovs_arr_cell)) + Kind::File { handle, .. } => { + if let Some(handle) = handle { + handle.seek(::std::io::SeekFrom::Start(offset as u64)); + wasi_try!(write_bytes(handle, memory, iovs_arr_cell)) + } else { + return __WASI_EINVAL; + } } Kind::Dir { .. } => { // TODO: verify @@ -1152,105 +1163,11 @@ pub fn path_filestat_get( }) .map_err(|_| __WASI_EINVAL)); debug!("=> path: {}", &path_string); - let path = std::path::PathBuf::from(path_string); - let path_vec = path - .components() - .map(|comp| comp.as_os_str().to_string_lossy().to_string()) - .collect::>(); + + let file_inode = wasi_try!(state.fs.get_inode_at_path(fd, path_string)); + let stat = wasi_try!(get_stat_for_kind(&state.fs.inodes[file_inode].kind).ok_or(__WASI_EIO)); + let buf_cell = wasi_try!(buf.deref(memory)); - - if path_vec.is_empty() { - return __WASI_EINVAL; - } - let mut cumulative_path = std::path::PathBuf::from(wasi_try!(state - .fs - .get_base_path_for_directory(root_dir.inode) - .ok_or(__WASI_EIO))); - - debug!("=> Path vec: {:?}:", &path_vec); - // find the inode by traversing the path - let mut inode = root_dir.inode; - 'outer: for segment in &path_vec[..(path_vec.len() - 1)] { - // loop to traverse symlinks - // TODO: proper cycle detection - let mut sym_count = 0; - loop { - match &state.fs.inodes[inode].kind { - Kind::Dir { entries, .. } => { - cumulative_path.push(&segment); - if let Some(entry) = entries.get(segment) { - debug!("Entry {:?} found", &segment); - inode = entry.clone(); - } else { - // lazily load - debug!("Lazily loading entry {:?}", &segment); - let path_metadata = - wasi_try!(cumulative_path.metadata().map_err(|_| __WASI_ENOENT)); - if !path_metadata.is_dir() { - // TODO: should this just return invalid arg? - return __WASI_ENOTDIR; - } - let kind = Kind::Dir { - parent: Some(inode), - path: std::path::PathBuf::from(&segment), - entries: Default::default(), - }; - let inode_val = InodeVal::from_file_metadata( - &path_metadata, - segment.clone(), - false, - kind, - ); - let new_inode = state.fs.inodes.insert(inode_val); - let inode_idx = state.fs.inode_counter.get(); - state.fs.inode_counter.replace(inode_idx + 1); - if let Kind::Dir { entries, .. } = &mut state.fs.inodes[inode].kind { - // check that we're not displacing any entries - assert!(entries.insert(segment.clone(), new_inode).is_none()); - state.fs.inodes[new_inode].stat.st_ino = state.fs.inode_counter.get(); - inode = new_inode; - } - debug!("Directory {:#?} lazily loaded", &cumulative_path); - } - continue 'outer; - } - Kind::Symlink { forwarded } => { - // TODO: updated cumulative path - sym_count += 1; - inode = forwarded.clone(); - if sym_count > MAX_SYMLINKS { - return __WASI_ELOOP; - } - } - _ => { - return __WASI_ENOTDIR; - } - } - } - } - - let stat = match &state.fs.inodes[inode].kind { - Kind::Dir { path, entries, .. } => { - // read it from internal data structures if we can - let last_segment = path_vec.last().unwrap(); - cumulative_path.push(last_segment); - - if entries.contains_key(last_segment) { - state.fs.inodes[entries[last_segment]].stat - } else { - // otherwise read it from the host FS - if !cumulative_path.exists() { - return __WASI_ENOENT; - } - let final_path_metadata = - wasi_try!(cumulative_path.metadata().map_err(|_| __WASI_EIO)); - wasi_try!(get_stat_for_kind(&state.fs.inodes[inode].kind).ok_or(__WASI_EIO)) - } - } - _ => { - return __WASI_ENOTDIR; - } - }; buf_cell.set(stat); __WASI_ESUCCESS @@ -1562,7 +1479,8 @@ pub fn path_open( real_open_file }; Kind::File { - handle: WasiFile::HostFile(real_opened_file), + handle: Some(WasiFile::HostFile(real_opened_file)), + path: file_path, } }; @@ -1612,24 +1530,37 @@ pub fn path_readlink( let memory = ctx.memory(0); let base_dir = wasi_try!(state.fs.fd_map.get(&dir_fd).ok_or(__WASI_EBADF)); + if !has_rights(base_dir.rights, __WASI_RIGHT_PATH_READLINK) { + return __WASI_EACCES; + } let path_str = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); - let result = wasi_try!(std::fs::read_link(path_str).ok().ok_or(__WASI_EIO)); - let result_path_as_str = result.to_string_lossy(); - let bytes = result_path_as_str.bytes(); - if bytes.len() < buf_len as usize { - return __WASI_EOVERFLOW; - } + let inode = wasi_try!(state.fs.get_inode_at_path(dir_fd, path_str)); - let out = wasi_try!(buf.deref(memory, 0, buf_len)); - let mut bytes_written = 0; - for b in bytes { - out[bytes_written].set(b); - bytes_written += 1; - } - // should we null terminate this? + if let Kind::Symlink { forwarded, .. } = &state.fs.inodes[inode].kind { + if let Some(fwd) = forwarded { + let resolved_name = &state.fs.inodes[*fwd].name; + let bytes = resolved_name.bytes(); + if bytes.len() < buf_len as usize { + return __WASI_EOVERFLOW; + } - let bytes_out = wasi_try!(buf_used.deref(memory)); - bytes_out.set(bytes_written as u32); + let out = wasi_try!(buf.deref(memory, 0, buf_len)); + let mut bytes_written = 0; + for b in bytes { + out[bytes_written].set(b); + bytes_written += 1; + } + // should we null terminate this? + + let bytes_out = wasi_try!(buf_used.deref(memory)); + bytes_out.set(bytes_written as u32); + } else { + panic!("do this before shipping"); + return __WASI_EINVAL; + } + } else { + return __WASI_EINVAL; + } __WASI_ESUCCESS }