kobako 0.12.1 → 0.13.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 -1
- data/CHANGELOG.md +22 -0
- data/Cargo.lock +138 -136
- data/Cargo.toml +6 -2
- data/README.md +3 -1
- data/crates/kobako-runtime/CHANGELOG.md +16 -0
- data/crates/kobako-runtime/Cargo.toml +23 -0
- data/crates/kobako-runtime/README.md +34 -0
- data/crates/kobako-runtime/src/dispatch.rs +22 -0
- data/crates/kobako-runtime/src/error.rs +64 -0
- data/crates/kobako-runtime/src/lib.rs +18 -0
- data/crates/kobako-runtime/src/profile.rs +35 -0
- data/crates/kobako-runtime/src/runtime.rs +57 -0
- data/crates/kobako-runtime/src/snapshot.rs +46 -0
- data/crates/kobako-runtime/src/yielder.rs +22 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +16 -0
- data/crates/kobako-wasmtime/Cargo.toml +62 -0
- data/crates/kobako-wasmtime/README.md +32 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/ambient.rs +8 -5
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/cache.rs +30 -41
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/capture.rs +2 -2
- data/crates/kobako-wasmtime/src/config.rs +33 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
- data/crates/kobako-wasmtime/src/driver.rs +297 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
- data/crates/kobako-wasmtime/src/frames.rs +276 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/guest_mem.rs +38 -15
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/instance_pre.rs +13 -21
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/invocation.rs +54 -49
- data/crates/kobako-wasmtime/src/lib.rs +47 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/trap.rs +29 -35
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +9 -32
- data/ext/kobako/src/lib.rs +0 -2
- data/ext/kobako/src/runtime/bridge.rs +150 -0
- data/ext/kobako/src/runtime/errors.rs +45 -13
- data/ext/kobako/src/runtime.rs +246 -420
- data/lib/kobako/catalog/handles.rb +3 -3
- data/lib/kobako/catalog/namespaces.rb +4 -0
- data/lib/kobako/catalog/snippets.rb +4 -0
- data/lib/kobako/codec/encoder.rb +5 -1
- data/lib/kobako/codec/factory.rb +41 -13
- data/lib/kobako/codec/handle_walk.rb +4 -0
- data/lib/kobako/codec.rb +1 -1
- data/lib/kobako/errors.rb +18 -16
- data/lib/kobako/sandbox.rb +71 -39
- data/lib/kobako/sandbox_options.rb +71 -13
- data/lib/kobako/transport/dispatcher.rb +2 -2
- data/lib/kobako/transport/response.rb +14 -14
- data/lib/kobako/transport/run.rb +2 -6
- data/lib/kobako/transport/yield.rb +1 -1
- data/lib/kobako/transport/yielder.rb +2 -2
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +0 -1
- data/release-please-config.json +78 -3
- data/sig/kobako/codec/factory.rbs +3 -0
- data/sig/kobako/errors.rbs +7 -14
- data/sig/kobako/runtime.rbs +16 -6
- data/sig/kobako/sandbox.rbs +11 -7
- data/sig/kobako/sandbox_options.rbs +15 -4
- data/sig/kobako/transport/dispatcher.rbs +1 -1
- data/sig/kobako/transport/run.rbs +2 -2
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -0
- metadata +28 -15
- data/ext/kobako/src/runtime/config.rs +0 -25
- data/ext/kobako/src/runtime/dispatch.rs +0 -211
- data/ext/kobako/src/runtime/frames.rs +0 -188
- data/ext/kobako/src/snapshot.rs +0 -110
- data/lib/kobako/snapshot.rb +0 -38
- data/sig/kobako/snapshot.rbs +0 -15
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
//! Per-invocation byte-shuttle between the host and guest linear memory:
|
|
2
|
+
//! it resolves the required `memory` / ABI-export handles, writes the
|
|
3
|
+
//! `#run` envelope into a freshly allocated guest buffer, builds the
|
|
4
|
+
//! stdin frame stream plus stdout / stderr capture pipes for the WASI
|
|
5
|
+
//! context, and reads the OUTCOME_BUFFER back out. The driver owns no
|
|
6
|
+
//! wire codec — these helpers move raw bytes; the frontend decodes them.
|
|
7
|
+
|
|
8
|
+
use wasmtime::{AsContextMut, Memory, Store as WtStore, TypedFunc};
|
|
9
|
+
use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
|
|
10
|
+
use wasmtime_wasi::WasiCtxBuilder;
|
|
11
|
+
|
|
12
|
+
use crate::config::Config;
|
|
13
|
+
use crate::exports::Exports;
|
|
14
|
+
use crate::invocation::Invocation;
|
|
15
|
+
use crate::{ambient, capture, guest_mem};
|
|
16
|
+
use kobako_runtime::error::{Error, SetupError, Trap};
|
|
17
|
+
use kobako_runtime::profile::Profile;
|
|
18
|
+
|
|
19
|
+
/// Return the resolved `memory` export handle, or a `Trap` when the loaded
|
|
20
|
+
/// module exports no linear memory — the "not a Kobako-shaped runtime"
|
|
21
|
+
/// failure mode (`SANDBOX_RUNTIME_NOT_KOBAKO`).
|
|
22
|
+
fn require_memory(exports: &Exports) -> Result<Memory, Trap> {
|
|
23
|
+
exports
|
|
24
|
+
.memory
|
|
25
|
+
.ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_NOT_KOBAKO.to_string()))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Allocate a `len`-byte buffer in guest linear memory via
|
|
29
|
+
/// `__kobako_alloc`, copy `envelope` into it, and return `(ptr, len)`
|
|
30
|
+
/// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
|
|
31
|
+
/// Returns a `Trap` when the allocation hook is missing or itself traps
|
|
32
|
+
/// (an engine fault), and a runtime-intact `SetupError` when the hook runs
|
|
33
|
+
/// but cannot reserve the buffer (`__kobako_alloc` returns 0). The ext
|
|
34
|
+
/// boundary maps these to `Kobako::TrapError` / `Kobako::SandboxError`.
|
|
35
|
+
pub(crate) fn write_envelope(
|
|
36
|
+
store: &mut WtStore<Invocation>,
|
|
37
|
+
exports: &Exports,
|
|
38
|
+
envelope: &[u8],
|
|
39
|
+
) -> Result<(i32, i32), Error> {
|
|
40
|
+
let len_i32 = guest_mem::checked_payload_len(envelope.len())
|
|
41
|
+
.map_err(|msg| Trap::Other(msg.to_string()))?;
|
|
42
|
+
|
|
43
|
+
let alloc = require_export(exports.alloc.as_ref())?;
|
|
44
|
+
let memory = require_memory(exports)?;
|
|
45
|
+
|
|
46
|
+
let ptr = alloc
|
|
47
|
+
.call(store.as_context_mut(), envelope.len() as u32)
|
|
48
|
+
.map_err(|e| Trap::Other(format!("failed to allocate input buffer: {e}")))?;
|
|
49
|
+
if ptr == 0 {
|
|
50
|
+
return Err(SetupError::Intact(
|
|
51
|
+
"could not allocate input buffer (out of memory)".to_string(),
|
|
52
|
+
)
|
|
53
|
+
.into());
|
|
54
|
+
}
|
|
55
|
+
let data = memory.data_mut(store.as_context_mut());
|
|
56
|
+
let range = guest_mem::guest_buffer_range(ptr as usize, envelope.len(), data.len())
|
|
57
|
+
.map_err(|msg| Trap::Other(msg.to_string()))?;
|
|
58
|
+
data[range].copy_from_slice(envelope);
|
|
59
|
+
|
|
60
|
+
Ok((ptr as i32, len_i32))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Build the per-invocation WASI context with stdin carrying every frame
|
|
64
|
+
/// in `frames` (each prefixed by its 4-byte big-endian u32 length —
|
|
65
|
+
/// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
|
|
66
|
+
/// pipes, and install it on the invocation's Store. `#eval` passes three
|
|
67
|
+
/// frames (preamble, source, snippets), `#run` passes two (preamble,
|
|
68
|
+
/// snippets — the invocation envelope arrives via linear memory
|
|
69
|
+
/// instead). Each output pipe is sized at `cap + 1` so
|
|
70
|
+
/// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
|
|
71
|
+
/// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
|
|
72
|
+
/// rely on `memory_limit` for the real ceiling.
|
|
73
|
+
/// Returns a `Trap` when any frame exceeds the 16 MiB cap that keeps its
|
|
74
|
+
/// `u32` length prefix from wrapping (boundary → `Kobako::TrapError`).
|
|
75
|
+
pub(crate) fn install_wasi_frames(
|
|
76
|
+
store: &mut WtStore<Invocation>,
|
|
77
|
+
config: &Config,
|
|
78
|
+
frames: &[&[u8]],
|
|
79
|
+
) -> Result<(), Trap> {
|
|
80
|
+
// Every frame carries the same 16 MiB cap as the `#run` envelope
|
|
81
|
+
// (`write_envelope`): the length prefix is a `u32`, so a frame past
|
|
82
|
+
// the cap would silently wrap and corrupt the stdin frame stream.
|
|
83
|
+
for &frame in frames {
|
|
84
|
+
guest_mem::checked_payload_len(frame.len()).map_err(|msg| Trap::Other(msg.to_string()))?;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let total: usize = frames.iter().map(|&f| 4 + f.len()).sum();
|
|
88
|
+
let mut stdin_content: Vec<u8> = Vec::with_capacity(total);
|
|
89
|
+
for &frame in frames {
|
|
90
|
+
stdin_content.extend_from_slice(&(frame.len() as u32).to_be_bytes());
|
|
91
|
+
stdin_content.extend_from_slice(frame);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let stdin_pipe = MemoryInputPipe::new(stdin_content);
|
|
95
|
+
let stdout_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stdout_limit_bytes));
|
|
96
|
+
let stderr_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stderr_limit_bytes));
|
|
97
|
+
|
|
98
|
+
let mut builder = WasiCtxBuilder::new();
|
|
99
|
+
builder.stdin(stdin_pipe);
|
|
100
|
+
builder.stdout(stdout_pipe.clone());
|
|
101
|
+
builder.stderr(stderr_pipe.clone());
|
|
102
|
+
// The requested profile decides the ambient-authority grant: the
|
|
103
|
+
// hermetic rung denies the preview1 time and entropy imports (see
|
|
104
|
+
// `ambient`), the permissive rung leaves the live WASI sources.
|
|
105
|
+
// Filesystem, environment, and network stay absent on both rungs —
|
|
106
|
+
// the builder grants none unless asked. The exhaustive match makes
|
|
107
|
+
// a future ladder rung a compile error here, not a silent grant.
|
|
108
|
+
match config.profile {
|
|
109
|
+
Profile::Hermetic => {
|
|
110
|
+
builder.wall_clock(ambient::FrozenWallClock);
|
|
111
|
+
builder.monotonic_clock(ambient::FrozenMonotonicClock);
|
|
112
|
+
builder.secure_random(ambient::deterministic_rng());
|
|
113
|
+
}
|
|
114
|
+
Profile::Permissive => {}
|
|
115
|
+
}
|
|
116
|
+
let wasi = builder.build_p1();
|
|
117
|
+
|
|
118
|
+
store
|
|
119
|
+
.data_mut()
|
|
120
|
+
.install_wasi(wasi, stdout_pipe, stderr_pipe);
|
|
121
|
+
Ok(())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Invoke `__kobako_take_outcome`, decode the packed `(ptr<<32)|len`
|
|
125
|
+
/// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Returns a
|
|
126
|
+
/// `Trap` (boundary → `Kobako::TrapError`) when the export is missing,
|
|
127
|
+
/// `len` exceeds the 16 MiB single-dispatch cap, the `ptr`/`len`
|
|
128
|
+
/// arithmetic overflows, the slice falls outside live memory, or the
|
|
129
|
+
/// `memory` export itself is absent.
|
|
130
|
+
pub(crate) fn fetch_outcome_bytes(
|
|
131
|
+
store: &mut WtStore<Invocation>,
|
|
132
|
+
exports: &Exports,
|
|
133
|
+
) -> Result<Vec<u8>, Trap> {
|
|
134
|
+
let take = require_export(exports.take_outcome.as_ref())?;
|
|
135
|
+
let mem = require_memory(exports)?;
|
|
136
|
+
|
|
137
|
+
let packed = take
|
|
138
|
+
.call(store.as_context_mut(), ())
|
|
139
|
+
.map_err(|e| Trap::Other(format!("failed to read the Sandbox result: {e}")))?;
|
|
140
|
+
let (ptr, len) = guest_mem::unpack_outcome_packed(packed);
|
|
141
|
+
if len > guest_mem::MAX_DISPATCH_PAYLOAD {
|
|
142
|
+
return Err(Trap::Other(
|
|
143
|
+
"result payload exceeds the 16 MiB limit".to_string(),
|
|
144
|
+
));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let data = mem.data(store.as_context_mut());
|
|
148
|
+
let range = guest_mem::guest_buffer_range(ptr, len, data.len())
|
|
149
|
+
.map_err(|msg| Trap::Other(format!("the Sandbox result is out of bounds: {msg}")))?;
|
|
150
|
+
Ok(data[range].to_vec())
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// User-facing message for the "Sandbox runtime is missing one of the
|
|
154
|
+
/// internal Kobako hooks" failure mode. Phrased in caller vocabulary —
|
|
155
|
+
/// the underlying ABI symbol names (`__kobako_alloc`, `__kobako_eval`,
|
|
156
|
+
/// `__kobako_take_outcome`) are not actionable to callers, and the
|
|
157
|
+
/// gem itself raises this error so a self-reference like "matches the
|
|
158
|
+
/// kobako gem version" reads as third-person. The actionable
|
|
159
|
+
/// diagnosis is "your data/kobako.wasm is out of sync; rebuild it".
|
|
160
|
+
const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required hooks; \
|
|
161
|
+
rebuild data/kobako.wasm against the installed version";
|
|
162
|
+
|
|
163
|
+
/// User-facing message for the "the loaded Wasm module is not a
|
|
164
|
+
/// Kobako-shaped runtime at all" failure mode (no linear memory
|
|
165
|
+
/// export). Same phrasing philosophy as
|
|
166
|
+
/// `SANDBOX_RUNTIME_MISSING_HOOKS`.
|
|
167
|
+
const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
|
|
168
|
+
"the loaded Wasm module is not a Kobako-compatible runtime";
|
|
169
|
+
|
|
170
|
+
/// Return the resolved `TypedFunc` for an ABI export, or a `Trap`
|
|
171
|
+
/// (boundary → `Kobako::TrapError`) when the option is `None`. Both
|
|
172
|
+
/// run-path methods (`#eval`, `#run`) plus the `build_snapshot` readout
|
|
173
|
+
/// that drains `OUTCOME_BUFFER` share the same "missing export" handling;
|
|
174
|
+
/// this helper collapses those sites onto one safe entry. The user-facing
|
|
175
|
+
/// message is intentionally export-agnostic (see
|
|
176
|
+
/// `SANDBOX_RUNTIME_MISSING_HOOKS`) — the ABI symbol name is not
|
|
177
|
+
/// actionable to callers, so it is not threaded in.
|
|
178
|
+
pub(crate) fn require_export<Params, Results>(
|
|
179
|
+
export: Option<&TypedFunc<Params, Results>>,
|
|
180
|
+
) -> Result<&TypedFunc<Params, Results>, Trap>
|
|
181
|
+
where
|
|
182
|
+
Params: wasmtime::WasmParams,
|
|
183
|
+
Results: wasmtime::WasmResults,
|
|
184
|
+
{
|
|
185
|
+
export.ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_MISSING_HOOKS.to_string()))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[cfg(test)]
|
|
189
|
+
mod tests {
|
|
190
|
+
//! Witness the B-54 rung split at the WASI boundary: one probe
|
|
191
|
+
//! module reads `wasi:clocks` / `wasi:random` through the context
|
|
192
|
+
//! `install_wasi_frames` builds — frozen under `Hermetic`, live
|
|
193
|
+
//! under `Permissive`.
|
|
194
|
+
use wasmtime::{Linker, Module};
|
|
195
|
+
use wasmtime_wasi::p1;
|
|
196
|
+
|
|
197
|
+
use super::*;
|
|
198
|
+
use crate::cache::shared_engine;
|
|
199
|
+
|
|
200
|
+
/// Preview1 probe with one export per ambient source: `clock_ns`
|
|
201
|
+
/// reads the realtime clock, `random_word` reads eight entropy bytes.
|
|
202
|
+
const AMBIENT_PROBE_WAT: &str = r#"
|
|
203
|
+
(module
|
|
204
|
+
(import "wasi_snapshot_preview1" "clock_time_get"
|
|
205
|
+
(func $clock (param i32 i64 i32) (result i32)))
|
|
206
|
+
(import "wasi_snapshot_preview1" "random_get"
|
|
207
|
+
(func $random (param i32 i32) (result i32)))
|
|
208
|
+
(memory (export "memory") 1)
|
|
209
|
+
(func (export "clock_ns") (result i64)
|
|
210
|
+
(drop (call $clock (i32.const 0) (i64.const 1) (i32.const 0)))
|
|
211
|
+
(i64.load (i32.const 0)))
|
|
212
|
+
(func (export "random_word") (result i64)
|
|
213
|
+
(drop (call $random (i32.const 8) (i32.const 8)))
|
|
214
|
+
(i64.load (i32.const 8))))
|
|
215
|
+
"#;
|
|
216
|
+
|
|
217
|
+
/// Instantiate the probe over a WASI context built at `profile` and
|
|
218
|
+
/// return the `(clock, random)` readings it observes.
|
|
219
|
+
fn probe_ambient(profile: Profile) -> (i64, i64) {
|
|
220
|
+
let engine = shared_engine().expect("shared engine must be constructible");
|
|
221
|
+
let config = Config {
|
|
222
|
+
timeout: None,
|
|
223
|
+
stdout_limit_bytes: None,
|
|
224
|
+
stderr_limit_bytes: None,
|
|
225
|
+
profile,
|
|
226
|
+
};
|
|
227
|
+
let mut store = WtStore::new(engine, Invocation::new(None));
|
|
228
|
+
store.set_epoch_deadline(u64::MAX);
|
|
229
|
+
install_wasi_frames(&mut store, &config, &[]).expect("WASI context must install");
|
|
230
|
+
|
|
231
|
+
let mut linker: Linker<Invocation> = Linker::new(engine);
|
|
232
|
+
p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
|
|
233
|
+
.expect("WASI imports must link");
|
|
234
|
+
let module = Module::new(engine, AMBIENT_PROBE_WAT).expect("probe module must compile");
|
|
235
|
+
let instance = linker
|
|
236
|
+
.instantiate(&mut store, &module)
|
|
237
|
+
.expect("probe module must instantiate");
|
|
238
|
+
|
|
239
|
+
let read = |store: &mut WtStore<Invocation>, name: &str| -> i64 {
|
|
240
|
+
instance
|
|
241
|
+
.get_typed_func::<(), i64>(store.as_context_mut(), name)
|
|
242
|
+
.expect("probe export must resolve")
|
|
243
|
+
.call(store.as_context_mut(), ())
|
|
244
|
+
.expect("probe export must run")
|
|
245
|
+
};
|
|
246
|
+
let clock = read(&mut store, "clock_ns");
|
|
247
|
+
let random = read(&mut store, "random_word");
|
|
248
|
+
(clock, random)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#[test]
|
|
252
|
+
fn hermetic_denies_ambient_time_and_entropy() {
|
|
253
|
+
let (clock, random) = probe_ambient(Profile::Hermetic);
|
|
254
|
+
assert_eq!(
|
|
255
|
+
clock, 0,
|
|
256
|
+
"a hermetic guest's wasi:clocks must read the Unix epoch, not host time"
|
|
257
|
+
);
|
|
258
|
+
assert_eq!(
|
|
259
|
+
random, 0,
|
|
260
|
+
"a hermetic guest's wasi:random must yield the constant stream, not host entropy"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[test]
|
|
265
|
+
fn permissive_grants_live_ambient_time_and_entropy() {
|
|
266
|
+
let (clock, random) = probe_ambient(Profile::Permissive);
|
|
267
|
+
assert!(
|
|
268
|
+
clock > 0,
|
|
269
|
+
"a permissive guest's wasi:clocks must read live host time"
|
|
270
|
+
);
|
|
271
|
+
assert_ne!(
|
|
272
|
+
random, 0,
|
|
273
|
+
"a permissive guest's wasi:random must yield host entropy"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -2,16 +2,39 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! Both directions of a host↔guest buffer handoff that run *inside* a wasm
|
|
4
4
|
//! callback frame go through here: writing the transport Response back
|
|
5
|
-
//! (`
|
|
5
|
+
//! (`crate::dispatch`) and shipping block-yield args into the guest
|
|
6
6
|
//! (`drive_yield`, below) performed the same `__kobako_alloc` +
|
|
7
7
|
//! bounds-check + `memory.write` dance with only the diagnostic strings
|
|
8
|
-
//! differing. The Store-based write path (`
|
|
9
|
-
//! separate beast — it holds the
|
|
10
|
-
//! in `
|
|
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`.
|
|
11
11
|
|
|
12
12
|
use wasmtime::{Caller, Extern, Memory};
|
|
13
13
|
|
|
14
|
-
use
|
|
14
|
+
use crate::invocation::Invocation;
|
|
15
|
+
use kobako_runtime::error::Trap;
|
|
16
|
+
use kobako_runtime::yielder::Yielder;
|
|
17
|
+
|
|
18
|
+
/// The wasmtime-backed `Yielder` (`kobako_runtime::yielder`): a
|
|
19
|
+
/// frame-scoped wrapper over the dispatch `Caller` that drives a block-yield
|
|
20
|
+
/// round-trip through `drive_yield`. Built per `__kobako_dispatch` frame and
|
|
21
|
+
/// handed to the dispatch handler, so nested dispatch frames each
|
|
22
|
+
/// carry their own and stack on the Rust call stack with no shared slot.
|
|
23
|
+
pub(crate) struct CallerYielder<'a, 'c> {
|
|
24
|
+
caller: &'a mut Caller<'c, Invocation>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl<'a, 'c> CallerYielder<'a, 'c> {
|
|
28
|
+
pub(crate) fn new(caller: &'a mut Caller<'c, Invocation>) -> Self {
|
|
29
|
+
Self { caller }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl Yielder for CallerYielder<'_, '_> {
|
|
34
|
+
fn yield_block(&mut self, args: &[u8]) -> Result<Vec<u8>, Trap> {
|
|
35
|
+
drive_yield(self.caller, args).map_err(|msg| Trap::Other(msg.to_string()))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
15
38
|
|
|
16
39
|
/// User-facing reason when a required guest export (the allocation or
|
|
17
40
|
/// block-yield hook) is absent or has the wrong signature — the loaded
|
|
@@ -37,7 +60,7 @@ fn memory_export(caller: &mut Caller<'_, Invocation>) -> Result<Memory, &'static
|
|
|
37
60
|
/// copy `bytes` in. Returns the guest pointer. Every failure path carries a
|
|
38
61
|
/// `&'static str` reason so the caller can surface a diagnostic rather than
|
|
39
62
|
/// a silent fault.
|
|
40
|
-
pub(
|
|
63
|
+
pub(crate) fn alloc_and_write(
|
|
41
64
|
caller: &mut Caller<'_, Invocation>,
|
|
42
65
|
bytes: &[u8],
|
|
43
66
|
) -> Result<u32, &'static str> {
|
|
@@ -67,7 +90,7 @@ pub(super) fn alloc_and_write(
|
|
|
67
90
|
/// diagnostic instead of a lumped one; a guest-claimed length past the
|
|
68
91
|
/// 16 MiB cap is a wire violation that names the cap (the caller walks
|
|
69
92
|
/// the trap path on any `Err`).
|
|
70
|
-
pub(
|
|
93
|
+
pub(crate) fn read(
|
|
71
94
|
caller: &mut Caller<'_, Invocation>,
|
|
72
95
|
ptr: i32,
|
|
73
96
|
len: i32,
|
|
@@ -93,15 +116,15 @@ pub(super) fn read(
|
|
|
93
116
|
/// transfer larger than this is a wire violation — the Host Gem walks
|
|
94
117
|
/// the trap path rather than allocate or copy the buffer. Held as a
|
|
95
118
|
/// constant for now; a future SPEC anchor may let the Host App raise it.
|
|
96
|
-
pub(
|
|
119
|
+
pub(crate) const MAX_DISPATCH_PAYLOAD: usize = 16 * 1024 * 1024;
|
|
97
120
|
|
|
98
121
|
/// Validate a payload length against `MAX_DISPATCH_PAYLOAD` and narrow it
|
|
99
122
|
/// to `i32` — the signed wasm ABI width for the guest buffer parameters.
|
|
100
123
|
/// Every host *write* boundary (`alloc_and_write`, `drive_yield`,
|
|
101
|
-
/// `
|
|
124
|
+
/// `frames::write_envelope`) routes its length through here so the
|
|
102
125
|
/// wire-violation reason is uniform; the *read* boundaries compare
|
|
103
126
|
/// against `MAX_DISPATCH_PAYLOAD` directly.
|
|
104
|
-
pub(
|
|
127
|
+
pub(crate) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
|
|
105
128
|
if len > MAX_DISPATCH_PAYLOAD {
|
|
106
129
|
return Err("payload exceeds the 16 MiB limit");
|
|
107
130
|
}
|
|
@@ -111,9 +134,9 @@ pub(super) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
|
|
|
111
134
|
|
|
112
135
|
/// Compute the half-open range `[ptr, ptr + len)` for a guest linear-memory
|
|
113
136
|
/// copy, validating that the arithmetic does not overflow and the range
|
|
114
|
-
/// fits inside `mem_size`. Shared by `
|
|
115
|
-
/// and `
|
|
116
|
-
pub(
|
|
137
|
+
/// fits inside `mem_size`. Shared by `frames::write_envelope` (write side)
|
|
138
|
+
/// and `frames::fetch_outcome_bytes` (read side).
|
|
139
|
+
pub(crate) fn guest_buffer_range(
|
|
117
140
|
ptr: usize,
|
|
118
141
|
len: usize,
|
|
119
142
|
mem_size: usize,
|
|
@@ -128,7 +151,7 @@ pub(super) fn guest_buffer_range(
|
|
|
128
151
|
/// Unpack the `(ptr, len)` u64 returned by `__kobako_take_outcome`:
|
|
129
152
|
/// high 32 bits = ptr, low 32 bits = len. Mirrors the guest-side
|
|
130
153
|
/// `unpack_u64` in `wasm/kobako-core/src/abi.rs`.
|
|
131
|
-
pub(
|
|
154
|
+
pub(crate) fn unpack_outcome_packed(packed: u64) -> (usize, usize) {
|
|
132
155
|
let ptr = (packed >> 32) as u32 as usize;
|
|
133
156
|
let len = packed as u32 as usize;
|
|
134
157
|
(ptr, len)
|
|
@@ -139,7 +162,7 @@ pub(super) fn unpack_outcome_packed(packed: u64) -> (usize, usize) {
|
|
|
139
162
|
/// the guest produced and return it. Mirrors `dispatch::write_response`'s
|
|
140
163
|
/// allocator dance but in the opposite direction — the host is the
|
|
141
164
|
/// *initiator* of this round-trip, not the responder.
|
|
142
|
-
pub(
|
|
165
|
+
pub(crate) fn drive_yield(
|
|
143
166
|
caller: &mut Caller<'_, Invocation>,
|
|
144
167
|
args: &[u8],
|
|
145
168
|
) -> Result<Vec<u8>, &'static str> {
|
|
@@ -6,23 +6,22 @@
|
|
|
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.
|
|
8
8
|
//! Caching the resolved `InstancePre` per path leaves only the
|
|
9
|
-
//! `instantiate` call itself on the `
|
|
9
|
+
//! `instantiate` call itself on the `Driver::new` hot path.
|
|
10
10
|
//!
|
|
11
|
-
//! Concurrency: see `
|
|
11
|
+
//! Concurrency: see `crate::cache` — under Ruby's GVL the Mutex serves
|
|
12
12
|
//! `Sync` bounds rather than real contention.
|
|
13
13
|
|
|
14
14
|
use std::collections::HashMap;
|
|
15
15
|
use std::path::{Path, PathBuf};
|
|
16
16
|
use std::sync::{Mutex, OnceLock};
|
|
17
17
|
|
|
18
|
-
use magnus::{Error as MagnusError, Ruby};
|
|
19
18
|
use wasmtime::{Caller, InstancePre, Linker};
|
|
20
19
|
use wasmtime_wasi::p1;
|
|
21
20
|
|
|
22
|
-
use
|
|
23
|
-
use
|
|
24
|
-
use
|
|
25
|
-
use
|
|
21
|
+
use crate::cache::{cached_module, shared_engine};
|
|
22
|
+
use crate::invocation::Invocation;
|
|
23
|
+
use crate::{dispatch, trap};
|
|
24
|
+
use kobako_runtime::error::SetupError;
|
|
26
25
|
|
|
27
26
|
static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocation>>>> =
|
|
28
27
|
OnceLock::new();
|
|
@@ -30,8 +29,8 @@ static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocatio
|
|
|
30
29
|
/// Look up `path` in the per-path `InstancePre` cache, wiring the
|
|
31
30
|
/// Linker and resolving the Module's imports on a miss. Compilation
|
|
32
31
|
/// faults surface through `cached_module`; import-resolution faults
|
|
33
|
-
///
|
|
34
|
-
pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>,
|
|
32
|
+
/// return a runtime-dead `SetupError` (boundary → `Kobako::SetupError`).
|
|
33
|
+
pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>, SetupError> {
|
|
35
34
|
let cache = INSTANCE_PRE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
|
36
35
|
|
|
37
36
|
if let Some(pre) = cache
|
|
@@ -45,10 +44,9 @@ pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>
|
|
|
45
44
|
|
|
46
45
|
let module = cached_module(path)?;
|
|
47
46
|
let linker = build_linker()?;
|
|
48
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
49
47
|
let pre = linker
|
|
50
48
|
.instantiate_pre(&module)
|
|
51
|
-
.map_err(
|
|
49
|
+
.map_err(trap::instantiate_err)?;
|
|
52
50
|
cache
|
|
53
51
|
.lock()
|
|
54
52
|
.expect("instance_pre cache mutex poisoned")
|
|
@@ -58,17 +56,16 @@ pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>
|
|
|
58
56
|
|
|
59
57
|
/// Build the host-import `Linker` every Guest Binary instantiates
|
|
60
58
|
/// against.
|
|
61
|
-
fn build_linker() -> Result<Linker<Invocation>,
|
|
62
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
59
|
+
fn build_linker() -> Result<Linker<Invocation>, SetupError> {
|
|
63
60
|
let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
|
|
64
61
|
|
|
65
62
|
// Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
|
|
66
63
|
// to the MemoryOutputPipes set up before each run via
|
|
67
|
-
// `
|
|
64
|
+
// `Driver::invoke`. The closure pulls a `&mut WasiP1Ctx` out of
|
|
68
65
|
// Invocation; the panic semantics live inside `Invocation::wasi_mut`
|
|
69
66
|
// so the wiring stays honest about its precondition.
|
|
70
67
|
p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
|
|
71
|
-
.map_err(|e|
|
|
68
|
+
.map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
|
|
72
69
|
|
|
73
70
|
// `__kobako_dispatch` host import. Signature per docs/wire-codec.md
|
|
74
71
|
// § ABI Signatures:
|
|
@@ -87,12 +84,7 @@ fn build_linker() -> Result<Linker<Invocation>, MagnusError> {
|
|
|
87
84
|
dispatch::handle(&mut caller, req_ptr, req_len)
|
|
88
85
|
},
|
|
89
86
|
)
|
|
90
|
-
.map_err(|e| {
|
|
91
|
-
setup_err(
|
|
92
|
-
&ruby,
|
|
93
|
-
format!("failed to set up the host callback bridge: {}", e),
|
|
94
|
-
)
|
|
95
|
-
})?;
|
|
87
|
+
.map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
|
|
96
88
|
|
|
97
89
|
Ok(linker)
|
|
98
90
|
}
|