kobako 0.4.0 → 0.5.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 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +1 -1
- data/README.md +0 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +4 -2
- data/ext/kobako/src/{wasm → runtime}/cache.rs +12 -16
- data/ext/kobako/src/runtime/capture.rs +91 -0
- data/ext/kobako/src/runtime/config.rs +26 -0
- data/ext/kobako/src/runtime/dispatch.rs +211 -0
- data/ext/kobako/src/runtime/exports.rs +51 -0
- data/ext/kobako/src/runtime/guest_mem.rs +228 -0
- data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +94 -86
- data/ext/kobako/src/runtime/trap.rs +134 -0
- data/ext/kobako/src/runtime.rs +782 -0
- data/ext/kobako/src/snapshot.rs +110 -0
- data/lib/kobako/capture.rb +11 -16
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +13 -5
- data/lib/kobako/codec/factory.rb +12 -12
- data/lib/kobako/codec/utils.rb +56 -59
- data/lib/kobako/codec.rb +6 -3
- data/lib/kobako/errors.rb +45 -28
- data/lib/kobako/fault.rb +40 -0
- data/lib/kobako/handle.rb +4 -6
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +31 -35
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +83 -72
- data/lib/kobako/sandbox_options.rb +6 -9
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +6 -7
- data/lib/kobako/snippet/source.rb +8 -8
- data/lib/kobako/snippet.rb +7 -9
- data/lib/kobako/transport/dispatcher.rb +195 -0
- data/lib/kobako/{rpc/wire_error.rb → transport/error.rb} +7 -6
- data/lib/kobako/transport/request.rb +78 -0
- data/lib/kobako/transport/response.rb +69 -0
- data/lib/kobako/transport/run.rb +141 -0
- data/lib/kobako/transport/yield.rb +91 -0
- data/lib/kobako/transport/yielder.rb +89 -0
- data/lib/kobako/transport.rb +24 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +4 -4
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/catalog/handles.rbs +19 -0
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
- data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
- data/sig/kobako/codec/decoder.rbs +2 -1
- data/sig/kobako/codec/factory.rbs +2 -2
- data/sig/kobako/codec/utils.rbs +5 -5
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +2 -3
- data/sig/kobako/namespace.rbs +19 -0
- data/sig/kobako/outcome.rbs +2 -2
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +5 -8
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/transport/dispatcher.rbs +34 -0
- data/sig/kobako/transport/error.rbs +6 -0
- data/sig/kobako/transport/request.rbs +32 -0
- data/sig/kobako/transport/response.rbs +30 -0
- data/sig/kobako/transport/run.rbs +27 -0
- data/sig/kobako/transport/yield.rbs +34 -0
- data/sig/kobako/transport/yielder.rbs +21 -0
- data/sig/kobako/transport.rbs +4 -0
- metadata +48 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -162
- data/ext/kobako/src/wasm/instance.rs +0 -873
- data/ext/kobako/src/wasm.rs +0 -126
- data/lib/kobako/handle_table.rb +0 -119
- data/lib/kobako/invocation.rb +0 -143
- data/lib/kobako/rpc/dispatcher.rb +0 -171
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -146
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/handle_table.rbs +0 -23
- data/sig/kobako/invocation.rbs +0 -25
- data/sig/kobako/rpc/dispatcher.rbs +0 -33
- data/sig/kobako/rpc/envelope.rbs +0 -51
- data/sig/kobako/rpc/fault.rbs +0 -20
- data/sig/kobako/rpc/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -31
- data/sig/kobako/rpc/wire_error.rbs +0 -6
- data/sig/kobako/wasm.rbs +0 -41
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
//! Per-
|
|
1
|
+
//! Per-invocation host state — the materialised
|
|
2
|
+
//! [SPEC.md Single-Invocation Slot] (one `Invocation` per OS thread
|
|
3
|
+
//! for the lifetime of one `Runtime::eval` / `Runtime::run` call).
|
|
2
4
|
//!
|
|
3
|
-
//! Owned by
|
|
5
|
+
//! Owned by `StoreCell` (a `RefCell` shim wrapping `wasmtime::Store`)
|
|
4
6
|
//! and threaded through every host import — the `__kobako_dispatch`
|
|
5
|
-
//! dispatcher reads the
|
|
6
|
-
//!
|
|
7
|
-
//! every
|
|
7
|
+
//! dispatcher reads the bound dispatch Proc, while the run-path methods
|
|
8
|
+
//! on `crate::runtime::Runtime` install fresh WASI context + pipes
|
|
9
|
+
//! before every invocation (docs/behavior.md B-03 / B-04).
|
|
8
10
|
//!
|
|
9
|
-
//! The
|
|
11
|
+
//! The slot also carries the per-invocation wall-clock deadline
|
|
10
12
|
//! (docs/behavior.md B-01, E-19) and the per-invocation linear-memory
|
|
11
|
-
//! delta cap
|
|
13
|
+
//! delta cap `MemoryLimiter` (docs/behavior.md B-01, E-20). Both are
|
|
12
14
|
//! read from the wasmtime `epoch_deadline_callback` / `ResourceLimiter`
|
|
13
|
-
//! callbacks installed in
|
|
15
|
+
//! callbacks installed in `crate::runtime::Runtime::from_path`. The
|
|
14
16
|
//! memory cap measures only the `memory.grow` delta past the linear-
|
|
15
17
|
//! memory size captured at invocation entry — the mruby image's
|
|
16
18
|
//! initial allocation and prior invocations' watermark are outside the
|
|
17
19
|
//! budget.
|
|
20
|
+
//!
|
|
21
|
+
//! [SPEC.md Single-Invocation Slot]: ../../../SPEC.md
|
|
18
22
|
|
|
19
23
|
use std::cell::{Ref, RefCell, RefMut};
|
|
20
24
|
use std::time::{Duration, Instant};
|
|
@@ -24,46 +28,48 @@ use wasmtime::{ResourceLimiter, Store as WtStore};
|
|
|
24
28
|
use wasmtime_wasi::p1::WasiP1Ctx;
|
|
25
29
|
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
|
|
26
30
|
|
|
27
|
-
/// Per-
|
|
31
|
+
/// Per-invocation host state — the data half of the Single-Invocation
|
|
32
|
+
/// Slot. Threaded through every host import callback.
|
|
28
33
|
///
|
|
29
|
-
/// All field access is mediated by methods on this type — the WASI ctx
|
|
30
|
-
/// rebuilt fresh before each
|
|
31
|
-
/// Ruby
|
|
32
|
-
/// captured stdout/stderr bytes
|
|
33
|
-
///
|
|
34
|
-
/// are private so the mutation
|
|
35
|
-
|
|
34
|
+
/// All field access is mediated by methods on this type — the WASI ctx
|
|
35
|
+
/// is rebuilt fresh before each invocation via
|
|
36
|
+
/// `Invocation::install_wasi`, the Ruby dispatch Proc is set once via
|
|
37
|
+
/// `Invocation::bind_on_dispatch`, and captured stdout/stderr bytes
|
|
38
|
+
/// are read after the invocation via `Invocation::stdout_bytes` /
|
|
39
|
+
/// `Invocation::stderr_bytes`. The fields are private so the mutation
|
|
40
|
+
/// surface stays narrow.
|
|
41
|
+
pub(super) struct Invocation {
|
|
36
42
|
wasi: Option<WasiP1Ctx>,
|
|
37
43
|
stdout_pipe: Option<MemoryOutputPipe>,
|
|
38
44
|
stderr_pipe: Option<MemoryOutputPipe>,
|
|
39
|
-
|
|
45
|
+
on_dispatch: Option<Opaque<Value>>,
|
|
40
46
|
deadline: Option<Instant>,
|
|
41
|
-
limiter:
|
|
47
|
+
limiter: MemoryLimiter,
|
|
42
48
|
wall_entry: Option<Instant>,
|
|
43
49
|
wall_time: Duration,
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
impl
|
|
52
|
+
impl Invocation {
|
|
47
53
|
/// Build a fresh per-Store host state. `memory_limit` carries the
|
|
48
54
|
/// `Sandbox#memory_limit` cap in bytes (or `None` to disable the cap);
|
|
49
|
-
/// it is read from the wasmtime
|
|
55
|
+
/// it is read from the wasmtime `ResourceLimiter` callback every
|
|
50
56
|
/// time the guest grows linear memory (docs/behavior.md B-01, E-20).
|
|
51
57
|
pub(super) fn new(memory_limit: Option<usize>) -> Self {
|
|
52
58
|
Self {
|
|
53
59
|
wasi: None,
|
|
54
60
|
stdout_pipe: None,
|
|
55
61
|
stderr_pipe: None,
|
|
56
|
-
|
|
62
|
+
on_dispatch: None,
|
|
57
63
|
deadline: None,
|
|
58
|
-
limiter:
|
|
64
|
+
limiter: MemoryLimiter::new(memory_limit),
|
|
59
65
|
wall_entry: None,
|
|
60
66
|
wall_time: Duration::ZERO,
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/// Install a freshly-built WASI context plus the matching stdout/stderr
|
|
65
|
-
/// pipe clones. Called from
|
|
66
|
-
///
|
|
71
|
+
/// pipe clones. Called from `crate::runtime::Runtime::eval` /
|
|
72
|
+
/// `crate::runtime::Runtime::run` at the top of every guest
|
|
67
73
|
/// invocation (docs/behavior.md B-03 / B-04).
|
|
68
74
|
pub(super) fn install_wasi(
|
|
69
75
|
&mut self,
|
|
@@ -76,10 +82,11 @@ impl HostState {
|
|
|
76
82
|
self.stderr_pipe = Some(stderr);
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
///
|
|
80
|
-
///
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
/// Register the Ruby-side dispatch +Proc+. From this point on, every
|
|
86
|
+
/// `__kobako_dispatch` host import invocation calls the Proc with the
|
|
87
|
+
/// request bytes and expects encoded Response bytes back.
|
|
88
|
+
pub(super) fn bind_on_dispatch(&mut self, proc_value: Opaque<Value>) {
|
|
89
|
+
self.on_dispatch = Some(proc_value);
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/// Snapshot the bytes captured on guest fd 1 during the most recent
|
|
@@ -100,21 +107,22 @@ impl HostState {
|
|
|
100
107
|
.unwrap_or_default()
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
/// Return the bound
|
|
104
|
-
/// handle is returned by value rather than by reference. None
|
|
105
|
-
///
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
/// Return the bound dispatch Proc handle. `Opaque<Value>` is `Copy`,
|
|
111
|
+
/// so the handle is returned by value rather than by reference. None
|
|
112
|
+
/// means no Proc has been bound yet via
|
|
113
|
+
/// `Invocation::bind_on_dispatch`.
|
|
114
|
+
pub(super) fn on_dispatch(&self) -> Option<Opaque<Value>> {
|
|
115
|
+
self.on_dispatch
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
/// Mutable handle to the live WASI context. Panics if no context has
|
|
111
119
|
/// been installed yet — every call site is downstream of
|
|
112
|
-
///
|
|
113
|
-
/// `
|
|
120
|
+
/// `Invocation::install_wasi` running at the top of
|
|
121
|
+
/// `Runtime::eval` / `Runtime::run`, so reaching this branch with
|
|
114
122
|
/// `None` signals a host-side wiring bug.
|
|
115
123
|
pub(super) fn wasi_mut(&mut self) -> &mut WasiP1Ctx {
|
|
116
124
|
self.wasi.as_mut().expect(
|
|
117
|
-
"WASI context not initialised — call
|
|
125
|
+
"WASI context not initialised — call Runtime#eval / Runtime#run before any WASI use",
|
|
118
126
|
)
|
|
119
127
|
}
|
|
120
128
|
|
|
@@ -126,19 +134,19 @@ impl HostState {
|
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
/// Return the current per-run deadline. Read from the epoch-deadline
|
|
129
|
-
/// callback installed by
|
|
137
|
+
/// callback installed by `crate::runtime::Runtime::from_path`.
|
|
130
138
|
pub(super) fn deadline(&self) -> Option<Instant> {
|
|
131
139
|
self.deadline
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
/// Mutable handle to the embedded
|
|
135
|
-
/// the wasmtime
|
|
136
|
-
///
|
|
142
|
+
/// Mutable handle to the embedded `MemoryLimiter`. Required by
|
|
143
|
+
/// the wasmtime `ResourceLimiter` callback wiring in
|
|
144
|
+
/// `crate::runtime::Runtime::from_path`
|
|
137
145
|
/// (`store.limiter(|state| state.limiter_mut())`); kept private to
|
|
138
146
|
/// the wasm submodule so the only public surface for arming the
|
|
139
|
-
/// cap goes through
|
|
140
|
-
///
|
|
141
|
-
pub(super) fn limiter_mut(&mut self) -> &mut
|
|
147
|
+
/// cap goes through `Invocation::arm_memory_cap` /
|
|
148
|
+
/// `Invocation::disarm_memory_cap`.
|
|
149
|
+
pub(super) fn limiter_mut(&mut self) -> &mut MemoryLimiter {
|
|
142
150
|
&mut self.limiter
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -147,7 +155,7 @@ impl HostState {
|
|
|
147
155
|
/// charges only the `memory.grow` delta past `baseline` against
|
|
148
156
|
/// the cap, so the mruby image's initial allocation and the
|
|
149
157
|
/// high-water mark left by prior invocations do not consume the
|
|
150
|
-
/// budget. Paired with
|
|
158
|
+
/// budget. Paired with `Invocation::disarm_memory_cap` around the
|
|
151
159
|
/// call to the corresponding `__kobako_*` export so post-run host
|
|
152
160
|
/// bookkeeping (e.g. fetching the OUTCOME_BUFFER) is not
|
|
153
161
|
/// attributed to the user script.
|
|
@@ -156,7 +164,7 @@ impl HostState {
|
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
/// Disarm the docs/behavior.md E-20 memory cap. See
|
|
159
|
-
///
|
|
167
|
+
/// `Invocation::arm_memory_cap`.
|
|
160
168
|
pub(super) fn disarm_memory_cap(&mut self) {
|
|
161
169
|
self.limiter.deactivate();
|
|
162
170
|
}
|
|
@@ -172,7 +180,7 @@ impl HostState {
|
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
/// Close the docs/behavior.md B-35 `wall_time` measurement
|
|
175
|
-
/// started by
|
|
183
|
+
/// started by `Invocation::start_wall_clock`. Idempotent — a
|
|
176
184
|
/// stop with no matching start (e.g. if the guest export call
|
|
177
185
|
/// never executed because of a host-side allocation failure)
|
|
178
186
|
/// leaves the previously-recorded value untouched.
|
|
@@ -203,30 +211,30 @@ impl HostState {
|
|
|
203
211
|
///
|
|
204
212
|
/// `max_memory` is the byte cap on per-invocation growth (`None` disables
|
|
205
213
|
/// the cap). `baseline` is the linear-memory size captured at invocation
|
|
206
|
-
/// entry by
|
|
214
|
+
/// entry by `MemoryLimiter::activate`; the limiter charges only the
|
|
207
215
|
/// `memory.grow` delta past `baseline` against `max_memory`, so the
|
|
208
216
|
/// mruby image's initial allocation and any high-water mark left by
|
|
209
217
|
/// prior invocations on the same Sandbox do not consume the budget.
|
|
210
218
|
/// `cap_active` gates whether the cap is enforced — wasmtime's
|
|
211
219
|
/// `ResourceLimiter` also fires for the module's declared initial
|
|
212
220
|
/// allocation at instantiation time, but the cap stays dormant until
|
|
213
|
-
///
|
|
214
|
-
/// `
|
|
221
|
+
/// `MemoryLimiter::activate` flips the flag for one
|
|
222
|
+
/// `Runtime::eval` / `Runtime::run` call. When `cap_active` is
|
|
215
223
|
/// `false`, the limiter always allows growth.
|
|
216
224
|
///
|
|
217
225
|
/// When `memory.grow` would push the per-invocation delta past
|
|
218
|
-
/// `max_memory`, the limiter returns
|
|
226
|
+
/// `max_memory`, the limiter returns `MemoryLimitTrap` from
|
|
219
227
|
/// `memory_growing`; wasmtime turns that into the trap surfaced to the
|
|
220
228
|
/// host as a guest invocation failure.
|
|
221
229
|
#[derive(Debug, Clone, Copy)]
|
|
222
|
-
pub(super) struct
|
|
230
|
+
pub(super) struct MemoryLimiter {
|
|
223
231
|
max_memory: Option<usize>,
|
|
224
232
|
baseline: usize,
|
|
225
233
|
cap_active: bool,
|
|
226
234
|
peak: usize,
|
|
227
235
|
}
|
|
228
236
|
|
|
229
|
-
impl
|
|
237
|
+
impl MemoryLimiter {
|
|
230
238
|
fn new(max_memory: Option<usize>) -> Self {
|
|
231
239
|
Self {
|
|
232
240
|
max_memory,
|
|
@@ -238,12 +246,12 @@ impl KobakoLimiter {
|
|
|
238
246
|
|
|
239
247
|
/// Arm the cap so subsequent `memory.grow` calls are charged
|
|
240
248
|
/// against `max_memory` starting from `baseline` bytes. Called via
|
|
241
|
-
///
|
|
249
|
+
/// `Invocation::arm_memory_cap` at the top of every invocation;
|
|
242
250
|
/// the cap is dormant by default — the module's declared initial
|
|
243
251
|
/// memory is allocated during `Linker::instantiate` and the
|
|
244
252
|
/// per-invocation budget excludes anything that existed before
|
|
245
253
|
/// arming (docs/behavior.md B-01 Notes, E-20). Also clears the
|
|
246
|
-
/// per-invocation
|
|
254
|
+
/// per-invocation `MemoryLimiter::peak` high-water so the
|
|
247
255
|
/// docs/behavior.md B-35 `memory_peak` accounting restarts from
|
|
248
256
|
/// zero for the new invocation.
|
|
249
257
|
fn activate(&mut self, baseline: usize) {
|
|
@@ -255,14 +263,14 @@ impl KobakoLimiter {
|
|
|
255
263
|
/// Disarm the cap so post-run host bookkeeping (e.g. fetching the
|
|
256
264
|
/// OUTCOME_BUFFER, which can grow guest memory transiently) is
|
|
257
265
|
/// not attributed to the user script. Paired with
|
|
258
|
-
///
|
|
266
|
+
/// `MemoryLimiter::activate`.
|
|
259
267
|
fn deactivate(&mut self) {
|
|
260
268
|
self.cap_active = false;
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
/// Return the high-water mark of the per-invocation
|
|
264
272
|
/// `memory.grow` delta past `baseline` observed since the last
|
|
265
|
-
///
|
|
273
|
+
/// `MemoryLimiter::activate`. Read after the guest export
|
|
266
274
|
/// returns to populate `Kobako::Usage#memory_peak`
|
|
267
275
|
/// (docs/behavior.md B-35). Pinned to the last accepted grow —
|
|
268
276
|
/// rejected `desired` values that trip the docs/behavior.md E-20
|
|
@@ -273,7 +281,7 @@ impl KobakoLimiter {
|
|
|
273
281
|
}
|
|
274
282
|
}
|
|
275
283
|
|
|
276
|
-
impl ResourceLimiter for
|
|
284
|
+
impl ResourceLimiter for MemoryLimiter {
|
|
277
285
|
fn memory_growing(
|
|
278
286
|
&mut self,
|
|
279
287
|
_current: usize,
|
|
@@ -305,9 +313,9 @@ impl ResourceLimiter for KobakoLimiter {
|
|
|
305
313
|
}
|
|
306
314
|
}
|
|
307
315
|
|
|
308
|
-
/// Marker error returned from
|
|
316
|
+
/// Marker error returned from `MemoryLimiter::memory_growing` on
|
|
309
317
|
/// docs/behavior.md E-20. Downcast from the wasmtime trap error to surface as
|
|
310
|
-
/// `Kobako::
|
|
318
|
+
/// `Kobako::MemoryLimitError` on the Ruby side. Callers use the
|
|
311
319
|
/// `Display` impl below — no field is read directly — so the inner
|
|
312
320
|
/// state stays private.
|
|
313
321
|
#[derive(Debug)]
|
|
@@ -318,7 +326,7 @@ pub(crate) struct MemoryLimitTrap {
|
|
|
318
326
|
|
|
319
327
|
impl MemoryLimitTrap {
|
|
320
328
|
/// Construct a trap with the given +desired+ / +limit+ pair. Used
|
|
321
|
-
/// internally by
|
|
329
|
+
/// internally by `MemoryLimiter::memory_growing` in production and
|
|
322
330
|
/// by the sibling-module +classify_trap+ unit tests to materialise
|
|
323
331
|
/// a representative error for downcast routing.
|
|
324
332
|
#[cfg(test)]
|
|
@@ -331,8 +339,8 @@ impl std::fmt::Display for MemoryLimitTrap {
|
|
|
331
339
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
332
340
|
write!(
|
|
333
341
|
f,
|
|
334
|
-
"
|
|
335
|
-
|
|
342
|
+
"memory usage exceeded memory_limit: \
|
|
343
|
+
requested={} bytes, limit={} bytes",
|
|
336
344
|
self.desired, self.limit
|
|
337
345
|
)
|
|
338
346
|
}
|
|
@@ -342,7 +350,7 @@ impl std::error::Error for MemoryLimitTrap {}
|
|
|
342
350
|
|
|
343
351
|
/// Marker error returned from the epoch-deadline callback on
|
|
344
352
|
/// docs/behavior.md E-19. Downcast from the wasmtime trap error to
|
|
345
|
-
/// surface as `Kobako::
|
|
353
|
+
/// surface as `Kobako::TimeoutError` on the Ruby side.
|
|
346
354
|
#[derive(Debug)]
|
|
347
355
|
pub(crate) struct TimeoutTrap;
|
|
348
356
|
|
|
@@ -354,20 +362,20 @@ impl std::fmt::Display for TimeoutTrap {
|
|
|
354
362
|
|
|
355
363
|
impl std::error::Error for TimeoutTrap {}
|
|
356
364
|
|
|
357
|
-
/// Interior-mutability wrapper around `wasmtime::Store<
|
|
365
|
+
/// Interior-mutability wrapper around `wasmtime::Store<Invocation>`.
|
|
358
366
|
///
|
|
359
367
|
/// Magnus requires `Send + Sync` for wrapped types. `wasmtime::Store` is not
|
|
360
368
|
/// `Sync`, so we wrap it in a `RefCell`. `RefCell` alone is sufficient
|
|
361
369
|
/// because magnus enforces single-threaded GVL access from Ruby; `Send` and
|
|
362
370
|
/// `Sync` are asserted via the unsafe impls below.
|
|
363
371
|
pub(super) struct StoreCell {
|
|
364
|
-
inner: RefCell<WtStore<
|
|
372
|
+
inner: RefCell<WtStore<Invocation>>,
|
|
365
373
|
}
|
|
366
374
|
|
|
367
375
|
impl StoreCell {
|
|
368
|
-
/// Wrap a freshly-built `wasmtime::Store<
|
|
369
|
-
/// by the magnus-wrapped `
|
|
370
|
-
pub(super) fn new(store: WtStore<
|
|
376
|
+
/// Wrap a freshly-built `wasmtime::Store<Invocation>` so it can be owned
|
|
377
|
+
/// by the magnus-wrapped `Runtime`.
|
|
378
|
+
pub(super) fn new(store: WtStore<Invocation>) -> Self {
|
|
371
379
|
Self {
|
|
372
380
|
inner: RefCell::new(store),
|
|
373
381
|
}
|
|
@@ -375,13 +383,13 @@ impl StoreCell {
|
|
|
375
383
|
|
|
376
384
|
/// Immutable borrow of the wrapped Store. Panics if a `borrow_mut()`
|
|
377
385
|
/// is currently live — matches `RefCell::borrow` semantics.
|
|
378
|
-
pub(super) fn borrow(&self) -> Ref<'_, WtStore<
|
|
386
|
+
pub(super) fn borrow(&self) -> Ref<'_, WtStore<Invocation>> {
|
|
379
387
|
self.inner.borrow()
|
|
380
388
|
}
|
|
381
389
|
|
|
382
390
|
/// Mutable borrow of the wrapped Store. Panics if any other borrow is
|
|
383
391
|
/// currently live — matches `RefCell::borrow_mut` semantics.
|
|
384
|
-
pub(super) fn borrow_mut(&self) -> RefMut<'_, WtStore<
|
|
392
|
+
pub(super) fn borrow_mut(&self) -> RefMut<'_, WtStore<Invocation>> {
|
|
385
393
|
self.inner.borrow_mut()
|
|
386
394
|
}
|
|
387
395
|
}
|
|
@@ -389,10 +397,10 @@ impl StoreCell {
|
|
|
389
397
|
// SAFETY: magnus requires `Send + Sync` on `#[magnus::wrap]` types. Both
|
|
390
398
|
// claims hold under the GVL invariant:
|
|
391
399
|
//
|
|
392
|
-
// * Send — `wasmtime::Store<
|
|
400
|
+
// * Send — `wasmtime::Store<Invocation>` is itself `Send` (verified
|
|
393
401
|
// upstream by wasmtime; see `wasmtime::Store`'s trait impls).
|
|
394
402
|
// `RefCell<T>: Send` whenever `T: Send`. The remaining stored state
|
|
395
|
-
// (`
|
|
403
|
+
// (`Invocation`) holds `Opaque<Value>` for the Ruby Server handle —
|
|
396
404
|
// `Opaque<Value>` is documented as `Send` by magnus precisely so
|
|
397
405
|
// wrapped objects can satisfy this bound.
|
|
398
406
|
//
|
|
@@ -412,24 +420,24 @@ unsafe impl Sync for StoreCell {}
|
|
|
412
420
|
|
|
413
421
|
#[cfg(test)]
|
|
414
422
|
mod tests {
|
|
415
|
-
//! Unit tests for
|
|
423
|
+
//! Unit tests for `MemoryLimiter` — the per-invocation memory
|
|
416
424
|
//! delta cap. The Ruby-facing E2E suite exercises the full path
|
|
417
425
|
//! through wasmtime; these tests pin the pure delta arithmetic so
|
|
418
426
|
//! a regression that breaks the baseline accounting (e.g. dropping
|
|
419
427
|
//! the `baseline` subtraction, or letting `activate` carry stale
|
|
420
428
|
//! state across invocations) is caught without spinning up a
|
|
421
429
|
//! Store.
|
|
422
|
-
use super::{
|
|
430
|
+
use super::{MemoryLimitTrap, MemoryLimiter};
|
|
423
431
|
use wasmtime::ResourceLimiter;
|
|
424
432
|
|
|
425
|
-
fn assert_growing(limiter: &mut
|
|
433
|
+
fn assert_growing(limiter: &mut MemoryLimiter, desired: usize) {
|
|
426
434
|
assert!(
|
|
427
435
|
limiter.memory_growing(0, desired, None).unwrap(),
|
|
428
436
|
"expected memory_growing({desired}) to allow growth"
|
|
429
437
|
);
|
|
430
438
|
}
|
|
431
439
|
|
|
432
|
-
fn assert_trapping(limiter: &mut
|
|
440
|
+
fn assert_trapping(limiter: &mut MemoryLimiter, desired: usize) {
|
|
433
441
|
let err = limiter
|
|
434
442
|
.memory_growing(0, desired, None)
|
|
435
443
|
.expect_err("expected memory_growing to trap");
|
|
@@ -441,7 +449,7 @@ mod tests {
|
|
|
441
449
|
|
|
442
450
|
#[test]
|
|
443
451
|
fn dormant_limiter_allows_any_growth() {
|
|
444
|
-
let mut limiter =
|
|
452
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
445
453
|
// Without `activate`, the cap is dormant — the module's
|
|
446
454
|
// declared initial allocation must pass through unconditionally.
|
|
447
455
|
assert_growing(&mut limiter, 100 << 20);
|
|
@@ -449,7 +457,7 @@ mod tests {
|
|
|
449
457
|
|
|
450
458
|
#[test]
|
|
451
459
|
fn delta_below_cap_passes_after_activate() {
|
|
452
|
-
let mut limiter =
|
|
460
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
453
461
|
limiter.activate(2 << 20);
|
|
454
462
|
// baseline=2 MiB, desired=2.5 MiB → delta=0.5 MiB ≤ 1 MiB cap.
|
|
455
463
|
assert_growing(&mut limiter, (2 << 20) + (1 << 19));
|
|
@@ -457,7 +465,7 @@ mod tests {
|
|
|
457
465
|
|
|
458
466
|
#[test]
|
|
459
467
|
fn delta_past_cap_traps_with_memory_limit_trap() {
|
|
460
|
-
let mut limiter =
|
|
468
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
461
469
|
limiter.activate(2 << 20);
|
|
462
470
|
// baseline=2 MiB, desired=4 MiB → delta=2 MiB > 1 MiB cap.
|
|
463
471
|
assert_trapping(&mut limiter, 4 << 20);
|
|
@@ -465,7 +473,7 @@ mod tests {
|
|
|
465
473
|
|
|
466
474
|
#[test]
|
|
467
475
|
fn activate_resets_baseline_on_each_invocation() {
|
|
468
|
-
let mut limiter =
|
|
476
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
469
477
|
limiter.activate(2 << 20);
|
|
470
478
|
assert_growing(&mut limiter, (2 << 20) + (1 << 20));
|
|
471
479
|
// Second invocation: linear memory has grown to 3 MiB. Re-arming
|
|
@@ -478,20 +486,20 @@ mod tests {
|
|
|
478
486
|
|
|
479
487
|
#[test]
|
|
480
488
|
fn disabled_cap_ignores_delta_size() {
|
|
481
|
-
let mut limiter =
|
|
489
|
+
let mut limiter = MemoryLimiter::new(None);
|
|
482
490
|
limiter.activate(0);
|
|
483
491
|
assert_growing(&mut limiter, 100 << 20);
|
|
484
492
|
}
|
|
485
493
|
|
|
486
494
|
#[test]
|
|
487
495
|
fn peak_starts_at_zero_before_any_grow() {
|
|
488
|
-
let limiter =
|
|
496
|
+
let limiter = MemoryLimiter::new(Some(1 << 20));
|
|
489
497
|
assert_eq!(limiter.peak(), 0);
|
|
490
498
|
}
|
|
491
499
|
|
|
492
500
|
#[test]
|
|
493
501
|
fn peak_tracks_high_water_of_delta_past_baseline() {
|
|
494
|
-
let mut limiter =
|
|
502
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
495
503
|
limiter.activate(2 << 20);
|
|
496
504
|
assert_growing(&mut limiter, (2 << 20) + (1 << 18)); // delta=256 KiB
|
|
497
505
|
assert_growing(&mut limiter, (2 << 20) + (1 << 19)); // delta=512 KiB (new peak)
|
|
@@ -501,7 +509,7 @@ mod tests {
|
|
|
501
509
|
|
|
502
510
|
#[test]
|
|
503
511
|
fn trap_does_not_update_peak() {
|
|
504
|
-
let mut limiter =
|
|
512
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
505
513
|
limiter.activate(2 << 20);
|
|
506
514
|
assert_growing(&mut limiter, (2 << 20) + (1 << 19)); // delta=512 KiB
|
|
507
515
|
assert_trapping(&mut limiter, (2 << 20) + (2 << 20)); // would be 2 MiB > 1 MiB cap
|
|
@@ -511,7 +519,7 @@ mod tests {
|
|
|
511
519
|
|
|
512
520
|
#[test]
|
|
513
521
|
fn activate_resets_peak_for_new_invocation() {
|
|
514
|
-
let mut limiter =
|
|
522
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
515
523
|
limiter.activate(2 << 20);
|
|
516
524
|
assert_growing(&mut limiter, (2 << 20) + (1 << 19));
|
|
517
525
|
assert_eq!(limiter.peak(), 1 << 19);
|
|
@@ -521,7 +529,7 @@ mod tests {
|
|
|
521
529
|
|
|
522
530
|
#[test]
|
|
523
531
|
fn disabled_cap_still_tracks_peak() {
|
|
524
|
-
let mut limiter =
|
|
532
|
+
let mut limiter = MemoryLimiter::new(None);
|
|
525
533
|
limiter.activate(1 << 20);
|
|
526
534
|
assert_growing(&mut limiter, (1 << 20) + (4 << 20));
|
|
527
535
|
assert_eq!(limiter.peak(), 4 << 20);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//! Trap classification for the run path.
|
|
2
|
+
//!
|
|
3
|
+
//! Maps a `wasmtime` run error to the right top-level `Kobako::*` Ruby
|
|
4
|
+
//! exception (`TimeoutError` / `MemoryLimitError` / `TrapError`), and
|
|
5
|
+
//! hosts the epoch-deadline callback that raises the wall-clock
|
|
6
|
+
//! `TimeoutTrap`. The classification is a pure function over the error's
|
|
7
|
+
//! downcast chain so it can be exercised from `cargo test` without the
|
|
8
|
+
//! magnus surface; the trap marker types themselves live in
|
|
9
|
+
//! `super::invocation` (where the limiter / callback construct them).
|
|
10
|
+
|
|
11
|
+
use std::time::Instant;
|
|
12
|
+
|
|
13
|
+
use magnus::{Error as MagnusError, Ruby};
|
|
14
|
+
use wasmtime::{StoreContextMut, UpdateDeadline};
|
|
15
|
+
|
|
16
|
+
use super::invocation::{Invocation, MemoryLimitTrap, TimeoutTrap};
|
|
17
|
+
use super::{memory_limit_err, setup_err, timeout_err, trap_err};
|
|
18
|
+
|
|
19
|
+
/// Epoch-deadline callback installed on every Store. Read the per-run
|
|
20
|
+
/// wall-clock deadline from `Invocation` (docs/behavior.md B-01) and trap with
|
|
21
|
+
/// `TimeoutTrap` once the deadline has passed; otherwise extend the
|
|
22
|
+
/// 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
|
+
/// `Runtime::eval` / `Runtime::run` flow because
|
|
25
|
+
/// `set_epoch_deadline(u64::MAX)` is used; returning a long extension
|
|
26
|
+
/// keeps the callback inert as a defence in depth.
|
|
27
|
+
pub(super) fn epoch_deadline_callback(
|
|
28
|
+
ctx: StoreContextMut<'_, Invocation>,
|
|
29
|
+
) -> wasmtime::Result<UpdateDeadline> {
|
|
30
|
+
match ctx.data().deadline() {
|
|
31
|
+
Some(deadline) if Instant::now() >= deadline => Err(wasmtime::Error::new(TimeoutTrap)),
|
|
32
|
+
Some(_) => Ok(UpdateDeadline::Continue(1)),
|
|
33
|
+
None => Ok(UpdateDeadline::Continue(u64::MAX / 2)),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Configured-cap path classification for a wasmtime error. The
|
|
38
|
+
/// downcast logic stays in a pure helper so the
|
|
39
|
+
/// `Kobako::TimeoutError` / `MemoryLimitError` /
|
|
40
|
+
/// `Kobako::TrapError` mapping can be exercised from `cargo test`
|
|
41
|
+
/// without the magnus surface.
|
|
42
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
43
|
+
enum TrapClass {
|
|
44
|
+
/// docs/behavior.md E-19 wall-clock cap path.
|
|
45
|
+
Timeout,
|
|
46
|
+
/// docs/behavior.md E-20 linear-memory cap path.
|
|
47
|
+
MemoryLimit,
|
|
48
|
+
/// Any other wasmtime error — surfaces as the base
|
|
49
|
+
/// `Kobako::TrapError`.
|
|
50
|
+
Other,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Inspect a wasmtime error to decide which top-level `Kobako::*` trap
|
|
54
|
+
/// class it should map to. Pure function — operates on the error's
|
|
55
|
+
/// downcast chain only, no magnus / Ruby state required.
|
|
56
|
+
fn classify_trap(err: &wasmtime::Error) -> TrapClass {
|
|
57
|
+
if err.downcast_ref::<TimeoutTrap>().is_some() {
|
|
58
|
+
TrapClass::Timeout
|
|
59
|
+
} else if err.downcast_ref::<MemoryLimitTrap>().is_some() {
|
|
60
|
+
TrapClass::MemoryLimit
|
|
61
|
+
} else {
|
|
62
|
+
TrapClass::Other
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Map a wasmtime call error to the right top-level `Kobako::*` Ruby
|
|
67
|
+
/// exception class. The ABI export symbol (`__kobako_eval` /
|
|
68
|
+
/// `__kobako_run`) is deliberately omitted from the message — the
|
|
69
|
+
/// Sandbox layer attaches the user-facing verb (`Sandbox#eval` /
|
|
70
|
+
/// `Sandbox#run`) so the message reads in caller vocabulary rather
|
|
71
|
+
/// than ABI vocabulary.
|
|
72
|
+
///
|
|
73
|
+
/// For the configured-cap paths (`TrapClass::Timeout` /
|
|
74
|
+
/// `TrapClass::MemoryLimit`) the trap's own `std::fmt::Display`
|
|
75
|
+
/// carries the user-facing reason (`"wall-clock deadline exceeded"`,
|
|
76
|
+
/// `"linear memory growth exceeded memory_limit: ..."`). The wasmtime
|
|
77
|
+
/// outer wrapper at `format!("{}", err)` would otherwise surface only
|
|
78
|
+
/// the `"error while executing at wasm backtrace: ..."` framing, which
|
|
79
|
+
/// is operator noise on a cap trap. For `TrapClass::Other` the
|
|
80
|
+
/// wasmtime wrapper IS the diagnostic (real script trap) so it stays.
|
|
81
|
+
pub(super) fn call_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
|
|
82
|
+
match classify_trap(&err) {
|
|
83
|
+
TrapClass::Timeout => {
|
|
84
|
+
let msg = err
|
|
85
|
+
.downcast_ref::<TimeoutTrap>()
|
|
86
|
+
.map(|t| t.to_string())
|
|
87
|
+
.unwrap_or_else(|| format!("{}", err));
|
|
88
|
+
timeout_err(ruby, msg)
|
|
89
|
+
}
|
|
90
|
+
TrapClass::MemoryLimit => {
|
|
91
|
+
let msg = err
|
|
92
|
+
.downcast_ref::<MemoryLimitTrap>()
|
|
93
|
+
.map(|t| t.to_string())
|
|
94
|
+
.unwrap_or_else(|| format!("{}", err));
|
|
95
|
+
memory_limit_err(ruby, msg)
|
|
96
|
+
}
|
|
97
|
+
TrapClass::Other => trap_err(ruby, format!("{}", err)),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Map an instantiation error to `Kobako::SetupError`. Instantiation runs
|
|
102
|
+
/// during `from_path` construction, before any invocation — docs/behavior.md
|
|
103
|
+
/// E-41 classifies every such failure as a construction setup fault, not a
|
|
104
|
+
/// per-invocation cap outcome. The memory cap is dormant during
|
|
105
|
+
/// instantiation (see `Invocation::arm_memory_cap` /
|
|
106
|
+
/// `Invocation::disarm_memory_cap`) and the epoch deadline is not yet
|
|
107
|
+
/// armed, so the `call_err` trap-class split does not apply here.
|
|
108
|
+
pub(super) fn instantiate_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
|
|
109
|
+
setup_err(ruby, format!("instantiate: {}", err))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[cfg(test)]
|
|
113
|
+
mod tests {
|
|
114
|
+
use super::{classify_trap, TrapClass};
|
|
115
|
+
use crate::runtime::invocation::{MemoryLimitTrap, TimeoutTrap};
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn classify_trap_routes_timeout_trap_to_timeout() {
|
|
119
|
+
let err = wasmtime::Error::new(TimeoutTrap);
|
|
120
|
+
assert_eq!(classify_trap(&err), TrapClass::Timeout);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn classify_trap_routes_memory_limit_trap_to_memory_limit() {
|
|
125
|
+
let err = wasmtime::Error::new(MemoryLimitTrap::new(1 << 20, 1 << 19));
|
|
126
|
+
assert_eq!(classify_trap(&err), TrapClass::MemoryLimit);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn classify_trap_falls_back_to_other_for_unknown_errors() {
|
|
131
|
+
let err = wasmtime::Error::msg("some other wasmtime fault");
|
|
132
|
+
assert_eq!(classify_trap(&err), TrapClass::Other);
|
|
133
|
+
}
|
|
134
|
+
}
|