Skip to content

Commit

Permalink
Merge pull request #4302 from wasmerio/wasi-runner-mount-fs-instances
Browse files Browse the repository at this point in the history
Allow `WasiRunner` to mount `FileSystem` instances
  • Loading branch information
Michael Bryan authored Jan 10, 2024
2 parents 214b76b + 0baaa7e commit 0cf0f5b
Showing 6 changed files with 351 additions and 64 deletions.
11 changes: 11 additions & 0 deletions lib/virtual-fs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
@@ -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::*;
@@ -672,6 +676,13 @@ impl FileType {
}
}

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

pub fn is_dir(&self) -> bool {
self.dir
}
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
@@ -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
@@ -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,
};
@@ -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
}

Loading

0 comments on commit 0cf0f5b

Please sign in to comment.