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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +217 -10
  4. data/Cargo.lock +139 -141
  5. data/README.md +96 -574
  6. data/ROADMAP.md +2 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +269 -1
  8. data/crates/kobako-runtime/Cargo.toml +25 -15
  9. data/crates/kobako-runtime/README.md +7 -3
  10. data/crates/kobako-runtime/src/dispatch.rs +1 -1
  11. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  12. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  13. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  14. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  15. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  16. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  17. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  18. data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
  19. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  20. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  21. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
  29. data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
  30. data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
  31. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  32. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  33. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  37. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  38. data/crates/kobako-runtime/src/filesystem.rs +21 -387
  39. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  40. data/crates/kobako-runtime/src/lib.rs +11 -6
  41. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  42. data/crates/kobako-runtime/src/locking.rs +218 -0
  43. data/crates/kobako-runtime/src/mount.rs +31 -104
  44. data/crates/kobako-runtime/src/profile.rs +137 -1
  45. data/crates/kobako-runtime/src/sched.rs +210 -0
  46. data/crates/kobako-runtime/src/security.rs +696 -0
  47. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  48. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  49. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  50. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  51. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  52. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  53. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  54. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  55. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  56. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  57. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  58. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  59. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  60. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  61. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  62. data/crates/kobako-runtime/src/vfs.rs +60 -0
  63. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  64. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  65. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  66. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  67. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  68. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  70. data/crates/kobako-transport/CHANGELOG.md +8 -2
  71. data/crates/kobako-transport/Cargo.toml +3 -3
  72. data/crates/kobako-transport/README.md +6 -5
  73. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  74. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  75. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  76. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  77. data/crates/kobako-transport/src/envelope/fault.rs +2 -2
  78. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  79. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  80. data/crates/kobako-transport/src/envelope/reply.rs +2 -2
  81. data/crates/kobako-transport/src/envelope/request.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  83. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  84. data/crates/kobako-transport/src/envelope.rs +2 -2
  85. data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
  86. data/crates/kobako-wasmtime/Cargo.toml +15 -14
  87. data/crates/kobako-wasmtime/README.md +9 -5
  88. data/crates/kobako-wasmtime/src/abi.rs +12 -20
  89. data/crates/kobako-wasmtime/src/ambient.rs +1 -2
  90. data/crates/kobako-wasmtime/src/cache.rs +5 -5
  91. data/crates/kobako-wasmtime/src/config.rs +22 -7
  92. data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
  93. data/crates/kobako-wasmtime/src/driver.rs +29 -96
  94. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  95. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  96. data/crates/kobako-wasmtime/src/imports.rs +14 -8
  97. data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
  98. data/crates/kobako-wasmtime/src/invocation.rs +26 -46
  99. data/crates/kobako-wasmtime/src/lib.rs +1 -4
  100. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  101. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
  102. data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
  103. data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
  104. data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
  105. data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
  106. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  107. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
  108. data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
  109. data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
  110. data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
  111. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  112. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  113. data/crates/kobako-wasmtime/src/preview1.rs +464 -413
  114. data/crates/kobako-wasmtime/src/trap.rs +9 -9
  115. data/crates/kobako-wasmtime/src/wasi.rs +53 -37
  116. data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
  117. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  118. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
  119. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
  120. data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
  121. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
  122. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  123. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  124. data/data/kobako.wasm +0 -0
  125. data/ext/kobako/Cargo.toml +1 -1
  126. data/ext/kobako/src/runtime/bridge.rs +1 -1
  127. data/ext/kobako/src/runtime/errors.rs +1 -1
  128. data/ext/kobako/src/runtime/escape.rs +28 -14
  129. data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
  130. data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
  131. data/ext/kobako/src/runtime/filesystem.rs +109 -137
  132. data/ext/kobako/src/runtime/gvl.rs +43 -8
  133. data/ext/kobako/src/runtime/snapshot.rs +1 -1
  134. data/ext/kobako/src/runtime.rs +6 -1
  135. data/lib/kobako/catalog/extensions.rb +5 -5
  136. data/lib/kobako/catalog/flags.rb +2 -2
  137. data/lib/kobako/catalog/handles.rb +6 -6
  138. data/lib/kobako/catalog/mount.rb +2 -10
  139. data/lib/kobako/catalog/services.rb +2 -2
  140. data/lib/kobako/catalog/snippets.rb +7 -7
  141. data/lib/kobako/catalog.rb +1 -1
  142. data/lib/kobako/codec/decoder.rb +10 -10
  143. data/lib/kobako/codec/ext_types.rb +3 -5
  144. data/lib/kobako/codec/handle_walk.rb +4 -6
  145. data/lib/kobako/codec/nesting.rb +7 -7
  146. data/lib/kobako/codec/utils.rb +1 -1
  147. data/lib/kobako/codec/value_budget.rb +3 -3
  148. data/lib/kobako/context.rb +5 -5
  149. data/lib/kobako/filesystem.rb +44 -13
  150. data/lib/kobako/payload/arguments.rb +2 -2
  151. data/lib/kobako/pool.rb +4 -4
  152. data/lib/kobako/sandbox.rb +28 -28
  153. data/lib/kobako/sandbox_options.rb +2 -3
  154. data/lib/kobako/snippet/bytecode.rb +1 -1
  155. data/lib/kobako/snippet/source.rb +1 -1
  156. data/lib/kobako/transport/dispatcher.rb +5 -5
  157. data/lib/kobako/transport/exposure.rb +2 -2
  158. data/lib/kobako/transport/reflection.rb +7 -7
  159. data/lib/kobako/transport/run.rb +5 -5
  160. data/lib/kobako/transport/yielder.rb +1 -1
  161. data/lib/kobako/version.rb +1 -1
  162. data/lib/kobako.rb +34 -0
  163. data/release-please-config.json +1 -0
  164. data/sig/kobako/catalog/extensions.rbs +3 -3
  165. data/sig/kobako/catalog/handles.rbs +3 -3
  166. data/sig/kobako/catalog/mount.rbs +0 -4
  167. data/sig/kobako/catalog/services.rbs +1 -1
  168. data/sig/kobako/catalog/snippets.rbs +3 -3
  169. data/sig/kobako/catalog.rbs +1 -1
  170. data/sig/kobako/codec/decoder.rbs +1 -1
  171. data/sig/kobako/codec/handle_walk.rbs +1 -1
  172. data/sig/kobako/codec/nesting.rbs +2 -2
  173. data/sig/kobako/codec/utils.rbs +1 -1
  174. data/sig/kobako/codec/value_budget.rbs +1 -1
  175. data/sig/kobako/codec.rbs +1 -2
  176. data/sig/kobako/context.rbs +1 -1
  177. data/sig/kobako/filesystem.rbs +19 -14
  178. data/sig/kobako/guest_yielder.rbs +1 -1
  179. data/sig/kobako/payload/arguments.rbs +1 -1
  180. data/sig/kobako/pool.rbs +2 -2
  181. data/sig/kobako/runtime.rbs +1 -1
  182. data/sig/kobako/sandbox.rbs +3 -5
  183. data/sig/kobako/sandbox_options.rbs +1 -1
  184. data/sig/kobako/transport/dispatcher.rbs +2 -2
  185. data/sig/kobako/transport/exposure.rbs +1 -1
  186. data/sig/kobako/transport/reflection.rbs +2 -2
  187. data/sig/kobako/transport/run.rbs +3 -3
  188. data/sig/kobako/transport.rbs +1 -1
  189. metadata +67 -19
  190. data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
  191. data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
  192. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
  193. data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
  194. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  195. data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
  196. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
  197. data/crates/kobako-wasmtime/src/io.rs +0 -89
  198. data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
  199. data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
  200. /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
  201. /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::Access;
11
+ use kobako_runtime::filesystem::Bind;
12
+ use kobako_runtime::filesystem::Filesystem;
9
13
  use kobako_runtime::filesystem::{
10
- DirEntry, Directory, File, Filesystem, FilesystemError, Hold, Memory, Metadata, OpenOptions,
11
- Overlay, Rename, Temporary,
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::shared_engine;
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
- /// The answer of a test Filesystem holding no links.
26
- macro_rules! holds_no_links {
27
- () => {
28
- fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
29
- self.metadata(path)
30
- }
31
-
32
- fn canonicalize_beneath(&self, _base: &str, path: &str) -> Result<String, FilesystemError> {
33
- Ok(path.to_string())
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 answer of a test Filesystem every Invocation shares.
61
- macro_rules! is_shared {
62
- () => {
63
- fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
64
- None
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 ERRNO_FBIG: i32 = 22;
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
- /// Memory holds an `iovec` at 64 over 512..576, a `ciovec` at 80 over
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 0) (i64.const 2) (i32.const 0) (i32.const 128)))
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 0) (i64.const 2) (i32.const 0) (i32.const 128)))
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 0) (i64.const 2) (i32.const 0) (i32.const 128)))
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 `OpenOptions` with `set` applied.
406
- fn build_options(set: impl FnOnce(&mut OpenOptions)) -> OpenOptions {
407
- let mut options = OpenOptions::default();
408
- set(&mut options);
409
- options
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 Filesystem>,
478
+ filesystem: Arc<dyn FileSystemType>,
414
479
  access: Access,
415
480
  policy: Option<Arc<dyn Policy>>,
416
481
  ) -> Grant {
417
- let mount = Mount::new("/data", filesystem, access).expect("/data is a guest path");
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
- let engine = shared_engine().expect("shared engine must be constructible");
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: None,
542
+ memory_limit,
437
543
  stdout_limit: None,
438
544
  stderr_limit: None,
439
- open_file_limit: None,
440
- profile: Profile::Hermetic,
545
+ open_file_limit,
546
+ profile,
547
+ security,
441
548
  };
442
- let wasi = Wasi::for_invocation(&config, &[], grants).expect("WASI context must build");
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 list(&mut self, cookie: u64, len: i32) -> Vec<String> {
508
- let errno = self
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
- fn contents(filesystem: &dyn Filesystem, path: &str) -> Option<Vec<u8>> {
548
- let mut file = filesystem
549
- .open(path, &build_options(|options| options.read = true))
550
- .ok()?;
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.read_at(&mut buf, 0).ok()?;
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(filesystem: &dyn Filesystem, path: &str) {
559
- filesystem
560
- .open(
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
- &build_options(|options| (options.write, options.create) = (true, true)),
737
+ &flags_of(&how)
563
738
  )
564
- .expect("the file must be made");
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 Memory storing every name in upper case while it reaches each by
584
- /// any case, as a host that folds case does.
585
- struct StoresUpperCase(Memory);
586
-
587
- impl Filesystem for StoresUpperCase {
588
- holds_no_links!();
589
- never_repeats_identities!();
590
- is_shared!();
591
-
592
- fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
593
- self.0.open(path, options)
594
- }
595
-
596
- fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
597
- self.0.metadata(path)
598
- }
599
-
600
- fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
601
- Ok(path.to_uppercase())
602
- }
603
-
604
- fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
605
- self.0.read_dir(path)
606
- }
607
-
608
- fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
609
- self.0.create_dir(path)
610
- }
611
-
612
- fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
613
- self.0.remove_dir(path)
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
- fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
617
- self.0.remove_file(path)
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 rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
621
- self.0.rename(from, to, rename)
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(memory)),
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(Memory::new()), Access::Read, None)]);
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 memory: Arc<Memory> = Arc::new([("a.txt", "")].into_iter().collect());
671
- let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
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
- memory
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
- guest.list(0, 4096),
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 memory: Memory = [("file.txt", "granted")].into_iter().collect();
699
- let mut guest = Guest::new(&[grant(Arc::new(memory), Access::Read, None)]);
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 Memory's content"
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 memory = Arc::new(Memory::new());
712
- let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
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(memory.as_ref(), "file.txt").as_deref(),
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 Memory"
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(Memory::new()), Access::Read, None)]);
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 memory: Arc<Memory> = Arc::new([("file.txt", "granted")].into_iter().collect());
739
- let mut guest = Guest::new(&[grant(memory.clone(), Access::Read, None)]);
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(memory.as_ref(), "file.txt")
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 memory: Memory = [("file.txt", "granted")].into_iter().collect();
755
- let mut guest = Guest::new(&[grant(Arc::new(memory), Access::Read, None)]);
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 memory: Memory = [("file.txt", "granted"), ("dir/inner.txt", "beneath")]
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(memory), Access::Read, None)]);
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 before `change` runs on
781
- /// the Memory holding it.
782
- fn stat_after(change: impl FnOnce(&Memory)) -> i32 {
783
- let memory = file_and_directory();
784
- let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
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(&memory);
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
- // identity check keeps the descriptor from reaching it.
1197
+ // descriptor still standing for the removed one keeps it from reaching it.
796
1198
  // @behavior RT-117
797
1199
  #[test]
798
- fn a_directory_made_in_an_opened_directorys_place_is_not_reached_through_it() {
799
- let answers = (
800
- stat_after(|memory| {
801
- memory.rename("dir", "moved", Rename::Any).unwrap();
802
- memory.create_dir("dir").unwrap();
803
- overwrite(memory, "dir/inner");
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
- answers,
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 another directory stands at its path, moved in or made anew"
1210
+ answer ENOENT once the directory was removed and another made at its path"
817
1211
  );
818
1212
  }
819
1213
 
820
- /// A Memory naming `dir` by one inode number for as long as nothing holds
821
- /// it, as a host hands a removed directory's number to the next one made,
822
- /// and by a fresh number once a held `dir` was removed.
823
- struct ReusesInodes {
824
- memory: Arc<Memory>,
825
- holds: Arc<AtomicUsize>,
826
- removed_while_held: AtomicUsize,
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
- /// Counts as holding a directory until it drops.
830
- struct Holding(Arc<AtomicUsize>);
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
- impl Drop for Holding {
833
- fn drop(&mut self) {
834
- self.0.fetch_sub(1, Ordering::SeqCst);
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
- impl Filesystem for ReusesInodes {
839
- holds_no_links!();
840
- matches_names_exactly!();
841
- is_shared!();
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
- fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
844
- self.memory.open(path, options)
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
- fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
848
- let found = self.memory.metadata(path)?;
849
- if path != "dir" {
850
- return Ok(found);
851
- }
852
- let ino = 100 + self.removed_while_held.load(Ordering::SeqCst) as u64;
853
- Ok(found.with_identity(1, ino))
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
- fn hold_directory(&self, path: &str) -> Result<Option<Hold>, FilesystemError> {
857
- if path != "dir" {
858
- return Ok(None);
859
- }
860
- self.holds.fetch_add(1, Ordering::SeqCst);
861
- Ok(Some(Hold::new(Holding(self.holds.clone()))))
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
- fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
865
- self.memory.read_dir(path)
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
- fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
869
- self.memory.create_dir(path)
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 remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
873
- self.memory.remove_dir(path)?;
874
- if self.holds.load(Ordering::SeqCst) > 0 {
875
- self.removed_while_held.fetch_add(1, Ordering::SeqCst);
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
- fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
881
- self.memory.remove_file(path)
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
- fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
885
- self.memory.rename(from, to, rename)
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 a Hold
890
- // on it lives, so only the descriptor keeping its Hold refuses the call.
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 memory = file_and_directory();
895
- let reuses = Arc::new(ReusesInodes {
896
- memory: memory.clone(),
897
- holds: Arc::new(AtomicUsize::new(0)),
898
- removed_while_held: AtomicUsize::new(0),
899
- });
900
- let mut guest = Guest::new(&[grant(reuses.clone(), Access::ReadWrite, None)]);
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.remove_file("dir/inner").unwrap();
907
- reuses.remove_dir("dir").unwrap();
908
- reuses.create_dir("dir").unwrap();
909
- overwrite(memory.as_ref(), "dir/inner");
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 upper: Arc<dyn Filesystem> = Arc::new([("dir/upper", "u")].into_iter().collect::<Memory>());
925
- let lower: Arc<dyn Filesystem> = file_and_directory();
926
- let overlay = Overlay::new()
927
- .layer(upper, Access::ReadWrite)
928
- .layer(lower, Access::Read);
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(|memory| {
942
- memory.rename("dir", "moved", Rename::Any).unwrap();
943
- memory.rename("moved", "dir", Rename::Any).unwrap();
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 memory: Memory = [("file.txt", "granted")].into_iter().collect();
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(memory), Access::Read, Some(policy))]);
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 a_policy_decides_in_the_place_of_a_mounts_access() {
968
- let memory = Arc::new(Memory::new());
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(memory.clone(), Access::Read, Some(policy))]);
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
- contents(memory.as_ref(), "file.txt").as_deref(),
974
- Some(&b"hello"[..]),
975
- "a write a Policy allows beneath a read-only Mount through preview1 must land"
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 each_invocation_is_handed_the_filesystem_it_starts_from() {
982
- let grants = [grant(Arc::new(Temporary::new()), Access::ReadWrite, None)];
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
- ERRNO_NOENT,
991
- "a file an earlier invocation wrote to a Temporary must not be found by the next one"
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 Memory holding `file.txt` that counts every change it is asked for,
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
- fn new() -> Arc<Self> {
1004
- Arc::new(Self {
1005
- memory: [("file.txt", "granted")].into_iter().collect(),
1006
- changes: AtomicUsize::new(0),
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 Filesystem for ChangeCounter {
1016
- holds_no_links!();
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
- self.memory.create_dir(path)
1668
+ inner.mkdir(dentry)
1036
1669
  }
1037
1670
 
1038
- fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
1671
+ fn rmdir(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
1039
1672
  self.change();
1040
- self.memory.remove_dir(path)
1673
+ inner.rmdir(dentry)
1041
1674
  }
1042
1675
 
1043
- fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
1676
+ fn unlink(&self, inner: &dyn InodeOperations, dentry: &Dentry) -> Result<(), FilesystemError> {
1044
1677
  self.change();
1045
- self.memory.remove_file(path)
1678
+ inner.unlink(dentry)
1046
1679
  }
1047
1680
 
1048
- fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
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
- self.memory.rename(from, to, rename)
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, checked as the Filesystem moves it, keeps `secret` from
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 swapped = SwappedForADirectory::new();
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(swapped.clone(), Access::ReadWrite, Some(policy))]);
1701
+ let mut guest = Guest::new(&[grant(tmpfs.clone(), Access::ReadWrite, Some(policy))]);
1115
1702
  assert_eq!(
1116
1703
  (
1117
- guest.call_bare("rename_to_new"),
1118
- swapped.metadata("new.txt/secret")
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, Err(FilesystemError::NotFound)),
1121
- "path_rename of what is a directory by the time it moves, beneath a Mount with a \
1122
- Policy, through preview1 must answer EXDEV and move nothing"
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 Memory whose files take another writer's `bbb` at their end as one
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(Memory);
1722
+ struct AppendedMeanwhile;
1130
1723
 
1131
1724
  struct AppendedMeanwhileFile {
1132
- file: Box<dyn File>,
1133
- other: std::sync::Mutex<Box<dyn File>>,
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
- .append(b"bbb")
1734
+ .write_iter(&mut Kiocb::at(0).with_append(), b"bbb")
1142
1735
  .map(drop)
1143
1736
  }
1144
1737
  }
1145
1738
 
1146
- impl File for AppendedMeanwhileFile {
1147
- fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
1148
- self.file.read_at(buf, offset)
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 set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
1164
- self.file.set_len(len)
1165
- }
1166
-
1167
- fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
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 Filesystem for AppendedMeanwhile {
1174
- holds_no_links!();
1175
- matches_names_exactly!();
1176
- never_repeats_identities!();
1177
- is_shared!();
1178
-
1179
- fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
1180
- let writes = build_options(|options| options.write = true);
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: self.0.open(path, options)?,
1183
- other: std::sync::Mutex::new(self.0.open(path, &writes)?),
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 memory: Memory = [("file.txt", "granted")].into_iter().collect();
1218
- let shared = Arc::new(AppendedMeanwhile(memory));
1219
- let mut guest = Guest::new(&[grant(shared.clone(), Access::ReadWrite, None)]);
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(&shared.0, "file.txt")
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 Directory
1798
+ /// made by `make_target`, and a guest handed it as a read-only Bind
1238
1799
  /// Mount.
1239
1800
  #[cfg(unix)]
1240
- fn linked(name: &str, make_target: fn(&std::path::Path)) -> (std::path::PathBuf, Guest) {
1241
- linked_under(name, make_target, Access::Read, None)
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 linked_under(
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 = Directory::new(&host).expect("the host directory must open");
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 Directory Mount whose host directory
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) = linked(name, make_target);
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 = Directory::new(&host).expect("the host directory must open");
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 Directory Mount through preview1 must report an \
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 Directory Mount through preview1 must report \
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 Directory Mount through preview1 must report the directory, \
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 Directory Mount taking
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) = linked_under(name, directory_with_key, Access::ReadWrite, policy);
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 Directory Mount with or without a Policy through preview1, \
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 = Directory::new(&host).expect("the host directory must open");
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 Directory Mount through preview1, must \
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 an_allocation_for_a_file_beneath_a_mount_is_unsupported() {
1445
- let memory: Arc<Memory> = Arc::new([("file.txt", "granted")].into_iter().collect());
1446
- let answer =
1447
- Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]).call("allocate_file", FILE);
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
- (answer, contents(memory.as_ref(), "file.txt")),
1450
- (ERRNO_NOTSUP, Some(b"granted".to_vec())),
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) = linked("open-nofollow", |target| {
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 Directory Mount \
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 Policy allows everything, so only the link on the way can refuse
1486
- // these; the same calls beneath a Mount without a Policy go through.
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 a_path_through_a_link_beneath_a_mount_with_a_policy_answers_eloop() {
1491
- let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
1492
- let (host, mut guest) = linked_under(
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(policy),
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 (host, mut guest) = linked_under("plain-link", directory_with_key, Access::Read, None);
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
- ([ERRNO_LOOP; 3], true, [0, 0]),
1514
- "path_open and path_unlink_file through a host link, or path_open following one at the \
1515
- end, beneath a Mount with a Policy through preview1 must answer ELOOP and change \
1516
- nothing, while a Mount without a Policy follows the link"
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
- // cap-std refuses to follow a link out of the host directory, and the
1521
- // host refuses a loop, so each open is refused whichever answers first;
1522
- // only ELOOP says the link is why. An unlink acts on the link itself,
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 a_link_out_of_a_directory_or_into_a_loop_answers_eloop_beneath_a_policy() {
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 = Directory::new(&host).expect("the host directory must open");
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
- (vec![([ERRNO_LOOP, 0], true); 3], true),
1565
- "path_open through a host link out of a Directory, by an absolute or a relative target, \
1566
- or into a loop, beneath a Mount with a Policy through preview1 must answer ELOOP, and \
1567
- path_unlink_file of each link must remove it"
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
- // no answer from but the walk for links on its way does.
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 a_link_check_that_fails_fails_the_call_beneath_a_policy() {
1576
- let failing = Arc::new(FailsLookingUpDir(
1577
- [("dir/file.txt", "granted")].into_iter().collect(),
1578
- ));
1579
- let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
1580
- let mut guest = Guest::new(&[grant(failing, Access::Read, Some(policy))]);
1581
- assert_eq!(
1582
- guest.call("read_file", DIR_FILE),
1583
- ERRNO_IO,
1584
- "path_open of a file beneath a directory the Filesystem fails to look up, beneath a \
1585
- Mount with a Policy through preview1, must answer that failure"
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
- fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
1638
- self.0.rename(from, to, rename)
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
- // The Mount has no Policy and follows links, so only the step out of
1643
- // the link can refuse these.
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 a_step_out_of_a_link_answers_eloop() {
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
- [ERRNO_LOOP; 2],
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 Directory Mount through preview1 must answer ELOOP"
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 file_and_directory() -> Arc<Memory> {
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 memory = file_and_directory();
1677
- let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
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
- (answers, memory.metadata("missing").is_err()),
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 memory = file_and_directory();
1696
- let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
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
- (answers, memory.metadata("dir/inner").is_ok()),
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
- ERRNO_INVAL,
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 EBUSY as wasmtime-wasi does, or ENOTDIR \
1720
- where a file stands before the dot, and leave the directory in place"
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 = Directory::new(&host).expect("the host directory must open");
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 Directory Mount \
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 a_seek_past_the_largest_offset_overflows() {
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, ERRNO_OVERFLOW,
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 EOVERFLOW, as Linux does"
2722
+ preview1 must answer EINVAL, as Linux does"
1783
2723
  );
1784
2724
  }
1785
2725
 
1786
- // A Memory clamps a read past its end and tries to grow for a write or 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
- Memory file beneath a Mount through preview1 must answer EINVAL, as Linux does"
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 a_wait_to_write_a_file_answers_by_what_its_mount_permits() {
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, ERRNO_PERM),
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 Mount and answer EPERM beneath a read-only one, as wasmtime-wasi \
1837
- answers by its preopen"
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
- (ERRNO_PERM, ERRNO_INVAL, 0),
1856
- "fd_filestat_set_size on a file opened only to read through preview1 must answer EPERM \
1857
- beneath a read-only Mount, as wasmtime-wasi does, and EINVAL beneath a read-write one, \
1858
- as Linux's ftruncate does, while a file opened to write is sized"
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 the Policy standing in its access's place lets either through.
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::Read,
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-only Mount whose Policy allows every change, \
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, where
1908
- // nothing stands yet, makes it a change.
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 a_policy_is_asked_once_for_an_open_that_may_create_as_what_it_does() {
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::Read,
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
- (0, vec![("/data/file.txt".to_owned(), Access::Read)]),
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 once for \
1931
- each, as a change where it creates and as a read where the file stands"
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
- // A Memory would refuse the bytes for want of space, so only the refusal
1981
- // before the Filesystem is asked answers EFBIG.
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 a_write_at_the_largest_signed_offset_is_too_large() {
1985
- let memory = file_and_directory();
1986
- let answer = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)])
1987
- .call_bare("write_at_the_largest_offset");
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
- (answer, contents(memory.as_ref(), "file.txt")),
1990
- (ERRNO_FBIG, Some(b"granted".to_vec())),
1991
- "fd_write at the largest signed 64-bit offset of a Memory file beneath a Mount through \
1992
- preview1 must answer EFBIG, as Linux does, and leave the file as it was"
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 an_advice_past_the_largest_signed_offset_is_invalid() {
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 from or for past the largest signed 64-bit offset of a Memory file beneath a \
2008
- Mount through preview1 must answer EINVAL, as an advice within it succeeds"
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 the refusal of not blocking.
3091
+ // stands in for a refusal of not blocking.
2014
3092
  // @behavior RT-129
2015
3093
  #[test]
2016
- fn a_descriptor_that_would_not_block_is_unsupported() {
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
- (ERRNO_NOTSUP, ERRNO_NOTSUP, 0),
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 answer ENOTSUP"
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 memory = file_and_directory();
2056
- let answer = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]).call_pair(
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(memory.as_ref(), "file.txt")),
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) = linked_under("dangling", |_| {}, Access::ReadWrite, None);
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 Directory Mount through \
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::new();
2095
- let mut guest = Guest::new(&[grant(counted.clone(), Access::ReadWrite, None)]);
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 memory: Memory = [("file.txt", "granted")].into_iter().collect();
2137
- Guest::new(&[grant(Arc::new(memory), Access::Read, None)]).call("read_file", path)
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 Memory would hold `file.txt` and any other name, so only the NUL
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 memory: Arc<Memory> = Arc::new([("file.txt", "granted")].into_iter().collect());
2197
- let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
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(memory.as_ref(), "file.txt")
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 memory = Arc::new(Memory::new());
2213
- let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
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
- memory.read_dir("").map(|entries| entries.len())
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 Memory holding `file.txt`, and the paths it holds after.
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 memory: Arc<Memory> = Arc::new([("file.txt", "granted")].into_iter().collect());
2237
- let errno = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)])
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 = memory
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 an_open_asking_for_synchronized_writes_is_unsupported() {
3413
+ fn an_open_may_ask_for_synchronized_writes() {
2251
3414
  assert_eq!(
2252
3415
  open_flagged(FILE, 0, FDFLAGS_SYNC).0,
2253
- ERRNO_NOTSUP,
2254
- "path_open of `file.txt` with FDFLAGS_SYNC beneath a Mount through preview1 must answer \
2255
- ENOTSUP"
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 an_open_of_a_directory_that_creates_or_truncates_is_invalid() {
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
- ((ERRNO_INVAL, held.clone()), (ERRNO_INVAL, held)),
2269
- "path_open with OFLAGS_DIRECTORY and CREAT or TRUNC beneath a Mount through preview1 \
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`), and links pointing
2284
- /// at nothing out of it (`dangle`) and within it (`nowhere`).
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")).expect("the host directory must be created");
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 Directory Mount, or as an
2313
- /// Overlay's writable Directory Layer above an empty Memory.
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(Directory::new(&self.0).expect("the host directory must open"));
2316
- let filesystem: Arc<dyn Filesystem> = if overlay {
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
- .layer(directory, Access::ReadWrite)
2320
- .layer(Arc::new(Memory::new()), Access::Read),
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 a_link_leading_out_of_an_opened_directory_is_not_permitted() {
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
- ([ERRNO_PERM; 5], [ERRNO_PERM; 5], false),
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 Directory Mount, or an Overlay's Directory Layer, must answer \
2362
- EPERM, as wasmtime-wasi does, and create nothing"
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 Directory Mount, or an Overlay's Directory Layer, must reach the file they lead to"
3647
+ a Bind Mount, or an Overlay's Bind Layer, must reach the file they lead to"
2383
3648
  );
2384
3649
  }
2385
3650
 
2386
- // The link's target lies within `dir`, where Linux would create it, so only
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 a_create_through_a_dangling_link_beneath_an_opened_directory_is_refused() {
2392
- let host = LinkedBeneath::new("dangle");
2393
- let answers = |overlay| {
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
- ((ERRNO_LOOP, false), (ERRNO_LOOP, false)),
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 Directory Mount, or an Overlay's Directory Layer, must answer \
2404
- ELOOP and create nothing"
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, so cap-std refuses it
2409
- // with no OS code, which wasmtime-wasi answers as EPERM.
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 a_link_leading_out_of_a_mount_is_not_permitted() {
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, ERRNO_PERM,
2424
- "an open through preview1 from a Directory Mount's root of a link to a file outside its \
2425
- host directory must answer EPERM, as wasmtime-wasi does"
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 a_buffer_outside_memory_is_bounded_before_a_files_access_is_checked() {
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
- [None, None, None, Some(ERRNO_BADF)],
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, and fd_pwrite must trap, and fd_pread must answer \
2448
- EBADF, as wasmtime-wasi does"
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, so only a call that
2473
- // reaches the path without following the link swapped in meanwhile is
2474
- // refused; a plain Directory would read `secret/x`.
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 = Directory::new(&host).expect("the host directory must open");
2487
- let errno = Guest::new(&[grant(
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("open_following", PUB_X);
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
- errno, ERRNO_LOOP,
2496
- "an open through preview1 beneath a Directory Mount with a Policy, of a path whose \
2497
- directory another writer swaps for a link as the Policy judges it, must answer ELOOP"
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
  }