1mod fd;
18mod fd_list;
19mod inode_guard;
20mod notification;
21mod path_posix;
22
23use std::{
24 borrow::Cow,
25 collections::{HashMap, HashSet},
26 ops::{Deref, DerefMut},
27 path::{Path, PathBuf},
28 pin::Pin,
29 sync::{
30 Arc, Mutex, RwLock, Weak,
31 atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering},
32 },
33 task::{Context, Poll},
34};
35
36use crate::{
37 net::socket::InodeSocketKind,
38 state::{Stderr, Stdin, Stdout},
39};
40use futures::{Future, future::BoxFuture};
41#[cfg(feature = "enable-serde")]
42use serde_derive::{Deserialize, Serialize};
43use tracing::{debug, trace, warn};
44use virtual_fs::{
45 ArcFileSystem, FileSystem, FsError, MountFileSystem, OpenOptions, OverlayFileSystem,
46 TmpFileSystem, VirtualFile, limiter::DynFsMemoryLimiter,
47};
48use wasmer_config::package::PackageId;
49use wasmer_wasix_types::{
50 types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO},
51 wasi::{
52 Errno, Fd as WasiFd, Fdflags, Fdflagsext, Fdstat, Filesize, Filestat, Filetype,
53 Preopentype, Prestat, PrestatEnum, Rights, Socktype,
54 },
55};
56
57pub(crate) use self::fd::VirtualFileLock;
58pub use self::fd::{Fd, FdInner, InodeVal, Kind, SymlinkKind};
59pub(crate) use self::fd_list::FdList;
60pub(crate) use self::inode_guard::{
61 InodeValFilePollGuard, InodeValFilePollGuardJoin, InodeValFilePollGuardMode,
62 InodeValFileReadGuard, InodeValFileWriteGuard, WasiStateFileGuard,
63};
64pub use self::notification::NotificationInner;
65pub(crate) use self::path_posix::{PosixPath, PosixPathBuf, PosixPathComponent};
66use crate::{ALL_RIGHTS, bin_factory::BinaryPackage, state::PreopenedDir};
67
68pub(crate) const MAX_FD: WasiFd = (64 * 1024) - 1;
72
73pub(crate) struct FlushPoller {
74 pub(crate) file: VirtualFileLock,
75}
76
77pub(crate) struct CloseFdOutcome {
80 pub skipped_preopen: bool,
81 pub removed: bool,
82 pub flush_target: Option<VirtualFileLock>,
83}
84
85impl CloseFdOutcome {
86 fn not_found() -> Self {
87 Self {
88 skipped_preopen: false,
89 removed: false,
90 flush_target: None,
91 }
92 }
93}
94
95impl Future for FlushPoller {
96 type Output = Result<(), Errno>;
97
98 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
99 let mut file = self.file.write().unwrap();
100 Pin::new(file.as_mut())
101 .poll_flush(cx)
102 .map_err(|_| Errno::Io)
103 }
104}
105
106pub const VIRTUAL_ROOT_FD: WasiFd = 3;
120
121pub const FS_STDIN_INO: Inode = Inode(10);
124pub const FS_STDOUT_INO: Inode = Inode(11);
125pub const FS_STDERR_INO: Inode = Inode(12);
126pub const FS_ROOT_INO: Inode = Inode(13);
127
128const STDIN_DEFAULT_RIGHTS: Rights = {
129 Rights::from_bits_truncate(
132 Rights::FD_DATASYNC.bits()
133 | Rights::FD_READ.bits()
134 | Rights::FD_SYNC.bits()
135 | Rights::FD_ADVISE.bits()
136 | Rights::FD_FILESTAT_GET.bits()
137 | Rights::FD_FDSTAT_SET_FLAGS.bits()
138 | Rights::POLL_FD_READWRITE.bits(),
139 )
140};
141const STDOUT_DEFAULT_RIGHTS: Rights = {
142 Rights::from_bits_truncate(
145 Rights::FD_DATASYNC.bits()
146 | Rights::FD_SYNC.bits()
147 | Rights::FD_WRITE.bits()
148 | Rights::FD_ADVISE.bits()
149 | Rights::FD_FILESTAT_GET.bits()
150 | Rights::FD_FDSTAT_SET_FLAGS.bits()
151 | Rights::POLL_FD_READWRITE.bits(),
152 )
153};
154const STDERR_DEFAULT_RIGHTS: Rights = STDOUT_DEFAULT_RIGHTS;
155
156pub const MAX_SYMLINKS: u32 = 128;
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
161#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
162pub struct Inode(u64);
163
164impl Inode {
165 pub fn as_u64(&self) -> u64 {
166 self.0
167 }
168
169 pub fn from_path(str: &str) -> Self {
170 Inode(xxhash_rust::xxh64::xxh64(str.as_bytes(), 0))
171 }
172}
173
174#[derive(Debug, Clone)]
175pub struct InodeGuard {
176 ino: Inode,
177 inner: Arc<InodeVal>,
178
179 open_handles: Arc<AtomicI32>,
184}
185impl InodeGuard {
186 pub fn ino(&self) -> Inode {
187 self.ino
188 }
189
190 pub fn downgrade(&self) -> InodeWeakGuard {
191 InodeWeakGuard {
192 ino: self.ino,
193 open_handles: self.open_handles.clone(),
194 inner: Arc::downgrade(&self.inner),
195 }
196 }
197
198 pub fn ref_cnt(&self) -> usize {
199 Arc::strong_count(&self.inner)
200 }
201
202 pub fn handle_count(&self) -> u32 {
203 self.open_handles.load(Ordering::SeqCst) as u32
204 }
205
206 pub fn acquire_handle(&self) {
207 let prev_handles = self.open_handles.fetch_add(1, Ordering::SeqCst);
208 trace!(ino = %self.ino.0, new_count = %(prev_handles + 1), "acquiring handle for InodeGuard");
209 }
210
211 pub fn drop_one_handle(&self) {
212 let prev_handles = self.open_handles.fetch_sub(1, Ordering::SeqCst);
213
214 trace!(ino = %self.ino.0, %prev_handles, "dropping handle for InodeGuard");
215
216 if prev_handles > 1 {
218 return;
219 }
220
221 let mut guard = self.inner.write();
223
224 if prev_handles != 1 {
229 panic!("InodeGuard handle dropped too many times");
230 }
231
232 if self.open_handles.load(Ordering::SeqCst) != 0 {
234 return;
235 }
236
237 let ino = self.ino.0;
238 trace!(%ino, "InodeGuard has no more open handles");
239
240 match guard.deref_mut() {
241 Kind::File { handle, .. } if handle.is_some() => {
242 let file_ref_count = Arc::strong_count(handle.as_ref().unwrap());
243 trace!(%file_ref_count, %ino, "dropping file handle");
244 drop(handle.take().unwrap());
245 }
246 Kind::PipeRx { rx } => {
247 trace!(%ino, "closing pipe rx");
248 rx.close();
249 }
250 Kind::PipeTx { tx } => {
251 trace!(%ino, "closing pipe tx");
252 tx.close();
253 }
254 _ => (),
255 }
256 }
257}
258impl std::ops::Deref for InodeGuard {
259 type Target = InodeVal;
260 fn deref(&self) -> &Self::Target {
261 self.inner.deref()
262 }
263}
264
265#[derive(Debug, Clone)]
266pub struct InodeWeakGuard {
267 ino: Inode,
268 open_handles: Arc<AtomicI32>,
272 inner: Weak<InodeVal>,
273}
274impl InodeWeakGuard {
275 pub fn ino(&self) -> Inode {
276 self.ino
277 }
278 pub fn upgrade(&self) -> Option<InodeGuard> {
279 Weak::upgrade(&self.inner).map(|inner| InodeGuard {
280 ino: self.ino,
281 open_handles: self.open_handles.clone(),
282 inner,
283 })
284 }
285}
286
287#[derive(Debug)]
288#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
289struct EphemeralSymlinkEntry {
290 path_to_symlink: PathBuf,
291 relative_path: PathBuf,
292}
293
294#[derive(Debug)]
295#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
296#[warn(unused)]
297enum ComponentResolution {
298 Create {
299 kind: Kind,
300 name: String,
301 entry_name: String,
302 is_ephemeral: bool,
303 },
304 BackingSymlink {
305 file: PathBuf,
306 link_value: PathBuf,
307 entry_name: String,
308 },
309 #[cfg(unix)]
310 Special {
311 kind: Kind,
312 name: Cow<'static, str>,
313 entry_name: String,
314 stat: Filestat,
315 },
316}
317
318#[derive(Debug)]
319#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
320struct WasiInodesProtected {
321 lookup: HashMap<Inode, Weak<InodeVal>>,
322}
323
324#[derive(Clone, Debug)]
325#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
326pub struct WasiInodes {
327 protected: Arc<RwLock<WasiInodesProtected>>,
328}
329
330impl WasiInodes {
331 pub fn new() -> Self {
332 Self {
333 protected: Arc::new(RwLock::new(WasiInodesProtected {
334 lookup: Default::default(),
335 })),
336 }
337 }
338
339 pub fn add_inode_val(&self, val: InodeVal) -> InodeGuard {
341 let val = Arc::new(val);
342 let st_ino = {
343 let guard = val.stat.read().unwrap();
344 guard.st_ino
345 };
346
347 let mut guard = self.protected.write().unwrap();
348 let ino = Inode(st_ino);
349 guard.lookup.insert(ino, Arc::downgrade(&val));
350
351 if guard.lookup.len() % 100 == 1 {
353 guard.lookup.retain(|_, v| Weak::strong_count(v) > 0);
354 }
355
356 let open_handles = Arc::new(AtomicI32::new(0));
357
358 InodeGuard {
359 ino,
360 open_handles,
361 inner: val,
362 }
363 }
364
365 pub(crate) fn stdout_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
367 Self::std_dev_get_mut(fd_map, __WASI_STDOUT_FILENO)
368 }
369
370 pub(crate) fn stderr_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
372 Self::std_dev_get_mut(fd_map, __WASI_STDERR_FILENO)
373 }
374
375 #[allow(dead_code)]
378 pub(crate) fn stdin(fd_map: &RwLock<FdList>) -> Result<InodeValFileReadGuard, FsError> {
379 Self::std_dev_get(fd_map, __WASI_STDIN_FILENO)
380 }
381 pub(crate) fn stdin_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
383 Self::std_dev_get_mut(fd_map, __WASI_STDIN_FILENO)
384 }
385
386 fn std_dev_get(fd_map: &RwLock<FdList>, fd: WasiFd) -> Result<InodeValFileReadGuard, FsError> {
389 if let Some(fd) = fd_map.read().unwrap().get(fd) {
390 let guard = fd.inode.read();
391 if let Kind::File {
392 handle: Some(handle),
393 ..
394 } = guard.deref()
395 {
396 Ok(InodeValFileReadGuard::new(handle))
397 } else {
398 Err(FsError::NotAFile)
400 }
401 } else {
402 Err(FsError::NoDevice)
404 }
405 }
406 fn std_dev_get_mut(
409 fd_map: &RwLock<FdList>,
410 fd: WasiFd,
411 ) -> Result<InodeValFileWriteGuard, FsError> {
412 if let Some(fd) = fd_map.read().unwrap().get(fd) {
413 let guard = fd.inode.read();
414 if let Kind::File {
415 handle: Some(handle),
416 ..
417 } = guard.deref()
418 {
419 Ok(InodeValFileWriteGuard::new(handle))
420 } else {
421 Err(FsError::NotAFile)
423 }
424 } else {
425 Err(FsError::NoDevice)
427 }
428 }
429}
430
431impl Default for WasiInodes {
432 fn default() -> Self {
433 Self::new()
434 }
435}
436
437#[derive(Debug, Clone)]
438pub struct WasiFsRoot {
439 root: Arc<MountFileSystem>,
440 memory_limiter: Option<DynFsMemoryLimiter>,
441}
442
443impl WasiFsRoot {
444 pub fn from_mount_fs(root: MountFileSystem) -> Self {
445 Self {
446 root: Arc::new(root),
447 memory_limiter: None,
448 }
449 }
450
451 pub fn from_filesystem(fs: Arc<dyn FileSystem + Send + Sync>) -> Self {
452 let root = MountFileSystem::new();
453 root.mount(Path::new("/"), fs)
454 .expect("mounting the root fs on an empty mount fs should succeed");
455
456 Self {
457 root: Arc::new(root),
458 memory_limiter: None,
459 }
460 }
461
462 pub fn with_memory_limiter_opt(mut self, limiter: Option<DynFsMemoryLimiter>) -> Self {
463 self.memory_limiter = limiter;
464 self
465 }
466
467 pub(crate) fn memory_limiter(&self) -> Option<&DynFsMemoryLimiter> {
468 self.memory_limiter.as_ref()
469 }
470
471 pub(crate) fn root(&self) -> &Arc<MountFileSystem> {
472 &self.root
473 }
474
475 pub(crate) fn writable_root(&self) -> Option<TmpFileSystem> {
476 let root = self.root.filesystem_at(Path::new("/"))?;
477 find_writable_root(root.as_ref())
478 }
479
480 pub(crate) fn stack_root_filesystem(
481 &self,
482 lower: Arc<dyn FileSystem + Send + Sync>,
483 ) -> Result<(), FsError> {
484 let current = self
485 .root
486 .filesystem_at(Path::new("/"))
487 .ok_or(FsError::EntryNotFound)?;
488 let overlay =
489 OverlayFileSystem::new(ArcFileSystem::new(current), [ArcFileSystem::new(lower)]);
490 self.root.set_mount(Path::new("/"), Arc::new(overlay))
491 }
492}
493
494fn find_writable_root(fs: &(dyn FileSystem + Send + Sync)) -> Option<TmpFileSystem> {
495 if let Some(tmp) = fs.upcast_any_ref().downcast_ref::<TmpFileSystem>() {
496 return Some(tmp.clone());
497 }
498
499 if let Some(arc_fs) = fs.upcast_any_ref().downcast_ref::<ArcFileSystem>() {
500 return find_writable_root(arc_fs.inner().as_ref());
501 }
502
503 if let Some(overlay) = fs
504 .upcast_any_ref()
505 .downcast_ref::<OverlayFileSystem<ArcFileSystem, Vec<Arc<dyn FileSystem + Send + Sync>>>>()
506 {
507 return find_writable_root(overlay.primary());
508 }
509
510 if let Some(overlay) = fs
511 .upcast_any_ref()
512 .downcast_ref::<OverlayFileSystem<ArcFileSystem, [ArcFileSystem; 1]>>()
513 {
514 return find_writable_root(overlay.primary());
515 }
516
517 None
518}
519
520impl FileSystem for WasiFsRoot {
521 fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
522 self.root.readlink(path)
523 }
524
525 fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
526 self.root.read_dir(path)
527 }
528
529 fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
530 self.root.create_dir(path)
531 }
532
533 fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> {
534 self.root.create_symlink(source, target)
535 }
536
537 fn hard_link(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> {
538 self.root.hard_link(source, target)
539 }
540
541 fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
542 self.root.remove_dir(path)
543 }
544
545 fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
546 let from = from.to_owned();
547 let to = to.to_owned();
548 let this = self.clone();
549 Box::pin(async move { this.root.rename(&from, &to).await })
550 }
551
552 fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
553 self.root.metadata(path)
554 }
555
556 fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
557 self.root.symlink_metadata(path)
558 }
559
560 fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
561 self.root.remove_file(path)
562 }
563
564 fn new_open_options(&self) -> OpenOptions<'_> {
565 self.root.new_open_options()
566 }
567}
568
569#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
577pub struct WasiFs {
578 pub preopen_fds: RwLock<Vec<u32>>,
580 pub fd_map: RwLock<FdList>,
581 pub current_dir: Mutex<String>,
582 #[cfg_attr(feature = "enable-serde", serde(skip, default))]
583 pub root_fs: WasiFsRoot,
584 pub root_inode: InodeGuard,
585 pub has_unioned: Mutex<HashSet<PackageId>>,
586 ephemeral_symlinks: Arc<RwLock<HashMap<PathBuf, EphemeralSymlinkEntry>>>,
587
588 is_wasix: AtomicBool,
593
594 pub(crate) init_preopens: Vec<PreopenedDir>,
596 pub(crate) init_vfs_preopens: Vec<String>,
598}
599
600impl WasiFs {
601 fn writable_package_mount(
602 fs: Arc<dyn FileSystem + Send + Sync>,
603 limiter: Option<&DynFsMemoryLimiter>,
604 ) -> Arc<dyn FileSystem + Send + Sync> {
605 let upper = TmpFileSystem::new();
606 if let Some(limiter) = limiter {
607 upper.set_memory_limiter(limiter.clone());
608 }
609
610 Arc::new(OverlayFileSystem::new(upper, [ArcFileSystem::new(fs)]))
611 }
612
613 pub fn is_wasix(&self) -> bool {
614 self.is_wasix.load(Ordering::Relaxed)
617 }
618
619 pub fn set_is_wasix(&self, is_wasix: bool) {
620 self.is_wasix.store(is_wasix, Ordering::SeqCst);
621 }
622
623 pub(crate) fn register_ephemeral_symlink(
624 &self,
625 full_path: PathBuf,
626 path_to_symlink: PathBuf,
627 relative_path: PathBuf,
628 ) {
629 let mut guard = self.ephemeral_symlinks.write().unwrap();
630 guard.insert(
631 PosixPath::from_path(&full_path)
632 .normalize_virtual_symlink_key()
633 .into_path_buf(),
634 EphemeralSymlinkEntry {
635 path_to_symlink: PosixPath::from_path(&path_to_symlink)
636 .normalize_virtual_symlink_key()
637 .into_path_buf(),
638 relative_path,
639 },
640 );
641 }
642
643 pub(crate) fn ephemeral_symlink_at(&self, full_path: &Path) -> Option<(PathBuf, PathBuf)> {
644 let guard = self.ephemeral_symlinks.read().unwrap();
645 let key = PosixPath::from_path(full_path)
646 .normalize_virtual_symlink_key()
647 .into_path_buf();
648 let entry = guard.get(&key)?;
649 Some((entry.path_to_symlink.clone(), entry.relative_path.clone()))
650 }
651
652 pub(crate) fn unregister_ephemeral_symlink(&self, full_path: &Path) {
653 let mut guard = self.ephemeral_symlinks.write().unwrap();
654 let key = PosixPath::from_path(full_path)
655 .normalize_virtual_symlink_key()
656 .into_path_buf();
657 guard.remove(&key);
658 }
659
660 pub(crate) fn move_ephemeral_symlink(
661 &self,
662 old_full_path: &Path,
663 new_full_path: &Path,
664 path_to_symlink: PathBuf,
665 relative_path: PathBuf,
666 ) {
667 let old_key = PosixPath::from_path(old_full_path)
668 .normalize_virtual_symlink_key()
669 .into_path_buf();
670 let new_key = PosixPath::from_path(new_full_path)
671 .normalize_virtual_symlink_key()
672 .into_path_buf();
673
674 let mut guard = self.ephemeral_symlinks.write().unwrap();
675 guard.remove(&old_key);
676 guard.insert(
677 new_key,
678 EphemeralSymlinkEntry {
679 path_to_symlink: PosixPath::from_path(&path_to_symlink)
680 .normalize_virtual_symlink_key()
681 .into_path_buf(),
682 relative_path,
683 },
684 );
685 }
686
687 pub fn fork(&self) -> Self {
689 Self {
690 preopen_fds: RwLock::new(self.preopen_fds.read().unwrap().clone()),
691 fd_map: RwLock::new(self.fd_map.read().unwrap().clone()),
692 current_dir: Mutex::new(self.current_dir.lock().unwrap().clone()),
693 is_wasix: AtomicBool::new(self.is_wasix.load(Ordering::Acquire)),
694 root_fs: self.root_fs.clone(),
695 root_inode: self.root_inode.clone(),
696 has_unioned: Mutex::new(self.has_unioned.lock().unwrap().clone()),
697 ephemeral_symlinks: self.ephemeral_symlinks.clone(),
698 init_preopens: self.init_preopens.clone(),
699 init_vfs_preopens: self.init_vfs_preopens.clone(),
700 }
701 }
702
703 pub async fn close_cloexec_fds(&self) {
705 let flush_targets = {
706 let mut fd_map = self.fd_map.write().unwrap();
707 let to_close: Vec<WasiFd> = fd_map
708 .iter()
709 .filter_map(|(k, v)| {
710 if v.inner.fd_flags.contains(Fdflagsext::CLOEXEC)
711 && !v.is_stdio
712 && !v.inode.is_preopened
713 {
714 tracing::trace!(fd = %k, "Closing FD due to CLOEXEC flag");
715 Some(k)
716 } else {
717 None
718 }
719 })
720 .collect();
721 let mut flush_targets = Vec::new();
722 for fd in to_close {
723 let outcome = Self::close_fd_locked(&mut fd_map, fd);
724 if let Some(target) = outcome.flush_target {
725 flush_targets.push(target);
726 }
727 }
728 flush_targets
729 };
730
731 for file in flush_targets {
732 Self::flush_file_best_effort(file).await;
733 }
734 }
735
736 pub async fn close_all(&self) {
738 let flush_targets = {
739 let mut fd_map = self.fd_map.write().unwrap();
740 let mut fds: HashSet<WasiFd> = fd_map.keys().collect();
741 fds.insert(__WASI_STDOUT_FILENO);
742 fds.insert(__WASI_STDERR_FILENO);
743
744 let mut flush_targets = Vec::new();
745 for fd in fds {
746 let outcome = Self::close_fd_locked(&mut fd_map, fd);
747 if let Some(target) = outcome.flush_target {
748 flush_targets.push(target);
749 }
750 }
751
752 for (_fd, fd_ref) in fd_map.iter().collect::<Vec<_>>() {
754 if let Some(target) = Self::file_flush_target(&fd_ref.inode) {
755 flush_targets.push(target);
756 }
757 }
758 fd_map.clear();
759 flush_targets
760 };
761
762 for file in flush_targets {
763 Self::flush_file_best_effort(file).await;
764 }
765 }
766
767 pub async fn conditional_union(
770 &self,
771 binary: &BinaryPackage,
772 ) -> Result<(), virtual_fs::FsError> {
773 let Some(package_mounts) = &binary.package_mounts else {
774 return Ok(());
775 };
776
777 let needs_to_be_unioned = self.has_unioned.lock().unwrap().insert(binary.id.clone());
778 if !needs_to_be_unioned {
779 return Ok(());
780 }
781
782 if let Some(root_layer) = &package_mounts.root_layer {
783 self.root_fs
784 .stack_root_filesystem(Self::writable_package_mount(
785 root_layer.clone(),
786 self.root_fs.memory_limiter(),
787 ))?;
788 }
789
790 for mount in &package_mounts.mounts {
791 self.root_fs.root().mount_with_source(
792 &mount.guest_path,
793 &mount.source_path,
794 Self::writable_package_mount(mount.fs.clone(), self.root_fs.memory_limiter()),
795 )?;
796 }
797
798 Ok(())
799 }
800
801 pub(crate) fn new_with_preopen(
803 inodes: &WasiInodes,
804 preopens: &[PreopenedDir],
805 vfs_preopens: &[String],
806 fs_backing: WasiFsRoot,
807 ) -> Result<Self, String> {
808 let mut wasi_fs = Self::new_init(fs_backing, inodes, FS_ROOT_INO)?;
809 wasi_fs.init_preopens = preopens.to_vec();
810 wasi_fs.init_vfs_preopens = vfs_preopens.to_vec();
811 wasi_fs.create_preopens(inodes, false)?;
812 Ok(wasi_fs)
813 }
814
815 pub(crate) fn relative_path_to_absolute(&self, path: String) -> String {
817 if path.starts_with('/') {
818 return path;
819 }
820
821 let current_dir = self.current_dir.lock().unwrap();
822 format!("{}/{}", current_dir.trim_end_matches('/'), path)
823 }
824
825 fn new_init(
828 fs_backing: WasiFsRoot,
829 inodes: &WasiInodes,
830 st_ino: Inode,
831 ) -> Result<Self, String> {
832 debug!("Initializing WASI filesystem");
833
834 let stat = Filestat {
835 st_filetype: Filetype::Directory,
836 st_ino: st_ino.as_u64(),
837 ..Filestat::default()
838 };
839 let root_kind = Kind::Root {
840 entries: HashMap::new(),
841 };
842 let root_inode = inodes.add_inode_val(InodeVal {
843 stat: RwLock::new(stat),
844 is_preopened: true,
845 name: RwLock::new("/".into()),
846 kind: RwLock::new(root_kind),
847 });
848
849 let wasi_fs = Self {
850 preopen_fds: RwLock::new(vec![]),
851 fd_map: RwLock::new(FdList::new()),
852 current_dir: Mutex::new("/".to_string()),
853 is_wasix: AtomicBool::new(false),
854 root_fs: fs_backing,
855 root_inode,
856 has_unioned: Mutex::new(HashSet::new()),
857 ephemeral_symlinks: Arc::new(RwLock::new(HashMap::new())),
858 init_preopens: Default::default(),
859 init_vfs_preopens: Default::default(),
860 };
861 wasi_fs.create_stdin(inodes);
862 wasi_fs.create_stdout(inodes);
863 wasi_fs.create_stderr(inodes);
864 wasi_fs.create_rootfd()?;
865
866 Ok(wasi_fs)
867 }
868
869 #[allow(dead_code)]
879 #[allow(clippy::too_many_arguments)]
880 pub unsafe fn open_dir_all(
881 &mut self,
882 inodes: &WasiInodes,
883 base: WasiFd,
884 name: String,
885 rights: Rights,
886 rights_inheriting: Rights,
887 flags: Fdflags,
888 fd_flags: Fdflagsext,
889 ) -> Result<WasiFd, FsError> {
890 let mut cur_inode = self.get_fd_inode(base).map_err(fs_error_from_wasi_err)?;
893
894 let path: &Path = Path::new(&name);
895 for c in path.components() {
897 let segment_name = c.as_os_str().to_string_lossy().to_string();
898 let guard = cur_inode.read();
899 match guard.deref() {
900 Kind::Dir { entries, .. } | Kind::Root { entries } => {
901 if let Some(_entry) = entries.get(&segment_name) {
902 return Err(FsError::AlreadyExists);
904 }
905
906 let kind = Kind::Dir {
907 parent: cur_inode.downgrade(),
908 path: PathBuf::from(""),
909 entries: HashMap::new(),
910 };
911
912 drop(guard);
913 let inode = self.create_inode_with_default_stat(
914 inodes,
915 kind,
916 false,
917 segment_name.clone().into(),
918 );
919
920 {
922 let mut guard = cur_inode.write();
923 match guard.deref_mut() {
924 Kind::Dir { entries, .. } | Kind::Root { entries } => {
925 entries.insert(segment_name, inode.clone());
926 }
927 _ => unreachable!("Dir or Root became not Dir or Root"),
928 }
929 }
930 cur_inode = inode;
931 }
932 _ => return Err(FsError::BaseNotDirectory),
933 }
934 }
935
936 self.create_fd(
938 rights,
939 rights_inheriting,
940 flags,
941 fd_flags,
942 Fd::READ | Fd::WRITE,
943 cur_inode,
944 )
945 .map_err(fs_error_from_wasi_err)
946 }
947
948 #[allow(dead_code, clippy::too_many_arguments)]
953 pub fn open_file_at(
954 &mut self,
955 inodes: &WasiInodes,
956 base: WasiFd,
957 file: Box<dyn VirtualFile + Send + Sync + 'static>,
958 open_flags: u16,
959 name: String,
960 rights: Rights,
961 rights_inheriting: Rights,
962 flags: Fdflags,
963 fd_flags: Fdflagsext,
964 ) -> Result<WasiFd, FsError> {
965 let base_inode = self.get_fd_inode(base).map_err(fs_error_from_wasi_err)?;
968
969 let guard = base_inode.read();
970 match guard.deref() {
971 Kind::Dir { entries, .. } | Kind::Root { entries } => {
972 if let Some(_entry) = entries.get(&name) {
973 return Err(FsError::AlreadyExists);
975 }
976
977 let kind = Kind::File {
978 handle: Some(Arc::new(RwLock::new(file))),
979 path: PathBuf::from(""),
980 fd: None,
981 };
982
983 drop(guard);
984 let inode = self
985 .create_inode(inodes, kind, false, name.clone())
986 .map_err(|_| FsError::IOError)?;
987
988 {
989 let mut guard = base_inode.write();
990 match guard.deref_mut() {
991 Kind::Dir { entries, .. } | Kind::Root { entries } => {
992 entries.insert(name, inode.clone());
993 }
994 _ => unreachable!("Dir or Root became not Dir or Root"),
995 }
996 }
997
998 let real_fd = self
1000 .create_fd(
1001 rights,
1002 rights_inheriting,
1003 flags,
1004 fd_flags,
1005 open_flags,
1006 inode.clone(),
1007 )
1008 .map_err(fs_error_from_wasi_err)?;
1009
1010 {
1011 let mut guard = inode.kind.write().unwrap();
1012 match guard.deref_mut() {
1013 Kind::File { fd, .. } => {
1014 *fd = Some(real_fd);
1015 }
1016 _ => unreachable!("We just created a Kind::File"),
1017 }
1018 }
1019
1020 Ok(real_fd)
1021 }
1022 _ => Err(FsError::BaseNotDirectory),
1023 }
1024 }
1025
1026 #[allow(dead_code)]
1030 pub fn swap_file(
1031 &self,
1032 fd: WasiFd,
1033 mut file: Box<dyn VirtualFile + Send + Sync + 'static>,
1034 ) -> Result<Option<Box<dyn VirtualFile + Send + Sync + 'static>>, FsError> {
1035 match fd {
1036 __WASI_STDIN_FILENO => {
1037 let mut target = WasiInodes::stdin_mut(&self.fd_map)?;
1038 Ok(Some(target.swap(file)))
1039 }
1040 __WASI_STDOUT_FILENO => {
1041 let mut target = WasiInodes::stdout_mut(&self.fd_map)?;
1042 Ok(Some(target.swap(file)))
1043 }
1044 __WASI_STDERR_FILENO => {
1045 let mut target = WasiInodes::stderr_mut(&self.fd_map)?;
1046 Ok(Some(target.swap(file)))
1047 }
1048 _ => {
1049 let base_inode = self.get_fd_inode(fd).map_err(fs_error_from_wasi_err)?;
1050 {
1051 let guard = base_inode.read();
1053 match guard.deref() {
1054 Kind::File { handle, .. } => {
1055 if let Some(handle) = handle {
1056 let mut handle = handle.write().unwrap();
1057 std::mem::swap(handle.deref_mut(), &mut file);
1058 return Ok(Some(file));
1059 }
1060 }
1061 _ => return Err(FsError::NotAFile),
1062 }
1063 }
1064 let mut guard = base_inode.write();
1066 match guard.deref_mut() {
1067 Kind::File { handle, .. } => {
1068 if let Some(handle) = handle {
1069 let mut handle = handle.write().unwrap();
1070 std::mem::swap(handle.deref_mut(), &mut file);
1071 Ok(Some(file))
1072 } else {
1073 handle.replace(Arc::new(RwLock::new(file)));
1074 Ok(None)
1075 }
1076 }
1077 _ => Err(FsError::NotAFile),
1078 }
1079 }
1080 }
1081 }
1082
1083 pub fn filestat_resync_size(&self, fd: WasiFd) -> Result<Filesize, Errno> {
1085 let inode = self.get_fd_inode(fd)?;
1086 let mut guard = inode.write();
1087 match guard.deref_mut() {
1088 Kind::File { handle, .. } => {
1089 if let Some(h) = handle {
1090 let h = h.read().unwrap();
1091 let new_size = h.size();
1092 drop(h);
1093 drop(guard);
1094
1095 inode.stat.write().unwrap().st_size = new_size;
1096 Ok(new_size as Filesize)
1097 } else {
1098 Err(Errno::Badf)
1099 }
1100 }
1101 Kind::Dir { .. } | Kind::Root { .. } => Err(Errno::Isdir),
1102 _ => Err(Errno::Inval),
1103 }
1104 }
1105
1106 pub fn set_current_dir(&self, path: &str) {
1108 let mut guard = self.current_dir.lock().unwrap();
1109 *guard = path.to_string();
1110 }
1111
1112 pub fn get_current_dir(
1114 &self,
1115 inodes: &WasiInodes,
1116 base: WasiFd,
1117 ) -> Result<(InodeGuard, String), Errno> {
1118 self.get_current_dir_inner(inodes, base, 0)
1119 }
1120
1121 pub(crate) fn get_current_dir_inner(
1122 &self,
1123 inodes: &WasiInodes,
1124 base: WasiFd,
1125 symlink_count: u32,
1126 ) -> Result<(InodeGuard, String), Errno> {
1127 let mut symlink_count = symlink_count;
1128 let current_dir = {
1129 let guard = self.current_dir.lock().unwrap();
1130 guard.clone()
1131 };
1132 let cur_inode = self.get_fd_inode(base)?;
1133 let inode = self.get_inode_at_path_inner(
1134 inodes,
1135 cur_inode,
1136 current_dir.as_str(),
1137 &mut symlink_count,
1138 true,
1139 )?;
1140 Ok((inode, current_dir))
1141 }
1142
1143 fn get_inode_at_path_inner(
1228 &self,
1229 inodes: &WasiInodes,
1230 mut cur_inode: InodeGuard,
1231 path_str: &str,
1232 symlink_count: &mut u32,
1233 follow_symlinks: bool,
1234 ) -> Result<InodeGuard, Errno> {
1235 if *symlink_count > MAX_SYMLINKS {
1236 return Err(Errno::Loop);
1237 }
1238
1239 if path_str.is_empty() {
1240 return Err(Errno::Noent);
1241 }
1242
1243 if path_str.starts_with('/') {
1244 cur_inode = self.get_fd_inode(VIRTUAL_ROOT_FD)?;
1245 }
1246
1247 let is_all_slashes = path_str.bytes().all(|b| b == b'/');
1248
1249 if is_all_slashes {
1252 if let Kind::Root { entries } = cur_inode.read().deref()
1253 && let Some(root_entry) = entries.get("/")
1254 {
1255 return Ok(root_entry.clone());
1256 }
1257 return Ok(cur_inode);
1258 }
1259
1260 let path = PosixPath::new(path_str);
1264 let components = path.components(true, true);
1265
1266 let n_components = components.len();
1267
1268 'path_iter: for (i, component) in components.into_iter().enumerate() {
1272 if matches!(component, PosixPathComponent::RootDir) {
1276 continue 'path_iter;
1277 }
1278
1279 let last_component = i == n_components - 1;
1287
1288 let component_str = match component {
1289 PosixPathComponent::CurDir => {
1290 let is_dir = {
1291 let guard = cur_inode.read();
1292 matches!(guard.deref(), Kind::Dir { .. } | Kind::Root { .. })
1293 };
1294 if is_dir {
1295 continue 'path_iter;
1296 }
1297 return Err(Errno::Notdir);
1298 }
1299 PosixPathComponent::ParentDir => {
1300 let parent_inode = {
1301 let guard = cur_inode.read();
1302 match guard.deref() {
1303 Kind::Root { .. } => None,
1304 Kind::Dir { parent, .. } => {
1305 Some(parent.upgrade().ok_or(Errno::Access)?)
1306 }
1307 _ => return Err(Errno::Notdir),
1308 }
1309 };
1310 if let Some(parent_inode) = parent_inode {
1311 cur_inode = parent_inode;
1312 }
1313 continue 'path_iter;
1314 }
1315 PosixPathComponent::Normal(component) => component,
1316 PosixPathComponent::RootDir => unreachable!("RootDir is handled above"),
1317 };
1318
1319 'component_lookup: loop {
1320 let component_resolution = {
1330 match cur_inode.clone().read().deref() {
1331 Kind::Buffer { .. } => {
1332 unimplemented!("state::get_inode_at_path for buffers")
1333 }
1334 Kind::File { .. }
1335 | Kind::Socket { .. }
1336 | Kind::PipeRx { .. }
1337 | Kind::PipeTx { .. }
1338 | Kind::DuplexPipe { .. }
1339 | Kind::EventNotifications { .. }
1340 | Kind::Epoll { .. } => {
1341 return Err(Errno::Notdir);
1342 }
1343 Kind::Symlink { .. } => break 'component_lookup,
1344 Kind::Root { entries } => {
1345 if let Some(entry) = entries.get(component_str) {
1346 cur_inode = entry.clone();
1347 break 'component_lookup;
1348 } else if let Some(root) = entries.get("/") {
1349 cur_inode = root.clone();
1357 continue 'component_lookup;
1358 } else {
1359 return Err(Errno::Notcapable);
1362 }
1363 }
1364 Kind::Dir {
1365 entries,
1366 path: cur_dir,
1367 ..
1368 } => {
1369 if let Some(entry) = entries.get(component_str) {
1376 cur_inode = entry.clone();
1380 break 'component_lookup;
1381 }
1382
1383 let entry_path_buf = PosixPath::from_path(cur_dir)
1386 .join(&PosixPath::new(component_str))
1387 .into_path_buf();
1388
1389 let entry_name = component_str.to_string();
1392
1393 let entry_path = entry_path_buf.to_string_lossy().to_string();
1399
1400 if let Some((path_to_symlink, relative_path)) =
1401 self.ephemeral_symlink_at(&entry_path_buf)
1402 {
1403 ComponentResolution::Create {
1410 kind: Kind::Symlink {
1411 symlink_kind: SymlinkKind::Virtual,
1412 path_to_symlink,
1413 relative_path,
1414 },
1415 name: entry_path,
1416 entry_name,
1417 is_ephemeral: true,
1418 }
1419 } else {
1420 let metadata = self
1426 .root_fs
1427 .symlink_metadata(&entry_path_buf)
1428 .ok()
1429 .ok_or(Errno::Noent)?;
1430 let file_type = metadata.file_type();
1431 if file_type.is_dir() {
1432 ComponentResolution::Create {
1434 kind: Kind::Dir {
1435 parent: cur_inode.downgrade(),
1436 path: entry_path_buf,
1437 entries: Default::default(),
1438 },
1439 name: entry_path,
1440 entry_name,
1441 is_ephemeral: false,
1442 }
1443 } else if file_type.is_file() {
1444 ComponentResolution::Create {
1446 kind: Kind::File {
1447 handle: None,
1448 path: entry_path_buf,
1449 fd: None,
1450 },
1451 name: entry_path,
1452 entry_name,
1453 is_ephemeral: false,
1454 }
1455 } else if file_type.is_symlink() {
1456 let link_value = self
1463 .root_fs
1464 .readlink(&entry_path_buf)
1465 .ok()
1466 .ok_or(Errno::Noent)?;
1467 debug!("attempting to decompose path {:?}", link_value);
1468 ComponentResolution::BackingSymlink {
1469 file: entry_path_buf,
1470 link_value,
1471 entry_name,
1472 }
1473 } else {
1474 #[cfg(unix)]
1475 {
1476 let file_type: Filetype = if file_type.is_char_device() {
1478 Filetype::CharacterDevice
1479 } else if file_type.is_block_device() {
1480 Filetype::BlockDevice
1481 } else if file_type.is_fifo() {
1482 Filetype::Unknown
1484 } else if file_type.is_socket() {
1485 Filetype::SocketStream
1488 } else {
1489 unimplemented!(
1490 "state::get_inode_at_path unknown file type: not file, directory, symlink, char device, block device, fifo, or socket"
1491 );
1492 };
1493
1494 ComponentResolution::Special {
1495 kind: Kind::File {
1496 handle: None,
1497 path: entry_path_buf,
1498 fd: None,
1499 },
1500 name: entry_path.into(),
1501 entry_name,
1502 stat: Filestat {
1503 st_filetype: file_type,
1504 st_ino: Inode::from_path(path_str).as_u64(),
1505 st_size: metadata.len(),
1506 st_ctim: metadata.created(),
1507 st_mtim: metadata.modified(),
1508 st_atim: metadata.accessed(),
1509 ..Filestat::default()
1510 },
1511 }
1512 }
1513 #[cfg(not(unix))]
1514 unimplemented!(
1515 "state::get_inode_at_path unknown file type: not file, directory, or symlink"
1516 );
1517 }
1518 } } } }; let (entry_name, new_inode, should_insert, should_return) =
1529 match component_resolution {
1530 ComponentResolution::Create {
1531 kind,
1532 name,
1533 entry_name,
1534 is_ephemeral,
1535 } => {
1536 let new_inode = self.create_inode(inodes, kind, false, name)?;
1537 (entry_name, new_inode, !is_ephemeral, false)
1538 }
1539 ComponentResolution::BackingSymlink {
1540 file,
1541 link_value,
1542 entry_name,
1543 } => {
1544 let new_inode = self.create_inode(
1545 inodes,
1546 Kind::Symlink {
1547 symlink_kind: SymlinkKind::Backing,
1548 path_to_symlink: PosixPath::from_path(&file)
1549 .strip_root_prefix()
1550 .into_path_buf(),
1551 relative_path: link_value,
1552 },
1553 false,
1554 file.to_string_lossy().to_string(),
1555 )?;
1556 (entry_name, new_inode, false, false)
1557 }
1558 #[cfg(unix)]
1559 ComponentResolution::Special {
1560 kind,
1561 name,
1562 entry_name,
1563 stat,
1564 } => {
1565 let new_inode =
1566 self.create_inode_with_stat(inodes, kind, false, name, stat);
1567 (entry_name, new_inode, true, true)
1568 }
1569 };
1570
1571 {
1572 let mut guard = cur_inode.write();
1573 let Kind::Dir { entries, .. } = guard.deref_mut() else {
1574 unreachable!("Attempted to insert special device into non-directory");
1575 };
1576
1577 if should_insert {
1578 entries.insert(entry_name, new_inode.clone());
1579 }
1580
1581 if should_return {
1582 if last_component {
1584 return Ok(new_inode);
1585 }
1586 return Err(Errno::Notdir);
1587 }
1588 }
1589
1590 cur_inode = new_inode;
1595 break 'component_lookup;
1596 } if last_component && !follow_symlinks {
1602 continue 'path_iter;
1606 }
1607
1608 let (symlink_kind, path_to_symlink, relative_path) = {
1610 let guard = cur_inode.read();
1611 let Kind::Symlink {
1612 symlink_kind,
1613 path_to_symlink,
1614 relative_path,
1615 } = guard.deref()
1616 else {
1617 continue 'path_iter;
1619 };
1620 (
1621 *symlink_kind,
1622 path_to_symlink.clone(),
1623 relative_path.clone(),
1624 )
1625 };
1626
1627 let (new_base_fd, new_path) =
1628 self.resolve_symlink_target_path(symlink_kind, &path_to_symlink, &relative_path)?;
1629 let new_base_inode = self.get_fd_inode(new_base_fd)?;
1630 let new_path = PosixPath::from_path(&new_path).as_str().to_owned();
1631
1632 let follow_symlinks_inner = !last_component || follow_symlinks;
1636
1637 debug!("Following symlink recursively");
1638 *symlink_count += 1;
1639 if *symlink_count > MAX_SYMLINKS {
1640 return Err(Errno::Loop);
1641 }
1642 let symlink_inode = self.get_inode_at_path_inner(
1643 inodes,
1644 new_base_inode,
1645 &new_path,
1646 symlink_count,
1647 follow_symlinks_inner,
1648 )?;
1649
1650 cur_inode = symlink_inode;
1653 }
1654
1655 Ok(cur_inode)
1656 }
1657
1658 pub(crate) fn resolve_symlink_target_path(
1659 &self,
1660 symlink_kind: SymlinkKind,
1661 path_to_symlink: &Path,
1662 relative_path: &Path,
1663 ) -> Result<(WasiFd, PathBuf), Errno> {
1664 let relative_posix = PosixPath::from_path(relative_path);
1665 if matches!(symlink_kind, SymlinkKind::Virtual) && relative_posix.is_absolute() {
1666 return Ok((VIRTUAL_ROOT_FD, relative_path.to_owned()));
1667 }
1668
1669 let symlink_parent = match symlink_kind {
1670 SymlinkKind::Virtual => PosixPath::from_path(path_to_symlink)
1671 .parent()
1672 .into_path_buf(),
1673 SymlinkKind::Backing => {
1674 let symlink_path_buf =
1675 PosixPath::new("/").join(&PosixPath::from_path(path_to_symlink));
1676 let symlink_path = symlink_path_buf.as_posix_path();
1677 let mount_entry = self
1678 .root_fs
1679 .root()
1680 .mount_entries()
1681 .into_iter()
1682 .filter(|entry| {
1683 symlink_path
1684 .strip_prefix(&PosixPath::from_path(&entry.path))
1685 .is_some()
1686 })
1687 .max_by_key(|entry| PosixPath::from_path(&entry.path).as_str().len())
1688 .ok_or(Errno::Perm)?;
1689 let mount_path = mount_entry.path;
1690
1691 let symlink_relative = symlink_path
1692 .strip_prefix(&PosixPath::from_path(&mount_path))
1693 .ok_or(Errno::Perm)?;
1694 let symlink_parent = symlink_relative.parent().into_path_buf();
1695 let contained_target = if relative_posix.is_absolute() {
1696 let stripped = relative_posix
1697 .strip_prefix(&PosixPath::from_path(&mount_entry.source_path))
1698 .ok_or(Errno::Perm)?;
1699 PosixPathBuf::from(stripped.as_str().to_owned())
1700 } else {
1701 PosixPathBuf::resolve_relative(
1702 &PosixPath::from_path(&symlink_parent),
1703 &relative_posix,
1704 false,
1705 )?
1706 };
1707
1708 return Ok((
1709 VIRTUAL_ROOT_FD,
1710 PosixPath::from_path(&mount_path)
1711 .join(&contained_target.as_posix_path())
1712 .into_path_buf(),
1713 ));
1714 }
1715 };
1716
1717 Ok((
1718 VIRTUAL_ROOT_FD,
1719 PosixPathBuf::resolve_relative(
1720 &PosixPath::from_path(&symlink_parent),
1721 &relative_posix,
1722 true,
1723 )?
1724 .into_path_buf(),
1725 ))
1726 }
1727
1728 pub(crate) fn rebase_symlink_location(&self, new_symlink_path: &Path) -> PathBuf {
1729 PosixPath::from_path(new_symlink_path)
1730 .strip_root_prefix()
1731 .into_path_buf()
1732 }
1733
1734 pub(crate) fn get_inode_at_path(
1741 &self,
1742 inodes: &WasiInodes,
1743 base: WasiFd,
1744 path: &str,
1745 follow_symlinks: bool,
1746 ) -> Result<InodeGuard, Errno> {
1747 let base_inode = self.get_fd_inode(base)?;
1748 let mut symlink_count = 0;
1749 self.get_inode_at_path_inner(
1750 inodes,
1751 base_inode,
1752 path,
1753 &mut symlink_count,
1754 follow_symlinks,
1755 )
1756 }
1757
1758 pub(crate) fn get_inode_at_path_from_inode(
1759 &self,
1760 inodes: &WasiInodes,
1761 base_inode: InodeGuard,
1762 path: &str,
1763 follow_symlinks: bool,
1764 ) -> Result<InodeGuard, Errno> {
1765 let mut symlink_count = 0;
1766 self.get_inode_at_path_inner(
1767 inodes,
1768 base_inode,
1769 path,
1770 &mut symlink_count,
1771 follow_symlinks,
1772 )
1773 }
1774
1775 pub(crate) fn get_parent_inode_at_path(
1778 &self,
1779 inodes: &WasiInodes,
1780 base: WasiFd,
1781 path: &Path,
1782 follow_symlinks: bool,
1783 ) -> Result<(InodeGuard, String), Errno> {
1784 let (parent_dir, new_entity_name) = PosixPath::from_path(path).parent_path_and_name()?;
1785 if parent_dir.as_str().is_empty() {
1786 return self.get_fd_inode(base).map(|v| (v, new_entity_name));
1787 }
1788 self.get_inode_at_path(inodes, base, parent_dir.as_str(), follow_symlinks)
1789 .map(|v| (v, new_entity_name))
1790 }
1791
1792 pub fn get_fd(&self, fd: WasiFd) -> Result<Fd, Errno> {
1793 let ret = self
1794 .fd_map
1795 .read()
1796 .unwrap()
1797 .get(fd)
1798 .ok_or(Errno::Badf)
1799 .cloned();
1800
1801 if ret.is_err() && fd == VIRTUAL_ROOT_FD {
1802 Ok(Self::virtual_root_fd(self.root_inode.clone()))
1803 } else {
1804 ret
1805 }
1806 }
1807
1808 pub fn get_fd_inode(&self, fd: WasiFd) -> Result<InodeGuard, Errno> {
1809 if fd == VIRTUAL_ROOT_FD {
1811 return Ok(self.root_inode.clone());
1812 }
1813 self.fd_map
1814 .read()
1815 .unwrap()
1816 .get(fd)
1817 .ok_or(Errno::Badf)
1818 .map(|a| a.inode.clone())
1819 }
1820
1821 pub fn filestat_fd(&self, fd: WasiFd) -> Result<Filestat, Errno> {
1822 let inode = self.get_fd_inode(fd)?;
1823 let guard = inode.stat.read().unwrap();
1824 Ok(*guard.deref())
1825 }
1826
1827 pub fn fdstat(&self, fd: WasiFd) -> Result<Fdstat, Errno> {
1828 match fd {
1829 __WASI_STDIN_FILENO => {
1830 return Ok(Fdstat {
1831 fs_filetype: Filetype::CharacterDevice,
1832 fs_flags: Fdflags::empty(),
1833 fs_rights_base: STDIN_DEFAULT_RIGHTS,
1834 fs_rights_inheriting: Rights::empty(),
1835 });
1836 }
1837 __WASI_STDOUT_FILENO => {
1838 return Ok(Fdstat {
1839 fs_filetype: Filetype::CharacterDevice,
1840 fs_flags: Fdflags::APPEND,
1841 fs_rights_base: STDOUT_DEFAULT_RIGHTS,
1842 fs_rights_inheriting: Rights::empty(),
1843 });
1844 }
1845 __WASI_STDERR_FILENO => {
1846 return Ok(Fdstat {
1847 fs_filetype: Filetype::CharacterDevice,
1848 fs_flags: Fdflags::APPEND,
1849 fs_rights_base: STDERR_DEFAULT_RIGHTS,
1850 fs_rights_inheriting: Rights::empty(),
1851 });
1852 }
1853 VIRTUAL_ROOT_FD => {
1854 return Ok(Fdstat {
1855 fs_filetype: Filetype::Directory,
1856 fs_flags: Fdflags::empty(),
1857 fs_rights_base: ALL_RIGHTS,
1859 fs_rights_inheriting: ALL_RIGHTS,
1860 });
1861 }
1862 _ => (),
1863 }
1864 let fd = self.get_fd(fd)?;
1865
1866 let guard = fd.inode.read();
1867 let deref = guard.deref();
1868 Ok(Fdstat {
1869 fs_filetype: match deref {
1870 Kind::File { .. } => Filetype::RegularFile,
1871 Kind::Dir { .. } => Filetype::Directory,
1872 Kind::Symlink { .. } => Filetype::SymbolicLink,
1873 Kind::Socket { socket } => match &socket.inner.protected.read().unwrap().kind {
1874 InodeSocketKind::TcpStream { .. } => Filetype::SocketStream,
1875 InodeSocketKind::Raw { .. } => Filetype::SocketRaw,
1876 InodeSocketKind::PreSocket { props, .. } => match props.ty {
1877 Socktype::Stream => Filetype::SocketStream,
1878 Socktype::Dgram => Filetype::SocketDgram,
1879 Socktype::Raw => Filetype::SocketRaw,
1880 Socktype::Seqpacket => Filetype::SocketSeqpacket,
1881 _ => Filetype::Unknown,
1882 },
1883 _ => Filetype::Unknown,
1884 },
1885 _ => Filetype::Unknown,
1886 },
1887 fs_flags: fd.inner.flags,
1888 fs_rights_base: fd.inner.rights,
1889 fs_rights_inheriting: fd.inner.rights_inheriting, })
1891 }
1892
1893 pub fn prestat_fd(&self, fd: WasiFd) -> Result<Prestat, Errno> {
1894 let inode = self.get_fd_inode(fd)?;
1895 if inode.is_preopened {
1898 Ok(self.prestat_fd_inner(inode.deref()))
1899 } else {
1900 Err(Errno::Badf)
1901 }
1902 }
1903
1904 pub(crate) fn prestat_fd_inner(&self, inode_val: &InodeVal) -> Prestat {
1905 Prestat {
1906 pr_type: Preopentype::Dir,
1907 u: PrestatEnum::Dir {
1908 pr_name_len: inode_val.name.read().unwrap().len() as u32,
1910 }
1911 .untagged(),
1912 }
1913 }
1914
1915 pub(crate) fn create_inode(
1917 &self,
1918 inodes: &WasiInodes,
1919 kind: Kind,
1920 is_preopened: bool,
1921 name: String,
1922 ) -> Result<InodeGuard, Errno> {
1923 let stat = self.get_stat_for_kind(&kind)?;
1924 Ok(self.create_inode_with_stat(inodes, kind, is_preopened, name.into(), stat))
1925 }
1926
1927 pub(crate) fn create_inode_with_default_stat(
1929 &self,
1930 inodes: &WasiInodes,
1931 kind: Kind,
1932 is_preopened: bool,
1933 name: Cow<'static, str>,
1934 ) -> InodeGuard {
1935 let stat = Filestat::default();
1936 self.create_inode_with_stat(inodes, kind, is_preopened, name, stat)
1937 }
1938
1939 pub(crate) fn create_inode_with_stat(
1941 &self,
1942 inodes: &WasiInodes,
1943 kind: Kind,
1944 is_preopened: bool,
1945 name: Cow<'static, str>,
1946 mut stat: Filestat,
1947 ) -> InodeGuard {
1948 match &kind {
1949 Kind::File {
1950 handle: Some(handle),
1951 ..
1952 } => {
1953 let guard = handle.read().unwrap();
1954 stat.st_size = guard.size();
1955 }
1956 Kind::Buffer { buffer } => {
1957 stat.st_size = buffer.len() as u64;
1958 }
1959 _ => {}
1960 }
1961
1962 let inode_key: Cow<'_, str> = match &kind {
1963 Kind::File { path, .. } | Kind::Dir { path, .. } => {
1964 let path_str = path.to_string_lossy();
1965 if path_str.is_empty() {
1966 Cow::Borrowed(name.as_ref())
1967 } else {
1968 path_str
1969 }
1970 }
1971 Kind::Symlink {
1972 path_to_symlink, ..
1973 } => {
1974 let path_str = path_to_symlink.to_string_lossy();
1975 if path_str.is_empty() {
1976 Cow::Borrowed(name.as_ref())
1977 } else {
1978 path_str
1979 }
1980 }
1981 _ => Cow::Borrowed(name.as_ref()),
1982 };
1983
1984 let st_ino = Inode::from_path(&inode_key);
1985 stat.st_ino = st_ino.as_u64();
1986
1987 inodes.add_inode_val(InodeVal {
1988 stat: RwLock::new(stat),
1989 is_preopened,
1990 name: RwLock::new(name),
1991 kind: RwLock::new(kind),
1992 })
1993 }
1994
1995 fn make_fd(
1996 rights: Rights,
1997 rights_inheriting: Rights,
1998 fs_flags: Fdflags,
1999 fd_flags: Fdflagsext,
2000 open_flags: u16,
2001 inode: InodeGuard,
2002 idx: Option<WasiFd>,
2003 ) -> Fd {
2004 let is_stdio = matches!(
2005 idx,
2006 Some(__WASI_STDIN_FILENO) | Some(__WASI_STDOUT_FILENO) | Some(__WASI_STDERR_FILENO)
2007 );
2008 Fd {
2009 inner: FdInner {
2010 rights,
2011 rights_inheriting,
2012 flags: fs_flags,
2013 offset: Arc::new(AtomicU64::new(0)),
2014 fd_flags,
2015 },
2016 open_flags,
2017 inode,
2018 is_stdio,
2019 }
2020 }
2021
2022 #[allow(clippy::too_many_arguments)]
2027 pub(crate) fn insert_fd_locked(
2028 fd_map: &mut FdList,
2029 rights: Rights,
2030 rights_inheriting: Rights,
2031 fs_flags: Fdflags,
2032 fd_flags: Fdflagsext,
2033 open_flags: u16,
2034 inode: InodeGuard,
2035 idx: Option<WasiFd>,
2036 exclusive: bool,
2037 ) -> Result<WasiFd, Errno> {
2038 let fd = Self::make_fd(
2039 rights,
2040 rights_inheriting,
2041 fs_flags,
2042 fd_flags,
2043 open_flags,
2044 inode,
2045 idx,
2046 );
2047
2048 match idx {
2049 Some(idx) => {
2050 if idx > MAX_FD {
2051 return Err(Errno::Badf);
2052 }
2053 if fd_map.insert(exclusive, idx, fd) {
2054 Ok(idx)
2055 } else {
2056 Err(Errno::Exist)
2057 }
2058 }
2059 None => Ok(fd_map.insert_first_free(fd)),
2060 }
2061 }
2062
2063 pub(crate) fn clone_fd_locked(
2065 fs: &WasiFs,
2066 fd_map: &mut FdList,
2067 fd: WasiFd,
2068 min_result_fd: WasiFd,
2069 cloexec: Option<bool>,
2070 ) -> Result<WasiFd, Errno> {
2071 let fd = Self::get_fd_from_locked_map(fs, fd_map, fd)?;
2072 Self::ensure_file_handle_present(&fd)?;
2073 if min_result_fd > MAX_FD {
2074 return Err(Errno::Inval);
2075 }
2076 Ok(fd_map.insert_first_free_after(
2077 Fd {
2078 inner: FdInner {
2079 rights: fd.inner.rights,
2080 rights_inheriting: fd.inner.rights_inheriting,
2081 flags: fd.inner.flags,
2082 offset: fd.inner.offset.clone(),
2083 fd_flags: match cloexec {
2084 None => fd.inner.fd_flags,
2085 Some(cloexec) => {
2086 let mut f = fd.inner.fd_flags;
2087 f.set(Fdflagsext::CLOEXEC, cloexec);
2088 f
2089 }
2090 },
2091 },
2092 open_flags: fd.open_flags,
2093 inode: fd.inode,
2094 is_stdio: fd.is_stdio,
2095 },
2096 min_result_fd,
2097 ))
2098 }
2099
2100 pub(crate) fn get_fd_from_locked_map(
2102 fs: &WasiFs,
2103 fd_map: &FdList,
2104 fd: WasiFd,
2105 ) -> Result<Fd, Errno> {
2106 match fd_map.get(fd) {
2107 Some(fd) => Ok(fd.clone()),
2108 None if fd == VIRTUAL_ROOT_FD => Ok(Self::virtual_root_fd(fs.root_inode.clone())),
2109 None => Err(Errno::Badf),
2110 }
2111 }
2112
2113 fn virtual_root_fd(root_inode: InodeGuard) -> Fd {
2114 Fd {
2115 inner: FdInner {
2116 rights: ALL_RIGHTS,
2117 rights_inheriting: ALL_RIGHTS,
2118 flags: Fdflags::empty(),
2119 offset: Arc::new(AtomicU64::new(0)),
2120 fd_flags: Fdflagsext::empty(),
2121 },
2122 open_flags: 0,
2123 inode: root_inode,
2124 is_stdio: false,
2125 }
2126 }
2127
2128 fn ensure_file_handle_present(fd: &Fd) -> Result<(), Errno> {
2129 let guard = fd.inode.read();
2130 match guard.deref() {
2131 Kind::File { handle: None, .. } => Err(Errno::Badf),
2132 _ => Ok(()),
2133 }
2134 }
2135
2136 pub(crate) fn dup2_at(
2142 &self,
2143 src: WasiFd,
2144 dst: WasiFd,
2145 ) -> Result<Option<VirtualFileLock>, Errno> {
2146 if dst > MAX_FD {
2147 return Err(Errno::Badf);
2148 }
2149
2150 let flush_target = {
2151 let mut fd_map = self.fd_map.write().unwrap();
2152
2153 let fd_entry = fd_map.get(src).ok_or(Errno::Badf)?;
2154 Self::ensure_file_handle_present(fd_entry)?;
2155
2156 if src == dst {
2157 return Ok(None);
2158 }
2159
2160 if let Some(target_fd) = fd_map.get(dst)
2161 && !target_fd.is_stdio
2162 && target_fd.inode.is_preopened
2163 {
2164 warn!("Refusing dup2({src}, {dst}) because FD {dst} is pre-opened");
2165 return Err(Errno::Notsup);
2166 }
2167
2168 let new_fd_entry = Fd {
2169 inner: FdInner {
2170 offset: fd_entry.inner.offset.clone(),
2171 rights: fd_entry.inner.rights_inheriting,
2172 fd_flags: {
2173 let mut f = fd_entry.inner.fd_flags;
2174 f.set(Fdflagsext::CLOEXEC, false);
2175 f
2176 },
2177 ..fd_entry.inner
2178 },
2179 inode: fd_entry.inode.clone(),
2180 ..*fd_entry
2181 };
2182
2183 let flush_target = fd_map
2184 .get(dst)
2185 .and_then(|fd| Self::file_flush_target(&fd.inode));
2186
2187 fd_map.remove(dst);
2188
2189 if !fd_map.insert(true, dst, new_fd_entry) {
2190 panic!("Internal error: expected FD {dst} to be free after remove in dup2_at");
2191 }
2192
2193 flush_target
2194 };
2195
2196 Ok(flush_target)
2197 }
2198
2199 pub fn create_fd(
2200 &self,
2201 rights: Rights,
2202 rights_inheriting: Rights,
2203 fs_flags: Fdflags,
2204 fd_flags: Fdflagsext,
2205 open_flags: u16,
2206 inode: InodeGuard,
2207 ) -> Result<WasiFd, Errno> {
2208 self.create_fd_ext(
2209 rights,
2210 rights_inheriting,
2211 fs_flags,
2212 fd_flags,
2213 open_flags,
2214 inode,
2215 None,
2216 false,
2217 )
2218 }
2219
2220 #[allow(clippy::too_many_arguments)]
2221 pub fn with_fd(
2222 &self,
2223 rights: Rights,
2224 rights_inheriting: Rights,
2225 fs_flags: Fdflags,
2226 fd_flags: Fdflagsext,
2227 open_flags: u16,
2228 inode: InodeGuard,
2229 idx: WasiFd,
2230 ) -> Result<(), Errno> {
2231 self.create_fd_ext(
2232 rights,
2233 rights_inheriting,
2234 fs_flags,
2235 fd_flags,
2236 open_flags,
2237 inode,
2238 Some(idx),
2239 true,
2240 )?;
2241 Ok(())
2242 }
2243
2244 #[allow(clippy::too_many_arguments)]
2245 pub fn create_fd_ext(
2246 &self,
2247 rights: Rights,
2248 rights_inheriting: Rights,
2249 fs_flags: Fdflags,
2250 fd_flags: Fdflagsext,
2251 open_flags: u16,
2252 inode: InodeGuard,
2253 idx: Option<WasiFd>,
2254 exclusive: bool,
2255 ) -> Result<WasiFd, Errno> {
2256 let mut fd_map = self.fd_map.write().unwrap();
2257 Self::insert_fd_locked(
2258 &mut fd_map,
2259 rights,
2260 rights_inheriting,
2261 fs_flags,
2262 fd_flags,
2263 open_flags,
2264 inode,
2265 idx,
2266 exclusive,
2267 )
2268 }
2269
2270 pub fn clone_fd(&self, fd: WasiFd) -> Result<WasiFd, Errno> {
2271 self.clone_fd_ext(fd, 0, None)
2272 }
2273
2274 pub fn clone_fd_ext(
2275 &self,
2276 fd: WasiFd,
2277 min_result_fd: WasiFd,
2278 cloexec: Option<bool>,
2279 ) -> Result<WasiFd, Errno> {
2280 let mut fd_map = self.fd_map.write().unwrap();
2281 Self::clone_fd_locked(self, &mut fd_map, fd, min_result_fd, cloexec)
2282 }
2283
2284 pub unsafe fn remove_inode(&self, inodes: &WasiInodes, ino: Inode) -> Option<Arc<InodeVal>> {
2293 let mut guard = inodes.protected.write().unwrap();
2294 guard.lookup.remove(&ino).and_then(|a| Weak::upgrade(&a))
2295 }
2296
2297 pub(crate) fn create_stdout(&self, inodes: &WasiInodes) {
2298 self.create_std_dev_inner(
2299 inodes,
2300 Box::<Stdout>::default(),
2301 "stdout",
2302 __WASI_STDOUT_FILENO,
2303 STDOUT_DEFAULT_RIGHTS,
2304 Fdflags::APPEND,
2305 FS_STDOUT_INO,
2306 );
2307 }
2308
2309 pub(crate) fn create_stdin(&self, inodes: &WasiInodes) {
2310 self.create_std_dev_inner(
2311 inodes,
2312 Box::<Stdin>::default(),
2313 "stdin",
2314 __WASI_STDIN_FILENO,
2315 STDIN_DEFAULT_RIGHTS,
2316 Fdflags::empty(),
2317 FS_STDIN_INO,
2318 );
2319 }
2320
2321 pub(crate) fn create_stderr(&self, inodes: &WasiInodes) {
2322 self.create_std_dev_inner(
2323 inodes,
2324 Box::<Stderr>::default(),
2325 "stderr",
2326 __WASI_STDERR_FILENO,
2327 STDERR_DEFAULT_RIGHTS,
2328 Fdflags::APPEND,
2329 FS_STDERR_INO,
2330 );
2331 }
2332
2333 pub(crate) fn create_rootfd(&self) -> Result<(), String> {
2334 let all_rights = ALL_RIGHTS;
2336 let root_rights = all_rights
2339 ;
2355 let fd = self
2356 .create_fd(
2357 root_rights,
2358 root_rights,
2359 Fdflags::empty(),
2360 Fdflagsext::empty(),
2361 Fd::READ,
2362 self.root_inode.clone(),
2363 )
2364 .map_err(|e| format!("Could not create root fd: {e}"))?;
2365 self.preopen_fds.write().unwrap().push(fd);
2366 Ok(())
2367 }
2368
2369 pub(crate) fn create_preopens(
2370 &self,
2371 inodes: &WasiInodes,
2372 ignore_duplicates: bool,
2373 ) -> Result<(), String> {
2374 for preopen_name in self.init_vfs_preopens.iter() {
2375 let kind = Kind::Dir {
2376 parent: self.root_inode.downgrade(),
2377 path: PathBuf::from(preopen_name),
2378 entries: Default::default(),
2379 };
2380 let rights = Rights::FD_ADVISE
2381 | Rights::FD_TELL
2382 | Rights::FD_SEEK
2383 | Rights::FD_READ
2384 | Rights::PATH_OPEN
2385 | Rights::FD_READDIR
2386 | Rights::PATH_READLINK
2387 | Rights::PATH_FILESTAT_GET
2388 | Rights::FD_FILESTAT_GET
2389 | Rights::PATH_LINK_SOURCE
2390 | Rights::PATH_RENAME_SOURCE
2391 | Rights::POLL_FD_READWRITE
2392 | Rights::SOCK_SHUTDOWN;
2393 let inode = self
2394 .create_inode(inodes, kind, true, preopen_name.clone())
2395 .map_err(|e| {
2396 format!(
2397 "Failed to create inode for preopened dir (name `{preopen_name}`): WASI error code: {e}",
2398 )
2399 })?;
2400 let fd_flags = Fd::READ;
2401 let fd = self
2402 .create_fd(
2403 rights,
2404 rights,
2405 Fdflags::empty(),
2406 Fdflagsext::empty(),
2407 fd_flags,
2408 inode.clone(),
2409 )
2410 .map_err(|e| format!("Could not open fd for file {preopen_name:?}: {e}"))?;
2411 {
2412 let mut guard = self.root_inode.write();
2413 if let Kind::Root { entries } = guard.deref_mut() {
2414 let existing_entry = entries.insert(preopen_name.clone(), inode);
2415 if existing_entry.is_some() && !ignore_duplicates {
2416 return Err(format!("Found duplicate entry for alias `{preopen_name}`"));
2417 }
2418 }
2419 }
2420 self.preopen_fds.write().unwrap().push(fd);
2421 }
2422
2423 for PreopenedDir {
2424 path,
2425 alias,
2426 read,
2427 write,
2428 create,
2429 } in self.init_preopens.iter()
2430 {
2431 debug!(
2432 "Attempting to preopen {} with alias {:?}",
2433 &path.to_string_lossy(),
2434 &alias
2435 );
2436 let cur_dir_metadata = self
2437 .root_fs
2438 .metadata(path)
2439 .map_err(|e| format!("Could not get metadata for file {path:?}: {e}"))?;
2440
2441 let kind = if cur_dir_metadata.is_dir() {
2442 Kind::Dir {
2443 parent: self.root_inode.downgrade(),
2444 path: path.clone(),
2445 entries: Default::default(),
2446 }
2447 } else {
2448 return Err(format!(
2449 "WASI only supports pre-opened directories right now; found \"{}\"",
2450 path.to_string_lossy()
2451 ));
2452 };
2453
2454 let rights = {
2455 let mut rights = Rights::FD_ADVISE | Rights::FD_TELL | Rights::FD_SEEK;
2457 if *read {
2458 rights |= Rights::FD_READ
2459 | Rights::PATH_OPEN
2460 | Rights::FD_READDIR
2461 | Rights::PATH_READLINK
2462 | Rights::PATH_FILESTAT_GET
2463 | Rights::FD_FILESTAT_GET
2464 | Rights::PATH_LINK_SOURCE
2465 | Rights::PATH_RENAME_SOURCE
2466 | Rights::POLL_FD_READWRITE
2467 | Rights::SOCK_SHUTDOWN;
2468 }
2469 if *write {
2470 rights |= Rights::FD_DATASYNC
2471 | Rights::FD_FDSTAT_SET_FLAGS
2472 | Rights::FD_WRITE
2473 | Rights::FD_SYNC
2474 | Rights::FD_ALLOCATE
2475 | Rights::PATH_OPEN
2476 | Rights::PATH_RENAME_TARGET
2477 | Rights::PATH_FILESTAT_SET_SIZE
2478 | Rights::PATH_FILESTAT_SET_TIMES
2479 | Rights::FD_FILESTAT_SET_SIZE
2480 | Rights::FD_FILESTAT_SET_TIMES
2481 | Rights::PATH_REMOVE_DIRECTORY
2482 | Rights::PATH_UNLINK_FILE
2483 | Rights::POLL_FD_READWRITE
2484 | Rights::SOCK_SHUTDOWN;
2485 }
2486 if *create {
2487 rights |= Rights::PATH_CREATE_DIRECTORY
2488 | Rights::PATH_CREATE_FILE
2489 | Rights::PATH_LINK_TARGET
2490 | Rights::PATH_OPEN
2491 | Rights::PATH_RENAME_TARGET
2492 | Rights::PATH_SYMLINK;
2493 }
2494
2495 rights
2496 };
2497 let inode = if let Some(alias) = &alias {
2498 self.create_inode(inodes, kind, true, alias.clone())
2499 } else {
2500 self.create_inode(inodes, kind, true, path.to_string_lossy().into_owned())
2501 }
2502 .map_err(|e| {
2503 format!("Failed to create inode for preopened dir: WASI error code: {e}")
2504 })?;
2505 let fd_flags = {
2506 let mut fd_flags = 0;
2507 if *read {
2508 fd_flags |= Fd::READ;
2509 }
2510 if *write {
2511 fd_flags |= Fd::WRITE | Fd::APPEND | Fd::TRUNCATE;
2513 }
2514 if *create {
2515 fd_flags |= Fd::CREATE;
2516 }
2517 fd_flags
2518 };
2519 let fd = self
2520 .create_fd(
2521 rights,
2522 rights,
2523 Fdflags::empty(),
2524 Fdflagsext::empty(),
2525 fd_flags,
2526 inode.clone(),
2527 )
2528 .map_err(|e| format!("Could not open fd for file {path:?}: {e}"))?;
2529 {
2530 let mut guard = self.root_inode.write();
2531 if let Kind::Root { entries } = guard.deref_mut() {
2532 let key = if let Some(alias) = &alias {
2533 alias.clone()
2534 } else {
2535 path.to_string_lossy().into_owned()
2536 };
2537 let existing_entry = entries.insert(key.clone(), inode);
2538 if existing_entry.is_some() && !ignore_duplicates {
2539 return Err(format!("Found duplicate entry for alias `{key}`"));
2540 }
2541 }
2542 }
2543 self.preopen_fds.write().unwrap().push(fd);
2544 }
2545
2546 Ok(())
2547 }
2548
2549 #[allow(clippy::too_many_arguments)]
2550 pub(crate) fn create_std_dev_inner(
2551 &self,
2552 inodes: &WasiInodes,
2553 handle: Box<dyn VirtualFile + Send + Sync + 'static>,
2554 name: &'static str,
2555 raw_fd: WasiFd,
2556 rights: Rights,
2557 fd_flags: Fdflags,
2558 st_ino: Inode,
2559 ) {
2560 let inode = {
2561 let stat = Filestat {
2562 st_filetype: Filetype::CharacterDevice,
2563 st_ino: st_ino.as_u64(),
2564 ..Filestat::default()
2565 };
2566 let kind = Kind::File {
2567 fd: Some(raw_fd),
2568 handle: Some(Arc::new(RwLock::new(handle))),
2569 path: "".into(),
2570 };
2571 inodes.add_inode_val(InodeVal {
2572 stat: RwLock::new(stat),
2573 is_preopened: true,
2574 name: RwLock::new(name.to_string().into()),
2575 kind: RwLock::new(kind),
2576 })
2577 };
2578 self.fd_map.write().unwrap().insert(
2579 false,
2580 raw_fd,
2581 Fd {
2582 inner: FdInner {
2583 rights,
2584 rights_inheriting: Rights::empty(),
2585 flags: fd_flags,
2586 offset: Arc::new(AtomicU64::new(0)),
2587 fd_flags: Fdflagsext::empty(),
2588 },
2589 open_flags: 0,
2591 inode,
2592 is_stdio: true,
2593 },
2594 );
2595 }
2596
2597 pub fn get_stat_for_kind(&self, kind: &Kind) -> Result<Filestat, Errno> {
2598 let md = match kind {
2599 Kind::File { handle, path, .. } => match handle {
2600 Some(wf) => {
2601 let wf = wf.read().unwrap();
2602 return Ok(Filestat {
2603 st_filetype: Filetype::RegularFile,
2604 st_ino: Inode::from_path(path.to_string_lossy().as_ref()).as_u64(),
2605 st_size: wf.size(),
2606 st_atim: wf.last_accessed(),
2607 st_mtim: wf.last_modified(),
2608 st_ctim: wf.created_time(),
2609
2610 ..Filestat::default()
2611 });
2612 }
2613 None => self
2614 .root_fs
2615 .metadata(path)
2616 .map_err(fs_error_into_wasi_err)?,
2617 },
2618 Kind::Dir { path, .. } => self
2619 .root_fs
2620 .metadata(path)
2621 .map_err(fs_error_into_wasi_err)?,
2622 Kind::Symlink {
2623 path_to_symlink,
2624 relative_path,
2625 ..
2626 } => {
2627 let symlink_path = PosixPath::new("/")
2628 .join(&PosixPath::from_path(path_to_symlink))
2629 .into_path_buf();
2630
2631 match self.root_fs.symlink_metadata(&symlink_path) {
2632 Ok(md) => md,
2633 Err(FsError::EntryNotFound)
2634 if self.ephemeral_symlink_at(&symlink_path).is_some() =>
2635 {
2636 return Ok(Filestat {
2637 st_filetype: Filetype::SymbolicLink,
2638 st_size: relative_path.as_os_str().len() as u64,
2639 ..Filestat::default()
2640 });
2641 }
2642 Err(err) => return Err(fs_error_into_wasi_err(err)),
2643 }
2644 }
2645 _ => return Err(Errno::Io),
2646 };
2647 Ok(Filestat {
2648 st_filetype: virtual_file_type_to_wasi_file_type(md.file_type()),
2649 st_size: md.len(),
2650 st_atim: md.accessed(),
2651 st_mtim: md.modified(),
2652 st_ctim: md.created(),
2653 ..Filestat::default()
2654 })
2655 }
2656
2657 pub(crate) fn close_fd_and_capture_flush(&self, fd: WasiFd) -> CloseFdOutcome {
2662 let mut fd_map = self.fd_map.write().unwrap();
2663 Self::close_fd_locked(&mut fd_map, fd)
2664 }
2665
2666 fn close_fd_locked(fd_map: &mut FdList, fd: WasiFd) -> CloseFdOutcome {
2668 let Some(fd_ref) = fd_map.get(fd) else {
2669 trace!(%fd, "closing file descriptor failed - {}", Errno::Badf);
2670 return CloseFdOutcome::not_found();
2671 };
2672
2673 if !fd_ref.is_stdio && fd_ref.inode.is_preopened {
2674 return CloseFdOutcome {
2675 skipped_preopen: true,
2676 removed: false,
2677 flush_target: None,
2678 };
2679 }
2680
2681 let flush_target = Self::file_flush_target(&fd_ref.inode);
2682
2683 match fd_map.remove(fd) {
2684 Some(fd_ref) => {
2685 let inode = fd_ref.inode.ino().as_u64();
2686 let ref_cnt = fd_ref.inode.ref_cnt();
2687 if ref_cnt == 1 {
2688 trace!(%fd, %inode, %ref_cnt, "closing file descriptor");
2689 } else {
2690 trace!(%fd, %inode, %ref_cnt, "weakening file descriptor");
2691 }
2692 }
2693 None => {
2694 trace!(%fd, "closing file descriptor failed - {}", Errno::Badf);
2695 return CloseFdOutcome::not_found();
2696 }
2697 }
2698
2699 CloseFdOutcome {
2700 skipped_preopen: false,
2701 removed: true,
2702 flush_target,
2703 }
2704 }
2705
2706 pub(crate) async fn flush_file_best_effort(file: VirtualFileLock) {
2707 let result = FlushPoller { file }.await;
2708 match result {
2709 Ok(())
2710 | Err(Errno::Isdir)
2711 | Err(Errno::Io)
2712 | Err(Errno::Access)
2713 | Err(Errno::Inval) => {}
2715 Err(err) => trace!("flush during bulk close failed - {}", err),
2716 }
2717 }
2718
2719 fn file_flush_target(inode: &InodeGuard) -> Option<VirtualFileLock> {
2720 let guard = inode.read();
2721 match guard.deref() {
2722 Kind::File {
2723 handle: Some(file), ..
2724 } => Some(file.clone()),
2725 _ => None,
2726 }
2727 }
2728
2729 pub(crate) fn close_fd(&self, fd: WasiFd) -> Result<(), Errno> {
2731 let _ = self.close_fd_and_capture_flush(fd);
2732 Ok(())
2733 }
2734}
2735
2736impl std::fmt::Debug for WasiFs {
2737 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2738 if let Ok(guard) = self.current_dir.try_lock() {
2739 write!(f, "current_dir={} ", guard.as_str())?;
2740 } else {
2741 write!(f, "current_dir=(locked) ")?;
2742 }
2743 if let Ok(guard) = self.fd_map.read() {
2744 write!(
2745 f,
2746 "next_fd={} max_fd={:?} ",
2747 guard.next_free_fd(),
2748 guard.last_fd()
2749 )?;
2750 } else {
2751 write!(f, "next_fd=(locked) max_fd=(locked) ")?;
2752 }
2753 write!(f, "{:?}", self.root_fs)
2754 }
2755}
2756
2757pub fn default_fs_backing() -> Arc<dyn virtual_fs::FileSystem + Send + Sync> {
2759 cfg_if::cfg_if! {
2760 if #[cfg(feature = "host-fs")] {
2761 Arc::new(virtual_fs::host_fs::FileSystem::new(tokio::runtime::Handle::current(), "/").unwrap())
2762 } else if #[cfg(not(feature = "host-fs"))] {
2763 Arc::<virtual_fs::mem_fs::FileSystem>::default()
2764 } else {
2765 Arc::<FallbackFileSystem>::default()
2766 }
2767 }
2768}
2769
2770#[derive(Debug, Default)]
2771pub struct FallbackFileSystem;
2772
2773impl FallbackFileSystem {
2774 fn fail() -> ! {
2775 panic!(
2776 "No filesystem set for wasmer-wasi, please enable either the `host-fs` or `mem-fs` feature or set your custom filesystem with `WasiEnvBuilder::set_fs`"
2777 );
2778 }
2779}
2780
2781impl FileSystem for FallbackFileSystem {
2782 fn readlink(&self, _path: &Path) -> virtual_fs::Result<PathBuf> {
2783 Self::fail()
2784 }
2785 fn read_dir(&self, _path: &Path) -> Result<virtual_fs::ReadDir, FsError> {
2786 Self::fail();
2787 }
2788 fn create_dir(&self, _path: &Path) -> Result<(), FsError> {
2789 Self::fail();
2790 }
2791 fn remove_dir(&self, _path: &Path) -> Result<(), FsError> {
2792 Self::fail();
2793 }
2794 fn rename<'a>(&'a self, _from: &Path, _to: &Path) -> BoxFuture<'a, Result<(), FsError>> {
2795 Self::fail();
2796 }
2797 fn metadata(&self, _path: &Path) -> Result<virtual_fs::Metadata, FsError> {
2798 Self::fail();
2799 }
2800 fn symlink_metadata(&self, _path: &Path) -> Result<virtual_fs::Metadata, FsError> {
2801 Self::fail();
2802 }
2803 fn remove_file(&self, _path: &Path) -> Result<(), FsError> {
2804 Self::fail();
2805 }
2806 fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
2807 Self::fail();
2808 }
2809}
2810
2811pub fn virtual_file_type_to_wasi_file_type(file_type: virtual_fs::FileType) -> Filetype {
2812 if file_type.is_dir() {
2814 Filetype::Directory
2815 } else if file_type.is_file() {
2816 Filetype::RegularFile
2817 } else if file_type.is_symlink() {
2818 Filetype::SymbolicLink
2819 } else {
2820 Filetype::Unknown
2821 }
2822}
2823
2824pub fn fs_error_from_wasi_err(err: Errno) -> FsError {
2825 match err {
2826 Errno::Badf => FsError::InvalidFd,
2827 Errno::Exist => FsError::AlreadyExists,
2828 Errno::Io => FsError::IOError,
2829 Errno::Addrinuse => FsError::AddressInUse,
2830 Errno::Addrnotavail => FsError::AddressNotAvailable,
2831 Errno::Pipe => FsError::BrokenPipe,
2832 Errno::Connaborted => FsError::ConnectionAborted,
2833 Errno::Connrefused => FsError::ConnectionRefused,
2834 Errno::Connreset => FsError::ConnectionReset,
2835 Errno::Intr => FsError::Interrupted,
2836 Errno::Inval => FsError::InvalidInput,
2837 Errno::Notconn => FsError::NotConnected,
2838 Errno::Nodev => FsError::NoDevice,
2839 Errno::Noent => FsError::EntryNotFound,
2840 Errno::Perm => FsError::PermissionDenied,
2841 Errno::Timedout => FsError::TimedOut,
2842 Errno::Proto => FsError::UnexpectedEof,
2843 Errno::Again => FsError::WouldBlock,
2844 Errno::Nospc => FsError::WriteZero,
2845 Errno::Notempty => FsError::DirectoryNotEmpty,
2846 _ => FsError::UnknownError,
2847 }
2848}
2849
2850pub fn fs_error_into_wasi_err(fs_error: FsError) -> Errno {
2851 match fs_error {
2852 FsError::AlreadyExists => Errno::Exist,
2853 FsError::AddressInUse => Errno::Addrinuse,
2854 FsError::AddressNotAvailable => Errno::Addrnotavail,
2855 FsError::BaseNotDirectory => Errno::Notdir,
2856 FsError::BrokenPipe => Errno::Pipe,
2857 FsError::ConnectionAborted => Errno::Connaborted,
2858 FsError::ConnectionRefused => Errno::Connrefused,
2859 FsError::ConnectionReset => Errno::Connreset,
2860 FsError::Interrupted => Errno::Intr,
2861 FsError::InvalidData => Errno::Io,
2862 FsError::InvalidFd => Errno::Badf,
2863 FsError::InvalidInput => Errno::Inval,
2864 FsError::IOError => Errno::Io,
2865 FsError::NoDevice => Errno::Nodev,
2866 FsError::NotAFile => Errno::Inval,
2867 FsError::NotConnected => Errno::Notconn,
2868 FsError::EntryNotFound => Errno::Noent,
2869 FsError::PermissionDenied => Errno::Perm,
2870 FsError::TimedOut => Errno::Timedout,
2871 FsError::UnexpectedEof => Errno::Proto,
2872 FsError::WouldBlock => Errno::Again,
2873 FsError::WriteZero => Errno::Nospc,
2874 FsError::DirectoryNotEmpty => Errno::Notempty,
2875 FsError::StorageFull => Errno::Overflow,
2876 FsError::Lock | FsError::UnknownError => Errno::Io,
2877 FsError::Unsupported => Errno::Notsup,
2878 }
2879}
2880
2881#[cfg(test)]
2882mod tests {
2883 use super::*;
2884 use once_cell::sync::OnceCell;
2885 use tempfile::tempdir;
2886 use virtual_fs::{RootFileSystemBuilder, TmpFileSystem};
2887 use wasmer::Engine;
2888 use wasmer_config::package::PackageId;
2889
2890 use crate::WasiEnvBuilder;
2891 use crate::bin_factory::{BinaryPackage, BinaryPackageMount, BinaryPackageMounts};
2892
2893 fn webc_symlink_fs() -> virtual_fs::WebcVolumeFileSystem {
2894 let timestamps = webc::v3::Timestamps::default();
2895 let dir = webc::v3::write::Directory::new(
2896 std::collections::BTreeMap::from_iter([
2897 (
2898 webc::PathSegment::parse("target.txt").unwrap(),
2899 webc::v3::write::DirEntry::File(webc::v3::write::FileEntry::borrowed(
2900 b"target", timestamps,
2901 )),
2902 ),
2903 (
2904 webc::PathSegment::parse("link").unwrap(),
2905 webc::v3::write::DirEntry::Symlink(webc::v3::write::SymlinkEntry::borrowed(
2906 "target.txt",
2907 timestamps,
2908 )),
2909 ),
2910 ]),
2911 timestamps,
2912 );
2913 let manifest = webc::metadata::Manifest::default();
2914 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
2915 .write_manifest(&manifest)
2916 .unwrap()
2917 .write_atoms(std::collections::BTreeMap::new())
2918 .unwrap();
2919 writer.write_volume("atom", dir).unwrap();
2920 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
2921 let container = wasmer_package::utils::from_bytes(webc).unwrap();
2922 let volume = container.volumes()["atom"].clone();
2923
2924 virtual_fs::WebcVolumeFileSystem::new(volume)
2925 }
2926
2927 #[tokio::test]
2928 async fn test_relative_path_to_absolute() {
2929 let inodes = WasiInodes::new();
2930 let fs_backing =
2931 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
2932 let wasi_fs = WasiFs::new_init(fs_backing, &inodes, FS_ROOT_INO).unwrap();
2933
2934 assert_eq!(
2936 wasi_fs.relative_path_to_absolute("/foo/bar".to_string()),
2937 "/foo/bar"
2938 );
2939 assert_eq!(wasi_fs.relative_path_to_absolute("/".to_string()), "/");
2940
2941 assert_eq!(
2943 wasi_fs.relative_path_to_absolute("//foo//bar//".to_string()),
2944 "//foo//bar//"
2945 );
2946 assert_eq!(
2947 wasi_fs.relative_path_to_absolute("/a/b/./c".to_string()),
2948 "/a/b/./c"
2949 );
2950 assert_eq!(
2951 wasi_fs.relative_path_to_absolute("/a/b/../c".to_string()),
2952 "/a/b/../c"
2953 );
2954
2955 assert_eq!(
2957 wasi_fs.relative_path_to_absolute("foo/bar".to_string()),
2958 "/foo/bar"
2959 );
2960 assert_eq!(wasi_fs.relative_path_to_absolute("foo".to_string()), "/foo");
2961
2962 wasi_fs.set_current_dir("/home/user");
2964 assert_eq!(
2965 wasi_fs.relative_path_to_absolute("file.txt".to_string()),
2966 "/home/user/file.txt"
2967 );
2968 assert_eq!(
2969 wasi_fs.relative_path_to_absolute("dir/file.txt".to_string()),
2970 "/home/user/dir/file.txt"
2971 );
2972
2973 wasi_fs.set_current_dir("/a/b/c");
2975 assert_eq!(
2976 wasi_fs.relative_path_to_absolute("./file.txt".to_string()),
2977 "/a/b/c/./file.txt"
2978 );
2979 assert_eq!(
2980 wasi_fs.relative_path_to_absolute("../file.txt".to_string()),
2981 "/a/b/c/../file.txt"
2982 );
2983 assert_eq!(
2984 wasi_fs.relative_path_to_absolute("../../file.txt".to_string()),
2985 "/a/b/c/../../file.txt"
2986 );
2987
2988 assert_eq!(
2990 wasi_fs.relative_path_to_absolute(".".to_string()),
2991 "/a/b/c/."
2992 );
2993 assert_eq!(
2994 wasi_fs.relative_path_to_absolute("..".to_string()),
2995 "/a/b/c/.."
2996 );
2997 assert_eq!(wasi_fs.relative_path_to_absolute("".to_string()), "/a/b/c/");
2998
2999 wasi_fs.set_current_dir("/home/user/");
3001 assert_eq!(
3002 wasi_fs.relative_path_to_absolute("file.txt".to_string()),
3003 "/home/user/file.txt"
3004 );
3005
3006 wasi_fs.set_current_dir("/home/user");
3008 assert_eq!(
3009 wasi_fs.relative_path_to_absolute("file.txt".to_string()),
3010 "/home/user/file.txt"
3011 );
3012 }
3013
3014 #[cfg(feature = "host-fs")]
3015 #[tokio::test]
3016 async fn mapped_preopen_inode_paths_should_stay_in_guest_space() {
3017 let root_dir = tempdir().unwrap();
3018 let hamlet_dir = root_dir.path().join("hamlet");
3019 std::fs::create_dir_all(&hamlet_dir).unwrap();
3020
3021 let host_fs = virtual_fs::host_fs::FileSystem::new(
3022 tokio::runtime::Handle::current(),
3023 root_dir.path(),
3024 )
3025 .unwrap();
3026
3027 let init = WasiEnvBuilder::new("test_prog")
3028 .engine(Engine::default())
3029 .fs(Arc::new(host_fs) as Arc<dyn FileSystem + Send + Sync>)
3030 .map_dir("hamlet", "/hamlet")
3031 .unwrap()
3032 .build_init()
3033 .unwrap();
3034
3035 let preopen_inode = {
3036 let guard = init.state.fs.root_inode.read();
3037 let Kind::Root { entries } = guard.deref() else {
3038 panic!("expected root inode");
3039 };
3040 entries.get("hamlet").unwrap().clone()
3041 };
3042 let guard = preopen_inode.read();
3043
3044 let Kind::Dir { path, .. } = guard.deref() else {
3045 panic!("expected preopen inode to be a directory");
3046 };
3047
3048 assert_eq!(path, std::path::Path::new("/hamlet"));
3049 }
3050
3051 #[cfg(all(unix, feature = "host-fs", feature = "sys"))]
3052 #[tokio::test]
3053 async fn backing_absolute_host_symlink_targets_stay_within_guest_mount() {
3054 let root_dir = tempfile::Builder::new()
3055 .prefix("wasix-backing-symlink")
3056 .tempdir_in("/tmp")
3057 .unwrap();
3058 let dir1 = root_dir.path().join("dir1");
3059 let dir2 = root_dir.path().join("dir2");
3060 std::fs::create_dir_all(&dir1).unwrap();
3061 std::fs::write(dir1.join("file1"), b"hello").unwrap();
3062 std::os::unix::fs::symlink(&dir1, &dir2).unwrap();
3063
3064 let host_fs = virtual_fs::host_fs::FileSystem::new(
3065 tokio::runtime::Handle::current(),
3066 root_dir.path(),
3067 )
3068 .unwrap();
3069 let mount_fs = virtual_fs::MountFileSystem::new();
3070 mount_fs
3071 .mount(
3072 Path::new("/"),
3073 Arc::new(RootFileSystemBuilder::default().build_tmp()),
3074 )
3075 .unwrap();
3076 mount_fs
3077 .mount(
3078 Path::new("/host"),
3079 Arc::new(host_fs) as Arc<dyn FileSystem + Send + Sync>,
3080 )
3081 .unwrap();
3082
3083 let inodes = WasiInodes::new();
3084 let fs_backing = WasiFsRoot::from_mount_fs(mount_fs);
3085 let wasi_fs =
3086 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3087
3088 let literal_link = wasi_fs
3089 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2", false)
3090 .unwrap();
3091 assert!(matches!(
3092 literal_link.read().deref(),
3093 Kind::Symlink {
3094 symlink_kind: SymlinkKind::Backing,
3095 relative_path,
3096 ..
3097 } if relative_path == Path::new("/dir1")
3098 ));
3099
3100 let followed_dir = wasi_fs
3101 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2", true)
3102 .unwrap();
3103 let followed_dir_path = {
3104 let guard = followed_dir.read();
3105 let Kind::Dir { path, .. } = guard.deref() else {
3106 panic!("expected followed backing symlink to resolve to a directory");
3107 };
3108 assert_eq!(path, Path::new("/host/dir1"));
3109 path.clone()
3110 };
3111 let mut entries = wasi_fs.root_fs.read_dir(&followed_dir_path).unwrap();
3112 assert!(entries.any(|entry| entry.unwrap().path() == Path::new("/host/dir1/file1")));
3113
3114 let child = wasi_fs
3115 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2/file1", true)
3116 .unwrap();
3117 assert!(matches!(
3118 child.read().deref(),
3119 Kind::File { path, .. } if path == Path::new("/host/dir1/file1")
3120 ));
3121 }
3122
3123 #[tokio::test]
3124 async fn dot_mapped_preopen_uses_guest_current_dir() {
3125 let init = WasiEnvBuilder::new("test_prog")
3126 .engine(Engine::default())
3127 .current_dir("/work")
3128 .map_dir(".", "/work")
3129 .unwrap()
3130 .build_init()
3131 .unwrap();
3132
3133 let preopen_inode = {
3134 let guard = init.state.fs.root_inode.read();
3135 let Kind::Root { entries } = guard.deref() else {
3136 panic!("expected root inode");
3137 };
3138 entries.get(".").unwrap().clone()
3139 };
3140 let guard = preopen_inode.read();
3141
3142 let Kind::Dir { path, .. } = guard.deref() else {
3143 panic!("expected preopen inode to be a directory");
3144 };
3145
3146 assert_eq!(path, std::path::Path::new("/work"));
3147 }
3148
3149 #[tokio::test]
3150 async fn symlinked_directory_components_resolve_to_target_entries() {
3151 let inodes = WasiInodes::new();
3152 let fs_backing =
3153 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3154 let wasi_fs =
3155 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3156 let root = &wasi_fs.root_fs;
3157
3158 root.create_dir(Path::new("/orig")).unwrap();
3159 root.new_open_options()
3160 .create(true)
3161 .write(true)
3162 .open(Path::new("/orig/child.txt"))
3163 .unwrap();
3164 root.create_symlink(Path::new("/orig"), Path::new("/linked"))
3165 .unwrap();
3166
3167 let literal_link = wasi_fs
3168 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked", false)
3169 .unwrap();
3170 assert!(matches!(
3171 literal_link.read().deref(),
3172 Kind::Symlink {
3173 relative_path,
3174 ..
3175 } if relative_path == Path::new("/orig")
3176 ));
3177
3178 let followed_dir = wasi_fs
3179 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked", true)
3180 .unwrap();
3181 assert!(matches!(
3182 followed_dir.read().deref(),
3183 Kind::Dir { path, .. } if path == Path::new("/orig")
3184 ));
3185
3186 let child = wasi_fs
3187 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked/child.txt", true)
3188 .unwrap();
3189 assert!(matches!(
3190 child.read().deref(),
3191 Kind::File { path, .. } if path == Path::new("/orig/child.txt")
3192 ));
3193
3194 let child_without_final_follow = wasi_fs
3195 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked/child.txt", false)
3196 .unwrap();
3197 assert!(matches!(
3198 child_without_final_follow.read().deref(),
3199 Kind::File { path, .. } if path == Path::new("/orig/child.txt")
3200 ));
3201 }
3202
3203 #[tokio::test]
3204 async fn webc_backing_symlink_resolves_to_target_entry() {
3205 let inodes = WasiInodes::new();
3206 let fs_backing = WasiFsRoot::from_filesystem(Arc::new(webc_symlink_fs()));
3207 let wasi_fs =
3208 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3209
3210 let literal_link = wasi_fs
3211 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/link", false)
3212 .unwrap();
3213 assert!(matches!(
3214 literal_link.read().deref(),
3215 Kind::Symlink {
3216 relative_path,
3217 ..
3218 } if relative_path == Path::new("target.txt")
3219 ));
3220
3221 let followed_file = wasi_fs
3222 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/link", true)
3223 .unwrap();
3224 assert!(matches!(
3225 followed_file.read().deref(),
3226 Kind::File { path, .. } if path == Path::new("/target.txt")
3227 ));
3228 }
3229
3230 #[tokio::test]
3231 async fn path_resolution_preserves_posix_directory_component_rules() {
3232 let inodes = WasiInodes::new();
3233 let fs_backing =
3234 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3235 let wasi_fs =
3236 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3237 let root = &wasi_fs.root_fs;
3238
3239 root.create_dir(Path::new("/dir")).unwrap();
3240 root.new_open_options()
3241 .create(true)
3242 .write(true)
3243 .open(Path::new("/file"))
3244 .unwrap();
3245 root.create_symlink(Path::new("/dir"), Path::new("/dir-link"))
3246 .unwrap();
3247 root.create_symlink(Path::new("/file"), Path::new("/file-link"))
3248 .unwrap();
3249
3250 let empty_path = wasi_fs
3251 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "", true)
3252 .unwrap_err();
3253 assert_eq!(empty_path, Errno::Noent);
3254
3255 let (single_component_parent, single_component_name) = wasi_fs
3256 .get_parent_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, Path::new("new-file"), true)
3257 .unwrap();
3258 assert_eq!(single_component_name, "new-file");
3259 assert!(matches!(
3260 single_component_parent.read().deref(),
3261 Kind::Root { .. }
3262 ));
3263
3264 let root_parent = wasi_fs
3265 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/..", true)
3266 .unwrap();
3267 assert!(matches!(root_parent.read().deref(), Kind::Root { .. }));
3268
3269 let escaped_symlink_target = wasi_fs
3270 .resolve_symlink_target_path(
3271 SymlinkKind::Virtual,
3272 Path::new("fs_sandbox_symlink.dir/link"),
3273 Path::new("../../README.md"),
3274 )
3275 .unwrap_err();
3276 assert_eq!(escaped_symlink_target, Errno::Perm);
3277
3278 let (_, contained_symlink_target) = wasi_fs
3279 .resolve_symlink_target_path(
3280 SymlinkKind::Virtual,
3281 Path::new("fs_sandbox_symlink.dir/link"),
3282 Path::new("../README.md"),
3283 )
3284 .unwrap();
3285 assert_eq!(contained_symlink_target, Path::new("README.md"));
3286
3287 let (_, sibling_preopen_symlink_target) = wasi_fs
3288 .resolve_symlink_target_path(
3289 SymlinkKind::Virtual,
3290 Path::new("temp/act3"),
3291 Path::new("../hamlet/act3"),
3292 )
3293 .unwrap();
3294 assert_eq!(sibling_preopen_symlink_target, Path::new("hamlet/act3"));
3295
3296 let escaped_sibling_preopen_symlink_target = wasi_fs
3297 .resolve_symlink_target_path(
3298 SymlinkKind::Virtual,
3299 Path::new("temp/act3"),
3300 Path::new("../../outside"),
3301 )
3302 .unwrap_err();
3303 assert_eq!(escaped_sibling_preopen_symlink_target, Errno::Perm);
3304
3305 root.create_dir(Path::new("/outerdir")).unwrap();
3306 root.create_dir(Path::new("/outerdir/dest")).unwrap();
3307 root.new_open_options()
3308 .create(true)
3309 .write(true)
3310 .open(Path::new("/outerdir/evil"))
3311 .unwrap();
3312 let dest_dir = wasi_fs
3313 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/outerdir/dest", true)
3314 .unwrap();
3315 let current_link = wasi_fs.create_inode_with_default_stat(
3316 &inodes,
3317 Kind::Symlink {
3318 symlink_kind: SymlinkKind::Virtual,
3319 path_to_symlink: PathBuf::from("outerdir/dest/current"),
3320 relative_path: PathBuf::from("."),
3321 },
3322 false,
3323 Cow::Borrowed("current"),
3324 );
3325 let parent_link = wasi_fs.create_inode_with_default_stat(
3326 &inodes,
3327 Kind::Symlink {
3328 symlink_kind: SymlinkKind::Virtual,
3329 path_to_symlink: PathBuf::from("outerdir/dest/parent"),
3330 relative_path: PathBuf::from("current/.."),
3331 },
3332 false,
3333 Cow::Borrowed("parent"),
3334 );
3335 {
3336 let mut guard = dest_dir.write();
3337 let Kind::Dir { entries, .. } = guard.deref_mut() else {
3338 panic!("expected destination to be a directory");
3339 };
3340 entries.insert("current".to_string(), current_link);
3341 entries.insert("parent".to_string(), parent_link);
3342 }
3343
3344 let parent_symlink_target = wasi_fs
3345 .get_inode_at_path(
3346 &inodes,
3347 crate::VIRTUAL_ROOT_FD,
3348 "/outerdir/dest/parent/evil",
3349 true,
3350 )
3351 .unwrap();
3352 assert!(matches!(
3353 parent_symlink_target.read().deref(),
3354 Kind::File { path, .. } if path == Path::new("/outerdir/evil")
3355 ));
3356
3357 let file_dot = wasi_fs
3358 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file/.", true)
3359 .unwrap_err();
3360 assert_eq!(file_dot, Errno::Notdir);
3361
3362 let file_slash = wasi_fs
3363 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file/", true)
3364 .unwrap_err();
3365 assert_eq!(file_slash, Errno::Notdir);
3366
3367 let symlinked_dir_slash = wasi_fs
3368 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/dir-link/", false)
3369 .unwrap();
3370 assert!(matches!(
3371 symlinked_dir_slash.read().deref(),
3372 Kind::Dir { path, .. } if path == Path::new("/dir")
3373 ));
3374
3375 let symlinked_file_slash = wasi_fs
3376 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file-link/", false)
3377 .unwrap_err();
3378 assert_eq!(symlinked_file_slash, Errno::Notdir);
3379
3380 root.create_symlink(Path::new("/loop"), Path::new("/loop"))
3381 .unwrap();
3382 let symlink_loop = wasi_fs
3383 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/loop", true)
3384 .unwrap_err();
3385 assert_eq!(symlink_loop, Errno::Loop);
3386 }
3387
3388 #[tokio::test]
3389 async fn writable_root_is_preserved_through_root_overlays() {
3390 let base_root = Arc::new(RootFileSystemBuilder::default().build_tmp());
3391 let root = WasiFsRoot::from_filesystem(base_root);
3392 assert!(root.writable_root().is_some());
3393
3394 let lower = Arc::new(TmpFileSystem::new()) as Arc<dyn FileSystem + Send + Sync>;
3395 root.stack_root_filesystem(lower).unwrap();
3396
3397 assert!(root.writable_root().is_some());
3398 }
3399
3400 #[tokio::test]
3401 async fn conditional_union_merges_root_and_non_root_package_mounts_once() {
3402 let inodes = WasiInodes::new();
3403 let fs_backing =
3404 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3405 let wasi_fs = WasiFs::new_init(fs_backing, &inodes, FS_ROOT_INO).unwrap();
3406
3407 let root_layer = TmpFileSystem::new();
3408 root_layer
3409 .new_open_options()
3410 .create(true)
3411 .write(true)
3412 .open(Path::new("/root.txt"))
3413 .unwrap();
3414
3415 let public_mount = TmpFileSystem::new();
3416 public_mount
3417 .new_open_options()
3418 .create(true)
3419 .write(true)
3420 .open(Path::new("/index.html"))
3421 .unwrap();
3422
3423 let pkg = BinaryPackage {
3424 id: PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap()),
3425 package_ids: vec![],
3426 when_cached: None,
3427 entrypoint_cmd: None,
3428 hash: OnceCell::new(),
3429 package_mounts: Some(Arc::new(BinaryPackageMounts {
3430 root_layer: Some(Arc::new(root_layer)),
3431 mounts: vec![BinaryPackageMount {
3432 guest_path: PathBuf::from("/public"),
3433 fs: Arc::new(public_mount),
3434 source_path: PathBuf::from("/"),
3435 }],
3436 })),
3437 commands: vec![],
3438 uses: vec![],
3439 file_system_memory_footprint: 0,
3440 additional_host_mapped_directories: vec![],
3441 };
3442
3443 wasi_fs.conditional_union(&pkg).await.unwrap();
3444 assert!(
3445 wasi_fs
3446 .root_fs
3447 .metadata(Path::new("/root.txt"))
3448 .unwrap()
3449 .is_file()
3450 );
3451 assert!(
3452 wasi_fs
3453 .root_fs
3454 .metadata(Path::new("/public/index.html"))
3455 .unwrap()
3456 .is_file()
3457 );
3458
3459 wasi_fs.conditional_union(&pkg).await.unwrap();
3460 assert!(
3461 wasi_fs
3462 .root_fs
3463 .metadata(Path::new("/root.txt"))
3464 .unwrap()
3465 .is_file()
3466 );
3467 assert!(
3468 wasi_fs
3469 .root_fs
3470 .metadata(Path::new("/public/index.html"))
3471 .unwrap()
3472 .is_file()
3473 );
3474 }
3475}