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