wasmer_wasix/bin_factory/
exec.rs

1#![allow(clippy::result_large_err)]
2use std::sync::Arc;
3
4use crate::{
5    RewindState, SpawnError, WasiError, WasiRuntimeError,
6    os::task::{
7        TaskJoinHandle,
8        thread::{RewindResultType, WasiThreadRunGuard},
9    },
10    runtime::{
11        TaintReason,
12        module_cache::HashedModuleData,
13        task_manager::{
14            TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, TaskWasmRunProperties,
15        },
16    },
17    syscalls::rewind_ext,
18};
19use tracing::*;
20use virtual_mio::InlineWaker;
21use wasmer::{Function, Memory32, Memory64, Module, RuntimeError, Store, Value};
22use wasmer_wasix_types::wasi::Errno;
23
24use super::{BinaryPackage, BinaryPackageCommand};
25use crate::{Runtime, WasiEnv, WasiFunctionEnv};
26
27#[tracing::instrument(level = "trace", skip_all, fields(%name, package_id=%binary.id))]
28pub async fn spawn_exec(
29    binary: BinaryPackage,
30    name: &str,
31    env: WasiEnv,
32    runtime: &Arc<dyn Runtime + Send + Sync + 'static>,
33) -> Result<TaskJoinHandle, SpawnError> {
34    spawn_union_fs(&env, &binary).await?;
35
36    let cmd = package_command_by_name(&binary, name)?;
37    let module = runtime.load_command_module(cmd).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 spawn_union_fs(env: &WasiEnv, binary: &BinaryPackage) -> Result<(), SpawnError> {
107    // If the file system has not already been union'ed then do so
108    env.state
109        .fs
110        .conditional_union(binary)
111        .await
112        .map_err(|err| {
113            tracing::warn!("failed to union file system - {err}");
114            SpawnError::FileSystemError(crate::ExtendedFsError::with_msg(
115                err,
116                "could not union filesystems",
117            ))
118        })?;
119    tracing::debug!("{:?}", env.state.fs);
120    Ok(())
121}
122
123pub fn spawn_exec_module(
124    module: Module,
125    env: WasiEnv,
126    runtime: &Arc<dyn Runtime + Send + Sync + 'static>,
127) -> Result<TaskJoinHandle, SpawnError> {
128    // Create a new task manager
129    let tasks = runtime.task_manager();
130
131    // Create the signaler
132    let pid = env.pid();
133
134    let join_handle = env.thread.join_handle();
135    {
136        // Create a thread that will run this process
137        let tasks_outer = tasks.clone();
138
139        tasks_outer
140            .task_wasm(
141                TaskWasm::new(Box::new(run_exec), env, module, true, true).with_pre_run(Box::new(
142                    |ctx, store| {
143                        Box::pin(async move {
144                            ctx.data(store).state.fs.close_cloexec_fds().await;
145                        })
146                    },
147                )),
148            )
149            .map_err(|err| {
150                error!("wasi[{}]::failed to launch module - {}", pid, err);
151                SpawnError::Other(Box::new(err))
152            })?
153    };
154
155    Ok(join_handle)
156}
157
158/// # SAFETY
159/// This must be executed from the same thread that owns the instance as
160/// otherwise it will cause a panic
161unsafe fn run_recycle(
162    callback: Option<Box<TaskWasmRecycle>>,
163    ctx: WasiFunctionEnv,
164    mut store: Store,
165) {
166    if let Some(callback) = callback {
167        let env = ctx.data_mut(&mut store);
168        let memory = unsafe { env.memory() }.clone();
169
170        let props = TaskWasmRecycleProperties {
171            env: env.clone(),
172            memory,
173            store,
174        };
175        callback(props);
176    }
177}
178
179pub fn run_exec(props: TaskWasmRunProperties) {
180    let ctx = props.ctx;
181    let mut store = props.store;
182
183    // Create the WasiFunctionEnv
184    let thread = WasiThreadRunGuard::new(ctx.data(&store).thread.clone());
185    let recycle = props.recycle;
186
187    // Perform the initialization
188    let ctx = {
189        // If this module exports an _initialize function, run that first.
190        if let Ok(initialize) = ctx
191            .data(&store)
192            .inner()
193            .main_module_instance_handles()
194            .instance
195            .exports
196            .get_function("_initialize")
197        {
198            let initialize = initialize.clone();
199            if let Err(err) = initialize.call(&mut store, &[]) {
200                thread.thread.set_status_finished(Err(err.into()));
201                ctx.data(&store)
202                    .blocking_on_exit(Some(Errno::Noexec.into()));
203                unsafe { run_recycle(recycle, ctx, store) };
204                return;
205            }
206        }
207
208        WasiFunctionEnv { env: ctx.env }
209    };
210
211    // Bootstrap the process
212    // Unsafe: The bootstrap must be executed in the same thread that runs the
213    //         actual WASM code
214    let rewind_state = match unsafe { ctx.bootstrap(&mut store) } {
215        Ok(r) => r,
216        Err(err) => {
217            tracing::warn!("failed to bootstrap - {}", err);
218            thread.thread.set_status_finished(Err(err));
219            ctx.data(&store)
220                .blocking_on_exit(Some(Errno::Noexec.into()));
221            unsafe { run_recycle(recycle, ctx, store) };
222            return;
223        }
224    };
225
226    // If there is a start function
227    debug!("wasi[{}]::called main()", ctx.data(&store).pid());
228    // TODO: rewrite to use crate::run_wasi_func
229
230    // Call the module
231    call_module(ctx, store, thread, rewind_state, recycle);
232}
233
234fn get_start(ctx: &WasiFunctionEnv, store: &Store) -> Option<Function> {
235    ctx.data(store)
236        .inner()
237        .main_module_instance_handles()
238        .instance
239        .exports
240        .get_function("_start")
241        .cloned()
242        .ok()
243}
244
245/// Calls the module
246fn call_module(
247    ctx: WasiFunctionEnv,
248    mut store: Store,
249    handle: WasiThreadRunGuard,
250    rewind_state: Option<(RewindState, RewindResultType)>,
251    recycle: Option<Box<TaskWasmRecycle>>,
252) {
253    let env = ctx.data(&store);
254    let pid = env.pid();
255    let tasks = env.tasks().clone();
256    handle.thread.set_status_running();
257    let runtime = env.runtime.clone();
258
259    // If we need to rewind then do so
260    if let Some((rewind_state, rewind_result)) = rewind_state {
261        let mut ctx = ctx.env.clone().into_mut(&mut store);
262        if rewind_state.is_64bit {
263            let res = rewind_ext::<Memory64>(
264                &mut ctx,
265                Some(rewind_state.memory_stack),
266                rewind_state.rewind_stack,
267                rewind_state.store_data,
268                rewind_result,
269            );
270            if res != Errno::Success {
271                ctx.data().blocking_on_exit(Some(res.into()));
272                unsafe { run_recycle(recycle, WasiFunctionEnv { env: ctx.as_ref() }, store) };
273                return;
274            }
275        } else {
276            let res = rewind_ext::<Memory32>(
277                &mut ctx,
278                Some(rewind_state.memory_stack),
279                rewind_state.rewind_stack,
280                rewind_state.store_data,
281                rewind_result,
282            );
283            if res != Errno::Success {
284                ctx.data().blocking_on_exit(Some(res.into()));
285                unsafe { run_recycle(recycle, WasiFunctionEnv { env: ctx.as_ref() }, store) };
286                return;
287            }
288        };
289    }
290
291    // Invoke the start function
292    let ret = {
293        // Call the module
294        let Some(start) = get_start(&ctx, &store) else {
295            debug!("wasi[{}]::exec-failed: missing _start function", pid);
296            ctx.data(&store)
297                .blocking_on_exit(Some(Errno::Noexec.into()));
298            unsafe { run_recycle(recycle, ctx, store) };
299            return;
300        };
301
302        let mut call_ret = start.call(&mut store, &[]);
303
304        loop {
305            // Technically, it's an error for a vfork to return from main, but anyway...
306            match resume_vfork(&ctx, &mut store, &start, &call_ret) {
307                // A vfork was resumed, there may be another, so loop back
308                Ok(Some(ret)) => call_ret = ret,
309
310                // An error was encountered when restoring from the vfork, report it
311                Err(e) => {
312                    call_ret = Err(RuntimeError::user(Box::new(WasiError::Exit(e.into()))));
313                    break;
314                }
315
316                // No vfork, keep the call_ret value
317                Ok(None) => break,
318            }
319        }
320
321        if let Err(err) = call_ret {
322            match err.downcast::<WasiError>() {
323                Ok(WasiError::Exit(code)) if code.is_success() => Ok(Errno::Success),
324                Ok(WasiError::ThreadExit) => Ok(Errno::Success),
325                Ok(WasiError::Exit(code)) => {
326                    runtime.on_taint(TaintReason::NonZeroExitCode(code));
327                    Err(WasiError::Exit(code).into())
328                }
329                Ok(WasiError::DeepSleep(deep)) => {
330                    // Create the callback that will be invoked when the thread respawns after a deep sleep
331                    let rewind = deep.rewind;
332                    let respawn = {
333                        move |ctx, store, rewind_result| {
334                            // Call the thread
335                            call_module(
336                                ctx,
337                                store,
338                                handle,
339                                Some((rewind, RewindResultType::RewindWithResult(rewind_result))),
340                                recycle,
341                            );
342                        }
343                    };
344
345                    // Spawns the WASM process after a trigger
346                    if let Err(err) = unsafe {
347                        tasks.resume_wasm_after_poller(Box::new(respawn), ctx, store, deep.trigger)
348                    } {
349                        debug!("failed to go into deep sleep - {}", err);
350                    }
351                    return;
352                }
353                Ok(WasiError::UnknownWasiVersion) => {
354                    debug!("failed as wasi version is unknown");
355                    runtime.on_taint(TaintReason::UnknownWasiVersion);
356                    Ok(Errno::Noexec)
357                }
358                Ok(WasiError::DlSymbolResolutionFailed(symbol)) => {
359                    debug!("failed as a needed DL symbol could not be resolved");
360                    runtime.on_taint(TaintReason::DlSymbolResolutionFailed(symbol.clone()));
361                    Err(WasiError::DlSymbolResolutionFailed(symbol).into())
362                }
363                Err(err) => {
364                    runtime.on_taint(TaintReason::RuntimeError(err.clone()));
365                    Err(WasiRuntimeError::from(err))
366                }
367            }
368        } else {
369            Ok(Errno::Success)
370        }
371    };
372
373    let code = if let Err(err) = &ret {
374        match err.as_exit_code() {
375            Some(s) => s,
376            None => {
377                let err_display = err.display(&mut store);
378                error!("{err_display}");
379                eprintln!("{err_display}");
380                Errno::Noexec.into()
381            }
382        }
383    } else {
384        Errno::Success.into()
385    };
386
387    // Cleanup the environment
388    ctx.data(&store).blocking_on_exit(Some(code));
389    unsafe { run_recycle(recycle, ctx, store) };
390
391    debug!("wasi[{pid}]::main() has exited with {code}");
392    handle.thread.set_status_finished(ret.map(|a| a.into()));
393}
394
395#[allow(clippy::type_complexity)]
396fn resume_vfork(
397    ctx: &WasiFunctionEnv,
398    store: &mut Store,
399    start: &Function,
400    call_ret: &Result<Box<[Value]>, RuntimeError>,
401) -> Result<Option<Result<Box<[Value]>, RuntimeError>>, Errno> {
402    let (err, code) = match call_ret {
403        Ok(_) => (None, wasmer_wasix_types::wasi::ExitCode::from(0u16)),
404        Err(err) => match err.downcast_ref::<WasiError>() {
405            // If the child process is just deep sleeping, we don't restore the vfork
406            Some(WasiError::DeepSleep(..)) => return Ok(None),
407
408            Some(WasiError::Exit(code)) => (None, *code),
409            Some(WasiError::ThreadExit) => (None, wasmer_wasix_types::wasi::ExitCode::from(0u16)),
410            Some(WasiError::UnknownWasiVersion) => (None, Errno::Noexec.into()),
411            Some(WasiError::DlSymbolResolutionFailed(_)) => (None, Errno::Nolink.into()),
412            None => (
413                Some(WasiRuntimeError::from(err.clone())),
414                Errno::Unknown.into(),
415            ),
416        },
417    };
418
419    if let Some(mut vfork) = ctx.data_mut(store).vfork.take() {
420        if let Some(err) = err {
421            error!(%err, "Error from child process");
422            eprintln!("{err}");
423        }
424
425        InlineWaker::block_on(
426            unsafe { ctx.data(store).get_memory_and_wasi_state(store, 0) }
427                .1
428                .fs
429                .close_all(),
430        );
431
432        tracing::debug!(
433            pid = %ctx.data_mut(store).process.pid(),
434            vfork_pid = %vfork.env.process.pid(),
435            "Resuming from vfork after child process was terminated"
436        );
437
438        // Restore the WasiEnv to the point when we vforked
439        vfork.env.swap_inner(ctx.data_mut(store));
440        std::mem::swap(vfork.env.as_mut(), ctx.data_mut(store));
441        let mut child_env = *vfork.env;
442        child_env.owned_handles.push(vfork.handle);
443
444        // Terminate the child process
445        child_env.process.terminate(code);
446
447        // Jump back to the vfork point and current on execution
448        let child_pid = child_env.process.pid();
449        let rewind_stack = vfork.rewind_stack.freeze();
450        let store_data = vfork.store_data;
451
452        let ctx = ctx.env.clone().into_mut(store);
453        // Now rewind the previous stack and carry on from where we did the vfork
454        let rewind_result = if vfork.is_64bit {
455            crate::syscalls::rewind::<Memory64, _>(
456                ctx,
457                None,
458                rewind_stack,
459                store_data,
460                crate::syscalls::ForkResult {
461                    pid: child_pid.raw() as wasmer_wasix_types::wasi::Pid,
462                    ret: Errno::Success,
463                },
464            )
465        } else {
466            crate::syscalls::rewind::<Memory32, _>(
467                ctx,
468                None,
469                rewind_stack,
470                store_data,
471                crate::syscalls::ForkResult {
472                    pid: child_pid.raw() as wasmer_wasix_types::wasi::Pid,
473                    ret: Errno::Success,
474                },
475            )
476        };
477
478        match rewind_result {
479            Errno::Success => Ok(Some(start.call(store, &[]))),
480            err => {
481                warn!("fork failed - could not rewind the stack - errno={}", err);
482                Err(err)
483            }
484        }
485    } else {
486        Ok(None)
487    }
488}