kobako 0.27.0 → 0.28.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +71 -0
  4. data/Cargo.lock +117 -140
  5. data/README.md +100 -125
  6. data/ROADMAP.md +23 -22
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +31 -0
  9. data/crates/kobako-runtime/Cargo.toml +2 -2
  10. data/crates/kobako-runtime/README.md +4 -3
  11. data/crates/kobako-runtime/src/error.rs +8 -9
  12. data/crates/kobako-runtime/src/lib.rs +1 -1
  13. data/crates/kobako-runtime/src/profile.rs +1 -1
  14. data/crates/kobako-runtime/src/runtime.rs +31 -17
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -10
  16. data/crates/kobako-runtime/src/yielder.rs +9 -6
  17. data/crates/kobako-transport/CHANGELOG.md +28 -0
  18. data/crates/kobako-transport/Cargo.toml +1 -1
  19. data/crates/kobako-transport/README.md +21 -12
  20. data/crates/kobako-transport/src/abi.rs +11 -14
  21. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  22. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  23. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  24. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  25. data/crates/kobako-transport/src/envelope/reply.rs +77 -53
  26. data/crates/kobako-transport/src/envelope/request.rs +295 -0
  27. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  28. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  29. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  30. data/crates/kobako-transport/src/envelope.rs +19 -12
  31. data/crates/kobako-transport/src/lib.rs +0 -2
  32. data/crates/kobako-wasmtime/CHANGELOG.md +44 -0
  33. data/crates/kobako-wasmtime/Cargo.toml +7 -5
  34. data/crates/kobako-wasmtime/README.md +2 -2
  35. data/crates/kobako-wasmtime/src/abi.rs +5 -8
  36. data/crates/kobako-wasmtime/src/ambient.rs +2 -2
  37. data/crates/kobako-wasmtime/src/cache.rs +25 -59
  38. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  39. data/crates/kobako-wasmtime/src/config.rs +11 -14
  40. data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
  41. data/crates/kobako-wasmtime/src/driver.rs +133 -86
  42. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  43. data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
  44. data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
  45. data/crates/kobako-wasmtime/src/invocation.rs +55 -372
  46. data/crates/kobako-wasmtime/src/io.rs +286 -0
  47. data/crates/kobako-wasmtime/src/lib.rs +3 -24
  48. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  49. data/crates/kobako-wasmtime/src/trap.rs +32 -45
  50. data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
  51. data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
  52. data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
  53. data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
  54. data/data/kobako.wasm +0 -0
  55. data/ext/kobako/Cargo.toml +2 -2
  56. data/ext/kobako/src/runtime/bridge.rs +38 -47
  57. data/ext/kobako/src/runtime/errors.rs +26 -32
  58. data/ext/kobako/src/runtime/gvl.rs +4 -12
  59. data/ext/kobako/src/runtime.rs +169 -212
  60. data/lib/kobako/capture.rb +1 -4
  61. data/lib/kobako/catalog/extensions.rb +21 -59
  62. data/lib/kobako/catalog/flags.rb +41 -0
  63. data/lib/kobako/catalog/handles.rb +7 -54
  64. data/lib/kobako/catalog/services.rb +7 -61
  65. data/lib/kobako/catalog/snippets.rb +42 -66
  66. data/lib/kobako/catalog.rb +2 -10
  67. data/lib/kobako/codec/decoder.rb +9 -28
  68. data/lib/kobako/codec/encoder.rb +3 -22
  69. data/lib/kobako/codec/error.rb +6 -27
  70. data/lib/kobako/codec/ext_types.rb +72 -94
  71. data/lib/kobako/codec/handle_walk.rb +65 -130
  72. data/lib/kobako/codec/nesting.rb +3 -8
  73. data/lib/kobako/codec/state.rb +4 -10
  74. data/lib/kobako/codec/utils.rb +6 -34
  75. data/lib/kobako/codec.rb +3 -22
  76. data/lib/kobako/constant_name.rb +8 -0
  77. data/lib/kobako/context.rb +51 -112
  78. data/lib/kobako/errors.rb +51 -127
  79. data/lib/kobako/execution.rb +16 -21
  80. data/lib/kobako/extension.rb +23 -40
  81. data/lib/kobako/handle.rb +7 -12
  82. data/lib/kobako/outcome.rb +51 -80
  83. data/lib/kobako/payload/arguments.rb +1 -16
  84. data/lib/kobako/payload.rb +4 -12
  85. data/lib/kobako/pool.rb +23 -53
  86. data/lib/kobako/runtime.rb +7 -21
  87. data/lib/kobako/sandbox.rb +96 -133
  88. data/lib/kobako/sandbox_options.rb +38 -84
  89. data/lib/kobako/snippet/binary.rb +1 -16
  90. data/lib/kobako/snippet/source.rb +1 -16
  91. data/lib/kobako/snippet.rb +1 -9
  92. data/lib/kobako/transport/call.rb +1 -14
  93. data/lib/kobako/transport/dispatcher.rb +112 -195
  94. data/lib/kobako/transport/error.rb +6 -20
  95. data/lib/kobako/transport/exposure.rb +5 -10
  96. data/lib/kobako/transport/reflection.rb +39 -50
  97. data/lib/kobako/transport/run.rb +17 -71
  98. data/lib/kobako/transport/yielder.rb +21 -73
  99. data/lib/kobako/transport.rb +3 -15
  100. data/lib/kobako/unresolved.rb +4 -11
  101. data/lib/kobako/usage.rb +2 -6
  102. data/lib/kobako/version.rb +1 -1
  103. data/sig/kobako/catalog/extensions.rbs +2 -2
  104. data/sig/kobako/catalog/flags.rbs +17 -0
  105. data/sig/kobako/catalog/services.rbs +0 -6
  106. data/sig/kobako/catalog/snippets.rbs +4 -4
  107. data/sig/kobako/codec/ext_types.rbs +10 -8
  108. data/sig/kobako/codec/handle_walk.rbs +9 -7
  109. data/sig/kobako/codec/utils.rbs +2 -2
  110. data/sig/kobako/codec.rbs +2 -2
  111. data/sig/kobako/constant_name.rbs +3 -0
  112. data/sig/kobako/context.rbs +2 -3
  113. data/sig/kobako/errors.rbs +6 -0
  114. data/sig/kobako/outcome.rbs +6 -8
  115. data/sig/kobako/runtime.rbs +12 -8
  116. data/sig/kobako/sandbox.rbs +4 -0
  117. data/sig/kobako/sandbox_options.rbs +2 -6
  118. data/sig/kobako/transport/dispatcher.rbs +14 -18
  119. data/sig/kobako/transport/reflection.rbs +5 -3
  120. data/sig/kobako/transport/run.rbs +1 -3
  121. data/sig/kobako/transport/yielder.rbs +3 -3
  122. data/sig/kobako/transport.rbs +7 -6
  123. metadata +16 -4
  124. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  125. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  126. data/rust-toolchain.toml +0 -15
@@ -1,130 +1,153 @@
1
- //! Caller-based guest linear-memory I/O shared by the host-import paths.
1
+ //! Guest linear-memory I/O for every host↔guest buffer handoff, whether
2
+ //! the invocation body drives it through its `Store` or a host import
3
+ //! drives it through its `Caller`.
2
4
  //!
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`.
5
+ //! The two shared steps answer an error kind, and each caller words it,
6
+ //! because depth decides what the same problem means: before the export
7
+ //! call it is a `Trap` or an intact runtime, inside a callback it is a
8
+ //! wire failure the guest receives.
11
9
 
12
- use wasmtime::{Caller, Extern, Memory};
10
+ use wasmtime::{AsContext, AsContextMut, Caller, Memory};
13
11
 
12
+ use crate::exports::{Exports, RUNTIME_INCOMPATIBLE};
14
13
  use crate::invocation::Invocation;
15
- use kobako_runtime::error::Trap;
16
- use kobako_runtime::yielder::Yielder;
17
14
  use kobako_transport::abi::{unpack_ptr_len, MAX_DISPATCH_PAYLOAD};
15
+ use kobako_transport::envelope::Downcall;
18
16
 
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";
17
+ // The size-limit messages here and in `io` name the cap as 16 MiB, so
18
+ // moving the cap fails the build until they are reworded with it.
19
+ const _: () = assert!(MAX_DISPATCH_PAYLOAD == 16 << 20);
47
20
 
48
21
  /// 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.
22
+ /// Kobako-shaped runtime at all" failure mode, such as an instance with no
23
+ /// `memory` export.
52
24
  pub(crate) const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
53
25
  "the loaded Wasm module is not a Kobako-compatible runtime";
54
26
 
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
- }
27
+ /// Why a host write into guest memory failed.
28
+ pub(crate) enum WriteError {
29
+ TooLarge,
30
+ /// The allocator or the memory is missing or mistyped; carries the
31
+ /// message for it.
32
+ Incompatible(&'static str),
33
+ Trapped(wasmtime::Error),
34
+ /// The allocator ran and answered 0.
35
+ OutOfMemory,
36
+ OutOfBounds,
65
37
  }
66
38
 
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.
39
+ /// Why a host read of a guest-named buffer failed.
40
+ pub(crate) enum ReadError {
41
+ TooLarge,
42
+ OutOfBounds,
43
+ }
44
+
45
+ /// Allocate a guest buffer through `__kobako_alloc` and copy `bytes` into
46
+ /// it, answering the buffer's address.
71
47
  pub(crate) fn alloc_and_write(
72
- caller: &mut Caller<'_, Invocation>,
48
+ mut ctx: impl AsContextMut,
49
+ exports: &Exports,
73
50
  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())?;
51
+ ) -> Result<u32, WriteError> {
52
+ let len = checked_payload_len(bytes.len()).map_err(|_| WriteError::TooLarge)?;
53
+ let alloc = exports
54
+ .alloc
55
+ .as_ref()
56
+ .ok_or(WriteError::Incompatible(RUNTIME_INCOMPATIBLE))?;
57
+ let memory = exports
58
+ .memory
59
+ .ok_or(WriteError::Incompatible(SANDBOX_RUNTIME_NOT_KOBAKO))?;
82
60
  let ptr = alloc
83
- .call(&mut *caller, len)
84
- .map_err(|_| "the Sandbox trapped while allocating memory for the request")?;
61
+ .call(&mut ctx, len as u32)
62
+ .map_err(WriteError::Trapped)?;
85
63
  if ptr == 0 {
86
- return Err("the Sandbox ran out of memory while preparing the request");
64
+ return Err(WriteError::OutOfMemory);
87
65
  }
66
+ memory
67
+ .write(&mut ctx, ptr as usize, bytes)
68
+ .map_err(|_| WriteError::OutOfBounds)?;
69
+ Ok(ptr)
70
+ }
88
71
 
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)
72
+ /// Copy out the buffer the guest names; a length past the 16 MiB cap is
73
+ /// refused before memory is touched.
74
+ pub(crate) fn read_buffer(
75
+ ctx: impl AsContext,
76
+ memory: Memory,
77
+ ptr: usize,
78
+ len: usize,
79
+ ) -> Result<Vec<u8>, ReadError> {
80
+ if len > MAX_DISPATCH_PAYLOAD {
81
+ return Err(ReadError::TooLarge);
82
+ }
83
+ let data = memory.data(&ctx);
84
+ let range = guest_buffer_range(ptr, len, data.len()).map_err(|_| ReadError::OutOfBounds)?;
85
+ Ok(data[range].to_vec())
93
86
  }
94
87
 
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(
88
+ /// The handles of the instance a host import runs inside.
89
+ fn callback_exports(caller: &Caller<'_, Invocation>) -> Result<Exports, &'static str> {
90
+ caller.data().exports().ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)
91
+ }
92
+
93
+ /// Keep a trap the guest raised during a callback into it for the dispatch
94
+ /// import to end the invocation with, and answer the callback's own reason.
95
+ fn trapped(
102
96
  caller: &mut Caller<'_, Invocation>,
103
- ptr: i32,
104
- len: i32,
97
+ trap: wasmtime::Error,
98
+ reason: &'static str,
99
+ ) -> &'static str {
100
+ caller.data_mut().record_reentry_trap(trap);
101
+ reason
102
+ }
103
+
104
+ /// The request the guest hands the upcall import. Its `(ptr, len)` are
105
+ /// guest addresses, so they are read unsigned.
106
+ pub(crate) fn read_request(
107
+ caller: &mut Caller<'_, Invocation>,
108
+ req_ptr: i32,
109
+ req_len: i32,
105
110
  ) -> 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");
111
+ let memory = callback_exports(caller)?
112
+ .memory
113
+ .ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
114
+ read_buffer(
115
+ &*caller,
116
+ memory,
117
+ req_ptr as u32 as usize,
118
+ req_len as u32 as usize,
119
+ )
120
+ .map_err(|err| match err {
121
+ ReadError::TooLarge => "request payload exceeds the 16 MiB limit",
122
+ ReadError::OutOfBounds => "the Sandbox produced an out-of-bounds request",
123
+ })
124
+ }
125
+
126
+ /// Write `bytes` into the guest from inside a callback. A guest that has
127
+ /// already trapped is not called again.
128
+ pub(crate) fn write_from_callback(
129
+ caller: &mut Caller<'_, Invocation>,
130
+ bytes: &[u8],
131
+ ) -> Result<u32, &'static str> {
132
+ if caller.data().reentry_trapped() {
133
+ return Err("the Sandbox already trapped during this invocation");
109
134
  }
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")
135
+ let exports = callback_exports(caller)?;
136
+ alloc_and_write(&mut *caller, &exports, bytes).map_err(|err| match err {
137
+ WriteError::TooLarge => "payload exceeds the 16 MiB limit",
138
+ WriteError::Incompatible(message) => message,
139
+ WriteError::Trapped(trap) => trapped(
140
+ caller,
141
+ trap,
142
+ "the Sandbox trapped while allocating memory for the request",
143
+ ),
144
+ WriteError::OutOfMemory => "the Sandbox ran out of memory while preparing the request",
145
+ WriteError::OutOfBounds => "could not write the request into the Sandbox's memory",
146
+ })
120
147
  }
121
148
 
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.
149
+ /// Every host write boundary routes its length through here, so the
150
+ /// wire-violation reason is uniform.
128
151
  pub(crate) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
129
152
  if len > MAX_DISPATCH_PAYLOAD {
130
153
  return Err("payload exceeds the 16 MiB limit");
@@ -133,10 +156,6 @@ pub(crate) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
133
156
  i32::try_from(len).map_err(|_| "payload exceeds the 16 MiB limit")
134
157
  }
135
158
 
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
159
  pub(crate) fn guest_buffer_range(
141
160
  ptr: usize,
142
161
  len: usize,
@@ -149,53 +168,41 @@ pub(crate) fn guest_buffer_range(
149
168
  Ok(ptr..end)
150
169
  }
151
170
 
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) {
171
+ /// The `(ptr, len)` the guest packs for any answer it hands back, the
172
+ /// outcome and a block's result alike.
173
+ pub(crate) fn unpack_guest_buffer(packed: u64) -> (usize, usize) {
155
174
  let (ptr, len) = unpack_ptr_len(packed);
156
175
  (ptr as usize, len as usize)
157
176
  }
158
177
 
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
178
  pub(crate) fn drive_yield(
165
179
  caller: &mut Caller<'_, Invocation>,
166
180
  args: &[u8],
167
181
  ) -> 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;
182
+ let request = Downcall::Yield(args.to_vec()).encode();
183
+ let len_i32 = checked_payload_len(request.len())?;
184
+ let req_ptr = write_from_callback(caller, &request)? as i32;
170
185
 
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
- };
186
+ let exports = callback_exports(caller)?;
187
+ let yield_fn = exports.downcall.ok_or(RUNTIME_INCOMPATIBLE)?;
188
+ let memory = exports.memory.ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
177
189
  let packed = yield_fn
178
190
  .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 {
191
+ .map_err(|trap| trapped(caller, trap, "the Sandbox trapped while invoking a block"))?;
192
+ let (reply_ptr, reply_len) = unpack_guest_buffer(packed);
193
+ if reply_len == 0 {
182
194
  return Err("the Sandbox returned an empty block result");
183
195
  }
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())
196
+ read_buffer(&*caller, memory, reply_ptr, reply_len).map_err(|err| match err {
197
+ ReadError::TooLarge => "block result payload exceeds the 16 MiB limit",
198
+ ReadError::OutOfBounds => "the Sandbox returned an out-of-bounds block result",
199
+ })
193
200
  }
194
201
 
195
202
  #[cfg(test)]
196
203
  mod tests {
197
204
  use super::{
198
- checked_payload_len, guest_buffer_range, unpack_outcome_packed, MAX_DISPATCH_PAYLOAD,
205
+ checked_payload_len, guest_buffer_range, unpack_guest_buffer, MAX_DISPATCH_PAYLOAD,
199
206
  };
200
207
 
201
208
  // @behavior WE-059
@@ -243,16 +250,16 @@ mod tests {
243
250
 
244
251
  // @behavior WE-001
245
252
  #[test]
246
- fn unpack_outcome_packed_extracts_high_ptr_low_len() {
253
+ fn unpack_guest_buffer_extracts_high_ptr_low_len() {
247
254
  assert_eq!(
248
- unpack_outcome_packed(0xAABB_CCDD_1122_3344),
255
+ unpack_guest_buffer(0xAABB_CCDD_1122_3344),
249
256
  (0xAABB_CCDD, 0x1122_3344)
250
257
  );
251
258
  }
252
259
 
253
260
  // @behavior WE-065
254
261
  #[test]
255
- fn unpack_outcome_packed_zero_decodes_to_zero_pair() {
256
- assert_eq!(unpack_outcome_packed(0), (0, 0));
262
+ fn unpack_guest_buffer_zero_decodes_to_zero_pair() {
263
+ assert_eq!(unpack_guest_buffer(0), (0, 0));
257
264
  }
258
265
  }
@@ -1,7 +1,7 @@
1
1
  //! Per-path cache of pre-instantiated wasmtime artifacts.
2
2
  //!
3
3
  //! The `Linker` wiring (the WASI preview1 import set plus the
4
- //! `__kobako_dispatch` host import) and its type-check against the
4
+ //! `__kobako_upcall` host import) and its type-check against the
5
5
  //! compiled Module are identical for every `Kobako::Runtime` on the
6
6
  //! same Guest Binary — both host closures read all their state from
7
7
  //! the `Invocation` inside the calling Store, never from the Runtime.
@@ -9,8 +9,8 @@
9
9
  //! version check it guards (`crate::abi`) — takes both off the
10
10
  //! `Driver::new` hot path.
11
11
  //!
12
- //! Concurrency: see `crate::cache` — under Ruby's GVL the Mutex serves
13
- //! `Sync` bounds rather than real contention.
12
+ //! Concurrency: the same as `crate::cache` — the Mutex covers lookup and
13
+ //! insert only, and a concurrent miss builds an equivalent `InstancePre`.
14
14
 
15
15
  use std::collections::HashMap;
16
16
  use std::path::{Path, PathBuf};
@@ -27,11 +27,6 @@ use kobako_runtime::error::SetupError;
27
27
  static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocation>>>> =
28
28
  OnceLock::new();
29
29
 
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
30
  pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>, SetupError> {
36
31
  let cache = INSTANCE_PRE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
37
32
 
@@ -59,8 +54,6 @@ pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>
59
54
  Ok(pre)
60
55
  }
61
56
 
62
- /// Build the host-import `Linker` every Guest Binary instantiates
63
- /// against.
64
57
  fn build_linker() -> Result<Linker<Invocation>, SetupError> {
65
58
  let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
66
59
 
@@ -72,21 +65,17 @@ fn build_linker() -> Result<Linker<Invocation>, SetupError> {
72
65
  p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
73
66
  .map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
74
67
 
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`.
68
+ // `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` per
69
+ // docs/wire/abi.md § ABI Signatures; `dispatch::handle` owns what it
70
+ // answers.
85
71
  linker
86
72
  .func_wrap(
87
73
  "env",
88
- "__kobako_dispatch",
89
- |mut caller: Caller<'_, Invocation>, req_ptr: i32, req_len: i32| -> i64 {
74
+ "__kobako_upcall",
75
+ |mut caller: Caller<'_, Invocation>,
76
+ req_ptr: i32,
77
+ req_len: i32|
78
+ -> wasmtime::Result<i64> {
90
79
  dispatch::handle(&mut caller, req_ptr, req_len)
91
80
  },
92
81
  )