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