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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +81 -0
  4. data/Cargo.lock +3 -3
  5. data/README.md +107 -32
  6. data/ROADMAP.md +25 -0
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +14 -0
  9. data/crates/kobako-runtime/Cargo.toml +1 -1
  10. data/crates/kobako-runtime/README.md +1 -1
  11. data/crates/kobako-runtime/src/error.rs +8 -0
  12. data/crates/kobako-runtime/src/snapshot.rs +7 -3
  13. data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
  14. data/crates/kobako-wasmtime/Cargo.toml +2 -2
  15. data/crates/kobako-wasmtime/README.md +1 -1
  16. data/crates/kobako-wasmtime/src/capture.rs +17 -13
  17. data/crates/kobako-wasmtime/src/driver.rs +7 -8
  18. data/crates/kobako-wasmtime/src/frames.rs +3 -10
  19. data/crates/kobako-wasmtime/src/guest_mem.rs +8 -1
  20. data/crates/kobako-wasmtime/src/instance_pre.rs +7 -6
  21. data/crates/kobako-wasmtime/src/invocation.rs +4 -5
  22. data/crates/kobako-wasmtime/src/trap.rs +65 -71
  23. data/data/kobako.wasm +0 -0
  24. data/ext/kobako/Cargo.toml +1 -1
  25. data/ext/kobako/src/runtime/errors.rs +9 -39
  26. data/ext/kobako/src/runtime.rs +5 -20
  27. data/lib/kobako/catalog/handles.rb +3 -3
  28. data/lib/kobako/catalog/services.rb +123 -0
  29. data/lib/kobako/catalog/snippets.rb +2 -2
  30. data/lib/kobako/catalog.rb +2 -2
  31. data/lib/kobako/codec/decoder.rb +10 -10
  32. data/lib/kobako/codec/encoder.rb +5 -5
  33. data/lib/kobako/codec/error.rb +3 -3
  34. data/lib/kobako/codec/ext_types.rb +169 -0
  35. data/lib/kobako/codec/handle_walk.rb +17 -21
  36. data/lib/kobako/codec/state.rb +98 -0
  37. data/lib/kobako/codec/utils.rb +9 -9
  38. data/lib/kobako/codec.rb +23 -6
  39. data/lib/kobako/errors.rb +14 -39
  40. data/lib/kobako/fault.rb +1 -1
  41. data/lib/kobako/handle.rb +6 -3
  42. data/lib/kobako/outcome.rb +14 -7
  43. data/lib/kobako/pool.rb +1 -1
  44. data/lib/kobako/sandbox.rb +30 -25
  45. data/lib/kobako/transport/dispatcher.rb +41 -31
  46. data/lib/kobako/transport/request.rb +16 -11
  47. data/lib/kobako/transport/response.rb +11 -4
  48. data/lib/kobako/transport/run.rb +1 -1
  49. data/lib/kobako/transport/yield.rb +7 -4
  50. data/lib/kobako/transport/yielder.rb +20 -12
  51. data/lib/kobako/version.rb +1 -1
  52. data/release-please-config.json +37 -7
  53. data/sig/kobako/catalog/services.rbs +25 -0
  54. data/sig/kobako/codec/ext_types.rbs +31 -0
  55. data/sig/kobako/codec/handle_walk.rbs +2 -2
  56. data/sig/kobako/codec/state.rbs +20 -0
  57. data/sig/kobako/codec.rbs +14 -0
  58. data/sig/kobako/handle.rbs +0 -2
  59. data/sig/kobako/sandbox.rbs +1 -1
  60. data/sig/kobako/transport/dispatcher.rbs +8 -8
  61. data/sig/kobako/transport/run.rbs +1 -1
  62. data/sig/kobako/transport/yielder.rbs +3 -3
  63. data/sig/kobako/transport.rbs +8 -0
  64. metadata +8 -7
  65. data/lib/kobako/catalog/namespaces.rb +0 -115
  66. data/lib/kobako/codec/factory.rb +0 -187
  67. data/lib/kobako/namespace.rb +0 -78
  68. data/sig/kobako/catalog/namespaces.rbs +0 -17
  69. data/sig/kobako/codec/factory.rbs +0 -34
  70. 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 slicing core shared by the snapshot readback: given the unclipped
22
- /// pipe snapshot and the configured cap, return the bytes Ruby should
23
- /// observe (clipped to `cap`) plus the truncation flag. `truncated` is
24
- /// `true` only when the snapshot strictly exceeded the cap — this is the
25
- /// "wrote `cap + 1` bytes into a `cap + 1`-sized pipe" case; "wrote
26
- /// exactly `cap` bytes" stays `false`.
27
- pub(crate) fn clip_capture(raw: &[u8], cap: Option<usize>) -> (&[u8], bool) {
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 => (&raw[..c], true),
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(b"", Some(5));
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(u64::MAX);
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 (stdout_raw, stderr_raw) = (data.stdout_bytes(), data.stderr_bytes());
207
- let (stdout_visible, stdout_truncated) =
208
- capture::clip_capture(&stdout_raw, self.config.stdout_limit_bytes);
209
- let (stderr_visible, stderr_truncated) =
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: stdout_visible.to_vec(),
213
+ bytes: stdout_bytes,
215
214
  truncated: stdout_truncated,
216
215
  },
217
216
  stderr: Capture {
218
- bytes: stderr_visible.to_vec(),
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(u64::MAX);
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("the loaded Wasm module is not a Kobako-compatible runtime"),
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
- // Decodes the Request bytes, dispatches via the Ruby-side
74
- // dispatch Proc (bound per-Sandbox through `Runtime#on_dispatch=`),
75
- // allocates a guest buffer through `__kobako_alloc`, writes
76
- // the Response bytes there, and returns the packed
77
- // `(ptr<<32)|len`. The dispatcher returns 0 on any wire-layer
78
- // fault (including no Proc bound); see `dispatch::handle`.
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 surface as
322
- /// `Kobako::MemoryLimitError` on the Ruby side. Callers use the
323
- /// `Display` impl below — no field is read directly — so the inner
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 surface as `Kobako::TimeoutError` on the Ruby side.
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
- //! 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
- //! `crate::invocation` (where the limiter / callback construct them).
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
- /// `set_epoch_deadline(u64::MAX)` is used; returning a long extension
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(u64::MAX / 2)),
41
+ None => Ok(UpdateDeadline::Continue(NO_TIMEOUT_EPOCH_DELTA)),
33
42
  }
34
43
  }
35
44
 
36
- /// Configured-cap path classification for a wasmtime error. The
37
- /// downcast logic stays in a pure helper so the
38
- /// `Kobako::TimeoutError` / `MemoryLimitError` /
39
- /// `Kobako::TrapError` mapping can be exercised from `cargo test`
40
- /// without the magnus surface.
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 (`TrapClass::Timeout` /
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: ..."`). The wasmtime
75
- /// outer wrapper at `format!("{err}")` would otherwise surface only
76
- /// the `"error while executing at wasm backtrace: ..."` framing, which
77
- /// is operator noise on a cap trap. For `TrapClass::Other` the framing
78
- /// is kept but the chain's root cause is appended (see
79
- /// `other_trap_message`) so the real trap reason survives.
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
- match classify_trap(&err) {
82
- TrapClass::Timeout => Trap::Timeout(
83
- err.downcast_ref::<TimeoutTrap>()
84
- .map(|t| t.to_string())
85
- .unwrap_or_else(|| format!("{err}")),
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::{classify_trap, other_trap_message, TrapClass};
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 classify_trap_routes_timeout_trap_to_timeout() {
121
+ fn trap_from_routes_timeout_trap_to_timeout() {
131
122
  let err = wasmtime::Error::new(TimeoutTrap);
132
- assert_eq!(classify_trap(&err), TrapClass::Timeout);
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 classify_trap_routes_memory_limit_trap_to_memory_limit() {
137
- let err = wasmtime::Error::new(MemoryLimitTrap::new(1 << 20, 1 << 19));
138
- assert_eq!(classify_trap(&err), TrapClass::MemoryLimit);
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 classify_trap_falls_back_to_other_for_unknown_errors() {
136
+ fn trap_from_falls_back_to_other_for_unknown_errors() {
143
137
  let err = wasmtime::Error::msg("some other wasmtime fault");
144
- assert_eq!(classify_trap(&err), TrapClass::Other);
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
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "kobako"
3
- version = "0.13.0"
3
+ version = "0.15.0"
4
4
  edition = "2021"
5
5
  authors = ["Aotokitsuruya <contact@aotoki.me>"]
6
6
  license = "Apache-2.0"
@@ -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) => timeout_err(ruby, msg),
99
- Trap::MemoryLimit(msg) => memory_limit_err(ruby, 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) => setup_err(ruby, msg),
110
- SetupError::Intact(msg) => sandbox_err(ruby, msg),
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
 
@@ -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` (top-level
71
- // `Kobako::TrapError` / `TimeoutError` / `MemoryLimitError` /
72
- // `SetupError` / `ModuleNotBuiltError`). The ext raises directly into
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
- wall_time: 0.0,
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::Namespaces+ so guest→host dispatch
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 {#alloc} against issuing an ID past the cap. Returns +nil+
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
- # {#fetch}. Returns +nil+ on success; raises +Kobako::SandboxError+
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)