kobako 0.6.2 → 0.8.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.
data/README.md CHANGED
@@ -283,6 +283,29 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
283
283
 
284
284
  Use the source form for snippets authored in your repo (compile errors fail fast at `#preload`); use the bytecode form when snippets ship as build artifacts from a separate `mrbc` pipeline. Both replay through the same per-invocation path.
285
285
 
286
+ ## Security
287
+
288
+ kobako isolates the guest, but **what it may reach is whatever you `bind`** — and `bind`
289
+ exposes *every* public method of the object. So bind a purpose-built object scoped to the
290
+ task, not a capable one whose other methods leak more than you intend.
291
+
292
+ ```ruby
293
+ class ThemeReader # only #color is reachable; AppConfig.secret_key is not
294
+ def color = AppConfig.theme.color
295
+ end
296
+
297
+ sandbox = Kobako::Sandbox.new
298
+ sandbox.define(:Cfg).bind(:Settings, ThemeReader.new) # not: bind(:Settings, AppConfig)
299
+
300
+ sandbox.eval('Cfg::Settings.color') # => "#3366ff" — every other method raises NoMethodError
301
+ ```
302
+
303
+ Guest code can name any `<Namespace>::<Member>` path, but a forged name only resolves to
304
+ something you bound — the real authorization gate is this host-side allowlist. Give each
305
+ trust context its own Sandbox, and see [`docs/security.md`](docs/security.md) for the rest
306
+ as security-design concerns: validating untrusted input, default-deny external effects,
307
+ and controlling the return surface.
308
+
286
309
  ## Performance
287
310
 
288
311
  Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values vary by hardware but ratios are stable across machines. Full numbers, methodology, and the +10%-regression gate live in [`benchmark/README.md`](benchmark/README.md).
data/data/kobako.wasm CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "kobako"
3
- version = "0.6.2"
3
+ version = "0.8.0"
4
4
  edition = "2021"
5
5
  authors = ["Aotokitsuruya <contact@aotoki.me>"]
6
6
  license = "Apache-2.0"
@@ -19,7 +19,7 @@ magnus = { version = "0.8.2" }
19
19
  # later via `wasmtime-wasi` once stdin/stdout wiring is needed (item #16).
20
20
  # `cache` / `parallel-compilation` / `pooling` / `component-model` / `async`
21
21
  # are intentionally off — kobako runs short-lived synchronous sandboxes.
22
- wasmtime = { version = "44.0.1", default-features = false, features = [
22
+ wasmtime = { version = "45.0.0", default-features = false, features = [
23
23
  "cranelift",
24
24
  "runtime",
25
25
  "gc",
@@ -33,4 +33,4 @@ wasmtime = { version = "44.0.1", default-features = false, features = [
33
33
  # WasiCtxBuilder + preview1 adapter which wires fd 1/2 to pipes. We omit
34
34
  # `p2` (component-model) and `p0`/`p3` (async) because kobako runs
35
35
  # synchronous sandboxes only.
36
- wasmtime-wasi = { version = "44.0.1", default-features = false, features = ["p1"] }
36
+ wasmtime-wasi = { version = "45.0.0", default-features = false, features = ["p1"] }
@@ -76,8 +76,9 @@ fn classify_trap(err: &wasmtime::Error) -> TrapClass {
76
76
  /// `"linear memory growth exceeded memory_limit: ..."`). The wasmtime
77
77
  /// outer wrapper at `format!("{}", err)` would otherwise surface only
78
78
  /// the `"error while executing at wasm backtrace: ..."` framing, which
79
- /// is operator noise on a cap trap. For `TrapClass::Other` the
80
- /// wasmtime wrapper IS the diagnostic (real script trap) so it stays.
79
+ /// is operator noise on a cap trap. For `TrapClass::Other` the framing
80
+ /// is kept but the chain's root cause is appended (see
81
+ /// `other_trap_message`) so the real trap reason survives.
81
82
  pub(super) fn call_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
82
83
  match classify_trap(&err) {
83
84
  TrapClass::Timeout => {
@@ -94,7 +95,24 @@ pub(super) fn call_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
94
95
  .unwrap_or_else(|| format!("{}", err));
95
96
  memory_limit_err(ruby, msg)
96
97
  }
97
- TrapClass::Other => trap_err(ruby, format!("{}", err)),
98
+ TrapClass::Other => trap_err(ruby, other_trap_message(&err)),
99
+ }
100
+ }
101
+
102
+ /// Compose the message for a non-cap trap. wasmtime's `Display` surfaces only
103
+ /// the `"error while executing at wasm backtrace: ..."` framing; the actual
104
+ /// trap reason (e.g. `"wasm trap: indirect call type mismatch"`) is the
105
+ /// chain's root cause and would otherwise be dropped, making real guest
106
+ /// faults undiagnosable. Append the root cause unless the framing already
107
+ /// carries it. Pure so it can be exercised from `cargo test` without the
108
+ /// magnus surface.
109
+ fn other_trap_message(err: &wasmtime::Error) -> String {
110
+ let display = format!("{}", err);
111
+ let root = err.root_cause().to_string();
112
+ if display.contains(&root) {
113
+ display
114
+ } else {
115
+ format!("{display}\n\n{root}")
98
116
  }
99
117
  }
100
118
 
@@ -111,7 +129,7 @@ pub(super) fn instantiate_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError
111
129
 
112
130
  #[cfg(test)]
113
131
  mod tests {
114
- use super::{classify_trap, TrapClass};
132
+ use super::{classify_trap, other_trap_message, TrapClass};
115
133
  use crate::runtime::invocation::{MemoryLimitTrap, TimeoutTrap};
116
134
 
117
135
  #[test]
@@ -131,4 +149,30 @@ mod tests {
131
149
  let err = wasmtime::Error::msg("some other wasmtime fault");
132
150
  assert_eq!(classify_trap(&err), TrapClass::Other);
133
151
  }
152
+
153
+ // A guest hard trap reaches the host as a wasmtime error whose Display is
154
+ // only the backtrace framing, with the trap reason buried as the chain's
155
+ // root cause. The named-capture regex bug surfaced as exactly this shape.
156
+ #[test]
157
+ fn other_trap_message_surfaces_buried_trap_reason() {
158
+ let err = wasmtime::Error::msg("wasm trap: indirect call type mismatch")
159
+ .context("error while executing at wasm backtrace:\n 0: 0x1 - <unknown>");
160
+ let msg = other_trap_message(&err);
161
+ assert!(
162
+ msg.contains("indirect call type mismatch"),
163
+ "a non-cap trap surfaced through Kobako::TrapError must carry the root trap reason, not only the backtrace framing; got: {msg}"
164
+ );
165
+ assert!(
166
+ msg.contains("error while executing"),
167
+ "a non-cap trap surfaced through Kobako::TrapError must keep the wasm backtrace framing; got: {msg}"
168
+ );
169
+ }
170
+
171
+ // A flat error (no cause chain) is its own root_cause; appending it would
172
+ // duplicate the whole message.
173
+ #[test]
174
+ fn other_trap_message_does_not_duplicate_a_flat_error() {
175
+ let err = wasmtime::Error::msg("plain fault");
176
+ assert_eq!(other_trap_message(&err), "plain fault");
177
+ }
134
178
  }
@@ -65,6 +65,12 @@ use self::config::Config;
65
65
  use self::exports::Exports;
66
66
  use self::invocation::{Invocation, StoreCell};
67
67
 
68
+ /// The wire ABI version this host implements (docs/wire-codec.md § ABI
69
+ /// Version). A Guest Binary is accepted only when its
70
+ /// `__kobako_abi_version` export reports the same value (B-40 / E-42);
71
+ /// the guest-side mirror is `kobako_core::abi::ABI_VERSION`.
72
+ const ABI_VERSION: u32 = 1;
73
+
68
74
  /// Copy the bytes of +s+ into a fresh `Vec<u8>`. Single safe entry to
69
75
  /// what would otherwise be an inline +unsafe { rstring.as_slice() }
70
76
  /// .to_vec()+ duplicated at every host-↔-guest boundary. The borrow
@@ -349,6 +355,8 @@ impl Runtime {
349
355
  .map_err(|e| trap::instantiate_err(&ruby, e))?
350
356
  };
351
357
 
358
+ Self::validate_abi_version(&instance, &store_cell, &ruby)?;
359
+
352
360
  let exports = Exports::resolve(&instance, &store_cell);
353
361
 
354
362
  Ok(Self {
@@ -364,6 +372,48 @@ impl Runtime {
364
372
  })
365
373
  }
366
374
 
375
+ /// Probe the guest's `__kobako_abi_version` export once at
376
+ /// construction and require equality with `ABI_VERSION`
377
+ /// (docs/behavior.md B-40). An absent export or a non-equal value is
378
+ /// E-42 — a deterministic artifact fault raised as
379
+ /// `Kobako::SetupError`.
380
+ fn validate_abi_version(
381
+ instance: &WtInstance,
382
+ store: &StoreCell,
383
+ ruby: &Ruby,
384
+ ) -> Result<(), MagnusError> {
385
+ let mut store_ref = store.borrow_mut();
386
+ let mut ctx = store_ref.as_context_mut();
387
+ let probe = instance
388
+ .get_typed_func::<(), u32>(&mut ctx, "__kobako_abi_version")
389
+ .map_err(|_| {
390
+ setup_err(
391
+ ruby,
392
+ format!(
393
+ "the Guest Binary does not export __kobako_abi_version; \
394
+ rebuild it against ABI version {ABI_VERSION}"
395
+ ),
396
+ )
397
+ })?;
398
+ let reported = probe.call(&mut ctx, ()).map_err(|e| {
399
+ setup_err(
400
+ ruby,
401
+ format!("failed to read the Guest Binary's ABI version: {e}"),
402
+ )
403
+ })?;
404
+ if reported != ABI_VERSION {
405
+ return Err(setup_err(
406
+ ruby,
407
+ format!(
408
+ "the Guest Binary reports ABI version {reported}, but this host \
409
+ implements ABI version {ABI_VERSION}; rebuild the Guest Binary \
410
+ against the host's version"
411
+ ),
412
+ ));
413
+ }
414
+ Ok(())
415
+ }
416
+
367
417
  /// Register the Ruby-side dispatch +Proc+ on the active Invocation.
368
418
  /// Bound to Ruby as +Kobako::Runtime#on_dispatch=+. From this point on,
369
419
  /// every +__kobako_dispatch+ host import invocation calls the Proc
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.6.2"
4
+ VERSION = "0.8.0"
5
5
  end
@@ -7,6 +7,17 @@
7
7
  "component": "kobako",
8
8
  "include-component-in-tag": false,
9
9
  "release-type": "ruby"
10
+ },
11
+ "wasm/kobako-core": {
12
+ "component": "kobako-core",
13
+ "release-type": "rust",
14
+ "extra-files": [
15
+ {
16
+ "type": "toml",
17
+ "path": "/wasm/Cargo.lock",
18
+ "jsonpath": "$.package[?(@.name=='kobako-core')].version"
19
+ }
20
+ ]
10
21
  }
11
22
  },
12
23
  "extra-files": [
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kobako
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aotokitsuruya