kobako 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +1 -1
- data/README.md +0 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +4 -2
- data/ext/kobako/src/{wasm → runtime}/cache.rs +12 -16
- data/ext/kobako/src/runtime/capture.rs +91 -0
- data/ext/kobako/src/runtime/config.rs +26 -0
- data/ext/kobako/src/runtime/dispatch.rs +211 -0
- data/ext/kobako/src/runtime/exports.rs +51 -0
- data/ext/kobako/src/runtime/guest_mem.rs +228 -0
- data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +94 -86
- data/ext/kobako/src/runtime/trap.rs +134 -0
- data/ext/kobako/src/runtime.rs +782 -0
- data/ext/kobako/src/snapshot.rs +110 -0
- data/lib/kobako/capture.rb +11 -16
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +13 -5
- data/lib/kobako/codec/factory.rb +12 -12
- data/lib/kobako/codec/utils.rb +56 -59
- data/lib/kobako/codec.rb +6 -3
- data/lib/kobako/errors.rb +45 -28
- data/lib/kobako/fault.rb +40 -0
- data/lib/kobako/handle.rb +4 -6
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +31 -35
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +83 -72
- data/lib/kobako/sandbox_options.rb +6 -9
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +6 -7
- data/lib/kobako/snippet/source.rb +8 -8
- data/lib/kobako/snippet.rb +7 -9
- data/lib/kobako/transport/dispatcher.rb +195 -0
- data/lib/kobako/{rpc/wire_error.rb → transport/error.rb} +7 -6
- data/lib/kobako/transport/request.rb +78 -0
- data/lib/kobako/transport/response.rb +69 -0
- data/lib/kobako/transport/run.rb +141 -0
- data/lib/kobako/transport/yield.rb +91 -0
- data/lib/kobako/transport/yielder.rb +89 -0
- data/lib/kobako/transport.rb +24 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +4 -4
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/catalog/handles.rbs +19 -0
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
- data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
- data/sig/kobako/codec/decoder.rbs +2 -1
- data/sig/kobako/codec/factory.rbs +2 -2
- data/sig/kobako/codec/utils.rbs +5 -5
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +2 -3
- data/sig/kobako/namespace.rbs +19 -0
- data/sig/kobako/outcome.rbs +2 -2
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +5 -8
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/transport/dispatcher.rbs +34 -0
- data/sig/kobako/transport/error.rbs +6 -0
- data/sig/kobako/transport/request.rbs +32 -0
- data/sig/kobako/transport/response.rbs +30 -0
- data/sig/kobako/transport/run.rbs +27 -0
- data/sig/kobako/transport/yield.rbs +34 -0
- data/sig/kobako/transport/yielder.rbs +21 -0
- data/sig/kobako/transport.rbs +4 -0
- metadata +48 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -162
- data/ext/kobako/src/wasm/instance.rs +0 -873
- data/ext/kobako/src/wasm.rs +0 -126
- data/lib/kobako/handle_table.rb +0 -119
- data/lib/kobako/invocation.rb +0 -143
- data/lib/kobako/rpc/dispatcher.rb +0 -171
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -146
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/handle_table.rbs +0 -23
- data/sig/kobako/invocation.rbs +0 -25
- data/sig/kobako/rpc/dispatcher.rbs +0 -33
- data/sig/kobako/rpc/envelope.rbs +0 -51
- data/sig/kobako/rpc/fault.rbs +0 -20
- data/sig/kobako/rpc/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -31
- data/sig/kobako/rpc/wire_error.rbs +0 -6
- data/sig/kobako/wasm.rbs +0 -41
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
//! Caller-based guest linear-memory I/O shared by the host-import paths.
|
|
2
|
+
//!
|
|
3
|
+
//! Both directions of a host↔guest buffer handoff that run *inside* a wasm
|
|
4
|
+
//! callback frame go through here: writing the transport Response back
|
|
5
|
+
//! (`super::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 (`Runtime::write_envelope`) is a
|
|
9
|
+
//! separate beast — it holds the cached `Store`, not a `Caller` — and stays
|
|
10
|
+
//! in `runtime.rs`.
|
|
11
|
+
|
|
12
|
+
use wasmtime::{Caller, Extern, Memory};
|
|
13
|
+
|
|
14
|
+
use super::invocation::Invocation;
|
|
15
|
+
|
|
16
|
+
/// User-facing reason when a required guest export (the allocation or
|
|
17
|
+
/// block-yield hook) is absent or has the wrong signature — the loaded
|
|
18
|
+
/// `data/kobako.wasm` does not match the installed gem. Phrased in caller
|
|
19
|
+
/// vocabulary: the underlying hook symbol names are not actionable, and
|
|
20
|
+
/// the actionable fix is to rebuild the runtime.
|
|
21
|
+
const RUNTIME_INCOMPATIBLE: &str =
|
|
22
|
+
"the Sandbox runtime is incompatible; rebuild data/kobako.wasm against the installed version";
|
|
23
|
+
|
|
24
|
+
/// Resolve the guest's exported linear `memory`. The lookup shape (and its
|
|
25
|
+
/// diagnostic) is shared by every Caller-based path here — the write side
|
|
26
|
+
/// (`alloc_and_write`), the read side (`read`), and the yield round-trip
|
|
27
|
+
/// (`drive_yield`) — so the "no linear memory" reason lives in one place.
|
|
28
|
+
/// `read` maps the `Err` to its own `None` outcome via `.ok()`.
|
|
29
|
+
fn memory_export(caller: &mut Caller<'_, Invocation>) -> Result<Memory, &'static str> {
|
|
30
|
+
match caller.get_export("memory") {
|
|
31
|
+
Some(Extern::Memory(m)) => Ok(m),
|
|
32
|
+
_ => Err("the loaded Wasm module is not a Kobako-compatible runtime"),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Allocate `bytes.len()` bytes in guest memory via `__kobako_alloc` and
|
|
37
|
+
/// copy `bytes` in. Returns the guest pointer. Every failure path carries a
|
|
38
|
+
/// `&'static str` reason so the caller can surface a diagnostic rather than
|
|
39
|
+
/// a silent fault.
|
|
40
|
+
pub(super) fn alloc_and_write(
|
|
41
|
+
caller: &mut Caller<'_, Invocation>,
|
|
42
|
+
bytes: &[u8],
|
|
43
|
+
) -> Result<u32, &'static str> {
|
|
44
|
+
let alloc = match caller.get_export("__kobako_alloc") {
|
|
45
|
+
Some(Extern::Func(f)) => f
|
|
46
|
+
.typed::<i32, i32>(&*caller)
|
|
47
|
+
.map_err(|_| RUNTIME_INCOMPATIBLE)?,
|
|
48
|
+
_ => return Err(RUNTIME_INCOMPATIBLE),
|
|
49
|
+
};
|
|
50
|
+
let len = checked_payload_len(bytes.len())?;
|
|
51
|
+
let ptr = alloc
|
|
52
|
+
.call(&mut *caller, len)
|
|
53
|
+
.map_err(|_| "the Sandbox trapped while allocating memory for the request")?;
|
|
54
|
+
if ptr == 0 {
|
|
55
|
+
return Err("the Sandbox ran out of memory while preparing the request");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let mem = memory_export(caller)?;
|
|
59
|
+
mem.write(&mut *caller, ptr as usize, bytes)
|
|
60
|
+
.map_err(|_| "could not write the request into the Sandbox's memory")?;
|
|
61
|
+
Ok(ptr as u32)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Copy `[ptr, ptr + len)` out of the guest's linear memory as seen from
|
|
65
|
+
/// `caller`. Each failure carries a `&'static str` reason — matching the
|
|
66
|
+
/// other Caller-based ops here — so the caller surfaces a specific
|
|
67
|
+
/// diagnostic instead of a lumped one; a guest-claimed length past the
|
|
68
|
+
/// 16 MiB cap is a wire violation that names the cap (the caller walks
|
|
69
|
+
/// the trap path on any `Err`).
|
|
70
|
+
pub(super) fn read(
|
|
71
|
+
caller: &mut Caller<'_, Invocation>,
|
|
72
|
+
ptr: i32,
|
|
73
|
+
len: i32,
|
|
74
|
+
) -> Result<Vec<u8>, &'static str> {
|
|
75
|
+
let len = usize::try_from(len).map_err(|_| "the Sandbox produced a negative request length")?;
|
|
76
|
+
if len > MAX_DISPATCH_PAYLOAD {
|
|
77
|
+
return Err("request payload exceeds the 16 MiB limit");
|
|
78
|
+
}
|
|
79
|
+
let mem = memory_export(caller)?;
|
|
80
|
+
let data = mem.data(&caller);
|
|
81
|
+
let start =
|
|
82
|
+
usize::try_from(ptr).map_err(|_| "the Sandbox produced a negative request pointer")?;
|
|
83
|
+
let end = start
|
|
84
|
+
.checked_add(len)
|
|
85
|
+
.ok_or("the Sandbox produced an out-of-range request")?;
|
|
86
|
+
data.get(start..end)
|
|
87
|
+
.map(|s| s.to_vec())
|
|
88
|
+
.ok_or("the Sandbox produced an out-of-bounds request")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Single-dispatch payload cap: 16 MiB in either direction
|
|
92
|
+
/// (SPEC.md § Wire Codec; docs/wire-codec.md § ABI). A host↔guest
|
|
93
|
+
/// transfer larger than this is a wire violation — the Host Gem walks
|
|
94
|
+
/// the trap path rather than allocate or copy the buffer. Held as a
|
|
95
|
+
/// constant for now; a future SPEC anchor may let the Host App raise it.
|
|
96
|
+
pub(super) const MAX_DISPATCH_PAYLOAD: usize = 16 * 1024 * 1024;
|
|
97
|
+
|
|
98
|
+
/// Validate a payload length against `MAX_DISPATCH_PAYLOAD` and narrow it
|
|
99
|
+
/// to `i32` — the signed wasm ABI width for the guest buffer parameters.
|
|
100
|
+
/// Every host *write* boundary (`alloc_and_write`, `drive_yield`,
|
|
101
|
+
/// `Runtime::write_envelope`) routes its length through here so the
|
|
102
|
+
/// wire-violation reason is uniform; the *read* boundaries compare
|
|
103
|
+
/// against `MAX_DISPATCH_PAYLOAD` directly.
|
|
104
|
+
pub(super) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
|
|
105
|
+
if len > MAX_DISPATCH_PAYLOAD {
|
|
106
|
+
return Err("payload exceeds the 16 MiB limit");
|
|
107
|
+
}
|
|
108
|
+
// The cap above sits below `i32::MAX`, so this conversion cannot wrap.
|
|
109
|
+
i32::try_from(len).map_err(|_| "payload exceeds the 16 MiB limit")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Compute the half-open range `[ptr, ptr + len)` for a guest linear-memory
|
|
113
|
+
/// copy, validating that the arithmetic does not overflow and the range
|
|
114
|
+
/// fits inside `mem_size`. Shared by `Runtime::write_envelope` (write side)
|
|
115
|
+
/// and `Runtime::fetch_outcome_bytes` (read side).
|
|
116
|
+
pub(super) fn guest_buffer_range(
|
|
117
|
+
ptr: usize,
|
|
118
|
+
len: usize,
|
|
119
|
+
mem_size: usize,
|
|
120
|
+
) -> Result<core::ops::Range<usize>, &'static str> {
|
|
121
|
+
let end = ptr.checked_add(len).ok_or("ptr + len overflow")?;
|
|
122
|
+
if end > mem_size {
|
|
123
|
+
return Err("range exceeds Sandbox memory size");
|
|
124
|
+
}
|
|
125
|
+
Ok(ptr..end)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Unpack the `(ptr, len)` u64 returned by `__kobako_take_outcome`:
|
|
129
|
+
/// high 32 bits = ptr, low 32 bits = len. Mirrors the guest-side
|
|
130
|
+
/// `crate::abi::unpack_u64` in `wasm/kobako-wasm/src/abi.rs`.
|
|
131
|
+
pub(super) fn unpack_outcome_packed(packed: u64) -> (usize, usize) {
|
|
132
|
+
let ptr = (packed >> 32) as u32 as usize;
|
|
133
|
+
let len = packed as u32 as usize;
|
|
134
|
+
(ptr, len)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Allocate `args.len()` bytes in guest memory, copy the args payload in,
|
|
138
|
+
/// call `__kobako_yield_to_block(ptr, len)`, then read the response slice
|
|
139
|
+
/// the guest produced and return it. Mirrors `dispatch::write_response`'s
|
|
140
|
+
/// allocator dance but in the opposite direction — the host is the
|
|
141
|
+
/// *initiator* of this round-trip, not the responder.
|
|
142
|
+
pub(super) fn drive_yield(
|
|
143
|
+
caller: &mut Caller<'_, Invocation>,
|
|
144
|
+
args: &[u8],
|
|
145
|
+
) -> Result<Vec<u8>, &'static str> {
|
|
146
|
+
let len_i32 = checked_payload_len(args.len())?;
|
|
147
|
+
let req_ptr = alloc_and_write(caller, args)? as i32;
|
|
148
|
+
|
|
149
|
+
let yield_fn = match caller.get_export("__kobako_yield_to_block") {
|
|
150
|
+
Some(Extern::Func(f)) => f
|
|
151
|
+
.typed::<(i32, i32), u64>(&*caller)
|
|
152
|
+
.map_err(|_| RUNTIME_INCOMPATIBLE)?,
|
|
153
|
+
_ => return Err(RUNTIME_INCOMPATIBLE),
|
|
154
|
+
};
|
|
155
|
+
let packed = yield_fn
|
|
156
|
+
.call(&mut *caller, (req_ptr, len_i32))
|
|
157
|
+
.map_err(|_| "the Sandbox trapped while invoking a block")?;
|
|
158
|
+
let (resp_ptr, resp_len) = unpack_outcome_packed(packed);
|
|
159
|
+
if resp_len == 0 {
|
|
160
|
+
return Err("the Sandbox returned an empty block result");
|
|
161
|
+
}
|
|
162
|
+
if resp_len > MAX_DISPATCH_PAYLOAD {
|
|
163
|
+
return Err("block result payload exceeds the 16 MiB limit");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let mem = memory_export(caller)?;
|
|
167
|
+
let data = mem.data(&caller);
|
|
168
|
+
let range = guest_buffer_range(resp_ptr, resp_len, data.len())
|
|
169
|
+
.map_err(|_| "the Sandbox returned an out-of-bounds block result")?;
|
|
170
|
+
Ok(data[range].to_vec())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[cfg(test)]
|
|
174
|
+
mod tests {
|
|
175
|
+
use super::{
|
|
176
|
+
checked_payload_len, guest_buffer_range, unpack_outcome_packed, MAX_DISPATCH_PAYLOAD,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
#[test]
|
|
180
|
+
fn checked_payload_len_accepts_zero_and_the_cap() {
|
|
181
|
+
assert_eq!(checked_payload_len(0), Ok(0));
|
|
182
|
+
assert_eq!(
|
|
183
|
+
checked_payload_len(MAX_DISPATCH_PAYLOAD),
|
|
184
|
+
Ok(MAX_DISPATCH_PAYLOAD as i32)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn checked_payload_len_rejects_past_the_cap() {
|
|
190
|
+
assert!(checked_payload_len(MAX_DISPATCH_PAYLOAD + 1).is_err());
|
|
191
|
+
assert!(checked_payload_len(usize::MAX).is_err());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[test]
|
|
195
|
+
fn guest_buffer_range_returns_half_open_range() {
|
|
196
|
+
assert_eq!(guest_buffer_range(10, 5, 100), Ok(10..15));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[test]
|
|
200
|
+
fn guest_buffer_range_accepts_zero_length_at_any_in_bounds_ptr() {
|
|
201
|
+
assert_eq!(guest_buffer_range(0, 0, 0), Ok(0..0));
|
|
202
|
+
assert_eq!(guest_buffer_range(42, 0, 100), Ok(42..42));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[test]
|
|
206
|
+
fn guest_buffer_range_rejects_ptr_plus_len_overflow() {
|
|
207
|
+
assert!(guest_buffer_range(usize::MAX, 1, usize::MAX).is_err());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[test]
|
|
211
|
+
fn guest_buffer_range_rejects_end_past_memory() {
|
|
212
|
+
assert!(guest_buffer_range(10, 100, 50).is_err());
|
|
213
|
+
assert_eq!(guest_buffer_range(0, 50, 50), Ok(0..50));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn unpack_outcome_packed_extracts_high_ptr_low_len() {
|
|
218
|
+
assert_eq!(
|
|
219
|
+
unpack_outcome_packed(0xAABB_CCDD_1122_3344),
|
|
220
|
+
(0xAABB_CCDD, 0x1122_3344)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn unpack_outcome_packed_zero_decodes_to_zero_pair() {
|
|
226
|
+
assert_eq!(unpack_outcome_packed(0), (0, 0));
|
|
227
|
+
}
|
|
228
|
+
}
|