//! Capability-sandboxed host files. Every system call runs on the blocking pool. //! Writable files currently open a second append descriptor by path. A concurrent rename //! between those opens can make append refer to a different file (review F9). use crate::*; use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt}; use cap_std::fs::{Dir, FileExt, OpenOptions}; use std::{io::Write, path::Path, sync::Mutex}; /// A host directory bound to one process tracker. #[derive(Clone)] pub struct Host { dir: Arc, process: Process, } impl Host { /// Open the sandbox root. Call this outside an asynchronous worker. pub fn new(path: impl AsRef, process: Process) -> Result { Ok(Self { dir: Arc::new(Dir::open_ambient_dir(path, cap_std::ambient_authority())?), process, }) } /// Share a sandbox with a different process tracker. pub fn for_process(&self, process: Process) -> Self { Self { dir: self.dir.clone(), process, } } } fn rel(path: &str) -> String { let p = path.trim_start_matches('/'); if p.is_empty() { ".".into() } else { p.into() } } fn stat(m: cap_std::fs::Metadata) -> Stat { Stat { directory: m.is_dir(), symlink: m.file_type().is_symlink(), size: m.len(), accessed: m.accessed().ok().map(|t| t.into_std()), modified: m.modified().ok().map(|t| t.into_std()), } } struct Handle { file: Arc>, append: Arc>, process: Process, } #[async_trait] impl Layer for Host { async fn stat(&self, path: &str, follow: bool) -> Result { let (d, p) = (self.dir.clone(), rel(path)); self.process .blocking(move || { if follow { d.metadata(p) } else { d.symlink_metadata(p) } .map(stat) }) .await } async fn readlink(&self, path: &str) -> Result { let (d, p) = (self.dir.clone(), rel(path)); self.process .blocking(move || d.read_link(p).map(|p| p.to_string_lossy().into_owned())) .await } async fn list(&self, path: &str) -> Result> { let (d, p) = (self.dir.clone(), rel(path)); self.process .blocking(move || { let mut entries = d .read_dir(p)? .map(|e| { let e = e?; Ok(Entry { name: e.file_name().to_string_lossy().into_owned(), directory: e.file_type()?.is_dir(), symlink: e.file_type()?.is_symlink(), }) }) .collect::>>()?; entries.sort_by(|a, b| a.name.cmp(&b.name)); Ok(entries) }) .await } async fn open(&self, path: &str, o: Open) -> Result> { let (d, p) = (self.dir.clone(), rel(path)); let (file, append) = self .process .blocking(move || { let mut options = OpenOptions::new(); options .read(true) .write(o.write) .create(o.create) .create_new(o.create && o.exclusive) .truncate(o.truncate); options.follow(if o.follow { FollowSymlinks::Yes } else { FollowSymlinks::No }); let f = d.open_with(&p, &options)?; let directory = f.metadata()?.is_dir(); if o.directory && !directory { return Err(error(io::ErrorKind::NotADirectory)); } let append = if o.write && !directory { { let mut append = OpenOptions::new(); append.append(true).follow(if o.follow { FollowSymlinks::Yes } else { FollowSymlinks::No }); d.open_with(&p, &append)? } } else { f.try_clone()? }; Ok((f, append)) }) .await?; Ok(Arc::new(Handle { file: Arc::new(Mutex::new(file)), append: Arc::new(Mutex::new(append)), process: self.process.clone(), })) } async fn mkdir(&self, path: &str) -> Result<()> { let (d, p) = (self.dir.clone(), rel(path)); self.process.blocking(move || d.create_dir(p)).await } async fn remove_file(&self, path: &str) -> Result<()> { let (d, p) = (self.dir.clone(), rel(path)); self.process.blocking(move || d.remove_file(p)).await } async fn remove_dir(&self, path: &str) -> Result<()> { let (d, p) = (self.dir.clone(), rel(path)); self.process.blocking(move || d.remove_dir(p)).await } async fn rename(&self, from: &str, to: &str) -> Result<()> { let (d, a, b) = (self.dir.clone(), rel(from), rel(to)); self.process.blocking(move || d.rename(a, &d, b)).await } } #[async_trait] impl File for Handle { async fn stat(&self) -> Result { let f = self.file.clone(); self.process .blocking(move || f.lock().unwrap().metadata().map(stat)) .await } async fn read(&self, offset: u64, length: usize) -> Result> { let f = self.file.clone(); self.process .blocking(move || { let f = f.lock().unwrap(); let mut bytes = Vec::new(); bytes .try_reserve(length) .map_err(|_| error(io::ErrorKind::FileTooLarge))?; bytes.resize(length, 0); let n = f.read_at(&mut bytes, offset)?; bytes.truncate(n); Ok(bytes) }) .await } async fn write(&self, offset: u64, bytes: Vec) -> Result { let f = self.file.clone(); self.process .blocking(move || { let f = f.lock().unwrap(); f.write_at(&bytes, offset) }) .await } async fn append(&self, bytes: Vec) -> Result { let f = self.append.clone(); self.process .blocking(move || f.lock().unwrap().write(&bytes)) .await } async fn set_size(&self, size: u64) -> Result<()> { let f = self.file.clone(); self.process .blocking(move || f.lock().unwrap().set_len(size)) .await } async fn sync(&self) -> Result<()> { let f = self.file.clone(); self.process .blocking(move || f.lock().unwrap().sync_all()) .await } async fn set_times( &self, accessed: Option, modified: Option, ) -> Result<()> { let f = self.file.clone(); self.process .blocking(move || { let f = f.lock().unwrap().try_clone()?.into_std(); let mut times = std::fs::FileTimes::new(); if let Some(t) = accessed { times = times.set_accessed(t); } if let Some(t) = modified { times = times.set_modified(t); } f.set_times(times) }) .await } } #[cfg(test)] mod tests { use super::*; use std::io::{Seek, SeekFrom}; /// Positional reads and writes do not change the descriptor's cursor. #[tokio::test] async fn positional_io_keeps_cursor() { let temp = tempfile::tempdir().unwrap(); std::fs::write(temp.path().join("file"), b"abcd").unwrap(); let host = Host::new(temp.path(), Process::default()).unwrap(); let file = host .dir .open_with("file", OpenOptions::new().read(true).write(true)) .unwrap(); let append = file.try_clone().unwrap(); let file = Arc::new(Mutex::new(file)); file.lock().unwrap().seek(SeekFrom::Start(3)).unwrap(); let handle = Handle { file: file.clone(), append: Arc::new(Mutex::new(append)), process: Process::default(), }; assert_eq!(handle.read(0, 1).await.unwrap(), b"a"); handle.write(1, b"Z".to_vec()).await.unwrap(); assert_eq!(file.lock().unwrap().stream_position().unwrap(), 3); } }