From de11b9384bdbdb2f3d80d77980b4ec4c3eadb6ca Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 21 Jul 2023 17:02:55 -0700 Subject: [PATCH] host passthrough --- Cargo.lock | 171 +++++++++- Cargo.toml | 1 + README.md | 81 ++++- lib/virtual_adapter.wasm | Bin 74702 -> 83565 bytes src/lib.rs | 38 ++- src/virt_fs.rs | 139 +++++--- tests/cases/fs-host-read.toml | 230 ++++++++++++++ tests/cases/fs-nested-dir-read.toml | 2 +- tests/cases/fs-passive-file-read.toml | 2 +- ...st-dir-read.toml => fs-virt-dir-read.toml} | 2 +- tests/virt.rs | 15 +- virtual-adapter/src/fs.rs | 300 +++++++++++++----- 12 files changed, 834 insertions(+), 147 deletions(-) create mode 100644 tests/cases/fs-host-read.toml rename tests/cases/{fs-host-dir-read.toml => fs-virt-dir-read.toml} (88%) 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 b8d1b92ba7a16e66c5faadb25f6809386a508814..0b14df21c2db5e3f74be3ef6733d00a19c0f5db5 100755 GIT binary patch delta 25577 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 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> {