1use std::{
4 collections::{HashMap, HashSet},
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use rand::RngExt;
10use thiserror::Error;
11use virtual_fs::{
12 ArcFile, FileSystem, FsError, MountFileSystem, RootFileSystemBuilder, VirtualFile,
13};
14use wasmer::{AsStoreMut, Engine, Instance, Module};
15use wasmer_config::package::PackageId;
16
17#[cfg(feature = "journal")]
18use crate::journal::{DynJournal, DynReadableJournal, SnapshotTrigger};
19use crate::{
20 Runtime, WasiEnv, WasiFunctionEnv, WasiRuntimeError, WasiThreadError,
21 bin_factory::{BinFactory, BinaryPackage},
22 capabilities::Capabilities,
23 fs::{WasiFs, WasiFsRoot, WasiInodes},
24 os::command::VirtualCommand,
25 os::task::control_plane::{ControlPlaneConfig, ControlPlaneError, WasiControlPlane},
26 state::WasiState,
27 syscalls::types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO},
28};
29use wasmer_types::ModuleHash;
30use wasmer_wasix_types::wasi::SignalDisposition;
31
32use super::env::WasiEnvInit;
33
34#[derive(Default)]
51pub struct WasiEnvBuilder {
52 pub(super) entry_function: Option<String>,
54 pub(super) args: Vec<String>,
56 pub(super) envs: Vec<(String, Vec<u8>)>,
58 pub(super) signals: Vec<SignalDisposition>,
60 pub(super) preopens: Vec<PreopenedDir>,
62 vfs_preopens: Vec<String>,
64 #[allow(clippy::type_complexity)]
65 pub(super) setup_fs_fn:
66 Option<Box<dyn Fn(&WasiInodes, &mut WasiFs) -> Result<(), String> + Send>>,
67 pub(super) stdout: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
68 pub(super) stderr: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
69 pub(super) stdin: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
70 pub(super) fs: Option<WasiFsRoot>,
71 pub(super) engine: Option<Engine>,
72 pub(super) runtime: Option<Arc<dyn crate::Runtime + Send + Sync + 'static>>,
73 pub(super) current_dir: Option<PathBuf>,
74
75 pub(super) uses: Vec<BinaryPackage>,
77
78 pub(super) included_packages: HashSet<PackageId>,
79
80 pub(super) module_hash: Option<ModuleHash>,
81
82 pub(super) map_commands: HashMap<String, PathBuf>,
84 pub(super) disable_default_builtins: bool,
86 pub(super) builtin_commands: Vec<(String, Arc<dyn VirtualCommand + Send + Sync + 'static>)>,
88
89 pub(super) capabilities: Capabilities,
90
91 #[cfg(feature = "journal")]
92 pub(super) snapshot_on: Vec<SnapshotTrigger>,
93
94 #[cfg(feature = "journal")]
95 pub(super) snapshot_interval: Option<std::time::Duration>,
96
97 #[cfg(feature = "journal")]
98 pub(super) stop_running_after_snapshot: bool,
99
100 #[cfg(feature = "journal")]
101 pub(super) read_only_journals: Vec<Arc<DynReadableJournal>>,
102
103 #[cfg(feature = "journal")]
104 pub(super) writable_journals: Vec<Arc<DynJournal>>,
105
106 pub(super) skip_stdio_during_bootstrap: bool,
107
108 #[cfg(feature = "ctrlc")]
109 pub(super) attach_ctrl_c: bool,
110}
111
112impl std::fmt::Debug for WasiEnvBuilder {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.debug_struct("WasiEnvBuilder")
116 .field("entry_function", &self.entry_function)
117 .field("args", &self.args)
118 .field("envs", &self.envs)
119 .field("signals", &self.signals)
120 .field("preopens", &self.preopens)
121 .field("uses", &self.uses)
122 .field("setup_fs_fn exists", &self.setup_fs_fn.is_some())
123 .field("stdout_override exists", &self.stdout.is_some())
124 .field("stderr_override exists", &self.stderr.is_some())
125 .field("stdin_override exists", &self.stdin.is_some())
126 .field("disable_default_builtins", &self.disable_default_builtins)
127 .field("builtin_commands_count", &self.builtin_commands.len())
128 .field("engine_override_exists", &self.engine.is_some())
129 .field("runtime_override_exists", &self.runtime.is_some())
130 .finish()
131 }
132}
133
134#[derive(Error, Debug, Clone, PartialEq, Eq)]
136pub enum WasiStateCreationError {
137 #[error("bad environment variable format: `{0}`")]
138 EnvironmentVariableFormatError(String),
139 #[error("argument contains null byte: `{0}`")]
140 ArgumentContainsNulByte(String),
141 #[error("preopened directory not found: `{0}`")]
142 PreopenedDirectoryNotFound(PathBuf),
143 #[error("preopened directory error: `{0}`")]
144 PreopenedDirectoryError(String),
145 #[error("mapped dir alias has wrong format: `{0}`")]
146 MappedDirAliasFormattingError(String),
147 #[error("wasi filesystem creation error: `{0}`")]
148 WasiFsCreationError(String),
149 #[error("wasi filesystem setup error: `{0}`")]
150 WasiFsSetupError(String),
151 #[error(transparent)]
152 FileSystemError(#[from] FsError),
153 #[error("wasi inherit error: `{0}`")]
154 WasiInheritError(String),
155 #[error("wasi include package: `{0}`")]
156 WasiIncludePackageError(String),
157 #[error("control plane error")]
158 ControlPlane(#[from] ControlPlaneError),
159}
160
161fn validate_mapped_dir_alias(alias: &str) -> Result<(), WasiStateCreationError> {
162 if !alias.bytes().all(|b| b != b'\0') {
163 return Err(WasiStateCreationError::MappedDirAliasFormattingError(
164 format!("Alias \"{alias}\" contains a nul byte"),
165 ));
166 }
167
168 Ok(())
169}
170
171pub type SetupFsFn = Box<dyn Fn(&WasiInodes, &mut WasiFs) -> Result<(), String> + Send>;
172
173impl WasiEnvBuilder {
176 pub fn new(program_name: impl Into<String>) -> Self {
178 WasiEnvBuilder {
179 args: vec![program_name.into()],
180 ..WasiEnvBuilder::default()
181 }
182 }
183
184 #[cfg(feature = "ctrlc")]
187 pub fn attach_ctrl_c(mut self) -> Self {
188 self.attach_ctrl_c = true;
189 self
190 }
191
192 pub fn env<Key, Value>(mut self, key: Key, value: Value) -> Self
198 where
199 Key: AsRef<[u8]>,
200 Value: AsRef<[u8]>,
201 {
202 self.add_env(key, value);
203 self
204 }
205
206 pub fn add_env<Key, Value>(&mut self, key: Key, value: Value)
212 where
213 Key: AsRef<[u8]>,
214 Value: AsRef<[u8]>,
215 {
216 let key = String::from_utf8_lossy(key.as_ref()).to_string();
217 let value = value.as_ref().to_vec();
218 if let Some((_, existing_value)) = self
219 .envs
220 .iter_mut()
221 .find(|(existing_key, _)| existing_key == &key)
222 {
223 *existing_value = value;
224 } else {
225 self.envs.push((key, value));
226 }
227 }
228
229 pub fn envs<I, Key, Value>(mut self, env_pairs: I) -> Self
235 where
236 I: IntoIterator<Item = (Key, Value)>,
237 Key: AsRef<[u8]>,
238 Value: AsRef<[u8]>,
239 {
240 self.add_envs(env_pairs);
241
242 self
243 }
244
245 pub fn add_envs<I, Key, Value>(&mut self, env_pairs: I)
251 where
252 I: IntoIterator<Item = (Key, Value)>,
253 Key: AsRef<[u8]>,
254 Value: AsRef<[u8]>,
255 {
256 for (key, value) in env_pairs {
257 self.add_env(key, value);
258 }
259 }
260
261 pub fn get_env(&self) -> &[(String, Vec<u8>)] {
263 &self.envs
264 }
265
266 pub fn get_env_mut(&mut self) -> &mut Vec<(String, Vec<u8>)> {
268 &mut self.envs
269 }
270
271 pub fn signal(mut self, sig_action: SignalDisposition) -> Self {
273 self.add_signal(sig_action);
274 self
275 }
276
277 pub fn add_signal(&mut self, sig_action: SignalDisposition) {
279 self.signals.push(sig_action);
280 }
281
282 pub fn signals<I>(mut self, signal_pairs: I) -> Self
284 where
285 I: IntoIterator<Item = SignalDisposition>,
286 {
287 self.add_signals(signal_pairs);
288
289 self
290 }
291
292 pub fn add_signals<I>(&mut self, signal_pairs: I)
294 where
295 I: IntoIterator<Item = SignalDisposition>,
296 {
297 for sig in signal_pairs {
298 self.add_signal(sig);
299 }
300 }
301
302 pub fn get_signals(&self) -> &[SignalDisposition] {
304 &self.signals
305 }
306
307 pub fn get_signals_mut(&mut self) -> &mut Vec<SignalDisposition> {
309 &mut self.signals
310 }
311
312 pub fn entry_function<S>(mut self, entry_function: S) -> Self
313 where
314 S: AsRef<str>,
315 {
316 self.set_entry_function(entry_function);
317 self
318 }
319
320 pub fn set_entry_function<S>(&mut self, entry_function: S)
321 where
322 S: AsRef<str>,
323 {
324 self.entry_function = Some(entry_function.as_ref().to_owned());
325 }
326
327 pub fn arg<V>(mut self, arg: V) -> Self
332 where
333 V: AsRef<[u8]>,
334 {
335 self.add_arg(arg);
336 self
337 }
338
339 pub fn add_arg<V>(&mut self, arg: V)
344 where
345 V: AsRef<[u8]>,
346 {
347 self.args
348 .push(String::from_utf8_lossy(arg.as_ref()).to_string());
349 }
350
351 pub fn args<I, Arg>(mut self, args: I) -> Self
355 where
356 I: IntoIterator<Item = Arg>,
357 Arg: AsRef<[u8]>,
358 {
359 self.add_args(args);
360
361 self
362 }
363
364 pub fn add_args<I, Arg>(&mut self, args: I)
368 where
369 I: IntoIterator<Item = Arg>,
370 Arg: AsRef<[u8]>,
371 {
372 for arg in args {
373 self.add_arg(arg);
374 }
375 }
376
377 pub fn get_args(&self) -> &[String] {
379 &self.args
380 }
381
382 pub fn get_args_mut(&mut self) -> &mut Vec<String> {
384 &mut self.args
385 }
386
387 pub fn use_webc(mut self, pkg: BinaryPackage) -> Self {
392 self.add_webc(pkg);
393 self
394 }
395
396 pub fn set_module_hash(&mut self, hash: ModuleHash) -> &mut Self {
400 self.module_hash.replace(hash);
401 self
402 }
403
404 pub fn add_webc(&mut self, pkg: BinaryPackage) -> &mut Self {
409 self.uses.push(pkg);
410 self
411 }
412
413 pub fn include_package(&mut self, pkg_id: PackageId) -> &mut Self {
416 self.included_packages.insert(pkg_id);
417 self
418 }
419
420 pub fn include_packages(&mut self, pkg_ids: impl IntoIterator<Item = PackageId>) -> &mut Self {
423 self.included_packages.extend(pkg_ids);
424
425 self
426 }
427
428 pub fn uses<I>(mut self, uses: I) -> Self
433 where
434 I: IntoIterator<Item = BinaryPackage>,
435 {
436 for pkg in uses {
437 self.add_webc(pkg);
438 }
439 self
440 }
441
442 pub fn disable_default_builtins(mut self, disable_default_builtins: bool) -> Self {
444 self.set_disable_default_builtins(disable_default_builtins);
445 self
446 }
447
448 pub fn set_disable_default_builtins(&mut self, disable_default_builtins: bool) {
450 self.disable_default_builtins = disable_default_builtins;
451 }
452
453 pub fn builtin_command<C>(mut self, command: C) -> Self
455 where
456 C: VirtualCommand + Send + Sync + 'static,
457 {
458 self.add_builtin_command(command);
459 self
460 }
461
462 pub fn add_builtin_command<C>(&mut self, command: C)
464 where
465 C: VirtualCommand + Send + Sync + 'static,
466 {
467 let path = format!("/bin/{}", command.name());
468 self.add_builtin_command_with_path(command, path);
469 }
470
471 pub fn builtin_command_with_path<C, P>(mut self, command: C, path: P) -> Self
473 where
474 C: VirtualCommand + Send + Sync + 'static,
475 P: Into<String>,
476 {
477 self.add_builtin_command_with_path(command, path);
478 self
479 }
480
481 pub fn add_builtin_command_with_path<C, P>(&mut self, command: C, path: P)
483 where
484 C: VirtualCommand + Send + Sync + 'static,
485 P: Into<String>,
486 {
487 self.add_builtin_command_with_path_shared(Arc::new(command), path);
488 }
489
490 fn add_builtin_command_with_path_shared<P>(
492 &mut self,
493 command: Arc<dyn VirtualCommand + Send + Sync + 'static>,
494 path: P,
495 ) where
496 P: Into<String>,
497 {
498 self.builtin_commands.push((path.into(), command));
499 }
500
501 pub fn map_command<Name, Target>(mut self, name: Name, target: Target) -> Self
503 where
504 Name: AsRef<str>,
505 Target: AsRef<str>,
506 {
507 self.add_mapped_command(name, target);
508 self
509 }
510
511 pub fn add_mapped_command<Name, Target>(&mut self, name: Name, target: Target)
513 where
514 Name: AsRef<str>,
515 Target: AsRef<str>,
516 {
517 let path_buf = PathBuf::from(target.as_ref().to_string());
518 self.map_commands
519 .insert(name.as_ref().to_string(), path_buf);
520 }
521
522 pub fn map_commands<I, Name, Target>(mut self, map_commands: I) -> Self
524 where
525 I: IntoIterator<Item = (Name, Target)>,
526 Name: AsRef<str>,
527 Target: AsRef<str>,
528 {
529 self.add_mapped_commands(map_commands);
530 self
531 }
532
533 pub fn add_mapped_commands<I, Name, Target>(&mut self, map_commands: I)
535 where
536 I: IntoIterator<Item = (Name, Target)>,
537 Name: AsRef<str>,
538 Target: AsRef<str>,
539 {
540 for (alias, target) in map_commands {
541 self.add_mapped_command(alias, target);
542 }
543 }
544
545 pub fn preopen_dir<P>(mut self, po_dir: P) -> Result<Self, WasiStateCreationError>
550 where
551 P: AsRef<Path>,
552 {
553 self.add_preopen_dir(po_dir)?;
554 Ok(self)
555 }
556
557 pub fn add_preopen_dir<P>(&mut self, po_dir: P) -> Result<(), WasiStateCreationError>
562 where
563 P: AsRef<Path>,
564 {
565 let mut pdb = PreopenDirBuilder::new();
566 let path = po_dir.as_ref();
567 pdb.directory(path).read(true).write(true).create(true);
568 let preopen = pdb.build()?;
569
570 self.preopens.push(preopen);
571
572 Ok(())
573 }
574
575 pub fn preopen_dirs<I, P>(mut self, dirs: I) -> Result<Self, WasiStateCreationError>
580 where
581 I: IntoIterator<Item = P>,
582 P: AsRef<Path>,
583 {
584 for po_dir in dirs {
585 self.add_preopen_dir(po_dir)?;
586 }
587
588 Ok(self)
589 }
590
591 pub fn preopen_build<F>(mut self, inner: F) -> Result<Self, WasiStateCreationError>
606 where
607 F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
608 {
609 self.add_preopen_build(inner)?;
610 Ok(self)
611 }
612
613 pub fn add_preopen_build<F>(&mut self, inner: F) -> Result<(), WasiStateCreationError>
628 where
629 F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
630 {
631 let mut pdb = PreopenDirBuilder::new();
632 let po_dir = inner(&mut pdb).build()?;
633
634 self.preopens.push(po_dir);
635
636 Ok(())
637 }
638
639 pub fn preopen_vfs_dirs<I>(&mut self, po_dirs: I) -> Result<&mut Self, WasiStateCreationError>
642 where
643 I: IntoIterator<Item = String>,
644 {
645 for po_dir in po_dirs {
646 self.vfs_preopens.push(po_dir);
647 }
648
649 Ok(self)
650 }
651
652 pub fn map_dir<P>(mut self, alias: &str, po_dir: P) -> Result<Self, WasiStateCreationError>
654 where
655 P: AsRef<Path>,
656 {
657 self.add_map_dir(alias, po_dir)?;
658 Ok(self)
659 }
660
661 pub fn add_map_dir<P>(&mut self, alias: &str, po_dir: P) -> Result<(), WasiStateCreationError>
663 where
664 P: AsRef<Path>,
665 {
666 let mut pdb = PreopenDirBuilder::new();
667 let path = po_dir.as_ref();
668 pdb.directory(path)
669 .alias(alias)
670 .read(true)
671 .write(true)
672 .create(true);
673 let preopen = pdb.build()?;
674
675 self.preopens.push(preopen);
676
677 Ok(())
678 }
679
680 pub fn map_dirs<I, P>(mut self, mapped_dirs: I) -> Result<Self, WasiStateCreationError>
682 where
683 I: IntoIterator<Item = (String, P)>,
684 P: AsRef<Path>,
685 {
686 for (alias, dir) in mapped_dirs {
687 self.add_map_dir(&alias, dir)?;
688 }
689
690 Ok(self)
691 }
692
693 #[cfg(feature = "journal")]
699 pub fn add_read_only_journal(&mut self, journal: Arc<DynReadableJournal>) {
700 self.read_only_journals.push(journal);
701 }
702
703 #[cfg(feature = "journal")]
713 pub fn add_writable_journal(&mut self, journal: Arc<DynJournal>) {
714 self.writable_journals.push(journal);
715 }
716
717 pub fn get_current_dir(&mut self) -> Option<PathBuf> {
718 self.current_dir.clone()
719 }
720
721 pub fn set_current_dir(&mut self, dir: impl Into<PathBuf>) {
722 self.current_dir = Some(dir.into());
723 }
724
725 pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
726 self.set_current_dir(dir);
727 self
728 }
729
730 pub fn stdout(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
733 self.stdout = Some(new_file);
734
735 self
736 }
737
738 pub fn set_stdout(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
741 self.stdout = Some(new_file);
742 }
743
744 pub fn stderr(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
747 self.set_stderr(new_file);
748 self
749 }
750
751 pub fn set_stderr(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
754 self.stderr = Some(new_file);
755 }
756
757 pub fn stdin(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
760 self.stdin = Some(new_file);
761
762 self
763 }
764
765 pub fn set_stdin(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
768 self.stdin = Some(new_file);
769 }
770
771 pub fn fs(mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) -> Self {
775 self.set_fs(fs);
776 self
777 }
778
779 pub fn set_fs(&mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) {
780 self.fs = Some(WasiFsRoot::from_filesystem(fs.into()));
781 }
782
783 pub fn mount_fs(mut self, fs: MountFileSystem) -> Self {
784 self.set_mount_fs(fs);
785 self
786 }
787
788 pub fn set_mount_fs(&mut self, fs: MountFileSystem) {
789 self.fs = Some(WasiFsRoot::from_mount_fs(fs));
790 }
791
792 pub(crate) fn set_fs_root(&mut self, fs: WasiFsRoot) {
793 self.fs = Some(fs);
794 }
795
796 pub fn sandbox_fs(mut self, fs: MountFileSystem) -> Self {
798 self.fs = Some(WasiFsRoot::from_mount_fs(fs));
799 self
800 }
801
802 pub fn setup_fs(mut self, setup_fs_fn: SetupFsFn) -> Self {
805 self.setup_fs_fn = Some(setup_fs_fn);
806
807 self
808 }
809
810 pub fn engine(mut self, engine: Engine) -> Self {
813 self.set_engine(engine);
814 self
815 }
816
817 pub fn set_engine(&mut self, engine: Engine) {
818 self.engine = Some(engine);
819 }
820
821 pub fn runtime(mut self, runtime: Arc<dyn Runtime + Send + Sync>) -> Self {
824 self.set_runtime(runtime);
825 self
826 }
827
828 pub fn set_runtime(&mut self, runtime: Arc<dyn Runtime + Send + Sync>) {
829 self.runtime = Some(runtime);
830 }
831
832 pub fn capabilities(mut self, capabilities: Capabilities) -> Self {
833 self.set_capabilities(capabilities);
834 self
835 }
836
837 pub fn capabilities_mut(&mut self) -> &mut Capabilities {
838 &mut self.capabilities
839 }
840
841 pub fn set_capabilities(&mut self, capabilities: Capabilities) {
842 self.capabilities = capabilities;
843 }
844
845 #[cfg(feature = "journal")]
846 pub fn add_snapshot_trigger(&mut self, on: SnapshotTrigger) {
847 self.snapshot_on.push(on);
848 }
849
850 #[cfg(feature = "journal")]
851 pub fn with_snapshot_interval(&mut self, interval: std::time::Duration) {
852 self.snapshot_interval.replace(interval);
853 }
854
855 #[cfg(feature = "journal")]
856 pub fn with_stop_running_after_snapshot(&mut self, stop_running: bool) {
857 self.stop_running_after_snapshot = stop_running;
858 }
859
860 pub fn with_skip_stdio_during_bootstrap(&mut self, skip: bool) {
861 self.skip_stdio_during_bootstrap = skip;
862 }
863
864 pub fn build_init(mut self) -> Result<WasiEnvInit, WasiStateCreationError> {
873 for arg in self.args.iter() {
874 for b in arg.as_bytes().iter() {
875 if *b == 0 {
876 return Err(WasiStateCreationError::ArgumentContainsNulByte(arg.clone()));
877 }
878 }
879 }
880
881 enum InvalidCharacter {
882 Nul,
883 Equal,
884 }
885
886 for (env_key, env_value) in self.envs.iter() {
887 match env_key.as_bytes().iter().find_map(|&ch| {
888 if ch == 0 {
889 Some(InvalidCharacter::Nul)
890 } else if ch == b'=' {
891 Some(InvalidCharacter::Equal)
892 } else {
893 None
894 }
895 }) {
896 Some(InvalidCharacter::Nul) => {
897 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
898 format!("found nul byte in env var key \"{env_key}\" (key=value)"),
899 ));
900 }
901
902 Some(InvalidCharacter::Equal) => {
903 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
904 format!("found equal sign in env var key \"{env_key}\" (key=value)"),
905 ));
906 }
907
908 None => (),
909 }
910
911 if env_value.contains(&0) {
912 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
913 format!(
914 "found nul byte in env var value \"{}\" (key=value)",
915 String::from_utf8_lossy(env_value),
916 ),
917 ));
918 }
919 }
920
921 let stdin: Box<dyn VirtualFile + Send + Sync + 'static> = self
923 .stdin
924 .take()
925 .unwrap_or_else(|| Box::new(ArcFile::new(Box::<super::Stdin>::default())));
926
927 let fs_backing = self.fs.take().unwrap_or_else(|| {
928 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()))
929 });
930
931 if let Some(dir) = &self.current_dir {
932 match fs_backing.read_dir(dir) {
933 Ok(_) => {
934 }
936 Err(FsError::EntryNotFound) => {
937 fs_backing.create_dir(dir).map_err(|err| {
938 WasiStateCreationError::WasiFsSetupError(format!(
939 "Could not create specified current directory at '{}': {err}",
940 dir.display()
941 ))
942 })?;
943 }
944 Err(err) => {
945 return Err(WasiStateCreationError::WasiFsSetupError(format!(
946 "Could not check specified current directory at '{}': {err}",
947 dir.display()
948 )));
949 }
950 }
951 }
952
953 let resolved_preopens = self.preopens.clone();
954
955 let inodes = crate::state::WasiInodes::new();
957 let wasi_fs = {
958 let mut wasi_fs = WasiFs::new_with_preopen(
960 &inodes,
961 &resolved_preopens,
962 &self.vfs_preopens,
963 fs_backing,
964 )
965 .map_err(WasiStateCreationError::WasiFsCreationError)?;
966
967 wasi_fs
969 .swap_file(__WASI_STDIN_FILENO, stdin)
970 .map_err(WasiStateCreationError::FileSystemError)?;
971
972 if let Some(stdout_override) = self.stdout.take() {
973 wasi_fs
974 .swap_file(__WASI_STDOUT_FILENO, stdout_override)
975 .map_err(WasiStateCreationError::FileSystemError)?;
976 }
977
978 if let Some(stderr_override) = self.stderr.take() {
979 wasi_fs
980 .swap_file(__WASI_STDERR_FILENO, stderr_override)
981 .map_err(WasiStateCreationError::FileSystemError)?;
982 }
983
984 if let Some(f) = &self.setup_fs_fn {
985 f(&inodes, &mut wasi_fs).map_err(WasiStateCreationError::WasiFsSetupError)?;
986 }
987 wasi_fs
988 };
989
990 if let Some(dir) = &self.current_dir {
991 let s = dir.to_str().ok_or_else(|| {
992 WasiStateCreationError::WasiFsSetupError(format!(
993 "Specified current directory is not valid UTF-8: '{}'",
994 dir.display()
995 ))
996 })?;
997 wasi_fs.set_current_dir(s);
998 }
999
1000 for id in &self.included_packages {
1001 wasi_fs.has_unioned.lock().unwrap().insert(id.clone());
1002 }
1003
1004 let state = WasiState {
1005 fs: wasi_fs,
1006 secret: rand::rng().random::<[u8; 32]>(),
1007 inodes,
1008 args: std::sync::Mutex::new(self.args.clone()),
1009 preopen: self.vfs_preopens.clone(),
1010 futexs: Default::default(),
1011 clock_offset: Default::default(),
1012 envs: std::sync::Mutex::new(conv_env_vars(self.envs)),
1013 signals: std::sync::Mutex::new(self.signals.iter().map(|s| (s.sig, s.disp)).collect()),
1014 signal_handler_registered: std::sync::atomic::AtomicBool::new(false),
1015 };
1016
1017 let runtime = self.runtime.unwrap_or_else(|| {
1018 #[cfg(feature = "sys-thread")]
1019 {
1020 #[allow(unused_mut)]
1021 let mut runtime = crate::runtime::PluggableRuntime::new(Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::default()));
1022 runtime.set_engine(
1023 self
1024 .engine
1025 .as_ref()
1026 .expect(
1027 "Neither a runtime nor an engine was provided to WasiEnvBuilder. \
1028 This is not supported because it means the module that's going to \
1029 run with the resulting WasiEnv will have been loaded using a \
1030 different engine than the one that will exist within the WasiEnv. \
1031 Use either `set_runtime` or `set_engine` before calling `build_init`.",
1032 )
1033 .clone()
1034 );
1035 #[cfg(feature = "journal")]
1036 for journal in self.read_only_journals.clone() {
1037 runtime.add_read_only_journal(journal);
1038 }
1039 #[cfg(feature = "journal")]
1040 for journal in self.writable_journals.clone() {
1041 runtime.add_writable_journal(journal);
1042 }
1043 Arc::new(runtime)
1044 }
1045
1046 #[cfg(not(feature = "sys-thread"))]
1047 {
1048 panic!("this build does not support a default runtime - specify one with WasiEnvBuilder::runtime()");
1049 }
1050 });
1051
1052 let uses = self.uses;
1053 let map_commands = self.map_commands;
1054 let disable_default_builtins = self.disable_default_builtins;
1055 let builtin_commands = self.builtin_commands;
1056
1057 let mut bin_factory = BinFactory::new(runtime.clone());
1058 if disable_default_builtins {
1059 bin_factory.clear_builtin_commands();
1060 }
1061 for (path, command) in builtin_commands {
1062 bin_factory.register_builtin_command_with_path_shared(command, path);
1063 }
1064
1065 let capabilities = self.capabilities;
1066
1067 let plane_config = ControlPlaneConfig {
1068 max_task_count: capabilities.threading.max_threads,
1069 enable_asynchronous_threading: capabilities.threading.enable_asynchronous_threading,
1070 enable_exponential_cpu_backoff: capabilities.threading.enable_exponential_cpu_backoff,
1071 };
1072 let control_plane = WasiControlPlane::new(plane_config);
1073
1074 let init = WasiEnvInit {
1075 state,
1076 runtime,
1077 webc_dependencies: uses,
1078 mapped_commands: map_commands,
1079 control_plane,
1080 bin_factory,
1081 capabilities,
1082 memory_ty: None,
1083 process: None,
1084 thread: None,
1085 #[cfg(feature = "journal")]
1086 call_initialize: self.read_only_journals.is_empty()
1087 && self.writable_journals.is_empty(),
1088 #[cfg(not(feature = "journal"))]
1089 call_initialize: true,
1090 can_deep_sleep: false,
1091 extra_tracing: true,
1092 #[cfg(feature = "journal")]
1093 snapshot_on: self.snapshot_on,
1094 #[cfg(feature = "journal")]
1095 stop_running_after_snapshot: self.stop_running_after_snapshot,
1096 skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
1097 };
1098
1099 Ok(init)
1100 }
1101
1102 #[allow(clippy::result_large_err)]
1103 pub fn build(self) -> Result<WasiEnv, WasiRuntimeError> {
1104 let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1105 let init = self.build_init()?;
1106 WasiEnv::from_init(init, module_hash)
1107 }
1108
1109 #[doc(hidden)]
1114 #[allow(clippy::result_large_err)]
1115 pub fn finalize(
1116 self,
1117 store: &mut impl AsStoreMut,
1118 ) -> Result<WasiFunctionEnv, WasiRuntimeError> {
1119 let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1120 let init = self.build_init()?;
1121 let env = WasiEnv::from_init(init, module_hash)?;
1122 let func_env = WasiFunctionEnv::new(store, env);
1123 Ok(func_env)
1124 }
1125
1126 #[allow(clippy::result_large_err)]
1132 pub fn instantiate(
1133 self,
1134 module: Module,
1135 store: &mut impl AsStoreMut,
1136 ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1137 self.instantiate_ext(module, ModuleHash::random(), store)
1138 }
1139
1140 #[allow(clippy::result_large_err)]
1141 pub fn instantiate_ext(
1142 self,
1143 module: Module,
1144 module_hash: ModuleHash,
1145 store: &mut impl AsStoreMut,
1146 ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1147 let init = self.build_init()?;
1148 let call_init = init.call_initialize;
1149 let env = WasiEnv::from_init(init, module_hash)?;
1150 let memory = module
1151 .imports()
1152 .find_map(|i| match i.ty() {
1153 wasmer::ExternType::Memory(ty) => Some(*ty),
1154 _ => None,
1155 })
1156 .map(|ty| wasmer::Memory::new(store, ty))
1157 .transpose()
1158 .map_err(WasiThreadError::MemoryCreateFailed)?;
1159 Ok(env.instantiate(module, store, memory, true, call_init, None)?)
1160 }
1161}
1162
1163pub(crate) fn conv_env_vars(envs: Vec<(String, Vec<u8>)>) -> Vec<Vec<u8>> {
1164 envs.into_iter()
1165 .map(|(key, value)| {
1166 let mut env = Vec::with_capacity(key.len() + value.len() + 1);
1167 env.extend_from_slice(key.as_bytes());
1168 env.push(b'=');
1169 env.extend_from_slice(&value);
1170
1171 env
1172 })
1173 .collect()
1174}
1175
1176#[derive(Debug, Default)]
1178pub struct PreopenDirBuilder {
1179 path: Option<PathBuf>,
1180 alias: Option<String>,
1181 read: bool,
1182 write: bool,
1183 create: bool,
1184}
1185
1186#[derive(Debug, Clone, Default)]
1188pub(crate) struct PreopenedDir {
1189 pub(crate) path: PathBuf,
1190 pub(crate) alias: Option<String>,
1191 pub(crate) read: bool,
1192 pub(crate) write: bool,
1193 pub(crate) create: bool,
1194}
1195
1196impl PreopenDirBuilder {
1197 pub(crate) fn new() -> Self {
1199 PreopenDirBuilder::default()
1200 }
1201
1202 pub fn directory<FilePath>(&mut self, po_dir: FilePath) -> &mut Self
1204 where
1205 FilePath: AsRef<Path>,
1206 {
1207 let path = po_dir.as_ref();
1208 self.path = Some(path.to_path_buf());
1209
1210 self
1211 }
1212
1213 pub fn alias(&mut self, alias: &str) -> &mut Self {
1215 let alias = alias.trim_start_matches('/');
1218 self.alias = Some(alias.to_string());
1219
1220 self
1221 }
1222
1223 pub fn read(&mut self, toggle: bool) -> &mut Self {
1225 self.read = toggle;
1226
1227 self
1228 }
1229
1230 pub fn write(&mut self, toggle: bool) -> &mut Self {
1232 self.write = toggle;
1233
1234 self
1235 }
1236
1237 pub fn create(&mut self, toggle: bool) -> &mut Self {
1241 self.create = toggle;
1242 if toggle {
1243 self.write = true;
1244 }
1245
1246 self
1247 }
1248
1249 pub(crate) fn build(&self) -> Result<PreopenedDir, WasiStateCreationError> {
1250 if !(self.read || self.write || self.create) {
1252 return Err(WasiStateCreationError::PreopenedDirectoryError("Preopened directories must have at least one of read, write, create permissions set".to_string()));
1253 }
1254
1255 if self.path.is_none() {
1256 return Err(WasiStateCreationError::PreopenedDirectoryError(
1257 "Preopened directories must point to a host directory".to_string(),
1258 ));
1259 }
1260 let path = self.path.clone().unwrap();
1261
1262 if let Some(alias) = &self.alias {
1269 validate_mapped_dir_alias(alias)?;
1270 }
1271
1272 Ok(PreopenedDir {
1273 path,
1274 alias: self.alias.clone(),
1275 read: self.read,
1276 write: self.write,
1277 create: self.create,
1278 })
1279 }
1280}
1281
1282#[cfg(test)]
1283mod test {
1284 use super::*;
1285 use crate::{
1286 SpawnError,
1287 os::{
1288 command::{BuiltinCommand, VirtualCommand},
1289 task::{OwnedTaskStatus, TaskJoinHandle},
1290 },
1291 };
1292 use wasmer::FunctionEnvMut;
1293 use wasmer_wasix_types::wasi::Errno;
1294
1295 fn enter_tokio_runtime() -> Option<tokio::runtime::Runtime> {
1296 #[cfg(not(target_arch = "wasm32"))]
1297 {
1298 let runtime = tokio::runtime::Builder::new_multi_thread()
1299 .enable_all()
1300 .build()
1301 .unwrap();
1302 Some(runtime)
1303 }
1304
1305 #[cfg(target_arch = "wasm32")]
1306 {
1307 None
1308 }
1309 }
1310
1311 #[test]
1312 fn duplicate_environment_variables_use_the_last_value() {
1313 let mut builder = WasiEnvBuilder::new("test");
1314 builder.add_env("PORT", "5000");
1315 builder.add_env("OTHER", "value");
1316 builder.add_env("PORT", "8080");
1317
1318 assert_eq!(
1319 builder.get_env(),
1320 [
1321 ("PORT".to_owned(), b"8080".to_vec()),
1322 ("OTHER".to_owned(), b"value".to_vec()),
1323 ]
1324 );
1325 }
1326
1327 #[derive(Debug)]
1328 struct TestBuiltinCommand {
1329 name: &'static str,
1330 }
1331
1332 impl TestBuiltinCommand {
1333 fn new(name: &'static str) -> Self {
1334 Self { name }
1335 }
1336 }
1337
1338 impl VirtualCommand for TestBuiltinCommand {
1339 fn name(&self) -> &str {
1340 self.name
1341 }
1342
1343 fn as_any(&self) -> &dyn std::any::Any {
1344 self
1345 }
1346
1347 fn exec(
1348 &self,
1349 _parent_ctx: &FunctionEnvMut<'_, WasiEnv>,
1350 _path: &str,
1351 _config: &mut Option<WasiEnv>,
1352 ) -> Result<TaskJoinHandle, SpawnError> {
1353 let handle = OwnedTaskStatus::new_finished_with_code(Errno::Success.into()).handle();
1354 Ok(handle)
1355 }
1356 }
1357
1358 #[test]
1359 fn env_var_errors() {
1360 #[cfg(not(target_arch = "wasm32"))]
1361 let runtime = tokio::runtime::Builder::new_multi_thread()
1362 .enable_all()
1363 .build()
1364 .unwrap();
1365 #[cfg(not(target_arch = "wasm32"))]
1366 let handle = runtime.handle().clone();
1367 #[cfg(not(target_arch = "wasm32"))]
1368 let _guard = handle.enter();
1369
1370 assert!(
1372 WasiEnv::builder("test_prog")
1373 .env("HOM=E", "/home/home")
1374 .build_init()
1375 .is_err(),
1376 "equal sign in key must be invalid"
1377 );
1378
1379 assert!(
1381 WasiEnvBuilder::new("test_prog")
1382 .env("HOME\0", "/home/home")
1383 .build_init()
1384 .is_err(),
1385 "nul in key must be invalid"
1386 );
1387
1388 assert!(
1390 WasiEnvBuilder::new("test_prog")
1391 .env("HOME", "/home/home\0")
1392 .build_init()
1393 .is_err(),
1394 "nul in value must be invalid"
1395 );
1396
1397 assert!(
1399 WasiEnvBuilder::new("test_prog")
1400 .env("HOME", "/home/home=home")
1401 .engine(Engine::default())
1402 .build_init()
1403 .is_ok(),
1404 "equal sign in the value must be valid"
1405 );
1406 }
1407
1408 #[test]
1409 fn nul_character_in_args() {
1410 let output = WasiEnvBuilder::new("test_prog")
1411 .arg("--h\0elp")
1412 .build_init();
1413 let err = output.expect_err("should fail");
1414 assert!(matches!(
1415 err,
1416 WasiStateCreationError::ArgumentContainsNulByte(_)
1417 ));
1418
1419 let output = WasiEnvBuilder::new("test_prog")
1420 .args(["--help", "--wat\0"])
1421 .build_init();
1422 let err = output.expect_err("should fail");
1423 assert!(matches!(
1424 err,
1425 WasiStateCreationError::ArgumentContainsNulByte(_)
1426 ));
1427 }
1428
1429 #[test]
1430 fn custom_builtin_command_uses_default_bin_path() {
1431 let runtime = enter_tokio_runtime();
1432 let _guard = runtime.as_ref().map(|rt| rt.enter());
1433
1434 let init = WasiEnvBuilder::new("test_prog")
1435 .engine(Engine::default())
1436 .builtin_command(TestBuiltinCommand::new("custom"))
1437 .build_init()
1438 .unwrap();
1439
1440 assert!(init.bin_factory.commands.exists("/bin/custom"));
1441 }
1442
1443 #[test]
1444 fn custom_builtin_command_supports_custom_path() {
1445 let runtime = enter_tokio_runtime();
1446 let _guard = runtime.as_ref().map(|rt| rt.enter());
1447
1448 let init = WasiEnvBuilder::new("test_prog")
1449 .engine(Engine::default())
1450 .builtin_command_with_path(TestBuiltinCommand::new("custom"), "/custom/bin/custom")
1451 .build_init()
1452 .unwrap();
1453
1454 assert!(init.bin_factory.commands.exists("/custom/bin/custom"));
1455 }
1456
1457 #[test]
1458 fn can_disable_default_builtin_commands() {
1459 let runtime = enter_tokio_runtime();
1460 let _guard = runtime.as_ref().map(|rt| rt.enter());
1461
1462 let init = WasiEnvBuilder::new("test_prog")
1463 .engine(Engine::default())
1464 .disable_default_builtins(true)
1465 .build_init()
1466 .unwrap();
1467
1468 assert!(!init.bin_factory.commands.exists("/bin/wasmer"));
1469 }
1470
1471 #[test]
1472 fn builtin_command_registration_overwrites_existing_path() {
1473 let runtime = enter_tokio_runtime();
1474 let _guard = runtime.as_ref().map(|rt| rt.enter());
1475
1476 let init = WasiEnvBuilder::new("test_prog")
1477 .engine(Engine::default())
1478 .builtin_command_with_path(TestBuiltinCommand::new("first"), "/bin/custom")
1479 .builtin_command_with_path(TestBuiltinCommand::new("second"), "/bin/custom")
1480 .build_init()
1481 .unwrap();
1482
1483 let command = init.bin_factory.commands.get("/bin/custom").unwrap();
1484 assert_eq!(command.name(), "second");
1485 }
1486
1487 #[test]
1488 fn closure_based_builtin_command_can_be_registered() {
1489 let runtime = enter_tokio_runtime();
1490 let _guard = runtime.as_ref().map(|rt| rt.enter());
1491
1492 let command = BuiltinCommand::new("closure", |_parent_ctx, _path, _config| {
1493 let handle = OwnedTaskStatus::new_finished_with_code(Errno::Success.into()).handle();
1494 Ok(handle)
1495 });
1496
1497 let init = WasiEnvBuilder::new("test_prog")
1498 .engine(Engine::default())
1499 .builtin_command(command)
1500 .build_init()
1501 .unwrap();
1502
1503 assert!(init.bin_factory.commands.exists("/bin/closure"));
1504 }
1505}