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,105 @@
1
+ //! A Mount: the Grant handing a Filesystem to the guest at an absolute
2
+ //! guest path, with the access it allows there and the Policy that may refuse
3
+ //! more, which the VFS asks through its Cred, so a Filesystem of a third
4
+ //! party's checks nothing.
5
+
6
+ use std::fmt;
7
+ use std::sync::Arc;
8
+
9
+ use crate::filesystem::Filesystem;
10
+ use crate::vfs::VfsMount;
11
+
12
+ /// What a Mount allows a guest beneath it, and what a Policy is asked to allow
13
+ /// for one path.
14
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
15
+ pub enum Access {
16
+ Read,
17
+ ReadWrite,
18
+ }
19
+
20
+ /// The Host App's decision for one path a guest opens or changes, made after
21
+ /// a Mount's access allows it, so it can only refuse more.
22
+ pub trait Policy: Send + Sync {
23
+ /// Whether the guest may reach `path`, the absolute guest path, with
24
+ /// `access`.
25
+ fn allows(&self, path: &str, access: Access) -> bool;
26
+ }
27
+
28
+ /// A Filesystem instance at an absolute guest path. Its path bounds what a
29
+ /// Policy can allow either way.
30
+ #[derive(Clone)]
31
+ pub struct Mount {
32
+ at: String,
33
+ policy: Option<Arc<dyn Policy>>,
34
+ mnt: Arc<VfsMount>,
35
+ }
36
+
37
+ /// A path no Mount can stand at: one that is not an absolute guest path.
38
+ #[derive(Clone, Debug, PartialEq, Eq)]
39
+ pub struct MountPathError(String);
40
+
41
+ impl fmt::Display for MountPathError {
42
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43
+ write!(
44
+ f,
45
+ "mount path must be an absolute guest path, got {:?}",
46
+ self.0
47
+ )
48
+ }
49
+ }
50
+
51
+ impl std::error::Error for MountPathError {}
52
+
53
+ /// An absolute guest path has no empty, `.`, or `..` component; the root
54
+ /// is `/` alone.
55
+ fn is_guest_path(at: &str) -> bool {
56
+ at == "/"
57
+ || at.strip_prefix('/').is_some_and(|rest| {
58
+ rest.split('/')
59
+ .all(|part| !part.is_empty() && part != "." && part != "..")
60
+ })
61
+ }
62
+
63
+ impl Mount {
64
+ /// A Mount of `filesystem` at `at`, refused unless `at` is an absolute
65
+ /// guest path, which every path the Mount answers for is built on.
66
+ pub fn new(
67
+ at: impl Into<String>,
68
+ filesystem: Arc<Filesystem>,
69
+ access: Access,
70
+ ) -> Result<Self, MountPathError> {
71
+ let at = at.into();
72
+ if !is_guest_path(&at) {
73
+ return Err(MountPathError(at));
74
+ }
75
+ Ok(Self {
76
+ at,
77
+ mnt: VfsMount::new(filesystem, access == Access::Read),
78
+ policy: None,
79
+ })
80
+ }
81
+
82
+ /// This Mount with `policy` also judging each open and each change its
83
+ /// access allows; it can only refuse more, as an LSM can.
84
+ pub fn with_policy(self, policy: Arc<dyn Policy>) -> Self {
85
+ Self {
86
+ policy: Some(policy),
87
+ ..self
88
+ }
89
+ }
90
+
91
+ /// The Filesystem as the VFS walks it.
92
+ pub fn mnt(&self) -> &Arc<VfsMount> {
93
+ &self.mnt
94
+ }
95
+
96
+ /// The Policy deciding beneath this Mount, once one was given.
97
+ pub(crate) fn policy(&self) -> Option<&Arc<dyn Policy>> {
98
+ self.policy.as_ref()
99
+ }
100
+
101
+ /// The absolute guest path the Filesystem appears at.
102
+ pub fn at(&self) -> &str {
103
+ &self.at
104
+ }
105
+ }
@@ -6,13 +6,20 @@
6
6
  //! actually built, and the frontend refuses a declaration below the
7
7
  //! request — so the request is also the floor.
8
8
 
9
+ use std::hash::{BuildHasher, RandomState};
10
+ use std::sync::LazyLock;
11
+ use std::time::{SystemTime, UNIX_EPOCH};
12
+
13
+ use crate::vfs::{FileType, Identity, Kstat};
14
+
9
15
  /// The ordered isolation ladder a runtime builds one rung of.
10
16
  ///
11
17
  /// `Hermetic` is the full ambient-denial posture: ambient time and
12
- /// entropy denied at the WASI boundary, no filesystem / environment /
13
- /// network reachability, and no host import beyond the wire ABI's
14
- /// `__kobako_upcall`. `Permissive` differs in exactly one grant —
15
- /// live ambient time and entropy at the WASI boundary. Ordering
18
+ /// entropy denied at the WASI boundary, no ambient filesystem /
19
+ /// environment / network reachability, and no host import beyond the wire ABI's
20
+ /// `__kobako_upcall`. `Permissive` differs only in the ambient
21
+ /// authority it grants — live time and entropy at the WASI boundary — and in
22
+ /// the times and inode numbers it shows of an entry. Ordering
16
23
  /// follows strength (`Permissive < Hermetic`), so a floor check is a
17
24
  /// plain comparison.
18
25
  ///
@@ -29,10 +36,139 @@ pub enum Profile {
29
36
  Hermetic,
30
37
  }
31
38
 
39
+ impl Profile {
40
+ /// The inode number a guest on this posture sees for `identity`. A
41
+ /// hermetic guest sees none, as a Filesystem's device number depends on
42
+ /// how many were made before it in the process. Any other sees a hash of
43
+ /// it under a key the process draws once, so it can tell entries apart
44
+ /// and cannot work back to a host's inode number.
45
+ pub fn guest_ino(self, identity: Option<Identity>) -> u64 {
46
+ static KEY: LazyLock<RandomState> = LazyLock::new(RandomState::new);
47
+ match (self, identity) {
48
+ (Self::Hermetic, _) | (_, None) => 0,
49
+ (Self::Permissive, Some(identity)) => KEY.hash_one(identity),
50
+ }
51
+ }
52
+
53
+ /// `stat` as a guest on this posture sees it, whichever ABI a Wasm
54
+ /// Driver encodes it into. A Filesystem records the host's real time whatever
55
+ /// the invocation writing to it runs under, so one shared across
56
+ /// invocations keeps one record; a hermetic guest sees every time at the
57
+ /// Unix epoch, where its wall clock stands, so a time a write left behind
58
+ /// is no clock it can read. A link reports no size, so nothing of where it
59
+ /// points reaches the guest, and an entry is told apart only by what
60
+ /// `guest_ino` answers.
61
+ pub fn guest_kstat(self, stat: Kstat) -> Kstat {
62
+ let ino = self.guest_ino(Identity::of(&stat));
63
+ let masked = match self {
64
+ Self::Permissive => stat,
65
+ Self::Hermetic => {
66
+ let epoch = |time: Option<SystemTime>| time.map(|_| UNIX_EPOCH);
67
+ stat.with_times(epoch(stat.atime), epoch(stat.mtime), epoch(stat.ctime))
68
+ }
69
+ };
70
+ Kstat {
71
+ size: match masked.file_type {
72
+ FileType::Symlink => 0,
73
+ _ => masked.size,
74
+ },
75
+ ..masked.with_identity(0, ino)
76
+ }
77
+ }
78
+ }
79
+
32
80
  #[cfg(test)]
33
81
  mod tests {
34
82
  use super::*;
35
83
 
84
+ // The Wasm Driver only encodes this, so another one shows the same, and a
85
+ // guest reads a link's size and a time here or nowhere.
86
+ // @behavior RT-173 RT-183
87
+ #[test]
88
+ fn a_guest_sees_no_size_for_a_link_and_only_what_its_posture_allows() {
89
+ let reported = Kstat::new(FileType::Symlink, 42)
90
+ .with_identity(7, 9)
91
+ .with_times(Some(SystemTime::now()), None, None);
92
+ let (hermetic, permissive) = (
93
+ Profile::Hermetic.guest_kstat(reported),
94
+ Profile::Permissive.guest_kstat(reported),
95
+ );
96
+ assert_eq!(
97
+ (hermetic.size, permissive.size, hermetic.ino, hermetic.atime),
98
+ (0, 0, 0, Some(UNIX_EPOCH)),
99
+ "Profile::guest_kstat of a link must report no size on any posture, and at the \
100
+ hermetic rung no inode number and every time at the Unix epoch"
101
+ );
102
+ assert!(
103
+ permissive.ino != 0 && permissive.ino != 9 && permissive.atime == reported.atime,
104
+ "Profile::guest_kstat at the permissive rung must hash the inode number and keep \
105
+ the time"
106
+ );
107
+ }
108
+
109
+ // A device number depends on how many Filesystems the process made before,
110
+ // so the inode number a guest reads folds it in and the device stays out.
111
+ // @behavior RT-283
112
+ #[test]
113
+ fn a_guest_sees_one_device_for_every_entry() {
114
+ let reported = Kstat::new(FileType::RegFile, 1).with_identity(7, 9);
115
+ assert_eq!(
116
+ [Profile::Hermetic, Profile::Permissive]
117
+ .map(|profile| profile.guest_kstat(reported).dev),
118
+ [0, 0],
119
+ "Profile::guest_kstat of an entry a Filesystem reports on device 7 must report \
120
+ device 0 on either posture"
121
+ );
122
+ }
123
+
124
+ // An unkeyed hash answers what anyone can compute from a host's numbers,
125
+ // so the hash a guest reads must differ from it.
126
+ // @behavior RT-194
127
+ #[test]
128
+ fn a_guest_cannot_compute_the_inode_number_it_reads() {
129
+ use std::collections::hash_map::DefaultHasher;
130
+ use std::hash::{Hash, Hasher};
131
+
132
+ let reported = Kstat::new(FileType::RegFile, 0).with_identity(7, 9);
133
+ let identity = Identity::of(&reported);
134
+ let mut unkeyed = DefaultHasher::new();
135
+ identity
136
+ .expect("an inode number names an identity")
137
+ .hash(&mut unkeyed);
138
+ let seen = Profile::Permissive.guest_ino(identity);
139
+ assert_eq!(
140
+ seen,
141
+ Profile::Permissive.guest_ino(identity),
142
+ "Profile::guest_ino of one identity must answer one number within a process"
143
+ );
144
+ assert_ne!(
145
+ seen,
146
+ unkeyed.finish(),
147
+ "Profile::guest_ino at the permissive rung must not answer a hash anyone can compute \
148
+ from the device and inode numbers"
149
+ );
150
+ }
151
+
152
+ // A guest reads the count on any ABI, so the count starts in the runtime
153
+ // rather than in what a Wasm Driver encodes.
154
+ // @behavior RT-193
155
+ #[test]
156
+ fn an_entry_whose_filesystem_keeps_no_count_is_named_once() {
157
+ let reported = Kstat::new(FileType::RegFile, 0);
158
+ assert_eq!(
159
+ (
160
+ Profile::Hermetic.guest_kstat(reported).nlink,
161
+ Profile::Permissive.guest_kstat(reported).nlink,
162
+ Profile::Permissive
163
+ .guest_kstat(reported.with_nlink(3))
164
+ .nlink,
165
+ ),
166
+ (1, 1, 3),
167
+ "Profile::guest_kstat of an entry reporting no count must show one name on each \
168
+ posture, and of one reporting three must show three"
169
+ );
170
+ }
171
+
36
172
  // @behavior RT-014
37
173
  #[test]
38
174
  fn a_declaration_satisfies_any_floor_at_or_below_it() {
@@ -13,6 +13,7 @@ use kobako_transport::envelope::Preamble;
13
13
 
14
14
  use crate::dispatch::DispatchHandler;
15
15
  use crate::error::InvokeError;
16
+ use crate::grant::{Grant, GrantKind};
16
17
  use crate::profile::Profile;
17
18
  use crate::snapshot::Snapshot;
18
19
 
@@ -49,13 +50,24 @@ pub enum Entry<'a> {
49
50
  /// wrapper and GC-marking it; the runtime itself touches no frontend
50
51
  /// value.
51
52
  pub trait Runtime {
53
+ /// `grants` are this Invocation's, each of a kind `supports` declared;
54
+ /// the runtime presents them to the guest and keeps none afterwards.
52
55
  fn invoke(
53
56
  &self,
54
57
  entry: Entry<'_>,
55
58
  preamble: &Preamble,
56
59
  handler: Option<Arc<dyn DispatchHandler>>,
60
+ grants: &[Grant],
57
61
  ) -> Result<Snapshot, InvokeError>;
58
62
 
63
+ /// Whether this runtime presents a Grant of `kind` to the guest. A
64
+ /// frontend refuses to register one it does not, before any guest runs.
65
+ /// The default supports none, so an engine that has not learned Grants
66
+ /// still serves every invocation without one.
67
+ fn supports(&self, _kind: GrantKind) -> bool {
68
+ false
69
+ }
70
+
59
71
  /// The isolation profile this runtime provides. Deliberately
60
72
  /// without a default: every engine states its posture explicitly,
61
73
  /// and a frontend refuses construction when the declaration falls
@@ -0,0 +1,210 @@
1
+ //! Who is running and how it waits, beneath every lock, as Linux's
2
+ //! `kernel/sched` is: a lock asks only for its wait to happen, so nothing
3
+ //! above it knows what waiting means to the frontend. A run past its deadline,
4
+ //! or one its frontend interrupted, stops waiting, as a fatal signal ends
5
+ //! Linux's killable waits, so one Sandbox holding a lock never holds another's
6
+ //! thread past that one's own end.
7
+
8
+ use std::cell::RefCell;
9
+ use std::sync::atomic::{AtomicBool, Ordering};
10
+ use std::sync::{Arc, OnceLock};
11
+ use std::thread::{self, ThreadId};
12
+ use std::time::{Duration, Instant};
13
+
14
+ /// How a thread waits for a lock another thread holds. A frontend whose
15
+ /// threads share a lock of their own, as Ruby's threads share the GVL,
16
+ /// gives that lock up for the wait, or a holder that needs it to finish
17
+ /// never could.
18
+ pub trait Blocking: Send + Sync {
19
+ /// Run `wait`, which returns once the lock is taken.
20
+ fn wait(&self, wait: &mut dyn FnMut());
21
+ }
22
+
23
+ static BLOCKING: OnceLock<Box<dyn Blocking>> = OnceLock::new();
24
+
25
+ /// Have every thread of this process wait through `blocking`, as a frontend
26
+ /// says once at startup; a later one is handed back, as
27
+ /// `register_filesystem` refuses a second. Without one a thread just waits.
28
+ pub fn register_blocking(blocking: Box<dyn Blocking>) -> Result<(), Box<dyn Blocking>> {
29
+ BLOCKING.set(blocking)
30
+ }
31
+
32
+ /// Run `wait` as this process's threads wait, as a lock's slow path calls
33
+ /// `schedule`.
34
+ pub(crate) fn schedule(wait: &mut dyn FnMut()) {
35
+ match BLOCKING.get() {
36
+ Some(blocking) => blocking.wait(wait),
37
+ None => wait(),
38
+ }
39
+ }
40
+
41
+ thread_local! {
42
+ static CURRENT: ThreadId = thread::current().id();
43
+ }
44
+
45
+ /// Who is running, as Linux's `current`.
46
+ pub(crate) fn current() -> ThreadId {
47
+ CURRENT.with(|current| *current)
48
+ }
49
+
50
+ /// A thread as its frontend may interrupt it from another thread, as a signal
51
+ /// reaches a Linux task.
52
+ #[derive(Clone, Default)]
53
+ pub struct Task {
54
+ interrupted: Arc<AtomicBool>,
55
+ }
56
+
57
+ impl Task {
58
+ /// The calling thread.
59
+ pub fn current() -> Self {
60
+ TASK.with(|task| task.borrow().task.clone())
61
+ }
62
+
63
+ /// Have every wait of the run this thread carries out stop, as a fatal
64
+ /// signal ends a killable wait; it holds until that run ends.
65
+ pub fn interrupt(&self) {
66
+ self.interrupted.store(true, Ordering::SeqCst);
67
+ }
68
+ }
69
+
70
+ /// The task this thread carries out, as Linux's `current`, and when its run is
71
+ /// to stop.
72
+ #[derive(Default)]
73
+ struct Current {
74
+ task: Task,
75
+ deadline: Option<Instant>,
76
+ /// How many runs the thread is inside, one within another where a
77
+ /// Filesystem's own call starts another.
78
+ depth: usize,
79
+ }
80
+
81
+ thread_local! {
82
+ static TASK: RefCell<Current> = RefCell::default();
83
+ }
84
+
85
+ /// Run `run` as a run that stops waiting once `deadline` passes or its thread
86
+ /// is interrupted, as a Linux task stops a killable wait on a fatal signal. A
87
+ /// run within another keeps the outer one's interrupt.
88
+ pub fn run_task<T>(deadline: Option<Instant>, run: impl FnOnce() -> T) -> T {
89
+ /// Puts back the deadline of the run around this one, however `run` ends,
90
+ /// and lets an interrupt go once no run is left.
91
+ struct Outer(Option<Instant>);
92
+
93
+ impl Drop for Outer {
94
+ fn drop(&mut self) {
95
+ TASK.with(|task| {
96
+ let mut running = task.borrow_mut();
97
+ running.deadline = self.0;
98
+ running.depth -= 1;
99
+ if running.depth == 0 {
100
+ running.task.interrupted.store(false, Ordering::SeqCst);
101
+ }
102
+ });
103
+ }
104
+ }
105
+
106
+ let _outer = Outer(TASK.with(|task| {
107
+ let mut running = task.borrow_mut();
108
+ running.depth += 1;
109
+ std::mem::replace(&mut running.deadline, deadline)
110
+ }));
111
+ run()
112
+ }
113
+
114
+ /// Whether the calling thread's run is to stop waiting, as Linux's
115
+ /// `fatal_signal_pending(current)` answers.
116
+ pub(crate) fn fatal_signal_pending() -> bool {
117
+ TASK.with(|task| {
118
+ let running = task.borrow();
119
+ running.task.interrupted.load(Ordering::SeqCst)
120
+ || running.deadline.is_some_and(|at| Instant::now() >= at)
121
+ })
122
+ }
123
+
124
+ /// The most a wait sleeps before it looks again whether its run is to stop.
125
+ const MAX_SLEEP: Duration = Duration::from_millis(10);
126
+
127
+ /// How long a wait sleeps before it looks again whether its run is to stop:
128
+ /// never past its deadline.
129
+ pub(crate) fn sleep_slice() -> Duration {
130
+ TASK.with(|task| match task.borrow().deadline {
131
+ Some(at) => at.saturating_duration_since(Instant::now()).min(MAX_SLEEP),
132
+ None => MAX_SLEEP,
133
+ })
134
+ }
135
+
136
+ /// A process has one Blocking, so a test counts only the waits of the thread
137
+ /// that asked to.
138
+ #[cfg(test)]
139
+ pub(crate) mod testing {
140
+ use std::cell::RefCell;
141
+ use std::sync::atomic::{AtomicUsize, Ordering};
142
+ use std::sync::Arc;
143
+ use std::time::{Duration, Instant};
144
+
145
+ /// The longest a test waits for another thread, so what never arrives fails
146
+ /// the test instead of hanging the suite.
147
+ pub(crate) const MAX_WAIT: Duration = Duration::from_secs(10);
148
+
149
+ /// Wait until `arrived`, failing the test with `what` past `MAX_WAIT`.
150
+ pub(crate) fn wait_until(what: &str, mut arrived: impl FnMut() -> bool) {
151
+ let started = Instant::now();
152
+ while !arrived() {
153
+ assert!(
154
+ started.elapsed() < MAX_WAIT,
155
+ "{what} did not happen within {MAX_WAIT:?}"
156
+ );
157
+ std::thread::yield_now();
158
+ }
159
+ }
160
+
161
+ /// What the thread `handle` runs answers, failing the test with `what` past
162
+ /// `MAX_WAIT`, so a thread stuck on a lock fails rather than hangs.
163
+ pub(crate) fn join<T>(what: &str, handle: std::thread::JoinHandle<T>) -> T {
164
+ wait_until(what, || handle.is_finished());
165
+ handle
166
+ .join()
167
+ .unwrap_or_else(|_| panic!("{what} must not panic"))
168
+ }
169
+
170
+ /// What the scoped thread `handle` runs answers, as `join` waits for one.
171
+ pub(crate) fn join_scoped<T>(what: &str, handle: std::thread::ScopedJoinHandle<'_, T>) -> T {
172
+ wait_until(what, || handle.is_finished());
173
+ handle
174
+ .join()
175
+ .unwrap_or_else(|_| panic!("{what} must not panic"))
176
+ }
177
+
178
+ /// What `run` answers, each wait for a lock in it ending past `MAX_WAIT`, so a
179
+ /// test thread waiting on a lock itself fails rather than hangs.
180
+ pub(crate) fn within_max_wait<T>(run: impl FnOnce() -> T) -> T {
181
+ super::run_task(Some(Instant::now() + MAX_WAIT), run)
182
+ }
183
+
184
+ use super::{Blocking, BLOCKING};
185
+
186
+ thread_local! {
187
+ static COUNTS: RefCell<Option<Arc<AtomicUsize>>> = const { RefCell::new(None) };
188
+ }
189
+
190
+ struct CountsWaits;
191
+
192
+ impl Blocking for CountsWaits {
193
+ fn wait(&self, wait: &mut dyn FnMut()) {
194
+ COUNTS.with(|counts| {
195
+ if let Some(counts) = counts.borrow().as_ref() {
196
+ counts.fetch_add(1, Ordering::SeqCst);
197
+ }
198
+ });
199
+ wait();
200
+ }
201
+ }
202
+
203
+ /// Counts each wait this thread makes through the registered Blocking.
204
+ pub(crate) fn count_waits() -> Arc<AtomicUsize> {
205
+ BLOCKING.get_or_init(|| Box::new(CountsWaits));
206
+ let counts = Arc::<AtomicUsize>::default();
207
+ COUNTS.with(|held| *held.borrow_mut() = Some(counts.clone()));
208
+ counts
209
+ }
210
+ }