wasmer_wasix/fs/
mod.rs

1// TODO: currently, hard links are broken in the presence or renames.
2// It is impossible to fix them with the current setup, since a hard
3// link must point to the actual file rather than its path, but the
4// only way we can get to a file on a FileSystem instance is by going
5// through its respective FileOpener and giving it a path as input.
6// TODO: refactor away the InodeVal type
7//
8// ## FD map / inode lock order
9//
10// When both locks are needed: acquire `fd_map` before `inode`, never the reverse.
11// Do not hold an `inode` lock while waiting on `fd_map`. Mutations that install or
12// remove map entries (`insert`, `remove`, `acquire_handle`, `drop_one_handle`) must
13// run under `fd_map.write()`. Capture `VirtualFile` handles (or cloned `Fd` data)
14// under the map lock before any `await`; never resolve I/O by fd number after dropping
15// the lock.
16
17mod 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};
41use tracing::{debug, trace, warn};
42use virtual_fs::{
43    ArcFileSystem, FileSystem, FsError, MountFileSystem, OpenOptions, OverlayFileSystem,
44    TmpFileSystem, VirtualFile, limiter::DynFsMemoryLimiter,
45};
46use wasmer_config::package::PackageId;
47use wasmer_wasix_types::{
48    types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO},
49    wasi::{
50        Errno, Fd as WasiFd, Fdflags, Fdflagsext, Fdstat, Filesize, Filestat, Filetype,
51        Preopentype, Prestat, PrestatEnum, Rights, Socktype,
52    },
53};
54
55pub(crate) use self::fd::VirtualFileLock;
56pub use self::fd::{Fd, FdInner, InodeVal, Kind, SymlinkKind};
57pub(crate) use self::fd_list::FdList;
58pub(crate) use self::inode_guard::{
59    InodeValFilePollGuard, InodeValFilePollGuardJoin, InodeValFilePollGuardMode,
60    InodeValFileReadGuard, InodeValFileWriteGuard, WasiStateFileGuard,
61};
62pub use self::notification::NotificationInner;
63pub(crate) use self::path_posix::{PosixPath, PosixPathBuf, PosixPathComponent};
64use crate::{ALL_RIGHTS, bin_factory::BinaryPackage, state::PreopenedDir};
65
66// POSIX bounds descriptor numbers by the process fd limit (`OPEN_MAX`,
67// `RLIMIT_NOFILE` on Linux). Other OSes commonly override the default, so
68// use a Linux-like 64k ceiling until WASIX models per-process fd limits.
69pub(crate) const MAX_FD: WasiFd = (64 * 1024) - 1;
70
71pub(crate) struct FlushPoller {
72    pub(crate) file: VirtualFileLock,
73}
74
75/// Result of removing an fd under `fd_map.write()`, with an optional flush target
76/// captured before `drop_one_handle` may clear the inode handle.
77pub(crate) struct CloseFdOutcome {
78    pub skipped_preopen: bool,
79    pub removed: bool,
80    pub flush_target: Option<VirtualFileLock>,
81}
82
83impl CloseFdOutcome {
84    fn not_found() -> Self {
85        Self {
86            skipped_preopen: false,
87            removed: false,
88            flush_target: None,
89        }
90    }
91}
92
93impl Future for FlushPoller {
94    type Output = Result<(), Errno>;
95
96    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
97        let mut file = self.file.write().unwrap();
98        Pin::new(file.as_mut())
99            .poll_flush(cx)
100            .map_err(|_| Errno::Io)
101    }
102}
103
104/// the fd value of the virtual root
105///
106/// Used for interacting with the file system when it has no
107/// pre-opened file descriptors at the root level. Normally
108/// a WASM process will do this in the libc initialization stage
109/// however that does not happen when the WASM process has never
110/// been run. Further that logic could change at any time in libc
111/// which would then break functionality. Instead we use this fixed
112/// file descriptor
113///
114/// This is especially important for fuse mounting journals which
115/// use the same syscalls as a normal WASI application but do not
116/// run the libc initialization logic
117pub const VIRTUAL_ROOT_FD: WasiFd = 3;
118
119/// The root inode and stdio inodes are the first inodes in the
120/// file system tree
121pub const FS_STDIN_INO: Inode = Inode(10);
122pub const FS_STDOUT_INO: Inode = Inode(11);
123pub const FS_STDERR_INO: Inode = Inode(12);
124pub const FS_ROOT_INO: Inode = Inode(13);
125
126const STDIN_DEFAULT_RIGHTS: Rights = {
127    // This might seem a bit overenineered, but it's the only way I
128    // discovered for getting the values in a const environment
129    Rights::from_bits_truncate(
130        Rights::FD_DATASYNC.bits()
131            | Rights::FD_READ.bits()
132            | Rights::FD_SYNC.bits()
133            | Rights::FD_ADVISE.bits()
134            | Rights::FD_FILESTAT_GET.bits()
135            | Rights::FD_FDSTAT_SET_FLAGS.bits()
136            | Rights::POLL_FD_READWRITE.bits(),
137    )
138};
139const STDOUT_DEFAULT_RIGHTS: Rights = {
140    // This might seem a bit overenineered, but it's the only way I
141    // discovered for getting the values in a const environment
142    Rights::from_bits_truncate(
143        Rights::FD_DATASYNC.bits()
144            | Rights::FD_SYNC.bits()
145            | Rights::FD_WRITE.bits()
146            | Rights::FD_ADVISE.bits()
147            | Rights::FD_FILESTAT_GET.bits()
148            | Rights::FD_FDSTAT_SET_FLAGS.bits()
149            | Rights::POLL_FD_READWRITE.bits(),
150    )
151};
152const STDERR_DEFAULT_RIGHTS: Rights = STDOUT_DEFAULT_RIGHTS;
153
154/// A completely arbitrary "big enough" number used as the upper limit for
155/// the number of symlinks that can be traversed when resolving a path
156pub const MAX_SYMLINKS: u32 = 128;
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
159pub struct Inode(u64);
160
161impl Inode {
162    pub fn as_u64(&self) -> u64 {
163        self.0
164    }
165
166    pub fn from_path(str: &str) -> Self {
167        Inode(xxhash_rust::xxh64::xxh64(str.as_bytes(), 0))
168    }
169}
170
171#[derive(Debug, Clone)]
172pub struct InodeGuard {
173    ino: Inode,
174    inner: Arc<InodeVal>,
175
176    // This exists because self.inner doesn't really represent the
177    // number of FDs referencing this InodeGuard. We need that number
178    // so we can know when to drop the file handle, which should result
179    // in the backing file (which may be a host file) getting closed.
180    open_handles: Arc<AtomicI32>,
181}
182impl InodeGuard {
183    pub fn ino(&self) -> Inode {
184        self.ino
185    }
186
187    pub fn downgrade(&self) -> InodeWeakGuard {
188        InodeWeakGuard {
189            ino: self.ino,
190            open_handles: self.open_handles.clone(),
191            inner: Arc::downgrade(&self.inner),
192        }
193    }
194
195    pub fn ref_cnt(&self) -> usize {
196        Arc::strong_count(&self.inner)
197    }
198
199    pub fn handle_count(&self) -> u32 {
200        self.open_handles.load(Ordering::SeqCst) as u32
201    }
202
203    pub fn acquire_handle(&self) {
204        let prev_handles = self.open_handles.fetch_add(1, Ordering::SeqCst);
205        trace!(ino = %self.ino.0, new_count = %(prev_handles + 1), "acquiring handle for InodeGuard");
206    }
207
208    pub fn drop_one_handle(&self) {
209        let prev_handles = self.open_handles.fetch_sub(1, Ordering::SeqCst);
210
211        trace!(ino = %self.ino.0, %prev_handles, "dropping handle for InodeGuard");
212
213        // If this wasn't the last handle, nothing else to do...
214        if prev_handles > 1 {
215            return;
216        }
217
218        // ... otherwise, drop the VirtualFile reference
219        let mut guard = self.inner.write();
220
221        // Must have at least one open handle before we can drop.
222        // This check happens after `inner` is locked so we can
223        // poison the lock and keep people from using this (possibly
224        // corrupt) InodeGuard.
225        if prev_handles != 1 {
226            panic!("InodeGuard handle dropped too many times");
227        }
228
229        // Re-check the open handles to account for race conditions
230        if self.open_handles.load(Ordering::SeqCst) != 0 {
231            return;
232        }
233
234        let ino = self.ino.0;
235        trace!(%ino, "InodeGuard has no more open handles");
236
237        match guard.deref_mut() {
238            Kind::File { handle, .. } if handle.is_some() => {
239                let file_ref_count = Arc::strong_count(handle.as_ref().unwrap());
240                trace!(%file_ref_count, %ino, "dropping file handle");
241                drop(handle.take().unwrap());
242            }
243            Kind::PipeRx { rx } => {
244                trace!(%ino, "closing pipe rx");
245                rx.close();
246            }
247            Kind::PipeTx { tx } => {
248                trace!(%ino, "closing pipe tx");
249                tx.close();
250            }
251            _ => (),
252        }
253    }
254}
255impl std::ops::Deref for InodeGuard {
256    type Target = InodeVal;
257    fn deref(&self) -> &Self::Target {
258        self.inner.deref()
259    }
260}
261
262#[derive(Debug, Clone)]
263pub struct InodeWeakGuard {
264    ino: Inode,
265    // Needed for when we want to upgrade back. We don't exactly
266    // care too much when the AtomicI32 is dropped, so this is
267    // a strong reference to keep things simple.
268    open_handles: Arc<AtomicI32>,
269    inner: Weak<InodeVal>,
270}
271impl InodeWeakGuard {
272    pub fn ino(&self) -> Inode {
273        self.ino
274    }
275    pub fn upgrade(&self) -> Option<InodeGuard> {
276        Weak::upgrade(&self.inner).map(|inner| InodeGuard {
277            ino: self.ino,
278            open_handles: self.open_handles.clone(),
279            inner,
280        })
281    }
282}
283
284#[derive(Debug)]
285struct EphemeralSymlinkEntry {
286    path_to_symlink: PathBuf,
287    relative_path: PathBuf,
288}
289
290#[derive(Debug)]
291#[warn(unused)]
292enum ComponentResolution {
293    Create {
294        kind: Kind,
295        name: String,
296        entry_name: String,
297        is_ephemeral: bool,
298    },
299    BackingSymlink {
300        file: PathBuf,
301        link_value: PathBuf,
302        entry_name: String,
303    },
304    #[cfg(unix)]
305    Special {
306        kind: Kind,
307        name: Cow<'static, str>,
308        entry_name: String,
309        stat: Filestat,
310    },
311}
312
313#[derive(Debug)]
314struct WasiInodesProtected {
315    lookup: HashMap<Inode, Weak<InodeVal>>,
316}
317
318#[derive(Clone, Debug)]
319pub struct WasiInodes {
320    protected: Arc<RwLock<WasiInodesProtected>>,
321}
322
323impl WasiInodes {
324    pub fn new() -> Self {
325        Self {
326            protected: Arc::new(RwLock::new(WasiInodesProtected {
327                lookup: Default::default(),
328            })),
329        }
330    }
331
332    /// adds another value to the inodes
333    pub fn add_inode_val(&self, val: InodeVal) -> InodeGuard {
334        let val = Arc::new(val);
335        let st_ino = {
336            let guard = val.stat.read().unwrap();
337            guard.st_ino
338        };
339
340        let mut guard = self.protected.write().unwrap();
341        let ino = Inode(st_ino);
342        guard.lookup.insert(ino, Arc::downgrade(&val));
343
344        // every 100 calls we clear out dead weaks
345        if guard.lookup.len() % 100 == 1 {
346            guard.lookup.retain(|_, v| Weak::strong_count(v) > 0);
347        }
348
349        let open_handles = Arc::new(AtomicI32::new(0));
350
351        InodeGuard {
352            ino,
353            open_handles,
354            inner: val,
355        }
356    }
357
358    /// Get the `VirtualFile` object at stdout mutably
359    pub(crate) fn stdout_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
360        Self::std_dev_get_mut(fd_map, __WASI_STDOUT_FILENO)
361    }
362
363    /// Get the `VirtualFile` object at stderr mutably
364    pub(crate) fn stderr_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
365        Self::std_dev_get_mut(fd_map, __WASI_STDERR_FILENO)
366    }
367
368    /// Get the `VirtualFile` object at stdin
369    /// TODO: Review why this is dead
370    #[allow(dead_code)]
371    pub(crate) fn stdin(fd_map: &RwLock<FdList>) -> Result<InodeValFileReadGuard, FsError> {
372        Self::std_dev_get(fd_map, __WASI_STDIN_FILENO)
373    }
374    /// Get the `VirtualFile` object at stdin mutably
375    pub(crate) fn stdin_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
376        Self::std_dev_get_mut(fd_map, __WASI_STDIN_FILENO)
377    }
378
379    /// Internal helper function to get a standard device handle.
380    /// Expects one of `__WASI_STDIN_FILENO`, `__WASI_STDOUT_FILENO`, `__WASI_STDERR_FILENO`.
381    fn std_dev_get(fd_map: &RwLock<FdList>, fd: WasiFd) -> Result<InodeValFileReadGuard, FsError> {
382        if let Some(fd) = fd_map.read().unwrap().get(fd) {
383            let guard = fd.inode.read();
384            if let Kind::File {
385                handle: Some(handle),
386                ..
387            } = guard.deref()
388            {
389                Ok(InodeValFileReadGuard::new(handle))
390            } else {
391                // Our public API should ensure that this is not possible
392                Err(FsError::NotAFile)
393            }
394        } else {
395            // this should only trigger if we made a mistake in this crate
396            Err(FsError::NoDevice)
397        }
398    }
399    /// Internal helper function to mutably get a standard device handle.
400    /// Expects one of `__WASI_STDIN_FILENO`, `__WASI_STDOUT_FILENO`, `__WASI_STDERR_FILENO`.
401    fn std_dev_get_mut(
402        fd_map: &RwLock<FdList>,
403        fd: WasiFd,
404    ) -> Result<InodeValFileWriteGuard, FsError> {
405        if let Some(fd) = fd_map.read().unwrap().get(fd) {
406            let guard = fd.inode.read();
407            if let Kind::File {
408                handle: Some(handle),
409                ..
410            } = guard.deref()
411            {
412                Ok(InodeValFileWriteGuard::new(handle))
413            } else {
414                // Our public API should ensure that this is not possible
415                Err(FsError::NotAFile)
416            }
417        } else {
418            // this should only trigger if we made a mistake in this crate
419            Err(FsError::NoDevice)
420        }
421    }
422}
423
424impl Default for WasiInodes {
425    fn default() -> Self {
426        Self::new()
427    }
428}
429
430#[derive(Debug, Clone)]
431pub struct WasiFsRoot {
432    root: Arc<MountFileSystem>,
433    memory_limiter: Option<DynFsMemoryLimiter>,
434}
435
436impl WasiFsRoot {
437    pub fn from_mount_fs(root: MountFileSystem) -> Self {
438        Self {
439            root: Arc::new(root),
440            memory_limiter: None,
441        }
442    }
443
444    pub fn from_filesystem(fs: Arc<dyn FileSystem + Send + Sync>) -> Self {
445        let root = MountFileSystem::new();
446        root.mount(Path::new("/"), fs)
447            .expect("mounting the root fs on an empty mount fs should succeed");
448
449        Self {
450            root: Arc::new(root),
451            memory_limiter: None,
452        }
453    }
454
455    pub fn with_memory_limiter_opt(mut self, limiter: Option<DynFsMemoryLimiter>) -> Self {
456        self.memory_limiter = limiter;
457        self
458    }
459
460    pub(crate) fn memory_limiter(&self) -> Option<&DynFsMemoryLimiter> {
461        self.memory_limiter.as_ref()
462    }
463
464    pub(crate) fn root(&self) -> &Arc<MountFileSystem> {
465        &self.root
466    }
467
468    pub(crate) fn writable_root(&self) -> Option<TmpFileSystem> {
469        let root = self.root.filesystem_at(Path::new("/"))?;
470        find_writable_root(root.as_ref())
471    }
472
473    pub(crate) fn stack_root_filesystem(
474        &self,
475        lower: Arc<dyn FileSystem + Send + Sync>,
476    ) -> Result<(), FsError> {
477        let current = self
478            .root
479            .filesystem_at(Path::new("/"))
480            .ok_or(FsError::EntryNotFound)?;
481        let overlay =
482            OverlayFileSystem::new(ArcFileSystem::new(current), [ArcFileSystem::new(lower)]);
483        self.root.set_mount(Path::new("/"), Arc::new(overlay))
484    }
485}
486
487fn find_writable_root(fs: &(dyn FileSystem + Send + Sync)) -> Option<TmpFileSystem> {
488    if let Some(tmp) = fs.upcast_any_ref().downcast_ref::<TmpFileSystem>() {
489        return Some(tmp.clone());
490    }
491
492    if let Some(arc_fs) = fs.upcast_any_ref().downcast_ref::<ArcFileSystem>() {
493        return find_writable_root(arc_fs.inner().as_ref());
494    }
495
496    if let Some(overlay) = fs
497        .upcast_any_ref()
498        .downcast_ref::<OverlayFileSystem<ArcFileSystem, Vec<Arc<dyn FileSystem + Send + Sync>>>>()
499    {
500        return find_writable_root(overlay.primary());
501    }
502
503    if let Some(overlay) = fs
504        .upcast_any_ref()
505        .downcast_ref::<OverlayFileSystem<ArcFileSystem, [ArcFileSystem; 1]>>()
506    {
507        return find_writable_root(overlay.primary());
508    }
509
510    None
511}
512
513impl FileSystem for WasiFsRoot {
514    fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
515        self.root.readlink(path)
516    }
517
518    fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
519        self.root.read_dir(path)
520    }
521
522    fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
523        self.root.create_dir(path)
524    }
525
526    fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> {
527        self.root.create_symlink(source, target)
528    }
529
530    fn hard_link(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> {
531        self.root.hard_link(source, target)
532    }
533
534    fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
535        self.root.remove_dir(path)
536    }
537
538    fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
539        let from = from.to_owned();
540        let to = to.to_owned();
541        let this = self.clone();
542        Box::pin(async move { this.root.rename(&from, &to).await })
543    }
544
545    fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
546        self.root.metadata(path)
547    }
548
549    fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
550        self.root.symlink_metadata(path)
551    }
552
553    fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
554        self.root.remove_file(path)
555    }
556
557    fn new_open_options(&self) -> OpenOptions<'_> {
558        self.root.new_open_options()
559    }
560}
561
562/// Warning, modifying these fields directly may cause invariants to break and
563/// should be considered unsafe.  These fields may be made private in a future release
564///
565/// Lock order when touching both the fd map and an inode: **`fd_map` first, then
566/// `inode`**. Prefer the `*_locked` helpers on [`WasiFs`] (`insert_fd_locked`,
567/// `clone_fd_locked`, `close_fd_locked`, `dup2_at`) so handle counts and map slots
568/// stay consistent under concurrency.
569pub struct WasiFs {
570    //pub repo: Repo,
571    pub preopen_fds: RwLock<Vec<u32>>,
572    pub fd_map: RwLock<FdList>,
573    pub current_dir: Mutex<String>,
574    pub root_fs: WasiFsRoot,
575    pub root_inode: InodeGuard,
576    pub has_unioned: Mutex<HashSet<PackageId>>,
577    ephemeral_symlinks: Arc<RwLock<HashMap<PathBuf, EphemeralSymlinkEntry>>>,
578
579    // TODO: remove
580    // using an atomic is a hack to enable customization after construction,
581    // but it shouldn't be necessary
582    // It should not be necessary at all.
583    is_wasix: AtomicBool,
584
585    // The preopens when this was initialized
586    pub(crate) init_preopens: Vec<PreopenedDir>,
587    // The virtual file system preopens when this was initialized
588    pub(crate) init_vfs_preopens: Vec<String>,
589}
590
591impl WasiFs {
592    fn writable_package_mount(
593        fs: Arc<dyn FileSystem + Send + Sync>,
594        limiter: Option<&DynFsMemoryLimiter>,
595    ) -> Arc<dyn FileSystem + Send + Sync> {
596        let upper = TmpFileSystem::new();
597        if let Some(limiter) = limiter {
598            upper.set_memory_limiter(limiter.clone());
599        }
600
601        Arc::new(OverlayFileSystem::new(upper, [ArcFileSystem::new(fs)]))
602    }
603
604    pub fn is_wasix(&self) -> bool {
605        // NOTE: this will only be set once very early in the instance lifetime,
606        // so Relaxed should be okay.
607        self.is_wasix.load(Ordering::Relaxed)
608    }
609
610    pub fn set_is_wasix(&self, is_wasix: bool) {
611        self.is_wasix.store(is_wasix, Ordering::SeqCst);
612    }
613
614    pub(crate) fn register_ephemeral_symlink(
615        &self,
616        full_path: PathBuf,
617        path_to_symlink: PathBuf,
618        relative_path: PathBuf,
619    ) {
620        let mut guard = self.ephemeral_symlinks.write().unwrap();
621        guard.insert(
622            PosixPath::from_path(&full_path)
623                .normalize_virtual_symlink_key()
624                .into_path_buf(),
625            EphemeralSymlinkEntry {
626                path_to_symlink: PosixPath::from_path(&path_to_symlink)
627                    .normalize_virtual_symlink_key()
628                    .into_path_buf(),
629                relative_path,
630            },
631        );
632    }
633
634    pub(crate) fn ephemeral_symlink_at(&self, full_path: &Path) -> Option<(PathBuf, PathBuf)> {
635        let guard = self.ephemeral_symlinks.read().unwrap();
636        let key = PosixPath::from_path(full_path)
637            .normalize_virtual_symlink_key()
638            .into_path_buf();
639        let entry = guard.get(&key)?;
640        Some((entry.path_to_symlink.clone(), entry.relative_path.clone()))
641    }
642
643    pub(crate) fn unregister_ephemeral_symlink(&self, full_path: &Path) {
644        let mut guard = self.ephemeral_symlinks.write().unwrap();
645        let key = PosixPath::from_path(full_path)
646            .normalize_virtual_symlink_key()
647            .into_path_buf();
648        guard.remove(&key);
649    }
650
651    /// Removes a symlink's backing file (if any) and drops its ephemeral record.
652    ///
653    /// A purely ephemeral (virtual) link has no host file, so `Noent` for a
654    /// still-registered ephemeral link counts as success. Returns the `Errno`
655    /// the syscall should report.
656    pub(crate) fn remove_symlink_file(&self, host_path: &Path) -> Errno {
657        match self
658            .root_fs
659            .remove_file(host_path)
660            .map_err(fs_error_into_wasi_err)
661        {
662            Ok(()) => {
663                self.unregister_ephemeral_symlink(host_path);
664                Errno::Success
665            }
666            Err(Errno::Noent) if self.ephemeral_symlink_at(host_path).is_some() => {
667                self.unregister_ephemeral_symlink(host_path);
668                Errno::Success
669            }
670            Err(e) => e,
671        }
672    }
673
674    pub(crate) fn move_ephemeral_symlink(
675        &self,
676        old_full_path: &Path,
677        new_full_path: &Path,
678        path_to_symlink: PathBuf,
679        relative_path: PathBuf,
680    ) {
681        let old_key = PosixPath::from_path(old_full_path)
682            .normalize_virtual_symlink_key()
683            .into_path_buf();
684        let new_key = PosixPath::from_path(new_full_path)
685            .normalize_virtual_symlink_key()
686            .into_path_buf();
687
688        let mut guard = self.ephemeral_symlinks.write().unwrap();
689        guard.remove(&old_key);
690        guard.insert(
691            new_key,
692            EphemeralSymlinkEntry {
693                path_to_symlink: PosixPath::from_path(&path_to_symlink)
694                    .normalize_virtual_symlink_key()
695                    .into_path_buf(),
696                relative_path,
697            },
698        );
699    }
700
701    /// Forking the WasiState is used when either fork or vfork is called
702    pub fn fork(&self) -> Self {
703        Self {
704            preopen_fds: RwLock::new(self.preopen_fds.read().unwrap().clone()),
705            fd_map: RwLock::new(self.fd_map.read().unwrap().clone()),
706            current_dir: Mutex::new(self.current_dir.lock().unwrap().clone()),
707            is_wasix: AtomicBool::new(self.is_wasix.load(Ordering::Acquire)),
708            root_fs: self.root_fs.clone(),
709            root_inode: self.root_inode.clone(),
710            has_unioned: Mutex::new(self.has_unioned.lock().unwrap().clone()),
711            ephemeral_symlinks: self.ephemeral_symlinks.clone(),
712            init_preopens: self.init_preopens.clone(),
713            init_vfs_preopens: self.init_vfs_preopens.clone(),
714        }
715    }
716
717    /// Closes all file descriptors marked CLOEXEC (except stdio and preopens).
718    pub async fn close_cloexec_fds(&self) {
719        let flush_targets = {
720            let mut fd_map = self.fd_map.write().unwrap();
721            let to_close: Vec<WasiFd> = fd_map
722                .iter()
723                .filter_map(|(k, v)| {
724                    if v.inner.fd_flags.contains(Fdflagsext::CLOEXEC)
725                        && !v.is_stdio
726                        && !v.inode.is_preopened
727                    {
728                        tracing::trace!(fd = %k, "Closing FD due to CLOEXEC flag");
729                        Some(k)
730                    } else {
731                        None
732                    }
733                })
734                .collect();
735            let mut flush_targets = Vec::new();
736            for fd in to_close {
737                let outcome = Self::close_fd_locked(&mut fd_map, fd);
738                if let Some(target) = outcome.flush_target {
739                    flush_targets.push(target);
740                }
741            }
742            flush_targets
743        };
744
745        for file in flush_targets {
746            Self::flush_file_best_effort(file).await;
747        }
748    }
749
750    /// Closes all file descriptors, flushing captured handles after dropping the map lock.
751    pub async fn close_all(&self) {
752        let flush_targets = {
753            let mut fd_map = self.fd_map.write().unwrap();
754            let mut fds: HashSet<WasiFd> = fd_map.keys().collect();
755            fds.insert(__WASI_STDOUT_FILENO);
756            fds.insert(__WASI_STDERR_FILENO);
757
758            let mut flush_targets = Vec::new();
759            for fd in fds {
760                let outcome = Self::close_fd_locked(&mut fd_map, fd);
761                if let Some(target) = outcome.flush_target {
762                    flush_targets.push(target);
763                }
764            }
765
766            // Preopens skipped by close_fd_locked remain until clear().
767            for (_fd, fd_ref) in fd_map.iter().collect::<Vec<_>>() {
768                if let Some(target) = Self::file_flush_target(&fd_ref.inode) {
769                    flush_targets.push(target);
770                }
771            }
772            fd_map.clear();
773            flush_targets
774        };
775
776        for file in flush_targets {
777            Self::flush_file_best_effort(file).await;
778        }
779    }
780
781    /// Will conditionally union the binary file system with this one
782    /// if it has not already been unioned
783    pub async fn conditional_union(
784        &self,
785        binary: &BinaryPackage,
786    ) -> Result<(), virtual_fs::FsError> {
787        let Some(package_mounts) = &binary.package_mounts else {
788            return Ok(());
789        };
790
791        let needs_to_be_unioned = self.has_unioned.lock().unwrap().insert(binary.id.clone());
792        if !needs_to_be_unioned {
793            return Ok(());
794        }
795
796        if let Some(root_layer) = &package_mounts.root_layer {
797            self.root_fs
798                .stack_root_filesystem(Self::writable_package_mount(
799                    root_layer.clone(),
800                    self.root_fs.memory_limiter(),
801                ))?;
802        }
803
804        for mount in &package_mounts.mounts {
805            self.root_fs.root().mount_with_source(
806                &mount.guest_path,
807                &mount.source_path,
808                Self::writable_package_mount(mount.fs.clone(), self.root_fs.memory_limiter()),
809            )?;
810        }
811
812        Ok(())
813    }
814
815    /// Created for the builder API. like `new` but with more information
816    pub(crate) fn new_with_preopen(
817        inodes: &WasiInodes,
818        preopens: &[PreopenedDir],
819        vfs_preopens: &[String],
820        fs_backing: WasiFsRoot,
821    ) -> Result<Self, String> {
822        let mut wasi_fs = Self::new_init(fs_backing, inodes, FS_ROOT_INO)?;
823        wasi_fs.init_preopens = preopens.to_vec();
824        wasi_fs.init_vfs_preopens = vfs_preopens.to_vec();
825        wasi_fs.create_preopens(inodes, false)?;
826        Ok(wasi_fs)
827    }
828
829    /// Converts a relative path into an absolute path
830    pub(crate) fn relative_path_to_absolute(&self, path: String) -> String {
831        if path.starts_with('/') {
832            return path;
833        }
834
835        let current_dir = self.current_dir.lock().unwrap();
836        format!("{}/{}", current_dir.trim_end_matches('/'), path)
837    }
838
839    /// Private helper function to init the filesystem, called in `new` and
840    /// `new_with_preopen`
841    fn new_init(
842        fs_backing: WasiFsRoot,
843        inodes: &WasiInodes,
844        st_ino: Inode,
845    ) -> Result<Self, String> {
846        debug!("Initializing WASI filesystem");
847
848        let stat = Filestat {
849            st_filetype: Filetype::Directory,
850            st_ino: st_ino.as_u64(),
851            ..Filestat::default()
852        };
853        let root_kind = Kind::Root {
854            entries: HashMap::new(),
855        };
856        let root_inode = inodes.add_inode_val(InodeVal {
857            stat: RwLock::new(stat),
858            is_preopened: true,
859            name: RwLock::new("/".into()),
860            kind: RwLock::new(root_kind),
861        });
862
863        let wasi_fs = Self {
864            preopen_fds: RwLock::new(vec![]),
865            fd_map: RwLock::new(FdList::new()),
866            current_dir: Mutex::new("/".to_string()),
867            is_wasix: AtomicBool::new(false),
868            root_fs: fs_backing,
869            root_inode,
870            has_unioned: Mutex::new(HashSet::new()),
871            ephemeral_symlinks: Arc::new(RwLock::new(HashMap::new())),
872            init_preopens: Default::default(),
873            init_vfs_preopens: Default::default(),
874        };
875        wasi_fs.create_stdin(inodes);
876        wasi_fs.create_stdout(inodes);
877        wasi_fs.create_stderr(inodes);
878        wasi_fs.create_rootfd()?;
879
880        Ok(wasi_fs)
881    }
882
883    /// This function is like create dir all, but it also opens it.
884    /// Function is unsafe because it may break invariants and hasn't been tested.
885    /// This is an experimental function and may be removed
886    ///
887    /// # Safety
888    /// - Virtual directories created with this function must not conflict with
889    ///   the standard operation of the WASI filesystem.  This is vague and
890    ///   unlikely in practice.  [Join the discussion](https://github.com/wasmerio/wasmer/issues/1219)
891    ///   for what the newer, safer WASI FS APIs should look like.
892    #[allow(dead_code)]
893    #[allow(clippy::too_many_arguments)]
894    pub unsafe fn open_dir_all(
895        &mut self,
896        inodes: &WasiInodes,
897        base: WasiFd,
898        name: String,
899        rights: Rights,
900        rights_inheriting: Rights,
901        flags: Fdflags,
902        fd_flags: Fdflagsext,
903    ) -> Result<WasiFd, FsError> {
904        // TODO: check permissions here? probably not, but this should be
905        // an explicit choice, so justify it in a comment when we remove this one
906        let mut cur_inode = self.get_fd_inode(base).map_err(fs_error_from_wasi_err)?;
907
908        let path: &Path = Path::new(&name);
909        //let n_components = path.components().count();
910        for c in path.components() {
911            let segment_name = c.as_os_str().to_string_lossy().to_string();
912            let guard = cur_inode.read();
913            match guard.deref() {
914                Kind::Dir { entries, .. } | Kind::Root { entries } => {
915                    if let Some(_entry) = entries.get(&segment_name) {
916                        // TODO: this should be fixed
917                        return Err(FsError::AlreadyExists);
918                    }
919
920                    let kind = Kind::Dir {
921                        parent: cur_inode.downgrade(),
922                        path: PathBuf::from(""),
923                        entries: HashMap::new(),
924                    };
925
926                    drop(guard);
927                    let inode = self.create_inode_with_default_stat(
928                        inodes,
929                        kind,
930                        false,
931                        segment_name.clone().into(),
932                    );
933
934                    // reborrow to insert
935                    {
936                        let mut guard = cur_inode.write();
937                        match guard.deref_mut() {
938                            Kind::Dir { entries, .. } | Kind::Root { entries } => {
939                                entries.insert(segment_name, inode.clone());
940                            }
941                            _ => unreachable!("Dir or Root became not Dir or Root"),
942                        }
943                    }
944                    cur_inode = inode;
945                }
946                _ => return Err(FsError::BaseNotDirectory),
947            }
948        }
949
950        // TODO: review open flags (read, write); they were added without consideration
951        self.create_fd(
952            rights,
953            rights_inheriting,
954            flags,
955            fd_flags,
956            Fd::READ | Fd::WRITE,
957            cur_inode,
958        )
959        .map_err(fs_error_from_wasi_err)
960    }
961
962    /// Opens a user-supplied file in the directory specified with the
963    /// name and flags given
964    // dead code because this is an API for external use
965    // TODO: is this used anywhere? Is it even sound?
966    #[allow(dead_code, clippy::too_many_arguments)]
967    pub fn open_file_at(
968        &mut self,
969        inodes: &WasiInodes,
970        base: WasiFd,
971        file: Box<dyn VirtualFile + Send + Sync + 'static>,
972        open_flags: u16,
973        name: String,
974        rights: Rights,
975        rights_inheriting: Rights,
976        flags: Fdflags,
977        fd_flags: Fdflagsext,
978    ) -> Result<WasiFd, FsError> {
979        // TODO: check permissions here? probably not, but this should be
980        // an explicit choice, so justify it in a comment when we remove this one
981        let base_inode = self.get_fd_inode(base).map_err(fs_error_from_wasi_err)?;
982
983        let guard = base_inode.read();
984        match guard.deref() {
985            Kind::Dir { entries, .. } | Kind::Root { entries } => {
986                if let Some(_entry) = entries.get(&name) {
987                    // TODO: eventually change the logic here to allow overwrites
988                    return Err(FsError::AlreadyExists);
989                }
990
991                let kind = Kind::File {
992                    handle: Some(Arc::new(RwLock::new(file))),
993                    path: PathBuf::from(""),
994                    fd: None,
995                };
996
997                drop(guard);
998                let inode = self
999                    .create_inode(inodes, kind, false, name.clone())
1000                    .map_err(|_| FsError::IOError)?;
1001
1002                {
1003                    let mut guard = base_inode.write();
1004                    match guard.deref_mut() {
1005                        Kind::Dir { entries, .. } | Kind::Root { entries } => {
1006                            entries.insert(name, inode.clone());
1007                        }
1008                        _ => unreachable!("Dir or Root became not Dir or Root"),
1009                    }
1010                }
1011
1012                // Here, we clone the inode so we can use it to overwrite the fd field below.
1013                let real_fd = self
1014                    .create_fd(
1015                        rights,
1016                        rights_inheriting,
1017                        flags,
1018                        fd_flags,
1019                        open_flags,
1020                        inode.clone(),
1021                    )
1022                    .map_err(fs_error_from_wasi_err)?;
1023
1024                {
1025                    let mut guard = inode.kind.write().unwrap();
1026                    match guard.deref_mut() {
1027                        Kind::File { fd, .. } => {
1028                            *fd = Some(real_fd);
1029                        }
1030                        _ => unreachable!("We just created a Kind::File"),
1031                    }
1032                }
1033
1034                Ok(real_fd)
1035            }
1036            _ => Err(FsError::BaseNotDirectory),
1037        }
1038    }
1039
1040    /// Change the backing of a given file descriptor
1041    /// Returns the old backing
1042    /// TODO: add examples
1043    #[allow(dead_code)]
1044    pub fn swap_file(
1045        &self,
1046        fd: WasiFd,
1047        mut file: Box<dyn VirtualFile + Send + Sync + 'static>,
1048    ) -> Result<Option<Box<dyn VirtualFile + Send + Sync + 'static>>, FsError> {
1049        match fd {
1050            __WASI_STDIN_FILENO => {
1051                let mut target = WasiInodes::stdin_mut(&self.fd_map)?;
1052                Ok(Some(target.swap(file)))
1053            }
1054            __WASI_STDOUT_FILENO => {
1055                let mut target = WasiInodes::stdout_mut(&self.fd_map)?;
1056                Ok(Some(target.swap(file)))
1057            }
1058            __WASI_STDERR_FILENO => {
1059                let mut target = WasiInodes::stderr_mut(&self.fd_map)?;
1060                Ok(Some(target.swap(file)))
1061            }
1062            _ => {
1063                let base_inode = self.get_fd_inode(fd).map_err(fs_error_from_wasi_err)?;
1064                {
1065                    // happy path
1066                    let guard = base_inode.read();
1067                    match guard.deref() {
1068                        Kind::File { handle, .. } => {
1069                            if let Some(handle) = handle {
1070                                let mut handle = handle.write().unwrap();
1071                                std::mem::swap(handle.deref_mut(), &mut file);
1072                                return Ok(Some(file));
1073                            }
1074                        }
1075                        _ => return Err(FsError::NotAFile),
1076                    }
1077                }
1078                // slow path
1079                let mut guard = base_inode.write();
1080                match guard.deref_mut() {
1081                    Kind::File { handle, .. } => {
1082                        if let Some(handle) = handle {
1083                            let mut handle = handle.write().unwrap();
1084                            std::mem::swap(handle.deref_mut(), &mut file);
1085                            Ok(Some(file))
1086                        } else {
1087                            handle.replace(Arc::new(RwLock::new(file)));
1088                            Ok(None)
1089                        }
1090                    }
1091                    _ => Err(FsError::NotAFile),
1092                }
1093            }
1094        }
1095    }
1096
1097    /// refresh size from filesystem
1098    pub fn filestat_resync_size(&self, fd: WasiFd) -> Result<Filesize, Errno> {
1099        let inode = self.get_fd_inode(fd)?;
1100        let mut guard = inode.write();
1101        match guard.deref_mut() {
1102            Kind::File { handle, .. } => {
1103                if let Some(h) = handle {
1104                    let h = h.read().unwrap();
1105                    let new_size = h.size();
1106                    drop(h);
1107                    drop(guard);
1108
1109                    inode.stat.write().unwrap().st_size = new_size;
1110                    Ok(new_size as Filesize)
1111                } else {
1112                    Err(Errno::Badf)
1113                }
1114            }
1115            Kind::Dir { .. } | Kind::Root { .. } => Err(Errno::Isdir),
1116            _ => Err(Errno::Inval),
1117        }
1118    }
1119
1120    /// Changes the current directory
1121    pub fn set_current_dir(&self, path: &str) {
1122        let mut guard = self.current_dir.lock().unwrap();
1123        *guard = path.to_string();
1124    }
1125
1126    /// Gets the current directory
1127    pub fn get_current_dir(
1128        &self,
1129        inodes: &WasiInodes,
1130        base: WasiFd,
1131    ) -> Result<(InodeGuard, String), Errno> {
1132        self.get_current_dir_inner(inodes, base, 0)
1133    }
1134
1135    pub(crate) fn get_current_dir_inner(
1136        &self,
1137        inodes: &WasiInodes,
1138        base: WasiFd,
1139        symlink_count: u32,
1140    ) -> Result<(InodeGuard, String), Errno> {
1141        let mut symlink_count = symlink_count;
1142        let current_dir = {
1143            let guard = self.current_dir.lock().unwrap();
1144            guard.clone()
1145        };
1146        let cur_inode = self.get_fd_inode(base)?;
1147        let inode = self.get_inode_at_path_inner(
1148            inodes,
1149            cur_inode,
1150            current_dir.as_str(),
1151            &mut symlink_count,
1152            true,
1153        )?;
1154        Ok((inode, current_dir))
1155    }
1156
1157    /// Resolve a path in the POSIX namespace visible to the WASIX guest.
1158    ///
1159    /// This function intentionally resolves guest paths, not host-native paths.
1160    /// A Windows host path may contain `\`, drive prefixes, or UNC prefixes, but
1161    /// those belong to mount setup and backing filesystem access. Once a host
1162    /// directory is mounted into WASIX, the guest observes a POSIX path tree
1163    /// where `/` is the only separator. Raw syscall paths must therefore be
1164    /// parsed with POSIX rules even when the runtime itself is running on
1165    /// Windows.
1166    ///
1167    /// POSIX path resolution is stricter than Rust's `Path::components()`:
1168    /// explicit `.`, explicit `..`, an empty pathname, and a trailing slash are
1169    /// all observable. In particular, `file/` and `file/.` must fail with
1170    /// `Errno::Notdir`, `lstat("symlink_to_dir/")` must follow the symlink to
1171    /// prove the result is a directory, and `lstat("symlink_to_file/")` must
1172    /// fail with `Errno::Notdir`. For that reason this function uses a small
1173    /// POSIX component parser instead of `Path::components()`.
1174    ///
1175    /// Symlink following follows the POSIX rule used by `openat`-style APIs:
1176    /// intermediate symlinks are always followed, while the final component is
1177    /// followed only when `follow_symlinks` is true. Recursive symlink
1178    /// resolution increments `symlink_count`, and symlink depth exhaustion maps
1179    /// to `Errno::Loop`.
1180    ///
1181    /// There are two loops here with different jobs. The outer loop walks the
1182    /// parsed path components. The inner `component_lookup` loop normally runs
1183    /// once, but has one virtual-root overlay case: when the current inode is
1184    /// `Kind::Root` and a component is not found directly, it can jump through
1185    /// the mounted `entries["/"]` inode and retry the same component. That is
1186    /// WASIX virtual-root behavior, not plain POSIX filesystem traversal.
1187    ///
1188    /// Keep these edge cases intact when editing this function:
1189    ///
1190    /// - Empty pathnames are `Errno::Noent`; they do not resolve to the base
1191    ///   inode unless a separate `AT_EMPTY_PATH`-style extension is introduced.
1192    /// - Absolute paths resolve from `VIRTUAL_ROOT_FD`, independent of the
1193    ///   caller-provided starting inode.
1194    /// - A literal root pathname (`/`, `//`, and so on) preserves historical
1195    ///   WASIX behavior: if the virtual root contains a mounted `entries["/"]`
1196    ///   directory, the literal root path resolves to that mounted directory.
1197    ///   This special case is intentionally limited to an all-slashes pathname.
1198    /// - Parent traversal is semantic, not a string rewrite. The virtual root's
1199    ///   parent is itself, but a mounted directory whose guest name is `/` still
1200    ///   has the virtual root as its parent. Therefore `/..` may resolve to
1201    ///   `Kind::Root` after walking from the mounted `/` directory upward, and
1202    ///   traversal that genuinely reaches `Kind::Root` must not be remapped
1203    ///   back to `entries["/"]` at the end. That distinction lets WASI guests
1204    ///   see the virtual root with all preopens via `..` without changing the
1205    ///   behavior of opening literal `/`.
1206    /// - `.` and `..` are semantic components: they require the current inode
1207    ///   to be a directory or virtual root, otherwise they fail with
1208    ///   `Errno::Notdir`.
1209    /// - Special files may be returned only as the final component. As path
1210    ///   prefixes, they fail with `Errno::Notdir`.
1211    ///
1212    /// The returned `InodeGuard` is the inode for the resolved final object in
1213    /// the WASIX inode graph. It is not necessarily an already-open host file:
1214    /// file inodes discovered here are normally created with `handle: None`,
1215    /// and `path_open` or a similar caller opens the backing file later. If the
1216    /// final object is a symlink and `follow_symlinks` is false, the returned
1217    /// inode is the symlink itself; otherwise symlink targets are resolved
1218    /// recursively and the returned inode is the target.
1219    ///
1220    /// Directory `entries` are a lazy cache over the backing filesystem. When a
1221    /// child name is already present in the current `Kind::Dir` or `Kind::Root`,
1222    /// that cached inode wins. When a child is missing from a `Kind::Dir`, this
1223    /// resolver builds the backing path for that one component, checks the
1224    /// ephemeral symlink table, then calls `root_fs.symlink_metadata()` without
1225    /// following symlinks. Based on that metadata it materializes a `Kind::Dir`,
1226    /// `Kind::File`, `Kind::Symlink`, or supported special-file inode. Persistent
1227    /// backing entries are inserted into the parent directory cache; ephemeral
1228    /// symlink inodes are transient and are not cached as directory entries.
1229    ///
1230    /// Cached directory entries are part of the guest-visible directory model,
1231    /// not merely an implementation detail. A later `fd_readdir` over a backing
1232    /// directory must merge these cached children with host children instead of
1233    /// hiding non-preopen cache entries; otherwise cleanup and tree-walking code
1234    /// can miss inodes that this resolver can still reach.
1235    ///
1236    /// This function is therefore not a full synchronization pass. It observes
1237    /// the backing filesystem on cache misses, but cached entries are reused
1238    /// without re-statting. Syscalls that mutate the filesystem are responsible
1239    /// for keeping the inode cache and ephemeral symlink map coherent with their
1240    /// changes.
1241    fn get_inode_at_path_inner(
1242        &self,
1243        inodes: &WasiInodes,
1244        mut cur_inode: InodeGuard,
1245        path_str: &str,
1246        symlink_count: &mut u32,
1247        follow_symlinks: bool,
1248    ) -> Result<InodeGuard, Errno> {
1249        if *symlink_count > MAX_SYMLINKS {
1250            return Err(Errno::Loop);
1251        }
1252
1253        if path_str.is_empty() {
1254            return Err(Errno::Noent);
1255        }
1256
1257        if path_str.starts_with('/') {
1258            cur_inode = self.get_fd_inode(VIRTUAL_ROOT_FD)?;
1259        }
1260
1261        let is_all_slashes = path_str.bytes().all(|b| b == b'/');
1262
1263        // Absolute root paths should resolve to the mounted "/" inode when present.
1264        // This keeps "/" behavior aligned with historical path traversal semantics.
1265        if is_all_slashes {
1266            if let Kind::Root { entries } = cur_inode.read().deref()
1267                && let Some(root_entry) = entries.get("/")
1268            {
1269                return Ok(root_entry.clone());
1270            }
1271            return Ok(cur_inode);
1272        }
1273
1274        // POSIX path resolution is stricter than `Path::components()`: explicit
1275        // `.`/`..` and a trailing slash are observable because they require the
1276        // current result to be a directory after symlink resolution.
1277        let path = PosixPath::new(path_str);
1278        let components = path.components(true, true);
1279
1280        let n_components = components.len();
1281
1282        // TODO: rights checks
1283        // for each component traverse file structure loading inodes as
1284        // necessary.
1285        'path_iter: for (i, component) in components.into_iter().enumerate() {
1286            // Since we're resolving the path against the given inode, we want to
1287            // assume '/a/b' to be the same as `a/b` relative to the inode, so
1288            // we skip over the RootDir component.
1289            if matches!(component, PosixPathComponent::RootDir) {
1290                continue 'path_iter;
1291            }
1292
1293            // Note: when current component is last and follow is off, then we
1294            // return inode of the symlink itself, however if current component
1295            // is inner we will follow symlinks even with follow off.
1296            // Following symlinks uses recursive resolution, thus if current
1297            // component is not last, we recurse with follow always on. Only
1298            // last component with follow off will result in symlink not being
1299            // followed.
1300            let last_component = i == n_components - 1;
1301
1302            let component_str = match component {
1303                PosixPathComponent::CurDir => {
1304                    let is_dir = {
1305                        let guard = cur_inode.read();
1306                        matches!(guard.deref(), Kind::Dir { .. } | Kind::Root { .. })
1307                    };
1308                    if is_dir {
1309                        continue 'path_iter;
1310                    }
1311                    return Err(Errno::Notdir);
1312                }
1313                PosixPathComponent::ParentDir => {
1314                    let parent_inode = {
1315                        let guard = cur_inode.read();
1316                        match guard.deref() {
1317                            Kind::Root { .. } => None,
1318                            Kind::Dir { parent, .. } => {
1319                                Some(parent.upgrade().ok_or(Errno::Access)?)
1320                            }
1321                            _ => return Err(Errno::Notdir),
1322                        }
1323                    };
1324                    if let Some(parent_inode) = parent_inode {
1325                        cur_inode = parent_inode;
1326                    }
1327                    continue 'path_iter;
1328                }
1329                PosixPathComponent::Normal(component) => component,
1330                PosixPathComponent::RootDir => unreachable!("RootDir is handled above"),
1331            };
1332
1333            'component_lookup: loop {
1334                // 1. Read-Only Lookup Phase
1335                // --
1336                // Match current inode against known entry types, and if it happens
1337                // to be a directory, then resolve current component as an entry in
1338                // that directory.
1339                // Note: this loop practically never does more than one iteration.
1340                // There is only one exotic case when this loop would do another
1341                // iteration, and it is when current inode happens to be Root
1342                // containing '/' entry.
1343                let component_resolution = {
1344                    match cur_inode.clone().read().deref() {
1345                        Kind::Buffer { .. } => {
1346                            unimplemented!("state::get_inode_at_path for buffers")
1347                        }
1348                        Kind::File { .. }
1349                        | Kind::Socket { .. }
1350                        | Kind::PipeRx { .. }
1351                        | Kind::PipeTx { .. }
1352                        | Kind::DuplexPipe { .. }
1353                        | Kind::EventNotifications { .. }
1354                        | Kind::Epoll { .. } => {
1355                            return Err(Errno::Notdir);
1356                        }
1357                        Kind::Symlink { .. } => break 'component_lookup,
1358                        Kind::Root { entries } => {
1359                            if let Some(entry) = entries.get(component_str) {
1360                                cur_inode = entry.clone();
1361                                break 'component_lookup;
1362                            } else if let Some(root) = entries.get("/") {
1363                                // This is quite exotic case where Root itself
1364                                // has '/' entry in it, and we want to follow
1365                                // from there.
1366                                // Note: this is one and only case where
1367                                // 'component_lookup loop would do another
1368                                // iteration - the only actual reason for it to
1369                                // be a loop.
1370                                cur_inode = root.clone();
1371                                continue 'component_lookup;
1372                            } else {
1373                                // Root is not capable of having something other
1374                                // then preopenned folders
1375                                return Err(Errno::Notcapable);
1376                            }
1377                        }
1378                        Kind::Dir {
1379                            entries,
1380                            path: cur_dir,
1381                            ..
1382                        } => {
1383                            // When component resolves to directory entry, then
1384                            // next component needs to resolve to a child node
1385                            // within that directory.
1386                            // Here we are handling all variants of directory
1387                            // children.
1388
1389                            if let Some(entry) = entries.get(component_str) {
1390                                // We found component in cached entries, so we
1391                                // can continue. If it is a symlink it will be
1392                                // resolved in next the step.
1393                                cur_inode = entry.clone();
1394                                break 'component_lookup;
1395                            }
1396
1397                            // We did not find the component in cached entries,
1398                            // so we will create new inode for it.
1399                            let entry_path_buf = PosixPath::from_path(cur_dir)
1400                                .join(&PosixPath::new(component_str))
1401                                .into_path_buf();
1402
1403                            // Current component of the path we're resolving, as
1404                            // a string...
1405                            let entry_name = component_str.to_string();
1406
1407                            // ...and its relevant path within current inode
1408                            // being the directory.
1409                            // Note: the entry_path does not need to match the
1410                            // path we're resolving, e.g. if this is a recursive
1411                            // call from symlink resolution branch.
1412                            let entry_path = entry_path_buf.to_string_lossy().to_string();
1413
1414                            if let Some((path_to_symlink, relative_path)) =
1415                                self.ephemeral_symlink_at(&entry_path_buf)
1416                            {
1417                                // Ephemeral symlink are transient records; they
1418                                // are virtual, and they are not persisted in
1419                                // directory like symbolic links, so we will
1420                                // create a temporary inode for them.
1421                                // We resolve them but don't cache them as dir
1422                                // entries.
1423                                ComponentResolution::Create {
1424                                    kind: Kind::Symlink {
1425                                        symlink_kind: SymlinkKind::Virtual,
1426                                        path_to_symlink,
1427                                        relative_path,
1428                                    },
1429                                    name: entry_path,
1430                                    entry_name,
1431                                    is_ephemeral: true,
1432                                }
1433                            } else {
1434                                // Otherwise it is persistent, and we create new
1435                                // inode for it that we will cache in directory
1436                                // entries.
1437                                // Note: this gets metadata of the file entry
1438                                // without following symbolic links.
1439                                let metadata = self
1440                                    .root_fs
1441                                    .symlink_metadata(&entry_path_buf)
1442                                    .ok()
1443                                    .ok_or(Errno::Noent)?;
1444                                let file_type = metadata.file_type();
1445                                if file_type.is_dir() {
1446                                    // load DIR
1447                                    ComponentResolution::Create {
1448                                        kind: Kind::Dir {
1449                                            parent: cur_inode.downgrade(),
1450                                            path: entry_path_buf,
1451                                            entries: Default::default(),
1452                                        },
1453                                        name: entry_path,
1454                                        entry_name,
1455                                        is_ephemeral: false,
1456                                    }
1457                                } else if file_type.is_file() {
1458                                    // load file
1459                                    ComponentResolution::Create {
1460                                        kind: Kind::File {
1461                                            handle: None,
1462                                            path: entry_path_buf,
1463                                            fd: None,
1464                                        },
1465                                        name: entry_path,
1466                                        entry_name,
1467                                        is_ephemeral: false,
1468                                    }
1469                                } else if file_type.is_symlink() {
1470                                    // load symbolic link
1471                                    // Note: as opposed to ephemeral symlinks,
1472                                    // which are transient, these are
1473                                    // persistent, i.e. they have actual entry
1474                                    // in the directory
1475                                    // structure.
1476                                    let link_value = self
1477                                        .root_fs
1478                                        .readlink(&entry_path_buf)
1479                                        .ok()
1480                                        .ok_or(Errno::Noent)?;
1481                                    debug!("attempting to decompose path {:?}", link_value);
1482                                    ComponentResolution::BackingSymlink {
1483                                        file: entry_path_buf,
1484                                        link_value,
1485                                        entry_name,
1486                                    }
1487                                } else {
1488                                    #[cfg(unix)]
1489                                    {
1490                                        //use std::os::unix::fs::FileTypeExt;
1491                                        let file_type: Filetype = if file_type.is_char_device() {
1492                                            Filetype::CharacterDevice
1493                                        } else if file_type.is_block_device() {
1494                                            Filetype::BlockDevice
1495                                        } else if file_type.is_fifo() {
1496                                            // FIFO doesn't seem to fit any other type, so unknown
1497                                            Filetype::Unknown
1498                                        } else if file_type.is_socket() {
1499                                            // TODO: how do we know if it's a `SocketStream` or
1500                                            // a `SocketDgram`?
1501                                            Filetype::SocketStream
1502                                        } else {
1503                                            unimplemented!(
1504                                                "state::get_inode_at_path unknown file type: not file, directory, symlink, char device, block device, fifo, or socket"
1505                                            );
1506                                        };
1507
1508                                        ComponentResolution::Special {
1509                                            kind: Kind::File {
1510                                                handle: None,
1511                                                path: entry_path_buf,
1512                                                fd: None,
1513                                            },
1514                                            name: entry_path.into(),
1515                                            entry_name,
1516                                            stat: Filestat {
1517                                                st_filetype: file_type,
1518                                                st_ino: Inode::from_path(path_str).as_u64(),
1519                                                st_size: metadata.len(),
1520                                                st_ctim: metadata.created(),
1521                                                st_mtim: metadata.modified(),
1522                                                st_atim: metadata.accessed(),
1523                                                ..Filestat::default()
1524                                            },
1525                                        }
1526                                    }
1527                                    #[cfg(not(unix))]
1528                                    unimplemented!(
1529                                        "state::get_inode_at_path unknown file type: not file, directory, or symlink"
1530                                    );
1531                                }
1532                            } // end of non-ephemeral entry case
1533                        } // end of Kind::Dir match case
1534                    } // end of match
1535                }; // end of component_resolution block
1536
1537                // 2. Create an INode and update directory entries
1538                // --
1539                // The cur_inode is definitely a directory (Kind::Dir) at this
1540                // stage, and we need to create an inode (new_inode) and cache
1541                // as an entry in current directory (entry_name => cur_inode).
1542                let (entry_name, new_inode, should_insert, should_return) =
1543                    match component_resolution {
1544                        ComponentResolution::Create {
1545                            kind,
1546                            name,
1547                            entry_name,
1548                            is_ephemeral,
1549                        } => {
1550                            let new_inode = self.create_inode(inodes, kind, false, name)?;
1551                            (entry_name, new_inode, !is_ephemeral, false)
1552                        }
1553                        ComponentResolution::BackingSymlink {
1554                            file,
1555                            link_value,
1556                            entry_name,
1557                        } => {
1558                            let new_inode = self.create_inode(
1559                                inodes,
1560                                Kind::Symlink {
1561                                    symlink_kind: SymlinkKind::Backing,
1562                                    path_to_symlink: PosixPath::from_path(&file)
1563                                        .strip_root_prefix()
1564                                        .into_path_buf(),
1565                                    relative_path: link_value,
1566                                },
1567                                false,
1568                                file.to_string_lossy().to_string(),
1569                            )?;
1570                            (entry_name, new_inode, false, false)
1571                        }
1572                        #[cfg(unix)]
1573                        ComponentResolution::Special {
1574                            kind,
1575                            name,
1576                            entry_name,
1577                            stat,
1578                        } => {
1579                            let new_inode =
1580                                self.create_inode_with_stat(inodes, kind, false, name, stat);
1581                            (entry_name, new_inode, true, true)
1582                        }
1583                    };
1584
1585                {
1586                    let mut guard = cur_inode.write();
1587                    let Kind::Dir { entries, .. } = guard.deref_mut() else {
1588                        unreachable!("Attempted to insert special device into non-directory");
1589                    };
1590
1591                    if should_insert {
1592                        entries.insert(entry_name, new_inode.clone());
1593                    }
1594
1595                    if should_return {
1596                        // Special files cannot be traversed further, so return the inode directly.
1597                        if last_component {
1598                            return Ok(new_inode);
1599                        }
1600                        return Err(Errno::Notdir);
1601                    }
1602                }
1603
1604                // Assign current inode and leave 'component_loop
1605                // Note: this is a shortcut for doing next iteration matching
1606                // Kind::Dir for same cur_inode and finding there matching entry
1607                // that we just inserted, and exiting 'component_lookup.
1608                cur_inode = new_inode;
1609                break 'component_lookup;
1610            } // end of 'component_lookup loop
1611
1612            // 3. Follow Symbolic Links
1613            // --
1614            // We continue with Symlink resolution unless...
1615            if last_component && !follow_symlinks {
1616                // ...this symlink is the very last component of the path to
1617                // resolve, and symlink following is off,
1618                // ...or this is not a symlink at all
1619                continue 'path_iter;
1620            }
1621
1622            // The cur_inode can be a symlink (Kind::Symlink) or something else.
1623            let (symlink_kind, path_to_symlink, relative_path) = {
1624                let guard = cur_inode.read();
1625                let Kind::Symlink {
1626                    symlink_kind,
1627                    path_to_symlink,
1628                    relative_path,
1629                } = guard.deref()
1630                else {
1631                    // not a symlink, so we continue with next path component
1632                    continue 'path_iter;
1633                };
1634                (
1635                    *symlink_kind,
1636                    path_to_symlink.clone(),
1637                    relative_path.clone(),
1638                )
1639            };
1640
1641            let (new_base_fd, new_path) =
1642                self.resolve_symlink_target_path(symlink_kind, &path_to_symlink, &relative_path)?;
1643            let new_base_inode = self.get_fd_inode(new_base_fd)?;
1644            let new_path = PosixPath::from_path(&new_path).as_str().to_owned();
1645
1646            // We want to always follow symlinks unless we're resolving very
1647            // last path component, then and only then we want to stop symlink
1648            // following if it was originally off.
1649            let follow_symlinks_inner = !last_component || follow_symlinks;
1650
1651            debug!("Following symlink recursively");
1652            *symlink_count += 1;
1653            if *symlink_count > MAX_SYMLINKS {
1654                return Err(Errno::Loop);
1655            }
1656            let symlink_inode = self.get_inode_at_path_inner(
1657                inodes,
1658                new_base_inode,
1659                &new_path,
1660                symlink_count,
1661                follow_symlinks_inner,
1662            )?;
1663
1664            // The rest of the path resolution will be relative to resolved
1665            // symlink target.
1666            cur_inode = symlink_inode;
1667        }
1668
1669        Ok(cur_inode)
1670    }
1671
1672    pub(crate) fn resolve_symlink_target_path(
1673        &self,
1674        symlink_kind: SymlinkKind,
1675        path_to_symlink: &Path,
1676        relative_path: &Path,
1677    ) -> Result<(WasiFd, PathBuf), Errno> {
1678        let relative_posix = PosixPath::from_path(relative_path);
1679        if matches!(symlink_kind, SymlinkKind::Virtual) && relative_posix.is_absolute() {
1680            return Ok((VIRTUAL_ROOT_FD, relative_path.to_owned()));
1681        }
1682
1683        let symlink_parent = match symlink_kind {
1684            SymlinkKind::Virtual => PosixPath::from_path(path_to_symlink)
1685                .parent()
1686                .into_path_buf(),
1687            SymlinkKind::Backing => {
1688                let symlink_path_buf =
1689                    PosixPath::new("/").join(&PosixPath::from_path(path_to_symlink));
1690                let symlink_path = symlink_path_buf.as_posix_path();
1691                let mount_entry = self
1692                    .root_fs
1693                    .root()
1694                    .mount_entries()
1695                    .into_iter()
1696                    .filter(|entry| {
1697                        symlink_path
1698                            .strip_prefix(&PosixPath::from_path(&entry.path))
1699                            .is_some()
1700                    })
1701                    .max_by_key(|entry| PosixPath::from_path(&entry.path).as_str().len())
1702                    .ok_or(Errno::Perm)?;
1703                let mount_path = mount_entry.path;
1704
1705                let symlink_relative = symlink_path
1706                    .strip_prefix(&PosixPath::from_path(&mount_path))
1707                    .ok_or(Errno::Perm)?;
1708                let symlink_parent = symlink_relative.parent().into_path_buf();
1709                let contained_target = if relative_posix.is_absolute() {
1710                    let stripped = relative_posix
1711                        .strip_prefix(&PosixPath::from_path(&mount_entry.source_path))
1712                        .ok_or(Errno::Perm)?;
1713                    PosixPathBuf::from(stripped.as_str().to_owned())
1714                } else {
1715                    PosixPathBuf::resolve_relative(
1716                        &PosixPath::from_path(&symlink_parent),
1717                        &relative_posix,
1718                        false,
1719                    )?
1720                };
1721
1722                return Ok((
1723                    VIRTUAL_ROOT_FD,
1724                    PosixPath::from_path(&mount_path)
1725                        .join(&contained_target.as_posix_path())
1726                        .into_path_buf(),
1727                ));
1728            }
1729        };
1730
1731        Ok((
1732            VIRTUAL_ROOT_FD,
1733            PosixPathBuf::resolve_relative(
1734                &PosixPath::from_path(&symlink_parent),
1735                &relative_posix,
1736                true,
1737            )?
1738            .into_path_buf(),
1739        ))
1740    }
1741
1742    pub(crate) fn rebase_symlink_location(&self, new_symlink_path: &Path) -> PathBuf {
1743        PosixPath::from_path(new_symlink_path)
1744            .strip_root_prefix()
1745            .into_path_buf()
1746    }
1747
1748    /// gets a host file from a base directory and a path
1749    /// this function ensures the fs remains sandboxed
1750    // NOTE: follow symlinks is super weird right now
1751    // even if it's false, it still follows symlinks, just not the last
1752    // symlink so
1753    // This will be resolved when we have tests asserting the correct behavior
1754    pub(crate) fn get_inode_at_path(
1755        &self,
1756        inodes: &WasiInodes,
1757        base: WasiFd,
1758        path: &str,
1759        follow_symlinks: bool,
1760    ) -> Result<InodeGuard, Errno> {
1761        let base_inode = self.get_fd_inode(base)?;
1762        let mut symlink_count = 0;
1763        self.get_inode_at_path_inner(
1764            inodes,
1765            base_inode,
1766            path,
1767            &mut symlink_count,
1768            follow_symlinks,
1769        )
1770    }
1771
1772    pub(crate) fn get_inode_at_path_from_inode(
1773        &self,
1774        inodes: &WasiInodes,
1775        base_inode: InodeGuard,
1776        path: &str,
1777        follow_symlinks: bool,
1778    ) -> Result<InodeGuard, Errno> {
1779        let mut symlink_count = 0;
1780        self.get_inode_at_path_inner(
1781            inodes,
1782            base_inode,
1783            path,
1784            &mut symlink_count,
1785            follow_symlinks,
1786        )
1787    }
1788
1789    /// Returns the parent Dir or Root that the file at a given path is in and the file name
1790    /// stripped off
1791    pub(crate) fn get_parent_inode_at_path(
1792        &self,
1793        inodes: &WasiInodes,
1794        base: WasiFd,
1795        path: &Path,
1796        follow_symlinks: bool,
1797    ) -> Result<(InodeGuard, String), Errno> {
1798        let (parent_dir, new_entity_name) = PosixPath::from_path(path).parent_path_and_name()?;
1799        if parent_dir.as_str().is_empty() {
1800            return self.get_fd_inode(base).map(|v| (v, new_entity_name));
1801        }
1802        self.get_inode_at_path(inodes, base, parent_dir.as_str(), follow_symlinks)
1803            .map(|v| (v, new_entity_name))
1804    }
1805
1806    pub fn get_fd(&self, fd: WasiFd) -> Result<Fd, Errno> {
1807        let ret = self
1808            .fd_map
1809            .read()
1810            .unwrap()
1811            .get(fd)
1812            .ok_or(Errno::Badf)
1813            .cloned();
1814
1815        if ret.is_err() && fd == VIRTUAL_ROOT_FD {
1816            Ok(Self::virtual_root_fd(self.root_inode.clone()))
1817        } else {
1818            ret
1819        }
1820    }
1821
1822    pub fn get_fd_inode(&self, fd: WasiFd) -> Result<InodeGuard, Errno> {
1823        // see `VIRTUAL_ROOT_FD` for details as to why this exists
1824        if fd == VIRTUAL_ROOT_FD {
1825            return Ok(self.root_inode.clone());
1826        }
1827        self.fd_map
1828            .read()
1829            .unwrap()
1830            .get(fd)
1831            .ok_or(Errno::Badf)
1832            .map(|a| a.inode.clone())
1833    }
1834
1835    pub fn filestat_fd(&self, fd: WasiFd) -> Result<Filestat, Errno> {
1836        let inode = self.get_fd_inode(fd)?;
1837        let guard = inode.stat.read().unwrap();
1838        Ok(*guard.deref())
1839    }
1840
1841    pub fn fdstat(&self, fd: WasiFd) -> Result<Fdstat, Errno> {
1842        match fd {
1843            __WASI_STDIN_FILENO => {
1844                return Ok(Fdstat {
1845                    fs_filetype: Filetype::CharacterDevice,
1846                    fs_flags: Fdflags::empty(),
1847                    fs_rights_base: STDIN_DEFAULT_RIGHTS,
1848                    fs_rights_inheriting: Rights::empty(),
1849                });
1850            }
1851            __WASI_STDOUT_FILENO => {
1852                return Ok(Fdstat {
1853                    fs_filetype: Filetype::CharacterDevice,
1854                    fs_flags: Fdflags::APPEND,
1855                    fs_rights_base: STDOUT_DEFAULT_RIGHTS,
1856                    fs_rights_inheriting: Rights::empty(),
1857                });
1858            }
1859            __WASI_STDERR_FILENO => {
1860                return Ok(Fdstat {
1861                    fs_filetype: Filetype::CharacterDevice,
1862                    fs_flags: Fdflags::APPEND,
1863                    fs_rights_base: STDERR_DEFAULT_RIGHTS,
1864                    fs_rights_inheriting: Rights::empty(),
1865                });
1866            }
1867            VIRTUAL_ROOT_FD => {
1868                return Ok(Fdstat {
1869                    fs_filetype: Filetype::Directory,
1870                    fs_flags: Fdflags::empty(),
1871                    // TODO: fix this
1872                    fs_rights_base: ALL_RIGHTS,
1873                    fs_rights_inheriting: ALL_RIGHTS,
1874                });
1875            }
1876            _ => (),
1877        }
1878        let fd = self.get_fd(fd)?;
1879
1880        let guard = fd.inode.read();
1881        let deref = guard.deref();
1882        Ok(Fdstat {
1883            fs_filetype: match deref {
1884                Kind::File { .. } => Filetype::RegularFile,
1885                Kind::Dir { .. } => Filetype::Directory,
1886                Kind::Symlink { .. } => Filetype::SymbolicLink,
1887                Kind::Socket { socket } => match &socket.inner.protected.read().unwrap().kind {
1888                    InodeSocketKind::TcpStream { .. } => Filetype::SocketStream,
1889                    InodeSocketKind::Raw { .. } => Filetype::SocketRaw,
1890                    InodeSocketKind::PreSocket { props, .. } => match props.ty {
1891                        Socktype::Stream => Filetype::SocketStream,
1892                        Socktype::Dgram => Filetype::SocketDgram,
1893                        Socktype::Raw => Filetype::SocketRaw,
1894                        Socktype::Seqpacket => Filetype::SocketSeqpacket,
1895                        _ => Filetype::Unknown,
1896                    },
1897                    _ => Filetype::Unknown,
1898                },
1899                _ => Filetype::Unknown,
1900            },
1901            fs_flags: fd.inner.flags,
1902            fs_rights_base: fd.inner.rights,
1903            fs_rights_inheriting: fd.inner.rights_inheriting, // TODO(lachlan): Is this right?
1904        })
1905    }
1906
1907    pub fn prestat_fd(&self, fd: WasiFd) -> Result<Prestat, Errno> {
1908        let inode = self.get_fd_inode(fd)?;
1909        //trace!("in prestat_fd {:?}", self.get_fd(fd)?);
1910
1911        if inode.is_preopened {
1912            Ok(self.prestat_fd_inner(inode.deref()))
1913        } else {
1914            Err(Errno::Badf)
1915        }
1916    }
1917
1918    pub(crate) fn prestat_fd_inner(&self, inode_val: &InodeVal) -> Prestat {
1919        Prestat {
1920            pr_type: Preopentype::Dir,
1921            u: PrestatEnum::Dir {
1922                // WASI spec: pr_name_len is the length of the path string, NOT including null terminator
1923                pr_name_len: inode_val.name.read().unwrap().len() as u32,
1924            }
1925            .untagged(),
1926        }
1927    }
1928
1929    /// Creates an inode and inserts it given a Kind and some extra data
1930    pub(crate) fn create_inode(
1931        &self,
1932        inodes: &WasiInodes,
1933        kind: Kind,
1934        is_preopened: bool,
1935        name: String,
1936    ) -> Result<InodeGuard, Errno> {
1937        let stat = self.get_stat_for_kind(&kind)?;
1938        Ok(self.create_inode_with_stat(inodes, kind, is_preopened, name.into(), stat))
1939    }
1940
1941    /// Creates an inode and inserts it given a Kind, does not assume the file exists.
1942    pub(crate) fn create_inode_with_default_stat(
1943        &self,
1944        inodes: &WasiInodes,
1945        kind: Kind,
1946        is_preopened: bool,
1947        name: Cow<'static, str>,
1948    ) -> InodeGuard {
1949        let stat = Filestat::default();
1950        self.create_inode_with_stat(inodes, kind, is_preopened, name, stat)
1951    }
1952
1953    /// Creates an inode with the given filestat and inserts it.
1954    pub(crate) fn create_inode_with_stat(
1955        &self,
1956        inodes: &WasiInodes,
1957        kind: Kind,
1958        is_preopened: bool,
1959        name: Cow<'static, str>,
1960        mut stat: Filestat,
1961    ) -> InodeGuard {
1962        match &kind {
1963            Kind::File {
1964                handle: Some(handle),
1965                ..
1966            } => {
1967                let guard = handle.read().unwrap();
1968                stat.st_size = guard.size();
1969            }
1970            Kind::Buffer { buffer } => {
1971                stat.st_size = buffer.len() as u64;
1972            }
1973            _ => {}
1974        }
1975
1976        let inode_key: Cow<'_, str> = match &kind {
1977            Kind::File { path, .. } | Kind::Dir { path, .. } => {
1978                let path_str = path.to_string_lossy();
1979                if path_str.is_empty() {
1980                    Cow::Borrowed(name.as_ref())
1981                } else {
1982                    path_str
1983                }
1984            }
1985            Kind::Symlink {
1986                path_to_symlink, ..
1987            } => {
1988                let path_str = path_to_symlink.to_string_lossy();
1989                if path_str.is_empty() {
1990                    Cow::Borrowed(name.as_ref())
1991                } else {
1992                    path_str
1993                }
1994            }
1995            _ => Cow::Borrowed(name.as_ref()),
1996        };
1997
1998        let st_ino = Inode::from_path(&inode_key);
1999        stat.st_ino = st_ino.as_u64();
2000
2001        inodes.add_inode_val(InodeVal {
2002            stat: RwLock::new(stat),
2003            is_preopened,
2004            name: RwLock::new(name),
2005            kind: RwLock::new(kind),
2006        })
2007    }
2008
2009    fn make_fd(
2010        rights: Rights,
2011        rights_inheriting: Rights,
2012        fs_flags: Fdflags,
2013        fd_flags: Fdflagsext,
2014        open_flags: u16,
2015        inode: InodeGuard,
2016        idx: Option<WasiFd>,
2017    ) -> Fd {
2018        let is_stdio = matches!(
2019            idx,
2020            Some(__WASI_STDIN_FILENO) | Some(__WASI_STDOUT_FILENO) | Some(__WASI_STDERR_FILENO)
2021        );
2022        Fd {
2023            inner: FdInner {
2024                rights,
2025                rights_inheriting,
2026                flags: fs_flags,
2027                offset: Arc::new(AtomicU64::new(0)),
2028                fd_flags,
2029            },
2030            open_flags,
2031            inode,
2032            is_stdio,
2033        }
2034    }
2035
2036    /// Insert a new fd into an already write-locked fd map.
2037    ///
2038    /// Lock order: callers must hold `fd_map.write()` and must not hold any inode
2039    /// lock while acquiring the fd map lock.
2040    #[allow(clippy::too_many_arguments)]
2041    pub(crate) fn insert_fd_locked(
2042        fd_map: &mut FdList,
2043        rights: Rights,
2044        rights_inheriting: Rights,
2045        fs_flags: Fdflags,
2046        fd_flags: Fdflagsext,
2047        open_flags: u16,
2048        inode: InodeGuard,
2049        idx: Option<WasiFd>,
2050        exclusive: bool,
2051    ) -> Result<WasiFd, Errno> {
2052        let fd = Self::make_fd(
2053            rights,
2054            rights_inheriting,
2055            fs_flags,
2056            fd_flags,
2057            open_flags,
2058            inode,
2059            idx,
2060        );
2061
2062        match idx {
2063            Some(idx) => {
2064                if idx > MAX_FD {
2065                    return Err(Errno::Badf);
2066                }
2067                if fd_map.insert(exclusive, idx, fd) {
2068                    Ok(idx)
2069                } else {
2070                    Err(Errno::Exist)
2071                }
2072            }
2073            None => Ok(fd_map.insert_first_free(fd)),
2074        }
2075    }
2076
2077    /// Duplicate an fd into an already write-locked fd map.
2078    pub(crate) fn clone_fd_locked(
2079        fs: &WasiFs,
2080        fd_map: &mut FdList,
2081        fd: WasiFd,
2082        min_result_fd: WasiFd,
2083        cloexec: Option<bool>,
2084    ) -> Result<WasiFd, Errno> {
2085        let fd = Self::get_fd_from_locked_map(fs, fd_map, fd)?;
2086        Self::ensure_file_handle_present(&fd)?;
2087        if min_result_fd > MAX_FD {
2088            return Err(Errno::Inval);
2089        }
2090        Ok(fd_map.insert_first_free_after(
2091            Fd {
2092                inner: FdInner {
2093                    rights: fd.inner.rights,
2094                    rights_inheriting: fd.inner.rights_inheriting,
2095                    flags: fd.inner.flags,
2096                    offset: fd.inner.offset.clone(),
2097                    fd_flags: match cloexec {
2098                        None => fd.inner.fd_flags,
2099                        Some(cloexec) => {
2100                            let mut f = fd.inner.fd_flags;
2101                            f.set(Fdflagsext::CLOEXEC, cloexec);
2102                            f
2103                        }
2104                    },
2105                },
2106                open_flags: fd.open_flags,
2107                inode: fd.inode,
2108                is_stdio: fd.is_stdio,
2109            },
2110            min_result_fd,
2111        ))
2112    }
2113
2114    /// Resolve an fd from a write-locked map (includes [`VIRTUAL_ROOT_FD`] fallback).
2115    pub(crate) fn get_fd_from_locked_map(
2116        fs: &WasiFs,
2117        fd_map: &FdList,
2118        fd: WasiFd,
2119    ) -> Result<Fd, Errno> {
2120        match fd_map.get(fd) {
2121            Some(fd) => Ok(fd.clone()),
2122            None if fd == VIRTUAL_ROOT_FD => Ok(Self::virtual_root_fd(fs.root_inode.clone())),
2123            None => Err(Errno::Badf),
2124        }
2125    }
2126
2127    fn virtual_root_fd(root_inode: InodeGuard) -> Fd {
2128        Fd {
2129            inner: FdInner {
2130                rights: ALL_RIGHTS,
2131                rights_inheriting: ALL_RIGHTS,
2132                flags: Fdflags::empty(),
2133                offset: Arc::new(AtomicU64::new(0)),
2134                fd_flags: Fdflagsext::empty(),
2135            },
2136            open_flags: 0,
2137            inode: root_inode,
2138            is_stdio: false,
2139        }
2140    }
2141
2142    fn ensure_file_handle_present(fd: &Fd) -> Result<(), Errno> {
2143        let guard = fd.inode.read();
2144        match guard.deref() {
2145            Kind::File { handle: None, .. } => Err(Errno::Badf),
2146            _ => Ok(()),
2147        }
2148    }
2149
2150    /// POSIX dup2: copy `src` onto exact slot `dst`, replacing any existing entry.
2151    ///
2152    /// Holds `fd_map.write()` for the full remove+insert. Returns a flush target for
2153    /// the replaced `dst` entry (if any), captured while the lock is held and before
2154    /// `remove` calls `drop_one_handle`, which may clear the inode's file handle.
2155    pub(crate) fn dup2_at(
2156        &self,
2157        src: WasiFd,
2158        dst: WasiFd,
2159    ) -> Result<Option<VirtualFileLock>, Errno> {
2160        if dst > MAX_FD {
2161            return Err(Errno::Badf);
2162        }
2163
2164        let flush_target = {
2165            let mut fd_map = self.fd_map.write().unwrap();
2166
2167            let fd_entry = fd_map.get(src).ok_or(Errno::Badf)?;
2168            Self::ensure_file_handle_present(fd_entry)?;
2169
2170            if src == dst {
2171                return Ok(None);
2172            }
2173
2174            if let Some(target_fd) = fd_map.get(dst)
2175                && !target_fd.is_stdio
2176                && target_fd.inode.is_preopened
2177            {
2178                warn!("Refusing dup2({src}, {dst}) because FD {dst} is pre-opened");
2179                return Err(Errno::Notsup);
2180            }
2181
2182            let new_fd_entry = Fd {
2183                inner: FdInner {
2184                    offset: fd_entry.inner.offset.clone(),
2185                    rights: fd_entry.inner.rights_inheriting,
2186                    fd_flags: {
2187                        let mut f = fd_entry.inner.fd_flags;
2188                        f.set(Fdflagsext::CLOEXEC, false);
2189                        f
2190                    },
2191                    ..fd_entry.inner
2192                },
2193                inode: fd_entry.inode.clone(),
2194                ..*fd_entry
2195            };
2196
2197            let flush_target = fd_map
2198                .get(dst)
2199                .and_then(|fd| Self::file_flush_target(&fd.inode));
2200
2201            fd_map.remove(dst);
2202
2203            if !fd_map.insert(true, dst, new_fd_entry) {
2204                panic!("Internal error: expected FD {dst} to be free after remove in dup2_at");
2205            }
2206
2207            flush_target
2208        };
2209
2210        Ok(flush_target)
2211    }
2212
2213    pub fn create_fd(
2214        &self,
2215        rights: Rights,
2216        rights_inheriting: Rights,
2217        fs_flags: Fdflags,
2218        fd_flags: Fdflagsext,
2219        open_flags: u16,
2220        inode: InodeGuard,
2221    ) -> Result<WasiFd, Errno> {
2222        self.create_fd_ext(
2223            rights,
2224            rights_inheriting,
2225            fs_flags,
2226            fd_flags,
2227            open_flags,
2228            inode,
2229            None,
2230            false,
2231        )
2232    }
2233
2234    #[allow(clippy::too_many_arguments)]
2235    pub fn with_fd(
2236        &self,
2237        rights: Rights,
2238        rights_inheriting: Rights,
2239        fs_flags: Fdflags,
2240        fd_flags: Fdflagsext,
2241        open_flags: u16,
2242        inode: InodeGuard,
2243        idx: WasiFd,
2244    ) -> Result<(), Errno> {
2245        self.create_fd_ext(
2246            rights,
2247            rights_inheriting,
2248            fs_flags,
2249            fd_flags,
2250            open_flags,
2251            inode,
2252            Some(idx),
2253            true,
2254        )?;
2255        Ok(())
2256    }
2257
2258    #[allow(clippy::too_many_arguments)]
2259    pub fn create_fd_ext(
2260        &self,
2261        rights: Rights,
2262        rights_inheriting: Rights,
2263        fs_flags: Fdflags,
2264        fd_flags: Fdflagsext,
2265        open_flags: u16,
2266        inode: InodeGuard,
2267        idx: Option<WasiFd>,
2268        exclusive: bool,
2269    ) -> Result<WasiFd, Errno> {
2270        let mut fd_map = self.fd_map.write().unwrap();
2271        Self::insert_fd_locked(
2272            &mut fd_map,
2273            rights,
2274            rights_inheriting,
2275            fs_flags,
2276            fd_flags,
2277            open_flags,
2278            inode,
2279            idx,
2280            exclusive,
2281        )
2282    }
2283
2284    pub fn clone_fd(&self, fd: WasiFd) -> Result<WasiFd, Errno> {
2285        self.clone_fd_ext(fd, 0, None)
2286    }
2287
2288    pub fn clone_fd_ext(
2289        &self,
2290        fd: WasiFd,
2291        min_result_fd: WasiFd,
2292        cloexec: Option<bool>,
2293    ) -> Result<WasiFd, Errno> {
2294        let mut fd_map = self.fd_map.write().unwrap();
2295        Self::clone_fd_locked(self, &mut fd_map, fd, min_result_fd, cloexec)
2296    }
2297
2298    /// Low level function to remove an inode, that is it deletes the WASI FS's
2299    /// knowledge of a file.
2300    ///
2301    /// This function returns the inode if it existed and was removed.
2302    ///
2303    /// # Safety
2304    /// - The caller must ensure that all references to the specified inode have
2305    ///   been removed from the filesystem.
2306    pub unsafe fn remove_inode(&self, inodes: &WasiInodes, ino: Inode) -> Option<Arc<InodeVal>> {
2307        let mut guard = inodes.protected.write().unwrap();
2308        guard.lookup.remove(&ino).and_then(|a| Weak::upgrade(&a))
2309    }
2310
2311    pub(crate) fn create_stdout(&self, inodes: &WasiInodes) {
2312        self.create_std_dev_inner(
2313            inodes,
2314            Box::<Stdout>::default(),
2315            "stdout",
2316            __WASI_STDOUT_FILENO,
2317            STDOUT_DEFAULT_RIGHTS,
2318            Fdflags::APPEND,
2319            FS_STDOUT_INO,
2320        );
2321    }
2322
2323    pub(crate) fn create_stdin(&self, inodes: &WasiInodes) {
2324        self.create_std_dev_inner(
2325            inodes,
2326            Box::<Stdin>::default(),
2327            "stdin",
2328            __WASI_STDIN_FILENO,
2329            STDIN_DEFAULT_RIGHTS,
2330            Fdflags::empty(),
2331            FS_STDIN_INO,
2332        );
2333    }
2334
2335    pub(crate) fn create_stderr(&self, inodes: &WasiInodes) {
2336        self.create_std_dev_inner(
2337            inodes,
2338            Box::<Stderr>::default(),
2339            "stderr",
2340            __WASI_STDERR_FILENO,
2341            STDERR_DEFAULT_RIGHTS,
2342            Fdflags::APPEND,
2343            FS_STDERR_INO,
2344        );
2345    }
2346
2347    pub(crate) fn create_rootfd(&self) -> Result<(), String> {
2348        // create virtual root
2349        let all_rights = ALL_RIGHTS;
2350        // TODO: make this a list of positive rights instead of negative ones
2351        // root gets all right for now
2352        let root_rights = all_rights
2353            /*
2354            & (!Rights::FD_WRITE)
2355            & (!Rights::FD_ALLOCATE)
2356            & (!Rights::PATH_CREATE_DIRECTORY)
2357            & (!Rights::PATH_CREATE_FILE)
2358            & (!Rights::PATH_LINK_SOURCE)
2359            & (!Rights::PATH_RENAME_SOURCE)
2360            & (!Rights::PATH_RENAME_TARGET)
2361            & (!Rights::PATH_FILESTAT_SET_SIZE)
2362            & (!Rights::PATH_FILESTAT_SET_TIMES)
2363            & (!Rights::FD_FILESTAT_SET_SIZE)
2364            & (!Rights::FD_FILESTAT_SET_TIMES)
2365            & (!Rights::PATH_SYMLINK)
2366            & (!Rights::PATH_UNLINK_FILE)
2367            & (!Rights::PATH_REMOVE_DIRECTORY)
2368            */;
2369        let fd = self
2370            .create_fd(
2371                root_rights,
2372                root_rights,
2373                Fdflags::empty(),
2374                Fdflagsext::empty(),
2375                Fd::READ,
2376                self.root_inode.clone(),
2377            )
2378            .map_err(|e| format!("Could not create root fd: {e}"))?;
2379        self.preopen_fds.write().unwrap().push(fd);
2380        Ok(())
2381    }
2382
2383    pub(crate) fn create_preopens(
2384        &self,
2385        inodes: &WasiInodes,
2386        ignore_duplicates: bool,
2387    ) -> Result<(), String> {
2388        for preopen_name in self.init_vfs_preopens.iter() {
2389            let kind = Kind::Dir {
2390                parent: self.root_inode.downgrade(),
2391                path: PathBuf::from(preopen_name),
2392                entries: Default::default(),
2393            };
2394            let rights = Rights::FD_ADVISE
2395                | Rights::FD_TELL
2396                | Rights::FD_SEEK
2397                | Rights::FD_READ
2398                | Rights::PATH_OPEN
2399                | Rights::FD_READDIR
2400                | Rights::PATH_READLINK
2401                | Rights::PATH_FILESTAT_GET
2402                | Rights::FD_FILESTAT_GET
2403                | Rights::PATH_LINK_SOURCE
2404                | Rights::PATH_RENAME_SOURCE
2405                | Rights::POLL_FD_READWRITE
2406                | Rights::SOCK_SHUTDOWN;
2407            let inode = self
2408                .create_inode(inodes, kind, true, preopen_name.clone())
2409                .map_err(|e| {
2410                    format!(
2411                        "Failed to create inode for preopened dir (name `{preopen_name}`): WASI error code: {e}",
2412                    )
2413                })?;
2414            let fd_flags = Fd::READ;
2415            let fd = self
2416                .create_fd(
2417                    rights,
2418                    rights,
2419                    Fdflags::empty(),
2420                    Fdflagsext::empty(),
2421                    fd_flags,
2422                    inode.clone(),
2423                )
2424                .map_err(|e| format!("Could not open fd for file {preopen_name:?}: {e}"))?;
2425            {
2426                let mut guard = self.root_inode.write();
2427                if let Kind::Root { entries } = guard.deref_mut() {
2428                    let existing_entry = entries.insert(preopen_name.clone(), inode);
2429                    if existing_entry.is_some() && !ignore_duplicates {
2430                        return Err(format!("Found duplicate entry for alias `{preopen_name}`"));
2431                    }
2432                }
2433            }
2434            self.preopen_fds.write().unwrap().push(fd);
2435        }
2436
2437        for PreopenedDir {
2438            path,
2439            alias,
2440            read,
2441            write,
2442            create,
2443        } in self.init_preopens.iter()
2444        {
2445            debug!(
2446                "Attempting to preopen {} with alias {:?}",
2447                &path.to_string_lossy(),
2448                &alias
2449            );
2450            let cur_dir_metadata = self
2451                .root_fs
2452                .metadata(path)
2453                .map_err(|e| format!("Could not get metadata for file {path:?}: {e}"))?;
2454
2455            let kind = if cur_dir_metadata.is_dir() {
2456                Kind::Dir {
2457                    parent: self.root_inode.downgrade(),
2458                    path: path.clone(),
2459                    entries: Default::default(),
2460                }
2461            } else {
2462                return Err(format!(
2463                    "WASI only supports pre-opened directories right now; found \"{}\"",
2464                    path.to_string_lossy()
2465                ));
2466            };
2467
2468            let rights = {
2469                // TODO: review tell' and fd_readwrite
2470                let mut rights = Rights::FD_ADVISE | Rights::FD_TELL | Rights::FD_SEEK;
2471                if *read {
2472                    rights |= Rights::FD_READ
2473                        | Rights::PATH_OPEN
2474                        | Rights::FD_READDIR
2475                        | Rights::PATH_READLINK
2476                        | Rights::PATH_FILESTAT_GET
2477                        | Rights::FD_FILESTAT_GET
2478                        | Rights::PATH_LINK_SOURCE
2479                        | Rights::PATH_RENAME_SOURCE
2480                        | Rights::POLL_FD_READWRITE
2481                        | Rights::SOCK_SHUTDOWN;
2482                }
2483                if *write {
2484                    rights |= Rights::FD_DATASYNC
2485                        | Rights::FD_FDSTAT_SET_FLAGS
2486                        | Rights::FD_WRITE
2487                        | Rights::FD_SYNC
2488                        | Rights::FD_ALLOCATE
2489                        | Rights::PATH_OPEN
2490                        | Rights::PATH_RENAME_TARGET
2491                        | Rights::PATH_FILESTAT_SET_SIZE
2492                        | Rights::PATH_FILESTAT_SET_TIMES
2493                        | Rights::FD_FILESTAT_SET_SIZE
2494                        | Rights::FD_FILESTAT_SET_TIMES
2495                        | Rights::PATH_REMOVE_DIRECTORY
2496                        | Rights::PATH_UNLINK_FILE
2497                        | Rights::POLL_FD_READWRITE
2498                        | Rights::SOCK_SHUTDOWN;
2499                }
2500                if *create {
2501                    rights |= Rights::PATH_CREATE_DIRECTORY
2502                        | Rights::PATH_CREATE_FILE
2503                        | Rights::PATH_LINK_TARGET
2504                        | Rights::PATH_OPEN
2505                        | Rights::PATH_RENAME_TARGET
2506                        | Rights::PATH_SYMLINK;
2507                }
2508
2509                rights
2510            };
2511            let inode = if let Some(alias) = &alias {
2512                self.create_inode(inodes, kind, true, alias.clone())
2513            } else {
2514                self.create_inode(inodes, kind, true, path.to_string_lossy().into_owned())
2515            }
2516            .map_err(|e| {
2517                format!("Failed to create inode for preopened dir: WASI error code: {e}")
2518            })?;
2519            let fd_flags = {
2520                let mut fd_flags = 0;
2521                if *read {
2522                    fd_flags |= Fd::READ;
2523                }
2524                if *write {
2525                    // TODO: introduce API for finer grained control
2526                    fd_flags |= Fd::WRITE | Fd::APPEND | Fd::TRUNCATE;
2527                }
2528                if *create {
2529                    fd_flags |= Fd::CREATE;
2530                }
2531                fd_flags
2532            };
2533            let fd = self
2534                .create_fd(
2535                    rights,
2536                    rights,
2537                    Fdflags::empty(),
2538                    Fdflagsext::empty(),
2539                    fd_flags,
2540                    inode.clone(),
2541                )
2542                .map_err(|e| format!("Could not open fd for file {path:?}: {e}"))?;
2543            {
2544                let mut guard = self.root_inode.write();
2545                if let Kind::Root { entries } = guard.deref_mut() {
2546                    let key = if let Some(alias) = &alias {
2547                        alias.clone()
2548                    } else {
2549                        path.to_string_lossy().into_owned()
2550                    };
2551                    let existing_entry = entries.insert(key.clone(), inode);
2552                    if existing_entry.is_some() && !ignore_duplicates {
2553                        return Err(format!("Found duplicate entry for alias `{key}`"));
2554                    }
2555                }
2556            }
2557            self.preopen_fds.write().unwrap().push(fd);
2558        }
2559
2560        Ok(())
2561    }
2562
2563    #[allow(clippy::too_many_arguments)]
2564    pub(crate) fn create_std_dev_inner(
2565        &self,
2566        inodes: &WasiInodes,
2567        handle: Box<dyn VirtualFile + Send + Sync + 'static>,
2568        name: &'static str,
2569        raw_fd: WasiFd,
2570        rights: Rights,
2571        fd_flags: Fdflags,
2572        st_ino: Inode,
2573    ) {
2574        let inode = {
2575            let stat = Filestat {
2576                st_filetype: Filetype::CharacterDevice,
2577                st_ino: st_ino.as_u64(),
2578                ..Filestat::default()
2579            };
2580            let kind = Kind::File {
2581                fd: Some(raw_fd),
2582                handle: Some(Arc::new(RwLock::new(handle))),
2583                path: "".into(),
2584            };
2585            inodes.add_inode_val(InodeVal {
2586                stat: RwLock::new(stat),
2587                is_preopened: true,
2588                name: RwLock::new(name.to_string().into()),
2589                kind: RwLock::new(kind),
2590            })
2591        };
2592        self.fd_map.write().unwrap().insert(
2593            false,
2594            raw_fd,
2595            Fd {
2596                inner: FdInner {
2597                    rights,
2598                    rights_inheriting: Rights::empty(),
2599                    flags: fd_flags,
2600                    offset: Arc::new(AtomicU64::new(0)),
2601                    fd_flags: Fdflagsext::empty(),
2602                },
2603                // since we're not calling open on this, we don't need open flags
2604                open_flags: 0,
2605                inode,
2606                is_stdio: true,
2607            },
2608        );
2609    }
2610
2611    pub fn get_stat_for_kind(&self, kind: &Kind) -> Result<Filestat, Errno> {
2612        let md = match kind {
2613            Kind::File { handle, path, .. } => match handle {
2614                Some(wf) => {
2615                    let wf = wf.read().unwrap();
2616                    return Ok(Filestat {
2617                        st_filetype: Filetype::RegularFile,
2618                        st_ino: Inode::from_path(path.to_string_lossy().as_ref()).as_u64(),
2619                        st_size: wf.size(),
2620                        st_atim: wf.last_accessed(),
2621                        st_mtim: wf.last_modified(),
2622                        st_ctim: wf.created_time(),
2623
2624                        ..Filestat::default()
2625                    });
2626                }
2627                None => self
2628                    .root_fs
2629                    .metadata(path)
2630                    .map_err(fs_error_into_wasi_err)?,
2631            },
2632            Kind::Dir { path, .. } => self
2633                .root_fs
2634                .metadata(path)
2635                .map_err(fs_error_into_wasi_err)?,
2636            Kind::Symlink {
2637                path_to_symlink,
2638                relative_path,
2639                ..
2640            } => {
2641                let symlink_path = PosixPath::new("/")
2642                    .join(&PosixPath::from_path(path_to_symlink))
2643                    .into_path_buf();
2644
2645                match self.root_fs.symlink_metadata(&symlink_path) {
2646                    Ok(md) => md,
2647                    Err(FsError::EntryNotFound)
2648                        if self.ephemeral_symlink_at(&symlink_path).is_some() =>
2649                    {
2650                        return Ok(Filestat {
2651                            st_filetype: Filetype::SymbolicLink,
2652                            st_size: relative_path.as_os_str().len() as u64,
2653                            ..Filestat::default()
2654                        });
2655                    }
2656                    Err(err) => return Err(fs_error_into_wasi_err(err)),
2657                }
2658            }
2659            _ => return Err(Errno::Io),
2660        };
2661        Ok(Filestat {
2662            st_filetype: virtual_file_type_to_wasi_file_type(md.file_type()),
2663            st_size: md.len(),
2664            st_atim: md.accessed(),
2665            st_mtim: md.modified(),
2666            st_ctim: md.created(),
2667            ..Filestat::default()
2668        })
2669    }
2670
2671    /// Closes an open FD under `fd_map.write()`, capturing a file handle for
2672    /// post-close flush while the map lock is held.
2673    ///
2674    /// Lock order: `fd_map` write, then inode read (never the reverse).
2675    pub(crate) fn close_fd_and_capture_flush(&self, fd: WasiFd) -> CloseFdOutcome {
2676        let mut fd_map = self.fd_map.write().unwrap();
2677        Self::close_fd_locked(&mut fd_map, fd)
2678    }
2679
2680    /// Closes an open FD in an already write-locked fd map.
2681    fn close_fd_locked(fd_map: &mut FdList, fd: WasiFd) -> CloseFdOutcome {
2682        let Some(fd_ref) = fd_map.get(fd) else {
2683            trace!(%fd, "closing file descriptor failed - {}", Errno::Badf);
2684            return CloseFdOutcome::not_found();
2685        };
2686
2687        if !fd_ref.is_stdio && fd_ref.inode.is_preopened {
2688            return CloseFdOutcome {
2689                skipped_preopen: true,
2690                removed: false,
2691                flush_target: None,
2692            };
2693        }
2694
2695        let flush_target = Self::file_flush_target(&fd_ref.inode);
2696
2697        match fd_map.remove(fd) {
2698            Some(fd_ref) => {
2699                let inode = fd_ref.inode.ino().as_u64();
2700                let ref_cnt = fd_ref.inode.ref_cnt();
2701                if ref_cnt == 1 {
2702                    trace!(%fd, %inode, %ref_cnt, "closing file descriptor");
2703                } else {
2704                    trace!(%fd, %inode, %ref_cnt, "weakening file descriptor");
2705                }
2706            }
2707            None => {
2708                trace!(%fd, "closing file descriptor failed - {}", Errno::Badf);
2709                return CloseFdOutcome::not_found();
2710            }
2711        }
2712
2713        CloseFdOutcome {
2714            skipped_preopen: false,
2715            removed: true,
2716            flush_target,
2717        }
2718    }
2719
2720    pub(crate) async fn flush_file_best_effort(file: VirtualFileLock) {
2721        let result = FlushPoller { file }.await;
2722        match result {
2723            Ok(())
2724            | Err(Errno::Isdir)
2725            | Err(Errno::Io)
2726            | Err(Errno::Access)
2727            // EINVAL is returned by e.g. pipe-backed stdio and is safe to ignore.
2728            | Err(Errno::Inval) => {}
2729            Err(err) => trace!("flush during bulk close failed - {}", err),
2730        }
2731    }
2732
2733    fn file_flush_target(inode: &InodeGuard) -> Option<VirtualFileLock> {
2734        let guard = inode.read();
2735        match guard.deref() {
2736            Kind::File {
2737                handle: Some(file), ..
2738            } => Some(file.clone()),
2739            _ => None,
2740        }
2741    }
2742
2743    /// Closes an open FD, handling all details such as FD being preopen
2744    pub(crate) fn close_fd(&self, fd: WasiFd) -> Result<(), Errno> {
2745        let _ = self.close_fd_and_capture_flush(fd);
2746        Ok(())
2747    }
2748}
2749
2750impl std::fmt::Debug for WasiFs {
2751    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2752        if let Ok(guard) = self.current_dir.try_lock() {
2753            write!(f, "current_dir={} ", guard.as_str())?;
2754        } else {
2755            write!(f, "current_dir=(locked) ")?;
2756        }
2757        if let Ok(guard) = self.fd_map.read() {
2758            write!(
2759                f,
2760                "next_fd={} max_fd={:?} ",
2761                guard.next_free_fd(),
2762                guard.last_fd()
2763            )?;
2764        } else {
2765            write!(f, "next_fd=(locked) max_fd=(locked) ")?;
2766        }
2767        write!(f, "{:?}", self.root_fs)
2768    }
2769}
2770
2771/// Returns the default filesystem backing
2772pub fn default_fs_backing() -> Arc<dyn virtual_fs::FileSystem + Send + Sync> {
2773    cfg_if::cfg_if! {
2774        if #[cfg(feature = "host-fs")] {
2775            Arc::new(virtual_fs::host_fs::FileSystem::new(tokio::runtime::Handle::current(), "/").unwrap())
2776        } else if #[cfg(not(feature = "host-fs"))] {
2777            Arc::<virtual_fs::mem_fs::FileSystem>::default()
2778        } else {
2779            Arc::<FallbackFileSystem>::default()
2780        }
2781    }
2782}
2783
2784#[derive(Debug, Default)]
2785pub struct FallbackFileSystem;
2786
2787impl FallbackFileSystem {
2788    fn fail() -> ! {
2789        panic!(
2790            "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`"
2791        );
2792    }
2793}
2794
2795impl FileSystem for FallbackFileSystem {
2796    fn readlink(&self, _path: &Path) -> virtual_fs::Result<PathBuf> {
2797        Self::fail()
2798    }
2799    fn read_dir(&self, _path: &Path) -> Result<virtual_fs::ReadDir, FsError> {
2800        Self::fail();
2801    }
2802    fn create_dir(&self, _path: &Path) -> Result<(), FsError> {
2803        Self::fail();
2804    }
2805    fn remove_dir(&self, _path: &Path) -> Result<(), FsError> {
2806        Self::fail();
2807    }
2808    fn rename<'a>(&'a self, _from: &Path, _to: &Path) -> BoxFuture<'a, Result<(), FsError>> {
2809        Self::fail();
2810    }
2811    fn metadata(&self, _path: &Path) -> Result<virtual_fs::Metadata, FsError> {
2812        Self::fail();
2813    }
2814    fn symlink_metadata(&self, _path: &Path) -> Result<virtual_fs::Metadata, FsError> {
2815        Self::fail();
2816    }
2817    fn remove_file(&self, _path: &Path) -> Result<(), FsError> {
2818        Self::fail();
2819    }
2820    fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
2821        Self::fail();
2822    }
2823}
2824
2825pub fn virtual_file_type_to_wasi_file_type(file_type: virtual_fs::FileType) -> Filetype {
2826    // TODO: handle other file types
2827    if file_type.is_dir() {
2828        Filetype::Directory
2829    } else if file_type.is_file() {
2830        Filetype::RegularFile
2831    } else if file_type.is_symlink() {
2832        Filetype::SymbolicLink
2833    } else {
2834        Filetype::Unknown
2835    }
2836}
2837
2838pub fn fs_error_from_wasi_err(err: Errno) -> FsError {
2839    match err {
2840        Errno::Badf => FsError::InvalidFd,
2841        Errno::Exist => FsError::AlreadyExists,
2842        Errno::Io => FsError::IOError,
2843        Errno::Addrinuse => FsError::AddressInUse,
2844        Errno::Addrnotavail => FsError::AddressNotAvailable,
2845        Errno::Pipe => FsError::BrokenPipe,
2846        Errno::Connaborted => FsError::ConnectionAborted,
2847        Errno::Connrefused => FsError::ConnectionRefused,
2848        Errno::Connreset => FsError::ConnectionReset,
2849        Errno::Intr => FsError::Interrupted,
2850        Errno::Inval => FsError::InvalidInput,
2851        Errno::Notconn => FsError::NotConnected,
2852        Errno::Nodev => FsError::NoDevice,
2853        Errno::Noent => FsError::EntryNotFound,
2854        Errno::Perm => FsError::PermissionDenied,
2855        Errno::Timedout => FsError::TimedOut,
2856        Errno::Proto => FsError::UnexpectedEof,
2857        Errno::Again => FsError::WouldBlock,
2858        Errno::Nospc => FsError::WriteZero,
2859        Errno::Notempty => FsError::DirectoryNotEmpty,
2860        _ => FsError::UnknownError,
2861    }
2862}
2863
2864pub fn fs_error_into_wasi_err(fs_error: FsError) -> Errno {
2865    match fs_error {
2866        FsError::AlreadyExists => Errno::Exist,
2867        FsError::AddressInUse => Errno::Addrinuse,
2868        FsError::AddressNotAvailable => Errno::Addrnotavail,
2869        FsError::BaseNotDirectory => Errno::Notdir,
2870        FsError::BrokenPipe => Errno::Pipe,
2871        FsError::ConnectionAborted => Errno::Connaborted,
2872        FsError::ConnectionRefused => Errno::Connrefused,
2873        FsError::ConnectionReset => Errno::Connreset,
2874        FsError::Interrupted => Errno::Intr,
2875        FsError::InvalidData => Errno::Io,
2876        FsError::InvalidFd => Errno::Badf,
2877        FsError::InvalidInput => Errno::Inval,
2878        FsError::IOError => Errno::Io,
2879        FsError::NoDevice => Errno::Nodev,
2880        FsError::NotAFile => Errno::Inval,
2881        FsError::NotConnected => Errno::Notconn,
2882        FsError::EntryNotFound => Errno::Noent,
2883        FsError::PermissionDenied => Errno::Perm,
2884        FsError::TimedOut => Errno::Timedout,
2885        FsError::UnexpectedEof => Errno::Proto,
2886        FsError::WouldBlock => Errno::Again,
2887        FsError::WriteZero => Errno::Nospc,
2888        FsError::DirectoryNotEmpty => Errno::Notempty,
2889        FsError::StorageFull => Errno::Overflow,
2890        FsError::Lock | FsError::UnknownError => Errno::Io,
2891        FsError::Unsupported => Errno::Notsup,
2892    }
2893}
2894
2895#[cfg(test)]
2896mod tests {
2897    use super::*;
2898    use once_cell::sync::OnceCell;
2899    use tempfile::tempdir;
2900    use virtual_fs::{RootFileSystemBuilder, TmpFileSystem};
2901    use wasmer::Engine;
2902    use wasmer_config::package::PackageId;
2903
2904    use crate::WasiEnvBuilder;
2905    use crate::bin_factory::{BinaryPackage, BinaryPackageMount, BinaryPackageMounts};
2906
2907    fn webc_symlink_fs() -> virtual_fs::WebcVolumeFileSystem {
2908        let timestamps = webc::v3::Timestamps::default();
2909        let dir = webc::v3::write::Directory::new(
2910            std::collections::BTreeMap::from_iter([
2911                (
2912                    webc::PathSegment::parse("target.txt").unwrap(),
2913                    webc::v3::write::DirEntry::File(webc::v3::write::FileEntry::borrowed(
2914                        b"target", timestamps,
2915                    )),
2916                ),
2917                (
2918                    webc::PathSegment::parse("link").unwrap(),
2919                    webc::v3::write::DirEntry::Symlink(webc::v3::write::SymlinkEntry::borrowed(
2920                        "target.txt",
2921                        timestamps,
2922                    )),
2923                ),
2924            ]),
2925            timestamps,
2926        );
2927        let manifest = webc::metadata::Manifest::default();
2928        let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
2929            .write_manifest(&manifest)
2930            .unwrap()
2931            .write_atoms(std::collections::BTreeMap::new())
2932            .unwrap();
2933        writer.write_volume("atom", dir).unwrap();
2934        let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
2935        let container = wasmer_package::utils::from_bytes(webc).unwrap();
2936        let volume = container.volumes()["atom"].clone();
2937
2938        virtual_fs::WebcVolumeFileSystem::new(volume)
2939    }
2940
2941    #[tokio::test]
2942    async fn test_relative_path_to_absolute() {
2943        let inodes = WasiInodes::new();
2944        let fs_backing =
2945            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
2946        let wasi_fs = WasiFs::new_init(fs_backing, &inodes, FS_ROOT_INO).unwrap();
2947
2948        // Test absolute path (returned as-is, no normalization)
2949        assert_eq!(
2950            wasi_fs.relative_path_to_absolute("/foo/bar".to_string()),
2951            "/foo/bar"
2952        );
2953        assert_eq!(wasi_fs.relative_path_to_absolute("/".to_string()), "/");
2954
2955        // Absolute paths with special components are not normalized
2956        assert_eq!(
2957            wasi_fs.relative_path_to_absolute("//foo//bar//".to_string()),
2958            "//foo//bar//"
2959        );
2960        assert_eq!(
2961            wasi_fs.relative_path_to_absolute("/a/b/./c".to_string()),
2962            "/a/b/./c"
2963        );
2964        assert_eq!(
2965            wasi_fs.relative_path_to_absolute("/a/b/../c".to_string()),
2966            "/a/b/../c"
2967        );
2968
2969        // Test relative path with root as current dir
2970        assert_eq!(
2971            wasi_fs.relative_path_to_absolute("foo/bar".to_string()),
2972            "/foo/bar"
2973        );
2974        assert_eq!(wasi_fs.relative_path_to_absolute("foo".to_string()), "/foo");
2975
2976        // Test with different current directory
2977        wasi_fs.set_current_dir("/home/user");
2978        assert_eq!(
2979            wasi_fs.relative_path_to_absolute("file.txt".to_string()),
2980            "/home/user/file.txt"
2981        );
2982        assert_eq!(
2983            wasi_fs.relative_path_to_absolute("dir/file.txt".to_string()),
2984            "/home/user/dir/file.txt"
2985        );
2986
2987        // Test relative paths with . and .. components
2988        wasi_fs.set_current_dir("/a/b/c");
2989        assert_eq!(
2990            wasi_fs.relative_path_to_absolute("./file.txt".to_string()),
2991            "/a/b/c/./file.txt"
2992        );
2993        assert_eq!(
2994            wasi_fs.relative_path_to_absolute("../file.txt".to_string()),
2995            "/a/b/c/../file.txt"
2996        );
2997        assert_eq!(
2998            wasi_fs.relative_path_to_absolute("../../file.txt".to_string()),
2999            "/a/b/c/../../file.txt"
3000        );
3001
3002        // Test edge cases
3003        assert_eq!(
3004            wasi_fs.relative_path_to_absolute(".".to_string()),
3005            "/a/b/c/."
3006        );
3007        assert_eq!(
3008            wasi_fs.relative_path_to_absolute("..".to_string()),
3009            "/a/b/c/.."
3010        );
3011        assert_eq!(wasi_fs.relative_path_to_absolute("".to_string()), "/a/b/c/");
3012
3013        // Test current directory with trailing slash
3014        wasi_fs.set_current_dir("/home/user/");
3015        assert_eq!(
3016            wasi_fs.relative_path_to_absolute("file.txt".to_string()),
3017            "/home/user/file.txt"
3018        );
3019
3020        // Test current directory without trailing slash
3021        wasi_fs.set_current_dir("/home/user");
3022        assert_eq!(
3023            wasi_fs.relative_path_to_absolute("file.txt".to_string()),
3024            "/home/user/file.txt"
3025        );
3026    }
3027
3028    #[cfg(feature = "host-fs")]
3029    #[tokio::test]
3030    async fn mapped_preopen_inode_paths_should_stay_in_guest_space() {
3031        let root_dir = tempdir().unwrap();
3032        let hamlet_dir = root_dir.path().join("hamlet");
3033        std::fs::create_dir_all(&hamlet_dir).unwrap();
3034
3035        let host_fs = virtual_fs::host_fs::FileSystem::new(
3036            tokio::runtime::Handle::current(),
3037            root_dir.path(),
3038        )
3039        .unwrap();
3040
3041        let init = WasiEnvBuilder::new("test_prog")
3042            .engine(Engine::default())
3043            .fs(Arc::new(host_fs) as Arc<dyn FileSystem + Send + Sync>)
3044            .map_dir("hamlet", "/hamlet")
3045            .unwrap()
3046            .build_init()
3047            .unwrap();
3048
3049        let preopen_inode = {
3050            let guard = init.state.fs.root_inode.read();
3051            let Kind::Root { entries } = guard.deref() else {
3052                panic!("expected root inode");
3053            };
3054            entries.get("hamlet").unwrap().clone()
3055        };
3056        let guard = preopen_inode.read();
3057
3058        let Kind::Dir { path, .. } = guard.deref() else {
3059            panic!("expected preopen inode to be a directory");
3060        };
3061
3062        assert_eq!(path, std::path::Path::new("/hamlet"));
3063    }
3064
3065    #[cfg(all(unix, feature = "host-fs", feature = "sys"))]
3066    #[tokio::test]
3067    async fn backing_absolute_host_symlink_targets_stay_within_guest_mount() {
3068        let root_dir = tempfile::Builder::new()
3069            .prefix("wasix-backing-symlink")
3070            .tempdir_in("/tmp")
3071            .unwrap();
3072        let dir1 = root_dir.path().join("dir1");
3073        let dir2 = root_dir.path().join("dir2");
3074        std::fs::create_dir_all(&dir1).unwrap();
3075        std::fs::write(dir1.join("file1"), b"hello").unwrap();
3076        std::os::unix::fs::symlink(&dir1, &dir2).unwrap();
3077
3078        let host_fs = virtual_fs::host_fs::FileSystem::new(
3079            tokio::runtime::Handle::current(),
3080            root_dir.path(),
3081        )
3082        .unwrap();
3083        let mount_fs = virtual_fs::MountFileSystem::new();
3084        mount_fs
3085            .mount(
3086                Path::new("/"),
3087                Arc::new(RootFileSystemBuilder::default().build_tmp()),
3088            )
3089            .unwrap();
3090        mount_fs
3091            .mount(
3092                Path::new("/host"),
3093                Arc::new(host_fs) as Arc<dyn FileSystem + Send + Sync>,
3094            )
3095            .unwrap();
3096
3097        let inodes = WasiInodes::new();
3098        let fs_backing = WasiFsRoot::from_mount_fs(mount_fs);
3099        let wasi_fs =
3100            WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3101
3102        let literal_link = wasi_fs
3103            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2", false)
3104            .unwrap();
3105        assert!(matches!(
3106            literal_link.read().deref(),
3107            Kind::Symlink {
3108                symlink_kind: SymlinkKind::Backing,
3109                relative_path,
3110                ..
3111            } if relative_path == Path::new("/dir1")
3112        ));
3113
3114        let followed_dir = wasi_fs
3115            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2", true)
3116            .unwrap();
3117        let followed_dir_path = {
3118            let guard = followed_dir.read();
3119            let Kind::Dir { path, .. } = guard.deref() else {
3120                panic!("expected followed backing symlink to resolve to a directory");
3121            };
3122            assert_eq!(path, Path::new("/host/dir1"));
3123            path.clone()
3124        };
3125        let mut entries = wasi_fs.root_fs.read_dir(&followed_dir_path).unwrap();
3126        assert!(entries.any(|entry| entry.unwrap().path() == Path::new("/host/dir1/file1")));
3127
3128        let child = wasi_fs
3129            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2/file1", true)
3130            .unwrap();
3131        assert!(matches!(
3132            child.read().deref(),
3133            Kind::File { path, .. } if path == Path::new("/host/dir1/file1")
3134        ));
3135    }
3136
3137    #[tokio::test]
3138    async fn dot_mapped_preopen_uses_guest_current_dir() {
3139        let init = WasiEnvBuilder::new("test_prog")
3140            .engine(Engine::default())
3141            .current_dir("/work")
3142            .map_dir(".", "/work")
3143            .unwrap()
3144            .build_init()
3145            .unwrap();
3146
3147        let preopen_inode = {
3148            let guard = init.state.fs.root_inode.read();
3149            let Kind::Root { entries } = guard.deref() else {
3150                panic!("expected root inode");
3151            };
3152            entries.get(".").unwrap().clone()
3153        };
3154        let guard = preopen_inode.read();
3155
3156        let Kind::Dir { path, .. } = guard.deref() else {
3157            panic!("expected preopen inode to be a directory");
3158        };
3159
3160        assert_eq!(path, std::path::Path::new("/work"));
3161    }
3162
3163    #[tokio::test]
3164    async fn symlinked_directory_components_resolve_to_target_entries() {
3165        let inodes = WasiInodes::new();
3166        let fs_backing =
3167            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3168        let wasi_fs =
3169            WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3170        let root = &wasi_fs.root_fs;
3171
3172        root.create_dir(Path::new("/orig")).unwrap();
3173        root.new_open_options()
3174            .create(true)
3175            .write(true)
3176            .open(Path::new("/orig/child.txt"))
3177            .unwrap();
3178        root.create_symlink(Path::new("/orig"), Path::new("/linked"))
3179            .unwrap();
3180
3181        let literal_link = wasi_fs
3182            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked", false)
3183            .unwrap();
3184        assert!(matches!(
3185            literal_link.read().deref(),
3186            Kind::Symlink {
3187                relative_path,
3188                ..
3189            } if relative_path == Path::new("/orig")
3190        ));
3191
3192        let followed_dir = wasi_fs
3193            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked", true)
3194            .unwrap();
3195        assert!(matches!(
3196            followed_dir.read().deref(),
3197            Kind::Dir { path, .. } if path == Path::new("/orig")
3198        ));
3199
3200        let child = wasi_fs
3201            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked/child.txt", true)
3202            .unwrap();
3203        assert!(matches!(
3204            child.read().deref(),
3205            Kind::File { path, .. } if path == Path::new("/orig/child.txt")
3206        ));
3207
3208        let child_without_final_follow = wasi_fs
3209            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked/child.txt", false)
3210            .unwrap();
3211        assert!(matches!(
3212            child_without_final_follow.read().deref(),
3213            Kind::File { path, .. } if path == Path::new("/orig/child.txt")
3214        ));
3215    }
3216
3217    #[tokio::test]
3218    async fn webc_backing_symlink_resolves_to_target_entry() {
3219        let inodes = WasiInodes::new();
3220        let fs_backing = WasiFsRoot::from_filesystem(Arc::new(webc_symlink_fs()));
3221        let wasi_fs =
3222            WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3223
3224        let literal_link = wasi_fs
3225            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/link", false)
3226            .unwrap();
3227        assert!(matches!(
3228            literal_link.read().deref(),
3229            Kind::Symlink {
3230                relative_path,
3231                ..
3232            } if relative_path == Path::new("target.txt")
3233        ));
3234
3235        let followed_file = wasi_fs
3236            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/link", true)
3237            .unwrap();
3238        assert!(matches!(
3239            followed_file.read().deref(),
3240            Kind::File { path, .. } if path == Path::new("/target.txt")
3241        ));
3242    }
3243
3244    #[tokio::test]
3245    async fn path_resolution_preserves_posix_directory_component_rules() {
3246        let inodes = WasiInodes::new();
3247        let fs_backing =
3248            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3249        let wasi_fs =
3250            WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3251        let root = &wasi_fs.root_fs;
3252
3253        root.create_dir(Path::new("/dir")).unwrap();
3254        root.new_open_options()
3255            .create(true)
3256            .write(true)
3257            .open(Path::new("/file"))
3258            .unwrap();
3259        root.create_symlink(Path::new("/dir"), Path::new("/dir-link"))
3260            .unwrap();
3261        root.create_symlink(Path::new("/file"), Path::new("/file-link"))
3262            .unwrap();
3263
3264        let empty_path = wasi_fs
3265            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "", true)
3266            .unwrap_err();
3267        assert_eq!(empty_path, Errno::Noent);
3268
3269        let (single_component_parent, single_component_name) = wasi_fs
3270            .get_parent_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, Path::new("new-file"), true)
3271            .unwrap();
3272        assert_eq!(single_component_name, "new-file");
3273        assert!(matches!(
3274            single_component_parent.read().deref(),
3275            Kind::Root { .. }
3276        ));
3277
3278        let root_parent = wasi_fs
3279            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/..", true)
3280            .unwrap();
3281        assert!(matches!(root_parent.read().deref(), Kind::Root { .. }));
3282
3283        let escaped_symlink_target = wasi_fs
3284            .resolve_symlink_target_path(
3285                SymlinkKind::Virtual,
3286                Path::new("fs_sandbox_symlink.dir/link"),
3287                Path::new("../../README.md"),
3288            )
3289            .unwrap_err();
3290        assert_eq!(escaped_symlink_target, Errno::Perm);
3291
3292        let (_, contained_symlink_target) = wasi_fs
3293            .resolve_symlink_target_path(
3294                SymlinkKind::Virtual,
3295                Path::new("fs_sandbox_symlink.dir/link"),
3296                Path::new("../README.md"),
3297            )
3298            .unwrap();
3299        assert_eq!(contained_symlink_target, Path::new("README.md"));
3300
3301        let (_, sibling_preopen_symlink_target) = wasi_fs
3302            .resolve_symlink_target_path(
3303                SymlinkKind::Virtual,
3304                Path::new("temp/act3"),
3305                Path::new("../hamlet/act3"),
3306            )
3307            .unwrap();
3308        assert_eq!(sibling_preopen_symlink_target, Path::new("hamlet/act3"));
3309
3310        let escaped_sibling_preopen_symlink_target = wasi_fs
3311            .resolve_symlink_target_path(
3312                SymlinkKind::Virtual,
3313                Path::new("temp/act3"),
3314                Path::new("../../outside"),
3315            )
3316            .unwrap_err();
3317        assert_eq!(escaped_sibling_preopen_symlink_target, Errno::Perm);
3318
3319        root.create_dir(Path::new("/outerdir")).unwrap();
3320        root.create_dir(Path::new("/outerdir/dest")).unwrap();
3321        root.new_open_options()
3322            .create(true)
3323            .write(true)
3324            .open(Path::new("/outerdir/evil"))
3325            .unwrap();
3326        let dest_dir = wasi_fs
3327            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/outerdir/dest", true)
3328            .unwrap();
3329        let current_link = wasi_fs.create_inode_with_default_stat(
3330            &inodes,
3331            Kind::Symlink {
3332                symlink_kind: SymlinkKind::Virtual,
3333                path_to_symlink: PathBuf::from("outerdir/dest/current"),
3334                relative_path: PathBuf::from("."),
3335            },
3336            false,
3337            Cow::Borrowed("current"),
3338        );
3339        let parent_link = wasi_fs.create_inode_with_default_stat(
3340            &inodes,
3341            Kind::Symlink {
3342                symlink_kind: SymlinkKind::Virtual,
3343                path_to_symlink: PathBuf::from("outerdir/dest/parent"),
3344                relative_path: PathBuf::from("current/.."),
3345            },
3346            false,
3347            Cow::Borrowed("parent"),
3348        );
3349        {
3350            let mut guard = dest_dir.write();
3351            let Kind::Dir { entries, .. } = guard.deref_mut() else {
3352                panic!("expected destination to be a directory");
3353            };
3354            entries.insert("current".to_string(), current_link);
3355            entries.insert("parent".to_string(), parent_link);
3356        }
3357
3358        let parent_symlink_target = wasi_fs
3359            .get_inode_at_path(
3360                &inodes,
3361                crate::VIRTUAL_ROOT_FD,
3362                "/outerdir/dest/parent/evil",
3363                true,
3364            )
3365            .unwrap();
3366        assert!(matches!(
3367            parent_symlink_target.read().deref(),
3368            Kind::File { path, .. } if path == Path::new("/outerdir/evil")
3369        ));
3370
3371        let file_dot = wasi_fs
3372            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file/.", true)
3373            .unwrap_err();
3374        assert_eq!(file_dot, Errno::Notdir);
3375
3376        let file_slash = wasi_fs
3377            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file/", true)
3378            .unwrap_err();
3379        assert_eq!(file_slash, Errno::Notdir);
3380
3381        let symlinked_dir_slash = wasi_fs
3382            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/dir-link/", false)
3383            .unwrap();
3384        assert!(matches!(
3385            symlinked_dir_slash.read().deref(),
3386            Kind::Dir { path, .. } if path == Path::new("/dir")
3387        ));
3388
3389        let symlinked_file_slash = wasi_fs
3390            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file-link/", false)
3391            .unwrap_err();
3392        assert_eq!(symlinked_file_slash, Errno::Notdir);
3393
3394        root.create_symlink(Path::new("/loop"), Path::new("/loop"))
3395            .unwrap();
3396        let symlink_loop = wasi_fs
3397            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/loop", true)
3398            .unwrap_err();
3399        assert_eq!(symlink_loop, Errno::Loop);
3400    }
3401
3402    #[tokio::test]
3403    async fn writable_root_is_preserved_through_root_overlays() {
3404        let base_root = Arc::new(RootFileSystemBuilder::default().build_tmp());
3405        let root = WasiFsRoot::from_filesystem(base_root);
3406        assert!(root.writable_root().is_some());
3407
3408        let lower = Arc::new(TmpFileSystem::new()) as Arc<dyn FileSystem + Send + Sync>;
3409        root.stack_root_filesystem(lower).unwrap();
3410
3411        assert!(root.writable_root().is_some());
3412    }
3413
3414    #[tokio::test]
3415    async fn conditional_union_merges_root_and_non_root_package_mounts_once() {
3416        let inodes = WasiInodes::new();
3417        let fs_backing =
3418            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3419        let wasi_fs = WasiFs::new_init(fs_backing, &inodes, FS_ROOT_INO).unwrap();
3420
3421        let root_layer = TmpFileSystem::new();
3422        root_layer
3423            .new_open_options()
3424            .create(true)
3425            .write(true)
3426            .open(Path::new("/root.txt"))
3427            .unwrap();
3428
3429        let public_mount = TmpFileSystem::new();
3430        public_mount
3431            .new_open_options()
3432            .create(true)
3433            .write(true)
3434            .open(Path::new("/index.html"))
3435            .unwrap();
3436
3437        let pkg = BinaryPackage {
3438            id: PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap()),
3439            package_ids: vec![],
3440            when_cached: None,
3441            entrypoint_cmd: None,
3442            hash: OnceCell::new(),
3443            package_mounts: Some(Arc::new(BinaryPackageMounts {
3444                root_layer: Some(Arc::new(root_layer)),
3445                mounts: vec![BinaryPackageMount {
3446                    guest_path: PathBuf::from("/public"),
3447                    fs: Arc::new(public_mount),
3448                    source_path: PathBuf::from("/"),
3449                }],
3450            })),
3451            commands: vec![],
3452            uses: vec![],
3453            file_system_memory_footprint: 0,
3454            additional_host_mapped_directories: vec![],
3455        };
3456
3457        wasi_fs.conditional_union(&pkg).await.unwrap();
3458        assert!(
3459            wasi_fs
3460                .root_fs
3461                .metadata(Path::new("/root.txt"))
3462                .unwrap()
3463                .is_file()
3464        );
3465        assert!(
3466            wasi_fs
3467                .root_fs
3468                .metadata(Path::new("/public/index.html"))
3469                .unwrap()
3470                .is_file()
3471        );
3472
3473        wasi_fs.conditional_union(&pkg).await.unwrap();
3474        assert!(
3475            wasi_fs
3476                .root_fs
3477                .metadata(Path::new("/root.txt"))
3478                .unwrap()
3479                .is_file()
3480        );
3481        assert!(
3482            wasi_fs
3483                .root_fs
3484                .metadata(Path::new("/public/index.html"))
3485                .unwrap()
3486                .is_file()
3487        );
3488    }
3489
3490    #[tokio::test]
3491    async fn ephemeral_symlink_resolves_without_caching_a_directory_entry() {
3492        // An ephemeral symlink must resolve to a `Kind::Symlink` that is NOT cached in
3493        // the parent's `entries`, so unlink takes the uncached branch.
3494        let inodes = WasiInodes::new();
3495        let fs_backing =
3496            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3497        let wasi_fs =
3498            WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3499
3500        wasi_fs.register_ephemeral_symlink(
3501            PathBuf::from("/link"),
3502            PathBuf::from("link"),
3503            PathBuf::from("target.txt"),
3504        );
3505
3506        let link = wasi_fs
3507            .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/link", false)
3508            .unwrap();
3509        assert!(matches!(
3510            link.read().deref(),
3511            Kind::Symlink {
3512                symlink_kind: SymlinkKind::Virtual,
3513                relative_path,
3514                ..
3515            } if relative_path == Path::new("target.txt")
3516        ));
3517
3518        // Parent must not have cached the symlink.
3519        let (parent_inode, child_name) = wasi_fs
3520            .get_parent_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, Path::new("/link"), false)
3521            .unwrap();
3522        assert_eq!(child_name, "link");
3523        match parent_inode.read().deref() {
3524            Kind::Dir { entries, .. } | Kind::Root { entries } => {
3525                assert!(!entries.contains_key("link"));
3526            }
3527            _ => panic!("expected the parent of /link to be a directory"),
3528        }
3529    }
3530
3531    #[tokio::test]
3532    async fn remove_symlink_file_drops_ephemeral_link_without_backing_file() {
3533        // Ephemeral link, no host file: removal succeeds, unregisters, target untouched.
3534        let inodes = WasiInodes::new();
3535        let fs_backing =
3536            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3537        let wasi_fs =
3538            WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3539
3540        wasi_fs
3541            .root_fs
3542            .new_open_options()
3543            .create(true)
3544            .write(true)
3545            .open(Path::new("/target.txt"))
3546            .unwrap();
3547        wasi_fs.register_ephemeral_symlink(
3548            PathBuf::from("/link"),
3549            PathBuf::from("link"),
3550            PathBuf::from("target.txt"),
3551        );
3552        assert!(wasi_fs.ephemeral_symlink_at(Path::new("/link")).is_some());
3553
3554        assert_eq!(
3555            wasi_fs.remove_symlink_file(Path::new("/link")),
3556            Errno::Success
3557        );
3558        assert!(wasi_fs.ephemeral_symlink_at(Path::new("/link")).is_none());
3559        assert!(
3560            wasi_fs
3561                .root_fs
3562                .metadata(Path::new("/target.txt"))
3563                .unwrap()
3564                .is_file()
3565        );
3566    }
3567
3568    #[tokio::test]
3569    async fn remove_symlink_file_reports_noent_when_nothing_to_remove() {
3570        let inodes = WasiInodes::new();
3571        let fs_backing =
3572            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3573        let wasi_fs =
3574            WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3575
3576        assert_eq!(
3577            wasi_fs.remove_symlink_file(Path::new("/missing")),
3578            Errno::Noent
3579        );
3580    }
3581}