kobako 0.12.2 → 0.14.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 +41 -0
- data/Cargo.lock +126 -137
- data/README.md +5 -3
- data/ROADMAP.md +26 -0
- data/crates/kobako-runtime/CHANGELOG.md +15 -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/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 +15 -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 +13 -1
- data/crates/kobako-wasmtime/src/frames.rs +105 -5
- 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 +42 -21
- 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/catalog/handles.rb +2 -2
- data/lib/kobako/catalog/namespaces.rb +2 -2
- data/lib/kobako/catalog/snippets.rb +1 -1
- data/lib/kobako/codec/decoder.rb +7 -7
- data/lib/kobako/codec/encoder.rb +2 -2
- data/lib/kobako/codec/error.rb +3 -3
- data/lib/kobako/codec/factory.rb +10 -10
- data/lib/kobako/codec/handle_walk.rb +17 -21
- data/lib/kobako/codec/utils.rb +9 -9
- data/lib/kobako/codec.rb +4 -4
- data/lib/kobako/errors.rb +14 -39
- data/lib/kobako/handle.rb +5 -2
- data/lib/kobako/namespace.rb +3 -3
- data/lib/kobako/outcome.rb +1 -0
- data/lib/kobako/sandbox.rb +61 -33
- data/lib/kobako/sandbox_options.rb +51 -9
- data/lib/kobako/transport/dispatcher.rb +17 -14
- data/lib/kobako/transport/request.rb +2 -2
- data/lib/kobako/transport/response.rb +2 -2
- data/lib/kobako/transport/run.rb +1 -1
- data/lib/kobako/transport/yield.rb +3 -3
- data/lib/kobako/transport/yielder.rb +6 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +0 -1
- data/release-please-config.json +66 -6
- data/sig/kobako/codec/handle_walk.rbs +2 -2
- data/sig/kobako/codec.rbs +11 -0
- data/sig/kobako/handle.rbs +0 -2
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +11 -2
- data/sig/kobako/transport/dispatcher.rbs +8 -8
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -0
- metadata +3 -4
- data/ext/kobako/src/snapshot.rs +0 -75
- data/lib/kobako/snapshot.rb +0 -32
- data/sig/kobako/snapshot.rbs +0 -12
data/README.md
CHANGED
|
@@ -154,6 +154,8 @@ sandbox = Kobako::Sandbox.new(
|
|
|
154
154
|
|
|
155
155
|
`memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget.
|
|
156
156
|
|
|
157
|
+
A fifth option, `profile:`, requests the Sandbox's isolation posture on the `:permissive` < `:hermetic` ladder (default `:hermetic`). `:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources — an explicit trade of reproducibility, with filesystem, environment, and network still unreachable. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/security-model.md`](docs/security-model.md) § Isolation profiles.
|
|
158
|
+
|
|
157
159
|
### Invocation Lifecycle
|
|
158
160
|
|
|
159
161
|
One Sandbox serves many invocations. Service bindings and preloaded snippets persist across calls; capability state (Handles, stdout, stderr, memory delta) resets between them.
|
|
@@ -300,10 +302,10 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
|
|
|
300
302
|
|
|
301
303
|
| Form | Signature | Snippet name source | Validation timing |
|
|
302
304
|
|----------|--------------------------------------|---------------------------------------|----------------------------------------------------------------------------|
|
|
303
|
-
| Source | `preload(code: "...", name: :Const)` | The `name:` keyword |
|
|
304
|
-
| Bytecode | `preload(binary: bytes)` | Read from the bytecode's `debug_info` |
|
|
305
|
+
| Source | `preload(code: "...", name: :Const)` | The `name:` keyword | First invocation's replay; compile errors raise `Kobako::SandboxError` |
|
|
306
|
+
| Bytecode | `preload(binary: bytes)` | Read from the bytecode's `debug_info` | First invocation's replay; structural failure raises `Kobako::BytecodeError` |
|
|
305
307
|
|
|
306
|
-
Use the source form for snippets authored in your repo
|
|
308
|
+
Use the source form for snippets authored in your repo; use the bytecode form when snippets ship as build artifacts from a separate `mrbc` pipeline. Both replay through the same per-invocation path, so no snippet content failure surfaces at `#preload` — force the first replay with a no-op invocation (e.g. `sandbox.eval("nil")`) when you want validation before real traffic.
|
|
307
309
|
|
|
308
310
|
## Security
|
|
309
311
|
|
data/ROADMAP.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
kobako is a Ruby gem providing an in-process Wasm sandbox for untrusted mruby
|
|
4
|
+
code: a wasmtime host runs a precompiled `kobako.wasm` guest, with host↔guest
|
|
5
|
+
Transport over a MessagePack wire. Features cover one-shot `#eval`, preload +
|
|
6
|
+
`#run` dispatch, Service injection through Namespaces / Members, opaque
|
|
7
|
+
Capability Handles, block yield re-entry, three-class error attribution,
|
|
8
|
+
output capture, and a warm Sandbox pool.
|
|
9
|
+
|
|
10
|
+
| Feature | Entry Points | Notes |
|
|
11
|
+
|---------|-------------|-------|
|
|
12
|
+
| ✅ [F-01 Sandbox instantiation](docs/behavior/lifecycle.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | B-22 (per-Thread isolation) is exercised only indirectly through the pool contention tests; no test cites it |
|
|
13
|
+
| ✅ [F-02 Namespace declaration](docs/behavior/registration.md) | [lib/kobako/catalog/namespaces.rb](lib/kobako/catalog/namespaces.rb) | — |
|
|
14
|
+
| ✅ [F-03 Member binding](docs/behavior/registration.md) | [lib/kobako/namespace.rb](lib/kobako/namespace.rb) | — |
|
|
15
|
+
| ✅ [F-04 Synchronous mruby source execution (`#eval`)](docs/behavior/lifecycle.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
|
|
16
|
+
| ✅ [F-05 Guest-initiated Transport dispatch](docs/behavior/dispatch.md) | [lib/kobako/transport/dispatcher.rb](lib/kobako/transport/dispatcher.rb) | — |
|
|
17
|
+
| ✅ [F-06 Capability Handle encoding and referencing](docs/behavior/dispatch.md) | [lib/kobako/catalog/handles.rb](lib/kobako/catalog/handles.rb) | — |
|
|
18
|
+
| ✅ [F-07 Three-class error attribution and raising](docs/behavior/errors.md) | [lib/kobako/outcome.rb](lib/kobako/outcome.rb) | E-26 (guest-entry envelope decode failure) has no exercising test — not reachable through the public API |
|
|
19
|
+
| ✅ [F-08 Guest output capture](docs/behavior/lifecycle.md) | [lib/kobako/capture.rb](lib/kobako/capture.rb) | — |
|
|
20
|
+
| ✅ [F-09 Host–guest message codec](docs/wire-codec.md) | [lib/kobako/codec/](lib/kobako/codec/) | — |
|
|
21
|
+
| ✅ [F-10 Reproducible build pipeline](SPEC.md#code-organization) | [tasks/wasm/build.rake](tasks/wasm/build.rake) | Verified by build-time gates (`rake anchors`, double-bake byte-identity, gemspec whitelist), not `test/` |
|
|
22
|
+
| ✅ [F-11 Multi-layer test and benchmark suite](SPEC.md#testing-style) | [test/](test/) | Benchmarks live in [benchmark/](benchmark/) with the gate in `tasks/bench/`; the anchor baseline advances only by deliberate re-bless |
|
|
23
|
+
| ✅ [F-12 Guest block reception and yield re-entry](docs/behavior/yield.md) | [lib/kobako/transport/yielder.rb](lib/kobako/transport/yielder.rb) | — |
|
|
24
|
+
| ✅ [F-13 Snippet preloading (`#preload`)](docs/behavior/invocation.md) | [lib/kobako/catalog/snippets.rb](lib/kobako/catalog/snippets.rb) | — |
|
|
25
|
+
| ✅ [F-14 Synchronous entrypoint dispatch (`#run`)](docs/behavior/invocation.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
|
|
26
|
+
| ✅ [F-15 Warm Sandbox pool checkout (`Kobako::Pool`)](docs/behavior/runtime.md) | [lib/kobako/pool.rb](lib/kobako/pool.rb) | — |
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.7.0...kobako-runtime-v0.8.0) (2026-07-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **crates:** give SetupError a Display so Error::Setup reads cleanly ([bc8b128](https://github.com/elct9620/kobako/commit/bc8b128c9e296319c7fef47441b412d2ce345dff))
|
|
9
|
+
|
|
10
|
+
## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.1...kobako-runtime-v0.7.0) (2026-07-03)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
|
|
16
|
+
* **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
|
|
17
|
+
|
|
3
18
|
## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.0...kobako-runtime-v0.6.1) (2026-07-02)
|
|
4
19
|
|
|
5
20
|
|
|
@@ -62,3 +62,11 @@ impl fmt::Display for Trap {
|
|
|
62
62
|
f.write_str(msg)
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
impl fmt::Display for SetupError {
|
|
67
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
68
|
+
let (SetupError::ModuleNotBuilt(msg) | SetupError::Dead(msg) | SetupError::Intact(msg)) =
|
|
69
|
+
self;
|
|
70
|
+
f.write_str(msg)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -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,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.7.0...kobako-wasmtime-v0.8.0) (2026-07-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **crates:** keep the no-timeout epoch deadline within range ([a3255df](https://github.com/elct9620/kobako/commit/a3255df98a77825bb39b571a60fe6ff83d269d19))
|
|
9
|
+
|
|
10
|
+
## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.1...kobako-wasmtime-v0.7.0) (2026-07-03)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
|
|
16
|
+
* **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
|
|
17
|
+
|
|
3
18
|
## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.0...kobako-wasmtime-v0.6.1) (2026-07-02)
|
|
4
19
|
|
|
5
20
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
[package]
|
|
17
17
|
name = "kobako-wasmtime"
|
|
18
|
-
version = "0.
|
|
18
|
+
version = "0.8.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.8.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
|
|
|
@@ -182,7 +183,7 @@ impl Driver {
|
|
|
182
183
|
}
|
|
183
184
|
None => {
|
|
184
185
|
store.data_mut().set_deadline(None);
|
|
185
|
-
store.set_epoch_deadline(
|
|
186
|
+
store.set_epoch_deadline(trap::NO_TIMEOUT_EPOCH_DELTA);
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
let baseline = match exports.memory {
|
|
@@ -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(crate::trap::NO_TIMEOUT_EPOCH_DELTA);
|
|
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
|
+
}
|
|
@@ -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,43 +16,48 @@ 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
45
|
/// Configured-cap path classification for a wasmtime error. The
|
|
37
|
-
/// downcast logic stays in a pure helper so the
|
|
38
|
-
///
|
|
39
|
-
/// `Kobako::TrapError` mapping can be exercised from `cargo test`
|
|
40
|
-
/// without the magnus surface.
|
|
46
|
+
/// downcast logic stays in a pure helper so the `Trap` kind routing
|
|
47
|
+
/// can be exercised from `cargo test` without any frontend.
|
|
41
48
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
42
49
|
enum TrapClass {
|
|
43
50
|
/// Wall-clock cap path.
|
|
44
51
|
Timeout,
|
|
45
52
|
/// Linear-memory cap path.
|
|
46
53
|
MemoryLimit,
|
|
47
|
-
/// Any other wasmtime error — surfaces as the base
|
|
48
|
-
/// `Kobako::TrapError`.
|
|
54
|
+
/// Any other wasmtime error — surfaces as the base `Trap::Other`.
|
|
49
55
|
Other,
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
/// Inspect a wasmtime error to decide which
|
|
53
|
-
///
|
|
54
|
-
///
|
|
58
|
+
/// Inspect a wasmtime error to decide which neutral trap kind it
|
|
59
|
+
/// belongs to. Pure function — operates on the error's downcast chain
|
|
60
|
+
/// only, no frontend state required.
|
|
55
61
|
fn classify_trap(err: &wasmtime::Error) -> TrapClass {
|
|
56
62
|
if err.downcast_ref::<TimeoutTrap>().is_some() {
|
|
57
63
|
TrapClass::Timeout
|
|
@@ -123,8 +129,23 @@ pub(crate) fn instantiate_err(err: wasmtime::Error) -> SetupError {
|
|
|
123
129
|
|
|
124
130
|
#[cfg(test)]
|
|
125
131
|
mod tests {
|
|
126
|
-
use super::{classify_trap, other_trap_message, TrapClass};
|
|
127
|
-
use crate::invocation::{MemoryLimitTrap, TimeoutTrap};
|
|
132
|
+
use super::{classify_trap, other_trap_message, TrapClass, NO_TIMEOUT_EPOCH_DELTA};
|
|
133
|
+
use crate::invocation::{Invocation, MemoryLimitTrap, TimeoutTrap};
|
|
134
|
+
|
|
135
|
+
// The no-timeout priming delta is added to the engine's current
|
|
136
|
+
// epoch inside wasmtime, and the process-wide ticker advances that
|
|
137
|
+
// epoch from the first `shared_engine` call on — so the sum must
|
|
138
|
+
// stay in range for a long-lived engine, not just a fresh one.
|
|
139
|
+
// `increment_epoch` stands in for the ticker to make the ticked
|
|
140
|
+
// state deterministic; under debug overflow checks an overflowing
|
|
141
|
+
// delta panics right here.
|
|
142
|
+
#[test]
|
|
143
|
+
fn no_timeout_delta_survives_a_ticked_engine_epoch() {
|
|
144
|
+
let engine = crate::cache::shared_engine().expect("shared engine must be constructible");
|
|
145
|
+
engine.increment_epoch();
|
|
146
|
+
let mut store = wasmtime::Store::new(engine, Invocation::new(None));
|
|
147
|
+
store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
|
|
148
|
+
}
|
|
128
149
|
|
|
129
150
|
#[test]
|
|
130
151
|
fn classify_trap_routes_timeout_trap_to_timeout() {
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/ext/kobako/Cargo.toml
CHANGED
data/ext/kobako/src/lib.rs
CHANGED