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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +11 -0
  4. data/Cargo.lock +15 -2
  5. data/Cargo.toml +6 -2
  6. data/README.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +8 -0
  8. data/crates/kobako-runtime/Cargo.toml +23 -0
  9. data/crates/kobako-runtime/README.md +34 -0
  10. data/crates/kobako-runtime/src/dispatch.rs +22 -0
  11. data/crates/kobako-runtime/src/error.rs +64 -0
  12. data/crates/kobako-runtime/src/lib.rs +16 -0
  13. data/crates/kobako-runtime/src/runtime.rs +50 -0
  14. data/crates/kobako-runtime/src/snapshot.rs +46 -0
  15. data/crates/kobako-runtime/src/yielder.rs +22 -0
  16. data/crates/kobako-wasmtime/CHANGELOG.md +8 -0
  17. data/crates/kobako-wasmtime/Cargo.toml +62 -0
  18. data/crates/kobako-wasmtime/README.md +32 -0
  19. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/ambient.rs +3 -3
  20. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/cache.rs +30 -41
  21. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/capture.rs +2 -2
  22. data/crates/kobako-wasmtime/src/config.rs +25 -0
  23. data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
  24. data/crates/kobako-wasmtime/src/driver.rs +285 -0
  25. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
  26. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/frames.rs +70 -82
  27. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/guest_mem.rs +38 -15
  28. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/instance_pre.rs +13 -21
  29. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/invocation.rs +54 -49
  30. data/crates/kobako-wasmtime/src/lib.rs +47 -0
  31. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/trap.rs +29 -35
  32. data/data/kobako.wasm +0 -0
  33. data/ext/kobako/Cargo.toml +9 -32
  34. data/ext/kobako/src/runtime/bridge.rs +150 -0
  35. data/ext/kobako/src/runtime/errors.rs +45 -13
  36. data/ext/kobako/src/runtime.rs +156 -406
  37. data/ext/kobako/src/snapshot.rs +27 -62
  38. data/lib/kobako/catalog/handles.rb +3 -3
  39. data/lib/kobako/catalog/namespaces.rb +4 -0
  40. data/lib/kobako/catalog/snippets.rb +4 -0
  41. data/lib/kobako/codec/encoder.rb +5 -1
  42. data/lib/kobako/codec/factory.rb +41 -13
  43. data/lib/kobako/codec/handle_walk.rb +4 -0
  44. data/lib/kobako/errors.rb +18 -16
  45. data/lib/kobako/sandbox.rb +20 -18
  46. data/lib/kobako/sandbox_options.rb +25 -9
  47. data/lib/kobako/snapshot.rb +7 -13
  48. data/lib/kobako/transport/dispatcher.rb +2 -2
  49. data/lib/kobako/transport/response.rb +14 -14
  50. data/lib/kobako/transport/run.rb +2 -6
  51. data/lib/kobako/transport/yield.rb +1 -1
  52. data/lib/kobako/transport/yielder.rb +2 -2
  53. data/lib/kobako/version.rb +1 -1
  54. data/release-please-config.json +48 -3
  55. data/sig/kobako/codec/factory.rbs +3 -0
  56. data/sig/kobako/errors.rbs +7 -14
  57. data/sig/kobako/runtime.rbs +8 -3
  58. data/sig/kobako/sandbox.rbs +2 -2
  59. data/sig/kobako/sandbox_options.rbs +4 -2
  60. data/sig/kobako/snapshot.rbs +0 -3
  61. data/sig/kobako/transport/dispatcher.rbs +1 -1
  62. data/sig/kobako/transport/run.rbs +2 -2
  63. data/sig/kobako/transport/yielder.rbs +2 -2
  64. data/sig/kobako/transport.rbs +8 -0
  65. metadata +27 -12
  66. data/ext/kobako/src/runtime/config.rs +0 -25
  67. data/ext/kobako/src/runtime/dispatch.rs +0 -211
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b245ee235cc4309150a54ed8d5b8246216ebee7787fa103ebd70f7434797f3d8
4
- data.tar.gz: b2857e6f02527c476b32e381886d20d73571b3fe2224907aea868223e0b13fee
3
+ metadata.gz: 3b5515d639e59a612b4aa1008c6456bdc493152b725d89c36c8c51250368754e
4
+ data.tar.gz: 4fd2e14daaea5892b500cf8ba4d0fe94212d95e674a762afc482f22bd8a30f7d
5
5
  SHA512:
6
- metadata.gz: 8c612f87f0cb0665f4510f224eea4bd7a79cdae06df9aef287933296aba78b3ade6dc3849bdc3bc48dcccef1d8789125aac30ba265d33ce4f56dcbafa6635cd1
7
- data.tar.gz: f1191f5e65a1448b8ff675fe34de83052d1a1b5ed6757dd448b9cc119c3790d6f78184706fe09eddffef27521b81dd9f365d49ca2aa19615c2c5beb7f744289d
6
+ metadata.gz: 4676997989e6e7a6711c3f597b20a2db4a0ae2a50361d3ef66f235123741c043e936b73ae553484703cb5bf384ca9b3b42a6d4282bac94dba8ef3d018bf96130
7
+ data.tar.gz: 3b8573f8f67a79f00fd747961166835e89d735cdb02462bdb36ae399b1ceddd5cf3b38a1e82f0b22797f9d67b0a7c273a03bb5e4cb8b74806d0dadb41dcc0fb0
@@ -1 +1 @@
1
- {".":"0.12.1","wasm/kobako-core":"0.6.0","wasm/kobako":"0.6.0","wasm/kobako-io":"0.6.0","wasm/kobako-json":"0.6.0","wasm/kobako-regexp":"0.6.0","wasm/kobako-baker":"0.6.0"}
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.1"
925
+ version = "0.12.2"
926
926
  dependencies = [
927
- "libc",
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
- exclude = ["wasm", "vendor"]
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::HandlerExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
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,8 @@
1
+ # Changelog
2
+
3
+ ## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.0...kobako-runtime-v0.6.1) (2026-07-02)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **kobako-runtime:** Synchronize kobako crates versions
@@ -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,8 @@
1
+ # Changelog
2
+
3
+ ## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.0...kobako-wasmtime-v0.6.1) (2026-07-02)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **kobako-wasmtime:** Synchronize kobako crates versions
@@ -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(super) struct FrozenWallClock;
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(super) struct FrozenMonotonicClock;
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(super) fn deterministic_rng() -> Deterministic {
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" pins `ext/` as private and forbids
5
- //! exposing wasm engine types to the Host App or downstream gems. To
6
- //! amortise Engine creation and Module JIT compilation across multiple
7
- //! `Kobako::Sandbox` constructions, the ext keeps a process-scope
8
- //! shared Engine and a per-path Module cache. Both are transparent to
9
- //! Ruby callers, who construct a `Runtime` via
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 super::errors::{setup_err, MODULE_NOT_BUILT_ERROR};
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, MagnusError> {
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 = WtEngine::new(&config).map_err(|e| {
71
- let ruby = Ruby::get().expect("Ruby thread");
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. Raises `Kobako::ModuleNotBuiltError`
99
- /// when the file is missing — the headline error for the common
100
- /// pre-build state on a fresh clone before `rake compile`.
101
- pub(crate) fn cached_module(path: &Path) -> Result<WtModule, MagnusError> {
102
- let ruby = Ruby::get().expect("Ruby thread");
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(MagnusError::new(
116
- ruby.get_inner(&MODULE_NOT_BUILT_ERROR),
117
- format!(
118
- "Sandbox runtime not found at {}; run `bundle exec rake wasm:build` to build it",
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
- setup_err(
126
- &ruby,
127
- format!(
128
- "failed to read Sandbox runtime at {}: {}",
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).map_err(|e| {
140
- setup_err(&ruby, format!("failed to compile Sandbox runtime: {}", e))
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>-<gem version>.cwasm`.
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 gem-version segment keeps
165
- /// two installed kobako versions (each pinning its own wasmtime) from
166
- /// sharing a key and recompile-thrashing each other's entry. wasmtime
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.