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,696 @@
1
+ //! The security modules an Invocation registers: kobako's own rules, and the
2
+ //! Host App's Policy for each Mount that has one. Linux has neither, so they
3
+ //! answer through the hooks it leaves for such modules, and the VFS never
4
+ //! learns what they decide.
5
+
6
+ use std::sync::Arc;
7
+
8
+ use crate::mount::{Access, Mount, Policy};
9
+ use crate::vfs::{
10
+ d_revalidate, Cred, Dentry, FMode, FileSecurity, FileType, FilesystemError, OpenHow, Path,
11
+ SecurityOperations, SecurityPath, VfsMount, VmCommitted, PATH_MAX,
12
+ };
13
+
14
+ /// What a Mount's Policy allowed a file as it opened, kept on the open file, as
15
+ /// Landlock's file blob keeps `allowed_access`.
16
+ struct AllowedAccess(Access);
17
+
18
+ /// Whether each name from the root down to `path` still names what the runtime
19
+ /// recorded, as gVisor's `revalidatePath` walks one. Linux asks only names a walk
20
+ /// passes through, so a `WalkGuard` asks this of where a call starts. A name moved
21
+ /// while it was asked answers EAGAIN, as a scoped walk's `..` answers a move it
22
+ /// met, and the caller makes the call again.
23
+ fn revalidate_path(path: &Path) -> Result<bool, FilesystemError> {
24
+ let mut chain = vec![path.dentry().clone()];
25
+ while let Some(at) = chain.last().filter(|at| !at.is_root()) {
26
+ let above = at.parent();
27
+ chain.push(above);
28
+ }
29
+ for pair in chain.windows(2).rev() {
30
+ let [name, above] = pair else { continue };
31
+ let _held = above.inode_lock_shared()?;
32
+ if !Arc::ptr_eq(&name.parent(), above) {
33
+ return Err(FilesystemError::WouldBlock);
34
+ }
35
+ if !d_revalidate(path.mnt().sb(), above, name)? {
36
+ return Ok(false);
37
+ }
38
+ }
39
+ Ok(true)
40
+ }
41
+
42
+ /// What every Invocation holds to, whatever its Mounts allow.
43
+ struct KobakoSecurity;
44
+
45
+ impl SecurityOperations for KobakoSecurity {
46
+ /// Only a file or a directory opens: the runtime provides no peer for a
47
+ /// FIFO or a socket, and a device would act on being opened.
48
+ fn file_open(
49
+ &self,
50
+ _path: SecurityPath<'_>,
51
+ kind: FileType,
52
+ _mode: FMode,
53
+ _how: &OpenHow,
54
+ ) -> Result<FileSecurity, FilesystemError> {
55
+ match kind {
56
+ FileType::RegFile | FileType::Dir => Ok(None),
57
+ _ => Err(FilesystemError::PermissionDenied),
58
+ }
59
+ }
60
+
61
+ /// A guest never reads where a link points.
62
+ fn inode_readlink(&self, _dentry: &Dentry) -> Result<(), FilesystemError> {
63
+ Err(FilesystemError::Unsupported)
64
+ }
65
+ }
66
+
67
+ /// A Mount's Policy, answering only for that Mount.
68
+ struct MountPolicy {
69
+ mnt: Arc<VfsMount>,
70
+ at: String,
71
+ policy: Arc<dyn Policy>,
72
+ }
73
+
74
+ impl MountPolicy {
75
+ fn guards(&self, mnt: &Arc<VfsMount>) -> bool {
76
+ Arc::ptr_eq(&self.mnt, mnt)
77
+ }
78
+
79
+ /// The absolute guest path `path`, from the Mount's root, stands for.
80
+ fn guest_path(&self, path: &str) -> String {
81
+ match (self.at.as_str(), path) {
82
+ (at, "/") => at.to_string(),
83
+ ("/", path) => path.to_string(),
84
+ (at, path) => format!("{at}{path}"),
85
+ }
86
+ }
87
+
88
+ /// Judge `path` as AppArmor's `aa_path_perm` judges a whole path once, each
89
+ /// hook below reaching it as AppArmor's do. The Policy judges the path as the
90
+ /// Filesystem stores its names, so a host folding case or Unicode form cannot
91
+ /// hand it one spelling of an entry it refuses by another. One a Policy could
92
+ /// not be handed within `PATH_MAX` is refused as too long, as AppArmor's
93
+ /// `aa_path_name` refuses one past its buffer.
94
+ fn path_perm(&self, path: SecurityPath<'_>, access: Access) -> Result<(), FilesystemError> {
95
+ if !self.guards(path.mnt) {
96
+ return Ok(());
97
+ }
98
+ let path = self.guest_path(&(path.name)());
99
+ if path.len() >= PATH_MAX {
100
+ return Err(FilesystemError::InvalidFilename);
101
+ }
102
+ match self.policy.allows(&path, access) {
103
+ true => Ok(()),
104
+ false => Err(FilesystemError::PermissionDenied),
105
+ }
106
+ }
107
+ }
108
+
109
+ impl SecurityOperations for MountPolicy {
110
+ fn path_mknod(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
111
+ self.path_perm(path, Access::ReadWrite)
112
+ }
113
+
114
+ fn path_mkdir(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
115
+ self.path_perm(path, Access::ReadWrite)
116
+ }
117
+
118
+ fn path_rmdir(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
119
+ self.path_perm(path, Access::ReadWrite)
120
+ }
121
+
122
+ fn path_unlink(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
123
+ self.path_perm(path, Access::ReadWrite)
124
+ }
125
+
126
+ fn path_symlink(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
127
+ self.path_perm(path, Access::ReadWrite)
128
+ }
129
+
130
+ fn path_link(
131
+ &self,
132
+ old: SecurityPath<'_>,
133
+ new: SecurityPath<'_>,
134
+ ) -> Result<(), FilesystemError> {
135
+ self.path_perm(old, Access::ReadWrite)?;
136
+ self.path_perm(new, Access::ReadWrite)
137
+ }
138
+
139
+ /// Asked once for each open, for what the open may change, and kept on the
140
+ /// file, as Landlock keeps the rights it allowed. A file the open made was
141
+ /// judged as a change already, through `path_mknod`.
142
+ fn file_open(
143
+ &self,
144
+ path: SecurityPath<'_>,
145
+ _kind: FileType,
146
+ mode: FMode,
147
+ how: &OpenHow,
148
+ ) -> Result<FileSecurity, FilesystemError> {
149
+ if !self.guards(path.mnt) {
150
+ return Ok(None);
151
+ }
152
+ let created = mode.contains(FMode::CREATED);
153
+ let access = match created || mode.contains(FMode::WRITE) || how.truncate {
154
+ true => Access::ReadWrite,
155
+ false => Access::Read,
156
+ };
157
+ if !created {
158
+ self.path_perm(path, access)?;
159
+ }
160
+ Ok(Some(Arc::new(AllowedAccess(access))))
161
+ }
162
+
163
+ /// A truncate follows what the Policy allowed as the file opened, wherever
164
+ /// the file stands now, as Landlock's `hook_file_truncate` reads the file's
165
+ /// rights.
166
+ fn file_truncate(&self, kept: &FileSecurity) -> Result<(), FilesystemError> {
167
+ match kept
168
+ .as_deref()
169
+ .and_then(|kept| kept.downcast_ref::<AllowedAccess>())
170
+ {
171
+ Some(AllowedAccess(Access::Read)) => Err(FilesystemError::PermissionDenied),
172
+ _ => Ok(()),
173
+ }
174
+ }
175
+
176
+ fn path_utimes(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
177
+ self.path_perm(path, Access::ReadWrite)
178
+ }
179
+
180
+ /// Both paths are judged. Everything beneath a directory would take paths the
181
+ /// Policy never judged, and a Policy answers for one path, not a tree, so its
182
+ /// move is refused as one across devices and the guest copies entry by entry
183
+ /// instead, as Landlock answers a move it cannot show keeps each right beneath.
184
+ /// A move onto its own name takes no new path.
185
+ fn path_rename(
186
+ &self,
187
+ old: SecurityPath<'_>,
188
+ new: SecurityPath<'_>,
189
+ (old_dentry, new_dentry): (&Arc<Dentry>, &Arc<Dentry>),
190
+ ) -> Result<(), FilesystemError> {
191
+ self.path_perm(old, Access::ReadWrite)?;
192
+ self.path_perm(new, Access::ReadWrite)?;
193
+ let moves_dir =
194
+ old_dentry.kind() == Ok(FileType::Dir) && !Arc::ptr_eq(old_dentry, new_dentry);
195
+ match moves_dir && self.guards(old.mnt) {
196
+ true => Err(FilesystemError::CrossesDevices),
197
+ false => Ok(()),
198
+ }
199
+ }
200
+ }
201
+
202
+ /// What keeps a Policy judging the path a descriptor stands at, kobako's own
203
+ /// guarantee: Linux asks nothing of the dentry a walk starts from, so nothing in
204
+ /// its VFS can ask this, and a Wasm Driver asks it wherever a descriptor becomes where
205
+ /// a call starts.
206
+ pub struct WalkGuard {
207
+ /// The mounts a Policy judges; no other holds anything to keep true.
208
+ guarded: Vec<Arc<VfsMount>>,
209
+ }
210
+
211
+ impl WalkGuard {
212
+ /// What keeps each Policy over `mounts` judging the path it means to.
213
+ pub fn new<'a>(mounts: impl IntoIterator<Item = &'a Mount>) -> Self {
214
+ let guarded = mounts
215
+ .into_iter()
216
+ .filter(|mount| mount.policy().is_some())
217
+ .map(|mount| mount.mnt().clone())
218
+ .collect();
219
+ Self { guarded }
220
+ }
221
+
222
+ /// Refuse a call through a descriptor unless its dentry still names what the
223
+ /// runtime opened there, so the Policy judges the path it stands at. A dentry
224
+ /// dropped or removed is refused as Linux's AppArmor refuses an unlinked one,
225
+ /// and one whose identity the runtime never learned cannot be told from another,
226
+ /// so it is refused too. This keeps a host or another Filesystem instance moving
227
+ /// a directory behind the runtime's back from having a Policy judge a path it no
228
+ /// longer stands at.
229
+ pub fn check(&self, path: &Path) -> Result<(), FilesystemError> {
230
+ if !self.guarded.iter().any(|mnt| Arc::ptr_eq(mnt, path.mnt())) {
231
+ return Ok(());
232
+ }
233
+ let dentry = path.dentry();
234
+ if dentry.is_root() {
235
+ return Ok(());
236
+ }
237
+ let told_apart = dentry
238
+ .inode()
239
+ .is_some_and(|inode| inode.identity().is_some());
240
+ if path.mnt().sb().dentries().unlinked(dentry) || !told_apart || !revalidate_path(path)? {
241
+ return Err(FilesystemError::NotFound);
242
+ }
243
+ Ok(())
244
+ }
245
+ }
246
+
247
+ /// The Cred an Invocation opens its Mounts' roots with, its Filesystems holding at
248
+ /// most `memory_limit` bytes on its behalf, or without bound for `None`: kobako's
249
+ /// own rules, then each of `modules` in order, before any Mount's Policy applies,
250
+ /// as a Linux task opens the files it keeps before `landlock_restrict_self`.
251
+ /// The modules are those the Wasm Driver was built with, as Linux stacks the LSMs
252
+ /// `lsm=` names at boot after the capability module.
253
+ pub fn prepare_creds_within(
254
+ modules: &[Arc<dyn SecurityOperations>],
255
+ memory_limit: Option<usize>,
256
+ ) -> Arc<Cred> {
257
+ let mut security: Vec<Arc<dyn SecurityOperations>> = vec![Arc::new(KobakoSecurity)];
258
+ security.extend(modules.iter().cloned());
259
+ Cred::within(security, memory_limit.map(VmCommitted::new))
260
+ }
261
+
262
+ /// `cred` with each Mount's Policy added, the modules it held and the memory
263
+ /// held on its behalf shared, so what either Cred charges counts against one
264
+ /// limit, as `landlock_restrict_self` adds a domain to the task's Cred.
265
+ pub fn restrict_self<'a>(cred: &Cred, mounts: impl IntoIterator<Item = &'a Mount>) -> Arc<Cred> {
266
+ cred.with_modules(policies_of(mounts))
267
+ }
268
+
269
+ fn policies_of<'a>(
270
+ mounts: impl IntoIterator<Item = &'a Mount>,
271
+ ) -> Vec<Arc<dyn SecurityOperations>> {
272
+ mounts
273
+ .into_iter()
274
+ .filter_map(|mount| {
275
+ let policy = mount.policy()?;
276
+ Some(Arc::new(MountPolicy {
277
+ mnt: mount.mnt().clone(),
278
+ at: mount.at().to_string(),
279
+ policy: policy.clone(),
280
+ }) as Arc<dyn SecurityOperations>)
281
+ })
282
+ .collect()
283
+ }
284
+
285
+ #[cfg(test)]
286
+ mod tests {
287
+ use super::*;
288
+ use crate::filesystem::wrap::{Hook, Wrap};
289
+ use crate::filesystem::{DentryOperations, Filesystem, InodeOperations, Tmpfs};
290
+
291
+ /// A Policy allowing everything, counting how often it is asked.
292
+ #[derive(Default)]
293
+ struct Counts(std::sync::atomic::AtomicUsize);
294
+
295
+ impl Policy for Counts {
296
+ fn allows(&self, _path: &str, _access: Access) -> bool {
297
+ self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
298
+ true
299
+ }
300
+ }
301
+
302
+ /// A Policy refusing every change beneath `/guarded/q`.
303
+ struct RefusesChangesInQ;
304
+
305
+ impl Policy for RefusesChangesInQ {
306
+ fn allows(&self, path: &str, access: Access) -> bool {
307
+ access == Access::Read || !path.starts_with("/guarded/q/")
308
+ }
309
+ }
310
+
311
+ // The Policy refuses a change where the file stands by the truncate, so only
312
+ // a decision kept from the open lets it through, as Landlock keeps the
313
+ // truncate right it allowed on the file.
314
+ // @behavior RT-272
315
+ #[test]
316
+ fn a_truncate_follows_what_the_policy_allowed_as_the_file_opened() {
317
+ let filesystem = Filesystem::new(Arc::new(
318
+ [("p/f", "x"), ("q/keep", "x")]
319
+ .into_iter()
320
+ .collect::<Tmpfs>(),
321
+ ))
322
+ .expect("the instance must stand");
323
+ let guarded = Mount::new("/guarded", filesystem.clone(), Access::ReadWrite)
324
+ .expect("/guarded is a guest path")
325
+ .with_policy(Arc::new(RefusesChangesInQ));
326
+ let plain =
327
+ Mount::new("/plain", filesystem, Access::ReadWrite).expect("/plain is a guest path");
328
+ let cred = restrict_self(&prepare_creds_within(&[], None), [&guarded]);
329
+ let write = OpenHow {
330
+ write: true,
331
+ ..Default::default()
332
+ };
333
+ let mut held =
334
+ crate::vfs::kern_path::filp_open(&cred, &Path::root(guarded.mnt()), "p/f", &write)
335
+ .unwrap_or_else(|_| panic!("the file must open"));
336
+ let root = Path::root(plain.mnt());
337
+ crate::vfs::filename_renameat2(
338
+ &restrict_self(&prepare_creds_within(&[], None), []),
339
+ &root,
340
+ "p/f",
341
+ &root,
342
+ "q/f",
343
+ )
344
+ .expect("the move must land");
345
+ assert_eq!(
346
+ held.truncate(0),
347
+ Ok(()),
348
+ "File::truncate of a file opened to write beneath a Policy, moved since to where the \
349
+ Policy refuses changes, must follow what the Policy allowed as it opened"
350
+ );
351
+ }
352
+
353
+ // A Policy refusing nothing still counts each ask, so only an open asking
354
+ // no module leaves it at zero.
355
+ // @behavior RT-302
356
+ #[test]
357
+ fn an_open_standing_for_a_path_asks_no_policy() {
358
+ let policy = Arc::new(Counts::default());
359
+ let filesystem = Filesystem::new(Arc::new([("f", "x")].into_iter().collect::<Tmpfs>()))
360
+ .expect("the instance must stand");
361
+ let mount = Mount::new("/data", filesystem, Access::ReadWrite)
362
+ .expect("/data is a guest path")
363
+ .with_policy(policy.clone());
364
+ let cred = restrict_self(&prepare_creds_within(&[], None), [&mount]);
365
+ let opened = crate::vfs::kern_path::filp_open(
366
+ &cred,
367
+ &Path::root(mount.mnt()),
368
+ "f",
369
+ &OpenHow::default(),
370
+ );
371
+ assert_eq!(
372
+ (
373
+ opened.map(|file| file.is_path()),
374
+ policy.0.load(std::sync::atomic::Ordering::SeqCst)
375
+ ),
376
+ (Ok(true), 0),
377
+ "vfs::do_file_open of a file asking neither to read nor to write, beneath a Policy, \
378
+ must stand for the file without asking the Policy, as Linux's do_o_path asks no \
379
+ security module"
380
+ );
381
+ }
382
+
383
+ /// What making an entry answers through a directory moved past
384
+ /// `PATH_MAX`, beneath a mount `prepare` finishes.
385
+ fn made_past_the_limit(prepare: impl FnOnce(Mount) -> Mount) -> Result<(), FilesystemError> {
386
+ let (deep, seed) = (vec!["a"; 2000].join("/"), "x");
387
+ let tmpfs: Tmpfs = [
388
+ (format!("c0/{deep}/file"), seed),
389
+ (format!("c1/{deep}/file"), seed),
390
+ ]
391
+ .into_iter()
392
+ .collect();
393
+ let mount = Mount::new(
394
+ "/data",
395
+ Filesystem::new(Arc::new(tmpfs)).expect("the instance must stand"),
396
+ Access::ReadWrite,
397
+ )
398
+ .expect("/data is a guest path");
399
+ let mount = prepare(mount);
400
+ let cred = restrict_self(&prepare_creds_within(&[], None), [&mount]);
401
+ let root = Path::root(mount.mnt());
402
+ let how = crate::vfs::OpenHow {
403
+ read: true,
404
+ directory: true,
405
+ ..Default::default()
406
+ };
407
+ let held = crate::vfs::kern_path::filp_open(&cred, &root, &format!("c0/{deep}"), &how)
408
+ .unwrap_or_else(|_| panic!("the directory must open"));
409
+ crate::vfs::filename_renameat2(
410
+ &Arc::default(),
411
+ &root,
412
+ "c0",
413
+ &root,
414
+ &format!("c1/{deep}/z"),
415
+ )
416
+ .expect("the move must land");
417
+ crate::vfs::filename_mkdirat(&cred, held.path(), "made")
418
+ }
419
+
420
+ // Linux's walk sets no limit on where a directory stands, so a walk
421
+ // without a Policy has no reason to.
422
+ // @behavior RT-203
423
+ #[test]
424
+ fn a_path_past_the_limit_is_made_where_no_policy_needs_it() {
425
+ assert_eq!(
426
+ made_past_the_limit(|mount| mount),
427
+ Ok(()),
428
+ "vfs::filename_mkdirat through a directory moved past PATH_MAX, beneath a Mount with \
429
+ no Policy, must make the entry"
430
+ );
431
+ }
432
+
433
+ // @behavior RT-204
434
+ #[test]
435
+ fn a_path_a_policy_cannot_be_handed_is_too_long() {
436
+ let policy = Arc::new(Counts::default());
437
+ let asked = policy.clone();
438
+ let made = made_past_the_limit(move |mount| mount.with_policy(policy));
439
+ let asked_past_the_limit = asked.0.load(std::sync::atomic::Ordering::SeqCst);
440
+ assert_eq!(
441
+ (made, asked_past_the_limit),
442
+ (Err(FilesystemError::InvalidFilename), 1),
443
+ "vfs::filename_mkdirat through a directory moved past PATH_MAX, beneath a Policy, \
444
+ must answer ENAMETOOLONG, the Policy asked only of the open before the move"
445
+ );
446
+ }
447
+
448
+ /// Holds the first ask of `revalidate` about `d` once it reached the
449
+ /// Filesystem, so a move of `d` racing it either lands first or waits for
450
+ /// the answer.
451
+ struct PausesRevalidate(std::sync::Mutex<Option<std::sync::mpsc::Sender<()>>>);
452
+
453
+ impl Hook for PausesRevalidate {
454
+ fn revalidate(
455
+ &self,
456
+ ops: &dyn DentryOperations,
457
+ dir: &dyn InodeOperations,
458
+ dentry: &Dentry,
459
+ ) -> Result<bool, FilesystemError> {
460
+ let reached = match dentry.name() == "d" {
461
+ true => self.0.lock().expect("the sender").take(),
462
+ false => None,
463
+ };
464
+ if let Some(reached) = reached {
465
+ reached.send(()).expect("the test waits for the ask");
466
+ std::thread::sleep(std::time::Duration::from_millis(100));
467
+ }
468
+ ops.revalidate(dir, dentry)
469
+ }
470
+ }
471
+
472
+ /// One Filesystem with a Mount under an allow-all Policy holding `p/d` open,
473
+ /// and a Mount with none to move it through.
474
+ fn held_beneath_a_policy(
475
+ filesystem: Arc<Filesystem>,
476
+ ) -> (Arc<Cred>, crate::vfs::File, Mount, WalkGuard) {
477
+ let guarded = Mount::new("/guarded", filesystem.clone(), Access::ReadWrite)
478
+ .expect("/guarded is a guest path")
479
+ .with_policy(Arc::new(Counts::default()));
480
+ let plain =
481
+ Mount::new("/plain", filesystem, Access::ReadWrite).expect("/plain is a guest path");
482
+ let cred = restrict_self(&prepare_creds_within(&[], None), [&guarded]);
483
+ let how = crate::vfs::OpenHow {
484
+ read: true,
485
+ directory: true,
486
+ ..Default::default()
487
+ };
488
+ let held = crate::vfs::kern_path::filp_open(&cred, &Path::root(guarded.mnt()), "p/d", &how)
489
+ .unwrap_or_else(|_| panic!("the directory must open"));
490
+ (cred, held, plain, WalkGuard::new([&guarded]))
491
+ }
492
+
493
+ // The ask of `revalidate` and a move through the other Mount meet only if
494
+ // the walk holds no lock: then the Filesystem has moved the entry when it is
495
+ // asked, the runtime lets go of a dentry still in use, and the move finds
496
+ // nothing to carry along.
497
+ // @behavior RT-234
498
+ #[test]
499
+ fn a_directory_moved_while_a_call_starts_from_it_stays_reachable_beneath_a_policy() {
500
+ let tmpfs: Tmpfs = [("p/d/f", "x"), ("q/keep", "x")].into_iter().collect();
501
+ let (reached, asked) = std::sync::mpsc::channel();
502
+ let hook = Arc::new(PausesRevalidate(std::sync::Mutex::new(None)));
503
+ let filesystem = Filesystem::new(Arc::new(Wrap::new(Arc::new(tmpfs), hook.clone())))
504
+ .expect("the instance must stand");
505
+ let (cred, held, plain, guard) = held_beneath_a_policy(filesystem);
506
+ *hook.0.lock().expect("the sender") = Some(reached);
507
+ let start = held.path().clone();
508
+ let first = std::thread::scope(|scope| {
509
+ let first = scope.spawn(|| guard.check(&start));
510
+ asked
511
+ .recv_timeout(crate::sched::testing::MAX_WAIT)
512
+ .expect("the call must ask revalidate");
513
+ let root = Path::root(plain.mnt());
514
+ crate::vfs::filename_renameat2(
515
+ &restrict_self(&prepare_creds_within(&[], None), []),
516
+ &root,
517
+ "p/d",
518
+ &root,
519
+ "q/d",
520
+ )
521
+ .expect("the move must land");
522
+ crate::sched::testing::join_scoped("the call", first)
523
+ });
524
+ assert!(
525
+ first.is_ok()
526
+ && guard.check(held.path()).is_ok()
527
+ && crate::vfs::vfs_fstatat(&cred, held.path(), "f", false).is_ok()
528
+ && held.path().dentry().path_raw() == "/q/d",
529
+ "WalkGuard::check of a directory moved by another Mount while revalidate is asked, \
530
+ beneath a Policy, must let a call through both then and afterwards, at /q/d"
531
+ );
532
+ }
533
+
534
+ /// Moves `d` between `p` and `q` through `plain` each time the guarding thread
535
+ /// asks either whether it still stands, so every pass from the root meets a
536
+ /// move, until it has moved `MOVES` times.
537
+ struct MovesEachPass {
538
+ guarding: std::sync::OnceLock<std::thread::ThreadId>,
539
+ plain: std::sync::OnceLock<Path>,
540
+ moved: std::sync::atomic::AtomicUsize,
541
+ }
542
+
543
+ impl MovesEachPass {
544
+ const MOVES: usize = 100;
545
+ }
546
+
547
+ impl Hook for MovesEachPass {
548
+ fn revalidate(
549
+ &self,
550
+ ops: &dyn crate::vfs::DentryOperations,
551
+ dir: &dyn crate::filesystem::InodeOperations,
552
+ dentry: &Dentry,
553
+ ) -> Result<bool, FilesystemError> {
554
+ use std::sync::atomic::Ordering;
555
+ let guarding = self.guarding.get() == Some(&std::thread::current().id());
556
+ let from = dentry.name();
557
+ if guarding
558
+ && (from == "p" || from == "q")
559
+ && self.moved.load(Ordering::SeqCst) < Self::MOVES
560
+ {
561
+ let to = if from == "p" { "q" } else { "p" };
562
+ let root = self.plain.get().expect("the plain Mount stands").clone();
563
+ std::thread::spawn(move || {
564
+ crate::vfs::filename_renameat2(
565
+ &restrict_self(&prepare_creds_within(&[], None), []),
566
+ &root,
567
+ &format!("{from}/d"),
568
+ &root,
569
+ &format!("{to}/d"),
570
+ )
571
+ })
572
+ .join()
573
+ .expect("the move must not panic")
574
+ .expect("the move must land");
575
+ self.moved.fetch_add(1, Ordering::SeqCst);
576
+ }
577
+ ops.revalidate(dir, dentry)
578
+ }
579
+ }
580
+
581
+ // Each pass from the root meets a move another Mount makes, so a guard asking
582
+ // again would make the call wait while the moves go on; one pass answers,
583
+ // and the caller makes the call again, as a scoped walk's `..` does.
584
+ // @behavior RT-266
585
+ #[test]
586
+ fn a_start_moved_while_it_is_asked_answers_eagain_beneath_a_policy() {
587
+ use std::sync::atomic::Ordering;
588
+
589
+ let tmpfs: Tmpfs = [("p/d/f", "x"), ("q/keep", "x")].into_iter().collect();
590
+ let hook = Arc::new(MovesEachPass {
591
+ guarding: std::sync::OnceLock::new(),
592
+ plain: std::sync::OnceLock::new(),
593
+ moved: std::sync::atomic::AtomicUsize::new(0),
594
+ });
595
+ let filesystem = Filesystem::new(Arc::new(Wrap::new(Arc::new(tmpfs), hook.clone())))
596
+ .expect("the instance must stand");
597
+ let (_cred, held, plain, guard) = held_beneath_a_policy(filesystem);
598
+ hook.plain.set(Path::root(plain.mnt())).ok();
599
+ hook.guarding.set(std::thread::current().id()).ok();
600
+ assert_eq!(
601
+ (guard.check(held.path()), hook.moved.load(Ordering::SeqCst)),
602
+ (Err(FilesystemError::WouldBlock), 1),
603
+ "WalkGuard::check of a directory another Mount moves while it is asked, beneath a \
604
+ Policy, must answer EAGAIN from the pass that met the move"
605
+ );
606
+ }
607
+
608
+ mod stress {
609
+ use std::sync::atomic::{AtomicBool, Ordering};
610
+
611
+ use super::*;
612
+
613
+ // Nothing refuses a directory that stands, so a single refusal while it
614
+ // moves between two names is a dentry the walk let go of. A move met
615
+ // while the guard asks answers EAGAIN for the caller to ask again, which
616
+ // refuses nothing.
617
+ // @behavior RT-234
618
+ #[test]
619
+ #[ignore = "a stress witness; run by rake crates:stress"]
620
+ fn a_directory_moving_while_calls_start_from_it_stays_reachable_beneath_a_policy() {
621
+ let tmpfs: Tmpfs = [("p/d/f", "x"), ("q/keep", "x")].into_iter().collect();
622
+ let (cred, held, plain, guard) = held_beneath_a_policy(
623
+ Filesystem::new(Arc::new(tmpfs)).expect("the instance must stand"),
624
+ );
625
+ let stop = AtomicBool::new(false);
626
+ let start = held.path().clone();
627
+ let refused = std::thread::scope(|scope| {
628
+ scope.spawn(|| {
629
+ let (root, cred) = (
630
+ Path::root(plain.mnt()),
631
+ restrict_self(&prepare_creds_within(&[], None), []),
632
+ );
633
+ while !stop.load(Ordering::SeqCst) {
634
+ for (from, to) in [("p/d", "q/d"), ("q/d", "p/d")] {
635
+ crate::vfs::filename_renameat2(&cred, &root, from, &root, to)
636
+ .expect("the move must land");
637
+ }
638
+ }
639
+ });
640
+ let call = || {
641
+ guard.check(&start)?;
642
+ crate::vfs::vfs_fstatat(&cred, &start, "f", false)
643
+ };
644
+ let refused = (0..200_000)
645
+ .filter(|_| call().is_err_and(|error| error != FilesystemError::WouldBlock))
646
+ .count();
647
+ stop.store(true, Ordering::SeqCst);
648
+ refused
649
+ });
650
+ assert_eq!(
651
+ refused, 0,
652
+ "vfs::vfs_fstatat through a directory another Mount keeps moving, beneath a \
653
+ Policy, must always find its file"
654
+ );
655
+ }
656
+ }
657
+
658
+ // A Bind refuses these itself, so only asking the rule directly shows it
659
+ // holds for a Filesystem that would open them.
660
+ // @behavior RT-197
661
+ #[test]
662
+ fn only_a_file_or_a_directory_opens() {
663
+ let mount = Mount::new(
664
+ "/data",
665
+ crate::filesystem::Filesystem::new(Arc::new(crate::filesystem::Tmpfs::new()))
666
+ .expect("the instance must stand"),
667
+ Access::Read,
668
+ )
669
+ .expect("/data is a guest path");
670
+ let judged = |kind| {
671
+ let path = SecurityPath {
672
+ mnt: mount.mnt(),
673
+ name: &|| "/".to_owned(),
674
+ };
675
+ KobakoSecurity
676
+ .file_open(path, kind, FMode::READ, &OpenHow::default())
677
+ .is_ok()
678
+ };
679
+ assert_eq!(
680
+ [
681
+ FileType::RegFile,
682
+ FileType::Dir,
683
+ FileType::Symlink,
684
+ FileType::Fifo,
685
+ FileType::Sock,
686
+ FileType::Blkdev,
687
+ FileType::Chrdev,
688
+ FileType::Unknown,
689
+ ]
690
+ .map(judged),
691
+ [true, true, false, false, false, false, false, false],
692
+ "KobakoSecurity::file_open must pass a file and a directory, and refuse every other \
693
+ kind"
694
+ );
695
+ }
696
+ }