diff --git a/Cargo.lock b/Cargo.lock index e483701..7382734 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1900,8 +1900,7 @@ dependencies = [ [[package]] name = "walrus" version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc27d837c587f863d99515dc8cae7cef1098bd1d99fa29373e3660c12766265e" +source = "git+https://github.com/rustwasm/walrus?rev=db5d437b91e80c564f5e45204b8b165027d2a870#db5d437b91e80c564f5e45204b8b165027d2a870" dependencies = [ "anyhow", "gimli 0.26.2", @@ -1916,13 +1915,12 @@ dependencies = [ [[package]] name = "walrus-macro" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e5bd22c71e77d60140b0bd5be56155a37e5bd14e24f5f87298040d0cc40d7" +source = "git+https://github.com/rustwasm/walrus?rev=db5d437b91e80c564f5e45204b8b165027d2a870#db5d437b91e80c564f5e45204b8b165027d2a870" dependencies = [ "heck 0.3.3", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.37", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ccca0f2..a66cd1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ "tests/components/do-everything", "tests/components/file-read", "tests/components/get-env", - "tests/components/stdio" + "tests/components/stdio", ] [profile.release] @@ -33,7 +33,8 @@ anyhow = "1" clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } toml = "0.7" -walrus = "0.20.1" +# TODO: use published version of walrus +walrus = { git = "https://github.com/rustwasm/walrus", rev = "db5d437b91e80c564f5e45204b8b165027d2a870" } wasm-compose = "0.4.2" wasm-metadata = "0.10.5" wasm-opt = "0.114.1" @@ -45,12 +46,14 @@ anyhow = "1" [dev-dependencies] anyhow = "1" cap-std = "1.0.12" -heck = { version = "0.4" } +heck = { version = "0.4" } tokio = { version = "1.30.0", features = ["macros"] } -wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", features = ["component-model"] } +wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", features = [ + "component-model", +] } wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime" } wasmparser = "0.113.1" [workspace.dependencies] anyhow = "1" -wit-bindgen = "0.12.0" \ No newline at end of file +wit-bindgen = "0.12.0" diff --git a/src/data.rs b/src/data.rs index ee54588..103aefa 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,6 +1,3 @@ -use crate::walrus_ops::{ - bump_stack_global, get_exported_func, get_memory_id, remove_exported_func, -}; use anyhow::{bail, Result}; use std::collections::HashMap; use walrus::{ @@ -8,6 +5,8 @@ use walrus::{ FunctionKind, InitExpr, Module, ValType, }; +use crate::walrus_ops::bump_stack_global; + /// Data section /// Because data is stack-allocated we create a corresponding byte vector as large /// as the stack, zero fill it then populate it backwards from the @@ -115,7 +114,7 @@ impl Data { } pub fn finish(mut self, module: &mut Module) -> Result<()> { // stack embedding - let memory = get_memory_id(module)?; + let memory = module.get_memory_id()?; let rem = (self.stack_start - self.stack_ptr) % 8; if rem != 0 { self.stack_ptr -= 8 - rem; @@ -132,7 +131,7 @@ impl Data { // passive segment embedding // we create one function for each passive segment, due to if self.passive_segments.len() > 0 { - let alloc_fid = get_exported_func(module, "cabi_realloc")?; + let alloc_fid = module.exports.get_func("cabi_realloc")?; let offset_local = module.locals.add(ValType::I32); let len_local = module.locals.add(ValType::I32); @@ -206,13 +205,13 @@ impl Data { .call_indirect(passive_fn_alloc_type, passive_tid); // update the existing passive_alloc function export with the new function body - let passive_alloc_fid = get_exported_func(module, "passive_alloc")?; + let passive_alloc_fid = module.exports.get_func("passive_alloc")?; let passive_alloc_func = module.funcs.get_mut(passive_alloc_fid); passive_alloc_func.kind = FunctionKind::Local(builder.local_func(vec![passive_idx, offset_local, len_local])); } - remove_exported_func(module, "passive_alloc")?; + module.exports.remove("passive_alloc")?; Ok(()) } diff --git a/src/virt_deny.rs b/src/virt_deny.rs deleted file mode 100644 index 3056715..0000000 --- a/src/virt_deny.rs +++ /dev/null @@ -1,771 +0,0 @@ -use anyhow::Result; -use walrus::{Module, ValType}; - -use crate::{ - virt_io::{stub_clocks_virt, stub_http_virt, stub_sockets_virt}, - walrus_ops::add_stub_exported_func, -}; - -// set exports to deny clock access -pub(crate) fn deny_clocks_virt(module: &mut Module) -> Result<()> { - stub_clocks_virt(module)?; - add_stub_exported_func( - module, - "wasi:clocks/monotonic-clock#now", - vec![], - vec![ValType::I64], - )?; - add_stub_exported_func( - module, - "wasi:clocks/monotonic-clock#resolution", - vec![], - vec![ValType::I64], - )?; - add_stub_exported_func( - module, - "wasi:clocks/monotonic-clock#subscribe", - vec![ValType::I64, ValType::I32], - vec![ValType::I32], - )?; - - add_stub_exported_func( - module, - "wasi:clocks/wall-clock#now", - vec![], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:clocks/wall-clock#resolution", - vec![], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:clocks/wall-clock#subscribe", - vec![ValType::I64, ValType::I32], - vec![ValType::I32], - )?; - - add_stub_exported_func( - module, - "wasi:clocks/timezone#display", - vec![ValType::I32, ValType::I64, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "cabi_post_wasi:clocks/timezone#display", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:clocks/timezone#utc-offset", - vec![ValType::I32, ValType::I64, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:clocks/timezone#drop-timezone", - vec![ValType::I32], - vec![], - )?; - - Ok(()) -} - -pub(crate) fn deny_http_virt(module: &mut Module) -> Result<()> { - stub_http_virt(module)?; - add_stub_exported_func( - module, - "wasi:http/incoming-handler#handle", - vec![ValType::I32, ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/outgoing-handler#handle", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#drop-fields", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#new-fields", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#fields-get", - vec![ValType::I32, ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#fields-set", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#fields-delete", - vec![ValType::I32, ValType::I32, ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#fields-append", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#fields-entries", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#fields-clone", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#finish-incoming-stream", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#finish-outgoing-stream", - vec![ValType::I32, ValType::I32, ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#drop-incoming-request", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#drop-outgoing-request", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-request-method", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-request-path-with-query", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-request-scheme", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-request-authority", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-request-headers", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-request-consume", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#new-outgoing-request", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#outgoing-request-write", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#drop-response-outparam", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#set-response-outparam", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#drop-incoming-response", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#drop-outgoing-response", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-response-status", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-response-headers", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#incoming-response-consume", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#new-outgoing-response", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#outgoing-response-write", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#drop-future-incoming-response", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:http/types#future-incoming-response-get", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:http/types#listen-to-future-incoming-response", - vec![ValType::I32], - vec![ValType::I32], - )?; - Ok(()) -} - -pub(crate) fn deny_random_virt(module: &mut Module) -> Result<()> { - add_stub_exported_func( - module, - "wasi:random/random#get-random-bytes", - vec![ValType::I64], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "cabi_post_wasi:random/random#get-random-bytes", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:random/random#get-random-u64", - vec![], - vec![ValType::I64], - )?; - add_stub_exported_func( - module, - "wasi:random/insecure#get-insecure-random-bytes", - vec![ValType::I64], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "cabi_post_wasi:random/insecure#get-insecure-random-bytes", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:random/insecure#get-insecure-random-u64", - vec![], - vec![ValType::I64], - )?; - add_stub_exported_func( - module, - "wasi:random/insecure-seed#insecure-seed", - vec![], - vec![ValType::I32], - )?; - Ok(()) -} - -pub(crate) fn deny_exit_virt(module: &mut Module) -> Result<()> { - add_stub_exported_func(module, "wasi:cli/exit#exit", vec![ValType::I32], vec![])?; - Ok(()) -} - -pub(crate) fn deny_sockets_virt(module: &mut Module) -> Result<()> { - stub_sockets_virt(module)?; - add_stub_exported_func( - module, - "wasi:sockets/network#drop-network", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:sockets/instance-network#instance-network", - vec![], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/ip-name-lookup#resolve-addresses", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/ip-name-lookup#resolve-next-address", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/ip-name-lookup#drop-resolve-address-stream", - vec![ValType::I32], - vec![], - )?; - add_stub_exported_func( - module, - "wasi:sockets/ip-name-lookup#subscribe", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp-create-socket#create-tcp-socket", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#start-bind", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#finish-bind", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#start-connect", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#finish-connect", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#start-listen", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#finish-listen", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#accept", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#local-address", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#remote-address", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#address-family", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#ipv6-only", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#set-ipv6-only", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#set-listen-backlog-size", - vec![ValType::I32, ValType::I64], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#keep-alive", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#set-keep-alive", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#no-delay", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#set-no-delay", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#unicast-hop-limit", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#set-unicast-hop-limit", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#receive-buffer-size", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#set-receive-buffer-size", - vec![ValType::I32, ValType::I64], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#send-buffer-size", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#set-send-buffer-size", - vec![ValType::I32, ValType::I64], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#subscribe", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#shutdown", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/tcp#drop-tcp-socket", - vec![ValType::I32], - vec![], - )?; - - add_stub_exported_func( - module, - "wasi:sockets/udp-create-socket#create-udp-socket", - vec![ValType::I32], - vec![ValType::I32], - )?; - - add_stub_exported_func( - module, - "wasi:sockets/udp#start-bind", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#finish-bind", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#start-connect", - vec![ - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ValType::I32, - ], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#finish-connect", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#receive", - vec![ValType::I32, ValType::I64], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#send", - vec![ValType::I32, ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#local-address", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#remote-address", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#address-family", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#ipv6-only", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#set-ipv6-only", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#unicast-hop-limit", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#set-unicast-hop-limit", - vec![ValType::I32, ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#receive-buffer-size", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#set-receive-buffer-size", - vec![ValType::I32, ValType::I64], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#send-buffer-size", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#set-send-buffer-size", - vec![ValType::I32, ValType::I64], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#subscribe", - vec![ValType::I32], - vec![ValType::I32], - )?; - add_stub_exported_func( - module, - "wasi:sockets/udp#drop-udp-socket", - vec![ValType::I32], - vec![], - )?; - - Ok(()) -} diff --git a/src/virt_deny/clocks.rs b/src/virt_deny/clocks.rs new file mode 100644 index 0000000..c22440f --- /dev/null +++ b/src/virt_deny/clocks.rs @@ -0,0 +1,71 @@ +use std::sync::OnceLock; + +use anyhow::Result; +use walrus::{FuncParams, FuncResults, Module, ValType}; + +use crate::virt_io::stub_clocks_virt; + +use super::replace_or_insert_stub_for_exports; + +/// Functions that represent the environment functionality provided by WASI clocks +static WASI_CLOCK_FNS: OnceLock> = OnceLock::new(); + +/// Retrieve or initialize the static list of functions related to clocks in WASI +fn get_wasi_clock_fns() -> &'static Vec<(&'static str, FuncParams, FuncResults)> { + WASI_CLOCK_FNS.get_or_init(|| { + Vec::from([ + ( + "wasi:clocks/monotonic-clock#now", + vec![], + vec![ValType::I64], + ), + ( + "wasi:clocks/monotonic-clock#resolution", + vec![], + vec![ValType::I64], + ), + ( + "wasi:clocks/monotonic-clock#subscribe", + vec![ValType::I64, ValType::I32], + vec![ValType::I32], + ), + ("wasi:clocks/wall-clock#now", vec![], vec![ValType::I32]), + ( + "wasi:clocks/wall-clock#resolution", + vec![], + vec![ValType::I32], + ), + ( + "wasi:clocks/wall-clock#subscribe", + vec![ValType::I64, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:clocks/timezone#display", + vec![ValType::I32, ValType::I64, ValType::I32], + vec![ValType::I32], + ), + ( + "cabi_post_wasi:clocks/timezone#display", + vec![ValType::I32], + vec![], + ), + ( + "wasi:clocks/timezone#utc-offset", + vec![ValType::I32, ValType::I64, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:clocks/timezone#drop-timezone", + vec![ValType::I32], + vec![], + ), + ]) + }) +} + +/// Replace exports related to clocks in WASI to deny access +pub(crate) fn deny_clocks_virt(module: &mut Module) -> Result<()> { + stub_clocks_virt(module)?; + replace_or_insert_stub_for_exports(module, get_wasi_clock_fns()) +} diff --git a/src/virt_deny/exit.rs b/src/virt_deny/exit.rs new file mode 100644 index 0000000..7d0b546 --- /dev/null +++ b/src/virt_deny/exit.rs @@ -0,0 +1,19 @@ +use std::sync::OnceLock; + +use anyhow::Result; +use walrus::{FuncParams, FuncResults, Module, ValType}; + +use super::replace_or_insert_stub_for_exports; + +/// Functions that represent the environment functionality provided by WASI exits +static WASI_EXIT_FNS: OnceLock> = OnceLock::new(); + +/// Retrieve or initialize the static list of functions related to exiting in WASI +fn get_wasi_exit_fns() -> &'static Vec<(&'static str, FuncParams, FuncResults)> { + WASI_EXIT_FNS.get_or_init(|| Vec::from([("wasi:cli/exit#exit", vec![ValType::I32], vec![])])) +} + +/// Replace exports related to exiting in WASI to deny access +pub(crate) fn deny_exit_virt(module: &mut Module) -> Result<()> { + replace_or_insert_stub_for_exports(module, get_wasi_exit_fns()) +} diff --git a/src/virt_deny/http.rs b/src/virt_deny/http.rs new file mode 100644 index 0000000..7d12361 --- /dev/null +++ b/src/virt_deny/http.rs @@ -0,0 +1,233 @@ +use std::sync::OnceLock; + +use anyhow::Result; +use walrus::{FuncParams, FuncResults, Module, ValType}; + +use crate::virt_io::stub_http_virt; + +use super::replace_or_insert_stub_for_exports; + +/// Functions that represent the HTTP functionality provided by WASI https +pub(crate) static WASI_HTTP_FNS: OnceLock> = OnceLock::new(); + +/// Retrieve or initialize the static list of functions related to HTTP in WASI +fn get_wasi_http_fns() -> &'static Vec<(&'static str, FuncParams, FuncResults)> { + WASI_HTTP_FNS.get_or_init(|| { + Vec::from([ + ( + "wasi:http/incoming-handler#handle", + vec![ValType::I32, ValType::I32], + vec![], + ), + ( + "wasi:http/outgoing-handler#handle", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![ValType::I32], + ), + ("wasi:http/types#drop-fields", vec![ValType::I32], vec![]), + ( + "wasi:http/types#new-fields", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#fields-get", + vec![ValType::I32, ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#fields-set", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![], + ), + ( + "wasi:http/types#fields-delete", + vec![ValType::I32, ValType::I32, ValType::I32], + vec![], + ), + ( + "wasi:http/types#fields-append", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![], + ), + ( + "wasi:http/types#fields-entries", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#fields-clone", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#finish-incoming-stream", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#finish-outgoing-stream", + vec![ValType::I32, ValType::I32, ValType::I32], + vec![], + ), + ( + "wasi:http/types#drop-incoming-request", + vec![ValType::I32], + vec![], + ), + ( + "wasi:http/types#drop-outgoing-request", + vec![ValType::I32], + vec![], + ), + ( + "wasi:http/types#incoming-request-method", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#incoming-request-path-with-query", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#incoming-request-scheme", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#incoming-request-authority", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#incoming-request-headers", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#incoming-request-consume", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#new-outgoing-request", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![ValType::I32], + ), + ( + "wasi:http/types#outgoing-request-write", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#drop-response-outparam", + vec![ValType::I32], + vec![], + ), + ( + "wasi:http/types#set-response-outparam", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![ValType::I32], + ), + ( + "wasi:http/types#drop-incoming-response", + vec![ValType::I32], + vec![], + ), + ( + "wasi:http/types#drop-outgoing-response", + vec![ValType::I32], + vec![], + ), + ( + "wasi:http/types#incoming-response-status", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#incoming-response-headers", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#incoming-response-consume", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#new-outgoing-response", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#outgoing-response-write", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#drop-future-incoming-response", + vec![ValType::I32], + vec![], + ), + ( + "wasi:http/types#future-incoming-response-get", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:http/types#listen-to-future-incoming-response", + vec![ValType::I32], + vec![ValType::I32], + ), + ]) + }) +} + +/// Replace exports related to HTTP in WASI to deny access +pub(crate) fn deny_http_virt(module: &mut Module) -> Result<()> { + stub_http_virt(module)?; + replace_or_insert_stub_for_exports(module, get_wasi_http_fns()) +} diff --git a/src/virt_deny/mod.rs b/src/virt_deny/mod.rs new file mode 100644 index 0000000..2628d68 --- /dev/null +++ b/src/virt_deny/mod.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use walrus::{ExportItem, FuncParams, FuncResults, FunctionBuilder, Module}; + +mod clocks; +mod exit; +mod http; +mod random; +mod sockets; +pub(crate) use clocks::deny_clocks_virt; +pub(crate) use exit::deny_exit_virt; +pub(crate) use http::deny_http_virt; +pub(crate) use random::deny_random_virt; +pub(crate) use sockets::deny_sockets_virt; + +/// Helper function for replacing or inserting exports with stub functions +fn replace_or_insert_stub_for_exports<'a>( + module: &mut Module, + exports: impl IntoIterator, +) -> Result<()> { + for (export_name, params, results) in exports { + // If the export exists, replace it directly + if let Ok(fid) = module.exports.get_func(&export_name) { + module.replace_exported_func(fid, |(body, _)| { + body.unreachable(); + })?; + continue; + } + + // Create and use a new stub function for the export + let mut builder = FunctionBuilder::new(&mut module.types, ¶ms, &results); + let mut body = builder.func_body(); + body.unreachable(); + module.exports.add( + &export_name, + ExportItem::Function(module.funcs.add_local(builder.local_func(vec![]))), + ); + } + Ok(()) +} diff --git a/src/virt_deny/random.rs b/src/virt_deny/random.rs new file mode 100644 index 0000000..760996b --- /dev/null +++ b/src/virt_deny/random.rs @@ -0,0 +1,57 @@ +use std::sync::OnceLock; + +use anyhow::Result; +use walrus::{FuncParams, FuncResults, Module, ValType}; + +use super::replace_or_insert_stub_for_exports; + +/// Functions that represent the environment functionality provided by WASI randoms +static WASI_RANDOM_FNS: OnceLock> = OnceLock::new(); + +/// Retrieve or initialize the static list of functions related to randomness in WASI +fn get_wasi_random_fns() -> &'static Vec<(&'static str, FuncParams, FuncResults)> { + WASI_RANDOM_FNS.get_or_init(|| { + Vec::from([ + ( + "wasi:random/random#get-random-bytes", + vec![ValType::I64], + vec![ValType::I32], + ), + ( + "cabi_post_wasi:random/random#get-random-bytes", + vec![ValType::I32], + vec![], + ), + ( + "wasi:random/random#get-random-u64", + vec![], + vec![ValType::I64], + ), + ( + "wasi:random/insecure#get-insecure-random-bytes", + vec![ValType::I64], + vec![ValType::I32], + ), + ( + "cabi_post_wasi:random/insecure#get-insecure-random-bytes", + vec![ValType::I32], + vec![], + ), + ( + "wasi:random/insecure#get-insecure-random-u64", + vec![], + vec![ValType::I64], + ), + ( + "wasi:random/insecure-seed#insecure-seed", + vec![], + vec![ValType::I32], + ), + ]) + }) +} + +/// Replace exports related to randomness in WASI to deny access +pub(crate) fn deny_random_virt(module: &mut Module) -> Result<()> { + replace_or_insert_stub_for_exports(module, get_wasi_random_fns()) +} diff --git a/src/virt_deny/sockets.rs b/src/virt_deny/sockets.rs new file mode 100644 index 0000000..b19ca3b --- /dev/null +++ b/src/virt_deny/sockets.rs @@ -0,0 +1,357 @@ +use std::sync::OnceLock; + +use anyhow::Result; +use walrus::{FuncParams, FuncResults, Module, ValType}; + +use crate::virt_io::stub_sockets_virt; + +use super::replace_or_insert_stub_for_exports; + +/// Functions that represent the environment functionality provided by WASI sockets +static WASI_SOCKETS_FNS: OnceLock> = OnceLock::new(); + +/// Retrieve or initialize the static list of functions related to sockets in WASI +pub fn get_wasi_sockets_fns() -> &'static Vec<(&'static str, FuncParams, FuncResults)> { + WASI_SOCKETS_FNS.get_or_init(|| { + Vec::from([ + ( + "wasi:sockets/network#drop-network", + vec![ValType::I32], + vec![], + ), + ( + "wasi:sockets/instance-network#instance-network", + vec![], + vec![ValType::I32], + ), + ( + "wasi:sockets/ip-name-lookup#resolve-addresses", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![ValType::I32], + ), + ( + "wasi:sockets/ip-name-lookup#resolve-next-address", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/ip-name-lookup#drop-resolve-address-stream", + vec![ValType::I32], + vec![], + ), + ( + "wasi:sockets/ip-name-lookup#subscribe", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp-create-socket#create-tcp-socket", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#start-bind", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#finish-bind", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#start-connect", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#finish-connect", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#start-listen", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#finish-listen", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#accept", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#local-address", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#remote-address", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#address-family", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#ipv6-only", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#set-ipv6-only", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#set-listen-backlog-size", + vec![ValType::I32, ValType::I64], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#keep-alive", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#set-keep-alive", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#no-delay", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#set-no-delay", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#unicast-hop-limit", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#set-unicast-hop-limit", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#receive-buffer-size", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#set-receive-buffer-size", + vec![ValType::I32, ValType::I64], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#send-buffer-size", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#set-send-buffer-size", + vec![ValType::I32, ValType::I64], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#subscribe", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#shutdown", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/tcp#drop-tcp-socket", + vec![ValType::I32], + vec![], + ), + ( + "wasi:sockets/udp-create-socket#create-udp-socket", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#start-bind", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#finish-bind", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#start-connect", + vec![ + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ValType::I32, + ], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#finish-connect", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#receive", + vec![ValType::I32, ValType::I64], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#send", + vec![ValType::I32, ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#local-address", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#remote-address", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#address-family", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#ipv6-only", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#set-ipv6-only", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#unicast-hop-limit", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#set-unicast-hop-limit", + vec![ValType::I32, ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#receive-buffer-size", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#set-receive-buffer-size", + vec![ValType::I32, ValType::I64], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#send-buffer-size", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#set-send-buffer-size", + vec![ValType::I32, ValType::I64], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#subscribe", + vec![ValType::I32], + vec![ValType::I32], + ), + ( + "wasi:sockets/udp#drop-udp-socket", + vec![ValType::I32], + vec![], + ), + ]) + }) +} + +/// Replace exports related to sockets in WASI to deny access +pub(crate) fn deny_sockets_virt(module: &mut Module) -> Result<()> { + stub_sockets_virt(module)?; + replace_or_insert_stub_for_exports(module, get_wasi_sockets_fns()) +} diff --git a/src/virt_env.rs b/src/virt_env.rs index 28f494c..e4f8b79 100644 --- a/src/virt_env.rs +++ b/src/virt_env.rs @@ -1,13 +1,11 @@ -use crate::walrus_ops::{ - bump_stack_global, get_active_data_segment, get_memory_id, remove_exported_func, - stub_imported_func, -}; use anyhow::{bail, Context, Result}; use serde::Deserialize; use walrus::{ ir::Value, ActiveData, ActiveDataLocation, DataKind, ExportItem, GlobalKind, InitExpr, Module, }; +use crate::walrus_ops::{bump_stack_global, get_active_data_segment}; + #[derive(Deserialize, Debug, Clone, Default)] #[serde(deny_unknown_fields)] pub struct VirtEnv { @@ -95,7 +93,7 @@ pub(crate) fn create_env_virt<'a>(module: &'a mut Module, env: &VirtEnv) -> Resu // we do arguments as well because virt assumes reactors for now... } - let memory = get_memory_id(module)?; + let memory = module.get_memory_id()?; // prepare the field data list vector for writing // strings must be sorted as binary searches are used against this data @@ -213,17 +211,41 @@ pub(crate) fn create_env_virt<'a>(module: &'a mut Module, env: &VirtEnv) -> Resu Ok(()) } +/// Functions that represent the environment functionality provided by WASI CLI +const WASI_ENV_FNS: [&str; 3] = ["get-arguments", "get-environment", "initial-cwd"]; + +/// Stub imported functions that implement the WASI CLI environment functionality +/// +/// This function throws an error if any imported functions do not exist pub(crate) fn stub_env_virt(module: &mut Module) -> Result<()> { - stub_imported_func(module, "wasi:cli/environment", "get-arguments", true)?; - stub_imported_func(module, "wasi:cli/environment", "get-environment", true)?; - stub_imported_func(module, "wasi:cli/environment", "initial-cwd", true)?; + for fn_name in WASI_ENV_FNS { + module.replace_imported_func( + module.imports.get_func("wasi:cli/environment", fn_name)?, + |(body, _)| { + body.unreachable(); + }, + )?; + } + Ok(()) } +/// Strip exported functions that implement the WASI CLI environment functionality +/// +/// This function *does not* throw an error if an export does not exist. pub(crate) fn strip_env_virt(module: &mut Module) -> Result<()> { stub_env_virt(module)?; - remove_exported_func(module, "wasi:cli/environment#get-arguments")?; - remove_exported_func(module, "wasi:cli/environment#get-environment")?; - remove_exported_func(module, "wasi:cli/environment#initial-cwd")?; + + for fn_name in WASI_ENV_FNS { + if let Ok(fid) = module + .exports + .get_func(format!("wasi:cli/environment#{fn_name}")) + { + module.replace_exported_func(fid, |(body, _)| { + body.unreachable(); + })?; + }; + } + Ok(()) } diff --git a/src/virt_io.rs b/src/virt_io.rs deleted file mode 100644 index 6ab57b1..0000000 --- a/src/virt_io.rs +++ /dev/null @@ -1,1053 +0,0 @@ -use std::fmt; -use std::{collections::BTreeMap, fs}; - -use anyhow::{bail, Context, Result}; -use clap::ValueEnum; -use serde::Deserialize; -use walrus::{ir::Value, ExportItem, GlobalKind, InitExpr, Module}; - -use crate::walrus_ops::remove_exported_func; -use crate::{ - data::{Data, WasmEncode}, - walrus_ops::{get_active_data_segment, get_stack_global, stub_imported_func}, -}; - -pub type VirtualFiles = BTreeMap; - -#[derive(ValueEnum, Clone, Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum StdioCfg { - #[default] - Allow, - Ignore, - Deny, -} - -#[derive(Deserialize, Debug, Default, Clone)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct VirtStdio { - pub stdin: StdioCfg, - pub stdout: StdioCfg, - pub stderr: StdioCfg, -} - -impl VirtStdio { - pub fn ignore(&mut self) -> &mut Self { - self.stdin = StdioCfg::Ignore; - self.stdout = StdioCfg::Ignore; - self.stderr = StdioCfg::Ignore; - self - } - pub fn allow(&mut self) -> &mut Self { - self.stdin = StdioCfg::Allow; - self.stdout = StdioCfg::Allow; - self.stderr = StdioCfg::Allow; - self - } - pub fn deny(&mut self) -> &mut Self { - self.stdin = StdioCfg::Deny; - self.stdout = StdioCfg::Deny; - self.stderr = StdioCfg::Deny; - self - } - pub fn stdin(&mut self, cfg: StdioCfg) -> &mut Self { - self.stdin = cfg; - self - } - pub fn stdout(&mut self, cfg: StdioCfg) -> &mut Self { - self.stdout = cfg; - self - } - pub fn stderr(&mut self, cfg: StdioCfg) -> &mut Self { - self.stderr = cfg; - self - } -} - -#[derive(Deserialize, Debug, Default, Clone)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct VirtFs { - /// Enable verbatim host preopens - #[serde(default)] - pub host_preopens: bool, - /// Filesystem state to virtualize - #[serde(default)] - pub preopens: BTreeMap, - /// A cutoff size in bytes, above which - /// files will be treated as passive segments. - /// Per-file control may also be provided. - pub passive_cutoff: Option, -} - -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub enum FsEntry { - /// symlink absolute or relative file path on the virtual filesystem - Symlink(String), - /// host path at virtualization time - Virtualize(String), - /// host path st runtime - RuntimeDir(String), - RuntimeFile(String), - /// Virtual file - File(Vec), - /// String (UTF8) file source convenience - Source(String), - /// Virtual directory - Dir(VirtDir), -} - -#[derive(Deserialize, Debug, Clone)] -#[serde(deny_unknown_fields)] -pub struct VirtFile { - pub bytes: Option>, - pub source: Option, -} - -type VirtDir = BTreeMap; - -impl VirtFs { - /// Deny host preopens at runtime - pub fn deny_host_preopens(&mut self) { - self.host_preopens = false; - } - /// Allow host preopens at runtime - pub fn allow_host_preopens(&mut self) { - self.host_preopens = true; - } - /// Add a preopen entry - pub fn preopen(&mut self, name: String, preopen: FsEntry) -> &mut Self { - self.preopens.insert(name, preopen); - self - } - /// Add a runtime preopen host mapping - pub fn host_preopen(&mut self, name: String, dir: String) -> &mut Self { - self.preopens.insert(name, FsEntry::RuntimeDir(dir)); - self - } - /// Add a preopen virtualized local directory (which will be globbed) - pub fn virtual_preopen(&mut self, name: String, dir: String) -> &mut Self { - self.preopens.insert(name, FsEntry::Virtualize(dir)); - self - } - /// Set the passive cutoff size in bytes for creating Wasm passive segments - pub fn passive_cutoff(&mut self, passive_cutoff: usize) -> &mut Self { - self.passive_cutoff = Some(passive_cutoff); - self - } -} - -#[derive(Debug)] -struct StaticIndexEntry { - name: u32, - ty: StaticIndexType, - data: StaticFileData, -} - -impl WasmEncode for StaticIndexEntry { - fn align() -> usize { - 4 - } - fn size() -> usize { - 16 - } - fn encode(&self, bytes: &mut [u8]) { - self.name.encode(&mut bytes[0..4]); - self.ty.encode(&mut bytes[4..8]); - self.data.encode(&mut bytes[8..16]); - } -} - -#[allow(dead_code)] -#[derive(Debug, Clone, Copy)] -#[repr(u32)] -enum StaticIndexType { - ActiveFile, - PassiveFile, - Dir, - RuntimeHostDir, - RuntimeHostFile, -} - -impl WasmEncode for StaticIndexType { - fn align() -> usize { - 4 - } - fn size() -> usize { - 4 - } - fn encode(&self, bytes: &mut [u8]) { - bytes[0..4].copy_from_slice(&(*self as u32).to_le_bytes()); - } -} - -union StaticFileData { - /// Active memory data pointer for ActiveFile - active: (u32, u32), - - /// Passive memory element index and len for PassiveFile - passive: (u32, u32), - - /// Host path string for HostDir / HostFile - host_path: u32, - - /// Pointer and child entry count for Dir - dir: (u32, u32), -} - -impl WasmEncode for StaticFileData { - fn align() -> usize { - 4 - } - fn size() -> usize { - 8 - } - fn encode(&self, bytes: &mut [u8]) { - bytes[0..4].copy_from_slice(&unsafe { self.dir.0.to_le_bytes() }); - bytes[4..8].copy_from_slice(&unsafe { self.dir.1.to_le_bytes() }); - } -} - -impl fmt::Debug for StaticFileData { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&format!( - "STATIC [{:?}, {:?}]", - unsafe { self.dir.0 }, - unsafe { self.dir.1 } - ))?; - Ok(()) - } -} - -impl FsEntry { - fn visit_pre_mut<'a, Visitor>(&'a mut self, base_path: &str, visit: &mut Visitor) -> Result<()> - where - Visitor: FnMut(&mut FsEntry, &str, &str) -> Result<()>, - { - visit(self, base_path, "")?; - self.visit_pre_mut_inner(visit, base_path) - } - - fn visit_pre_mut_inner<'a, Visitor>( - &'a mut self, - visit: &mut Visitor, - base_path: &str, - ) -> Result<()> - where - Visitor: FnMut(&mut FsEntry, &str, &str) -> Result<()>, - { - if let FsEntry::Dir(dir) = self { - for (name, sub_entry) in dir.iter_mut() { - visit(sub_entry, name, base_path)?; - } - for (name, sub_entry) in dir.iter_mut() { - let path = format!( - "{base_path}{}{name}", - if base_path.ends_with('/') { "" } else { "/" } - ); - sub_entry.visit_pre_mut_inner(visit, &path)?; - } - } - Ok(()) - } - - pub fn visit_bfs<'a, Visitor>(&'a self, base_path: &str, visit: &mut Visitor) -> Result<()> - where - Visitor: FnMut(&FsEntry, &str, &str, usize) -> Result<()>, - { - visit(self, base_path, "", 1)?; - let mut children_of = vec![(base_path.to_string(), self)]; - let mut next_children_of; - while children_of.len() > 0 { - next_children_of = Vec::new(); - FsEntry::visit_bfs_level(children_of, visit, &mut next_children_of)?; - children_of = next_children_of; - } - Ok(()) - } - - fn visit_bfs_level<'a, Visitor>( - children_of: Vec<(String, &'a FsEntry)>, - visit: &mut Visitor, - next_children_of: &mut Vec<(String, &'a FsEntry)>, - ) -> Result<()> - where - Visitor: FnMut(&FsEntry, &str, &str, usize) -> Result<()>, - { - // first we do a full len count at this depth to be able to predict the - // next depth offset position for children of this item from the current index - let mut child_offset = 0; - for (_, parent) in &children_of { - match parent { - FsEntry::Dir(dir) => { - child_offset += dir.iter().len(); - } - _ => {} - } - } - for (base_path, parent) in children_of { - match parent { - FsEntry::Dir(dir) => { - for (name, sub_entry) in dir.iter() { - visit(sub_entry, name, &base_path, child_offset)?; - child_offset -= 1; - let path = format!( - "{base_path}{}{name}", - if base_path.ends_with('/') { "" } else { "/" } - ); - next_children_of.push((path, sub_entry)); - if let FsEntry::Dir(dir) = sub_entry { - child_offset += dir.iter().len(); - } - } - } - _ => {} - } - } - Ok(()) - } -} - -// io flags -const FLAGS_ENABLE_STDIN: u32 = 1 << 0; -const FLAGS_ENABLE_STDOUT: u32 = 1 << 1; -const FLAGS_ENABLE_STDERR: u32 = 1 << 2; -const FLAGS_IGNORE_STDIN: u32 = 1 << 3; -const FLAGS_IGNORE_STDOUT: u32 = 1 << 4; -const FLAGS_IGNORE_STDERR: u32 = 1 << 5; -const FLAGS_HOST_PREOPENS: u32 = 1 << 6; -const FLAGS_HOST_PASSTHROUGH: u32 = 1 << 7; - -pub(crate) fn create_io_virt<'a>( - module: &'a mut Module, - fs: Option<&VirtFs>, - stdio: Option<&VirtStdio>, -) -> Result { - let mut virtual_files = BTreeMap::new(); - let mut flags: u32 = 0; - - if let Some(fs) = fs { - if fs.host_preopens { - flags |= FLAGS_HOST_PREOPENS; - } - } - let mut disable_stdio = true; - if let Some(stdio) = stdio { - match stdio.stdin { - StdioCfg::Allow => { - flags |= FLAGS_ENABLE_STDIN; - disable_stdio = false; - } - StdioCfg::Ignore => flags |= FLAGS_IGNORE_STDIN, - // deny is the default - StdioCfg::Deny => {} - } - match stdio.stdout { - StdioCfg::Allow => { - flags |= FLAGS_ENABLE_STDOUT; - disable_stdio = false; - } - StdioCfg::Ignore => flags |= FLAGS_IGNORE_STDOUT, - StdioCfg::Deny => {} - } - match stdio.stderr { - StdioCfg::Allow => { - flags |= FLAGS_ENABLE_STDERR; - disable_stdio = false; - } - StdioCfg::Ignore => flags |= FLAGS_IGNORE_STDERR, - StdioCfg::Deny => {} - } - } else { - strip_stdio_virt(module)?; - } - if disable_stdio { - stub_stdio_virt(module)?; - } - - // First we iterate the options and fill in all HostDir and HostFile entries - // With inline directory and file entries - let fs = if let Some(fs) = fs { - let mut fs = fs.clone(); - for (name, entry) in fs.preopens.iter_mut() { - entry.visit_pre_mut(name, &mut |entry, name, path| { - match entry { - FsEntry::Source(source) => { - *entry = FsEntry::File(source.as_bytes().to_vec()) - }, - FsEntry::Virtualize(host_path) => { - // read a directory or file path from the host - let metadata = fs::metadata(&host_path)?; - if metadata.is_dir() { - let mut entries: BTreeMap = BTreeMap::new(); - for entry in fs::read_dir(&host_path)? { - let entry = entry?; - let file_name = entry.file_name(); - let file_name_str = file_name.to_str().unwrap(); - let mut full_path = host_path.clone(); - if !full_path.ends_with('/') { - full_path.push('/'); - } - full_path.push_str(file_name_str); - virtual_files.insert(format!( - "{path}{}{name}{}{file_name_str}", - if path.len() > 0 && !path.ends_with('/') { "/" } else { "" }, - if name.len() > 0 && !name.ends_with('/') { "/" } else { "" } - ), full_path.to_string()); - entries.insert(file_name_str.into(), FsEntry::Virtualize(full_path)); - } - *entry = FsEntry::Dir(entries); - } else { - if !metadata.is_file() { - bail!("Only files and directories are currently supported for host paths to virtualize"); - } - let bytes = fs::read(&host_path)?; - *entry = FsEntry::File(bytes) - } - } - FsEntry::File(_) | FsEntry::RuntimeFile(_) | FsEntry::RuntimeDir(_) | FsEntry::Symlink(_) | FsEntry::Dir(_) => {} - } - Ok(()) - })?; - } - Some(fs) - } else { - None - }; - - // Create the data section bytes - let mut data_section = Data::new(get_stack_global(module)? as usize); - let mut fs_passthrough = if let Some(fs) = &fs { - fs.host_preopens - } else { - false - }; - - // Next we linearize the bfs-order directory graph as the static file data - // Using a pre-order traversal - // Each parent node is formed along with its child length and deep subgraph - // length. - let mut static_fs_data: Vec = Vec::new(); - let mut preopen_indices: Vec = Vec::new(); - if let Some(fs) = &fs { - for (name, entry) in &fs.preopens { - preopen_indices.push(static_fs_data.len() as u32); - let mut cur_idx = 0; - entry.visit_bfs(name, &mut |entry, name, _path, child_offset| { - let name_str_ptr = data_section.string(name)?; - let (ty, data) = match &entry { - // removed during previous step - FsEntry::Virtualize(_) | FsEntry::Source(_) => unreachable!(), - FsEntry::Symlink(_) => todo!("symlink support"), - FsEntry::RuntimeFile(path) => { - fs_passthrough = true; - let str = data_section.string(path)?; - ( - StaticIndexType::RuntimeHostFile, - StaticFileData { host_path: str }, - ) - } - FsEntry::RuntimeDir(path) => { - fs_passthrough = true; - let str = data_section.string(path)?; - ( - StaticIndexType::RuntimeHostDir, - StaticFileData { host_path: str }, - ) - } - FsEntry::Dir(dir) => ( - StaticIndexType::Dir, - StaticFileData { - dir: (child_offset as u32, dir.len() as u32), - }, - ), - FsEntry::File(bytes) => { - let byte_len = bytes.len(); - if byte_len > fs.passive_cutoff.unwrap_or(1024) as usize { - let passive_idx = data_section.passive_bytes(bytes); - ( - StaticIndexType::PassiveFile, - StaticFileData { - passive: (passive_idx, bytes.len() as u32), - }, - ) - } else { - let ptr = data_section.stack_bytes(bytes)?; - ( - StaticIndexType::ActiveFile, - StaticFileData { - active: (ptr, bytes.len() as u32), - }, - ) - } - } - }; - static_fs_data.push(StaticIndexEntry { - name: name_str_ptr as u32, - ty, - data, - }); - cur_idx += 1; - Ok(()) - })?; - } - } - - // now write the linearized static index entry section into the data - let static_index_addr = data_section.write_slice(static_fs_data.as_slice())?; - - let memory = module.memories.iter().nth(0).unwrap().id(); - - let io_ptr_addr = { - let io_ptr_export = module - .exports - .iter() - .find(|expt| expt.name.as_str() == "io") - .context("Virt adapter 'io' is not exported")?; - let ExportItem::Global(io_ptr_global) = io_ptr_export.item else { - bail!("Virt adapter 'io' not a global"); - }; - let GlobalKind::Local(InitExpr::Value(Value::I32(io_ptr_addr))) = - &module.globals.get(io_ptr_global).kind - else { - bail!("Virt adapter 'io' not a local I32 global value"); - }; - *io_ptr_addr as u32 - }; - - // If host fs is disabled, remove its imports entirely - // replacing it with a stub panic - if !fs_passthrough { - if disable_stdio { - stub_io_virt(module)?; - } - stub_fs_virt(module, true)?; - } else { - flags |= FLAGS_HOST_PASSTHROUGH; - } - - let (data, data_offset) = get_active_data_segment(module, memory, io_ptr_addr)?; - - let preopen_addr = data_section.write_slice(preopen_indices.as_slice())?; - - 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); - } - - let bytes = data.value.as_mut_slice(); - - // In the existing static data segment, update the static data options. - // - // From virtual-adapter/src/io.rs: - // - // #[repr(C)] - // pub static mut io: Io = Io { - // preopen_cnt: 0, // [byte 0] - // preopens: 0 as *const usize, // [byte 4] - // static_index_cnt: 0, // [byte 8] - // static_index: 0 as *const StaticIndexEntry, // [byte 12] - // flags: 0 // [byte 16] - // }; - if let Some(fs) = &fs { - 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()); - bytes[data_offset + 8..data_offset + 12] - .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()); - - bytes[data_offset + 16..data_offset + 20].copy_from_slice(&flags.to_le_bytes()); - - data_section.finish(module)?; - - // return the processed virtualized filesystem - Ok(virtual_files) -} - -// Stubs must be _comprehensive_ in order to act as full deny over entire subsystem -// when stubbing functions that are not part of the virtual adapter exports, we therefore -// have to create this functions fresh. -// Ideally, we should generate these stubs automatically from WASI definitions. -pub(crate) fn stub_fs_virt(module: &mut Module, uses_fs: bool) -> Result<()> { - stub_imported_func( - module, - "wasi:filesystem/preopens", - "get-directories", - uses_fs, - )?; - stub_imported_func(module, "wasi:filesystem/types", "read-via-stream", uses_fs)?; - stub_imported_func(module, "wasi:filesystem/types", "write-via-stream", false)?; - stub_imported_func(module, "wasi:filesystem/types", "append-via-stream", false)?; - stub_imported_func(module, "wasi:filesystem/types", "advise", false)?; - stub_imported_func(module, "wasi:filesystem/types", "sync-data", false)?; - stub_imported_func(module, "wasi:filesystem/types", "get-flags", false)?; - stub_imported_func(module, "wasi:filesystem/types", "get-type", uses_fs)?; - stub_imported_func(module, "wasi:filesystem/types", "set-size", false)?; - stub_imported_func(module, "wasi:filesystem/types", "set-times", false)?; - stub_imported_func(module, "wasi:filesystem/types", "read", uses_fs)?; - stub_imported_func(module, "wasi:filesystem/types", "write", false)?; - stub_imported_func(module, "wasi:filesystem/types", "read-directory", false)?; - stub_imported_func(module, "wasi:filesystem/types", "sync", false)?; - stub_imported_func( - module, - "wasi:filesystem/types", - "create-directory-at", - false, - )?; - stub_imported_func(module, "wasi:filesystem/types", "stat", uses_fs)?; - stub_imported_func(module, "wasi:filesystem/types", "stat-at", uses_fs)?; - stub_imported_func(module, "wasi:filesystem/types", "set-times-at", false)?; - stub_imported_func(module, "wasi:filesystem/types", "link-at", false)?; - stub_imported_func(module, "wasi:filesystem/types", "open-at", uses_fs)?; - stub_imported_func(module, "wasi:filesystem/types", "readlink-at", false)?; - stub_imported_func( - module, - "wasi:filesystem/types", - "remove-directory-at", - false, - )?; - stub_imported_func(module, "wasi:filesystem/types", "rename-at", false)?; - stub_imported_func(module, "wasi:filesystem/types", "symlink-at", false)?; - stub_imported_func(module, "wasi:filesystem/types", "access-at", false)?; - stub_imported_func(module, "wasi:filesystem/types", "unlink-file-at", false)?; - stub_imported_func( - module, - "wasi:filesystem/types", - "change-file-permissions-at", - false, - )?; - stub_imported_func( - module, - "wasi:filesystem/types", - "change-directory-permissions-at", - false, - )?; - stub_imported_func(module, "wasi:filesystem/types", "lock-shared", false)?; - stub_imported_func(module, "wasi:filesystem/types", "lock-exclusive", false)?; - stub_imported_func(module, "wasi:filesystem/types", "try-lock-shared", false)?; - stub_imported_func(module, "wasi:filesystem/types", "try-lock-exclusive", false)?; - stub_imported_func(module, "wasi:filesystem/types", "unlock", false)?; - stub_imported_func(module, "wasi:filesystem/types", "drop-descriptor", uses_fs)?; - stub_imported_func( - module, - "wasi:filesystem/types", - "read-directory-entry", - uses_fs, - )?; - stub_imported_func( - module, - "wasi:filesystem/types", - "drop-directory-entry-stream", - uses_fs, - )?; - stub_imported_func(module, "wasi:filesystem/types", "is-same-object", uses_fs)?; - stub_imported_func(module, "wasi:filesystem/types", "metadata-hash", false)?; - stub_imported_func(module, "wasi:filesystem/types", "metadata-hash-at", false)?; - Ok(()) -} - -fn stub_io_virt(module: &mut Module) -> Result<()> { - stub_imported_func(module, "wasi:poll/poll", "drop-pollable", true)?; - stub_imported_func(module, "wasi:poll/poll", "poll-oneoff", true)?; - stub_imported_func(module, "wasi:io/streams", "read", false)?; - stub_imported_func(module, "wasi:io/streams", "blocking-read", true)?; - stub_imported_func(module, "wasi:io/streams", "skip", true)?; - stub_imported_func(module, "wasi:io/streams", "blocking-skip", true)?; - stub_imported_func(module, "wasi:io/streams", "subscribe-to-input-stream", true)?; - stub_imported_func(module, "wasi:io/streams", "drop-input-stream", true)?; - stub_imported_func(module, "wasi:io/streams", "write", true)?; - stub_imported_func(module, "wasi:io/streams", "blocking-write-and-flush", false)?; - stub_imported_func(module, "wasi:io/streams", "flush", false)?; - stub_imported_func(module, "wasi:io/streams", "blocking-flush", false)?; - stub_imported_func(module, "wasi:io/streams", "check-write", false)?; - stub_imported_func(module, "wasi:io/streams", "write-zeroes", true)?; - stub_imported_func(module, "wasi:io/streams", "splice", true)?; - stub_imported_func(module, "wasi:io/streams", "blocking-splice", true)?; - stub_imported_func(module, "wasi:io/streams", "forward", true)?; - stub_imported_func( - module, - "wasi:io/streams", - "subscribe-to-output-stream", - true, - )?; - stub_imported_func(module, "wasi:io/streams", "drop-output-stream", true)?; - Ok(()) -} - -pub(crate) fn stub_clocks_virt(module: &mut Module) -> Result<()> { - stub_imported_func(module, "wasi:clocks/monotonic-clock", "now", true)?; - stub_imported_func(module, "wasi:clocks/monotonic-clock", "resolution", true)?; - stub_imported_func(module, "wasi:clocks/monotonic-clock", "subscribe", true)?; - Ok(()) -} - -pub(crate) fn stub_stdio_virt(module: &mut Module) -> Result<()> { - stub_imported_func(module, "wasi:cli/stdin", "get-stdin", false)?; - stub_imported_func(module, "wasi:cli/stdout", "get-stdout", false)?; - stub_imported_func(module, "wasi:cli/stderr", "get-stderr", false)?; - stub_imported_func( - module, - "wasi:cli/terminal-stdin", - "get-terminal-stdin", - false, - )?; - stub_imported_func( - module, - "wasi:cli/terminal-stdout", - "get-terminal-stdout", - false, - )?; - stub_imported_func( - module, - "wasi:cli/terminal-stderr", - "get-terminal-stderr", - false, - )?; - stub_imported_func( - module, - "wasi:cli/terminal-input", - "drop-terminal-input", - false, - )?; - stub_imported_func( - module, - "wasi:cli/terminal-output", - "drop-terminal-output", - false, - )?; - Ok(()) -} - -pub(crate) fn stub_sockets_virt(module: &mut Module) -> Result<()> { - stub_imported_func( - module, - "wasi:sockets/ip-name-lookup", - "resolve-addresses", - true, - )?; - stub_imported_func( - module, - "wasi:sockets/ip-name-lookup", - "resolve-next-address", - true, - )?; - stub_imported_func( - module, - "wasi:sockets/ip-name-lookup", - "drop-resolve-address-stream", - true, - )?; - stub_imported_func(module, "wasi:sockets/ip-name-lookup", "subscribe", true)?; - - stub_imported_func(module, "wasi:sockets/tcp", "start-bind", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "finish-bind", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "start-connect", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "finish-connect", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "start-listen", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "finish-listen", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "accept", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "local-address", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "remote-address", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "address-family", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "ipv6-only", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "set-ipv6-only", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "set-listen-backlog-size", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "keep-alive", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "set-keep-alive", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "no-delay", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "set-no-delay", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "unicast-hop-limit", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "set-unicast-hop-limit", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "receive-buffer-size", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "set-receive-buffer-size", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "send-buffer-size", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "set-send-buffer-size", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "subscribe", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "shutdown", true)?; - stub_imported_func(module, "wasi:sockets/tcp", "drop-tcp-socket", true)?; - - stub_imported_func(module, "wasi:sockets/udp", "start-bind", true)?; - stub_imported_func(module, "wasi:sockets/udp", "finish-bind", true)?; - stub_imported_func(module, "wasi:sockets/udp", "start-connect", true)?; - stub_imported_func(module, "wasi:sockets/udp", "finish-connect", true)?; - stub_imported_func(module, "wasi:sockets/udp", "receive", true)?; - stub_imported_func(module, "wasi:sockets/udp", "send", true)?; - stub_imported_func(module, "wasi:sockets/udp", "local-address", true)?; - stub_imported_func(module, "wasi:sockets/udp", "remote-address", true)?; - stub_imported_func(module, "wasi:sockets/udp", "address-family", true)?; - stub_imported_func(module, "wasi:sockets/udp", "ipv6-only", true)?; - stub_imported_func(module, "wasi:sockets/udp", "set-ipv6-only", true)?; - stub_imported_func(module, "wasi:sockets/udp", "unicast-hop-limit", true)?; - stub_imported_func(module, "wasi:sockets/udp", "set-unicast-hop-limit", true)?; - stub_imported_func(module, "wasi:sockets/udp", "receive-buffer-size", true)?; - stub_imported_func(module, "wasi:sockets/udp", "set-receive-buffer-size", true)?; - stub_imported_func(module, "wasi:sockets/udp", "send-buffer-size", true)?; - stub_imported_func(module, "wasi:sockets/udp", "set-send-buffer-size", true)?; - stub_imported_func(module, "wasi:sockets/udp", "subscribe", true)?; - stub_imported_func(module, "wasi:sockets/udp", "drop-udp-socket", true)?; - - Ok(()) -} - -// strip functions only have to dce the virtual adapter -pub(crate) fn strip_fs_virt(module: &mut Module) -> Result<()> { - stub_fs_virt(module, false)?; - remove_exported_func(module, "wasi:filesystem/preopens#get-directories")?; - - remove_exported_func(module, "wasi:filesystem/types#read-via-stream")?; - remove_exported_func(module, "wasi:filesystem/types#write-via-stream")?; - remove_exported_func(module, "wasi:filesystem/types#append-via-stream")?; - remove_exported_func(module, "wasi:filesystem/types#advise")?; - remove_exported_func(module, "wasi:filesystem/types#sync-data")?; - remove_exported_func(module, "wasi:filesystem/types#get-flags")?; - remove_exported_func(module, "wasi:filesystem/types#get-type")?; - remove_exported_func(module, "wasi:filesystem/types#set-size")?; - remove_exported_func(module, "wasi:filesystem/types#set-times")?; - remove_exported_func(module, "wasi:filesystem/types#read")?; - remove_exported_func(module, "wasi:filesystem/types#write")?; - remove_exported_func(module, "wasi:filesystem/types#read-directory")?; - remove_exported_func(module, "wasi:filesystem/types#sync")?; - remove_exported_func(module, "wasi:filesystem/types#create-directory-at")?; - remove_exported_func(module, "wasi:filesystem/types#stat")?; - remove_exported_func(module, "wasi:filesystem/types#stat-at")?; - remove_exported_func(module, "wasi:filesystem/types#set-times-at")?; - remove_exported_func(module, "wasi:filesystem/types#link-at")?; - remove_exported_func(module, "wasi:filesystem/types#open-at")?; - remove_exported_func(module, "wasi:filesystem/types#readlink-at")?; - remove_exported_func(module, "wasi:filesystem/types#remove-directory-at")?; - remove_exported_func(module, "wasi:filesystem/types#rename-at")?; - remove_exported_func(module, "wasi:filesystem/types#symlink-at")?; - remove_exported_func(module, "wasi:filesystem/types#access-at")?; - remove_exported_func(module, "wasi:filesystem/types#unlink-file-at")?; - remove_exported_func(module, "wasi:filesystem/types#change-file-permissions-at")?; - remove_exported_func( - module, - "wasi:filesystem/types#change-directory-permissions-at", - )?; - remove_exported_func(module, "wasi:filesystem/types#lock-shared")?; - remove_exported_func(module, "wasi:filesystem/types#lock-exclusive")?; - remove_exported_func(module, "wasi:filesystem/types#try-lock-shared")?; - remove_exported_func(module, "wasi:filesystem/types#try-lock-exclusive")?; - remove_exported_func(module, "wasi:filesystem/types#unlock")?; - remove_exported_func(module, "wasi:filesystem/types#drop-descriptor")?; - remove_exported_func(module, "wasi:filesystem/types#read-directory-entry")?; - remove_exported_func(module, "wasi:filesystem/types#drop-directory-entry-stream")?; - Ok(()) -} - -pub(crate) fn strip_clocks_virt(module: &mut Module) -> Result<()> { - stub_clocks_virt(module)?; - remove_exported_func(module, "wasi:clocks/monotonic-clock#now")?; - remove_exported_func(module, "wasi:clocks/monotonic-clock#resolution")?; - remove_exported_func(module, "wasi:clocks/monotonic-clock#subscribe")?; - Ok(()) -} - -pub(crate) fn strip_http_virt(module: &mut Module) -> Result<()> { - stub_http_virt(module)?; - remove_exported_func(module, "wasi:http/types#drop-fields")?; - remove_exported_func(module, "wasi:http/types#new-fields")?; - remove_exported_func(module, "wasi:http/types#fields-get")?; - remove_exported_func(module, "wasi:http/types#fields-set")?; - remove_exported_func(module, "wasi:http/types#fields-delete")?; - remove_exported_func(module, "wasi:http/types#fields-append")?; - remove_exported_func(module, "wasi:http/types#fields-entries")?; - remove_exported_func(module, "wasi:http/types#fields-clone")?; - remove_exported_func(module, "wasi:http/types#finish-incoming-stream")?; - remove_exported_func(module, "wasi:http/types#finish-outgoing-stream")?; - remove_exported_func(module, "wasi:http/types#drop-incoming-request")?; - remove_exported_func(module, "wasi:http/types#drop-outgoing-request")?; - remove_exported_func(module, "wasi:http/types#incoming-request-method")?; - remove_exported_func(module, "wasi:http/types#incoming-request-path-with-query")?; - remove_exported_func(module, "wasi:http/types#incoming-request-scheme")?; - remove_exported_func(module, "wasi:http/types#incoming-request-authority")?; - remove_exported_func(module, "wasi:http/types#incoming-request-headers")?; - remove_exported_func(module, "wasi:http/types#incoming-request-consume")?; - remove_exported_func(module, "wasi:http/types#new-outgoing-request")?; - remove_exported_func(module, "wasi:http/types#outgoing-request-write")?; - remove_exported_func(module, "wasi:http/types#drop-response-outparam")?; - remove_exported_func(module, "wasi:http/types#set-response-outparam")?; - remove_exported_func(module, "wasi:http/types#drop-incoming-response")?; - remove_exported_func(module, "wasi:http/types#drop-outgoing-response")?; - remove_exported_func(module, "wasi:http/types#incoming-response-status")?; - remove_exported_func(module, "wasi:http/types#incoming-response-headers")?; - remove_exported_func(module, "wasi:http/types#incoming-response-consume")?; - remove_exported_func(module, "wasi:http/types#new-outgoing-response")?; - remove_exported_func(module, "wasi:http/types#outgoing-response-write")?; - remove_exported_func(module, "wasi:http/types#drop-future-incoming-response")?; - remove_exported_func(module, "wasi:http/types#future-incoming-response-get")?; - remove_exported_func(module, "wasi:http/types#listen-to-future-incoming-response")?; - Ok(()) -} - -pub(crate) fn stub_http_virt(module: &mut Module) -> Result<()> { - stub_imported_func(module, "wasi:http/types", "drop-fields", false)?; - stub_imported_func(module, "wasi:http/types", "new-fields", false)?; - stub_imported_func(module, "wasi:http/types", "fields-get", false)?; - stub_imported_func(module, "wasi:http/types", "fields-set", false)?; - stub_imported_func(module, "wasi:http/types", "fields-delete", false)?; - stub_imported_func(module, "wasi:http/types", "fields-append", false)?; - stub_imported_func(module, "wasi:http/types", "fields-entries", false)?; - stub_imported_func(module, "wasi:http/types", "fields-clone", false)?; - stub_imported_func(module, "wasi:http/types", "finish-incoming-stream", false)?; - stub_imported_func(module, "wasi:http/types", "finish-outgoing-stream", false)?; - stub_imported_func(module, "wasi:http/types", "drop-incoming-request", false)?; - stub_imported_func(module, "wasi:http/types", "drop-outgoing-request", false)?; - stub_imported_func(module, "wasi:http/types", "incoming-request-method", false)?; - stub_imported_func( - module, - "wasi:http/types", - "incoming-request-path-with-query", - false, - )?; - stub_imported_func(module, "wasi:http/types", "incoming-request-scheme", false)?; - stub_imported_func( - module, - "wasi:http/types", - "incoming-request-authority", - false, - )?; - stub_imported_func(module, "wasi:http/types", "incoming-request-headers", false)?; - stub_imported_func(module, "wasi:http/types", "incoming-request-consume", false)?; - stub_imported_func(module, "wasi:http/types", "new-outgoing-request", false)?; - stub_imported_func(module, "wasi:http/types", "outgoing-request-write", false)?; - stub_imported_func(module, "wasi:http/types", "drop-response-outparam", false)?; - stub_imported_func(module, "wasi:http/types", "set-response-outparam", false)?; - stub_imported_func(module, "wasi:http/types", "drop-incoming-response", false)?; - stub_imported_func(module, "wasi:http/types", "drop-outgoing-response", false)?; - stub_imported_func(module, "wasi:http/types", "incoming-response-status", false)?; - stub_imported_func( - module, - "wasi:http/types", - "incoming-response-headers", - false, - )?; - stub_imported_func( - module, - "wasi:http/types", - "incoming-response-consume", - false, - )?; - stub_imported_func(module, "wasi:http/types", "new-outgoing-response", false)?; - stub_imported_func(module, "wasi:http/types", "outgoing-response-write", false)?; - stub_imported_func( - module, - "wasi:http/types", - "drop-future-incoming-response", - false, - )?; - stub_imported_func( - module, - "wasi:http/types", - "future-incoming-response-get", - false, - )?; - stub_imported_func( - module, - "wasi:http/types", - "listen-to-future-incoming-response", - false, - )?; - Ok(()) -} - -pub(crate) fn strip_stdio_virt(module: &mut Module) -> Result<()> { - stub_stdio_virt(module)?; - remove_exported_func(module, "wasi:cli/stdin#get-stdin")?; - remove_exported_func(module, "wasi:cli/stdout#get-stdout")?; - remove_exported_func(module, "wasi:cli/stderr#get-stderr")?; - remove_exported_func(module, "wasi:cli/terminal-stdin#get-terminal-stdin")?; - remove_exported_func(module, "wasi:cli/terminal-stdout#get-terminal-stdout")?; - remove_exported_func(module, "wasi:cli/terminal-stderr#get-terminal-stderr")?; - remove_exported_func(module, "wasi:cli/terminal-input#drop-terminal-input")?; - remove_exported_func(module, "wasi:cli/terminal-output#drop-terminal-output")?; - Ok(()) -} - -pub(crate) fn strip_io_virt(module: &mut Module) -> Result<()> { - stub_io_virt(module)?; - remove_exported_func(module, "wasi:io/streams#read")?; - remove_exported_func(module, "wasi:io/streams#blocking-read")?; - remove_exported_func(module, "wasi:io/streams#skip")?; - remove_exported_func(module, "wasi:io/streams#blocking-skip")?; - remove_exported_func(module, "wasi:io/streams#subscribe-to-input-stream")?; - remove_exported_func(module, "wasi:io/streams#drop-input-stream")?; - remove_exported_func(module, "wasi:io/streams#write")?; - remove_exported_func(module, "wasi:io/streams#blocking-write-and-flush")?; - remove_exported_func(module, "wasi:io/streams#flush")?; - remove_exported_func(module, "wasi:io/streams#blocking-flush")?; - remove_exported_func(module, "wasi:io/streams#check-write")?; - remove_exported_func(module, "wasi:io/streams#write-zeroes")?; - remove_exported_func(module, "wasi:io/streams#splice")?; - remove_exported_func(module, "wasi:io/streams#blocking-splice")?; - remove_exported_func(module, "wasi:io/streams#forward")?; - remove_exported_func(module, "wasi:io/streams#subscribe-to-output-stream")?; - remove_exported_func(module, "wasi:io/streams#drop-output-stream")?; - - remove_exported_func(module, "wasi:poll/poll#drop-pollable")?; - remove_exported_func(module, "wasi:poll/poll#poll-oneoff")?; - Ok(()) -} - -pub(crate) fn strip_sockets_virt(module: &mut Module) -> Result<()> { - stub_sockets_virt(module)?; - remove_exported_func(module, "wasi:sockets/ip-name-lookup#resolve-addresses")?; - remove_exported_func(module, "wasi:sockets/ip-name-lookup#resolve-next-address")?; - remove_exported_func( - module, - "wasi:sockets/ip-name-lookup#drop-resolve-address-stream", - )?; - remove_exported_func(module, "wasi:sockets/ip-name-lookup#subscribe")?; - - remove_exported_func(module, "wasi:sockets/tcp#start-bind")?; - remove_exported_func(module, "wasi:sockets/tcp#finish-bind")?; - remove_exported_func(module, "wasi:sockets/tcp#start-connect")?; - remove_exported_func(module, "wasi:sockets/tcp#finish-connect")?; - remove_exported_func(module, "wasi:sockets/tcp#start-listen")?; - remove_exported_func(module, "wasi:sockets/tcp#finish-listen")?; - remove_exported_func(module, "wasi:sockets/tcp#accept")?; - remove_exported_func(module, "wasi:sockets/tcp#local-address")?; - remove_exported_func(module, "wasi:sockets/tcp#remote-address")?; - remove_exported_func(module, "wasi:sockets/tcp#address-family")?; - remove_exported_func(module, "wasi:sockets/tcp#ipv6-only")?; - remove_exported_func(module, "wasi:sockets/tcp#set-ipv6-only")?; - remove_exported_func(module, "wasi:sockets/tcp#set-listen-backlog-size")?; - remove_exported_func(module, "wasi:sockets/tcp#keep-alive")?; - remove_exported_func(module, "wasi:sockets/tcp#set-keep-alive")?; - remove_exported_func(module, "wasi:sockets/tcp#no-delay")?; - remove_exported_func(module, "wasi:sockets/tcp#set-no-delay")?; - remove_exported_func(module, "wasi:sockets/tcp#unicast-hop-limit")?; - remove_exported_func(module, "wasi:sockets/tcp#set-unicast-hop-limit")?; - remove_exported_func(module, "wasi:sockets/tcp#receive-buffer-size")?; - remove_exported_func(module, "wasi:sockets/tcp#set-receive-buffer-size")?; - remove_exported_func(module, "wasi:sockets/tcp#send-buffer-size")?; - remove_exported_func(module, "wasi:sockets/tcp#set-send-buffer-size")?; - remove_exported_func(module, "wasi:sockets/tcp#subscribe")?; - remove_exported_func(module, "wasi:sockets/tcp#shutdown")?; - remove_exported_func(module, "wasi:sockets/tcp#drop-tcp-socket")?; - - remove_exported_func(module, "wasi:sockets/udp#start-bind")?; - remove_exported_func(module, "wasi:sockets/udp#finish-bind")?; - remove_exported_func(module, "wasi:sockets/udp#start-connect")?; - remove_exported_func(module, "wasi:sockets/udp#finish-connect")?; - remove_exported_func(module, "wasi:sockets/udp#receive")?; - remove_exported_func(module, "wasi:sockets/udp#send")?; - remove_exported_func(module, "wasi:sockets/udp#local-address")?; - remove_exported_func(module, "wasi:sockets/udp#remote-address")?; - remove_exported_func(module, "wasi:sockets/udp#address-family")?; - remove_exported_func(module, "wasi:sockets/udp#ipv6-only")?; - remove_exported_func(module, "wasi:sockets/udp#set-ipv6-only")?; - remove_exported_func(module, "wasi:sockets/udp#unicast-hop-limit")?; - remove_exported_func(module, "wasi:sockets/udp#set-unicast-hop-limit")?; - remove_exported_func(module, "wasi:sockets/udp#receive-buffer-size")?; - remove_exported_func(module, "wasi:sockets/udp#set-receive-buffer-size")?; - remove_exported_func(module, "wasi:sockets/udp#send-buffer-size")?; - remove_exported_func(module, "wasi:sockets/udp#set-send-buffer-size")?; - remove_exported_func(module, "wasi:sockets/udp#subscribe")?; - remove_exported_func(module, "wasi:sockets/udp#drop-udp-socket")?; - Ok(()) -} diff --git a/src/virt_io/clocks.rs b/src/virt_io/clocks.rs new file mode 100644 index 0000000..ba8ff42 --- /dev/null +++ b/src/virt_io/clocks.rs @@ -0,0 +1,69 @@ +use anyhow::{bail, Context, Result}; +use walrus::Module; + +use super::StubRequirement; + +/// Imports exposed by WASI for clocks functionality which are allowed to be +const WASI_CLOCKS_IMPORTS: [(&str, &str, &StubRequirement); 3] = [ + ( + "wasi:clocks/monotonic-clock", + "now", + &StubRequirement::Required, + ), + ( + "wasi:clocks/monotonic-clock", + "resolution", + &StubRequirement::Required, + ), + ( + "wasi:clocks/monotonic-clock", + "subscribe", + &StubRequirement::Required, + ), +]; + +/// Replace imported WASI functions that implement clocks access with no-ops +pub(crate) fn stub_clocks_virt(module: &mut Module) -> Result<()> { + for (module_name, func_name, stub_requirement) in WASI_CLOCKS_IMPORTS { + match stub_requirement { + StubRequirement::Required => { + let fid = module + .imports + .get_func(module_name, func_name) + .with_context(|| { + format!( + "failed to find required clocks import [{func_name}] in module [{module_name}]" + ) + })?; + module + .replace_imported_func(fid, |(body, _)| { + body.unreachable(); + }) + .with_context(|| { + "failed to stub clocks functionality [{}] in module [{export_name}]" + })?; + } + _ => bail!("unexpected stub requirement in imports for WASI clocks"), + } + } + Ok(()) +} + +/// Exported functions related to WASI clocks +const WASI_CLOCK_EXPORTS: [&str; 3] = [ + "wasi:clocks/monotonic-clock#now", + "wasi:clocks/monotonic-clock#resolution", + "wasi:clocks/monotonic-clock#subscribe", +]; + +/// Strip exported WASI functions that implement clock access +pub(crate) fn strip_clocks_virt(module: &mut Module) -> Result<()> { + stub_clocks_virt(module)?; + for export_name in WASI_CLOCK_EXPORTS { + module + .exports + .remove(export_name) + .with_context(|| format!("failed to strip WASI clocks function [{export_name}]"))?; + } + Ok(()) +} diff --git a/src/virt_io/filesystem.rs b/src/virt_io/filesystem.rs new file mode 100644 index 0000000..671ddf6 --- /dev/null +++ b/src/virt_io/filesystem.rs @@ -0,0 +1,292 @@ +use anyhow::{Context, Result}; +use walrus::Module; + +use super::StubRequirement; + +/// Imports exposed by WASI for Filesystem functionality +/// +/// Some are allowed to be missing, some are required depending on +/// whether the FS is used or not (`fs_used` in `stub_fs_virt`) +const WASI_FS_IMPORTS: [(&str, &str, &StubRequirement); 39] = [ + ( + "wasi:filesystem/types", + "access-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "advise", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "append-via-stream", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "change-directory-permissions-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "change-file-permissions-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "create-directory-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "get-flags", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "link-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "lock-exclusive", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "lock-shared", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "metadata-hash", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "metadata-hash-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "read-directory", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "readlink-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "remove-directory-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "rename-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "set-size", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "set-times", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "set-times-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "symlink-at", + &StubRequirement::Optional, + ), + ("wasi:filesystem/types", "sync", &StubRequirement::Optional), + ( + "wasi:filesystem/types", + "sync-data", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "try-lock-exclusive", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "try-lock-shared", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "unlink-file-at", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/types", + "unlock", + &StubRequirement::Optional, + ), + ("wasi:filesystem/types", "write", &StubRequirement::Optional), + ( + "wasi:filesystem/types", + "write-via-stream", + &StubRequirement::Optional, + ), + ( + "wasi:filesystem/preopens", + "get-directories", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "drop-descriptor", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "drop-directory-entry-stream", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "get-type", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "is-same-object", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "open-at", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "read", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "read-directory-entry", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "read-via-stream", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "stat", + &StubRequirement::DependsOnFsUsage, + ), + ( + "wasi:filesystem/types", + "stat-at", + &StubRequirement::DependsOnFsUsage, + ), +]; + +/// Replace imported WASI functions that implement filesystem access with no-ops +// Stubs must be _comprehensive_ in order to act as full deny over entire subsystem +// when stubbing functions that are not part of the virtual adapter exports, we therefore +// have to create this functions fresh. +// Ideally, we should generate these stubs automatically from WASI definitions. +pub(crate) fn stub_fs_virt(module: &mut Module, uses_fs: bool) -> Result<()> { + // Replace the filesystem functions that are allowed to be missing + for (module_name, func_name, stub_requirement) in WASI_FS_IMPORTS { + match (stub_requirement, uses_fs) { + // If the stub is always required, or depends on FS usage and uses_fs is set + // then we *must* find the function and replace it + (StubRequirement::Required, _) | (StubRequirement::DependsOnFsUsage, true) => { + let fid = module.imports.get_func(module_name, func_name) + .with_context(|| format!("failed to find required filesystem import [{func_name}] in module [{module_name}]"))?; + + module + .replace_imported_func(fid, |(body, _)| { + body.unreachable(); + }) + .with_context(|| { + "failed to stub filesystem functionality [{}] in module [{export_name}]" + })?; + } + // If the stub is optional, or required w/ FS usage and fs is not used, we can replace + // the functions optimistically, and not fail if they are missing + (StubRequirement::Optional, _) | (StubRequirement::DependsOnFsUsage, false) => { + if let Ok(fid) = module.imports.get_func(module_name, func_name) { + module + .replace_imported_func(fid, |(body, _)| { + body.unreachable(); + }) + .with_context(|| { + "failed to stub filesystem functionality [{}] in module [{export_name}]" + })?; + } + } + }; + } + Ok(()) +} + +const WASI_FILESYSTEM_EXPORTS: [&str; 36] = [ + "wasi:filesystem/preopens#get-directories", + "wasi:filesystem/types#read-via-stream", + "wasi:filesystem/types#write-via-stream", + "wasi:filesystem/types#append-via-stream", + "wasi:filesystem/types#advise", + "wasi:filesystem/types#sync-data", + "wasi:filesystem/types#get-flags", + "wasi:filesystem/types#get-type", + "wasi:filesystem/types#set-size", + "wasi:filesystem/types#set-times", + "wasi:filesystem/types#read", + "wasi:filesystem/types#write", + "wasi:filesystem/types#read-directory", + "wasi:filesystem/types#sync", + "wasi:filesystem/types#create-directory-at", + "wasi:filesystem/types#stat", + "wasi:filesystem/types#stat-at", + "wasi:filesystem/types#set-times-at", + "wasi:filesystem/types#link-at", + "wasi:filesystem/types#open-at", + "wasi:filesystem/types#readlink-at", + "wasi:filesystem/types#remove-directory-at", + "wasi:filesystem/types#rename-at", + "wasi:filesystem/types#symlink-at", + "wasi:filesystem/types#access-at", + "wasi:filesystem/types#unlink-file-at", + "wasi:filesystem/types#change-file-permissions-at", + "wasi:filesystem/types#change-directory-permissions-at", + "wasi:filesystem/types#lock-shared", + "wasi:filesystem/types#lock-exclusive", + "wasi:filesystem/types#try-lock-shared", + "wasi:filesystem/types#try-lock-exclusive", + "wasi:filesystem/types#unlock", + "wasi:filesystem/types#drop-descriptor", + "wasi:filesystem/types#read-directory-entry", + "wasi:filesystem/types#drop-directory-entry-stream", +]; + +/// Strip exported WASI functions that implement filesystem access +pub(crate) fn strip_fs_virt(module: &mut Module) -> Result<()> { + stub_fs_virt(module, false)?; + + for export_name in WASI_FILESYSTEM_EXPORTS { + module + .exports + .remove(export_name) + .with_context(|| format!("failed to strip WASI FS function [{export_name}]"))?; + } + + Ok(()) +} diff --git a/src/virt_io/http.rs b/src/virt_io/http.rs new file mode 100644 index 0000000..fb9e592 --- /dev/null +++ b/src/virt_io/http.rs @@ -0,0 +1,221 @@ +use anyhow::{bail, Context, Result}; +use walrus::Module; + +use super::StubRequirement; + +/// Exported functions related to WASI http +const WASI_HTTP_EXPORTS: [&str; 32] = [ + "wasi:http/types#drop-fields", + "wasi:http/types#new-fields", + "wasi:http/types#fields-get", + "wasi:http/types#fields-set", + "wasi:http/types#fields-delete", + "wasi:http/types#fields-append", + "wasi:http/types#fields-entries", + "wasi:http/types#fields-clone", + "wasi:http/types#finish-incoming-stream", + "wasi:http/types#finish-outgoing-stream", + "wasi:http/types#drop-incoming-request", + "wasi:http/types#drop-outgoing-request", + "wasi:http/types#incoming-request-method", + "wasi:http/types#incoming-request-path-with-query", + "wasi:http/types#incoming-request-scheme", + "wasi:http/types#incoming-request-authority", + "wasi:http/types#incoming-request-headers", + "wasi:http/types#incoming-request-consume", + "wasi:http/types#new-outgoing-request", + "wasi:http/types#outgoing-request-write", + "wasi:http/types#drop-response-outparam", + "wasi:http/types#set-response-outparam", + "wasi:http/types#drop-incoming-response", + "wasi:http/types#drop-outgoing-response", + "wasi:http/types#incoming-response-status", + "wasi:http/types#incoming-response-headers", + "wasi:http/types#incoming-response-consume", + "wasi:http/types#new-outgoing-response", + "wasi:http/types#outgoing-response-write", + "wasi:http/types#drop-future-incoming-response", + "wasi:http/types#future-incoming-response-get", + "wasi:http/types#listen-to-future-incoming-response", +]; + +/// Strip exported WASI functions that implement HTTP access +pub(crate) fn strip_http_virt(module: &mut Module) -> Result<()> { + stub_http_virt(module)?; + for export_name in WASI_HTTP_EXPORTS { + module + .exports + .remove(export_name) + .with_context(|| format!("failed to strip WASI HTTP function [{export_name}]"))?; + } + Ok(()) +} + +/// Imports exposed by WASI for HTTP functionality +const WASI_HTTP_IMPORTS: [(&str, &str, &StubRequirement); 32] = [ + ("wasi:http/types", "drop-fields", &StubRequirement::Optional), + ("wasi:http/types", "new-fields", &StubRequirement::Optional), + ("wasi:http/types", "fields-get", &StubRequirement::Optional), + ("wasi:http/types", "fields-set", &StubRequirement::Optional), + ( + "wasi:http/types", + "fields-delete", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "fields-append", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "fields-entries", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "fields-clone", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "finish-incoming-stream", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "finish-outgoing-stream", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "drop-incoming-request", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "drop-outgoing-request", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-request-method", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-request-path-with-query", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-request-scheme", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-request-authority", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-request-headers", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-request-consume", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "new-outgoing-request", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "outgoing-request-write", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "drop-response-outparam", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "set-response-outparam", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "drop-incoming-response", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "drop-outgoing-response", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-response-status", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-response-headers", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "incoming-response-consume", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "new-outgoing-response", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "outgoing-response-write", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "drop-future-incoming-response", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "future-incoming-response-get", + &StubRequirement::Optional, + ), + ( + "wasi:http/types", + "listen-to-future-incoming-response", + &StubRequirement::Optional, + ), +]; + +/// Replace imported WASI functions that implement HTTP access with no-ops +pub(crate) fn stub_http_virt(module: &mut Module) -> Result<()> { + for (module_name, func_name, stub_requirement) in WASI_HTTP_IMPORTS { + match stub_requirement { + StubRequirement::Optional => { + if let Ok(fid) = module.imports.get_func(module_name, func_name) { + module + .replace_imported_func(fid, |(body, _)| { + body.unreachable(); + }) + .with_context(|| { + "failed to stub WASI HTTP functionality [{}] in module [{export_name}]" + })?; + } + } + _ => bail!("unexpected stub requirement in imports for WASI HTTP"), + } + } + Ok(()) +} diff --git a/src/virt_io/io.rs b/src/virt_io/io.rs new file mode 100644 index 0000000..b3fc6b1 --- /dev/null +++ b/src/virt_io/io.rs @@ -0,0 +1,144 @@ +use anyhow::{bail, Context, Result}; +use walrus::Module; + +use super::StubRequirement; + +/// Imports exposed by WASI for IO functionality +/// +/// Some imports are required, and others are optional. +const WASI_IO_IMPORTS: [(&str, &str, &StubRequirement); 19] = [ + ( + "wasi:io/streams", + "blocking-flush", + &StubRequirement::Optional, + ), + ( + "wasi:io/streams", + "blocking-read", + &StubRequirement::Required, + ), + ( + "wasi:io/streams", + "blocking-skip", + &StubRequirement::Required, + ), + ( + "wasi:io/streams", + "blocking-splice", + &StubRequirement::Required, + ), + ( + "wasi:io/streams", + "blocking-write-and-flush", + &StubRequirement::Optional, + ), + ("wasi:io/streams", "check-write", &StubRequirement::Optional), + ( + "wasi:io/streams", + "drop-input-stream", + &StubRequirement::Required, + ), + ( + "wasi:io/streams", + "drop-output-stream", + &StubRequirement::Required, + ), + ("wasi:io/streams", "flush", &StubRequirement::Optional), + ("wasi:io/streams", "forward", &StubRequirement::Required), + ("wasi:io/streams", "read", &StubRequirement::Optional), + ("wasi:io/streams", "skip", &StubRequirement::Required), + ("wasi:io/streams", "splice", &StubRequirement::Required), + ( + "wasi:io/streams", + "subscribe-to-input-stream", + &StubRequirement::Required, + ), + ( + "wasi:io/streams", + "subscribe-to-output-stream", + &StubRequirement::Required, + ), + ("wasi:io/streams", "write", &StubRequirement::Required), + ( + "wasi:io/streams", + "write-zeroes", + &StubRequirement::Required, + ), + ( + "wasi:poll/poll", + "drop-pollable", + &StubRequirement::Required, + ), + ("wasi:poll/poll", "poll-oneoff", &StubRequirement::Required), +]; + +/// Replace imported WASI functions that implement general I/O access with no-ops +pub(crate) fn stub_io_virt(module: &mut Module) -> Result<()> { + // Replace the I/O functions that are allowed to be missing + for (module_name, func_name, stub_requirement) in WASI_IO_IMPORTS { + match stub_requirement { + // If the stub is always required we *must* find the function and replace it + StubRequirement::Required => { + let fid = module.imports.get_func(module_name, func_name) + .with_context(|| format!("failed to find required filesystem import [{func_name}] in module [{module_name}]"))?; + + module + .replace_imported_func(fid, |(body, _)| { + body.unreachable(); + }) + .with_context(|| { + "failed to stub filesystem functionality [{}] in module [{export_name}]" + })?; + } + // If the stub is optional, we can replace the functions optimistically, and not fail if they are missing + StubRequirement::Optional => { + if let Ok(fid) = module.imports.get_func(module_name, func_name) { + module + .replace_imported_func(fid, |(body, _)| { + body.unreachable(); + }) + .with_context(|| { + "failed to stub filesystem functionality [{}] in module [{export_name}]" + })?; + } + } + _ => bail!("unexpected stub requirement in imports for WASI I/O"), + }; + } + + Ok(()) +} + +/// Exported functions related to IO +const WASI_IO_EXPORTS: [&str; 19] = [ + "wasi:io/streams#blocking-flush", + "wasi:io/streams#blocking-read", + "wasi:io/streams#blocking-skip", + "wasi:io/streams#blocking-splice", + "wasi:io/streams#blocking-write-and-flush", + "wasi:io/streams#check-write", + "wasi:io/streams#drop-input-stream", + "wasi:io/streams#drop-output-stream", + "wasi:io/streams#flush", + "wasi:io/streams#forward", + "wasi:io/streams#read", + "wasi:io/streams#skip", + "wasi:io/streams#splice", + "wasi:io/streams#subscribe-to-input-stream", + "wasi:io/streams#subscribe-to-output-stream", + "wasi:io/streams#write", + "wasi:io/streams#write-zeroes", + "wasi:poll/poll#drop-pollable", + "wasi:poll/poll#poll-oneoff", +]; + +/// Strip exported WASI functions that implement IO (streams, polling) access +pub(crate) fn strip_io_virt(module: &mut Module) -> Result<()> { + stub_io_virt(module)?; + for export_name in WASI_IO_EXPORTS { + module.exports.remove(export_name).with_context(|| { + format!("failed to strip general I/O export function [{export_name}]") + })?; + } + Ok(()) +} diff --git a/src/virt_io/mod.rs b/src/virt_io/mod.rs new file mode 100644 index 0000000..c9f3409 --- /dev/null +++ b/src/virt_io/mod.rs @@ -0,0 +1,597 @@ +use std::{collections::BTreeMap, fmt, fs}; + +use anyhow::{bail, Context, Result}; +use clap::ValueEnum; +use serde::Deserialize; +use walrus::{ir::Value, ExportItem, GlobalKind, InitExpr, Module}; + +use crate::{ + data::{Data, WasmEncode}, + walrus_ops::{get_active_data_segment, get_stack_global}, +}; + +mod clocks; +mod filesystem; +mod http; +mod io; +mod sockets; +mod stdio; + +pub(crate) use clocks::{strip_clocks_virt, stub_clocks_virt}; +pub(crate) use filesystem::{strip_fs_virt, stub_fs_virt}; +pub(crate) use http::{strip_http_virt, stub_http_virt}; +pub(crate) use io::{strip_io_virt, stub_io_virt}; +pub(crate) use sockets::{strip_sockets_virt, stub_sockets_virt}; +pub(crate) use stdio::{strip_stdio_virt, stub_stdio_virt}; + +pub type VirtualFiles = BTreeMap; + +/// How to deal with a stubbed/stripped export +/// +/// This enum is used mostly for nearly-static data, which exists +/// to make it easy to iterate over stubbing modules & functions that must +/// be manipulated. +enum StubRequirement { + /// The import/export that is stubbed/stripped must be present *and* must be replaced + Required, + /// The import/export that is stubbed/stripped is allowed to be missing + Optional, + /// Whether the import/export must be present and replaced + /// depends on external factors (use-case specific), in this case + /// whether the filesystem is used or not + DependsOnFsUsage, +} + +#[derive(ValueEnum, Clone, Debug, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum StdioCfg { + #[default] + Allow, + Ignore, + Deny, +} + +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct VirtStdio { + pub stdin: StdioCfg, + pub stdout: StdioCfg, + pub stderr: StdioCfg, +} + +impl VirtStdio { + pub fn ignore(&mut self) -> &mut Self { + self.stdin = StdioCfg::Ignore; + self.stdout = StdioCfg::Ignore; + self.stderr = StdioCfg::Ignore; + self + } + pub fn allow(&mut self) -> &mut Self { + self.stdin = StdioCfg::Allow; + self.stdout = StdioCfg::Allow; + self.stderr = StdioCfg::Allow; + self + } + pub fn deny(&mut self) -> &mut Self { + self.stdin = StdioCfg::Deny; + self.stdout = StdioCfg::Deny; + self.stderr = StdioCfg::Deny; + self + } + pub fn stdin(&mut self, cfg: StdioCfg) -> &mut Self { + self.stdin = cfg; + self + } + pub fn stdout(&mut self, cfg: StdioCfg) -> &mut Self { + self.stdout = cfg; + self + } + pub fn stderr(&mut self, cfg: StdioCfg) -> &mut Self { + self.stderr = cfg; + self + } +} + +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct VirtFs { + /// Enable verbatim host preopens + #[serde(default)] + pub host_preopens: bool, + /// Filesystem state to virtualize + #[serde(default)] + pub preopens: BTreeMap, + /// A cutoff size in bytes, above which + /// files will be treated as passive segments. + /// Per-file control may also be provided. + pub passive_cutoff: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub enum FsEntry { + /// symlink absolute or relative file path on the virtual filesystem + Symlink(String), + /// host path at virtualization time + Virtualize(String), + /// host path st runtime + RuntimeDir(String), + RuntimeFile(String), + /// Virtual file + File(Vec), + /// String (UTF8) file source convenience + Source(String), + /// Virtual directory + Dir(VirtDir), +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct VirtFile { + pub bytes: Option>, + pub source: Option, +} + +type VirtDir = BTreeMap; + +impl VirtFs { + /// Deny host preopens at runtime + pub fn deny_host_preopens(&mut self) { + self.host_preopens = false; + } + /// Allow host preopens at runtime + pub fn allow_host_preopens(&mut self) { + self.host_preopens = true; + } + /// Add a preopen entry + pub fn preopen(&mut self, name: String, preopen: FsEntry) -> &mut Self { + self.preopens.insert(name, preopen); + self + } + /// Add a runtime preopen host mapping + pub fn host_preopen(&mut self, name: String, dir: String) -> &mut Self { + self.preopens.insert(name, FsEntry::RuntimeDir(dir)); + self + } + /// Add a preopen virtualized local directory (which will be globbed) + pub fn virtual_preopen(&mut self, name: String, dir: String) -> &mut Self { + self.preopens.insert(name, FsEntry::Virtualize(dir)); + self + } + /// Set the passive cutoff size in bytes for creating Wasm passive segments + pub fn passive_cutoff(&mut self, passive_cutoff: usize) -> &mut Self { + self.passive_cutoff = Some(passive_cutoff); + self + } +} + +#[derive(Debug)] +struct StaticIndexEntry { + name: u32, + ty: StaticIndexType, + data: StaticFileData, +} + +impl WasmEncode for StaticIndexEntry { + fn align() -> usize { + 4 + } + fn size() -> usize { + 16 + } + fn encode(&self, bytes: &mut [u8]) { + self.name.encode(&mut bytes[0..4]); + self.ty.encode(&mut bytes[4..8]); + self.data.encode(&mut bytes[8..16]); + } +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +#[repr(u32)] +enum StaticIndexType { + ActiveFile, + PassiveFile, + Dir, + RuntimeHostDir, + RuntimeHostFile, +} + +impl WasmEncode for StaticIndexType { + fn align() -> usize { + 4 + } + fn size() -> usize { + 4 + } + fn encode(&self, bytes: &mut [u8]) { + bytes[0..4].copy_from_slice(&(*self as u32).to_le_bytes()); + } +} + +union StaticFileData { + /// Active memory data pointer for ActiveFile + active: (u32, u32), + + /// Passive memory element index and len for PassiveFile + passive: (u32, u32), + + /// Host path string for HostDir / HostFile + host_path: u32, + + /// Pointer and child entry count for Dir + dir: (u32, u32), +} + +impl WasmEncode for StaticFileData { + fn align() -> usize { + 4 + } + fn size() -> usize { + 8 + } + fn encode(&self, bytes: &mut [u8]) { + bytes[0..4].copy_from_slice(&unsafe { self.dir.0.to_le_bytes() }); + bytes[4..8].copy_from_slice(&unsafe { self.dir.1.to_le_bytes() }); + } +} + +impl fmt::Debug for StaticFileData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&format!( + "STATIC [{:?}, {:?}]", + unsafe { self.dir.0 }, + unsafe { self.dir.1 } + ))?; + Ok(()) + } +} + +impl FsEntry { + fn visit_pre_mut<'a, Visitor>(&'a mut self, base_path: &str, visit: &mut Visitor) -> Result<()> + where + Visitor: FnMut(&mut FsEntry, &str, &str) -> Result<()>, + { + visit(self, base_path, "")?; + self.visit_pre_mut_inner(visit, base_path) + } + + fn visit_pre_mut_inner<'a, Visitor>( + &'a mut self, + visit: &mut Visitor, + base_path: &str, + ) -> Result<()> + where + Visitor: FnMut(&mut FsEntry, &str, &str) -> Result<()>, + { + if let FsEntry::Dir(dir) = self { + for (name, sub_entry) in dir.iter_mut() { + visit(sub_entry, name, base_path)?; + } + for (name, sub_entry) in dir.iter_mut() { + let path = format!( + "{base_path}{}{name}", + if base_path.ends_with('/') { "" } else { "/" } + ); + sub_entry.visit_pre_mut_inner(visit, &path)?; + } + } + Ok(()) + } + + pub fn visit_bfs<'a, Visitor>(&'a self, base_path: &str, visit: &mut Visitor) -> Result<()> + where + Visitor: FnMut(&FsEntry, &str, &str, usize) -> Result<()>, + { + visit(self, base_path, "", 1)?; + let mut children_of = vec![(base_path.to_string(), self)]; + let mut next_children_of; + while children_of.len() > 0 { + next_children_of = Vec::new(); + FsEntry::visit_bfs_level(children_of, visit, &mut next_children_of)?; + children_of = next_children_of; + } + Ok(()) + } + + fn visit_bfs_level<'a, Visitor>( + children_of: Vec<(String, &'a FsEntry)>, + visit: &mut Visitor, + next_children_of: &mut Vec<(String, &'a FsEntry)>, + ) -> Result<()> + where + Visitor: FnMut(&FsEntry, &str, &str, usize) -> Result<()>, + { + // first we do a full len count at this depth to be able to predict the + // next depth offset position for children of this item from the current index + let mut child_offset = 0; + for (_, parent) in &children_of { + match parent { + FsEntry::Dir(dir) => { + child_offset += dir.iter().len(); + } + _ => {} + } + } + for (base_path, parent) in children_of { + match parent { + FsEntry::Dir(dir) => { + for (name, sub_entry) in dir.iter() { + visit(sub_entry, name, &base_path, child_offset)?; + child_offset -= 1; + let path = format!( + "{base_path}{}{name}", + if base_path.ends_with('/') { "" } else { "/" } + ); + next_children_of.push((path, sub_entry)); + if let FsEntry::Dir(dir) = sub_entry { + child_offset += dir.iter().len(); + } + } + } + _ => {} + } + } + Ok(()) + } +} + +// io flags +const FLAGS_ENABLE_STDIN: u32 = 1 << 0; +const FLAGS_ENABLE_STDOUT: u32 = 1 << 1; +const FLAGS_ENABLE_STDERR: u32 = 1 << 2; +const FLAGS_IGNORE_STDIN: u32 = 1 << 3; +const FLAGS_IGNORE_STDOUT: u32 = 1 << 4; +const FLAGS_IGNORE_STDERR: u32 = 1 << 5; +const FLAGS_HOST_PREOPENS: u32 = 1 << 6; +const FLAGS_HOST_PASSTHROUGH: u32 = 1 << 7; + +pub(crate) fn create_io_virt<'a>( + module: &'a mut Module, + fs: Option<&VirtFs>, + stdio: Option<&VirtStdio>, +) -> Result { + let mut virtual_files = BTreeMap::new(); + let mut flags: u32 = 0; + + if let Some(fs) = fs { + if fs.host_preopens { + flags |= FLAGS_HOST_PREOPENS; + } + } + let mut disable_stdio = true; + if let Some(stdio) = stdio { + match stdio.stdin { + StdioCfg::Allow => { + flags |= FLAGS_ENABLE_STDIN; + disable_stdio = false; + } + StdioCfg::Ignore => flags |= FLAGS_IGNORE_STDIN, + // deny is the default + StdioCfg::Deny => {} + } + match stdio.stdout { + StdioCfg::Allow => { + flags |= FLAGS_ENABLE_STDOUT; + disable_stdio = false; + } + StdioCfg::Ignore => flags |= FLAGS_IGNORE_STDOUT, + StdioCfg::Deny => {} + } + match stdio.stderr { + StdioCfg::Allow => { + flags |= FLAGS_ENABLE_STDERR; + disable_stdio = false; + } + StdioCfg::Ignore => flags |= FLAGS_IGNORE_STDERR, + StdioCfg::Deny => {} + } + } else { + strip_stdio_virt(module)?; + } + if disable_stdio { + stub_stdio_virt(module)?; + } + + // First we iterate the options and fill in all HostDir and HostFile entries + // With inline directory and file entries + let fs = if let Some(fs) = fs { + let mut fs = fs.clone(); + for (name, entry) in fs.preopens.iter_mut() { + entry.visit_pre_mut(name, &mut |entry, name, path| { + match entry { + FsEntry::Source(source) => { + *entry = FsEntry::File(source.as_bytes().to_vec()) + }, + FsEntry::Virtualize(host_path) => { + // read a directory or file path from the host + let metadata = fs::metadata(&host_path)?; + if metadata.is_dir() { + let mut entries: BTreeMap = BTreeMap::new(); + for entry in fs::read_dir(&host_path)? { + let entry = entry?; + let file_name = entry.file_name(); + let file_name_str = file_name.to_str().unwrap(); + let mut full_path = host_path.clone(); + if !full_path.ends_with('/') { + full_path.push('/'); + } + full_path.push_str(file_name_str); + virtual_files.insert(format!( + "{path}{}{name}{}{file_name_str}", + if path.len() > 0 && !path.ends_with('/') { "/" } else { "" }, + if name.len() > 0 && !name.ends_with('/') { "/" } else { "" } + ), full_path.to_string()); + entries.insert(file_name_str.into(), FsEntry::Virtualize(full_path)); + } + *entry = FsEntry::Dir(entries); + } else { + if !metadata.is_file() { + bail!("Only files and directories are currently supported for host paths to virtualize"); + } + let bytes = fs::read(&host_path)?; + *entry = FsEntry::File(bytes) + } + } + FsEntry::File(_) | FsEntry::RuntimeFile(_) | FsEntry::RuntimeDir(_) | FsEntry::Symlink(_) | FsEntry::Dir(_) => {} + } + Ok(()) + })?; + } + Some(fs) + } else { + None + }; + + // Create the data section bytes + let mut data_section = Data::new(get_stack_global(module)? as usize); + let mut fs_passthrough = if let Some(fs) = &fs { + fs.host_preopens + } else { + false + }; + + // Next we linearize the bfs-order directory graph as the static file data + // Using a pre-order traversal + // Each parent node is formed along with its child length and deep subgraph + // length. + let mut static_fs_data: Vec = Vec::new(); + let mut preopen_indices: Vec = Vec::new(); + if let Some(fs) = &fs { + for (name, entry) in &fs.preopens { + preopen_indices.push(static_fs_data.len() as u32); + let mut cur_idx = 0; + entry.visit_bfs(name, &mut |entry, name, _path, child_offset| { + let name_str_ptr = data_section.string(name)?; + let (ty, data) = match &entry { + // removed during previous step + FsEntry::Virtualize(_) | FsEntry::Source(_) => unreachable!(), + FsEntry::Symlink(_) => todo!("symlink support"), + FsEntry::RuntimeFile(path) => { + fs_passthrough = true; + let str = data_section.string(path)?; + ( + StaticIndexType::RuntimeHostFile, + StaticFileData { host_path: str }, + ) + } + FsEntry::RuntimeDir(path) => { + fs_passthrough = true; + let str = data_section.string(path)?; + ( + StaticIndexType::RuntimeHostDir, + StaticFileData { host_path: str }, + ) + } + FsEntry::Dir(dir) => ( + StaticIndexType::Dir, + StaticFileData { + dir: (child_offset as u32, dir.len() as u32), + }, + ), + FsEntry::File(bytes) => { + let byte_len = bytes.len(); + if byte_len > fs.passive_cutoff.unwrap_or(1024) as usize { + let passive_idx = data_section.passive_bytes(bytes); + ( + StaticIndexType::PassiveFile, + StaticFileData { + passive: (passive_idx, bytes.len() as u32), + }, + ) + } else { + let ptr = data_section.stack_bytes(bytes)?; + ( + StaticIndexType::ActiveFile, + StaticFileData { + active: (ptr, bytes.len() as u32), + }, + ) + } + } + }; + static_fs_data.push(StaticIndexEntry { + name: name_str_ptr as u32, + ty, + data, + }); + cur_idx += 1; + Ok(()) + })?; + } + } + + // now write the linearized static index entry section into the data + let static_index_addr = data_section.write_slice(static_fs_data.as_slice())?; + + let memory = module.memories.iter().nth(0).unwrap().id(); + + let io_ptr_addr = { + let io_ptr_export = module + .exports + .iter() + .find(|expt| expt.name.as_str() == "io") + .context("Virt adapter 'io' is not exported")?; + let ExportItem::Global(io_ptr_global) = io_ptr_export.item else { + bail!("Virt adapter 'io' not a global"); + }; + let GlobalKind::Local(InitExpr::Value(Value::I32(io_ptr_addr))) = + &module.globals.get(io_ptr_global).kind + else { + bail!("Virt adapter 'io' not a local I32 global value"); + }; + *io_ptr_addr as u32 + }; + + // If host fs is disabled, remove its imports entirely + // replacing it with a stub panic + if !fs_passthrough { + if disable_stdio { + stub_io_virt(module)?; + } + stub_fs_virt(module, true)?; + } else { + flags |= FLAGS_HOST_PASSTHROUGH; + } + + let (data, data_offset) = get_active_data_segment(module, memory, io_ptr_addr)?; + + let preopen_addr = data_section.write_slice(preopen_indices.as_slice())?; + + 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); + } + + let bytes = data.value.as_mut_slice(); + + // In the existing static data segment, update the static data options. + // + // From virtual-adapter/src/io.rs: + // + // #[repr(C)] + // pub static mut io: Io = Io { + // preopen_cnt: 0, // [byte 0] + // preopens: 0 as *const usize, // [byte 4] + // static_index_cnt: 0, // [byte 8] + // static_index: 0 as *const StaticIndexEntry, // [byte 12] + // flags: 0 // [byte 16] + // }; + if let Some(fs) = &fs { + 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()); + bytes[data_offset + 8..data_offset + 12] + .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()); + + bytes[data_offset + 16..data_offset + 20].copy_from_slice(&flags.to_le_bytes()); + + data_section.finish(module)?; + + // return the processed virtualized filesystem + Ok(virtual_files) +} diff --git a/src/virt_io/sockets.rs b/src/virt_io/sockets.rs new file mode 100644 index 0000000..4bc6f94 --- /dev/null +++ b/src/virt_io/sockets.rs @@ -0,0 +1,297 @@ +use anyhow::{bail, Context, Result}; +use walrus::Module; + +use super::StubRequirement; + +/// Imports exposed by WASI for sockets functionality which are allowed to be missing +const WASI_SOCKETS_IMPORTS: [(&str, &str, &StubRequirement); 49] = [ + ( + "wasi:sockets/ip-name-lookup", + "resolve-addresses", + &StubRequirement::Required, + ), + ( + "wasi:sockets/ip-name-lookup", + "resolve-next-address", + &StubRequirement::Required, + ), + ( + "wasi:sockets/ip-name-lookup", + "drop-resolve-address-stream", + &StubRequirement::Required, + ), + ( + "wasi:sockets/ip-name-lookup", + "subscribe", + &StubRequirement::Required, + ), + ("wasi:sockets/tcp", "start-bind", &StubRequirement::Required), + ( + "wasi:sockets/tcp", + "finish-bind", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "start-connect", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "finish-connect", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "start-listen", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "finish-listen", + &StubRequirement::Required, + ), + ("wasi:sockets/tcp", "accept", &StubRequirement::Required), + ( + "wasi:sockets/tcp", + "local-address", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "remote-address", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "address-family", + &StubRequirement::Required, + ), + ("wasi:sockets/tcp", "ipv6-only", &StubRequirement::Required), + ( + "wasi:sockets/tcp", + "set-ipv6-only", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "set-listen-backlog-size", + &StubRequirement::Required, + ), + ("wasi:sockets/tcp", "keep-alive", &StubRequirement::Required), + ( + "wasi:sockets/tcp", + "set-keep-alive", + &StubRequirement::Required, + ), + ("wasi:sockets/tcp", "no-delay", &StubRequirement::Required), + ( + "wasi:sockets/tcp", + "set-no-delay", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "unicast-hop-limit", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "set-unicast-hop-limit", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "receive-buffer-size", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "set-receive-buffer-size", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "send-buffer-size", + &StubRequirement::Required, + ), + ( + "wasi:sockets/tcp", + "set-send-buffer-size", + &StubRequirement::Required, + ), + ("wasi:sockets/tcp", "subscribe", &StubRequirement::Required), + ("wasi:sockets/tcp", "shutdown", &StubRequirement::Required), + ( + "wasi:sockets/tcp", + "drop-tcp-socket", + &StubRequirement::Required, + ), + ("wasi:sockets/udp", "start-bind", &StubRequirement::Required), + ( + "wasi:sockets/udp", + "finish-bind", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "start-connect", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "finish-connect", + &StubRequirement::Required, + ), + ("wasi:sockets/udp", "receive", &StubRequirement::Required), + ("wasi:sockets/udp", "send", &StubRequirement::Required), + ( + "wasi:sockets/udp", + "local-address", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "remote-address", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "address-family", + &StubRequirement::Required, + ), + ("wasi:sockets/udp", "ipv6-only", &StubRequirement::Required), + ( + "wasi:sockets/udp", + "set-ipv6-only", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "unicast-hop-limit", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "set-unicast-hop-limit", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "receive-buffer-size", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "set-receive-buffer-size", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "send-buffer-size", + &StubRequirement::Required, + ), + ( + "wasi:sockets/udp", + "set-send-buffer-size", + &StubRequirement::Required, + ), + ("wasi:sockets/udp", "subscribe", &StubRequirement::Required), + ( + "wasi:sockets/udp", + "drop-udp-socket", + &StubRequirement::Required, + ), +]; + +/// Replace imported WASI functions that implement socket access with no-ops +pub(crate) fn stub_sockets_virt(module: &mut Module) -> Result<()> { + for (module_name, func_name, stub_requirement) in WASI_SOCKETS_IMPORTS { + match stub_requirement { + StubRequirement::Required => { + let fid = module + .imports + .get_func(module_name, func_name) + .with_context(|| { + format!( + "failed to find required clocks import [{func_name}] in module [{module_name}]" + ) + })?; + module + .replace_imported_func(fid, |(body, _)| { + body.unreachable(); + }) + .with_context(|| { + "failed to stub clocks functionality [{}] in module [{export_name}]" + })?; + } + _ => bail!("unexpected stub requirement in imports for WASI sockets"), + } + } + + Ok(()) +} + +/// Exported functions related to sockets +const WASI_SOCKETS_EXPORTS: [&str; 49] = [ + "wasi:sockets/ip-name-lookup#resolve-addresses", + "wasi:sockets/ip-name-lookup#resolve-next-address", + "wasi:sockets/ip-name-lookup#drop-resolve-address-stream", + "wasi:sockets/ip-name-lookup#subscribe", + "wasi:sockets/tcp#start-bind", + "wasi:sockets/tcp#finish-bind", + "wasi:sockets/tcp#start-connect", + "wasi:sockets/tcp#finish-connect", + "wasi:sockets/tcp#start-listen", + "wasi:sockets/tcp#finish-listen", + "wasi:sockets/tcp#accept", + "wasi:sockets/tcp#local-address", + "wasi:sockets/tcp#remote-address", + "wasi:sockets/tcp#address-family", + "wasi:sockets/tcp#ipv6-only", + "wasi:sockets/tcp#set-ipv6-only", + "wasi:sockets/tcp#set-listen-backlog-size", + "wasi:sockets/tcp#keep-alive", + "wasi:sockets/tcp#set-keep-alive", + "wasi:sockets/tcp#no-delay", + "wasi:sockets/tcp#set-no-delay", + "wasi:sockets/tcp#unicast-hop-limit", + "wasi:sockets/tcp#set-unicast-hop-limit", + "wasi:sockets/tcp#receive-buffer-size", + "wasi:sockets/tcp#set-receive-buffer-size", + "wasi:sockets/tcp#send-buffer-size", + "wasi:sockets/tcp#set-send-buffer-size", + "wasi:sockets/tcp#subscribe", + "wasi:sockets/tcp#shutdown", + "wasi:sockets/tcp#drop-tcp-socket", + "wasi:sockets/udp#start-bind", + "wasi:sockets/udp#finish-bind", + "wasi:sockets/udp#start-connect", + "wasi:sockets/udp#finish-connect", + "wasi:sockets/udp#receive", + "wasi:sockets/udp#send", + "wasi:sockets/udp#local-address", + "wasi:sockets/udp#remote-address", + "wasi:sockets/udp#address-family", + "wasi:sockets/udp#ipv6-only", + "wasi:sockets/udp#set-ipv6-only", + "wasi:sockets/udp#unicast-hop-limit", + "wasi:sockets/udp#set-unicast-hop-limit", + "wasi:sockets/udp#receive-buffer-size", + "wasi:sockets/udp#set-receive-buffer-size", + "wasi:sockets/udp#send-buffer-size", + "wasi:sockets/udp#set-send-buffer-size", + "wasi:sockets/udp#subscribe", + "wasi:sockets/udp#drop-udp-socket", +]; + +/// Strip exported WASI functions that implement sockets access +pub(crate) fn strip_sockets_virt(module: &mut Module) -> Result<()> { + stub_sockets_virt(module)?; + for export_name in WASI_SOCKETS_EXPORTS { + module.exports.remove(export_name).with_context(|| { + format!("failed to strip WASI sockets export function [{export_name}]") + })?; + } + Ok(()) +} diff --git a/src/virt_io/stdio.rs b/src/virt_io/stdio.rs new file mode 100644 index 0000000..bb9f2ff --- /dev/null +++ b/src/virt_io/stdio.rs @@ -0,0 +1,83 @@ +use anyhow::{bail, Context, Result}; +use walrus::Module; + +use super::StubRequirement; + +/// Imports exposed by WASI for STDIO functionality which are allowed to be missing +const WASI_STDIO_IMPORTS: [(&str, &str, &StubRequirement); 8] = [ + ("wasi:cli/stdin", "get-stdin", &StubRequirement::Optional), + ("wasi:cli/stdout", "get-stdout", &StubRequirement::Optional), + ("wasi:cli/stderr", "get-stderr", &StubRequirement::Optional), + ( + "wasi:cli/terminal-stdin", + "get-terminal-stdin", + &StubRequirement::Optional, + ), + ( + "wasi:cli/terminal-stdout", + "get-terminal-stdout", + &StubRequirement::Optional, + ), + ( + "wasi:cli/terminal-stderr", + "get-terminal-stderr", + &StubRequirement::Optional, + ), + ( + "wasi:cli/terminal-input", + "drop-terminal-input", + &StubRequirement::Optional, + ), + ( + "wasi:cli/terminal-output", + "drop-terminal-output", + &StubRequirement::Optional, + ), +]; + +/// Replace imported WASI functions that implement STDIO access with no-ops +pub(crate) fn stub_stdio_virt(module: &mut Module) -> Result<()> { + for (module_name, func_name, stub_requirement) in WASI_STDIO_IMPORTS { + match stub_requirement { + StubRequirement::Optional => { + if let Ok(fid) = module.imports.get_func(module_name, func_name) { + module + .replace_imported_func(fid, |(body, _)| { + body.unreachable(); + }) + .with_context(|| { + format!( + "failed to stub STDIO functionality [{func_name}] in module [{module_name}]" + ) + })?; + } + } + _ => bail!("unexpected stub requirement in imports for WASI STD I/O"), + } + } + Ok(()) +} + +/// Exported functions related to STDIO +const WASI_STDIO_EXPORTS: [&str; 8] = [ + "wasi:cli/stdin#get-stdin", + "wasi:cli/stdout#get-stdout", + "wasi:cli/stderr#get-stderr", + "wasi:cli/terminal-stdin#get-terminal-stdin", + "wasi:cli/terminal-stdout#get-terminal-stdout", + "wasi:cli/terminal-stderr#get-terminal-stderr", + "wasi:cli/terminal-input#drop-terminal-input", + "wasi:cli/terminal-output#drop-terminal-output", +]; + +/// Strip exported WASI functions that implement standard I/O (stdin, stdout, etc) access +pub(crate) fn strip_stdio_virt(module: &mut Module) -> Result<()> { + stub_stdio_virt(module)?; + for export_name in WASI_STDIO_EXPORTS { + module + .exports + .remove(export_name) + .with_context(|| format!("failed to strip std I/O function [{export_name}]"))?; + } + Ok(()) +} diff --git a/src/walrus_ops.rs b/src/walrus_ops.rs index 9a60630..c9386d6 100644 --- a/src/walrus_ops.rs +++ b/src/walrus_ops.rs @@ -1,8 +1,7 @@ use anyhow::{bail, Context, Result}; use walrus::{ - ir::Value, ActiveData, ActiveDataLocation, Data, DataKind, ExportItem, Function, - FunctionBuilder, FunctionId, FunctionKind, GlobalKind, ImportKind, ImportedFunction, InitExpr, - MemoryId, Module, ValType, + ir::Value, ActiveData, ActiveDataLocation, Data, DataKind, GlobalKind, InitExpr, MemoryId, + Module, }; pub(crate) fn get_active_data_start(data: &Data, mem: MemoryId) -> Result { @@ -49,15 +48,6 @@ pub(crate) fn get_active_data_segment( Ok((module.data.get_mut(data_id), offset)) } -pub(crate) fn get_memory_id(module: &Module) -> Result { - let mut mem_iter = module.memories.iter(); - let memory = mem_iter.next().context("Module does not export a memory")?; - if mem_iter.next().is_some() { - bail!("Multiple memories unsupported") - } - Ok(memory.id()) -} - pub(crate) fn get_stack_global(module: &Module) -> Result { let stack_global_id = module .globals @@ -96,100 +86,3 @@ pub(crate) fn bump_stack_global(module: &mut Module, offset: i32) -> Result *stack_value = new_stack_value; Ok(new_stack_value as u32) } - -pub(crate) fn get_exported_func(module: &mut Module, name: &str) -> Result { - let exported_fn = module - .exports - .iter() - .find(|expt| expt.name == name) - .with_context(|| format!("Unable to find export '{name}'"))?; - let ExportItem::Function(fid) = exported_fn.item else { - bail!("{name} not a function"); - }; - Ok(fid) -} - -pub(crate) fn add_stub_exported_func( - module: &mut Module, - export_name: &str, - params: Vec, - results: Vec, -) -> Result<()> { - let exported_fn = module.exports.iter().find(|expt| expt.name == export_name); - - let mut builder = FunctionBuilder::new(&mut module.types, ¶ms, &results); - builder.func_body().unreachable(); - let local_func = builder.local_func(vec![]); - let fid = module.funcs.add_local(local_func); - - // if it exists, replace it - if let Some(exported_fn) = exported_fn { - let export = module.exports.get_mut(exported_fn.id()); - export.item = ExportItem::Function(fid); - } else { - module.exports.add(export_name, ExportItem::Function(fid)); - } - - Ok(()) -} - -pub(crate) fn stub_imported_func( - module: &mut Module, - import_module: &str, - import_name: &str, - throw_if_not_found: bool, -) -> Result<()> { - let imported_fn = match module - .imports - .iter() - .find(|impt| impt.module == import_module && impt.name == import_name) - { - Some(found) => found, - None => { - if throw_if_not_found { - bail!("Unable to find import {import_module}#{import_name} to stub"); - } else { - return Ok(()); - } - } - }; - - let ImportKind::Function(fid) = imported_fn.kind else { - bail!("Unable to stub import {import_module}#{import_name}, as it is not an imported function"); - }; - let Function { - kind: FunctionKind::Import(ImportedFunction { ty: tid, .. }), - .. - } = module.funcs.get(fid) - else { - bail!("Unable to stub import {import_module}#{import_name}, as it is not an imported function"); - }; - - let ty = module.types.get(*tid); - let (params, results) = (ty.params().to_vec(), ty.results().to_vec()); - - let mut builder = FunctionBuilder::new(&mut module.types, ¶ms, &results); - builder.func_body().unreachable(); - let local_func = builder.local_func(vec![]); - - // substitute the local func into the imported func id - let func = module.funcs.get_mut(fid); - func.kind = FunctionKind::Local(local_func); - - // remove the import - module.imports.delete(imported_fn.id()); - - Ok(()) -} - -pub(crate) fn remove_exported_func(module: &mut Module, export_name: &str) -> Result<()> { - let exported_fn = module - .exports - .iter() - .find(|expt| expt.name == export_name) - .with_context(|| format!("Unable to find export {export_name}"))?; - - module.exports.delete(exported_fn.id()); - - Ok(()) -} diff --git a/tests/virt.rs b/tests/virt.rs index df66ea0..3bc3328 100644 --- a/tests/virt.rs +++ b/tests/virt.rs @@ -267,7 +267,7 @@ async fn virt_test() -> Result<()> { } } - if let Some(expect_stdout) = &test.expect.stdout { + if let Some(_expect_stdout) = &test.expect.stdout { // todo: expectation pending wasmtime stream flushing instance.call_test_stdio(&mut store).await?; }