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: Box<dyn virtual_fs::FileSystem + Send + Sync>) -> Self {
702        self.set_fs(fs);
703        self
704    }
705
706    pub fn set_fs(&mut self, fs: Box<dyn virtual_fs::FileSystem + Send + Sync>) {
707        self.fs = Some(WasiFsRoot::Backing(Arc::new(fs)));
708    }
709
710    /// Sets a new sandbox FileSystem to be used with this WASI instance.
711    ///
712    /// This is usually used in case a custom `virtual_fs::FileSystem` is needed.
713    pub fn sandbox_fs(mut self, fs: TmpFileSystem) -> Self {
714        self.fs = Some(WasiFsRoot::Sandbox(Arc::new(fs)));
715        self
716    }
717
718    /// Configure the WASI filesystem before running.
719    // TODO: improve ergonomics on this function
720    pub fn setup_fs(mut self, setup_fs_fn: SetupFsFn) -> Self {
721        self.setup_fs_fn = Some(setup_fs_fn);
722
723        self
724    }
725
726    /// Sets the wasmer engine and overrides the default; only used if
727    /// a runtime override is not provided.
728    pub fn engine(mut self, engine: Engine) -> Self {
729        self.set_engine(engine);
730        self
731    }
732
733    pub fn set_engine(&mut self, engine: Engine) {
734        self.engine = Some(engine);
735    }
736
737    /// Sets the WASI runtime implementation and overrides the default
738    /// implementation
739    pub fn runtime(mut self, runtime: Arc<dyn Runtime + Send + Sync>) -> Self {
740        self.set_runtime(runtime);
741        self
742    }
743
744    pub fn set_runtime(&mut self, runtime: Arc<dyn Runtime + Send + Sync>) {
745        self.runtime = Some(runtime);
746    }
747
748    pub fn capabilities(mut self, capabilities: Capabilities) -> Self {
749        self.set_capabilities(capabilities);
750        self
751    }
752
753    pub fn capabilities_mut(&mut self) -> &mut Capabilities {
754        &mut self.capabilites
755    }
756
757    pub fn set_capabilities(&mut self, capabilities: Capabilities) {
758        self.capabilites = capabilities;
759    }
760
761    #[cfg(feature = "journal")]
762    pub fn add_snapshot_trigger(&mut self, on: SnapshotTrigger) {
763        self.snapshot_on.push(on);
764    }
765
766    #[cfg(feature = "journal")]
767    pub fn with_snapshot_interval(&mut self, interval: std::time::Duration) {
768        self.snapshot_interval.replace(interval);
769    }
770
771    #[cfg(feature = "journal")]
772    pub fn with_stop_running_after_snapshot(&mut self, stop_running: bool) {
773        self.stop_running_after_snapshot = stop_running;
774    }
775
776    pub fn with_skip_stdio_during_bootstrap(&mut self, skip: bool) {
777        self.skip_stdio_during_bootstrap = skip;
778    }
779
780    /// Consumes the [`WasiEnvBuilder`] and produces a [`WasiEnvInit`], which
781    /// can be used to construct a new [`WasiEnv`].
782    ///
783    /// Returns the error from `WasiFs::new` if there's an error
784    ///
785    /// NOTE: You should prefer to not work directly with [`WasiEnvInit`].
786    /// Use [`WasiEnvBuilder::build`] or [`WasiEnvBuilder::instantiate`] instead
787    /// to ensure proper invokation of WASI modules.
788    pub fn build_init(mut self) -> Result<WasiEnvInit, WasiStateCreationError> {
789        for arg in self.args.iter() {
790            for b in arg.as_bytes().iter() {
791                if *b == 0 {
792                    return Err(WasiStateCreationError::ArgumentContainsNulByte(arg.clone()));
793                }
794            }
795        }
796
797        enum InvalidCharacter {
798            Nul,
799            Equal,
800        }
801
802        for (env_key, env_value) in self.envs.iter() {
803            match env_key.as_bytes().iter().find_map(|&ch| {
804                if ch == 0 {
805                    Some(InvalidCharacter::Nul)
806                } else if ch == b'=' {
807                    Some(InvalidCharacter::Equal)
808                } else {
809                    None
810                }
811            }) {
812                Some(InvalidCharacter::Nul) => {
813                    return Err(WasiStateCreationError::EnvironmentVariableFormatError(
814                        format!("found nul byte in env var key \"{env_key}\" (key=value)"),
815                    ));
816                }
817
818                Some(InvalidCharacter::Equal) => {
819                    return Err(WasiStateCreationError::EnvironmentVariableFormatError(
820                        format!("found equal sign in env var key \"{env_key}\" (key=value)"),
821                    ));
822                }
823
824                None => (),
825            }
826
827            if env_value.contains(&0) {
828                return Err(WasiStateCreationError::EnvironmentVariableFormatError(
829                    format!(
830                        "found nul byte in env var value \"{}\" (key=value)",
831                        String::from_utf8_lossy(env_value),
832                    ),
833                ));
834            }
835        }
836
837        // Determine the STDIN
838        let stdin: Box<dyn VirtualFile + Send + Sync + 'static> = self
839            .stdin
840            .take()
841            .unwrap_or_else(|| Box::new(ArcFile::new(Box::<super::Stdin>::default())));
842
843        let fs_backing = self
844            .fs
845            .take()
846            .unwrap_or_else(|| WasiFsRoot::Sandbox(Arc::new(TmpFileSystem::new())));
847
848        if let Some(dir) = &self.current_dir {
849            match fs_backing.read_dir(dir) {
850                Ok(_) => {
851                    // All good
852                }
853                Err(FsError::EntryNotFound) => {
854                    fs_backing.create_dir(dir).map_err(|err| {
855                        WasiStateCreationError::WasiFsSetupError(format!(
856                            "Could not create specified current directory at '{}': {err}",
857                            dir.display()
858                        ))
859                    })?;
860                }
861                Err(err) => {
862                    return Err(WasiStateCreationError::WasiFsSetupError(format!(
863                        "Could check specified current directory at '{}': {err}",
864                        dir.display()
865                    )));
866                }
867            }
868        }
869
870        // self.preopens are checked in [`PreopenDirBuilder::build`]
871        let inodes = crate::state::WasiInodes::new();
872        let wasi_fs = {
873            // self.preopens are checked in [`PreopenDirBuilder::build`]
874            let mut wasi_fs =
875                WasiFs::new_with_preopen(&inodes, &self.preopens, &self.vfs_preopens, fs_backing)
876                    .map_err(WasiStateCreationError::WasiFsCreationError)?;
877
878            // set up the file system, overriding base files and calling the setup function
879            wasi_fs
880                .swap_file(__WASI_STDIN_FILENO, stdin)
881                .map_err(WasiStateCreationError::FileSystemError)?;
882
883            if let Some(stdout_override) = self.stdout.take() {
884                wasi_fs
885                    .swap_file(__WASI_STDOUT_FILENO, stdout_override)
886                    .map_err(WasiStateCreationError::FileSystemError)?;
887            }
888
889            if let Some(stderr_override) = self.stderr.take() {
890                wasi_fs
891                    .swap_file(__WASI_STDERR_FILENO, stderr_override)
892                    .map_err(WasiStateCreationError::FileSystemError)?;
893            }
894
895            if let Some(f) = &self.setup_fs_fn {
896                f(&inodes, &mut wasi_fs).map_err(WasiStateCreationError::WasiFsSetupError)?;
897            }
898            wasi_fs
899        };
900
901        if let Some(dir) = &self.current_dir {
902            let s = dir.to_str().ok_or_else(|| {
903                WasiStateCreationError::WasiFsSetupError(format!(
904                    "Specified current directory is not valid UTF-8: '{}'",
905                    dir.display()
906                ))
907            })?;
908            wasi_fs.set_current_dir(s);
909        }
910
911        for id in &self.included_packages {
912            wasi_fs.has_unioned.lock().unwrap().insert(id.clone());
913        }
914
915        let state = WasiState {
916            fs: wasi_fs,
917            secret: rand::thread_rng().r#gen::<[u8; 32]>(),
918            inodes,
919            args: std::sync::Mutex::new(self.args.clone()),
920            preopen: self.vfs_preopens.clone(),
921            futexs: Default::default(),
922            clock_offset: Default::default(),
923            envs: std::sync::Mutex::new(conv_env_vars(self.envs)),
924            signals: std::sync::Mutex::new(self.signals.iter().map(|s| (s.sig, s.disp)).collect()),
925        };
926
927        let runtime = self.runtime.unwrap_or_else(|| {
928            #[cfg(feature = "sys-thread")]
929            {
930                #[allow(unused_mut)]
931                let mut runtime = crate::runtime::PluggableRuntime::new(Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::default()));
932                runtime.set_engine(
933                    self
934                        .engine
935                        .as_ref()
936                        .expect(
937                            "Neither a runtime nor an engine was provided to WasiEnvBuilder. \
938                            This is not supported because it means the module that's going to \
939                            run with the resulting WasiEnv will have been loaded using a \
940                            different engine than the one that will exist within the WasiEnv. \
941                            Use either `set_runtime` or `set_engine` before calling `build_init`.",
942                        )
943                        .clone()
944                );
945                #[cfg(feature = "journal")]
946                for journal in self.read_only_journals.clone() {
947                    runtime.add_read_only_journal(journal);
948                }
949                #[cfg(feature = "journal")]
950                for journal in self.writable_journals.clone() {
951                    runtime.add_writable_journal(journal);
952                }
953                Arc::new(runtime)
954            }
955
956            #[cfg(not(feature = "sys-thread"))]
957            {
958                panic!("this build does not support a default runtime - specify one with WasiEnvBuilder::runtime()");
959            }
960        });
961
962        let uses = self.uses;
963        let map_commands = self.map_commands;
964
965        let bin_factory = BinFactory::new(runtime.clone());
966
967        let capabilities = self.capabilites;
968
969        let plane_config = ControlPlaneConfig {
970            max_task_count: capabilities.threading.max_threads,
971            enable_asynchronous_threading: capabilities.threading.enable_asynchronous_threading,
972            enable_exponential_cpu_backoff: capabilities.threading.enable_exponential_cpu_backoff,
973        };
974        let control_plane = WasiControlPlane::new(plane_config);
975
976        let init = WasiEnvInit {
977            state,
978            runtime,
979            webc_dependencies: uses,
980            mapped_commands: map_commands,
981            control_plane,
982            bin_factory,
983            capabilities,
984            memory_ty: None,
985            process: None,
986            thread: None,
987            #[cfg(feature = "journal")]
988            call_initialize: self.read_only_journals.is_empty()
989                && self.writable_journals.is_empty(),
990            #[cfg(not(feature = "journal"))]
991            call_initialize: true,
992            can_deep_sleep: false,
993            extra_tracing: true,
994            #[cfg(feature = "journal")]
995            snapshot_on: self.snapshot_on,
996            #[cfg(feature = "journal")]
997            stop_running_after_snapshot: self.stop_running_after_snapshot,
998            skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
999        };
1000
1001        Ok(init)
1002    }
1003
1004    #[allow(clippy::result_large_err)]
1005    pub fn build(self) -> Result<WasiEnv, WasiRuntimeError> {
1006        let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1007        let init = self.build_init()?;
1008        WasiEnv::from_init(init, module_hash)
1009    }
1010
1011    /// Construct a [`WasiFunctionEnv`].
1012    ///
1013    /// NOTE: you still must call [`WasiFunctionEnv::initialize`] to make an
1014    /// instance usable.
1015    #[doc(hidden)]
1016    #[allow(clippy::result_large_err)]
1017    pub fn finalize(
1018        self,
1019        store: &mut impl AsStoreMut,
1020    ) -> Result<WasiFunctionEnv, WasiRuntimeError> {
1021        let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1022        let init = self.build_init()?;
1023        let env = WasiEnv::from_init(init, module_hash)?;
1024        let func_env = WasiFunctionEnv::new(store, env);
1025        Ok(func_env)
1026    }
1027
1028    /// Consumes the [`WasiEnvBuilder`] and produces a [`WasiEnvInit`], which
1029    /// can be used to construct a new [`WasiEnv`].
1030    ///
1031    /// Returns the error from `WasiFs::new` if there's an error
1032    // FIXME: use a proper custom error type
1033    #[allow(clippy::result_large_err)]
1034    pub fn instantiate(
1035        self,
1036        module: Module,
1037        store: &mut impl AsStoreMut,
1038    ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1039        self.instantiate_ext(module, ModuleHash::random(), store)
1040    }
1041
1042    #[allow(clippy::result_large_err)]
1043    pub fn instantiate_ext(
1044        self,
1045        module: Module,
1046        module_hash: ModuleHash,
1047        store: &mut impl AsStoreMut,
1048    ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1049        let init = self.build_init()?;
1050        let call_init = init.call_initialize;
1051        let env = WasiEnv::from_init(init, module_hash)?;
1052        let memory = module
1053            .imports()
1054            .find_map(|i| match i.ty() {
1055                wasmer::ExternType::Memory(ty) => Some(*ty),
1056                _ => None,
1057            })
1058            .map(|ty| wasmer::Memory::new(store, ty))
1059            .transpose()
1060            .map_err(WasiThreadError::MemoryCreateFailed)?;
1061        Ok(env.instantiate(module, store, memory, true, call_init, None)?)
1062    }
1063}
1064
1065pub(crate) fn conv_env_vars(envs: Vec<(String, Vec<u8>)>) -> Vec<Vec<u8>> {
1066    envs.into_iter()
1067        .map(|(key, value)| {
1068            let mut env = Vec::with_capacity(key.len() + value.len() + 1);
1069            env.extend_from_slice(key.as_bytes());
1070            env.push(b'=');
1071            env.extend_from_slice(&value);
1072
1073            env
1074        })
1075        .collect()
1076}
1077
1078/// Builder for preopened directories.
1079#[derive(Debug, Default)]
1080pub struct PreopenDirBuilder {
1081    path: Option<PathBuf>,
1082    alias: Option<String>,
1083    read: bool,
1084    write: bool,
1085    create: bool,
1086}
1087
1088/// The built version of `PreopenDirBuilder`
1089#[derive(Debug, Clone, Default)]
1090pub(crate) struct PreopenedDir {
1091    pub(crate) path: PathBuf,
1092    pub(crate) alias: Option<String>,
1093    pub(crate) read: bool,
1094    pub(crate) write: bool,
1095    pub(crate) create: bool,
1096}
1097
1098impl PreopenDirBuilder {
1099    /// Create an empty builder
1100    pub(crate) fn new() -> Self {
1101        PreopenDirBuilder::default()
1102    }
1103
1104    /// Point the preopened directory to the path given by `po_dir`
1105    pub fn directory<FilePath>(&mut self, po_dir: FilePath) -> &mut Self
1106    where
1107        FilePath: AsRef<Path>,
1108    {
1109        let path = po_dir.as_ref();
1110        self.path = Some(path.to_path_buf());
1111
1112        self
1113    }
1114
1115    /// Make this preopened directory appear to the WASI program as `alias`
1116    pub fn alias(&mut self, alias: &str) -> &mut Self {
1117        // We mount at preopened dirs at `/` by default and multiple `/` in a row
1118        // are equal to a single `/`.
1119        let alias = alias.trim_start_matches('/');
1120        self.alias = Some(alias.to_string());
1121
1122        self
1123    }
1124
1125    /// Set read permissions affecting files in the directory
1126    pub fn read(&mut self, toggle: bool) -> &mut Self {
1127        self.read = toggle;
1128
1129        self
1130    }
1131
1132    /// Set write permissions affecting files in the directory
1133    pub fn write(&mut self, toggle: bool) -> &mut Self {
1134        self.write = toggle;
1135
1136        self
1137    }
1138
1139    /// Set create permissions affecting files in the directory
1140    ///
1141    /// Create implies `write` permissions
1142    pub fn create(&mut self, toggle: bool) -> &mut Self {
1143        self.create = toggle;
1144        if toggle {
1145            self.write = true;
1146        }
1147
1148        self
1149    }
1150
1151    pub(crate) fn build(&self) -> Result<PreopenedDir, WasiStateCreationError> {
1152        // ensure at least one is set
1153        if !(self.read || self.write || self.create) {
1154            return Err(WasiStateCreationError::PreopenedDirectoryError("Preopened directories must have at least one of read, write, create permissions set".to_string()));
1155        }
1156
1157        if self.path.is_none() {
1158            return Err(WasiStateCreationError::PreopenedDirectoryError(
1159                "Preopened directories must point to a host directory".to_string(),
1160            ));
1161        }
1162        let path = self.path.clone().unwrap();
1163
1164        /*
1165        if !path.exists() {
1166            return Err(WasiStateCreationError::PreopenedDirectoryNotFound(path));
1167        }
1168        */
1169
1170        if let Some(alias) = &self.alias {
1171            validate_mapped_dir_alias(alias)?;
1172        }
1173
1174        Ok(PreopenedDir {
1175            path,
1176            alias: self.alias.clone(),
1177            read: self.read,
1178            write: self.write,
1179            create: self.create,
1180        })
1181    }
1182}
1183
1184#[cfg(test)]
1185mod test {
1186    use super::*;
1187
1188    #[test]
1189    fn env_var_errors() {
1190        #[cfg(not(target_arch = "wasm32"))]
1191        let runtime = tokio::runtime::Builder::new_multi_thread()
1192            .enable_all()
1193            .build()
1194            .unwrap();
1195        #[cfg(not(target_arch = "wasm32"))]
1196        let handle = runtime.handle().clone();
1197        #[cfg(not(target_arch = "wasm32"))]
1198        let _guard = handle.enter();
1199
1200        // `=` in the key is invalid.
1201        assert!(
1202            WasiEnv::builder("test_prog")
1203                .env("HOM=E", "/home/home")
1204                .build_init()
1205                .is_err(),
1206            "equal sign in key must be invalid"
1207        );
1208
1209        // `\0` in the key is invalid.
1210        assert!(
1211            WasiEnvBuilder::new("test_prog")
1212                .env("HOME\0", "/home/home")
1213                .build_init()
1214                .is_err(),
1215            "nul in key must be invalid"
1216        );
1217
1218        // `\0` in the value is invalid.
1219        assert!(
1220            WasiEnvBuilder::new("test_prog")
1221                .env("HOME", "/home/home\0")
1222                .build_init()
1223                .is_err(),
1224            "nul in value must be invalid"
1225        );
1226
1227        // `=` in the value is valid.
1228        assert!(
1229            WasiEnvBuilder::new("test_prog")
1230                .env("HOME", "/home/home=home")
1231                .engine(Engine::default())
1232                .build_init()
1233                .is_ok(),
1234            "equal sign in the value must be valid"
1235        );
1236    }
1237
1238    #[test]
1239    fn nul_character_in_args() {
1240        let output = WasiEnvBuilder::new("test_prog")
1241            .arg("--h\0elp")
1242            .build_init();
1243        let err = output.expect_err("should fail");
1244        assert!(matches!(
1245            err,
1246            WasiStateCreationError::ArgumentContainsNulByte(_)
1247        ));
1248
1249        let output = WasiEnvBuilder::new("test_prog")
1250            .args(["--help", "--wat\0"])
1251            .build_init();
1252        let err = output.expect_err("should fail");
1253        assert!(matches!(
1254            err,
1255            WasiStateCreationError::ArgumentContainsNulByte(_)
1256        ));
1257    }
1258}