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
@@ -7,12 +7,12 @@
7
7
  //! pure function over the error's downcast chain so it can be exercised
8
8
  //! from `cargo test` without any frontend.
9
9
 
10
- use std::time::Instant;
10
+ use std::time::{Duration, Instant};
11
11
 
12
- use wasmtime::{StoreContextMut, UpdateDeadline};
12
+ use wasmtime::{Store, StoreHookState, UpdateDeadline};
13
13
 
14
14
  use crate::invocation::Invocation;
15
- use crate::limiter::MemoryLimitTrap;
15
+ use kobako_runtime::endpoint::MemoryLimitExceeded;
16
16
  use kobako_runtime::error::{SetupError, Trap};
17
17
 
18
18
  /// Marker error returned from the epoch-deadline callback when the
@@ -41,7 +41,7 @@ pub(crate) const NO_TIMEOUT_EPOCH_DELTA: u64 = u64::MAX / 2;
41
41
  /// firing; returning the same long extension keeps it inert as a defence
42
42
  /// in depth.
43
43
  pub(crate) fn epoch_deadline_callback(
44
- ctx: StoreContextMut<'_, Invocation>,
44
+ ctx: StoreHookState<'_, Invocation>,
45
45
  ) -> wasmtime::Result<UpdateDeadline> {
46
46
  match ctx.data().deadline() {
47
47
  Some(deadline) if Instant::now() >= deadline => Err(wasmtime::Error::new(TimeoutTrap)),
@@ -53,13 +53,33 @@ pub(crate) fn epoch_deadline_callback(
53
53
  /// The message leaves out the ABI export symbol, so the Sandbox layer can
54
54
  /// attach the caller's verb instead. A cap trap carries its own message,
55
55
  /// since wasmtime's backtrace framing is noise there.
56
- pub(crate) fn trap_from(err: wasmtime::Error) -> Trap {
56
+ pub(crate) fn to_trap(err: wasmtime::Error) -> Trap {
57
+ cap_trap(&err).unwrap_or_else(|| Trap::Other(other_trap_message(&err)))
58
+ }
59
+
60
+ /// Arm a Store's wall-clock deadline `limit` from now, or none. Epoch
61
+ /// interruption is on engine-wide and a fresh Store's deadline has already
62
+ /// elapsed, so an unbounded Store names one it cannot reach.
63
+ pub(crate) fn arm_deadline(store: &mut Store<Invocation>, limit: Option<Duration>) {
64
+ store.epoch_deadline_callback(epoch_deadline_callback);
65
+ store
66
+ .data_mut()
67
+ .set_deadline(limit.map(|limit| Instant::now() + limit));
68
+ store.set_epoch_deadline(if limit.is_some() {
69
+ 1
70
+ } else {
71
+ NO_TIMEOUT_EPOCH_DELTA
72
+ });
73
+ }
74
+
75
+ /// The cap trap `err` carries, if a cap fired: a setup step that fails
76
+ /// for any other reason words its own message instead.
77
+ pub(crate) fn cap_trap(err: &wasmtime::Error) -> Option<Trap> {
57
78
  if let Some(t) = err.downcast_ref::<TimeoutTrap>() {
58
- Trap::Timeout(t.to_string())
59
- } else if let Some(t) = err.downcast_ref::<MemoryLimitTrap>() {
60
- Trap::MemoryLimit(t.to_string())
79
+ Some(Trap::Timeout(t.to_string()))
61
80
  } else {
62
- Trap::Other(other_trap_message(&err))
81
+ err.downcast_ref::<MemoryLimitExceeded>()
82
+ .map(|t| Trap::MemoryLimit(t.to_string()))
63
83
  }
64
84
  }
65
85
 
@@ -76,23 +96,23 @@ fn other_trap_message(err: &wasmtime::Error) -> String {
76
96
  }
77
97
  }
78
98
 
79
- /// The template is built before any invocation, so neither cap can fire:
80
- /// the memory cap is not yet armed and the probe Store's epoch deadline is
81
- /// out of reach. Every failure here is a setup fault, not a trap.
99
+ /// The template is built before any invocation, so no invocation cap can
100
+ /// fire: the memory cap is not yet armed, and the probe's own setup
101
+ /// deadline is told apart where it fires. Every failure here is a setup
102
+ /// fault, not a trap.
82
103
  pub(crate) fn instantiate_err(err: wasmtime::Error) -> SetupError {
83
104
  SetupError::Dead(format!("instantiate: {err}"))
84
105
  }
85
106
 
86
107
  #[cfg(test)]
87
108
  mod tests {
88
- use super::{other_trap_message, trap_from, TimeoutTrap, NO_TIMEOUT_EPOCH_DELTA};
89
- use crate::invocation::Invocation;
90
- use crate::limiter::MemoryLimitTrap;
109
+ use super::{other_trap_message, to_trap, TimeoutTrap, NO_TIMEOUT_EPOCH_DELTA};
110
+ use kobako_runtime::endpoint::MemoryLimitExceeded;
91
111
  use kobako_runtime::error::Trap;
92
112
 
93
113
  // The no-timeout priming delta is added to the engine's current
94
114
  // epoch inside wasmtime, and the process-wide ticker advances that
95
- // epoch from the first `shared_engine` call on — so the sum must
115
+ // epoch from the first `load_engine` call on — so the sum must
96
116
  // stay in range for a long-lived engine, not just a fresh one.
97
117
  // `increment_epoch` stands in for the ticker to make the ticked
98
118
  // state deterministic; under debug overflow checks an overflowing
@@ -100,34 +120,34 @@ mod tests {
100
120
  // @behavior S-119
101
121
  #[test]
102
122
  fn no_timeout_delta_survives_a_ticked_engine_epoch() {
103
- let engine = crate::cache::shared_engine().expect("shared engine must be constructible");
123
+ let engine = crate::cache::load_engine().expect("shared engine must be constructible");
104
124
  engine.increment_epoch();
105
- let mut store = wasmtime::Store::new(engine, Invocation::new(None));
125
+ let mut store = wasmtime::Store::new(engine, ());
106
126
  store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
107
127
  }
108
128
 
109
129
  // @behavior OC-032
110
130
  #[test]
111
- fn trap_from_routes_timeout_trap_to_timeout() {
131
+ fn to_trap_routes_timeout_trap_to_timeout() {
112
132
  let err = wasmtime::Error::new(TimeoutTrap);
113
133
  let expected = TimeoutTrap.to_string();
114
- assert!(matches!(trap_from(err), Trap::Timeout(msg) if msg == expected));
134
+ assert!(matches!(to_trap(err), Trap::Timeout(msg) if msg == expected));
115
135
  }
116
136
 
117
137
  // @behavior OC-033
118
138
  #[test]
119
- fn trap_from_routes_memory_limit_trap_to_memory_limit() {
120
- let trap = MemoryLimitTrap::new(1 << 20, 1 << 19);
139
+ fn to_trap_routes_memory_limit_exceeded_to_memory_limit() {
140
+ let trap = MemoryLimitExceeded::new(1 << 20, 1 << 19);
121
141
  let expected = trap.to_string();
122
142
  let err = wasmtime::Error::new(trap);
123
- assert!(matches!(trap_from(err), Trap::MemoryLimit(msg) if msg == expected));
143
+ assert!(matches!(to_trap(err), Trap::MemoryLimit(msg) if msg == expected));
124
144
  }
125
145
 
126
146
  // @behavior OC-028
127
147
  #[test]
128
- fn trap_from_falls_back_to_other_for_unknown_errors() {
148
+ fn to_trap_falls_back_to_other_for_unknown_errors() {
129
149
  let err = wasmtime::Error::msg("some other wasmtime fault");
130
- assert!(matches!(trap_from(err), Trap::Other(_)));
150
+ assert!(matches!(to_trap(err), Trap::Other(_)));
131
151
  }
132
152
 
133
153
  // A guest hard trap reaches the host as a wasmtime error whose Display is
@@ -0,0 +1,333 @@
1
+ //! The guest's WASI context for one instance: the Preamble on stdin,
2
+ //! a capture pipe for each of stdout and stderr, the ambient authority
3
+ //! the instance's purpose grants, and a preopen for each Mount an
4
+ //! invocation was handed.
5
+
6
+ use wasmtime_wasi::p1::WasiP1Ctx;
7
+ use wasmtime_wasi::WasiCtxBuilder;
8
+
9
+ use crate::ambient::{self, Clocks};
10
+ use crate::config::Config;
11
+ use crate::descriptors::Descriptors;
12
+ use kobako_runtime::endpoint;
13
+ use kobako_runtime::error::Trap;
14
+ use std::sync::Arc;
15
+
16
+ use kobako_runtime::filesystem::{create_pipe_files, PipeInodeInfo};
17
+ use kobako_runtime::grant::Grant;
18
+ use kobako_runtime::mount::Mount;
19
+ use kobako_runtime::profile::Profile;
20
+ use kobako_runtime::security::prepare_creds_within;
21
+ use kobako_runtime::vfs::Cred;
22
+
23
+ /// The Cred an invocation's Mounts open as: kobako's own rules, then each
24
+ /// module the Driver was built with, its Filesystems holding at most the
25
+ /// memory the Driver allots, as Linux's `prepare_creds` gives a task the
26
+ /// LSMs it booted with.
27
+ fn prepare_creds(config: &Config) -> Arc<Cred> {
28
+ prepare_creds_within(&config.security, config.memory_limit)
29
+ }
30
+
31
+ /// One instance's WASI context: its descriptor table, the clocks its
32
+ /// waits pass on, and the capture pipes stdout and stderr write into, so
33
+ /// the captures are read back from the same owner that wired them.
34
+ pub(crate) struct Wasi {
35
+ ctx: WasiP1Ctx,
36
+ descriptors: Descriptors,
37
+ clocks: Clocks,
38
+ stdout: Arc<PipeInodeInfo>,
39
+ stderr: Arc<PipeInodeInfo>,
40
+ }
41
+
42
+ impl Wasi {
43
+ /// An invocation's context, granting the posture the Sandbox requested
44
+ /// and presenting each of the invocation's Mounts as a preopen, from
45
+ /// descriptor 3 in order.
46
+ pub(crate) fn for_invocation(
47
+ config: &Config,
48
+ stdin: Vec<u8>,
49
+ grants: &[Grant],
50
+ ) -> Result<Self, Trap> {
51
+ Self::new(
52
+ stdin,
53
+ config.stdout_limit,
54
+ config.stderr_limit,
55
+ config.open_file_limit,
56
+ prepare_creds(config),
57
+ config.profile,
58
+ grants.iter().filter_map(to_mount).collect(),
59
+ )
60
+ }
61
+
62
+ /// A compile's context, at the strongest posture whatever the Sandbox
63
+ /// requested: compiling the Snippets needs no ambient authority, so
64
+ /// it is granted none.
65
+ pub(crate) fn for_compile(config: &Config, stdin: Vec<u8>) -> Result<Self, Trap> {
66
+ Self::new(
67
+ stdin,
68
+ config.stdout_limit,
69
+ config.stderr_limit,
70
+ config.open_file_limit,
71
+ prepare_creds(config),
72
+ Profile::Hermetic,
73
+ Vec::new(),
74
+ )
75
+ }
76
+
77
+ /// The ABI version probe's context. Instantiating it runs the
78
+ /// artifact's start section, yet reading a version needs no input, no
79
+ /// ambient authority, and no output: the probe takes an empty stdin, the
80
+ /// strongest posture, and pipes sized to keep nothing.
81
+ pub(crate) fn for_probe() -> Result<Self, Trap> {
82
+ Self::new(
83
+ Vec::new(),
84
+ Some(0),
85
+ Some(0),
86
+ None,
87
+ prepare_creds_within(&[], None),
88
+ Profile::Hermetic,
89
+ Vec::new(),
90
+ )
91
+ }
92
+
93
+ /// An uncapped output channel has no ceiling on the host: a guest
94
+ /// rewriting one buffer grows the pipe, not its own memory.
95
+ fn new(
96
+ stdin: Vec<u8>,
97
+ stdout_limit: Option<usize>,
98
+ stderr_limit: Option<usize>,
99
+ open_file_limit: Option<usize>,
100
+ opener: Arc<Cred>,
101
+ profile: Profile,
102
+ mounts: Vec<Mount>,
103
+ ) -> Result<Self, Trap> {
104
+ let stdout = PipeInodeInfo::new(endpoint::pipe_capacity(stdout_limit));
105
+ let stderr = PipeInodeInfo::new(endpoint::pipe_capacity(stderr_limit));
106
+
107
+ // stdio lives in kobako's descriptor table, which answers every
108
+ // call that reads a descriptor (`crate::preview1`); the builder
109
+ // keeps only what the remaining imports read.
110
+ let descriptors = Descriptors::new(
111
+ create_pipe_files(&PipeInodeInfo::from_bytes(stdin), &opener).0,
112
+ create_pipe_files(&stdout, &opener).1,
113
+ create_pipe_files(&stderr, &opener).1,
114
+ mounts,
115
+ open_file_limit,
116
+ opener,
117
+ profile,
118
+ )
119
+ .map_err(|err| Trap::Other(format!("a Mount's root did not open: {err:?}")))?;
120
+
121
+ let mut builder = WasiCtxBuilder::new();
122
+ // The profile decides the ambient authority granted: the
123
+ // hermetic rung denies the preview1 time and entropy imports (see
124
+ // `ambient`), the permissive rung leaves the live WASI sources.
125
+ // Ambient filesystem, environment, and network stay absent on both rungs —
126
+ // the builder grants none unless asked. The exhaustive match makes
127
+ // a future ladder rung a compile error here, not authority granted
128
+ // silently.
129
+ let clocks = match profile {
130
+ Profile::Hermetic => {
131
+ builder.wall_clock(ambient::FrozenWallClock);
132
+ builder.monotonic_clock(ambient::FrozenMonotonicClock);
133
+ builder.secure_random(ambient::deterministic_rng());
134
+ Clocks::Frozen
135
+ }
136
+ Profile::Permissive => {
137
+ let monotonic = ambient::LiveMonotonicClock::new();
138
+ builder.monotonic_clock(monotonic);
139
+ Clocks::Live(monotonic)
140
+ }
141
+ };
142
+
143
+ Ok(Self {
144
+ ctx: builder.build_p1(),
145
+ descriptors,
146
+ clocks,
147
+ stdout,
148
+ stderr,
149
+ })
150
+ }
151
+
152
+ pub(crate) fn ctx_mut(&mut self) -> &mut WasiP1Ctx {
153
+ &mut self.ctx
154
+ }
155
+
156
+ pub(crate) fn descriptors(&self) -> &Descriptors {
157
+ &self.descriptors
158
+ }
159
+
160
+ pub(crate) fn descriptors_mut(&mut self) -> &mut Descriptors {
161
+ &mut self.descriptors
162
+ }
163
+
164
+ pub(crate) fn clocks(&self) -> Clocks {
165
+ self.clocks
166
+ }
167
+
168
+ pub(crate) fn stdout_bytes(&self) -> Vec<u8> {
169
+ self.stdout.contents()
170
+ }
171
+
172
+ pub(crate) fn stderr_bytes(&self) -> Vec<u8> {
173
+ self.stderr.contents()
174
+ }
175
+ }
176
+
177
+ /// A Mount as this invocation presents it. A kind this driver does not
178
+ /// support has no descriptor to stand behind; `Driver::supports` keeps a
179
+ /// frontend from handing one over.
180
+ fn to_mount(grant: &Grant) -> Option<Mount> {
181
+ match grant {
182
+ Grant::Mount(mount) => Some(mount.clone()),
183
+ _ => None,
184
+ }
185
+ }
186
+
187
+ #[cfg(test)]
188
+ mod tests {
189
+ //! Witness what an invocation's context grants at each profile rung:
190
+ //! `wasi:clocks` / `wasi:random` frozen under `Hermetic` and live
191
+ //! under `Permissive`, and no ambient filesystem, environment, or
192
+ //! socket under either.
193
+ use wasmtime::{AsContextMut, Module, Store};
194
+
195
+ use super::*;
196
+ use crate::cache::load_engine;
197
+ use crate::imports;
198
+ use crate::invocation::Invocation;
199
+
200
+ /// Preview1 `errno` for a descriptor the context does not hold.
201
+ const ERRNO_BADF: i64 = 8;
202
+
203
+ /// Preview1 probe with one export per ambient source: `clock_ns`
204
+ /// reads the realtime clock, `random_word` reads eight entropy bytes,
205
+ /// `environ_count` counts environment variables, and `prestat_errno`
206
+ /// / `accept_errno` ask descriptor 3 for a directory and a
207
+ /// connection. Preview1 numbers preopens from 3, so nothing there
208
+ /// means no directory and no socket was granted.
209
+ const PROBE_WAT: &str = r#"
210
+ (module
211
+ (import "wasi_snapshot_preview1" "clock_time_get"
212
+ (func $clock (param i32 i64 i32) (result i32)))
213
+ (import "wasi_snapshot_preview1" "random_get"
214
+ (func $random (param i32 i32) (result i32)))
215
+ (import "wasi_snapshot_preview1" "environ_sizes_get"
216
+ (func $environ (param i32 i32) (result i32)))
217
+ (import "wasi_snapshot_preview1" "fd_prestat_get"
218
+ (func $prestat (param i32 i32) (result i32)))
219
+ (import "wasi_snapshot_preview1" "sock_accept"
220
+ (func $accept (param i32 i32 i32) (result i32)))
221
+ (memory (export "memory") 1)
222
+ (func (export "clock_ns") (result i64)
223
+ (drop (call $clock (i32.const 0) (i64.const 1) (i32.const 0)))
224
+ (i64.load (i32.const 0)))
225
+ (func (export "random_word") (result i64)
226
+ (drop (call $random (i32.const 8) (i32.const 8)))
227
+ (i64.load (i32.const 8)))
228
+ (func (export "environ_count") (result i64)
229
+ (drop (call $environ (i32.const 16) (i32.const 20)))
230
+ (i64.extend_i32_u (i32.load (i32.const 16))))
231
+ (func (export "prestat_errno") (result i64)
232
+ (i64.extend_i32_u (call $prestat (i32.const 3) (i32.const 24))))
233
+ (func (export "accept_errno") (result i64)
234
+ (i64.extend_i32_u (call $accept (i32.const 3) (i32.const 0) (i32.const 24)))))
235
+ "#;
236
+
237
+ /// Instantiate the probe over an invocation's WASI context built at
238
+ /// `profile` and return a reader for its exports.
239
+ fn probe(profile: Profile) -> impl FnMut(&str) -> i64 {
240
+ let engine = load_engine().expect("shared engine must be constructible");
241
+ let config = Config {
242
+ timeout: None,
243
+ setup_timeout: None,
244
+ memory_limit: None,
245
+ stdout_limit: None,
246
+ stderr_limit: None,
247
+ open_file_limit: None,
248
+ profile,
249
+ security: Vec::new(),
250
+ };
251
+ let wasi = Wasi::for_invocation(&config, Vec::new(), &[]).expect("WASI context must build");
252
+ let mut store = Store::new(engine, Invocation::new(None, wasi));
253
+ store.set_epoch_deadline(crate::trap::NO_TIMEOUT_EPOCH_DELTA);
254
+
255
+ let linker = imports::linker().expect("host imports must link");
256
+ let module = Module::new(engine, PROBE_WAT).expect("probe module must compile");
257
+ let instance = linker
258
+ .instantiate(&mut store, &module)
259
+ .expect("probe module must instantiate");
260
+
261
+ move |name| {
262
+ instance
263
+ .get_typed_func::<(), i64>(store.as_context_mut(), name)
264
+ .expect("probe export must resolve")
265
+ .call(store.as_context_mut(), ())
266
+ .expect("probe export must run")
267
+ }
268
+ }
269
+
270
+ /// Assert that the context built at `profile` grants no ambient
271
+ /// directory, environment variable, or socket.
272
+ fn assert_grants_no_ambient_resource(profile: Profile) {
273
+ let mut read = probe(profile);
274
+ assert_eq!(
275
+ read("prestat_errno"),
276
+ ERRNO_BADF,
277
+ "a {profile:?} guest's WASI context must preopen no directory"
278
+ );
279
+ assert_eq!(
280
+ read("environ_count"),
281
+ 0,
282
+ "a {profile:?} guest's WASI context must carry no environment variable"
283
+ );
284
+ assert_eq!(
285
+ read("accept_errno"),
286
+ ERRNO_BADF,
287
+ "a {profile:?} guest's WASI context must hold no socket"
288
+ );
289
+ }
290
+
291
+ // @behavior RT-047 RT-048
292
+ #[test]
293
+ fn hermetic_denies_ambient_time_and_entropy() {
294
+ let mut read = probe(Profile::Hermetic);
295
+ assert_eq!(
296
+ read("clock_ns"),
297
+ 0,
298
+ "a hermetic guest's wasi:clocks must read the Unix epoch, not host time"
299
+ );
300
+ assert_eq!(
301
+ read("random_word"),
302
+ 0,
303
+ "a hermetic guest's wasi:random must yield the constant stream, not host entropy"
304
+ );
305
+ }
306
+
307
+ // @behavior RT-049 RT-050
308
+ #[test]
309
+ fn permissive_grants_live_ambient_time_and_entropy() {
310
+ let mut read = probe(Profile::Permissive);
311
+ assert!(
312
+ read("clock_ns") > 0,
313
+ "a permissive guest's wasi:clocks must read live host time"
314
+ );
315
+ assert_ne!(
316
+ read("random_word"),
317
+ 0,
318
+ "a permissive guest's wasi:random must yield host entropy"
319
+ );
320
+ }
321
+
322
+ // @behavior RT-067
323
+ #[test]
324
+ fn hermetic_grants_no_ambient_resource() {
325
+ assert_grants_no_ambient_resource(Profile::Hermetic);
326
+ }
327
+
328
+ // @behavior RT-067
329
+ #[test]
330
+ fn permissive_grants_no_ambient_resource() {
331
+ assert_grants_no_ambient_resource(Profile::Permissive);
332
+ }
333
+ }
@@ -0,0 +1,43 @@
1
+ //! A compile reads no host time or entropy, whatever posture the Sandbox
2
+ //! requested. Compiling runs no script, so the bundled guest has no way to
3
+ //! show what its clock read; a fixture guest whose compile answers the
4
+ //! ambient bytes it read shows it instead.
5
+
6
+ use std::path::Path;
7
+
8
+ use kobako_runtime::{Profile, Runtime};
9
+ use kobako_transport::envelope::{Preamble, Snippet, Snippets};
10
+ use kobako_wasmtime::{Config, Driver};
11
+
12
+ const FIXTURE: &str = concat!(
13
+ env!("CARGO_MANIFEST_DIR"),
14
+ "/../../test/fixtures/minimal_compile_ambient.wat"
15
+ );
16
+
17
+ // @behavior RT-072
18
+ #[test]
19
+ fn a_compile_reads_no_host_time_or_entropy_under_a_permissive_request() {
20
+ let mut config = Config::default();
21
+ config.timeout = None;
22
+ config.setup_timeout = None;
23
+ config.memory_limit = None;
24
+ config.stdout_limit = None;
25
+ config.stderr_limit = None;
26
+ config.open_file_limit = None;
27
+ config.profile = Profile::Permissive;
28
+ let driver = Driver::new(Path::new(FIXTURE), config).expect("the fixture guest must load");
29
+ let mut preamble = Preamble::default();
30
+ preamble.snippets = Snippets {
31
+ entries: vec![Snippet::Source {
32
+ name: "Only".into(),
33
+ body: "1".into(),
34
+ }],
35
+ };
36
+
37
+ assert_eq!(
38
+ driver.compile(&preamble),
39
+ Some(vec![vec![0; 16]]),
40
+ "a permissive request through Driver#compile must read the Unix epoch and the \
41
+ constant stream, not host time and entropy"
42
+ );
43
+ }