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,2665 @@
1
+ //! `Bind`: a Filesystem over a host directory, from which nothing
2
+ //! outside it is reachable.
3
+ //!
4
+ //! The runtime hands a Bind one name at a time and follows every link
5
+ //! itself, so the VFS keeps the guest inside. Each name still resolves
6
+ //! through `cap_std::fs::Dir`, or the host's kernel where it confines a
7
+ //! path itself, which refuses a `..` or a link leading out of the
8
+ //! directory whatever a caller hands over, and answers alike on every
9
+ //! host, Windows among them.
10
+
11
+ mod fid;
12
+ mod lookup;
13
+ mod walk;
14
+
15
+ use std::any::Any;
16
+ use std::io::{self, Read, Seek, SeekFrom, Write};
17
+ use std::path::Path;
18
+ use std::sync::{Arc, Mutex, PoisonError, Weak};
19
+
20
+ use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt, SystemTimeSpec};
21
+ use cap_std::ambient_authority;
22
+ use cap_std::fs::{Dir, OpenOptions as CapOpenOptions};
23
+
24
+ use super::rdir::Rdir;
25
+ use super::{
26
+ DentryOperations, DirEntry, FileOperations, FileSystemType, FileType, Filesystem,
27
+ FilesystemError, Finish, Iattr, InodeOperations, Kiocb, Kstat, Lookup, OpenHow, RenameFlag,
28
+ SetTime,
29
+ };
30
+ use crate::vfs::{
31
+ generic_file_llseek, generic_read_dir, Dentry, DirContext, Identity, Inode, Whence,
32
+ };
33
+ use fid::{FidCache, OpenDirFid};
34
+
35
+ /// A host directory and everything beneath it. What a guest writes lands
36
+ /// on the host and lasts as long as the directory does.
37
+ ///
38
+ /// Every entry beneath it answers through a Bind too, finding what a call is
39
+ /// about through the dentry it is handed, as Linux's 9p does. A directory is
40
+ /// opened on the host when first asked, as 9p walks to a dentry's fid only
41
+ /// when a call needs one. One a guest holds open holds a host descriptor; the
42
+ /// others a walk asked about hold one only up to a bound.
43
+ #[derive(Clone)]
44
+ pub struct Bind {
45
+ /// The host directory the Bind was made at.
46
+ root: Arc<Dir>,
47
+ fids: Arc<FidCache>,
48
+ }
49
+
50
+ impl Bind {
51
+ /// Open the host directory at `path`, the one place this Filesystem
52
+ /// reaches outside itself. One the host lets no one read is refused, so its
53
+ /// root answers an open to read as any directory beneath it does, on every
54
+ /// host alike.
55
+ pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
56
+ let root = Dir::open_ambient_dir(path, ambient_authority())?;
57
+ may_read_dir(&root).map_err(|_| io::Error::from(io::ErrorKind::PermissionDenied))?;
58
+ Ok(Self {
59
+ root: Arc::new(root),
60
+ fids: Arc::default(),
61
+ })
62
+ }
63
+
64
+ /// The host directory kept for `dentry`, if one is.
65
+ fn find_fid(&self, dentry: &Dentry) -> Option<Arc<Dir>> {
66
+ if dentry.is_root() {
67
+ return Some(self.root.clone());
68
+ }
69
+ OpenDirFid::get(dentry).or_else(|| self.fids.get(dentry))
70
+ }
71
+
72
+ /// The host directory `dentry` names, as 9p's `v9fs_fid_lookup` finds a
73
+ /// fid: the one kept for it, or the names beneath the nearest directory
74
+ /// kept, read from the dentries as `build_path_from_dentry` reads them.
75
+ /// 9p holds the rename lock through a walk; one here compares what it found
76
+ /// with what the dentry recorded instead, as NFS compares a file handle.
77
+ fn fid_lookup(&self, dentry: &Dentry) -> Result<Arc<Dir>, FilesystemError> {
78
+ if let Some(held) = self.find_fid(dentry) {
79
+ return Ok(held);
80
+ }
81
+ let mut names = vec![dentry.name()];
82
+ let mut at = dentry.parent();
83
+ let above = loop {
84
+ if let Some(held) = self.find_fid(&at) {
85
+ break held;
86
+ }
87
+ names.push(at.name());
88
+ at = at.parent();
89
+ };
90
+ names.reverse();
91
+ let recorded = dentry.inode().and_then(|inode| inode.identity());
92
+ let stands = |dir: &Dir| {
93
+ recorded.is_none()
94
+ || dir
95
+ .dir_metadata()
96
+ .is_ok_and(|found| Identity::of(&to_kstat(found)) == recorded)
97
+ };
98
+ let dir = walk::walk(&above, &names)?;
99
+ if !stands(&dir) {
100
+ return Err(FilesystemError::NotFound);
101
+ }
102
+ let dir = Arc::new(dir);
103
+ self.fids.put(dentry, &dir);
104
+ Ok(dir)
105
+ }
106
+
107
+ /// Answer `call` with the directory above `dentry` and the name it
108
+ /// carries as the host spells it.
109
+ fn at<T>(
110
+ &self,
111
+ dentry: &Dentry,
112
+ call: impl FnOnce(&Dir, &str) -> Result<T, FilesystemError>,
113
+ ) -> Result<T, FilesystemError> {
114
+ let (parent, name) = dentry.parent_and_name();
115
+ call(&*self.fid_lookup(&parent)?, to_host_name(&name)?)
116
+ }
117
+
118
+ /// Open the file `dentry` names and keep its descriptor on the inode, so
119
+ /// the entry answers from it however its name changes, as 9p's
120
+ /// `v9fs_open_fid_add` keeps an open fid. What this made is recorded
121
+ /// first, as `->atomic_open` calls `d_instantiate`.
122
+ fn open_file(
123
+ &self,
124
+ dentry: &Dentry,
125
+ how: &OpenHow,
126
+ create: bool,
127
+ ) -> Result<Box<dyn FileOperations>, FilesystemError> {
128
+ let file = self.at(dentry, |dir, name| open_in(dir, name, how, create))?;
129
+ let opened = to_kstat(cap_std::fs::Metadata::from_file(&file).map_err(to_error)?);
130
+ match dentry.inode().map(|inode| inode.identity()) {
131
+ None => dentry.add(Some(Inode::new(
132
+ &opened,
133
+ Arc::new(BindRegular(self.clone())),
134
+ ))),
135
+ // A host path may by now hold another entry than the one judged,
136
+ // which only what opened can tell, as NFS's open compares the file
137
+ // handle it gets back.
138
+ Some(Some(identity)) if Identity::of(&opened) != Some(identity) => {
139
+ return Err(FilesystemError::NotFound)
140
+ }
141
+ Some(_) => {}
142
+ }
143
+ let file = Arc::new(file);
144
+ let inode = dentry.inode().ok_or(FilesystemError::NotFound)?;
145
+ let kept = inode
146
+ .private()
147
+ .get_or_init(|| Arc::new(OpenFids::default()))
148
+ .clone();
149
+ if let Ok(fids) = kept.downcast::<OpenFids>() {
150
+ let mut fids = fids.0.lock().unwrap_or_else(PoisonError::into_inner);
151
+ fids.retain(|(fid, _)| fid.strong_count() > 0);
152
+ fids.push((Arc::downgrade(&file), how.write || how.truncate));
153
+ }
154
+ Ok(Box::new(BindFile { file }))
155
+ }
156
+
157
+ /// Whether the entry `dentry` recorded still stands at its name in the
158
+ /// directory above, as a walk asks each name once the one above it stood.
159
+ /// A host may reuse an inode number for what it made in its place, so the
160
+ /// kind must match too, as NFS compares a file handle's attributes.
161
+ fn stands_as_recorded(&self, dentry: &Dentry) -> Result<bool, FilesystemError> {
162
+ let inode = dentry.inode();
163
+ if inode
164
+ .as_ref()
165
+ .is_some_and(|inode| inode.identity().is_none())
166
+ {
167
+ return Ok(true);
168
+ }
169
+ let found = self.at(dentry, |dir, name| {
170
+ dir.symlink_metadata(name).map_err(to_error)
171
+ });
172
+ match (found, inode) {
173
+ (Ok(found), Some(inode)) => {
174
+ let found = to_kstat(found);
175
+ Ok(Identity::of(&found) == inode.identity() && found.file_type == inode.kind())
176
+ }
177
+ (Ok(_), None) => Ok(false),
178
+ (Err(FilesystemError::NotFound), None) => Ok(true),
179
+ (
180
+ Err(
181
+ FilesystemError::NotFound
182
+ | FilesystemError::NotADirectory
183
+ | FilesystemError::FilesystemLoop,
184
+ ),
185
+ _,
186
+ ) => Ok(false),
187
+ (Err(err), _) => Err(err),
188
+ }
189
+ }
190
+ }
191
+
192
+ /// The names `dir` holds, each with the kind and the identity a listing gives.
193
+ fn entries_of(dir: &Dir) -> Result<Vec<DirEntry>, FilesystemError> {
194
+ #[cfg(unix)]
195
+ let dev = {
196
+ use cap_std::fs::MetadataExt;
197
+ dir.dir_metadata().map_err(to_error)?.dev()
198
+ };
199
+ dir.entries()
200
+ .map_err(to_error)?
201
+ .map(|entry| {
202
+ let entry = entry.map_err(to_error)?;
203
+ let name = entry
204
+ .file_name()
205
+ .into_string()
206
+ .map_err(|_| FilesystemError::IllegalSequence)?;
207
+ let listed = DirEntry::new(name, to_file_type(entry.file_type().map_err(to_error)?));
208
+ #[cfg(unix)]
209
+ let listed = {
210
+ use std::os::unix::fs::DirEntryExt;
211
+ listed.with_identity(dev, entry.ino())
212
+ };
213
+ Ok(listed)
214
+ })
215
+ .collect()
216
+ }
217
+
218
+ /// The directory `name` in `dir`, a link there refused as a loop, as
219
+ /// Linux's `RESOLVE_NO_SYMLINKS` refuses one, where the host names it no
220
+ /// directory.
221
+ fn open_dir_nofollow(dir: &Dir, name: &str) -> Result<Dir, FilesystemError> {
222
+ dir.open_dir_nofollow(name)
223
+ .map_err(|err| nofollow_error(dir, name, err))
224
+ }
225
+
226
+ /// Why an open of `name` in `dir` that follows no link failed, a link there
227
+ /// named a loop, as Linux's `O_NOFOLLOW` answers `ELOOP`, where the host
228
+ /// names it otherwise.
229
+ fn nofollow_error(dir: &Dir, name: &str, err: io::Error) -> FilesystemError {
230
+ match dir.symlink_metadata(name) {
231
+ Ok(metadata) if metadata.file_type().is_symlink() => FilesystemError::FilesystemLoop,
232
+ _ => to_error(err),
233
+ }
234
+ }
235
+
236
+ /// `name` as `cap_std` takes it. Windows reads some names as other names or as
237
+ /// devices, so there one is refused rather than reached as an entry a Mount's
238
+ /// Policy never saw.
239
+ fn to_host_name(name: &str) -> Result<&str, FilesystemError> {
240
+ if cfg!(windows) && is_windows_alias(name) {
241
+ return Err(FilesystemError::InvalidInput);
242
+ }
243
+ Ok(name)
244
+ }
245
+
246
+ /// Whether Windows reads `name` as something else: a `\` as a separator, a `:`
247
+ /// as a stream of a file, a trailing `.` or space as nothing, a wildcard as a
248
+ /// pattern, or a device's name, whatever extension follows, as the device.
249
+ fn is_windows_alias(name: &str) -> bool {
250
+ const DEVICES: [&str; 33] = [
251
+ "CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$", "CLOCK$", "COM0", "COM1", "COM2", "COM3",
252
+ "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1",
253
+ "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³",
254
+ ];
255
+ let reserved = |c: char| c.is_ascii_control() || "\\:*?\"<>|".contains(c);
256
+ let stem = name.split('.').next().unwrap_or(name).trim_end();
257
+ name.contains(reserved)
258
+ || name.ends_with(['.', ' '])
259
+ || DEVICES.contains(&stem.to_uppercase().as_str())
260
+ }
261
+
262
+ /// The FilesystemError a host failure names. cap-std's own refusal of a path
263
+ /// leading out of the directory carries no OS code. A Bind hands it one name and
264
+ /// no `..`, so that refusal only says a move carried the name out of the
265
+ /// directory as it was opened, as `complete_walk` answers EXDEV, and nothing
266
+ /// stands at it now. A link loop stays unnamed until std's `FilesystemLoop` is
267
+ /// stable.
268
+ fn to_error(err: io::Error) -> FilesystemError {
269
+ match err.kind() {
270
+ io::ErrorKind::PermissionDenied if err.raw_os_error().is_none() => {
271
+ FilesystemError::NotFound
272
+ }
273
+ io::ErrorKind::NotFound => FilesystemError::NotFound,
274
+ io::ErrorKind::AlreadyExists => FilesystemError::AlreadyExists,
275
+ io::ErrorKind::NotADirectory => FilesystemError::NotADirectory,
276
+ io::ErrorKind::IsADirectory => FilesystemError::IsADirectory,
277
+ io::ErrorKind::DirectoryNotEmpty => FilesystemError::DirectoryNotEmpty,
278
+ io::ErrorKind::PermissionDenied => FilesystemError::PermissionDenied,
279
+ io::ErrorKind::ReadOnlyFilesystem => FilesystemError::ReadOnlyFilesystem,
280
+ io::ErrorKind::InvalidInput => FilesystemError::InvalidInput,
281
+ io::ErrorKind::Unsupported => FilesystemError::Unsupported,
282
+ io::ErrorKind::StorageFull => FilesystemError::StorageFull,
283
+ io::ErrorKind::FileTooLarge => FilesystemError::FileTooLarge,
284
+ io::ErrorKind::CrossesDevices => FilesystemError::CrossesDevices,
285
+ io::ErrorKind::Deadlock => FilesystemError::Deadlock,
286
+ io::ErrorKind::ResourceBusy => FilesystemError::ResourceBusy,
287
+ io::ErrorKind::InvalidFilename => FilesystemError::InvalidFilename,
288
+ io::ErrorKind::NotSeekable => FilesystemError::NotSeekable,
289
+ io::ErrorKind::Interrupted => FilesystemError::Interrupted,
290
+ io::ErrorKind::WouldBlock => FilesystemError::WouldBlock,
291
+ io::ErrorKind::OutOfMemory => FilesystemError::OutOfMemory,
292
+ _ => FilesystemError::Other(err.to_string()),
293
+ }
294
+ }
295
+
296
+ fn to_file_type(host: cap_std::fs::FileType) -> FileType {
297
+ if host.is_symlink() {
298
+ FileType::Symlink
299
+ } else if host.is_dir() {
300
+ FileType::Dir
301
+ } else if host.is_file() {
302
+ FileType::RegFile
303
+ } else {
304
+ to_special_file_type(host)
305
+ }
306
+ }
307
+
308
+ /// What a host entry neither a file, a directory, nor a link is, as `st_mode`
309
+ /// names it.
310
+ #[cfg(unix)]
311
+ fn to_special_file_type(host: cap_std::fs::FileType) -> FileType {
312
+ use cap_std::fs::FileTypeExt;
313
+ if host.is_fifo() {
314
+ FileType::Fifo
315
+ } else if host.is_socket() {
316
+ FileType::Sock
317
+ } else if host.is_block_device() {
318
+ FileType::Blkdev
319
+ } else if host.is_char_device() {
320
+ FileType::Chrdev
321
+ } else {
322
+ FileType::Unknown
323
+ }
324
+ }
325
+
326
+ /// Elsewhere a Bind names no kind.
327
+ #[cfg(not(unix))]
328
+ fn to_special_file_type(_host: cap_std::fs::FileType) -> FileType {
329
+ FileType::Unknown
330
+ }
331
+
332
+ /// What a host entry is, with the size the host reports. The host names
333
+ /// its identity, link count, and times on unix; elsewhere a Bind reports
334
+ /// none.
335
+ fn to_kstat(metadata: cap_std::fs::Metadata) -> Kstat {
336
+ let file_type = to_file_type(metadata.file_type());
337
+ let size = metadata.len();
338
+ #[cfg(unix)]
339
+ {
340
+ use cap_std::fs::MetadataExt;
341
+ Kstat::new(file_type, size)
342
+ .with_identity(metadata.dev(), metadata.ino())
343
+ .with_nlink(metadata.nlink())
344
+ .with_times(
345
+ unix_time(metadata.atime(), metadata.atime_nsec()),
346
+ unix_time(metadata.mtime(), metadata.mtime_nsec()),
347
+ unix_time(metadata.ctime(), metadata.ctime_nsec()),
348
+ )
349
+ }
350
+ #[cfg(not(unix))]
351
+ Kstat::new(file_type, size)
352
+ }
353
+
354
+ /// The time `seconds` and `nanoseconds` after the Unix epoch, as `stat`
355
+ /// reports one; `None` before the epoch, which no guest's clock reads.
356
+ #[cfg(unix)]
357
+ fn unix_time(seconds: i64, nanoseconds: i64) -> Option<std::time::SystemTime> {
358
+ let seconds = u64::try_from(seconds).ok()?;
359
+ let nanoseconds = u32::try_from(nanoseconds).ok()?;
360
+ std::time::UNIX_EPOCH.checked_add(std::time::Duration::new(seconds, nanoseconds))
361
+ }
362
+
363
+ /// `time` as the host takes it, so a time of now is the host's own.
364
+ fn to_host_time(time: Option<SetTime>) -> Option<SystemTimeSpec> {
365
+ time.map(|time| match time {
366
+ SetTime::Now => SystemTimeSpec::SymbolicNow,
367
+ SetTime::At(at) => SystemTimeSpec::Absolute(cap_std::time::SystemTime::from_std(at)),
368
+ })
369
+ }
370
+
371
+ /// The descriptors open on one file, and which were opened to write, as 9p
372
+ /// keeps open fids on `i_private`.
373
+ #[derive(Default)]
374
+ struct OpenFids(Mutex<Vec<(Weak<std::fs::File>, bool)>>);
375
+
376
+ /// A descriptor open on the file `dentry` records, one open to write where
377
+ /// `writable` asks, as 9p's `v9fs_fid_find_inode` finds one.
378
+ fn v9fs_fid_find_inode(dentry: &Dentry, writable: bool) -> Option<Arc<std::fs::File>> {
379
+ let inode = dentry.inode()?;
380
+ let fids = inode.private().get()?.clone().downcast::<OpenFids>().ok()?;
381
+ let fids = fids.0.lock().unwrap_or_else(PoisonError::into_inner);
382
+ fids.iter()
383
+ .filter(|(_, opened_to_write)| *opened_to_write || !writable)
384
+ .find_map(|(fid, _)| fid.upgrade())
385
+ }
386
+
387
+ /// Open `name` in `dir` as `how` asks, made first where `create` says,
388
+ /// refusing one that already stands.
389
+ fn open_in(
390
+ dir: &Dir,
391
+ name: &str,
392
+ how: &OpenHow,
393
+ create: bool,
394
+ ) -> Result<std::fs::File, FilesystemError> {
395
+ let writes = how.write || how.truncate || create;
396
+ let mut open = CapOpenOptions::new();
397
+ // The host refuses one that creates or truncates without writing; the
398
+ // runtime decides what is written, so such opens write here.
399
+ open.read(how.read).write(writes).create_new(create);
400
+ // Only a file or a directory opens: a FIFO would wait for a peer
401
+ // and a device would act on being opened. The host could swap an
402
+ // entry in between, so what opened is checked again before its
403
+ // first use, the open itself never waiting for a peer, nor following
404
+ // a link swapped in, since the runtime follows a link itself.
405
+ open.follow(FollowSymlinks::No);
406
+ #[cfg(unix)]
407
+ {
408
+ use cap_std::fs::OpenOptionsExt;
409
+ use rustix::fs::OFlags;
410
+ open.custom_flags((OFlags::NONBLOCK | OFlags::NOCTTY).bits() as i32);
411
+ }
412
+ let file = dir
413
+ .open_with(name, &open)
414
+ .map_err(|err| nofollow_error(dir, name, err))?;
415
+ match to_file_type(file.metadata().map_err(to_error)?.file_type()) {
416
+ FileType::RegFile => {}
417
+ FileType::Dir => return Err(FilesystemError::IsADirectory),
418
+ _ => return Err(FilesystemError::PermissionDenied),
419
+ }
420
+ #[cfg(unix)]
421
+ {
422
+ use rustix::fs::{fcntl_getfl, fcntl_setfl, OFlags};
423
+ let flags = fcntl_getfl(&file)
424
+ .map_err(io::Error::from)
425
+ .map_err(to_error)?;
426
+ fcntl_setfl(&file, flags - OFlags::NONBLOCK)
427
+ .map_err(io::Error::from)
428
+ .map_err(to_error)?;
429
+ }
430
+ Ok(file.into_std())
431
+ }
432
+
433
+ /// The host may move or remake what a dentry names behind the runtime's
434
+ /// back, so a dentry recording an identity is valid only while the same
435
+ /// entry stands at the path it stands for, as NFS compares the handle a
436
+ /// fresh lookup finds. One recording none has nothing to compare, so nothing
437
+ /// keeps it once its holder lets go.
438
+ impl FileSystemType for Bind {
439
+ /// Its root reports the host directory it holds, as 9p's `fill_super` asks of
440
+ /// the attached fid, and a host that cannot answer makes no instance.
441
+ fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
442
+ sb.set_default_d_op(self.clone());
443
+ let root = to_kstat(self.root.dir_metadata().map_err(to_error)?);
444
+ sb.d_make_root(Inode::new(&root, self));
445
+ Ok(())
446
+ }
447
+ }
448
+
449
+ impl DentryOperations for Bind {
450
+ fn revalidate(
451
+ &self,
452
+ _dir: &dyn InodeOperations,
453
+ dentry: &Dentry,
454
+ ) -> Result<bool, FilesystemError> {
455
+ self.stands_as_recorded(dentry)
456
+ }
457
+
458
+ fn delete(&self, dentry: &Dentry) -> bool {
459
+ dentry
460
+ .inode()
461
+ .is_some_and(|inode| inode.identity().is_none())
462
+ }
463
+ }
464
+
465
+ /// A file of a Bind, or an entry a host holds that is none of the others, as
466
+ /// `v9fs_file_inode_operations_dotl` answers for one.
467
+ struct BindRegular(Bind);
468
+
469
+ /// A link of a Bind, as `v9fs_symlink_inode_operations_dotl` answers for one.
470
+ struct BindLink(Bind);
471
+
472
+ /// What an entry standing at its name in the directory above reports.
473
+ fn stat_at(bind: &Bind, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
474
+ bind.at(dentry, |dir, name| {
475
+ Ok(to_kstat(dir.symlink_metadata(name).map_err(to_error)?))
476
+ })
477
+ }
478
+
479
+ impl InodeOperations for BindRegular {
480
+ /// A file open anywhere answers from the descriptor that holds it, even
481
+ /// once its name is gone.
482
+ fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
483
+ match v9fs_fid_find_inode(dentry, false) {
484
+ Some(file) => Ok(to_kstat(
485
+ cap_std::fs::Metadata::from_file(&file).map_err(to_error)?,
486
+ )),
487
+ None => stat_at(&self.0, dentry),
488
+ }
489
+ }
490
+
491
+ fn setattr(&self, dentry: &Dentry, attr: &Iattr) -> Result<(), FilesystemError> {
492
+ if let Some(len) = attr.size {
493
+ // A truncate made through an open takes that open's descriptor, as
494
+ // 9p takes `ia_file`'s fid; one made by name, one open to write
495
+ // elsewhere.
496
+ let through = attr
497
+ .file
498
+ .and_then(|file| (file as &dyn Any).downcast_ref::<BindFile>())
499
+ .map(|opened| opened.file.clone());
500
+ return match through.or_else(|| v9fs_fid_find_inode(dentry, true)) {
501
+ Some(file) => file.set_len(len).map_err(to_error),
502
+ None => Err(FilesystemError::InvalidInput),
503
+ };
504
+ }
505
+ // A file open anywhere takes them through what holds it, whatever stands
506
+ // at its name by now.
507
+ if let Some(file) = v9fs_fid_find_inode(dentry, false) {
508
+ let mut times = std::fs::FileTimes::new();
509
+ if let Some(atime) = attr.atime {
510
+ times = times.set_accessed(attr.time_of(atime));
511
+ }
512
+ if let Some(mtime) = attr.mtime {
513
+ times = times.set_modified(attr.time_of(mtime));
514
+ }
515
+ return file.set_times(times).map_err(to_error);
516
+ }
517
+ let (atime, mtime) = (to_host_time(attr.atime), to_host_time(attr.mtime));
518
+ self.0.at(dentry, |dir, name| {
519
+ dir.set_symlink_times(name, atime, mtime).map_err(to_error)
520
+ })
521
+ }
522
+
523
+ fn open(
524
+ &self,
525
+ dentry: &Dentry,
526
+ how: &OpenHow,
527
+ ) -> Result<Box<dyn FileOperations>, FilesystemError> {
528
+ self.0.open_file(dentry, how, false)
529
+ }
530
+ }
531
+
532
+ impl InodeOperations for BindLink {
533
+ fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
534
+ stat_at(&self.0, dentry)
535
+ }
536
+
537
+ /// Where it points as the host holds it, even outside this directory,
538
+ /// as `->get_link` answers, since the walk judges what a link leads to. A
539
+ /// target that is no valid UTF-8 answers `IllegalSequence`, as a guest path
540
+ /// is one.
541
+ fn get_link(&self, dentry: &Dentry) -> Result<String, FilesystemError> {
542
+ self.0.at(dentry, |dir, name| {
543
+ dir.read_link_contents(name)
544
+ .map_err(to_error)?
545
+ .into_os_string()
546
+ .into_string()
547
+ .map_err(|_| FilesystemError::IllegalSequence)
548
+ })
549
+ }
550
+ }
551
+
552
+ impl InodeOperations for Bind {
553
+ /// A directory answers from its own descriptor, which follows it wherever
554
+ /// it moves and keeps its inode taken while it is open.
555
+ fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
556
+ Ok(to_kstat(
557
+ self.fid_lookup(dentry)?.dir_metadata().map_err(to_error)?,
558
+ ))
559
+ }
560
+
561
+ /// A directory takes the times through the descriptor that holds it, as it
562
+ /// answers for itself.
563
+ fn setattr(&self, dentry: &Dentry, attr: &Iattr) -> Result<(), FilesystemError> {
564
+ let (atime, mtime) = (to_host_time(attr.atime), to_host_time(attr.mtime));
565
+ self.fid_lookup(dentry)?
566
+ .set_times(".", atime, mtime)
567
+ .map_err(to_error)
568
+ }
569
+
570
+ /// What stands at the name `dentry` carries, spelled as the host spells
571
+ /// it, as a lookup hands `d_splice_alias` the inode it found, so
572
+ /// `revalidate` can tell a dentry the host replaced behind the
573
+ /// runtime's back. That costs one host stat a name.
574
+ fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
575
+ self.at(dentry, |dir, name| {
576
+ let stored_name = lookup::lookup(dir, name)?;
577
+ match dir.symlink_metadata(&stored_name).map_err(to_error) {
578
+ Ok(found) => {
579
+ let found = to_kstat(found);
580
+ let ops: Arc<dyn InodeOperations> = match found.file_type {
581
+ FileType::Dir => Arc::new(self.clone()),
582
+ FileType::Symlink => Arc::new(BindLink(self.clone())),
583
+ _ => Arc::new(BindRegular(self.clone())),
584
+ };
585
+ let inode = Inode::new(&found, ops);
586
+ match stored_name == name {
587
+ true => Ok(Lookup::Found(inode)),
588
+ false => Ok(Lookup::Spelled {
589
+ name: stored_name,
590
+ inode,
591
+ }),
592
+ }
593
+ }
594
+ Err(FilesystemError::NotFound) => Ok(Lookup::Absent),
595
+ Err(err) => Err(err),
596
+ }
597
+ })
598
+ }
599
+
600
+ fn open(
601
+ &self,
602
+ dentry: &Dentry,
603
+ how: &OpenHow,
604
+ ) -> Result<Box<dyn FileOperations>, FilesystemError> {
605
+ let fid = self.fid_lookup(dentry)?;
606
+ if how.read {
607
+ may_read_dir(&fid)?;
608
+ }
609
+ OpenDirFid::set(dentry, &fid);
610
+ Ok(Box::new(BindDir(fid, Rdir::default())))
611
+ }
612
+
613
+ /// A name still in lookup is looked up first, and any name is left to the
614
+ /// VFS unless the open may make it and nothing stands there, as 9p's
615
+ /// `v9fs_vfs_atomic_open_dotl` asks its server before it creates.
616
+ fn atomic_open(&self, dentry: &Dentry, how: &OpenHow) -> Result<Finish, FilesystemError> {
617
+ if dentry.in_lookup() {
618
+ let found = self.lookup(dentry)?;
619
+ if !how.create || !matches!(found, Lookup::Absent) {
620
+ return Ok(Finish::Lookup(found));
621
+ }
622
+ }
623
+ if !how.create {
624
+ return Ok(Finish::NoOpen { created: false });
625
+ }
626
+ let file = self.open_file(dentry, how, true)?;
627
+ Ok(Finish::Open {
628
+ file,
629
+ created: true,
630
+ })
631
+ }
632
+
633
+ fn mkdir(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
634
+ self.at(dentry, |dir, name| dir.create_dir(name).map_err(to_error))
635
+ }
636
+
637
+ fn rmdir(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
638
+ self.at(dentry, |dir, name| dir.remove_dir(name).map_err(to_error))
639
+ }
640
+
641
+ fn unlink(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
642
+ self.at(dentry, |dir, name| dir.remove_file(name).map_err(to_error))
643
+ }
644
+
645
+ /// A host directory takes no lock the runtime's check of the source
646
+ /// holds, so another process of the host's changing it in between is
647
+ /// the host's to rule out. A link moves as itself, as `rename(2)`
648
+ /// moves one.
649
+ fn rename(
650
+ &self,
651
+ from: &Dentry,
652
+ to_dir: &dyn InodeOperations,
653
+ to: &Dentry,
654
+ flag: Option<RenameFlag>,
655
+ ) -> Result<(), FilesystemError> {
656
+ if flag.is_some() {
657
+ return Err(FilesystemError::InvalidInput);
658
+ }
659
+ let target = (to_dir as &dyn Any)
660
+ .downcast_ref::<Self>()
661
+ .ok_or(FilesystemError::CrossesDevices)?;
662
+ self.at(from, |from_dir, from_name| {
663
+ target.at(to, |to_dir, to_name| {
664
+ from_dir
665
+ .rename(from_name, to_dir, to_name)
666
+ .map_err(to_error)
667
+ })
668
+ })
669
+ }
670
+ }
671
+
672
+ /// A host directory, open on the descriptor it holds, as 9p's `v9fs_dir_readdir`
673
+ /// reads the fid in `private_data`, so it follows the directory wherever it
674
+ /// moves.
675
+ struct BindDir(Arc<Dir>, Rdir);
676
+
677
+ impl FileOperations for BindDir {
678
+ fn llseek(
679
+ &mut self,
680
+ dentry: &Dentry,
681
+ pos: u64,
682
+ offset: i64,
683
+ whence: Whence,
684
+ ) -> Result<u64, FilesystemError> {
685
+ generic_file_llseek(dentry, pos, offset, whence)
686
+ }
687
+
688
+ fn read_iter(&mut self, _iocb: &mut Kiocb, _buf: &mut [u8]) -> Result<usize, FilesystemError> {
689
+ generic_read_dir()
690
+ }
691
+
692
+ /// What a listing began with stays on the open directory until it ends, as
693
+ /// 9p's `fid->rdir` holds what the server sent.
694
+ fn iterate_shared(
695
+ &mut self,
696
+ dentry: &Dentry,
697
+ ctx: &mut DirContext<'_>,
698
+ ) -> Result<(), FilesystemError> {
699
+ let dir = self.0.clone();
700
+ self.1.iterate(dentry, ctx, || entries_of(&dir))
701
+ }
702
+
703
+ /// Make the host directory's names durable. Windows syncs no directory, so
704
+ /// there the host answers for them itself.
705
+ fn fsync(&mut self, _datasync: bool) -> Result<(), FilesystemError> {
706
+ sync_dir(&self.0)
707
+ }
708
+ }
709
+
710
+ /// `dir` opened to read. cap-std holds a directory for its path alone on Linux,
711
+ /// which reads and syncs nothing, so the host judges reading it only here.
712
+ #[cfg(unix)]
713
+ fn open_to_read(dir: &Dir) -> Result<rustix::fd::OwnedFd, FilesystemError> {
714
+ use rustix::fs::{openat, Mode, OFlags};
715
+
716
+ let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC;
717
+ openat(dir, ".", flags, Mode::empty()).map_err(|err| to_error(io::Error::from(err)))
718
+ }
719
+
720
+ #[cfg(unix)]
721
+ fn sync_dir(dir: &Dir) -> Result<(), FilesystemError> {
722
+ rustix::fs::fsync(open_to_read(dir)?).map_err(|err| to_error(io::Error::from(err)))
723
+ }
724
+
725
+ #[cfg(not(unix))]
726
+ fn sync_dir(_dir: &Dir) -> Result<(), FilesystemError> {
727
+ Ok(())
728
+ }
729
+
730
+ /// Refuse to open `dir` to read unless the host lets it be read, as `may_open`
731
+ /// asks `inode_permission`. A file holds it for what it was opened for, so a
732
+ /// directory reached only for its path answers no such question.
733
+ #[cfg(unix)]
734
+ fn may_read_dir(dir: &Dir) -> Result<(), FilesystemError> {
735
+ open_to_read(dir).map(drop)
736
+ }
737
+
738
+ #[cfg(not(unix))]
739
+ fn may_read_dir(_dir: &Dir) -> Result<(), FilesystemError> {
740
+ Ok(())
741
+ }
742
+
743
+ /// An open host file. The runtime names every offset, so each access seeks
744
+ /// first.
745
+ struct BindFile {
746
+ file: Arc<std::fs::File>,
747
+ }
748
+
749
+ impl BindFile {
750
+ /// Windows has no append to switch one handle to for a single write,
751
+ /// so an append that could land over another writer's is refused.
752
+ #[cfg(not(unix))]
753
+ fn append(&mut self, _iocb: &mut Kiocb, _buf: &[u8]) -> Result<usize, FilesystemError> {
754
+ Err(FilesystemError::Unsupported)
755
+ }
756
+
757
+ /// The host places the write at the file's end as `O_APPEND` does, so
758
+ /// another writer's append lands before or after it, never over it.
759
+ #[cfg(unix)]
760
+ fn append(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
761
+ use rustix::fs::{fcntl_getfl, fcntl_setfl, OFlags};
762
+
763
+ let mut file = &*self.file;
764
+ let flags = fcntl_getfl(file)
765
+ .map_err(io::Error::from)
766
+ .map_err(to_error)?;
767
+ fcntl_setfl(file, flags | OFlags::APPEND)
768
+ .map_err(io::Error::from)
769
+ .map_err(to_error)?;
770
+ let written = file.write(buf);
771
+ let end = file.stream_position();
772
+ fcntl_setfl(file, flags)
773
+ .map_err(io::Error::from)
774
+ .map_err(to_error)?;
775
+ let written = written.map_err(to_error)?;
776
+ iocb.pos = end.map_err(to_error)?;
777
+ Ok(written)
778
+ }
779
+ }
780
+
781
+ impl FileOperations for BindFile {
782
+ fn llseek(
783
+ &mut self,
784
+ dentry: &Dentry,
785
+ pos: u64,
786
+ offset: i64,
787
+ whence: Whence,
788
+ ) -> Result<u64, FilesystemError> {
789
+ generic_file_llseek(dentry, pos, offset, whence)
790
+ }
791
+
792
+ fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
793
+ let mut file = &*self.file;
794
+ file.seek(SeekFrom::Start(iocb.pos)).map_err(to_error)?;
795
+ let read = file.read(buf).map_err(to_error)?;
796
+ iocb.pos += read as u64;
797
+ Ok(read)
798
+ }
799
+
800
+ fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
801
+ let written = match iocb.append {
802
+ true => self.append(iocb, buf)?,
803
+ false => {
804
+ let mut file = &*self.file;
805
+ file.seek(SeekFrom::Start(iocb.pos)).map_err(to_error)?;
806
+ let written = file.write(buf).map_err(to_error)?;
807
+ iocb.pos += written as u64;
808
+ written
809
+ }
810
+ };
811
+ crate::vfs::generic_write_sync(self, iocb, written)
812
+ }
813
+
814
+ fn fsync(&mut self, datasync: bool) -> Result<(), FilesystemError> {
815
+ match datasync {
816
+ true => self.file.sync_data(),
817
+ false => self.file.sync_all(),
818
+ }
819
+ .map_err(to_error)
820
+ }
821
+
822
+ /// Reserve the room on the host, as `v9fs_file_fallocate` asks its server. Only
823
+ /// Linux grows a file in one step without shortening one a peer just grew, so
824
+ /// elsewhere this refuses, as an append that cannot land in one step does.
825
+ #[cfg(target_os = "linux")]
826
+ fn fallocate(&mut self, offset: u64, len: u64) -> Result<(), FilesystemError> {
827
+ let flags = rustix::fs::FallocateFlags::empty();
828
+ rustix::fs::fallocate(&*self.file, flags, offset, len)
829
+ .map_err(|err| to_error(io::Error::from(err)))
830
+ }
831
+ }
832
+
833
+ #[cfg(test)]
834
+ mod tests {
835
+ use std::fs;
836
+ use std::path::PathBuf;
837
+
838
+ use super::*;
839
+ use crate::vfs::kern_path::{kern_path, KernPath};
840
+
841
+ /// A Bind reports times only where the host names them.
842
+ #[cfg(unix)]
843
+ mod set_times {
844
+ use super::*;
845
+ use crate::filesystem::{Overlay, Tmpfs};
846
+ use crate::mount::{Access, Mount};
847
+ use crate::vfs::kern_path::filp_open;
848
+ use crate::vfs::{do_utimes_fd, do_utimes_path, vfs_fstatat, OpenHow, Path as VfsPath};
849
+
850
+ /// One second past the Unix epoch, which no clock is at.
851
+ fn moment() -> std::time::SystemTime {
852
+ std::time::UNIX_EPOCH + std::time::Duration::from_secs(1)
853
+ }
854
+
855
+ fn set_modified() -> Iattr<'static> {
856
+ Iattr::times(None, Some(SetTime::At(moment())))
857
+ }
858
+
859
+ /// The root of `filesystem` mounted to change.
860
+ fn root_of(filesystem: Arc<dyn FileSystemType>) -> VfsPath {
861
+ let mount = Mount::new(
862
+ "/data",
863
+ Filesystem::new(filesystem).expect("the instance must stand"),
864
+ Access::ReadWrite,
865
+ )
866
+ .expect("/data is a guest path");
867
+ VfsPath::root(mount.mnt())
868
+ }
869
+
870
+ /// An Overlay of `lower` beneath an empty upper.
871
+ fn overlay_of(lower: Bind) -> Arc<Overlay> {
872
+ Arc::new(
873
+ Overlay::new(
874
+ vec![Filesystem::new(Arc::new(lower)).expect("the instance must stand")],
875
+ Some(Filesystem::new(Arc::new(Tmpfs::new())).expect("the instance must stand")),
876
+ )
877
+ .unwrap_or_else(|_| panic!("the Overlay must stack")),
878
+ )
879
+ }
880
+
881
+ /// What a stat of `path` beneath `root` reports as its modified time, once
882
+ /// it was set.
883
+ fn set_and_read_modified(root: &VfsPath, path: &str) -> Option<std::time::SystemTime> {
884
+ do_utimes_path(&Arc::default(), root, path, false, &set_modified())
885
+ .expect("the time must be set");
886
+ vfs_fstatat(&Arc::default(), root, path, false)
887
+ .ok()
888
+ .and_then(|stat| stat.mtime)
889
+ }
890
+
891
+ /// When the host last changed what `path` holds.
892
+ fn host_modified(path: &std::path::Path) -> Option<std::time::SystemTime> {
893
+ fs::metadata(path).and_then(|found| found.modified()).ok()
894
+ }
895
+
896
+ /// A host directory holding `dir/file.txt`.
897
+ fn host_with_a_file(name: &str) -> HostDir {
898
+ let host = HostDir::new(name);
899
+ fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
900
+ fs::write(host.0.join("dir/file.txt"), "x").expect("the host file must be written");
901
+ host
902
+ }
903
+
904
+ fn bind_of(host: &HostDir) -> Bind {
905
+ Bind::new(&host.0).expect("the host directory must open")
906
+ }
907
+
908
+ // @behavior FS-185
909
+ #[test]
910
+ fn a_built_in_filesystem_keeps_the_times_a_caller_sets() {
911
+ let host = host_with_a_file("set-times");
912
+ let tmpfs: Tmpfs = [("dir/file.txt", "x")].into_iter().collect();
913
+ let roots = [
914
+ root_of(Arc::new(tmpfs)),
915
+ root_of(Arc::new(bind_of(&host))),
916
+ root_of(overlay_of(bind_of(&host))),
917
+ ];
918
+ let answers = roots.map(|root| {
919
+ (
920
+ set_and_read_modified(&root, "dir/file.txt"),
921
+ set_and_read_modified(&root, "dir"),
922
+ )
923
+ });
924
+ let set = Some(moment());
925
+ assert_eq!(
926
+ (answers, host_modified(&host.0.join("dir/file.txt"))),
927
+ ([(set, set); 3], set),
928
+ "vfs::do_utimes_pathon a file and a directory of a Tmpfs, a Bind, and an Overlay must \
929
+ keep the time, a Bind's on the host"
930
+ );
931
+ }
932
+
933
+ // @behavior FS-186
934
+ #[test]
935
+ fn a_directory_time_set_through_an_overlay_leaves_its_lower_layer() {
936
+ let host = host_with_a_file("overlay-dir-time");
937
+ let before = host_modified(&host.0.join("dir"));
938
+ let seen = set_and_read_modified(&root_of(overlay_of(bind_of(&host))), "dir");
939
+ assert_eq!(
940
+ (seen, host_modified(&host.0.join("dir")) == before),
941
+ (Some(moment()), true),
942
+ "vfs::do_utimes_pathon a directory through an Overlay must show the time and leave the \
943
+ lower Layer's directory as it was"
944
+ );
945
+ }
946
+
947
+ // A guest sees nothing of a link but that it is one, so its times are no more
948
+ // its to set than where it points is to read.
949
+ // @behavior FS-187
950
+ #[test]
951
+ fn a_link_keeps_its_own_times() {
952
+ let host = host_with_a_file("link-time");
953
+ std::os::unix::fs::symlink("dir", host.0.join("link")).expect("the link must be made");
954
+ assert_eq!(
955
+ do_utimes_path(
956
+ &Arc::default(),
957
+ &root_of(Arc::new(bind_of(&host))),
958
+ "link",
959
+ false,
960
+ &set_modified(),
961
+ ),
962
+ Err(FilesystemError::Unsupported),
963
+ "vfs::do_utimes_pathfollowing no link of a Bind's link must answer ENOTSUP"
964
+ );
965
+ }
966
+
967
+ // Another file takes the name, so a time reaching it by name would be seen.
968
+ // @behavior FS-188
969
+ #[test]
970
+ fn a_time_set_through_a_descriptor_reaches_the_file_it_holds() {
971
+ let host = host_with_a_file("held-time");
972
+ let root = root_of(Arc::new(bind_of(&host)));
973
+ let how = OpenHow {
974
+ read: true,
975
+ ..OpenHow::default()
976
+ };
977
+ let held = filp_open(&Arc::default(), &root, "dir/file.txt", &how)
978
+ .unwrap_or_else(|_| panic!("the file must open"));
979
+ let file = host.0.join("dir/file.txt");
980
+ fs::remove_file(&file).expect("the host removes the file");
981
+ fs::write(&file, "y").expect("the host makes another");
982
+ let other_before = host_modified(&file);
983
+ let answer = do_utimes_fd(&Arc::default(), held.path(), &set_modified());
984
+ let held_time = held.getattr().ok().and_then(|stat| stat.mtime);
985
+ assert_eq!(
986
+ (answer, held_time, host_modified(&file) == other_before),
987
+ (Ok(()), Some(moment()), true),
988
+ "vfs::do_utimes_fd through a Bind's descriptor must set the file it holds, and leave \
989
+ what took its name"
990
+ );
991
+ }
992
+ }
993
+
994
+ /// The host directory at `path` as a Bind, reached through the
995
+ /// VFS.
996
+ fn bind_host(path: impl AsRef<Path>) -> KernPath {
997
+ kern_path(Arc::new(
998
+ Bind::new(path).expect("the host directory must open"),
999
+ ))
1000
+ }
1001
+
1002
+ /// A fresh host directory of the test's own, removed when it drops.
1003
+ struct HostDir(PathBuf);
1004
+
1005
+ impl HostDir {
1006
+ fn new(name: &str) -> Self {
1007
+ let path = std::env::temp_dir()
1008
+ .join(format!("kobako-directory-{name}-{}", std::process::id()));
1009
+ let _ = fs::remove_dir_all(&path);
1010
+ fs::create_dir_all(&path).expect("the host directory must be created");
1011
+ Self(path)
1012
+ }
1013
+ }
1014
+
1015
+ impl Drop for HostDir {
1016
+ fn drop(&mut self) {
1017
+ let _ = fs::remove_dir_all(&self.0);
1018
+ }
1019
+ }
1020
+
1021
+ /// A Bind over `depth` nested directories each named `d`, with the file
1022
+ /// `f` at the bottom, and the path to that file.
1023
+ #[cfg(unix)]
1024
+ fn nested_tree(host: &HostDir, depth: usize) -> (Arc<Bind>, String) {
1025
+ let path = vec!["d"; depth].join("/");
1026
+ fs::create_dir_all(host.0.join(&path)).expect("the host tree must be made");
1027
+ fs::write(host.0.join(&path).join("f"), "x").expect("the host file must be written");
1028
+ let bind = Arc::new(Bind::new(&host.0).expect("the host directory must open"));
1029
+ (bind, format!("{path}/f"))
1030
+ }
1031
+
1032
+ // Counted on the Bind, as a count of the process's descriptors would see
1033
+ // every other test's. Past the bound the walk still lands, so nothing a
1034
+ // guest reaches depends on what was kept.
1035
+ // @behavior FS-151
1036
+ #[cfg(unix)]
1037
+ #[test]
1038
+ fn a_walk_keeps_a_bounded_number_of_host_directories_open() {
1039
+ use crate::vfs::{self, OpenHow, Path as VfsPath};
1040
+ let host = HostDir::new("bounded-fids");
1041
+ let (bind, file) = nested_tree(&host, 200);
1042
+ let mount = crate::mount::Mount::new(
1043
+ "/data",
1044
+ crate::filesystem::Filesystem::new(bind.clone()).expect("the instance must stand"),
1045
+ crate::mount::Access::Read,
1046
+ )
1047
+ .expect("/data is a guest path");
1048
+ let root = VfsPath::root(mount.mnt());
1049
+ let read = OpenHow {
1050
+ read: true,
1051
+ ..OpenHow::default()
1052
+ };
1053
+ let opened = vfs::kern_path::filp_open(&Arc::default(), &root, &file, &read);
1054
+ assert!(
1055
+ opened.is_ok() && bind.fids.len() <= 64,
1056
+ "vfs::do_file_open 200 directories beneath a Bind must land and keep at most 64 of \
1057
+ them open, keeping {}",
1058
+ bind.fids.len()
1059
+ );
1060
+ }
1061
+
1062
+ // @behavior FS-196
1063
+ #[test]
1064
+ fn a_directory_of_each_built_in_filesystem_answers_its_read_as_a_directory() {
1065
+ use crate::vfs::File;
1066
+ let host = HostDir::new("reads-directory");
1067
+ let read = |fs: Arc<dyn FileSystemType>| {
1068
+ let mount = crate::mount::Mount::new(
1069
+ "/data",
1070
+ Filesystem::new(fs).expect("the instance must stand"),
1071
+ crate::mount::Access::Read,
1072
+ )
1073
+ .expect("/data is a guest path");
1074
+ let mut dir = File::root(mount.mnt(), &Arc::default()).expect("the root opens");
1075
+ (dir.read(&mut [0; 1]).err(), dir.pread(&mut [0; 1], 0).err())
1076
+ };
1077
+ let overlay = crate::filesystem::Overlay::new(
1078
+ vec![
1079
+ Filesystem::new(Arc::new(crate::filesystem::Tmpfs::new()))
1080
+ .expect("the instance must stand"),
1081
+ Filesystem::new(Arc::new(crate::filesystem::Tmpfs::new()))
1082
+ .expect("the instance must stand"),
1083
+ ],
1084
+ None,
1085
+ )
1086
+ .expect("the Overlay must stack");
1087
+ let refused = Some(FilesystemError::IsADirectory);
1088
+ for answer in [
1089
+ read(Arc::new(crate::filesystem::Tmpfs::new())),
1090
+ read(Arc::new(
1091
+ Bind::new(&host.0).expect("the host directory must open"),
1092
+ )),
1093
+ read(Arc::new(overlay)),
1094
+ ] {
1095
+ assert_eq!(
1096
+ answer,
1097
+ (refused.clone(), refused.clone()),
1098
+ "File::read and pread of a directory of each built-in Filesystem must answer \
1099
+ EISDIR, as generic_read_dir does"
1100
+ );
1101
+ }
1102
+ }
1103
+
1104
+ // Nothing holds `x` between the two stats, so what answers the second is the
1105
+ // negative dentry the first left, asked whether it still holds.
1106
+ // @behavior FS-193
1107
+ #[test]
1108
+ fn a_name_kept_as_absent_shows_what_the_host_made_there() {
1109
+ use crate::vfs::{self, Path as VfsPath};
1110
+ let host = HostDir::new("kept-absent");
1111
+ let mount = crate::mount::Mount::new(
1112
+ "/data",
1113
+ crate::filesystem::Filesystem::new(Arc::new(
1114
+ Bind::new(&host.0).expect("the host directory must open"),
1115
+ ))
1116
+ .expect("the instance must stand"),
1117
+ crate::mount::Access::ReadWrite,
1118
+ )
1119
+ .expect("/data is a guest path");
1120
+ let root = VfsPath::root(mount.mnt());
1121
+ let cred = Arc::default();
1122
+ let stat = || vfs::vfs_fstatat(&cred, &root, "x", false).err();
1123
+ let before = stat();
1124
+ fs::write(host.0.join("x"), "x").expect("the host file must be written");
1125
+ assert_eq!(
1126
+ (before, stat()),
1127
+ (Some(FilesystemError::NotFound), None),
1128
+ "vfs::vfs_fstatat of a name kept as absent, once the host made a file there, must \
1129
+ find the file"
1130
+ );
1131
+ }
1132
+
1133
+ // The Bind records nothing it makes, so what stands at the name is known only
1134
+ // to a lookup, which the absent dentry the walk kept must not answer for.
1135
+ // @behavior FS-194
1136
+ #[test]
1137
+ fn a_directory_made_where_a_name_was_kept_as_absent_is_found() {
1138
+ use crate::vfs::{self, Path as VfsPath};
1139
+ let host = HostDir::new("made-at-absent");
1140
+ let mount = crate::mount::Mount::new(
1141
+ "/data",
1142
+ crate::filesystem::Filesystem::new(Arc::new(
1143
+ Bind::new(&host.0).expect("the host directory must open"),
1144
+ ))
1145
+ .expect("the instance must stand"),
1146
+ crate::mount::Access::ReadWrite,
1147
+ )
1148
+ .expect("/data is a guest path");
1149
+ let root = VfsPath::root(mount.mnt());
1150
+ let cred = Arc::default();
1151
+ let before = vfs::vfs_fstatat(&cred, &root, "made", false).err();
1152
+ vfs::filename_mkdirat(&cred, &root, "made").expect("the directory must be made");
1153
+ let after = vfs::vfs_fstatat(&cred, &root, "made", false).map(|made| made.file_type);
1154
+ assert_eq!(
1155
+ (before, after),
1156
+ (Some(FilesystemError::NotFound), Ok(FileType::Dir)),
1157
+ "vfs::filename_mkdirat at a name kept as absent must leave the next walk finding \
1158
+ the directory, as d_drop leaves a dentry nothing recorded on"
1159
+ );
1160
+ }
1161
+
1162
+ // Nothing holds `x` between the two opens, so only a walk asking whether
1163
+ // the name it kept still stands finds the directory gone from beneath the
1164
+ // Mount, rather than following what it kept out through the moved one.
1165
+ // @behavior FS-161
1166
+ #[cfg(unix)]
1167
+ #[test]
1168
+ fn a_name_kept_for_the_next_walk_reaches_nothing_the_host_moved_out() {
1169
+ use crate::vfs::{self, OpenHow, Path as VfsPath};
1170
+ let host = HostDir::new("kept-name");
1171
+ fs::create_dir_all(host.0.join("m/x")).expect("the host tree must be made");
1172
+ fs::create_dir(host.0.join("outside")).expect("the host tree must be made");
1173
+ fs::write(host.0.join("m/x/secret"), "s").expect("the host file must be written");
1174
+ let mount = crate::mount::Mount::new(
1175
+ "/data",
1176
+ crate::filesystem::Filesystem::new(Arc::new(
1177
+ Bind::new(host.0.join("m")).expect("the host directory must open"),
1178
+ ))
1179
+ .expect("the instance must stand"),
1180
+ crate::mount::Access::Read,
1181
+ )
1182
+ .expect("/data is a guest path");
1183
+ let root = VfsPath::root(mount.mnt());
1184
+ let cred = crate::security::restrict_self(
1185
+ &crate::security::prepare_creds_within(&[], None),
1186
+ [&mount],
1187
+ );
1188
+ let read = OpenHow {
1189
+ read: true,
1190
+ ..OpenHow::default()
1191
+ };
1192
+ let open = || vfs::kern_path::filp_open(&cred, &root, "x/secret", &read).err();
1193
+ let before = open();
1194
+ fs::rename(host.0.join("m/x"), host.0.join("outside/x")).expect("the host must move it");
1195
+ assert_eq!(
1196
+ (before, open()),
1197
+ (None, Some(FilesystemError::NotFound)),
1198
+ "vfs::do_file_open through a name kept since the last walk, once the host moved it \
1199
+ out of the Mount, must answer ENOENT"
1200
+ );
1201
+ }
1202
+
1203
+ // Another writer moves the name the Policy judges, then moves a refused
1204
+ // file onto where it went, between the judgement and the open, so only
1205
+ // telling what opened from what was judged keeps the open from reaching it.
1206
+ // @behavior FS-156
1207
+ #[cfg(unix)]
1208
+ #[test]
1209
+ fn an_open_reaches_only_the_entry_it_judged() {
1210
+ use crate::vfs::{self, OpenHow, Path as VfsPath};
1211
+
1212
+ /// Runs what it holds once, as a Policy is asked, then allows it.
1213
+ struct RunsWhenAsked(std::sync::Mutex<Option<Box<dyn FnOnce() + Send>>>);
1214
+
1215
+ impl crate::mount::Policy for RunsWhenAsked {
1216
+ fn allows(&self, _path: &str, _access: crate::mount::Access) -> bool {
1217
+ let run = self.0.lock().expect("never poisoned").take();
1218
+ if let Some(run) = run {
1219
+ run();
1220
+ }
1221
+ true
1222
+ }
1223
+ }
1224
+
1225
+ let host = HostDir::new("open-identity");
1226
+ for dir in ["public", "archive"] {
1227
+ fs::create_dir(host.0.join(dir)).expect("the host tree must be made");
1228
+ }
1229
+ fs::write(host.0.join("public/x"), "judged").expect("the host file must be written");
1230
+ fs::write(host.0.join("archive/secret"), "refused").expect("the host file must be written");
1231
+ let filesystem = crate::filesystem::Filesystem::new(Arc::new(
1232
+ Bind::new(&host.0).expect("the host directory must open"),
1233
+ ))
1234
+ .expect("the instance must stand");
1235
+ let writer =
1236
+ crate::mount::Mount::new("/data", filesystem.clone(), crate::mount::Access::ReadWrite)
1237
+ .expect("/data is a guest path");
1238
+ let writer = VfsPath::root(writer.mnt());
1239
+ let swap = Box::new(move || {
1240
+ let cred = Arc::default();
1241
+ vfs::filename_renameat2(&cred, &writer, "public/x", &writer, "archive/x")
1242
+ .expect("the name must move");
1243
+ vfs::filename_renameat2(&cred, &writer, "archive/secret", &writer, "archive/x")
1244
+ .expect("the refused file must move onto it");
1245
+ });
1246
+ let mount = crate::mount::Mount::new("/data", filesystem, crate::mount::Access::Read)
1247
+ .expect("/data is a guest path")
1248
+ .with_policy(Arc::new(RunsWhenAsked(std::sync::Mutex::new(Some(swap)))));
1249
+ let read = OpenHow {
1250
+ read: true,
1251
+ ..OpenHow::default()
1252
+ };
1253
+ let opened = vfs::kern_path::filp_open(
1254
+ &crate::security::restrict_self(
1255
+ &crate::security::prepare_creds_within(&[], None),
1256
+ [&mount],
1257
+ ),
1258
+ &VfsPath::root(mount.mnt()),
1259
+ "public/x",
1260
+ &read,
1261
+ );
1262
+ assert_eq!(
1263
+ opened.err(),
1264
+ Some(FilesystemError::NotFound),
1265
+ "vfs::do_file_open of a file another writer replaced after the Policy judged it must \
1266
+ answer ENOENT rather than open what stands there now"
1267
+ );
1268
+ }
1269
+
1270
+ // Other walks push what the descriptor walked to out of the bound, so only
1271
+ // a descriptor keeping its own directory still reaches it once the host
1272
+ // moved it.
1273
+ // @behavior FS-152
1274
+ #[cfg(unix)]
1275
+ #[test]
1276
+ fn a_directory_held_open_keeps_its_host_directory_past_the_bound() {
1277
+ use crate::vfs::{self, OpenHow, Path as VfsPath};
1278
+ let host = HostDir::new("held-past-bound");
1279
+ fs::create_dir_all(host.0.join("held")).expect("the host tree must be made");
1280
+ fs::write(host.0.join("held/f"), "x").expect("the host file must be written");
1281
+ let (bind, file) = nested_tree(&host, 100);
1282
+ let mount = crate::mount::Mount::new(
1283
+ "/data",
1284
+ crate::filesystem::Filesystem::new(bind).expect("the instance must stand"),
1285
+ crate::mount::Access::Read,
1286
+ )
1287
+ .expect("/data is a guest path");
1288
+ let root = VfsPath::root(mount.mnt());
1289
+ let cred = Arc::default();
1290
+ let directory = OpenHow {
1291
+ read: true,
1292
+ directory: true,
1293
+ ..OpenHow::default()
1294
+ };
1295
+ let Ok(held) = vfs::kern_path::filp_open(&cred, &root, "held", &directory) else {
1296
+ panic!("the directory must open");
1297
+ };
1298
+ let read = OpenHow {
1299
+ read: true,
1300
+ ..OpenHow::default()
1301
+ };
1302
+ assert!(
1303
+ vfs::kern_path::filp_open(&cred, &root, &file, &read).is_ok(),
1304
+ "the deep file must open"
1305
+ );
1306
+ fs::rename(host.0.join("held"), host.0.join("moved")).expect("the host must move it");
1307
+ assert!(
1308
+ vfs::vfs_fstatat(&cred, held.path(), "f", false).is_ok(),
1309
+ "vfs::vfs_fstatat through a directory held open beneath a Bind, once a walk passed \
1310
+ the bound and the host moved it, must still reach the directory it opened"
1311
+ );
1312
+ }
1313
+
1314
+ // The dentry the descriptor holds names the moved directory, so only a
1315
+ // Bind answering it stale keeps the removal of the new one from
1316
+ // killing it.
1317
+ // @behavior FS-135
1318
+ #[cfg(unix)]
1319
+ #[test]
1320
+ fn a_directory_remade_by_the_host_leaves_the_moved_ones_descriptor_alive() {
1321
+ use crate::mount::Mount;
1322
+ use crate::vfs::{self, OpenHow};
1323
+ let host = HostDir::new("remade");
1324
+ fs::create_dir_all(host.0.join("a")).expect("the host directory must be made");
1325
+ let filesystem = crate::filesystem::Filesystem::new(Arc::new(
1326
+ Bind::new(&host.0).expect("the host directory must open"),
1327
+ ))
1328
+ .expect("the instance must stand");
1329
+ let mount = Mount::new("/data", filesystem, crate::mount::Access::ReadWrite)
1330
+ .expect("/data is a guest path");
1331
+ let root = vfs::Path::root(mount.mnt());
1332
+ let how = OpenHow {
1333
+ read: true,
1334
+ directory: true,
1335
+ ..OpenHow::default()
1336
+ };
1337
+ let Ok(opened) = vfs::kern_path::filp_open(&Arc::default(), &root, "a", &how) else {
1338
+ panic!("the directory must open");
1339
+ };
1340
+ let held = opened.path();
1341
+ fs::rename(host.0.join("a"), host.0.join("moved")).expect("the host must move it");
1342
+ fs::create_dir(host.0.join("a")).expect("the host must remake it");
1343
+ vfs::filename_rmdir(&Arc::default(), &root, "a").expect("the new directory must go");
1344
+ assert_eq!(
1345
+ (
1346
+ vfs::filename_mkdirat(&Arc::default(),held, "made"),
1347
+ host.0.join("moved/made").is_dir()
1348
+ ),
1349
+ (Ok(()), true),
1350
+ "vfs::filename_mkdirat through a Bind directory the host moved away, once vfs::filename_rmdir \
1351
+ removed the one remade at its path, must make the directory in the moved one"
1352
+ );
1353
+ }
1354
+
1355
+ /// A Policy refusing everything beneath `/data/refused`.
1356
+ #[cfg(unix)]
1357
+ struct RefusesRefused;
1358
+
1359
+ #[cfg(unix)]
1360
+ impl crate::mount::Policy for RefusesRefused {
1361
+ fn allows(&self, path: &str, _access: crate::mount::Access) -> bool {
1362
+ !path.starts_with("/data/refused")
1363
+ }
1364
+ }
1365
+
1366
+ // The guest's descriptor of `a/d` keeps `a`'s dentry, and the host
1367
+ // directory a walk opened for it, so only a dentry the Bind answers
1368
+ // stale once the host moved `a` keeps a later walk through `a` from
1369
+ // reaching the moved directory while the Policy judges `a`.
1370
+ // @behavior FS-136
1371
+ #[cfg(unix)]
1372
+ #[test]
1373
+ fn a_walk_through_a_held_directory_the_host_moved_reaches_nothing_the_policy_refuses() {
1374
+ use crate::mount::Mount;
1375
+ use crate::vfs::{self, OpenHow};
1376
+ let host = HostDir::new("held-ancestor");
1377
+ fs::create_dir_all(host.0.join("a/d")).expect("the host tree must be made");
1378
+ fs::create_dir_all(host.0.join("refused")).expect("the host tree must be made");
1379
+ fs::write(host.0.join("a/d/secret"), "s").expect("the host file must be written");
1380
+ let filesystem = crate::filesystem::Filesystem::new(Arc::new(
1381
+ Bind::new(&host.0).expect("the host directory must open"),
1382
+ ))
1383
+ .expect("the instance must stand");
1384
+ let mount = Mount::new("/data", filesystem, crate::mount::Access::Read)
1385
+ .expect("/data is a guest path")
1386
+ .with_policy(Arc::new(RefusesRefused));
1387
+ let root = vfs::Path::root(mount.mnt());
1388
+ let cred = crate::security::restrict_self(
1389
+ &crate::security::prepare_creds_within(&[], None),
1390
+ [&mount],
1391
+ );
1392
+ let directory = OpenHow {
1393
+ read: true,
1394
+ directory: true,
1395
+ ..OpenHow::default()
1396
+ };
1397
+ let Ok(_held) = vfs::kern_path::filp_open(&cred, &root, "a/d", &directory) else {
1398
+ panic!("a/d must open");
1399
+ };
1400
+ fs::rename(host.0.join("a"), host.0.join("refused/a")).expect("the host must move a");
1401
+ fs::create_dir(host.0.join("a")).expect("the host must remake a");
1402
+ let read = OpenHow {
1403
+ read: true,
1404
+ ..OpenHow::default()
1405
+ };
1406
+ assert_eq!(
1407
+ vfs::kern_path::filp_open(&cred, &root, "a/d/secret", &read).err(),
1408
+ Some(FilesystemError::NotFound),
1409
+ "vfs::do_file_open beneath a Bind Mount with a Policy, through a directory the host moved \
1410
+ beneath a refused path while a descriptor beneath it was held, must answer NotFound"
1411
+ );
1412
+ }
1413
+
1414
+ /// The roots of a Tmpfs, a Bind, and an Overlay, each holding a five byte
1415
+ /// `f`, by name.
1416
+ #[cfg(unix)]
1417
+ fn each_holding_five_bytes(host: &HostDir) -> [(&'static str, crate::vfs::Path); 3] {
1418
+ use crate::filesystem::Filesystem;
1419
+ use crate::filesystem::{Overlay, Tmpfs};
1420
+ use crate::mount::{Access, Mount};
1421
+ fs::write(host.0.join("f"), "hello").expect("the host file must be written");
1422
+ let tmpfs = || [("f", "hello")].into_iter().collect::<Tmpfs>();
1423
+ let overlay = Overlay::new(
1424
+ vec![Filesystem::new(Arc::new(tmpfs())).expect("the instance must stand")],
1425
+ Some(Filesystem::new(Arc::new(Tmpfs::new())).expect("the instance must stand")),
1426
+ )
1427
+ .expect("the Overlay must stack");
1428
+ let root = |filesystem: Arc<dyn FileSystemType>| {
1429
+ let mount = Mount::new(
1430
+ "/data",
1431
+ Filesystem::new(filesystem).expect("the instance must stand"),
1432
+ Access::ReadWrite,
1433
+ )
1434
+ .expect("/data is a guest path");
1435
+ crate::vfs::Path::root(mount.mnt())
1436
+ };
1437
+ [
1438
+ ("Tmpfs", root(Arc::new(tmpfs()))),
1439
+ (
1440
+ "Bind",
1441
+ root(Arc::new(
1442
+ Bind::new(&host.0).expect("the host directory must open"),
1443
+ )),
1444
+ ),
1445
+ ("Overlay", root(Arc::new(overlay))),
1446
+ ]
1447
+ }
1448
+
1449
+ // @behavior FS-141
1450
+ #[cfg(unix)]
1451
+ #[test]
1452
+ fn a_file_removed_while_open_still_reports_itself() {
1453
+ use crate::vfs::{self, OpenHow};
1454
+ let host = HostDir::new("removed-open");
1455
+ let read = OpenHow {
1456
+ read: true,
1457
+ ..OpenHow::default()
1458
+ };
1459
+ for (name, root) in each_holding_five_bytes(&host) {
1460
+ let Ok(mut file) = vfs::kern_path::filp_open(&Arc::default(), &root, "f", &read) else {
1461
+ panic!("{name}'s f must open");
1462
+ };
1463
+ vfs::filename_unlinkat(&Arc::default(), &root, "f").expect("the name must go");
1464
+ let mut buf = [0; 5];
1465
+ assert_eq!(
1466
+ (
1467
+ file.getattr().map(|found| found.size).ok(),
1468
+ file.pread(&mut buf, 0),
1469
+ &buf
1470
+ ),
1471
+ (Some(5), Ok(5), b"hello"),
1472
+ "File::getattr and File::pread of a file whose name \
1473
+ vfs::filename_unlinkat removed on a {name} must answer what it holds"
1474
+ );
1475
+ }
1476
+ }
1477
+
1478
+ // @behavior FS-142
1479
+ #[cfg(unix)]
1480
+ #[test]
1481
+ fn a_file_removed_while_open_can_still_be_sized() {
1482
+ use crate::vfs::{self, OpenHow};
1483
+ let host = HostDir::new("removed-sized");
1484
+ let write = OpenHow {
1485
+ write: true,
1486
+ ..OpenHow::default()
1487
+ };
1488
+ for (name, root) in each_holding_five_bytes(&host) {
1489
+ let Ok(mut file) = vfs::kern_path::filp_open(&Arc::default(), &root, "f", &write)
1490
+ else {
1491
+ panic!("{name}'s f must open");
1492
+ };
1493
+ vfs::filename_unlinkat(&Arc::default(), &root, "f").expect("the name must go");
1494
+ assert_eq!(
1495
+ (
1496
+ file.truncate(2),
1497
+ file.getattr().map(|found| found.size).ok()
1498
+ ),
1499
+ (Ok(()), Some(2)),
1500
+ "File::truncate of a file whose name vfs::filename_unlinkat removed on a {name} \
1501
+ must size what it holds"
1502
+ );
1503
+ }
1504
+ }
1505
+
1506
+ // A dentry the host replaced is dropped by its identity, so the path
1507
+ // reaches the new file while the open one keeps its own descriptor.
1508
+ // @behavior FS-143
1509
+ #[cfg(unix)]
1510
+ #[test]
1511
+ fn a_file_replaced_while_open_answers_apart_from_its_path() {
1512
+ use crate::vfs::{self, OpenHow};
1513
+ let host = HostDir::new("replaced-open");
1514
+ let [_, (_, root), _] = each_holding_five_bytes(&host);
1515
+ let read = OpenHow {
1516
+ read: true,
1517
+ ..OpenHow::default()
1518
+ };
1519
+ let Ok(file) = vfs::kern_path::filp_open(&Arc::default(), &root, "f", &read) else {
1520
+ panic!("f must open");
1521
+ };
1522
+ fs::rename(host.0.join("f"), host.0.join("aside")).expect("the host must move f");
1523
+ fs::write(host.0.join("f"), "longer!").expect("the host must remake f");
1524
+ assert_eq!(
1525
+ (
1526
+ vfs::vfs_fstatat(&Arc::default(), &root, "f", true)
1527
+ .map(|found| found.size)
1528
+ .ok(),
1529
+ file.getattr().map(|found| found.size).ok()
1530
+ ),
1531
+ (Some(7), Some(5)),
1532
+ "vfs::vfs_fstatat of a path the host replaced under an open File must answer the new \
1533
+ file, and File::getattr the one it holds"
1534
+ );
1535
+ }
1536
+
1537
+ // The guest's descriptor of `a` keeps the Overlay's dentry and the
1538
+ // Bind's, whose host directory the host then moved aside; only the
1539
+ // Overlay asking the Bind's dentry drops its own for the `a` remade there.
1540
+ // @behavior FS-139
1541
+ #[cfg(unix)]
1542
+ #[test]
1543
+ fn an_overlay_takes_afresh_a_name_its_bind_layer_no_longer_holds() {
1544
+ use crate::filesystem::Filesystem;
1545
+ use crate::filesystem::{Overlay, Tmpfs};
1546
+ use crate::mount::{Access, Mount};
1547
+ use crate::vfs::{self, OpenHow};
1548
+ let host = HostDir::new("overlay-replaced");
1549
+ fs::create_dir(host.0.join("a")).expect("the host directory must be made");
1550
+ let lower = Filesystem::new(Arc::new(
1551
+ Bind::new(&host.0).expect("the host directory must open"),
1552
+ ))
1553
+ .expect("the instance must stand");
1554
+ let upper = Filesystem::new(Arc::new(Tmpfs::new())).expect("the instance must stand");
1555
+ let overlay = Overlay::new(vec![lower], Some(upper)).expect("the Overlay must stack");
1556
+ let mount = Mount::new(
1557
+ "/data",
1558
+ Filesystem::new(Arc::new(overlay)).expect("the instance must stand"),
1559
+ Access::Read,
1560
+ )
1561
+ .expect("/data is a guest path");
1562
+ let root = vfs::Path::root(mount.mnt());
1563
+ let directory = OpenHow {
1564
+ read: true,
1565
+ directory: true,
1566
+ ..OpenHow::default()
1567
+ };
1568
+ let Ok(_held) = vfs::kern_path::filp_open(&Arc::default(), &root, "a", &directory) else {
1569
+ panic!("a must open");
1570
+ };
1571
+ fs::rename(host.0.join("a"), host.0.join("aside")).expect("the host must move a");
1572
+ fs::create_dir(host.0.join("a")).expect("the host must remake a");
1573
+ fs::write(host.0.join("a/f"), "new").expect("the host file must be written");
1574
+ let read = OpenHow {
1575
+ read: true,
1576
+ ..OpenHow::default()
1577
+ };
1578
+ let Ok(mut file) = vfs::kern_path::filp_open(&Arc::default(), &root, "a/f", &read) else {
1579
+ panic!("a/f must open through the directory the host remade");
1580
+ };
1581
+ let mut buf = [0; 3];
1582
+ assert_eq!(
1583
+ (file.pread(&mut buf, 0), &buf),
1584
+ (Ok(3), b"new"),
1585
+ "vfs::do_file_open through an Overlay over a Bind whose host replaced a directory \
1586
+ a descriptor holds must read the file standing there now"
1587
+ );
1588
+ }
1589
+
1590
+ // The host took the directory after the name was found, so only the open
1591
+ // asking the host answers that it is gone, rather than a descriptor that
1592
+ // fails at its first use.
1593
+ // @behavior FS-166
1594
+ #[test]
1595
+ fn a_directory_that_cannot_open_answers_why() {
1596
+ let host = HostDir::new("gone-before-open");
1597
+ fs::create_dir_all(host.0.join("a")).expect("the host directory must be made");
1598
+ let filesystem = crate::filesystem::Filesystem::new(Arc::new(
1599
+ Bind::new(&host.0).expect("the host directory must open"),
1600
+ ))
1601
+ .expect("the instance must stand");
1602
+ let dentries = filesystem.dentries();
1603
+ let root = filesystem.s_root();
1604
+ let dir = dentries
1605
+ .alloc_parallel(&root, "a")
1606
+ .expect("no other walk holds the name");
1607
+ dir.lookup_inode(&filesystem, &root)
1608
+ .expect("the name must be found");
1609
+ fs::remove_dir(host.0.join("a")).expect("the host must remove it");
1610
+ let answer = dir
1611
+ .inode_ops()
1612
+ .expect("the name records its directory")
1613
+ .open(&dir, &OpenHow::default());
1614
+ assert_eq!(
1615
+ answer.err(),
1616
+ Some(FilesystemError::NotFound),
1617
+ "InodeOperations::open of a Bind's directory the host removed must answer ENOENT"
1618
+ );
1619
+ }
1620
+
1621
+ // A walk finds what stands at the names now, so only comparing it with what
1622
+ // the dentry recorded keeps another directory from answering for it.
1623
+ // @behavior FS-167
1624
+ #[cfg(unix)]
1625
+ #[test]
1626
+ fn a_directory_the_host_replaced_is_not_walked_to_in_its_place() {
1627
+ let host = HostDir::new("replaced-before-walk");
1628
+ fs::create_dir_all(host.0.join("a")).expect("the host directory must be made");
1629
+ let filesystem = crate::filesystem::Filesystem::new(Arc::new(
1630
+ Bind::new(&host.0).expect("the host directory must open"),
1631
+ ))
1632
+ .expect("the instance must stand");
1633
+ let dentries = filesystem.dentries();
1634
+ let root = filesystem.s_root();
1635
+ let dir = dentries
1636
+ .alloc_parallel(&root, "a")
1637
+ .expect("no other walk holds the name");
1638
+ dir.lookup_inode(&filesystem, &root)
1639
+ .expect("the name must be found");
1640
+ fs::rename(host.0.join("a"), host.0.join("moved")).expect("the host must move it");
1641
+ fs::create_dir(host.0.join("a")).expect("the host must make another");
1642
+ assert_eq!(
1643
+ dir.getattr().err(),
1644
+ Some(FilesystemError::NotFound),
1645
+ "InodeOperations::getattr of a Bind directory the host replaced must answer ENOENT \
1646
+ rather than what stands there now"
1647
+ );
1648
+ }
1649
+
1650
+ // A host's own rename takes no such flag, so only the refusal keeps a
1651
+ // call asking for one from moving the file as a plain rename would.
1652
+ // @behavior FS-163
1653
+ #[test]
1654
+ fn a_filesystem_taking_no_rename_flag_refuses_one() {
1655
+ let host = HostDir::new("rename-flag");
1656
+ fs::write(host.0.join("a"), "a").expect("the host file must be written");
1657
+ let filesystem = crate::filesystem::Filesystem::new(Arc::new(
1658
+ Bind::new(&host.0).expect("the host directory must open"),
1659
+ ))
1660
+ .expect("the instance must stand");
1661
+ let dentries = filesystem.dentries();
1662
+ let root = filesystem.s_root();
1663
+ let dir = root.inode().expect("the root stands").ops().clone();
1664
+ let at = |name| {
1665
+ dentries
1666
+ .alloc_parallel(&root, name)
1667
+ .expect("no other walk holds the name")
1668
+ };
1669
+ let answer = dir.rename(
1670
+ &at("a"),
1671
+ dir.as_ref(),
1672
+ &at("b"),
1673
+ Some(RenameFlag::NoReplace),
1674
+ );
1675
+ assert_eq!(
1676
+ (answer, host.0.join("a").exists()),
1677
+ (Err(FilesystemError::InvalidInput), true),
1678
+ "InodeOperations::rename of a Bind asked for a flag must answer EINVAL and move \
1679
+ nothing"
1680
+ );
1681
+ }
1682
+
1683
+ // A directory a Bind has named but not opened yet is walked to by the
1684
+ // names its dentry stands at when first asked, so a move the dentry
1685
+ // followed meanwhile leads to it, as 9p's walk reads them then.
1686
+ #[test]
1687
+ fn a_directory_named_before_a_move_opens_where_its_dentry_went() {
1688
+ let host = HostDir::new("named-then-moved");
1689
+ fs::create_dir_all(host.0.join("a")).expect("the host directory must be made");
1690
+ fs::write(host.0.join("a/note"), "n").expect("the host file must be written");
1691
+ let filesystem = crate::filesystem::Filesystem::new(Arc::new(
1692
+ Bind::new(&host.0).expect("the host directory must open"),
1693
+ ))
1694
+ .expect("the instance must stand");
1695
+ let dentries = filesystem.dentries();
1696
+ let root = filesystem.s_root();
1697
+ let at = |dir: &Arc<Dentry>, name| {
1698
+ dentries
1699
+ .alloc_parallel(dir, name)
1700
+ .expect("no other walk holds the name")
1701
+ };
1702
+ let a = at(&root, "a");
1703
+ fs::rename(host.0.join("a"), host.0.join("x")).expect("the host must move it");
1704
+ dentries.move_dentry(&a, &at(&root, "x"));
1705
+ let note = at(&a, "note");
1706
+ assert!(
1707
+ matches!(
1708
+ root.inode().expect("the root stands").ops().lookup(&note),
1709
+ Ok(Lookup::Found(inode)) if inode.kind() == FileType::RegFile
1710
+ ),
1711
+ "a name looked up through Bind in a directory not yet opened, once its dentry moved \
1712
+ with it, must be found where the dentry stands"
1713
+ );
1714
+ }
1715
+
1716
+ /// A host directory of the test's own holding the FIFO `pipe`.
1717
+ #[cfg(unix)]
1718
+ fn make_fifo_host(name: &str) -> HostDir {
1719
+ let host = HostDir::new(name);
1720
+ let made = std::process::Command::new("mkfifo")
1721
+ .arg(host.0.join("pipe"))
1722
+ .status()
1723
+ .expect("mkfifo must run");
1724
+ assert!(made.success(), "the FIFO must be made");
1725
+ host
1726
+ }
1727
+
1728
+ // Opening the FIFO with no writer would wait for one, so only an open
1729
+ // refused before it waits answers within the deadline; `/dev/null`
1730
+ // is a device every unix host holds.
1731
+ // @behavior FS-090
1732
+ #[cfg(unix)]
1733
+ #[test]
1734
+ fn a_directory_opens_only_a_file_or_a_directory() {
1735
+ let host = make_fifo_host("fifo-open");
1736
+ let fifo = bind_host(&host.0);
1737
+ let devices = bind_host("/dev");
1738
+ let (sender, answered) = std::sync::mpsc::channel();
1739
+ std::thread::spawn(move || {
1740
+ let read = OpenHow {
1741
+ read: true,
1742
+ ..OpenHow::default()
1743
+ };
1744
+ let _ = sender.send([
1745
+ fifo.open("pipe", &read).err(),
1746
+ devices.open("null", &read).err(),
1747
+ ]);
1748
+ });
1749
+ assert_eq!(
1750
+ answered.recv_timeout(crate::sched::testing::MAX_WAIT),
1751
+ Ok([
1752
+ Some(FilesystemError::PermissionDenied),
1753
+ Some(FilesystemError::PermissionDenied)
1754
+ ]),
1755
+ "a FIFO and a device opened through Bind::open must be refused as permission \
1756
+ denied without waiting"
1757
+ );
1758
+ }
1759
+
1760
+ // @behavior FS-091
1761
+ #[cfg(unix)]
1762
+ #[test]
1763
+ fn a_directory_reports_a_fifo_as_one() {
1764
+ let host = make_fifo_host("fifo-type");
1765
+ let directory = bind_host(&host.0);
1766
+ let listed: Vec<_> = directory
1767
+ .iterate_dir()
1768
+ .expect("the directory must list")
1769
+ .into_iter()
1770
+ .map(|entry| entry.file_type)
1771
+ .collect();
1772
+ assert_eq!(
1773
+ (
1774
+ directory.vfs_lstat("pipe").map(|stat| stat.file_type),
1775
+ listed
1776
+ ),
1777
+ (Ok(FileType::Fifo), vec![FileType::Fifo]),
1778
+ "a FIFO beneath a Bind, looked up and listed through the VFS, must answer a FIFO"
1779
+ );
1780
+ }
1781
+
1782
+ // `/dev/null` stands on every unix host, and a socket needs only its file
1783
+ // to stand here, so neither is opened.
1784
+ // @behavior FS-144
1785
+ #[cfg(unix)]
1786
+ #[test]
1787
+ fn a_directory_reports_the_kind_its_host_gives_a_special_file() {
1788
+ let host = HostDir::new("special");
1789
+ let _socket = std::os::unix::net::UnixListener::bind(host.0.join("sock"))
1790
+ .expect("the socket must be bound");
1791
+ let kind = |directory: KernPath, name| directory.vfs_lstat(name).map(|stat| stat.file_type);
1792
+ assert_eq!(
1793
+ (
1794
+ kind(bind_host(&host.0), "sock"),
1795
+ kind(bind_host("/dev"), "null"),
1796
+ ),
1797
+ (Ok(FileType::Sock), Ok(FileType::Chrdev)),
1798
+ "vfs_lstat of a socket and of /dev/null must answer the kind the host \
1799
+ gives each"
1800
+ );
1801
+ }
1802
+
1803
+ // @behavior FS-005
1804
+ #[test]
1805
+ fn a_directory_serves_the_files_of_its_host_directory() {
1806
+ let host = HostDir::new("serves");
1807
+ fs::write(host.0.join("data.csv"), "a,b").expect("the host file must be written");
1808
+
1809
+ let directory = bind_host(&host.0);
1810
+ let mut file = directory
1811
+ .open(
1812
+ "data.csv",
1813
+ &OpenHow {
1814
+ read: true,
1815
+ ..OpenHow::default()
1816
+ },
1817
+ )
1818
+ .expect("the host file must open");
1819
+ let mut buf = [0; 16];
1820
+ let read = file
1821
+ .read_iter(&mut Kiocb::at(0), &mut buf)
1822
+ .expect("the host file must read");
1823
+ assert_eq!(
1824
+ &buf[..read],
1825
+ b"a,b",
1826
+ "a host file read through a Bind's open must read back its content"
1827
+ );
1828
+ }
1829
+
1830
+ // @behavior FS-006
1831
+ #[test]
1832
+ fn a_directory_writes_into_its_host_directory() {
1833
+ let host = HostDir::new("writes");
1834
+ let directory = bind_host(&host.0);
1835
+ directory
1836
+ .open_or_create(
1837
+ "out.txt",
1838
+ &OpenHow {
1839
+ write: true,
1840
+ ..OpenHow::default()
1841
+ },
1842
+ )
1843
+ .expect("a new file must be created")
1844
+ .write_iter(&mut Kiocb::at(0), b"written")
1845
+ .expect("the write must land");
1846
+ assert_eq!(
1847
+ fs::read(host.0.join("out.txt")).expect("the host file must exist"),
1848
+ b"written",
1849
+ "a file created through a Bind's open must land in the host directory"
1850
+ );
1851
+ }
1852
+
1853
+ // The file is opened to write, so only the refusal keeps the append
1854
+ // from taking the two steps another writer could land between.
1855
+ // @behavior FS-093
1856
+ #[cfg(windows)]
1857
+ #[test]
1858
+ fn a_directory_on_windows_refuses_to_append() {
1859
+ let host = HostDir::new("append");
1860
+ fs::write(host.0.join("f"), "x").expect("the host file must be written");
1861
+ let directory = bind_host(&host.0);
1862
+ let mut file = directory
1863
+ .open(
1864
+ "f",
1865
+ &OpenHow {
1866
+ write: true,
1867
+ ..OpenHow::default()
1868
+ },
1869
+ )
1870
+ .expect("the host file must open");
1871
+ assert_eq!(
1872
+ (
1873
+ file.write_iter(&mut Kiocb::at(0).with_append(), b"y").err(),
1874
+ fs::read(host.0.join("f")).expect("the host file must read")
1875
+ ),
1876
+ (Some(FilesystemError::Unsupported), b"x".to_vec()),
1877
+ "an append through a Bind file on Windows must be refused as unsupported and \
1878
+ leave the file as it was"
1879
+ );
1880
+ }
1881
+
1882
+ // The host file exists beneath `d`, so only the refusal can keep the
1883
+ // open from reaching it.
1884
+ // @behavior FS-029
1885
+ #[cfg(windows)]
1886
+ #[test]
1887
+ fn a_directory_on_windows_refuses_a_name_holding_a_backslash() {
1888
+ let host = HostDir::new("backslash");
1889
+ fs::create_dir_all(host.0.join("d")).expect("the host directory must be created");
1890
+ fs::write(host.0.join("d").join("f"), "x").expect("the host file must be written");
1891
+ let directory = bind_host(&host.0);
1892
+ assert!(
1893
+ matches!(
1894
+ directory.open(r"d\f", &OpenHow::default()),
1895
+ Err(FilesystemError::InvalidInput)
1896
+ ),
1897
+ "a path holding a backslash opened through a Bind on Windows must be refused as \
1898
+ invalid input"
1899
+ );
1900
+ }
1901
+
1902
+ // Judged on every host, as only Windows's own CI reaches the refusal, and
1903
+ // what the host would do with each name is what no other host can show.
1904
+ // @behavior FS-153
1905
+ #[test]
1906
+ fn a_name_windows_reads_as_another_is_told_apart() {
1907
+ let aliases = [
1908
+ "secret.",
1909
+ "secret ",
1910
+ "file:stream",
1911
+ r"d\f",
1912
+ "con",
1913
+ "NUL.txt",
1914
+ "conin$",
1915
+ "com1 .log",
1916
+ "a?",
1917
+ "a*",
1918
+ "a<",
1919
+ "a|",
1920
+ "a\"",
1921
+ "a\u{1}",
1922
+ ];
1923
+ let plain = ["secret", "file.txt", "console", "com10", ".hidden", "a.b.c"];
1924
+ assert_eq!(
1925
+ (
1926
+ aliases
1927
+ .iter()
1928
+ .filter(|name| !is_windows_alias(name))
1929
+ .count(),
1930
+ plain.iter().filter(|name| is_windows_alias(name)).count(),
1931
+ ),
1932
+ (0, 0),
1933
+ "is_windows_alias must name every spelling Windows reads as another, and no plain name"
1934
+ );
1935
+ }
1936
+
1937
+ // @behavior FS-153
1938
+ #[cfg(windows)]
1939
+ #[test]
1940
+ fn a_directory_on_windows_refuses_a_name_it_reads_as_another() {
1941
+ let host = HostDir::new("aliases");
1942
+ fs::write(host.0.join("secret"), "x").expect("the host file must be written");
1943
+ let directory = bind_host(&host.0);
1944
+ assert!(
1945
+ ["secret.", "secret:stream", "CON"]
1946
+ .iter()
1947
+ .all(|name| matches!(
1948
+ directory.open(name, &OpenHow::default()),
1949
+ Err(FilesystemError::InvalidInput)
1950
+ )),
1951
+ "a name Windows reads as another, opened through a Bind on Windows, must be refused \
1952
+ as invalid input"
1953
+ );
1954
+ }
1955
+
1956
+ // The host answers ENAMETOOLONG, which only a mapping naming it keeps
1957
+ // from reaching the guest as an EIO.
1958
+ // @behavior FS-065
1959
+ #[test]
1960
+ fn a_directory_answers_a_host_failure_as_the_error_naming_it() {
1961
+ let host = HostDir::new("names");
1962
+ let directory = bind_host(&host.0);
1963
+ let created = directory.open_or_create(
1964
+ &"x".repeat(300),
1965
+ &OpenHow {
1966
+ write: true,
1967
+ ..OpenHow::default()
1968
+ },
1969
+ );
1970
+ assert_eq!(
1971
+ created.map(drop),
1972
+ Err(FilesystemError::InvalidFilename),
1973
+ "a name past the host's limit created through Bind::open must fail as \
1974
+ InvalidFilename, not as an unnamed failure"
1975
+ );
1976
+ }
1977
+
1978
+ // Which offset a host refuses depends on its filesystem: tmpfs, XFS and
1979
+ // overlay keep files up to `i64::MAX`. The host's own EFBIG, 27 on Linux
1980
+ // and macOS alike, is what the Bind must name.
1981
+ // @behavior FS-068
1982
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
1983
+ #[test]
1984
+ fn a_directory_answers_a_host_file_too_large_as_too_large() {
1985
+ assert_eq!(
1986
+ to_error(io::Error::from_raw_os_error(27)),
1987
+ FilesystemError::FileTooLarge,
1988
+ "a host EFBIG through Bind's error mapping must fail as FileTooLarge, \
1989
+ not as an unnamed failure"
1990
+ );
1991
+ }
1992
+
1993
+ // A Bind's root is the directory cap-std opened for its path alone on Linux,
1994
+ // so a sync of the descriptor it holds would answer EBADF only there.
1995
+ // @behavior FS-171
1996
+ #[test]
1997
+ fn a_binds_root_syncs() {
1998
+ let host = HostDir::new("sync-root");
1999
+ let mount = crate::mount::Mount::new(
2000
+ "/data",
2001
+ Filesystem::new(Arc::new(
2002
+ Bind::new(&host.0).expect("the host directory must open"),
2003
+ ))
2004
+ .expect("the instance must stand"),
2005
+ crate::mount::Access::Read,
2006
+ )
2007
+ .expect("/data is a guest path");
2008
+ let Ok(mut root) = crate::vfs::File::root(mount.mnt(), &Arc::default()) else {
2009
+ panic!("the root must open");
2010
+ };
2011
+ assert_eq!(
2012
+ root.fsync(false),
2013
+ Ok(()),
2014
+ "File::fsync of a Bind's root must succeed, as Linux syncs a directory"
2015
+ );
2016
+ }
2017
+
2018
+ // A rename moves a name out from under `openat2` only within a race, which
2019
+ // the Linux baseline meets at times and no test can stage, so the answer the
2020
+ // refusal reaches is held here.
2021
+ // @behavior FS-191
2022
+ #[test]
2023
+ fn a_name_carried_out_as_it_is_opened_answers_not_found() {
2024
+ let refused = io::Error::new(io::ErrorKind::PermissionDenied, "a path led outside");
2025
+ assert_eq!(
2026
+ to_error(refused),
2027
+ FilesystemError::NotFound,
2028
+ "cap-std's refusal of a single name leading out of a Bind's directory must answer \
2029
+ ENOENT, as a move carried it away"
2030
+ );
2031
+ }
2032
+
2033
+ // Eviction leaves a walk to reach a directory from one far above it, so only
2034
+ // a path past what the host takes in one call shows it goes on by name.
2035
+ // @behavior FS-190
2036
+ #[cfg(unix)]
2037
+ #[test]
2038
+ fn a_directory_past_the_hosts_path_limit_is_reached() {
2039
+ let host = HostDir::new("past-path-max");
2040
+ let top = Dir::open_ambient_dir(&host.0, ambient_authority()).expect("the host directory");
2041
+ let names = vec!["n".repeat(200); 30];
2042
+ let deepest = names
2043
+ .iter()
2044
+ .fold(top.try_clone().expect("a second handle"), |at, name| {
2045
+ at.create_dir(name)
2046
+ .expect("the host directory must be made");
2047
+ at.open_dir(name).expect("the host directory must open")
2048
+ });
2049
+ deepest
2050
+ .write("key", "x")
2051
+ .expect("the host file must be written");
2052
+ let reached = walk::walk(&top, &names).map(|dir| dir.exists("key"));
2053
+ assert_eq!(
2054
+ reached.ok(),
2055
+ Some(true),
2056
+ "a Bind's walk to a directory 6000 bytes beneath the one it holds must reach it, past \
2057
+ what the host takes in one call"
2058
+ );
2059
+ }
2060
+
2061
+ // Root reads anything, so a host letting this one be read has nothing to
2062
+ // show. Linux reaches a directory for its path alone, so only asking to read
2063
+ // it there makes the host answer; macOS answers on any open.
2064
+ // @behavior FS-189
2065
+ #[cfg(unix)]
2066
+ #[test]
2067
+ fn a_directory_the_host_lets_no_one_read_refuses_an_open_to_read() {
2068
+ use std::os::unix::fs::PermissionsExt;
2069
+
2070
+ let host = HostDir::new("unreadable");
2071
+ let dir = host.0.join("sealed");
2072
+ fs::create_dir(&dir).expect("the host directory must be made");
2073
+ let mode = |mode| fs::Permissions::from_mode(mode);
2074
+ fs::set_permissions(&dir, mode(0o000)).expect("the host directory's mode must be set");
2075
+ let enforced = fs::read_dir(&dir).is_err();
2076
+ let mount = crate::mount::Mount::new(
2077
+ "/data",
2078
+ Filesystem::new(Arc::new(
2079
+ Bind::new(&host.0).expect("the host directory must open"),
2080
+ ))
2081
+ .expect("the instance must stand"),
2082
+ crate::mount::Access::Read,
2083
+ )
2084
+ .expect("/data is a guest path");
2085
+ let root = crate::vfs::Path::root(mount.mnt());
2086
+ let how = crate::vfs::OpenHow {
2087
+ read: true,
2088
+ directory: true,
2089
+ ..Default::default()
2090
+ };
2091
+ let answer = crate::vfs::kern_path::filp_open(&Arc::default(), &root, "sealed", &how).err();
2092
+ fs::set_permissions(&dir, mode(0o755)).expect("the host directory's mode must be restored");
2093
+ if enforced {
2094
+ assert_eq!(
2095
+ answer,
2096
+ Some(FilesystemError::PermissionDenied),
2097
+ "vfs::do_file_open to read a host directory no one may read must answer EACCES"
2098
+ );
2099
+ }
2100
+ }
2101
+
2102
+ // Linux holds a directory for its path alone, so only asking the host to
2103
+ // read it refuses one no one may read; macOS refuses it on any open. Root
2104
+ // reads anything, so only another user sees the refusal.
2105
+ // @behavior FS-200
2106
+ #[cfg(unix)]
2107
+ #[test]
2108
+ fn a_bind_of_a_directory_the_host_lets_no_one_read_is_refused() {
2109
+ use std::os::unix::fs::PermissionsExt;
2110
+
2111
+ let host = HostDir::new("unreadable-root");
2112
+ let mode = |mode| fs::Permissions::from_mode(mode);
2113
+ fs::set_permissions(&host.0, mode(0o100)).expect("the host directory's mode must be set");
2114
+ let enforced = fs::read_dir(&host.0).is_err();
2115
+ let answer = Bind::new(&host.0).err().map(|err| err.kind());
2116
+ fs::set_permissions(&host.0, mode(0o755))
2117
+ .expect("the host directory's mode must be restored");
2118
+ if enforced {
2119
+ assert_eq!(
2120
+ answer,
2121
+ Some(io::ErrorKind::PermissionDenied),
2122
+ "Bind::new of a host directory no one may read must be refused as permission denied"
2123
+ );
2124
+ }
2125
+ }
2126
+
2127
+ // macOS has no open for a directory's path alone. Root reads anything, so
2128
+ // only a walk run as another user sees the difference.
2129
+ // @behavior FS-172
2130
+ #[cfg(target_os = "linux")]
2131
+ #[test]
2132
+ fn a_directory_a_host_lets_be_searched_reports_itself() {
2133
+ use std::os::unix::fs::PermissionsExt;
2134
+
2135
+ let host = HostDir::new("search-only");
2136
+ fs::create_dir(host.0.join("searched")).expect("the host directory must be made");
2137
+ let mode = |mode| fs::Permissions::from_mode(mode);
2138
+ fs::set_permissions(host.0.join("searched"), mode(0o111))
2139
+ .expect("the host directory's mode must be set");
2140
+ let mount = crate::mount::Mount::new(
2141
+ "/data",
2142
+ Filesystem::new(Arc::new(
2143
+ Bind::new(&host.0).expect("the host directory must open"),
2144
+ ))
2145
+ .expect("the instance must stand"),
2146
+ crate::mount::Access::Read,
2147
+ )
2148
+ .expect("/data is a guest path");
2149
+ let root = crate::vfs::Path::root(mount.mnt());
2150
+ let found = crate::vfs::vfs_fstatat(&Arc::default(), &root, "searched", false);
2151
+ fs::set_permissions(host.0.join("searched"), mode(0o755))
2152
+ .expect("the host directory's mode must be restored");
2153
+ assert_eq!(
2154
+ found.map(|metadata| metadata.file_type),
2155
+ Ok(FileType::Dir),
2156
+ "vfs::vfs_fstatat of a host directory only searchable must report it, as Linux's stat \
2157
+ needs no read permission on it"
2158
+ );
2159
+ }
2160
+
2161
+ // A host that folds case answers `A` with the entry stored as `a`, so the
2162
+ // VFS finds one entry on both sides; only a host that folds witnesses it.
2163
+ // @behavior FS-205
2164
+ #[test]
2165
+ fn a_rename_to_another_spelling_of_one_name_changes_nothing() {
2166
+ let host = HostDir::new("case-only-rename");
2167
+ fs::write(host.0.join("a"), "x").expect("the host file must be written");
2168
+ if !host.0.join("A").exists() {
2169
+ return;
2170
+ }
2171
+ let mount = crate::mount::Mount::new(
2172
+ "/data",
2173
+ Filesystem::new(Arc::new(
2174
+ Bind::new(&host.0).expect("the host directory must open"),
2175
+ ))
2176
+ .expect("the instance must stand"),
2177
+ crate::mount::Access::ReadWrite,
2178
+ )
2179
+ .expect("/data is a guest path");
2180
+ let root = crate::vfs::Path::root(mount.mnt());
2181
+ let renamed = crate::vfs::filename_renameat2(&Arc::default(), &root, "a", &root, "A");
2182
+ let stored: Vec<_> = fs::read_dir(&host.0)
2183
+ .expect("the host directory must list")
2184
+ .map(|entry| entry.expect("the entry must read").file_name())
2185
+ .collect();
2186
+ // Windows reports no inode, so nothing ties `A` to the stored `a` and
2187
+ // the spelling is refused rather than followed.
2188
+ let answered = match cfg!(windows) {
2189
+ true => Err(FilesystemError::PermissionDenied),
2190
+ false => Ok(()),
2191
+ };
2192
+ assert_eq!(
2193
+ (renamed, stored),
2194
+ (answered, vec![std::ffi::OsString::from("a")]),
2195
+ "vfs::filename_renameat2 of `a` to `A` on a Bind of a host that folds case must \
2196
+ leave `a` spelled as it was, as vfs_rename finds one entry, refusing it only \
2197
+ where the host ties no spelling to the entry"
2198
+ );
2199
+ }
2200
+
2201
+ // Linux walks a directory it may only search, so a stat through one
2202
+ // succeeds there; the Bind reads the directory to learn the name's
2203
+ // spelling, which the mode refuses. Root reads anything, so only another
2204
+ // user sees the refusal.
2205
+ // @behavior FS-202
2206
+ #[cfg(unix)]
2207
+ #[test]
2208
+ fn a_name_beneath_a_directory_the_host_lets_only_be_searched_is_refused() {
2209
+ use std::os::unix::fs::PermissionsExt;
2210
+
2211
+ let host = HostDir::new("search-only-name");
2212
+ fs::create_dir(host.0.join("searched")).expect("the host directory must be made");
2213
+ fs::write(host.0.join("searched/x"), "x").expect("the host file must be written");
2214
+ let mode = |mode| fs::Permissions::from_mode(mode);
2215
+ fs::set_permissions(host.0.join("searched"), mode(0o111))
2216
+ .expect("the host directory's mode must be set");
2217
+ let enforced = fs::read_dir(host.0.join("searched")).is_err();
2218
+ let mount = crate::mount::Mount::new(
2219
+ "/data",
2220
+ Filesystem::new(Arc::new(
2221
+ Bind::new(&host.0).expect("the host directory must open"),
2222
+ ))
2223
+ .expect("the instance must stand"),
2224
+ crate::mount::Access::Read,
2225
+ )
2226
+ .expect("/data is a guest path");
2227
+ let root = crate::vfs::Path::root(mount.mnt());
2228
+ let found = crate::vfs::vfs_fstatat(&Arc::default(), &root, "searched/x", false);
2229
+ fs::set_permissions(host.0.join("searched"), mode(0o755))
2230
+ .expect("the host directory's mode must be restored");
2231
+ if enforced {
2232
+ assert_eq!(
2233
+ found.err(),
2234
+ Some(FilesystemError::PermissionDenied),
2235
+ "vfs::vfs_fstatat of a file beneath a host directory only searchable must \
2236
+ answer EACCES, as the Bind reads the directory to spell the name"
2237
+ );
2238
+ }
2239
+ }
2240
+
2241
+ // Only Linux keeps a name that is not UTF-8; macOS refuses to make one.
2242
+ // @behavior FS-066
2243
+ #[cfg(target_os = "linux")]
2244
+ #[test]
2245
+ fn a_directory_refuses_to_list_a_host_name_that_is_not_utf8() {
2246
+ use std::os::unix::ffi::OsStrExt;
2247
+
2248
+ let host = HostDir::new("ilseq");
2249
+ fs::write(host.0.join(std::ffi::OsStr::from_bytes(b"\xff")), "")
2250
+ .expect("the host file must be written");
2251
+ let directory = bind_host(&host.0);
2252
+ assert_eq!(
2253
+ directory.iterate_dir().map(drop),
2254
+ Err(FilesystemError::IllegalSequence),
2255
+ "a host name that is not UTF-8 listed through iterate_dir must fail as \
2256
+ IllegalSequence, not as an unnamed failure"
2257
+ );
2258
+ }
2259
+
2260
+ // The host file only reads, so a host open asking to write, as one that
2261
+ // creates must, would be refused where nothing is to be created.
2262
+ // @behavior FS-064
2263
+ #[cfg(unix)]
2264
+ #[test]
2265
+ fn a_directory_opens_a_file_already_there_without_writing_to_create_it() {
2266
+ use std::os::unix::fs::PermissionsExt;
2267
+ let host = HostDir::new("create-existing");
2268
+ fs::write(host.0.join("note"), "x").expect("the host file must be written");
2269
+ fs::set_permissions(host.0.join("note"), fs::Permissions::from_mode(0o444))
2270
+ .expect("the host file must become read-only");
2271
+ let directory = bind_host(&host.0);
2272
+ let creates = OpenHow {
2273
+ read: true,
2274
+ ..OpenHow::default()
2275
+ };
2276
+ assert_eq!(
2277
+ (
2278
+ directory.open_or_create("note", &creates).map(drop),
2279
+ directory.open_or_create("new", &creates).map(drop),
2280
+ host.0.join("new").exists()
2281
+ ),
2282
+ (Ok(()), Ok(()), true),
2283
+ "a Bind::open that may create, of a read-only host file already there, must open \
2284
+ it, and of a missing one must create it"
2285
+ );
2286
+ }
2287
+
2288
+ // The link's target is a readable file, so only the refusal of the
2289
+ // link itself keeps the open from reaching it.
2290
+ // @behavior FS-047
2291
+ #[cfg(unix)]
2292
+ #[test]
2293
+ fn a_directory_opens_no_link() {
2294
+ let host = HostDir::new("nofollow");
2295
+ fs::write(host.0.join("target"), "x").expect("the host file must be written");
2296
+ std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
2297
+ let opened = bind_host(&host.0)
2298
+ .open(
2299
+ "link",
2300
+ &OpenHow {
2301
+ read: true,
2302
+ ..OpenHow::default()
2303
+ },
2304
+ )
2305
+ .map(drop);
2306
+ assert_eq!(
2307
+ opened,
2308
+ Err(FilesystemError::FilesystemLoop),
2309
+ "a host link opened through a Bind must be refused as a link, its target unreached"
2310
+ );
2311
+ }
2312
+
2313
+ // The VFS refuses a link it found before asking the Bind, so only the
2314
+ // Bind's own open sees a link the host put in place after the lookup.
2315
+ // @behavior FS-047
2316
+ #[cfg(unix)]
2317
+ #[test]
2318
+ fn a_directory_refuses_a_link_put_in_place_of_what_it_opens() {
2319
+ let host = HostDir::new("nofollow-swapped");
2320
+ fs::write(host.0.join("target"), "x").expect("the host file must be written");
2321
+ std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
2322
+ let dir = Dir::open_ambient_dir(&host.0, ambient_authority()).expect("the host directory");
2323
+ let read = OpenHow {
2324
+ read: true,
2325
+ ..OpenHow::default()
2326
+ };
2327
+ assert_eq!(
2328
+ open_in(&dir, "link", &read, false).map(drop),
2329
+ Err(FilesystemError::FilesystemLoop),
2330
+ "a host link standing where a Bind opens a file must be refused as a link, its \
2331
+ target unreached"
2332
+ );
2333
+ }
2334
+
2335
+ // The target's five bytes would show as the link's size if a lookup
2336
+ // that follows none still followed it.
2337
+ // @behavior FS-048
2338
+ #[cfg(unix)]
2339
+ #[test]
2340
+ fn a_directory_reports_a_link_a_lookup_does_not_follow_as_a_link() {
2341
+ let host = HostDir::new("lstat");
2342
+ fs::write(host.0.join("target"), "hello").expect("the host file must be written");
2343
+ std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
2344
+ let directory = bind_host(&host.0);
2345
+ let listed = directory
2346
+ .iterate_dir()
2347
+ .expect("the host directory must list")
2348
+ .into_iter()
2349
+ .find(|entry| entry.name == "link")
2350
+ .map(|entry| entry.file_type);
2351
+ assert_eq!(
2352
+ (
2353
+ directory.vfs_lstat("link").map(|stat| stat.file_type),
2354
+ listed
2355
+ ),
2356
+ (Ok(FileType::Symlink), Some(FileType::Symlink)),
2357
+ "a host link beneath a Bind, looked up through vfs_lstat and listed through \
2358
+ iterate_dir, must report a link"
2359
+ );
2360
+ }
2361
+
2362
+ // A directory's size is the host's to choose, so the host's own stat
2363
+ // is the only witness of it.
2364
+ // @behavior FS-138
2365
+ #[test]
2366
+ fn a_directory_reports_the_size_its_host_reports() {
2367
+ let host = HostDir::new("dir-size");
2368
+ fs::create_dir(host.0.join("sub")).expect("the host directory must be created");
2369
+ fs::write(host.0.join("sub/file"), "hello").expect("the host file must be written");
2370
+ let directory = bind_host(&host.0);
2371
+ assert_eq!(
2372
+ directory.vfs_lstat("sub").map(|stat| stat.size),
2373
+ Ok(fs::metadata(host.0.join("sub"))
2374
+ .expect("the host directory must stand")
2375
+ .len()),
2376
+ "a host directory beneath a Bind looked up through vfs_lstat must report the size \
2377
+ its host reports"
2378
+ );
2379
+ }
2380
+
2381
+ // @behavior FS-028
2382
+ #[test]
2383
+ fn a_directory_refuses_to_remove_a_directory_as_a_file() {
2384
+ let host = HostDir::new("unlink-dir");
2385
+ fs::create_dir(host.0.join("sub")).expect("the host directory must be created");
2386
+ let directory = bind_host(&host.0);
2387
+ assert_eq!(
2388
+ directory.vfs_unlink("sub"),
2389
+ Err(FilesystemError::IsADirectory),
2390
+ "a host directory beneath a Bind removed through vfs_unlink must answer EISDIR"
2391
+ );
2392
+ }
2393
+
2394
+ /// A host directory holding `dir/sub/y` and `secret`, with `dir/in`
2395
+ /// linking to `sub` and `dir/up` to `../secret`.
2396
+ #[cfg(unix)]
2397
+ fn make_links(name: &str) -> HostDir {
2398
+ let host = HostDir::new(name);
2399
+ fs::create_dir_all(host.0.join("dir/sub")).expect("the host directory must be created");
2400
+ fs::write(host.0.join("dir/sub/y"), "inner").expect("the inner file must be written");
2401
+ fs::write(host.0.join("secret"), "outer").expect("the outer file must be written");
2402
+ std::os::unix::fs::symlink("sub", host.0.join("dir/in")).expect("the link must be made");
2403
+ std::os::unix::fs::symlink("../secret", host.0.join("dir/up"))
2404
+ .expect("the link must be made");
2405
+ host
2406
+ }
2407
+
2408
+ // The link leads to a directory inside the host directory, so only
2409
+ // refusing the link itself keeps it from being opened.
2410
+ // @behavior FS-099
2411
+ #[cfg(unix)]
2412
+ #[test]
2413
+ fn a_directory_opens_no_link_as_a_directory_where_asked_not_to_follow() {
2414
+ let host = make_links("open-dir-nofollow");
2415
+ let directory = bind_host(&host.0);
2416
+ let dir = directory.open_dir("dir").expect("the directory must open");
2417
+ assert_eq!(
2418
+ dir.open_dir("in").map(drop),
2419
+ Err(FilesystemError::FilesystemLoop),
2420
+ "a link to a directory opened through Bind::open_dir must be a link loop"
2421
+ );
2422
+ }
2423
+
2424
+ // @behavior FS-114
2425
+ #[cfg(unix)]
2426
+ #[test]
2427
+ fn a_directory_reads_where_a_link_points() {
2428
+ let host = make_links("read-link");
2429
+ let directory = bind_host(&host.0);
2430
+ let dir = directory.open_dir("dir").expect("the directory must open");
2431
+ assert_eq!(
2432
+ (dir.vfs_readlink("up"), dir.vfs_readlink("sub")),
2433
+ (
2434
+ Ok("../secret".to_string()),
2435
+ Err(FilesystemError::InvalidInput)
2436
+ ),
2437
+ "vfs_readlink beneath a Bind must answer where a link points as stored, and refuse \
2438
+ a directory with EINVAL"
2439
+ );
2440
+ }
2441
+
2442
+ // @behavior FS-007
2443
+ #[cfg(unix)]
2444
+ #[test]
2445
+ fn a_directory_reaches_nothing_outside_its_host_directory() {
2446
+ let outside = HostDir::new("outside");
2447
+ fs::write(outside.0.join("secret"), "host").expect("the outside file must be written");
2448
+ let host = HostDir::new("confined");
2449
+ std::os::unix::fs::symlink(outside.0.join("secret"), host.0.join("escape"))
2450
+ .expect("the link must be created");
2451
+
2452
+ let directory = bind_host(&host.0);
2453
+ assert!(
2454
+ directory
2455
+ .open(
2456
+ "escape",
2457
+ &OpenHow {
2458
+ read: true,
2459
+ ..OpenHow::default()
2460
+ }
2461
+ )
2462
+ .is_err(),
2463
+ "a link out of the host directory opened through a Bind's open must be refused"
2464
+ );
2465
+ }
2466
+
2467
+ // @behavior FS-071
2468
+ #[cfg(unix)]
2469
+ #[test]
2470
+ fn a_directory_reports_the_identity_its_host_gives_a_path() {
2471
+ use std::os::unix::fs::MetadataExt;
2472
+
2473
+ let host = HostDir::new("identity");
2474
+ fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
2475
+ let expected = fs::metadata(host.0.join("dir")).expect("the host must stat it");
2476
+ let directory = bind_host(&host.0);
2477
+ let reported = directory
2478
+ .vfs_lstat("dir")
2479
+ .expect("the directory must stand");
2480
+ assert_eq!(
2481
+ (reported.dev, reported.ino),
2482
+ (expected.dev(), expected.ino()),
2483
+ "a host directory beneath a Bind looked up through vfs_lstat must carry the device \
2484
+ and inode numbers the host reports"
2485
+ );
2486
+ }
2487
+
2488
+ // Two handles racing one-byte appends: a two-step append (read the
2489
+ // length, then write there) loses about half the bytes to the other
2490
+ // handle's writes, so only an append the host places keeps them all.
2491
+ // @behavior FS-069
2492
+ #[cfg(unix)]
2493
+ #[test]
2494
+ fn a_directory_appends_at_its_host_files_end_in_one_step() {
2495
+ let host = HostDir::new("append");
2496
+ fs::write(host.0.join("log.txt"), "").expect("the host file must be written");
2497
+ let directory = bind_host(&host.0);
2498
+
2499
+ let writers: Vec<_> = [b"a", b"b"]
2500
+ .into_iter()
2501
+ .map(|byte| {
2502
+ let directory = directory.clone();
2503
+ std::thread::spawn(move || {
2504
+ let how = OpenHow {
2505
+ write: true,
2506
+ ..OpenHow::default()
2507
+ };
2508
+ let mut file = directory
2509
+ .open("log.txt", &how)
2510
+ .expect("the host file must open to write");
2511
+ for _ in 0..10_000 {
2512
+ file.write_iter(&mut Kiocb::at(0).with_append(), byte)
2513
+ .expect("the append must succeed");
2514
+ }
2515
+ })
2516
+ })
2517
+ .collect();
2518
+ for writer in writers {
2519
+ writer.join().expect("the writer must finish");
2520
+ }
2521
+
2522
+ let written = fs::read(host.0.join("log.txt")).expect("the host file must be read");
2523
+ assert_eq!(
2524
+ (
2525
+ written.iter().filter(|&&b| b == b'a').count(),
2526
+ written.iter().filter(|&&b| b == b'b').count()
2527
+ ),
2528
+ (10_000, 10_000),
2529
+ "two handles appending to one host file through a Bind's FileOperations::write_iter must each \
2530
+ keep every byte"
2531
+ );
2532
+ }
2533
+
2534
+ // ext4 hands a removed directory's inode number to the next one made,
2535
+ // so the number staying unused while open is what the opened directory
2536
+ // keeps. A host that never reuses numbers cannot show that, but every
2537
+ // Linux host lists the removed directory among the process's
2538
+ // descriptors while it is open.
2539
+ // @behavior FS-086
2540
+ #[cfg(target_os = "linux")]
2541
+ #[test]
2542
+ fn a_directory_opened_keeps_its_inode_number_from_the_next_one_made() {
2543
+ use std::os::unix::fs::MetadataExt;
2544
+
2545
+ let host = HostDir::new("held");
2546
+ let ino = |path: &str| {
2547
+ fs::metadata(host.0.join(path))
2548
+ .expect("it must stand")
2549
+ .ino()
2550
+ };
2551
+ fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
2552
+ let first = ino("dir");
2553
+ fs::remove_dir(host.0.join("dir")).expect("the host directory must go");
2554
+ fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
2555
+ let reuses = ino("dir") == first;
2556
+ let first = ino("dir");
2557
+ let directory = bind_host(&host.0);
2558
+ let opened = directory.open_dir("dir").expect("the directory must open");
2559
+ fs::remove_dir(host.0.join("dir")).expect("the host directory must go");
2560
+ let removed = format!("{} (deleted)", host.0.join("dir").display());
2561
+ let held_open = fs::read_dir("/proc/self/fd")
2562
+ .expect("the process's descriptors must list")
2563
+ .filter_map(|entry| fs::read_link(entry.ok()?.path()).ok())
2564
+ .any(|target| target.to_string_lossy() == removed);
2565
+ fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
2566
+ let made = ino("dir");
2567
+ drop(opened);
2568
+ assert!(
2569
+ held_open,
2570
+ "a directory Bind::open_dir opened must stay open once removed"
2571
+ );
2572
+ if !reuses {
2573
+ eprintln!("the host does not reuse inode numbers; nothing more to witness");
2574
+ return;
2575
+ }
2576
+ assert_ne!(
2577
+ made, first,
2578
+ "a directory made where Bind::open_dir holds a removed one open must take \
2579
+ another inode number on a host reusing them"
2580
+ );
2581
+ }
2582
+
2583
+ // Windows refuses to move a directory a handle holds open, so the host
2584
+ // cannot make the move this witnesses there.
2585
+ // @behavior FS-108
2586
+ #[cfg(unix)]
2587
+ #[test]
2588
+ fn a_directory_opened_from_a_directory_follows_it_when_the_host_moves_it() {
2589
+ let host = HostDir::new("opened-moves");
2590
+ fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
2591
+ let directory = bind_host(&host.0);
2592
+ let dir = directory
2593
+ .open_dir("dir")
2594
+ .expect("the host directory must open");
2595
+ fs::rename(host.0.join("dir"), host.0.join("moved")).expect("the host must move it");
2596
+ dir.open_or_create(
2597
+ "made",
2598
+ &OpenHow {
2599
+ write: true,
2600
+ ..OpenHow::default()
2601
+ },
2602
+ )
2603
+ .expect("the file must be made");
2604
+ assert_eq!(
2605
+ (
2606
+ host.0.join("moved/made").is_file(),
2607
+ host.0.join("dir").exists()
2608
+ ),
2609
+ (true, false),
2610
+ "a file made through a directory Bind::open_dir opened before the host moved it \
2611
+ must stand beneath the directory's new host path"
2612
+ );
2613
+ }
2614
+
2615
+ // @behavior FS-081
2616
+ #[test]
2617
+ fn a_directory_spells_names_it_matches_exactly_as_given() {
2618
+ let host = HostDir::new("spelled");
2619
+ fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
2620
+ fs::write(host.0.join("dir/file"), "x").expect("the host file must be written");
2621
+ let directory = bind_host(&host.0);
2622
+ let dir = directory
2623
+ .open_dir("dir")
2624
+ .expect("the host directory must open");
2625
+ assert_eq!(
2626
+ (dir.lookup("file"), dir.lookup("new")),
2627
+ (Ok("file".to_string()), Ok("new".to_string())),
2628
+ "a name asked of Bind::lookup in the spelling its host stores, and one it \
2629
+ does not hold, must come back as given"
2630
+ );
2631
+ }
2632
+
2633
+ // macOS volumes fold case and Unicode form unless formatted
2634
+ // case-sensitive, where every spelling is already the stored one.
2635
+ // @behavior FS-082
2636
+ #[cfg(target_os = "macos")]
2637
+ #[test]
2638
+ fn a_directory_spells_a_path_as_a_folding_host_stores_it() {
2639
+ let host = HostDir::new("folded");
2640
+ fs::create_dir(host.0.join("private")).expect("the host directory must be made");
2641
+ fs::write(host.0.join("private/caf\u{e9}"), "x").expect("the host file must be written");
2642
+ if !host.0.join("PRIVATE").exists() {
2643
+ eprintln!("the host volume matches names exactly; nothing folds to witness");
2644
+ return;
2645
+ }
2646
+ let directory = bind_host(&host.0);
2647
+ let private = directory
2648
+ .open_dir("PRIVATE")
2649
+ .expect("the host directory must open in any case");
2650
+ assert_eq!(
2651
+ (
2652
+ directory.lookup("Private"),
2653
+ private.lookup("CAFE\u{301}"),
2654
+ private.lookup("missing")
2655
+ ),
2656
+ (
2657
+ Ok("private".to_string()),
2658
+ Ok("caf\u{e9}".to_string()),
2659
+ Ok("missing".to_string())
2660
+ ),
2661
+ "a name asked of Bind::lookup in another case and Unicode form on a host \
2662
+ folding both must come back as the host stores it, one it does not hold as given"
2663
+ );
2664
+ }
2665
+ }