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
@@ -1,130 +1,154 @@
1
- //! Caller-based guest linear-memory I/O shared by the host-import paths.
1
+ //! Guest linear-memory I/O for the ABI's buffer handoffs, whether the
2
+ //! invocation body drives one through its `Store` or a dispatch or yield
3
+ //! import drives one through its `Caller`. preview1 reads and writes its
4
+ //! arguments through its own `GuestMemory`.
2
5
  //!
3
- //! Both directions of a host↔guest buffer handoff that run *inside* a wasm
4
- //! callback frame go through here: writing the transport Reply back
5
- //! (`crate::dispatch`) and shipping block-yield args into the guest
6
- //! (`drive_yield`, below) performed the same `__kobako_alloc` +
7
- //! bounds-check + `memory.write` dance with only the diagnostic strings
8
- //! differing. The Store-based write path (`frames::write_envelope`) is a
9
- //! separate beast — it holds the per-invocation `Store`, not a `Caller` —
10
- //! and stays in `frames`.
6
+ //! The two shared steps answer an error kind, and each caller words it,
7
+ //! because depth decides what the same problem means: before the export
8
+ //! call it is a `Trap` or an intact runtime, inside a callback it is a
9
+ //! wire failure the guest receives.
11
10
 
12
- use wasmtime::{Caller, Extern, Memory};
11
+ use wasmtime::{AsContext, AsContextMut, Caller, Memory};
13
12
 
13
+ use crate::exports::{Exports, RUNTIME_INCOMPATIBLE};
14
14
  use crate::invocation::Invocation;
15
- use kobako_runtime::error::Trap;
16
- use kobako_runtime::yielder::Yielder;
17
15
  use kobako_transport::abi::{unpack_ptr_len, MAX_DISPATCH_PAYLOAD};
16
+ use kobako_transport::envelope::Downcall;
18
17
 
19
- /// The wasmtime-backed `Yielder` (`kobako_runtime::yielder`): a
20
- /// frame-scoped wrapper over the dispatch `Caller` that drives a block-yield
21
- /// round-trip through `drive_yield`. Built per `__kobako_dispatch` frame and
22
- /// handed to the dispatch handler, so nested dispatch frames each
23
- /// carry their own and stack on the Rust call stack with no shared slot.
24
- pub(crate) struct CallerYielder<'a, 'c> {
25
- caller: &'a mut Caller<'c, Invocation>,
26
- }
27
-
28
- impl<'a, 'c> CallerYielder<'a, 'c> {
29
- pub(crate) fn new(caller: &'a mut Caller<'c, Invocation>) -> Self {
30
- Self { caller }
31
- }
32
- }
33
-
34
- impl Yielder for CallerYielder<'_, '_> {
35
- fn yield_to_block(&mut self, args: &[u8]) -> Result<Vec<u8>, Trap> {
36
- drive_yield(self.caller, args).map_err(|msg| Trap::Other(msg.to_string()))
37
- }
38
- }
39
-
40
- /// User-facing reason when a required guest export (the allocation or
41
- /// block-yield hook) is absent or has the wrong signature — the loaded
42
- /// `data/kobako.wasm` does not match the installed gem. Phrased in caller
43
- /// vocabulary: the underlying hook symbol names are not actionable, and
44
- /// the actionable fix is to rebuild the runtime.
45
- const RUNTIME_INCOMPATIBLE: &str =
46
- "the Sandbox runtime is incompatible; rebuild data/kobako.wasm against the installed version";
18
+ // The size-limit messages here and in `io` name the cap as 16 MiB, so
19
+ // moving the cap fails the build until they are reworded with it.
20
+ const _: () = assert!(MAX_DISPATCH_PAYLOAD == 16 << 20);
47
21
 
48
22
  /// User-facing message for the "the loaded Wasm module is not a
49
- /// Kobako-shaped runtime at all" failure mode — no linear memory export
50
- /// here, no `memory` module export on the instantiation path in
51
- /// `frames`. One constant so the two detection sites cannot drift.
23
+ /// Kobako-shaped runtime at all" failure mode, such as an instance with no
24
+ /// `memory` export.
52
25
  pub(crate) const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
53
26
  "the loaded Wasm module is not a Kobako-compatible runtime";
54
27
 
55
- /// Resolve the guest's exported linear `memory`. The lookup shape (and its
56
- /// diagnostic) is shared by every Caller-based path here — the write side
57
- /// (`alloc_and_write`), the read side (`read`), and the yield round-trip
58
- /// (`drive_yield`) — so the "no linear memory" reason lives in one place.
59
- /// `read` maps the `Err` to its own `None` outcome via `.ok()`.
60
- fn memory_export(caller: &mut Caller<'_, Invocation>) -> Result<Memory, &'static str> {
61
- match caller.get_export("memory") {
62
- Some(Extern::Memory(m)) => Ok(m),
63
- _ => Err(SANDBOX_RUNTIME_NOT_KOBAKO),
64
- }
28
+ /// Why a host write into guest memory failed.
29
+ pub(crate) enum WriteError {
30
+ TooLarge,
31
+ /// The allocator or the memory is missing or mistyped; carries the
32
+ /// message for it.
33
+ Incompatible(&'static str),
34
+ Trapped(wasmtime::Error),
35
+ /// The allocator ran and answered 0.
36
+ OutOfMemory,
37
+ OutOfBounds,
65
38
  }
66
39
 
67
- /// Allocate `bytes.len()` bytes in guest memory via `__kobako_alloc` and
68
- /// copy `bytes` in. Returns the guest pointer. Every failure path carries a
69
- /// `&'static str` reason so the caller can surface a diagnostic rather than
70
- /// a silent fault.
40
+ /// Why a host read of a guest-named buffer failed.
41
+ pub(crate) enum ReadError {
42
+ TooLarge,
43
+ OutOfBounds,
44
+ }
45
+
46
+ /// Allocate a guest buffer through `__kobako_alloc` and copy `bytes` into
47
+ /// it, answering the buffer's address.
71
48
  pub(crate) fn alloc_and_write(
72
- caller: &mut Caller<'_, Invocation>,
49
+ mut ctx: impl AsContextMut,
50
+ exports: &Exports,
73
51
  bytes: &[u8],
74
- ) -> Result<u32, &'static str> {
75
- let alloc = match caller.get_export("__kobako_alloc") {
76
- Some(Extern::Func(f)) => f
77
- .typed::<i32, i32>(&*caller)
78
- .map_err(|_| RUNTIME_INCOMPATIBLE)?,
79
- _ => return Err(RUNTIME_INCOMPATIBLE),
80
- };
81
- let len = checked_payload_len(bytes.len())?;
52
+ ) -> Result<u32, WriteError> {
53
+ let len = checked_payload_len(bytes.len()).map_err(|_| WriteError::TooLarge)?;
54
+ let alloc = exports
55
+ .alloc
56
+ .as_ref()
57
+ .ok_or(WriteError::Incompatible(RUNTIME_INCOMPATIBLE))?;
58
+ let memory = exports
59
+ .memory
60
+ .ok_or(WriteError::Incompatible(SANDBOX_RUNTIME_NOT_KOBAKO))?;
82
61
  let ptr = alloc
83
- .call(&mut *caller, len)
84
- .map_err(|_| "the Sandbox trapped while allocating memory for the request")?;
62
+ .call(&mut ctx, len as u32)
63
+ .map_err(WriteError::Trapped)?;
85
64
  if ptr == 0 {
86
- return Err("the Sandbox ran out of memory while preparing the request");
65
+ return Err(WriteError::OutOfMemory);
66
+ }
67
+ memory
68
+ .write(&mut ctx, ptr as usize, bytes)
69
+ .map_err(|_| WriteError::OutOfBounds)?;
70
+ Ok(ptr)
71
+ }
72
+
73
+ /// Copy out the buffer the guest names; a length past the 16 MiB cap is
74
+ /// refused before memory is touched.
75
+ pub(crate) fn read_buffer(
76
+ ctx: impl AsContext,
77
+ memory: Memory,
78
+ ptr: usize,
79
+ len: usize,
80
+ ) -> Result<Vec<u8>, ReadError> {
81
+ if len > MAX_DISPATCH_PAYLOAD {
82
+ return Err(ReadError::TooLarge);
87
83
  }
84
+ let data = memory.data(&ctx);
85
+ let range = guest_buffer_range(ptr, len, data.len()).map_err(|_| ReadError::OutOfBounds)?;
86
+ Ok(data[range].to_vec())
87
+ }
88
88
 
89
- let mem = memory_export(caller)?;
90
- mem.write(&mut *caller, ptr as usize, bytes)
91
- .map_err(|_| "could not write the request into the Sandbox's memory")?;
92
- Ok(ptr as u32)
89
+ /// The handles of the instance a host import runs inside.
90
+ fn callback_exports(caller: &Caller<'_, Invocation>) -> Result<Exports, &'static str> {
91
+ caller.data().exports().ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)
93
92
  }
94
93
 
95
- /// Copy `[ptr, ptr + len)` out of the guest's linear memory as seen from
96
- /// `caller`. Each failure carries a `&'static str` reason — matching the
97
- /// other Caller-based ops here — so the caller surfaces a specific
98
- /// diagnostic instead of a lumped one; a guest-claimed length past the
99
- /// 16 MiB cap is a wire violation that names the cap (the caller walks
100
- /// the trap path on any `Err`).
101
- pub(crate) fn read(
94
+ /// Keep a trap the guest raised during a callback into it for the dispatch
95
+ /// import to end the invocation with, and answer the callback's own reason.
96
+ fn keep_trap(
102
97
  caller: &mut Caller<'_, Invocation>,
103
- ptr: i32,
104
- len: i32,
98
+ trap: wasmtime::Error,
99
+ reason: &'static str,
100
+ ) -> &'static str {
101
+ caller.data_mut().record_reentry_trap(trap);
102
+ reason
103
+ }
104
+
105
+ /// The request the guest hands the upcall import. Its `(ptr, len)` are
106
+ /// guest addresses, so they are read unsigned.
107
+ pub(crate) fn read_request(
108
+ caller: &mut Caller<'_, Invocation>,
109
+ req_ptr: i32,
110
+ req_len: i32,
105
111
  ) -> Result<Vec<u8>, &'static str> {
106
- let len = usize::try_from(len).map_err(|_| "the Sandbox produced a negative request length")?;
107
- if len > MAX_DISPATCH_PAYLOAD {
108
- return Err("request payload exceeds the 16 MiB limit");
112
+ let memory = callback_exports(caller)?
113
+ .memory
114
+ .ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
115
+ read_buffer(
116
+ &*caller,
117
+ memory,
118
+ req_ptr as u32 as usize,
119
+ req_len as u32 as usize,
120
+ )
121
+ .map_err(|err| match err {
122
+ ReadError::TooLarge => "request payload exceeds the 16 MiB limit",
123
+ ReadError::OutOfBounds => "the Sandbox produced an out-of-bounds request",
124
+ })
125
+ }
126
+
127
+ /// Write `bytes` into the guest from inside a callback. A guest that has
128
+ /// already trapped is not called again.
129
+ pub(crate) fn write_from_callback(
130
+ caller: &mut Caller<'_, Invocation>,
131
+ bytes: &[u8],
132
+ ) -> Result<u32, &'static str> {
133
+ if caller.data().reentry_trapped() {
134
+ return Err("the Sandbox already trapped during this invocation");
109
135
  }
110
- let mem = memory_export(caller)?;
111
- let data = mem.data(&caller);
112
- let start =
113
- usize::try_from(ptr).map_err(|_| "the Sandbox produced a negative request pointer")?;
114
- let end = start
115
- .checked_add(len)
116
- .ok_or("the Sandbox produced an out-of-range request")?;
117
- data.get(start..end)
118
- .map(|s| s.to_vec())
119
- .ok_or("the Sandbox produced an out-of-bounds request")
136
+ let exports = callback_exports(caller)?;
137
+ alloc_and_write(&mut *caller, &exports, bytes).map_err(|err| match err {
138
+ WriteError::TooLarge => "payload exceeds the 16 MiB limit",
139
+ WriteError::Incompatible(message) => message,
140
+ WriteError::Trapped(trap) => keep_trap(
141
+ caller,
142
+ trap,
143
+ "the Sandbox trapped while allocating memory for the request",
144
+ ),
145
+ WriteError::OutOfMemory => "the Sandbox ran out of memory while preparing the request",
146
+ WriteError::OutOfBounds => "could not write the request into the Sandbox's memory",
147
+ })
120
148
  }
121
149
 
122
- /// Validate a payload length against `MAX_DISPATCH_PAYLOAD` and narrow it
123
- /// to `i32` — the signed wasm ABI width for the guest buffer parameters.
124
- /// Every host *write* boundary (`alloc_and_write`, `drive_yield`,
125
- /// `frames::write_envelope`) routes its length through here so the
126
- /// wire-violation reason is uniform; the *read* boundaries compare
127
- /// against `MAX_DISPATCH_PAYLOAD` directly.
150
+ /// Every host write boundary routes its length through here, so the
151
+ /// wire-violation reason is uniform.
128
152
  pub(crate) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
129
153
  if len > MAX_DISPATCH_PAYLOAD {
130
154
  return Err("payload exceeds the 16 MiB limit");
@@ -133,10 +157,6 @@ pub(crate) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
133
157
  i32::try_from(len).map_err(|_| "payload exceeds the 16 MiB limit")
134
158
  }
135
159
 
136
- /// Compute the half-open range `[ptr, ptr + len)` for a guest linear-memory
137
- /// copy, validating that the arithmetic does not overflow and the range
138
- /// fits inside `mem_size`. Shared by `frames::write_envelope` (write side)
139
- /// and `frames::fetch_outcome_bytes` (read side).
140
160
  pub(crate) fn guest_buffer_range(
141
161
  ptr: usize,
142
162
  len: usize,
@@ -149,53 +169,46 @@ pub(crate) fn guest_buffer_range(
149
169
  Ok(ptr..end)
150
170
  }
151
171
 
152
- /// The `(ptr, len)` a buffer-returning ABI export answered with, widened
153
- /// to the `usize` the linear-memory ranges here are computed in.
154
- pub(crate) fn unpack_outcome_packed(packed: u64) -> (usize, usize) {
172
+ /// The `(ptr, len)` the guest packs for any answer it hands back, the
173
+ /// outcome and a block's result alike.
174
+ pub(crate) fn unpack_guest_buffer(packed: u64) -> (usize, usize) {
155
175
  let (ptr, len) = unpack_ptr_len(packed);
156
176
  (ptr as usize, len as usize)
157
177
  }
158
178
 
159
- /// Allocate `args.len()` bytes in guest memory, copy the args payload in,
160
- /// call `__kobako_yield_to_block(ptr, len)`, then read the response slice
161
- /// the guest produced and return it. Mirrors `dispatch::write_response`'s
162
- /// allocator dance but in the opposite direction — the host is the
163
- /// *initiator* of this round-trip, not the responder.
164
179
  pub(crate) fn drive_yield(
165
180
  caller: &mut Caller<'_, Invocation>,
181
+ frame: u32,
166
182
  args: &[u8],
167
183
  ) -> Result<Vec<u8>, &'static str> {
168
- let len_i32 = checked_payload_len(args.len())?;
169
- let req_ptr = alloc_and_write(caller, args)? as i32;
184
+ let request = Downcall::Yield {
185
+ frame,
186
+ args: args.to_vec(),
187
+ }
188
+ .encode();
189
+ let len_i32 = checked_payload_len(request.len())?;
190
+ let req_ptr = write_from_callback(caller, &request)? as i32;
170
191
 
171
- let yield_fn = match caller.get_export("__kobako_yield_to_block") {
172
- Some(Extern::Func(f)) => f
173
- .typed::<(i32, i32), u64>(&*caller)
174
- .map_err(|_| RUNTIME_INCOMPATIBLE)?,
175
- _ => return Err(RUNTIME_INCOMPATIBLE),
176
- };
192
+ let exports = callback_exports(caller)?;
193
+ let yield_fn = exports.downcall.ok_or(RUNTIME_INCOMPATIBLE)?;
194
+ let memory = exports.memory.ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
177
195
  let packed = yield_fn
178
196
  .call(&mut *caller, (req_ptr, len_i32))
179
- .map_err(|_| "the Sandbox trapped while invoking a block")?;
180
- let (resp_ptr, resp_len) = unpack_outcome_packed(packed);
181
- if resp_len == 0 {
197
+ .map_err(|trap| keep_trap(caller, trap, "the Sandbox trapped while invoking a block"))?;
198
+ let (reply_ptr, reply_len) = unpack_guest_buffer(packed);
199
+ if reply_len == 0 {
182
200
  return Err("the Sandbox returned an empty block result");
183
201
  }
184
- if resp_len > MAX_DISPATCH_PAYLOAD {
185
- return Err("block result payload exceeds the 16 MiB limit");
186
- }
187
-
188
- let mem = memory_export(caller)?;
189
- let data = mem.data(&caller);
190
- let range = guest_buffer_range(resp_ptr, resp_len, data.len())
191
- .map_err(|_| "the Sandbox returned an out-of-bounds block result")?;
192
- Ok(data[range].to_vec())
202
+ read_buffer(&*caller, memory, reply_ptr, reply_len).map_err(|err| match err {
203
+ ReadError::TooLarge => "block result payload exceeds the 16 MiB limit",
204
+ ReadError::OutOfBounds => "the Sandbox returned an out-of-bounds block result",
205
+ })
193
206
  }
194
207
 
195
208
  #[cfg(test)]
196
209
  mod tests {
197
210
  use super::{
198
- checked_payload_len, guest_buffer_range, unpack_outcome_packed, MAX_DISPATCH_PAYLOAD,
211
+ checked_payload_len, guest_buffer_range, unpack_guest_buffer, MAX_DISPATCH_PAYLOAD,
199
212
  };
200
213
 
201
214
  // @behavior WE-059
@@ -243,16 +256,16 @@ mod tests {
243
256
 
244
257
  // @behavior WE-001
245
258
  #[test]
246
- fn unpack_outcome_packed_extracts_high_ptr_low_len() {
259
+ fn unpack_guest_buffer_extracts_high_ptr_low_len() {
247
260
  assert_eq!(
248
- unpack_outcome_packed(0xAABB_CCDD_1122_3344),
261
+ unpack_guest_buffer(0xAABB_CCDD_1122_3344),
249
262
  (0xAABB_CCDD, 0x1122_3344)
250
263
  );
251
264
  }
252
265
 
253
266
  // @behavior WE-065
254
267
  #[test]
255
- fn unpack_outcome_packed_zero_decodes_to_zero_pair() {
256
- assert_eq!(unpack_outcome_packed(0), (0, 0));
268
+ fn unpack_guest_buffer_zero_decodes_to_zero_pair() {
269
+ assert_eq!(unpack_guest_buffer(0), (0, 0));
257
270
  }
258
271
  }
@@ -0,0 +1,46 @@
1
+ //! The host imports a Guest Binary resolves: the WASI preview1 set and
2
+ //! the wire ABI's `__kobako_upcall`.
3
+ //!
4
+ //! Both closures read all their state from the `Invocation` inside the
5
+ //! calling Store, never from a Runtime, so the wiring is the same for
6
+ //! every Sandbox and `crate::instance_pre` can cache what it resolves.
7
+
8
+ use wasmtime::{Caller, Linker};
9
+ use wasmtime_wasi::p1;
10
+
11
+ use crate::cache::shared_engine;
12
+ use crate::invocation::Invocation;
13
+ use crate::{dispatch, preview1};
14
+ use kobako_runtime::error::SetupError;
15
+
16
+ pub(crate) fn linker() -> Result<Linker<Invocation>, SetupError> {
17
+ let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
18
+
19
+ // Wire the wasmtime-wasi preview1 WASI imports against each
20
+ // invocation's own `Wasi` context, then answer every call that reads
21
+ // a descriptor from kobako's own table, which routes guest fd 1/2 to
22
+ // its capture pipes.
23
+ p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| {
24
+ state.wasi_mut().ctx_mut()
25
+ })
26
+ .and_then(|_| preview1::shadow(&mut linker))
27
+ .map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
28
+
29
+ // `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` per
30
+ // docs/wire/abi.md § ABI Signatures; `dispatch::handle` owns what it
31
+ // answers.
32
+ linker
33
+ .func_wrap(
34
+ "env",
35
+ "__kobako_upcall",
36
+ |mut caller: Caller<'_, Invocation>,
37
+ req_ptr: i32,
38
+ req_len: i32|
39
+ -> wasmtime::Result<i64> {
40
+ dispatch::handle(&mut caller, req_ptr, req_len)
41
+ },
42
+ )
43
+ .map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
44
+
45
+ Ok(linker)
46
+ }
@@ -1,38 +1,35 @@
1
1
  //! Per-path cache of pre-instantiated wasmtime artifacts.
2
2
  //!
3
- //! The `Linker` wiring (the WASI preview1 import set plus the
4
- //! `__kobako_dispatch` host import) and its type-check against the
5
- //! compiled Module are identical for every `Kobako::Runtime` on the
6
- //! same Guest Binary — both host closures read all their state from
7
- //! the `Invocation` inside the calling Store, never from the Runtime.
8
- //! Caching the resolved `InstancePre` per path — together with the ABI
9
- //! version check it guards (`crate::abi`) — takes both off the
10
- //! `Driver::new` hot path.
3
+ //! The host imports (`crate::imports`) and their type-check against the
4
+ //! compiled Module are identical for every `Kobako::Runtime` on the same
5
+ //! Guest Binary. Caching the resolved `InstancePre` per path — together
6
+ //! with the ABI version check it guards (`crate::abi`) — takes both off
7
+ //! the `Driver::new` hot path.
11
8
  //!
12
- //! Concurrency: see `crate::cache` — under Ruby's GVL the Mutex serves
13
- //! `Sync` bounds rather than real contention.
9
+ //! Concurrency: the same as `crate::cache` — the Mutex covers lookup and
10
+ //! insert only, and a concurrent miss builds an equivalent `InstancePre`.
14
11
 
15
12
  use std::collections::HashMap;
16
13
  use std::path::{Path, PathBuf};
17
14
  use std::sync::{Mutex, OnceLock};
15
+ use std::time::Duration;
18
16
 
19
- use wasmtime::{Caller, InstancePre, Linker};
20
- use wasmtime_wasi::p1;
17
+ use wasmtime::InstancePre;
21
18
 
22
- use crate::cache::{cached_module, shared_engine};
19
+ use crate::cache::cached_module;
23
20
  use crate::invocation::Invocation;
24
- use crate::{abi, dispatch, trap};
21
+ use crate::{abi, imports, trap};
25
22
  use kobako_runtime::error::SetupError;
26
23
 
27
24
  static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocation>>>> =
28
25
  OnceLock::new();
29
26
 
30
- /// Look up `path` in the per-path `InstancePre` cache, wiring the
31
- /// Linker, resolving the Module's imports, and verifying the artifact's
32
- /// ABI version on a miss. Compilation faults surface through
33
- /// `cached_module`; import-resolution and ABI faults return a
34
- /// runtime-dead `SetupError` (boundary → `Kobako::SetupError`).
35
- pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>, SetupError> {
27
+ /// The probe runs only on the first load of a path, so only that load's
28
+ /// `setup_timeout` bounds it; a path already verified is not set up again.
29
+ pub(crate) fn cached_instance_pre(
30
+ path: &Path,
31
+ setup_timeout: Option<Duration>,
32
+ ) -> Result<InstancePre<Invocation>, SetupError> {
36
33
  let cache = INSTANCE_PRE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
37
34
 
38
35
  if let Some(pre) = cache
@@ -45,52 +42,16 @@ pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>
45
42
  }
46
43
 
47
44
  let module = cached_module(path)?;
48
- let linker = build_linker()?;
45
+ let linker = imports::linker()?;
49
46
  let pre = linker
50
47
  .instantiate_pre(&module)
51
48
  .map_err(trap::instantiate_err)?;
52
49
  // Verify before the insert, so a rejected artifact is never
53
50
  // remembered as a usable template.
54
- abi::verify(&pre)?;
51
+ abi::verify(&pre, setup_timeout)?;
55
52
  cache
56
53
  .lock()
57
54
  .expect("instance_pre cache mutex poisoned")
58
55
  .insert(path.to_path_buf(), pre.clone());
59
56
  Ok(pre)
60
57
  }
61
-
62
- /// Build the host-import `Linker` every Guest Binary instantiates
63
- /// against.
64
- fn build_linker() -> Result<Linker<Invocation>, SetupError> {
65
- let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
66
-
67
- // Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
68
- // to the MemoryOutputPipes set up before each run via
69
- // `Driver::invoke`. The closure pulls a `&mut WasiP1Ctx` out of
70
- // Invocation; the panic semantics live inside `Invocation::wasi_mut`
71
- // so the wiring stays honest about its precondition.
72
- p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
73
- .map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
74
-
75
- // `__kobako_dispatch` host import. Signature per docs/wire-codec.md
76
- // § ABI Signatures:
77
- // (req_ptr: i32, req_len: i32) -> i64
78
- // Reads the Call bytes from guest memory and hands them —
79
- // undecoded — to the bound `DispatchHandler` (the frontend's
80
- // dispatch bridge, e.g. a Ruby Proc), then allocates a guest
81
- // buffer through `__kobako_alloc`, writes the handler's Reply
82
- // bytes there, and returns the packed `(ptr<<32)|len`. The
83
- // dispatcher returns 0 on any wire-layer fault (including no
84
- // handler bound); see `dispatch::handle`.
85
- linker
86
- .func_wrap(
87
- "env",
88
- "__kobako_dispatch",
89
- |mut caller: Caller<'_, Invocation>, req_ptr: i32, req_len: i32| -> i64 {
90
- dispatch::handle(&mut caller, req_ptr, req_len)
91
- },
92
- )
93
- .map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
94
-
95
- Ok(linker)
96
- }