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