kobako 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +81 -0
- data/Cargo.lock +3 -3
- data/README.md +107 -32
- data/ROADMAP.md +25 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/error.rs +8 -0
- data/crates/kobako-runtime/src/snapshot.rs +7 -3
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/capture.rs +17 -13
- data/crates/kobako-wasmtime/src/driver.rs +7 -8
- data/crates/kobako-wasmtime/src/frames.rs +3 -10
- data/crates/kobako-wasmtime/src/guest_mem.rs +8 -1
- data/crates/kobako-wasmtime/src/instance_pre.rs +7 -6
- data/crates/kobako-wasmtime/src/invocation.rs +4 -5
- data/crates/kobako-wasmtime/src/trap.rs +65 -71
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/runtime/errors.rs +9 -39
- data/ext/kobako/src/runtime.rs +5 -20
- data/lib/kobako/catalog/handles.rb +3 -3
- data/lib/kobako/catalog/services.rb +123 -0
- data/lib/kobako/catalog/snippets.rb +2 -2
- data/lib/kobako/catalog.rb +2 -2
- data/lib/kobako/codec/decoder.rb +10 -10
- data/lib/kobako/codec/encoder.rb +5 -5
- data/lib/kobako/codec/error.rb +3 -3
- data/lib/kobako/codec/ext_types.rb +169 -0
- data/lib/kobako/codec/handle_walk.rb +17 -21
- data/lib/kobako/codec/state.rb +98 -0
- data/lib/kobako/codec/utils.rb +9 -9
- data/lib/kobako/codec.rb +23 -6
- data/lib/kobako/errors.rb +14 -39
- data/lib/kobako/fault.rb +1 -1
- data/lib/kobako/handle.rb +6 -3
- data/lib/kobako/outcome.rb +14 -7
- data/lib/kobako/pool.rb +1 -1
- data/lib/kobako/sandbox.rb +30 -25
- data/lib/kobako/transport/dispatcher.rb +41 -31
- data/lib/kobako/transport/request.rb +16 -11
- data/lib/kobako/transport/response.rb +11 -4
- data/lib/kobako/transport/run.rb +1 -1
- data/lib/kobako/transport/yield.rb +7 -4
- data/lib/kobako/transport/yielder.rb +20 -12
- data/lib/kobako/version.rb +1 -1
- data/release-please-config.json +37 -7
- data/sig/kobako/catalog/services.rbs +25 -0
- data/sig/kobako/codec/ext_types.rbs +31 -0
- data/sig/kobako/codec/handle_walk.rbs +2 -2
- data/sig/kobako/codec/state.rbs +20 -0
- data/sig/kobako/codec.rbs +14 -0
- data/sig/kobako/handle.rbs +0 -2
- data/sig/kobako/sandbox.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +8 -8
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +8 -0
- metadata +8 -7
- data/lib/kobako/catalog/namespaces.rb +0 -115
- data/lib/kobako/codec/factory.rb +0 -187
- data/lib/kobako/namespace.rb +0 -78
- data/sig/kobako/catalog/namespaces.rbs +0 -17
- data/sig/kobako/codec/factory.rbs +0 -34
- data/sig/kobako/namespace.rbs +0 -21
|
@@ -18,15 +18,19 @@ pub(crate) fn pipe_capacity(cap: Option<usize>) -> usize {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/// Pure
|
|
22
|
-
/// pipe snapshot
|
|
23
|
-
///
|
|
24
|
-
///
|
|
25
|
-
///
|
|
26
|
-
///
|
|
27
|
-
|
|
21
|
+
/// Pure clipping core shared by the snapshot readback: given the
|
|
22
|
+
/// unclipped pipe snapshot (owned — truncated in place, so the readback
|
|
23
|
+
/// costs one copy out of the pipe, not two), return the bytes Ruby
|
|
24
|
+
/// should observe plus the truncation flag. `truncated` is `true` only
|
|
25
|
+
/// when the snapshot strictly exceeded the cap — this is the "wrote
|
|
26
|
+
/// `cap + 1` bytes into a `cap + 1`-sized pipe" case; "wrote exactly
|
|
27
|
+
/// `cap` bytes" stays `false`.
|
|
28
|
+
pub(crate) fn clip_capture(mut raw: Vec<u8>, cap: Option<usize>) -> (Vec<u8>, bool) {
|
|
28
29
|
match cap {
|
|
29
|
-
Some(c) if raw.len() > c =>
|
|
30
|
+
Some(c) if raw.len() > c => {
|
|
31
|
+
raw.truncate(c);
|
|
32
|
+
(raw, true)
|
|
33
|
+
}
|
|
30
34
|
_ => (raw, false),
|
|
31
35
|
}
|
|
32
36
|
}
|
|
@@ -53,14 +57,14 @@ mod tests {
|
|
|
53
57
|
|
|
54
58
|
#[test]
|
|
55
59
|
fn clip_capture_returns_full_bytes_when_under_cap() {
|
|
56
|
-
let (bytes, truncated) = clip_capture(b"abc", Some(5));
|
|
60
|
+
let (bytes, truncated) = clip_capture(b"abc".to_vec(), Some(5));
|
|
57
61
|
assert_eq!(bytes, b"abc");
|
|
58
62
|
assert!(!truncated);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
#[test]
|
|
62
66
|
fn clip_capture_does_not_flag_truncation_at_exactly_cap_bytes() {
|
|
63
|
-
let (bytes, truncated) = clip_capture(b"abcde", Some(5));
|
|
67
|
+
let (bytes, truncated) = clip_capture(b"abcde".to_vec(), Some(5));
|
|
64
68
|
assert_eq!(bytes, b"abcde");
|
|
65
69
|
assert!(!truncated);
|
|
66
70
|
}
|
|
@@ -70,21 +74,21 @@ mod tests {
|
|
|
70
74
|
// The pipe is sized `cap + 1`, so the snapshot can be at most
|
|
71
75
|
// 6 bytes when `cap == 5`; that surface is what triggers the
|
|
72
76
|
// truncation flag.
|
|
73
|
-
let (bytes, truncated) = clip_capture(b"abcdef", Some(5));
|
|
77
|
+
let (bytes, truncated) = clip_capture(b"abcdef".to_vec(), Some(5));
|
|
74
78
|
assert_eq!(bytes, b"abcde");
|
|
75
79
|
assert!(truncated);
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
#[test]
|
|
79
83
|
fn clip_capture_treats_none_as_uncapped() {
|
|
80
|
-
let (bytes, truncated) = clip_capture(b"abcdef", None);
|
|
84
|
+
let (bytes, truncated) = clip_capture(b"abcdef".to_vec(), None);
|
|
81
85
|
assert_eq!(bytes, b"abcdef");
|
|
82
86
|
assert!(!truncated);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
#[test]
|
|
86
90
|
fn clip_capture_handles_empty_input() {
|
|
87
|
-
let (bytes, truncated) = clip_capture(
|
|
91
|
+
let (bytes, truncated) = clip_capture(Vec::new(), Some(5));
|
|
88
92
|
assert_eq!(bytes, b"");
|
|
89
93
|
assert!(!truncated);
|
|
90
94
|
}
|
|
@@ -183,7 +183,7 @@ impl Driver {
|
|
|
183
183
|
}
|
|
184
184
|
None => {
|
|
185
185
|
store.data_mut().set_deadline(None);
|
|
186
|
-
store.set_epoch_deadline(
|
|
186
|
+
store.set_epoch_deadline(trap::NO_TIMEOUT_EPOCH_DELTA);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
let baseline = match exports.memory {
|
|
@@ -203,19 +203,18 @@ impl Driver {
|
|
|
203
203
|
wall_time: data.wall_time().as_secs_f64(),
|
|
204
204
|
memory_peak: data.memory_peak(),
|
|
205
205
|
};
|
|
206
|
-
let (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
capture::clip_capture(&stderr_raw, self.config.stderr_limit_bytes);
|
|
206
|
+
let (stdout_bytes, stdout_truncated) =
|
|
207
|
+
capture::clip_capture(data.stdout_bytes(), self.config.stdout_limit_bytes);
|
|
208
|
+
let (stderr_bytes, stderr_truncated) =
|
|
209
|
+
capture::clip_capture(data.stderr_bytes(), self.config.stderr_limit_bytes);
|
|
211
210
|
Snapshot {
|
|
212
211
|
completion,
|
|
213
212
|
stdout: Capture {
|
|
214
|
-
bytes:
|
|
213
|
+
bytes: stdout_bytes,
|
|
215
214
|
truncated: stdout_truncated,
|
|
216
215
|
},
|
|
217
216
|
stderr: Capture {
|
|
218
|
-
bytes:
|
|
217
|
+
bytes: stderr_bytes,
|
|
219
218
|
truncated: stderr_truncated,
|
|
220
219
|
},
|
|
221
220
|
usage,
|
|
@@ -18,11 +18,11 @@ use kobako_runtime::profile::Profile;
|
|
|
18
18
|
|
|
19
19
|
/// Return the resolved `memory` export handle, or a `Trap` when the loaded
|
|
20
20
|
/// module exports no linear memory — the "not a Kobako-shaped runtime"
|
|
21
|
-
/// failure mode (`SANDBOX_RUNTIME_NOT_KOBAKO`).
|
|
21
|
+
/// failure mode (`guest_mem::SANDBOX_RUNTIME_NOT_KOBAKO`).
|
|
22
22
|
fn require_memory(exports: &Exports) -> Result<Memory, Trap> {
|
|
23
23
|
exports
|
|
24
24
|
.memory
|
|
25
|
-
.ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_NOT_KOBAKO.to_string()))
|
|
25
|
+
.ok_or_else(|| Trap::Other(guest_mem::SANDBOX_RUNTIME_NOT_KOBAKO.to_string()))
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/// Allocate a `len`-byte buffer in guest linear memory via
|
|
@@ -160,13 +160,6 @@ pub(crate) fn fetch_outcome_bytes(
|
|
|
160
160
|
const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required hooks; \
|
|
161
161
|
rebuild data/kobako.wasm against the installed version";
|
|
162
162
|
|
|
163
|
-
/// User-facing message for the "the loaded Wasm module is not a
|
|
164
|
-
/// Kobako-shaped runtime at all" failure mode (no linear memory
|
|
165
|
-
/// export). Same phrasing philosophy as
|
|
166
|
-
/// `SANDBOX_RUNTIME_MISSING_HOOKS`.
|
|
167
|
-
const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
|
|
168
|
-
"the loaded Wasm module is not a Kobako-compatible runtime";
|
|
169
|
-
|
|
170
163
|
/// Return the resolved `TypedFunc` for an ABI export, or a `Trap`
|
|
171
164
|
/// (boundary → `Kobako::TrapError`) when the option is `None`. Both
|
|
172
165
|
/// run-path methods (`#eval`, `#run`) plus the `build_snapshot` readout
|
|
@@ -225,7 +218,7 @@ mod tests {
|
|
|
225
218
|
profile,
|
|
226
219
|
};
|
|
227
220
|
let mut store = WtStore::new(engine, Invocation::new(None));
|
|
228
|
-
store.set_epoch_deadline(
|
|
221
|
+
store.set_epoch_deadline(crate::trap::NO_TIMEOUT_EPOCH_DELTA);
|
|
229
222
|
install_wasi_frames(&mut store, &config, &[]).expect("WASI context must install");
|
|
230
223
|
|
|
231
224
|
let mut linker: Linker<Invocation> = Linker::new(engine);
|
|
@@ -44,6 +44,13 @@ impl Yielder for CallerYielder<'_, '_> {
|
|
|
44
44
|
const RUNTIME_INCOMPATIBLE: &str =
|
|
45
45
|
"the Sandbox runtime is incompatible; rebuild data/kobako.wasm against the installed version";
|
|
46
46
|
|
|
47
|
+
/// User-facing message for the "the loaded Wasm module is not a
|
|
48
|
+
/// Kobako-shaped runtime at all" failure mode — no linear memory export
|
|
49
|
+
/// here, no `memory` module export on the instantiation path in
|
|
50
|
+
/// `frames`. One constant so the two detection sites cannot drift.
|
|
51
|
+
pub(crate) const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
|
|
52
|
+
"the loaded Wasm module is not a Kobako-compatible runtime";
|
|
53
|
+
|
|
47
54
|
/// Resolve the guest's exported linear `memory`. The lookup shape (and its
|
|
48
55
|
/// diagnostic) is shared by every Caller-based path here — the write side
|
|
49
56
|
/// (`alloc_and_write`), the read side (`read`), and the yield round-trip
|
|
@@ -52,7 +59,7 @@ const RUNTIME_INCOMPATIBLE: &str =
|
|
|
52
59
|
fn memory_export(caller: &mut Caller<'_, Invocation>) -> Result<Memory, &'static str> {
|
|
53
60
|
match caller.get_export("memory") {
|
|
54
61
|
Some(Extern::Memory(m)) => Ok(m),
|
|
55
|
-
_ => Err(
|
|
62
|
+
_ => Err(SANDBOX_RUNTIME_NOT_KOBAKO),
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -70,12 +70,13 @@ fn build_linker() -> Result<Linker<Invocation>, SetupError> {
|
|
|
70
70
|
// `__kobako_dispatch` host import. Signature per docs/wire-codec.md
|
|
71
71
|
// § ABI Signatures:
|
|
72
72
|
// (req_ptr: i32, req_len: i32) -> i64
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// `(ptr<<32)|len`. The
|
|
78
|
-
// fault (including no
|
|
73
|
+
// Reads the Request bytes from guest memory and hands them —
|
|
74
|
+
// undecoded — to the bound `DispatchHandler` (the frontend's
|
|
75
|
+
// dispatch bridge, e.g. a Ruby Proc), then allocates a guest
|
|
76
|
+
// buffer through `__kobako_alloc`, writes the handler's Response
|
|
77
|
+
// bytes there, and returns the packed `(ptr<<32)|len`. The
|
|
78
|
+
// dispatcher returns 0 on any wire-layer fault (including no
|
|
79
|
+
// handler bound); see `dispatch::handle`.
|
|
79
80
|
linker
|
|
80
81
|
.func_wrap(
|
|
81
82
|
"env",
|
|
@@ -318,10 +318,9 @@ impl ResourceLimiter for MemoryLimiter {
|
|
|
318
318
|
|
|
319
319
|
/// Marker error returned from `MemoryLimiter::memory_growing` when the
|
|
320
320
|
/// per-invocation memory cap is exceeded. Downcast from the wasmtime
|
|
321
|
-
/// trap error to
|
|
322
|
-
/// `
|
|
323
|
-
///
|
|
324
|
-
/// state stays private.
|
|
321
|
+
/// trap error to classify the failure as a memory-limit `Trap`. Callers
|
|
322
|
+
/// use the `Display` impl below — no field is read directly — so the
|
|
323
|
+
/// inner state stays private.
|
|
325
324
|
#[derive(Debug)]
|
|
326
325
|
pub(crate) struct MemoryLimitTrap {
|
|
327
326
|
desired: usize,
|
|
@@ -354,7 +353,7 @@ impl std::error::Error for MemoryLimitTrap {}
|
|
|
354
353
|
|
|
355
354
|
/// Marker error returned from the epoch-deadline callback when the
|
|
356
355
|
/// wall-clock deadline is exceeded. Downcast from the wasmtime trap
|
|
357
|
-
/// error to
|
|
356
|
+
/// error to classify the failure as a timeout `Trap`.
|
|
358
357
|
#[derive(Debug)]
|
|
359
358
|
pub(crate) struct TimeoutTrap;
|
|
360
359
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
//! Trap classification for the run path.
|
|
2
2
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//! hosts the epoch-deadline callback
|
|
6
|
-
//! `TimeoutTrap`. The classification is a
|
|
7
|
-
//! downcast chain so it can be exercised
|
|
8
|
-
//!
|
|
9
|
-
//! `crate::invocation` (where the limiter / callback
|
|
3
|
+
//! Classifies a `wasmtime` run error into the engine-neutral `Trap`
|
|
4
|
+
//! kind (`Timeout` / `MemoryLimit` / `Other`) that each frontend maps
|
|
5
|
+
//! onto its own error surface, and hosts the epoch-deadline callback
|
|
6
|
+
//! that raises the wall-clock `TimeoutTrap`. The classification is a
|
|
7
|
+
//! pure function over the error's downcast chain so it can be exercised
|
|
8
|
+
//! from `cargo test` without any frontend; the trap marker types
|
|
9
|
+
//! themselves live in `crate::invocation` (where the limiter / callback
|
|
10
|
+
//! construct them).
|
|
10
11
|
|
|
11
12
|
use std::time::Instant;
|
|
12
13
|
|
|
@@ -15,81 +16,55 @@ use wasmtime::{StoreContextMut, UpdateDeadline};
|
|
|
15
16
|
use crate::invocation::{Invocation, MemoryLimitTrap, TimeoutTrap};
|
|
16
17
|
use kobako_runtime::error::{SetupError, Trap};
|
|
17
18
|
|
|
19
|
+
/// Epoch delta that keeps the deadline effectively unreachable when no
|
|
20
|
+
/// wall-clock cap is configured. Half the epoch range rather than
|
|
21
|
+
/// `u64::MAX`: wasmtime adds the delta to the engine's current epoch,
|
|
22
|
+
/// which the process-wide ticker advances for the engine's whole
|
|
23
|
+
/// lifetime, so the full range overflows the sum (a panic under debug
|
|
24
|
+
/// overflow checks).
|
|
25
|
+
pub(crate) const NO_TIMEOUT_EPOCH_DELTA: u64 = u64::MAX / 2;
|
|
26
|
+
|
|
18
27
|
/// Epoch-deadline callback installed on every Store. Read the per-run
|
|
19
28
|
/// wall-clock deadline from `Invocation` and trap with
|
|
20
29
|
/// `TimeoutTrap` once the deadline has passed; otherwise extend the
|
|
21
30
|
/// next check by one tick of the process-wide epoch ticker. When the
|
|
22
31
|
/// deadline is `None` the callback should not fire under the normal
|
|
23
32
|
/// `Driver` invoke flow because
|
|
24
|
-
/// `
|
|
25
|
-
/// keeps the callback inert as a defence in depth.
|
|
33
|
+
/// `NO_TIMEOUT_EPOCH_DELTA` is primed; returning the same long
|
|
34
|
+
/// extension keeps the callback inert as a defence in depth.
|
|
26
35
|
pub(crate) fn epoch_deadline_callback(
|
|
27
36
|
ctx: StoreContextMut<'_, Invocation>,
|
|
28
37
|
) -> wasmtime::Result<UpdateDeadline> {
|
|
29
38
|
match ctx.data().deadline() {
|
|
30
39
|
Some(deadline) if Instant::now() >= deadline => Err(wasmtime::Error::new(TimeoutTrap)),
|
|
31
40
|
Some(_) => Ok(UpdateDeadline::Continue(1)),
|
|
32
|
-
None => Ok(UpdateDeadline::Continue(
|
|
41
|
+
None => Ok(UpdateDeadline::Continue(NO_TIMEOUT_EPOCH_DELTA)),
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
///
|
|
37
|
-
/// downcast
|
|
38
|
-
/// `
|
|
39
|
-
/// `
|
|
40
|
-
///
|
|
41
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
42
|
-
enum TrapClass {
|
|
43
|
-
/// Wall-clock cap path.
|
|
44
|
-
Timeout,
|
|
45
|
-
/// Linear-memory cap path.
|
|
46
|
-
MemoryLimit,
|
|
47
|
-
/// Any other wasmtime error — surfaces as the base
|
|
48
|
-
/// `Kobako::TrapError`.
|
|
49
|
-
Other,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/// Inspect a wasmtime error to decide which top-level `Kobako::*` trap
|
|
53
|
-
/// class it should map to. Pure function — operates on the error's
|
|
54
|
-
/// downcast chain only, no magnus / Ruby state required.
|
|
55
|
-
fn classify_trap(err: &wasmtime::Error) -> TrapClass {
|
|
56
|
-
if err.downcast_ref::<TimeoutTrap>().is_some() {
|
|
57
|
-
TrapClass::Timeout
|
|
58
|
-
} else if err.downcast_ref::<MemoryLimitTrap>().is_some() {
|
|
59
|
-
TrapClass::MemoryLimit
|
|
60
|
-
} else {
|
|
61
|
-
TrapClass::Other
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
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
|
|
45
|
+
/// Classify a wasmtime call error into a neutral `Trap`. Pure function
|
|
46
|
+
/// over the error's downcast chain, so the kind routing is exercisable
|
|
47
|
+
/// from `cargo test` without any frontend. The ABI export symbol
|
|
48
|
+
/// (`__kobako_eval` / `__kobako_run`) is deliberately omitted from the
|
|
49
|
+
/// message — the Sandbox layer attaches the user-facing verb
|
|
68
50
|
/// (`Sandbox#eval` / `Sandbox#run`) so the message reads in caller
|
|
69
51
|
/// vocabulary rather than ABI vocabulary.
|
|
70
52
|
///
|
|
71
|
-
/// For the configured-cap paths
|
|
72
|
-
/// `TrapClass::MemoryLimit`) the trap's own `std::fmt::Display`
|
|
53
|
+
/// For the configured-cap paths the trap's own `std::fmt::Display`
|
|
73
54
|
/// carries the user-facing reason (`"wall-clock deadline exceeded"`,
|
|
74
|
-
/// `"linear memory growth exceeded memory_limit: ..."`)
|
|
75
|
-
/// outer wrapper
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
///
|
|
79
|
-
///
|
|
55
|
+
/// `"linear memory growth exceeded memory_limit: ..."`); the wasmtime
|
|
56
|
+
/// outer wrapper would otherwise surface only the `"error while
|
|
57
|
+
/// executing at wasm backtrace: ..."` framing, which is operator noise
|
|
58
|
+
/// on a cap trap. For any other error the framing is kept but the
|
|
59
|
+
/// chain's root cause is appended (see `other_trap_message`) so the
|
|
60
|
+
/// real trap reason survives.
|
|
80
61
|
pub(crate) fn trap_from(err: wasmtime::Error) -> Trap {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
TrapClass::MemoryLimit => Trap::MemoryLimit(
|
|
88
|
-
err.downcast_ref::<MemoryLimitTrap>()
|
|
89
|
-
.map(|t| t.to_string())
|
|
90
|
-
.unwrap_or_else(|| format!("{err}")),
|
|
91
|
-
),
|
|
92
|
-
TrapClass::Other => Trap::Other(other_trap_message(&err)),
|
|
62
|
+
if let Some(t) = err.downcast_ref::<TimeoutTrap>() {
|
|
63
|
+
Trap::Timeout(t.to_string())
|
|
64
|
+
} else if let Some(t) = err.downcast_ref::<MemoryLimitTrap>() {
|
|
65
|
+
Trap::MemoryLimit(t.to_string())
|
|
66
|
+
} else {
|
|
67
|
+
Trap::Other(other_trap_message(&err))
|
|
93
68
|
}
|
|
94
69
|
}
|
|
95
70
|
|
|
@@ -123,25 +98,44 @@ pub(crate) fn instantiate_err(err: wasmtime::Error) -> SetupError {
|
|
|
123
98
|
|
|
124
99
|
#[cfg(test)]
|
|
125
100
|
mod tests {
|
|
126
|
-
use super::{
|
|
127
|
-
use crate::invocation::{MemoryLimitTrap, TimeoutTrap};
|
|
101
|
+
use super::{other_trap_message, trap_from, NO_TIMEOUT_EPOCH_DELTA};
|
|
102
|
+
use crate::invocation::{Invocation, MemoryLimitTrap, TimeoutTrap};
|
|
103
|
+
use kobako_runtime::error::Trap;
|
|
104
|
+
|
|
105
|
+
// The no-timeout priming delta is added to the engine's current
|
|
106
|
+
// epoch inside wasmtime, and the process-wide ticker advances that
|
|
107
|
+
// epoch from the first `shared_engine` call on — so the sum must
|
|
108
|
+
// stay in range for a long-lived engine, not just a fresh one.
|
|
109
|
+
// `increment_epoch` stands in for the ticker to make the ticked
|
|
110
|
+
// state deterministic; under debug overflow checks an overflowing
|
|
111
|
+
// delta panics right here.
|
|
112
|
+
#[test]
|
|
113
|
+
fn no_timeout_delta_survives_a_ticked_engine_epoch() {
|
|
114
|
+
let engine = crate::cache::shared_engine().expect("shared engine must be constructible");
|
|
115
|
+
engine.increment_epoch();
|
|
116
|
+
let mut store = wasmtime::Store::new(engine, Invocation::new(None));
|
|
117
|
+
store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
|
|
118
|
+
}
|
|
128
119
|
|
|
129
120
|
#[test]
|
|
130
|
-
fn
|
|
121
|
+
fn trap_from_routes_timeout_trap_to_timeout() {
|
|
131
122
|
let err = wasmtime::Error::new(TimeoutTrap);
|
|
132
|
-
|
|
123
|
+
let expected = TimeoutTrap.to_string();
|
|
124
|
+
assert!(matches!(trap_from(err), Trap::Timeout(msg) if msg == expected));
|
|
133
125
|
}
|
|
134
126
|
|
|
135
127
|
#[test]
|
|
136
|
-
fn
|
|
137
|
-
let
|
|
138
|
-
|
|
128
|
+
fn trap_from_routes_memory_limit_trap_to_memory_limit() {
|
|
129
|
+
let trap = MemoryLimitTrap::new(1 << 20, 1 << 19);
|
|
130
|
+
let expected = trap.to_string();
|
|
131
|
+
let err = wasmtime::Error::new(trap);
|
|
132
|
+
assert!(matches!(trap_from(err), Trap::MemoryLimit(msg) if msg == expected));
|
|
139
133
|
}
|
|
140
134
|
|
|
141
135
|
#[test]
|
|
142
|
-
fn
|
|
136
|
+
fn trap_from_falls_back_to_other_for_unknown_errors() {
|
|
143
137
|
let err = wasmtime::Error::msg("some other wasmtime fault");
|
|
144
|
-
|
|
138
|
+
assert!(matches!(trap_from(err), Trap::Other(_)));
|
|
145
139
|
}
|
|
146
140
|
|
|
147
141
|
// A guest hard trap reaches the host as a wasmtime error whose Display is
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/ext/kobako/Cargo.toml
CHANGED
|
@@ -55,48 +55,15 @@ pub(super) fn trap_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
|
55
55
|
error_in(ruby, &TRAP_ERROR, msg)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/// Construct a `Kobako::SetupError` magnus error. Used for every
|
|
59
|
-
/// construction-time failure on the `Runtime.from_path` path before any
|
|
60
|
-
/// invocation runs — unreadable artifact, bytes that are not a valid Wasm
|
|
61
|
-
/// module, or engine / linker / instantiation setup failure. The
|
|
62
|
-
/// `ModuleNotBuiltError` subclass (artifact absent) is
|
|
63
|
-
/// raised through `MODULE_NOT_BUILT_ERROR` directly.
|
|
64
|
-
pub(super) fn setup_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
65
|
-
error_in(ruby, &SETUP_ERROR, msg)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/// Construct a `Kobako::TimeoutError` magnus error. Surfaces the
|
|
69
|
-
/// wall-clock cap path with the verb prefix added
|
|
70
|
-
/// by `Kobako::Sandbox#invoke!`.
|
|
71
|
-
fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
72
|
-
error_in(ruby, &TIMEOUT_ERROR, msg)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/// Construct a `Kobako::MemoryLimitError` magnus error. Surfaces the
|
|
76
|
-
/// linear-memory cap path with the verb prefix
|
|
77
|
-
/// added by `Kobako::Sandbox#invoke!`.
|
|
78
|
-
fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
79
|
-
error_in(ruby, &MEMORY_LIMIT_ERROR, msg)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/// Construct a `Kobako::SandboxError` magnus error. Used for the
|
|
83
|
-
/// host-side pre-call faults the SPEC attributes to the sandbox / wire
|
|
84
|
-
/// layer rather than the Wasm engine — currently the `#run` invocation
|
|
85
|
-
/// envelope reservation failure (`__kobako_alloc` returns 0).
|
|
86
|
-
/// The runtime is intact, so this must not be a
|
|
87
|
-
/// `TrapError`: no discard-and-recreate recovery is owed to the caller.
|
|
88
|
-
fn sandbox_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
89
|
-
error_in(ruby, &SANDBOX_ERROR, msg)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
58
|
/// Map a neutral `Trap` onto its `Kobako::TrapError`-family Ruby exception.
|
|
93
59
|
/// The boundary between the magnus-free run mechanics and the Ruby surface:
|
|
94
60
|
/// the run path classifies a fault into a `Trap`, and this is where it
|
|
95
|
-
/// becomes a raised exception.
|
|
61
|
+
/// becomes a raised exception. The verb prefix (`Sandbox#eval` / `#run`)
|
|
62
|
+
/// is added by `Kobako::Sandbox#invoke!`.
|
|
96
63
|
pub(super) fn trap_to_magnus(ruby: &Ruby, trap: Trap) -> MagnusError {
|
|
97
64
|
match trap {
|
|
98
|
-
Trap::Timeout(msg) =>
|
|
99
|
-
Trap::MemoryLimit(msg) =>
|
|
65
|
+
Trap::Timeout(msg) => error_in(ruby, &TIMEOUT_ERROR, msg),
|
|
66
|
+
Trap::MemoryLimit(msg) => error_in(ruby, &MEMORY_LIMIT_ERROR, msg),
|
|
100
67
|
Trap::Other(msg) => trap_err(ruby, msg),
|
|
101
68
|
}
|
|
102
69
|
}
|
|
@@ -106,8 +73,11 @@ pub(super) fn trap_to_magnus(ruby: &Ruby, trap: Trap) -> MagnusError {
|
|
|
106
73
|
pub(super) fn setup_to_magnus(ruby: &Ruby, err: SetupError) -> MagnusError {
|
|
107
74
|
match err {
|
|
108
75
|
SetupError::ModuleNotBuilt(msg) => error_in(ruby, &MODULE_NOT_BUILT_ERROR, msg),
|
|
109
|
-
SetupError::Dead(msg) =>
|
|
110
|
-
|
|
76
|
+
SetupError::Dead(msg) => error_in(ruby, &SETUP_ERROR, msg),
|
|
77
|
+
// Runtime intact means a host-side pre-call fault the SPEC
|
|
78
|
+
// attributes to the sandbox / wire layer, not the engine — no
|
|
79
|
+
// discard-and-recreate recovery is owed, so never a TrapError.
|
|
80
|
+
SetupError::Intact(msg) => error_in(ruby, &SANDBOX_ERROR, msg),
|
|
111
81
|
}
|
|
112
82
|
}
|
|
113
83
|
|
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -53,26 +53,14 @@ fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
|
53
53
|
unsafe { s.as_slice() }.to_vec()
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/// The pre-invocation sentinel for one capture channel: no bytes, cap
|
|
57
|
-
/// not reached. Fresh `Vec`s per call because `Capture` owns its buffer.
|
|
58
|
-
fn empty_capture() -> Capture {
|
|
59
|
-
Capture {
|
|
60
|
-
bytes: Vec::new(),
|
|
61
|
-
truncated: false,
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
56
|
// ---------------------------------------------------------------------------
|
|
66
57
|
// Ruby init
|
|
67
58
|
// ---------------------------------------------------------------------------
|
|
68
59
|
|
|
69
60
|
pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
70
|
-
// Error hierarchy lives in `lib/kobako/errors.rb
|
|
71
|
-
//
|
|
72
|
-
// `
|
|
73
|
-
// those classes through `trap_err` / `timeout_err` / `memory_limit_err`
|
|
74
|
-
// / `sandbox_err` / `setup_err` / `MODULE_NOT_BUILT_ERROR`; no
|
|
75
|
-
// intermediate hierarchy is registered.
|
|
61
|
+
// Error hierarchy lives in `lib/kobako/errors.rb`; the ext raises
|
|
62
|
+
// directly into those classes through the constructors and mappers
|
|
63
|
+
// in `runtime/errors.rs` — no intermediate hierarchy is registered.
|
|
76
64
|
|
|
77
65
|
let runtime = kobako.define_class("Runtime", ruby.class_object())?;
|
|
78
66
|
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 6))?;
|
|
@@ -210,11 +198,8 @@ impl Runtime {
|
|
|
210
198
|
Ok(Self {
|
|
211
199
|
driver,
|
|
212
200
|
on_dispatch: Cell::new(None),
|
|
213
|
-
last_usage: Cell::new(Usage
|
|
214
|
-
|
|
215
|
-
memory_peak: 0,
|
|
216
|
-
}),
|
|
217
|
-
last_captures: RefCell::new((empty_capture(), empty_capture())),
|
|
201
|
+
last_usage: Cell::new(Usage::default()),
|
|
202
|
+
last_captures: RefCell::new((Capture::default(), Capture::default())),
|
|
218
203
|
})
|
|
219
204
|
}
|
|
220
205
|
|
|
@@ -6,7 +6,7 @@ module Kobako
|
|
|
6
6
|
module Catalog
|
|
7
7
|
# Host-side mapping from opaque integer Handle IDs to Ruby objects.
|
|
8
8
|
# The table is owned by +Kobako::Sandbox+ and injected
|
|
9
|
-
# into the per-Sandbox +Kobako::Catalog::
|
|
9
|
+
# into the per-Sandbox +Kobako::Catalog::Services+ so guest→host dispatch
|
|
10
10
|
# resolves Handle targets and arguments against the same table that
|
|
11
11
|
# host→guest wire encoding allocates into.
|
|
12
12
|
#
|
|
@@ -95,7 +95,7 @@ module Kobako
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# Guard
|
|
98
|
+
# Guard #alloc against issuing an ID past the cap. Returns +nil+
|
|
99
99
|
# on success; raises +Kobako::HandleExhaustedError+ at exhaustion.
|
|
100
100
|
def ensure_capacity!
|
|
101
101
|
cap = Kobako::Handle::MAX_ID
|
|
@@ -107,7 +107,7 @@ module Kobako
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Single source of truth for the "unknown Handle id" raise used by
|
|
110
|
-
#
|
|
110
|
+
# #fetch. Returns +nil+ on success; raises +Kobako::SandboxError+
|
|
111
111
|
# when +id+ is not currently bound.
|
|
112
112
|
def require_bound!(id)
|
|
113
113
|
return if @entries.key?(id)
|