API: Layer::stat(path, follow), Layer::readlink, Open::follow, Stat::symlink and Entry::symlink. Process::idle documents the shutdown order: abort, await the task, await idle, release locks.
251 lines
8.6 KiB
Rust
251 lines
8.6 KiB
Rust
//! 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<Dir>,
|
|
process: Process,
|
|
}
|
|
impl Host {
|
|
/// Open the sandbox root. Call this outside an asynchronous worker.
|
|
pub fn new(path: impl AsRef<Path>, process: Process) -> Result<Self> {
|
|
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<Mutex<cap_std::fs::File>>,
|
|
append: Arc<Mutex<cap_std::fs::File>>,
|
|
process: Process,
|
|
}
|
|
#[async_trait]
|
|
impl Layer for Host {
|
|
async fn stat(&self, path: &str, follow: bool) -> Result<Stat> {
|
|
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<String> {
|
|
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<Vec<Entry>> {
|
|
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::<Result<Vec<_>>>()?;
|
|
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
|
Ok(entries)
|
|
})
|
|
.await
|
|
}
|
|
async fn open(&self, path: &str, o: Open) -> Result<Arc<dyn File>> {
|
|
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<Stat> {
|
|
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<Vec<u8>> {
|
|
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<u8>) -> Result<usize> {
|
|
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<u8>) -> Result<usize> {
|
|
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<SystemTime>,
|
|
modified: Option<SystemTime>,
|
|
) -> 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);
|
|
}
|
|
}
|