From 6ac12ed118184f0e6241c9cab34c6a4c3b617e67 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Thu, 13 Nov 2025 17:09:14 +0100 Subject: [PATCH 001/114] Implement continuation_switch prototype --- lib/wasix/src/bin_factory/exec.rs | 4 +- lib/wasix/src/lib.rs | 3 + lib/wasix/src/state/env.rs | 26 ++- .../src/syscalls/wasix/continuation_switch.rs | 209 ++++++++++++++++++ lib/wasix/src/syscalls/wasix/mod.rs | 2 + 5 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 lib/wasix/src/syscalls/wasix/continuation_switch.rs diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index d077394ddb8..3e235164d10 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -14,7 +14,7 @@ use crate::{ TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, TaskWasmRunProperties, }, }, - syscalls::rewind_ext, + syscalls::{call_in_async_runtime, rewind_ext}, }; use tracing::*; use virtual_mio::block_on; @@ -299,7 +299,7 @@ fn call_module( return; }; - let mut call_ret = start.call(&mut store, &[]); + let mut call_ret = { call_in_async_runtime(&ctx, &mut store, start.clone(), &[]) }; loop { // Technically, it's an error for a vfork to return from main, but anyway... diff --git a/lib/wasix/src/lib.rs b/lib/wasix/src/lib.rs index 4af85bc2e6a..97730c5660e 100644 --- a/lib/wasix/src/lib.rs +++ b/lib/wasix/src/lib.rs @@ -617,6 +617,9 @@ fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) "sched_yield" => Function::new_typed_with_env(&mut store, env, sched_yield::), "stack_checkpoint" => Function::new_typed_with_env(&mut store, env, stack_checkpoint::), "stack_restore" => Function::new_typed_with_env(&mut store, env, stack_restore::), + "continuation_new" => Function::new_typed_with_env(&mut store, env, continuation_new::), + "continuation_switch" => Function::new_typed_with_env_async(&mut store, env, continuation_switch), + "continuation_delete" => Function::new_typed_with_env(&mut store, env, continuation_delete), "futex_wait" => Function::new_typed_with_env(&mut store, env, futex_wait::), "futex_wake" => Function::new_typed_with_env(&mut store, env, futex_wake::), "futex_wake_all" => Function::new_typed_with_env(&mut store, env, futex_wake_all::), diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 739adccdf3b..ea21c776af0 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -1,9 +1,9 @@ use std::{ - collections::HashMap, + collections::{BTreeMap, HashMap}, ops::Deref, path::{Path, PathBuf}, str, - sync::Arc, + sync::{Arc, RwLock, atomic::AtomicU32}, time::Duration, }; @@ -38,13 +38,15 @@ use crate::{ process::{WasiProcess, WasiProcessId}, thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, }, - syscalls::platform_clock_time_get, + syscalls::{platform_clock_time_get, wasix::continuation_switch::Greenthread}, }; use wasmer_types::ModuleHash; pub use super::handles::*; use super::{Linker, WasiState, conv_env_vars}; +static MAIN_CONTINUATION_ID: u32 = 0; + /// Data required to construct a [`WasiEnv`]. #[derive(Debug)] pub struct WasiEnvInit { @@ -179,6 +181,11 @@ pub struct WasiEnv { /// not be cloned when `WasiEnv` is cloned) /// TODO: We should move this outside of `WasiEnv` with some refactoring inner: WasiInstanceHandlesPointer, + + /// TODO: Document these fields + pub(crate) greenthreads: Arc>>, + pub(crate) current_greenthread_id: Arc>, + pub(crate) next_free_id: AtomicU32, } impl std::fmt::Debug for WasiEnv { @@ -208,6 +215,12 @@ impl Clone for WasiEnv { replaying_journal: self.replaying_journal, skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, + greenthreads: self.greenthreads.clone(), + current_greenthread_id: self.current_greenthread_id.clone(), + // TODO: This is wrong; The two lines above as well + next_free_id: AtomicU32::new( + self.next_free_id.load(std::sync::atomic::Ordering::SeqCst), + ), } } } @@ -249,6 +262,10 @@ impl WasiEnv { replaying_journal: false, skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, + // TODO: Not sure if we can even properly fork coroutines at all + greenthreads: Arc::new(RwLock::new(BTreeMap::new())), + current_greenthread_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), + next_free_id: AtomicU32::new(MAIN_CONTINUATION_ID + 1), }; Ok((new_env, handle)) } @@ -393,6 +410,9 @@ impl WasiEnv { bin_factory: init.bin_factory, capabilities: init.capabilities, disable_fs_cleanup: false, + greenthreads: Arc::new(RwLock::new(BTreeMap::new())), + current_greenthread_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), + next_free_id: AtomicU32::new(MAIN_CONTINUATION_ID + 1), }; env.owned_handles.push(thread); diff --git a/lib/wasix/src/syscalls/wasix/continuation_switch.rs b/lib/wasix/src/syscalls/wasix/continuation_switch.rs new file mode 100644 index 00000000000..7607c9178a7 --- /dev/null +++ b/lib/wasix/src/syscalls/wasix/continuation_switch.rs @@ -0,0 +1,209 @@ +use super::*; +use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; +use anyhow::Result; +use core::panic; +use futures::task::LocalSpawnExt; +use futures::{FutureExt, channel::oneshot}; +use rkyv::vec; +use std::collections::BTreeMap; +use std::sync::atomic::AtomicU32; +use std::sync::{Arc, OnceLock, RwLock}; +use wasmer::{ + AsStoreMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, Module, + RuntimeError, Store, Value, imports, +}; +use wasmer::{StoreMut, Tag, Type}; + +pub struct Greenthread { + resumer: Option>, +} + +impl Clone for Greenthread { + fn clone(&self) -> Self { + if self.resumer.is_some() { + panic!("Cannot clone a coroutine with a resumer"); + } + Self { resumer: None } + } +} + +thread_local! { + static LOCAL_SPAWNER: OnceLock = OnceLock::new(); +} +fn spawn_local(future: F) +where + F: std::future::Future + 'static, +{ + LOCAL_SPAWNER.with(|spawner_lock| { + let spawner = spawner_lock.get().expect("Local spawner not initialized"); + spawner + .spawn_local(future) + .expect("Failed to spawn local future"); + }); +} + +#[instrument(level = "trace", skip(ctx, store), ret)] +pub fn call_in_async_runtime<'a>( + ctx: &WasiFunctionEnv, + store: &mut Store, + entrypoint: wasmer::Function, + params: &'a [wasmer::Value], +) -> Result, RuntimeError> { + let mut runtime_builder = tokio::runtime::Builder::new_current_thread(); + let runtime = runtime_builder.enable_all().build().unwrap(); + let local = tokio::task::LocalSet::new(); + let cloned_params = params.to_vec(); + + let main_greenthread = Greenthread { resumer: None }; + + let env = ctx.data_mut(store); + env.greenthreads + .write() + .unwrap() + .insert(0, main_greenthread); + + let mut localpool = futures::executor::LocalPool::new(); + let local_spawner = localpool.spawner(); + LOCAL_SPAWNER.with(|spawner_lock| { + spawner_lock + .set(local_spawner) + .expect("Failed to set local spawner"); + }); + let result = localpool.run_until(entrypoint.call_async(&mut *store, &[])); + + result +} + +/// ### `greenthread_delete()` +#[instrument(level = "trace", skip(ctx), ret)] +pub fn continuation_delete( + mut ctx: FunctionEnvMut<'_, WasiEnv>, + coroutine: u32, +) -> Result { + WasiEnv::do_pending_operations(&mut ctx)?; + + let env = ctx.data(); + let memory: MemoryView<'_> = unsafe { env.memory_view(&ctx) }; + // TODO: implement + + Ok(Errno::Success) +} + +/// ### `greenthread_new()` +#[instrument(level = "trace", skip(ctx), ret)] +pub fn continuation_new( + mut ctx: FunctionEnvMut<'_, WasiEnv>, + new_coroutine_ptr: WasmPtr, + entrypoint: u32, +) -> Result { + WasiEnv::do_pending_operations(&mut ctx)?; + + let (data, mut store) = ctx.data_and_store_mut(); + let new_greenthread_id = data + .next_free_id + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + + let function = data + .inner() + .indirect_function_table_lookup(&mut store, entrypoint) + .expect("Function not found in table"); + // let function = function.as_ref().unwrap_or(entrypoint); + + let (sender, receiver) = oneshot::channel::<()>(); + + let new_greenthread = Greenthread { + // entrypoint: Some(entrypoint_data), + resumer: Some(sender), + }; + + data.greenthreads + .write() + .unwrap() + .insert(new_greenthread_id, new_greenthread); + + // SAFETY: This is fine if we can ensure that ??? + // A: The future does not outlive the store + // B: we now have multiple mutable references, this is dangerous + let mut unsafe_static_store = + unsafe { std::mem::transmute::<_, StoreMut<'static>>(store.as_store_mut()) }; + + tokio::task::spawn_local(async move { + receiver.await.unwrap(); + let resumer = function.call_async(&mut unsafe_static_store, &[]).await; + panic!("Greenthread function returned {:?}", resumer); + }); + + let env = ctx.data(); + let memory = unsafe { env.memory_view(&ctx) }; + + new_coroutine_ptr + .write(&memory, new_greenthread_id as u32) + .unwrap(); + + Ok(Errno::Success) +} + +/// Switch to another coroutine +// #[instrument(level = "trace", skip(ctx), ret)] +pub fn continuation_switch( + mut ctx: FunctionEnvMut, + next_greenthread_id: u32, + // params: &[Value], +) -> impl Future> + Send + 'static + use<> { + // let next_continuation_id = next_continuation_id + // .first() + // .expect("Expected one argument for continuation_switch") + // .unwrap_i32() as u32; + + match WasiEnv::do_pending_operations(&mut ctx) { + Ok(()) => {} + Err(e) => { + // TODO: Move this to the end to only need a single async block + panic!("Error in do_pending_operations"); + // return async move {Err(RuntimeError::user(Box::new(e)))} + } + } + // let next_greenthread_id = params[0].unwrap_i32() as u32; + + let (data, _store) = ctx.data_and_store_mut(); + let current_greenthread_id = { + let mut current = data.current_greenthread_id.write().unwrap(); + let old = *current; + *current = next_greenthread_id; + old + }; + + if current_greenthread_id == next_greenthread_id { + panic!("Switching to self is not allowed"); + } + + let (sender, receiver) = oneshot::channel::<()>(); + + { + let mut greenthreads = data.greenthreads.write().unwrap(); + let this_one = greenthreads.get_mut(¤t_greenthread_id).unwrap(); + if this_one.resumer.is_some() { + panic!("Switching from a greenthread that is already switched out"); + } + this_one.resumer = Some(sender); + } + + { + let mut greenthreads = data.greenthreads.write().unwrap(); + let next_one = greenthreads.get_mut(&next_greenthread_id).unwrap(); + let Some(resumer) = next_one.resumer.take() else { + panic!("Switching to greenthread that has no resumer"); + }; + resumer.send(()).unwrap(); + } + + let current_id_arc = data.current_greenthread_id.clone(); + + async move { + let _ = receiver.map(|_| ()).await; + + *current_id_arc.write().unwrap() = current_greenthread_id; + + Ok(Errno::Success) // TODO: Errno::success + } +} diff --git a/lib/wasix/src/syscalls/wasix/mod.rs b/lib/wasix/src/syscalls/wasix/mod.rs index 9f1d2b81f4c..1de1e5f12dd 100644 --- a/lib/wasix/src/syscalls/wasix/mod.rs +++ b/lib/wasix/src/syscalls/wasix/mod.rs @@ -4,6 +4,7 @@ mod chdir; mod closure_allocate; mod closure_free; mod closure_prepare; +pub mod continuation_switch; mod dl_invalid_handle; mod dlopen; mod dlsym; @@ -91,6 +92,7 @@ pub use chdir::*; pub use closure_allocate::*; pub use closure_free::*; pub use closure_prepare::*; +pub use continuation_switch::*; pub use dl_invalid_handle::*; pub use dlopen::*; pub use dlsym::*; From ca770c5ccc688e51ebd9763f6406b5851eebe4bc Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 14 Nov 2025 15:24:05 +0100 Subject: [PATCH 002/114] Rename continuations to WASIX context switching API --- lib/wasix/src/lib.rs | 6 +- lib/wasix/src/state/env.rs | 16 +- .../src/syscalls/wasix/continuation_switch.rs | 151 +++++++++++------- 3 files changed, 103 insertions(+), 70 deletions(-) diff --git a/lib/wasix/src/lib.rs b/lib/wasix/src/lib.rs index 97730c5660e..c692a9ba001 100644 --- a/lib/wasix/src/lib.rs +++ b/lib/wasix/src/lib.rs @@ -617,9 +617,9 @@ fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) "sched_yield" => Function::new_typed_with_env(&mut store, env, sched_yield::), "stack_checkpoint" => Function::new_typed_with_env(&mut store, env, stack_checkpoint::), "stack_restore" => Function::new_typed_with_env(&mut store, env, stack_restore::), - "continuation_new" => Function::new_typed_with_env(&mut store, env, continuation_new::), - "continuation_switch" => Function::new_typed_with_env_async(&mut store, env, continuation_switch), - "continuation_delete" => Function::new_typed_with_env(&mut store, env, continuation_delete), + "context_new" => Function::new_typed_with_env(&mut store, env, context_new::), + "context_switch" => Function::new_typed_with_env_async(&mut store, env, context_switch), + "context_delete" => Function::new_typed_with_env(&mut store, env, context_delete), "futex_wait" => Function::new_typed_with_env(&mut store, env, futex_wait::), "futex_wake" => Function::new_typed_with_env(&mut store, env, futex_wake::), "futex_wake_all" => Function::new_typed_with_env(&mut store, env, futex_wake_all::), diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index ea21c776af0..0c8f5c9da78 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -3,7 +3,7 @@ use std::{ ops::Deref, path::{Path, PathBuf}, str, - sync::{Arc, RwLock, atomic::AtomicU32}, + sync::{Arc, RwLock, atomic::AtomicU64}, time::Duration, }; @@ -45,7 +45,7 @@ use wasmer_types::ModuleHash; pub use super::handles::*; use super::{Linker, WasiState, conv_env_vars}; -static MAIN_CONTINUATION_ID: u32 = 0; +static MAIN_CONTINUATION_ID: u64 = 0; /// Data required to construct a [`WasiEnv`]. #[derive(Debug)] @@ -183,9 +183,9 @@ pub struct WasiEnv { inner: WasiInstanceHandlesPointer, /// TODO: Document these fields - pub(crate) greenthreads: Arc>>, - pub(crate) current_greenthread_id: Arc>, - pub(crate) next_free_id: AtomicU32, + pub(crate) greenthreads: Arc>>, + pub(crate) current_greenthread_id: Arc>, + pub(crate) next_free_id: AtomicU64, } impl std::fmt::Debug for WasiEnv { @@ -218,7 +218,7 @@ impl Clone for WasiEnv { greenthreads: self.greenthreads.clone(), current_greenthread_id: self.current_greenthread_id.clone(), // TODO: This is wrong; The two lines above as well - next_free_id: AtomicU32::new( + next_free_id: AtomicU64::new( self.next_free_id.load(std::sync::atomic::Ordering::SeqCst), ), } @@ -265,7 +265,7 @@ impl WasiEnv { // TODO: Not sure if we can even properly fork coroutines at all greenthreads: Arc::new(RwLock::new(BTreeMap::new())), current_greenthread_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), - next_free_id: AtomicU32::new(MAIN_CONTINUATION_ID + 1), + next_free_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), }; Ok((new_env, handle)) } @@ -412,7 +412,7 @@ impl WasiEnv { disable_fs_cleanup: false, greenthreads: Arc::new(RwLock::new(BTreeMap::new())), current_greenthread_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), - next_free_id: AtomicU32::new(MAIN_CONTINUATION_ID + 1), + next_free_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), }; env.owned_handles.push(thread); diff --git a/lib/wasix/src/syscalls/wasix/continuation_switch.rs b/lib/wasix/src/syscalls/wasix/continuation_switch.rs index 7607c9178a7..0f24cfde6f9 100644 --- a/lib/wasix/src/syscalls/wasix/continuation_switch.rs +++ b/lib/wasix/src/syscalls/wasix/continuation_switch.rs @@ -1,6 +1,7 @@ use super::*; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use anyhow::Result; +pub use context::Greenthread; use core::panic; use futures::task::LocalSpawnExt; use futures::{FutureExt, channel::oneshot}; @@ -14,16 +15,56 @@ use wasmer::{ }; use wasmer::{StoreMut, Tag, Type}; -pub struct Greenthread { - resumer: Option>, -} +mod context { + use futures::{FutureExt, channel::oneshot}; + use wasmer::RuntimeError; + + pub struct Greenthread { + resumer: Option>>, + } -impl Clone for Greenthread { - fn clone(&self) -> Self { - if self.resumer.is_some() { - panic!("Cannot clone a coroutine with a resumer"); + impl Greenthread { + // Create a new non-suspended context + pub fn new() -> Self { + Self { resumer: None } + } + // Lock this greenthread until resumed + // + // Panics if the greenthread is already locked + pub fn suspend(&mut self) -> impl Future> + use<> { + let (sender, receiver) = oneshot::channel(); + if self.resumer.is_some() { + panic!("Switching from a greenthread that is already switched out"); + } + self.resumer = Some(sender); + receiver.map(|r| { + match r { + Ok(v) => v, + Err(_canceled) => { + // TODO: Handle canceled properly + panic!("Greenthread was canceled"); + } + } + }) + // TODO: Think about whether canceled should be handled + } + + // Allow this greenthread to be resumed + pub fn resume(&mut self, value: Result<(), RuntimeError>) -> () { + let resumer = self + .resumer + .take() + .expect("Resuming a greenthread that is not switched out"); + resumer.send(value).unwrap(); + } + } + impl Clone for Greenthread { + fn clone(&self) -> Self { + if self.resumer.is_some() { + panic!("Cannot clone a coroutine with a resumer"); + } + Self { resumer: None } } - Self { resumer: None } } } @@ -49,12 +90,9 @@ pub fn call_in_async_runtime<'a>( entrypoint: wasmer::Function, params: &'a [wasmer::Value], ) -> Result, RuntimeError> { - let mut runtime_builder = tokio::runtime::Builder::new_current_thread(); - let runtime = runtime_builder.enable_all().build().unwrap(); - let local = tokio::task::LocalSet::new(); let cloned_params = params.to_vec(); - let main_greenthread = Greenthread { resumer: None }; + let main_greenthread = Greenthread::new(); let env = ctx.data_mut(store); env.greenthreads @@ -69,16 +107,16 @@ pub fn call_in_async_runtime<'a>( .set(local_spawner) .expect("Failed to set local spawner"); }); - let result = localpool.run_until(entrypoint.call_async(&mut *store, &[])); + let result = localpool.run_until(entrypoint.call_async(&mut *store, &cloned_params)); result } /// ### `greenthread_delete()` #[instrument(level = "trace", skip(ctx), ret)] -pub fn continuation_delete( +pub fn context_delete( mut ctx: FunctionEnvMut<'_, WasiEnv>, - coroutine: u32, + coroutine: u64, ) -> Result { WasiEnv::do_pending_operations(&mut ctx)?; @@ -91,14 +129,15 @@ pub fn continuation_delete( /// ### `greenthread_new()` #[instrument(level = "trace", skip(ctx), ret)] -pub fn continuation_new( +pub fn context_new( mut ctx: FunctionEnvMut<'_, WasiEnv>, - new_coroutine_ptr: WasmPtr, + new_coroutine_ptr: WasmPtr, entrypoint: u32, ) -> Result { WasiEnv::do_pending_operations(&mut ctx)?; let (data, mut store) = ctx.data_and_store_mut(); + let new_greenthread_id = data .next_free_id .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -107,14 +146,9 @@ pub fn continuation_new( .inner() .indirect_function_table_lookup(&mut store, entrypoint) .expect("Function not found in table"); - // let function = function.as_ref().unwrap_or(entrypoint); - let (sender, receiver) = oneshot::channel::<()>(); - - let new_greenthread = Greenthread { - // entrypoint: Some(entrypoint_data), - resumer: Some(sender), - }; + let mut new_greenthread = Greenthread::new(); + let new_context_resume = new_greenthread.suspend(); data.greenthreads .write() @@ -127,34 +161,46 @@ pub fn continuation_new( let mut unsafe_static_store = unsafe { std::mem::transmute::<_, StoreMut<'static>>(store.as_store_mut()) }; - tokio::task::spawn_local(async move { - receiver.await.unwrap(); - let resumer = function.call_async(&mut unsafe_static_store, &[]).await; - panic!("Greenthread function returned {:?}", resumer); + let greenthreads_arc = data.greenthreads.clone(); + + spawn_local(async move { + new_context_resume.await; + let result = function.call_async(&mut unsafe_static_store, &[]).await; + + let mut main_greenthread = greenthreads_arc.write().unwrap(); + let main_greenthread = main_greenthread.get_mut(&0).unwrap(); + match result { + Err(e) => { + eprintln!("Context {} returned error {:?}", new_greenthread_id, e); + main_greenthread.resume(Err(e)); + } + Ok(v) => { + panic!( + "Context {} returned a value ({:?}). This is not allowed for now", + new_greenthread_id, v + ); + // TODO: Handle this properly + } + } + // TODO: Delete own greenthread }); let env = ctx.data(); let memory = unsafe { env.memory_view(&ctx) }; new_coroutine_ptr - .write(&memory, new_greenthread_id as u32) + .write(&memory, new_greenthread_id) .unwrap(); Ok(Errno::Success) } /// Switch to another coroutine -// #[instrument(level = "trace", skip(ctx), ret)] -pub fn continuation_switch( +#[instrument(level = "trace", skip(ctx), ret)] +pub fn context_switch( mut ctx: FunctionEnvMut, - next_greenthread_id: u32, - // params: &[Value], + next_greenthread_id: u64, ) -> impl Future> + Send + 'static + use<> { - // let next_continuation_id = next_continuation_id - // .first() - // .expect("Expected one argument for continuation_switch") - // .unwrap_i32() as u32; - match WasiEnv::do_pending_operations(&mut ctx) { Ok(()) => {} Err(e) => { @@ -163,7 +209,6 @@ pub fn continuation_switch( // return async move {Err(RuntimeError::user(Box::new(e)))} } } - // let next_greenthread_id = params[0].unwrap_i32() as u32; let (data, _store) = ctx.data_and_store_mut(); let current_greenthread_id = { @@ -177,33 +222,21 @@ pub fn continuation_switch( panic!("Switching to self is not allowed"); } - let (sender, receiver) = oneshot::channel::<()>(); + let mut greenthreads = data.greenthreads.write().unwrap(); + let this_one = greenthreads.get_mut(¤t_greenthread_id).unwrap(); + let receiver_promise = this_one.suspend(); - { - let mut greenthreads = data.greenthreads.write().unwrap(); - let this_one = greenthreads.get_mut(¤t_greenthread_id).unwrap(); - if this_one.resumer.is_some() { - panic!("Switching from a greenthread that is already switched out"); - } - this_one.resumer = Some(sender); - } - - { - let mut greenthreads = data.greenthreads.write().unwrap(); - let next_one = greenthreads.get_mut(&next_greenthread_id).unwrap(); - let Some(resumer) = next_one.resumer.take() else { - panic!("Switching to greenthread that has no resumer"); - }; - resumer.send(()).unwrap(); - } + let next_one = greenthreads.get_mut(&next_greenthread_id).unwrap(); + next_one.resume(Ok(())); let current_id_arc = data.current_greenthread_id.clone(); async move { - let _ = receiver.map(|_| ()).await; + let result = receiver_promise.await; *current_id_arc.write().unwrap() = current_greenthread_id; - Ok(Errno::Success) // TODO: Errno::success + // If we get relayed a trap, propagate it. Other wise return success + result.and(Ok(Errno::Success)) } } From 83f62ead0ee402e3429ffd4910b44b353d394225 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 17 Nov 2025 10:23:41 +0100 Subject: [PATCH 003/114] Use unambigous name context for the WASIX context switching API --- lib/wasix/src/state/env.rs | 29 +++--- ...ntinuation_switch.rs => context_switch.rs} | 89 +++++++++---------- lib/wasix/src/syscalls/wasix/mod.rs | 4 +- 3 files changed, 59 insertions(+), 63 deletions(-) rename lib/wasix/src/syscalls/wasix/{continuation_switch.rs => context_switch.rs} (72%) diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 0c8f5c9da78..1bd35bbf94c 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -38,7 +38,7 @@ use crate::{ process::{WasiProcess, WasiProcessId}, thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, }, - syscalls::{platform_clock_time_get, wasix::continuation_switch::Greenthread}, + syscalls::{platform_clock_time_get, wasix::context_switch::Context}, }; use wasmer_types::ModuleHash; @@ -183,9 +183,9 @@ pub struct WasiEnv { inner: WasiInstanceHandlesPointer, /// TODO: Document these fields - pub(crate) greenthreads: Arc>>, - pub(crate) current_greenthread_id: Arc>, - pub(crate) next_free_id: AtomicU64, + pub(crate) contexts: Arc>>, + pub(crate) current_context_id: Arc>, + pub(crate) next_available_context_id: AtomicU64, } impl std::fmt::Debug for WasiEnv { @@ -215,11 +215,12 @@ impl Clone for WasiEnv { replaying_journal: self.replaying_journal, skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, - greenthreads: self.greenthreads.clone(), - current_greenthread_id: self.current_greenthread_id.clone(), + contexts: self.contexts.clone(), + current_context_id: self.current_context_id.clone(), // TODO: This is wrong; The two lines above as well - next_free_id: AtomicU64::new( - self.next_free_id.load(std::sync::atomic::Ordering::SeqCst), + next_available_context_id: AtomicU64::new( + self.next_available_context_id + .load(std::sync::atomic::Ordering::SeqCst), ), } } @@ -263,9 +264,9 @@ impl WasiEnv { skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, // TODO: Not sure if we can even properly fork coroutines at all - greenthreads: Arc::new(RwLock::new(BTreeMap::new())), - current_greenthread_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), - next_free_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), + contexts: Arc::new(RwLock::new(BTreeMap::new())), + current_context_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), + next_available_context_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), }; Ok((new_env, handle)) } @@ -410,9 +411,9 @@ impl WasiEnv { bin_factory: init.bin_factory, capabilities: init.capabilities, disable_fs_cleanup: false, - greenthreads: Arc::new(RwLock::new(BTreeMap::new())), - current_greenthread_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), - next_free_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), + contexts: Arc::new(RwLock::new(BTreeMap::new())), + current_context_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), + next_available_context_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), }; env.owned_handles.push(thread); diff --git a/lib/wasix/src/syscalls/wasix/continuation_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs similarity index 72% rename from lib/wasix/src/syscalls/wasix/continuation_switch.rs rename to lib/wasix/src/syscalls/wasix/context_switch.rs index 0f24cfde6f9..6d561e022ef 100644 --- a/lib/wasix/src/syscalls/wasix/continuation_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,7 +1,7 @@ use super::*; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use anyhow::Result; -pub use context::Greenthread; +pub use context::Context; use core::panic; use futures::task::LocalSpawnExt; use futures::{FutureExt, channel::oneshot}; @@ -19,22 +19,22 @@ mod context { use futures::{FutureExt, channel::oneshot}; use wasmer::RuntimeError; - pub struct Greenthread { + pub struct Context { resumer: Option>>, } - impl Greenthread { + impl Context { // Create a new non-suspended context pub fn new() -> Self { Self { resumer: None } } - // Lock this greenthread until resumed + // Lock this context until resumed // - // Panics if the greenthread is already locked + // Panics if the context is already locked pub fn suspend(&mut self) -> impl Future> + use<> { let (sender, receiver) = oneshot::channel(); if self.resumer.is_some() { - panic!("Switching from a greenthread that is already switched out"); + panic!("Switching from a context that is already switched out"); } self.resumer = Some(sender); receiver.map(|r| { @@ -42,26 +42,26 @@ mod context { Ok(v) => v, Err(_canceled) => { // TODO: Handle canceled properly - panic!("Greenthread was canceled"); + panic!("Context was canceled"); } } }) // TODO: Think about whether canceled should be handled } - // Allow this greenthread to be resumed + // Allow this context to be resumed pub fn resume(&mut self, value: Result<(), RuntimeError>) -> () { let resumer = self .resumer .take() - .expect("Resuming a greenthread that is not switched out"); + .expect("Resuming a context that is not switched out"); resumer.send(value).unwrap(); } } - impl Clone for Greenthread { + impl Clone for Context { fn clone(&self) -> Self { if self.resumer.is_some() { - panic!("Cannot clone a coroutine with a resumer"); + panic!("Cannot clone a context with a resumer"); } Self { resumer: None } } @@ -92,13 +92,10 @@ pub fn call_in_async_runtime<'a>( ) -> Result, RuntimeError> { let cloned_params = params.to_vec(); - let main_greenthread = Greenthread::new(); + let main_context = Context::new(); let env = ctx.data_mut(store); - env.greenthreads - .write() - .unwrap() - .insert(0, main_greenthread); + env.contexts.write().unwrap().insert(0, main_context); let mut localpool = futures::executor::LocalPool::new(); let local_spawner = localpool.spawner(); @@ -112,11 +109,11 @@ pub fn call_in_async_runtime<'a>( result } -/// ### `greenthread_delete()` +/// ### `context_delete()` #[instrument(level = "trace", skip(ctx), ret)] pub fn context_delete( mut ctx: FunctionEnvMut<'_, WasiEnv>, - coroutine: u64, + context_id: u64, ) -> Result { WasiEnv::do_pending_operations(&mut ctx)?; @@ -127,19 +124,19 @@ pub fn context_delete( Ok(Errno::Success) } -/// ### `greenthread_new()` +/// ### `context_new()` #[instrument(level = "trace", skip(ctx), ret)] pub fn context_new( mut ctx: FunctionEnvMut<'_, WasiEnv>, - new_coroutine_ptr: WasmPtr, + new_context_ptr: WasmPtr, entrypoint: u32, ) -> Result { WasiEnv::do_pending_operations(&mut ctx)?; let (data, mut store) = ctx.data_and_store_mut(); - let new_greenthread_id = data - .next_free_id + let new_context_id = data + .next_available_context_id .fetch_add(1, std::sync::atomic::Ordering::SeqCst); let function = data @@ -147,13 +144,13 @@ pub fn context_new( .indirect_function_table_lookup(&mut store, entrypoint) .expect("Function not found in table"); - let mut new_greenthread = Greenthread::new(); - let new_context_resume = new_greenthread.suspend(); + let mut new_context = Context::new(); + let new_context_resume = new_context.suspend(); - data.greenthreads + data.contexts .write() .unwrap() - .insert(new_greenthread_id, new_greenthread); + .insert(new_context_id, new_context); // SAFETY: This is fine if we can ensure that ??? // A: The future does not outlive the store @@ -161,45 +158,43 @@ pub fn context_new( let mut unsafe_static_store = unsafe { std::mem::transmute::<_, StoreMut<'static>>(store.as_store_mut()) }; - let greenthreads_arc = data.greenthreads.clone(); + let contexts_arc = data.contexts.clone(); spawn_local(async move { new_context_resume.await; let result = function.call_async(&mut unsafe_static_store, &[]).await; - let mut main_greenthread = greenthreads_arc.write().unwrap(); - let main_greenthread = main_greenthread.get_mut(&0).unwrap(); + let mut main_context = contexts_arc.write().unwrap(); + let main_context = main_context.get_mut(&0).unwrap(); match result { Err(e) => { - eprintln!("Context {} returned error {:?}", new_greenthread_id, e); - main_greenthread.resume(Err(e)); + eprintln!("Context {} returned error {:?}", new_context_id, e); + main_context.resume(Err(e)); } Ok(v) => { panic!( "Context {} returned a value ({:?}). This is not allowed for now", - new_greenthread_id, v + new_context_id, v ); // TODO: Handle this properly } } - // TODO: Delete own greenthread + // TODO: Delete own context }); let env = ctx.data(); let memory = unsafe { env.memory_view(&ctx) }; - new_coroutine_ptr - .write(&memory, new_greenthread_id) - .unwrap(); + new_context_ptr.write(&memory, new_context_id).unwrap(); Ok(Errno::Success) } -/// Switch to another coroutine +/// Switch to another context #[instrument(level = "trace", skip(ctx), ret)] pub fn context_switch( mut ctx: FunctionEnvMut, - next_greenthread_id: u64, + next_context_id: u64, ) -> impl Future> + Send + 'static + use<> { match WasiEnv::do_pending_operations(&mut ctx) { Ok(()) => {} @@ -211,30 +206,30 @@ pub fn context_switch( } let (data, _store) = ctx.data_and_store_mut(); - let current_greenthread_id = { - let mut current = data.current_greenthread_id.write().unwrap(); + let current_context_id = { + let mut current = data.current_context_id.write().unwrap(); let old = *current; - *current = next_greenthread_id; + *current = next_context_id; old }; - if current_greenthread_id == next_greenthread_id { + if current_context_id == next_context_id { panic!("Switching to self is not allowed"); } - let mut greenthreads = data.greenthreads.write().unwrap(); - let this_one = greenthreads.get_mut(¤t_greenthread_id).unwrap(); + let mut contexts = data.contexts.write().unwrap(); + let this_one = contexts.get_mut(¤t_context_id).unwrap(); let receiver_promise = this_one.suspend(); - let next_one = greenthreads.get_mut(&next_greenthread_id).unwrap(); + let next_one = contexts.get_mut(&next_context_id).unwrap(); next_one.resume(Ok(())); - let current_id_arc = data.current_greenthread_id.clone(); + let current_id_arc = data.current_context_id.clone(); async move { let result = receiver_promise.await; - *current_id_arc.write().unwrap() = current_greenthread_id; + *current_id_arc.write().unwrap() = current_context_id; // If we get relayed a trap, propagate it. Other wise return success result.and(Ok(Errno::Success)) diff --git a/lib/wasix/src/syscalls/wasix/mod.rs b/lib/wasix/src/syscalls/wasix/mod.rs index 1de1e5f12dd..a9278ebee57 100644 --- a/lib/wasix/src/syscalls/wasix/mod.rs +++ b/lib/wasix/src/syscalls/wasix/mod.rs @@ -4,7 +4,7 @@ mod chdir; mod closure_allocate; mod closure_free; mod closure_prepare; -pub mod continuation_switch; +pub mod context_switch; mod dl_invalid_handle; mod dlopen; mod dlsym; @@ -92,7 +92,7 @@ pub use chdir::*; pub use closure_allocate::*; pub use closure_free::*; pub use closure_prepare::*; -pub use continuation_switch::*; +pub use context_switch::*; pub use dl_invalid_handle::*; pub use dlopen::*; pub use dlsym::*; From f9d4b55259db149048097c8cb70cc97ddc6f9b1e Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 17 Nov 2025 13:13:17 +0100 Subject: [PATCH 004/114] Cleanup WASIX context switching api --- lib/wasix/src/bin_factory/exec.rs | 11 +- lib/wasix/src/state/env.rs | 11 +- lib/wasix/src/state/env/context.rs | 49 ++++++++ lib/wasix/src/state/env/local_spawn.rs | 93 ++++++++++++++++ lib/wasix/src/state/mod.rs | 5 +- .../src/syscalls/wasix/context_switch.rs | 105 ++---------------- 6 files changed, 168 insertions(+), 106 deletions(-) create mode 100644 lib/wasix/src/state/env/context.rs create mode 100644 lib/wasix/src/state/env/local_spawn.rs diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 3e235164d10..86ab19066fc 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -1,6 +1,5 @@ #![allow(clippy::result_large_err)] -use std::sync::Arc; - +use super::{BinaryPackage, BinaryPackageCommand}; use crate::{ RewindState, SpawnError, WasiError, WasiRuntimeError, os::task::{ @@ -14,16 +13,16 @@ use crate::{ TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, TaskWasmRunProperties, }, }, - syscalls::{call_in_async_runtime, rewind_ext}, + state::call_in_async_runtime, + syscalls::rewind_ext, }; +use crate::{Runtime, WasiEnv, WasiFunctionEnv}; +use std::sync::Arc; use tracing::*; use virtual_mio::block_on; use wasmer::{Function, Memory32, Memory64, Module, RuntimeError, Store, Value}; use wasmer_wasix_types::wasi::Errno; -use super::{BinaryPackage, BinaryPackageCommand}; -use crate::{Runtime, WasiEnv, WasiFunctionEnv}; - #[tracing::instrument(level = "trace", skip_all, fields(%name, package_id=%binary.id))] pub async fn spawn_exec( binary: BinaryPackage, diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 1bd35bbf94c..f8d2cafadc4 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -1,3 +1,6 @@ +pub mod context; +pub mod local_spawn; + use std::{ collections::{BTreeMap, HashMap}, ops::Deref, @@ -38,8 +41,10 @@ use crate::{ process::{WasiProcess, WasiProcessId}, thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, }, - syscalls::{platform_clock_time_get, wasix::context_switch::Context}, + state::env::local_spawn::ThreadLocalSpawner, + syscalls::platform_clock_time_get, }; +use context::Context; use wasmer_types::ModuleHash; pub use super::handles::*; @@ -186,6 +191,7 @@ pub struct WasiEnv { pub(crate) contexts: Arc>>, pub(crate) current_context_id: Arc>, pub(crate) next_available_context_id: AtomicU64, + pub(crate) current_spawner: Option, } impl std::fmt::Debug for WasiEnv { @@ -222,6 +228,7 @@ impl Clone for WasiEnv { self.next_available_context_id .load(std::sync::atomic::Ordering::SeqCst), ), + current_spawner: self.current_spawner.clone(), } } } @@ -267,6 +274,7 @@ impl WasiEnv { contexts: Arc::new(RwLock::new(BTreeMap::new())), current_context_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), next_available_context_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), + current_spawner: None, }; Ok((new_env, handle)) } @@ -414,6 +422,7 @@ impl WasiEnv { contexts: Arc::new(RwLock::new(BTreeMap::new())), current_context_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), next_available_context_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), + current_spawner: None, }; env.owned_handles.push(thread); diff --git a/lib/wasix/src/state/env/context.rs b/lib/wasix/src/state/env/context.rs new file mode 100644 index 00000000000..c3b43420579 --- /dev/null +++ b/lib/wasix/src/state/env/context.rs @@ -0,0 +1,49 @@ +use futures::{TryFutureExt, channel::oneshot}; +use wasmer::RuntimeError; + +#[derive(Default, Debug)] +pub struct Context { + resumer: Option>>, +} + +impl Context { + // Create a new non-suspended context + pub fn new() -> Self { + Default::default() + } + // Suspend this context + // + // Returns a Future that resolves when the context is resumed + // + // Panics if the context is already locked + pub fn suspend(&mut self) -> impl Future> + use<> { + let (sender, receiver) = oneshot::channel(); + if self.resumer.is_some() { + panic!("Switching from a context that is already switched out"); + } + self.resumer = Some(sender); + receiver.unwrap_or_else(|_canceled| { + // TODO: Handle canceled properly + // TODO: Think about whether canceled should be handled at all + todo!("Context was canceled. Cleanup not implemented yet so we just panic"); + }) + } + + // Allow this context to be resumed + pub fn resume(&mut self, value: Result<(), RuntimeError>) -> () { + let resumer = self + .resumer + .take() + .expect("Resuming a context that is not switched out"); + resumer.send(value).unwrap(); + } +} + +impl Clone for Context { + fn clone(&self) -> Self { + if self.resumer.is_some() { + panic!("Cannot clone a context with a resumer"); + } + Self { resumer: None } + } +} diff --git a/lib/wasix/src/state/env/local_spawn.rs b/lib/wasix/src/state/env/local_spawn.rs new file mode 100644 index 00000000000..9b4f72b20a2 --- /dev/null +++ b/lib/wasix/src/state/env/local_spawn.rs @@ -0,0 +1,93 @@ +use dashmap::DashMap; +use futures::task::LocalSpawnExt; +use std::sync::atomic::AtomicU64; +use wasmer::{RuntimeError, Store, Value}; + +use crate::{WasiFunctionEnv, state::env::context::Context}; + +thread_local! { + static NEXT_SPAWNER_ID: AtomicU64 = AtomicU64::new(0); + static LOCAL_SPAWNERS: DashMap = DashMap::new(); +} + +// Send, just a handle +#[derive(Clone, Debug)] +pub struct ThreadLocalSpawner { + id: u64, +} + +// Not send +pub struct ThreadLocalExecutor { + id: u64, + pool: futures::executor::LocalPool, +} + +impl ThreadLocalSpawner { + // Spawn a future onto the same thread as the local spawner + pub fn spawn_local + 'static>(&self, future: F) { + LOCAL_SPAWNERS.with(|runtimes| { + let spawner = runtimes + .get(&self.id) + .expect("Failed to find local spawner. Maybe you are on the wrong thread?"); + spawner + .spawn_local(future) + .expect("Failed to spawn local future"); + }); + } +} + +impl ThreadLocalExecutor { + fn new() -> (ThreadLocalSpawner, Self) { + let localpool = futures::executor::LocalPool::new(); + let local_spawner = localpool.spawner(); + let runtime_id = + NEXT_SPAWNER_ID.with(|id| id.fetch_add(1, std::sync::atomic::Ordering::Relaxed)); + LOCAL_SPAWNERS.with(|runtimes| { + runtimes.insert(runtime_id, local_spawner); + }); + ( + ThreadLocalSpawner { id: runtime_id }, + Self { + id: runtime_id, + pool: localpool, + }, + ) + } + + fn run_until(&mut self, future: F) -> F::Output { + self.pool.run_until(future) + } +} + +impl Drop for ThreadLocalExecutor { + fn drop(&mut self) { + LOCAL_SPAWNERS.with(|runtimes| { + runtimes.remove(&self.id); + }); + } +} + +pub fn call_in_async_runtime<'a>( + ctx: &WasiFunctionEnv, + mut store: &mut Store, + entrypoint: wasmer::Function, + params: &'a [wasmer::Value], +) -> Result, RuntimeError> { + let cloned_params = params.to_vec(); + let main_context = Context::new(); + let env = ctx.data_mut(&mut store); + env.contexts.write().unwrap().insert(0, main_context); + + // Set spawner in env + let (spawner, mut local_executor) = ThreadLocalExecutor::new(); + let previous_spawner = env.current_spawner.replace(spawner); + + // Run function with the spawner + let result = local_executor.run_until(entrypoint.call_async(&mut *store, &cloned_params)); + + // Reset to previous spawner + let env = ctx.data_mut(&mut store); + env.current_spawner = previous_spawner; + + result +} diff --git a/lib/wasix/src/state/mod.rs b/lib/wasix/src/state/mod.rs index 18637cee3e0..adef0305014 100644 --- a/lib/wasix/src/state/mod.rs +++ b/lib/wasix/src/state/mod.rs @@ -39,7 +39,10 @@ use wasmer_wasix_types::wasi::{ pub use self::{ builder::*, - env::{WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles}, + env::{ + WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles, context::Context, + local_spawn::call_in_async_runtime, + }, func_env::WasiFunctionEnv, types::*, }; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 6d561e022ef..963a149aa08 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,7 +1,6 @@ use super::*; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use anyhow::Result; -pub use context::Context; use core::panic; use futures::task::LocalSpawnExt; use futures::{FutureExt, channel::oneshot}; @@ -15,100 +14,6 @@ use wasmer::{ }; use wasmer::{StoreMut, Tag, Type}; -mod context { - use futures::{FutureExt, channel::oneshot}; - use wasmer::RuntimeError; - - pub struct Context { - resumer: Option>>, - } - - impl Context { - // Create a new non-suspended context - pub fn new() -> Self { - Self { resumer: None } - } - // Lock this context until resumed - // - // Panics if the context is already locked - pub fn suspend(&mut self) -> impl Future> + use<> { - let (sender, receiver) = oneshot::channel(); - if self.resumer.is_some() { - panic!("Switching from a context that is already switched out"); - } - self.resumer = Some(sender); - receiver.map(|r| { - match r { - Ok(v) => v, - Err(_canceled) => { - // TODO: Handle canceled properly - panic!("Context was canceled"); - } - } - }) - // TODO: Think about whether canceled should be handled - } - - // Allow this context to be resumed - pub fn resume(&mut self, value: Result<(), RuntimeError>) -> () { - let resumer = self - .resumer - .take() - .expect("Resuming a context that is not switched out"); - resumer.send(value).unwrap(); - } - } - impl Clone for Context { - fn clone(&self) -> Self { - if self.resumer.is_some() { - panic!("Cannot clone a context with a resumer"); - } - Self { resumer: None } - } - } -} - -thread_local! { - static LOCAL_SPAWNER: OnceLock = OnceLock::new(); -} -fn spawn_local(future: F) -where - F: std::future::Future + 'static, -{ - LOCAL_SPAWNER.with(|spawner_lock| { - let spawner = spawner_lock.get().expect("Local spawner not initialized"); - spawner - .spawn_local(future) - .expect("Failed to spawn local future"); - }); -} - -#[instrument(level = "trace", skip(ctx, store), ret)] -pub fn call_in_async_runtime<'a>( - ctx: &WasiFunctionEnv, - store: &mut Store, - entrypoint: wasmer::Function, - params: &'a [wasmer::Value], -) -> Result, RuntimeError> { - let cloned_params = params.to_vec(); - - let main_context = Context::new(); - - let env = ctx.data_mut(store); - env.contexts.write().unwrap().insert(0, main_context); - - let mut localpool = futures::executor::LocalPool::new(); - let local_spawner = localpool.spawner(); - LOCAL_SPAWNER.with(|spawner_lock| { - spawner_lock - .set(local_spawner) - .expect("Failed to set local spawner"); - }); - let result = localpool.run_until(entrypoint.call_async(&mut *store, &cloned_params)); - - result -} - /// ### `context_delete()` #[instrument(level = "trace", skip(ctx), ret)] pub fn context_delete( @@ -144,7 +49,7 @@ pub fn context_new( .indirect_function_table_lookup(&mut store, entrypoint) .expect("Function not found in table"); - let mut new_context = Context::new(); + let mut new_context = crate::state::Context::new(); let new_context_resume = new_context.suspend(); data.contexts @@ -160,7 +65,12 @@ pub fn context_new( let contexts_arc = data.contexts.clone(); - spawn_local(async move { + let spawner = data + .current_spawner + .clone() + .expect("No async spawner set on WasiEnv. Did you enter the async env before?"); + + spawner.spawn_local(async move { new_context_resume.await; let result = function.call_async(&mut unsafe_static_store, &[]).await; @@ -168,7 +78,6 @@ pub fn context_new( let main_context = main_context.get_mut(&0).unwrap(); match result { Err(e) => { - eprintln!("Context {} returned error {:?}", new_context_id, e); main_context.resume(Err(e)); } Ok(v) => { From bb702967430035847e47d811ba3d96f66328c7da Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 09:51:02 +0100 Subject: [PATCH 005/114] Continue cleaning up --- lib/wasix/src/state/env.rs | 7 +-- lib/wasix/src/state/env/context.rs | 49 ---------------- lib/wasix/src/state/env/local_spawn.rs | 6 +- lib/wasix/src/state/mod.rs | 2 +- .../src/syscalls/wasix/context_switch.rs | 56 +++++++++++-------- 5 files changed, 41 insertions(+), 79 deletions(-) delete mode 100644 lib/wasix/src/state/env/context.rs diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index f8d2cafadc4..f52044fee68 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -1,4 +1,3 @@ -pub mod context; pub mod local_spawn; use std::{ @@ -17,7 +16,7 @@ use virtual_mio::block_on; use virtual_net::DynVirtualNetworking; use wasmer::{ AsStoreMut, AsStoreRef, ExportError, FunctionEnvMut, Instance, Memory, MemoryType, MemoryView, - Module, + Module, RuntimeError, }; use wasmer_config::package::PackageSource; use wasmer_wasix_types::{ @@ -44,7 +43,6 @@ use crate::{ state::env::local_spawn::ThreadLocalSpawner, syscalls::platform_clock_time_get, }; -use context::Context; use wasmer_types::ModuleHash; pub use super::handles::*; @@ -188,7 +186,8 @@ pub struct WasiEnv { inner: WasiInstanceHandlesPointer, /// TODO: Document these fields - pub(crate) contexts: Arc>>, + pub(crate) contexts: + Arc>>>>, pub(crate) current_context_id: Arc>, pub(crate) next_available_context_id: AtomicU64, pub(crate) current_spawner: Option, diff --git a/lib/wasix/src/state/env/context.rs b/lib/wasix/src/state/env/context.rs deleted file mode 100644 index c3b43420579..00000000000 --- a/lib/wasix/src/state/env/context.rs +++ /dev/null @@ -1,49 +0,0 @@ -use futures::{TryFutureExt, channel::oneshot}; -use wasmer::RuntimeError; - -#[derive(Default, Debug)] -pub struct Context { - resumer: Option>>, -} - -impl Context { - // Create a new non-suspended context - pub fn new() -> Self { - Default::default() - } - // Suspend this context - // - // Returns a Future that resolves when the context is resumed - // - // Panics if the context is already locked - pub fn suspend(&mut self) -> impl Future> + use<> { - let (sender, receiver) = oneshot::channel(); - if self.resumer.is_some() { - panic!("Switching from a context that is already switched out"); - } - self.resumer = Some(sender); - receiver.unwrap_or_else(|_canceled| { - // TODO: Handle canceled properly - // TODO: Think about whether canceled should be handled at all - todo!("Context was canceled. Cleanup not implemented yet so we just panic"); - }) - } - - // Allow this context to be resumed - pub fn resume(&mut self, value: Result<(), RuntimeError>) -> () { - let resumer = self - .resumer - .take() - .expect("Resuming a context that is not switched out"); - resumer.send(value).unwrap(); - } -} - -impl Clone for Context { - fn clone(&self) -> Self { - if self.resumer.is_some() { - panic!("Cannot clone a context with a resumer"); - } - Self { resumer: None } - } -} diff --git a/lib/wasix/src/state/env/local_spawn.rs b/lib/wasix/src/state/env/local_spawn.rs index 9b4f72b20a2..373e2fab000 100644 --- a/lib/wasix/src/state/env/local_spawn.rs +++ b/lib/wasix/src/state/env/local_spawn.rs @@ -3,7 +3,7 @@ use futures::task::LocalSpawnExt; use std::sync::atomic::AtomicU64; use wasmer::{RuntimeError, Store, Value}; -use crate::{WasiFunctionEnv, state::env::context::Context}; +use crate::WasiFunctionEnv; thread_local! { static NEXT_SPAWNER_ID: AtomicU64 = AtomicU64::new(0); @@ -67,6 +67,7 @@ impl Drop for ThreadLocalExecutor { } } +// TODO: This does not belong here pub fn call_in_async_runtime<'a>( ctx: &WasiFunctionEnv, mut store: &mut Store, @@ -74,9 +75,8 @@ pub fn call_in_async_runtime<'a>( params: &'a [wasmer::Value], ) -> Result, RuntimeError> { let cloned_params = params.to_vec(); - let main_context = Context::new(); let env = ctx.data_mut(&mut store); - env.contexts.write().unwrap().insert(0, main_context); + // TODO: Ensure there is only one executor at a time? // Set spawner in env let (spawner, mut local_executor) = ThreadLocalExecutor::new(); diff --git a/lib/wasix/src/state/mod.rs b/lib/wasix/src/state/mod.rs index adef0305014..abbeb2c8235 100644 --- a/lib/wasix/src/state/mod.rs +++ b/lib/wasix/src/state/mod.rs @@ -40,7 +40,7 @@ use wasmer_wasix_types::wasi::{ pub use self::{ builder::*, env::{ - WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles, context::Context, + WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles, local_spawn::call_in_async_runtime, }, func_env::WasiFunctionEnv, diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 963a149aa08..b2a75058e2e 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -2,9 +2,9 @@ use super::*; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use anyhow::Result; use core::panic; +use futures::TryFutureExt; use futures::task::LocalSpawnExt; use futures::{FutureExt, channel::oneshot}; -use rkyv::vec; use std::collections::BTreeMap; use std::sync::atomic::AtomicU32; use std::sync::{Arc, OnceLock, RwLock}; @@ -39,48 +39,50 @@ pub fn context_new( WasiEnv::do_pending_operations(&mut ctx)?; let (data, mut store) = ctx.data_and_store_mut(); - let new_context_id = data .next_available_context_id .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - let function = data + let entrypoint = data .inner() .indirect_function_table_lookup(&mut store, entrypoint) .expect("Function not found in table"); - let mut new_context = crate::state::Context::new(); - let new_context_resume = new_context.suspend(); - - data.contexts + // Setup sender and receiver for the new context + let (sender, receiver) = oneshot::channel::>(); + let wait_for_resume = receiver.unwrap_or_else(|_canceled| { + // TODO: Handle canceled properly + todo!("Context was canceled. Cleanup not implemented yet so we just panic"); + }); + let mut contexts = data + .contexts .write() .unwrap() - .insert(new_context_id, new_context); + .insert(new_context_id, sender); // SAFETY: This is fine if we can ensure that ??? // A: The future does not outlive the store // B: we now have multiple mutable references, this is dangerous let mut unsafe_static_store = unsafe { std::mem::transmute::<_, StoreMut<'static>>(store.as_store_mut()) }; - - let contexts_arc = data.contexts.clone(); + let contexts_cloned = data.contexts.clone(); let spawner = data .current_spawner .clone() .expect("No async spawner set on WasiEnv. Did you enter the async env before?"); - spawner.spawn_local(async move { - new_context_resume.await; - let result = function.call_async(&mut unsafe_static_store, &[]).await; + wait_for_resume.await; + let result = entrypoint.call_async(&mut unsafe_static_store, &[]).await; - let mut main_context = contexts_arc.write().unwrap(); - let main_context = main_context.get_mut(&0).unwrap(); + let mut main_context = contexts_cloned.write().unwrap(); + let main_context = main_context.remove(&0).unwrap(); match result { Err(e) => { - main_context.resume(Err(e)); + main_context.send(Err(e)); } Ok(v) => { + // If we get here, something went wrong, as the entrypoint function is not supposed to return panic!( "Context {} returned a value ({:?}). This is not allowed for now", new_context_id, v @@ -126,17 +128,27 @@ pub fn context_switch( panic!("Switching to self is not allowed"); } - let mut contexts = data.contexts.write().unwrap(); - let this_one = contexts.get_mut(¤t_context_id).unwrap(); - let receiver_promise = this_one.suspend(); + let (sender, receiver) = oneshot::channel::>(); + // if self.resumer.is_some() { + // panic!("Switching from a context that is already switched out"); + // } + let wait_for_resume = receiver.unwrap_or_else(|_canceled| { + // TODO: Handle canceled properly + todo!("Context was canceled. Cleanup not implemented yet so we just panic"); + }); - let next_one = contexts.get_mut(&next_context_id).unwrap(); - next_one.resume(Ok(())); + // let this_one = contexts.get_mut(¤t_context_id).unwrap(); + // let receiver_promise = this_one.suspend(); + + let mut contexts = data.contexts.write().unwrap(); + contexts.insert(current_context_id, sender); + let next_one = contexts.remove(&next_context_id).unwrap(); + next_one.send(Ok(())); let current_id_arc = data.current_context_id.clone(); async move { - let result = receiver_promise.await; + let result = wait_for_resume.await; *current_id_arc.write().unwrap() = current_context_id; From eac75be5b05c585d1e3843d28bd931a73ef2f05b Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 10:08:57 +0100 Subject: [PATCH 006/114] Add error type to spawn_local --- lib/wasix/src/state/env/local_spawn.rs | 46 ++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/wasix/src/state/env/local_spawn.rs b/lib/wasix/src/state/env/local_spawn.rs index 373e2fab000..c678f47d6b2 100644 --- a/lib/wasix/src/state/env/local_spawn.rs +++ b/lib/wasix/src/state/env/local_spawn.rs @@ -1,6 +1,7 @@ use dashmap::DashMap; use futures::task::LocalSpawnExt; -use std::sync::atomic::AtomicU64; +use std::{sync::atomic::AtomicU64, thread::ThreadId}; +use thiserror::Error; use wasmer::{RuntimeError, Store, Value}; use crate::WasiFunctionEnv; @@ -14,6 +15,24 @@ thread_local! { #[derive(Clone, Debug)] pub struct ThreadLocalSpawner { id: u64, + /// The thread this spawner is associated with + /// + /// Used to generate better error messages when trying to spawn on the wrong thread + thread: ThreadId, +} + +#[derive(Debug, Error)] +pub enum ThreadLocalSpawnerError { + #[error( + "Trying to spawn on a different thread than the one the ThreadLocalSpawner was created on. Expected: {expected:?}, found: {found:?}" + )] + NotOnTheCorrectThread { expected: ThreadId, found: ThreadId }, + #[error( + "The local executor associated with this spawner has been shut down and cannot accept new tasks" + )] + LocalPoolShutDown, + #[error("An error occurred while spawning the task")] + SpawnError, } // Not send @@ -23,16 +42,28 @@ pub struct ThreadLocalExecutor { } impl ThreadLocalSpawner { - // Spawn a future onto the same thread as the local spawner - pub fn spawn_local + 'static>(&self, future: F) { + /// Spawn a future onto the same thread as the local spawner + /// + /// Needs to be called from the same thread as the spawner was created on + pub fn spawn_local + 'static>( + &self, + future: F, + ) -> Result<(), ThreadLocalSpawnerError> { + if std::thread::current().id() != self.thread { + return Err(ThreadLocalSpawnerError::NotOnTheCorrectThread { + expected: self.thread, + found: std::thread::current().id(), + }); + } LOCAL_SPAWNERS.with(|runtimes| { let spawner = runtimes .get(&self.id) - .expect("Failed to find local spawner. Maybe you are on the wrong thread?"); + .ok_or(ThreadLocalSpawnerError::LocalPoolShutDown)?; spawner .spawn_local(future) - .expect("Failed to spawn local future"); + .map_err(|_| ThreadLocalSpawnerError::SpawnError)?; }); + Ok(()) } } @@ -46,7 +77,10 @@ impl ThreadLocalExecutor { runtimes.insert(runtime_id, local_spawner); }); ( - ThreadLocalSpawner { id: runtime_id }, + ThreadLocalSpawner { + id: runtime_id, + thread: std::thread::current().id(), + }, Self { id: runtime_id, pool: localpool, From 3da8e7749ea56a682866da70aaebca8467c6edd1 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 10:37:14 +0100 Subject: [PATCH 007/114] Refactor ThreadLocalExecutor to not use a thread-local DashMap --- lib/wasix/src/state/env/local_spawn.rs | 119 +++++++++++++++---------- 1 file changed, 70 insertions(+), 49 deletions(-) diff --git a/lib/wasix/src/state/env/local_spawn.rs b/lib/wasix/src/state/env/local_spawn.rs index c678f47d6b2..cdb580207b5 100644 --- a/lib/wasix/src/state/env/local_spawn.rs +++ b/lib/wasix/src/state/env/local_spawn.rs @@ -1,26 +1,36 @@ -use dashmap::DashMap; -use futures::task::LocalSpawnExt; -use std::{sync::atomic::AtomicU64, thread::ThreadId}; +use crate::WasiFunctionEnv; +use futures::{ + executor::{LocalPool, LocalSpawner}, + task::LocalSpawnExt, +}; +use std::{ + sync::{Arc, Mutex, Weak}, + thread::ThreadId, +}; use thiserror::Error; use wasmer::{RuntimeError, Store, Value}; -use crate::WasiFunctionEnv; - -thread_local! { - static NEXT_SPAWNER_ID: AtomicU64 = AtomicU64::new(0); - static LOCAL_SPAWNERS: DashMap = DashMap::new(); -} - -// Send, just a handle +/// A `Send`able spawner that spawns onto a thread-local executor +/// +/// Despite being `Send`, the spawner enforces at runtime that +/// it is only used to spawn on the thread it was created on. +// +// If that limitation is a problem, we can consider implementing a version that +// accepts `Send` futures and sends them to the correct thread via channels. #[derive(Clone, Debug)] pub struct ThreadLocalSpawner { - id: u64, + /// A reference to the local executor's spawner + pool: Weak>>, /// The thread this spawner is associated with /// /// Used to generate better error messages when trying to spawn on the wrong thread thread: ThreadId, } +// SAFETY: The ThreadLocalSpawner enforces using the spawner on the thread it was created on +// through runtime checks. See the safety comment in spawn_local. +unsafe impl Send for ThreadLocalSpawner {} + #[derive(Debug, Error)] pub enum ThreadLocalSpawnerError { #[error( @@ -35,57 +45,61 @@ pub enum ThreadLocalSpawnerError { SpawnError, } -// Not send -pub struct ThreadLocalExecutor { - id: u64, - pool: futures::executor::LocalPool, -} - impl ThreadLocalSpawner { /// Spawn a future onto the same thread as the local spawner /// - /// Needs to be called from the same thread as the spawner was created on + /// Needs to be called from the same thread on which the associated executor was created pub fn spawn_local + 'static>( &self, future: F, ) -> Result<(), ThreadLocalSpawnerError> { + // SAFETY: This is what makes implementing Send on ThreadLocalSpawner safe. We ensure that we only spawn + // on the same thread as the one the spawner was created on. if std::thread::current().id() != self.thread { return Err(ThreadLocalSpawnerError::NotOnTheCorrectThread { expected: self.thread, found: std::thread::current().id(), }); } - LOCAL_SPAWNERS.with(|runtimes| { - let spawner = runtimes - .get(&self.id) - .ok_or(ThreadLocalSpawnerError::LocalPoolShutDown)?; - spawner - .spawn_local(future) - .map_err(|_| ThreadLocalSpawnerError::SpawnError)?; - }); + + let spawner = self + .pool + .upgrade() + .ok_or(ThreadLocalSpawnerError::LocalPoolShutDown)?; + // Unwrap on the mutex, as it should never be poisoned + let spawner = spawner.lock().unwrap(); + let spawner = spawner + .as_ref() + .ok_or(ThreadLocalSpawnerError::LocalPoolShutDown)?; + + spawner + .spawn_local(future) + .map_err(|_| ThreadLocalSpawnerError::SpawnError); Ok(()) } } +/// A thread-local executor that can run tasks on the current thread +pub struct ThreadLocalExecutor { + pool: LocalPool, + spawner: Arc>>, +} + impl ThreadLocalExecutor { - fn new() -> (ThreadLocalSpawner, Self) { - let localpool = futures::executor::LocalPool::new(); - let local_spawner = localpool.spawner(); - let runtime_id = - NEXT_SPAWNER_ID.with(|id| id.fetch_add(1, std::sync::atomic::Ordering::Relaxed)); - LOCAL_SPAWNERS.with(|runtimes| { - runtimes.insert(runtime_id, local_spawner); - }); - ( - ThreadLocalSpawner { - id: runtime_id, - thread: std::thread::current().id(), - }, - Self { - id: runtime_id, - pool: localpool, - }, - ) + fn new() -> Self { + let local_pool = futures::executor::LocalPool::new(); + let local_spawner = Arc::new(Mutex::new(Some(local_pool.spawner()))); + Self { + pool: local_pool, + spawner: local_spawner, + } + } + + fn get_spawner(&self) -> ThreadLocalSpawner { + ThreadLocalSpawner { + pool: Arc::downgrade(&self.spawner), + thread: std::thread::current().id(), + } } fn run_until(&mut self, future: F) -> F::Output { @@ -95,9 +109,15 @@ impl ThreadLocalExecutor { impl Drop for ThreadLocalExecutor { fn drop(&mut self) { - LOCAL_SPAWNERS.with(|runtimes| { - runtimes.remove(&self.id); - }); + // Remove the spawner so no new tasks can be spawned + // + // This is technically not necessary, as the Weak upgrading in + // LocalSpawner does a similar thing, but if the Weak was upgraded + // before dropping the Executor this could lead to a SpawnError + // instead of a LocalPoolShutDown. We can differentiate "real" + // SpawnErrors from "dropped executor" errors this way + let mut spawner = self.spawner.lock().unwrap(); + *spawner = None; } } @@ -113,7 +133,8 @@ pub fn call_in_async_runtime<'a>( // TODO: Ensure there is only one executor at a time? // Set spawner in env - let (spawner, mut local_executor) = ThreadLocalExecutor::new(); + let mut local_executor = ThreadLocalExecutor::new(); + let spawner = local_executor.get_spawner(); let previous_spawner = env.current_spawner.replace(spawner); // Run function with the spawner From 4bed1e6022564a5d4ed4f724e081090a501a551c Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 10:49:29 +0100 Subject: [PATCH 008/114] Refactor local_spawn to use an Rc instead of an Arc --- lib/wasix/src/state/env/local_spawn.rs | 43 ++++++++++---------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/lib/wasix/src/state/env/local_spawn.rs b/lib/wasix/src/state/env/local_spawn.rs index cdb580207b5..b5cc68225e2 100644 --- a/lib/wasix/src/state/env/local_spawn.rs +++ b/lib/wasix/src/state/env/local_spawn.rs @@ -3,10 +3,7 @@ use futures::{ executor::{LocalPool, LocalSpawner}, task::LocalSpawnExt, }; -use std::{ - sync::{Arc, Mutex, Weak}, - thread::ThreadId, -}; +use std::{cell::RefCell, rc::Rc, thread::ThreadId}; use thiserror::Error; use wasmer::{RuntimeError, Store, Value}; @@ -20,7 +17,7 @@ use wasmer::{RuntimeError, Store, Value}; #[derive(Clone, Debug)] pub struct ThreadLocalSpawner { /// A reference to the local executor's spawner - pool: Weak>>, + pool: Rc>>, /// The thread this spawner is associated with /// /// Used to generate better error messages when trying to spawn on the wrong thread @@ -62,17 +59,12 @@ impl ThreadLocalSpawner { }); } - let spawner = self - .pool - .upgrade() - .ok_or(ThreadLocalSpawnerError::LocalPoolShutDown)?; - // Unwrap on the mutex, as it should never be poisoned - let spawner = spawner.lock().unwrap(); - let spawner = spawner - .as_ref() - .ok_or(ThreadLocalSpawnerError::LocalPoolShutDown)?; - + // As we now know that we are on the correct thread, we can borrow_mut safely + let mut spawner = self.pool.borrow_mut(); + // spawn_local does not run the future immediately, it just schedules it, so this is fine spawner + .as_mut() + .ok_or(ThreadLocalSpawnerError::LocalPoolShutDown)? .spawn_local(future) .map_err(|_| ThreadLocalSpawnerError::SpawnError); Ok(()) @@ -81,14 +73,16 @@ impl ThreadLocalSpawner { /// A thread-local executor that can run tasks on the current thread pub struct ThreadLocalExecutor { + /// The local pool pool: LocalPool, - spawner: Arc>>, + /// A reference to the spawner. This reference is shared with all spawners created from this executor + spawner: Rc>>, } impl ThreadLocalExecutor { fn new() -> Self { let local_pool = futures::executor::LocalPool::new(); - let local_spawner = Arc::new(Mutex::new(Some(local_pool.spawner()))); + let local_spawner = Rc::new(RefCell::new(Some(local_pool.spawner()))); Self { pool: local_pool, spawner: local_spawner, @@ -97,7 +91,8 @@ impl ThreadLocalExecutor { fn get_spawner(&self) -> ThreadLocalSpawner { ThreadLocalSpawner { - pool: Arc::downgrade(&self.spawner), + pool: self.spawner.clone(), + // SAFETY: This will always be the thread where the spawner was created on, as the ThreadLocalExecutor is not Send thread: std::thread::current().id(), } } @@ -109,15 +104,9 @@ impl ThreadLocalExecutor { impl Drop for ThreadLocalExecutor { fn drop(&mut self) { - // Remove the spawner so no new tasks can be spawned - // - // This is technically not necessary, as the Weak upgrading in - // LocalSpawner does a similar thing, but if the Weak was upgraded - // before dropping the Executor this could lead to a SpawnError - // instead of a LocalPoolShutDown. We can differentiate "real" - // SpawnErrors from "dropped executor" errors this way - let mut spawner = self.spawner.lock().unwrap(); - *spawner = None; + // Remove the spawner so the spawners can detect that the executor has been shut down + // If we don't do this, they will just get an opaque SpawnError + *self.spawner.borrow_mut() = None; } } From 9e85d96d36e507bee61cc9172fde0f71c83ead94 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 10:58:30 +0100 Subject: [PATCH 009/114] Further simplify ThreadLocalExecutor --- lib/wasix/src/state/env/local_spawn.rs | 39 ++++++++------------------ 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/lib/wasix/src/state/env/local_spawn.rs b/lib/wasix/src/state/env/local_spawn.rs index b5cc68225e2..392a78316a5 100644 --- a/lib/wasix/src/state/env/local_spawn.rs +++ b/lib/wasix/src/state/env/local_spawn.rs @@ -3,7 +3,7 @@ use futures::{ executor::{LocalPool, LocalSpawner}, task::LocalSpawnExt, }; -use std::{cell::RefCell, rc::Rc, thread::ThreadId}; +use std::thread::ThreadId; use thiserror::Error; use wasmer::{RuntimeError, Store, Value}; @@ -17,13 +17,12 @@ use wasmer::{RuntimeError, Store, Value}; #[derive(Clone, Debug)] pub struct ThreadLocalSpawner { /// A reference to the local executor's spawner - pool: Rc>>, + spawner: LocalSpawner, /// The thread this spawner is associated with /// /// Used to generate better error messages when trying to spawn on the wrong thread thread: ThreadId, } - // SAFETY: The ThreadLocalSpawner enforces using the spawner on the thread it was created on // through runtime checks. See the safety comment in spawn_local. unsafe impl Send for ThreadLocalSpawner {} @@ -31,7 +30,7 @@ unsafe impl Send for ThreadLocalSpawner {} #[derive(Debug, Error)] pub enum ThreadLocalSpawnerError { #[error( - "Trying to spawn on a different thread than the one the ThreadLocalSpawner was created on. Expected: {expected:?}, found: {found:?}" + "The ThreadLocalSpawner can only spawn tasks on the thread it was created on. Expected to be on {expected:?} but was actually on {found:?}" )] NotOnTheCorrectThread { expected: ThreadId, found: ThreadId }, #[error( @@ -59,15 +58,13 @@ impl ThreadLocalSpawner { }); } - // As we now know that we are on the correct thread, we can borrow_mut safely - let mut spawner = self.pool.borrow_mut(); - // spawn_local does not run the future immediately, it just schedules it, so this is fine - spawner - .as_mut() - .ok_or(ThreadLocalSpawnerError::LocalPoolShutDown)? + // As we now know that we are on the correct thread, we can use the spawner safely + self.spawner .spawn_local(future) - .map_err(|_| ThreadLocalSpawnerError::SpawnError); - Ok(()) + .map_err(|e| match e.is_shutdown() { + true => ThreadLocalSpawnerError::LocalPoolShutDown, + false => ThreadLocalSpawnerError::SpawnError, + }) } } @@ -75,23 +72,17 @@ impl ThreadLocalSpawner { pub struct ThreadLocalExecutor { /// The local pool pool: LocalPool, - /// A reference to the spawner. This reference is shared with all spawners created from this executor - spawner: Rc>>, } impl ThreadLocalExecutor { fn new() -> Self { let local_pool = futures::executor::LocalPool::new(); - let local_spawner = Rc::new(RefCell::new(Some(local_pool.spawner()))); - Self { - pool: local_pool, - spawner: local_spawner, - } + Self { pool: local_pool } } fn get_spawner(&self) -> ThreadLocalSpawner { ThreadLocalSpawner { - pool: self.spawner.clone(), + spawner: self.pool.spawner(), // SAFETY: This will always be the thread where the spawner was created on, as the ThreadLocalExecutor is not Send thread: std::thread::current().id(), } @@ -102,14 +93,6 @@ impl ThreadLocalExecutor { } } -impl Drop for ThreadLocalExecutor { - fn drop(&mut self) { - // Remove the spawner so the spawners can detect that the executor has been shut down - // If we don't do this, they will just get an opaque SpawnError - *self.spawner.borrow_mut() = None; - } -} - // TODO: This does not belong here pub fn call_in_async_runtime<'a>( ctx: &WasiFunctionEnv, From 9c41830e760a27a6500addcd7d67e2a4a80064eb Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 11:08:51 +0100 Subject: [PATCH 010/114] Move ThreadLocalExecutor to utils --- lib/wasix/src/bin_factory/exec.rs | 2 +- lib/wasix/src/state/env.rs | 44 +++++++++---------- lib/wasix/src/state/mod.rs | 5 +-- lib/wasix/src/utils/mod.rs | 1 + .../thread_local_executor.rs} | 22 ++++++---- 5 files changed, 36 insertions(+), 38 deletions(-) rename lib/wasix/src/{state/env/local_spawn.rs => utils/thread_local_executor.rs} (82%) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 86ab19066fc..4cce986d3e7 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -13,8 +13,8 @@ use crate::{ TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, TaskWasmRunProperties, }, }, - state::call_in_async_runtime, syscalls::rewind_ext, + utils::thread_local_executor::call_in_async_runtime, }; use crate::{Runtime, WasiEnv, WasiFunctionEnv}; use std::sync::Arc; diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index f52044fee68..b38bf1e22c5 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -1,5 +1,22 @@ -pub mod local_spawn; - +#[cfg(feature = "journal")] +use crate::journal::{DynJournal, JournalEffector, SnapshotTrigger}; +use crate::{ + Runtime, VirtualTaskManager, WasiControlPlane, WasiEnvBuilder, WasiError, WasiFunctionEnv, + WasiResult, WasiRuntimeError, WasiStateCreationError, WasiThreadError, WasiVFork, + bin_factory::{BinFactory, BinaryPackage, BinaryPackageCommand}, + capabilities::Capabilities, + fs::{WasiFsRoot, WasiInodes}, + import_object_for_all_wasi_versions, + os::task::{ + control_plane::ControlPlaneError, + process::{WasiProcess, WasiProcessId}, + thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, + }, + syscalls::platform_clock_time_get, + utils::thread_local_executor::ThreadLocalSpawner, +}; +use futures::future::BoxFuture; +use rand::Rng; use std::{ collections::{BTreeMap, HashMap}, ops::Deref, @@ -8,9 +25,6 @@ use std::{ sync::{Arc, RwLock, atomic::AtomicU64}, time::Duration, }; - -use futures::future::BoxFuture; -use rand::Rng; use virtual_fs::{FileSystem, FsError, VirtualFile}; use virtual_mio::block_on; use virtual_net::DynVirtualNetworking; @@ -19,6 +33,7 @@ use wasmer::{ Module, RuntimeError, }; use wasmer_config::package::PackageSource; +use wasmer_types::ModuleHash; use wasmer_wasix_types::{ types::Signal, wasi::{Errno, ExitCode, Snapshot0Clockid}, @@ -26,25 +41,6 @@ use wasmer_wasix_types::{ }; use webc::metadata::annotations::Wasi; -#[cfg(feature = "journal")] -use crate::journal::{DynJournal, JournalEffector, SnapshotTrigger}; -use crate::{ - Runtime, VirtualTaskManager, WasiControlPlane, WasiEnvBuilder, WasiError, WasiFunctionEnv, - WasiResult, WasiRuntimeError, WasiStateCreationError, WasiThreadError, WasiVFork, - bin_factory::{BinFactory, BinaryPackage, BinaryPackageCommand}, - capabilities::Capabilities, - fs::{WasiFsRoot, WasiInodes}, - import_object_for_all_wasi_versions, - os::task::{ - control_plane::ControlPlaneError, - process::{WasiProcess, WasiProcessId}, - thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, - }, - state::env::local_spawn::ThreadLocalSpawner, - syscalls::platform_clock_time_get, -}; -use wasmer_types::ModuleHash; - pub use super::handles::*; use super::{Linker, WasiState, conv_env_vars}; diff --git a/lib/wasix/src/state/mod.rs b/lib/wasix/src/state/mod.rs index abbeb2c8235..18637cee3e0 100644 --- a/lib/wasix/src/state/mod.rs +++ b/lib/wasix/src/state/mod.rs @@ -39,10 +39,7 @@ use wasmer_wasix_types::wasi::{ pub use self::{ builder::*, - env::{ - WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles, - local_spawn::call_in_async_runtime, - }, + env::{WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles}, func_env::WasiFunctionEnv, types::*, }; diff --git a/lib/wasix/src/utils/mod.rs b/lib/wasix/src/utils/mod.rs index 9fc874d4a61..b9017cc4097 100644 --- a/lib/wasix/src/utils/mod.rs +++ b/lib/wasix/src/utils/mod.rs @@ -1,6 +1,7 @@ mod dummy_waker; mod owned_mutex_guard; pub mod store; +pub mod thread_local_executor; mod thread_parker; #[cfg(feature = "js")] diff --git a/lib/wasix/src/state/env/local_spawn.rs b/lib/wasix/src/utils/thread_local_executor.rs similarity index 82% rename from lib/wasix/src/state/env/local_spawn.rs rename to lib/wasix/src/utils/thread_local_executor.rs index 392a78316a5..a06b00ccc70 100644 --- a/lib/wasix/src/state/env/local_spawn.rs +++ b/lib/wasix/src/utils/thread_local_executor.rs @@ -15,7 +15,7 @@ use wasmer::{RuntimeError, Store, Value}; // If that limitation is a problem, we can consider implementing a version that // accepts `Send` futures and sends them to the correct thread via channels. #[derive(Clone, Debug)] -pub struct ThreadLocalSpawner { +pub(crate) struct ThreadLocalSpawner { /// A reference to the local executor's spawner spawner: LocalSpawner, /// The thread this spawner is associated with @@ -23,10 +23,14 @@ pub struct ThreadLocalSpawner { /// Used to generate better error messages when trying to spawn on the wrong thread thread: ThreadId, } -// SAFETY: The ThreadLocalSpawner enforces using the spawner on the thread it was created on -// through runtime checks. See the safety comment in spawn_local. +// SAFETY: The ThreadLocalSpawner enforces the spawner is only used on the correct thread. +// See the safety comment in ThreadLocalSpawner::spawn_local and ThreadLocalSpawner::spawner unsafe impl Send for ThreadLocalSpawner {} +// SAFETY: The ThreadLocalSpawner enforces the spawner is only used on the correct thread. +// See the safety comment in ThreadLocalSpawner::spawn_local and ThreadLocalSpawner::spawner +unsafe impl Sync for ThreadLocalSpawner {} +/// Errors that can occur during `spawn_local` calls #[derive(Debug, Error)] pub enum ThreadLocalSpawnerError { #[error( @@ -45,7 +49,7 @@ impl ThreadLocalSpawner { /// Spawn a future onto the same thread as the local spawner /// /// Needs to be called from the same thread on which the associated executor was created - pub fn spawn_local + 'static>( + pub(crate) fn spawn_local + 'static>( &self, future: F, ) -> Result<(), ThreadLocalSpawnerError> { @@ -69,18 +73,18 @@ impl ThreadLocalSpawner { } /// A thread-local executor that can run tasks on the current thread -pub struct ThreadLocalExecutor { +pub(crate) struct ThreadLocalExecutor { /// The local pool pool: LocalPool, } impl ThreadLocalExecutor { - fn new() -> Self { + pub(crate) fn new() -> Self { let local_pool = futures::executor::LocalPool::new(); Self { pool: local_pool } } - fn get_spawner(&self) -> ThreadLocalSpawner { + pub(crate) fn spawner(&self) -> ThreadLocalSpawner { ThreadLocalSpawner { spawner: self.pool.spawner(), // SAFETY: This will always be the thread where the spawner was created on, as the ThreadLocalExecutor is not Send @@ -88,7 +92,7 @@ impl ThreadLocalExecutor { } } - fn run_until(&mut self, future: F) -> F::Output { + pub(crate) fn run_until(&mut self, future: F) -> F::Output { self.pool.run_until(future) } } @@ -106,7 +110,7 @@ pub fn call_in_async_runtime<'a>( // Set spawner in env let mut local_executor = ThreadLocalExecutor::new(); - let spawner = local_executor.get_spawner(); + let spawner = local_executor.spawner(); let previous_spawner = env.current_spawner.replace(spawner); // Run function with the spawner From 4b44b57c886abff2cc54d663bc13b05b0578a4d3 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 11:23:23 +0100 Subject: [PATCH 011/114] Move call function to exec --- lib/wasix/src/bin_factory/exec.rs | 31 ++++++++++++++++++-- lib/wasix/src/utils/thread_local_executor.rs | 28 ------------------ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 4cce986d3e7..2c0c8051580 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -14,7 +14,7 @@ use crate::{ }, }, syscalls::rewind_ext, - utils::thread_local_executor::call_in_async_runtime, + utils::thread_local_executor::ThreadLocalExecutor, }; use crate::{Runtime, WasiEnv, WasiFunctionEnv}; use std::sync::Arc; @@ -241,6 +241,33 @@ fn get_start(ctx: &WasiFunctionEnv, store: &Store) -> Option { .ok() } +// TODO: Figure out a better place for this function +// Calls an async wasm in a blocking context +// +// This call blocks until the entrypoint returns, or it or any of the contexts it spawns traps +fn call_with_threadlocal_executor( + ctx: &WasiFunctionEnv, + mut store: &mut Store, + entrypoint: wasmer::Function, + params: Vec, +) -> Result, RuntimeError> { + // Create a new executor + let mut local_executor = ThreadLocalExecutor::new(); + + // Put the spawner into the WASI env, so that syscalls can use it to queue up new tasks + let spawner = local_executor.spawner(); + let previous_spawner = ctx.data_mut(&mut store).current_spawner.replace(spawner); + + // Run function with the spawner + let result = local_executor.run_until(entrypoint.call_async(&mut *store, ¶ms)); + + // Reset to previous spawner + let env = ctx.data_mut(&mut store); + env.current_spawner = previous_spawner; + + result +} + /// Calls the module fn call_module( ctx: WasiFunctionEnv, @@ -298,7 +325,7 @@ fn call_module( return; }; - let mut call_ret = { call_in_async_runtime(&ctx, &mut store, start.clone(), &[]) }; + let mut call_ret = call_with_threadlocal_executor(&ctx, &mut store, start.clone(), vec![]); loop { // Technically, it's an error for a vfork to return from main, but anyway... diff --git a/lib/wasix/src/utils/thread_local_executor.rs b/lib/wasix/src/utils/thread_local_executor.rs index a06b00ccc70..74914f29dc1 100644 --- a/lib/wasix/src/utils/thread_local_executor.rs +++ b/lib/wasix/src/utils/thread_local_executor.rs @@ -1,11 +1,9 @@ -use crate::WasiFunctionEnv; use futures::{ executor::{LocalPool, LocalSpawner}, task::LocalSpawnExt, }; use std::thread::ThreadId; use thiserror::Error; -use wasmer::{RuntimeError, Store, Value}; /// A `Send`able spawner that spawns onto a thread-local executor /// @@ -96,29 +94,3 @@ impl ThreadLocalExecutor { self.pool.run_until(future) } } - -// TODO: This does not belong here -pub fn call_in_async_runtime<'a>( - ctx: &WasiFunctionEnv, - mut store: &mut Store, - entrypoint: wasmer::Function, - params: &'a [wasmer::Value], -) -> Result, RuntimeError> { - let cloned_params = params.to_vec(); - let env = ctx.data_mut(&mut store); - // TODO: Ensure there is only one executor at a time? - - // Set spawner in env - let mut local_executor = ThreadLocalExecutor::new(); - let spawner = local_executor.spawner(); - let previous_spawner = env.current_spawner.replace(spawner); - - // Run function with the spawner - let result = local_executor.run_until(entrypoint.call_async(&mut *store, &cloned_params)); - - // Reset to previous spawner - let env = ctx.data_mut(&mut store); - env.current_spawner = previous_spawner; - - result -} From 06cd7a52b26ca5313b91e65b30175fe4eacd7d7f Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 12:00:37 +0100 Subject: [PATCH 012/114] Switch wasix context to use DashMap for tracking contexts --- lib/wasix/src/state/env.rs | 29 +++---- lib/wasix/src/state/mod.rs | 4 +- .../src/syscalls/wasix/context_switch.rs | 80 +++++++++++-------- 3 files changed, 65 insertions(+), 48 deletions(-) diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index b38bf1e22c5..56cb5f6dbd8 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -15,14 +15,15 @@ use crate::{ syscalls::platform_clock_time_get, utils::thread_local_executor::ThreadLocalSpawner, }; -use futures::future::BoxFuture; +use dashmap::DashMap; +use futures::{channel::oneshot::Sender, future::BoxFuture}; use rand::Rng; use std::{ - collections::{BTreeMap, HashMap}, + collections::HashMap, ops::Deref, path::{Path, PathBuf}, str, - sync::{Arc, RwLock, atomic::AtomicU64}, + sync::{Arc, atomic::AtomicU64}, time::Duration, }; use virtual_fs::{FileSystem, FsError, VirtualFile}; @@ -44,7 +45,8 @@ use webc::metadata::annotations::Wasi; pub use super::handles::*; use super::{Linker, WasiState, conv_env_vars}; -static MAIN_CONTINUATION_ID: u64 = 0; +// TODO: Figure out a better place for this constant +pub static MAIN_CONTEXT_ID: u64 = 0; /// Data required to construct a [`WasiEnv`]. #[derive(Debug)] @@ -182,9 +184,8 @@ pub struct WasiEnv { inner: WasiInstanceHandlesPointer, /// TODO: Document these fields - pub(crate) contexts: - Arc>>>>, - pub(crate) current_context_id: Arc>, + pub(crate) contexts: Arc>>>, + pub(crate) current_context_id: Arc, pub(crate) next_available_context_id: AtomicU64, pub(crate) current_spawner: Option, } @@ -217,8 +218,8 @@ impl Clone for WasiEnv { skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, contexts: self.contexts.clone(), + // TODO: This is wrong; The contexts can't really be cloned. What do we even use clone on WasiEnv for? current_context_id: self.current_context_id.clone(), - // TODO: This is wrong; The two lines above as well next_available_context_id: AtomicU64::new( self.next_available_context_id .load(std::sync::atomic::Ordering::SeqCst), @@ -266,9 +267,9 @@ impl WasiEnv { skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, // TODO: Not sure if we can even properly fork coroutines at all - contexts: Arc::new(RwLock::new(BTreeMap::new())), - current_context_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), - next_available_context_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), + contexts: Arc::new(DashMap::new()), + current_context_id: Arc::new(AtomicU64::new(MAIN_CONTEXT_ID)), + next_available_context_id: AtomicU64::new(MAIN_CONTEXT_ID + 1), current_spawner: None, }; Ok((new_env, handle)) @@ -414,9 +415,9 @@ impl WasiEnv { bin_factory: init.bin_factory, capabilities: init.capabilities, disable_fs_cleanup: false, - contexts: Arc::new(RwLock::new(BTreeMap::new())), - current_context_id: Arc::new(RwLock::new(MAIN_CONTINUATION_ID)), - next_available_context_id: AtomicU64::new(MAIN_CONTINUATION_ID + 1), + contexts: Arc::new(DashMap::new()), + current_context_id: Arc::new(AtomicU64::new(MAIN_CONTEXT_ID)), + next_available_context_id: AtomicU64::new(MAIN_CONTEXT_ID + 1), current_spawner: None, }; env.owned_handles.push(thread); diff --git a/lib/wasix/src/state/mod.rs b/lib/wasix/src/state/mod.rs index 18637cee3e0..f5fe97f4519 100644 --- a/lib/wasix/src/state/mod.rs +++ b/lib/wasix/src/state/mod.rs @@ -39,7 +39,9 @@ use wasmer_wasix_types::wasi::{ pub use self::{ builder::*, - env::{WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles}, + env::{ + MAIN_CONTEXT_ID, WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles, + }, func_env::WasiFunctionEnv, types::*, }; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index b2a75058e2e..01d3d4f0d94 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,4 +1,5 @@ use super::*; +use crate::state::MAIN_CONTEXT_ID; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use anyhow::Result; use core::panic; @@ -50,36 +51,42 @@ pub fn context_new( // Setup sender and receiver for the new context let (sender, receiver) = oneshot::channel::>(); - let wait_for_resume = receiver.unwrap_or_else(|_canceled| { + let wait_for_unblock = receiver.unwrap_or_else(|_canceled| { // TODO: Handle canceled properly todo!("Context was canceled. Cleanup not implemented yet so we just panic"); }); - let mut contexts = data - .contexts - .write() - .unwrap() - .insert(new_context_id, sender); + let maybe_old_value = data.contexts.insert(new_context_id, sender); + if maybe_old_value.is_some() { + panic!( + "Context ID {} was already taken when creatin new context", + new_context_id + ); + } // SAFETY: This is fine if we can ensure that ??? // A: The future does not outlive the store // B: we now have multiple mutable references, this is dangerous let mut unsafe_static_store = - unsafe { std::mem::transmute::<_, StoreMut<'static>>(store.as_store_mut()) }; - let contexts_cloned = data.contexts.clone(); + unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; + let contexts_cloned = data.contexts.clone(); let spawner = data .current_spawner .clone() .expect("No async spawner set on WasiEnv. Did you enter the async env before?"); spawner.spawn_local(async move { - wait_for_resume.await; + wait_for_unblock.await; let result = entrypoint.call_async(&mut unsafe_static_store, &[]).await; - let mut main_context = contexts_cloned.write().unwrap(); - let main_context = main_context.remove(&0).unwrap(); + let main_context = contexts_cloned + .remove(&MAIN_CONTEXT_ID) + .map(|(_id, val)| val) + .expect("The main context should always be suspended when another context returns."); match result { Err(e) => { - main_context.send(Err(e)); + main_context + .send(Err(e)) + .expect("Failed to send error to main context, this should not happen"); } Ok(v) => { // If we get here, something went wrong, as the entrypoint function is not supposed to return @@ -105,7 +112,7 @@ pub fn context_new( #[instrument(level = "trace", skip(ctx), ret)] pub fn context_switch( mut ctx: FunctionEnvMut, - next_context_id: u64, + target_context_id: u64, ) -> impl Future> + Send + 'static + use<> { match WasiEnv::do_pending_operations(&mut ctx) { Ok(()) => {} @@ -116,23 +123,18 @@ pub fn context_switch( } } - let (data, _store) = ctx.data_and_store_mut(); - let current_context_id = { - let mut current = data.current_context_id.write().unwrap(); - let old = *current; - *current = next_context_id; - old - }; + let (data) = ctx.data_mut(); + // TODO: Review which Ordering is appropriate here + let own_context_id = data + .current_context_id + .swap(target_context_id, Ordering::SeqCst); - if current_context_id == next_context_id { + if own_context_id == target_context_id { panic!("Switching to self is not allowed"); } let (sender, receiver) = oneshot::channel::>(); - // if self.resumer.is_some() { - // panic!("Switching from a context that is already switched out"); - // } - let wait_for_resume = receiver.unwrap_or_else(|_canceled| { + let wait_for_unblock = receiver.unwrap_or_else(|_canceled| { // TODO: Handle canceled properly todo!("Context was canceled. Cleanup not implemented yet so we just panic"); }); @@ -140,17 +142,29 @@ pub fn context_switch( // let this_one = contexts.get_mut(¤t_context_id).unwrap(); // let receiver_promise = this_one.suspend(); - let mut contexts = data.contexts.write().unwrap(); - contexts.insert(current_context_id, sender); - let next_one = contexts.remove(&next_context_id).unwrap(); - next_one.send(Ok(())); - - let current_id_arc = data.current_context_id.clone(); + let maybe_old_value = data.contexts.insert(own_context_id, sender); + if maybe_old_value.is_some() { + panic!( + "Context ID {} was already suspended when switching context to {}", + own_context_id, target_context_id + ); + } + // Unblock the other context + let unblock_target = data + .contexts + .remove(&target_context_id) + .map(|(_id, val)| val) + .expect("Context to switch to does not exist"); + unblock_target + .send(Ok(())) + .expect("Failed to unblock target context, this should not happen"); + + let current_context_id = data.current_context_id.clone(); async move { - let result = wait_for_resume.await; + let result = wait_for_unblock.await; - *current_id_arc.write().unwrap() = current_context_id; + current_context_id.store(own_context_id, Ordering::SeqCst); // If we get relayed a trap, propagate it. Other wise return success result.and(Ok(Errno::Success)) From 13b506f5024a44dfaf5b89783547aa3ca2d9021e Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 13:02:05 +0100 Subject: [PATCH 013/114] Implement context_delete --- .../src/syscalls/wasix/context_switch.rs | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 01d3d4f0d94..01b7c366fa7 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -19,13 +19,42 @@ use wasmer::{StoreMut, Tag, Type}; #[instrument(level = "trace", skip(ctx), ret)] pub fn context_delete( mut ctx: FunctionEnvMut<'_, WasiEnv>, - context_id: u64, + target_context_id: u64, ) -> Result { WasiEnv::do_pending_operations(&mut ctx)?; let env = ctx.data(); let memory: MemoryView<'_> = unsafe { env.memory_view(&ctx) }; - // TODO: implement + + // TODO: Review which Ordering is appropriate here + let own_context_id = env.current_context_id.load(Ordering::SeqCst); + if own_context_id == target_context_id { + tracing::trace!( + "Context {} tried to delete itself, which is not allowed", + target_context_id + ); + return Ok(Errno::Inval); + } + + if target_context_id == MAIN_CONTEXT_ID { + tracing::trace!( + "Context {} tried to delete the main context, which is not allowed", + own_context_id + ); + return Ok(Errno::Inval); + } + + // TODO: actually delete the context + let removed_future = env.contexts.remove(&target_context_id); + let Some((_id, _val)) = removed_future else { + // Context did not exist, so we do not need to remove it + tracing::trace!( + "Context {} tried to delete context {} but it is already removed", + own_context_id, + target_context_id + ); + return Ok(Errno::Success); + }; Ok(Errno::Success) } From 05f1704d3e90f801f016e21c1f56285b034ddeec Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 13:09:49 +0100 Subject: [PATCH 014/114] Improve handling of contexts returning a value --- .../src/syscalls/wasix/context_switch.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 01b7c366fa7..61f1d19a77b 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -111,22 +111,21 @@ pub fn context_new( .remove(&MAIN_CONTEXT_ID) .map(|(_id, val)| val) .expect("The main context should always be suspended when another context returns."); - match result { - Err(e) => { - main_context - .send(Err(e)) - .expect("Failed to send error to main context, this should not happen"); - } + + // Take the underlying error, or create a new error if the context returned a value + let error = match result { + Err(e) => e, Ok(v) => { - // If we get here, something went wrong, as the entrypoint function is not supposed to return - panic!( - "Context {} returned a value ({:?}). This is not allowed for now", - new_context_id, v - ); // TODO: Handle this properly + RuntimeError::user( + format!("Context {new_context_id} returned a value ({v:?}). This is not allowed for now") + .into(), + ) } - } - // TODO: Delete own context + }; + main_context + .send(Err(error)) + .expect("Failed to send error to main context, this should not happen"); }); let env = ctx.data(); @@ -143,6 +142,7 @@ pub fn context_switch( mut ctx: FunctionEnvMut, target_context_id: u64, ) -> impl Future> + Send + 'static + use<> { + // TODO: Should we call do_pending_operations here? match WasiEnv::do_pending_operations(&mut ctx) { Ok(()) => {} Err(e) => { From 7f81a61b59926b706159c20da027f0b0137e5c17 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 13:19:11 +0100 Subject: [PATCH 015/114] Split context functions into multiple files --- .../src/syscalls/wasix/context_delete.rs | 60 +++++++++ lib/wasix/src/syscalls/wasix/context_new.rs | 93 ++++++++++++++ .../src/syscalls/wasix/context_switch.rs | 121 ------------------ lib/wasix/src/syscalls/wasix/mod.rs | 6 +- 4 files changed, 158 insertions(+), 122 deletions(-) create mode 100644 lib/wasix/src/syscalls/wasix/context_delete.rs create mode 100644 lib/wasix/src/syscalls/wasix/context_new.rs diff --git a/lib/wasix/src/syscalls/wasix/context_delete.rs b/lib/wasix/src/syscalls/wasix/context_delete.rs new file mode 100644 index 00000000000..44048114f29 --- /dev/null +++ b/lib/wasix/src/syscalls/wasix/context_delete.rs @@ -0,0 +1,60 @@ +use super::*; +use crate::state::MAIN_CONTEXT_ID; +use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; +use anyhow::Result; +use core::panic; +use futures::TryFutureExt; +use futures::task::LocalSpawnExt; +use futures::{FutureExt, channel::oneshot}; +use std::collections::BTreeMap; +use std::sync::atomic::AtomicU32; +use std::sync::{Arc, OnceLock, RwLock}; +use wasmer::{ + AsStoreMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, Module, + RuntimeError, Store, Value, imports, +}; +use wasmer::{StoreMut, Tag, Type}; + +/// ### `context_delete()` +#[instrument(level = "trace", skip(ctx), ret)] +pub fn context_delete( + mut ctx: FunctionEnvMut<'_, WasiEnv>, + target_context_id: u64, +) -> Result { + WasiEnv::do_pending_operations(&mut ctx)?; + + let env = ctx.data(); + let memory: MemoryView<'_> = unsafe { env.memory_view(&ctx) }; + + // TODO: Review which Ordering is appropriate here + let own_context_id = env.current_context_id.load(Ordering::SeqCst); + if own_context_id == target_context_id { + tracing::trace!( + "Context {} tried to delete itself, which is not allowed", + target_context_id + ); + return Ok(Errno::Inval); + } + + if target_context_id == MAIN_CONTEXT_ID { + tracing::trace!( + "Context {} tried to delete the main context, which is not allowed", + own_context_id + ); + return Ok(Errno::Inval); + } + + // TODO: actually delete the context + let removed_future = env.contexts.remove(&target_context_id); + let Some((_id, _val)) = removed_future else { + // Context did not exist, so we do not need to remove it + tracing::trace!( + "Context {} tried to delete context {} but it is already removed", + own_context_id, + target_context_id + ); + return Ok(Errno::Success); + }; + + Ok(Errno::Success) +} diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs new file mode 100644 index 00000000000..1677ede0580 --- /dev/null +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -0,0 +1,93 @@ +use super::*; +use crate::state::MAIN_CONTEXT_ID; +use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; +use anyhow::Result; +use core::panic; +use futures::TryFutureExt; +use futures::task::LocalSpawnExt; +use futures::{FutureExt, channel::oneshot}; +use std::collections::BTreeMap; +use std::sync::atomic::AtomicU32; +use std::sync::{Arc, OnceLock, RwLock}; +use wasmer::{ + AsStoreMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, Module, + RuntimeError, Store, Value, imports, +}; +use wasmer::{StoreMut, Tag, Type}; + +/// ### `context_new()` +#[instrument(level = "trace", skip(ctx), ret)] +pub fn context_new( + mut ctx: FunctionEnvMut<'_, WasiEnv>, + new_context_ptr: WasmPtr, + entrypoint: u32, +) -> Result { + WasiEnv::do_pending_operations(&mut ctx)?; + + let (data, mut store) = ctx.data_and_store_mut(); + let new_context_id = data + .next_available_context_id + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + + let entrypoint = data + .inner() + .indirect_function_table_lookup(&mut store, entrypoint) + .expect("Function not found in table"); + + // Setup sender and receiver for the new context + let (sender, receiver) = oneshot::channel::>(); + let wait_for_unblock = receiver.unwrap_or_else(|_canceled| { + // TODO: Handle canceled properly + todo!("Context was canceled. Cleanup not implemented yet so we just panic"); + }); + let maybe_old_value = data.contexts.insert(new_context_id, sender); + if maybe_old_value.is_some() { + panic!( + "Context ID {} was already taken when creatin new context", + new_context_id + ); + } + + // SAFETY: This is fine if we can ensure that ??? + // A: The future does not outlive the store + // B: we now have multiple mutable references, this is dangerous + let mut unsafe_static_store = + unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; + + let contexts_cloned = data.contexts.clone(); + let spawner = data + .current_spawner + .clone() + .expect("No async spawner set on WasiEnv. Did you enter the async env before?"); + spawner.spawn_local(async move { + wait_for_unblock.await; + let result = entrypoint.call_async(&mut unsafe_static_store, &[]).await; + + let main_context = contexts_cloned + .remove(&MAIN_CONTEXT_ID) + .map(|(_id, val)| val) + .expect("The main context should always be suspended when another context returns."); + + // Take the underlying error, or create a new error if the context returned a value + let error = match result { + Err(e) => e, + Ok(v) => { + // TODO: Handle this properly + RuntimeError::user( + format!("Context {new_context_id} returned a value ({v:?}). This is not allowed for now") + .into(), + ) + } + }; + main_context + .send(Err(error)) + .expect("Failed to send error to main context, this should not happen"); + }); + + let env = ctx.data(); + let memory = unsafe { env.memory_view(&ctx) }; + + new_context_ptr.write(&memory, new_context_id).unwrap(); + + Ok(Errno::Success) +} diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 61f1d19a77b..3d55c612978 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -15,127 +15,6 @@ use wasmer::{ }; use wasmer::{StoreMut, Tag, Type}; -/// ### `context_delete()` -#[instrument(level = "trace", skip(ctx), ret)] -pub fn context_delete( - mut ctx: FunctionEnvMut<'_, WasiEnv>, - target_context_id: u64, -) -> Result { - WasiEnv::do_pending_operations(&mut ctx)?; - - let env = ctx.data(); - let memory: MemoryView<'_> = unsafe { env.memory_view(&ctx) }; - - // TODO: Review which Ordering is appropriate here - let own_context_id = env.current_context_id.load(Ordering::SeqCst); - if own_context_id == target_context_id { - tracing::trace!( - "Context {} tried to delete itself, which is not allowed", - target_context_id - ); - return Ok(Errno::Inval); - } - - if target_context_id == MAIN_CONTEXT_ID { - tracing::trace!( - "Context {} tried to delete the main context, which is not allowed", - own_context_id - ); - return Ok(Errno::Inval); - } - - // TODO: actually delete the context - let removed_future = env.contexts.remove(&target_context_id); - let Some((_id, _val)) = removed_future else { - // Context did not exist, so we do not need to remove it - tracing::trace!( - "Context {} tried to delete context {} but it is already removed", - own_context_id, - target_context_id - ); - return Ok(Errno::Success); - }; - - Ok(Errno::Success) -} - -/// ### `context_new()` -#[instrument(level = "trace", skip(ctx), ret)] -pub fn context_new( - mut ctx: FunctionEnvMut<'_, WasiEnv>, - new_context_ptr: WasmPtr, - entrypoint: u32, -) -> Result { - WasiEnv::do_pending_operations(&mut ctx)?; - - let (data, mut store) = ctx.data_and_store_mut(); - let new_context_id = data - .next_available_context_id - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - - let entrypoint = data - .inner() - .indirect_function_table_lookup(&mut store, entrypoint) - .expect("Function not found in table"); - - // Setup sender and receiver for the new context - let (sender, receiver) = oneshot::channel::>(); - let wait_for_unblock = receiver.unwrap_or_else(|_canceled| { - // TODO: Handle canceled properly - todo!("Context was canceled. Cleanup not implemented yet so we just panic"); - }); - let maybe_old_value = data.contexts.insert(new_context_id, sender); - if maybe_old_value.is_some() { - panic!( - "Context ID {} was already taken when creatin new context", - new_context_id - ); - } - - // SAFETY: This is fine if we can ensure that ??? - // A: The future does not outlive the store - // B: we now have multiple mutable references, this is dangerous - let mut unsafe_static_store = - unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; - - let contexts_cloned = data.contexts.clone(); - let spawner = data - .current_spawner - .clone() - .expect("No async spawner set on WasiEnv. Did you enter the async env before?"); - spawner.spawn_local(async move { - wait_for_unblock.await; - let result = entrypoint.call_async(&mut unsafe_static_store, &[]).await; - - let main_context = contexts_cloned - .remove(&MAIN_CONTEXT_ID) - .map(|(_id, val)| val) - .expect("The main context should always be suspended when another context returns."); - - // Take the underlying error, or create a new error if the context returned a value - let error = match result { - Err(e) => e, - Ok(v) => { - // TODO: Handle this properly - RuntimeError::user( - format!("Context {new_context_id} returned a value ({v:?}). This is not allowed for now") - .into(), - ) - } - }; - main_context - .send(Err(error)) - .expect("Failed to send error to main context, this should not happen"); - }); - - let env = ctx.data(); - let memory = unsafe { env.memory_view(&ctx) }; - - new_context_ptr.write(&memory, new_context_id).unwrap(); - - Ok(Errno::Success) -} - /// Switch to another context #[instrument(level = "trace", skip(ctx), ret)] pub fn context_switch( diff --git a/lib/wasix/src/syscalls/wasix/mod.rs b/lib/wasix/src/syscalls/wasix/mod.rs index a9278ebee57..bb8e04c1663 100644 --- a/lib/wasix/src/syscalls/wasix/mod.rs +++ b/lib/wasix/src/syscalls/wasix/mod.rs @@ -4,7 +4,9 @@ mod chdir; mod closure_allocate; mod closure_free; mod closure_prepare; -pub mod context_switch; +mod context_delete; +mod context_new; +mod context_switch; mod dl_invalid_handle; mod dlopen; mod dlsym; @@ -92,6 +94,8 @@ pub use chdir::*; pub use closure_allocate::*; pub use closure_free::*; pub use closure_prepare::*; +pub use context_delete::*; +pub use context_new::*; pub use context_switch::*; pub use dl_invalid_handle::*; pub use dlopen::*; From 6ff430d38ff81ec294150d38caf5c1a07149c7c1 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 14:48:15 +0100 Subject: [PATCH 016/114] Improve error handling in context_switch --- .../src/syscalls/wasix/context_switch.rs | 75 ++++++++++++------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 3d55c612978..9f927975eeb 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -21,13 +21,28 @@ pub fn context_switch( mut ctx: FunctionEnvMut, target_context_id: u64, ) -> impl Future> + Send + 'static + use<> { + let sync_part = inner_context_switch(ctx, target_context_id); + async move { + match sync_part { + Ok(fut) => fut.await, + Err(res) => res, + } + } +} + +/// Helper function that allows us to return from the synchronous part early +fn inner_context_switch( + mut ctx: FunctionEnvMut, + target_context_id: u64, +) -> Result< + impl Future> + Send + 'static + use<>, + Result, +> { // TODO: Should we call do_pending_operations here? match WasiEnv::do_pending_operations(&mut ctx) { Ok(()) => {} Err(e) => { - // TODO: Move this to the end to only need a single async block - panic!("Error in do_pending_operations"); - // return async move {Err(RuntimeError::user(Box::new(e)))} + return Err(Err(RuntimeError::user(Box::new(e)))); } } @@ -38,43 +53,53 @@ pub fn context_switch( .swap(target_context_id, Ordering::SeqCst); if own_context_id == target_context_id { - panic!("Switching to self is not allowed"); + tracing::trace!("Switching context {own_context_id} to itself, which is a no-op"); + return Err(Ok(Errno::Success)); } - let (sender, receiver) = oneshot::channel::>(); - let wait_for_unblock = receiver.unwrap_or_else(|_canceled| { - // TODO: Handle canceled properly - todo!("Context was canceled. Cleanup not implemented yet so we just panic"); - }); - - // let this_one = contexts.get_mut(¤t_context_id).unwrap(); - // let receiver_promise = this_one.suspend(); + let (sender, wait_for_unblock) = oneshot::channel::>(); let maybe_old_value = data.contexts.insert(own_context_id, sender); if maybe_old_value.is_some() { + // This should never happen, and if it does, it is an error in WASIX panic!( - "Context ID {} was already suspended when switching context to {}", - own_context_id, target_context_id + "Context {own_context_id} is already suspended but tried to switch to context {target_context_id}. How did we get here?", ); } // Unblock the other context - let unblock_target = data - .contexts - .remove(&target_context_id) - .map(|(_id, val)| val) - .expect("Context to switch to does not exist"); - unblock_target - .send(Ok(())) - .expect("Failed to unblock target context, this should not happen"); + let Some(unblock_target) = data.contexts.remove(&target_context_id).map(|(_, val)| val) else { + tracing::trace!( + "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" + ); + return Err(Ok(Errno::Inval)); + }; + let Ok(_) = unblock_target.send(Ok(())) else { + // This should never happen, and if it does, it is an error in WASIX + // TODO: Handle cancellation properly + panic!( + "Context {own_context_id} failed to unblock target context {target_context_id}. This should never happen" + ); + }; let current_context_id = data.current_context_id.clone(); - async move { + Ok(async move { let result = wait_for_unblock.await; - current_context_id.store(own_context_id, Ordering::SeqCst); + let result = match result { + Ok(v) => v, + Err(canceled) => { + tracing::trace!( + "Context {own_context_id} was canceled while it was suspended: {}", + canceled + ); + // TODO: Handle cancellation properly + panic!("Sender was dropped: {canceled}"); + } + }; + // If we get relayed a trap, propagate it. Other wise return success result.and(Ok(Errno::Success)) - } + }) } From f9081bec755f49096f79af1a33b60a5a425090c8 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 18 Nov 2025 16:08:11 +0100 Subject: [PATCH 017/114] Cleanup error handling in context_new --- lib/wasix/src/syscalls/wasix/context_new.rs | 192 +++++++++++++++----- 1 file changed, 143 insertions(+), 49 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 1677ede0580..ff7df544f6b 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -1,9 +1,10 @@ use super::*; use crate::state::MAIN_CONTEXT_ID; +use crate::utils::thread_local_executor::ThreadLocalSpawnerError; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; -use anyhow::Result; use core::panic; use futures::TryFutureExt; +use futures::channel::oneshot::{Receiver, Sender}; use futures::task::LocalSpawnExt; use futures::{FutureExt, channel::oneshot}; use std::collections::BTreeMap; @@ -15,7 +16,91 @@ use wasmer::{ }; use wasmer::{StoreMut, Tag, Type}; -/// ### `context_new()` +/// Return the function corresponding to the given entrypoint index if it exists and has the signature `() -> ()` +pub fn lookup_typechecked_entrypoint( + data: &WasiEnv, + mut store: &mut StoreMut<'_>, + entrypoint_id: u32, +) -> Result { + let entrypoint = match data + .inner() + .indirect_function_table_lookup(&mut store, entrypoint_id) + { + Ok(func) => func, + Err(e) => { + tracing::trace!( + "Failed to lookup entrypoint function {}: {:?}", + entrypoint_id, + e + ); + return Err(Errno::Inval); + } + }; + + let entrypoint_type = entrypoint.ty(&store); + if !entrypoint_type.params().is_empty() || !entrypoint_type.results().is_empty() { + tracing::trace!( + "Entrypoint function {} has invalid signature: expected () -> (), got {:?} -> {:?}", + entrypoint_id, + entrypoint_type.params(), + entrypoint_type.results() + ); + return Err(Errno::Inval); + } + + return Ok(entrypoint); +} + +async fn launch_function( + mut unsafe_static_store: StoreMut<'static>, + wait_for_unblock: Receiver>, + current_context_id: Arc, + new_context_id: u64, + contexts_cloned: Arc>>>, + typechecked_entrypoint: Function, +) -> () { + // Wait for the context to be unblocked + let prelaunch_result = wait_for_unblock.await; + current_context_id.store(new_context_id, Ordering::SeqCst); + + match prelaunch_result { + Ok(_) => (), + Err(canceled) => { + tracing::trace!( + "Context {new_context_id} was canceled before it even started: {canceled}", + ); + // TODO: Handle cancellation properly + panic!("Sender was dropped: {canceled}"); + } + }; + + let result = typechecked_entrypoint + .call_async(&mut unsafe_static_store, &[]) + .await; + + let main_context = contexts_cloned + .remove(&MAIN_CONTEXT_ID) + .map(|(_id, val)| val) + .expect("The main context should always be suspended when another context returns."); + + // Take the underlying error, or create a new error if the context returned a value + let error = match result { + Err(e) => e, + Ok(v) => { + // TODO: Handle returning functions with a real error type + RuntimeError::user( + format!( + "Context {new_context_id} returned a value ({v:?}). This is not allowed for now" + ) + .into(), + ) + } + }; + main_context + .send(Err(error)) + .expect("Failed to send error to main context, this should not happen"); +} + #[instrument(level = "trace", skip(ctx), ret)] pub fn context_new( mut ctx: FunctionEnvMut<'_, WasiEnv>, @@ -25,26 +110,38 @@ pub fn context_new( WasiEnv::do_pending_operations(&mut ctx)?; let (data, mut store) = ctx.data_and_store_mut(); + + // Lookup and check the entrypoint function + let typechecked_entrypoint = match lookup_typechecked_entrypoint(data, &mut store, entrypoint) { + Ok(func) => func, + Err(e) => { + return Ok(e); + } + }; + + // Verify that we are in an async context + let Some(spawner) = data.current_spawner.clone() else { + tracing::trace!("No async spawner set on WasiEnv. Did you enter the async env before?"); + return Ok(Errno::Again); + }; + + // TODO: Review which Ordering is appropriate here let new_context_id = data .next_available_context_id .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - - let entrypoint = data - .inner() - .indirect_function_table_lookup(&mut store, entrypoint) - .expect("Function not found in table"); + { + let memory = unsafe { data.memory_view(&mut store) }; + wasi_try_mem_ok!(new_context_ptr.write(&memory, new_context_id)); + } // Setup sender and receiver for the new context - let (sender, receiver) = oneshot::channel::>(); - let wait_for_unblock = receiver.unwrap_or_else(|_canceled| { - // TODO: Handle canceled properly - todo!("Context was canceled. Cleanup not implemented yet so we just panic"); - }); + let (sender, wait_for_unblock) = oneshot::channel::>(); + let maybe_old_value = data.contexts.insert(new_context_id, sender); if maybe_old_value.is_some() { + // This should never happen, and if it does, it is an error in WASIX panic!( - "Context ID {} was already taken when creatin new context", - new_context_id + "There already is a context suspended with ID {new_context_id}. How did we get here?", ); } @@ -54,40 +151,37 @@ pub fn context_new( let mut unsafe_static_store = unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; - let contexts_cloned = data.contexts.clone(); - let spawner = data - .current_spawner - .clone() - .expect("No async spawner set on WasiEnv. Did you enter the async env before?"); - spawner.spawn_local(async move { - wait_for_unblock.await; - let result = entrypoint.call_async(&mut unsafe_static_store, &[]).await; - - let main_context = contexts_cloned - .remove(&MAIN_CONTEXT_ID) - .map(|(_id, val)| val) - .expect("The main context should always be suspended when another context returns."); - - // Take the underlying error, or create a new error if the context returned a value - let error = match result { - Err(e) => e, - Ok(v) => { - // TODO: Handle this properly - RuntimeError::user( - format!("Context {new_context_id} returned a value ({v:?}). This is not allowed for now") - .into(), - ) - } - }; - main_context - .send(Err(error)) - .expect("Failed to send error to main context, this should not happen"); - }); - - let env = ctx.data(); - let memory = unsafe { env.memory_view(&ctx) }; - - new_context_ptr.write(&memory, new_context_id).unwrap(); + let contexts_cloned: Arc< + dashmap::DashMap>>, + > = data.contexts.clone(); + let current_context_id: Arc = data.current_context_id.clone(); - Ok(Errno::Success) + let entrypoint_future = launch_function( + unsafe_static_store, + wait_for_unblock, + current_context_id, + new_context_id, + contexts_cloned, + typechecked_entrypoint, + ); + // Queue the + match spawner.spawn_local(entrypoint_future) { + Ok(()) => Ok(Errno::Success), + Err(ThreadLocalSpawnerError::LocalPoolShutDown) => { + // TODO: Handle cancellation properly + panic!( + "Failed to spawn context {new_context_id} because the local executor has been shut down", + ); + } + Err(ThreadLocalSpawnerError::NotOnTheCorrectThread { expected, found }) => { + // Not on the correct host thread. If this error happens, it is a bug in WASIX. + panic!( + "Failed to spawn context {new_context_id} because the current thread ({found:?}) is not the expected thread ({expected:?}) for the local executor" + ) + } + Err(ThreadLocalSpawnerError::SpawnError) => { + // This should never happen + panic!("Failed to spawn_local context {new_context_id} , this should not happen"); + } + } } From b321d81e0c21d1dfd4a015c6feaeb80fe42c5f0f Mon Sep 17 00:00:00 2001 From: Zebreus Date: Wed, 19 Nov 2025 10:57:40 +0100 Subject: [PATCH 018/114] Add comments explaining the individual steps in the context switching functions --- lib/wasix/src/syscalls/wasix/context_new.rs | 44 ++++++++++++------- .../src/syscalls/wasix/context_switch.rs | 23 +++++++--- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index ff7df544f6b..155fdfa91e4 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -48,7 +48,7 @@ pub fn lookup_typechecked_entrypoint( return Err(Errno::Inval); } - return Ok(entrypoint); + Ok(entrypoint) } async fn launch_function( @@ -63,6 +63,7 @@ async fn launch_function( let prelaunch_result = wait_for_unblock.await; current_context_id.store(new_context_id, Ordering::SeqCst); + // Handle if the context was canceled before it even started match prelaunch_result { Ok(_) => (), Err(canceled) => { @@ -74,10 +75,14 @@ async fn launch_function( } }; + // Actually call the entrypoint function let result = typechecked_entrypoint .call_async(&mut unsafe_static_store, &[]) .await; + // If that function returns, we need to resume the main context with an error + + // Retrieve the main context let main_context = contexts_cloned .remove(&MAIN_CONTEXT_ID) .map(|(_id, val)| val) @@ -96,6 +101,8 @@ async fn launch_function( ) } }; + + // Resume the main context with the error main_context .send(Err(error)) .expect("Failed to send error to main context, this should not happen"); @@ -125,37 +132,36 @@ pub fn context_new( return Ok(Errno::Again); }; + // Create a new context ID // TODO: Review which Ordering is appropriate here let new_context_id = data .next_available_context_id .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - { - let memory = unsafe { data.memory_view(&mut store) }; - wasi_try_mem_ok!(new_context_ptr.write(&memory, new_context_id)); - } + + // Write the new context ID into memory + let memory = unsafe { data.memory_view(&store) }; + wasi_try_mem_ok!(new_context_ptr.write(&memory, new_context_id)); // Setup sender and receiver for the new context - let (sender, wait_for_unblock) = oneshot::channel::>(); + let (unblock, wait_for_unblock) = oneshot::channel::>(); - let maybe_old_value = data.contexts.insert(new_context_id, sender); - if maybe_old_value.is_some() { + // Store the unblock function into the WasiEnv + let previous_unblock = data.contexts.insert(new_context_id, unblock); + if previous_unblock.is_some() { // This should never happen, and if it does, it is an error in WASIX - panic!( - "There already is a context suspended with ID {new_context_id}. How did we get here?", - ); + panic!("There already is a context suspended with ID {new_context_id}"); } - // SAFETY: This is fine if we can ensure that ??? - // A: The future does not outlive the store - // B: we now have multiple mutable references, this is dangerous + // Clone necessary arcs for the entrypoint future + // SAFETY: Will be made safe with the proper wasmer async API let mut unsafe_static_store = unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; - let contexts_cloned: Arc< dashmap::DashMap>>, > = data.contexts.clone(); let current_context_id: Arc = data.current_context_id.clone(); + // Create the future that will launch the entrypoint function let entrypoint_future = launch_function( unsafe_static_store, wait_for_unblock, @@ -164,8 +170,12 @@ pub fn context_new( contexts_cloned, typechecked_entrypoint, ); - // Queue the - match spawner.spawn_local(entrypoint_future) { + + // Queue the future onto the thread-local executor + let spawn_result = spawner.spawn_local(entrypoint_future); + + // Return failure if spawning failed + match spawn_result { Ok(()) => Ok(Errno::Success), Err(ThreadLocalSpawnerError::LocalPoolShutDown) => { // TODO: Handle cancellation properly diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 9f927975eeb..2a146ba8390 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -47,24 +47,27 @@ fn inner_context_switch( } let (data) = ctx.data_mut(); + + // Get own context ID // TODO: Review which Ordering is appropriate here let own_context_id = data .current_context_id .swap(target_context_id, Ordering::SeqCst); + // If switching to self, do nothing if own_context_id == target_context_id { tracing::trace!("Switching context {own_context_id} to itself, which is a no-op"); return Err(Ok(Errno::Success)); } - let (sender, wait_for_unblock) = oneshot::channel::>(); + // Setup sender and receiver for the new context + let (unblock, wait_for_unblock) = oneshot::channel::>(); - let maybe_old_value = data.contexts.insert(own_context_id, sender); - if maybe_old_value.is_some() { + // Store the unblock function into the WasiEnv + let previous_unblock = data.contexts.insert(own_context_id, unblock); + if previous_unblock.is_some() { // This should never happen, and if it does, it is an error in WASIX - panic!( - "Context {own_context_id} is already suspended but tried to switch to context {target_context_id}. How did we get here?", - ); + panic!("There is already a unblock present for the current context {own_context_id}"); } // Unblock the other context @@ -82,11 +85,19 @@ fn inner_context_switch( ); }; + // Clone necessary arcs for the future let current_context_id = data.current_context_id.clone(); + + // Create the future that will resolve when this context is switched back to + // again Ok(async move { + // Wait until we are unblocked again let result = wait_for_unblock.await; + + // Restore our own context ID current_context_id.store(own_context_id, Ordering::SeqCst); + // Handle if we were canceled instead of beeing unblocked let result = match result { Ok(v) => v, Err(canceled) => { From 97751c59f63813abe2dbad82e3d2ca4edc9f4648 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Wed, 19 Nov 2025 11:04:06 +0100 Subject: [PATCH 019/114] Always access current_context_id with relaxed ordering --- lib/wasix/src/syscalls/wasix/context_delete.rs | 3 +-- lib/wasix/src/syscalls/wasix/context_new.rs | 5 ++--- lib/wasix/src/syscalls/wasix/context_switch.rs | 5 ++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_delete.rs b/lib/wasix/src/syscalls/wasix/context_delete.rs index 44048114f29..7e9bb799334 100644 --- a/lib/wasix/src/syscalls/wasix/context_delete.rs +++ b/lib/wasix/src/syscalls/wasix/context_delete.rs @@ -26,8 +26,7 @@ pub fn context_delete( let env = ctx.data(); let memory: MemoryView<'_> = unsafe { env.memory_view(&ctx) }; - // TODO: Review which Ordering is appropriate here - let own_context_id = env.current_context_id.load(Ordering::SeqCst); + let own_context_id = env.current_context_id.load(Ordering::Relaxed); if own_context_id == target_context_id { tracing::trace!( "Context {} tried to delete itself, which is not allowed", diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 155fdfa91e4..852355b1435 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -61,7 +61,7 @@ async fn launch_function( ) -> () { // Wait for the context to be unblocked let prelaunch_result = wait_for_unblock.await; - current_context_id.store(new_context_id, Ordering::SeqCst); + current_context_id.store(new_context_id, Ordering::Relaxed); // Handle if the context was canceled before it even started match prelaunch_result { @@ -133,10 +133,9 @@ pub fn context_new( }; // Create a new context ID - // TODO: Review which Ordering is appropriate here let new_context_id = data .next_available_context_id - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); // Write the new context ID into memory let memory = unsafe { data.memory_view(&store) }; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 2a146ba8390..6365ba73dad 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -49,10 +49,9 @@ fn inner_context_switch( let (data) = ctx.data_mut(); // Get own context ID - // TODO: Review which Ordering is appropriate here let own_context_id = data .current_context_id - .swap(target_context_id, Ordering::SeqCst); + .swap(target_context_id, Ordering::Relaxed); // If switching to self, do nothing if own_context_id == target_context_id { @@ -95,7 +94,7 @@ fn inner_context_switch( let result = wait_for_unblock.await; // Restore our own context ID - current_context_id.store(own_context_id, Ordering::SeqCst); + current_context_id.store(own_context_id, Ordering::Relaxed); // Handle if we were canceled instead of beeing unblocked let result = match result { From 0a86147572c079450ac98544233f8a0a513bc1b0 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Thu, 20 Nov 2025 11:22:01 +0100 Subject: [PATCH 020/114] Only set the context id in the switched to context --- lib/wasix/src/syscalls/wasix/context_new.rs | 1 + lib/wasix/src/syscalls/wasix/context_switch.rs | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 852355b1435..d949dca8132 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -61,6 +61,7 @@ async fn launch_function( ) -> () { // Wait for the context to be unblocked let prelaunch_result = wait_for_unblock.await; + // Restore our own context ID current_context_id.store(new_context_id, Ordering::Relaxed); // Handle if the context was canceled before it even started diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 6365ba73dad..cc0995b5a88 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -49,9 +49,7 @@ fn inner_context_switch( let (data) = ctx.data_mut(); // Get own context ID - let own_context_id = data - .current_context_id - .swap(target_context_id, Ordering::Relaxed); + let own_context_id = data.current_context_id.load(Ordering::Relaxed); // If switching to self, do nothing if own_context_id == target_context_id { @@ -92,7 +90,6 @@ fn inner_context_switch( Ok(async move { // Wait until we are unblocked again let result = wait_for_unblock.await; - // Restore our own context ID current_context_id.store(own_context_id, Ordering::Relaxed); From b34f916234d1c6766760cde88cc555aa5e5550bd Mon Sep 17 00:00:00 2001 From: Zebreus Date: Thu, 20 Nov 2025 14:57:02 +0100 Subject: [PATCH 021/114] Fix race condition in context_switch --- lib/wasix/src/state/env.rs | 11 ++-- .../src/syscalls/wasix/context_delete.rs | 4 +- lib/wasix/src/syscalls/wasix/context_new.rs | 27 ++++----- .../src/syscalls/wasix/context_switch.rs | 55 ++++++++++++------- 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 56cb5f6dbd8..9d96d508450 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -15,15 +15,14 @@ use crate::{ syscalls::platform_clock_time_get, utils::thread_local_executor::ThreadLocalSpawner, }; -use dashmap::DashMap; use futures::{channel::oneshot::Sender, future::BoxFuture}; use rand::Rng; use std::{ - collections::HashMap, + collections::{BTreeMap, HashMap}, ops::Deref, path::{Path, PathBuf}, str, - sync::{Arc, atomic::AtomicU64}, + sync::{Arc, RwLock, atomic::AtomicU64}, time::Duration, }; use virtual_fs::{FileSystem, FsError, VirtualFile}; @@ -184,7 +183,7 @@ pub struct WasiEnv { inner: WasiInstanceHandlesPointer, /// TODO: Document these fields - pub(crate) contexts: Arc>>>, + pub(crate) contexts: Arc>>>>, pub(crate) current_context_id: Arc, pub(crate) next_available_context_id: AtomicU64, pub(crate) current_spawner: Option, @@ -267,7 +266,7 @@ impl WasiEnv { skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, // TODO: Not sure if we can even properly fork coroutines at all - contexts: Arc::new(DashMap::new()), + contexts: Default::default(), current_context_id: Arc::new(AtomicU64::new(MAIN_CONTEXT_ID)), next_available_context_id: AtomicU64::new(MAIN_CONTEXT_ID + 1), current_spawner: None, @@ -415,7 +414,7 @@ impl WasiEnv { bin_factory: init.bin_factory, capabilities: init.capabilities, disable_fs_cleanup: false, - contexts: Arc::new(DashMap::new()), + contexts: Default::default(), current_context_id: Arc::new(AtomicU64::new(MAIN_CONTEXT_ID)), next_available_context_id: AtomicU64::new(MAIN_CONTEXT_ID + 1), current_spawner: None, diff --git a/lib/wasix/src/syscalls/wasix/context_delete.rs b/lib/wasix/src/syscalls/wasix/context_delete.rs index 7e9bb799334..ff48544a7b7 100644 --- a/lib/wasix/src/syscalls/wasix/context_delete.rs +++ b/lib/wasix/src/syscalls/wasix/context_delete.rs @@ -44,8 +44,8 @@ pub fn context_delete( } // TODO: actually delete the context - let removed_future = env.contexts.remove(&target_context_id); - let Some((_id, _val)) = removed_future else { + let removed_future = env.contexts.write().unwrap().remove(&target_context_id); + let Some(_) = removed_future else { // Context did not exist, so we do not need to remove it tracing::trace!( "Context {} tried to delete context {} but it is already removed", diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index d949dca8132..6a2f7abeb62 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -56,7 +56,7 @@ async fn launch_function( wait_for_unblock: Receiver>, current_context_id: Arc, new_context_id: u64, - contexts_cloned: Arc>>>, + contexts_cloned: Arc>>>>, typechecked_entrypoint: Function, ) -> () { // Wait for the context to be unblocked @@ -85,8 +85,9 @@ async fn launch_function( // Retrieve the main context let main_context = contexts_cloned + .write() + .unwrap() .remove(&MAIN_CONTEXT_ID) - .map(|(_id, val)| val) .expect("The main context should always be suspended when another context returns."); // Take the underlying error, or create a new error if the context returned a value @@ -143,22 +144,22 @@ pub fn context_new( wasi_try_mem_ok!(new_context_ptr.write(&memory, new_context_id)); // Setup sender and receiver for the new context - let (unblock, wait_for_unblock) = oneshot::channel::>(); - - // Store the unblock function into the WasiEnv - let previous_unblock = data.contexts.insert(new_context_id, unblock); - if previous_unblock.is_some() { - // This should never happen, and if it does, it is an error in WASIX - panic!("There already is a context suspended with ID {new_context_id}"); - } + let wait_for_unblock = { + let (unblock, wait_for_unblock) = oneshot::channel::>(); + + let mut contexts = data.contexts.write().unwrap(); + // Store the unblock function into the WasiEnv + let None = contexts.insert(new_context_id, unblock) else { + panic!("There already is a context suspended with ID {new_context_id}"); + }; + wait_for_unblock + }; // Clone necessary arcs for the entrypoint future // SAFETY: Will be made safe with the proper wasmer async API let mut unsafe_static_store = unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; - let contexts_cloned: Arc< - dashmap::DashMap>>, - > = data.contexts.clone(); + let contexts_cloned = data.contexts.clone(); let current_context_id: Arc = data.current_context_id.clone(); // Create the future that will launch the entrypoint function diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index cc0995b5a88..3e6784dbae2 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -60,28 +60,45 @@ fn inner_context_switch( // Setup sender and receiver for the new context let (unblock, wait_for_unblock) = oneshot::channel::>(); - // Store the unblock function into the WasiEnv - let previous_unblock = data.contexts.insert(own_context_id, unblock); - if previous_unblock.is_some() { - // This should never happen, and if it does, it is an error in WASIX - panic!("There is already a unblock present for the current context {own_context_id}"); - } + // Put our unblock function into the env and get the target's unblock function + let unblock_target = { + // Lock contexts for this block + let mut contexts = data.contexts.write().unwrap(); - // Unblock the other context - let Some(unblock_target) = data.contexts.remove(&target_context_id).map(|(_, val)| val) else { - tracing::trace!( - "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" - ); - return Err(Ok(Errno::Inval)); - }; - let Ok(_) = unblock_target.send(Ok(())) else { - // This should never happen, and if it does, it is an error in WASIX - // TODO: Handle cancellation properly - panic!( - "Context {own_context_id} failed to unblock target context {target_context_id}. This should never happen" - ); + // Assert preconditions + if contexts.get(&target_context_id).is_none() { + tracing::trace!( + "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" + ); + + return Err(Ok(Errno::Inval)); + } + if contexts.get(&own_context_id).is_some() { + // This should never happen, and if it does, it is an error in WASIX + panic!("There is already a unblock present for the current context {own_context_id}"); + } + + // Insert our unblock function and remove the target's unblock function + let unblock_target = contexts.remove(&target_context_id).unwrap(); // Unwrap is safe due to precondition check above + contexts.insert(own_context_id, unblock); + unblock_target }; + // Unblock the target + let unblock_result = unblock_target.send(Ok(())); + match unblock_result { + Ok(_) => { + // Successfully unblocked target context + } + Err(_) => { + // This should never happen, and if it does, it is an error in WASIX + // TODO: Handle cancellation properly + panic!( + "Context {own_context_id} failed to unblock target context {target_context_id}. This should never happen" + ); + } + } + // Clone necessary arcs for the future let current_context_id = data.current_context_id.clone(); From 62dad92bf585b85957bda5fdd67986e3aa56dc64 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 21 Nov 2025 14:46:42 +0100 Subject: [PATCH 022/114] Implement cancellations for contexts --- lib/wasix/src/syscalls/wasix/context_new.rs | 35 +++++++--- .../src/syscalls/wasix/context_switch.rs | 68 +++++++++++-------- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 6a2f7abeb62..b9e388d8381 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -71,8 +71,8 @@ async fn launch_function( tracing::trace!( "Context {new_context_id} was canceled before it even started: {canceled}", ); - // TODO: Handle cancellation properly - panic!("Sender was dropped: {canceled}"); + // At this point we don't need to do anything else + return; } }; @@ -82,18 +82,24 @@ async fn launch_function( .await; // If that function returns, we need to resume the main context with an error - - // Retrieve the main context - let main_context = contexts_cloned - .write() - .unwrap() - .remove(&MAIN_CONTEXT_ID) - .expect("The main context should always be suspended when another context returns."); - // Take the underlying error, or create a new error if the context returned a value let error = match result { - Err(e) => e, + Err(e) => match e.downcast_ref::() { + Some(s) => { + tracing::trace!("Context {new_context_id} exited with error string: {}", s); + // Context was cancelled, so we can just exit here. + // + // At this point we don't need to do anything else + return; + } + None => { + // Propagate the runtime error to main + e + } + }, Ok(v) => { + // Not really sure how we should handle this case + // // TODO: Handle returning functions with a real error type RuntimeError::user( format!( @@ -104,6 +110,13 @@ async fn launch_function( } }; + // Retrieve the main context + let main_context = contexts_cloned + .write() + .unwrap() + .remove(&MAIN_CONTEXT_ID) + .expect("The main context should always be suspended when another context returns."); + // Resume the main context with the error main_context .send(Err(error)) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 3e6784dbae2..b61f64c5470 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -9,12 +9,24 @@ use futures::{FutureExt, channel::oneshot}; use std::collections::BTreeMap; use std::sync::atomic::AtomicU32; use std::sync::{Arc, OnceLock, RwLock}; +use thiserror::Error; use wasmer::{ AsStoreMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, Module, RuntimeError, Store, Value, imports, }; use wasmer::{StoreMut, Tag, Type}; +/// Error type for errors internal to context switching +/// +/// Will be returned as a RuntimeError::User +#[derive(Error, Debug)] +pub(crate) enum ContextError { + // Should always be handled by the launch_entrypoint function and thus never propagated + // to the user + #[error("Context was cancelled. If you see this message, something went wrong.")] + Cancelled, +} + /// Switch to another context #[instrument(level = "trace", skip(ctx), ret)] pub fn context_switch( @@ -31,6 +43,10 @@ pub fn context_switch( } /// Helper function that allows us to return from the synchronous part early +/// +/// The order of operations in here is quite delicate, so be careful when +/// modifying this function. It's important to not leave the env in +/// an inconsistent state. fn inner_context_switch( mut ctx: FunctionEnvMut, target_context_id: u64, @@ -60,50 +76,47 @@ fn inner_context_switch( // Setup sender and receiver for the new context let (unblock, wait_for_unblock) = oneshot::channel::>(); - // Put our unblock function into the env and get the target's unblock function - let unblock_target = { + // Try to unblock the target and put our unblock function into the env, if successful + { // Lock contexts for this block let mut contexts = data.contexts.write().unwrap(); - // Assert preconditions + // Assert preconditions (target is blocked && we are unblocked) if contexts.get(&target_context_id).is_none() { tracing::trace!( "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" ); - return Err(Ok(Errno::Inval)); } if contexts.get(&own_context_id).is_some() { - // This should never happen, and if it does, it is an error in WASIX + // This should never happen, because the active context should never have an unblock function (as it is not suspended) + // If it does, it is an error in WASIX panic!("There is already a unblock present for the current context {own_context_id}"); } - // Insert our unblock function and remove the target's unblock function + // Unblock the target + // Dont mark ourself as blocked yet, as we first need to know that unblocking succeeded let unblock_target = contexts.remove(&target_context_id).unwrap(); // Unwrap is safe due to precondition check above + let unblock_result = unblock_target.send(Ok(())); + let Ok(_) = unblock_result else { + // If there is no target to unblock, we assume it exited, but the unblock function was not removed + // For now we treat this like a missing context + // It can't happen again, as we already removed the unblock function + // + // TODO: Think about whether this is correct + tracing::trace!( + "Context {own_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" + ); + return Err(Ok(Errno::Inval)); + }; + + // After we have unblocked the target, we can insert our own unblock function contexts.insert(own_context_id, unblock); - unblock_target }; - // Unblock the target - let unblock_result = unblock_target.send(Ok(())); - match unblock_result { - Ok(_) => { - // Successfully unblocked target context - } - Err(_) => { - // This should never happen, and if it does, it is an error in WASIX - // TODO: Handle cancellation properly - panic!( - "Context {own_context_id} failed to unblock target context {target_context_id}. This should never happen" - ); - } - } - // Clone necessary arcs for the future let current_context_id = data.current_context_id.clone(); - - // Create the future that will resolve when this context is switched back to - // again + // Create the future that will resolve when this context is switched back to again Ok(async move { // Wait until we are unblocked again let result = wait_for_unblock.await; @@ -118,8 +131,9 @@ fn inner_context_switch( "Context {own_context_id} was canceled while it was suspended: {}", canceled ); - // TODO: Handle cancellation properly - panic!("Sender was dropped: {canceled}"); + + let err = ContextError::Cancelled.into(); + return Err(RuntimeError::user(err)); } }; From 0cdfcd2996e1da1337d8c9c287863960f63bcf6d Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 21 Nov 2025 17:12:13 +0100 Subject: [PATCH 023/114] Move all context switching state into one struct --- lib/wasix/src/bin_factory/exec.rs | 20 ++- lib/wasix/src/os/task/thread.rs | 28 ++--- .../src/os/task/thread/context_switching.rs | 119 ++++++++++++++++++ lib/wasix/src/state/env.rs | 41 ++---- .../src/syscalls/wasix/context_delete.rs | 13 +- lib/wasix/src/syscalls/wasix/context_new.rs | 54 ++++---- .../src/syscalls/wasix/context_switch.rs | 44 +++---- 7 files changed, 219 insertions(+), 100 deletions(-) create mode 100644 lib/wasix/src/os/task/thread/context_switching.rs diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 2c0c8051580..56d9a4ca601 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -4,7 +4,9 @@ use crate::{ RewindState, SpawnError, WasiError, WasiRuntimeError, os::task::{ TaskJoinHandle, - thread::{RewindResultType, WasiThreadRunGuard}, + thread::{ + RewindResultType, WasiThreadRunGuard, context_switching::ContextSwitchingContext, + }, }, runtime::{ TaintReason, @@ -251,19 +253,29 @@ fn call_with_threadlocal_executor( entrypoint: wasmer::Function, params: Vec, ) -> Result, RuntimeError> { + // TODO: Maybe move this to the runtime or task manager, so that the runtime can + // control which executor is used. That would also allow using only one shared + // executor per thread. // Create a new executor let mut local_executor = ThreadLocalExecutor::new(); // Put the spawner into the WASI env, so that syscalls can use it to queue up new tasks let spawner = local_executor.spawner(); - let previous_spawner = ctx.data_mut(&mut store).current_spawner.replace(spawner); + let env = ctx.data_mut(&mut store); + if env.context_switching_context.is_some() { + panic!("Can not start a threadlocal executor as there is already one present"); + } + env.context_switching_context = Some(Arc::new(ContextSwitchingContext::new(spawner))); // Run function with the spawner let result = local_executor.run_until(entrypoint.call_async(&mut *store, ¶ms)); - // Reset to previous spawner + // Remove the spawner again let env = ctx.data_mut(&mut store); - env.current_spawner = previous_spawner; + if env.context_switching_context.is_none() { + panic!("No context switching context present in env after the main function completed."); + } + env.context_switching_context = None; result } diff --git a/lib/wasix/src/os/task/thread.rs b/lib/wasix/src/os/task/thread.rs index 799d0fcdd05..93a3cd32d6a 100644 --- a/lib/wasix/src/os/task/thread.rs +++ b/lib/wasix/src/os/task/thread.rs @@ -1,3 +1,17 @@ +// TODO: Move to better location +pub mod context_switching; + +use super::{ + control_plane::TaskCountGuard, + task_join_handle::{OwnedTaskStatus, TaskJoinHandle}, +}; +use crate::{ + WasiRuntimeError, + os::task::process::{WasiProcessId, WasiProcessInner}, + state::LinkError, + syscalls::HandleRewindType, +}; +use bytes::{Bytes, BytesMut}; use serde::{Deserialize, Serialize}; use std::sync::atomic::{AtomicBool, Ordering}; use std::{ @@ -6,8 +20,6 @@ use std::{ sync::{Arc, Condvar, Mutex, Weak}, task::Waker, }; - -use bytes::{Bytes, BytesMut}; use wasmer::{ExportError, InstantiationError, MemoryError}; use wasmer_wasix_types::{ types::Signal, @@ -15,18 +27,6 @@ use wasmer_wasix_types::{ wasix::ThreadStartType, }; -use crate::{ - WasiRuntimeError, - os::task::process::{WasiProcessId, WasiProcessInner}, - state::LinkError, - syscalls::HandleRewindType, -}; - -use super::{ - control_plane::TaskCountGuard, - task_join_handle::{OwnedTaskStatus, TaskJoinHandle}, -}; - /// Represents the ID of a WASI thread #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct WasiThreadId(u32); diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs new file mode 100644 index 00000000000..b73300149b7 --- /dev/null +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -0,0 +1,119 @@ +use std::{ + collections::BTreeMap, + sync::{RwLock, atomic::AtomicU64}, +}; + +use futures::channel::oneshot::Sender; +use thiserror::Error; +use wasmer::RuntimeError; + +use crate::utils::thread_local_executor::ThreadLocalSpawner; + +#[derive(Debug)] +pub(crate) struct ContextSwitchingContext { + /// TODO: Document these fields + unblockers: RwLock>>>, + current_context_id: AtomicU64, + next_available_context_id: AtomicU64, + current_spawner: ThreadLocalSpawner, +} + +#[derive(Debug, Error)] +pub enum ContextSwitchError { + #[error("Target context to switch to is missing")] + SwitchTargetMissing, + #[error("Failed to unblock target context")] + SwitchUnblockFailed, + #[error("Own context is already blocked")] + OwnContextAlreadyBlocked, +} + +impl ContextSwitchingContext { + pub(crate) fn new(spawner: ThreadLocalSpawner) -> Self { + Self { + unblockers: RwLock::new(BTreeMap::new()), + current_context_id: AtomicU64::new(0), + next_available_context_id: AtomicU64::new(1), + current_spawner: spawner, + } + } + + pub(crate) fn active_context_id(&self) -> u64 { + self.current_context_id + .load(std::sync::atomic::Ordering::Relaxed) + } + + pub(crate) fn set_active_context_id(&self, context_id: u64) { + self.current_context_id + .store(context_id, std::sync::atomic::Ordering::Relaxed); + } + + pub(crate) fn allocate_new_context_id(&self) -> u64 { + self.next_available_context_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } + + pub(crate) fn get_spawner(&self) -> ThreadLocalSpawner { + self.current_spawner.clone() + } + + pub(crate) fn remove_unblocker( + &self, + target_context_id: &u64, + ) -> Option>> { + self.unblockers.write().unwrap().remove(target_context_id) + } + + /// Insert an unblocker for the given context ID + /// + /// Returns the previous unblocker if one existed + pub(crate) fn insert_unblocker( + &self, + target_context_id: u64, + unblocker: Sender>, + ) -> Option>> { + self.unblockers + .write() + .unwrap() + .insert(target_context_id, unblocker) + } + + pub(crate) fn switch( + &self, + target_context_id: u64, + own_unblocker: Sender>, + ) -> Result<(), ContextSwitchError> { + // Lock contexts for this block + let mut contexts = self.unblockers.write().unwrap(); + let own_context_id = self.active_context_id(); + + // Assert preconditions (target is blocked && we are unblocked) + if contexts.get(&target_context_id).is_none() { + return Err(ContextSwitchError::SwitchTargetMissing); + } + if contexts.get(&own_context_id).is_some() { + return Err(ContextSwitchError::OwnContextAlreadyBlocked); + } + + // Unblock the target + // Dont mark ourself as blocked yet, as we first need to know that unblocking succeeded + let unblock_target = contexts.remove(&target_context_id).unwrap(); // Unwrap is safe due to precondition check above + let unblock_result: std::result::Result<(), std::result::Result<(), RuntimeError>> = + unblock_target.send(Ok(())); + let Ok(_) = unblock_result else { + // If there is no target to unblock, we assume it exited, but the unblock function was not removed + // For now we treat this like a missing context + // It can't happen again, as we already removed the unblock function + // + // TODO: Think about whether this is correct + tracing::trace!( + "Context {own_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" + ); + return Err(ContextSwitchError::SwitchUnblockFailed); + }; + + // After we have unblocked the target, we can insert our own unblock function + contexts.insert(own_context_id, own_unblocker); + Ok(()) + } +} diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 9d96d508450..4098f60ac1e 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -10,19 +10,21 @@ use crate::{ os::task::{ control_plane::ControlPlaneError, process::{WasiProcess, WasiProcessId}, - thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, + thread::{ + WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId, + context_switching::ContextSwitchingContext, + }, }, syscalls::platform_clock_time_get, - utils::thread_local_executor::ThreadLocalSpawner, }; -use futures::{channel::oneshot::Sender, future::BoxFuture}; +use futures::future::BoxFuture; use rand::Rng; use std::{ - collections::{BTreeMap, HashMap}, + collections::HashMap, ops::Deref, path::{Path, PathBuf}, str, - sync::{Arc, RwLock, atomic::AtomicU64}, + sync::Arc, time::Duration, }; use virtual_fs::{FileSystem, FsError, VirtualFile}; @@ -30,7 +32,7 @@ use virtual_mio::block_on; use virtual_net::DynVirtualNetworking; use wasmer::{ AsStoreMut, AsStoreRef, ExportError, FunctionEnvMut, Instance, Memory, MemoryType, MemoryView, - Module, RuntimeError, + Module, }; use wasmer_config::package::PackageSource; use wasmer_types::ModuleHash; @@ -182,11 +184,8 @@ pub struct WasiEnv { /// TODO: We should move this outside of `WasiEnv` with some refactoring inner: WasiInstanceHandlesPointer, - /// TODO: Document these fields - pub(crate) contexts: Arc>>>>, - pub(crate) current_context_id: Arc, - pub(crate) next_available_context_id: AtomicU64, - pub(crate) current_spawner: Option, + // TODO: Move this to something thread local + pub(crate) context_switching_context: Option>, } impl std::fmt::Debug for WasiEnv { @@ -216,14 +215,7 @@ impl Clone for WasiEnv { replaying_journal: self.replaying_journal, skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, - contexts: self.contexts.clone(), - // TODO: This is wrong; The contexts can't really be cloned. What do we even use clone on WasiEnv for? - current_context_id: self.current_context_id.clone(), - next_available_context_id: AtomicU64::new( - self.next_available_context_id - .load(std::sync::atomic::Ordering::SeqCst), - ), - current_spawner: self.current_spawner.clone(), + context_switching_context: None, } } } @@ -265,11 +257,7 @@ impl WasiEnv { replaying_journal: false, skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, - // TODO: Not sure if we can even properly fork coroutines at all - contexts: Default::default(), - current_context_id: Arc::new(AtomicU64::new(MAIN_CONTEXT_ID)), - next_available_context_id: AtomicU64::new(MAIN_CONTEXT_ID + 1), - current_spawner: None, + context_switching_context: None, }; Ok((new_env, handle)) } @@ -414,10 +402,7 @@ impl WasiEnv { bin_factory: init.bin_factory, capabilities: init.capabilities, disable_fs_cleanup: false, - contexts: Default::default(), - current_context_id: Arc::new(AtomicU64::new(MAIN_CONTEXT_ID)), - next_available_context_id: AtomicU64::new(MAIN_CONTEXT_ID + 1), - current_spawner: None, + context_switching_context: None, }; env.owned_handles.push(thread); diff --git a/lib/wasix/src/syscalls/wasix/context_delete.rs b/lib/wasix/src/syscalls/wasix/context_delete.rs index ff48544a7b7..0ac6668cbcf 100644 --- a/lib/wasix/src/syscalls/wasix/context_delete.rs +++ b/lib/wasix/src/syscalls/wasix/context_delete.rs @@ -26,7 +26,16 @@ pub fn context_delete( let env = ctx.data(); let memory: MemoryView<'_> = unsafe { env.memory_view(&ctx) }; - let own_context_id = env.current_context_id.load(Ordering::Relaxed); + // Verify that we are in an async context + let contexts = match &env.context_switching_context { + Some(c) => c, + None => { + tracing::trace!("Context switching is not enabled"); + return Ok(Errno::Again); + } + }; + + let own_context_id = contexts.active_context_id(); if own_context_id == target_context_id { tracing::trace!( "Context {} tried to delete itself, which is not allowed", @@ -44,7 +53,7 @@ pub fn context_delete( } // TODO: actually delete the context - let removed_future = env.contexts.write().unwrap().remove(&target_context_id); + let removed_future = contexts.remove_unblocker(&target_context_id); let Some(_) = removed_future else { // Context did not exist, so we do not need to remove it tracing::trace!( diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index b9e388d8381..f41dcfbd2b9 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -1,4 +1,5 @@ use super::*; +use crate::os::task::thread::context_switching::ContextSwitchingContext; use crate::state::MAIN_CONTEXT_ID; use crate::utils::thread_local_executor::ThreadLocalSpawnerError; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; @@ -53,16 +54,15 @@ pub fn lookup_typechecked_entrypoint( async fn launch_function( mut unsafe_static_store: StoreMut<'static>, + contexts: Arc, wait_for_unblock: Receiver>, - current_context_id: Arc, new_context_id: u64, - contexts_cloned: Arc>>>>, typechecked_entrypoint: Function, ) -> () { // Wait for the context to be unblocked let prelaunch_result = wait_for_unblock.await; // Restore our own context ID - current_context_id.store(new_context_id, Ordering::Relaxed); + contexts.set_active_context_id(new_context_id); // Handle if the context was canceled before it even started match prelaunch_result { @@ -111,11 +111,9 @@ async fn launch_function( }; // Retrieve the main context - let main_context = contexts_cloned - .write() - .unwrap() - .remove(&MAIN_CONTEXT_ID) - .expect("The main context should always be suspended when another context returns."); + let main_context = contexts.remove_unblocker(&MAIN_CONTEXT_ID).expect( + "The main context should always be suspended when another context returns or traps.", + ); // Resume the main context with the error main_context @@ -133,6 +131,15 @@ pub fn context_new( let (data, mut store) = ctx.data_and_store_mut(); + // Verify that we are in an async context + let contexts = match &data.context_switching_context { + Some(c) => c, + None => { + tracing::trace!("Context switching is not enabled"); + return Ok(Errno::Again); + } + }; + // Lookup and check the entrypoint function let typechecked_entrypoint = match lookup_typechecked_entrypoint(data, &mut store, entrypoint) { Ok(func) => func, @@ -141,47 +148,34 @@ pub fn context_new( } }; - // Verify that we are in an async context - let Some(spawner) = data.current_spawner.clone() else { - tracing::trace!("No async spawner set on WasiEnv. Did you enter the async env before?"); - return Ok(Errno::Again); - }; - // Create a new context ID - let new_context_id = data - .next_available_context_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let new_context_id = contexts.allocate_new_context_id(); // Write the new context ID into memory let memory = unsafe { data.memory_view(&store) }; wasi_try_mem_ok!(new_context_ptr.write(&memory, new_context_id)); // Setup sender and receiver for the new context - let wait_for_unblock = { - let (unblock, wait_for_unblock) = oneshot::channel::>(); - - let mut contexts = data.contexts.write().unwrap(); - // Store the unblock function into the WasiEnv - let None = contexts.insert(new_context_id, unblock) else { - panic!("There already is a context suspended with ID {new_context_id}"); - }; - wait_for_unblock + let (unblock, wait_for_unblock) = oneshot::channel::>(); + + // Store the unblock function into the WasiEnv + let None = contexts.insert_unblocker(new_context_id, unblock) else { + panic!("There already is a context suspended with ID {new_context_id}"); }; // Clone necessary arcs for the entrypoint future // SAFETY: Will be made safe with the proper wasmer async API let mut unsafe_static_store = unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; - let contexts_cloned = data.contexts.clone(); - let current_context_id: Arc = data.current_context_id.clone(); + let contexts_cloned = contexts.clone(); + let spawner = contexts.get_spawner(); // Create the future that will launch the entrypoint function let entrypoint_future = launch_function( unsafe_static_store, + contexts_cloned, wait_for_unblock, - current_context_id, new_context_id, - contexts_cloned, typechecked_entrypoint, ); diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index b61f64c5470..dd8cba3fcd5 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,4 +1,5 @@ use super::*; +use crate::os::task::thread::context_switching::ContextSwitchError; use crate::state::MAIN_CONTEXT_ID; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use anyhow::Result; @@ -64,8 +65,17 @@ fn inner_context_switch( let (data) = ctx.data_mut(); + // Verify that we are in an async context + let contexts = match &data.context_switching_context { + Some(c) => c, + None => { + tracing::trace!("Context switching is not enabled"); + return Err(Ok(Errno::Again)); + } + }; + // Get own context ID - let own_context_id = data.current_context_id.load(Ordering::Relaxed); + let own_context_id = contexts.active_context_id(); // If switching to self, do nothing if own_context_id == target_context_id { @@ -73,34 +83,27 @@ fn inner_context_switch( return Err(Ok(Errno::Success)); } + // TODO: Move into switch // Setup sender and receiver for the new context let (unblock, wait_for_unblock) = oneshot::channel::>(); // Try to unblock the target and put our unblock function into the env, if successful - { - // Lock contexts for this block - let mut contexts = data.contexts.write().unwrap(); - - // Assert preconditions (target is blocked && we are unblocked) - if contexts.get(&target_context_id).is_none() { + match contexts.switch(target_context_id, unblock) { + Ok(()) => {} + Err(ContextSwitchError::SwitchTargetMissing) => { tracing::trace!( "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" ); return Err(Ok(Errno::Inval)); } - if contexts.get(&own_context_id).is_some() { + Err(ContextSwitchError::OwnContextAlreadyBlocked) => { // This should never happen, because the active context should never have an unblock function (as it is not suspended) // If it does, it is an error in WASIX panic!("There is already a unblock present for the current context {own_context_id}"); } - - // Unblock the target - // Dont mark ourself as blocked yet, as we first need to know that unblocking succeeded - let unblock_target = contexts.remove(&target_context_id).unwrap(); // Unwrap is safe due to precondition check above - let unblock_result = unblock_target.send(Ok(())); - let Ok(_) = unblock_result else { - // If there is no target to unblock, we assume it exited, but the unblock function was not removed - // For now we treat this like a missing context + Err(ContextSwitchError::SwitchUnblockFailed) => { + // If there is no target to unblock, we assume it exited, but the unblock + // function was not removed. For now we treat this like a missing context // It can't happen again, as we already removed the unblock function // // TODO: Think about whether this is correct @@ -108,20 +111,17 @@ fn inner_context_switch( "Context {own_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" ); return Err(Ok(Errno::Inval)); - }; - - // After we have unblocked the target, we can insert our own unblock function - contexts.insert(own_context_id, unblock); + } }; // Clone necessary arcs for the future - let current_context_id = data.current_context_id.clone(); + let contexts_cloned = contexts.clone(); // Create the future that will resolve when this context is switched back to again Ok(async move { // Wait until we are unblocked again let result = wait_for_unblock.await; // Restore our own context ID - current_context_id.store(own_context_id, Ordering::Relaxed); + contexts_cloned.set_active_context_id(own_context_id); // Handle if we were canceled instead of beeing unblocked let result = match result { From 1937829b66d8f759cf3e0c2d8352bb27bca57b42 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 10:08:10 +0100 Subject: [PATCH 024/114] Move context switching logic to the context switching module --- .../src/os/task/thread/context_switching.rs | 23 +++++++++++++++---- .../src/syscalls/wasix/context_switch.rs | 8 ++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index b73300149b7..236a9dd2b84 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -3,7 +3,10 @@ use std::{ sync::{RwLock, atomic::AtomicU64}, }; -use futures::channel::oneshot::Sender; +use futures::{ + TryFutureExt, + channel::oneshot::{self, Sender}, +}; use thiserror::Error; use wasmer::RuntimeError; @@ -28,6 +31,10 @@ pub enum ContextSwitchError { OwnContextAlreadyBlocked, } +#[derive(Error, Debug)] +#[error("Context was cancelled")] +pub struct ContextCancelled(); + impl ContextSwitchingContext { pub(crate) fn new(spawner: ThreadLocalSpawner) -> Self { Self { @@ -81,8 +88,16 @@ impl ContextSwitchingContext { pub(crate) fn switch( &self, target_context_id: u64, - own_unblocker: Sender>, - ) -> Result<(), ContextSwitchError> { + ) -> Result< + impl Future, ContextCancelled>> + + Send + + Sync + + use<> + + 'static, + ContextSwitchError, + > { + let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); + // Lock contexts for this block let mut contexts = self.unblockers.write().unwrap(); let own_context_id = self.active_context_id(); @@ -114,6 +129,6 @@ impl ContextSwitchingContext { // After we have unblocked the target, we can insert our own unblock function contexts.insert(own_context_id, own_unblocker); - Ok(()) + Ok(async move { wait_for_unblock.map_err(|_| ContextCancelled()).await }) } } diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index dd8cba3fcd5..d66c4aa8356 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -83,13 +83,9 @@ fn inner_context_switch( return Err(Ok(Errno::Success)); } - // TODO: Move into switch - // Setup sender and receiver for the new context - let (unblock, wait_for_unblock) = oneshot::channel::>(); - // Try to unblock the target and put our unblock function into the env, if successful - match contexts.switch(target_context_id, unblock) { - Ok(()) => {} + let wait_for_unblock = match contexts.switch(target_context_id) { + Ok(wait_for_unblock) => wait_for_unblock, Err(ContextSwitchError::SwitchTargetMissing) => { tracing::trace!( "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" From 35e8c28867943689f708909b49bf6a7b59454573 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 10:55:14 +0100 Subject: [PATCH 025/114] Move remaining spawner interaction to context_switching --- .../src/os/task/thread/context_switching.rs | 72 ++++++++++++++-- lib/wasix/src/syscalls/wasix/context_new.rs | 82 +++++++------------ 2 files changed, 94 insertions(+), 60 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 236a9dd2b84..0eb7f564ec7 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -1,5 +1,6 @@ use std::{ collections::BTreeMap, + pin::Pin, sync::{RwLock, atomic::AtomicU64}, }; @@ -10,7 +11,7 @@ use futures::{ use thiserror::Error; use wasmer::RuntimeError; -use crate::utils::thread_local_executor::ThreadLocalSpawner; +use crate::utils::thread_local_executor::{ThreadLocalSpawner, ThreadLocalSpawnerError}; #[derive(Debug)] pub(crate) struct ContextSwitchingContext { @@ -18,7 +19,7 @@ pub(crate) struct ContextSwitchingContext { unblockers: RwLock>>>, current_context_id: AtomicU64, next_available_context_id: AtomicU64, - current_spawner: ThreadLocalSpawner, + spawner: ThreadLocalSpawner, } #[derive(Debug, Error)] @@ -41,7 +42,7 @@ impl ContextSwitchingContext { unblockers: RwLock::new(BTreeMap::new()), current_context_id: AtomicU64::new(0), next_available_context_id: AtomicU64::new(1), - current_spawner: spawner, + spawner, } } @@ -60,10 +61,6 @@ impl ContextSwitchingContext { .fetch_add(1, std::sync::atomic::Ordering::Relaxed) } - pub(crate) fn get_spawner(&self) -> ThreadLocalSpawner { - self.current_spawner.clone() - } - pub(crate) fn remove_unblocker( &self, target_context_id: &u64, @@ -131,4 +128,65 @@ impl ContextSwitchingContext { contexts.insert(own_context_id, own_unblocker); Ok(async move { wait_for_unblock.map_err(|_| ContextCancelled()).await }) } + + /// Create a new context and spawn it onto the thread-local executor + /// + /// The creator function is given the new context ID and a future that resolves when the context is unblocked + /// It is expected to return a future that waits for that future to be unblocked and then runs the context + /// + /// This function always succeeds or panics, as there are no recoverable errors possible when spawning onto the thread-local executor + pub(crate) fn new_context(&self, creator: T) -> u64 + where + T: FnOnce( + u64, + Pin< + Box< + dyn Future, ContextCancelled>> + + Send + + Sync + + 'static, + >, + >, + ) -> F, + F: Future + 'static, + { + // Create a new context ID + let new_context_id = self.allocate_new_context_id(); + + let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); + + // Store the unblocker + let None = self.insert_unblocker(new_context_id, own_unblocker) else { + panic!("There already is a context suspended with ID {new_context_id}"); + }; + + // Create the future for the new context + let future = creator( + new_context_id, + Box::pin(wait_for_unblock.map_err(|_| ContextCancelled())), + ); + + // Queue the future onto the thread-local executor + let spawn_result = self.spawner.spawn_local(future); + + match spawn_result { + Ok(()) => new_context_id, + Err(ThreadLocalSpawnerError::LocalPoolShutDown) => { + // TODO: Handle cancellation properly + panic!( + "Failed to spawn context {new_context_id} because the local executor has been shut down", + ); + } + Err(ThreadLocalSpawnerError::NotOnTheCorrectThread { expected, found }) => { + // Not on the correct host thread. If this error happens, it is a bug in WASIX. + panic!( + "Failed to spawn context {new_context_id} because the current thread ({found:?}) is not the expected thread ({expected:?}) for the local executor" + ) + } + Err(ThreadLocalSpawnerError::SpawnError) => { + // This should never happen + panic!("Failed to spawn_local context {new_context_id} , this should not happen"); + } + } + } } diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index f41dcfbd2b9..9db4e183e3a 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -1,5 +1,5 @@ use super::*; -use crate::os::task::thread::context_switching::ContextSwitchingContext; +use crate::os::task::thread::context_switching::{ContextCancelled, ContextSwitchingContext}; use crate::state::MAIN_CONTEXT_ID; use crate::utils::thread_local_executor::ThreadLocalSpawnerError; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; @@ -55,21 +55,24 @@ pub fn lookup_typechecked_entrypoint( async fn launch_function( mut unsafe_static_store: StoreMut<'static>, contexts: Arc, - wait_for_unblock: Receiver>, - new_context_id: u64, + wait_for_unblock: impl Future, ContextCancelled>> + + Send + + Sync + + 'static, + own_context_id: u64, typechecked_entrypoint: Function, ) -> () { // Wait for the context to be unblocked let prelaunch_result = wait_for_unblock.await; // Restore our own context ID - contexts.set_active_context_id(new_context_id); + contexts.set_active_context_id(own_context_id); // Handle if the context was canceled before it even started match prelaunch_result { Ok(_) => (), Err(canceled) => { tracing::trace!( - "Context {new_context_id} was canceled before it even started: {canceled}", + "Context {own_context_id} was canceled before it even started: {canceled}", ); // At this point we don't need to do anything else return; @@ -86,7 +89,7 @@ async fn launch_function( let error = match result { Err(e) => match e.downcast_ref::() { Some(s) => { - tracing::trace!("Context {new_context_id} exited with error string: {}", s); + tracing::trace!("Context {own_context_id} exited with error string: {}", s); // Context was cancelled, so we can just exit here. // // At this point we don't need to do anything else @@ -103,7 +106,7 @@ async fn launch_function( // TODO: Handle returning functions with a real error type RuntimeError::user( format!( - "Context {new_context_id} returned a value ({v:?}). This is not allowed for now" + "Context {own_context_id} returned a value ({v:?}). This is not allowed for now" ) .into(), ) @@ -148,58 +151,31 @@ pub fn context_new( } }; - // Create a new context ID - let new_context_id = contexts.allocate_new_context_id(); - - // Write the new context ID into memory - let memory = unsafe { data.memory_view(&store) }; - wasi_try_mem_ok!(new_context_ptr.write(&memory, new_context_id)); - - // Setup sender and receiver for the new context - let (unblock, wait_for_unblock) = oneshot::channel::>(); - - // Store the unblock function into the WasiEnv - let None = contexts.insert_unblocker(new_context_id, unblock) else { - panic!("There already is a context suspended with ID {new_context_id}"); - }; - // Clone necessary arcs for the entrypoint future // SAFETY: Will be made safe with the proper wasmer async API let mut unsafe_static_store = unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; let contexts_cloned = contexts.clone(); - let spawner = contexts.get_spawner(); - - // Create the future that will launch the entrypoint function - let entrypoint_future = launch_function( - unsafe_static_store, - contexts_cloned, - wait_for_unblock, - new_context_id, - typechecked_entrypoint, - ); - // Queue the future onto the thread-local executor - let spawn_result = spawner.spawn_local(entrypoint_future); + // Setup sender and receiver for the new context + let new_context_id = contexts.new_context(|new_context_id, wait_for_unblock| { + // Create the future that will launch the entrypoint function + let entrypoint_future = launch_function( + unsafe_static_store, + contexts_cloned, + wait_for_unblock, + new_context_id, + typechecked_entrypoint, + ); + + entrypoint_future + }); + + // Write the new context ID into memory + let memory = unsafe { data.memory_view(&store) }; + wasi_try_mem_ok!(new_context_ptr.write(&memory, new_context_id)); + + return Ok(Errno::Success); // Return failure if spawning failed - match spawn_result { - Ok(()) => Ok(Errno::Success), - Err(ThreadLocalSpawnerError::LocalPoolShutDown) => { - // TODO: Handle cancellation properly - panic!( - "Failed to spawn context {new_context_id} because the local executor has been shut down", - ); - } - Err(ThreadLocalSpawnerError::NotOnTheCorrectThread { expected, found }) => { - // Not on the correct host thread. If this error happens, it is a bug in WASIX. - panic!( - "Failed to spawn context {new_context_id} because the current thread ({found:?}) is not the expected thread ({expected:?}) for the local executor" - ) - } - Err(ThreadLocalSpawnerError::SpawnError) => { - // This should never happen - panic!("Failed to spawn_local context {new_context_id} , this should not happen"); - } - } } From cb46f97f3f95ae4af2a44d1c21f334315eff8a3e Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 11:08:56 +0100 Subject: [PATCH 026/114] Move more logic to context_switching --- .../src/os/task/thread/context_switching.rs | 45 ++++++++++--------- lib/wasix/src/syscalls/wasix/context_new.rs | 35 +++------------ 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 0eb7f564ec7..5568e8ba309 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -1,6 +1,5 @@ use std::{ collections::BTreeMap, - pin::Pin, sync::{RwLock, atomic::AtomicU64}, }; @@ -131,23 +130,12 @@ impl ContextSwitchingContext { /// Create a new context and spawn it onto the thread-local executor /// - /// The creator function is given the new context ID and a future that resolves when the context is unblocked - /// It is expected to return a future that waits for that future to be unblocked and then runs the context + /// The entrypoint function is called when the context is unblocked for the first time /// - /// This function always succeeds or panics, as there are no recoverable errors possible when spawning onto the thread-local executor - pub(crate) fn new_context(&self, creator: T) -> u64 + /// If the context is cancelled before it is unblocked, the entrypoint will not be called + pub(crate) fn new_context(&self, entrypoint: T) -> u64 where - T: FnOnce( - u64, - Pin< - Box< - dyn Future, ContextCancelled>> - + Send - + Sync - + 'static, - >, - >, - ) -> F, + T: FnOnce(u64) -> F + 'static, F: Future + 'static, { // Create a new context ID @@ -161,13 +149,28 @@ impl ContextSwitchingContext { }; // Create the future for the new context - let future = creator( - new_context_id, - Box::pin(wait_for_unblock.map_err(|_| ContextCancelled())), - ); + let context_future = async move { + // First wait for the unblock signal + let prelaunch_result = wait_for_unblock.map_err(|_| ContextCancelled()).await; + + // Handle if the context was canceled before it even started + match prelaunch_result { + Ok(_) => (), + Err(canceled) => { + tracing::trace!( + "Context {new_context_id} was canceled before it even started: {canceled}", + ); + // At this point we don't need to do anything else + return; + } + }; + + // Launch the context entrypoint + entrypoint(new_context_id).await + }; // Queue the future onto the thread-local executor - let spawn_result = self.spawner.spawn_local(future); + let spawn_result = self.spawner.spawn_local(context_future); match spawn_result { Ok(()) => new_context_id, diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 9db4e183e3a..4aefcb295c5 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -52,33 +52,15 @@ pub fn lookup_typechecked_entrypoint( Ok(entrypoint) } -async fn launch_function( +async fn async_entrypoint( mut unsafe_static_store: StoreMut<'static>, contexts: Arc, - wait_for_unblock: impl Future, ContextCancelled>> - + Send - + Sync - + 'static, own_context_id: u64, typechecked_entrypoint: Function, ) -> () { - // Wait for the context to be unblocked - let prelaunch_result = wait_for_unblock.await; // Restore our own context ID contexts.set_active_context_id(own_context_id); - // Handle if the context was canceled before it even started - match prelaunch_result { - Ok(_) => (), - Err(canceled) => { - tracing::trace!( - "Context {own_context_id} was canceled before it even started: {canceled}", - ); - // At this point we don't need to do anything else - return; - } - }; - // Actually call the entrypoint function let result = typechecked_entrypoint .call_async(&mut unsafe_static_store, &[]) @@ -157,25 +139,20 @@ pub fn context_new( unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; let contexts_cloned = contexts.clone(); - // Setup sender and receiver for the new context - let new_context_id = contexts.new_context(|new_context_id, wait_for_unblock| { - // Create the future that will launch the entrypoint function - let entrypoint_future = launch_function( + // Create the new context + let new_context_id = contexts.new_context(|new_context_id| { + async_entrypoint( unsafe_static_store, contexts_cloned, - wait_for_unblock, new_context_id, typechecked_entrypoint, - ); - - entrypoint_future + ) }); // Write the new context ID into memory let memory = unsafe { data.memory_view(&store) }; wasi_try_mem_ok!(new_context_ptr.write(&memory, new_context_id)); + // Return success return Ok(Errno::Success); - - // Return failure if spawning failed } From 5cc609aff182392be7ab2d12067e9b80fcfe0ba1 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 11:23:32 +0100 Subject: [PATCH 027/114] Move context id management into context_switching --- .../src/os/task/thread/context_switching.rs | 26 ++++++++++++------- lib/wasix/src/syscalls/wasix/context_new.rs | 3 --- .../src/syscalls/wasix/context_switch.rs | 2 -- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 5568e8ba309..9adb6e3abdf 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -1,6 +1,9 @@ use std::{ collections::BTreeMap, - sync::{RwLock, atomic::AtomicU64}, + sync::{ + Arc, RwLock, + atomic::{AtomicU64, Ordering}, + }, }; use futures::{ @@ -16,7 +19,7 @@ use crate::utils::thread_local_executor::{ThreadLocalSpawner, ThreadLocalSpawner pub(crate) struct ContextSwitchingContext { /// TODO: Document these fields unblockers: RwLock>>>, - current_context_id: AtomicU64, + current_context_id: Arc, next_available_context_id: AtomicU64, spawner: ThreadLocalSpawner, } @@ -39,22 +42,18 @@ impl ContextSwitchingContext { pub(crate) fn new(spawner: ThreadLocalSpawner) -> Self { Self { unblockers: RwLock::new(BTreeMap::new()), - current_context_id: AtomicU64::new(0), + current_context_id: Arc::new(AtomicU64::new(0)), next_available_context_id: AtomicU64::new(1), spawner, } } + // Get the currently active context ID pub(crate) fn active_context_id(&self) -> u64 { self.current_context_id .load(std::sync::atomic::Ordering::Relaxed) } - pub(crate) fn set_active_context_id(&self, context_id: u64) { - self.current_context_id - .store(context_id, std::sync::atomic::Ordering::Relaxed); - } - pub(crate) fn allocate_new_context_id(&self) -> u64 { self.next_available_context_id .fetch_add(1, std::sync::atomic::Ordering::Relaxed) @@ -125,7 +124,13 @@ impl ContextSwitchingContext { // After we have unblocked the target, we can insert our own unblock function contexts.insert(own_context_id, own_unblocker); - Ok(async move { wait_for_unblock.map_err(|_| ContextCancelled()).await }) + let current_context_id_cloned = self.current_context_id.clone(); + Ok(async move { + let unblock_result = wait_for_unblock.map_err(|_| ContextCancelled()).await; + // Restore our own context ID + current_context_id_cloned.store(own_context_id, std::sync::atomic::Ordering::Relaxed); + unblock_result + }) } /// Create a new context and spawn it onto the thread-local executor @@ -149,9 +154,12 @@ impl ContextSwitchingContext { }; // Create the future for the new context + let current_context_id_cloned = self.current_context_id.clone(); let context_future = async move { // First wait for the unblock signal let prelaunch_result = wait_for_unblock.map_err(|_| ContextCancelled()).await; + // Set the current context ID + current_context_id_cloned.store(new_context_id, Ordering::Relaxed); // Handle if the context was canceled before it even started match prelaunch_result { diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 4aefcb295c5..47e8f4a5f43 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -58,9 +58,6 @@ async fn async_entrypoint( own_context_id: u64, typechecked_entrypoint: Function, ) -> () { - // Restore our own context ID - contexts.set_active_context_id(own_context_id); - // Actually call the entrypoint function let result = typechecked_entrypoint .call_async(&mut unsafe_static_store, &[]) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index d66c4aa8356..fb035b1519e 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -116,8 +116,6 @@ fn inner_context_switch( Ok(async move { // Wait until we are unblocked again let result = wait_for_unblock.await; - // Restore our own context ID - contexts_cloned.set_active_context_id(own_context_id); // Handle if we were canceled instead of beeing unblocked let result = match result { From bc2e69e5c434f5500e5f02551d5f601054975ff7 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 11:33:39 +0100 Subject: [PATCH 028/114] Move Arc into ContextSwitchingContext --- .../src/os/task/thread/context_switching.rs | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 9adb6e3abdf..3a51fc90694 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -1,7 +1,7 @@ use std::{ collections::BTreeMap, sync::{ - Arc, RwLock, + Arc, RwLock, Weak, atomic::{AtomicU64, Ordering}, }, }; @@ -17,9 +17,15 @@ use crate::utils::thread_local_executor::{ThreadLocalSpawner, ThreadLocalSpawner #[derive(Debug)] pub(crate) struct ContextSwitchingContext { + /// TODO: Document these fields + inner: Arc, +} + +#[derive(Debug)] +pub(crate) struct ContextSwitchingContextInner { /// TODO: Document these fields unblockers: RwLock>>>, - current_context_id: Arc, + current_context_id: AtomicU64, next_available_context_id: AtomicU64, spawner: ThreadLocalSpawner, } @@ -41,21 +47,25 @@ pub struct ContextCancelled(); impl ContextSwitchingContext { pub(crate) fn new(spawner: ThreadLocalSpawner) -> Self { Self { - unblockers: RwLock::new(BTreeMap::new()), - current_context_id: Arc::new(AtomicU64::new(0)), - next_available_context_id: AtomicU64::new(1), - spawner, + inner: Arc::new(ContextSwitchingContextInner { + unblockers: RwLock::new(BTreeMap::new()), + current_context_id: AtomicU64::new(0), + next_available_context_id: AtomicU64::new(1), + spawner, + }), } } // Get the currently active context ID pub(crate) fn active_context_id(&self) -> u64 { - self.current_context_id + self.inner + .current_context_id .load(std::sync::atomic::Ordering::Relaxed) } pub(crate) fn allocate_new_context_id(&self) -> u64 { - self.next_available_context_id + self.inner + .next_available_context_id .fetch_add(1, std::sync::atomic::Ordering::Relaxed) } @@ -63,7 +73,11 @@ impl ContextSwitchingContext { &self, target_context_id: &u64, ) -> Option>> { - self.unblockers.write().unwrap().remove(target_context_id) + self.inner + .unblockers + .write() + .unwrap() + .remove(target_context_id) } /// Insert an unblocker for the given context ID @@ -74,7 +88,8 @@ impl ContextSwitchingContext { target_context_id: u64, unblocker: Sender>, ) -> Option>> { - self.unblockers + self.inner + .unblockers .write() .unwrap() .insert(target_context_id, unblocker) @@ -94,7 +109,7 @@ impl ContextSwitchingContext { let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); // Lock contexts for this block - let mut contexts = self.unblockers.write().unwrap(); + let mut contexts = self.inner.unblockers.write().unwrap(); let own_context_id = self.active_context_id(); // Assert preconditions (target is blocked && we are unblocked) @@ -124,11 +139,21 @@ impl ContextSwitchingContext { // After we have unblocked the target, we can insert our own unblock function contexts.insert(own_context_id, own_unblocker); - let current_context_id_cloned = self.current_context_id.clone(); + let weak_inner = Arc::downgrade(&self.inner); Ok(async move { let unblock_result = wait_for_unblock.map_err(|_| ContextCancelled()).await; + // Restore our own context ID - current_context_id_cloned.store(own_context_id, std::sync::atomic::Ordering::Relaxed); + let Some(inner) = Weak::upgrade(&weak_inner) else { + // The context switching context has been dropped, so we can't proceed + // TODO: Handle this properly + todo!(); + }; + inner + .current_context_id + .store(own_context_id, Ordering::Relaxed); + drop(inner); + unblock_result }) } @@ -154,12 +179,21 @@ impl ContextSwitchingContext { }; // Create the future for the new context - let current_context_id_cloned = self.current_context_id.clone(); + let weak_inner = Arc::downgrade(&self.inner); let context_future = async move { // First wait for the unblock signal let prelaunch_result = wait_for_unblock.map_err(|_| ContextCancelled()).await; + // Set the current context ID - current_context_id_cloned.store(new_context_id, Ordering::Relaxed); + let Some(inner) = Weak::upgrade(&weak_inner) else { + // The context switching context has been dropped, so we can't proceed + // TODO: Handle this properly + return; + }; + inner + .current_context_id + .store(new_context_id, Ordering::Relaxed); + drop(inner); // Handle if the context was canceled before it even started match prelaunch_result { @@ -178,7 +212,7 @@ impl ContextSwitchingContext { }; // Queue the future onto the thread-local executor - let spawn_result = self.spawner.spawn_local(context_future); + let spawn_result = self.inner.spawner.spawn_local(context_future); match spawn_result { Ok(()) => new_context_id, From 47d192f685df33aa78c3bf8b815c418b26c3f946 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 11:59:03 +0100 Subject: [PATCH 029/114] Move new context cancellation logic to context_switching --- .../src/os/task/thread/context_switching.rs | 34 +++++++- lib/wasix/src/syscalls/wasix/context_new.rs | 79 +++++-------------- 2 files changed, 53 insertions(+), 60 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 3a51fc90694..b3deefe3128 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -166,7 +166,7 @@ impl ContextSwitchingContext { pub(crate) fn new_context(&self, entrypoint: T) -> u64 where T: FnOnce(u64) -> F + 'static, - F: Future + 'static, + F: Future + 'static, { // Create a new context ID let new_context_id = self.allocate_new_context_id(); @@ -208,7 +208,37 @@ impl ContextSwitchingContext { }; // Launch the context entrypoint - entrypoint(new_context_id).await + let launch_result = entrypoint(new_context_id).await; + + // If that function returns something went wrong. + // If it's a cancellation, we can just let this context run out. + // If it's another error, we resume the main context with the error + let error = match launch_result.downcast_ref::() { + Some(err) => { + tracing::trace!("Context {new_context_id} exited with error: {}", err); + // Context was cancelled, so we can just let it run out. + return; + } + None => launch_result, // Propagate the runtime error to main + }; + + // Retrieve the main context + let Some(inner) = Weak::upgrade(&weak_inner) else { + // The context switching context has been dropped, so we can't proceed + // TODO: Handle this properly + return; + }; + let Some(main_context) = inner.unblockers.write().unwrap().remove(&0) else { + // The main context should always be suspended when another context returns or traps with anything but cancellation + panic!( + "The main context should always be suspended when another context returns or traps (with anything but a cancellation)." + ); + }; + // Resume the main context with the error + main_context + .send(Err(error)) + .expect("Failed to send error to main context, this should not happen"); + drop(inner); }; // Queue the future onto the thread-local executor diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 47e8f4a5f43..3ae219a74b6 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -52,57 +52,6 @@ pub fn lookup_typechecked_entrypoint( Ok(entrypoint) } -async fn async_entrypoint( - mut unsafe_static_store: StoreMut<'static>, - contexts: Arc, - own_context_id: u64, - typechecked_entrypoint: Function, -) -> () { - // Actually call the entrypoint function - let result = typechecked_entrypoint - .call_async(&mut unsafe_static_store, &[]) - .await; - - // If that function returns, we need to resume the main context with an error - // Take the underlying error, or create a new error if the context returned a value - let error = match result { - Err(e) => match e.downcast_ref::() { - Some(s) => { - tracing::trace!("Context {own_context_id} exited with error string: {}", s); - // Context was cancelled, so we can just exit here. - // - // At this point we don't need to do anything else - return; - } - None => { - // Propagate the runtime error to main - e - } - }, - Ok(v) => { - // Not really sure how we should handle this case - // - // TODO: Handle returning functions with a real error type - RuntimeError::user( - format!( - "Context {own_context_id} returned a value ({v:?}). This is not allowed for now" - ) - .into(), - ) - } - }; - - // Retrieve the main context - let main_context = contexts.remove_unblocker(&MAIN_CONTEXT_ID).expect( - "The main context should always be suspended when another context returns or traps.", - ); - - // Resume the main context with the error - main_context - .send(Err(error)) - .expect("Failed to send error to main context, this should not happen"); -} - #[instrument(level = "trace", skip(ctx), ret)] pub fn context_new( mut ctx: FunctionEnvMut<'_, WasiEnv>, @@ -134,16 +83,30 @@ pub fn context_new( // SAFETY: Will be made safe with the proper wasmer async API let mut unsafe_static_store = unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; - let contexts_cloned = contexts.clone(); // Create the new context let new_context_id = contexts.new_context(|new_context_id| { - async_entrypoint( - unsafe_static_store, - contexts_cloned, - new_context_id, - typechecked_entrypoint, - ) + // Sync part (not needed for now, but will make it easier to work with more complex entrypoints later) + async move { + // Call the entrypoint function + let result = typechecked_entrypoint + .call_async(&mut unsafe_static_store, &[]) + .await; + + // If that function returns, we need to resume the main context with an error + // Take the underlying error, or create a new error if the context returned a value + result.map_or_else( + |e| e, + |v| { + RuntimeError::user( + format!( + "Context {new_context_id} returned a value ({v:?}). This is not allowed for now" + ) + .into(), + ) + }, + ) + } }); // Write the new context ID into memory From 02d3e4c58677e7d7df97275486aeb2477fba1d14 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 12:06:20 +0100 Subject: [PATCH 030/114] Move all context cancelation handling to context_switching --- .../src/os/task/thread/context_switching.rs | 30 ++++++++++++------- lib/wasix/src/syscalls/wasix/context_new.rs | 2 +- .../src/syscalls/wasix/context_switch.rs | 17 +---------- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index b3deefe3128..43cc87c4bc1 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -41,8 +41,8 @@ pub enum ContextSwitchError { } #[derive(Error, Debug)] -#[error("Context was cancelled")] -pub struct ContextCancelled(); +#[error("Context was canceled")] +pub struct ContextCanceled(); impl ContextSwitchingContext { pub(crate) fn new(spawner: ThreadLocalSpawner) -> Self { @@ -99,11 +99,7 @@ impl ContextSwitchingContext { &self, target_context_id: u64, ) -> Result< - impl Future, ContextCancelled>> - + Send - + Sync - + use<> - + 'static, + impl Future> + Send + Sync + use<> + 'static, ContextSwitchError, > { let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); @@ -141,7 +137,7 @@ impl ContextSwitchingContext { contexts.insert(own_context_id, own_unblocker); let weak_inner = Arc::downgrade(&self.inner); Ok(async move { - let unblock_result = wait_for_unblock.map_err(|_| ContextCancelled()).await; + let unblock_result = wait_for_unblock.map_err(|_| ContextCanceled()).await; // Restore our own context ID let Some(inner) = Weak::upgrade(&weak_inner) else { @@ -154,7 +150,19 @@ impl ContextSwitchingContext { .store(own_context_id, Ordering::Relaxed); drop(inner); - unblock_result + // Handle if we were canceled instead of being unblocked + match unblock_result { + Ok(v) => v, + Err(canceled) => { + tracing::trace!( + "Context {own_context_id} was canceled while it was suspended: {}", + canceled + ); + + let err = ContextCanceled().into(); + return Err(RuntimeError::user(err)); + } + } }) } @@ -182,7 +190,7 @@ impl ContextSwitchingContext { let weak_inner = Arc::downgrade(&self.inner); let context_future = async move { // First wait for the unblock signal - let prelaunch_result = wait_for_unblock.map_err(|_| ContextCancelled()).await; + let prelaunch_result = wait_for_unblock.map_err(|_| ContextCanceled()).await; // Set the current context ID let Some(inner) = Weak::upgrade(&weak_inner) else { @@ -213,7 +221,7 @@ impl ContextSwitchingContext { // If that function returns something went wrong. // If it's a cancellation, we can just let this context run out. // If it's another error, we resume the main context with the error - let error = match launch_result.downcast_ref::() { + let error = match launch_result.downcast_ref::() { Some(err) => { tracing::trace!("Context {new_context_id} exited with error: {}", err); // Context was cancelled, so we can just let it run out. diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 3ae219a74b6..b2b384fb865 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -1,5 +1,5 @@ use super::*; -use crate::os::task::thread::context_switching::{ContextCancelled, ContextSwitchingContext}; +use crate::os::task::thread::context_switching::{ContextCanceled, ContextSwitchingContext}; use crate::state::MAIN_CONTEXT_ID; use crate::utils::thread_local_executor::ThreadLocalSpawnerError; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index fb035b1519e..748b91270ed 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -115,23 +115,8 @@ fn inner_context_switch( // Create the future that will resolve when this context is switched back to again Ok(async move { // Wait until we are unblocked again - let result = wait_for_unblock.await; - - // Handle if we were canceled instead of beeing unblocked - let result = match result { - Ok(v) => v, - Err(canceled) => { - tracing::trace!( - "Context {own_context_id} was canceled while it was suspended: {}", - canceled - ); - - let err = ContextError::Cancelled.into(); - return Err(RuntimeError::user(err)); - } - }; + wait_for_unblock.map(|v| v.map(|_| Errno::Success)).await // If we get relayed a trap, propagate it. Other wise return success - result.and(Ok(Errno::Success)) }) } From 0a2a08f18a697808bbfcb359d9783cc695fdc97a Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 12:38:12 +0100 Subject: [PATCH 031/114] Clean up context_switch --- .../src/syscalls/wasix/context_switch.rs | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 748b91270ed..346d36ed117 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -2,6 +2,7 @@ use super::*; use crate::os::task::thread::context_switching::ContextSwitchError; use crate::state::MAIN_CONTEXT_ID; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; +use MaybeLater::{Later, Now}; use anyhow::Result; use core::panic; use futures::TryFutureExt; @@ -17,28 +18,29 @@ use wasmer::{ }; use wasmer::{StoreMut, Tag, Type}; -/// Error type for errors internal to context switching -/// -/// Will be returned as a RuntimeError::User -#[derive(Error, Debug)] -pub(crate) enum ContextError { - // Should always be handled by the launch_entrypoint function and thus never propagated - // to the user - #[error("Context was cancelled. If you see this message, something went wrong.")] - Cancelled, -} - /// Switch to another context -#[instrument(level = "trace", skip(ctx), ret)] +#[instrument(level = "trace", skip(ctx))] pub fn context_switch( mut ctx: FunctionEnvMut, target_context_id: u64, ) -> impl Future> + Send + 'static + use<> { - let sync_part = inner_context_switch(ctx, target_context_id); - async move { - match sync_part { - Ok(fut) => fut.await, - Err(res) => res, + inner_context_switch(ctx, target_context_id).future() +} + +enum MaybeLater< + F: Future + Send + 'static, + T: Send + 'static = Result, +> { + Now(T), + Later(F), +} +impl + Send + 'static, T: Send + 'static> MaybeLater { + fn future(self) -> impl Future + Send + 'static { + async move { + match self { + MaybeLater::Now(v) => v, + MaybeLater::Later(fut) => fut.await, + } } } } @@ -51,15 +53,12 @@ pub fn context_switch( fn inner_context_switch( mut ctx: FunctionEnvMut, target_context_id: u64, -) -> Result< - impl Future> + Send + 'static + use<>, - Result, -> { +) -> MaybeLater> + Send + 'static + use<>> { // TODO: Should we call do_pending_operations here? match WasiEnv::do_pending_operations(&mut ctx) { Ok(()) => {} Err(e) => { - return Err(Err(RuntimeError::user(Box::new(e)))); + return Now(Err(RuntimeError::user(Box::new(e)))); } } @@ -70,7 +69,7 @@ fn inner_context_switch( Some(c) => c, None => { tracing::trace!("Context switching is not enabled"); - return Err(Ok(Errno::Again)); + return Now(Ok(Errno::Again)); } }; @@ -80,7 +79,7 @@ fn inner_context_switch( // If switching to self, do nothing if own_context_id == target_context_id { tracing::trace!("Switching context {own_context_id} to itself, which is a no-op"); - return Err(Ok(Errno::Success)); + return Now(Ok(Errno::Success)); } // Try to unblock the target and put our unblock function into the env, if successful @@ -90,7 +89,7 @@ fn inner_context_switch( tracing::trace!( "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" ); - return Err(Ok(Errno::Inval)); + return Now(Ok(Errno::Inval)); } Err(ContextSwitchError::OwnContextAlreadyBlocked) => { // This should never happen, because the active context should never have an unblock function (as it is not suspended) @@ -106,17 +105,10 @@ fn inner_context_switch( tracing::trace!( "Context {own_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" ); - return Err(Ok(Errno::Inval)); + return Now(Ok(Errno::Inval)); } }; - // Clone necessary arcs for the future - let contexts_cloned = contexts.clone(); - // Create the future that will resolve when this context is switched back to again - Ok(async move { - // Wait until we are unblocked again - wait_for_unblock.map(|v| v.map(|_| Errno::Success)).await - - // If we get relayed a trap, propagate it. Other wise return success - }) + // Wait until we are unblocked again + Later(wait_for_unblock.map(|v| v.map(|_| Errno::Success))) } From 89805c4169a39db571fd107f8e241f2c9bdb0742 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 12:45:48 +0100 Subject: [PATCH 032/114] Remove unnecessary Arc around context switching context --- lib/wasix/src/bin_factory/exec.rs | 2 +- lib/wasix/src/state/env.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 56d9a4ca601..e7494a975ab 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -265,7 +265,7 @@ fn call_with_threadlocal_executor( if env.context_switching_context.is_some() { panic!("Can not start a threadlocal executor as there is already one present"); } - env.context_switching_context = Some(Arc::new(ContextSwitchingContext::new(spawner))); + env.context_switching_context = Some(ContextSwitchingContext::new(spawner)); // Run function with the spawner let result = local_executor.run_until(entrypoint.call_async(&mut *store, ¶ms)); diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 4098f60ac1e..228508d305b 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -184,8 +184,10 @@ pub struct WasiEnv { /// TODO: We should move this outside of `WasiEnv` with some refactoring inner: WasiInstanceHandlesPointer, - // TODO: Move this to something thread local - pub(crate) context_switching_context: Option>, + /// Tracks the active contexts of the WASIX context switching API + /// + /// This is `None` when the main function was not launched with context switching + pub(crate) context_switching_context: Option, } impl std::fmt::Debug for WasiEnv { From acec740744dcbcd3ba2455937a3570313c1e191d Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 13:18:43 +0100 Subject: [PATCH 033/114] Move context switching entrypoint to context_switching --- lib/wasix/src/bin_factory/exec.rs | 41 +------------- .../src/os/task/thread/context_switching.rs | 54 ++++++++++++++++--- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index e7494a975ab..1a81e924e30 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -16,7 +16,6 @@ use crate::{ }, }, syscalls::rewind_ext, - utils::thread_local_executor::ThreadLocalExecutor, }; use crate::{Runtime, WasiEnv, WasiFunctionEnv}; use std::sync::Arc; @@ -243,43 +242,6 @@ fn get_start(ctx: &WasiFunctionEnv, store: &Store) -> Option { .ok() } -// TODO: Figure out a better place for this function -// Calls an async wasm in a blocking context -// -// This call blocks until the entrypoint returns, or it or any of the contexts it spawns traps -fn call_with_threadlocal_executor( - ctx: &WasiFunctionEnv, - mut store: &mut Store, - entrypoint: wasmer::Function, - params: Vec, -) -> Result, RuntimeError> { - // TODO: Maybe move this to the runtime or task manager, so that the runtime can - // control which executor is used. That would also allow using only one shared - // executor per thread. - // Create a new executor - let mut local_executor = ThreadLocalExecutor::new(); - - // Put the spawner into the WASI env, so that syscalls can use it to queue up new tasks - let spawner = local_executor.spawner(); - let env = ctx.data_mut(&mut store); - if env.context_switching_context.is_some() { - panic!("Can not start a threadlocal executor as there is already one present"); - } - env.context_switching_context = Some(ContextSwitchingContext::new(spawner)); - - // Run function with the spawner - let result = local_executor.run_until(entrypoint.call_async(&mut *store, ¶ms)); - - // Remove the spawner again - let env = ctx.data_mut(&mut store); - if env.context_switching_context.is_none() { - panic!("No context switching context present in env after the main function completed."); - } - env.context_switching_context = None; - - result -} - /// Calls the module fn call_module( ctx: WasiFunctionEnv, @@ -337,7 +299,8 @@ fn call_module( return; }; - let mut call_ret = call_with_threadlocal_executor(&ctx, &mut store, start.clone(), vec![]); + let mut call_ret = + ContextSwitchingContext::run_main_context(&ctx, &mut store, start.clone(), vec![]); loop { // Technically, it's an error for a vfork to return from main, but anyway... diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 43cc87c4bc1..eb102e399c7 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -1,3 +1,13 @@ +use crate::{ + WasiFunctionEnv, + utils::thread_local_executor::{ + ThreadLocalExecutor, ThreadLocalSpawner, ThreadLocalSpawnerError, + }, +}; +use futures::{ + TryFutureExt, + channel::oneshot::{self, Sender}, +}; use std::{ collections::BTreeMap, sync::{ @@ -5,16 +15,9 @@ use std::{ atomic::{AtomicU64, Ordering}, }, }; - -use futures::{ - TryFutureExt, - channel::oneshot::{self, Sender}, -}; use thiserror::Error; use wasmer::RuntimeError; -use crate::utils::thread_local_executor::{ThreadLocalSpawner, ThreadLocalSpawnerError}; - #[derive(Debug)] pub(crate) struct ContextSwitchingContext { /// TODO: Document these fields @@ -45,7 +48,7 @@ pub enum ContextSwitchError { pub struct ContextCanceled(); impl ContextSwitchingContext { - pub(crate) fn new(spawner: ThreadLocalSpawner) -> Self { + fn new(spawner: ThreadLocalSpawner) -> Self { Self { inner: Arc::new(ContextSwitchingContextInner { unblockers: RwLock::new(BTreeMap::new()), @@ -56,6 +59,41 @@ impl ContextSwitchingContext { } } + /// Run the main context function in a context switching context + /// + /// This call blocks until the entrypoint returns, or it or any of the contexts it spawns traps + pub(crate) fn run_main_context( + ctx: &WasiFunctionEnv, + mut store: &mut (impl wasmer::AsStoreMut + 'static), + entrypoint: wasmer::Function, + params: Vec, + ) -> Result, RuntimeError> { + // Create a new executor + let mut local_executor = ThreadLocalExecutor::new(); + + let this = Self::new(local_executor.spawner()); + + // Put the spawner into the WASI env, so that syscalls can use it to queue up new tasks + let env = ctx.data_mut(&mut store); + let previous_context = env.context_switching_context.replace(this); + if previous_context.is_some() { + panic!( + "Failed to start a wasix main context as there was already a context switching context present in the WASI env." + ); + } + + // Run function with the spawner + let result = local_executor.run_until(entrypoint.call_async(&mut *store, ¶ms)); + + // Remove the spawner again + let env = ctx.data_mut(&mut store); + env.context_switching_context.take().expect( + "Failed to remove wasix context switching context from WASI env after main context finished, this should never happen", + ); + + result + } + // Get the currently active context ID pub(crate) fn active_context_id(&self) -> u64 { self.inner From 1fbefb22f57b0e6229d9fa284b8b9e32e412ad9e Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 13:36:43 +0100 Subject: [PATCH 034/114] Replace the main context ID magic number with a constant --- .../src/os/task/thread/context_switching.rs | 25 +++--- lib/wasix/src/state/env.rs | 3 - lib/wasix/src/state/mod.rs | 4 +- .../src/syscalls/wasix/context_delete.rs | 5 +- lib/wasix/src/syscalls/wasix/context_new.rs | 1 - .../src/syscalls/wasix/context_switch.rs | 1 - lib/wasix/tests/cancel-in-context.rs | 79 +++++++++++++++++++ 7 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 lib/wasix/tests/cancel-in-context.rs diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index eb102e399c7..fb12a5e6f38 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -25,7 +25,7 @@ pub(crate) struct ContextSwitchingContext { } #[derive(Debug)] -pub(crate) struct ContextSwitchingContextInner { +struct ContextSwitchingContextInner { /// TODO: Document these fields unblockers: RwLock>>>, current_context_id: AtomicU64, @@ -43,6 +43,8 @@ pub enum ContextSwitchError { OwnContextAlreadyBlocked, } +const MAIN_CONTEXT_ID: u64 = 0; + #[derive(Error, Debug)] #[error("Context was canceled")] pub struct ContextCanceled(); @@ -52,8 +54,8 @@ impl ContextSwitchingContext { Self { inner: Arc::new(ContextSwitchingContextInner { unblockers: RwLock::new(BTreeMap::new()), - current_context_id: AtomicU64::new(0), - next_available_context_id: AtomicU64::new(1), + current_context_id: AtomicU64::new(MAIN_CONTEXT_ID), + next_available_context_id: AtomicU64::new(MAIN_CONTEXT_ID + 1), spawner, }), } @@ -94,17 +96,16 @@ impl ContextSwitchingContext { result } - // Get the currently active context ID + /// Get the ID of the currently active context pub(crate) fn active_context_id(&self) -> u64 { self.inner .current_context_id .load(std::sync::atomic::Ordering::Relaxed) } - pub(crate) fn allocate_new_context_id(&self) -> u64 { - self.inner - .next_available_context_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + /// Get the id of the main context (0) + pub(crate) fn main_context_id(&self) -> u64 { + MAIN_CONTEXT_ID } pub(crate) fn remove_unblocker( @@ -215,7 +216,10 @@ impl ContextSwitchingContext { F: Future + 'static, { // Create a new context ID - let new_context_id = self.allocate_new_context_id(); + let new_context_id = self + .inner + .next_available_context_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); @@ -274,7 +278,8 @@ impl ContextSwitchingContext { // TODO: Handle this properly return; }; - let Some(main_context) = inner.unblockers.write().unwrap().remove(&0) else { + let Some(main_context) = inner.unblockers.write().unwrap().remove(&MAIN_CONTEXT_ID) + else { // The main context should always be suspended when another context returns or traps with anything but cancellation panic!( "The main context should always be suspended when another context returns or traps (with anything but a cancellation)." diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 228508d305b..d43aed7cadc 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -46,9 +46,6 @@ use webc::metadata::annotations::Wasi; pub use super::handles::*; use super::{Linker, WasiState, conv_env_vars}; -// TODO: Figure out a better place for this constant -pub static MAIN_CONTEXT_ID: u64 = 0; - /// Data required to construct a [`WasiEnv`]. #[derive(Debug)] pub struct WasiEnvInit { diff --git a/lib/wasix/src/state/mod.rs b/lib/wasix/src/state/mod.rs index f5fe97f4519..18637cee3e0 100644 --- a/lib/wasix/src/state/mod.rs +++ b/lib/wasix/src/state/mod.rs @@ -39,9 +39,7 @@ use wasmer_wasix_types::wasi::{ pub use self::{ builder::*, - env::{ - MAIN_CONTEXT_ID, WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles, - }, + env::{WasiEnv, WasiEnvInit, WasiModuleInstanceHandles, WasiModuleTreeHandles}, func_env::WasiFunctionEnv, types::*, }; diff --git a/lib/wasix/src/syscalls/wasix/context_delete.rs b/lib/wasix/src/syscalls/wasix/context_delete.rs index 0ac6668cbcf..0d8dac431ee 100644 --- a/lib/wasix/src/syscalls/wasix/context_delete.rs +++ b/lib/wasix/src/syscalls/wasix/context_delete.rs @@ -1,5 +1,4 @@ use super::*; -use crate::state::MAIN_CONTEXT_ID; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use anyhow::Result; use core::panic; @@ -36,6 +35,8 @@ pub fn context_delete( }; let own_context_id = contexts.active_context_id(); + let main_context_id = contexts.main_context_id(); + if own_context_id == target_context_id { tracing::trace!( "Context {} tried to delete itself, which is not allowed", @@ -44,7 +45,7 @@ pub fn context_delete( return Ok(Errno::Inval); } - if target_context_id == MAIN_CONTEXT_ID { + if target_context_id == main_context_id { tracing::trace!( "Context {} tried to delete the main context, which is not allowed", own_context_id diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index b2b384fb865..5d4e6f3700e 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -1,6 +1,5 @@ use super::*; use crate::os::task::thread::context_switching::{ContextCanceled, ContextSwitchingContext}; -use crate::state::MAIN_CONTEXT_ID; use crate::utils::thread_local_executor::ThreadLocalSpawnerError; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use core::panic; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 346d36ed117..b54170191ef 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,6 +1,5 @@ use super::*; use crate::os::task::thread::context_switching::ContextSwitchError; -use crate::state::MAIN_CONTEXT_ID; use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use MaybeLater::{Later, Now}; use anyhow::Result; diff --git a/lib/wasix/tests/cancel-in-context.rs b/lib/wasix/tests/cancel-in-context.rs new file mode 100644 index 00000000000..efe57b40b62 --- /dev/null +++ b/lib/wasix/tests/cancel-in-context.rs @@ -0,0 +1,79 @@ +use std::path::PathBuf; +use std::process::Command; +use std::sync::Once; +use wasmer::Module; +use wasmer_types::ModuleHash; +use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; + +static INIT: Once = Once::new(); + +fn setup_tracing() { + INIT.call_once(|| { + // Set up tracing subscriber for tracing macros + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("debug")), + ) + .with_test_writer() + .try_init() + .ok(); + }); +} + +#[test] +fn cancel_in_context() { + // Setup tracing and log + setup_tracing(); + tracing::error!("Starting test: cancel_in_context"); + + // Set up tokio runtime + #[cfg(not(target_arch = "wasm32"))] + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + #[cfg(not(target_arch = "wasm32"))] + let handle = runtime.handle().clone(); + #[cfg(not(target_arch = "wasm32"))] + let _guard = handle.enter(); + + let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/cancel-in-context"); + + let main_c = test_dir.join("main.c"); + let main_wasm = test_dir.join("main.wasm"); + + // Compile with wasixcc + let compile_status = Command::new("wasixcc") + .arg(&main_c) + .arg("-sSYSROOT=/home/lennart/Documents/build-scripts/pkgs/cpython.sysroot") + .arg("-fwasm-exceptions") + .arg("-o") + .arg(&main_wasm) + .current_dir(&test_dir) + .status() + .expect("Failed to run wasixcc"); + + assert!(compile_status.success(), "wasixcc compilation failed"); + + // Load the compiled WASM module + let wasm_bytes = std::fs::read(&main_wasm).expect("Failed to read compiled WASM file"); + + let engine = wasmer::Engine::default(); + let module = Module::new(&engine, &wasm_bytes).expect("Failed to create module"); + + // Run the WASM module using WasiRunner + let runner = WasiRunner::new(); + let result = runner.run_wasm( + RuntimeOrEngine::Engine(engine), + "context-switching", + module, + ModuleHash::random(), + ); + + // Clean up + let _ = std::fs::remove_file(&main_wasm); + + // Assert the program ran successfully + assert!(result.is_ok(), "WASM execution failed: {:?}", result.err()); +} From 76b0f440e7077662a4ca57926ca09e2cfd1fa6ee Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 14:44:10 +0100 Subject: [PATCH 035/114] Add WASIX test for context switching --- lib/wasix/tests/.gitignore | 1 + lib/wasix/tests/context-switching.rs | 45 ++++++++++++++++++++ lib/wasix/tests/context-switching/main.c | 54 ++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 lib/wasix/tests/.gitignore create mode 100644 lib/wasix/tests/context-switching.rs create mode 100644 lib/wasix/tests/context-switching/main.c diff --git a/lib/wasix/tests/.gitignore b/lib/wasix/tests/.gitignore new file mode 100644 index 00000000000..32041ef1b80 --- /dev/null +++ b/lib/wasix/tests/.gitignore @@ -0,0 +1 @@ +*/*.wasm \ No newline at end of file diff --git a/lib/wasix/tests/context-switching.rs b/lib/wasix/tests/context-switching.rs new file mode 100644 index 00000000000..347da71c7c9 --- /dev/null +++ b/lib/wasix/tests/context-switching.rs @@ -0,0 +1,45 @@ +use std::path::PathBuf; +use std::process::Command; +use wasmer::Module; +use wasmer_types::ModuleHash; +use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; + +#[cfg(target_os = "linux")] +#[test] +fn test_context_switching() { + let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join(PathBuf::from( + file!().split('/').last().unwrap().trim_end_matches(".rs"), + )); + let main_c = test_dir.join("main.c"); + let main_wasm = test_dir.join("main.tmp.wasm"); + + // Compile with wasixcc + let mut command = Command::new("wasixcc"); + command + .arg(&main_c) + .arg("-fwasm-exceptions") + .arg("-o") + .arg(&main_wasm) + .current_dir(&test_dir); + eprintln!("Running wasixcc: {:?}", command); + let compile_status = command.status().expect("Failed to run wasixcc"); + assert!(compile_status.success(), "wasixcc compilation failed"); + + // Load the compiled WASM module + let wasm_bytes = std::fs::read(&main_wasm).expect("Failed to read compiled WASM file"); + let engine = wasmer::Engine::default(); + let module = Module::new(&engine, &wasm_bytes).expect("Failed to create module"); + + // Run the WASM module using WasiRunner + let runner = WasiRunner::new(); + runner + .run_wasm( + RuntimeOrEngine::Engine(engine), + "wasix-test", + module, + ModuleHash::random(), + ) + .unwrap(); +} diff --git a/lib/wasix/tests/context-switching/main.c b/lib/wasix/tests/context-switching/main.c new file mode 100644 index 00000000000..0a06d3ed5a7 --- /dev/null +++ b/lib/wasix/tests/context-switching/main.c @@ -0,0 +1,54 @@ +#include +#include +#include +#include +#include + +wasix_context_id_t context1; +wasix_context_id_t context2; + +char* message = "Uninitialized\n"; +int stop = 0; +int counter = 0; + +void test1(void) { + while (1) { + wasix_context_switch(context2); + if (stop == 1) { + wasix_context_switch(context_main_context); + } + counter++; + printf("%s", message); + } +} + +void test2(void) { + printf("Starting test2\n"); + + message = "Switch 1\n"; + wasix_context_switch(context1); + + message = "Switch 2\n"; + wasix_context_switch(context1); + + message = "Switch 3\n"; + wasix_context_switch(context1); + + message = "Switch 4\n"; + wasix_context_switch(context1); + + stop = 1; + wasix_context_switch(context1); + + exit(50); +} + +int main() { + wasix_context_new(&context1, test1); + wasix_context_new(&context2, test2); + wasix_context_switch(context1); + + assert(counter == 4); + + return 0; +} \ No newline at end of file From cabd70d36674b17fa305edc100b25c7f75f0fa7a Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 16:30:03 +0100 Subject: [PATCH 036/114] Make thread_spawn work with contexts --- lib/wasix/src/syscalls/wasix/thread_spawn.rs | 34 ++++++++++++-------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/thread_spawn.rs b/lib/wasix/src/syscalls/wasix/thread_spawn.rs index 35778280021..b2c180af1ff 100644 --- a/lib/wasix/src/syscalls/wasix/thread_spawn.rs +++ b/lib/wasix/src/syscalls/wasix/thread_spawn.rs @@ -5,7 +5,7 @@ use super::*; use crate::journal::JournalEffector; use crate::{ WasiThreadHandle, - os::task::thread::WasiMemoryLayout, + os::task::thread::{WasiMemoryLayout, context_switching::ContextSwitchingContext}, runtime::{ TaintReason, task_manager::{TaskWasm, TaskWasmRunProperties}, @@ -185,7 +185,7 @@ pub fn thread_spawn_internal_using_layout( // This function calls into the module fn call_module_internal( - env: &WasiFunctionEnv, + ctx: &WasiFunctionEnv, store: &mut Store, start_ptr_offset: M::Offset, ) -> Result<(), DeepSleepWork> { @@ -193,28 +193,36 @@ fn call_module_internal( //trace!("threading: invoking thread callback (reactor={})", reactor); // Note: we ensure both unwraps can happen before getting to this point - let spawn = env + let spawn = ctx .data(&store) .inner() .main_module_instance_handles() .thread_spawn .clone() .unwrap(); - let tid = env.data(&store).tid(); - let thread_result = spawn.call( + let tid = ctx.data(&store).tid(); + // TODO: Find a better way to get a Function from a TypedFunction + // SAFETY: no + let spawn = unsafe { std::mem::transmute::, Function>(spawn) }; + let tid_i32 = tid.raw().try_into().map_err(|_| Errno::Overflow).unwrap(); + let start_pointer_i32 = start_ptr_offset + .try_into() + .map_err(|_| Errno::Overflow) + .unwrap(); + let thread_result = ContextSwitchingContext::run_main_context( + ctx, store, - tid.raw().try_into().map_err(|_| Errno::Overflow).unwrap(), - start_ptr_offset - .try_into() - .map_err(|_| Errno::Overflow) - .unwrap(), - ); + spawn, + vec![Value::I32(tid_i32), Value::I32(start_pointer_i32)], + ) + .map(|_| ()); + trace!("callback finished (ret={:?})", thread_result); - let exit_code = handle_thread_result(env, store, thread_result)?; + let exit_code = handle_thread_result(ctx, store, thread_result)?; // Clean up the environment on exit - env.on_exit(store, exit_code); + ctx.on_exit(store, exit_code); Ok(()) } From 667c070431624e510c9e9fb80eef1f79d005f9d9 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 16:39:39 +0100 Subject: [PATCH 037/114] Wrap call to initialize in run_exec in contexts --- lib/wasix/src/bin_factory/exec.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 1a81e924e30..14e0af2fe21 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -196,7 +196,10 @@ pub fn run_exec(props: TaskWasmRunProperties) { .get_function("_initialize") { let initialize = initialize.clone(); - if let Err(err) = initialize.call(&mut store, &[]) { + + if let Err(err) = + ContextSwitchingContext::run_main_context(&ctx, &mut store, initialize, vec![]) + { thread.thread.set_status_finished(Err(err.into())); ctx.data(&store) .blocking_on_exit(Some(Errno::Noexec.into())); From e173c68bd6288b43214b4ded682a893a4d543820 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 17:27:00 +0100 Subject: [PATCH 038/114] Move one more call to _initialize to run in the main context --- lib/wasix/src/state/env.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index d43aed7cadc..2beff268f1d 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -568,16 +568,27 @@ impl WasiEnv { } // If this module exports an _initialize function, run that first. - if call_initialize - && let Ok(initialize) = instance.exports.get_function("_initialize") - && let Err(err) = crate::run_wasi_func_start(initialize, &mut store) - { - func_env - .data(&store) - .blocking_on_exit(Some(Errno::Noexec.into())); - return Err(WasiThreadError::InitFailed(Arc::new(anyhow::Error::from( - err, - )))); + if call_initialize && let Ok(initialize) = instance.exports.get_function("_initialize") { + // TODO: Fix lifetime issues once rebased onto the static store branch + let mut static_store = unsafe { + std::mem::transmute::, wasmer::StoreMut<'static>>( + store.as_store_mut(), + ) + }; + let initialize_result = ContextSwitchingContext::run_main_context( + &func_env, + &mut static_store, + initialize.clone(), + vec![], + ); + if let Err(err) = initialize_result { + func_env + .data(&store) + .blocking_on_exit(Some(Errno::Noexec.into())); + return Err(WasiThreadError::InitFailed(Arc::new(anyhow::Error::from( + err, + )))); + } } Ok((instance, func_env)) From ce2080bbdd3a398f1cba55c6f545eb2f4f188bca Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 24 Nov 2025 17:27:20 +0100 Subject: [PATCH 039/114] Remove unused imports --- lib/wasix/src/syscalls/wasix/context_delete.rs | 2 +- lib/wasix/src/syscalls/wasix/context_new.rs | 2 +- lib/wasix/src/syscalls/wasix/context_switch.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_delete.rs b/lib/wasix/src/syscalls/wasix/context_delete.rs index 0d8dac431ee..c4fed28c444 100644 --- a/lib/wasix/src/syscalls/wasix/context_delete.rs +++ b/lib/wasix/src/syscalls/wasix/context_delete.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; +use crate::syscalls::*; use anyhow::Result; use core::panic; use futures::TryFutureExt; diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_new.rs index 5d4e6f3700e..4a8eb90344a 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_new.rs @@ -1,7 +1,7 @@ use super::*; use crate::os::task::thread::context_switching::{ContextCanceled, ContextSwitchingContext}; +use crate::syscalls::*; use crate::utils::thread_local_executor::ThreadLocalSpawnerError; -use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; use core::panic; use futures::TryFutureExt; use futures::channel::oneshot::{Receiver, Sender}; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index b54170191ef..b987b6bcb79 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,6 +1,6 @@ use super::*; use crate::os::task::thread::context_switching::ContextSwitchError; -use crate::{run_wasi_func, run_wasi_func_start, syscalls::*}; +use crate::syscalls::*; use MaybeLater::{Later, Now}; use anyhow::Result; use core::panic; From cf01013165343a6da24f78043325342948feda57 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 25 Nov 2025 10:53:13 +0100 Subject: [PATCH 040/114] Add more wasixcc based switching tests --- lib/wasix/tests/.gitignore | 2 +- ...text-switching.rs => context_switching.rs} | 47 +++++++++---- .../simple_switching.c} | 2 +- .../context_switching/switching_in_threads.c | 70 +++++++++++++++++++ .../switching_to_a_deleted_context.c | 46 ++++++++++++ .../context_switching/switching_with_main.c | 38 ++++++++++ 6 files changed, 190 insertions(+), 15 deletions(-) rename lib/wasix/tests/{context-switching.rs => context_switching.rs} (57%) rename lib/wasix/tests/{context-switching/main.c => context_switching/simple_switching.c} (98%) create mode 100644 lib/wasix/tests/context_switching/switching_in_threads.c create mode 100644 lib/wasix/tests/context_switching/switching_to_a_deleted_context.c create mode 100644 lib/wasix/tests/context_switching/switching_with_main.c diff --git a/lib/wasix/tests/.gitignore b/lib/wasix/tests/.gitignore index 32041ef1b80..dba9407aa1f 100644 --- a/lib/wasix/tests/.gitignore +++ b/lib/wasix/tests/.gitignore @@ -1 +1 @@ -*/*.wasm \ No newline at end of file +*/*.test.wasm \ No newline at end of file diff --git a/lib/wasix/tests/context-switching.rs b/lib/wasix/tests/context_switching.rs similarity index 57% rename from lib/wasix/tests/context-switching.rs rename to lib/wasix/tests/context_switching.rs index 347da71c7c9..6d045dc7090 100644 --- a/lib/wasix/tests/context-switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -4,16 +4,15 @@ use wasmer::Module; use wasmer_types::ModuleHash; use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; -#[cfg(target_os = "linux")] -#[test] -fn test_context_switching() { +fn test_with_wasixcc(name: &str) -> Result<(), anyhow::Error> { + eprintln!("Compiling test case: {}", name); let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") .join(PathBuf::from( file!().split('/').last().unwrap().trim_end_matches(".rs"), )); - let main_c = test_dir.join("main.c"); - let main_wasm = test_dir.join("main.tmp.wasm"); + let main_c = test_dir.join(format!("{name}.c")); + let main_wasm = test_dir.join(format!("{name}.test.wasm")); // Compile with wasixcc let mut command = Command::new("wasixcc"); @@ -34,12 +33,34 @@ fn test_context_switching() { // Run the WASM module using WasiRunner let runner = WasiRunner::new(); - runner - .run_wasm( - RuntimeOrEngine::Engine(engine), - "wasix-test", - module, - ModuleHash::random(), - ) - .unwrap(); + runner.run_wasm( + RuntimeOrEngine::Engine(engine), + "wasix-test", + module, + ModuleHash::random(), + ) +} + +#[cfg(target_os = "linux")] +#[test] +fn test_simple_switching() { + test_with_wasixcc("simple_switching").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_switching_with_main() { + test_with_wasixcc("switching_with_main").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_switching_to_a_deleted_context() { + test_with_wasixcc("switching_to_a_deleted_context").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_switching_threads() { + test_with_wasixcc("switching_in_threads").unwrap(); } diff --git a/lib/wasix/tests/context-switching/main.c b/lib/wasix/tests/context_switching/simple_switching.c similarity index 98% rename from lib/wasix/tests/context-switching/main.c rename to lib/wasix/tests/context_switching/simple_switching.c index 0a06d3ed5a7..5b1f61d63fa 100644 --- a/lib/wasix/tests/context-switching/main.c +++ b/lib/wasix/tests/context_switching/simple_switching.c @@ -40,7 +40,7 @@ void test2(void) { stop = 1; wasix_context_switch(context1); - exit(50); + exit(1); } int main() { diff --git a/lib/wasix/tests/context_switching/switching_in_threads.c b/lib/wasix/tests/context_switching/switching_in_threads.c new file mode 100644 index 00000000000..22202007fdf --- /dev/null +++ b/lib/wasix/tests/context_switching/switching_in_threads.c @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include +#include + +#ifndef NULL +#define NULL ((void*)0) +#endif + +wasix_context_id_t context1; +wasix_context_id_t context2; + +char* message = "Uninitialized\n"; +int stop = 0; +int counter = 0; + +void test1(void) { + while (1) { + wasix_context_switch(context2); + if (stop == 1) { + wasix_context_switch(context_main_context); + } + counter++; + printf("%s", message); + } +} + +void test2(void) { + printf("Starting test2\n"); + + message = "Switch 1\n"; + wasix_context_switch(context1); + + message = "Switch 2\n"; + wasix_context_switch(context1); + + message = "Switch 3\n"; + wasix_context_switch(context1); + + message = "Switch 4\n"; + wasix_context_switch(context1); + + stop = 1; + wasix_context_switch(context1); + + exit(50); +} + +void* abort_in_thread(void* arg) { + wasix_context_new(&context1, test1); + wasix_context_new(&context2, test2); + wasix_context_switch(context1); + + return NULL; +} + +int main() { + pthread_t thread; + pthread_create(&thread, NULL, abort_in_thread, NULL); + pthread_join(thread, NULL); + + if(counter != 4) { + printf("Error: expected counter to be 4 but it is %d\n", counter); + exit(1); + } + + return 0; +} \ No newline at end of file diff --git a/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c b/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c new file mode 100644 index 00000000000..d1b50eb667f --- /dev/null +++ b/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include + +wasix_context_id_t context1; +wasix_context_id_t context2; + +int counter = 0; + +void test1(void) { + counter += 1; + wasix_context_switch(context_main_context); +} + +// Required because switching with the real main currently segfaults +void test_main(void) { + wasix_context_new(&context1, test1); + wasix_context_new(&context2, test1); + + // Assert that test1 increments the context. + assert(counter == 0); + wasix_context_switch(context1); + assert(counter == 1); + + // Assert that calling the deleteed context again fails + wasix_context_delete(context1); + wasix_context_switch(context1); + assert(counter == 1); + + // Assert that switching to a context that was deleteed before the first switch works + wasix_context_delete(context2); + wasix_context_switch(context2); + assert(counter == 1); + + exit(0); +} + +int main() { + wasix_context_id_t test_main_context; + wasix_context_new(&test_main_context, test_main); + wasix_context_switch(test_main_context); + + return 0; +} \ No newline at end of file diff --git a/lib/wasix/tests/context_switching/switching_with_main.c b/lib/wasix/tests/context_switching/switching_with_main.c new file mode 100644 index 00000000000..f55d86a6a31 --- /dev/null +++ b/lib/wasix/tests/context_switching/switching_with_main.c @@ -0,0 +1,38 @@ +#include +#include +#include +#include +#include + +wasix_context_id_t context1; +wasix_context_id_t context2; + +int stop = 0; +int counter = 0; + +void test1(void) { + while (1) { + wasix_context_switch(context_main_context); + if (stop == 1) { + wasix_context_switch(context_main_context); + break; + } + counter++; + } +} + +int main() { + wasix_context_new(&context1, test1); + wasix_context_switch(context1); + + + wasix_context_switch(context1); + wasix_context_switch(context1); + wasix_context_switch(context1); + wasix_context_switch(context1); + stop = 1; + wasix_context_switch(context1); + + assert(counter == 4); + return 0; +} \ No newline at end of file From 3ee06096e069b48ac3ee958ab0891ccdb69fb0a1 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 25 Nov 2025 15:43:24 +0100 Subject: [PATCH 041/114] Rename context new/delete to create/destroy --- lib/wasix/src/lib.rs | 7 +++++-- .../syscalls/wasix/{context_new.rs => context_create.rs} | 2 +- .../wasix/{context_delete.rs => context_destroy.rs} | 2 +- lib/wasix/src/syscalls/wasix/mod.rs | 8 ++++---- 4 files changed, 11 insertions(+), 8 deletions(-) rename lib/wasix/src/syscalls/wasix/{context_new.rs => context_create.rs} (99%) rename lib/wasix/src/syscalls/wasix/{context_delete.rs => context_destroy.rs} (98%) diff --git a/lib/wasix/src/lib.rs b/lib/wasix/src/lib.rs index c692a9ba001..92320811beb 100644 --- a/lib/wasix/src/lib.rs +++ b/lib/wasix/src/lib.rs @@ -617,9 +617,9 @@ fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) "sched_yield" => Function::new_typed_with_env(&mut store, env, sched_yield::), "stack_checkpoint" => Function::new_typed_with_env(&mut store, env, stack_checkpoint::), "stack_restore" => Function::new_typed_with_env(&mut store, env, stack_restore::), - "context_new" => Function::new_typed_with_env(&mut store, env, context_new::), + "context_create" => Function::new_typed_with_env(&mut store, env, context_create::), "context_switch" => Function::new_typed_with_env_async(&mut store, env, context_switch), - "context_delete" => Function::new_typed_with_env(&mut store, env, context_delete), + "context_destroy" => Function::new_typed_with_env(&mut store, env, context_destroy), "futex_wait" => Function::new_typed_with_env(&mut store, env, futex_wait::), "futex_wake" => Function::new_typed_with_env(&mut store, env, futex_wake::), "futex_wake_all" => Function::new_typed_with_env(&mut store, env, futex_wake_all::), @@ -760,6 +760,9 @@ fn wasix_exports_64(mut store: &mut impl AsStoreMut, env: &FunctionEnv) "sched_yield" => Function::new_typed_with_env(&mut store, env, sched_yield::), "stack_checkpoint" => Function::new_typed_with_env(&mut store, env, stack_checkpoint::), "stack_restore" => Function::new_typed_with_env(&mut store, env, stack_restore::), + "context_create" => Function::new_typed_with_env(&mut store, env, context_create::), + "context_switch" => Function::new_typed_with_env_async(&mut store, env, context_switch), + "context_destroy" => Function::new_typed_with_env(&mut store, env, context_destroy), "futex_wait" => Function::new_typed_with_env(&mut store, env, futex_wait::), "futex_wake" => Function::new_typed_with_env(&mut store, env, futex_wake::), "futex_wake_all" => Function::new_typed_with_env(&mut store, env, futex_wake_all::), diff --git a/lib/wasix/src/syscalls/wasix/context_new.rs b/lib/wasix/src/syscalls/wasix/context_create.rs similarity index 99% rename from lib/wasix/src/syscalls/wasix/context_new.rs rename to lib/wasix/src/syscalls/wasix/context_create.rs index 4a8eb90344a..0380f17cfe3 100644 --- a/lib/wasix/src/syscalls/wasix/context_new.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -52,7 +52,7 @@ pub fn lookup_typechecked_entrypoint( } #[instrument(level = "trace", skip(ctx), ret)] -pub fn context_new( +pub fn context_create( mut ctx: FunctionEnvMut<'_, WasiEnv>, new_context_ptr: WasmPtr, entrypoint: u32, diff --git a/lib/wasix/src/syscalls/wasix/context_delete.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs similarity index 98% rename from lib/wasix/src/syscalls/wasix/context_delete.rs rename to lib/wasix/src/syscalls/wasix/context_destroy.rs index c4fed28c444..76825910fe5 100644 --- a/lib/wasix/src/syscalls/wasix/context_delete.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -16,7 +16,7 @@ use wasmer::{StoreMut, Tag, Type}; /// ### `context_delete()` #[instrument(level = "trace", skip(ctx), ret)] -pub fn context_delete( +pub fn context_destroy( mut ctx: FunctionEnvMut<'_, WasiEnv>, target_context_id: u64, ) -> Result { diff --git a/lib/wasix/src/syscalls/wasix/mod.rs b/lib/wasix/src/syscalls/wasix/mod.rs index bb8e04c1663..7617a367b1a 100644 --- a/lib/wasix/src/syscalls/wasix/mod.rs +++ b/lib/wasix/src/syscalls/wasix/mod.rs @@ -4,8 +4,8 @@ mod chdir; mod closure_allocate; mod closure_free; mod closure_prepare; -mod context_delete; -mod context_new; +mod context_create; +mod context_destroy; mod context_switch; mod dl_invalid_handle; mod dlopen; @@ -94,8 +94,8 @@ pub use chdir::*; pub use closure_allocate::*; pub use closure_free::*; pub use closure_prepare::*; -pub use context_delete::*; -pub use context_new::*; +pub use context_create::*; +pub use context_destroy::*; pub use context_switch::*; pub use dl_invalid_handle::*; pub use dlopen::*; From 6472a5e5afa0d05bdbddaf16a027da9b6f73c881 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 25 Nov 2025 15:45:37 +0100 Subject: [PATCH 042/114] Update context-switching tests to the new names --- lib/wasix/tests/cancel-in-context.rs | 79 ------------------- .../context_switching/simple_switching.c | 56 ++++++------- .../context_switching/switching_in_threads.c | 72 ++++++++--------- .../switching_to_a_deleted_context.c | 55 ++++++------- .../context_switching/switching_with_main.c | 39 +++++---- 5 files changed, 111 insertions(+), 190 deletions(-) delete mode 100644 lib/wasix/tests/cancel-in-context.rs diff --git a/lib/wasix/tests/cancel-in-context.rs b/lib/wasix/tests/cancel-in-context.rs deleted file mode 100644 index efe57b40b62..00000000000 --- a/lib/wasix/tests/cancel-in-context.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::path::PathBuf; -use std::process::Command; -use std::sync::Once; -use wasmer::Module; -use wasmer_types::ModuleHash; -use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; - -static INIT: Once = Once::new(); - -fn setup_tracing() { - INIT.call_once(|| { - // Set up tracing subscriber for tracing macros - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("debug")), - ) - .with_test_writer() - .try_init() - .ok(); - }); -} - -#[test] -fn cancel_in_context() { - // Setup tracing and log - setup_tracing(); - tracing::error!("Starting test: cancel_in_context"); - - // Set up tokio runtime - #[cfg(not(target_arch = "wasm32"))] - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - #[cfg(not(target_arch = "wasm32"))] - let handle = runtime.handle().clone(); - #[cfg(not(target_arch = "wasm32"))] - let _guard = handle.enter(); - - let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/cancel-in-context"); - - let main_c = test_dir.join("main.c"); - let main_wasm = test_dir.join("main.wasm"); - - // Compile with wasixcc - let compile_status = Command::new("wasixcc") - .arg(&main_c) - .arg("-sSYSROOT=/home/lennart/Documents/build-scripts/pkgs/cpython.sysroot") - .arg("-fwasm-exceptions") - .arg("-o") - .arg(&main_wasm) - .current_dir(&test_dir) - .status() - .expect("Failed to run wasixcc"); - - assert!(compile_status.success(), "wasixcc compilation failed"); - - // Load the compiled WASM module - let wasm_bytes = std::fs::read(&main_wasm).expect("Failed to read compiled WASM file"); - - let engine = wasmer::Engine::default(); - let module = Module::new(&engine, &wasm_bytes).expect("Failed to create module"); - - // Run the WASM module using WasiRunner - let runner = WasiRunner::new(); - let result = runner.run_wasm( - RuntimeOrEngine::Engine(engine), - "context-switching", - module, - ModuleHash::random(), - ); - - // Clean up - let _ = std::fs::remove_file(&main_wasm); - - // Assert the program ran successfully - assert!(result.is_ok(), "WASM execution failed: {:?}", result.err()); -} diff --git a/lib/wasix/tests/context_switching/simple_switching.c b/lib/wasix/tests/context_switching/simple_switching.c index 5b1f61d63fa..7f4c1945378 100644 --- a/lib/wasix/tests/context_switching/simple_switching.c +++ b/lib/wasix/tests/context_switching/simple_switching.c @@ -1,54 +1,54 @@ -#include -#include #include #include +#include +#include #include wasix_context_id_t context1; wasix_context_id_t context2; -char* message = "Uninitialized\n"; +char *message = "Uninitialized\n"; int stop = 0; int counter = 0; void test1(void) { - while (1) { - wasix_context_switch(context2); - if (stop == 1) { - wasix_context_switch(context_main_context); - } - counter++; - printf("%s", message); + while (1) { + wasix_context_switch(context2); + if (stop == 1) { + wasix_context_switch(context_main_context); } + counter++; + printf("%s", message); + } } void test2(void) { - printf("Starting test2\n"); + printf("Starting test2\n"); - message = "Switch 1\n"; - wasix_context_switch(context1); + message = "Switch 1\n"; + wasix_context_switch(context1); - message = "Switch 2\n"; - wasix_context_switch(context1); + message = "Switch 2\n"; + wasix_context_switch(context1); - message = "Switch 3\n"; - wasix_context_switch(context1); + message = "Switch 3\n"; + wasix_context_switch(context1); - message = "Switch 4\n"; - wasix_context_switch(context1); + message = "Switch 4\n"; + wasix_context_switch(context1); - stop = 1; - wasix_context_switch(context1); + stop = 1; + wasix_context_switch(context1); - exit(1); + exit(1); } int main() { - wasix_context_new(&context1, test1); - wasix_context_new(&context2, test2); - wasix_context_switch(context1); - - assert(counter == 4); + wasix_context_create(&context1, test1); + wasix_context_create(&context2, test2); + wasix_context_switch(context1); + + assert(counter == 4); - return 0; + return 0; } \ No newline at end of file diff --git a/lib/wasix/tests/context_switching/switching_in_threads.c b/lib/wasix/tests/context_switching/switching_in_threads.c index 22202007fdf..c7ca22c7533 100644 --- a/lib/wasix/tests/context_switching/switching_in_threads.c +++ b/lib/wasix/tests/context_switching/switching_in_threads.c @@ -1,70 +1,70 @@ -#include -#include #include #include #include +#include +#include #include #ifndef NULL -#define NULL ((void*)0) +#define NULL ((void *)0) #endif wasix_context_id_t context1; wasix_context_id_t context2; -char* message = "Uninitialized\n"; +char *message = "Uninitialized\n"; int stop = 0; int counter = 0; void test1(void) { - while (1) { - wasix_context_switch(context2); - if (stop == 1) { - wasix_context_switch(context_main_context); - } - counter++; - printf("%s", message); + while (1) { + wasix_context_switch(context2); + if (stop == 1) { + wasix_context_switch(context_main_context); } + counter++; + printf("%s", message); + } } void test2(void) { - printf("Starting test2\n"); + printf("Starting test2\n"); - message = "Switch 1\n"; - wasix_context_switch(context1); + message = "Switch 1\n"; + wasix_context_switch(context1); - message = "Switch 2\n"; - wasix_context_switch(context1); + message = "Switch 2\n"; + wasix_context_switch(context1); - message = "Switch 3\n"; - wasix_context_switch(context1); + message = "Switch 3\n"; + wasix_context_switch(context1); - message = "Switch 4\n"; - wasix_context_switch(context1); + message = "Switch 4\n"; + wasix_context_switch(context1); - stop = 1; - wasix_context_switch(context1); + stop = 1; + wasix_context_switch(context1); - exit(50); + exit(50); } -void* abort_in_thread(void* arg) { - wasix_context_new(&context1, test1); - wasix_context_new(&context2, test2); - wasix_context_switch(context1); +void *abort_in_thread(void *arg) { + wasix_context_create(&context1, test1); + wasix_context_create(&context2, test2); + wasix_context_switch(context1); - return NULL; + return NULL; } int main() { - pthread_t thread; - pthread_create(&thread, NULL, abort_in_thread, NULL); - pthread_join(thread, NULL); + pthread_t thread; + pthread_create(&thread, NULL, abort_in_thread, NULL); + pthread_join(thread, NULL); - if(counter != 4) { - printf("Error: expected counter to be 4 but it is %d\n", counter); - exit(1); - } + if (counter != 4) { + printf("Error: expected counter to be 4 but it is %d\n", counter); + exit(1); + } - return 0; + return 0; } \ No newline at end of file diff --git a/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c b/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c index d1b50eb667f..aca92c196cb 100644 --- a/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c +++ b/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c @@ -1,7 +1,7 @@ -#include -#include #include #include +#include +#include #include wasix_context_id_t context1; @@ -10,37 +10,38 @@ wasix_context_id_t context2; int counter = 0; void test1(void) { - counter += 1; - wasix_context_switch(context_main_context); + counter += 1; + wasix_context_switch(context_main_context); } // Required because switching with the real main currently segfaults void test_main(void) { - wasix_context_new(&context1, test1); - wasix_context_new(&context2, test1); - - // Assert that test1 increments the context. - assert(counter == 0); - wasix_context_switch(context1); - assert(counter == 1); - - // Assert that calling the deleteed context again fails - wasix_context_delete(context1); - wasix_context_switch(context1); - assert(counter == 1); - - // Assert that switching to a context that was deleteed before the first switch works - wasix_context_delete(context2); - wasix_context_switch(context2); - assert(counter == 1); - - exit(0); + wasix_context_create(&context1, test1); + wasix_context_create(&context2, test1); + + // Assert that test1 increments the context. + assert(counter == 0); + wasix_context_switch(context1); + assert(counter == 1); + + // Assert that calling the destroyed context again fails + wasix_context_destroy(context1); + wasix_context_switch(context1); + assert(counter == 1); + + // Assert that switching to a context that was destroyed before the first + // switch works + wasix_context_destroy(context2); + wasix_context_switch(context2); + assert(counter == 1); + + exit(0); } int main() { - wasix_context_id_t test_main_context; - wasix_context_new(&test_main_context, test_main); - wasix_context_switch(test_main_context); + wasix_context_id_t test_main_context; + wasix_context_create(&test_main_context, test_main); + wasix_context_switch(test_main_context); - return 0; + return 0; } \ No newline at end of file diff --git a/lib/wasix/tests/context_switching/switching_with_main.c b/lib/wasix/tests/context_switching/switching_with_main.c index f55d86a6a31..043a4d0a018 100644 --- a/lib/wasix/tests/context_switching/switching_with_main.c +++ b/lib/wasix/tests/context_switching/switching_with_main.c @@ -1,7 +1,7 @@ -#include -#include #include #include +#include +#include #include wasix_context_id_t context1; @@ -11,28 +11,27 @@ int stop = 0; int counter = 0; void test1(void) { - while (1) { - wasix_context_switch(context_main_context); - if (stop == 1) { - wasix_context_switch(context_main_context); - break; - } - counter++; + while (1) { + wasix_context_switch(context_main_context); + if (stop == 1) { + wasix_context_switch(context_main_context); + break; } + counter++; + } } int main() { - wasix_context_new(&context1, test1); - wasix_context_switch(context1); - + wasix_context_create(&context1, test1); + wasix_context_switch(context1); - wasix_context_switch(context1); - wasix_context_switch(context1); - wasix_context_switch(context1); - wasix_context_switch(context1); - stop = 1; - wasix_context_switch(context1); + wasix_context_switch(context1); + wasix_context_switch(context1); + wasix_context_switch(context1); + wasix_context_switch(context1); + stop = 1; + wasix_context_switch(context1); - assert(counter == 4); - return 0; + assert(counter == 4); + return 0; } \ No newline at end of file From 221a47fa95315a60aa23c538ed781a418d6e1e8d Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 28 Nov 2025 11:47:51 +0100 Subject: [PATCH 043/114] Add todo for moving the context switching file --- lib/wasix/src/os/task/thread/context_switching.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index fb12a5e6f38..4b99f3519b6 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -1,3 +1,4 @@ +// TODO: Move file out of thread folder use crate::{ WasiFunctionEnv, utils::thread_local_executor::{ From 2f95154e168746e2a26e22eec7c168ec8d2ec316 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 28 Nov 2025 11:57:19 +0100 Subject: [PATCH 044/114] Adjust context_switch for the changed async API --- .../src/syscalls/wasix/context_switch.rs | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index b987b6bcb79..9c2914e079c 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -12,18 +12,18 @@ use std::sync::atomic::AtomicU32; use std::sync::{Arc, OnceLock, RwLock}; use thiserror::Error; use wasmer::{ - AsStoreMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, Module, - RuntimeError, Store, Value, imports, + AsStoreMut, AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, + Memory, Module, RuntimeError, Store, Value, imports, }; use wasmer::{StoreMut, Tag, Type}; - +// TODO: combine context_switch and inner_context_switch /// Switch to another context #[instrument(level = "trace", skip(ctx))] pub fn context_switch( - mut ctx: FunctionEnvMut, + mut ctx: AsyncFunctionEnvMut, target_context_id: u64, -) -> impl Future> + Send + 'static + use<> { - inner_context_switch(ctx, target_context_id).future() +) -> impl Future> + 'static + use<> { + inner_context_switch(ctx, target_context_id) } enum MaybeLater< @@ -49,26 +49,27 @@ impl + Send + 'static, T: Send + 'static> MaybeLater /// The order of operations in here is quite delicate, so be careful when /// modifying this function. It's important to not leave the env in /// an inconsistent state. -fn inner_context_switch( - mut ctx: FunctionEnvMut, +async fn inner_context_switch( + mut ctx: AsyncFunctionEnvMut, target_context_id: u64, -) -> MaybeLater> + Send + 'static + use<>> { - // TODO: Should we call do_pending_operations here? - match WasiEnv::do_pending_operations(&mut ctx) { - Ok(()) => {} - Err(e) => { - return Now(Err(RuntimeError::user(Box::new(e)))); - } - } - - let (data) = ctx.data_mut(); +) -> Result { + // // TODO: Should we call do_pending_operations here? + // match WasiEnv::do_pending_operations(&mut ctx) { + // Ok(()) => {} + // Err(e) => { + // return Now(Err(RuntimeError::user(Box::new(e)))); + // } + // } + let mut write_lock = ctx.write().await; + // let (data) = ctx.(); + let data = write_lock.data_mut(); // Verify that we are in an async context let contexts = match &data.context_switching_context { Some(c) => c, None => { tracing::trace!("Context switching is not enabled"); - return Now(Ok(Errno::Again)); + return Ok(Errno::Again); } }; @@ -78,7 +79,7 @@ fn inner_context_switch( // If switching to self, do nothing if own_context_id == target_context_id { tracing::trace!("Switching context {own_context_id} to itself, which is a no-op"); - return Now(Ok(Errno::Success)); + return Ok(Errno::Success); } // Try to unblock the target and put our unblock function into the env, if successful @@ -88,7 +89,7 @@ fn inner_context_switch( tracing::trace!( "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" ); - return Now(Ok(Errno::Inval)); + return Ok(Errno::Inval); } Err(ContextSwitchError::OwnContextAlreadyBlocked) => { // This should never happen, because the active context should never have an unblock function (as it is not suspended) @@ -104,10 +105,10 @@ fn inner_context_switch( tracing::trace!( "Context {own_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" ); - return Now(Ok(Errno::Inval)); + return Ok(Errno::Inval); } }; // Wait until we are unblocked again - Later(wait_for_unblock.map(|v| v.map(|_| Errno::Success))) + wait_for_unblock.map(|v| v.map(|_| Errno::Success)).await } From 963dba7da6662f0b2ca8b46708d790521ea43df0 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 28 Nov 2025 12:01:26 +0100 Subject: [PATCH 045/114] Adjust context_create for the changed API --- lib/wasix/src/syscalls/wasix/context_create.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 0380f17cfe3..032edc621b9 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -59,6 +59,15 @@ pub fn context_create( ) -> Result { WasiEnv::do_pending_operations(&mut ctx)?; + // TODO: Unify this check with the one below for context_switching_context + let mut async_store = match ctx.as_store_async() { + Some(c) => c, + None => { + tracing::trace!("The current store is not async"); + return Ok(Errno::Again); + } + }; + let (data, mut store) = ctx.data_and_store_mut(); // Verify that we are in an async context @@ -78,18 +87,13 @@ pub fn context_create( } }; - // Clone necessary arcs for the entrypoint future - // SAFETY: Will be made safe with the proper wasmer async API - let mut unsafe_static_store = - unsafe { std::mem::transmute::, StoreMut<'static>>(store.as_store_mut()) }; - // Create the new context let new_context_id = contexts.new_context(|new_context_id| { // Sync part (not needed for now, but will make it easier to work with more complex entrypoints later) async move { // Call the entrypoint function - let result = typechecked_entrypoint - .call_async(&mut unsafe_static_store, &[]) + let result: Result, RuntimeError> = typechecked_entrypoint + .call_async(&mut async_store, &[]) .await; // If that function returns, we need to resume the main context with an error From ba84494aa41910ff245dd6b6fc6bf1881863132b Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 15:55:13 +0100 Subject: [PATCH 046/114] Adjust WASIX context switching to work with the real wasmer async api --- lib/wasix/src/bin_factory/exec.rs | 191 +++++++++--------- .../src/os/task/thread/context_switching.rs | 14 +- lib/wasix/src/state/env.rs | 13 +- .../src/syscalls/wasix/context_create.rs | 2 +- lib/wasix/src/syscalls/wasix/thread_spawn.rs | 21 +- 5 files changed, 117 insertions(+), 124 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 14e0af2fe21..ab9e85559df 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -185,31 +185,30 @@ pub fn run_exec(props: TaskWasmRunProperties) { let recycle = props.recycle; // Perform the initialization - let ctx = { - // If this module exports an _initialize function, run that first. - if let Ok(initialize) = ctx - .data(&store) - .inner() - .main_module_instance_handles() - .instance - .exports - .get_function("_initialize") - { - let initialize = initialize.clone(); - - if let Err(err) = - ContextSwitchingContext::run_main_context(&ctx, &mut store, initialize, vec![]) - { - thread.thread.set_status_finished(Err(err.into())); - ctx.data(&store) - .blocking_on_exit(Some(Errno::Noexec.into())); - unsafe { run_recycle(recycle, ctx, store) }; - return; - } + // If this module exports an _initialize function, run that first. + if let Ok(initialize) = ctx + .data(&store) + .inner() + .main_module_instance_handles() + .instance + .exports + .get_function("_initialize") + .cloned() + { + // TODO: The call to initialize for the main module does not yet run in an context-switching environment. + // The call to initialize for dlopen'ed modules does run in an context-switching environment. + // This should be made consistent. + // TODO: Or document that the context-switching environment only starts with main + let result = initialize.call(&mut store, &[]); + + if let Err(err) = result { + thread.thread.set_status_finished(Err(err.into())); + ctx.data(&store) + .blocking_on_exit(Some(Errno::Noexec.into())); + unsafe { run_recycle(recycle, ctx, store) }; + return; } - - WasiFunctionEnv { env: ctx.env } - }; + } // Bootstrap the process // Unsafe: The bootstrap must be executed in the same thread that runs the @@ -292,86 +291,86 @@ fn call_module( } // Invoke the start function - let ret = { - // Call the module - let Some(start) = get_start(&ctx, &store) else { - debug!("wasi[{}]::exec-failed: missing _start function", pid); - ctx.data(&store) - .blocking_on_exit(Some(Errno::Noexec.into())); - unsafe { run_recycle(recycle, ctx, store) }; - return; - }; - - let mut call_ret = - ContextSwitchingContext::run_main_context(&ctx, &mut store, start.clone(), vec![]); - - loop { - // Technically, it's an error for a vfork to return from main, but anyway... - match resume_vfork(&ctx, &mut store, &start, &call_ret) { - // A vfork was resumed, there may be another, so loop back - Ok(Some(ret)) => call_ret = ret, - - // An error was encountered when restoring from the vfork, report it - Err(e) => { - call_ret = Err(RuntimeError::user(Box::new(WasiError::Exit(e.into())))); - break; - } + // Call the module + let Some(start) = get_start(&ctx, &store) else { + debug!("wasi[{}]::exec-failed: missing _start function", pid); + ctx.data(&store) + .blocking_on_exit(Some(Errno::Noexec.into())); + unsafe { run_recycle(recycle, ctx, store) }; + return; + }; - // No vfork, keep the call_ret value - Ok(None) => break, + let (mut store, mut call_ret) = + ContextSwitchingContext::run_main_context(&ctx, store, start.clone(), vec![]); + + loop { + // Technically, it's an error for a vfork to return from main, but anyway... + // TODO: Then we should probably just trap immediately when that happens... + // this code is complex enough as it is + match resume_vfork(&ctx, &mut store, &start, &call_ret) { + // A vfork was resumed, there may be another, so loop back + Ok(Some(ret)) => call_ret = ret, + + // An error was encountered when restoring from the vfork, report it + Err(e) => { + call_ret = Err(RuntimeError::user(Box::new(WasiError::Exit(e.into())))); + break; } + + // No vfork, keep the call_ret value + Ok(None) => break, } + } - if let Err(err) = call_ret { - match err.downcast::() { - Ok(WasiError::Exit(code)) if code.is_success() => Ok(Errno::Success), - Ok(WasiError::ThreadExit) => Ok(Errno::Success), - Ok(WasiError::Exit(code)) => { - runtime.on_taint(TaintReason::NonZeroExitCode(code)); - Err(WasiError::Exit(code).into()) - } - Ok(WasiError::DeepSleep(deep)) => { - // Create the callback that will be invoked when the thread respawns after a deep sleep - let rewind = deep.rewind; - let respawn = { - move |ctx, store, rewind_result| { - // Call the thread - call_module( - ctx, - store, - handle, - Some((rewind, RewindResultType::RewindWithResult(rewind_result))), - recycle, - ); - } - }; - - // Spawns the WASM process after a trigger - if let Err(err) = unsafe { - tasks.resume_wasm_after_poller(Box::new(respawn), ctx, store, deep.trigger) - } { - debug!("failed to go into deep sleep - {}", err); + let ret = if let Err(err) = call_ret { + match err.downcast::() { + Ok(WasiError::Exit(code)) if code.is_success() => Ok(Errno::Success), + Ok(WasiError::ThreadExit) => Ok(Errno::Success), + Ok(WasiError::Exit(code)) => { + runtime.on_taint(TaintReason::NonZeroExitCode(code)); + Err(WasiError::Exit(code).into()) + } + Ok(WasiError::DeepSleep(deep)) => { + // Create the callback that will be invoked when the thread respawns after a deep sleep + let rewind = deep.rewind; + let respawn = { + move |ctx, store, rewind_result| { + // Call the thread + call_module( + ctx, + store, + handle, + Some((rewind, RewindResultType::RewindWithResult(rewind_result))), + recycle, + ); } - return; - } - Ok(WasiError::UnknownWasiVersion) => { - debug!("failed as wasi version is unknown"); - runtime.on_taint(TaintReason::UnknownWasiVersion); - Ok(Errno::Noexec) - } - Ok(WasiError::DlSymbolResolutionFailed(symbol)) => { - debug!("failed as a needed DL symbol could not be resolved"); - runtime.on_taint(TaintReason::DlSymbolResolutionFailed(symbol.clone())); - Err(WasiError::DlSymbolResolutionFailed(symbol).into()) - } - Err(err) => { - runtime.on_taint(TaintReason::RuntimeError(err.clone())); - Err(WasiRuntimeError::from(err)) + }; + + // Spawns the WASM process after a trigger + if let Err(err) = unsafe { + tasks.resume_wasm_after_poller(Box::new(respawn), ctx, store, deep.trigger) + } { + debug!("failed to go into deep sleep - {}", err); } + return; + } + Ok(WasiError::UnknownWasiVersion) => { + debug!("failed as wasi version is unknown"); + runtime.on_taint(TaintReason::UnknownWasiVersion); + Ok(Errno::Noexec) + } + Ok(WasiError::DlSymbolResolutionFailed(symbol)) => { + debug!("failed as a needed DL symbol could not be resolved"); + runtime.on_taint(TaintReason::DlSymbolResolutionFailed(symbol.clone())); + Err(WasiError::DlSymbolResolutionFailed(symbol).into()) + } + Err(err) => { + runtime.on_taint(TaintReason::RuntimeError(err.clone())); + Err(WasiRuntimeError::from(err)) } - } else { - Ok(Errno::Success) } + } else { + Ok(Errno::Success) }; let code = if let Err(err) = &ret { diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 4b99f3519b6..cb77fbd4e42 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -1,4 +1,3 @@ -// TODO: Move file out of thread folder use crate::{ WasiFunctionEnv, utils::thread_local_executor::{ @@ -17,7 +16,7 @@ use std::{ }, }; use thiserror::Error; -use wasmer::RuntimeError; +use wasmer::{AsStoreRef, RuntimeError, Store}; #[derive(Debug)] pub(crate) struct ContextSwitchingContext { @@ -67,10 +66,10 @@ impl ContextSwitchingContext { /// This call blocks until the entrypoint returns, or it or any of the contexts it spawns traps pub(crate) fn run_main_context( ctx: &WasiFunctionEnv, - mut store: &mut (impl wasmer::AsStoreMut + 'static), + mut store: Store, entrypoint: wasmer::Function, params: Vec, - ) -> Result, RuntimeError> { + ) -> (Store, Result, RuntimeError>) { // Create a new executor let mut local_executor = ThreadLocalExecutor::new(); @@ -85,16 +84,19 @@ impl ContextSwitchingContext { ); } + let store_async = store.into_async(); // Run function with the spawner - let result = local_executor.run_until(entrypoint.call_async(&mut *store, ¶ms)); + let result = local_executor.run_until(entrypoint.call_async(&store_async, params)); // Remove the spawner again + let mut store = store_async.into_store().ok().unwrap(); + let env = ctx.data_mut(&mut store); env.context_switching_context.take().expect( "Failed to remove wasix context switching context from WASI env after main context finished, this should never happen", ); - result + (store, result) } /// Get the ID of the currently active context diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 2beff268f1d..c0e0f179c31 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -569,18 +569,7 @@ impl WasiEnv { // If this module exports an _initialize function, run that first. if call_initialize && let Ok(initialize) = instance.exports.get_function("_initialize") { - // TODO: Fix lifetime issues once rebased onto the static store branch - let mut static_store = unsafe { - std::mem::transmute::, wasmer::StoreMut<'static>>( - store.as_store_mut(), - ) - }; - let initialize_result = ContextSwitchingContext::run_main_context( - &func_env, - &mut static_store, - initialize.clone(), - vec![], - ); + let initialize_result = initialize.call(&mut store, &[]); if let Err(err) = initialize_result { func_env .data(&store) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 032edc621b9..5f1b95af14d 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -93,7 +93,7 @@ pub fn context_create( async move { // Call the entrypoint function let result: Result, RuntimeError> = typechecked_entrypoint - .call_async(&mut async_store, &[]) + .call_async(&mut async_store, vec![]) .await; // If that function returns, we need to resume the main context with an error diff --git a/lib/wasix/src/syscalls/wasix/thread_spawn.rs b/lib/wasix/src/syscalls/wasix/thread_spawn.rs index b2c180af1ff..6e87af8cee4 100644 --- a/lib/wasix/src/syscalls/wasix/thread_spawn.rs +++ b/lib/wasix/src/syscalls/wasix/thread_spawn.rs @@ -186,9 +186,9 @@ pub fn thread_spawn_internal_using_layout( // This function calls into the module fn call_module_internal( ctx: &WasiFunctionEnv, - store: &mut Store, + mut store: Store, start_ptr_offset: M::Offset, -) -> Result<(), DeepSleepWork> { +) -> (Store, Result<(), DeepSleepWork>) { // We either call the reactor callback or the thread spawn callback //trace!("threading: invoking thread callback (reactor={})", reactor); @@ -209,21 +209,24 @@ fn call_module_internal( .try_into() .map_err(|_| Errno::Overflow) .unwrap(); - let thread_result = ContextSwitchingContext::run_main_context( + let (mut store, thread_result) = ContextSwitchingContext::run_main_context( ctx, store, spawn, vec![Value::I32(tid_i32), Value::I32(start_pointer_i32)], - ) - .map(|_| ()); + ); + let thread_result = thread_result.map(|_| ()); trace!("callback finished (ret={:?})", thread_result); - let exit_code = handle_thread_result(ctx, store, thread_result)?; + let exit_code = match handle_thread_result(ctx, &mut store, thread_result) { + Ok(code) => code, + Err(deep_sleep) => return (store, Err(deep_sleep)), + }; // Clean up the environment on exit - ctx.on_exit(store, exit_code); - Ok(()) + ctx.on_exit(&mut store, exit_code); + (store, Ok(())) } fn handle_thread_result( @@ -309,7 +312,7 @@ fn call_module( } // Now invoke the module - let ret = call_module_internal::(&ctx, &mut store, start_ptr_offset); + let (store, ret) = call_module_internal::(&ctx, store, start_ptr_offset); // If it went to deep sleep then we need to handle that if let Err(deep) = ret { From 66b89fbc2e38774d2ccae82a0d664030d12330e3 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 15:56:36 +0100 Subject: [PATCH 047/114] Add todo to remind me of adding a proper error type for returning context entrypoints --- lib/wasix/src/syscalls/wasix/context_create.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 5f1b95af14d..fce9ab55969 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -101,6 +101,7 @@ pub fn context_create( result.map_or_else( |e| e, |v| { + // TODO: Proper error type RuntimeError::user( format!( "Context {new_context_id} returned a value ({v:?}). This is not allowed for now" From 2b7f2d484940051487ab70c3203c1be002e7bdaf Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 11:29:12 +0100 Subject: [PATCH 048/114] Fix deadlocks --- lib/wasix/src/os/task/thread/context_switching.rs | 2 ++ lib/wasix/src/syscalls/wasix/context_switch.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index cb77fbd4e42..8c29c5c9424 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -87,6 +87,8 @@ impl ContextSwitchingContext { let store_async = store.into_async(); // Run function with the spawner let result = local_executor.run_until(entrypoint.call_async(&store_async, params)); + // Drop the executor to ensure all spawned tasks are dropped, so we have no references to the StoreAsync left + drop(local_executor); // Remove the spawner again let mut store = store_async.into_store().ok().unwrap(); diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 9c2914e079c..5c25cc71bb4 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -109,6 +109,9 @@ async fn inner_context_switch( } }; + // Drop the write lock before awaiting, as that would cause a deadlock + drop(write_lock); + // Wait until we are unblocked again wait_for_unblock.map(|v| v.map(|_| Errno::Success)).await } From 876dc15723d0600f835edf711ce1112dd56d5098 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 11:29:29 +0100 Subject: [PATCH 049/114] Fix context switching tests --- lib/wasix/tests/context_switching/simple_switching.c | 2 +- lib/wasix/tests/context_switching/switching_in_threads.c | 2 +- .../tests/context_switching/switching_to_a_deleted_context.c | 2 +- lib/wasix/tests/context_switching/switching_with_main.c | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/wasix/tests/context_switching/simple_switching.c b/lib/wasix/tests/context_switching/simple_switching.c index 7f4c1945378..1aefb122b0c 100644 --- a/lib/wasix/tests/context_switching/simple_switching.c +++ b/lib/wasix/tests/context_switching/simple_switching.c @@ -15,7 +15,7 @@ void test1(void) { while (1) { wasix_context_switch(context2); if (stop == 1) { - wasix_context_switch(context_main_context); + wasix_context_switch(wasix_context_main); } counter++; printf("%s", message); diff --git a/lib/wasix/tests/context_switching/switching_in_threads.c b/lib/wasix/tests/context_switching/switching_in_threads.c index c7ca22c7533..6f30531152b 100644 --- a/lib/wasix/tests/context_switching/switching_in_threads.c +++ b/lib/wasix/tests/context_switching/switching_in_threads.c @@ -20,7 +20,7 @@ void test1(void) { while (1) { wasix_context_switch(context2); if (stop == 1) { - wasix_context_switch(context_main_context); + wasix_context_switch(wasix_context_main); } counter++; printf("%s", message); diff --git a/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c b/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c index aca92c196cb..674e69c1b3a 100644 --- a/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c +++ b/lib/wasix/tests/context_switching/switching_to_a_deleted_context.c @@ -11,7 +11,7 @@ int counter = 0; void test1(void) { counter += 1; - wasix_context_switch(context_main_context); + wasix_context_switch(wasix_context_main); } // Required because switching with the real main currently segfaults diff --git a/lib/wasix/tests/context_switching/switching_with_main.c b/lib/wasix/tests/context_switching/switching_with_main.c index 043a4d0a018..96fecbd7252 100644 --- a/lib/wasix/tests/context_switching/switching_with_main.c +++ b/lib/wasix/tests/context_switching/switching_with_main.c @@ -12,9 +12,9 @@ int counter = 0; void test1(void) { while (1) { - wasix_context_switch(context_main_context); + wasix_context_switch(wasix_context_main); if (stop == 1) { - wasix_context_switch(context_main_context); + wasix_context_switch(wasix_context_main); break; } counter++; From b8f2c77e57aa06400f0ea5287aa04266530f89e2 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 13:35:25 +0100 Subject: [PATCH 050/114] Add more tests for context switching --- lib/wasix/tests/context_switching.rs | 72 ++++++++++++++ .../tests/context_switching/cleanup_order.c | 82 +++++++++++++++ .../tests/context_switching/deep_recursion.c | 93 +++++++++++++++++ .../tests/context_switching/error_handling.c | 59 +++++++++++ .../context_switching/file_io_switching.c | 99 +++++++++++++++++++ .../context_switching/heap_allocations.c | 71 +++++++++++++ .../tests/context_switching/main_context_id.c | 55 +++++++++++ .../tests/context_switching/many_contexts.c | 62 ++++++++++++ .../context_switching/multiple_contexts.c | 74 ++++++++++++++ .../tests/context_switching/nested_switches.c | 75 ++++++++++++++ .../tests/context_switching/rapid_switching.c | 53 ++++++++++ .../tests/context_switching/self_switching.c | 55 +++++++++++ .../context_switching/state_preservation.c | 82 +++++++++++++++ 13 files changed, 932 insertions(+) create mode 100644 lib/wasix/tests/context_switching/cleanup_order.c create mode 100644 lib/wasix/tests/context_switching/deep_recursion.c create mode 100644 lib/wasix/tests/context_switching/error_handling.c create mode 100644 lib/wasix/tests/context_switching/file_io_switching.c create mode 100644 lib/wasix/tests/context_switching/heap_allocations.c create mode 100644 lib/wasix/tests/context_switching/main_context_id.c create mode 100644 lib/wasix/tests/context_switching/many_contexts.c create mode 100644 lib/wasix/tests/context_switching/multiple_contexts.c create mode 100644 lib/wasix/tests/context_switching/nested_switches.c create mode 100644 lib/wasix/tests/context_switching/rapid_switching.c create mode 100644 lib/wasix/tests/context_switching/self_switching.c create mode 100644 lib/wasix/tests/context_switching/state_preservation.c diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index 6d045dc7090..e6a8ef46981 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -64,3 +64,75 @@ fn test_switching_to_a_deleted_context() { fn test_switching_threads() { test_with_wasixcc("switching_in_threads").unwrap(); } + +#[cfg(target_os = "linux")] +#[test] +fn test_multiple_contexts() { + test_with_wasixcc("multiple_contexts").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_error_handling() { + test_with_wasixcc("error_handling").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_nested_switches() { + test_with_wasixcc("nested_switches").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_state_preservation() { + test_with_wasixcc("state_preservation").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_main_context_id() { + test_with_wasixcc("main_context_id").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_self_switching() { + test_with_wasixcc("self_switching").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_cleanup_order() { + test_with_wasixcc("cleanup_order").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_deep_recursion() { + test_with_wasixcc("deep_recursion").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_rapid_switching() { + test_with_wasixcc("rapid_switching").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_heap_allocations() { + test_with_wasixcc("heap_allocations").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_many_contexts() { + test_with_wasixcc("many_contexts").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_file_io_switching() { + test_with_wasixcc("file_io_switching").unwrap(); +} diff --git a/lib/wasix/tests/context_switching/cleanup_order.c b/lib/wasix/tests/context_switching/cleanup_order.c new file mode 100644 index 00000000000..60510848e16 --- /dev/null +++ b/lib/wasix/tests/context_switching/cleanup_order.c @@ -0,0 +1,82 @@ +#include +#include +#include +#include + +// Test creating and destroying multiple contexts in various orders + +#define NUM_CONTEXTS 5 + +wasix_context_id_t contexts[NUM_CONTEXTS]; +int execution_flags[NUM_CONTEXTS] = {0}; + +void context_fn_0(void) { + execution_flags[0] = 1; + wasix_context_switch(wasix_context_main); +} + +void context_fn_1(void) { + execution_flags[1] = 1; + wasix_context_switch(wasix_context_main); +} + +void context_fn_2(void) { + execution_flags[2] = 1; + wasix_context_switch(wasix_context_main); +} + +void context_fn_3(void) { + execution_flags[3] = 1; + wasix_context_switch(wasix_context_main); +} + +void context_fn_4(void) { + execution_flags[4] = 1; + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + void (*entrypoints[])(void) = {context_fn_0, context_fn_1, context_fn_2, + context_fn_3, context_fn_4}; + + // Create all contexts + for (int i = 0; i < NUM_CONTEXTS; i++) { + ret = wasix_context_create(&contexts[i], entrypoints[i]); + assert(ret == 0 && "Failed to create context"); + } + + // Execute some contexts + wasix_context_switch(contexts[0]); + assert(execution_flags[0] == 1 && "Context 0 should have executed"); + + wasix_context_switch(contexts[2]); + assert(execution_flags[2] == 1 && "Context 2 should have executed"); + + wasix_context_switch(contexts[4]); + assert(execution_flags[4] == 1 && "Context 4 should have executed"); + + // Destroy contexts in non-creation order + ret = wasix_context_destroy(contexts[2]); + assert(ret == 0 && "Failed to destroy context 2"); + + ret = wasix_context_destroy(contexts[0]); + assert(ret == 0 && "Failed to destroy context 0"); + + ret = wasix_context_destroy(contexts[4]); + assert(ret == 0 && "Failed to destroy context 4"); + + // Execute a context that wasn't executed before, then destroy it + wasix_context_switch(contexts[1]); + assert(execution_flags[1] == 1 && "Context 1 should have executed"); + + ret = wasix_context_destroy(contexts[1]); + assert(ret == 0 && "Failed to destroy context 1"); + + // Destroy the remaining context without ever executing it + ret = wasix_context_destroy(contexts[3]); + assert(ret == 0 && "Failed to destroy unexecuted context 3"); + + fprintf(stderr, "Context cleanup test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/deep_recursion.c b/lib/wasix/tests/context_switching/deep_recursion.c new file mode 100644 index 00000000000..2ab4b765b44 --- /dev/null +++ b/lib/wasix/tests/context_switching/deep_recursion.c @@ -0,0 +1,93 @@ +#include +#include +#include +#include + +// Test that contexts have independent stacks with deep recursion + +#define RECURSION_DEPTH 100 + +wasix_context_id_t ctx1, ctx2; +int ctx1_depth = 0; +int ctx2_depth = 0; +int ctx1_max_depth = 0; +int ctx2_max_depth = 0; + +void ctx1_recursive(int depth); +void ctx2_recursive(int depth); + +void ctx1_recursive(int depth) { + ctx1_depth = depth; + if (depth > ctx1_max_depth) { + ctx1_max_depth = depth; + } + + if (depth < RECURSION_DEPTH) { + // Recurse deeper + ctx1_recursive(depth + 1); + } else { + // Switch to context 2 at max depth + wasix_context_switch(ctx2); + } +} + +void ctx2_recursive(int depth) { + ctx2_depth = depth; + if (depth > ctx2_max_depth) { + ctx2_max_depth = depth; + } + + if (depth < RECURSION_DEPTH) { + // Recurse deeper + ctx2_recursive(depth + 1); + } else { + // Switch back to context 1 at max depth + wasix_context_switch(ctx1); + } +} + +void context1_fn(void) { + // Start recursion in context 1 + ctx1_recursive(0); + + // Verify we completed the full recursion + assert(ctx1_max_depth == RECURSION_DEPTH && + "Context 1 should have reached max depth"); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + // Start recursion in context 2 + ctx2_recursive(0); + + // Verify we completed the full recursion + assert(ctx2_max_depth == RECURSION_DEPTH && + "Context 2 should have reached max depth"); + + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start the recursion chain + wasix_context_switch(ctx1); + + // Verify both contexts reached max depth independently + assert(ctx1_max_depth == RECURSION_DEPTH && "Context 1 max depth incorrect"); + assert(ctx2_max_depth == RECURSION_DEPTH && "Context 2 max depth incorrect"); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Deep recursion test passed (depth=%d)\n", RECURSION_DEPTH); + return 0; +} diff --git a/lib/wasix/tests/context_switching/error_handling.c b/lib/wasix/tests/context_switching/error_handling.c new file mode 100644 index 00000000000..176fbc8e931 --- /dev/null +++ b/lib/wasix/tests/context_switching/error_handling.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include + +// Test error handling: invalid operations should fail with appropriate errors + +wasix_context_id_t ctx1; +int test_phase = 0; + +void context1_fn(void) { + int ret; + + // Test: Cannot destroy the active context (self) + ret = wasix_context_destroy(ctx1); + assert(ret == -1 && "Should fail to destroy active context"); + assert(errno == EINVAL && + "Should set errno to EINVAL for active context destroy"); + + test_phase = 1; + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + // Create a context + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context"); + + // Switch to context to test destroying active context + wasix_context_switch(ctx1); + assert(test_phase == 1 && "Context did not execute"); + + // Test: Cannot destroy main context + ret = wasix_context_destroy(wasix_context_main); + assert(ret == -1 && "Should fail to destroy main context"); + assert(errno == EINVAL && + "Should set errno to EINVAL for main context destroy"); + + // Destroy the context + ret = wasix_context_destroy(ctx1); + assert(ret == 0 && "Failed to destroy context"); + + // Test: Switching to a destroyed context should fail + errno = 0; + ret = wasix_context_switch(ctx1); + assert(ret == -1 && "Should fail to switch to destroyed context"); + assert(errno == EINVAL && "Should set errno to EINVAL for destroyed context"); + + // Test: Destroying an already destroyed context is a no-op + ret = wasix_context_destroy(ctx1); + assert(ret == 0 && + "Destroying already destroyed context should succeed (no-op)"); + + fprintf(stderr, "All error handling tests passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/file_io_switching.c b/lib/wasix/tests/context_switching/file_io_switching.c new file mode 100644 index 00000000000..ea1f2e288f4 --- /dev/null +++ b/lib/wasix/tests/context_switching/file_io_switching.c @@ -0,0 +1,99 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// Test file I/O operations between context switches + +wasix_context_id_t ctx1, ctx2; +const char *test_file = "/tmp/context_switch_test.txt"; + +#define TEST_DATA_1 "Hello from context 1\n" +#define TEST_DATA_2 "Hello from context 2\n" + +void context1_fn(void) { + char buffer[128]; + ssize_t n; + int fd; + + // Write to file + fd = open(test_file, O_WRONLY | O_CREAT | O_TRUNC, 0644); + assert(fd >= 0 && "Failed to open file in context 1"); + + n = write(fd, TEST_DATA_1, strlen(TEST_DATA_1)); + assert(n == strlen(TEST_DATA_1) && "Failed to write to file in context 1"); + close(fd); + + // Switch to context 2 + wasix_context_switch(ctx2); + + // After resuming, read what context 2 appended + fd = open(test_file, O_RDONLY); + assert(fd >= 0 && "Failed to open file for reading in context 1"); + + memset(buffer, 0, sizeof(buffer)); + n = read(fd, buffer, sizeof(buffer) - 1); + assert(n > 0 && "Failed to read from file in context 1"); + close(fd); + + // Verify both messages are there + assert(strstr(buffer, TEST_DATA_1) != NULL && "Context 1 data missing"); + assert(strstr(buffer, TEST_DATA_2) != NULL && "Context 2 data missing"); + + // Clean up + unlink(test_file); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + char buffer[128]; + ssize_t n; + int fd; + + // Read what context 1 wrote + fd = open(test_file, O_RDONLY); + assert(fd >= 0 && "Failed to open file in context 2"); + + memset(buffer, 0, sizeof(buffer)); + n = read(fd, buffer, sizeof(buffer) - 1); + assert(n > 0 && "Failed to read from file in context 2"); + assert(strcmp(buffer, TEST_DATA_1) == 0 && + "Read incorrect data in context 2"); + close(fd); + + // Append to file + fd = open(test_file, O_WRONLY | O_APPEND); + assert(fd >= 0 && "Failed to open file for append in context 2"); + + n = write(fd, TEST_DATA_2, strlen(TEST_DATA_2)); + assert(n == strlen(TEST_DATA_2) && "Failed to append to file in context 2"); + close(fd); + + // Switch back to context 1 + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "File I/O switching test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/heap_allocations.c b/lib/wasix/tests/context_switching/heap_allocations.c new file mode 100644 index 00000000000..9853f30365a --- /dev/null +++ b/lib/wasix/tests/context_switching/heap_allocations.c @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include + +// Test that contexts maintain independent heap allocations + +wasix_context_id_t ctx1, ctx2; + +#define BUFFER_SIZE 1024 + +void context1_fn(void) { + // Allocate and fill memory + char *buffer1 = (char *)malloc(BUFFER_SIZE); + assert(buffer1 != NULL && "Failed to allocate buffer in context 1"); + + memset(buffer1, 'A', BUFFER_SIZE); + buffer1[BUFFER_SIZE - 1] = '\0'; + + // Switch to context 2 + wasix_context_switch(ctx2); + + // Verify our buffer is still intact after context 2 executed + assert(buffer1[0] == 'A' && "Buffer corrupted after context switch"); + assert(buffer1[BUFFER_SIZE - 2] == 'A' && + "Buffer corrupted after context switch"); + + free(buffer1); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + // Allocate and fill memory with different pattern + char *buffer2 = (char *)malloc(BUFFER_SIZE * 2); + assert(buffer2 != NULL && "Failed to allocate buffer in context 2"); + + memset(buffer2, 'B', BUFFER_SIZE * 2); + buffer2[BUFFER_SIZE * 2 - 1] = '\0'; + + // Switch back to context 1 + wasix_context_switch(ctx1); + + // Verify our buffer is still intact + assert(buffer2[0] == 'B' && "Buffer corrupted after context switch"); + assert(buffer2[BUFFER_SIZE * 2 - 2] == 'B' && + "Buffer corrupted after context switch"); + + free(buffer2); + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Heap allocations test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/main_context_id.c b/lib/wasix/tests/context_switching/main_context_id.c new file mode 100644 index 00000000000..90fa915884a --- /dev/null +++ b/lib/wasix/tests/context_switching/main_context_id.c @@ -0,0 +1,55 @@ +#include +#include +#include +#include + +// Test wasix_context_main identifier behavior + +wasix_context_id_t ctx1; +wasix_context_id_t main_ctx_from_ctx1; +wasix_context_id_t main_ctx_from_main; + +void context1_fn(void) { + // Get main context identifier from within a non-main context + main_ctx_from_ctx1 = wasix_context_main; + + // Switch back to main + wasix_context_switch(wasix_context_main); + + // This should not be reached + fprintf(stderr, + "ERROR: Execution continued in context1 after switch to main\n"); + exit(1); +} + +int main() { + int ret; + + // Get main context identifier from main + main_ctx_from_main = wasix_context_main; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + // Switch to context 1 + wasix_context_switch(ctx1); + + // Verify that wasix_context_main is consistent + assert(main_ctx_from_main == main_ctx_from_ctx1 && + "wasix_context_main should be the same from all contexts"); + + // Test that we can switch to main context using the identifier + ret = wasix_context_create( + &ctx1, context1_fn); // Recreate since previous one terminated + assert(ret == 0 && "Failed to recreate context 1"); + + wasix_context_switch(ctx1); + + // We should be back here after context1 switches to main + + // Cleanup + wasix_context_destroy(ctx1); + + fprintf(stderr, "Main context identifier test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/many_contexts.c b/lib/wasix/tests/context_switching/many_contexts.c new file mode 100644 index 00000000000..f3f94e80ea2 --- /dev/null +++ b/lib/wasix/tests/context_switching/many_contexts.c @@ -0,0 +1,62 @@ +#include +#include +#include +#include + +// Test creating many contexts to stress resource management + +#define NUM_CONTEXTS 20 + +wasix_context_id_t contexts[NUM_CONTEXTS]; +int executed[NUM_CONTEXTS] = {0}; + +void generic_context_fn(void) { + // Find which context this is + for (int i = 0; i < NUM_CONTEXTS; i++) { + if (!executed[i]) { + executed[i] = 1; + break; + } + } + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + // Create many contexts + for (int i = 0; i < NUM_CONTEXTS; i++) { + ret = wasix_context_create(&contexts[i], generic_context_fn); + assert(ret == 0 && "Failed to create context"); + } + + // Execute them all + for (int i = 0; i < NUM_CONTEXTS; i++) { + wasix_context_switch(contexts[i]); + assert(executed[i] == 1 && "Context should have executed"); + } + + // Destroy them all + for (int i = 0; i < NUM_CONTEXTS; i++) { + ret = wasix_context_destroy(contexts[i]); + assert(ret == 0 && "Failed to destroy context"); + } + + // Create another batch to ensure resources were properly freed + for (int i = 0; i < NUM_CONTEXTS; i++) { + ret = wasix_context_create(&contexts[i], generic_context_fn); + assert(ret == 0 && "Failed to create context in second batch"); + } + + // Destroy second batch + for (int i = 0; i < NUM_CONTEXTS; i++) { + ret = wasix_context_destroy(contexts[i]); + assert(ret == 0 && "Failed to destroy context in second batch"); + } + + fprintf( + stderr, + "Many contexts test passed (%d contexts created and destroyed twice)\n", + NUM_CONTEXTS); + return 0; +} diff --git a/lib/wasix/tests/context_switching/multiple_contexts.c b/lib/wasix/tests/context_switching/multiple_contexts.c new file mode 100644 index 00000000000..c2d74230ca6 --- /dev/null +++ b/lib/wasix/tests/context_switching/multiple_contexts.c @@ -0,0 +1,74 @@ +#include +#include +#include +#include +#include + +// Test creating multiple contexts and switching between them in various orders +// Expected execution order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 + +wasix_context_id_t ctx1, ctx2, ctx3; +int execution_order[10]; +int order_idx = 0; + +void context1_fn(void) { + execution_order[order_idx++] = 1; + wasix_context_switch(ctx2); + + execution_order[order_idx++] = 4; + wasix_context_switch(ctx3); + + execution_order[order_idx++] = 6; + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + execution_order[order_idx++] = 2; + wasix_context_switch(ctx3); + + execution_order[order_idx++] = 5; + wasix_context_switch(ctx1); +} + +void context3_fn(void) { + execution_order[order_idx++] = 3; + wasix_context_switch(ctx1); + + execution_order[order_idx++] = 7; + wasix_context_switch(ctx2); +} + +int main() { + int ret; + + // Create three contexts + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + ret = wasix_context_create(&ctx3, context3_fn); + assert(ret == 0 && "Failed to create context 3"); + + // Start the chain by switching to context 1 + // Expected execution order: 1 -> 2 -> 3 -> 4 -> 7 -> 5 -> 6 + // Control flow: main -> ctx1(1) -> ctx2(2) -> ctx3(3) -> ctx1(4) -> ctx3(7) + // -> ctx2(5) -> ctx1(6) -> main + wasix_context_switch(ctx1); + + // Verify execution order + int expected[] = {1, 2, 3, 4, 7, 5, 6}; + assert(order_idx == 7 && "Incorrect number of context switches"); + for (int i = 0; i < 7; i++) { + assert(execution_order[i] == expected[i] && "Incorrect execution order"); + } + + ret = wasix_context_destroy(ctx2); + assert(ret == 0 && "Failed to destroy context 2"); + + ret = wasix_context_destroy(ctx3); + assert(ret == 0 && "Failed to destroy context 3"); + + return 0; +} diff --git a/lib/wasix/tests/context_switching/nested_switches.c b/lib/wasix/tests/context_switching/nested_switches.c new file mode 100644 index 00000000000..4c88964ffc0 --- /dev/null +++ b/lib/wasix/tests/context_switching/nested_switches.c @@ -0,0 +1,75 @@ +#include +#include +#include +#include + +// Test nested context switches with multiple visits to each context + +wasix_context_id_t ctxA, ctxB, ctxC; +int visit_count_A = 0; +int visit_count_B = 0; +int visit_count_C = 0; + +void contextA_fn(void) { + visit_count_A++; + + if (visit_count_A == 1) { + // First visit: go to B + wasix_context_switch(ctxB); + // When we resume here, go to main + wasix_context_switch(wasix_context_main); + } else { + // Second visit: shouldn't happen in this flow + exit(1); + } +} + +void contextB_fn(void) { + visit_count_B++; + + if (visit_count_B == 1) { + // First visit: go to C + wasix_context_switch(ctxC); + // When we resume here, go back to A + wasix_context_switch(ctxA); + } else { + // Second visit: shouldn't happen in this flow + exit(1); + } +} + +void contextC_fn(void) { + visit_count_C++; + + // Switch to B to resume it + wasix_context_switch(ctxB); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctxA, contextA_fn); + assert(ret == 0 && "Failed to create context A"); + + ret = wasix_context_create(&ctxB, contextB_fn); + assert(ret == 0 && "Failed to create context B"); + + ret = wasix_context_create(&ctxC, contextC_fn); + assert(ret == 0 && "Failed to create context C"); + + // Start the nested chain + wasix_context_switch(ctxA); + + // Verify contexts were visited + // Flow: main -> A(1) -> B(1) -> C(1) -> B -> A -> main + assert(visit_count_A == 1 && "Context A visited wrong number of times"); + assert(visit_count_B == 1 && "Context B visited wrong number of times"); + assert(visit_count_C == 1 && "Context C visited wrong number of times"); + + // Cleanup + wasix_context_destroy(ctxA); + wasix_context_destroy(ctxB); + wasix_context_destroy(ctxC); + + return 0; +} diff --git a/lib/wasix/tests/context_switching/rapid_switching.c b/lib/wasix/tests/context_switching/rapid_switching.c new file mode 100644 index 00000000000..f2106218248 --- /dev/null +++ b/lib/wasix/tests/context_switching/rapid_switching.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include + +// Test rapid context switching with many iterations + +#define SWITCH_COUNT 1000 + +wasix_context_id_t ctx1, ctx2; +int ping_count = 0; +int pong_count = 0; + +void ping_context(void) { + while (ping_count < SWITCH_COUNT) { + ping_count++; + wasix_context_switch(ctx2); + } + wasix_context_switch(wasix_context_main); +} + +void pong_context(void) { + while (pong_count < SWITCH_COUNT) { + pong_count++; + wasix_context_switch(ctx1); + } + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, ping_context); + assert(ret == 0 && "Failed to create ping context"); + + ret = wasix_context_create(&ctx2, pong_context); + assert(ret == 0 && "Failed to create pong context"); + + // Start the ping-pong + wasix_context_switch(ctx1); + + // Verify all switches occurred + assert(ping_count == SWITCH_COUNT && "Ping count mismatch"); + assert(pong_count == SWITCH_COUNT && "Pong count mismatch"); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Rapid switching test passed (%d switches)\n", + SWITCH_COUNT * 2); + return 0; +} diff --git a/lib/wasix/tests/context_switching/self_switching.c b/lib/wasix/tests/context_switching/self_switching.c new file mode 100644 index 00000000000..b0a92c4ddf7 --- /dev/null +++ b/lib/wasix/tests/context_switching/self_switching.c @@ -0,0 +1,55 @@ +#include +#include +#include +#include + +// Test switching to the currently active context (should be a no-op) + +wasix_context_id_t ctx1; +int switch_count = 0; + +void context1_fn(void) { + int local_var = 42; + + // Switch to self (should be a no-op) + int ret = wasix_context_switch(ctx1); + assert(ret == 0 && "Self-switch should succeed"); + + // Verify we're still in the same context + assert(local_var == 42 && + "Local variable should be unchanged after self-switch"); + switch_count++; + + // Try again + ret = wasix_context_switch(ctx1); + assert(ret == 0 && "Second self-switch should succeed"); + switch_count++; + + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + int main_local = 123; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + // Test self-switch in main context + ret = wasix_context_switch(wasix_context_main); + assert(ret == 0 && "Main context self-switch should succeed"); + assert(main_local == 123 && "Main local variable should be unchanged"); + + // Switch to context 1 + wasix_context_switch(ctx1); + + // Verify context1 executed + assert(switch_count == 2 && + "Context 1 should have performed 2 self-switches"); + + // Cleanup + wasix_context_destroy(ctx1); + + fprintf(stderr, "Self-switching test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/state_preservation.c b/lib/wasix/tests/context_switching/state_preservation.c new file mode 100644 index 00000000000..5bc853971a6 --- /dev/null +++ b/lib/wasix/tests/context_switching/state_preservation.c @@ -0,0 +1,82 @@ +#include +#include +#include +#include + +// Test that context state (local variables, stack) is preserved across switches + +wasix_context_id_t ctx1, ctx2; + +void context1_fn(void) { + int local1 = 100; + int local2 = 200; + int local3 = 300; + + // Switch to context2 + wasix_context_switch(ctx2); + + // After returning, verify our local variables are preserved + assert(local1 == 100 && "Local variable 1 should be preserved"); + assert(local2 == 200 && "Local variable 2 should be preserved"); + assert(local3 == 300 && "Local variable 3 should be preserved"); + + // Modify the variables + local1 = 111; + local2 = 222; + local3 = 333; + + // Switch again + wasix_context_switch(ctx2); + + // Verify modified values are preserved + assert(local1 == 111 && "Modified local variable 1 should be preserved"); + assert(local2 == 222 && "Modified local variable 2 should be preserved"); + assert(local3 == 333 && "Modified local variable 3 should be preserved"); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + int local_a = 10; + int local_b = 20; + + // Switch back to context1 + wasix_context_switch(ctx1); + + // After returning, verify our local variables are preserved + assert(local_a == 10 && "Local variable a should be preserved"); + assert(local_b == 20 && "Local variable b should be preserved"); + + // Modify + local_a = 99; + local_b = 88; + + // Switch again + wasix_context_switch(ctx1); + + // Verify modified values + assert(local_a == 99 && "Modified local variable a should be preserved"); + assert(local_b == 88 && "Modified local variable b should be preserved"); + + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Context state preservation test passed\n"); + return 0; +} From d096b25593be5be93206f6660e771c17bbc55590 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 13:45:42 +0100 Subject: [PATCH 051/114] Add even more context switching tests --- lib/wasix/tests/context_switching.rs | 36 ++++++++ .../contexts_with_env_vars.c | 72 +++++++++++++++ .../context_switching/contexts_with_getcwd.c | 89 +++++++++++++++++++ .../context_switching/contexts_with_mutexes.c | 76 ++++++++++++++++ .../context_switching/contexts_with_pipes.c | 77 ++++++++++++++++ .../context_switching/contexts_with_signals.c | 69 ++++++++++++++ .../context_switching/contexts_with_timers.c | 76 ++++++++++++++++ 7 files changed, 495 insertions(+) create mode 100644 lib/wasix/tests/context_switching/contexts_with_env_vars.c create mode 100644 lib/wasix/tests/context_switching/contexts_with_getcwd.c create mode 100644 lib/wasix/tests/context_switching/contexts_with_mutexes.c create mode 100644 lib/wasix/tests/context_switching/contexts_with_pipes.c create mode 100644 lib/wasix/tests/context_switching/contexts_with_signals.c create mode 100644 lib/wasix/tests/context_switching/contexts_with_timers.c diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index e6a8ef46981..79891ec02a0 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -136,3 +136,39 @@ fn test_many_contexts() { fn test_file_io_switching() { test_with_wasixcc("file_io_switching").unwrap(); } + +#[cfg(target_os = "linux")] +#[test] +fn test_contexts_with_mutexes() { + test_with_wasixcc("contexts_with_mutexes").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_contexts_with_env_vars() { + test_with_wasixcc("contexts_with_env_vars").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_contexts_with_getcwd() { + test_with_wasixcc("contexts_with_getcwd").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_contexts_with_signals() { + test_with_wasixcc("contexts_with_signals").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_contexts_with_timers() { + test_with_wasixcc("contexts_with_timers").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_contexts_with_pipes() { + test_with_wasixcc("contexts_with_pipes").unwrap(); +} diff --git a/lib/wasix/tests/context_switching/contexts_with_env_vars.c b/lib/wasix/tests/context_switching/contexts_with_env_vars.c new file mode 100644 index 00000000000..6176f42829d --- /dev/null +++ b/lib/wasix/tests/context_switching/contexts_with_env_vars.c @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include + +// Test context switching with environment variable operations + +wasix_context_id_t ctx1, ctx2; + +void context1_fn(void) { + // Set an environment variable + int ret = setenv("CTX_TEST_VAR", "from_context_1", 1); + assert(ret == 0 && "Failed to set env var in context 1"); + + const char *val = getenv("CTX_TEST_VAR"); + assert(val != NULL && "Env var should exist"); + assert(strcmp(val, "from_context_1") == 0 && "Env var should match"); + + // Switch to context 2 + wasix_context_switch(ctx2); + + // After resuming, check if context 2 modified it + val = getenv("CTX_TEST_VAR"); + assert(val != NULL && "Env var should still exist"); + assert(strcmp(val, "from_context_2") == 0 && + "Env var should be modified by context 2"); + + // Clean up + unsetenv("CTX_TEST_VAR"); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + // Check that we can see the env var set by context 1 + const char *val = getenv("CTX_TEST_VAR"); + assert(val != NULL && "Env var should be visible in context 2"); + assert(strcmp(val, "from_context_1") == 0 && + "Env var should have context 1's value"); + + // Modify it + int ret = setenv("CTX_TEST_VAR", "from_context_2", 1); + assert(ret == 0 && "Failed to modify env var in context 2"); + + // Switch back to context 1 + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Verify env var was cleaned up + const char *val = getenv("CTX_TEST_VAR"); + assert(val == NULL && "Env var should be cleaned up"); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Environment variable switching test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/contexts_with_getcwd.c b/lib/wasix/tests/context_switching/contexts_with_getcwd.c new file mode 100644 index 00000000000..1288c0abcf4 --- /dev/null +++ b/lib/wasix/tests/context_switching/contexts_with_getcwd.c @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#include +#include + +// Test context switching with directory operations + +wasix_context_id_t ctx1, ctx2; +char original_dir[1024]; + +void context1_fn(void) { + char buf[1024]; + + // Get current directory + char *ret_cwd = getcwd(buf, sizeof(buf)); + assert(ret_cwd != NULL && "Failed to get cwd in context 1"); + + // Change to /tmp + int ret = chdir("/tmp"); + assert(ret == 0 && "Failed to chdir in context 1"); + + // Verify the change + ret_cwd = getcwd(buf, sizeof(buf)); + assert(ret_cwd != NULL && "Failed to get cwd after chdir"); + assert(strcmp(buf, "/tmp") == 0 && "Should be in /tmp"); + + // Switch to context 2 + wasix_context_switch(ctx2); + + // After resuming, check if we're still in /tmp + ret_cwd = getcwd(buf, sizeof(buf)); + assert(ret_cwd != NULL && "Failed to get cwd after resume"); + // Current directory is shared across contexts (same process) + assert(strcmp(buf, "/") == 0 && "Context 2 should have changed to /"); + + // Restore original directory + ret = chdir(original_dir); + assert(ret == 0 && "Failed to restore original directory"); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + char buf[1024]; + + // Check current directory (should be /tmp from context 1) + char *ret_cwd = getcwd(buf, sizeof(buf)); + assert(ret_cwd != NULL && "Failed to get cwd in context 2"); + assert(strcmp(buf, "/tmp") == 0 && "Should be in /tmp from context 1"); + + // Change to root + int ret = chdir("/"); + assert(ret == 0 && "Failed to chdir to / in context 2"); + + // Switch back to context 1 + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + // Save original directory + char *ret_cwd = getcwd(original_dir, sizeof(original_dir)); + assert(ret_cwd != NULL && "Failed to get original cwd"); + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Verify we're back in original directory + char buf[1024]; + ret_cwd = getcwd(buf, sizeof(buf)); + assert(ret_cwd != NULL && "Failed to get final cwd"); + assert(strcmp(buf, original_dir) == 0 && "Should be back in original dir"); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Directory operations switching test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/contexts_with_mutexes.c b/lib/wasix/tests/context_switching/contexts_with_mutexes.c new file mode 100644 index 00000000000..49dc5997c5f --- /dev/null +++ b/lib/wasix/tests/context_switching/contexts_with_mutexes.c @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include + +// Test context switching with pthread mutexes + +wasix_context_id_t ctx1, ctx2; +pthread_mutex_t shared_mutex; +int shared_counter = 0; + +void context1_fn(void) { + // Lock mutex in context 1 + int ret = pthread_mutex_lock(&shared_mutex); + assert(ret == 0 && "Failed to lock mutex in context 1"); + + shared_counter++; + assert(shared_counter == 1 && "Counter should be 1"); + + // Switch to context 2 while holding the mutex + // Context 2 should be able to unlock it (same thread, shared mutex state) + wasix_context_switch(ctx2); + + // After resuming, verify counter was updated by context 2 + assert(shared_counter == 2 && "Counter should be 2 after context 2"); + + // Lock again to verify mutex is still usable + ret = pthread_mutex_lock(&shared_mutex); + assert(ret == 0 && "Failed to lock mutex again in context 1"); + + pthread_mutex_unlock(&shared_mutex); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + // Unlock the mutex that was locked by context 1 + // This tests that mutex state is shared across contexts (same thread) + int ret = pthread_mutex_unlock(&shared_mutex); + assert(ret == 0 && "Failed to unlock mutex in context 2"); + + shared_counter++; + assert(shared_counter == 2 && "Counter should be 2"); + + // Switch back to context 1 without the mutex locked + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + // Initialize mutex + pthread_mutex_init(&shared_mutex, NULL); + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Verify final state + assert(shared_counter == 2 && "Final counter should be 2"); + + // Cleanup + pthread_mutex_destroy(&shared_mutex); + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Mutex switching test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/contexts_with_pipes.c b/lib/wasix/tests/context_switching/contexts_with_pipes.c new file mode 100644 index 00000000000..c69b6269593 --- /dev/null +++ b/lib/wasix/tests/context_switching/contexts_with_pipes.c @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include +#include +#include + +// Test context switching with pipe I/O + +wasix_context_id_t ctx1, ctx2; +int pipe_fds[2]; + +#define MSG1 "Message from context 1" +#define MSG2 "Message from context 2" + +void context1_fn(void) { + char buffer[128]; + + // Write to pipe + ssize_t n = write(pipe_fds[1], MSG1, strlen(MSG1)); + assert(n == strlen(MSG1) && "Failed to write to pipe"); + + // Switch to context 2 + wasix_context_switch(ctx2); + + // After resuming, read what context 2 wrote + memset(buffer, 0, sizeof(buffer)); + n = read(pipe_fds[0], buffer, sizeof(buffer) - 1); + assert(n == strlen(MSG2) && "Failed to read from pipe"); + assert(strcmp(buffer, MSG2) == 0 && "Read incorrect data"); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + char buffer[128]; + + // Read what context 1 wrote + memset(buffer, 0, sizeof(buffer)); + ssize_t n = read(pipe_fds[0], buffer, sizeof(buffer) - 1); + assert(n == strlen(MSG1) && "Failed to read from pipe"); + assert(strcmp(buffer, MSG1) == 0 && "Read incorrect data"); + + // Write response + n = write(pipe_fds[1], MSG2, strlen(MSG2)); + assert(n == strlen(MSG2) && "Failed to write to pipe"); + + // Switch back to context 1 + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + // Create pipe + ret = pipe(pipe_fds); + assert(ret == 0 && "Failed to create pipe"); + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + close(pipe_fds[0]); + close(pipe_fds[1]); + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Pipe I/O switching test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/contexts_with_signals.c b/lib/wasix/tests/context_switching/contexts_with_signals.c new file mode 100644 index 00000000000..b853ae1c73b --- /dev/null +++ b/lib/wasix/tests/context_switching/contexts_with_signals.c @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include +#include + +// Test context switching with signal handlers + +wasix_context_id_t ctx1, ctx2; +volatile int signal_received = 0; + +void signal_handler(int sig) { + signal_received++; + fprintf(stderr, "Signal %d received (count=%d)\n", sig, signal_received); +} + +void context1_fn(void) { + // Install signal handler + signal(SIGUSR1, signal_handler); + + // Raise a signal + raise(SIGUSR1); + assert(signal_received == 1 && "Signal should have been received"); + + // Switch to context 2 + wasix_context_switch(ctx2); + + // After resuming, check if context 2's signal was received + assert(signal_received == 2 && + "Signal from context 2 should have been received"); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + // The signal handler should still be installed + // (signal handlers are per-process, not per-context) + + // Raise another signal + raise(SIGUSR1); + assert(signal_received == 2 && "Second signal should have been received"); + + // Switch back to context 1 + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Verify both signals were received + assert(signal_received == 2 && "Total of 2 signals should be received"); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Signal handling switching test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/contexts_with_timers.c b/lib/wasix/tests/context_switching/contexts_with_timers.c new file mode 100644 index 00000000000..a6f55cb48bf --- /dev/null +++ b/lib/wasix/tests/context_switching/contexts_with_timers.c @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include + +// Test context switching with time operations + +wasix_context_id_t ctx1, ctx2; + +void context1_fn(void) { + struct timespec start, end; + + // Get time before sleep + clock_gettime(CLOCK_MONOTONIC, &start); + + // Sleep for a short time + usleep(10000); // 10ms + + // Get time after sleep + clock_gettime(CLOCK_MONOTONIC, &end); + + // Calculate elapsed time + long elapsed_ns = + (end.tv_sec - start.tv_sec) * 1000000000L + (end.tv_nsec - start.tv_nsec); + assert(elapsed_ns >= 10000000 && "Sleep should take at least 10ms"); + + // Switch to context 2 + wasix_context_switch(ctx2); + + // After resuming, do another timing operation + clock_gettime(CLOCK_MONOTONIC, &start); + usleep(5000); // 5ms + clock_gettime(CLOCK_MONOTONIC, &end); + + elapsed_ns = + (end.tv_sec - start.tv_sec) * 1000000000L + (end.tv_nsec - start.tv_nsec); + assert(elapsed_ns >= 5000000 && "Second sleep should take at least 5ms"); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + struct timespec ts; + + // Get current time + clock_gettime(CLOCK_REALTIME, &ts); + assert(ts.tv_sec > 0 && "Should have valid timestamp"); + + // Do a small sleep + usleep(3000); // 3ms + + // Switch back to context 1 + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Timer operations switching test passed\n"); + return 0; +} From 66045b336f27c8351eab733e039a4143cc322cdf Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 13:57:58 +0100 Subject: [PATCH 052/114] Add even more tests including one that triggers an error --- lib/wasix/tests/context_switching.rs | 42 ++++++ .../context_switching/active_context_id.c | 63 +++++++++ .../complex_nested_operations.c | 77 +++++++++++ .../context_switching/contexts_with_setjmp.c | 66 +++++++++ .../deep_call_stack_switching.c | 93 +++++++++++++ .../context_switching/malloc_during_switch.c | 108 +++++++++++++++ .../nested_host_call_switch.c | 125 ++++++++++++++++++ .../pending_file_operations.c | 108 +++++++++++++++ .../context_switching/recursive_host_calls.c | 107 +++++++++++++++ 9 files changed, 789 insertions(+) create mode 100644 lib/wasix/tests/context_switching/active_context_id.c create mode 100644 lib/wasix/tests/context_switching/complex_nested_operations.c create mode 100644 lib/wasix/tests/context_switching/contexts_with_setjmp.c create mode 100644 lib/wasix/tests/context_switching/deep_call_stack_switching.c create mode 100644 lib/wasix/tests/context_switching/malloc_during_switch.c create mode 100644 lib/wasix/tests/context_switching/nested_host_call_switch.c create mode 100644 lib/wasix/tests/context_switching/pending_file_operations.c create mode 100644 lib/wasix/tests/context_switching/recursive_host_calls.c diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index 79891ec02a0..d3b9dc364ac 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -172,3 +172,45 @@ fn test_contexts_with_timers() { fn test_contexts_with_pipes() { test_with_wasixcc("contexts_with_pipes").unwrap(); } + +#[cfg(target_os = "linux")] +#[test] +fn test_complex_nested_operations() { + test_with_wasixcc("complex_nested_operations").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_pending_file_operations() { + test_with_wasixcc("pending_file_operations").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_deep_call_stack_switching() { + test_with_wasixcc("deep_call_stack_switching").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_recursive_host_calls() { + test_with_wasixcc("recursive_host_calls").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_malloc_during_switch() { + test_with_wasixcc("malloc_during_switch").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_nested_host_call_switch() { + test_with_wasixcc("nested_host_call_switch").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_active_context_id() { + test_with_wasixcc("active_context_id").unwrap(); +} diff --git a/lib/wasix/tests/context_switching/active_context_id.c b/lib/wasix/tests/context_switching/active_context_id.c new file mode 100644 index 00000000000..103ea23018e --- /dev/null +++ b/lib/wasix/tests/context_switching/active_context_id.c @@ -0,0 +1,63 @@ +// Test that active_context_id is updated correctly during context switches +// This is a simple test to verify the implementation bug where +// current_context_id is not updated when switching to a target context +#include +#include +#include + +wasix_context_id_t ctx1, ctx2; +int phase = 0; + +void context1_fn(void) { + phase = 1; + + // When ctx1 is running, wasix_context_main should return ctx1's ID + wasix_context_id_t active_id = wasix_context_main; + fprintf(stderr, "Phase 1: active context ID in ctx1 = %llu (expected %llu)\n", + (unsigned long long)active_id, (unsigned long long)ctx1); + + // This assertion should pass - the active context should be ctx1 + assert(active_id == ctx1 && + "Active context should be ctx1 when running in ctx1"); + + wasix_context_switch(ctx2); + + phase = 3; + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + phase = 2; + + // When ctx2 is running, wasix_context_main should return ctx2's ID + wasix_context_id_t active_id = wasix_context_main; + fprintf(stderr, "Phase 2: active context ID in ctx2 = %llu (expected %llu)\n", + (unsigned long long)active_id, (unsigned long long)ctx2); + + // This assertion exposes the bug - active context is still ctx1 instead of + // ctx2 + assert(active_id == ctx2 && + "Active context should be ctx2 when running in ctx2"); + + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Active context ID test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/complex_nested_operations.c b/lib/wasix/tests/context_switching/complex_nested_operations.c new file mode 100644 index 00000000000..662ce897a2c --- /dev/null +++ b/lib/wasix/tests/context_switching/complex_nested_operations.c @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include + +// Test context switching with complex nested operations that might leave +// internal state borrowed + +#define NUM_CONTEXTS 5 + +wasix_context_id_t contexts[NUM_CONTEXTS]; +int execution_count = 0; + +// Recursive function that does complex operations and switches contexts +void recursive_switch(int depth, int max_depth, wasix_context_id_t next_ctx) { + char buffer[1024]; + + if (depth >= max_depth) { + execution_count++; + // Switch to next context at maximum depth + wasix_context_switch(next_ctx); + return; + } + + // Do some complex string operations + snprintf(buffer, sizeof(buffer), "Depth %d recursion", depth); + + // Allocate and free memory + void *ptr = malloc(1024); + memset(ptr, depth, 1024); + free(ptr); + + // Recurse + recursive_switch(depth + 1, max_depth, next_ctx); + + // After recursion, do more operations + snprintf(buffer, sizeof(buffer), "Returning from depth %d", depth); +} + +void context0_fn(void) { recursive_switch(0, 10, contexts[1]); } + +void context1_fn(void) { recursive_switch(0, 10, contexts[2]); } + +void context2_fn(void) { recursive_switch(0, 10, contexts[3]); } + +void context3_fn(void) { recursive_switch(0, 10, contexts[4]); } + +void context4_fn(void) { recursive_switch(0, 10, wasix_context_main); } + +int main() { + int ret; + + // Create contexts with entrypoints + void (*entrypoints[])(void) = {context0_fn, context1_fn, context2_fn, + context3_fn, context4_fn}; + + for (int i = 0; i < NUM_CONTEXTS; i++) { + ret = wasix_context_create(&contexts[i], entrypoints[i]); + assert(ret == 0 && "Failed to create context"); + } + + // Start the chain + wasix_context_switch(contexts[0]); + + // Verify all contexts executed + assert(execution_count == NUM_CONTEXTS && + "All contexts should have executed"); + + // Cleanup + for (int i = 0; i < NUM_CONTEXTS; i++) { + wasix_context_destroy(contexts[i]); + } + + fprintf(stderr, "Complex nested operations test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/contexts_with_setjmp.c b/lib/wasix/tests/context_switching/contexts_with_setjmp.c new file mode 100644 index 00000000000..6989fa30b4e --- /dev/null +++ b/lib/wasix/tests/context_switching/contexts_with_setjmp.c @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include + +// Test context switching with setjmp/longjmp (which manipulates stack state) + +wasix_context_id_t ctx1, ctx2; +jmp_buf jump_buffer; +int phase = 0; + +void context1_fn(void) { + int val; + + // Set up a jump point + val = setjmp(jump_buffer); + + if (val == 0) { + // First time through + phase = 1; + wasix_context_switch(ctx2); + + // After resuming from ctx2 + phase = 3; + // Try to longjmp after context switch + longjmp(jump_buffer, 1); + } else { + // Jumped back here + phase = 4; + wasix_context_switch(wasix_context_main); + } +} + +void context2_fn(void) { + phase = 2; + + // Do some operations + char *buf = malloc(1024); + free(buf); + + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Verify execution order + assert(phase == 4 && "Should have gone through all phases"); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Setjmp/longjmp switching test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/deep_call_stack_switching.c b/lib/wasix/tests/context_switching/deep_call_stack_switching.c new file mode 100644 index 00000000000..02e2163f4d1 --- /dev/null +++ b/lib/wasix/tests/context_switching/deep_call_stack_switching.c @@ -0,0 +1,93 @@ +#include +#include +#include +#include + +// Test rapid context switching with active function calls on the stack + +wasix_context_id_t ctx1, ctx2, ctx3; +int counter = 0; + +// Deep call stack with context switches in the middle +void deeply_nested_function_a(int depth); +void deeply_nested_function_b(int depth); +void deeply_nested_function_c(int depth); + +void deeply_nested_function_a(int depth) { + if (depth == 0) { + counter++; + wasix_context_switch(ctx2); + return; + } + + deeply_nested_function_b(depth - 1); +} + +void deeply_nested_function_b(int depth) { + if (depth == 0) { + counter++; + wasix_context_switch(ctx3); + return; + } + + deeply_nested_function_c(depth - 1); +} + +void deeply_nested_function_c(int depth) { + if (depth == 0) { + counter++; + wasix_context_switch(ctx1); + return; + } + + deeply_nested_function_a(depth - 1); +} + +void context1_fn(void) { + deeply_nested_function_a(20); + + // After resuming from the switch and the recursion unwinding + // We should always have counter >= 3 because all three contexts incremented + // it + assert(counter >= 3); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + deeply_nested_function_b(15); + // After resuming and recursion unwinding, switch back to continue the chain + // This shouldn't actually be reached in the current flow + wasix_context_switch(wasix_context_main); +} + +void context3_fn(void) { + deeply_nested_function_c(10); + // After resuming and recursion unwinding + // This shouldn't actually be reached in the current flow + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + ret = wasix_context_create(&ctx3, context3_fn); + assert(ret == 0 && "Failed to create context 3"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + wasix_context_destroy(ctx3); + + fprintf(stderr, "Deep call stack switching test passed (counter=%d)\n", + counter); + return 0; +} diff --git a/lib/wasix/tests/context_switching/malloc_during_switch.c b/lib/wasix/tests/context_switching/malloc_during_switch.c new file mode 100644 index 00000000000..91f8848789f --- /dev/null +++ b/lib/wasix/tests/context_switching/malloc_during_switch.c @@ -0,0 +1,108 @@ +// Test memory allocations during context switches +// This can trigger store context issues if malloc implementation +// does host calls while the store is borrowed +#include +#include +#include +#include +#include + +wasix_context_id_t ctx1, ctx2, ctx3; +void *allocations[100]; +int alloc_count = 0; + +void allocate_and_switch(void); + +void context1_fn(void) { + allocate_and_switch(); + + // Free some allocations + for (int i = 0; i < 10 && i < alloc_count; i++) { + free(allocations[i]); + allocations[i] = NULL; + } + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + allocate_and_switch(); + + // Reallocate some memory + for (int i = 10; i < 20 && i < alloc_count; i++) { + if (allocations[i]) { + allocations[i] = realloc(allocations[i], 2048); + assert(allocations[i] != NULL); + } + } + + wasix_context_switch(ctx1); +} + +void context3_fn(void) { + allocate_and_switch(); + + // More allocations + for (int i = 0; i < 15; i++) { + void *ptr = calloc(1, 512); + assert(ptr != NULL); + if (alloc_count < 100) { + allocations[alloc_count++] = ptr; + } + } + + wasix_context_switch(ctx2); +} + +void allocate_and_switch(void) { + // Allocate memory + for (int i = 0; i < 10; i++) { + void *ptr = malloc(1024 + i * 100); + assert(ptr != NULL); + memset(ptr, i, 1024 + i * 100); + + if (alloc_count < 100) { + allocations[alloc_count++] = ptr; + } + + // Switch contexts during allocations + if (i == 5) { + if (alloc_count < 30) { + wasix_context_switch(ctx3); + } else if (alloc_count < 60) { + wasix_context_switch(ctx2); + } + } + } +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + ret = wasix_context_create(&ctx3, context3_fn); + assert(ret == 0 && "Failed to create context 3"); + + // Start execution + wasix_context_switch(ctx1); + + // Free remaining allocations + for (int i = 0; i < alloc_count; i++) { + if (allocations[i]) { + free(allocations[i]); + } + } + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + wasix_context_destroy(ctx3); + + fprintf(stderr, "Malloc during switch test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/nested_host_call_switch.c b/lib/wasix/tests/context_switching/nested_host_call_switch.c new file mode 100644 index 00000000000..1f63f6a1070 --- /dev/null +++ b/lib/wasix/tests/context_switching/nested_host_call_switch.c @@ -0,0 +1,125 @@ +// Test switching contexts while nested in the middle of syscalls +// This directly tries to trigger "store context still borrowed" by +// calling wasix_context_switch during operations that hold a store borrow +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +wasix_context_id_t ctx1, ctx2; +int phase = 0; + +void do_directory_operations(void); +void do_file_operations(void); + +void context1_fn(void) { + phase = 1; + + // Do syscalls that might hold store borrows + do_directory_operations(); + + phase = 3; + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + phase = 2; + + // Do different syscalls while ctx1 might have pending operations + do_file_operations(); + + phase = 4; + wasix_context_switch(ctx1); +} + +void do_directory_operations(void) { + // Create a directory + mkdir("/tmp/test_dir", 0755); + + // Open the directory + DIR *dir = opendir("/tmp"); + if (!dir) { + perror("opendir"); + return; + } + + // Read some entries + struct dirent *entry; + int count = 0; + while ((entry = readdir(dir)) != NULL && count < 5) { + count++; + + // Switch context while directory is open and we're in the middle of reading + if (count == 2) { + wasix_context_switch(ctx2); + // After resuming, continue reading directory + } + } + + // Close directory + closedir(dir); + + // Remove the directory + rmdir("/tmp/test_dir"); +} + +void do_file_operations(void) { + // Open multiple files + int fd1 = open("/tmp/file1.txt", O_CREAT | O_RDWR, 0644); + int fd2 = open("/tmp/file2.txt", O_CREAT | O_RDWR, 0644); + + if (fd1 < 0 || fd2 < 0) { + perror("open"); + if (fd1 >= 0) + close(fd1); + if (fd2 >= 0) + close(fd2); + return; + } + + // Write to both + const char *data = "test data\n"; + write(fd1, data, strlen(data)); + write(fd2, data, strlen(data)); + + // Get file stats while files are open + struct stat st; + fstat(fd1, &st); + + // Close files + close(fd1); + close(fd2); + + // Remove files + unlink("/tmp/file1.txt"); + unlink("/tmp/file2.txt"); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + // Start execution + wasix_context_switch(ctx1); + + // Verify phases completed + assert(phase >= 2 && "Should have executed both contexts"); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Nested host call switch test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/pending_file_operations.c b/lib/wasix/tests/context_switching/pending_file_operations.c new file mode 100644 index 00000000000..53f35f17e50 --- /dev/null +++ b/lib/wasix/tests/context_switching/pending_file_operations.c @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include +#include + +// Test context switching while file operations are pending + +wasix_context_id_t ctx1, ctx2, ctx3; +int pipe_fds[2]; + +void context1_fn(void) { + char buffer[256]; + + // Open a file for writing + FILE *fp = fopen("/tmp/ctx_test1.txt", "w"); + assert(fp != NULL && "Failed to open file"); + + // Write some data + fprintf(fp, "Context 1 writing data\n"); + fflush(fp); + + // Don't close the file yet, switch to another context + wasix_context_switch(ctx2); + + // After resuming, continue with the file + fprintf(fp, "Context 1 continued\n"); + fclose(fp); + + // Read from pipe + ssize_t n = read(pipe_fds[0], buffer, sizeof(buffer)); + assert(n > 0 && "Failed to read from pipe"); + + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + char buffer[256]; + + // Open another file + FILE *fp = fopen("/tmp/ctx_test2.txt", "w"); + assert(fp != NULL && "Failed to open file"); + + fprintf(fp, "Context 2 data\n"); + + // Write to pipe while file is open + write(pipe_fds[1], "pipe data", 9); + + // Switch with file still open + wasix_context_switch(ctx3); + + // Resume and close + fclose(fp); + + wasix_context_switch(ctx1); +} + +void context3_fn(void) { + // Do some operations + FILE *fp = tmpfile(); + assert(fp != NULL && "Failed to create temp file"); + + fprintf(fp, "Temp data\n"); + rewind(fp); + + char buffer[64]; + fgets(buffer, sizeof(buffer), fp); + + // Switch while temp file is open + wasix_context_switch(ctx2); + + // This code won't execute in this flow + fclose(fp); +} + +int main() { + int ret; + + // Create pipe + ret = pipe(pipe_fds); + assert(ret == 0 && "Failed to create pipe"); + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + ret = wasix_context_create(&ctx3, context3_fn); + assert(ret == 0 && "Failed to create context 3"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + close(pipe_fds[0]); + close(pipe_fds[1]); + unlink("/tmp/ctx_test1.txt"); + unlink("/tmp/ctx_test2.txt"); + + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + wasix_context_destroy(ctx3); + + fprintf(stderr, "Pending file operations test passed\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/recursive_host_calls.c b/lib/wasix/tests/context_switching/recursive_host_calls.c new file mode 100644 index 00000000000..c073f1b67b7 --- /dev/null +++ b/lib/wasix/tests/context_switching/recursive_host_calls.c @@ -0,0 +1,107 @@ +// Test context switching during recursive system calls +// This tries to trigger the store context borrow error by doing +// complex syscalls that might borrow the store while switching +#include +#include +#include +#include +#include +#include +#include +#include + +wasix_context_id_t ctx1, ctx2, ctx3; +int recursion_depth = 0; +int max_depth = 5; + +void recursive_file_operations(int depth); + +void context1_fn(void) { + recursive_file_operations(0); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + recursive_file_operations(0); + wasix_context_switch(ctx3); + // Should not reach here + wasix_context_switch(wasix_context_main); +} + +void context3_fn(void) { + recursive_file_operations(0); + wasix_context_switch(ctx1); + // Should not reach here + wasix_context_switch(wasix_context_main); +} + +void recursive_file_operations(int depth) { + if (depth >= max_depth) { + return; + } + + recursion_depth = depth; + + // Create a file with dynamic name + char filename[64]; + snprintf(filename, sizeof(filename), "/tmp/test_%d_%d.txt", (int)getpid(), + depth); + + // Open file (syscall that borrows store) + int fd = open(filename, O_CREAT | O_RDWR, 0644); + if (fd < 0) { + perror("open"); + return; + } + + // Write some data (another syscall) + char data[256]; + snprintf(data, sizeof(data), "Depth: %d, PID: %d\n", depth, (int)getpid()); + ssize_t written = write(fd, data, strlen(data)); + + // Switch context while file is open + if (depth == 2) { + if (recursion_depth == 2) { + wasix_context_switch(ctx2); + // After resuming, continue operations + } + } + + // Recurse deeper + recursive_file_operations(depth + 1); + + // More operations after recursion + lseek(fd, 0, SEEK_SET); + char readbuf[256] = {0}; + read(fd, readbuf, sizeof(readbuf) - 1); + + // Close the file + close(fd); + + // Remove the file (another syscall) + unlink(filename); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + + ret = wasix_context_create(&ctx3, context3_fn); + assert(ret == 0 && "Failed to create context 3"); + + // Start execution + wasix_context_switch(ctx1); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + wasix_context_destroy(ctx3); + + fprintf(stderr, "Recursive host calls test passed\n"); + return 0; +} From 5e00cf969c69a96be1c6e019bcf8a3fb56910a0d Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 14:43:14 +0100 Subject: [PATCH 053/114] Fix hallucinated broken tests --- lib/wasix/tests/context_switching.rs | 60 ++++++++- .../context_switching/active_context_id.c | 45 ++++--- .../correct_context_activated.c | 63 ++++++++++ .../deep_call_stack_switching.c | 93 -------------- .../function_args_preserved.c | 71 +++++++++++ .../tests/context_switching/global_ctx_ids.c | 59 +++++++++ .../context_switching/mutual_recursion.c | 60 +++++++++ .../context_switching/shared_recursion.c | 59 +++++++++ .../context_switching/switch_from_recursion.c | 69 +++++++++++ .../switch_to_never_resumed.c | 116 ++++++++++++++++++ .../context_switching/three_way_recursion.c | 88 +++++++++++++ .../context_switching/wrong_entrypoint.c | 54 ++++++++ 12 files changed, 719 insertions(+), 118 deletions(-) create mode 100644 lib/wasix/tests/context_switching/correct_context_activated.c delete mode 100644 lib/wasix/tests/context_switching/deep_call_stack_switching.c create mode 100644 lib/wasix/tests/context_switching/function_args_preserved.c create mode 100644 lib/wasix/tests/context_switching/global_ctx_ids.c create mode 100644 lib/wasix/tests/context_switching/mutual_recursion.c create mode 100644 lib/wasix/tests/context_switching/shared_recursion.c create mode 100644 lib/wasix/tests/context_switching/switch_from_recursion.c create mode 100644 lib/wasix/tests/context_switching/switch_to_never_resumed.c create mode 100644 lib/wasix/tests/context_switching/three_way_recursion.c create mode 100644 lib/wasix/tests/context_switching/wrong_entrypoint.c diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index d3b9dc364ac..3ac7c7f5dca 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -185,12 +185,6 @@ fn test_pending_file_operations() { test_with_wasixcc("pending_file_operations").unwrap(); } -#[cfg(target_os = "linux")] -#[test] -fn test_deep_call_stack_switching() { - test_with_wasixcc("deep_call_stack_switching").unwrap(); -} - #[cfg(target_os = "linux")] #[test] fn test_recursive_host_calls() { @@ -214,3 +208,57 @@ fn test_nested_host_call_switch() { fn test_active_context_id() { test_with_wasixcc("active_context_id").unwrap(); } + +#[cfg(target_os = "linux")] +#[test] +fn test_wrong_entrypoint() { + test_with_wasixcc("wrong_entrypoint").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_switch_to_never_resumed() { + test_with_wasixcc("switch_to_never_resumed").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_function_args_preserved() { + test_with_wasixcc("function_args_preserved").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_correct_context_activated() { + test_with_wasixcc("correct_context_activated").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_switch_from_recursion() { + test_with_wasixcc("switch_from_recursion").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_global_ctx_ids() { + test_with_wasixcc("global_ctx_ids").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_shared_recursion() { + test_with_wasixcc("shared_recursion").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_mutual_recursion() { + test_with_wasixcc("mutual_recursion").unwrap(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_three_way_recursion() { + test_with_wasixcc("three_way_recursion").unwrap(); +} diff --git a/lib/wasix/tests/context_switching/active_context_id.c b/lib/wasix/tests/context_switching/active_context_id.c index 103ea23018e..5c5d7d53663 100644 --- a/lib/wasix/tests/context_switching/active_context_id.c +++ b/lib/wasix/tests/context_switching/active_context_id.c @@ -1,24 +1,26 @@ -// Test that active_context_id is updated correctly during context switches -// This is a simple test to verify the implementation bug where -// current_context_id is not updated when switching to a target context +// Test that wasix_context_main always returns the main context ID +// regardless of which context is currently active #include #include #include wasix_context_id_t ctx1, ctx2; +wasix_context_id_t main_ctx_id; int phase = 0; void context1_fn(void) { phase = 1; - // When ctx1 is running, wasix_context_main should return ctx1's ID - wasix_context_id_t active_id = wasix_context_main; - fprintf(stderr, "Phase 1: active context ID in ctx1 = %llu (expected %llu)\n", - (unsigned long long)active_id, (unsigned long long)ctx1); + // wasix_context_main should always return the main context's ID, + // even when running in ctx1 + wasix_context_id_t main_id = wasix_context_main; + fprintf(stderr, + "Phase 1: wasix_context_main in ctx1 = %llu (expected %llu)\n", + (unsigned long long)main_id, (unsigned long long)main_ctx_id); - // This assertion should pass - the active context should be ctx1 - assert(active_id == ctx1 && - "Active context should be ctx1 when running in ctx1"); + // This assertion verifies that wasix_context_main returns the main context + assert(main_id == main_ctx_id && + "wasix_context_main should return main context ID even in ctx1"); wasix_context_switch(ctx2); @@ -29,15 +31,16 @@ void context1_fn(void) { void context2_fn(void) { phase = 2; - // When ctx2 is running, wasix_context_main should return ctx2's ID - wasix_context_id_t active_id = wasix_context_main; - fprintf(stderr, "Phase 2: active context ID in ctx2 = %llu (expected %llu)\n", - (unsigned long long)active_id, (unsigned long long)ctx2); + // wasix_context_main should always return the main context's ID, + // even when running in ctx2 + wasix_context_id_t main_id = wasix_context_main; + fprintf(stderr, + "Phase 2: wasix_context_main in ctx2 = %llu (expected %llu)\n", + (unsigned long long)main_id, (unsigned long long)main_ctx_id); - // This assertion exposes the bug - active context is still ctx1 instead of - // ctx2 - assert(active_id == ctx2 && - "Active context should be ctx2 when running in ctx2"); + // This assertion verifies that wasix_context_main returns the main context + assert(main_id == main_ctx_id && + "wasix_context_main should return main context ID even in ctx2"); wasix_context_switch(ctx1); } @@ -45,6 +48,10 @@ void context2_fn(void) { int main() { int ret; + // Store the main context ID for comparison in other contexts + main_ctx_id = wasix_context_main; + fprintf(stderr, "Main context ID = %llu\n", (unsigned long long)main_ctx_id); + ret = wasix_context_create(&ctx1, context1_fn); assert(ret == 0 && "Failed to create context 1"); @@ -58,6 +65,6 @@ int main() { wasix_context_destroy(ctx1); wasix_context_destroy(ctx2); - fprintf(stderr, "Active context ID test passed\n"); + fprintf(stderr, "wasix_context_main test passed\n"); return 0; } diff --git a/lib/wasix/tests/context_switching/correct_context_activated.c b/lib/wasix/tests/context_switching/correct_context_activated.c new file mode 100644 index 00000000000..6eb0cf13a54 --- /dev/null +++ b/lib/wasix/tests/context_switching/correct_context_activated.c @@ -0,0 +1,63 @@ +// Test that the correct context is activated when switching by ID +// Creates 3 contexts and verifies each one's entrypoint is called when +// switching to it +#include +#include +#include + +wasix_context_id_t ctx1, ctx2, ctx3; + +void context1_fn(void) { + fprintf(stderr, "context1_fn was called (expected for ctx1=%llu)\n", + (unsigned long long)ctx1); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "context2_fn was called (expected for ctx2=%llu)\n", + (unsigned long long)ctx2); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context3_fn(void) { + fprintf(stderr, "context3_fn was called (expected for ctx3=%llu)\n", + (unsigned long long)ctx3); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + // Create contexts in order: ctx1, ctx2, ctx3 + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0); + fprintf(stderr, "Created ctx1=%llu with entrypoint=context1_fn\n", + (unsigned long long)ctx1); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0); + fprintf(stderr, "Created ctx2=%llu with entrypoint=context2_fn\n", + (unsigned long long)ctx2); + + ret = wasix_context_create(&ctx3, context3_fn); + assert(ret == 0); + fprintf(stderr, "Created ctx3=%llu with entrypoint=context3_fn\n", + (unsigned long long)ctx3); + + // Now switch to ctx1 specifically + fprintf(stderr, "\nSwitching to ctx1 (id=%llu)\n", (unsigned long long)ctx1); + fflush(stderr); + wasix_context_switch(ctx1); + fprintf(stderr, "Back from ctx1\n\n"); + + // Cleanup and test passed + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + wasix_context_destroy(ctx3); + + fprintf(stderr, "Test passed - ctx1 was correctly activated!\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/deep_call_stack_switching.c b/lib/wasix/tests/context_switching/deep_call_stack_switching.c deleted file mode 100644 index 02e2163f4d1..00000000000 --- a/lib/wasix/tests/context_switching/deep_call_stack_switching.c +++ /dev/null @@ -1,93 +0,0 @@ -#include -#include -#include -#include - -// Test rapid context switching with active function calls on the stack - -wasix_context_id_t ctx1, ctx2, ctx3; -int counter = 0; - -// Deep call stack with context switches in the middle -void deeply_nested_function_a(int depth); -void deeply_nested_function_b(int depth); -void deeply_nested_function_c(int depth); - -void deeply_nested_function_a(int depth) { - if (depth == 0) { - counter++; - wasix_context_switch(ctx2); - return; - } - - deeply_nested_function_b(depth - 1); -} - -void deeply_nested_function_b(int depth) { - if (depth == 0) { - counter++; - wasix_context_switch(ctx3); - return; - } - - deeply_nested_function_c(depth - 1); -} - -void deeply_nested_function_c(int depth) { - if (depth == 0) { - counter++; - wasix_context_switch(ctx1); - return; - } - - deeply_nested_function_a(depth - 1); -} - -void context1_fn(void) { - deeply_nested_function_a(20); - - // After resuming from the switch and the recursion unwinding - // We should always have counter >= 3 because all three contexts incremented - // it - assert(counter >= 3); - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - deeply_nested_function_b(15); - // After resuming and recursion unwinding, switch back to continue the chain - // This shouldn't actually be reached in the current flow - wasix_context_switch(wasix_context_main); -} - -void context3_fn(void) { - deeply_nested_function_c(10); - // After resuming and recursion unwinding - // This shouldn't actually be reached in the current flow - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0 && "Failed to create context 2"); - - ret = wasix_context_create(&ctx3, context3_fn); - assert(ret == 0 && "Failed to create context 3"); - - // Start execution - wasix_context_switch(ctx1); - - // Cleanup - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - wasix_context_destroy(ctx3); - - fprintf(stderr, "Deep call stack switching test passed (counter=%d)\n", - counter); - return 0; -} diff --git a/lib/wasix/tests/context_switching/function_args_preserved.c b/lib/wasix/tests/context_switching/function_args_preserved.c new file mode 100644 index 00000000000..f022cf96b54 --- /dev/null +++ b/lib/wasix/tests/context_switching/function_args_preserved.c @@ -0,0 +1,71 @@ +// Test that function arguments are preserved correctly when contexts start +#include +#include +#include + +wasix_context_id_t ctx1, ctx2; +int value_seen_in_ctx1 = -1; +int value_seen_in_ctx2 = -1; + +void recursive_function(int depth, int expected_depth) { + if (depth != expected_depth) { + fprintf(stderr, "ERROR: depth=%d but expected_depth=%d\n", depth, + expected_depth); + fflush(stderr); + } + + if (depth == 0) { + fprintf(stderr, "Reached depth 0\n"); + fflush(stderr); + return; + } + + recursive_function(depth - 1, expected_depth - 1); +} + +void context1_fn(void) { + fprintf(stderr, "context1_fn: calling recursive_function(5, 5)\n"); + fflush(stderr); + recursive_function(5, 5); + fprintf(stderr, "context1_fn: returned from recursive_function\n"); + fflush(stderr); + value_seen_in_ctx1 = 5; + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "context2_fn: calling recursive_function(3, 3)\n"); + fflush(stderr); + recursive_function(3, 3); + fprintf(stderr, "context2_fn: returned from recursive_function\n"); + fflush(stderr); + value_seen_in_ctx2 = 3; + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0); + + fprintf(stderr, "Switching to ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); + + fprintf(stderr, "Switching to ctx2\n"); + fflush(stderr); + wasix_context_switch(ctx2); + + assert(value_seen_in_ctx1 == 5 && "ctx1 should have completed with depth 5"); + assert(value_seen_in_ctx2 == 3 && "ctx2 should have completed with depth 3"); + + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Test passed - function arguments preserved correctly!\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/global_ctx_ids.c b/lib/wasix/tests/context_switching/global_ctx_ids.c new file mode 100644 index 00000000000..b794fa10c6f --- /dev/null +++ b/lib/wasix/tests/context_switching/global_ctx_ids.c @@ -0,0 +1,59 @@ +// Test that global variables containing context IDs are accessible from context +// entrypoints +#include +#include +#include + +wasix_context_id_t ctx1, ctx2; + +void context1_fn(void) { + fprintf(stderr, "ctx1 entrypoint: ctx1=%llu, ctx2=%llu\n", + (unsigned long long)ctx1, (unsigned long long)ctx2); + fflush(stderr); + + // Try to switch to ctx2 + fprintf(stderr, "ctx1: switching to ctx2\n"); + fflush(stderr); + wasix_context_switch(ctx2); + + fprintf(stderr, "ctx1: resumed\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "ctx2 entrypoint: ctx1=%llu, ctx2=%llu\n", + (unsigned long long)ctx1, (unsigned long long)ctx2); + fflush(stderr); + + fprintf(stderr, "ctx2: switching back to ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); +} + +int main() { + int ret; + + fprintf(stderr, "Before creation: ctx1=%llu, ctx2=%llu\n", + (unsigned long long)ctx1, (unsigned long long)ctx2); + fflush(stderr); + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0); + fprintf(stderr, "After ctx1 creation: ctx1=%llu\n", (unsigned long long)ctx1); + fflush(stderr); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0); + fprintf(stderr, "After ctx2 creation: ctx2=%llu\n", (unsigned long long)ctx2); + fflush(stderr); + + fprintf(stderr, "main: switching to ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); + + fprintf(stderr, "Test passed!\n"); + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + return 0; +} diff --git a/lib/wasix/tests/context_switching/mutual_recursion.c b/lib/wasix/tests/context_switching/mutual_recursion.c new file mode 100644 index 00000000000..86cecedb322 --- /dev/null +++ b/lib/wasix/tests/context_switching/mutual_recursion.c @@ -0,0 +1,60 @@ +// Absolute minimal bug: mutually recursive functions with context switch +#include +#include +#include + +wasix_context_id_t ctx1, ctx2; + +void func_b(int depth); + +void func_a(int depth) { + fprintf(stderr, "[func_a] depth=%d\n", depth); + fflush(stderr); + if (depth == 0) + return; + func_b(depth - 1); +} + +void func_b(int depth) { + fprintf(stderr, "[func_b] depth=%d\n", depth); + fflush(stderr); + if (depth == 0) + return; + func_a(depth - 1); +} + +void context1_fn(void) { + fprintf(stderr, "ctx1: calling func_a(3)\n"); + fflush(stderr); + func_a(3); + fprintf(stderr, "ctx1: done\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "ctx2: calling func_b(2)\n"); + fflush(stderr); + func_b(2); + fprintf(stderr, "ctx2: done\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +int main() { + wasix_context_create(&ctx1, context1_fn); + wasix_context_create(&ctx2, context2_fn); + + fprintf(stderr, "main: switching to ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); + + fprintf(stderr, "main: switching to ctx2\n"); + fflush(stderr); + wasix_context_switch(ctx2); + + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + fprintf(stderr, "Test passed!\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/shared_recursion.c b/lib/wasix/tests/context_switching/shared_recursion.c new file mode 100644 index 00000000000..21aeeb3298f --- /dev/null +++ b/lib/wasix/tests/context_switching/shared_recursion.c @@ -0,0 +1,59 @@ +// Minimal bug reproduction: calling same recursive functions from different +// contexts +#include +#include +#include + +wasix_context_id_t ctx1, ctx2; +int switch_count = 0; + +void shared_func(int depth) { + fprintf(stderr, "[shared_func] depth=%d\n", depth); + fflush(stderr); + + if (depth == 1 && switch_count == 0) { + switch_count++; + fprintf(stderr, "[shared_func] switching to ctx2\n"); + fflush(stderr); + wasix_context_switch(ctx2); + fprintf(stderr, "[shared_func] resumed\n"); + fflush(stderr); + } + + if (depth == 0) { + return; + } + shared_func(depth - 1); +} + +void context1_fn(void) { + fprintf(stderr, "ctx1: calling shared_func(2)\n"); + fflush(stderr); + shared_func(2); + fprintf(stderr, "ctx1: done\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "ctx2: calling shared_func(1)\n"); + fflush(stderr); + shared_func(1); + fprintf(stderr, "ctx2: done\n"); + fflush(stderr); + wasix_context_switch(ctx1); +} + +int main() { + wasix_context_create(&ctx1, context1_fn); + wasix_context_create(&ctx2, context2_fn); + + fprintf(stderr, "Switching to ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); + + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + fprintf(stderr, "Test passed!\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/switch_from_recursion.c b/lib/wasix/tests/context_switching/switch_from_recursion.c new file mode 100644 index 00000000000..21ef2b12e9d --- /dev/null +++ b/lib/wasix/tests/context_switching/switch_from_recursion.c @@ -0,0 +1,69 @@ +// Minimal test: context switches from within a recursive function +#include +#include +#include + +wasix_context_id_t ctx1, ctx2; +int call_count = 0; + +void recursive_func(int depth) { + call_count++; + fprintf(stderr, "[recursive_func depth=%d] call_count=%d\n", depth, + call_count); + fflush(stderr); + + if (depth == 0) { + fprintf(stderr, "[recursive_func] reached depth 0, switching to ctx2\n"); + fflush(stderr); + wasix_context_switch(ctx2); + fprintf(stderr, "[recursive_func] resumed after switch\n"); + fflush(stderr); + return; + } + + recursive_func(depth - 1); + fprintf(stderr, "[recursive_func depth=%d] returning\n", depth); + fflush(stderr); +} + +void context1_fn(void) { + fprintf(stderr, "ctx1: starting\n"); + fflush(stderr); + recursive_func(3); + fprintf(stderr, "ctx1: after recursive_func returned\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "ctx2: starting\n"); + fflush(stderr); + wasix_context_switch(ctx1); + fprintf(stderr, "ctx2: ERROR - should not reach here\n"); + fflush(stderr); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0); + + fprintf(stderr, "main: switching to ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); + + fprintf(stderr, "main: back from ctx1, call_count=%d\n", call_count); + fflush(stderr); + assert(call_count == 4 && + "Should have made 4 recursive calls (depth 3,2,1,0)"); + + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + + fprintf(stderr, "Test passed!\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/switch_to_never_resumed.c b/lib/wasix/tests/context_switching/switch_to_never_resumed.c new file mode 100644 index 00000000000..b56c0baad92 --- /dev/null +++ b/lib/wasix/tests/context_switching/switch_to_never_resumed.c @@ -0,0 +1,116 @@ +// Test that switching to a never-resumed context activates the correct context +// This test creates 3 contexts and switches between them to verify +// that the correct entrypoint is called for each +#include +#include +#include + +wasix_context_id_t ctx1, ctx2, ctx3; +int execution_order[10]; +int order_idx = 0; + +void context1_fn(void) { + fprintf(stderr, "context1_fn executing (ctx1=%llu)\n", + (unsigned long long)ctx1); + fflush(stderr); + execution_order[order_idx++] = 1; + + // Switch to ctx2 (which has never been resumed before) + fprintf(stderr, "ctx1 switching to ctx2 (id=%llu)\n", + (unsigned long long)ctx2); + fflush(stderr); + wasix_context_switch(ctx2); + + // After ctx2 switches back to us + fprintf(stderr, "ctx1 resumed\n"); + fflush(stderr); + execution_order[order_idx++] = 4; + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "context2_fn executing (ctx2=%llu)\n", + (unsigned long long)ctx2); + fflush(stderr); + execution_order[order_idx++] = 2; + + // Switch to ctx3 (which has never been resumed before) + fprintf(stderr, "ctx2 switching to ctx3 (id=%llu)\n", + (unsigned long long)ctx3); + fflush(stderr); + wasix_context_switch(ctx3); + + // Should not reach here in this test + fprintf(stderr, "ERROR: ctx2 resumed unexpectedly\n"); + fflush(stderr); + execution_order[order_idx++] = 99; + wasix_context_switch(wasix_context_main); +} + +void context3_fn(void) { + fprintf(stderr, "context3_fn executing (ctx3=%llu)\n", + (unsigned long long)ctx3); + fflush(stderr); + execution_order[order_idx++] = 3; + + // Switch back to ctx1 + fprintf(stderr, "ctx3 switching to ctx1 (id=%llu)\n", + (unsigned long long)ctx1); + fflush(stderr); + wasix_context_switch(ctx1); + + // Should not reach here in this test + fprintf(stderr, "ERROR: ctx3 resumed unexpectedly\n"); + fflush(stderr); + execution_order[order_idx++] = 99; + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + // Create all three contexts while in main + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + fprintf(stderr, "Created ctx1 with id=%llu\n", (unsigned long long)ctx1); + fflush(stderr); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + fprintf(stderr, "Created ctx2 with id=%llu\n", (unsigned long long)ctx2); + fflush(stderr); + + ret = wasix_context_create(&ctx3, context3_fn); + assert(ret == 0 && "Failed to create context 3"); + fprintf(stderr, "Created ctx3 with id=%llu\n", (unsigned long long)ctx3); + fflush(stderr); + + // Now switch to ctx1 (which will switch to ctx2, which will switch to ctx3, + // which will switch back to ctx1) + fprintf(stderr, "Main switching to ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); + + // Verify execution order: ctx1 -> ctx2 -> ctx3 -> ctx1 (resumed) + fprintf(stderr, "Back in main. Execution order:"); + for (int i = 0; i < order_idx; i++) { + fprintf(stderr, " %d", execution_order[i]); + } + fprintf(stderr, "\n"); + fflush(stderr); + + assert(order_idx == 4 && "Should have 4 execution steps"); + assert(execution_order[0] == 1 && "ctx1 should run first"); + assert(execution_order[1] == 2 && + "ctx2 should run second (BUG: might be ctx3)"); + assert(execution_order[2] == 3 && "ctx3 should run third"); + assert(execution_order[3] == 4 && "ctx1 should resume fourth"); + + // Cleanup + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + wasix_context_destroy(ctx3); + + fprintf(stderr, "Test passed - correct contexts were activated!\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/three_way_recursion.c b/lib/wasix/tests/context_switching/three_way_recursion.c new file mode 100644 index 00000000000..8cf27760be7 --- /dev/null +++ b/lib/wasix/tests/context_switching/three_way_recursion.c @@ -0,0 +1,88 @@ +// Minimal: 3 mutually recursive funcs, 3 contexts, circular pattern +#include +#include +#include + +wasix_context_id_t ctx1, ctx2, ctx3; + +void func_b(int d); +void func_c(int d); + +void func_a(int d) { + fprintf(stderr, "[func_a] d=%d\n", d); + fflush(stderr); + if (d == 0) { + fprintf(stderr, "[func_a] SWITCH to ctx2\n"); + fflush(stderr); + wasix_context_switch(ctx2); + return; + } + func_b(d - 1); +} + +void func_b(int d) { + fprintf(stderr, "[func_b] d=%d\n", d); + fflush(stderr); + if (d == 0) { + fprintf(stderr, "[func_b] SWITCH to ctx3\n"); + fflush(stderr); + wasix_context_switch(ctx3); + return; + } + func_c(d - 1); +} + +void func_c(int d) { + fprintf(stderr, "[func_c] d=%d\n", d); + fflush(stderr); + if (d == 0) { + fprintf(stderr, "[func_c] SWITCH to ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); + return; + } + func_a(d - 1); +} + +void context1_fn(void) { + fprintf(stderr, "ctx1_start\n"); + fflush(stderr); + func_a(5); + fprintf(stderr, "ctx1_end\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "ctx2_start\n"); + fflush(stderr); + func_b(3); + fprintf(stderr, "ctx2_end\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context3_fn(void) { + fprintf(stderr, "ctx3_start\n"); + fflush(stderr); + func_c(2); + fprintf(stderr, "ctx3_end\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +int main() { + wasix_context_create(&ctx1, context1_fn); + wasix_context_create(&ctx2, context2_fn); + wasix_context_create(&ctx3, context3_fn); + + fprintf(stderr, "==> ctx1\n"); + fflush(stderr); + wasix_context_switch(ctx1); + + wasix_context_destroy(ctx1); + wasix_context_destroy(ctx2); + wasix_context_destroy(ctx3); + fprintf(stderr, "PASS\n"); + return 0; +} diff --git a/lib/wasix/tests/context_switching/wrong_entrypoint.c b/lib/wasix/tests/context_switching/wrong_entrypoint.c new file mode 100644 index 00000000000..a2428555bf0 --- /dev/null +++ b/lib/wasix/tests/context_switching/wrong_entrypoint.c @@ -0,0 +1,54 @@ +// Minimal test to check if the correct entrypoint is being called +#include +#include +#include + +wasix_context_id_t ctx1, ctx2, ctx3; + +void context1_fn(void) { + fprintf(stderr, "context1_fn was called!\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context2_fn(void) { + fprintf(stderr, "context2_fn was called!\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +void context3_fn(void) { + fprintf(stderr, "context3_fn was called!\n"); + fflush(stderr); + wasix_context_switch(wasix_context_main); +} + +int main() { + int ret; + + ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0); + + ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0); + + ret = wasix_context_create(&ctx3, context3_fn); + assert(ret == 0); + + fprintf(stderr, "Switching to ctx1 (id=%llu)\n", (unsigned long long)ctx1); + fflush(stderr); + wasix_context_switch(ctx1); + + fprintf(stderr, "Back in main, switching to ctx2 (id=%llu)\n", + (unsigned long long)ctx2); + fflush(stderr); + wasix_context_switch(ctx2); + + fprintf(stderr, "Back in main, switching to ctx3 (id=%llu)\n", + (unsigned long long)ctx3); + fflush(stderr); + wasix_context_switch(ctx3); + + fprintf(stderr, "Test passed\n"); + return 0; +} From 739ff1ad5ebbdc972fafe051617add1845899790 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 16:19:08 +0100 Subject: [PATCH 054/114] Add a short descriptions to the context switching functions --- lib/wasix/src/syscalls/wasix/context_create.rs | 9 +++++++++ lib/wasix/src/syscalls/wasix/context_destroy.rs | 10 +++++++++- lib/wasix/src/syscalls/wasix/context_switch.rs | 10 +++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index fce9ab55969..cbdf8031884 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -51,6 +51,15 @@ pub fn lookup_typechecked_entrypoint( Ok(entrypoint) } +/// Create a new context. +/// +/// Creates a new context in the suspended state. On its first resumption, +/// `entrypoint` is invoked within that context. +/// +/// Refer to the wasix-libc [`wasix/context.h`] header for authoritative +/// documentation. +/// +/// [`wasix/context.h`]: https://github.com/wasix-org/wasix-libc/blob/main/libc-bottom-half/headers/public/wasix/context.h #[instrument(level = "trace", skip(ctx), ret)] pub fn context_create( mut ctx: FunctionEnvMut<'_, WasiEnv>, diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index 76825910fe5..b0bb19f726d 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -14,7 +14,15 @@ use wasmer::{ }; use wasmer::{StoreMut, Tag, Type}; -/// ### `context_delete()` +/// Destroy a suspended or terminated context +/// +/// After a successful call its identifier becomes invalid and its +/// resources are released. Destroying an already deleted context is a no-op. +/// +/// Refer to the wasix-libc [`wasix/context.h`] header for authoritative +/// documentation. +/// +/// [`wasix/context.h`]: https://github.com/wasix-org/wasix-libc/blob/main/libc-bottom-half/headers/public/wasix/context.h #[instrument(level = "trace", skip(ctx), ret)] pub fn context_destroy( mut ctx: FunctionEnvMut<'_, WasiEnv>, diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 5c25cc71bb4..f2ce185f517 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -17,7 +17,15 @@ use wasmer::{ }; use wasmer::{StoreMut, Tag, Type}; // TODO: combine context_switch and inner_context_switch -/// Switch to another context +/// Suspend the active context and resume another +/// +/// The resumed context continues from where it was last suspended, or from its +/// entrypoint if it has never been resumed. +/// +/// Refer to the wasix-libc [`wasix/context.h`] header for authoritative +/// documentation. +/// +/// [`wasix/context.h`]: https://github.com/wasix-org/wasix-libc/blob/main/libc-bottom-half/headers/public/wasix/context.h #[instrument(level = "trace", skip(ctx))] pub fn context_switch( mut ctx: AsyncFunctionEnvMut, From 5131716b8611ee081d8a03af2233da1d1ce73be9 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 16:31:02 +0100 Subject: [PATCH 055/114] Add two more todo comments for context switching --- lib/wasix/src/state/env.rs | 3 +++ lib/wasix/src/syscalls/wasix/context_create.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index c0e0f179c31..91c91840e99 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -184,6 +184,9 @@ pub struct WasiEnv { /// Tracks the active contexts of the WASIX context switching API /// /// This is `None` when the main function was not launched with context switching + /// + /// Should probably only be set by [`ContextSwitchingContext::run_main_context`] + // TODO: Rename to context_switching_environment pub(crate) context_switching_context: Option, } diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index cbdf8031884..0ed88f4679e 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -17,6 +17,7 @@ use wasmer::{ use wasmer::{StoreMut, Tag, Type}; /// Return the function corresponding to the given entrypoint index if it exists and has the signature `() -> ()` +// TODO: Use a typed function pub fn lookup_typechecked_entrypoint( data: &WasiEnv, mut store: &mut StoreMut<'_>, From a5d6f0275875d18aa0daa64e916d03646879616c Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 16:37:01 +0100 Subject: [PATCH 056/114] Add a comment to explain how context deletion is initiated --- lib/wasix/src/syscalls/wasix/context_destroy.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index b0bb19f726d..1059973baf6 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -61,8 +61,11 @@ pub fn context_destroy( return Ok(Errno::Inval); } - // TODO: actually delete the context let removed_future = contexts.remove_unblocker(&target_context_id); + // As soon as the Sender is dropped, the corresponding context will be able unblocked, + // the executor will continue executing it. The context will respond to the + // cancelation by terminating gracefully. + let Some(_) = removed_future else { // Context did not exist, so we do not need to remove it tracing::trace!( From 1a810630073eae7d9e1df42702d45477cf2e190b Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 16:59:11 +0100 Subject: [PATCH 057/114] Organize imports in context switching functions --- .../src/syscalls/wasix/context_create.rs | 21 ++++------------- .../src/syscalls/wasix/context_destroy.rs | 19 ++++----------- .../src/syscalls/wasix/context_switch.rs | 23 +++++-------------- 3 files changed, 14 insertions(+), 49 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 0ed88f4679e..bd127694cf2 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -1,20 +1,7 @@ -use super::*; -use crate::os::task::thread::context_switching::{ContextCanceled, ContextSwitchingContext}; -use crate::syscalls::*; -use crate::utils::thread_local_executor::ThreadLocalSpawnerError; -use core::panic; -use futures::TryFutureExt; -use futures::channel::oneshot::{Receiver, Sender}; -use futures::task::LocalSpawnExt; -use futures::{FutureExt, channel::oneshot}; -use std::collections::BTreeMap; -use std::sync::atomic::AtomicU32; -use std::sync::{Arc, OnceLock, RwLock}; -use wasmer::{ - AsStoreMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, Module, - RuntimeError, Store, Value, imports, -}; -use wasmer::{StoreMut, Tag, Type}; +use crate::{WasiEnv, WasiError}; +use tracing::instrument; +use wasmer::{Function, FunctionEnvMut, MemorySize, RuntimeError, StoreMut, Value, WasmPtr}; +use wasmer_wasix_types::wasi::Errno; /// Return the function corresponding to the given entrypoint index if it exists and has the signature `() -> ()` // TODO: Use a typed function diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index 1059973baf6..4fd06ea06e4 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -1,18 +1,7 @@ -use super::*; -use crate::syscalls::*; -use anyhow::Result; -use core::panic; -use futures::TryFutureExt; -use futures::task::LocalSpawnExt; -use futures::{FutureExt, channel::oneshot}; -use std::collections::BTreeMap; -use std::sync::atomic::AtomicU32; -use std::sync::{Arc, OnceLock, RwLock}; -use wasmer::{ - AsStoreMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, Module, - RuntimeError, Store, Value, imports, -}; -use wasmer::{StoreMut, Tag, Type}; +use crate::{WasiEnv, WasiError}; +use tracing::instrument; +use wasmer::{FunctionEnvMut, MemoryView}; +use wasmer_wasix_types::wasi::Errno; /// Destroy a suspended or terminated context /// diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index f2ce185f517..ed8d59d49c2 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,21 +1,10 @@ -use super::*; -use crate::os::task::thread::context_switching::ContextSwitchError; -use crate::syscalls::*; +use crate::{WasiEnv, os::task::thread::context_switching::ContextSwitchError}; use MaybeLater::{Later, Now}; -use anyhow::Result; -use core::panic; -use futures::TryFutureExt; -use futures::task::LocalSpawnExt; -use futures::{FutureExt, channel::oneshot}; -use std::collections::BTreeMap; -use std::sync::atomic::AtomicU32; -use std::sync::{Arc, OnceLock, RwLock}; -use thiserror::Error; -use wasmer::{ - AsStoreMut, AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, - Memory, Module, RuntimeError, Store, Value, imports, -}; -use wasmer::{StoreMut, Tag, Type}; +use futures::FutureExt; +use tracing::instrument; +use wasmer::{AsyncFunctionEnvMut, RuntimeError}; +use wasmer_wasix_types::wasi::Errno; + // TODO: combine context_switch and inner_context_switch /// Suspend the active context and resume another /// From 1f6cd81598f5f85c97fbecf111aedc0e981a0405 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 17:13:04 +0100 Subject: [PATCH 058/114] Clean up context_switch --- .../src/os/task/thread/context_switching.rs | 6 +- .../src/syscalls/wasix/context_switch.rs | 58 +++++-------------- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 8c29c5c9424..aa69dc7ef44 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -16,7 +16,7 @@ use std::{ }, }; use thiserror::Error; -use wasmer::{AsStoreRef, RuntimeError, Store}; +use wasmer::{RuntimeError, Store}; #[derive(Debug)] pub(crate) struct ContextSwitchingContext { @@ -139,6 +139,10 @@ impl ContextSwitchingContext { .insert(target_context_id, unblocker) } + /// Unblock the target context and suspend own context + /// + /// If this function succeeds, you MUST await the returned future + #[must_use] pub(crate) fn switch( &self, target_context_id: u64, diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index ed8d59d49c2..93f6f6aca94 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,11 +1,9 @@ use crate::{WasiEnv, os::task::thread::context_switching::ContextSwitchError}; -use MaybeLater::{Later, Now}; use futures::FutureExt; use tracing::instrument; use wasmer::{AsyncFunctionEnvMut, RuntimeError}; use wasmer_wasix_types::wasi::Errno; -// TODO: combine context_switch and inner_context_switch /// Suspend the active context and resume another /// /// The resumed context continues from where it was last suspended, or from its @@ -16,49 +14,19 @@ use wasmer_wasix_types::wasi::Errno; /// /// [`wasix/context.h`]: https://github.com/wasix-org/wasix-libc/blob/main/libc-bottom-half/headers/public/wasix/context.h #[instrument(level = "trace", skip(ctx))] -pub fn context_switch( - mut ctx: AsyncFunctionEnvMut, - target_context_id: u64, -) -> impl Future> + 'static + use<> { - inner_context_switch(ctx, target_context_id) -} - -enum MaybeLater< - F: Future + Send + 'static, - T: Send + 'static = Result, -> { - Now(T), - Later(F), -} -impl + Send + 'static, T: Send + 'static> MaybeLater { - fn future(self) -> impl Future + Send + 'static { - async move { - match self { - MaybeLater::Now(v) => v, - MaybeLater::Later(fut) => fut.await, - } - } - } -} - -/// Helper function that allows us to return from the synchronous part early -/// -/// The order of operations in here is quite delicate, so be careful when -/// modifying this function. It's important to not leave the env in -/// an inconsistent state. -async fn inner_context_switch( +pub async fn context_switch( mut ctx: AsyncFunctionEnvMut, target_context_id: u64, ) -> Result { - // // TODO: Should we call do_pending_operations here? + // TODO: Should we call do_pending_operations here? // match WasiEnv::do_pending_operations(&mut ctx) { // Ok(()) => {} // Err(e) => { // return Now(Err(RuntimeError::user(Box::new(e)))); // } // } + let mut write_lock = ctx.write().await; - // let (data) = ctx.(); let data = write_lock.data_mut(); // Verify that we are in an async context @@ -71,27 +39,31 @@ async fn inner_context_switch( }; // Get own context ID - let own_context_id = contexts.active_context_id(); + let active_context_id = contexts.active_context_id(); // If switching to self, do nothing - if own_context_id == target_context_id { - tracing::trace!("Switching context {own_context_id} to itself, which is a no-op"); + if active_context_id == target_context_id { + tracing::trace!("Switching context {active_context_id} to itself, which is a no-op"); return Ok(Errno::Success); } - // Try to unblock the target and put our unblock function into the env, if successful + // Try to unblock the target and get future to wait until we are unblocked again + // + // We must be careful not to return after this point without awaiting the resulting future let wait_for_unblock = match contexts.switch(target_context_id) { Ok(wait_for_unblock) => wait_for_unblock, Err(ContextSwitchError::SwitchTargetMissing) => { tracing::trace!( - "Context {own_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" + "Context {active_context_id} tried to switch to context {target_context_id} but it does not exist or is not suspended" ); return Ok(Errno::Inval); } Err(ContextSwitchError::OwnContextAlreadyBlocked) => { // This should never happen, because the active context should never have an unblock function (as it is not suspended) // If it does, it is an error in WASIX - panic!("There is already a unblock present for the current context {own_context_id}"); + panic!( + "There is already a unblock present for the current context {active_context_id}" + ); } Err(ContextSwitchError::SwitchUnblockFailed) => { // If there is no target to unblock, we assume it exited, but the unblock @@ -100,13 +72,13 @@ async fn inner_context_switch( // // TODO: Think about whether this is correct tracing::trace!( - "Context {own_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" + "Context {active_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" ); return Ok(Errno::Inval); } }; - // Drop the write lock before awaiting, as that would cause a deadlock + // Drop the write lock before we suspend ourself, as that would cause a deadlock drop(write_lock); // Wait until we are unblocked again From 8e593bcb13865f38f8c269ad8e33e7b0ba1a1e2e Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 17:25:02 +0100 Subject: [PATCH 059/114] Remove dead code --- lib/wasix/src/lib.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/lib/wasix/src/lib.rs b/lib/wasix/src/lib.rs index 92320811beb..4363f5baafc 100644 --- a/lib/wasix/src/lib.rs +++ b/lib/wasix/src/lib.rs @@ -320,37 +320,6 @@ impl std::fmt::Display for WasiRuntimeErrorDisplay<'_> { } } -#[allow(clippy::result_large_err)] -pub(crate) fn run_wasi_func( - func: &wasmer::Function, - store: &mut impl AsStoreMut, - params: &[wasmer::Value], -) -> Result, WasiRuntimeError> { - func.call(store, params).map_err(|err| { - if let Some(_werr) = err.downcast_ref::() { - let werr = err.downcast::().unwrap(); - WasiRuntimeError::Wasi(werr) - } else { - WasiRuntimeError::Runtime(err) - } - }) -} - -/// Run a main function. -/// -/// This is usually called "_start" in WASI modules. -/// The function will not receive arguments or return values. -/// -/// An exit code that is not 0 will be returned as a `WasiError::Exit`. -#[allow(clippy::result_large_err)] -pub(crate) fn run_wasi_func_start( - func: &wasmer::Function, - store: &mut impl AsStoreMut, -) -> Result<(), WasiRuntimeError> { - run_wasi_func(func, store, &[])?; - Ok(()) -} - #[derive(Debug)] pub struct WasiVFork { /// The unwound stack before the vfork occured From 4700fe28afd22371e6ce556e4f5f7177c6da1f89 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 1 Dec 2025 17:28:16 +0100 Subject: [PATCH 060/114] Document fields in the context-switching environment --- lib/wasix/src/os/task/thread/context_switching.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index aa69dc7ef44..dd256ab3b43 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -26,10 +26,14 @@ pub(crate) struct ContextSwitchingContext { #[derive(Debug)] struct ContextSwitchingContextInner { - /// TODO: Document these fields + /// List of the unblockers for all suspended contexts unblockers: RwLock>>>, + /// The ID of the currently active context current_context_id: AtomicU64, + /// The next available context ID next_available_context_id: AtomicU64, + /// This spawner can be used to spawn tasks onto the thread-local executor + /// associated with this context switching environment spawner: ThreadLocalSpawner, } From bbd2f544c9550e6aae32e12b7c50a1451c009c33 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 09:54:43 +0100 Subject: [PATCH 061/114] Rename context switching context to context-switching environment --- lib/wasix/src/bin_factory/exec.rs | 4 ++-- .../src/os/task/thread/context_switching.rs | 19 +++++++++---------- lib/wasix/src/state/env.rs | 11 +++++------ .../src/syscalls/wasix/context_create.rs | 4 ++-- .../src/syscalls/wasix/context_destroy.rs | 8 ++++---- .../src/syscalls/wasix/context_switch.rs | 6 +++--- lib/wasix/src/syscalls/wasix/thread_spawn.rs | 4 ++-- 7 files changed, 27 insertions(+), 29 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index ab9e85559df..393611fc9ba 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -5,7 +5,7 @@ use crate::{ os::task::{ TaskJoinHandle, thread::{ - RewindResultType, WasiThreadRunGuard, context_switching::ContextSwitchingContext, + RewindResultType, WasiThreadRunGuard, context_switching::ContextSwitchingEnvironment, }, }, runtime::{ @@ -301,7 +301,7 @@ fn call_module( }; let (mut store, mut call_ret) = - ContextSwitchingContext::run_main_context(&ctx, store, start.clone(), vec![]); + ContextSwitchingEnvironment::run_main_context(&ctx, store, start.clone(), vec![]); loop { // Technically, it's an error for a vfork to return from main, but anyway... diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index dd256ab3b43..2f50921d045 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -19,13 +19,12 @@ use thiserror::Error; use wasmer::{RuntimeError, Store}; #[derive(Debug)] -pub(crate) struct ContextSwitchingContext { - /// TODO: Document these fields - inner: Arc, +pub(crate) struct ContextSwitchingEnvironment { + inner: Arc, } #[derive(Debug)] -struct ContextSwitchingContextInner { +struct ContextSwitchingEnvironmentInner { /// List of the unblockers for all suspended contexts unblockers: RwLock>>>, /// The ID of the currently active context @@ -53,10 +52,10 @@ const MAIN_CONTEXT_ID: u64 = 0; #[error("Context was canceled")] pub struct ContextCanceled(); -impl ContextSwitchingContext { +impl ContextSwitchingEnvironment { fn new(spawner: ThreadLocalSpawner) -> Self { Self { - inner: Arc::new(ContextSwitchingContextInner { + inner: Arc::new(ContextSwitchingEnvironmentInner { unblockers: RwLock::new(BTreeMap::new()), current_context_id: AtomicU64::new(MAIN_CONTEXT_ID), next_available_context_id: AtomicU64::new(MAIN_CONTEXT_ID + 1), @@ -81,10 +80,10 @@ impl ContextSwitchingContext { // Put the spawner into the WASI env, so that syscalls can use it to queue up new tasks let env = ctx.data_mut(&mut store); - let previous_context = env.context_switching_context.replace(this); - if previous_context.is_some() { + let previous_environment = env.context_switching_environment.replace(this); + if previous_environment.is_some() { panic!( - "Failed to start a wasix main context as there was already a context switching context present in the WASI env." + "Failed to start a wasix main context as there was already a context-switching environment present." ); } @@ -98,7 +97,7 @@ impl ContextSwitchingContext { let mut store = store_async.into_store().ok().unwrap(); let env = ctx.data_mut(&mut store); - env.context_switching_context.take().expect( + env.context_switching_environment.take().expect( "Failed to remove wasix context switching context from WASI env after main context finished, this should never happen", ); diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 91c91840e99..8da5976a256 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -12,7 +12,7 @@ use crate::{ process::{WasiProcess, WasiProcessId}, thread::{ WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId, - context_switching::ContextSwitchingContext, + context_switching::ContextSwitchingEnvironment, }, }, syscalls::platform_clock_time_get, @@ -186,8 +186,7 @@ pub struct WasiEnv { /// This is `None` when the main function was not launched with context switching /// /// Should probably only be set by [`ContextSwitchingContext::run_main_context`] - // TODO: Rename to context_switching_environment - pub(crate) context_switching_context: Option, + pub(crate) context_switching_environment: Option, } impl std::fmt::Debug for WasiEnv { @@ -217,7 +216,7 @@ impl Clone for WasiEnv { replaying_journal: self.replaying_journal, skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, - context_switching_context: None, + context_switching_environment: None, } } } @@ -259,7 +258,7 @@ impl WasiEnv { replaying_journal: false, skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap, disable_fs_cleanup: self.disable_fs_cleanup, - context_switching_context: None, + context_switching_environment: None, }; Ok((new_env, handle)) } @@ -404,7 +403,7 @@ impl WasiEnv { bin_factory: init.bin_factory, capabilities: init.capabilities, disable_fs_cleanup: false, - context_switching_context: None, + context_switching_environment: None, }; env.owned_handles.push(thread); diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index bd127694cf2..d773be832fe 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -68,7 +68,7 @@ pub fn context_create( let (data, mut store) = ctx.data_and_store_mut(); // Verify that we are in an async context - let contexts = match &data.context_switching_context { + let environment = match &data.context_switching_environment { Some(c) => c, None => { tracing::trace!("Context switching is not enabled"); @@ -85,7 +85,7 @@ pub fn context_create( }; // Create the new context - let new_context_id = contexts.new_context(|new_context_id| { + let new_context_id = environment.new_context(|new_context_id| { // Sync part (not needed for now, but will make it easier to work with more complex entrypoints later) async move { // Call the entrypoint function diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index 4fd06ea06e4..29617c7425c 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -23,7 +23,7 @@ pub fn context_destroy( let memory: MemoryView<'_> = unsafe { env.memory_view(&ctx) }; // Verify that we are in an async context - let contexts = match &env.context_switching_context { + let environment = match &env.context_switching_environment { Some(c) => c, None => { tracing::trace!("Context switching is not enabled"); @@ -31,8 +31,8 @@ pub fn context_destroy( } }; - let own_context_id = contexts.active_context_id(); - let main_context_id = contexts.main_context_id(); + let own_context_id = environment.active_context_id(); + let main_context_id = environment.main_context_id(); if own_context_id == target_context_id { tracing::trace!( @@ -50,7 +50,7 @@ pub fn context_destroy( return Ok(Errno::Inval); } - let removed_future = contexts.remove_unblocker(&target_context_id); + let removed_future = environment.remove_unblocker(&target_context_id); // As soon as the Sender is dropped, the corresponding context will be able unblocked, // the executor will continue executing it. The context will respond to the // cancelation by terminating gracefully. diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 93f6f6aca94..0f8301aba12 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -30,7 +30,7 @@ pub async fn context_switch( let data = write_lock.data_mut(); // Verify that we are in an async context - let contexts = match &data.context_switching_context { + let environment = match &data.context_switching_environment { Some(c) => c, None => { tracing::trace!("Context switching is not enabled"); @@ -39,7 +39,7 @@ pub async fn context_switch( }; // Get own context ID - let active_context_id = contexts.active_context_id(); + let active_context_id = environment.active_context_id(); // If switching to self, do nothing if active_context_id == target_context_id { @@ -50,7 +50,7 @@ pub async fn context_switch( // Try to unblock the target and get future to wait until we are unblocked again // // We must be careful not to return after this point without awaiting the resulting future - let wait_for_unblock = match contexts.switch(target_context_id) { + let wait_for_unblock = match environment.switch(target_context_id) { Ok(wait_for_unblock) => wait_for_unblock, Err(ContextSwitchError::SwitchTargetMissing) => { tracing::trace!( diff --git a/lib/wasix/src/syscalls/wasix/thread_spawn.rs b/lib/wasix/src/syscalls/wasix/thread_spawn.rs index 6e87af8cee4..8c3b02957b4 100644 --- a/lib/wasix/src/syscalls/wasix/thread_spawn.rs +++ b/lib/wasix/src/syscalls/wasix/thread_spawn.rs @@ -5,7 +5,7 @@ use super::*; use crate::journal::JournalEffector; use crate::{ WasiThreadHandle, - os::task::thread::{WasiMemoryLayout, context_switching::ContextSwitchingContext}, + os::task::thread::{WasiMemoryLayout, context_switching::ContextSwitchingEnvironment}, runtime::{ TaintReason, task_manager::{TaskWasm, TaskWasmRunProperties}, @@ -209,7 +209,7 @@ fn call_module_internal( .try_into() .map_err(|_| Errno::Overflow) .unwrap(); - let (mut store, thread_result) = ContextSwitchingContext::run_main_context( + let (mut store, thread_result) = ContextSwitchingEnvironment::run_main_context( ctx, store, spawn, From 7de15fba84268977b617c0eb884a86f1b40315c1 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 10:18:37 +0100 Subject: [PATCH 062/114] Light refactoring in context_switching --- .../src/os/task/thread/context_switching.rs | 26 +++++++------------ .../src/syscalls/wasix/context_create.rs | 2 +- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 2f50921d045..854f8e48da7 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -127,21 +127,6 @@ impl ContextSwitchingEnvironment { .remove(target_context_id) } - /// Insert an unblocker for the given context ID - /// - /// Returns the previous unblocker if one existed - pub(crate) fn insert_unblocker( - &self, - target_context_id: u64, - unblocker: Sender>, - ) -> Option>> { - self.inner - .unblockers - .write() - .unwrap() - .insert(target_context_id, unblocker) - } - /// Unblock the target context and suspend own context /// /// If this function succeeds, you MUST await the returned future @@ -222,7 +207,7 @@ impl ContextSwitchingEnvironment { /// The entrypoint function is called when the context is unblocked for the first time /// /// If the context is cancelled before it is unblocked, the entrypoint will not be called - pub(crate) fn new_context(&self, entrypoint: T) -> u64 + pub(crate) fn create_context(&self, entrypoint: T) -> u64 where T: FnOnce(u64) -> F + 'static, F: Future + 'static, @@ -236,7 +221,14 @@ impl ContextSwitchingEnvironment { let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); // Store the unblocker - let None = self.insert_unblocker(new_context_id, own_unblocker) else { + + let None = self + .inner + .unblockers + .write() + .unwrap() + .insert(new_context_id, own_unblocker) + else { panic!("There already is a context suspended with ID {new_context_id}"); }; diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index d773be832fe..e8724636c7d 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -85,7 +85,7 @@ pub fn context_create( }; // Create the new context - let new_context_id = environment.new_context(|new_context_id| { + let new_context_id = environment.create_context(|new_context_id| { // Sync part (not needed for now, but will make it easier to work with more complex entrypoints later) async move { // Call the entrypoint function From 4e0a1a172192d3fc84fe041a2247bded69a98370 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 10:32:14 +0100 Subject: [PATCH 063/114] Add more comments in context_switching --- .../src/os/task/thread/context_switching.rs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 854f8e48da7..eec20a61169 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -18,6 +18,8 @@ use std::{ use thiserror::Error; use wasmer::{RuntimeError, Store}; +/// The context-switching environment represents all state for WASIX context-switching +/// on a single host thread. #[derive(Debug)] pub(crate) struct ContextSwitchingEnvironment { inner: Arc, @@ -32,10 +34,11 @@ struct ContextSwitchingEnvironmentInner { /// The next available context ID next_available_context_id: AtomicU64, /// This spawner can be used to spawn tasks onto the thread-local executor - /// associated with this context switching environment + /// associated with this context-switching environment spawner: ThreadLocalSpawner, } +/// Errors that can occur during a context switch #[derive(Debug, Error)] pub enum ContextSwitchError { #[error("Target context to switch to is missing")] @@ -48,6 +51,13 @@ pub enum ContextSwitchError { const MAIN_CONTEXT_ID: u64 = 0; +/// Contexts will trap with this error as a RuntimeError::user when they are canceled +/// +/// If encountered in a host function you should do cleanup and return it unchanged +/// +/// When it bubbles up to the start of the entrypoint function of a context, it will be +/// handled by just letting the context exit silently. This is the only error that will +/// not be propagated to the main context. #[derive(Error, Debug)] #[error("Context was canceled")] pub struct ContextCanceled(); @@ -64,9 +74,9 @@ impl ContextSwitchingEnvironment { } } - /// Run the main context function in a context switching context + /// Run the main context function in a context-switching environment /// - /// This call blocks until the entrypoint returns, or it or any of the contexts it spawns traps + /// This call blocks until the entrypoint returns or traps pub(crate) fn run_main_context( ctx: &WasiFunctionEnv, mut store: Store, @@ -98,7 +108,7 @@ impl ContextSwitchingEnvironment { let env = ctx.data_mut(&mut store); env.context_switching_environment.take().expect( - "Failed to remove wasix context switching context from WASI env after main context finished, this should never happen", + "Failed to remove wasix context-switching environment from WASIX env after main context finished, this should never happen", ); (store, result) @@ -106,9 +116,7 @@ impl ContextSwitchingEnvironment { /// Get the ID of the currently active context pub(crate) fn active_context_id(&self) -> u64 { - self.inner - .current_context_id - .load(std::sync::atomic::Ordering::Relaxed) + self.inner.current_context_id.load(Ordering::Relaxed) } /// Get the id of the main context (0) @@ -177,7 +185,7 @@ impl ContextSwitchingEnvironment { // Restore our own context ID let Some(inner) = Weak::upgrade(&weak_inner) else { - // The context switching context has been dropped, so we can't proceed + // The context-switching environment has been dropped, so we can't proceed // TODO: Handle this properly todo!(); }; @@ -216,7 +224,7 @@ impl ContextSwitchingEnvironment { let new_context_id = self .inner .next_available_context_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + .fetch_add(1, Ordering::Relaxed); let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); @@ -240,7 +248,7 @@ impl ContextSwitchingEnvironment { // Set the current context ID let Some(inner) = Weak::upgrade(&weak_inner) else { - // The context switching context has been dropped, so we can't proceed + // The context-switching environment has been dropped, so we can't proceed // TODO: Handle this properly return; }; @@ -278,7 +286,7 @@ impl ContextSwitchingEnvironment { // Retrieve the main context let Some(inner) = Weak::upgrade(&weak_inner) else { - // The context switching context has been dropped, so we can't proceed + // The context-switching environment has been dropped, so we can't proceed // TODO: Handle this properly return; }; From 48ef3724dfe5cca23cb7cb4ff3de627c3a43aeb8 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 10:38:46 +0100 Subject: [PATCH 064/114] Fix clippy lints --- lib/wasix/src/os/task/thread/context_switching.rs | 11 ++++------- lib/wasix/src/syscalls/wasix/context_create.rs | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index eec20a61169..8c8bb0e1e21 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -138,7 +138,6 @@ impl ContextSwitchingEnvironment { /// Unblock the target context and suspend own context /// /// If this function succeeds, you MUST await the returned future - #[must_use] pub(crate) fn switch( &self, target_context_id: u64, @@ -198,13 +197,11 @@ impl ContextSwitchingEnvironment { match unblock_result { Ok(v) => v, Err(canceled) => { - tracing::trace!( - "Context {own_context_id} was canceled while it was suspended: {}", - canceled - ); + tracing::trace!("Context {own_context_id} was canceled while it was suspended"); - let err = ContextCanceled().into(); - return Err(RuntimeError::user(err)); + // When our context was canceled return the `ContextCanceled` error. + // It will be handled by the entrypoint wrapper and the context will exit silently. + Err(RuntimeError::user(canceled.into())) } } }) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index e8724636c7d..0c436e76bb6 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -57,7 +57,7 @@ pub fn context_create( WasiEnv::do_pending_operations(&mut ctx)?; // TODO: Unify this check with the one below for context_switching_context - let mut async_store = match ctx.as_store_async() { + let async_store = match ctx.as_store_async() { Some(c) => c, None => { tracing::trace!("The current store is not async"); @@ -90,7 +90,7 @@ pub fn context_create( async move { // Call the entrypoint function let result: Result, RuntimeError> = typechecked_entrypoint - .call_async(&mut async_store, vec![]) + .call_async(&async_store, vec![]) .await; // If that function returns, we need to resume the main context with an error From 34977917db5117c3d7d0a16a9af97f28ca311923 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 10:56:37 +0100 Subject: [PATCH 065/114] Define how to handle error cases during context switching --- .../src/os/task/thread/context_switching.rs | 36 ++++++++++--------- .../src/syscalls/wasix/context_switch.rs | 18 ---------- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 8c8bb0e1e21..6e4b114c843 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -43,10 +43,6 @@ struct ContextSwitchingEnvironmentInner { pub enum ContextSwitchError { #[error("Target context to switch to is missing")] SwitchTargetMissing, - #[error("Failed to unblock target context")] - SwitchUnblockFailed, - #[error("Own context is already blocked")] - OwnContextAlreadyBlocked, } const MAIN_CONTEXT_ID: u64 = 0; @@ -148,36 +144,42 @@ impl ContextSwitchingEnvironment { let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); // Lock contexts for this block - let mut contexts = self.inner.unblockers.write().unwrap(); + let mut unblockers = self.inner.unblockers.write().unwrap(); let own_context_id = self.active_context_id(); // Assert preconditions (target is blocked && we are unblocked) - if contexts.get(&target_context_id).is_none() { + if unblockers.get(&target_context_id).is_none() { return Err(ContextSwitchError::SwitchTargetMissing); } - if contexts.get(&own_context_id).is_some() { - return Err(ContextSwitchError::OwnContextAlreadyBlocked); + if unblockers.get(&own_context_id).is_some() { + // This should never happen, because if we are blocked, we should not be running code at all + // + // This is a bug in WASIX and should never happen, so we panic here. + panic!("There is already a unblock present for the current context {own_context_id}"); } // Unblock the target // Dont mark ourself as blocked yet, as we first need to know that unblocking succeeded - let unblock_target = contexts.remove(&target_context_id).unwrap(); // Unwrap is safe due to precondition check above + let unblock_target = unblockers.remove(&target_context_id).unwrap(); // Unwrap is safe due to precondition check above let unblock_result: std::result::Result<(), std::result::Result<(), RuntimeError>> = unblock_target.send(Ok(())); let Ok(_) = unblock_result else { - // If there is no target to unblock, we assume it exited, but the unblock function was not removed - // For now we treat this like a missing context - // It can't happen again, as we already removed the unblock function + // If there is a unblock function in unblockers, the target context must be awaiting the related future. + // One way we can get into this path is, when the target context was already resumed and we somehow managed to keep the unblocker around. + // This can't happen as calling the unblocker consumes it. + // Another way this could happen is if the future waiting for the unblocker was canceled before we called it. + // This should not happen. This would be a bug in WASIX. + // Another way this could happen is if the target context never awaited the unblocker future in the first place. + // This also would be a bug in WASIX. // - // TODO: Think about whether this is correct - tracing::trace!( - "Context {own_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" + // So if we reach this path it is a bug in WASIX and should never happen, so we panic here. + panic!( + "Context {own_context_id} tried to unblock context {target_context_id} but the unblock target does not seem to exist." ); - return Err(ContextSwitchError::SwitchUnblockFailed); }; // After we have unblocked the target, we can insert our own unblock function - contexts.insert(own_context_id, own_unblocker); + unblockers.insert(own_context_id, own_unblocker); let weak_inner = Arc::downgrade(&self.inner); Ok(async move { let unblock_result = wait_for_unblock.map_err(|_| ContextCanceled()).await; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 0f8301aba12..4092512cadb 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -58,24 +58,6 @@ pub async fn context_switch( ); return Ok(Errno::Inval); } - Err(ContextSwitchError::OwnContextAlreadyBlocked) => { - // This should never happen, because the active context should never have an unblock function (as it is not suspended) - // If it does, it is an error in WASIX - panic!( - "There is already a unblock present for the current context {active_context_id}" - ); - } - Err(ContextSwitchError::SwitchUnblockFailed) => { - // If there is no target to unblock, we assume it exited, but the unblock - // function was not removed. For now we treat this like a missing context - // It can't happen again, as we already removed the unblock function - // - // TODO: Think about whether this is correct - tracing::trace!( - "Context {active_context_id} tried to switch to context {target_context_id} but it could not be unblocked (perhaps it exited?)" - ); - return Ok(Errno::Inval); - } }; // Drop the write lock before we suspend ourself, as that would cause a deadlock From 473915f5f4c7b4e26100a2b2237ffacd5c09fea0 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 13:19:31 +0100 Subject: [PATCH 066/114] Properly define behaviour in edgecases where the executor outlives the context-switching environment --- .../src/os/task/thread/context_switching.rs | 153 +++++++++++++----- 1 file changed, 111 insertions(+), 42 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 6e4b114c843..35f3cbea08d 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -10,12 +10,14 @@ use futures::{ }; use std::{ collections::BTreeMap, + mem::forget, sync::{ Arc, RwLock, Weak, atomic::{AtomicU64, Ordering}, }, }; use thiserror::Error; +use tracing::trace; use wasmer::{RuntimeError, Store}; /// The context-switching environment represents all state for WASIX context-switching @@ -49,14 +51,31 @@ const MAIN_CONTEXT_ID: u64 = 0; /// Contexts will trap with this error as a RuntimeError::user when they are canceled /// -/// If encountered in a host function you should do cleanup and return it unchanged +/// If encountered in a host function it MUST be propagated to the context's entrypoint. +/// To make it harder to run into that behaviour by ignoring this error, dropping it +/// will cause a panic with a message that it was not propagated properly. If you think +/// you know what you are doing, you can call `defuse` (or just forget it) to avoid +/// the panic. /// /// When it bubbles up to the start of the entrypoint function of a context, it will be -/// handled by just letting the context exit silently. This is the only error that will -/// not be propagated to the main context. +/// handled by just letting the context exit silently. #[derive(Error, Debug)] #[error("Context was canceled")] -pub struct ContextCanceled(); +pub struct ContextCanceled(()); +impl ContextCanceled { + /// Defuse the ContextCanceled so it does not panic when dropped + pub fn defuse(self) { + // Consume self without panicking + forget(self); + } +} +impl Drop for ContextCanceled { + fn drop(&mut self) { + panic!( + "A ContextCanceled error was dropped without being propagated to the context's entrypoint. This is likely a bug in a host function, please make sure to propagate ContextCanceled errors properly." + ); + } +} impl ContextSwitchingEnvironment { fn new(spawner: ThreadLocalSpawner) -> Self { @@ -142,6 +161,7 @@ impl ContextSwitchingEnvironment { ContextSwitchError, > { let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); + let wait_for_unblock = wait_for_unblock.map_err(|_| ContextCanceled(())); // Lock contexts for this block let mut unblockers = self.inner.unblockers.write().unwrap(); @@ -182,30 +202,45 @@ impl ContextSwitchingEnvironment { unblockers.insert(own_context_id, own_unblocker); let weak_inner = Arc::downgrade(&self.inner); Ok(async move { - let unblock_result = wait_for_unblock.map_err(|_| ContextCanceled()).await; + let unblock_result = wait_for_unblock.await; + + // Handle if we were canceled instead of being unblocked + let result = match unblock_result { + Ok(v) => v, + Err(canceled) => { + tracing::trace!("Context {own_context_id} was canceled while it was suspended"); + + // When our context was canceled return the `ContextCanceled` error. + // It will be handled by the entrypoint wrapper and the context will exit silently. + // + // If we reach this point, we must try to restore our context ID as it will not be read again + return Err(RuntimeError::user(canceled.into())); + } + }; // Restore our own context ID let Some(inner) = Weak::upgrade(&weak_inner) else { // The context-switching environment has been dropped, so we can't proceed - // TODO: Handle this properly - todo!(); + // + // This should only happen during shutdown when the ContextSwitchingEnvironment and thus the list of unblockers + // is dropped and the futures continue being polled (because dropping that list would cause all wait_for_unblock + // futures to resolve to canceled). + // However looking at the implementation in `run_main_context` this should not happen, as we drop the executor + // before dropping the environment, + // + // In a future implementation that allows the executor to outlive the environment, we should handle this case, + // most likely by returning a `ContextCanceled` error here as well. + // For now this should never happen, so it's a WASIX bug, so we panic here. + panic!( + "The switch future for context {own_context_id} was polled after the context-switching environment was dropped, this should not happen" + ); }; inner .current_context_id .store(own_context_id, Ordering::Relaxed); drop(inner); - // Handle if we were canceled instead of being unblocked - match unblock_result { - Ok(v) => v, - Err(canceled) => { - tracing::trace!("Context {own_context_id} was canceled while it was suspended"); - - // When our context was canceled return the `ContextCanceled` error. - // It will be handled by the entrypoint wrapper and the context will exit silently. - Err(RuntimeError::user(canceled.into())) - } - } + result }) } @@ -213,6 +248,10 @@ impl ContextSwitchingEnvironment { /// /// The entrypoint function is called when the context is unblocked for the first time /// + /// If entrypoint returns, it must be a RuntimeError, as it is not allowed to return normally. + /// If the RuntimeError is a [`ContextCanceled`], the context will just exit silently. + /// Otherwise, the error will be propagated to the main context. + /// /// If the context is cancelled before it is unblocked, the entrypoint will not be called pub(crate) fn create_context(&self, entrypoint: T) -> u64 where @@ -226,6 +265,7 @@ impl ContextSwitchingEnvironment { .fetch_add(1, Ordering::Relaxed); let (own_unblocker, wait_for_unblock) = oneshot::channel::>(); + let wait_for_unblock = wait_for_unblock.map_err(|_| ContextCanceled(())); // Store the unblocker @@ -243,51 +283,80 @@ impl ContextSwitchingEnvironment { let weak_inner = Arc::downgrade(&self.inner); let context_future = async move { // First wait for the unblock signal - let prelaunch_result = wait_for_unblock.map_err(|_| ContextCanceled()).await; - - // Set the current context ID - let Some(inner) = Weak::upgrade(&weak_inner) else { - // The context-switching environment has been dropped, so we can't proceed - // TODO: Handle this properly - return; - }; - inner - .current_context_id - .store(new_context_id, Ordering::Relaxed); - drop(inner); + let prelaunch_result = wait_for_unblock.await; // Handle if the context was canceled before it even started match prelaunch_result { Ok(_) => (), Err(canceled) => { - tracing::trace!( - "Context {new_context_id} was canceled before it even started: {canceled}", - ); - // At this point we don't need to do anything else + trace!("Context {new_context_id} was successfully destroyed before it started"); + // We know what we are doing, so we can prevent the panic on drop + canceled.defuse(); + // Context was cancelled before it was started, so we can just let it return. + // This will resolve the original future passed to `spawn_local` with + // `Ok(())` which should make the executor drop it properly return; } }; + let Some(inner) = Weak::upgrade(&weak_inner) else { + // The context-switching environment has been dropped, so we can't proceed. + // See the comments on the first Weak::upgrade call in this file for background on when this can happen. + // + // Note that in case the context was canceled properly, we accept that and allowed it to exit + // silently (in the match block above). That could happen if the main context canceled the + // this context before exiting itself and the executor outlives the environment. + // + // However it should not be possible to switch to this context after the main context has exited, + // as there can only be one active context at a time and that one (the main context) just exited. + // So there can't be another context in that context-switching environment that could switch to this one. + panic!( + "Context {new_context_id} was switched to after the context-switching environment was dropped. This indicates a bug where multiple contexts are active at the same time which should never happen" + ); + }; + // Set the current context ID + inner + .current_context_id + .store(new_context_id, Ordering::Relaxed); + // Drop inner again so we don't hold a strong ref while running the entrypoint, so it cleans itself up properly + drop(inner); + // Launch the context entrypoint let launch_result = entrypoint(new_context_id).await; // If that function returns something went wrong. // If it's a cancellation, we can just let this context run out. // If it's another error, we resume the main context with the error - let error = match launch_result.downcast_ref::() { - Some(err) => { - tracing::trace!("Context {new_context_id} exited with error: {}", err); - // Context was cancelled, so we can just let it run out. + let error = match launch_result.downcast::() { + Ok(canceled) => { + tracing::trace!("Context {new_context_id} was successfully destroyed"); + // We know what we are doing, so we can prevent the panic on drop + canceled.defuse(); + // Context was cancelled, so we can just let it return. + // This will resolve the original future passed to `spawn_local` with + // `Ok(())` which should make the executor drop it properly return; } - None => launch_result, // Propagate the runtime error to main + Err(error) => error, // Propagate the runtime error to main }; // Retrieve the main context let Some(inner) = Weak::upgrade(&weak_inner) else { - // The context-switching environment has been dropped, so we can't proceed - // TODO: Handle this properly - return; + // The context-switching environment has been dropped, so we can't proceed. + // See the comments on the first Weak::upgrade call in this file for background on when this can happen. + // + // Note that in case the context was canceled properly, we accept that and allowed it to exit + // silently (in the match block above). That could happen if the main context canceled the + // this context before exiting itself and the executor outlives the environment. + // + // However it should not be possible to switch to this context after the main context has exited, + // as there can only be one active context at a time and that one (the main context) just exited. + // So there can't be another context in that context-switching environment that could switch to this one. + // + // So in conclusion if we reach this point it is a bug in WASIX and should never happen, so we panic here. + panic!( + "Context {new_context_id} was switched to after the context-switching environment was dropped. This indicates a bug where multiple contexts are active at the same time which should never happen" + ); }; let Some(main_context) = inner.unblockers.write().unwrap().remove(&MAIN_CONTEXT_ID) else { From 0601815753e194ca4aa44f13cf73191bc07198a9 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 13:38:21 +0100 Subject: [PATCH 067/114] Fix remaining TODO comment in context_switching --- .../src/os/task/thread/context_switching.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/os/task/thread/context_switching.rs index 35f3cbea08d..cea38c618fc 100644 --- a/lib/wasix/src/os/task/thread/context_switching.rs +++ b/lib/wasix/src/os/task/thread/context_switching.rs @@ -378,20 +378,25 @@ impl ContextSwitchingEnvironment { match spawn_result { Ok(()) => new_context_id, Err(ThreadLocalSpawnerError::LocalPoolShutDown) => { - // TODO: Handle cancellation properly + // This case could happen if the executor is being shut down while it is still polling a future (this one). + // Which shouldn't be able with a single-threaded executor, as the shutdown would have to + // be initiated from within a future running on that executor. + // I the current WASIX context switching implemenation should not be able to produce this case, + // but maybe it will be possible in future implementations. If someone manages to produce this case, + // they should open an issue so we can discuss how to handle this case properly. + // If this case is reachable we could return the same error as when no context-switching environment is present, panic!( - "Failed to spawn context {new_context_id} because the local executor has been shut down", + "Failed to spawn context {new_context_id} because the local executor has been shut down. Please open an issue and let me know how you produced this error.", ); } Err(ThreadLocalSpawnerError::NotOnTheCorrectThread { expected, found }) => { - // Not on the correct host thread. If this error happens, it is a bug in WASIX. + // This should never happen and is a bug in WASIX, so we panic here panic!( - "Failed to spawn context {new_context_id} because the current thread ({found:?}) is not the expected thread ({expected:?}) for the local executor" + "Failed to create context because the thread local spawner lives on {expected:?} but you are on {found:?}" ) } Err(ThreadLocalSpawnerError::SpawnError) => { - // This should never happen - panic!("Failed to spawn_local context {new_context_id} , this should not happen"); + panic!("Failed to spawn_local context {new_context_id}, this should not happen"); } } } From 0121d724773bde2b82cf655a42587f3653b15bb4 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 13:47:17 +0100 Subject: [PATCH 068/114] Move context-switching environment into a better location --- lib/wasix/src/bin_factory/exec.rs | 5 ++--- lib/wasix/src/os/task/thread.rs | 3 --- .../src/{os/task/thread => state}/context_switching.rs | 0 lib/wasix/src/state/env.rs | 7 ++----- lib/wasix/src/state/mod.rs | 1 + lib/wasix/src/syscalls/wasix/context_switch.rs | 2 +- lib/wasix/src/syscalls/wasix/thread_spawn.rs | 3 ++- 7 files changed, 8 insertions(+), 13 deletions(-) rename lib/wasix/src/{os/task/thread => state}/context_switching.rs (100%) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 393611fc9ba..c145dc72012 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -4,9 +4,7 @@ use crate::{ RewindState, SpawnError, WasiError, WasiRuntimeError, os::task::{ TaskJoinHandle, - thread::{ - RewindResultType, WasiThreadRunGuard, context_switching::ContextSwitchingEnvironment, - }, + thread::{RewindResultType, WasiThreadRunGuard}, }, runtime::{ TaintReason, @@ -15,6 +13,7 @@ use crate::{ TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, TaskWasmRunProperties, }, }, + state::context_switching::ContextSwitchingEnvironment, syscalls::rewind_ext, }; use crate::{Runtime, WasiEnv, WasiFunctionEnv}; diff --git a/lib/wasix/src/os/task/thread.rs b/lib/wasix/src/os/task/thread.rs index 93a3cd32d6a..5b25363e269 100644 --- a/lib/wasix/src/os/task/thread.rs +++ b/lib/wasix/src/os/task/thread.rs @@ -1,6 +1,3 @@ -// TODO: Move to better location -pub mod context_switching; - use super::{ control_plane::TaskCountGuard, task_join_handle::{OwnedTaskStatus, TaskJoinHandle}, diff --git a/lib/wasix/src/os/task/thread/context_switching.rs b/lib/wasix/src/state/context_switching.rs similarity index 100% rename from lib/wasix/src/os/task/thread/context_switching.rs rename to lib/wasix/src/state/context_switching.rs diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 8da5976a256..e316a216842 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -10,10 +10,7 @@ use crate::{ os::task::{ control_plane::ControlPlaneError, process::{WasiProcess, WasiProcessId}, - thread::{ - WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId, - context_switching::ContextSwitchingEnvironment, - }, + thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, }, syscalls::platform_clock_time_get, }; @@ -44,7 +41,7 @@ use wasmer_wasix_types::{ use webc::metadata::annotations::Wasi; pub use super::handles::*; -use super::{Linker, WasiState, conv_env_vars}; +use super::{Linker, WasiState, context_switching::ContextSwitchingEnvironment, conv_env_vars}; /// Data required to construct a [`WasiEnv`]. #[derive(Debug)] diff --git a/lib/wasix/src/state/mod.rs b/lib/wasix/src/state/mod.rs index 18637cee3e0..fc3463c83ba 100644 --- a/lib/wasix/src/state/mod.rs +++ b/lib/wasix/src/state/mod.rs @@ -16,6 +16,7 @@ #![allow(clippy::cognitive_complexity, clippy::too_many_arguments)] mod builder; +pub mod context_switching; mod env; mod func_env; mod handles; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 4092512cadb..c5e30fe6fd7 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,4 +1,4 @@ -use crate::{WasiEnv, os::task::thread::context_switching::ContextSwitchError}; +use crate::{WasiEnv, state::context_switching::ContextSwitchError}; use futures::FutureExt; use tracing::instrument; use wasmer::{AsyncFunctionEnvMut, RuntimeError}; diff --git a/lib/wasix/src/syscalls/wasix/thread_spawn.rs b/lib/wasix/src/syscalls/wasix/thread_spawn.rs index 8c3b02957b4..f8db1a68239 100644 --- a/lib/wasix/src/syscalls/wasix/thread_spawn.rs +++ b/lib/wasix/src/syscalls/wasix/thread_spawn.rs @@ -5,11 +5,12 @@ use super::*; use crate::journal::JournalEffector; use crate::{ WasiThreadHandle, - os::task::thread::{WasiMemoryLayout, context_switching::ContextSwitchingEnvironment}, + os::task::thread::WasiMemoryLayout, runtime::{ TaintReason, task_manager::{TaskWasm, TaskWasmRunProperties}, }, + state::context_switching::ContextSwitchingEnvironment, syscalls::*, }; From b7ae9e545eddb214f5f2fab7ae6b13c380a7e979 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 13:56:46 +0100 Subject: [PATCH 069/114] Fix unsafe conversion from TypedFunction into Function --- lib/wasix/src/syscalls/wasix/thread_spawn.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/thread_spawn.rs b/lib/wasix/src/syscalls/wasix/thread_spawn.rs index f8db1a68239..aedf26c74cb 100644 --- a/lib/wasix/src/syscalls/wasix/thread_spawn.rs +++ b/lib/wasix/src/syscalls/wasix/thread_spawn.rs @@ -190,9 +190,6 @@ fn call_module_internal( mut store: Store, start_ptr_offset: M::Offset, ) -> (Store, Result<(), DeepSleepWork>) { - // We either call the reactor callback or the thread spawn callback - //trace!("threading: invoking thread callback (reactor={})", reactor); - // Note: we ensure both unwraps can happen before getting to this point let spawn = ctx .data(&store) @@ -202,9 +199,8 @@ fn call_module_internal( .clone() .unwrap(); let tid = ctx.data(&store).tid(); - // TODO: Find a better way to get a Function from a TypedFunction - // SAFETY: no - let spawn = unsafe { std::mem::transmute::, Function>(spawn) }; + + let spawn: Function = spawn.into(); let tid_i32 = tid.raw().try_into().map_err(|_| Errno::Overflow).unwrap(); let start_pointer_i32 = start_ptr_offset .try_into() From a116748e61c12b07a3619fe43d670e3690c571b1 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 14:03:11 +0100 Subject: [PATCH 070/114] Remove do_pending_operations from context_switch --- lib/wasix/src/syscalls/wasix/context_switch.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index c5e30fe6fd7..6e13f118a1b 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -18,14 +18,6 @@ pub async fn context_switch( mut ctx: AsyncFunctionEnvMut, target_context_id: u64, ) -> Result { - // TODO: Should we call do_pending_operations here? - // match WasiEnv::do_pending_operations(&mut ctx) { - // Ok(()) => {} - // Err(e) => { - // return Now(Err(RuntimeError::user(Box::new(e)))); - // } - // } - let mut write_lock = ctx.write().await; let data = write_lock.data_mut(); From 21882f70830d237999ce65936fc873b393c6fe44 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 14:06:41 +0100 Subject: [PATCH 071/114] Add a note that _initialize for the main module and it's shared libs is not in a context switching env --- lib/wasix/src/bin_factory/exec.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index c145dc72012..0ce48e26573 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -194,10 +194,8 @@ pub fn run_exec(props: TaskWasmRunProperties) { .get_function("_initialize") .cloned() { - // TODO: The call to initialize for the main module does not yet run in an context-switching environment. - // The call to initialize for dlopen'ed modules does run in an context-switching environment. - // This should be made consistent. - // TODO: Or document that the context-switching environment only starts with main + // This does not need a context switching environment as the documentation + // states that that is only available after the first call to main let result = initialize.call(&mut store, &[]); if let Err(err) = result { From 6d06ed1b4be63da27ca74bc945fb25c3df527735 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 14:13:44 +0100 Subject: [PATCH 072/114] Remove useless TODO --- lib/wasix/src/bin_factory/exec.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 0ce48e26573..82d471eb8fa 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -302,8 +302,6 @@ fn call_module( loop { // Technically, it's an error for a vfork to return from main, but anyway... - // TODO: Then we should probably just trap immediately when that happens... - // this code is complex enough as it is match resume_vfork(&ctx, &mut store, &start, &call_ret) { // A vfork was resumed, there may be another, so loop back Ok(Some(ret)) => call_ret = ret, From 0682bfa4f11aa72cd9e65d4731947c8064e2839c Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 14:37:03 +0100 Subject: [PATCH 073/114] Use a typed function as the entrypoint when creating a new context --- .../src/syscalls/wasix/context_create.rs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 0c436e76bb6..e84da3034c4 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -1,15 +1,16 @@ use crate::{WasiEnv, WasiError}; use tracing::instrument; -use wasmer::{Function, FunctionEnvMut, MemorySize, RuntimeError, StoreMut, Value, WasmPtr}; +use wasmer::{ + Function, FunctionEnvMut, MemorySize, RuntimeError, StoreMut, TypedFunction, Value, WasmPtr, +}; use wasmer_wasix_types::wasi::Errno; /// Return the function corresponding to the given entrypoint index if it exists and has the signature `() -> ()` -// TODO: Use a typed function pub fn lookup_typechecked_entrypoint( data: &WasiEnv, mut store: &mut StoreMut<'_>, entrypoint_id: u32, -) -> Result { +) -> Result, Errno> { let entrypoint = match data .inner() .indirect_function_table_lookup(&mut store, entrypoint_id) @@ -26,15 +27,14 @@ pub fn lookup_typechecked_entrypoint( }; let entrypoint_type = entrypoint.ty(&store); - if !entrypoint_type.params().is_empty() || !entrypoint_type.results().is_empty() { + let Ok(entrypoint) = entrypoint.typed::<(), ()>(&store) else { tracing::trace!( - "Entrypoint function {} has invalid signature: expected () -> (), got {:?} -> {:?}", - entrypoint_id, + "Entrypoint function {entrypoint_id} has invalid signature: expected () -> (), got {:?} -> {:?}", entrypoint_type.params(), entrypoint_type.results() ); return Err(Errno::Inval); - } + }; Ok(entrypoint) } @@ -77,7 +77,7 @@ pub fn context_create( }; // Lookup and check the entrypoint function - let typechecked_entrypoint = match lookup_typechecked_entrypoint(data, &mut store, entrypoint) { + let entrypoint = match lookup_typechecked_entrypoint(data, &mut store, entrypoint) { Ok(func) => func, Err(e) => { return Ok(e); @@ -89,9 +89,7 @@ pub fn context_create( // Sync part (not needed for now, but will make it easier to work with more complex entrypoints later) async move { // Call the entrypoint function - let result: Result, RuntimeError> = typechecked_entrypoint - .call_async(&async_store, vec![]) - .await; + let result: Result<(), RuntimeError> = entrypoint.call_async(&async_store).await; // If that function returns, we need to resume the main context with an error // Take the underlying error, or create a new error if the context returned a value From 0b802a03153168ccf5450684f308a824a8f28cfe Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 14:41:26 +0100 Subject: [PATCH 074/114] Remove resolved TODO comment in context_create --- lib/wasix/src/syscalls/wasix/context_create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index e84da3034c4..14c66da6918 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -56,7 +56,7 @@ pub fn context_create( ) -> Result { WasiEnv::do_pending_operations(&mut ctx)?; - // TODO: Unify this check with the one below for context_switching_context + // Verify that we are in an async context let async_store = match ctx.as_store_async() { Some(c) => c, None => { From a0831c67e3fec1b781f44d04146e99ccebfbf700 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 15:39:57 +0100 Subject: [PATCH 075/114] Make sure Senders never leave ContextSwitchingEnvironment --- lib/wasix/src/state/context_switching.rs | 6 ++---- lib/wasix/src/syscalls/wasix/context_destroy.rs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index cea38c618fc..4a735ed13c3 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -139,15 +139,13 @@ impl ContextSwitchingEnvironment { MAIN_CONTEXT_ID } - pub(crate) fn remove_unblocker( - &self, - target_context_id: &u64, - ) -> Option>> { + pub(crate) fn remove_unblocker(&self, target_context_id: &u64) -> bool { self.inner .unblockers .write() .unwrap() .remove(target_context_id) + .is_some() } /// Unblock the target context and suspend own context diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index 29617c7425c..24b9a044576 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -50,12 +50,12 @@ pub fn context_destroy( return Ok(Errno::Inval); } - let removed_future = environment.remove_unblocker(&target_context_id); + let removed_unblocker = environment.remove_unblocker(&target_context_id); // As soon as the Sender is dropped, the corresponding context will be able unblocked, // the executor will continue executing it. The context will respond to the // cancelation by terminating gracefully. - let Some(_) = removed_future else { + if !removed_unblocker { // Context did not exist, so we do not need to remove it tracing::trace!( "Context {} tried to delete context {} but it is already removed", From 32b7778c0cc9c3d3a4e3033716ae4605700bf49a Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 2 Dec 2025 17:27:03 +0100 Subject: [PATCH 076/114] Handle context entrypoint functions returning --- lib/wasix/src/state/context_switching.rs | 45 ++++++++++++++++--- .../src/syscalls/wasix/context_create.rs | 27 ++--------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 4a735ed13c3..10afeebfa34 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -1,5 +1,5 @@ use crate::{ - WasiFunctionEnv, + WasiError, WasiFunctionEnv, utils::thread_local_executor::{ ThreadLocalExecutor, ThreadLocalSpawner, ThreadLocalSpawnerError, }, @@ -19,6 +19,7 @@ use std::{ use thiserror::Error; use tracing::trace; use wasmer::{RuntimeError, Store}; +use wasmer_wasix_types::wasi::ExitCode; /// The context-switching environment represents all state for WASIX context-switching /// on a single host thread. @@ -77,6 +78,16 @@ impl Drop for ContextCanceled { } } +/// Contexts will trap with this error as a RuntimeError::user when they entrypoint returns +/// +/// It is not allowed for context entrypoints to return normally, they must always +/// either get destroyed while suspended or trap with an error (like ContextCanceled) +/// +/// This error will be picked up by the main context and cause it to trap as well. +#[derive(Error, Debug)] +#[error("The entrypoint of context {0} returned which is not allowed")] +pub struct ContextEntrypointReturned(u64); + impl ContextSwitchingEnvironment { fn new(spawner: ThreadLocalSpawner) -> Self { Self { @@ -115,6 +126,22 @@ impl ContextSwitchingEnvironment { let store_async = store.into_async(); // Run function with the spawner let result = local_executor.run_until(entrypoint.call_async(&store_async, params)); + + // Process if this was terminated by a context entrypoint returning + let result = match &result { + Err(e) => match e.downcast_ref::() { + Some(ContextEntrypointReturned(id)) => { + // Context entrypoint returned, which is not allowed + // Exit with code 129 + tracing::error!("The entrypoint of context {id} returned which is not allowed"); + Err(RuntimeError::user( + WasiError::Exit(ExitCode::from(129)).into(), + )) + } + _ => result, + }, + _ => result, + }; // Drop the executor to ensure all spawned tasks are dropped, so we have no references to the StoreAsync left drop(local_executor); @@ -251,10 +278,9 @@ impl ContextSwitchingEnvironment { /// Otherwise, the error will be propagated to the main context. /// /// If the context is cancelled before it is unblocked, the entrypoint will not be called - pub(crate) fn create_context(&self, entrypoint: T) -> u64 + pub(crate) fn create_context(&self, entrypoint: F) -> u64 where - T: FnOnce(u64) -> F + 'static, - F: Future + 'static, + F: Future> + 'static, { // Create a new context ID let new_context_id = self @@ -320,12 +346,19 @@ impl ContextSwitchingEnvironment { drop(inner); // Launch the context entrypoint - let launch_result = entrypoint(new_context_id).await; + let entrypoint_result = entrypoint.await; + + // If that function returns, we need to resume the main context with an error + // Take the underlying error, or create a new error if the context returned a value + let entrypoint_result = entrypoint_result.map_or_else( + |e| e, + |_| RuntimeError::user(ContextEntrypointReturned(new_context_id).into()), + ); // If that function returns something went wrong. // If it's a cancellation, we can just let this context run out. // If it's another error, we resume the main context with the error - let error = match launch_result.downcast::() { + let error = match entrypoint_result.downcast::() { Ok(canceled) => { tracing::trace!("Context {new_context_id} was successfully destroyed"); // We know what we are doing, so we can prevent the panic on drop diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 14c66da6918..a86a5f10336 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -79,34 +79,13 @@ pub fn context_create( // Lookup and check the entrypoint function let entrypoint = match lookup_typechecked_entrypoint(data, &mut store, entrypoint) { Ok(func) => func, - Err(e) => { - return Ok(e); + Err(err) => { + return Ok(err); } }; // Create the new context - let new_context_id = environment.create_context(|new_context_id| { - // Sync part (not needed for now, but will make it easier to work with more complex entrypoints later) - async move { - // Call the entrypoint function - let result: Result<(), RuntimeError> = entrypoint.call_async(&async_store).await; - - // If that function returns, we need to resume the main context with an error - // Take the underlying error, or create a new error if the context returned a value - result.map_or_else( - |e| e, - |v| { - // TODO: Proper error type - RuntimeError::user( - format!( - "Context {new_context_id} returned a value ({v:?}). This is not allowed for now" - ) - .into(), - ) - }, - ) - } - }); + let new_context_id = environment.create_context(entrypoint.call_async(&async_store)); // Write the new context ID into memory let memory = unsafe { data.memory_view(&store) }; From f21b949d160ac8c149d828296a6dbf3af7c800c0 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Wed, 3 Dec 2025 11:48:01 +0100 Subject: [PATCH 077/114] Add do_pending_operations to context_switch --- lib/wasix/src/syscalls/wasix/context_switch.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 6e13f118a1b..a7fdf2f0fa4 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -19,6 +19,15 @@ pub async fn context_switch( target_context_id: u64, ) -> Result { let mut write_lock = ctx.write().await; + + let mut sync_env = write_lock.as_function_env_mut(); + match WasiEnv::do_pending_operations(&mut sync_env) { + Ok(()) => {} + Err(e) => { + return Err(RuntimeError::user(e.into())); + } + } + let data = write_lock.data_mut(); // Verify that we are in an async context From e95354d20d5954c8732952948088df70d80affe9 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Thu, 4 Dec 2025 10:09:45 +0100 Subject: [PATCH 078/114] Improve context-switching trace messages --- lib/wasix/src/state/context_switching.rs | 24 ++++++++++++++----- .../src/syscalls/wasix/context_switch.rs | 8 ++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 10afeebfa34..444d9864050 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -233,7 +233,7 @@ impl ContextSwitchingEnvironment { let result = match unblock_result { Ok(v) => v, Err(canceled) => { - tracing::trace!("Context {own_context_id} was canceled while it was suspended"); + tracing::trace!("Canceled context {own_context_id} while it was suspended"); // When our context was canceled return the `ContextCanceled` error. // It will be handled by the entrypoint wrapper and the context will exit silently. @@ -335,7 +335,7 @@ impl ContextSwitchingEnvironment { // as there can only be one active context at a time and that one (the main context) just exited. // So there can't be another context in that context-switching environment that could switch to this one. panic!( - "Context {new_context_id} was switched to after the context-switching environment was dropped. This indicates a bug where multiple contexts are active at the same time which should never happen" + "Resumed context {new_context_id} after the context-switching environment was dropped. This indicates a bug where multiple contexts are active at the same time which should never happen" ); }; // Set the current context ID @@ -345,6 +345,8 @@ impl ContextSwitchingEnvironment { // Drop inner again so we don't hold a strong ref while running the entrypoint, so it cleans itself up properly drop(inner); + tracing::trace!("Resumed context {new_context_id} for the first time"); + // Launch the context entrypoint let entrypoint_result = entrypoint.await; @@ -360,7 +362,9 @@ impl ContextSwitchingEnvironment { // If it's another error, we resume the main context with the error let error = match entrypoint_result.downcast::() { Ok(canceled) => { - tracing::trace!("Context {new_context_id} was successfully destroyed"); + tracing::trace!( + "Destroyed context {new_context_id} successfully after it was canceled" + ); // We know what we are doing, so we can prevent the panic on drop canceled.defuse(); // Context was cancelled, so we can just let it return. @@ -371,6 +375,8 @@ impl ContextSwitchingEnvironment { Err(error) => error, // Propagate the runtime error to main }; + tracing::trace!("Context {new_context_id} entrypoint returned with {error:?}"); + // Retrieve the main context let Some(inner) = Weak::upgrade(&weak_inner) else { // The context-switching environment has been dropped, so we can't proceed. @@ -386,9 +392,13 @@ impl ContextSwitchingEnvironment { // // So in conclusion if we reach this point it is a bug in WASIX and should never happen, so we panic here. panic!( - "Context {new_context_id} was switched to after the context-switching environment was dropped. This indicates a bug where multiple contexts are active at the same time which should never happen" + "Context {new_context_id} entrypoint returned after the context-switching environment was dropped. This indicates a bug where multiple contexts are active at the same time which should never happen" ); }; + + tracing::trace!( + "Resuming main context {MAIN_CONTEXT_ID} with error from context {new_context_id}" + ); let Some(main_context) = inner.unblockers.write().unwrap().remove(&MAIN_CONTEXT_ID) else { // The main context should always be suspended when another context returns or traps with anything but cancellation @@ -396,14 +406,16 @@ impl ContextSwitchingEnvironment { "The main context should always be suspended when another context returns or traps (with anything but a cancellation)." ); }; + drop(inner); + // Resume the main context with the error main_context .send(Err(error)) .expect("Failed to send error to main context, this should not happen"); - drop(inner); }; // Queue the future onto the thread-local executor + tracing::trace!("Spawning context {new_context_id} onto the thread-local executor"); let spawn_result = self.inner.spawner.spawn_local(context_future); match spawn_result { @@ -427,7 +439,7 @@ impl ContextSwitchingEnvironment { ) } Err(ThreadLocalSpawnerError::SpawnError) => { - panic!("Failed to spawn_local context {new_context_id}, this should not happen"); + panic!("Failed to spawn context {new_context_id}, this should not happen"); } } } diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index a7fdf2f0fa4..6ba4ececdd9 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -63,7 +63,13 @@ pub async fn context_switch( // Drop the write lock before we suspend ourself, as that would cause a deadlock drop(write_lock); + tracing::trace!("Suspending context {active_context_id} to switch to {target_context_id}"); // Wait until we are unblocked again - wait_for_unblock.map(|v| v.map(|_| Errno::Success)).await + let result = wait_for_unblock.map(|v| v.map(|_| Errno::Success)).await; + tracing::trace!("Resumed context {active_context_id} after being switched back to"); + if let Err(e) = &result { + tracing::trace!("But it has an error {e:?}"); + } + result } From f0f312695cede47d84442eb7919131ae2df24e41 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 5 Dec 2025 11:54:31 +0100 Subject: [PATCH 079/114] Add support for engines not supporting async execution --- lib/wasix/Cargo.toml | 5 ++-- lib/wasix/src/lib.rs | 8 ++++-- lib/wasix/src/state/context_switching.rs | 17 +++++++---- .../src/syscalls/wasix/context_create.rs | 28 +++++++++---------- .../src/syscalls/wasix/context_destroy.rs | 6 ++-- .../src/syscalls/wasix/context_switch.rs | 21 ++++++++++++-- 6 files changed, 58 insertions(+), 27 deletions(-) diff --git a/lib/wasix/Cargo.toml b/lib/wasix/Cargo.toml index a46dbf9402f..3ffdb251694 100644 --- a/lib/wasix/Cargo.toml +++ b/lib/wasix/Cargo.toml @@ -18,7 +18,9 @@ wasmer-wasix-types = { path = "../wasi-types", version = "0.601.0", features = [ "enable-serde", ] } wasmer-types = { path = "../types", version = "=6.1.0", default-features = false } -wasmer = { path = "../api", version = "=6.1.0", default-features = false } +wasmer = { path = "../api", version = "=6.1.0", default-features = false, features = [ + "experimental-async", +] } virtual-mio = { path = "../virtual-io", version = "0.601.0", default-features = false } virtual-fs = { path = "../virtual-fs", version = "0.601.0", default-features = false, features = [ "webc-fs", @@ -221,7 +223,6 @@ sys = [ "ctrlc", "wasmer/wat", "wasmer/js-serializable-module", - "wasmer/experimental-async", ] sys-default = ["sys", "wasmer/sys"] sys-poll = [] diff --git a/lib/wasix/src/lib.rs b/lib/wasix/src/lib.rs index 4363f5baafc..696bf7e94e9 100644 --- a/lib/wasix/src/lib.rs +++ b/lib/wasix/src/lib.rs @@ -494,6 +494,8 @@ fn wasi_snapshot_preview1_exports( } fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) -> Exports { + let engine_supports_async = store.as_store_ref().engine().is_sys(); + use syscalls::*; let namespace = namespace! { "args_get" => Function::new_typed_with_env(&mut store, env, args_get::), @@ -587,7 +589,7 @@ fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) "stack_checkpoint" => Function::new_typed_with_env(&mut store, env, stack_checkpoint::), "stack_restore" => Function::new_typed_with_env(&mut store, env, stack_restore::), "context_create" => Function::new_typed_with_env(&mut store, env, context_create::), - "context_switch" => Function::new_typed_with_env_async(&mut store, env, context_switch), + "context_switch" => if engine_supports_async { Function::new_typed_with_env_async(&mut store, env, context_switch) } else { Function::new_typed_with_env(&mut store, env, context_switch_not_supported) }, "context_destroy" => Function::new_typed_with_env(&mut store, env, context_destroy), "futex_wait" => Function::new_typed_with_env(&mut store, env, futex_wait::), "futex_wake" => Function::new_typed_with_env(&mut store, env, futex_wake::), @@ -637,6 +639,8 @@ fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) } fn wasix_exports_64(mut store: &mut impl AsStoreMut, env: &FunctionEnv) -> Exports { + let engine_supports_async = store.as_store_ref().engine().is_sys(); + use syscalls::*; let namespace = namespace! { "args_get" => Function::new_typed_with_env(&mut store, env, args_get::), @@ -730,7 +734,7 @@ fn wasix_exports_64(mut store: &mut impl AsStoreMut, env: &FunctionEnv) "stack_checkpoint" => Function::new_typed_with_env(&mut store, env, stack_checkpoint::), "stack_restore" => Function::new_typed_with_env(&mut store, env, stack_restore::), "context_create" => Function::new_typed_with_env(&mut store, env, context_create::), - "context_switch" => Function::new_typed_with_env_async(&mut store, env, context_switch), + "context_switch" => if engine_supports_async { Function::new_typed_with_env_async(&mut store, env, context_switch) } else { Function::new_typed_with_env(&mut store, env, context_switch_not_supported) }, "context_destroy" => Function::new_typed_with_env(&mut store, env, context_destroy), "futex_wait" => Function::new_typed_with_env(&mut store, env, futex_wait::), "futex_wake" => Function::new_typed_with_env(&mut store, env, futex_wake::), diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 444d9864050..6b3bacd8a57 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -109,12 +109,19 @@ impl ContextSwitchingEnvironment { entrypoint: wasmer::Function, params: Vec, ) -> (Store, Result, RuntimeError>) { + // Do a normal call and dont install the context switching env, if the engine does not support async + let engine_supports_async = store.engine().is_sys(); + if !engine_supports_async { + let result = entrypoint.call(&mut store, ¶ms); + return (store, result); + } + // Create a new executor let mut local_executor = ThreadLocalExecutor::new(); let this = Self::new(local_executor.spawner()); - // Put the spawner into the WASI env, so that syscalls can use it to queue up new tasks + // Add the context-switching environment to the WasiEnv let env = ctx.data_mut(&mut store); let previous_environment = env.context_switching_environment.replace(this); if previous_environment.is_some() { @@ -123,8 +130,8 @@ impl ContextSwitchingEnvironment { ); } + // Turn the store into an async store and run the entrypoint let store_async = store.into_async(); - // Run function with the spawner let result = local_executor.run_until(entrypoint.call_async(&store_async, params)); // Process if this was terminated by a context entrypoint returning @@ -142,12 +149,12 @@ impl ContextSwitchingEnvironment { }, _ => result, }; - // Drop the executor to ensure all spawned tasks are dropped, so we have no references to the StoreAsync left - drop(local_executor); - // Remove the spawner again + // Drop the executor to ensure all references to the StoreAsync are gone and convert back to a normal store + drop(local_executor); let mut store = store_async.into_store().ok().unwrap(); + // Remove the context-switching environment from the WasiEnv let env = ctx.data_mut(&mut store); env.context_switching_environment.take().expect( "Failed to remove wasix context-switching environment from WASIX env after main context finished, this should never happen", diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index a86a5f10336..cb9062b7bf0 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -1,7 +1,8 @@ use crate::{WasiEnv, WasiError}; use tracing::instrument; use wasmer::{ - Function, FunctionEnvMut, MemorySize, RuntimeError, StoreMut, TypedFunction, Value, WasmPtr, + AsStoreRef, Function, FunctionEnvMut, MemorySize, RuntimeError, StoreMut, TypedFunction, Value, + WasmPtr, }; use wasmer_wasix_types::wasi::Errno; @@ -57,23 +58,22 @@ pub fn context_create( WasiEnv::do_pending_operations(&mut ctx)?; // Verify that we are in an async context - let async_store = match ctx.as_store_async() { - Some(c) => c, - None => { - tracing::trace!("The current store is not async"); - return Ok(Errno::Again); - } + // We need to do this first, before we borrow the store mutably + let Some(async_store) = ctx.as_store_async() else { + tracing::warn!( + "The WASIX context-switching API is only available in engines supporting async execution" + ); + return Ok(Errno::Again); }; let (data, mut store) = ctx.data_and_store_mut(); - // Verify that we are in an async context - let environment = match &data.context_switching_environment { - Some(c) => c, - None => { - tracing::trace!("Context switching is not enabled"); - return Ok(Errno::Again); - } + // Get the context-switching environment + let Some(environment) = &data.context_switching_environment else { + tracing::warn!( + "The WASIX context-switching API is only available in engines supporting async execution" + ); + return Ok(Errno::Again); }; // Lookup and check the entrypoint function diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index 24b9a044576..c68526929ed 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -1,6 +1,6 @@ use crate::{WasiEnv, WasiError}; use tracing::instrument; -use wasmer::{FunctionEnvMut, MemoryView}; +use wasmer::{AsStoreRef, FunctionEnvMut, MemoryView}; use wasmer_wasix_types::wasi::Errno; /// Destroy a suspended or terminated context @@ -26,7 +26,9 @@ pub fn context_destroy( let environment = match &env.context_switching_environment { Some(c) => c, None => { - tracing::trace!("Context switching is not enabled"); + tracing::warn!( + "The WASIX context-switching API is only available in engines supporting async execution" + ); return Ok(Errno::Again); } }; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 6ba4ececdd9..72d4aa6440e 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -1,7 +1,7 @@ -use crate::{WasiEnv, state::context_switching::ContextSwitchError}; +use crate::{WasiEnv, WasiError, state::context_switching::ContextSwitchError}; use futures::FutureExt; use tracing::instrument; -use wasmer::{AsyncFunctionEnvMut, RuntimeError}; +use wasmer::{AsyncFunctionEnvMut, FunctionEnvMut, RuntimeError}; use wasmer_wasix_types::wasi::Errno; /// Suspend the active context and resume another @@ -73,3 +73,20 @@ pub async fn context_switch( } result } + +/// This stub is used for context_switch, when the engine does not support async +/// +/// It prints a warning and indicates that no context-switching environment is available. +#[instrument(level = "trace", skip(ctx))] +pub fn context_switch_not_supported( + mut ctx: FunctionEnvMut<'_, WasiEnv>, + _target_context_id: u64, +) -> Result { + WasiEnv::do_pending_operations(&mut ctx)?; + + tracing::warn!( + "The WASIX context-switching API is only available in engines supporting async execution" + ); + // Indicate that no context-switching environment is available + Ok(Errno::Again) +} From 22452b2ca95b343e7548601e3ce8ff6286d94b19 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 5 Dec 2025 14:36:22 +0100 Subject: [PATCH 080/114] Rename the functions of context_switching --- lib/wasix/src/state/context_switching.rs | 4 ++-- lib/wasix/src/syscalls/wasix/context_destroy.rs | 2 +- lib/wasix/src/syscalls/wasix/context_switch.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 6b3bacd8a57..4f13331b9bf 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -173,7 +173,7 @@ impl ContextSwitchingEnvironment { MAIN_CONTEXT_ID } - pub(crate) fn remove_unblocker(&self, target_context_id: &u64) -> bool { + pub(crate) fn destroy_context(&self, target_context_id: &u64) -> bool { self.inner .unblockers .write() @@ -185,7 +185,7 @@ impl ContextSwitchingEnvironment { /// Unblock the target context and suspend own context /// /// If this function succeeds, you MUST await the returned future - pub(crate) fn switch( + pub(crate) fn switch_context( &self, target_context_id: u64, ) -> Result< diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index c68526929ed..88b7d8f529d 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -52,7 +52,7 @@ pub fn context_destroy( return Ok(Errno::Inval); } - let removed_unblocker = environment.remove_unblocker(&target_context_id); + let removed_unblocker = environment.destroy_context(&target_context_id); // As soon as the Sender is dropped, the corresponding context will be able unblocked, // the executor will continue executing it. The context will respond to the // cancelation by terminating gracefully. diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 72d4aa6440e..990bac9b242 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -51,7 +51,7 @@ pub async fn context_switch( // Try to unblock the target and get future to wait until we are unblocked again // // We must be careful not to return after this point without awaiting the resulting future - let wait_for_unblock = match environment.switch(target_context_id) { + let wait_for_unblock = match environment.switch_context(target_context_id) { Ok(wait_for_unblock) => wait_for_unblock, Err(ContextSwitchError::SwitchTargetMissing) => { tracing::trace!( From ab0fadf66ccdefe393e3c090e75b6338cb5a4eac Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 5 Dec 2025 14:44:18 +0100 Subject: [PATCH 081/114] Refactor getting the unblocker in switch_context --- lib/wasix/src/state/context_switching.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 4f13331b9bf..c3d77871bcf 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -199,10 +199,7 @@ impl ContextSwitchingEnvironment { let mut unblockers = self.inner.unblockers.write().unwrap(); let own_context_id = self.active_context_id(); - // Assert preconditions (target is blocked && we are unblocked) - if unblockers.get(&target_context_id).is_none() { - return Err(ContextSwitchError::SwitchTargetMissing); - } + // Assert that we are unblocked if unblockers.get(&own_context_id).is_some() { // This should never happen, because if we are blocked, we should not be running code at all // @@ -210,9 +207,13 @@ impl ContextSwitchingEnvironment { panic!("There is already a unblock present for the current context {own_context_id}"); } + // Assert that the target is blocked + let Some(unblock_target) = unblockers.remove(&target_context_id) else { + return Err(ContextSwitchError::SwitchTargetMissing); + }; + // Unblock the target // Dont mark ourself as blocked yet, as we first need to know that unblocking succeeded - let unblock_target = unblockers.remove(&target_context_id).unwrap(); // Unwrap is safe due to precondition check above let unblock_result: std::result::Result<(), std::result::Result<(), RuntimeError>> = unblock_target.send(Ok(())); let Ok(_) = unblock_result else { From 62239e994f181ecf581dbddd36852350ee77dd92 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 5 Dec 2025 15:21:15 +0100 Subject: [PATCH 082/114] Replace the again errors with not supported errors --- lib/wasix/src/syscalls/wasix/context_create.rs | 4 ++-- lib/wasix/src/syscalls/wasix/context_destroy.rs | 2 +- lib/wasix/src/syscalls/wasix/context_switch.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index cb9062b7bf0..5a4860ff87a 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -63,7 +63,7 @@ pub fn context_create( tracing::warn!( "The WASIX context-switching API is only available in engines supporting async execution" ); - return Ok(Errno::Again); + return Ok(Errno::Notsup); }; let (data, mut store) = ctx.data_and_store_mut(); @@ -73,7 +73,7 @@ pub fn context_create( tracing::warn!( "The WASIX context-switching API is only available in engines supporting async execution" ); - return Ok(Errno::Again); + return Ok(Errno::Notsup); }; // Lookup and check the entrypoint function diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index 88b7d8f529d..0f0112c7456 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -29,7 +29,7 @@ pub fn context_destroy( tracing::warn!( "The WASIX context-switching API is only available in engines supporting async execution" ); - return Ok(Errno::Again); + return Ok(Errno::Notsup); } }; diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 990bac9b242..63c41ca4edd 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -35,7 +35,7 @@ pub async fn context_switch( Some(c) => c, None => { tracing::trace!("Context switching is not enabled"); - return Ok(Errno::Again); + return Ok(Errno::Notsup); } }; @@ -88,5 +88,5 @@ pub fn context_switch_not_supported( "The WASIX context-switching API is only available in engines supporting async execution" ); // Indicate that no context-switching environment is available - Ok(Errno::Again) + Ok(Errno::Notsup) } From 0e424681c0ab4b46d8162022a7fc888d70ba1dd1 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 5 Dec 2025 15:52:33 +0100 Subject: [PATCH 083/114] Change error message --- lib/wasix/src/syscalls/wasix/context_switch.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/wasix/src/syscalls/wasix/context_switch.rs b/lib/wasix/src/syscalls/wasix/context_switch.rs index 63c41ca4edd..a291aeeb155 100644 --- a/lib/wasix/src/syscalls/wasix/context_switch.rs +++ b/lib/wasix/src/syscalls/wasix/context_switch.rs @@ -34,7 +34,9 @@ pub async fn context_switch( let environment = match &data.context_switching_environment { Some(c) => c, None => { - tracing::trace!("Context switching is not enabled"); + tracing::warn!( + "The WASIX context-switching API is only available after entering the main function" + ); return Ok(Errno::Notsup); } }; From 29c8b736a3b9da1f395bedd3b050ef6859506ef5 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 8 Dec 2025 14:42:32 +0100 Subject: [PATCH 084/114] Change entrypoint to untyped function --- .../src/syscalls/wasix/context_create.rs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 5a4860ff87a..1cab52a7355 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -1,4 +1,5 @@ use crate::{WasiEnv, WasiError}; +use futures::FutureExt; use tracing::instrument; use wasmer::{ AsStoreRef, Function, FunctionEnvMut, MemorySize, RuntimeError, StoreMut, TypedFunction, Value, @@ -11,7 +12,7 @@ pub fn lookup_typechecked_entrypoint( data: &WasiEnv, mut store: &mut StoreMut<'_>, entrypoint_id: u32, -) -> Result, Errno> { +) -> Result { let entrypoint = match data .inner() .indirect_function_table_lookup(&mut store, entrypoint_id) @@ -27,15 +28,19 @@ pub fn lookup_typechecked_entrypoint( } }; - let entrypoint_type = entrypoint.ty(&store); - let Ok(entrypoint) = entrypoint.typed::<(), ()>(&store) else { - tracing::trace!( - "Entrypoint function {entrypoint_id} has invalid signature: expected () -> (), got {:?} -> {:?}", - entrypoint_type.params(), - entrypoint_type.results() - ); - return Err(Errno::Inval); - }; + // TODO: Remove this check and return a TypedFunction once all backends support types + #[cfg(not(feature = "js"))] + { + let entrypoint_type = entrypoint.ty(&store); + if !entrypoint_type.params().is_empty() && !entrypoint_type.results().is_empty() { + tracing::trace!( + "Entrypoint function {entrypoint_id} has invalid signature: expected () -> (), got {:?} -> {:?}", + entrypoint_type.params(), + entrypoint_type.results() + ); + return Err(Errno::Inval); + } + } Ok(entrypoint) } @@ -85,7 +90,11 @@ pub fn context_create( }; // Create the new context - let new_context_id = environment.create_context(entrypoint.call_async(&async_store)); + let new_context_id = environment.create_context( + entrypoint + .call_async(&async_store, vec![]) + .map(|r| r.map(|_| ())), + ); // Write the new context ID into memory let memory = unsafe { data.memory_view(&store) }; From 92dd61c6344dd81638ad38388d2539aa1fa3868f Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 8 Dec 2025 15:11:47 +0100 Subject: [PATCH 085/114] Add assertions to prevent context switching to be used in combination with vfork --- lib/wasix/src/state/context_switching.rs | 15 +++++++++++++++ lib/wasix/src/syscalls/wasix/proc_fork.rs | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index c3d77871bcf..c6bf5a2e171 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -123,6 +123,21 @@ impl ContextSwitchingEnvironment { // Add the context-switching environment to the WasiEnv let env = ctx.data_mut(&mut store); + + if env.vfork.is_some() { + // This is enforced here and in proc_fork + tracing::error!( + "process forking is mutally exclusive with WASIX context-switching features. If you need both, please open an issue." + ); + return ( + store, + Err(RuntimeError::user( + // Exit with code 129 to indicate an internal error + WasiError::Exit(ExitCode::from(129)).into(), + )), + ); + } + let previous_environment = env.context_switching_environment.replace(this); if previous_environment.is_some() { panic!( diff --git a/lib/wasix/src/syscalls/wasix/proc_fork.rs b/lib/wasix/src/syscalls/wasix/proc_fork.rs index dfc5e4eb88c..e2ddf778dcf 100644 --- a/lib/wasix/src/syscalls/wasix/proc_fork.rs +++ b/lib/wasix/src/syscalls/wasix/proc_fork.rs @@ -31,6 +31,11 @@ pub fn proc_fork( Errno::Notsup })); + wasi_try_ok!(ctx.data().context_switching_environment.as_ref().ok_or_else(|| { + warn!("process forking is mutally exclusive with WASIX context-switching features. If you need both, please open an issue."); + Errno::Notsup + })); + // If we were just restored then we need to return the value instead if let Some(result) = unsafe { handle_rewind::(&mut ctx) } { if result.pid == 0 { From 8c49e89f695f2cd8865d39351a1f977beb76de8d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 9 Dec 2025 14:48:39 +0100 Subject: [PATCH 086/114] Make all tests pass with node api --- lib/api/Cargo.toml | 2 +- lib/api/src/entities/engine/mod.rs | 10 ++++++++++ lib/wasix/src/lib.rs | 4 ++-- lib/wasix/src/state/context_switching.rs | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/api/Cargo.toml b/lib/api/Cargo.toml index a5c0ed4b562..95940c39a58 100644 --- a/lib/api/Cargo.toml +++ b/lib/api/Cargo.toml @@ -133,7 +133,7 @@ sys = ["std", "dep:wasmer-vm", "dep:wasmer-compiler"] sys-default = ["sys", "wat", "cranelift"] # Experimental concurrent execution support -experimental-async = ["sys", "dep:futures", "dep:corosensei"] +experimental-async = ["dep:futures", "dep:corosensei"] # - Compilers. compiler = [ diff --git a/lib/api/src/entities/engine/mod.rs b/lib/api/src/entities/engine/mod.rs index 1537507a9de..ec2d2aa7587 100644 --- a/lib/api/src/entities/engine/mod.rs +++ b/lib/api/src/entities/engine/mod.rs @@ -255,4 +255,14 @@ impl Engine { _ => Ok(()), } } + + #[cfg(feature = "experimental-async")] + /// Returns true if the engine supports async operations. + pub fn supports_async(&self) -> bool { + match self.be { + #[cfg(feature = "sys")] + BackendEngine::Sys(ref e) => true, + _ => false, + } + } } diff --git a/lib/wasix/src/lib.rs b/lib/wasix/src/lib.rs index 696bf7e94e9..60e6b2aba0d 100644 --- a/lib/wasix/src/lib.rs +++ b/lib/wasix/src/lib.rs @@ -494,7 +494,7 @@ fn wasi_snapshot_preview1_exports( } fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) -> Exports { - let engine_supports_async = store.as_store_ref().engine().is_sys(); + let engine_supports_async = store.as_store_ref().engine().supports_async(); use syscalls::*; let namespace = namespace! { @@ -639,7 +639,7 @@ fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) } fn wasix_exports_64(mut store: &mut impl AsStoreMut, env: &FunctionEnv) -> Exports { - let engine_supports_async = store.as_store_ref().engine().is_sys(); + let engine_supports_async = store.as_store_ref().engine().supports_async(); use syscalls::*; let namespace = namespace! { diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index c6bf5a2e171..6674d35e0a6 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -110,7 +110,7 @@ impl ContextSwitchingEnvironment { params: Vec, ) -> (Store, Result, RuntimeError>) { // Do a normal call and dont install the context switching env, if the engine does not support async - let engine_supports_async = store.engine().is_sys(); + let engine_supports_async = store.engine().supports_async(); if !engine_supports_async { let result = entrypoint.call(&mut store, ¶ms); return (store, result); From 981a041adf4f4741827871b7952be703bd7c00bb Mon Sep 17 00:00:00 2001 From: Zebreus Date: Mon, 8 Dec 2025 16:22:19 +0100 Subject: [PATCH 087/114] Add context switching for forked processes --- lib/wasix/src/syscalls/wasix/proc_fork.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/proc_fork.rs b/lib/wasix/src/syscalls/wasix/proc_fork.rs index e2ddf778dcf..076f263538a 100644 --- a/lib/wasix/src/syscalls/wasix/proc_fork.rs +++ b/lib/wasix/src/syscalls/wasix/proc_fork.rs @@ -3,6 +3,7 @@ use crate::{ WasiThreadHandle, capture_store_snapshot, os::task::OwnedTaskStatus, runtime::task_manager::{TaskWasm, TaskWasmRunProperties}, + state::context_switching::ContextSwitchingEnvironment, syscalls::*, }; use serde::{Deserialize, Serialize}; @@ -270,7 +271,7 @@ fn run( } let mut ret: ExitCode = Errno::Success.into(); - let err = if ctx.data(&store).thread.is_main() { + let (mut store, err) = if ctx.data(&store).thread.is_main() { trace!(%pid, %tid, "re-invoking main"); let start = ctx .data(&store) @@ -280,7 +281,7 @@ fn run( .start .clone() .unwrap(); - start.call(&mut store) + ContextSwitchingEnvironment::run_main_context(&ctx, store, start.into(), vec![]) } else { trace!(%pid, %tid, "re-invoking thread_spawn"); let start = ctx @@ -291,7 +292,8 @@ fn run( .thread_spawn .clone() .unwrap(); - start.call(&mut store, 0, 0) + let params = vec![0i32.into(), 0i32.into()]; + ContextSwitchingEnvironment::run_main_context(&ctx, store, start.into(), params) }; if let Err(err) = err { match err.downcast::() { From 9f4608d0d0bbf988c85e8d47d664618433d6175d Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 9 Dec 2025 17:29:59 +0100 Subject: [PATCH 088/114] Support process_fork with context switching --- lib/wasix/src/syscalls/wasi/proc_exit.rs | 6 +++--- lib/wasix/src/syscalls/wasix/proc_exec3.rs | 10 ++++++++-- lib/wasix/src/syscalls/wasix/proc_fork.rs | 14 ++++++++++---- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/wasix/src/syscalls/wasi/proc_exit.rs b/lib/wasix/src/syscalls/wasi/proc_exit.rs index 9932ecb5032..722e1bd7044 100644 --- a/lib/wasix/src/syscalls/wasi/proc_exit.rs +++ b/lib/wasix/src/syscalls/wasi/proc_exit.rs @@ -34,9 +34,9 @@ pub fn proc_exit( ); // Restore the WasiEnv to the point when we vforked - vfork.env.swap_inner(ctx.data_mut()); - std::mem::swap(vfork.env.as_mut(), ctx.data_mut()); - let mut child_env = *vfork.env; + let mut parent_env = vfork.env; + ctx.data_mut().swap_inner(parent_env.as_mut()); + let mut child_env = std::mem::replace(ctx.data_mut(), *parent_env); child_env.owned_handles.push(vfork.handle); // Terminate the child process diff --git a/lib/wasix/src/syscalls/wasix/proc_exec3.rs b/lib/wasix/src/syscalls/wasix/proc_exec3.rs index d26d732d005..d090e4b75ca 100644 --- a/lib/wasix/src/syscalls/wasix/proc_exec3.rs +++ b/lib/wasix/src/syscalls/wasix/proc_exec3.rs @@ -149,7 +149,7 @@ pub fn proc_exec3( wasi_env.owned_handles.push(vfork.handle.clone()); _prepare_wasi(&mut wasi_env, Some(args), envs, None); - // Recrod the stack offsets before we give up ownership of the wasi_env + // Record the stack offsets before we give up ownership of the wasi_env let stack_lower = wasi_env.layout.stack_lower; let stack_upper = wasi_env.layout.stack_upper; @@ -195,7 +195,7 @@ pub fn proc_exec3( match spawn_result { Err(e) => { - // We failed to spawn a new process - put the vfork back + // We failed to spawn a new process - put the child env back child_env.swap_inner(ctx.data_mut()); std::mem::swap(child_env.as_mut(), ctx.data_mut()); @@ -203,6 +203,12 @@ pub fn proc_exec3( return Ok(e); } Ok(()) => { + // We spawned a new process - put the parent env back + ctx.data_mut().swap_inner(&mut vfork.env); + std::mem::swap(ctx.data_mut(), &mut vfork.env); + + assert!(!vfork.env.context_switching_environment.is_some()); + assert!(ctx.data().context_switching_environment.is_some()); // Jump back to the vfork point and current on execution // note: fork does not return any values hence passing `()` let rewind_stack = vfork.rewind_stack.freeze(); diff --git a/lib/wasix/src/syscalls/wasix/proc_fork.rs b/lib/wasix/src/syscalls/wasix/proc_fork.rs index 076f263538a..0ade98ee873 100644 --- a/lib/wasix/src/syscalls/wasix/proc_fork.rs +++ b/lib/wasix/src/syscalls/wasix/proc_fork.rs @@ -32,10 +32,16 @@ pub fn proc_fork( Errno::Notsup })); - wasi_try_ok!(ctx.data().context_switching_environment.as_ref().ok_or_else(|| { - warn!("process forking is mutally exclusive with WASIX context-switching features. If you need both, please open an issue."); - Errno::Notsup - })); + if let Some(context_switching_environment) = ctx.data().context_switching_environment.as_ref() { + if context_switching_environment.active_context_id() + != context_switching_environment.main_context_id() + { + warn!( + "process forking is only supported from the main context when using WASIX context-switching features" + ); + return Ok(Errno::Notsup); + } + } // If we were just restored then we need to return the value instead if let Some(result) = unsafe { handle_rewind::(&mut ctx) } { From 51dfecf84f7a8f92eb6cbf92253b0e79b12d3eb9 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 9 Dec 2025 17:31:17 +0100 Subject: [PATCH 089/114] Improve handling of undefined behaviour when vfork traps --- lib/wasix/src/bin_factory/exec.rs | 8 ++++++ lib/wasix/src/state/context_switching.rs | 35 ++++++++++++------------ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 82d471eb8fa..53b743dd52e 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -442,6 +442,14 @@ fn resume_vfork( // Terminate the child process child_env.process.terminate(code); + // If the vfork contained a context-switching environment, exit now + if ctx.data(store).context_switching_environment.is_some() { + tracing::error!( + "Terminated a vfork in another way than exit or exec which is undefined behaviour. In this case the parent parent process will be terminated." + ); + return Err(code.into()); + } + // Jump back to the vfork point and current on execution let child_pid = child_env.process.pid(); let rewind_stack = vfork.rewind_stack.freeze(); diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 6674d35e0a6..35c5fcaa06e 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -124,20 +124,6 @@ impl ContextSwitchingEnvironment { // Add the context-switching environment to the WasiEnv let env = ctx.data_mut(&mut store); - if env.vfork.is_some() { - // This is enforced here and in proc_fork - tracing::error!( - "process forking is mutally exclusive with WASIX context-switching features. If you need both, please open an issue." - ); - return ( - store, - Err(RuntimeError::user( - // Exit with code 129 to indicate an internal error - WasiError::Exit(ExitCode::from(129)).into(), - )), - ); - } - let previous_environment = env.context_switching_environment.replace(this); if previous_environment.is_some() { panic!( @@ -171,9 +157,24 @@ impl ContextSwitchingEnvironment { // Remove the context-switching environment from the WasiEnv let env = ctx.data_mut(&mut store); - env.context_switching_environment.take().expect( - "Failed to remove wasix context-switching environment from WASIX env after main context finished, this should never happen", - ); + if env.context_switching_environment.take().is_none() { + if env + .vfork + .as_ref() + .and_then(|vfork| vfork.env.context_switching_environment.as_ref()) + .is_some() + { + // Grace for vforks, so they don't bring everything down with them. + // This is still an error. + tracing::error!( + "Failed to remove wasix context-switching environment from WASIX env after main context finished, this means you triggered undefined behaviour" + ); + } else { + panic!( + "Failed to remove wasix context-switching environment from WASIX env after main context finished, this should never happen" + ) + } + } (store, result) } From 4f306b48d432ea585ab6ca55c69050d989379a8a Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 9 Dec 2025 17:46:16 +0100 Subject: [PATCH 090/114] Change expected behaviour for a test to failing --- tests/wasix/vfork/run.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/wasix/vfork/run.sh b/tests/wasix/vfork/run.sh index 9ad149cc758..2bff8d12bbc 100755 --- a/tests/wasix/vfork/run.sh +++ b/tests/wasix/vfork/run.sh @@ -10,4 +10,5 @@ $WASMER_RUN main.wasm --dir . -- cloexec $WASMER_RUN main.wasm --dir . -- exiting_child $WASMER_RUN main.wasm --dir . -- trapping_child $WASMER_RUN main.wasm --dir . -- exit_before_exec -$WASMER_RUN main.wasm --dir . -- trap_before_exec +# This test is expected to fail with a non-zero exit code +! $WASMER_RUN main.wasm --dir . -- trap_before_exec \ No newline at end of file From 5748a248cb0356619f68682354375003325d4cee Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 9 Dec 2025 17:51:24 +0100 Subject: [PATCH 091/114] Fix clippy lints --- lib/wasix/src/syscalls/wasix/proc_exec3.rs | 2 +- lib/wasix/src/syscalls/wasix/proc_fork.rs | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/wasix/src/syscalls/wasix/proc_exec3.rs b/lib/wasix/src/syscalls/wasix/proc_exec3.rs index d090e4b75ca..cf7a4bae840 100644 --- a/lib/wasix/src/syscalls/wasix/proc_exec3.rs +++ b/lib/wasix/src/syscalls/wasix/proc_exec3.rs @@ -207,7 +207,7 @@ pub fn proc_exec3( ctx.data_mut().swap_inner(&mut vfork.env); std::mem::swap(ctx.data_mut(), &mut vfork.env); - assert!(!vfork.env.context_switching_environment.is_some()); + assert!(vfork.env.context_switching_environment.is_none()); assert!(ctx.data().context_switching_environment.is_some()); // Jump back to the vfork point and current on execution // note: fork does not return any values hence passing `()` diff --git a/lib/wasix/src/syscalls/wasix/proc_fork.rs b/lib/wasix/src/syscalls/wasix/proc_fork.rs index 0ade98ee873..0dd25b64846 100644 --- a/lib/wasix/src/syscalls/wasix/proc_fork.rs +++ b/lib/wasix/src/syscalls/wasix/proc_fork.rs @@ -32,15 +32,14 @@ pub fn proc_fork( Errno::Notsup })); - if let Some(context_switching_environment) = ctx.data().context_switching_environment.as_ref() { - if context_switching_environment.active_context_id() + if let Some(context_switching_environment) = ctx.data().context_switching_environment.as_ref() + && context_switching_environment.active_context_id() != context_switching_environment.main_context_id() - { - warn!( - "process forking is only supported from the main context when using WASIX context-switching features" - ); - return Ok(Errno::Notsup); - } + { + warn!( + "process forking is only supported from the main context when using WASIX context-switching features" + ); + return Ok(Errno::Notsup); } // If we were just restored then we need to return the value instead From 6f4ff406ca0288c6b7f7c5fb8328c21731bf90e3 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Tue, 9 Dec 2025 19:09:01 +0100 Subject: [PATCH 092/114] Disable context switching tests until wasixcc supports it and is available in tests --- lib/wasix/tests/context_switching.rs | 74 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index 3ac7c7f5dca..5699dae1b93 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -41,223 +41,223 @@ fn test_with_wasixcc(name: &str) -> Result<(), anyhow::Error> { ) } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_simple_switching() { test_with_wasixcc("simple_switching").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_switching_with_main() { test_with_wasixcc("switching_with_main").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_switching_to_a_deleted_context() { test_with_wasixcc("switching_to_a_deleted_context").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_switching_threads() { test_with_wasixcc("switching_in_threads").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_multiple_contexts() { test_with_wasixcc("multiple_contexts").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_error_handling() { test_with_wasixcc("error_handling").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_nested_switches() { test_with_wasixcc("nested_switches").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_state_preservation() { test_with_wasixcc("state_preservation").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_main_context_id() { test_with_wasixcc("main_context_id").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_self_switching() { test_with_wasixcc("self_switching").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_cleanup_order() { test_with_wasixcc("cleanup_order").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_deep_recursion() { test_with_wasixcc("deep_recursion").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_rapid_switching() { test_with_wasixcc("rapid_switching").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_heap_allocations() { test_with_wasixcc("heap_allocations").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_many_contexts() { test_with_wasixcc("many_contexts").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_file_io_switching() { test_with_wasixcc("file_io_switching").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_contexts_with_mutexes() { test_with_wasixcc("contexts_with_mutexes").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_contexts_with_env_vars() { test_with_wasixcc("contexts_with_env_vars").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_contexts_with_getcwd() { test_with_wasixcc("contexts_with_getcwd").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_contexts_with_signals() { test_with_wasixcc("contexts_with_signals").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_contexts_with_timers() { test_with_wasixcc("contexts_with_timers").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_contexts_with_pipes() { test_with_wasixcc("contexts_with_pipes").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_complex_nested_operations() { test_with_wasixcc("complex_nested_operations").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_pending_file_operations() { test_with_wasixcc("pending_file_operations").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_recursive_host_calls() { test_with_wasixcc("recursive_host_calls").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_malloc_during_switch() { test_with_wasixcc("malloc_during_switch").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_nested_host_call_switch() { test_with_wasixcc("nested_host_call_switch").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_active_context_id() { test_with_wasixcc("active_context_id").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_wrong_entrypoint() { test_with_wasixcc("wrong_entrypoint").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_switch_to_never_resumed() { test_with_wasixcc("switch_to_never_resumed").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_function_args_preserved() { test_with_wasixcc("function_args_preserved").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_correct_context_activated() { test_with_wasixcc("correct_context_activated").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_switch_from_recursion() { test_with_wasixcc("switch_from_recursion").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_global_ctx_ids() { test_with_wasixcc("global_ctx_ids").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_shared_recursion() { test_with_wasixcc("shared_recursion").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_mutual_recursion() { test_with_wasixcc("mutual_recursion").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(any())] #[test] fn test_three_way_recursion() { test_with_wasixcc("three_way_recursion").unwrap(); From d62fa0abebe4cfb9eb67ddaa1e694b3a73e73fa4 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Wed, 10 Dec 2025 15:36:52 +0100 Subject: [PATCH 093/114] Add context switching tests to the WASIX tests --- tests/wasix/context-switching/main.c | 354 +++++++++++++++++++++++++++ tests/wasix/context-switching/run.sh | 8 + 2 files changed, 362 insertions(+) create mode 100644 tests/wasix/context-switching/main.c create mode 100755 tests/wasix/context-switching/run.sh diff --git a/tests/wasix/context-switching/main.c b/tests/wasix/context-switching/main.c new file mode 100644 index 00000000000..2efe10369d3 --- /dev/null +++ b/tests/wasix/context-switching/main.c @@ -0,0 +1,354 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +wasix_context_id_t ctx1; +int was_in_two = 0; + +void context2_fn(void) { + was_in_two++; + wasix_context_switch(ctx1); + assert(0 && "Should not return to context 2"); +} + +void context1_fn(void) { + wasix_context_id_t ctx2; + int ret = wasix_context_create(&ctx2, context2_fn); + assert(ret == 0 && "Failed to create context 2"); + wasix_context_switch(ctx2); + wasix_context_destroy(ctx2); + wasix_context_switch(wasix_context_main); +} + +int was_in_context_fn_switch_to_main = 0; +void context_fn_switch_to_main(void) { + was_in_context_fn_switch_to_main = 1; + wasix_context_switch(wasix_context_main); + assert(0); +} + +// Test a simple context switching scenario +int test_basic_switching() { + // Create three contexts + int ret = wasix_context_create(&ctx1, context1_fn); + assert(ret == 0 && "Failed to create context 1"); + + wasix_context_switch(ctx1); + + assert(was_in_two == 1 && "Context 2 was not executed exactly once"); + + return 0; +} + + + +int vfork_exec() +{ + int pid = vfork(); + + if (pid == 0) + { + execl("./main.wasm", "main.wasm", "subprocess", NULL); + perror("execl"); + exit(10); + } + else + { + int status; + waitpid(pid, &status, 0); + if (WEXITSTATUS(status) != 20) + { + printf("Expected exit code 20 from subprocess, got %d\n", WEXITSTATUS(status)); + return 1; + } + + return 0; + } +} + +// Test that vfork works even after other contexts are active +int vfork_after_switching() +{ + wasix_context_id_t ctx; + int ret = wasix_context_create(&ctx, context_fn_switch_to_main); + assert(ret == 0 && "Failed to create context"); + wasix_context_switch(ctx); + + int pid = vfork(); + + if (pid == 0) + { + execl("./main.wasm", "main.wasm", "subprocess", NULL); + perror("execl"); + exit(10); + } + else + { + int status; + waitpid(pid, &status, 0); + if (WEXITSTATUS(status) != 20) + { + printf("Expected exit code 20 from subprocess, got %d\n", WEXITSTATUS(status)); + return 1; + } + + return 0; + } +} + +// Test that vfork works even after other contexts are active +int vfork_after_switching2() +{ + // Create two contexts. One is to verify switching works now and the other to verify it works later on. + wasix_context_id_t ctx_a, ctx_b; + int ret = wasix_context_create(&ctx_a, context_fn_switch_to_main); + assert(ret == 0 && "Failed to create context"); + ret = wasix_context_create(&ctx_b, context_fn_switch_to_main); + assert(ret == 0 && "Failed to create context"); + + // Verify that switching works before vfork + was_in_context_fn_switch_to_main = 0; + wasix_context_switch(ctx_a); + assert(was_in_context_fn_switch_to_main == 1 && "Context function was not executed"); + + int pid = vfork(); + + if (pid == 0) + { + // The process created by vfork should not have a context switching environment + // so we get a ENOSYS here + int ret = wasix_context_switch(ctx_b); + if (ret != -1) { + exit(11); + } + if (errno != ENOTSUP) { + exit(12); + } + + execl("./main.wasm", "main.wasm", "subprocess_with_switching", NULL); + perror("execl"); + exit(10); + } + else + { + // The parent should still be in the same context switching environment + was_in_context_fn_switch_to_main = 0; + wasix_context_switch(ctx_b); + assert(was_in_context_fn_switch_to_main == 1 && "Context function was not executed"); + + int status; + waitpid(pid, &status, 0); + if (WEXITSTATUS(status) != 20) + { + printf("Expected exit code 20 from subprocess, got %d\n", WEXITSTATUS(status)); + return 1; + } + + return 0; + } +} + +// Test that vfork works even after other contexts are active +int fork_after_switching() +{ + // Create two contexts. One is to verify switching works now and the other to verify it works later on. + wasix_context_id_t ctx_a, ctx_b; + int ret = wasix_context_create(&ctx_a, context_fn_switch_to_main); + assert(ret == 0 && "Failed to create context"); + ret = wasix_context_create(&ctx_b, context_fn_switch_to_main); + assert(ret == 0 && "Failed to create context"); + + // Verify that switching works before vfork + was_in_context_fn_switch_to_main = 0; + wasix_context_switch(ctx_a); + assert(was_in_context_fn_switch_to_main == 1 && "Context function was not executed"); + + int pid = fork(); + + if (pid == 0) + { + // The process created by fork should have a new context switching environment + // so we get a EINVAL here because the context does not exist in this new environment + int ret = wasix_context_switch(ctx_b); + if (ret != -1) { + exit(11); + } + if (errno != EINVAL) { + exit(12); + } + // Recreate the context in the child process + ret = wasix_context_create(&ctx_b, context_fn_switch_to_main); + assert(ret == 0 && "Failed to create context"); + + // Now we can switch to it. Switching back to main should work too. + was_in_context_fn_switch_to_main = 0; + wasix_context_switch(ctx_b); + assert(was_in_context_fn_switch_to_main == 1 && "Context function was not executed"); + + // exec should always bring us in a new context switching environment + execl("./main.wasm", "main.wasm", "subprocess_with_switching", NULL); + perror("execl"); + exit(10); + } + else + { + // The parent should still be in the same context switching environment + was_in_context_fn_switch_to_main = 0; + wasix_context_switch(ctx_b); + assert(was_in_context_fn_switch_to_main == 1 && "Context function was not executed"); + + int status; + waitpid(pid, &status, 0); + if (WEXITSTATUS(status) != 20) + { + printf("Expected exit code 20 from subprocess, got %d\n", WEXITSTATUS(status)); + return 1; + } + + return 0; + } +} + +void context_fn_that_executes_fork_and_vfork() +{ + int pid = fork(); + assert(pid == -1 && "fork should fail in a context"); + assert(errno == ENOTSUP && "fork should return ENOTSUP in a context"); + pid = vfork(); + assert(pid == -1 && "vfork should fail in a context"); + assert(errno == ENOTSUP && "vfork should return ENOTSUP in a context"); + wasix_context_switch(wasix_context_main); + assert(0 && "Should not return to this context"); +} + +int fork_and_vfork_only_work_in_main_context() +{ + wasix_context_id_t ctx; + int ret = wasix_context_create(&ctx, context_fn_that_executes_fork_and_vfork); + assert(ret == 0 && "Failed to create context"); + ret = wasix_context_switch(ctx); + assert(ret == 0 && "Failed to switch to context"); + return 0; +} + +extern char **environ; +void context_fn_posix_spawn_a_forking_subprocess_from_a_context() +{ + int pid; + int status; + int exit_code; + + // Test a subprocess that does context switching + char *argV[] = {"./main.wasm", "subprocess_with_switching",(char *) 0}; + posix_spawn(&pid, "./main.wasm", NULL, NULL, argV, environ); + waitpid(pid, &status, 0); + exit_code = WEXITSTATUS(status); + assert(exit_code == 20 && "Expected exit code 20 from subprocess"); + + // Test a subprocess that does fork and vfork + char *argV2[] = {"./main.wasm", "subprocess_with_fork_and_vfork",(char *) 0}; + posix_spawn(&pid, "./main.wasm", NULL, NULL, argV2, environ); + waitpid(pid, &status, 0); + exit_code = WEXITSTATUS(status); + assert(exit_code == 20 && "Expected exit code 20 from subprocess"); + + wasix_context_switch(wasix_context_main); + assert(0 && "Should not return to this context"); +} + +int posix_spawning_a_forking_subprocess_from_a_context() +{ + wasix_context_id_t ctx; + int ret = wasix_context_create(&ctx, context_fn_posix_spawn_a_forking_subprocess_from_a_context); + assert(ret == 0 && "Failed to create context"); + ret = wasix_context_switch(ctx); + assert(ret == 0 && "Failed to switch to context"); + return 0; +} + +int subprocess() +{ + return 20; +} + +// Test a simple context switching scenario +int subprocess_with_switching() { + test_basic_switching(); + + return 20; +} + +// Test a simple context switching scenario +int subprocess_with_fork_and_vfork() { + vfork_after_switching2(); + + fork_after_switching(); + + return 20; +} + +int main(int argc, char **argv) +{ + if (argc < 2) + { + return -1; + } + + + if (!strcmp(argv[1], "subprocess")) + { + return subprocess(); + } + else if (!strcmp(argv[1], "subprocess_with_switching")) + { + return subprocess_with_switching(); + } + else if (!strcmp(argv[1], "subprocess_with_fork_and_vfork")) + { + return subprocess_with_fork_and_vfork(); + } + else if (!strcmp(argv[1], "basic_switching")) + { + return test_basic_switching(); + } + else if (!strcmp(argv[1], "vfork_after_switching")) + { + return vfork_after_switching(); + } + else if (!strcmp(argv[1], "vfork_after_switching2")) + { + return vfork_after_switching2(); + } + else if (!strcmp(argv[1], "fork_after_switching")) + { + return fork_after_switching(); + } + else if (!strcmp(argv[1], "fork_and_vfork_only_work_in_main_context")) + { + return fork_and_vfork_only_work_in_main_context(); + } + else if (!strcmp(argv[1], "posix_spawning_a_forking_subprocess_from_a_context")) + { + return posix_spawning_a_forking_subprocess_from_a_context(); + } + else if (!strcmp(argv[1], "fork_and_vfork_only_work_in_main_context2")) + { + return fork_and_vfork_only_work_in_main_context(); + } + else if (!strcmp(argv[1], "fork_and_vfork_only_work_in_main_context2")) + { + return fork_and_vfork_only_work_in_main_context(); + } + else + { + printf("bad command %s\n", argv[1]); + return 1; + } +} diff --git a/tests/wasix/context-switching/run.sh b/tests/wasix/context-switching/run.sh new file mode 100755 index 00000000000..e94a7ec901f --- /dev/null +++ b/tests/wasix/context-switching/run.sh @@ -0,0 +1,8 @@ +set -e + +$WASMER -q run main.wasm --dir . -- basic_switching +$WASMER -q run main.wasm --dir . -- vfork_after_switching +$WASMER -q run main.wasm --dir . -- vfork_after_switching2 +$WASMER -q run main.wasm --dir . -- fork_after_switching +$WASMER -q run main.wasm --dir . -- fork_and_vfork_only_work_in_main_context +$WASMER -q run main.wasm --dir . -- posix_spawning_a_forking_subprocess_from_a_context \ No newline at end of file From 6da0f7f3e26f2ca42d286334605eac72a2e6aeea Mon Sep 17 00:00:00 2001 From: Zebreus Date: Wed, 10 Dec 2025 16:32:45 +0100 Subject: [PATCH 094/114] Update WASIX sysroot release --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 61a68b46266..32cfc88ae08 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -200,7 +200,7 @@ jobs: echo "$(pwd)/wasixcc-install" >> $GITHUB_PATH mkdir -p ~/.wasixcc/llvm mkdir -p ~/.wasixcc/sysroot - wasixcc --download-sysroot v2025-11-06.1 + wasixcc --download-sysroot v2025-12-10.1 wasixcc --download-llvm - name: Tool versions From 051f762c11ab7bf3b4c29386b26627e18bde0c6a Mon Sep 17 00:00:00 2001 From: Zebreus Date: Wed, 10 Dec 2025 17:33:25 +0100 Subject: [PATCH 095/114] Allow undefined behaviour test to pass --- tests/wasix/vfork/run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/wasix/vfork/run.sh b/tests/wasix/vfork/run.sh index 2bff8d12bbc..c939ed03f4b 100755 --- a/tests/wasix/vfork/run.sh +++ b/tests/wasix/vfork/run.sh @@ -10,5 +10,5 @@ $WASMER_RUN main.wasm --dir . -- cloexec $WASMER_RUN main.wasm --dir . -- exiting_child $WASMER_RUN main.wasm --dir . -- trapping_child $WASMER_RUN main.wasm --dir . -- exit_before_exec -# This test is expected to fail with a non-zero exit code -! $WASMER_RUN main.wasm --dir . -- trap_before_exec \ No newline at end of file +# This test is triggering undefined behaviour +$WASMER_RUN main.wasm --dir . -- trap_before_exec || true From cef1e670da020c2dc543812fd276e986904794e0 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Wed, 10 Dec 2025 17:11:13 +0100 Subject: [PATCH 096/114] Make preconditions for run_main_context more strict --- lib/wasix/src/state/context_switching.rs | 58 +++++++++++++----------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 35c5fcaa06e..34e5c615437 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -109,6 +109,17 @@ impl ContextSwitchingEnvironment { entrypoint: wasmer::Function, params: Vec, ) -> (Store, Result, RuntimeError>) { + // If we are already in a context-switching environment, something went wrong + if ctx + .data_mut(&mut store) + .context_switching_environment + .is_some() + { + panic!( + "Failed to start a WASIX main context as there was already a context-switching environment present." + ); + } + // Do a normal call and dont install the context switching env, if the engine does not support async let engine_supports_async = store.engine().supports_async(); if !engine_supports_async { @@ -122,14 +133,11 @@ impl ContextSwitchingEnvironment { let this = Self::new(local_executor.spawner()); // Add the context-switching environment to the WasiEnv - let env = ctx.data_mut(&mut store); - - let previous_environment = env.context_switching_environment.replace(this); - if previous_environment.is_some() { - panic!( - "Failed to start a wasix main context as there was already a context-switching environment present." - ); - } + let previous = ctx + .data_mut(&mut store) + .context_switching_environment + .replace(this); + assert!(previous.is_none()); // Should never be hit because of the check at the top // Turn the store into an async store and run the entrypoint let store_async = store.into_async(); @@ -150,6 +158,7 @@ impl ContextSwitchingEnvironment { }, _ => result, }; + tracing::trace!("Main context finished execution and returned {result:?}"); // Drop the executor to ensure all references to the StoreAsync are gone and convert back to a normal store drop(local_executor); @@ -157,24 +166,21 @@ impl ContextSwitchingEnvironment { // Remove the context-switching environment from the WasiEnv let env = ctx.data_mut(&mut store); - if env.context_switching_environment.take().is_none() { - if env - .vfork - .as_ref() - .and_then(|vfork| vfork.env.context_switching_environment.as_ref()) - .is_some() - { - // Grace for vforks, so they don't bring everything down with them. - // This is still an error. - tracing::error!( - "Failed to remove wasix context-switching environment from WASIX env after main context finished, this means you triggered undefined behaviour" - ); - } else { - panic!( - "Failed to remove wasix context-switching environment from WASIX env after main context finished, this should never happen" - ) - } - } + + env.context_switching_environment + .take() + .or_else(|| { + env.vfork + .as_mut() + .and_then(|vfork| vfork.env.context_switching_environment.take()) + .inspect(|_| { + // Grace for vforks, so they don't bring everything down with them. + // This is still an error. + // The message below is oversimplified there is more nuance to this. + tracing::error!("Exiting a vforked process in any other way than calling `_exit()` is undefined behavior but the current program just did that."); + }) + }) + .expect("Failed to remove wasix context-switching environment from WASIX env after main context finished. This means we lost it somehow which should never happen."); (store, result) } From e8f7562b0434d3cd2fb072b7b2814941e765b8eb Mon Sep 17 00:00:00 2001 From: Zebreus Date: Thu, 11 Dec 2025 13:54:30 +0100 Subject: [PATCH 097/114] Add wasixcc install to unit tests --- .github/workflows/test.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 32cfc88ae08..57afb4cdf43 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -896,6 +896,23 @@ jobs: run: | brew install llvm echo "/opt/homebrew/opt/llvm/bin" >> GITHUB_PATH + + - name: Install wasixcc + if: matrix.metadata.build == 'linux-x64' + run: | + export RUST_LOG=wasixcc=trace + export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + cargo install --git https://github.com/wasix-org/wasixcc.git --rev d9ab260eb557ce90406ccf07ac52ac5e211caf60 -F bin + mkdir wasixcc-install + wasixcc --install-executables ./wasixcc-install + echo "$(pwd)/wasixcc-install" >> $GITHUB_PATH + mkdir -p ~/.wasixcc/llvm + mkdir -p ~/.wasixcc/sysroot + mkdir -p ~/.wasixcc/binaryen + wasixcc --download-sysroot v2025-12-10.1 + wasixcc --download-llvm + wasixcc --download-binaryen + - name: Install LLVM shell: bash if: matrix.metadata.llvm_url From 64ae9a339393a79a5c7196a1c3a8251be6ce384d Mon Sep 17 00:00:00 2001 From: Zebreus Date: Thu, 11 Dec 2025 13:54:43 +0100 Subject: [PATCH 098/114] Reenable wasixcc based unit tests --- lib/wasix/tests/context_switching.rs | 74 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index 5699dae1b93..3ac7c7f5dca 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -41,223 +41,223 @@ fn test_with_wasixcc(name: &str) -> Result<(), anyhow::Error> { ) } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_simple_switching() { test_with_wasixcc("simple_switching").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_switching_with_main() { test_with_wasixcc("switching_with_main").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_switching_to_a_deleted_context() { test_with_wasixcc("switching_to_a_deleted_context").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_switching_threads() { test_with_wasixcc("switching_in_threads").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_multiple_contexts() { test_with_wasixcc("multiple_contexts").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_error_handling() { test_with_wasixcc("error_handling").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_nested_switches() { test_with_wasixcc("nested_switches").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_state_preservation() { test_with_wasixcc("state_preservation").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_main_context_id() { test_with_wasixcc("main_context_id").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_self_switching() { test_with_wasixcc("self_switching").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_cleanup_order() { test_with_wasixcc("cleanup_order").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_deep_recursion() { test_with_wasixcc("deep_recursion").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_rapid_switching() { test_with_wasixcc("rapid_switching").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_heap_allocations() { test_with_wasixcc("heap_allocations").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_many_contexts() { test_with_wasixcc("many_contexts").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_file_io_switching() { test_with_wasixcc("file_io_switching").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_contexts_with_mutexes() { test_with_wasixcc("contexts_with_mutexes").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_contexts_with_env_vars() { test_with_wasixcc("contexts_with_env_vars").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_contexts_with_getcwd() { test_with_wasixcc("contexts_with_getcwd").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_contexts_with_signals() { test_with_wasixcc("contexts_with_signals").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_contexts_with_timers() { test_with_wasixcc("contexts_with_timers").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_contexts_with_pipes() { test_with_wasixcc("contexts_with_pipes").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_complex_nested_operations() { test_with_wasixcc("complex_nested_operations").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_pending_file_operations() { test_with_wasixcc("pending_file_operations").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_recursive_host_calls() { test_with_wasixcc("recursive_host_calls").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_malloc_during_switch() { test_with_wasixcc("malloc_during_switch").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_nested_host_call_switch() { test_with_wasixcc("nested_host_call_switch").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_active_context_id() { test_with_wasixcc("active_context_id").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_wrong_entrypoint() { test_with_wasixcc("wrong_entrypoint").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_switch_to_never_resumed() { test_with_wasixcc("switch_to_never_resumed").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_function_args_preserved() { test_with_wasixcc("function_args_preserved").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_correct_context_activated() { test_with_wasixcc("correct_context_activated").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_switch_from_recursion() { test_with_wasixcc("switch_from_recursion").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_global_ctx_ids() { test_with_wasixcc("global_ctx_ids").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_shared_recursion() { test_with_wasixcc("shared_recursion").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_mutual_recursion() { test_with_wasixcc("mutual_recursion").unwrap(); } -#[cfg(any())] +#[cfg(target_os = "linux")] #[test] fn test_three_way_recursion() { test_with_wasixcc("three_way_recursion").unwrap(); From 6f904beec6f7fc22106fec0f8bfc66e466e2449e Mon Sep 17 00:00:00 2001 From: Zebreus Date: Thu, 11 Dec 2025 15:47:47 +0100 Subject: [PATCH 099/114] Try wasixcc install on musl linux --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 57afb4cdf43..19f71ae924c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -898,7 +898,7 @@ jobs: echo "/opt/homebrew/opt/llvm/bin" >> GITHUB_PATH - name: Install wasixcc - if: matrix.metadata.build == 'linux-x64' + if: matrix.metadata.build == 'linux-x64' || matrix.metadata.build == 'linux-musl' run: | export RUST_LOG=wasixcc=trace export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} From 12a9e404e3f23349a58ca56384c956a58354d9c5 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 00:53:16 +0100 Subject: [PATCH 100/114] Remove duplicate tests --- lib/wasix/tests/context_switching.rs | 116 +----------------- .../context_switching/active_context_id.c | 70 ----------- .../tests/context_switching/cleanup_order.c | 82 ------------- .../complex_nested_operations.c | 77 ------------ .../context_switching/contexts_with_getcwd.c | 89 -------------- .../correct_context_activated.c | 63 ---------- .../tests/context_switching/deep_recursion.c | 93 -------------- .../context_switching/file_io_switching.c | 99 --------------- .../function_args_preserved.c | 71 ----------- .../tests/context_switching/global_ctx_ids.c | 59 --------- .../context_switching/heap_allocations.c | 71 ----------- .../tests/context_switching/main_context_id.c | 55 --------- .../tests/context_switching/many_contexts.c | 62 ---------- .../context_switching/mutual_recursion.c | 60 --------- .../tests/context_switching/rapid_switching.c | 53 -------- .../tests/context_switching/self_switching.c | 55 --------- .../context_switching/shared_recursion.c | 59 --------- .../context_switching/state_preservation.c | 82 ------------- .../context_switching/switch_from_recursion.c | 69 ----------- .../context_switching/wrong_entrypoint.c | 54 -------- 20 files changed, 4 insertions(+), 1435 deletions(-) delete mode 100644 lib/wasix/tests/context_switching/active_context_id.c delete mode 100644 lib/wasix/tests/context_switching/cleanup_order.c delete mode 100644 lib/wasix/tests/context_switching/complex_nested_operations.c delete mode 100644 lib/wasix/tests/context_switching/contexts_with_getcwd.c delete mode 100644 lib/wasix/tests/context_switching/correct_context_activated.c delete mode 100644 lib/wasix/tests/context_switching/deep_recursion.c delete mode 100644 lib/wasix/tests/context_switching/file_io_switching.c delete mode 100644 lib/wasix/tests/context_switching/function_args_preserved.c delete mode 100644 lib/wasix/tests/context_switching/global_ctx_ids.c delete mode 100644 lib/wasix/tests/context_switching/heap_allocations.c delete mode 100644 lib/wasix/tests/context_switching/main_context_id.c delete mode 100644 lib/wasix/tests/context_switching/many_contexts.c delete mode 100644 lib/wasix/tests/context_switching/mutual_recursion.c delete mode 100644 lib/wasix/tests/context_switching/rapid_switching.c delete mode 100644 lib/wasix/tests/context_switching/self_switching.c delete mode 100644 lib/wasix/tests/context_switching/shared_recursion.c delete mode 100644 lib/wasix/tests/context_switching/state_preservation.c delete mode 100644 lib/wasix/tests/context_switching/switch_from_recursion.c delete mode 100644 lib/wasix/tests/context_switching/wrong_entrypoint.c diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index 3ac7c7f5dca..67bce05f930 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -83,60 +83,6 @@ fn test_nested_switches() { test_with_wasixcc("nested_switches").unwrap(); } -#[cfg(target_os = "linux")] -#[test] -fn test_state_preservation() { - test_with_wasixcc("state_preservation").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_main_context_id() { - test_with_wasixcc("main_context_id").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_self_switching() { - test_with_wasixcc("self_switching").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_cleanup_order() { - test_with_wasixcc("cleanup_order").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_deep_recursion() { - test_with_wasixcc("deep_recursion").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_rapid_switching() { - test_with_wasixcc("rapid_switching").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_heap_allocations() { - test_with_wasixcc("heap_allocations").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_many_contexts() { - test_with_wasixcc("many_contexts").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_file_io_switching() { - test_with_wasixcc("file_io_switching").unwrap(); -} - #[cfg(target_os = "linux")] #[test] fn test_contexts_with_mutexes() { @@ -149,12 +95,6 @@ fn test_contexts_with_env_vars() { test_with_wasixcc("contexts_with_env_vars").unwrap(); } -#[cfg(target_os = "linux")] -#[test] -fn test_contexts_with_getcwd() { - test_with_wasixcc("contexts_with_getcwd").unwrap(); -} - #[cfg(target_os = "linux")] #[test] fn test_contexts_with_signals() { @@ -173,12 +113,6 @@ fn test_contexts_with_pipes() { test_with_wasixcc("contexts_with_pipes").unwrap(); } -#[cfg(target_os = "linux")] -#[test] -fn test_complex_nested_operations() { - test_with_wasixcc("complex_nested_operations").unwrap(); -} - #[cfg(target_os = "linux")] #[test] fn test_pending_file_operations() { @@ -203,18 +137,6 @@ fn test_nested_host_call_switch() { test_with_wasixcc("nested_host_call_switch").unwrap(); } -#[cfg(target_os = "linux")] -#[test] -fn test_active_context_id() { - test_with_wasixcc("active_context_id").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_wrong_entrypoint() { - test_with_wasixcc("wrong_entrypoint").unwrap(); -} - #[cfg(target_os = "linux")] #[test] fn test_switch_to_never_resumed() { @@ -223,42 +145,12 @@ fn test_switch_to_never_resumed() { #[cfg(target_os = "linux")] #[test] -fn test_function_args_preserved() { - test_with_wasixcc("function_args_preserved").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_correct_context_activated() { - test_with_wasixcc("correct_context_activated").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_switch_from_recursion() { - test_with_wasixcc("switch_from_recursion").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_global_ctx_ids() { - test_with_wasixcc("global_ctx_ids").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_shared_recursion() { - test_with_wasixcc("shared_recursion").unwrap(); -} - -#[cfg(target_os = "linux")] -#[test] -fn test_mutual_recursion() { - test_with_wasixcc("mutual_recursion").unwrap(); +fn test_three_way_recursion() { + test_with_wasixcc("three_way_recursion").unwrap(); } #[cfg(target_os = "linux")] #[test] -fn test_three_way_recursion() { - test_with_wasixcc("three_way_recursion").unwrap(); +fn test_contexts_with_setjmp() { + test_with_wasixcc("contexts_with_setjmp").unwrap(); } diff --git a/lib/wasix/tests/context_switching/active_context_id.c b/lib/wasix/tests/context_switching/active_context_id.c deleted file mode 100644 index 5c5d7d53663..00000000000 --- a/lib/wasix/tests/context_switching/active_context_id.c +++ /dev/null @@ -1,70 +0,0 @@ -// Test that wasix_context_main always returns the main context ID -// regardless of which context is currently active -#include -#include -#include - -wasix_context_id_t ctx1, ctx2; -wasix_context_id_t main_ctx_id; -int phase = 0; - -void context1_fn(void) { - phase = 1; - - // wasix_context_main should always return the main context's ID, - // even when running in ctx1 - wasix_context_id_t main_id = wasix_context_main; - fprintf(stderr, - "Phase 1: wasix_context_main in ctx1 = %llu (expected %llu)\n", - (unsigned long long)main_id, (unsigned long long)main_ctx_id); - - // This assertion verifies that wasix_context_main returns the main context - assert(main_id == main_ctx_id && - "wasix_context_main should return main context ID even in ctx1"); - - wasix_context_switch(ctx2); - - phase = 3; - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - phase = 2; - - // wasix_context_main should always return the main context's ID, - // even when running in ctx2 - wasix_context_id_t main_id = wasix_context_main; - fprintf(stderr, - "Phase 2: wasix_context_main in ctx2 = %llu (expected %llu)\n", - (unsigned long long)main_id, (unsigned long long)main_ctx_id); - - // This assertion verifies that wasix_context_main returns the main context - assert(main_id == main_ctx_id && - "wasix_context_main should return main context ID even in ctx2"); - - wasix_context_switch(ctx1); -} - -int main() { - int ret; - - // Store the main context ID for comparison in other contexts - main_ctx_id = wasix_context_main; - fprintf(stderr, "Main context ID = %llu\n", (unsigned long long)main_ctx_id); - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0 && "Failed to create context 2"); - - // Start execution - wasix_context_switch(ctx1); - - // Cleanup - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "wasix_context_main test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/cleanup_order.c b/lib/wasix/tests/context_switching/cleanup_order.c deleted file mode 100644 index 60510848e16..00000000000 --- a/lib/wasix/tests/context_switching/cleanup_order.c +++ /dev/null @@ -1,82 +0,0 @@ -#include -#include -#include -#include - -// Test creating and destroying multiple contexts in various orders - -#define NUM_CONTEXTS 5 - -wasix_context_id_t contexts[NUM_CONTEXTS]; -int execution_flags[NUM_CONTEXTS] = {0}; - -void context_fn_0(void) { - execution_flags[0] = 1; - wasix_context_switch(wasix_context_main); -} - -void context_fn_1(void) { - execution_flags[1] = 1; - wasix_context_switch(wasix_context_main); -} - -void context_fn_2(void) { - execution_flags[2] = 1; - wasix_context_switch(wasix_context_main); -} - -void context_fn_3(void) { - execution_flags[3] = 1; - wasix_context_switch(wasix_context_main); -} - -void context_fn_4(void) { - execution_flags[4] = 1; - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - void (*entrypoints[])(void) = {context_fn_0, context_fn_1, context_fn_2, - context_fn_3, context_fn_4}; - - // Create all contexts - for (int i = 0; i < NUM_CONTEXTS; i++) { - ret = wasix_context_create(&contexts[i], entrypoints[i]); - assert(ret == 0 && "Failed to create context"); - } - - // Execute some contexts - wasix_context_switch(contexts[0]); - assert(execution_flags[0] == 1 && "Context 0 should have executed"); - - wasix_context_switch(contexts[2]); - assert(execution_flags[2] == 1 && "Context 2 should have executed"); - - wasix_context_switch(contexts[4]); - assert(execution_flags[4] == 1 && "Context 4 should have executed"); - - // Destroy contexts in non-creation order - ret = wasix_context_destroy(contexts[2]); - assert(ret == 0 && "Failed to destroy context 2"); - - ret = wasix_context_destroy(contexts[0]); - assert(ret == 0 && "Failed to destroy context 0"); - - ret = wasix_context_destroy(contexts[4]); - assert(ret == 0 && "Failed to destroy context 4"); - - // Execute a context that wasn't executed before, then destroy it - wasix_context_switch(contexts[1]); - assert(execution_flags[1] == 1 && "Context 1 should have executed"); - - ret = wasix_context_destroy(contexts[1]); - assert(ret == 0 && "Failed to destroy context 1"); - - // Destroy the remaining context without ever executing it - ret = wasix_context_destroy(contexts[3]); - assert(ret == 0 && "Failed to destroy unexecuted context 3"); - - fprintf(stderr, "Context cleanup test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/complex_nested_operations.c b/lib/wasix/tests/context_switching/complex_nested_operations.c deleted file mode 100644 index 662ce897a2c..00000000000 --- a/lib/wasix/tests/context_switching/complex_nested_operations.c +++ /dev/null @@ -1,77 +0,0 @@ -#include -#include -#include -#include -#include - -// Test context switching with complex nested operations that might leave -// internal state borrowed - -#define NUM_CONTEXTS 5 - -wasix_context_id_t contexts[NUM_CONTEXTS]; -int execution_count = 0; - -// Recursive function that does complex operations and switches contexts -void recursive_switch(int depth, int max_depth, wasix_context_id_t next_ctx) { - char buffer[1024]; - - if (depth >= max_depth) { - execution_count++; - // Switch to next context at maximum depth - wasix_context_switch(next_ctx); - return; - } - - // Do some complex string operations - snprintf(buffer, sizeof(buffer), "Depth %d recursion", depth); - - // Allocate and free memory - void *ptr = malloc(1024); - memset(ptr, depth, 1024); - free(ptr); - - // Recurse - recursive_switch(depth + 1, max_depth, next_ctx); - - // After recursion, do more operations - snprintf(buffer, sizeof(buffer), "Returning from depth %d", depth); -} - -void context0_fn(void) { recursive_switch(0, 10, contexts[1]); } - -void context1_fn(void) { recursive_switch(0, 10, contexts[2]); } - -void context2_fn(void) { recursive_switch(0, 10, contexts[3]); } - -void context3_fn(void) { recursive_switch(0, 10, contexts[4]); } - -void context4_fn(void) { recursive_switch(0, 10, wasix_context_main); } - -int main() { - int ret; - - // Create contexts with entrypoints - void (*entrypoints[])(void) = {context0_fn, context1_fn, context2_fn, - context3_fn, context4_fn}; - - for (int i = 0; i < NUM_CONTEXTS; i++) { - ret = wasix_context_create(&contexts[i], entrypoints[i]); - assert(ret == 0 && "Failed to create context"); - } - - // Start the chain - wasix_context_switch(contexts[0]); - - // Verify all contexts executed - assert(execution_count == NUM_CONTEXTS && - "All contexts should have executed"); - - // Cleanup - for (int i = 0; i < NUM_CONTEXTS; i++) { - wasix_context_destroy(contexts[i]); - } - - fprintf(stderr, "Complex nested operations test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/contexts_with_getcwd.c b/lib/wasix/tests/context_switching/contexts_with_getcwd.c deleted file mode 100644 index 1288c0abcf4..00000000000 --- a/lib/wasix/tests/context_switching/contexts_with_getcwd.c +++ /dev/null @@ -1,89 +0,0 @@ -#include -#include -#include -#include -#include -#include - -// Test context switching with directory operations - -wasix_context_id_t ctx1, ctx2; -char original_dir[1024]; - -void context1_fn(void) { - char buf[1024]; - - // Get current directory - char *ret_cwd = getcwd(buf, sizeof(buf)); - assert(ret_cwd != NULL && "Failed to get cwd in context 1"); - - // Change to /tmp - int ret = chdir("/tmp"); - assert(ret == 0 && "Failed to chdir in context 1"); - - // Verify the change - ret_cwd = getcwd(buf, sizeof(buf)); - assert(ret_cwd != NULL && "Failed to get cwd after chdir"); - assert(strcmp(buf, "/tmp") == 0 && "Should be in /tmp"); - - // Switch to context 2 - wasix_context_switch(ctx2); - - // After resuming, check if we're still in /tmp - ret_cwd = getcwd(buf, sizeof(buf)); - assert(ret_cwd != NULL && "Failed to get cwd after resume"); - // Current directory is shared across contexts (same process) - assert(strcmp(buf, "/") == 0 && "Context 2 should have changed to /"); - - // Restore original directory - ret = chdir(original_dir); - assert(ret == 0 && "Failed to restore original directory"); - - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - char buf[1024]; - - // Check current directory (should be /tmp from context 1) - char *ret_cwd = getcwd(buf, sizeof(buf)); - assert(ret_cwd != NULL && "Failed to get cwd in context 2"); - assert(strcmp(buf, "/tmp") == 0 && "Should be in /tmp from context 1"); - - // Change to root - int ret = chdir("/"); - assert(ret == 0 && "Failed to chdir to / in context 2"); - - // Switch back to context 1 - wasix_context_switch(ctx1); -} - -int main() { - int ret; - - // Save original directory - char *ret_cwd = getcwd(original_dir, sizeof(original_dir)); - assert(ret_cwd != NULL && "Failed to get original cwd"); - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0 && "Failed to create context 2"); - - // Start execution - wasix_context_switch(ctx1); - - // Verify we're back in original directory - char buf[1024]; - ret_cwd = getcwd(buf, sizeof(buf)); - assert(ret_cwd != NULL && "Failed to get final cwd"); - assert(strcmp(buf, original_dir) == 0 && "Should be back in original dir"); - - // Cleanup - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "Directory operations switching test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/correct_context_activated.c b/lib/wasix/tests/context_switching/correct_context_activated.c deleted file mode 100644 index 6eb0cf13a54..00000000000 --- a/lib/wasix/tests/context_switching/correct_context_activated.c +++ /dev/null @@ -1,63 +0,0 @@ -// Test that the correct context is activated when switching by ID -// Creates 3 contexts and verifies each one's entrypoint is called when -// switching to it -#include -#include -#include - -wasix_context_id_t ctx1, ctx2, ctx3; - -void context1_fn(void) { - fprintf(stderr, "context1_fn was called (expected for ctx1=%llu)\n", - (unsigned long long)ctx1); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - fprintf(stderr, "context2_fn was called (expected for ctx2=%llu)\n", - (unsigned long long)ctx2); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -void context3_fn(void) { - fprintf(stderr, "context3_fn was called (expected for ctx3=%llu)\n", - (unsigned long long)ctx3); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - // Create contexts in order: ctx1, ctx2, ctx3 - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0); - fprintf(stderr, "Created ctx1=%llu with entrypoint=context1_fn\n", - (unsigned long long)ctx1); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0); - fprintf(stderr, "Created ctx2=%llu with entrypoint=context2_fn\n", - (unsigned long long)ctx2); - - ret = wasix_context_create(&ctx3, context3_fn); - assert(ret == 0); - fprintf(stderr, "Created ctx3=%llu with entrypoint=context3_fn\n", - (unsigned long long)ctx3); - - // Now switch to ctx1 specifically - fprintf(stderr, "\nSwitching to ctx1 (id=%llu)\n", (unsigned long long)ctx1); - fflush(stderr); - wasix_context_switch(ctx1); - fprintf(stderr, "Back from ctx1\n\n"); - - // Cleanup and test passed - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - wasix_context_destroy(ctx3); - - fprintf(stderr, "Test passed - ctx1 was correctly activated!\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/deep_recursion.c b/lib/wasix/tests/context_switching/deep_recursion.c deleted file mode 100644 index 2ab4b765b44..00000000000 --- a/lib/wasix/tests/context_switching/deep_recursion.c +++ /dev/null @@ -1,93 +0,0 @@ -#include -#include -#include -#include - -// Test that contexts have independent stacks with deep recursion - -#define RECURSION_DEPTH 100 - -wasix_context_id_t ctx1, ctx2; -int ctx1_depth = 0; -int ctx2_depth = 0; -int ctx1_max_depth = 0; -int ctx2_max_depth = 0; - -void ctx1_recursive(int depth); -void ctx2_recursive(int depth); - -void ctx1_recursive(int depth) { - ctx1_depth = depth; - if (depth > ctx1_max_depth) { - ctx1_max_depth = depth; - } - - if (depth < RECURSION_DEPTH) { - // Recurse deeper - ctx1_recursive(depth + 1); - } else { - // Switch to context 2 at max depth - wasix_context_switch(ctx2); - } -} - -void ctx2_recursive(int depth) { - ctx2_depth = depth; - if (depth > ctx2_max_depth) { - ctx2_max_depth = depth; - } - - if (depth < RECURSION_DEPTH) { - // Recurse deeper - ctx2_recursive(depth + 1); - } else { - // Switch back to context 1 at max depth - wasix_context_switch(ctx1); - } -} - -void context1_fn(void) { - // Start recursion in context 1 - ctx1_recursive(0); - - // Verify we completed the full recursion - assert(ctx1_max_depth == RECURSION_DEPTH && - "Context 1 should have reached max depth"); - - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - // Start recursion in context 2 - ctx2_recursive(0); - - // Verify we completed the full recursion - assert(ctx2_max_depth == RECURSION_DEPTH && - "Context 2 should have reached max depth"); - - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0 && "Failed to create context 2"); - - // Start the recursion chain - wasix_context_switch(ctx1); - - // Verify both contexts reached max depth independently - assert(ctx1_max_depth == RECURSION_DEPTH && "Context 1 max depth incorrect"); - assert(ctx2_max_depth == RECURSION_DEPTH && "Context 2 max depth incorrect"); - - // Cleanup - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "Deep recursion test passed (depth=%d)\n", RECURSION_DEPTH); - return 0; -} diff --git a/lib/wasix/tests/context_switching/file_io_switching.c b/lib/wasix/tests/context_switching/file_io_switching.c deleted file mode 100644 index ea1f2e288f4..00000000000 --- a/lib/wasix/tests/context_switching/file_io_switching.c +++ /dev/null @@ -1,99 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -// Test file I/O operations between context switches - -wasix_context_id_t ctx1, ctx2; -const char *test_file = "/tmp/context_switch_test.txt"; - -#define TEST_DATA_1 "Hello from context 1\n" -#define TEST_DATA_2 "Hello from context 2\n" - -void context1_fn(void) { - char buffer[128]; - ssize_t n; - int fd; - - // Write to file - fd = open(test_file, O_WRONLY | O_CREAT | O_TRUNC, 0644); - assert(fd >= 0 && "Failed to open file in context 1"); - - n = write(fd, TEST_DATA_1, strlen(TEST_DATA_1)); - assert(n == strlen(TEST_DATA_1) && "Failed to write to file in context 1"); - close(fd); - - // Switch to context 2 - wasix_context_switch(ctx2); - - // After resuming, read what context 2 appended - fd = open(test_file, O_RDONLY); - assert(fd >= 0 && "Failed to open file for reading in context 1"); - - memset(buffer, 0, sizeof(buffer)); - n = read(fd, buffer, sizeof(buffer) - 1); - assert(n > 0 && "Failed to read from file in context 1"); - close(fd); - - // Verify both messages are there - assert(strstr(buffer, TEST_DATA_1) != NULL && "Context 1 data missing"); - assert(strstr(buffer, TEST_DATA_2) != NULL && "Context 2 data missing"); - - // Clean up - unlink(test_file); - - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - char buffer[128]; - ssize_t n; - int fd; - - // Read what context 1 wrote - fd = open(test_file, O_RDONLY); - assert(fd >= 0 && "Failed to open file in context 2"); - - memset(buffer, 0, sizeof(buffer)); - n = read(fd, buffer, sizeof(buffer) - 1); - assert(n > 0 && "Failed to read from file in context 2"); - assert(strcmp(buffer, TEST_DATA_1) == 0 && - "Read incorrect data in context 2"); - close(fd); - - // Append to file - fd = open(test_file, O_WRONLY | O_APPEND); - assert(fd >= 0 && "Failed to open file for append in context 2"); - - n = write(fd, TEST_DATA_2, strlen(TEST_DATA_2)); - assert(n == strlen(TEST_DATA_2) && "Failed to append to file in context 2"); - close(fd); - - // Switch back to context 1 - wasix_context_switch(ctx1); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0 && "Failed to create context 2"); - - // Start execution - wasix_context_switch(ctx1); - - // Cleanup - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "File I/O switching test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/function_args_preserved.c b/lib/wasix/tests/context_switching/function_args_preserved.c deleted file mode 100644 index f022cf96b54..00000000000 --- a/lib/wasix/tests/context_switching/function_args_preserved.c +++ /dev/null @@ -1,71 +0,0 @@ -// Test that function arguments are preserved correctly when contexts start -#include -#include -#include - -wasix_context_id_t ctx1, ctx2; -int value_seen_in_ctx1 = -1; -int value_seen_in_ctx2 = -1; - -void recursive_function(int depth, int expected_depth) { - if (depth != expected_depth) { - fprintf(stderr, "ERROR: depth=%d but expected_depth=%d\n", depth, - expected_depth); - fflush(stderr); - } - - if (depth == 0) { - fprintf(stderr, "Reached depth 0\n"); - fflush(stderr); - return; - } - - recursive_function(depth - 1, expected_depth - 1); -} - -void context1_fn(void) { - fprintf(stderr, "context1_fn: calling recursive_function(5, 5)\n"); - fflush(stderr); - recursive_function(5, 5); - fprintf(stderr, "context1_fn: returned from recursive_function\n"); - fflush(stderr); - value_seen_in_ctx1 = 5; - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - fprintf(stderr, "context2_fn: calling recursive_function(3, 3)\n"); - fflush(stderr); - recursive_function(3, 3); - fprintf(stderr, "context2_fn: returned from recursive_function\n"); - fflush(stderr); - value_seen_in_ctx2 = 3; - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0); - - fprintf(stderr, "Switching to ctx1\n"); - fflush(stderr); - wasix_context_switch(ctx1); - - fprintf(stderr, "Switching to ctx2\n"); - fflush(stderr); - wasix_context_switch(ctx2); - - assert(value_seen_in_ctx1 == 5 && "ctx1 should have completed with depth 5"); - assert(value_seen_in_ctx2 == 3 && "ctx2 should have completed with depth 3"); - - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "Test passed - function arguments preserved correctly!\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/global_ctx_ids.c b/lib/wasix/tests/context_switching/global_ctx_ids.c deleted file mode 100644 index b794fa10c6f..00000000000 --- a/lib/wasix/tests/context_switching/global_ctx_ids.c +++ /dev/null @@ -1,59 +0,0 @@ -// Test that global variables containing context IDs are accessible from context -// entrypoints -#include -#include -#include - -wasix_context_id_t ctx1, ctx2; - -void context1_fn(void) { - fprintf(stderr, "ctx1 entrypoint: ctx1=%llu, ctx2=%llu\n", - (unsigned long long)ctx1, (unsigned long long)ctx2); - fflush(stderr); - - // Try to switch to ctx2 - fprintf(stderr, "ctx1: switching to ctx2\n"); - fflush(stderr); - wasix_context_switch(ctx2); - - fprintf(stderr, "ctx1: resumed\n"); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - fprintf(stderr, "ctx2 entrypoint: ctx1=%llu, ctx2=%llu\n", - (unsigned long long)ctx1, (unsigned long long)ctx2); - fflush(stderr); - - fprintf(stderr, "ctx2: switching back to ctx1\n"); - fflush(stderr); - wasix_context_switch(ctx1); -} - -int main() { - int ret; - - fprintf(stderr, "Before creation: ctx1=%llu, ctx2=%llu\n", - (unsigned long long)ctx1, (unsigned long long)ctx2); - fflush(stderr); - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0); - fprintf(stderr, "After ctx1 creation: ctx1=%llu\n", (unsigned long long)ctx1); - fflush(stderr); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0); - fprintf(stderr, "After ctx2 creation: ctx2=%llu\n", (unsigned long long)ctx2); - fflush(stderr); - - fprintf(stderr, "main: switching to ctx1\n"); - fflush(stderr); - wasix_context_switch(ctx1); - - fprintf(stderr, "Test passed!\n"); - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - return 0; -} diff --git a/lib/wasix/tests/context_switching/heap_allocations.c b/lib/wasix/tests/context_switching/heap_allocations.c deleted file mode 100644 index 9853f30365a..00000000000 --- a/lib/wasix/tests/context_switching/heap_allocations.c +++ /dev/null @@ -1,71 +0,0 @@ -#include -#include -#include -#include -#include - -// Test that contexts maintain independent heap allocations - -wasix_context_id_t ctx1, ctx2; - -#define BUFFER_SIZE 1024 - -void context1_fn(void) { - // Allocate and fill memory - char *buffer1 = (char *)malloc(BUFFER_SIZE); - assert(buffer1 != NULL && "Failed to allocate buffer in context 1"); - - memset(buffer1, 'A', BUFFER_SIZE); - buffer1[BUFFER_SIZE - 1] = '\0'; - - // Switch to context 2 - wasix_context_switch(ctx2); - - // Verify our buffer is still intact after context 2 executed - assert(buffer1[0] == 'A' && "Buffer corrupted after context switch"); - assert(buffer1[BUFFER_SIZE - 2] == 'A' && - "Buffer corrupted after context switch"); - - free(buffer1); - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - // Allocate and fill memory with different pattern - char *buffer2 = (char *)malloc(BUFFER_SIZE * 2); - assert(buffer2 != NULL && "Failed to allocate buffer in context 2"); - - memset(buffer2, 'B', BUFFER_SIZE * 2); - buffer2[BUFFER_SIZE * 2 - 1] = '\0'; - - // Switch back to context 1 - wasix_context_switch(ctx1); - - // Verify our buffer is still intact - assert(buffer2[0] == 'B' && "Buffer corrupted after context switch"); - assert(buffer2[BUFFER_SIZE * 2 - 2] == 'B' && - "Buffer corrupted after context switch"); - - free(buffer2); - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0 && "Failed to create context 2"); - - // Start execution - wasix_context_switch(ctx1); - - // Cleanup - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "Heap allocations test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/main_context_id.c b/lib/wasix/tests/context_switching/main_context_id.c deleted file mode 100644 index 90fa915884a..00000000000 --- a/lib/wasix/tests/context_switching/main_context_id.c +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include -#include -#include - -// Test wasix_context_main identifier behavior - -wasix_context_id_t ctx1; -wasix_context_id_t main_ctx_from_ctx1; -wasix_context_id_t main_ctx_from_main; - -void context1_fn(void) { - // Get main context identifier from within a non-main context - main_ctx_from_ctx1 = wasix_context_main; - - // Switch back to main - wasix_context_switch(wasix_context_main); - - // This should not be reached - fprintf(stderr, - "ERROR: Execution continued in context1 after switch to main\n"); - exit(1); -} - -int main() { - int ret; - - // Get main context identifier from main - main_ctx_from_main = wasix_context_main; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - // Switch to context 1 - wasix_context_switch(ctx1); - - // Verify that wasix_context_main is consistent - assert(main_ctx_from_main == main_ctx_from_ctx1 && - "wasix_context_main should be the same from all contexts"); - - // Test that we can switch to main context using the identifier - ret = wasix_context_create( - &ctx1, context1_fn); // Recreate since previous one terminated - assert(ret == 0 && "Failed to recreate context 1"); - - wasix_context_switch(ctx1); - - // We should be back here after context1 switches to main - - // Cleanup - wasix_context_destroy(ctx1); - - fprintf(stderr, "Main context identifier test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/many_contexts.c b/lib/wasix/tests/context_switching/many_contexts.c deleted file mode 100644 index f3f94e80ea2..00000000000 --- a/lib/wasix/tests/context_switching/many_contexts.c +++ /dev/null @@ -1,62 +0,0 @@ -#include -#include -#include -#include - -// Test creating many contexts to stress resource management - -#define NUM_CONTEXTS 20 - -wasix_context_id_t contexts[NUM_CONTEXTS]; -int executed[NUM_CONTEXTS] = {0}; - -void generic_context_fn(void) { - // Find which context this is - for (int i = 0; i < NUM_CONTEXTS; i++) { - if (!executed[i]) { - executed[i] = 1; - break; - } - } - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - // Create many contexts - for (int i = 0; i < NUM_CONTEXTS; i++) { - ret = wasix_context_create(&contexts[i], generic_context_fn); - assert(ret == 0 && "Failed to create context"); - } - - // Execute them all - for (int i = 0; i < NUM_CONTEXTS; i++) { - wasix_context_switch(contexts[i]); - assert(executed[i] == 1 && "Context should have executed"); - } - - // Destroy them all - for (int i = 0; i < NUM_CONTEXTS; i++) { - ret = wasix_context_destroy(contexts[i]); - assert(ret == 0 && "Failed to destroy context"); - } - - // Create another batch to ensure resources were properly freed - for (int i = 0; i < NUM_CONTEXTS; i++) { - ret = wasix_context_create(&contexts[i], generic_context_fn); - assert(ret == 0 && "Failed to create context in second batch"); - } - - // Destroy second batch - for (int i = 0; i < NUM_CONTEXTS; i++) { - ret = wasix_context_destroy(contexts[i]); - assert(ret == 0 && "Failed to destroy context in second batch"); - } - - fprintf( - stderr, - "Many contexts test passed (%d contexts created and destroyed twice)\n", - NUM_CONTEXTS); - return 0; -} diff --git a/lib/wasix/tests/context_switching/mutual_recursion.c b/lib/wasix/tests/context_switching/mutual_recursion.c deleted file mode 100644 index 86cecedb322..00000000000 --- a/lib/wasix/tests/context_switching/mutual_recursion.c +++ /dev/null @@ -1,60 +0,0 @@ -// Absolute minimal bug: mutually recursive functions with context switch -#include -#include -#include - -wasix_context_id_t ctx1, ctx2; - -void func_b(int depth); - -void func_a(int depth) { - fprintf(stderr, "[func_a] depth=%d\n", depth); - fflush(stderr); - if (depth == 0) - return; - func_b(depth - 1); -} - -void func_b(int depth) { - fprintf(stderr, "[func_b] depth=%d\n", depth); - fflush(stderr); - if (depth == 0) - return; - func_a(depth - 1); -} - -void context1_fn(void) { - fprintf(stderr, "ctx1: calling func_a(3)\n"); - fflush(stderr); - func_a(3); - fprintf(stderr, "ctx1: done\n"); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - fprintf(stderr, "ctx2: calling func_b(2)\n"); - fflush(stderr); - func_b(2); - fprintf(stderr, "ctx2: done\n"); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -int main() { - wasix_context_create(&ctx1, context1_fn); - wasix_context_create(&ctx2, context2_fn); - - fprintf(stderr, "main: switching to ctx1\n"); - fflush(stderr); - wasix_context_switch(ctx1); - - fprintf(stderr, "main: switching to ctx2\n"); - fflush(stderr); - wasix_context_switch(ctx2); - - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - fprintf(stderr, "Test passed!\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/rapid_switching.c b/lib/wasix/tests/context_switching/rapid_switching.c deleted file mode 100644 index f2106218248..00000000000 --- a/lib/wasix/tests/context_switching/rapid_switching.c +++ /dev/null @@ -1,53 +0,0 @@ -#include -#include -#include -#include - -// Test rapid context switching with many iterations - -#define SWITCH_COUNT 1000 - -wasix_context_id_t ctx1, ctx2; -int ping_count = 0; -int pong_count = 0; - -void ping_context(void) { - while (ping_count < SWITCH_COUNT) { - ping_count++; - wasix_context_switch(ctx2); - } - wasix_context_switch(wasix_context_main); -} - -void pong_context(void) { - while (pong_count < SWITCH_COUNT) { - pong_count++; - wasix_context_switch(ctx1); - } - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, ping_context); - assert(ret == 0 && "Failed to create ping context"); - - ret = wasix_context_create(&ctx2, pong_context); - assert(ret == 0 && "Failed to create pong context"); - - // Start the ping-pong - wasix_context_switch(ctx1); - - // Verify all switches occurred - assert(ping_count == SWITCH_COUNT && "Ping count mismatch"); - assert(pong_count == SWITCH_COUNT && "Pong count mismatch"); - - // Cleanup - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "Rapid switching test passed (%d switches)\n", - SWITCH_COUNT * 2); - return 0; -} diff --git a/lib/wasix/tests/context_switching/self_switching.c b/lib/wasix/tests/context_switching/self_switching.c deleted file mode 100644 index b0a92c4ddf7..00000000000 --- a/lib/wasix/tests/context_switching/self_switching.c +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include -#include -#include - -// Test switching to the currently active context (should be a no-op) - -wasix_context_id_t ctx1; -int switch_count = 0; - -void context1_fn(void) { - int local_var = 42; - - // Switch to self (should be a no-op) - int ret = wasix_context_switch(ctx1); - assert(ret == 0 && "Self-switch should succeed"); - - // Verify we're still in the same context - assert(local_var == 42 && - "Local variable should be unchanged after self-switch"); - switch_count++; - - // Try again - ret = wasix_context_switch(ctx1); - assert(ret == 0 && "Second self-switch should succeed"); - switch_count++; - - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - int main_local = 123; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - // Test self-switch in main context - ret = wasix_context_switch(wasix_context_main); - assert(ret == 0 && "Main context self-switch should succeed"); - assert(main_local == 123 && "Main local variable should be unchanged"); - - // Switch to context 1 - wasix_context_switch(ctx1); - - // Verify context1 executed - assert(switch_count == 2 && - "Context 1 should have performed 2 self-switches"); - - // Cleanup - wasix_context_destroy(ctx1); - - fprintf(stderr, "Self-switching test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/shared_recursion.c b/lib/wasix/tests/context_switching/shared_recursion.c deleted file mode 100644 index 21aeeb3298f..00000000000 --- a/lib/wasix/tests/context_switching/shared_recursion.c +++ /dev/null @@ -1,59 +0,0 @@ -// Minimal bug reproduction: calling same recursive functions from different -// contexts -#include -#include -#include - -wasix_context_id_t ctx1, ctx2; -int switch_count = 0; - -void shared_func(int depth) { - fprintf(stderr, "[shared_func] depth=%d\n", depth); - fflush(stderr); - - if (depth == 1 && switch_count == 0) { - switch_count++; - fprintf(stderr, "[shared_func] switching to ctx2\n"); - fflush(stderr); - wasix_context_switch(ctx2); - fprintf(stderr, "[shared_func] resumed\n"); - fflush(stderr); - } - - if (depth == 0) { - return; - } - shared_func(depth - 1); -} - -void context1_fn(void) { - fprintf(stderr, "ctx1: calling shared_func(2)\n"); - fflush(stderr); - shared_func(2); - fprintf(stderr, "ctx1: done\n"); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - fprintf(stderr, "ctx2: calling shared_func(1)\n"); - fflush(stderr); - shared_func(1); - fprintf(stderr, "ctx2: done\n"); - fflush(stderr); - wasix_context_switch(ctx1); -} - -int main() { - wasix_context_create(&ctx1, context1_fn); - wasix_context_create(&ctx2, context2_fn); - - fprintf(stderr, "Switching to ctx1\n"); - fflush(stderr); - wasix_context_switch(ctx1); - - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - fprintf(stderr, "Test passed!\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/state_preservation.c b/lib/wasix/tests/context_switching/state_preservation.c deleted file mode 100644 index 5bc853971a6..00000000000 --- a/lib/wasix/tests/context_switching/state_preservation.c +++ /dev/null @@ -1,82 +0,0 @@ -#include -#include -#include -#include - -// Test that context state (local variables, stack) is preserved across switches - -wasix_context_id_t ctx1, ctx2; - -void context1_fn(void) { - int local1 = 100; - int local2 = 200; - int local3 = 300; - - // Switch to context2 - wasix_context_switch(ctx2); - - // After returning, verify our local variables are preserved - assert(local1 == 100 && "Local variable 1 should be preserved"); - assert(local2 == 200 && "Local variable 2 should be preserved"); - assert(local3 == 300 && "Local variable 3 should be preserved"); - - // Modify the variables - local1 = 111; - local2 = 222; - local3 = 333; - - // Switch again - wasix_context_switch(ctx2); - - // Verify modified values are preserved - assert(local1 == 111 && "Modified local variable 1 should be preserved"); - assert(local2 == 222 && "Modified local variable 2 should be preserved"); - assert(local3 == 333 && "Modified local variable 3 should be preserved"); - - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - int local_a = 10; - int local_b = 20; - - // Switch back to context1 - wasix_context_switch(ctx1); - - // After returning, verify our local variables are preserved - assert(local_a == 10 && "Local variable a should be preserved"); - assert(local_b == 20 && "Local variable b should be preserved"); - - // Modify - local_a = 99; - local_b = 88; - - // Switch again - wasix_context_switch(ctx1); - - // Verify modified values - assert(local_a == 99 && "Modified local variable a should be preserved"); - assert(local_b == 88 && "Modified local variable b should be preserved"); - - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0 && "Failed to create context 1"); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0 && "Failed to create context 2"); - - // Start execution - wasix_context_switch(ctx1); - - // Cleanup - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "Context state preservation test passed\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/switch_from_recursion.c b/lib/wasix/tests/context_switching/switch_from_recursion.c deleted file mode 100644 index 21ef2b12e9d..00000000000 --- a/lib/wasix/tests/context_switching/switch_from_recursion.c +++ /dev/null @@ -1,69 +0,0 @@ -// Minimal test: context switches from within a recursive function -#include -#include -#include - -wasix_context_id_t ctx1, ctx2; -int call_count = 0; - -void recursive_func(int depth) { - call_count++; - fprintf(stderr, "[recursive_func depth=%d] call_count=%d\n", depth, - call_count); - fflush(stderr); - - if (depth == 0) { - fprintf(stderr, "[recursive_func] reached depth 0, switching to ctx2\n"); - fflush(stderr); - wasix_context_switch(ctx2); - fprintf(stderr, "[recursive_func] resumed after switch\n"); - fflush(stderr); - return; - } - - recursive_func(depth - 1); - fprintf(stderr, "[recursive_func depth=%d] returning\n", depth); - fflush(stderr); -} - -void context1_fn(void) { - fprintf(stderr, "ctx1: starting\n"); - fflush(stderr); - recursive_func(3); - fprintf(stderr, "ctx1: after recursive_func returned\n"); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - fprintf(stderr, "ctx2: starting\n"); - fflush(stderr); - wasix_context_switch(ctx1); - fprintf(stderr, "ctx2: ERROR - should not reach here\n"); - fflush(stderr); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0); - - fprintf(stderr, "main: switching to ctx1\n"); - fflush(stderr); - wasix_context_switch(ctx1); - - fprintf(stderr, "main: back from ctx1, call_count=%d\n", call_count); - fflush(stderr); - assert(call_count == 4 && - "Should have made 4 recursive calls (depth 3,2,1,0)"); - - wasix_context_destroy(ctx1); - wasix_context_destroy(ctx2); - - fprintf(stderr, "Test passed!\n"); - return 0; -} diff --git a/lib/wasix/tests/context_switching/wrong_entrypoint.c b/lib/wasix/tests/context_switching/wrong_entrypoint.c deleted file mode 100644 index a2428555bf0..00000000000 --- a/lib/wasix/tests/context_switching/wrong_entrypoint.c +++ /dev/null @@ -1,54 +0,0 @@ -// Minimal test to check if the correct entrypoint is being called -#include -#include -#include - -wasix_context_id_t ctx1, ctx2, ctx3; - -void context1_fn(void) { - fprintf(stderr, "context1_fn was called!\n"); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -void context2_fn(void) { - fprintf(stderr, "context2_fn was called!\n"); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -void context3_fn(void) { - fprintf(stderr, "context3_fn was called!\n"); - fflush(stderr); - wasix_context_switch(wasix_context_main); -} - -int main() { - int ret; - - ret = wasix_context_create(&ctx1, context1_fn); - assert(ret == 0); - - ret = wasix_context_create(&ctx2, context2_fn); - assert(ret == 0); - - ret = wasix_context_create(&ctx3, context3_fn); - assert(ret == 0); - - fprintf(stderr, "Switching to ctx1 (id=%llu)\n", (unsigned long long)ctx1); - fflush(stderr); - wasix_context_switch(ctx1); - - fprintf(stderr, "Back in main, switching to ctx2 (id=%llu)\n", - (unsigned long long)ctx2); - fflush(stderr); - wasix_context_switch(ctx2); - - fprintf(stderr, "Back in main, switching to ctx3 (id=%llu)\n", - (unsigned long long)ctx3); - fflush(stderr); - wasix_context_switch(ctx3); - - fprintf(stderr, "Test passed\n"); - return 0; -} From 1fe0a5b5b3535756d69717acc0b751fa822ae0d2 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 10:18:02 +0100 Subject: [PATCH 101/114] Disable context-switching test that only works with eh --- lib/wasix/tests/context_switching.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index 67bce05f930..cf32ce03e2a 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -149,8 +149,9 @@ fn test_three_way_recursion() { test_with_wasixcc("three_way_recursion").unwrap(); } -#[cfg(target_os = "linux")] -#[test] -fn test_contexts_with_setjmp() { - test_with_wasixcc("contexts_with_setjmp").unwrap(); -} +// TODO: Reenable once cranelift supports exception handling +// #[cfg(target_os = "linux")] +// #[test] +// fn test_contexts_with_setjmp() { +// test_with_wasixcc("contexts_with_setjmp").unwrap(); +// } From 36753ddc1b22bfb07613155bd27be7ca9ac384ae Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 10:19:16 +0100 Subject: [PATCH 102/114] Enable wasixcc based tests on all unix targets --- lib/wasix/tests/context_switching.rs | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/wasix/tests/context_switching.rs b/lib/wasix/tests/context_switching.rs index cf32ce03e2a..7da31bfd3ff 100644 --- a/lib/wasix/tests/context_switching.rs +++ b/lib/wasix/tests/context_switching.rs @@ -41,116 +41,116 @@ fn test_with_wasixcc(name: &str) -> Result<(), anyhow::Error> { ) } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_simple_switching() { test_with_wasixcc("simple_switching").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_switching_with_main() { test_with_wasixcc("switching_with_main").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_switching_to_a_deleted_context() { test_with_wasixcc("switching_to_a_deleted_context").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_switching_threads() { test_with_wasixcc("switching_in_threads").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_multiple_contexts() { test_with_wasixcc("multiple_contexts").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_error_handling() { test_with_wasixcc("error_handling").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_nested_switches() { test_with_wasixcc("nested_switches").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_contexts_with_mutexes() { test_with_wasixcc("contexts_with_mutexes").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_contexts_with_env_vars() { test_with_wasixcc("contexts_with_env_vars").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_contexts_with_signals() { test_with_wasixcc("contexts_with_signals").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_contexts_with_timers() { test_with_wasixcc("contexts_with_timers").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_contexts_with_pipes() { test_with_wasixcc("contexts_with_pipes").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_pending_file_operations() { test_with_wasixcc("pending_file_operations").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_recursive_host_calls() { test_with_wasixcc("recursive_host_calls").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_malloc_during_switch() { test_with_wasixcc("malloc_during_switch").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_nested_host_call_switch() { test_with_wasixcc("nested_host_call_switch").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_switch_to_never_resumed() { test_with_wasixcc("switch_to_never_resumed").unwrap(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_three_way_recursion() { test_with_wasixcc("three_way_recursion").unwrap(); } // TODO: Reenable once cranelift supports exception handling -// #[cfg(target_os = "linux")] +// #[cfg(unix)] // #[test] // fn test_contexts_with_setjmp() { // test_with_wasixcc("contexts_with_setjmp").unwrap(); From 0265c962c3de332ee4812c1a08511307ca99768d Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 10:49:08 +0100 Subject: [PATCH 103/114] Update wasixcc version in tests --- .github/workflows/test.yaml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 19f71ae924c..fac57484f1a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -182,19 +182,11 @@ jobs: file: binaryen-version_123-x86_64-linux.tar.gz target: binaryen/binaryen.tar.gz - - name: Unpack binaryen - run: | - tar -xf binaryen/binaryen.tar.gz -C binaryen --strip-components=1 - chmod +x binaryen/bin/* - export BINARYEN_DIR=$(pwd)/binaryen - echo "$BINARYEN_DIR/bin" >> $GITHUB_PATH - - name: Install wasixcc, sysroot and LLVM run: | export RUST_LOG=wasixcc=trace export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} - rustup toolchain install 1.90 --profile minimal --no-self-update - cargo +1.90 install wasixcc -F bin + cargo install --git https://github.com/wasix-org/wasixcc.git --rev 2df8392a1cb9e7b050130f6ac01cd84d9e39406f -F bin mkdir wasixcc-install wasixcc --install-executables ./wasixcc-install echo "$(pwd)/wasixcc-install" >> $GITHUB_PATH @@ -202,6 +194,7 @@ jobs: mkdir -p ~/.wasixcc/sysroot wasixcc --download-sysroot v2025-12-10.1 wasixcc --download-llvm + wasixcc --download-binaryen - name: Tool versions run: | @@ -902,7 +895,7 @@ jobs: run: | export RUST_LOG=wasixcc=trace export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} - cargo install --git https://github.com/wasix-org/wasixcc.git --rev d9ab260eb557ce90406ccf07ac52ac5e211caf60 -F bin + cargo install --git https://github.com/wasix-org/wasixcc.git --rev 2df8392a1cb9e7b050130f6ac01cd84d9e39406f -F bin mkdir wasixcc-install wasixcc --install-executables ./wasixcc-install echo "$(pwd)/wasixcc-install" >> $GITHUB_PATH From 206a35cd1c228a0d33f964aa44c555340aa83618 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 11:11:15 +0100 Subject: [PATCH 104/114] Adjust wasixcc install in CI --- .github/workflows/test.yaml | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fac57484f1a..6b29fd962f4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -174,27 +174,19 @@ jobs: sudo apt-get update sudo apt-get install -y git make lld curl - - name: Download binaryen - uses: dsaltares/fetch-gh-release-asset@1.1.2 - with: - repo: WebAssembly/binaryen - version: tags/version_123 - file: binaryen-version_123-x86_64-linux.tar.gz - target: binaryen/binaryen.tar.gz - - - name: Install wasixcc, sysroot and LLVM + - name: Install wasixcc + if: matrix.metadata.build != 'windows-x64' run: | export RUST_LOG=wasixcc=trace export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} cargo install --git https://github.com/wasix-org/wasixcc.git --rev 2df8392a1cb9e7b050130f6ac01cd84d9e39406f -F bin mkdir wasixcc-install - wasixcc --install-executables ./wasixcc-install - echo "$(pwd)/wasixcc-install" >> $GITHUB_PATH - mkdir -p ~/.wasixcc/llvm - mkdir -p ~/.wasixcc/sysroot + wasixcc --install-executables $HOME/.wasixcc/bin + echo $HOME/.wasixcc/bin >> $GITHUB_PATH wasixcc --download-sysroot v2025-12-10.1 wasixcc --download-llvm wasixcc --download-binaryen + echo $HOME/.wasixcc/binaryen/bin >> $GITHUB_PATH - name: Tool versions run: | @@ -891,20 +883,18 @@ jobs: echo "/opt/homebrew/opt/llvm/bin" >> GITHUB_PATH - name: Install wasixcc - if: matrix.metadata.build == 'linux-x64' || matrix.metadata.build == 'linux-musl' + if: matrix.metadata.build != 'windows-x64' run: | export RUST_LOG=wasixcc=trace export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} cargo install --git https://github.com/wasix-org/wasixcc.git --rev 2df8392a1cb9e7b050130f6ac01cd84d9e39406f -F bin mkdir wasixcc-install - wasixcc --install-executables ./wasixcc-install - echo "$(pwd)/wasixcc-install" >> $GITHUB_PATH - mkdir -p ~/.wasixcc/llvm - mkdir -p ~/.wasixcc/sysroot - mkdir -p ~/.wasixcc/binaryen + wasixcc --install-executables $HOME/.wasixcc/bin + echo $HOME/.wasixcc/bin >> $GITHUB_PATH wasixcc --download-sysroot v2025-12-10.1 wasixcc --download-llvm wasixcc --download-binaryen + echo $HOME/.wasixcc/binaryen/bin >> $GITHUB_PATH - name: Install LLVM shell: bash From d7419094f72e8d399a36b51bdf0451b880c5072b Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 11:28:52 +0100 Subject: [PATCH 105/114] Prevent process forking if a vfork is already active --- lib/wasix/src/syscalls/wasix/proc_fork.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/wasix/src/syscalls/wasix/proc_fork.rs b/lib/wasix/src/syscalls/wasix/proc_fork.rs index 0dd25b64846..c86bd1a50df 100644 --- a/lib/wasix/src/syscalls/wasix/proc_fork.rs +++ b/lib/wasix/src/syscalls/wasix/proc_fork.rs @@ -58,6 +58,11 @@ pub fn proc_fork( } trace!(%copy_memory, "capturing"); + if let Some(vfork) = ctx.data().vfork.as_ref() { + warn!("process forking not supported in an active vfork"); + return Ok(Errno::Notsup); + } + // Fork the environment which will copy all the open file handlers // and associate a new context but otherwise shares things like the // file system interface. The handle to the forked process is stored @@ -108,13 +113,14 @@ pub fn proc_fork( // if it had actually forked child_env.swap_inner(ctx.data_mut()); std::mem::swap(ctx.data_mut(), &mut child_env); - ctx.data_mut().vfork.replace(WasiVFork { + let previous_vfork = ctx.data_mut().vfork.replace(WasiVFork { rewind_stack: rewind_stack.clone(), store_data: store_data.clone(), env: Box::new(child_env), handle: child_handle, is_64bit: M::is_64bit(), }); + assert!(previous_vfork.is_none()); // Already checked above // Carry on as if the fork had taken place (which basically means // it prevents to be the new process with the old one suspended) From b31403d3949b070f52de672a643944bc86b8d090 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 11:47:01 +0100 Subject: [PATCH 106/114] Add a test to ensure nested vforks are not allowed --- tests/wasix/vfork/main.c | 39 +++++++++++++++++++++++++++++++++++++++ tests/wasix/vfork/run.sh | 1 + 2 files changed, 40 insertions(+) diff --git a/tests/wasix/vfork/main.c b/tests/wasix/vfork/main.c index c1b1de2720f..7d0d53e65d3 100644 --- a/tests/wasix/vfork/main.c +++ b/tests/wasix/vfork/main.c @@ -86,6 +86,41 @@ int failing_exec() } } +int nested_vfork() +{ + int pid = vfork(); + + if (pid == 0) + { + int pid2 = vfork(); + if (pid2 != -1) + { + // A nested vfork should always fail + exit(13); + } + int err = errno; + if (err != ENOTSUP) + { + // errno should be set to EAGAIN + printf("Expected errno to be ENOTSUP (%d), got %d\n", ENOTSUP, err); + exit(14); + } + exit(30); + } + else + { + int status; + waitpid(pid, &status, 0); + if (WEXITSTATUS(status) != 30) + { + printf("Expected exit code 30 from subprocess, got %d\n", WEXITSTATUS(status)); + return 1; + } + + return 0; + } +} + int cloexec() { int fd = open("/bin/file", O_RDONLY | O_CREAT | O_CLOEXEC); @@ -427,6 +462,10 @@ int main(int argc, char **argv) { return cloexec(); } + else if (!strcmp(argv[1], "nested_vfork")) + { + return nested_vfork(); + } else if (!strcmp(argv[1], "exiting_child")) { return exiting_child(); diff --git a/tests/wasix/vfork/run.sh b/tests/wasix/vfork/run.sh index c939ed03f4b..5f6e9a6a236 100755 --- a/tests/wasix/vfork/run.sh +++ b/tests/wasix/vfork/run.sh @@ -7,6 +7,7 @@ $WASMER_RUN main.wasm --dir . -- successful_exec $WASMER_RUN main.wasm --dir . -- successful_execlp $WASMER_RUN main.wasm --dir . -- failing_exec $WASMER_RUN main.wasm --dir . -- cloexec +$WASMER_RUN main.wasm --dir . -- nested_vfork $WASMER_RUN main.wasm --dir . -- exiting_child $WASMER_RUN main.wasm --dir . -- trapping_child $WASMER_RUN main.wasm --dir . -- exit_before_exec From 6de9324005fd616ca8c3c8159616dc28414e8b10 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 14:33:48 +0100 Subject: [PATCH 107/114] Adjust resume_context to also use the ContextSwitchingEnvironment entrypoint --- lib/wasix/src/bin_factory/exec.rs | 62 +++++++++++++++++++------------ 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 53b743dd52e..9da63983185 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -300,22 +300,25 @@ fn call_module( let (mut store, mut call_ret) = ContextSwitchingEnvironment::run_main_context(&ctx, store, start.clone(), vec![]); - loop { + let mut store = loop { // Technically, it's an error for a vfork to return from main, but anyway... - match resume_vfork(&ctx, &mut store, &start, &call_ret) { + store = match resume_vfork(&ctx, store, &start, &call_ret) { // A vfork was resumed, there may be another, so loop back - Ok(Some(ret)) => call_ret = ret, + (store, Ok(Some(ret))) => { + call_ret = ret; + store + } // An error was encountered when restoring from the vfork, report it - Err(e) => { + (store, Err(e)) => { call_ret = Err(RuntimeError::user(Box::new(WasiError::Exit(e.into())))); - break; + break store; } // No vfork, keep the call_ret value - Ok(None) => break, - } - } + (store, Ok(None)) => break store, + }; + }; let ret = if let Err(err) = call_ret { match err.downcast::() { @@ -393,15 +396,18 @@ fn call_module( #[allow(clippy::type_complexity)] fn resume_vfork( ctx: &WasiFunctionEnv, - store: &mut Store, + mut store: Store, start: &Function, call_ret: &Result, RuntimeError>, -) -> Result, RuntimeError>>, Errno> { +) -> ( + Store, + Result, RuntimeError>>, Errno>, +) { let (err, code) = match call_ret { Ok(_) => (None, wasmer_wasix_types::wasi::ExitCode::from(0u16)), Err(err) => match err.downcast_ref::() { // If the child process is just deep sleeping, we don't restore the vfork - Some(WasiError::DeepSleep(..)) => return Ok(None), + Some(WasiError::DeepSleep(..)) => return (store, Ok(None)), Some(WasiError::Exit(code)) => (None, *code), Some(WasiError::ThreadExit) => (None, wasmer_wasix_types::wasi::ExitCode::from(0u16)), @@ -414,28 +420,28 @@ fn resume_vfork( }, }; - if let Some(mut vfork) = ctx.data_mut(store).vfork.take() { + if let Some(mut vfork) = ctx.data_mut(&mut store).vfork.take() { if let Some(err) = err { error!(%err, "Error from child process"); eprintln!("{err}"); } block_on( - unsafe { ctx.data(store).get_memory_and_wasi_state(store, 0) } + unsafe { ctx.data(&store).get_memory_and_wasi_state(&store, 0) } .1 .fs .close_all(), ); tracing::debug!( - pid = %ctx.data_mut(store).process.pid(), + pid = %ctx.data_mut(&mut store).process.pid(), vfork_pid = %vfork.env.process.pid(), "Resuming from vfork after child process was terminated" ); // Restore the WasiEnv to the point when we vforked - vfork.env.swap_inner(ctx.data_mut(store)); - std::mem::swap(vfork.env.as_mut(), ctx.data_mut(store)); + vfork.env.swap_inner(ctx.data_mut(&mut store)); + std::mem::swap(vfork.env.as_mut(), ctx.data_mut(&mut store)); let mut child_env = *vfork.env; child_env.owned_handles.push(vfork.handle); @@ -443,11 +449,11 @@ fn resume_vfork( child_env.process.terminate(code); // If the vfork contained a context-switching environment, exit now - if ctx.data(store).context_switching_environment.is_some() { + if ctx.data(&store).context_switching_environment.is_some() { tracing::error!( "Terminated a vfork in another way than exit or exec which is undefined behaviour. In this case the parent parent process will be terminated." ); - return Err(code.into()); + return (store, Err(code.into())); } // Jump back to the vfork point and current on execution @@ -455,11 +461,11 @@ fn resume_vfork( let rewind_stack = vfork.rewind_stack.freeze(); let store_data = vfork.store_data; - let ctx = ctx.env.clone().into_mut(store); + let ctx_cloned = ctx.env.clone().into_mut(&mut store); // Now rewind the previous stack and carry on from where we did the vfork let rewind_result = if vfork.is_64bit { crate::syscalls::rewind::( - ctx, + ctx_cloned, None, rewind_stack, store_data, @@ -470,7 +476,7 @@ fn resume_vfork( ) } else { crate::syscalls::rewind::( - ctx, + ctx_cloned, None, rewind_stack, store_data, @@ -482,13 +488,21 @@ fn resume_vfork( }; match rewind_result { - Errno::Success => Ok(Some(start.call(store, &[]))), + Errno::Success => { + let (store, result) = ContextSwitchingEnvironment::run_main_context( + ctx, + store, + start.clone(), + vec![], + ); + (store, Ok(Some(result))) + } err => { warn!("fork failed - could not rewind the stack - errno={}", err); - Err(err) + (store, Err(err)) } } } else { - Ok(None) + (store, Ok(None)) } } From d41ec07fac49095192434bb2bfd47e2c01248ab3 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 14:36:23 +0100 Subject: [PATCH 108/114] Add comment to justify usage of run_main_context --- lib/wasix/src/bin_factory/exec.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 9da63983185..75e7f254eaa 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -489,6 +489,8 @@ fn resume_vfork( match rewind_result { Errno::Success => { + // We should only get here, if the engine does not support context switching + // If the engine supports it, we should exit in the check a few lines above let (store, result) = ContextSwitchingEnvironment::run_main_context( ctx, store, From a363d4cb6a5f695d9e456edb85102fa5c972a867 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 14:41:45 +0100 Subject: [PATCH 109/114] Add a warning about potential memory usage to destroy_context --- lib/wasix/src/state/context_switching.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 34e5c615437..27a415120ec 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -196,6 +196,13 @@ impl ContextSwitchingEnvironment { } pub(crate) fn destroy_context(&self, target_context_id: &u64) -> bool { + // For now this only queues the context up for destruction by removing its unblocker. + // This will only cause destruction when the context_switch is called. + // That could cause memory issues if many contexts are created and destroyed without switching + // which could happen in applications that use contexts during setup, but not during main execution. + // We don't do immediate destruction because that would make this more complex, as it is essentially + // identical to switching to the target context + // TODO: Implement immediate destruction if the above becoms an issue self.inner .unblockers .write() From 15b69b1ad3923ed5fc1ab0a091b9f2181cfb94fa Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 14:51:06 +0100 Subject: [PATCH 110/114] Fix warning in context_create --- lib/wasix/src/syscalls/wasix/context_create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 1cab52a7355..896b67ed196 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -76,7 +76,7 @@ pub fn context_create( // Get the context-switching environment let Some(environment) = &data.context_switching_environment else { tracing::warn!( - "The WASIX context-switching API is only available in engines supporting async execution" + "The WASIX context-switching API is only available in a context-switching environment" ); return Ok(Errno::Notsup); }; From 2e7d73b5f47a265249f98a4af00d77d9f64e7c33 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 14:53:28 +0100 Subject: [PATCH 111/114] Fix comment in context_destroy --- lib/wasix/src/syscalls/wasix/context_destroy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wasix/src/syscalls/wasix/context_destroy.rs b/lib/wasix/src/syscalls/wasix/context_destroy.rs index 0f0112c7456..f7367c68104 100644 --- a/lib/wasix/src/syscalls/wasix/context_destroy.rs +++ b/lib/wasix/src/syscalls/wasix/context_destroy.rs @@ -27,7 +27,7 @@ pub fn context_destroy( Some(c) => c, None => { tracing::warn!( - "The WASIX context-switching API is only available in engines supporting async execution" + "The WASIX context-switching API is only available in a context-switching environment" ); return Ok(Errno::Notsup); } From dddae2ca5e68810e8708f652c49aa1cf545c775b Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 14:59:52 +0100 Subject: [PATCH 112/114] Add todo to remove the arc from ContextSwitchingEnvironment --- lib/wasix/src/state/context_switching.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 27a415120ec..c65825f371c 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -25,6 +25,9 @@ use wasmer_wasix_types::wasi::ExitCode; /// on a single host thread. #[derive(Debug)] pub(crate) struct ContextSwitchingEnvironment { + // TODO: We might be able to get rid of this Arc by passing an AsyncFunctionEnvMut to the functions instead + // We need to be super-careful about memory leaks by cyclical references if we do that change. + // The Arc allows us to have weak references to the ContextSwitchingEnvironment indepenend of the WasiEnv. inner: Arc, } From 3764871949b6b763e8d1c093d0114c927cc4ccb1 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 15:19:02 +0100 Subject: [PATCH 113/114] Fix test runner for context switching tests --- tests/wasix/context-switching/run.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/wasix/context-switching/run.sh b/tests/wasix/context-switching/run.sh index e94a7ec901f..29b942c0c9f 100755 --- a/tests/wasix/context-switching/run.sh +++ b/tests/wasix/context-switching/run.sh @@ -1,8 +1,8 @@ set -e -$WASMER -q run main.wasm --dir . -- basic_switching -$WASMER -q run main.wasm --dir . -- vfork_after_switching -$WASMER -q run main.wasm --dir . -- vfork_after_switching2 -$WASMER -q run main.wasm --dir . -- fork_after_switching -$WASMER -q run main.wasm --dir . -- fork_and_vfork_only_work_in_main_context -$WASMER -q run main.wasm --dir . -- posix_spawning_a_forking_subprocess_from_a_context \ No newline at end of file +$WASMER_RUN main.wasm --dir . -- basic_switching +$WASMER_RUN main.wasm --dir . -- vfork_after_switching +$WASMER_RUN main.wasm --dir . -- vfork_after_switching2 +$WASMER_RUN main.wasm --dir . -- fork_after_switching +$WASMER_RUN main.wasm --dir . -- fork_and_vfork_only_work_in_main_context +$WASMER_RUN main.wasm --dir . -- posix_spawning_a_forking_subprocess_from_a_context \ No newline at end of file From dd6166a87baed5735bf44899d1807c502481f82e Mon Sep 17 00:00:00 2001 From: Zebreus Date: Fri, 12 Dec 2025 16:05:35 +0100 Subject: [PATCH 114/114] Speed up wasixcc install --- .github/workflows/test.yaml | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6b29fd962f4..e0b93bc69cf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -175,18 +175,7 @@ jobs: sudo apt-get install -y git make lld curl - name: Install wasixcc - if: matrix.metadata.build != 'windows-x64' - run: | - export RUST_LOG=wasixcc=trace - export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} - cargo install --git https://github.com/wasix-org/wasixcc.git --rev 2df8392a1cb9e7b050130f6ac01cd84d9e39406f -F bin - mkdir wasixcc-install - wasixcc --install-executables $HOME/.wasixcc/bin - echo $HOME/.wasixcc/bin >> $GITHUB_PATH - wasixcc --download-sysroot v2025-12-10.1 - wasixcc --download-llvm - wasixcc --download-binaryen - echo $HOME/.wasixcc/binaryen/bin >> $GITHUB_PATH + uses: wasix-org/wasixcc - name: Tool versions run: | @@ -884,17 +873,7 @@ jobs: - name: Install wasixcc if: matrix.metadata.build != 'windows-x64' - run: | - export RUST_LOG=wasixcc=trace - export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} - cargo install --git https://github.com/wasix-org/wasixcc.git --rev 2df8392a1cb9e7b050130f6ac01cd84d9e39406f -F bin - mkdir wasixcc-install - wasixcc --install-executables $HOME/.wasixcc/bin - echo $HOME/.wasixcc/bin >> $GITHUB_PATH - wasixcc --download-sysroot v2025-12-10.1 - wasixcc --download-llvm - wasixcc --download-binaryen - echo $HOME/.wasixcc/binaryen/bin >> $GITHUB_PATH + uses: wasix-org/wasixcc - name: Install LLVM shell: bash