Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow WasiRunner to mount FileSystem instances #4302

Merged
merged 7 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/virtual-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ mod filesystems;
pub(crate) mod ops;
mod overlay_fs;
pub mod pipe;
#[cfg(feature = "host-fs")]
mod scoped_directory_fs;
mod static_file;
#[cfg(feature = "static-fs")]
pub mod static_fs;
Expand All @@ -65,6 +67,8 @@ pub use null_file::*;
pub use overlay_fs::OverlayFileSystem;
pub use passthru_fs::*;
pub use pipe::*;
#[cfg(feature = "host-fs")]
pub use scoped_directory_fs::ScopedDirectoryFileSystem;
pub use special_file::*;
pub use static_file::StaticFile;
pub use tmp_fs::*;
Expand Down Expand Up @@ -672,6 +676,13 @@ impl FileType {
}
}

pub fn new_file() -> Self {
Self {
file: true,
..Default::default()
}
}

pub fn is_dir(&self) -> bool {
self.dir
}
Expand Down
198 changes: 198 additions & 0 deletions lib/virtual-fs/src/scoped_directory_fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use std::path::{Component, Path, PathBuf};

use futures::future::BoxFuture;

use crate::{
DirEntry, FileOpener, FileSystem, FsError, Metadata, OpenOptions, OpenOptionsConfig, ReadDir,
VirtualFile,
};

/// A [`FileSystem`] implementation that is scoped to a specific directory on
/// the host.
#[derive(Debug, Clone)]
pub struct ScopedDirectoryFileSystem {
root: PathBuf,
inner: crate::host_fs::FileSystem,
}

impl ScopedDirectoryFileSystem {
pub fn new(root: impl Into<PathBuf>, inner: crate::host_fs::FileSystem) -> Self {
ScopedDirectoryFileSystem {
root: root.into(),
inner,
}
}

/// Create a new [`ScopedDirectoryFileSystem`] using the current
/// [`tokio::runtime::Handle`].
///
/// # Panics
///
/// This will panic if called outside of a `tokio` context.
pub fn new_with_default_runtime(root: impl Into<PathBuf>) -> Self {
let handle = tokio::runtime::Handle::current();
let fs = crate::host_fs::FileSystem::new(handle);
ScopedDirectoryFileSystem::new(root, fs)
}

fn prepare_path(&self, path: &Path) -> PathBuf {
let path = normalize_path(path);
let path = path.strip_prefix("/").unwrap_or(&path);

let path = if !path.starts_with(&self.root) {
self.root.join(path)
} else {
path.to_owned()
};

debug_assert!(path.starts_with(&self.root));
path
}
}

impl FileSystem for ScopedDirectoryFileSystem {
fn read_dir(&self, path: &Path) -> Result<ReadDir, FsError> {
let path = self.prepare_path(path);

let mut entries = Vec::new();

for entry in self.inner.read_dir(&path)? {
let entry = entry?;
let path = entry
.path
.strip_prefix(&self.root)
.map_err(|_| FsError::InvalidData)?;
entries.push(DirEntry {
path: Path::new("/").join(path),
..entry
});
}

Ok(ReadDir::new(entries))
}

fn create_dir(&self, path: &Path) -> Result<(), FsError> {
let path = self.prepare_path(path);
self.inner.create_dir(&path)
}

fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
let path = self.prepare_path(path);
self.inner.remove_dir(&path)
}

fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<(), FsError>> {
Box::pin(async move {
let from = self.prepare_path(from);
let to = self.prepare_path(to);
self.inner.rename(&from, &to).await
})
}

fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
let path = self.prepare_path(path);
self.inner.metadata(&path)
}

fn remove_file(&self, path: &Path) -> Result<(), FsError> {
let path = self.prepare_path(path);
self.inner.remove_file(&path)
}

fn new_open_options(&self) -> OpenOptions {
OpenOptions::new(self)
}
}

impl FileOpener for ScopedDirectoryFileSystem {
fn open(
&self,
path: &Path,
conf: &OpenOptionsConfig,
) -> Result<Box<dyn VirtualFile + Send + Sync + 'static>, FsError> {
let path = self.prepare_path(path);
self.inner
.new_open_options()
.options(conf.clone())
.open(&path)
}
}

// Copied from cargo
// https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();

if matches!(components.peek(), Some(Component::Prefix(..))) {
// This bit diverges from the original cargo implementation, but we want
// to ignore the drive letter or UNC prefix on Windows. This shouldn't
// make a difference in practice because WASI is meant to give us
// Unix-style paths, not Windows-style ones.
let _ = components.next();
}

let mut ret = PathBuf::new();

for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}

#[cfg(test)]
mod tests {
use tempfile::TempDir;
use tokio::io::AsyncReadExt;

use super::*;

#[tokio::test]
async fn open_files() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.txt"), "Hello, World!").unwrap();
let fs = ScopedDirectoryFileSystem::new_with_default_runtime(temp.path());

let mut f = fs.new_open_options().read(true).open("/file.txt").unwrap();
let mut contents = String::new();
f.read_to_string(&mut contents).await.unwrap();

assert_eq!(contents, "Hello, World!");
}

#[tokio::test]
async fn cant_access_outside_the_scoped_directory() {
let scoped_directory = TempDir::new().unwrap();
std::fs::write(scoped_directory.path().join("file.txt"), "").unwrap();
std::fs::create_dir_all(scoped_directory.path().join("nested").join("dir")).unwrap();
let fs = ScopedDirectoryFileSystem::new_with_default_runtime(scoped_directory.path());

// Using ".." shouldn't let you escape the scoped directory
let mut directory_entries: Vec<_> = fs
.read_dir("/../../../".as_ref())
.unwrap()
.map(|e| e.unwrap().path())
.collect();
directory_entries.sort();
assert_eq!(
directory_entries,
vec![PathBuf::from("/file.txt"), PathBuf::from("/nested")],
);

// Using a directory's absolute path also shouldn't work
let other_dir = TempDir::new().unwrap();
assert_eq!(
fs.read_dir(other_dir.path()).unwrap_err(),
FsError::EntryNotFound
);
}
}
16 changes: 4 additions & 12 deletions lib/wasix/src/runners/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,7 @@ mod wasi_common;
#[cfg(feature = "webc_runner_rt_wcgi")]
pub mod wcgi;

pub use self::{runner::Runner, wasi_common::MappedCommand};

/// A directory that should be mapped from the host filesystem into a WASI
/// instance (the "guest").
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MappedDirectory {
/// The absolute path for a directory on the host filesystem.
pub host: std::path::PathBuf,
/// The absolute path specifying where the host directory should be mounted
/// inside the guest.
pub guest: String,
}
pub use self::{
runner::Runner,
wasi_common::{MappedCommand, MappedDirectory, MountedDirectory},
};
23 changes: 17 additions & 6 deletions lib/wasix/src/runners/wasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ use std::{path::PathBuf, sync::Arc};

use anyhow::{Context, Error};
use tracing::Instrument;
use virtual_fs::{ArcBoxFile, TmpFileSystem, VirtualFile};
use virtual_fs::{ArcBoxFile, FileSystem, TmpFileSystem, VirtualFile};
use wasmer::Module;
use webc::metadata::{annotations::Wasi, Command};

use crate::{
bin_factory::BinaryPackage,
capabilities::Capabilities,
journal::{DynJournal, SnapshotTrigger},
runners::{wasi_common::CommonWasiOptions, MappedDirectory},
runners::{wasi_common::CommonWasiOptions, MappedDirectory, MountedDirectory},
runtime::{module_cache::ModuleHash, task_manager::VirtualTaskManagerExt},
Runtime, WasiEnvBuilder, WasiError, WasiRuntimeError,
};
Expand Down Expand Up @@ -98,14 +98,25 @@ impl WasiRunner {
self.wasi.forward_host_env = forward;
}

pub fn with_mapped_directories<I, D>(mut self, dirs: I) -> Self
pub fn with_mapped_directories<I, D>(self, dirs: I) -> Self
where
I: IntoIterator<Item = D>,
D: Into<MappedDirectory>,
{
self.wasi
.mapped_dirs
.extend(dirs.into_iter().map(|d| d.into()));
self.with_mounted_directories(dirs.into_iter().map(Into::into).map(MountedDirectory::from))
}

pub fn with_mounted_directories<I, D>(mut self, dirs: I) -> Self
where
I: IntoIterator<Item = D>,
D: Into<MountedDirectory>,
{
self.wasi.mounts.extend(dirs.into_iter().map(Into::into));
self
}

pub fn mount(&mut self, dest: String, fs: Arc<dyn FileSystem + Send + Sync>) -> &mut Self {
self.wasi.mounts.push(MountedDirectory { guest: dest, fs });
self
}

Expand Down
Loading
Loading