kobako 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +377 -0
- data/Cargo.lock +181 -154
- data/README.md +99 -459
- data/ROADMAP.md +7 -1
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +354 -0
- data/crates/kobako-runtime/Cargo.toml +38 -7
- data/crates/kobako-runtime/README.md +17 -8
- data/crates/kobako-runtime/src/dispatch.rs +9 -1
- data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
- data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
- data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
- data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
- data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
- data/crates/kobako-runtime/src/endpoint.rs +166 -0
- data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
- data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
- data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
- data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
- data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
- data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
- data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
- data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
- data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
- data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
- data/crates/kobako-runtime/src/filesystem.rs +26 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +16 -1
- data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
- data/crates/kobako-runtime/src/locking.rs +218 -0
- data/crates/kobako-runtime/src/mount.rs +105 -0
- data/crates/kobako-runtime/src/profile.rs +140 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
- data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
- data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
- data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
- data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
- data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
- data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
- data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
- data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
- data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
- data/crates/kobako-runtime/src/vfs/path.rs +64 -0
- data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
- data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
- data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
- data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
- data/crates/kobako-runtime/src/vfs.rs +60 -0
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
- data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
- data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
- data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
- data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
- data/crates/kobako-transport/CHANGELOG.md +23 -0
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
- data/crates/kobako-transport/src/envelope/call.rs +5 -5
- data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
- data/crates/kobako-transport/src/envelope/fault.rs +4 -3
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
- data/crates/kobako-transport/src/envelope/reply.rs +34 -16
- data/crates/kobako-transport/src/envelope/request.rs +19 -9
- data/crates/kobako-transport/src/envelope/run.rs +1 -1
- data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
- data/crates/kobako-transport/src/envelope.rs +2 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
- data/crates/kobako-wasmtime/Cargo.toml +25 -19
- data/crates/kobako-wasmtime/README.md +13 -7
- data/crates/kobako-wasmtime/src/abi.rs +75 -49
- data/crates/kobako-wasmtime/src/ambient.rs +64 -6
- data/crates/kobako-wasmtime/src/cache.rs +188 -77
- data/crates/kobako-wasmtime/src/config.rs +77 -5
- data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
- data/crates/kobako-wasmtime/src/driver.rs +70 -135
- data/crates/kobako-wasmtime/src/exports.rs +5 -16
- data/crates/kobako-wasmtime/src/guest.rs +38 -0
- data/crates/kobako-wasmtime/src/imports.rs +52 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
- data/crates/kobako-wasmtime/src/invocation.rs +43 -74
- data/crates/kobako-wasmtime/src/lib.rs +5 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
- data/crates/kobako-wasmtime/src/preview1.rs +512 -0
- data/crates/kobako-wasmtime/src/trap.rs +45 -25
- data/crates/kobako-wasmtime/src/wasi.rs +333 -0
- data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
- data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
- data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
- data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +67 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
- data/ext/kobako/src/runtime/filesystem.rs +208 -0
- data/ext/kobako/src/runtime/gvl.rs +49 -8
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +125 -199
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +19 -0
- data/lib/kobako/catalog/services.rb +13 -2
- data/lib/kobako/catalog/snippets.rb +11 -15
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +10 -9
- data/lib/kobako/codec/ext_types.rb +4 -6
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +40 -13
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +69 -0
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +6 -6
- data/lib/kobako/sandbox.rb +55 -27
- data/lib/kobako/sandbox_options.rb +39 -13
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +33 -29
- data/lib/kobako/transport/exposure.rb +7 -9
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +9 -31
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +35 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +24 -0
- data/sig/kobako/catalog/services.rbs +5 -1
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/decoder.rbs +1 -1
- data/sig/kobako/codec/handle_walk.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +2 -2
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +3 -2
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +93 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +3 -3
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +25 -3
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +4 -4
- data/sig/kobako/transport/exposure.rbs +2 -2
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +3 -3
- metadata +116 -15
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
- data/crates/kobako-wasmtime/src/io.rs +0 -286
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -0,0 +1,3766 @@
|
|
|
1
|
+
//! Witness what a guest sees through a Mount: the preopen at the Mount's
|
|
2
|
+
//! path, the files a Filesystem holds, and the refusals the runtime
|
|
3
|
+
//! answers before the Filesystem is asked.
|
|
4
|
+
|
|
5
|
+
mod wrap;
|
|
6
|
+
|
|
7
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
8
|
+
use std::sync::{Arc, Mutex, Weak};
|
|
9
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
10
|
+
|
|
11
|
+
use kobako_runtime::filesystem::Bind;
|
|
12
|
+
use kobako_runtime::filesystem::Filesystem;
|
|
13
|
+
use kobako_runtime::filesystem::{
|
|
14
|
+
FileOperations, FileSystemType, FileType, FilesystemError, InodeOperations, Kiocb, Kstat,
|
|
15
|
+
Lookup, OpenHow, Overlay, RenameFlag, Tmpfs,
|
|
16
|
+
};
|
|
17
|
+
use kobako_runtime::grant::Grant;
|
|
18
|
+
use kobako_runtime::mount::{Access, Mount, Policy};
|
|
19
|
+
use kobako_runtime::profile::Profile;
|
|
20
|
+
use kobako_runtime::security::prepare_creds_within;
|
|
21
|
+
use kobako_runtime::vfs::{self, Dentry, Inode, Path, SecurityOperations, SecurityPath, PATH_MAX};
|
|
22
|
+
use wasmtime::{Instance, Module, Store};
|
|
23
|
+
|
|
24
|
+
use crate::cache::load_engine;
|
|
25
|
+
use crate::config::Config;
|
|
26
|
+
use crate::imports;
|
|
27
|
+
use crate::invocation::Invocation;
|
|
28
|
+
use crate::trap::NO_TIMEOUT_EPOCH_DELTA;
|
|
29
|
+
use crate::wasi::Wasi;
|
|
30
|
+
use wrap::{Hook, Wrap};
|
|
31
|
+
|
|
32
|
+
/// The root of `filesystem` as the host reaches it through the VFS, a
|
|
33
|
+
/// writer of its own beside the guest.
|
|
34
|
+
fn host<F: FileSystemType>(filesystem: &Arc<F>) -> Path {
|
|
35
|
+
let mount = Mount::new(
|
|
36
|
+
"/data",
|
|
37
|
+
Filesystem::new(filesystem.clone()).expect("the instance must stand"),
|
|
38
|
+
Access::ReadWrite,
|
|
39
|
+
)
|
|
40
|
+
.expect("/data is a guest path");
|
|
41
|
+
Path::root(mount.mnt())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// The names `filesystem`'s root holds, as the host lists them.
|
|
45
|
+
fn host_entries<F: FileSystemType>(filesystem: &Arc<F>) -> Result<Vec<String>, FilesystemError> {
|
|
46
|
+
let mount = Mount::new(
|
|
47
|
+
"/data",
|
|
48
|
+
Filesystem::new(filesystem.clone()).expect("the instance must stand"),
|
|
49
|
+
Access::ReadWrite,
|
|
50
|
+
)
|
|
51
|
+
.expect("/data is a guest path");
|
|
52
|
+
let mut root = vfs::File::root(mount.mnt(), &prepare_creds_within(&[], None))?;
|
|
53
|
+
let mut names = Vec::new();
|
|
54
|
+
root.llseek(2, vfs::Whence::Set)?;
|
|
55
|
+
root.iterate_dir(&mut vfs::DirContext::new(0, &mut |emit| {
|
|
56
|
+
names.push(emit.name.to_string());
|
|
57
|
+
true
|
|
58
|
+
}))?;
|
|
59
|
+
Ok(names)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ERRNO_ACCES: i32 = 2;
|
|
63
|
+
const ERRNO_BADF: i32 = 8;
|
|
64
|
+
const ERRNO_BUSY: i32 = 10;
|
|
65
|
+
const ERRNO_MFILE: i32 = 33;
|
|
66
|
+
const ERRNO_INVAL: i32 = 28;
|
|
67
|
+
const ERRNO_IO: i32 = 29;
|
|
68
|
+
const ERRNO_ISDIR: i32 = 31;
|
|
69
|
+
#[cfg(unix)]
|
|
70
|
+
const ERRNO_LOOP: i32 = 32;
|
|
71
|
+
const ERRNO_NOENT: i32 = 44;
|
|
72
|
+
const ERRNO_NOMEM: i32 = 48;
|
|
73
|
+
const ERRNO_NOTDIR: i32 = 54;
|
|
74
|
+
const ERRNO_NOTEMPTY: i32 = 55;
|
|
75
|
+
const ERRNO_NOSPC: i32 = 51;
|
|
76
|
+
const ERRNO_NOTSUP: i32 = 58;
|
|
77
|
+
const ERRNO_ROFS: i32 = 69;
|
|
78
|
+
const ERRNO_XDEV: i32 = 75;
|
|
79
|
+
const ERRNO_NOTCAPABLE: i32 = 76;
|
|
80
|
+
|
|
81
|
+
/// Tmpfs holds an `iovec` at 64 over 512..576, a `ciovec` at 80 over
|
|
82
|
+
/// "hello" at 600, the paths `file.txt` at 700 and `../file.txt` at 720,
|
|
83
|
+
/// and from 740 the paths the resolution rules turn on. An opened
|
|
84
|
+
/// descriptor lands at 128, a count at 132, a listing's count at 136, a
|
|
85
|
+
/// stat at 384, and a listing at 2048.
|
|
86
|
+
const PROBE_WAT: &str = r#"
|
|
87
|
+
(module
|
|
88
|
+
(import "wasi_snapshot_preview1" "fd_prestat_get" (func $prestat (param i32 i32) (result i32)))
|
|
89
|
+
(import "wasi_snapshot_preview1" "fd_prestat_dir_name" (func $prestat_name (param i32 i32 i32) (result i32)))
|
|
90
|
+
(import "wasi_snapshot_preview1" "path_open" (func $open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
|
|
91
|
+
(import "wasi_snapshot_preview1" "path_filestat_get" (func $stat (param i32 i32 i32 i32 i32) (result i32)))
|
|
92
|
+
(import "wasi_snapshot_preview1" "fd_filestat_get" (func $fstat (param i32 i32) (result i32)))
|
|
93
|
+
(import "wasi_snapshot_preview1" "fd_read" (func $read (param i32 i32 i32 i32) (result i32)))
|
|
94
|
+
(import "wasi_snapshot_preview1" "fd_write" (func $write (param i32 i32 i32 i32) (result i32)))
|
|
95
|
+
(import "wasi_snapshot_preview1" "path_remove_directory" (func $rmdir (param i32 i32 i32) (result i32)))
|
|
96
|
+
(import "wasi_snapshot_preview1" "path_rename" (func $rename (param i32 i32 i32 i32 i32 i32) (result i32)))
|
|
97
|
+
(import "wasi_snapshot_preview1" "path_unlink_file" (func $unlink (param i32 i32 i32) (result i32)))
|
|
98
|
+
(import "wasi_snapshot_preview1" "fd_allocate" (func $allocate (param i32 i64 i64) (result i32)))
|
|
99
|
+
(import "wasi_snapshot_preview1" "fd_seek" (func $seek (param i32 i64 i32 i32) (result i32)))
|
|
100
|
+
(import "wasi_snapshot_preview1" "fd_readdir" (func $readdir (param i32 i32 i32 i64 i32) (result i32)))
|
|
101
|
+
(import "wasi_snapshot_preview1" "path_create_directory" (func $mkdir (param i32 i32 i32) (result i32)))
|
|
102
|
+
(import "wasi_snapshot_preview1" "fd_pread" (func $pread (param i32 i32 i32 i64 i32) (result i32)))
|
|
103
|
+
(import "wasi_snapshot_preview1" "fd_pwrite" (func $pwrite (param i32 i32 i32 i64 i32) (result i32)))
|
|
104
|
+
(import "wasi_snapshot_preview1" "fd_filestat_set_size" (func $set_size (param i32 i64) (result i32)))
|
|
105
|
+
(import "wasi_snapshot_preview1" "fd_advise" (func $advise (param i32 i64 i64 i32) (result i32)))
|
|
106
|
+
(import "wasi_snapshot_preview1" "fd_fdstat_get" (func $fdstat (param i32 i32) (result i32)))
|
|
107
|
+
(import "wasi_snapshot_preview1" "fd_fdstat_set_flags" (func $set_flags (param i32 i32) (result i32)))
|
|
108
|
+
(import "wasi_snapshot_preview1" "poll_oneoff" (func $poll (param i32 i32 i32 i32) (result i32)))
|
|
109
|
+
(import "wasi_snapshot_preview1" "fd_sync" (func $sync (param i32) (result i32)))
|
|
110
|
+
(import "wasi_snapshot_preview1" "fd_filestat_set_times" (func $set_times (param i32 i64 i64 i32) (result i32)))
|
|
111
|
+
(import "wasi_snapshot_preview1" "sock_shutdown" (func $shutdown (param i32 i32) (result i32)))
|
|
112
|
+
(memory (export "memory") 1)
|
|
113
|
+
(data (i32.const 740) ".")
|
|
114
|
+
(data (i32.const 760) "missing/../file.txt")
|
|
115
|
+
(data (i32.const 780) "file.txt/..")
|
|
116
|
+
(data (i32.const 800) "file.txt/")
|
|
117
|
+
(data (i32.const 820) "new.txt/")
|
|
118
|
+
(data (i32.const 840) "new.txt")
|
|
119
|
+
(data (i32.const 860) "file.txt/key")
|
|
120
|
+
(data (i32.const 880) "dir/.")
|
|
121
|
+
(data (i32.const 890) "dir/..")
|
|
122
|
+
(data (i32.const 900) "file.txt/x")
|
|
123
|
+
(data (i32.const 920) "moved")
|
|
124
|
+
(data (i32.const 930) "dir")
|
|
125
|
+
(data (i32.const 940) "missing/.")
|
|
126
|
+
(data (i32.const 960) "inner")
|
|
127
|
+
(data (i32.const 970) "file.txt/.")
|
|
128
|
+
(data (i32.const 990) "file.txt/../file.txt")
|
|
129
|
+
(data (i32.const 1010) "file.txt\00")
|
|
130
|
+
(data (i32.const 1030) "dir/file.txt")
|
|
131
|
+
(data (i32.const 1050) "dir/")
|
|
132
|
+
(data (i32.const 1060) "gone/")
|
|
133
|
+
(data (i32.const 1070) "up")
|
|
134
|
+
(data (i32.const 1080) "updir/x")
|
|
135
|
+
(data (i32.const 1090) "in/y")
|
|
136
|
+
(data (i32.const 1100) "twisty")
|
|
137
|
+
(data (i32.const 1110) "dangle")
|
|
138
|
+
(data (i32.const 1120) "dir/up")
|
|
139
|
+
(data (i32.const 1130) "nowhere")
|
|
140
|
+
(data (i32.const 1150) "pub/x")
|
|
141
|
+
(data (i32.const 1160) "in/inner/../../sub/y")
|
|
142
|
+
(data (i32.const 1184) "loop")
|
|
143
|
+
(data (i32.const 1190) "dir/sub")
|
|
144
|
+
(data (i32.const 1400) "dir/in/y")
|
|
145
|
+
(data (i32.const 1420) "dir/sub/y")
|
|
146
|
+
(data (i32.const 1440) "\ff")
|
|
147
|
+
;; An iovec naming 16 bytes past the one page of memory.
|
|
148
|
+
(data (i32.const 1140) "\00\00\02\00\10\00\00\00")
|
|
149
|
+
(data (i32.const 64) "\00\02\00\00\40\00\00\00")
|
|
150
|
+
(data (i32.const 80) "\58\02\00\00\05\00\00\00")
|
|
151
|
+
(data (i32.const 600) "hello")
|
|
152
|
+
(data (i32.const 700) "file.txt")
|
|
153
|
+
(data (i32.const 720) "../file.txt")
|
|
154
|
+
|
|
155
|
+
(func (export "preopen_name") (result i32)
|
|
156
|
+
(local $errno i32)
|
|
157
|
+
(local.set $errno (call $prestat (i32.const 3) (i32.const 256)))
|
|
158
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
159
|
+
(call $prestat_name (i32.const 3) (i32.const 512) (i32.load (i32.const 260))))
|
|
160
|
+
|
|
161
|
+
;; Open the path read-only and read it into 512.
|
|
162
|
+
(func (export "read_file") (param $path i32) (param $len i32) (result i32)
|
|
163
|
+
(local $errno i32)
|
|
164
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
165
|
+
(i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
166
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
167
|
+
(call $read (i32.load (i32.const 128)) (i32.const 64) (i32.const 1) (i32.const 132)))
|
|
168
|
+
|
|
169
|
+
;; Open the path asking no right, with the given oflags, and read from it.
|
|
170
|
+
(func (export "read_unasked") (param $path i32) (param $len i32) (param $oflags i32) (result i32)
|
|
171
|
+
(local $errno i32)
|
|
172
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
173
|
+
(local.get $oflags) (i64.const 0) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
174
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
175
|
+
(call $read (i32.load (i32.const 128)) (i32.const 64) (i32.const 1) (i32.const 132)))
|
|
176
|
+
|
|
177
|
+
;; Open the path with read rights only, asking to truncate it.
|
|
178
|
+
(func (export "truncate_file") (param $path i32) (param $len i32) (result i32)
|
|
179
|
+
(call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
180
|
+
(i32.const 8) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
181
|
+
|
|
182
|
+
;; Create or truncate the path and write "hello" into it.
|
|
183
|
+
(func (export "write_file") (param $path i32) (param $len i32) (result i32)
|
|
184
|
+
(local $errno i32)
|
|
185
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
186
|
+
(i32.const 9) (i64.const 64) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
187
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
188
|
+
(call $write (i32.load (i32.const 128)) (i32.const 80) (i32.const 1) (i32.const 132)))
|
|
189
|
+
|
|
190
|
+
;; Open the path read-only with the given oflags and fdflags.
|
|
191
|
+
(func (export "open_flagged") (param $path i32) (param $len i32) (param $oflags i32) (param $fdflags i32) (result i32)
|
|
192
|
+
(call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
193
|
+
(local.get $oflags) (i64.const 2) (i64.const 0) (local.get $fdflags) (i32.const 128)))
|
|
194
|
+
|
|
195
|
+
;; Stat the path with the given lookupflags into 384.
|
|
196
|
+
(func (export "stat_flagged") (param $path i32) (param $len i32) (param $flags i32) (result i32)
|
|
197
|
+
(call $stat (i32.const 3) (local.get $flags) (local.get $path) (local.get $len) (i32.const 384)))
|
|
198
|
+
|
|
199
|
+
;; Open the path read-only and stat what is open into 384.
|
|
200
|
+
(func (export "stat_opened") (param $path i32) (param $len i32) (result i32)
|
|
201
|
+
(local $errno i32)
|
|
202
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
203
|
+
(i32.const 0) (i64.const 2097154) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
204
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
205
|
+
(call $fstat (i32.load (i32.const 128)) (i32.const 384)))
|
|
206
|
+
|
|
207
|
+
;; Change the root `.` at 740.
|
|
208
|
+
(func (export "remove_root") (result i32)
|
|
209
|
+
(call $rmdir (i32.const 3) (i32.const 740) (i32.const 1)))
|
|
210
|
+
(func (export "rename_root") (result i32)
|
|
211
|
+
(call $rename (i32.const 3) (i32.const 740) (i32.const 1) (i32.const 3) (i32.const 700) (i32.const 8)))
|
|
212
|
+
(func (export "rename_onto_root") (result i32)
|
|
213
|
+
(call $rename (i32.const 3) (i32.const 700) (i32.const 8) (i32.const 3) (i32.const 740) (i32.const 1)))
|
|
214
|
+
;; Open the path to write and append, and write "hello" into it.
|
|
215
|
+
(func (export "append_file") (param $path i32) (param $len i32) (result i32)
|
|
216
|
+
(local $errno i32)
|
|
217
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
218
|
+
(i32.const 0) (i64.const 64) (i64.const 0) (i32.const 1) (i32.const 128)))
|
|
219
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
220
|
+
(call $write (i32.load (i32.const 128)) (i32.const 80) (i32.const 1) (i32.const 132)))
|
|
221
|
+
|
|
222
|
+
;; Open the path to write and allocate 100 bytes from its start.
|
|
223
|
+
(func (export "allocate_file") (param $path i32) (param $len i32) (result i32)
|
|
224
|
+
(local $errno i32)
|
|
225
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
226
|
+
(i32.const 0) (i64.const 64) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
227
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
228
|
+
(call $allocate (i32.load (i32.const 128)) (i64.const 0) (i64.const 100)))
|
|
229
|
+
|
|
230
|
+
;; Rename `file.txt` onto `file.txt/`, its own path as a directory.
|
|
231
|
+
(func (export "rename_onto_itself_as_directory") (result i32)
|
|
232
|
+
(call $rename (i32.const 3) (i32.const 700) (i32.const 8) (i32.const 3) (i32.const 800) (i32.const 9)))
|
|
233
|
+
(func (export "unlink_root") (result i32)
|
|
234
|
+
(call $unlink (i32.const 3) (i32.const 740) (i32.const 1)))
|
|
235
|
+
(func (export "unlink_path") (param $path i32) (param $len i32) (result i32)
|
|
236
|
+
(call $unlink (i32.const 3) (local.get $path) (local.get $len)))
|
|
237
|
+
(func (export "remove_path") (param $path i32) (param $len i32) (result i32)
|
|
238
|
+
(call $rmdir (i32.const 3) (local.get $path) (local.get $len)))
|
|
239
|
+
(func (export "make_dir") (param $path i32) (param $len i32) (result i32)
|
|
240
|
+
(call $mkdir (i32.const 3) (local.get $path) (local.get $len)))
|
|
241
|
+
(func (export "rename_paths") (param $from i32) (param $from_len i32) (param $to i32) (param $to_len i32) (result i32)
|
|
242
|
+
(call $rename (i32.const 3) (local.get $from) (local.get $from_len) (i32.const 3) (local.get $to) (local.get $to_len)))
|
|
243
|
+
;; Open `file.txt`, seek it to the largest offset, then that far again.
|
|
244
|
+
(func (export "seek_past_the_largest_offset") (result i32)
|
|
245
|
+
(local $errno i32)
|
|
246
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
|
|
247
|
+
(i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
248
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
249
|
+
(local.set $errno (call $seek (i32.load (i32.const 128)) (i64.const 0x7fffffffffffffff) (i32.const 0) (i32.const 136)))
|
|
250
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
251
|
+
(call $seek (i32.load (i32.const 128)) (i64.const 0x7fffffffffffffff) (i32.const 1) (i32.const 136)))
|
|
252
|
+
;; Open `file.txt` to read and write into 128.
|
|
253
|
+
(func $open_read_write (result i32)
|
|
254
|
+
(call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
|
|
255
|
+
(i32.const 0) (i64.const 66) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
256
|
+
;; Read `file.txt` into 512 at `offset`.
|
|
257
|
+
(func (export "read_at") (param $offset i64) (result i32)
|
|
258
|
+
(local $errno i32)
|
|
259
|
+
(local.set $errno (call $open_read_write))
|
|
260
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
261
|
+
(call $pread (i32.load (i32.const 128)) (i32.const 64) (i32.const 1) (local.get $offset) (i32.const 132)))
|
|
262
|
+
;; Write "hello" into `file.txt` at `offset`.
|
|
263
|
+
(func (export "write_at") (param $offset i64) (result i32)
|
|
264
|
+
(local $errno i32)
|
|
265
|
+
(local.set $errno (call $open_read_write))
|
|
266
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
267
|
+
(call $pwrite (i32.load (i32.const 128)) (i32.const 80) (i32.const 1) (local.get $offset) (i32.const 132)))
|
|
268
|
+
;; Open `file.txt`, seek it to ten before the largest offset, and read
|
|
269
|
+
;; the 64 bytes the iovec at 64 holds.
|
|
270
|
+
(func (export "read_near_the_largest_offset") (result i32)
|
|
271
|
+
(local $errno i32)
|
|
272
|
+
(local.set $errno (call $open_read_write))
|
|
273
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
274
|
+
(local.set $errno (call $seek (i32.load (i32.const 128)) (i64.const 0x7ffffffffffffff5) (i32.const 0) (i32.const 136)))
|
|
275
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
276
|
+
(call $read (i32.load (i32.const 128)) (i32.const 64) (i32.const 1) (i32.const 132)))
|
|
277
|
+
;; Open `file.txt` with `rights`, then wait to write to it with the one
|
|
278
|
+
;; subscription at 1200, its event landing at 1264.
|
|
279
|
+
(func (export "poll_write_with") (param $rights i64) (result i32)
|
|
280
|
+
(local $errno i32)
|
|
281
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
|
|
282
|
+
(i32.const 0) (local.get $rights) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
283
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
284
|
+
(i32.store8 (i32.const 1208) (i32.const 2))
|
|
285
|
+
(i32.store (i32.const 1216) (i32.load (i32.const 128)))
|
|
286
|
+
(call $poll (i32.const 1200) (i32.const 1264) (i32.const 1) (i32.const 1296)))
|
|
287
|
+
;; Open `file.txt`, seek it to the largest offset, and write "hello".
|
|
288
|
+
(func (export "write_at_the_largest_offset") (result i32)
|
|
289
|
+
(local $errno i32)
|
|
290
|
+
(local.set $errno (call $open_read_write))
|
|
291
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
292
|
+
(local.set $errno (call $seek (i32.load (i32.const 128)) (i64.const 0x7fffffffffffffff) (i32.const 0) (i32.const 136)))
|
|
293
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
294
|
+
(call $write (i32.load (i32.const 128)) (i32.const 80) (i32.const 1) (i32.const 132)))
|
|
295
|
+
;; Advise on `file.txt` from `offset`, and for `offset` bytes from 0.
|
|
296
|
+
(func (export "advise_from") (param $offset i64) (result i32)
|
|
297
|
+
(local $errno i32)
|
|
298
|
+
(local.set $errno (call $open_read_write))
|
|
299
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
300
|
+
(call $advise (i32.load (i32.const 128)) (local.get $offset) (i64.const 0) (i32.const 0)))
|
|
301
|
+
(func (export "advise_for") (param $len i64) (result i32)
|
|
302
|
+
(local $errno i32)
|
|
303
|
+
(local.set $errno (call $open_read_write))
|
|
304
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
305
|
+
(call $advise (i32.load (i32.const 128)) (i64.const 0) (local.get $len) (i32.const 0)))
|
|
306
|
+
;; Open `file.txt`, then set it not to block.
|
|
307
|
+
(func (export "set_nonblocking") (result i32)
|
|
308
|
+
(local $errno i32)
|
|
309
|
+
(local.set $errno (call $open_read_write))
|
|
310
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
311
|
+
(call $set_flags (i32.load (i32.const 128)) (i32.const 4)))
|
|
312
|
+
;; Open `dir` as a directory and read its fdstat into 384.
|
|
313
|
+
(func (export "dir_fdstat") (result i32)
|
|
314
|
+
(local $errno i32)
|
|
315
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
|
|
316
|
+
(i32.const 2) (i64.const 0x4002) (i64.const 2) (i32.const 0) (i32.const 128)))
|
|
317
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
318
|
+
(call $fdstat (i32.load (i32.const 128)) (i32.const 384)))
|
|
319
|
+
;; Open `dir` as a directory with `rights`, then make call `$call` of it:
|
|
320
|
+
;; list, sync, advise, set its flags, size, or times, wait on it with its
|
|
321
|
+
;; event at 1264, stat it, stat `inner` from it, or read its fdstat into
|
|
322
|
+
;; 384.
|
|
323
|
+
(func (export "dir_with") (param $rights i64) (param $call i32) (result i32)
|
|
324
|
+
(local $errno i32)
|
|
325
|
+
(local $fd i32)
|
|
326
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
|
|
327
|
+
(i32.const 2) (local.get $rights) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
328
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
329
|
+
(local.set $fd (i32.load (i32.const 128)))
|
|
330
|
+
(if (i32.eq (local.get $call) (i32.const 0)) (then (return
|
|
331
|
+
(call $readdir (local.get $fd) (i32.const 2048) (i32.const 64) (i64.const 0) (i32.const 136)))))
|
|
332
|
+
(if (i32.eq (local.get $call) (i32.const 1)) (then (return (call $sync (local.get $fd)))))
|
|
333
|
+
(if (i32.eq (local.get $call) (i32.const 2)) (then (return
|
|
334
|
+
(call $advise (local.get $fd) (i64.const 0) (i64.const 0) (i32.const 0)))))
|
|
335
|
+
(if (i32.eq (local.get $call) (i32.const 3)) (then (return (call $set_flags (local.get $fd) (i32.const 0)))))
|
|
336
|
+
(if (i32.eq (local.get $call) (i32.const 4)) (then (return (call $set_size (local.get $fd) (i64.const 0)))))
|
|
337
|
+
(if (i32.eq (local.get $call) (i32.const 5)) (then (return
|
|
338
|
+
(call $set_times (local.get $fd) (i64.const 0) (i64.const 0) (i32.const 2)))))
|
|
339
|
+
(if (i32.eq (local.get $call) (i32.const 7)) (then (return (call $fstat (local.get $fd) (i32.const 384)))))
|
|
340
|
+
(if (i32.eq (local.get $call) (i32.const 8)) (then (return
|
|
341
|
+
(call $stat (local.get $fd) (i32.const 0) (i32.const 960) (i32.const 5) (i32.const 384)))))
|
|
342
|
+
(if (i32.eq (local.get $call) (i32.const 9)) (then (return (call $fdstat (local.get $fd) (i32.const 384)))))
|
|
343
|
+
(if (i32.eq (local.get $call) (i32.const 10)) (then (return (call $shutdown (local.get $fd) (i32.const 3)))))
|
|
344
|
+
(i32.store8 (i32.const 1208) (i32.const 1))
|
|
345
|
+
(i32.store (i32.const 1216) (local.get $fd))
|
|
346
|
+
(local.set $errno (call $poll (i32.const 1200) (i32.const 1264) (i32.const 1) (i32.const 1296)))
|
|
347
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
348
|
+
(i32.load16_u (i32.const 1272)))
|
|
349
|
+
;; Size `file.txt` to `offset` bytes.
|
|
350
|
+
(func (export "size_at") (param $offset i64) (result i32)
|
|
351
|
+
(local $errno i32)
|
|
352
|
+
(local.set $errno (call $open_read_write))
|
|
353
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
354
|
+
(call $set_size (i32.load (i32.const 128)) (local.get $offset)))
|
|
355
|
+
;; Open `file.txt` with `rights`, then read or write it through the
|
|
356
|
+
;; iovec past memory at 1140.
|
|
357
|
+
(func $open_with (param $rights i64) (result i32)
|
|
358
|
+
(call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
|
|
359
|
+
(i32.const 0) (local.get $rights) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
360
|
+
(func (export "read_far_with") (param $rights i64) (result i32)
|
|
361
|
+
(local $errno i32)
|
|
362
|
+
(local.set $errno (call $open_with (local.get $rights)))
|
|
363
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
364
|
+
(call $read (i32.load (i32.const 128)) (i32.const 1140) (i32.const 1) (i32.const 132)))
|
|
365
|
+
(func (export "pread_far_with") (param $rights i64) (result i32)
|
|
366
|
+
(local $errno i32)
|
|
367
|
+
(local.set $errno (call $open_with (local.get $rights)))
|
|
368
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
369
|
+
(call $pread (i32.load (i32.const 128)) (i32.const 1140) (i32.const 1) (i64.const 0) (i32.const 132)))
|
|
370
|
+
(func (export "write_far_with") (param $rights i64) (result i32)
|
|
371
|
+
(local $errno i32)
|
|
372
|
+
(local.set $errno (call $open_with (local.get $rights)))
|
|
373
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
374
|
+
(call $write (i32.load (i32.const 128)) (i32.const 1140) (i32.const 1) (i32.const 132)))
|
|
375
|
+
(func (export "pwrite_far_with") (param $rights i64) (result i32)
|
|
376
|
+
(local $errno i32)
|
|
377
|
+
(local.set $errno (call $open_with (local.get $rights)))
|
|
378
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
379
|
+
(call $pwrite (i32.load (i32.const 128)) (i32.const 1140) (i32.const 1) (i64.const 0) (i32.const 132)))
|
|
380
|
+
;; Open `file.txt` with `rights`, then size it to one byte.
|
|
381
|
+
(func (export "size_with") (param $rights i64) (result i32)
|
|
382
|
+
(local $errno i32)
|
|
383
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
|
|
384
|
+
(i32.const 0) (local.get $rights) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
385
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
386
|
+
(call $set_size (i32.load (i32.const 128)) (i64.const 1)))
|
|
387
|
+
;; List the root from `cookie` into the `len` bytes at 2048.
|
|
388
|
+
(func (export "list") (param $cookie i64) (param $len i32) (result i32)
|
|
389
|
+
(call $readdir (i32.const 3) (i32.const 2048) (local.get $len) (local.get $cookie) (i32.const 136)))
|
|
390
|
+
;; Open `path` for reading, following a link standing at its end.
|
|
391
|
+
(func (export "open_following") (param $path i32) (param $len i32) (result i32)
|
|
392
|
+
(call $open (i32.const 3) (i32.const 1) (local.get $path) (local.get $len)
|
|
393
|
+
(i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
394
|
+
;; Open `dir` as a directory, then the path beneath that descriptor.
|
|
395
|
+
(func (export "open_beneath_dir") (param $path i32) (param $len i32) (result i32)
|
|
396
|
+
(local $errno i32)
|
|
397
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
|
|
398
|
+
(i32.const 2) (i64.const 0x4002) (i64.const 2) (i32.const 0) (i32.const 128)))
|
|
399
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
400
|
+
(call $open (i32.load (i32.const 128)) (i32.const 0) (local.get $path) (local.get $len)
|
|
401
|
+
(i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 132)))
|
|
402
|
+
;; As `open_beneath_dir`, following a link at the path's end.
|
|
403
|
+
(func (export "open_following_beneath_dir") (param $path i32) (param $len i32) (result i32)
|
|
404
|
+
(local $errno i32)
|
|
405
|
+
(local.set $errno (call $open_dir))
|
|
406
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
407
|
+
(call $open (i32.load (i32.const 128)) (i32.const 1) (local.get $path) (local.get $len)
|
|
408
|
+
(i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 132)))
|
|
409
|
+
;; As `open_following_beneath_dir`, creating a file to write.
|
|
410
|
+
(func (export "create_following_beneath_dir") (param $path i32) (param $len i32) (result i32)
|
|
411
|
+
(local $errno i32)
|
|
412
|
+
(local.set $errno (call $open_dir))
|
|
413
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
414
|
+
(call $open (i32.load (i32.const 128)) (i32.const 1) (local.get $path) (local.get $len)
|
|
415
|
+
(i32.const 1) (i64.const 0x40) (i64.const 0) (i32.const 0) (i32.const 132)))
|
|
416
|
+
;; Stat the path beneath `dir`, following a link at its end, into 384.
|
|
417
|
+
(func (export "stat_following_beneath_dir") (param $path i32) (param $len i32) (result i32)
|
|
418
|
+
(local $errno i32)
|
|
419
|
+
(local.set $errno (call $open_dir))
|
|
420
|
+
(if (local.get $errno) (then (return (local.get $errno))))
|
|
421
|
+
(call $stat (i32.load (i32.const 128)) (i32.const 1) (local.get $path) (local.get $len) (i32.const 384)))
|
|
422
|
+
;; Open `dir` as a directory into 128.
|
|
423
|
+
(func $open_dir (export "open_dir") (result i32)
|
|
424
|
+
(call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
|
|
425
|
+
(i32.const 2) (i64.const 0x4002) (i64.const 2) (i32.const 0) (i32.const 128)))
|
|
426
|
+
;; Stat the directory opened into 128 itself.
|
|
427
|
+
(func (export "fstat_dir") (result i32)
|
|
428
|
+
(call $fstat (i32.load (i32.const 128)) (i32.const 384)))
|
|
429
|
+
;; List the directory opened into 128 from its start.
|
|
430
|
+
(func (export "list_dir") (result i32)
|
|
431
|
+
(call $readdir (i32.load (i32.const 128)) (i32.const 2048) (i32.const 256) (i64.const 0) (i32.const 136)))
|
|
432
|
+
;; Stat `inner` beneath the directory opened into 128.
|
|
433
|
+
(func (export "stat_inner") (result i32)
|
|
434
|
+
(call $stat (i32.load (i32.const 128)) (i32.const 0) (i32.const 960) (i32.const 5) (i32.const 384)))
|
|
435
|
+
;; Open `inner` for reading beneath the directory opened into 128.
|
|
436
|
+
(func (export "open_inner") (result i32)
|
|
437
|
+
(call $open (i32.load (i32.const 128)) (i32.const 0) (i32.const 960) (i32.const 5)
|
|
438
|
+
(i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 132))))
|
|
439
|
+
"#;
|
|
440
|
+
|
|
441
|
+
const FILE: (i32, i32) = (700, 8);
|
|
442
|
+
const ABOVE_ROOT: (i32, i32) = (720, 11);
|
|
443
|
+
const EMPTY: (i32, i32) = (700, 0);
|
|
444
|
+
const THROUGH_MISSING: (i32, i32) = (760, 19);
|
|
445
|
+
const OUT_OF_FILE: (i32, i32) = (780, 11);
|
|
446
|
+
const FILE_AS_DIRECTORY: (i32, i32) = (800, 9);
|
|
447
|
+
const NEW_AS_DIRECTORY: (i32, i32) = (820, 8);
|
|
448
|
+
#[cfg(unix)]
|
|
449
|
+
const THROUGH_FILE: (i32, i32) = (860, 12);
|
|
450
|
+
const DIR_DOT: (i32, i32) = (880, 5);
|
|
451
|
+
const DIR_DOT_DOT: (i32, i32) = (890, 6);
|
|
452
|
+
const BENEATH_FILE: (i32, i32) = (900, 10);
|
|
453
|
+
const MOVED: (i32, i32) = (920, 5);
|
|
454
|
+
const DIR: (i32, i32) = (930, 3);
|
|
455
|
+
const MISSING_DOT: (i32, i32) = (940, 9);
|
|
456
|
+
#[cfg(unix)]
|
|
457
|
+
const LINK_DOT: (i32, i32) = (970, 10);
|
|
458
|
+
#[cfg(unix)]
|
|
459
|
+
const OUT_OF_LINK: (i32, i32) = (990, 20);
|
|
460
|
+
const HOLDING_NUL: (i32, i32) = (1010, 9);
|
|
461
|
+
const NOT_UTF8: (i32, i32) = (1440, 1);
|
|
462
|
+
const DIR_FILE: (i32, i32) = (1030, 12);
|
|
463
|
+
#[cfg(unix)]
|
|
464
|
+
const DIR_AS_DIRECTORY: (i32, i32) = (1050, 4);
|
|
465
|
+
#[cfg(unix)]
|
|
466
|
+
const GONE_AS_DIRECTORY: (i32, i32) = (1060, 5);
|
|
467
|
+
const ERRNO_EXIST: i32 = 20;
|
|
468
|
+
const OFLAGS_EXCL: i32 = 1 << 2;
|
|
469
|
+
|
|
470
|
+
/// The default `OpenHow` with `set` applied.
|
|
471
|
+
fn build_how(set: impl FnOnce(&mut OpenHow)) -> OpenHow {
|
|
472
|
+
let mut how = OpenHow::default();
|
|
473
|
+
set(&mut how);
|
|
474
|
+
how
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
fn grant(
|
|
478
|
+
filesystem: Arc<dyn FileSystemType>,
|
|
479
|
+
access: Access,
|
|
480
|
+
policy: Option<Arc<dyn Policy>>,
|
|
481
|
+
) -> Grant {
|
|
482
|
+
let mount = Mount::new(
|
|
483
|
+
"/data",
|
|
484
|
+
Filesystem::new(filesystem).expect("the instance must stand"),
|
|
485
|
+
access,
|
|
486
|
+
)
|
|
487
|
+
.expect("/data is a guest path");
|
|
488
|
+
Grant::Mount(match policy {
|
|
489
|
+
Some(policy) => mount.with_policy(policy),
|
|
490
|
+
None => mount,
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/// One invocation's guest, handed `grants`.
|
|
495
|
+
struct Guest {
|
|
496
|
+
store: Store<Invocation>,
|
|
497
|
+
instance: Instance,
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
impl Guest {
|
|
501
|
+
fn new(grants: &[Grant]) -> Self {
|
|
502
|
+
Self::with_open_file_limit(None, grants)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/// A guest holding at most `open_file_limit` descriptors at once.
|
|
506
|
+
fn with_open_file_limit(open_file_limit: Option<usize>, grants: &[Grant]) -> Self {
|
|
507
|
+
Self::build(open_file_limit, None, Profile::Hermetic, Vec::new(), grants)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/// A guest allotted `memory_limit` bytes.
|
|
511
|
+
fn with_memory_limit(memory_limit: usize, grants: &[Grant]) -> Self {
|
|
512
|
+
Self::build(
|
|
513
|
+
None,
|
|
514
|
+
Some(memory_limit),
|
|
515
|
+
Profile::Hermetic,
|
|
516
|
+
Vec::new(),
|
|
517
|
+
grants,
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/// A guest on `profile`.
|
|
522
|
+
fn for_posture(profile: Profile, grants: &[Grant]) -> Self {
|
|
523
|
+
Self::build(None, None, profile, Vec::new(), grants)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/// A guest whose Driver was built with `security`.
|
|
527
|
+
fn with_security(security: Vec<Arc<dyn SecurityOperations>>, grants: &[Grant]) -> Self {
|
|
528
|
+
Self::build(None, None, Profile::Hermetic, security, grants)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
fn build(
|
|
532
|
+
open_file_limit: Option<usize>,
|
|
533
|
+
memory_limit: Option<usize>,
|
|
534
|
+
profile: Profile,
|
|
535
|
+
security: Vec<Arc<dyn SecurityOperations>>,
|
|
536
|
+
grants: &[Grant],
|
|
537
|
+
) -> Self {
|
|
538
|
+
let engine = load_engine().expect("shared engine must be constructible");
|
|
539
|
+
let config = Config {
|
|
540
|
+
timeout: None,
|
|
541
|
+
setup_timeout: None,
|
|
542
|
+
memory_limit,
|
|
543
|
+
stdout_limit: None,
|
|
544
|
+
stderr_limit: None,
|
|
545
|
+
open_file_limit,
|
|
546
|
+
profile,
|
|
547
|
+
security,
|
|
548
|
+
};
|
|
549
|
+
let wasi =
|
|
550
|
+
Wasi::for_invocation(&config, Vec::new(), grants).expect("WASI context must build");
|
|
551
|
+
let mut store = Store::new(engine, Invocation::new(None, wasi));
|
|
552
|
+
store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
|
|
553
|
+
let module = Module::new(engine, PROBE_WAT).expect("probe module must compile");
|
|
554
|
+
let instance = imports::linker()
|
|
555
|
+
.expect("host imports must link")
|
|
556
|
+
.instantiate(&mut store, &module)
|
|
557
|
+
.expect("probe module must instantiate");
|
|
558
|
+
Self { store, instance }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
fn call(&mut self, name: &str, path: (i32, i32)) -> i32 {
|
|
562
|
+
self.instance
|
|
563
|
+
.get_typed_func::<(i32, i32), i32>(&mut self.store, name)
|
|
564
|
+
.expect("probe export must resolve")
|
|
565
|
+
.call(&mut self.store, path)
|
|
566
|
+
.expect("probe call must not trap")
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/// Open `path` read-only with `oflags` and `fdflags`.
|
|
570
|
+
fn call_flagged(&mut self, path: (i32, i32), oflags: i32, fdflags: i32) -> i32 {
|
|
571
|
+
self.instance
|
|
572
|
+
.get_typed_func::<(i32, i32, i32, i32), i32>(&mut self.store, "open_flagged")
|
|
573
|
+
.expect("probe export must resolve")
|
|
574
|
+
.call(&mut self.store, (path.0, path.1, oflags, fdflags))
|
|
575
|
+
.expect("probe call must not trap")
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/// Call an export that takes two paths.
|
|
579
|
+
fn call_pair(&mut self, name: &str, first: (i32, i32), second: (i32, i32)) -> i32 {
|
|
580
|
+
self.instance
|
|
581
|
+
.get_typed_func::<(i32, i32, i32, i32), i32>(&mut self.store, name)
|
|
582
|
+
.expect("probe export must resolve")
|
|
583
|
+
.call(&mut self.store, (first.0, first.1, second.0, second.1))
|
|
584
|
+
.expect("probe call must not trap")
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/// Call an export that takes an offset, `None` when it traps.
|
|
588
|
+
fn try_call_at(&mut self, name: &str, offset: u64) -> Option<i32> {
|
|
589
|
+
self.instance
|
|
590
|
+
.get_typed_func::<i64, i32>(&mut self.store, name)
|
|
591
|
+
.expect("probe export must resolve")
|
|
592
|
+
.call(&mut self.store, offset as i64)
|
|
593
|
+
.ok()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/// Call an export that takes an offset.
|
|
597
|
+
fn call_at(&mut self, name: &str, offset: u64) -> i32 {
|
|
598
|
+
self.instance
|
|
599
|
+
.get_typed_func::<i64, i32>(&mut self.store, name)
|
|
600
|
+
.expect("probe export must resolve")
|
|
601
|
+
.call(&mut self.store, offset as i64)
|
|
602
|
+
.expect("probe call must not trap")
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/// Open `dir` with `rights` and make call `call` of it.
|
|
606
|
+
fn call_dir_with(&mut self, rights: i64, call: i32) -> i32 {
|
|
607
|
+
self.instance
|
|
608
|
+
.get_typed_func::<(i64, i32), i32>(&mut self.store, "dir_with")
|
|
609
|
+
.expect("probe export must resolve")
|
|
610
|
+
.call(&mut self.store, (rights, call))
|
|
611
|
+
.expect("probe call must not trap")
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/// Call an export that takes no path.
|
|
615
|
+
fn call_bare(&mut self, name: &str) -> i32 {
|
|
616
|
+
self.instance
|
|
617
|
+
.get_typed_func::<(), i32>(&mut self.store, name)
|
|
618
|
+
.expect("probe export must resolve")
|
|
619
|
+
.call(&mut self.store, ())
|
|
620
|
+
.expect("probe call must not trap")
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// Stat `file.txt`, without following a link, into 384.
|
|
624
|
+
fn call_flagged_stat(&mut self) -> i32 {
|
|
625
|
+
self.instance
|
|
626
|
+
.get_typed_func::<(i32, i32, i32), i32>(&mut self.store, "stat_flagged")
|
|
627
|
+
.expect("probe export must resolve")
|
|
628
|
+
.call(&mut self.store, (FILE.0, FILE.1, 0))
|
|
629
|
+
.expect("probe call must not trap")
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/// The names a listing from `cookie` into `len` bytes holds whole.
|
|
633
|
+
fn call_list(&mut self, cookie: u64, len: i32) -> i32 {
|
|
634
|
+
self.instance
|
|
635
|
+
.get_typed_func::<(i64, i32), i32>(&mut self.store, "list")
|
|
636
|
+
.expect("probe export must resolve")
|
|
637
|
+
.call(&mut self.store, (cookie as i64, len))
|
|
638
|
+
.expect("probe call must not trap")
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
fn list(&mut self, cookie: u64, len: i32) -> Vec<String> {
|
|
642
|
+
let errno = self.call_list(cookie, len);
|
|
643
|
+
assert_eq!(errno, 0, "the listing must succeed");
|
|
644
|
+
let memory = self
|
|
645
|
+
.instance
|
|
646
|
+
.get_memory(&mut self.store, "memory")
|
|
647
|
+
.expect("probe exports its memory");
|
|
648
|
+
let data = memory.data(&self.store);
|
|
649
|
+
let used = u32::from_le_bytes(data[136..140].try_into().unwrap()) as usize;
|
|
650
|
+
let listed = &data[2048..2048 + used];
|
|
651
|
+
let mut names = Vec::new();
|
|
652
|
+
let mut at = 0;
|
|
653
|
+
while at + 24 <= listed.len() {
|
|
654
|
+
let len = u32::from_le_bytes(listed[at + 16..at + 20].try_into().unwrap()) as usize;
|
|
655
|
+
let Some(name) = listed.get(at + 24..at + 24 + len) else {
|
|
656
|
+
break;
|
|
657
|
+
};
|
|
658
|
+
names.push(String::from_utf8(name.to_vec()).expect("a listed name is UTF-8"));
|
|
659
|
+
at += 24 + len;
|
|
660
|
+
}
|
|
661
|
+
names
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/// What `path_filestat_get` answers for `path` with `flags`.
|
|
665
|
+
fn stat_flagged(&mut self, path: (i32, i32), flags: i32) -> i32 {
|
|
666
|
+
self.instance
|
|
667
|
+
.get_typed_func::<(i32, i32, i32), i32>(&mut self.store, "stat_flagged")
|
|
668
|
+
.expect("probe export must resolve")
|
|
669
|
+
.call(&mut self.store, (path.0, path.1, flags))
|
|
670
|
+
.expect("probe call must not trap")
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/// The `u64` the last stat placed at `field` of its `filestat`.
|
|
674
|
+
fn stat_field(&mut self, field: usize) -> u64 {
|
|
675
|
+
let memory = self
|
|
676
|
+
.instance
|
|
677
|
+
.get_memory(&mut self.store, "memory")
|
|
678
|
+
.expect("probe exports its memory");
|
|
679
|
+
let at = 384 + field;
|
|
680
|
+
u64::from_le_bytes(memory.data(&self.store)[at..at + 8].try_into().unwrap())
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/// The `atim`, `mtim`, and `ctim` the last stat placed at 384.
|
|
684
|
+
fn stat_times(&mut self) -> [u64; 3] {
|
|
685
|
+
[40, 48, 56].map(|field| self.stat_field(field))
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/// The times `stat_flagged` answers for `path`.
|
|
689
|
+
fn times_of_path(&mut self, path: (i32, i32)) -> [u64; 3] {
|
|
690
|
+
assert_eq!(self.stat_flagged(path, 0), 0, "the stat must succeed");
|
|
691
|
+
self.stat_times()
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/// The bytes the last read placed at 512.
|
|
695
|
+
fn read_back(&mut self) -> Vec<u8> {
|
|
696
|
+
let memory = self
|
|
697
|
+
.instance
|
|
698
|
+
.get_memory(&mut self.store, "memory")
|
|
699
|
+
.expect("probe exports its memory");
|
|
700
|
+
let data = memory.data(&self.store);
|
|
701
|
+
let read = u32::from_le_bytes(data[132..136].try_into().unwrap()) as usize;
|
|
702
|
+
data[512..512 + read].to_vec()
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/// What `do_file_open` takes for an open `how` asks.
|
|
707
|
+
fn flags_of(how: &OpenHow) -> vfs::OpenFlags {
|
|
708
|
+
vfs::build_open_flags(how).expect("the flags must mean an open")
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
fn contents(root: &Path, path: &str) -> Option<Vec<u8>> {
|
|
712
|
+
let mut how = OpenHow::default();
|
|
713
|
+
how.read = true;
|
|
714
|
+
let Ok(mut file) = vfs::do_file_open(
|
|
715
|
+
&prepare_creds_within(&[], None),
|
|
716
|
+
root,
|
|
717
|
+
path,
|
|
718
|
+
&flags_of(&how),
|
|
719
|
+
) else {
|
|
720
|
+
return None;
|
|
721
|
+
};
|
|
722
|
+
let mut buf = vec![0; 64];
|
|
723
|
+
let read = file.pread(&mut buf, 0).ok()?;
|
|
724
|
+
buf.truncate(read);
|
|
725
|
+
Some(buf)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/// Make an empty file at `path`.
|
|
729
|
+
fn overwrite(root: &Path, path: &str) {
|
|
730
|
+
let mut how = OpenHow::default();
|
|
731
|
+
(how.write, how.create) = (true, true);
|
|
732
|
+
assert!(
|
|
733
|
+
vfs::do_file_open(
|
|
734
|
+
&prepare_creds_within(&[], None),
|
|
735
|
+
root,
|
|
736
|
+
path,
|
|
737
|
+
&flags_of(&how)
|
|
738
|
+
)
|
|
739
|
+
.is_ok(),
|
|
740
|
+
"the file must be made"
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
struct Refuses(&'static str);
|
|
745
|
+
|
|
746
|
+
impl Policy for Refuses {
|
|
747
|
+
fn allows(&self, path: &str, _access: Access) -> bool {
|
|
748
|
+
path != self.0
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
struct AllowsAll;
|
|
753
|
+
|
|
754
|
+
impl Policy for AllowsAll {
|
|
755
|
+
fn allows(&self, _path: &str, _access: Access) -> bool {
|
|
756
|
+
true
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/// A Filesystem holding the files `files`, each path stored in upper case and
|
|
761
|
+
/// reached by any case, as a host storing every name in upper case and
|
|
762
|
+
/// folding case does; a lookup hands back the entry it found under its stored
|
|
763
|
+
/// spelling.
|
|
764
|
+
#[derive(Clone)]
|
|
765
|
+
struct StoresUpperCase(&'static [&'static str]);
|
|
766
|
+
|
|
767
|
+
impl StoresUpperCase {
|
|
768
|
+
/// What stands at `path` once folded: a file it holds, a directory above
|
|
769
|
+
/// one, or nothing, each with an identity of its stored path, so a Policy
|
|
770
|
+
/// can tell it from another.
|
|
771
|
+
fn stat(&self, path: &str) -> Option<Kstat> {
|
|
772
|
+
let path = path.to_uppercase();
|
|
773
|
+
let beneath = format!("{path}/");
|
|
774
|
+
let kind = self.0.iter().find_map(|file| match *file {
|
|
775
|
+
file if file == path => Some(FileType::RegFile),
|
|
776
|
+
file if path.is_empty() || file.starts_with(&beneath) => Some(FileType::Dir),
|
|
777
|
+
_ => None,
|
|
778
|
+
})?;
|
|
779
|
+
let ino = path.bytes().fold(1, |ino: u64, byte| {
|
|
780
|
+
ino.wrapping_mul(31).wrapping_add(u64::from(byte))
|
|
781
|
+
});
|
|
782
|
+
Some(Kstat::new(kind, 0).with_identity(1, ino))
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
impl FileSystemType for StoresUpperCase {
|
|
787
|
+
fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
|
|
788
|
+
sb.d_make_root(vfs::Inode::new(
|
|
789
|
+
&vfs::Kstat::new(vfs::FileType::Dir, 0),
|
|
790
|
+
self,
|
|
791
|
+
));
|
|
792
|
+
Ok(())
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
impl InodeOperations for StoresUpperCase {
|
|
797
|
+
fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
798
|
+
self.stat(&dentry.path_raw()[1..])
|
|
799
|
+
.ok_or(FilesystemError::NotFound)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
|
|
803
|
+
let Some(found) = self.stat(&dentry.path_raw()[1..]) else {
|
|
804
|
+
return Ok(Lookup::Absent);
|
|
805
|
+
};
|
|
806
|
+
let inode = Inode::new(&found, Arc::new(self.clone()));
|
|
807
|
+
let name = dentry.name().to_uppercase();
|
|
808
|
+
Ok(match name == dentry.name() {
|
|
809
|
+
true => Lookup::Found(inode),
|
|
810
|
+
false => Lookup::Spelled { name, inode },
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// The Policy refuses only the stored spelling, so a Policy handed the
|
|
816
|
+
// spelling the guest asked for would let the read through.
|
|
817
|
+
// @behavior RT-120
|
|
818
|
+
#[test]
|
|
819
|
+
fn a_policy_judges_a_path_as_its_filesystem_stores_it() {
|
|
820
|
+
let policy: Arc<dyn Policy> = Arc::new(Refuses("/data/FILE.TXT"));
|
|
821
|
+
let mut guest = Guest::new(&[grant(
|
|
822
|
+
Arc::new(StoresUpperCase(&["FILE.TXT"])),
|
|
823
|
+
Access::Read,
|
|
824
|
+
Some(policy),
|
|
825
|
+
)]);
|
|
826
|
+
assert_eq!(
|
|
827
|
+
guest.call("read_file", FILE),
|
|
828
|
+
ERRNO_ACCES,
|
|
829
|
+
"path_open of `file.txt` beneath a Mount whose Policy refuses `/data/FILE.TXT`, over a \
|
|
830
|
+
Filesystem storing that name as `FILE.TXT`, through preview1 must answer EACCES"
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// A Policy handed the descriptor's path as the guest spelled it would see
|
|
835
|
+
// `/data/dir/inner` and let the read through.
|
|
836
|
+
// @behavior RT-120
|
|
837
|
+
#[test]
|
|
838
|
+
fn a_policy_judges_a_path_beneath_an_opened_directory_as_its_filesystem_stores_it() {
|
|
839
|
+
const INNER: (i32, i32) = (960, 5);
|
|
840
|
+
let policy: Arc<dyn Policy> = Arc::new(Refuses("/data/DIR/INNER"));
|
|
841
|
+
let mut guest = Guest::new(&[grant(
|
|
842
|
+
Arc::new(StoresUpperCase(&["DIR/INNER"])),
|
|
843
|
+
Access::Read,
|
|
844
|
+
Some(policy),
|
|
845
|
+
)]);
|
|
846
|
+
assert_eq!(
|
|
847
|
+
guest.call("open_beneath_dir", INNER),
|
|
848
|
+
ERRNO_ACCES,
|
|
849
|
+
"path_open of `inner` through `dir` opened beneath a Mount whose Policy refuses \
|
|
850
|
+
`/data/DIR/INNER`, over a Filesystem storing both names in upper case, through \
|
|
851
|
+
preview1 must answer EACCES"
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// @behavior RT-075
|
|
856
|
+
#[test]
|
|
857
|
+
fn a_mount_appears_as_a_directory_at_its_path() {
|
|
858
|
+
let mut guest = Guest::new(&[grant(Arc::new(Tmpfs::new()), Access::Read, None)]);
|
|
859
|
+
assert_eq!(
|
|
860
|
+
guest.call_bare("preopen_name"),
|
|
861
|
+
0,
|
|
862
|
+
"the first preopen must answer its name"
|
|
863
|
+
);
|
|
864
|
+
let memory = guest
|
|
865
|
+
.instance
|
|
866
|
+
.get_memory(&mut guest.store, "memory")
|
|
867
|
+
.expect("probe exports its memory");
|
|
868
|
+
assert_eq!(
|
|
869
|
+
&memory.data(&guest.store)[512..517],
|
|
870
|
+
b"/data",
|
|
871
|
+
"a Mount at /data through Runtime::invoke must preopen /data as descriptor 3"
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/// The inode number the guest reads for `file.txt` from a stat and from a
|
|
876
|
+
/// listing, and for the Mount's root from `.`.
|
|
877
|
+
fn inodes_seen(profile: Profile) -> (u64, u64, u64) {
|
|
878
|
+
let mut guest = Guest::for_posture(
|
|
879
|
+
profile,
|
|
880
|
+
&[grant(file_and_directory(), Access::ReadWrite, None)],
|
|
881
|
+
);
|
|
882
|
+
assert_eq!(guest.call_flagged_stat(), 0, "the stat must succeed");
|
|
883
|
+
assert!(
|
|
884
|
+
guest.list(0, 1024).contains(&"file.txt".to_string()),
|
|
885
|
+
"the listing must hold file.txt"
|
|
886
|
+
);
|
|
887
|
+
let memory = guest
|
|
888
|
+
.instance
|
|
889
|
+
.get_memory(&mut guest.store, "memory")
|
|
890
|
+
.expect("probe exports its memory");
|
|
891
|
+
let data = memory.data(&guest.store);
|
|
892
|
+
let u64_at = |at: usize| u64::from_le_bytes(data[at..at + 8].try_into().unwrap());
|
|
893
|
+
let used = u32::from_le_bytes(data[136..140].try_into().unwrap()) as usize;
|
|
894
|
+
let mut at = 2048;
|
|
895
|
+
let (mut dot, mut listed) = (None, None);
|
|
896
|
+
while at + 24 <= 2048 + used {
|
|
897
|
+
let len = u32::from_le_bytes(data[at + 16..at + 20].try_into().unwrap()) as usize;
|
|
898
|
+
match &data[at + 24..at + 24 + len] {
|
|
899
|
+
b"." => dot = Some(u64_at(at + 8)),
|
|
900
|
+
b"file.txt" => listed = Some(u64_at(at + 8)),
|
|
901
|
+
_ => {}
|
|
902
|
+
}
|
|
903
|
+
at += 24 + len;
|
|
904
|
+
}
|
|
905
|
+
(
|
|
906
|
+
u64_at(384 + 8),
|
|
907
|
+
listed.expect("the listing names file.txt"),
|
|
908
|
+
dot.expect("the listing names `.`"),
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// The hash is a host detail, so only what a guest can rely on is asserted: a
|
|
913
|
+
// listing and a stat agree, an entry is told from another, and it is no raw
|
|
914
|
+
// inode number, which a fresh Tmpfs numbers from 1.
|
|
915
|
+
// @behavior RT-183
|
|
916
|
+
#[test]
|
|
917
|
+
fn a_permissive_guest_tells_entries_apart_by_their_inode_numbers() {
|
|
918
|
+
let (stat, listed, dot) = inodes_seen(Profile::Permissive);
|
|
919
|
+
assert_eq!(
|
|
920
|
+
stat, listed,
|
|
921
|
+
"fd_readdir and path_filestat_get of a file beneath a Mount through a permissive \
|
|
922
|
+
guest's preview1 must answer the same inode number"
|
|
923
|
+
);
|
|
924
|
+
assert!(
|
|
925
|
+
stat > u64::from(u16::MAX) && dot > u64::from(u16::MAX) && dot != stat,
|
|
926
|
+
"a permissive guest's inode numbers for a file and its directory must differ and be \
|
|
927
|
+
hashes rather than a Filesystem's own small numbers, not {stat} and {dot}"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// A Tmpfs's device number depends on how many came before it, so any number
|
|
932
|
+
// drawn from it would differ between otherwise identical runs.
|
|
933
|
+
// @behavior RT-183
|
|
934
|
+
#[test]
|
|
935
|
+
fn a_hermetic_guest_sees_no_inode_numbers() {
|
|
936
|
+
assert_eq!(
|
|
937
|
+
inodes_seen(Profile::Hermetic),
|
|
938
|
+
(0, 0, 0),
|
|
939
|
+
"a stat and a listing of a file and its directory beneath a Mount through a hermetic \
|
|
940
|
+
guest's preview1 must answer inode number 0"
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/// A host directory holding only `a.txt`, which a Bind reads whole to list.
|
|
945
|
+
fn host_dir_with_a_file(name: &str) -> std::path::PathBuf {
|
|
946
|
+
let dir = std::env::temp_dir().join(format!("kobako-listing-{name}-{}", std::process::id()));
|
|
947
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
948
|
+
std::fs::create_dir_all(&dir).expect("the host directory must be created");
|
|
949
|
+
std::fs::write(dir.join("a.txt"), "").expect("the host file must be written");
|
|
950
|
+
dir
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// The VFS hands a Bind every name no walk looked up, so a Bind making what
|
|
954
|
+
// any open asks for would leave a file on the host for a guest that only
|
|
955
|
+
// tried to write one standing.
|
|
956
|
+
// @behavior RT-311
|
|
957
|
+
#[test]
|
|
958
|
+
fn a_bind_opens_a_name_no_walk_looked_up_and_makes_nothing() {
|
|
959
|
+
let dir = host_dir_with_a_file("opens-unlooked");
|
|
960
|
+
std::fs::write(dir.join("file.txt"), "granted").expect("the host file must be written");
|
|
961
|
+
let bind = Arc::new(Bind::new(&dir).expect("the host directory must open"));
|
|
962
|
+
let mut guest = Guest::new(&[grant(bind, Access::ReadWrite, None)]);
|
|
963
|
+
let read = guest.call("read_file", FILE);
|
|
964
|
+
let content = guest.read_back();
|
|
965
|
+
let appended = guest.call("append_file", NEW);
|
|
966
|
+
assert_eq!(
|
|
967
|
+
(read, content, appended, dir.join("new.txt").exists()),
|
|
968
|
+
(0, b"granted".to_vec(), ERRNO_NOENT, false),
|
|
969
|
+
"path_open of a file no walk looked up beneath a Bind through preview1 must read it, \
|
|
970
|
+
and of a missing path without O_CREAT must answer ENOENT and make nothing on the host"
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// The first listing fills its buffer with `.` and `..` exactly, so it has
|
|
975
|
+
// not reached its end when the file is made.
|
|
976
|
+
// @behavior RT-107
|
|
977
|
+
#[test]
|
|
978
|
+
fn a_listing_goes_on_over_the_entries_it_began_with() {
|
|
979
|
+
let dir = host_dir_with_a_file("goes-on");
|
|
980
|
+
let bind = Arc::new(Bind::new(&dir).expect("the host directory must open"));
|
|
981
|
+
let mut guest = Guest::new(&[grant(bind.clone(), Access::ReadWrite, None)]);
|
|
982
|
+
assert_eq!(
|
|
983
|
+
guest.list(0, 51),
|
|
984
|
+
[".", ".."],
|
|
985
|
+
"the first listing must hold the dots"
|
|
986
|
+
);
|
|
987
|
+
overwrite(&host(&bind), "b.txt");
|
|
988
|
+
assert_eq!(
|
|
989
|
+
guest.list(2, 4096),
|
|
990
|
+
["a.txt"],
|
|
991
|
+
"a listing going on through fd_readdir must read the entries it began with"
|
|
992
|
+
);
|
|
993
|
+
let mut from_the_start = guest.list(0, 4096);
|
|
994
|
+
from_the_start.sort();
|
|
995
|
+
assert_eq!(
|
|
996
|
+
from_the_start,
|
|
997
|
+
[".", "..", "a.txt", "b.txt"],
|
|
998
|
+
"a listing from the start through fd_readdir must read the directory as it stands"
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// A buffer of 51 takes only the dots, one of 4096 the whole listing; the limit
|
|
1003
|
+
// the listing is held against is the memory the guest was allotted, counted
|
|
1004
|
+
// from the moment the listing is read.
|
|
1005
|
+
// @behavior RT-237
|
|
1006
|
+
#[test]
|
|
1007
|
+
fn a_listing_it_cannot_hold_within_a_guests_memory_answers_enomem() {
|
|
1008
|
+
let dir = host_dir_with_a_file("holds");
|
|
1009
|
+
let answer = |memory_limit, len| {
|
|
1010
|
+
let bind = Arc::new(Bind::new(&dir).expect("the host directory must open"));
|
|
1011
|
+
let mut guest =
|
|
1012
|
+
Guest::with_memory_limit(memory_limit, &[grant(bind, Access::ReadWrite, None)]);
|
|
1013
|
+
let errno = guest.call_list(0, len);
|
|
1014
|
+
let memory = guest
|
|
1015
|
+
.instance
|
|
1016
|
+
.get_memory(&mut guest.store, "memory")
|
|
1017
|
+
.expect("probe exports its memory");
|
|
1018
|
+
let used = u32::from_le_bytes(memory.data(&guest.store)[136..140].try_into().unwrap());
|
|
1019
|
+
(errno, used)
|
|
1020
|
+
};
|
|
1021
|
+
assert_eq!(
|
|
1022
|
+
[answer(1, 51), answer(1, 4096), answer(1 << 20, 51)],
|
|
1023
|
+
[(ERRNO_NOMEM, 0), (ERRNO_NOMEM, 0), (0, 51)],
|
|
1024
|
+
"fd_readdir through preview1, with more to hold than the guest's memory_limit, must \
|
|
1025
|
+
answer ENOMEM before it hands any entry, read part way or whole, and within it succeed"
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/// A root whose listing hands `.` and `..`, then fails as a host read would.
|
|
1030
|
+
struct FailsPastTheDots;
|
|
1031
|
+
|
|
1032
|
+
struct FailsPastTheDotsDir;
|
|
1033
|
+
|
|
1034
|
+
impl FileOperations for FailsPastTheDotsDir {
|
|
1035
|
+
fn iterate_shared(
|
|
1036
|
+
&mut self,
|
|
1037
|
+
dentry: &Dentry,
|
|
1038
|
+
ctx: &mut vfs::DirContext<'_>,
|
|
1039
|
+
) -> Result<(), FilesystemError> {
|
|
1040
|
+
if !vfs::dir_emit_dots(dentry, ctx) {
|
|
1041
|
+
return Ok(());
|
|
1042
|
+
}
|
|
1043
|
+
Err(FilesystemError::Other("the host read failed".into()))
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
impl InodeOperations for FailsPastTheDots {
|
|
1048
|
+
fn getattr(&self, _dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
1049
|
+
Ok(Kstat::new(FileType::Dir, 0))
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
fn open(
|
|
1053
|
+
&self,
|
|
1054
|
+
_dentry: &Dentry,
|
|
1055
|
+
_how: &OpenHow,
|
|
1056
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
1057
|
+
Ok(Box::new(FailsPastTheDotsDir))
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
impl FileSystemType for FailsPastTheDots {
|
|
1062
|
+
fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
|
|
1063
|
+
sb.d_make_root(vfs::Inode::new(
|
|
1064
|
+
&vfs::Kstat::new(vfs::FileType::Dir, 0),
|
|
1065
|
+
self,
|
|
1066
|
+
));
|
|
1067
|
+
Ok(())
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// The dots are written before the Filesystem fails, so only the call going on
|
|
1072
|
+
// from them meets the failure with nothing written.
|
|
1073
|
+
// @behavior RT-301
|
|
1074
|
+
#[test]
|
|
1075
|
+
fn a_listing_failing_once_it_has_handed_entries_answers_those_entries() {
|
|
1076
|
+
let mut guest = Guest::new(&[grant(Arc::new(FailsPastTheDots), Access::Read, None)]);
|
|
1077
|
+
let first = guest.list(0, 4096);
|
|
1078
|
+
assert_eq!(
|
|
1079
|
+
(first, guest.call_list(2, 4096)),
|
|
1080
|
+
(vec![".".to_string(), "..".to_string()], ERRNO_IO),
|
|
1081
|
+
"fd_readdir through preview1 of a Filesystem failing after it hands the dots must \
|
|
1082
|
+
answer the dots, and the next call from them the error, as getdents64 answers the \
|
|
1083
|
+
bytes it wrote over iterate_dir's error"
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// @behavior RT-076
|
|
1088
|
+
#[test]
|
|
1089
|
+
fn a_guest_reads_a_file_beneath_a_mount() {
|
|
1090
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
1091
|
+
let mut guest = Guest::new(&[grant(Arc::new(tmpfs), Access::Read, None)]);
|
|
1092
|
+
assert_eq!(guest.call("read_file", FILE), 0, "the read must succeed");
|
|
1093
|
+
assert_eq!(
|
|
1094
|
+
guest.read_back(),
|
|
1095
|
+
b"granted",
|
|
1096
|
+
"a file read beneath a Mount through preview1 must read the Tmpfs's content"
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// @behavior RT-077
|
|
1101
|
+
#[test]
|
|
1102
|
+
fn a_guest_writes_a_file_beneath_a_read_write_mount() {
|
|
1103
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
1104
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
1105
|
+
assert_eq!(guest.call("write_file", FILE), 0, "the write must succeed");
|
|
1106
|
+
assert_eq!(
|
|
1107
|
+
contents(&host(&tmpfs), "file.txt").as_deref(),
|
|
1108
|
+
Some(&b"hello"[..]),
|
|
1109
|
+
"a file written beneath a read-write Mount through preview1 must land in the Tmpfs"
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// @behavior RT-078
|
|
1114
|
+
#[test]
|
|
1115
|
+
fn a_change_beneath_a_read_only_mount_is_a_read_only_filesystem() {
|
|
1116
|
+
let mut guest = Guest::new(&[grant(Arc::new(Tmpfs::new()), Access::Read, None)]);
|
|
1117
|
+
assert_eq!(
|
|
1118
|
+
guest.call("write_file", FILE),
|
|
1119
|
+
ERRNO_ROFS,
|
|
1120
|
+
"a write opened beneath a read-only Mount through preview1 must answer EROFS"
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// An open asking only to read can still truncate. wasmtime-wasi once let
|
|
1125
|
+
// such an open past a read-only file permission (CVE-2026-47261), so the
|
|
1126
|
+
// truncation alone must count as a change.
|
|
1127
|
+
// @behavior RT-078
|
|
1128
|
+
#[test]
|
|
1129
|
+
fn a_truncating_open_beneath_a_read_only_mount_is_refused() {
|
|
1130
|
+
let tmpfs = tmpfs_with("file.txt", "granted");
|
|
1131
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::Read, None)]);
|
|
1132
|
+
assert_eq!(
|
|
1133
|
+
(
|
|
1134
|
+
guest.call("truncate_file", FILE),
|
|
1135
|
+
contents(&host(&tmpfs), "file.txt")
|
|
1136
|
+
),
|
|
1137
|
+
(ERRNO_ROFS, Some(b"granted".to_vec())),
|
|
1138
|
+
"a read-rights open with O_TRUNC beneath a read-only Mount through preview1 must answer \
|
|
1139
|
+
EROFS and leave the file whole"
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// @behavior RT-079
|
|
1144
|
+
#[test]
|
|
1145
|
+
fn a_path_above_a_mounts_root_is_refused() {
|
|
1146
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
1147
|
+
let mut guest = Guest::new(&[grant(Arc::new(tmpfs), Access::Read, None)]);
|
|
1148
|
+
assert_eq!(
|
|
1149
|
+
guest.call("read_file", ABOVE_ROOT),
|
|
1150
|
+
ERRNO_NOTCAPABLE,
|
|
1151
|
+
"a path climbing above a Mount's root through preview1 must answer ENOTCAPABLE"
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// A descriptor confines the guest to what lies beneath it, as cap-std
|
|
1156
|
+
// confines wasmtime-wasi's, even where its Mount reaches further.
|
|
1157
|
+
// @behavior RT-115
|
|
1158
|
+
#[test]
|
|
1159
|
+
fn a_path_above_the_directory_it_is_opened_beneath_is_refused() {
|
|
1160
|
+
let tmpfs: Tmpfs = [("file.txt", "granted"), ("dir/inner.txt", "beneath")]
|
|
1161
|
+
.into_iter()
|
|
1162
|
+
.collect();
|
|
1163
|
+
let mut guest = Guest::new(&[grant(Arc::new(tmpfs), Access::Read, None)]);
|
|
1164
|
+
assert_eq!(
|
|
1165
|
+
guest.call("open_beneath_dir", ABOVE_ROOT),
|
|
1166
|
+
ERRNO_NOTCAPABLE,
|
|
1167
|
+
"a path climbing above a non-preopened directory descriptor through preview1 must answer \
|
|
1168
|
+
ENOTCAPABLE, as wasmtime-wasi does"
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/// What `stat_inner` answers through `dir` opened, beneath a Mount with
|
|
1173
|
+
/// `policy`, before `change` runs on the host's root of the Tmpfs
|
|
1174
|
+
/// holding it.
|
|
1175
|
+
fn stat_after(policy: Option<Arc<dyn Policy>>, change: impl FnOnce(&Path)) -> i32 {
|
|
1176
|
+
let tmpfs = file_and_directory();
|
|
1177
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, policy)]);
|
|
1178
|
+
assert_eq!(
|
|
1179
|
+
guest.call_bare("open_dir"),
|
|
1180
|
+
0,
|
|
1181
|
+
"dir must open as a directory"
|
|
1182
|
+
);
|
|
1183
|
+
change(&host(&tmpfs));
|
|
1184
|
+
guest.call_bare("stat_inner")
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/// Move `dir` away, its `inner` with it, and make another `dir` holding
|
|
1188
|
+
/// an `inner` of its own, so only where the descriptor reaches decides
|
|
1189
|
+
/// whether the lookup finds one.
|
|
1190
|
+
fn move_and_replace(root: &Path) {
|
|
1191
|
+
vfs::filename_renameat2(&prepare_creds_within(&[], None), root, "dir", root, "moved").unwrap();
|
|
1192
|
+
vfs::filename_mkdirat(&prepare_creds_within(&[], None), root, "dir").unwrap();
|
|
1193
|
+
overwrite(root, "dir/inner");
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// A directory made at the path holds an `inner` of its own, so only the
|
|
1197
|
+
// descriptor still standing for the removed one keeps it from reaching it.
|
|
1198
|
+
// @behavior RT-117
|
|
1199
|
+
#[test]
|
|
1200
|
+
fn a_directory_made_in_a_removed_directorys_place_is_not_reached_through_it() {
|
|
1201
|
+
let answer = stat_after(None, |root| {
|
|
1202
|
+
vfs::filename_unlinkat(&prepare_creds_within(&[], None), root, "dir/inner").unwrap();
|
|
1203
|
+
vfs::filename_rmdir(&prepare_creds_within(&[], None), root, "dir").unwrap();
|
|
1204
|
+
vfs::filename_mkdirat(&prepare_creds_within(&[], None), root, "dir").unwrap();
|
|
1205
|
+
overwrite(root, "dir/inner");
|
|
1206
|
+
});
|
|
1207
|
+
assert_eq!(
|
|
1208
|
+
answer, ERRNO_NOENT,
|
|
1209
|
+
"path_filestat_get through a directory descriptor beneath a Mount through preview1 must \
|
|
1210
|
+
answer ENOENT once the directory was removed and another made at its path"
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// @behavior RT-149
|
|
1215
|
+
#[test]
|
|
1216
|
+
fn a_directory_moved_while_opened_is_reached_through_its_descriptor() {
|
|
1217
|
+
assert_eq!(
|
|
1218
|
+
stat_after(None, move_and_replace),
|
|
1219
|
+
0,
|
|
1220
|
+
"path_filestat_get through a directory descriptor beneath a Mount without a Policy \
|
|
1221
|
+
through preview1 must reach the directory where it moved"
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// A Bind opens a directory a walk only steps through when a call needs
|
|
1226
|
+
// it, so only an open that holds the host directory at once keeps the
|
|
1227
|
+
// descriptor on it once another stands at its path.
|
|
1228
|
+
// @behavior RT-149
|
|
1229
|
+
#[cfg(unix)]
|
|
1230
|
+
#[test]
|
|
1231
|
+
fn a_host_directory_moved_while_opened_is_reached_through_its_descriptor() {
|
|
1232
|
+
let host = LinkedHost::new("moved-open");
|
|
1233
|
+
std::fs::create_dir_all(host.0.join("real2/inner")).expect("the host tree must be made");
|
|
1234
|
+
std::fs::remove_file(host.0.join("dir")).expect("the link must go");
|
|
1235
|
+
std::fs::rename(host.0.join("real2"), host.0.join("dir")).expect("the directory must move");
|
|
1236
|
+
let mut guest = host.guest();
|
|
1237
|
+
assert_eq!(guest.call_bare("open_dir"), 0, "dir must open");
|
|
1238
|
+
std::fs::rename(host.0.join("dir"), host.0.join("moved")).expect("the directory must move");
|
|
1239
|
+
std::fs::create_dir(host.0.join("dir")).expect("another directory must be made");
|
|
1240
|
+
assert_eq!(
|
|
1241
|
+
guest.call_bare("stat_inner"),
|
|
1242
|
+
0,
|
|
1243
|
+
"path_filestat_get of inner through a descriptor of a host directory beneath a Bind \
|
|
1244
|
+
Mount, moved away after it opened and another made at its path, through preview1 must \
|
|
1245
|
+
reach the directory where it moved"
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// The move goes through another Filesystem instance, which the guest's
|
|
1250
|
+
// dentries never see, so only the Tmpfs answering the descriptor's dentry
|
|
1251
|
+
// stale, once another directory stands at its path, refuses the call.
|
|
1252
|
+
// @behavior RT-150
|
|
1253
|
+
#[test]
|
|
1254
|
+
fn a_directory_moved_while_opened_beneath_a_policy_is_not_reached() {
|
|
1255
|
+
assert_eq!(
|
|
1256
|
+
stat_after(Some(Arc::new(AllowsAll)), move_and_replace),
|
|
1257
|
+
ERRNO_NOENT,
|
|
1258
|
+
"path_filestat_get through a directory descriptor beneath a Mount with a Policy through \
|
|
1259
|
+
preview1 must answer ENOENT once the directory moved away"
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/// What `call` answers through the directory a guest holds beneath a Policy,
|
|
1264
|
+
/// once the directory moved away outside the runtime.
|
|
1265
|
+
fn answer_once_moved_away(call: &str) -> i32 {
|
|
1266
|
+
let tmpfs = file_and_directory();
|
|
1267
|
+
let mut guest = Guest::new(&[grant(
|
|
1268
|
+
tmpfs.clone(),
|
|
1269
|
+
Access::ReadWrite,
|
|
1270
|
+
Some(Arc::new(AllowsAll)),
|
|
1271
|
+
)]);
|
|
1272
|
+
assert_eq!(guest.call_bare("open_dir"), 0, "dir must open");
|
|
1273
|
+
move_and_replace(&host(&tmpfs));
|
|
1274
|
+
guest.call_bare(call)
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// A Policy judges no stat or listing, so nothing asks whether the descriptor's
|
|
1278
|
+
// dentry still stands; the descriptor answers as its Filesystem holds it.
|
|
1279
|
+
// @behavior RT-226
|
|
1280
|
+
#[test]
|
|
1281
|
+
fn a_descriptor_moved_beneath_a_policy_still_reports_itself() {
|
|
1282
|
+
assert_eq!(
|
|
1283
|
+
answer_once_moved_away("fstat_dir"),
|
|
1284
|
+
0,
|
|
1285
|
+
"fd_filestat_get of a directory descriptor beneath a Mount with a Policy through preview1 \
|
|
1286
|
+
must succeed once the directory moved away"
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// @behavior RT-227
|
|
1291
|
+
#[test]
|
|
1292
|
+
fn a_descriptor_moved_beneath_a_policy_still_lists_itself() {
|
|
1293
|
+
assert_eq!(
|
|
1294
|
+
answer_once_moved_away("list_dir"),
|
|
1295
|
+
0,
|
|
1296
|
+
"fd_readdir of a directory descriptor beneath a Mount with a Policy through preview1 must \
|
|
1297
|
+
succeed once the directory moved away"
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// The second Mount shares the instance's dentries, so the move carries
|
|
1302
|
+
// the descriptor's along, and the Policy judges the path it moved to:
|
|
1303
|
+
// allowed there it reaches `inner`, refused there it answers EACCES.
|
|
1304
|
+
// @behavior RT-170
|
|
1305
|
+
#[test]
|
|
1306
|
+
fn a_directory_moved_beneath_another_mount_of_its_instance_is_reached_beneath_a_policy() {
|
|
1307
|
+
let open_after_move = |policy: Arc<dyn Policy>| {
|
|
1308
|
+
let instance = Filesystem::new(file_and_directory()).expect("the instance must stand");
|
|
1309
|
+
let mount = Mount::new("/data", instance.clone(), Access::ReadWrite)
|
|
1310
|
+
.expect("/data is a guest path");
|
|
1311
|
+
let mut guest = Guest::new(&[Grant::Mount(mount.clone().with_policy(policy))]);
|
|
1312
|
+
assert_eq!(guest.call_bare("open_dir"), 0, "dir must open");
|
|
1313
|
+
let other = Path::root(mount.mnt());
|
|
1314
|
+
vfs::filename_renameat2(
|
|
1315
|
+
&prepare_creds_within(&[], None),
|
|
1316
|
+
&other,
|
|
1317
|
+
"dir",
|
|
1318
|
+
&other,
|
|
1319
|
+
"moved",
|
|
1320
|
+
)
|
|
1321
|
+
.expect("the directory must move");
|
|
1322
|
+
vfs::filename_mkdirat(&prepare_creds_within(&[], None), &other, "dir")
|
|
1323
|
+
.expect("another directory must be made");
|
|
1324
|
+
guest.call_bare("open_inner")
|
|
1325
|
+
};
|
|
1326
|
+
assert_eq!(
|
|
1327
|
+
(
|
|
1328
|
+
open_after_move(Arc::new(AllowsAll)),
|
|
1329
|
+
open_after_move(Arc::new(Refuses("/data/moved/inner")))
|
|
1330
|
+
),
|
|
1331
|
+
(0, ERRNO_ACCES),
|
|
1332
|
+
"path_open of inner through a directory descriptor beneath a Mount with a Policy, \
|
|
1333
|
+
once the directory moved beneath another Mount of the same Filesystem instance, through \
|
|
1334
|
+
preview1 must reach it where it moved, as the Policy judges that path"
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/// A Tmpfs naming `dir` by one inode number for as long as no directory
|
|
1339
|
+
/// it opened there is open, as a host hands a removed directory's number
|
|
1340
|
+
/// to the next one made, and by a fresh number once an open `dir` was
|
|
1341
|
+
/// removed.
|
|
1342
|
+
#[derive(Default)]
|
|
1343
|
+
struct ReusesInodes {
|
|
1344
|
+
found: Mutex<Vec<Weak<dyn InodeOperations>>>,
|
|
1345
|
+
removed_while_open: AtomicUsize,
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
impl ReusesInodes {
|
|
1349
|
+
fn dir_kstat(&self, found: Kstat) -> Kstat {
|
|
1350
|
+
let ino = 100 + self.removed_while_open.load(Ordering::SeqCst) as u64;
|
|
1351
|
+
found.with_identity(1, ino)
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
impl Hook for ReusesInodes {
|
|
1356
|
+
fn getattr(
|
|
1357
|
+
&self,
|
|
1358
|
+
inner: &dyn InodeOperations,
|
|
1359
|
+
dentry: &Dentry,
|
|
1360
|
+
) -> Result<Kstat, FilesystemError> {
|
|
1361
|
+
let found = inner.getattr(dentry)?;
|
|
1362
|
+
Ok(match dentry.name().as_str() {
|
|
1363
|
+
"dir" => self.dir_kstat(found),
|
|
1364
|
+
_ => found,
|
|
1365
|
+
})
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
fn lookup(
|
|
1369
|
+
&self,
|
|
1370
|
+
inner: &dyn InodeOperations,
|
|
1371
|
+
dentry: &Dentry,
|
|
1372
|
+
) -> Result<Lookup, FilesystemError> {
|
|
1373
|
+
Ok(match inner.lookup(dentry)? {
|
|
1374
|
+
Lookup::Found(inode) if dentry.name() == "dir" => {
|
|
1375
|
+
self.found.lock().unwrap().push(Arc::downgrade(inode.ops()));
|
|
1376
|
+
let found = self.dir_kstat(inode.ops().getattr(dentry)?);
|
|
1377
|
+
Lookup::Found(Inode::new(&found, inode.ops().clone()))
|
|
1378
|
+
}
|
|
1379
|
+
other => other,
|
|
1380
|
+
})
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/// Its root answers as the Filesystem's and sets up nothing of it.
|
|
1384
|
+
fn fill_super(
|
|
1385
|
+
&self,
|
|
1386
|
+
fs: Arc<dyn FileSystemType>,
|
|
1387
|
+
sb: &mut Filesystem,
|
|
1388
|
+
) -> Result<(), FilesystemError> {
|
|
1389
|
+
let beneath = Filesystem::new(fs)?;
|
|
1390
|
+
let root = beneath
|
|
1391
|
+
.root()
|
|
1392
|
+
.and_then(|root| root.inode())
|
|
1393
|
+
.ok_or(FilesystemError::NotFound)?;
|
|
1394
|
+
sb.d_make_root(vfs::Inode::new(
|
|
1395
|
+
&vfs::Kstat::new(vfs::FileType::Dir, 0),
|
|
1396
|
+
root.ops().clone(),
|
|
1397
|
+
));
|
|
1398
|
+
Ok(())
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/// The call removing it holds what it looked up, so `dir` is open
|
|
1402
|
+
/// beyond that only where a descriptor holds it too.
|
|
1403
|
+
fn rmdir(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
1404
|
+
let removed = inner.rmdir(dentry);
|
|
1405
|
+
let held = self
|
|
1406
|
+
.found
|
|
1407
|
+
.lock()
|
|
1408
|
+
.unwrap()
|
|
1409
|
+
.iter()
|
|
1410
|
+
.filter(|dir| dir.strong_count() > 0)
|
|
1411
|
+
.count();
|
|
1412
|
+
if removed.is_ok() && dentry.name() == "dir" && held > 1 {
|
|
1413
|
+
self.removed_while_open.fetch_add(1, Ordering::SeqCst);
|
|
1414
|
+
}
|
|
1415
|
+
removed
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// The Filesystem gives the new `dir` the old one's number unless the
|
|
1420
|
+
// directory opened there is still open, and beneath a Policy the
|
|
1421
|
+
// descriptor finds its directory again by that number, so only the
|
|
1422
|
+
// descriptor keeping the directory open refuses the call.
|
|
1423
|
+
// @behavior RT-121
|
|
1424
|
+
#[test]
|
|
1425
|
+
fn a_directory_descriptor_holds_its_directory_while_open() {
|
|
1426
|
+
let tmpfs = file_and_directory();
|
|
1427
|
+
let reuses = Wrap::new(tmpfs.clone(), Arc::new(ReusesInodes::default()));
|
|
1428
|
+
let mut guest = Guest::new(&[grant(
|
|
1429
|
+
reuses.clone(),
|
|
1430
|
+
Access::ReadWrite,
|
|
1431
|
+
Some(Arc::new(AllowsAll)),
|
|
1432
|
+
)]);
|
|
1433
|
+
assert_eq!(
|
|
1434
|
+
guest.call_bare("open_dir"),
|
|
1435
|
+
0,
|
|
1436
|
+
"dir must open as a directory"
|
|
1437
|
+
);
|
|
1438
|
+
let outside = host(&reuses);
|
|
1439
|
+
vfs::filename_unlinkat(&prepare_creds_within(&[], None), &outside, "dir/inner").unwrap();
|
|
1440
|
+
vfs::filename_rmdir(&prepare_creds_within(&[], None), &outside, "dir").unwrap();
|
|
1441
|
+
vfs::filename_mkdirat(&prepare_creds_within(&[], None), &outside, "dir").unwrap();
|
|
1442
|
+
overwrite(&host(&tmpfs), "dir/inner");
|
|
1443
|
+
assert_eq!(
|
|
1444
|
+
guest.call_bare("stat_inner"),
|
|
1445
|
+
ERRNO_NOENT,
|
|
1446
|
+
"path_filestat_get through a directory descriptor beneath a Mount through preview1, once \
|
|
1447
|
+
its directory is removed and made again on a Filesystem reusing inode numbers, must \
|
|
1448
|
+
answer ENOENT"
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/// A Tmpfs reporting no identity for anything it holds, as a host whose
|
|
1453
|
+
/// stat names no inode.
|
|
1454
|
+
struct NamesNoIdentity;
|
|
1455
|
+
|
|
1456
|
+
impl Hook for NamesNoIdentity {
|
|
1457
|
+
fn lookup(
|
|
1458
|
+
&self,
|
|
1459
|
+
inner: &dyn InodeOperations,
|
|
1460
|
+
dentry: &Dentry,
|
|
1461
|
+
) -> Result<Lookup, FilesystemError> {
|
|
1462
|
+
Ok(match inner.lookup(dentry)? {
|
|
1463
|
+
Lookup::Found(inode) => {
|
|
1464
|
+
let found = inode.ops().getattr(dentry)?;
|
|
1465
|
+
let bare = Kstat::new(found.file_type, found.size);
|
|
1466
|
+
Lookup::Found(Inode::new(&bare, inode.ops().clone()))
|
|
1467
|
+
}
|
|
1468
|
+
other => other,
|
|
1469
|
+
})
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Without an identity the descriptor cannot tell its directory from one
|
|
1474
|
+
// made in its place, so beneath a Policy it answers less rather than
|
|
1475
|
+
// reach another.
|
|
1476
|
+
// @behavior RT-151
|
|
1477
|
+
#[test]
|
|
1478
|
+
fn a_directory_without_an_identity_is_not_reached_again_beneath_a_policy() {
|
|
1479
|
+
let mut guest = Guest::new(&[grant(
|
|
1480
|
+
Wrap::new(file_and_directory(), Arc::new(NamesNoIdentity)),
|
|
1481
|
+
Access::ReadWrite,
|
|
1482
|
+
Some(Arc::new(AllowsAll)),
|
|
1483
|
+
)]);
|
|
1484
|
+
assert_eq!(
|
|
1485
|
+
(guest.call_bare("open_dir"), guest.call_bare("stat_inner")),
|
|
1486
|
+
(0, ERRNO_NOENT),
|
|
1487
|
+
"path_filestat_get through a directory descriptor beneath a Mount with a Policy, over a \
|
|
1488
|
+
Filesystem reporting no identity, through preview1 must answer ENOENT"
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// @behavior RT-233
|
|
1493
|
+
#[test]
|
|
1494
|
+
fn a_directory_without_an_identity_still_reports_itself_beneath_a_policy() {
|
|
1495
|
+
let mut guest = Guest::new(&[grant(
|
|
1496
|
+
Wrap::new(file_and_directory(), Arc::new(NamesNoIdentity)),
|
|
1497
|
+
Access::ReadWrite,
|
|
1498
|
+
Some(Arc::new(AllowsAll)),
|
|
1499
|
+
)]);
|
|
1500
|
+
assert_eq!(
|
|
1501
|
+
(guest.call_bare("open_dir"), guest.call_bare("fstat_dir")),
|
|
1502
|
+
(0, 0),
|
|
1503
|
+
"fd_filestat_get of a directory descriptor beneath a Mount with a Policy, over a \
|
|
1504
|
+
Filesystem reporting no identity, through preview1 must succeed"
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// The table holds the three standard descriptors and the preopen alone, so a
|
|
1509
|
+
// missing path, or one no call can take, could only answer EMFILE by asking
|
|
1510
|
+
// the table first. A path of `PATH_MAX` bytes is too long, an empty one or one
|
|
1511
|
+
// holding a NUL names nothing, and one that is not UTF-8 is never decoded.
|
|
1512
|
+
// @behavior RT-190
|
|
1513
|
+
#[test]
|
|
1514
|
+
fn a_full_table_answers_emfile_whatever_the_path_names() {
|
|
1515
|
+
let mut guest = Guest::with_open_file_limit(
|
|
1516
|
+
Some(4),
|
|
1517
|
+
&[grant(file_and_directory(), Access::ReadWrite, None)],
|
|
1518
|
+
);
|
|
1519
|
+
let too_long = (0, i32::try_from(PATH_MAX).expect("PATH_MAX fits an i32"));
|
|
1520
|
+
let answers = [THROUGH_MISSING, EMPTY, HOLDING_NUL, too_long, NOT_UTF8]
|
|
1521
|
+
.map(|path| guest.call("read_file", path));
|
|
1522
|
+
assert_eq!(
|
|
1523
|
+
answers, [ERRNO_MFILE; 5],
|
|
1524
|
+
"path_open of a missing, empty, NUL-holding, too long, or non-UTF-8 path through \
|
|
1525
|
+
preview1 with no descriptor left must answer EMFILE, as Linux's FD_ADD takes one \
|
|
1526
|
+
before do_file_open answers for the name"
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// The three standard descriptors and the preopen leave room for one more,
|
|
1531
|
+
// so a directory spanning both Layers taking more than one leaves none
|
|
1532
|
+
// for the file.
|
|
1533
|
+
// @behavior RT-152
|
|
1534
|
+
#[test]
|
|
1535
|
+
fn an_overlay_directory_takes_one_descriptor_however_many_layers_it_spans() {
|
|
1536
|
+
let overlay = Overlay::new(
|
|
1537
|
+
vec![Filesystem::new(file_and_directory()).expect("the instance must stand")],
|
|
1538
|
+
Some(Filesystem::new(tmpfs_with("dir/upper", "u")).expect("the instance must stand")),
|
|
1539
|
+
)
|
|
1540
|
+
.expect("an upper over a lower Layer must make a view");
|
|
1541
|
+
let mut guest = Guest::with_open_file_limit(
|
|
1542
|
+
Some(6),
|
|
1543
|
+
&[grant(Arc::new(overlay), Access::ReadWrite, None)],
|
|
1544
|
+
);
|
|
1545
|
+
assert_eq!(
|
|
1546
|
+
(
|
|
1547
|
+
guest.call_bare("open_dir"),
|
|
1548
|
+
guest.call("read_file", FILE),
|
|
1549
|
+
guest.call("read_file", FILE)
|
|
1550
|
+
),
|
|
1551
|
+
(0, 0, ERRNO_MFILE),
|
|
1552
|
+
"path_open of a directory two Layers of an Overlay hold, then of files, beneath a Mount \
|
|
1553
|
+
through preview1 with room for two descriptors must take one descriptor for each"
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// The open looks the directory up without following a link at its end, so
|
|
1558
|
+
// the identity it remembers comes from the Overlay's lstat, and every later
|
|
1559
|
+
// call compares it with the Overlay's stat.
|
|
1560
|
+
#[test]
|
|
1561
|
+
fn a_directory_two_layers_hold_is_reached_through_its_descriptor() {
|
|
1562
|
+
let overlay = Overlay::new(
|
|
1563
|
+
vec![Filesystem::new(file_and_directory()).expect("the instance must stand")],
|
|
1564
|
+
Some(Filesystem::new(tmpfs_with("dir/upper", "u")).expect("the instance must stand")),
|
|
1565
|
+
)
|
|
1566
|
+
.expect("an upper over a lower Layer must make a view");
|
|
1567
|
+
let mut guest = Guest::new(&[grant(Arc::new(overlay), Access::ReadWrite, None)]);
|
|
1568
|
+
assert_eq!(
|
|
1569
|
+
(guest.call_bare("open_dir"), guest.call_bare("stat_inner")),
|
|
1570
|
+
(0, 0),
|
|
1571
|
+
"path_filestat_get through a directory descriptor of an Overlay whose two Layers hold the \
|
|
1572
|
+
directory, beneath a Mount through preview1, must reach it"
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// @behavior RT-118
|
|
1577
|
+
#[test]
|
|
1578
|
+
fn a_directory_moved_back_to_its_path_is_reached_through_its_descriptor_again() {
|
|
1579
|
+
let answer = stat_after(None, |root| {
|
|
1580
|
+
vfs::filename_renameat2(&prepare_creds_within(&[], None), root, "dir", root, "moved")
|
|
1581
|
+
.unwrap();
|
|
1582
|
+
vfs::filename_renameat2(&prepare_creds_within(&[], None), root, "moved", root, "dir")
|
|
1583
|
+
.unwrap();
|
|
1584
|
+
});
|
|
1585
|
+
assert_eq!(
|
|
1586
|
+
answer, 0,
|
|
1587
|
+
"path_filestat_get through a directory descriptor beneath a Mount through preview1 must \
|
|
1588
|
+
reach the directory once it moves away and back"
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// @behavior RT-080
|
|
1593
|
+
#[test]
|
|
1594
|
+
fn a_policy_refusing_an_open_answers_access_denied() {
|
|
1595
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
1596
|
+
let policy: Arc<dyn Policy> = Arc::new(Refuses("/data/file.txt"));
|
|
1597
|
+
let mut guest = Guest::new(&[grant(Arc::new(tmpfs), Access::Read, Some(policy))]);
|
|
1598
|
+
assert_eq!(
|
|
1599
|
+
guest.call("read_file", FILE),
|
|
1600
|
+
ERRNO_ACCES,
|
|
1601
|
+
"an open a Mount's Policy refuses through preview1 must answer EACCES"
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// @behavior RT-081
|
|
1606
|
+
#[test]
|
|
1607
|
+
fn a_policy_cannot_widen_a_mounts_access() {
|
|
1608
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
1609
|
+
let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
|
|
1610
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::Read, Some(policy))]);
|
|
1611
|
+
assert_eq!(
|
|
1612
|
+
(
|
|
1613
|
+
guest.call("write_file", FILE),
|
|
1614
|
+
contents(&host(&tmpfs), "file.txt")
|
|
1615
|
+
),
|
|
1616
|
+
(ERRNO_ROFS, None),
|
|
1617
|
+
"a write a Policy allows beneath a read-only Mount through preview1 must answer EROFS \
|
|
1618
|
+
and leave nothing behind"
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// @behavior RT-082
|
|
1623
|
+
#[test]
|
|
1624
|
+
fn every_invocation_reaches_the_filesystem_its_mount_holds() {
|
|
1625
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
1626
|
+
let grants = [grant(tmpfs.clone(), Access::ReadWrite, None)];
|
|
1627
|
+
assert_eq!(
|
|
1628
|
+
Guest::new(&grants).call("write_file", FILE),
|
|
1629
|
+
0,
|
|
1630
|
+
"the write must succeed"
|
|
1631
|
+
);
|
|
1632
|
+
assert_eq!(
|
|
1633
|
+
Guest::new(&grants).call("read_file", FILE),
|
|
1634
|
+
0,
|
|
1635
|
+
"a file an earlier invocation wrote beneath a Mount over a Tmpfs must be found by the \
|
|
1636
|
+
next invocation through preview1"
|
|
1637
|
+
);
|
|
1638
|
+
assert_eq!(
|
|
1639
|
+
contents(&host(&tmpfs), "file.txt").as_deref(),
|
|
1640
|
+
Some(&b"hello"[..]),
|
|
1641
|
+
"the file the next invocation finds must hold what the earlier invocation wrote"
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/// A Tmpfs holding `file.txt` that counts every change it is asked for,
|
|
1646
|
+
/// so a test can tell the runtime refused before asking it.
|
|
1647
|
+
#[derive(Default)]
|
|
1648
|
+
struct ChangeCounter {
|
|
1649
|
+
changes: AtomicUsize,
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
impl ChangeCounter {
|
|
1653
|
+
/// A counter, and a Tmpfs holding `file.txt` it counts the changes of.
|
|
1654
|
+
fn over_file() -> (Arc<Self>, Arc<Wrap<Self>>) {
|
|
1655
|
+
let counter = Arc::new(Self::default());
|
|
1656
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
1657
|
+
(counter.clone(), Wrap::new(Arc::new(tmpfs), counter))
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
fn change(&self) {
|
|
1661
|
+
self.changes.fetch_add(1, Ordering::Relaxed);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
impl Hook for ChangeCounter {
|
|
1666
|
+
fn mkdir(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
1667
|
+
self.change();
|
|
1668
|
+
inner.mkdir(dentry)
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
fn rmdir(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
1672
|
+
self.change();
|
|
1673
|
+
inner.rmdir(dentry)
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
fn unlink(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
1677
|
+
self.change();
|
|
1678
|
+
inner.unlink(dentry)
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
fn rename(
|
|
1682
|
+
&self,
|
|
1683
|
+
inner: &dyn InodeOperations,
|
|
1684
|
+
from: &Dentry,
|
|
1685
|
+
to_dir: &dyn InodeOperations,
|
|
1686
|
+
to: &Dentry,
|
|
1687
|
+
flag: Option<RenameFlag>,
|
|
1688
|
+
) -> Result<(), FilesystemError> {
|
|
1689
|
+
self.change();
|
|
1690
|
+
inner.rename(from, to_dir, to, flag)
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// The Policy allows everything, so only the rename's own refusal of a
|
|
1695
|
+
// directory keeps `secret` from taking a path the Policy never judged.
|
|
1696
|
+
// @behavior RT-094
|
|
1697
|
+
#[test]
|
|
1698
|
+
fn a_directory_beneath_a_mount_with_a_policy_does_not_move() {
|
|
1699
|
+
let tmpfs = Arc::new([("dir/secret", "x")].into_iter().collect::<Tmpfs>());
|
|
1700
|
+
let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
|
|
1701
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, Some(policy))]);
|
|
1702
|
+
assert_eq!(
|
|
1703
|
+
(
|
|
1704
|
+
guest.call_pair("rename_paths", DIR, MOVED),
|
|
1705
|
+
vfs::vfs_fstatat(
|
|
1706
|
+
&prepare_creds_within(&[], None),
|
|
1707
|
+
&host(&tmpfs),
|
|
1708
|
+
"dir/secret",
|
|
1709
|
+
false
|
|
1710
|
+
)
|
|
1711
|
+
.map(|m| m.file_type)
|
|
1712
|
+
),
|
|
1713
|
+
(ERRNO_XDEV, Ok(FileType::RegFile)),
|
|
1714
|
+
"path_rename of a directory beneath a Mount with a Policy through preview1 must \
|
|
1715
|
+
answer EXDEV and move nothing"
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/// A Tmpfs whose files take another writer's `bbb` at their end as one
|
|
1720
|
+
/// is written, once the length the write would land at is known and
|
|
1721
|
+
/// before it lands, as another invocation sharing the file could.
|
|
1722
|
+
struct AppendedMeanwhile;
|
|
1723
|
+
|
|
1724
|
+
struct AppendedMeanwhileFile {
|
|
1725
|
+
file: Box<dyn FileOperations>,
|
|
1726
|
+
other: std::sync::Mutex<Box<dyn FileOperations>>,
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
impl AppendedMeanwhileFile {
|
|
1730
|
+
fn other_appends(&self) -> Result<(), FilesystemError> {
|
|
1731
|
+
self.other
|
|
1732
|
+
.lock()
|
|
1733
|
+
.expect("the other writer's lock must not be poisoned")
|
|
1734
|
+
.write_iter(&mut Kiocb::at(0).with_append(), b"bbb")
|
|
1735
|
+
.map(drop)
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
impl FileOperations for AppendedMeanwhileFile {
|
|
1740
|
+
fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
1741
|
+
self.file.read_iter(iocb, buf)
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
1745
|
+
if iocb.append {
|
|
1746
|
+
self.other_appends()?;
|
|
1747
|
+
}
|
|
1748
|
+
self.file.write_iter(iocb, buf)
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
impl Hook for AppendedMeanwhile {
|
|
1753
|
+
fn open(
|
|
1754
|
+
&self,
|
|
1755
|
+
inner: &dyn InodeOperations,
|
|
1756
|
+
dentry: &Dentry,
|
|
1757
|
+
how: &OpenHow,
|
|
1758
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
1759
|
+
let writes = build_how(|how| how.write = true);
|
|
1760
|
+
let file = inner.open(dentry, how)?;
|
|
1761
|
+
let other = inner.open(dentry, &writes)?;
|
|
1762
|
+
Ok(Box::new(AppendedMeanwhileFile {
|
|
1763
|
+
file,
|
|
1764
|
+
other: std::sync::Mutex::new(other),
|
|
1765
|
+
}))
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// The other writer's `bbb` lands as the guest's append is made, so only
|
|
1770
|
+
// an append the file places itself keeps from writing over it.
|
|
1771
|
+
// @behavior RT-095
|
|
1772
|
+
#[test]
|
|
1773
|
+
fn an_append_lands_after_another_writers_append() {
|
|
1774
|
+
let tmpfs = tmpfs_with("file.txt", "granted");
|
|
1775
|
+
let shared = Wrap::new(tmpfs.clone(), Arc::new(AppendedMeanwhile));
|
|
1776
|
+
let mut guest = Guest::new(&[grant(shared, Access::ReadWrite, None)]);
|
|
1777
|
+
assert_eq!(
|
|
1778
|
+
(
|
|
1779
|
+
guest.call("append_file", FILE),
|
|
1780
|
+
contents(&host(&tmpfs), "file.txt")
|
|
1781
|
+
),
|
|
1782
|
+
(0, Some(b"grantedbbbhello".to_vec())),
|
|
1783
|
+
"fd_write to a file opened to append beneath a Mount through preview1 must land after \
|
|
1784
|
+
what another writer appended meanwhile"
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
#[cfg(unix)]
|
|
1789
|
+
const FILETYPE_UNKNOWN: u8 = 0;
|
|
1790
|
+
#[cfg(unix)]
|
|
1791
|
+
const FILETYPE_DIRECTORY: u8 = 3;
|
|
1792
|
+
#[cfg(unix)]
|
|
1793
|
+
const FILETYPE_REGULAR_FILE: u8 = 4;
|
|
1794
|
+
#[cfg(unix)]
|
|
1795
|
+
const FILETYPE_SYMBOLIC_LINK: u8 = 7;
|
|
1796
|
+
|
|
1797
|
+
/// A host directory of its own holding `file.txt` as a link to `target`,
|
|
1798
|
+
/// made by `make_target`, and a guest handed it as a read-only Bind
|
|
1799
|
+
/// Mount.
|
|
1800
|
+
#[cfg(unix)]
|
|
1801
|
+
fn mount_link(name: &str, make_target: fn(&std::path::Path)) -> (std::path::PathBuf, Guest) {
|
|
1802
|
+
mount_link_under(name, make_target, Access::Read, None)
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/// As `linked`, the Mount taking `access` and `policy`.
|
|
1806
|
+
#[cfg(unix)]
|
|
1807
|
+
fn mount_link_under(
|
|
1808
|
+
name: &str,
|
|
1809
|
+
make_target: fn(&std::path::Path),
|
|
1810
|
+
access: Access,
|
|
1811
|
+
policy: Option<Arc<dyn Policy>>,
|
|
1812
|
+
) -> (std::path::PathBuf, Guest) {
|
|
1813
|
+
let host = std::env::temp_dir().join(format!("kobako-granted-{name}-{}", std::process::id()));
|
|
1814
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
1815
|
+
std::fs::create_dir_all(&host).expect("the host directory must be created");
|
|
1816
|
+
make_target(&host.join("target"));
|
|
1817
|
+
std::os::unix::fs::symlink("target", host.join("file.txt")).expect("the link must be made");
|
|
1818
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
1819
|
+
let guest = Guest::new(&[grant(Arc::new(directory), access, policy)]);
|
|
1820
|
+
(host, guest)
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/// What each stat of `path` with the given lookupflags answers, and the
|
|
1824
|
+
/// filetype it wrote, beneath a Bind Mount whose host directory
|
|
1825
|
+
/// holds `file.txt` as a link to `target`, made by `make_target`.
|
|
1826
|
+
#[cfg(unix)]
|
|
1827
|
+
fn stat_link(
|
|
1828
|
+
name: &str,
|
|
1829
|
+
make_target: fn(&std::path::Path),
|
|
1830
|
+
stats: &[((i32, i32), i32)],
|
|
1831
|
+
) -> Vec<(i32, u8)> {
|
|
1832
|
+
let (host, mut guest) = mount_link(name, make_target);
|
|
1833
|
+
let answers = stats
|
|
1834
|
+
.iter()
|
|
1835
|
+
.map(|&(path, flags)| {
|
|
1836
|
+
let errno = guest.stat_flagged(path, flags);
|
|
1837
|
+
let memory = guest
|
|
1838
|
+
.instance
|
|
1839
|
+
.get_memory(&mut guest.store, "memory")
|
|
1840
|
+
.expect("probe exports its memory");
|
|
1841
|
+
(errno, memory.data(&guest.store)[384 + 16])
|
|
1842
|
+
})
|
|
1843
|
+
.collect();
|
|
1844
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
1845
|
+
answers
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// A FIFO with no writer would hold an open that waits for one, so only
|
|
1849
|
+
// an open refused before it waits returns at all.
|
|
1850
|
+
// @behavior RT-127
|
|
1851
|
+
#[cfg(unix)]
|
|
1852
|
+
#[test]
|
|
1853
|
+
fn a_guest_sees_a_fifo_as_of_an_unknown_type_and_cannot_open_it() {
|
|
1854
|
+
let host = std::env::temp_dir().join(format!("kobako-granted-fifo-{}", std::process::id()));
|
|
1855
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
1856
|
+
std::fs::create_dir_all(&host).expect("the host directory must be created");
|
|
1857
|
+
let made = std::process::Command::new("mkfifo")
|
|
1858
|
+
.arg(host.join("file.txt"))
|
|
1859
|
+
.status()
|
|
1860
|
+
.expect("mkfifo must run");
|
|
1861
|
+
assert!(made.success(), "the FIFO must be made");
|
|
1862
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
1863
|
+
let mut guest = Guest::new(&[grant(Arc::new(directory), Access::Read, None)]);
|
|
1864
|
+
let stat = guest.stat_flagged(FILE, 0);
|
|
1865
|
+
let filestat = guest
|
|
1866
|
+
.instance
|
|
1867
|
+
.get_memory(&mut guest.store, "memory")
|
|
1868
|
+
.expect("probe exports its memory")
|
|
1869
|
+
.data(&guest.store)[384..384 + 32]
|
|
1870
|
+
.to_vec();
|
|
1871
|
+
let nlink = u64::from_le_bytes(filestat[24..32].try_into().expect("nlink is 8 bytes"));
|
|
1872
|
+
let opened = guest.call("read_file", FILE);
|
|
1873
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
1874
|
+
assert_eq!(
|
|
1875
|
+
(stat, filestat[16], nlink, opened),
|
|
1876
|
+
(0, FILETYPE_UNKNOWN, 1, ERRNO_ACCES),
|
|
1877
|
+
"path_filestat_get of a FIFO beneath a Bind Mount through preview1 must report an \
|
|
1878
|
+
unknown filetype named once, and path_open of it must answer EACCES"
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// Bound and never accepted, so an open reaching it would answer ENXIO rather
|
|
1883
|
+
// than the refusal asserted here.
|
|
1884
|
+
// @behavior RT-196
|
|
1885
|
+
#[cfg(unix)]
|
|
1886
|
+
#[test]
|
|
1887
|
+
fn a_guest_sees_a_socket_as_one_and_cannot_open_it() {
|
|
1888
|
+
let host = std::env::temp_dir().join(format!("kobako-granted-sock-{}", std::process::id()));
|
|
1889
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
1890
|
+
std::fs::create_dir_all(&host).expect("the host directory must be created");
|
|
1891
|
+
let _socket = std::os::unix::net::UnixListener::bind(host.join("file.txt"))
|
|
1892
|
+
.expect("the socket must be bound");
|
|
1893
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
1894
|
+
let mut guest = Guest::new(&[grant(Arc::new(directory), Access::Read, None)]);
|
|
1895
|
+
let stat = guest.stat_flagged(FILE, 0);
|
|
1896
|
+
let filetype = guest
|
|
1897
|
+
.instance
|
|
1898
|
+
.get_memory(&mut guest.store, "memory")
|
|
1899
|
+
.expect("probe exports its memory")
|
|
1900
|
+
.data(&guest.store)[384 + 16];
|
|
1901
|
+
let opened = guest.call("read_file", FILE);
|
|
1902
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
1903
|
+
assert_eq!(
|
|
1904
|
+
(stat, filetype, opened),
|
|
1905
|
+
(0, 6, ERRNO_ACCES),
|
|
1906
|
+
"path_filestat_get of a socket beneath a Bind Mount through preview1 must report a \
|
|
1907
|
+
stream socket, and path_open of it must answer EACCES"
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// The link points at a regular file, so only a stat that follows none
|
|
1912
|
+
// can answer a link.
|
|
1913
|
+
// @behavior RT-097
|
|
1914
|
+
#[cfg(unix)]
|
|
1915
|
+
#[test]
|
|
1916
|
+
fn a_stat_following_no_link_reports_the_link_itself() {
|
|
1917
|
+
let answers = stat_link(
|
|
1918
|
+
"lstat",
|
|
1919
|
+
|target| std::fs::write(target, "granted").expect("the host file must be written"),
|
|
1920
|
+
&[(FILE, 1), (FILE, 0)],
|
|
1921
|
+
);
|
|
1922
|
+
assert_eq!(
|
|
1923
|
+
answers,
|
|
1924
|
+
[(0, FILETYPE_REGULAR_FILE), (0, FILETYPE_SYMBOLIC_LINK)],
|
|
1925
|
+
"path_filestat_get of a host link beneath a Bind Mount through preview1 must report \
|
|
1926
|
+
the file it points to when following links, and a symbolic link when following none"
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// A host link reports the length of where it points, so only what the runtime
|
|
1931
|
+
// shows a guest keeps that length from it.
|
|
1932
|
+
// @behavior RT-173
|
|
1933
|
+
#[cfg(unix)]
|
|
1934
|
+
#[test]
|
|
1935
|
+
fn a_link_reports_no_size_however_its_filesystem_sizes_it() {
|
|
1936
|
+
let (host, mut guest) = mount_link("link-size", |target| {
|
|
1937
|
+
std::fs::write(target, "granted").expect("the host file must be written")
|
|
1938
|
+
});
|
|
1939
|
+
let answered = guest.stat_flagged(FILE, 0);
|
|
1940
|
+
let size = guest.stat_field(32);
|
|
1941
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
1942
|
+
assert_eq!(
|
|
1943
|
+
(answered, size),
|
|
1944
|
+
(0, 0),
|
|
1945
|
+
"path_filestat_get following no link of a host link beneath a Bind Mount through \
|
|
1946
|
+
preview1 must report a size of zero"
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/// One Tmpfs read-write, as an invocation on each posture is handed it.
|
|
1951
|
+
fn tmpfs_grant(tmpfs: &Arc<Tmpfs>) -> [Grant; 1] {
|
|
1952
|
+
[grant(tmpfs.clone(), Access::ReadWrite, None)]
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// @behavior RT-156
|
|
1956
|
+
#[test]
|
|
1957
|
+
fn the_weaker_posture_sees_the_times_a_filesystem_keeps() {
|
|
1958
|
+
let mut guest = Guest::for_posture(Profile::Permissive, &tmpfs_grant(&Arc::new(Tmpfs::new())));
|
|
1959
|
+
let before = SystemTime::now()
|
|
1960
|
+
.duration_since(UNIX_EPOCH)
|
|
1961
|
+
.expect("the host clock stands past the epoch")
|
|
1962
|
+
.as_nanos() as u64;
|
|
1963
|
+
assert_eq!(guest.call("write_file", FILE), 0, "the write must succeed");
|
|
1964
|
+
assert!(
|
|
1965
|
+
guest.times_of_path(FILE)[1] >= before,
|
|
1966
|
+
"path_filestat_get of a file written beneath a Mount through preview1 on the weaker \
|
|
1967
|
+
posture must report a modification time no earlier than the write"
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// A Tmpfs records the host's real time, so only the posture's view turns it
|
|
1972
|
+
// into the epoch.
|
|
1973
|
+
// @behavior RT-157
|
|
1974
|
+
#[test]
|
|
1975
|
+
fn the_strongest_posture_sees_every_time_at_the_unix_epoch() {
|
|
1976
|
+
let mut guest = Guest::for_posture(Profile::Hermetic, &tmpfs_grant(&Arc::new(Tmpfs::new())));
|
|
1977
|
+
assert_eq!(guest.call("write_file", FILE), 0, "the write must succeed");
|
|
1978
|
+
let path = guest.times_of_path(FILE);
|
|
1979
|
+
assert_eq!(guest.call("stat_opened", FILE), 0, "the stat must succeed");
|
|
1980
|
+
assert_eq!(
|
|
1981
|
+
(path, guest.stat_times()),
|
|
1982
|
+
([0; 3], [0; 3]),
|
|
1983
|
+
"path_filestat_get and fd_filestat_get of a written file through preview1 on the \
|
|
1984
|
+
strongest posture must report each time as the Unix epoch"
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// @behavior RT-158
|
|
1989
|
+
#[test]
|
|
1990
|
+
fn a_time_another_posture_wrote_reaches_the_strongest_posture_as_the_epoch() {
|
|
1991
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
1992
|
+
let mut writer = Guest::for_posture(Profile::Permissive, &tmpfs_grant(&tmpfs));
|
|
1993
|
+
assert_eq!(writer.call("write_file", FILE), 0, "the write must succeed");
|
|
1994
|
+
let mut reader = Guest::for_posture(Profile::Hermetic, &tmpfs_grant(&tmpfs));
|
|
1995
|
+
assert_eq!(
|
|
1996
|
+
reader.times_of_path(FILE),
|
|
1997
|
+
[0; 3],
|
|
1998
|
+
"path_filestat_get through preview1 on the strongest posture, of a file the weaker \
|
|
1999
|
+
posture wrote into the same Tmpfs, must report each time as the Unix epoch"
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// The stat follows no link, so only the trailing `/` can make it reach
|
|
2004
|
+
// the directory the link points at, as Linux's `lstat` does.
|
|
2005
|
+
// @behavior RT-098
|
|
2006
|
+
#[cfg(unix)]
|
|
2007
|
+
#[test]
|
|
2008
|
+
fn a_stat_of_a_link_named_with_a_trailing_slash_reports_its_directory() {
|
|
2009
|
+
let answers = stat_link(
|
|
2010
|
+
"lstat-slash",
|
|
2011
|
+
|target| std::fs::create_dir(target).expect("the host directory must be created"),
|
|
2012
|
+
&[(FILE_AS_DIRECTORY, 0), (FILE, 0)],
|
|
2013
|
+
);
|
|
2014
|
+
assert_eq!(
|
|
2015
|
+
answers,
|
|
2016
|
+
[(0, FILETYPE_DIRECTORY), (0, FILETYPE_SYMBOLIC_LINK)],
|
|
2017
|
+
"path_filestat_get following no link of a host link to a directory, named with a \
|
|
2018
|
+
trailing slash, beneath a Bind Mount through preview1 must report the directory, \
|
|
2019
|
+
and without the slash a symbolic link"
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
/// What `path_unlink_file` and `path_rename` answer for the link
|
|
2024
|
+
/// `file.txt/` to a directory beneath a read-write Bind Mount taking
|
|
2025
|
+
/// `policy`, and whether the link stays; `name` keeps its host directory
|
|
2026
|
+
/// its own.
|
|
2027
|
+
#[cfg(unix)]
|
|
2028
|
+
fn change_link_named_as_directory(name: &str, policy: Option<Arc<dyn Policy>>) -> ([i32; 2], bool) {
|
|
2029
|
+
let (host, mut guest) = mount_link_under(name, directory_with_key, Access::ReadWrite, policy);
|
|
2030
|
+
let answers = [
|
|
2031
|
+
guest.call("unlink_path", FILE_AS_DIRECTORY),
|
|
2032
|
+
guest.call_pair("rename_paths", FILE_AS_DIRECTORY, MOVED),
|
|
2033
|
+
];
|
|
2034
|
+
let kept = host.join("file.txt").symlink_metadata().is_ok();
|
|
2035
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2036
|
+
(answers, kept)
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// Linux's `unlink` and `rename` both act on the link itself, which is no
|
|
2040
|
+
// directory, so a Policy, refusing only a link a call follows, changes
|
|
2041
|
+
// neither answer.
|
|
2042
|
+
// @behavior RT-123
|
|
2043
|
+
#[cfg(unix)]
|
|
2044
|
+
#[test]
|
|
2045
|
+
fn a_link_named_with_a_trailing_slash_is_neither_removed_nor_moved() {
|
|
2046
|
+
assert_eq!(
|
|
2047
|
+
[
|
|
2048
|
+
change_link_named_as_directory("link-slash", None),
|
|
2049
|
+
change_link_named_as_directory("policy-link-slash", Some(Arc::new(AllowsAll))),
|
|
2050
|
+
],
|
|
2051
|
+
[([ERRNO_NOTDIR, ERRNO_NOTDIR], true); 2],
|
|
2052
|
+
"path_unlink_file and path_rename of a host link to a directory named with a trailing \
|
|
2053
|
+
slash, beneath a read-write Bind Mount with or without a Policy through preview1, \
|
|
2054
|
+
must answer ENOTDIR as Linux does and leave the link"
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// The link points at nothing, so only an unlink that follows no link
|
|
2059
|
+
// finds it there to answer ENOTDIR rather than ENOENT.
|
|
2060
|
+
// @behavior RT-134
|
|
2061
|
+
#[cfg(unix)]
|
|
2062
|
+
#[test]
|
|
2063
|
+
fn an_unlink_with_a_trailing_slash_answers_for_the_name_itself() {
|
|
2064
|
+
let host = std::env::temp_dir().join(format!(
|
|
2065
|
+
"kobako-granted-unlink-slash-{}",
|
|
2066
|
+
std::process::id()
|
|
2067
|
+
));
|
|
2068
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2069
|
+
std::fs::create_dir_all(host.join("dir")).expect("the host directory must be created");
|
|
2070
|
+
std::fs::write(host.join("file.txt"), "granted").expect("the host file must be written");
|
|
2071
|
+
std::os::unix::fs::symlink("nowhere", host.join("gone")).expect("the link must be made");
|
|
2072
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
2073
|
+
let mut guest = Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, None)]);
|
|
2074
|
+
let answers = [
|
|
2075
|
+
DIR_AS_DIRECTORY,
|
|
2076
|
+
FILE_AS_DIRECTORY,
|
|
2077
|
+
GONE_AS_DIRECTORY,
|
|
2078
|
+
NEW_AS_DIRECTORY,
|
|
2079
|
+
]
|
|
2080
|
+
.map(|path| guest.call("unlink_path", path));
|
|
2081
|
+
let kept = ["dir", "file.txt", "gone"].map(|name| host.join(name).symlink_metadata().is_ok());
|
|
2082
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2083
|
+
assert_eq!(
|
|
2084
|
+
(answers, kept),
|
|
2085
|
+
(
|
|
2086
|
+
[ERRNO_ISDIR, ERRNO_NOTDIR, ERRNO_NOTDIR, ERRNO_NOENT],
|
|
2087
|
+
[true; 3]
|
|
2088
|
+
),
|
|
2089
|
+
"path_unlink_file of a directory, a file, a link to nothing, and a missing name, each \
|
|
2090
|
+
named with a trailing slash beneath a read-write Bind Mount through preview1, must \
|
|
2091
|
+
answer EISDIR, ENOTDIR, ENOTDIR, and ENOENT as Linux does and remove nothing"
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// @behavior RT-188
|
|
2096
|
+
#[test]
|
|
2097
|
+
fn a_name_no_filesystem_makes_is_answered_by_what_stands() {
|
|
2098
|
+
let tmpfs = tmpfs_with("file.txt", "granted");
|
|
2099
|
+
let mount = Mount::new(
|
|
2100
|
+
"/data",
|
|
2101
|
+
Filesystem::new(tmpfs.clone()).expect("the instance must stand"),
|
|
2102
|
+
Access::ReadWrite,
|
|
2103
|
+
)
|
|
2104
|
+
.expect("/data is a guest path");
|
|
2105
|
+
let root = vfs::Path::root(mount.mnt());
|
|
2106
|
+
let cred = prepare_creds_within(&[], None);
|
|
2107
|
+
let link = |old, new| vfs::filename_linkat(&cred, &root, old, &root, new, false);
|
|
2108
|
+
let symlink = |new| vfs::filename_symlinkat(&cred, "target", &root, new);
|
|
2109
|
+
let refused = Err;
|
|
2110
|
+
assert_eq!(
|
|
2111
|
+
(
|
|
2112
|
+
link("missing", "new"),
|
|
2113
|
+
link("file.txt", "file.txt"),
|
|
2114
|
+
link("file.txt", "new"),
|
|
2115
|
+
symlink("file.txt"),
|
|
2116
|
+
symlink("new"),
|
|
2117
|
+
),
|
|
2118
|
+
(
|
|
2119
|
+
refused(FilesystemError::NotFound),
|
|
2120
|
+
refused(FilesystemError::AlreadyExists),
|
|
2121
|
+
refused(FilesystemError::NotPermitted),
|
|
2122
|
+
refused(FilesystemError::AlreadyExists),
|
|
2123
|
+
refused(FilesystemError::NotPermitted),
|
|
2124
|
+
),
|
|
2125
|
+
"vfs::filename_linkat and filename_symlinkat over a Filesystem making no links must \
|
|
2126
|
+
answer ENOENT for a missing source, EEXIST for a name that stands, and otherwise EPERM"
|
|
2127
|
+
);
|
|
2128
|
+
assert_eq!(
|
|
2129
|
+
host_entries(&tmpfs).expect("the root must list"),
|
|
2130
|
+
vec!["file.txt".to_string()],
|
|
2131
|
+
"a refused link must make nothing"
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// Only Linux grows a file in one step, so a Bind elsewhere refuses and leaves it.
|
|
2136
|
+
// @behavior RT-099
|
|
2137
|
+
// @behavior RT-239
|
|
2138
|
+
#[test]
|
|
2139
|
+
fn an_allocation_grows_a_file_to_where_its_room_ends_and_shortens_none() {
|
|
2140
|
+
// What a file holds from where the room might start, and how long it is.
|
|
2141
|
+
let read = |bytes: Vec<u8>| (bytes.len(), bytes.get(7).copied());
|
|
2142
|
+
let long = "x".repeat(200);
|
|
2143
|
+
let through_tmpfs = |seed: &str| {
|
|
2144
|
+
let tmpfs = tmpfs_with("file.txt", seed);
|
|
2145
|
+
let answer = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)])
|
|
2146
|
+
.call("allocate_file", FILE);
|
|
2147
|
+
let len = vfs::vfs_fstatat(
|
|
2148
|
+
&prepare_creds_within(&[], None),
|
|
2149
|
+
&host(&tmpfs),
|
|
2150
|
+
"file.txt",
|
|
2151
|
+
false,
|
|
2152
|
+
)
|
|
2153
|
+
.map(|stat| stat.size);
|
|
2154
|
+
let held = contents(&host(&tmpfs), "file.txt").map(|head| head[7]);
|
|
2155
|
+
(answer, len.ok().map(|len| len as usize), held)
|
|
2156
|
+
};
|
|
2157
|
+
let through_bind = |seed: &str| {
|
|
2158
|
+
let dir = std::env::temp_dir().join(format!("kobako-allocate-{}", std::process::id()));
|
|
2159
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
2160
|
+
std::fs::create_dir_all(&dir).expect("the host directory must be created");
|
|
2161
|
+
std::fs::write(dir.join("file.txt"), seed).expect("the host file must be written");
|
|
2162
|
+
let bind = Bind::new(&dir).expect("the host directory must open");
|
|
2163
|
+
let answer = Guest::new(&[grant(Arc::new(bind), Access::ReadWrite, None)])
|
|
2164
|
+
.call("allocate_file", FILE);
|
|
2165
|
+
let (len, held) = read(std::fs::read(dir.join("file.txt")).expect("the file stands"));
|
|
2166
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
2167
|
+
(answer, Some(len), held)
|
|
2168
|
+
};
|
|
2169
|
+
let bind_answers = match cfg!(target_os = "linux") {
|
|
2170
|
+
true => [(0, Some(100), Some(0)), (0, Some(200), Some(b'x'))],
|
|
2171
|
+
false => [
|
|
2172
|
+
(ERRNO_NOTSUP, Some(7), None),
|
|
2173
|
+
(ERRNO_NOTSUP, Some(200), Some(b'x')),
|
|
2174
|
+
],
|
|
2175
|
+
};
|
|
2176
|
+
assert_eq!(
|
|
2177
|
+
(
|
|
2178
|
+
[through_tmpfs("granted"), through_tmpfs(&long)],
|
|
2179
|
+
[through_bind("granted"), through_bind(&long)],
|
|
2180
|
+
),
|
|
2181
|
+
(
|
|
2182
|
+
[(0, Some(100), Some(0)), (0, Some(200), Some(b'x'))],
|
|
2183
|
+
bind_answers,
|
|
2184
|
+
),
|
|
2185
|
+
"fd_allocate of 100 bytes from the start through preview1, over a Tmpfs and a Bind on \
|
|
2186
|
+
Linux, must grow a shorter file with zeros and leave a longer one as it was, and over a \
|
|
2187
|
+
Bind elsewhere answer ENOTSUP and leave both"
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// Linux counts room reserved in `i_blocks` against the tmpfs's size, and a
|
|
2192
|
+
// Tmpfs holds every byte up to a file's end, so its budget refuses the growth.
|
|
2193
|
+
// @behavior RT-238
|
|
2194
|
+
#[test]
|
|
2195
|
+
fn an_allocation_past_a_tmpfs_size_answers_enospc() {
|
|
2196
|
+
let tmpfs: Tmpfs = [("file.txt", "x")].into_iter().collect();
|
|
2197
|
+
let answer = Guest::new(&[grant(
|
|
2198
|
+
Arc::new(tmpfs.with_size(Some(50))),
|
|
2199
|
+
Access::ReadWrite,
|
|
2200
|
+
None,
|
|
2201
|
+
)])
|
|
2202
|
+
.call("allocate_file", FILE);
|
|
2203
|
+
assert_eq!(
|
|
2204
|
+
answer, ERRNO_NOSPC,
|
|
2205
|
+
"fd_allocate of 100 bytes in a Tmpfs of 50 through preview1 must answer ENOSPC"
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// The probe's opens follow no link, so only the trailing `/` can make one
|
|
2210
|
+
// reach the directory the link points at, as Linux's `open` does.
|
|
2211
|
+
// @behavior RT-100
|
|
2212
|
+
#[cfg(unix)]
|
|
2213
|
+
#[test]
|
|
2214
|
+
fn an_open_following_no_link_refuses_a_link_to_a_directory() {
|
|
2215
|
+
let (host, mut guest) = mount_link("open-nofollow", |target| {
|
|
2216
|
+
std::fs::create_dir(target).expect("the host directory must be created")
|
|
2217
|
+
});
|
|
2218
|
+
let answers = [
|
|
2219
|
+
guest.call_flagged(FILE, 0, 0),
|
|
2220
|
+
guest.call_flagged(FILE_AS_DIRECTORY, 0, 0),
|
|
2221
|
+
];
|
|
2222
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2223
|
+
assert_eq!(
|
|
2224
|
+
answers,
|
|
2225
|
+
[ERRNO_LOOP, 0],
|
|
2226
|
+
"path_open following no link of a host link to a directory beneath a Bind Mount \
|
|
2227
|
+
through preview1 must answer ELOOP, and with a trailing slash open the directory"
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
/// A host directory linked in as `file.txt`, holding `key`.
|
|
2232
|
+
#[cfg(unix)]
|
|
2233
|
+
fn directory_with_key(target: &std::path::Path) {
|
|
2234
|
+
std::fs::create_dir(target).expect("the host directory must be created");
|
|
2235
|
+
std::fs::write(target.join("key"), "secret").expect("the host file must be written");
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// The link itself is allowed; only judging where it leads refuses the key, so
|
|
2239
|
+
// a walk that took the link's own path for the answer would hand it over.
|
|
2240
|
+
// @behavior RT-101
|
|
2241
|
+
#[cfg(unix)]
|
|
2242
|
+
#[test]
|
|
2243
|
+
fn a_path_through_a_link_beneath_a_mount_with_a_policy_is_judged_where_it_leads() {
|
|
2244
|
+
let refuses_key: Arc<dyn Policy> = Arc::new(Refuses("/data/target/key"));
|
|
2245
|
+
let (host, mut guest) = mount_link_under(
|
|
2246
|
+
"policy-link-refused",
|
|
2247
|
+
directory_with_key,
|
|
2248
|
+
Access::ReadWrite,
|
|
2249
|
+
Some(refuses_key),
|
|
2250
|
+
);
|
|
2251
|
+
let refused = [
|
|
2252
|
+
guest.call("read_file", THROUGH_FILE),
|
|
2253
|
+
guest.call("unlink_path", THROUGH_FILE),
|
|
2254
|
+
];
|
|
2255
|
+
let kept = host.join("target/key").exists();
|
|
2256
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2257
|
+
let allows: Arc<dyn Policy> = Arc::new(AllowsAll);
|
|
2258
|
+
let (host, mut guest) = mount_link_under(
|
|
2259
|
+
"policy-link-allowed",
|
|
2260
|
+
directory_with_key,
|
|
2261
|
+
Access::ReadWrite,
|
|
2262
|
+
Some(allows),
|
|
2263
|
+
);
|
|
2264
|
+
let followed = [
|
|
2265
|
+
guest.call("read_file", THROUGH_FILE),
|
|
2266
|
+
guest.call("open_following", FILE),
|
|
2267
|
+
];
|
|
2268
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2269
|
+
assert_eq!(
|
|
2270
|
+
(refused, kept, followed),
|
|
2271
|
+
([ERRNO_ACCES; 2], true, [0, 0]),
|
|
2272
|
+
"path_open and path_unlink_file through a host link beneath a Mount with a Policy \
|
|
2273
|
+
through preview1 must answer what the Policy says of where the link leads, EACCES \
|
|
2274
|
+
for a refused path, changing nothing, and follow it to an allowed one"
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// A Policy changes nothing of where a link may lead: one out of the Bind escapes
|
|
2279
|
+
// it and one into itself loops, as beneath a Mount without a Policy, and an unlink
|
|
2280
|
+
// acts on the link itself.
|
|
2281
|
+
// @behavior RT-125
|
|
2282
|
+
#[cfg(unix)]
|
|
2283
|
+
#[test]
|
|
2284
|
+
fn a_link_out_of_a_directory_or_into_a_loop_answers_as_without_a_policy() {
|
|
2285
|
+
let outside = std::env::temp_dir().join(format!("kobako-outside-{}", std::process::id()));
|
|
2286
|
+
directory_with_key(&outside);
|
|
2287
|
+
let targets = [
|
|
2288
|
+
outside.to_string_lossy().into_owned(),
|
|
2289
|
+
format!("../{}", outside.file_name().unwrap().to_string_lossy()),
|
|
2290
|
+
"file.txt".to_string(),
|
|
2291
|
+
];
|
|
2292
|
+
let answers: Vec<_> = targets
|
|
2293
|
+
.iter()
|
|
2294
|
+
.enumerate()
|
|
2295
|
+
.map(|(index, target)| {
|
|
2296
|
+
let host = std::env::temp_dir().join(format!(
|
|
2297
|
+
"kobako-granted-escaping-{index}-{}",
|
|
2298
|
+
std::process::id()
|
|
2299
|
+
));
|
|
2300
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2301
|
+
std::fs::create_dir_all(&host).expect("the host directory must be created");
|
|
2302
|
+
std::os::unix::fs::symlink(target, host.join("file.txt"))
|
|
2303
|
+
.expect("the link must be made");
|
|
2304
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
2305
|
+
let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
|
|
2306
|
+
let mut guest =
|
|
2307
|
+
Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, Some(policy))]);
|
|
2308
|
+
let answers = [
|
|
2309
|
+
guest.call("read_file", THROUGH_FILE),
|
|
2310
|
+
guest.call("unlink_path", FILE),
|
|
2311
|
+
];
|
|
2312
|
+
let removed = host.join("file.txt").symlink_metadata().is_err();
|
|
2313
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2314
|
+
(answers, removed)
|
|
2315
|
+
})
|
|
2316
|
+
.collect();
|
|
2317
|
+
let kept = outside.join("key").exists();
|
|
2318
|
+
let _ = std::fs::remove_dir_all(&outside);
|
|
2319
|
+
assert_eq!(
|
|
2320
|
+
(answers, kept),
|
|
2321
|
+
(
|
|
2322
|
+
vec![
|
|
2323
|
+
([ERRNO_NOTCAPABLE, 0], true),
|
|
2324
|
+
([ERRNO_NOTCAPABLE, 0], true),
|
|
2325
|
+
([ERRNO_LOOP, 0], true),
|
|
2326
|
+
],
|
|
2327
|
+
true
|
|
2328
|
+
),
|
|
2329
|
+
"path_open through a host link out of a Bind, by an absolute or a relative target, \
|
|
2330
|
+
beneath a Mount with a Policy through preview1 must answer NOTCAPABLE, into a loop \
|
|
2331
|
+
ELOOP, and path_unlink_file of each link must remove it"
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// The Filesystem fails only the lookup of `dir`, which the open needs no answer
|
|
2336
|
+
// from but the walk on its way does.
|
|
2337
|
+
// @behavior RT-126
|
|
2338
|
+
#[test]
|
|
2339
|
+
fn a_lookup_that_fails_fails_the_call_beneath_a_policy() {
|
|
2340
|
+
let tmpfs: Tmpfs = [("dir/file.txt", "granted")].into_iter().collect();
|
|
2341
|
+
let failing = Wrap::new(Arc::new(tmpfs), Arc::new(FailsLookingUpDir));
|
|
2342
|
+
let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
|
|
2343
|
+
let mut guest = Guest::new(&[grant(failing, Access::Read, Some(policy))]);
|
|
2344
|
+
assert_eq!(
|
|
2345
|
+
guest.call("read_file", DIR_FILE),
|
|
2346
|
+
ERRNO_IO,
|
|
2347
|
+
"path_open of a file beneath a directory the Filesystem fails to look up, beneath a \
|
|
2348
|
+
Mount with a Policy through preview1, must answer that failure"
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
/// A Tmpfs failing every lookup of `dir` itself, as a host directory
|
|
2353
|
+
/// that cannot be read could.
|
|
2354
|
+
struct FailsLookingUpDir;
|
|
2355
|
+
|
|
2356
|
+
impl Hook for FailsLookingUpDir {
|
|
2357
|
+
fn lookup(
|
|
2358
|
+
&self,
|
|
2359
|
+
inner: &dyn InodeOperations,
|
|
2360
|
+
dentry: &Dentry,
|
|
2361
|
+
) -> Result<Lookup, FilesystemError> {
|
|
2362
|
+
match dentry.name().as_str() {
|
|
2363
|
+
"dir" => Err(FilesystemError::Other("unreadable".into())),
|
|
2364
|
+
_ => inner.lookup(dentry),
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// Each path lands on what the link's target or its parent holds, so only a
|
|
2370
|
+
// walk taking `.` and `..` from where the link led answers.
|
|
2371
|
+
// @behavior RT-119
|
|
2372
|
+
#[cfg(unix)]
|
|
2373
|
+
#[test]
|
|
2374
|
+
fn a_step_out_of_a_link_lands_where_it_leads() {
|
|
2375
|
+
assert_eq!(
|
|
2376
|
+
stat_link(
|
|
2377
|
+
"dot-link",
|
|
2378
|
+
directory_with_key,
|
|
2379
|
+
&[(LINK_DOT, 1), (OUT_OF_LINK, 0)]
|
|
2380
|
+
)
|
|
2381
|
+
.into_iter()
|
|
2382
|
+
.map(|(errno, _)| errno)
|
|
2383
|
+
.collect::<Vec<_>>(),
|
|
2384
|
+
[0; 2],
|
|
2385
|
+
"path_filestat_get of `file.txt/.` and `file.txt/../file.txt`, where `file.txt` is a \
|
|
2386
|
+
host link to a directory, beneath a Bind Mount through preview1 must answer what they \
|
|
2387
|
+
lead to, as Linux's walk does"
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
fn tmpfs_with(path: &str, contents: &str) -> Arc<Tmpfs> {
|
|
2392
|
+
Arc::new([(path, contents)].into_iter().collect())
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
fn file_and_directory() -> Arc<Tmpfs> {
|
|
2396
|
+
Arc::new(
|
|
2397
|
+
[("file.txt", "granted"), ("dir/inner", "x")]
|
|
2398
|
+
.into_iter()
|
|
2399
|
+
.collect(),
|
|
2400
|
+
)
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/// A security module refusing every directory made.
|
|
2404
|
+
struct RefusesMkdir;
|
|
2405
|
+
|
|
2406
|
+
impl SecurityOperations for RefusesMkdir {
|
|
2407
|
+
fn path_mkdir(&self, _path: SecurityPath<'_>) -> Result<(), FilesystemError> {
|
|
2408
|
+
Err(FilesystemError::PermissionDenied)
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// The Mount allows every change and has no Policy, so only the module the
|
|
2413
|
+
// Driver was built with refuses the make.
|
|
2414
|
+
// @behavior RT-329
|
|
2415
|
+
#[test]
|
|
2416
|
+
fn a_security_module_the_driver_was_built_with_judges_the_guest() {
|
|
2417
|
+
let tmpfs = file_and_directory();
|
|
2418
|
+
let mut guest = Guest::with_security(
|
|
2419
|
+
vec![Arc::new(RefusesMkdir)],
|
|
2420
|
+
&[grant(tmpfs.clone(), Access::ReadWrite, None)],
|
|
2421
|
+
);
|
|
2422
|
+
assert_eq!(
|
|
2423
|
+
(
|
|
2424
|
+
guest.call("make_dir", NEW_AS_DIRECTORY),
|
|
2425
|
+
vfs::vfs_fstatat(
|
|
2426
|
+
&prepare_creds_within(&[], None),
|
|
2427
|
+
&host(&tmpfs),
|
|
2428
|
+
"new.txt",
|
|
2429
|
+
false
|
|
2430
|
+
)
|
|
2431
|
+
.is_err()
|
|
2432
|
+
),
|
|
2433
|
+
(ERRNO_ACCES, true),
|
|
2434
|
+
"path_create_directory beneath a Mount through preview1, the Driver built with a \
|
|
2435
|
+
security module refusing it, must answer EACCES and make nothing, as an LSM stacked \
|
|
2436
|
+
at boot refuses security_path_mkdir"
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// A `.` after a name resolves through it, so Linux and macOS both refuse
|
|
2441
|
+
// `missing/.` at the missing name rather than make it.
|
|
2442
|
+
// @behavior RT-109
|
|
2443
|
+
#[test]
|
|
2444
|
+
fn a_path_ending_in_a_dot_makes_nothing_where_no_directory_stands() {
|
|
2445
|
+
let tmpfs = file_and_directory();
|
|
2446
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
2447
|
+
let answers = [
|
|
2448
|
+
guest.call("make_dir", MISSING_DOT),
|
|
2449
|
+
guest.call_flagged(MISSING_DOT, OFLAGS_CREAT, 0),
|
|
2450
|
+
];
|
|
2451
|
+
assert_eq!(
|
|
2452
|
+
(
|
|
2453
|
+
answers,
|
|
2454
|
+
vfs::vfs_fstatat(
|
|
2455
|
+
&prepare_creds_within(&[], None),
|
|
2456
|
+
&host(&tmpfs),
|
|
2457
|
+
"missing",
|
|
2458
|
+
false
|
|
2459
|
+
)
|
|
2460
|
+
.is_err()
|
|
2461
|
+
),
|
|
2462
|
+
([ERRNO_NOENT, ERRNO_NOENT], true),
|
|
2463
|
+
"path_create_directory and path_open with O_CREAT of missing/. beneath a Mount through \
|
|
2464
|
+
preview1 must answer ENOENT, as Linux does, and make nothing named missing"
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// Resolving `dir/.` reaches `dir` itself, so only refusing the path as
|
|
2469
|
+
// written keeps either call from taking `dir`.
|
|
2470
|
+
// @behavior RT-102
|
|
2471
|
+
#[test]
|
|
2472
|
+
fn a_path_ending_in_a_dot_is_neither_removed_nor_moved() {
|
|
2473
|
+
let tmpfs = file_and_directory();
|
|
2474
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
2475
|
+
let answers = [
|
|
2476
|
+
guest.call("remove_path", DIR_DOT),
|
|
2477
|
+
guest.call("remove_path", DIR_DOT_DOT),
|
|
2478
|
+
guest.call_pair("rename_paths", DIR_DOT, MOVED),
|
|
2479
|
+
guest.call_pair("rename_paths", DIR_DOT_DOT, MOVED),
|
|
2480
|
+
guest.call("remove_path", OUT_OF_FILE),
|
|
2481
|
+
guest.call_pair("rename_paths", OUT_OF_FILE, MOVED),
|
|
2482
|
+
];
|
|
2483
|
+
assert_eq!(
|
|
2484
|
+
(
|
|
2485
|
+
answers,
|
|
2486
|
+
vfs::vfs_fstatat(
|
|
2487
|
+
&prepare_creds_within(&[], None),
|
|
2488
|
+
&host(&tmpfs),
|
|
2489
|
+
"dir/inner",
|
|
2490
|
+
false
|
|
2491
|
+
)
|
|
2492
|
+
.is_ok()
|
|
2493
|
+
),
|
|
2494
|
+
(
|
|
2495
|
+
[
|
|
2496
|
+
ERRNO_INVAL,
|
|
2497
|
+
ERRNO_NOTEMPTY,
|
|
2498
|
+
ERRNO_BUSY,
|
|
2499
|
+
ERRNO_BUSY,
|
|
2500
|
+
ERRNO_NOTDIR,
|
|
2501
|
+
ERRNO_NOTDIR
|
|
2502
|
+
],
|
|
2503
|
+
true
|
|
2504
|
+
),
|
|
2505
|
+
"path_remove_directory of a path ending in . or .., and path_rename of one, beneath a \
|
|
2506
|
+
Mount through preview1 must answer EINVAL for . and ENOTEMPTY for .. and EBUSY, as \
|
|
2507
|
+
Linux does, or ENOTDIR where a file stands before the dot, and leave the directory in \
|
|
2508
|
+
place"
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
// The Mount only reads, so only judging a create of what already stands
|
|
2513
|
+
// as no change lets the open through.
|
|
2514
|
+
// @behavior RT-103
|
|
2515
|
+
#[test]
|
|
2516
|
+
fn a_create_of_what_already_stands_changes_nothing() {
|
|
2517
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::Read, None)]);
|
|
2518
|
+
let answers = [
|
|
2519
|
+
guest.call_flagged(FILE, OFLAGS_CREAT, 0),
|
|
2520
|
+
guest.call_flagged(DIR, OFLAGS_CREAT, 0),
|
|
2521
|
+
];
|
|
2522
|
+
assert_eq!(
|
|
2523
|
+
answers,
|
|
2524
|
+
[0, ERRNO_ISDIR],
|
|
2525
|
+
"path_open with O_CREAT and no write of an existing file beneath a read-only Mount \
|
|
2526
|
+
through preview1 must open it, and of an existing directory answer EISDIR, as Linux does"
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// The host file only reads, so a host open asking to create, and so to
|
|
2531
|
+
// write, would be refused where the guest asked only to read.
|
|
2532
|
+
// @behavior RT-103
|
|
2533
|
+
#[cfg(unix)]
|
|
2534
|
+
#[test]
|
|
2535
|
+
fn a_create_of_a_host_file_that_only_reads_opens_it() {
|
|
2536
|
+
use std::os::unix::fs::PermissionsExt;
|
|
2537
|
+
let host = std::env::temp_dir().join(format!("kobako-granted-creat-{}", std::process::id()));
|
|
2538
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2539
|
+
std::fs::create_dir_all(&host).expect("the host directory must be created");
|
|
2540
|
+
std::fs::write(host.join("file.txt"), "granted").expect("the host file must be written");
|
|
2541
|
+
std::fs::set_permissions(
|
|
2542
|
+
host.join("file.txt"),
|
|
2543
|
+
std::fs::Permissions::from_mode(0o444),
|
|
2544
|
+
)
|
|
2545
|
+
.expect("the host file must become read-only");
|
|
2546
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
2547
|
+
let answer = Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, None)]).call_flagged(
|
|
2548
|
+
FILE,
|
|
2549
|
+
OFLAGS_CREAT,
|
|
2550
|
+
0,
|
|
2551
|
+
);
|
|
2552
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2553
|
+
assert_eq!(
|
|
2554
|
+
answer, 0,
|
|
2555
|
+
"path_open with O_CREAT and no write of a read-only host file beneath a Bind Mount \
|
|
2556
|
+
through preview1 must open it"
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
/// A host directory of the test's own beneath a read-write Bind Mount,
|
|
2561
|
+
/// `dir` a link to the directory `real` and `gone` a link to nothing,
|
|
2562
|
+
/// removed when it drops.
|
|
2563
|
+
#[cfg(unix)]
|
|
2564
|
+
struct LinkedHost(std::path::PathBuf);
|
|
2565
|
+
|
|
2566
|
+
#[cfg(unix)]
|
|
2567
|
+
impl LinkedHost {
|
|
2568
|
+
fn new(name: &str) -> Self {
|
|
2569
|
+
let host = std::env::temp_dir().join(format!("kobako-{name}-{}", std::process::id()));
|
|
2570
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
2571
|
+
std::fs::create_dir_all(host.join("real/inner")).expect("the host tree must be made");
|
|
2572
|
+
std::os::unix::fs::symlink("real", host.join("dir")).expect("the link must be made");
|
|
2573
|
+
std::os::unix::fs::symlink("nowhere", host.join("gone")).expect("the link must be made");
|
|
2574
|
+
Self(host)
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
fn guest(&self) -> Guest {
|
|
2578
|
+
let directory = Bind::new(&self.0).expect("the host directory must open");
|
|
2579
|
+
Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, None)])
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
#[cfg(unix)]
|
|
2584
|
+
impl Drop for LinkedHost {
|
|
2585
|
+
fn drop(&mut self) {
|
|
2586
|
+
let _ = std::fs::remove_dir_all(&self.0);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// The link stands before the dot, so it is followed as Linux follows a
|
|
2591
|
+
// link that is not the last name, and the dot answers as one through a
|
|
2592
|
+
// directory does.
|
|
2593
|
+
// @behavior RT-161
|
|
2594
|
+
#[cfg(unix)]
|
|
2595
|
+
#[test]
|
|
2596
|
+
fn a_path_ending_in_a_dot_through_a_link_is_neither_removed_nor_moved() {
|
|
2597
|
+
let host = LinkedHost::new("dot-link");
|
|
2598
|
+
let mut guest = host.guest();
|
|
2599
|
+
let answers = [
|
|
2600
|
+
guest.call("remove_path", DIR_DOT),
|
|
2601
|
+
guest.call("remove_path", DIR_DOT_DOT),
|
|
2602
|
+
guest.call_pair("rename_paths", DIR_DOT, MOVED),
|
|
2603
|
+
guest.call_pair("rename_paths", DIR_DOT_DOT, MOVED),
|
|
2604
|
+
];
|
|
2605
|
+
assert_eq!(
|
|
2606
|
+
(answers, host.0.join("real/inner").is_dir()),
|
|
2607
|
+
([ERRNO_INVAL, ERRNO_NOTEMPTY, ERRNO_BUSY, ERRNO_BUSY], true),
|
|
2608
|
+
"path_remove_directory and path_rename of dir/. and dir/.., dir a host link to a \
|
|
2609
|
+
directory beneath a Bind Mount, through preview1 must answer EINVAL, ENOTEMPTY, and \
|
|
2610
|
+
EBUSY, as Linux does, and leave the directory"
|
|
2611
|
+
);
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// The dot is the last name, which these act on rather than land on, so
|
|
2615
|
+
// nothing of where the link points shows, and the answers are Linux's.
|
|
2616
|
+
// @behavior RT-219
|
|
2617
|
+
#[cfg(unix)]
|
|
2618
|
+
#[test]
|
|
2619
|
+
fn a_change_naming_a_dot_through_a_link_answers_as_linux() {
|
|
2620
|
+
let host = LinkedHost::new("dot-change");
|
|
2621
|
+
let mut guest = host.guest();
|
|
2622
|
+
assert_eq!(
|
|
2623
|
+
(
|
|
2624
|
+
guest.call("make_dir", DIR_DOT),
|
|
2625
|
+
guest.call("unlink_path", DIR_DOT),
|
|
2626
|
+
host.0.join("real/inner").is_dir()
|
|
2627
|
+
),
|
|
2628
|
+
(ERRNO_EXIST, ERRNO_ISDIR, true),
|
|
2629
|
+
"path_create_directory and path_unlink_file of dir/., dir a host link to a directory \
|
|
2630
|
+
beneath a Bind Mount, through preview1 must answer EEXIST and EISDIR, as Linux does, \
|
|
2631
|
+
and leave the directory"
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/// The root of `host` beneath a Bind Mount, as the VFS walks it.
|
|
2636
|
+
#[cfg(unix)]
|
|
2637
|
+
fn bind_root(host: &std::path::Path) -> Path {
|
|
2638
|
+
let directory = Bind::new(host).expect("the host directory must open");
|
|
2639
|
+
let mount = Mount::new(
|
|
2640
|
+
"/data",
|
|
2641
|
+
Filesystem::new(Arc::new(directory)).expect("the instance must stand"),
|
|
2642
|
+
Access::ReadWrite,
|
|
2643
|
+
)
|
|
2644
|
+
.expect("/data is a guest path");
|
|
2645
|
+
Path::root(mount.mnt())
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// No security module is registered, so were the link followed the file would
|
|
2649
|
+
// be made where it points, and only `O_EXCL` keeps that from happening.
|
|
2650
|
+
// @behavior RT-220
|
|
2651
|
+
#[cfg(unix)]
|
|
2652
|
+
#[test]
|
|
2653
|
+
fn an_exclusive_create_follows_no_link_at_the_end() {
|
|
2654
|
+
let host = LinkedHost::new("excl-link");
|
|
2655
|
+
let mut how = OpenHow::default();
|
|
2656
|
+
how.write = true;
|
|
2657
|
+
how.create = true;
|
|
2658
|
+
how.exclusive = true;
|
|
2659
|
+
how.follow = true;
|
|
2660
|
+
let answer = vfs::do_file_open(
|
|
2661
|
+
&prepare_creds_within(&[], None),
|
|
2662
|
+
&bind_root(&host.0),
|
|
2663
|
+
"gone",
|
|
2664
|
+
&flags_of(&how),
|
|
2665
|
+
);
|
|
2666
|
+
assert_eq!(
|
|
2667
|
+
(answer.err(), host.0.join("nowhere").exists()),
|
|
2668
|
+
(Some(FilesystemError::AlreadyExists), false),
|
|
2669
|
+
"vfs::do_file_open with create and exclusive of a link to nothing, asking to follow it, \
|
|
2670
|
+
must answer EEXIST, as Linux's O_EXCL does, and make nothing"
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// The trailing `/` follows the link, so what is read stands at the directory,
|
|
2675
|
+
// where the walk that reports a link as itself would answer ENOTDIR.
|
|
2676
|
+
// @behavior RT-221
|
|
2677
|
+
#[cfg(unix)]
|
|
2678
|
+
#[test]
|
|
2679
|
+
fn a_trailing_slash_on_a_link_to_a_directory_reads_no_link() {
|
|
2680
|
+
let host = LinkedHost::new("readlink-slash");
|
|
2681
|
+
assert_eq!(
|
|
2682
|
+
vfs::do_readlinkat(
|
|
2683
|
+
&prepare_creds_within(&[], None),
|
|
2684
|
+
&bind_root(&host.0),
|
|
2685
|
+
"dir/"
|
|
2686
|
+
)
|
|
2687
|
+
.err(),
|
|
2688
|
+
Some(FilesystemError::InvalidInput),
|
|
2689
|
+
"vfs::do_readlinkat of dir/, dir a host link to a directory beneath a Bind Mount, must \
|
|
2690
|
+
answer EINVAL, as Linux's does"
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// Linux answers the trailing slash of a create before it would follow the
|
|
2695
|
+
// link, which points at nothing a create could make.
|
|
2696
|
+
// @behavior RT-162
|
|
2697
|
+
#[cfg(unix)]
|
|
2698
|
+
#[test]
|
|
2699
|
+
fn a_create_through_a_trailing_slash_answers_eisdir_before_following_a_link() {
|
|
2700
|
+
let host = LinkedHost::new("slash-link");
|
|
2701
|
+
let answer = host
|
|
2702
|
+
.guest()
|
|
2703
|
+
.call_flagged(GONE_AS_DIRECTORY, OFLAGS_CREAT, 0);
|
|
2704
|
+
assert_eq!(
|
|
2705
|
+
(answer, host.0.join("nowhere").exists()),
|
|
2706
|
+
(ERRNO_ISDIR, false),
|
|
2707
|
+
"path_open with O_CREAT of gone/, gone a host link to nothing beneath a Bind Mount, \
|
|
2708
|
+
through preview1 must answer EISDIR, as Linux does, and make nothing"
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// The second seek would land past what a signed offset holds, so only
|
|
2713
|
+
// the cap keeps the position from reaching it.
|
|
2714
|
+
// @behavior RT-104
|
|
2715
|
+
#[test]
|
|
2716
|
+
fn a_seek_past_the_largest_offset_is_invalid() {
|
|
2717
|
+
let answer = Guest::new(&[grant(file_and_directory(), Access::Read, None)])
|
|
2718
|
+
.call_bare("seek_past_the_largest_offset");
|
|
2719
|
+
assert_eq!(
|
|
2720
|
+
answer, ERRNO_INVAL,
|
|
2721
|
+
"fd_seek past the largest signed 64-bit offset of a file beneath a Mount through \
|
|
2722
|
+
preview1 must answer EINVAL, as Linux does"
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// A Tmpfs clamps a read past its end and tries to grow for a write or a
|
|
2727
|
+
// size, so only the check before the Filesystem answers each as Linux does.
|
|
2728
|
+
// @behavior RT-116
|
|
2729
|
+
#[test]
|
|
2730
|
+
fn an_offset_past_the_largest_signed_offset_is_invalid() {
|
|
2731
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
2732
|
+
assert_eq!(
|
|
2733
|
+
(
|
|
2734
|
+
guest.call_at("read_at", 1 << 63),
|
|
2735
|
+
guest.call_at("write_at", 1 << 63),
|
|
2736
|
+
guest.call_at("size_at", 1 << 63)
|
|
2737
|
+
),
|
|
2738
|
+
(ERRNO_INVAL, ERRNO_INVAL, ERRNO_INVAL),
|
|
2739
|
+
"fd_pread, fd_pwrite, and fd_filestat_set_size past the largest signed 64-bit offset of a \
|
|
2740
|
+
Tmpfs file beneath a Mount through preview1 must answer EINVAL, as Linux does"
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// The file is far shorter than either offset, so a read reaching the
|
|
2745
|
+
// Filesystem answers its end; only the refusal before it answers EINVAL.
|
|
2746
|
+
// @behavior RT-136
|
|
2747
|
+
#[test]
|
|
2748
|
+
fn a_read_whose_end_passes_the_largest_signed_offset_is_invalid() {
|
|
2749
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
2750
|
+
assert_eq!(
|
|
2751
|
+
(
|
|
2752
|
+
guest.call_at("read_at", i64::MAX as u64 - 10),
|
|
2753
|
+
guest.call_bare("read_near_the_largest_offset"),
|
|
2754
|
+
guest.call_at("read_at", i64::MAX as u64 - 64),
|
|
2755
|
+
),
|
|
2756
|
+
(ERRNO_INVAL, ERRNO_INVAL, 0),
|
|
2757
|
+
"fd_pread and fd_read of 64 bytes from ten before the largest signed 64-bit offset of a \
|
|
2758
|
+
file beneath a Mount through preview1 must answer EINVAL as Linux does, while a read \
|
|
2759
|
+
ending at it answers the file's end"
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// Each file is opened only to read, so only its Mount's access can tell the
|
|
2764
|
+
// two answers apart.
|
|
2765
|
+
// @behavior RT-135
|
|
2766
|
+
#[test]
|
|
2767
|
+
fn a_wait_to_write_a_file_is_ready_whatever_its_mount_permits() {
|
|
2768
|
+
let wait = |access| {
|
|
2769
|
+
Guest::new(&[grant(file_and_directory(), access, None)])
|
|
2770
|
+
.call_at("poll_write_with", RIGHTS_FD_READ)
|
|
2771
|
+
};
|
|
2772
|
+
assert_eq!(
|
|
2773
|
+
(wait(Access::ReadWrite), wait(Access::Read)),
|
|
2774
|
+
(0, 0),
|
|
2775
|
+
"poll_oneoff waiting to write a file opened only to read through preview1 must be ready \
|
|
2776
|
+
beneath a read-write and a read-only Mount alike, as Linux's vfs_poll answers a file \
|
|
2777
|
+
with no poll of its own"
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// The same file opened with the same rights, so only its Mount's access
|
|
2782
|
+
// tells the two refusals apart.
|
|
2783
|
+
// @behavior RT-137
|
|
2784
|
+
#[test]
|
|
2785
|
+
fn sizing_a_file_opened_only_to_read_answers_by_its_mount_then_as_linux_does() {
|
|
2786
|
+
let size = |access, rights| {
|
|
2787
|
+
Guest::new(&[grant(file_and_directory(), access, None)]).call_at("size_with", rights)
|
|
2788
|
+
};
|
|
2789
|
+
assert_eq!(
|
|
2790
|
+
(
|
|
2791
|
+
size(Access::Read, RIGHTS_FD_READ),
|
|
2792
|
+
size(Access::ReadWrite, RIGHTS_FD_READ),
|
|
2793
|
+
size(Access::ReadWrite, RIGHTS_FD_WRITE),
|
|
2794
|
+
),
|
|
2795
|
+
(ERRNO_INVAL, ERRNO_INVAL, 0),
|
|
2796
|
+
"fd_filestat_set_size on a file opened only to read through preview1 must answer EINVAL \
|
|
2797
|
+
beneath a read-only and a read-write Mount, as Linux's ftruncate does, while a file \
|
|
2798
|
+
opened to write is sized"
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
/// A Policy allowing every change while keeping each one it is asked.
|
|
2803
|
+
#[derive(Default)]
|
|
2804
|
+
struct RecordsAsks(std::sync::Mutex<Vec<(String, Access)>>);
|
|
2805
|
+
|
|
2806
|
+
impl Policy for RecordsAsks {
|
|
2807
|
+
fn allows(&self, path: &str, access: Access) -> bool {
|
|
2808
|
+
self.0.lock().unwrap().push((path.to_owned(), access));
|
|
2809
|
+
true
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// The Mount is made read-only, as a Ruby mount with a block is by default,
|
|
2814
|
+
// so only a Mount allowing writes lets either through.
|
|
2815
|
+
// Each Policy is asked only by the open before the call, so a wait or a size
|
|
2816
|
+
// asking it as well adds an ask the literal lists below do not hold.
|
|
2817
|
+
// @behavior RT-139
|
|
2818
|
+
#[test]
|
|
2819
|
+
fn a_mount_whose_policy_decides_may_be_written() {
|
|
2820
|
+
let call = |name, rights| {
|
|
2821
|
+
let policy = Arc::new(RecordsAsks::default());
|
|
2822
|
+
let errno = Guest::new(&[grant(
|
|
2823
|
+
file_and_directory(),
|
|
2824
|
+
Access::ReadWrite,
|
|
2825
|
+
Some(policy.clone() as Arc<dyn Policy>),
|
|
2826
|
+
)])
|
|
2827
|
+
.call_at(name, rights);
|
|
2828
|
+
let asked = policy.0.lock().unwrap().clone();
|
|
2829
|
+
(errno, asked)
|
|
2830
|
+
};
|
|
2831
|
+
assert_eq!(
|
|
2832
|
+
(
|
|
2833
|
+
call("poll_write_with", RIGHTS_FD_READ),
|
|
2834
|
+
call("size_with", RIGHTS_FD_WRITE),
|
|
2835
|
+
),
|
|
2836
|
+
(
|
|
2837
|
+
(0, vec![("/data/file.txt".to_owned(), Access::Read)]),
|
|
2838
|
+
(0, vec![("/data/file.txt".to_owned(), Access::ReadWrite)]),
|
|
2839
|
+
),
|
|
2840
|
+
"through preview1 beneath a read-write Mount whose Policy allows every change, \
|
|
2841
|
+
poll_oneoff waiting to write a file opened only to read must be ready, and \
|
|
2842
|
+
fd_filestat_set_size on a file opened to write must size it, the Policy asked \
|
|
2843
|
+
only by each open"
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// Each open asks without the right to write, so only its CREAT makes it a
|
|
2848
|
+
// change; that is asked before the walk knows the file stands, as Linux's
|
|
2849
|
+
// `may_o_create` is, and the open of the file then asks as a read.
|
|
2850
|
+
// @behavior RT-146
|
|
2851
|
+
#[test]
|
|
2852
|
+
fn an_open_that_may_create_asks_to_make_the_file_before_it_finds_what_stands() {
|
|
2853
|
+
let open = |path| {
|
|
2854
|
+
let policy = Arc::new(RecordsAsks::default());
|
|
2855
|
+
let errno = Guest::new(&[grant(
|
|
2856
|
+
file_and_directory(),
|
|
2857
|
+
Access::ReadWrite,
|
|
2858
|
+
Some(policy.clone() as Arc<dyn Policy>),
|
|
2859
|
+
)])
|
|
2860
|
+
.call_flagged(path, OFLAGS_CREAT, 0);
|
|
2861
|
+
let asked = policy.0.lock().unwrap().clone();
|
|
2862
|
+
(errno, asked)
|
|
2863
|
+
};
|
|
2864
|
+
assert_eq!(
|
|
2865
|
+
(open(NEW), open(FILE)),
|
|
2866
|
+
(
|
|
2867
|
+
(0, vec![("/data/new.txt".to_owned(), Access::ReadWrite)]),
|
|
2868
|
+
(
|
|
2869
|
+
0,
|
|
2870
|
+
vec![
|
|
2871
|
+
("/data/file.txt".to_owned(), Access::ReadWrite),
|
|
2872
|
+
("/data/file.txt".to_owned(), Access::Read),
|
|
2873
|
+
]
|
|
2874
|
+
),
|
|
2875
|
+
),
|
|
2876
|
+
"path_open with O_CREAT and no write of a missing path and of an existing file beneath \
|
|
2877
|
+
a Mount whose Policy allows every change through preview1 must ask the Policy a change \
|
|
2878
|
+
for each, then a read for the file, as lookup_open asks may_o_create first"
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/// A Policy refusing the Mount's root and allowing what lies beneath it,
|
|
2883
|
+
/// keeping each path it is asked.
|
|
2884
|
+
#[derive(Default)]
|
|
2885
|
+
struct RefusesTheRoot(std::sync::Mutex<Vec<String>>);
|
|
2886
|
+
|
|
2887
|
+
impl Policy for RefusesTheRoot {
|
|
2888
|
+
fn allows(&self, path: &str, _access: Access) -> bool {
|
|
2889
|
+
self.0.lock().unwrap().push(path.to_owned());
|
|
2890
|
+
path != "/data"
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// A Policy opening its Mount's root itself would refuse the invocation before
|
|
2895
|
+
// the guest runs, so only a root opened before the Policy applies lets the
|
|
2896
|
+
// read through.
|
|
2897
|
+
// @behavior RT-290
|
|
2898
|
+
#[test]
|
|
2899
|
+
fn a_mount_whose_policy_refuses_its_root_still_reads_beneath_it() {
|
|
2900
|
+
let policy = Arc::new(RefusesTheRoot::default());
|
|
2901
|
+
let mut guest = Guest::new(&[grant(
|
|
2902
|
+
file_and_directory(),
|
|
2903
|
+
Access::Read,
|
|
2904
|
+
Some(policy.clone() as Arc<dyn Policy>),
|
|
2905
|
+
)]);
|
|
2906
|
+
let read = guest.call("read_file", FILE);
|
|
2907
|
+
let asked = policy.0.lock().unwrap().clone();
|
|
2908
|
+
assert_eq!(
|
|
2909
|
+
(read, asked),
|
|
2910
|
+
(0, vec!["/data/file.txt".to_owned()]),
|
|
2911
|
+
"path_open of a file beneath a Mount whose Policy refuses its root, through preview1, \
|
|
2912
|
+
must read the file and ask the Policy only about it, as a Linux task opens what it \
|
|
2913
|
+
keeps before landlock_restrict_self"
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// The open asks to write a path nothing stands at, so only the lookup
|
|
2918
|
+
// before the Mount judges it answers ENOENT, as Linux does on a read-only
|
|
2919
|
+
// mount, rather than the EROFS the same open of a file answers.
|
|
2920
|
+
// @behavior RT-147
|
|
2921
|
+
#[test]
|
|
2922
|
+
fn an_open_of_a_missing_path_that_makes_nothing_answers_enoent() {
|
|
2923
|
+
let read_only =
|
|
2924
|
+
Guest::new(&[grant(file_and_directory(), Access::Read, None)]).call("append_file", NEW);
|
|
2925
|
+
let policy = Arc::new(RecordsAsks::default());
|
|
2926
|
+
let policed = Guest::new(&[grant(
|
|
2927
|
+
file_and_directory(),
|
|
2928
|
+
Access::Read,
|
|
2929
|
+
Some(policy.clone() as Arc<dyn Policy>),
|
|
2930
|
+
)])
|
|
2931
|
+
.call("append_file", NEW);
|
|
2932
|
+
let asked = policy.0.lock().unwrap().clone();
|
|
2933
|
+
assert_eq!(
|
|
2934
|
+
(read_only, policed, asked),
|
|
2935
|
+
(ERRNO_NOENT, ERRNO_NOENT, Vec::new()),
|
|
2936
|
+
"path_open to write a missing path without O_CREAT beneath a read-only Mount and beneath \
|
|
2937
|
+
a Mount with a Policy through preview1 must answer ENOENT and ask the Policy nothing"
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
// The Mount is read-only, so an open judged before what stands at its
|
|
2942
|
+
// path is looked up would answer EROFS for both.
|
|
2943
|
+
// @behavior RT-148
|
|
2944
|
+
#[test]
|
|
2945
|
+
fn what_stands_at_a_path_answers_before_a_read_only_mount_refuses() {
|
|
2946
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::Read, None)]);
|
|
2947
|
+
assert_eq!(
|
|
2948
|
+
(
|
|
2949
|
+
guest.call_flagged(FILE, OFLAGS_CREAT | OFLAGS_EXCL, 0),
|
|
2950
|
+
guest.call("append_file", DIR),
|
|
2951
|
+
),
|
|
2952
|
+
(ERRNO_EXIST, ERRNO_ISDIR),
|
|
2953
|
+
"path_open with O_CREAT | O_EXCL of a file, and to write a directory, beneath a read-only \
|
|
2954
|
+
Mount through preview1 must answer EEXIST and EISDIR, as Linux does"
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// wasi-testsuite makes a file asking no right, so only an open that reads
|
|
2959
|
+
// lets the descriptor read what it made or emptied.
|
|
2960
|
+
// @behavior RT-303
|
|
2961
|
+
#[test]
|
|
2962
|
+
fn an_open_making_or_emptying_a_file_with_no_right_opens_it_to_read() {
|
|
2963
|
+
let tmpfs = file_and_directory();
|
|
2964
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
2965
|
+
let mut read_unasked = |path: (i32, i32), oflags: i32| {
|
|
2966
|
+
guest
|
|
2967
|
+
.instance
|
|
2968
|
+
.get_typed_func::<(i32, i32, i32), i32>(&mut guest.store, "read_unasked")
|
|
2969
|
+
.expect("probe export must resolve")
|
|
2970
|
+
.call(&mut guest.store, (path.0, path.1, oflags))
|
|
2971
|
+
.expect("probe call must not trap")
|
|
2972
|
+
};
|
|
2973
|
+
let answers = (
|
|
2974
|
+
read_unasked(NEW, OFLAGS_CREAT),
|
|
2975
|
+
read_unasked(FILE, OFLAGS_TRUNC),
|
|
2976
|
+
);
|
|
2977
|
+
assert_eq!(
|
|
2978
|
+
(
|
|
2979
|
+
answers,
|
|
2980
|
+
contents(&host(&tmpfs), "new.txt"),
|
|
2981
|
+
contents(&host(&tmpfs), "file.txt")
|
|
2982
|
+
),
|
|
2983
|
+
((0, 0), Some(Vec::new()), Some(Vec::new())),
|
|
2984
|
+
"path_open asking no right with O_CREAT, or with O_TRUNC, of a Tmpfs beneath a read-write \
|
|
2985
|
+
Mount through preview1 must make or empty the file and open it to read, as an O_RDONLY \
|
|
2986
|
+
open does"
|
|
2987
|
+
);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
const NEW: (i32, i32) = (840, 7);
|
|
2991
|
+
const RIGHTS_FD_READ: u64 = 1 << 1;
|
|
2992
|
+
const ERRNO_PERM: i32 = 63;
|
|
2993
|
+
|
|
2994
|
+
/// A Tmpfs with no operation to make, remove, or move a name, as a NULL in
|
|
2995
|
+
/// Linux's `inode_operations`.
|
|
2996
|
+
struct ChangesNothing(Arc<dyn InodeOperations>);
|
|
2997
|
+
|
|
2998
|
+
impl FileSystemType for ChangesNothing {
|
|
2999
|
+
fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
|
|
3000
|
+
sb.d_make_root(vfs::Inode::new(
|
|
3001
|
+
&vfs::Kstat::new(vfs::FileType::Dir, 0),
|
|
3002
|
+
self,
|
|
3003
|
+
));
|
|
3004
|
+
Ok(())
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
impl InodeOperations for ChangesNothing {
|
|
3009
|
+
fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
3010
|
+
self.0.getattr(dentry)
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
|
|
3014
|
+
Ok(match self.0.lookup(dentry)? {
|
|
3015
|
+
Lookup::Found(inode) => {
|
|
3016
|
+
Lookup::Found(inode.with_ops(Arc::new(Self(inode.ops().clone()))))
|
|
3017
|
+
}
|
|
3018
|
+
other => other,
|
|
3019
|
+
})
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
fn open(
|
|
3023
|
+
&self,
|
|
3024
|
+
dentry: &Dentry,
|
|
3025
|
+
how: &OpenHow,
|
|
3026
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
3027
|
+
self.0.open(dentry, how)
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// The Filesystem has nothing to answer these with, so each errno is the
|
|
3032
|
+
// runtime's, the one Linux's VFS answers for a NULL in `inode_operations`.
|
|
3033
|
+
// @behavior RT-175
|
|
3034
|
+
#[test]
|
|
3035
|
+
fn the_runtime_answers_for_an_operation_a_filesystem_lacks() {
|
|
3036
|
+
let filesystem = Arc::new(ChangesNothing(file_and_directory()));
|
|
3037
|
+
let mut guest = Guest::new(&[grant(filesystem, Access::ReadWrite, None)]);
|
|
3038
|
+
assert_eq!(
|
|
3039
|
+
(
|
|
3040
|
+
guest.call("make_dir", NEW),
|
|
3041
|
+
guest.call_flagged(NEW, OFLAGS_CREAT, 0),
|
|
3042
|
+
guest.call("remove_path", DIR),
|
|
3043
|
+
guest.call("unlink_path", FILE),
|
|
3044
|
+
guest.call_pair("rename_paths", FILE, MOVED),
|
|
3045
|
+
),
|
|
3046
|
+
(ERRNO_PERM, ERRNO_ACCES, ERRNO_PERM, ERRNO_PERM, ERRNO_PERM),
|
|
3047
|
+
"path_create_directory, path_open with O_CREAT, path_remove_directory, \
|
|
3048
|
+
path_unlink_file, and path_rename through preview1, of a Filesystem lacking each \
|
|
3049
|
+
operation, must answer EPERM, EACCES, EPERM, EPERM, and EPERM"
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// A Tmpfs would refuse the bytes for want of space, so only the refusal
|
|
3054
|
+
// before the Filesystem is asked answers EINVAL.
|
|
3055
|
+
// @behavior RT-133
|
|
3056
|
+
#[test]
|
|
3057
|
+
fn a_write_ending_past_the_largest_signed_offset_is_invalid() {
|
|
3058
|
+
let tmpfs = file_and_directory();
|
|
3059
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
3060
|
+
let answers = (
|
|
3061
|
+
guest.call_bare("write_at_the_largest_offset"),
|
|
3062
|
+
guest.call_at("write_at", i64::MAX as u64 - 1),
|
|
3063
|
+
);
|
|
3064
|
+
assert_eq!(
|
|
3065
|
+
(answers, contents(&host(&tmpfs), "file.txt")),
|
|
3066
|
+
((ERRNO_INVAL, ERRNO_INVAL), Some(b"granted".to_vec())),
|
|
3067
|
+
"fd_write at the largest signed 64-bit offset, and fd_pwrite from one before it, of a \
|
|
3068
|
+
Tmpfs file beneath a Mount through preview1 must answer EINVAL, as Linux does, and \
|
|
3069
|
+
leave the file as it was"
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// @behavior RT-130
|
|
3074
|
+
#[test]
|
|
3075
|
+
fn an_advice_reaching_past_the_largest_signed_offset_is_invalid() {
|
|
3076
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
3077
|
+
assert_eq!(
|
|
3078
|
+
(
|
|
3079
|
+
guest.call_at("advise_from", 1 << 63),
|
|
3080
|
+
guest.call_at("advise_for", 1 << 63),
|
|
3081
|
+
guest.call_at("advise_for", 0)
|
|
3082
|
+
),
|
|
3083
|
+
(ERRNO_INVAL, ERRNO_INVAL, 0),
|
|
3084
|
+
"fd_advise of a Tmpfs file beneath a Mount through preview1 must answer EINVAL from an \
|
|
3085
|
+
offset or for a length past the largest signed 64-bit offset, as Linux's \
|
|
3086
|
+
generic_fadvise does"
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// The open asks to read only, so nothing else the open could refuse
|
|
3091
|
+
// stands in for a refusal of not blocking.
|
|
3092
|
+
// @behavior RT-129
|
|
3093
|
+
#[test]
|
|
3094
|
+
fn a_descriptor_may_ask_not_to_block() {
|
|
3095
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
3096
|
+
assert_eq!(
|
|
3097
|
+
(
|
|
3098
|
+
guest.call_flagged(FILE, 0, FDFLAGS_NONBLOCK),
|
|
3099
|
+
guest.call_bare("set_nonblocking"),
|
|
3100
|
+
guest.call_flagged(FILE, 0, 0)
|
|
3101
|
+
),
|
|
3102
|
+
(0, 0, 0),
|
|
3103
|
+
"path_open asking not to block and fd_fdstat_set_flags setting an open file not to \
|
|
3104
|
+
block, beneath a Mount through preview1, must succeed, as Linux takes O_NONBLOCK on a \
|
|
3105
|
+
file"
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// @behavior RT-128
|
|
3110
|
+
#[test]
|
|
3111
|
+
fn a_directory_a_guest_opens_carries_no_right_to_write() {
|
|
3112
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
3113
|
+
let errno = guest.call_bare("dir_fdstat");
|
|
3114
|
+
let memory = guest
|
|
3115
|
+
.instance
|
|
3116
|
+
.get_memory(&mut guest.store, "memory")
|
|
3117
|
+
.expect("probe exports its memory");
|
|
3118
|
+
let rights = u64::from_le_bytes(
|
|
3119
|
+
memory.data(&guest.store)[392..400]
|
|
3120
|
+
.try_into()
|
|
3121
|
+
.expect("the rights are eight bytes"),
|
|
3122
|
+
);
|
|
3123
|
+
assert_eq!(
|
|
3124
|
+
(errno, rights & RIGHTS_FD_WRITE, rights & RIGHTS_FD_READDIR),
|
|
3125
|
+
(0, 0, RIGHTS_FD_READDIR),
|
|
3126
|
+
"fd_fdstat_get of a directory opened through path_open beneath a read-write Mount \
|
|
3127
|
+
through preview1 must report its rights without fd_write, as wasmtime-wasi does"
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
// wasi-libc's `openat` asks only the rights its directory hands down, so a
|
|
3132
|
+
// write beneath a directory a guest opened needs `fd_write` among them.
|
|
3133
|
+
// @behavior RT-298
|
|
3134
|
+
#[test]
|
|
3135
|
+
fn a_directory_a_guest_opens_hands_down_the_right_to_write() {
|
|
3136
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
3137
|
+
let errno = guest.call_dir_with((RIGHTS_FD_READ | RIGHTS_FD_READDIR) as i64, 9);
|
|
3138
|
+
let memory = guest
|
|
3139
|
+
.instance
|
|
3140
|
+
.get_memory(&mut guest.store, "memory")
|
|
3141
|
+
.expect("probe exports its memory");
|
|
3142
|
+
let inheriting = u64::from_le_bytes(
|
|
3143
|
+
memory.data(&guest.store)[400..408]
|
|
3144
|
+
.try_into()
|
|
3145
|
+
.expect("the rights are eight bytes"),
|
|
3146
|
+
);
|
|
3147
|
+
assert_eq!(
|
|
3148
|
+
(errno, inheriting & RIGHTS_FD_WRITE),
|
|
3149
|
+
(0, RIGHTS_FD_WRITE),
|
|
3150
|
+
"fd_fdstat_get of a directory opened through path_open beneath a read-write Mount \
|
|
3151
|
+
through preview1 must hand down fd_write, as Linux takes an open's mode from its \
|
|
3152
|
+
flags alone"
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// wasi-libc opens `O_SEARCH`, which glibc spells `O_PATH`, without `fd_read`,
|
|
3157
|
+
// `fd_readdir`, or `fd_write`; the poll answers the event's error, its call
|
|
3158
|
+
// succeeding. wasi-testsuite lists a directory opened with `fd_readdir` alone.
|
|
3159
|
+
// @behavior RT-297
|
|
3160
|
+
// @behavior RT-299
|
|
3161
|
+
// @behavior RT-300
|
|
3162
|
+
#[test]
|
|
3163
|
+
fn a_directory_opened_for_neither_reading_nor_writing_stands_only_for_its_path() {
|
|
3164
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
3165
|
+
assert_eq!(
|
|
3166
|
+
guest.call_dir_with(RIGHTS_FD_READDIR as i64, 0),
|
|
3167
|
+
0,
|
|
3168
|
+
"fd_readdir of a directory opened with fd_readdir alone through preview1 must list it"
|
|
3169
|
+
);
|
|
3170
|
+
let answers: Vec<i32> = (0..9).map(|call| guest.call_dir_with(0, call)).collect();
|
|
3171
|
+
assert_eq!(
|
|
3172
|
+
answers,
|
|
3173
|
+
[
|
|
3174
|
+
ERRNO_BADF, ERRNO_BADF, ERRNO_BADF, ERRNO_BADF, ERRNO_BADF, ERRNO_BADF, ERRNO_BADF, 0,
|
|
3175
|
+
0
|
|
3176
|
+
],
|
|
3177
|
+
"fd_readdir, fd_sync, fd_advise, fd_fdstat_set_flags, fd_filestat_set_size, \
|
|
3178
|
+
fd_filestat_set_times, and poll_oneoff of a directory opened without fd_read or \
|
|
3179
|
+
fd_write beneath a read-write Mount through preview1 must answer EBADF, as fdget \
|
|
3180
|
+
refuses O_PATH, while fd_filestat_get and path_filestat_get from it succeed"
|
|
3181
|
+
);
|
|
3182
|
+
let errno = guest.call_dir_with(0, 9);
|
|
3183
|
+
let memory = guest
|
|
3184
|
+
.instance
|
|
3185
|
+
.get_memory(&mut guest.store, "memory")
|
|
3186
|
+
.expect("probe exports its memory");
|
|
3187
|
+
let rights = u64::from_le_bytes(
|
|
3188
|
+
memory.data(&guest.store)[392..400]
|
|
3189
|
+
.try_into()
|
|
3190
|
+
.expect("the rights are eight bytes"),
|
|
3191
|
+
);
|
|
3192
|
+
assert_eq!(
|
|
3193
|
+
(errno, rights & RIGHTS_FD_READDIR),
|
|
3194
|
+
(0, 0),
|
|
3195
|
+
"fd_fdstat_get of a directory opened without fd_read or fd_write through preview1 \
|
|
3196
|
+
must report no fd_readdir, as it lists nothing"
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// A directory opened to list answers NOTSOCK, so only a socket call taking its
|
|
3201
|
+
// descriptor through fdget answers EBADF for one standing only for its path.
|
|
3202
|
+
// @behavior RT-306
|
|
3203
|
+
#[test]
|
|
3204
|
+
fn a_socket_call_refuses_a_descriptor_standing_only_for_its_path() {
|
|
3205
|
+
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
3206
|
+
assert_eq!(
|
|
3207
|
+
(
|
|
3208
|
+
guest.call_dir_with(0, 10),
|
|
3209
|
+
guest.call_dir_with(RIGHTS_FD_READDIR as i64, 10)
|
|
3210
|
+
),
|
|
3211
|
+
(ERRNO_BADF, ERRNO_NOTSOCK),
|
|
3212
|
+
"sock_shutdown through preview1 of a directory opened without fd_read or fd_write must \
|
|
3213
|
+
answer EBADF, as Linux's socket calls take theirs through fdget, and of one opened to \
|
|
3214
|
+
list ENOTSOCK"
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
const ERRNO_NOTSOCK: i32 = 57;
|
|
3219
|
+
|
|
3220
|
+
// @behavior RT-105
|
|
3221
|
+
#[test]
|
|
3222
|
+
fn a_file_moved_beneath_itself_is_no_directory() {
|
|
3223
|
+
let tmpfs = file_and_directory();
|
|
3224
|
+
let answer = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]).call_pair(
|
|
3225
|
+
"rename_paths",
|
|
3226
|
+
FILE,
|
|
3227
|
+
BENEATH_FILE,
|
|
3228
|
+
);
|
|
3229
|
+
assert_eq!(
|
|
3230
|
+
(answer, contents(&host(&tmpfs), "file.txt")),
|
|
3231
|
+
(ERRNO_NOTDIR, Some(b"granted".to_vec())),
|
|
3232
|
+
"path_rename of a file to a path beneath it, beneath a Mount through preview1, must \
|
|
3233
|
+
answer ENOTDIR as Linux does and leave the file in place"
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
// The link points at nothing, so only the link itself, not a file it
|
|
3238
|
+
// would follow to, can be what each open finds there.
|
|
3239
|
+
// @behavior RT-106
|
|
3240
|
+
#[cfg(unix)]
|
|
3241
|
+
#[test]
|
|
3242
|
+
fn an_open_following_no_link_finds_a_dangling_link_as_itself() {
|
|
3243
|
+
let (host, mut guest) = mount_link_under("dangling", |_| {}, Access::ReadWrite, None);
|
|
3244
|
+
let answers = [
|
|
3245
|
+
guest.call_flagged(FILE, 0, 0),
|
|
3246
|
+
guest.call_flagged(FILE, OFLAGS_CREAT | OFLAGS_EXCL, 0),
|
|
3247
|
+
];
|
|
3248
|
+
let made = host.join("target").exists();
|
|
3249
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
3250
|
+
assert_eq!(
|
|
3251
|
+
(answers, made),
|
|
3252
|
+
([ERRNO_LOOP, ERRNO_EXIST], false),
|
|
3253
|
+
"path_open following no link of a dangling host link beneath a Bind Mount through \
|
|
3254
|
+
preview1 must answer ELOOP, and with O_CREAT and O_EXCL EEXIST, as Linux does, making \
|
|
3255
|
+
nothing where it points"
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
/// What each root-changing export answers, and how many changes the
|
|
3260
|
+
/// Filesystem was asked for along the way.
|
|
3261
|
+
fn change_root(exports: &[&str]) -> (Vec<i32>, usize) {
|
|
3262
|
+
let (counted, filesystem) = ChangeCounter::over_file();
|
|
3263
|
+
let mut guest = Guest::new(&[grant(filesystem, Access::ReadWrite, None)]);
|
|
3264
|
+
let answers = exports.iter().map(|name| guest.call_bare(name)).collect();
|
|
3265
|
+
(answers, counted.changes.load(Ordering::Relaxed))
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
// @behavior RT-083
|
|
3269
|
+
#[test]
|
|
3270
|
+
fn removing_a_mounts_root_is_refused_as_invalid() {
|
|
3271
|
+
assert_eq!(
|
|
3272
|
+
change_root(&["remove_root"]),
|
|
3273
|
+
(vec![ERRNO_INVAL], 0),
|
|
3274
|
+
"path_remove_directory of `.` beneath a Mount through preview1 must answer EINVAL \
|
|
3275
|
+
without asking the Filesystem"
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// @behavior RT-084
|
|
3280
|
+
#[test]
|
|
3281
|
+
fn renaming_a_mounts_root_is_refused_as_busy() {
|
|
3282
|
+
assert_eq!(
|
|
3283
|
+
change_root(&["rename_root", "rename_onto_root"]),
|
|
3284
|
+
(vec![ERRNO_BUSY, ERRNO_BUSY], 0),
|
|
3285
|
+
"path_rename from or onto `.` beneath a Mount through preview1 must answer EBUSY \
|
|
3286
|
+
without asking the Filesystem"
|
|
3287
|
+
);
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
// @behavior RT-085
|
|
3291
|
+
#[test]
|
|
3292
|
+
fn unlinking_a_mounts_root_answers_that_it_is_a_directory() {
|
|
3293
|
+
assert_eq!(
|
|
3294
|
+
change_root(&["unlink_root"]),
|
|
3295
|
+
(vec![ERRNO_ISDIR], 0),
|
|
3296
|
+
"path_unlink_file of `.` beneath a Mount through preview1 must answer EISDIR \
|
|
3297
|
+
without asking the Filesystem"
|
|
3298
|
+
);
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
/// What reading `path` answers beneath a read-only Mount holding
|
|
3302
|
+
/// `file.txt`.
|
|
3303
|
+
fn read_beneath(path: (i32, i32)) -> i32 {
|
|
3304
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
3305
|
+
Guest::new(&[grant(Arc::new(tmpfs), Access::Read, None)]).call("read_file", path)
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// @behavior RT-086
|
|
3309
|
+
#[test]
|
|
3310
|
+
fn an_empty_path_names_nothing_beneath_a_mount() {
|
|
3311
|
+
assert_eq!(
|
|
3312
|
+
read_beneath(EMPTY),
|
|
3313
|
+
ERRNO_NOENT,
|
|
3314
|
+
"path_open of the empty path beneath a Mount through preview1 must answer ENOENT"
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
// @behavior RT-087
|
|
3319
|
+
#[test]
|
|
3320
|
+
fn a_dot_dot_after_a_missing_name_names_nothing() {
|
|
3321
|
+
assert_eq!(
|
|
3322
|
+
read_beneath(THROUGH_MISSING),
|
|
3323
|
+
ERRNO_NOENT,
|
|
3324
|
+
"path_open of `missing/../file.txt` beneath a Mount through preview1 must answer ENOENT"
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
// The Tmpfs would hold `file.txt` and any other name, so only the NUL
|
|
3329
|
+
// refuses the open.
|
|
3330
|
+
// @behavior RT-122
|
|
3331
|
+
#[test]
|
|
3332
|
+
fn a_path_holding_a_nul_is_invalid() {
|
|
3333
|
+
assert_eq!(
|
|
3334
|
+
read_beneath(HOLDING_NUL),
|
|
3335
|
+
ERRNO_INVAL,
|
|
3336
|
+
"path_open of `file.txt\\0` beneath a Mount through preview1 must answer EINVAL"
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// @behavior RT-088
|
|
3341
|
+
#[test]
|
|
3342
|
+
fn a_dot_dot_after_a_file_is_not_a_directory() {
|
|
3343
|
+
assert_eq!(
|
|
3344
|
+
read_beneath(OUT_OF_FILE),
|
|
3345
|
+
ERRNO_NOTDIR,
|
|
3346
|
+
"path_open of `file.txt/..` beneath a Mount through preview1 must answer ENOTDIR"
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// @behavior RT-089
|
|
3351
|
+
#[test]
|
|
3352
|
+
fn a_path_ending_in_a_slash_names_only_a_directory() {
|
|
3353
|
+
assert_eq!(
|
|
3354
|
+
read_beneath(FILE_AS_DIRECTORY),
|
|
3355
|
+
ERRNO_NOTDIR,
|
|
3356
|
+
"path_open of `file.txt/` beneath a Mount through preview1 must answer ENOTDIR"
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
// The target is the source's own path, so only the slash can refuse it.
|
|
3361
|
+
// @behavior RT-093
|
|
3362
|
+
#[test]
|
|
3363
|
+
fn a_rename_of_a_file_onto_a_path_ending_in_a_slash_is_not_a_directory() {
|
|
3364
|
+
let tmpfs = tmpfs_with("file.txt", "granted");
|
|
3365
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
3366
|
+
assert_eq!(
|
|
3367
|
+
(
|
|
3368
|
+
guest.call_bare("rename_onto_itself_as_directory"),
|
|
3369
|
+
contents(&host(&tmpfs), "file.txt")
|
|
3370
|
+
),
|
|
3371
|
+
(ERRNO_NOTDIR, Some(b"granted".to_vec())),
|
|
3372
|
+
"path_rename of `file.txt` onto `file.txt/` beneath a Mount through preview1 must \
|
|
3373
|
+
answer ENOTDIR and leave the file"
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// @behavior RT-090
|
|
3378
|
+
#[test]
|
|
3379
|
+
fn a_file_created_at_a_path_ending_in_a_slash_is_refused_as_a_directory() {
|
|
3380
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
3381
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
3382
|
+
assert_eq!(
|
|
3383
|
+
(
|
|
3384
|
+
guest.call("write_file", NEW_AS_DIRECTORY),
|
|
3385
|
+
host_entries(&tmpfs).map(|entries| entries.len())
|
|
3386
|
+
),
|
|
3387
|
+
(ERRNO_ISDIR, Ok(0)),
|
|
3388
|
+
"path_open creating `new.txt/` beneath a Mount through preview1 must answer EISDIR and \
|
|
3389
|
+
make no file"
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
const OFLAGS_CREAT: i32 = 1 << 0;
|
|
3394
|
+
const OFLAGS_DIRECTORY: i32 = 1 << 1;
|
|
3395
|
+
const OFLAGS_TRUNC: i32 = 1 << 3;
|
|
3396
|
+
const FDFLAGS_SYNC: i32 = 1 << 4;
|
|
3397
|
+
const FDFLAGS_NONBLOCK: i32 = 1 << 2;
|
|
3398
|
+
const RIGHTS_FD_WRITE: u64 = 1 << 6;
|
|
3399
|
+
const RIGHTS_FD_READDIR: u64 = 1 << 14;
|
|
3400
|
+
|
|
3401
|
+
/// What opening `path` with `oflags` and `fdflags` answers beneath a
|
|
3402
|
+
/// read-write Tmpfs holding `file.txt`, and the paths it holds after.
|
|
3403
|
+
fn open_flagged(path: (i32, i32), oflags: i32, fdflags: i32) -> (i32, Vec<String>) {
|
|
3404
|
+
let tmpfs = tmpfs_with("file.txt", "granted");
|
|
3405
|
+
let errno = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)])
|
|
3406
|
+
.call_flagged(path, oflags, fdflags);
|
|
3407
|
+
let held = host_entries(&tmpfs).expect("the root must list");
|
|
3408
|
+
(errno, held)
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
// @behavior RT-091
|
|
3412
|
+
#[test]
|
|
3413
|
+
fn an_open_may_ask_for_synchronized_writes() {
|
|
3414
|
+
assert_eq!(
|
|
3415
|
+
open_flagged(FILE, 0, FDFLAGS_SYNC).0,
|
|
3416
|
+
0,
|
|
3417
|
+
"path_open of `file.txt` with FDFLAGS_SYNC beneath a Mount through preview1 must succeed, \
|
|
3418
|
+
as Linux takes O_SYNC"
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
// @behavior RT-092
|
|
3423
|
+
#[test]
|
|
3424
|
+
fn an_open_of_a_directory_answers_its_other_flags_as_linux_does() {
|
|
3425
|
+
let held = vec!["file.txt".to_string()];
|
|
3426
|
+
assert_eq!(
|
|
3427
|
+
(
|
|
3428
|
+
open_flagged(NEW_AS_DIRECTORY, OFLAGS_DIRECTORY | OFLAGS_CREAT, 0),
|
|
3429
|
+
open_flagged(FILE, OFLAGS_DIRECTORY | OFLAGS_TRUNC, 0),
|
|
3430
|
+
open_flagged(FILE, OFLAGS_DIRECTORY, 0),
|
|
3431
|
+
),
|
|
3432
|
+
(
|
|
3433
|
+
(ERRNO_INVAL, held.clone()),
|
|
3434
|
+
(ERRNO_NOTDIR, held.clone()),
|
|
3435
|
+
(ERRNO_NOTDIR, held)
|
|
3436
|
+
),
|
|
3437
|
+
"path_open with OFLAGS_DIRECTORY beneath a Mount through preview1 must answer EINVAL \
|
|
3438
|
+
with CREAT and ENOTDIR for a file, and make nothing, as Linux does"
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
#[cfg(unix)]
|
|
3443
|
+
const UP: (i32, i32) = (1070, 2);
|
|
3444
|
+
#[cfg(unix)]
|
|
3445
|
+
const THROUGH_UPDIR: (i32, i32) = (1080, 7);
|
|
3446
|
+
#[cfg(unix)]
|
|
3447
|
+
const IN_LINKED: (i32, i32) = (1090, 4);
|
|
3448
|
+
#[cfg(unix)]
|
|
3449
|
+
const TWISTY: (i32, i32) = (1100, 6);
|
|
3450
|
+
#[cfg(unix)]
|
|
3451
|
+
const DANGLE: (i32, i32) = (1110, 6);
|
|
3452
|
+
#[cfg(unix)]
|
|
3453
|
+
const DIR_UP: (i32, i32) = (1120, 6);
|
|
3454
|
+
#[cfg(unix)]
|
|
3455
|
+
const NOWHERE: (i32, i32) = (1130, 7);
|
|
3456
|
+
#[cfg(unix)]
|
|
3457
|
+
const OUT_OF_IN: (i32, i32) = (1160, 20);
|
|
3458
|
+
#[cfg(unix)]
|
|
3459
|
+
const LOOP: (i32, i32) = (1184, 4);
|
|
3460
|
+
#[cfg(unix)]
|
|
3461
|
+
const DIR_SUB: (i32, i32) = (1190, 7);
|
|
3462
|
+
#[cfg(unix)]
|
|
3463
|
+
const DIR_IN_Y: (i32, i32) = (1400, 8);
|
|
3464
|
+
#[cfg(unix)]
|
|
3465
|
+
const DIR_SUB_Y: (i32, i32) = (1420, 9);
|
|
3466
|
+
|
|
3467
|
+
/// A host directory whose `dir` holds links staying within it (`in`,
|
|
3468
|
+
/// `twisty`), links leading out of it (`up`, `updir`), links pointing at
|
|
3469
|
+
/// nothing out of it (`dangle`) and within it (`nowhere`), and a link
|
|
3470
|
+
/// pointing at itself (`loop`).
|
|
3471
|
+
#[cfg(unix)]
|
|
3472
|
+
struct LinkedBeneath(std::path::PathBuf);
|
|
3473
|
+
|
|
3474
|
+
#[cfg(unix)]
|
|
3475
|
+
impl LinkedBeneath {
|
|
3476
|
+
fn new(name: &str) -> Self {
|
|
3477
|
+
let host =
|
|
3478
|
+
std::env::temp_dir().join(format!("kobako-beneath-{name}-{}", std::process::id()));
|
|
3479
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
3480
|
+
std::fs::create_dir_all(host.join("dir/sub/inner"))
|
|
3481
|
+
.expect("the host directory must be created");
|
|
3482
|
+
std::fs::create_dir_all(host.join("other")).expect("the host directory must be created");
|
|
3483
|
+
for file in ["secret", "other/x", "dir/sub/y"] {
|
|
3484
|
+
std::fs::write(host.join(file), "x").expect("the host file must be written");
|
|
3485
|
+
}
|
|
3486
|
+
for (target, link) in [
|
|
3487
|
+
("../secret", "dir/up"),
|
|
3488
|
+
("../other", "dir/updir"),
|
|
3489
|
+
("sub", "dir/in"),
|
|
3490
|
+
("sub/../sub/y", "dir/twisty"),
|
|
3491
|
+
("../escaped", "dir/dangle"),
|
|
3492
|
+
("missing", "dir/nowhere"),
|
|
3493
|
+
("loop", "dir/loop"),
|
|
3494
|
+
] {
|
|
3495
|
+
std::os::unix::fs::symlink(target, host.join(link)).expect("the link must be made");
|
|
3496
|
+
}
|
|
3497
|
+
Self(host)
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
/// A guest over this host directory as a Bind Mount, or as an
|
|
3501
|
+
/// Overlay's lower Bind Layer beneath an empty upper.
|
|
3502
|
+
fn guest(&self, overlay: bool) -> Guest {
|
|
3503
|
+
let directory = Arc::new(Bind::new(&self.0).expect("the host directory must open"));
|
|
3504
|
+
let filesystem: Arc<dyn FileSystemType> = if overlay {
|
|
3505
|
+
Arc::new(
|
|
3506
|
+
Overlay::new(
|
|
3507
|
+
vec![Filesystem::new(directory).expect("the instance must stand")],
|
|
3508
|
+
Some(Filesystem::new(Arc::new(Tmpfs::new())).expect("the instance must stand")),
|
|
3509
|
+
)
|
|
3510
|
+
.expect("an upper over a lower Layer must make a view"),
|
|
3511
|
+
)
|
|
3512
|
+
} else {
|
|
3513
|
+
directory
|
|
3514
|
+
};
|
|
3515
|
+
Guest::new(&[grant(filesystem, Access::ReadWrite, None)])
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
#[cfg(unix)]
|
|
3520
|
+
impl Drop for LinkedBeneath {
|
|
3521
|
+
fn drop(&mut self) {
|
|
3522
|
+
let _ = std::fs::remove_dir_all(&self.0);
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
// Each link names a real file outside `dir` yet inside the Mount, so only
|
|
3527
|
+
// a link kept within the descriptor, not the Mount's root, is refused.
|
|
3528
|
+
// @behavior RT-140
|
|
3529
|
+
#[cfg(unix)]
|
|
3530
|
+
#[test]
|
|
3531
|
+
fn a_link_leading_out_of_an_opened_directory_escapes_it() {
|
|
3532
|
+
let host = LinkedBeneath::new("out");
|
|
3533
|
+
let answers = |overlay| {
|
|
3534
|
+
let mut guest = host.guest(overlay);
|
|
3535
|
+
[
|
|
3536
|
+
guest.call("open_following_beneath_dir", UP),
|
|
3537
|
+
guest.call("stat_following_beneath_dir", UP),
|
|
3538
|
+
guest.call("open_beneath_dir", THROUGH_UPDIR),
|
|
3539
|
+
guest.call("stat_following_beneath_dir", THROUGH_UPDIR),
|
|
3540
|
+
guest.call("create_following_beneath_dir", DANGLE),
|
|
3541
|
+
]
|
|
3542
|
+
};
|
|
3543
|
+
assert_eq!(
|
|
3544
|
+
(
|
|
3545
|
+
answers(false),
|
|
3546
|
+
answers(true),
|
|
3547
|
+
host.0.join("escaped").exists()
|
|
3548
|
+
),
|
|
3549
|
+
([ERRNO_NOTCAPABLE; 5], [ERRNO_NOTCAPABLE; 5], false),
|
|
3550
|
+
"an open, a stat, or a create through preview1 of a path whose link leads out of a \
|
|
3551
|
+
directory opened beneath a Bind Mount, or an Overlay's Bind Layer, must answer \
|
|
3552
|
+
ENOTCAPABLE, as a `..` leading out does, and create nothing"
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// The second `..` leaves `sub`, where `in` led, two names after the link, so
|
|
3557
|
+
// only a walk standing where the link led reaches `sub/y` from `dir`.
|
|
3558
|
+
// @behavior RT-119
|
|
3559
|
+
#[cfg(unix)]
|
|
3560
|
+
#[test]
|
|
3561
|
+
fn a_step_out_of_where_a_link_led_lands_beside_it() {
|
|
3562
|
+
let host = LinkedBeneath::new("led");
|
|
3563
|
+
let answers = |overlay| host.guest(overlay).call("open_beneath_dir", OUT_OF_IN);
|
|
3564
|
+
assert_eq!(
|
|
3565
|
+
(answers(false), answers(true)),
|
|
3566
|
+
(0, 0),
|
|
3567
|
+
"an open through preview1 of `in/inner/../../sub/y`, where `in` is a link within a \
|
|
3568
|
+
directory opened beneath a Bind Mount or an Overlay's Bind Layer, must reach the file \
|
|
3569
|
+
beside where the link points, as Linux's walk does"
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
// A Policy lets no directory move, and each side of the rename is walked
|
|
3574
|
+
// on its own, so only a rename recognised as onto its own path, rather
|
|
3575
|
+
// than by the directory each walk opened, changes nothing and succeeds.
|
|
3576
|
+
// @behavior RT-155
|
|
3577
|
+
#[cfg(unix)]
|
|
3578
|
+
#[test]
|
|
3579
|
+
fn a_directory_renamed_onto_its_own_path_beneath_a_policy_stays() {
|
|
3580
|
+
let host = LinkedBeneath::new("onto-itself");
|
|
3581
|
+
let directory = Arc::new(Bind::new(&host.0).expect("the host directory must open"));
|
|
3582
|
+
let mut guest = Guest::new(&[grant(
|
|
3583
|
+
directory,
|
|
3584
|
+
Access::ReadWrite,
|
|
3585
|
+
Some(Arc::new(AllowsAll)),
|
|
3586
|
+
)]);
|
|
3587
|
+
assert_eq!(
|
|
3588
|
+
(
|
|
3589
|
+
guest.call_pair("rename_paths", DIR_SUB, DIR_SUB),
|
|
3590
|
+
host.0.join("dir/sub/y").is_file()
|
|
3591
|
+
),
|
|
3592
|
+
(0, true),
|
|
3593
|
+
"path_rename of `dir/sub` onto itself beneath a Bind Mount with a Policy through \
|
|
3594
|
+
preview1 must change nothing and succeed, as rename(2) does"
|
|
3595
|
+
);
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
// The two paths differ, so only the walk ending on one dentry tells the
|
|
3599
|
+
// rename it is onto itself; otherwise it waits on that name's lock twice.
|
|
3600
|
+
// @behavior RT-168
|
|
3601
|
+
#[cfg(unix)]
|
|
3602
|
+
#[test]
|
|
3603
|
+
fn a_file_renamed_onto_itself_through_a_link_stays() {
|
|
3604
|
+
let host = LinkedBeneath::new("onto-itself-linked");
|
|
3605
|
+
let mut guest = host.guest(false);
|
|
3606
|
+
assert_eq!(
|
|
3607
|
+
(
|
|
3608
|
+
guest.call_pair("rename_paths", DIR_IN_Y, DIR_SUB_Y),
|
|
3609
|
+
host.0.join("dir/sub/y").is_file()
|
|
3610
|
+
),
|
|
3611
|
+
(0, true),
|
|
3612
|
+
"path_rename of `dir/in/y` onto `dir/sub/y`, where `in` links to `sub`, beneath a Bind \
|
|
3613
|
+
Mount through preview1 must change nothing and succeed, as rename(2) does"
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
// @behavior RT-154
|
|
3618
|
+
#[cfg(unix)]
|
|
3619
|
+
#[test]
|
|
3620
|
+
fn a_link_loop_answers_eloop() {
|
|
3621
|
+
let host = LinkedBeneath::new("loop");
|
|
3622
|
+
let answers = |overlay| host.guest(overlay).call("open_following_beneath_dir", LOOP);
|
|
3623
|
+
assert_eq!(
|
|
3624
|
+
(answers(false), answers(true)),
|
|
3625
|
+
(ERRNO_LOOP, ERRNO_LOOP),
|
|
3626
|
+
"an open through preview1 following a link pointing at itself, beneath a Directory \
|
|
3627
|
+
Mount or an Overlay's Bind Layer, must answer ELOOP"
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
// @behavior RT-141
|
|
3632
|
+
#[cfg(unix)]
|
|
3633
|
+
#[test]
|
|
3634
|
+
fn a_link_staying_within_an_opened_directory_is_followed() {
|
|
3635
|
+
let host = LinkedBeneath::new("in");
|
|
3636
|
+
let answers = |overlay| {
|
|
3637
|
+
let mut guest = host.guest(overlay);
|
|
3638
|
+
[
|
|
3639
|
+
guest.call("open_following_beneath_dir", IN_LINKED),
|
|
3640
|
+
guest.call("open_following_beneath_dir", TWISTY),
|
|
3641
|
+
]
|
|
3642
|
+
};
|
|
3643
|
+
assert_eq!(
|
|
3644
|
+
(answers(false), answers(true)),
|
|
3645
|
+
([0; 2], [0; 2]),
|
|
3646
|
+
"an open through preview1 of a path whose links stay within a directory opened beneath \
|
|
3647
|
+
a Bind Mount, or an Overlay's Bind Layer, must reach the file they lead to"
|
|
3648
|
+
);
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
// An Overlay's change lands in its upper, so only a Bind shows it on the host.
|
|
3652
|
+
// @behavior RT-142
|
|
3653
|
+
#[cfg(unix)]
|
|
3654
|
+
#[test]
|
|
3655
|
+
fn a_create_through_a_dangling_link_beneath_an_opened_directory_makes_its_target() {
|
|
3656
|
+
let answers = |name, overlay| {
|
|
3657
|
+
let host = LinkedBeneath::new(name);
|
|
3658
|
+
let errno = host
|
|
3659
|
+
.guest(overlay)
|
|
3660
|
+
.call("create_following_beneath_dir", NOWHERE);
|
|
3661
|
+
(errno, host.0.join("dir/missing").exists())
|
|
3662
|
+
};
|
|
3663
|
+
assert_eq!(
|
|
3664
|
+
(answers("dangle", false), answers("dangle-overlay", true)),
|
|
3665
|
+
((0, true), (0, false)),
|
|
3666
|
+
"an open through preview1 creating a file through a link pointing at nothing beneath a \
|
|
3667
|
+
directory opened beneath a Bind Mount must make the file where the link points, as \
|
|
3668
|
+
Linux's open does, and beneath an Overlay's Bind Layer must leave that Layer as it was"
|
|
3669
|
+
);
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
// The link names a file outside the host directory by an absolute path, so
|
|
3673
|
+
// the walk refuses it before any host call is made.
|
|
3674
|
+
// @behavior RT-143
|
|
3675
|
+
#[cfg(unix)]
|
|
3676
|
+
#[test]
|
|
3677
|
+
fn a_link_leading_out_of_a_mount_escapes_it() {
|
|
3678
|
+
let host = LinkedBeneath::new("mount");
|
|
3679
|
+
let outside =
|
|
3680
|
+
std::env::temp_dir().join(format!("kobako-beneath-outside-{}", std::process::id()));
|
|
3681
|
+
std::fs::write(&outside, "host").expect("the outside file must be written");
|
|
3682
|
+
std::fs::remove_file(host.0.join("dir/up")).expect("the link must be removed");
|
|
3683
|
+
std::os::unix::fs::symlink(&outside, host.0.join("dir/up")).expect("the link must be made");
|
|
3684
|
+
let errno = host.guest(false).call("open_following", DIR_UP);
|
|
3685
|
+
let _ = std::fs::remove_file(&outside);
|
|
3686
|
+
assert_eq!(
|
|
3687
|
+
errno, ERRNO_NOTCAPABLE,
|
|
3688
|
+
"an open through preview1 from a Bind Mount's root of a link to a file outside its \
|
|
3689
|
+
host directory must answer ENOTCAPABLE, as an absolute path does"
|
|
3690
|
+
);
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// Each file is open but lacks the access its call needs, so only the order
|
|
3694
|
+
// of the bound and the access check decides between a trap and EBADF.
|
|
3695
|
+
// @behavior RT-144
|
|
3696
|
+
#[test]
|
|
3697
|
+
fn a_files_access_is_checked_before_a_buffer_outside_memory_is_bounded() {
|
|
3698
|
+
let call = |name, rights| {
|
|
3699
|
+
Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)])
|
|
3700
|
+
.try_call_at(name, rights)
|
|
3701
|
+
};
|
|
3702
|
+
assert_eq!(
|
|
3703
|
+
[
|
|
3704
|
+
call("read_far_with", RIGHTS_FD_WRITE),
|
|
3705
|
+
call("write_far_with", RIGHTS_FD_READ),
|
|
3706
|
+
call("pwrite_far_with", RIGHTS_FD_READ),
|
|
3707
|
+
call("pread_far_with", RIGHTS_FD_WRITE),
|
|
3708
|
+
],
|
|
3709
|
+
[Some(ERRNO_BADF); 4],
|
|
3710
|
+
"through preview1 on a file opened without the access each call needs, with a buffer \
|
|
3711
|
+
past memory, fd_read, fd_write, fd_pwrite, and fd_pread must answer EBADF, as Linux \
|
|
3712
|
+
asks f_mode before it touches the buffer"
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
#[cfg(unix)]
|
|
3717
|
+
const PUB_X: (i32, i32) = (1150, 5);
|
|
3718
|
+
|
|
3719
|
+
/// A Policy allowing `/data/pub/` alone, which swaps `pub` for a link to
|
|
3720
|
+
/// `secret` the first time it is asked, as another writer could between
|
|
3721
|
+
/// its judgment and the call.
|
|
3722
|
+
#[cfg(unix)]
|
|
3723
|
+
struct SwapsWhileAsked(std::path::PathBuf, std::sync::atomic::AtomicBool);
|
|
3724
|
+
|
|
3725
|
+
#[cfg(unix)]
|
|
3726
|
+
impl Policy for SwapsWhileAsked {
|
|
3727
|
+
fn allows(&self, path: &str, _access: Access) -> bool {
|
|
3728
|
+
if self.1.swap(false, Ordering::SeqCst) {
|
|
3729
|
+
std::fs::rename(self.0.join("pub"), self.0.join("pub_old")).expect("pub must move");
|
|
3730
|
+
std::os::unix::fs::symlink("secret", self.0.join("pub"))
|
|
3731
|
+
.expect("the link must be made");
|
|
3732
|
+
}
|
|
3733
|
+
path.starts_with("/data/pub/")
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
// The swap happens inside the Policy's own judgment, after the walk opened
|
|
3738
|
+
// `pub`, so only a call acting on the directory it walked to, rather than
|
|
3739
|
+
// on the path again, keeps from reading `secret/x`.
|
|
3740
|
+
// @behavior RT-145
|
|
3741
|
+
#[cfg(unix)]
|
|
3742
|
+
#[test]
|
|
3743
|
+
fn a_link_swapped_in_after_a_policy_judges_a_path_is_not_followed() {
|
|
3744
|
+
let host = std::env::temp_dir().join(format!("kobako-swapped-{}", std::process::id()));
|
|
3745
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
3746
|
+
for dir in ["pub", "secret"] {
|
|
3747
|
+
std::fs::create_dir_all(host.join(dir)).expect("the host directory must be created");
|
|
3748
|
+
std::fs::write(host.join(dir).join("x"), dir).expect("the host file must be written");
|
|
3749
|
+
}
|
|
3750
|
+
let policy = SwapsWhileAsked(host.clone(), std::sync::atomic::AtomicBool::new(true));
|
|
3751
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
3752
|
+
let mut guest = Guest::new(&[grant(
|
|
3753
|
+
Arc::new(directory),
|
|
3754
|
+
Access::Read,
|
|
3755
|
+
Some(Arc::new(policy)),
|
|
3756
|
+
)]);
|
|
3757
|
+
let answer = (guest.call("read_file", PUB_X), guest.read_back());
|
|
3758
|
+
let _ = std::fs::remove_dir_all(&host);
|
|
3759
|
+
assert_eq!(
|
|
3760
|
+
answer,
|
|
3761
|
+
(0, b"pub".to_vec()),
|
|
3762
|
+
"a read through preview1 beneath a Bind Mount with a Policy, of a path whose \
|
|
3763
|
+
directory another writer swaps for a link as the Policy judges it, must read the \
|
|
3764
|
+
directory the walk held and nothing where the link points"
|
|
3765
|
+
);
|
|
3766
|
+
}
|