11use crate :: {
2- WasiFunctionEnv ,
2+ WasiError , WasiFunctionEnv ,
33 utils:: thread_local_executor:: {
44 ThreadLocalExecutor , ThreadLocalSpawner , ThreadLocalSpawnerError ,
55 } ,
@@ -19,6 +19,7 @@ use std::{
1919use thiserror:: Error ;
2020use tracing:: trace;
2121use wasmer:: { RuntimeError , Store } ;
22+ use wasmer_wasix_types:: wasi:: ExitCode ;
2223
2324/// The context-switching environment represents all state for WASIX context-switching
2425/// on a single host thread.
@@ -77,6 +78,16 @@ impl Drop for ContextCanceled {
7778 }
7879}
7980
81+ /// Contexts will trap with this error as a RuntimeError::user when they entrypoint returns
82+ ///
83+ /// It is not allowed for context entrypoints to return normally, they must always
84+ /// either get destroyed while suspended or trap with an error (like ContextCanceled)
85+ ///
86+ /// This error will be picked up by the main context and cause it to trap as well.
87+ #[ derive( Error , Debug ) ]
88+ #[ error( "The entrypoint of context {0} returned which is not allowed" ) ]
89+ pub struct ContextEntrypointReturned ( u64 ) ;
90+
8091impl ContextSwitchingEnvironment {
8192 fn new ( spawner : ThreadLocalSpawner ) -> Self {
8293 Self {
@@ -115,6 +126,22 @@ impl ContextSwitchingEnvironment {
115126 let store_async = store. into_async ( ) ;
116127 // Run function with the spawner
117128 let result = local_executor. run_until ( entrypoint. call_async ( & store_async, params) ) ;
129+
130+ // Process if this was terminated by a context entrypoint returning
131+ let result = match & result {
132+ Err ( e) => match e. downcast_ref :: < ContextEntrypointReturned > ( ) {
133+ Some ( ContextEntrypointReturned ( id) ) => {
134+ // Context entrypoint returned, which is not allowed
135+ // Exit with code 129
136+ tracing:: error!( "The entrypoint of context {id} returned which is not allowed" ) ;
137+ Err ( RuntimeError :: user (
138+ WasiError :: Exit ( ExitCode :: from ( 129 ) ) . into ( ) ,
139+ ) )
140+ }
141+ _ => result,
142+ } ,
143+ _ => result,
144+ } ;
118145 // Drop the executor to ensure all spawned tasks are dropped, so we have no references to the StoreAsync left
119146 drop ( local_executor) ;
120147
@@ -251,10 +278,9 @@ impl ContextSwitchingEnvironment {
251278 /// Otherwise, the error will be propagated to the main context.
252279 ///
253280 /// If the context is cancelled before it is unblocked, the entrypoint will not be called
254- pub ( crate ) fn create_context < T , F > ( & self , entrypoint : T ) -> u64
281+ pub ( crate ) fn create_context < F > ( & self , entrypoint : F ) -> u64
255282 where
256- T : FnOnce ( u64 ) -> F + ' static ,
257- F : Future < Output = RuntimeError > + ' static ,
283+ F : Future < Output = Result < ( ) , RuntimeError > > + ' static ,
258284 {
259285 // Create a new context ID
260286 let new_context_id = self
@@ -320,12 +346,19 @@ impl ContextSwitchingEnvironment {
320346 drop ( inner) ;
321347
322348 // Launch the context entrypoint
323- let launch_result = entrypoint ( new_context_id) . await ;
349+ let entrypoint_result = entrypoint. await ;
350+
351+ // If that function returns, we need to resume the main context with an error
352+ // Take the underlying error, or create a new error if the context returned a value
353+ let entrypoint_result = entrypoint_result. map_or_else (
354+ |e| e,
355+ |_| RuntimeError :: user ( ContextEntrypointReturned ( new_context_id) . into ( ) ) ,
356+ ) ;
324357
325358 // If that function returns something went wrong.
326359 // If it's a cancellation, we can just let this context run out.
327360 // If it's another error, we resume the main context with the error
328- let error = match launch_result . downcast :: < ContextCanceled > ( ) {
361+ let error = match entrypoint_result . downcast :: < ContextCanceled > ( ) {
329362 Ok ( canceled) => {
330363 tracing:: trace!( "Context {new_context_id} was successfully destroyed" ) ;
331364 // We know what we are doing, so we can prevent the panic on drop
0 commit comments