wasmer_wasix/bin_factory/
exec.rs

1#![allow(clippy::result_large_err)]
2use super::{BinaryPackage, BinaryPackageCommand};
3use crate::{
4    RewindState, SpawnError, WasiError, WasiRuntimeError,
5    os::task::{
6        TaskJoinHandle,
7        thread::{RewindResultType, WasiThreadRunGuard},
8    },
9    runtime::{
10        ModuleInput, TaintReason,
11        module_cache::HashedModuleData,
12        task_manager::{
13            TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, TaskWasmRunProperties,
14        },
15    },
16    state::context_switching::ContextSwitchingEnvironment,
17    syscalls::rewind_ext,
18};
19use crate::{Runtime, WasiEnv, WasiFunctionEnv};
20use std::{borrow::Cow, sync::Arc};
21use tracing::*;
22use virtual_mio::block_on;
23use wasmer::{Function, Memory32, Memory64, Module, RuntimeError, Store, Value};
24use wasmer_wasix_types::wasi::Errno;
25
26#[tracing::instrument(level = "trace", skip_all, fields(%name, package_id=%binary.id))]
27pub async fn spawn_exec(
28    binary: BinaryPackage,
29    name: &str,
30    env: WasiEnv,
31    runtime: &Arc<dyn Runtime + Send + Sync + 'static>,
32) -> Result<TaskJoinHandle, SpawnError> {
33    import_package_mounts(&env, &binary).await?;
34
35    let cmd = package_command_by_name(&binary, name)?;
36    let input = ModuleInput::Command(Cow::Borrowed(cmd));
37    let module = runtime.resolve_module(input, None, None).await?;
38
39    // Free the space used by the binary, since we don't need it
40    // any longer
41    drop(binary);
42
43    spawn_exec_module(module, env, runtime)
44}
45
46#[tracing::instrument(level = "trace", skip_all, fields(%name))]
47pub async fn spawn_exec_wasm(
48    wasm: HashedModuleData,
49    name: &str,
50    env: WasiEnv,
51    runtime: &Arc<dyn Runtime + Send + Sync + 'static>,
52) -> Result<TaskJoinHandle, SpawnError> {
53    let module = spawn_load_module(name, wasm, runtime).await?;
54
55    spawn_exec_module(module, env, runtime)
56}
57
58pub fn package_command_by_name<'a>(
59    pkg: &'a BinaryPackage,
60    name: &str,
61) -> Result<&'a BinaryPackageCommand, SpawnError> {
62    // If an explicit command is provided, use it.
63    // Otherwise, use the entrypoint.
64    // If no entrypoint exists, and the package has a single
65    // command, then use it. This is done for backwards
66    // compatibility.
67    let cmd = if let Some(cmd) = pkg.get_command(name) {
68        cmd
69    } else if let Some(cmd) = pkg.get_entrypoint_command() {
70        cmd
71    } else {
72        match pkg.commands.as_slice() {
73            // Package only has a single command, so use it.
74            [first] => first,
75            // Package either has no command, or has multiple commands, which
76            // would make the choice ambiguous, so fail.
77            _ => {
78                return Err(SpawnError::MissingEntrypoint {
79                    package_id: pkg.id.clone(),
80                });
81            }
82        }
83    };
84
85    Ok(cmd)
86}
87
88pub async fn spawn_load_module(
89    name: &str,
90    wasm: HashedModuleData,
91    runtime: &Arc<dyn Runtime + Send + Sync + 'static>,
92) -> Result<Module, SpawnError> {
93    match runtime.load_hashed_module(wasm, None).await {
94        Ok(module) => Ok(module),
95        Err(err) => {
96            tracing::error!(
97                command = name,
98                error = &err as &dyn std::error::Error,
99                "Failed to compile the module",
100            );
101            Err(err)
102        }
103    }
104}
105
106pub async fn import_package_mounts(
107    env: &WasiEnv,
108    binary: &BinaryPackage,
109) -> Result<(), SpawnError> {
110    // If the package mounts have not already been imported then do so.
111    env.state
112        .fs
113        .conditional_union(binary)
114        .await
115        .map_err(|err| {
116            tracing::warn!("failed to import package mounts - {err}");
117            SpawnError::FileSystemError(crate::ExtendedFsError::with_msg(
118                err,
119                "could not import package mounts",
120            ))
121        })?;
122    tracing::debug!("{:?}", env.state.fs);
123    Ok(())
124}
125
126pub fn spawn_exec_module(
127    module: Module,
128    env: WasiEnv,
129    runtime: &Arc<dyn Runtime + Send + Sync + 'static>,
130) -> Result<TaskJoinHandle, SpawnError> {
131    // Create a new task manager
132    let tasks = runtime.task_manager();
133
134    // Create the signaler
135    let pid = env.pid();
136
137    let join_handle = env.thread.join_handle();
138    {
139        // Create a thread that will run this process
140        let tasks_outer = tasks.clone();
141
142        tasks_outer
143            .task_wasm(
144                TaskWasm::new(Box::new(run_exec), env, module, true, true).with_pre_run(Box::new(
145                    |ctx, store| {
146                        let wasi_state = ctx.data(store).state.clone();
147                        Box::pin(async move {
148                            wasi_state.fs.close_cloexec_fds().await;
149                        })
150                    },
151                )),
152            )
153            .map_err(|err| {
154                error!("wasi[{}]::failed to launch module - {}", pid, err);
155                SpawnError::Other(Box::new(err))
156            })?
157    };
158
159    Ok(join_handle)
160}
161
162/// # SAFETY
163/// This must be executed from the same thread that owns the instance as
164/// otherwise it will cause a panic
165unsafe fn run_recycle(
166    callback: Option<Box<TaskWasmRecycle>>,
167    ctx: WasiFunctionEnv,
168    mut store: Store,
169) {
170    if let Some(callback) = callback {
171        let env = ctx.data_mut(&mut store);
172        let memory = unsafe { env.memory() }.clone();
173
174        let props = TaskWasmRecycleProperties {
175            env: env.clone(),
176            memory,
177            store,
178        };
179        callback(props);
180    }
181}
182
183pub fn run_exec(props: TaskWasmRunProperties) {
184    let ctx = props.ctx;
185    let mut store = props.store;
186
187    // Create the WasiFunctionEnv
188    let thread = WasiThreadRunGuard::new(ctx.data(&store).thread.clone());
189    let recycle = props.recycle;
190
191    // Perform the initialization
192    // If this module exports an _initialize function, run that first.
193    if let Ok(initialize) = ctx
194        .data(&store)
195        .inner()
196        .main_module_instance_handles()
197        .instance
198        .exports
199        .get_function("_initialize")
200        .cloned()
201    {
202        // This does not need a context switching environment as the documentation
203        // states that that is only available after the first call to main
204        let result = initialize.call(&mut store, &[]);
205
206        if let Err(err) = result {
207            thread.thread.set_status_finished(Err(err.into()));
208            ctx.data(&store)
209                .blocking_on_exit(Some(Errno::Noexec.into()));
210            unsafe { run_recycle(recycle, ctx, store) };
211            return;
212        }
213    }
214
215    // Bootstrap the process
216    // Unsafe: The bootstrap must be executed in the same thread that runs the
217    //         actual WASM code
218    let rewind_state = match unsafe { ctx.bootstrap(&mut store) } {
219        Ok(r) => r,
220        Err(err) => {
221            tracing::warn!("failed to bootstrap - {}", err);
222            thread.thread.set_status_finished(Err(err));
223            ctx.data(&store)
224                .blocking_on_exit(Some(Errno::Noexec.into()));
225            unsafe { run_recycle(recycle, ctx, store) };
226            return;
227        }
228    };
229
230    // If there is a start function
231    debug!("wasi[{}]::called main()", ctx.data(&store).pid());
232    // TODO: rewrite to use crate::run_wasi_func
233
234    // Call the module
235    call_module(ctx, store, thread, rewind_state, recycle);
236}
237
238fn get_start(ctx: &WasiFunctionEnv, store: &Store) -> Option<Function> {
239    ctx.data(store)
240        .inner()
241        .main_module_instance_handles()
242        .instance
243        .exports
244        .get_function("_start")
245        .cloned()
246        .ok()
247}
248
249/// Calls the module
250fn call_module(
251    ctx: WasiFunctionEnv,
252    mut store: Store,
253    handle: WasiThreadRunGuard,
254    rewind_state: Option<(RewindState, RewindResultType)>,
255    recycle: Option<Box<TaskWasmRecycle>>,
256) {
257    let env = ctx.data(&store);
258    let pid = env.pid();
259    let tasks = env.tasks().clone();
260    handle.thread.set_status_running();
261    let runtime = env.runtime.clone();
262
263    // If we need to rewind then do so
264    if let Some((rewind_state, rewind_result)) = rewind_state {
265        let mut ctx = ctx.env.clone().into_mut(&mut store);
266        if rewind_state.is_64bit {
267            let res = rewind_ext::<Memory64>(
268                &mut ctx,
269                Some(rewind_state.memory_stack),
270                rewind_state.rewind_stack,
271                rewind_state.store_data,
272                rewind_result,
273            );
274            if res != Errno::Success {
275                ctx.data().blocking_on_exit(Some(res.into()));
276                unsafe { run_recycle(recycle, WasiFunctionEnv { env: ctx.as_ref() }, store) };
277                return;
278            }
279        } else {
280            let res = rewind_ext::<Memory32>(
281                &mut ctx,
282                Some(rewind_state.memory_stack),
283                rewind_state.rewind_stack,
284                rewind_state.store_data,
285                rewind_result,
286            );
287            if res != Errno::Success {
288                ctx.data().blocking_on_exit(Some(res.into()));
289                unsafe { run_recycle(recycle, WasiFunctionEnv { env: ctx.as_ref() }, store) };
290                return;
291            }
292        };
293    }
294
295    // Invoke the start function
296    // Call the module
297    let Some(start) = get_start(&ctx, &store) else {
298        debug!("wasi[{}]::exec-failed: missing _start function", pid);
299        ctx.data(&store)
300            .blocking_on_exit(Some(Errno::Noexec.into()));
301        unsafe { run_recycle(recycle, ctx, store) };
302        return;
303    };
304
305    let (mut store, mut call_ret) =
306        ContextSwitchingEnvironment::run_main_context(&ctx, store, start.clone(), vec![]);
307
308    let mut store = loop {
309        // Technically, it's an error for a vfork to return from main, but anyway...
310        store = match resume_vfork(&ctx, store, &start, &call_ret) {
311            // A vfork was resumed, there may be another, so loop back
312            (store, Ok(Some(ret))) => {
313                call_ret = ret;
314                store
315            }
316
317            // An error was encountered when restoring from the vfork, report it
318            (store, Err(e)) => {
319                call_ret = Err(RuntimeError::user(Box::new(WasiError::Exit(e.into()))));
320                break store;
321            }
322
323            // No vfork, keep the call_ret value
324            (store, Ok(None)) => break store,
325        };
326    };
327
328    let ret = if let Err(err) = call_ret {
329        match err.downcast::<WasiError>() {
330            Ok(WasiError::Exit(code)) if code.is_success() => Ok(Errno::Success),
331            Ok(WasiError::ThreadExit) => Ok(Errno::Success),
332            Ok(WasiError::Exit(code)) => {
333                runtime.on_taint(TaintReason::NonZeroExitCode(code));
334                Err(WasiError::Exit(code).into())
335            }
336            Ok(WasiError::DeepSleep(deep)) => {
337                // Create the callback that will be invoked when the thread respawns after a deep sleep
338                let rewind = deep.rewind;
339                let respawn = {
340                    move |ctx, store, rewind_result| {
341                        // Call the thread
342                        call_module(
343                            ctx,
344                            store,
345                            handle,
346                            Some((rewind, RewindResultType::RewindWithResult(rewind_result))),
347                            recycle,
348                        );
349                    }
350                };
351
352                // Spawns the WASM process after a trigger
353                if let Err(err) = unsafe {
354                    tasks.resume_wasm_after_poller(Box::new(respawn), ctx, store, deep.trigger)
355                } {
356                    debug!("failed to go into deep sleep - {}", err);
357                }
358                return;
359            }
360            Ok(WasiError::UnknownWasiVersion) => {
361                debug!("failed as wasi version is unknown");
362                runtime.on_taint(TaintReason::UnknownWasiVersion);
363                Ok(Errno::Noexec)
364            }
365            Ok(WasiError::DlSymbolResolutionFailed(symbol)) => {
366                debug!("failed as a needed DL symbol could not be resolved");
367                runtime.on_taint(TaintReason::DlSymbolResolutionFailed(symbol.clone()));
368                Err(WasiError::DlSymbolResolutionFailed(symbol).into())
369            }
370            Err(err) => {
371                runtime.on_taint(TaintReason::RuntimeError(err.clone()));
372                Err(WasiRuntimeError::from(err))
373            }
374        }
375    } else {
376        Ok(Errno::Success)
377    };
378
379    let code = if let Err(err) = &ret {
380        match err.as_exit_code() {
381            Some(s) => s,
382            None => {
383                let err_display = err.display(&mut store);
384                if matches!(
385                    err,
386                    WasiRuntimeError::Runtime(runtime_err)
387                        if runtime_err.clone().to_trap() == Some(wasmer_types::TrapCode::HostInterrupt)
388                ) {
389                    debug!("{err_display}");
390                } else {
391                    error!("{err_display}");
392                    eprintln!("{err_display}");
393                }
394                Errno::Noexec.into()
395            }
396        }
397    } else {
398        Errno::Success.into()
399    };
400
401    // Cleanup the environment
402    ctx.data(&store).blocking_on_exit(Some(code));
403    unsafe { run_recycle(recycle, ctx, store) };
404
405    debug!("wasi[{pid}]::main() has exited with {code}");
406    handle.thread.set_status_finished(ret.map(|a| a.into()));
407}
408
409#[allow(clippy::type_complexity)]
410fn resume_vfork(
411    ctx: &WasiFunctionEnv,
412    mut store: Store,
413    start: &Function,
414    call_ret: &Result<Box<[Value]>, RuntimeError>,
415) -> (
416    Store,
417    Result<Option<Result<Box<[Value]>, RuntimeError>>, Errno>,
418) {
419    let (err, code) = match call_ret {
420        Ok(_) => (None, wasmer_wasix_types::wasi::ExitCode::from(0u16)),
421        Err(err) => match err.downcast_ref::<WasiError>() {
422            // If the child process is just deep sleeping, we don't restore the vfork
423            Some(WasiError::DeepSleep(..)) => return (store, Ok(None)),
424
425            Some(WasiError::Exit(code)) => (None, *code),
426            Some(WasiError::ThreadExit) => (None, wasmer_wasix_types::wasi::ExitCode::from(0u16)),
427            Some(WasiError::UnknownWasiVersion) => (None, Errno::Noexec.into()),
428            Some(WasiError::DlSymbolResolutionFailed(_)) => (None, Errno::Nolink.into()),
429            None => (
430                Some(WasiRuntimeError::from(err.clone())),
431                Errno::Unknown.into(),
432            ),
433        },
434    };
435
436    if let Some(mut vfork) = ctx.data_mut(&mut store).vfork.take() {
437        if let Some(err) = err {
438            error!(%err, "Error from child process");
439            eprintln!("{err}");
440        }
441
442        block_on(
443            unsafe { ctx.data(&store).get_memory_and_wasi_state(&store, 0) }
444                .1
445                .fs
446                .close_all(),
447        );
448
449        tracing::debug!(
450            pid = %ctx.data_mut(&mut store).process.pid(),
451            vfork_pid = %vfork.env.process.pid(),
452            "Resuming from vfork after child process was terminated"
453        );
454
455        // Restore the WasiEnv to the point when we vforked
456        vfork.env.swap_inner(ctx.data_mut(&mut store));
457        std::mem::swap(vfork.env.as_mut(), ctx.data_mut(&mut store));
458        let mut child_env = *vfork.env;
459        child_env.owned_handles.push(vfork.handle);
460
461        // Terminate the child process
462        child_env.process.terminate(code);
463
464        // If the vfork contained a context-switching environment, exit now
465        if ctx.data(&store).context_switching_environment.is_some() {
466            // We cannot recover from this situation when using context switching
467            tracing::error!(
468                "Terminated a vfork in another way than exit or exec which is undefined behaviour. In this case the parent process will be terminated."
469            );
470            return (store, Err(code.into()));
471        }
472        let Some(asyncify_info) = vfork.asyncify else {
473            // We can only recover from this situation when using asyncify-based vforking; since asyncify is not in use here, we cannot recover and must terminate the parent process
474            tracing::error!(
475                "Terminated a vfork in another way than exit or exec which is undefined behaviour. In this case the parent process will be terminated."
476            );
477            return (store, Err(code.into()));
478        };
479        // TODO: We can also only safely recover if we are not using nested calling
480        // TODO: Just delete this branch
481
482        // Jump back to the vfork point and continue execution
483        let child_pid = child_env.process.pid();
484        let rewind_stack = asyncify_info.rewind_stack.freeze();
485        let store_data = asyncify_info.store_data;
486
487        let ctx_cloned = ctx.env.clone().into_mut(&mut store);
488        // Now rewind the previous stack and carry on from where we did the vfork
489        let rewind_result = if asyncify_info.is_64bit {
490            crate::syscalls::rewind::<Memory64, _>(
491                ctx_cloned,
492                None,
493                rewind_stack,
494                store_data,
495                crate::syscalls::ForkResult {
496                    pid: child_pid.raw() as wasmer_wasix_types::wasi::Pid,
497                    ret: Errno::Success,
498                },
499            )
500        } else {
501            crate::syscalls::rewind::<Memory32, _>(
502                ctx_cloned,
503                None,
504                rewind_stack,
505                store_data,
506                crate::syscalls::ForkResult {
507                    pid: child_pid.raw() as wasmer_wasix_types::wasi::Pid,
508                    ret: Errno::Success,
509                },
510            )
511        };
512
513        match rewind_result {
514            Errno::Success => {
515                // We should only get here, if the engine does not support context switching
516                // If the engine supports it, we should exit in the check a few lines above
517                let (store, result) = ContextSwitchingEnvironment::run_main_context(
518                    ctx,
519                    store,
520                    start.clone(),
521                    vec![],
522                );
523                (store, Ok(Some(result)))
524            }
525            err => {
526                warn!("fork failed - could not rewind the stack - errno={}", err);
527                (store, Err(err))
528            }
529        }
530    } else {
531        (store, Ok(None))
532    }
533}