From c274c56895d48f68a576ba7bc5087cc9d834b024 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 5 Mar 2026 15:37:33 +0000 Subject: [PATCH 01/10] Enable multiple concurrent sync host calls from different threads in the same task (#12735) * Move sync call set to thread state * Add test * Cleanup * Cleanup --- .../src/runtime/component/concurrent.rs | 86 +++++++++--------- tests/all/component_model/async.rs | 90 +++++++++++++++++++ 2 files changed, 131 insertions(+), 45 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 1740522c736d..bdd3579ee443 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -785,14 +785,14 @@ pub(crate) fn poll_and_block( // It did not complete immediately; add it to // `ConcurrentState::futures` so it will be polled via the event loop; - // then use `GuestTask::sync_call_set` to wait for the task to + // then use `GuestThread::sync_call_set` to wait for the task to // complete, suspending the current fiber until it does so. Poll::Pending => { let state = store.concurrent_state_mut(); state.push_future(future); let caller = state.get_mut(task)?.caller; - let set = state.get_mut(caller.task)?.sync_call_set; + let set = state.get_mut(caller.thread)?.sync_call_set; Waitable::Host(task).join(state, Some(set))?; store.suspend(SuspendReason::Waiting { @@ -1464,7 +1464,6 @@ impl StoreOpaque { debug_assert_eq!(instance, guest_caller); } let task = GuestTask::new( - state, Box::new(move |_, _| bail_bug!("cannot lower params in sync call")), LiftResult { lift: Box::new(move |_, _| bail_bug!("cannot lift result in sync call")), @@ -1487,7 +1486,7 @@ impl StoreOpaque { )?; let guest_task = state.push(task)?; - let new_thread = GuestThread::new_implicit(guest_task); + let new_thread = GuestThread::new_implicit(state, guest_task)?; let guest_thread = state.push(new_thread)?; Instance::from_wasmtime(self, callee.instance).add_guest_thread_to_instance_table( guest_thread, @@ -1879,7 +1878,7 @@ impl StoreOpaque { let state = self.concurrent_state_mut(); let caller = state.current_guest_thread()?; let old_set = waitable.common(state)?.set; - let set = state.get_mut(caller.task)?.sync_call_set; + let set = state.get_mut(caller.thread)?.sync_call_set; waitable.join(state, Some(set))?; self.suspend(SuspendReason::Waiting { set, @@ -2075,14 +2074,24 @@ impl Instance { guest_thread: QualifiedThreadId, runtime_instance: RuntimeComponentInstanceIndex, ) -> Result<()> { - let guest_id = match store - .concurrent_state_mut() - .get_mut(guest_thread.thread)? - .instance_rep - { + let state = store.concurrent_state_mut(); + let thread_data = state.get_mut(guest_thread.thread)?; + let guest_id = match thread_data.instance_rep { Some(id) => id, None => bail_bug!("thread must have instance_rep set by now"), }; + let sync_call_set = thread_data.sync_call_set; + + // Clean up any pending subtasks in the sync_call_set + for waitable in mem::take(&mut state.get_mut(sync_call_set)?.ready) { + if let Some(Event::Subtask { + status: Status::Returned | Status::ReturnCancelled, + }) = waitable.common(state)?.event + { + waitable.delete_from(state)?; + } + } + store .instance_state(RuntimeInstance { instance: self.id().instance(), @@ -2092,6 +2101,7 @@ impl Instance { .guest_thread_remove(guest_id)?; store.concurrent_state_mut().delete(guest_thread.thread)?; + store.concurrent_state_mut().delete(sync_call_set)?; let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; task.threads.remove(&guest_thread.thread); Ok(()) @@ -2437,7 +2447,6 @@ impl Instance { ); let new_task = GuestTask::new( - state, Box::new(move |store, dst| { let mut store = token.as_context_mut(store); assert!(dst.len() <= MAX_FLAT_PARAMS); @@ -2542,7 +2551,7 @@ impl Instance { )?; let guest_task = state.push(new_task)?; - let new_thread = GuestThread::new_implicit(guest_task); + let new_thread = GuestThread::new_implicit(state, guest_task)?; let guest_thread = state.push(new_thread)?; state.get_mut(guest_task)?.threads.insert(guest_thread); @@ -2660,11 +2669,11 @@ impl Instance { let state = store.0.concurrent_state_mut(); - // Use the caller's `GuestTask::sync_call_set` to register interest in + // Use the caller's `GuestThread::sync_call_set` to register interest in // the subtask... let guest_waitable = Waitable::Guest(guest_thread.task); let old_set = guest_waitable.common(state)?.set; - let set = state.get_mut(caller.task)?.sync_call_set; + let set = state.get_mut(caller.thread)?.sync_call_set; guest_waitable.join(state, Some(set))?; // ... and suspend this fiber temporarily while we wait for it to start. @@ -3281,7 +3290,7 @@ impl Instance { let current_thread = state.current_guest_thread()?; let parent_task = current_thread.task; - let new_thread = GuestThread::new_explicit(parent_task, start_func); + let new_thread = GuestThread::new_explicit(state, parent_task, start_func)?; let thread_id = state.push(new_thread)?; state.get_mut(parent_task)?.threads.insert(thread_id); @@ -4339,6 +4348,8 @@ pub struct GuestThread { /// The index of this thread in the component instance's handle table. /// This must always be `Some` after initialization. instance_rep: Option, + /// Scratch waitable set used to watch subtasks during synchronous calls. + sync_call_set: TableId, } impl GuestThread { @@ -4355,29 +4366,34 @@ impl GuestThread { Ok(TableId::new(rep)) } - fn new_implicit(parent_task: TableId) -> Self { - Self { + fn new_implicit(state: &mut ConcurrentState, parent_task: TableId) -> Result { + let sync_call_set = state.push(WaitableSet::default())?; + Ok(Self { context: [0; 2], parent_task, wake_on_cancel: None, state: GuestThreadState::NotStartedImplicit, instance_rep: None, - } + sync_call_set, + }) } fn new_explicit( + state: &mut ConcurrentState, parent_task: TableId, start_func: Box< dyn FnOnce(&mut dyn VMStore, QualifiedThreadId) -> Result<()> + Send + Sync, >, - ) -> Self { - Self { + ) -> Result { + let sync_call_set = state.push(WaitableSet::default())?; + Ok(Self { context: [0; 2], parent_task, wake_on_cancel: None, state: GuestThreadState::NotStartedExplicit(start_func), instance_rep: None, - } + sync_call_set, + }) } } @@ -4442,8 +4458,6 @@ pub(crate) struct GuestTask { /// Whether or not we've sent a `Status::Starting` event to any current or /// future waiters for this waitable. starting_sent: bool, - /// Scratch waitable set used to watch subtasks during synchronous calls. - sync_call_set: TableId, /// The runtime instance to which the exported function for this guest task /// belongs. /// @@ -4501,7 +4515,6 @@ impl GuestTask { } fn new( - state: &mut ConcurrentState, lower_params: RawLower, lift_result: LiftResult, caller: Caller, @@ -4509,7 +4522,6 @@ impl GuestTask { instance: RuntimeInstance, async_function: bool, ) -> Result { - let sync_call_set = state.push(WaitableSet::default())?; let host_future_state = match &caller { Caller::Guest { .. } => HostFutureState::NotApplicable, Caller::Host { @@ -4534,7 +4546,6 @@ impl GuestTask { sync_result: SyncResult::NotProduced, cancel_sent: false, starting_sent: false, - sync_call_set, instance, event: None, exited: false, @@ -4544,24 +4555,9 @@ impl GuestTask { }) } - /// Dispose of this guest task, reparenting any pending subtasks to the - /// caller. - fn dispose(self, state: &mut ConcurrentState) -> Result<()> { - // If there are not-yet-delivered completion events for subtasks in - // `self.sync_call_set`, recursively dispose of those subtasks as well. - for waitable in mem::take(&mut state.get_mut(self.sync_call_set)?.ready) { - if let Some(Event::Subtask { - status: Status::Returned | Status::ReturnCancelled, - }) = waitable.common(state)?.event - { - waitable.delete_from(state)?; - } - } - + /// Dispose of this guest task. + fn dispose(self, _state: &mut ConcurrentState) -> Result<()> { assert!(self.threads.is_empty()); - - state.delete(self.sync_call_set)?; - Ok(()) } } @@ -5346,7 +5342,6 @@ pub(crate) fn prepare_call( }; let caller = state.current_thread; let task = GuestTask::new( - state, Box::new(for_any_lower(move |store, params| { lower_params(handle, token.as_context_mut(store), params) })), @@ -5378,7 +5373,8 @@ pub(crate) fn prepare_call( )?; let task = state.push(task)?; - let thread = state.push(GuestThread::new_implicit(task))?; + let new_thread = GuestThread::new_implicit(state, task)?; + let thread = state.push(new_thread)?; state.get_mut(task)?.threads.insert(thread); if !store.0.may_enter(instance)? { diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs index 0c31867080fe..c11c89b7c560 100644 --- a/tests/all/component_model/async.rs +++ b/tests/all/component_model/async.rs @@ -1177,3 +1177,93 @@ async fn stream_cancel_read_async_does_not_corrupt_state() -> Result<()> { } } } + +/// Regression test: multiple threads may concurrently make a synchronous +/// call into the same async host function without corrupting state. +/// +/// Bug: waitable sets for host calls used to be shared across all threads, so if two threads +/// called a sync-lowered async host function concurrently, the waitable set state got overwritten. +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn concurrent_sync_calls_to_async_host() -> Result<()> { + _ = env_logger::try_init(); + + let mut config = Config::new(); + config.wasm_component_model_async(true); + config.wasm_component_model_async_builtins(true); + config.wasm_component_model_async_stackful(true); + config.wasm_component_model_threading(true); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, 0); + + let component = Component::new( + &engine, + r#"(component + (import "await-three-calls" (func $await-three-calls async)) + + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + + (core module $m + (import "" "await-three-calls" (func $await-three-calls)) + (import "" "thread.new-indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.unsuspend" (func $thread-unsuspend (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + + (func (export "run") + (call $thread-new-indirect (i32.const 0) (i32.const 0)) + (call $thread-unsuspend) + (call $thread-new-indirect (i32.const 0) (i32.const 0)) + (call $thread-unsuspend) + (call $await-three-calls) + ) + (func $thread-entry (param i32) + (call $await-three-calls) + ) + (elem (table $indirect-function-table) (i32.const 0) func $thread-entry) + ) + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new-indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + (core func $thread-new-indirect + (canon thread.new-indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-unsuspend (canon thread.unsuspend)) + + (core func $await-three-calls (canon lower (func $await-three-calls) )) + (core instance $i (instantiate $m + (with "" (instance + (export "await-three-calls" (func $await-three-calls)) + (export "thread.new-indirect" (func $thread-new-indirect)) + (export "thread.unsuspend" (func $thread-unsuspend)) + )) + (with "libc" (instance $libc)) + )) + (func (export "run") async + (canon lift (core func $i "run"))) + )"#, + )?; + + let mut linker = Linker::::new(&engine); + linker + .root() + .func_wrap_concurrent("await-three-calls", |accessor, (): ()| { + Box::pin(async move { + accessor.with(|mut s| { + *s.data_mut() += 1; + }); + while accessor.with(|mut s| *s.data_mut()) < 3 { + tokio::task::yield_now().await; + } + Ok(()) + }) + })?; + let instance = linker.instantiate_async(&mut store, &component).await?; + let func = instance.get_typed_func::<(), ()>(&mut store, "run")?; + func.call_async(&mut store, ()).await?; + + store.assert_concurrent_state_empty(); + + Ok(()) +} From d2e8d8139b68f21c2de87c43c614de801e739368 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 5 Mar 2026 14:46:38 -0600 Subject: [PATCH 02/10] Fix caller context on fused component<->component returns (#12737) In #12718 I added a test for more cases but forgot to update the `*.wast` to actually execute the test. Turns out all the cases were failing, and so this commit properly enables the tests and then fixes them. --- crates/wasmtime/src/runtime/component/concurrent.rs | 13 +++++++++++++ .../component-model/async/task-builtins.wast | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index bdd3579ee443..fd73e5622ec5 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -2507,6 +2507,14 @@ impl Instance { if let ResultInfo::Heap { results } = &result_info { my_src.push(ValRaw::u32(*results)); } + + // Execute the `return_` hook, generated by Wasmtime's FACT + // compiler, in the context of the old thread. The old + // thread, this thread's caller, may have `realloc` + // callbacks invoked for example and those need the correct + // context set for the current thread. + let prev = store.0.set_thread(old_thread)?; + // SAFETY: `return_` is a valid `*mut VMFuncRef` from // `wasmtime-cranelift`-generated fused adapter code. Based // on how it was constructed (see @@ -2520,6 +2528,11 @@ impl Instance { my_src.as_mut_slice().into(), )?; } + + // Restore the previous current thread after the + // lifting/lowering has returned. + store.0.set_thread(prev)?; + let state = store.0.concurrent_state_mut(); let thread = state.current_guest_thread()?; if sync_caller { diff --git a/tests/misc_testsuite/component-model/async/task-builtins.wast b/tests/misc_testsuite/component-model/async/task-builtins.wast index 9fec3e1bf76a..5476fb8222c9 100644 --- a/tests/misc_testsuite/component-model/async/task-builtins.wast +++ b/tests/misc_testsuite/component-model/async/task-builtins.wast @@ -381,9 +381,15 @@ (instance $a (instantiate $A)) (instance $b (instantiate $B (with "a" (instance $a)))) (export "sync-to-sync" (func $b "sync-to-sync")) + (export "sync-to-async" (func $b "sync-to-async")) + (export "async-to-sync" (func $b "async-to-sync")) + (export "async-to-async" (func $b "async-to-async")) ) (assert_return (invoke "sync-to-sync")) +(assert_return (invoke "sync-to-async")) +(assert_return (invoke "async-to-sync")) +(assert_return (invoke "async-to-async")) ;; Same as above, but when calling the host. (component From 7e366bfc7199c991d02cbdacad9e8f64625983ad Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Mar 2026 12:02:35 -0500 Subject: [PATCH 03/10] wasip3: Limit random number generation by default (#12745) This commit extends the random-related fixes of #12652 to WASIp3's implementation of randomness-related interfaces. cc #12674 --- .../src/bin/p3_cli_random_limits.rs | 26 +++++++++++++++++++ crates/wasi/src/p3/random/host.rs | 12 ++++++--- tests/all/cli_tests.rs | 23 ++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 crates/test-programs/src/bin/p3_cli_random_limits.rs diff --git a/crates/test-programs/src/bin/p3_cli_random_limits.rs b/crates/test-programs/src/bin/p3_cli_random_limits.rs new file mode 100644 index 000000000000..b4212dceee87 --- /dev/null +++ b/crates/test-programs/src/bin/p3_cli_random_limits.rs @@ -0,0 +1,26 @@ +use test_programs::p3::wasi::random; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let args = std::env::args().collect::>(); + let args = args.iter().map(|s| s.as_str()).collect::>(); + match &args[1..] { + ["random", n] => { + random::random::get_random_bytes(n.parse().unwrap()); + } + ["insecure", n] => { + random::insecure::get_insecure_random_bytes(n.parse().unwrap()); + } + other => { + panic!("unexpected args: {other:?}"); + } + } + Ok(()) + } +} + +fn main() {} diff --git a/crates/wasi/src/p3/random/host.rs b/crates/wasi/src/p3/random/host.rs index 03f106a078ff..163248fcd5fd 100644 --- a/crates/wasi/src/p3/random/host.rs +++ b/crates/wasi/src/p3/random/host.rs @@ -1,11 +1,14 @@ -use cap_rand::Rng; -use cap_rand::distributions::Standard; - use crate::p3::bindings::random::{insecure, insecure_seed, random}; use crate::random::WasiRandomCtx; +use cap_rand::Rng; +use cap_rand::distributions::Standard; +use wasmtime::bail; impl random::Host for WasiRandomCtx { fn get_random_bytes(&mut self, len: u64) -> wasmtime::Result> { + if len > self.max_size { + bail!("requested len {len:?} exceeds limit {}", self.max_size); + } Ok((&mut self.random) .sample_iter(Standard) .take(len as usize) @@ -19,6 +22,9 @@ impl random::Host for WasiRandomCtx { impl insecure::Host for WasiRandomCtx { fn get_insecure_random_bytes(&mut self, len: u64) -> wasmtime::Result> { + if len > self.max_size { + bail!("requested len {len:?} exceeds limit {}", self.max_size); + } Ok((&mut self.insecure_random) .sample_iter(Standard) .take(len as usize) diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index 122181e2883c..4aaa0d692f1b 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -2757,6 +2757,29 @@ start a print 1234 } Ok(()) } + + #[test] + fn p3_cli_random_limits() -> Result<()> { + let c = P3_CLI_RANDOM_LIMITS_COMPONENT; + + for rand in ["random", "insecure"] { + run_wasmtime(&["run", "-Sp3", "-Wcomponent-model-async", c, rand, "256"])?; + assert!( + run_wasmtime(&[ + "run", + "-Sp3", + "-Wcomponent-model-async", + "-Smax-random-size=255", + c, + rand, + "256" + ]) + .is_err() + ); + } + + Ok(()) + } } #[test] From 20c89a27250a448fc5dff83cd1c6e5093d324984 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Mar 2026 16:39:58 -0500 Subject: [PATCH 04/10] Fix lost wakeups with stdin and wasip3 (#12711) I've been running some tests with wasip3 recently and I was running into a situation where a program would read stdin, get some data, and then stdin would be closed. The second read of stdin wouldn't get a wakeup and would get stuck forever despite stdin being closed. I'm not 100% sure what was happening but I'm highly suspect of the `Notify`-based synchronization here as I know historically that's a tricky primitive to work with. This applies a hammer and moves some lock scopes up a bit further to avoid dealing with trickiness and instead ensure everything proceeds in lockstep. --- .../src/bin/p3_cli_read_stdin.rs | 25 +++++++++++++++++++ crates/wasi/src/cli/worker_thread_stdin.rs | 17 +++++++------ tests/all/cli_tests.rs | 21 ++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 crates/test-programs/src/bin/p3_cli_read_stdin.rs diff --git a/crates/test-programs/src/bin/p3_cli_read_stdin.rs b/crates/test-programs/src/bin/p3_cli_read_stdin.rs new file mode 100644 index 000000000000..4518450f5d33 --- /dev/null +++ b/crates/test-programs/src/bin/p3_cli_read_stdin.rs @@ -0,0 +1,25 @@ +use test_programs::p3::wasi; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let (mut stream, result) = wasi::cli::stdin::read_via_stream(); + let (sresult, buf) = stream.read(Vec::with_capacity(100)).await; + assert_eq!(buf, b"hello!".to_vec()); + assert_eq!(sresult, wit_bindgen::StreamResult::Complete(6)); + + let (sresult, buf) = stream.read(Vec::with_capacity(100)).await; + assert!(buf.is_empty()); + assert_eq!(sresult, wit_bindgen::StreamResult::Dropped); + + result.await.unwrap(); + Ok(()) + } +} + +fn main() { + unreachable!(); +} diff --git a/crates/wasi/src/cli/worker_thread_stdin.rs b/crates/wasi/src/cli/worker_thread_stdin.rs index 10400cd121fd..10465c04da37 100644 --- a/crates/wasi/src/cli/worker_thread_stdin.rs +++ b/crates/wasi/src/cli/worker_thread_stdin.rs @@ -121,7 +121,8 @@ fn create() -> GlobalStdin { *state.state.lock().unwrap(), StdinState::ReadRequested )); - *state.state.lock().unwrap() = new_state; + let mut lock = state.state.lock().unwrap(); + *lock = new_state; state.read_completed.notify_waiters(); if done { break; @@ -205,6 +206,14 @@ impl AsyncRead for WasiStdinAsyncRead { ) -> Poll> { let g = GlobalStdin::get(); + // Everything below is executed under the global stdin lock. It's not + // going to block below so that's semantically fine. Optimization-wise + // it's probably possible to move this within the loop around just a + // small part of reading/writing the state, but that was done + // historically and it resulted in lost wakeups with `Notify`, so this + // is conservatively hoisted up here. + let mut locked = g.state.lock().unwrap(); + // Perform everything below in a `loop` to handle the case that a read // was stolen by another thread, for example, or perhaps a spurious // notification to `Notified`. @@ -222,7 +231,6 @@ impl AsyncRead for WasiStdinAsyncRead { // Once we're in the "ready" state then take a look at the global // state of stdin. - let mut locked = g.state.lock().unwrap(); match mem::replace(&mut *locked, StdinState::ReadRequested) { // If data is available then drain what we can into `buf`. StdinState::Data(mut data) => { @@ -260,11 +268,6 @@ impl AsyncRead for WasiStdinAsyncRead { } self.set(WasiStdinAsyncRead::Waiting(g.read_completed.notified())); - - // Intentionally drop the lock after the `notified()` future - // creation just above as to work correctly this needs to happen - // within the lock. - drop(locked); } } } diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index 4aaa0d692f1b..db87e1bca3fb 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -2780,6 +2780,27 @@ start a print 1234 Ok(()) } + + #[test] + fn p3_cli_read_stdin() -> Result<()> { + let mut cmd = get_wasmtime_command()?; + let mut child = cmd + .arg("-Sp3") + .arg("-Wcomponent-model-async") + .arg(P3_CLI_READ_STDIN_COMPONENT) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(b"hello!").unwrap(); + let output = child.wait_with_output()?; + println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); + println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + assert!(output.status.success()); + + Ok(()) + } } #[test] From 643e9b0881600297e199488450085b69a38fdb57 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 10 Mar 2026 11:24:22 -0500 Subject: [PATCH 05/10] Refactor WASIp2 `wasi:http` implementation (#12748) * Sequester WASIp2 in `wasmtime-wasi-http` to a module This mirrors the `wasmtime-wasi` crate's organization where there's a `p2` module and a `p3` module at the top level. * Refactor WASIp2 `wasi:http` implementation This commit reorganizes and refactors the WASIp2 implementation of `wasi:http` to look more like other `wasmtime-wasi`-style interfaces. Specifically the old `WasiHttpImpl` structure is removed in favor of as `WasiHttpCtxView<'_>` type that is used to implement bindgen-generated `Host` traits. This necessitated reorganizing the methods of the previous `WasiHttpView` trait like so: * The `WasiHttpView` trait is renamed to `WasiHttpHooks` to make space for a new `WasiHttpView` which behaves like `WasiView`, for example. * The `ctx` and `table` methods of `WasiHttpHooks` were removed since they'll be fields in `WasiHttpCtxView`. * Helper methods for WASIp2 were moved to methods on `WasiHttpCtxView` instead of default methods on `WasiHttpHooks`. With these changes in place the WASIp3 organization was also updated slightly as well. Notably WASIp3 now contains a reference to the crate's `WasiHttpCtx` structure (which has field limits for example). WASIp3's previous `WasiHttpCtx` trait is now renamed to `WasiHttpHooks` as well. This means that there are two `WasiHttpHooks` traits right now, one for WASIp2 and one for WASIp3. In the future I would like to unify these two but that will require some more work around the default `send_request`. A final note here is that the `WasiHttpHooks` trait previously, and continues to be, optional for embedders to implement. Default functions are provided as `wasmtime_wasi_http::{p2, p3}::default_hooks`. Additionally there's a `Default for &mut dyn WasiHttpHooks` implementation, too. With all that outlined: the motivation for this change is to bring the WASIp2 and WASIp3 implementations of `wasi:http` closer together. This is inspired by refactorings I was doing for #12674 to apply the same header limitations for WASIp3 as is done for WASIp2. Prior to this change there were a number of differences such as WASIp3 not having `crate::WasiHttpCtx` around, WASIp2 having a different organization of structures/borrows, etc. The goal is to bring the two implementations closer in line with each other to make refactoring across them more consistent and easier. * Make `WasiHttp` in WASIp2 public * Fix some conditional build * Fix some doctests * Fix configured build * Fixup documentation --- .github/workflows/main.yml | 1 + Cargo.toml | 2 + crates/wasi-http/Cargo.toml | 3 +- crates/wasi-http/src/ctx.rs | 41 + crates/wasi-http/src/handler.rs | 6 +- crates/wasi-http/src/lib.rs | 402 +-------- crates/wasi-http/src/{ => p2}/bindings.rs | 8 +- crates/wasi-http/src/{ => p2}/body.rs | 4 +- crates/wasi-http/src/{ => p2}/error.rs | 4 +- crates/wasi-http/src/{ => p2}/http_impl.rs | 19 +- crates/wasi-http/src/p2/mod.rs | 706 ++++++++++++++++ crates/wasi-http/src/p2/types.rs | 449 ++++++++++ crates/wasi-http/src/{ => p2}/types_impl.rs | 272 +++--- crates/wasi-http/src/p3/host/handler.rs | 2 +- crates/wasi-http/src/p3/host/types.rs | 10 +- crates/wasi-http/src/p3/mod.rs | 35 +- crates/wasi-http/src/p3/request.rs | 15 +- crates/wasi-http/src/types.rs | 889 -------------------- crates/wasi-http/tests/all/p2.rs | 53 +- crates/wasi-http/tests/all/p2/async_.rs | 2 +- crates/wasi-http/tests/all/p2/sync.rs | 2 +- crates/wasi-http/tests/all/p3/mod.rs | 15 +- crates/wasi/Cargo.toml | 3 + src/commands/run.rs | 61 +- src/commands/serve.rs | 55 +- src/common.rs | 52 +- 26 files changed, 1535 insertions(+), 1576 deletions(-) create mode 100644 crates/wasi-http/src/ctx.rs rename crates/wasi-http/src/{ => p2}/bindings.rs (93%) rename crates/wasi-http/src/{ => p2}/body.rs (99%) rename crates/wasi-http/src/{ => p2}/error.rs (96%) rename crates/wasi-http/src/{ => p2}/http_impl.rs (89%) create mode 100644 crates/wasi-http/src/p2/mod.rs create mode 100644 crates/wasi-http/src/p2/types.rs rename crates/wasi-http/src/{ => p2}/types_impl.rs (78%) delete mode 100644 crates/wasi-http/src/types.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 82e1804bf26e..598787c1a17c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -413,6 +413,7 @@ jobs: - name: wasmtime-wasi-http checks: | -p wasmtime-wasi-http --no-default-features + -p wasmtime-wasi-http --no-default-features --features p2 -p wasmtime-wasi-http --no-default-features --features p3 -p wasmtime-wasi-http --no-default-features --features p3 --all-targets diff --git a/Cargo.toml b/Cargo.toml index 9f732fc37c36..c9ff71bcd146 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -576,6 +576,7 @@ serve = [ "dep:http-body-util", "dep:http", "wasmtime-cli-flags/async", + "wasmtime-wasi-http?/p2", ] explore = ["dep:wasmtime-explorer", "dep:tempfile"] wast = ["dep:wasmtime-wast"] @@ -589,6 +590,7 @@ run = [ "dep:wasi-common", "dep:tokio", "wasmtime-cli-flags/async", + "wasmtime-wasi-http?/p2", ] completion = ["dep:clap_complete"] objdump = [ diff --git a/crates/wasi-http/Cargo.toml b/crates/wasi-http/Cargo.toml index 042e04a2f3a9..7eb31ae2de4e 100644 --- a/crates/wasi-http/Cargo.toml +++ b/crates/wasi-http/Cargo.toml @@ -15,8 +15,9 @@ workspace = true all-features = true [features] -default = ["default-send-request"] +default = ["default-send-request", "p2"] default-send-request = ["dep:tokio-rustls", "dep:rustls", "dep:webpki-roots"] +p2 = ["wasmtime-wasi/p2"] p3 = ["wasmtime-wasi/p3", "dep:tokio-util"] component-model-async = ["futures/alloc", "wasmtime/component-model-async"] diff --git a/crates/wasi-http/src/ctx.rs b/crates/wasi-http/src/ctx.rs new file mode 100644 index 000000000000..0ae1a9678e22 --- /dev/null +++ b/crates/wasi-http/src/ctx.rs @@ -0,0 +1,41 @@ +/// Default maximum size for the contents of a fields resource. +/// +/// Typically, HTTP proxies limit headers to 8k. This number is higher than that +/// because it not only includes the wire-size of headers but it additionally +/// includes factors for the in-memory representation of `HeaderMap`. This is in +/// theory high enough that no one runs into it but low enough such that a +/// completely full `HeaderMap` doesn't break the bank in terms of memory +/// consumption. +const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024; + +/// Capture the state necessary for use in the wasi-http API implementation. +#[derive(Debug, Clone)] +pub struct WasiHttpCtx { + pub(crate) field_size_limit: usize, +} + +impl WasiHttpCtx { + /// Create a new context. + pub fn new() -> Self { + Self { + field_size_limit: DEFAULT_FIELD_SIZE_LIMIT, + } + } + + /// Set the maximum size for any fields resources created by this context. + /// + /// The limit specified here is roughly a byte limit for the size of the + /// in-memory representation of headers. This means that the limit needs to + /// be larger than the literal representation of headers on the wire to + /// account for in-memory Rust-side data structures representing the header + /// names/values/etc. + pub fn set_field_size_limit(&mut self, limit: usize) { + self.field_size_limit = limit; + } +} + +impl Default for WasiHttpCtx { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/wasi-http/src/handler.rs b/crates/wasi-http/src/handler.rs index c8496710d199..ce646a78a560 100644 --- a/crates/wasi-http/src/handler.rs +++ b/crates/wasi-http/src/handler.rs @@ -24,6 +24,7 @@ use wasmtime::{Result, Store, StoreContextMut, format_err}; /// Alternative p2 bindings generated with `exports: { default: async | store }` /// so we can use `TypedFunc::call_concurrent` with both p2 and p3 instances. +#[cfg(feature = "p2")] pub mod p2 { #[expect(missing_docs, reason = "bindgen-generated code")] pub mod bindings { @@ -35,7 +36,7 @@ pub mod p2 { require_store_data_send: true, with: { // http is in this crate - "wasi:http": crate::bindings::http, + "wasi:http": crate::p2::bindings::http, // Upstream package dependencies "wasi:io": wasmtime_wasi::p2::bindings::io, } @@ -49,6 +50,7 @@ pub mod p2 { /// `wasi:http/handler@0.3.x` pre-instance. pub enum ProxyPre { /// A `wasi:http/incoming-handler@0.2.x` pre-instance. + #[cfg(feature = "p2")] P2(p2::bindings::ProxyPre), /// A `wasi:http/handler@0.3.x` pre-instance. #[cfg(feature = "p3")] @@ -61,6 +63,7 @@ impl ProxyPre { T: Send, { Ok(match self { + #[cfg(feature = "p2")] Self::P2(pre) => Proxy::P2(pre.instantiate_async(store).await?), #[cfg(feature = "p3")] Self::P3(pre) => Proxy::P3(pre.instantiate_async(store).await?), @@ -72,6 +75,7 @@ impl ProxyPre { /// `wasi:http/handler@0.3.x` instance. pub enum Proxy { /// A `wasi:http/incoming-handler@0.2.x` instance. + #[cfg(feature = "p2")] P2(p2::bindings::Proxy), /// A `wasi:http/handler@0.3.x` instance. #[cfg(feature = "p3")] diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs index 358682ddc91b..cdf8648ac37a 100644 --- a/crates/wasi-http/src/lib.rs +++ b/crates/wasi-http/src/lib.rs @@ -1,402 +1,50 @@ -//! # Wasmtime's WASI HTTP Implementation +//! Wasmtime's implementation of `wasi:http` //! -//! This crate is Wasmtime's host implementation of the `wasi:http` package as -//! part of WASIp2. This crate's implementation is primarily built on top of -//! [`hyper`] and [`tokio`]. -//! -//! # WASI HTTP Interfaces -//! -//! This crate contains implementations of the following interfaces: -//! -//! * [`wasi:http/incoming-handler`] -//! * [`wasi:http/outgoing-handler`] -//! * [`wasi:http/types`] -//! -//! The crate also contains an implementation of the [`wasi:http/proxy`] world. -//! -//! [`wasi:http/proxy`]: crate::bindings::Proxy -//! [`wasi:http/outgoing-handler`]: crate::bindings::http::outgoing_handler::Host -//! [`wasi:http/types`]: crate::bindings::http::types::Host -//! [`wasi:http/incoming-handler`]: crate::bindings::exports::wasi::http::incoming_handler::Guest -//! -//! This crate is very similar to [`wasmtime_wasi`] in the it uses the -//! `bindgen!` macro in Wasmtime to generate bindings to interfaces. Bindings -//! are located in the [`bindings`] module. -//! -//! # The `WasiHttpView` trait -//! -//! All `bindgen!`-generated `Host` traits are implemented in terms of a -//! [`WasiHttpView`] trait which provides basic access to [`WasiHttpCtx`], -//! configuration for WASI HTTP, and a [`wasmtime_wasi::ResourceTable`], the -//! state for all host-defined component model resources. -//! -//! The [`WasiHttpView`] trait additionally offers a few other configuration -//! methods such as [`WasiHttpView::send_request`] to customize how outgoing -//! HTTP requests are handled. -//! -//! # Async and Sync -//! -//! There are both asynchronous and synchronous bindings in this crate. For -//! example [`add_to_linker_async`] is for asynchronous embedders and -//! [`add_to_linker_sync`] is for synchronous embedders. Note that under the -//! hood both versions are implemented with `async` on top of [`tokio`]. -//! -//! # Examples -//! -//! Usage of this crate is done through a few steps to get everything hooked up: -//! -//! 1. First implement [`WasiHttpView`] for your type which is the `T` in -//! [`wasmtime::Store`]. -//! 2. Add WASI HTTP interfaces to a [`wasmtime::component::Linker`]. There -//! are a few options of how to do this: -//! * Use [`add_to_linker_async`] to bundle all interfaces in -//! `wasi:http/proxy` together -//! * Use [`add_only_http_to_linker_async`] to add only HTTP interfaces but -//! no others. This is useful when working with -//! [`wasmtime_wasi::p2::add_to_linker_async`] for example. -//! * Add individual interfaces such as with the -//! [`bindings::http::outgoing_handler::add_to_linker`] function. -//! 3. Use [`ProxyPre`](bindings::ProxyPre) to pre-instantiate a component -//! before serving requests. -//! 4. When serving requests use -//! [`ProxyPre::instantiate_async`](bindings::ProxyPre::instantiate_async) -//! to create instances and handle HTTP requests. -//! -//! A standalone example of doing all this looks like: -//! -//! ```no_run -//! use wasmtime::bail; -//! use hyper::server::conn::http1; -//! use std::sync::Arc; -//! use tokio::net::TcpListener; -//! use wasmtime::component::{Component, Linker, ResourceTable}; -//! use wasmtime::{Engine, Result, Store}; -//! use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -//! use wasmtime_wasi_http::bindings::ProxyPre; -//! use wasmtime_wasi_http::bindings::http::types::Scheme; -//! use wasmtime_wasi_http::body::HyperOutgoingBody; -//! use wasmtime_wasi_http::io::TokioIo; -//! use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -//! -//! #[tokio::main] -//! async fn main() -> Result<()> { -//! let component = std::env::args().nth(1).unwrap(); -//! -//! // Prepare the `Engine` for Wasmtime -//! let engine = Engine::default(); -//! -//! // Compile the component on the command line to machine code -//! let component = Component::from_file(&engine, &component)?; -//! -//! // Prepare the `ProxyPre` which is a pre-instantiated version of the -//! // component that we have. This will make per-request instantiation -//! // much quicker. -//! let mut linker = Linker::new(&engine); -//! wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; -//! wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?; -//! let pre = ProxyPre::new(linker.instantiate_pre(&component)?)?; -//! -//! // Prepare our server state and start listening for connections. -//! let server = Arc::new(MyServer { pre }); -//! let listener = TcpListener::bind("127.0.0.1:8000").await?; -//! println!("Listening on {}", listener.local_addr()?); -//! -//! loop { -//! // Accept a TCP connection and serve all of its requests in a separate -//! // tokio task. Note that for now this only works with HTTP/1.1. -//! let (client, addr) = listener.accept().await?; -//! println!("serving new client from {addr}"); -//! -//! let server = server.clone(); -//! tokio::task::spawn(async move { -//! if let Err(e) = http1::Builder::new() -//! .keep_alive(true) -//! .serve_connection( -//! TokioIo::new(client), -//! hyper::service::service_fn(move |req| { -//! let server = server.clone(); -//! async move { server.handle_request(req).await } -//! }), -//! ) -//! .await -//! { -//! eprintln!("error serving client[{addr}]: {e:?}"); -//! } -//! }); -//! } -//! } -//! -//! struct MyServer { -//! pre: ProxyPre, -//! } -//! -//! impl MyServer { -//! async fn handle_request( -//! &self, -//! req: hyper::Request, -//! ) -> Result> { -//! // Create per-http-request state within a `Store` and prepare the -//! // initial resources passed to the `handle` function. -//! let mut store = Store::new( -//! self.pre.engine(), -//! MyClientState { -//! table: ResourceTable::new(), -//! wasi: WasiCtx::builder().inherit_stdio().build(), -//! http: WasiHttpCtx::new(), -//! }, -//! ); -//! let (sender, receiver) = tokio::sync::oneshot::channel(); -//! let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; -//! let out = store.data_mut().new_response_outparam(sender)?; -//! let pre = self.pre.clone(); -//! -//! // Run the http request itself in a separate task so the task can -//! // optionally continue to execute beyond after the initial -//! // headers/response code are sent. -//! let task = tokio::task::spawn(async move { -//! let proxy = pre.instantiate_async(&mut store).await?; -//! -//! if let Err(e) = proxy -//! .wasi_http_incoming_handler() -//! .call_handle(store, req, out) -//! .await -//! { -//! return Err(e); -//! } -//! -//! Ok(()) -//! }); -//! -//! match receiver.await { -//! // If the client calls `response-outparam::set` then one of these -//! // methods will be called. -//! Ok(Ok(resp)) => Ok(resp), -//! Ok(Err(e)) => Err(e.into()), -//! -//! // Otherwise the `sender` will get dropped along with the `Store` -//! // meaning that the oneshot will get disconnected and here we can -//! // inspect the `task` result to see what happened -//! Err(_) => { -//! let e = match task.await { -//! Ok(Ok(())) => { -//! bail!("guest never invoked `response-outparam::set` method") -//! } -//! Ok(Err(e)) => e, -//! Err(e) => e.into(), -//! }; -//! return Err(e.context("guest never invoked `response-outparam::set` method")); -//! } -//! } -//! } -//! } -//! -//! struct MyClientState { -//! wasi: WasiCtx, -//! http: WasiHttpCtx, -//! table: ResourceTable, -//! } -//! -//! impl WasiView for MyClientState { -//! fn ctx(&mut self) -> WasiCtxView<'_> { -//! WasiCtxView { ctx: &mut self.wasi, table: &mut self.table } -//! } -//! } -//! -//! impl WasiHttpView for MyClientState { -//! fn ctx(&mut self) -> &mut WasiHttpCtx { -//! &mut self.http -//! } -//! -//! fn table(&mut self) -> &mut ResourceTable { -//! &mut self.table -//! } -//! } -//! ``` +//! This crate is organized similarly to [`wasmtime_wasi`] where there is a +//! top-level [`p2`] and [`p3`] module corresponding to the implementation for +//! WASIp2 and WASIp3. #![deny(missing_docs)] #![doc(test(attr(deny(warnings))))] #![doc(test(attr(allow(dead_code, unused_variables, unused_mut))))] #![cfg_attr(docsrs, feature(doc_cfg))] -mod error; -mod http_impl; -mod types_impl; +use http::{HeaderName, header}; -pub mod body; +mod ctx; #[cfg(feature = "component-model-async")] pub mod handler; pub mod io; -pub mod types; - -pub mod bindings; - +#[cfg(feature = "p2")] +pub mod p2; #[cfg(feature = "p3")] pub mod p3; -pub use crate::error::{ - HttpError, HttpResult, http_request_error, hyper_request_error, hyper_response_error, -}; -#[doc(inline)] -pub use crate::types::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, - WasiHttpImpl, WasiHttpView, -}; -use http::header::CONTENT_LENGTH; -use wasmtime::component::{HasData, Linker}; - -/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. -/// -/// This function will add the `async` variant of all interfaces into the -/// `Linker` provided. For embeddings with async support disabled see -/// [`add_to_linker_sync`] instead. -/// -/// # Example -/// -/// ``` -/// use wasmtime::{Engine, Result}; -/// use wasmtime::component::{ResourceTable, Linker}; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -/// -/// fn main() -> Result<()> { -/// let engine = Engine::default(); -/// -/// let mut linker = Linker::::new(&engine); -/// wasmtime_wasi_http::add_to_linker_async(&mut linker)?; -/// // ... add any further functionality to `linker` if desired ... -/// -/// Ok(()) -/// } -/// -/// struct MyState { -/// ctx: WasiCtx, -/// http_ctx: WasiHttpCtx, -/// table: ResourceTable, -/// } -/// -/// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// ``` -pub fn add_to_linker_async(l: &mut wasmtime::component::Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + wasmtime_wasi::WasiView + 'static, -{ - wasmtime_wasi::p2::add_to_linker_proxy_interfaces_async(l)?; - add_only_http_to_linker_async(l) -} - -/// A slimmed down version of [`add_to_linker_async`] which only adds -/// `wasi:http` interfaces to the linker. -/// -/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_async`] for -/// example to avoid re-adding the same interfaces twice. -pub fn add_only_http_to_linker_async( - l: &mut wasmtime::component::Linker, -) -> wasmtime::Result<()> -where - T: WasiHttpView + 'static, -{ - let options = crate::bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. - crate::bindings::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| { - WasiHttpImpl(x) - })?; - crate::bindings::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { - WasiHttpImpl(x) - })?; - - Ok(()) -} - -struct WasiHttp(T); - -impl HasData for WasiHttp { - type Data<'a> = WasiHttpImpl<&'a mut T>; -} - -/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. -/// -/// This function will add the `sync` variant of all interfaces into the -/// `Linker` provided. For embeddings with async support see -/// [`add_to_linker_async`] instead. -/// -/// # Example -/// -/// ``` -/// use wasmtime::{Engine, Result, Config}; -/// use wasmtime::component::{ResourceTable, Linker}; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -/// -/// fn main() -> Result<()> { -/// let config = Config::default(); -/// let engine = Engine::new(&config)?; -/// -/// let mut linker = Linker::::new(&engine); -/// wasmtime_wasi_http::add_to_linker_sync(&mut linker)?; -/// // ... add any further functionality to `linker` if desired ... -/// -/// Ok(()) -/// } -/// -/// struct MyState { -/// ctx: WasiCtx, -/// http_ctx: WasiHttpCtx, -/// table: ResourceTable, -/// } -/// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// ``` -pub fn add_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + wasmtime_wasi::WasiView + 'static, -{ - wasmtime_wasi::p2::add_to_linker_proxy_interfaces_sync(l)?; - add_only_http_to_linker_sync(l) -} - -/// A slimmed down version of [`add_to_linker_sync`] which only adds -/// `wasi:http` interfaces to the linker. -/// -/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_sync`] for -/// example to avoid re-adding the same interfaces twice. -pub fn add_only_http_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + 'static, -{ - let options = crate::bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. - crate::bindings::sync::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| { - WasiHttpImpl(x) - })?; - crate::bindings::sync::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { - WasiHttpImpl(x) - })?; - - Ok(()) -} +pub use ctx::*; /// Extract the `Content-Length` header value from a [`http::HeaderMap`], returning `None` if it's not /// present. This function will return `Err` if it's not possible to parse the `Content-Length` /// header. +#[cfg(any(feature = "p2", feature = "p3"))] fn get_content_length(headers: &http::HeaderMap) -> wasmtime::Result> { - let Some(v) = headers.get(CONTENT_LENGTH) else { + let Some(v) = headers.get(header::CONTENT_LENGTH) else { return Ok(None); }; let v = v.to_str()?; let v = v.parse()?; Ok(Some(v)) } + +/// Set of [http::header::HeaderName], that are forbidden by default +/// for requests and responses originating in the guest. +pub const DEFAULT_FORBIDDEN_HEADERS: [HeaderName; 9] = [ + header::CONNECTION, + HeaderName::from_static("keep-alive"), + header::PROXY_AUTHENTICATE, + header::PROXY_AUTHORIZATION, + HeaderName::from_static("proxy-connection"), + header::TRANSFER_ENCODING, + header::UPGRADE, + header::HOST, + HeaderName::from_static("http2-settings"), +]; diff --git a/crates/wasi-http/src/bindings.rs b/crates/wasi-http/src/p2/bindings.rs similarity index 93% rename from crates/wasi-http/src/bindings.rs rename to crates/wasi-http/src/p2/bindings.rs index bec9402a8775..e870ea033042 100644 --- a/crates/wasi-http/src/bindings.rs +++ b/crates/wasi-http/src/p2/bindings.rs @@ -2,8 +2,8 @@ #[expect(missing_docs, reason = "bindgen-generated code")] mod generated { - use crate::body; - use crate::types; + use crate::p2::body; + use crate::p2::types; wasmtime::component::bindgen!({ path: "wit", @@ -30,7 +30,7 @@ mod generated { "wasi:http/types.request-options": types::HostRequestOptions, }, trappable_error_type: { - "wasi:http/types.error-code" => crate::HttpError, + "wasi:http/types.error-code" => crate::p2::HttpError, }, }); } @@ -52,7 +52,7 @@ pub mod sync { imports: { default: tracing }, with: { // http is in this crate - "wasi:http": crate::bindings::http, + "wasi:http": crate::p2::bindings::http, // sync requires the wrapper in the wasmtime_wasi crate, in // order to have in_tokio "wasi:io": wasmtime_wasi::p2::bindings::sync::io, diff --git a/crates/wasi-http/src/body.rs b/crates/wasi-http/src/p2/body.rs similarity index 99% rename from crates/wasi-http/src/body.rs rename to crates/wasi-http/src/p2/body.rs index 000bd567c8c7..5d8c929b4278 100644 --- a/crates/wasi-http/src/body.rs +++ b/crates/wasi-http/src/p2/body.rs @@ -1,7 +1,7 @@ //! Implementation of the `wasi:http/types` interface's various body types. -use crate::bindings::http::types; -use crate::types::FieldMap; +use crate::p2::bindings::http::types; +use crate::p2::types::FieldMap; use bytes::Bytes; use http_body::{Body, Frame}; use http_body_util::BodyExt; diff --git a/crates/wasi-http/src/error.rs b/crates/wasi-http/src/p2/error.rs similarity index 96% rename from crates/wasi-http/src/error.rs rename to crates/wasi-http/src/p2/error.rs index 4279bd8483f3..9bc1ebd6f057 100644 --- a/crates/wasi-http/src/error.rs +++ b/crates/wasi-http/src/p2/error.rs @@ -1,4 +1,4 @@ -use crate::bindings::http::types::ErrorCode; +use crate::p2::bindings::http::types::ErrorCode; use std::error::Error; use std::fmt; use wasmtime::component::ResourceTableError; @@ -60,7 +60,7 @@ impl Error for HttpError {} #[cfg(feature = "default-send-request")] pub(crate) fn dns_error(rcode: String, info_code: u16) -> ErrorCode { - ErrorCode::DnsError(crate::bindings::http::types::DnsErrorPayload { + ErrorCode::DnsError(crate::p2::bindings::http::types::DnsErrorPayload { rcode: Some(rcode), info_code: Some(info_code), }) diff --git a/crates/wasi-http/src/http_impl.rs b/crates/wasi-http/src/p2/http_impl.rs similarity index 89% rename from crates/wasi-http/src/http_impl.rs rename to crates/wasi-http/src/p2/http_impl.rs index 903072cd43aa..45b4312044d9 100644 --- a/crates/wasi-http/src/http_impl.rs +++ b/crates/wasi-http/src/p2/http_impl.rs @@ -1,7 +1,7 @@ //! Implementation of the `wasi:http/outgoing-handler` interface. -use crate::{ - WasiHttpImpl, WasiHttpView, +use crate::p2::{ + HttpResult, WasiHttpCtxView, bindings::http::{ outgoing_handler, types::{self, Scheme}, @@ -15,16 +15,13 @@ use http_body_util::{BodyExt, Empty}; use hyper::Method; use wasmtime::component::Resource; -impl outgoing_handler::Host for WasiHttpImpl -where - T: WasiHttpView, -{ +impl outgoing_handler::Host for WasiHttpCtxView<'_> { fn handle( &mut self, request_id: Resource, options: Option>, - ) -> crate::HttpResult> { - let opts = options.and_then(|opts| self.table().get(&opts).ok()); + ) -> HttpResult> { + let opts = options.and_then(|opts| self.table.get(&opts).ok()); let connect_timeout = opts .and_then(|opts| opts.connect_timeout) @@ -38,7 +35,7 @@ where .and_then(|opts| opts.between_bytes_timeout) .unwrap_or(std::time::Duration::from_secs(600)); - let req = self.table().delete(request_id)?; + let req = self.table.delete(request_id)?; let mut builder = hyper::Request::builder(); builder = builder.method(match req.method { @@ -93,7 +90,7 @@ where .body(body) .map_err(|err| internal_error(err.to_string()))?; - let future = self.send_request( + let future = self.hooks.send_request( request, OutgoingRequestConfig { use_tls, @@ -103,6 +100,6 @@ where }, )?; - Ok(self.table().push(future)?) + Ok(self.table.push(future)?) } } diff --git a/crates/wasi-http/src/p2/mod.rs b/crates/wasi-http/src/p2/mod.rs new file mode 100644 index 000000000000..6f42d9260c26 --- /dev/null +++ b/crates/wasi-http/src/p2/mod.rs @@ -0,0 +1,706 @@ +//! # Wasmtime's WASI HTTPp2 Implementation +//! +//! This module is Wasmtime's host implementation of the `wasi:http` package as +//! part of WASIp2. This crate's implementation is primarily built on top of +//! [`hyper`] and [`tokio`]. +//! +//! # WASI HTTP Interfaces +//! +//! This crate contains implementations of the following interfaces: +//! +//! * [`wasi:http/incoming-handler`] +//! * [`wasi:http/outgoing-handler`] +//! * [`wasi:http/types`] +//! +//! The crate also contains an implementation of the [`wasi:http/proxy`] world. +//! +//! [`wasi:http/proxy`]: crate::p2::bindings::Proxy +//! [`wasi:http/outgoing-handler`]: crate::p2::bindings::http::outgoing_handler::Host +//! [`wasi:http/types`]: crate::p2::bindings::http::types::Host +//! [`wasi:http/incoming-handler`]: crate::p2::bindings::exports::wasi::http::incoming_handler::Guest +//! +//! This crate is very similar to [`wasmtime_wasi`] in the it uses the +//! `bindgen!` macro in Wasmtime to generate bindings to interfaces. Bindings +//! are located in the [`bindings`] module. +//! +//! # The `WasiHttp{View,Hooks}` traits +//! +//! All `bindgen!`-generated `Host` traits are implemented for the +//! [`WasiHttpCtxView`] type. This type is created from a store's data `T` +//! through the [`WasiHttpView`] trait. The [`add_to_linker_async`] function, +//! for example, uses [`WasiHttpView`] to acquire the context view. +//! +//! The [`WasiHttpCtxView`] structure requires that a [`ResourceTable`] and +//! [`WasiHttpCtx`] live within the store. This is store-specific state that is +//! used to implement various APIs and store host state. +//! +//! The final `hooks` field within [`WasiHttpCtxView`] is a trait object of +//! [`WasiHttpHooks`]. This provides a few more hooks, dynamically, to configure +//! how `wasi:http` behaves. For example [`WasiHttpHooks::send_request`] can +//! customize how outgoing HTTP requests are handled. The `hooks` field can be +//! initialized with the [`default_hooks`] function for the default behavior. +//! +//! # Async and Sync +//! +//! There are both asynchronous and synchronous bindings in this crate. For +//! example [`add_to_linker_async`] is for asynchronous embedders and +//! [`add_to_linker_sync`] is for synchronous embedders. Note that under the +//! hood both versions are implemented with `async` on top of [`tokio`]. +//! +//! # Examples +//! +//! Usage of this crate is done through a few steps to get everything hooked up: +//! +//! 1. First implement [`WasiHttpView`] for your type which is the `T` in +//! [`wasmtime::Store`]. +//! 2. Add WASI HTTP interfaces to a [`wasmtime::component::Linker`]. There +//! are a few options of how to do this: +//! * Use [`add_to_linker_async`] to bundle all interfaces in +//! `wasi:http/proxy` together +//! * Use [`add_only_http_to_linker_async`] to add only HTTP interfaces but +//! no others. This is useful when working with +//! [`wasmtime_wasi::p2::add_to_linker_async`] for example. +//! * Add individual interfaces such as with the +//! [`bindings::http::outgoing_handler::add_to_linker`] function. +//! 3. Use [`ProxyPre`](bindings::ProxyPre) to pre-instantiate a component +//! before serving requests. +//! 4. When serving requests use +//! [`ProxyPre::instantiate_async`](bindings::ProxyPre::instantiate_async) +//! to create instances and handle HTTP requests. +//! +//! A standalone example of doing all this looks like: +//! +//! ```no_run +//! use wasmtime::bail; +//! use hyper::server::conn::http1; +//! use std::sync::Arc; +//! use tokio::net::TcpListener; +//! use wasmtime::component::{Component, Linker, ResourceTable}; +//! use wasmtime::{Engine, Result, Store}; +//! use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +//! use wasmtime_wasi_http::p2::bindings::ProxyPre; +//! use wasmtime_wasi_http::p2::bindings::http::types::Scheme; +//! use wasmtime_wasi_http::p2::body::HyperOutgoingBody; +//! use wasmtime_wasi_http::io::TokioIo; +//! use wasmtime_wasi_http::{WasiHttpCtx, p2::{WasiHttpView, WasiHttpCtxView}}; +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let component = std::env::args().nth(1).unwrap(); +//! +//! // Prepare the `Engine` for Wasmtime +//! let engine = Engine::default(); +//! +//! // Compile the component on the command line to machine code +//! let component = Component::from_file(&engine, &component)?; +//! +//! // Prepare the `ProxyPre` which is a pre-instantiated version of the +//! // component that we have. This will make per-request instantiation +//! // much quicker. +//! let mut linker = Linker::new(&engine); +//! wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; +//! wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?; +//! let pre = ProxyPre::new(linker.instantiate_pre(&component)?)?; +//! +//! // Prepare our server state and start listening for connections. +//! let server = Arc::new(MyServer { pre }); +//! let listener = TcpListener::bind("127.0.0.1:8000").await?; +//! println!("Listening on {}", listener.local_addr()?); +//! +//! loop { +//! // Accept a TCP connection and serve all of its requests in a separate +//! // tokio task. Note that for now this only works with HTTP/1.1. +//! let (client, addr) = listener.accept().await?; +//! println!("serving new client from {addr}"); +//! +//! let server = server.clone(); +//! tokio::task::spawn(async move { +//! if let Err(e) = http1::Builder::new() +//! .keep_alive(true) +//! .serve_connection( +//! TokioIo::new(client), +//! hyper::service::service_fn(move |req| { +//! let server = server.clone(); +//! async move { server.handle_request(req).await } +//! }), +//! ) +//! .await +//! { +//! eprintln!("error serving client[{addr}]: {e:?}"); +//! } +//! }); +//! } +//! } +//! +//! struct MyServer { +//! pre: ProxyPre, +//! } +//! +//! impl MyServer { +//! async fn handle_request( +//! &self, +//! req: hyper::Request, +//! ) -> Result> { +//! // Create per-http-request state within a `Store` and prepare the +//! // initial resources passed to the `handle` function. +//! let mut store = Store::new( +//! self.pre.engine(), +//! MyClientState { +//! table: ResourceTable::new(), +//! wasi: WasiCtx::builder().inherit_stdio().build(), +//! http: WasiHttpCtx::new(), +//! }, +//! ); +//! let (sender, receiver) = tokio::sync::oneshot::channel(); +//! let req = store.data_mut().http().new_incoming_request(Scheme::Http, req)?; +//! let out = store.data_mut().http().new_response_outparam(sender)?; +//! let pre = self.pre.clone(); +//! +//! // Run the http request itself in a separate task so the task can +//! // optionally continue to execute beyond after the initial +//! // headers/response code are sent. +//! let task = tokio::task::spawn(async move { +//! let proxy = pre.instantiate_async(&mut store).await?; +//! +//! if let Err(e) = proxy +//! .wasi_http_incoming_handler() +//! .call_handle(store, req, out) +//! .await +//! { +//! return Err(e); +//! } +//! +//! Ok(()) +//! }); +//! +//! match receiver.await { +//! // If the client calls `response-outparam::set` then one of these +//! // methods will be called. +//! Ok(Ok(resp)) => Ok(resp), +//! Ok(Err(e)) => Err(e.into()), +//! +//! // Otherwise the `sender` will get dropped along with the `Store` +//! // meaning that the oneshot will get disconnected and here we can +//! // inspect the `task` result to see what happened +//! Err(_) => { +//! let e = match task.await { +//! Ok(Ok(())) => { +//! bail!("guest never invoked `response-outparam::set` method") +//! } +//! Ok(Err(e)) => e, +//! Err(e) => e.into(), +//! }; +//! return Err(e.context("guest never invoked `response-outparam::set` method")); +//! } +//! } +//! } +//! } +//! +//! struct MyClientState { +//! wasi: WasiCtx, +//! http: WasiHttpCtx, +//! table: ResourceTable, +//! } +//! +//! impl WasiView for MyClientState { +//! fn ctx(&mut self) -> WasiCtxView<'_> { +//! WasiCtxView { ctx: &mut self.wasi, table: &mut self.table } +//! } +//! } +//! +//! impl WasiHttpView for MyClientState { +//! fn http(&mut self) -> WasiHttpCtxView<'_> { +//! WasiHttpCtxView { +//! ctx: &mut self.http, +//! table: &mut self.table, +//! hooks: Default::default(), +//! } +//! } +//! } +//! ``` + +#[cfg(feature = "default-send-request")] +use self::bindings::http::types::ErrorCode; +use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; +use http::HeaderName; +use wasmtime::component::{HasData, Linker, ResourceTable}; + +mod error; +mod http_impl; +mod types_impl; + +pub mod bindings; +pub mod body; +pub mod types; + +pub use self::error::{ + HttpError, HttpResult, http_request_error, hyper_request_error, hyper_response_error, +}; + +/// A trait which provides hooks into internal WASI HTTP operations. +/// +/// # Example +/// +/// ``` +/// use wasmtime::component::ResourceTable; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// +/// impl MyState { +/// fn new() -> MyState { +/// let mut wasi = WasiCtx::builder(); +/// wasi.arg("./foo.wasm"); +/// wasi.arg("--help"); +/// wasi.env("FOO", "bar"); +/// +/// MyState { +/// ctx: wasi.build(), +/// table: ResourceTable::new(), +/// http_ctx: WasiHttpCtx::new(), +/// } +/// } +/// } +/// ``` +pub trait WasiHttpHooks { + /// Send an outgoing request. + #[cfg(feature = "default-send-request")] + fn send_request( + &mut self, + request: hyper::Request, + config: types::OutgoingRequestConfig, + ) -> HttpResult { + Ok(default_send_request(request, config)) + } + + /// Send an outgoing request. + #[cfg(not(feature = "default-send-request"))] + fn send_request( + &mut self, + request: hyper::Request, + config: types::OutgoingRequestConfig, + ) -> HttpResult; + + /// Whether a given header should be considered forbidden and not allowed. + fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { + DEFAULT_FORBIDDEN_HEADERS.contains(name) + } + + /// Number of distinct write calls to the outgoing body's output-stream + /// that the implementation will buffer. + /// Default: 1. + fn outgoing_body_buffer_chunks(&mut self) -> usize { + DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS + } + + /// Maximum size allowed in a write call to the outgoing body's output-stream. + /// Default: 1024 * 1024. + fn outgoing_body_chunk_size(&mut self) -> usize { + DEFAULT_OUTGOING_BODY_CHUNK_SIZE + } +} + +#[cfg(feature = "default-send-request")] +impl<'a> Default for &'a mut dyn WasiHttpHooks { + fn default() -> Self { + let x: &mut [(); 0] = &mut []; + x + } +} + +#[doc(hidden)] +#[cfg(feature = "default-send-request")] +impl WasiHttpHooks for [(); 0] {} + +/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has +/// the default behavior for `wasi:http`. +#[cfg(feature = "default-send-request")] +pub fn default_hooks() -> &'static mut dyn WasiHttpHooks { + Default::default() +} + +/// The default value configured for [`WasiHttpHooks::outgoing_body_buffer_chunks`] in [`WasiHttpView`]. +pub const DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS: usize = 1; +/// The default value configured for [`WasiHttpHooks::outgoing_body_chunk_size`] in [`WasiHttpView`]. +pub const DEFAULT_OUTGOING_BODY_CHUNK_SIZE: usize = 1024 * 1024; + +/// Structure which `wasi:http` `Host`-style traits are implemented for. +/// +/// This structure is used by embedders with the [`WasiHttpView`] trait's return +/// value and is used to provide access to this crate all internals necessary to +/// implement `wasi:http`. This is similar to [`wasmtime_wasi::WasiCtxView`] +/// for example. +pub struct WasiHttpCtxView<'a> { + /// A reference to a per-store [`WasiHttpCtx`]. + pub ctx: &'a mut WasiHttpCtx, + /// A reference to a per-store table of resources to store host structures + /// within. + pub table: &'a mut ResourceTable, + /// A reference to a per-store set of hooks that can be used to customize + /// `wasi:http` behavior. + pub hooks: &'a mut dyn WasiHttpHooks, +} + +/// The type for which this crate implements the `wasi:http` interfaces. +pub struct WasiHttp; + +impl HasData for WasiHttp { + type Data<'a> = WasiHttpCtxView<'a>; +} + +/// A trait used to project state that this crate needs to implement `wasi:http` +/// from the `self` type. +/// +/// This trait is used in [`add_to_linker_sync`] and [`add_to_linker_async`] for +/// example as a bound on `T` in `Store`. This is used to access data from +/// `T`, the data within a `Store`, an instance of [`WasiHttpCtxView`]. The +/// [`WasiHttpCtxView`] contains contextual information such as the +/// [`ResourceTable`] for the store, HTTP context info in [`WasiHttpCtx`], and +/// any hooks via [`WasiHttpHooks`] if the embedder desires. +/// +/// # Example +/// +/// ``` +/// use wasmtime::component::ResourceTable; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; +/// +/// struct MyState { +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// ``` +pub trait WasiHttpView { + /// Returns an instance of [`WasiHttpCtxView`] projected out of `self`. + fn http(&mut self) -> WasiHttpCtxView<'_>; +} + +/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. +/// +/// This function will add the `async` variant of all interfaces into the +/// `Linker` provided. For embeddings with async support disabled see +/// [`add_to_linker_sync`] instead. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p2::{WasiHttpView, WasiHttpCtxView}}; +/// +/// fn main() -> Result<()> { +/// let engine = Engine::default(); +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_http::p2::add_to_linker_async(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker_async(l: &mut wasmtime::component::Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + wasmtime_wasi::WasiView + 'static, +{ + wasmtime_wasi::p2::add_to_linker_proxy_interfaces_async(l)?; + add_only_http_to_linker_async(l) +} + +/// A slimmed down version of [`add_to_linker_async`] which only adds +/// `wasi:http` interfaces to the linker. +/// +/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_async`] for +/// example to avoid re-adding the same interfaces twice. +pub fn add_only_http_to_linker_async( + l: &mut wasmtime::component::Linker, +) -> wasmtime::Result<()> +where + T: WasiHttpView + 'static, +{ + let options = bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. + bindings::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, T::http)?; + bindings::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), T::http)?; + + Ok(()) +} + +/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. +/// +/// This function will add the `sync` variant of all interfaces into the +/// `Linker` provided. For embeddings with async support see +/// [`add_to_linker_async`] instead. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; +/// +/// fn main() -> Result<()> { +/// let config = Config::default(); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_http::p2::add_to_linker_sync(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + wasmtime_wasi::WasiView + 'static, +{ + wasmtime_wasi::p2::add_to_linker_proxy_interfaces_sync(l)?; + add_only_http_to_linker_sync(l) +} + +/// A slimmed down version of [`add_to_linker_sync`] which only adds +/// `wasi:http` interfaces to the linker. +/// +/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_sync`] for +/// example to avoid re-adding the same interfaces twice. +pub fn add_only_http_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + 'static, +{ + let options = bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. + bindings::sync::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, T::http)?; + bindings::sync::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), T::http)?; + + Ok(()) +} + +/// The default implementation of how an outgoing request is sent. +/// +/// This implementation is used by the `wasi:http/outgoing-handler` interface +/// default implementation. +#[cfg(feature = "default-send-request")] +pub fn default_send_request( + request: hyper::Request, + config: types::OutgoingRequestConfig, +) -> types::HostFutureIncomingResponse { + let handle = wasmtime_wasi::runtime::spawn(async move { + Ok(default_send_request_handler(request, config).await) + }); + types::HostFutureIncomingResponse::pending(handle) +} + +/// The underlying implementation of how an outgoing request is sent. This should likely be spawned +/// in a task. +/// +/// This is called from [default_send_request] to actually send the request. +#[cfg(feature = "default-send-request")] +pub async fn default_send_request_handler( + mut request: hyper::Request, + types::OutgoingRequestConfig { + use_tls, + connect_timeout, + first_byte_timeout, + between_bytes_timeout, + }: types::OutgoingRequestConfig, +) -> Result { + use crate::io::TokioIo; + use crate::p2::{error::dns_error, hyper_request_error}; + use http_body_util::BodyExt; + use tokio::net::TcpStream; + use tokio::time::timeout; + + let authority = if let Some(authority) = request.uri().authority() { + if authority.port().is_some() { + authority.to_string() + } else { + let port = if use_tls { 443 } else { 80 }; + format!("{}:{port}", authority.to_string()) + } + } else { + return Err(ErrorCode::HttpRequestUriInvalid); + }; + let tcp_stream = timeout(connect_timeout, TcpStream::connect(&authority)) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(|e| match e.kind() { + std::io::ErrorKind::AddrNotAvailable => { + dns_error("address not available".to_string(), 0) + } + + _ => { + if e.to_string() + .starts_with("failed to lookup address information") + { + dns_error("address not available".to_string(), 0) + } else { + ErrorCode::ConnectionRefused + } + } + })?; + + let (mut sender, worker) = if use_tls { + use rustls::pki_types::ServerName; + + // derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs + let root_cert_store = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.into(), + }; + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_cert_store) + .with_no_client_auth(); + let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config)); + let mut parts = authority.split(":"); + let host = parts.next().unwrap_or(&authority); + let domain = ServerName::try_from(host) + .map_err(|e| { + tracing::warn!("dns lookup error: {e:?}"); + dns_error("invalid dns name".to_string(), 0) + })? + .to_owned(); + let stream = connector.connect(domain, tcp_stream).await.map_err(|e| { + tracing::warn!("tls protocol error: {e:?}"); + ErrorCode::TlsProtocolError + })?; + let stream = TokioIo::new(stream); + + let (sender, conn) = timeout( + connect_timeout, + hyper::client::conn::http1::handshake(stream), + ) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(hyper_request_error)?; + + let worker = wasmtime_wasi::runtime::spawn(async move { + match conn.await { + Ok(()) => {} + // TODO: shouldn't throw away this error and ideally should + // surface somewhere. + Err(e) => tracing::warn!("dropping error {e}"), + } + }); + + (sender, worker) + } else { + let tcp_stream = TokioIo::new(tcp_stream); + let (sender, conn) = timeout( + connect_timeout, + // TODO: we should plumb the builder through the http context, and use it here + hyper::client::conn::http1::handshake(tcp_stream), + ) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(hyper_request_error)?; + + let worker = wasmtime_wasi::runtime::spawn(async move { + match conn.await { + Ok(()) => {} + // TODO: same as above, shouldn't throw this error away. + Err(e) => tracing::warn!("dropping error {e}"), + } + }); + + (sender, worker) + }; + + // at this point, the request contains the scheme and the authority, but + // the http packet should only include those if addressing a proxy, so + // remove them here, since SendRequest::send_request does not do it for us + *request.uri_mut() = http::Uri::builder() + .path_and_query( + request + .uri() + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or("/"), + ) + .build() + .expect("comes from valid request"); + + let resp = timeout(first_byte_timeout, sender.send_request(request)) + .await + .map_err(|_| ErrorCode::ConnectionReadTimeout)? + .map_err(hyper_request_error)? + .map(|body| body.map_err(hyper_request_error).boxed_unsync()); + + Ok(types::IncomingResponse { + resp, + worker: Some(worker), + between_bytes_timeout, + }) +} diff --git a/crates/wasi-http/src/p2/types.rs b/crates/wasi-http/src/p2/types.rs new file mode 100644 index 000000000000..d53290acd306 --- /dev/null +++ b/crates/wasi-http/src/p2/types.rs @@ -0,0 +1,449 @@ +//! Implements the base structure that will provide the implementation of the +//! wasi-http API. + +use crate::p2::{ + WasiHttpCtxView, WasiHttpHooks, + bindings::http::types::{self, ErrorCode, Method, Scheme}, + body::{HostIncomingBody, HyperIncomingBody, HyperOutgoingBody}, +}; +use bytes::Bytes; +use http::header::{HeaderMap, HeaderName, HeaderValue}; +use http_body_util::BodyExt; +use hyper::body::Body; +use std::any::Any; +use std::fmt; +use std::time::Duration; +use wasmtime::component::Resource; +use wasmtime::{Result, bail}; +use wasmtime_wasi::p2::Pollable; +use wasmtime_wasi::runtime::AbortOnDropJoinHandle; + +/// Removes forbidden headers from a [`FieldMap`]. +pub(crate) fn remove_forbidden_headers(hooks: &mut dyn WasiHttpHooks, headers: &mut FieldMap) { + let forbidden_keys = Vec::from_iter(headers.as_ref().keys().filter_map(|name| { + if hooks.is_forbidden_header(name) { + Some(name.clone()) + } else { + None + } + })); + + for name in forbidden_keys { + headers.remove_all(&name); + } +} + +/// Configuration for an outgoing request. +pub struct OutgoingRequestConfig { + /// Whether to use TLS for the request. + pub use_tls: bool, + /// The timeout for connecting. + pub connect_timeout: Duration, + /// The timeout until the first byte. + pub first_byte_timeout: Duration, + /// The timeout between chunks of a streaming body + pub between_bytes_timeout: Duration, +} + +impl From for types::Method { + fn from(method: http::Method) -> Self { + if method == http::Method::GET { + types::Method::Get + } else if method == hyper::Method::HEAD { + types::Method::Head + } else if method == hyper::Method::POST { + types::Method::Post + } else if method == hyper::Method::PUT { + types::Method::Put + } else if method == hyper::Method::DELETE { + types::Method::Delete + } else if method == hyper::Method::CONNECT { + types::Method::Connect + } else if method == hyper::Method::OPTIONS { + types::Method::Options + } else if method == hyper::Method::TRACE { + types::Method::Trace + } else if method == hyper::Method::PATCH { + types::Method::Patch + } else { + types::Method::Other(method.to_string()) + } + } +} + +impl TryInto for types::Method { + type Error = http::method::InvalidMethod; + + fn try_into(self) -> Result { + match self { + Method::Get => Ok(http::Method::GET), + Method::Head => Ok(http::Method::HEAD), + Method::Post => Ok(http::Method::POST), + Method::Put => Ok(http::Method::PUT), + Method::Delete => Ok(http::Method::DELETE), + Method::Connect => Ok(http::Method::CONNECT), + Method::Options => Ok(http::Method::OPTIONS), + Method::Trace => Ok(http::Method::TRACE), + Method::Patch => Ok(http::Method::PATCH), + Method::Other(s) => http::Method::from_bytes(s.as_bytes()), + } + } +} + +/// The concrete type behind a `wasi:http/types.incoming-request` resource. +#[derive(Debug)] +pub struct HostIncomingRequest { + pub(crate) method: http::method::Method, + pub(crate) uri: http::uri::Uri, + pub(crate) headers: FieldMap, + pub(crate) scheme: Scheme, + pub(crate) authority: String, + /// The body of the incoming request. + pub body: Option, +} + +impl WasiHttpCtxView<'_> { + /// Create a new incoming request resource. + pub fn new_incoming_request( + &mut self, + scheme: Scheme, + req: hyper::Request, + ) -> wasmtime::Result> + where + B: Body + Send + 'static, + B::Error: Into, + { + let field_size_limit = self.ctx.field_size_limit; + let (parts, body) = req.into_parts(); + let body = body.map_err(Into::into).boxed_unsync(); + let body = HostIncomingBody::new( + body, + // TODO: this needs to be plumbed through + std::time::Duration::from_millis(600 * 1000), + field_size_limit, + ); + let authority = match parts.uri.authority() { + Some(authority) => authority.to_string(), + None => match parts.headers.get(http::header::HOST) { + Some(host) => host.to_str()?.to_string(), + None => bail!("invalid HTTP request missing authority in URI and host header"), + }, + }; + + let mut headers = FieldMap::new(parts.headers, field_size_limit); + remove_forbidden_headers(self.hooks, &mut headers); + + let req = HostIncomingRequest { + method: parts.method, + uri: parts.uri, + headers, + authority, + scheme, + body: Some(body), + }; + Ok(self.table.push(req)?) + } +} + +/// The concrete type behind a `wasi:http/types.response-outparam` resource. +pub struct HostResponseOutparam { + /// The sender for sending a response. + pub result: + tokio::sync::oneshot::Sender, types::ErrorCode>>, +} + +impl WasiHttpCtxView<'_> { + /// Create a new outgoing response resource. + pub fn new_response_outparam( + &mut self, + result: tokio::sync::oneshot::Sender< + Result, types::ErrorCode>, + >, + ) -> wasmtime::Result> { + let id = self.table.push(HostResponseOutparam { result })?; + Ok(id) + } +} + +/// The concrete type behind a `wasi:http/types.outgoing-response` resource. +pub struct HostOutgoingResponse { + /// The status of the response. + pub status: http::StatusCode, + /// The headers of the response. + pub headers: FieldMap, + /// The body of the response. + pub body: Option, +} + +impl TryFrom for hyper::Response { + type Error = http::Error; + + fn try_from( + resp: HostOutgoingResponse, + ) -> Result, Self::Error> { + use http_body_util::Empty; + + let mut builder = hyper::Response::builder().status(resp.status); + + *builder.headers_mut().unwrap() = resp.headers.map; + + match resp.body { + Some(body) => builder.body(body), + None => builder.body( + Empty::::new() + .map_err(|_| unreachable!("Infallible error")) + .boxed_unsync(), + ), + } + } +} + +/// The concrete type behind a `wasi:http/types.outgoing-request` resource. +#[derive(Debug)] +pub struct HostOutgoingRequest { + /// The method of the request. + pub method: Method, + /// The scheme of the request. + pub scheme: Option, + /// The authority of the request. + pub authority: Option, + /// The path and query of the request. + pub path_with_query: Option, + /// The request headers. + pub headers: FieldMap, + /// The request body. + pub body: Option, +} + +/// The concrete type behind a `wasi:http/types.request-options` resource. +#[derive(Debug, Default)] +pub struct HostRequestOptions { + /// How long to wait for a connection to be established. + pub connect_timeout: Option, + /// How long to wait for the first byte of the response body. + pub first_byte_timeout: Option, + /// How long to wait between frames of the response body. + pub between_bytes_timeout: Option, +} + +/// The concrete type behind a `wasi:http/types.incoming-response` resource. +#[derive(Debug)] +pub struct HostIncomingResponse { + /// The response status + pub status: u16, + /// The response headers + pub headers: FieldMap, + /// The response body + pub body: Option, +} + +/// The concrete type behind a `wasi:http/types.fields` resource. +#[derive(Debug)] +pub enum HostFields { + /// A reference to the fields of a parent entry. + Ref { + /// The parent resource rep. + parent: u32, + + /// The function to get the fields from the parent. + // NOTE: there's not failure in the result here because we assume that HostFields will + // always be registered as a child of the entry with the `parent` id. This ensures that the + // entry will always exist while this `HostFields::Ref` entry exists in the table, thus we + // don't need to account for failure when fetching the fields ref from the parent. + get_fields: for<'a> fn(elem: &'a mut (dyn Any + 'static)) -> &'a mut FieldMap, + }, + /// An owned version of the fields. + Owned { + /// The fields themselves. + fields: FieldMap, + }, +} + +/// An owned version of `HostFields`. A wrapper on http `HeaderMap` that +/// keeps a running tally of memory consumed by header names and values. +#[derive(Debug, Clone)] +pub struct FieldMap { + map: HeaderMap, + limit: usize, + size: usize, +} + +/// Error given when a `FieldMap` has exceeded the size limit. +#[derive(Debug)] +pub struct FieldSizeLimitError { + /// The erroring `FieldMap` operation would require this content size + pub(crate) size: usize, + /// The limit set on `FieldMap` content size + pub(crate) limit: usize, +} +impl fmt::Display for FieldSizeLimitError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Field size limit {} exceeded: {}", self.limit, self.size) + } +} +impl std::error::Error for FieldSizeLimitError {} + +impl FieldMap { + /// Construct a `FieldMap` from a `HeaderMap` and a size limit. + /// + /// Construction with a `HeaderMap` which exceeds the size limit is + /// allowed, but subsequent operations to expand the resource use will + /// fail. + pub fn new(map: HeaderMap, limit: usize) -> Self { + let size = Self::content_size(&map); + Self { map, size, limit } + } + /// Construct an empty `FieldMap` + pub fn empty(limit: usize) -> Self { + Self { + map: HeaderMap::new(), + size: 0, + limit, + } + } + /// Get the `HeaderMap` out of the `FieldMap` + pub fn into_inner(self) -> HeaderMap { + self.map + } + /// Calculate the content size of a `HeaderMap`. This is a sum of the size + /// of all of the keys and all of the values. + pub(crate) fn content_size(map: &HeaderMap) -> usize { + let mut sum = 0; + for key in map.keys() { + sum += header_name_size(key); + } + for value in map.values() { + sum += header_value_size(value); + } + sum + } + /// Remove all values associated with a key in a map. + /// + /// Returns an empty list if the key is not already present within the map. + pub fn remove_all(&mut self, key: &HeaderName) -> Vec { + use http::header::Entry; + match self.map.try_entry(key) { + Ok(Entry::Vacant { .. }) | Err(_) => Vec::new(), + Ok(Entry::Occupied(e)) => { + let (name, value_drain) = e.remove_entry_mult(); + let mut removed = header_name_size(&name); + let values = value_drain.collect::>(); + for v in values.iter() { + removed += header_value_size(v); + } + self.size -= removed; + values + } + } + } + /// Add a value associated with a key to the map. + /// + /// If `key` is already present within the map then `value` is appended to + /// the list of values it already has. + pub fn append(&mut self, key: &HeaderName, value: HeaderValue) -> Result { + let key_size = header_name_size(key); + let val_size = header_value_size(&value); + let new_size = if !self.map.contains_key(key) { + self.size + key_size + val_size + } else { + self.size + val_size + }; + if new_size > self.limit { + bail!(FieldSizeLimitError { + limit: self.limit, + size: new_size + }) + } + self.size = new_size; + Ok(self.map.try_append(key, value)?) + } +} + +/// Returns the size, in accounting cost, to consider for `name`. +/// +/// This includes both the byte length of the `name` itself as well as the size +/// of the data structure itself as it'll reside within a `HeaderMap`. +fn header_name_size(name: &HeaderName) -> usize { + name.as_str().len() + size_of::() +} + +/// Same as `header_name_size`, but for values. +/// +/// This notably includes the size of `HeaderValue` itself to ensure that all +/// headers have a nonzero size as otherwise this would never limit addition of +/// an empty header value. +fn header_value_size(value: &HeaderValue) -> usize { + value.len() + size_of::() +} + +// We impl AsRef, but not AsMut, because any modifications of the +// underlying HeaderMap must account for changes in size +impl AsRef for FieldMap { + fn as_ref(&self) -> &HeaderMap { + &self.map + } +} + +/// A handle to a future incoming response. +pub type FutureIncomingResponseHandle = + AbortOnDropJoinHandle>>; + +/// A response that is in the process of being received. +#[derive(Debug)] +pub struct IncomingResponse { + /// The response itself. + pub resp: hyper::Response, + /// Optional worker task that continues to process the response. + pub worker: Option>, + /// The timeout between chunks of the response. + pub between_bytes_timeout: std::time::Duration, +} + +/// The concrete type behind a `wasi:http/types.future-incoming-response` resource. +#[derive(Debug)] +pub enum HostFutureIncomingResponse { + /// A pending response + Pending(FutureIncomingResponseHandle), + /// The response is ready. + /// + /// An outer error will trap while the inner error gets returned to the guest. + Ready(wasmtime::Result>), + /// The response has been consumed. + Consumed, +} + +impl HostFutureIncomingResponse { + /// Create a new `HostFutureIncomingResponse` that is pending on the provided task handle. + pub fn pending(handle: FutureIncomingResponseHandle) -> Self { + Self::Pending(handle) + } + + /// Create a new `HostFutureIncomingResponse` that is ready. + pub fn ready(result: wasmtime::Result>) -> Self { + Self::Ready(result) + } + + /// Returns `true` if the response is ready. + pub fn is_ready(&self) -> bool { + matches!(self, Self::Ready(_)) + } + + /// Unwrap the response, panicking if it is not ready. + pub fn unwrap_ready(self) -> wasmtime::Result> { + match self { + Self::Ready(res) => res, + Self::Pending(_) | Self::Consumed => { + panic!("unwrap_ready called on a pending HostFutureIncomingResponse") + } + } + } +} + +#[async_trait::async_trait] +impl Pollable for HostFutureIncomingResponse { + async fn ready(&mut self) { + if let Self::Pending(handle) = self { + *self = Self::Ready(handle.await); + } + } +} diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/p2/types_impl.rs similarity index 78% rename from crates/wasi-http/src/types_impl.rs rename to crates/wasi-http/src/p2/types_impl.rs index 45db0df7d8ab..3bf7e377cc07 100644 --- a/crates/wasi-http/src/types_impl.rs +++ b/crates/wasi-http/src/p2/types_impl.rs @@ -1,13 +1,14 @@ //! Implementation for the `wasi:http/types` interface. -use crate::bindings::http::types::{self, Headers, Method, Scheme, StatusCode, Trailers}; -use crate::body::{HostFutureTrailers, HostIncomingBody, HostOutgoingBody, StreamContext}; -use crate::types::{ +use crate::get_content_length; +use crate::p2::bindings::http::types::{self, Headers, Method, Scheme, StatusCode, Trailers}; +use crate::p2::body::{HostFutureTrailers, HostIncomingBody, HostOutgoingBody, StreamContext}; +use crate::p2::types::{ FieldMap, FieldSizeLimitError, HostFields, HostFutureIncomingResponse, HostIncomingRequest, HostIncomingResponse, HostOutgoingRequest, HostOutgoingResponse, HostResponseOutparam, remove_forbidden_headers, }; -use crate::{HttpError, HttpResult, WasiHttpImpl, WasiHttpView, get_content_length}; +use crate::p2::{HttpError, HttpResult, WasiHttpCtxView}; use std::any::Any; use std::str::FromStr; use wasmtime::bail; @@ -15,11 +16,8 @@ use wasmtime::component::{Resource, ResourceTable, ResourceTableError}; use wasmtime::{error::Context as _, format_err}; use wasmtime_wasi::p2::{DynInputStream, DynOutputStream, DynPollable}; -impl crate::bindings::http::types::Host for WasiHttpImpl -where - T: WasiHttpView, -{ - fn convert_error_code(&mut self, err: crate::HttpError) -> wasmtime::Result { +impl types::Host for WasiHttpCtxView<'_> { + fn convert_error_code(&mut self, err: HttpError) -> wasmtime::Result { err.downcast() } @@ -27,7 +25,7 @@ where &mut self, err: wasmtime::component::Resource, ) -> wasmtime::Result> { - let e = self.table().get(&err)?; + let e = self.table.get(&err)?; Ok(e.downcast_ref::().cloned()) } } @@ -77,14 +75,11 @@ fn get_fields_mut<'a>( } } -impl crate::bindings::http::types::HostFields for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFields for WasiHttpCtxView<'_> { fn new(&mut self) -> wasmtime::Result> { - let limit = self.ctx().field_size_limit; + let limit = self.ctx.field_size_limit; let id = self - .table() + .table .push(HostFields::Owned { fields: FieldMap::empty(limit), }) @@ -105,7 +100,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -118,15 +113,15 @@ where } let size = FieldMap::content_size(&fields); - if size > self.ctx().field_size_limit { + if size > self.ctx.field_size_limit { bail!(FieldSizeLimitError { size, - limit: self.ctx().field_size_limit, + limit: self.ctx.field_size_limit, }); } - let fields = FieldMap::new(fields, self.ctx().field_size_limit); + let fields = FieldMap::new(fields, self.ctx.field_size_limit); let id = self - .table() + .table .push(HostFields::Owned { fields }) .context("[new_fields] pushing fields")?; @@ -134,7 +129,7 @@ where } fn drop(&mut self, fields: Resource) -> wasmtime::Result<()> { - self.table() + self.table .delete(fields) .context("[drop_fields] deleting fields")?; Ok(()) @@ -145,7 +140,7 @@ where fields: Resource, name: String, ) -> wasmtime::Result>> { - let fields = get_fields(self.table(), &fields).context("[fields_get] getting fields")?; + let fields = get_fields(self.table, &fields).context("[fields_get] getting fields")?; let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) { Ok(header) => header, @@ -166,7 +161,7 @@ where } fn has(&mut self, fields: Resource, name: String) -> wasmtime::Result { - let fields = get_fields(self.table(), &fields).context("[fields_get] getting fields")?; + let fields = get_fields(self.table, &fields).context("[fields_get] getting fields")?; match hyper::header::HeaderName::from_bytes(name.as_bytes()) { Ok(header) => Ok(fields.as_ref().contains_key(&header)), @@ -185,7 +180,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -197,9 +192,7 @@ where } } - match get_fields_mut(self.table(), &fields) - .context("[fields_set] getting mutable fields")? - { + match get_fields_mut(self.table, &fields).context("[fields_set] getting mutable fields")? { Ok(fields) => { fields.remove_all(&header); for value in values { @@ -221,11 +214,11 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } - Ok(get_fields_mut(self.table(), &fields)?.map(|fields| { + Ok(get_fields_mut(self.table, &fields)?.map(|fields| { fields.remove_all(&header); })) } @@ -241,7 +234,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -250,7 +243,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - match get_fields_mut(self.table(), &fields) + match get_fields_mut(self.table, &fields) .context("[fields_append] getting mutable fields")? { Ok(fields) => { @@ -265,7 +258,7 @@ where &mut self, fields: Resource, ) -> wasmtime::Result)>> { - Ok(get_fields(self.table(), &fields)? + Ok(get_fields(self.table, &fields)? .as_ref() .iter() .map(|(name, value)| (name.as_str().to_owned(), value.as_bytes().to_owned())) @@ -273,12 +266,12 @@ where } fn clone(&mut self, fields: Resource) -> wasmtime::Result> { - let fields = get_fields(self.table(), &fields) + let fields = get_fields(self.table, &fields) .context("[fields_clone] getting fields")? .clone(); let id = self - .table() + .table .push(HostFields::Owned { fields }) .context("[fields_clone] pushing fields")?; @@ -286,30 +279,27 @@ where } } -impl crate::bindings::http::types::HostIncomingRequest for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingRequest for WasiHttpCtxView<'_> { fn method(&mut self, id: Resource) -> wasmtime::Result { - let method = self.table().get(&id)?.method.clone(); + let method = self.table.get(&id)?.method.clone(); Ok(method.into()) } fn path_with_query( &mut self, id: Resource, ) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(req .uri .path_and_query() .map(|path_and_query| path_and_query.as_str().to_owned())) } fn scheme(&mut self, id: Resource) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(Some(req.scheme.clone())) } fn authority(&mut self, id: Resource) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(Some(req.authority.clone())) } @@ -317,13 +307,13 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - let _ = self.table().get(&id)?; + let _ = self.table.get(&id)?; fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { &mut elem.downcast_mut::().unwrap().headers } - let headers = self.table().push_child( + let headers = self.table.push_child( HostFields::Ref { parent: id.rep(), get_fields, @@ -338,10 +328,10 @@ where &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let req = self.table().get_mut(&id)?; + let req = self.table.get_mut(&id)?; match req.body.take() { Some(body) => { - let id = self.table().push(body)?; + let id = self.table.push(body)?; Ok(Ok(id)) } @@ -350,22 +340,19 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl crate::bindings::http::types::HostOutgoingRequest for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingRequest for WasiHttpCtxView<'_> { fn new( &mut self, headers: Resource, ) -> wasmtime::Result> { - let headers = move_fields(self.table(), headers)?; + let headers = move_fields(self.table, headers)?; - self.table() + self.table .push(HostOutgoingRequest { path_with_query: None, authority: None, @@ -381,10 +368,10 @@ where &mut self, request: Resource, ) -> wasmtime::Result, ()>> { - let buffer_chunks = self.outgoing_body_buffer_chunks(); - let chunk_size = self.outgoing_body_chunk_size(); + let buffer_chunks = self.hooks.outgoing_body_buffer_chunks(); + let chunk_size = self.hooks.outgoing_body_chunk_size(); let req = self - .table() + .table .get_mut(&request) .context("[outgoing_request_write] getting request")?; @@ -404,13 +391,13 @@ where // The output stream will necessarily outlive the request, because we could be still // writing to the stream after `outgoing-handler.handle` is called. - let outgoing_body = self.table().push(host_body)?; + let outgoing_body = self.table.push(host_body)?; Ok(Ok(outgoing_body)) } fn drop(&mut self, request: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(request)?; + let _ = self.table.delete(request)?; Ok(()) } @@ -418,7 +405,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result { - Ok(self.table().get(&request)?.method.clone()) + Ok(self.table.get(&request)?.method.clone()) } fn set_method( @@ -426,7 +413,7 @@ where request: wasmtime::component::Resource, method: Method, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Method::Other(s) = &method { if let Err(_) = http::Method::from_str(s) { @@ -443,7 +430,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.path_with_query.clone()) + Ok(self.table.get(&request)?.path_with_query.clone()) } fn set_path_with_query( @@ -451,7 +438,7 @@ where request: wasmtime::component::Resource, path_with_query: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(s) = path_with_query.as_ref() { if let Err(_) = http::uri::PathAndQuery::from_str(s) { @@ -468,7 +455,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.scheme.clone()) + Ok(self.table.get(&request)?.scheme.clone()) } fn set_scheme( @@ -476,7 +463,7 @@ where request: wasmtime::component::Resource, scheme: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(types::Scheme::Other(s)) = scheme.as_ref() { if let Err(_) = http::uri::Scheme::from_str(s.as_str()) { @@ -493,7 +480,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.authority.clone()) + Ok(self.table.get(&request)?.authority.clone()) } fn set_authority( @@ -501,7 +488,7 @@ where request: wasmtime::component::Resource, authority: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(s) = authority.as_ref() { if let Err(_) = http::uri::Authority::from_str(s.as_str()) { @@ -519,7 +506,7 @@ where request: wasmtime::component::Resource, ) -> wasmtime::Result> { let _ = self - .table() + .table .get(&request) .context("[outgoing_request_headers] getting request")?; @@ -530,7 +517,7 @@ where .headers } - let id = self.table().push_child( + let id = self.table.push_child( HostFields::Ref { parent: request.rep(), get_fields, @@ -542,12 +529,9 @@ where } } -impl crate::bindings::http::types::HostResponseOutparam for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostResponseOutparam for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } fn set( @@ -556,11 +540,11 @@ where resp: Result, types::ErrorCode>, ) -> wasmtime::Result<()> { let val = match resp { - Ok(resp) => Ok(self.table().delete(resp)?.try_into()?), + Ok(resp) => Ok(self.table.delete(resp)?.try_into()?), Err(e) => Err(e), }; - let resp = self.table().delete(id)?; + let resp = self.table.delete(id)?; // Giving the API doesn't return any error, it's probably // better to ignore the error than trap the guest, in case of // host timeout and dropped the receiver side of the channel. @@ -579,13 +563,10 @@ where } } -impl crate::bindings::http::types::HostIncomingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingResponse for WasiHttpCtxView<'_> { fn drop(&mut self, response: Resource) -> wasmtime::Result<()> { let _ = self - .table() + .table .delete(response) .context("[drop_incoming_response] deleting response")?; Ok(()) @@ -593,7 +574,7 @@ where fn status(&mut self, response: Resource) -> wasmtime::Result { let r = self - .table() + .table .get(&response) .context("[incoming_response_status] getting response")?; Ok(r.status) @@ -604,7 +585,7 @@ where response: Resource, ) -> wasmtime::Result> { let _ = self - .table() + .table .get(&response) .context("[incoming_response_headers] getting response")?; @@ -612,7 +593,7 @@ where &mut elem.downcast_mut::().unwrap().headers } - let id = self.table().push_child( + let id = self.table.push_child( HostFields::Ref { parent: response.rep(), get_fields, @@ -627,14 +608,14 @@ where &mut self, response: Resource, ) -> wasmtime::Result, ()>> { - let table = self.table(); - let r = table + let r = self + .table .get_mut(&response) .context("[incoming_response_consume] getting response")?; match r.body.take() { Some(body) => { - let id = self.table().push(body)?; + let id = self.table.push(body)?; Ok(Ok(id)) } @@ -643,13 +624,10 @@ where } } -impl crate::bindings::http::types::HostFutureTrailers for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFutureTrailers for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { let _ = self - .table() + .table .delete(id) .context("[drop future-trailers] deleting future-trailers")?; Ok(()) @@ -659,7 +637,7 @@ where &mut self, index: Resource, ) -> wasmtime::Result> { - wasmtime_wasi::p2::subscribe(self.table(), index) + wasmtime_wasi::p2::subscribe(self.table, index) } fn get( @@ -667,7 +645,7 @@ where id: Resource, ) -> wasmtime::Result>, types::ErrorCode>, ()>>> { - let trailers = self.table().get_mut(&id)?; + let trailers = self.table.get_mut(&id)?; match trailers { HostFutureTrailers::Waiting { .. } => return Ok(None), HostFutureTrailers::Consumed => return Ok(Some(Err(()))), @@ -685,27 +663,24 @@ where Err(e) => return Ok(Some(Ok(Err(e)))), }; - remove_forbidden_headers(self, &mut fields); + remove_forbidden_headers(self.hooks, &mut fields); - let ts = self.table().push(HostFields::Owned { fields })?; + let ts = self.table.push(HostFields::Owned { fields })?; Ok(Some(Ok(Ok(Some(ts))))) } } -impl crate::bindings::http::types::HostIncomingBody for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingBody for WasiHttpCtxView<'_> { fn stream( &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let body = self.table().get_mut(&id)?; + let body = self.table.get_mut(&id)?; if let Some(stream) = body.take_stream() { let stream: DynInputStream = Box::new(stream); - let stream = self.table().push_child(stream, &id)?; + let stream = self.table.push_child(stream, &id)?; return Ok(Ok(stream)); } @@ -716,28 +691,25 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - let body = self.table().delete(id)?; - let trailers = self.table().push(body.into_future_trailers())?; + let body = self.table.delete(id)?; + let trailers = self.table.push(body.into_future_trailers())?; Ok(trailers) } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl crate::bindings::http::types::HostOutgoingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingResponse for WasiHttpCtxView<'_> { fn new( &mut self, headers: Resource, ) -> wasmtime::Result> { - let fields = move_fields(self.table(), headers)?; + let fields = move_fields(self.table, headers)?; - let id = self.table().push(HostOutgoingResponse { + let id = self.table.push(HostOutgoingResponse { status: http::StatusCode::OK, headers: fields, body: None, @@ -750,9 +722,9 @@ where &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let buffer_chunks = self.outgoing_body_buffer_chunks(); - let chunk_size = self.outgoing_body_chunk_size(); - let resp = self.table().get_mut(&id)?; + let buffer_chunks = self.hooks.outgoing_body_buffer_chunks(); + let chunk_size = self.hooks.outgoing_body_chunk_size(); + let resp = self.table.get_mut(&id)?; if resp.body.is_some() { return Ok(Err(())); @@ -768,7 +740,7 @@ where resp.body.replace(body); - let id = self.table().push(host)?; + let id = self.table.push(host)?; Ok(Ok(id)) } @@ -777,7 +749,7 @@ where &mut self, id: Resource, ) -> wasmtime::Result { - Ok(self.table().get(&id)?.status.into()) + Ok(self.table.get(&id)?.status.into()) } fn set_status_code( @@ -785,7 +757,7 @@ where id: Resource, status: types::StatusCode, ) -> wasmtime::Result> { - let resp = self.table().get_mut(&id)?; + let resp = self.table.get_mut(&id)?; match http::StatusCode::from_u16(status) { Ok(status) => resp.status = status, @@ -800,14 +772,14 @@ where id: Resource, ) -> wasmtime::Result> { // Trap if the outgoing-response doesn't exist. - let _ = self.table().get(&id)?; + let _ = self.table.get(&id)?; fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { let resp = elem.downcast_mut::().unwrap(); &mut resp.headers } - Ok(self.table().push_child( + Ok(self.table.push_child( HostFields::Ref { parent: id.rep(), get_fields, @@ -817,17 +789,14 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl crate::bindings::http::types::HostFutureIncomingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFutureIncomingResponse for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } @@ -837,8 +806,8 @@ where ) -> wasmtime::Result< Option, types::ErrorCode>, ()>>, > { - let field_size_limit = self.ctx().field_size_limit; - let resp = self.table().get_mut(&id)?; + let field_size_limit = self.ctx.field_size_limit; + let resp = self.table.get_mut(&id)?; match resp { HostFutureIncomingResponse::Pending(_) => return Ok(None), @@ -861,9 +830,9 @@ where let (parts, body) = resp.resp.into_parts(); let mut headers = FieldMap::new(parts.headers, field_size_limit); - remove_forbidden_headers(self, &mut headers); + remove_forbidden_headers(self.hooks, &mut headers); - let resp = self.table().push(HostIncomingResponse { + let resp = self.table.push(HostIncomingResponse { status: parts.status.as_u16(), headers, body: Some({ @@ -883,21 +852,18 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - wasmtime_wasi::p2::subscribe(self.table(), id) + wasmtime_wasi::p2::subscribe(self.table, id) } } -impl crate::bindings::http::types::HostOutgoingBody for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingBody for WasiHttpCtxView<'_> { fn write( &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let body = self.table().get_mut(&id)?; + let body = self.table.get_mut(&id)?; if let Some(stream) = body.take_output_stream() { - let id = self.table().push_child(stream, &id)?; + let id = self.table.push_child(stream, &id)?; Ok(Ok(id)) } else { Ok(Err(())) @@ -908,11 +874,11 @@ where &mut self, id: Resource, ts: Option>, - ) -> crate::HttpResult<()> { - let body = self.table().delete(id)?; + ) -> HttpResult<()> { + let body = self.table.delete(id)?; let ts = if let Some(ts) = ts { - Some(move_fields(self.table(), ts)?) + Some(move_fields(self.table, ts)?) } else { None }; @@ -922,17 +888,14 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - self.table().delete(id)?.abort(); + self.table.delete(id)?.abort(); Ok(()) } } -impl crate::bindings::http::types::HostRequestOptions for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostRequestOptions for WasiHttpCtxView<'_> { fn new(&mut self) -> wasmtime::Result> { - let id = self.table().push(types::RequestOptions::default())?; + let id = self.table.push(types::RequestOptions::default())?; Ok(id) } @@ -940,11 +903,7 @@ where &mut self, opts: Resource, ) -> wasmtime::Result> { - let nanos = self - .table() - .get(&opts)? - .connect_timeout - .map(|d| d.as_nanos()); + let nanos = self.table.get(&opts)?.connect_timeout.map(|d| d.as_nanos()); if let Some(nanos) = nanos { Ok(Some(nanos.try_into()?)) @@ -958,8 +917,7 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.connect_timeout = - duration.map(std::time::Duration::from_nanos); + self.table.get_mut(&opts)?.connect_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } @@ -968,7 +926,7 @@ where opts: Resource, ) -> wasmtime::Result> { let nanos = self - .table() + .table .get(&opts)? .first_byte_timeout .map(|d| d.as_nanos()); @@ -985,7 +943,7 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.first_byte_timeout = + self.table.get_mut(&opts)?.first_byte_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } @@ -995,7 +953,7 @@ where opts: Resource, ) -> wasmtime::Result> { let nanos = self - .table() + .table .get(&opts)? .between_bytes_timeout .map(|d| d.as_nanos()); @@ -1012,13 +970,13 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.between_bytes_timeout = + self.table.get_mut(&opts)?.between_bytes_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(rep)?; + let _ = self.table.delete(rep)?; Ok(()) } } diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index 8e57d898257b..ffa2fea1baa6 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -58,7 +58,7 @@ impl HostWithStore for WasiHttp { .map_err(HttpError::trap)?; let (req, options) = req.into_http_with_getter(&mut store, io_task_result(io_result_rx), getter)?; - HttpResult::Ok(store.get().ctx.send_request( + HttpResult::Ok(store.get().hooks.send_request( req.map(|body| body.with_state(io_task_rx).boxed_unsync()), options.as_deref().copied(), Box::new(async { diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index ff7f397f7665..6179bea36d26 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -189,7 +189,7 @@ impl HostFields for WasiHttpCtxView<'_> { let mut fields = http::HeaderMap::default(); for (name, value) in entries { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let value = parse_header_value(&name, value)?; @@ -225,7 +225,7 @@ impl HostFields for WasiHttpCtxView<'_> { value: Vec, ) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let mut values = Vec::with_capacity(value.len()); @@ -244,7 +244,7 @@ impl HostFields for WasiHttpCtxView<'_> { fn delete(&mut self, fields: Resource, name: FieldName) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let fields = get_fields_mut(self.table, &fields)?; @@ -259,7 +259,7 @@ impl HostFields for WasiHttpCtxView<'_> { name: FieldName, ) -> HeaderResult> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let fields = get_fields_mut(self.table, &fields)?; @@ -278,7 +278,7 @@ impl HostFields for WasiHttpCtxView<'_> { value: FieldValue, ) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let value = parse_header_value(&name, value)?; diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index d8af50b68533..bce694348e59 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -22,7 +22,7 @@ pub use request::{Request, RequestOptions}; pub use response::Response; use crate::p3::bindings::http::types::ErrorCode; -use crate::types::DEFAULT_FORBIDDEN_HEADERS; +use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; use bindings::http::{client, types}; use bytes::Bytes; use core::ops::Deref; @@ -50,7 +50,7 @@ impl HasData for WasiHttp { } /// A trait which provides internal WASI HTTP state. -pub trait WasiHttpCtx: Send { +pub trait WasiHttpHooks: Send { /// Whether a given header should be considered forbidden and not allowed. fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { DEFAULT_FORBIDDEN_HEADERS.contains(name) @@ -150,21 +150,35 @@ pub trait WasiHttpCtx: Send { >; } -/// Default implementation of [WasiHttpCtx]. #[cfg(feature = "default-send-request")] -#[derive(Clone, Default)] -pub struct DefaultWasiHttpCtx; +impl<'a> Default for &'a mut dyn WasiHttpHooks { + fn default() -> Self { + let x: &mut [(); 0] = &mut []; + x + } +} + +#[doc(hidden)] +#[cfg(feature = "default-send-request")] +impl WasiHttpHooks for [(); 0] {} +/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has +/// the default behavior for `wasi:http`. #[cfg(feature = "default-send-request")] -impl WasiHttpCtx for DefaultWasiHttpCtx {} +pub fn default_hooks() -> &'static mut dyn WasiHttpHooks { + Default::default() +} /// View into [WasiHttpCtx] implementation and [ResourceTable]. pub struct WasiHttpCtxView<'a> { - /// Mutable reference to the WASI HTTP context. - pub ctx: &'a mut dyn WasiHttpCtx, + /// Mutable reference to the WASI HTTP hooks. + pub hooks: &'a mut dyn WasiHttpHooks, /// Mutable reference to table used to manage resources. pub table: &'a mut ResourceTable, + + /// Mutable reference to the WASI HTTP context. + pub ctx: &'a mut WasiHttpCtx, } /// A trait which provides internal WASI HTTP state. @@ -184,7 +198,7 @@ pub trait WasiHttpView: Send { /// ``` /// use wasmtime::{Engine, Result, Store, Config}; /// use wasmtime::component::{Linker, ResourceTable}; -/// use wasmtime_wasi_http::p3::{DefaultWasiHttpCtx, WasiHttpCtxView, WasiHttpView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p3::{WasiHttpCtxView, WasiHttpView}}; /// /// fn main() -> Result<()> { /// let mut config = Config::new(); @@ -207,7 +221,7 @@ pub trait WasiHttpView: Send { /// /// #[derive(Default)] /// struct MyState { -/// http: DefaultWasiHttpCtx, +/// http: WasiHttpCtx, /// table: ResourceTable, /// } /// @@ -216,6 +230,7 @@ pub trait WasiHttpView: Send { /// WasiHttpCtxView { /// ctx: &mut self.http, /// table: &mut self.table, +/// hooks: Default::default(), /// } /// } /// } diff --git a/crates/wasi-http/src/p3/request.rs b/crates/wasi-http/src/p3/request.rs index e0671fc0dba2..458919f56fd2 100644 --- a/crates/wasi-http/src/p3/request.rs +++ b/crates/wasi-http/src/p3/request.rs @@ -209,8 +209,8 @@ impl Request { }; let mut headers = Arc::unwrap_or_clone(headers); let mut store = store.as_context_mut(); - let WasiHttpCtxView { ctx, .. } = getter(store.data_mut()); - if ctx.set_host_header() { + let WasiHttpCtxView { hooks, .. } = getter(store.data_mut()); + if hooks.set_host_header() { let host = if let Some(authority) = authority.as_ref() { HeaderValue::try_from(authority.as_str()) .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))? @@ -220,8 +220,8 @@ impl Request { headers.insert(HOST, host); } let scheme = match scheme { - None => ctx.default_scheme().ok_or(ErrorCode::HttpProtocolError)?, - Some(scheme) if ctx.is_supported_scheme(&scheme) => scheme, + None => hooks.default_scheme().ok_or(ErrorCode::HttpProtocolError)?, + Some(scheme) if hooks.is_supported_scheme(&scheme) => scheme, Some(..) => return Err(ErrorCode::HttpProtocolError.into()), }; let mut uri = Uri::builder().scheme(scheme); @@ -478,7 +478,7 @@ pub async fn default_send_request( #[cfg(test)] mod tests { use super::*; - use crate::p3::DefaultWasiHttpCtx; + use crate::WasiHttpCtx; use core::future::Future; use core::pin::pin; use core::str::FromStr; @@ -491,7 +491,7 @@ mod tests { struct TestCtx { table: ResourceTable, wasi: WasiCtx, - http: DefaultWasiHttpCtx, + http: WasiHttpCtx, } impl TestCtx { @@ -499,7 +499,7 @@ mod tests { Self { table: ResourceTable::default(), wasi: WasiCtxBuilder::new().build(), - http: DefaultWasiHttpCtx, + http: Default::default(), } } } @@ -518,6 +518,7 @@ mod tests { WasiHttpCtxView { ctx: &mut self.http, table: &mut self.table, + hooks: crate::p3::default_hooks(), } } } diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/types.rs deleted file mode 100644 index d120e0555410..000000000000 --- a/crates/wasi-http/src/types.rs +++ /dev/null @@ -1,889 +0,0 @@ -//! Implements the base structure (i.e. [WasiHttpCtx]) that will provide the -//! implementation of the wasi-http API. - -use crate::{ - bindings::http::types::{self, ErrorCode, Method, Scheme}, - body::{HostIncomingBody, HyperIncomingBody, HyperOutgoingBody}, -}; -use bytes::Bytes; -use http::header::{HeaderMap, HeaderName, HeaderValue}; -use http_body_util::BodyExt; -use hyper::body::Body; -use std::any::Any; -use std::fmt; -use std::time::Duration; -use wasmtime::component::{Resource, ResourceTable}; -use wasmtime::{Result, bail}; -use wasmtime_wasi::p2::Pollable; -use wasmtime_wasi::runtime::AbortOnDropJoinHandle; - -#[cfg(feature = "default-send-request")] -use { - crate::io::TokioIo, - crate::{error::dns_error, hyper_request_error}, - tokio::net::TcpStream, - tokio::time::timeout, -}; - -/// Default maximum size for the contents of a fields resource. -/// -/// Typically, HTTP proxies limit headers to 8k. This number is higher than that -/// because it not only includes the wire-size of headers but it additionally -/// includes factors for the in-memory representation of `HeaderMap`. This is in -/// theory high enough that no one runs into it but low enough such that a -/// completely full `HeaderMap` doesn't break the bank in terms of memory -/// consumption. -const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024; - -/// Capture the state necessary for use in the wasi-http API implementation. -#[derive(Debug)] -pub struct WasiHttpCtx { - pub(crate) field_size_limit: usize, -} - -impl WasiHttpCtx { - /// Create a new context. - pub fn new() -> Self { - Self { - field_size_limit: DEFAULT_FIELD_SIZE_LIMIT, - } - } - - /// Set the maximum size for any fields resources created by this context. - /// - /// The limit specified here is roughly a byte limit for the size of the - /// in-memory representation of headers. This means that the limit needs to - /// be larger than the literal representation of headers on the wire to - /// account for in-memory Rust-side data structures representing the header - /// names/values/etc. - pub fn set_field_size_limit(&mut self, limit: usize) { - self.field_size_limit = limit; - } -} - -/// A trait which provides internal WASI HTTP state. -/// -/// # Example -/// -/// ``` -/// use wasmtime::component::ResourceTable; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -/// -/// struct MyState { -/// ctx: WasiCtx, -/// http_ctx: WasiHttpCtx, -/// table: ResourceTable, -/// } -/// -/// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// -/// impl MyState { -/// fn new() -> MyState { -/// let mut wasi = WasiCtx::builder(); -/// wasi.arg("./foo.wasm"); -/// wasi.arg("--help"); -/// wasi.env("FOO", "bar"); -/// -/// MyState { -/// ctx: wasi.build(), -/// table: ResourceTable::new(), -/// http_ctx: WasiHttpCtx::new(), -/// } -/// } -/// } -/// ``` -pub trait WasiHttpView { - /// Returns a mutable reference to the WASI HTTP context. - fn ctx(&mut self) -> &mut WasiHttpCtx; - - /// Returns the table used to manage resources. - fn table(&mut self) -> &mut ResourceTable; - - /// Create a new incoming request resource. - fn new_incoming_request( - &mut self, - scheme: Scheme, - req: hyper::Request, - ) -> wasmtime::Result> - where - B: Body + Send + 'static, - B::Error: Into, - Self: Sized, - { - let field_size_limit = self.ctx().field_size_limit; - let (parts, body) = req.into_parts(); - let body = body.map_err(Into::into).boxed_unsync(); - let body = HostIncomingBody::new( - body, - // TODO: this needs to be plumbed through - std::time::Duration::from_millis(600 * 1000), - field_size_limit, - ); - let incoming_req = - HostIncomingRequest::new(self, parts, scheme, Some(body), field_size_limit)?; - Ok(self.table().push(incoming_req)?) - } - - /// Create a new outgoing response resource. - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - let id = self.table().push(HostResponseOutparam { result })?; - Ok(id) - } - - /// Send an outgoing request. - #[cfg(feature = "default-send-request")] - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult { - Ok(default_send_request(request, config)) - } - - /// Send an outgoing request. - #[cfg(not(feature = "default-send-request"))] - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult; - - /// Whether a given header should be considered forbidden and not allowed. - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - DEFAULT_FORBIDDEN_HEADERS.contains(name) - } - - /// Number of distinct write calls to the outgoing body's output-stream - /// that the implementation will buffer. - /// Default: 1. - fn outgoing_body_buffer_chunks(&mut self) -> usize { - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS - } - - /// Maximum size allowed in a write call to the outgoing body's output-stream. - /// Default: 1024 * 1024. - fn outgoing_body_chunk_size(&mut self) -> usize { - DEFAULT_OUTGOING_BODY_CHUNK_SIZE - } -} - -/// The default value configured for [`WasiHttpView::outgoing_body_buffer_chunks`] in [`WasiHttpView`]. -pub const DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS: usize = 1; -/// The default value configured for [`WasiHttpView::outgoing_body_chunk_size`] in [`WasiHttpView`]. -pub const DEFAULT_OUTGOING_BODY_CHUNK_SIZE: usize = 1024 * 1024; - -impl WasiHttpView for &mut T { - fn ctx(&mut self) -> &mut WasiHttpCtx { - T::ctx(self) - } - - fn table(&mut self) -> &mut ResourceTable { - T::table(self) - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - T::new_response_outparam(self, result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult { - T::send_request(self, request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - T::is_forbidden_header(self, name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - T::outgoing_body_buffer_chunks(self) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - T::outgoing_body_chunk_size(self) - } -} - -impl WasiHttpView for Box { - fn ctx(&mut self) -> &mut WasiHttpCtx { - T::ctx(self) - } - - fn table(&mut self) -> &mut ResourceTable { - T::table(self) - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - T::new_response_outparam(self, result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult { - T::send_request(self, request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - T::is_forbidden_header(self, name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - T::outgoing_body_buffer_chunks(self) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - T::outgoing_body_chunk_size(self) - } -} - -/// A concrete structure that all generated `Host` traits are implemented for. -/// -/// This type serves as a small newtype wrapper to implement all of the `Host` -/// traits for `wasi:http`. This type is internally used and is only needed if -/// you're interacting with `add_to_linker` functions generated by bindings -/// themselves (or `add_to_linker_get_host`). -/// -/// This type is automatically used when using -/// [`add_to_linker_async`](crate::add_to_linker_async) -/// or -/// [`add_to_linker_sync`](crate::add_to_linker_sync) -/// and doesn't need to be manually configured. -#[repr(transparent)] -pub struct WasiHttpImpl(pub T); - -impl WasiHttpView for WasiHttpImpl { - fn ctx(&mut self) -> &mut WasiHttpCtx { - self.0.ctx() - } - - fn table(&mut self) -> &mut ResourceTable { - self.0.table() - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - self.0.new_response_outparam(result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult { - self.0.send_request(request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - self.0.is_forbidden_header(name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.0.outgoing_body_buffer_chunks() - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.0.outgoing_body_chunk_size() - } -} - -/// Set of [http::header::HeaderName], that are forbidden by default -/// for requests and responses originating in the guest. -pub const DEFAULT_FORBIDDEN_HEADERS: [http::header::HeaderName; 9] = [ - hyper::header::CONNECTION, - HeaderName::from_static("keep-alive"), - hyper::header::PROXY_AUTHENTICATE, - hyper::header::PROXY_AUTHORIZATION, - HeaderName::from_static("proxy-connection"), - hyper::header::TRANSFER_ENCODING, - hyper::header::UPGRADE, - hyper::header::HOST, - HeaderName::from_static("http2-settings"), -]; - -/// Removes forbidden headers from a [`FieldMap`]. -pub(crate) fn remove_forbidden_headers(view: &mut dyn WasiHttpView, headers: &mut FieldMap) { - let forbidden_keys = Vec::from_iter(headers.as_ref().keys().filter_map(|name| { - if view.is_forbidden_header(name) { - Some(name.clone()) - } else { - None - } - })); - - for name in forbidden_keys { - headers.remove_all(&name); - } -} - -/// Configuration for an outgoing request. -pub struct OutgoingRequestConfig { - /// Whether to use TLS for the request. - pub use_tls: bool, - /// The timeout for connecting. - pub connect_timeout: Duration, - /// The timeout until the first byte. - pub first_byte_timeout: Duration, - /// The timeout between chunks of a streaming body - pub between_bytes_timeout: Duration, -} - -/// The default implementation of how an outgoing request is sent. -/// -/// This implementation is used by the `wasi:http/outgoing-handler` interface -/// default implementation. -#[cfg(feature = "default-send-request")] -pub fn default_send_request( - request: hyper::Request, - config: OutgoingRequestConfig, -) -> HostFutureIncomingResponse { - let handle = wasmtime_wasi::runtime::spawn(async move { - Ok(default_send_request_handler(request, config).await) - }); - HostFutureIncomingResponse::pending(handle) -} - -/// The underlying implementation of how an outgoing request is sent. This should likely be spawned -/// in a task. -/// -/// This is called from [default_send_request] to actually send the request. -#[cfg(feature = "default-send-request")] -pub async fn default_send_request_handler( - mut request: hyper::Request, - OutgoingRequestConfig { - use_tls, - connect_timeout, - first_byte_timeout, - between_bytes_timeout, - }: OutgoingRequestConfig, -) -> Result { - let authority = if let Some(authority) = request.uri().authority() { - if authority.port().is_some() { - authority.to_string() - } else { - let port = if use_tls { 443 } else { 80 }; - format!("{}:{port}", authority.to_string()) - } - } else { - return Err(types::ErrorCode::HttpRequestUriInvalid); - }; - let tcp_stream = timeout(connect_timeout, TcpStream::connect(&authority)) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(|e| match e.kind() { - std::io::ErrorKind::AddrNotAvailable => { - dns_error("address not available".to_string(), 0) - } - - _ => { - if e.to_string() - .starts_with("failed to lookup address information") - { - dns_error("address not available".to_string(), 0) - } else { - types::ErrorCode::ConnectionRefused - } - } - })?; - - let (mut sender, worker) = if use_tls { - use rustls::pki_types::ServerName; - - // derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs - let root_cert_store = rustls::RootCertStore { - roots: webpki_roots::TLS_SERVER_ROOTS.into(), - }; - let config = rustls::ClientConfig::builder() - .with_root_certificates(root_cert_store) - .with_no_client_auth(); - let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config)); - let mut parts = authority.split(":"); - let host = parts.next().unwrap_or(&authority); - let domain = ServerName::try_from(host) - .map_err(|e| { - tracing::warn!("dns lookup error: {e:?}"); - dns_error("invalid dns name".to_string(), 0) - })? - .to_owned(); - let stream = connector.connect(domain, tcp_stream).await.map_err(|e| { - tracing::warn!("tls protocol error: {e:?}"); - types::ErrorCode::TlsProtocolError - })?; - let stream = TokioIo::new(stream); - - let (sender, conn) = timeout( - connect_timeout, - hyper::client::conn::http1::handshake(stream), - ) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(hyper_request_error)?; - - let worker = wasmtime_wasi::runtime::spawn(async move { - match conn.await { - Ok(()) => {} - // TODO: shouldn't throw away this error and ideally should - // surface somewhere. - Err(e) => tracing::warn!("dropping error {e}"), - } - }); - - (sender, worker) - } else { - let tcp_stream = TokioIo::new(tcp_stream); - let (sender, conn) = timeout( - connect_timeout, - // TODO: we should plumb the builder through the http context, and use it here - hyper::client::conn::http1::handshake(tcp_stream), - ) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(hyper_request_error)?; - - let worker = wasmtime_wasi::runtime::spawn(async move { - match conn.await { - Ok(()) => {} - // TODO: same as above, shouldn't throw this error away. - Err(e) => tracing::warn!("dropping error {e}"), - } - }); - - (sender, worker) - }; - - // at this point, the request contains the scheme and the authority, but - // the http packet should only include those if addressing a proxy, so - // remove them here, since SendRequest::send_request does not do it for us - *request.uri_mut() = http::Uri::builder() - .path_and_query( - request - .uri() - .path_and_query() - .map(|p| p.as_str()) - .unwrap_or("/"), - ) - .build() - .expect("comes from valid request"); - - let resp = timeout(first_byte_timeout, sender.send_request(request)) - .await - .map_err(|_| types::ErrorCode::ConnectionReadTimeout)? - .map_err(hyper_request_error)? - .map(|body| body.map_err(hyper_request_error).boxed_unsync()); - - Ok(IncomingResponse { - resp, - worker: Some(worker), - between_bytes_timeout, - }) -} - -impl From for types::Method { - fn from(method: http::Method) -> Self { - if method == http::Method::GET { - types::Method::Get - } else if method == hyper::Method::HEAD { - types::Method::Head - } else if method == hyper::Method::POST { - types::Method::Post - } else if method == hyper::Method::PUT { - types::Method::Put - } else if method == hyper::Method::DELETE { - types::Method::Delete - } else if method == hyper::Method::CONNECT { - types::Method::Connect - } else if method == hyper::Method::OPTIONS { - types::Method::Options - } else if method == hyper::Method::TRACE { - types::Method::Trace - } else if method == hyper::Method::PATCH { - types::Method::Patch - } else { - types::Method::Other(method.to_string()) - } - } -} - -impl TryInto for types::Method { - type Error = http::method::InvalidMethod; - - fn try_into(self) -> Result { - match self { - Method::Get => Ok(http::Method::GET), - Method::Head => Ok(http::Method::HEAD), - Method::Post => Ok(http::Method::POST), - Method::Put => Ok(http::Method::PUT), - Method::Delete => Ok(http::Method::DELETE), - Method::Connect => Ok(http::Method::CONNECT), - Method::Options => Ok(http::Method::OPTIONS), - Method::Trace => Ok(http::Method::TRACE), - Method::Patch => Ok(http::Method::PATCH), - Method::Other(s) => http::Method::from_bytes(s.as_bytes()), - } - } -} - -/// The concrete type behind a `wasi:http/types.incoming-request` resource. -#[derive(Debug)] -pub struct HostIncomingRequest { - pub(crate) method: http::method::Method, - pub(crate) uri: http::uri::Uri, - pub(crate) headers: FieldMap, - pub(crate) scheme: Scheme, - pub(crate) authority: String, - /// The body of the incoming request. - pub body: Option, -} - -impl HostIncomingRequest { - /// Create a new `HostIncomingRequest`. - pub fn new( - view: &mut dyn WasiHttpView, - parts: http::request::Parts, - scheme: Scheme, - body: Option, - field_size_limit: usize, - ) -> wasmtime::Result { - let authority = match parts.uri.authority() { - Some(authority) => authority.to_string(), - None => match parts.headers.get(http::header::HOST) { - Some(host) => host.to_str()?.to_string(), - None => bail!("invalid HTTP request missing authority in URI and host header"), - }, - }; - - let mut headers = FieldMap::new(parts.headers, field_size_limit); - remove_forbidden_headers(view, &mut headers); - - Ok(Self { - method: parts.method, - uri: parts.uri, - headers, - authority, - scheme, - body, - }) - } -} - -/// The concrete type behind a `wasi:http/types.response-outparam` resource. -pub struct HostResponseOutparam { - /// The sender for sending a response. - pub result: - tokio::sync::oneshot::Sender, types::ErrorCode>>, -} - -/// The concrete type behind a `wasi:http/types.outgoing-response` resource. -pub struct HostOutgoingResponse { - /// The status of the response. - pub status: http::StatusCode, - /// The headers of the response. - pub headers: FieldMap, - /// The body of the response. - pub body: Option, -} - -impl TryFrom for hyper::Response { - type Error = http::Error; - - fn try_from( - resp: HostOutgoingResponse, - ) -> Result, Self::Error> { - use http_body_util::Empty; - - let mut builder = hyper::Response::builder().status(resp.status); - - *builder.headers_mut().unwrap() = resp.headers.map; - - match resp.body { - Some(body) => builder.body(body), - None => builder.body( - Empty::::new() - .map_err(|_| unreachable!("Infallible error")) - .boxed_unsync(), - ), - } - } -} - -/// The concrete type behind a `wasi:http/types.outgoing-request` resource. -#[derive(Debug)] -pub struct HostOutgoingRequest { - /// The method of the request. - pub method: Method, - /// The scheme of the request. - pub scheme: Option, - /// The authority of the request. - pub authority: Option, - /// The path and query of the request. - pub path_with_query: Option, - /// The request headers. - pub headers: FieldMap, - /// The request body. - pub body: Option, -} - -/// The concrete type behind a `wasi:http/types.request-options` resource. -#[derive(Debug, Default)] -pub struct HostRequestOptions { - /// How long to wait for a connection to be established. - pub connect_timeout: Option, - /// How long to wait for the first byte of the response body. - pub first_byte_timeout: Option, - /// How long to wait between frames of the response body. - pub between_bytes_timeout: Option, -} - -/// The concrete type behind a `wasi:http/types.incoming-response` resource. -#[derive(Debug)] -pub struct HostIncomingResponse { - /// The response status - pub status: u16, - /// The response headers - pub headers: FieldMap, - /// The response body - pub body: Option, -} - -/// The concrete type behind a `wasi:http/types.fields` resource. -#[derive(Debug)] -pub enum HostFields { - /// A reference to the fields of a parent entry. - Ref { - /// The parent resource rep. - parent: u32, - - /// The function to get the fields from the parent. - // NOTE: there's not failure in the result here because we assume that HostFields will - // always be registered as a child of the entry with the `parent` id. This ensures that the - // entry will always exist while this `HostFields::Ref` entry exists in the table, thus we - // don't need to account for failure when fetching the fields ref from the parent. - get_fields: for<'a> fn(elem: &'a mut (dyn Any + 'static)) -> &'a mut FieldMap, - }, - /// An owned version of the fields. - Owned { - /// The fields themselves. - fields: FieldMap, - }, -} - -/// An owned version of `HostFields`. A wrapper on http `HeaderMap` that -/// keeps a running tally of memory consumed by header names and values. -#[derive(Debug, Clone)] -pub struct FieldMap { - map: HeaderMap, - limit: usize, - size: usize, -} - -/// Error given when a `FieldMap` has exceeded the size limit. -#[derive(Debug)] -pub struct FieldSizeLimitError { - /// The erroring `FieldMap` operation would require this content size - pub(crate) size: usize, - /// The limit set on `FieldMap` content size - pub(crate) limit: usize, -} -impl fmt::Display for FieldSizeLimitError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Field size limit {} exceeded: {}", self.limit, self.size) - } -} -impl std::error::Error for FieldSizeLimitError {} - -impl FieldMap { - /// Construct a `FieldMap` from a `HeaderMap` and a size limit. - /// - /// Construction with a `HeaderMap` which exceeds the size limit is - /// allowed, but subsequent operations to expand the resource use will - /// fail. - pub fn new(map: HeaderMap, limit: usize) -> Self { - let size = Self::content_size(&map); - Self { map, size, limit } - } - /// Construct an empty `FieldMap` - pub fn empty(limit: usize) -> Self { - Self { - map: HeaderMap::new(), - size: 0, - limit, - } - } - /// Get the `HeaderMap` out of the `FieldMap` - pub fn into_inner(self) -> HeaderMap { - self.map - } - /// Calculate the content size of a `HeaderMap`. This is a sum of the size - /// of all of the keys and all of the values. - pub(crate) fn content_size(map: &HeaderMap) -> usize { - let mut sum = 0; - for key in map.keys() { - sum += header_name_size(key); - } - for value in map.values() { - sum += header_value_size(value); - } - sum - } - /// Remove all values associated with a key in a map. - /// - /// Returns an empty list if the key is not already present within the map. - pub fn remove_all(&mut self, key: &HeaderName) -> Vec { - use http::header::Entry; - match self.map.try_entry(key) { - Ok(Entry::Vacant { .. }) | Err(_) => Vec::new(), - Ok(Entry::Occupied(e)) => { - let (name, value_drain) = e.remove_entry_mult(); - let mut removed = header_name_size(&name); - let values = value_drain.collect::>(); - for v in values.iter() { - removed += header_value_size(v); - } - self.size -= removed; - values - } - } - } - /// Add a value associated with a key to the map. - /// - /// If `key` is already present within the map then `value` is appended to - /// the list of values it already has. - pub fn append(&mut self, key: &HeaderName, value: HeaderValue) -> Result { - let key_size = header_name_size(key); - let val_size = header_value_size(&value); - let new_size = if !self.map.contains_key(key) { - self.size + key_size + val_size - } else { - self.size + val_size - }; - if new_size > self.limit { - bail!(FieldSizeLimitError { - limit: self.limit, - size: new_size - }) - } - self.size = new_size; - Ok(self.map.try_append(key, value)?) - } -} - -/// Returns the size, in accounting cost, to consider for `name`. -/// -/// This includes both the byte length of the `name` itself as well as the size -/// of the data structure itself as it'll reside within a `HeaderMap`. -fn header_name_size(name: &HeaderName) -> usize { - name.as_str().len() + size_of::() -} - -/// Same as `header_name_size`, but for values. -/// -/// This notably includes the size of `HeaderValue` itself to ensure that all -/// headers have a nonzero size as otherwise this would never limit addition of -/// an empty header value. -fn header_value_size(value: &HeaderValue) -> usize { - value.len() + size_of::() -} - -// We impl AsRef, but not AsMut, because any modifications of the -// underlying HeaderMap must account for changes in size -impl AsRef for FieldMap { - fn as_ref(&self) -> &HeaderMap { - &self.map - } -} - -/// A handle to a future incoming response. -pub type FutureIncomingResponseHandle = - AbortOnDropJoinHandle>>; - -/// A response that is in the process of being received. -#[derive(Debug)] -pub struct IncomingResponse { - /// The response itself. - pub resp: hyper::Response, - /// Optional worker task that continues to process the response. - pub worker: Option>, - /// The timeout between chunks of the response. - pub between_bytes_timeout: std::time::Duration, -} - -/// The concrete type behind a `wasi:http/types.future-incoming-response` resource. -#[derive(Debug)] -pub enum HostFutureIncomingResponse { - /// A pending response - Pending(FutureIncomingResponseHandle), - /// The response is ready. - /// - /// An outer error will trap while the inner error gets returned to the guest. - Ready(wasmtime::Result>), - /// The response has been consumed. - Consumed, -} - -impl HostFutureIncomingResponse { - /// Create a new `HostFutureIncomingResponse` that is pending on the provided task handle. - pub fn pending(handle: FutureIncomingResponseHandle) -> Self { - Self::Pending(handle) - } - - /// Create a new `HostFutureIncomingResponse` that is ready. - pub fn ready(result: wasmtime::Result>) -> Self { - Self::Ready(result) - } - - /// Returns `true` if the response is ready. - pub fn is_ready(&self) -> bool { - matches!(self, Self::Ready(_)) - } - - /// Unwrap the response, panicking if it is not ready. - pub fn unwrap_ready(self) -> wasmtime::Result> { - match self { - Self::Ready(res) => res, - Self::Pending(_) | Self::Consumed => { - panic!("unwrap_ready called on a pending HostFutureIncomingResponse") - } - } - } -} - -#[async_trait::async_trait] -impl Pollable for HostFutureIncomingResponse { - async fn ready(&mut self) { - if let Self::Pending(handle) = self { - *self = Self::Ready(handle.await); - } - } -} diff --git a/crates/wasi-http/tests/all/p2.rs b/crates/wasi-http/tests/all/p2.rs index 9d070789d027..7de12249ed4c 100644 --- a/crates/wasi-http/tests/all/p2.rs +++ b/crates/wasi-http/tests/all/p2.rs @@ -15,11 +15,12 @@ use wasmtime::{ }; use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView, p2::pipe::MemoryOutputPipe}; use wasmtime_wasi_http::{ - HttpResult, WasiHttpCtx, WasiHttpView, - bindings::http::types::{ErrorCode, Scheme}, - body::HyperOutgoingBody, + WasiHttpCtx, io::TokioIo, - types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, + p2::bindings::http::types::{ErrorCode, Scheme}, + p2::body::HyperOutgoingBody, + p2::types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, + p2::{HttpResult, WasiHttpCtxView, WasiHttpHooks, WasiHttpView}, }; type RequestSender = Arc< @@ -34,6 +35,10 @@ struct Ctx { http: WasiHttpCtx, stdout: MemoryOutputPipe, stderr: MemoryOutputPipe, + hooks: MyHttpHooks, +} + +struct MyHttpHooks { send_request: Option, rejected_authority: Option, } @@ -48,14 +53,16 @@ impl WasiView for Ctx { } impl WasiHttpView for Ctx { - fn ctx(&mut self) -> &mut WasiHttpCtx { - &mut self.http - } - - fn table(&mut self) -> &mut ResourceTable { - &mut self.table + fn http(&mut self) -> WasiHttpCtxView<'_> { + WasiHttpCtxView { + ctx: &mut self.http, + table: &mut self.table, + hooks: &mut self.hooks, + } } +} +impl WasiHttpHooks for MyHttpHooks { fn send_request( &mut self, request: hyper::Request, @@ -70,12 +77,14 @@ impl WasiHttpView for Ctx { if let Some(send_request) = self.send_request.clone() { Ok(send_request(request, config)) } else { - Ok(types::default_send_request(request, config)) + Ok(wasmtime_wasi_http::p2::default_send_request( + request, config, + )) } } fn is_forbidden_header(&mut self, name: &hyper::header::HeaderName) -> bool { - types::DEFAULT_FORBIDDEN_HEADERS.contains(name) + wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS.contains(name) || name.as_str() == "custom-forbidden-header" } } @@ -95,8 +104,10 @@ fn store(engine: &Engine, server: &Server) -> Store { http: WasiHttpCtx::new(), stderr, stdout, - send_request: None, - rejected_authority: None, + hooks: MyHttpHooks { + send_request: None, + rejected_authority: None, + }, }; Store::new(&engine, ctx) @@ -152,26 +163,30 @@ async fn run_wasi_http( http, stderr, stdout, - send_request, - rejected_authority, + hooks: MyHttpHooks { + send_request, + rejected_authority, + }, }; let mut store = Store::new(&engine, ctx); let mut linker = Linker::new(&engine); - wasmtime_wasi_http::add_to_linker_async(&mut linker).context("add crate to linker")?; + wasmtime_wasi_http::p2::add_to_linker_async(&mut linker).context("add crate to linker")?; let proxy = - wasmtime_wasi_http::bindings::Proxy::instantiate_async(&mut store, &component, &linker) + wasmtime_wasi_http::p2::bindings::Proxy::instantiate_async(&mut store, &component, &linker) .await .context("instantiate proxy")?; let req = store .data_mut() + .http() .new_incoming_request(Scheme::Http, req) .context("new incoming request")?; let (sender, receiver) = tokio::sync::oneshot::channel(); let out = store .data_mut() + .http() .new_response_outparam(sender) .context("new response outparam")?; @@ -298,7 +313,7 @@ async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> { let response = handle(request.into_parts().0).map(|resp| { Ok(IncomingResponse { resp: resp.map(|body| { - body.map_err(wasmtime_wasi_http::hyper_response_error) + body.map_err(wasmtime_wasi_http::p2::hyper_response_error) .boxed_unsync() }), worker: None, diff --git a/crates/wasi-http/tests/all/p2/async_.rs b/crates/wasi-http/tests/all/p2/async_.rs index f5a8674255e5..176a713cd67b 100644 --- a/crates/wasi-http/tests/all/p2/async_.rs +++ b/crates/wasi-http/tests/all/p2/async_.rs @@ -12,7 +12,7 @@ async fn run(path: &str, server: &Server) -> Result<()> { let mut store = store(&engine, server); let mut linker = Linker::new(&engine); wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; - wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?; let command = Command::instantiate_async(&mut store, &component, &linker).await?; let result = command.wasi_cli_run().call_run(&mut store).await?; result.map_err(|()| wasmtime::format_err!("run returned an error")) diff --git a/crates/wasi-http/tests/all/p2/sync.rs b/crates/wasi-http/tests/all/p2/sync.rs index 5ac57caa427e..a715c4ba31d2 100644 --- a/crates/wasi-http/tests/all/p2/sync.rs +++ b/crates/wasi-http/tests/all/p2/sync.rs @@ -12,7 +12,7 @@ fn run(path: &str, server: &Server) -> Result<()> { let mut store = store(&engine, server); let mut linker = Linker::new(&engine); wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?; - wasmtime_wasi_http::add_only_http_to_linker_sync(&mut linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_sync(&mut linker)?; let command = Command::instantiate(&mut store, &component, &linker)?; let result = command.wasi_cli_run().call_run(&mut store)?; result.map_err(|()| wasmtime::format_err!("run returned an error")) diff --git a/crates/wasi-http/tests/all/p3/mod.rs b/crates/wasi-http/tests/all/p3/mod.rs index 9d11a50ecd39..fd7f67e3073e 100644 --- a/crates/wasi-http/tests/all/p3/mod.rs +++ b/crates/wasi-http/tests/all/p3/mod.rs @@ -22,17 +22,17 @@ use wasmtime_wasi::{TrappableError, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiVi use wasmtime_wasi_http::p3::bindings::Service; use wasmtime_wasi_http::p3::bindings::http::types::ErrorCode; use wasmtime_wasi_http::p3::{ - self, Request, RequestOptions, WasiHttpCtx, WasiHttpCtxView, WasiHttpView, + self, Request, RequestOptions, WasiHttpCtxView, WasiHttpHooks, WasiHttpView, }; -use wasmtime_wasi_http::types::DEFAULT_FORBIDDEN_HEADERS; +use wasmtime_wasi_http::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; foreach_p3_http!(assert_test_exists); -struct TestHttpCtx { +struct TestHooks { request_body_tx: Option>>, } -impl WasiHttpCtx for TestHttpCtx { +impl WasiHttpHooks for TestHooks { fn is_forbidden_header(&mut self, name: &http::header::HeaderName) -> bool { name.as_str() == "custom-forbidden-header" || DEFAULT_FORBIDDEN_HEADERS.contains(name) } @@ -83,7 +83,8 @@ impl WasiHttpCtx for TestHttpCtx { struct Ctx { table: ResourceTable, wasi: WasiCtx, - http: TestHttpCtx, + http: WasiHttpCtx, + hooks: TestHooks, } impl Ctx { @@ -91,7 +92,8 @@ impl Ctx { Self { table: ResourceTable::default(), wasi: WasiCtxBuilder::new().inherit_stdio().build(), - http: TestHttpCtx { + http: WasiHttpCtx::new(), + hooks: TestHooks { request_body_tx: Some(request_body_tx), }, } @@ -112,6 +114,7 @@ impl WasiHttpView for Ctx { WasiHttpCtxView { ctx: &mut self.http, table: &mut self.table, + hooks: &mut self.hooks, } } } diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index ec65711df966..ee2d4a95db80 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -15,6 +15,9 @@ include = ["src/**/*", "README.md", "LICENSE", "witx/*", "wit/**/*", "tests/*"] [lints] workspace = true +[package.metadata.docs.rs] +all-features = true + [dependencies] wasmtime = { workspace = true, features = ["runtime", "std"] } wasmtime-wasi-io = { workspace = true, features = ["std"] } diff --git a/src/commands/run.rs b/src/commands/run.rs index 91aa2bf2435c..ffb4f458a09c 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -21,9 +21,7 @@ use wasmtime_wasi::{WasiCtxView, WasiView}; #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; #[cfg(feature = "wasi-http")] -use wasmtime_wasi_http::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, -}; +use wasmtime_wasi_http::WasiHttpCtx; #[cfg(feature = "wasi-keyvalue")] use wasmtime_wasi_keyvalue::{WasiKeyValue, WasiKeyValueCtx, WasiKeyValueCtxBuilder}; #[cfg(feature = "wasi-nn")] @@ -184,17 +182,7 @@ impl RunCommand { } } - let host = Host { - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_buffer_chunks: self - .run - .common - .wasi - .http_outgoing_body_buffer_chunks, - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_chunk_size: self.run.common.wasi.http_outgoing_body_chunk_size, - ..Default::default() - }; + let host = Host::default(); let mut store = Store::new(&engine, host); self.populate_with_wasi(&mut linker, &mut store, &main)?; @@ -1050,7 +1038,7 @@ impl RunCommand { bail!("Cannot enable wasi-http for core wasm modules"); } CliLinker::Component(linker) => { - wasmtime_wasi_http::add_only_http_to_linker_sync(linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; @@ -1197,6 +1185,10 @@ impl RunCommand { /// not compatible with `wasi-threads`. #[derive(Default, Clone)] pub struct Host { + limits: StoreLimits, + #[cfg(feature = "profiling")] + guest_profiler: Option>, + // Legacy wasip1 context using `wasi_common`, not set unless opted-in-to // with the CLI. legacy_p1_ctx: Option, @@ -1220,14 +1212,7 @@ pub struct Host { #[cfg(feature = "wasi-http")] wasi_http: Option>, #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_buffer_chunks: Option, - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_chunk_size: Option, - #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] - p3_http: crate::common::DefaultP3Ctx, - limits: StoreLimits, - #[cfg(feature = "profiling")] - guest_profiler: Option>, + wasi_http_hooks: crate::common::HttpHooks, #[cfg(feature = "wasi-config")] wasi_config: Option>, @@ -1258,33 +1243,27 @@ impl WasiView for Host { } #[cfg(feature = "wasi-http")] -impl wasmtime_wasi_http::types::WasiHttpView for Host { - fn ctx(&mut self) -> &mut WasiHttpCtx { +impl wasmtime_wasi_http::p2::WasiHttpView for Host { + fn http(&mut self) -> wasmtime_wasi_http::p2::WasiHttpCtxView<'_> { let ctx = self.wasi_http.as_mut().unwrap(); - Arc::get_mut(ctx).expect("wasmtime_wasi is not compatible with threads") - } - - fn table(&mut self) -> &mut wasmtime::component::ResourceTable { - WasiView::ctx(self).table - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.wasi_http_outgoing_body_buffer_chunks - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.wasi_http_outgoing_body_chunk_size - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_CHUNK_SIZE) + let ctx = Arc::get_mut(ctx).expect("wasmtime_wasi_http is not compatible with threads"); + wasmtime_wasi_http::p2::WasiHttpCtxView { + table: WasiView::ctx(unwrap_singlethread_context(&mut self.wasip1_ctx)).table, + ctx, + hooks: &mut self.wasi_http_hooks, + } } } #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] impl wasmtime_wasi_http::p3::WasiHttpView for Host { fn http(&mut self) -> wasmtime_wasi_http::p3::WasiHttpCtxView<'_> { + let ctx = self.wasi_http.as_mut().unwrap(); + let ctx = Arc::get_mut(ctx).expect("wasmtime_wasi_http is not compatible with threads"); wasmtime_wasi_http::p3::WasiHttpCtxView { table: WasiView::ctx(unwrap_singlethread_context(&mut self.wasip1_ctx)).table, - ctx: &mut self.p3_http, + ctx, + hooks: &mut self.wasi_http_hooks, } } } diff --git a/src/commands/serve.rs b/src/commands/serve.rs index cacf7110faa9..4badbe845734 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -1,4 +1,4 @@ -use crate::common::{Profile, RunCommon, RunTarget}; +use crate::common::{HttpHooks, Profile, RunCommon, RunTarget}; use bytes::Bytes; use clap::Parser; use futures::future::FutureExt; @@ -20,7 +20,7 @@ use std::{ }; use tokio::io::{self, AsyncWrite}; use tokio::sync::Notify; -use wasmtime::component::{Component, Linker, ResourceTable}; +use wasmtime::component::{Component, Linker}; use wasmtime::{ Engine, Result, Store, StoreContextMut, StoreLimits, UpdateDeadline, bail, error::Context as _, }; @@ -31,10 +31,7 @@ use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; use wasmtime_wasi_http::handler::p2::bindings as p2; use wasmtime_wasi_http::handler::{HandlerState, Proxy, ProxyHandler, ProxyPre, StoreBundle}; use wasmtime_wasi_http::io::TokioIo; -use wasmtime_wasi_http::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, - WasiHttpView, -}; +use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; @@ -51,11 +48,7 @@ struct Host { table: wasmtime::component::ResourceTable, ctx: WasiCtx, http: WasiHttpCtx, - http_outgoing_body_buffer_chunks: Option, - http_outgoing_body_chunk_size: Option, - - #[cfg(feature = "component-model-async")] - p3_http: crate::common::DefaultP3Ctx, + hooks: HttpHooks, limits: StoreLimits, @@ -81,22 +74,13 @@ impl WasiView for Host { } } -impl WasiHttpView for Host { - fn ctx(&mut self) -> &mut WasiHttpCtx { - &mut self.http - } - fn table(&mut self) -> &mut ResourceTable { - &mut self.table - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.http_outgoing_body_buffer_chunks - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.http_outgoing_body_chunk_size - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_CHUNK_SIZE) +impl wasmtime_wasi_http::p2::WasiHttpView for Host { + fn http(&mut self) -> wasmtime_wasi_http::p2::WasiHttpCtxView<'_> { + wasmtime_wasi_http::p2::WasiHttpCtxView { + ctx: &mut self.http, + table: &mut self.table, + hooks: &mut self.hooks, + } } } @@ -105,7 +89,8 @@ impl wasmtime_wasi_http::p3::WasiHttpView for Host { fn http(&mut self) -> wasmtime_wasi_http::p3::WasiHttpCtxView<'_> { wasmtime_wasi_http::p3::WasiHttpCtxView { table: &mut self.table, - ctx: &mut self.p3_http, + ctx: &mut self.http, + hooks: &mut self.hooks, } } } @@ -240,8 +225,7 @@ impl ServeCommand { table, ctx: builder.build(), http: self.run.wasi_http_ctx()?, - http_outgoing_body_buffer_chunks: self.run.common.wasi.http_outgoing_body_buffer_chunks, - http_outgoing_body_chunk_size: self.run.common.wasi.http_outgoing_body_chunk_size, + hooks: self.run.wasi_http_hooks(), limits: StoreLimits::default(), @@ -253,8 +237,6 @@ impl ServeCommand { wasi_keyvalue: None, #[cfg(feature = "profiling")] guest_profiler: None, - #[cfg(feature = "component-model-async")] - p3_http: crate::common::DefaultP3Ctx, }; if self.run.common.wasi.nn == Some(true) { @@ -337,13 +319,13 @@ impl ServeCommand { // uses. if cli == Some(true) { self.run.add_wasmtime_wasi_to_linker(linker)?; - wasmtime_wasi_http::add_only_http_to_linker_async(linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; } } else { - wasmtime_wasi_http::add_to_linker_async(linker)?; + wasmtime_wasi_http::p2::add_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; @@ -854,7 +836,7 @@ async fn handle_request( // `wasmtime::Error`. type P2Response = Result< - hyper::Response, + hyper::Response, p2::http::types::ErrorCode, >; type P3Response = hyper::Response>; @@ -895,8 +877,9 @@ async fn handle_request( let (req, out) = store.with(move |mut store| { let req = store .data_mut() + .http() .new_incoming_request(p2::http::types::Scheme::Http, req)?; - let out = store.data_mut().new_response_outparam(tx)?; + let out = store.data_mut().http().new_response_outparam(tx)?; wasmtime::error::Ok((req, out)) })?; diff --git a/src/common.rs b/src/common.rs index fcaf4c9bfc69..fa7fb061e775 100644 --- a/src/common.rs +++ b/src/common.rs @@ -341,6 +341,22 @@ impl RunCommon { Ok(http) } + #[cfg(feature = "wasi-http")] + pub fn wasi_http_hooks(&self) -> HttpHooks { + HttpHooks { + p2_outgoing_body_buffer_chunks: self + .common + .wasi + .http_outgoing_body_buffer_chunks + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS), + p2_outgoing_body_chunk_size: self + .common + .wasi + .http_outgoing_body_chunk_size + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE), + } + } + pub fn compute_preopen_sockets(&self) -> Result> { let mut listeners = vec![]; @@ -444,8 +460,34 @@ impl Profile { } } -#[derive(Default, Clone)] -#[cfg(all(feature = "wasi-http", feature = "component-model-async"))] -pub struct DefaultP3Ctx; -#[cfg(all(feature = "wasi-http", feature = "component-model-async"))] -impl wasmtime_wasi_http::p3::WasiHttpCtx for DefaultP3Ctx {} +#[derive(Copy, Clone, Debug)] +#[cfg(feature = "wasi-http")] +pub struct HttpHooks { + p2_outgoing_body_buffer_chunks: usize, + p2_outgoing_body_chunk_size: usize, +} + +#[cfg(feature = "wasi-http")] +impl Default for HttpHooks { + fn default() -> Self { + Self { + p2_outgoing_body_buffer_chunks: + wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, + p2_outgoing_body_chunk_size: wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE, + } + } +} + +#[cfg(feature = "wasi-http")] +impl wasmtime_wasi_http::p2::WasiHttpHooks for HttpHooks { + fn outgoing_body_buffer_chunks(&mut self) -> usize { + self.p2_outgoing_body_buffer_chunks + } + + fn outgoing_body_chunk_size(&mut self) -> usize { + self.p2_outgoing_body_chunk_size + } +} + +#[cfg(feature = "wasi-http")] +impl wasmtime_wasi_http::p3::WasiHttpHooks for HttpHooks {} From d085910099c7d933ccf818e6812a2beee022710b Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 10 Mar 2026 16:49:13 -0500 Subject: [PATCH 06/10] Refactor `wasi:http` headers' host representation (#12754) * Refactor `wasi:http` headers' host representation This commit is a follow-on/extension of #12748 and extends the changes made for WASIp2 headers in #12652 to the WASIp3 implementation as well. This is done through a number of refactorings to make the WASIp2 and WASIp3 implementations more similar in terms of how they represent headers. Changes here are: * `FieldMap` now has its own dedicated module at the crate root instead of intermingling with other WASIp2 types. * `FieldMap` is now internally-`Arc`'d and is cheaply clonable. `FieldMap` itself now tracks whether it's mutable or immutable (WASI semantics) and doesn't need different wrappers in WASIp2 and WASIp3. * Creation of an immutable `FieldMap` can be done without needing a size limit. Flagging a `FieldMap` as mutable, however, requires a size limit. * `FieldMap::set` was added to be a bit more efficient w.r.t. clones. * `FieldMapError` is a new error type that covers all of the possible error modes of operating with a `FieldMap`. Conversions from this to WASIp{2,3} `header-error` types are now implemented as well. * WASIp2 now flags `header-error` as a trappable-error-type, allowing the use of `?` in implementing header functions (like WASIp3). * Much of WASIp2's header implementation was refactored with `?`, moving methods around, shuffling where headers are made vs `FieldMap`, some minor idioms, etc. * WASIp3 no longer uses `MaybeMutable` for headers and instead uses `FieldMap` directly. cc #12674 * Clippy warnings --- .../src/bin/p2_cli_http_headers.rs | 64 +++- crates/wasi-http/src/field_map.rs | 357 ++++++++++++++++++ crates/wasi-http/src/lib.rs | 2 + crates/wasi-http/src/p2/bindings.rs | 3 +- crates/wasi-http/src/p2/body.rs | 18 +- crates/wasi-http/src/p2/error.rs | 78 +++- crates/wasi-http/src/p2/http_impl.rs | 2 +- crates/wasi-http/src/p2/mod.rs | 4 +- crates/wasi-http/src/p2/types.rs | 170 +-------- crates/wasi-http/src/p2/types_impl.rs | 346 +++++------------ crates/wasi-http/src/p3/bindings.rs | 2 +- crates/wasi-http/src/p3/body.rs | 16 +- crates/wasi-http/src/p3/host/handler.rs | 3 +- crates/wasi-http/src/p3/host/types.rs | 61 ++- crates/wasi-http/src/p3/mod.rs | 14 +- crates/wasi-http/src/p3/request.rs | 24 +- crates/wasi-http/src/p3/response.rs | 11 +- crates/wasi-http/tests/all/p2.rs | 8 +- tests/all/cli_tests.rs | 75 ++-- 19 files changed, 714 insertions(+), 544 deletions(-) create mode 100644 crates/wasi-http/src/field_map.rs diff --git a/crates/test-programs/src/bin/p2_cli_http_headers.rs b/crates/test-programs/src/bin/p2_cli_http_headers.rs index 1d0939264344..c5c21c1fe32a 100644 --- a/crates/test-programs/src/bin/p2_cli_http_headers.rs +++ b/crates/test-programs/src/bin/p2_cli_http_headers.rs @@ -1,31 +1,71 @@ -fn main() { - let fields = wasip2::http::types::Fields::new(); +use test_programs::p3::wasi as wasip3; +fn main() { match std::env::args().nth(1).as_deref() { - Some("append") => { + Some("p2-append") => { + let fields = wasip2::http::types::Fields::new(); + for i in 0.. { + if fields.append(&format!("a{i}"), b"a").is_err() { + break; + } + } + } + Some("p2-append-empty") => { + let fields = wasip2::http::types::Fields::new(); + for i in 0.. { + if fields.append(&format!("a{i}"), b"").is_err() { + break; + } + } + } + Some("p2-append-same") => { + let fields = wasip2::http::types::Fields::new(); + loop { + if fields.append("a", b"b").is_err() { + break; + } + } + } + Some("p2-append-same-empty") => { + let fields = wasip2::http::types::Fields::new(); + loop { + if fields.append("a", b"").is_err() { + break; + } + } + } + Some("p3-append") => { + let fields = wasip3::http::types::Fields::new(); for i in 0.. { if fields.append(&format!("a{i}"), b"a").is_err() { break; } } } - Some("append-empty") => { + Some("p3-append-empty") => { + let fields = wasip3::http::types::Fields::new(); for i in 0.. { if fields.append(&format!("a{i}"), b"").is_err() { break; } } } - Some("append-same") => loop { - if fields.append("a", b"b").is_err() { - break; + Some("p3-append-same") => { + let fields = wasip3::http::types::Fields::new(); + loop { + if fields.append("a", b"b").is_err() { + break; + } } - }, - Some("append-same-empty") => loop { - if fields.append("a", b"").is_err() { - break; + } + Some("p3-append-same-empty") => { + let fields = wasip3::http::types::Fields::new(); + loop { + if fields.append("a", b"").is_err() { + break; + } } - }, + } other => panic!("unknown test {other:?}"), } diff --git a/crates/wasi-http/src/field_map.rs b/crates/wasi-http/src/field_map.rs new file mode 100644 index 000000000000..556e87b6d496 --- /dev/null +++ b/crates/wasi-http/src/field_map.rs @@ -0,0 +1,357 @@ +use http::header::Entry; +use http::{HeaderMap, HeaderName, HeaderValue}; +use std::fmt; +use std::ops::Deref; +use std::sync::Arc; +use wasmtime::Result; + +/// A wrapper around [`http::HeaderMap`] which implements `wasi:http` semantics. +/// +/// The main differences from [`http::HeaderMap`] and this type are: +/// +/// * A slimmed down mutability API to just what `wasi:http` needs. +/// * `FieldMap` is cheaply clone-able with the internal `HeaderMap` being +/// behind an `Arc`. +/// * `FieldMap` is either immutable or mutable. Mutations on immutable values +/// are rejected with an error. Mutations on mutable values will never panic +/// unlike `HeaderMap` and additionally require a limit to be set on the size +/// of the map. +/// +/// Overall the intention is that this is a slim wrapper around +/// [`http::HeaderMap`] with slightly different ownership, panic, and error +/// semantics. +#[derive(Debug, Clone)] +pub struct FieldMap { + map: Arc, + limit: Limit, + size: usize, +} + +#[derive(Debug, Clone)] +enum Limit { + Mutable(usize), + Immutable, +} + +impl Default for FieldMap { + fn default() -> Self { + Self::new_immutable(HeaderMap::default()) + } +} + +impl FieldMap { + /// Creates a new immutable `FieldMap` from the provided + /// [`http::HeaderMap`]. + /// + /// The returned value cannot be mutated and attempting to mutate it will + /// return an error. + pub fn new_immutable(map: HeaderMap) -> Self { + let size = Self::content_size(&map); + Self { + map: Arc::new(map), + size, + limit: Limit::Immutable, + } + } + + /// Creates a new, empty, mutable `FieldMap`. + /// + /// Mutations are allowed on the returned value and up to `limit` bytes of + /// memory (roughly) may be consumed by this map. + pub fn new_mutable(limit: usize) -> Self { + Self { + map: Arc::new(HeaderMap::new()), + size: 0, + limit: Limit::Mutable(limit), + } + } + + /// Calculate the content size of a `HeaderMap`. This is a sum of the size + /// of all of the keys and all of the values. + pub(crate) fn content_size(map: &HeaderMap) -> usize { + let mut sum = 0; + for key in map.keys() { + sum += header_name_size(key); + } + for value in map.values() { + sum += header_value_size(value); + } + sum + } + + /// Sets the header `key` to the `values` list provided. + /// + /// Removes the previous value, if any. + /// + /// If `values` is empty then this removes the header `key`. + // + // FIXME(WebAssembly/WASI#900): is this the right behavior? + pub fn set(&mut self, key: HeaderName, values: Vec) -> Result<(), FieldMapError> { + let (map, limit, size) = self.mutable()?; + let key_size = header_name_size(&key); + let values_size = values.iter().map(header_value_size).sum::(); + let mut values = values.into_iter(); + let mut entry = match map.try_entry(key)? { + Entry::Vacant(e) => match values.next() { + Some(v) => { + update_size(size, limit, *size + values_size + key_size)?; + e.try_insert_entry(v)? + } + None => return Ok(()), + }, + Entry::Occupied(mut e) => { + let prev_values_size = e.iter().map(header_value_size).sum::(); + let _prev = match values.next() { + Some(v) => { + update_size(size, limit, *size - prev_values_size + values_size)?; + e.insert(v); + } + None => { + update_size(size, limit, *size - prev_values_size - key_size)?; + e.remove(); + return Ok(()); + } + }; + e + } + }; + for value in values { + entry.append(value); + } + Ok(()) + } + + /// Remove all values associated with a key in a map. + /// + /// Returns an empty list if the key is not already present within the map. + pub fn remove_all(&mut self, key: HeaderName) -> Result, FieldMapError> { + let (map, _limit, size) = self.mutable()?; + match map.try_entry(key)? { + Entry::Vacant { .. } => Ok(Vec::new()), + Entry::Occupied(e) => { + let (name, value_drain) = e.remove_entry_mult(); + let mut removed = header_name_size(&name); + let values = value_drain.collect::>(); + for v in values.iter() { + removed += header_value_size(v); + } + *size -= removed; + Ok(values) + } + } + } + + fn mutable(&mut self) -> Result<(&mut HeaderMap, usize, &mut usize), FieldMapError> { + match self.limit { + Limit::Immutable => Err(FieldMapError::Immutable), + Limit::Mutable(limit) => Ok((Arc::make_mut(&mut self.map), limit, &mut self.size)), + } + } + + /// Add a value associated with a key to the map. + /// + /// If `key` is already present within the map then `value` is appended to + /// the list of values it already has. + pub fn append(&mut self, key: HeaderName, value: HeaderValue) -> Result { + let (map, limit, size) = self.mutable()?; + let key_size = header_name_size(&key); + let val_size = header_value_size(&value); + let new_size = if !map.contains_key(&key) { + *size + key_size + val_size + } else { + *size + val_size + }; + update_size(size, limit, new_size)?; + let already_present = map.try_append(key, value)?; + self.size = new_size; + Ok(already_present) + } + + /// Flags this map as mutable, allowing mutations which can allocate as much + /// as `limit` memory, in bytes, for this entire map (roughly). + pub fn set_mutable(&mut self, limit: usize) { + self.limit = Limit::Mutable(limit); + } + + /// Flags this map as immutable, forbidding all further mutations. + pub fn set_immutable(&mut self) { + self.limit = Limit::Immutable; + } +} + +/// Returns the size, in accounting cost, to consider for `name`. +/// +/// This includes both the byte length of the `name` itself as well as the size +/// of the data structure itself as it'll reside within a `HeaderMap`. +fn header_name_size(name: &HeaderName) -> usize { + name.as_str().len() + size_of::() +} + +/// Same as `header_name_size`, but for values. +/// +/// This notably includes the size of `HeaderValue` itself to ensure that all +/// headers have a nonzero size as otherwise this would never limit addition of +/// an empty header value. +fn header_value_size(value: &HeaderValue) -> usize { + value.len() + size_of::() +} + +fn update_size(size: &mut usize, limit: usize, new: usize) -> Result<(), FieldMapError> { + if new > limit { + Err(FieldMapError::TotalSizeTooBig) + } else { + *size = new; + Ok(()) + } +} + +// Note that `DerefMut` is specifically omitted here to force all mutations +// through the `FieldMap` wrapper. +impl Deref for FieldMap { + type Target = HeaderMap; + + fn deref(&self) -> &HeaderMap { + &self.map + } +} + +impl From for HeaderMap { + fn from(map: FieldMap) -> Self { + Arc::unwrap_or_clone(map.map) + } +} + +/// Errors that can happen when mutating/operating on a [`FieldMap`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum FieldMapError { + /// A mutation was attempted when the map is not mutable. + Immutable, + /// The map has too many fields and is not allowed to add more. + /// + /// Note that this is currently a limitation inherited from + /// [`http::HeaderMap`]. + TooManyFields, + /// The map's total size, of keys and values, is too large. + TotalSizeTooBig, + /// An invalid header name was attempted to be added. + InvalidHeaderName, +} + +impl fmt::Display for FieldMapError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = match self { + FieldMapError::Immutable => "cannot mutate an immutable field map", + FieldMapError::TooManyFields => "too many fields in the field map", + FieldMapError::TotalSizeTooBig => "total size of fields exceeds limit", + FieldMapError::InvalidHeaderName => "invalid header name", + }; + f.write_str(s) + } +} + +impl std::error::Error for FieldMapError {} + +impl From for FieldMapError { + fn from(_: http::header::MaxSizeReached) -> Self { + Self::TooManyFields + } +} + +impl From for FieldMapError { + fn from(_: http::header::InvalidHeaderName) -> Self { + Self::InvalidHeaderName + } +} + +#[cfg(test)] +mod tests { + use super::{FieldMap, FieldMapError}; + + #[test] + fn test_immutable() { + let mut map = FieldMap::default(); + assert_eq!( + map.set("foo".parse().unwrap(), vec!["bar".parse().unwrap()]), + Err(FieldMapError::Immutable) + ); + assert_eq!( + map.append("foo".parse().unwrap(), "bar".parse().unwrap()), + Err(FieldMapError::Immutable) + ); + assert_eq!( + map.remove_all("foo".parse().unwrap()), + Err(FieldMapError::Immutable) + ); + } + + #[test] + fn test_limits() { + let mut map = FieldMap::new_mutable(100); + loop { + match map.append("foo".parse().unwrap(), "bar".parse().unwrap()) { + Ok(_) => {} + Err(FieldMapError::TotalSizeTooBig) => break, + Err(e) => panic!("unexpected error: {e}"), + } + } + + map = FieldMap::new_mutable(100); + for i in 0.. { + match map.set( + "foo".parse().unwrap(), + (0..i).map(|j| format!("bar{j}").parse().unwrap()).collect(), + ) { + Ok(_) => {} + Err(FieldMapError::TotalSizeTooBig) => break, + Err(e) => panic!("unexpected error: {e}"), + } + } + + map = FieldMap::new_mutable(100); + for i in 0.. { + match map.set( + format!("foo{i}").parse().unwrap(), + vec!["bar".parse().unwrap()], + ) { + Ok(_) => {} + Err(FieldMapError::TotalSizeTooBig) => break, + Err(e) => panic!("unexpected error: {e}"), + } + } + } + + #[test] + fn test_size() -> Result<(), FieldMapError> { + let mut map = FieldMap::new_mutable(2000); + let name: http::HeaderName = "foo".parse().unwrap(); + + map.append(name.clone(), "bar".parse().unwrap())?; + assert!(map.size > 0); + map.remove_all(name.clone())?; + assert_eq!(map.size, 0); + + map.set(name.clone(), vec!["bar".parse().unwrap()])?; + assert!(map.size > 0); + map.remove_all(name.clone())?; + assert_eq!(map.size, 0); + + map.set(name.clone(), vec![])?; + assert_eq!(map.size, 0); + map.set(name.clone(), vec!["bar".parse().unwrap()])?; + assert!(map.size > 0); + map.set(name.clone(), vec![])?; + assert_eq!(map.size, 0); + + map.set(name.clone(), vec!["bar".parse().unwrap()])?; + assert!(map.size > 0); + map.set( + name.clone(), + vec!["bar".parse().unwrap(), "baz".parse().unwrap()], + )?; + assert!(map.size > 0); + map.remove_all(name.clone())?; + assert_eq!(map.size, 0); + + Ok(()) + } +} diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs index cdf8648ac37a..b61c11c6466c 100644 --- a/crates/wasi-http/src/lib.rs +++ b/crates/wasi-http/src/lib.rs @@ -12,6 +12,7 @@ use http::{HeaderName, header}; mod ctx; +mod field_map; #[cfg(feature = "component-model-async")] pub mod handler; pub mod io; @@ -21,6 +22,7 @@ pub mod p2; pub mod p3; pub use ctx::*; +pub use field_map::*; /// Extract the `Content-Length` header value from a [`http::HeaderMap`], returning `None` if it's not /// present. This function will return `Err` if it's not possible to parse the `Content-Length` diff --git a/crates/wasi-http/src/p2/bindings.rs b/crates/wasi-http/src/p2/bindings.rs index e870ea033042..f3e8b3503576 100644 --- a/crates/wasi-http/src/p2/bindings.rs +++ b/crates/wasi-http/src/p2/bindings.rs @@ -26,11 +26,12 @@ mod generated { "wasi:http/types.response-outparam": types::HostResponseOutparam, "wasi:http/types.outgoing-request": types::HostOutgoingRequest, "wasi:http/types.incoming-request": types::HostIncomingRequest, - "wasi:http/types.fields": types::HostFields, + "wasi:http/types.fields": crate::FieldMap, "wasi:http/types.request-options": types::HostRequestOptions, }, trappable_error_type: { "wasi:http/types.error-code" => crate::p2::HttpError, + "wasi:http/types.header-error" => crate::p2::HeaderError, }, }); } diff --git a/crates/wasi-http/src/p2/body.rs b/crates/wasi-http/src/p2/body.rs index 5d8c929b4278..99dbb949e8f8 100644 --- a/crates/wasi-http/src/p2/body.rs +++ b/crates/wasi-http/src/p2/body.rs @@ -1,7 +1,7 @@ //! Implementation of the `wasi:http/types` interface's various body types. +use crate::FieldMap; use crate::p2::bindings::http::types; -use crate::p2::types::FieldMap; use bytes::Bytes; use http_body::{Body, Frame}; use http_body_util::BodyExt; @@ -25,7 +25,6 @@ pub type HyperOutgoingBody = UnsyncBoxBody; #[derive(Debug)] pub struct HostIncomingBody { body: IncomingBodyState, - field_size_limit: usize, /// An optional worker task to keep alive while this body is being read. /// This ensures that if the parent of this body is dropped before the body /// then the backing data behind this worker is kept alive. @@ -34,15 +33,10 @@ pub struct HostIncomingBody { impl HostIncomingBody { /// Create a new `HostIncomingBody` with the given `body` and a per-frame timeout - pub fn new( - body: HyperIncomingBody, - between_bytes_timeout: Duration, - field_size_limit: usize, - ) -> HostIncomingBody { + pub fn new(body: HyperIncomingBody, between_bytes_timeout: Duration) -> HostIncomingBody { let body = BodyWithTimeout::new(body, between_bytes_timeout); HostIncomingBody { body: IncomingBodyState::Start(body), - field_size_limit, worker: None, } } @@ -325,7 +319,7 @@ pub enum HostFutureTrailers { /// /// Note that `Ok(None)` means that there were no trailers for this request /// while `Ok(Some(_))` means that trailers were found in the request. - Done(Result, types::ErrorCode>), + Done(Result, types::ErrorCode>), /// Trailers have been consumed by `future-trailers.get`. Consumed, @@ -347,7 +341,7 @@ impl Pollable for HostFutureTrailers { // Trailers were read for us and here they are, so store the // result. Ok(StreamEnd::Trailers(Some(t))) => { - *self = Self::Done(Ok(Some(FieldMap::new(t, body.field_size_limit)))); + *self = Self::Done(Ok(Some(t))); } // The body wasn't fully read and was dropped before trailers // were reached. It's up to us now to complete the body. @@ -379,7 +373,7 @@ impl Pollable for HostFutureTrailers { // If this frame is a data frame ignore it as we're only // interested in trailers. if let Ok(header_map) = frame.into_trailers() { - break Ok(Some(FieldMap::new(header_map, body.field_size_limit))); + break Ok(Some(header_map)); } } } @@ -529,7 +523,7 @@ impl HostOutgoingBody { } let message = if let Some(ts) = trailers { - FinishMessage::Trailers(ts.into_inner()) + FinishMessage::Trailers(ts.into()) } else { FinishMessage::Finished }; diff --git a/crates/wasi-http/src/p2/error.rs b/crates/wasi-http/src/p2/error.rs index 9bc1ebd6f057..7dae4ee30f46 100644 --- a/crates/wasi-http/src/p2/error.rs +++ b/crates/wasi-http/src/p2/error.rs @@ -1,4 +1,5 @@ -use crate::p2::bindings::http::types::ErrorCode; +use crate::FieldMapError; +use crate::p2::bindings::http::types::{self, ErrorCode}; use std::error::Error; use std::fmt; use wasmtime::component::ResourceTableError; @@ -58,6 +59,81 @@ impl fmt::Display for HttpError { impl Error for HttpError {} +/// A [`Result`] type where the error type defaults to [`HeaderError`]. +pub type HeaderResult = Result; + +/// A `wasi:http`-specific error type used to represent either a trap or an +/// [`types::HeaderError`]. +/// +/// Modeled after [`TrappableError`](wasmtime_wasi::TrappableError). +#[repr(transparent)] +pub struct HeaderError { + err: wasmtime::Error, +} + +impl HeaderError { + /// Create a new `HeaderError` that represents a trap. + pub fn trap(err: impl Into) -> HeaderError { + HeaderError { err: err.into() } + } + + /// Downcast this error to an [`ErrorCode`]. + pub fn downcast(self) -> wasmtime::Result { + self.err.downcast() + } + + /// Downcast this error to a reference to an [`ErrorCode`] + pub fn downcast_ref(&self) -> Option<&types::HeaderError> { + self.err.downcast_ref() + } +} + +impl From for HeaderError { + fn from(error: types::HeaderError) -> Self { + Self { err: error.into() } + } +} + +impl From for HeaderError { + fn from(error: ResourceTableError) -> Self { + HeaderError::trap(error) + } +} + +impl From for HeaderError { + fn from(_: http::header::InvalidHeaderName) -> Self { + HeaderError::from(types::HeaderError::InvalidSyntax) + } +} + +impl From for HeaderError { + fn from(_: http::header::InvalidHeaderValue) -> Self { + HeaderError::from(types::HeaderError::InvalidSyntax) + } +} + +impl From for HeaderError { + fn from(err: FieldMapError) -> Self { + match err { + FieldMapError::Immutable => types::HeaderError::Immutable.into(), + FieldMapError::InvalidHeaderName => types::HeaderError::InvalidSyntax.into(), + FieldMapError::TooManyFields | FieldMapError::TotalSizeTooBig => HeaderError::trap(err), + } + } +} + +impl fmt::Debug for HeaderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.err.fmt(f) + } +} + +impl fmt::Display for HeaderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.err.fmt(f) + } +} + #[cfg(feature = "default-send-request")] pub(crate) fn dns_error(rcode: String, info_code: u16) -> ErrorCode { ErrorCode::DnsError(crate::p2::bindings::http::types::DnsErrorPayload { diff --git a/crates/wasi-http/src/p2/http_impl.rs b/crates/wasi-http/src/p2/http_impl.rs index 45b4312044d9..9510f7a44d6f 100644 --- a/crates/wasi-http/src/p2/http_impl.rs +++ b/crates/wasi-http/src/p2/http_impl.rs @@ -76,7 +76,7 @@ impl outgoing_handler::Host for WasiHttpCtxView<'_> { builder = builder.uri(uri.build().map_err(http_request_error)?); - for (k, v) in req.headers.as_ref().iter() { + for (k, v) in req.headers.iter() { builder = builder.header(k, v); } diff --git a/crates/wasi-http/src/p2/mod.rs b/crates/wasi-http/src/p2/mod.rs index 6f42d9260c26..72d0f7637fe6 100644 --- a/crates/wasi-http/src/p2/mod.rs +++ b/crates/wasi-http/src/p2/mod.rs @@ -233,9 +233,7 @@ pub mod bindings; pub mod body; pub mod types; -pub use self::error::{ - HttpError, HttpResult, http_request_error, hyper_request_error, hyper_response_error, -}; +pub use self::error::*; /// A trait which provides hooks into internal WASI HTTP operations. /// diff --git a/crates/wasi-http/src/p2/types.rs b/crates/wasi-http/src/p2/types.rs index d53290acd306..dd845ad7b2e5 100644 --- a/crates/wasi-http/src/p2/types.rs +++ b/crates/wasi-http/src/p2/types.rs @@ -1,17 +1,15 @@ //! Implements the base structure that will provide the implementation of the //! wasi-http API. +use crate::FieldMap; use crate::p2::{ WasiHttpCtxView, WasiHttpHooks, bindings::http::types::{self, ErrorCode, Method, Scheme}, body::{HostIncomingBody, HyperIncomingBody, HyperOutgoingBody}, }; use bytes::Bytes; -use http::header::{HeaderMap, HeaderName, HeaderValue}; use http_body_util::BodyExt; use hyper::body::Body; -use std::any::Any; -use std::fmt; use std::time::Duration; use wasmtime::component::Resource; use wasmtime::{Result, bail}; @@ -19,8 +17,11 @@ use wasmtime_wasi::p2::Pollable; use wasmtime_wasi::runtime::AbortOnDropJoinHandle; /// Removes forbidden headers from a [`FieldMap`]. -pub(crate) fn remove_forbidden_headers(hooks: &mut dyn WasiHttpHooks, headers: &mut FieldMap) { - let forbidden_keys = Vec::from_iter(headers.as_ref().keys().filter_map(|name| { +pub(crate) fn remove_forbidden_headers( + hooks: &mut dyn WasiHttpHooks, + headers: &mut http::HeaderMap, +) { + let forbidden_keys = Vec::from_iter(headers.keys().filter_map(|name| { if hooks.is_forbidden_header(name) { Some(name.clone()) } else { @@ -29,7 +30,7 @@ pub(crate) fn remove_forbidden_headers(hooks: &mut dyn WasiHttpHooks, headers: & })); for name in forbidden_keys { - headers.remove_all(&name); + headers.remove(&name); } } @@ -113,14 +114,12 @@ impl WasiHttpCtxView<'_> { B: Body + Send + 'static, B::Error: Into, { - let field_size_limit = self.ctx.field_size_limit; - let (parts, body) = req.into_parts(); + let (mut parts, body) = req.into_parts(); let body = body.map_err(Into::into).boxed_unsync(); let body = HostIncomingBody::new( body, // TODO: this needs to be plumbed through std::time::Duration::from_millis(600 * 1000), - field_size_limit, ); let authority = match parts.uri.authority() { Some(authority) => authority.to_string(), @@ -130,8 +129,8 @@ impl WasiHttpCtxView<'_> { }, }; - let mut headers = FieldMap::new(parts.headers, field_size_limit); - remove_forbidden_headers(self.hooks, &mut headers); + remove_forbidden_headers(self.hooks, &mut parts.headers); + let headers = FieldMap::new_immutable(parts.headers); let req = HostIncomingRequest { method: parts.method, @@ -185,7 +184,7 @@ impl TryFrom for hyper::Response { let mut builder = hyper::Response::builder().status(resp.status); - *builder.headers_mut().unwrap() = resp.headers.map; + *builder.headers_mut().unwrap() = resp.headers.into(); match resp.body { Some(body) => builder.body(body), @@ -237,153 +236,6 @@ pub struct HostIncomingResponse { pub body: Option, } -/// The concrete type behind a `wasi:http/types.fields` resource. -#[derive(Debug)] -pub enum HostFields { - /// A reference to the fields of a parent entry. - Ref { - /// The parent resource rep. - parent: u32, - - /// The function to get the fields from the parent. - // NOTE: there's not failure in the result here because we assume that HostFields will - // always be registered as a child of the entry with the `parent` id. This ensures that the - // entry will always exist while this `HostFields::Ref` entry exists in the table, thus we - // don't need to account for failure when fetching the fields ref from the parent. - get_fields: for<'a> fn(elem: &'a mut (dyn Any + 'static)) -> &'a mut FieldMap, - }, - /// An owned version of the fields. - Owned { - /// The fields themselves. - fields: FieldMap, - }, -} - -/// An owned version of `HostFields`. A wrapper on http `HeaderMap` that -/// keeps a running tally of memory consumed by header names and values. -#[derive(Debug, Clone)] -pub struct FieldMap { - map: HeaderMap, - limit: usize, - size: usize, -} - -/// Error given when a `FieldMap` has exceeded the size limit. -#[derive(Debug)] -pub struct FieldSizeLimitError { - /// The erroring `FieldMap` operation would require this content size - pub(crate) size: usize, - /// The limit set on `FieldMap` content size - pub(crate) limit: usize, -} -impl fmt::Display for FieldSizeLimitError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Field size limit {} exceeded: {}", self.limit, self.size) - } -} -impl std::error::Error for FieldSizeLimitError {} - -impl FieldMap { - /// Construct a `FieldMap` from a `HeaderMap` and a size limit. - /// - /// Construction with a `HeaderMap` which exceeds the size limit is - /// allowed, but subsequent operations to expand the resource use will - /// fail. - pub fn new(map: HeaderMap, limit: usize) -> Self { - let size = Self::content_size(&map); - Self { map, size, limit } - } - /// Construct an empty `FieldMap` - pub fn empty(limit: usize) -> Self { - Self { - map: HeaderMap::new(), - size: 0, - limit, - } - } - /// Get the `HeaderMap` out of the `FieldMap` - pub fn into_inner(self) -> HeaderMap { - self.map - } - /// Calculate the content size of a `HeaderMap`. This is a sum of the size - /// of all of the keys and all of the values. - pub(crate) fn content_size(map: &HeaderMap) -> usize { - let mut sum = 0; - for key in map.keys() { - sum += header_name_size(key); - } - for value in map.values() { - sum += header_value_size(value); - } - sum - } - /// Remove all values associated with a key in a map. - /// - /// Returns an empty list if the key is not already present within the map. - pub fn remove_all(&mut self, key: &HeaderName) -> Vec { - use http::header::Entry; - match self.map.try_entry(key) { - Ok(Entry::Vacant { .. }) | Err(_) => Vec::new(), - Ok(Entry::Occupied(e)) => { - let (name, value_drain) = e.remove_entry_mult(); - let mut removed = header_name_size(&name); - let values = value_drain.collect::>(); - for v in values.iter() { - removed += header_value_size(v); - } - self.size -= removed; - values - } - } - } - /// Add a value associated with a key to the map. - /// - /// If `key` is already present within the map then `value` is appended to - /// the list of values it already has. - pub fn append(&mut self, key: &HeaderName, value: HeaderValue) -> Result { - let key_size = header_name_size(key); - let val_size = header_value_size(&value); - let new_size = if !self.map.contains_key(key) { - self.size + key_size + val_size - } else { - self.size + val_size - }; - if new_size > self.limit { - bail!(FieldSizeLimitError { - limit: self.limit, - size: new_size - }) - } - self.size = new_size; - Ok(self.map.try_append(key, value)?) - } -} - -/// Returns the size, in accounting cost, to consider for `name`. -/// -/// This includes both the byte length of the `name` itself as well as the size -/// of the data structure itself as it'll reside within a `HeaderMap`. -fn header_name_size(name: &HeaderName) -> usize { - name.as_str().len() + size_of::() -} - -/// Same as `header_name_size`, but for values. -/// -/// This notably includes the size of `HeaderValue` itself to ensure that all -/// headers have a nonzero size as otherwise this would never limit addition of -/// an empty header value. -fn header_value_size(value: &HeaderValue) -> usize { - value.len() + size_of::() -} - -// We impl AsRef, but not AsMut, because any modifications of the -// underlying HeaderMap must account for changes in size -impl AsRef for FieldMap { - fn as_ref(&self) -> &HeaderMap { - &self.map - } -} - /// A handle to a future incoming response. pub type FutureIncomingResponseHandle = AbortOnDropJoinHandle>>; diff --git a/crates/wasi-http/src/p2/types_impl.rs b/crates/wasi-http/src/p2/types_impl.rs index 3bf7e377cc07..e403784a7b52 100644 --- a/crates/wasi-http/src/p2/types_impl.rs +++ b/crates/wasi-http/src/p2/types_impl.rs @@ -1,18 +1,17 @@ //! Implementation for the `wasi:http/types` interface. +use crate::FieldMap; use crate::get_content_length; -use crate::p2::bindings::http::types::{self, Headers, Method, Scheme, StatusCode, Trailers}; +use crate::p2::bindings::http::types::{self, Method, Scheme, StatusCode, Trailers}; use crate::p2::body::{HostFutureTrailers, HostIncomingBody, HostOutgoingBody, StreamContext}; use crate::p2::types::{ - FieldMap, FieldSizeLimitError, HostFields, HostFutureIncomingResponse, HostIncomingRequest, - HostIncomingResponse, HostOutgoingRequest, HostOutgoingResponse, HostResponseOutparam, - remove_forbidden_headers, + HostFutureIncomingResponse, HostIncomingRequest, HostIncomingResponse, HostOutgoingRequest, + HostOutgoingResponse, HostResponseOutparam, remove_forbidden_headers, }; -use crate::p2::{HttpError, HttpResult, WasiHttpCtxView}; -use std::any::Any; +use crate::p2::{HeaderError, HeaderResult, HttpError, HttpResult, WasiHttpCtxView}; +use http::{HeaderName, HeaderValue}; use std::str::FromStr; -use wasmtime::bail; -use wasmtime::component::{Resource, ResourceTable, ResourceTableError}; +use wasmtime::component::Resource; use wasmtime::{error::Context as _, format_err}; use wasmtime_wasi::p2::{DynInputStream, DynOutputStream, DynPollable}; @@ -21,6 +20,10 @@ impl types::Host for WasiHttpCtxView<'_> { err.downcast() } + fn convert_header_error(&mut self, err: HeaderError) -> wasmtime::Result { + err.downcast() + } + fn http_error_code( &mut self, err: wasmtime::component::Resource, @@ -30,129 +33,52 @@ impl types::Host for WasiHttpCtxView<'_> { } } -/// Take ownership of the underlying [`FieldMap`] associated with this fields resource. If the -/// fields resource references another fields, the returned [`FieldMap`] will be cloned. -fn move_fields( - table: &mut ResourceTable, - id: Resource, -) -> Result { - match table.delete(id)? { - HostFields::Ref { parent, get_fields } => { - let entry = table.get_any_mut(parent)?; - Ok(get_fields(entry).clone()) - } - - HostFields::Owned { fields } => Ok(fields), - } -} - -fn get_fields<'a>( - table: &'a mut ResourceTable, - id: &Resource, -) -> wasmtime::Result<&'a FieldMap> { - let fields = table.get(&id)?; - if let HostFields::Ref { parent, get_fields } = *fields { - let entry = table.get_any_mut(parent)?; - return Ok(get_fields(entry)); - } - - match table.get_mut(&id)? { - HostFields::Owned { fields } => Ok(fields), - // NB: ideally the `if let` above would go here instead. That makes - // the borrow-checker unhappy. Unclear why. If you, dear reader, can - // refactor this to remove the `unreachable!` please do. - HostFields::Ref { .. } => unreachable!(), - } -} - -fn get_fields_mut<'a>( - table: &'a mut ResourceTable, - id: &Resource, -) -> wasmtime::Result> { - match table.get_mut(&id)? { - HostFields::Owned { fields } => Ok(Ok(fields)), - HostFields::Ref { .. } => Ok(Err(types::HeaderError::Immutable)), - } -} - impl types::HostFields for WasiHttpCtxView<'_> { - fn new(&mut self) -> wasmtime::Result> { + fn new(&mut self) -> wasmtime::Result> { let limit = self.ctx.field_size_limit; let id = self .table - .push(HostFields::Owned { - fields: FieldMap::empty(limit), - }) + .push(FieldMap::new_mutable(limit)) .context("[new_fields] pushing fields")?; Ok(id) } - fn from_list( - &mut self, - entries: Vec<(String, Vec)>, - ) -> wasmtime::Result, types::HeaderError>> { - let mut fields = hyper::HeaderMap::new(); + fn from_list(&mut self, entries: Vec<(String, Vec)>) -> HeaderResult> { + let mut fields = FieldMap::new_mutable(self.ctx.field_size_limit); for (header, value) in entries { - let header = match hyper::header::HeaderName::from_bytes(header.as_bytes()) { - Ok(header) => header, - Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), - }; - + let header = HeaderName::from_bytes(header.as_bytes())?; if self.hooks.is_forbidden_header(&header) { - return Ok(Err(types::HeaderError::Forbidden)); + return Err(types::HeaderError::Forbidden.into()); } - - let value = match hyper::header::HeaderValue::from_bytes(&value) { - Ok(value) => value, - Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), - }; - - fields.append(header, value); - } - - let size = FieldMap::content_size(&fields); - if size > self.ctx.field_size_limit { - bail!(FieldSizeLimitError { - size, - limit: self.ctx.field_size_limit, - }); + let value = HeaderValue::from_bytes(&value)?; + fields.append(header, value)?; } - let fields = FieldMap::new(fields, self.ctx.field_size_limit); - let id = self - .table - .push(HostFields::Owned { fields }) - .context("[new_fields] pushing fields")?; - Ok(Ok(id)) + Ok(self.table.push(fields)?) } - fn drop(&mut self, fields: Resource) -> wasmtime::Result<()> { + fn drop(&mut self, fields: Resource) -> wasmtime::Result<()> { self.table .delete(fields) .context("[drop_fields] deleting fields")?; Ok(()) } - fn get( - &mut self, - fields: Resource, - name: String, - ) -> wasmtime::Result>> { - let fields = get_fields(self.table, &fields).context("[fields_get] getting fields")?; + fn get(&mut self, fields: Resource, name: String) -> wasmtime::Result>> { + let fields = self.table.get(&fields)?; - let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) { + let header = match HeaderName::from_bytes(name.as_bytes()) { Ok(header) => header, Err(_) => return Ok(vec![]), }; - if !fields.as_ref().contains_key(&header) { + if !fields.contains_key(&header) { return Ok(vec![]); } let res = fields - .as_ref() .get_all(&header) .into_iter() .map(|val| val.as_bytes().to_owned()) @@ -160,121 +86,81 @@ impl types::HostFields for WasiHttpCtxView<'_> { Ok(res) } - fn has(&mut self, fields: Resource, name: String) -> wasmtime::Result { - let fields = get_fields(self.table, &fields).context("[fields_get] getting fields")?; + fn has(&mut self, fields: Resource, name: String) -> wasmtime::Result { + let fields = self.table.get(&fields)?; - match hyper::header::HeaderName::from_bytes(name.as_bytes()) { - Ok(header) => Ok(fields.as_ref().contains_key(&header)), + match HeaderName::from_bytes(name.as_bytes()) { + Ok(header) => Ok(fields.contains_key(&header)), Err(_) => Ok(false), } } fn set( &mut self, - fields: Resource, + fields: Resource, name: String, byte_values: Vec>, - ) -> wasmtime::Result> { - let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) { - Ok(header) => header, - Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), - }; + ) -> HeaderResult<()> { + let header = HeaderName::from_bytes(name.as_bytes())?; if self.hooks.is_forbidden_header(&header) { - return Ok(Err(types::HeaderError::Forbidden)); + return Err(types::HeaderError::Forbidden.into()); } let mut values = Vec::with_capacity(byte_values.len()); for value in byte_values { - match hyper::header::HeaderValue::from_bytes(&value) { - Ok(value) => values.push(value), - Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), - } + values.push(HeaderValue::from_bytes(&value)?); } - match get_fields_mut(self.table, &fields).context("[fields_set] getting mutable fields")? { - Ok(fields) => { - fields.remove_all(&header); - for value in values { - fields.append(&header, value)?; - } - Ok(Ok(())) - } - Err(e) => Ok(Err(e)), - } + let fields = self.table.get_mut(&fields)?; + fields.set(header, values)?; + Ok(()) } - fn delete( - &mut self, - fields: Resource, - name: String, - ) -> wasmtime::Result> { - let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) { - Ok(header) => header, - Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), - }; + fn delete(&mut self, fields: Resource, name: String) -> HeaderResult<()> { + let header = HeaderName::from_bytes(name.as_bytes())?; if self.hooks.is_forbidden_header(&header) { - return Ok(Err(types::HeaderError::Forbidden)); + return Err(types::HeaderError::Forbidden.into()); } - Ok(get_fields_mut(self.table, &fields)?.map(|fields| { - fields.remove_all(&header); - })) + let fields = self.table.get_mut(&fields)?; + fields.remove_all(header)?; + Ok(()) } fn append( &mut self, - fields: Resource, + fields: Resource, name: String, value: Vec, - ) -> wasmtime::Result> { - let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) { - Ok(header) => header, - Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), - }; + ) -> HeaderResult<()> { + let header = HeaderName::from_bytes(name.as_bytes())?; if self.hooks.is_forbidden_header(&header) { - return Ok(Err(types::HeaderError::Forbidden)); + return Err(types::HeaderError::Forbidden.into()); } - let value = match hyper::header::HeaderValue::from_bytes(&value) { - Ok(value) => value, - Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), - }; + let value = HeaderValue::from_bytes(&value)?; - match get_fields_mut(self.table, &fields) - .context("[fields_append] getting mutable fields")? - { - Ok(fields) => { - fields.append(&header, value)?; - Ok(Ok(())) - } - Err(e) => Ok(Err(e)), - } + let fields = self.table.get_mut(&fields)?; + fields.append(header, value)?; + Ok(()) } - fn entries( - &mut self, - fields: Resource, - ) -> wasmtime::Result)>> { - Ok(get_fields(self.table, &fields)? - .as_ref() + fn entries(&mut self, fields: Resource) -> wasmtime::Result)>> { + Ok(self + .table + .get(&fields)? .iter() .map(|(name, value)| (name.as_str().to_owned(), value.as_bytes().to_owned())) .collect()) } - fn clone(&mut self, fields: Resource) -> wasmtime::Result> { - let fields = get_fields(self.table, &fields) - .context("[fields_clone] getting fields")? - .clone(); - - let id = self - .table - .push(HostFields::Owned { fields }) - .context("[fields_clone] pushing fields")?; - + fn clone(&mut self, fields: Resource) -> wasmtime::Result> { + let mut fields = self.table.get(&fields)?.clone(); + fields.set_mutable(self.ctx.field_size_limit); + let id = self.table.push(fields)?; Ok(id) } } @@ -306,22 +192,9 @@ impl types::HostIncomingRequest for WasiHttpCtxView<'_> { fn headers( &mut self, id: Resource, - ) -> wasmtime::Result> { - let _ = self.table.get(&id)?; - - fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { - &mut elem.downcast_mut::().unwrap().headers - } - - let headers = self.table.push_child( - HostFields::Ref { - parent: id.rep(), - get_fields, - }, - &id, - )?; - - Ok(headers) + ) -> wasmtime::Result> { + let req = self.table.get(&id)?; + Ok(self.table.push(req.headers.clone())?) } fn consume( @@ -348,9 +221,10 @@ impl types::HostIncomingRequest for WasiHttpCtxView<'_> { impl types::HostOutgoingRequest for WasiHttpCtxView<'_> { fn new( &mut self, - headers: Resource, + headers: Resource, ) -> wasmtime::Result> { - let headers = move_fields(self.table, headers)?; + let mut headers = self.table.delete(headers)?; + headers.set_immutable(); self.table .push(HostOutgoingRequest { @@ -379,7 +253,7 @@ impl types::HostOutgoingRequest for WasiHttpCtxView<'_> { return Ok(Err(())); } - let size = match get_content_length(req.headers.as_ref()) { + let size = match get_content_length(&req.headers) { Ok(size) => size, Err(..) => return Ok(Err(())), }; @@ -504,27 +378,9 @@ impl types::HostOutgoingRequest for WasiHttpCtxView<'_> { fn headers( &mut self, request: wasmtime::component::Resource, - ) -> wasmtime::Result> { - let _ = self - .table - .get(&request) - .context("[outgoing_request_headers] getting request")?; - - fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { - &mut elem - .downcast_mut::() - .unwrap() - .headers - } - - let id = self.table.push_child( - HostFields::Ref { - parent: request.rep(), - get_fields, - }, - &request, - )?; - + ) -> wasmtime::Result> { + let req = self.table.get(&request)?; + let id = self.table.push(req.headers.clone())?; Ok(id) } } @@ -557,7 +413,7 @@ impl types::HostResponseOutparam for WasiHttpCtxView<'_> { &mut self, _id: Resource, _status: u16, - _headers: Resource, + _headers: Resource, ) -> HttpResult<()> { Err(HttpError::trap(format_err!("not implemented"))) } @@ -583,24 +439,9 @@ impl types::HostIncomingResponse for WasiHttpCtxView<'_> { fn headers( &mut self, response: Resource, - ) -> wasmtime::Result> { - let _ = self - .table - .get(&response) - .context("[incoming_response_headers] getting response")?; - - fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { - &mut elem.downcast_mut::().unwrap().headers - } - - let id = self.table.push_child( - HostFields::Ref { - parent: response.rep(), - get_fields, - }, - &response, - )?; - + ) -> wasmtime::Result> { + let resp = self.table.get(&response)?; + let id = self.table.push(resp.headers.clone())?; Ok(id) } @@ -665,7 +506,7 @@ impl types::HostFutureTrailers for WasiHttpCtxView<'_> { remove_forbidden_headers(self.hooks, &mut fields); - let ts = self.table.push(HostFields::Owned { fields })?; + let ts = self.table.push(FieldMap::new_immutable(fields))?; Ok(Some(Ok(Ok(Some(ts))))) } @@ -705,9 +546,10 @@ impl types::HostIncomingBody for WasiHttpCtxView<'_> { impl types::HostOutgoingResponse for WasiHttpCtxView<'_> { fn new( &mut self, - headers: Resource, + headers: Resource, ) -> wasmtime::Result> { - let fields = move_fields(self.table, headers)?; + let mut fields = self.table.delete(headers)?; + fields.set_immutable(); let id = self.table.push(HostOutgoingResponse { status: http::StatusCode::OK, @@ -730,7 +572,7 @@ impl types::HostOutgoingResponse for WasiHttpCtxView<'_> { return Ok(Err(())); } - let size = match get_content_length(resp.headers.as_ref()) { + let size = match get_content_length(&resp.headers) { Ok(size) => size, Err(..) => return Ok(Err(())), }; @@ -770,22 +612,9 @@ impl types::HostOutgoingResponse for WasiHttpCtxView<'_> { fn headers( &mut self, id: Resource, - ) -> wasmtime::Result> { - // Trap if the outgoing-response doesn't exist. - let _ = self.table.get(&id)?; - - fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { - let resp = elem.downcast_mut::().unwrap(); - &mut resp.headers - } - - Ok(self.table.push_child( - HostFields::Ref { - parent: id.rep(), - get_fields, - }, - &id, - )?) + ) -> wasmtime::Result> { + let resp = self.table.get(&id)?; + Ok(self.table.push(resp.headers.clone())?) } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { @@ -806,7 +635,6 @@ impl types::HostFutureIncomingResponse for WasiHttpCtxView<'_> { ) -> wasmtime::Result< Option, types::ErrorCode>, ()>>, > { - let field_size_limit = self.ctx.field_size_limit; let resp = self.table.get_mut(&id)?; match resp { @@ -827,17 +655,15 @@ impl types::HostFutureIncomingResponse for WasiHttpCtxView<'_> { Ok(Err(e)) => return Ok(Some(Ok(Err(e)))), }; - let (parts, body) = resp.resp.into_parts(); - - let mut headers = FieldMap::new(parts.headers, field_size_limit); - remove_forbidden_headers(self.hooks, &mut headers); + let (mut parts, body) = resp.resp.into_parts(); + remove_forbidden_headers(self.hooks, &mut parts.headers); + let headers = FieldMap::new_immutable(parts.headers); let resp = self.table.push(HostIncomingResponse { status: parts.status.as_u16(), headers, body: Some({ - let mut body = - HostIncomingBody::new(body, resp.between_bytes_timeout, field_size_limit); + let mut body = HostIncomingBody::new(body, resp.between_bytes_timeout); if let Some(worker) = resp.worker { body.retain_worker(worker); } @@ -878,7 +704,7 @@ impl types::HostOutgoingBody for WasiHttpCtxView<'_> { let body = self.table.delete(id)?; let ts = if let Some(ts) = ts { - Some(move_fields(self.table, ts)?) + Some(self.table.delete(ts)?) } else { None }; diff --git a/crates/wasi-http/src/p3/bindings.rs b/crates/wasi-http/src/p3/bindings.rs index 307d3ff3480a..2e7804ce72fe 100644 --- a/crates/wasi-http/src/p3/bindings.rs +++ b/crates/wasi-http/src/p3/bindings.rs @@ -30,7 +30,7 @@ mod generated { }); mod with { - pub type Fields = crate::p3::MaybeMutable; + pub type Fields = crate::FieldMap; pub type RequestOptions = crate::p3::MaybeMutable; } } diff --git a/crates/wasi-http/src/p3/body.rs b/crates/wasi-http/src/p3/body.rs index 79e27a4f4751..0d2da68dacd6 100644 --- a/crates/wasi-http/src/p3/body.rs +++ b/crates/wasi-http/src/p3/body.rs @@ -1,11 +1,11 @@ -use crate::p3::bindings::http::types::{ErrorCode, Fields, Trailers}; +use crate::FieldMap; +use crate::p3::bindings::http::types::{ErrorCode, Trailers}; use crate::p3::{WasiHttp, WasiHttpCtxView}; use bytes::Bytes; use core::iter; use core::num::NonZeroUsize; use core::pin::Pin; use core::task::{Context, Poll, ready}; -use http::HeaderMap; use http_body::Body as _; use http_body_util::combinators::UnsyncBoxBody; use std::any::{Any, TypeId}; @@ -259,7 +259,7 @@ impl StreamConsumer for UnlimitedGuestBodyConsumer { /// [http_body::Body] implementation for bodies originating in the guest. pub(crate) struct GuestBody { contents_rx: Option>>, - trailers_rx: Option>, ErrorCode>>>, + trailers_rx: Option>, ErrorCode>>>, content_length: Option, } @@ -364,7 +364,7 @@ impl http_body::Body for GuestBody { self.trailers_rx = None; match res { Ok(Ok(Some(trailers))) => Poll::Ready(Some(Ok(http_body::Frame::trailers( - Arc::unwrap_or_clone(trailers), + Arc::unwrap_or_clone(trailers).into(), )))), Ok(Ok(None)) => Poll::Ready(None), Ok(Err(err)) => Poll::Ready(Some(Err(err))), @@ -404,7 +404,7 @@ impl http_body::Body for GuestBody { /// [FutureConsumer] implementation for trailers originating in the guest. struct GuestTrailerConsumer { - tx: Option>, ErrorCode>>>, + tx: Option>, ErrorCode>>>, getter: fn(&mut T) -> WasiHttpCtxView<'_>, } @@ -520,9 +520,11 @@ where return Poll::Ready(Ok(StreamResult::Completed)); } Err(Ok(trailers)) => { - let trailers = (self.getter)(store.data_mut()) + let view = (self.getter)(store.data_mut()); + let trailers = FieldMap::new_immutable(trailers); + let trailers = view .table - .push(Fields::new_mutable(trailers)) + .push(trailers) .context("failed to push trailers to table")?; break 'result Ok(Some(trailers)); } diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index ffa2fea1baa6..ae49bfdfd842 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -1,3 +1,4 @@ +use crate::FieldMap; use crate::p3::bindings::http::client::{Host, HostWithStore}; use crate::p3::bindings::http::types::{ErrorCode, Request, Response}; use crate::p3::body::{Body, BodyExt as _}; @@ -97,7 +98,7 @@ impl HostWithStore for WasiHttp { }; let res = Response { status, - headers: Arc::new(headers), + headers: FieldMap::new_immutable(headers), body: Body::Host { body, result_tx: res_result_tx, diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index 6179bea36d26..aa48d265526c 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -1,3 +1,4 @@ +use crate::FieldMap; use crate::p3::bindings::clocks::monotonic_clock::Duration; use crate::p3::bindings::http::types::{ ErrorCode, FieldName, FieldValue, Fields, HeaderError, Headers, Host, HostFields, HostRequest, @@ -42,9 +43,15 @@ fn push_fields(table: &mut ResourceTable, fields: Fields) -> wasmtime::Result) -> wasmtime::Result { - table + let mut fields = table .delete(fields) - .context("failed to delete fields from table") + .context("failed to delete fields from table")?; + // When fields are passed by ownership to the host that flags them as + // immutable within `wasi:http`, and this semantically means that putting + // fields in a request, then getting them back out, will return an immutable + // view of the headers rather than mutable for example. + fields.set_immutable(); + Ok(fields) } fn get_request<'a>( @@ -179,24 +186,23 @@ impl FutureProducer for GuestBodyResultProducer { impl HostFields for WasiHttpCtxView<'_> { fn new(&mut self) -> wasmtime::Result> { - push_fields(self.table, Fields::new_mutable_default()) + push_fields(self.table, FieldMap::new_mutable(self.ctx.field_size_limit)) } fn from_list( &mut self, entries: Vec<(FieldName, FieldValue)>, ) -> HeaderResult> { - let mut fields = http::HeaderMap::default(); + let mut fields = FieldMap::new_mutable(self.ctx.field_size_limit); for (name, value) in entries { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let value = parse_header_value(&name, value)?; - fields.append(name, value); + fields.append(name, value)?; } - let fields = push_fields(self.table, Fields::new_mutable(fields)) - .map_err(crate::p3::HeaderError::trap)?; + let fields = push_fields(self.table, fields).map_err(crate::p3::HeaderError::trap)?; Ok(fields) } @@ -224,7 +230,7 @@ impl HostFields for WasiHttpCtxView<'_> { name: FieldName, value: Vec, ) -> HeaderResult<()> { - let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; + let name = name.parse().map_err(|_| HeaderError::InvalidSyntax)?; if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } @@ -233,23 +239,16 @@ impl HostFields for WasiHttpCtxView<'_> { let value = parse_header_value(&name, value)?; values.push(value); } - let fields = get_fields_mut(self.table, &fields)?; - let fields = fields.get_mut().ok_or(HeaderError::Immutable)?; - fields.remove(&name); - for value in values { - fields.append(&name, value); - } + get_fields_mut(self.table, &fields)?.set(name, values)?; Ok(()) } fn delete(&mut self, fields: Resource, name: FieldName) -> HeaderResult<()> { - let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; + let name = name.parse().map_err(|_| HeaderError::InvalidSyntax)?; if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } - let fields = get_fields_mut(self.table, &fields)?; - let fields = fields.get_mut().ok_or(HeaderError::Immutable)?; - fields.remove(&name); + get_fields_mut(self.table, &fields)?.remove_all(name)?; Ok(()) } @@ -262,12 +261,9 @@ impl HostFields for WasiHttpCtxView<'_> { if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } - let fields = get_fields_mut(self.table, &fields)?; - let fields = fields.get_mut().ok_or(HeaderError::Immutable)?; - let http::header::Entry::Occupied(entry) = fields.entry(name) else { - return Ok(Vec::default()); - }; - let (.., values) = entry.remove_entry_mult(); + let values = get_fields_mut(self.table, &fields)? + .remove_all(name)? + .into_iter(); Ok(values.map(|value| value.as_bytes().into()).collect()) } @@ -282,9 +278,7 @@ impl HostFields for WasiHttpCtxView<'_> { return Err(HeaderError::Forbidden.into()); } let value = parse_header_value(&name, value)?; - let fields = get_fields_mut(self.table, &fields)?; - let fields = fields.get_mut().ok_or(HeaderError::Immutable)?; - fields.append(name, value); + get_fields_mut(self.table, &fields)?.append(name, value)?; Ok(()) } @@ -301,8 +295,9 @@ impl HostFields for WasiHttpCtxView<'_> { } fn clone(&mut self, fields: Resource) -> wasmtime::Result> { - let fields = get_fields(self.table, &fields)?; - push_fields(self.table, Fields::new_mutable(Arc::clone(fields))) + let mut fields = get_fields(self.table, &fields)?.clone(); + fields.set_mutable(self.ctx.field_size_limit); + push_fields(self.table, fields) } fn drop(&mut self, fields: Resource) -> wasmtime::Result<()> { @@ -348,7 +343,7 @@ impl HostRequestWithStore for WasiHttp { scheme: None, authority: None, path_with_query: None, - headers: headers.into(), + headers, options: options.map(Into::into), body, }; @@ -496,7 +491,7 @@ impl HostRequest for WasiHttpCtxView<'_> { fn get_headers(&mut self, req: Resource) -> wasmtime::Result> { let Request { headers, .. } = get_request(self.table, &req)?; - push_fields(self.table, Fields::new_immutable(Arc::clone(headers))) + push_fields(self.table, headers.clone()) } } @@ -624,7 +619,7 @@ impl HostResponseWithStore for WasiHttp { let headers = delete_fields(table, headers)?; let res = Response { status: http::StatusCode::OK, - headers: headers.into(), + headers, body, }; let res = table @@ -687,7 +682,7 @@ impl HostResponse for WasiHttpCtxView<'_> { fn get_headers(&mut self, res: Resource) -> wasmtime::Result> { let Response { headers, .. } = get_response(self.table, &res)?; - push_fields(self.table, Fields::new_immutable(Arc::clone(headers))) + push_fields(self.table, headers.clone()) } } diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index bce694348e59..04e50b3a75a3 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -22,7 +22,7 @@ pub use request::{Request, RequestOptions}; pub use response::Response; use crate::p3::bindings::http::types::ErrorCode; -use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; +use crate::{DEFAULT_FORBIDDEN_HEADERS, FieldMapError, WasiHttpCtx}; use bindings::http::{client, types}; use bytes::Bytes; use core::ops::Deref; @@ -39,6 +39,18 @@ pub(crate) type HttpError = TrappableError; pub(crate) type HeaderResult = Result; pub(crate) type HeaderError = TrappableError; +impl From for HeaderError { + fn from(e: FieldMapError) -> Self { + match e { + FieldMapError::Immutable => types::HeaderError::Immutable.into(), + FieldMapError::InvalidHeaderName => types::HeaderError::InvalidSyntax.into(), + // FIXME(WebAssembly/WASI#889): these ideally would map to an error + // code instead of trapping. + FieldMapError::TooManyFields | FieldMapError::TotalSizeTooBig => HeaderError::trap(e), + } + } +} + pub(crate) type RequestOptionsResult = Result; pub(crate) type RequestOptionsError = TrappableError; diff --git a/crates/wasi-http/src/p3/request.rs b/crates/wasi-http/src/p3/request.rs index 458919f56fd2..aa3ed706abbb 100644 --- a/crates/wasi-http/src/p3/request.rs +++ b/crates/wasi-http/src/p3/request.rs @@ -1,12 +1,12 @@ -use crate::get_content_length; use crate::p3::bindings::http::types::ErrorCode; use crate::p3::body::{Body, BodyExt as _, GuestBody}; use crate::p3::{HttpError, HttpResult, WasiHttpCtxView, WasiHttpView}; +use crate::{FieldMap, get_content_length}; use bytes::Bytes; use core::time::Duration; use http::header::HOST; use http::uri::{Authority, PathAndQuery, Scheme}; -use http::{HeaderMap, HeaderValue, Method, Uri}; +use http::{HeaderValue, Method, Uri}; use http_body_util::BodyExt as _; use http_body_util::combinators::UnsyncBoxBody; use std::sync::Arc; @@ -36,7 +36,7 @@ pub struct Request { /// The path and query of the request. pub path_with_query: Option, /// The request headers. - pub headers: Arc, + pub headers: FieldMap, /// Request options. pub options: Option>, /// Request body. @@ -55,7 +55,7 @@ impl Request { scheme: Option, authority: Option, path_with_query: Option, - headers: impl Into>, + headers: impl Into, options: Option>, body: impl Into>, ) -> ( @@ -119,7 +119,7 @@ impl Request { scheme, authority, path_and_query, - headers, + FieldMap::new_immutable(headers), None, body.map_err(Into::into).boxed_unsync(), ) @@ -156,7 +156,7 @@ impl Request { scheme, authority, path_with_query, - headers, + mut headers, options, body, } = self; @@ -207,9 +207,9 @@ impl Request { } } }; - let mut headers = Arc::unwrap_or_clone(headers); let mut store = store.as_context_mut(); - let WasiHttpCtxView { hooks, .. } = getter(store.data_mut()); + let WasiHttpCtxView { hooks, ctx, .. } = getter(store.data_mut()); + headers.set_mutable(ctx.field_size_limit); if hooks.set_host_header() { let host = if let Some(authority) = authority.as_ref() { HeaderValue::try_from(authority.as_str()) @@ -217,7 +217,7 @@ impl Request { } else { HeaderValue::from_static("") }; - headers.insert(HOST, host); + headers.append(HOST, host).map_err(HttpError::trap)?; } let scheme = match scheme { None => hooks.default_scheme().ok_or(ErrorCode::HttpProtocolError)?, @@ -236,7 +236,7 @@ impl Request { ErrorCode::HttpRequestUriInvalid })?; let mut req = http::Request::builder(); - *req.headers_mut().unwrap() = headers; + *req.headers_mut().unwrap() = headers.into(); let req = req .method(method) .uri(uri) @@ -534,7 +534,7 @@ mod tests { scheme.clone(), Some(Authority::from_static("example.com")), Some(PathAndQuery::from_static("/path?query=1")), - HeaderMap::new(), + FieldMap::default(), None, Full::new(Bytes::from_static(b"body")) .map_err(|x| match x {}) @@ -570,7 +570,7 @@ mod tests { Some(Scheme::HTTP), Some(Authority::from_static("example.com")), None, // <-- should fail, must be Some(_) when authority is set - HeaderMap::new(), + FieldMap::default(), None, Empty::new().map_err(|x| match x {}).boxed_unsync(), ); diff --git a/crates/wasi-http/src/p3/response.rs b/crates/wasi-http/src/p3/response.rs index 9c057138373a..6776faf0523f 100644 --- a/crates/wasi-http/src/p3/response.rs +++ b/crates/wasi-http/src/p3/response.rs @@ -1,12 +1,11 @@ -use crate::get_content_length; use crate::p3::bindings::http::types::ErrorCode; use crate::p3::body::{Body, GuestBody}; use crate::p3::{WasiHttpCtxView, WasiHttpView}; +use crate::{FieldMap, get_content_length}; use bytes::Bytes; -use http::{HeaderMap, StatusCode}; +use http::StatusCode; use http_body_util::BodyExt as _; use http_body_util::combinators::UnsyncBoxBody; -use std::sync::Arc; use wasmtime::AsContextMut; use wasmtime::error::Context as _; @@ -15,7 +14,7 @@ pub struct Response { /// The status of the response. pub status: StatusCode, /// The headers of the response. - pub headers: Arc, + pub headers: FieldMap, /// Response body. pub(crate) body: Body, } @@ -31,7 +30,7 @@ impl TryFrom for http::Response { }: Response, ) -> Result { let mut res = http::Response::builder().status(status); - *res.headers_mut().unwrap() = Arc::unwrap_or_clone(headers); + *res.headers_mut().unwrap() = headers.into(); res.body(body) } } @@ -106,7 +105,7 @@ impl Response { let wasi_response = Response { status: parts.status, - headers: Arc::new(parts.headers), + headers: FieldMap::new_immutable(parts.headers), body: Body::Host { body: body.map_err(Into::into).boxed_unsync(), result_tx, diff --git a/crates/wasi-http/tests/all/p2.rs b/crates/wasi-http/tests/all/p2.rs index 7de12249ed4c..382cf0c03965 100644 --- a/crates/wasi-http/tests/all/p2.rs +++ b/crates/wasi-http/tests/all/p2.rs @@ -19,7 +19,7 @@ use wasmtime_wasi_http::{ io::TokioIo, p2::bindings::http::types::{ErrorCode, Scheme}, p2::body::HyperOutgoingBody, - p2::types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, + p2::types::{HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, p2::{HttpResult, WasiHttpCtxView, WasiHttpHooks, WasiHttpView}, }; @@ -620,10 +620,10 @@ async fn wasi_http_no_trap_on_early_drop() -> Result<()> { #[test_log::test(tokio::test)] async fn wasi_http_fields_limit_incoming_request() -> Result<()> { - use crate::p2::types::FieldSizeLimitError; use http::{HeaderName, HeaderValue, Request}; use http_body_util::combinators::BoxBody; use hyper::Error; + use wasmtime_wasi_http::FieldMapError; fn request_with_header_size(uri: &str, total: usize) -> Request> { let mut builder = hyper::Request::builder().uri(uri).method(http::Method::GET); @@ -684,7 +684,7 @@ async fn wasi_http_fields_limit_incoming_request() -> Result<()> { .await .err() .expect("new_fields exceeding the size limit"); - assert!(err.downcast_ref::().is_some()); + assert!(err.downcast_ref::().is_some()); let resp = run_wasi_http( test_programs_artifacts::P2_API_PROXY_COMPONENT, @@ -708,7 +708,7 @@ async fn wasi_http_fields_limit_incoming_request() -> Result<()> { .await .err() .expect("run_wasi_http should give error"); - assert!(err.downcast_ref::().is_some()); + assert!(err.downcast_ref::().is_some()); Ok(()) } diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index db87e1bca3fb..a3dad513e517 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -2312,44 +2312,59 @@ start a print 1234 #[test] fn p2_cli_http_headers() -> Result<()> { - for test in ["append", "append-empty", "append-same", "append-same-empty"] { + let td = tempfile::TempDir::new()?; + let cwasm = td.path().join("http_headers.cwasm"); + let cwasm = cwasm.to_str().unwrap(); + run_wasmtime(&["compile", P2_CLI_HTTP_HEADERS_COMPONENT, "-o", cwasm])?; + for wasi in ["p2", "p3"] { + for test in ["append", "append-empty", "append-same", "append-same-empty"] { + let err = run_wasmtime(&[ + "run", + "-Shttp,p3", + "-Smax-http-fields-size=1048576", + "--allow-precompiled", + cwasm, + &format!("{wasi}-{test}"), + ]) + .unwrap_err(); + assert!( + err.to_string() + .contains("total size of fields exceeds limit") + || err.to_string().contains("too many fields in the field map"), + "bad error message: {err:?}" + ); + + // gated by default too + let err = run_wasmtime(&[ + "run", + "-Shttp,p3", + "--allow-precompiled", + cwasm, + &format!("{wasi}-{test}"), + ]) + .unwrap_err(); + assert!( + err.to_string() + .contains("total size of fields exceeds limit"), + "bad error message: {err:?}" + ); + } + + // With an extremely large limit Wasmtime still shouldn't panic. let err = run_wasmtime(&[ "run", - "-Shttp", - "-Smax-http-fields-size=1048576", - P2_CLI_HTTP_HEADERS_COMPONENT, - test, + "-Shttp,p3", + &format!("-Smax-http-fields-size={}", 1 << 30), + "--allow-precompiled", + cwasm, + &format!("{wasi}-append"), ]) .unwrap_err(); assert!( - err.to_string() - .contains("Field size limit 1048576 exceeded") - || err.to_string().contains("max size reached"), - "bad error message: {err:?}" - ); - - // gated by default too - let err = - run_wasmtime(&["run", "-Shttp", P2_CLI_HTTP_HEADERS_COMPONENT, test]).unwrap_err(); - assert!( - err.to_string().contains("Field size limit"), + err.to_string().contains("too many fields in the field map"), "bad error message: {err:?}" ); } - - // With an extremely large limit Wasmtime still shouldn't panic. - let err = run_wasmtime(&[ - "run", - "-Shttp", - &format!("-Smax-http-fields-size={}", 1 << 30), - P2_CLI_HTTP_HEADERS_COMPONENT, - "append", - ]) - .unwrap_err(); - assert!( - err.to_string().contains("max size reached"), - "bad error message: {err:?}" - ); Ok(()) } From d144e0fdd3117801756d84bf45dfe63273d63770 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 11 Mar 2026 14:28:39 +0000 Subject: [PATCH 07/10] Set current thread before lowering stream/future reads (#12736) * Set thread before lowering stream/future reads * Restore thread * Make CurrentThread pub(crate) * fmt * Remove with_thread * Remove DS_Store * Add test * Check return values * Move string data --- .../src/runtime/component/concurrent.rs | 4 +- .../concurrent/futures_and_streams.rs | 76 ++++-- .../component-model/async/task-builtins.wast | 255 ++++++++++++++++++ 3 files changed, 314 insertions(+), 21 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index fd73e5622ec5..822e3fceb4e6 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -4311,7 +4311,7 @@ struct LiftResult { /// This exists to minimize table lookups and the necessity to pass stores around mutably /// for the common case of identifying the task to which a thread belongs. #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] -struct QualifiedThreadId { +pub(crate) struct QualifiedThreadId { task: TableId, thread: TableId, } @@ -4806,7 +4806,7 @@ impl ConcurrentInstanceState { } #[derive(Debug, Copy, Clone)] -enum CurrentThread { +pub(crate) enum CurrentThread { Guest(QualifiedThreadId), Host(TableId), None, diff --git a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs index 81e84275f904..fdc7f18ad33a 100644 --- a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs +++ b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs @@ -1,6 +1,6 @@ use super::table::{TableDebug, TableId}; use super::{Event, GlobalErrorContextRefCount, Waitable, WaitableCommon}; -use crate::component::concurrent::{ConcurrentState, WorkItem, tls}; +use crate::component::concurrent::{ConcurrentState, QualifiedThreadId, WorkItem, tls}; use crate::component::func::{self, LiftContext, LowerContext}; use crate::component::matching::InstanceType; use crate::component::types; @@ -143,6 +143,7 @@ fn get_mut_by_index_from( fn lower, U: 'static>( mut store: StoreContextMut, instance: Instance, + caller_thread: QualifiedThreadId, options: OptionsIndex, ty: TransmitIndex, address: usize, @@ -151,11 +152,21 @@ fn lower, U: 'static>( ) -> Result<()> { let count = buffer.remaining().len().min(count); - let lower = &mut if T::MAY_REQUIRE_REALLOC { - LowerContext::new + // If lowering may call realloc in the guest, then the guest may need + // to access its thread context, so we need to set the current thread before lowering + // and restore the old one afterward. + let (lower, old_thread) = if T::MAY_REQUIRE_REALLOC { + let old_thread = store.0.set_thread(caller_thread)?; + ( + &mut LowerContext::new(store.as_context_mut(), options, instance), + Some(old_thread), + ) } else { - LowerContext::new_without_realloc - }(store.as_context_mut(), options, instance); + ( + &mut LowerContext::new_without_realloc(store.as_context_mut(), options, instance), + None, + ) + }; if address % usize::try_from(T::ALIGN32)? != 0 { bail!("read pointer not aligned"); @@ -170,6 +181,10 @@ fn lower, U: 'static>( T::linear_store_list_to_memory(lower, ty, address, &buffer.remaining()[..count])?; } + if let Some(old_thread) = old_thread { + store.0.set_thread(old_thread)?; + } + buffer.skip(count); Ok(()) @@ -2196,7 +2211,8 @@ enum ReadState { /// The read end is owned by a guest task and a read is pending. GuestReady { ty: TransmitIndex, - caller: RuntimeComponentInstanceIndex, + caller_instance: RuntimeComponentInstanceIndex, + caller_thread: QualifiedThreadId, flat_abi: Option, instance: Instance, options: OptionsIndex, @@ -2930,7 +2946,8 @@ async fn write { let guest_offset = match guest_offset { Some(i) => i, @@ -2952,6 +2969,7 @@ async fn write( store.as_context_mut(), instance, + caller_thread, options, ty, address + (T::SIZE32 * guest_offset), @@ -3008,7 +3026,8 @@ async fn write, flat_abi: Option, - write_caller: RuntimeComponentInstanceIndex, + write_caller_instance: RuntimeComponentInstanceIndex, write_ty: TransmitIndex, write_options: OptionsIndex, write_address: usize, - read_caller: RuntimeComponentInstanceIndex, + read_caller_instance: RuntimeComponentInstanceIndex, + read_caller_thread: QualifiedThreadId, read_ty: TransmitIndex, read_options: OptionsIndex, read_address: usize, @@ -3198,7 +3218,9 @@ impl Instance { let payload = types[types[write_ty].ty].payload; - if write_caller == read_caller && !allow_intra_component_read_write(payload) { + if write_caller_instance == read_caller_instance + && !allow_intra_component_read_write(payload) + { bail!( "cannot read from and write to intra-component future with non-numeric payload" ) @@ -3228,6 +3250,10 @@ impl Instance { .transpose()?; if let Some(val) = val { + // Serializing the value may require calling the guest's realloc function, so we + // set the guest's thread context in case realloc requires it, and restore the original + // thread context after the copy is complete. + let old_thread = store.0.set_thread(read_caller_thread)?; let lower = &mut LowerContext::new(store.as_context_mut(), read_options, self); let types = lower.types; let ty = match types[types[read_ty].ty].payload { @@ -3240,10 +3266,11 @@ impl Instance { &ValRaw::u32(read_address.try_into()?), )?; val.store(lower, ty, ptr)?; + store.0.set_thread(old_thread)?; } } (TransmitIndex::Stream(write_ty), TransmitIndex::Stream(read_ty)) => { - if write_caller == read_caller + if write_caller_instance == read_caller_instance && !allow_intra_component_read_write(types[types[write_ty].ty].payload) { bail!( @@ -3284,7 +3311,7 @@ impl Instance { // SAFETY: Both `src` and `dst` have been validated // above. unsafe { - if write_caller == read_caller { + if write_caller_instance == read_caller_instance { // If the same instance owns both ends of // the stream, the source and destination // buffers might overlap. @@ -3326,6 +3353,10 @@ impl Instance { let id = TableId::::new(rep); log::trace!("copy values {values:?} for {id:?}"); + // Serializing the value may require calling the guest's realloc function, so we + // set the guest's thread context in case realloc requires it, and restore the original + // thread context after the copy is complete. + let old_thread = store.0.set_thread(read_caller_thread)?; let lower = &mut LowerContext::new(store.as_context_mut(), read_options, self); let ty = match lower.types[lower.types[read_ty].ty].payload { Some(ty) => ty, @@ -3348,6 +3379,7 @@ impl Instance { value.store(lower, ty, ptr)?; ptr += size } + store.0.set_thread(old_thread)?; } } _ => bail_bug!("mismatched transmit types in copy"), @@ -3469,7 +3501,8 @@ impl Instance { count: read_count, handle: read_handle, instance: read_instance, - caller: read_caller, + caller_instance: read_caller_instance, + caller_thread: read_caller_thread, } => { if flat_abi != read_flat_abi { bail_bug!("expected flat ABI calculations to be the same"); @@ -3515,7 +3548,8 @@ impl Instance { ty, options, address, - read_caller, + read_caller_instance, + read_caller_thread, read_ty, read_options, read_address, @@ -3557,7 +3591,8 @@ impl Instance { count: read_count - count, handle: read_handle, instance: read_instance, - caller: read_caller, + caller_instance: read_caller_instance, + caller_thread: read_caller_thread, }; } @@ -3634,7 +3669,7 @@ impl Instance { pub(super) fn guest_read( self, mut store: StoreContextMut, - caller: RuntimeComponentInstanceIndex, + caller_instance: RuntimeComponentInstanceIndex, ty: TransmitIndex, options: OptionsIndex, flat_abi: Option, @@ -3664,6 +3699,7 @@ impl Instance { *state = TransmitLocalState::Busy; let transmit_handle = TableId::::new(rep); let concurrent_state = store.0.concurrent_state_mut(); + let caller_thread = concurrent_state.current_guest_thread()?; let transmit_id = concurrent_state.get_mut(transmit_handle)?.state; let transmit = concurrent_state.get_mut(transmit_id)?; log::trace!( @@ -3694,7 +3730,8 @@ impl Instance { count, handle, instance: self, - caller, + caller_instance, + caller_thread, }; Ok::<_, crate::Error>(()) }; @@ -3737,7 +3774,8 @@ impl Instance { write_ty, write_options, write_address, - caller, + caller_instance, + caller_thread, ty, options, address, diff --git a/tests/misc_testsuite/component-model/async/task-builtins.wast b/tests/misc_testsuite/component-model/async/task-builtins.wast index 5476fb8222c9..86155a312e01 100644 --- a/tests/misc_testsuite/component-model/async/task-builtins.wast +++ b/tests/misc_testsuite/component-model/async/task-builtins.wast @@ -463,3 +463,258 @@ ) (assert_return (invoke "run")) + +;; Test when realloc is called to communicate stream/future values that various +;; intrinsics work and have their expected values. +(component + ;; Component that will drive the reader forward and write to the future/stream + (component $writer + (type $FT (future string)) + (type $ST (stream string)) + ;; The reader will provide runner functions for futures and streams separately + (import "reader" (instance $reader + (export "run-future" (func async (param "future" $FT) (result u32))) + (export "run-stream" (func async (param "stream" $ST) (result u32))) + )) + (core module $libc + (memory (export "memory") 1)) + (core instance $libc (instantiate $libc)) + (core func $run-reader-future (canon lower (func $reader "run-future") (memory $libc "memory") async)) + (core func $run-reader-stream (canon lower (func $reader "run-stream") (memory $libc "memory") async)) + (core module $m + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "task.return" (func $task.return (param i32))) + (import "" "run-reader-future" (func $run-reader-future (param i32 i32) (result i32))) + (import "" "run-reader-stream" (func $run-reader-stream (param i32 i32) (result i32))) + (import "" "memory" (memory 1)) + + (global $ws (mut i32) (i32.const 0)) + (global $fw (mut i32) (i32.const 0)) + (global $sw (mut i32) (i32.const 0)) + (global $state (mut i32) (i32.const 0)) + (global $future-subtask (mut i32) (i32.const 0)) + (global $stream-subtask (mut i32) (i32.const 0)) + (global $future-retp i32 (i32.const 0x60)) + (global $stream-retp i32 (i32.const 0x70)) + + (func (export "run") (result i32) + (local $ret i32) (local $ret64 i64) + (local $fr i32) (local $sr i32) + + ;; store address and length of string + (i32.store offset=0 (i32.const 40) (i32.const 0x100)) + (i32.store offset=4 (i32.const 40) (i32.const 2)) + + (local.set $ret64 (call $future.new)) + (local.set $fr (i32.wrap_i64 (local.get $ret64))) + (global.set $fw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret64 (call $stream.new)) + (local.set $sr (i32.wrap_i64 (local.get $ret64))) + (global.set $sw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + (local.set $ret (call $run-reader-future (local.get $fr) (global.get $future-retp))) + (global.set $future-subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (local.set $ret (call $future.write (global.get $fw) (i32.const 40))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + (local.set $ret (call $run-reader-stream (local.get $sr) (global.get $stream-retp))) + (global.set $stream-subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (local.set $ret (call $stream.write (global.get $sw) (i32.const 40) (i32.const 1))) + (if (i32.ne (i32.const 0x10 (; COMPLETED | 1<<4 ;)) (local.get $ret)) (then (unreachable))) + + ;; Create a waitable set and join both subtasks to wait for both to complete + (global.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $stream-subtask) (global.get $ws)) + (call $waitable.join (global.get $future-subtask) (global.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) + ) + + (global $future-completed (mut i32) (i32.const 0)) + (global $stream-completed (mut i32) (i32.const 0)) + + ;; Callback invoked when a subtask completes. Since we joined both subtasks to the + ;; same waitable set, this will be called once for each completion. We track which + ;; subtasks have completed and only return when both are done. + (func (export "run-cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.ne (local.get $event_code) (i32.const 1 (; SUBTASK ;))) (then (unreachable))) + (if (i32.ne (local.get $payload) (i32.const 2 (; RETURNED ;))) (then (unreachable))) + + ;; Track which subtask completed + (if (i32.eq (local.get $index) (global.get $future-subtask)) + (then + (if (i32.ne (i32.load (global.get $future-retp)) (i32.const 42)) (then (unreachable))) + (global.set $future-completed (i32.const 1))) + (else + (if (i32.eq (local.get $index) (global.get $stream-subtask)) + (then + (if (i32.ne (i32.load (global.get $stream-retp)) (i32.const 42)) (then (unreachable))) + (global.set $stream-completed (i32.const 1))) + (else unreachable)))) + + ;; If both completed, exit; otherwise keep waiting + (if (result i32) + (i32.and (i32.eq (global.get $future-completed) (i32.const 1)) + (i32.eq (global.get $stream-completed) (i32.const 1))) + (then + (call $task.return (i32.const 42)) + (i32.const 0 (; EXIT ;))) + (else + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))))) + ) + + (data (i32.const 0x100) "hi") + ) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async + (memory $libc "memory") (core func $future.write)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.write $ST async + (memory $libc "memory") (core func $stream.write)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon task.return (result u32) (memory $libc "memory") (core func $task.return)) + (canon context.set i32 0 (core func $context.set)) + + (core instance $M (instantiate $m (with "" (instance + (export "memory" (memory $libc "memory")) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "task.return" (func $task.return)) + (export "run-reader-future" (func $run-reader-future)) + (export "run-reader-stream" (func $run-reader-stream)) + )))) + + (func (export "run") async (result u32) + (canon lift (core func $M "run") (memory $libc "memory") + async (callback (func $M "run-cb"))) + ) + ) + + (component $reader + (core module $libc + (import "" "backpressure.inc" (func $backpressure.inc)) + (import "" "backpressure.dec" (func $backpressure.dec)) + (import "" "context.get" (func $context.get (result i32))) + (import "" "context.set" (func $context.set (param i32))) + + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + (if (i32.ne (local.get 0) (i32.const 0)) (then (unreachable))) + (if (i32.ne (local.get 1) (i32.const 0)) (then (unreachable))) + (if (i32.ne (local.get 2) (i32.const 1)) (then (unreachable))) + (if (i32.ne (local.get 3) (i32.const 2)) (then (unreachable))) + + call $context.get + i32.const 400 + i32.ne + if unreachable end + + i32.const 500 + call $context.set + + call $backpressure.inc + call $backpressure.dec + + i32.const 200 + ) + ) + + (core func $backpressure.inc (canon backpressure.inc)) + (core func $backpressure.dec (canon backpressure.dec)) + (core func $context.get (canon context.get i32 0)) + (core func $context.set (canon context.set i32 0)) + + (core instance $libc (instantiate $libc (with "" (instance + (export "backpressure.inc" (func $backpressure.inc)) + (export "backpressure.dec" (func $backpressure.dec)) + (export "context.get" (func $context.get)) + (export "context.set" (func $context.set)) + )))) + + (type $FT (future string)) + (type $ST (stream string)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT + (memory $libc "memory") (realloc (func $libc "realloc")) (core func $future.read)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST + (memory $libc "memory") (realloc (func $libc "realloc")) (core func $stream.read)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $libc "memory") (core func $waitable-set.wait)) + (canon task.return (result u32) (memory $libc "memory") (core func $task.return)) + + (core module $m + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "context.set" (func $context.set (param i32))) + (import "" "context.get" (func $context.get (result i32))) + (import "" "task.return" (func $task.return (param i32))) + (import "" "memory" (memory 1)) + + ;; Set context[0] to 400, then read the future, which should call realloc and set + ;; context[0] to 500, then check that we see that value. + (func (export "run-future") (param $fr i32) (result i32) + (local $ret i32) + + (call $context.set (i32.const 400)) + (local.set $ret (call $future.read (local.get $fr) (i32.const 40))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) (then (unreachable))) + (if (i32.ne (call $context.get) (i32.const 500)) (then (unreachable))) + + (call $task.return (i32.const 42)) + (i32.const 0 (; EXIT ;)) + ) + + ;; Same as above, but for streams. + (func (export "run-stream") (param $sr i32) (result i32) + (local $ret i32) + + (call $context.set (i32.const 400)) + (local.set $ret (call $stream.read (local.get $sr) (i32.const 40) (i32.const 1))) + (if (i32.ne (i32.const 0x10 (; COMPLETED | 1<<4 ;)) (local.get $ret)) (then (unreachable))) + (if (i32.ne (call $context.get) (i32.const 500)) (then (unreachable))) + + (call $task.return (i32.const 42)) + (i32.const 0 (; EXIT ;)) + ) + + (func (export "run-cb") (param i32 i32 i32) (result i32) unreachable)) + + (core instance $M (instantiate $m (with "" (instance + (export "future.read" (func $future.read)) + (export "stream.read" (func $stream.read)) + (export "context.set" (func $context.set)) + (export "context.get" (func $context.get)) + (export "task.return" (func $task.return)) + (export "memory" (memory 1)))))) + (func (export "run-future") async (param "future" $FT) (result u32) + (canon lift (core func $M "run-future") (memory $libc "memory") (realloc (func $libc "realloc")) + async (callback (func $M "run-cb")) + ) + ) + (func (export "run-stream") async (param "stream" $ST) (result u32) + (canon lift (core func $M "run-stream") (memory $libc "memory") (realloc (func $libc "realloc")) + async (callback (func $M "run-cb")) + ) + ) + ) + + (instance $Reader (instantiate $reader)) + (instance $Writer (instantiate $writer + (with "reader" (instance $Reader) + ))) + + (func (export "run") (alias export $Writer "run")) +) +(assert_return (invoke "run") (u32.const 42)) From 9dd8634e01773ae19ddcaba86db53505abc620a5 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 11 Mar 2026 14:46:02 -0500 Subject: [PATCH 08/10] Update some github actions versions (#12762) CI is warning us about these, so try updating. --- .github/actions/install-cargo-vet/action.yml | 2 +- .github/workflows/cargo-audit.yml | 4 +- .github/workflows/ci-cron-trigger.yml | 2 +- .github/workflows/main.yml | 72 ++++++++++---------- .github/workflows/performance.yml | 4 +- .github/workflows/publish-artifacts.yml | 2 +- .github/workflows/publish-to-cratesio.yml | 2 +- .github/workflows/release-process.yml | 2 +- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.github/actions/install-cargo-vet/action.yml b/.github/actions/install-cargo-vet/action.yml index 39ef0c0e63c4..823480e60ae2 100644 --- a/.github/actions/install-cargo-vet/action.yml +++ b/.github/actions/install-cargo-vet/action.yml @@ -10,7 +10,7 @@ inputs: runs: using: composite steps: - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ runner.tool_cache }}/cargo-vet key: cargo-vet-bin-${{ inputs.version }} diff --git a/.github/workflows/cargo-audit.yml b/.github/workflows/cargo-audit.yml index cc03ff43d69e..ccf5a7b72409 100644 --- a/.github/workflows/cargo-audit.yml +++ b/.github/workflows/cargo-audit.yml @@ -8,10 +8,10 @@ jobs: env: CARGO_AUDIT_VERSION: 0.22.1 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ runner.tool_cache }}/cargo-audit key: cargo-audit-${{ env.CARGO_AUDIT_VERSION }} diff --git a/.github/workflows/ci-cron-trigger.yml b/.github/workflows/ci-cron-trigger.yml index 2dfaab44b52d..de2780ad9c75 100644 --- a/.github/workflows/ci-cron-trigger.yml +++ b/.github/workflows/ci-cron-trigger.yml @@ -32,7 +32,7 @@ jobs: name: Trigger release branch CI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 598787c1a17c..e38f5f287242 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,7 +40,7 @@ jobs: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -53,7 +53,7 @@ jobs: name: Check JS runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: npm install working-directory: ./crates/explorer - run: npm run lint @@ -68,7 +68,7 @@ jobs: name: Clang format runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - run: | @@ -89,7 +89,7 @@ jobs: if: needs.determine.outputs.audit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -118,7 +118,7 @@ jobs: outputs: outcome: ${{ steps.vet.outcome }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -179,7 +179,7 @@ jobs: run-dwarf: ${{ steps.calculate.outputs.run-dwarf }} platform-checks: ${{ steps.calculate.outputs.platform-checks }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: calculate env: GH_TOKEN: ${{ github.token }} @@ -264,7 +264,7 @@ jobs: RUSTDOCFLAGS: -Dwarnings --cfg docsrs OPENVINO_SKIP_LINKING: 1 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -281,7 +281,7 @@ jobs: - run: cmake --build target/c-api --target doc # install mdbook, build the docs, and test the docs - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ runner.tool_cache }}/mdbook key: cargo-mdbook-${{ env.MDBOOK_VERSION }}-langtabs-${{ env.MDBOOK_LANGTABS_VERSION }} @@ -313,7 +313,7 @@ jobs: mv crates/c-api/html gh-pages/c-api mv target/doc gh-pages/api tar czf gh-pages.tar.gz gh-pages - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: gh-pages path: gh-pages.tar.gz @@ -435,7 +435,7 @@ jobs: -p wasmtime-wizer --no-default-features --all-features runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -454,7 +454,7 @@ jobs: needs: determine if: needs.determine.outputs.run-full steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -468,7 +468,7 @@ jobs: env: CARGO_NDK_VERSION: 2.12.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -485,7 +485,7 @@ jobs: env: CARGO_NDK_VERSION: 2.12.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -589,7 +589,7 @@ jobs: test: cargo check -p wasmtime --no-default-features --features runtime,gc,component-model,async env: ${{ matrix.env || fromJSON('{}') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -613,7 +613,7 @@ jobs: name: Nightly tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -694,7 +694,7 @@ jobs: flags: -DCMAKE_C_FLAGS=-fsanitize=address -DCMAKE_CXX_FLAGS=-fsanitize=address -DBUILD_SHARED_LIBS=ON -DBUILD_TESTS=ON name: Linux (ASAN) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -731,7 +731,7 @@ jobs: fail-fast: ${{ github.event_name != 'pull_request' }} matrix: ${{ fromJson(needs.determine.outputs.test-matrix) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -769,13 +769,13 @@ jobs: echo CARGO_TARGET_${upcase}_LINKER=${{ matrix.gcc }} >> $GITHUB_ENV if: matrix.gcc != '' - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ runner.tool_cache }}/qemu key: qemu-${{ matrix.target }}-${{ env.QEMU_BUILD_VERSION }}-patchcpuinfo if: matrix.qemu != '' - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ runner.tool_cache }}/sde key: sde-${{ env.SDE_BUILD_VERSION }} @@ -918,7 +918,7 @@ jobs: needs: determine if: needs.determine.outputs.run-full steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -960,7 +960,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -988,7 +988,7 @@ jobs: name: Test wasmtime-fuzzing runs-on: 'ubuntu-latest' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -1004,7 +1004,7 @@ jobs: name: Test DWARF debugging runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -1034,7 +1034,7 @@ jobs: deployments: write contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -1051,7 +1051,7 @@ jobs: env: VERSION: ${{ github.sha }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: bins-wasi-preview1-component-adapter path: target/wasm32-unknown-unknown/release/wasi_snapshot_preview1.*.wasm @@ -1061,7 +1061,7 @@ jobs: needs: build-preview1-component-adapter runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -1076,7 +1076,7 @@ jobs: if: needs.determine.outputs.run-full runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -1103,7 +1103,7 @@ jobs: MIN_PLATFORM_TEST_DISABLE_WASI: 1 # Add the `wasmtime-platform.h` file as a release artifact - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: wasmtime-platform-header path: examples/min-platform/embedding/wasmtime-platform.h @@ -1114,7 +1114,7 @@ jobs: name: Run benchmarks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -1128,7 +1128,7 @@ jobs: name: Meta deterministic check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -1140,7 +1140,7 @@ jobs: if: github.repository == 'bytecodealliance/wasmtime' && needs.determine.outputs.run-full runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust @@ -1187,14 +1187,14 @@ jobs: CARGO_NEXTEST_VERSION: 0.9.88 MIRIFLAGS: -Zmiri-permissive-provenance steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/actions/install-rust with: toolchain: wasmtime-ci-pinned-nightly - run: rustup component add rust-src miri - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ runner.tool_cache }}/cargo-nextest key: cargo-nextest-bin-${{ env.CARGO_NEXTEST_VERSION }} @@ -1221,7 +1221,7 @@ jobs: matrix: ${{ fromJson(needs.determine.outputs.build-matrix) }} env: ${{ matrix.env || fromJSON('{}') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -1249,7 +1249,7 @@ jobs: # unconditionally to this workflow's files so we have a copy of them. - run: ./ci/build-tarballs.sh "${{ matrix.build }}" "${{ matrix.target }}" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: bins-${{ matrix.build }} path: dist @@ -1335,7 +1335,7 @@ jobs: && startsWith(github.ref, 'refs/heads/release-') && github.repository == 'bytecodealliance/wasmtime' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index f3544251cbfd..f3967b3c1d51 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -74,7 +74,7 @@ jobs: cargo build --release - name: Checkout patch from bytecodealliance/wasmtime (pushed and triggering on this perf repo) - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: true path: wasmtime_commit @@ -89,7 +89,7 @@ jobs: cp target/release/libwasmtime_bench_api.so /tmp/wasmtime_commit.so - name: Checkout main from bytecodealliance/wasmtime - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: 'main' repository: 'bytecodealliance/wasmtime' diff --git a/.github/workflows/publish-artifacts.yml b/.github/workflows/publish-artifacts.yml index 3f634d645f17..6d2f01be889b 100644 --- a/.github/workflows/publish-artifacts.yml +++ b/.github/workflows/publish-artifacts.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'bytecodealliance/wasmtime' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ./.github/actions/fetch-run-id - run: | gh run download ${COMMIT_RUN_ID} diff --git a/.github/workflows/publish-to-cratesio.yml b/.github/workflows/publish-to-cratesio.yml index 2f712967878a..c26b6107ae02 100644 --- a/.github/workflows/publish-to-cratesio.yml +++ b/.github/workflows/publish-to-cratesio.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest environment: publish steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - run: rustup update stable && rustup default stable diff --git a/.github/workflows/release-process.yml b/.github/workflows/release-process.yml index c8436ff2f8b5..5d13cacd180c 100644 --- a/.github/workflows/release-process.yml +++ b/.github/workflows/release-process.yml @@ -41,7 +41,7 @@ jobs: name: Run the release process runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Setup From 29d50d0aecd702648b47df4132264b56e2aa6610 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 11 Mar 2026 15:14:24 -0500 Subject: [PATCH 09/10] Enable limiting wasip3 resource limits (#12761) * Enable limiting wasip3 resource limits This commit adds a new `Store::concurrent_resource_table` method which enables getting a handle to the underlying `ResourceTable` used by the concurrent implementation of component-model-async. This can in turn be used to set the max capacity on the table and limit the guest usage of the table. Closes #11552 * Adjust features * Fix imports --- .../src/bin/p2_cli_many_resources.rs | 3 +- .../src/bin/p3_cli_many_tasks.rs | 32 +++++++++++++++++++ .../src/runtime/component/concurrent.rs | 4 +++ .../wasmtime/src/runtime/component/store.rs | 28 ++++++++++++++++ src/commands/run.rs | 4 +++ tests/all/cli_tests.rs | 24 +++++++++++++- 6 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 crates/test-programs/src/bin/p3_cli_many_tasks.rs diff --git a/crates/test-programs/src/bin/p2_cli_many_resources.rs b/crates/test-programs/src/bin/p2_cli_many_resources.rs index fd3d995e06d8..c4d207427c16 100644 --- a/crates/test-programs/src/bin/p2_cli_many_resources.rs +++ b/crates/test-programs/src/bin/p2_cli_many_resources.rs @@ -1,7 +1,8 @@ use test_programs::wasi::clocks::monotonic_clock::subscribe_duration; fn main() { - loop { + for _ in 0..1000 { std::mem::forget(subscribe_duration(1_000_000)); } + panic!("should have trapped before now"); } diff --git a/crates/test-programs/src/bin/p3_cli_many_tasks.rs b/crates/test-programs/src/bin/p3_cli_many_tasks.rs new file mode 100644 index 000000000000..c2784146cb32 --- /dev/null +++ b/crates/test-programs/src/bin/p3_cli_many_tasks.rs @@ -0,0 +1,32 @@ +use test_programs::p3::wasi as wasip3; + +#[link(wasm_import_module = "wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09")] +unsafe extern "C" { + #[link_name = "[async-lower]wait-for"] + fn wait_for(dur: u64) -> u32; +} + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + // Execute it once to ensure we get type information pulled in. + wasip3::clocks::monotonic_clock::wait_for(0).await; + + // Execute the raw function without Rust bindings to stress invoking it + // many times. + for _ in 0..1000 { + unsafe { + wait_for(1 << 60); + } + } + + panic!("should have trapped before now"); + } +} + +fn main() { + unreachable!(); +} diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 822e3fceb4e6..34c9d67c4f58 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -5179,6 +5179,10 @@ impl ConcurrentState { None => bail_bug!("futures field of concurrent state is currently taken"), } } + + pub(crate) fn table(&mut self) -> &mut ResourceTable { + self.table.get_mut() + } } /// Provide a type hint to compiler about the shape of a parameter lower diff --git a/crates/wasmtime/src/runtime/component/store.rs b/crates/wasmtime/src/runtime/component/store.rs index def9d7f3e60c..d5364221f20c 100644 --- a/crates/wasmtime/src/runtime/component/store.rs +++ b/crates/wasmtime/src/runtime/component/store.rs @@ -1,4 +1,6 @@ use crate::prelude::*; +#[cfg(feature = "component-model-async")] +use crate::runtime::component::ResourceTable; use crate::runtime::component::concurrent::ConcurrentState; use crate::runtime::component::{HostResourceData, Instance}; use crate::runtime::vm; @@ -415,6 +417,15 @@ impl StoreOpaque { pub(crate) fn set_hostcall_fuel(&mut self, fuel: usize) { self.component_data_mut().hostcall_fuel = fuel; } + + #[cfg(feature = "component-model-async")] + fn concurrent_resource_table(&mut self) -> Option<&mut ResourceTable> { + if self.concurrency_support() { + Some(self.concurrent_state_mut().table()) + } else { + None + } + } } impl Store { @@ -455,6 +466,17 @@ impl Store { pub fn set_hostcall_fuel(&mut self, fuel: usize) { self.as_context_mut().set_hostcall_fuel(fuel) } + + /// Returns the underlying [`ResourceTable`] that the implementation of + /// concurrency in the component model is using. + /// + /// Returns `None` if [`Config::concurrency_support`] is disabled. + /// + /// [`Config::concurrency_support`]: crate::Config::concurrency_support + #[cfg(feature = "component-model-async")] + pub fn concurrent_resource_table(&mut self) -> Option<&mut ResourceTable> { + self.as_context_mut().0.concurrent_resource_table() + } } impl StoreContextMut<'_, T> { @@ -467,6 +489,12 @@ impl StoreContextMut<'_, T> { pub fn set_hostcall_fuel(&mut self, fuel: usize) { self.0.set_hostcall_fuel(fuel) } + + /// See [`Store::concurrent_resource_table`]. + #[cfg(feature = "component-model-async")] + pub fn concurrent_resource_table(&mut self) -> Option<&mut ResourceTable> { + self.0.concurrent_resource_table() + } } #[derive(Default)] diff --git a/src/commands/run.rs b/src/commands/run.rs index ffb4f458a09c..497da6497bc6 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -1144,6 +1144,10 @@ impl RunCommand { let mut ctx = builder.build_p1(); if let Some(max) = self.run.common.wasi.max_resources { ctx.ctx().table.set_max_capacity(max); + #[cfg(feature = "component-model-async")] + if let Some(table) = store.concurrent_resource_table() { + table.set_max_capacity(max); + } } if let Some(fuel) = self.run.common.wasi.hostcall_fuel { store.set_hostcall_fuel(fuel); diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index a3dad513e517..b303fbc5348a 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -2683,7 +2683,29 @@ start a print 1234 #[test] fn p2_cli_many_resources() -> Result<()> { - let err = run_wasmtime(&["run", P2_CLI_MANY_RESOURCES_COMPONENT]).unwrap_err(); + let err = run_wasmtime(&[ + "run", + "-Smax-resources=100", + P2_CLI_MANY_RESOURCES_COMPONENT, + ]) + .unwrap_err(); + assert!( + err.to_string().contains("resource table has no free keys"), + "bad error message: {err}" + ); + Ok(()) + } + + #[test] + fn p3_cli_many_tasks() -> Result<()> { + let err = run_wasmtime(&[ + "run", + "-Smax-resources=100", + "-Sp3", + "-Wcomponent-model-async", + dbg!(P3_CLI_MANY_TASKS_COMPONENT), + ]) + .unwrap_err(); assert!( err.to_string().contains("resource table has no free keys"), "bad error message: {err}" From c6e2ea2d7e25b095be182583e22aba3ed1400bbf Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 12 Mar 2026 16:54:51 -0500 Subject: [PATCH 10/10] Consume hostcall fuel when buffering stream data in the host (#12767) For guest-to-guest communication stream reads/writes rendezvousing together will currently copy data through `Val`. This is expected to become more optimized in the future, but for now this needs to consume the concept of "hostcall fuel" introduced in #12652 to ensure that the guest can't exhaust memory in the host. This additionally tweaks some hostcall fuel calculations to more accurately reflect the size of values on the host, notably by using `size_of::()` on the host rather than the size in the guest. Closes #12674 --- .../concurrent/futures_and_streams.rs | 1 + .../src/runtime/component/func/options.rs | 30 ++- .../src/runtime/component/func/typed.rs | 2 +- .../wasmtime/src/runtime/component/values.rs | 2 +- .../async/streams-massive-send.wast | 239 ++++++++++++++++++ 5 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 tests/misc_testsuite/component-model/async/streams-massive-send.wast diff --git a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs index fdc7f18ad33a..a2f2a335af0a 100644 --- a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs +++ b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs @@ -3345,6 +3345,7 @@ impl Instance { .ok_or_else(|| { crate::format_err!("write pointer out of bounds of memory") })?; + lift.consume_fuel_array(count, size_of::())?; let values = (0..count) .map(|index| Val::load(lift, ty, &bytes[(index * size)..][..size])) diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 76f55f1e8ec3..265ad9ff8d51 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -10,6 +10,7 @@ use crate::runtime::vm::VMFuncRef; use crate::runtime::vm::component::{ComponentInstance, HandleTable, ResourceTables}; use crate::store::{StoreId, StoreOpaque}; use alloc::sync::Arc; +use core::fmt; use core::pin::Pin; use core::ptr::NonNull; use wasmtime_environ::component::{ @@ -480,11 +481,32 @@ impl<'a> LiftContext<'a> { pub fn consume_fuel(&mut self, amt: usize) -> Result<()> { match self.hostcall_fuel.checked_sub(amt) { Some(new) => self.hostcall_fuel = new, - None => bail!( - "too much data is being copied between the host and the guest: \ - fuel allocated for hostcalls has been exhausted" - ), + None => bail!(HostcallFuelExhausted), } Ok(()) } + + /// Same as [`Self::consume_fuel`], but safely multiplies `len` and `size` + /// together before calling that. + pub fn consume_fuel_array(&mut self, len: usize, size: usize) -> Result<()> { + match len.checked_mul(size) { + Some(bytes) => self.consume_fuel(bytes), + None => bail!(HostcallFuelExhausted), + } + } } + +#[derive(Debug)] +struct HostcallFuelExhausted; + +impl fmt::Display for HostcallFuelExhausted { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "too much data is being copied between the host and the guest: \ + fuel allocated for hostcalls has been exhausted" + ) + } +} + +impl core::error::Error for HostcallFuelExhausted {} diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index fe2dee93e9ca..48ff645c2b6e 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -1909,7 +1909,7 @@ impl WasmList { .checked_mul(T::SIZE32) .and_then(|len| ptr.checked_add(len)) { - Some(n) if n <= cx.memory().len() => cx.consume_fuel(n - ptr)?, + Some(n) if n <= cx.memory().len() => cx.consume_fuel_array(len, size_of::())?, _ => bail!("list pointer/length out of bounds of memory"), } if ptr % usize::try_from(T::ALIGN32)? != 0 { diff --git a/crates/wasmtime/src/runtime/component/values.rs b/crates/wasmtime/src/runtime/component/values.rs index 1049d1c12712..d0a61d00c0d4 100644 --- a/crates/wasmtime/src/runtime/component/values.rs +++ b/crates/wasmtime/src/runtime/component/values.rs @@ -919,7 +919,7 @@ fn load_list(cx: &mut LiftContext<'_>, ty: TypeListIndex, ptr: usize, len: usize .checked_mul(element_size) .and_then(|len| ptr.checked_add(len)) { - Some(n) if n <= cx.memory().len() => cx.consume_fuel(n - ptr)?, + Some(n) if n <= cx.memory().len() => cx.consume_fuel_array(len, size_of::())?, _ => bail!("list pointer/length out of bounds of memory"), } if ptr % usize::try_from(element_alignment)? != 0 { diff --git a/tests/misc_testsuite/component-model/async/streams-massive-send.wast b/tests/misc_testsuite/component-model/async/streams-massive-send.wast new file mode 100644 index 000000000000..cc44606b909b --- /dev/null +++ b/tests/misc_testsuite/component-model/async/streams-massive-send.wast @@ -0,0 +1,239 @@ +;;! component_model_async = true +;;! reference_types = true + +;; This test exercises corner cases where extremely large values are sent +;; between guests and currently require copying out to the host in Wasmtime +;; which should result in a trap of some form rather than the host spending all +;; its time allocating and copying memory. + +(component definition $A + (type $t (list (list (list (list u8))))) + (type $s (stream $t)) + (type $f (future $t)) + (type $functy (func async (result $s))) + + (component $A + (core module $libc (memory (export "memory") 1)) + (core instance $libc (instantiate $libc)) + + (core module $m + (import "libc" "memory" (memory 1)) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "task.return future" (func $task.return-future (param i32))) + (import "" "task.return stream" (func $task.return-stream (param i32))) + + (func (export "big-stream") (result i32) + (local $w i32) + (local $r i32) + (local $s i64) + (local.set $s (call $stream.new)) + (local.set $r (i32.wrap_i64 (local.get $s))) + (local.set $w (i32.wrap_i64 (i64.shr_u (local.get $s) (i64.const 32)))) + + (call $task.return-stream (local.get $r)) + + local.get $w + (call $prepare-list-to-write (i32.const 5) (i32.const 2)) + call $stream.write + unreachable + ) + + (func (export "big-future") (result i32) + (local $w i32) + (local $r i32) + (local $s i64) + (local $base i32) + (local $len i32) + (local.set $s (call $future.new)) + (local.set $r (i32.wrap_i64 (local.get $s))) + (local.set $w (i32.wrap_i64 (i64.shr_u (local.get $s) (i64.const 32)))) + + (call $task.return-future (local.get $r)) + + (call $prepare-list-to-write (i32.const 4) (i32.const 2)) + local.set $len + local.set $base + + (i32.store offset=0 (i32.const 100) (local.get $base)) + (i32.store offset=4 (i32.const 100) (local.get $len)) + + + local.get $w + i32.const 100 + call $future.write + unreachable + ) + + ;; Prepare $depth+1 layers of lists where the leaves point to all of + ;; memory and each layer otherwise is a list of the previous layer. + ;; + ;; Each layer-of-lists is `$pages` large. + (func $prepare-list-to-write (param $depth i32) (param $pages i32) (result i32 i32) + (local $base i32) + (local $len i32) + + (local $c_base i32) + (local $c_len i32) + (local $i i32) + + local.get $depth + if + ;; Case of $depth>0 meaning that this is a list-of-lists layer. + ;; Allocate some memory to store this list itself then generate the + ;; layer down by recursing. + (local.set $base (call $grow (local.get $pages))) + (local.set $len + (i32.div_u + (i32.mul (local.get $pages) (i32.const 65536)) + (i32.const 8) + ) + ) + + (call $prepare-list-to-write + (i32.sub (local.get $depth) (i32.const 1)) + (local.get $pages)) + local.set $c_len + local.set $c_base + + ;; Initialize this list-of-lists with all copies of the previous + ;; layer's list. + loop $l + (i32.store offset=0 + (i32.add (local.get $base) (i32.mul (local.get $i) (i32.const 8))) + (local.get $c_base)) + (i32.store offset=4 + (i32.add (local.get $base) (i32.mul (local.get $i) (i32.const 8))) + (local.get $c_len)) + + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (if (i32.lt_u (local.get $i) (local.get $len)) + (then (br $l))) + end + + else + ;; base case: the bottom list is just a byte list of all of memory. + (local.set $base (i32.const 0)) + (local.set $len (i32.mul (memory.size) (i32.const 65536))) + end + + local.get $base + local.get $len + ) + + (func $grow (param i32) (result i32) + (local $r i32) + (local.set $r (memory.grow (local.get 0))) + local.get $r + i32.const -1 + i32.eq + if unreachable end + local.get $r + i32.const 65536 + i32.mul + ) + + (func (export "cb") (param i32 i32 i32) (result i32) unreachable) + ) + (core func $future.new (canon future.new $f)) + (core func $future.write (canon future.write $f (memory $libc "memory"))) + (core func $stream.new (canon stream.new $s)) + (core func $stream.write (canon stream.write $s (memory $libc "memory"))) + (core func $task.return-future (canon task.return (result $f))) + (core func $task.return-stream (canon task.return (result $s))) + (core instance $m (instantiate $m + (with "libc" (instance $libc)) + (with "" (instance + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + (export "task.return future" (func $task.return-future)) + (export "task.return stream" (func $task.return-stream)) + )) + )) + + (func (export "big-stream") (result $s) + (canon lift (core func $m "big-stream") async + (callback (func $m "cb")))) + (func (export "big-future") (result $f) + (canon lift (core func $m "big-future") async + (callback (func $m "cb")))) + ) + + (component $B + (import "a" (instance $a + (export "big-future" (func (result $f))) + (export "big-stream" (func (result $s))) + )) + + (core module $libc + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) unreachable) + ) + (core instance $libc (instantiate $libc)) + + (core module $m + (import "libc" "memory" (memory 1)) + (import "" "big-stream" (func $big-stream (result i32))) + (import "" "big-future" (func $big-future (result i32))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + + (func (export "stream") + (call $stream.read + (call $big-stream) + i32.const 0 + i32.const 100 + ) + unreachable + ) + (func (export "future") + (call $future.read + (call $big-future) + i32.const 0 + ) + unreachable + ) + ) + (core func $big-stream (canon lower (func $a "big-stream"))) + (core func $big-future (canon lower (func $a "big-future"))) + (core func $stream.read + (canon stream.read $s + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + (core func $future.read + (canon future.read $f + (memory $libc "memory") + (realloc (func $libc "realloc")) + ) + ) + (core instance $m (instantiate $m + (with "libc" (instance $libc)) + (with "" (instance + (export "big-stream" (func $big-stream)) + (export "big-future" (func $big-future)) + (export "future.read" (func $future.read)) + (export "stream.read" (func $stream.read)) + )) + )) + + (func (export "stream") async (canon lift (core func $m "stream"))) + (func (export "future") async (canon lift (core func $m "future"))) + + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "a" (instance $a)))) + (export "stream" (func $b "stream")) + (export "future" (func $b "future")) +) + +(component instance $A $A) +(assert_trap (invoke "stream") "fuel allocated for hostcalls has been exhausted") +(component instance $A $A) +(assert_trap (invoke "future") "fuel allocated for hostcalls has been exhausted")