kobako 0.2.1 → 0.4.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/Cargo.lock +1 -1
- data/README.md +205 -59
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/wasm/cache.rs +15 -7
- data/ext/kobako/src/wasm/dispatch.rs +88 -36
- data/ext/kobako/src/wasm/host_state.rs +298 -55
- data/ext/kobako/src/wasm/instance.rs +477 -160
- data/ext/kobako/src/wasm.rs +20 -5
- data/lib/kobako/capture.rb +12 -10
- data/lib/kobako/codec/decoder.rb +3 -4
- data/lib/kobako/codec/encoder.rb +1 -1
- data/lib/kobako/codec/error.rb +3 -2
- data/lib/kobako/codec/factory.rb +24 -17
- data/lib/kobako/codec/utils.rb +105 -12
- data/lib/kobako/codec.rb +2 -1
- data/lib/kobako/errors.rb +22 -10
- data/lib/kobako/handle.rb +62 -0
- data/lib/kobako/handle_table.rb +119 -0
- data/lib/kobako/invocation.rb +143 -0
- data/lib/kobako/outcome/panic.rb +2 -2
- data/lib/kobako/outcome.rb +61 -24
- data/lib/kobako/rpc/dispatcher.rb +30 -28
- data/lib/kobako/rpc/envelope.rb +10 -10
- data/lib/kobako/rpc/fault.rb +4 -3
- data/lib/kobako/rpc/namespace.rb +3 -3
- data/lib/kobako/rpc/server.rb +23 -33
- data/lib/kobako/rpc/wire_error.rb +23 -0
- data/lib/kobako/sandbox.rb +211 -136
- data/lib/kobako/sandbox_options.rb +73 -0
- data/lib/kobako/snippet/binary.rb +30 -0
- data/lib/kobako/snippet/source.rb +28 -0
- data/lib/kobako/snippet/table.rb +174 -0
- data/lib/kobako/snippet.rb +20 -0
- data/lib/kobako/usage.rb +41 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/codec/factory.rbs +1 -1
- data/sig/kobako/codec/utils.rbs +10 -0
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/handle.rbs +19 -0
- data/sig/kobako/handle_table.rbs +23 -0
- data/sig/kobako/invocation.rbs +25 -0
- data/sig/kobako/outcome.rbs +1 -1
- data/sig/kobako/rpc/dispatcher.rbs +7 -7
- data/sig/kobako/rpc/envelope.rbs +3 -3
- data/sig/kobako/rpc/server.rbs +1 -7
- data/sig/kobako/rpc/wire_error.rbs +6 -0
- data/sig/kobako/sandbox.rbs +22 -17
- data/sig/kobako/sandbox_options.rbs +32 -0
- data/sig/kobako/snippet/binary.rbs +12 -0
- data/sig/kobako/snippet/source.rbs +13 -0
- data/sig/kobako/snippet/table.rbs +36 -0
- data/sig/kobako/snippet.rbs +4 -0
- data/sig/kobako/usage.rbs +11 -0
- data/sig/kobako/wasm.rbs +5 -1
- metadata +21 -5
- data/lib/kobako/rpc/handle.rb +0 -38
- data/lib/kobako/rpc/handle_table.rb +0 -107
- data/sig/kobako/rpc/handle.rbs +0 -19
- data/sig/kobako/rpc/handle_table.rbs +0 -25
|
@@ -3,32 +3,37 @@
|
|
|
3
3
|
//! Constructed via [`Instance::from_path`]; the wasmtime [`Engine`] and
|
|
4
4
|
//! compiled [`Module`] are owned by the [`super::cache`] singletons and
|
|
5
5
|
//! never surface to Ruby. The instance wraps a [`StoreCell`] (interior-
|
|
6
|
-
//! mutability around `wasmtime::Store<HostState>`) plus
|
|
7
|
-
//! [`TypedFunc`] handles for the
|
|
8
|
-
//! run path.
|
|
6
|
+
//! mutability around `wasmtime::Store<HostState>`) plus three cached
|
|
7
|
+
//! [`TypedFunc`] handles for the docs/wire-codec.md ABI exports used by
|
|
8
|
+
//! the host-driven run path.
|
|
9
9
|
//!
|
|
10
10
|
//! The Ruby surface intentionally exposes intent, not the underlying ABI
|
|
11
|
-
//! (SPEC.md "Code Organization"). The
|
|
12
|
-
//!
|
|
13
|
-
//!
|
|
14
|
-
//!
|
|
15
|
-
//!
|
|
11
|
+
//! (SPEC.md "Code Organization"). The length-prefixed stdin frame
|
|
12
|
+
//! protocol (three frames for `#eval`: preamble + source + snippets;
|
|
13
|
+
//! two for `#run`: preamble + snippets), packed-u64 outcome encoding,
|
|
14
|
+
//! and the `__kobako_eval` / `__kobako_run` / `__kobako_alloc` /
|
|
15
|
+
//! `__kobako_take_outcome` exports are all wrapped inside
|
|
16
|
+
//! [`Instance::eval`], [`Instance::run`], and [`Instance::outcome`];
|
|
17
|
+
//! Ruby callers see only `#eval(preamble, source, snippets)`,
|
|
18
|
+
//! `#run(preamble, snippets, envelope)`, `#stdout`, `#stderr`,
|
|
19
|
+
//! `#outcome!`, and `#server=`.
|
|
16
20
|
//!
|
|
17
|
-
//! WASI stdout/stderr capture (
|
|
18
|
-
//! route guest fd 1 and fd 2 into per-run [`MemoryOutputPipe`]
|
|
19
|
-
//! rebuilt at the start of every [`Instance::
|
|
20
|
-
//! is enforced directly on the
|
|
21
|
-
//!
|
|
22
|
-
//! that exceeded the cap, and
|
|
23
|
-
//! bytes back to `cap` before
|
|
24
|
-
//!
|
|
25
|
-
//! `
|
|
21
|
+
//! WASI stdout/stderr capture (docs/behavior.md B-04): wasmtime-wasi p1
|
|
22
|
+
//! bindings route guest fd 1 and fd 2 into per-run [`MemoryOutputPipe`]
|
|
23
|
+
//! instances rebuilt at the start of every [`Instance::eval`] /
|
|
24
|
+
//! [`Instance::run`]. The per-channel cap is enforced directly on the
|
|
25
|
+
//! pipe — the pipe is sized at `cap + 1` so a guest that writes exactly
|
|
26
|
+
//! `cap` bytes is distinguishable from one that exceeded the cap, and
|
|
27
|
+
//! `#stdout` / `#stderr` slice the captured bytes back to `cap` before
|
|
28
|
+
//! returning them paired with a truncation flag. Uncapped channels
|
|
29
|
+
//! (`None`) build the pipe at `usize::MAX`; `memory_limit` provides
|
|
30
|
+
//! the real upper bound in that case.
|
|
26
31
|
//!
|
|
27
|
-
//! Per-run cap enforcement (
|
|
28
|
-
//! installs an epoch-deadline callback for wall-clock timeout and
|
|
29
|
-
//! [`ResourceLimiter`] for the linear-memory cap. Wasmtime turns
|
|
30
|
-
//! limiter / callback errors into traps;
|
|
31
|
-
//! trap source to surface as `Kobako::Wasm::TimeoutError` or
|
|
32
|
+
//! Per-run cap enforcement (docs/behavior.md B-01, E-19, E-20): every
|
|
33
|
+
//! Store installs an epoch-deadline callback for wall-clock timeout and
|
|
34
|
+
//! a [`ResourceLimiter`] for the linear-memory cap. Wasmtime turns
|
|
35
|
+
//! limiter / callback errors into traps; the run-path methods downcast
|
|
36
|
+
//! the trap source to surface as `Kobako::Wasm::TimeoutError` or
|
|
32
37
|
//! `Kobako::Wasm::MemoryLimitError` so the `Sandbox` layer can map them
|
|
33
38
|
//! to the named `Kobako::TrapError` subclasses.
|
|
34
39
|
//!
|
|
@@ -53,7 +58,7 @@ use wasmtime_wasi::WasiCtxBuilder;
|
|
|
53
58
|
use super::cache::{cached_module, shared_engine};
|
|
54
59
|
use super::dispatch;
|
|
55
60
|
use super::host_state::{HostState, MemoryLimitTrap, StoreCell, TimeoutTrap};
|
|
56
|
-
use super::{memory_limit_err, timeout_err, wasm_err};
|
|
61
|
+
use super::{memory_limit_err, rstring_to_vec, timeout_err, wasm_err};
|
|
57
62
|
|
|
58
63
|
#[magnus::wrap(class = "Kobako::Wasm::Instance", free_immediately, size)]
|
|
59
64
|
pub(crate) struct Instance {
|
|
@@ -66,14 +71,16 @@ pub(crate) struct Instance {
|
|
|
66
71
|
//
|
|
67
72
|
// `__kobako_alloc` is NOT cached here — only `dispatch.rs` calls it,
|
|
68
73
|
// and it does so through `Caller::get_export` on the wasmtime side.
|
|
69
|
-
|
|
74
|
+
eval: Option<TypedFunc<(), ()>>,
|
|
75
|
+
run: Option<TypedFunc<(i32, i32), ()>>,
|
|
70
76
|
take_outcome: Option<TypedFunc<(), u64>>,
|
|
71
|
-
// Wall-clock cap for one guest `#run` (
|
|
77
|
+
// Wall-clock cap for one guest `#run` (docs/behavior.md B-01); `None` disables
|
|
72
78
|
// the cap. Translated into an `Instant`-based deadline stamped into
|
|
73
|
-
// [`HostState`] at the top of every `Instance::
|
|
79
|
+
// [`HostState`] at the top of every `Instance::eval`.
|
|
74
80
|
timeout: Option<Duration>,
|
|
75
|
-
// Per-channel byte caps for guest stdout / stderr capture
|
|
76
|
-
// B-01 / B-04). `None` disables the cap on that
|
|
81
|
+
// Per-channel byte caps for guest stdout / stderr capture
|
|
82
|
+
// (docs/behavior.md B-01 / B-04). `None` disables the cap on that
|
|
83
|
+
// channel. Read by
|
|
77
84
|
// [`Instance::refresh_wasi`] to size the MemoryOutputPipe and by
|
|
78
85
|
// [`Instance::stdout`] / [`Instance::stderr`] to compute the
|
|
79
86
|
// truncation flag. See the module-level note above for the `cap + 1`
|
|
@@ -91,10 +98,10 @@ impl Instance {
|
|
|
91
98
|
/// constructor for `Kobako::Wasm::Instance` — Engine and Module are
|
|
92
99
|
/// never visible to Ruby.
|
|
93
100
|
///
|
|
94
|
-
/// `timeout_seconds` is the
|
|
101
|
+
/// `timeout_seconds` is the docs/behavior.md B-01 wall-clock cap in seconds
|
|
95
102
|
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
96
103
|
/// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
|
|
97
|
-
/// are the per-channel output caps (
|
|
104
|
+
/// are the per-channel output caps (docs/behavior.md B-01 / B-04; `None`
|
|
98
105
|
/// disables). All four are validated by the caller
|
|
99
106
|
/// (`Kobako::Sandbox`); this method only refuses non-finite or
|
|
100
107
|
/// non-positive timeouts as a defence in depth.
|
|
@@ -112,7 +119,7 @@ impl Instance {
|
|
|
112
119
|
Some(secs) => {
|
|
113
120
|
return Err(wasm_err(
|
|
114
121
|
&ruby,
|
|
115
|
-
format!("
|
|
122
|
+
format!("timeout must be > 0 and finite, got {secs} seconds"),
|
|
116
123
|
));
|
|
117
124
|
}
|
|
118
125
|
};
|
|
@@ -151,13 +158,20 @@ impl Instance {
|
|
|
151
158
|
|
|
152
159
|
// Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
|
|
153
160
|
// to the MemoryOutputPipes set up before each run via
|
|
154
|
-
// `Instance::
|
|
161
|
+
// `Instance::eval`. The closure pulls a `&mut WasiP1Ctx` out of
|
|
155
162
|
// HostState; the panic semantics live inside `HostState::wasi_mut`
|
|
156
163
|
// so the wiring stays honest about its precondition.
|
|
157
|
-
p1::add_to_linker_sync(&mut linker, |state: &mut HostState| state.wasi_mut())
|
|
158
|
-
|
|
164
|
+
p1::add_to_linker_sync(&mut linker, |state: &mut HostState| state.wasi_mut()).map_err(
|
|
165
|
+
|e| {
|
|
166
|
+
wasm_err(
|
|
167
|
+
&ruby,
|
|
168
|
+
format!("failed to wire WASI runtime into Sandbox: {}", e),
|
|
169
|
+
)
|
|
170
|
+
},
|
|
171
|
+
)?;
|
|
159
172
|
|
|
160
|
-
// `__kobako_dispatch` host import. Signature per
|
|
173
|
+
// `__kobako_dispatch` host import. Signature per docs/wire-codec.md
|
|
174
|
+
// § ABI Signatures:
|
|
161
175
|
// (req_ptr: i32, req_len: i32) -> i64
|
|
162
176
|
// Decodes the Request bytes, dispatches via the Ruby-side
|
|
163
177
|
// `Kobako::RPC::Server` (set per-run via `set_server`), allocates a
|
|
@@ -173,7 +187,12 @@ impl Instance {
|
|
|
173
187
|
dispatch::handle(&mut caller, req_ptr, req_len)
|
|
174
188
|
},
|
|
175
189
|
)
|
|
176
|
-
.map_err(|e|
|
|
190
|
+
.map_err(|e| {
|
|
191
|
+
wasm_err(
|
|
192
|
+
&ruby,
|
|
193
|
+
format!("failed to register host RPC dispatch import: {}", e),
|
|
194
|
+
)
|
|
195
|
+
})?;
|
|
177
196
|
|
|
178
197
|
let instance = {
|
|
179
198
|
let mut store_ref = store_cell.borrow_mut();
|
|
@@ -186,22 +205,28 @@ impl Instance {
|
|
|
186
205
|
// (test fixture is a bare module); the host enforces presence at
|
|
187
206
|
// invocation time by raising a Ruby `Kobako::Wasm::Error` when the
|
|
188
207
|
// cached Option is None. Only the SPEC ABI `() -> ()` shape is
|
|
189
|
-
// accepted for `__kobako_run
|
|
190
|
-
|
|
208
|
+
// accepted for `__kobako_eval`; `__kobako_run` takes
|
|
209
|
+
// `(env_ptr, env_len) -> ()` per docs/wire-codec.md § ABI
|
|
210
|
+
// Signatures.
|
|
211
|
+
let (eval, run, take_outcome) = {
|
|
191
212
|
let mut store_ref = store_cell.borrow_mut();
|
|
192
213
|
let mut ctx = store_ref.as_context_mut();
|
|
214
|
+
let eval = instance
|
|
215
|
+
.get_typed_func::<(), ()>(&mut ctx, "__kobako_eval")
|
|
216
|
+
.ok();
|
|
193
217
|
let run = instance
|
|
194
|
-
.get_typed_func::<(), ()>(&mut ctx, "__kobako_run")
|
|
218
|
+
.get_typed_func::<(i32, i32), ()>(&mut ctx, "__kobako_run")
|
|
195
219
|
.ok();
|
|
196
220
|
let take_outcome = instance
|
|
197
221
|
.get_typed_func::<(), u64>(&mut ctx, "__kobako_take_outcome")
|
|
198
222
|
.ok();
|
|
199
|
-
(run, take_outcome)
|
|
223
|
+
(eval, run, take_outcome)
|
|
200
224
|
};
|
|
201
225
|
|
|
202
226
|
Ok(Self {
|
|
203
227
|
inner: instance,
|
|
204
228
|
store: store_cell,
|
|
229
|
+
eval,
|
|
205
230
|
run,
|
|
206
231
|
take_outcome,
|
|
207
232
|
timeout,
|
|
@@ -227,25 +252,63 @@ impl Instance {
|
|
|
227
252
|
// taxonomy.
|
|
228
253
|
// -----------------------------------------------------------------
|
|
229
254
|
|
|
230
|
-
/// Execute one guest
|
|
255
|
+
/// Execute one guest invocation (`__kobako_eval` — one-shot source).
|
|
231
256
|
///
|
|
232
257
|
/// Rebuilds the WASI context with fresh stdin / stdout / stderr pipes
|
|
233
|
-
/// (the
|
|
234
|
-
///
|
|
235
|
-
/// caps (
|
|
236
|
-
///
|
|
237
|
-
///
|
|
238
|
-
|
|
258
|
+
/// (the three-frame stdin protocol carries +preamble+, +source+, then
|
|
259
|
+
/// +snippets+ — docs/wire-codec.md § Invocation channels), then
|
|
260
|
+
/// invokes `__kobako_eval`. Per-invocation caps (docs/behavior.md
|
|
261
|
+
/// B-01) are primed here: the wall-clock deadline is stamped into
|
|
262
|
+
/// [`HostState`] and the epoch deadline is set to fire at the next
|
|
263
|
+
/// ticker tick; the memory-cap limiter is already wired.
|
|
264
|
+
pub(crate) fn eval(
|
|
265
|
+
&self,
|
|
266
|
+
preamble: RString,
|
|
267
|
+
source: RString,
|
|
268
|
+
snippets: RString,
|
|
269
|
+
) -> Result<(), MagnusError> {
|
|
270
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
271
|
+
let eval = require_export(&ruby, self.eval.as_ref(), "__kobako_eval")?;
|
|
272
|
+
self.refresh_wasi(&[
|
|
273
|
+
rstring_to_vec(preamble),
|
|
274
|
+
rstring_to_vec(source),
|
|
275
|
+
rstring_to_vec(snippets),
|
|
276
|
+
]);
|
|
277
|
+
self.prime_caps();
|
|
278
|
+
let result = {
|
|
279
|
+
let mut store_ref = self.store.borrow_mut();
|
|
280
|
+
eval.call(store_ref.as_context_mut(), ())
|
|
281
|
+
};
|
|
282
|
+
self.disarm_caps();
|
|
283
|
+
result.map_err(|e| call_err(&ruby, e))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// Execute one entrypoint dispatch (`__kobako_run`).
|
|
287
|
+
///
|
|
288
|
+
/// Rebuilds the WASI context with the two-frame stdin protocol
|
|
289
|
+
/// (preamble + snippets; no user source frame — docs/wire-codec.md
|
|
290
|
+
/// § Invocation channels), copies +envelope+ bytes into guest linear
|
|
291
|
+
/// memory via `__kobako_alloc`, and calls `__kobako_run(env_ptr,
|
|
292
|
+
/// env_len)`. Per-invocation cap semantics match [`Instance::eval`].
|
|
293
|
+
/// Returns +Kobako::Wasm::Error+ ("alloc returned 0") when guest
|
|
294
|
+
/// allocation fails (docs/behavior.md E-31).
|
|
295
|
+
pub(crate) fn run(
|
|
296
|
+
&self,
|
|
297
|
+
preamble: RString,
|
|
298
|
+
snippets: RString,
|
|
299
|
+
envelope: RString,
|
|
300
|
+
) -> Result<(), MagnusError> {
|
|
239
301
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
240
|
-
let run = self
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
.ok_or_else(|| wasm_err(&ruby, "guest does not export __kobako_run"))?;
|
|
244
|
-
self.refresh_wasi(preamble, source)?;
|
|
302
|
+
let run = require_export(&ruby, self.run.as_ref(), "__kobako_run")?;
|
|
303
|
+
self.refresh_wasi(&[rstring_to_vec(preamble), rstring_to_vec(snippets)]);
|
|
304
|
+
let (env_ptr, env_len) = self.write_envelope(&ruby, envelope)?;
|
|
245
305
|
self.prime_caps();
|
|
246
|
-
let result =
|
|
306
|
+
let result = {
|
|
307
|
+
let mut store_ref = self.store.borrow_mut();
|
|
308
|
+
run.call(store_ref.as_context_mut(), (env_ptr, env_len))
|
|
309
|
+
};
|
|
247
310
|
self.disarm_caps();
|
|
248
|
-
result.map_err(|e|
|
|
311
|
+
result.map_err(|e| call_err(&ruby, e))
|
|
249
312
|
}
|
|
250
313
|
|
|
251
314
|
/// Return the stdout capture from the most recent run as a Ruby
|
|
@@ -282,15 +345,61 @@ impl Instance {
|
|
|
282
345
|
Ok(ruby.str_from_slice(&bytes))
|
|
283
346
|
}
|
|
284
347
|
|
|
348
|
+
/// Return the docs/behavior.md B-35 per-last-invocation usage as a
|
|
349
|
+
/// Ruby 2-tuple `[wall_time, memory_peak]`. The element order
|
|
350
|
+
/// matches the `Kobako::Usage` field order declared in
|
|
351
|
+
/// `lib/kobako/usage.rb`; reorder both sides together if the field
|
|
352
|
+
/// list ever grows.
|
|
353
|
+
///
|
|
354
|
+
/// * `wall_time` (Float seconds) — the wall-clock duration the
|
|
355
|
+
/// most recent invocation spent inside the guest export call.
|
|
356
|
+
/// Bracket opens in [`Instance::prime_caps`] and closes in
|
|
357
|
+
/// [`Instance::disarm_caps`], so the value mirrors the
|
|
358
|
+
/// `timeout` deadline accounting and excludes everything that
|
|
359
|
+
/// runs after the guest export returns — the post-export
|
|
360
|
+
/// `OUTCOME_BUFFER` fetch and decode, plus stdout / stderr
|
|
361
|
+
/// capture readout. `0.0` before the first invocation.
|
|
362
|
+
/// * `memory_peak` (Integer bytes) — the high-water mark of the
|
|
363
|
+
/// per-invocation `memory.grow` delta past the linear-memory
|
|
364
|
+
/// size captured at invocation entry. `0` before the first
|
|
365
|
+
/// invocation.
|
|
366
|
+
///
|
|
367
|
+
/// Packing both readers into one ext call mirrors the
|
|
368
|
+
/// [`Instance::stdout`] / [`Instance::stderr`] pattern: one
|
|
369
|
+
/// `store.borrow()` per readout and a single magnus binding to
|
|
370
|
+
/// extend when B-35's field list grows past two.
|
|
371
|
+
pub(crate) fn usage(&self) -> Result<RArray, MagnusError> {
|
|
372
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
373
|
+
let state = self.store.borrow();
|
|
374
|
+
let data = state.data();
|
|
375
|
+
let arr = ruby.ary_new_capa(2);
|
|
376
|
+
arr.push(data.wall_time().as_secs_f64())?;
|
|
377
|
+
arr.push(data.memory_peak())?;
|
|
378
|
+
Ok(arr)
|
|
379
|
+
}
|
|
380
|
+
|
|
285
381
|
// -----------------------------------------------------------------
|
|
286
382
|
// Private helpers.
|
|
287
383
|
// -----------------------------------------------------------------
|
|
288
384
|
|
|
289
|
-
/// Stamp the per-
|
|
290
|
-
/// the wasmtime epoch deadline so the next ticker tick
|
|
291
|
-
/// epoch-deadline callback. When `timeout` is disabled,
|
|
292
|
-
/// is set far enough in the future that the callback
|
|
293
|
-
/// never fires.
|
|
385
|
+
/// Stamp the per-invocation wall-clock deadline into [`HostState`]
|
|
386
|
+
/// and prime the wasmtime epoch deadline so the next ticker tick
|
|
387
|
+
/// wakes the epoch-deadline callback. When `timeout` is disabled,
|
|
388
|
+
/// the deadline is set far enough in the future that the callback
|
|
389
|
+
/// effectively never fires.
|
|
390
|
+
///
|
|
391
|
+
/// Also captures the current linear-memory size as the baseline
|
|
392
|
+
/// for the docs/behavior.md E-20 per-invocation memory delta cap.
|
|
393
|
+
/// The mruby image's declared initial allocation and the high-water
|
|
394
|
+
/// mark left by prior invocations on the same Sandbox are folded
|
|
395
|
+
/// into the baseline rather than the budget — only `memory.grow`
|
|
396
|
+
/// past +baseline+ counts against `memory_limit`.
|
|
397
|
+
///
|
|
398
|
+
/// Also stamps the wall-clock entry instant for the
|
|
399
|
+
/// docs/behavior.md B-35 `wall_time` measurement. The bracket
|
|
400
|
+
/// closes in [`Instance::disarm_caps`] so it matches the
|
|
401
|
+
/// `timeout` deadline window and excludes `OUTCOME_BUFFER`
|
|
402
|
+
/// decoding and stdout / stderr capture readout.
|
|
294
403
|
fn prime_caps(&self) {
|
|
295
404
|
let mut store_ref = self.store.borrow_mut();
|
|
296
405
|
match self.timeout {
|
|
@@ -304,52 +413,80 @@ impl Instance {
|
|
|
304
413
|
store_ref.set_epoch_deadline(u64::MAX);
|
|
305
414
|
}
|
|
306
415
|
}
|
|
307
|
-
|
|
416
|
+
let baseline = match self.inner.get_export(store_ref.as_context_mut(), "memory") {
|
|
417
|
+
Some(Extern::Memory(m)) => m.data_size(store_ref.as_context_mut()),
|
|
418
|
+
_ => 0,
|
|
419
|
+
};
|
|
420
|
+
store_ref.data_mut().arm_memory_cap(baseline);
|
|
421
|
+
store_ref.data_mut().start_wall_clock();
|
|
308
422
|
}
|
|
309
423
|
|
|
310
424
|
/// Drop the memory cap as soon as the guest call returns so that
|
|
311
425
|
/// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
|
|
312
426
|
/// which can grow guest memory transiently) is not attributed to
|
|
313
|
-
/// the user script.
|
|
427
|
+
/// the user script. Also closes the docs/behavior.md B-35
|
|
428
|
+
/// `wall_time` bracket opened by [`Instance::prime_caps`]. Paired
|
|
429
|
+
/// with [`Instance::prime_caps`].
|
|
314
430
|
fn disarm_caps(&self) {
|
|
315
|
-
self.store
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
.limiter_mut()
|
|
319
|
-
.deactivate();
|
|
431
|
+
let mut store_ref = self.store.borrow_mut();
|
|
432
|
+
store_ref.data_mut().stop_wall_clock();
|
|
433
|
+
store_ref.data_mut().disarm_memory_cap();
|
|
320
434
|
}
|
|
321
435
|
|
|
322
|
-
///
|
|
323
|
-
///
|
|
324
|
-
///
|
|
325
|
-
///
|
|
326
|
-
///
|
|
327
|
-
fn
|
|
436
|
+
/// Allocate a +len+-byte buffer in guest linear memory via
|
|
437
|
+
/// `__kobako_alloc`, copy +envelope+ into it, and return +(ptr, len)+
|
|
438
|
+
/// as +i32+ values matching the `__kobako_run(env_ptr, env_len)` ABI.
|
|
439
|
+
/// Raises +Kobako::Wasm::Error+ when the guest export is missing or
|
|
440
|
+
/// allocation fails (docs/behavior.md E-31).
|
|
441
|
+
fn write_envelope(&self, ruby: &Ruby, envelope: RString) -> Result<(i32, i32), MagnusError> {
|
|
442
|
+
let bytes = rstring_to_vec(envelope);
|
|
443
|
+
let len_i32 = envelope_len_to_i32(bytes.len()).map_err(|msg| wasm_err(ruby, msg))?;
|
|
444
|
+
|
|
328
445
|
let mut store_ref = self.store.borrow_mut();
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
let
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
446
|
+
let alloc: TypedFunc<u32, u32> = self
|
|
447
|
+
.inner
|
|
448
|
+
.get_typed_func(store_ref.as_context_mut(), "__kobako_alloc")
|
|
449
|
+
.map_err(|_| wasm_err(ruby, SANDBOX_RUNTIME_MISSING_HOOKS))?;
|
|
450
|
+
let ptr = alloc
|
|
451
|
+
.call(store_ref.as_context_mut(), bytes.len() as u32)
|
|
452
|
+
.map_err(|e| wasm_err(ruby, format!("failed to allocate input buffer: {}", e)))?;
|
|
453
|
+
if ptr == 0 {
|
|
454
|
+
return Err(wasm_err(
|
|
455
|
+
ruby,
|
|
456
|
+
"could not allocate input buffer (out of memory)",
|
|
457
|
+
));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let memory: Memory = match self.inner.get_export(store_ref.as_context_mut(), "memory") {
|
|
461
|
+
Some(Extern::Memory(m)) => m,
|
|
462
|
+
_ => return Err(wasm_err(ruby, SANDBOX_RUNTIME_NOT_KOBAKO)),
|
|
463
|
+
};
|
|
464
|
+
let data = memory.data_mut(store_ref.as_context_mut());
|
|
465
|
+
let range = guest_buffer_range(ptr as usize, bytes.len(), data.len())
|
|
466
|
+
.map_err(|msg| wasm_err(ruby, msg))?;
|
|
467
|
+
data[range].copy_from_slice(&bytes);
|
|
468
|
+
|
|
469
|
+
Ok((ptr as i32, len_i32))
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/// Rebuild the WASI context with fresh stdin (carrying every frame in
|
|
473
|
+
/// +frames+, each prefixed by its 4-byte big-endian u32 length —
|
|
474
|
+
/// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
|
|
475
|
+
/// pipes. Called at the top of every guest invocation: +#eval+ passes
|
|
476
|
+
/// three frames (preamble, source, snippets), +#run+ passes two
|
|
477
|
+
/// (preamble, snippets — the invocation envelope arrives via linear
|
|
478
|
+
/// memory instead). Each output pipe is sized at `cap + 1` so
|
|
479
|
+
/// [`Instance::stdout`] / [`Instance::stderr`] can distinguish "wrote
|
|
480
|
+
/// exactly cap bytes" from "exceeded cap"; uncapped channels fall back
|
|
481
|
+
/// to `usize::MAX` and rely on `memory_limit` (docs/behavior.md E-20)
|
|
482
|
+
/// for the real ceiling.
|
|
483
|
+
fn refresh_wasi(&self, frames: &[Vec<u8>]) {
|
|
484
|
+
let total: usize = frames.iter().map(|f| 4 + f.len()).sum();
|
|
485
|
+
let mut stdin_content: Vec<u8> = Vec::with_capacity(total);
|
|
486
|
+
for frame in frames {
|
|
487
|
+
stdin_content.extend_from_slice(&(frame.len() as u32).to_be_bytes());
|
|
488
|
+
stdin_content.extend_from_slice(frame);
|
|
489
|
+
}
|
|
353
490
|
|
|
354
491
|
let stdin_pipe = MemoryInputPipe::new(stdin_content);
|
|
355
492
|
let stdout_pipe = MemoryOutputPipe::new(pipe_capacity(self.stdout_limit_bytes));
|
|
@@ -365,8 +502,6 @@ impl Instance {
|
|
|
365
502
|
.borrow_mut()
|
|
366
503
|
.data_mut()
|
|
367
504
|
.install_wasi(wasi, stdout_pipe, stderr_pipe);
|
|
368
|
-
|
|
369
|
-
Ok(())
|
|
370
505
|
}
|
|
371
506
|
|
|
372
507
|
/// Invoke `__kobako_take_outcome`, decode the packed +(ptr<<32)|len+
|
|
@@ -375,39 +510,94 @@ impl Instance {
|
|
|
375
510
|
/// arithmetic overflows, the slice falls outside live memory, or the
|
|
376
511
|
/// `memory` export itself is absent.
|
|
377
512
|
fn fetch_outcome_bytes(&self, ruby: &Ruby) -> Result<Vec<u8>, MagnusError> {
|
|
378
|
-
let take = self
|
|
379
|
-
.take_outcome
|
|
380
|
-
.as_ref()
|
|
381
|
-
.ok_or_else(|| wasm_err(ruby, "guest does not export __kobako_take_outcome"))?;
|
|
513
|
+
let take = require_export(ruby, self.take_outcome.as_ref(), "__kobako_take_outcome")?;
|
|
382
514
|
|
|
383
515
|
let mut store_ref = self.store.borrow_mut();
|
|
384
516
|
let packed = take
|
|
385
517
|
.call(store_ref.as_context_mut(), ())
|
|
386
|
-
.map_err(|e| wasm_err(ruby, format!("
|
|
387
|
-
let ptr = (
|
|
388
|
-
let len = (packed & 0xffff_ffff) as usize;
|
|
518
|
+
.map_err(|e| wasm_err(ruby, format!("failed to read invocation result: {}", e)))?;
|
|
519
|
+
let (ptr, len) = unpack_outcome_packed(packed);
|
|
389
520
|
|
|
390
521
|
let mem: Memory = match self.inner.get_export(store_ref.as_context_mut(), "memory") {
|
|
391
522
|
Some(Extern::Memory(m)) => m,
|
|
392
|
-
_ => return Err(wasm_err(ruby,
|
|
523
|
+
_ => return Err(wasm_err(ruby, SANDBOX_RUNTIME_NOT_KOBAKO)),
|
|
393
524
|
};
|
|
394
525
|
let data = mem.data(store_ref.as_context_mut());
|
|
395
|
-
let
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
526
|
+
let range = guest_buffer_range(ptr, len, data.len()).map_err(|msg| {
|
|
527
|
+
wasm_err(ruby, format!("invocation result is out of bounds: {}", msg))
|
|
528
|
+
})?;
|
|
529
|
+
Ok(data[range].to_vec())
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/// User-facing message for the "Sandbox runtime is missing one of the
|
|
534
|
+
/// internal Kobako hooks" failure mode. Phrased in caller vocabulary —
|
|
535
|
+
/// the underlying ABI symbol names (`__kobako_alloc`, `__kobako_eval`,
|
|
536
|
+
/// `__kobako_take_outcome`) are not actionable to callers, and the
|
|
537
|
+
/// gem itself raises this error so a self-reference like "matches the
|
|
538
|
+
/// kobako gem version" reads as third-person. The actionable
|
|
539
|
+
/// diagnosis is "your data/kobako.wasm is out of sync; rebuild it".
|
|
540
|
+
const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required hooks; \
|
|
541
|
+
rebuild data/kobako.wasm against the installed version";
|
|
542
|
+
|
|
543
|
+
/// User-facing message for the "the loaded Wasm module is not a
|
|
544
|
+
/// Kobako-shaped runtime at all" failure mode (no linear memory
|
|
545
|
+
/// export). Same phrasing philosophy as
|
|
546
|
+
/// [`SANDBOX_RUNTIME_MISSING_HOOKS`].
|
|
547
|
+
const SANDBOX_RUNTIME_NOT_KOBAKO: &str = "Sandbox runtime does not export linear memory; \
|
|
548
|
+
this is not a Kobako-compatible Wasm module";
|
|
549
|
+
|
|
550
|
+
/// Return the cached +TypedFunc+ for an ABI export, or raise
|
|
551
|
+
/// +Kobako::Wasm::Error+ when the option is +None+. The run-path
|
|
552
|
+
/// methods (+#eval+, +#run+, +#outcome!+) all share the same
|
|
553
|
+
/// "missing export → Ruby error" boilerplate; this helper collapses
|
|
554
|
+
/// the three sites onto one safe entry. The +_name+ argument is
|
|
555
|
+
/// retained for future operator-side logging but is deliberately not
|
|
556
|
+
/// spliced into the user-facing message (see
|
|
557
|
+
/// [`SANDBOX_RUNTIME_MISSING_HOOKS`]).
|
|
558
|
+
fn require_export<'a, Params, Results>(
|
|
559
|
+
ruby: &Ruby,
|
|
560
|
+
export: Option<&'a TypedFunc<Params, Results>>,
|
|
561
|
+
_name: &str,
|
|
562
|
+
) -> Result<&'a TypedFunc<Params, Results>, MagnusError>
|
|
563
|
+
where
|
|
564
|
+
Params: wasmtime::WasmParams,
|
|
565
|
+
Results: wasmtime::WasmResults,
|
|
566
|
+
{
|
|
567
|
+
export.ok_or_else(|| wasm_err(ruby, SANDBOX_RUNTIME_MISSING_HOOKS))
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/// Validate the invocation envelope length and return it as +i32+ — the
|
|
571
|
+
/// signed wasm ABI parameter type for the guest-run entrypoint.
|
|
572
|
+
/// Rejects sizes above +i32::MAX+ (2 GiB) so the downstream cast cannot
|
|
573
|
+
/// silently wrap.
|
|
574
|
+
fn envelope_len_to_i32(len: usize) -> Result<i32, &'static str> {
|
|
575
|
+
i32::try_from(len).map_err(|_| "invocation payload exceeds 2 GiB")
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/// Compute the half-open range `[ptr, ptr + len)` for a guest linear-memory
|
|
579
|
+
/// copy, validating that the arithmetic does not overflow and the range
|
|
580
|
+
/// fits inside `mem_size`. Shared by [`Instance::write_envelope`] (write
|
|
581
|
+
/// side) and [`Instance::fetch_outcome_bytes`] (read side).
|
|
582
|
+
fn guest_buffer_range(
|
|
583
|
+
ptr: usize,
|
|
584
|
+
len: usize,
|
|
585
|
+
mem_size: usize,
|
|
586
|
+
) -> Result<core::ops::Range<usize>, &'static str> {
|
|
587
|
+
let end = ptr.checked_add(len).ok_or("ptr + len overflow")?;
|
|
588
|
+
if end > mem_size {
|
|
589
|
+
return Err("range exceeds Sandbox memory size");
|
|
410
590
|
}
|
|
591
|
+
Ok(ptr..end)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/// Unpack the `(ptr, len)` u64 returned by `__kobako_take_outcome`:
|
|
595
|
+
/// high 32 bits = ptr, low 32 bits = len. Mirrors the guest-side
|
|
596
|
+
/// `crate::abi::unpack_u64` in `wasm/kobako-wasm/src/abi.rs`.
|
|
597
|
+
fn unpack_outcome_packed(packed: u64) -> (usize, usize) {
|
|
598
|
+
let ptr = (packed >> 32) as u32 as usize;
|
|
599
|
+
let len = packed as u32 as usize;
|
|
600
|
+
(ptr, len)
|
|
411
601
|
}
|
|
412
602
|
|
|
413
603
|
/// Translate a per-channel byte cap into the MemoryOutputPipe capacity:
|
|
@@ -446,6 +636,104 @@ fn capture_pair(ruby: &Ruby, raw: &[u8], cap: Option<usize>) -> Result<RArray, M
|
|
|
446
636
|
Ok(arr)
|
|
447
637
|
}
|
|
448
638
|
|
|
639
|
+
/// Epoch-deadline callback installed on every Store. Read the per-run
|
|
640
|
+
/// wall-clock deadline from [`HostState`] (docs/behavior.md B-01) and trap with
|
|
641
|
+
/// [`TimeoutTrap`] once the deadline has passed; otherwise extend the
|
|
642
|
+
/// next check by one tick of the process-wide epoch ticker. When the
|
|
643
|
+
/// deadline is `None` the callback should not fire under normal
|
|
644
|
+
/// `Instance::eval` / `Instance::run` flow because
|
|
645
|
+
/// `set_epoch_deadline(u64::MAX)` is used; returning a long extension
|
|
646
|
+
/// keeps the callback inert as a defence in depth.
|
|
647
|
+
fn epoch_deadline_callback(
|
|
648
|
+
ctx: StoreContextMut<'_, HostState>,
|
|
649
|
+
) -> wasmtime::Result<UpdateDeadline> {
|
|
650
|
+
match ctx.data().deadline() {
|
|
651
|
+
Some(deadline) if Instant::now() >= deadline => Err(wasmtime::Error::new(TimeoutTrap)),
|
|
652
|
+
Some(_) => Ok(UpdateDeadline::Continue(1)),
|
|
653
|
+
None => Ok(UpdateDeadline::Continue(u64::MAX / 2)),
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/// Configured-cap path classification for a wasmtime error. The
|
|
658
|
+
/// downcast logic stays in a pure helper so the
|
|
659
|
+
/// `Kobako::Wasm::TimeoutError` / `MemoryLimitError` /
|
|
660
|
+
/// `Kobako::Wasm::Error` mapping can be exercised from `cargo test`
|
|
661
|
+
/// without the magnus surface.
|
|
662
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
663
|
+
enum TrapClass {
|
|
664
|
+
/// docs/behavior.md E-19 wall-clock cap path.
|
|
665
|
+
Timeout,
|
|
666
|
+
/// docs/behavior.md E-20 linear-memory cap path.
|
|
667
|
+
MemoryLimit,
|
|
668
|
+
/// Any other wasmtime error — surfaces as the base
|
|
669
|
+
/// `Kobako::Wasm::Error`.
|
|
670
|
+
Other,
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/// Inspect a wasmtime error to decide which `Kobako::Wasm::*` class it
|
|
674
|
+
/// should map to. Pure function — operates on the error's downcast
|
|
675
|
+
/// chain only, no magnus / Ruby state required.
|
|
676
|
+
fn classify_trap(err: &wasmtime::Error) -> TrapClass {
|
|
677
|
+
if err.downcast_ref::<TimeoutTrap>().is_some() {
|
|
678
|
+
TrapClass::Timeout
|
|
679
|
+
} else if err.downcast_ref::<MemoryLimitTrap>().is_some() {
|
|
680
|
+
TrapClass::MemoryLimit
|
|
681
|
+
} else {
|
|
682
|
+
TrapClass::Other
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/// Map a wasmtime call error to the right `Kobako::Wasm::*` Ruby
|
|
687
|
+
/// exception class. The ABI export symbol (`__kobako_eval` /
|
|
688
|
+
/// `__kobako_run`) is deliberately omitted from the message — the
|
|
689
|
+
/// Sandbox layer attaches the user-facing verb (`Sandbox#eval` /
|
|
690
|
+
/// `Sandbox#run`) so the message reads in caller vocabulary rather
|
|
691
|
+
/// than ABI vocabulary.
|
|
692
|
+
///
|
|
693
|
+
/// For the configured-cap paths ([`TrapClass::Timeout`] /
|
|
694
|
+
/// [`TrapClass::MemoryLimit`]) the trap's own [`std::fmt::Display`]
|
|
695
|
+
/// carries the user-facing reason (`"wall-clock deadline exceeded"`,
|
|
696
|
+
/// `"linear memory growth exceeded memory_limit: ..."`). The wasmtime
|
|
697
|
+
/// outer wrapper at `format!("{}", err)` would otherwise surface only
|
|
698
|
+
/// the `"error while executing at wasm backtrace: ..."` framing, which
|
|
699
|
+
/// is operator noise on a cap trap. For [`TrapClass::Other`] the
|
|
700
|
+
/// wasmtime wrapper IS the diagnostic (real script trap) so it stays.
|
|
701
|
+
fn call_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
|
|
702
|
+
match classify_trap(&err) {
|
|
703
|
+
TrapClass::Timeout => {
|
|
704
|
+
let msg = err
|
|
705
|
+
.downcast_ref::<TimeoutTrap>()
|
|
706
|
+
.map(|t| t.to_string())
|
|
707
|
+
.unwrap_or_else(|| format!("{}", err));
|
|
708
|
+
timeout_err(ruby, msg)
|
|
709
|
+
}
|
|
710
|
+
TrapClass::MemoryLimit => {
|
|
711
|
+
let msg = err
|
|
712
|
+
.downcast_ref::<MemoryLimitTrap>()
|
|
713
|
+
.map(|t| t.to_string())
|
|
714
|
+
.unwrap_or_else(|| format!("{}", err));
|
|
715
|
+
memory_limit_err(ruby, msg)
|
|
716
|
+
}
|
|
717
|
+
TrapClass::Other => wasm_err(ruby, format!("{}", err)),
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/// Map an instantiation error to the right `Kobako::Wasm::*` Ruby
|
|
722
|
+
/// exception. The memory cap is dormant during instantiation by design
|
|
723
|
+
/// (see [`HostState::arm_memory_cap`] / [`HostState::disarm_memory_cap`]),
|
|
724
|
+
/// but [`MemoryLimitTrap`] is still possible if a future Sandbox
|
|
725
|
+
/// configuration enables it during instantiation — keep the mapping
|
|
726
|
+
/// symmetric with [`call_err`]. [`TrapClass::Timeout`] is unreachable on
|
|
727
|
+
/// the instantiation path (the epoch deadline is not armed yet) but
|
|
728
|
+
/// folding it into the same `match` keeps the two paths visually paired.
|
|
729
|
+
fn instantiate_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
|
|
730
|
+
let msg = format!("instantiate: {}", err);
|
|
731
|
+
match classify_trap(&err) {
|
|
732
|
+
TrapClass::MemoryLimit => memory_limit_err(ruby, msg),
|
|
733
|
+
TrapClass::Timeout | TrapClass::Other => wasm_err(ruby, msg),
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
449
737
|
#[cfg(test)]
|
|
450
738
|
mod tests {
|
|
451
739
|
//! Host-side unit tests for the pure capture helpers. The Ruby-
|
|
@@ -453,7 +741,11 @@ mod tests {
|
|
|
453
741
|
//! allowlist excludes guest fd 2 writes); these tests pin the
|
|
454
742
|
//! channel-agnostic slicing so a regression that only breaks one
|
|
455
743
|
//! channel cannot sneak through.
|
|
456
|
-
use super::{
|
|
744
|
+
use super::{
|
|
745
|
+
classify_trap, clip_capture, envelope_len_to_i32, guest_buffer_range, pipe_capacity,
|
|
746
|
+
unpack_outcome_packed, TrapClass,
|
|
747
|
+
};
|
|
748
|
+
use super::{MemoryLimitTrap, TimeoutTrap};
|
|
457
749
|
|
|
458
750
|
#[test]
|
|
459
751
|
fn pipe_capacity_adds_one_when_cap_is_set() {
|
|
@@ -508,49 +800,74 @@ mod tests {
|
|
|
508
800
|
assert_eq!(bytes, b"");
|
|
509
801
|
assert!(!truncated);
|
|
510
802
|
}
|
|
511
|
-
}
|
|
512
803
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
/// deadline is `None` the callback should not fire under normal
|
|
518
|
-
/// `Instance::run` flow because `set_epoch_deadline(u64::MAX)` is used;
|
|
519
|
-
/// returning a long extension keeps the callback inert as a defence in
|
|
520
|
-
/// depth.
|
|
521
|
-
fn epoch_deadline_callback(
|
|
522
|
-
ctx: StoreContextMut<'_, HostState>,
|
|
523
|
-
) -> wasmtime::Result<UpdateDeadline> {
|
|
524
|
-
match ctx.data().deadline() {
|
|
525
|
-
Some(deadline) if Instant::now() >= deadline => Err(wasmtime::Error::new(TimeoutTrap)),
|
|
526
|
-
Some(_) => Ok(UpdateDeadline::Continue(1)),
|
|
527
|
-
None => Ok(UpdateDeadline::Continue(u64::MAX / 2)),
|
|
804
|
+
#[test]
|
|
805
|
+
fn envelope_len_to_i32_accepts_zero_and_max() {
|
|
806
|
+
assert_eq!(envelope_len_to_i32(0), Ok(0));
|
|
807
|
+
assert_eq!(envelope_len_to_i32(i32::MAX as usize), Ok(i32::MAX));
|
|
528
808
|
}
|
|
529
|
-
}
|
|
530
809
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
fn run_call_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
|
|
536
|
-
if err.downcast_ref::<TimeoutTrap>().is_some() {
|
|
537
|
-
return timeout_err(ruby, format!("__kobako_run(): {}", err));
|
|
810
|
+
#[test]
|
|
811
|
+
fn envelope_len_to_i32_rejects_past_i32_max() {
|
|
812
|
+
assert!(envelope_len_to_i32(i32::MAX as usize + 1).is_err());
|
|
813
|
+
assert!(envelope_len_to_i32(usize::MAX).is_err());
|
|
538
814
|
}
|
|
539
|
-
|
|
540
|
-
|
|
815
|
+
|
|
816
|
+
#[test]
|
|
817
|
+
fn guest_buffer_range_returns_half_open_range() {
|
|
818
|
+
// Standard case: ptr + len fits inside memory.
|
|
819
|
+
assert_eq!(guest_buffer_range(10, 5, 100), Ok(10..15));
|
|
541
820
|
}
|
|
542
|
-
wasm_err(ruby, format!("__kobako_run(): {}", err))
|
|
543
|
-
}
|
|
544
821
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
822
|
+
#[test]
|
|
823
|
+
fn guest_buffer_range_accepts_zero_length_at_any_in_bounds_ptr() {
|
|
824
|
+
// Zero-length writes / reads must succeed as long as ptr is in
|
|
825
|
+
// bounds — both reactor calls hand zero-length frames through
|
|
826
|
+
// (e.g. an empty Frame 3 snippets list).
|
|
827
|
+
assert_eq!(guest_buffer_range(0, 0, 0), Ok(0..0));
|
|
828
|
+
assert_eq!(guest_buffer_range(42, 0, 100), Ok(42..42));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
#[test]
|
|
832
|
+
fn guest_buffer_range_rejects_ptr_plus_len_overflow() {
|
|
833
|
+
assert!(guest_buffer_range(usize::MAX, 1, usize::MAX).is_err());
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
#[test]
|
|
837
|
+
fn guest_buffer_range_rejects_end_past_memory() {
|
|
838
|
+
assert!(guest_buffer_range(10, 100, 50).is_err());
|
|
839
|
+
// End exactly equal to mem_size is in-bounds.
|
|
840
|
+
assert_eq!(guest_buffer_range(0, 50, 50), Ok(0..50));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
#[test]
|
|
844
|
+
fn unpack_outcome_packed_extracts_high_ptr_low_len() {
|
|
845
|
+
assert_eq!(
|
|
846
|
+
unpack_outcome_packed(0xAABB_CCDD_1122_3344),
|
|
847
|
+
(0xAABB_CCDD, 0x1122_3344)
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
#[test]
|
|
852
|
+
fn unpack_outcome_packed_zero_decodes_to_zero_pair() {
|
|
853
|
+
assert_eq!(unpack_outcome_packed(0), (0, 0));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
#[test]
|
|
857
|
+
fn classify_trap_routes_timeout_trap_to_timeout() {
|
|
858
|
+
let err = wasmtime::Error::new(TimeoutTrap);
|
|
859
|
+
assert_eq!(classify_trap(&err), TrapClass::Timeout);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
#[test]
|
|
863
|
+
fn classify_trap_routes_memory_limit_trap_to_memory_limit() {
|
|
864
|
+
let err = wasmtime::Error::new(MemoryLimitTrap::new(1 << 20, 1 << 19));
|
|
865
|
+
assert_eq!(classify_trap(&err), TrapClass::MemoryLimit);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
#[test]
|
|
869
|
+
fn classify_trap_falls_back_to_other_for_unknown_errors() {
|
|
870
|
+
let err = wasmtime::Error::msg("some other wasmtime fault");
|
|
871
|
+
assert_eq!(classify_trap(&err), TrapClass::Other);
|
|
554
872
|
}
|
|
555
|
-
wasm_err(ruby, format!("instantiate: {}", err))
|
|
556
873
|
}
|