kobako 0.12.1 → 0.12.2
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 +15 -2
- data/Cargo.toml +6 -2
- data/README.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +8 -0
- data/crates/kobako-runtime/Cargo.toml +23 -0
- data/crates/kobako-runtime/README.md +34 -0
- data/crates/kobako-runtime/src/dispatch.rs +22 -0
- data/crates/kobako-runtime/src/error.rs +64 -0
- data/crates/kobako-runtime/src/lib.rs +16 -0
- data/crates/kobako-runtime/src/runtime.rs +50 -0
- data/crates/kobako-runtime/src/snapshot.rs +46 -0
- data/crates/kobako-runtime/src/yielder.rs +22 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +8 -0
- data/crates/kobako-wasmtime/Cargo.toml +62 -0
- data/crates/kobako-wasmtime/README.md +32 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/ambient.rs +3 -3
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/cache.rs +30 -41
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/capture.rs +2 -2
- data/crates/kobako-wasmtime/src/config.rs +25 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
- data/crates/kobako-wasmtime/src/driver.rs +285 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/frames.rs +70 -82
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/guest_mem.rs +38 -15
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/instance_pre.rs +13 -21
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/invocation.rs +54 -49
- data/crates/kobako-wasmtime/src/lib.rs +47 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/trap.rs +29 -35
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +9 -32
- data/ext/kobako/src/runtime/bridge.rs +150 -0
- data/ext/kobako/src/runtime/errors.rs +45 -13
- data/ext/kobako/src/runtime.rs +156 -406
- data/ext/kobako/src/snapshot.rs +27 -62
- data/lib/kobako/catalog/handles.rb +3 -3
- data/lib/kobako/catalog/namespaces.rb +4 -0
- data/lib/kobako/catalog/snippets.rb +4 -0
- data/lib/kobako/codec/encoder.rb +5 -1
- data/lib/kobako/codec/factory.rb +41 -13
- data/lib/kobako/codec/handle_walk.rb +4 -0
- data/lib/kobako/errors.rb +18 -16
- data/lib/kobako/sandbox.rb +20 -18
- data/lib/kobako/sandbox_options.rb +25 -9
- data/lib/kobako/snapshot.rb +7 -13
- data/lib/kobako/transport/dispatcher.rb +2 -2
- data/lib/kobako/transport/response.rb +14 -14
- data/lib/kobako/transport/run.rb +2 -6
- data/lib/kobako/transport/yield.rb +1 -1
- data/lib/kobako/transport/yielder.rb +2 -2
- data/lib/kobako/version.rb +1 -1
- data/release-please-config.json +48 -3
- data/sig/kobako/codec/factory.rbs +3 -0
- data/sig/kobako/errors.rbs +7 -14
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/sandbox_options.rbs +4 -2
- data/sig/kobako/snapshot.rbs +0 -3
- data/sig/kobako/transport/dispatcher.rbs +1 -1
- data/sig/kobako/transport/run.rbs +2 -2
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -0
- metadata +27 -12
- data/ext/kobako/src/runtime/config.rs +0 -25
- data/ext/kobako/src/runtime/dispatch.rs +0 -211
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b5515d639e59a612b4aa1008c6456bdc493152b725d89c36c8c51250368754e
|
|
4
|
+
data.tar.gz: 4fd2e14daaea5892b500cf8ba4d0fe94212d95e674a762afc482f22bd8a30f7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4676997989e6e7a6711c3f597b20a2db4a0ae2a50361d3ef66f235123741c043e936b73ae553484703cb5bf384ca9b3b42a6d4282bac94dba8ef3d018bf96130
|
|
7
|
+
data.tar.gz: 3b8573f8f67a79f00fd747961166835e89d735cdb02462bdb36ae399b1ceddd5cf3b38a1e82f0b22797f9d67b0a7c273a03bb5e4cb8b74806d0dadb41dcc0fb0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.12.
|
|
1
|
+
{".":"0.12.2","wasm/kobako-core":"0.6.1","wasm/kobako":"0.6.1","wasm/kobako-io":"0.6.1","wasm/kobako-json":"0.6.1","wasm/kobako-regexp":"0.6.1","wasm/kobako-baker":"0.6.1","crates/kobako-runtime":"0.6.1","crates/kobako-wasmtime":"0.6.1"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.2](https://github.com/elct9620/kobako/compare/v0.12.1...v0.12.2) (2026-07-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **codec:** bound ext-envelope nesting to keep deep Fault chains off the native stack ([7bed2b2](https://github.com/elct9620/kobako/commit/7bed2b2f43a63538aa60610c82d2eb65bcce7b15))
|
|
9
|
+
* **guest:** size collection conversions by C array length, not #length ([90ecbd0](https://github.com/elct9620/kobako/commit/90ecbd0cb6a990b8c5a1e5deec3a10df4eaa37df))
|
|
10
|
+
* **io:** enforce the fd allowlist at the write syscall ([1b300df](https://github.com/elct9620/kobako/commit/1b300df7bee8f87b701f76b42300163a8899b93e))
|
|
11
|
+
* **release:** advance last-release-sha past the unparseable fork merge ([d06117d](https://github.com/elct9620/kobako/commit/d06117d462eea0ae5648e5bdd6886b735765f3b3))
|
|
12
|
+
* **sandbox:** honor nil to disable the output caps, and validate them ([51d1e90](https://github.com/elct9620/kobako/commit/51d1e900e3d7d659ffd432ff3d613786e9073b05))
|
|
13
|
+
|
|
3
14
|
## [0.12.1](https://github.com/elct9620/kobako/compare/v0.12.0...v0.12.1) (2026-06-27)
|
|
4
15
|
|
|
5
16
|
|
data/Cargo.lock
CHANGED
|
@@ -922,10 +922,23 @@ dependencies = [
|
|
|
922
922
|
|
|
923
923
|
[[package]]
|
|
924
924
|
name = "kobako"
|
|
925
|
-
version = "0.12.
|
|
925
|
+
version = "0.12.2"
|
|
926
926
|
dependencies = [
|
|
927
|
-
"
|
|
927
|
+
"kobako-runtime",
|
|
928
|
+
"kobako-wasmtime",
|
|
928
929
|
"magnus",
|
|
930
|
+
]
|
|
931
|
+
|
|
932
|
+
[[package]]
|
|
933
|
+
name = "kobako-runtime"
|
|
934
|
+
version = "0.6.1"
|
|
935
|
+
|
|
936
|
+
[[package]]
|
|
937
|
+
name = "kobako-wasmtime"
|
|
938
|
+
version = "0.6.1"
|
|
939
|
+
dependencies = [
|
|
940
|
+
"kobako-runtime",
|
|
941
|
+
"libc",
|
|
929
942
|
"sha2 0.11.0",
|
|
930
943
|
"wasmtime",
|
|
931
944
|
"wasmtime-wasi",
|
data/Cargo.toml
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
members = ["./ext/kobako"]
|
|
7
7
|
# `wasm/` is a sibling workspace (kobako-wasm crate) compiled for
|
|
8
8
|
# wasm32-wasip1; excluding it keeps the host (wasmtime) and guest
|
|
9
|
-
# dependency graphs separate.
|
|
10
|
-
|
|
9
|
+
# dependency graphs separate. `crates/` is the native host-side crate
|
|
10
|
+
# workspace; excluding it lets the ext consume its members as plain
|
|
11
|
+
# path dependencies resolved as standalone packages — the same shape
|
|
12
|
+
# the packaged gem sees, which ships the crates without their
|
|
13
|
+
# workspace manifest.
|
|
14
|
+
exclude = ["wasm", "vendor", "crates"]
|
|
11
15
|
resolver = "2"
|
|
12
16
|
|
|
13
17
|
# Strip the local-symbol table from the shipped ext (~22% of the binary).
|
data/README.md
CHANGED
|
@@ -127,7 +127,7 @@ end
|
|
|
127
127
|
|---------------------------------|----------------|------------------------------------------------------|
|
|
128
128
|
| `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
|
|
129
129
|
| `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
|
|
130
|
-
| `Kobako::
|
|
130
|
+
| `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
|
|
131
131
|
| `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
|
|
132
132
|
|
|
133
133
|
`SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` / `details` fields when the guest produced a panic envelope.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# kobako-runtime — engine-neutral host runtime contract.
|
|
2
|
+
#
|
|
3
|
+
# The contract a wasm engine must satisfy to drive a kobako guest,
|
|
4
|
+
# free of any engine or frontend type: the `Runtime` trait plus the
|
|
5
|
+
# neutral value types (`Snapshot`, `Trap`, `SetupError`, ...) and the
|
|
6
|
+
# dispatch / yield re-entry traits a frontend supplies. Engine
|
|
7
|
+
# implementations (`kobako-wasmtime`) and host frontends (the Ruby
|
|
8
|
+
# ext) meet on this surface, so the engine stays swappable.
|
|
9
|
+
#
|
|
10
|
+
# Self-contained by design: the Ruby gem ships this crate without the
|
|
11
|
+
# `crates/` workspace manifest, so no `workspace = true` inheritance
|
|
12
|
+
# fields may appear here.
|
|
13
|
+
|
|
14
|
+
[package]
|
|
15
|
+
name = "kobako-runtime"
|
|
16
|
+
version = "0.6.1"
|
|
17
|
+
edition = "2021"
|
|
18
|
+
description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
|
|
19
|
+
license = "Apache-2.0"
|
|
20
|
+
repository = "https://github.com/elct9620/kobako"
|
|
21
|
+
readme = "README.md"
|
|
22
|
+
keywords = ["wasm", "mruby", "sandbox", "wasi"]
|
|
23
|
+
categories = ["wasm", "virtualization"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# kobako-runtime
|
|
2
|
+
|
|
3
|
+
Engine-neutral host runtime contract for
|
|
4
|
+
[kobako](https://github.com/elct9620/kobako), an in-process Wasm
|
|
5
|
+
sandbox for running untrusted mruby scripts.
|
|
6
|
+
|
|
7
|
+
A kobako host drives a Guest Binary through a wasm engine; this crate
|
|
8
|
+
is the surface where the two meet, free of any engine or frontend
|
|
9
|
+
type, so the engine stays swappable:
|
|
10
|
+
|
|
11
|
+
- `runtime` — the `Runtime` trait: one guest invocation on a fresh
|
|
12
|
+
instance in, its observable `Snapshot` out
|
|
13
|
+
- `snapshot` — the per-invocation observables: `Completion` (outcome
|
|
14
|
+
or trap), the two output `Capture`s, and resource `Usage`, uniform
|
|
15
|
+
across success and trap
|
|
16
|
+
- `error` — the neutral failure channels: `Trap` (engine fault) and
|
|
17
|
+
`SetupError` (the invocation never started)
|
|
18
|
+
- `dispatch` / `yielder` — the `DispatchHandler` and `Yielder` traits
|
|
19
|
+
a frontend supplies for guest→host dispatch and block-yield re-entry
|
|
20
|
+
|
|
21
|
+
Engine implementations (such as `kobako-wasmtime`) implement
|
|
22
|
+
`Runtime`; host frontends (such as the kobako Ruby gem's native ext)
|
|
23
|
+
map the neutral types onto their own language surface.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```toml
|
|
28
|
+
[dependencies]
|
|
29
|
+
kobako-runtime = "0.6.1" # x-release-please-version
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
Apache-2.0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//! Engine-neutral guest→host dispatch contract, free of any `magnus`
|
|
2
|
+
//! dependency.
|
|
3
|
+
//!
|
|
4
|
+
//! The wasm runtime hands a handler the raw Request bytes a guest produced
|
|
5
|
+
//! and expects raw Response bytes back. What the handler *is* — a Ruby Proc,
|
|
6
|
+
//! a Rust closure — is the frontend's concern; the runtime only sees this
|
|
7
|
+
//! trait. The Ruby ext conforms by bridging its dispatch Proc behind it.
|
|
8
|
+
|
|
9
|
+
use crate::yielder::Yielder;
|
|
10
|
+
|
|
11
|
+
/// Host-side handler for a guest→host dispatch.
|
|
12
|
+
///
|
|
13
|
+
/// `dispatch` receives the request bytes plus a `Yielder` for re-entering
|
|
14
|
+
/// the in-flight guest when a Service method yields to a block, and returns
|
|
15
|
+
/// the raw Response bytes — or `None` when the handler itself failed, in
|
|
16
|
+
/// which case the runtime walks its 0-return wire-fault path. The bound
|
|
17
|
+
/// handler is contracted to fold application failures into a `Response.err`
|
|
18
|
+
/// envelope, so `None` signals a contract violation (the handler raised)
|
|
19
|
+
/// rather than a normal dispatch outcome.
|
|
20
|
+
pub trait DispatchHandler: Send + Sync {
|
|
21
|
+
fn dispatch(&self, request: &[u8], yielder: &mut dyn Yielder) -> Option<Vec<u8>>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//! Engine-neutral host error channels, free of any frontend dependency.
|
|
2
|
+
//!
|
|
3
|
+
//! The run path produces these instead of constructing host-language
|
|
4
|
+
//! exceptions directly; each frontend's boundary is the single place that
|
|
5
|
+
//! maps them onto its own error classes (the Ruby ext does so in its error
|
|
6
|
+
//! mapper). Keeping the channels frontend-free lets any engine
|
|
7
|
+
//! implementation produce them unchanged.
|
|
8
|
+
|
|
9
|
+
use std::fmt;
|
|
10
|
+
|
|
11
|
+
/// A guest invocation that faulted in the wasm engine, or a host-detected
|
|
12
|
+
/// runtime corruption during invocation, classified into the host-facing
|
|
13
|
+
/// kinds a frontend surfaces distinctly: the wall-clock cap (`Timeout`),
|
|
14
|
+
/// the linear-memory cap (`MemoryLimit`), and every other engine fault
|
|
15
|
+
/// (`Other`).
|
|
16
|
+
#[derive(Debug)]
|
|
17
|
+
pub enum Trap {
|
|
18
|
+
Timeout(String),
|
|
19
|
+
MemoryLimit(String),
|
|
20
|
+
Other(String),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// A failure that yields no invocation outcome. The discriminant records
|
|
24
|
+
/// the runtime's state so a frontend can attribute the failure per SPEC:
|
|
25
|
+
/// `ModuleNotBuilt` (the guest artifact is absent), `Dead` (the runtime
|
|
26
|
+
/// could not be constructed), and `Intact` (the runtime is live but a
|
|
27
|
+
/// host-side pre-call step failed, so no discard-and-recreate recovery is
|
|
28
|
+
/// owed).
|
|
29
|
+
#[derive(Debug)]
|
|
30
|
+
pub enum SetupError {
|
|
31
|
+
ModuleNotBuilt(String),
|
|
32
|
+
Dead(String),
|
|
33
|
+
Intact(String),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// A failure that prevented the invocation from starting — a pre-call
|
|
37
|
+
/// engine fault (`Trap`) or a host-side setup fault (`Setup`) — unified so
|
|
38
|
+
/// the run mechanics can propagate both with `?`; a frontend destructures
|
|
39
|
+
/// it back into the two channels. Faults after the guest export starts
|
|
40
|
+
/// ride in `Completion::Trap` instead, so captures and usage survive them.
|
|
41
|
+
#[derive(Debug)]
|
|
42
|
+
pub enum Error {
|
|
43
|
+
Trap(Trap),
|
|
44
|
+
Setup(SetupError),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl From<Trap> for Error {
|
|
48
|
+
fn from(trap: Trap) -> Self {
|
|
49
|
+
Error::Trap(trap)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl From<SetupError> for Error {
|
|
54
|
+
fn from(err: SetupError) -> Self {
|
|
55
|
+
Error::Setup(err)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
impl fmt::Display for Trap {
|
|
60
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
61
|
+
let (Trap::Timeout(msg) | Trap::MemoryLimit(msg) | Trap::Other(msg)) = self;
|
|
62
|
+
f.write_str(msg)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//! kobako-runtime — engine-neutral host runtime contract.
|
|
2
|
+
//!
|
|
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.
|
|
6
|
+
//! Nothing here depends on an engine or a frontend type — each engine
|
|
7
|
+
//! hides its own machinery behind `Runtime`, and each frontend maps
|
|
8
|
+
//! these shapes onto its own host-language surface at its boundary
|
|
9
|
+
//! (for the Ruby ext that is the error mapper in its runtime module),
|
|
10
|
+
//! so the engine stays swappable.
|
|
11
|
+
|
|
12
|
+
pub mod dispatch;
|
|
13
|
+
pub mod error;
|
|
14
|
+
pub mod runtime;
|
|
15
|
+
pub mod snapshot;
|
|
16
|
+
pub mod yielder;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//! Engine-neutral host runtime contract.
|
|
2
|
+
//!
|
|
3
|
+
//! The contract a wasm engine must satisfy to drive a kobako guest: take a
|
|
4
|
+
//! per-invocation entry plus its stdin frames, run one invocation on a
|
|
5
|
+
//! fresh instance, and return the observable `Snapshot` — `Ok` iff the
|
|
6
|
+
//! guest export ran, `Err` when the invocation never started. Nothing here
|
|
7
|
+
//! mentions `magnus` or a Ruby type — a frontend supplies the dispatch
|
|
8
|
+
//! handler, the contract only borrows it.
|
|
9
|
+
|
|
10
|
+
use std::sync::Arc;
|
|
11
|
+
|
|
12
|
+
use crate::dispatch::DispatchHandler;
|
|
13
|
+
use crate::error::Error;
|
|
14
|
+
use crate::snapshot::Snapshot;
|
|
15
|
+
|
|
16
|
+
/// The per-invocation entry: a one-shot mruby source (`Eval`) or an
|
|
17
|
+
/// entrypoint-dispatch envelope (`Run`). Both ride alongside the stdin
|
|
18
|
+
/// `Frames`; `Run` additionally copies its envelope into guest memory.
|
|
19
|
+
pub enum Entry<'a> {
|
|
20
|
+
Eval { source: &'a [u8] },
|
|
21
|
+
Run { envelope: &'a [u8] },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The stdin frames shared by both entries: the Frame 1 preamble (the
|
|
25
|
+
/// Sandbox's registrations) and the Frame 3 snippet-replay payload.
|
|
26
|
+
pub struct Frames<'a> {
|
|
27
|
+
pub preamble: &'a [u8],
|
|
28
|
+
pub snippets: &'a [u8],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Engine-neutral runtime: drives one guest invocation on a fresh instance
|
|
32
|
+
/// and returns its observable `Snapshot`. `Ok` means the guest export ran
|
|
33
|
+
/// — the `Snapshot` carries the completion (outcome or trap), captures,
|
|
34
|
+
/// and usage uniformly; `Err` means the invocation never started.
|
|
35
|
+
///
|
|
36
|
+
/// Safety contract for `handler`: the runtime only *borrows* the handler
|
|
37
|
+
/// for the duration of `invoke` and never roots it. A frontend whose
|
|
38
|
+
/// handler references a GC-managed object (e.g. a Ruby `Proc`) must keep
|
|
39
|
+
/// that object alive — and, under a moving GC, pinned — for the whole call.
|
|
40
|
+
/// The Ruby ext does this by holding the `Proc` on its long-lived Runtime
|
|
41
|
+
/// wrapper and GC-marking it; the runtime itself touches no frontend
|
|
42
|
+
/// value.
|
|
43
|
+
pub trait Runtime {
|
|
44
|
+
fn invoke(
|
|
45
|
+
&self,
|
|
46
|
+
entry: Entry<'_>,
|
|
47
|
+
frames: Frames<'_>,
|
|
48
|
+
handler: Option<Arc<dyn DispatchHandler>>,
|
|
49
|
+
) -> Result<Snapshot, Error>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//! Engine-neutral, frontend-free per-invocation observable bundle.
|
|
2
|
+
//!
|
|
3
|
+
//! The observables of a single guest invocation, expressed without any
|
|
4
|
+
//! frontend type and uniform across success and trap: how the invocation
|
|
5
|
+
//! completed, the two captured output channels, and the resource usage.
|
|
6
|
+
//! A `Snapshot` exists iff the guest export ran — a failure to even start
|
|
7
|
+
//! the invocation travels on the `invoke` `Err` channel instead.
|
|
8
|
+
|
|
9
|
+
use crate::error::Trap;
|
|
10
|
+
|
|
11
|
+
/// One captured output channel: the bytes the guest wrote (already clipped
|
|
12
|
+
/// to the channel's cap) and whether the cap was reached.
|
|
13
|
+
pub struct Capture {
|
|
14
|
+
pub bytes: Vec<u8>,
|
|
15
|
+
pub truncated: bool,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// How the guest invocation completed: `Outcome` carries the
|
|
19
|
+
/// OUTCOME_BUFFER bytes the guest returned; `Trap` is an engine fault
|
|
20
|
+
/// after the export call started, kept as a value so the rest of the
|
|
21
|
+
/// `Snapshot` survives it.
|
|
22
|
+
pub enum Completion {
|
|
23
|
+
Outcome(Vec<u8>),
|
|
24
|
+
Trap(Trap),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Resource usage of one guest invocation, measured across the same
|
|
28
|
+
/// bracket as the wall-clock / memory caps: `wall_time` is the seconds
|
|
29
|
+
/// spent inside the guest export call; `memory_peak` is the high-water
|
|
30
|
+
/// `memory.grow` delta in bytes past the entry-time baseline.
|
|
31
|
+
#[derive(Clone, Copy)]
|
|
32
|
+
pub struct Usage {
|
|
33
|
+
pub wall_time: f64,
|
|
34
|
+
pub memory_peak: usize,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// The observables of one guest invocation, uniform across completion
|
|
38
|
+
/// kinds: captures and usage are collected on trap and outcome alike.
|
|
39
|
+
/// What a frontend exposes from the trap path is its own contract
|
|
40
|
+
/// decision.
|
|
41
|
+
pub struct Snapshot {
|
|
42
|
+
pub completion: Completion,
|
|
43
|
+
pub stdout: Capture,
|
|
44
|
+
pub stderr: Capture,
|
|
45
|
+
pub usage: Usage,
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//! Engine-neutral block-yield re-entry contract, free of `magnus` and of
|
|
2
|
+
//! any wasmtime type.
|
|
3
|
+
//!
|
|
4
|
+
//! During a guest→host dispatch, a Service method may yield to a guest
|
|
5
|
+
//! block. The host drives that re-entry through a `Yielder`: it ships the
|
|
6
|
+
//! yield-arguments payload into the in-flight guest and returns the
|
|
7
|
+
//! YieldResponse bytes. What backs the re-entry — a wasmtime `Caller`, some
|
|
8
|
+
//! other engine handle — is the implementer's concern; the dispatch
|
|
9
|
+
//! contract sees only this trait.
|
|
10
|
+
|
|
11
|
+
use crate::error::Trap;
|
|
12
|
+
|
|
13
|
+
/// Host-initiated re-entry into the in-flight guest instance to run a
|
|
14
|
+
/// yielded block.
|
|
15
|
+
///
|
|
16
|
+
/// `yield_block` ships `args` to `__kobako_yield_to_block` and returns the
|
|
17
|
+
/// raw YieldResponse bytes, or a `Trap` — surfaced through the frontend's
|
|
18
|
+
/// trap-error mapping — when the re-entry traps, the guest returns an empty
|
|
19
|
+
/// result, or a payload exceeds the 16 MiB cap.
|
|
20
|
+
pub trait Yielder {
|
|
21
|
+
fn yield_block(&mut self, args: &[u8]) -> Result<Vec<u8>, Trap>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# kobako-wasmtime — the wasmtime implementation of the kobako runtime
|
|
2
|
+
# contract.
|
|
3
|
+
#
|
|
4
|
+
# The engine half of a kobako host: `Driver` implements
|
|
5
|
+
# `kobako_runtime::runtime::Runtime` over wasmtime, owning every
|
|
6
|
+
# engine-bound mechanic — the process-wide Engine + compiled-Module
|
|
7
|
+
# caches, the pre-linked `InstancePre`, the epoch wall-clock timeout,
|
|
8
|
+
# the linear-memory cap, and the frozen-clock / constant-RNG ambient
|
|
9
|
+
# denial. Frontends (the kobako Ruby gem's native ext) construct a
|
|
10
|
+
# `Driver` and drive invocations through the contract trait only.
|
|
11
|
+
#
|
|
12
|
+
# Self-contained by design: the Ruby gem ships this crate without the
|
|
13
|
+
# `crates/` workspace manifest, so no `workspace = true` inheritance
|
|
14
|
+
# fields may appear here.
|
|
15
|
+
|
|
16
|
+
[package]
|
|
17
|
+
name = "kobako-wasmtime"
|
|
18
|
+
version = "0.6.1"
|
|
19
|
+
edition = "2021"
|
|
20
|
+
description = "wasmtime implementation of the kobako host runtime contract."
|
|
21
|
+
license = "Apache-2.0"
|
|
22
|
+
repository = "https://github.com/elct9620/kobako"
|
|
23
|
+
readme = "README.md"
|
|
24
|
+
keywords = ["wasm", "mruby", "sandbox", "wasi", "wasmtime"]
|
|
25
|
+
categories = ["wasm", "virtualization"]
|
|
26
|
+
|
|
27
|
+
[dependencies]
|
|
28
|
+
# The engine-neutral contract this crate implements. The version pin
|
|
29
|
+
# rides the linked release group; the path keeps in-tree builds (and
|
|
30
|
+
# the Ruby gem, which ships both crates) resolving locally.
|
|
31
|
+
kobako-runtime = { version = "0.6.1", path = "../kobako-runtime" }
|
|
32
|
+
# wasmtime — host-side embedder for kobako.wasm. We disable default-features
|
|
33
|
+
# and opt back in only what kobako needs: a Cranelift-backed runtime that can
|
|
34
|
+
# compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
|
|
35
|
+
# feature so test fixtures can be expressed as text.
|
|
36
|
+
# `cache` / `parallel-compilation` / `pooling` / `component-model` / `async`
|
|
37
|
+
# are intentionally off — kobako runs short-lived synchronous sandboxes.
|
|
38
|
+
wasmtime = { version = "45.0.0", default-features = false, features = [
|
|
39
|
+
"cranelift",
|
|
40
|
+
"runtime",
|
|
41
|
+
"gc",
|
|
42
|
+
"gc-drc",
|
|
43
|
+
"addr2line",
|
|
44
|
+
"demangle",
|
|
45
|
+
"wat",
|
|
46
|
+
] }
|
|
47
|
+
# wasmtime-wasi provides WASI preview1 support for routing guest stdout/stderr
|
|
48
|
+
# into in-memory buffers. The `p1` feature enables the
|
|
49
|
+
# WasiCtxBuilder + preview1 adapter which wires fd 1/2 to pipes. We omit
|
|
50
|
+
# `p2` (component-model) and `p0`/`p3` (async) because kobako runs
|
|
51
|
+
# synchronous sandboxes only.
|
|
52
|
+
wasmtime-wasi = { version = "45.0.0", default-features = false, features = ["p1"] }
|
|
53
|
+
# sha2 keys the on-disk compiled-module cache by Guest Binary content
|
|
54
|
+
# (see cache.rs); a collision would load the wrong artifact, so the
|
|
55
|
+
# hash must be cryptographic.
|
|
56
|
+
sha2 = "0.11"
|
|
57
|
+
|
|
58
|
+
# libc supplies geteuid for the cache-directory ownership check gating
|
|
59
|
+
# the unsafe artifact deserialize (see cache.rs); std exposes a file's
|
|
60
|
+
# owner but not the process's effective uid.
|
|
61
|
+
[target.'cfg(unix)'.dependencies]
|
|
62
|
+
libc = "0.2"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# kobako-wasmtime
|
|
2
|
+
|
|
3
|
+
The [wasmtime](https://wasmtime.dev) implementation of the
|
|
4
|
+
[kobako](https://github.com/elct9620/kobako) host runtime contract
|
|
5
|
+
([`kobako-runtime`](https://crates.io/crates/kobako-runtime)).
|
|
6
|
+
|
|
7
|
+
`Driver` implements the contract's `Runtime` trait over wasmtime and
|
|
8
|
+
owns every engine-bound mechanic, so frontends see only the neutral
|
|
9
|
+
contract surface:
|
|
10
|
+
|
|
11
|
+
- process-wide Engine and compiled-Module caches with an on-disk AOT
|
|
12
|
+
(`.cwasm`) artifact cache keyed by Guest Binary content
|
|
13
|
+
- a pre-linked `InstancePre` per guest path; every invocation runs on
|
|
14
|
+
a fresh instance and discards its Store afterwards
|
|
15
|
+
- the epoch-based wall-clock timeout and the per-invocation
|
|
16
|
+
linear-memory cap
|
|
17
|
+
- ambient denial: frozen WASI clocks and a constant RNG, so a guest
|
|
18
|
+
observes no real time and no real entropy
|
|
19
|
+
|
|
20
|
+
The kobako Ruby gem's native ext is the first frontend; a Rust host
|
|
21
|
+
SDK consumes the same surface.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```toml
|
|
26
|
+
[dependencies]
|
|
27
|
+
kobako-wasmtime = "0.6.1" # x-release-please-version
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
Apache-2.0
|
|
@@ -19,7 +19,7 @@ use wasmtime_wasi::random::Deterministic;
|
|
|
19
19
|
use wasmtime_wasi::{HostMonotonicClock, HostWallClock};
|
|
20
20
|
|
|
21
21
|
/// Wall clock frozen at the Unix epoch — the guest observes no real time.
|
|
22
|
-
pub(
|
|
22
|
+
pub(crate) struct FrozenWallClock;
|
|
23
23
|
|
|
24
24
|
impl HostWallClock for FrozenWallClock {
|
|
25
25
|
fn resolution(&self) -> Duration {
|
|
@@ -32,7 +32,7 @@ impl HostWallClock for FrozenWallClock {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/// Monotonic clock frozen at zero — the guest observes no elapsed time.
|
|
35
|
-
pub(
|
|
35
|
+
pub(crate) struct FrozenMonotonicClock;
|
|
36
36
|
|
|
37
37
|
impl HostMonotonicClock for FrozenMonotonicClock {
|
|
38
38
|
fn resolution(&self) -> u64 {
|
|
@@ -46,7 +46,7 @@ impl HostMonotonicClock for FrozenMonotonicClock {
|
|
|
46
46
|
|
|
47
47
|
/// Constant-stream RNG handed to the guest's `wasi:random`, so a guest that
|
|
48
48
|
/// reaches `random_get` receives no host entropy.
|
|
49
|
-
pub(
|
|
49
|
+
pub(crate) fn deterministic_rng() -> Deterministic {
|
|
50
50
|
Deterministic::new(vec![0])
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
//! Process-wide caches for the wasmtime `Engine` and compiled
|
|
2
2
|
//! `Module`, plus the on-disk compiled-artifact cache.
|
|
3
3
|
//!
|
|
4
|
-
//! SPEC.md "Code Organization"
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
7
|
-
//!
|
|
8
|
-
//!
|
|
9
|
-
//!
|
|
10
|
-
//! `Kobako::Runtime.from_path(...)` and never see Engine or Module.
|
|
4
|
+
//! SPEC.md "Code Organization" forbids exposing wasm engine types to
|
|
5
|
+
//! the Host App or downstream gems. To amortise Engine creation and
|
|
6
|
+
//! Module JIT compilation across multiple sandbox constructions, the
|
|
7
|
+
//! driver keeps a process-scope shared Engine and a per-path Module
|
|
8
|
+
//! cache. Both are transparent to frontends, which construct a
|
|
9
|
+
//! `Driver` via `Driver::new` and never see Engine or Module.
|
|
11
10
|
//!
|
|
12
11
|
//! Across processes, the Cranelift compile cost is amortised by a
|
|
13
12
|
//! best-effort `.cwasm` disk cache keyed by the SHA-256 of the Guest
|
|
@@ -27,11 +26,10 @@ use std::sync::{Mutex, OnceLock};
|
|
|
27
26
|
use std::thread;
|
|
28
27
|
use std::time::{Duration, SystemTime};
|
|
29
28
|
|
|
30
|
-
use magnus::{Error as MagnusError, Ruby};
|
|
31
29
|
use sha2::{Digest, Sha256};
|
|
32
30
|
use wasmtime::{Config as WtConfig, Engine as WtEngine, Module as WtModule};
|
|
33
31
|
|
|
34
|
-
use
|
|
32
|
+
use kobako_runtime::error::SetupError;
|
|
35
33
|
|
|
36
34
|
static SHARED_ENGINE: OnceLock<WtEngine> = OnceLock::new();
|
|
37
35
|
static MODULE_CACHE: OnceLock<Mutex<HashMap<PathBuf, WtModule>>> = OnceLock::new();
|
|
@@ -60,17 +58,15 @@ const EPOCH_TICK: Duration = Duration::from_millis(10);
|
|
|
60
58
|
/// cap. The first call spawns the process-singleton ticker
|
|
61
59
|
/// thread that drives `engine.increment_epoch()` at `EPOCH_TICK`
|
|
62
60
|
/// cadence; subsequent calls reuse the same engine and ticker.
|
|
63
|
-
pub(crate) fn shared_engine() -> Result<&'static WtEngine,
|
|
61
|
+
pub(crate) fn shared_engine() -> Result<&'static WtEngine, SetupError> {
|
|
64
62
|
if let Some(engine) = SHARED_ENGINE.get() {
|
|
65
63
|
return Ok(engine);
|
|
66
64
|
}
|
|
67
65
|
let mut config = WtConfig::new();
|
|
68
66
|
config.wasm_exceptions(true);
|
|
69
67
|
config.epoch_interruption(true);
|
|
70
|
-
let engine =
|
|
71
|
-
|
|
72
|
-
setup_err(&ruby, format!("engine init: {}", e))
|
|
73
|
-
})?;
|
|
68
|
+
let engine =
|
|
69
|
+
WtEngine::new(&config).map_err(|e| SetupError::Dead(format!("engine init: {e}")))?;
|
|
74
70
|
let engine = SHARED_ENGINE.get_or_init(|| engine);
|
|
75
71
|
spawn_epoch_ticker(engine.clone());
|
|
76
72
|
Ok(engine)
|
|
@@ -95,11 +91,11 @@ fn spawn_epoch_ticker(engine: WtEngine) {
|
|
|
95
91
|
}
|
|
96
92
|
|
|
97
93
|
/// Look up `path` in the per-path Module cache, compiling and inserting
|
|
98
|
-
/// the artifact on a miss.
|
|
99
|
-
/// when the file is missing —
|
|
100
|
-
/// pre-build state on a fresh clone
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
/// the artifact on a miss. Returns `SetupError::ModuleNotBuilt`
|
|
95
|
+
/// (boundary → `Kobako::ModuleNotBuiltError`) when the file is missing —
|
|
96
|
+
/// the headline error for the common pre-build state on a fresh clone
|
|
97
|
+
/// before `rake compile`.
|
|
98
|
+
pub(crate) fn cached_module(path: &Path) -> Result<WtModule, SetupError> {
|
|
103
99
|
let cache = MODULE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
|
104
100
|
|
|
105
101
|
if let Some(module) = cache
|
|
@@ -112,33 +108,25 @@ pub(crate) fn cached_module(path: &Path) -> Result<WtModule, MagnusError> {
|
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
if !path.exists() {
|
|
115
|
-
return Err(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
path.display()
|
|
120
|
-
),
|
|
121
|
-
));
|
|
111
|
+
return Err(SetupError::ModuleNotBuilt(format!(
|
|
112
|
+
"Sandbox runtime not found at {}; run `bundle exec rake wasm:build` to build it",
|
|
113
|
+
path.display()
|
|
114
|
+
)));
|
|
122
115
|
}
|
|
123
116
|
|
|
124
117
|
let bytes = fs::read(path).map_err(|e| {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
path.display(),
|
|
130
|
-
e
|
|
131
|
-
),
|
|
132
|
-
)
|
|
118
|
+
SetupError::Dead(format!(
|
|
119
|
+
"failed to read Sandbox runtime at {}: {e}",
|
|
120
|
+
path.display()
|
|
121
|
+
))
|
|
133
122
|
})?;
|
|
134
123
|
let engine = shared_engine()?;
|
|
135
124
|
let artifact = artifact_path(&bytes);
|
|
136
125
|
let module = match artifact.as_deref().and_then(|p| load_artifact(engine, p)) {
|
|
137
126
|
Some(module) => module,
|
|
138
127
|
None => {
|
|
139
|
-
let module = WtModule::new(engine, &bytes)
|
|
140
|
-
|
|
141
|
-
})?;
|
|
128
|
+
let module = WtModule::new(engine, &bytes)
|
|
129
|
+
.map_err(|e| SetupError::Dead(format!("failed to compile Sandbox runtime: {e}")))?;
|
|
142
130
|
if let Some(p) = artifact.as_deref() {
|
|
143
131
|
store_artifact(&module, p);
|
|
144
132
|
}
|
|
@@ -159,11 +147,12 @@ const ARTIFACT_TTL: Duration = Duration::from_secs(30 * 24 * 60 * 60);
|
|
|
159
147
|
|
|
160
148
|
/// Compute the disk-cache location for a Guest Binary's compiled
|
|
161
149
|
/// artifact: `$XDG_CACHE_HOME/kobako` (falling back to
|
|
162
|
-
/// `~/.cache/kobako`) `/<sha256 of the wasm bytes>-<
|
|
150
|
+
/// `~/.cache/kobako`) `/<sha256 of the wasm bytes>-<crate version>.cwasm`.
|
|
163
151
|
/// Content addressing makes a rebuilt Guest Binary a new cache entry
|
|
164
|
-
/// rather than an invalidation problem; the
|
|
165
|
-
/// two installed kobako versions (each pinning its own
|
|
166
|
-
/// sharing a key and recompile-thrashing each other's
|
|
152
|
+
/// rather than an invalidation problem; the crate-version segment keeps
|
|
153
|
+
/// two installed kobako-wasmtime versions (each pinning its own
|
|
154
|
+
/// wasmtime) from sharing a key and recompile-thrashing each other's
|
|
155
|
+
/// entry. wasmtime
|
|
167
156
|
/// itself rejects an artifact produced by an incompatible wasmtime
|
|
168
157
|
/// version or Config at deserialize time. Returns `None` when no home
|
|
169
158
|
/// directory is available — the caller then just compiles in-process.
|