Skip to main content

wasmtime_wasi/
filesystem.rs

1use crate::clocks::Datetime;
2use crate::runtime::{AbortOnDropJoinHandle, spawn_blocking};
3use cap_fs_ext::{FileTypeExt as _, MetadataExt as _};
4use fs_set_times::SystemTimeSpec;
5use std::collections::hash_map;
6use std::sync::Arc;
7use tracing::debug;
8use wasmtime::component::{HasData, Resource, ResourceTable};
9use wasmtime::error::Context as _;
10
11#[cfg(unix)]
12pub(crate) mod unix;
13#[cfg(unix)]
14pub(crate) use unix as sys;
15#[cfg(windows)]
16pub(crate) mod windows;
17#[cfg(windows)]
18pub(crate) use windows as sys;
19
20/// A helper struct which implements [`HasData`] for the `wasi:filesystem` APIs.
21///
22/// This can be useful when directly calling `add_to_linker` functions directly,
23/// such as [`wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker`] as
24/// the `D` type parameter. See [`HasData`] for more information about the type
25/// parameter's purpose.
26///
27/// When using this type you can skip the [`WasiFilesystemView`] trait, for
28/// example.
29///
30/// [`wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker`]: crate::p2::bindings::filesystem::types::add_to_linker
31///
32/// # Examples
33///
34/// ```
35/// use wasmtime::component::{Linker, ResourceTable};
36/// use wasmtime::{Engine, Result};
37/// use wasmtime_wasi::filesystem::*;
38///
39/// struct MyStoreState {
40///     table: ResourceTable,
41///     filesystem: WasiFilesystemCtx,
42/// }
43///
44/// fn main() -> Result<()> {
45///     let engine = Engine::default();
46///     let mut linker = Linker::new(&engine);
47///
48///     wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker::<MyStoreState, WasiFilesystem>(
49///         &mut linker,
50///         |state| WasiFilesystemCtxView {
51///             table: &mut state.table,
52///             ctx: &mut state.filesystem,
53///         },
54///     )?;
55///     Ok(())
56/// }
57/// ```
58pub struct WasiFilesystem;
59
60impl HasData for WasiFilesystem {
61    type Data<'a> = WasiFilesystemCtxView<'a>;
62}
63
64#[derive(Clone, Default)]
65pub struct WasiFilesystemCtx {
66    pub(crate) allow_blocking_current_thread: bool,
67    pub(crate) preopens: Vec<(Dir, String)>,
68}
69
70pub struct WasiFilesystemCtxView<'a> {
71    pub ctx: &'a mut WasiFilesystemCtx,
72    pub table: &'a mut ResourceTable,
73}
74
75pub trait WasiFilesystemView: Send {
76    fn filesystem(&mut self) -> WasiFilesystemCtxView<'_>;
77}
78
79bitflags::bitflags! {
80    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
81    pub struct FilePerms: usize {
82        const READ = 0b1;
83        const WRITE = 0b10;
84    }
85}
86
87bitflags::bitflags! {
88    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
89    pub struct OpenMode: usize {
90        const READ = 0b1;
91        const WRITE = 0b10;
92    }
93}
94
95bitflags::bitflags! {
96    /// Permission bits for operating on a directory.
97    ///
98    /// Directories can be limited to being readonly. This will restrict what
99    /// can be done with them, for example preventing creation of new files.
100    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
101    pub struct DirPerms: usize {
102        /// This directory can be read, for example its entries can be iterated
103        /// over and files can be opened.
104        const READ = 0b1;
105
106        /// This directory can be mutated, for example by creating new files
107        /// within it.
108        const MUTATE = 0b10;
109    }
110}
111
112bitflags::bitflags! {
113    /// Flags determining the method of how paths are resolved.
114    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
115    pub(crate) struct PathFlags: usize {
116        /// This directory can be read, for example its entries can be iterated
117        /// over and files can be opened.
118        const SYMLINK_FOLLOW = 0b1;
119    }
120}
121
122bitflags::bitflags! {
123    /// Open flags used by `open-at`.
124    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
125    pub(crate) struct OpenFlags: usize {
126        /// Create file if it does not exist, similar to `O_CREAT` in POSIX.
127        const CREATE = 0b1;
128        /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX.
129        const DIRECTORY = 0b10;
130        /// Fail if file already exists, similar to `O_EXCL` in POSIX.
131        const EXCLUSIVE = 0b100;
132        /// Truncate file to size 0, similar to `O_TRUNC` in POSIX.
133        const TRUNCATE = 0b1000;
134    }
135}
136
137bitflags::bitflags! {
138    /// Descriptor flags.
139    ///
140    /// Note: This was called `fdflags` in earlier versions of WASI.
141    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
142    pub(crate) struct DescriptorFlags: usize {
143        /// Read mode: Data can be read.
144        const READ = 0b1;
145        /// Write mode: Data can be written to.
146        const WRITE = 0b10;
147        /// Request that writes be performed according to synchronized I/O file
148        /// integrity completion. The data stored in the file and the file's
149        /// metadata are synchronized. This is similar to `O_SYNC` in POSIX.
150        ///
151        /// The precise semantics of this operation have not yet been defined for
152        /// WASI. At this time, it should be interpreted as a request, and not a
153        /// requirement.
154        const FILE_INTEGRITY_SYNC = 0b100;
155        /// Request that writes be performed according to synchronized I/O data
156        /// integrity completion. Only the data stored in the file is
157        /// synchronized. This is similar to `O_DSYNC` in POSIX.
158        ///
159        /// The precise semantics of this operation have not yet been defined for
160        /// WASI. At this time, it should be interpreted as a request, and not a
161        /// requirement.
162        const DATA_INTEGRITY_SYNC = 0b1000;
163        /// Requests that reads be performed at the same level of integrity
164        /// requested for writes. This is similar to `O_RSYNC` in POSIX.
165        ///
166        /// The precise semantics of this operation have not yet been defined for
167        /// WASI. At this time, it should be interpreted as a request, and not a
168        /// requirement.
169        const REQUESTED_WRITE_SYNC = 0b10000;
170        /// Mutating directories mode: Directory contents may be mutated.
171        ///
172        /// When this flag is unset on a descriptor, operations using the
173        /// descriptor which would create, rename, delete, modify the data or
174        /// metadata of filesystem objects, or obtain another handle which
175        /// would permit any of those, shall fail with `error-code::read-only` if
176        /// they would otherwise succeed.
177        ///
178        /// This may only be set on directories.
179        const MUTATE_DIRECTORY = 0b100000;
180    }
181}
182
183/// Error codes returned by functions, similar to `errno` in POSIX.
184/// Not all of these error codes are returned by the functions provided by this
185/// API; some are used in higher-level library layers, and others are provided
186/// merely for alignment with POSIX.
187#[cfg_attr(
188    windows,
189    expect(dead_code, reason = "on Windows, some of these are not used")
190)]
191pub(crate) enum ErrorCode {
192    /// Permission denied, similar to `EACCES` in POSIX.
193    Access,
194    /// Connection already in progress, similar to `EALREADY` in POSIX.
195    Already,
196    /// Bad descriptor, similar to `EBADF` in POSIX.
197    BadDescriptor,
198    /// Device or resource busy, similar to `EBUSY` in POSIX.
199    Busy,
200    /// File exists, similar to `EEXIST` in POSIX.
201    Exist,
202    /// File too large, similar to `EFBIG` in POSIX.
203    FileTooLarge,
204    /// Illegal byte sequence, similar to `EILSEQ` in POSIX.
205    IllegalByteSequence,
206    /// Operation in progress, similar to `EINPROGRESS` in POSIX.
207    InProgress,
208    /// Interrupted function, similar to `EINTR` in POSIX.
209    Interrupted,
210    /// Invalid argument, similar to `EINVAL` in POSIX.
211    Invalid,
212    /// I/O error, similar to `EIO` in POSIX.
213    Io,
214    /// Is a directory, similar to `EISDIR` in POSIX.
215    IsDirectory,
216    /// Too many levels of symbolic links, similar to `ELOOP` in POSIX.
217    Loop,
218    /// Too many links, similar to `EMLINK` in POSIX.
219    TooManyLinks,
220    /// Filename too long, similar to `ENAMETOOLONG` in POSIX.
221    NameTooLong,
222    /// No such file or directory, similar to `ENOENT` in POSIX.
223    NoEntry,
224    /// Not enough space, similar to `ENOMEM` in POSIX.
225    InsufficientMemory,
226    /// No space left on device, similar to `ENOSPC` in POSIX.
227    InsufficientSpace,
228    /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX.
229    NotDirectory,
230    /// Directory not empty, similar to `ENOTEMPTY` in POSIX.
231    NotEmpty,
232    /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX.
233    Unsupported,
234    /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX.
235    Overflow,
236    /// Operation not permitted, similar to `EPERM` in POSIX.
237    NotPermitted,
238    /// Broken pipe, similar to `EPIPE` in POSIX.
239    Pipe,
240    /// Invalid seek, similar to `ESPIPE` in POSIX.
241    InvalidSeek,
242}
243
244fn datetime_from(t: std::time::SystemTime) -> Datetime {
245    // FIXME make this infallible or handle errors properly
246    Datetime::try_from(cap_std::time::SystemTime::from_std(t)).unwrap()
247}
248
249/// The type of a filesystem object referenced by a descriptor.
250///
251/// Note: This was called `filetype` in earlier versions of WASI.
252pub(crate) enum DescriptorType {
253    /// The type of the descriptor or file is unknown or is different from
254    /// any of the other types specified.
255    Unknown,
256    /// The descriptor refers to a block device inode.
257    BlockDevice,
258    /// The descriptor refers to a character device inode.
259    CharacterDevice,
260    /// The descriptor refers to a directory inode.
261    Directory,
262    /// The file refers to a symbolic link inode.
263    SymbolicLink,
264    /// The descriptor refers to a regular file inode.
265    RegularFile,
266}
267
268impl From<cap_std::fs::FileType> for DescriptorType {
269    fn from(ft: cap_std::fs::FileType) -> Self {
270        if ft.is_dir() {
271            DescriptorType::Directory
272        } else if ft.is_symlink() {
273            DescriptorType::SymbolicLink
274        } else if ft.is_block_device() {
275            DescriptorType::BlockDevice
276        } else if ft.is_char_device() {
277            DescriptorType::CharacterDevice
278        } else if ft.is_file() {
279            DescriptorType::RegularFile
280        } else {
281            DescriptorType::Unknown
282        }
283    }
284}
285
286/// File attributes.
287///
288/// Note: This was called `filestat` in earlier versions of WASI.
289pub(crate) struct DescriptorStat {
290    /// File type.
291    pub type_: DescriptorType,
292    /// Number of hard links to the file.
293    pub link_count: u64,
294    /// For regular files, the file size in bytes. For symbolic links, the
295    /// length in bytes of the pathname contained in the symbolic link.
296    pub size: u64,
297    /// Last data access timestamp.
298    ///
299    /// If the `option` is none, the platform doesn't maintain an access
300    /// timestamp for this file.
301    pub data_access_timestamp: Option<Datetime>,
302    /// Last data modification timestamp.
303    ///
304    /// If the `option` is none, the platform doesn't maintain a
305    /// modification timestamp for this file.
306    pub data_modification_timestamp: Option<Datetime>,
307    /// Last file status-change timestamp.
308    ///
309    /// If the `option` is none, the platform doesn't maintain a
310    /// status-change timestamp for this file.
311    pub status_change_timestamp: Option<Datetime>,
312}
313
314impl From<cap_std::fs::Metadata> for DescriptorStat {
315    fn from(meta: cap_std::fs::Metadata) -> Self {
316        Self {
317            type_: meta.file_type().into(),
318            link_count: meta.nlink(),
319            size: meta.len(),
320            data_access_timestamp: meta.accessed().map(|t| datetime_from(t.into_std())).ok(),
321            data_modification_timestamp: meta.modified().map(|t| datetime_from(t.into_std())).ok(),
322            status_change_timestamp: meta.created().map(|t| datetime_from(t.into_std())).ok(),
323        }
324    }
325}
326
327/// A 128-bit hash value, split into parts because wasm doesn't have a
328/// 128-bit integer type.
329pub(crate) struct MetadataHashValue {
330    /// 64 bits of a 128-bit hash value.
331    pub lower: u64,
332    /// Another 64 bits of a 128-bit hash value.
333    pub upper: u64,
334}
335
336impl From<&cap_std::fs::Metadata> for MetadataHashValue {
337    fn from(meta: &cap_std::fs::Metadata) -> Self {
338        use cap_fs_ext::MetadataExt;
339        // Without incurring any deps, std provides us with a 64 bit hash
340        // function:
341        use std::hash::Hasher;
342        // Note that this means that the metadata hash (which becomes a preview1 ino) may
343        // change when a different rustc release is used to build this host implementation:
344        let mut hasher = hash_map::DefaultHasher::new();
345        hasher.write_u64(meta.dev());
346        hasher.write_u64(meta.ino());
347        let lower = hasher.finish();
348        // MetadataHashValue has a pair of 64-bit members for representing a
349        // single 128-bit number. However, we only have 64 bits of entropy. To
350        // synthesize the upper 64 bits, lets xor the lower half with an arbitrary
351        // constant, in this case the 64 bit integer corresponding to the IEEE
352        // double representation of (a number as close as possible to) pi.
353        // This seems better than just repeating the same bits in the upper and
354        // lower parts outright, which could make folks wonder if the struct was
355        // mangled in the ABI, or worse yet, lead to consumers of this interface
356        // expecting them to be equal.
357        let upper = lower ^ 4614256656552045848u64;
358        Self { lower, upper }
359    }
360}
361
362#[derive(Copy, Clone, Debug)]
363pub(crate) enum Advice {
364    Normal,
365    Sequential,
366    Random,
367    WillNeed,
368    DontNeed,
369    NoReuse,
370}
371
372#[cfg(unix)]
373fn from_raw_os_error(err: Option<i32>) -> Option<ErrorCode> {
374    use rustix::io::Errno as RustixErrno;
375    if err.is_none() {
376        return None;
377    }
378    Some(match RustixErrno::from_raw_os_error(err.unwrap()) {
379        RustixErrno::PIPE => ErrorCode::Pipe,
380        RustixErrno::PERM => ErrorCode::NotPermitted,
381        RustixErrno::NOENT => ErrorCode::NoEntry,
382        RustixErrno::NOMEM => ErrorCode::InsufficientMemory,
383        RustixErrno::IO => ErrorCode::Io,
384        RustixErrno::BADF => ErrorCode::BadDescriptor,
385        RustixErrno::BUSY => ErrorCode::Busy,
386        RustixErrno::ACCESS => ErrorCode::Access,
387        RustixErrno::NOTDIR => ErrorCode::NotDirectory,
388        RustixErrno::ISDIR => ErrorCode::IsDirectory,
389        RustixErrno::INVAL => ErrorCode::Invalid,
390        RustixErrno::EXIST => ErrorCode::Exist,
391        RustixErrno::FBIG => ErrorCode::FileTooLarge,
392        RustixErrno::NOSPC => ErrorCode::InsufficientSpace,
393        RustixErrno::SPIPE => ErrorCode::InvalidSeek,
394        RustixErrno::MLINK => ErrorCode::TooManyLinks,
395        RustixErrno::NAMETOOLONG => ErrorCode::NameTooLong,
396        RustixErrno::NOTEMPTY => ErrorCode::NotEmpty,
397        RustixErrno::LOOP => ErrorCode::Loop,
398        RustixErrno::OVERFLOW => ErrorCode::Overflow,
399        RustixErrno::ILSEQ => ErrorCode::IllegalByteSequence,
400        RustixErrno::NOTSUP => ErrorCode::Unsupported,
401        RustixErrno::ALREADY => ErrorCode::Already,
402        RustixErrno::INPROGRESS => ErrorCode::InProgress,
403        RustixErrno::INTR => ErrorCode::Interrupted,
404
405        // On some platforms, these have the same value as other errno values.
406        #[allow(unreachable_patterns, reason = "see comment")]
407        RustixErrno::OPNOTSUPP => ErrorCode::Unsupported,
408
409        _ => return None,
410    })
411}
412
413#[cfg(windows)]
414fn from_raw_os_error(raw_os_error: Option<i32>) -> Option<ErrorCode> {
415    use windows_sys::Win32::Foundation;
416    Some(match raw_os_error.map(|code| code as u32) {
417        Some(Foundation::ERROR_FILE_NOT_FOUND) => ErrorCode::NoEntry,
418        Some(Foundation::ERROR_PATH_NOT_FOUND) => ErrorCode::NoEntry,
419        Some(Foundation::ERROR_ACCESS_DENIED) => ErrorCode::Access,
420        Some(Foundation::ERROR_SHARING_VIOLATION) => ErrorCode::Access,
421        Some(Foundation::ERROR_PRIVILEGE_NOT_HELD) => ErrorCode::NotPermitted,
422        Some(Foundation::ERROR_INVALID_HANDLE) => ErrorCode::BadDescriptor,
423        Some(Foundation::ERROR_INVALID_NAME) => ErrorCode::NoEntry,
424        Some(Foundation::ERROR_NOT_ENOUGH_MEMORY) => ErrorCode::InsufficientMemory,
425        Some(Foundation::ERROR_OUTOFMEMORY) => ErrorCode::InsufficientMemory,
426        Some(Foundation::ERROR_DIR_NOT_EMPTY) => ErrorCode::NotEmpty,
427        Some(Foundation::ERROR_NOT_READY) => ErrorCode::Busy,
428        Some(Foundation::ERROR_BUSY) => ErrorCode::Busy,
429        Some(Foundation::ERROR_NOT_SUPPORTED) => ErrorCode::Unsupported,
430        Some(Foundation::ERROR_FILE_EXISTS) => ErrorCode::Exist,
431        Some(Foundation::ERROR_BROKEN_PIPE) => ErrorCode::Pipe,
432        Some(Foundation::ERROR_BUFFER_OVERFLOW) => ErrorCode::NameTooLong,
433        Some(Foundation::ERROR_NOT_A_REPARSE_POINT) => ErrorCode::Invalid,
434        Some(Foundation::ERROR_NEGATIVE_SEEK) => ErrorCode::Invalid,
435        Some(Foundation::ERROR_DIRECTORY) => ErrorCode::NotDirectory,
436        Some(Foundation::ERROR_ALREADY_EXISTS) => ErrorCode::Exist,
437        Some(Foundation::ERROR_STOPPED_ON_SYMLINK) => ErrorCode::Loop,
438        Some(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED) => ErrorCode::IsDirectory,
439        _ => return None,
440    })
441}
442
443impl<'a> From<&'a std::io::Error> for ErrorCode {
444    fn from(err: &'a std::io::Error) -> ErrorCode {
445        match from_raw_os_error(err.raw_os_error()) {
446            Some(errno) => errno,
447            None => {
448                debug!("unknown raw os error: {err}");
449                match err.kind() {
450                    std::io::ErrorKind::NotFound => ErrorCode::NoEntry,
451                    std::io::ErrorKind::PermissionDenied => ErrorCode::NotPermitted,
452                    std::io::ErrorKind::AlreadyExists => ErrorCode::Exist,
453                    std::io::ErrorKind::InvalidInput => ErrorCode::Invalid,
454                    _ => ErrorCode::Io,
455                }
456            }
457        }
458    }
459}
460
461impl From<std::io::Error> for ErrorCode {
462    fn from(err: std::io::Error) -> ErrorCode {
463        ErrorCode::from(&err)
464    }
465}
466
467#[derive(Clone)]
468pub enum Descriptor {
469    File(File),
470    Dir(Dir),
471}
472
473impl Descriptor {
474    pub(crate) fn file(&self) -> Result<&File, ErrorCode> {
475        match self {
476            Descriptor::File(f) => Ok(f),
477            Descriptor::Dir(_) => Err(ErrorCode::BadDescriptor),
478        }
479    }
480
481    pub(crate) fn dir(&self) -> Result<&Dir, ErrorCode> {
482        match self {
483            Descriptor::Dir(d) => Ok(d),
484            Descriptor::File(_) => Err(ErrorCode::NotDirectory),
485        }
486    }
487
488    async fn get_metadata(&self) -> std::io::Result<cap_std::fs::Metadata> {
489        match self {
490            Self::File(f) => {
491                // No permissions check on metadata: if opened, allowed to stat it
492                f.run_blocking(|f| f.metadata()).await
493            }
494            Self::Dir(d) => {
495                // No permissions check on metadata: if opened, allowed to stat it
496                d.run_blocking(|d| d.dir_metadata()).await
497            }
498        }
499    }
500
501    pub(crate) async fn sync_data(&self) -> Result<(), ErrorCode> {
502        match self {
503            Self::File(f) => {
504                match f.run_blocking(|f| f.sync_data()).await {
505                    Ok(()) => Ok(()),
506                    // On windows, `sync_data` uses `FileFlushBuffers` which fails with
507                    // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore
508                    // this error, for POSIX compatibility.
509                    #[cfg(windows)]
510                    Err(err)
511                        if err.raw_os_error()
512                            == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) =>
513                    {
514                        Ok(())
515                    }
516                    Err(err) => Err(err.into()),
517                }
518            }
519            Self::Dir(d) => {
520                d.run_blocking(|d| {
521                    let d = d.open(std::path::Component::CurDir)?;
522                    d.sync_data()?;
523                    Ok(())
524                })
525                .await
526            }
527        }
528    }
529
530    pub(crate) async fn get_flags(&self) -> Result<DescriptorFlags, ErrorCode> {
531        match self {
532            Self::File(f) => {
533                let mut flags = f.run_blocking(|f| sys::get_flags(f)).await?;
534                if f.open_mode.contains(OpenMode::READ) {
535                    flags |= DescriptorFlags::READ;
536                }
537                if f.open_mode.contains(OpenMode::WRITE) {
538                    flags |= DescriptorFlags::WRITE;
539                }
540                Ok(flags)
541            }
542            Self::Dir(d) => {
543                let mut flags = d.run_blocking(|d| sys::get_flags(d)).await?;
544                if d.open_mode.contains(OpenMode::READ) {
545                    flags |= DescriptorFlags::READ;
546                }
547                if d.open_mode.contains(OpenMode::WRITE) {
548                    flags |= DescriptorFlags::MUTATE_DIRECTORY;
549                }
550                Ok(flags)
551            }
552        }
553    }
554
555    pub(crate) async fn get_type(&self) -> Result<DescriptorType, ErrorCode> {
556        match self {
557            Self::File(f) => {
558                let meta = f.run_blocking(|f| f.metadata()).await?;
559                Ok(meta.file_type().into())
560            }
561            Self::Dir(_) => Ok(DescriptorType::Directory),
562        }
563    }
564
565    pub(crate) async fn set_times(
566        &self,
567        atim: Option<SystemTimeSpec>,
568        mtim: Option<SystemTimeSpec>,
569    ) -> Result<(), ErrorCode> {
570        use fs_set_times::SetTimes as _;
571        match self {
572            Self::File(f) => {
573                if !f.perms.contains(FilePerms::WRITE) {
574                    return Err(ErrorCode::NotPermitted);
575                }
576                f.run_blocking(|f| f.set_times(atim, mtim)).await?;
577                Ok(())
578            }
579            Self::Dir(d) => {
580                if !d.perms.contains(DirPerms::MUTATE) {
581                    return Err(ErrorCode::NotPermitted);
582                }
583                d.run_blocking(|d| d.set_times(atim, mtim)).await?;
584                Ok(())
585            }
586        }
587    }
588
589    pub(crate) async fn sync(&self) -> Result<(), ErrorCode> {
590        match self {
591            Self::File(f) => {
592                match f.run_blocking(|f| f.sync_all()).await {
593                    Ok(()) => Ok(()),
594                    // On windows, `sync_data` uses `FileFlushBuffers` which fails with
595                    // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore
596                    // this error, for POSIX compatibility.
597                    #[cfg(windows)]
598                    Err(err)
599                        if err.raw_os_error()
600                            == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) =>
601                    {
602                        Ok(())
603                    }
604                    Err(err) => Err(err.into()),
605                }
606            }
607            Self::Dir(d) => {
608                d.run_blocking(|d| {
609                    let d = d.open(std::path::Component::CurDir)?;
610                    d.sync_all()?;
611                    Ok(())
612                })
613                .await
614            }
615        }
616    }
617
618    pub(crate) async fn stat(&self) -> Result<DescriptorStat, ErrorCode> {
619        match self {
620            Self::File(f) => {
621                // No permissions check on stat: if opened, allowed to stat it
622                let meta = f.run_blocking(|f| f.metadata()).await?;
623                Ok(meta.into())
624            }
625            Self::Dir(d) => {
626                // No permissions check on stat: if opened, allowed to stat it
627                let meta = d.run_blocking(|d| d.dir_metadata()).await?;
628                Ok(meta.into())
629            }
630        }
631    }
632
633    pub(crate) async fn is_same_object(&self, other: &Self) -> wasmtime::Result<bool> {
634        use cap_fs_ext::MetadataExt;
635        let meta_a = self.get_metadata().await?;
636        let meta_b = other.get_metadata().await?;
637        if meta_a.dev() == meta_b.dev() && meta_a.ino() == meta_b.ino() {
638            // MetadataHashValue does not derive eq, so use a pair of
639            // comparisons to check equality:
640            debug_assert_eq!(
641                MetadataHashValue::from(&meta_a).upper,
642                MetadataHashValue::from(&meta_b).upper,
643            );
644            debug_assert_eq!(
645                MetadataHashValue::from(&meta_a).lower,
646                MetadataHashValue::from(&meta_b).lower,
647            );
648            Ok(true)
649        } else {
650            // Hash collisions are possible, so don't assert the negative here
651            Ok(false)
652        }
653    }
654
655    pub(crate) async fn metadata_hash(&self) -> Result<MetadataHashValue, ErrorCode> {
656        let meta = self.get_metadata().await?;
657        Ok(MetadataHashValue::from(&meta))
658    }
659}
660
661#[derive(Clone)]
662pub struct File {
663    /// The operating system File this struct is mediating access to.
664    ///
665    /// Wrapped in an Arc because the same underlying file is used for
666    /// implementing the stream types. A copy is also needed for
667    /// `spawn_blocking`.
668    pub file: Arc<cap_std::fs::File>,
669    /// Permissions to enforce on access to the file. These permissions are
670    /// specified by a user of the `crate::WasiCtxBuilder`, and are
671    /// enforced prior to any enforced by the underlying operating system.
672    pub perms: FilePerms,
673    /// The mode the file was opened under: bits for reading, and writing.
674    /// Required to correctly report the DescriptorFlags, because cap-std
675    /// doesn't presently provide a cross-platform equivalent of reading the
676    /// oflags back out using fcntl.
677    pub open_mode: OpenMode,
678
679    allow_blocking_current_thread: bool,
680}
681
682impl File {
683    pub fn new(
684        file: cap_std::fs::File,
685        perms: FilePerms,
686        open_mode: OpenMode,
687        allow_blocking_current_thread: bool,
688    ) -> Self {
689        Self {
690            file: Arc::new(file),
691            perms,
692            open_mode,
693            allow_blocking_current_thread,
694        }
695    }
696
697    /// Execute the blocking `body` function.
698    ///
699    /// Depending on how the WasiCtx was configured, the body may either be:
700    /// - Executed directly on the current thread. In this case the `async`
701    ///   signature of this method is effectively a lie and the returned
702    ///   Future will always be immediately Ready. Or:
703    /// - Spawned on a background thread using [`tokio::task::spawn_blocking`]
704    ///   and immediately awaited.
705    ///
706    /// Intentionally blocking the executor thread might seem unorthodox, but is
707    /// not actually a problem for specific workloads. See:
708    /// - [`crate::WasiCtxBuilder::allow_blocking_current_thread`]
709    /// - [Poor performance of wasmtime file I/O maybe because tokio](https://github.com/bytecodealliance/wasmtime/issues/7973)
710    /// - [Implement opt-in for enabling WASI to block the current thread](https://github.com/bytecodealliance/wasmtime/pull/8190)
711    pub(crate) async fn run_blocking<F, R>(&self, body: F) -> R
712    where
713        F: FnOnce(&cap_std::fs::File) -> R + Send + 'static,
714        R: Send + 'static,
715    {
716        match self.as_blocking_file() {
717            Some(file) => body(file),
718            None => self.spawn_blocking(body).await,
719        }
720    }
721
722    pub(crate) fn spawn_blocking<F, R>(&self, body: F) -> AbortOnDropJoinHandle<R>
723    where
724        F: FnOnce(&cap_std::fs::File) -> R + Send + 'static,
725        R: Send + 'static,
726    {
727        let f = self.file.clone();
728        spawn_blocking(move || body(&f))
729    }
730
731    /// Returns `Some` when the current thread is allowed to block in filesystem
732    /// operations, and otherwise returns `None` to indicate that
733    /// `spawn_blocking` must be used.
734    pub(crate) fn as_blocking_file(&self) -> Option<&cap_std::fs::File> {
735        if self.allow_blocking_current_thread {
736            Some(&self.file)
737        } else {
738            None
739        }
740    }
741
742    /// Returns reference to the underlying [`cap_std::fs::File`]
743    #[cfg(feature = "p3")]
744    pub(crate) fn as_file(&self) -> &Arc<cap_std::fs::File> {
745        &self.file
746    }
747
748    pub(crate) async fn advise(
749        &self,
750        offset: u64,
751        len: u64,
752        advice: Advice,
753    ) -> Result<(), ErrorCode> {
754        self.run_blocking(move |f| sys::advise(f, offset, len, advice))
755            .await?;
756        Ok(())
757    }
758
759    pub(crate) async fn set_size(&self, size: u64) -> Result<(), ErrorCode> {
760        if !self.perms.contains(FilePerms::WRITE) {
761            return Err(ErrorCode::NotPermitted);
762        }
763        self.run_blocking(move |f| f.set_len(size)).await?;
764        Ok(())
765    }
766}
767
768#[derive(Clone)]
769pub struct Dir {
770    /// The operating system file descriptor this struct is mediating access
771    /// to.
772    ///
773    /// Wrapped in an Arc because a copy is needed for `run_blocking`.
774    pub dir: Arc<cap_std::fs::Dir>,
775    /// Permissions to enforce on access to this directory. These permissions
776    /// are specified by a user of the `crate::WasiCtxBuilder`, and
777    /// are enforced prior to any enforced by the underlying operating system.
778    ///
779    /// These permissions are also enforced on any directories opened under
780    /// this directory.
781    pub perms: DirPerms,
782    /// Permissions to enforce on any files opened under this directory.
783    pub file_perms: FilePerms,
784    /// The mode the directory was opened under: bits for reading, and writing.
785    /// Required to correctly report the DescriptorFlags, because cap-std
786    /// doesn't presently provide a cross-platform equivalent of reading the
787    /// oflags back out using fcntl.
788    pub open_mode: OpenMode,
789
790    pub(crate) allow_blocking_current_thread: bool,
791}
792
793impl Dir {
794    pub fn new(
795        dir: cap_std::fs::Dir,
796        perms: DirPerms,
797        file_perms: FilePerms,
798        open_mode: OpenMode,
799        allow_blocking_current_thread: bool,
800    ) -> Self {
801        Dir {
802            dir: Arc::new(dir),
803            perms,
804            file_perms,
805            open_mode,
806            allow_blocking_current_thread,
807        }
808    }
809
810    /// Execute the blocking `body` function.
811    ///
812    /// Depending on how the WasiCtx was configured, the body may either be:
813    /// - Executed directly on the current thread. In this case the `async`
814    ///   signature of this method is effectively a lie and the returned
815    ///   Future will always be immediately Ready. Or:
816    /// - Spawned on a background thread using [`tokio::task::spawn_blocking`]
817    ///   and immediately awaited.
818    ///
819    /// Intentionally blocking the executor thread might seem unorthodox, but is
820    /// not actually a problem for specific workloads. See:
821    /// - [`crate::WasiCtxBuilder::allow_blocking_current_thread`]
822    /// - [Poor performance of wasmtime file I/O maybe because tokio](https://github.com/bytecodealliance/wasmtime/issues/7973)
823    /// - [Implement opt-in for enabling WASI to block the current thread](https://github.com/bytecodealliance/wasmtime/pull/8190)
824    pub(crate) async fn run_blocking<F, R>(&self, body: F) -> R
825    where
826        F: FnOnce(&cap_std::fs::Dir) -> R + Send + 'static,
827        R: Send + 'static,
828    {
829        if self.allow_blocking_current_thread {
830            body(&self.dir)
831        } else {
832            let d = self.dir.clone();
833            spawn_blocking(move || body(&d)).await
834        }
835    }
836
837    /// Returns reference to the underlying [`cap_std::fs::Dir`]
838    #[cfg(feature = "p3")]
839    pub(crate) fn as_dir(&self) -> &Arc<cap_std::fs::Dir> {
840        &self.dir
841    }
842
843    pub(crate) async fn create_directory_at(&self, path: String) -> Result<(), ErrorCode> {
844        if !self.perms.contains(DirPerms::MUTATE) {
845            return Err(ErrorCode::NotPermitted);
846        }
847        self.run_blocking(move |d| d.create_dir(&path)).await?;
848        Ok(())
849    }
850
851    pub(crate) async fn stat_at(
852        &self,
853        path_flags: PathFlags,
854        path: String,
855    ) -> Result<DescriptorStat, ErrorCode> {
856        if !self.perms.contains(DirPerms::READ) {
857            return Err(ErrorCode::NotPermitted);
858        }
859
860        let meta = if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
861            self.run_blocking(move |d| d.metadata(&path)).await?
862        } else {
863            self.run_blocking(move |d| d.symlink_metadata(&path))
864                .await?
865        };
866        Ok(meta.into())
867    }
868
869    pub(crate) async fn set_times_at(
870        &self,
871        path_flags: PathFlags,
872        path: String,
873        atim: Option<SystemTimeSpec>,
874        mtim: Option<SystemTimeSpec>,
875    ) -> Result<(), ErrorCode> {
876        use cap_fs_ext::DirExt as _;
877
878        if !self.perms.contains(DirPerms::MUTATE) {
879            return Err(ErrorCode::NotPermitted);
880        }
881        if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
882            self.run_blocking(move |d| {
883                d.set_times(
884                    &path,
885                    atim.map(cap_fs_ext::SystemTimeSpec::from_std),
886                    mtim.map(cap_fs_ext::SystemTimeSpec::from_std),
887                )
888            })
889            .await?;
890        } else {
891            self.run_blocking(move |d| {
892                d.set_symlink_times(
893                    &path,
894                    atim.map(cap_fs_ext::SystemTimeSpec::from_std),
895                    mtim.map(cap_fs_ext::SystemTimeSpec::from_std),
896                )
897            })
898            .await?;
899        }
900        Ok(())
901    }
902
903    pub(crate) async fn link_at(
904        &self,
905        old_path_flags: PathFlags,
906        old_path: String,
907        new_dir: &Self,
908        new_path: String,
909    ) -> Result<(), ErrorCode> {
910        if !self.perms.contains(DirPerms::MUTATE) {
911            return Err(ErrorCode::NotPermitted);
912        }
913        if !new_dir.perms.contains(DirPerms::MUTATE) {
914            return Err(ErrorCode::NotPermitted);
915        }
916        if old_path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
917            return Err(ErrorCode::Invalid);
918        }
919        let new_dir_handle = Arc::clone(&new_dir.dir);
920        self.run_blocking(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path))
921            .await?;
922        Ok(())
923    }
924
925    pub(crate) async fn open_at(
926        &self,
927        path_flags: PathFlags,
928        path: String,
929        oflags: OpenFlags,
930        flags: DescriptorFlags,
931        allow_blocking_current_thread: bool,
932    ) -> Result<Descriptor, ErrorCode> {
933        use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt};
934
935        if !self.perms.contains(DirPerms::READ) {
936            return Err(ErrorCode::NotPermitted);
937        }
938
939        if !self.perms.contains(DirPerms::MUTATE) {
940            if oflags.contains(OpenFlags::CREATE) || oflags.contains(OpenFlags::TRUNCATE) {
941                return Err(ErrorCode::NotPermitted);
942            }
943            if flags.contains(DescriptorFlags::WRITE) {
944                return Err(ErrorCode::NotPermitted);
945            }
946        }
947
948        // Track whether we are creating file, for permission check:
949        let mut create = false;
950        // Track open mode, for permission check and recording in created descriptor:
951        let mut open_mode = OpenMode::empty();
952        // Construct the OpenOptions to give the OS:
953        let mut opts = cap_std::fs::OpenOptions::new();
954        opts.maybe_dir(true);
955
956        if oflags.contains(OpenFlags::CREATE) {
957            if oflags.contains(OpenFlags::EXCLUSIVE) {
958                opts.create_new(true);
959            } else {
960                opts.create(true);
961            }
962            create = true;
963            opts.write(true);
964            open_mode |= OpenMode::WRITE;
965        }
966
967        if oflags.contains(OpenFlags::TRUNCATE) {
968            opts.truncate(true).write(true);
969        }
970        if flags.contains(DescriptorFlags::READ) {
971            opts.read(true);
972            open_mode |= OpenMode::READ;
973        }
974        if flags.contains(DescriptorFlags::WRITE) {
975            opts.write(true);
976            open_mode |= OpenMode::WRITE;
977        } else {
978            // If not opened write, open read. This way the OS lets us open
979            // the file, but we can use perms to reject use of the file later.
980            opts.read(true);
981            open_mode |= OpenMode::READ;
982        }
983        if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
984            opts.follow(FollowSymlinks::Yes);
985        } else {
986            opts.follow(FollowSymlinks::No);
987        }
988
989        // These flags are not yet supported in cap-std:
990        if flags.contains(DescriptorFlags::FILE_INTEGRITY_SYNC)
991            || flags.contains(DescriptorFlags::DATA_INTEGRITY_SYNC)
992            || flags.contains(DescriptorFlags::REQUESTED_WRITE_SYNC)
993        {
994            return Err(ErrorCode::Unsupported);
995        }
996
997        if oflags.contains(OpenFlags::DIRECTORY) {
998            if oflags.contains(OpenFlags::CREATE)
999                || oflags.contains(OpenFlags::EXCLUSIVE)
1000                || oflags.contains(OpenFlags::TRUNCATE)
1001            {
1002                return Err(ErrorCode::Invalid);
1003            }
1004        }
1005
1006        // Now enforce this WasiCtx's permissions before letting the OS have
1007        // its shot:
1008        if !self.perms.contains(DirPerms::MUTATE) && create {
1009            return Err(ErrorCode::NotPermitted);
1010        }
1011        if !self.file_perms.contains(FilePerms::WRITE) && open_mode.contains(OpenMode::WRITE) {
1012            return Err(ErrorCode::NotPermitted);
1013        }
1014
1015        // Represents each possible outcome from the spawn_blocking operation.
1016        // This makes sure we don't have to give spawn_blocking any way to
1017        // manipulate the table.
1018        enum OpenResult {
1019            Dir(cap_std::fs::Dir),
1020            File(cap_std::fs::File),
1021            NotDir,
1022        }
1023
1024        let opened = self
1025            .run_blocking::<_, std::io::Result<OpenResult>>(move |d| {
1026                let opened = d.open_with(&path, &opts)?;
1027                if opened.metadata()?.is_dir() {
1028                    Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file(
1029                        opened.into_std(),
1030                    )))
1031                } else if oflags.contains(OpenFlags::DIRECTORY) {
1032                    Ok(OpenResult::NotDir)
1033                } else {
1034                    Ok(OpenResult::File(opened))
1035                }
1036            })
1037            .await?;
1038
1039        match opened {
1040            // Paper over a divergence between Windows and POSIX, where
1041            // POSIX returns EISDIR if you open a directory with the
1042            // WRITE flag: https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html#:~:text=EISDIR
1043            #[cfg(windows)]
1044            OpenResult::Dir(_) if flags.contains(DescriptorFlags::WRITE) => {
1045                Err(ErrorCode::IsDirectory)
1046            }
1047
1048            OpenResult::Dir(dir) => Ok(Descriptor::Dir(Dir::new(
1049                dir,
1050                self.perms,
1051                self.file_perms,
1052                open_mode,
1053                allow_blocking_current_thread,
1054            ))),
1055
1056            OpenResult::File(file) => Ok(Descriptor::File(File::new(
1057                file,
1058                self.file_perms,
1059                open_mode,
1060                allow_blocking_current_thread,
1061            ))),
1062
1063            OpenResult::NotDir => Err(ErrorCode::NotDirectory),
1064        }
1065    }
1066
1067    pub(crate) async fn readlink_at(&self, path: String) -> Result<String, ErrorCode> {
1068        if !self.perms.contains(DirPerms::READ) {
1069            return Err(ErrorCode::NotPermitted);
1070        }
1071        let link = self.run_blocking(move |d| d.read_link(&path)).await?;
1072        link.into_os_string()
1073            .into_string()
1074            .or(Err(ErrorCode::IllegalByteSequence))
1075    }
1076
1077    pub(crate) async fn remove_directory_at(&self, path: String) -> Result<(), ErrorCode> {
1078        if !self.perms.contains(DirPerms::MUTATE) {
1079            return Err(ErrorCode::NotPermitted);
1080        }
1081        self.run_blocking(move |d| d.remove_dir(&path)).await?;
1082        Ok(())
1083    }
1084
1085    pub(crate) async fn rename_at(
1086        &self,
1087        old_path: String,
1088        new_dir: &Self,
1089        new_path: String,
1090    ) -> Result<(), ErrorCode> {
1091        if !self.perms.contains(DirPerms::MUTATE) {
1092            return Err(ErrorCode::NotPermitted);
1093        }
1094        if !new_dir.perms.contains(DirPerms::MUTATE) {
1095            return Err(ErrorCode::NotPermitted);
1096        }
1097        let new_dir_handle = Arc::clone(&new_dir.dir);
1098        self.run_blocking(move |d| d.rename(&old_path, &new_dir_handle, &new_path))
1099            .await?;
1100        Ok(())
1101    }
1102
1103    pub(crate) async fn symlink_at(
1104        &self,
1105        src_path: String,
1106        dest_path: String,
1107    ) -> Result<(), ErrorCode> {
1108        // On windows, Dir.symlink is provided by DirExt
1109        #[cfg(windows)]
1110        use cap_fs_ext::DirExt;
1111
1112        if !self.perms.contains(DirPerms::MUTATE) {
1113            return Err(ErrorCode::NotPermitted);
1114        }
1115        self.run_blocking(move |d| d.symlink(&src_path, &dest_path))
1116            .await?;
1117        Ok(())
1118    }
1119
1120    pub(crate) async fn unlink_file_at(&self, path: String) -> Result<(), ErrorCode> {
1121        use cap_fs_ext::DirExt;
1122
1123        if !self.perms.contains(DirPerms::MUTATE) {
1124            return Err(ErrorCode::NotPermitted);
1125        }
1126        self.run_blocking(move |d| d.remove_file_or_symlink(&path))
1127            .await?;
1128        Ok(())
1129    }
1130
1131    pub(crate) async fn metadata_hash_at(
1132        &self,
1133        path_flags: PathFlags,
1134        path: String,
1135    ) -> Result<MetadataHashValue, ErrorCode> {
1136        // No permissions check on metadata: if dir opened, allowed to stat it
1137        let meta = self
1138            .run_blocking(move |d| {
1139                if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
1140                    d.metadata(path)
1141                } else {
1142                    d.symlink_metadata(path)
1143                }
1144            })
1145            .await?;
1146        Ok(MetadataHashValue::from(&meta))
1147    }
1148}
1149
1150impl WasiFilesystemCtxView<'_> {
1151    pub(crate) fn get_directories(
1152        &mut self,
1153    ) -> wasmtime::Result<Vec<(Resource<Descriptor>, String)>> {
1154        let preopens = self.ctx.preopens.clone();
1155        let mut results = Vec::with_capacity(preopens.len());
1156        for (dir, name) in preopens {
1157            let fd = self
1158                .table
1159                .push(Descriptor::Dir(dir))
1160                .with_context(|| format!("failed to push preopen {name}"))?;
1161            results.push((fd, name));
1162        }
1163        Ok(results)
1164    }
1165}