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
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
244/// The type of a filesystem object referenced by a descriptor.
245///
246/// Note: This was called `filetype` in earlier versions of WASI.
247pub(crate) enum DescriptorType {
248    /// The type of the descriptor or file is unknown or is different from
249    /// any of the other types specified.
250    Unknown,
251    /// The descriptor refers to a block device inode.
252    #[cfg_attr(
253        windows,
254        expect(dead_code, reason = "windows has no notion of block devices")
255    )]
256    BlockDevice,
257    /// The descriptor refers to a character device inode.
258    CharacterDevice,
259    /// The descriptor refers to a directory inode.
260    Directory,
261    /// The file refers to a symbolic link inode.
262    SymbolicLink,
263    /// The descriptor refers to a regular file inode.
264    RegularFile,
265}
266
267impl From<cap_primitives::fs::FileType> for DescriptorType {
268    fn from(ft: cap_primitives::fs::FileType) -> Self {
269        if ft.is_dir() {
270            DescriptorType::Directory
271        } else if ft.is_symlink() {
272            DescriptorType::SymbolicLink
273        } else if ft.is_file() {
274            DescriptorType::RegularFile
275        } else {
276            sys::descriptor_type(ft)
277        }
278    }
279}
280
281/// File attributes.
282///
283/// Note: This was called `filestat` in earlier versions of WASI.
284pub(crate) struct DescriptorStat {
285    /// File type.
286    pub type_: DescriptorType,
287    /// Number of hard links to the file.
288    pub link_count: u64,
289    /// For regular files, the file size in bytes. For symbolic links, the
290    /// length in bytes of the pathname contained in the symbolic link.
291    pub size: u64,
292    /// Last data access timestamp.
293    ///
294    /// If the `option` is none, the platform doesn't maintain an access
295    /// timestamp for this file.
296    pub data_access_timestamp: Option<Datetime>,
297    /// Last data modification timestamp.
298    ///
299    /// If the `option` is none, the platform doesn't maintain a
300    /// modification timestamp for this file.
301    pub data_modification_timestamp: Option<Datetime>,
302    /// Last file status-change timestamp.
303    ///
304    /// If the `option` is none, the platform doesn't maintain a
305    /// status-change timestamp for this file.
306    pub status_change_timestamp: Option<Datetime>,
307}
308
309impl DescriptorStat {
310    /// Creates a `DescriptorStat` from a `Metadata` plus the hard link
311    /// count.
312    fn new(meta: &Metadata, link_count: u64) -> Self {
313        Self {
314            type_: meta.file_type().into(),
315            link_count,
316            size: meta.len(),
317            data_access_timestamp: meta
318                .accessed()
319                .ok()
320                .and_then(|t| Datetime::try_from(t.into_std()).ok()),
321            data_modification_timestamp: meta
322                .modified()
323                .ok()
324                .and_then(|t| Datetime::try_from(t.into_std()).ok()),
325            status_change_timestamp: meta
326                .created()
327                .ok()
328                .and_then(|t| Datetime::try_from(t.into_std()).ok()),
329        }
330    }
331}
332
333/// A 128-bit hash value, split into parts because wasm doesn't have a
334/// 128-bit integer type.
335pub(crate) struct MetadataHashValue {
336    /// 64 bits of a 128-bit hash value.
337    pub lower: u64,
338    /// Another 64 bits of a 128-bit hash value.
339    pub upper: u64,
340}
341
342impl MetadataHashValue {
343    /// Creates a hash value from a file's unique identity, e.g. a
344    /// device/inode number pair.
345    fn new(identity: impl std::hash::Hash) -> Self {
346        // Without incurring any deps, std provides us with a 64 bit hash
347        // function:
348        use std::hash::Hasher as _;
349        // Note that this means that the metadata hash (which becomes a preview1 ino) may
350        // change when a different rustc release is used to build this host implementation:
351        let mut hasher = hash_map::DefaultHasher::new();
352        identity.hash(&mut hasher);
353        let lower = hasher.finish();
354        // MetadataHashValue has a pair of 64-bit members for representing a
355        // single 128-bit number. However, we only have 64 bits of entropy. To
356        // synthesize the upper 64 bits, lets xor the lower half with an arbitrary
357        // constant, in this case the 64 bit integer corresponding to the IEEE
358        // double representation of (a number as close as possible to) pi.
359        // This seems better than just repeating the same bits in the upper and
360        // lower parts outright, which could make folks wonder if the struct was
361        // mangled in the ABI, or worse yet, lead to consumers of this interface
362        // expecting them to be equal.
363        let upper = lower ^ 4614256656552045848u64;
364        Self { lower, upper }
365    }
366}
367
368#[derive(Copy, Clone, Debug)]
369pub(crate) enum Advice {
370    Normal,
371    Sequential,
372    Random,
373    WillNeed,
374    DontNeed,
375    NoReuse,
376}
377
378#[cfg(unix)]
379fn from_raw_os_error(err: Option<i32>) -> Option<ErrorCode> {
380    use rustix::io::Errno as RustixErrno;
381    if err.is_none() {
382        return None;
383    }
384    Some(match RustixErrno::from_raw_os_error(err.unwrap()) {
385        RustixErrno::PIPE => ErrorCode::Pipe,
386        RustixErrno::PERM => ErrorCode::NotPermitted,
387        RustixErrno::NOENT => ErrorCode::NoEntry,
388        RustixErrno::NOMEM => ErrorCode::InsufficientMemory,
389        RustixErrno::IO => ErrorCode::Io,
390        RustixErrno::BADF => ErrorCode::BadDescriptor,
391        RustixErrno::BUSY => ErrorCode::Busy,
392        RustixErrno::ACCESS => ErrorCode::Access,
393        RustixErrno::NOTDIR => ErrorCode::NotDirectory,
394        RustixErrno::ISDIR => ErrorCode::IsDirectory,
395        RustixErrno::INVAL => ErrorCode::Invalid,
396        RustixErrno::EXIST => ErrorCode::Exist,
397        RustixErrno::FBIG => ErrorCode::FileTooLarge,
398        RustixErrno::NOSPC => ErrorCode::InsufficientSpace,
399        RustixErrno::SPIPE => ErrorCode::InvalidSeek,
400        RustixErrno::MLINK => ErrorCode::TooManyLinks,
401        RustixErrno::NAMETOOLONG => ErrorCode::NameTooLong,
402        RustixErrno::NOTEMPTY => ErrorCode::NotEmpty,
403        RustixErrno::LOOP => ErrorCode::Loop,
404        RustixErrno::OVERFLOW => ErrorCode::Overflow,
405        RustixErrno::ILSEQ => ErrorCode::IllegalByteSequence,
406        RustixErrno::NOTSUP => ErrorCode::Unsupported,
407        RustixErrno::ALREADY => ErrorCode::Already,
408        RustixErrno::INPROGRESS => ErrorCode::InProgress,
409        RustixErrno::INTR => ErrorCode::Interrupted,
410
411        // On some platforms, these have the same value as other errno values.
412        #[allow(unreachable_patterns, reason = "see comment")]
413        RustixErrno::OPNOTSUPP => ErrorCode::Unsupported,
414
415        _ => return None,
416    })
417}
418
419#[cfg(windows)]
420fn from_raw_os_error(raw_os_error: Option<i32>) -> Option<ErrorCode> {
421    use windows_sys::Win32::Foundation;
422    Some(match raw_os_error.map(|code| code as u32) {
423        Some(Foundation::ERROR_FILE_NOT_FOUND) => ErrorCode::NoEntry,
424        Some(Foundation::ERROR_PATH_NOT_FOUND) => ErrorCode::NoEntry,
425        Some(Foundation::ERROR_ACCESS_DENIED) => ErrorCode::Access,
426        Some(Foundation::ERROR_SHARING_VIOLATION) => ErrorCode::Access,
427        Some(Foundation::ERROR_PRIVILEGE_NOT_HELD) => ErrorCode::NotPermitted,
428        Some(Foundation::ERROR_INVALID_HANDLE) => ErrorCode::BadDescriptor,
429        Some(Foundation::ERROR_INVALID_NAME) => ErrorCode::NoEntry,
430        Some(Foundation::ERROR_NOT_ENOUGH_MEMORY) => ErrorCode::InsufficientMemory,
431        Some(Foundation::ERROR_OUTOFMEMORY) => ErrorCode::InsufficientMemory,
432        Some(Foundation::ERROR_DIR_NOT_EMPTY) => ErrorCode::NotEmpty,
433        Some(Foundation::ERROR_NOT_READY) => ErrorCode::Busy,
434        Some(Foundation::ERROR_BUSY) => ErrorCode::Busy,
435        Some(Foundation::ERROR_NOT_SUPPORTED) => ErrorCode::Unsupported,
436        Some(Foundation::ERROR_FILE_EXISTS) => ErrorCode::Exist,
437        Some(Foundation::ERROR_BROKEN_PIPE) => ErrorCode::Pipe,
438        Some(Foundation::ERROR_BUFFER_OVERFLOW) => ErrorCode::NameTooLong,
439        Some(Foundation::ERROR_NOT_A_REPARSE_POINT) => ErrorCode::Invalid,
440        Some(Foundation::ERROR_NEGATIVE_SEEK) => ErrorCode::Invalid,
441        Some(Foundation::ERROR_DIRECTORY) => ErrorCode::NotDirectory,
442        Some(Foundation::ERROR_ALREADY_EXISTS) => ErrorCode::Exist,
443        Some(Foundation::ERROR_STOPPED_ON_SYMLINK) => ErrorCode::Loop,
444        Some(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED) => ErrorCode::IsDirectory,
445        _ => return None,
446    })
447}
448
449impl<'a> From<&'a std::io::Error> for ErrorCode {
450    fn from(err: &'a std::io::Error) -> ErrorCode {
451        match from_raw_os_error(err.raw_os_error()) {
452            Some(errno) => errno,
453            None => {
454                debug!("unknown raw os error: {err}");
455                match err.kind() {
456                    std::io::ErrorKind::NotFound => ErrorCode::NoEntry,
457                    std::io::ErrorKind::PermissionDenied => ErrorCode::NotPermitted,
458                    std::io::ErrorKind::AlreadyExists => ErrorCode::Exist,
459                    std::io::ErrorKind::InvalidInput => ErrorCode::Invalid,
460                    _ => ErrorCode::Io,
461                }
462            }
463        }
464    }
465}
466
467impl From<std::io::Error> for ErrorCode {
468    fn from(err: std::io::Error) -> ErrorCode {
469        ErrorCode::from(&err)
470    }
471}
472
473#[derive(Clone)]
474pub enum Descriptor {
475    File(File),
476    Dir(Dir),
477}
478
479impl Descriptor {
480    pub(crate) fn file(&self) -> Result<&File, ErrorCode> {
481        match self {
482            Descriptor::File(f) => Ok(f),
483            Descriptor::Dir(_) => Err(ErrorCode::BadDescriptor),
484        }
485    }
486
487    pub(crate) fn dir(&self) -> Result<&Dir, ErrorCode> {
488        match self {
489            Descriptor::Dir(d) => Ok(d),
490            Descriptor::File(_) => Err(ErrorCode::NotDirectory),
491        }
492    }
493
494    pub(crate) async fn sync_data(&self) -> Result<(), ErrorCode> {
495        match self {
496            Self::File(f) => {
497                match f.run_blocking(|f| f.sync_data()).await {
498                    Ok(()) => Ok(()),
499                    // On windows, `sync_data` uses `FileFlushBuffers` which fails with
500                    // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore
501                    // this error, for POSIX compatibility.
502                    #[cfg(windows)]
503                    Err(err)
504                        if err.raw_os_error()
505                            == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) =>
506                    {
507                        Ok(())
508                    }
509                    Err(err) => Err(err.into()),
510                }
511            }
512            Self::Dir(d) => {
513                d.run_blocking(|d| {
514                    let d = cap_primitives::fs::open(
515                        d,
516                        std::path::Component::CurDir.as_ref(),
517                        OpenOptions::new().read(true),
518                    )?;
519                    d.sync_data()?;
520                    Ok(())
521                })
522                .await
523            }
524        }
525    }
526
527    pub(crate) async fn get_flags(&self) -> Result<DescriptorFlags, ErrorCode> {
528        match self {
529            Self::File(f) => {
530                let mut flags = f.run_blocking(|f| sys::get_flags(f)).await?;
531                if f.open_mode.contains(OpenMode::READ) {
532                    flags |= DescriptorFlags::READ;
533                }
534                if f.open_mode.contains(OpenMode::WRITE) {
535                    flags |= DescriptorFlags::WRITE;
536                }
537                Ok(flags)
538            }
539            Self::Dir(d) => {
540                let mut flags = d.run_blocking(|d| sys::get_flags(d)).await?;
541                if d.open_mode.contains(OpenMode::READ) {
542                    flags |= DescriptorFlags::READ;
543                }
544                if d.open_mode.contains(OpenMode::WRITE) {
545                    flags |= DescriptorFlags::MUTATE_DIRECTORY;
546                }
547                Ok(flags)
548            }
549        }
550    }
551
552    pub(crate) async fn get_type(&self) -> Result<DescriptorType, ErrorCode> {
553        match self {
554            Self::File(f) => {
555                let meta = f.run_blocking(|f| Metadata::from_file(f)).await?;
556                Ok(meta.file_type().into())
557            }
558            Self::Dir(_) => Ok(DescriptorType::Directory),
559        }
560    }
561
562    pub(crate) async fn set_times(
563        &self,
564        atim: Option<SystemTime>,
565        mtim: Option<SystemTime>,
566    ) -> Result<(), ErrorCode> {
567        let mut times = std::fs::FileTimes::new();
568        if let Some(atim) = atim {
569            times = times.set_accessed(atim);
570        }
571        if let Some(mtim) = mtim {
572            times = times.set_modified(mtim);
573        }
574        match self {
575            Self::File(f) => {
576                if !f.perms.contains(FilePerms::WRITE) {
577                    return Err(ErrorCode::NotPermitted);
578                }
579                f.run_blocking(move |f| f.set_times(times)).await?;
580                Ok(())
581            }
582            Self::Dir(d) => {
583                if !d.perms.contains(DirPerms::MUTATE) {
584                    return Err(ErrorCode::NotPermitted);
585                }
586                d.run_blocking(move |d| d.set_times(times)).await?;
587                Ok(())
588            }
589        }
590    }
591
592    pub(crate) async fn sync(&self) -> Result<(), ErrorCode> {
593        match self {
594            Self::File(f) => {
595                match f.run_blocking(|f| f.sync_all()).await {
596                    Ok(()) => Ok(()),
597                    // On windows, `sync_data` uses `FileFlushBuffers` which fails with
598                    // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore
599                    // this error, for POSIX compatibility.
600                    #[cfg(windows)]
601                    Err(err)
602                        if err.raw_os_error()
603                            == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) =>
604                    {
605                        Ok(())
606                    }
607                    Err(err) => Err(err.into()),
608                }
609            }
610            Self::Dir(d) => {
611                d.run_blocking(|d| {
612                    let d = cap_primitives::fs::open(
613                        d,
614                        std::path::Component::CurDir.as_ref(),
615                        OpenOptions::new().read(true),
616                    )?;
617                    d.sync_all()?;
618                    Ok(())
619                })
620                .await
621            }
622        }
623    }
624
625    pub(crate) async fn stat(&self) -> Result<DescriptorStat, ErrorCode> {
626        match self {
627            Self::File(f) => Ok(f.run_blocking(|f| sys::stat(f)).await?),
628            Self::Dir(d) => Ok(d.run_blocking(|f| sys::stat(f)).await?),
629        }
630    }
631
632    pub(crate) async fn is_same_object(&self, other: &Self) -> wasmtime::Result<bool> {
633        // No permissions check on metadata: if opened, allowed to stat it
634        let other = match other {
635            Self::File(f) => Arc::clone(&f.file),
636            Self::Dir(d) => Arc::clone(&d.dir),
637        };
638        Ok(match self {
639            Self::File(f) => {
640                f.run_blocking(move |f| sys::is_same_file(f, &other))
641                    .await?
642            }
643            Self::Dir(d) => {
644                d.run_blocking(move |d| sys::is_same_file(d, &other))
645                    .await?
646            }
647        })
648    }
649
650    pub(crate) async fn metadata_hash(&self) -> Result<MetadataHashValue, ErrorCode> {
651        match self {
652            Self::File(f) => Ok(f.run_blocking(|f| sys::metadata_hash(f)).await?),
653            Self::Dir(d) => Ok(d.run_blocking(|d| sys::metadata_hash(d)).await?),
654        }
655    }
656}
657
658#[derive(Clone)]
659pub struct File {
660    /// The operating system File this struct is mediating access to.
661    ///
662    /// Wrapped in an Arc because the same underlying file is used for
663    /// implementing the stream types. A copy is also needed for
664    /// `spawn_blocking`.
665    pub file: Arc<std::fs::File>,
666    /// Permissions to enforce on access to the file. These permissions are
667    /// specified by a user of the `crate::WasiCtxBuilder`, and are
668    /// enforced prior to any enforced by the underlying operating system.
669    pub perms: FilePerms,
670    /// The mode the file was opened under: bits for reading, and writing.
671    /// Required to correctly report the DescriptorFlags, because
672    /// cap-primitives doesn't presently provide a cross-platform equivalent
673    /// of reading the oflags back out using fcntl.
674    pub open_mode: OpenMode,
675
676    allow_blocking_current_thread: bool,
677}
678
679impl File {
680    pub fn new(
681        file: std::fs::File,
682        perms: FilePerms,
683        open_mode: OpenMode,
684        allow_blocking_current_thread: bool,
685    ) -> Self {
686        Self {
687            file: Arc::new(file),
688            perms,
689            open_mode,
690            allow_blocking_current_thread,
691        }
692    }
693
694    /// Execute the blocking `body` function.
695    ///
696    /// Depending on how the WasiCtx was configured, the body may either be:
697    /// - Executed directly on the current thread. In this case the `async`
698    ///   signature of this method is effectively a lie and the returned
699    ///   Future will always be immediately Ready. Or:
700    /// - Spawned on a background thread using [`tokio::task::spawn_blocking`]
701    ///   and immediately awaited.
702    ///
703    /// Intentionally blocking the executor thread might seem unorthodox, but is
704    /// not actually a problem for specific workloads. See:
705    /// - [`crate::WasiCtxBuilder::allow_blocking_current_thread`]
706    /// - [Poor performance of wasmtime file I/O maybe because tokio](https://github.com/bytecodealliance/wasmtime/issues/7973)
707    /// - [Implement opt-in for enabling WASI to block the current thread](https://github.com/bytecodealliance/wasmtime/pull/8190)
708    pub(crate) async fn run_blocking<F, R>(&self, body: F) -> R
709    where
710        F: FnOnce(&std::fs::File) -> R + Send + 'static,
711        R: Send + 'static,
712    {
713        match self.as_blocking_file() {
714            Some(file) => body(file),
715            None => self.spawn_blocking(body).await,
716        }
717    }
718
719    pub(crate) fn spawn_blocking<F, R>(&self, body: F) -> AbortOnDropJoinHandle<R>
720    where
721        F: FnOnce(&std::fs::File) -> R + Send + 'static,
722        R: Send + 'static,
723    {
724        let f = self.file.clone();
725        spawn_blocking(move || body(&f))
726    }
727
728    /// Returns `Some` when the current thread is allowed to block in filesystem
729    /// operations, and otherwise returns `None` to indicate that
730    /// `spawn_blocking` must be used.
731    pub(crate) fn as_blocking_file(&self) -> Option<&std::fs::File> {
732        if self.allow_blocking_current_thread {
733            Some(&self.file)
734        } else {
735            None
736        }
737    }
738
739    /// Returns reference to the underlying [`std::fs::File`]
740    #[cfg(feature = "p3")]
741    pub(crate) fn as_file(&self) -> &Arc<std::fs::File> {
742        &self.file
743    }
744
745    pub(crate) async fn advise(
746        &self,
747        offset: u64,
748        len: u64,
749        advice: Advice,
750    ) -> Result<(), ErrorCode> {
751        self.run_blocking(move |f| sys::advise(f, offset, len, advice))
752            .await?;
753        Ok(())
754    }
755
756    pub(crate) async fn set_size(&self, size: u64) -> Result<(), ErrorCode> {
757        if !self.perms.contains(FilePerms::WRITE) {
758            return Err(ErrorCode::NotPermitted);
759        }
760        self.run_blocking(move |f| f.set_len(size)).await?;
761        Ok(())
762    }
763}
764
765#[derive(Clone)]
766pub struct Dir {
767    /// The operating system file descriptor this struct is mediating access
768    /// to.
769    ///
770    /// This is a handle to a directory, and all paths accessed through this
771    /// struct are sandboxed to be within this directory via `cap-primitives`.
772    ///
773    /// Wrapped in an Arc because a copy is needed for `run_blocking`.
774    pub dir: Arc<std::fs::File>,
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
786    /// cap-primitives doesn't presently provide a cross-platform equivalent
787    /// of reading the 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: std::fs::File,
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(&std::fs::File) -> 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 directory handle.
838    #[cfg(feature = "p3")]
839    pub(crate) fn as_dir(&self) -> &Arc<std::fs::File> {
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| {
848            cap_primitives::fs::create_dir(d, path.as_ref(), &DirOptions::new())
849        })
850        .await?;
851        Ok(())
852    }
853
854    pub(crate) async fn stat_at(
855        &self,
856        path_flags: PathFlags,
857        path: String,
858    ) -> Result<DescriptorStat, ErrorCode> {
859        if !self.perms.contains(DirPerms::READ) {
860            return Err(ErrorCode::NotPermitted);
861        }
862
863        let follow = if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
864            FollowSymlinks::Yes
865        } else {
866            FollowSymlinks::No
867        };
868        let ret = self
869            .run_blocking(move |d| sys::stat_at(d, path.as_ref(), follow))
870            .await?;
871        Ok(ret)
872    }
873
874    pub(crate) async fn set_times_at(
875        &self,
876        path_flags: PathFlags,
877        path: String,
878        atim: Option<SystemTime>,
879        mtim: Option<SystemTime>,
880    ) -> Result<(), ErrorCode> {
881        if !self.perms.contains(DirPerms::MUTATE) {
882            return Err(ErrorCode::NotPermitted);
883        }
884        let atim =
885            atim.map(|t| SystemTimeSpec::Absolute(cap_primitives::time::SystemTime::from_std(t)));
886        let mtim =
887            mtim.map(|t| SystemTimeSpec::Absolute(cap_primitives::time::SystemTime::from_std(t)));
888        if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
889            self.run_blocking(move |d| cap_primitives::fs::set_times(d, path.as_ref(), atim, mtim))
890                .await?;
891        } else {
892            self.run_blocking(move |d| {
893                cap_primitives::fs::set_times_nofollow(d, path.as_ref(), atim, mtim)
894            })
895            .await?;
896        }
897        Ok(())
898    }
899
900    pub(crate) async fn link_at(
901        &self,
902        old_path_flags: PathFlags,
903        old_path: String,
904        new_dir: &Self,
905        new_path: String,
906    ) -> Result<(), ErrorCode> {
907        if !self.perms.contains(DirPerms::MUTATE) {
908            return Err(ErrorCode::NotPermitted);
909        }
910        if !new_dir.perms.contains(DirPerms::MUTATE) {
911            return Err(ErrorCode::NotPermitted);
912        }
913        if old_path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
914            return Err(ErrorCode::Invalid);
915        }
916        if self.perms != new_dir.perms || self.file_perms != new_dir.file_perms {
917            return Err(ErrorCode::NotPermitted);
918        }
919        let new_dir_handle = Arc::clone(&new_dir.dir);
920        self.run_blocking(move |d| {
921            cap_primitives::fs::hard_link(d, old_path.as_ref(), &new_dir_handle, new_path.as_ref())
922        })
923        .await?;
924        Ok(())
925    }
926
927    pub(crate) async fn open_at(
928        &self,
929        path_flags: PathFlags,
930        path: String,
931        oflags: OpenFlags,
932        flags: DescriptorFlags,
933        allow_blocking_current_thread: bool,
934    ) -> Result<Descriptor, ErrorCode> {
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 = OpenOptions::new();
954        sys::maybe_dir(&mut opts);
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            open_mode |= OpenMode::WRITE;
970        }
971        if flags.contains(DescriptorFlags::READ) {
972            opts.read(true);
973            open_mode |= OpenMode::READ;
974        }
975        if flags.contains(DescriptorFlags::WRITE) {
976            opts.write(true);
977            open_mode |= OpenMode::WRITE;
978        } else {
979            // If not opened write, open read. This way the OS lets us open
980            // the file, but we can use perms to reject use of the file later.
981            opts.read(true);
982            open_mode |= OpenMode::READ;
983        }
984
985        // Note that this is intentionally scoped to a separate block to
986        // minimize the surface area that is depended on by cap-fs-ext. Ideally
987        // the underlying functionality in `cap-primitives` would get exposed,
988        // but that'll require an upstream PR.
989        {
990            use cap_fs_ext_avoid_using_this::OpenOptionsFollowExt;
991            if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
992                opts.follow(FollowSymlinks::Yes);
993            } else {
994                opts.follow(FollowSymlinks::No);
995            }
996        }
997
998        // These flags are not yet supported in cap-primitives:
999        if flags.contains(DescriptorFlags::FILE_INTEGRITY_SYNC)
1000            || flags.contains(DescriptorFlags::DATA_INTEGRITY_SYNC)
1001            || flags.contains(DescriptorFlags::REQUESTED_WRITE_SYNC)
1002        {
1003            return Err(ErrorCode::Unsupported);
1004        }
1005
1006        if oflags.contains(OpenFlags::DIRECTORY) {
1007            if oflags.contains(OpenFlags::CREATE)
1008                || oflags.contains(OpenFlags::EXCLUSIVE)
1009                || oflags.contains(OpenFlags::TRUNCATE)
1010            {
1011                return Err(ErrorCode::Invalid);
1012            }
1013        }
1014
1015        // Now enforce this WasiCtx's permissions before letting the OS have
1016        // its shot:
1017        if !self.perms.contains(DirPerms::MUTATE) && create {
1018            return Err(ErrorCode::NotPermitted);
1019        }
1020        if !self.file_perms.contains(FilePerms::WRITE) && open_mode.contains(OpenMode::WRITE) {
1021            return Err(ErrorCode::NotPermitted);
1022        }
1023
1024        // Represents each possible outcome from the spawn_blocking operation.
1025        // This makes sure we don't have to give spawn_blocking any way to
1026        // manipulate the table.
1027        enum OpenResult {
1028            Dir(std::fs::File),
1029            File(std::fs::File),
1030            NotDir,
1031        }
1032
1033        let opened = self
1034            .run_blocking::<_, std::io::Result<OpenResult>>(move |d| {
1035                let opened = cap_primitives::fs::open(d, path.as_ref(), &opts)?;
1036                if Metadata::from_file(&opened)?.is_dir() {
1037                    Ok(OpenResult::Dir(opened))
1038                } else if oflags.contains(OpenFlags::DIRECTORY) {
1039                    Ok(OpenResult::NotDir)
1040                } else {
1041                    Ok(OpenResult::File(opened))
1042                }
1043            })
1044            .await?;
1045
1046        match opened {
1047            // Paper over a divergence between Windows and POSIX, where
1048            // POSIX returns EISDIR if you open a directory with the
1049            // WRITE flag: https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html#:~:text=EISDIR
1050            #[cfg(windows)]
1051            OpenResult::Dir(_) if flags.contains(DescriptorFlags::WRITE) => {
1052                Err(ErrorCode::IsDirectory)
1053            }
1054
1055            OpenResult::Dir(dir) => Ok(Descriptor::Dir(Dir::new(
1056                dir,
1057                self.perms,
1058                self.file_perms,
1059                open_mode,
1060                allow_blocking_current_thread,
1061            ))),
1062
1063            OpenResult::File(file) => Ok(Descriptor::File(File::new(
1064                file,
1065                self.file_perms,
1066                open_mode,
1067                allow_blocking_current_thread,
1068            ))),
1069
1070            OpenResult::NotDir => Err(ErrorCode::NotDirectory),
1071        }
1072    }
1073
1074    pub(crate) async fn readlink_at(&self, path: String) -> Result<String, ErrorCode> {
1075        if !self.perms.contains(DirPerms::READ) {
1076            return Err(ErrorCode::NotPermitted);
1077        }
1078        let link = self
1079            .run_blocking(move |d| cap_primitives::fs::read_link(d, path.as_ref()))
1080            .await?;
1081        link.into_os_string()
1082            .into_string()
1083            .or(Err(ErrorCode::IllegalByteSequence))
1084    }
1085
1086    pub(crate) async fn remove_directory_at(&self, path: String) -> Result<(), ErrorCode> {
1087        if !self.perms.contains(DirPerms::MUTATE) {
1088            return Err(ErrorCode::NotPermitted);
1089        }
1090        self.run_blocking(move |d| cap_primitives::fs::remove_dir(d, path.as_ref()))
1091            .await?;
1092        Ok(())
1093    }
1094
1095    pub(crate) async fn rename_at(
1096        &self,
1097        old_path: String,
1098        new_dir: &Self,
1099        new_path: String,
1100    ) -> Result<(), ErrorCode> {
1101        if !self.perms.contains(DirPerms::MUTATE) {
1102            return Err(ErrorCode::NotPermitted);
1103        }
1104        if !new_dir.perms.contains(DirPerms::MUTATE) {
1105            return Err(ErrorCode::NotPermitted);
1106        }
1107        if self.perms != new_dir.perms || self.file_perms != new_dir.file_perms {
1108            return Err(ErrorCode::NotPermitted);
1109        }
1110        let new_dir_handle = Arc::clone(&new_dir.dir);
1111        self.run_blocking(move |d| {
1112            cap_primitives::fs::rename(d, old_path.as_ref(), &new_dir_handle, new_path.as_ref())
1113        })
1114        .await?;
1115        Ok(())
1116    }
1117
1118    pub(crate) async fn symlink_at(
1119        &self,
1120        src_path: String,
1121        dest_path: String,
1122    ) -> Result<(), ErrorCode> {
1123        if !self.perms.contains(DirPerms::MUTATE) {
1124            return Err(ErrorCode::NotPermitted);
1125        }
1126        self.run_blocking(move |d| sys::symlink(src_path.as_ref(), d, dest_path.as_ref()))
1127            .await?;
1128        Ok(())
1129    }
1130
1131    pub(crate) async fn unlink_file_at(&self, path: String) -> Result<(), ErrorCode> {
1132        if !self.perms.contains(DirPerms::MUTATE) {
1133            return Err(ErrorCode::NotPermitted);
1134        }
1135        self.run_blocking(move |d| sys::remove_file_or_symlink(d, path.as_ref()))
1136            .await?;
1137        Ok(())
1138    }
1139
1140    pub(crate) async fn metadata_hash_at(
1141        &self,
1142        path_flags: PathFlags,
1143        path: String,
1144    ) -> Result<MetadataHashValue, ErrorCode> {
1145        // No permissions check on metadata: if dir opened, allowed to stat it
1146        let follow = if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
1147            FollowSymlinks::Yes
1148        } else {
1149            FollowSymlinks::No
1150        };
1151        let hash = self
1152            .run_blocking(move |d| sys::metadata_hash_at(d, path.as_ref(), follow))
1153            .await?;
1154        Ok(hash)
1155    }
1156}
1157
1158impl WasiFilesystemCtxView<'_> {
1159    pub(crate) fn get_directories(
1160        &mut self,
1161    ) -> wasmtime::Result<Vec<(Resource<Descriptor>, String)>> {
1162        let preopens = self.ctx.preopens.clone();
1163        let mut results = Vec::with_capacity(preopens.len());
1164        for (dir, name) in preopens {
1165            let fd = self
1166                .table
1167                .push(Descriptor::Dir(dir))
1168                .with_context(|| format!("failed to push preopen {name}"))?;
1169            results.push((fd, name));
1170        }
1171        Ok(results)
1172    }
1173}