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
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -1,82 +1,46 @@
|
|
|
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, profile)`. 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 / capture readouts.
|
|
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
|
-
|
|
53
|
-
use magnus::{function, method, prelude::*, Error as MagnusError, RModule, RString, Ruby};
|
|
54
|
-
|
|
55
|
-
use std::cell::Cell;
|
|
56
|
-
use std::path::Path;
|
|
57
|
-
use std::time::{Duration, Instant};
|
|
58
|
-
|
|
59
|
-
use magnus::{gc, typed_data::DataTypeFunctions, value::Opaque, RArray, TypedData, Value};
|
|
60
28
|
|
|
61
|
-
use
|
|
62
|
-
|
|
63
|
-
|
|
29
|
+
use magnus::{
|
|
30
|
+
function, gc, method, prelude::*, typed_data::DataTypeFunctions, value::Opaque,
|
|
31
|
+
Error as MagnusError, RArray, RModule, RString, Ruby, Symbol, TypedData, Value,
|
|
64
32
|
};
|
|
65
33
|
|
|
66
|
-
use
|
|
67
|
-
use
|
|
68
|
-
use
|
|
69
|
-
use
|
|
34
|
+
use std::cell::{Cell, RefCell};
|
|
35
|
+
use std::path::Path;
|
|
36
|
+
use std::sync::Arc;
|
|
37
|
+
use std::time::Duration;
|
|
70
38
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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::profile::Profile;
|
|
41
|
+
use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
|
|
42
|
+
use kobako_runtime::snapshot::{Capture, Completion, Snapshot as RuntimeSnapshot, Usage};
|
|
43
|
+
use kobako_wasmtime::{Config, Driver};
|
|
80
44
|
|
|
81
45
|
/// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
|
|
82
46
|
/// what would otherwise be an inline `unsafe { rstring.as_slice() }
|
|
@@ -84,11 +48,20 @@ const ABI_VERSION: u32 = 2;
|
|
|
84
48
|
/// does not outlive this call, so no Ruby allocation can move the
|
|
85
49
|
/// underlying RString between the borrow and the copy — the safety
|
|
86
50
|
/// invariant the inline form relied on is established once here.
|
|
87
|
-
|
|
51
|
+
fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
88
52
|
// SAFETY: see item doc.
|
|
89
53
|
unsafe { s.as_slice() }.to_vec()
|
|
90
54
|
}
|
|
91
55
|
|
|
56
|
+
/// The pre-invocation sentinel for one capture channel: no bytes, cap
|
|
57
|
+
/// not reached. Fresh `Vec`s per call because `Capture` owns its buffer.
|
|
58
|
+
fn empty_capture() -> Capture {
|
|
59
|
+
Capture {
|
|
60
|
+
bytes: Vec::new(),
|
|
61
|
+
truncated: false,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
92
65
|
// ---------------------------------------------------------------------------
|
|
93
66
|
// Ruby init
|
|
94
67
|
// ---------------------------------------------------------------------------
|
|
@@ -102,49 +75,47 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
102
75
|
// intermediate hierarchy is registered.
|
|
103
76
|
|
|
104
77
|
let runtime = kobako.define_class("Runtime", ruby.class_object())?;
|
|
105
|
-
runtime.define_singleton_method("from_path", function!(Runtime::from_path,
|
|
78
|
+
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 6))?;
|
|
106
79
|
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
80
|
runtime.define_method("eval", method!(Runtime::eval, 3))?;
|
|
112
81
|
runtime.define_method("run", method!(Runtime::run, 3))?;
|
|
113
82
|
runtime.define_method("usage", method!(Runtime::usage, 0))?;
|
|
83
|
+
runtime.define_method("captures", method!(Runtime::captures, 0))?;
|
|
84
|
+
runtime.define_method("profile", method!(Runtime::profile, 0))?;
|
|
85
|
+
// The guest re-enters for a block yield through a frame-scoped
|
|
86
|
+
// `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
|
|
87
|
+
// method on Runtime.
|
|
88
|
+
bridge::register(runtime)?;
|
|
114
89
|
|
|
115
90
|
Ok(())
|
|
116
91
|
}
|
|
117
92
|
|
|
118
93
|
#[derive(TypedData)]
|
|
119
94
|
#[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,
|
|
95
|
+
struct Runtime {
|
|
96
|
+
// The magnus-free wasmtime driver that runs every invocation; the
|
|
97
|
+
// shell only shuttles Ruby values across its boundary.
|
|
98
|
+
driver: Driver,
|
|
134
99
|
// The host-side dispatch Proc, held here only
|
|
135
100
|
// to give `DataTypeFunctions::mark` a read path so it can pin the
|
|
136
|
-
// Proc across GC.
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
101
|
+
// Proc across GC. For each invocation `build_handler` wraps a copy of
|
|
102
|
+
// this handle in a `RubyDispatchHandler`, and the driver's `invoke`
|
|
103
|
+
// binds that `Arc<dyn DispatchHandler>` onto the per-invocation
|
|
104
|
+
// `Invocation`, where the `__kobako_dispatch` import calls it — both
|
|
105
|
+
// reference the one Proc this `Opaque` pins. `Cell` is sound under the
|
|
106
|
+
// GVL (see the `unsafe impl Sync` below).
|
|
141
107
|
on_dispatch: Cell<Option<Opaque<Value>>>,
|
|
142
|
-
// Usage of the most recent invocation
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
// invocation
|
|
147
|
-
last_usage
|
|
108
|
+
// Usage of the most recent invocation, stashed here so `#usage` reads
|
|
109
|
+
// survive the per-invocation Store teardown and the trap path's
|
|
110
|
+
// raise. Zeroed before the first invocation.
|
|
111
|
+
last_usage: Cell<Usage>,
|
|
112
|
+
// Output captures of the most recent invocation, stashed for the same
|
|
113
|
+
// reason as `last_usage`: the trap path raises, and this readout is
|
|
114
|
+
// what keeps the guest's partial output readable after a rescue.
|
|
115
|
+
// `RefCell` (not `Cell`) because `Capture` owns its byte buffer; the
|
|
116
|
+
// same GVL single-thread discipline applies (see the `unsafe impl
|
|
117
|
+
// Sync` below). Empty before the first invocation.
|
|
118
|
+
last_captures: RefCell<(Capture, Capture)>,
|
|
148
119
|
}
|
|
149
120
|
|
|
150
121
|
impl DataTypeFunctions for Runtime {
|
|
@@ -165,11 +136,11 @@ impl DataTypeFunctions for Runtime {
|
|
|
165
136
|
}
|
|
166
137
|
|
|
167
138
|
// SAFETY: magnus requires `Send + Sync` on TypedData types. The
|
|
168
|
-
// `on_dispatch` / `last_usage` `Cell`s
|
|
169
|
-
// unavailable, but every access to them
|
|
170
|
-
// thread at a time — Ruby method calls,
|
|
171
|
-
// holds the GVL. No cross-thread access to
|
|
172
|
-
// stays auto-derived.
|
|
139
|
+
// `on_dispatch` / `last_usage` `Cell`s and the `last_captures` `RefCell`
|
|
140
|
+
// make the auto-derived `Sync` unavailable, but every access to them
|
|
141
|
+
// happens under the GVL on a single thread at a time — Ruby method calls,
|
|
142
|
+
// and a GC `mark` pass that also holds the GVL. No cross-thread access to
|
|
143
|
+
// any of them can occur. `Send` stays auto-derived.
|
|
173
144
|
unsafe impl Sync for Runtime {}
|
|
174
145
|
|
|
175
146
|
impl Runtime {
|
|
@@ -182,15 +153,18 @@ impl Runtime {
|
|
|
182
153
|
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
183
154
|
/// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
|
|
184
155
|
/// are the per-channel output caps (`None`
|
|
185
|
-
/// disables)
|
|
186
|
-
/// (`
|
|
187
|
-
///
|
|
188
|
-
|
|
156
|
+
/// disables); `profile` is the isolation rung the driver builds
|
|
157
|
+
/// (`:permissive` / `:hermetic`). All five are validated by the
|
|
158
|
+
/// caller (`Kobako::Sandbox`); this method only refuses non-finite
|
|
159
|
+
/// or non-positive timeouts and off-ladder profiles as a defence in
|
|
160
|
+
/// depth.
|
|
161
|
+
fn from_path(
|
|
189
162
|
path: String,
|
|
190
163
|
timeout_seconds: Option<f64>,
|
|
191
164
|
memory_limit: Option<usize>,
|
|
192
165
|
stdout_limit_bytes: Option<usize>,
|
|
193
166
|
stderr_limit_bytes: Option<usize>,
|
|
167
|
+
profile: Symbol,
|
|
194
168
|
) -> Result<Self, MagnusError> {
|
|
195
169
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
196
170
|
let timeout = match timeout_seconds {
|
|
@@ -208,113 +182,54 @@ impl Runtime {
|
|
|
208
182
|
));
|
|
209
183
|
}
|
|
210
184
|
};
|
|
185
|
+
// Fail closed on an off-ladder rung: an unrecognized posture must
|
|
186
|
+
// never fall back to a grant. Same defence-in-depth posture as the
|
|
187
|
+
// timeout guard above — `SandboxOptions` is the primary validator.
|
|
188
|
+
let profile = match profile.name()?.as_ref() {
|
|
189
|
+
"hermetic" => Profile::Hermetic,
|
|
190
|
+
"permissive" => Profile::Permissive,
|
|
191
|
+
other => {
|
|
192
|
+
return Err(MagnusError::new(
|
|
193
|
+
ruby.exception_arg_error(),
|
|
194
|
+
format!("profile must be :permissive or :hermetic, got :{other}"),
|
|
195
|
+
));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
211
198
|
|
|
212
|
-
let
|
|
213
|
-
|
|
199
|
+
let driver = Driver::new(
|
|
200
|
+
Path::new(&path),
|
|
214
201
|
memory_limit,
|
|
215
|
-
|
|
202
|
+
Config {
|
|
216
203
|
timeout,
|
|
217
204
|
stdout_limit_bytes,
|
|
218
205
|
stderr_limit_bytes,
|
|
206
|
+
profile,
|
|
219
207
|
},
|
|
208
|
+
)
|
|
209
|
+
.map_err(|e| errors::setup_to_magnus(&ruby, e))?;
|
|
210
|
+
Ok(Self {
|
|
211
|
+
driver,
|
|
220
212
|
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(())
|
|
213
|
+
last_usage: Cell::new(Usage {
|
|
214
|
+
wall_time: 0.0,
|
|
215
|
+
memory_peak: 0,
|
|
216
|
+
}),
|
|
217
|
+
last_captures: RefCell::new((empty_capture(), empty_capture())),
|
|
218
|
+
})
|
|
271
219
|
}
|
|
272
220
|
|
|
273
221
|
/// Register the Ruby-side dispatch `Proc`.
|
|
274
222
|
/// Bound to Ruby as `Kobako::Runtime#on_dispatch=`. The handle is
|
|
275
|
-
/// pinned by `DataTypeFunctions::mark
|
|
276
|
-
///
|
|
277
|
-
/// `
|
|
278
|
-
|
|
223
|
+
/// pinned by `DataTypeFunctions::mark`; for each invocation
|
|
224
|
+
/// `build_handler` wraps a copy in a `RubyDispatchHandler` and the
|
|
225
|
+
/// driver's `invoke` binds it onto the per-invocation `Invocation`,
|
|
226
|
+
/// where the `__kobako_dispatch` import reads it through
|
|
227
|
+
/// `Caller<Invocation>`.
|
|
228
|
+
fn set_on_dispatch(&self, proc_value: Value) -> Result<(), MagnusError> {
|
|
279
229
|
self.on_dispatch.set(Some(Opaque::from(proc_value)));
|
|
280
230
|
Ok(())
|
|
281
231
|
}
|
|
282
232
|
|
|
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
233
|
// -----------------------------------------------------------------
|
|
319
234
|
// Run-path methods. Each method is best-effort — it raises a Ruby
|
|
320
235
|
// `Kobako::TrapError` when the corresponding export is missing or
|
|
@@ -322,75 +237,107 @@ impl Runtime {
|
|
|
322
237
|
// taxonomy.
|
|
323
238
|
// -----------------------------------------------------------------
|
|
324
239
|
|
|
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(
|
|
240
|
+
/// One-shot mruby source execution (`#eval`). The Ruby-facing entry:
|
|
241
|
+
/// builds the dispatch handler from the registered Proc, hands the
|
|
242
|
+
/// three stdin frames (`preamble`, `source`, `snippets`) and the source
|
|
243
|
+
/// to the driver, and settles the invocation through
|
|
244
|
+
/// `finish_invocation` — or maps a could-not-start `Error` onto its
|
|
245
|
+
/// `Kobako::*` exception. The run mechanics — frames, caps, trap
|
|
246
|
+
/// classification — live in `kobako_wasmtime::Driver`.
|
|
247
|
+
fn eval(
|
|
342
248
|
&self,
|
|
343
249
|
preamble: RString,
|
|
344
250
|
source: RString,
|
|
345
251
|
snippets: RString,
|
|
346
|
-
) -> Result<
|
|
252
|
+
) -> Result<RString, MagnusError> {
|
|
347
253
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
348
|
-
let
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
254
|
+
let handler = self.build_handler();
|
|
255
|
+
let preamble = rstring_to_vec(preamble);
|
|
256
|
+
let source = rstring_to_vec(source);
|
|
257
|
+
let snippets = rstring_to_vec(snippets);
|
|
258
|
+
let snapshot = self
|
|
259
|
+
.driver
|
|
260
|
+
.invoke(
|
|
261
|
+
Entry::Eval { source: &source },
|
|
262
|
+
Frames {
|
|
263
|
+
preamble: &preamble,
|
|
264
|
+
snippets: &snippets,
|
|
265
|
+
},
|
|
266
|
+
handler,
|
|
267
|
+
)
|
|
268
|
+
.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
269
|
+
self.finish_invocation(&ruby, snapshot)
|
|
363
270
|
}
|
|
364
271
|
|
|
365
|
-
/// Execute one entrypoint dispatch (`__kobako_run`) and return
|
|
366
|
-
///
|
|
272
|
+
/// Execute one entrypoint dispatch (`__kobako_run`) and return the
|
|
273
|
+
/// guest's raw outcome bytes.
|
|
367
274
|
///
|
|
368
|
-
///
|
|
369
|
-
///
|
|
370
|
-
///
|
|
371
|
-
/// `
|
|
372
|
-
///
|
|
373
|
-
|
|
374
|
-
/// ("alloc returned 0") when guest allocation fails.
|
|
375
|
-
pub(crate) fn run(
|
|
275
|
+
/// The two-frame stdin protocol (preamble + snippets; no user source
|
|
276
|
+
/// frame — docs/wire-codec.md § Invocation channels) plus the
|
|
277
|
+
/// `envelope` copied into guest linear memory; cap semantics match
|
|
278
|
+
/// `#eval`. Raises `Kobako::TrapError` / `Kobako::SandboxError` per the
|
|
279
|
+
/// engine-vs-host-fault split inside the driver.
|
|
280
|
+
fn run(
|
|
376
281
|
&self,
|
|
377
282
|
preamble: RString,
|
|
378
283
|
snippets: RString,
|
|
379
284
|
envelope: RString,
|
|
380
|
-
) -> Result<
|
|
285
|
+
) -> Result<RString, MagnusError> {
|
|
381
286
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
382
|
-
let
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
287
|
+
let handler = self.build_handler();
|
|
288
|
+
let preamble = rstring_to_vec(preamble);
|
|
289
|
+
let snippets = rstring_to_vec(snippets);
|
|
290
|
+
let envelope = rstring_to_vec(envelope);
|
|
291
|
+
let snapshot = self
|
|
292
|
+
.driver
|
|
293
|
+
.invoke(
|
|
294
|
+
Entry::Run {
|
|
295
|
+
envelope: &envelope,
|
|
296
|
+
},
|
|
297
|
+
Frames {
|
|
298
|
+
preamble: &preamble,
|
|
299
|
+
snippets: &snippets,
|
|
300
|
+
},
|
|
301
|
+
handler,
|
|
302
|
+
)
|
|
303
|
+
.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
304
|
+
self.finish_invocation(&ruby, snapshot)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Settle one invocation's `Snapshot` at the Ruby boundary: usage and
|
|
308
|
+
/// the two output captures are recorded on every outcome, so the
|
|
309
|
+
/// `#usage` / `#captures` readouts survive the trap path's raise —
|
|
310
|
+
/// that is what keeps the guest's partial output readable after the
|
|
311
|
+
/// Host App rescues the trap. A completed guest invocation returns
|
|
312
|
+
/// its raw outcome bytes; the Sandbox layer decodes them.
|
|
313
|
+
fn finish_invocation(
|
|
314
|
+
&self,
|
|
315
|
+
ruby: &Ruby,
|
|
316
|
+
snapshot: RuntimeSnapshot,
|
|
317
|
+
) -> Result<RString, MagnusError> {
|
|
318
|
+
let RuntimeSnapshot {
|
|
319
|
+
completion,
|
|
320
|
+
stdout,
|
|
321
|
+
stderr,
|
|
322
|
+
usage,
|
|
323
|
+
} = snapshot;
|
|
324
|
+
self.last_usage.set(usage);
|
|
325
|
+
self.last_captures.replace((stdout, stderr));
|
|
326
|
+
match completion {
|
|
327
|
+
Completion::Outcome(bytes) => Ok(ruby.str_from_slice(&bytes)),
|
|
328
|
+
Completion::Trap(trap) => Err(errors::trap_to_magnus(ruby, trap)),
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// Build the dispatch handler for one invocation from the registered
|
|
333
|
+
/// `on_dispatch` Proc, or `None` when none is set. The `Opaque` the
|
|
334
|
+
/// handler wraps stays GC-rooted by `Runtime`'s `mark`, so the driver
|
|
335
|
+
/// only borrows it for the call (the safety contract on
|
|
336
|
+
/// `kobako_runtime::runtime::Runtime`).
|
|
337
|
+
fn build_handler(&self) -> Option<Arc<dyn DispatchHandler>> {
|
|
338
|
+
self.on_dispatch.get().map(|proc| {
|
|
339
|
+
Arc::new(bridge::RubyDispatchHandler::new(proc)) as Arc<dyn DispatchHandler>
|
|
340
|
+
})
|
|
394
341
|
}
|
|
395
342
|
|
|
396
343
|
/// Return the per-last-invocation usage as a
|
|
@@ -401,176 +348,55 @@ impl Runtime {
|
|
|
401
348
|
///
|
|
402
349
|
/// * `wall_time` (Float seconds) — the wall-clock duration the
|
|
403
350
|
/// most recent invocation spent inside the guest export call.
|
|
404
|
-
///
|
|
405
|
-
///
|
|
406
|
-
///
|
|
407
|
-
/// export returns. `0.0` before the first invocation.
|
|
351
|
+
/// The bracket mirrors the `timeout` deadline accounting and
|
|
352
|
+
/// excludes everything that runs after the guest export
|
|
353
|
+
/// returns. `0.0` before the first invocation.
|
|
408
354
|
/// * `memory_peak` (Integer bytes) — the high-water mark of the
|
|
409
355
|
/// per-invocation `memory.grow` delta past the linear-memory
|
|
410
356
|
/// size captured at invocation entry. `0` before the first
|
|
411
357
|
/// invocation.
|
|
412
358
|
///
|
|
413
|
-
/// Reads the `last_usage` Cell `
|
|
414
|
-
/// per-invocation Store was discarded.
|
|
415
|
-
|
|
359
|
+
/// Reads the `last_usage` Cell `finish_invocation` populated before
|
|
360
|
+
/// the per-invocation Store was discarded.
|
|
361
|
+
fn usage(&self) -> Result<RArray, MagnusError> {
|
|
416
362
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
417
|
-
let
|
|
363
|
+
let usage = self.last_usage.get();
|
|
418
364
|
let arr = ruby.ary_new_capa(2);
|
|
419
|
-
arr.push(wall_time)?;
|
|
420
|
-
arr.push(memory_peak)?;
|
|
365
|
+
arr.push(usage.wall_time)?;
|
|
366
|
+
arr.push(usage.memory_peak)?;
|
|
421
367
|
Ok(arr)
|
|
422
368
|
}
|
|
423
369
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
///
|
|
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.
|
|
370
|
+
/// Return the per-last-invocation output captures as a Ruby 4-tuple
|
|
371
|
+
/// `[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]`
|
|
372
|
+
/// — the flat positional layout mirrors `#usage`, and the element
|
|
373
|
+
/// order matches the destructure in `Kobako::Sandbox#read_captures!`;
|
|
374
|
+
/// reorder both sides together.
|
|
500
375
|
///
|
|
501
|
-
///
|
|
502
|
-
///
|
|
503
|
-
///
|
|
504
|
-
///
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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();
|
|
376
|
+
/// Reads the `last_captures` pair `finish_invocation` stashed on
|
|
377
|
+
/// every outcome, so the readout also covers the trap path, where
|
|
378
|
+
/// `#eval` / `#run` raise instead of returning outcome bytes.
|
|
379
|
+
/// Empty bytes and `false` flags before the first invocation.
|
|
380
|
+
fn captures(&self) -> Result<RArray, MagnusError> {
|
|
381
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
382
|
+
let captures = self.last_captures.borrow();
|
|
383
|
+
let (stdout, stderr) = &*captures;
|
|
384
|
+
let arr = ruby.ary_new_capa(4);
|
|
385
|
+
arr.push(ruby.str_from_slice(&stdout.bytes))?;
|
|
386
|
+
arr.push(stdout.truncated)?;
|
|
387
|
+
arr.push(ruby.str_from_slice(&stderr.bytes))?;
|
|
388
|
+
arr.push(stderr.truncated)?;
|
|
389
|
+
Ok(arr)
|
|
527
390
|
}
|
|
528
391
|
|
|
529
|
-
///
|
|
530
|
-
///
|
|
531
|
-
///
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
))
|
|
392
|
+
/// Return the isolation profile the driver built, as a Symbol
|
|
393
|
+
/// (`:hermetic` / `:permissive`) — the declaration the Sandbox
|
|
394
|
+
/// compares against the posture its `profile:` option requested.
|
|
395
|
+
fn profile(&self) -> Symbol {
|
|
396
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
397
|
+
match self.driver.profile() {
|
|
398
|
+
Profile::Hermetic => ruby.to_symbol("hermetic"),
|
|
399
|
+
Profile::Permissive => ruby.to_symbol("permissive"),
|
|
400
|
+
}
|
|
564
401
|
}
|
|
565
402
|
}
|
|
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
|
-
}
|