Path: blob/main/crates/wasi/src/filesystem.rs
3072 views
use crate::clocks::Datetime;1use crate::runtime::{AbortOnDropJoinHandle, spawn_blocking};2use cap_fs_ext::{FileTypeExt as _, MetadataExt as _};3use fs_set_times::SystemTimeSpec;4use std::collections::hash_map;5use std::sync::Arc;6use tracing::debug;7use wasmtime::component::{HasData, Resource, ResourceTable};8use wasmtime::error::Context as _;910/// A helper struct which implements [`HasData`] for the `wasi:filesystem` APIs.11///12/// This can be useful when directly calling `add_to_linker` functions directly,13/// such as [`wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker`] as14/// the `D` type parameter. See [`HasData`] for more information about the type15/// parameter's purpose.16///17/// When using this type you can skip the [`WasiFilesystemView`] trait, for18/// example.19///20/// [`wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker`]: crate::p2::bindings::filesystem::types::add_to_linker21///22/// # Examples23///24/// ```25/// use wasmtime::component::{Linker, ResourceTable};26/// use wasmtime::{Engine, Result};27/// use wasmtime_wasi::filesystem::*;28///29/// struct MyStoreState {30/// table: ResourceTable,31/// filesystem: WasiFilesystemCtx,32/// }33///34/// fn main() -> Result<()> {35/// let engine = Engine::default();36/// let mut linker = Linker::new(&engine);37///38/// wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker::<MyStoreState, WasiFilesystem>(39/// &mut linker,40/// |state| WasiFilesystemCtxView {41/// table: &mut state.table,42/// ctx: &mut state.filesystem,43/// },44/// )?;45/// Ok(())46/// }47/// ```48pub struct WasiFilesystem;4950impl HasData for WasiFilesystem {51type Data<'a> = WasiFilesystemCtxView<'a>;52}5354#[derive(Clone, Default)]55pub struct WasiFilesystemCtx {56pub(crate) allow_blocking_current_thread: bool,57pub(crate) preopens: Vec<(Dir, String)>,58}5960pub struct WasiFilesystemCtxView<'a> {61pub ctx: &'a mut WasiFilesystemCtx,62pub table: &'a mut ResourceTable,63}6465pub trait WasiFilesystemView: Send {66fn filesystem(&mut self) -> WasiFilesystemCtxView<'_>;67}6869bitflags::bitflags! {70#[derive(Copy, Clone, Debug, PartialEq, Eq)]71pub struct FilePerms: usize {72const READ = 0b1;73const WRITE = 0b10;74}75}7677bitflags::bitflags! {78#[derive(Copy, Clone, Debug, PartialEq, Eq)]79pub struct OpenMode: usize {80const READ = 0b1;81const WRITE = 0b10;82}83}8485bitflags::bitflags! {86/// Permission bits for operating on a directory.87///88/// Directories can be limited to being readonly. This will restrict what89/// can be done with them, for example preventing creation of new files.90#[derive(Copy, Clone, Debug, PartialEq, Eq)]91pub struct DirPerms: usize {92/// This directory can be read, for example its entries can be iterated93/// over and files can be opened.94const READ = 0b1;9596/// This directory can be mutated, for example by creating new files97/// within it.98const MUTATE = 0b10;99}100}101102bitflags::bitflags! {103/// Flags determining the method of how paths are resolved.104#[derive(Copy, Clone, Debug, PartialEq, Eq)]105pub(crate) struct PathFlags: usize {106/// This directory can be read, for example its entries can be iterated107/// over and files can be opened.108const SYMLINK_FOLLOW = 0b1;109}110}111112bitflags::bitflags! {113/// Open flags used by `open-at`.114#[derive(Copy, Clone, Debug, PartialEq, Eq)]115pub(crate) struct OpenFlags: usize {116/// Create file if it does not exist, similar to `O_CREAT` in POSIX.117const CREATE = 0b1;118/// Fail if not a directory, similar to `O_DIRECTORY` in POSIX.119const DIRECTORY = 0b10;120/// Fail if file already exists, similar to `O_EXCL` in POSIX.121const EXCLUSIVE = 0b100;122/// Truncate file to size 0, similar to `O_TRUNC` in POSIX.123const TRUNCATE = 0b1000;124}125}126127bitflags::bitflags! {128/// Descriptor flags.129///130/// Note: This was called `fdflags` in earlier versions of WASI.131#[derive(Copy, Clone, Debug, PartialEq, Eq)]132pub(crate) struct DescriptorFlags: usize {133/// Read mode: Data can be read.134const READ = 0b1;135/// Write mode: Data can be written to.136const WRITE = 0b10;137/// Request that writes be performed according to synchronized I/O file138/// integrity completion. The data stored in the file and the file's139/// metadata are synchronized. This is similar to `O_SYNC` in POSIX.140///141/// The precise semantics of this operation have not yet been defined for142/// WASI. At this time, it should be interpreted as a request, and not a143/// requirement.144const FILE_INTEGRITY_SYNC = 0b100;145/// Request that writes be performed according to synchronized I/O data146/// integrity completion. Only the data stored in the file is147/// synchronized. This is similar to `O_DSYNC` in POSIX.148///149/// The precise semantics of this operation have not yet been defined for150/// WASI. At this time, it should be interpreted as a request, and not a151/// requirement.152const DATA_INTEGRITY_SYNC = 0b1000;153/// Requests that reads be performed at the same level of integrity154/// requested for writes. This is similar to `O_RSYNC` in POSIX.155///156/// The precise semantics of this operation have not yet been defined for157/// WASI. At this time, it should be interpreted as a request, and not a158/// requirement.159const REQUESTED_WRITE_SYNC = 0b10000;160/// Mutating directories mode: Directory contents may be mutated.161///162/// When this flag is unset on a descriptor, operations using the163/// descriptor which would create, rename, delete, modify the data or164/// metadata of filesystem objects, or obtain another handle which165/// would permit any of those, shall fail with `error-code::read-only` if166/// they would otherwise succeed.167///168/// This may only be set on directories.169const MUTATE_DIRECTORY = 0b100000;170}171}172173/// Error codes returned by functions, similar to `errno` in POSIX.174/// Not all of these error codes are returned by the functions provided by this175/// API; some are used in higher-level library layers, and others are provided176/// merely for alignment with POSIX.177#[cfg_attr(178windows,179expect(dead_code, reason = "on Windows, some of these are not used")180)]181pub(crate) enum ErrorCode {182/// Permission denied, similar to `EACCES` in POSIX.183Access,184/// Connection already in progress, similar to `EALREADY` in POSIX.185Already,186/// Bad descriptor, similar to `EBADF` in POSIX.187BadDescriptor,188/// Device or resource busy, similar to `EBUSY` in POSIX.189Busy,190/// File exists, similar to `EEXIST` in POSIX.191Exist,192/// File too large, similar to `EFBIG` in POSIX.193FileTooLarge,194/// Illegal byte sequence, similar to `EILSEQ` in POSIX.195IllegalByteSequence,196/// Operation in progress, similar to `EINPROGRESS` in POSIX.197InProgress,198/// Interrupted function, similar to `EINTR` in POSIX.199Interrupted,200/// Invalid argument, similar to `EINVAL` in POSIX.201Invalid,202/// I/O error, similar to `EIO` in POSIX.203Io,204/// Is a directory, similar to `EISDIR` in POSIX.205IsDirectory,206/// Too many levels of symbolic links, similar to `ELOOP` in POSIX.207Loop,208/// Too many links, similar to `EMLINK` in POSIX.209TooManyLinks,210/// Filename too long, similar to `ENAMETOOLONG` in POSIX.211NameTooLong,212/// No such file or directory, similar to `ENOENT` in POSIX.213NoEntry,214/// Not enough space, similar to `ENOMEM` in POSIX.215InsufficientMemory,216/// No space left on device, similar to `ENOSPC` in POSIX.217InsufficientSpace,218/// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX.219NotDirectory,220/// Directory not empty, similar to `ENOTEMPTY` in POSIX.221NotEmpty,222/// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX.223Unsupported,224/// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX.225Overflow,226/// Operation not permitted, similar to `EPERM` in POSIX.227NotPermitted,228/// Broken pipe, similar to `EPIPE` in POSIX.229Pipe,230/// Invalid seek, similar to `ESPIPE` in POSIX.231InvalidSeek,232}233234fn datetime_from(t: std::time::SystemTime) -> Datetime {235// FIXME make this infallible or handle errors properly236Datetime::try_from(cap_std::time::SystemTime::from_std(t)).unwrap()237}238239/// The type of a filesystem object referenced by a descriptor.240///241/// Note: This was called `filetype` in earlier versions of WASI.242pub(crate) enum DescriptorType {243/// The type of the descriptor or file is unknown or is different from244/// any of the other types specified.245Unknown,246/// The descriptor refers to a block device inode.247BlockDevice,248/// The descriptor refers to a character device inode.249CharacterDevice,250/// The descriptor refers to a directory inode.251Directory,252/// The file refers to a symbolic link inode.253SymbolicLink,254/// The descriptor refers to a regular file inode.255RegularFile,256}257258impl From<cap_std::fs::FileType> for DescriptorType {259fn from(ft: cap_std::fs::FileType) -> Self {260if ft.is_dir() {261DescriptorType::Directory262} else if ft.is_symlink() {263DescriptorType::SymbolicLink264} else if ft.is_block_device() {265DescriptorType::BlockDevice266} else if ft.is_char_device() {267DescriptorType::CharacterDevice268} else if ft.is_file() {269DescriptorType::RegularFile270} else {271DescriptorType::Unknown272}273}274}275276/// File attributes.277///278/// Note: This was called `filestat` in earlier versions of WASI.279pub(crate) struct DescriptorStat {280/// File type.281pub type_: DescriptorType,282/// Number of hard links to the file.283pub link_count: u64,284/// For regular files, the file size in bytes. For symbolic links, the285/// length in bytes of the pathname contained in the symbolic link.286pub size: u64,287/// Last data access timestamp.288///289/// If the `option` is none, the platform doesn't maintain an access290/// timestamp for this file.291pub data_access_timestamp: Option<Datetime>,292/// Last data modification timestamp.293///294/// If the `option` is none, the platform doesn't maintain a295/// modification timestamp for this file.296pub data_modification_timestamp: Option<Datetime>,297/// Last file status-change timestamp.298///299/// If the `option` is none, the platform doesn't maintain a300/// status-change timestamp for this file.301pub status_change_timestamp: Option<Datetime>,302}303304impl From<cap_std::fs::Metadata> for DescriptorStat {305fn from(meta: cap_std::fs::Metadata) -> Self {306Self {307type_: meta.file_type().into(),308link_count: meta.nlink(),309size: meta.len(),310data_access_timestamp: meta.accessed().map(|t| datetime_from(t.into_std())).ok(),311data_modification_timestamp: meta.modified().map(|t| datetime_from(t.into_std())).ok(),312status_change_timestamp: meta.created().map(|t| datetime_from(t.into_std())).ok(),313}314}315}316317/// A 128-bit hash value, split into parts because wasm doesn't have a318/// 128-bit integer type.319pub(crate) struct MetadataHashValue {320/// 64 bits of a 128-bit hash value.321pub lower: u64,322/// Another 64 bits of a 128-bit hash value.323pub upper: u64,324}325326impl From<&cap_std::fs::Metadata> for MetadataHashValue {327fn from(meta: &cap_std::fs::Metadata) -> Self {328use cap_fs_ext::MetadataExt;329// Without incurring any deps, std provides us with a 64 bit hash330// function:331use std::hash::Hasher;332// Note that this means that the metadata hash (which becomes a preview1 ino) may333// change when a different rustc release is used to build this host implementation:334let mut hasher = hash_map::DefaultHasher::new();335hasher.write_u64(meta.dev());336hasher.write_u64(meta.ino());337let lower = hasher.finish();338// MetadataHashValue has a pair of 64-bit members for representing a339// single 128-bit number. However, we only have 64 bits of entropy. To340// synthesize the upper 64 bits, lets xor the lower half with an arbitrary341// constant, in this case the 64 bit integer corresponding to the IEEE342// double representation of (a number as close as possible to) pi.343// This seems better than just repeating the same bits in the upper and344// lower parts outright, which could make folks wonder if the struct was345// mangled in the ABI, or worse yet, lead to consumers of this interface346// expecting them to be equal.347let upper = lower ^ 4614256656552045848u64;348Self { lower, upper }349}350}351352#[cfg(unix)]353fn from_raw_os_error(err: Option<i32>) -> Option<ErrorCode> {354use rustix::io::Errno as RustixErrno;355if err.is_none() {356return None;357}358Some(match RustixErrno::from_raw_os_error(err.unwrap()) {359RustixErrno::PIPE => ErrorCode::Pipe,360RustixErrno::PERM => ErrorCode::NotPermitted,361RustixErrno::NOENT => ErrorCode::NoEntry,362RustixErrno::NOMEM => ErrorCode::InsufficientMemory,363RustixErrno::IO => ErrorCode::Io,364RustixErrno::BADF => ErrorCode::BadDescriptor,365RustixErrno::BUSY => ErrorCode::Busy,366RustixErrno::ACCESS => ErrorCode::Access,367RustixErrno::NOTDIR => ErrorCode::NotDirectory,368RustixErrno::ISDIR => ErrorCode::IsDirectory,369RustixErrno::INVAL => ErrorCode::Invalid,370RustixErrno::EXIST => ErrorCode::Exist,371RustixErrno::FBIG => ErrorCode::FileTooLarge,372RustixErrno::NOSPC => ErrorCode::InsufficientSpace,373RustixErrno::SPIPE => ErrorCode::InvalidSeek,374RustixErrno::MLINK => ErrorCode::TooManyLinks,375RustixErrno::NAMETOOLONG => ErrorCode::NameTooLong,376RustixErrno::NOTEMPTY => ErrorCode::NotEmpty,377RustixErrno::LOOP => ErrorCode::Loop,378RustixErrno::OVERFLOW => ErrorCode::Overflow,379RustixErrno::ILSEQ => ErrorCode::IllegalByteSequence,380RustixErrno::NOTSUP => ErrorCode::Unsupported,381RustixErrno::ALREADY => ErrorCode::Already,382RustixErrno::INPROGRESS => ErrorCode::InProgress,383RustixErrno::INTR => ErrorCode::Interrupted,384385// On some platforms, these have the same value as other errno values.386#[allow(unreachable_patterns, reason = "see comment")]387RustixErrno::OPNOTSUPP => ErrorCode::Unsupported,388389_ => return None,390})391}392393#[cfg(windows)]394fn from_raw_os_error(raw_os_error: Option<i32>) -> Option<ErrorCode> {395use windows_sys::Win32::Foundation;396Some(match raw_os_error.map(|code| code as u32) {397Some(Foundation::ERROR_FILE_NOT_FOUND) => ErrorCode::NoEntry,398Some(Foundation::ERROR_PATH_NOT_FOUND) => ErrorCode::NoEntry,399Some(Foundation::ERROR_ACCESS_DENIED) => ErrorCode::Access,400Some(Foundation::ERROR_SHARING_VIOLATION) => ErrorCode::Access,401Some(Foundation::ERROR_PRIVILEGE_NOT_HELD) => ErrorCode::NotPermitted,402Some(Foundation::ERROR_INVALID_HANDLE) => ErrorCode::BadDescriptor,403Some(Foundation::ERROR_INVALID_NAME) => ErrorCode::NoEntry,404Some(Foundation::ERROR_NOT_ENOUGH_MEMORY) => ErrorCode::InsufficientMemory,405Some(Foundation::ERROR_OUTOFMEMORY) => ErrorCode::InsufficientMemory,406Some(Foundation::ERROR_DIR_NOT_EMPTY) => ErrorCode::NotEmpty,407Some(Foundation::ERROR_NOT_READY) => ErrorCode::Busy,408Some(Foundation::ERROR_BUSY) => ErrorCode::Busy,409Some(Foundation::ERROR_NOT_SUPPORTED) => ErrorCode::Unsupported,410Some(Foundation::ERROR_FILE_EXISTS) => ErrorCode::Exist,411Some(Foundation::ERROR_BROKEN_PIPE) => ErrorCode::Pipe,412Some(Foundation::ERROR_BUFFER_OVERFLOW) => ErrorCode::NameTooLong,413Some(Foundation::ERROR_NOT_A_REPARSE_POINT) => ErrorCode::Invalid,414Some(Foundation::ERROR_NEGATIVE_SEEK) => ErrorCode::Invalid,415Some(Foundation::ERROR_DIRECTORY) => ErrorCode::NotDirectory,416Some(Foundation::ERROR_ALREADY_EXISTS) => ErrorCode::Exist,417Some(Foundation::ERROR_STOPPED_ON_SYMLINK) => ErrorCode::Loop,418Some(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED) => ErrorCode::IsDirectory,419_ => return None,420})421}422423impl<'a> From<&'a std::io::Error> for ErrorCode {424fn from(err: &'a std::io::Error) -> ErrorCode {425match from_raw_os_error(err.raw_os_error()) {426Some(errno) => errno,427None => {428debug!("unknown raw os error: {err}");429match err.kind() {430std::io::ErrorKind::NotFound => ErrorCode::NoEntry,431std::io::ErrorKind::PermissionDenied => ErrorCode::NotPermitted,432std::io::ErrorKind::AlreadyExists => ErrorCode::Exist,433std::io::ErrorKind::InvalidInput => ErrorCode::Invalid,434_ => ErrorCode::Io,435}436}437}438}439}440441impl From<std::io::Error> for ErrorCode {442fn from(err: std::io::Error) -> ErrorCode {443ErrorCode::from(&err)444}445}446447#[derive(Clone)]448pub enum Descriptor {449File(File),450Dir(Dir),451}452453impl Descriptor {454pub(crate) fn file(&self) -> Result<&File, ErrorCode> {455match self {456Descriptor::File(f) => Ok(f),457Descriptor::Dir(_) => Err(ErrorCode::BadDescriptor),458}459}460461pub(crate) fn dir(&self) -> Result<&Dir, ErrorCode> {462match self {463Descriptor::Dir(d) => Ok(d),464Descriptor::File(_) => Err(ErrorCode::NotDirectory),465}466}467468async fn get_metadata(&self) -> std::io::Result<cap_std::fs::Metadata> {469match self {470Self::File(f) => {471// No permissions check on metadata: if opened, allowed to stat it472f.run_blocking(|f| f.metadata()).await473}474Self::Dir(d) => {475// No permissions check on metadata: if opened, allowed to stat it476d.run_blocking(|d| d.dir_metadata()).await477}478}479}480481pub(crate) async fn sync_data(&self) -> Result<(), ErrorCode> {482match self {483Self::File(f) => {484match f.run_blocking(|f| f.sync_data()).await {485Ok(()) => Ok(()),486// On windows, `sync_data` uses `FileFlushBuffers` which fails with487// `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore488// this error, for POSIX compatibility.489#[cfg(windows)]490Err(err)491if err.raw_os_error()492== Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) =>493{494Ok(())495}496Err(err) => Err(err.into()),497}498}499Self::Dir(d) => {500d.run_blocking(|d| {501let d = d.open(std::path::Component::CurDir)?;502d.sync_data()?;503Ok(())504})505.await506}507}508}509510pub(crate) async fn get_flags(&self) -> Result<DescriptorFlags, ErrorCode> {511use system_interface::fs::{FdFlags, GetSetFdFlags};512513fn get_from_fdflags(flags: FdFlags) -> DescriptorFlags {514let mut out = DescriptorFlags::empty();515if flags.contains(FdFlags::DSYNC) {516out |= DescriptorFlags::REQUESTED_WRITE_SYNC;517}518if flags.contains(FdFlags::RSYNC) {519out |= DescriptorFlags::DATA_INTEGRITY_SYNC;520}521if flags.contains(FdFlags::SYNC) {522out |= DescriptorFlags::FILE_INTEGRITY_SYNC;523}524out525}526match self {527Self::File(f) => {528let flags = f.run_blocking(|f| f.get_fd_flags()).await?;529let mut flags = get_from_fdflags(flags);530if f.open_mode.contains(OpenMode::READ) {531flags |= DescriptorFlags::READ;532}533if f.open_mode.contains(OpenMode::WRITE) {534flags |= DescriptorFlags::WRITE;535}536Ok(flags)537}538Self::Dir(d) => {539let flags = d.run_blocking(|d| d.get_fd_flags()).await?;540let mut flags = get_from_fdflags(flags);541if d.open_mode.contains(OpenMode::READ) {542flags |= DescriptorFlags::READ;543}544if d.open_mode.contains(OpenMode::WRITE) {545flags |= DescriptorFlags::MUTATE_DIRECTORY;546}547Ok(flags)548}549}550}551552pub(crate) async fn get_type(&self) -> Result<DescriptorType, ErrorCode> {553match self {554Self::File(f) => {555let meta = f.run_blocking(|f| f.metadata()).await?;556Ok(meta.file_type().into())557}558Self::Dir(_) => Ok(DescriptorType::Directory),559}560}561562pub(crate) async fn set_times(563&self,564atim: Option<SystemTimeSpec>,565mtim: Option<SystemTimeSpec>,566) -> Result<(), ErrorCode> {567use fs_set_times::SetTimes as _;568match self {569Self::File(f) => {570if !f.perms.contains(FilePerms::WRITE) {571return Err(ErrorCode::NotPermitted);572}573f.run_blocking(|f| f.set_times(atim, mtim)).await?;574Ok(())575}576Self::Dir(d) => {577if !d.perms.contains(DirPerms::MUTATE) {578return Err(ErrorCode::NotPermitted);579}580d.run_blocking(|d| d.set_times(atim, mtim)).await?;581Ok(())582}583}584}585586pub(crate) async fn sync(&self) -> Result<(), ErrorCode> {587match self {588Self::File(f) => {589match f.run_blocking(|f| f.sync_all()).await {590Ok(()) => Ok(()),591// On windows, `sync_data` uses `FileFlushBuffers` which fails with592// `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore593// this error, for POSIX compatibility.594#[cfg(windows)]595Err(err)596if err.raw_os_error()597== Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) =>598{599Ok(())600}601Err(err) => Err(err.into()),602}603}604Self::Dir(d) => {605d.run_blocking(|d| {606let d = d.open(std::path::Component::CurDir)?;607d.sync_all()?;608Ok(())609})610.await611}612}613}614615pub(crate) async fn stat(&self) -> Result<DescriptorStat, ErrorCode> {616match self {617Self::File(f) => {618// No permissions check on stat: if opened, allowed to stat it619let meta = f.run_blocking(|f| f.metadata()).await?;620Ok(meta.into())621}622Self::Dir(d) => {623// No permissions check on stat: if opened, allowed to stat it624let meta = d.run_blocking(|d| d.dir_metadata()).await?;625Ok(meta.into())626}627}628}629630pub(crate) async fn is_same_object(&self, other: &Self) -> wasmtime::Result<bool> {631use cap_fs_ext::MetadataExt;632let meta_a = self.get_metadata().await?;633let meta_b = other.get_metadata().await?;634if meta_a.dev() == meta_b.dev() && meta_a.ino() == meta_b.ino() {635// MetadataHashValue does not derive eq, so use a pair of636// comparisons to check equality:637debug_assert_eq!(638MetadataHashValue::from(&meta_a).upper,639MetadataHashValue::from(&meta_b).upper,640);641debug_assert_eq!(642MetadataHashValue::from(&meta_a).lower,643MetadataHashValue::from(&meta_b).lower,644);645Ok(true)646} else {647// Hash collisions are possible, so don't assert the negative here648Ok(false)649}650}651652pub(crate) async fn metadata_hash(&self) -> Result<MetadataHashValue, ErrorCode> {653let meta = self.get_metadata().await?;654Ok(MetadataHashValue::from(&meta))655}656}657658#[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 for663/// implementing the stream types. A copy is also needed for664/// `spawn_blocking`.665pub file: Arc<cap_std::fs::File>,666/// Permissions to enforce on access to the file. These permissions are667/// specified by a user of the `crate::WasiCtxBuilder`, and are668/// enforced prior to any enforced by the underlying operating system.669pub perms: FilePerms,670/// The mode the file was opened under: bits for reading, and writing.671/// Required to correctly report the DescriptorFlags, because cap-std672/// doesn't presently provide a cross-platform equivalent of reading the673/// oflags back out using fcntl.674pub open_mode: OpenMode,675676allow_blocking_current_thread: bool,677}678679impl File {680pub fn new(681file: cap_std::fs::File,682perms: FilePerms,683open_mode: OpenMode,684allow_blocking_current_thread: bool,685) -> Self {686Self {687file: Arc::new(file),688perms,689open_mode,690allow_blocking_current_thread,691}692}693694/// 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 returned699/// 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 is704/// 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)708pub(crate) async fn run_blocking<F, R>(&self, body: F) -> R709where710F: FnOnce(&cap_std::fs::File) -> R + Send + 'static,711R: Send + 'static,712{713match self.as_blocking_file() {714Some(file) => body(file),715None => self.spawn_blocking(body).await,716}717}718719pub(crate) fn spawn_blocking<F, R>(&self, body: F) -> AbortOnDropJoinHandle<R>720where721F: FnOnce(&cap_std::fs::File) -> R + Send + 'static,722R: Send + 'static,723{724let f = self.file.clone();725spawn_blocking(move || body(&f))726}727728/// Returns `Some` when the current thread is allowed to block in filesystem729/// operations, and otherwise returns `None` to indicate that730/// `spawn_blocking` must be used.731pub(crate) fn as_blocking_file(&self) -> Option<&cap_std::fs::File> {732if self.allow_blocking_current_thread {733Some(&self.file)734} else {735None736}737}738739/// Returns reference to the underlying [`cap_std::fs::File`]740#[cfg(feature = "p3")]741pub(crate) fn as_file(&self) -> &Arc<cap_std::fs::File> {742&self.file743}744745pub(crate) async fn advise(746&self,747offset: u64,748len: u64,749advice: system_interface::fs::Advice,750) -> Result<(), ErrorCode> {751use system_interface::fs::FileIoExt as _;752self.run_blocking(move |f| f.advise(offset, len, advice))753.await?;754Ok(())755}756757pub(crate) async fn set_size(&self, size: u64) -> Result<(), ErrorCode> {758if !self.perms.contains(FilePerms::WRITE) {759return Err(ErrorCode::NotPermitted);760}761self.run_blocking(move |f| f.set_len(size)).await?;762Ok(())763}764}765766#[derive(Clone)]767pub struct Dir {768/// The operating system file descriptor this struct is mediating access769/// to.770///771/// Wrapped in an Arc because a copy is needed for `run_blocking`.772pub dir: Arc<cap_std::fs::Dir>,773/// Permissions to enforce on access to this directory. These permissions774/// are specified by a user of the `crate::WasiCtxBuilder`, and775/// are enforced prior to any enforced by the underlying operating system.776///777/// These permissions are also enforced on any directories opened under778/// this directory.779pub perms: DirPerms,780/// Permissions to enforce on any files opened under this directory.781pub file_perms: FilePerms,782/// The mode the directory was opened under: bits for reading, and writing.783/// Required to correctly report the DescriptorFlags, because cap-std784/// doesn't presently provide a cross-platform equivalent of reading the785/// oflags back out using fcntl.786pub open_mode: OpenMode,787788pub(crate) allow_blocking_current_thread: bool,789}790791impl Dir {792pub fn new(793dir: cap_std::fs::Dir,794perms: DirPerms,795file_perms: FilePerms,796open_mode: OpenMode,797allow_blocking_current_thread: bool,798) -> Self {799Dir {800dir: Arc::new(dir),801perms,802file_perms,803open_mode,804allow_blocking_current_thread,805}806}807808/// Execute the blocking `body` function.809///810/// Depending on how the WasiCtx was configured, the body may either be:811/// - Executed directly on the current thread. In this case the `async`812/// signature of this method is effectively a lie and the returned813/// Future will always be immediately Ready. Or:814/// - Spawned on a background thread using [`tokio::task::spawn_blocking`]815/// and immediately awaited.816///817/// Intentionally blocking the executor thread might seem unorthodox, but is818/// not actually a problem for specific workloads. See:819/// - [`crate::WasiCtxBuilder::allow_blocking_current_thread`]820/// - [Poor performance of wasmtime file I/O maybe because tokio](https://github.com/bytecodealliance/wasmtime/issues/7973)821/// - [Implement opt-in for enabling WASI to block the current thread](https://github.com/bytecodealliance/wasmtime/pull/8190)822pub(crate) async fn run_blocking<F, R>(&self, body: F) -> R823where824F: FnOnce(&cap_std::fs::Dir) -> R + Send + 'static,825R: Send + 'static,826{827if self.allow_blocking_current_thread {828body(&self.dir)829} else {830let d = self.dir.clone();831spawn_blocking(move || body(&d)).await832}833}834835/// Returns reference to the underlying [`cap_std::fs::Dir`]836#[cfg(feature = "p3")]837pub(crate) fn as_dir(&self) -> &Arc<cap_std::fs::Dir> {838&self.dir839}840841pub(crate) async fn create_directory_at(&self, path: String) -> Result<(), ErrorCode> {842if !self.perms.contains(DirPerms::MUTATE) {843return Err(ErrorCode::NotPermitted);844}845self.run_blocking(move |d| d.create_dir(&path)).await?;846Ok(())847}848849pub(crate) async fn stat_at(850&self,851path_flags: PathFlags,852path: String,853) -> Result<DescriptorStat, ErrorCode> {854if !self.perms.contains(DirPerms::READ) {855return Err(ErrorCode::NotPermitted);856}857858let meta = if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {859self.run_blocking(move |d| d.metadata(&path)).await?860} else {861self.run_blocking(move |d| d.symlink_metadata(&path))862.await?863};864Ok(meta.into())865}866867pub(crate) async fn set_times_at(868&self,869path_flags: PathFlags,870path: String,871atim: Option<SystemTimeSpec>,872mtim: Option<SystemTimeSpec>,873) -> Result<(), ErrorCode> {874use cap_fs_ext::DirExt as _;875876if !self.perms.contains(DirPerms::MUTATE) {877return Err(ErrorCode::NotPermitted);878}879if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {880self.run_blocking(move |d| {881d.set_times(882&path,883atim.map(cap_fs_ext::SystemTimeSpec::from_std),884mtim.map(cap_fs_ext::SystemTimeSpec::from_std),885)886})887.await?;888} else {889self.run_blocking(move |d| {890d.set_symlink_times(891&path,892atim.map(cap_fs_ext::SystemTimeSpec::from_std),893mtim.map(cap_fs_ext::SystemTimeSpec::from_std),894)895})896.await?;897}898Ok(())899}900901pub(crate) async fn link_at(902&self,903old_path_flags: PathFlags,904old_path: String,905new_dir: &Self,906new_path: String,907) -> Result<(), ErrorCode> {908if !self.perms.contains(DirPerms::MUTATE) {909return Err(ErrorCode::NotPermitted);910}911if !new_dir.perms.contains(DirPerms::MUTATE) {912return Err(ErrorCode::NotPermitted);913}914if old_path_flags.contains(PathFlags::SYMLINK_FOLLOW) {915return Err(ErrorCode::Invalid);916}917let new_dir_handle = Arc::clone(&new_dir.dir);918self.run_blocking(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path))919.await?;920Ok(())921}922923pub(crate) async fn open_at(924&self,925path_flags: PathFlags,926path: String,927oflags: OpenFlags,928flags: DescriptorFlags,929allow_blocking_current_thread: bool,930) -> Result<Descriptor, ErrorCode> {931use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt};932use system_interface::fs::{FdFlags, GetSetFdFlags};933934if !self.perms.contains(DirPerms::READ) {935return Err(ErrorCode::NotPermitted);936}937938if !self.perms.contains(DirPerms::MUTATE) {939if oflags.contains(OpenFlags::CREATE) || oflags.contains(OpenFlags::TRUNCATE) {940return Err(ErrorCode::NotPermitted);941}942if flags.contains(DescriptorFlags::WRITE) {943return Err(ErrorCode::NotPermitted);944}945}946947// Track whether we are creating file, for permission check:948let mut create = false;949// Track open mode, for permission check and recording in created descriptor:950let mut open_mode = OpenMode::empty();951// Construct the OpenOptions to give the OS:952let mut opts = cap_std::fs::OpenOptions::new();953opts.maybe_dir(true);954955if oflags.contains(OpenFlags::CREATE) {956if oflags.contains(OpenFlags::EXCLUSIVE) {957opts.create_new(true);958} else {959opts.create(true);960}961create = true;962opts.write(true);963open_mode |= OpenMode::WRITE;964}965966if oflags.contains(OpenFlags::TRUNCATE) {967opts.truncate(true).write(true);968}969if flags.contains(DescriptorFlags::READ) {970opts.read(true);971open_mode |= OpenMode::READ;972}973if flags.contains(DescriptorFlags::WRITE) {974opts.write(true);975open_mode |= OpenMode::WRITE;976} else {977// If not opened write, open read. This way the OS lets us open978// the file, but we can use perms to reject use of the file later.979opts.read(true);980open_mode |= OpenMode::READ;981}982if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {983opts.follow(FollowSymlinks::Yes);984} else {985opts.follow(FollowSymlinks::No);986}987988// These flags are not yet supported in cap-std:989if flags.contains(DescriptorFlags::FILE_INTEGRITY_SYNC)990|| flags.contains(DescriptorFlags::DATA_INTEGRITY_SYNC)991|| flags.contains(DescriptorFlags::REQUESTED_WRITE_SYNC)992{993return Err(ErrorCode::Unsupported);994}995996if oflags.contains(OpenFlags::DIRECTORY) {997if oflags.contains(OpenFlags::CREATE)998|| oflags.contains(OpenFlags::EXCLUSIVE)999|| oflags.contains(OpenFlags::TRUNCATE)1000{1001return Err(ErrorCode::Invalid);1002}1003}10041005// Now enforce this WasiCtx's permissions before letting the OS have1006// its shot:1007if !self.perms.contains(DirPerms::MUTATE) && create {1008return Err(ErrorCode::NotPermitted);1009}1010if !self.file_perms.contains(FilePerms::WRITE) && open_mode.contains(OpenMode::WRITE) {1011return Err(ErrorCode::NotPermitted);1012}10131014// Represents each possible outcome from the spawn_blocking operation.1015// This makes sure we don't have to give spawn_blocking any way to1016// manipulate the table.1017enum OpenResult {1018Dir(cap_std::fs::Dir),1019File(cap_std::fs::File),1020NotDir,1021}10221023let opened = self1024.run_blocking::<_, std::io::Result<OpenResult>>(move |d| {1025let mut opened = d.open_with(&path, &opts)?;1026if opened.metadata()?.is_dir() {1027Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file(1028opened.into_std(),1029)))1030} else if oflags.contains(OpenFlags::DIRECTORY) {1031Ok(OpenResult::NotDir)1032} else {1033// FIXME cap-std needs a nonblocking open option so that files reads and writes1034// are nonblocking. Instead we set it after opening here:1035let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?;1036opened.set_fd_flags(set_fd_flags)?;1037Ok(OpenResult::File(opened))1038}1039})1040.await?;10411042match opened {1043// Paper over a divergence between Windows and POSIX, where1044// POSIX returns EISDIR if you open a directory with the1045// WRITE flag: https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html#:~:text=EISDIR1046#[cfg(windows)]1047OpenResult::Dir(_) if flags.contains(DescriptorFlags::WRITE) => {1048Err(ErrorCode::IsDirectory)1049}10501051OpenResult::Dir(dir) => Ok(Descriptor::Dir(Dir::new(1052dir,1053self.perms,1054self.file_perms,1055open_mode,1056allow_blocking_current_thread,1057))),10581059OpenResult::File(file) => Ok(Descriptor::File(File::new(1060file,1061self.file_perms,1062open_mode,1063allow_blocking_current_thread,1064))),10651066OpenResult::NotDir => Err(ErrorCode::NotDirectory),1067}1068}10691070pub(crate) async fn readlink_at(&self, path: String) -> Result<String, ErrorCode> {1071if !self.perms.contains(DirPerms::READ) {1072return Err(ErrorCode::NotPermitted);1073}1074let link = self.run_blocking(move |d| d.read_link(&path)).await?;1075link.into_os_string()1076.into_string()1077.or(Err(ErrorCode::IllegalByteSequence))1078}10791080pub(crate) async fn remove_directory_at(&self, path: String) -> Result<(), ErrorCode> {1081if !self.perms.contains(DirPerms::MUTATE) {1082return Err(ErrorCode::NotPermitted);1083}1084self.run_blocking(move |d| d.remove_dir(&path)).await?;1085Ok(())1086}10871088pub(crate) async fn rename_at(1089&self,1090old_path: String,1091new_dir: &Self,1092new_path: String,1093) -> Result<(), ErrorCode> {1094if !self.perms.contains(DirPerms::MUTATE) {1095return Err(ErrorCode::NotPermitted);1096}1097if !new_dir.perms.contains(DirPerms::MUTATE) {1098return Err(ErrorCode::NotPermitted);1099}1100let new_dir_handle = Arc::clone(&new_dir.dir);1101self.run_blocking(move |d| d.rename(&old_path, &new_dir_handle, &new_path))1102.await?;1103Ok(())1104}11051106pub(crate) async fn symlink_at(1107&self,1108src_path: String,1109dest_path: String,1110) -> Result<(), ErrorCode> {1111// On windows, Dir.symlink is provided by DirExt1112#[cfg(windows)]1113use cap_fs_ext::DirExt;11141115if !self.perms.contains(DirPerms::MUTATE) {1116return Err(ErrorCode::NotPermitted);1117}1118self.run_blocking(move |d| d.symlink(&src_path, &dest_path))1119.await?;1120Ok(())1121}11221123pub(crate) async fn unlink_file_at(&self, path: String) -> Result<(), ErrorCode> {1124use cap_fs_ext::DirExt;11251126if !self.perms.contains(DirPerms::MUTATE) {1127return Err(ErrorCode::NotPermitted);1128}1129self.run_blocking(move |d| d.remove_file_or_symlink(&path))1130.await?;1131Ok(())1132}11331134pub(crate) async fn metadata_hash_at(1135&self,1136path_flags: PathFlags,1137path: String,1138) -> Result<MetadataHashValue, ErrorCode> {1139// No permissions check on metadata: if dir opened, allowed to stat it1140let meta = self1141.run_blocking(move |d| {1142if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {1143d.metadata(path)1144} else {1145d.symlink_metadata(path)1146}1147})1148.await?;1149Ok(MetadataHashValue::from(&meta))1150}1151}11521153impl WasiFilesystemCtxView<'_> {1154pub(crate) fn get_directories(1155&mut self,1156) -> wasmtime::Result<Vec<(Resource<Descriptor>, String)>> {1157let preopens = self.ctx.preopens.clone();1158let mut results = Vec::with_capacity(preopens.len());1159for (dir, name) in preopens {1160let fd = self1161.table1162.push(Descriptor::Dir(dir))1163.with_context(|| format!("failed to push preopen {name}"))?;1164results.push((fd, name));1165}1166Ok(results)1167}1168}116911701171