From 68092ff7bd363dd32590c6b6abd60e9a10298097 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Sun, 30 Nov 2025 21:23:39 +0100 Subject: [PATCH 1/3] chore(virtual-fs): Make PassthruFileSystem Arc based Store the inner FS as an arc to make it more easily shareable. Preserves constructor backwards compatibility. --- lib/virtual-fs/src/passthru_fs.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/virtual-fs/src/passthru_fs.rs b/lib/virtual-fs/src/passthru_fs.rs index fda21b91cdf..f377c3eab12 100644 --- a/lib/virtual-fs/src/passthru_fs.rs +++ b/lib/virtual-fs/src/passthru_fs.rs @@ -2,17 +2,24 @@ //! needed so that a `Box` can be wrapped in an Arc and //! shared - some of the interfaces pass around a `Box` -use std::path::Path; +use std::{path::Path, sync::Arc}; use crate::*; #[derive(Debug)] pub struct PassthruFileSystem { - fs: Box, + fs: Arc, } impl PassthruFileSystem { + /// Creates a new PassthruFileSystem that wraps the given FileSystem. + // NOTE: only kept for backwards compatibility. + // TODO: change to only accept Arc, and remove Self::new_arc in the next breaking API change! pub fn new(inner: Box) -> Self { + Self { fs: inner.into() } + } + + pub fn new_arc(inner: Arc) -> Self { Self { fs: inner } } } @@ -66,6 +73,8 @@ impl FileSystem for PassthruFileSystem { #[cfg(test)] mod test_builder { + use std::sync::Arc; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{FileSystem, PassthruFileSystem}; @@ -96,7 +105,7 @@ mod test_builder { .unwrap(); assert_eq!(buf, b"hello"); - let passthru_fs = PassthruFileSystem::new(Box::new(mem_fs.clone())); + let passthru_fs = PassthruFileSystem::new_arc(Arc::new(mem_fs.clone())); let mut buf = Vec::new(); passthru_fs .new_open_options() From 2fcb28d7d61b832122d64457e4137f0105e25808 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Sun, 30 Nov 2025 21:24:25 +0100 Subject: [PATCH 2/3] chore(virtual-fs): UnionFileSystem::merge Add the ability to merge a union fs into another using a given merge mode. --- lib/virtual-fs/src/union_fs.rs | 54 +++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 406fb959600..b3b815b56c5 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -8,7 +8,7 @@ use crate::*; use std::{path::Path, sync::Arc}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MountPoint { pub path: PathBuf, pub name: String, @@ -36,6 +36,17 @@ pub struct UnionFileSystem { pub mounts: DashMap, } +/// Defines how to handle conflicts when merging two UnionFileSystems +#[derive(Clone, Copy, Debug)] +pub enum UnionMergeMode { + /// Replace existing nodes with the new ones. + Replace, + /// Skip conflicting nodes, and keep the existing ones. + Skip, + /// Return an error if a conflict is found. + Fail, +} + impl UnionFileSystem { pub fn new() -> Self { Self::default() @@ -58,6 +69,47 @@ impl UnionFileSystem { .to_owned() } } + + /// Merge another UnionFileSystem into this one. + pub fn merge(&self, other: &UnionFileSystem, mode: UnionMergeMode) -> Result<()> { + for item in other.mounts.iter() { + if self.mounts.contains_key(item.key()) { + match mode { + UnionMergeMode::Replace => { + self.mounts.insert(item.key().clone(), item.value().clone()); + } + UnionMergeMode::Skip => { + tracing::debug!( + path = %item.key().display(), + "skipping existing mount point while merging two union file systems" + ); + } + UnionMergeMode::Fail => { + return Err(FsError::AlreadyExists); + } + } + } else { + self.mounts.insert(item.key().clone(), item.value().clone()); + } + } + + Ok(()) + } + + /// Duplicate this UnionFileSystem. + /// + /// This differs from the Clone implementation in that it creates a new + /// underlying shared map. + /// Clone just does a shallow copy. + pub fn duplicate(&self) -> Self { + let mounts = DashMap::new(); + + for item in self.mounts.iter() { + mounts.insert(item.key().clone(), item.value().clone()); + } + + Self { mounts } + } } impl UnionFileSystem { From f8ce5e30835c41813f679d365407d9f4a91f4247 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Sun, 30 Nov 2025 21:26:03 +0100 Subject: [PATCH 3/3] fix: Prevent filesystem merge for packages Previously adding additional dependencies to a WASI environment would lead to a recursive file system merge, which would clone all files in memory, which is very expensive. This was caused by a very unstructed creation and extension logic in the WasiFs, which could take on a complicated form of . There previosly were attempts to downcast and extend without merging, but those would fail in common use cases. This is fixed by adding a dedicated WasiFs::Overlay variant, which is a dedicated representation for the common wasix package environment use case. This allows to deterministically extend an existing overlay with additional read-only mounts. --- lib/c-api/src/wasm_c_api/wasi/mod.rs | 4 +- lib/cli/src/commands/run/wasi.rs | 2 +- lib/wasix/src/bin_factory/binary_package.rs | 15 +- lib/wasix/src/fs/mod.rs | 141 ++++++++------ lib/wasix/src/fs/relative_path_hack.rs | 91 +++++++++ lib/wasix/src/runners/wasi.rs | 3 +- lib/wasix/src/runners/wasi_common.rs | 172 +++++------------- lib/wasix/src/runners/wcgi/runner.rs | 5 +- .../package_loader/load_package_tree.rs | 77 ++++---- lib/wasix/src/state/builder.rs | 14 +- lib/wasix/src/state/env.rs | 26 +++ tests/lib/wast/src/wasi_wast.rs | 16 +- 12 files changed, 334 insertions(+), 232 deletions(-) create mode 100644 lib/wasix/src/fs/relative_path_hack.rs diff --git a/lib/c-api/src/wasm_c_api/wasi/mod.rs b/lib/c-api/src/wasm_c_api/wasi/mod.rs index 3b312be01d2..72ee5f47ce4 100644 --- a/lib/c-api/src/wasm_c_api/wasi/mod.rs +++ b/lib/c-api/src/wasm_c_api/wasi/mod.rs @@ -264,6 +264,7 @@ fn prepare_webc_env( package_name: &str, ) -> Option<(WasiFunctionEnv, Imports)> { use virtual_fs::static_fs::StaticFileSystem; + use wasmer_wasix::virtual_fs::FileSystem; use webc::v1::{FsEntryType, WebC}; let store_mut = store.as_store_mut(); @@ -298,7 +299,8 @@ fn prepare_webc_env( }) .collect::>(); - let filesystem = Box::new(StaticFileSystem::init(slice, package_name)?); + let filesystem = + Arc::new(StaticFileSystem::init(slice, package_name)?) as Arc; let mut builder = config.builder.runtime(Arc::new(rt)); if !config.inherit_stdout { diff --git a/lib/cli/src/commands/run/wasi.rs b/lib/cli/src/commands/run/wasi.rs index 1ec5a0bb547..e324e668e26 100644 --- a/lib/cli/src/commands/run/wasi.rs +++ b/lib/cli/src/commands/run/wasi.rs @@ -399,7 +399,7 @@ impl Wasi { if !mapped_dirs.is_empty() { // TODO: should we expose the common ancestor instead of root? let fs_backing: Arc = - Arc::new(PassthruFileSystem::new(default_fs_backing())); + Arc::new(PassthruFileSystem::new_arc(default_fs_backing())); for MappedDirectory { host, guest } in self.mapped_dirs.clone() { let host = if !host.is_absolute() { Path::new("/").join(host) diff --git a/lib/wasix/src/bin_factory/binary_package.rs b/lib/wasix/src/bin_factory/binary_package.rs index bc13a77a833..ca6779fcffc 100644 --- a/lib/wasix/src/bin_factory/binary_package.rs +++ b/lib/wasix/src/bin_factory/binary_package.rs @@ -3,7 +3,7 @@ use std::{path::Path, sync::Arc}; use anyhow::Context; use once_cell::sync::OnceCell; use sha2::Digest; -use virtual_fs::FileSystem; +use virtual_fs::UnionFileSystem; use wasmer_config::package::{ PackageHash, PackageId, PackageSource, SuggestedCompilerOptimizations, }; @@ -90,7 +90,10 @@ pub struct BinaryPackage { /// entrypoint. pub entrypoint_cmd: Option, pub hash: OnceCell, - pub webc_fs: Arc, + // TODO: using a UnionFileSystem here directly is suboptimal, since cloning + // it is expensive. Should instead store an immutable map that can easily + // be converted into a dashmap. + pub webc_fs: Option>, pub commands: Vec, pub uses: Vec, pub file_system_memory_footprint: u64, @@ -264,7 +267,7 @@ impl BinaryPackage { mod tests { use sha2::Digest; use tempfile::TempDir; - use virtual_fs::AsyncReadExt; + use virtual_fs::{AsyncReadExt, FileSystem as _}; use wasmer_package::utils::from_disk; use crate::{ @@ -326,6 +329,8 @@ mod tests { // "/public/file.txt" on the guest. let mut f = pkg .webc_fs + .as_ref() + .expect("no webc fs") .new_open_options() .read(true) .open("/public/file.txt") @@ -352,10 +357,10 @@ mod tests { name = "foo" source = "foo.wasm" abi = "wasi" - + [[command]] name = "cmd" - module = "foo" + module = "foo" "#; let manifest = temp.path().join("wasmer.toml"); std::fs::write(&manifest, wasmer_toml).unwrap(); diff --git a/lib/wasix/src/fs/mod.rs b/lib/wasix/src/fs/mod.rs index 51f1661537a..1de82fc713f 100644 --- a/lib/wasix/src/fs/mod.rs +++ b/lib/wasix/src/fs/mod.rs @@ -9,6 +9,7 @@ mod fd; mod fd_list; mod inode_guard; mod notification; +pub(crate) mod relative_path_hack; use std::{ borrow::{Borrow, Cow}, @@ -33,7 +34,10 @@ use futures::{Future, TryStreamExt, future::BoxFuture}; use serde_derive::{Deserialize, Serialize}; use tokio::io::AsyncWriteExt; use tracing::{debug, trace}; -use virtual_fs::{FileSystem, FsError, OpenOptions, VirtualFile, copy_reference}; +use virtual_fs::{ + FileSystem, FsError, OpenOptions, UnionFileSystem, VirtualFile, copy_reference, + tmp_fs::TmpFileSystem, +}; use wasmer_config::package::PackageId; use wasmer_wasix_types::{ types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO}, @@ -49,6 +53,7 @@ pub(crate) use self::inode_guard::{ InodeValFileReadGuard, InodeValFileWriteGuard, POLL_GUARD_MAX_RET, WasiStateFileGuard, }; pub use self::notification::NotificationInner; +use self::relative_path_hack::RelativeOrAbsolutePathHack; use crate::syscalls::map_io_err; use crate::{ALL_RIGHTS, bin_factory::BinaryPackage, state::PreopenedDir}; @@ -362,91 +367,105 @@ impl Default for WasiInodes { #[derive(Debug, Clone)] pub enum WasiFsRoot { - Sandbox(Arc), - Backing(Arc>), -} - -impl WasiFsRoot { - /// Merge the contents of a filesystem into this one. - #[tracing::instrument(level = "debug", skip_all)] - pub(crate) async fn merge( - &self, - other: &Arc, - ) -> Result<(), virtual_fs::FsError> { - match self { - WasiFsRoot::Sandbox(fs) => { - fs.union(other); - Ok(()) - } - WasiFsRoot::Backing(fs) => { - merge_filesystems(other, fs).await?; - Ok(()) - } - } - } + Sandbox(TmpFileSystem), + /// Dedicated canonical overlay representation. + /// + /// Overlays are the common form of the file system for WASIX packages. + /// Dependencies are all added to the overlay, with the regular file system + /// as the foundation. + /// + /// This dedicated variant is necessary to norm the behaviour and prevent + /// redundant recursive merging of filesystems when additional dependencies + /// are added. + Overlay( + Arc< + virtual_fs::OverlayFileSystem< + TmpFileSystem, + [RelativeOrAbsolutePathHack; 1], + >, + >, + ), + Backing(Arc), } impl FileSystem for WasiFsRoot { fn readlink(&self, path: &Path) -> virtual_fs::Result { match self { - WasiFsRoot::Sandbox(fs) => fs.readlink(path), - WasiFsRoot::Backing(fs) => fs.readlink(path), + Self::Sandbox(fs) => fs.readlink(path), + Self::Overlay(overlay) => overlay.readlink(path), + Self::Backing(fs) => fs.readlink(path), } } fn read_dir(&self, path: &Path) -> virtual_fs::Result { match self { - WasiFsRoot::Sandbox(fs) => fs.read_dir(path), - WasiFsRoot::Backing(fs) => fs.read_dir(path), + Self::Sandbox(fs) => fs.read_dir(path), + Self::Overlay(overlay) => overlay.read_dir(path), + Self::Backing(fs) => fs.read_dir(path), } } + fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { match self { - WasiFsRoot::Sandbox(fs) => fs.create_dir(path), - WasiFsRoot::Backing(fs) => fs.create_dir(path), + Self::Sandbox(fs) => fs.create_dir(path), + Self::Overlay(overlay) => overlay.create_dir(path), + Self::Backing(fs) => fs.create_dir(path), } } + fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { match self { - WasiFsRoot::Sandbox(fs) => fs.remove_dir(path), - WasiFsRoot::Backing(fs) => fs.remove_dir(path), + Self::Sandbox(fs) => fs.remove_dir(path), + Self::Overlay(overlay) => overlay.remove_dir(path), + Self::Backing(fs) => fs.remove_dir(path), } } + fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> { let from = from.to_owned(); let to = to.to_owned(); let this = self.clone(); Box::pin(async move { match this { - WasiFsRoot::Sandbox(fs) => fs.rename(&from, &to).await, - WasiFsRoot::Backing(fs) => fs.rename(&from, &to).await, + Self::Sandbox(fs) => fs.rename(&from, &to).await, + Self::Overlay(overlay) => overlay.rename(&from, &to).await, + Self::Backing(fs) => fs.rename(&from, &to).await, } }) } + fn metadata(&self, path: &Path) -> virtual_fs::Result { match self { - WasiFsRoot::Sandbox(fs) => fs.metadata(path), - WasiFsRoot::Backing(fs) => fs.metadata(path), + Self::Sandbox(fs) => fs.metadata(path), + Self::Overlay(overlay) => overlay.metadata(path), + Self::Backing(fs) => fs.metadata(path), } } + fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { match self { - WasiFsRoot::Sandbox(fs) => fs.symlink_metadata(path), - WasiFsRoot::Backing(fs) => fs.symlink_metadata(path), + Self::Sandbox(fs) => fs.symlink_metadata(path), + Self::Overlay(overlay) => overlay.symlink_metadata(path), + Self::Backing(fs) => fs.symlink_metadata(path), } } + fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { match self { - WasiFsRoot::Sandbox(fs) => fs.remove_file(path), - WasiFsRoot::Backing(fs) => fs.remove_file(path), + Self::Sandbox(fs) => fs.remove_file(path), + Self::Overlay(overlay) => overlay.remove_file(path), + Self::Backing(fs) => fs.remove_file(path), } } + fn new_open_options(&self) -> OpenOptions<'_> { match self { - WasiFsRoot::Sandbox(fs) => fs.new_open_options(), - WasiFsRoot::Backing(fs) => fs.new_open_options(), + Self::Sandbox(fs) => fs.new_open_options(), + Self::Overlay(overlay) => overlay.new_open_options(), + Self::Backing(fs) => fs.new_open_options(), } } + fn mount( &self, name: String, @@ -454,20 +473,25 @@ impl FileSystem for WasiFsRoot { fs: Box, ) -> virtual_fs::Result<()> { match self { - WasiFsRoot::Sandbox(f) => f.mount(name, path, fs), - WasiFsRoot::Backing(f) => f.mount(name, path, fs), + Self::Sandbox(root) => FileSystem::mount(root, name, path, fs), + Self::Overlay(overlay) => FileSystem::mount(overlay.primary(), name, path, fs), + Self::Backing(f) => f.mount(name, path, fs), } } } /// Merge the contents of one filesystem into another. /// +/// NOTE: merging is a very expensive operation, since it requires copying +/// many files in memory, even if the underlying files are immutable and +/// mapped through mmap or similar mechanisms. +/// Merging should be avoided when possible. #[tracing::instrument(level = "trace", skip_all)] async fn merge_filesystems( source: &dyn FileSystem, destination: &dyn FileSystem, ) -> Result<(), virtual_fs::FsError> { - tracing::debug!("Falling back to a recursive copy to merge filesystems"); + tracing::warn!("Falling back to a recursive copy to merge filesystems"); let files = futures::stream::FuturesUnordered::new(); let mut to_check = VecDeque::new(); @@ -638,15 +662,28 @@ impl WasiFs { &self, binary: &BinaryPackage, ) -> Result<(), virtual_fs::FsError> { - let needs_to_be_unioned = self.has_unioned.lock().unwrap().insert(binary.id.clone()); + let Some(webc_fs) = &binary.webc_fs else { + return Ok(()); + }; + let needs_to_be_unioned = self.has_unioned.lock().unwrap().insert(binary.id.clone()); if !needs_to_be_unioned { return Ok(()); } - self.root_fs.merge(&binary.webc_fs).await?; - - Ok(()) + match &self.root_fs { + WasiFsRoot::Sandbox(fs) => { + // TODO: this can be changed to switch to Self::Overlay instead! + let fdyn: Arc = webc_fs.clone(); + fs.union(&fdyn); + Ok(()) + } + WasiFsRoot::Overlay(overlay) => { + let union = &overlay.secondaries()[0]; + union.0.merge(webc_fs, virtual_fs::UnionMergeMode::Skip) + } + WasiFsRoot::Backing(backing) => merge_filesystems(webc_fs, backing).await, + } } /// Created for the builder API. like `new` but with more information @@ -2211,14 +2248,14 @@ impl std::fmt::Debug for WasiFs { } /// Returns the default filesystem backing -pub fn default_fs_backing() -> Box { +pub fn default_fs_backing() -> Arc { cfg_if::cfg_if! { if #[cfg(feature = "host-fs")] { - Box::new(virtual_fs::host_fs::FileSystem::new(tokio::runtime::Handle::current(), "/").unwrap()) + Arc::new(virtual_fs::host_fs::FileSystem::new(tokio::runtime::Handle::current(), "/").unwrap()) } else if #[cfg(not(feature = "host-fs"))] { - Box::::default() + Arc::::default() } else { - Box::::default() + Arc::::default() } } } diff --git a/lib/wasix/src/fs/relative_path_hack.rs b/lib/wasix/src/fs/relative_path_hack.rs new file mode 100644 index 00000000000..40d6145d7b4 --- /dev/null +++ b/lib/wasix/src/fs/relative_path_hack.rs @@ -0,0 +1,91 @@ +use std::{path::Path, sync::Arc}; + +use futures::future::BoxFuture; +use virtual_fs::{FileSystem, FsError, OpenOptions, OpenOptionsConfig}; + +#[derive(Debug)] +pub struct RelativeOrAbsolutePathHack(pub F); + +impl RelativeOrAbsolutePathHack { + fn execute(&self, path: &Path, operation: Func) -> Result + where + Func: Fn(&F, &Path) -> Result, + { + // First, try it with the path we were given + let result = operation(&self.0, path); + + if result.is_err() && !path.is_absolute() { + // we were given a relative path, but maybe the operation will work + // using absolute paths instead. + let path = Path::new("/").join(path); + operation(&self.0, &path) + } else { + result + } + } +} + +impl virtual_fs::FileSystem for RelativeOrAbsolutePathHack { + fn readlink(&self, path: &Path) -> virtual_fs::Result { + self.execute(path, |fs, p| fs.readlink(p)) + } + + fn read_dir(&self, path: &Path) -> virtual_fs::Result { + self.execute(path, |fs, p| fs.read_dir(p)) + } + + fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.execute(path, |fs, p| fs.create_dir(p)) + } + + fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.execute(path, |fs, p| fs.remove_dir(p)) + } + + fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> { + let from = from.to_owned(); + let to = to.to_owned(); + Box::pin(async move { self.0.rename(&from, &to).await }) + } + + fn metadata(&self, path: &Path) -> virtual_fs::Result { + self.execute(path, |fs, p| fs.metadata(p)) + } + + fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { + self.execute(path, |fs, p| fs.symlink_metadata(p)) + } + + fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { + self.execute(path, |fs, p| fs.remove_file(p)) + } + + fn new_open_options(&self) -> OpenOptions<'_> { + virtual_fs::OpenOptions::new(self) + } + + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> virtual_fs::Result<()> { + let name_ref = &name; + let f_ref = &Arc::new(fs); + self.execute(path, move |f, p| { + f.mount(name_ref.clone(), p, Box::new(f_ref.clone())) + }) + } +} + +impl virtual_fs::FileOpener for RelativeOrAbsolutePathHack { + fn open( + &self, + path: &Path, + conf: &OpenOptionsConfig, + ) -> virtual_fs::Result> { + self.execute(path, |fs, p| { + fs.new_open_options().options(conf.clone()).open(p) + }) + } +} diff --git a/lib/wasix/src/runners/wasi.rs b/lib/wasix/src/runners/wasi.rs index 98accf33d7b..a41496d4d28 100644 --- a/lib/wasix/src/runners/wasi.rs +++ b/lib/wasix/src/runners/wasi.rs @@ -291,7 +291,8 @@ impl WasiRunner { builder.add_webc(pkg.clone()); builder.set_module_hash(pkg.hash()); builder.include_packages(pkg.package_ids.clone()); - Some(Arc::clone(&pkg.webc_fs)) + + pkg.webc_fs.as_deref().map(|fs| fs.duplicate()) } PackageOrHash::Hash(hash) => { builder.set_module_hash(hash); diff --git a/lib/wasix/src/runners/wasi_common.rs b/lib/wasix/src/runners/wasi_common.rs index 367c8ccd95f..2dd326a0a57 100644 --- a/lib/wasix/src/runners/wasi_common.rs +++ b/lib/wasix/src/runners/wasi_common.rs @@ -5,15 +5,17 @@ use std::{ }; use anyhow::{Context, Error}; -use futures::future::BoxFuture; use tokio::runtime::Handle; -use virtual_fs::{FileSystem, FsError, OverlayFileSystem, RootFileSystemBuilder, TmpFileSystem}; +use virtual_fs::{ + FileSystem, OverlayFileSystem, RootFileSystemBuilder, TmpFileSystem, UnionFileSystem, +}; use webc::metadata::annotations::Wasi as WasiAnnotation; use crate::{ WasiEnvBuilder, bin_factory::BinaryPackage, capabilities::Capabilities, + fs::{WasiFsRoot, relative_path_hack::RelativeOrAbsolutePathHack}, journal::{DynJournal, DynReadableJournal, SnapshotTrigger}, }; @@ -51,7 +53,7 @@ impl CommonWasiOptions { pub(crate) fn prepare_webc_env( &self, builder: &mut WasiEnvBuilder, - container_fs: Option>, + container_fs: Option, wasi: &WasiAnnotation, root_fs: Option, ) -> Result<(), anyhow::Error> { @@ -78,7 +80,7 @@ impl CommonWasiOptions { builder.add_preopen_dir("/")?; - builder.set_fs(Box::new(fs)); + builder.set_fs_root(fs); for pkg in &self.injected_packages { builder.add_webc(pkg.clone()); @@ -199,8 +201,8 @@ fn build_directory_mappings( fn prepare_filesystem( mut root_fs: TmpFileSystem, mounted_dirs: &[MountedDirectory], - container_fs: Option>, -) -> Result, Error> { + container_fs: Option, +) -> Result { if !mounted_dirs.is_empty() { build_directory_mappings(&mut root_fs, mounted_dirs)?; } @@ -217,10 +219,9 @@ fn prepare_filesystem( let fs = if let Some(container) = container_fs { let container = RelativeOrAbsolutePathHack(container); let fs = OverlayFileSystem::new(root_fs, [container]); - Box::new(fs) as Box + WasiFsRoot::Overlay(Arc::new(fs)) } else { - let fs = RelativeOrAbsolutePathHack(root_fs); - Box::new(fs) as Box + WasiFsRoot::Sandbox(root_fs) }; Ok(fs) @@ -309,100 +310,12 @@ impl From for MountedDirectory { } } -#[derive(Debug)] -struct RelativeOrAbsolutePathHack(F); - -impl RelativeOrAbsolutePathHack { - fn execute(&self, path: &Path, operation: Func) -> Result - where - Func: Fn(&F, &Path) -> Result, - { - // First, try it with the path we were given - let result = operation(&self.0, path); - - if result.is_err() && !path.is_absolute() { - // we were given a relative path, but maybe the operation will work - // using absolute paths instead. - let path = Path::new("/").join(path); - operation(&self.0, &path) - } else { - result - } - } -} - -impl virtual_fs::FileSystem for RelativeOrAbsolutePathHack { - fn readlink(&self, path: &Path) -> virtual_fs::Result { - self.execute(path, |fs, p| fs.readlink(p)) - } - - fn read_dir(&self, path: &Path) -> virtual_fs::Result { - self.execute(path, |fs, p| fs.read_dir(p)) - } - - fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { - self.execute(path, |fs, p| fs.create_dir(p)) - } - - fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { - self.execute(path, |fs, p| fs.remove_dir(p)) - } - - fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> { - let from = from.to_owned(); - let to = to.to_owned(); - Box::pin(async move { self.0.rename(&from, &to).await }) - } - - fn metadata(&self, path: &Path) -> virtual_fs::Result { - self.execute(path, |fs, p| fs.metadata(p)) - } - - fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { - self.execute(path, |fs, p| fs.symlink_metadata(p)) - } - - fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { - self.execute(path, |fs, p| fs.remove_file(p)) - } - - fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> { - virtual_fs::OpenOptions::new(self) - } - - fn mount( - &self, - name: String, - path: &Path, - fs: Box, - ) -> virtual_fs::Result<()> { - let name_ref = &name; - let f_ref = &Arc::new(fs); - self.execute(path, move |f, p| { - f.mount(name_ref.clone(), p, Box::new(f_ref.clone())) - }) - } -} - -impl virtual_fs::FileOpener for RelativeOrAbsolutePathHack { - fn open( - &self, - path: &Path, - conf: &virtual_fs::OpenOptionsConfig, - ) -> virtual_fs::Result> { - self.execute(path, |fs, p| { - fs.new_open_options().options(conf.clone()).open(p) - }) - } -} - #[cfg(test)] mod tests { use std::time::SystemTime; use tempfile::TempDir; - use virtual_fs::{DirEntry, FileType, Metadata, WebcVolumeFileSystem}; - use wasmer_package::utils::from_bytes; + use virtual_fs::{DirEntry, FileType, Metadata}; use super::*; @@ -416,7 +329,6 @@ mod tests { ..Default::default() }; let mut builder = WasiEnvBuilder::new("program-name"); - let fs = Arc::new(virtual_fs::EmptyFileSystem::default()); let mut annotations = WasiAnnotation::new("some-atom"); annotations.main_args = Some(vec![ "hard".to_string(), @@ -424,7 +336,7 @@ mod tests { "args".to_string(), ]); - args.prepare_webc_env(&mut builder, Some(fs), &annotations, None) + args.prepare_webc_env(&mut builder, None, &annotations, None) .unwrap(); assert_eq!( @@ -452,11 +364,10 @@ mod tests { ..Default::default() }; let mut builder = WasiEnvBuilder::new("python"); - let fs = Arc::new(virtual_fs::EmptyFileSystem::default()); let mut annotations = WasiAnnotation::new("python"); annotations.env = Some(vec!["HARD_CODED=env-vars".to_string()]); - args.prepare_webc_env(&mut builder, Some(fs), &annotations, None) + args.prepare_webc_env(&mut builder, None, &annotations, None) .unwrap(); assert_eq!( @@ -468,6 +379,11 @@ mod tests { ); } + fn unix_timestamp_nanos(instant: SystemTime) -> Option { + let duration = instant.duration_since(SystemTime::UNIX_EPOCH).ok()?; + Some(duration.as_nanos() as u64) + } + #[tokio::test] #[cfg_attr(not(feature = "host-fs"), ignore)] async fn python_use_case() { @@ -479,29 +395,39 @@ mod tests { guest: "/home".to_string(), host: sub_dir, })]; - let container = from_bytes(PYTHON).unwrap(); - let webc_fs = WebcVolumeFileSystem::mount_all(&container); + let container = wasmer_package::utils::from_bytes(PYTHON).unwrap(); + let webc_fs = virtual_fs::WebcVolumeFileSystem::mount_all(&container); + let union_fs = UnionFileSystem::new(); + union_fs + .mount("webc".to_string(), Path::new("/"), Box::new(webc_fs)) + .unwrap(); let root_fs = RootFileSystemBuilder::default().build(); - let fs = prepare_filesystem(root_fs, &mapping, Some(Arc::new(webc_fs))).unwrap(); - - assert!(fs.metadata("/home/file.txt".as_ref()).unwrap().is_file()); - assert!(fs.metadata("lib".as_ref()).unwrap().is_dir()); - assert!( - fs.metadata("lib/python3.6/collections/__init__.py".as_ref()) - .unwrap() - .is_file() - ); - assert!( - fs.metadata("lib/python3.6/encodings/__init__.py".as_ref()) - .unwrap() - .is_file() - ); - } - - fn unix_timestamp_nanos(instant: SystemTime) -> Option { - let duration = instant.duration_since(SystemTime::UNIX_EPOCH).ok()?; - Some(duration.as_nanos() as u64) + let fs = prepare_filesystem(root_fs, &mapping, Some(union_fs)).unwrap(); + + assert!(matches!(fs, WasiFsRoot::Overlay(_))); + if let WasiFsRoot::Overlay(overlay_fs) = &fs { + use virtual_fs::FileSystem; + assert!( + overlay_fs + .metadata("/home/file.txt".as_ref()) + .unwrap() + .is_file() + ); + assert!(overlay_fs.metadata("lib".as_ref()).unwrap().is_dir()); + assert!( + overlay_fs + .metadata("lib/python3.6/collections/__init__.py".as_ref()) + .unwrap() + .is_file() + ); + assert!( + overlay_fs + .metadata("lib/python3.6/encodings/__init__.py".as_ref()) + .unwrap() + .is_file() + ); + } } #[tokio::test] diff --git a/lib/wasix/src/runners/wcgi/runner.rs b/lib/wasix/src/runners/wcgi/runner.rs index 59a793105b9..d9626ac4287 100644 --- a/lib/wasix/src/runners/wcgi/runner.rs +++ b/lib/wasix/src/runners/wcgi/runner.rs @@ -70,12 +70,13 @@ impl WcgiRunner { None => default_dialect, }; - let container_fs = Arc::clone(&pkg.webc_fs); + let container_fs = pkg.webc_fs.clone(); let wasi_common = self.config.wasi.clone(); let rt = Arc::clone(&runtime); let setup_builder = move |builder: &mut WasiEnvBuilder| { - wasi_common.prepare_webc_env(builder, Some(Arc::clone(&container_fs)), &wasi, None)?; + let container_fs = container_fs.as_ref().map(|x| x.duplicate()); + wasi_common.prepare_webc_env(builder, container_fs, &wasi, None)?; builder.set_runtime(Arc::clone(&rt)); Ok(()) }; diff --git a/lib/wasix/src/runtime/package_loader/load_package_tree.rs b/lib/wasix/src/runtime/package_loader/load_package_tree.rs index 3adb83a094d..d8f5dd4ed62 100644 --- a/lib/wasix/src/runtime/package_loader/load_package_tree.rs +++ b/lib/wasix/src/runtime/package_loader/load_package_tree.rs @@ -9,7 +9,7 @@ use anyhow::{Context, Error}; use futures::{StreamExt, TryStreamExt, future::BoxFuture}; use once_cell::sync::OnceCell; use petgraph::visit::EdgeRef; -use virtual_fs::{FileSystem, OverlayFileSystem, UnionFileSystem, WebcVolumeFileSystem}; +use virtual_fs::{FileSystem, UnionFileSystem, WebcVolumeFileSystem}; use wasmer_config::package::{PackageId, SuggestedCompilerOptimizations}; use wasmer_package::utils::wasm_annotations_to_features; use webc::metadata::annotations::Atom as AtomAnnotation; @@ -62,13 +62,17 @@ pub async fn load_package_tree( let mut containers = fetch_dependencies(loader, &resolution.package, &resolution.graph).await?; containers.insert(resolution.package.root_package.clone(), root.clone()); let package_ids = containers.keys().cloned().collect(); - let fs = filesystem(&containers, &resolution.package, root_is_local_dir)?; + let fs_opt = filesystem(&containers, &resolution.package, root_is_local_dir)?; let root = &resolution.package.root_package; let commands: Vec = commands(&resolution.package.commands, &containers, resolution)?; - let file_system_memory_footprint = count_file_system(&fs, Path::new("/")); + let file_system_memory_footprint = if let Some(fs) = &fs_opt { + count_file_system(fs, Path::new("/")) + } else { + 0 + }; let loaded = BinaryPackage { id: root.clone(), @@ -81,7 +85,7 @@ pub async fn load_package_tree( .map(|ts| ts as u128), hash: OnceCell::new(), entrypoint_cmd: resolution.package.entrypoint.clone(), - webc_fs: Arc::new(fs), + webc_fs: fs_opt.map(Arc::new), commands, uses: Vec::new(), file_system_memory_footprint, @@ -398,18 +402,15 @@ fn count_file_system(fs: &dyn FileSystem, path: &Path) -> u64 { /// Given a set of [`ResolvedFileSystemMapping`]s and the [`Container`] for each /// package in a dependency tree, construct the resulting filesystem. +/// +/// Returns `Ok(None)` if no filesystem mappings were specified. fn filesystem( packages: &HashMap, pkg: &ResolvedPackage, root_is_local_dir: bool, -) -> Result, Error> { +) -> Result, Error> { if pkg.filesystem.is_empty() { - return Ok(Box::new(OverlayFileSystem::< - virtual_fs::EmptyFileSystem, - Vec, - >::new( - virtual_fs::EmptyFileSystem::default(), vec![] - ))); + return Ok(None); } let mut found_v2 = None; @@ -423,17 +424,31 @@ fn filesystem( ) })?; - if container.version() == webc::Version::V2 && found_v2.is_none() { - found_v2 = Some(package.clone()); - } - if container.version() == webc::Version::V3 && found_v3.is_none() { - found_v3 = Some(package.clone()); + match container.version() { + webc::Version::V1 => { + anyhow::bail!( + "the package '{package}' is a webc v1 package, but webc v1 support was removed" + ); + } + webc::Version::V2 => { + if found_v2.is_none() { + found_v2 = Some(package.clone()); + } + } + webc::Version::V3 => { + if found_v3.is_none() { + found_v3 = Some(package.clone()); + } + } + other => { + anyhow::bail!("the package '{package}' has an unknown webc version: {other}"); + } } } match (found_v2, found_v3) { - (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir), - (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir), + (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir).map(Some), + (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir).map(Some), (Some(v2), Some(v3)) => { anyhow::bail!( "Mix of webc v2 and v3 in the same dependency tree is not supported; v2: {v2}, v3: {v3}" @@ -448,7 +463,7 @@ fn filesystem_v3( packages: &HashMap, pkg: &ResolvedPackage, root_is_local_dir: bool, -) -> Result, Error> { +) -> Result { let mut volumes: HashMap<&PackageId, BTreeMap> = HashMap::new(); let mut mountings: Vec<_> = pkg.filesystem.iter().collect(); @@ -490,9 +505,7 @@ fn filesystem_v3( union_fs.mount(volume_name.clone(), mount_path, Box::new(webc_vol))?; } - let fs = OverlayFileSystem::new(virtual_fs::EmptyFileSystem::default(), [union_fs]); - - Ok(Box::new(fs)) + Ok(union_fs) } /// Build the filesystem for webc v2 packages. @@ -522,13 +535,14 @@ fn filesystem_v2( packages: &HashMap, pkg: &ResolvedPackage, root_is_local_dir: bool, -) -> Result, Error> { - let mut filesystems = Vec::new(); +) -> Result { let mut volumes: HashMap<&PackageId, BTreeMap> = HashMap::new(); let mut mountings: Vec<_> = pkg.filesystem.iter().collect(); mountings.sort_by_key(|m| std::cmp::Reverse(m.mount_path.as_path())); + let union_fs = UnionFileSystem::new(); + for ResolvedFileSystemMapping { mount_path, volume_name, @@ -561,7 +575,7 @@ fn filesystem_v2( format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume") })?; - let mount_path = mount_path.clone(); + let mount_path_clone = mount_path.clone(); // Get a filesystem which will map "$mount_dir/some-path" to // "$original_path/some-path" on the original volume let fs = if let Some(original) = original_path { @@ -571,7 +585,7 @@ fn filesystem_v2( WebcVolumeFileSystem::new(volume.clone()), Box::new(move |path: &Path| { let without_mount_dir = path - .strip_prefix(&mount_path) + .strip_prefix(&mount_path_clone) .map_err(|_| virtual_fs::FsError::BaseNotDirectory)?; Ok(original.join(without_mount_dir)) }) as DynPathMapper, @@ -581,26 +595,21 @@ fn filesystem_v2( WebcVolumeFileSystem::new(volume.clone()), Box::new(move |path: &Path| { let without_mount_dir = path - .strip_prefix(&mount_path) + .strip_prefix(&mount_path_clone) .map_err(|_| virtual_fs::FsError::BaseNotDirectory)?; Ok(without_mount_dir.to_owned()) }) as DynPathMapper, ) }; - filesystems.push(fs); + union_fs.mount(volume_name.clone(), mount_path, Box::new(fs))?; } - let fs = OverlayFileSystem::new(virtual_fs::EmptyFileSystem::default(), filesystems); - - Ok(Box::new(fs)) + Ok(union_fs) } type DynPathMapper = Box Result + Send + Sync>; -/// A [`FileSystem`] implementation that lets you map the [`Path`] to something -/// else. -#[derive(Clone, PartialEq)] struct MappedPathFileSystem { inner: F, map: M, diff --git a/lib/wasix/src/state/builder.rs b/lib/wasix/src/state/builder.rs index 9c68a0d8b04..4f2c7745b18 100644 --- a/lib/wasix/src/state/builder.rs +++ b/lib/wasix/src/state/builder.rs @@ -698,20 +698,24 @@ impl WasiEnvBuilder { /// Sets the FileSystem to be used with this WASI instance. /// /// This is usually used in case a custom `virtual_fs::FileSystem` is needed. - pub fn fs(mut self, fs: Box) -> Self { + pub fn fs(mut self, fs: impl Into>) -> Self { self.set_fs(fs); self } - pub fn set_fs(&mut self, fs: Box) { - self.fs = Some(WasiFsRoot::Backing(Arc::new(fs))); + pub fn set_fs(&mut self, fs: impl Into>) { + self.fs = Some(WasiFsRoot::Backing(fs.into())); + } + + pub(crate) fn set_fs_root(&mut self, fs: WasiFsRoot) { + self.fs = Some(fs); } /// Sets a new sandbox FileSystem to be used with this WASI instance. /// /// This is usually used in case a custom `virtual_fs::FileSystem` is needed. pub fn sandbox_fs(mut self, fs: TmpFileSystem) -> Self { - self.fs = Some(WasiFsRoot::Sandbox(Arc::new(fs))); + self.fs = Some(WasiFsRoot::Sandbox(fs)); self } @@ -843,7 +847,7 @@ impl WasiEnvBuilder { let fs_backing = self .fs .take() - .unwrap_or_else(|| WasiFsRoot::Sandbox(Arc::new(TmpFileSystem::new()))); + .unwrap_or_else(|| WasiFsRoot::Sandbox(TmpFileSystem::new())); if let Some(dir) = &self.current_dir { match fs_backing.read_dir(dir) { diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 46dee1fbe49..739adccdf3b 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -1083,6 +1083,32 @@ impl WasiEnv { continue; } } + WasiFsRoot::Overlay(ofs) => { + let root_fs = ofs.primary(); + + if let Err(err) = root_fs + .new_open_options_ext() + .insert_ro_file(path, atom.clone()) + { + tracing::debug!( + "failed to add package [{}] command [{}] - {}", + pkg.id, + command.name(), + err + ); + continue; + } + if let Err(err) = root_fs.new_open_options_ext().insert_ro_file(path2, atom) + { + tracing::debug!( + "failed to add package [{}] command [{}] - {}", + pkg.id, + command.name(), + err + ); + continue; + } + } WasiFsRoot::Backing(fs) => { // FIXME: we're counting on the fs being a mem_fs here. Otherwise, memory // usage will be very high. diff --git a/tests/lib/wast/src/wasi_wast.rs b/tests/lib/wast/src/wasi_wast.rs index f53863c193e..8089d0aa7df 100644 --- a/tests/lib/wast/src/wasi_wast.rs +++ b/tests/lib/wast/src/wasi_wast.rs @@ -239,19 +239,19 @@ impl<'a> WasiTest<'a> { host_temp_dirs_to_not_drop.push(temp_dir); } - builder.set_fs(Box::new(fs)); + builder.set_fs(Arc::new(fs) as Arc); } other => { - let fs: Box = match other { - WasiFileSystemKind::InMemory => Box::::default(), - WasiFileSystemKind::Tmp => Box::::default(), + let fs: Arc = match other { + WasiFileSystemKind::InMemory => Arc::::default(), + WasiFileSystemKind::Tmp => Arc::::default(), WasiFileSystemKind::PassthruMemory => { - let fs = Box::::default(); - Box::new(passthru_fs::PassthruFileSystem::new(fs)) + let fs = Arc::::default(); + Arc::new(passthru_fs::PassthruFileSystem::new_arc(fs)) } WasiFileSystemKind::RootFileSystemBuilder => { - Box::new(RootFileSystemBuilder::new().build()) + Arc::new(RootFileSystemBuilder::new().build()) } WasiFileSystemKind::UnionHostMemory => { let a = mem_fs::FileSystem::default(); @@ -294,7 +294,7 @@ impl<'a> WasiTest<'a> { Box::new(f), )?; - Box::new(union) + Arc::new(union) } _ => { panic!("unexpected filesystem type {other:?}");