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