kobako 0.12.1 → 0.12.2
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 +11 -0
- data/Cargo.lock +15 -2
- data/Cargo.toml +6 -2
- data/README.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +8 -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 +16 -0
- data/crates/kobako-runtime/src/runtime.rs +50 -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 +8 -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 +3 -3
- 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 +25 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
- data/crates/kobako-wasmtime/src/driver.rs +285 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/frames.rs +70 -82
- 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/runtime/bridge.rs +150 -0
- data/ext/kobako/src/runtime/errors.rs +45 -13
- data/ext/kobako/src/runtime.rs +156 -406
- data/ext/kobako/src/snapshot.rs +27 -62
- 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/errors.rb +18 -16
- data/lib/kobako/sandbox.rb +20 -18
- data/lib/kobako/sandbox_options.rb +25 -9
- data/lib/kobako/snapshot.rb +7 -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/release-please-config.json +48 -3
- data/sig/kobako/codec/factory.rbs +3 -0
- data/sig/kobako/errors.rbs +7 -14
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/sandbox_options.rbs +4 -2
- data/sig/kobako/snapshot.rbs +0 -3
- 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 +27 -12
- data/ext/kobako/src/runtime/config.rs +0 -25
- data/ext/kobako/src/runtime/dispatch.rs +0 -211
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -1,82 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// memory cap, and the trap marker types
|
|
28
|
-
// (`TimeoutTrap` / `MemoryLimitTrap`).
|
|
29
|
-
// * `dispatch` — `__kobako_dispatch` host-import dispatch helpers.
|
|
30
|
-
// * `guest_mem` — Caller-based guest linear-memory alloc / write / read.
|
|
31
|
-
// * `capture` — stdout / stderr pipe sizing + clip helpers.
|
|
32
|
-
// * `trap` — wasmtime-error → `Kobako::*` trap classification.
|
|
33
|
-
//
|
|
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` /
|
|
37
|
-
// `timeout_err` / `memory_limit_err` / `setup_err` constructors shared by
|
|
38
|
-
// every submodule, and the Ruby init() that registers the class.
|
|
39
|
-
|
|
40
|
-
mod ambient;
|
|
41
|
-
mod cache;
|
|
42
|
-
mod capture;
|
|
43
|
-
mod config;
|
|
44
|
-
mod dispatch;
|
|
1
|
+
//! Host-side magnus shell over the extracted wasmtime driver.
|
|
2
|
+
//!
|
|
3
|
+
//! The only Ruby-visible class is
|
|
4
|
+
//!
|
|
5
|
+
//! Kobako::Runtime — wraps a `kobako_wasmtime::Driver` + the Ruby seams
|
|
6
|
+
//!
|
|
7
|
+
//! constructed via `Kobako::Runtime.from_path(path, timeout, memory_limit,
|
|
8
|
+
//! stdout_limit, stderr_limit)`. Every invocation (`#eval` / `#run`)
|
|
9
|
+
//! instantiates a fresh instance and discards the whole Store afterwards —
|
|
10
|
+
//! the per-invocation instance discipline. The run mechanics —
|
|
11
|
+
//! engine/module caches, caps, trap classification — live in the
|
|
12
|
+
//! `kobako-wasmtime` crate behind the `kobako_runtime` contract; no wasm
|
|
13
|
+
//! engine type reaches this crate or the Host App.
|
|
14
|
+
//!
|
|
15
|
+
//! Module layout — one responsibility per file:
|
|
16
|
+
//!
|
|
17
|
+
//! * `bridge` — the magnus dispatch bridge: `RubyDispatchHandler` plus the
|
|
18
|
+
//! frame-scoped `GuestYielder` Ruby class.
|
|
19
|
+
//! * `errors` — the single boundary mapping the neutral `Trap` /
|
|
20
|
+
//! `SetupError` channels onto the `Kobako::*` classes.
|
|
21
|
+
//!
|
|
22
|
+
//! This file owns the `Kobako::Runtime` magnus class itself — the Ruby
|
|
23
|
+
//! init() that registers the class, the byte↔`RString` shuttling, the
|
|
24
|
+
//! dispatch-Proc GC root, and the per-invocation usage readout.
|
|
25
|
+
|
|
26
|
+
mod bridge;
|
|
45
27
|
mod errors;
|
|
46
|
-
mod exports;
|
|
47
|
-
mod frames;
|
|
48
|
-
mod guest_mem;
|
|
49
|
-
mod instance_pre;
|
|
50
|
-
mod invocation;
|
|
51
|
-
mod trap;
|
|
52
28
|
|
|
53
29
|
use magnus::{function, method, prelude::*, Error as MagnusError, RModule, RString, Ruby};
|
|
54
30
|
|
|
55
31
|
use std::cell::Cell;
|
|
56
32
|
use std::path::Path;
|
|
57
|
-
use std::
|
|
33
|
+
use std::sync::Arc;
|
|
34
|
+
use std::time::Duration;
|
|
58
35
|
|
|
59
36
|
use magnus::{gc, typed_data::DataTypeFunctions, value::Opaque, RArray, TypedData, Value};
|
|
60
37
|
|
|
61
38
|
use crate::snapshot::Snapshot;
|
|
62
|
-
use
|
|
63
|
-
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
use self::cache::shared_engine;
|
|
67
|
-
use self::config::Config;
|
|
68
|
-
use self::exports::Exports;
|
|
69
|
-
use self::invocation::Invocation;
|
|
70
|
-
|
|
71
|
-
/// The wire ABI version this host implements (docs/wire-codec.md § ABI
|
|
72
|
-
/// Version). A Guest Binary is accepted only when its
|
|
73
|
-
/// `__kobako_abi_version` export reports the same value; a mismatch
|
|
74
|
-
/// is a deterministic artifact fault. The guest-side mirror is
|
|
75
|
-
/// `kobako_core::abi::ABI_VERSION`. Version 2
|
|
76
|
-
/// carries the per-invocation instance discipline: the host
|
|
77
|
-
/// drives every invocation on a fresh instance, so the guest may leave
|
|
78
|
-
/// its VM state dirty at exit.
|
|
79
|
-
const ABI_VERSION: u32 = 2;
|
|
39
|
+
use kobako_runtime::dispatch::DispatchHandler;
|
|
40
|
+
use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
|
|
41
|
+
use kobako_runtime::snapshot::{Completion, Snapshot as RuntimeSnapshot, Usage};
|
|
42
|
+
use kobako_wasmtime::{Config, Driver};
|
|
80
43
|
|
|
81
44
|
/// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
|
|
82
45
|
/// what would otherwise be an inline `unsafe { rstring.as_slice() }
|
|
@@ -84,7 +47,7 @@ const ABI_VERSION: u32 = 2;
|
|
|
84
47
|
/// does not outlive this call, so no Ruby allocation can move the
|
|
85
48
|
/// underlying RString between the borrow and the copy — the safety
|
|
86
49
|
/// invariant the inline form relied on is established once here.
|
|
87
|
-
|
|
50
|
+
fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
88
51
|
// SAFETY: see item doc.
|
|
89
52
|
unsafe { s.as_slice() }.to_vec()
|
|
90
53
|
}
|
|
@@ -104,47 +67,37 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
104
67
|
let runtime = kobako.define_class("Runtime", ruby.class_object())?;
|
|
105
68
|
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 5))?;
|
|
106
69
|
runtime.define_method("on_dispatch=", method!(Runtime::set_on_dispatch, 1))?;
|
|
107
|
-
runtime.define_method(
|
|
108
|
-
"yield_to_active_invocation",
|
|
109
|
-
method!(Runtime::yield_to_active_invocation, 1),
|
|
110
|
-
)?;
|
|
111
70
|
runtime.define_method("eval", method!(Runtime::eval, 3))?;
|
|
112
71
|
runtime.define_method("run", method!(Runtime::run, 3))?;
|
|
113
72
|
runtime.define_method("usage", method!(Runtime::usage, 0))?;
|
|
73
|
+
// The guest re-enters for a block yield through a frame-scoped
|
|
74
|
+
// `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
|
|
75
|
+
// method on Runtime.
|
|
76
|
+
bridge::register(runtime)?;
|
|
114
77
|
|
|
115
78
|
Ok(())
|
|
116
79
|
}
|
|
117
80
|
|
|
118
81
|
#[derive(TypedData)]
|
|
119
82
|
#[magnus(class = "Kobako::Runtime", free_immediately, size, mark)]
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
// whole Store afterwards — the per-invocation instance discipline.
|
|
125
|
-
instance_pre: WtInstancePre<Invocation>,
|
|
126
|
-
// Per-invocation linear-memory cap,
|
|
127
|
-
// threaded into each fresh `Invocation`; lives apart from `Config`
|
|
128
|
-
// because the wasmtime `ResourceLimiter` callback consumes it from
|
|
129
|
-
// inside the wasm engine.
|
|
130
|
-
memory_limit: Option<usize>,
|
|
131
|
-
// Wall-clock + per-channel capture caps forwarded from the Sandbox;
|
|
132
|
-
// see `Config`.
|
|
133
|
-
config: Config,
|
|
83
|
+
struct Runtime {
|
|
84
|
+
// The magnus-free wasmtime driver that runs every invocation; the
|
|
85
|
+
// shell only shuttles Ruby values across its boundary.
|
|
86
|
+
driver: Driver,
|
|
134
87
|
// The host-side dispatch Proc, held here only
|
|
135
88
|
// to give `DataTypeFunctions::mark` a read path so it can pin the
|
|
136
|
-
// Proc across GC.
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
89
|
+
// Proc across GC. For each invocation `build_handler` wraps a copy of
|
|
90
|
+
// this handle in a `RubyDispatchHandler`, and the driver's `invoke`
|
|
91
|
+
// binds that `Arc<dyn DispatchHandler>` onto the per-invocation
|
|
92
|
+
// `Invocation`, where the `__kobako_dispatch` import calls it — both
|
|
93
|
+
// reference the one Proc this `Opaque` pins. `Cell` is sound under the
|
|
94
|
+
// GVL (see the `unsafe impl Sync` below).
|
|
141
95
|
on_dispatch: Cell<Option<Opaque<Value>>>,
|
|
142
|
-
// Usage of the most recent invocation
|
|
143
|
-
// `
|
|
144
|
-
//
|
|
145
|
-
// `#usage` reads survive the teardown. `(0.0, 0)` before the first
|
|
96
|
+
// Usage of the most recent invocation, kept apart from the returned
|
|
97
|
+
// `Snapshot` so `#usage` reads survive the per-invocation Store
|
|
98
|
+
// teardown and the trap path's raise. Zeroed before the first
|
|
146
99
|
// invocation.
|
|
147
|
-
last_usage: Cell<
|
|
100
|
+
last_usage: Cell<Usage>,
|
|
148
101
|
}
|
|
149
102
|
|
|
150
103
|
impl DataTypeFunctions for Runtime {
|
|
@@ -185,7 +138,7 @@ impl Runtime {
|
|
|
185
138
|
/// disables). All four are validated by the caller
|
|
186
139
|
/// (`Kobako::Sandbox`); this method only refuses non-finite or
|
|
187
140
|
/// non-positive timeouts as a defence in depth.
|
|
188
|
-
|
|
141
|
+
fn from_path(
|
|
189
142
|
path: String,
|
|
190
143
|
timeout_seconds: Option<f64>,
|
|
191
144
|
memory_limit: Option<usize>,
|
|
@@ -209,112 +162,38 @@ impl Runtime {
|
|
|
209
162
|
}
|
|
210
163
|
};
|
|
211
164
|
|
|
212
|
-
let
|
|
213
|
-
|
|
165
|
+
let driver = Driver::new(
|
|
166
|
+
Path::new(&path),
|
|
214
167
|
memory_limit,
|
|
215
|
-
|
|
168
|
+
Config {
|
|
216
169
|
timeout,
|
|
217
170
|
stdout_limit_bytes,
|
|
218
171
|
stderr_limit_bytes,
|
|
219
172
|
},
|
|
173
|
+
)
|
|
174
|
+
.map_err(|e| errors::setup_to_magnus(&ruby, e))?;
|
|
175
|
+
Ok(Self {
|
|
176
|
+
driver,
|
|
220
177
|
on_dispatch: Cell::new(None),
|
|
221
|
-
last_usage: Cell::new(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
/// Instantiate a throwaway probe instance at construction and require
|
|
228
|
-
/// the guest's `__kobako_abi_version` export to equal `ABI_VERSION`
|
|
229
|
-
/// An absent export or a non-equal value is
|
|
230
|
-
/// a deterministic artifact fault raised as
|
|
231
|
-
/// `Kobako::SetupError`. The probe Store drops here; invocation
|
|
232
|
-
/// instances are created per `#eval` / `#run`. The frameless WASI
|
|
233
|
-
/// context keeps a third-party guest whose start section touches
|
|
234
|
-
/// WASI on the `SetupError` path instead of panicking in
|
|
235
|
-
/// `Invocation::wasi_mut`.
|
|
236
|
-
fn probe_abi_version(&self, ruby: &Ruby) -> Result<(), MagnusError> {
|
|
237
|
-
let mut store = self.new_store()?;
|
|
238
|
-
frames::install_wasi_frames(&mut store, &self.config, &[])?;
|
|
239
|
-
let instance = self
|
|
240
|
-
.instance_pre
|
|
241
|
-
.instantiate(store.as_context_mut())
|
|
242
|
-
.map_err(|e| trap::instantiate_err(ruby, e))?;
|
|
243
|
-
let probe = instance
|
|
244
|
-
.get_typed_func::<(), u32>(store.as_context_mut(), "__kobako_abi_version")
|
|
245
|
-
.map_err(|_| {
|
|
246
|
-
errors::setup_err(
|
|
247
|
-
ruby,
|
|
248
|
-
format!(
|
|
249
|
-
"the Guest Binary does not export __kobako_abi_version; \
|
|
250
|
-
rebuild it against ABI version {ABI_VERSION}"
|
|
251
|
-
),
|
|
252
|
-
)
|
|
253
|
-
})?;
|
|
254
|
-
let reported = probe.call(store.as_context_mut(), ()).map_err(|e| {
|
|
255
|
-
errors::setup_err(
|
|
256
|
-
ruby,
|
|
257
|
-
format!("failed to read the Guest Binary's ABI version: {e}"),
|
|
258
|
-
)
|
|
259
|
-
})?;
|
|
260
|
-
if reported != ABI_VERSION {
|
|
261
|
-
return Err(errors::setup_err(
|
|
262
|
-
ruby,
|
|
263
|
-
format!(
|
|
264
|
-
"the Guest Binary reports ABI version {reported}, but this host \
|
|
265
|
-
implements ABI version {ABI_VERSION}; rebuild the Guest Binary \
|
|
266
|
-
against the host's version"
|
|
267
|
-
),
|
|
268
|
-
));
|
|
269
|
-
}
|
|
270
|
-
Ok(())
|
|
178
|
+
last_usage: Cell::new(Usage {
|
|
179
|
+
wall_time: 0.0,
|
|
180
|
+
memory_peak: 0,
|
|
181
|
+
}),
|
|
182
|
+
})
|
|
271
183
|
}
|
|
272
184
|
|
|
273
185
|
/// Register the Ruby-side dispatch `Proc`.
|
|
274
186
|
/// Bound to Ruby as `Kobako::Runtime#on_dispatch=`. The handle is
|
|
275
|
-
/// pinned by `DataTypeFunctions::mark
|
|
276
|
-
///
|
|
277
|
-
/// `
|
|
278
|
-
|
|
187
|
+
/// pinned by `DataTypeFunctions::mark`; for each invocation
|
|
188
|
+
/// `build_handler` wraps a copy in a `RubyDispatchHandler` and the
|
|
189
|
+
/// driver's `invoke` binds it onto the per-invocation `Invocation`,
|
|
190
|
+
/// where the `__kobako_dispatch` import reads it through
|
|
191
|
+
/// `Caller<Invocation>`.
|
|
192
|
+
fn set_on_dispatch(&self, proc_value: Value) -> Result<(), MagnusError> {
|
|
279
193
|
self.on_dispatch.set(Some(Opaque::from(proc_value)));
|
|
280
194
|
Ok(())
|
|
281
195
|
}
|
|
282
196
|
|
|
283
|
-
/// Synchronously re-enter the guest's `__kobako_yield_to_block`
|
|
284
|
-
/// export with `args_bytes` as the yield-arguments payload, and
|
|
285
|
-
/// return the YieldResponse bytes the guest produced.
|
|
286
|
-
///
|
|
287
|
-
/// Bound to Ruby as `Kobako::Runtime#yield_to_active_invocation`.
|
|
288
|
-
/// Recovers the dispatcher's `&mut Caller` from the per-thread
|
|
289
|
-
/// Invocation slot (SPEC.md Single-Invocation Slot) — the host is
|
|
290
|
-
/// already inside a `__kobako_dispatch` callback, so the Caller
|
|
291
|
-
/// parked on the Rust stack is the same one the Sandbox-level
|
|
292
|
-
/// `#eval` / `#run` is driving. Invoked from the host-side yield
|
|
293
|
-
/// proxy that the dispatcher hands to Service methods;
|
|
294
|
-
/// raises `Kobako::TrapError` when called outside an active dispatch
|
|
295
|
-
/// frame, or when any of the underlying allocation / write / call /
|
|
296
|
-
/// read steps fails.
|
|
297
|
-
pub(crate) fn yield_to_active_invocation(
|
|
298
|
-
&self,
|
|
299
|
-
args_bytes: RString,
|
|
300
|
-
) -> Result<RString, MagnusError> {
|
|
301
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
302
|
-
let _ = self; // The Caller carries its own Store; `self` is only
|
|
303
|
-
// a marker that the method belongs to a Runtime.
|
|
304
|
-
|
|
305
|
-
let bytes = rstring_to_vec(args_bytes);
|
|
306
|
-
let Some(caller) = dispatch::current_caller() else {
|
|
307
|
-
return Err(errors::trap_err(
|
|
308
|
-
&ruby,
|
|
309
|
-
"yield_to_active_invocation called outside an active Sandbox dispatch frame",
|
|
310
|
-
));
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
let resp_bytes =
|
|
314
|
-
guest_mem::drive_yield(caller, &bytes).map_err(|msg| errors::trap_err(&ruby, msg))?;
|
|
315
|
-
Ok(ruby.str_from_slice(&resp_bytes))
|
|
316
|
-
}
|
|
317
|
-
|
|
318
197
|
// -----------------------------------------------------------------
|
|
319
198
|
// Run-path methods. Each method is best-effort — it raises a Ruby
|
|
320
199
|
// `Kobako::TrapError` when the corresponding export is missing or
|
|
@@ -322,75 +201,100 @@ impl Runtime {
|
|
|
322
201
|
// taxonomy.
|
|
323
202
|
// -----------------------------------------------------------------
|
|
324
203
|
|
|
325
|
-
///
|
|
326
|
-
///
|
|
327
|
-
///
|
|
328
|
-
///
|
|
329
|
-
///
|
|
330
|
-
///
|
|
331
|
-
///
|
|
332
|
-
|
|
333
|
-
/// and the epoch deadline is set to fire at the next ticker tick;
|
|
334
|
-
/// the memory-cap limiter is already wired.
|
|
335
|
-
///
|
|
336
|
-
/// On a wasmtime trap the configured-cap path raises
|
|
337
|
-
/// `Kobako::TimeoutError` / `Kobako::MemoryLimitError`; everything
|
|
338
|
-
/// else raises `Kobako::TrapError`. On success the Snapshot carries
|
|
339
|
-
/// the OUTCOME_BUFFER bytes, the per-channel stdout / stderr captures
|
|
340
|
-
/// with their truncation flags, and the usage figures.
|
|
341
|
-
pub(crate) fn eval(
|
|
204
|
+
/// One-shot mruby source execution (`#eval`). The Ruby-facing entry:
|
|
205
|
+
/// builds the dispatch handler from the registered Proc, hands the
|
|
206
|
+
/// three stdin frames (`preamble`, `source`, `snippets`) and the source
|
|
207
|
+
/// to the driver, and settles the returned `Snapshot` through
|
|
208
|
+
/// `finish_invocation` — or maps a could-not-start `Error` onto its
|
|
209
|
+
/// `Kobako::*` exception. The run mechanics — frames, caps, trap
|
|
210
|
+
/// classification — live in `kobako_wasmtime::Driver`.
|
|
211
|
+
fn eval(
|
|
342
212
|
&self,
|
|
343
213
|
preamble: RString,
|
|
344
214
|
source: RString,
|
|
345
215
|
snippets: RString,
|
|
346
216
|
) -> Result<Snapshot, MagnusError> {
|
|
347
217
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
348
|
-
let
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
218
|
+
let handler = self.build_handler();
|
|
219
|
+
let preamble = rstring_to_vec(preamble);
|
|
220
|
+
let source = rstring_to_vec(source);
|
|
221
|
+
let snippets = rstring_to_vec(snippets);
|
|
222
|
+
let snapshot = self
|
|
223
|
+
.driver
|
|
224
|
+
.invoke(
|
|
225
|
+
Entry::Eval { source: &source },
|
|
226
|
+
Frames {
|
|
227
|
+
preamble: &preamble,
|
|
228
|
+
snippets: &snippets,
|
|
229
|
+
},
|
|
230
|
+
handler,
|
|
231
|
+
)
|
|
232
|
+
.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
233
|
+
self.finish_invocation(&ruby, snapshot)
|
|
363
234
|
}
|
|
364
235
|
|
|
365
236
|
/// Execute one entrypoint dispatch (`__kobako_run`) and return a
|
|
366
237
|
/// `Snapshot` bundling every per-invocation observable.
|
|
367
238
|
///
|
|
368
|
-
///
|
|
369
|
-
///
|
|
370
|
-
///
|
|
371
|
-
/// `
|
|
372
|
-
///
|
|
373
|
-
|
|
374
|
-
/// ("alloc returned 0") when guest allocation fails.
|
|
375
|
-
pub(crate) fn run(
|
|
239
|
+
/// The two-frame stdin protocol (preamble + snippets; no user source
|
|
240
|
+
/// frame — docs/wire-codec.md § Invocation channels) plus the
|
|
241
|
+
/// `envelope` copied into guest linear memory; cap semantics match
|
|
242
|
+
/// `#eval`. Raises `Kobako::TrapError` / `Kobako::SandboxError` per the
|
|
243
|
+
/// engine-vs-host-fault split inside the driver.
|
|
244
|
+
fn run(
|
|
376
245
|
&self,
|
|
377
246
|
preamble: RString,
|
|
378
247
|
snippets: RString,
|
|
379
248
|
envelope: RString,
|
|
380
249
|
) -> Result<Snapshot, MagnusError> {
|
|
381
250
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
382
|
-
let
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
251
|
+
let handler = self.build_handler();
|
|
252
|
+
let preamble = rstring_to_vec(preamble);
|
|
253
|
+
let snippets = rstring_to_vec(snippets);
|
|
254
|
+
let envelope = rstring_to_vec(envelope);
|
|
255
|
+
let snapshot = self
|
|
256
|
+
.driver
|
|
257
|
+
.invoke(
|
|
258
|
+
Entry::Run {
|
|
259
|
+
envelope: &envelope,
|
|
260
|
+
},
|
|
261
|
+
Frames {
|
|
262
|
+
preamble: &preamble,
|
|
263
|
+
snippets: &snippets,
|
|
264
|
+
},
|
|
265
|
+
handler,
|
|
266
|
+
)
|
|
267
|
+
.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
268
|
+
self.finish_invocation(&ruby, snapshot)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Settle one invocation's `Snapshot` at the Ruby boundary: usage is
|
|
272
|
+
/// recorded even when the completion is a trap and this call raises,
|
|
273
|
+
/// while trap-path captures are deliberately dropped — exposing them
|
|
274
|
+
/// is a SPEC decision the Ruby surface has not taken.
|
|
275
|
+
fn finish_invocation(
|
|
276
|
+
&self,
|
|
277
|
+
ruby: &Ruby,
|
|
278
|
+
snapshot: RuntimeSnapshot,
|
|
279
|
+
) -> Result<Snapshot, MagnusError> {
|
|
280
|
+
self.last_usage.set(snapshot.usage);
|
|
281
|
+
match snapshot.completion {
|
|
282
|
+
Completion::Outcome(bytes) => {
|
|
283
|
+
Ok(Snapshot::new(bytes, snapshot.stdout, snapshot.stderr))
|
|
284
|
+
}
|
|
285
|
+
Completion::Trap(trap) => Err(errors::trap_to_magnus(ruby, trap)),
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Build the dispatch handler for one invocation from the registered
|
|
290
|
+
/// `on_dispatch` Proc, or `None` when none is set. The `Opaque` the
|
|
291
|
+
/// handler wraps stays GC-rooted by `Runtime`'s `mark`, so the driver
|
|
292
|
+
/// only borrows it for the call (the safety contract on
|
|
293
|
+
/// `kobako_runtime::runtime::Runtime`).
|
|
294
|
+
fn build_handler(&self) -> Option<Arc<dyn DispatchHandler>> {
|
|
295
|
+
self.on_dispatch.get().map(|proc| {
|
|
296
|
+
Arc::new(bridge::RubyDispatchHandler::new(proc)) as Arc<dyn DispatchHandler>
|
|
297
|
+
})
|
|
394
298
|
}
|
|
395
299
|
|
|
396
300
|
/// Return the per-last-invocation usage as a
|
|
@@ -401,176 +305,22 @@ impl Runtime {
|
|
|
401
305
|
///
|
|
402
306
|
/// * `wall_time` (Float seconds) — the wall-clock duration the
|
|
403
307
|
/// most recent invocation spent inside the guest export call.
|
|
404
|
-
///
|
|
405
|
-
///
|
|
406
|
-
///
|
|
407
|
-
/// export returns. `0.0` before the first invocation.
|
|
308
|
+
/// The bracket mirrors the `timeout` deadline accounting and
|
|
309
|
+
/// excludes everything that runs after the guest export
|
|
310
|
+
/// returns. `0.0` before the first invocation.
|
|
408
311
|
/// * `memory_peak` (Integer bytes) — the high-water mark of the
|
|
409
312
|
/// per-invocation `memory.grow` delta past the linear-memory
|
|
410
313
|
/// size captured at invocation entry. `0` before the first
|
|
411
314
|
/// invocation.
|
|
412
315
|
///
|
|
413
|
-
/// Reads the `last_usage` Cell `
|
|
414
|
-
/// per-invocation Store was discarded.
|
|
415
|
-
|
|
316
|
+
/// Reads the `last_usage` Cell `finish_invocation` populated from the
|
|
317
|
+
/// returned `Snapshot` before the per-invocation Store was discarded.
|
|
318
|
+
fn usage(&self) -> Result<RArray, MagnusError> {
|
|
416
319
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
417
|
-
let
|
|
320
|
+
let usage = self.last_usage.get();
|
|
418
321
|
let arr = ruby.ary_new_capa(2);
|
|
419
|
-
arr.push(wall_time)?;
|
|
420
|
-
arr.push(memory_peak)?;
|
|
322
|
+
arr.push(usage.wall_time)?;
|
|
323
|
+
arr.push(usage.memory_peak)?;
|
|
421
324
|
Ok(arr)
|
|
422
325
|
}
|
|
423
|
-
|
|
424
|
-
// -----------------------------------------------------------------
|
|
425
|
-
// Private helpers.
|
|
426
|
-
// -----------------------------------------------------------------
|
|
427
|
-
|
|
428
|
-
/// Build the per-invocation Store: a fresh `Invocation` wired with
|
|
429
|
-
/// the memory limiter, the epoch-deadline callback, and the
|
|
430
|
-
/// registered dispatch Proc.
|
|
431
|
-
fn new_store(&self) -> Result<WtStore<Invocation>, MagnusError> {
|
|
432
|
-
let mut store = WtStore::new(shared_engine()?, Invocation::new(self.memory_limit));
|
|
433
|
-
store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
|
|
434
|
-
store.epoch_deadline_callback(trap::epoch_deadline_callback);
|
|
435
|
-
if let Some(on_dispatch) = self.on_dispatch.get() {
|
|
436
|
-
store.data_mut().bind_on_dispatch(on_dispatch);
|
|
437
|
-
}
|
|
438
|
-
Ok(store)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/// Instantiate the per-invocation instance from the pre-linked
|
|
442
|
-
/// template and resolve its host-driven export handles. An
|
|
443
|
-
/// instantiation failure at invocation time is an engine fault —
|
|
444
|
-
/// `Kobako::TrapError` — unlike the construction-time probe, whose
|
|
445
|
-
/// failure is `SetupError`.
|
|
446
|
-
fn instantiate(
|
|
447
|
-
&self,
|
|
448
|
-
ruby: &Ruby,
|
|
449
|
-
store: &mut WtStore<Invocation>,
|
|
450
|
-
) -> Result<Exports, MagnusError> {
|
|
451
|
-
let instance = self
|
|
452
|
-
.instance_pre
|
|
453
|
-
.instantiate(store.as_context_mut())
|
|
454
|
-
.map_err(|e| {
|
|
455
|
-
errors::trap_err(
|
|
456
|
-
ruby,
|
|
457
|
-
format!("failed to instantiate the Sandbox runtime: {e}"),
|
|
458
|
-
)
|
|
459
|
-
})?;
|
|
460
|
-
Ok(Exports::resolve(&instance, store.as_context_mut()))
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/// Run one guest export call inside the per-invocation cap window:
|
|
464
|
-
/// `Runtime::prime_caps` before, `disarm_caps` after — the shared
|
|
465
|
-
/// bracket for both run-path exports (`__kobako_eval` /
|
|
466
|
-
/// `__kobako_run`). Disarm runs whether the call returns or traps, so
|
|
467
|
-
/// the `wall_time` bracket and the memory
|
|
468
|
-
/// cap always close — that close-on-trap guarantee is the reason this
|
|
469
|
-
/// bracket lives in one place rather than inline at each call site.
|
|
470
|
-
/// The wasmtime trap is returned unmapped; each caller wraps it
|
|
471
|
-
/// through `trap::call_err` for its own error context.
|
|
472
|
-
fn call_with_caps<Params, Results>(
|
|
473
|
-
&self,
|
|
474
|
-
store: &mut WtStore<Invocation>,
|
|
475
|
-
exports: &Exports,
|
|
476
|
-
export: &TypedFunc<Params, Results>,
|
|
477
|
-
params: Params,
|
|
478
|
-
) -> Result<Results, wasmtime::Error>
|
|
479
|
-
where
|
|
480
|
-
Params: wasmtime::WasmParams,
|
|
481
|
-
Results: wasmtime::WasmResults,
|
|
482
|
-
{
|
|
483
|
-
self.prime_caps(store, exports);
|
|
484
|
-
let result = export.call(store.as_context_mut(), params);
|
|
485
|
-
disarm_caps(store);
|
|
486
|
-
// Stash the usage figures on every outcome — including the
|
|
487
|
-
// trap paths, where `build_snapshot` never runs and the Store is
|
|
488
|
-
// about to be discarded with the error.
|
|
489
|
-
let data = store.data();
|
|
490
|
-
self.last_usage
|
|
491
|
-
.set((data.wall_time().as_secs_f64(), data.memory_peak()));
|
|
492
|
-
result
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/// Stamp the per-invocation wall-clock deadline into `Invocation`
|
|
496
|
-
/// and prime the wasmtime epoch deadline so the next ticker tick
|
|
497
|
-
/// wakes the epoch-deadline callback. When `timeout` is disabled,
|
|
498
|
-
/// the deadline is set far enough in the future that the callback
|
|
499
|
-
/// effectively never fires.
|
|
500
|
-
///
|
|
501
|
-
/// Also captures the current linear-memory size as the baseline
|
|
502
|
-
/// for the per-invocation memory delta cap —
|
|
503
|
-
/// the pre-initialized image's allocation is folded into the
|
|
504
|
-
/// baseline rather than the budget — and stamps the wall-clock
|
|
505
|
-
/// entry instant for the `wall_time`
|
|
506
|
-
/// measurement. The bracket closes in `disarm_caps` so it matches
|
|
507
|
-
/// the `timeout` deadline window and excludes `OUTCOME_BUFFER`
|
|
508
|
-
/// decoding and stdout / stderr capture readout.
|
|
509
|
-
fn prime_caps(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
|
|
510
|
-
match self.config.timeout {
|
|
511
|
-
Some(timeout) => {
|
|
512
|
-
let deadline = Instant::now() + timeout;
|
|
513
|
-
store.data_mut().set_deadline(Some(deadline));
|
|
514
|
-
store.set_epoch_deadline(1);
|
|
515
|
-
}
|
|
516
|
-
None => {
|
|
517
|
-
store.data_mut().set_deadline(None);
|
|
518
|
-
store.set_epoch_deadline(u64::MAX);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
let baseline = match exports.memory {
|
|
522
|
-
Some(m) => m.data_size(store.as_context_mut()),
|
|
523
|
-
None => 0,
|
|
524
|
-
};
|
|
525
|
-
store.data_mut().arm_memory_cap(baseline);
|
|
526
|
-
store.data_mut().start_wall_clock();
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/// Collect every per-invocation observable into a fresh `Snapshot`.
|
|
530
|
-
/// Called from the run-path methods after the guest export returns
|
|
531
|
-
/// successfully: drains OUTCOME_BUFFER via `__kobako_take_outcome`
|
|
532
|
-
/// and snapshots the per-channel stdout / stderr pipes (clipped to
|
|
533
|
-
/// their caps). The usage figures were already stashed by
|
|
534
|
-
/// `call_with_caps`.
|
|
535
|
-
fn build_snapshot(
|
|
536
|
-
&self,
|
|
537
|
-
ruby: &Ruby,
|
|
538
|
-
store: &mut WtStore<Invocation>,
|
|
539
|
-
exports: &Exports,
|
|
540
|
-
) -> Result<Snapshot, MagnusError> {
|
|
541
|
-
let return_bytes = frames::fetch_outcome_bytes(ruby, store, exports)?;
|
|
542
|
-
let data = store.data();
|
|
543
|
-
let (stdout_raw, stderr_raw, wall_time, memory_peak) = (
|
|
544
|
-
data.stdout_bytes(),
|
|
545
|
-
data.stderr_bytes(),
|
|
546
|
-
data.wall_time(),
|
|
547
|
-
data.memory_peak(),
|
|
548
|
-
);
|
|
549
|
-
let (stdout_visible, stdout_truncated) =
|
|
550
|
-
capture::clip_capture(&stdout_raw, self.config.stdout_limit_bytes);
|
|
551
|
-
let stdout_bytes = stdout_visible.to_vec();
|
|
552
|
-
let (stderr_visible, stderr_truncated) =
|
|
553
|
-
capture::clip_capture(&stderr_raw, self.config.stderr_limit_bytes);
|
|
554
|
-
let stderr_bytes = stderr_visible.to_vec();
|
|
555
|
-
Ok(Snapshot::new(
|
|
556
|
-
return_bytes,
|
|
557
|
-
stdout_bytes,
|
|
558
|
-
stdout_truncated,
|
|
559
|
-
stderr_bytes,
|
|
560
|
-
stderr_truncated,
|
|
561
|
-
wall_time,
|
|
562
|
-
memory_peak,
|
|
563
|
-
))
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/// Drop the memory cap as soon as the guest call returns so that
|
|
568
|
-
/// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
|
|
569
|
-
/// which can grow guest memory transiently) is not attributed to
|
|
570
|
-
/// the user script. Also closes the
|
|
571
|
-
/// `wall_time` bracket opened by `Runtime::prime_caps`. Paired
|
|
572
|
-
/// with `Runtime::prime_caps`.
|
|
573
|
-
fn disarm_caps(store: &mut WtStore<Invocation>) {
|
|
574
|
-
store.data_mut().stop_wall_clock();
|
|
575
|
-
store.data_mut().disarm_memory_cap();
|
|
576
326
|
}
|