1#![allow(clippy::result_large_err)]
2use std::{
3 collections::HashMap,
4 future::Future,
5 ops::Deref,
6 path::Path,
7 pin::Pin,
8 sync::{Arc, RwLock},
9};
10
11use anyhow::Context;
12use shared_buffer::OwnedBuffer;
13use virtual_fs::{AsyncReadExt, FileSystem};
14use wasmer::FunctionEnvMut;
15use wasmer_package::utils::from_bytes;
16
17mod binary_package;
18mod exec;
19
20pub use self::{
21 binary_package::*,
22 exec::{
23 import_package_mounts, package_command_by_name, run_exec, spawn_exec, spawn_exec_module,
24 spawn_exec_wasm, spawn_load_module,
25 },
26};
27use crate::{
28 Runtime, SpawnError, VIRTUAL_ROOT_FD, WasiEnv,
29 fs::Kind,
30 os::{
31 command::{Commands, VirtualCommand},
32 task::TaskJoinHandle,
33 },
34 runtime::module_cache::HashedModuleData,
35};
36
37#[derive(Debug, Clone)]
38pub struct BinFactory {
39 pub(crate) commands: Commands,
40 runtime: Arc<dyn Runtime + Send + Sync + 'static>,
41 pub(crate) local: Arc<RwLock<HashMap<String, Option<Arc<BinaryPackage>>>>>,
42}
43
44impl BinFactory {
45 pub fn new(runtime: Arc<dyn Runtime + Send + Sync + 'static>) -> BinFactory {
46 BinFactory {
47 commands: Commands::new_with_builtins(runtime.clone()),
48 runtime,
49 local: Arc::new(RwLock::new(HashMap::new())),
50 }
51 }
52
53 pub fn runtime(&self) -> &(dyn Runtime + Send + Sync) {
54 self.runtime.deref()
55 }
56
57 pub fn register_builtin_command<C>(&mut self, cmd: C)
59 where
60 C: VirtualCommand + Send + Sync + 'static,
61 {
62 self.commands.register_command(cmd);
63 }
64
65 pub fn register_builtin_command_with_path<C, P>(&mut self, cmd: C, path: P)
67 where
68 C: VirtualCommand + Send + Sync + 'static,
69 P: Into<String>,
70 {
71 self.commands.register_command_with_path(cmd, path.into());
72 }
73
74 pub(crate) fn register_builtin_command_with_path_shared<P>(
76 &mut self,
77 cmd: Arc<dyn VirtualCommand + Send + Sync + 'static>,
78 path: P,
79 ) where
80 P: Into<String>,
81 {
82 self.commands
83 .register_command_with_path_shared(cmd, path.into());
84 }
85
86 pub fn clear_builtin_commands(&mut self) {
88 self.commands.clear();
89 }
90
91 pub fn set_binary(&self, name: &str, binary: &Arc<BinaryPackage>) {
92 let mut cache = self.local.write().unwrap();
93 cache.insert(name.to_string(), Some(binary.clone()));
94 }
95
96 #[allow(clippy::await_holding_lock)]
97 pub async fn get_binary(
98 &self,
99 name: &str,
100 fs: Option<&dyn FileSystem>,
101 ) -> Option<Arc<BinaryPackage>> {
102 self.get_executable(name, fs)
103 .await
104 .and_then(|executable| match executable {
105 Executable::Wasm(_) | Executable::Script(_) => None,
106 Executable::BinaryPackage(pkg) => Some(pkg),
107 })
108 }
109
110 pub fn spawn<'a>(
124 &'a self,
125 name: String,
126 invoked_as: Option<String>,
127 env: WasiEnv,
128 ) -> Pin<Box<dyn Future<Output = Result<TaskJoinHandle, SpawnError>> + 'a>> {
129 Box::pin(async move {
130 let mut name = name;
131 let mut invoked_as = invoked_as;
132 let mut rewritten_args: Option<Vec<String>> = None;
137
138 for _ in 0..MAX_SHEBANG_DEPTH {
144 let (resolved_name, executable) = self
145 .get_executable_for_spawn(name.as_str(), &env)
146 .await?
147 .ok_or_else(|| SpawnError::BinaryNotFound {
148 binary: name.clone(),
149 })?;
150 name = resolved_name;
151
152 match executable {
153 Executable::Wasm(bytes) => {
154 let data = HashedModuleData::new(bytes.clone());
155 commit_script_args(&env, rewritten_args);
156 return spawn_exec_wasm(data, name.as_str(), env, &self.runtime).await;
157 }
158 Executable::BinaryPackage(pkg) => {
159 {
160 let cmd = package_command_by_name(&pkg, name.as_str())?;
163 commit_script_args(&env, rewritten_args);
164 env.prepare_spawn(cmd);
165 }
166
167 return spawn_exec(pkg.as_ref().clone(), name.as_str(), env, &self.runtime)
168 .await;
169 }
170 Executable::Script(script) => {
171 let script_path = invoked_as.unwrap_or_else(|| name.clone());
172 let args = match &rewritten_args {
173 Some(args) => args.clone(),
174 None => env.state.args.lock().unwrap().clone(),
175 };
176 let (interpreter, args) = script_command(&script_path, script, &args);
177 rewritten_args = Some(args);
178 name = interpreter;
179 invoked_as = Some(name.clone());
182 }
183 }
184 }
185
186 Err(SpawnError::ShebangLoop)
187 })
188 }
189
190 pub fn try_built_in(
191 &self,
192 name: String,
193 parent_ctx: Option<&FunctionEnvMut<'_, WasiEnv>>,
194 builder: &mut Option<WasiEnv>,
195 ) -> Result<TaskJoinHandle, SpawnError> {
196 if let Some(parent_ctx) = parent_ctx {
198 if self.commands.exists(name.as_str()) {
199 return self.commands.exec(parent_ctx, name.as_str(), builder);
200 }
201 } else if self.commands.exists(name.as_str()) {
202 tracing::warn!("builtin command without a parent ctx - {}", name);
203 }
204 Err(SpawnError::BinaryNotFound { binary: name })
205 }
206
207 #[allow(clippy::await_holding_lock)]
210 pub async fn get_executable(
211 &self,
212 name: &str,
213 fs: Option<&dyn FileSystem>,
214 ) -> Option<Executable> {
215 let name = name.to_string();
216
217 {
219 let cache = self.local.read().unwrap();
220 if let Some(data) = cache.get(&name) {
221 return data.clone().map(Executable::BinaryPackage);
222 }
223 }
224
225 let mut cache = self.local.write().unwrap();
226
227 if let Some(data) = cache.get(&name) {
229 return data.clone().map(Executable::BinaryPackage);
230 }
231
232 if name.starts_with('/')
234 && let Some(fs) = fs
235 {
236 match load_executable_from_filesystem(fs, name.as_ref(), self.runtime()).await {
237 Ok(executable) => {
238 if let Executable::BinaryPackage(pkg) = &executable {
239 cache.insert(name, Some(pkg.clone()));
240 }
241
242 return Some(executable);
243 }
244 Err(e) => {
245 tracing::warn!(
246 path = name,
247 error = &*e,
248 "Unable to load the package from disk"
249 );
250 }
251 }
252 }
253
254 None
257 }
258
259 async fn get_executable_for_spawn(
260 &self,
261 name: &str,
262 env: &WasiEnv,
263 ) -> Result<Option<(String, Executable)>, SpawnError> {
264 if name.contains('/') {
265 let name = if name.starts_with('/') {
266 name.to_string()
267 } else {
268 env.state.fs.relative_path_to_absolute(name.to_string())
269 };
270 let executable = self.get_executable_from_wasi_fs(&name, env).await?;
271 return Ok(executable.map(|executable| (name, executable)));
272 }
273
274 for directory in executable_search_path(env) {
275 let path = if directory.is_empty() {
276 name.to_string()
277 } else {
278 format!("{}/{}", directory.trim_end_matches('/'), name)
279 };
280 let path = if path.starts_with('/') {
281 path
282 } else {
283 env.state.fs.relative_path_to_absolute(path)
284 };
285 if let Some(executable) = self.get_executable_from_wasi_fs(&path, env).await? {
286 return Ok(Some((path, executable)));
287 }
288 }
289
290 Ok(None)
291 }
292
293 async fn get_executable_from_wasi_fs(
294 &self,
295 path: &str,
296 env: &WasiEnv,
297 ) -> Result<Option<Executable>, SpawnError> {
298 if let Some(binary) = self.local.read().unwrap().get(path).cloned().flatten() {
299 return Ok(Some(Executable::BinaryPackage(binary)));
300 }
301
302 match load_executable_from_wasi_fs(env, Path::new(path), self.runtime()).await {
303 Ok(executable) => {
304 if let Executable::BinaryPackage(package) = &executable {
305 self.local
306 .write()
307 .unwrap()
308 .insert(path.to_string(), Some(package.clone()));
309 }
310 Ok(Some(executable))
311 }
312 Err(error) if error.downcast_ref::<MalformedShebang>().is_some() => {
315 Err(SpawnError::InvalidShebang {
316 path: path.to_string(),
317 })
318 }
319 Err(error) => {
320 tracing::debug!(path, error = &*error, "Unable to load executable");
321 Ok(None)
322 }
323 }
324 }
325}
326
327fn executable_search_path(env: &WasiEnv) -> Vec<String> {
328 env.state
329 .envs
330 .lock()
331 .unwrap()
332 .iter()
333 .rev()
334 .find_map(|entry| {
335 entry
336 .strip_prefix(b"PATH=")
337 .map(|path| String::from_utf8_lossy(path).into_owned())
338 })
339 .unwrap_or_else(|| "/usr/local/bin:/bin:/usr/bin".to_string())
340 .split(':')
341 .map(str::to_string)
342 .collect()
343}
344
345pub enum Executable {
346 Wasm(OwnedBuffer),
347 BinaryPackage(Arc<BinaryPackage>),
348 Script(Shebang),
349}
350
351const MAX_SHEBANG_DEPTH: usize = 6;
355
356const MAX_SHEBANG_LINE: usize = 256;
359
360#[derive(Debug)]
361pub struct Shebang {
362 interpreter: String,
363 argument: Option<String>,
364}
365
366#[derive(Debug, thiserror::Error)]
371#[error("the shebang line cannot be used")]
372struct MalformedShebang;
373
374enum ShebangLine {
375 Absent,
376 Malformed,
377 Parsed(Shebang),
378}
379
380fn parse_shebang(bytes: &[u8]) -> ShebangLine {
381 if !bytes.starts_with(b"#!") {
382 return ShebangLine::Absent;
383 }
384
385 let window = &bytes[..bytes.len().min(MAX_SHEBANG_LINE)];
386 let line = match window.iter().position(|byte| *byte == b'\n') {
387 Some(end) => &window[..end],
388 None if bytes.len() < MAX_SHEBANG_LINE => window,
391 None => return ShebangLine::Malformed,
392 };
393
394 let Ok(line) = std::str::from_utf8(&line[2..]) else {
397 return ShebangLine::Malformed;
398 };
399 let line = line.trim_end_matches('\r').trim();
400
401 let (interpreter, argument) = line
402 .split_once(char::is_whitespace)
403 .map(|(interpreter, argument)| (interpreter, Some(argument.trim().to_string())))
404 .unwrap_or((line, None));
405
406 if interpreter.is_empty() {
407 return ShebangLine::Malformed;
408 }
409
410 ShebangLine::Parsed(Shebang {
411 interpreter: interpreter.to_string(),
412 argument: argument.filter(|argument| !argument.is_empty()),
413 })
414}
415
416fn commit_script_args(env: &WasiEnv, args: Option<Vec<String>>) {
419 if let Some(args) = args {
420 *env.state.args.lock().unwrap() = args;
421 }
422}
423
424fn script_command(
425 script_name: &str,
426 script: Shebang,
427 original_args: &[String],
428) -> (String, Vec<String>) {
429 let user_args = original_args.iter().skip(1).cloned();
430
431 let args = std::iter::once(script.interpreter.clone())
436 .chain(script.argument)
437 .chain(std::iter::once(script_name.to_string()))
438 .chain(user_args)
439 .collect();
440
441 (script.interpreter, args)
442}
443
444async fn load_executable_from_filesystem(
445 fs: &dyn FileSystem,
446 path: &Path,
447 rt: &(dyn Runtime + Send + Sync),
448) -> Result<Executable, anyhow::Error> {
449 let mut f = fs
450 .new_open_options()
451 .read(true)
452 .open(path)
453 .context("Unable to open the file")?;
454
455 if let Some(buf) = f.as_owned_buffer() {
458 load_executable_from_buffer(buf, rt).await
459 } else {
460 let mut data = Vec::with_capacity(f.size() as usize);
461 f.read_to_end(&mut data).await.context("Read failed")?;
462 load_executable_from_buffer(OwnedBuffer::from_bytes(data), rt).await
463 }
464}
465
466async fn load_executable_from_wasi_fs(
467 env: &WasiEnv,
468 path: &Path,
469 rt: &(dyn Runtime + Send + Sync),
470) -> Result<Executable, anyhow::Error> {
471 let inode = match env.state.fs.get_inode_at_path(
477 &env.state.inodes,
478 VIRTUAL_ROOT_FD,
479 path.to_string_lossy().as_ref(),
480 true,
481 ) {
482 Ok(inode) => inode,
483 Err(error) => {
484 tracing::debug!(
485 %error,
486 path = path.to_string_lossy().as_ref(),
487 "Unable to resolve executable through the preopens",
488 );
489 return load_executable_from_filesystem(env.fs_root(), path, rt).await;
490 }
491 };
492
493 let (buffer, backing_path) = {
494 let kind = inode.read();
495 match &*kind {
496 Kind::File { handle, path, .. } => {
497 let buffer = handle
498 .as_ref()
499 .and_then(|handle| handle.read().unwrap().as_owned_buffer());
500 (buffer, path.clone())
501 }
502 _ => anyhow::bail!("Executable is not a regular file"),
503 }
504 };
505
506 if let Some(buffer) = buffer {
507 load_executable_from_buffer(buffer, rt).await
508 } else {
509 load_executable_from_filesystem(&env.state.fs.root_fs, &backing_path, rt).await
510 }
511}
512
513async fn load_executable_from_buffer(
514 buffer: OwnedBuffer,
515 rt: &(dyn Runtime + Send + Sync),
516) -> Result<Executable, anyhow::Error> {
517 if wasmer_package::utils::is_container(buffer.as_slice()) {
518 let bytes = buffer.clone().into_bytes();
519 if let Ok(container) = from_bytes(bytes) {
520 let package = BinaryPackage::from_webc(&container, rt)
521 .await
522 .context("Unable to load the package")?;
523 return Ok(Executable::BinaryPackage(Arc::new(package)));
524 }
525 }
526
527 match parse_shebang(buffer.as_slice()) {
528 ShebangLine::Parsed(script) => Ok(Executable::Script(script)),
529 ShebangLine::Malformed => Err(MalformedShebang.into()),
530 ShebangLine::Absent => Ok(Executable::Wasm(buffer)),
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use std::{path::Path, sync::Arc};
537
538 use virtual_fs::{AsyncWriteExt, FileSystem};
539 use wasmer::Engine;
540 use wasmer_wasix_types::wasi::Errno;
541
542 use super::{
543 Executable, MAX_SHEBANG_LINE, Shebang, ShebangLine, load_executable_from_wasi_fs,
544 parse_shebang, script_command,
545 };
546 use crate::{SpawnError, VIRTUAL_ROOT_FD, WasiEnvBuilder};
547
548 fn expect_parsed(line: ShebangLine) -> Shebang {
549 match line {
550 ShebangLine::Parsed(script) => script,
551 ShebangLine::Absent => panic!("expected a shebang"),
552 ShebangLine::Malformed => panic!("expected a usable shebang"),
553 }
554 }
555
556 #[test]
557 fn parses_env_shebang() {
558 let script = expect_parsed(parse_shebang(
559 b"#!/usr/bin/env node\nconsole.log('hello')\n",
560 ));
561 assert_eq!(script.interpreter, "/usr/bin/env");
562 assert_eq!(script.argument.as_deref(), Some("node"));
563 }
564
565 #[test]
566 fn parses_direct_shebang_with_crlf() {
567 let script = expect_parsed(parse_shebang(b"#!/bin/bash -e\r\necho hello\r\n"));
568 assert_eq!(script.interpreter, "/bin/bash");
569 assert_eq!(script.argument.as_deref(), Some("-e"));
570 }
571
572 #[test]
573 fn ignores_regular_files() {
574 assert!(matches!(
575 parse_shebang(b"console.log('hello')\n"),
576 ShebangLine::Absent
577 ));
578 }
579
580 #[test]
581 fn a_line_longer_than_the_kernel_buffer_is_rejected() {
582 let fits = format!("#!/{}\n", "y".repeat(MAX_SHEBANG_LINE - 4));
585 assert_eq!(fits.len(), MAX_SHEBANG_LINE);
586 assert!(matches!(
587 parse_shebang(fits.as_bytes()),
588 ShebangLine::Parsed(_)
589 ));
590
591 let one_too_long = format!("#!/{}\n", "y".repeat(MAX_SHEBANG_LINE - 3));
592 assert_eq!(one_too_long.len(), MAX_SHEBANG_LINE + 1);
593 assert!(matches!(
594 parse_shebang(one_too_long.as_bytes()),
595 ShebangLine::Malformed
596 ));
597 }
598
599 #[test]
600 fn a_short_line_needs_no_newline() {
601 let Shebang {
602 interpreter,
603 argument,
604 } = expect_parsed(parse_shebang(b"#!/bin/echo hi"));
605 assert_eq!(interpreter, "/bin/echo");
606 assert_eq!(argument.as_deref(), Some("hi"));
607 }
608
609 #[test]
610 fn a_non_utf8_interpreter_is_rejected_rather_than_compiled() {
611 assert!(matches!(
614 parse_shebang(b"#!/usr/bin/int\xff\xfebad\n"),
615 ShebangLine::Malformed
616 ));
617 }
618
619 #[test]
620 fn an_empty_interpreter_is_rejected() {
621 assert!(matches!(parse_shebang(b"#!\n"), ShebangLine::Malformed));
622 assert!(matches!(parse_shebang(b"#! \n"), ShebangLine::Malformed));
623 }
624
625 #[test]
626 fn script_keeps_the_path_the_caller_spelled() {
627 let script = expect_parsed(parse_shebang(b"#!/bin/interp\n"));
630 let original = vec!["./tool".to_string(), "arg".to_string()];
631 let (interpreter, args) = script_command("./tool", script, &original);
632
633 assert_eq!(interpreter, "/bin/interp");
634 assert_eq!(args, ["/bin/interp", "./tool", "arg"]);
635 }
636
637 #[test]
638 fn env_shebang_execs_env_itself() {
639 let script = expect_parsed(parse_shebang(b"#!/usr/bin/env node\n"));
640 let original = vec!["next".to_string(), "dev".to_string()];
641 let (interpreter, args) = script_command("/workspace/.bin/next", script, &original);
642
643 assert_eq!(interpreter, "/usr/bin/env");
644 assert_eq!(
645 args,
646 ["/usr/bin/env", "node", "/workspace/.bin/next", "dev"]
647 );
648 }
649
650 #[test]
651 fn env_split_string_stays_one_argument() {
652 let script = expect_parsed(parse_shebang(b"#!/usr/bin/env -S node --no-warnings\n"));
653 let original = vec!["tool".to_string(), "input.js".to_string()];
654 let (interpreter, args) = script_command("/workspace/tool", script, &original);
655
656 assert_eq!(interpreter, "/usr/bin/env");
657 assert_eq!(
658 args,
659 [
660 "/usr/bin/env",
661 "-S node --no-warnings",
662 "/workspace/tool",
663 "input.js"
664 ]
665 );
666 }
667
668 #[test]
669 fn direct_shebang_inserts_optional_argument_before_script() {
670 let script = expect_parsed(parse_shebang(b"#!/bin/bash -e\n"));
671 let original = vec!["script".to_string(), "hello".to_string()];
672 let (interpreter, args) = script_command("/workspace/script", script, &original);
673
674 assert_eq!(interpreter, "/bin/bash");
675 assert_eq!(args, ["/bin/bash", "-e", "/workspace/script", "hello"]);
676 }
677
678 #[tokio::test]
679 async fn loads_shebang_through_relative_symlink() {
680 let mut builder = WasiEnvBuilder::new("test").engine(Engine::default());
681 builder.preopen_vfs_dirs(["/".to_string()]).unwrap();
682 let env = builder.build().unwrap();
683 let fs = &env.state.fs.root_fs;
684 fs.create_dir(Path::new("/bin")).unwrap();
685 fs.create_dir(Path::new("/pkg")).unwrap();
686
687 let mut target = fs
688 .new_open_options()
689 .create(true)
690 .write(true)
691 .open(Path::new("/pkg/next"))
692 .unwrap();
693 target
694 .write_all(b"#!/usr/bin/env node\nconsole.log('hello')\n")
695 .await
696 .unwrap();
697 fs.create_symlink(Path::new("../pkg/next"), Path::new("/bin/next"))
698 .unwrap();
699
700 let executable = load_executable_from_wasi_fs(&env, Path::new("/bin/next"), env.runtime())
701 .await
702 .unwrap();
703 assert!(matches!(executable, Executable::Script(_)));
704 }
705
706 #[tokio::test]
707 async fn loads_an_executable_outside_every_preopen() {
708 let backing = virtual_fs::mem_fs::FileSystem::default();
712 backing.create_dir(Path::new("/app")).unwrap();
713 backing.create_dir(Path::new("/bin")).unwrap();
714
715 let mut builder = WasiEnvBuilder::new("test")
716 .engine(Engine::default())
717 .fs(Arc::new(backing) as Arc<dyn FileSystem + Send + Sync>);
718 builder.preopen_vfs_dirs(["/app".to_string()]).unwrap();
719 let env = builder.build().unwrap();
720 let fs = &env.state.fs.root_fs;
721
722 let mut target = fs
723 .new_open_options()
724 .create(true)
725 .write(true)
726 .open(Path::new("/bin/tool"))
727 .unwrap();
728 target.write_all(b"#!/bin/sh\necho hello\n").await.unwrap();
729
730 assert!(
731 env.state
732 .fs
733 .get_inode_at_path(&env.state.inodes, VIRTUAL_ROOT_FD, "/bin/tool", true)
734 .is_err(),
735 "the preopens should not cover this path, or the test proves nothing"
736 );
737
738 let executable = load_executable_from_wasi_fs(&env, Path::new("/bin/tool"), env.runtime())
739 .await
740 .unwrap();
741 assert!(matches!(executable, Executable::Script(_)));
742 }
743
744 #[tokio::test]
745 async fn a_failed_script_spawn_leaves_the_caller_argv_alone() {
746 let backing = virtual_fs::mem_fs::FileSystem::default();
750 backing.create_dir(Path::new("/bin")).unwrap();
751
752 let mut builder = WasiEnvBuilder::new("tool")
753 .args(["one", "two"])
754 .engine(Engine::default())
755 .fs(Arc::new(backing) as Arc<dyn FileSystem + Send + Sync>);
756 builder.preopen_vfs_dirs(["/".to_string()]).unwrap();
757 let env = builder.build().unwrap();
758
759 let mut script = env
760 .state
761 .fs
762 .root_fs
763 .new_open_options()
764 .create(true)
765 .write(true)
766 .open(Path::new("/bin/tool"))
767 .unwrap();
768 script
769 .write_all(b"#!/bin/definitely-not-here\n")
770 .await
771 .unwrap();
772
773 let before = env.state.args.lock().unwrap().clone();
774 let result = env
775 .bin_factory
776 .clone()
777 .spawn("/bin/tool".to_string(), None, env.clone())
778 .await;
779
780 assert!(result.is_err(), "the interpreter does not exist");
781 assert_eq!(*env.state.args.lock().unwrap(), before);
782 }
783
784 #[tokio::test]
785 async fn a_shebang_cycle_reports_a_loop() {
786 let backing = virtual_fs::mem_fs::FileSystem::default();
789 backing.create_dir(Path::new("/bin")).unwrap();
790
791 let mut builder = WasiEnvBuilder::new("tool")
792 .engine(Engine::default())
793 .fs(Arc::new(backing) as Arc<dyn FileSystem + Send + Sync>);
794 builder.preopen_vfs_dirs(["/".to_string()]).unwrap();
795 let env = builder.build().unwrap();
796
797 for (name, target) in [("a", "/bin/b"), ("b", "/bin/a")] {
798 let mut file = env
799 .state
800 .fs
801 .root_fs
802 .new_open_options()
803 .create(true)
804 .write(true)
805 .open(Path::new(&format!("/bin/{name}")))
806 .unwrap();
807 file.write_all(format!("#!{target}\n").as_bytes())
808 .await
809 .unwrap();
810 }
811
812 let result = env
813 .bin_factory
814 .clone()
815 .spawn("/bin/a".to_string(), None, env.clone())
816 .await;
817
818 assert!(matches!(result, Err(SpawnError::ShebangLoop)));
819 assert_eq!(
820 crate::syscalls::conv_spawn_err_to_errno(&SpawnError::ShebangLoop),
821 Errno::Loop
822 );
823 }
824
825 #[tokio::test]
826 async fn a_symlink_outside_every_preopen_is_not_followed() {
827 let backing = virtual_fs::mem_fs::FileSystem::default();
831 backing.create_dir(Path::new("/app")).unwrap();
832 backing.create_dir(Path::new("/bin")).unwrap();
833 backing.create_dir(Path::new("/pkg")).unwrap();
834
835 let mut builder = WasiEnvBuilder::new("test")
836 .engine(Engine::default())
837 .fs(Arc::new(backing) as Arc<dyn FileSystem + Send + Sync>);
838 builder.preopen_vfs_dirs(["/app".to_string()]).unwrap();
839 let env = builder.build().unwrap();
840 let fs = &env.state.fs.root_fs;
841
842 let mut target = fs
843 .new_open_options()
844 .create(true)
845 .write(true)
846 .open(Path::new("/pkg/tool"))
847 .unwrap();
848 target.write_all(b"#!/bin/sh\necho hello\n").await.unwrap();
849 fs.create_symlink(Path::new("../pkg/tool"), Path::new("/bin/tool"))
850 .unwrap();
851
852 assert!(
853 load_executable_from_wasi_fs(&env, Path::new("/bin/tool"), env.runtime())
854 .await
855 .is_err()
856 );
857
858 let executable = load_executable_from_wasi_fs(&env, Path::new("/pkg/tool"), env.runtime())
861 .await
862 .unwrap();
863 assert!(matches!(executable, Executable::Script(_)));
864 }
865}