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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +11 -0
- data/Cargo.lock +126 -137
- data/README.md +2 -0
- data/crates/kobako-runtime/CHANGELOG.md +8 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/lib.rs +4 -2
- data/crates/kobako-runtime/src/profile.rs +35 -0
- data/crates/kobako-runtime/src/runtime.rs +7 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +8 -0
- data/crates/kobako-wasmtime/Cargo.toml +4 -4
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/ambient.rs +5 -2
- data/crates/kobako-wasmtime/src/config.rs +10 -2
- data/crates/kobako-wasmtime/src/driver.rs +12 -0
- data/crates/kobako-wasmtime/src/frames.rs +105 -5
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +0 -2
- data/ext/kobako/src/runtime.rs +114 -38
- data/lib/kobako/codec.rb +1 -1
- data/lib/kobako/sandbox.rb +58 -28
- data/lib/kobako/sandbox_options.rb +51 -9
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +0 -1
- data/release-please-config.json +31 -1
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +11 -2
- metadata +2 -4
- data/ext/kobako/src/snapshot.rs +0 -75
- data/lib/kobako/snapshot.rb +0 -32
- data/sig/kobako/snapshot.rbs +0 -12
|
@@ -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
|
|
|
@@ -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
|
|
5
|
-
//!
|
|
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.
|
|
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.
|
|
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 = "
|
|
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 = "
|
|
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.
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
//!
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
builder.
|
|
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
|
data/ext/kobako/Cargo.toml
CHANGED
data/ext/kobako/src/lib.rs
CHANGED