kobako 0.13.0 → 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 +30 -0
- data/Cargo.lock +3 -3
- data/README.md +3 -3
- data/ROADMAP.md +26 -0
- data/crates/kobako-runtime/CHANGELOG.md +7 -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-wasmtime/CHANGELOG.md +7 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/driver.rs +1 -1
- data/crates/kobako-wasmtime/src/frames.rs +1 -1
- 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/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 +3 -3
- 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 +10 -12
- 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/release-please-config.json +37 -7
- 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/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 +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f89723d7b8cc8160176c299eaf28548ff9c7efda380ce545a7676d5d7f7cfdb6
|
|
4
|
+
data.tar.gz: 3494ee4ff50aef160504b53eb75b0192e9e9c7108d165db39b3903fb4e7343df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 26c982a047b82e32dcd4017ce603d6c4094bb6d4cd34f38e1df6eacba253cdb9589a8092fe0558e0ddcb64f9acacb4ef191f51916c37dffebf4f1ee0bc52a4fb
|
|
7
|
+
data.tar.gz: b593374ee6c18e132535813aa046a7059aca5a9f0d16c6b32519f8154a6d76b2fc5e7b2b4791e5213e26b7828748f64f3ec61b00029a9bb6cf0cc6336a48fdf0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.14.0","wasm/kobako-core":"0.8.0","wasm/kobako-mruby":"0.8.0","wasm/kobako-io":"0.8.0","wasm/kobako-json":"0.8.0","wasm/kobako-regexp":"0.8.0","wasm/kobako-baker":"0.8.0","crates/kobako-codec":"0.8.0","crates/kobako-runtime":"0.8.0","crates/kobako-wasmtime":"0.8.0","crates/kobako":"0.8.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/elct9620/kobako/compare/v0.13.0...v0.14.0) (2026-07-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **codec:** add the Run invocation envelope to the wire tier ([dbdd760](https://github.com/elct9620/kobako/commit/dbdd760ddc258681669f7f620f63b75f36322687))
|
|
9
|
+
* **crates:** add the kobako host SDK skeleton ([8a99d09](https://github.com/elct9620/kobako/commit/8a99d09ef7068a6738d44f1a735d39516b24156b))
|
|
10
|
+
* **crates:** add the parity runner to the kobako SDK ([998f059](https://github.com/elct9620/kobako/commit/998f059abd308ef921c295658aaf8377febb44e2))
|
|
11
|
+
* **crates:** grow the SDK capability-Handle table ([f93fe8f](https://github.com/elct9620/kobako/commit/f93fe8f3dce2509cfa527229f8f593f4d816b940))
|
|
12
|
+
* **crates:** grow the SDK Member block-yield seam ([4404713](https://github.com/elct9620/kobako/commit/44047130f309a2c935198077fb4f7f86839355e7))
|
|
13
|
+
* **crates:** grow the SDK preload and run invocation seams ([d8d5fe2](https://github.com/elct9620/kobako/commit/d8d5fe268a56d45dad4f8b35a25e942a559dcd5f))
|
|
14
|
+
* **crates:** honor the respond_to_guest narrowing on the SDK Member seam ([0f5eff1](https://github.com/elct9620/kobako/commit/0f5eff16a1e9f2229ad9d9c9316bf94e92035301))
|
|
15
|
+
* **crates:** let a resolved Handle recover its concrete member type ([abd5502](https://github.com/elct9620/kobako/commit/abd55029a44a7631d323c3aec3b625d9692f9c5b))
|
|
16
|
+
* **crates:** mark the SDK Error taxonomy non_exhaustive ([001fc69](https://github.com/elct9620/kobako/commit/001fc69e637d2c046f55cb517f9d9cf931793715))
|
|
17
|
+
* **examples:** add the guest→host dispatch half to the Rust host ([5cdde68](https://github.com/elct9620/kobako/commit/5cdde68824352c5685aaa94ae464e909705a4648))
|
|
18
|
+
* **examples:** assemble a minimal Rust host on the published crates ([6b98680](https://github.com/elct9620/kobako/commit/6b98680bcb625f9790f44df839035427a576e266))
|
|
19
|
+
* **tasks:** add a rails-stats-style rake stats size report ([c02d530](https://github.com/elct9620/kobako/commit/c02d530d43a85594323b7801fdbe75aa95ae7f8c))
|
|
20
|
+
* **tasks:** gate parity coverage over the CORE anchor manifest ([d26972c](https://github.com/elct9620/kobako/commit/d26972c1ad51e05df951643e17d66a6c811ba862))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **crates:** give SetupError a Display so Error::Setup reads cleanly ([bc8b128](https://github.com/elct9620/kobako/commit/bc8b128c9e296319c7fef47441b412d2ce345dff))
|
|
26
|
+
* **crates:** keep the no-timeout epoch deadline within range ([a3255df](https://github.com/elct9620/kobako/commit/a3255df98a77825bb39b571a60fe6ff83d269d19))
|
|
27
|
+
* **crates:** reject trailing bytes on Request and Run decode ([8e4929b](https://github.com/elct9620/kobako/commit/8e4929b64f3ea690f38211888070c2511da84754))
|
|
28
|
+
* **lib:** close the Data #with seam on Handle construction ([c0b50ae](https://github.com/elct9620/kobako/commit/c0b50aec659e32a72c905aaf94c939ac009ad621))
|
|
29
|
+
* **release:** resume the gem push loop past already-live versions ([170ee9b](https://github.com/elct9620/kobako/commit/170ee9bae6da3a65b4016040259e31e4721dfcfb))
|
|
30
|
+
* **release:** treat a yanked-only version as unpublished when publishing ([3111cc0](https://github.com/elct9620/kobako/commit/3111cc072a7c9d18b129fd2762f267d855b48d55))
|
|
31
|
+
* **transport:** fold fault messages to UTF-8 before they ride the wire ([0be8d40](https://github.com/elct9620/kobako/commit/0be8d4068dc5647f86746e80b356b0b9d52051f7))
|
|
32
|
+
|
|
3
33
|
## [0.13.0](https://github.com/elct9620/kobako/compare/v0.12.2...v0.13.0) (2026-07-03)
|
|
4
34
|
|
|
5
35
|
|
data/Cargo.lock
CHANGED
|
@@ -929,7 +929,7 @@ dependencies = [
|
|
|
929
929
|
|
|
930
930
|
[[package]]
|
|
931
931
|
name = "kobako"
|
|
932
|
-
version = "0.
|
|
932
|
+
version = "0.14.0"
|
|
933
933
|
dependencies = [
|
|
934
934
|
"kobako-runtime",
|
|
935
935
|
"kobako-wasmtime",
|
|
@@ -938,11 +938,11 @@ dependencies = [
|
|
|
938
938
|
|
|
939
939
|
[[package]]
|
|
940
940
|
name = "kobako-runtime"
|
|
941
|
-
version = "0.
|
|
941
|
+
version = "0.8.0"
|
|
942
942
|
|
|
943
943
|
[[package]]
|
|
944
944
|
name = "kobako-wasmtime"
|
|
945
|
-
version = "0.
|
|
945
|
+
version = "0.8.0"
|
|
946
946
|
dependencies = [
|
|
947
947
|
"kobako-runtime",
|
|
948
948
|
"libc",
|
data/README.md
CHANGED
|
@@ -302,10 +302,10 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
|
|
|
302
302
|
|
|
303
303
|
| Form | Signature | Snippet name source | Validation timing |
|
|
304
304
|
|----------|--------------------------------------|---------------------------------------|----------------------------------------------------------------------------|
|
|
305
|
-
| Source | `preload(code: "...", name: :Const)` | The `name:` keyword |
|
|
306
|
-
| 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` |
|
|
307
307
|
|
|
308
|
-
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.
|
|
309
309
|
|
|
310
310
|
## Security
|
|
311
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,12 @@
|
|
|
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
|
+
|
|
3
10
|
## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.1...kobako-runtime-v0.7.0) (2026-07-03)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -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,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.1...kobako-wasmtime-v0.7.0) (2026-07-03)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -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,7 +28,7 @@ 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`
|
|
@@ -225,7 +225,7 @@ mod tests {
|
|
|
225
225
|
profile,
|
|
226
226
|
};
|
|
227
227
|
let mut store = WtStore::new(engine, Invocation::new(None));
|
|
228
|
-
store.set_epoch_deadline(
|
|
228
|
+
store.set_epoch_deadline(crate::trap::NO_TIMEOUT_EPOCH_DELTA);
|
|
229
229
|
install_wasi_frames(&mut store, &config, &[]).expect("WASI context must install");
|
|
230
230
|
|
|
231
231
|
let mut linker: Linker<Invocation> = Linker::new(engine);
|
|
@@ -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
|
@@ -95,7 +95,7 @@ module Kobako
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# Guard
|
|
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
|
-
#
|
|
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)
|
|
@@ -71,7 +71,7 @@ module Kobako
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery.
|
|
74
|
-
# Routes through
|
|
74
|
+
# Routes through Kobako::Codec::Encoder like every other host-side
|
|
75
75
|
# wire encode so there is a single codec path; the preamble carries
|
|
76
76
|
# only Strings and Arrays, so none of the kobako ext types actually
|
|
77
77
|
# fire. Structure: +[["Namespace", ["MemberA", "MemberB"]], ...]+.
|
|
@@ -106,7 +106,7 @@ module Kobako
|
|
|
106
106
|
self
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
# Returns +true+ when
|
|
109
|
+
# Returns +true+ when #seal! has been called, +false+ otherwise.
|
|
110
110
|
def sealed?
|
|
111
111
|
@sealed
|
|
112
112
|
end
|
|
@@ -43,7 +43,7 @@ module Kobako
|
|
|
43
43
|
#
|
|
44
44
|
# The bytes are memoized — the table is replayed verbatim on every
|
|
45
45
|
# invocation after sealing, so Frame 3 never changes between
|
|
46
|
-
# encodes;
|
|
46
|
+
# encodes; #register drops the memo while the table is still open.
|
|
47
47
|
# Unlike +Catalog::Namespaces#encode+, which gates its memo on the
|
|
48
48
|
# seal, this one can fill eagerly and invalidate in +#register+
|
|
49
49
|
# because every mutation funnels through that single method — there is
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -12,23 +12,23 @@ module Kobako
|
|
|
12
12
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
|
|
13
13
|
#
|
|
14
14
|
# Translates msgpack gem exceptions into the kobako error taxonomy
|
|
15
|
-
# (
|
|
15
|
+
# (Truncated, InvalidType, InvalidEncoding, UnsupportedType) so
|
|
16
16
|
# callers can pattern-match on the SPEC's wire-violation categories
|
|
17
17
|
# without leaking the gem's internal exception classes.
|
|
18
18
|
#
|
|
19
|
-
# Public API is a single function —
|
|
19
|
+
# Public API is a single function — +.decode+. The decoder is
|
|
20
20
|
# stateless; the +MessagePack::Unpacker+ instance is built per call
|
|
21
21
|
# because callers always decode exactly one wire value at a time.
|
|
22
22
|
module Decoder
|
|
23
23
|
# Decode +bytes+ into one Ruby value and validate transitively
|
|
24
|
-
# against the SPEC type mapping. Raises
|
|
25
|
-
# or
|
|
24
|
+
# against the SPEC type mapping. Raises Truncated, InvalidType,
|
|
25
|
+
# or InvalidEncoding on wire violations.
|
|
26
26
|
#
|
|
27
27
|
# When a block is given, the decoded value is yielded and the block's
|
|
28
28
|
# result is returned — wire Value Objects use this to build themselves
|
|
29
29
|
# from the decoded payload. The block runs inside this method's
|
|
30
30
|
# rescue, so a Value Object's +ArgumentError+ invariant failure
|
|
31
|
-
# surfaces as
|
|
31
|
+
# surfaces as InvalidType without a separate Utils.with_boundary
|
|
32
32
|
# wrapper at the call site.
|
|
33
33
|
def self.decode(bytes)
|
|
34
34
|
value = Factory.load(bytes.b)
|
|
@@ -37,7 +37,7 @@ module Kobako
|
|
|
37
37
|
# msgpack gem raises the format/type errors below; +ArgumentError+
|
|
38
38
|
# comes from our ext-type validators (Handle id range, Exception type
|
|
39
39
|
# whitelist) and from a yielded block's Value Object invariants — both
|
|
40
|
-
# are wire violations, so both map to
|
|
40
|
+
# are wire violations, so both map to InvalidType.
|
|
41
41
|
rescue ::MessagePack::UnknownExtTypeError, ::MessagePack::MalformedFormatError,
|
|
42
42
|
::MessagePack::StackError, ::ArgumentError => e
|
|
43
43
|
raise InvalidType, e.message
|
|
@@ -53,7 +53,7 @@ module Kobako
|
|
|
53
53
|
# Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
|
|
54
54
|
# str family but does not validate the bytes; +bin+ family decodes
|
|
55
55
|
# to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
|
|
56
|
-
# str-typed leaf via
|
|
56
|
+
# str-typed leaf via Utils.assert_utf8!. Kobako::Fault
|
|
57
57
|
# payloads are validated transitively: +Factory.unpack_fault+
|
|
58
58
|
# feeds the inner ext-0x02 bytes back through this Decoder, so their
|
|
59
59
|
# +str+ fields are already covered by the time control returns here.
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -14,9 +14,9 @@ module Kobako
|
|
|
14
14
|
# strings, arrays, and maps go through the gem's narrowest-encoding
|
|
15
15
|
# logic; the three kobako-specific ext types (0x00 Symbol, 0x01
|
|
16
16
|
# Capability Handle, 0x02 Exception envelope) are registered on
|
|
17
|
-
# the cached
|
|
17
|
+
# the cached Kobako::Codec::Factory singleton.
|
|
18
18
|
#
|
|
19
|
-
# Public API is a single function —
|
|
19
|
+
# Public API is a single function — +.encode+. The codec is stateless;
|
|
20
20
|
# there is no buffer accumulator and no streaming write API. Callers
|
|
21
21
|
# that need to concatenate multiple encodings build the bytes
|
|
22
22
|
# themselves.
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -7,11 +7,11 @@ module Kobako
|
|
|
7
7
|
# The wire codec implements the binary contract pinned in
|
|
8
8
|
# {docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping.
|
|
9
9
|
# Every wire violation surfaces as a
|
|
10
|
-
# subclass of
|
|
10
|
+
# subclass of Error so callers can pattern-match on the specific
|
|
11
11
|
# fault while still rescuing all codec faults via this base class.
|
|
12
12
|
#
|
|
13
13
|
# Higher layers (e.g. the Sandbox dispatch loop) translate these into
|
|
14
|
-
# the public
|
|
14
|
+
# the public Kobako::SandboxError / Kobako::TrapError taxonomy.
|
|
15
15
|
class Error < StandardError; end
|
|
16
16
|
|
|
17
17
|
# Input ended before the type prefix or payload was fully consumed.
|
|
@@ -21,7 +21,7 @@ module Kobako
|
|
|
21
21
|
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag).
|
|
22
22
|
class InvalidType < Error; end
|
|
23
23
|
|
|
24
|
-
# A msgpack
|
|
24
|
+
# A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
|
|
25
25
|
# payload was not valid UTF-8 — both are wire violations per SPEC.
|
|
26
26
|
class InvalidEncoding < Error; end
|
|
27
27
|
|
data/lib/kobako/codec/factory.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Kobako
|
|
|
16
16
|
# § Ext Types).
|
|
17
17
|
#
|
|
18
18
|
# The factory is the single place in the host gem that touches the
|
|
19
|
-
# msgpack API — both
|
|
19
|
+
# msgpack API — both Encoder and Decoder delegate through it, so
|
|
20
20
|
# the three kobako ext codes (0x00 Symbol, 0x01 Capability Handle,
|
|
21
21
|
# 0x02 Exception envelope) are configured exactly once at first use.
|
|
22
22
|
#
|
|
@@ -93,11 +93,11 @@ module Kobako
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
# Validate the ext-0x00 payload as UTF-8 and intern. Raises
|
|
96
|
-
#
|
|
96
|
+
# InvalidEncoding on invalid bytes — SPEC forbids the
|
|
97
97
|
# binary-encoding fallback that msgpack-gem's default unpacker
|
|
98
98
|
# would otherwise apply. The re-tag step lives here because the
|
|
99
99
|
# msgpack ext-type unpacker hands us binary bytes; the assertion
|
|
100
|
-
# itself is shared with
|
|
100
|
+
# itself is shared with Decoder via Utils.assert_utf8!. The
|
|
101
101
|
# +"Symbol"+ label keeps the error message in Ruby vocabulary
|
|
102
102
|
# rather than wire-ext-code vocabulary.
|
|
103
103
|
def unpack_symbol(payload)
|
|
@@ -125,7 +125,7 @@ module Kobako
|
|
|
125
125
|
# Peel off the fixext-4 frame, hand the bytes to the
|
|
126
126
|
# Host-Gem-internal +Kobako::Handle.restore+ factory, and
|
|
127
127
|
# translate the +ArgumentError+ raised by Handle's invariants
|
|
128
|
-
# into a wire-layer +InvalidType+ via
|
|
128
|
+
# into a wire-layer +InvalidType+ via Codec::Utils.with_boundary.
|
|
129
129
|
# The Value Object owns the id-range contract; this method only
|
|
130
130
|
# owns the frame shape.
|
|
131
131
|
def unpack_handle(payload)
|
|
@@ -136,11 +136,11 @@ module Kobako
|
|
|
136
136
|
Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
# Encode the inner ext-0x02 map via
|
|
139
|
+
# Encode the inner ext-0x02 map via Encoder (not +factory.dump+) so
|
|
140
140
|
# the embedded payload flows through the same boundary as a top-level
|
|
141
141
|
# encode — nested kobako values (Handle, nested Fault) reach the
|
|
142
142
|
# registered ext-type packers via the cached singleton. A +details+
|
|
143
|
-
# chain nested past
|
|
143
|
+
# chain nested past MAX_EXT_DEPTH has no wire representation and
|
|
144
144
|
# surfaces as +UnsupportedType+.
|
|
145
145
|
def pack_fault(fault)
|
|
146
146
|
within_ext_frame(UnsupportedType) do
|
|
@@ -149,12 +149,12 @@ module Kobako
|
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
# Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
|
|
152
|
-
# inside
|
|
152
|
+
# inside Decoder.decode's block form, so the value-object's
|
|
153
153
|
# +ArgumentError+ invariants surface as +InvalidType+ through the
|
|
154
|
-
# decoder boundary. Inner decode goes through
|
|
154
|
+
# decoder boundary. Inner decode goes through Decoder (not
|
|
155
155
|
# +factory.load+) so the embedded +str+ payloads flow through the
|
|
156
156
|
# same UTF-8 validation as a top-level decode. A nested ext 0x02 in
|
|
157
|
-
# +details+ re-enters this method, so
|
|
157
|
+
# +details+ re-enters this method, so #within_ext_frame bounds the
|
|
158
158
|
# chain depth to keep it from exhausting the native stack.
|
|
159
159
|
def unpack_fault(payload)
|
|
160
160
|
within_ext_frame(InvalidType) do
|
|
@@ -167,7 +167,7 @@ module Kobako
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# Track ext-envelope re-entry depth and refuse a chain past
|
|
170
|
-
#
|
|
170
|
+
# MAX_EXT_DEPTH, raising +over_limit+ so the failure lands in the
|
|
171
171
|
# caller's existing wire-error class. The counter is thread-scoped and
|
|
172
172
|
# balanced by the +ensure+, so a raise mid-chain still unwinds it to
|
|
173
173
|
# its entry value.
|