Skip to main content

wasmtime_wasi/
filesystem.rs

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