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
|
@@ -1,46 +1,49 @@
|
|
|
1
1
|
//! Per-invocation host state — the materialised
|
|
2
2
|
//! [SPEC.md Single-Invocation Slot] (one `Invocation` per OS thread
|
|
3
|
-
//! for the lifetime of one `
|
|
3
|
+
//! for the lifetime of one `Driver` invoke call).
|
|
4
4
|
//!
|
|
5
5
|
//! Owned as the data of each per-invocation `wasmtime::Store`
|
|
6
6
|
//! and threaded through every host import —
|
|
7
|
-
//! the `__kobako_dispatch` dispatcher reads the bound dispatch
|
|
8
|
-
//! while
|
|
9
|
-
//!
|
|
7
|
+
//! the `__kobako_dispatch` dispatcher reads the bound dispatch handler,
|
|
8
|
+
//! while `Driver::invoke` installs the invocation's WASI
|
|
9
|
+
//! context + pipes (via `frames::install_wasi_frames`) before the guest
|
|
10
|
+
//! export call.
|
|
10
11
|
//!
|
|
11
12
|
//! The slot also carries the per-invocation wall-clock deadline
|
|
12
13
|
//! and the per-invocation linear-memory
|
|
13
14
|
//! delta cap `MemoryLimiter`. Both are
|
|
14
15
|
//! read from the wasmtime `epoch_deadline_callback` / `ResourceLimiter`
|
|
15
|
-
//! callbacks installed in `
|
|
16
|
+
//! callbacks installed in `Driver::new_store`. The
|
|
16
17
|
//! memory cap measures only the `memory.grow` delta past the linear-
|
|
17
18
|
//! memory size captured at invocation entry — the image's initial
|
|
18
19
|
//! allocation is outside the budget.
|
|
19
20
|
//!
|
|
20
|
-
//! [SPEC.md Single-Invocation Slot]:
|
|
21
|
+
//! [SPEC.md Single-Invocation Slot]: ../../../SPEC.md
|
|
21
22
|
|
|
23
|
+
use std::sync::Arc;
|
|
22
24
|
use std::time::{Duration, Instant};
|
|
23
25
|
|
|
24
|
-
use magnus::{value::Opaque, Value};
|
|
25
26
|
use wasmtime::ResourceLimiter;
|
|
26
27
|
use wasmtime_wasi::p1::WasiP1Ctx;
|
|
27
28
|
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
|
|
28
29
|
|
|
30
|
+
use kobako_runtime::dispatch::DispatchHandler;
|
|
31
|
+
|
|
29
32
|
/// Per-invocation host state — the data half of the Single-Invocation
|
|
30
33
|
/// Slot. Threaded through every host import callback.
|
|
31
34
|
///
|
|
32
35
|
/// All field access is mediated by methods on this type — the WASI ctx
|
|
33
36
|
/// is rebuilt fresh before each invocation via
|
|
34
|
-
/// `Invocation::install_wasi`, the
|
|
37
|
+
/// `Invocation::install_wasi`, the dispatch handler is set once via
|
|
35
38
|
/// `Invocation::bind_on_dispatch`, and captured stdout/stderr bytes
|
|
36
39
|
/// are read after the invocation via `Invocation::stdout_bytes` /
|
|
37
40
|
/// `Invocation::stderr_bytes`. The fields are private so the mutation
|
|
38
41
|
/// surface stays narrow.
|
|
39
|
-
pub(
|
|
42
|
+
pub(crate) struct Invocation {
|
|
40
43
|
wasi: Option<WasiP1Ctx>,
|
|
41
44
|
stdout_pipe: Option<MemoryOutputPipe>,
|
|
42
45
|
stderr_pipe: Option<MemoryOutputPipe>,
|
|
43
|
-
on_dispatch: Option<
|
|
46
|
+
on_dispatch: Option<Arc<dyn DispatchHandler>>,
|
|
44
47
|
deadline: Option<Instant>,
|
|
45
48
|
limiter: MemoryLimiter,
|
|
46
49
|
wall_entry: Option<Instant>,
|
|
@@ -52,7 +55,7 @@ impl Invocation {
|
|
|
52
55
|
/// `Sandbox#memory_limit` cap in bytes (or `None` to disable the cap);
|
|
53
56
|
/// it is read from the wasmtime `ResourceLimiter` callback every
|
|
54
57
|
/// time the guest grows linear memory.
|
|
55
|
-
pub(
|
|
58
|
+
pub(crate) fn new(memory_limit: Option<usize>) -> Self {
|
|
56
59
|
Self {
|
|
57
60
|
wasi: None,
|
|
58
61
|
stdout_pipe: None,
|
|
@@ -66,10 +69,10 @@ impl Invocation {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
/// Install a freshly-built WASI context plus the matching stdout/stderr
|
|
69
|
-
/// pipe clones. Called from `
|
|
70
|
-
/// `
|
|
72
|
+
/// pipe clones. Called from `frames::install_wasi_frames`, which
|
|
73
|
+
/// `Driver::invoke` runs at the top of every guest
|
|
71
74
|
/// invocation.
|
|
72
|
-
pub(
|
|
75
|
+
pub(crate) fn install_wasi(
|
|
73
76
|
&mut self,
|
|
74
77
|
wasi: WasiP1Ctx,
|
|
75
78
|
stdout: MemoryOutputPipe,
|
|
@@ -80,16 +83,16 @@ impl Invocation {
|
|
|
80
83
|
self.stderr_pipe = Some(stderr);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
///
|
|
84
|
-
/// `__kobako_dispatch` host import invocation
|
|
85
|
-
/// request bytes and expects encoded Response bytes back.
|
|
86
|
-
pub(
|
|
87
|
-
self.on_dispatch = Some(
|
|
86
|
+
/// Bind the dispatch handler for this invocation. From this point on,
|
|
87
|
+
/// every `__kobako_dispatch` host import invocation hands the handler
|
|
88
|
+
/// the request bytes and expects encoded Response bytes back.
|
|
89
|
+
pub(crate) fn bind_on_dispatch(&mut self, handler: Arc<dyn DispatchHandler>) {
|
|
90
|
+
self.on_dispatch = Some(handler);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/// Snapshot the bytes captured on guest fd 1 during the most recent
|
|
91
94
|
/// run. Empty vec before any run.
|
|
92
|
-
pub(
|
|
95
|
+
pub(crate) fn stdout_bytes(&self) -> Vec<u8> {
|
|
93
96
|
self.stdout_pipe
|
|
94
97
|
.as_ref()
|
|
95
98
|
.map(|p| p.contents().to_vec())
|
|
@@ -98,53 +101,55 @@ impl Invocation {
|
|
|
98
101
|
|
|
99
102
|
/// Snapshot the bytes captured on guest fd 2 during the most recent
|
|
100
103
|
/// run. Empty vec before any run.
|
|
101
|
-
pub(
|
|
104
|
+
pub(crate) fn stderr_bytes(&self) -> Vec<u8> {
|
|
102
105
|
self.stderr_pipe
|
|
103
106
|
.as_ref()
|
|
104
107
|
.map(|p| p.contents().to_vec())
|
|
105
108
|
.unwrap_or_default()
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
/// Return the bound dispatch
|
|
109
|
-
///
|
|
110
|
-
///
|
|
111
|
+
/// Return a clone of the bound dispatch handler (an `Arc`, so the clone
|
|
112
|
+
/// is a cheap refcount bump). Cloning releases the borrow on the
|
|
113
|
+
/// `Caller` so the dispatcher can re-borrow it to write the response.
|
|
114
|
+
/// None means no handler has been bound yet via
|
|
111
115
|
/// `Invocation::bind_on_dispatch`.
|
|
112
|
-
pub(
|
|
113
|
-
self.on_dispatch
|
|
116
|
+
pub(crate) fn on_dispatch(&self) -> Option<Arc<dyn DispatchHandler>> {
|
|
117
|
+
self.on_dispatch.clone()
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
/// Mutable handle to the live WASI context. Panics if no context has
|
|
117
121
|
/// been installed yet — every call site is downstream of
|
|
118
|
-
/// `Invocation::install_wasi` running at the top of
|
|
119
|
-
///
|
|
120
|
-
///
|
|
121
|
-
pub(
|
|
122
|
+
/// `Invocation::install_wasi` running at the top of every `Driver`
|
|
123
|
+
/// invoke, so reaching this branch with `None` signals a host-side
|
|
124
|
+
/// wiring bug.
|
|
125
|
+
pub(crate) fn wasi_mut(&mut self) -> &mut WasiP1Ctx {
|
|
122
126
|
self.wasi.as_mut().expect(
|
|
123
|
-
"WASI context not initialised —
|
|
127
|
+
"WASI context not initialised — the driver must install frames before any WASI use",
|
|
124
128
|
)
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
/// Replace the per-run wall-clock deadline. `Some(at)` makes the
|
|
128
132
|
/// epoch-deadline callback trap once `Instant::now() >= at`; `None`
|
|
129
|
-
/// disables the cap. Called at the top of
|
|
130
|
-
|
|
133
|
+
/// disables the cap. Called from `Driver::prime_caps` at the top of
|
|
134
|
+
/// every invocation (`#eval` and `#run`).
|
|
135
|
+
pub(crate) fn set_deadline(&mut self, deadline: Option<Instant>) {
|
|
131
136
|
self.deadline = deadline;
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
/// Return the current per-run deadline. Read from the epoch-deadline
|
|
135
|
-
/// callback installed by `
|
|
136
|
-
pub(
|
|
140
|
+
/// callback installed by `Driver::new_store`.
|
|
141
|
+
pub(crate) fn deadline(&self) -> Option<Instant> {
|
|
137
142
|
self.deadline
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/// Mutable handle to the embedded `MemoryLimiter`. Required by
|
|
141
146
|
/// the wasmtime `ResourceLimiter` callback wiring in
|
|
142
|
-
/// `
|
|
147
|
+
/// `Driver::new_store`
|
|
143
148
|
/// (`store.limiter(|state| state.limiter_mut())`); kept private to
|
|
144
149
|
/// the wasm submodule so the only public surface for arming the
|
|
145
150
|
/// cap goes through `Invocation::arm_memory_cap` /
|
|
146
151
|
/// `Invocation::disarm_memory_cap`.
|
|
147
|
-
pub(
|
|
152
|
+
pub(crate) fn limiter_mut(&mut self) -> &mut MemoryLimiter {
|
|
148
153
|
&mut self.limiter
|
|
149
154
|
}
|
|
150
155
|
|
|
@@ -157,13 +162,13 @@ impl Invocation {
|
|
|
157
162
|
/// call to the corresponding `__kobako_*` export so post-run host
|
|
158
163
|
/// bookkeeping (e.g. fetching the OUTCOME_BUFFER) is not
|
|
159
164
|
/// attributed to the user script.
|
|
160
|
-
pub(
|
|
165
|
+
pub(crate) fn arm_memory_cap(&mut self, baseline: usize) {
|
|
161
166
|
self.limiter.activate(baseline);
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
/// Disarm the memory cap. See
|
|
165
170
|
/// `Invocation::arm_memory_cap`.
|
|
166
|
-
pub(
|
|
171
|
+
pub(crate) fn disarm_memory_cap(&mut self) {
|
|
167
172
|
self.limiter.deactivate();
|
|
168
173
|
}
|
|
169
174
|
|
|
@@ -173,7 +178,7 @@ impl Invocation {
|
|
|
173
178
|
/// bracket matches the `timeout` deadline accounting and
|
|
174
179
|
/// excludes post-run host bookkeeping such as `OUTCOME_BUFFER`
|
|
175
180
|
/// decoding.
|
|
176
|
-
pub(
|
|
181
|
+
pub(crate) fn start_wall_clock(&mut self) {
|
|
177
182
|
self.wall_entry = Some(Instant::now());
|
|
178
183
|
}
|
|
179
184
|
|
|
@@ -182,7 +187,7 @@ impl Invocation {
|
|
|
182
187
|
/// stop with no matching start (e.g. if the guest export call
|
|
183
188
|
/// never executed because of a host-side allocation failure)
|
|
184
189
|
/// leaves the previously-recorded value untouched.
|
|
185
|
-
pub(
|
|
190
|
+
pub(crate) fn stop_wall_clock(&mut self) {
|
|
186
191
|
if let Some(entry) = self.wall_entry.take() {
|
|
187
192
|
self.wall_time = entry.elapsed();
|
|
188
193
|
}
|
|
@@ -191,7 +196,7 @@ impl Invocation {
|
|
|
191
196
|
/// Return the wall-clock duration the most recent invocation
|
|
192
197
|
/// spent inside the guest export call.
|
|
193
198
|
/// Zero before the first invocation.
|
|
194
|
-
pub(
|
|
199
|
+
pub(crate) fn wall_time(&self) -> Duration {
|
|
195
200
|
self.wall_time
|
|
196
201
|
}
|
|
197
202
|
|
|
@@ -199,7 +204,7 @@ impl Invocation {
|
|
|
199
204
|
/// water mark of the per-invocation `memory.grow` delta past the
|
|
200
205
|
/// linear-memory size captured at invocation entry. Zero before
|
|
201
206
|
/// the first invocation.
|
|
202
|
-
pub(
|
|
207
|
+
pub(crate) fn memory_peak(&self) -> usize {
|
|
203
208
|
self.limiter.peak()
|
|
204
209
|
}
|
|
205
210
|
}
|
|
@@ -216,16 +221,16 @@ impl Invocation {
|
|
|
216
221
|
/// `cap_active` gates whether the cap is enforced — wasmtime's
|
|
217
222
|
/// `ResourceLimiter` also fires for the module's declared initial
|
|
218
223
|
/// allocation at instantiation time, but the cap stays dormant until
|
|
219
|
-
/// `MemoryLimiter::activate` flips the flag for one
|
|
220
|
-
///
|
|
221
|
-
///
|
|
224
|
+
/// `MemoryLimiter::activate` flips the flag for one `Driver` invoke
|
|
225
|
+
/// call. When `cap_active` is `false`, the limiter always allows
|
|
226
|
+
/// growth.
|
|
222
227
|
///
|
|
223
228
|
/// When `memory.grow` would push the per-invocation delta past
|
|
224
229
|
/// `max_memory`, the limiter returns `MemoryLimitTrap` from
|
|
225
230
|
/// `memory_growing`; wasmtime turns that into the trap surfaced to the
|
|
226
231
|
/// host as a guest invocation failure.
|
|
227
232
|
#[derive(Debug, Clone, Copy)]
|
|
228
|
-
pub(
|
|
233
|
+
pub(crate) struct MemoryLimiter {
|
|
229
234
|
max_memory: Option<usize>,
|
|
230
235
|
baseline: usize,
|
|
231
236
|
cap_active: bool,
|
|
@@ -274,7 +279,7 @@ impl MemoryLimiter {
|
|
|
274
279
|
/// rejected `desired` values that trip the memory
|
|
275
280
|
/// cap never update the peak, so the reported value never exceeds
|
|
276
281
|
/// `memory_limit`.
|
|
277
|
-
pub(
|
|
282
|
+
pub(crate) fn peak(&self) -> usize {
|
|
278
283
|
self.peak
|
|
279
284
|
}
|
|
280
285
|
}
|
|
@@ -329,7 +334,7 @@ impl MemoryLimitTrap {
|
|
|
329
334
|
/// by the sibling-module `classify_trap` unit tests to materialise
|
|
330
335
|
/// a representative error for downcast routing.
|
|
331
336
|
#[cfg(test)]
|
|
332
|
-
pub(
|
|
337
|
+
pub(crate) fn new(desired: usize, limit: usize) -> Self {
|
|
333
338
|
Self { desired, limit }
|
|
334
339
|
}
|
|
335
340
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//! kobako-wasmtime — the wasmtime implementation of the kobako runtime
|
|
2
|
+
//! contract.
|
|
3
|
+
//!
|
|
4
|
+
//! `Driver` implements `kobako_runtime::runtime::Runtime` over wasmtime:
|
|
5
|
+
//! every invocation instantiates a fresh instance from a pre-linked
|
|
6
|
+
//! template and discards the whole Store afterwards — the
|
|
7
|
+
//! per-invocation instance discipline (ABI v2). Everything engine-bound
|
|
8
|
+
//! lives behind the contract surface, so a frontend shell (the Ruby
|
|
9
|
+
//! ext's `Kobako::Runtime`) sees no wasmtime type.
|
|
10
|
+
//!
|
|
11
|
+
//! Module layout (one responsibility per file):
|
|
12
|
+
//!
|
|
13
|
+
//! * `driver` — `Driver` + `impl kobako_runtime::runtime::Runtime`
|
|
14
|
+
//! (the run mechanics).
|
|
15
|
+
//! * `cache` — process-wide Engine + per-path Module cache and the
|
|
16
|
+
//! process-singleton epoch ticker thread.
|
|
17
|
+
//! * `config` — per-Driver caps (timeout / stdout / stderr limits).
|
|
18
|
+
//! * `exports` — per-invocation `__kobako_eval` / `_run` /
|
|
19
|
+
//! `_take_outcome` / `_alloc` / `memory` handles.
|
|
20
|
+
//! * `instance_pre` — host-import Linker wiring + per-path
|
|
21
|
+
//! `InstancePre` cache.
|
|
22
|
+
//! * `invocation` — Invocation (per-Store context), the
|
|
23
|
+
//! `MemoryLimiter` memory cap, and the trap marker types
|
|
24
|
+
//! (`TimeoutTrap` / `MemoryLimitTrap`).
|
|
25
|
+
//! * `dispatch` — `__kobako_dispatch` host-import dispatch helpers.
|
|
26
|
+
//! * `frames` — stdin frame stream + WASI context assembly, `#run`
|
|
27
|
+
//! envelope write, OUTCOME_BUFFER readout.
|
|
28
|
+
//! * `guest_mem` — Caller-based guest linear-memory alloc / write / read.
|
|
29
|
+
//! * `capture` — stdout / stderr pipe sizing + clip helpers.
|
|
30
|
+
//! * `ambient` — frozen WASI clocks + constant RNG (ambient denial).
|
|
31
|
+
//! * `trap` — wasmtime-error → neutral `Trap` classification.
|
|
32
|
+
|
|
33
|
+
mod ambient;
|
|
34
|
+
mod cache;
|
|
35
|
+
mod capture;
|
|
36
|
+
mod config;
|
|
37
|
+
mod dispatch;
|
|
38
|
+
mod driver;
|
|
39
|
+
mod exports;
|
|
40
|
+
mod frames;
|
|
41
|
+
mod guest_mem;
|
|
42
|
+
mod instance_pre;
|
|
43
|
+
mod invocation;
|
|
44
|
+
mod trap;
|
|
45
|
+
|
|
46
|
+
pub use config::Config;
|
|
47
|
+
pub use driver::Driver;
|
|
@@ -6,25 +6,24 @@
|
|
|
6
6
|
//! `TimeoutTrap`. The classification is a pure function over the error's
|
|
7
7
|
//! downcast chain so it can be exercised from `cargo test` without the
|
|
8
8
|
//! magnus surface; the trap marker types themselves live in
|
|
9
|
-
//! `
|
|
9
|
+
//! `crate::invocation` (where the limiter / callback construct them).
|
|
10
10
|
|
|
11
11
|
use std::time::Instant;
|
|
12
12
|
|
|
13
|
-
use magnus::{Error as MagnusError, Ruby};
|
|
14
13
|
use wasmtime::{StoreContextMut, UpdateDeadline};
|
|
15
14
|
|
|
16
|
-
use
|
|
17
|
-
use
|
|
15
|
+
use crate::invocation::{Invocation, MemoryLimitTrap, TimeoutTrap};
|
|
16
|
+
use kobako_runtime::error::{SetupError, Trap};
|
|
18
17
|
|
|
19
18
|
/// Epoch-deadline callback installed on every Store. Read the per-run
|
|
20
19
|
/// wall-clock deadline from `Invocation` and trap with
|
|
21
20
|
/// `TimeoutTrap` once the deadline has passed; otherwise extend the
|
|
22
21
|
/// next check by one tick of the process-wide epoch ticker. When the
|
|
23
|
-
/// deadline is `None` the callback should not fire under normal
|
|
24
|
-
/// `
|
|
22
|
+
/// deadline is `None` the callback should not fire under the normal
|
|
23
|
+
/// `Driver` invoke flow because
|
|
25
24
|
/// `set_epoch_deadline(u64::MAX)` is used; returning a long extension
|
|
26
25
|
/// keeps the callback inert as a defence in depth.
|
|
27
|
-
pub(
|
|
26
|
+
pub(crate) fn epoch_deadline_callback(
|
|
28
27
|
ctx: StoreContextMut<'_, Invocation>,
|
|
29
28
|
) -> wasmtime::Result<UpdateDeadline> {
|
|
30
29
|
match ctx.data().deadline() {
|
|
@@ -63,39 +62,34 @@ fn classify_trap(err: &wasmtime::Error) -> TrapClass {
|
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
///
|
|
67
|
-
///
|
|
68
|
-
///
|
|
69
|
-
/// Sandbox
|
|
70
|
-
///
|
|
71
|
-
/// than ABI vocabulary.
|
|
65
|
+
/// Classify a wasmtime call error into a neutral `Trap`. The ABI export
|
|
66
|
+
/// symbol (`__kobako_eval` / `__kobako_run`) is deliberately omitted from
|
|
67
|
+
/// the message — the Sandbox layer attaches the user-facing verb
|
|
68
|
+
/// (`Sandbox#eval` / `Sandbox#run`) so the message reads in caller
|
|
69
|
+
/// vocabulary rather than ABI vocabulary.
|
|
72
70
|
///
|
|
73
71
|
/// For the configured-cap paths (`TrapClass::Timeout` /
|
|
74
72
|
/// `TrapClass::MemoryLimit`) the trap's own `std::fmt::Display`
|
|
75
73
|
/// carries the user-facing reason (`"wall-clock deadline exceeded"`,
|
|
76
74
|
/// `"linear memory growth exceeded memory_limit: ..."`). The wasmtime
|
|
77
|
-
/// outer wrapper at `format!("{}"
|
|
75
|
+
/// outer wrapper at `format!("{err}")` would otherwise surface only
|
|
78
76
|
/// the `"error while executing at wasm backtrace: ..."` framing, which
|
|
79
77
|
/// is operator noise on a cap trap. For `TrapClass::Other` the framing
|
|
80
78
|
/// is kept but the chain's root cause is appended (see
|
|
81
79
|
/// `other_trap_message`) so the real trap reason survives.
|
|
82
|
-
pub(
|
|
80
|
+
pub(crate) fn trap_from(err: wasmtime::Error) -> Trap {
|
|
83
81
|
match classify_trap(&err) {
|
|
84
|
-
TrapClass::Timeout =>
|
|
85
|
-
|
|
86
|
-
.downcast_ref::<TimeoutTrap>()
|
|
82
|
+
TrapClass::Timeout => Trap::Timeout(
|
|
83
|
+
err.downcast_ref::<TimeoutTrap>()
|
|
87
84
|
.map(|t| t.to_string())
|
|
88
|
-
.unwrap_or_else(|| format!("{}"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let msg = err
|
|
93
|
-
.downcast_ref::<MemoryLimitTrap>()
|
|
85
|
+
.unwrap_or_else(|| format!("{err}")),
|
|
86
|
+
),
|
|
87
|
+
TrapClass::MemoryLimit => Trap::MemoryLimit(
|
|
88
|
+
err.downcast_ref::<MemoryLimitTrap>()
|
|
94
89
|
.map(|t| t.to_string())
|
|
95
|
-
.unwrap_or_else(|| format!("{}"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
TrapClass::Other => trap_err(ruby, other_trap_message(&err)),
|
|
90
|
+
.unwrap_or_else(|| format!("{err}")),
|
|
91
|
+
),
|
|
92
|
+
TrapClass::Other => Trap::Other(other_trap_message(&err)),
|
|
99
93
|
}
|
|
100
94
|
}
|
|
101
95
|
|
|
@@ -116,21 +110,21 @@ fn other_trap_message(err: &wasmtime::Error) -> String {
|
|
|
116
110
|
}
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
///
|
|
120
|
-
/// during `from_path` construction, before any
|
|
121
|
-
/// failure is a construction setup fault, not a
|
|
113
|
+
/// Classify an instantiation error as a runtime-dead `SetupError`.
|
|
114
|
+
/// Instantiation runs during `from_path` construction, before any
|
|
115
|
+
/// invocation — every such failure is a construction setup fault, not a
|
|
122
116
|
/// per-invocation cap outcome. The memory cap is dormant during
|
|
123
117
|
/// instantiation (see `Invocation::arm_memory_cap` /
|
|
124
118
|
/// `Invocation::disarm_memory_cap`) and the epoch deadline is not yet
|
|
125
|
-
/// armed, so the `
|
|
126
|
-
pub(
|
|
127
|
-
|
|
119
|
+
/// armed, so the `trap_from` trap-class split does not apply here.
|
|
120
|
+
pub(crate) fn instantiate_err(err: wasmtime::Error) -> SetupError {
|
|
121
|
+
SetupError::Dead(format!("instantiate: {err}"))
|
|
128
122
|
}
|
|
129
123
|
|
|
130
124
|
#[cfg(test)]
|
|
131
125
|
mod tests {
|
|
132
126
|
use super::{classify_trap, other_trap_message, TrapClass};
|
|
133
|
-
use crate::
|
|
127
|
+
use crate::invocation::{MemoryLimitTrap, TimeoutTrap};
|
|
134
128
|
|
|
135
129
|
#[test]
|
|
136
130
|
fn classify_trap_routes_timeout_trap_to_timeout() {
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/ext/kobako/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "kobako"
|
|
3
|
-
version = "0.12.
|
|
3
|
+
version = "0.12.2"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
authors = ["Aotokitsuruya <contact@aotoki.me>"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -11,34 +11,11 @@ crate-type = ["cdylib"]
|
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
13
|
magnus = { version = "0.8.2" }
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"runtime",
|
|
23
|
-
"gc",
|
|
24
|
-
"gc-drc",
|
|
25
|
-
"addr2line",
|
|
26
|
-
"demangle",
|
|
27
|
-
"wat",
|
|
28
|
-
] }
|
|
29
|
-
# wasmtime-wasi provides WASI preview1 support for routing guest stdout/stderr
|
|
30
|
-
# into in-memory buffers. The `p1` feature enables the
|
|
31
|
-
# WasiCtxBuilder + preview1 adapter which wires fd 1/2 to pipes. We omit
|
|
32
|
-
# `p2` (component-model) and `p0`/`p3` (async) because kobako runs
|
|
33
|
-
# synchronous sandboxes only.
|
|
34
|
-
wasmtime-wasi = { version = "45.0.0", default-features = false, features = ["p1"] }
|
|
35
|
-
# sha2 keys the on-disk compiled-module cache by Guest Binary content
|
|
36
|
-
# (see runtime/cache.rs); a collision would load the wrong artifact, so
|
|
37
|
-
# the hash must be cryptographic.
|
|
38
|
-
sha2 = "0.11"
|
|
39
|
-
|
|
40
|
-
# libc supplies geteuid for the cache-directory ownership check gating
|
|
41
|
-
# the unsafe artifact deserialize (see runtime/cache.rs); std exposes a
|
|
42
|
-
# file's owner but not the process's effective uid.
|
|
43
|
-
[target.'cfg(unix)'.dependencies]
|
|
44
|
-
libc = "0.2"
|
|
14
|
+
# The engine-neutral host runtime contract plus its wasmtime driver —
|
|
15
|
+
# the ext is a thin magnus shim over them. Pure path dependencies: both
|
|
16
|
+
# crates ship inside the gem, so installs never resolve them against
|
|
17
|
+
# crates.io and the gem release cadence stays decoupled from theirs.
|
|
18
|
+
# wasmtime itself enters only through kobako-wasmtime; no engine type
|
|
19
|
+
# reaches this crate.
|
|
20
|
+
kobako-runtime = { path = "../../crates/kobako-runtime" }
|
|
21
|
+
kobako-wasmtime = { path = "../../crates/kobako-wasmtime" }
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//! The magnus bridge for the guest→host dispatch seam.
|
|
2
|
+
//!
|
|
3
|
+
//! The Ruby-Proc `DispatchHandler` and the frame-scoped `GuestYielder`
|
|
4
|
+
//! handle the Proc re-enters the guest through — the one place the
|
|
5
|
+
//! dispatch seam touches `magnus`. The wasm-side dispatch path
|
|
6
|
+
//! (`kobako_wasmtime`'s dispatch module) sees only the contract traits.
|
|
7
|
+
|
|
8
|
+
use core::cell::Cell;
|
|
9
|
+
use core::ptr::NonNull;
|
|
10
|
+
|
|
11
|
+
use magnus::value::{Opaque, ReprValue};
|
|
12
|
+
use magnus::{method, prelude::*, Error as MagnusError, RClass, RString, Ruby, Value};
|
|
13
|
+
|
|
14
|
+
use kobako_runtime::dispatch::DispatchHandler;
|
|
15
|
+
use kobako_runtime::yielder::Yielder;
|
|
16
|
+
|
|
17
|
+
/// Register the `Kobako::Runtime::GuestYielder` Ruby class. Called from
|
|
18
|
+
/// `crate::runtime::init` after `Kobako::Runtime` is defined so the
|
|
19
|
+
/// `#[magnus::wrap]` class name resolves before any object is wrapped.
|
|
20
|
+
pub(super) fn register(runtime_class: RClass) -> Result<(), MagnusError> {
|
|
21
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
22
|
+
let class = runtime_class.define_class("GuestYielder", ruby.class_object())?;
|
|
23
|
+
class.define_method("call", method!(GuestYielder::call, 1))?;
|
|
24
|
+
Ok(())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Frame-scoped Ruby handle that lets the dispatch `Proc` re-enter the
|
|
28
|
+
/// guest to run a yielded block. It wraps the active `&mut dyn Yielder`
|
|
29
|
+
/// for exactly one `__kobako_dispatch` frame: the bridge builds one, hands
|
|
30
|
+
/// it to the `Proc` as the second argument, and `invalidate`s it the
|
|
31
|
+
/// instant the `Proc` returns. A guest block stashed and called after that
|
|
32
|
+
/// frame normally raises `LocalJumpError` at the Ruby
|
|
33
|
+
/// `Transport::Yielder` net — invalidated in the dispatcher's `ensure`,
|
|
34
|
+
/// which fires before this handle is reached. This inner invalidation is
|
|
35
|
+
/// the backstop behind that outer net: it keeps `call`'s `unsafe`
|
|
36
|
+
/// `NonNull` deref from touching freed stack should the outer net ever be
|
|
37
|
+
/// bypassed, so neither net is redundant.
|
|
38
|
+
///
|
|
39
|
+
/// This is the single, explicit, frame-scoped FFI pointer the host↔guest
|
|
40
|
+
/// re-entry still costs: `magnus`' `funcall` sits between two Rust frames,
|
|
41
|
+
/// so the typed `&mut dyn Yielder` cannot cross it and is erased to a raw
|
|
42
|
+
/// pointer here. Unlike the dispatch `Proc`, this handle holds **no Ruby
|
|
43
|
+
/// `Value`**, so GC has nothing to trace through it — it needs no `mark`.
|
|
44
|
+
#[magnus::wrap(class = "Kobako::Runtime::GuestYielder", free_immediately, size)]
|
|
45
|
+
struct GuestYielder {
|
|
46
|
+
yielder: Cell<Option<NonNull<dyn Yielder>>>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// SAFETY: magnus requires `Send + Sync` on wrapped types. The raw pointer
|
|
50
|
+
// is created, used, and invalidated within a single `__kobako_dispatch`
|
|
51
|
+
// frame on the one Ruby thread that owns the active Invocation (SPEC.md
|
|
52
|
+
// Single-Invocation Slot); it is never read from another thread.
|
|
53
|
+
unsafe impl Send for GuestYielder {}
|
|
54
|
+
unsafe impl Sync for GuestYielder {}
|
|
55
|
+
|
|
56
|
+
impl GuestYielder {
|
|
57
|
+
/// Erase the frame-scoped `&mut dyn Yielder` into a Ruby-owned handle.
|
|
58
|
+
/// Safety contract for the caller: `invalidate` MUST run before the
|
|
59
|
+
/// borrow this pointer came from ends (i.e. before the dispatch frame
|
|
60
|
+
/// returns).
|
|
61
|
+
fn new(yielder: &mut dyn Yielder) -> Self {
|
|
62
|
+
let ptr = NonNull::from(yielder);
|
|
63
|
+
// Erase the borrow's lifetime to `'static`; the pointer is only
|
|
64
|
+
// ever dereferenced while it is still `Some` (i.e. `invalidate`
|
|
65
|
+
// has not run), so the referent is guaranteed live.
|
|
66
|
+
let ptr: NonNull<dyn Yielder> = unsafe {
|
|
67
|
+
std::mem::transmute::<NonNull<dyn Yielder + '_>, NonNull<dyn Yielder + 'static>>(ptr)
|
|
68
|
+
};
|
|
69
|
+
Self {
|
|
70
|
+
yielder: Cell::new(Some(ptr)),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Mark this handle dead. Called the instant the dispatch frame's
|
|
75
|
+
/// `funcall` returns, so a guest block stashed beyond its frame raises
|
|
76
|
+
/// instead of dereferencing freed stack.
|
|
77
|
+
fn invalidate(&self) {
|
|
78
|
+
self.yielder.set(None);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Ruby-visible `call(args_bytes) -> resp_bytes`: drive one yield
|
|
82
|
+
/// round-trip. Stands in for the `String -> String` callable the host
|
|
83
|
+
/// `Transport::Yielder` invokes. Raises `Kobako::TrapError` when the
|
|
84
|
+
/// handle has been invalidated (escaped guest block) or the re-entry
|
|
85
|
+
/// itself traps.
|
|
86
|
+
fn call(&self, args: RString) -> Result<RString, MagnusError> {
|
|
87
|
+
let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_yield");
|
|
88
|
+
let Some(mut ptr) = self.yielder.get() else {
|
|
89
|
+
return Err(super::errors::trap_err(
|
|
90
|
+
&ruby,
|
|
91
|
+
"guest block invoked after the host dispatch frame returned",
|
|
92
|
+
));
|
|
93
|
+
};
|
|
94
|
+
let bytes = super::rstring_to_vec(args);
|
|
95
|
+
// SAFETY: `yielder` is `Some`, so `invalidate` has not run — the
|
|
96
|
+
// dispatch frame that lent the `&mut dyn Yielder` is still on the
|
|
97
|
+
// Rust stack, and the Single-Invocation Slot guarantees no other
|
|
98
|
+
// frame aliases it. The borrow ends with this method.
|
|
99
|
+
let yielder: &mut dyn Yielder = unsafe { ptr.as_mut() };
|
|
100
|
+
let resp = yielder
|
|
101
|
+
.yield_block(&bytes)
|
|
102
|
+
.map_err(|t| super::errors::trap_to_magnus(&ruby, t))?;
|
|
103
|
+
Ok(ruby.str_from_slice(&resp))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// The Ruby-Proc bridge: a `DispatchHandler` backed by the host-side
|
|
108
|
+
/// dispatch `Proc` registered through `Runtime#on_dispatch=`. This is the
|
|
109
|
+
/// one place the dispatch seam touches `magnus`; the wasm runtime sees
|
|
110
|
+
/// only the trait. The Proc is GC-rooted by `Runtime`'s `mark`; this
|
|
111
|
+
/// struct holds an `Opaque` copy of the same handle.
|
|
112
|
+
pub(super) struct RubyDispatchHandler {
|
|
113
|
+
on_dispatch: Opaque<Value>,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl RubyDispatchHandler {
|
|
117
|
+
pub(super) fn new(on_dispatch: Opaque<Value>) -> Self {
|
|
118
|
+
Self { on_dispatch }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
impl DispatchHandler for RubyDispatchHandler {
|
|
123
|
+
/// Call the Ruby Proc with the request bytes and return the encoded
|
|
124
|
+
/// Response bytes. The Proc is contracted to fold every dispatch
|
|
125
|
+
/// failure into a `Response.err` envelope (see
|
|
126
|
+
/// `Kobako::Transport::Dispatcher.dispatch`), so a raise is a contract
|
|
127
|
+
/// violation surfaced as `None` — the dispatcher then walks the
|
|
128
|
+
/// 0-return wire-fault path.
|
|
129
|
+
fn dispatch(&self, request: &[u8], yielder: &mut dyn Yielder) -> Option<Vec<u8>> {
|
|
130
|
+
// The wasmtime callback runs on the same Ruby thread that called
|
|
131
|
+
// the active Sandbox invocation (#eval or #run) — the invariant
|
|
132
|
+
// SPEC Implementation Standards Architecture pins for the host gem
|
|
133
|
+
// — so `Ruby::get()` is always available here. Panicking with
|
|
134
|
+
// `expect` localises the violation rather than letting a nonsense
|
|
135
|
+
// error propagate.
|
|
136
|
+
let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_dispatch");
|
|
137
|
+
let proc_value: Value = ruby.get_inner(self.on_dispatch);
|
|
138
|
+
let req_str = ruby.str_from_slice(request);
|
|
139
|
+
// Hand the Proc a frame-scoped yielder object as its second arg and
|
|
140
|
+
// invalidate it the instant the Proc returns, so a guest block that
|
|
141
|
+
// escapes the dispatch frame can never deref the freed stack
|
|
142
|
+
// pointer. `guest_yielder` holds no Ruby Value, so it needs no GC
|
|
143
|
+
// mark — the GC has nothing to trace through it.
|
|
144
|
+
let guest_yielder = ruby.obj_wrap(GuestYielder::new(yielder));
|
|
145
|
+
let resp: Result<RString, magnus::Error> =
|
|
146
|
+
proc_value.funcall("call", (req_str, guest_yielder));
|
|
147
|
+
guest_yielder.invalidate();
|
|
148
|
+
resp.ok().map(super::rstring_to_vec)
|
|
149
|
+
}
|
|
150
|
+
}
|