1use std::{
4 collections::{HashMap, HashSet},
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use rand::RngExt;
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::command::VirtualCommand,
23 os::task::control_plane::{ControlPlaneConfig, ControlPlaneError, WasiControlPlane},
24 state::WasiState,
25 syscalls::types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO},
26};
27use wasmer_types::ModuleHash;
28use wasmer_wasix_types::wasi::SignalDisposition;
29
30use super::env::WasiEnvInit;
31
32#[derive(Default)]
49pub struct WasiEnvBuilder {
50 pub(super) entry_function: Option<String>,
52 pub(super) args: Vec<String>,
54 pub(super) envs: Vec<(String, Vec<u8>)>,
56 pub(super) signals: Vec<SignalDisposition>,
58 pub(super) preopens: Vec<PreopenedDir>,
60 vfs_preopens: Vec<String>,
62 #[allow(clippy::type_complexity)]
63 pub(super) setup_fs_fn:
64 Option<Box<dyn Fn(&WasiInodes, &mut WasiFs) -> Result<(), String> + Send>>,
65 pub(super) stdout: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
66 pub(super) stderr: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
67 pub(super) stdin: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
68 pub(super) fs: Option<WasiFsRoot>,
69 pub(super) engine: Option<Engine>,
70 pub(super) runtime: Option<Arc<dyn crate::Runtime + Send + Sync + 'static>>,
71 pub(super) current_dir: Option<PathBuf>,
72
73 pub(super) uses: Vec<BinaryPackage>,
75
76 pub(super) included_packages: HashSet<PackageId>,
77
78 pub(super) module_hash: Option<ModuleHash>,
79
80 pub(super) map_commands: HashMap<String, PathBuf>,
82 pub(super) disable_default_builtins: bool,
84 pub(super) builtin_commands: Vec<(String, Arc<dyn VirtualCommand + Send + Sync + 'static>)>,
86
87 pub(super) capabilites: Capabilities,
88
89 #[cfg(feature = "journal")]
90 pub(super) snapshot_on: Vec<SnapshotTrigger>,
91
92 #[cfg(feature = "journal")]
93 pub(super) snapshot_interval: Option<std::time::Duration>,
94
95 #[cfg(feature = "journal")]
96 pub(super) stop_running_after_snapshot: bool,
97
98 #[cfg(feature = "journal")]
99 pub(super) read_only_journals: Vec<Arc<DynReadableJournal>>,
100
101 #[cfg(feature = "journal")]
102 pub(super) writable_journals: Vec<Arc<DynJournal>>,
103
104 pub(super) skip_stdio_during_bootstrap: bool,
105
106 #[cfg(feature = "ctrlc")]
107 pub(super) attach_ctrl_c: bool,
108}
109
110impl std::fmt::Debug for WasiEnvBuilder {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("WasiEnvBuilder")
114 .field("entry_function", &self.entry_function)
115 .field("args", &self.args)
116 .field("envs", &self.envs)
117 .field("signals", &self.signals)
118 .field("preopens", &self.preopens)
119 .field("uses", &self.uses)
120 .field("setup_fs_fn exists", &self.setup_fs_fn.is_some())
121 .field("stdout_override exists", &self.stdout.is_some())
122 .field("stderr_override exists", &self.stderr.is_some())
123 .field("stdin_override exists", &self.stdin.is_some())
124 .field("disable_default_builtins", &self.disable_default_builtins)
125 .field("builtin_commands_count", &self.builtin_commands.len())
126 .field("engine_override_exists", &self.engine.is_some())
127 .field("runtime_override_exists", &self.runtime.is_some())
128 .finish()
129 }
130}
131
132#[derive(Error, Debug, Clone, PartialEq, Eq)]
134pub enum WasiStateCreationError {
135 #[error("bad environment variable format: `{0}`")]
136 EnvironmentVariableFormatError(String),
137 #[error("argument contains null byte: `{0}`")]
138 ArgumentContainsNulByte(String),
139 #[error("preopened directory not found: `{0}`")]
140 PreopenedDirectoryNotFound(PathBuf),
141 #[error("preopened directory error: `{0}`")]
142 PreopenedDirectoryError(String),
143 #[error("mapped dir alias has wrong format: `{0}`")]
144 MappedDirAliasFormattingError(String),
145 #[error("wasi filesystem creation error: `{0}`")]
146 WasiFsCreationError(String),
147 #[error("wasi filesystem setup error: `{0}`")]
148 WasiFsSetupError(String),
149 #[error(transparent)]
150 FileSystemError(#[from] FsError),
151 #[error("wasi inherit error: `{0}`")]
152 WasiInheritError(String),
153 #[error("wasi include package: `{0}`")]
154 WasiIncludePackageError(String),
155 #[error("control plane error")]
156 ControlPlane(#[from] ControlPlaneError),
157}
158
159fn validate_mapped_dir_alias(alias: &str) -> Result<(), WasiStateCreationError> {
160 if !alias.bytes().all(|b| b != b'\0') {
161 return Err(WasiStateCreationError::MappedDirAliasFormattingError(
162 format!("Alias \"{alias}\" contains a nul byte"),
163 ));
164 }
165
166 Ok(())
167}
168
169pub type SetupFsFn = Box<dyn Fn(&WasiInodes, &mut WasiFs) -> Result<(), String> + Send>;
170
171impl WasiEnvBuilder {
174 pub fn new(program_name: impl Into<String>) -> Self {
176 WasiEnvBuilder {
177 args: vec![program_name.into()],
178 ..WasiEnvBuilder::default()
179 }
180 }
181
182 #[cfg(feature = "ctrlc")]
185 pub fn attach_ctrl_c(mut self) -> Self {
186 self.attach_ctrl_c = true;
187 self
188 }
189
190 pub fn env<Key, Value>(mut self, key: Key, value: Value) -> Self
196 where
197 Key: AsRef<[u8]>,
198 Value: AsRef<[u8]>,
199 {
200 self.add_env(key, value);
201 self
202 }
203
204 pub fn add_env<Key, Value>(&mut self, key: Key, value: Value)
210 where
211 Key: AsRef<[u8]>,
212 Value: AsRef<[u8]>,
213 {
214 self.envs.push((
215 String::from_utf8_lossy(key.as_ref()).to_string(),
216 value.as_ref().to_vec(),
217 ));
218 }
219
220 pub fn envs<I, Key, Value>(mut self, env_pairs: I) -> Self
226 where
227 I: IntoIterator<Item = (Key, Value)>,
228 Key: AsRef<[u8]>,
229 Value: AsRef<[u8]>,
230 {
231 self.add_envs(env_pairs);
232
233 self
234 }
235
236 pub fn add_envs<I, Key, Value>(&mut self, env_pairs: I)
242 where
243 I: IntoIterator<Item = (Key, Value)>,
244 Key: AsRef<[u8]>,
245 Value: AsRef<[u8]>,
246 {
247 for (key, value) in env_pairs {
248 self.add_env(key, value);
249 }
250 }
251
252 pub fn get_env(&self) -> &[(String, Vec<u8>)] {
254 &self.envs
255 }
256
257 pub fn get_env_mut(&mut self) -> &mut Vec<(String, Vec<u8>)> {
259 &mut self.envs
260 }
261
262 pub fn signal(mut self, sig_action: SignalDisposition) -> Self {
264 self.add_signal(sig_action);
265 self
266 }
267
268 pub fn add_signal(&mut self, sig_action: SignalDisposition) {
270 self.signals.push(sig_action);
271 }
272
273 pub fn signals<I>(mut self, signal_pairs: I) -> Self
275 where
276 I: IntoIterator<Item = SignalDisposition>,
277 {
278 self.add_signals(signal_pairs);
279
280 self
281 }
282
283 pub fn add_signals<I>(&mut self, signal_pairs: I)
285 where
286 I: IntoIterator<Item = SignalDisposition>,
287 {
288 for sig in signal_pairs {
289 self.add_signal(sig);
290 }
291 }
292
293 pub fn get_signals(&self) -> &[SignalDisposition] {
295 &self.signals
296 }
297
298 pub fn get_signals_mut(&mut self) -> &mut Vec<SignalDisposition> {
300 &mut self.signals
301 }
302
303 pub fn entry_function<S>(mut self, entry_function: S) -> Self
304 where
305 S: AsRef<str>,
306 {
307 self.set_entry_function(entry_function);
308 self
309 }
310
311 pub fn set_entry_function<S>(&mut self, entry_function: S)
312 where
313 S: AsRef<str>,
314 {
315 self.entry_function = Some(entry_function.as_ref().to_owned());
316 }
317
318 pub fn arg<V>(mut self, arg: V) -> Self
323 where
324 V: AsRef<[u8]>,
325 {
326 self.add_arg(arg);
327 self
328 }
329
330 pub fn add_arg<V>(&mut self, arg: V)
335 where
336 V: AsRef<[u8]>,
337 {
338 self.args
339 .push(String::from_utf8_lossy(arg.as_ref()).to_string());
340 }
341
342 pub fn args<I, Arg>(mut self, args: I) -> Self
346 where
347 I: IntoIterator<Item = Arg>,
348 Arg: AsRef<[u8]>,
349 {
350 self.add_args(args);
351
352 self
353 }
354
355 pub fn add_args<I, Arg>(&mut self, args: I)
359 where
360 I: IntoIterator<Item = Arg>,
361 Arg: AsRef<[u8]>,
362 {
363 for arg in args {
364 self.add_arg(arg);
365 }
366 }
367
368 pub fn get_args(&self) -> &[String] {
370 &self.args
371 }
372
373 pub fn get_args_mut(&mut self) -> &mut Vec<String> {
375 &mut self.args
376 }
377
378 pub fn use_webc(mut self, pkg: BinaryPackage) -> Self {
383 self.add_webc(pkg);
384 self
385 }
386
387 pub fn set_module_hash(&mut self, hash: ModuleHash) -> &mut Self {
391 self.module_hash.replace(hash);
392 self
393 }
394
395 pub fn add_webc(&mut self, pkg: BinaryPackage) -> &mut Self {
400 self.uses.push(pkg);
401 self
402 }
403
404 pub fn include_package(&mut self, pkg_id: PackageId) -> &mut Self {
407 self.included_packages.insert(pkg_id);
408 self
409 }
410
411 pub fn include_packages(&mut self, pkg_ids: impl IntoIterator<Item = PackageId>) -> &mut Self {
414 self.included_packages.extend(pkg_ids);
415
416 self
417 }
418
419 pub fn uses<I>(mut self, uses: I) -> Self
424 where
425 I: IntoIterator<Item = BinaryPackage>,
426 {
427 for pkg in uses {
428 self.add_webc(pkg);
429 }
430 self
431 }
432
433 pub fn disable_default_builtins(mut self, disable_default_builtins: bool) -> Self {
435 self.set_disable_default_builtins(disable_default_builtins);
436 self
437 }
438
439 pub fn set_disable_default_builtins(&mut self, disable_default_builtins: bool) {
441 self.disable_default_builtins = disable_default_builtins;
442 }
443
444 pub fn builtin_command<C>(mut self, command: C) -> Self
446 where
447 C: VirtualCommand + Send + Sync + 'static,
448 {
449 self.add_builtin_command(command);
450 self
451 }
452
453 pub fn add_builtin_command<C>(&mut self, command: C)
455 where
456 C: VirtualCommand + Send + Sync + 'static,
457 {
458 let path = format!("/bin/{}", command.name());
459 self.add_builtin_command_with_path(command, path);
460 }
461
462 pub fn builtin_command_with_path<C, P>(mut self, command: C, path: P) -> Self
464 where
465 C: VirtualCommand + Send + Sync + 'static,
466 P: Into<String>,
467 {
468 self.add_builtin_command_with_path(command, path);
469 self
470 }
471
472 pub fn add_builtin_command_with_path<C, P>(&mut self, command: C, path: P)
474 where
475 C: VirtualCommand + Send + Sync + 'static,
476 P: Into<String>,
477 {
478 self.add_builtin_command_with_path_shared(Arc::new(command), path);
479 }
480
481 fn add_builtin_command_with_path_shared<P>(
483 &mut self,
484 command: Arc<dyn VirtualCommand + Send + Sync + 'static>,
485 path: P,
486 ) where
487 P: Into<String>,
488 {
489 self.builtin_commands.push((path.into(), command));
490 }
491
492 pub fn map_command<Name, Target>(mut self, name: Name, target: Target) -> Self
494 where
495 Name: AsRef<str>,
496 Target: AsRef<str>,
497 {
498 self.add_mapped_command(name, target);
499 self
500 }
501
502 pub fn add_mapped_command<Name, Target>(&mut self, name: Name, target: Target)
504 where
505 Name: AsRef<str>,
506 Target: AsRef<str>,
507 {
508 let path_buf = PathBuf::from(target.as_ref().to_string());
509 self.map_commands
510 .insert(name.as_ref().to_string(), path_buf);
511 }
512
513 pub fn map_commands<I, Name, Target>(mut self, map_commands: I) -> Self
515 where
516 I: IntoIterator<Item = (Name, Target)>,
517 Name: AsRef<str>,
518 Target: AsRef<str>,
519 {
520 self.add_mapped_commands(map_commands);
521 self
522 }
523
524 pub fn add_mapped_commands<I, Name, Target>(&mut self, map_commands: I)
526 where
527 I: IntoIterator<Item = (Name, Target)>,
528 Name: AsRef<str>,
529 Target: AsRef<str>,
530 {
531 for (alias, target) in map_commands {
532 self.add_mapped_command(alias, target);
533 }
534 }
535
536 pub fn preopen_dir<P>(mut self, po_dir: P) -> Result<Self, WasiStateCreationError>
541 where
542 P: AsRef<Path>,
543 {
544 self.add_preopen_dir(po_dir)?;
545 Ok(self)
546 }
547
548 pub fn add_preopen_dir<P>(&mut self, po_dir: P) -> Result<(), WasiStateCreationError>
553 where
554 P: AsRef<Path>,
555 {
556 let mut pdb = PreopenDirBuilder::new();
557 let path = po_dir.as_ref();
558 pdb.directory(path).read(true).write(true).create(true);
559 let preopen = pdb.build()?;
560
561 self.preopens.push(preopen);
562
563 Ok(())
564 }
565
566 pub fn preopen_dirs<I, P>(mut self, dirs: I) -> Result<Self, WasiStateCreationError>
571 where
572 I: IntoIterator<Item = P>,
573 P: AsRef<Path>,
574 {
575 for po_dir in dirs {
576 self.add_preopen_dir(po_dir)?;
577 }
578
579 Ok(self)
580 }
581
582 pub fn preopen_build<F>(mut self, inner: F) -> Result<Self, WasiStateCreationError>
597 where
598 F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
599 {
600 self.add_preopen_build(inner)?;
601 Ok(self)
602 }
603
604 pub fn add_preopen_build<F>(&mut self, inner: F) -> Result<(), WasiStateCreationError>
619 where
620 F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
621 {
622 let mut pdb = PreopenDirBuilder::new();
623 let po_dir = inner(&mut pdb).build()?;
624
625 self.preopens.push(po_dir);
626
627 Ok(())
628 }
629
630 pub fn preopen_vfs_dirs<I>(&mut self, po_dirs: I) -> Result<&mut Self, WasiStateCreationError>
633 where
634 I: IntoIterator<Item = String>,
635 {
636 for po_dir in po_dirs {
637 self.vfs_preopens.push(po_dir);
638 }
639
640 Ok(self)
641 }
642
643 pub fn map_dir<P>(mut self, alias: &str, po_dir: P) -> Result<Self, WasiStateCreationError>
645 where
646 P: AsRef<Path>,
647 {
648 self.add_map_dir(alias, po_dir)?;
649 Ok(self)
650 }
651
652 pub fn add_map_dir<P>(&mut self, alias: &str, po_dir: P) -> Result<(), WasiStateCreationError>
654 where
655 P: AsRef<Path>,
656 {
657 let mut pdb = PreopenDirBuilder::new();
658 let path = po_dir.as_ref();
659 pdb.directory(path)
660 .alias(alias)
661 .read(true)
662 .write(true)
663 .create(true);
664 let preopen = pdb.build()?;
665
666 self.preopens.push(preopen);
667
668 Ok(())
669 }
670
671 pub fn map_dirs<I, P>(mut self, mapped_dirs: I) -> Result<Self, WasiStateCreationError>
673 where
674 I: IntoIterator<Item = (String, P)>,
675 P: AsRef<Path>,
676 {
677 for (alias, dir) in mapped_dirs {
678 self.add_map_dir(&alias, dir)?;
679 }
680
681 Ok(self)
682 }
683
684 #[cfg(feature = "journal")]
690 pub fn add_read_only_journal(&mut self, journal: Arc<DynReadableJournal>) {
691 self.read_only_journals.push(journal);
692 }
693
694 #[cfg(feature = "journal")]
704 pub fn add_writable_journal(&mut self, journal: Arc<DynJournal>) {
705 self.writable_journals.push(journal);
706 }
707
708 pub fn get_current_dir(&mut self) -> Option<PathBuf> {
709 self.current_dir.clone()
710 }
711
712 pub fn set_current_dir(&mut self, dir: impl Into<PathBuf>) {
713 self.current_dir = Some(dir.into());
714 }
715
716 pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
717 self.set_current_dir(dir);
718 self
719 }
720
721 pub fn stdout(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
724 self.stdout = Some(new_file);
725
726 self
727 }
728
729 pub fn set_stdout(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
732 self.stdout = Some(new_file);
733 }
734
735 pub fn stderr(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
738 self.set_stderr(new_file);
739 self
740 }
741
742 pub fn set_stderr(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
745 self.stderr = Some(new_file);
746 }
747
748 pub fn stdin(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
751 self.stdin = Some(new_file);
752
753 self
754 }
755
756 pub fn set_stdin(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
759 self.stdin = Some(new_file);
760 }
761
762 pub fn fs(mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) -> Self {
766 self.set_fs(fs);
767 self
768 }
769
770 pub fn set_fs(&mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) {
771 self.fs = Some(WasiFsRoot::Backing(fs.into()));
772 }
773
774 pub(crate) fn set_fs_root(&mut self, fs: WasiFsRoot) {
775 self.fs = Some(fs);
776 }
777
778 pub fn sandbox_fs(mut self, fs: TmpFileSystem) -> Self {
782 self.fs = Some(WasiFsRoot::Sandbox(fs));
783 self
784 }
785
786 pub fn setup_fs(mut self, setup_fs_fn: SetupFsFn) -> Self {
789 self.setup_fs_fn = Some(setup_fs_fn);
790
791 self
792 }
793
794 pub fn engine(mut self, engine: Engine) -> Self {
797 self.set_engine(engine);
798 self
799 }
800
801 pub fn set_engine(&mut self, engine: Engine) {
802 self.engine = Some(engine);
803 }
804
805 pub fn runtime(mut self, runtime: Arc<dyn Runtime + Send + Sync>) -> Self {
808 self.set_runtime(runtime);
809 self
810 }
811
812 pub fn set_runtime(&mut self, runtime: Arc<dyn Runtime + Send + Sync>) {
813 self.runtime = Some(runtime);
814 }
815
816 pub fn capabilities(mut self, capabilities: Capabilities) -> Self {
817 self.set_capabilities(capabilities);
818 self
819 }
820
821 pub fn capabilities_mut(&mut self) -> &mut Capabilities {
822 &mut self.capabilites
823 }
824
825 pub fn set_capabilities(&mut self, capabilities: Capabilities) {
826 self.capabilites = capabilities;
827 }
828
829 #[cfg(feature = "journal")]
830 pub fn add_snapshot_trigger(&mut self, on: SnapshotTrigger) {
831 self.snapshot_on.push(on);
832 }
833
834 #[cfg(feature = "journal")]
835 pub fn with_snapshot_interval(&mut self, interval: std::time::Duration) {
836 self.snapshot_interval.replace(interval);
837 }
838
839 #[cfg(feature = "journal")]
840 pub fn with_stop_running_after_snapshot(&mut self, stop_running: bool) {
841 self.stop_running_after_snapshot = stop_running;
842 }
843
844 pub fn with_skip_stdio_during_bootstrap(&mut self, skip: bool) {
845 self.skip_stdio_during_bootstrap = skip;
846 }
847
848 pub fn build_init(mut self) -> Result<WasiEnvInit, WasiStateCreationError> {
857 for arg in self.args.iter() {
858 for b in arg.as_bytes().iter() {
859 if *b == 0 {
860 return Err(WasiStateCreationError::ArgumentContainsNulByte(arg.clone()));
861 }
862 }
863 }
864
865 enum InvalidCharacter {
866 Nul,
867 Equal,
868 }
869
870 for (env_key, env_value) in self.envs.iter() {
871 match env_key.as_bytes().iter().find_map(|&ch| {
872 if ch == 0 {
873 Some(InvalidCharacter::Nul)
874 } else if ch == b'=' {
875 Some(InvalidCharacter::Equal)
876 } else {
877 None
878 }
879 }) {
880 Some(InvalidCharacter::Nul) => {
881 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
882 format!("found nul byte in env var key \"{env_key}\" (key=value)"),
883 ));
884 }
885
886 Some(InvalidCharacter::Equal) => {
887 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
888 format!("found equal sign in env var key \"{env_key}\" (key=value)"),
889 ));
890 }
891
892 None => (),
893 }
894
895 if env_value.contains(&0) {
896 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
897 format!(
898 "found nul byte in env var value \"{}\" (key=value)",
899 String::from_utf8_lossy(env_value),
900 ),
901 ));
902 }
903 }
904
905 let stdin: Box<dyn VirtualFile + Send + Sync + 'static> = self
907 .stdin
908 .take()
909 .unwrap_or_else(|| Box::new(ArcFile::new(Box::<super::Stdin>::default())));
910
911 let fs_backing = self
912 .fs
913 .take()
914 .unwrap_or_else(|| WasiFsRoot::Sandbox(TmpFileSystem::new()));
915
916 if let Some(dir) = &self.current_dir {
917 match fs_backing.read_dir(dir) {
918 Ok(_) => {
919 }
921 Err(FsError::EntryNotFound) => {
922 fs_backing.create_dir(dir).map_err(|err| {
923 WasiStateCreationError::WasiFsSetupError(format!(
924 "Could not create specified current directory at '{}': {err}",
925 dir.display()
926 ))
927 })?;
928 }
929 Err(err) => {
930 return Err(WasiStateCreationError::WasiFsSetupError(format!(
931 "Could not check specified current directory at '{}': {err}",
932 dir.display()
933 )));
934 }
935 }
936 }
937
938 let inodes = crate::state::WasiInodes::new();
940 let wasi_fs = {
941 let mut wasi_fs =
943 WasiFs::new_with_preopen(&inodes, &self.preopens, &self.vfs_preopens, fs_backing)
944 .map_err(WasiStateCreationError::WasiFsCreationError)?;
945
946 wasi_fs
948 .swap_file(__WASI_STDIN_FILENO, stdin)
949 .map_err(WasiStateCreationError::FileSystemError)?;
950
951 if let Some(stdout_override) = self.stdout.take() {
952 wasi_fs
953 .swap_file(__WASI_STDOUT_FILENO, stdout_override)
954 .map_err(WasiStateCreationError::FileSystemError)?;
955 }
956
957 if let Some(stderr_override) = self.stderr.take() {
958 wasi_fs
959 .swap_file(__WASI_STDERR_FILENO, stderr_override)
960 .map_err(WasiStateCreationError::FileSystemError)?;
961 }
962
963 if let Some(f) = &self.setup_fs_fn {
964 f(&inodes, &mut wasi_fs).map_err(WasiStateCreationError::WasiFsSetupError)?;
965 }
966 wasi_fs
967 };
968
969 if let Some(dir) = &self.current_dir {
970 let s = dir.to_str().ok_or_else(|| {
971 WasiStateCreationError::WasiFsSetupError(format!(
972 "Specified current directory is not valid UTF-8: '{}'",
973 dir.display()
974 ))
975 })?;
976 wasi_fs.set_current_dir(s);
977 }
978
979 for id in &self.included_packages {
980 wasi_fs.has_unioned.lock().unwrap().insert(id.clone());
981 }
982
983 let state = WasiState {
984 fs: wasi_fs,
985 secret: rand::rng().random::<[u8; 32]>(),
986 inodes,
987 args: std::sync::Mutex::new(self.args.clone()),
988 preopen: self.vfs_preopens.clone(),
989 futexs: Default::default(),
990 clock_offset: Default::default(),
991 envs: std::sync::Mutex::new(conv_env_vars(self.envs)),
992 signals: std::sync::Mutex::new(self.signals.iter().map(|s| (s.sig, s.disp)).collect()),
993 };
994
995 let runtime = self.runtime.unwrap_or_else(|| {
996 #[cfg(feature = "sys-thread")]
997 {
998 #[allow(unused_mut)]
999 let mut runtime = crate::runtime::PluggableRuntime::new(Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::default()));
1000 runtime.set_engine(
1001 self
1002 .engine
1003 .as_ref()
1004 .expect(
1005 "Neither a runtime nor an engine was provided to WasiEnvBuilder. \
1006 This is not supported because it means the module that's going to \
1007 run with the resulting WasiEnv will have been loaded using a \
1008 different engine than the one that will exist within the WasiEnv. \
1009 Use either `set_runtime` or `set_engine` before calling `build_init`.",
1010 )
1011 .clone()
1012 );
1013 #[cfg(feature = "journal")]
1014 for journal in self.read_only_journals.clone() {
1015 runtime.add_read_only_journal(journal);
1016 }
1017 #[cfg(feature = "journal")]
1018 for journal in self.writable_journals.clone() {
1019 runtime.add_writable_journal(journal);
1020 }
1021 Arc::new(runtime)
1022 }
1023
1024 #[cfg(not(feature = "sys-thread"))]
1025 {
1026 panic!("this build does not support a default runtime - specify one with WasiEnvBuilder::runtime()");
1027 }
1028 });
1029
1030 let uses = self.uses;
1031 let map_commands = self.map_commands;
1032 let disable_default_builtins = self.disable_default_builtins;
1033 let builtin_commands = self.builtin_commands;
1034
1035 let mut bin_factory = BinFactory::new(runtime.clone());
1036 if disable_default_builtins {
1037 bin_factory.clear_builtin_commands();
1038 }
1039 for (path, command) in builtin_commands {
1040 bin_factory.register_builtin_command_with_path_shared(command, path);
1041 }
1042
1043 let capabilities = self.capabilites;
1044
1045 let plane_config = ControlPlaneConfig {
1046 max_task_count: capabilities.threading.max_threads,
1047 enable_asynchronous_threading: capabilities.threading.enable_asynchronous_threading,
1048 enable_exponential_cpu_backoff: capabilities.threading.enable_exponential_cpu_backoff,
1049 };
1050 let control_plane = WasiControlPlane::new(plane_config);
1051
1052 let init = WasiEnvInit {
1053 state,
1054 runtime,
1055 webc_dependencies: uses,
1056 mapped_commands: map_commands,
1057 control_plane,
1058 bin_factory,
1059 capabilities,
1060 memory_ty: None,
1061 process: None,
1062 thread: None,
1063 #[cfg(feature = "journal")]
1064 call_initialize: self.read_only_journals.is_empty()
1065 && self.writable_journals.is_empty(),
1066 #[cfg(not(feature = "journal"))]
1067 call_initialize: true,
1068 can_deep_sleep: false,
1069 extra_tracing: true,
1070 #[cfg(feature = "journal")]
1071 snapshot_on: self.snapshot_on,
1072 #[cfg(feature = "journal")]
1073 stop_running_after_snapshot: self.stop_running_after_snapshot,
1074 skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
1075 };
1076
1077 Ok(init)
1078 }
1079
1080 #[allow(clippy::result_large_err)]
1081 pub fn build(self) -> Result<WasiEnv, WasiRuntimeError> {
1082 let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1083 let init = self.build_init()?;
1084 WasiEnv::from_init(init, module_hash)
1085 }
1086
1087 #[doc(hidden)]
1092 #[allow(clippy::result_large_err)]
1093 pub fn finalize(
1094 self,
1095 store: &mut impl AsStoreMut,
1096 ) -> Result<WasiFunctionEnv, WasiRuntimeError> {
1097 let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1098 let init = self.build_init()?;
1099 let env = WasiEnv::from_init(init, module_hash)?;
1100 let func_env = WasiFunctionEnv::new(store, env);
1101 Ok(func_env)
1102 }
1103
1104 #[allow(clippy::result_large_err)]
1110 pub fn instantiate(
1111 self,
1112 module: Module,
1113 store: &mut impl AsStoreMut,
1114 ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1115 self.instantiate_ext(module, ModuleHash::random(), store)
1116 }
1117
1118 #[allow(clippy::result_large_err)]
1119 pub fn instantiate_ext(
1120 self,
1121 module: Module,
1122 module_hash: ModuleHash,
1123 store: &mut impl AsStoreMut,
1124 ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1125 let init = self.build_init()?;
1126 let call_init = init.call_initialize;
1127 let env = WasiEnv::from_init(init, module_hash)?;
1128 let memory = module
1129 .imports()
1130 .find_map(|i| match i.ty() {
1131 wasmer::ExternType::Memory(ty) => Some(*ty),
1132 _ => None,
1133 })
1134 .map(|ty| wasmer::Memory::new(store, ty))
1135 .transpose()
1136 .map_err(WasiThreadError::MemoryCreateFailed)?;
1137 Ok(env.instantiate(module, store, memory, true, call_init, None)?)
1138 }
1139}
1140
1141pub(crate) fn conv_env_vars(envs: Vec<(String, Vec<u8>)>) -> Vec<Vec<u8>> {
1142 envs.into_iter()
1143 .map(|(key, value)| {
1144 let mut env = Vec::with_capacity(key.len() + value.len() + 1);
1145 env.extend_from_slice(key.as_bytes());
1146 env.push(b'=');
1147 env.extend_from_slice(&value);
1148
1149 env
1150 })
1151 .collect()
1152}
1153
1154#[derive(Debug, Default)]
1156pub struct PreopenDirBuilder {
1157 path: Option<PathBuf>,
1158 alias: Option<String>,
1159 read: bool,
1160 write: bool,
1161 create: bool,
1162}
1163
1164#[derive(Debug, Clone, Default)]
1166pub(crate) struct PreopenedDir {
1167 pub(crate) path: PathBuf,
1168 pub(crate) alias: Option<String>,
1169 pub(crate) read: bool,
1170 pub(crate) write: bool,
1171 pub(crate) create: bool,
1172}
1173
1174impl PreopenDirBuilder {
1175 pub(crate) fn new() -> Self {
1177 PreopenDirBuilder::default()
1178 }
1179
1180 pub fn directory<FilePath>(&mut self, po_dir: FilePath) -> &mut Self
1182 where
1183 FilePath: AsRef<Path>,
1184 {
1185 let path = po_dir.as_ref();
1186 self.path = Some(path.to_path_buf());
1187
1188 self
1189 }
1190
1191 pub fn alias(&mut self, alias: &str) -> &mut Self {
1193 let alias = alias.trim_start_matches('/');
1196 self.alias = Some(alias.to_string());
1197
1198 self
1199 }
1200
1201 pub fn read(&mut self, toggle: bool) -> &mut Self {
1203 self.read = toggle;
1204
1205 self
1206 }
1207
1208 pub fn write(&mut self, toggle: bool) -> &mut Self {
1210 self.write = toggle;
1211
1212 self
1213 }
1214
1215 pub fn create(&mut self, toggle: bool) -> &mut Self {
1219 self.create = toggle;
1220 if toggle {
1221 self.write = true;
1222 }
1223
1224 self
1225 }
1226
1227 pub(crate) fn build(&self) -> Result<PreopenedDir, WasiStateCreationError> {
1228 if !(self.read || self.write || self.create) {
1230 return Err(WasiStateCreationError::PreopenedDirectoryError("Preopened directories must have at least one of read, write, create permissions set".to_string()));
1231 }
1232
1233 if self.path.is_none() {
1234 return Err(WasiStateCreationError::PreopenedDirectoryError(
1235 "Preopened directories must point to a host directory".to_string(),
1236 ));
1237 }
1238 let path = self.path.clone().unwrap();
1239
1240 if let Some(alias) = &self.alias {
1247 validate_mapped_dir_alias(alias)?;
1248 }
1249
1250 Ok(PreopenedDir {
1251 path,
1252 alias: self.alias.clone(),
1253 read: self.read,
1254 write: self.write,
1255 create: self.create,
1256 })
1257 }
1258}
1259
1260#[cfg(test)]
1261mod test {
1262 use super::*;
1263 use crate::{
1264 SpawnError,
1265 os::{
1266 command::{BuiltinCommand, VirtualCommand},
1267 task::{OwnedTaskStatus, TaskJoinHandle},
1268 },
1269 };
1270 use wasmer::FunctionEnvMut;
1271 use wasmer_wasix_types::wasi::Errno;
1272
1273 fn enter_tokio_runtime() -> Option<tokio::runtime::Runtime> {
1274 #[cfg(not(target_arch = "wasm32"))]
1275 {
1276 let runtime = tokio::runtime::Builder::new_multi_thread()
1277 .enable_all()
1278 .build()
1279 .unwrap();
1280 Some(runtime)
1281 }
1282
1283 #[cfg(target_arch = "wasm32")]
1284 {
1285 None
1286 }
1287 }
1288
1289 #[derive(Debug)]
1290 struct TestBuiltinCommand {
1291 name: &'static str,
1292 }
1293
1294 impl TestBuiltinCommand {
1295 fn new(name: &'static str) -> Self {
1296 Self { name }
1297 }
1298 }
1299
1300 impl VirtualCommand for TestBuiltinCommand {
1301 fn name(&self) -> &str {
1302 self.name
1303 }
1304
1305 fn as_any(&self) -> &dyn std::any::Any {
1306 self
1307 }
1308
1309 fn exec(
1310 &self,
1311 _parent_ctx: &FunctionEnvMut<'_, WasiEnv>,
1312 _path: &str,
1313 _config: &mut Option<WasiEnv>,
1314 ) -> Result<TaskJoinHandle, SpawnError> {
1315 let handle = OwnedTaskStatus::new_finished_with_code(Errno::Success.into()).handle();
1316 Ok(handle)
1317 }
1318 }
1319
1320 #[test]
1321 fn env_var_errors() {
1322 #[cfg(not(target_arch = "wasm32"))]
1323 let runtime = tokio::runtime::Builder::new_multi_thread()
1324 .enable_all()
1325 .build()
1326 .unwrap();
1327 #[cfg(not(target_arch = "wasm32"))]
1328 let handle = runtime.handle().clone();
1329 #[cfg(not(target_arch = "wasm32"))]
1330 let _guard = handle.enter();
1331
1332 assert!(
1334 WasiEnv::builder("test_prog")
1335 .env("HOM=E", "/home/home")
1336 .build_init()
1337 .is_err(),
1338 "equal sign in key must be invalid"
1339 );
1340
1341 assert!(
1343 WasiEnvBuilder::new("test_prog")
1344 .env("HOME\0", "/home/home")
1345 .build_init()
1346 .is_err(),
1347 "nul in key must be invalid"
1348 );
1349
1350 assert!(
1352 WasiEnvBuilder::new("test_prog")
1353 .env("HOME", "/home/home\0")
1354 .build_init()
1355 .is_err(),
1356 "nul in value must be invalid"
1357 );
1358
1359 assert!(
1361 WasiEnvBuilder::new("test_prog")
1362 .env("HOME", "/home/home=home")
1363 .engine(Engine::default())
1364 .build_init()
1365 .is_ok(),
1366 "equal sign in the value must be valid"
1367 );
1368 }
1369
1370 #[test]
1371 fn nul_character_in_args() {
1372 let output = WasiEnvBuilder::new("test_prog")
1373 .arg("--h\0elp")
1374 .build_init();
1375 let err = output.expect_err("should fail");
1376 assert!(matches!(
1377 err,
1378 WasiStateCreationError::ArgumentContainsNulByte(_)
1379 ));
1380
1381 let output = WasiEnvBuilder::new("test_prog")
1382 .args(["--help", "--wat\0"])
1383 .build_init();
1384 let err = output.expect_err("should fail");
1385 assert!(matches!(
1386 err,
1387 WasiStateCreationError::ArgumentContainsNulByte(_)
1388 ));
1389 }
1390
1391 #[test]
1392 fn custom_builtin_command_uses_default_bin_path() {
1393 let runtime = enter_tokio_runtime();
1394 let _guard = runtime.as_ref().map(|rt| rt.enter());
1395
1396 let init = WasiEnvBuilder::new("test_prog")
1397 .engine(Engine::default())
1398 .builtin_command(TestBuiltinCommand::new("custom"))
1399 .build_init()
1400 .unwrap();
1401
1402 assert!(init.bin_factory.commands.exists("/bin/custom"));
1403 }
1404
1405 #[test]
1406 fn custom_builtin_command_supports_custom_path() {
1407 let runtime = enter_tokio_runtime();
1408 let _guard = runtime.as_ref().map(|rt| rt.enter());
1409
1410 let init = WasiEnvBuilder::new("test_prog")
1411 .engine(Engine::default())
1412 .builtin_command_with_path(TestBuiltinCommand::new("custom"), "/custom/bin/custom")
1413 .build_init()
1414 .unwrap();
1415
1416 assert!(init.bin_factory.commands.exists("/custom/bin/custom"));
1417 }
1418
1419 #[test]
1420 fn can_disable_default_builtin_commands() {
1421 let runtime = enter_tokio_runtime();
1422 let _guard = runtime.as_ref().map(|rt| rt.enter());
1423
1424 let init = WasiEnvBuilder::new("test_prog")
1425 .engine(Engine::default())
1426 .disable_default_builtins(true)
1427 .build_init()
1428 .unwrap();
1429
1430 assert!(!init.bin_factory.commands.exists("/bin/wasmer"));
1431 }
1432
1433 #[test]
1434 fn builtin_command_registration_overwrites_existing_path() {
1435 let runtime = enter_tokio_runtime();
1436 let _guard = runtime.as_ref().map(|rt| rt.enter());
1437
1438 let init = WasiEnvBuilder::new("test_prog")
1439 .engine(Engine::default())
1440 .builtin_command_with_path(TestBuiltinCommand::new("first"), "/bin/custom")
1441 .builtin_command_with_path(TestBuiltinCommand::new("second"), "/bin/custom")
1442 .build_init()
1443 .unwrap();
1444
1445 let command = init.bin_factory.commands.get("/bin/custom").unwrap();
1446 assert_eq!(command.name(), "second");
1447 }
1448
1449 #[test]
1450 fn closure_based_builtin_command_can_be_registered() {
1451 let runtime = enter_tokio_runtime();
1452 let _guard = runtime.as_ref().map(|rt| rt.enter());
1453
1454 let command = BuiltinCommand::new("closure", |_parent_ctx, _path, _config| {
1455 let handle = OwnedTaskStatus::new_finished_with_code(Errno::Success.into()).handle();
1456 Ok(handle)
1457 });
1458
1459 let init = WasiEnvBuilder::new("test_prog")
1460 .engine(Engine::default())
1461 .builtin_command(command)
1462 .build_init()
1463 .unwrap();
1464
1465 assert!(init.bin_factory.commands.exists("/bin/closure"));
1466 }
1467}