diff --git a/Cargo.lock b/Cargo.lock index eaf6b40..b0c82fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,7 +128,7 @@ dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -279,7 +279,7 @@ dependencies = [ "async-lock", "async-task", "atomic-waker", - "fastrand", + "fastrand 1.9.0", "futures-lite", "log", ] @@ -415,6 +415,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -618,6 +628,50 @@ dependencies = [ "typenum", ] +[[package]] +name = "cxx" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f68e12e817cb19eaab81aaec582b4052d07debd3c3c6b083b9d361db47c7dc9d" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e789217e4ab7cf8cc9ce82253180a9fe331f35f5d339f0ccfe0270b39433f397" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.25", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a19f4c80fd9ab6c882286fa865e92e07688f4387370a209508014ead8751d0" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fcfa71f66c8563c4fa9dd2bb68368d50267856f831ac5d85367e0805f9606c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.25", +] + [[package]] name = "debugid" version = "0.8.0" @@ -754,6 +808,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "fd-lock" version = "4.0.0" @@ -836,7 +896,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1146,6 +1206,15 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "link-cplusplus" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1421,6 +1490,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -1428,7 +1506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] @@ -1515,6 +1593,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.14" @@ -1527,6 +1611,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" + [[package]] name = "semver" version = "1.0.17" @@ -1664,6 +1754,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" @@ -1708,6 +1817,19 @@ version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8e77cb757a61f51b947ec4a7e3646efd825b73561db1c232a8ccb639e611a0" +[[package]] +name = "tempfile" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +dependencies = [ + "cfg-if", + "fastrand 2.0.0", + "redox_syscall 0.3.5", + "rustix 0.38.4", + "windows-sys", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -2029,6 +2151,7 @@ dependencies = [ "walrus", "wasm-compose", "wasm-metadata 0.9.0 (git+https://github.com/bytecodealliance/wasm-tools)", + "wasm-opt", "wasmtime", "wasmtime-wasi", "wit-component 0.12.0 (git+https://github.com/bytecodealliance/wasm-tools)", @@ -2173,6 +2296,46 @@ dependencies = [ "wasmparser 0.108.0 (git+https://github.com/bytecodealliance/wasm-tools)", ] +[[package]] +name = "wasm-opt" +version = "0.113.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65a2799e08026234b07b44da6363703974e75be21430cef00756bbc438c8ff8a" +dependencies = [ + "anyhow", + "libc", + "strum", + "strum_macros", + "tempfile", + "thiserror", + "wasm-opt-cxx-sys", + "wasm-opt-sys", +] + +[[package]] +name = "wasm-opt-cxx-sys" +version = "0.113.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d26f86d1132245e8bcea8fac7f02b10fb885b6696799969c94d7d3c14db5e1" +dependencies = [ + "anyhow", + "cxx", + "cxx-build", + "wasm-opt-sys", +] + +[[package]] +name = "wasm-opt-sys" +version = "0.113.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497d069cd3420cdd52154a320b901114a20946878e2de62c670f9d906e472370" +dependencies = [ + "anyhow", + "cc", + "cxx", + "cxx-build", +] + [[package]] name = "wasmparser" version = "0.80.2" diff --git a/Cargo.toml b/Cargo.toml index e86756e..a834f7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ serde = { version = "1", features = ["derive"] } toml = "0.7" walrus = "0.20.1" wasm-metadata = { git = "https://github.com/bytecodealliance/wasm-tools" } +wasm-opt = "0.113.0" wit-component = { git = "https://github.com/bytecodealliance/wasm-tools" } [build-dependencies] diff --git a/README.md b/README.md index 4b09052..e22302c 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,30 @@ Subsystem support: While current virtualization support is limited, the goal for this project is to support a wide range of WASI virtualization use cases. -### Example +### Explainer + +When wanting to run WebAssembly Components depending on WASI APIs in other environments it can provide +a point of friction having to port WASI interop to every target platform. + +In addition having full unrestricted access to core operating system APIs is a security concern. + +WASI Virt allows taking a component that depends on WASI APIs and using a virtualized adapter to convert +it into a component that no longer depends on those WASI APIs, or conditionally only depends on them in +a configurable way. + +For example, consider converting an application to a WebAssembly Component that assumes it can load +some templates from the filesystem, but that is all it will load. + +Using WASI Virt, those specific file paths can be mounted and virtualized into the component itself as +a post-compile operation, while banning the final component from being able to access the filesystem at +all. The inner program still uses FS calls, but they are virtualized from the target host platform allowing +this application to run in different environments without filesystem API compat or security concerns. + +### Basic Usage ```rs use std::fs; -use wasi_virt::WasiVirt; +use wasi_virt::{WasiVirt, FsEntry}; fn main() { let virt_component_bytes = WasiVirt::new() @@ -37,8 +56,19 @@ fn main() { .env_host_allow(&["PUBLIC_ENV_VAR"]) // provide custom env overrides .env_overrides(&[("SOME", "ENV"), ("VAR", "OVERRIDES")]) - // virtualize a file - .fs_virt_file("/test.txt", "hello world") + // mount and virtualize a local directory recursively + .fs_preopen("/dir", FsEntry::Virtualize("/local/dir")) + // create a virtual directory containing some virtual files + .fs_preopen("/another-dir", FsEntry::Dir(BTreeMap::from([ + // create a virtual file from the given UTF8 source + ("file.txt", FsEntry::Source("Hello world")), + // create a virtual file read from a local file at + // virtualization time + ("another.wasm", FsEntry::Virtualize("/local/another.wasm")) + // create a virtual file which reads from a given file + // path at runtime using the runtime host filesystem API + ("host.txt", FsEntry::RuntimeFile("/runtime/host/path.txt")) + ]))) .create() .unwrap(); fs::write("virt.component.wasm", virt_component_bytes).unwrap(); @@ -51,6 +81,8 @@ With the created `virt.component.wasm` component, this can now be composed into wasm-tools compose mycomponent.wasm -d virt.component.wasm -o out.component.wasm ``` +When configuring a virtualization that does not fall back to the host, imports to the subsystem will be entirely stripped from the component. + ## CLI A CLI is also provided in this crate supporting: @@ -59,22 +91,41 @@ A CLI is also provided in this crate supporting: wasi-virt config.toml -o virt.wasm ``` +### Configuration + With the configuration file format: ``` +### Environment Virtualization [env] -# Support all env vars on the final host (apart from the overrides) -# Set to "none" to entirely encapsulate the host env -host = "all" -# Always ensures that this env var and value is set +### Set environment variable values: overrides = [["CUSTOM", "VAL"]] -``` - -Allow lists and deny lists can also be provided via: - -``` -[env.host] -allow = ["ENV_KEY"] # Or Deny = ... +### Enable environment vars for the host: +host = "all" +### Alternatively create an allow list: +# [env.host] +# allow = ["ENV_KEY"] +### or deny list: +# [env.host] +# deny = ["ENV_KEY"] + +### FS Virtualization + +### Create a virtual directory with file.txt from +### the provided inline UTF8 string, and with another.wasm +### inlined into the virtual adapter from the local filesystem +### path at virtualization time: +[fs.preopens."/".dir] +"file.txt" = { source = "inner contents" } +"another.wasm" = { virtualize = "/local/path/to/another.wasm" } + +### Mount a local directory as a virtualized directory: +[fs.preopens."/dir"] +virtualize = "/local/path" + +### Mount a passthrough runtime host directory: +[fs.preopens."/runtime-host"] +runtime = "/runtime/path" ``` # License diff --git a/lib/virtual_adapter.wasm b/lib/virtual_adapter.wasm index b8d1b92..0b14df2 100755 Binary files a/lib/virtual_adapter.wasm and b/lib/virtual_adapter.wasm differ diff --git a/src/lib.rs b/src/lib.rs index ee8d0ef..73e3422 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,13 @@ use anyhow::{Context, Result}; use serde::Deserialize; +use std::env; +use std::fs; +use std::time::SystemTime; use virt_env::{create_env_virt, strip_env_virt, VirtEnv}; use virt_fs::{create_fs_virt, strip_fs_virt, VirtFs}; use wasm_metadata::Producers; +use wasm_opt::Feature; +use wasm_opt::OptimizationOptions; use wit_component::metadata; use wit_component::ComponentEncoder; use wit_component::StringEncoding; @@ -19,6 +24,8 @@ pub struct VirtOpts { pub env: Option, /// Filesystem virtualization pub fs: Option, + /// Disable wasm-opt run if desired + pub wasm_opt: Option, } #[derive(Debug, Default, Clone)] @@ -101,7 +108,29 @@ pub fn create_virt<'a>(opts: &VirtOpts) -> Result { module.customs.add(component_section); - let bytes = module.emit_wasm(); + let mut bytes = module.emit_wasm(); + + // because we rely on dead code ellimination to remove unnecessary adapter code + // we save into a temporary file and run wasm-opt before returning + // this can be disabled with wasm_opt: false + if opts.wasm_opt.unwrap_or(true) { + let dir = env::temp_dir(); + let tmp_input = dir.join(format!("virt.core.input.{}.wasm", timestamp())); + let tmp_output = dir.join(format!("virt.core.output.{}.wasm", timestamp())); + fs::write(&tmp_input, bytes) + .with_context(|| "Unable to write temporary file for wasm-opt call on adapter")?; + OptimizationOptions::new_optimize_for_size_aggressively() + .enable_feature(Feature::ReferenceTypes) + .run(&tmp_input, &tmp_output) + .with_context(|| "Unable to apply wasm-opt optimization to virt. This can be disabled with wasm_opt: false.") + .or_else(|e| { + fs::remove_file(&tmp_input)?; + Err(e) + })?; + bytes = fs::read(&tmp_output)?; + fs::remove_file(&tmp_input)?; + fs::remove_file(&tmp_output)?; + } // now adapt the virtualized component let encoder = ComponentEncoder::default().validate(true).module(&bytes)?; @@ -112,3 +141,10 @@ pub fn create_virt<'a>(opts: &VirtOpts) -> Result { fs, }) } + +fn timestamp() -> u64 { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(n) => n.as_secs(), + Err(_) => panic!(), + } +} diff --git a/src/virt_fs.rs b/src/virt_fs.rs index a0e5789..03d8705 100644 --- a/src/virt_fs.rs +++ b/src/virt_fs.rs @@ -32,12 +32,13 @@ pub enum FsEntry { /// symlink absolute or relative file path on the virtual filesystem Symlink(String), /// host path at virtualization time - Host(String), + Virtualize(String), /// host path st runtime - Runtime(String), + RuntimeDir(String), + RuntimeFile(String), /// Virtual file File(Vec), - /// String convenience + /// String (UTF8) file source convenience Source(String), /// Virtual directory Dir(VirtDir), @@ -52,6 +53,24 @@ pub struct VirtFile { type VirtDir = BTreeMap; +impl WasiVirt { + fn get_or_create_fs(&mut self) -> &mut VirtFs { + self.virt_opts.fs.get_or_insert_with(Default::default) + } + + pub fn preopen(mut self, name: String, preopen: FsEntry) -> Self { + let fs = self.get_or_create_fs(); + fs.preopens.insert(name, preopen); + self + } + + pub fn passive_cutoff(mut self, passive_cutoff: usize) -> Self { + let fs = self.get_or_create_fs(); + fs.passive_cutoff = Some(passive_cutoff); + self + } +} + #[derive(Debug)] struct StaticIndexEntry { name: u32, @@ -103,9 +122,9 @@ union StaticFileData { /// Passive memory element index and len for PassiveFile passive: (u32, u32), - // TODO: Host passthrough mounts - // /// Host path string for HostDir / HostFile - // path: u32, + /// Host path string for HostDir / HostFile + host_path: u32, + /// Pointer and child entry count for Dir dir: (u32, u32), } @@ -194,7 +213,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result FsEntry::Source(source) => { *entry = FsEntry::File(source.as_bytes().to_vec()) }, - FsEntry::Host(host_path) => { + FsEntry::Virtualize(host_path) => { // read a directory or file path from the host let metadata = fs::metadata(&host_path)?; if metadata.is_dir() { @@ -206,7 +225,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result let mut full_path = host_path.clone(); full_path.push('/'); full_path.push_str(file_name_str); - entries.insert(file_name_str.into(), FsEntry::Host(full_path)); + entries.insert(file_name_str.into(), FsEntry::Virtualize(full_path)); } *entry = FsEntry::Dir(entries); } else { @@ -217,7 +236,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result *entry = FsEntry::File(bytes) } } - FsEntry::File(_) | FsEntry::Runtime(_) | FsEntry::Symlink(_) | FsEntry::Dir(_) => {} + FsEntry::File(_) | FsEntry::RuntimeFile(_) | FsEntry::RuntimeDir(_) | FsEntry::Symlink(_) | FsEntry::Dir(_) => {} } Ok(()) })?; @@ -225,6 +244,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result // Create the data section bytes let mut data_section = Data::new(get_stack_global(module)? as usize); + let mut host_passthrough = false; // Next we linearize the pre-order directory graph as the static file data // Using a pre-order traversal @@ -238,9 +258,24 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result let name_str_ptr = data_section.string(name)?; let (ty, data) = match &entry { // removed during previous step - FsEntry::Host(_) | FsEntry::Source(_) => unreachable!(), + FsEntry::Virtualize(_) | FsEntry::Source(_) => unreachable!(), FsEntry::Symlink(_) => todo!("symlink support"), - FsEntry::Runtime(_) => todo!("runtime passthrough mounts"), + FsEntry::RuntimeFile(path) => { + host_passthrough = true; + let str = data_section.string(path)?; + ( + StaticIndexType::RuntimeHostFile, + StaticFileData { host_path: str }, + ) + } + FsEntry::RuntimeDir(path) => { + host_passthrough = true; + let str = data_section.string(path)?; + ( + StaticIndexType::RuntimeHostDir, + StaticFileData { host_path: str }, + ) + } FsEntry::Dir(dir) => { let child_cnt = dir.len() as u32; // children will be visited next in preorder and contiguously @@ -309,7 +344,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result // If host fs is disabled, remove its imports entirely // replacing it with a stub panic - if true { + if !host_passthrough { stub_fs_virt(module)?; } @@ -317,7 +352,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result let preopen_addr = data_section.write_slice(preopen_indices.as_slice())?; - const FS_STATIC_LEN: usize = 12; + const FS_STATIC_LEN: usize = 16; if data.value.len() < data_offset + FS_STATIC_LEN { let padding = 4 - (data_offset + FS_STATIC_LEN) % 4; data.value.resize(data_offset + FS_STATIC_LEN + padding, 0); @@ -335,6 +370,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result // preopens: 0 as *const usize, // [byte 4] // static_index_cnt: 0, // [byte 8] // static_index: 0 as *const StaticIndexEntry, // [byte 12] + // host_passthrough: false, // [byte 16] // }; bytes[data_offset..data_offset + 4].copy_from_slice(&(fs.preopens.len() as u32).to_le_bytes()); bytes[data_offset + 4..data_offset + 8].copy_from_slice(&(preopen_addr as u32).to_le_bytes()); @@ -342,6 +378,9 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result .copy_from_slice(&(static_fs_data.len() as u32).to_le_bytes()); bytes[data_offset + 12..data_offset + 16] .copy_from_slice(&(static_index_addr as u32).to_le_bytes()); + if host_passthrough { + bytes[data_offset + 16..data_offset + 20].copy_from_slice(&(1 as u32).to_le_bytes()); + } data_section.finish(module)?; @@ -370,129 +409,129 @@ fn stub_fs_virt(module: &mut Module) -> Result<()> { false, )?; stub_imported_func(module, "wasi:filesystem/filesystem", "advise", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "sync_data", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "get_flags", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "get_type", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "set_size", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "set_times", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "sync-data", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "get-flags", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "get-type", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "set-size", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "set-times", false)?; stub_imported_func(module, "wasi:filesystem/filesystem", "read", false)?; stub_imported_func(module, "wasi:filesystem/filesystem", "write", false)?; stub_imported_func( module, "wasi:filesystem/filesystem", - "read_directory", + "read-directory", false, )?; stub_imported_func(module, "wasi:filesystem/filesystem", "sync", false)?; stub_imported_func( module, "wasi:filesystem/filesystem", - "create_directory_at", + "create-directory-at", false, )?; stub_imported_func(module, "wasi:filesystem/filesystem", "stat", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "stat_at", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "set_times_at", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "link_at", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "open_at", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "readlink_at", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "stat-at", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "set-times-at", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "link-at", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "open-at", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "readlink-at", false)?; stub_imported_func( module, "wasi:filesystem/filesystem", - "remove_directory_at", + "remove-directory-at", false, )?; - stub_imported_func(module, "wasi:filesystem/filesystem", "rename_at", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "symlink_at", false)?; - stub_imported_func(module, "wasi:filesystem/filesystem", "access_at", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "rename-at", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "symlink-at", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "access-at", false)?; stub_imported_func( module, "wasi:filesystem/filesystem", - "unlink_file_at", + "unlink-file-at", false, )?; stub_imported_func( module, "wasi:filesystem/filesystem", - "change_file_permissions_at", + "change-file-permissions-at", false, )?; stub_imported_func( module, "wasi:filesystem/filesystem", - "change_directory_permissions_at", + "change-directory-permissions-at", false, )?; - stub_imported_func(module, "wasi:filesystem/filesystem", "lock_shared", false)?; + stub_imported_func(module, "wasi:filesystem/filesystem", "lock-shared", false)?; stub_imported_func( module, "wasi:filesystem/filesystem", - "lock_exclusive", + "lock-exclusive", false, )?; stub_imported_func( module, "wasi:filesystem/filesystem", - "try_lock_shared", + "try-lock-shared", false, )?; stub_imported_func( module, "wasi:filesystem/filesystem", - "try_lock_exclusive", + "try-lock-exclusive", false, )?; stub_imported_func(module, "wasi:filesystem/filesystem", "unlock", false)?; stub_imported_func( module, "wasi:filesystem/filesystem", - "drop_descriptor", + "drop-descriptor", false, )?; stub_imported_func( module, "wasi:filesystem/filesystem", - "read_directory_entry", + "read-directory-entry", false, )?; stub_imported_func( module, "wasi:filesystem/filesystem", - "drop_directory_entry_stream", + "drop-directory-entry-stream", false, )?; stub_imported_func( module, "wasi:io/streams", - "drop_directory_entry_stream", + "drop-directory-entry-stream", false, )?; stub_imported_func(module, "wasi:io/streams", "read", false)?; - stub_imported_func(module, "wasi:io/streams", "blocking_read", false)?; + stub_imported_func(module, "wasi:io/streams", "blocking-read", false)?; stub_imported_func(module, "wasi:io/streams", "skip", false)?; - stub_imported_func(module, "wasi:io/streams", "blocking_skip", false)?; + stub_imported_func(module, "wasi:io/streams", "blocking-skip", false)?; stub_imported_func( module, "wasi:io/streams", - "subscribe_to_input_stream", + "subscribe-to-input-stream", false, )?; - stub_imported_func(module, "wasi:io/streams", "drop_input_stream", false)?; + stub_imported_func(module, "wasi:io/streams", "drop-input-stream", false)?; stub_imported_func(module, "wasi:io/streams", "write", false)?; - stub_imported_func(module, "wasi:io/streams", "blocking_write", false)?; - stub_imported_func(module, "wasi:io/streams", "write_zeroes", false)?; - stub_imported_func(module, "wasi:io/streams", "blocking_write_zeroes", false)?; + stub_imported_func(module, "wasi:io/streams", "blocking-write", false)?; + stub_imported_func(module, "wasi:io/streams", "write-zeroes", false)?; + stub_imported_func(module, "wasi:io/streams", "blocking-write-zeroes", false)?; stub_imported_func(module, "wasi:io/streams", "splice", false)?; - stub_imported_func(module, "wasi:io/streams", "blocking_splice", false)?; + stub_imported_func(module, "wasi:io/streams", "blocking-splice", false)?; stub_imported_func(module, "wasi:io/streams", "forward", false)?; stub_imported_func( module, "wasi:io/streams", - "subscribe_to_output_stream", + "subscribe-to-output-stream", false, )?; - stub_imported_func(module, "wasi:io/streams", "drop_output_stream", false)?; + stub_imported_func(module, "wasi:io/streams", "drop-output-stream", false)?; Ok(()) } diff --git a/tests/cases/fs-host-read.toml b/tests/cases/fs-host-read.toml new file mode 100644 index 0000000..f92104a --- /dev/null +++ b/tests/cases/fs-host-read.toml @@ -0,0 +1,230 @@ +component = "file-read" + +host-fs-path = "/file.txt" + +[virt-opts.fs.preopens."/".dir] +"file.txt" = { runtime-file = "/LICENSE" } + +[expect] +file-read = """ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +""" \ No newline at end of file diff --git a/tests/cases/fs-nested-dir-read.toml b/tests/cases/fs-nested-dir-read.toml index e0720f2..1e0fe65 100644 --- a/tests/cases/fs-nested-dir-read.toml +++ b/tests/cases/fs-nested-dir-read.toml @@ -3,7 +3,7 @@ component = "file-read" host-fs-path = "/clocks/monotonic-clock.wit" [virt-opts.fs.preopens."/"] -host = "./wit/deps" +virtualize = "./wit/deps" [expect] file-read = '''package wasi:clocks diff --git a/tests/cases/fs-passive-file-read.toml b/tests/cases/fs-passive-file-read.toml index 23811f0..a20b811 100644 --- a/tests/cases/fs-passive-file-read.toml +++ b/tests/cases/fs-passive-file-read.toml @@ -6,7 +6,7 @@ host-fs-path = "/env-none.toml" passive-cutoff = 10 [virt-opts.fs.preopens."/"] -host = "./tests/cases" +virtualize = "./tests/cases" [expect] file-read = '''component = "get-env" diff --git a/tests/cases/fs-host-dir-read.toml b/tests/cases/fs-virt-dir-read.toml similarity index 88% rename from tests/cases/fs-host-dir-read.toml rename to tests/cases/fs-virt-dir-read.toml index 11860ba..5805d38 100644 --- a/tests/cases/fs-host-dir-read.toml +++ b/tests/cases/fs-virt-dir-read.toml @@ -3,7 +3,7 @@ component = "file-read" host-fs-path = "/env-none.toml" [virt-opts.fs.preopens."/"] -host = "./tests/cases" +virtualize = "./tests/cases" [expect] file-read = '''component = "get-env" diff --git a/tests/virt.rs b/tests/virt.rs index c24e3a7..4100998 100644 --- a/tests/virt.rs +++ b/tests/virt.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Context, Result}; +use cap_std::ambient_authority; use heck::ToSnakeCase; use serde::Deserialize; use std::collections::BTreeMap; @@ -10,7 +11,10 @@ use wasmtime::{ component::{Component, Linker}, Config, Engine, Store, WasmBacktraceDetails, }; -use wasmtime_wasi::preview2::{wasi as wasi_preview2, Table, WasiCtx, WasiCtxBuilder, WasiView}; +use wasmtime_wasi::preview2::{ + wasi as wasi_preview2, DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, +}; +use wasmtime_wasi::Dir; use wit_component::ComponentEncoder; wasmtime::component::bindgen!({ @@ -68,7 +72,7 @@ async fn virt_test() -> Result<()> { let test_case_name = test_case_file_name.strip_suffix(".toml").unwrap(); // Filtering... - // if test_case_name != "fs-nested-dir-read" { + // if test_case_name != "fs-host-read" { // continue; // } @@ -132,7 +136,12 @@ async fn virt_test() -> Result<()> { } // execute the composed virtualized component test function - let mut builder = WasiCtxBuilder::new().inherit_stdio(); + let mut builder = WasiCtxBuilder::new().inherit_stdio().push_preopened_dir( + Dir::open_ambient_dir(".", ambient_authority())?, + DirPerms::READ, + FilePerms::READ, + "/", + ); if let Some(host_env) = &test.host_env { let env: Vec<(String, String)> = host_env .iter() diff --git a/virtual-adapter/src/fs.rs b/virtual-adapter/src/fs.rs index 2f6d68b..7dc668f 100644 --- a/virtual-adapter/src/fs.rs +++ b/virtual-adapter/src/fs.rs @@ -4,15 +4,16 @@ use crate::exports::wasi::filesystem::filesystem::{ ErrorCode, Filesystem, Modes, NewTimestamp, OpenFlags, PathFlags, }; use crate::exports::wasi::io::streams::{StreamError, Streams}; -// use crate::wasi::cli_base::preopens; -// use crate::wasi::filesystem::filesystem; +use crate::wasi::cli_base::preopens; +use crate::wasi::filesystem::filesystem; // use crate::wasi::io::streams; // for debugging -// use crate::console; +use crate::console; // use std::fmt; use crate::VirtAdapter; +use std::alloc::Layout; use std::cmp; use std::collections::BTreeMap; use std::ffi::CStr; @@ -25,6 +26,7 @@ pub struct Fs { preopens: *const usize, static_index_cnt: usize, static_index: *const StaticIndexEntry, + host_passthrough: bool, } impl Fs { @@ -43,13 +45,138 @@ impl Fs { // #[derive(Debug)] struct Descriptor { + // the static entry referenced by this descriptor entry: *const StaticIndexEntry, + // the descriptor index of this descriptor + fd: u32, + // if a host entry, the underlying host descriptor + // (if any) + host_fd: Option, } impl Descriptor { fn entry(&self) -> &StaticIndexEntry { unsafe { self.entry.as_ref() }.unwrap() } + + fn drop(&self) { + unsafe { + STATE.descriptor_table.remove(&self.fd); + } + if let Some(host_fd) = self.host_fd { + filesystem::drop_descriptor(host_fd); + } + } + + fn get_bytes<'a>(&mut self, offset: u64, len: u64) -> Result<(Vec, bool), ErrorCode> { + let entry = self.entry(); + match entry.ty { + StaticIndexType::ActiveFile => { + if offset as usize == unsafe { entry.data.active.1 } { + return Ok((vec![], true)); + } + if offset as usize > unsafe { entry.data.active.1 } { + return Err(ErrorCode::InvalidSeek); + } + let read_ptr = unsafe { entry.data.active.0.add(offset as usize) }; + let read_len = cmp::min( + unsafe { entry.data.active.1 } - offset as usize, + len as usize, + ); + let bytes = unsafe { slice::from_raw_parts(read_ptr, read_len) }; + Ok((bytes.to_vec(), read_len < len as usize)) + } + StaticIndexType::PassiveFile => { + if offset as usize == unsafe { entry.data.passive.1 } { + return Ok((vec![], true)); + } + if offset as usize > unsafe { entry.data.passive.1 } { + return Err(ErrorCode::InvalidSeek); + } + let read_len = cmp::min( + unsafe { entry.data.passive.1 } - offset as usize, + len as usize, + ); + let data = passive_alloc( + unsafe { entry.data.passive.0 }, + offset as u32, + read_len as u32, + ); + let bytes = unsafe { slice::from_raw_parts(data, read_len) }; + let vec = bytes.to_vec(); + unsafe { std::alloc::dealloc(data, Layout::from_size_align(1, 4).unwrap()) }; + Ok((vec, read_len < len as usize)) + } + StaticIndexType::Dir => todo!(), + StaticIndexType::RuntimeDir => todo!(), + StaticIndexType::RuntimeFile => { + if let Some(host_fd) = self.host_fd { + return filesystem::read(host_fd, len, offset).map_err(err_map); + } + + let path = unsafe { CStr::from_ptr(entry.data.runtime_path) }; + let path = path.to_str().unwrap(); + + let Some((preopen_fd, subpath)) = FsState::get_host_preopen(path) else { + return Err(ErrorCode::NoEntry); + }; + let host_fd = filesystem::open_at( + preopen_fd, + filesystem::PathFlags::empty(), + subpath, + filesystem::OpenFlags::empty(), + filesystem::DescriptorFlags::READ, + filesystem::Modes::READABLE, + ) + .map_err(err_map)?; + + self.host_fd = Some(host_fd); + filesystem::read(host_fd, len, offset).map_err(err_map) + } + } + } +} + +fn err_map(e: filesystem::ErrorCode) -> ErrorCode { + match e { + filesystem::ErrorCode::Access => ErrorCode::Access, + filesystem::ErrorCode::WouldBlock => ErrorCode::WouldBlock, + filesystem::ErrorCode::Already => ErrorCode::Already, + filesystem::ErrorCode::BadDescriptor => ErrorCode::BadDescriptor, + filesystem::ErrorCode::Busy => ErrorCode::Busy, + filesystem::ErrorCode::Deadlock => ErrorCode::Deadlock, + filesystem::ErrorCode::Quota => ErrorCode::Quota, + filesystem::ErrorCode::Exist => ErrorCode::Exist, + filesystem::ErrorCode::FileTooLarge => ErrorCode::FileTooLarge, + filesystem::ErrorCode::IllegalByteSequence => ErrorCode::IllegalByteSequence, + filesystem::ErrorCode::InProgress => ErrorCode::InProgress, + filesystem::ErrorCode::Interrupted => ErrorCode::Interrupted, + filesystem::ErrorCode::Invalid => ErrorCode::Invalid, + filesystem::ErrorCode::Io => ErrorCode::Io, + filesystem::ErrorCode::IsDirectory => ErrorCode::IsDirectory, + filesystem::ErrorCode::Loop => ErrorCode::Loop, + filesystem::ErrorCode::TooManyLinks => ErrorCode::TooManyLinks, + filesystem::ErrorCode::MessageSize => ErrorCode::MessageSize, + filesystem::ErrorCode::NameTooLong => ErrorCode::NameTooLong, + filesystem::ErrorCode::NoDevice => ErrorCode::NoDevice, + filesystem::ErrorCode::NoEntry => ErrorCode::NoEntry, + filesystem::ErrorCode::NoLock => ErrorCode::NoLock, + filesystem::ErrorCode::InsufficientMemory => ErrorCode::InsufficientMemory, + filesystem::ErrorCode::InsufficientSpace => ErrorCode::InsufficientSpace, + filesystem::ErrorCode::NotDirectory => ErrorCode::NotDirectory, + filesystem::ErrorCode::NotEmpty => ErrorCode::NotEmpty, + filesystem::ErrorCode::NotRecoverable => ErrorCode::NotRecoverable, + filesystem::ErrorCode::Unsupported => ErrorCode::Unsupported, + filesystem::ErrorCode::NoTty => ErrorCode::NoTty, + filesystem::ErrorCode::NoSuchDevice => ErrorCode::NoSuchDevice, + filesystem::ErrorCode::Overflow => ErrorCode::Overflow, + filesystem::ErrorCode::NotPermitted => ErrorCode::NotPermitted, + filesystem::ErrorCode::Pipe => ErrorCode::Pipe, + filesystem::ErrorCode::ReadOnly => ErrorCode::ReadOnly, + filesystem::ErrorCode::InvalidSeek => ErrorCode::InvalidSeek, + filesystem::ErrorCode::TextFileBusy => ErrorCode::TextFileBusy, + filesystem::ErrorCode::CrossDevice => ErrorCode::CrossDevice, + } } impl StaticIndexEntry { @@ -64,36 +191,27 @@ impl StaticIndexEntry { } fn ty(&self) -> DescriptorType { match self.ty { - StaticIndexType::RuntimeHostFile => todo!(), - StaticIndexType::ActiveFile => DescriptorType::RegularFile, - StaticIndexType::PassiveFile => DescriptorType::RegularFile, - StaticIndexType::RuntimeHostDir => todo!(), - StaticIndexType::Dir => DescriptorType::Directory, - } - } - fn size(&self) -> usize { - match self.ty { - StaticIndexType::ActiveFile => unsafe { self.data.active.1 }, - StaticIndexType::PassiveFile => unsafe { self.data.passive.1 }, - StaticIndexType::Dir => 0, - StaticIndexType::RuntimeHostDir => 0, - StaticIndexType::RuntimeHostFile => todo!(), + StaticIndexType::ActiveFile + | StaticIndexType::PassiveFile + | StaticIndexType::RuntimeFile => DescriptorType::RegularFile, + StaticIndexType::Dir | StaticIndexType::RuntimeDir => DescriptorType::Directory, } } - fn get_bytes<'a>(&'a self) -> &'a [u8] { + fn size(&self) -> Result { match self.ty { - StaticIndexType::ActiveFile => unsafe { - slice::from_raw_parts(self.data.active.0, self.data.active.1) - }, - StaticIndexType::PassiveFile => { - let passive_idx = unsafe { self.data.passive.0 }; - let passive_len = unsafe { self.data.passive.1 }; - let data = passive_alloc(passive_idx, 0, passive_len as u32); - unsafe { slice::from_raw_parts(data, passive_len) } + StaticIndexType::ActiveFile => Ok(unsafe { self.data.active.1 } as u64), + StaticIndexType::PassiveFile => Ok(unsafe { self.data.passive.1 } as u64), + StaticIndexType::Dir | StaticIndexType::RuntimeDir => Ok(0), + StaticIndexType::RuntimeFile => { + let path = unsafe { CStr::from_ptr(self.data.runtime_path) }; + let path = path.to_str().unwrap(); + let Some((fd, subpath)) = FsState::get_host_preopen(path) else { + return Err(ErrorCode::NoEntry); + }; + let stat = filesystem::stat_at(fd, filesystem::PathFlags::empty(), subpath) + .map_err(err_map)?; + Ok(stat.size) } - StaticIndexType::Dir => todo!(), - StaticIndexType::RuntimeHostDir => todo!(), - StaticIndexType::RuntimeHostFile => todo!(), } } fn child_list(&self) -> Result<&'static [StaticIndexEntry], ErrorCode> { @@ -139,7 +257,7 @@ union StaticFileData { /// Passive memory element index and len for PassiveFile passive: (u32, usize), /// Host path string for HostDir / HostFile - path: *const u8, + runtime_path: *const i8, // Index and child entry count for Dir dir: (usize, usize), } @@ -162,15 +280,15 @@ enum StaticIndexType { ActiveFile, PassiveFile, Dir, - RuntimeHostDir, - RuntimeHostFile, + RuntimeDir, + RuntimeFile, } // This function gets mutated by the virtualizer #[no_mangle] #[inline(never)] -pub fn passive_alloc(passive_idx: u32, offset: u32, len: u32) -> *const u8 { - return (passive_idx + offset + len) as *const u8; +pub fn passive_alloc(passive_idx: u32, offset: u32, len: u32) -> *mut u8 { + return (passive_idx + offset + len) as *mut u8; } #[no_mangle] @@ -179,6 +297,7 @@ pub static mut fs: Fs = Fs { preopens: 0 as *const usize, // [byte 4] static_index_cnt: 0, // [byte 8] static_index: 0 as *const StaticIndexEntry, // [byte 12] + host_passthrough: false, // [byte 16] }; // local fs state @@ -186,6 +305,7 @@ pub struct FsState { initialized: bool, descriptor_cnt: u32, preopen_directories: Vec, + host_preopen_directories: BTreeMap, descriptor_table: BTreeMap, stream_cnt: u32, stream_table: BTreeMap, @@ -195,6 +315,7 @@ static mut STATE: FsState = FsState { initialized: false, descriptor_cnt: 3, preopen_directories: Vec::new(), + host_preopen_directories: BTreeMap::new(), descriptor_table: BTreeMap::new(), stream_cnt: 0, stream_table: BTreeMap::new(), @@ -218,7 +339,9 @@ impl From for Stream { } struct FileStream { + // local file descriptor fd: u32, + // current offset offset: u64, } @@ -231,18 +354,15 @@ impl FileStream { fn new(fd: u32) -> Self { Self { fd, offset: 0 } } - fn read(&mut self, len: u64) -> Result>, StreamError> { + fn read(&mut self, len: u64) -> Result<(Vec, bool), StreamError> { let Some(descriptor) = FsState::get_descriptor(self.fd) else { return Err(StreamError {}); }; - let bytes = descriptor.entry().get_bytes(); - let read_len = cmp::min(bytes.len() as u64 - self.offset, len); - if read_len == 0 { - return Ok(None); - } - let byte_slice = &bytes[self.offset as usize..(self.offset + read_len) as usize]; - self.offset += read_len; - Ok(Some(byte_slice.to_vec())) + let (bytes, done) = descriptor + .get_bytes(self.offset, len) + .map_err(|_| StreamError {})?; + self.offset += bytes.len() as u64; + Ok((bytes, done)) } } @@ -274,8 +394,12 @@ impl FsState { if unsafe { STATE.initialized } { return; } - // TODO: Host passthrough - // let _host_preopen_directories = Some(preopens::get_directories()); + if unsafe { fs.host_passthrough } { + let host_preopen_directories = unsafe { &mut STATE.host_preopen_directories }; + for (fd, name) in preopens::get_directories() { + host_preopen_directories.insert(name, fd); + } + } let preopens = Fs::preopens(); for preopen in preopens { let fd = FsState::create_descriptor(preopen, DescriptorFlags::READ); @@ -283,20 +407,57 @@ impl FsState { } unsafe { STATE.initialized = true }; } + fn get_host_preopen<'a>(path: &'a str) -> Option<(u32, &'a str)> { + let path = if path.starts_with("./") { + &path[2..] + } else { + path + }; + for (preopen_name, fd) in unsafe { &STATE.host_preopen_directories } { + let preopen_name = if preopen_name.starts_with("./") { + &preopen_name[2..] + } else if preopen_name.starts_with(".") { + &preopen_name[1..] + } else { + preopen_name + }; + if path.starts_with(preopen_name) { + // ambient relative + if preopen_name.len() == 0 { + if path.as_bytes()[0] != b'/' { + return Some((*fd, &path)); + } + } else { + // root '/' match + if preopen_name == "/" && path.as_bytes()[0] == b'/' { + return Some((*fd, &path[1..])); + } + // exact match + if preopen_name.len() == path.len() { + return Some((*fd, "")); + } + // normal [x]/ match + if path.as_bytes()[preopen_name.len()] == b'/' { + return Some((*fd, &path[preopen_name.len() + 1..])); + } + } + } + } + None + } fn create_descriptor(entry: &StaticIndexEntry, _flags: DescriptorFlags) -> u32 { let fd = unsafe { STATE.descriptor_cnt }; unsafe { STATE.descriptor_cnt += 1 }; - let descriptor = Descriptor { entry }; + let descriptor = Descriptor { + entry, + fd, + host_fd: None, + }; assert!(unsafe { STATE.descriptor_table.insert(fd, descriptor) }.is_none()); fd } - fn get_descriptor<'a>(fd: u32) -> Option<&'a Descriptor> { - unsafe { STATE.descriptor_table.get(&fd) } - } - fn drop_descriptor(fd: u32) { - unsafe { - STATE.descriptor_table.remove(&fd); - } + fn get_descriptor<'a>(fd: u32) -> Option<&'a mut Descriptor> { + unsafe { STATE.descriptor_table.get_mut(&fd) } } fn get_preopen_directories() -> Vec<(u32, String)> { FsState::initialize(); @@ -346,7 +507,7 @@ impl Filesystem for VirtAdapter { todo!() } fn sync_data(_: u32) -> Result<(), ErrorCode> { - todo!() + Err(ErrorCode::Access) } fn get_flags(_fd: u32) -> Result { Ok(DescriptorFlags::READ) @@ -379,7 +540,7 @@ impl Filesystem for VirtAdapter { FsState::create_stream(DirStream::new(fd)) } fn sync(_: u32) -> Result<(), ErrorCode> { - todo!() + Err(ErrorCode::Access) } fn create_directory_at(_: u32, _: String) -> Result<(), ErrorCode> { Err(ErrorCode::Access) @@ -393,7 +554,7 @@ impl Filesystem for VirtAdapter { inode: 0, type_: descriptor.entry().ty(), link_count: 0, - size: descriptor.entry().size() as u64, + size: descriptor.entry().size()?, data_access_timestamp: Datetime { seconds: 0, nanoseconds: 0, @@ -418,7 +579,7 @@ impl Filesystem for VirtAdapter { inode: 0, type_: child.ty(), link_count: 0, - size: child.size() as u64, + size: child.size()?, data_access_timestamp: Datetime { seconds: 0, nanoseconds: 0, @@ -440,7 +601,7 @@ impl Filesystem for VirtAdapter { _: NewTimestamp, _: NewTimestamp, ) -> Result<(), ErrorCode> { - todo!() + Err(ErrorCode::Access) } fn link_at(_: u32, _: PathFlags, _: String, _: u32, _: String) -> Result<(), ErrorCode> { Err(ErrorCode::Access) @@ -463,19 +624,19 @@ impl Filesystem for VirtAdapter { todo!() } fn remove_directory_at(_: u32, _: String) -> Result<(), ErrorCode> { - todo!() + Err(ErrorCode::Access) } fn rename_at(_: u32, _: String, _: u32, _: String) -> Result<(), ErrorCode> { - todo!() + Err(ErrorCode::Access) } fn symlink_at(_: u32, _: String, _: String) -> Result<(), ErrorCode> { - todo!() + Err(ErrorCode::Access) } fn access_at(_: u32, _: PathFlags, _: String, _: AccessType) -> Result<(), ErrorCode> { - todo!() + Err(ErrorCode::Access) } fn unlink_file_at(_: u32, _: String) -> Result<(), ErrorCode> { - todo!() + Err(ErrorCode::Access) } fn change_file_permissions_at( _: u32, @@ -509,7 +670,9 @@ impl Filesystem for VirtAdapter { Ok(()) } fn drop_descriptor(fd: u32) { - FsState::drop_descriptor(fd); + if let Some(descriptor) = FsState::get_descriptor(fd) { + descriptor.drop(); + }; } fn read_directory_entry(sid: u32) -> Result, ErrorCode> { let Some(stream) = FsState::get_stream(sid) else { @@ -536,13 +699,8 @@ impl Streams for VirtAdapter { return Err(StreamError {}); }; match stream { - Stream::File(filestream) => match filestream.read(len)? { - Some(vec) => Ok((vec, false)), - None => Ok((vec![], true)), - }, - _ => { - return Err(StreamError {}); - } + Stream::File(filestream) => filestream.read(len), + _ => Err(StreamError {}), } } fn skip(_: u32, _: u64) -> Result<(u64, bool), StreamError> {