kobako 0.29.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 +217 -10
- data/Cargo.lock +139 -141
- data/README.md +96 -574
- data/ROADMAP.md +2 -1
- data/crates/kobako-runtime/CHANGELOG.md +269 -1
- data/crates/kobako-runtime/Cargo.toml +25 -15
- data/crates/kobako-runtime/README.md +7 -3
- data/crates/kobako-runtime/src/dispatch.rs +1 -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/{directory/spelling.rs → bind/lookup.rs} +91 -49
- 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 +227 -734
- data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
- data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
- 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 +21 -387
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +11 -6
- 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 +31 -104
- data/crates/kobako-runtime/src/profile.rs +137 -1
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- 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/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 +8 -2
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- 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 +2 -2
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/request.rs +2 -2
- 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 +154 -8
- data/crates/kobako-wasmtime/Cargo.toml +15 -14
- data/crates/kobako-wasmtime/README.md +9 -5
- data/crates/kobako-wasmtime/src/abi.rs +12 -20
- data/crates/kobako-wasmtime/src/ambient.rs +1 -2
- data/crates/kobako-wasmtime/src/cache.rs +5 -5
- data/crates/kobako-wasmtime/src/config.rs +22 -7
- data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
- data/crates/kobako-wasmtime/src/driver.rs +29 -96
- 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 +14 -8
- data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
- data/crates/kobako-wasmtime/src/invocation.rs +26 -46
- data/crates/kobako-wasmtime/src/lib.rs +1 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
- data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
- data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
- data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
- data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
- data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
- data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
- 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 +464 -413
- data/crates/kobako-wasmtime/src/trap.rs +9 -9
- data/crates/kobako-wasmtime/src/wasi.rs +53 -37
- data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -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 +1 -1
- data/ext/kobako/src/runtime/bridge.rs +1 -1
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +28 -14
- data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
- data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
- data/ext/kobako/src/runtime/filesystem.rs +109 -137
- data/ext/kobako/src/runtime/gvl.rs +43 -8
- data/ext/kobako/src/runtime/snapshot.rs +1 -1
- data/ext/kobako/src/runtime.rs +6 -1
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -2
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +2 -10
- data/lib/kobako/catalog/services.rb +2 -2
- data/lib/kobako/catalog/snippets.rb +7 -7
- data/lib/kobako/catalog.rb +1 -1
- data/lib/kobako/codec/decoder.rb +10 -10
- data/lib/kobako/codec/ext_types.rb +3 -5
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +3 -3
- data/lib/kobako/context.rb +5 -5
- data/lib/kobako/filesystem.rb +44 -13
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +4 -4
- data/lib/kobako/sandbox.rb +28 -28
- data/lib/kobako/sandbox_options.rb +2 -3
- data/lib/kobako/snippet/bytecode.rb +1 -1
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/transport/dispatcher.rb +5 -5
- data/lib/kobako/transport/exposure.rb +2 -2
- data/lib/kobako/transport/reflection.rb +7 -7
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +1 -1
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +34 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +0 -4
- data/sig/kobako/catalog/services.rbs +1 -1
- data/sig/kobako/catalog/snippets.rbs +3 -3
- data/sig/kobako/catalog.rbs +1 -1
- 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/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +1 -1
- data/sig/kobako/codec.rbs +1 -2
- data/sig/kobako/context.rbs +1 -1
- data/sig/kobako/filesystem.rbs +19 -14
- data/sig/kobako/guest_yielder.rbs +1 -1
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +2 -2
- data/sig/kobako/runtime.rbs +1 -1
- data/sig/kobako/sandbox.rbs +3 -5
- data/sig/kobako/sandbox_options.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +2 -2
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +2 -2
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport.rbs +1 -1
- metadata +67 -19
- data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
- data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
- data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
- data/crates/kobako-wasmtime/src/io.rs +0 -89
- data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
- /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
- /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
|
@@ -2,86 +2,83 @@
|
|
|
2
2
|
//! path, the files a Filesystem holds, and the refusals the runtime
|
|
3
3
|
//! answers before the Filesystem is asked.
|
|
4
4
|
|
|
5
|
+
mod wrap;
|
|
6
|
+
|
|
5
7
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
6
|
-
use std::sync::Arc;
|
|
8
|
+
use std::sync::{Arc, Mutex, Weak};
|
|
9
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
7
10
|
|
|
8
|
-
use kobako_runtime::filesystem::
|
|
11
|
+
use kobako_runtime::filesystem::Bind;
|
|
12
|
+
use kobako_runtime::filesystem::Filesystem;
|
|
9
13
|
use kobako_runtime::filesystem::{
|
|
10
|
-
|
|
11
|
-
Overlay,
|
|
14
|
+
FileOperations, FileSystemType, FileType, FilesystemError, InodeOperations, Kiocb, Kstat,
|
|
15
|
+
Lookup, OpenHow, Overlay, RenameFlag, Tmpfs,
|
|
12
16
|
};
|
|
13
17
|
use kobako_runtime::grant::Grant;
|
|
14
|
-
use kobako_runtime::mount::{Mount, Policy};
|
|
18
|
+
use kobako_runtime::mount::{Access, Mount, Policy};
|
|
15
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};
|
|
16
22
|
use wasmtime::{Instance, Module, Store};
|
|
17
23
|
|
|
18
|
-
use crate::cache::
|
|
24
|
+
use crate::cache::load_engine;
|
|
19
25
|
use crate::config::Config;
|
|
20
26
|
use crate::imports;
|
|
21
27
|
use crate::invocation::Invocation;
|
|
22
28
|
use crate::trap::NO_TIMEOUT_EPOCH_DELTA;
|
|
23
29
|
use crate::wasi::Wasi;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
|
|
37
|
-
None
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/// The answer of a test Filesystem matching names byte for byte.
|
|
43
|
-
macro_rules! matches_names_exactly {
|
|
44
|
-
() => {
|
|
45
|
-
fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
|
|
46
|
-
Ok(path.to_string())
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/// The answer of a test Filesystem whose identities never repeat.
|
|
52
|
-
macro_rules! never_repeats_identities {
|
|
53
|
-
() => {
|
|
54
|
-
fn hold_directory(&self, _path: &str) -> Result<Option<Hold>, FilesystemError> {
|
|
55
|
-
Ok(None)
|
|
56
|
-
}
|
|
57
|
-
};
|
|
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())
|
|
58
42
|
}
|
|
59
43
|
|
|
60
|
-
/// The
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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)
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
const ERRNO_ACCES: i32 = 2;
|
|
70
63
|
const ERRNO_BADF: i32 = 8;
|
|
71
64
|
const ERRNO_BUSY: i32 = 10;
|
|
72
|
-
const
|
|
65
|
+
const ERRNO_MFILE: i32 = 33;
|
|
73
66
|
const ERRNO_INVAL: i32 = 28;
|
|
74
67
|
const ERRNO_IO: i32 = 29;
|
|
75
68
|
const ERRNO_ISDIR: i32 = 31;
|
|
69
|
+
#[cfg(unix)]
|
|
76
70
|
const ERRNO_LOOP: i32 = 32;
|
|
77
71
|
const ERRNO_NOENT: i32 = 44;
|
|
72
|
+
const ERRNO_NOMEM: i32 = 48;
|
|
78
73
|
const ERRNO_NOTDIR: i32 = 54;
|
|
74
|
+
const ERRNO_NOTEMPTY: i32 = 55;
|
|
75
|
+
const ERRNO_NOSPC: i32 = 51;
|
|
79
76
|
const ERRNO_NOTSUP: i32 = 58;
|
|
80
77
|
const ERRNO_ROFS: i32 = 69;
|
|
81
78
|
const ERRNO_XDEV: i32 = 75;
|
|
82
79
|
const ERRNO_NOTCAPABLE: i32 = 76;
|
|
83
80
|
|
|
84
|
-
///
|
|
81
|
+
/// Tmpfs holds an `iovec` at 64 over 512..576, a `ciovec` at 80 over
|
|
85
82
|
/// "hello" at 600, the paths `file.txt` at 700 and `../file.txt` at 720,
|
|
86
83
|
/// and from 740 the paths the resolution rules turn on. An opened
|
|
87
84
|
/// descriptor lands at 128, a count at 132, a listing's count at 136, a
|
|
@@ -92,6 +89,7 @@ const PROBE_WAT: &str = r#"
|
|
|
92
89
|
(import "wasi_snapshot_preview1" "fd_prestat_dir_name" (func $prestat_name (param i32 i32 i32) (result i32)))
|
|
93
90
|
(import "wasi_snapshot_preview1" "path_open" (func $open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
|
|
94
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)))
|
|
95
93
|
(import "wasi_snapshot_preview1" "fd_read" (func $read (param i32 i32 i32 i32) (result i32)))
|
|
96
94
|
(import "wasi_snapshot_preview1" "fd_write" (func $write (param i32 i32 i32 i32) (result i32)))
|
|
97
95
|
(import "wasi_snapshot_preview1" "path_remove_directory" (func $rmdir (param i32 i32 i32) (result i32)))
|
|
@@ -108,6 +106,9 @@ const PROBE_WAT: &str = r#"
|
|
|
108
106
|
(import "wasi_snapshot_preview1" "fd_fdstat_get" (func $fdstat (param i32 i32) (result i32)))
|
|
109
107
|
(import "wasi_snapshot_preview1" "fd_fdstat_set_flags" (func $set_flags (param i32 i32) (result i32)))
|
|
110
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)))
|
|
111
112
|
(memory (export "memory") 1)
|
|
112
113
|
(data (i32.const 740) ".")
|
|
113
114
|
(data (i32.const 760) "missing/../file.txt")
|
|
@@ -137,6 +138,12 @@ const PROBE_WAT: &str = r#"
|
|
|
137
138
|
(data (i32.const 1120) "dir/up")
|
|
138
139
|
(data (i32.const 1130) "nowhere")
|
|
139
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")
|
|
140
147
|
;; An iovec naming 16 bytes past the one page of memory.
|
|
141
148
|
(data (i32.const 1140) "\00\00\02\00\10\00\00\00")
|
|
142
149
|
(data (i32.const 64) "\00\02\00\00\40\00\00\00")
|
|
@@ -159,6 +166,14 @@ const PROBE_WAT: &str = r#"
|
|
|
159
166
|
(if (local.get $errno) (then (return (local.get $errno))))
|
|
160
167
|
(call $read (i32.load (i32.const 128)) (i32.const 64) (i32.const 1) (i32.const 132)))
|
|
161
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
|
+
|
|
162
177
|
;; Open the path with read rights only, asking to truncate it.
|
|
163
178
|
(func (export "truncate_file") (param $path i32) (param $len i32) (result i32)
|
|
164
179
|
(call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
|
|
@@ -181,6 +196,14 @@ const PROBE_WAT: &str = r#"
|
|
|
181
196
|
(func (export "stat_flagged") (param $path i32) (param $len i32) (param $flags i32) (result i32)
|
|
182
197
|
(call $stat (i32.const 3) (local.get $flags) (local.get $path) (local.get $len) (i32.const 384)))
|
|
183
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
|
+
|
|
184
207
|
;; Change the root `.` at 740.
|
|
185
208
|
(func (export "remove_root") (result i32)
|
|
186
209
|
(call $rmdir (i32.const 3) (i32.const 740) (i32.const 1)))
|
|
@@ -204,9 +227,6 @@ const PROBE_WAT: &str = r#"
|
|
|
204
227
|
(if (local.get $errno) (then (return (local.get $errno))))
|
|
205
228
|
(call $allocate (i32.load (i32.const 128)) (i64.const 0) (i64.const 100)))
|
|
206
229
|
|
|
207
|
-
;; Rename `file.txt` to `new.txt`.
|
|
208
|
-
(func (export "rename_to_new") (result i32)
|
|
209
|
-
(call $rename (i32.const 3) (i32.const 700) (i32.const 8) (i32.const 3) (i32.const 840) (i32.const 7)))
|
|
210
230
|
;; Rename `file.txt` onto `file.txt/`, its own path as a directory.
|
|
211
231
|
(func (export "rename_onto_itself_as_directory") (result i32)
|
|
212
232
|
(call $rename (i32.const 3) (i32.const 700) (i32.const 8) (i32.const 3) (i32.const 800) (i32.const 9)))
|
|
@@ -293,9 +313,39 @@ const PROBE_WAT: &str = r#"
|
|
|
293
313
|
(func (export "dir_fdstat") (result i32)
|
|
294
314
|
(local $errno i32)
|
|
295
315
|
(local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
|
|
296
|
-
(i32.const 2) (i64.const
|
|
316
|
+
(i32.const 2) (i64.const 0x4002) (i64.const 2) (i32.const 0) (i32.const 128)))
|
|
297
317
|
(if (local.get $errno) (then (return (local.get $errno))))
|
|
298
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)))
|
|
299
349
|
;; Size `file.txt` to `offset` bytes.
|
|
300
350
|
(func (export "size_at") (param $offset i64) (result i32)
|
|
301
351
|
(local $errno i32)
|
|
@@ -345,7 +395,7 @@ const PROBE_WAT: &str = r#"
|
|
|
345
395
|
(func (export "open_beneath_dir") (param $path i32) (param $len i32) (result i32)
|
|
346
396
|
(local $errno i32)
|
|
347
397
|
(local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
|
|
348
|
-
(i32.const 2) (i64.const
|
|
398
|
+
(i32.const 2) (i64.const 0x4002) (i64.const 2) (i32.const 0) (i32.const 128)))
|
|
349
399
|
(if (local.get $errno) (then (return (local.get $errno))))
|
|
350
400
|
(call $open (i32.load (i32.const 128)) (i32.const 0) (local.get $path) (local.get $len)
|
|
351
401
|
(i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 132)))
|
|
@@ -372,10 +422,20 @@ const PROBE_WAT: &str = r#"
|
|
|
372
422
|
;; Open `dir` as a directory into 128.
|
|
373
423
|
(func $open_dir (export "open_dir") (result i32)
|
|
374
424
|
(call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
|
|
375
|
-
(i32.const 2) (i64.const
|
|
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)))
|
|
376
432
|
;; Stat `inner` beneath the directory opened into 128.
|
|
377
433
|
(func (export "stat_inner") (result i32)
|
|
378
|
-
(call $stat (i32.load (i32.const 128)) (i32.const 0) (i32.const 960) (i32.const 5) (i32.const 384)))
|
|
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))))
|
|
379
439
|
"#;
|
|
380
440
|
|
|
381
441
|
const FILE: (i32, i32) = (700, 8);
|
|
@@ -385,6 +445,7 @@ const THROUGH_MISSING: (i32, i32) = (760, 19);
|
|
|
385
445
|
const OUT_OF_FILE: (i32, i32) = (780, 11);
|
|
386
446
|
const FILE_AS_DIRECTORY: (i32, i32) = (800, 9);
|
|
387
447
|
const NEW_AS_DIRECTORY: (i32, i32) = (820, 8);
|
|
448
|
+
#[cfg(unix)]
|
|
388
449
|
const THROUGH_FILE: (i32, i32) = (860, 12);
|
|
389
450
|
const DIR_DOT: (i32, i32) = (880, 5);
|
|
390
451
|
const DIR_DOT_DOT: (i32, i32) = (890, 6);
|
|
@@ -392,29 +453,38 @@ const BENEATH_FILE: (i32, i32) = (900, 10);
|
|
|
392
453
|
const MOVED: (i32, i32) = (920, 5);
|
|
393
454
|
const DIR: (i32, i32) = (930, 3);
|
|
394
455
|
const MISSING_DOT: (i32, i32) = (940, 9);
|
|
456
|
+
#[cfg(unix)]
|
|
395
457
|
const LINK_DOT: (i32, i32) = (970, 10);
|
|
458
|
+
#[cfg(unix)]
|
|
396
459
|
const OUT_OF_LINK: (i32, i32) = (990, 20);
|
|
397
460
|
const HOLDING_NUL: (i32, i32) = (1010, 9);
|
|
461
|
+
const NOT_UTF8: (i32, i32) = (1440, 1);
|
|
398
462
|
const DIR_FILE: (i32, i32) = (1030, 12);
|
|
463
|
+
#[cfg(unix)]
|
|
399
464
|
const DIR_AS_DIRECTORY: (i32, i32) = (1050, 4);
|
|
465
|
+
#[cfg(unix)]
|
|
400
466
|
const GONE_AS_DIRECTORY: (i32, i32) = (1060, 5);
|
|
401
|
-
const ERRNO_OVERFLOW: i32 = 61;
|
|
402
467
|
const ERRNO_EXIST: i32 = 20;
|
|
403
468
|
const OFLAGS_EXCL: i32 = 1 << 2;
|
|
404
469
|
|
|
405
|
-
/// The default `
|
|
406
|
-
fn
|
|
407
|
-
let mut
|
|
408
|
-
set(&mut
|
|
409
|
-
|
|
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
|
|
410
475
|
}
|
|
411
476
|
|
|
412
477
|
fn grant(
|
|
413
|
-
filesystem: Arc<dyn
|
|
478
|
+
filesystem: Arc<dyn FileSystemType>,
|
|
414
479
|
access: Access,
|
|
415
480
|
policy: Option<Arc<dyn Policy>>,
|
|
416
481
|
) -> Grant {
|
|
417
|
-
let mount = Mount::new(
|
|
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");
|
|
418
488
|
Grant::Mount(match policy {
|
|
419
489
|
Some(policy) => mount.with_policy(policy),
|
|
420
490
|
None => mount,
|
|
@@ -429,17 +499,55 @@ struct Guest {
|
|
|
429
499
|
|
|
430
500
|
impl Guest {
|
|
431
501
|
fn new(grants: &[Grant]) -> Self {
|
|
432
|
-
|
|
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");
|
|
433
539
|
let config = Config {
|
|
434
540
|
timeout: None,
|
|
435
541
|
setup_timeout: None,
|
|
436
|
-
memory_limit
|
|
542
|
+
memory_limit,
|
|
437
543
|
stdout_limit: None,
|
|
438
544
|
stderr_limit: None,
|
|
439
|
-
open_file_limit
|
|
440
|
-
profile
|
|
545
|
+
open_file_limit,
|
|
546
|
+
profile,
|
|
547
|
+
security,
|
|
441
548
|
};
|
|
442
|
-
let wasi =
|
|
549
|
+
let wasi =
|
|
550
|
+
Wasi::for_invocation(&config, Vec::new(), grants).expect("WASI context must build");
|
|
443
551
|
let mut store = Store::new(engine, Invocation::new(None, wasi));
|
|
444
552
|
store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
|
|
445
553
|
let module = Module::new(engine, PROBE_WAT).expect("probe module must compile");
|
|
@@ -494,6 +602,15 @@ impl Guest {
|
|
|
494
602
|
.expect("probe call must not trap")
|
|
495
603
|
}
|
|
496
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
|
+
|
|
497
614
|
/// Call an export that takes no path.
|
|
498
615
|
fn call_bare(&mut self, name: &str) -> i32 {
|
|
499
616
|
self.instance
|
|
@@ -503,14 +620,26 @@ impl Guest {
|
|
|
503
620
|
.expect("probe call must not trap")
|
|
504
621
|
}
|
|
505
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
|
+
|
|
506
632
|
/// The names a listing from `cookie` into `len` bytes holds whole.
|
|
507
|
-
fn
|
|
508
|
-
|
|
509
|
-
.instance
|
|
633
|
+
fn call_list(&mut self, cookie: u64, len: i32) -> i32 {
|
|
634
|
+
self.instance
|
|
510
635
|
.get_typed_func::<(i64, i32), i32>(&mut self.store, "list")
|
|
511
636
|
.expect("probe export must resolve")
|
|
512
637
|
.call(&mut self.store, (cookie as i64, len))
|
|
513
|
-
.expect("probe call must not trap")
|
|
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);
|
|
514
643
|
assert_eq!(errno, 0, "the listing must succeed");
|
|
515
644
|
let memory = self
|
|
516
645
|
.instance
|
|
@@ -532,6 +661,36 @@ impl Guest {
|
|
|
532
661
|
names
|
|
533
662
|
}
|
|
534
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
|
+
|
|
535
694
|
/// The bytes the last read placed at 512.
|
|
536
695
|
fn read_back(&mut self) -> Vec<u8> {
|
|
537
696
|
let memory = self
|
|
@@ -544,24 +703,42 @@ impl Guest {
|
|
|
544
703
|
}
|
|
545
704
|
}
|
|
546
705
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
+
};
|
|
551
722
|
let mut buf = vec![0; 64];
|
|
552
|
-
let read = file.
|
|
723
|
+
let read = file.pread(&mut buf, 0).ok()?;
|
|
553
724
|
buf.truncate(read);
|
|
554
725
|
Some(buf)
|
|
555
726
|
}
|
|
556
727
|
|
|
557
728
|
/// Make an empty file at `path`.
|
|
558
|
-
fn overwrite(
|
|
559
|
-
|
|
560
|
-
|
|
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,
|
|
561
736
|
path,
|
|
562
|
-
&
|
|
737
|
+
&flags_of(&how)
|
|
563
738
|
)
|
|
564
|
-
.
|
|
739
|
+
.is_ok(),
|
|
740
|
+
"the file must be made"
|
|
741
|
+
);
|
|
565
742
|
}
|
|
566
743
|
|
|
567
744
|
struct Refuses(&'static str);
|
|
@@ -580,45 +757,58 @@ impl Policy for AllowsAll {
|
|
|
580
757
|
}
|
|
581
758
|
}
|
|
582
759
|
|
|
583
|
-
/// A
|
|
584
|
-
/// any case, as a host
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
self.0.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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(())
|
|
614
793
|
}
|
|
794
|
+
}
|
|
615
795
|
|
|
616
|
-
|
|
617
|
-
|
|
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)
|
|
618
800
|
}
|
|
619
801
|
|
|
620
|
-
fn
|
|
621
|
-
self.
|
|
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
|
+
})
|
|
622
812
|
}
|
|
623
813
|
}
|
|
624
814
|
|
|
@@ -627,10 +817,9 @@ impl Filesystem for StoresUpperCase {
|
|
|
627
817
|
// @behavior RT-120
|
|
628
818
|
#[test]
|
|
629
819
|
fn a_policy_judges_a_path_as_its_filesystem_stores_it() {
|
|
630
|
-
let memory: Memory = [("file.txt", "granted")].into_iter().collect();
|
|
631
820
|
let policy: Arc<dyn Policy> = Arc::new(Refuses("/data/FILE.TXT"));
|
|
632
821
|
let mut guest = Guest::new(&[grant(
|
|
633
|
-
Arc::new(StoresUpperCase(
|
|
822
|
+
Arc::new(StoresUpperCase(&["FILE.TXT"])),
|
|
634
823
|
Access::Read,
|
|
635
824
|
Some(policy),
|
|
636
825
|
)]);
|
|
@@ -642,10 +831,31 @@ fn a_policy_judges_a_path_as_its_filesystem_stores_it() {
|
|
|
642
831
|
);
|
|
643
832
|
}
|
|
644
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
|
+
|
|
645
855
|
// @behavior RT-075
|
|
646
856
|
#[test]
|
|
647
857
|
fn a_mount_appears_as_a_directory_at_its_path() {
|
|
648
|
-
let mut guest = Guest::new(&[grant(Arc::new(
|
|
858
|
+
let mut guest = Guest::new(&[grant(Arc::new(Tmpfs::new()), Access::Read, None)]);
|
|
649
859
|
assert_eq!(
|
|
650
860
|
guest.call_bare("preopen_name"),
|
|
651
861
|
0,
|
|
@@ -662,66 +872,248 @@ fn a_mount_appears_as_a_directory_at_its_path() {
|
|
|
662
872
|
);
|
|
663
873
|
}
|
|
664
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
|
+
|
|
665
974
|
// The first listing fills its buffer with `.` and `..` exactly, so it has
|
|
666
975
|
// not reached its end when the file is made.
|
|
667
976
|
// @behavior RT-107
|
|
668
977
|
#[test]
|
|
669
978
|
fn a_listing_goes_on_over_the_entries_it_began_with() {
|
|
670
|
-
let
|
|
671
|
-
let
|
|
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)]);
|
|
672
982
|
assert_eq!(
|
|
673
983
|
guest.list(0, 51),
|
|
674
984
|
[".", ".."],
|
|
675
985
|
"the first listing must hold the dots"
|
|
676
986
|
);
|
|
677
|
-
|
|
678
|
-
.open(
|
|
679
|
-
"b.txt",
|
|
680
|
-
&build_options(|options| (options.write, options.create) = (true, true)),
|
|
681
|
-
)
|
|
682
|
-
.expect("the file must be made");
|
|
987
|
+
overwrite(&host(&bind), "b.txt");
|
|
683
988
|
assert_eq!(
|
|
684
989
|
guest.list(2, 4096),
|
|
685
990
|
["a.txt"],
|
|
686
991
|
"a listing going on through fd_readdir must read the entries it began with"
|
|
687
992
|
);
|
|
993
|
+
let mut from_the_start = guest.list(0, 4096);
|
|
994
|
+
from_the_start.sort();
|
|
688
995
|
assert_eq!(
|
|
689
|
-
|
|
996
|
+
from_the_start,
|
|
690
997
|
[".", "..", "a.txt", "b.txt"],
|
|
691
998
|
"a listing from the start through fd_readdir must read the directory as it stands"
|
|
692
999
|
);
|
|
693
1000
|
}
|
|
694
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
|
+
|
|
695
1087
|
// @behavior RT-076
|
|
696
1088
|
#[test]
|
|
697
1089
|
fn a_guest_reads_a_file_beneath_a_mount() {
|
|
698
|
-
let
|
|
699
|
-
let mut guest = Guest::new(&[grant(Arc::new(
|
|
1090
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
1091
|
+
let mut guest = Guest::new(&[grant(Arc::new(tmpfs), Access::Read, None)]);
|
|
700
1092
|
assert_eq!(guest.call("read_file", FILE), 0, "the read must succeed");
|
|
701
1093
|
assert_eq!(
|
|
702
1094
|
guest.read_back(),
|
|
703
1095
|
b"granted",
|
|
704
|
-
"a file read beneath a Mount through preview1 must read the
|
|
1096
|
+
"a file read beneath a Mount through preview1 must read the Tmpfs's content"
|
|
705
1097
|
);
|
|
706
1098
|
}
|
|
707
1099
|
|
|
708
1100
|
// @behavior RT-077
|
|
709
1101
|
#[test]
|
|
710
1102
|
fn a_guest_writes_a_file_beneath_a_read_write_mount() {
|
|
711
|
-
let
|
|
712
|
-
let mut guest = Guest::new(&[grant(
|
|
1103
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
1104
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
713
1105
|
assert_eq!(guest.call("write_file", FILE), 0, "the write must succeed");
|
|
714
1106
|
assert_eq!(
|
|
715
|
-
contents(
|
|
1107
|
+
contents(&host(&tmpfs), "file.txt").as_deref(),
|
|
716
1108
|
Some(&b"hello"[..]),
|
|
717
|
-
"a file written beneath a read-write Mount through preview1 must land in the
|
|
1109
|
+
"a file written beneath a read-write Mount through preview1 must land in the Tmpfs"
|
|
718
1110
|
);
|
|
719
1111
|
}
|
|
720
1112
|
|
|
721
1113
|
// @behavior RT-078
|
|
722
1114
|
#[test]
|
|
723
1115
|
fn a_change_beneath_a_read_only_mount_is_a_read_only_filesystem() {
|
|
724
|
-
let mut guest = Guest::new(&[grant(Arc::new(
|
|
1116
|
+
let mut guest = Guest::new(&[grant(Arc::new(Tmpfs::new()), Access::Read, None)]);
|
|
725
1117
|
assert_eq!(
|
|
726
1118
|
guest.call("write_file", FILE),
|
|
727
1119
|
ERRNO_ROFS,
|
|
@@ -735,12 +1127,12 @@ fn a_change_beneath_a_read_only_mount_is_a_read_only_filesystem() {
|
|
|
735
1127
|
// @behavior RT-078
|
|
736
1128
|
#[test]
|
|
737
1129
|
fn a_truncating_open_beneath_a_read_only_mount_is_refused() {
|
|
738
|
-
let
|
|
739
|
-
let mut guest = Guest::new(&[grant(
|
|
1130
|
+
let tmpfs = tmpfs_with("file.txt", "granted");
|
|
1131
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::Read, None)]);
|
|
740
1132
|
assert_eq!(
|
|
741
1133
|
(
|
|
742
1134
|
guest.call("truncate_file", FILE),
|
|
743
|
-
contents(
|
|
1135
|
+
contents(&host(&tmpfs), "file.txt")
|
|
744
1136
|
),
|
|
745
1137
|
(ERRNO_ROFS, Some(b"granted".to_vec())),
|
|
746
1138
|
"a read-rights open with O_TRUNC beneath a read-only Mount through preview1 must answer \
|
|
@@ -751,8 +1143,8 @@ fn a_truncating_open_beneath_a_read_only_mount_is_refused() {
|
|
|
751
1143
|
// @behavior RT-079
|
|
752
1144
|
#[test]
|
|
753
1145
|
fn a_path_above_a_mounts_root_is_refused() {
|
|
754
|
-
let
|
|
755
|
-
let mut guest = Guest::new(&[grant(Arc::new(
|
|
1146
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
1147
|
+
let mut guest = Guest::new(&[grant(Arc::new(tmpfs), Access::Read, None)]);
|
|
756
1148
|
assert_eq!(
|
|
757
1149
|
guest.call("read_file", ABOVE_ROOT),
|
|
758
1150
|
ERRNO_NOTCAPABLE,
|
|
@@ -765,10 +1157,10 @@ fn a_path_above_a_mounts_root_is_refused() {
|
|
|
765
1157
|
// @behavior RT-115
|
|
766
1158
|
#[test]
|
|
767
1159
|
fn a_path_above_the_directory_it_is_opened_beneath_is_refused() {
|
|
768
|
-
let
|
|
1160
|
+
let tmpfs: Tmpfs = [("file.txt", "granted"), ("dir/inner.txt", "beneath")]
|
|
769
1161
|
.into_iter()
|
|
770
1162
|
.collect();
|
|
771
|
-
let mut guest = Guest::new(&[grant(Arc::new(
|
|
1163
|
+
let mut guest = Guest::new(&[grant(Arc::new(tmpfs), Access::Read, None)]);
|
|
772
1164
|
assert_eq!(
|
|
773
1165
|
guest.call("open_beneath_dir", ABOVE_ROOT),
|
|
774
1166
|
ERRNO_NOTCAPABLE,
|
|
@@ -777,136 +1169,277 @@ fn a_path_above_the_directory_it_is_opened_beneath_is_refused() {
|
|
|
777
1169
|
);
|
|
778
1170
|
}
|
|
779
1171
|
|
|
780
|
-
/// What `stat_inner` answers through `dir` opened
|
|
781
|
-
/// the
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
let
|
|
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)]);
|
|
785
1178
|
assert_eq!(
|
|
786
1179
|
guest.call_bare("open_dir"),
|
|
787
1180
|
0,
|
|
788
1181
|
"dir must open as a directory"
|
|
789
1182
|
);
|
|
790
|
-
change(&
|
|
1183
|
+
change(&host(&tmpfs));
|
|
791
1184
|
guest.call_bare("stat_inner")
|
|
792
1185
|
}
|
|
793
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
|
+
|
|
794
1196
|
// A directory made at the path holds an `inner` of its own, so only the
|
|
795
|
-
//
|
|
1197
|
+
// descriptor still standing for the removed one keeps it from reaching it.
|
|
796
1198
|
// @behavior RT-117
|
|
797
1199
|
#[test]
|
|
798
|
-
fn
|
|
799
|
-
let
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
stat_after(|memory| {
|
|
806
|
-
memory.remove_file("dir/inner").unwrap();
|
|
807
|
-
memory.remove_dir("dir").unwrap();
|
|
808
|
-
memory.create_dir("dir").unwrap();
|
|
809
|
-
overwrite(memory, "dir/inner");
|
|
810
|
-
}),
|
|
811
|
-
);
|
|
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
|
+
});
|
|
812
1207
|
assert_eq!(
|
|
813
|
-
|
|
814
|
-
(ERRNO_NOENT, ERRNO_NOENT),
|
|
1208
|
+
answer, ERRNO_NOENT,
|
|
815
1209
|
"path_filestat_get through a directory descriptor beneath a Mount through preview1 must \
|
|
816
|
-
answer ENOENT once
|
|
1210
|
+
answer ENOENT once the directory was removed and another made at its path"
|
|
817
1211
|
);
|
|
818
1212
|
}
|
|
819
1213
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
+
);
|
|
827
1223
|
}
|
|
828
1224
|
|
|
829
|
-
|
|
830
|
-
|
|
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
|
+
}
|
|
831
1248
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
+
);
|
|
836
1261
|
}
|
|
837
1262
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
+
}
|
|
842
1276
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
+
}
|
|
846
1289
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
+
}
|
|
855
1337
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
+
}
|
|
863
1347
|
|
|
864
|
-
|
|
865
|
-
|
|
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)
|
|
866
1352
|
}
|
|
1353
|
+
}
|
|
867
1354
|
|
|
868
|
-
|
|
869
|
-
|
|
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
|
+
})
|
|
870
1366
|
}
|
|
871
1367
|
|
|
872
|
-
fn
|
|
873
|
-
self
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
Ok(()
|
|
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
|
+
})
|
|
878
1381
|
}
|
|
879
1382
|
|
|
880
|
-
|
|
881
|
-
|
|
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(())
|
|
882
1399
|
}
|
|
883
1400
|
|
|
884
|
-
|
|
885
|
-
|
|
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
|
|
886
1416
|
}
|
|
887
1417
|
}
|
|
888
1418
|
|
|
889
|
-
// The Filesystem gives the new `dir` the old one's number unless
|
|
890
|
-
//
|
|
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.
|
|
891
1423
|
// @behavior RT-121
|
|
892
1424
|
#[test]
|
|
893
1425
|
fn a_directory_descriptor_holds_its_directory_while_open() {
|
|
894
|
-
let
|
|
895
|
-
let reuses = Arc::new(ReusesInodes
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
+
)]);
|
|
901
1433
|
assert_eq!(
|
|
902
1434
|
guest.call_bare("open_dir"),
|
|
903
1435
|
0,
|
|
904
1436
|
"dir must open as a directory"
|
|
905
1437
|
);
|
|
906
|
-
reuses
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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");
|
|
910
1443
|
assert_eq!(
|
|
911
1444
|
guest.call_bare("stat_inner"),
|
|
912
1445
|
ERRNO_NOENT,
|
|
@@ -916,16 +1449,121 @@ fn a_directory_descriptor_holds_its_directory_while_open() {
|
|
|
916
1449
|
);
|
|
917
1450
|
}
|
|
918
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
|
+
|
|
919
1557
|
// The open looks the directory up without following a link at its end, so
|
|
920
1558
|
// the identity it remembers comes from the Overlay's lstat, and every later
|
|
921
1559
|
// call compares it with the Overlay's stat.
|
|
922
1560
|
#[test]
|
|
923
1561
|
fn a_directory_two_layers_hold_is_reached_through_its_descriptor() {
|
|
924
|
-
let
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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");
|
|
929
1567
|
let mut guest = Guest::new(&[grant(Arc::new(overlay), Access::ReadWrite, None)]);
|
|
930
1568
|
assert_eq!(
|
|
931
1569
|
(guest.call_bare("open_dir"), guest.call_bare("stat_inner")),
|
|
@@ -938,9 +1576,11 @@ fn a_directory_two_layers_hold_is_reached_through_its_descriptor() {
|
|
|
938
1576
|
// @behavior RT-118
|
|
939
1577
|
#[test]
|
|
940
1578
|
fn a_directory_moved_back_to_its_path_is_reached_through_its_descriptor_again() {
|
|
941
|
-
let answer = stat_after(|
|
|
942
|
-
|
|
943
|
-
|
|
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();
|
|
944
1584
|
});
|
|
945
1585
|
assert_eq!(
|
|
946
1586
|
answer, 0,
|
|
@@ -952,9 +1592,9 @@ fn a_directory_moved_back_to_its_path_is_reached_through_its_descriptor_again()
|
|
|
952
1592
|
// @behavior RT-080
|
|
953
1593
|
#[test]
|
|
954
1594
|
fn a_policy_refusing_an_open_answers_access_denied() {
|
|
955
|
-
let
|
|
1595
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
956
1596
|
let policy: Arc<dyn Policy> = Arc::new(Refuses("/data/file.txt"));
|
|
957
|
-
let mut guest = Guest::new(&[grant(Arc::new(
|
|
1597
|
+
let mut guest = Guest::new(&[grant(Arc::new(tmpfs), Access::Read, Some(policy))]);
|
|
958
1598
|
assert_eq!(
|
|
959
1599
|
guest.call("read_file", FILE),
|
|
960
1600
|
ERRNO_ACCES,
|
|
@@ -964,22 +1604,26 @@ fn a_policy_refusing_an_open_answers_access_denied() {
|
|
|
964
1604
|
|
|
965
1605
|
// @behavior RT-081
|
|
966
1606
|
#[test]
|
|
967
|
-
fn
|
|
968
|
-
let
|
|
1607
|
+
fn a_policy_cannot_widen_a_mounts_access() {
|
|
1608
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
969
1609
|
let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
|
|
970
|
-
let mut guest = Guest::new(&[grant(
|
|
971
|
-
assert_eq!(guest.call("write_file", FILE), 0, "the write must succeed");
|
|
1610
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::Read, Some(policy))]);
|
|
972
1611
|
assert_eq!(
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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"
|
|
976
1619
|
);
|
|
977
1620
|
}
|
|
978
1621
|
|
|
979
1622
|
// @behavior RT-082
|
|
980
1623
|
#[test]
|
|
981
|
-
fn
|
|
982
|
-
let
|
|
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)];
|
|
983
1627
|
assert_eq!(
|
|
984
1628
|
Guest::new(&grants).call("write_file", FILE),
|
|
985
1629
|
0,
|
|
@@ -987,24 +1631,30 @@ fn each_invocation_is_handed_the_filesystem_it_starts_from() {
|
|
|
987
1631
|
);
|
|
988
1632
|
assert_eq!(
|
|
989
1633
|
Guest::new(&grants).call("read_file", FILE),
|
|
990
|
-
|
|
991
|
-
"a file an earlier invocation wrote
|
|
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"
|
|
992
1642
|
);
|
|
993
1643
|
}
|
|
994
1644
|
|
|
995
|
-
/// A
|
|
1645
|
+
/// A Tmpfs holding `file.txt` that counts every change it is asked for,
|
|
996
1646
|
/// so a test can tell the runtime refused before asking it.
|
|
1647
|
+
#[derive(Default)]
|
|
997
1648
|
struct ChangeCounter {
|
|
998
|
-
memory: Memory,
|
|
999
1649
|
changes: AtomicUsize,
|
|
1000
1650
|
}
|
|
1001
1651
|
|
|
1002
1652
|
impl ChangeCounter {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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))
|
|
1008
1658
|
}
|
|
1009
1659
|
|
|
1010
1660
|
fn change(&self) {
|
|
@@ -1012,125 +1662,68 @@ impl ChangeCounter {
|
|
|
1012
1662
|
}
|
|
1013
1663
|
}
|
|
1014
1664
|
|
|
1015
|
-
impl
|
|
1016
|
-
|
|
1017
|
-
matches_names_exactly!();
|
|
1018
|
-
never_repeats_identities!();
|
|
1019
|
-
is_shared!();
|
|
1020
|
-
|
|
1021
|
-
fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
|
|
1022
|
-
self.memory.open(path, options)
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1026
|
-
self.memory.metadata(path)
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1030
|
-
self.memory.read_dir(path)
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1665
|
+
impl Hook for ChangeCounter {
|
|
1666
|
+
fn mkdir(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
1034
1667
|
self.change();
|
|
1035
|
-
|
|
1668
|
+
inner.mkdir(dentry)
|
|
1036
1669
|
}
|
|
1037
1670
|
|
|
1038
|
-
fn
|
|
1671
|
+
fn rmdir(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
1039
1672
|
self.change();
|
|
1040
|
-
|
|
1673
|
+
inner.rmdir(dentry)
|
|
1041
1674
|
}
|
|
1042
1675
|
|
|
1043
|
-
fn
|
|
1676
|
+
fn unlink(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
1044
1677
|
self.change();
|
|
1045
|
-
|
|
1678
|
+
inner.unlink(dentry)
|
|
1046
1679
|
}
|
|
1047
1680
|
|
|
1048
|
-
fn rename(
|
|
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> {
|
|
1049
1689
|
self.change();
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
/// A Memory holding `file.txt` that, as a rename reaches it, replaces the
|
|
1055
|
-
/// file with a directory holding `secret`, as another invocation sharing
|
|
1056
|
-
/// the Memory could between the runtime's checks and the move.
|
|
1057
|
-
struct SwappedForADirectory(Memory);
|
|
1058
|
-
|
|
1059
|
-
impl SwappedForADirectory {
|
|
1060
|
-
fn new() -> Arc<Self> {
|
|
1061
|
-
Arc::new(Self([("file.txt", "granted")].into_iter().collect()))
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
impl Filesystem for SwappedForADirectory {
|
|
1066
|
-
holds_no_links!();
|
|
1067
|
-
matches_names_exactly!();
|
|
1068
|
-
never_repeats_identities!();
|
|
1069
|
-
is_shared!();
|
|
1070
|
-
|
|
1071
|
-
fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
|
|
1072
|
-
self.0.open(path, options)
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1076
|
-
self.0.metadata(path)
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1080
|
-
self.0.read_dir(path)
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1084
|
-
self.0.create_dir(path)
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1088
|
-
self.0.remove_dir(path)
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1092
|
-
self.0.remove_file(path)
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1096
|
-
self.0.remove_file(from)?;
|
|
1097
|
-
self.0.create_dir(from)?;
|
|
1098
|
-
drop(self.0.open(
|
|
1099
|
-
&format!("{from}/secret"),
|
|
1100
|
-
&build_options(|options| (options.write, options.create) = (true, true)),
|
|
1101
|
-
)?);
|
|
1102
|
-
self.0.rename(from, to, rename)
|
|
1690
|
+
inner.rename(from, to_dir, to, flag)
|
|
1103
1691
|
}
|
|
1104
1692
|
}
|
|
1105
1693
|
|
|
1106
1694
|
// The Policy allows everything, so only the rename's own refusal of a
|
|
1107
|
-
// directory
|
|
1108
|
-
// taking a path the Policy never judged.
|
|
1695
|
+
// directory keeps `secret` from taking a path the Policy never judged.
|
|
1109
1696
|
// @behavior RT-094
|
|
1110
1697
|
#[test]
|
|
1111
1698
|
fn a_directory_beneath_a_mount_with_a_policy_does_not_move() {
|
|
1112
|
-
let
|
|
1699
|
+
let tmpfs = Arc::new([("dir/secret", "x")].into_iter().collect::<Tmpfs>());
|
|
1113
1700
|
let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
|
|
1114
|
-
let mut guest = Guest::new(&[grant(
|
|
1701
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, Some(policy))]);
|
|
1115
1702
|
assert_eq!(
|
|
1116
1703
|
(
|
|
1117
|
-
guest.
|
|
1118
|
-
|
|
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)
|
|
1119
1712
|
),
|
|
1120
|
-
(ERRNO_XDEV,
|
|
1121
|
-
"path_rename of
|
|
1122
|
-
|
|
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"
|
|
1123
1716
|
);
|
|
1124
1717
|
}
|
|
1125
1718
|
|
|
1126
|
-
/// A
|
|
1719
|
+
/// A Tmpfs whose files take another writer's `bbb` at their end as one
|
|
1127
1720
|
/// is written, once the length the write would land at is known and
|
|
1128
1721
|
/// before it lands, as another invocation sharing the file could.
|
|
1129
|
-
struct AppendedMeanwhile
|
|
1722
|
+
struct AppendedMeanwhile;
|
|
1130
1723
|
|
|
1131
1724
|
struct AppendedMeanwhileFile {
|
|
1132
|
-
file: Box<dyn
|
|
1133
|
-
other: std::sync::Mutex<Box<dyn
|
|
1725
|
+
file: Box<dyn FileOperations>,
|
|
1726
|
+
other: std::sync::Mutex<Box<dyn FileOperations>>,
|
|
1134
1727
|
}
|
|
1135
1728
|
|
|
1136
1729
|
impl AppendedMeanwhileFile {
|
|
@@ -1138,75 +1731,39 @@ impl AppendedMeanwhileFile {
|
|
|
1138
1731
|
self.other
|
|
1139
1732
|
.lock()
|
|
1140
1733
|
.expect("the other writer's lock must not be poisoned")
|
|
1141
|
-
.
|
|
1734
|
+
.write_iter(&mut Kiocb::at(0).with_append(), b"bbb")
|
|
1142
1735
|
.map(drop)
|
|
1143
1736
|
}
|
|
1144
1737
|
}
|
|
1145
1738
|
|
|
1146
|
-
impl
|
|
1147
|
-
fn
|
|
1148
|
-
self.file.
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
|
|
1152
|
-
self.file.write_at(buf, offset)
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
/// The length is read before the other writer appends, so a write
|
|
1156
|
-
/// placed by it lands over what the other wrote.
|
|
1157
|
-
fn metadata(&self) -> Result<Metadata, FilesystemError> {
|
|
1158
|
-
let metadata = self.file.metadata()?;
|
|
1159
|
-
self.other_appends()?;
|
|
1160
|
-
Ok(metadata)
|
|
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)
|
|
1161
1742
|
}
|
|
1162
1743
|
|
|
1163
|
-
fn
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
self.other_appends()?;
|
|
1169
|
-
self.file.append(buf)
|
|
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)
|
|
1170
1749
|
}
|
|
1171
1750
|
}
|
|
1172
1751
|
|
|
1173
|
-
impl
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
let writes =
|
|
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)?;
|
|
1181
1762
|
Ok(Box::new(AppendedMeanwhileFile {
|
|
1182
|
-
file
|
|
1183
|
-
other: std::sync::Mutex::new(
|
|
1763
|
+
file,
|
|
1764
|
+
other: std::sync::Mutex::new(other),
|
|
1184
1765
|
}))
|
|
1185
1766
|
}
|
|
1186
|
-
|
|
1187
|
-
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1188
|
-
self.0.metadata(path)
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1192
|
-
self.0.read_dir(path)
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1196
|
-
self.0.create_dir(path)
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1200
|
-
self.0.remove_dir(path)
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1204
|
-
self.0.remove_file(path)
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1208
|
-
self.0.rename(from, to, rename)
|
|
1209
|
-
}
|
|
1210
1767
|
}
|
|
1211
1768
|
|
|
1212
1769
|
// The other writer's `bbb` lands as the guest's append is made, so only
|
|
@@ -1214,13 +1771,13 @@ impl Filesystem for AppendedMeanwhile {
|
|
|
1214
1771
|
// @behavior RT-095
|
|
1215
1772
|
#[test]
|
|
1216
1773
|
fn an_append_lands_after_another_writers_append() {
|
|
1217
|
-
let
|
|
1218
|
-
let shared = Arc::new(AppendedMeanwhile
|
|
1219
|
-
let mut guest = Guest::new(&[grant(shared
|
|
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)]);
|
|
1220
1777
|
assert_eq!(
|
|
1221
1778
|
(
|
|
1222
1779
|
guest.call("append_file", FILE),
|
|
1223
|
-
contents(&
|
|
1780
|
+
contents(&host(&tmpfs), "file.txt")
|
|
1224
1781
|
),
|
|
1225
1782
|
(0, Some(b"grantedbbbhello".to_vec())),
|
|
1226
1783
|
"fd_write to a file opened to append beneath a Mount through preview1 must land after \
|
|
@@ -1228,22 +1785,26 @@ fn an_append_lands_after_another_writers_append() {
|
|
|
1228
1785
|
);
|
|
1229
1786
|
}
|
|
1230
1787
|
|
|
1788
|
+
#[cfg(unix)]
|
|
1231
1789
|
const FILETYPE_UNKNOWN: u8 = 0;
|
|
1790
|
+
#[cfg(unix)]
|
|
1232
1791
|
const FILETYPE_DIRECTORY: u8 = 3;
|
|
1792
|
+
#[cfg(unix)]
|
|
1233
1793
|
const FILETYPE_REGULAR_FILE: u8 = 4;
|
|
1794
|
+
#[cfg(unix)]
|
|
1234
1795
|
const FILETYPE_SYMBOLIC_LINK: u8 = 7;
|
|
1235
1796
|
|
|
1236
1797
|
/// A host directory of its own holding `file.txt` as a link to `target`,
|
|
1237
|
-
/// made by `make_target`, and a guest handed it as a read-only
|
|
1798
|
+
/// made by `make_target`, and a guest handed it as a read-only Bind
|
|
1238
1799
|
/// Mount.
|
|
1239
1800
|
#[cfg(unix)]
|
|
1240
|
-
fn
|
|
1241
|
-
|
|
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)
|
|
1242
1803
|
}
|
|
1243
1804
|
|
|
1244
1805
|
/// As `linked`, the Mount taking `access` and `policy`.
|
|
1245
1806
|
#[cfg(unix)]
|
|
1246
|
-
fn
|
|
1807
|
+
fn mount_link_under(
|
|
1247
1808
|
name: &str,
|
|
1248
1809
|
make_target: fn(&std::path::Path),
|
|
1249
1810
|
access: Access,
|
|
@@ -1254,13 +1815,13 @@ fn linked_under(
|
|
|
1254
1815
|
std::fs::create_dir_all(&host).expect("the host directory must be created");
|
|
1255
1816
|
make_target(&host.join("target"));
|
|
1256
1817
|
std::os::unix::fs::symlink("target", host.join("file.txt")).expect("the link must be made");
|
|
1257
|
-
let directory =
|
|
1818
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
1258
1819
|
let guest = Guest::new(&[grant(Arc::new(directory), access, policy)]);
|
|
1259
1820
|
(host, guest)
|
|
1260
1821
|
}
|
|
1261
1822
|
|
|
1262
1823
|
/// What each stat of `path` with the given lookupflags answers, and the
|
|
1263
|
-
/// filetype it wrote, beneath a
|
|
1824
|
+
/// filetype it wrote, beneath a Bind Mount whose host directory
|
|
1264
1825
|
/// holds `file.txt` as a link to `target`, made by `make_target`.
|
|
1265
1826
|
#[cfg(unix)]
|
|
1266
1827
|
fn stat_link(
|
|
@@ -1268,16 +1829,11 @@ fn stat_link(
|
|
|
1268
1829
|
make_target: fn(&std::path::Path),
|
|
1269
1830
|
stats: &[((i32, i32), i32)],
|
|
1270
1831
|
) -> Vec<(i32, u8)> {
|
|
1271
|
-
let (host, mut guest) =
|
|
1832
|
+
let (host, mut guest) = mount_link(name, make_target);
|
|
1272
1833
|
let answers = stats
|
|
1273
1834
|
.iter()
|
|
1274
1835
|
.map(|&(path, flags)| {
|
|
1275
|
-
let errno = guest
|
|
1276
|
-
.instance
|
|
1277
|
-
.get_typed_func::<(i32, i32, i32), i32>(&mut guest.store, "stat_flagged")
|
|
1278
|
-
.expect("probe export must resolve")
|
|
1279
|
-
.call(&mut guest.store, (path.0, path.1, flags))
|
|
1280
|
-
.expect("probe call must not trap");
|
|
1836
|
+
let errno = guest.stat_flagged(path, flags);
|
|
1281
1837
|
let memory = guest
|
|
1282
1838
|
.instance
|
|
1283
1839
|
.get_memory(&mut guest.store, "memory")
|
|
@@ -1303,14 +1859,9 @@ fn a_guest_sees_a_fifo_as_of_an_unknown_type_and_cannot_open_it() {
|
|
|
1303
1859
|
.status()
|
|
1304
1860
|
.expect("mkfifo must run");
|
|
1305
1861
|
assert!(made.success(), "the FIFO must be made");
|
|
1306
|
-
let directory =
|
|
1862
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
1307
1863
|
let mut guest = Guest::new(&[grant(Arc::new(directory), Access::Read, None)]);
|
|
1308
|
-
let stat = guest
|
|
1309
|
-
.instance
|
|
1310
|
-
.get_typed_func::<(i32, i32, i32), i32>(&mut guest.store, "stat_flagged")
|
|
1311
|
-
.expect("probe export must resolve")
|
|
1312
|
-
.call(&mut guest.store, (FILE.0, FILE.1, 0))
|
|
1313
|
-
.expect("probe call must not trap");
|
|
1864
|
+
let stat = guest.stat_flagged(FILE, 0);
|
|
1314
1865
|
let filestat = guest
|
|
1315
1866
|
.instance
|
|
1316
1867
|
.get_memory(&mut guest.store, "memory")
|
|
@@ -1323,11 +1874,40 @@ fn a_guest_sees_a_fifo_as_of_an_unknown_type_and_cannot_open_it() {
|
|
|
1323
1874
|
assert_eq!(
|
|
1324
1875
|
(stat, filestat[16], nlink, opened),
|
|
1325
1876
|
(0, FILETYPE_UNKNOWN, 1, ERRNO_ACCES),
|
|
1326
|
-
"path_filestat_get of a FIFO beneath a
|
|
1877
|
+
"path_filestat_get of a FIFO beneath a Bind Mount through preview1 must report an \
|
|
1327
1878
|
unknown filetype named once, and path_open of it must answer EACCES"
|
|
1328
1879
|
);
|
|
1329
1880
|
}
|
|
1330
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
|
+
|
|
1331
1911
|
// The link points at a regular file, so only a stat that follows none
|
|
1332
1912
|
// can answer a link.
|
|
1333
1913
|
// @behavior RT-097
|
|
@@ -1342,11 +1922,84 @@ fn a_stat_following_no_link_reports_the_link_itself() {
|
|
|
1342
1922
|
assert_eq!(
|
|
1343
1923
|
answers,
|
|
1344
1924
|
[(0, FILETYPE_REGULAR_FILE), (0, FILETYPE_SYMBOLIC_LINK)],
|
|
1345
|
-
"path_filestat_get of a host link beneath a
|
|
1925
|
+
"path_filestat_get of a host link beneath a Bind Mount through preview1 must report \
|
|
1346
1926
|
the file it points to when following links, and a symbolic link when following none"
|
|
1347
1927
|
);
|
|
1348
1928
|
}
|
|
1349
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
|
+
|
|
1350
2003
|
// The stat follows no link, so only the trailing `/` can make it reach
|
|
1351
2004
|
// the directory the link points at, as Linux's `lstat` does.
|
|
1352
2005
|
// @behavior RT-098
|
|
@@ -1362,18 +2015,18 @@ fn a_stat_of_a_link_named_with_a_trailing_slash_reports_its_directory() {
|
|
|
1362
2015
|
answers,
|
|
1363
2016
|
[(0, FILETYPE_DIRECTORY), (0, FILETYPE_SYMBOLIC_LINK)],
|
|
1364
2017
|
"path_filestat_get following no link of a host link to a directory, named with a \
|
|
1365
|
-
trailing slash, beneath a
|
|
2018
|
+
trailing slash, beneath a Bind Mount through preview1 must report the directory, \
|
|
1366
2019
|
and without the slash a symbolic link"
|
|
1367
2020
|
);
|
|
1368
2021
|
}
|
|
1369
2022
|
|
|
1370
2023
|
/// What `path_unlink_file` and `path_rename` answer for the link
|
|
1371
|
-
/// `file.txt/` to a directory beneath a read-write
|
|
2024
|
+
/// `file.txt/` to a directory beneath a read-write Bind Mount taking
|
|
1372
2025
|
/// `policy`, and whether the link stays; `name` keeps its host directory
|
|
1373
2026
|
/// its own.
|
|
1374
2027
|
#[cfg(unix)]
|
|
1375
2028
|
fn change_link_named_as_directory(name: &str, policy: Option<Arc<dyn Policy>>) -> ([i32; 2], bool) {
|
|
1376
|
-
let (host, mut guest) =
|
|
2029
|
+
let (host, mut guest) = mount_link_under(name, directory_with_key, Access::ReadWrite, policy);
|
|
1377
2030
|
let answers = [
|
|
1378
2031
|
guest.call("unlink_path", FILE_AS_DIRECTORY),
|
|
1379
2032
|
guest.call_pair("rename_paths", FILE_AS_DIRECTORY, MOVED),
|
|
@@ -1397,7 +2050,7 @@ fn a_link_named_with_a_trailing_slash_is_neither_removed_nor_moved() {
|
|
|
1397
2050
|
],
|
|
1398
2051
|
[([ERRNO_NOTDIR, ERRNO_NOTDIR], true); 2],
|
|
1399
2052
|
"path_unlink_file and path_rename of a host link to a directory named with a trailing \
|
|
1400
|
-
slash, beneath a read-write
|
|
2053
|
+
slash, beneath a read-write Bind Mount with or without a Policy through preview1, \
|
|
1401
2054
|
must answer ENOTDIR as Linux does and leave the link"
|
|
1402
2055
|
);
|
|
1403
2056
|
}
|
|
@@ -1416,7 +2069,7 @@ fn an_unlink_with_a_trailing_slash_answers_for_the_name_itself() {
|
|
|
1416
2069
|
std::fs::create_dir_all(host.join("dir")).expect("the host directory must be created");
|
|
1417
2070
|
std::fs::write(host.join("file.txt"), "granted").expect("the host file must be written");
|
|
1418
2071
|
std::os::unix::fs::symlink("nowhere", host.join("gone")).expect("the link must be made");
|
|
1419
|
-
let directory =
|
|
2072
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
1420
2073
|
let mut guest = Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, None)]);
|
|
1421
2074
|
let answers = [
|
|
1422
2075
|
DIR_AS_DIRECTORY,
|
|
@@ -1434,22 +2087,122 @@ fn an_unlink_with_a_trailing_slash_answers_for_the_name_itself() {
|
|
|
1434
2087
|
[true; 3]
|
|
1435
2088
|
),
|
|
1436
2089
|
"path_unlink_file of a directory, a file, a link to nothing, and a missing name, each \
|
|
1437
|
-
named with a trailing slash beneath a read-write
|
|
2090
|
+
named with a trailing slash beneath a read-write Bind Mount through preview1, must \
|
|
1438
2091
|
answer EISDIR, ENOTDIR, ENOTDIR, and ENOENT as Linux does and remove nothing"
|
|
1439
2092
|
);
|
|
1440
2093
|
}
|
|
1441
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.
|
|
1442
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
|
|
1443
2194
|
#[test]
|
|
1444
|
-
fn
|
|
1445
|
-
let
|
|
1446
|
-
let answer =
|
|
1447
|
-
|
|
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);
|
|
1448
2203
|
assert_eq!(
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
"fd_allocate of a file beneath a read-write Mount through preview1 must answer ENOTSUP \
|
|
1452
|
-
as wasmtime-wasi does, leaving the file as it was"
|
|
2204
|
+
answer, ERRNO_NOSPC,
|
|
2205
|
+
"fd_allocate of 100 bytes in a Tmpfs of 50 through preview1 must answer ENOSPC"
|
|
1453
2206
|
);
|
|
1454
2207
|
}
|
|
1455
2208
|
|
|
@@ -1459,7 +2212,7 @@ fn an_allocation_for_a_file_beneath_a_mount_is_unsupported() {
|
|
|
1459
2212
|
#[cfg(unix)]
|
|
1460
2213
|
#[test]
|
|
1461
2214
|
fn an_open_following_no_link_refuses_a_link_to_a_directory() {
|
|
1462
|
-
let (host, mut guest) =
|
|
2215
|
+
let (host, mut guest) = mount_link("open-nofollow", |target| {
|
|
1463
2216
|
std::fs::create_dir(target).expect("the host directory must be created")
|
|
1464
2217
|
});
|
|
1465
2218
|
let answers = [
|
|
@@ -1470,7 +2223,7 @@ fn an_open_following_no_link_refuses_a_link_to_a_directory() {
|
|
|
1470
2223
|
assert_eq!(
|
|
1471
2224
|
answers,
|
|
1472
2225
|
[ERRNO_LOOP, 0],
|
|
1473
|
-
"path_open following no link of a host link to a directory beneath a
|
|
2226
|
+
"path_open following no link of a host link to a directory beneath a Bind Mount \
|
|
1474
2227
|
through preview1 must answer ELOOP, and with a trailing slash open the directory"
|
|
1475
2228
|
);
|
|
1476
2229
|
}
|
|
@@ -1482,27 +2235,32 @@ fn directory_with_key(target: &std::path::Path) {
|
|
|
1482
2235
|
std::fs::write(target.join("key"), "secret").expect("the host file must be written");
|
|
1483
2236
|
}
|
|
1484
2237
|
|
|
1485
|
-
// The
|
|
1486
|
-
//
|
|
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.
|
|
1487
2240
|
// @behavior RT-101
|
|
1488
2241
|
#[cfg(unix)]
|
|
1489
2242
|
#[test]
|
|
1490
|
-
fn
|
|
1491
|
-
let
|
|
1492
|
-
let (host, mut guest) =
|
|
1493
|
-
"policy-link",
|
|
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",
|
|
1494
2247
|
directory_with_key,
|
|
1495
2248
|
Access::ReadWrite,
|
|
1496
|
-
Some(
|
|
2249
|
+
Some(refuses_key),
|
|
1497
2250
|
);
|
|
1498
2251
|
let refused = [
|
|
1499
2252
|
guest.call("read_file", THROUGH_FILE),
|
|
1500
2253
|
guest.call("unlink_path", THROUGH_FILE),
|
|
1501
|
-
guest.call("open_following", FILE),
|
|
1502
2254
|
];
|
|
1503
2255
|
let kept = host.join("target/key").exists();
|
|
1504
2256
|
let _ = std::fs::remove_dir_all(&host);
|
|
1505
|
-
let
|
|
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
|
+
);
|
|
1506
2264
|
let followed = [
|
|
1507
2265
|
guest.call("read_file", THROUGH_FILE),
|
|
1508
2266
|
guest.call("open_following", FILE),
|
|
@@ -1510,21 +2268,20 @@ fn a_path_through_a_link_beneath_a_mount_with_a_policy_answers_eloop() {
|
|
|
1510
2268
|
let _ = std::fs::remove_dir_all(&host);
|
|
1511
2269
|
assert_eq!(
|
|
1512
2270
|
(refused, kept, followed),
|
|
1513
|
-
([
|
|
1514
|
-
"path_open and path_unlink_file through a host link
|
|
1515
|
-
|
|
1516
|
-
|
|
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"
|
|
1517
2275
|
);
|
|
1518
2276
|
}
|
|
1519
2277
|
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
// which no Policy-free walk past it is needed for.
|
|
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.
|
|
1524
2281
|
// @behavior RT-125
|
|
1525
2282
|
#[cfg(unix)]
|
|
1526
2283
|
#[test]
|
|
1527
|
-
fn
|
|
2284
|
+
fn a_link_out_of_a_directory_or_into_a_loop_answers_as_without_a_policy() {
|
|
1528
2285
|
let outside = std::env::temp_dir().join(format!("kobako-outside-{}", std::process::id()));
|
|
1529
2286
|
directory_with_key(&outside);
|
|
1530
2287
|
let targets = [
|
|
@@ -1544,7 +2301,7 @@ fn a_link_out_of_a_directory_or_into_a_loop_answers_eloop_beneath_a_policy() {
|
|
|
1544
2301
|
std::fs::create_dir_all(&host).expect("the host directory must be created");
|
|
1545
2302
|
std::os::unix::fs::symlink(target, host.join("file.txt"))
|
|
1546
2303
|
.expect("the link must be made");
|
|
1547
|
-
let directory =
|
|
2304
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
1548
2305
|
let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
|
|
1549
2306
|
let mut guest =
|
|
1550
2307
|
Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, Some(policy))]);
|
|
@@ -1561,90 +2318,60 @@ fn a_link_out_of_a_directory_or_into_a_loop_answers_eloop_beneath_a_policy() {
|
|
|
1561
2318
|
let _ = std::fs::remove_dir_all(&outside);
|
|
1562
2319
|
assert_eq!(
|
|
1563
2320
|
(answers, kept),
|
|
1564
|
-
(
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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"
|
|
1568
2332
|
);
|
|
1569
2333
|
}
|
|
1570
2334
|
|
|
1571
|
-
// The Filesystem fails only the lookup of `dir`, which the open needs
|
|
1572
|
-
//
|
|
2335
|
+
// The Filesystem fails only the lookup of `dir`, which the open needs no answer
|
|
2336
|
+
// from but the walk on its way does.
|
|
1573
2337
|
// @behavior RT-126
|
|
1574
2338
|
#[test]
|
|
1575
|
-
fn
|
|
1576
|
-
let
|
|
1577
|
-
|
|
1578
|
-
)
|
|
1579
|
-
let
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
/// A Memory failing every lookup of `dir` itself, as a host directory
|
|
1590
|
-
/// that cannot be read could.
|
|
1591
|
-
struct FailsLookingUpDir(Memory);
|
|
1592
|
-
|
|
1593
|
-
impl Filesystem for FailsLookingUpDir {
|
|
1594
|
-
matches_names_exactly!();
|
|
1595
|
-
never_repeats_identities!();
|
|
1596
|
-
is_shared!();
|
|
1597
|
-
|
|
1598
|
-
fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
|
|
1599
|
-
self.0.open(path, options)
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1603
|
-
self.0.metadata(path)
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1607
|
-
match path {
|
|
1608
|
-
"dir" => Err(FilesystemError::Other("unreadable".into())),
|
|
1609
|
-
path => self.0.symlink_metadata(path),
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
fn canonicalize_beneath(&self, base: &str, path: &str) -> Result<String, FilesystemError> {
|
|
1614
|
-
self.0.canonicalize_beneath(base, path)
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
|
|
1618
|
-
None
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1622
|
-
self.0.read_dir(path)
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1626
|
-
self.0.create_dir(path)
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1630
|
-
self.0.remove_dir(path)
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1634
|
-
self.0.remove_file(path)
|
|
1635
|
-
}
|
|
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
|
+
}
|
|
1636
2351
|
|
|
1637
|
-
|
|
1638
|
-
|
|
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
|
+
}
|
|
1639
2366
|
}
|
|
1640
2367
|
}
|
|
1641
2368
|
|
|
1642
|
-
//
|
|
1643
|
-
// the link
|
|
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.
|
|
1644
2371
|
// @behavior RT-119
|
|
1645
2372
|
#[cfg(unix)]
|
|
1646
2373
|
#[test]
|
|
1647
|
-
fn
|
|
2374
|
+
fn a_step_out_of_a_link_lands_where_it_leads() {
|
|
1648
2375
|
assert_eq!(
|
|
1649
2376
|
stat_link(
|
|
1650
2377
|
"dot-link",
|
|
@@ -1654,13 +2381,18 @@ fn a_step_out_of_a_link_answers_eloop() {
|
|
|
1654
2381
|
.into_iter()
|
|
1655
2382
|
.map(|(errno, _)| errno)
|
|
1656
2383
|
.collect::<Vec<_>>(),
|
|
1657
|
-
[
|
|
2384
|
+
[0; 2],
|
|
1658
2385
|
"path_filestat_get of `file.txt/.` and `file.txt/../file.txt`, where `file.txt` is a \
|
|
1659
|
-
host link to a directory, beneath 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"
|
|
1660
2388
|
);
|
|
1661
2389
|
}
|
|
1662
2390
|
|
|
1663
|
-
fn
|
|
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> {
|
|
1664
2396
|
Arc::new(
|
|
1665
2397
|
[("file.txt", "granted"), ("dir/inner", "x")]
|
|
1666
2398
|
.into_iter()
|
|
@@ -1668,19 +2400,65 @@ fn file_and_directory() -> Arc<Memory> {
|
|
|
1668
2400
|
)
|
|
1669
2401
|
}
|
|
1670
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
|
+
|
|
1671
2440
|
// A `.` after a name resolves through it, so Linux and macOS both refuse
|
|
1672
2441
|
// `missing/.` at the missing name rather than make it.
|
|
1673
2442
|
// @behavior RT-109
|
|
1674
2443
|
#[test]
|
|
1675
2444
|
fn a_path_ending_in_a_dot_makes_nothing_where_no_directory_stands() {
|
|
1676
|
-
let
|
|
1677
|
-
let mut guest = Guest::new(&[grant(
|
|
2445
|
+
let tmpfs = file_and_directory();
|
|
2446
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
1678
2447
|
let answers = [
|
|
1679
2448
|
guest.call("make_dir", MISSING_DOT),
|
|
1680
2449
|
guest.call_flagged(MISSING_DOT, OFLAGS_CREAT, 0),
|
|
1681
2450
|
];
|
|
1682
2451
|
assert_eq!(
|
|
1683
|
-
(
|
|
2452
|
+
(
|
|
2453
|
+
answers,
|
|
2454
|
+
vfs::vfs_fstatat(
|
|
2455
|
+
&prepare_creds_within(&[], None),
|
|
2456
|
+
&host(&tmpfs),
|
|
2457
|
+
"missing",
|
|
2458
|
+
false
|
|
2459
|
+
)
|
|
2460
|
+
.is_err()
|
|
2461
|
+
),
|
|
1684
2462
|
([ERRNO_NOENT, ERRNO_NOENT], true),
|
|
1685
2463
|
"path_create_directory and path_open with O_CREAT of missing/. beneath a Mount through \
|
|
1686
2464
|
preview1 must answer ENOENT, as Linux does, and make nothing named missing"
|
|
@@ -1692,8 +2470,8 @@ fn a_path_ending_in_a_dot_makes_nothing_where_no_directory_stands() {
|
|
|
1692
2470
|
// @behavior RT-102
|
|
1693
2471
|
#[test]
|
|
1694
2472
|
fn a_path_ending_in_a_dot_is_neither_removed_nor_moved() {
|
|
1695
|
-
let
|
|
1696
|
-
let mut guest = Guest::new(&[grant(
|
|
2473
|
+
let tmpfs = file_and_directory();
|
|
2474
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
1697
2475
|
let answers = [
|
|
1698
2476
|
guest.call("remove_path", DIR_DOT),
|
|
1699
2477
|
guest.call("remove_path", DIR_DOT_DOT),
|
|
@@ -1703,11 +2481,20 @@ fn a_path_ending_in_a_dot_is_neither_removed_nor_moved() {
|
|
|
1703
2481
|
guest.call_pair("rename_paths", OUT_OF_FILE, MOVED),
|
|
1704
2482
|
];
|
|
1705
2483
|
assert_eq!(
|
|
1706
|
-
(
|
|
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
|
+
),
|
|
1707
2494
|
(
|
|
1708
2495
|
[
|
|
1709
2496
|
ERRNO_INVAL,
|
|
1710
|
-
|
|
2497
|
+
ERRNO_NOTEMPTY,
|
|
1711
2498
|
ERRNO_BUSY,
|
|
1712
2499
|
ERRNO_BUSY,
|
|
1713
2500
|
ERRNO_NOTDIR,
|
|
@@ -1716,8 +2503,9 @@ fn a_path_ending_in_a_dot_is_neither_removed_nor_moved() {
|
|
|
1716
2503
|
true
|
|
1717
2504
|
),
|
|
1718
2505
|
"path_remove_directory of a path ending in . or .., and path_rename of one, beneath a \
|
|
1719
|
-
Mount through preview1 must answer EINVAL and
|
|
1720
|
-
where a file stands before the dot, and leave the directory in
|
|
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"
|
|
1721
2509
|
);
|
|
1722
2510
|
}
|
|
1723
2511
|
|
|
@@ -1755,7 +2543,7 @@ fn a_create_of_a_host_file_that_only_reads_opens_it() {
|
|
|
1755
2543
|
std::fs::Permissions::from_mode(0o444),
|
|
1756
2544
|
)
|
|
1757
2545
|
.expect("the host file must become read-only");
|
|
1758
|
-
let directory =
|
|
2546
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
1759
2547
|
let answer = Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, None)]).call_flagged(
|
|
1760
2548
|
FILE,
|
|
1761
2549
|
OFLAGS_CREAT,
|
|
@@ -1764,26 +2552,178 @@ fn a_create_of_a_host_file_that_only_reads_opens_it() {
|
|
|
1764
2552
|
let _ = std::fs::remove_dir_all(&host);
|
|
1765
2553
|
assert_eq!(
|
|
1766
2554
|
answer, 0,
|
|
1767
|
-
"path_open with O_CREAT and no write of a read-only host file beneath a
|
|
2555
|
+
"path_open with O_CREAT and no write of a read-only host file beneath a Bind Mount \
|
|
1768
2556
|
through preview1 must open it"
|
|
1769
2557
|
);
|
|
1770
2558
|
}
|
|
1771
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
|
+
|
|
1772
2712
|
// The second seek would land past what a signed offset holds, so only
|
|
1773
2713
|
// the cap keeps the position from reaching it.
|
|
1774
2714
|
// @behavior RT-104
|
|
1775
2715
|
#[test]
|
|
1776
|
-
fn
|
|
2716
|
+
fn a_seek_past_the_largest_offset_is_invalid() {
|
|
1777
2717
|
let answer = Guest::new(&[grant(file_and_directory(), Access::Read, None)])
|
|
1778
2718
|
.call_bare("seek_past_the_largest_offset");
|
|
1779
2719
|
assert_eq!(
|
|
1780
|
-
answer,
|
|
2720
|
+
answer, ERRNO_INVAL,
|
|
1781
2721
|
"fd_seek past the largest signed 64-bit offset of a file beneath a Mount through \
|
|
1782
|
-
preview1 must answer
|
|
2722
|
+
preview1 must answer EINVAL, as Linux does"
|
|
1783
2723
|
);
|
|
1784
2724
|
}
|
|
1785
2725
|
|
|
1786
|
-
// A
|
|
2726
|
+
// A Tmpfs clamps a read past its end and tries to grow for a write or a
|
|
1787
2727
|
// size, so only the check before the Filesystem answers each as Linux does.
|
|
1788
2728
|
// @behavior RT-116
|
|
1789
2729
|
#[test]
|
|
@@ -1797,7 +2737,7 @@ fn an_offset_past_the_largest_signed_offset_is_invalid() {
|
|
|
1797
2737
|
),
|
|
1798
2738
|
(ERRNO_INVAL, ERRNO_INVAL, ERRNO_INVAL),
|
|
1799
2739
|
"fd_pread, fd_pwrite, and fd_filestat_set_size past the largest signed 64-bit offset of a \
|
|
1800
|
-
|
|
2740
|
+
Tmpfs file beneath a Mount through preview1 must answer EINVAL, as Linux does"
|
|
1801
2741
|
);
|
|
1802
2742
|
}
|
|
1803
2743
|
|
|
@@ -1824,17 +2764,17 @@ fn a_read_whose_end_passes_the_largest_signed_offset_is_invalid() {
|
|
|
1824
2764
|
// two answers apart.
|
|
1825
2765
|
// @behavior RT-135
|
|
1826
2766
|
#[test]
|
|
1827
|
-
fn
|
|
2767
|
+
fn a_wait_to_write_a_file_is_ready_whatever_its_mount_permits() {
|
|
1828
2768
|
let wait = |access| {
|
|
1829
2769
|
Guest::new(&[grant(file_and_directory(), access, None)])
|
|
1830
2770
|
.call_at("poll_write_with", RIGHTS_FD_READ)
|
|
1831
2771
|
};
|
|
1832
2772
|
assert_eq!(
|
|
1833
2773
|
(wait(Access::ReadWrite), wait(Access::Read)),
|
|
1834
|
-
(0,
|
|
2774
|
+
(0, 0),
|
|
1835
2775
|
"poll_oneoff waiting to write a file opened only to read through preview1 must be ready \
|
|
1836
|
-
beneath a read-write
|
|
1837
|
-
|
|
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"
|
|
1838
2778
|
);
|
|
1839
2779
|
}
|
|
1840
2780
|
|
|
@@ -1852,10 +2792,10 @@ fn sizing_a_file_opened_only_to_read_answers_by_its_mount_then_as_linux_does() {
|
|
|
1852
2792
|
size(Access::ReadWrite, RIGHTS_FD_READ),
|
|
1853
2793
|
size(Access::ReadWrite, RIGHTS_FD_WRITE),
|
|
1854
2794
|
),
|
|
1855
|
-
(
|
|
1856
|
-
"fd_filestat_set_size on a file opened only to read through preview1 must answer
|
|
1857
|
-
beneath a read-only Mount, as
|
|
1858
|
-
|
|
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"
|
|
1859
2799
|
);
|
|
1860
2800
|
}
|
|
1861
2801
|
|
|
@@ -1871,7 +2811,7 @@ impl Policy for RecordsAsks {
|
|
|
1871
2811
|
}
|
|
1872
2812
|
|
|
1873
2813
|
// The Mount is made read-only, as a Ruby mount with a block is by default,
|
|
1874
|
-
// so only
|
|
2814
|
+
// so only a Mount allowing writes lets either through.
|
|
1875
2815
|
// Each Policy is asked only by the open before the call, so a wait or a size
|
|
1876
2816
|
// asking it as well adds an ask the literal lists below do not hold.
|
|
1877
2817
|
// @behavior RT-139
|
|
@@ -1881,7 +2821,7 @@ fn a_mount_whose_policy_decides_may_be_written() {
|
|
|
1881
2821
|
let policy = Arc::new(RecordsAsks::default());
|
|
1882
2822
|
let errno = Guest::new(&[grant(
|
|
1883
2823
|
file_and_directory(),
|
|
1884
|
-
Access::
|
|
2824
|
+
Access::ReadWrite,
|
|
1885
2825
|
Some(policy.clone() as Arc<dyn Policy>),
|
|
1886
2826
|
)])
|
|
1887
2827
|
.call_at(name, rights);
|
|
@@ -1897,23 +2837,24 @@ fn a_mount_whose_policy_decides_may_be_written() {
|
|
|
1897
2837
|
(0, vec![("/data/file.txt".to_owned(), Access::Read)]),
|
|
1898
2838
|
(0, vec![("/data/file.txt".to_owned(), Access::ReadWrite)]),
|
|
1899
2839
|
),
|
|
1900
|
-
"through preview1 beneath a read-
|
|
2840
|
+
"through preview1 beneath a read-write Mount whose Policy allows every change, \
|
|
1901
2841
|
poll_oneoff waiting to write a file opened only to read must be ready, and \
|
|
1902
2842
|
fd_filestat_set_size on a file opened to write must size it, the Policy asked \
|
|
1903
2843
|
only by each open"
|
|
1904
2844
|
);
|
|
1905
2845
|
}
|
|
1906
2846
|
|
|
1907
|
-
// Each open asks without the right to write, so only its CREAT
|
|
1908
|
-
//
|
|
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.
|
|
1909
2850
|
// @behavior RT-146
|
|
1910
2851
|
#[test]
|
|
1911
|
-
fn
|
|
2852
|
+
fn an_open_that_may_create_asks_to_make_the_file_before_it_finds_what_stands() {
|
|
1912
2853
|
let open = |path| {
|
|
1913
2854
|
let policy = Arc::new(RecordsAsks::default());
|
|
1914
2855
|
let errno = Guest::new(&[grant(
|
|
1915
2856
|
file_and_directory(),
|
|
1916
|
-
Access::
|
|
2857
|
+
Access::ReadWrite,
|
|
1917
2858
|
Some(policy.clone() as Arc<dyn Policy>),
|
|
1918
2859
|
)])
|
|
1919
2860
|
.call_flagged(path, OFLAGS_CREAT, 0);
|
|
@@ -1924,11 +2865,52 @@ fn a_policy_is_asked_once_for_an_open_that_may_create_as_what_it_does() {
|
|
|
1924
2865
|
(open(NEW), open(FILE)),
|
|
1925
2866
|
(
|
|
1926
2867
|
(0, vec![("/data/new.txt".to_owned(), Access::ReadWrite)]),
|
|
1927
|
-
(
|
|
2868
|
+
(
|
|
2869
|
+
0,
|
|
2870
|
+
vec![
|
|
2871
|
+
("/data/file.txt".to_owned(), Access::ReadWrite),
|
|
2872
|
+
("/data/file.txt".to_owned(), Access::Read),
|
|
2873
|
+
]
|
|
2874
|
+
),
|
|
1928
2875
|
),
|
|
1929
2876
|
"path_open with O_CREAT and no write of a missing path and of an existing file beneath \
|
|
1930
|
-
a Mount whose Policy allows every change through preview1 must ask the Policy
|
|
1931
|
-
each,
|
|
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"
|
|
1932
2914
|
);
|
|
1933
2915
|
}
|
|
1934
2916
|
|
|
@@ -1973,29 +2955,124 @@ fn what_stands_at_a_path_answers_before_a_read_only_mount_refuses() {
|
|
|
1973
2955
|
);
|
|
1974
2956
|
}
|
|
1975
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
|
+
|
|
1976
2990
|
const NEW: (i32, i32) = (840, 7);
|
|
1977
2991
|
const RIGHTS_FD_READ: u64 = 1 << 1;
|
|
1978
2992
|
const ERRNO_PERM: i32 = 63;
|
|
1979
2993
|
|
|
1980
|
-
|
|
1981
|
-
|
|
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.
|
|
1982
3055
|
// @behavior RT-133
|
|
1983
3056
|
#[test]
|
|
1984
|
-
fn
|
|
1985
|
-
let
|
|
1986
|
-
let
|
|
1987
|
-
|
|
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
|
+
);
|
|
1988
3064
|
assert_eq!(
|
|
1989
|
-
(
|
|
1990
|
-
(
|
|
1991
|
-
"fd_write at the largest signed 64-bit offset
|
|
1992
|
-
preview1 must answer
|
|
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"
|
|
1993
3070
|
);
|
|
1994
3071
|
}
|
|
1995
3072
|
|
|
1996
3073
|
// @behavior RT-130
|
|
1997
3074
|
#[test]
|
|
1998
|
-
fn
|
|
3075
|
+
fn an_advice_reaching_past_the_largest_signed_offset_is_invalid() {
|
|
1999
3076
|
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
2000
3077
|
assert_eq!(
|
|
2001
3078
|
(
|
|
@@ -2004,16 +3081,17 @@ fn an_advice_past_the_largest_signed_offset_is_invalid() {
|
|
|
2004
3081
|
guest.call_at("advise_for", 0)
|
|
2005
3082
|
),
|
|
2006
3083
|
(ERRNO_INVAL, ERRNO_INVAL, 0),
|
|
2007
|
-
"fd_advise
|
|
2008
|
-
|
|
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"
|
|
2009
3087
|
);
|
|
2010
3088
|
}
|
|
2011
3089
|
|
|
2012
3090
|
// The open asks to read only, so nothing else the open could refuse
|
|
2013
|
-
// stands in for
|
|
3091
|
+
// stands in for a refusal of not blocking.
|
|
2014
3092
|
// @behavior RT-129
|
|
2015
3093
|
#[test]
|
|
2016
|
-
fn
|
|
3094
|
+
fn a_descriptor_may_ask_not_to_block() {
|
|
2017
3095
|
let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
|
|
2018
3096
|
assert_eq!(
|
|
2019
3097
|
(
|
|
@@ -2021,9 +3099,10 @@ fn a_descriptor_that_would_not_block_is_unsupported() {
|
|
|
2021
3099
|
guest.call_bare("set_nonblocking"),
|
|
2022
3100
|
guest.call_flagged(FILE, 0, 0)
|
|
2023
3101
|
),
|
|
2024
|
-
(
|
|
3102
|
+
(0, 0, 0),
|
|
2025
3103
|
"path_open asking not to block and fd_fdstat_set_flags setting an open file not to \
|
|
2026
|
-
block, beneath a Mount through preview1, must
|
|
3104
|
+
block, beneath a Mount through preview1, must succeed, as Linux takes O_NONBLOCK on a \
|
|
3105
|
+
file"
|
|
2027
3106
|
);
|
|
2028
3107
|
}
|
|
2029
3108
|
|
|
@@ -2049,17 +3128,106 @@ fn a_directory_a_guest_opens_carries_no_right_to_write() {
|
|
|
2049
3128
|
);
|
|
2050
3129
|
}
|
|
2051
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
|
+
|
|
2052
3220
|
// @behavior RT-105
|
|
2053
3221
|
#[test]
|
|
2054
3222
|
fn a_file_moved_beneath_itself_is_no_directory() {
|
|
2055
|
-
let
|
|
2056
|
-
let answer = Guest::new(&[grant(
|
|
3223
|
+
let tmpfs = file_and_directory();
|
|
3224
|
+
let answer = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]).call_pair(
|
|
2057
3225
|
"rename_paths",
|
|
2058
3226
|
FILE,
|
|
2059
3227
|
BENEATH_FILE,
|
|
2060
3228
|
);
|
|
2061
3229
|
assert_eq!(
|
|
2062
|
-
(answer, contents(
|
|
3230
|
+
(answer, contents(&host(&tmpfs), "file.txt")),
|
|
2063
3231
|
(ERRNO_NOTDIR, Some(b"granted".to_vec())),
|
|
2064
3232
|
"path_rename of a file to a path beneath it, beneath a Mount through preview1, must \
|
|
2065
3233
|
answer ENOTDIR as Linux does and leave the file in place"
|
|
@@ -2072,7 +3240,7 @@ fn a_file_moved_beneath_itself_is_no_directory() {
|
|
|
2072
3240
|
#[cfg(unix)]
|
|
2073
3241
|
#[test]
|
|
2074
3242
|
fn an_open_following_no_link_finds_a_dangling_link_as_itself() {
|
|
2075
|
-
let (host, mut guest) =
|
|
3243
|
+
let (host, mut guest) = mount_link_under("dangling", |_| {}, Access::ReadWrite, None);
|
|
2076
3244
|
let answers = [
|
|
2077
3245
|
guest.call_flagged(FILE, 0, 0),
|
|
2078
3246
|
guest.call_flagged(FILE, OFLAGS_CREAT | OFLAGS_EXCL, 0),
|
|
@@ -2082,7 +3250,7 @@ fn an_open_following_no_link_finds_a_dangling_link_as_itself() {
|
|
|
2082
3250
|
assert_eq!(
|
|
2083
3251
|
(answers, made),
|
|
2084
3252
|
([ERRNO_LOOP, ERRNO_EXIST], false),
|
|
2085
|
-
"path_open following no link of a dangling host link beneath a
|
|
3253
|
+
"path_open following no link of a dangling host link beneath a Bind Mount through \
|
|
2086
3254
|
preview1 must answer ELOOP, and with O_CREAT and O_EXCL EEXIST, as Linux does, making \
|
|
2087
3255
|
nothing where it points"
|
|
2088
3256
|
);
|
|
@@ -2091,8 +3259,8 @@ fn an_open_following_no_link_finds_a_dangling_link_as_itself() {
|
|
|
2091
3259
|
/// What each root-changing export answers, and how many changes the
|
|
2092
3260
|
/// Filesystem was asked for along the way.
|
|
2093
3261
|
fn change_root(exports: &[&str]) -> (Vec<i32>, usize) {
|
|
2094
|
-
let counted = ChangeCounter::
|
|
2095
|
-
let mut guest = Guest::new(&[grant(
|
|
3262
|
+
let (counted, filesystem) = ChangeCounter::over_file();
|
|
3263
|
+
let mut guest = Guest::new(&[grant(filesystem, Access::ReadWrite, None)]);
|
|
2096
3264
|
let answers = exports.iter().map(|name| guest.call_bare(name)).collect();
|
|
2097
3265
|
(answers, counted.changes.load(Ordering::Relaxed))
|
|
2098
3266
|
}
|
|
@@ -2133,8 +3301,8 @@ fn unlinking_a_mounts_root_answers_that_it_is_a_directory() {
|
|
|
2133
3301
|
/// What reading `path` answers beneath a read-only Mount holding
|
|
2134
3302
|
/// `file.txt`.
|
|
2135
3303
|
fn read_beneath(path: (i32, i32)) -> i32 {
|
|
2136
|
-
let
|
|
2137
|
-
Guest::new(&[grant(Arc::new(
|
|
3304
|
+
let tmpfs: Tmpfs = [("file.txt", "granted")].into_iter().collect();
|
|
3305
|
+
Guest::new(&[grant(Arc::new(tmpfs), Access::Read, None)]).call("read_file", path)
|
|
2138
3306
|
}
|
|
2139
3307
|
|
|
2140
3308
|
// @behavior RT-086
|
|
@@ -2157,7 +3325,7 @@ fn a_dot_dot_after_a_missing_name_names_nothing() {
|
|
|
2157
3325
|
);
|
|
2158
3326
|
}
|
|
2159
3327
|
|
|
2160
|
-
// The
|
|
3328
|
+
// The Tmpfs would hold `file.txt` and any other name, so only the NUL
|
|
2161
3329
|
// refuses the open.
|
|
2162
3330
|
// @behavior RT-122
|
|
2163
3331
|
#[test]
|
|
@@ -2193,12 +3361,12 @@ fn a_path_ending_in_a_slash_names_only_a_directory() {
|
|
|
2193
3361
|
// @behavior RT-093
|
|
2194
3362
|
#[test]
|
|
2195
3363
|
fn a_rename_of_a_file_onto_a_path_ending_in_a_slash_is_not_a_directory() {
|
|
2196
|
-
let
|
|
2197
|
-
let mut guest = Guest::new(&[grant(
|
|
3364
|
+
let tmpfs = tmpfs_with("file.txt", "granted");
|
|
3365
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
2198
3366
|
assert_eq!(
|
|
2199
3367
|
(
|
|
2200
3368
|
guest.call_bare("rename_onto_itself_as_directory"),
|
|
2201
|
-
contents(
|
|
3369
|
+
contents(&host(&tmpfs), "file.txt")
|
|
2202
3370
|
),
|
|
2203
3371
|
(ERRNO_NOTDIR, Some(b"granted".to_vec())),
|
|
2204
3372
|
"path_rename of `file.txt` onto `file.txt/` beneath a Mount through preview1 must \
|
|
@@ -2209,12 +3377,12 @@ fn a_rename_of_a_file_onto_a_path_ending_in_a_slash_is_not_a_directory() {
|
|
|
2209
3377
|
// @behavior RT-090
|
|
2210
3378
|
#[test]
|
|
2211
3379
|
fn a_file_created_at_a_path_ending_in_a_slash_is_refused_as_a_directory() {
|
|
2212
|
-
let
|
|
2213
|
-
let mut guest = Guest::new(&[grant(
|
|
3380
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
3381
|
+
let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)]);
|
|
2214
3382
|
assert_eq!(
|
|
2215
3383
|
(
|
|
2216
3384
|
guest.call("write_file", NEW_AS_DIRECTORY),
|
|
2217
|
-
|
|
3385
|
+
host_entries(&tmpfs).map(|entries| entries.len())
|
|
2218
3386
|
),
|
|
2219
3387
|
(ERRNO_ISDIR, Ok(0)),
|
|
2220
3388
|
"path_open creating `new.txt/` beneath a Mount through preview1 must answer EISDIR and \
|
|
@@ -2231,57 +3399,75 @@ const RIGHTS_FD_WRITE: u64 = 1 << 6;
|
|
|
2231
3399
|
const RIGHTS_FD_READDIR: u64 = 1 << 14;
|
|
2232
3400
|
|
|
2233
3401
|
/// What opening `path` with `oflags` and `fdflags` answers beneath a
|
|
2234
|
-
/// read-write
|
|
3402
|
+
/// read-write Tmpfs holding `file.txt`, and the paths it holds after.
|
|
2235
3403
|
fn open_flagged(path: (i32, i32), oflags: i32, fdflags: i32) -> (i32, Vec<String>) {
|
|
2236
|
-
let
|
|
2237
|
-
let errno = Guest::new(&[grant(
|
|
3404
|
+
let tmpfs = tmpfs_with("file.txt", "granted");
|
|
3405
|
+
let errno = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, None)])
|
|
2238
3406
|
.call_flagged(path, oflags, fdflags);
|
|
2239
|
-
let held =
|
|
2240
|
-
.read_dir("")
|
|
2241
|
-
.expect("the root must list")
|
|
2242
|
-
.into_iter()
|
|
2243
|
-
.map(|entry| entry.name)
|
|
2244
|
-
.collect();
|
|
3407
|
+
let held = host_entries(&tmpfs).expect("the root must list");
|
|
2245
3408
|
(errno, held)
|
|
2246
3409
|
}
|
|
2247
3410
|
|
|
2248
3411
|
// @behavior RT-091
|
|
2249
3412
|
#[test]
|
|
2250
|
-
fn
|
|
3413
|
+
fn an_open_may_ask_for_synchronized_writes() {
|
|
2251
3414
|
assert_eq!(
|
|
2252
3415
|
open_flagged(FILE, 0, FDFLAGS_SYNC).0,
|
|
2253
|
-
|
|
2254
|
-
"path_open of `file.txt` with FDFLAGS_SYNC beneath a Mount through preview1 must
|
|
2255
|
-
|
|
3416
|
+
0,
|
|
3417
|
+
"path_open of `file.txt` with FDFLAGS_SYNC beneath a Mount through preview1 must succeed, \
|
|
3418
|
+
as Linux takes O_SYNC"
|
|
2256
3419
|
);
|
|
2257
3420
|
}
|
|
2258
3421
|
|
|
2259
3422
|
// @behavior RT-092
|
|
2260
3423
|
#[test]
|
|
2261
|
-
fn
|
|
3424
|
+
fn an_open_of_a_directory_answers_its_other_flags_as_linux_does() {
|
|
2262
3425
|
let held = vec!["file.txt".to_string()];
|
|
2263
3426
|
assert_eq!(
|
|
2264
3427
|
(
|
|
2265
3428
|
open_flagged(NEW_AS_DIRECTORY, OFLAGS_DIRECTORY | OFLAGS_CREAT, 0),
|
|
2266
|
-
open_flagged(FILE, OFLAGS_DIRECTORY | OFLAGS_TRUNC, 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)
|
|
2267
3436
|
),
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
must answer EINVAL and make nothing"
|
|
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"
|
|
2271
3439
|
);
|
|
2272
3440
|
}
|
|
2273
3441
|
|
|
3442
|
+
#[cfg(unix)]
|
|
2274
3443
|
const UP: (i32, i32) = (1070, 2);
|
|
3444
|
+
#[cfg(unix)]
|
|
2275
3445
|
const THROUGH_UPDIR: (i32, i32) = (1080, 7);
|
|
3446
|
+
#[cfg(unix)]
|
|
2276
3447
|
const IN_LINKED: (i32, i32) = (1090, 4);
|
|
3448
|
+
#[cfg(unix)]
|
|
2277
3449
|
const TWISTY: (i32, i32) = (1100, 6);
|
|
3450
|
+
#[cfg(unix)]
|
|
2278
3451
|
const DANGLE: (i32, i32) = (1110, 6);
|
|
3452
|
+
#[cfg(unix)]
|
|
2279
3453
|
const DIR_UP: (i32, i32) = (1120, 6);
|
|
3454
|
+
#[cfg(unix)]
|
|
2280
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);
|
|
2281
3466
|
|
|
2282
3467
|
/// A host directory whose `dir` holds links staying within it (`in`,
|
|
2283
|
-
/// `twisty`), links leading out of it (`up`, `updir`),
|
|
2284
|
-
///
|
|
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`).
|
|
2285
3471
|
#[cfg(unix)]
|
|
2286
3472
|
struct LinkedBeneath(std::path::PathBuf);
|
|
2287
3473
|
|
|
@@ -2291,7 +3477,8 @@ impl LinkedBeneath {
|
|
|
2291
3477
|
let host =
|
|
2292
3478
|
std::env::temp_dir().join(format!("kobako-beneath-{name}-{}", std::process::id()));
|
|
2293
3479
|
let _ = std::fs::remove_dir_all(&host);
|
|
2294
|
-
std::fs::create_dir_all(host.join("dir/sub"))
|
|
3480
|
+
std::fs::create_dir_all(host.join("dir/sub/inner"))
|
|
3481
|
+
.expect("the host directory must be created");
|
|
2295
3482
|
std::fs::create_dir_all(host.join("other")).expect("the host directory must be created");
|
|
2296
3483
|
for file in ["secret", "other/x", "dir/sub/y"] {
|
|
2297
3484
|
std::fs::write(host.join(file), "x").expect("the host file must be written");
|
|
@@ -2303,21 +3490,24 @@ impl LinkedBeneath {
|
|
|
2303
3490
|
("sub/../sub/y", "dir/twisty"),
|
|
2304
3491
|
("../escaped", "dir/dangle"),
|
|
2305
3492
|
("missing", "dir/nowhere"),
|
|
3493
|
+
("loop", "dir/loop"),
|
|
2306
3494
|
] {
|
|
2307
3495
|
std::os::unix::fs::symlink(target, host.join(link)).expect("the link must be made");
|
|
2308
3496
|
}
|
|
2309
3497
|
Self(host)
|
|
2310
3498
|
}
|
|
2311
3499
|
|
|
2312
|
-
/// A guest over this host directory as a
|
|
2313
|
-
/// Overlay's
|
|
3500
|
+
/// A guest over this host directory as a Bind Mount, or as an
|
|
3501
|
+
/// Overlay's lower Bind Layer beneath an empty upper.
|
|
2314
3502
|
fn guest(&self, overlay: bool) -> Guest {
|
|
2315
|
-
let directory = Arc::new(
|
|
2316
|
-
let filesystem: Arc<dyn
|
|
3503
|
+
let directory = Arc::new(Bind::new(&self.0).expect("the host directory must open"));
|
|
3504
|
+
let filesystem: Arc<dyn FileSystemType> = if overlay {
|
|
2317
3505
|
Arc::new(
|
|
2318
|
-
Overlay::new(
|
|
2319
|
-
.
|
|
2320
|
-
|
|
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"),
|
|
2321
3511
|
)
|
|
2322
3512
|
} else {
|
|
2323
3513
|
directory
|
|
@@ -2338,7 +3528,7 @@ impl Drop for LinkedBeneath {
|
|
|
2338
3528
|
// @behavior RT-140
|
|
2339
3529
|
#[cfg(unix)]
|
|
2340
3530
|
#[test]
|
|
2341
|
-
fn
|
|
3531
|
+
fn a_link_leading_out_of_an_opened_directory_escapes_it() {
|
|
2342
3532
|
let host = LinkedBeneath::new("out");
|
|
2343
3533
|
let answers = |overlay| {
|
|
2344
3534
|
let mut guest = host.guest(overlay);
|
|
@@ -2356,10 +3546,85 @@ fn a_link_leading_out_of_an_opened_directory_is_not_permitted() {
|
|
|
2356
3546
|
answers(true),
|
|
2357
3547
|
host.0.join("escaped").exists()
|
|
2358
3548
|
),
|
|
2359
|
-
([
|
|
3549
|
+
([ERRNO_NOTCAPABLE; 5], [ERRNO_NOTCAPABLE; 5], false),
|
|
2360
3550
|
"an open, a stat, or a create through preview1 of a path whose link leads out of a \
|
|
2361
|
-
directory opened beneath a
|
|
2362
|
-
|
|
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"
|
|
2363
3628
|
);
|
|
2364
3629
|
}
|
|
2365
3630
|
|
|
@@ -2379,38 +3644,37 @@ fn a_link_staying_within_an_opened_directory_is_followed() {
|
|
|
2379
3644
|
(answers(false), answers(true)),
|
|
2380
3645
|
([0; 2], [0; 2]),
|
|
2381
3646
|
"an open through preview1 of a path whose links stay within a directory opened beneath \
|
|
2382
|
-
a
|
|
3647
|
+
a Bind Mount, or an Overlay's Bind Layer, must reach the file they lead to"
|
|
2383
3648
|
);
|
|
2384
3649
|
}
|
|
2385
3650
|
|
|
2386
|
-
//
|
|
2387
|
-
// the refusal to create through a link it cannot resolve answers ELOOP.
|
|
3651
|
+
// An Overlay's change lands in its upper, so only a Bind shows it on the host.
|
|
2388
3652
|
// @behavior RT-142
|
|
2389
3653
|
#[cfg(unix)]
|
|
2390
3654
|
#[test]
|
|
2391
|
-
fn
|
|
2392
|
-
let
|
|
2393
|
-
|
|
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);
|
|
2394
3658
|
let errno = host
|
|
2395
3659
|
.guest(overlay)
|
|
2396
3660
|
.call("create_following_beneath_dir", NOWHERE);
|
|
2397
3661
|
(errno, host.0.join("dir/missing").exists())
|
|
2398
3662
|
};
|
|
2399
3663
|
assert_eq!(
|
|
2400
|
-
(answers(false), answers(true)),
|
|
2401
|
-
((
|
|
3664
|
+
(answers("dangle", false), answers("dangle-overlay", true)),
|
|
3665
|
+
((0, true), (0, false)),
|
|
2402
3666
|
"an open through preview1 creating a file through a link pointing at nothing beneath a \
|
|
2403
|
-
directory opened beneath a
|
|
2404
|
-
|
|
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"
|
|
2405
3669
|
);
|
|
2406
3670
|
}
|
|
2407
3671
|
|
|
2408
|
-
// The link names a file outside the host directory
|
|
2409
|
-
//
|
|
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.
|
|
2410
3674
|
// @behavior RT-143
|
|
2411
3675
|
#[cfg(unix)]
|
|
2412
3676
|
#[test]
|
|
2413
|
-
fn
|
|
3677
|
+
fn a_link_leading_out_of_a_mount_escapes_it() {
|
|
2414
3678
|
let host = LinkedBeneath::new("mount");
|
|
2415
3679
|
let outside =
|
|
2416
3680
|
std::env::temp_dir().join(format!("kobako-beneath-outside-{}", std::process::id()));
|
|
@@ -2420,9 +3684,9 @@ fn a_link_leading_out_of_a_mount_is_not_permitted() {
|
|
|
2420
3684
|
let errno = host.guest(false).call("open_following", DIR_UP);
|
|
2421
3685
|
let _ = std::fs::remove_file(&outside);
|
|
2422
3686
|
assert_eq!(
|
|
2423
|
-
errno,
|
|
2424
|
-
"an open through preview1 from a
|
|
2425
|
-
host directory must answer
|
|
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"
|
|
2426
3690
|
);
|
|
2427
3691
|
}
|
|
2428
3692
|
|
|
@@ -2430,7 +3694,7 @@ fn a_link_leading_out_of_a_mount_is_not_permitted() {
|
|
|
2430
3694
|
// of the bound and the access check decides between a trap and EBADF.
|
|
2431
3695
|
// @behavior RT-144
|
|
2432
3696
|
#[test]
|
|
2433
|
-
fn
|
|
3697
|
+
fn a_files_access_is_checked_before_a_buffer_outside_memory_is_bounded() {
|
|
2434
3698
|
let call = |name, rights| {
|
|
2435
3699
|
Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)])
|
|
2436
3700
|
.try_call_at(name, rights)
|
|
@@ -2442,13 +3706,14 @@ fn a_buffer_outside_memory_is_bounded_before_a_files_access_is_checked() {
|
|
|
2442
3706
|
call("pwrite_far_with", RIGHTS_FD_READ),
|
|
2443
3707
|
call("pread_far_with", RIGHTS_FD_WRITE),
|
|
2444
3708
|
],
|
|
2445
|
-
[
|
|
3709
|
+
[Some(ERRNO_BADF); 4],
|
|
2446
3710
|
"through preview1 on a file opened without the access each call needs, with a buffer \
|
|
2447
|
-
past memory, fd_read, fd_write,
|
|
2448
|
-
|
|
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"
|
|
2449
3713
|
);
|
|
2450
3714
|
}
|
|
2451
3715
|
|
|
3716
|
+
#[cfg(unix)]
|
|
2452
3717
|
const PUB_X: (i32, i32) = (1150, 5);
|
|
2453
3718
|
|
|
2454
3719
|
/// A Policy allowing `/data/pub/` alone, which swaps `pub` for a link to
|
|
@@ -2469,9 +3734,9 @@ impl Policy for SwapsWhileAsked {
|
|
|
2469
3734
|
}
|
|
2470
3735
|
}
|
|
2471
3736
|
|
|
2472
|
-
// The swap happens inside the Policy's own judgment,
|
|
2473
|
-
//
|
|
2474
|
-
//
|
|
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`.
|
|
2475
3740
|
// @behavior RT-145
|
|
2476
3741
|
#[cfg(unix)]
|
|
2477
3742
|
#[test]
|
|
@@ -2483,17 +3748,19 @@ fn a_link_swapped_in_after_a_policy_judges_a_path_is_not_followed() {
|
|
|
2483
3748
|
std::fs::write(host.join(dir).join("x"), dir).expect("the host file must be written");
|
|
2484
3749
|
}
|
|
2485
3750
|
let policy = SwapsWhileAsked(host.clone(), std::sync::atomic::AtomicBool::new(true));
|
|
2486
|
-
let directory =
|
|
2487
|
-
let
|
|
3751
|
+
let directory = Bind::new(&host).expect("the host directory must open");
|
|
3752
|
+
let mut guest = Guest::new(&[grant(
|
|
2488
3753
|
Arc::new(directory),
|
|
2489
3754
|
Access::Read,
|
|
2490
3755
|
Some(Arc::new(policy)),
|
|
2491
|
-
)])
|
|
2492
|
-
.call("
|
|
3756
|
+
)]);
|
|
3757
|
+
let answer = (guest.call("read_file", PUB_X), guest.read_back());
|
|
2493
3758
|
let _ = std::fs::remove_dir_all(&host);
|
|
2494
3759
|
assert_eq!(
|
|
2495
|
-
|
|
2496
|
-
"
|
|
2497
|
-
|
|
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"
|
|
2498
3765
|
);
|
|
2499
3766
|
}
|