From 5119f07ea36134f1254ac6db720562b123744158 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Fri, 12 Jul 2019 15:58:28 -0700 Subject: [PATCH 01/11] implement wasi::readlink --- lib/runtime-core/src/memory/ptr.rs | 9 +++++++++ lib/wasi/src/ptr.rs | 5 +++++ lib/wasi/src/syscalls/mod.rs | 24 +++++++++++++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/runtime-core/src/memory/ptr.rs b/lib/runtime-core/src/memory/ptr.rs index 465474c1316..0a9454acc27 100644 --- a/lib/runtime-core/src/memory/ptr.rs +++ b/lib/runtime-core/src/memory/ptr.rs @@ -126,6 +126,15 @@ impl WasmPtr { [index as usize..slice_full_len]; Some(cell_ptrs) } + + pub fn get_utf8_string<'a>(self, memory: &'a Memory, str_len: u32) -> Option<&'a str> { + if self.offset as usize + str_len as usize > memory.size().bytes().0 { + return None; + } + let ptr = unsafe { memory.view::().as_ptr().add(self.offset as usize) as *const u8 }; + let slice: &[u8] = unsafe { std::slice::from_raw_parts(ptr, str_len as usize) }; + std::str::from_utf8(slice).ok() + } } unsafe impl WasmExternType for WasmPtr { diff --git a/lib/wasi/src/ptr.rs b/lib/wasi/src/ptr.rs index 0af012eacdd..87bdc104c3c 100644 --- a/lib/wasi/src/ptr.rs +++ b/lib/wasi/src/ptr.rs @@ -75,4 +75,9 @@ impl WasmPtr { ) -> Result<&'a [Cell], __wasi_errno_t> { self.0.deref(memory, index, length).ok_or(__WASI_EFAULT) } + + #[inline(always)] + pub fn get_utf8_string<'a>(self, memory: &'a Memory, str_len: u32) -> Option<&'a str> { + self.0.get_utf8_string(memory, str_len) + } } diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index 45253262782..ac2354fc48e 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -1608,7 +1608,29 @@ pub fn path_readlink( buf_used: WasmPtr, ) -> __wasi_errno_t { debug!("wasi::path_readlink"); - unimplemented!("wasi::path_readlink") + let state = get_wasi_state(ctx); + let memory = ctx.memory(0); + + let base_dir = wasi_try!(state.fs.fd_map.get(&dir_fd).ok_or(__WASI_EBADF)); + 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 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; + } + + let bytes_out = wasi_try!(buf_used.deref(memory)); + bytes_out.set(bytes_written as u32); + + __WASI_ESUCCESS } pub fn path_remove_directory( From 122963909fae5c1e21667b44f7493accca65838e Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Mon, 15 Jul 2019 09:59:07 -0700 Subject: [PATCH 02/11] symlink code from last week --- lib/wasi/src/syscalls/mod.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index ac2354fc48e..8151a8c2248 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -1626,6 +1626,7 @@ pub fn path_readlink( 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); @@ -1639,9 +1640,18 @@ pub fn path_remove_directory( path: WasmPtr, path_len: u32, ) -> __wasi_errno_t { + // TODO check if fd is a dir, ensure it's within sandbox, etc. debug!("wasi::path_remove_directory"); - unimplemented!("wasi::path_remove_directory") + let state = get_wasi_state(ctx); + let memory = ctx.memory(0); + + let base_dir = wasi_try!(state.fs.fd_map.get(&fd).ok_or(__WASI_EBADF)); + let path_str = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); + let _result = wasi_try!(std::fs::remove_dir(path_str).ok().ok_or(__WASI_EIO)); + + __WASI_ESUCCESS } + pub fn path_rename( ctx: &mut Ctx, old_fd: __wasi_fd_t, @@ -1665,15 +1675,25 @@ pub fn path_symlink( debug!("wasi::path_symlink"); unimplemented!("wasi::path_symlink") } + pub fn path_unlink_file( ctx: &mut Ctx, fd: __wasi_fd_t, path: WasmPtr, path_len: u32, ) -> __wasi_errno_t { + // TODO check if fd is a dir, ensure it's within sandbox, etc. debug!("wasi::path_unlink_file"); - unimplemented!("wasi::path_unlink_file") + let state = get_wasi_state(ctx); + let memory = ctx.memory(0); + + let base_dir = wasi_try!(state.fs.fd_map.get(&fd).ok_or(__WASI_EBADF)); + let path_str = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); + let _result = wasi_try!(std::fs::remove_file(path_str).ok().ok_or(__WASI_EIO)); + + __WASI_ESUCCESS } + pub fn poll_oneoff( ctx: &mut Ctx, in_: WasmPtr<__wasi_subscription_t, Array>, From dd1ddea37bec589bd8959f22264f614fb1d6337f Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Mon, 15 Jul 2019 17:37:11 -0700 Subject: [PATCH 03/11] wip fs improvements --- lib/wasi/src/lib.rs | 2 - lib/wasi/src/state.rs | 199 +++++++++++++++++++++++++++++------ lib/wasi/src/syscalls/mod.rs | 177 ++++++++++--------------------- 3 files changed, 218 insertions(+), 160 deletions(-) diff --git a/lib/wasi/src/lib.rs b/lib/wasi/src/lib.rs index 2ebf294b31e..51371480fd9 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 bcb34d333ec..3eda2e88853 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()?; - - 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::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() + }) + } 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 8151a8c2248..c03f8d96bc7 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 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))); + 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)); - 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; - } - }; + let buf_cell = wasi_try!(buf.deref(memory)); 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)); - 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; + 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 inode = wasi_try!(state.fs.get_inode_at_path(dir_fd, path_str)); + + 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 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 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); + 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 } From cea7d5da7dff017987db9fffe6e0b058a5b5e775 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Tue, 16 Jul 2019 13:49:45 -0700 Subject: [PATCH 04/11] add readlink test --- lib/wasi-tests/tests/wasitests/envvar.rs | 2 +- lib/wasi-tests/tests/wasitests/mapdir.rs | 5 +++- lib/wasi-tests/tests/wasitests/mod.rs | 1 + lib/wasi-tests/tests/wasitests/readlink.rs | 10 +++++++ lib/wasi-tests/wasitests/mapdir.out | 13 +++++---- lib/wasi-tests/wasitests/readlink.out | 4 +++ lib/wasi-tests/wasitests/readlink.rs | 27 ++++++++++++++++++ lib/wasi-tests/wasitests/readlink.wasm | Bin 0 -> 83071 bytes .../test_fs/hamlet/bookmarks/2019-07-16 | 1 + 9 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 lib/wasi-tests/tests/wasitests/readlink.rs create mode 100644 lib/wasi-tests/wasitests/readlink.out create mode 100644 lib/wasi-tests/wasitests/readlink.rs create mode 100755 lib/wasi-tests/wasitests/readlink.wasm create mode 120000 lib/wasi-tests/wasitests/test_fs/hamlet/bookmarks/2019-07-16 diff --git a/lib/wasi-tests/tests/wasitests/envvar.rs b/lib/wasi-tests/tests/wasitests/envvar.rs index e54e58f40af..f32c6c36e35 100644 --- a/lib/wasi-tests/tests/wasitests/envvar.rs +++ b/lib/wasi-tests/tests/wasitests/envvar.rs @@ -4,7 +4,7 @@ fn test_envvar() { "../../wasitests/envvar.wasm", "envvar", vec![], - vec![], + vec!["DOG=1".to_string(), "CAT=2".to_string(),], "../../wasitests/envvar.out" ); } diff --git a/lib/wasi-tests/tests/wasitests/mapdir.rs b/lib/wasi-tests/tests/wasitests/mapdir.rs index beddf10c813..efb99c7a4d6 100644 --- a/lib/wasi-tests/tests/wasitests/mapdir.rs +++ b/lib/wasi-tests/tests/wasitests/mapdir.rs @@ -3,7 +3,10 @@ fn test_mapdir() { assert_wasi_output!( "../../wasitests/mapdir.wasm", "mapdir", - vec![], + vec![( + ".".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet") + ),], vec![], "../../wasitests/mapdir.out" ); diff --git a/lib/wasi-tests/tests/wasitests/mod.rs b/lib/wasi-tests/tests/wasitests/mod.rs index 362573068f9..c5e5ed802cf 100644 --- a/lib/wasi-tests/tests/wasitests/mod.rs +++ b/lib/wasi-tests/tests/wasitests/mod.rs @@ -12,3 +12,4 @@ mod fs_sandbox_test; mod hello; mod mapdir; mod quine; +mod readlink; diff --git a/lib/wasi-tests/tests/wasitests/readlink.rs b/lib/wasi-tests/tests/wasitests/readlink.rs new file mode 100644 index 00000000000..0c8df676128 --- /dev/null +++ b/lib/wasi-tests/tests/wasitests/readlink.rs @@ -0,0 +1,10 @@ +#[test] +fn test_readlink() { + assert_wasi_output!( + "../../wasitests/readlink.wasm", + "readlink", + vec![(".".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet")),], + vec![], + "../../wasitests/readlink.out" + ); +} diff --git a/lib/wasi-tests/wasitests/mapdir.out b/lib/wasi-tests/wasitests/mapdir.out index baa3809d212..8f5d6ad4449 100644 --- a/lib/wasi-tests/wasitests/mapdir.out +++ b/lib/wasi-tests/wasitests/mapdir.out @@ -1,6 +1,7 @@ -"wasitests/test_fs/hamlet/README.md" -"wasitests/test_fs/hamlet/act1" -"wasitests/test_fs/hamlet/act2" -"wasitests/test_fs/hamlet/act3" -"wasitests/test_fs/hamlet/act4" -"wasitests/test_fs/hamlet/act5" +"./README.md" +"./act1" +"./act2" +"./act3" +"./act4" +"./act5" +"./bookmarks" diff --git a/lib/wasi-tests/wasitests/readlink.out b/lib/wasi-tests/wasitests/readlink.out new file mode 100644 index 00000000000..547b33b18d2 --- /dev/null +++ b/lib/wasi-tests/wasitests/readlink.out @@ -0,0 +1,4 @@ +../act1/scene2.txt +SCENE II. A room of state in the castle. + + Enter KING CLAUDIUS, QUEEN GERTRUDE, HAMLET, POLONIUS, LAERTES, VOLTIMAND, CORNELI diff --git a/lib/wasi-tests/wasitests/readlink.rs b/lib/wasi-tests/wasitests/readlink.rs new file mode 100644 index 00000000000..867659dab49 --- /dev/null +++ b/lib/wasi-tests/wasitests/readlink.rs @@ -0,0 +1,27 @@ +// Args: +// mapdir: .:wasitests/test_fs/hamlet +use std::io::Read; + +fn main() { + #[cfg(not(target_os = "wasi"))] + let cur_dir = std::env::current_dir().unwrap(); + #[cfg(not(target_os = "wasi"))] + std::env::set_current_dir("wasitests/test_fs/hamlet").unwrap(); + + let sym_link_path = "bookmarks/2019-07-16"; + + let link_path = std::fs::read_link(sym_link_path).unwrap(); + println!("{}", link_path.to_string_lossy()); + + let mut some_contents = std::fs::File::open(sym_link_path).unwrap(); + + let mut buffer = [0; 128]; + + assert_eq!(some_contents.read(&mut buffer).unwrap(), 128); + let str_val = std::str::from_utf8(&buffer[..]).unwrap(); + println!("{}", str_val); + + // return to the current directory + #[cfg(not(target_os = "wasi"))] + std::env::set_current_dir(cur_dir).unwrap(); +} diff --git a/lib/wasi-tests/wasitests/readlink.wasm b/lib/wasi-tests/wasitests/readlink.wasm new file mode 100755 index 0000000000000000000000000000000000000000..3fe0fffdd4de82faa9141c3577d75b379a4efb9b GIT binary patch literal 83071 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 E0*XgLq5uE@ literal 0 HcmV?d00001 diff --git a/lib/wasi-tests/wasitests/test_fs/hamlet/bookmarks/2019-07-16 b/lib/wasi-tests/wasitests/test_fs/hamlet/bookmarks/2019-07-16 new file mode 120000 index 00000000000..b7541e40ff7 --- /dev/null +++ b/lib/wasi-tests/wasitests/test_fs/hamlet/bookmarks/2019-07-16 @@ -0,0 +1 @@ +../act1/scene2.txt \ No newline at end of file From 65bc9a52036f7a3f542ffbca5b3279c992f7dbd8 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Wed, 17 Jul 2019 14:00:51 -0700 Subject: [PATCH 05/11] massive fs rewrite; add virtual root, tests --- Makefile | 4 + lib/wasi-tests/build/wasitests.rs | 21 +- lib/wasi-tests/tests/wasitests/_common.rs | 4 +- lib/wasi-tests/tests/wasitests/create_dir.rs | 1 + lib/wasi-tests/tests/wasitests/envvar.rs | 3 +- .../tests/wasitests/file_metadata.rs | 1 + .../tests/wasitests/fs_sandbox_test.rs | 1 + lib/wasi-tests/tests/wasitests/fseek.rs | 6 +- lib/wasi-tests/tests/wasitests/hello.rs | 1 + lib/wasi-tests/tests/wasitests/mapdir.rs | 6 +- lib/wasi-tests/tests/wasitests/mod.rs | 1 + lib/wasi-tests/tests/wasitests/quine.rs | 1 + lib/wasi-tests/tests/wasitests/readlink.rs | 1 + .../tests/wasitests/wasi_sees_virtual_root.rs | 11 + lib/wasi-tests/wasitests/file_metadata.out | 2 +- lib/wasi-tests/wasitests/file_metadata.rs | 3 + lib/wasi-tests/wasitests/quine.out | 3 + lib/wasi-tests/wasitests/quine.rs | 3 + .../wasitests/wasi_sees_virtual_root.out | 12 + .../wasitests/wasi_sees_virtual_root.rs | 51 ++ .../wasitests/wasi_sees_virtual_root.wasm | Bin 0 -> 84930 bytes lib/wasi/src/state.rs | 390 ++++++++------ lib/wasi/src/syscalls/mod.rs | 475 +++++++----------- 23 files changed, 530 insertions(+), 471 deletions(-) create mode 100644 lib/wasi-tests/tests/wasitests/wasi_sees_virtual_root.rs create mode 100644 lib/wasi-tests/wasitests/wasi_sees_virtual_root.out create mode 100644 lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs create mode 100755 lib/wasi-tests/wasitests/wasi_sees_virtual_root.wasm diff --git a/Makefile b/Makefile index 0b518fe2d9f..598484b400f 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ generate-emtests: generate-wasitests: WASM_WASI_GENERATE_WASITESTS=1 cargo build -p wasmer-wasi-tests --release -vv +spectests-generate: generate-spectests +emtests-generate: generate-emtests +wasitests-generate: generate-wasitests + generate: generate-spectests generate-emtests generate-wasitests diff --git a/lib/wasi-tests/build/wasitests.rs b/lib/wasi-tests/build/wasitests.rs index 189ab9c1799..8c8b997fc63 100644 --- a/lib/wasi-tests/build/wasitests.rs +++ b/lib/wasi-tests/build/wasitests.rs @@ -89,7 +89,7 @@ pub fn compile(file: &str, ignores: &HashSet) -> Option { }; let src_code = fs::read_to_string(file).expect("read src file"); - let args = extract_args_from_source_file(&src_code).unwrap_or_default(); + let args: Args = extract_args_from_source_file(&src_code).unwrap_or_default(); let mapdir_args = { let mut out_str = String::new(); @@ -116,12 +116,25 @@ pub fn compile(file: &str, ignores: &HashSet) -> Option { out_str }; + let po_args = { + let mut out_str = String::new(); + out_str.push_str("vec!["); + + for entry in args.po_dirs { + out_str.push_str(&format!("\"{}\".to_string(),", entry)); + } + + out_str.push_str("]"); + out_str + }; + let contents = format!( "#[test]{ignore} fn test_{rs_module_name}() {{ assert_wasi_output!( \"../../{module_path}\", \"{rs_module_name}\", + {po_args}, {mapdir_args}, {envvar_args}, \"../../{test_output_path}\" @@ -132,6 +145,7 @@ fn test_{rs_module_name}() {{ module_path = wasm_out_name, rs_module_name = rs_module_name, test_output_path = format!("{}.out", normalized_name), + po_args = po_args, mapdir_args = mapdir_args, envvar_args = envvar_args ); @@ -192,6 +206,8 @@ fn read_ignore_list() -> HashSet { struct Args { pub mapdir: Vec<(String, String)>, pub envvars: Vec<(String, String)>, + /// pre-opened directories + pub po_dirs: Vec, } /// Pulls args to the program out of a comment at the top of the file starting with "// Args:" @@ -237,6 +253,9 @@ fn extract_args_from_source_file(source_code: &str) -> Option { eprintln!("Parse error in env {} not parsed correctly", &tokenized[1]); } } + "dir" => { + args.po_dirs.push(tokenized[1].to_string()); + } e => { eprintln!("WARN: comment arg: {} is not supported", e); } diff --git a/lib/wasi-tests/tests/wasitests/_common.rs b/lib/wasi-tests/tests/wasitests/_common.rs index b22af8989ec..0850d1d0a49 100644 --- a/lib/wasi-tests/tests/wasitests/_common.rs +++ b/lib/wasi-tests/tests/wasitests/_common.rs @@ -1,5 +1,5 @@ macro_rules! assert_wasi_output { - ($file:expr, $name:expr, $mapdir_args:expr, $envvar_args:expr, $expected:expr) => {{ + ($file:expr, $name:expr, $po_dir_args: expr, $mapdir_args:expr, $envvar_args:expr, $expected:expr) => {{ use wasmer_dev_utils::stdio::StdioCapturer; use wasmer_runtime_core::{backend::Compiler, Func}; use wasmer_wasi::generate_import_object; @@ -34,7 +34,7 @@ macro_rules! assert_wasi_output { .expect("WASM can't be compiled"); let import_object = - generate_import_object(vec![], vec![], vec![".".to_string()], $mapdir_args); + generate_import_object(vec![], vec![], $po_dir_args, $mapdir_args); let instance = module .instantiate(&import_object) diff --git a/lib/wasi-tests/tests/wasitests/create_dir.rs b/lib/wasi-tests/tests/wasitests/create_dir.rs index 87a73dc2b4c..42696b54544 100644 --- a/lib/wasi-tests/tests/wasitests/create_dir.rs +++ b/lib/wasi-tests/tests/wasitests/create_dir.rs @@ -6,6 +6,7 @@ fn test_create_dir() { "create_dir", vec![], vec![], + vec![], "../../wasitests/create_dir.out" ); } diff --git a/lib/wasi-tests/tests/wasitests/envvar.rs b/lib/wasi-tests/tests/wasitests/envvar.rs index f32c6c36e35..caaa76670c0 100644 --- a/lib/wasi-tests/tests/wasitests/envvar.rs +++ b/lib/wasi-tests/tests/wasitests/envvar.rs @@ -4,7 +4,8 @@ fn test_envvar() { "../../wasitests/envvar.wasm", "envvar", vec![], - vec!["DOG=1".to_string(), "CAT=2".to_string(),], + vec![], + vec!["DOG=1".to_string(),"CAT=2".to_string(),], "../../wasitests/envvar.out" ); } diff --git a/lib/wasi-tests/tests/wasitests/file_metadata.rs b/lib/wasi-tests/tests/wasitests/file_metadata.rs index f0b0b33aabf..67d3a8611b1 100644 --- a/lib/wasi-tests/tests/wasitests/file_metadata.rs +++ b/lib/wasi-tests/tests/wasitests/file_metadata.rs @@ -3,6 +3,7 @@ fn test_file_metadata() { assert_wasi_output!( "../../wasitests/file_metadata.wasm", "file_metadata", + vec![".".to_string(),], vec![], vec![], "../../wasitests/file_metadata.out" diff --git a/lib/wasi-tests/tests/wasitests/fs_sandbox_test.rs b/lib/wasi-tests/tests/wasitests/fs_sandbox_test.rs index 6a9c82efa0b..25fcfa1fa70 100644 --- a/lib/wasi-tests/tests/wasitests/fs_sandbox_test.rs +++ b/lib/wasi-tests/tests/wasitests/fs_sandbox_test.rs @@ -5,6 +5,7 @@ fn test_fs_sandbox_test() { "fs_sandbox_test", vec![], vec![], + vec![], "../../wasitests/fs_sandbox_test.out" ); } diff --git a/lib/wasi-tests/tests/wasitests/fseek.rs b/lib/wasi-tests/tests/wasitests/fseek.rs index e48dd5814d9..43bbf21a7a9 100644 --- a/lib/wasi-tests/tests/wasitests/fseek.rs +++ b/lib/wasi-tests/tests/wasitests/fseek.rs @@ -3,10 +3,8 @@ fn test_fseek() { assert_wasi_output!( "../../wasitests/fseek.wasm", "fseek", - vec![( - ".".to_string(), - ::std::path::PathBuf::from("wasitests/test_fs/hamlet") - ),], + vec![], + vec![(".".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet")),], vec![], "../../wasitests/fseek.out" ); diff --git a/lib/wasi-tests/tests/wasitests/hello.rs b/lib/wasi-tests/tests/wasitests/hello.rs index ec0c0a3519e..8cf9ebe4903 100644 --- a/lib/wasi-tests/tests/wasitests/hello.rs +++ b/lib/wasi-tests/tests/wasitests/hello.rs @@ -5,6 +5,7 @@ fn test_hello() { "hello", vec![], vec![], + vec![], "../../wasitests/hello.out" ); } diff --git a/lib/wasi-tests/tests/wasitests/mapdir.rs b/lib/wasi-tests/tests/wasitests/mapdir.rs index efb99c7a4d6..407d39f75b1 100644 --- a/lib/wasi-tests/tests/wasitests/mapdir.rs +++ b/lib/wasi-tests/tests/wasitests/mapdir.rs @@ -3,10 +3,8 @@ fn test_mapdir() { assert_wasi_output!( "../../wasitests/mapdir.wasm", "mapdir", - vec![( - ".".to_string(), - ::std::path::PathBuf::from("wasitests/test_fs/hamlet") - ),], + vec![], + vec![(".".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet")),], vec![], "../../wasitests/mapdir.out" ); diff --git a/lib/wasi-tests/tests/wasitests/mod.rs b/lib/wasi-tests/tests/wasitests/mod.rs index fc53db36aab..7c05db044b3 100644 --- a/lib/wasi-tests/tests/wasitests/mod.rs +++ b/lib/wasi-tests/tests/wasitests/mod.rs @@ -14,3 +14,4 @@ mod hello; mod mapdir; mod quine; mod readlink; +mod wasi_sees_virtual_root; diff --git a/lib/wasi-tests/tests/wasitests/quine.rs b/lib/wasi-tests/tests/wasitests/quine.rs index 193b2c19e4b..b8ab50ce7bc 100644 --- a/lib/wasi-tests/tests/wasitests/quine.rs +++ b/lib/wasi-tests/tests/wasitests/quine.rs @@ -3,6 +3,7 @@ fn test_quine() { assert_wasi_output!( "../../wasitests/quine.wasm", "quine", + vec![".".to_string(),], vec![], vec![], "../../wasitests/quine.out" diff --git a/lib/wasi-tests/tests/wasitests/readlink.rs b/lib/wasi-tests/tests/wasitests/readlink.rs index 0c8df676128..9c86e36667c 100644 --- a/lib/wasi-tests/tests/wasitests/readlink.rs +++ b/lib/wasi-tests/tests/wasitests/readlink.rs @@ -3,6 +3,7 @@ fn test_readlink() { assert_wasi_output!( "../../wasitests/readlink.wasm", "readlink", + vec![], vec![(".".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet")),], vec![], "../../wasitests/readlink.out" diff --git a/lib/wasi-tests/tests/wasitests/wasi_sees_virtual_root.rs b/lib/wasi-tests/tests/wasitests/wasi_sees_virtual_root.rs new file mode 100644 index 00000000000..be60d2b3e16 --- /dev/null +++ b/lib/wasi-tests/tests/wasitests/wasi_sees_virtual_root.rs @@ -0,0 +1,11 @@ +#[test] +fn test_wasi_sees_virtual_root() { + assert_wasi_output!( + "../../wasitests/wasi_sees_virtual_root.wasm", + "wasi_sees_virtual_root", + vec![], + vec![("act1".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act1")),("act2".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act2")),("act1-again".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act1")),], + vec![], + "../../wasitests/wasi_sees_virtual_root.out" + ); +} diff --git a/lib/wasi-tests/wasitests/file_metadata.out b/lib/wasi-tests/wasitests/file_metadata.out index 7250bc00f3a..faa15dc2901 100644 --- a/lib/wasi-tests/wasitests/file_metadata.out +++ b/lib/wasi-tests/wasitests/file_metadata.out @@ -1,3 +1,3 @@ is dir: false filetype: false true false -file info: 456 +file info: 476 diff --git a/lib/wasi-tests/wasitests/file_metadata.rs b/lib/wasi-tests/wasitests/file_metadata.rs index 630382a0113..67c3999ccba 100644 --- a/lib/wasi-tests/wasitests/file_metadata.rs +++ b/lib/wasi-tests/wasitests/file_metadata.rs @@ -1,3 +1,6 @@ +// Args: +// dir: . + use std::fs; use std::io::Read; diff --git a/lib/wasi-tests/wasitests/quine.out b/lib/wasi-tests/wasitests/quine.out index 13de73d0540..8edcd3d8e0e 100644 --- a/lib/wasi-tests/wasitests/quine.out +++ b/lib/wasi-tests/wasitests/quine.out @@ -1,3 +1,6 @@ +// Args: +// dir: . + use std::fs; use std::io::Read; diff --git a/lib/wasi-tests/wasitests/quine.rs b/lib/wasi-tests/wasitests/quine.rs index 6ca65ee8c4e..0d338d39c61 100644 --- a/lib/wasi-tests/wasitests/quine.rs +++ b/lib/wasi-tests/wasitests/quine.rs @@ -1,3 +1,6 @@ +// Args: +// dir: . + use std::fs; use std::io::Read; diff --git a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out new file mode 100644 index 00000000000..6790f63280c --- /dev/null +++ b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out @@ -0,0 +1,12 @@ +/act1 +act1-again +/act2 +/act1 +act1-again +/act2 +/act1 +act1-again +/act2 +/act1 +act1-again +/act2 diff --git a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs new file mode 100644 index 00000000000..9b20f5f046c --- /dev/null +++ b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs @@ -0,0 +1,51 @@ +// Args: +// mapdir: act1:wasitests/test_fs/hamlet/act1 +// mapdir: act2:wasitests/test_fs/hamlet/act2 +// mapdir: act1-again:wasitests/test_fs/hamlet/act1 + +use std::fs; + +fn main() { + // just cheat in this test because there is no comparison for native + #[cfg(not(target_os = "wasi"))] + let results = { + let start = vec!["/act1", "act1-again", "/act2"]; + + let mut out = vec![]; + for _ in 0..4 { + for path_str in &start { + out.push(path_str.to_string()); + } + } + + out + }; + + #[cfg(target_os = "wasi")] + let results = { + let mut out = vec![]; + + let read_dir = fs::read_dir("/").unwrap(); + for entry in read_dir { + out.push(format!("{:?}", entry.unwrap().path())) + } + let read_dir = fs::read_dir("act1/..").unwrap(); + for entry in read_dir { + out.push(format!("{:?}", entry.unwrap().path())) + } + let read_dir = fs::read_dir("act1/../../..").unwrap(); + for entry in read_dir { + out.push(format!("{:?}", entry.unwrap().path())) + } + let read_dir = fs::read_dir("act1/../../act2/../act1/../../../").unwrap(); + for entry in read_dir { + out.push(format!("{:?}", entry.unwrap().path())) + } + + out + }; + + for result in results { + println!("{}", result); + } +} diff --git a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.wasm b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.wasm new file mode 100755 index 0000000000000000000000000000000000000000..b9e42f55599c7075189b3b8a5f1b9dd0f6786614 GIT binary patch literal 84930 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 literal 0 HcmV?d00001 diff --git a/lib/wasi/src/state.rs b/lib/wasi/src/state.rs index 3eda2e88853..89edee9976c 100644 --- a/lib/wasi/src/state.rs +++ b/lib/wasi/src/state.rs @@ -3,9 +3,11 @@ // file_like::{FileLike, Metadata}; // }; use crate::syscalls::types::*; -use generational_arena::{Arena, Index as Inode}; +use generational_arena::Arena; +pub use generational_arena::Index as Inode; use hashbrown::hash_map::{Entry, HashMap}; use std::{ + borrow::Borrow, cell::Cell, fs, io::{self, Read, Seek, Write}, @@ -14,7 +16,7 @@ use std::{ }; use wasmer_runtime_core::debug; -pub const MAX_SYMLINKS: usize = 100; +pub const MAX_SYMLINKS: u32 = 128; #[derive(Debug)] pub enum WasiFile { @@ -90,49 +92,6 @@ pub struct InodeVal { pub kind: Kind, } -impl InodeVal { - // TODO: clean this up - pub fn from_file_metadata( - metadata: &std::fs::Metadata, - name: String, - is_preopened: bool, - kind: Kind, - ) -> Self { - InodeVal { - stat: __wasi_filestat_t { - st_filetype: if metadata.is_dir() { - __WASI_FILETYPE_DIRECTORY - } else { - __WASI_FILETYPE_REGULAR_FILE - }, - st_size: metadata.len(), - st_atim: metadata - .accessed() - .ok() - .and_then(|sys_time| sys_time.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|duration| duration.as_nanos() as u64) - .unwrap_or(0), - st_ctim: metadata - .created() - .ok() - .and_then(|sys_time| sys_time.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|duration| duration.as_nanos() as u64) - .unwrap_or(0), - st_mtim: metadata - .modified() - .ok() - .and_then(|sys_time| sys_time.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|duration| duration.as_nanos() as u64) - .unwrap_or(0), - ..__wasi_filestat_t::default() - }, - is_preopened, - name, - kind, - } - } -} - #[allow(dead_code)] #[derive(Debug)] pub enum Kind { @@ -151,10 +110,18 @@ pub enum Kind { /// The entries of a directory are lazily filled. entries: HashMap, }, + /// The same as Dir but without the irrelevant bits + /// The root is immutable after creation; generally the Kind::Root + /// branch of whatever code you're writing will be a simpler version of + /// your Kind::Dir logic + Root { + entries: HashMap, + }, Symlink { - forwarded: Option, - /// This is required because, at the very least, symlinks can be deleted and we'll need to check that - path: PathBuf, + /// The preopened dir that this symlink is relative to + base_po_dir: __wasi_fd_t, + /// the relative path from theroot of the preopened dir + relative_path: PathBuf, }, Buffer { buffer: Vec, @@ -173,11 +140,12 @@ pub struct Fd { #[derive(Debug)] pub struct WasiFs { //pub repo: Repo, + pub preopen_fds: Vec, pub name_map: HashMap, pub inodes: Arena, pub fd_map: HashMap, pub next_fd: Cell, - pub inode_counter: Cell, + inode_counter: Cell, } impl WasiFs { @@ -188,12 +156,24 @@ impl WasiFs { debug!("wasi::fs::inodes"); let inodes = Arena::new(); let mut wasi_fs = Self { + preopen_fds: vec![], name_map: HashMap::new(), - inodes: inodes, + inodes, fd_map: HashMap::new(), next_fd: Cell::new(3), - inode_counter: Cell::new(1000), + inode_counter: Cell::new(1024), + }; + // create virtual root + let root_inode = { + let default_rights = 0x1FFFFFFF; // all rights + let inode = wasi_fs.create_virtual_root(); + let fd = wasi_fs + .create_fd(default_rights, default_rights, 0, inode) + .expect("Could not create root fd"); + wasi_fs.preopen_fds.push(fd); + inode }; + debug!("wasi::fs::preopen_dirs"); for dir in preopened_dirs { debug!("Attempting to preopen {}", &dir); @@ -203,7 +183,7 @@ impl WasiFs { let cur_dir_metadata = cur_dir.metadata().expect("Could not find directory"); let kind = if cur_dir_metadata.is_dir() { Kind::Dir { - parent: None, + parent: Some(root_inode), path: cur_dir.clone(), entries: Default::default(), } @@ -214,14 +194,22 @@ impl WasiFs { )); }; // TODO: handle nested pats in `file` - let inode_val = - InodeVal::from_file_metadata(&cur_dir_metadata, dir.clone(), true, kind); - - let inode = wasi_fs.inodes.insert(inode_val); - wasi_fs.inodes[inode].stat.st_ino = wasi_fs.inode_counter.get(); - wasi_fs + let inode = wasi_fs + .create_inode(kind, true, dir.to_string()) + .map_err(|e| { + format!( + "Failed to create inode for preopened dir: WASI error code: {}", + e + ) + })?; + let fd = wasi_fs .create_fd(default_rights, default_rights, 0, inode) .expect("Could not open fd"); + if let Kind::Root { entries } = &mut wasi_fs.inodes[root_inode].kind { + // todo handle collisions + assert!(entries.insert(dbg!(dir.to_string()), inode).is_none()) + } + wasi_fs.preopen_fds.push(fd); } debug!("wasi::fs::mapped_dirs"); for (alias, real_dir) in mapped_dirs { @@ -233,7 +221,7 @@ impl WasiFs { .expect("mapped dir not at previously verified location"); let kind = if cur_dir_metadata.is_dir() { Kind::Dir { - parent: None, + parent: Some(root_inode), path: real_dir.clone(), entries: Default::default(), } @@ -244,19 +232,34 @@ impl WasiFs { )); }; // TODO: handle nested pats in `file` - let inode_val = - InodeVal::from_file_metadata(&cur_dir_metadata, alias.clone(), true, kind); - - let inode = wasi_fs.inodes.insert(inode_val); - wasi_fs.inodes[inode].stat.st_ino = wasi_fs.inode_counter.get(); - wasi_fs + let inode = wasi_fs + .create_inode(kind, true, alias.clone()) + .map_err(|e| { + format!( + "Failed to create inode for preopened dir: WASI error code: {}", + e + ) + })?; + let fd = wasi_fs .create_fd(default_rights, default_rights, 0, inode) .expect("Could not open fd"); + if let Kind::Root { entries } = &mut wasi_fs.inodes[root_inode].kind { + // todo handle collisions + assert!(dbg!(entries.insert(dbg!(alias.clone()), inode)).is_none()); + } + wasi_fs.preopen_fds.push(fd); } + debug!("wasi::fs::end"); Ok(wasi_fs) } + fn get_next_inode_index(&mut self) -> u64 { + let next = self.inode_counter.get(); + self.inode_counter.set(next + 1); + next + } + #[allow(dead_code)] fn get_inode(&mut self, path: &str) -> Option { Some(match self.name_map.entry(path.to_string()) { @@ -369,17 +372,19 @@ impl WasiFs { } */ - /// gets a host file from a base directory and a path - /// this function ensures the fs remains sandboxed - pub fn get_inode_at_path( + fn get_inode_at_path_inner( &mut self, base: __wasi_fd_t, path: &str, + symlink_count: u32, ) -> Result { + if symlink_count > MAX_SYMLINKS { + return Err(__WASI_EMLINK); + } + 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() { @@ -399,7 +404,6 @@ impl WasiFs { cur_inode = *p; continue 'path_iter; } else { - // TODO: be smart here with multiple preopened directories return Err(__WASI_EACCES); } } @@ -431,41 +435,55 @@ impl WasiFs { path: file.clone(), } } else if file_type.is_symlink() { - // use a stack and load symlinks? - // load symlink + let link_value = file.read_link().ok().ok_or(__WASI_EIO)?; + debug!("attempting to decompose path {:?}", link_value); + let (pre_open_dir_fd, relative_path) = + self.path_into_pre_open_and_relative_path(&link_value)?; Kind::Symlink { - forwarded: None, - path: file.clone(), + base_po_dir: pre_open_dir_fd, + relative_path: relative_path, } } 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 = + self.create_inode(kind, false, file.to_string_lossy().to_string())?; + } + } + Kind::Root { entries } => { + match component.as_os_str().to_string_lossy().borrow() { + // the root's parent is the root + ".." => continue 'path_iter, + // the root's current directory is the root + "." => continue 'path_iter, + _ => (), + } - cur_inode = new_inode; + if let Some(entry) = + entries.get(component.as_os_str().to_string_lossy().as_ref()) + { + cur_inode = *entry; + } else { + return Err(__WASI_EINVAL); } } 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; + Kind::Symlink { + base_po_dir, + relative_path, + } => { + let new_base_dir = *base_po_dir; + // allocate to reborrow mutabily to recur + let new_path = relative_path.to_string_lossy().to_string(); + let symlink_inode = self.get_inode_at_path_inner( + new_base_dir, + &new_path, + symlink_count + 1, + )?; + cur_inode = symlink_inode; continue 'symlink_resolution; } } @@ -476,6 +494,40 @@ impl WasiFs { Ok(cur_inode) } + fn path_into_pre_open_and_relative_path( + &self, + path: &Path, + ) -> Result<(__wasi_fd_t, PathBuf), __wasi_errno_t> { + // for each preopened directory + for po_fd in &self.preopen_fds { + let po_inode = self.fd_map[po_fd].inode; + let po_path = match &self.inodes[po_inode].kind { + Kind::Dir { path, .. } => &**path, + Kind::Root { .. } => Path::new("/"), + _ => unreachable!("Preopened FD that's not a directory or the root"), + }; + // stem path based on it + if let Ok(rest) = path.strip_prefix(po_path) { + // if any path meets this criteria + // (verify that all remaining components are not symlinks except for maybe last? (or do the more complex logic of resolving intermediary symlinks)) + // return preopened dir and the rest of the path + + return Ok((*po_fd, rest.to_owned())); + } + } + Err(__WASI_EINVAL) // this may not make sense + } + + /// 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 { + self.get_inode_at_path_inner(base, path, 0) + } + pub fn get_fd(&self, fd: __wasi_fd_t) -> Result<&Fd, __wasi_errno_t> { self.fd_map.get(&fd).ok_or(__WASI_EBADF) } @@ -553,6 +605,24 @@ impl WasiFs { Ok(()) } + /// Creates an inode and inserts it given a Kind and some extra data + pub fn create_inode( + &mut self, + kind: Kind, + is_preopened: bool, + name: String, + ) -> Result { + let mut stat = self.get_stat_for_kind(&kind).ok_or(__WASI_EIO)?; + stat.st_ino = self.get_next_inode_index(); + + Ok(self.inodes.insert(InodeVal { + stat: stat, + is_preopened, + name, + kind, + })) + } + pub fn create_fd( &mut self, rights: __wasi_rights_t, @@ -581,6 +651,78 @@ impl WasiFs { } None } + + fn create_virtual_root(&mut self) -> Inode { + let stat = __wasi_filestat_t { + st_filetype: __WASI_FILETYPE_DIRECTORY, + st_ino: self.get_next_inode_index(), + ..__wasi_filestat_t::default() + }; + let root_kind = Kind::Root { + entries: HashMap::new(), + }; + + self.inodes.insert(InodeVal { + stat: stat, + is_preopened: true, + name: "/".to_string(), + kind: root_kind, + }) + } + + pub fn get_stat_for_kind(&self, kind: &Kind) -> Option<__wasi_filestat_t> { + let md = match kind { + Kind::File { handle, path } => match handle { + Some(WasiFile::HostFile(hf)) => hf.metadata().ok()?, + None => path.metadata().ok()?, + }, + Kind::Dir { path, .. } => path.metadata().ok()?, + Kind::Symlink { + base_po_dir, + relative_path, + } => { + let base_po_inode = &self.fd_map[base_po_dir].inode; + let base_po_inode_v = &self.inodes[*base_po_inode]; + dbg!(&base_po_inode_v.name); + if let Kind::Dir { path, .. } = &base_po_inode_v.kind { + let mut real_path = path.clone(); + // PHASE 1: ignore all possible symlinks in `relative_path` + // TODO: walk the segments of `relative_path` via the entries of the Dir + // use helper function to avoid duplicating this logic (walking this will require + // &self to be &mut sel + real_path.push(relative_path); + real_path.metadata().ok()? + } else { + // if this triggers, there's a bug in the symlink code + unreachable!("Symlink pointing to something that's not a directory as its base preopened directory"); + } + } + __ => return None, + }; + 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() + }) + } } #[derive(Debug)] @@ -602,65 +744,3 @@ pub fn host_file_type_to_wasi_file_type(file_type: fs::FileType) -> __wasi_filet __WASI_FILETYPE_UNKNOWN } } - -pub fn get_stat_for_kind(kind: &Kind) -> Option<__wasi_filestat_t> { - match kind { - 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() - }) - } - Kind::Dir { path, .. } => { - let md = 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() - }) - } - _ => None, - } -} diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index 368bf327692..c524a19e9db 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -9,8 +9,8 @@ use self::types::*; use crate::{ ptr::{Array, WasmPtr}, state::{ - get_stat_for_kind, host_file_type_to_wasi_file_type, Fd, InodeVal, Kind, WasiFile, - WasiState, MAX_SYMLINKS, + host_file_type_to_wasi_file_type, Fd, Inode, InodeVal, Kind, WasiFile, WasiState, + MAX_SYMLINKS, }, ExitCode, }; @@ -568,28 +568,31 @@ pub fn fd_prestat_dir_name( // check inode-val.is_preopened? - if let Kind::Dir { .. } = inode_val.kind { - // TODO: verify this: null termination, etc - if inode_val.name.len() <= path_len as usize { - let mut i = 0; - for c in inode_val.name.bytes() { - path_chars[i].set(c); - i += 1 - } - path_chars[i].set(0); + match inode_val.kind { + Kind::Dir { .. } | Kind::Root { .. } => { + // TODO: verify this: null termination, etc + if inode_val.name.len() <= path_len as usize { + let mut i = 0; + for c in inode_val.name.bytes() { + path_chars[i].set(c); + i += 1 + } + path_chars[i].set(0); - debug!( - "=> result: \"{}\"", - ::std::str::from_utf8(unsafe { &*(&path_chars[..] as *const [_] as *const [u8]) }) + debug!( + "=> result: \"{}\"", + ::std::str::from_utf8(unsafe { + &*(&path_chars[..] as *const [_] as *const [u8]) + }) .unwrap() - ); + ); - __WASI_ESUCCESS - } else { - __WASI_EOVERFLOW + __WASI_ESUCCESS + } else { + __WASI_EOVERFLOW + } } - } else { - __WASI_ENOTDIR + Kind::Symlink { .. } | Kind::Buffer { .. } | Kind::File { .. } => __WASI_ENOTDIR, } } @@ -656,7 +659,7 @@ pub fn fd_pwrite( return __WASI_EINVAL; } } - Kind::Dir { .. } => { + Kind::Dir { .. } | Kind::Root { .. } => { // TODO: verify return __WASI_EISDIR; } @@ -748,7 +751,7 @@ pub fn fd_read( return __WASI_EINVAL; } } - Kind::Dir { .. } => { + Kind::Dir { .. } | Kind::Root { .. } => { // TODO: verify return __WASI_EISDIR; } @@ -804,54 +807,79 @@ pub fn fd_readdir( let mut cur_cookie = cookie; let mut buf_idx = 0; - if let Kind::Dir { path, .. } = &state.fs.inodes[working_dir.inode].kind { - // we need to support multiple calls, - // simple and obviously correct implementation for now: - // maintain consistent order via lexacographic sorting - let mut entries = wasi_try!(wasi_try!(std::fs::read_dir(path).map_err(|_| __WASI_EIO)) - .collect::, _>>() - .map_err(|_| __WASI_EIO)); - entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); - - for entry in entries.iter().skip(cookie as usize) { - cur_cookie += 1; - let entry_path = entry.path(); - let entry_path = wasi_try!(entry_path.file_name().ok_or(__WASI_EIO)); - let entry_path_str = entry_path.to_string_lossy(); - let namlen = entry_path_str.len(); - debug!("Returning dirent for {}", entry_path_str); - let dirent = __wasi_dirent_t { - d_next: cur_cookie, - d_ino: 0, // TODO: inode - d_namlen: namlen as u32, - d_type: host_file_type_to_wasi_file_type(wasi_try!(entry - .file_type() - .map_err(|_| __WASI_EIO))), + let entries = match &state.fs.inodes[working_dir.inode].kind { + Kind::Dir { path, .. } => { + // TODO: refactor this code + // we need to support multiple calls, + // simple and obviously correct implementation for now: + // maintain consistent order via lexacographic sorting + let mut entries = wasi_try!(wasi_try!(std::fs::read_dir(path).map_err(|_| __WASI_EIO)) + .collect::, _>>() + .map_err(|_| __WASI_EIO)); + entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + wasi_try!(entries + .into_iter() + .map(|entry| Ok(( + entry.file_name().to_string_lossy().to_string(), + host_file_type_to_wasi_file_type(entry.file_type().map_err(|_| __WASI_EIO)?), + 0, // TODO: inode + ))) + .collect::, __wasi_errno_t>>()) + } + Kind::Root { entries } => { + let sorted_entries = { + let mut entry_vec: Vec<(String, Inode)> = + entries.iter().map(|(a, b)| (a.clone(), *b)).collect(); + entry_vec.sort_by(|a, b| a.0.cmp(&b.0)); + entry_vec }; - let dirent_bytes = dirent_to_le_bytes(&dirent); - let upper_limit = std::cmp::min( - buf_len as usize - buf_idx, - std::mem::size_of::<__wasi_dirent_t>(), - ); - for i in 0..upper_limit { - buf_arr_cell[i + buf_idx].set(dirent_bytes[i]); - } - buf_idx += upper_limit; - if upper_limit != std::mem::size_of::<__wasi_dirent_t>() { - break; - } - let upper_limit = std::cmp::min(buf_len as usize - buf_idx, namlen); - for (i, b) in entry_path_str.bytes().take(upper_limit).enumerate() { - buf_arr_cell[i + buf_idx].set(b); - } - buf_idx += upper_limit; - if upper_limit != namlen { - break; - } + sorted_entries + .into_iter() + .map(|(name, inode)| { + let entry = &state.fs.inodes[inode]; + ( + format!("/{}", entry.name), + entry.stat.st_filetype, + entry.stat.st_ino, + ) + }) + .collect() + } + Kind::File { .. } | Kind::Symlink { .. } | Kind::Buffer { .. } => return __WASI_ENOTDIR, + }; + + for (entry_path_str, wasi_file_type, ino) in entries.iter().skip(cookie as usize) { + cur_cookie += 1; + let namlen = entry_path_str.len(); + debug!("Returning dirent for {}", entry_path_str); + let dirent = __wasi_dirent_t { + d_next: cur_cookie, + d_ino: *ino, + d_namlen: namlen as u32, + d_type: *wasi_file_type, + }; + let dirent_bytes = dirent_to_le_bytes(&dirent); + let upper_limit = std::cmp::min( + buf_len as usize - buf_idx, + std::mem::size_of::<__wasi_dirent_t>(), + ); + for i in 0..upper_limit { + buf_arr_cell[i + buf_idx].set(dirent_bytes[i]); + } + buf_idx += upper_limit; + if upper_limit != std::mem::size_of::<__wasi_dirent_t>() { + break; + } + let upper_limit = std::cmp::min(buf_len as usize - buf_idx, namlen); + for (i, b) in entry_path_str.bytes().take(upper_limit).enumerate() { + buf_arr_cell[i + buf_idx].set(b); + } + buf_idx += upper_limit; + if upper_limit != namlen { + break; } - } else { - return __WASI_ENOTDIR; } + bufused_cell.set(buf_idx as u32); __WASI_ESUCCESS } @@ -914,15 +942,19 @@ pub fn fd_seek( __WASI_WHENCE_END => { use std::io::SeekFrom; match state.fs.inodes[fd_entry.inode].kind { - Kind::File { ref mut handle } => { - let end = wasi_try!(handle.seek(SeekFrom::End(0)).ok().ok_or(__WASI_EIO)); - // TODO: handle case if fd_entry.offset uses 64 bits of a u64 - fd_entry.offset = (end as i64 + offset) as u64; + Kind::File { ref mut handle, .. } => { + if let Some(handle) = handle { + let end = wasi_try!(handle.seek(SeekFrom::End(0)).ok().ok_or(__WASI_EIO)); + // TODO: handle case if fd_entry.offset uses 64 bits of a u64 + fd_entry.offset = (end as i64 + offset) as u64; + } else { + return __WASI_EINVAL; + } } Kind::Symlink { .. } => { unimplemented!("wasi::fd_seek not implemented for symlinks") } - Kind::Dir { .. } => { + Kind::Dir { .. } | Kind::Root { .. } => { // TODO: check this return __WASI_EINVAL; } @@ -1048,7 +1080,7 @@ pub fn fd_write( return __WASI_EINVAL; } } - Kind::Dir { .. } => { + Kind::Dir { .. } | Kind::Root { .. } => { // TODO: verify return __WASI_EISDIR; } @@ -1132,7 +1164,7 @@ pub fn path_create_directory( entries: Default::default(), }; let new_inode = state.fs.inodes.insert(InodeVal { - stat: wasi_try!(get_stat_for_kind(&kind).ok_or(__WASI_EIO)), + stat: wasi_try!(state.fs.get_stat_for_kind(&kind).ok_or(__WASI_EIO)), is_preopened: false, name: path_vec[0].clone(), kind, @@ -1186,7 +1218,10 @@ pub fn path_filestat_get( debug!("=> path: {}", &path_string); 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 stat = wasi_try!(state + .fs + .get_stat_for_kind(&state.fs.inodes[file_inode].kind) + .ok_or(__WASI_EIO)); let buf_cell = wasi_try!(buf.deref(memory)); buf_cell.set(stat); @@ -1300,7 +1335,6 @@ pub fn path_open( } let fd_cell = wasi_try!(fd.deref(memory)); - let path_cells = wasi_try!(path.deref(memory, 0, path_len)); let state = get_wasi_state(ctx); // o_flags: @@ -1309,228 +1343,68 @@ pub fn path_open( // - __WASI_O_EXCL (fail if file exists) // - __WASI_O_TRUNC (truncate size to 0) - let working_dir = wasi_try!(state.fs.fd_map.get(&dirfd).ok_or(__WASI_EBADF)); + let working_dir = wasi_try!(state.fs.fd_map.get(&dirfd).ok_or(__WASI_EBADF)).clone(); // ASSUMPTION: open rights apply recursively if !has_rights(working_dir.rights, __WASI_RIGHT_PATH_OPEN) { return __WASI_EACCES; } - let path_string = - wasi_try!( - std::str::from_utf8(unsafe { &*(path_cells as *const [_] as *const [u8]) }) - .map_err(|_| __WASI_EINVAL) - ); + let path_string = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); let path = std::path::PathBuf::from(path_string); - let path_vec = wasi_try!(path - .components() - .map(|comp| { - comp.as_os_str() - .to_str() - .map(|inner_str| inner_str.to_string()) - .ok_or(__WASI_EINVAL) - }) - .collect::, __wasi_errno_t>>()); - debug!("Path vec: {:#?}", path_vec); - - if path_vec.is_empty() { - return __WASI_EINVAL; - } - - let mut cur_dir_inode = working_dir.inode; - let mut cumulative_path = std::path::PathBuf::from(wasi_try!(state - .fs - .get_base_path_for_directory(working_dir.inode) - .ok_or(__WASI_EIO))); - - // traverse path - if path_vec.len() > 1 { - for path_segment in &path_vec[..(path_vec.len() - 1)] { - match &state.fs.inodes[cur_dir_inode].kind { - Kind::Dir { entries, .. } => { - if let Some(child) = entries.get(path_segment) { - cumulative_path.push(path_segment); - let inode_val = *child; - cur_dir_inode = inode_val; - } else { - // attempt to lazily load or create - if path_segment == ".." { - unimplemented!( - "\"..\" in paths in `path_open` has not been implemented yet" - ); - } - // lazily load - cumulative_path.push(path_segment); - let mut open_options = std::fs::OpenOptions::new(); - let open_options = open_options.read(true); - // ASSUMPTION: __WASI_O_CREAT applies recursively - let open_options = if o_flags & __WASI_O_CREAT != 0 { - open_options.create(true) - } else { - open_options - }; - // TODO: handle __WASI_O_TRUNC on directories - - // TODO: refactor and reuse - let cur_file_metadata = - wasi_try!(cumulative_path.metadata().map_err(|_| __WASI_EINVAL)); - let kind = if cur_file_metadata.is_dir() { - Kind::Dir { - parent: Some(cur_dir_inode), - path: cumulative_path.clone(), - entries: Default::default(), - } - } else { - return __WASI_ENOTDIR; - }; - let inode_val = InodeVal::from_file_metadata( - &cur_file_metadata, - path_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); - // reborrow to insert entry - if let Kind::Dir { entries, .. } = &mut state.fs.inodes[cur_dir_inode].kind - { - assert!(entries.insert(path_segment.clone(), new_inode).is_none()); - state.fs.inodes[new_inode].stat.st_ino = state.fs.inode_counter.get(); - cur_dir_inode = new_inode; - } - } + let inode = wasi_try!(state.fs.get_inode_at_path(dirfd, path_string)); + + match &mut state.fs.inodes[inode].kind { + Kind::File { + ref mut handle, + path, + } => { + if o_flags & __WASI_O_DIRECTORY != 0 { + return __WASI_ENOTDIR; + } + if o_flags & __WASI_O_EXCL != 0 { + if path.exists() { + return __WASI_EEXIST; + } + } + let mut open_options = std::fs::OpenOptions::new(); + let open_options = open_options + .read(true) + // TODO: ensure these rights are actually valid given parent, etc. + .write(fs_rights_base & __WASI_RIGHT_FD_WRITE != 0) + .create(o_flags & __WASI_O_CREAT != 0) + .truncate(o_flags & __WASI_O_TRUNC != 0); + + *handle = Some(WasiFile::HostFile(wasi_try!(open_options + .open(&path) + .map_err(|_| __WASI_EIO)))); + } + Kind::Buffer { .. } => unimplemented!("wasi::path_open for Buffer type files"), + Kind::Dir { .. } | Kind::Root { .. } => { + if o_flags & __WASI_O_EXCL != 0 { + if path.exists() { + return __WASI_EEXIST; } - Kind::Symlink { .. } => unimplemented!("Symlinks not yet supported in `path_open`"), - _ => return __WASI_ENOTDIR, } } + Kind::Symlink { .. } => { + // TODO: figure out what to do here + unimplemented!("wasi::path_open on symlink"); + } } - let file_name = path_vec.last().unwrap(); - + // TODO: reimplement all the flag checking logic debug!( - "Looking for file {} in directory {:#?}", - file_name, cumulative_path + "inode {:?} value {:#?} found!", + inode, state.fs.inodes[inode] ); - cumulative_path.push(file_name); - let file_path = cumulative_path; - - let out_fd = if let Kind::Dir { - entries, parent, .. - } = &mut state.fs.inodes[cur_dir_inode].kind - { - // short circuit logic if attempting to get parent - if file_name == ".." { - if let Some(p) = parent { - let parent_inode = *p; - wasi_try!(state.fs.create_fd( - fs_rights_base, - fs_rights_inheriting, - fs_flags, - parent_inode - )) - } else { - return __WASI_EACCES; - } - } else { - if let Some(child) = entries.get(file_name).cloned() { - let child_inode_val = &state.fs.inodes[child]; - // early return based on flags - if o_flags & __WASI_O_EXCL != 0 { - return __WASI_EEXIST; - } - if o_flags & __WASI_O_DIRECTORY != 0 { - match &child_inode_val.kind { - Kind::Dir { .. } => (), - Kind::Symlink { .. } => { - unimplemented!("Symlinks not yet supported in path_open") - } - _ => return __WASI_ENOTDIR, - } - } - // do logic on child - wasi_try!(state - .fs - .create_fd(fs_rights_base, fs_rights_inheriting, fs_flags, child)) - } else { - debug!("Attempting to load file from host system"); - - let file_metadata = file_path.metadata(); - // if entry does not exist in parent directory, try to lazily - // load it; possibly creating or truncating it if flags set - let kind = if file_metadata.is_ok() && file_metadata.unwrap().is_dir() { - // special dir logic - Kind::Dir { - parent: Some(cur_dir_inode), - path: file_path.clone(), - entries: Default::default(), - } - } else { - // file is not a dir - let real_opened_file = { - let mut open_options = std::fs::OpenOptions::new(); - let open_options = open_options.read(true); - let open_options = if fs_rights_base & __WASI_RIGHT_FD_WRITE != 0 { - open_options.write(true) - } else { - open_options - }; - let open_options = if o_flags & __WASI_O_CREAT != 0 { - debug!( - "File {:?} may be created when opened if it does not exist", - &file_path - ); - open_options.create(true) - } else { - open_options - }; - let open_options = if o_flags & __WASI_O_TRUNC != 0 { - debug!("File {:?} will be truncated when opened", &file_path); - open_options.truncate(true) - } else { - open_options - }; - debug!("Opening host file {:?}", &file_path); - let real_open_file = - wasi_try!(open_options.open(&file_path).map_err(|_| __WASI_EIO)); - - real_open_file - }; - Kind::File { - handle: Some(WasiFile::HostFile(real_opened_file)), - path: file_path, - } - }; - - // record lazily loaded or newly created fd - let new_inode = state.fs.inodes.insert(InodeVal { - stat: wasi_try!(get_stat_for_kind(&kind).ok_or(__WASI_EIO)), - is_preopened: false, - name: file_name.clone(), - kind, - }); - - // reborrow to insert entry - if let Kind::Dir { entries, .. } = &mut state.fs.inodes[working_dir.inode].kind { - entries.insert(file_name.clone(), new_inode); - } - let new_fd = wasi_try!(state.fs.create_fd( - fs_rights_base, - fs_rights_inheriting, - fs_flags, - new_inode, - )); - - new_fd - } - } - } else { - // working_dir did not match on Kind::Dir - return __WASI_ENOTDIR; - }; + // TODO: check and reduce these + // TODO: ensure a mutable fd to root can never be opened + let out_fd = + wasi_try!(state + .fs + .create_fd(fs_rights_base, fs_rights_inheriting, fs_flags, inode)); fd_cell.set(out_fd); @@ -1557,28 +1431,23 @@ pub fn path_readlink( let path_str = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); let inode = wasi_try!(state.fs.get_inode_at_path(dir_fd, path_str)); - 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 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 { relative_path, .. } = &state.fs.inodes[inode].kind { + let rel_path_str = relative_path.to_string_lossy(); + let bytes = rel_path_str.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); - } else { - panic!("do this before shipping"); - return __WASI_EINVAL; + 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 { return __WASI_EINVAL; } From e7e1b8c7d340926dea237b144333824b7ce76047 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Wed, 17 Jul 2019 15:32:47 -0700 Subject: [PATCH 06/11] get tests passing! (more tests and clean up required) --- .../wasitests/wasi_sees_virtual_root.out | 24 ++--- .../wasitests/wasi_sees_virtual_root.rs | 2 +- lib/wasi/src/state.rs | 99 ++++++++++++++----- lib/wasi/src/syscalls/mod.rs | 37 +++++-- 4 files changed, 117 insertions(+), 45 deletions(-) diff --git a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out index 6790f63280c..1ce8a9c6a7a 100644 --- a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out +++ b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out @@ -1,12 +1,12 @@ -/act1 -act1-again -/act2 -/act1 -act1-again -/act2 -/act1 -act1-again -/act2 -/act1 -act1-again -/act2 +"/act1" +"/act1-again" +"/act2" +"/act1" +"/act1-again" +"/act2" +"/act1" +"/act1-again" +"/act2" +"/act1" +"/act1-again" +"/act2" diff --git a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs index 9b20f5f046c..f3ebb71fd27 100644 --- a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs +++ b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs @@ -9,7 +9,7 @@ fn main() { // just cheat in this test because there is no comparison for native #[cfg(not(target_os = "wasi"))] let results = { - let start = vec!["/act1", "act1-again", "/act2"]; + let start = vec!["\"/act1\"", "\"/act1-again\"", "\"/act2\""]; let mut out = vec![]; for _ in 0..4 { diff --git a/lib/wasi/src/state.rs b/lib/wasi/src/state.rs index 89edee9976c..4396be1154f 100644 --- a/lib/wasi/src/state.rs +++ b/lib/wasi/src/state.rs @@ -117,10 +117,18 @@ pub enum Kind { Root { entries: HashMap, }, + /// The first two fields are data _about_ the symlink + /// the last field is the data _inside_ the symlink + /// + /// `base_po_dir` should never be the root because: + /// - Right now symlinks are not allowed in the immutable root + /// - There is always a closer pre-opened dir to the symlink file (by definition of the root being a collection of preopened dirs) Symlink { - /// The preopened dir that this symlink is relative to + /// The preopened dir that this symlink file is relative to (via `path_to_symlink`) base_po_dir: __wasi_fd_t, - /// the relative path from theroot of the preopened dir + /// The path to the symlink from the `base_po_dir` + path_to_symlink: PathBuf, + /// the value of the symlink as a relative path relative_path: PathBuf, }, Buffer { @@ -207,7 +215,7 @@ impl WasiFs { .expect("Could not open fd"); if let Kind::Root { entries } = &mut wasi_fs.inodes[root_inode].kind { // todo handle collisions - assert!(entries.insert(dbg!(dir.to_string()), inode).is_none()) + assert!(entries.insert(dir.to_string(), inode).is_none()) } wasi_fs.preopen_fds.push(fd); } @@ -245,7 +253,7 @@ impl WasiFs { .expect("Could not open fd"); if let Kind::Root { entries } = &mut wasi_fs.inodes[root_inode].kind { // todo handle collisions - assert!(dbg!(entries.insert(dbg!(alias.clone()), inode)).is_none()); + assert!(entries.insert(alias.clone(), inode).is_none()); } wasi_fs.preopen_fds.push(fd); } @@ -376,7 +384,8 @@ impl WasiFs { &mut self, base: __wasi_fd_t, path: &str, - symlink_count: u32, + mut symlink_count: u32, + follow_symlinks: bool, ) -> Result { if symlink_count > MAX_SYMLINKS { return Err(__WASI_EMLINK); @@ -390,7 +399,7 @@ impl WasiFs { 'path_iter: for component in path.components() { // for each component traverse file structure // loading inodes as necessary - 'symlink_resolution: loop { + 'symlink_resolution: while symlink_count < MAX_SYMLINKS { match &mut self.inodes[cur_inode].kind { Kind::Buffer { .. } => unimplemented!("state::get_inode_at_path for buffers"), Kind::Dir { @@ -407,6 +416,8 @@ impl WasiFs { return Err(__WASI_EACCES); } } + // used for full resolution of symlinks + let mut loop_for_symlink = false; if let Some(entry) = entries.get(component.as_os_str().to_string_lossy().as_ref()) { @@ -437,11 +448,25 @@ impl WasiFs { } else if file_type.is_symlink() { let link_value = file.read_link().ok().ok_or(__WASI_EIO)?; debug!("attempting to decompose path {:?}", link_value); - let (pre_open_dir_fd, relative_path) = - self.path_into_pre_open_and_relative_path(&link_value)?; + + let (pre_open_dir_fd, relative_path) = if link_value.is_relative() { + // the symlink resolution part of canonicalize is not what we want: + // this should help tests pass, then we can make it fail with a new test + // actually, it might be fine + /*let canon_link_value = dbg!(link_value.canonicalize()) + .ok() + .ok_or(__WASI_EINVAL)?;*/ + dbg!(self.path_into_pre_open_and_relative_path(&file))? + } else { + unimplemented!("ABSOLUTE SYMLINKS ARE NO GOOD"); + //dbg!(self.path_into_pre_open_and_relative_path(&link_value))? + }; + loop_for_symlink = true; + symlink_count += 1; Kind::Symlink { base_po_dir: pre_open_dir_fd, - relative_path: relative_path, + path_to_symlink: relative_path, + relative_path: link_value, } } else { unimplemented!("state::get_inode_at_path unknown file type: not file, directory, or symlink"); @@ -449,6 +474,9 @@ impl WasiFs { cur_inode = self.create_inode(kind, false, file.to_string_lossy().to_string())?; + if loop_for_symlink && follow_symlinks { + continue 'symlink_resolution; + } } } Kind::Root { entries } => { @@ -473,18 +501,30 @@ impl WasiFs { } Kind::Symlink { base_po_dir, + path_to_symlink, relative_path, } => { let new_base_dir = *base_po_dir; // allocate to reborrow mutabily to recur - let new_path = relative_path.to_string_lossy().to_string(); + let new_path = { + /*if let Kind::Root { .. } = self.inodes[base_po_dir].kind { + assert!(false, "symlinks should never be relative to the root"); + }*/ + let mut base = path_to_symlink.clone(); + // remove the symlink file itself from the path, leaving just the path from the base + // to the dir containing the symlink + base.pop(); + base.push(relative_path); + base.to_string_lossy().to_string() + }; let symlink_inode = self.get_inode_at_path_inner( new_base_dir, &new_path, symlink_count + 1, + follow_symlinks, )?; cur_inode = symlink_inode; - continue 'symlink_resolution; + //continue 'symlink_resolution; } } break 'symlink_resolution; @@ -520,12 +560,17 @@ impl WasiFs { /// gets a host file from a base directory and a path /// this function ensures the fs remains sandboxed + // NOTE: follow symlinks is super weird right now + // even if it's false, it still follows symlinks, just not the last + // symlink so + // This will be resolved when we have tests asserting the correct behavior pub fn get_inode_at_path( &mut self, base: __wasi_fd_t, path: &str, + follow_symlinks: bool, ) -> Result { - self.get_inode_at_path_inner(base, path, 0) + self.get_inode_at_path_inner(base, path, 0, follow_symlinks) } pub fn get_fd(&self, fd: __wasi_fd_t) -> Result<&Fd, __wasi_errno_t> { @@ -679,22 +724,28 @@ impl WasiFs { Kind::Dir { path, .. } => path.metadata().ok()?, Kind::Symlink { base_po_dir, - relative_path, + path_to_symlink, + .. } => { let base_po_inode = &self.fd_map[base_po_dir].inode; let base_po_inode_v = &self.inodes[*base_po_inode]; - dbg!(&base_po_inode_v.name); - if let Kind::Dir { path, .. } = &base_po_inode_v.kind { - let mut real_path = path.clone(); - // PHASE 1: ignore all possible symlinks in `relative_path` - // TODO: walk the segments of `relative_path` via the entries of the Dir - // use helper function to avoid duplicating this logic (walking this will require - // &self to be &mut sel - real_path.push(relative_path); - real_path.metadata().ok()? - } else { + match &base_po_inode_v.kind { + Kind::Root { .. } => { + path_to_symlink.clone().symlink_metadata().ok()? + } + Kind::Dir { path, .. } => { + let mut real_path = path.clone(); + // PHASE 1: ignore all possible symlinks in `relative_path` + // TODO: walk the segments of `relative_path` via the entries of the Dir + // use helper function to avoid duplicating this logic (walking this will require + // &self to be &mut sel + // TODO: adjust size of symlink, too + // for all paths adjusted think about this + real_path.push(path_to_symlink); + real_path.symlink_metadata().ok()? + } // if this triggers, there's a bug in the symlink code - unreachable!("Symlink pointing to something that's not a directory as its base preopened directory"); + _ => unreachable!("Symlink pointing to something that's not a directory as its base preopened directory"), } } __ => return None, diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index c524a19e9db..1f5cc372cda 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -1217,7 +1217,11 @@ pub fn path_filestat_get( .map_err(|_| __WASI_EINVAL)); debug!("=> path: {}", &path_string); - let file_inode = wasi_try!(state.fs.get_inode_at_path(fd, path_string)); + let file_inode = wasi_try!(state.fs.get_inode_at_path( + fd, + path_string, + flags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, + )); let stat = wasi_try!(state .fs .get_stat_for_kind(&state.fs.inodes[file_inode].kind) @@ -1328,6 +1332,10 @@ pub fn path_open( fd: WasmPtr<__wasi_fd_t>, ) -> __wasi_errno_t { debug!("wasi::path_open"); + if dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0 { + // TODO: resolution fn needs to get this bit + debug!(" - will follow symlinks when opening path"); + } let memory = ctx.memory(0); /* TODO: find actual upper bound on name size (also this is a path, not a name :think-fish:) */ if path_len > 1024 * 1024 { @@ -1352,7 +1360,11 @@ pub fn path_open( let path_string = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); let path = std::path::PathBuf::from(path_string); - let inode = wasi_try!(state.fs.get_inode_at_path(dirfd, path_string)); + let inode = wasi_try!(state.fs.get_inode_at_path( + dirfd, + path_string, + dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, + )); match &mut state.fs.inodes[inode].kind { Kind::File { @@ -1387,13 +1399,22 @@ pub fn path_open( } } } - Kind::Symlink { .. } => { - // TODO: figure out what to do here - unimplemented!("wasi::path_open on symlink"); + Kind::Symlink { + base_po_dir, + path_to_symlink, + relative_path, + } => { + unimplemented!("SYMLINKS IN PATH_OPEN"); + /*if dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0 { + //state.fs.get_inode_at_path(base_po_dir, ) + + } else { + // TODO: figure out what to do here + return __WASI_EINVAL; + }*/ } } - // TODO: reimplement all the flag checking logic debug!( "inode {:?} value {:#?} found!", inode, state.fs.inodes[inode] @@ -1429,12 +1450,12 @@ pub fn path_readlink( return __WASI_EACCES; } let path_str = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); - let inode = wasi_try!(state.fs.get_inode_at_path(dir_fd, path_str)); + let inode = wasi_try!(state.fs.get_inode_at_path(dir_fd, path_str, false)); if let Kind::Symlink { relative_path, .. } = &state.fs.inodes[inode].kind { let rel_path_str = relative_path.to_string_lossy(); let bytes = rel_path_str.bytes(); - if bytes.len() < buf_len as usize { + if bytes.len() >= buf_len as usize { return __WASI_EOVERFLOW; } From dc19bf32b9e05f7e048ccd07d6026fab10d541b8 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Wed, 17 Jul 2019 15:47:59 -0700 Subject: [PATCH 07/11] fmt --- lib/wasi-tests/tests/wasitests/_common.rs | 3 +-- lib/wasi-tests/tests/wasitests/envvar.rs | 2 +- lib/wasi-tests/tests/wasitests/fseek.rs | 5 ++++- lib/wasi-tests/tests/wasitests/mapdir.rs | 5 ++++- lib/wasi-tests/tests/wasitests/readlink.rs | 5 ++++- .../tests/wasitests/wasi_sees_virtual_root.rs | 15 ++++++++++++++- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/wasi-tests/tests/wasitests/_common.rs b/lib/wasi-tests/tests/wasitests/_common.rs index 0850d1d0a49..958fdf6ad26 100644 --- a/lib/wasi-tests/tests/wasitests/_common.rs +++ b/lib/wasi-tests/tests/wasitests/_common.rs @@ -33,8 +33,7 @@ macro_rules! assert_wasi_output { let module = wasmer_runtime_core::compile_with(&wasm_bytes[..], &get_compiler()) .expect("WASM can't be compiled"); - let import_object = - generate_import_object(vec![], vec![], $po_dir_args, $mapdir_args); + let import_object = generate_import_object(vec![], vec![], $po_dir_args, $mapdir_args); let instance = module .instantiate(&import_object) diff --git a/lib/wasi-tests/tests/wasitests/envvar.rs b/lib/wasi-tests/tests/wasitests/envvar.rs index caaa76670c0..7c66b3d2ceb 100644 --- a/lib/wasi-tests/tests/wasitests/envvar.rs +++ b/lib/wasi-tests/tests/wasitests/envvar.rs @@ -5,7 +5,7 @@ fn test_envvar() { "envvar", vec![], vec![], - vec!["DOG=1".to_string(),"CAT=2".to_string(),], + vec!["DOG=1".to_string(), "CAT=2".to_string(),], "../../wasitests/envvar.out" ); } diff --git a/lib/wasi-tests/tests/wasitests/fseek.rs b/lib/wasi-tests/tests/wasitests/fseek.rs index 43bbf21a7a9..a1eac93ed75 100644 --- a/lib/wasi-tests/tests/wasitests/fseek.rs +++ b/lib/wasi-tests/tests/wasitests/fseek.rs @@ -4,7 +4,10 @@ fn test_fseek() { "../../wasitests/fseek.wasm", "fseek", vec![], - vec![(".".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet")),], + vec![( + ".".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet") + ),], vec![], "../../wasitests/fseek.out" ); diff --git a/lib/wasi-tests/tests/wasitests/mapdir.rs b/lib/wasi-tests/tests/wasitests/mapdir.rs index 407d39f75b1..301f4887ab8 100644 --- a/lib/wasi-tests/tests/wasitests/mapdir.rs +++ b/lib/wasi-tests/tests/wasitests/mapdir.rs @@ -4,7 +4,10 @@ fn test_mapdir() { "../../wasitests/mapdir.wasm", "mapdir", vec![], - vec![(".".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet")),], + vec![( + ".".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet") + ),], vec![], "../../wasitests/mapdir.out" ); diff --git a/lib/wasi-tests/tests/wasitests/readlink.rs b/lib/wasi-tests/tests/wasitests/readlink.rs index 9c86e36667c..72e0c7fa760 100644 --- a/lib/wasi-tests/tests/wasitests/readlink.rs +++ b/lib/wasi-tests/tests/wasitests/readlink.rs @@ -4,7 +4,10 @@ fn test_readlink() { "../../wasitests/readlink.wasm", "readlink", vec![], - vec![(".".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet")),], + vec![( + ".".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet") + ),], vec![], "../../wasitests/readlink.out" ); diff --git a/lib/wasi-tests/tests/wasitests/wasi_sees_virtual_root.rs b/lib/wasi-tests/tests/wasitests/wasi_sees_virtual_root.rs index be60d2b3e16..f604f967864 100644 --- a/lib/wasi-tests/tests/wasitests/wasi_sees_virtual_root.rs +++ b/lib/wasi-tests/tests/wasitests/wasi_sees_virtual_root.rs @@ -4,7 +4,20 @@ fn test_wasi_sees_virtual_root() { "../../wasitests/wasi_sees_virtual_root.wasm", "wasi_sees_virtual_root", vec![], - vec![("act1".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act1")),("act2".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act2")),("act1-again".to_string(), ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act1")),], + vec![ + ( + "act1".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act1") + ), + ( + "act2".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act2") + ), + ( + "act1-again".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act1") + ), + ], vec![], "../../wasitests/wasi_sees_virtual_root.out" ); From 9910527b3070222d90a6536c173c5d6eb1c9a839 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Thu, 18 Jul 2019 17:14:01 -0700 Subject: [PATCH 08/11] further improve sandbox, rework syscalls, add tests --- Makefile | 4 +- lib/wasi-tests/tests/wasitests/mod.rs | 1 + lib/wasi-tests/tests/wasitests/writing.rs | 24 +++ .../wasitests/wasi_sees_virtual_root.out | 1 + .../wasitests/wasi_sees_virtual_root.rs | 8 + .../wasitests/wasi_sees_virtual_root.wasm | Bin 84930 -> 86510 bytes lib/wasi-tests/wasitests/writing.out | 2 + lib/wasi-tests/wasitests/writing.rs | 41 ++++ lib/wasi-tests/wasitests/writing.wasm | Bin 0 -> 83821 bytes lib/wasi/src/state.rs | 42 +++- lib/wasi/src/syscalls/mod.rs | 181 +++++++++++++----- 11 files changed, 247 insertions(+), 57 deletions(-) create mode 100644 lib/wasi-tests/tests/wasitests/writing.rs create mode 100644 lib/wasi-tests/wasitests/writing.out create mode 100644 lib/wasi-tests/wasitests/writing.rs create mode 100755 lib/wasi-tests/wasitests/writing.wasm diff --git a/Makefile b/Makefile index 598484b400f..2d9af1cf2ad 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,9 @@ generate-emtests: WASM_EMSCRIPTEN_GENERATE_EMTESTS=1 cargo build -p wasmer-emscripten-tests --release generate-wasitests: - WASM_WASI_GENERATE_WASITESTS=1 cargo build -p wasmer-wasi-tests --release -vv + WASM_WASI_GENERATE_WASITESTS=1 cargo build -p wasmer-wasi-tests --release -vv \ + && echo "formatting" \ + && cargo fmt spectests-generate: generate-spectests emtests-generate: generate-emtests diff --git a/lib/wasi-tests/tests/wasitests/mod.rs b/lib/wasi-tests/tests/wasitests/mod.rs index 7c05db044b3..a7d06db0616 100644 --- a/lib/wasi-tests/tests/wasitests/mod.rs +++ b/lib/wasi-tests/tests/wasitests/mod.rs @@ -15,3 +15,4 @@ mod mapdir; mod quine; mod readlink; mod wasi_sees_virtual_root; +mod writing; diff --git a/lib/wasi-tests/tests/wasitests/writing.rs b/lib/wasi-tests/tests/wasitests/writing.rs new file mode 100644 index 00000000000..2709f2cfae3 --- /dev/null +++ b/lib/wasi-tests/tests/wasitests/writing.rs @@ -0,0 +1,24 @@ +#[test] +fn test_writing() { + assert_wasi_output!( + "../../wasitests/writing.wasm", + "writing", + vec![], + vec![ + ( + "act1".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act1") + ), + ( + "act2".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act2") + ), + ( + "act1-again".to_string(), + ::std::path::PathBuf::from("wasitests/test_fs/hamlet/act1") + ), + ], + vec![], + "../../wasitests/writing.out" + ); +} diff --git a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out index 1ce8a9c6a7a..f9e5ca114f6 100644 --- a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out +++ b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.out @@ -10,3 +10,4 @@ "/act1" "/act1-again" "/act2" +ROOT IS SAFE diff --git a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs index f3ebb71fd27..f31df50b748 100644 --- a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs +++ b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.rs @@ -18,6 +18,7 @@ fn main() { } } + out.push("ROOT IS SAFE".to_string()); out }; @@ -41,6 +42,13 @@ fn main() { for entry in read_dir { out.push(format!("{:?}", entry.unwrap().path())) } + let f = fs::OpenOptions::new().write(true).open("/abc"); + + if f.is_ok() { + out.push("ROOT IS NOT SAFE".to_string()); + } else { + out.push("ROOT IS SAFE".to_string()); + } out }; diff --git a/lib/wasi-tests/wasitests/wasi_sees_virtual_root.wasm b/lib/wasi-tests/wasitests/wasi_sees_virtual_root.wasm index b9e42f55599c7075189b3b8a5f1b9dd0f6786614..6d0b3cd3a974a9f57e87378e813010f69e539b76 100755 GIT binary patch delta 22534 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⪻l-cKvAAw2=NDls!FUAZ05oQ zuI^8py?p&S#C|#bo7%9-uR7p*LnuZf7o!gGq!i8ksEi4GBVN2I{+m;Q=i)E7;{Pd; zD*ma;b|^suc#h!4*|s7ATKF0~_fUi5lh_ zS2zE$C1z_FWrLlfvy7eN!gx@{6IZF>XoSxnz^|xQRASu1)i8`Mnl~S6=fXvcmn<#q zxnZFdD>YHJ7M91wt1sd68j0J6Su6_APBr7LSPZ6adY;$sG+AQ-6MOM=%A0weDavj9H^6_mm~_g-ag%VTkBK)H z#!ujL@xayBR?1Xp^b%`N9RmrMuj2pgMx=NKndN)s-42k-sg+haWg2!p^O)DKK0}+c zbQL#0hPK)U_RPcOwj)>FlgxVh2}#r>9?w}eOgS@IRBoneqbP44GplCg7z(^OJ^f~y z5nMO_nSdakKP~>tf7DocS?xRq*a9#U2l$WI$-nTPd4+XvmK=&{%6ER?W~gN1Y8%i628VX<}^U5yb$*65nB*bkvNoc!XB*XQ>I1YZJI zx(J%r0>P4&Ee9VeTjDr#Xunp0F|Bc~#r|5SbKUw4&iaj@TBoxK(Pk@Oi__U^J-^Mc z4Iho^u<}Nooz~;;!~aSn(6wnZyR*gVymV`KtY_Qy-W`bU{IAr--*sPh*(&T4&zqc} zZ(J(>xtwkf!~fKm_Z_x=5L}VEDeqz22aGf}EoAgV`Jt%rg^N|!{=#^F-Vh%5e(r{y p=^q+FT>8ypfBpmP9`{>9)1kz^&4iDYR6XSU{q_41`2T7I{x4xO(9Zw> literal 0 HcmV?d00001 diff --git a/lib/wasi/src/state.rs b/lib/wasi/src/state.rs index 4396be1154f..986134ccfa4 100644 --- a/lib/wasi/src/state.rs +++ b/lib/wasi/src/state.rs @@ -173,10 +173,27 @@ impl WasiFs { }; // create virtual root let root_inode = { - let default_rights = 0x1FFFFFFF; // all rights + let all_rights = 0x1FFFFFFF; + // TODO: make this a list of positive rigths instead of negative ones + // root gets all right for now + let root_rights = all_rights + /*& (!__WASI_RIGHT_FD_WRITE) + & (!__WASI_RIGHT_FD_ALLOCATE) + & (!__WASI_RIGHT_PATH_CREATE_DIRECTORY) + & (!__WASI_RIGHT_PATH_CREATE_FILE) + & (!__WASI_RIGHT_PATH_LINK_SOURCE) + & (!__WASI_RIGHT_PATH_RENAME_SOURCE) + & (!__WASI_RIGHT_PATH_RENAME_TARGET) + & (!__WASI_RIGHT_PATH_FILESTAT_SET_SIZE) + & (!__WASI_RIGHT_PATH_FILESTAT_SET_TIMES) + & (!__WASI_RIGHT_FD_FILESTAT_SET_SIZE) + & (!__WASI_RIGHT_FD_FILESTAT_SET_TIMES) + & (!__WASI_RIGHT_PATH_SYMLINK) + & (!__WASI_RIGHT_PATH_UNLINK_FILE) + & (!__WASI_RIGHT_PATH_REMOVE_DIRECTORY)*/; let inode = wasi_fs.create_virtual_root(); let fd = wasi_fs - .create_fd(default_rights, default_rights, 0, inode) + .create_fd(root_rights, root_rights, 0, inode) .expect("Could not create root fd"); wasi_fs.preopen_fds.push(fd); inode @@ -408,13 +425,17 @@ impl WasiFs { ref parent, .. } => { - if component.as_os_str().to_string_lossy() == ".." { - if let Some(p) = parent { - cur_inode = *p; - continue 'path_iter; - } else { - return Err(__WASI_EACCES); + match component.as_os_str().to_string_lossy().borrow() { + ".." => { + if let Some(p) = parent { + cur_inode = *p; + continue 'path_iter; + } else { + return Err(__WASI_EACCES); + } } + "." => continue 'path_iter, + _ => (), } // used for full resolution of symlinks let mut loop_for_symlink = false; @@ -426,10 +447,11 @@ impl WasiFs { let file = { let mut cd = path.clone(); cd.push(component); - cd + dbg!(cd) }; // TODO: verify this returns successfully when given a non-symlink - let metadata = file.symlink_metadata().ok().ok_or(__WASI_EEXIST)?; + let metadata = + dbg!(file.symlink_metadata()).ok().ok_or(__WASI_EINVAL)?; let file_type = metadata.file_type(); let kind = if file_type.is_dir() { diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index 1f5cc372cda..dfc1b08c8b0 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -1346,12 +1346,12 @@ pub fn path_open( let state = get_wasi_state(ctx); // o_flags: - // - __WASI_O_FLAG_CREAT (create if it does not exist) + // - __WASI_O_CREAT (create if it does not exist) // - __WASI_O_DIRECTORY (fail if not dir) // - __WASI_O_EXCL (fail if file exists) // - __WASI_O_TRUNC (truncate size to 0) - let working_dir = wasi_try!(state.fs.fd_map.get(&dirfd).ok_or(__WASI_EBADF)).clone(); + let working_dir = wasi_try!(state.fs.get_fd(dirfd)).clone(); // ASSUMPTION: open rights apply recursively if !has_rights(working_dir.rights, __WASI_RIGHT_PATH_OPEN) { @@ -1359,61 +1359,142 @@ pub fn path_open( } let path_string = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); - let path = std::path::PathBuf::from(path_string); - let inode = wasi_try!(state.fs.get_inode_at_path( + + debug!("=> fd: {}, path: {}", dirfd, &path_string); + + let path_arg = std::path::PathBuf::from(path_string); + let maybe_inode = dbg!(state.fs.get_inode_at_path( dirfd, path_string, dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, )); - match &mut state.fs.inodes[inode].kind { - Kind::File { - ref mut handle, - path, - } => { - if o_flags & __WASI_O_DIRECTORY != 0 { - return __WASI_ENOTDIR; + // TODO: traverse rights of dirs properly + let adjusted_rights = fs_rights_base & working_dir.rights_inheriting; + dbg!(fs_rights_base & __WASI_RIGHT_FD_WRITE != 0); + dbg!(working_dir.rights_inheriting & __WASI_RIGHT_FD_WRITE != 0); + let inode = if let Ok(inode) = maybe_inode { + // Happy path, we found the file we're trying to open + match dbg!(&mut state.fs.inodes[inode].kind) { + Kind::File { + ref mut handle, + path, + } => { + if o_flags & __WASI_O_DIRECTORY != 0 { + return __WASI_ENOTDIR; + } + if o_flags & __WASI_O_EXCL != 0 { + if dbg!(dbg!(&path).exists()) { + return __WASI_EEXIST; + } + } + let mut open_options = std::fs::OpenOptions::new(); + let open_options = open_options + .read(true) + // TODO: ensure these rights are actually valid given parent, etc. + .write(adjusted_rights & __WASI_RIGHT_FD_WRITE != 0) + .create(o_flags & __WASI_O_CREAT != 0) + .truncate(o_flags & __WASI_O_TRUNC != 0); + + *handle = Some(WasiFile::HostFile(wasi_try!(open_options + .open(&path) + .map_err(|_| __WASI_EIO)))); } - if o_flags & __WASI_O_EXCL != 0 { - if path.exists() { - return __WASI_EEXIST; + Kind::Buffer { .. } => unimplemented!("wasi::path_open for Buffer type files"), + Kind::Dir { .. } | Kind::Root { .. } => { + // TODO: adjust these to be correct + if o_flags & __WASI_O_EXCL != 0 { + if dbg!(dbg!(&path_arg).exists()) { + return __WASI_EEXIST; + } } } - let mut open_options = std::fs::OpenOptions::new(); - let open_options = open_options - .read(true) - // TODO: ensure these rights are actually valid given parent, etc. - .write(fs_rights_base & __WASI_RIGHT_FD_WRITE != 0) - .create(o_flags & __WASI_O_CREAT != 0) - .truncate(o_flags & __WASI_O_TRUNC != 0); - - *handle = Some(WasiFile::HostFile(wasi_try!(open_options - .open(&path) - .map_err(|_| __WASI_EIO)))); + Kind::Symlink { + base_po_dir, + path_to_symlink, + relative_path, + } => { + // I think this should return an error + // TODO: investigate this + unimplemented!("SYMLINKS IN PATH_OPEN"); + } } - Kind::Buffer { .. } => unimplemented!("wasi::path_open for Buffer type files"), - Kind::Dir { .. } | Kind::Root { .. } => { - if o_flags & __WASI_O_EXCL != 0 { - if path.exists() { - return __WASI_EEXIST; + inode + } else { + // less-happy path, we have to try to create the file + debug!("Maybe creating file"); + if o_flags & __WASI_O_CREAT != 0 { + if o_flags & __WASI_O_DIRECTORY != 0 { + return __WASI_ENOTDIR; + } + debug!("Creating file"); + // strip end file name + let mut parent_dir = std::path::PathBuf::new(); + let mut components = path_arg.components().rev(); + let new_entity_name = wasi_try!(components.next().ok_or(__WASI_EINVAL)) + .as_os_str() + .to_string_lossy() + .to_string(); + for comp in components.rev() { + parent_dir.push(comp); + } + + let parent_inode = wasi_try!(dbg!(state.fs.get_inode_at_path( + dirfd, + dbg!(&parent_dir.to_string_lossy()), + dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, + ))); + + let new_file_host_path = match &state.fs.inodes[parent_inode].kind { + Kind::Dir { path, .. } => { + let mut new_path = path.clone(); + dbg!(new_path.exists()); + new_path.push(&new_entity_name); + new_path } + Kind::Root { .. } => return __WASI_EACCES, + _ => return __WASI_EINVAL, + }; + // once we got the data we need from the parent, we lookup the host file + // todo: extra check that opening with write access is okay + let handle = { + let mut open_options = std::fs::OpenOptions::new(); + let open_options = open_options + .read(true) + // TODO: ensure these rights are actually valid given parent, etc. + // write access is required for creating a file + .write(true) + .create_new(true); + + dbg!(&new_file_host_path.exists()); + Some(WasiFile::HostFile(wasi_try!(dbg!(open_options + .open(dbg!(&new_file_host_path)) + .map_err(|e| { + debug!("Error opening file {}", e); + __WASI_EIO + }))))) + }; + + let new_inode = { + let kind = Kind::File { + handle, + path: dbg!(new_file_host_path), + }; + wasi_try!(state.fs.create_inode(kind, false, new_entity_name.clone())) + }; + + if let Kind::Dir { + ref mut entries, .. + } = &mut state.fs.inodes[parent_inode].kind + { + entries.insert(new_entity_name, new_inode); } - } - Kind::Symlink { - base_po_dir, - path_to_symlink, - relative_path, - } => { - unimplemented!("SYMLINKS IN PATH_OPEN"); - /*if dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0 { - //state.fs.get_inode_at_path(base_po_dir, ) - } else { - // TODO: figure out what to do here - return __WASI_EINVAL; - }*/ + new_inode + } else { + return maybe_inode.unwrap_err(); } - } + }; debug!( "inode {:?} value {:#?} found!", @@ -1425,7 +1506,7 @@ pub fn path_open( let out_fd = wasi_try!(state .fs - .create_fd(fs_rights_base, fs_rights_inheriting, fs_flags, inode)); + .create_fd(adjusted_rights, fs_rights_inheriting, fs_flags, inode)); fd_cell.set(out_fd); @@ -1531,7 +1612,15 @@ pub fn path_unlink_file( let base_dir = wasi_try!(state.fs.fd_map.get(&fd).ok_or(__WASI_EBADF)); let path_str = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); - let _result = wasi_try!(std::fs::remove_file(path_str).ok().ok_or(__WASI_EIO)); + + let inode = wasi_try!(state.fs.get_inode_at_path(fd, path_str, false)); + + match &state.fs.inodes[inode].kind { + Kind::File { path, .. } => { + let _result = wasi_try!(std::fs::remove_file(path).ok().ok_or(__WASI_EIO)); + } + _ => unimplemented!("wasi::path_unlink_file for non-files"), + } __WASI_ESUCCESS } From a8a0dbed91049d4f96f13beb83136c429e002c45 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Fri, 19 Jul 2019 11:47:31 -0700 Subject: [PATCH 09/11] improve abstraction impl rm syscalls, properly finish create_dir --- lib/wasi-tests/tests/wasitests/create_dir.rs | 3 +- lib/wasi-tests/wasitests/create_dir.rs | 3 + lib/wasi-tests/wasitests/create_dir.wasm | Bin 98722 -> 98722 bytes lib/wasi-tests/wasitests/ignores.txt | 2 +- lib/wasi/src/macros.rs | 2 +- lib/wasi/src/state.rs | 31 +++ lib/wasi/src/syscalls/mod.rs | 201 +++++++++++++------ 7 files changed, 180 insertions(+), 62 deletions(-) diff --git a/lib/wasi-tests/tests/wasitests/create_dir.rs b/lib/wasi-tests/tests/wasitests/create_dir.rs index 42696b54544..3c8866721ad 100644 --- a/lib/wasi-tests/tests/wasitests/create_dir.rs +++ b/lib/wasi-tests/tests/wasitests/create_dir.rs @@ -1,10 +1,9 @@ #[test] -#[ignore] fn test_create_dir() { assert_wasi_output!( "../../wasitests/create_dir.wasm", "create_dir", - vec![], + vec![".".to_string(),], vec![], vec![], "../../wasitests/create_dir.out" diff --git a/lib/wasi-tests/wasitests/create_dir.rs b/lib/wasi-tests/wasitests/create_dir.rs index 514387b0205..21e07d619ef 100644 --- a/lib/wasi-tests/wasitests/create_dir.rs +++ b/lib/wasi-tests/wasitests/create_dir.rs @@ -1,3 +1,6 @@ +// Args: +// dir: . + use std::fs; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::*; diff --git a/lib/wasi-tests/wasitests/create_dir.wasm b/lib/wasi-tests/wasitests/create_dir.wasm index 6908bfbe373a52609edea77e211b49d359c4d335..9847f3bec7ae269ae269744fe7aaa885fd4d795d 100755 GIT binary patch delta 40 wcmZ3~%(keRtziqJy+5PqbO(P%aYpIs5&n$EjPlca{27B8mA60eXFOyG0QE-;-~a#s delta 40 wcmZ3~%(keRtziqJy+5PSbO(P%aYl*h5&n$Ej55=E{27B86}CU{XFOyG0Q7YW)Bpeg diff --git a/lib/wasi-tests/wasitests/ignores.txt b/lib/wasi-tests/wasitests/ignores.txt index 061ac5e1e6d..8b137891791 100644 --- a/lib/wasi-tests/wasitests/ignores.txt +++ b/lib/wasi-tests/wasitests/ignores.txt @@ -1 +1 @@ -create_dir + diff --git a/lib/wasi/src/macros.rs b/lib/wasi/src/macros.rs index 86b6b07dcbd..819a4c91b89 100644 --- a/lib/wasi/src/macros.rs +++ b/lib/wasi/src/macros.rs @@ -12,7 +12,7 @@ macro_rules! wasi_try { } } }}; - ($expr:expr; $e:expr) => {{ + ($expr:expr, $e:expr) => {{ let opt: Option<_> = $expr; wasi_try!(opt.ok_or($e)) }}; diff --git a/lib/wasi/src/state.rs b/lib/wasi/src/state.rs index 986134ccfa4..fb22300cced 100644 --- a/lib/wasi/src/state.rs +++ b/lib/wasi/src/state.rs @@ -595,6 +595,29 @@ impl WasiFs { self.get_inode_at_path_inner(base, path, 0, follow_symlinks) } + /// Returns the parent Dir or Root that the file at a given path is in and the file name + /// stripped off + pub fn get_parent_inode_at_path( + &mut self, + base: __wasi_fd_t, + path: &Path, + follow_symlinks: bool, + ) -> Result<(Inode, String), __wasi_errno_t> { + let mut parent_dir = std::path::PathBuf::new(); + let mut components = path.components().rev(); + let new_entity_name = components + .next() + .ok_or(__WASI_EINVAL)? + .as_os_str() + .to_string_lossy() + .to_string(); + for comp in components.rev() { + parent_dir.push(comp); + } + dbg!(self.get_inode_at_path(base, dbg!(&parent_dir.to_string_lossy()), follow_symlinks,)) + .map(|v| (v, new_entity_name)) + } + pub fn get_fd(&self, fd: __wasi_fd_t) -> Result<&Fd, __wasi_errno_t> { self.fd_map.get(&fd).ok_or(__WASI_EBADF) } @@ -712,6 +735,14 @@ impl WasiFs { Ok(idx) } + /// This function is unsafe because it's the caller's responsibility to ensure that + /// all refences to the given inode have been removed from the filesystem + /// + /// returns true if the inode existed and was removed + pub unsafe fn remove_inode(&mut self, inode: Inode) -> bool { + self.inodes.remove(inode).is_some() + } + pub fn get_base_path_for_directory(&self, directory: Inode) -> Option { if let Kind::Dir { path, .. } = &self.inodes[directory].kind { return Some(path.to_string_lossy().to_string()); diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index dfc1b08c8b0..f88aa27a5da 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -15,6 +15,7 @@ use crate::{ ExitCode, }; use rand::{thread_rng, Rng}; +use std::borrow::Borrow; use std::cell::Cell; use std::convert::Infallible; use std::io::{self, Read, Seek, Write}; @@ -1124,17 +1125,15 @@ pub fn path_create_directory( let memory = ctx.memory(0); let state = get_wasi_state(ctx); - let working_dir = wasi_try!(state.fs.fd_map.get(&fd).ok_or(__WASI_EBADF)); + let working_dir = wasi_try!(state.fs.get_fd(fd)).clone(); + if let Kind::Root { .. } = &state.fs.inodes[working_dir.inode].kind { + return __WASI_EACCES; + } if !has_rights(working_dir.rights, __WASI_RIGHT_PATH_CREATE_DIRECTORY) { return __WASI_EACCES; } - let path_cells = wasi_try!(path.deref(memory, 0, path_len)); - let path_string = - wasi_try!( - std::str::from_utf8(unsafe { &*(path_cells as *const [_] as *const [u8]) }) - .map_err(|_| __WASI_EINVAL) - ); - debug!("=> path: {}", &path_string); + let path_string = wasi_try!(path.get_utf8_string(memory, path_len), __WASI_EINVAL); + debug!("=> fd: {}, path: {}", fd, &path_string); let path = std::path::PathBuf::from(path_string); let path_vec = wasi_try!(path @@ -1150,30 +1149,57 @@ pub fn path_create_directory( return __WASI_EINVAL; } - assert!( - path_vec.len() == 1, - "path_create_directory for paths greater than depth 1 has not been implemented because our WASI FS abstractions are a work in progress. We apologize for the inconvenience" - ); - debug!("Path vec: {:#?}", path_vec); - - wasi_try!(std::fs::create_dir(&path).map_err(|_| __WASI_EIO)); + debug!("Looking at components {:?}", &path_vec); - let kind = Kind::Dir { - parent: Some(working_dir.inode), - path: path.clone(), - entries: Default::default(), - }; - let new_inode = state.fs.inodes.insert(InodeVal { - stat: wasi_try!(state.fs.get_stat_for_kind(&kind).ok_or(__WASI_EIO)), - is_preopened: false, - name: path_vec[0].clone(), - kind, - }); - - if let Kind::Dir { entries, .. } = &mut state.fs.inodes[working_dir.inode].kind { - entries.insert(path_vec[0].clone(), new_inode); - } else { - return __WASI_ENOTDIR; + let mut cur_dir_inode = working_dir.inode; + for comp in &path_vec { + debug!("Creating dir {}", comp); + match dbg!(&mut state.fs.inodes[cur_dir_inode].kind) { + Kind::Dir { + ref mut entries, + path, + parent, + } => { + match comp.borrow() { + ".." => { + if let Some(p) = parent { + cur_dir_inode = *p; + continue; + } + } + "." => continue, + _ => (), + } + if let Some(child) = entries.get(comp) { + cur_dir_inode = *child; + } else { + let mut adjusted_path = path.clone(); + // TODO: double check this doesn't risk breaking the sandbox + adjusted_path.push(comp); + if adjusted_path.exists() && !adjusted_path.is_dir() { + return __WASI_ENOTDIR; + } else if !adjusted_path.exists() { + wasi_try!(std::fs::create_dir(&adjusted_path).ok(), __WASI_EIO); + } + let kind = Kind::Dir { + parent: Some(cur_dir_inode), + path: adjusted_path, + entries: Default::default(), + }; + let new_inode = wasi_try!(state.fs.create_inode(kind, false, comp.to_string())); + // reborrow to insert + if let Kind::Dir { + ref mut entries, .. + } = &mut state.fs.inodes[cur_dir_inode].kind + { + entries.insert(comp.to_string(), new_inode); + } + cur_dir_inode = new_inode; + } + } + Kind::Root { .. } => return __WASI_EACCES, + _ => return __WASI_ENOTDIR, + } } __WASI_ESUCCESS @@ -1205,17 +1231,15 @@ pub fn path_filestat_get( let state = get_wasi_state(ctx); let memory = ctx.memory(0); - let root_dir = wasi_try!(state.fs.fd_map.get(&fd).ok_or(__WASI_EBADF)); + let root_dir = wasi_try!(state.fs.get_fd(fd)); if !has_rights(root_dir.rights, __WASI_RIGHT_PATH_FILESTAT_GET) { return __WASI_EACCES; } - let path_string = wasi_try!(::std::str::from_utf8(unsafe { - &*(wasi_try!(path.deref(memory, 0, path_len)) as *const [_] as *const [u8]) - }) - .map_err(|_| __WASI_EINVAL)); - debug!("=> path: {}", &path_string); + let path_string = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); + + debug!("=> base_fd: {}, path: {}", fd, &path_string); let file_inode = wasi_try!(state.fs.get_inode_at_path( fd, @@ -1429,22 +1453,12 @@ pub fn path_open( } debug!("Creating file"); // strip end file name - let mut parent_dir = std::path::PathBuf::new(); - let mut components = path_arg.components().rev(); - let new_entity_name = wasi_try!(components.next().ok_or(__WASI_EINVAL)) - .as_os_str() - .to_string_lossy() - .to_string(); - for comp in components.rev() { - parent_dir.push(comp); - } - let parent_inode = wasi_try!(dbg!(state.fs.get_inode_at_path( + let (parent_inode, new_entity_name) = wasi_try!(state.fs.get_parent_inode_at_path( dirfd, - dbg!(&parent_dir.to_string_lossy()), - dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, - ))); - + &path_arg, + dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0 + )); let new_file_host_path = match &state.fs.inodes[parent_inode].kind { Kind::Dir { path, .. } => { let mut new_path = path.clone(); @@ -1557,6 +1571,7 @@ pub fn path_readlink( __WASI_ESUCCESS } +/// Returns __WASI_ENOTEMTPY if directory is not empty pub fn path_remove_directory( ctx: &mut Ctx, fd: __wasi_fd_t, @@ -1568,9 +1583,55 @@ pub fn path_remove_directory( let state = get_wasi_state(ctx); let memory = ctx.memory(0); - let base_dir = wasi_try!(state.fs.fd_map.get(&fd).ok_or(__WASI_EBADF)); - let path_str = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); - let _result = wasi_try!(std::fs::remove_dir(path_str).ok().ok_or(__WASI_EIO)); + let base_dir = wasi_try!(state.fs.fd_map.get(&fd), __WASI_EBADF); + let path_str = wasi_try!(path.get_utf8_string(memory, path_len), __WASI_EINVAL); + + let inode = wasi_try!(state.fs.get_inode_at_path(fd, path_str, false)); + let (parent_inode, childs_name) = + wasi_try!(state + .fs + .get_parent_inode_at_path(fd, std::path::Path::new(path_str), false)); + + let host_path_to_remove = match &state.fs.inodes[inode].kind { + Kind::Dir { entries, path, .. } => { + if !entries.is_empty() { + return __WASI_ENOTEMPTY; + } else { + if wasi_try!(std::fs::read_dir(path).ok(), __WASI_EIO).count() != 0 { + return __WASI_ENOTEMPTY; + } + } + path.clone() + } + Kind::Root { .. } => return __WASI_EACCES, + _ => return __WASI_ENOTDIR, + }; + + match &mut state.fs.inodes[parent_inode].kind { + Kind::Dir { + ref mut entries, .. + } => { + let removed_inode = wasi_try!(entries.remove(&childs_name).ok_or(__WASI_EINVAL)); + // TODO: make this a debug assert in the future + assert!(inode == removed_inode); + } + Kind::Root { .. } => return __WASI_EACCES, + _ => unreachable!( + "Internal logic error in wasi::path_remove_directory, parent is not a directory" + ), + } + + if let Err(_) = std::fs::remove_dir(path_str) { + // reinsert to prevent FS from being in bad state + if let Kind::Dir { + ref mut entries, .. + } = &mut state.fs.inodes[parent_inode].kind + { + entries.insert(childs_name, inode); + } + // TODO: more intelligently return error value by inspecting returned error value + return __WASI_EIO; + } __WASI_ESUCCESS } @@ -1614,13 +1675,37 @@ pub fn path_unlink_file( let path_str = wasi_try!(path.get_utf8_string(memory, path_len).ok_or(__WASI_EINVAL)); let inode = wasi_try!(state.fs.get_inode_at_path(fd, path_str, false)); + let (parent_inode, childs_name) = + wasi_try!(state + .fs + .get_parent_inode_at_path(fd, std::path::Path::new(path_str), false)); - match &state.fs.inodes[inode].kind { - Kind::File { path, .. } => { - let _result = wasi_try!(std::fs::remove_file(path).ok().ok_or(__WASI_EIO)); - } + let host_path_to_remove = match &state.fs.inodes[inode].kind { + Kind::File { path, .. } => path.clone(), _ => unimplemented!("wasi::path_unlink_file for non-files"), + }; + + match &mut state.fs.inodes[parent_inode].kind { + Kind::Dir { + ref mut entries, .. + } => { + let removed_inode = wasi_try!(entries.remove(&childs_name).ok_or(__WASI_EINVAL)); + // TODO: make this a debug assert in the future + assert!(inode == removed_inode); + } + Kind::Root { .. } => return __WASI_EACCES, + _ => unreachable!( + "Internal logic error in wasi::path_unlink_file, parent is not a directory" + ), } + let inode_was_removed = unsafe { state.fs.remove_inode(inode) }; + assert!( + inode_was_removed, + "Inode could not be removed because it doesn't exist" + ); + let _result = wasi_try!(std::fs::remove_file(host_path_to_remove) + .ok() + .ok_or(__WASI_EIO)); __WASI_ESUCCESS } From d74560e1d43496dd06e932d1b308c78919394543 Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Fri, 19 Jul 2019 12:10:45 -0700 Subject: [PATCH 10/11] impl fd_close --- lib/wasi/src/state.rs | 29 ++++++------------- lib/wasi/src/syscalls/mod.rs | 54 +++++++++++++++++++++++------------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/lib/wasi/src/state.rs b/lib/wasi/src/state.rs index fb22300cced..c331b09ae48 100644 --- a/lib/wasi/src/state.rs +++ b/lib/wasi/src/state.rs @@ -23,6 +23,10 @@ pub enum WasiFile { HostFile(fs::File), } +impl WasiFile { + pub fn close(self) {} +} + impl Write for WasiFile { fn write(&mut self, buf: &[u8]) -> io::Result { match self { @@ -447,11 +451,10 @@ impl WasiFs { let file = { let mut cd = path.clone(); cd.push(component); - dbg!(cd) + cd }; // TODO: verify this returns successfully when given a non-symlink - let metadata = - dbg!(file.symlink_metadata()).ok().ok_or(__WASI_EINVAL)?; + let metadata = file.symlink_metadata().ok().ok_or(__WASI_EINVAL)?; let file_type = metadata.file_type(); let kind = if file_type.is_dir() { @@ -472,16 +475,9 @@ impl WasiFs { debug!("attempting to decompose path {:?}", link_value); let (pre_open_dir_fd, relative_path) = if link_value.is_relative() { - // the symlink resolution part of canonicalize is not what we want: - // this should help tests pass, then we can make it fail with a new test - // actually, it might be fine - /*let canon_link_value = dbg!(link_value.canonicalize()) - .ok() - .ok_or(__WASI_EINVAL)?;*/ - dbg!(self.path_into_pre_open_and_relative_path(&file))? + self.path_into_pre_open_and_relative_path(&file)? } else { - unimplemented!("ABSOLUTE SYMLINKS ARE NO GOOD"); - //dbg!(self.path_into_pre_open_and_relative_path(&link_value))? + unimplemented!("Absolute symlinks are not yet supported"); }; loop_for_symlink = true; symlink_count += 1; @@ -614,7 +610,7 @@ impl WasiFs { for comp in components.rev() { parent_dir.push(comp); } - dbg!(self.get_inode_at_path(base, dbg!(&parent_dir.to_string_lossy()), follow_symlinks,)) + self.get_inode_at_path(base, &parent_dir.to_string_lossy(), follow_symlinks) .map(|v| (v, new_entity_name)) } @@ -743,13 +739,6 @@ impl WasiFs { self.inodes.remove(inode).is_some() } - pub fn get_base_path_for_directory(&self, directory: Inode) -> Option { - if let Kind::Dir { path, .. } = &self.inodes[directory].kind { - return Some(path.to_string_lossy().to_string()); - } - None - } - fn create_virtual_root(&mut self) -> Inode { let stat = __wasi_filestat_t { st_filetype: __WASI_FILETYPE_DIRECTORY, diff --git a/lib/wasi/src/syscalls/mod.rs b/lib/wasi/src/syscalls/mod.rs index f88aa27a5da..2df8b747777 100644 --- a/lib/wasi/src/syscalls/mod.rs +++ b/lib/wasi/src/syscalls/mod.rs @@ -314,12 +314,30 @@ pub fn fd_allocate( /// If `fd` is invalid or not open (TODO: consider __WASI_EINVAL) pub fn fd_close(ctx: &mut Ctx, fd: __wasi_fd_t) -> __wasi_errno_t { debug!("wasi::fd_close"); - // FD is too large - return __WASI_EMFILE; - // FD is a directory (due to user input) - return __WASI_EISDIR; - // FD is invalid - return __WASI_EBADF; + + let memory = ctx.memory(0); + let state = get_wasi_state(ctx); + let fd_entry = wasi_try!(state.fs.get_fd(fd)).clone(); + + let inode_val = &mut state.fs.inodes[fd_entry.inode]; + if inode_val.is_preopened { + return __WASI_EACCES; + } + match &mut inode_val.kind { + Kind::File { ref mut handle, .. } => { + let mut empty_handle = None; + std::mem::swap(handle, &mut empty_handle); + if let Some(handle_inner) = empty_handle { + handle_inner.close() + } else { + return __WASI_EINVAL; + } + } + Kind::Dir { .. } => return __WASI_EISDIR, + Kind::Root { .. } => return __WASI_EACCES, + Kind::Symlink { .. } | Kind::Buffer { .. } => return __WASI_EINVAL, + } + __WASI_ESUCCESS } @@ -1154,7 +1172,7 @@ pub fn path_create_directory( let mut cur_dir_inode = working_dir.inode; for comp in &path_vec { debug!("Creating dir {}", comp); - match dbg!(&mut state.fs.inodes[cur_dir_inode].kind) { + match &mut state.fs.inodes[cur_dir_inode].kind { Kind::Dir { ref mut entries, path, @@ -1387,19 +1405,17 @@ pub fn path_open( debug!("=> fd: {}, path: {}", dirfd, &path_string); let path_arg = std::path::PathBuf::from(path_string); - let maybe_inode = dbg!(state.fs.get_inode_at_path( + let maybe_inode = state.fs.get_inode_at_path( dirfd, path_string, dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0, - )); + ); // TODO: traverse rights of dirs properly let adjusted_rights = fs_rights_base & working_dir.rights_inheriting; - dbg!(fs_rights_base & __WASI_RIGHT_FD_WRITE != 0); - dbg!(working_dir.rights_inheriting & __WASI_RIGHT_FD_WRITE != 0); let inode = if let Ok(inode) = maybe_inode { // Happy path, we found the file we're trying to open - match dbg!(&mut state.fs.inodes[inode].kind) { + match &mut state.fs.inodes[inode].kind { Kind::File { ref mut handle, path, @@ -1408,7 +1424,7 @@ pub fn path_open( return __WASI_ENOTDIR; } if o_flags & __WASI_O_EXCL != 0 { - if dbg!(dbg!(&path).exists()) { + if path.exists() { return __WASI_EEXIST; } } @@ -1428,7 +1444,7 @@ pub fn path_open( Kind::Dir { .. } | Kind::Root { .. } => { // TODO: adjust these to be correct if o_flags & __WASI_O_EXCL != 0 { - if dbg!(dbg!(&path_arg).exists()) { + if path_arg.exists() { return __WASI_EEXIST; } } @@ -1462,7 +1478,6 @@ pub fn path_open( let new_file_host_path = match &state.fs.inodes[parent_inode].kind { Kind::Dir { path, .. } => { let mut new_path = path.clone(); - dbg!(new_path.exists()); new_path.push(&new_entity_name); new_path } @@ -1480,19 +1495,18 @@ pub fn path_open( .write(true) .create_new(true); - dbg!(&new_file_host_path.exists()); - Some(WasiFile::HostFile(wasi_try!(dbg!(open_options - .open(dbg!(&new_file_host_path)) + Some(WasiFile::HostFile(wasi_try!(open_options + .open(&new_file_host_path) .map_err(|e| { debug!("Error opening file {}", e); __WASI_EIO - }))))) + })))) }; let new_inode = { let kind = Kind::File { handle, - path: dbg!(new_file_host_path), + path: new_file_host_path, }; wasi_try!(state.fs.create_inode(kind, false, new_entity_name.clone())) }; From 328ef4e66e31566687366acbeaf138dcf3e601da Mon Sep 17 00:00:00 2001 From: Mark McCaskey Date: Fri, 19 Jul 2019 12:55:58 -0700 Subject: [PATCH 11/11] use correct debug macro on Windows --- lib/wasi/src/syscalls/windows.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/wasi/src/syscalls/windows.rs b/lib/wasi/src/syscalls/windows.rs index f6559290bbc..6c0cff71bcb 100644 --- a/lib/wasi/src/syscalls/windows.rs +++ b/lib/wasi/src/syscalls/windows.rs @@ -1,5 +1,6 @@ use crate::syscalls::types::*; use std::cell::Cell; +use wasmer_runtime_core::debug; pub fn platform_clock_res_get( clock_id: __wasi_clockid_t,