kobako 0.12.2 → 0.13.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.
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.1...kobako-runtime-v0.7.0) (2026-07-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
9
+ * **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
10
+
3
11
  ## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.0...kobako-runtime-v0.6.1) (2026-07-02)
4
12
 
5
13
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  [package]
15
15
  name = "kobako-runtime"
16
- version = "0.6.1"
16
+ version = "0.7.0"
17
17
  edition = "2021"
18
18
  description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
19
19
  license = "Apache-2.0"
@@ -26,7 +26,7 @@ map the neutral types onto their own language surface.
26
26
 
27
27
  ```toml
28
28
  [dependencies]
29
- kobako-runtime = "0.6.1" # x-release-please-version
29
+ kobako-runtime = "0.7.0" # x-release-please-version
30
30
  ```
31
31
 
32
32
  ## License
@@ -1,8 +1,9 @@
1
1
  //! kobako-runtime — engine-neutral host runtime contract.
2
2
  //!
3
3
  //! The surface where a wasm engine implementation and a host frontend
4
- //! meet: the `Runtime` trait, the neutral per-invocation value types,
5
- //! and the dispatch / yield re-entry traits a frontend supplies.
4
+ //! meet: the `Runtime` trait, the isolation `Profile` a runtime
5
+ //! declares, the neutral per-invocation value types, and the
6
+ //! dispatch / yield re-entry traits a frontend supplies.
6
7
  //! Nothing here depends on an engine or a frontend type — each engine
7
8
  //! hides its own machinery behind `Runtime`, and each frontend maps
8
9
  //! these shapes onto its own host-language surface at its boundary
@@ -11,6 +12,7 @@
11
12
 
12
13
  pub mod dispatch;
13
14
  pub mod error;
15
+ pub mod profile;
14
16
  pub mod runtime;
15
17
  pub mod snapshot;
16
18
  pub mod yielder;
@@ -0,0 +1,35 @@
1
+ //! Isolation profile — the posture a frontend requests and a runtime
2
+ //! builds and declares.
3
+ //!
4
+ //! A rung on an ordered ladder: the host application requests the
5
+ //! posture it wants, the runtime builds it and declares the posture it
6
+ //! actually built, and the frontend refuses a declaration below the
7
+ //! request — so the request is also the floor. The governing contract
8
+ //! lives in the spec corpus (docs/behavior/security.md).
9
+
10
+ /// The ordered isolation ladder a runtime builds one rung of.
11
+ ///
12
+ /// `Hermetic` is the full ambient-denial posture: ambient time and
13
+ /// entropy denied at the WASI boundary, no filesystem / environment /
14
+ /// network reachability, and no host import beyond the wire ABI's
15
+ /// `__kobako_dispatch`. `Permissive` differs in exactly one grant —
16
+ /// live ambient time and entropy at the WASI boundary. Ordering
17
+ /// follows strength (`Permissive < Hermetic`), so a floor check is a
18
+ /// plain comparison.
19
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
20
+ pub enum Profile {
21
+ Permissive,
22
+ Hermetic,
23
+ }
24
+
25
+ #[cfg(test)]
26
+ mod tests {
27
+ use super::*;
28
+
29
+ #[test]
30
+ fn a_declaration_satisfies_any_floor_at_or_below_it() {
31
+ assert!(Profile::Hermetic >= Profile::Hermetic);
32
+ assert!(Profile::Hermetic >= Profile::Permissive);
33
+ assert!(Profile::Permissive < Profile::Hermetic);
34
+ }
35
+ }
@@ -11,6 +11,7 @@ use std::sync::Arc;
11
11
 
12
12
  use crate::dispatch::DispatchHandler;
13
13
  use crate::error::Error;
14
+ use crate::profile::Profile;
14
15
  use crate::snapshot::Snapshot;
15
16
 
16
17
  /// The per-invocation entry: a one-shot mruby source (`Eval`) or an
@@ -47,4 +48,10 @@ pub trait Runtime {
47
48
  frames: Frames<'_>,
48
49
  handler: Option<Arc<dyn DispatchHandler>>,
49
50
  ) -> Result<Snapshot, Error>;
51
+
52
+ /// The isolation profile this runtime provides. Deliberately
53
+ /// without a default: every engine states its posture explicitly,
54
+ /// and a frontend refuses construction when the declaration falls
55
+ /// below the floor its host application requested.
56
+ fn profile(&self) -> Profile;
50
57
  }
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.1...kobako-wasmtime-v0.7.0) (2026-07-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
9
+ * **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
10
+
3
11
  ## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.0...kobako-wasmtime-v0.6.1) (2026-07-02)
4
12
 
5
13
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  [package]
17
17
  name = "kobako-wasmtime"
18
- version = "0.6.1"
18
+ version = "0.7.0"
19
19
  edition = "2021"
20
20
  description = "wasmtime implementation of the kobako host runtime contract."
21
21
  license = "Apache-2.0"
@@ -28,14 +28,14 @@ categories = ["wasm", "virtualization"]
28
28
  # The engine-neutral contract this crate implements. The version pin
29
29
  # rides the linked release group; the path keeps in-tree builds (and
30
30
  # the Ruby gem, which ships both crates) resolving locally.
31
- kobako-runtime = { version = "0.6.1", path = "../kobako-runtime" }
31
+ kobako-runtime = { version = "0.7.0", path = "../kobako-runtime" }
32
32
  # wasmtime — host-side embedder for kobako.wasm. We disable default-features
33
33
  # and opt back in only what kobako needs: a Cranelift-backed runtime that can
34
34
  # compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
35
35
  # feature so test fixtures can be expressed as text.
36
36
  # `cache` / `parallel-compilation` / `pooling` / `component-model` / `async`
37
37
  # are intentionally off — kobako runs short-lived synchronous sandboxes.
38
- wasmtime = { version = "45.0.0", default-features = false, features = [
38
+ wasmtime = { version = "46.0.1", default-features = false, features = [
39
39
  "cranelift",
40
40
  "runtime",
41
41
  "gc",
@@ -49,7 +49,7 @@ wasmtime = { version = "45.0.0", default-features = false, features = [
49
49
  # WasiCtxBuilder + preview1 adapter which wires fd 1/2 to pipes. We omit
50
50
  # `p2` (component-model) and `p0`/`p3` (async) because kobako runs
51
51
  # synchronous sandboxes only.
52
- wasmtime-wasi = { version = "45.0.0", default-features = false, features = ["p1"] }
52
+ wasmtime-wasi = { version = "46.0.1", default-features = false, features = ["p1"] }
53
53
  # sha2 keys the on-disk compiled-module cache by Guest Binary content
54
54
  # (see cache.rs); a collision would load the wrong artifact, so the
55
55
  # hash must be cryptographic.
@@ -24,7 +24,7 @@ SDK consumes the same surface.
24
24
 
25
25
  ```toml
26
26
  [dependencies]
27
- kobako-wasmtime = "0.6.1" # x-release-please-version
27
+ kobako-wasmtime = "0.7.0" # x-release-please-version
28
28
  ```
29
29
 
30
30
  ## License
@@ -1,11 +1,14 @@
1
- //! Defense-in-depth denial of guest ambient authority at the WASI layer.
1
+ //! Denial of guest ambient authority at the WASI layer — the one grant
2
+ //! that separates the hermetic rung from permissive;
3
+ //! `frames::install_wasi_frames` wires these sources on `Hermetic` only.
2
4
  //!
3
5
  //! `wasmtime-wasi`'s `WasiCtxBuilder` defaults the guest's `wasi:clocks` to
4
6
  //! the host wall / monotonic clock and `wasi:random` to a fresh per-context
5
7
  //! seed. No allowlisted mrbgem reaches these preview1 imports today
6
8
  //! (`build_config/wasi.rb`), but a future libc-backed gem would silently
7
9
  //! obtain real time and host entropy — a covert timing channel and a
8
- //! nondeterminism source the sandbox deliberately excludes (docs/security-model.md).
10
+ //! nondeterminism source the hermetic posture deliberately excludes
11
+ //! (docs/security-model.md).
9
12
  //! Pinning the clocks to the Unix epoch and the RNG to a constant stream
10
13
  //! makes that denial a property of the host, not merely of the gem allowlist.
11
14
  //!
@@ -10,8 +10,10 @@
10
10
 
11
11
  use std::time::Duration;
12
12
 
13
- /// Wall-clock and output caps for one `Driver`. `None` on any field
14
- /// disables that cap.
13
+ use kobako_runtime::profile::Profile;
14
+
15
+ /// Wall-clock and output caps plus the requested isolation profile for
16
+ /// one `Driver`. `None` on any cap field disables that cap.
15
17
  pub struct Config {
16
18
  /// Wall-clock cap for one guest `#eval` / `#run`. Stamped into a
17
19
  /// per-run `Instant` deadline by `Driver::prime_caps`.
@@ -22,4 +24,10 @@ pub struct Config {
22
24
  pub stdout_limit_bytes: Option<usize>,
23
25
  /// Byte cap for guest stderr capture. Mirror of `stdout_limit_bytes`.
24
26
  pub stderr_limit_bytes: Option<usize>,
27
+ /// Isolation posture the frontend requested. The per-invocation
28
+ /// WASI context is built to this rung — `Hermetic` freezes ambient
29
+ /// time and entropy (`crate::ambient`), `Permissive` leaves the
30
+ /// live WASI sources — and `Driver::profile` declares it back as
31
+ /// the built posture.
32
+ pub profile: Profile,
25
33
  }
@@ -22,6 +22,7 @@ use crate::invocation::Invocation;
22
22
  use crate::{capture, frames, instance_pre, trap};
23
23
  use kobako_runtime::dispatch::DispatchHandler;
24
24
  use kobako_runtime::error::{Error, SetupError, Trap};
25
+ use kobako_runtime::profile::Profile;
25
26
  use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
26
27
  use kobako_runtime::snapshot::{Capture, Completion, Snapshot, Usage};
27
28
 
@@ -271,6 +272,17 @@ impl ContractRuntime for Driver {
271
272
  };
272
273
  Ok(self.build_snapshot(&store, completion))
273
274
  }
275
+
276
+ /// The posture this driver built — exactly the rung requested via
277
+ /// `Config`. Every per-invocation WASI context is built to it
278
+ /// (`frames::install_wasi_frames`): `Hermetic` freezes ambient time
279
+ /// and entropy (`crate::ambient`), `Permissive` leaves the live WASI
280
+ /// sources. Both rungs wire no filesystem, environment, or network,
281
+ /// and the linker adds only the wire ABI's `__kobako_dispatch`
282
+ /// beyond that confined WASI surface (`crate::instance_pre`).
283
+ fn profile(&self) -> Profile {
284
+ self.config.profile
285
+ }
274
286
  }
275
287
 
276
288
  /// Drop the memory cap as soon as the guest call returns so that
@@ -14,6 +14,7 @@ use crate::exports::Exports;
14
14
  use crate::invocation::Invocation;
15
15
  use crate::{ambient, capture, guest_mem};
16
16
  use kobako_runtime::error::{Error, SetupError, Trap};
17
+ use kobako_runtime::profile::Profile;
17
18
 
18
19
  /// Return the resolved `memory` export handle, or a `Trap` when the loaded
19
20
  /// module exports no linear memory — the "not a Kobako-shaped runtime"
@@ -98,11 +99,20 @@ pub(crate) fn install_wasi_frames(
98
99
  builder.stdin(stdin_pipe);
99
100
  builder.stdout(stdout_pipe.clone());
100
101
  builder.stderr(stderr_pipe.clone());
101
- // Deny the preview1 ambient-authority imports the guest never legitimately
102
- // reaches but the WASI layer would otherwise grant (see `ambient`).
103
- builder.wall_clock(ambient::FrozenWallClock);
104
- builder.monotonic_clock(ambient::FrozenMonotonicClock);
105
- builder.secure_random(ambient::deterministic_rng());
102
+ // The requested profile decides the ambient-authority grant: the
103
+ // hermetic rung denies the preview1 time and entropy imports (see
104
+ // `ambient`), the permissive rung leaves the live WASI sources.
105
+ // Filesystem, environment, and network stay absent on both rungs —
106
+ // the builder grants none unless asked. The exhaustive match makes
107
+ // a future ladder rung a compile error here, not a silent grant.
108
+ match config.profile {
109
+ Profile::Hermetic => {
110
+ builder.wall_clock(ambient::FrozenWallClock);
111
+ builder.monotonic_clock(ambient::FrozenMonotonicClock);
112
+ builder.secure_random(ambient::deterministic_rng());
113
+ }
114
+ Profile::Permissive => {}
115
+ }
106
116
  let wasi = builder.build_p1();
107
117
 
108
118
  store
@@ -174,3 +184,93 @@ where
174
184
  {
175
185
  export.ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_MISSING_HOOKS.to_string()))
176
186
  }
187
+
188
+ #[cfg(test)]
189
+ mod tests {
190
+ //! Witness the B-54 rung split at the WASI boundary: one probe
191
+ //! module reads `wasi:clocks` / `wasi:random` through the context
192
+ //! `install_wasi_frames` builds — frozen under `Hermetic`, live
193
+ //! under `Permissive`.
194
+ use wasmtime::{Linker, Module};
195
+ use wasmtime_wasi::p1;
196
+
197
+ use super::*;
198
+ use crate::cache::shared_engine;
199
+
200
+ /// Preview1 probe with one export per ambient source: `clock_ns`
201
+ /// reads the realtime clock, `random_word` reads eight entropy bytes.
202
+ const AMBIENT_PROBE_WAT: &str = r#"
203
+ (module
204
+ (import "wasi_snapshot_preview1" "clock_time_get"
205
+ (func $clock (param i32 i64 i32) (result i32)))
206
+ (import "wasi_snapshot_preview1" "random_get"
207
+ (func $random (param i32 i32) (result i32)))
208
+ (memory (export "memory") 1)
209
+ (func (export "clock_ns") (result i64)
210
+ (drop (call $clock (i32.const 0) (i64.const 1) (i32.const 0)))
211
+ (i64.load (i32.const 0)))
212
+ (func (export "random_word") (result i64)
213
+ (drop (call $random (i32.const 8) (i32.const 8)))
214
+ (i64.load (i32.const 8))))
215
+ "#;
216
+
217
+ /// Instantiate the probe over a WASI context built at `profile` and
218
+ /// return the `(clock, random)` readings it observes.
219
+ fn probe_ambient(profile: Profile) -> (i64, i64) {
220
+ let engine = shared_engine().expect("shared engine must be constructible");
221
+ let config = Config {
222
+ timeout: None,
223
+ stdout_limit_bytes: None,
224
+ stderr_limit_bytes: None,
225
+ profile,
226
+ };
227
+ let mut store = WtStore::new(engine, Invocation::new(None));
228
+ store.set_epoch_deadline(u64::MAX);
229
+ install_wasi_frames(&mut store, &config, &[]).expect("WASI context must install");
230
+
231
+ let mut linker: Linker<Invocation> = Linker::new(engine);
232
+ p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
233
+ .expect("WASI imports must link");
234
+ let module = Module::new(engine, AMBIENT_PROBE_WAT).expect("probe module must compile");
235
+ let instance = linker
236
+ .instantiate(&mut store, &module)
237
+ .expect("probe module must instantiate");
238
+
239
+ let read = |store: &mut WtStore<Invocation>, name: &str| -> i64 {
240
+ instance
241
+ .get_typed_func::<(), i64>(store.as_context_mut(), name)
242
+ .expect("probe export must resolve")
243
+ .call(store.as_context_mut(), ())
244
+ .expect("probe export must run")
245
+ };
246
+ let clock = read(&mut store, "clock_ns");
247
+ let random = read(&mut store, "random_word");
248
+ (clock, random)
249
+ }
250
+
251
+ #[test]
252
+ fn hermetic_denies_ambient_time_and_entropy() {
253
+ let (clock, random) = probe_ambient(Profile::Hermetic);
254
+ assert_eq!(
255
+ clock, 0,
256
+ "a hermetic guest's wasi:clocks must read the Unix epoch, not host time"
257
+ );
258
+ assert_eq!(
259
+ random, 0,
260
+ "a hermetic guest's wasi:random must yield the constant stream, not host entropy"
261
+ );
262
+ }
263
+
264
+ #[test]
265
+ fn permissive_grants_live_ambient_time_and_entropy() {
266
+ let (clock, random) = probe_ambient(Profile::Permissive);
267
+ assert!(
268
+ clock > 0,
269
+ "a permissive guest's wasi:clocks must read live host time"
270
+ );
271
+ assert_ne!(
272
+ random, 0,
273
+ "a permissive guest's wasi:random must yield host entropy"
274
+ );
275
+ }
276
+ }
data/data/kobako.wasm CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "kobako"
3
- version = "0.12.2"
3
+ version = "0.13.0"
4
4
  edition = "2021"
5
5
  authors = ["Aotokitsuruya <contact@aotoki.me>"]
6
6
  license = "Apache-2.0"
@@ -1,12 +1,10 @@
1
1
  use magnus::{Error, Ruby};
2
2
 
3
3
  mod runtime;
4
- mod snapshot;
5
4
 
6
5
  #[magnus::init]
7
6
  fn init(ruby: &Ruby) -> Result<(), Error> {
8
7
  let module = ruby.define_module("Kobako")?;
9
8
  runtime::init(ruby, module)?;
10
- snapshot::init(ruby, module)?;
11
9
  Ok(())
12
10
  }