kobako 0.28.0 → 0.30.0

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