kobako 0.27.0 → 0.29.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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +241 -0
  4. data/Cargo.lock +132 -126
  5. data/README.md +240 -147
  6. data/ROADMAP.md +28 -22
  7. data/SECURITY.md +2 -2
  8. data/crates/kobako-runtime/CHANGELOG.md +117 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +16 -10
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/error.rs +8 -9
  13. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  14. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  15. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  18. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  19. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  20. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  21. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  22. data/crates/kobako-runtime/src/grant.rs +20 -0
  23. data/crates/kobako-runtime/src/lib.rs +12 -2
  24. data/crates/kobako-runtime/src/mount.rs +178 -0
  25. data/crates/kobako-runtime/src/profile.rs +4 -4
  26. data/crates/kobako-runtime/src/runtime.rs +43 -17
  27. data/crates/kobako-runtime/src/snapshot.rs +33 -15
  28. data/crates/kobako-runtime/src/yielder.rs +14 -6
  29. data/crates/kobako-transport/CHANGELOG.md +45 -0
  30. data/crates/kobako-transport/Cargo.toml +1 -1
  31. data/crates/kobako-transport/README.md +21 -12
  32. data/crates/kobako-transport/src/abi.rs +11 -14
  33. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  34. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  35. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  36. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  37. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  38. data/crates/kobako-transport/src/envelope/reply.rs +103 -61
  39. data/crates/kobako-transport/src/envelope/request.rs +305 -0
  40. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  41. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  42. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  43. data/crates/kobako-transport/src/envelope.rs +19 -12
  44. data/crates/kobako-transport/src/lib.rs +0 -2
  45. data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
  46. data/crates/kobako-wasmtime/Cargo.toml +23 -16
  47. data/crates/kobako-wasmtime/README.md +6 -4
  48. data/crates/kobako-wasmtime/src/abi.rs +67 -36
  49. data/crates/kobako-wasmtime/src/ambient.rs +64 -5
  50. data/crates/kobako-wasmtime/src/cache.rs +204 -127
  51. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  52. data/crates/kobako-wasmtime/src/config.rs +69 -15
  53. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  54. data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
  55. data/crates/kobako-wasmtime/src/driver.rs +174 -125
  56. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  57. data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
  58. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  59. data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
  60. data/crates/kobako-wasmtime/src/invocation.rs +84 -412
  61. data/crates/kobako-wasmtime/src/io.rs +89 -0
  62. data/crates/kobako-wasmtime/src/lib.rs +7 -24
  63. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  64. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  65. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  66. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  67. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  68. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  69. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  70. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  71. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  72. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  73. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  74. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  75. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  76. data/crates/kobako-wasmtime/src/trap.rs +66 -59
  77. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  78. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  79. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  80. data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
  81. data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
  82. data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
  83. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  84. data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
  85. data/data/kobako.wasm +0 -0
  86. data/ext/kobako/Cargo.toml +4 -2
  87. data/ext/kobako/src/runtime/bridge.rs +177 -113
  88. data/ext/kobako/src/runtime/errors.rs +26 -32
  89. data/ext/kobako/src/runtime/escape.rs +53 -0
  90. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  91. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  92. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  93. data/ext/kobako/src/runtime/gvl.rs +11 -13
  94. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  95. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  96. data/ext/kobako/src/runtime.rs +190 -312
  97. data/lib/kobako/capture.rb +1 -4
  98. data/lib/kobako/catalog/extensions.rb +21 -59
  99. data/lib/kobako/catalog/flags.rb +29 -0
  100. data/lib/kobako/catalog/grants.rb +30 -0
  101. data/lib/kobako/catalog/handles.rb +7 -54
  102. data/lib/kobako/catalog/mount.rb +27 -0
  103. data/lib/kobako/catalog/services.rb +17 -60
  104. data/lib/kobako/catalog/snippets.rb +41 -69
  105. data/lib/kobako/catalog.rb +13 -10
  106. data/lib/kobako/codec/decoder.rb +10 -28
  107. data/lib/kobako/codec/encoder.rb +3 -22
  108. data/lib/kobako/codec/error.rb +6 -27
  109. data/lib/kobako/codec/ext_types.rb +72 -94
  110. data/lib/kobako/codec/handle_walk.rb +65 -130
  111. data/lib/kobako/codec/nesting.rb +3 -8
  112. data/lib/kobako/codec/state.rb +21 -11
  113. data/lib/kobako/codec/utils.rb +6 -34
  114. data/lib/kobako/codec/value_budget.rb +86 -0
  115. data/lib/kobako/codec.rb +10 -22
  116. data/lib/kobako/constant_name.rb +8 -0
  117. data/lib/kobako/context.rb +83 -117
  118. data/lib/kobako/errors.rb +57 -132
  119. data/lib/kobako/execution.rb +16 -21
  120. data/lib/kobako/extension.rb +23 -40
  121. data/lib/kobako/filesystem.rb +38 -0
  122. data/lib/kobako/handle.rb +7 -12
  123. data/lib/kobako/outcome.rb +51 -80
  124. data/lib/kobako/payload/arguments.rb +1 -16
  125. data/lib/kobako/payload.rb +4 -12
  126. data/lib/kobako/pool.rb +25 -55
  127. data/lib/kobako/runtime.rb +7 -21
  128. data/lib/kobako/sandbox.rb +128 -137
  129. data/lib/kobako/sandbox_options.rb +71 -90
  130. data/lib/kobako/snippet/bytecode.rb +11 -0
  131. data/lib/kobako/snippet/source.rb +1 -16
  132. data/lib/kobako/snippet.rb +2 -10
  133. data/lib/kobako/transport/block_failure.rb +35 -0
  134. data/lib/kobako/transport/call.rb +1 -14
  135. data/lib/kobako/transport/dispatcher.rb +127 -206
  136. data/lib/kobako/transport/error.rb +6 -20
  137. data/lib/kobako/transport/exposure.rb +10 -17
  138. data/lib/kobako/transport/reflection.rb +108 -63
  139. data/lib/kobako/transport/run.rb +17 -71
  140. data/lib/kobako/transport/yielder.rb +20 -94
  141. data/lib/kobako/transport.rb +4 -15
  142. data/lib/kobako/unresolved.rb +4 -11
  143. data/lib/kobako/usage.rb +2 -6
  144. data/lib/kobako/version.rb +1 -1
  145. data/lib/kobako.rb +1 -0
  146. data/sig/kobako/catalog/extensions.rbs +2 -2
  147. data/sig/kobako/catalog/flags.rbs +13 -0
  148. data/sig/kobako/catalog/grants.rbs +13 -0
  149. data/sig/kobako/catalog/mount.rbs +28 -0
  150. data/sig/kobako/catalog/services.rbs +3 -5
  151. data/sig/kobako/catalog/snippets.rbs +5 -5
  152. data/sig/kobako/catalog.rbs +1 -0
  153. data/sig/kobako/codec/ext_types.rbs +10 -8
  154. data/sig/kobako/codec/handle_walk.rbs +9 -7
  155. data/sig/kobako/codec/state.rbs +7 -1
  156. data/sig/kobako/codec/utils.rbs +2 -2
  157. data/sig/kobako/codec/value_budget.rbs +23 -0
  158. data/sig/kobako/codec.rbs +4 -2
  159. data/sig/kobako/constant_name.rbs +3 -0
  160. data/sig/kobako/context.rbs +15 -8
  161. data/sig/kobako/errors.rbs +10 -4
  162. data/sig/kobako/filesystem.rbs +88 -0
  163. data/sig/kobako/guest_yielder.rbs +9 -0
  164. data/sig/kobako/outcome.rbs +6 -8
  165. data/sig/kobako/pool.rbs +1 -1
  166. data/sig/kobako/runtime.rbs +21 -45
  167. data/sig/kobako/sandbox.rbs +11 -1
  168. data/sig/kobako/sandbox_options.rbs +25 -7
  169. data/sig/kobako/snapshot.rbs +20 -0
  170. data/sig/kobako/snippet/bytecode.rbs +12 -0
  171. data/sig/kobako/transport/block_failure.rbs +13 -0
  172. data/sig/kobako/transport/dispatcher.rbs +15 -19
  173. data/sig/kobako/transport/exposure.rbs +1 -1
  174. data/sig/kobako/transport/reflection.rbs +19 -4
  175. data/sig/kobako/transport/run.rbs +1 -3
  176. data/sig/kobako/transport/yielder.rbs +5 -9
  177. data/sig/kobako/transport.rbs +9 -8
  178. metadata +75 -10
  179. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  180. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  181. data/lib/kobako/snippet/binary.rb +0 -26
  182. data/rust-toolchain.toml +0 -15
  183. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -9,7 +9,6 @@
9
9
 
10
10
  use std::path::Path;
11
11
  use std::sync::Arc;
12
- use std::time::Instant;
13
12
 
14
13
  use wasmtime::{
15
14
  AsContextMut, InstancePre as WtInstancePre, ResourceLimiter, Store as WtStore, TypedFunc,
@@ -17,14 +16,17 @@ use wasmtime::{
17
16
 
18
17
  use crate::cache::shared_engine;
19
18
  use crate::config::Config;
20
- use crate::exports::Exports;
19
+ use crate::exports::{self, Exports};
21
20
  use crate::invocation::Invocation;
22
- use crate::{capture, frames, instance_pre, trap};
21
+ use crate::wasi::Wasi;
22
+ use crate::{capture, guest_mem, instance_pre, io, trap};
23
23
  use kobako_runtime::dispatch::DispatchHandler;
24
24
  use kobako_runtime::error::{InvokeError, SetupError, Trap};
25
+ use kobako_runtime::grant::{Grant, GrantKind};
25
26
  use kobako_runtime::profile::Profile;
26
- use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
27
+ use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
27
28
  use kobako_runtime::snapshot::{Capture, Completion, Snapshot, Usage};
29
+ use kobako_transport::envelope::{CompileReply, Downcall, Preamble, Run, Snippet};
28
30
 
29
31
  /// The wasmtime execution unit behind one sandbox runtime.
30
32
  pub struct Driver {
@@ -39,49 +41,49 @@ pub struct Driver {
39
41
  }
40
42
 
41
43
  impl Driver {
42
- /// Construct a Driver from a wasm file path, using the process-wide
43
- /// shared Engine and per-path Module / InstancePre caches. The
44
- /// artifact's ABI version is verified with the cached template
45
- /// (`crate::abi`). Every failure is a `SetupError` for the frontend
46
- /// to attribute — Engine and Module never leave the driver.
44
+ /// Load the Guest Binary at `path` and verify its ABI version. The
45
+ /// Engine is shared by the process and the Module cached per path;
46
+ /// neither leaves the driver.
47
47
  pub fn new(path: &Path, config: Config) -> Result<Self, SetupError> {
48
48
  Ok(Self {
49
- instance_pre: instance_pre::cached_instance_pre(path)?,
49
+ instance_pre: instance_pre::cached_instance_pre(path, config.setup_timeout)?,
50
50
  config,
51
51
  })
52
52
  }
53
53
 
54
- /// Build the per-invocation Store: a fresh `Invocation` wired with
55
- /// the memory limiter and the epoch-deadline callback.
56
- fn new_store(&self) -> Result<WtStore<Invocation>, SetupError> {
57
- let mut store = WtStore::new(shared_engine()?, Invocation::new(self.config.memory_limit));
54
+ /// The deadline and the `wall_time` bracket open with the Store, so
55
+ /// instantiating the guest and writing its request count against the
56
+ /// `timeout` as the export call does.
57
+ fn new_store(&self, wasi: Wasi) -> Result<WtStore<Invocation>, SetupError> {
58
+ let mut store = WtStore::new(
59
+ shared_engine()?,
60
+ Invocation::new(self.config.memory_limit, wasi),
61
+ );
58
62
  store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
59
- store.epoch_deadline_callback(trap::epoch_deadline_callback);
63
+ trap::arm_deadline(&mut store, self.config.timeout);
64
+ store.data_mut().start_wall_clock();
60
65
  Ok(store)
61
66
  }
62
67
 
63
- /// Instantiate the per-invocation instance from the pre-linked
64
- /// template and resolve its host-driven export handles. An
65
- /// instantiation failure at invocation time is an engine fault —
66
- /// a `Trap` — unlike the construction-time probe, whose failure is
67
- /// `SetupError`.
68
+ /// Failing here is an engine fault — a `Trap` — unlike the
69
+ /// construction-time probe, whose failure is a `SetupError`.
68
70
  fn instantiate(&self, store: &mut WtStore<Invocation>) -> Result<Exports, Trap> {
69
71
  let instance = self
70
72
  .instance_pre
71
73
  .instantiate(store.as_context_mut())
72
- .map_err(|e| Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}")))?;
73
- Ok(Exports::resolve(&instance, store.as_context_mut()))
74
+ .map_err(|e| {
75
+ trap::cap_trap(&e).unwrap_or_else(|| {
76
+ Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}"))
77
+ })
78
+ })?;
79
+ let exports = Exports::resolve(&instance, store.as_context_mut());
80
+ store.data_mut().set_exports(exports.clone());
81
+ Ok(exports)
74
82
  }
75
83
 
76
- /// Run one guest export call inside the per-invocation cap window:
77
- /// `Driver::prime_caps` before, `disarm_caps` after — the shared
78
- /// bracket for both run-path exports (`__kobako_eval` /
79
- /// `__kobako_run`). Disarm runs whether the call returns or traps, so
80
- /// the `wall_time` bracket and the memory
81
- /// cap always close — that close-on-trap guarantee is the reason this
82
- /// bracket lives in one place rather than inline at each call site.
83
- /// The wasmtime trap is returned unmapped; the caller classifies it
84
- /// through `trap::trap_from`.
84
+ /// Disarm runs whether the call returns or traps, so the `wall_time`
85
+ /// bracket and the memory cap always close; that guarantee is why the
86
+ /// bracket lives in one place rather than at each call site.
85
87
  fn call_with_caps<Params, Results>(
86
88
  &self,
87
89
  store: &mut WtStore<Invocation>,
@@ -93,143 +95,190 @@ impl Driver {
93
95
  Params: wasmtime::WasmParams,
94
96
  Results: wasmtime::WasmResults,
95
97
  {
96
- self.prime_caps(store, exports);
98
+ self.arm_memory_cap(store, exports);
97
99
  let result = export.call(store.as_context_mut(), params);
98
100
  disarm_caps(store);
99
101
  result
100
102
  }
101
103
 
102
- /// Stamp the per-invocation wall-clock deadline into `Invocation`
103
- /// and prime the wasmtime epoch deadline so the next ticker tick
104
- /// wakes the epoch-deadline callback. When `timeout` is disabled,
105
- /// the deadline is set far enough in the future that the callback
106
- /// effectively never fires.
107
- ///
108
- /// Also captures the current linear-memory size as the baseline
109
- /// for the per-invocation memory delta cap —
110
- /// the pre-initialized image's allocation is folded into the
111
- /// baseline rather than the budget — and stamps the wall-clock
112
- /// entry instant for the `wall_time`
113
- /// measurement. The bracket closes in `disarm_caps` so it matches
114
- /// the `timeout` deadline window and excludes `OUTCOME_BUFFER`
115
- /// decoding and stdout / stderr capture readout.
116
- fn prime_caps(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
117
- match self.config.timeout {
118
- Some(timeout) => {
119
- let deadline = Instant::now() + timeout;
120
- store.data_mut().set_deadline(Some(deadline));
121
- store.set_epoch_deadline(1);
122
- }
123
- None => {
124
- store.data_mut().set_deadline(None);
125
- store.set_epoch_deadline(trap::NO_TIMEOUT_EPOCH_DELTA);
126
- }
127
- }
104
+ /// The pre-initialized image's allocation is folded into the memory
105
+ /// baseline rather than the budget, so the cap arms only once the
106
+ /// image is in place.
107
+ fn arm_memory_cap(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
128
108
  let baseline = match exports.memory {
129
109
  Some(m) => m.data_size(store.as_context_mut()),
130
110
  None => 0,
131
111
  };
132
112
  store.data_mut().arm_memory_cap(baseline);
133
- store.data_mut().start_wall_clock();
134
113
  }
135
114
 
136
- /// Bundle one invocation's observables into a fresh `Snapshot`,
137
- /// uniformly for every `completion` — the clipped captures and the
138
- /// cap-bracket usage must survive a trap just as they do an outcome.
115
+ /// Built the same for every `completion`: captures and usage must
116
+ /// survive a trap just as they do an outcome.
139
117
  fn build_snapshot(&self, store: &WtStore<Invocation>, completion: Completion) -> Snapshot {
140
118
  let data = store.data();
141
- let usage = Usage {
142
- wall_time: data.wall_time().as_secs_f64(),
143
- memory_peak: data.memory_peak(),
119
+ let mut usage = Usage::default();
120
+ usage.wall_time = data.wall_time().as_secs_f64();
121
+ usage.memory_peak = data.memory_peak();
122
+ let clip = |bytes, limit| {
123
+ let mut capture = Capture::default();
124
+ (capture.bytes, capture.truncated) = capture::clip_capture(bytes, limit);
125
+ capture
144
126
  };
145
- let (stdout_bytes, stdout_truncated) =
146
- capture::clip_capture(data.stdout_bytes(), self.config.stdout_limit);
147
- let (stderr_bytes, stderr_truncated) =
148
- capture::clip_capture(data.stderr_bytes(), self.config.stderr_limit);
149
- Snapshot {
127
+ Snapshot::new(
150
128
  completion,
151
- stdout: Capture {
152
- bytes: stdout_bytes,
153
- truncated: stdout_truncated,
154
- },
155
- stderr: Capture {
156
- bytes: stderr_bytes,
157
- truncated: stderr_truncated,
158
- },
129
+ clip(data.wasi().stdout_bytes(), self.config.stdout_limit),
130
+ clip(data.wasi().stderr_bytes(), self.config.stderr_limit),
159
131
  usage,
160
- }
132
+ )
161
133
  }
162
134
  }
163
135
 
164
136
  impl ContractRuntime for Driver {
165
- /// Drive one guest invocation on a fresh instance and return its
166
- /// `Snapshot`, `Ok` iff the guest export ran. Builds a fresh Store,
167
- /// binds the borrowed dispatch handler, installs the stdin frames
168
- /// (three for `Eval` — preamble / source / snippets; two for `Run` —
169
- /// preamble / snippets, with the envelope copied into guest memory),
170
- /// and primes the per-invocation caps around the export call. A fault
171
- /// before the export call is the `Err` channel; once the call starts,
172
- /// every fault folds into the Snapshot's `Completion` — the
173
- /// configured-cap paths as `Trap::Timeout` / `Trap::MemoryLimit`,
174
- /// everything else as `Trap::Other` — so captures and usage survive
175
- /// it. The body touches no frontend value — the handler is only
176
- /// borrowed (see the trait's safety contract).
137
+ /// A fault before the export call is the `Err`; once the call
138
+ /// starts, every fault folds into the Snapshot's `Completion` so
139
+ /// captures and usage survive it. The handler is only borrowed (see the
140
+ /// trait's safety contract).
177
141
  fn invoke(
178
142
  &self,
179
143
  entry: Entry<'_>,
180
- frames: Frames<'_>,
144
+ preamble: &Preamble,
181
145
  handler: Option<Arc<dyn DispatchHandler>>,
146
+ grants: &[Grant],
182
147
  ) -> Result<Snapshot, InvokeError> {
183
- let mut store = self.new_store()?;
148
+ let wasi = Wasi::for_invocation(&self.config, &preamble.encode(), grants)?;
149
+ let mut store = self.new_store(wasi)?;
184
150
  if let Some(handler) = handler {
185
151
  store.data_mut().bind_on_dispatch(handler);
186
152
  }
187
- let frame_list: Vec<&[u8]> = match &entry {
188
- Entry::Eval { source } => vec![frames.preamble, source, frames.snippets],
189
- Entry::Run { .. } => vec![frames.preamble, frames.snippets],
190
- };
191
- frames::install_wasi_frames(&mut store, &self.config, &frame_list)?;
192
153
  let exports = self.instantiate(&mut store)?;
193
- let called = match entry {
194
- Entry::Eval { .. } => {
195
- let eval = frames::require_export(exports.eval.as_ref())?;
196
- self.call_with_caps(&mut store, &exports, eval, ())
197
- }
198
- Entry::Run { envelope } => {
199
- let run = frames::require_export(exports.run.as_ref())?;
200
- let (env_ptr, env_len) = frames::write_envelope(&mut store, &exports, envelope)?;
201
- self.call_with_caps(&mut store, &exports, run, (env_ptr, env_len))
202
- }
203
- };
154
+ let downcall = exports::require(exports.downcall.as_ref())?;
155
+ let request = match entry {
156
+ Entry::Eval { source } => Downcall::Eval(source.to_vec()),
157
+ Entry::Run {
158
+ entrypoint,
159
+ payload,
160
+ } => Downcall::Run(Run {
161
+ entrypoint: entrypoint.to_owned(),
162
+ payload,
163
+ }),
164
+ }
165
+ .encode();
166
+ let (ptr, len) = io::write_request(&mut store, &exports, &request)?;
167
+ let called = self.call_with_caps(&mut store, &exports, downcall, (ptr, len));
204
168
  let completion = match called {
205
- Ok(()) => match frames::fetch_outcome_bytes(&mut store, &exports) {
206
- Ok(bytes) => Completion::Outcome(bytes),
169
+ Ok(packed) => match io::read_outcome(&store, &exports, packed) {
170
+ Ok(outcome) => Completion::Outcome(outcome),
207
171
  Err(t) => Completion::Trap(t),
208
172
  },
209
- Err(e) => Completion::Trap(trap::trap_from(e)),
173
+ Err(e) => Completion::Trap(trap::to_trap(e)),
210
174
  };
211
175
  Ok(self.build_snapshot(&store, completion))
212
176
  }
213
177
 
214
- /// The posture this driver built — exactly the rung requested via
215
- /// `Config`. Every per-invocation WASI context is built to it
216
- /// (`frames::install_wasi_frames`): `Hermetic` freezes ambient time
217
- /// and entropy (`crate::ambient`), `Permissive` leaves the live WASI
218
- /// sources. Both rungs wire no filesystem, environment, or network,
219
- /// and the linker adds only the wire ABI's `__kobako_dispatch`
220
- /// beyond that confined WASI surface (`crate::instance_pre`).
178
+ /// Exactly the rung `Config` requested. `Hermetic` freezes ambient time
179
+ /// and entropy; both rungs wire no ambient filesystem, environment, or network,
180
+ /// and the linker adds only `__kobako_upcall` beyond that WASI surface.
221
181
  fn profile(&self) -> Profile {
222
182
  self.config.profile
223
183
  }
184
+
185
+ /// A Mount becomes a preopen of kobako's own descriptor table, which
186
+ /// answers every preview1 call beneath it.
187
+ fn supports(&self, kind: GrantKind) -> bool {
188
+ matches!(kind, GrantKind::Mount)
189
+ }
190
+
191
+ /// Bounded by the same caps as an invocation, on an instance of its
192
+ /// own that no script ever runs on, so what it answers carries
193
+ /// nothing a script could have written.
194
+ fn compile(&self, preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
195
+ let wasi = Wasi::for_compile(&self.config, &preamble.encode()).ok()?;
196
+ let mut store = self.new_store(wasi).ok()?;
197
+ let exports = self.instantiate(&mut store).ok()?;
198
+ let downcall = exports::require(exports.downcall.as_ref()).ok()?;
199
+ let (ptr, len) =
200
+ io::write_request(&mut store, &exports, &Downcall::Compile.encode()).ok()?;
201
+ let packed = self
202
+ .call_with_caps(&mut store, &exports, downcall, (ptr, len))
203
+ .ok()?;
204
+ match io::read_compile_reply(&store, &exports, packed).ok()? {
205
+ CompileReply::Ok(bytecodes) => one_per_source(preamble, bytecodes)
206
+ .filter(|bytecodes| replay_fits(preamble, bytecodes)),
207
+ CompileReply::Error(_) | CompileReply::Unsupported(_) => None,
208
+ }
209
+ }
210
+ }
211
+
212
+ /// Each invocation sends the Preamble with every bytecode in place of its
213
+ /// source, so an answer that would carry it past the payload cap would trap
214
+ /// every invocation the source alone fits.
215
+ fn replay_fits(preamble: &Preamble, bytecodes: &[Vec<u8>]) -> bool {
216
+ let mut replay = preamble.clone();
217
+ let sources = replay
218
+ .snippets
219
+ .entries
220
+ .iter_mut()
221
+ .filter(|entry| matches!(entry, Snippet::Source { .. }));
222
+ for (entry, body) in sources.zip(bytecodes) {
223
+ *entry = Snippet::Bytecode { body: body.clone() };
224
+ }
225
+ guest_mem::checked_payload_len(replay.encode().len()).is_ok()
226
+ }
227
+
228
+ /// A frontend replays each bytecode in place of one source snippet, in
229
+ /// order, so an answer of any other length leaves nothing to replay.
230
+ fn one_per_source(preamble: &Preamble, bytecodes: Vec<Vec<u8>>) -> Option<Vec<Vec<u8>>> {
231
+ let sources = preamble
232
+ .snippets
233
+ .entries
234
+ .iter()
235
+ .filter(|entry| matches!(entry, Snippet::Source { .. }))
236
+ .count();
237
+ (bytecodes.len() == sources).then_some(bytecodes)
224
238
  }
225
239
 
226
- /// Drop the memory cap as soon as the guest call returns so that
227
- /// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
228
- /// which can grow guest memory transiently) is not attributed to
229
- /// the user script. Also closes the
230
- /// `wall_time` bracket opened by `Driver::prime_caps`. Paired
231
- /// with `Driver::prime_caps`.
232
240
  fn disarm_caps(store: &mut WtStore<Invocation>) {
233
241
  store.data_mut().stop_wall_clock();
234
242
  store.data_mut().disarm_memory_cap();
235
243
  }
244
+
245
+ #[cfg(test)]
246
+ mod tests {
247
+ use kobako_transport::envelope::Snippets;
248
+
249
+ use super::*;
250
+
251
+ /// One source between two bytecode snippets: a compile answers one
252
+ /// bytecode for it.
253
+ fn one_source() -> Preamble {
254
+ let mut preamble = Preamble::default();
255
+ preamble.snippets = Snippets {
256
+ entries: vec![
257
+ Snippet::Bytecode { body: vec![0xaa] },
258
+ Snippet::Source {
259
+ name: "Only".into(),
260
+ body: "1".into(),
261
+ },
262
+ Snippet::Bytecode { body: vec![0xbb] },
263
+ ],
264
+ };
265
+ preamble
266
+ }
267
+
268
+ // @behavior WE-089
269
+ #[test]
270
+ fn a_compiled_answer_of_another_length_leaves_nothing_compiled() {
271
+ let preamble = one_source();
272
+
273
+ assert_eq!(
274
+ [
275
+ one_per_source(&preamble, Vec::new()),
276
+ one_per_source(&preamble, vec![vec![0x01], vec![0x02]]),
277
+ one_per_source(&preamble, vec![vec![0x01]]),
278
+ ],
279
+ [None, None, Some(vec![vec![0x01]])],
280
+ "a compiled answer through Driver#compile must be kept only when it carries one \
281
+ bytecode per source snippet"
282
+ );
283
+ }
284
+ }
@@ -1,56 +1,47 @@
1
1
  //! Per-invocation wasmtime export handles for the host-driven ABI
2
2
  //! surface.
3
3
  //!
4
- //! `Driver::instantiate` resolves the ABI exports the run path drives
5
- //! (`__kobako_eval` / `__kobako_run` / `__kobako_take_outcome` /
6
- //! `__kobako_alloc`) plus the `memory` export against each fresh
7
- //! per-invocation instance and bundles their
8
- //! typed handles here, so the invocation body passes one struct around
9
- //! rather than re-resolving exports by name at every step. Distinct
10
- //! from `crate::cache` (the process-wide Engine / Module cache): this
11
- //! carries *which guest function to call*, per invocation.
12
- //!
13
- //! `crate::dispatch` does not reach this struct — a host import runs
14
- //! against a `Caller`, so the dispatch path resolves `__kobako_alloc`
15
- //! and `memory` through `Caller::get_export` instead.
4
+ //! Each ABI export is named and typed once, here, against each fresh
5
+ //! per-invocation instance. The invocation body and the host imports
6
+ //! running inside it both drive the guest through these handles, never
7
+ //! by looking an export up by name. Distinct from `crate::cache` (the
8
+ //! process-wide Engine / Module cache): this carries *which guest
9
+ //! function to call*, per invocation.
16
10
 
17
11
  use wasmtime::{AsContextMut, Instance as WtInstance, Memory, TypedFunc};
18
12
 
13
+ use crate::guest_mem::SANDBOX_RUNTIME_NOT_KOBAKO;
14
+ use kobako_runtime::error::Trap;
15
+
16
+ /// For a required guest export that is absent or mistyped, wherever it is
17
+ /// looked up. Names neither the export nor a frontend's artifact path;
18
+ /// what a caller can do is rebuild the runtime.
19
+ pub(crate) const RUNTIME_INCOMPATIBLE: &str =
20
+ "the Sandbox runtime is incompatible; rebuild it against the kobako version in use";
21
+
19
22
  /// The resolved host-driven export handles. Each is `Option` because test
20
23
  /// fixtures (a minimal "ping" module) need not provide them; real
21
- /// `kobako.wasm` always does, and the run-path methods surface a `Trap`
22
- /// (via `require_export` / `require_memory`) when a handle is `None`.
24
+ /// `kobako.wasm` always does, and the run path surfaces a `Trap` (via
25
+ /// `require` / `Exports::require_memory`) when a handle is `None`.
23
26
  ///
24
27
  /// The handles are indices into the owning Store, not borrows of the
25
28
  /// `Instance` — they stay valid for the Store's lifetime, which is why
26
- /// no `Instance` field is kept.
29
+ /// no `Instance` field is kept, and why a clone can ride in the Store's
30
+ /// own data for the host imports to reach.
31
+ #[derive(Clone)]
27
32
  pub(crate) struct Exports {
28
- pub(crate) eval: Option<TypedFunc<(), ()>>,
29
- pub(crate) run: Option<TypedFunc<(i32, i32), ()>>,
30
- pub(crate) take_outcome: Option<TypedFunc<(), u64>>,
33
+ pub(crate) downcall: Option<TypedFunc<(i32, i32), u64>>,
31
34
  pub(crate) alloc: Option<TypedFunc<u32, u32>>,
32
35
  pub(crate) memory: Option<Memory>,
33
36
  }
34
37
 
35
38
  impl Exports {
36
- /// Best-effort lookup of the host-driven exports against a freshly
37
- /// instantiated module. Missing exports are not an error here
38
- /// (the test fixture is a bare module); the host enforces presence at
39
- /// invocation time. Only the SPEC ABI shapes are accepted —
40
- /// `__kobako_eval` is `() -> ()`, `__kobako_run` is
41
- /// `(env_ptr, env_len) -> ()`, `__kobako_take_outcome` is `() -> u64`,
42
- /// `__kobako_alloc` is `(len) -> ptr`
43
- /// (docs/wire-codec.md § ABI Signatures).
39
+ /// A missing export is not an error here, because the test fixture is a
40
+ /// bare module; presence is enforced at invocation time.
44
41
  pub(crate) fn resolve(instance: &WtInstance, mut ctx: impl AsContextMut) -> Self {
45
42
  Self {
46
- eval: instance
47
- .get_typed_func::<(), ()>(&mut ctx, "__kobako_eval")
48
- .ok(),
49
- run: instance
50
- .get_typed_func::<(i32, i32), ()>(&mut ctx, "__kobako_run")
51
- .ok(),
52
- take_outcome: instance
53
- .get_typed_func::<(), u64>(&mut ctx, "__kobako_take_outcome")
43
+ downcall: instance
44
+ .get_typed_func::<(i32, i32), u64>(&mut ctx, "__kobako_downcall")
54
45
  .ok(),
55
46
  alloc: instance
56
47
  .get_typed_func::<u32, u32>(&mut ctx, "__kobako_alloc")
@@ -58,4 +49,67 @@ impl Exports {
58
49
  memory: instance.get_memory(&mut ctx, "memory"),
59
50
  }
60
51
  }
52
+
53
+ pub(crate) fn require_memory(&self) -> Result<Memory, Trap> {
54
+ self.memory
55
+ .ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_NOT_KOBAKO.to_string()))
56
+ }
57
+ }
58
+
59
+ pub(crate) fn require<Params, Results>(
60
+ export: Option<&TypedFunc<Params, Results>>,
61
+ ) -> Result<&TypedFunc<Params, Results>, Trap>
62
+ where
63
+ Params: wasmtime::WasmParams,
64
+ Results: wasmtime::WasmResults,
65
+ {
66
+ export.ok_or_else(|| Trap::Other(RUNTIME_INCOMPATIBLE.to_string()))
67
+ }
68
+
69
+ #[cfg(test)]
70
+ mod tests {
71
+ //! The ABI is a closed set of guest exports, so the bundled Guest
72
+ //! Binary is read for what it exports rather than only for whether
73
+ //! `resolve` finds what it looks up — an extra entry point would pass
74
+ //! every lookup. A missing binary is a hard failure under
75
+ //! CI (which always builds it) and a silent skip locally.
76
+ use std::path::Path;
77
+
78
+ use wasmtime::ExternType;
79
+
80
+ use crate::cache::cached_module;
81
+
82
+ const WASM: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../data/kobako.wasm");
83
+
84
+ const ABI_EXPORTS: [&str; 3] = [
85
+ "__kobako_abi_version",
86
+ "__kobako_alloc",
87
+ "__kobako_downcall",
88
+ ];
89
+
90
+ // @behavior RT-068
91
+ #[test]
92
+ fn the_guest_binary_exports_the_abi_functions_and_nothing_else() {
93
+ let path = Path::new(WASM);
94
+ if !path.exists() {
95
+ assert!(
96
+ std::env::var_os("CI").is_none(),
97
+ "data/kobako.wasm missing under CI — run `bundle exec rake wasm:build`"
98
+ );
99
+ return;
100
+ }
101
+ let module = cached_module(path).expect("the Guest Binary must load");
102
+
103
+ let mut functions: Vec<&str> = module
104
+ .exports()
105
+ .filter(|export| matches!(export.ty(), ExternType::Func(_)))
106
+ .map(|export| export.name())
107
+ .collect();
108
+ functions.sort_unstable();
109
+
110
+ assert_eq!(
111
+ functions, ABI_EXPORTS,
112
+ "data/kobako.wasm read through its export section must export exactly the ABI's functions"
113
+ );
114
+ }
61
115
  }