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