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) capabilites: 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 self.envs.push((
217 String::from_utf8_lossy(key.as_ref()).to_string(),
218 value.as_ref().to_vec(),
219 ));
220 }
221
222 pub fn envs<I, Key, Value>(mut self, env_pairs: I) -> Self
228 where
229 I: IntoIterator<Item = (Key, Value)>,
230 Key: AsRef<[u8]>,
231 Value: AsRef<[u8]>,
232 {
233 self.add_envs(env_pairs);
234
235 self
236 }
237
238 pub fn add_envs<I, Key, Value>(&mut self, env_pairs: I)
244 where
245 I: IntoIterator<Item = (Key, Value)>,
246 Key: AsRef<[u8]>,
247 Value: AsRef<[u8]>,
248 {
249 for (key, value) in env_pairs {
250 self.add_env(key, value);
251 }
252 }
253
254 pub fn get_env(&self) -> &[(String, Vec<u8>)] {
256 &self.envs
257 }
258
259 pub fn get_env_mut(&mut self) -> &mut Vec<(String, Vec<u8>)> {
261 &mut self.envs
262 }
263
264 pub fn signal(mut self, sig_action: SignalDisposition) -> Self {
266 self.add_signal(sig_action);
267 self
268 }
269
270 pub fn add_signal(&mut self, sig_action: SignalDisposition) {
272 self.signals.push(sig_action);
273 }
274
275 pub fn signals<I>(mut self, signal_pairs: I) -> Self
277 where
278 I: IntoIterator<Item = SignalDisposition>,
279 {
280 self.add_signals(signal_pairs);
281
282 self
283 }
284
285 pub fn add_signals<I>(&mut self, signal_pairs: I)
287 where
288 I: IntoIterator<Item = SignalDisposition>,
289 {
290 for sig in signal_pairs {
291 self.add_signal(sig);
292 }
293 }
294
295 pub fn get_signals(&self) -> &[SignalDisposition] {
297 &self.signals
298 }
299
300 pub fn get_signals_mut(&mut self) -> &mut Vec<SignalDisposition> {
302 &mut self.signals
303 }
304
305 pub fn entry_function<S>(mut self, entry_function: S) -> Self
306 where
307 S: AsRef<str>,
308 {
309 self.set_entry_function(entry_function);
310 self
311 }
312
313 pub fn set_entry_function<S>(&mut self, entry_function: S)
314 where
315 S: AsRef<str>,
316 {
317 self.entry_function = Some(entry_function.as_ref().to_owned());
318 }
319
320 pub fn arg<V>(mut self, arg: V) -> Self
325 where
326 V: AsRef<[u8]>,
327 {
328 self.add_arg(arg);
329 self
330 }
331
332 pub fn add_arg<V>(&mut self, arg: V)
337 where
338 V: AsRef<[u8]>,
339 {
340 self.args
341 .push(String::from_utf8_lossy(arg.as_ref()).to_string());
342 }
343
344 pub fn args<I, Arg>(mut self, args: I) -> Self
348 where
349 I: IntoIterator<Item = Arg>,
350 Arg: AsRef<[u8]>,
351 {
352 self.add_args(args);
353
354 self
355 }
356
357 pub fn add_args<I, Arg>(&mut self, args: I)
361 where
362 I: IntoIterator<Item = Arg>,
363 Arg: AsRef<[u8]>,
364 {
365 for arg in args {
366 self.add_arg(arg);
367 }
368 }
369
370 pub fn get_args(&self) -> &[String] {
372 &self.args
373 }
374
375 pub fn get_args_mut(&mut self) -> &mut Vec<String> {
377 &mut self.args
378 }
379
380 pub fn use_webc(mut self, pkg: BinaryPackage) -> Self {
385 self.add_webc(pkg);
386 self
387 }
388
389 pub fn set_module_hash(&mut self, hash: ModuleHash) -> &mut Self {
393 self.module_hash.replace(hash);
394 self
395 }
396
397 pub fn add_webc(&mut self, pkg: BinaryPackage) -> &mut Self {
402 self.uses.push(pkg);
403 self
404 }
405
406 pub fn include_package(&mut self, pkg_id: PackageId) -> &mut Self {
409 self.included_packages.insert(pkg_id);
410 self
411 }
412
413 pub fn include_packages(&mut self, pkg_ids: impl IntoIterator<Item = PackageId>) -> &mut Self {
416 self.included_packages.extend(pkg_ids);
417
418 self
419 }
420
421 pub fn uses<I>(mut self, uses: I) -> Self
426 where
427 I: IntoIterator<Item = BinaryPackage>,
428 {
429 for pkg in uses {
430 self.add_webc(pkg);
431 }
432 self
433 }
434
435 pub fn disable_default_builtins(mut self, disable_default_builtins: bool) -> Self {
437 self.set_disable_default_builtins(disable_default_builtins);
438 self
439 }
440
441 pub fn set_disable_default_builtins(&mut self, disable_default_builtins: bool) {
443 self.disable_default_builtins = disable_default_builtins;
444 }
445
446 pub fn builtin_command<C>(mut self, command: C) -> Self
448 where
449 C: VirtualCommand + Send + Sync + 'static,
450 {
451 self.add_builtin_command(command);
452 self
453 }
454
455 pub fn add_builtin_command<C>(&mut self, command: C)
457 where
458 C: VirtualCommand + Send + Sync + 'static,
459 {
460 let path = format!("/bin/{}", command.name());
461 self.add_builtin_command_with_path(command, path);
462 }
463
464 pub fn builtin_command_with_path<C, P>(mut self, command: C, path: P) -> Self
466 where
467 C: VirtualCommand + Send + Sync + 'static,
468 P: Into<String>,
469 {
470 self.add_builtin_command_with_path(command, path);
471 self
472 }
473
474 pub fn add_builtin_command_with_path<C, P>(&mut self, command: C, path: P)
476 where
477 C: VirtualCommand + Send + Sync + 'static,
478 P: Into<String>,
479 {
480 self.add_builtin_command_with_path_shared(Arc::new(command), path);
481 }
482
483 fn add_builtin_command_with_path_shared<P>(
485 &mut self,
486 command: Arc<dyn VirtualCommand + Send + Sync + 'static>,
487 path: P,
488 ) where
489 P: Into<String>,
490 {
491 self.builtin_commands.push((path.into(), command));
492 }
493
494 pub fn map_command<Name, Target>(mut self, name: Name, target: Target) -> Self
496 where
497 Name: AsRef<str>,
498 Target: AsRef<str>,
499 {
500 self.add_mapped_command(name, target);
501 self
502 }
503
504 pub fn add_mapped_command<Name, Target>(&mut self, name: Name, target: Target)
506 where
507 Name: AsRef<str>,
508 Target: AsRef<str>,
509 {
510 let path_buf = PathBuf::from(target.as_ref().to_string());
511 self.map_commands
512 .insert(name.as_ref().to_string(), path_buf);
513 }
514
515 pub fn map_commands<I, Name, Target>(mut self, map_commands: I) -> Self
517 where
518 I: IntoIterator<Item = (Name, Target)>,
519 Name: AsRef<str>,
520 Target: AsRef<str>,
521 {
522 self.add_mapped_commands(map_commands);
523 self
524 }
525
526 pub fn add_mapped_commands<I, Name, Target>(&mut self, map_commands: I)
528 where
529 I: IntoIterator<Item = (Name, Target)>,
530 Name: AsRef<str>,
531 Target: AsRef<str>,
532 {
533 for (alias, target) in map_commands {
534 self.add_mapped_command(alias, target);
535 }
536 }
537
538 pub fn preopen_dir<P>(mut self, po_dir: P) -> Result<Self, WasiStateCreationError>
543 where
544 P: AsRef<Path>,
545 {
546 self.add_preopen_dir(po_dir)?;
547 Ok(self)
548 }
549
550 pub fn add_preopen_dir<P>(&mut self, po_dir: P) -> Result<(), WasiStateCreationError>
555 where
556 P: AsRef<Path>,
557 {
558 let mut pdb = PreopenDirBuilder::new();
559 let path = po_dir.as_ref();
560 pdb.directory(path).read(true).write(true).create(true);
561 let preopen = pdb.build()?;
562
563 self.preopens.push(preopen);
564
565 Ok(())
566 }
567
568 pub fn preopen_dirs<I, P>(mut self, dirs: I) -> Result<Self, WasiStateCreationError>
573 where
574 I: IntoIterator<Item = P>,
575 P: AsRef<Path>,
576 {
577 for po_dir in dirs {
578 self.add_preopen_dir(po_dir)?;
579 }
580
581 Ok(self)
582 }
583
584 pub fn preopen_build<F>(mut self, inner: F) -> Result<Self, WasiStateCreationError>
599 where
600 F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
601 {
602 self.add_preopen_build(inner)?;
603 Ok(self)
604 }
605
606 pub fn add_preopen_build<F>(&mut self, inner: F) -> Result<(), WasiStateCreationError>
621 where
622 F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
623 {
624 let mut pdb = PreopenDirBuilder::new();
625 let po_dir = inner(&mut pdb).build()?;
626
627 self.preopens.push(po_dir);
628
629 Ok(())
630 }
631
632 pub fn preopen_vfs_dirs<I>(&mut self, po_dirs: I) -> Result<&mut Self, WasiStateCreationError>
635 where
636 I: IntoIterator<Item = String>,
637 {
638 for po_dir in po_dirs {
639 self.vfs_preopens.push(po_dir);
640 }
641
642 Ok(self)
643 }
644
645 pub fn map_dir<P>(mut self, alias: &str, po_dir: P) -> Result<Self, WasiStateCreationError>
647 where
648 P: AsRef<Path>,
649 {
650 self.add_map_dir(alias, po_dir)?;
651 Ok(self)
652 }
653
654 pub fn add_map_dir<P>(&mut self, alias: &str, po_dir: P) -> Result<(), WasiStateCreationError>
656 where
657 P: AsRef<Path>,
658 {
659 let mut pdb = PreopenDirBuilder::new();
660 let path = po_dir.as_ref();
661 pdb.directory(path)
662 .alias(alias)
663 .read(true)
664 .write(true)
665 .create(true);
666 let preopen = pdb.build()?;
667
668 self.preopens.push(preopen);
669
670 Ok(())
671 }
672
673 pub fn map_dirs<I, P>(mut self, mapped_dirs: I) -> Result<Self, WasiStateCreationError>
675 where
676 I: IntoIterator<Item = (String, P)>,
677 P: AsRef<Path>,
678 {
679 for (alias, dir) in mapped_dirs {
680 self.add_map_dir(&alias, dir)?;
681 }
682
683 Ok(self)
684 }
685
686 #[cfg(feature = "journal")]
692 pub fn add_read_only_journal(&mut self, journal: Arc<DynReadableJournal>) {
693 self.read_only_journals.push(journal);
694 }
695
696 #[cfg(feature = "journal")]
706 pub fn add_writable_journal(&mut self, journal: Arc<DynJournal>) {
707 self.writable_journals.push(journal);
708 }
709
710 pub fn get_current_dir(&mut self) -> Option<PathBuf> {
711 self.current_dir.clone()
712 }
713
714 pub fn set_current_dir(&mut self, dir: impl Into<PathBuf>) {
715 self.current_dir = Some(dir.into());
716 }
717
718 pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
719 self.set_current_dir(dir);
720 self
721 }
722
723 pub fn stdout(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
726 self.stdout = Some(new_file);
727
728 self
729 }
730
731 pub fn set_stdout(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
734 self.stdout = Some(new_file);
735 }
736
737 pub fn stderr(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
740 self.set_stderr(new_file);
741 self
742 }
743
744 pub fn set_stderr(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
747 self.stderr = Some(new_file);
748 }
749
750 pub fn stdin(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
753 self.stdin = Some(new_file);
754
755 self
756 }
757
758 pub fn set_stdin(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
761 self.stdin = Some(new_file);
762 }
763
764 pub fn fs(mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) -> Self {
768 self.set_fs(fs);
769 self
770 }
771
772 pub fn set_fs(&mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) {
773 self.fs = Some(WasiFsRoot::from_filesystem(fs.into()));
774 }
775
776 pub fn mount_fs(mut self, fs: MountFileSystem) -> Self {
777 self.set_mount_fs(fs);
778 self
779 }
780
781 pub fn set_mount_fs(&mut self, fs: MountFileSystem) {
782 self.fs = Some(WasiFsRoot::from_mount_fs(fs));
783 }
784
785 pub(crate) fn set_fs_root(&mut self, fs: WasiFsRoot) {
786 self.fs = Some(fs);
787 }
788
789 pub fn sandbox_fs(mut self, fs: MountFileSystem) -> Self {
791 self.fs = Some(WasiFsRoot::from_mount_fs(fs));
792 self
793 }
794
795 pub fn setup_fs(mut self, setup_fs_fn: SetupFsFn) -> Self {
798 self.setup_fs_fn = Some(setup_fs_fn);
799
800 self
801 }
802
803 pub fn engine(mut self, engine: Engine) -> Self {
806 self.set_engine(engine);
807 self
808 }
809
810 pub fn set_engine(&mut self, engine: Engine) {
811 self.engine = Some(engine);
812 }
813
814 pub fn runtime(mut self, runtime: Arc<dyn Runtime + Send + Sync>) -> Self {
817 self.set_runtime(runtime);
818 self
819 }
820
821 pub fn set_runtime(&mut self, runtime: Arc<dyn Runtime + Send + Sync>) {
822 self.runtime = Some(runtime);
823 }
824
825 pub fn capabilities(mut self, capabilities: Capabilities) -> Self {
826 self.set_capabilities(capabilities);
827 self
828 }
829
830 pub fn capabilities_mut(&mut self) -> &mut Capabilities {
831 &mut self.capabilites
832 }
833
834 pub fn set_capabilities(&mut self, capabilities: Capabilities) {
835 self.capabilites = capabilities;
836 }
837
838 #[cfg(feature = "journal")]
839 pub fn add_snapshot_trigger(&mut self, on: SnapshotTrigger) {
840 self.snapshot_on.push(on);
841 }
842
843 #[cfg(feature = "journal")]
844 pub fn with_snapshot_interval(&mut self, interval: std::time::Duration) {
845 self.snapshot_interval.replace(interval);
846 }
847
848 #[cfg(feature = "journal")]
849 pub fn with_stop_running_after_snapshot(&mut self, stop_running: bool) {
850 self.stop_running_after_snapshot = stop_running;
851 }
852
853 pub fn with_skip_stdio_during_bootstrap(&mut self, skip: bool) {
854 self.skip_stdio_during_bootstrap = skip;
855 }
856
857 pub fn build_init(mut self) -> Result<WasiEnvInit, WasiStateCreationError> {
866 for arg in self.args.iter() {
867 for b in arg.as_bytes().iter() {
868 if *b == 0 {
869 return Err(WasiStateCreationError::ArgumentContainsNulByte(arg.clone()));
870 }
871 }
872 }
873
874 enum InvalidCharacter {
875 Nul,
876 Equal,
877 }
878
879 for (env_key, env_value) in self.envs.iter() {
880 match env_key.as_bytes().iter().find_map(|&ch| {
881 if ch == 0 {
882 Some(InvalidCharacter::Nul)
883 } else if ch == b'=' {
884 Some(InvalidCharacter::Equal)
885 } else {
886 None
887 }
888 }) {
889 Some(InvalidCharacter::Nul) => {
890 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
891 format!("found nul byte in env var key \"{env_key}\" (key=value)"),
892 ));
893 }
894
895 Some(InvalidCharacter::Equal) => {
896 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
897 format!("found equal sign in env var key \"{env_key}\" (key=value)"),
898 ));
899 }
900
901 None => (),
902 }
903
904 if env_value.contains(&0) {
905 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
906 format!(
907 "found nul byte in env var value \"{}\" (key=value)",
908 String::from_utf8_lossy(env_value),
909 ),
910 ));
911 }
912 }
913
914 let stdin: Box<dyn VirtualFile + Send + Sync + 'static> = self
916 .stdin
917 .take()
918 .unwrap_or_else(|| Box::new(ArcFile::new(Box::<super::Stdin>::default())));
919
920 let fs_backing = self.fs.take().unwrap_or_else(|| {
921 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()))
922 });
923
924 if let Some(dir) = &self.current_dir {
925 match fs_backing.read_dir(dir) {
926 Ok(_) => {
927 }
929 Err(FsError::EntryNotFound) => {
930 fs_backing.create_dir(dir).map_err(|err| {
931 WasiStateCreationError::WasiFsSetupError(format!(
932 "Could not create specified current directory at '{}': {err}",
933 dir.display()
934 ))
935 })?;
936 }
937 Err(err) => {
938 return Err(WasiStateCreationError::WasiFsSetupError(format!(
939 "Could not check specified current directory at '{}': {err}",
940 dir.display()
941 )));
942 }
943 }
944 }
945
946 let resolved_preopens = self.preopens.clone();
947
948 let inodes = crate::state::WasiInodes::new();
950 let wasi_fs = {
951 let mut wasi_fs = WasiFs::new_with_preopen(
953 &inodes,
954 &resolved_preopens,
955 &self.vfs_preopens,
956 fs_backing,
957 )
958 .map_err(WasiStateCreationError::WasiFsCreationError)?;
959
960 wasi_fs
962 .swap_file(__WASI_STDIN_FILENO, stdin)
963 .map_err(WasiStateCreationError::FileSystemError)?;
964
965 if let Some(stdout_override) = self.stdout.take() {
966 wasi_fs
967 .swap_file(__WASI_STDOUT_FILENO, stdout_override)
968 .map_err(WasiStateCreationError::FileSystemError)?;
969 }
970
971 if let Some(stderr_override) = self.stderr.take() {
972 wasi_fs
973 .swap_file(__WASI_STDERR_FILENO, stderr_override)
974 .map_err(WasiStateCreationError::FileSystemError)?;
975 }
976
977 if let Some(f) = &self.setup_fs_fn {
978 f(&inodes, &mut wasi_fs).map_err(WasiStateCreationError::WasiFsSetupError)?;
979 }
980 wasi_fs
981 };
982
983 if let Some(dir) = &self.current_dir {
984 let s = dir.to_str().ok_or_else(|| {
985 WasiStateCreationError::WasiFsSetupError(format!(
986 "Specified current directory is not valid UTF-8: '{}'",
987 dir.display()
988 ))
989 })?;
990 wasi_fs.set_current_dir(s);
991 }
992
993 for id in &self.included_packages {
994 wasi_fs.has_unioned.lock().unwrap().insert(id.clone());
995 }
996
997 let state = WasiState {
998 fs: wasi_fs,
999 secret: rand::rng().random::<[u8; 32]>(),
1000 inodes,
1001 args: std::sync::Mutex::new(self.args.clone()),
1002 preopen: self.vfs_preopens.clone(),
1003 futexs: Default::default(),
1004 clock_offset: Default::default(),
1005 envs: std::sync::Mutex::new(conv_env_vars(self.envs)),
1006 signals: std::sync::Mutex::new(self.signals.iter().map(|s| (s.sig, s.disp)).collect()),
1007 };
1008
1009 let runtime = self.runtime.unwrap_or_else(|| {
1010 #[cfg(feature = "sys-thread")]
1011 {
1012 #[allow(unused_mut)]
1013 let mut runtime = crate::runtime::PluggableRuntime::new(Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::default()));
1014 runtime.set_engine(
1015 self
1016 .engine
1017 .as_ref()
1018 .expect(
1019 "Neither a runtime nor an engine was provided to WasiEnvBuilder. \
1020 This is not supported because it means the module that's going to \
1021 run with the resulting WasiEnv will have been loaded using a \
1022 different engine than the one that will exist within the WasiEnv. \
1023 Use either `set_runtime` or `set_engine` before calling `build_init`.",
1024 )
1025 .clone()
1026 );
1027 #[cfg(feature = "journal")]
1028 for journal in self.read_only_journals.clone() {
1029 runtime.add_read_only_journal(journal);
1030 }
1031 #[cfg(feature = "journal")]
1032 for journal in self.writable_journals.clone() {
1033 runtime.add_writable_journal(journal);
1034 }
1035 Arc::new(runtime)
1036 }
1037
1038 #[cfg(not(feature = "sys-thread"))]
1039 {
1040 panic!("this build does not support a default runtime - specify one with WasiEnvBuilder::runtime()");
1041 }
1042 });
1043
1044 let uses = self.uses;
1045 let map_commands = self.map_commands;
1046 let disable_default_builtins = self.disable_default_builtins;
1047 let builtin_commands = self.builtin_commands;
1048
1049 let mut bin_factory = BinFactory::new(runtime.clone());
1050 if disable_default_builtins {
1051 bin_factory.clear_builtin_commands();
1052 }
1053 for (path, command) in builtin_commands {
1054 bin_factory.register_builtin_command_with_path_shared(command, path);
1055 }
1056
1057 let capabilities = self.capabilites;
1058
1059 let plane_config = ControlPlaneConfig {
1060 max_task_count: capabilities.threading.max_threads,
1061 enable_asynchronous_threading: capabilities.threading.enable_asynchronous_threading,
1062 enable_exponential_cpu_backoff: capabilities.threading.enable_exponential_cpu_backoff,
1063 };
1064 let control_plane = WasiControlPlane::new(plane_config);
1065
1066 let init = WasiEnvInit {
1067 state,
1068 runtime,
1069 webc_dependencies: uses,
1070 mapped_commands: map_commands,
1071 control_plane,
1072 bin_factory,
1073 capabilities,
1074 memory_ty: None,
1075 process: None,
1076 thread: None,
1077 #[cfg(feature = "journal")]
1078 call_initialize: self.read_only_journals.is_empty()
1079 && self.writable_journals.is_empty(),
1080 #[cfg(not(feature = "journal"))]
1081 call_initialize: true,
1082 can_deep_sleep: false,
1083 extra_tracing: true,
1084 #[cfg(feature = "journal")]
1085 snapshot_on: self.snapshot_on,
1086 #[cfg(feature = "journal")]
1087 stop_running_after_snapshot: self.stop_running_after_snapshot,
1088 skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
1089 };
1090
1091 Ok(init)
1092 }
1093
1094 #[allow(clippy::result_large_err)]
1095 pub fn build(self) -> Result<WasiEnv, WasiRuntimeError> {
1096 let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1097 let init = self.build_init()?;
1098 WasiEnv::from_init(init, module_hash)
1099 }
1100
1101 #[doc(hidden)]
1106 #[allow(clippy::result_large_err)]
1107 pub fn finalize(
1108 self,
1109 store: &mut impl AsStoreMut,
1110 ) -> Result<WasiFunctionEnv, WasiRuntimeError> {
1111 let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1112 let init = self.build_init()?;
1113 let env = WasiEnv::from_init(init, module_hash)?;
1114 let func_env = WasiFunctionEnv::new(store, env);
1115 Ok(func_env)
1116 }
1117
1118 #[allow(clippy::result_large_err)]
1124 pub fn instantiate(
1125 self,
1126 module: Module,
1127 store: &mut impl AsStoreMut,
1128 ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1129 self.instantiate_ext(module, ModuleHash::random(), store)
1130 }
1131
1132 #[allow(clippy::result_large_err)]
1133 pub fn instantiate_ext(
1134 self,
1135 module: Module,
1136 module_hash: ModuleHash,
1137 store: &mut impl AsStoreMut,
1138 ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1139 let init = self.build_init()?;
1140 let call_init = init.call_initialize;
1141 let env = WasiEnv::from_init(init, module_hash)?;
1142 let memory = module
1143 .imports()
1144 .find_map(|i| match i.ty() {
1145 wasmer::ExternType::Memory(ty) => Some(*ty),
1146 _ => None,
1147 })
1148 .map(|ty| wasmer::Memory::new(store, ty))
1149 .transpose()
1150 .map_err(WasiThreadError::MemoryCreateFailed)?;
1151 Ok(env.instantiate(module, store, memory, true, call_init, None)?)
1152 }
1153}
1154
1155pub(crate) fn conv_env_vars(envs: Vec<(String, Vec<u8>)>) -> Vec<Vec<u8>> {
1156 envs.into_iter()
1157 .map(|(key, value)| {
1158 let mut env = Vec::with_capacity(key.len() + value.len() + 1);
1159 env.extend_from_slice(key.as_bytes());
1160 env.push(b'=');
1161 env.extend_from_slice(&value);
1162
1163 env
1164 })
1165 .collect()
1166}
1167
1168#[derive(Debug, Default)]
1170pub struct PreopenDirBuilder {
1171 path: Option<PathBuf>,
1172 alias: Option<String>,
1173 read: bool,
1174 write: bool,
1175 create: bool,
1176}
1177
1178#[derive(Debug, Clone, Default)]
1180pub(crate) struct PreopenedDir {
1181 pub(crate) path: PathBuf,
1182 pub(crate) alias: Option<String>,
1183 pub(crate) read: bool,
1184 pub(crate) write: bool,
1185 pub(crate) create: bool,
1186}
1187
1188impl PreopenDirBuilder {
1189 pub(crate) fn new() -> Self {
1191 PreopenDirBuilder::default()
1192 }
1193
1194 pub fn directory<FilePath>(&mut self, po_dir: FilePath) -> &mut Self
1196 where
1197 FilePath: AsRef<Path>,
1198 {
1199 let path = po_dir.as_ref();
1200 self.path = Some(path.to_path_buf());
1201
1202 self
1203 }
1204
1205 pub fn alias(&mut self, alias: &str) -> &mut Self {
1207 let alias = alias.trim_start_matches('/');
1210 self.alias = Some(alias.to_string());
1211
1212 self
1213 }
1214
1215 pub fn read(&mut self, toggle: bool) -> &mut Self {
1217 self.read = toggle;
1218
1219 self
1220 }
1221
1222 pub fn write(&mut self, toggle: bool) -> &mut Self {
1224 self.write = toggle;
1225
1226 self
1227 }
1228
1229 pub fn create(&mut self, toggle: bool) -> &mut Self {
1233 self.create = toggle;
1234 if toggle {
1235 self.write = true;
1236 }
1237
1238 self
1239 }
1240
1241 pub(crate) fn build(&self) -> Result<PreopenedDir, WasiStateCreationError> {
1242 if !(self.read || self.write || self.create) {
1244 return Err(WasiStateCreationError::PreopenedDirectoryError("Preopened directories must have at least one of read, write, create permissions set".to_string()));
1245 }
1246
1247 if self.path.is_none() {
1248 return Err(WasiStateCreationError::PreopenedDirectoryError(
1249 "Preopened directories must point to a host directory".to_string(),
1250 ));
1251 }
1252 let path = self.path.clone().unwrap();
1253
1254 if let Some(alias) = &self.alias {
1261 validate_mapped_dir_alias(alias)?;
1262 }
1263
1264 Ok(PreopenedDir {
1265 path,
1266 alias: self.alias.clone(),
1267 read: self.read,
1268 write: self.write,
1269 create: self.create,
1270 })
1271 }
1272}
1273
1274#[cfg(test)]
1275mod test {
1276 use super::*;
1277 use crate::{
1278 SpawnError,
1279 os::{
1280 command::{BuiltinCommand, VirtualCommand},
1281 task::{OwnedTaskStatus, TaskJoinHandle},
1282 },
1283 };
1284 use wasmer::FunctionEnvMut;
1285 use wasmer_wasix_types::wasi::Errno;
1286
1287 fn enter_tokio_runtime() -> Option<tokio::runtime::Runtime> {
1288 #[cfg(not(target_arch = "wasm32"))]
1289 {
1290 let runtime = tokio::runtime::Builder::new_multi_thread()
1291 .enable_all()
1292 .build()
1293 .unwrap();
1294 Some(runtime)
1295 }
1296
1297 #[cfg(target_arch = "wasm32")]
1298 {
1299 None
1300 }
1301 }
1302
1303 #[derive(Debug)]
1304 struct TestBuiltinCommand {
1305 name: &'static str,
1306 }
1307
1308 impl TestBuiltinCommand {
1309 fn new(name: &'static str) -> Self {
1310 Self { name }
1311 }
1312 }
1313
1314 impl VirtualCommand for TestBuiltinCommand {
1315 fn name(&self) -> &str {
1316 self.name
1317 }
1318
1319 fn as_any(&self) -> &dyn std::any::Any {
1320 self
1321 }
1322
1323 fn exec(
1324 &self,
1325 _parent_ctx: &FunctionEnvMut<'_, WasiEnv>,
1326 _path: &str,
1327 _config: &mut Option<WasiEnv>,
1328 ) -> Result<TaskJoinHandle, SpawnError> {
1329 let handle = OwnedTaskStatus::new_finished_with_code(Errno::Success.into()).handle();
1330 Ok(handle)
1331 }
1332 }
1333
1334 #[test]
1335 fn env_var_errors() {
1336 #[cfg(not(target_arch = "wasm32"))]
1337 let runtime = tokio::runtime::Builder::new_multi_thread()
1338 .enable_all()
1339 .build()
1340 .unwrap();
1341 #[cfg(not(target_arch = "wasm32"))]
1342 let handle = runtime.handle().clone();
1343 #[cfg(not(target_arch = "wasm32"))]
1344 let _guard = handle.enter();
1345
1346 assert!(
1348 WasiEnv::builder("test_prog")
1349 .env("HOM=E", "/home/home")
1350 .build_init()
1351 .is_err(),
1352 "equal sign in key must be invalid"
1353 );
1354
1355 assert!(
1357 WasiEnvBuilder::new("test_prog")
1358 .env("HOME\0", "/home/home")
1359 .build_init()
1360 .is_err(),
1361 "nul in key must be invalid"
1362 );
1363
1364 assert!(
1366 WasiEnvBuilder::new("test_prog")
1367 .env("HOME", "/home/home\0")
1368 .build_init()
1369 .is_err(),
1370 "nul in value must be invalid"
1371 );
1372
1373 assert!(
1375 WasiEnvBuilder::new("test_prog")
1376 .env("HOME", "/home/home=home")
1377 .engine(Engine::default())
1378 .build_init()
1379 .is_ok(),
1380 "equal sign in the value must be valid"
1381 );
1382 }
1383
1384 #[test]
1385 fn nul_character_in_args() {
1386 let output = WasiEnvBuilder::new("test_prog")
1387 .arg("--h\0elp")
1388 .build_init();
1389 let err = output.expect_err("should fail");
1390 assert!(matches!(
1391 err,
1392 WasiStateCreationError::ArgumentContainsNulByte(_)
1393 ));
1394
1395 let output = WasiEnvBuilder::new("test_prog")
1396 .args(["--help", "--wat\0"])
1397 .build_init();
1398 let err = output.expect_err("should fail");
1399 assert!(matches!(
1400 err,
1401 WasiStateCreationError::ArgumentContainsNulByte(_)
1402 ));
1403 }
1404
1405 #[test]
1406 fn custom_builtin_command_uses_default_bin_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(TestBuiltinCommand::new("custom"))
1413 .build_init()
1414 .unwrap();
1415
1416 assert!(init.bin_factory.commands.exists("/bin/custom"));
1417 }
1418
1419 #[test]
1420 fn custom_builtin_command_supports_custom_path() {
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 .builtin_command_with_path(TestBuiltinCommand::new("custom"), "/custom/bin/custom")
1427 .build_init()
1428 .unwrap();
1429
1430 assert!(init.bin_factory.commands.exists("/custom/bin/custom"));
1431 }
1432
1433 #[test]
1434 fn can_disable_default_builtin_commands() {
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 .disable_default_builtins(true)
1441 .build_init()
1442 .unwrap();
1443
1444 assert!(!init.bin_factory.commands.exists("/bin/wasmer"));
1445 }
1446
1447 #[test]
1448 fn builtin_command_registration_overwrites_existing_path() {
1449 let runtime = enter_tokio_runtime();
1450 let _guard = runtime.as_ref().map(|rt| rt.enter());
1451
1452 let init = WasiEnvBuilder::new("test_prog")
1453 .engine(Engine::default())
1454 .builtin_command_with_path(TestBuiltinCommand::new("first"), "/bin/custom")
1455 .builtin_command_with_path(TestBuiltinCommand::new("second"), "/bin/custom")
1456 .build_init()
1457 .unwrap();
1458
1459 let command = init.bin_factory.commands.get("/bin/custom").unwrap();
1460 assert_eq!(command.name(), "second");
1461 }
1462
1463 #[test]
1464 fn closure_based_builtin_command_can_be_registered() {
1465 let runtime = enter_tokio_runtime();
1466 let _guard = runtime.as_ref().map(|rt| rt.enter());
1467
1468 let command = BuiltinCommand::new("closure", |_parent_ctx, _path, _config| {
1469 let handle = OwnedTaskStatus::new_finished_with_code(Errno::Success.into()).handle();
1470 Ok(handle)
1471 });
1472
1473 let init = WasiEnvBuilder::new("test_prog")
1474 .engine(Engine::default())
1475 .builtin_command(command)
1476 .build_init()
1477 .unwrap();
1478
1479 assert!(init.bin_factory.commands.exists("/bin/closure"));
1480 }
1481}