wasmer_wasix/state/
builder.rs

1//! Builder system for configuring a [`WasiState`] and creating it.
2
3use std::{
4    collections::{HashMap, HashSet},
5    path::{Path, PathBuf},
6    sync::Arc,
7};
8
9use rand::Rng;
10use thiserror::Error;
11use virtual_fs::{ArcFile, FileSystem, FsError, TmpFileSystem, VirtualFile};
12use wasmer::{AsStoreMut, Engine, Instance, Module};
13use wasmer_config::package::PackageId;
14
15#[cfg(feature = "journal")]
16use crate::journal::{DynJournal, DynReadableJournal, SnapshotTrigger};
17use crate::{
18    Runtime, WasiEnv, WasiFunctionEnv, WasiRuntimeError, WasiThreadError,
19    bin_factory::{BinFactory, BinaryPackage},
20    capabilities::Capabilities,
21    fs::{WasiFs, WasiFsRoot, WasiInodes},
22    os::task::control_plane::{ControlPlaneConfig, ControlPlaneError, WasiControlPlane},
23    state::WasiState,
24    syscalls::types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO},
25};
26use wasmer_types::ModuleHash;
27use wasmer_wasix_types::wasi::SignalDisposition;
28
29use super::env::WasiEnvInit;
30
31// FIXME: additional import support was broken and has been removed. We need to re-introduce
32// it in a way that works with multi-threaded WASIX apps.
33/// Builder API for configuring a [`WasiEnv`] environment needed to run WASI modules.
34///
35/// Usage:
36/// ```no_run
37/// # use wasmer_wasix::{WasiEnv, WasiStateCreationError};
38/// # fn main() -> Result<(), WasiStateCreationError> {
39/// let mut state_builder = WasiEnv::builder("wasi-prog-name");
40/// state_builder
41///    .env("ENV_VAR", "ENV_VAL")
42///    .arg("--verbose")
43///    .preopen_dir("src")?
44///    .map_dir("name_wasi_sees", "path/on/host/fs")?
45///    .build_init()?;
46/// # Ok(())
47/// # }
48/// ```
49#[derive(Default)]
50pub struct WasiEnvBuilder {
51    /// Name of entry function. Defaults to running `_start` if not specified.
52    pub(super) entry_function: Option<String>,
53    /// Command line arguments.
54    pub(super) args: Vec<String>,
55    /// Environment variables.
56    pub(super) envs: Vec<(String, Vec<u8>)>,
57    /// Signals that should get their handler overridden.
58    pub(super) signals: Vec<SignalDisposition>,
59    /// Pre-opened directories that will be accessible from WASI.
60    pub(super) preopens: Vec<PreopenedDir>,
61    /// Pre-opened virtual directories that will be accessible from WASI.
62    vfs_preopens: Vec<String>,
63    #[allow(clippy::type_complexity)]
64    pub(super) setup_fs_fn:
65        Option<Box<dyn Fn(&WasiInodes, &mut WasiFs) -> Result<(), String> + Send>>,
66    pub(super) stdout: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
67    pub(super) stderr: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
68    pub(super) stdin: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
69    pub(super) fs: Option<WasiFsRoot>,
70    pub(super) engine: Option<Engine>,
71    pub(super) runtime: Option<Arc<dyn crate::Runtime + Send + Sync + 'static>>,
72    pub(super) current_dir: Option<PathBuf>,
73
74    /// List of webc dependencies to be injected.
75    pub(super) uses: Vec<BinaryPackage>,
76
77    pub(super) included_packages: HashSet<PackageId>,
78
79    pub(super) module_hash: Option<ModuleHash>,
80
81    /// List of host commands to map into the WASI instance.
82    pub(super) map_commands: HashMap<String, PathBuf>,
83
84    pub(super) capabilites: Capabilities,
85
86    #[cfg(feature = "journal")]
87    pub(super) snapshot_on: Vec<SnapshotTrigger>,
88
89    #[cfg(feature = "journal")]
90    pub(super) snapshot_interval: Option<std::time::Duration>,
91
92    #[cfg(feature = "journal")]
93    pub(super) stop_running_after_snapshot: bool,
94
95    #[cfg(feature = "journal")]
96    pub(super) read_only_journals: Vec<Arc<DynReadableJournal>>,
97
98    #[cfg(feature = "journal")]
99    pub(super) writable_journals: Vec<Arc<DynJournal>>,
100
101    pub(super) skip_stdio_during_bootstrap: bool,
102
103    #[cfg(feature = "ctrlc")]
104    pub(super) attach_ctrl_c: bool,
105}
106
107impl std::fmt::Debug for WasiEnvBuilder {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        // TODO: update this when stable
110        f.debug_struct("WasiEnvBuilder")
111            .field("entry_function", &self.entry_function)
112            .field("args", &self.args)
113            .field("envs", &self.envs)
114            .field("signals", &self.signals)
115            .field("preopens", &self.preopens)
116            .field("uses", &self.uses)
117            .field("setup_fs_fn exists", &self.setup_fs_fn.is_some())
118            .field("stdout_override exists", &self.stdout.is_some())
119            .field("stderr_override exists", &self.stderr.is_some())
120            .field("stdin_override exists", &self.stdin.is_some())
121            .field("engine_override_exists", &self.engine.is_some())
122            .field("runtime_override_exists", &self.runtime.is_some())
123            .finish()
124    }
125}
126
127/// Error type returned when bad data is given to [`WasiEnvBuilder`].
128#[derive(Error, Debug, Clone, PartialEq, Eq)]
129pub enum WasiStateCreationError {
130    #[error("bad environment variable format: `{0}`")]
131    EnvironmentVariableFormatError(String),
132    #[error("argument contains null byte: `{0}`")]
133    ArgumentContainsNulByte(String),
134    #[error("preopened directory not found: `{0}`")]
135    PreopenedDirectoryNotFound(PathBuf),
136    #[error("preopened directory error: `{0}`")]
137    PreopenedDirectoryError(String),
138    #[error("mapped dir alias has wrong format: `{0}`")]
139    MappedDirAliasFormattingError(String),
140    #[error("wasi filesystem creation error: `{0}`")]
141    WasiFsCreationError(String),
142    #[error("wasi filesystem setup error: `{0}`")]
143    WasiFsSetupError(String),
144    #[error(transparent)]
145    FileSystemError(#[from] FsError),
146    #[error("wasi inherit error: `{0}`")]
147    WasiInheritError(String),
148    #[error("wasi include package: `{0}`")]
149    WasiIncludePackageError(String),
150    #[error("control plane error")]
151    ControlPlane(#[from] ControlPlaneError),
152}
153
154fn validate_mapped_dir_alias(alias: &str) -> Result<(), WasiStateCreationError> {
155    if !alias.bytes().all(|b| b != b'\0') {
156        return Err(WasiStateCreationError::MappedDirAliasFormattingError(
157            format!("Alias \"{alias}\" contains a nul byte"),
158        ));
159    }
160
161    Ok(())
162}
163
164pub type SetupFsFn = Box<dyn Fn(&WasiInodes, &mut WasiFs) -> Result<(), String> + Send>;
165
166// TODO add other WasiFS APIs here like swapping out stdout, for example (though we need to
167// return stdout somehow, it's unclear what that API should look like)
168impl WasiEnvBuilder {
169    /// Creates an empty [`WasiEnvBuilder`].
170    pub fn new(program_name: impl Into<String>) -> Self {
171        WasiEnvBuilder {
172            args: vec![program_name.into()],
173            ..WasiEnvBuilder::default()
174        }
175    }
176
177    /// Attaches a ctrl-c handler which will send signals to the
178    /// process rather than immediately termiante it
179    #[cfg(feature = "ctrlc")]
180    pub fn attach_ctrl_c(mut self) -> Self {
181        self.attach_ctrl_c = true;
182        self
183    }
184
185    /// Add an environment variable pair.
186    ///
187    /// Both the key and value of an environment variable must not
188    /// contain a nul byte (`0x0`), and the key must not contain the
189    /// `=` byte (`0x3d`).
190    pub fn env<Key, Value>(mut self, key: Key, value: Value) -> Self
191    where
192        Key: AsRef<[u8]>,
193        Value: AsRef<[u8]>,
194    {
195        self.add_env(key, value);
196        self
197    }
198
199    /// Add an environment variable pair.
200    ///
201    /// Both the key and value of an environment variable must not
202    /// contain a nul byte (`0x0`), and the key must not contain the
203    /// `=` byte (`0x3d`).
204    pub fn add_env<Key, Value>(&mut self, key: Key, value: Value)
205    where
206        Key: AsRef<[u8]>,
207        Value: AsRef<[u8]>,
208    {
209        self.envs.push((
210            String::from_utf8_lossy(key.as_ref()).to_string(),
211            value.as_ref().to_vec(),
212        ));
213    }
214
215    /// Add multiple environment variable pairs.
216    ///
217    /// Both the key and value of the environment variables must not
218    /// contain a nul byte (`0x0`), and the key must not contain the
219    /// `=` byte (`0x3d`).
220    pub fn envs<I, Key, Value>(mut self, env_pairs: I) -> Self
221    where
222        I: IntoIterator<Item = (Key, Value)>,
223        Key: AsRef<[u8]>,
224        Value: AsRef<[u8]>,
225    {
226        self.add_envs(env_pairs);
227
228        self
229    }
230
231    /// Add multiple environment variable pairs.
232    ///
233    /// Both the key and value of the environment variables must not
234    /// contain a nul byte (`0x0`), and the key must not contain the
235    /// `=` byte (`0x3d`).
236    pub fn add_envs<I, Key, Value>(&mut self, env_pairs: I)
237    where
238        I: IntoIterator<Item = (Key, Value)>,
239        Key: AsRef<[u8]>,
240        Value: AsRef<[u8]>,
241    {
242        for (key, value) in env_pairs {
243            self.add_env(key, value);
244        }
245    }
246
247    /// Get a reference to the configured environment variables.
248    pub fn get_env(&self) -> &[(String, Vec<u8>)] {
249        &self.envs
250    }
251
252    /// Get a mutable reference to the configured environment variables.
253    pub fn get_env_mut(&mut self) -> &mut Vec<(String, Vec<u8>)> {
254        &mut self.envs
255    }
256
257    /// Add a signal handler override.
258    pub fn signal(mut self, sig_action: SignalDisposition) -> Self {
259        self.add_signal(sig_action);
260        self
261    }
262
263    /// Add a signal handler override.
264    pub fn add_signal(&mut self, sig_action: SignalDisposition) {
265        self.signals.push(sig_action);
266    }
267
268    /// Add multiple signal handler overrides.
269    pub fn signals<I>(mut self, signal_pairs: I) -> Self
270    where
271        I: IntoIterator<Item = SignalDisposition>,
272    {
273        self.add_signals(signal_pairs);
274
275        self
276    }
277
278    /// Add multiple signal handler overrides.
279    pub fn add_signals<I>(&mut self, signal_pairs: I)
280    where
281        I: IntoIterator<Item = SignalDisposition>,
282    {
283        for sig in signal_pairs {
284            self.add_signal(sig);
285        }
286    }
287
288    /// Get a reference to the configured signal handler overrides.
289    pub fn get_signals(&self) -> &[SignalDisposition] {
290        &self.signals
291    }
292
293    /// Get a mutable reference to the configured signalironment variables.
294    pub fn get_signals_mut(&mut self) -> &mut Vec<SignalDisposition> {
295        &mut self.signals
296    }
297
298    pub fn entry_function<S>(mut self, entry_function: S) -> Self
299    where
300        S: AsRef<str>,
301    {
302        self.set_entry_function(entry_function);
303        self
304    }
305
306    pub fn set_entry_function<S>(&mut self, entry_function: S)
307    where
308        S: AsRef<str>,
309    {
310        self.entry_function = Some(entry_function.as_ref().to_owned());
311    }
312
313    /// Add an argument.
314    ///
315    /// Arguments must not contain the nul (0x0) byte
316    // TODO: should take Into<Vec<u8>>
317    pub fn arg<V>(mut self, arg: V) -> Self
318    where
319        V: AsRef<[u8]>,
320    {
321        self.add_arg(arg);
322        self
323    }
324
325    /// Add an argument.
326    ///
327    /// Arguments must not contain the nul (0x0) byte.
328    // TODO: should take Into<Vec<u8>>
329    pub fn add_arg<V>(&mut self, arg: V)
330    where
331        V: AsRef<[u8]>,
332    {
333        self.args
334            .push(String::from_utf8_lossy(arg.as_ref()).to_string());
335    }
336
337    /// Add multiple arguments.
338    ///
339    /// Arguments must not contain the nul (0x0) byte
340    pub fn args<I, Arg>(mut self, args: I) -> Self
341    where
342        I: IntoIterator<Item = Arg>,
343        Arg: AsRef<[u8]>,
344    {
345        self.add_args(args);
346
347        self
348    }
349
350    /// Add multiple arguments.
351    ///
352    /// Arguments must not contain the nul (0x0) byte
353    pub fn add_args<I, Arg>(&mut self, args: I)
354    where
355        I: IntoIterator<Item = Arg>,
356        Arg: AsRef<[u8]>,
357    {
358        for arg in args {
359            self.add_arg(arg);
360        }
361    }
362
363    /// Get a reference to the configured arguments.
364    pub fn get_args(&self) -> &[String] {
365        &self.args
366    }
367
368    /// Get a mutable reference to the configured arguments.
369    pub fn get_args_mut(&mut self) -> &mut Vec<String> {
370        &mut self.args
371    }
372
373    /// Adds a container this module inherits from.
374    ///
375    /// This will make all of the container's files and commands available to the
376    /// resulting WASI instance.
377    pub fn use_webc(mut self, pkg: BinaryPackage) -> Self {
378        self.add_webc(pkg);
379        self
380    }
381
382    /// Sets the module hash for the running process. This ensures that the journal
383    /// can restore the records for the right module. If no module hash is supplied
384    /// then the process will start with a random module hash.
385    pub fn set_module_hash(&mut self, hash: ModuleHash) -> &mut Self {
386        self.module_hash.replace(hash);
387        self
388    }
389
390    /// Adds a container this module inherits from.
391    ///
392    /// This will make all of the container's files and commands available to the
393    /// resulting WASI instance.
394    pub fn add_webc(&mut self, pkg: BinaryPackage) -> &mut Self {
395        self.uses.push(pkg);
396        self
397    }
398
399    /// Adds a package that is already included in the [`WasiEnvBuilder`] filesystem.
400    /// These packages will not be merged to the final filesystem since they are already included.
401    pub fn include_package(&mut self, pkg_id: PackageId) -> &mut Self {
402        self.included_packages.insert(pkg_id);
403        self
404    }
405
406    /// Adds packages that is already included in the [`WasiEnvBuilder`] filesystem.
407    /// These packages will not be merged to the final filesystem since they are already included.
408    pub fn include_packages(&mut self, pkg_ids: impl IntoIterator<Item = PackageId>) -> &mut Self {
409        self.included_packages.extend(pkg_ids);
410
411        self
412    }
413
414    /// Adds a list of other containers this module inherits from.
415    ///
416    /// This will make all of the container's files and commands available to the
417    /// resulting WASI instance.
418    pub fn uses<I>(mut self, uses: I) -> Self
419    where
420        I: IntoIterator<Item = BinaryPackage>,
421    {
422        for pkg in uses {
423            self.add_webc(pkg);
424        }
425        self
426    }
427
428    /// Map an atom to a local binary
429    pub fn map_command<Name, Target>(mut self, name: Name, target: Target) -> Self
430    where
431        Name: AsRef<str>,
432        Target: AsRef<str>,
433    {
434        self.add_mapped_command(name, target);
435        self
436    }
437
438    /// Map an atom to a local binary
439    pub fn add_mapped_command<Name, Target>(&mut self, name: Name, target: Target)
440    where
441        Name: AsRef<str>,
442        Target: AsRef<str>,
443    {
444        let path_buf = PathBuf::from(target.as_ref().to_string());
445        self.map_commands
446            .insert(name.as_ref().to_string(), path_buf);
447    }
448
449    /// Maps a series of atoms to the local binaries
450    pub fn map_commands<I, Name, Target>(mut self, map_commands: I) -> Self
451    where
452        I: IntoIterator<Item = (Name, Target)>,
453        Name: AsRef<str>,
454        Target: AsRef<str>,
455    {
456        self.add_mapped_commands(map_commands);
457        self
458    }
459
460    /// Maps a series of atoms to local binaries.
461    pub fn add_mapped_commands<I, Name, Target>(&mut self, map_commands: I)
462    where
463        I: IntoIterator<Item = (Name, Target)>,
464        Name: AsRef<str>,
465        Target: AsRef<str>,
466    {
467        for (alias, target) in map_commands {
468            self.add_mapped_command(alias, target);
469        }
470    }
471
472    /// Preopen a directory
473    ///
474    /// This opens the given directory at the virtual root, `/`, and allows
475    /// the WASI module to read and write to the given directory.
476    pub fn preopen_dir<P>(mut self, po_dir: P) -> Result<Self, WasiStateCreationError>
477    where
478        P: AsRef<Path>,
479    {
480        self.add_preopen_dir(po_dir)?;
481        Ok(self)
482    }
483
484    /// Adds a preopen a directory
485    ///
486    /// This opens the given directory at the virtual root, `/`, and allows
487    /// the WASI module to read and write to the given directory.
488    pub fn add_preopen_dir<P>(&mut self, po_dir: P) -> Result<(), WasiStateCreationError>
489    where
490        P: AsRef<Path>,
491    {
492        let mut pdb = PreopenDirBuilder::new();
493        let path = po_dir.as_ref();
494        pdb.directory(path).read(true).write(true).create(true);
495        let preopen = pdb.build()?;
496
497        self.preopens.push(preopen);
498
499        Ok(())
500    }
501
502    /// Preopen multiple directories.
503    ///
504    /// This opens the given directories at the virtual root, `/`, and allows
505    /// the WASI module to read and write to the given directory.
506    pub fn preopen_dirs<I, P>(mut self, dirs: I) -> Result<Self, WasiStateCreationError>
507    where
508        I: IntoIterator<Item = P>,
509        P: AsRef<Path>,
510    {
511        for po_dir in dirs {
512            self.add_preopen_dir(po_dir)?;
513        }
514
515        Ok(self)
516    }
517
518    /// Preopen a directory and configure it.
519    ///
520    /// Usage:
521    ///
522    /// ```no_run
523    /// # use wasmer_wasix::{WasiEnv, WasiStateCreationError};
524    /// # fn main() -> Result<(), WasiStateCreationError> {
525    /// WasiEnv::builder("program_name")
526    ///    .preopen_build(|p| p.directory("src").read(true).write(true).create(true))?
527    ///    .preopen_build(|p| p.directory(".").alias("dot").read(true))?
528    ///    .build_init()?;
529    /// # Ok(())
530    /// # }
531    /// ```
532    pub fn preopen_build<F>(mut self, inner: F) -> Result<Self, WasiStateCreationError>
533    where
534        F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
535    {
536        self.add_preopen_build(inner)?;
537        Ok(self)
538    }
539
540    /// Preopen a directory and configure it.
541    ///
542    /// Usage:
543    ///
544    /// ```no_run
545    /// # use wasmer_wasix::{WasiEnv, WasiStateCreationError};
546    /// # fn main() -> Result<(), WasiStateCreationError> {
547    /// WasiEnv::builder("program_name")
548    ///    .preopen_build(|p| p.directory("src").read(true).write(true).create(true))?
549    ///    .preopen_build(|p| p.directory(".").alias("dot").read(true))?
550    ///    .build_init()?;
551    /// # Ok(())
552    /// # }
553    /// ```
554    pub fn add_preopen_build<F>(&mut self, inner: F) -> Result<(), WasiStateCreationError>
555    where
556        F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
557    {
558        let mut pdb = PreopenDirBuilder::new();
559        let po_dir = inner(&mut pdb).build()?;
560
561        self.preopens.push(po_dir);
562
563        Ok(())
564    }
565
566    /// Preopen the given directories from the
567    /// Virtual FS.
568    pub fn preopen_vfs_dirs<I>(&mut self, po_dirs: I) -> Result<&mut Self, WasiStateCreationError>
569    where
570        I: IntoIterator<Item = String>,
571    {
572        for po_dir in po_dirs {
573            self.vfs_preopens.push(po_dir);
574        }
575
576        Ok(self)
577    }
578
579    /// Preopen a directory with a different name exposed to the WASI.
580    pub fn map_dir<P>(mut self, alias: &str, po_dir: P) -> Result<Self, WasiStateCreationError>
581    where
582        P: AsRef<Path>,
583    {
584        self.add_map_dir(alias, po_dir)?;
585        Ok(self)
586    }
587
588    /// Preopen a directory with a different name exposed to the WASI.
589    pub fn add_map_dir<P>(&mut self, alias: &str, po_dir: P) -> Result<(), WasiStateCreationError>
590    where
591        P: AsRef<Path>,
592    {
593        let mut pdb = PreopenDirBuilder::new();
594        let path = po_dir.as_ref();
595        pdb.directory(path)
596            .alias(alias)
597            .read(true)
598            .write(true)
599            .create(true);
600        let preopen = pdb.build()?;
601
602        self.preopens.push(preopen);
603
604        Ok(())
605    }
606
607    /// Preopen directorys with a different names exposed to the WASI.
608    pub fn map_dirs<I, P>(mut self, mapped_dirs: I) -> Result<Self, WasiStateCreationError>
609    where
610        I: IntoIterator<Item = (String, P)>,
611        P: AsRef<Path>,
612    {
613        for (alias, dir) in mapped_dirs {
614            self.add_map_dir(&alias, dir)?;
615        }
616
617        Ok(self)
618    }
619
620    /// Specifies one or more journal files that Wasmer will use to restore
621    /// the state of the WASM process.
622    ///
623    /// The state of the WASM process and its sandbox will be reapplied use
624    /// the journals in the order that you specify here.
625    #[cfg(feature = "journal")]
626    pub fn add_read_only_journal(&mut self, journal: Arc<DynReadableJournal>) {
627        self.read_only_journals.push(journal);
628    }
629
630    /// Specifies one or more journal files that Wasmer will use to restore
631    /// the state of the WASM process.
632    ///
633    /// The state of the WASM process and its sandbox will be reapplied use
634    /// the journals in the order that you specify here.
635    ///
636    /// The last journal file specified will be created if it does not exist
637    /// and opened for read and write. New journal events will be written to this
638    /// file
639    #[cfg(feature = "journal")]
640    pub fn add_writable_journal(&mut self, journal: Arc<DynJournal>) {
641        self.writable_journals.push(journal);
642    }
643
644    pub fn get_current_dir(&mut self) -> Option<PathBuf> {
645        self.current_dir.clone()
646    }
647
648    pub fn set_current_dir(&mut self, dir: impl Into<PathBuf>) {
649        self.current_dir = Some(dir.into());
650    }
651
652    pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
653        self.set_current_dir(dir);
654        self
655    }
656
657    /// Overwrite the default WASI `stdout`, if you want to hold on to the
658    /// original `stdout` use [`WasiFs::swap_file`] after building.
659    pub fn stdout(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
660        self.stdout = Some(new_file);
661
662        self
663    }
664
665    /// Overwrite the default WASI `stdout`, if you want to hold on to the
666    /// original `stdout` use [`WasiFs::swap_file`] after building.
667    pub fn set_stdout(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
668        self.stdout = Some(new_file);
669    }
670
671    /// Overwrite the default WASI `stderr`, if you want to hold on to the
672    /// original `stderr` use [`WasiFs::swap_file`] after building.
673    pub fn stderr(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
674        self.set_stderr(new_file);
675        self
676    }
677
678    /// Overwrite the default WASI `stderr`, if you want to hold on to the
679    /// original `stderr` use [`WasiFs::swap_file`] after building.
680    pub fn set_stderr(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
681        self.stderr = Some(new_file);
682    }
683
684    /// Overwrite the default WASI `stdin`, if you want to hold on to the
685    /// original `stdin` use [`WasiFs::swap_file`] after building.
686    pub fn stdin(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
687        self.stdin = Some(new_file);
688
689        self
690    }
691
692    /// Overwrite the default WASI `stdin`, if you want to hold on to the
693    /// original `stdin` use [`WasiFs::swap_file`] after building.
694    pub fn set_stdin(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
695        self.stdin = Some(new_file);
696    }
697
698    /// Sets the FileSystem to be used with this WASI instance.
699    ///
700    /// This is usually used in case a custom `virtual_fs::FileSystem` is needed.
701    pub fn fs(mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) -> Self {
702        self.set_fs(fs);
703        self
704    }
705
706    pub fn set_fs(&mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) {
707        self.fs = Some(WasiFsRoot::Backing(fs.into()));
708    }
709
710    pub(crate) fn set_fs_root(&mut self, fs: WasiFsRoot) {
711        self.fs = Some(fs);
712    }
713
714    /// Sets a new sandbox FileSystem to be used with this WASI instance.
715    ///
716    /// This is usually used in case a custom `virtual_fs::FileSystem` is needed.
717    pub fn sandbox_fs(mut self, fs: TmpFileSystem) -> Self {
718        self.fs = Some(WasiFsRoot::Sandbox(fs));
719        self
720    }
721
722    /// Configure the WASI filesystem before running.
723    // TODO: improve ergonomics on this function
724    pub fn setup_fs(mut self, setup_fs_fn: SetupFsFn) -> Self {
725        self.setup_fs_fn = Some(setup_fs_fn);
726
727        self
728    }
729
730    /// Sets the wasmer engine and overrides the default; only used if
731    /// a runtime override is not provided.
732    pub fn engine(mut self, engine: Engine) -> Self {
733        self.set_engine(engine);
734        self
735    }
736
737    pub fn set_engine(&mut self, engine: Engine) {
738        self.engine = Some(engine);
739    }
740
741    /// Sets the WASI runtime implementation and overrides the default
742    /// implementation
743    pub fn runtime(mut self, runtime: Arc<dyn Runtime + Send + Sync>) -> Self {
744        self.set_runtime(runtime);
745        self
746    }
747
748    pub fn set_runtime(&mut self, runtime: Arc<dyn Runtime + Send + Sync>) {
749        self.runtime = Some(runtime);
750    }
751
752    pub fn capabilities(mut self, capabilities: Capabilities) -> Self {
753        self.set_capabilities(capabilities);
754        self
755    }
756
757    pub fn capabilities_mut(&mut self) -> &mut Capabilities {
758        &mut self.capabilites
759    }
760
761    pub fn set_capabilities(&mut self, capabilities: Capabilities) {
762        self.capabilites = capabilities;
763    }
764
765    #[cfg(feature = "journal")]
766    pub fn add_snapshot_trigger(&mut self, on: SnapshotTrigger) {
767        self.snapshot_on.push(on);
768    }
769
770    #[cfg(feature = "journal")]
771    pub fn with_snapshot_interval(&mut self, interval: std::time::Duration) {
772        self.snapshot_interval.replace(interval);
773    }
774
775    #[cfg(feature = "journal")]
776    pub fn with_stop_running_after_snapshot(&mut self, stop_running: bool) {
777        self.stop_running_after_snapshot = stop_running;
778    }
779
780    pub fn with_skip_stdio_during_bootstrap(&mut self, skip: bool) {
781        self.skip_stdio_during_bootstrap = skip;
782    }
783
784    /// Consumes the [`WasiEnvBuilder`] and produces a [`WasiEnvInit`], which
785    /// can be used to construct a new [`WasiEnv`].
786    ///
787    /// Returns the error from `WasiFs::new` if there's an error
788    ///
789    /// NOTE: You should prefer to not work directly with [`WasiEnvInit`].
790    /// Use [`WasiEnvBuilder::build`] or [`WasiEnvBuilder::instantiate`] instead
791    /// to ensure proper invokation of WASI modules.
792    pub fn build_init(mut self) -> Result<WasiEnvInit, WasiStateCreationError> {
793        for arg in self.args.iter() {
794            for b in arg.as_bytes().iter() {
795                if *b == 0 {
796                    return Err(WasiStateCreationError::ArgumentContainsNulByte(arg.clone()));
797                }
798            }
799        }
800
801        enum InvalidCharacter {
802            Nul,
803            Equal,
804        }
805
806        for (env_key, env_value) in self.envs.iter() {
807            match env_key.as_bytes().iter().find_map(|&ch| {
808                if ch == 0 {
809                    Some(InvalidCharacter::Nul)
810                } else if ch == b'=' {
811                    Some(InvalidCharacter::Equal)
812                } else {
813                    None
814                }
815            }) {
816                Some(InvalidCharacter::Nul) => {
817                    return Err(WasiStateCreationError::EnvironmentVariableFormatError(
818                        format!("found nul byte in env var key \"{env_key}\" (key=value)"),
819                    ));
820                }
821
822                Some(InvalidCharacter::Equal) => {
823                    return Err(WasiStateCreationError::EnvironmentVariableFormatError(
824                        format!("found equal sign in env var key \"{env_key}\" (key=value)"),
825                    ));
826                }
827
828                None => (),
829            }
830
831            if env_value.contains(&0) {
832                return Err(WasiStateCreationError::EnvironmentVariableFormatError(
833                    format!(
834                        "found nul byte in env var value \"{}\" (key=value)",
835                        String::from_utf8_lossy(env_value),
836                    ),
837                ));
838            }
839        }
840
841        // Determine the STDIN
842        let stdin: Box<dyn VirtualFile + Send + Sync + 'static> = self
843            .stdin
844            .take()
845            .unwrap_or_else(|| Box::new(ArcFile::new(Box::<super::Stdin>::default())));
846
847        let fs_backing = self
848            .fs
849            .take()
850            .unwrap_or_else(|| WasiFsRoot::Sandbox(TmpFileSystem::new()));
851
852        if let Some(dir) = &self.current_dir {
853            match fs_backing.read_dir(dir) {
854                Ok(_) => {
855                    // All good
856                }
857                Err(FsError::EntryNotFound) => {
858                    fs_backing.create_dir(dir).map_err(|err| {
859                        WasiStateCreationError::WasiFsSetupError(format!(
860                            "Could not create specified current directory at '{}': {err}",
861                            dir.display()
862                        ))
863                    })?;
864                }
865                Err(err) => {
866                    return Err(WasiStateCreationError::WasiFsSetupError(format!(
867                        "Could check specified current directory at '{}': {err}",
868                        dir.display()
869                    )));
870                }
871            }
872        }
873
874        // self.preopens are checked in [`PreopenDirBuilder::build`]
875        let inodes = crate::state::WasiInodes::new();
876        let wasi_fs = {
877            // self.preopens are checked in [`PreopenDirBuilder::build`]
878            let mut wasi_fs =
879                WasiFs::new_with_preopen(&inodes, &self.preopens, &self.vfs_preopens, fs_backing)
880                    .map_err(WasiStateCreationError::WasiFsCreationError)?;
881
882            // set up the file system, overriding base files and calling the setup function
883            wasi_fs
884                .swap_file(__WASI_STDIN_FILENO, stdin)
885                .map_err(WasiStateCreationError::FileSystemError)?;
886
887            if let Some(stdout_override) = self.stdout.take() {
888                wasi_fs
889                    .swap_file(__WASI_STDOUT_FILENO, stdout_override)
890                    .map_err(WasiStateCreationError::FileSystemError)?;
891            }
892
893            if let Some(stderr_override) = self.stderr.take() {
894                wasi_fs
895                    .swap_file(__WASI_STDERR_FILENO, stderr_override)
896                    .map_err(WasiStateCreationError::FileSystemError)?;
897            }
898
899            if let Some(f) = &self.setup_fs_fn {
900                f(&inodes, &mut wasi_fs).map_err(WasiStateCreationError::WasiFsSetupError)?;
901            }
902            wasi_fs
903        };
904
905        if let Some(dir) = &self.current_dir {
906            let s = dir.to_str().ok_or_else(|| {
907                WasiStateCreationError::WasiFsSetupError(format!(
908                    "Specified current directory is not valid UTF-8: '{}'",
909                    dir.display()
910                ))
911            })?;
912            wasi_fs.set_current_dir(s);
913        }
914
915        for id in &self.included_packages {
916            wasi_fs.has_unioned.lock().unwrap().insert(id.clone());
917        }
918
919        let state = WasiState {
920            fs: wasi_fs,
921            secret: rand::thread_rng().r#gen::<[u8; 32]>(),
922            inodes,
923            args: std::sync::Mutex::new(self.args.clone()),
924            preopen: self.vfs_preopens.clone(),
925            futexs: Default::default(),
926            clock_offset: Default::default(),
927            envs: std::sync::Mutex::new(conv_env_vars(self.envs)),
928            signals: std::sync::Mutex::new(self.signals.iter().map(|s| (s.sig, s.disp)).collect()),
929        };
930
931        let runtime = self.runtime.unwrap_or_else(|| {
932            #[cfg(feature = "sys-thread")]
933            {
934                #[allow(unused_mut)]
935                let mut runtime = crate::runtime::PluggableRuntime::new(Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::default()));
936                runtime.set_engine(
937                    self
938                        .engine
939                        .as_ref()
940                        .expect(
941                            "Neither a runtime nor an engine was provided to WasiEnvBuilder. \
942                            This is not supported because it means the module that's going to \
943                            run with the resulting WasiEnv will have been loaded using a \
944                            different engine than the one that will exist within the WasiEnv. \
945                            Use either `set_runtime` or `set_engine` before calling `build_init`.",
946                        )
947                        .clone()
948                );
949                #[cfg(feature = "journal")]
950                for journal in self.read_only_journals.clone() {
951                    runtime.add_read_only_journal(journal);
952                }
953                #[cfg(feature = "journal")]
954                for journal in self.writable_journals.clone() {
955                    runtime.add_writable_journal(journal);
956                }
957                Arc::new(runtime)
958            }
959
960            #[cfg(not(feature = "sys-thread"))]
961            {
962                panic!("this build does not support a default runtime - specify one with WasiEnvBuilder::runtime()");
963            }
964        });
965
966        let uses = self.uses;
967        let map_commands = self.map_commands;
968
969        let bin_factory = BinFactory::new(runtime.clone());
970
971        let capabilities = self.capabilites;
972
973        let plane_config = ControlPlaneConfig {
974            max_task_count: capabilities.threading.max_threads,
975            enable_asynchronous_threading: capabilities.threading.enable_asynchronous_threading,
976            enable_exponential_cpu_backoff: capabilities.threading.enable_exponential_cpu_backoff,
977        };
978        let control_plane = WasiControlPlane::new(plane_config);
979
980        let init = WasiEnvInit {
981            state,
982            runtime,
983            webc_dependencies: uses,
984            mapped_commands: map_commands,
985            control_plane,
986            bin_factory,
987            capabilities,
988            memory_ty: None,
989            process: None,
990            thread: None,
991            #[cfg(feature = "journal")]
992            call_initialize: self.read_only_journals.is_empty()
993                && self.writable_journals.is_empty(),
994            #[cfg(not(feature = "journal"))]
995            call_initialize: true,
996            can_deep_sleep: false,
997            extra_tracing: true,
998            #[cfg(feature = "journal")]
999            snapshot_on: self.snapshot_on,
1000            #[cfg(feature = "journal")]
1001            stop_running_after_snapshot: self.stop_running_after_snapshot,
1002            skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
1003        };
1004
1005        Ok(init)
1006    }
1007
1008    #[allow(clippy::result_large_err)]
1009    pub fn build(self) -> Result<WasiEnv, WasiRuntimeError> {
1010        let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1011        let init = self.build_init()?;
1012        WasiEnv::from_init(init, module_hash)
1013    }
1014
1015    /// Construct a [`WasiFunctionEnv`].
1016    ///
1017    /// NOTE: you still must call [`WasiFunctionEnv::initialize`] to make an
1018    /// instance usable.
1019    #[doc(hidden)]
1020    #[allow(clippy::result_large_err)]
1021    pub fn finalize(
1022        self,
1023        store: &mut impl AsStoreMut,
1024    ) -> Result<WasiFunctionEnv, WasiRuntimeError> {
1025        let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1026        let init = self.build_init()?;
1027        let env = WasiEnv::from_init(init, module_hash)?;
1028        let func_env = WasiFunctionEnv::new(store, env);
1029        Ok(func_env)
1030    }
1031
1032    /// Consumes the [`WasiEnvBuilder`] and produces a [`WasiEnvInit`], which
1033    /// can be used to construct a new [`WasiEnv`].
1034    ///
1035    /// Returns the error from `WasiFs::new` if there's an error
1036    // FIXME: use a proper custom error type
1037    #[allow(clippy::result_large_err)]
1038    pub fn instantiate(
1039        self,
1040        module: Module,
1041        store: &mut impl AsStoreMut,
1042    ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1043        self.instantiate_ext(module, ModuleHash::random(), store)
1044    }
1045
1046    #[allow(clippy::result_large_err)]
1047    pub fn instantiate_ext(
1048        self,
1049        module: Module,
1050        module_hash: ModuleHash,
1051        store: &mut impl AsStoreMut,
1052    ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1053        let init = self.build_init()?;
1054        let call_init = init.call_initialize;
1055        let env = WasiEnv::from_init(init, module_hash)?;
1056        let memory = module
1057            .imports()
1058            .find_map(|i| match i.ty() {
1059                wasmer::ExternType::Memory(ty) => Some(*ty),
1060                _ => None,
1061            })
1062            .map(|ty| wasmer::Memory::new(store, ty))
1063            .transpose()
1064            .map_err(WasiThreadError::MemoryCreateFailed)?;
1065        Ok(env.instantiate(module, store, memory, true, call_init, None)?)
1066    }
1067}
1068
1069pub(crate) fn conv_env_vars(envs: Vec<(String, Vec<u8>)>) -> Vec<Vec<u8>> {
1070    envs.into_iter()
1071        .map(|(key, value)| {
1072            let mut env = Vec::with_capacity(key.len() + value.len() + 1);
1073            env.extend_from_slice(key.as_bytes());
1074            env.push(b'=');
1075            env.extend_from_slice(&value);
1076
1077            env
1078        })
1079        .collect()
1080}
1081
1082/// Builder for preopened directories.
1083#[derive(Debug, Default)]
1084pub struct PreopenDirBuilder {
1085    path: Option<PathBuf>,
1086    alias: Option<String>,
1087    read: bool,
1088    write: bool,
1089    create: bool,
1090}
1091
1092/// The built version of `PreopenDirBuilder`
1093#[derive(Debug, Clone, Default)]
1094pub(crate) struct PreopenedDir {
1095    pub(crate) path: PathBuf,
1096    pub(crate) alias: Option<String>,
1097    pub(crate) read: bool,
1098    pub(crate) write: bool,
1099    pub(crate) create: bool,
1100}
1101
1102impl PreopenDirBuilder {
1103    /// Create an empty builder
1104    pub(crate) fn new() -> Self {
1105        PreopenDirBuilder::default()
1106    }
1107
1108    /// Point the preopened directory to the path given by `po_dir`
1109    pub fn directory<FilePath>(&mut self, po_dir: FilePath) -> &mut Self
1110    where
1111        FilePath: AsRef<Path>,
1112    {
1113        let path = po_dir.as_ref();
1114        self.path = Some(path.to_path_buf());
1115
1116        self
1117    }
1118
1119    /// Make this preopened directory appear to the WASI program as `alias`
1120    pub fn alias(&mut self, alias: &str) -> &mut Self {
1121        // We mount at preopened dirs at `/` by default and multiple `/` in a row
1122        // are equal to a single `/`.
1123        let alias = alias.trim_start_matches('/');
1124        self.alias = Some(alias.to_string());
1125
1126        self
1127    }
1128
1129    /// Set read permissions affecting files in the directory
1130    pub fn read(&mut self, toggle: bool) -> &mut Self {
1131        self.read = toggle;
1132
1133        self
1134    }
1135
1136    /// Set write permissions affecting files in the directory
1137    pub fn write(&mut self, toggle: bool) -> &mut Self {
1138        self.write = toggle;
1139
1140        self
1141    }
1142
1143    /// Set create permissions affecting files in the directory
1144    ///
1145    /// Create implies `write` permissions
1146    pub fn create(&mut self, toggle: bool) -> &mut Self {
1147        self.create = toggle;
1148        if toggle {
1149            self.write = true;
1150        }
1151
1152        self
1153    }
1154
1155    pub(crate) fn build(&self) -> Result<PreopenedDir, WasiStateCreationError> {
1156        // ensure at least one is set
1157        if !(self.read || self.write || self.create) {
1158            return Err(WasiStateCreationError::PreopenedDirectoryError("Preopened directories must have at least one of read, write, create permissions set".to_string()));
1159        }
1160
1161        if self.path.is_none() {
1162            return Err(WasiStateCreationError::PreopenedDirectoryError(
1163                "Preopened directories must point to a host directory".to_string(),
1164            ));
1165        }
1166        let path = self.path.clone().unwrap();
1167
1168        /*
1169        if !path.exists() {
1170            return Err(WasiStateCreationError::PreopenedDirectoryNotFound(path));
1171        }
1172        */
1173
1174        if let Some(alias) = &self.alias {
1175            validate_mapped_dir_alias(alias)?;
1176        }
1177
1178        Ok(PreopenedDir {
1179            path,
1180            alias: self.alias.clone(),
1181            read: self.read,
1182            write: self.write,
1183            create: self.create,
1184        })
1185    }
1186}
1187
1188#[cfg(test)]
1189mod test {
1190    use super::*;
1191
1192    #[test]
1193    fn env_var_errors() {
1194        #[cfg(not(target_arch = "wasm32"))]
1195        let runtime = tokio::runtime::Builder::new_multi_thread()
1196            .enable_all()
1197            .build()
1198            .unwrap();
1199        #[cfg(not(target_arch = "wasm32"))]
1200        let handle = runtime.handle().clone();
1201        #[cfg(not(target_arch = "wasm32"))]
1202        let _guard = handle.enter();
1203
1204        // `=` in the key is invalid.
1205        assert!(
1206            WasiEnv::builder("test_prog")
1207                .env("HOM=E", "/home/home")
1208                .build_init()
1209                .is_err(),
1210            "equal sign in key must be invalid"
1211        );
1212
1213        // `\0` in the key is invalid.
1214        assert!(
1215            WasiEnvBuilder::new("test_prog")
1216                .env("HOME\0", "/home/home")
1217                .build_init()
1218                .is_err(),
1219            "nul in key must be invalid"
1220        );
1221
1222        // `\0` in the value is invalid.
1223        assert!(
1224            WasiEnvBuilder::new("test_prog")
1225                .env("HOME", "/home/home\0")
1226                .build_init()
1227                .is_err(),
1228            "nul in value must be invalid"
1229        );
1230
1231        // `=` in the value is valid.
1232        assert!(
1233            WasiEnvBuilder::new("test_prog")
1234                .env("HOME", "/home/home=home")
1235                .engine(Engine::default())
1236                .build_init()
1237                .is_ok(),
1238            "equal sign in the value must be valid"
1239        );
1240    }
1241
1242    #[test]
1243    fn nul_character_in_args() {
1244        let output = WasiEnvBuilder::new("test_prog")
1245            .arg("--h\0elp")
1246            .build_init();
1247        let err = output.expect_err("should fail");
1248        assert!(matches!(
1249            err,
1250            WasiStateCreationError::ArgumentContainsNulByte(_)
1251        ));
1252
1253        let output = WasiEnvBuilder::new("test_prog")
1254            .args(["--help", "--wat\0"])
1255            .build_init();
1256        let err = output.expect_err("should fail");
1257        assert!(matches!(
1258            err,
1259            WasiStateCreationError::ArgumentContainsNulByte(_)
1260        ));
1261    }
1262}