kobako 0.9.2 → 0.11.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 +32 -0
- data/Cargo.lock +3 -1
- data/README.md +47 -19
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +12 -2
- data/ext/kobako/src/runtime/ambient.rs +1 -1
- data/ext/kobako/src/runtime/cache.rs +170 -6
- data/ext/kobako/src/runtime/capture.rs +1 -1
- data/ext/kobako/src/runtime/config.rs +3 -4
- data/ext/kobako/src/runtime/dispatch.rs +8 -8
- data/ext/kobako/src/runtime/exports.rs +32 -21
- data/ext/kobako/src/runtime/instance_pre.rs +97 -0
- data/ext/kobako/src/runtime/invocation.rs +36 -93
- data/ext/kobako/src/runtime/trap.rs +5 -5
- data/ext/kobako/src/runtime.rs +389 -403
- data/ext/kobako/src/snapshot.rs +2 -2
- data/lib/kobako/capture.rb +5 -7
- data/lib/kobako/catalog/handles.rb +28 -39
- data/lib/kobako/catalog/namespaces.rb +31 -20
- data/lib/kobako/catalog/snippets.rb +18 -16
- data/lib/kobako/codec/decoder.rb +5 -1
- data/lib/kobako/codec/utils.rb +6 -9
- data/lib/kobako/errors.rb +40 -36
- data/lib/kobako/handle.rb +2 -3
- data/lib/kobako/namespace.rb +17 -6
- data/lib/kobako/outcome.rb +12 -14
- data/lib/kobako/pool.rb +176 -0
- data/lib/kobako/sandbox.rb +68 -88
- data/lib/kobako/sandbox_options.rb +5 -9
- data/lib/kobako/snapshot.rb +2 -4
- data/lib/kobako/snippet/binary.rb +1 -3
- data/lib/kobako/snippet/source.rb +1 -2
- data/lib/kobako/snippet.rb +1 -2
- data/lib/kobako/transport/dispatcher.rb +39 -38
- data/lib/kobako/transport/request.rb +1 -1
- data/lib/kobako/transport/run.rb +23 -28
- data/lib/kobako/transport/yielder.rb +11 -17
- data/lib/kobako/transport.rb +2 -3
- data/lib/kobako/usage.rb +10 -13
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +16 -1
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/namespace.rbs +2 -0
- data/sig/kobako/pool.rbs +44 -0
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/transport/dispatcher.rbs +2 -0
- metadata +4 -1
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -2,32 +2,38 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The only Ruby-visible class is
|
|
4
4
|
//
|
|
5
|
-
// Kobako::Runtime — wraps
|
|
5
|
+
// Kobako::Runtime — wraps a pre-linked InstancePre + per-Runtime caps
|
|
6
6
|
//
|
|
7
7
|
// constructed via `Kobako::Runtime.from_path(path, timeout, memory_limit,
|
|
8
|
-
// stdout_limit, stderr_limit)`.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// stdout_limit, stderr_limit)`. Every invocation (`#eval` / `#run`)
|
|
9
|
+
// instantiates a fresh instance from the InstancePre and discards the
|
|
10
|
+
// whole Store afterwards — the per-invocation instance discipline
|
|
11
|
+
// (ABI v2). The underlying wasmtime Engine and
|
|
12
|
+
// compiled Module live in a process-scope cache (see the `cache`
|
|
13
|
+
// submodule) and never surface to Ruby (SPEC.md "Code Organization":
|
|
14
|
+
// `ext/` "exposes no Wasm engine types to the Host App or downstream
|
|
15
|
+
// gems").
|
|
13
16
|
//
|
|
14
17
|
// Module layout (per CLAUDE.md principle #2 — one responsibility per file):
|
|
15
18
|
//
|
|
16
19
|
// * `cache` — process-wide Engine + per-path Module cache and the
|
|
17
20
|
// process-singleton epoch ticker thread.
|
|
18
21
|
// * `config` — per-Runtime caps (timeout / stdout / stderr limits).
|
|
19
|
-
// * `exports` —
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
22
|
+
// * `exports` — per-invocation `__kobako_eval` / `_run` /
|
|
23
|
+
// `_take_outcome` / `_alloc` / `memory` handles.
|
|
24
|
+
// * `instance_pre`— host-import Linker wiring + per-path `InstancePre`
|
|
25
|
+
// cache.
|
|
26
|
+
// * `invocation` — Invocation (per-Store context), the `MemoryLimiter`
|
|
27
|
+
// memory cap, and the trap marker types
|
|
28
|
+
// (`TimeoutTrap` / `MemoryLimitTrap`).
|
|
23
29
|
// * `dispatch` — `__kobako_dispatch` host-import dispatch helpers.
|
|
24
30
|
// * `guest_mem` — Caller-based guest linear-memory alloc / write / read.
|
|
25
31
|
// * `capture` — stdout / stderr pipe sizing + clip helpers.
|
|
26
32
|
// * `trap` — wasmtime-error → `Kobako::*` trap classification.
|
|
27
33
|
//
|
|
28
|
-
// This file owns the `Kobako::Runtime` magnus class itself (the
|
|
29
|
-
//
|
|
30
|
-
//
|
|
34
|
+
// This file owns the `Kobako::Runtime` magnus class itself (the
|
|
35
|
+
// InstancePre + `Config` + the per-invocation `#eval` / `#run` run
|
|
36
|
+
// path), the Ruby error-class lazy-resolvers, the `trap_err` /
|
|
31
37
|
// `timeout_err` / `memory_limit_err` / `setup_err` constructors shared by
|
|
32
38
|
// every submodule, and the Ruby init() that registers the class.
|
|
33
39
|
|
|
@@ -38,6 +44,7 @@ mod config;
|
|
|
38
44
|
mod dispatch;
|
|
39
45
|
mod exports;
|
|
40
46
|
mod guest_mem;
|
|
47
|
+
mod instance_pre;
|
|
41
48
|
mod invocation;
|
|
42
49
|
mod trap;
|
|
43
50
|
|
|
@@ -54,23 +61,26 @@ use magnus::{gc, typed_data::DataTypeFunctions, value::Opaque, RArray, TypedData
|
|
|
54
61
|
|
|
55
62
|
use crate::snapshot::Snapshot;
|
|
56
63
|
use wasmtime::{
|
|
57
|
-
AsContextMut,
|
|
58
|
-
|
|
64
|
+
AsContextMut, InstancePre as WtInstancePre, Memory, ResourceLimiter, Store as WtStore,
|
|
65
|
+
TypedFunc,
|
|
59
66
|
};
|
|
60
|
-
use wasmtime_wasi::p1;
|
|
61
67
|
use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
|
|
62
68
|
use wasmtime_wasi::WasiCtxBuilder;
|
|
63
69
|
|
|
64
|
-
use self::cache::
|
|
70
|
+
use self::cache::shared_engine;
|
|
65
71
|
use self::config::Config;
|
|
66
72
|
use self::exports::Exports;
|
|
67
|
-
use self::invocation::
|
|
73
|
+
use self::invocation::Invocation;
|
|
68
74
|
|
|
69
75
|
/// The wire ABI version this host implements (docs/wire-codec.md § ABI
|
|
70
76
|
/// Version). A Guest Binary is accepted only when its
|
|
71
|
-
/// `__kobako_abi_version` export reports the same value
|
|
72
|
-
///
|
|
73
|
-
|
|
77
|
+
/// `__kobako_abi_version` export reports the same value; a mismatch
|
|
78
|
+
/// is a deterministic artifact fault. The guest-side mirror is
|
|
79
|
+
/// `kobako_core::abi::ABI_VERSION`. Version 2
|
|
80
|
+
/// carries the per-invocation instance discipline: the host
|
|
81
|
+
/// drives every invocation on a fresh instance, so the guest may leave
|
|
82
|
+
/// its VM state dirty at exit.
|
|
83
|
+
const ABI_VERSION: u32 = 2;
|
|
74
84
|
|
|
75
85
|
/// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
|
|
76
86
|
/// what would otherwise be an inline `unsafe { rstring.as_slice() }
|
|
@@ -140,22 +150,22 @@ pub(crate) fn trap_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
|
140
150
|
/// Construct a `Kobako::SetupError` magnus error. Used for every
|
|
141
151
|
/// construction-time failure on the `Runtime.from_path` path before any
|
|
142
152
|
/// invocation runs — unreadable artifact, bytes that are not a valid Wasm
|
|
143
|
-
/// module, or engine / linker / instantiation setup failure
|
|
144
|
-
///
|
|
153
|
+
/// module, or engine / linker / instantiation setup failure. The
|
|
154
|
+
/// `ModuleNotBuiltError` subclass (artifact absent) is
|
|
145
155
|
/// raised through `MODULE_NOT_BUILT_ERROR` directly.
|
|
146
156
|
pub(crate) fn setup_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
147
157
|
error_in(ruby, &SETUP_ERROR, msg)
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
/// Construct a `Kobako::TimeoutError` magnus error. Surfaces the
|
|
151
|
-
///
|
|
161
|
+
/// wall-clock cap path with the verb prefix added
|
|
152
162
|
/// by `Kobako::Sandbox#invoke!`.
|
|
153
163
|
pub(crate) fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
154
164
|
error_in(ruby, &TIMEOUT_ERROR, msg)
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
/// Construct a `Kobako::MemoryLimitError` magnus error. Surfaces the
|
|
158
|
-
///
|
|
168
|
+
/// linear-memory cap path with the verb prefix
|
|
159
169
|
/// added by `Kobako::Sandbox#invoke!`.
|
|
160
170
|
pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
161
171
|
error_in(ruby, &MEMORY_LIMIT_ERROR, msg)
|
|
@@ -164,8 +174,8 @@ pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusErr
|
|
|
164
174
|
/// Construct a `Kobako::SandboxError` magnus error. Used for the
|
|
165
175
|
/// host-side pre-call faults the SPEC attributes to the sandbox / wire
|
|
166
176
|
/// layer rather than the Wasm engine — currently the `#run` invocation
|
|
167
|
-
/// envelope reservation failure (`__kobako_alloc` returns 0
|
|
168
|
-
///
|
|
177
|
+
/// envelope reservation failure (`__kobako_alloc` returns 0).
|
|
178
|
+
/// The runtime is intact, so this must not be a
|
|
169
179
|
/// `TrapError`: no discard-and-recreate recovery is owed to the caller.
|
|
170
180
|
pub(crate) fn sandbox_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
171
181
|
error_in(ruby, &SANDBOX_ERROR, msg)
|
|
@@ -200,26 +210,33 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
200
210
|
#[derive(TypedData)]
|
|
201
211
|
#[magnus(class = "Kobako::Runtime", free_immediately, size, mark)]
|
|
202
212
|
pub(crate) struct Runtime {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
// Pre-linked instantiation template (import wiring + type checks
|
|
214
|
+
// done once in `instance_pre::cached_instance_pre`). Every
|
|
215
|
+
// invocation instantiates a fresh instance from it and discards the
|
|
216
|
+
// whole Store afterwards — the per-invocation instance discipline.
|
|
217
|
+
instance_pre: WtInstancePre<Invocation>,
|
|
218
|
+
// Per-invocation linear-memory cap,
|
|
219
|
+
// threaded into each fresh `Invocation`; lives apart from `Config`
|
|
220
|
+
// because the wasmtime `ResourceLimiter` callback consumes it from
|
|
221
|
+
// inside the wasm engine.
|
|
222
|
+
memory_limit: Option<usize>,
|
|
209
223
|
// Wall-clock + per-channel capture caps forwarded from the Sandbox;
|
|
210
|
-
// see `Config`.
|
|
211
|
-
// which lives on `Invocation` because the wasmtime `ResourceLimiter`
|
|
212
|
-
// callback consumes it from inside the wasm engine.
|
|
224
|
+
// see `Config`.
|
|
213
225
|
config: Config,
|
|
214
|
-
// The host-side dispatch Proc
|
|
215
|
-
// to give `DataTypeFunctions::mark` a
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
// `
|
|
219
|
-
// `
|
|
220
|
-
// one pinned Proc. `Cell` is sound under the GVL (see the `unsafe impl
|
|
226
|
+
// The host-side dispatch Proc, held here only
|
|
227
|
+
// to give `DataTypeFunctions::mark` a read path so it can pin the
|
|
228
|
+
// Proc across GC. The copy the `__kobako_dispatch` import actually
|
|
229
|
+
// calls is bound onto each per-invocation `Invocation` by
|
|
230
|
+
// `Runtime::new_store`. Both hold the same `Copy` handle to the one
|
|
231
|
+
// pinned Proc. `Cell` is sound under the GVL (see the `unsafe impl
|
|
221
232
|
// Sync` below).
|
|
222
233
|
on_dispatch: Cell<Option<Opaque<Value>>>,
|
|
234
|
+
// Usage of the most recent invocation —
|
|
235
|
+
// `(wall_time_seconds, memory_peak_bytes)` — captured by
|
|
236
|
+
// `build_snapshot` before the per-invocation Store is discarded so
|
|
237
|
+
// `#usage` reads survive the teardown. `(0.0, 0)` before the first
|
|
238
|
+
// invocation.
|
|
239
|
+
last_usage: Cell<(f64, usize)>,
|
|
223
240
|
}
|
|
224
241
|
|
|
225
242
|
impl DataTypeFunctions for Runtime {
|
|
@@ -228,7 +245,7 @@ impl DataTypeFunctions for Runtime {
|
|
|
228
245
|
/// copy on `Invocation` for the duration of a guest invocation.
|
|
229
246
|
/// `gc::Marker::mark` maps to `rb_gc_mark`, which pins: required because
|
|
230
247
|
/// the Invocation copy is a cached `VALUE` that compaction would
|
|
231
|
-
/// otherwise leave dangling
|
|
248
|
+
/// otherwise leave dangling. Without
|
|
232
249
|
/// this the Proc has no GC root at all — sweep collects it (SIGSEGV on
|
|
233
250
|
/// the next dispatch) and compaction relocates it (dispatch lands on
|
|
234
251
|
/// the wrong receiver).
|
|
@@ -239,25 +256,24 @@ impl DataTypeFunctions for Runtime {
|
|
|
239
256
|
}
|
|
240
257
|
}
|
|
241
258
|
|
|
242
|
-
// SAFETY: magnus requires `Send + Sync` on TypedData types. The
|
|
243
|
-
// `on_dispatch
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
// occur. `Send` stays auto-derived (`Opaque<Value>` is `Send`).
|
|
259
|
+
// SAFETY: magnus requires `Send + Sync` on TypedData types. The
|
|
260
|
+
// `on_dispatch` / `last_usage` `Cell`s make the auto-derived `Sync`
|
|
261
|
+
// unavailable, but every access to them happens under the GVL on a single
|
|
262
|
+
// thread at a time — Ruby method calls, and a GC `mark` pass that also
|
|
263
|
+
// holds the GVL. No cross-thread access to either Cell can occur. `Send`
|
|
264
|
+
// stays auto-derived.
|
|
249
265
|
unsafe impl Sync for Runtime {}
|
|
250
266
|
|
|
251
267
|
impl Runtime {
|
|
252
|
-
/// Construct
|
|
253
|
-
/// shared Engine and per-path Module
|
|
254
|
-
/// constructor for `Kobako::Runtime` — Engine and Module
|
|
255
|
-
/// visible to Ruby.
|
|
268
|
+
/// Construct a Runtime from a wasm file path, using the process-wide
|
|
269
|
+
/// shared Engine and per-path Module / InstancePre caches. The single
|
|
270
|
+
/// Ruby-facing constructor for `Kobako::Runtime` — Engine and Module
|
|
271
|
+
/// are never visible to Ruby.
|
|
256
272
|
///
|
|
257
|
-
/// `timeout_seconds` is the
|
|
273
|
+
/// `timeout_seconds` is the wall-clock cap in seconds
|
|
258
274
|
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
259
275
|
/// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
|
|
260
|
-
/// are the per-channel output caps (
|
|
276
|
+
/// are the per-channel output caps (`None`
|
|
261
277
|
/// disables). All four are validated by the caller
|
|
262
278
|
/// (`Kobako::Sandbox`); this method only refuses non-finite or
|
|
263
279
|
/// non-positive timeouts as a defence in depth.
|
|
@@ -273,7 +289,7 @@ impl Runtime {
|
|
|
273
289
|
None => None,
|
|
274
290
|
Some(secs) if secs.is_finite() && secs > 0.0 => Some(Duration::from_secs_f64(secs)),
|
|
275
291
|
Some(secs) => {
|
|
276
|
-
//
|
|
292
|
+
// An invalid cap argument is a Host App
|
|
277
293
|
// programming error and raises `ArgumentError`, outside the
|
|
278
294
|
// construction-failure `SetupError` branch. `SandboxOptions`
|
|
279
295
|
// is the primary guard (it never lets a bad timeout reach
|
|
@@ -285,108 +301,39 @@ impl Runtime {
|
|
|
285
301
|
}
|
|
286
302
|
};
|
|
287
303
|
|
|
288
|
-
let
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
let mut store = WtStore::new(engine, Invocation::new(memory_limit));
|
|
292
|
-
store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
|
|
293
|
-
store.epoch_deadline_callback(trap::epoch_deadline_callback);
|
|
294
|
-
|
|
295
|
-
let store_cell = StoreCell::new(store);
|
|
296
|
-
Self::build(
|
|
297
|
-
engine,
|
|
298
|
-
&module,
|
|
299
|
-
store_cell,
|
|
300
|
-
timeout,
|
|
301
|
-
stdout_limit_bytes,
|
|
302
|
-
stderr_limit_bytes,
|
|
303
|
-
)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/// Build an `Runtime` from an engine, module, and store cell. The
|
|
307
|
-
/// store cell is moved in and ends up owned by the returned Runtime.
|
|
308
|
-
/// Wires the WASI p1 imports plus the `__kobako_dispatch` host import.
|
|
309
|
-
fn build(
|
|
310
|
-
engine: &wasmtime::Engine,
|
|
311
|
-
module: &WtModule,
|
|
312
|
-
store_cell: StoreCell,
|
|
313
|
-
timeout: Option<Duration>,
|
|
314
|
-
stdout_limit_bytes: Option<usize>,
|
|
315
|
-
stderr_limit_bytes: Option<usize>,
|
|
316
|
-
) -> Result<Self, MagnusError> {
|
|
317
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
318
|
-
let mut linker: Linker<Invocation> = Linker::new(engine);
|
|
319
|
-
|
|
320
|
-
// Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
|
|
321
|
-
// to the MemoryOutputPipes set up before each run via
|
|
322
|
-
// `Runtime::eval`. The closure pulls a `&mut WasiP1Ctx` out of
|
|
323
|
-
// Invocation; the panic semantics live inside `Invocation::wasi_mut`
|
|
324
|
-
// so the wiring stays honest about its precondition.
|
|
325
|
-
p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
|
|
326
|
-
.map_err(|e| setup_err(&ruby, format!("failed to set up the WASI runtime: {}", e)))?;
|
|
327
|
-
|
|
328
|
-
// `__kobako_dispatch` host import. Signature per docs/wire-codec.md
|
|
329
|
-
// § ABI Signatures:
|
|
330
|
-
// (req_ptr: i32, req_len: i32) -> i64
|
|
331
|
-
// Decodes the Request bytes, dispatches via the Ruby-side
|
|
332
|
-
// dispatch Proc (bound per-Sandbox through `Runtime#on_dispatch=`),
|
|
333
|
-
// allocates a guest buffer through `__kobako_alloc`, writes
|
|
334
|
-
// the Response bytes there, and returns the packed
|
|
335
|
-
// `(ptr<<32)|len`. The dispatcher returns 0 on any wire-layer
|
|
336
|
-
// fault (including no Proc bound); see `dispatch::handle`.
|
|
337
|
-
linker
|
|
338
|
-
.func_wrap(
|
|
339
|
-
"env",
|
|
340
|
-
"__kobako_dispatch",
|
|
341
|
-
|mut caller: Caller<'_, Invocation>, req_ptr: i32, req_len: i32| -> i64 {
|
|
342
|
-
dispatch::handle(&mut caller, req_ptr, req_len)
|
|
343
|
-
},
|
|
344
|
-
)
|
|
345
|
-
.map_err(|e| {
|
|
346
|
-
setup_err(
|
|
347
|
-
&ruby,
|
|
348
|
-
format!("failed to set up the host callback bridge: {}", e),
|
|
349
|
-
)
|
|
350
|
-
})?;
|
|
351
|
-
|
|
352
|
-
let instance = {
|
|
353
|
-
let mut store_ref = store_cell.borrow_mut();
|
|
354
|
-
linker
|
|
355
|
-
.instantiate(store_ref.as_context_mut(), module)
|
|
356
|
-
.map_err(|e| trap::instantiate_err(&ruby, e))?
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
Self::validate_abi_version(&instance, &store_cell, &ruby)?;
|
|
360
|
-
|
|
361
|
-
let exports = Exports::resolve(&instance, &store_cell);
|
|
362
|
-
|
|
363
|
-
Ok(Self {
|
|
364
|
-
inner: instance,
|
|
365
|
-
store: store_cell,
|
|
366
|
-
exports,
|
|
304
|
+
let runtime = Self {
|
|
305
|
+
instance_pre: instance_pre::cached_instance_pre(Path::new(&path))?,
|
|
306
|
+
memory_limit,
|
|
367
307
|
config: Config {
|
|
368
308
|
timeout,
|
|
369
309
|
stdout_limit_bytes,
|
|
370
310
|
stderr_limit_bytes,
|
|
371
311
|
},
|
|
372
312
|
on_dispatch: Cell::new(None),
|
|
373
|
-
|
|
313
|
+
last_usage: Cell::new((0.0, 0)),
|
|
314
|
+
};
|
|
315
|
+
runtime.probe_abi_version(&ruby)?;
|
|
316
|
+
Ok(runtime)
|
|
374
317
|
}
|
|
375
318
|
|
|
376
|
-
///
|
|
377
|
-
///
|
|
378
|
-
///
|
|
379
|
-
///
|
|
380
|
-
/// `Kobako::SetupError`.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
) -> Result<(), MagnusError> {
|
|
386
|
-
let mut
|
|
387
|
-
|
|
319
|
+
/// Instantiate a throwaway probe instance at construction and require
|
|
320
|
+
/// the guest's `__kobako_abi_version` export to equal `ABI_VERSION`
|
|
321
|
+
/// An absent export or a non-equal value is
|
|
322
|
+
/// a deterministic artifact fault raised as
|
|
323
|
+
/// `Kobako::SetupError`. The probe Store drops here; invocation
|
|
324
|
+
/// instances are created per `#eval` / `#run`. The frameless WASI
|
|
325
|
+
/// context keeps a third-party guest whose start section touches
|
|
326
|
+
/// WASI on the `SetupError` path instead of panicking in
|
|
327
|
+
/// `Invocation::wasi_mut`.
|
|
328
|
+
fn probe_abi_version(&self, ruby: &Ruby) -> Result<(), MagnusError> {
|
|
329
|
+
let mut store = self.new_store()?;
|
|
330
|
+
install_wasi_frames(&mut store, &self.config, &[])?;
|
|
331
|
+
let instance = self
|
|
332
|
+
.instance_pre
|
|
333
|
+
.instantiate(store.as_context_mut())
|
|
334
|
+
.map_err(|e| trap::instantiate_err(ruby, e))?;
|
|
388
335
|
let probe = instance
|
|
389
|
-
.get_typed_func::<(), u32>(
|
|
336
|
+
.get_typed_func::<(), u32>(store.as_context_mut(), "__kobako_abi_version")
|
|
390
337
|
.map_err(|_| {
|
|
391
338
|
setup_err(
|
|
392
339
|
ruby,
|
|
@@ -396,7 +343,7 @@ impl Runtime {
|
|
|
396
343
|
),
|
|
397
344
|
)
|
|
398
345
|
})?;
|
|
399
|
-
let reported = probe.call(
|
|
346
|
+
let reported = probe.call(store.as_context_mut(), ()).map_err(|e| {
|
|
400
347
|
setup_err(
|
|
401
348
|
ruby,
|
|
402
349
|
format!("failed to read the Guest Binary's ABI version: {e}"),
|
|
@@ -415,31 +362,19 @@ impl Runtime {
|
|
|
415
362
|
Ok(())
|
|
416
363
|
}
|
|
417
364
|
|
|
418
|
-
/// Register the Ruby-side dispatch `Proc
|
|
419
|
-
/// Bound to Ruby as `Kobako::Runtime#on_dispatch=`.
|
|
420
|
-
///
|
|
421
|
-
///
|
|
422
|
-
///
|
|
365
|
+
/// Register the Ruby-side dispatch `Proc`.
|
|
366
|
+
/// Bound to Ruby as `Kobako::Runtime#on_dispatch=`. The handle is
|
|
367
|
+
/// pinned by `DataTypeFunctions::mark` and copied onto every
|
|
368
|
+
/// per-invocation `Invocation` by `Runtime::new_store`, where the
|
|
369
|
+
/// `__kobako_dispatch` import reads it through `Caller<Invocation>`.
|
|
423
370
|
pub(crate) fn set_on_dispatch(&self, proc_value: Value) -> Result<(), MagnusError> {
|
|
424
|
-
|
|
425
|
-
// Write both copies of the one Proc handle: the `on_dispatch` Cell
|
|
426
|
-
// gives `DataTypeFunctions::mark` a Store-free read path to pin the
|
|
427
|
-
// Proc across GC, and the `Invocation` copy is what the
|
|
428
|
-
// `__kobako_dispatch` import reads through `Caller<Invocation>`.
|
|
429
|
-
// `mark` cannot reach the Invocation copy itself — the Store is held
|
|
430
|
-
// `borrow_mut` for the whole guest call, exactly when GC may fire
|
|
431
|
-
// during dispatch — so the Cell is the dedicated GC-rooting anchor.
|
|
432
|
-
self.on_dispatch.set(Some(on_dispatch));
|
|
433
|
-
self.store
|
|
434
|
-
.borrow_mut()
|
|
435
|
-
.data_mut()
|
|
436
|
-
.bind_on_dispatch(on_dispatch);
|
|
371
|
+
self.on_dispatch.set(Some(Opaque::from(proc_value)));
|
|
437
372
|
Ok(())
|
|
438
373
|
}
|
|
439
374
|
|
|
440
375
|
/// Synchronously re-enter the guest's `__kobako_yield_to_block`
|
|
441
376
|
/// export with `args_bytes` as the yield-arguments payload, and
|
|
442
|
-
/// return the YieldResponse bytes the guest produced
|
|
377
|
+
/// return the YieldResponse bytes the guest produced.
|
|
443
378
|
///
|
|
444
379
|
/// Bound to Ruby as `Kobako::Runtime#yield_to_active_invocation`.
|
|
445
380
|
/// Recovers the dispatcher's `&mut Caller` from the per-thread
|
|
@@ -447,7 +382,7 @@ impl Runtime {
|
|
|
447
382
|
/// already inside a `__kobako_dispatch` callback, so the Caller
|
|
448
383
|
/// parked on the Rust stack is the same one the Sandbox-level
|
|
449
384
|
/// `#eval` / `#run` is driving. Invoked from the host-side yield
|
|
450
|
-
/// proxy that the dispatcher hands to Service methods
|
|
385
|
+
/// proxy that the dispatcher hands to Service methods;
|
|
451
386
|
/// raises `Kobako::TrapError` when called outside an active dispatch
|
|
452
387
|
/// frame, or when any of the underlying allocation / write / call /
|
|
453
388
|
/// read steps fails.
|
|
@@ -457,7 +392,7 @@ impl Runtime {
|
|
|
457
392
|
) -> Result<RString, MagnusError> {
|
|
458
393
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
459
394
|
let _ = self; // The Caller carries its own Store; `self` is only
|
|
460
|
-
// a marker that the method belongs to
|
|
395
|
+
// a marker that the method belongs to a Runtime.
|
|
461
396
|
|
|
462
397
|
let bytes = rstring_to_vec(args_bytes);
|
|
463
398
|
let Some(caller) = dispatch::current_caller() else {
|
|
@@ -482,19 +417,19 @@ impl Runtime {
|
|
|
482
417
|
/// Execute one guest invocation (`__kobako_eval` — one-shot source)
|
|
483
418
|
/// and return a `Snapshot` bundling every per-invocation observable.
|
|
484
419
|
///
|
|
485
|
-
///
|
|
486
|
-
///
|
|
487
|
-
///
|
|
488
|
-
///
|
|
489
|
-
///
|
|
490
|
-
///
|
|
491
|
-
///
|
|
420
|
+
/// Builds a fresh Store + instance whose WASI context carries
|
|
421
|
+
/// the three-frame stdin protocol (`preamble`, `source`, `snippets`
|
|
422
|
+
/// — docs/wire-codec.md § Invocation channels), then invokes
|
|
423
|
+
/// `__kobako_eval`. Per-invocation caps are
|
|
424
|
+
/// primed here: the wall-clock deadline is stamped into `Invocation`
|
|
425
|
+
/// and the epoch deadline is set to fire at the next ticker tick;
|
|
426
|
+
/// the memory-cap limiter is already wired.
|
|
492
427
|
///
|
|
493
428
|
/// On a wasmtime trap the configured-cap path raises
|
|
494
429
|
/// `Kobako::TimeoutError` / `Kobako::MemoryLimitError`; everything
|
|
495
430
|
/// else raises `Kobako::TrapError`. On success the Snapshot carries
|
|
496
431
|
/// the OUTCOME_BUFFER bytes, the per-channel stdout / stderr captures
|
|
497
|
-
/// with their truncation flags, and the
|
|
432
|
+
/// with their truncation flags, and the usage figures.
|
|
498
433
|
pub(crate) fn eval(
|
|
499
434
|
&self,
|
|
500
435
|
preamble: RString,
|
|
@@ -502,27 +437,33 @@ impl Runtime {
|
|
|
502
437
|
snippets: RString,
|
|
503
438
|
) -> Result<Snapshot, MagnusError> {
|
|
504
439
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
505
|
-
let
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
440
|
+
let mut store = self.new_store()?;
|
|
441
|
+
install_wasi_frames(
|
|
442
|
+
&mut store,
|
|
443
|
+
&self.config,
|
|
444
|
+
&[
|
|
445
|
+
rstring_to_vec(preamble),
|
|
446
|
+
rstring_to_vec(source),
|
|
447
|
+
rstring_to_vec(snippets),
|
|
448
|
+
],
|
|
449
|
+
)?;
|
|
450
|
+
let exports = self.instantiate(&ruby, &mut store)?;
|
|
451
|
+
let eval = require_export(&ruby, exports.eval.as_ref())?;
|
|
452
|
+
self.call_with_caps(&mut store, &exports, eval, ())
|
|
512
453
|
.map_err(|e| trap::call_err(&ruby, e))?;
|
|
513
|
-
self.build_snapshot(&ruby)
|
|
454
|
+
self.build_snapshot(&ruby, &mut store, &exports)
|
|
514
455
|
}
|
|
515
456
|
|
|
516
457
|
/// Execute one entrypoint dispatch (`__kobako_run`) and return a
|
|
517
458
|
/// `Snapshot` bundling every per-invocation observable.
|
|
518
459
|
///
|
|
519
|
-
///
|
|
520
|
-
/// (preamble + snippets; no user source
|
|
521
|
-
/// § Invocation channels), copies
|
|
522
|
-
/// memory via `__kobako_alloc`,
|
|
523
|
-
/// env_len)`. Per-invocation cap
|
|
524
|
-
/// Raises `Kobako::TrapError`
|
|
525
|
-
/// allocation fails
|
|
460
|
+
/// Builds a fresh Store + instance whose WASI context carries
|
|
461
|
+
/// the two-frame stdin protocol (preamble + snippets; no user source
|
|
462
|
+
/// frame — docs/wire-codec.md § Invocation channels), copies
|
|
463
|
+
/// `envelope` bytes into guest linear memory via `__kobako_alloc`,
|
|
464
|
+
/// and calls `__kobako_run(env_ptr, env_len)`. Per-invocation cap
|
|
465
|
+
/// semantics match `Runtime::eval`. Raises `Kobako::TrapError`
|
|
466
|
+
/// ("alloc returned 0") when guest allocation fails.
|
|
526
467
|
pub(crate) fn run(
|
|
527
468
|
&self,
|
|
528
469
|
preamble: RString,
|
|
@@ -530,49 +471,21 @@ impl Runtime {
|
|
|
530
471
|
envelope: RString,
|
|
531
472
|
) -> Result<Snapshot, MagnusError> {
|
|
532
473
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
533
|
-
let
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
474
|
+
let mut store = self.new_store()?;
|
|
475
|
+
install_wasi_frames(
|
|
476
|
+
&mut store,
|
|
477
|
+
&self.config,
|
|
478
|
+
&[rstring_to_vec(preamble), rstring_to_vec(snippets)],
|
|
479
|
+
)?;
|
|
480
|
+
let exports = self.instantiate(&ruby, &mut store)?;
|
|
481
|
+
let run = require_export(&ruby, exports.run.as_ref())?;
|
|
482
|
+
let (env_ptr, env_len) = write_envelope(&ruby, &mut store, &exports, envelope)?;
|
|
483
|
+
self.call_with_caps(&mut store, &exports, run, (env_ptr, env_len))
|
|
537
484
|
.map_err(|e| trap::call_err(&ruby, e))?;
|
|
538
|
-
self.build_snapshot(&ruby)
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/// Collect every per-invocation observable into a fresh `Snapshot`.
|
|
542
|
-
/// Called from the run-path methods after the guest export returns
|
|
543
|
-
/// successfully: drains OUTCOME_BUFFER via `__kobako_take_outcome`,
|
|
544
|
-
/// snapshots the per-channel stdout / stderr pipes (clipped to their
|
|
545
|
-
/// caps), and reads B-35 `wall_time` / `memory_peak` from Invocation.
|
|
546
|
-
fn build_snapshot(&self, ruby: &Ruby) -> Result<Snapshot, MagnusError> {
|
|
547
|
-
let return_bytes = self.fetch_outcome_bytes(ruby)?;
|
|
548
|
-
let (stdout_raw, stderr_raw, wall_time, memory_peak) = {
|
|
549
|
-
let state = self.store.borrow();
|
|
550
|
-
let data = state.data();
|
|
551
|
-
(
|
|
552
|
-
data.stdout_bytes(),
|
|
553
|
-
data.stderr_bytes(),
|
|
554
|
-
data.wall_time(),
|
|
555
|
-
data.memory_peak(),
|
|
556
|
-
)
|
|
557
|
-
};
|
|
558
|
-
let (stdout_visible, stdout_truncated) =
|
|
559
|
-
capture::clip_capture(&stdout_raw, self.config.stdout_limit_bytes);
|
|
560
|
-
let stdout_bytes = stdout_visible.to_vec();
|
|
561
|
-
let (stderr_visible, stderr_truncated) =
|
|
562
|
-
capture::clip_capture(&stderr_raw, self.config.stderr_limit_bytes);
|
|
563
|
-
let stderr_bytes = stderr_visible.to_vec();
|
|
564
|
-
Ok(Snapshot::new(
|
|
565
|
-
return_bytes,
|
|
566
|
-
stdout_bytes,
|
|
567
|
-
stdout_truncated,
|
|
568
|
-
stderr_bytes,
|
|
569
|
-
stderr_truncated,
|
|
570
|
-
wall_time,
|
|
571
|
-
memory_peak,
|
|
572
|
-
))
|
|
485
|
+
self.build_snapshot(&ruby, &mut store, &exports)
|
|
573
486
|
}
|
|
574
487
|
|
|
575
|
-
/// Return the
|
|
488
|
+
/// Return the per-last-invocation usage as a
|
|
576
489
|
/// Ruby 2-tuple `[wall_time, memory_peak]`. The element order
|
|
577
490
|
/// matches the `Kobako::Usage` field order declared in
|
|
578
491
|
/// `lib/kobako/usage.rb`; reorder both sides together if the field
|
|
@@ -581,27 +494,22 @@ impl Runtime {
|
|
|
581
494
|
/// * `wall_time` (Float seconds) — the wall-clock duration the
|
|
582
495
|
/// most recent invocation spent inside the guest export call.
|
|
583
496
|
/// Bracket opens in `Runtime::prime_caps` and closes in
|
|
584
|
-
/// `
|
|
585
|
-
///
|
|
586
|
-
///
|
|
587
|
-
/// `OUTCOME_BUFFER` fetch and decode, plus stdout / stderr
|
|
588
|
-
/// capture readout. `0.0` before the first invocation.
|
|
497
|
+
/// `disarm_caps`, so the value mirrors the `timeout` deadline
|
|
498
|
+
/// accounting and excludes everything that runs after the guest
|
|
499
|
+
/// export returns. `0.0` before the first invocation.
|
|
589
500
|
/// * `memory_peak` (Integer bytes) — the high-water mark of the
|
|
590
501
|
/// per-invocation `memory.grow` delta past the linear-memory
|
|
591
502
|
/// size captured at invocation entry. `0` before the first
|
|
592
503
|
/// invocation.
|
|
593
504
|
///
|
|
594
|
-
///
|
|
595
|
-
///
|
|
596
|
-
/// `store.borrow()` per readout and a single magnus binding to
|
|
597
|
-
/// extend when B-35's field list grows past two.
|
|
505
|
+
/// Reads the `last_usage` Cell `build_snapshot` populated before the
|
|
506
|
+
/// per-invocation Store was discarded.
|
|
598
507
|
pub(crate) fn usage(&self) -> Result<RArray, MagnusError> {
|
|
599
508
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
600
|
-
let
|
|
601
|
-
let data = state.data();
|
|
509
|
+
let (wall_time, memory_peak) = self.last_usage.get();
|
|
602
510
|
let arr = ruby.ary_new_capa(2);
|
|
603
|
-
arr.push(
|
|
604
|
-
arr.push(
|
|
511
|
+
arr.push(wall_time)?;
|
|
512
|
+
arr.push(memory_peak)?;
|
|
605
513
|
Ok(arr)
|
|
606
514
|
}
|
|
607
515
|
|
|
@@ -609,17 +517,54 @@ impl Runtime {
|
|
|
609
517
|
// Private helpers.
|
|
610
518
|
// -----------------------------------------------------------------
|
|
611
519
|
|
|
520
|
+
/// Build the per-invocation Store: a fresh `Invocation` wired with
|
|
521
|
+
/// the memory limiter, the epoch-deadline callback, and the
|
|
522
|
+
/// registered dispatch Proc.
|
|
523
|
+
fn new_store(&self) -> Result<WtStore<Invocation>, MagnusError> {
|
|
524
|
+
let mut store = WtStore::new(shared_engine()?, Invocation::new(self.memory_limit));
|
|
525
|
+
store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
|
|
526
|
+
store.epoch_deadline_callback(trap::epoch_deadline_callback);
|
|
527
|
+
if let Some(on_dispatch) = self.on_dispatch.get() {
|
|
528
|
+
store.data_mut().bind_on_dispatch(on_dispatch);
|
|
529
|
+
}
|
|
530
|
+
Ok(store)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/// Instantiate the per-invocation instance from the pre-linked
|
|
534
|
+
/// template and resolve its host-driven export handles. An
|
|
535
|
+
/// instantiation failure at invocation time is an engine fault —
|
|
536
|
+
/// `Kobako::TrapError` — unlike the construction-time probe, whose
|
|
537
|
+
/// failure is `SetupError`.
|
|
538
|
+
fn instantiate(
|
|
539
|
+
&self,
|
|
540
|
+
ruby: &Ruby,
|
|
541
|
+
store: &mut WtStore<Invocation>,
|
|
542
|
+
) -> Result<Exports, MagnusError> {
|
|
543
|
+
let instance = self
|
|
544
|
+
.instance_pre
|
|
545
|
+
.instantiate(store.as_context_mut())
|
|
546
|
+
.map_err(|e| {
|
|
547
|
+
trap_err(
|
|
548
|
+
ruby,
|
|
549
|
+
format!("failed to instantiate the Sandbox runtime: {e}"),
|
|
550
|
+
)
|
|
551
|
+
})?;
|
|
552
|
+
Ok(Exports::resolve(&instance, store.as_context_mut()))
|
|
553
|
+
}
|
|
554
|
+
|
|
612
555
|
/// Run one guest export call inside the per-invocation cap window:
|
|
613
|
-
/// `Runtime::prime_caps` before, `
|
|
614
|
-
///
|
|
556
|
+
/// `Runtime::prime_caps` before, `disarm_caps` after — the shared
|
|
557
|
+
/// bracket for both run-path exports (`__kobako_eval` /
|
|
615
558
|
/// `__kobako_run`). Disarm runs whether the call returns or traps, so
|
|
616
|
-
/// the
|
|
559
|
+
/// the `wall_time` bracket and the memory
|
|
617
560
|
/// cap always close — that close-on-trap guarantee is the reason this
|
|
618
561
|
/// bracket lives in one place rather than inline at each call site.
|
|
619
562
|
/// The wasmtime trap is returned unmapped; each caller wraps it
|
|
620
563
|
/// through `trap::call_err` for its own error context.
|
|
621
564
|
fn call_with_caps<Params, Results>(
|
|
622
565
|
&self,
|
|
566
|
+
store: &mut WtStore<Invocation>,
|
|
567
|
+
exports: &Exports,
|
|
623
568
|
export: &TypedFunc<Params, Results>,
|
|
624
569
|
params: Params,
|
|
625
570
|
) -> Result<Results, wasmtime::Error>
|
|
@@ -627,12 +572,15 @@ impl Runtime {
|
|
|
627
572
|
Params: wasmtime::WasmParams,
|
|
628
573
|
Results: wasmtime::WasmResults,
|
|
629
574
|
{
|
|
630
|
-
self.prime_caps();
|
|
631
|
-
let result =
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
575
|
+
self.prime_caps(store, exports);
|
|
576
|
+
let result = export.call(store.as_context_mut(), params);
|
|
577
|
+
disarm_caps(store);
|
|
578
|
+
// Stash the usage figures on every outcome — including the
|
|
579
|
+
// trap paths, where `build_snapshot` never runs and the Store is
|
|
580
|
+
// about to be discarded with the error.
|
|
581
|
+
let data = store.data();
|
|
582
|
+
self.last_usage
|
|
583
|
+
.set((data.wall_time().as_secs_f64(), data.memory_peak()));
|
|
636
584
|
result
|
|
637
585
|
}
|
|
638
586
|
|
|
@@ -643,173 +591,211 @@ impl Runtime {
|
|
|
643
591
|
/// effectively never fires.
|
|
644
592
|
///
|
|
645
593
|
/// Also captures the current linear-memory size as the baseline
|
|
646
|
-
/// for the
|
|
647
|
-
///
|
|
648
|
-
///
|
|
649
|
-
///
|
|
650
|
-
///
|
|
651
|
-
///
|
|
652
|
-
/// Also stamps the wall-clock entry instant for the
|
|
653
|
-
/// docs/behavior.md B-35 `wall_time` measurement. The bracket
|
|
654
|
-
/// closes in `Runtime::disarm_caps` so it matches the
|
|
655
|
-
/// `timeout` deadline window and excludes `OUTCOME_BUFFER`
|
|
594
|
+
/// for the per-invocation memory delta cap —
|
|
595
|
+
/// the pre-initialized image's allocation is folded into the
|
|
596
|
+
/// baseline rather than the budget — and stamps the wall-clock
|
|
597
|
+
/// entry instant for the `wall_time`
|
|
598
|
+
/// measurement. The bracket closes in `disarm_caps` so it matches
|
|
599
|
+
/// the `timeout` deadline window and excludes `OUTCOME_BUFFER`
|
|
656
600
|
/// decoding and stdout / stderr capture readout.
|
|
657
|
-
fn prime_caps(&self) {
|
|
658
|
-
let mut store_ref = self.store.borrow_mut();
|
|
601
|
+
fn prime_caps(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
|
|
659
602
|
match self.config.timeout {
|
|
660
603
|
Some(timeout) => {
|
|
661
604
|
let deadline = Instant::now() + timeout;
|
|
662
|
-
|
|
663
|
-
|
|
605
|
+
store.data_mut().set_deadline(Some(deadline));
|
|
606
|
+
store.set_epoch_deadline(1);
|
|
664
607
|
}
|
|
665
608
|
None => {
|
|
666
|
-
|
|
667
|
-
|
|
609
|
+
store.data_mut().set_deadline(None);
|
|
610
|
+
store.set_epoch_deadline(u64::MAX);
|
|
668
611
|
}
|
|
669
612
|
}
|
|
670
|
-
let baseline = match
|
|
671
|
-
Some(
|
|
672
|
-
|
|
613
|
+
let baseline = match exports.memory {
|
|
614
|
+
Some(m) => m.data_size(store.as_context_mut()),
|
|
615
|
+
None => 0,
|
|
673
616
|
};
|
|
674
|
-
|
|
675
|
-
|
|
617
|
+
store.data_mut().arm_memory_cap(baseline);
|
|
618
|
+
store.data_mut().start_wall_clock();
|
|
676
619
|
}
|
|
677
620
|
|
|
678
|
-
///
|
|
679
|
-
///
|
|
680
|
-
///
|
|
681
|
-
/// the
|
|
682
|
-
///
|
|
683
|
-
///
|
|
684
|
-
fn
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
621
|
+
/// Collect every per-invocation observable into a fresh `Snapshot`.
|
|
622
|
+
/// Called from the run-path methods after the guest export returns
|
|
623
|
+
/// successfully: drains OUTCOME_BUFFER via `__kobako_take_outcome`
|
|
624
|
+
/// and snapshots the per-channel stdout / stderr pipes (clipped to
|
|
625
|
+
/// their caps). The usage figures were already stashed by
|
|
626
|
+
/// `call_with_caps`.
|
|
627
|
+
fn build_snapshot(
|
|
628
|
+
&self,
|
|
629
|
+
ruby: &Ruby,
|
|
630
|
+
store: &mut WtStore<Invocation>,
|
|
631
|
+
exports: &Exports,
|
|
632
|
+
) -> Result<Snapshot, MagnusError> {
|
|
633
|
+
let return_bytes = fetch_outcome_bytes(ruby, store, exports)?;
|
|
634
|
+
let data = store.data();
|
|
635
|
+
let (stdout_raw, stderr_raw, wall_time, memory_peak) = (
|
|
636
|
+
data.stdout_bytes(),
|
|
637
|
+
data.stderr_bytes(),
|
|
638
|
+
data.wall_time(),
|
|
639
|
+
data.memory_peak(),
|
|
640
|
+
);
|
|
641
|
+
let (stdout_visible, stdout_truncated) =
|
|
642
|
+
capture::clip_capture(&stdout_raw, self.config.stdout_limit_bytes);
|
|
643
|
+
let stdout_bytes = stdout_visible.to_vec();
|
|
644
|
+
let (stderr_visible, stderr_truncated) =
|
|
645
|
+
capture::clip_capture(&stderr_raw, self.config.stderr_limit_bytes);
|
|
646
|
+
let stderr_bytes = stderr_visible.to_vec();
|
|
647
|
+
Ok(Snapshot::new(
|
|
648
|
+
return_bytes,
|
|
649
|
+
stdout_bytes,
|
|
650
|
+
stdout_truncated,
|
|
651
|
+
stderr_bytes,
|
|
652
|
+
stderr_truncated,
|
|
653
|
+
wall_time,
|
|
654
|
+
memory_peak,
|
|
655
|
+
))
|
|
688
656
|
}
|
|
657
|
+
}
|
|
689
658
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
guest_mem::checked_payload_len(bytes.len()).map_err(|msg| trap_err(ruby, msg))?;
|
|
701
|
-
|
|
702
|
-
let mut store_ref = self.store.borrow_mut();
|
|
703
|
-
let alloc: TypedFunc<u32, u32> = self
|
|
704
|
-
.inner
|
|
705
|
-
.get_typed_func(store_ref.as_context_mut(), "__kobako_alloc")
|
|
706
|
-
.map_err(|_| trap_err(ruby, SANDBOX_RUNTIME_MISSING_HOOKS))?;
|
|
707
|
-
let ptr = alloc
|
|
708
|
-
.call(store_ref.as_context_mut(), bytes.len() as u32)
|
|
709
|
-
.map_err(|e| trap_err(ruby, format!("failed to allocate input buffer: {}", e)))?;
|
|
710
|
-
if ptr == 0 {
|
|
711
|
-
return Err(sandbox_err(
|
|
712
|
-
ruby,
|
|
713
|
-
"could not allocate input buffer (out of memory)",
|
|
714
|
-
));
|
|
715
|
-
}
|
|
659
|
+
/// Drop the memory cap as soon as the guest call returns so that
|
|
660
|
+
/// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
|
|
661
|
+
/// which can grow guest memory transiently) is not attributed to
|
|
662
|
+
/// the user script. Also closes the
|
|
663
|
+
/// `wall_time` bracket opened by `Runtime::prime_caps`. Paired
|
|
664
|
+
/// with `Runtime::prime_caps`.
|
|
665
|
+
fn disarm_caps(store: &mut WtStore<Invocation>) {
|
|
666
|
+
store.data_mut().stop_wall_clock();
|
|
667
|
+
store.data_mut().disarm_memory_cap();
|
|
668
|
+
}
|
|
716
669
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
670
|
+
/// Return the resolved `memory` export handle, or raise
|
|
671
|
+
/// `Kobako::TrapError` when the loaded module exports no linear
|
|
672
|
+
/// memory — the "not a Kobako-shaped runtime" failure mode
|
|
673
|
+
/// (`SANDBOX_RUNTIME_NOT_KOBAKO`).
|
|
674
|
+
fn require_memory(ruby: &Ruby, exports: &Exports) -> Result<Memory, MagnusError> {
|
|
675
|
+
exports
|
|
676
|
+
.memory
|
|
677
|
+
.ok_or_else(|| trap_err(ruby, SANDBOX_RUNTIME_NOT_KOBAKO))
|
|
678
|
+
}
|
|
725
679
|
|
|
726
|
-
|
|
680
|
+
/// Allocate a `len`-byte buffer in guest linear memory via
|
|
681
|
+
/// `__kobako_alloc`, copy `envelope` into it, and return `(ptr, len)`
|
|
682
|
+
/// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
|
|
683
|
+
/// Raises `Kobako::TrapError` when the allocation hook is missing or
|
|
684
|
+
/// itself traps, and `Kobako::SandboxError` when the hook runs but
|
|
685
|
+
/// cannot reserve the buffer (`__kobako_alloc` returns 0) — an
|
|
686
|
+
/// intact runtime, not an engine fault.
|
|
687
|
+
fn write_envelope(
|
|
688
|
+
ruby: &Ruby,
|
|
689
|
+
store: &mut WtStore<Invocation>,
|
|
690
|
+
exports: &Exports,
|
|
691
|
+
envelope: RString,
|
|
692
|
+
) -> Result<(i32, i32), MagnusError> {
|
|
693
|
+
let bytes = rstring_to_vec(envelope);
|
|
694
|
+
let len_i32 = guest_mem::checked_payload_len(bytes.len()).map_err(|msg| trap_err(ruby, msg))?;
|
|
695
|
+
|
|
696
|
+
let alloc = require_export(ruby, exports.alloc.as_ref())?;
|
|
697
|
+
let memory = require_memory(ruby, exports)?;
|
|
698
|
+
|
|
699
|
+
let ptr = alloc
|
|
700
|
+
.call(store.as_context_mut(), bytes.len() as u32)
|
|
701
|
+
.map_err(|e| trap_err(ruby, format!("failed to allocate input buffer: {}", e)))?;
|
|
702
|
+
if ptr == 0 {
|
|
703
|
+
return Err(sandbox_err(
|
|
704
|
+
ruby,
|
|
705
|
+
"could not allocate input buffer (out of memory)",
|
|
706
|
+
));
|
|
727
707
|
}
|
|
708
|
+
let data = memory.data_mut(store.as_context_mut());
|
|
709
|
+
let range = guest_mem::guest_buffer_range(ptr as usize, bytes.len(), data.len())
|
|
710
|
+
.map_err(|msg| trap_err(ruby, msg))?;
|
|
711
|
+
data[range].copy_from_slice(&bytes);
|
|
728
712
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
/// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
|
|
732
|
-
/// pipes. Called at the top of every guest invocation: `#eval` passes
|
|
733
|
-
/// three frames (preamble, source, snippets), `#run` passes two
|
|
734
|
-
/// (preamble, snippets — the invocation envelope arrives via linear
|
|
735
|
-
/// memory instead). Each output pipe is sized at `cap + 1` so
|
|
736
|
-
/// `capture::clip_capture` can distinguish "wrote exactly cap
|
|
737
|
-
/// bytes" from "exceeded cap"; uncapped channels fall back
|
|
738
|
-
/// to `usize::MAX` and rely on `memory_limit` (docs/behavior.md E-20)
|
|
739
|
-
/// for the real ceiling. Raises `Kobako::TrapError` when any frame
|
|
740
|
-
/// exceeds the 16 MiB cap that keeps its `u32` length prefix from
|
|
741
|
-
/// wrapping.
|
|
742
|
-
fn refresh_wasi(&self, frames: &[Vec<u8>]) -> Result<(), MagnusError> {
|
|
743
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
744
|
-
// Every frame carries the same 16 MiB cap as the `#run` envelope
|
|
745
|
-
// (`write_envelope`): the length prefix is a `u32`, so a frame past
|
|
746
|
-
// the cap would silently wrap and corrupt the stdin frame stream.
|
|
747
|
-
for frame in frames {
|
|
748
|
-
guest_mem::checked_payload_len(frame.len()).map_err(|msg| trap_err(&ruby, msg))?;
|
|
749
|
-
}
|
|
713
|
+
Ok((ptr as i32, len_i32))
|
|
714
|
+
}
|
|
750
715
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
716
|
+
/// Build the per-invocation WASI context with stdin carrying every frame
|
|
717
|
+
/// in `frames` (each prefixed by its 4-byte big-endian u32 length —
|
|
718
|
+
/// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
|
|
719
|
+
/// pipes, and install it on the invocation's Store. `#eval` passes three
|
|
720
|
+
/// frames (preamble, source, snippets), `#run` passes two (preamble,
|
|
721
|
+
/// snippets — the invocation envelope arrives via linear memory
|
|
722
|
+
/// instead). Each output pipe is sized at `cap + 1` so
|
|
723
|
+
/// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
|
|
724
|
+
/// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
|
|
725
|
+
/// rely on `memory_limit` for the real ceiling.
|
|
726
|
+
/// Raises `Kobako::TrapError` when any frame exceeds the 16 MiB cap that
|
|
727
|
+
/// keeps its `u32` length prefix from wrapping.
|
|
728
|
+
fn install_wasi_frames(
|
|
729
|
+
store: &mut WtStore<Invocation>,
|
|
730
|
+
config: &Config,
|
|
731
|
+
frames: &[Vec<u8>],
|
|
732
|
+
) -> Result<(), MagnusError> {
|
|
733
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
734
|
+
// Every frame carries the same 16 MiB cap as the `#run` envelope
|
|
735
|
+
// (`write_envelope`): the length prefix is a `u32`, so a frame past
|
|
736
|
+
// the cap would silently wrap and corrupt the stdin frame stream.
|
|
737
|
+
for frame in frames {
|
|
738
|
+
guest_mem::checked_payload_len(frame.len()).map_err(|msg| trap_err(&ruby, msg))?;
|
|
739
|
+
}
|
|
757
740
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
let mut builder = WasiCtxBuilder::new();
|
|
765
|
-
builder.stdin(stdin_pipe);
|
|
766
|
-
builder.stdout(stdout_pipe.clone());
|
|
767
|
-
builder.stderr(stderr_pipe.clone());
|
|
768
|
-
// Deny the preview1 ambient-authority imports the guest never legitimately
|
|
769
|
-
// reaches but the WASI layer would otherwise grant (see `ambient`).
|
|
770
|
-
builder.wall_clock(ambient::FrozenWallClock);
|
|
771
|
-
builder.monotonic_clock(ambient::FrozenMonotonicClock);
|
|
772
|
-
builder.secure_random(ambient::deterministic_rng());
|
|
773
|
-
let wasi = builder.build_p1();
|
|
774
|
-
|
|
775
|
-
self.store
|
|
776
|
-
.borrow_mut()
|
|
777
|
-
.data_mut()
|
|
778
|
-
.install_wasi(wasi, stdout_pipe, stderr_pipe);
|
|
779
|
-
Ok(())
|
|
741
|
+
let total: usize = frames.iter().map(|f| 4 + f.len()).sum();
|
|
742
|
+
let mut stdin_content: Vec<u8> = Vec::with_capacity(total);
|
|
743
|
+
for frame in frames {
|
|
744
|
+
stdin_content.extend_from_slice(&(frame.len() as u32).to_be_bytes());
|
|
745
|
+
stdin_content.extend_from_slice(frame);
|
|
780
746
|
}
|
|
781
747
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
748
|
+
let stdin_pipe = MemoryInputPipe::new(stdin_content);
|
|
749
|
+
let stdout_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stdout_limit_bytes));
|
|
750
|
+
let stderr_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stderr_limit_bytes));
|
|
751
|
+
|
|
752
|
+
let mut builder = WasiCtxBuilder::new();
|
|
753
|
+
builder.stdin(stdin_pipe);
|
|
754
|
+
builder.stdout(stdout_pipe.clone());
|
|
755
|
+
builder.stderr(stderr_pipe.clone());
|
|
756
|
+
// Deny the preview1 ambient-authority imports the guest never legitimately
|
|
757
|
+
// reaches but the WASI layer would otherwise grant (see `ambient`).
|
|
758
|
+
builder.wall_clock(ambient::FrozenWallClock);
|
|
759
|
+
builder.monotonic_clock(ambient::FrozenMonotonicClock);
|
|
760
|
+
builder.secure_random(ambient::deterministic_rng());
|
|
761
|
+
let wasi = builder.build_p1();
|
|
762
|
+
|
|
763
|
+
store
|
|
764
|
+
.data_mut()
|
|
765
|
+
.install_wasi(wasi, stdout_pipe, stderr_pipe);
|
|
766
|
+
Ok(())
|
|
767
|
+
}
|
|
799
768
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
769
|
+
/// Invoke `__kobako_take_outcome`, decode the packed `(ptr<<32)|len`
|
|
770
|
+
/// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Raises
|
|
771
|
+
/// `Kobako::TrapError` when the export is missing, `len` exceeds the
|
|
772
|
+
/// 16 MiB single-dispatch cap, the `ptr`/`len` arithmetic overflows,
|
|
773
|
+
/// the slice falls outside live memory, or the `memory` export itself
|
|
774
|
+
/// is absent.
|
|
775
|
+
fn fetch_outcome_bytes(
|
|
776
|
+
ruby: &Ruby,
|
|
777
|
+
store: &mut WtStore<Invocation>,
|
|
778
|
+
exports: &Exports,
|
|
779
|
+
) -> Result<Vec<u8>, MagnusError> {
|
|
780
|
+
let take = require_export(ruby, exports.take_outcome.as_ref())?;
|
|
781
|
+
let mem = require_memory(ruby, exports)?;
|
|
782
|
+
|
|
783
|
+
let packed = take
|
|
784
|
+
.call(store.as_context_mut(), ())
|
|
785
|
+
.map_err(|e| trap_err(ruby, format!("failed to read the Sandbox result: {}", e)))?;
|
|
786
|
+
let (ptr, len) = guest_mem::unpack_outcome_packed(packed);
|
|
787
|
+
if len > guest_mem::MAX_DISPATCH_PAYLOAD {
|
|
788
|
+
return Err(trap_err(ruby, "result payload exceeds the 16 MiB limit"));
|
|
812
789
|
}
|
|
790
|
+
|
|
791
|
+
let data = mem.data(store.as_context_mut());
|
|
792
|
+
let range = guest_mem::guest_buffer_range(ptr, len, data.len()).map_err(|msg| {
|
|
793
|
+
trap_err(
|
|
794
|
+
ruby,
|
|
795
|
+
format!("the Sandbox result is out of bounds: {}", msg),
|
|
796
|
+
)
|
|
797
|
+
})?;
|
|
798
|
+
Ok(data[range].to_vec())
|
|
813
799
|
}
|
|
814
800
|
|
|
815
801
|
/// User-facing message for the "Sandbox runtime is missing one of the
|
|
@@ -829,7 +815,7 @@ const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required
|
|
|
829
815
|
const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
|
|
830
816
|
"the loaded Wasm module is not a Kobako-compatible runtime";
|
|
831
817
|
|
|
832
|
-
/// Return the
|
|
818
|
+
/// Return the resolved `TypedFunc` for an ABI export, or raise
|
|
833
819
|
/// `Kobako::TrapError` when the option is `None`. Both run-path
|
|
834
820
|
/// methods (`#eval`, `#run`) plus the `build_snapshot` readout that
|
|
835
821
|
/// drains `OUTCOME_BUFFER` share the same "missing export → Ruby
|