kobako 0.12.1-aarch64-linux → 0.13.0-aarch64-linux
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 +22 -0
- data/README.md +3 -1
- data/crates/kobako-runtime/CHANGELOG.md +16 -0
- data/crates/kobako-runtime/README.md +34 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +16 -0
- data/crates/kobako-wasmtime/README.md +32 -0
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- 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/codec.rb +1 -1
- data/lib/kobako/errors.rb +18 -16
- data/lib/kobako/sandbox.rb +71 -39
- data/lib/kobako/sandbox_options.rb +71 -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/lib/kobako.rb +0 -1
- data/release-please-config.json +78 -3
- data/sig/kobako/codec/factory.rbs +3 -0
- data/sig/kobako/errors.rbs +7 -14
- data/sig/kobako/runtime.rbs +16 -6
- data/sig/kobako/sandbox.rbs +11 -7
- data/sig/kobako/sandbox_options.rbs +15 -4
- 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 +6 -4
- data/lib/kobako/snapshot.rb +0 -38
- data/sig/kobako/snapshot.rbs +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0a1b4ccd9f4e1debe32912b925847390d00fbf09e798b41eb54ac0756ef4ab3
|
|
4
|
+
data.tar.gz: 4819002a95e4407ebacb91d1ae77cd8a7c52d80a1eb0511dcc01ac405d6b7459
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 902954ca6a47611ab670ea52dbe5edc2268428128ffaac0dd9d55e309af6d5b4a70fc47d07eed4f31261bdcdc7158cbdd91fbfad32d56098c8a2c84077db2855
|
|
7
|
+
data.tar.gz: 53174e7d4e921b558bca17b3683c0865954ff73219a0c07d86d11e18e6e812506aece5d5dc0626ce021fe9342a63d8d3b5ab612f513d7349e939e0e6e08ed063
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.13.0","wasm/kobako-core":"0.7.0","wasm/kobako":"0.7.0","wasm/kobako-io":"0.7.0","wasm/kobako-json":"0.7.0","wasm/kobako-regexp":"0.7.0","wasm/kobako-baker":"0.7.0","crates/kobako-codec":"0.7.0","crates/kobako-runtime":"0.7.0","crates/kobako-wasmtime":"0.7.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/elct9620/kobako/compare/v0.12.2...v0.13.0) (2026-07-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
|
|
9
|
+
* enforce the isolation-profile floor at Sandbox construction ([73f0dfe](https://github.com/elct9620/kobako/commit/73f0dfe34a144045a559a28caec31468351de64a))
|
|
10
|
+
* forward the Sandbox profile request through the ext to the driver ([f47f906](https://github.com/elct9620/kobako/commit/f47f9063aa56462be023f5c76cbc73db6e4315ad))
|
|
11
|
+
* keep guest output readable after a trap ([464454c](https://github.com/elct9620/kobako/commit/464454cd774db96ad72b3787007f2ce015673587))
|
|
12
|
+
* **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
|
|
13
|
+
|
|
14
|
+
## [0.12.2](https://github.com/elct9620/kobako/compare/v0.12.1...v0.12.2) (2026-07-02)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **codec:** bound ext-envelope nesting to keep deep Fault chains off the native stack ([7bed2b2](https://github.com/elct9620/kobako/commit/7bed2b2f43a63538aa60610c82d2eb65bcce7b15))
|
|
20
|
+
* **guest:** size collection conversions by C array length, not #length ([90ecbd0](https://github.com/elct9620/kobako/commit/90ecbd0cb6a990b8c5a1e5deec3a10df4eaa37df))
|
|
21
|
+
* **io:** enforce the fd allowlist at the write syscall ([1b300df](https://github.com/elct9620/kobako/commit/1b300df7bee8f87b701f76b42300163a8899b93e))
|
|
22
|
+
* **release:** advance last-release-sha past the unparseable fork merge ([d06117d](https://github.com/elct9620/kobako/commit/d06117d462eea0ae5648e5bdd6886b735765f3b3))
|
|
23
|
+
* **sandbox:** honor nil to disable the output caps, and validate them ([51d1e90](https://github.com/elct9620/kobako/commit/51d1e900e3d7d659ffd432ff3d613786e9073b05))
|
|
24
|
+
|
|
3
25
|
## [0.12.1](https://github.com/elct9620/kobako/compare/v0.12.0...v0.12.1) (2026-06-27)
|
|
4
26
|
|
|
5
27
|
|
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.
|
|
@@ -154,6 +154,8 @@ sandbox = Kobako::Sandbox.new(
|
|
|
154
154
|
|
|
155
155
|
`memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget.
|
|
156
156
|
|
|
157
|
+
A fifth option, `profile:`, requests the Sandbox's isolation posture on the `:permissive` < `:hermetic` ladder (default `:hermetic`). `:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources — an explicit trade of reproducibility, with filesystem, environment, and network still unreachable. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/security-model.md`](docs/security-model.md) § Isolation profiles.
|
|
158
|
+
|
|
157
159
|
### Invocation Lifecycle
|
|
158
160
|
|
|
159
161
|
One Sandbox serves many invocations. Service bindings and preloaded snippets persist across calls; capability state (Handles, stdout, stderr, memory delta) resets between them.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.1...kobako-runtime-v0.7.0) (2026-07-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
|
|
9
|
+
* **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
|
|
10
|
+
|
|
11
|
+
## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.0...kobako-runtime-v0.6.1) (2026-07-02)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Miscellaneous Chores
|
|
15
|
+
|
|
16
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
@@ -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.7.0" # x-release-please-version
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
Apache-2.0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.1...kobako-wasmtime-v0.7.0) (2026-07-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
|
|
9
|
+
* **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
|
|
10
|
+
|
|
11
|
+
## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.0...kobako-wasmtime-v0.6.1) (2026-07-02)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Miscellaneous Chores
|
|
15
|
+
|
|
16
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
@@ -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.7.0" # x-release-please-version
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
Apache-2.0
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/lib/kobako/3.3/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/3.4/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/4.0/kobako.so
CHANGED
|
Binary file
|
|
@@ -38,7 +38,7 @@ module Kobako
|
|
|
38
38
|
# for it. +object+ is any host-side Ruby object to bind. Returns a
|
|
39
39
|
# freshly-allocated +Kobako::Handle+ whose +#id+ falls in
|
|
40
40
|
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
41
|
-
# +Kobako::
|
|
41
|
+
# +Kobako::HandleExhaustedError+ if the next ID would exceed the
|
|
42
42
|
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
43
43
|
# and the allocator share the same invariant.
|
|
44
44
|
#
|
|
@@ -96,12 +96,12 @@ module Kobako
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
# Guard {#alloc} against issuing an ID past the cap. Returns +nil+
|
|
99
|
-
# on success; raises +Kobako::
|
|
99
|
+
# on success; raises +Kobako::HandleExhaustedError+ at exhaustion.
|
|
100
100
|
def ensure_capacity!
|
|
101
101
|
cap = Kobako::Handle::MAX_ID
|
|
102
102
|
return unless @next_id > cap
|
|
103
103
|
|
|
104
|
-
raise
|
|
104
|
+
raise HandleExhaustedError,
|
|
105
105
|
"Out of handle allocations: too many host objects were referenced " \
|
|
106
106
|
"in a single invocation (limit #{cap})"
|
|
107
107
|
end
|
|
@@ -82,6 +82,10 @@ module Kobako
|
|
|
82
82
|
# first invocation, so the preamble is exactly the bindings that
|
|
83
83
|
# existed at that moment — a bind reaching a +Kobako::Namespace+
|
|
84
84
|
# after the seal raises +ArgumentError+ and never alters Frame 1.
|
|
85
|
+
# The memo is gated on the seal rather than dropped per mutation (the
|
|
86
|
+
# +Catalog::Snippets#encode+ approach) because a +Member+ bind lands
|
|
87
|
+
# on a child +Kobako::Namespace+, invisible to this collection; only
|
|
88
|
+
# the seal guarantees nothing further can change.
|
|
85
89
|
def encode
|
|
86
90
|
return @encoded if @encoded
|
|
87
91
|
|
|
@@ -44,6 +44,10 @@ module Kobako
|
|
|
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
46
|
# encodes; {#register} drops the memo while the table is still open.
|
|
47
|
+
# Unlike +Catalog::Namespaces#encode+, which gates its memo on the
|
|
48
|
+
# seal, this one can fill eagerly and invalidate in +#register+
|
|
49
|
+
# because every mutation funnels through that single method — there is
|
|
50
|
+
# no out-of-sight child object to change the result behind its back.
|
|
47
51
|
def encode
|
|
48
52
|
return @encoded if @encoded
|
|
49
53
|
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -26,7 +26,11 @@ module Kobako
|
|
|
26
26
|
# mapping is a closed set, and anything outside it is rejected by
|
|
27
27
|
# the msgpack gem itself (arbitrary objects raise +NoMethodError+
|
|
28
28
|
# from missing +to_msgpack+, integers outside i64..u64 raise
|
|
29
|
-
# +RangeError+).
|
|
29
|
+
# +RangeError+). The +NoMethodError+ catch is deliberately broad:
|
|
30
|
+
# MessagePack signals "no wire representation" only through that error,
|
|
31
|
+
# so there is no narrower discriminator — a packer-internal
|
|
32
|
+
# +NoMethodError+ is likewise reported as +UnsupportedType+ rather than
|
|
33
|
+
# propagating.
|
|
30
34
|
def self.encode(value)
|
|
31
35
|
Factory.dump(value)
|
|
32
36
|
rescue ::RangeError, ::NoMethodError => e
|
data/lib/kobako/codec/factory.rb
CHANGED
|
@@ -48,6 +48,16 @@ module Kobako
|
|
|
48
48
|
EXT_ERRENV = 0x02
|
|
49
49
|
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
50
50
|
|
|
51
|
+
# An ext 0x02 (Fault) envelope nests through its +details+ field, and
|
|
52
|
+
# each level re-enters the codec with a fresh +MessagePack+ unpacker
|
|
53
|
+
# whose built-in stack guard resets — so ext-envelope depth is tracked
|
|
54
|
+
# here instead. The cap matches the wire's overall nesting bound and
|
|
55
|
+
# keeps a nested chain from exhausting the native stack: an over-deep
|
|
56
|
+
# chain fails as a clean wire error, never a stack-level trap.
|
|
57
|
+
MAX_EXT_DEPTH = 128
|
|
58
|
+
EXT_DEPTH_KEY = :__kobako_codec_ext_depth__
|
|
59
|
+
private_constant :MAX_EXT_DEPTH, :EXT_DEPTH_KEY
|
|
60
|
+
|
|
51
61
|
# Instance-level pass-through onto the wrapped +MessagePack::Factory+.
|
|
52
62
|
# Spelled +def_instance_delegators+ rather than +def_delegators+ because
|
|
53
63
|
# the class also extends +SingleForwardable+ (see the +extend+ block
|
|
@@ -129,9 +139,13 @@ module Kobako
|
|
|
129
139
|
# Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
|
|
130
140
|
# the embedded payload flows through the same boundary as a top-level
|
|
131
141
|
# encode — nested kobako values (Handle, nested Fault) reach the
|
|
132
|
-
# registered ext-type packers via the cached singleton.
|
|
142
|
+
# registered ext-type packers via the cached singleton. A +details+
|
|
143
|
+
# chain nested past {MAX_EXT_DEPTH} has no wire representation and
|
|
144
|
+
# surfaces as +UnsupportedType+.
|
|
133
145
|
def pack_fault(fault)
|
|
134
|
-
|
|
146
|
+
within_ext_frame(UnsupportedType) do
|
|
147
|
+
Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
|
|
148
|
+
end
|
|
135
149
|
end
|
|
136
150
|
|
|
137
151
|
# Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
|
|
@@ -139,19 +153,33 @@ module Kobako
|
|
|
139
153
|
# +ArgumentError+ invariants surface as +InvalidType+ through the
|
|
140
154
|
# decoder boundary. Inner decode goes through {Decoder} (not
|
|
141
155
|
# +factory.load+) so the embedded +str+ payloads flow through the
|
|
142
|
-
# same UTF-8 validation as a top-level decode.
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
# singleton instance feeds +Decoder.decode+, which re-enters this
|
|
146
|
-
# method when a nested ext 0x02 appears inside +details+. The recursion
|
|
147
|
-
# is bounded by msgpack nesting depth — identical to nested Array /
|
|
148
|
-
# Hash payloads — so no extra guard is needed. Do not switch back to
|
|
149
|
-
# +factory.load+ to "simplify": that path bypasses UTF-8 validation.
|
|
156
|
+
# same UTF-8 validation as a top-level decode. A nested ext 0x02 in
|
|
157
|
+
# +details+ re-enters this method, so {#within_ext_frame} bounds the
|
|
158
|
+
# chain depth to keep it from exhausting the native stack.
|
|
150
159
|
def unpack_fault(payload)
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
within_ext_frame(InvalidType) do
|
|
161
|
+
Decoder.decode(payload) do |map|
|
|
162
|
+
raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
153
168
|
|
|
154
|
-
|
|
169
|
+
# Track ext-envelope re-entry depth and refuse a chain past
|
|
170
|
+
# {MAX_EXT_DEPTH}, raising +over_limit+ so the failure lands in the
|
|
171
|
+
# caller's existing wire-error class. The counter is thread-scoped and
|
|
172
|
+
# balanced by the +ensure+, so a raise mid-chain still unwinds it to
|
|
173
|
+
# its entry value.
|
|
174
|
+
def within_ext_frame(over_limit)
|
|
175
|
+
depth = (Thread.current[EXT_DEPTH_KEY] || 0) + 1
|
|
176
|
+
raise over_limit, "ext envelope nesting exceeds #{MAX_EXT_DEPTH} levels" if depth > MAX_EXT_DEPTH
|
|
177
|
+
|
|
178
|
+
Thread.current[EXT_DEPTH_KEY] = depth
|
|
179
|
+
begin
|
|
180
|
+
yield
|
|
181
|
+
ensure
|
|
182
|
+
Thread.current[EXT_DEPTH_KEY] = depth - 1
|
|
155
183
|
end
|
|
156
184
|
end
|
|
157
185
|
end
|
|
@@ -97,6 +97,10 @@ module Kobako
|
|
|
97
97
|
case value
|
|
98
98
|
when ::Array then value.map { |element| HandleWalk.deep_restore(element, handler) }
|
|
99
99
|
when ::Hash
|
|
100
|
+
# Rebuilt with each key restored: two distinct Handle keys that
|
|
101
|
+
# resolve to equal host objects collapse to the later pair, as in
|
|
102
|
+
# any Ruby Hash. The guest authored this payload, so that collapse
|
|
103
|
+
# is its own concern, not a fidelity guarantee the host owes it.
|
|
100
104
|
value.to_h { |key, val| [HandleWalk.deep_restore(key, handler), HandleWalk.deep_restore(val, handler)] }
|
|
101
105
|
when Kobako::Handle then handler.fetch(value.id)
|
|
102
106
|
else value
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Kobako
|
|
|
22
22
|
# {Decoder} are thin wrappers that register the three kobako-specific
|
|
23
23
|
# ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
|
|
24
24
|
# envelope) on a single +MessagePack::Factory+ instance. The Rust side
|
|
25
|
-
# mirrors this layer as the +codec+ module in the +kobako-
|
|
25
|
+
# mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
|
|
26
26
|
# the ext-code constants live as module-private values on {Factory}
|
|
27
27
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
28
28
|
# +codec::EXT_ERRENV+ on that side.
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Kobako
|
|
|
35
35
|
#
|
|
36
36
|
# * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
|
|
37
37
|
# at +wasm_path+.
|
|
38
|
-
# * {
|
|
38
|
+
# * {HandleExhaustedError} < {SandboxError} — Handle id cap hit.
|
|
39
39
|
|
|
40
40
|
# Base for all kobako-raised errors so callers that want to ignore the
|
|
41
41
|
# taxonomy can rescue a single class.
|
|
@@ -89,10 +89,13 @@ module Kobako
|
|
|
89
89
|
# +ModuleNotBuiltError+ first.
|
|
90
90
|
class ModuleNotBuiltError < SetupError; end
|
|
91
91
|
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
|
|
92
|
+
# The structured attribution the two invocation-failure classes carry
|
|
93
|
+
# from a decoded guest exception — its +origin+, original +klass+,
|
|
94
|
+
# +backtrace_lines+, and +details+ — so a Host App can inspect a failure
|
|
95
|
+
# beyond its message. Mixed into both rather than promoted to a shared
|
|
96
|
+
# superclass because +SandboxError+ and +ServiceError+ sit in distinct
|
|
97
|
+
# branches of the invocation-outcome taxonomy under +Kobako::Error+.
|
|
98
|
+
module StructuredError
|
|
96
99
|
attr_reader :origin, :klass, :backtrace_lines, :details
|
|
97
100
|
|
|
98
101
|
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
|
|
@@ -104,26 +107,25 @@ module Kobako
|
|
|
104
107
|
end
|
|
105
108
|
end
|
|
106
109
|
|
|
110
|
+
# Sandbox / wire layer. Raised when the guest ran to completion but
|
|
111
|
+
# execution failed due to a mruby script error, a protocol fault, or a
|
|
112
|
+
# host-side wire decode failure on an otherwise valid outcome tag.
|
|
113
|
+
class SandboxError < Error
|
|
114
|
+
include StructuredError
|
|
115
|
+
end
|
|
116
|
+
|
|
107
117
|
# Service layer. Raised when a Service capability call inside a mruby
|
|
108
118
|
# script reported an application-level failure that the script did not
|
|
109
119
|
# rescue.
|
|
110
120
|
class ServiceError < Error
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
|
|
114
|
-
super(message)
|
|
115
|
-
@origin = origin
|
|
116
|
-
@klass = klass
|
|
117
|
-
@backtrace_lines = backtrace_lines
|
|
118
|
-
@details = details
|
|
119
|
-
end
|
|
121
|
+
include StructuredError
|
|
120
122
|
end
|
|
121
123
|
|
|
122
|
-
#
|
|
124
|
+
# HandleExhaustedError is the canonical SandboxError subclass for the
|
|
123
125
|
# id-cap-hit path. Raised when the per-invocation Handle ID counter in
|
|
124
126
|
# Catalog::Handles reaches +0x7fff_ffff+ (2³¹ − 1) and further
|
|
125
127
|
# allocation would exceed the cap.
|
|
126
|
-
class
|
|
128
|
+
class HandleExhaustedError < SandboxError; end
|
|
127
129
|
|
|
128
130
|
# BytecodeError is the SandboxError subclass raised when a
|
|
129
131
|
# `#preload(binary:)` snippet fails structural validation during the
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -38,14 +38,16 @@ module Kobako
|
|
|
38
38
|
|
|
39
39
|
attr_reader :wasm_path, :options
|
|
40
40
|
|
|
41
|
-
# Per-
|
|
41
|
+
# Per-option accessors forward to the immutable +SandboxOptions+ Value
|
|
42
42
|
# Object so the Host App still reads them off Sandbox directly.
|
|
43
|
-
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit
|
|
43
|
+
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile
|
|
44
44
|
|
|
45
45
|
# Returns the bytes the guest wrote to stdout during the most recent
|
|
46
46
|
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
47
47
|
# any invocation; the byte content never contains a truncation sentinel,
|
|
48
|
-
# so use +#stdout_truncated?+ to observe overflow.
|
|
48
|
+
# so use +#stdout_truncated?+ to observe overflow. Populated on every
|
|
49
|
+
# outcome — including a rescued +TrapError+, after which it holds the
|
|
50
|
+
# bytes written before the trap fired — mirroring +#usage+.
|
|
49
51
|
def stdout
|
|
50
52
|
@stdout_capture.bytes
|
|
51
53
|
end
|
|
@@ -83,23 +85,24 @@ module Kobako
|
|
|
83
85
|
# Build a fresh Sandbox.
|
|
84
86
|
#
|
|
85
87
|
# +wasm_path+ is the absolute path to the Guest Binary; defaults to the
|
|
86
|
-
# gem-bundled +data/kobako.wasm+.
|
|
87
|
-
# +stderr_limit+, +timeout+, +memory_limit+)
|
|
88
|
-
# +
|
|
88
|
+
# gem-bundled +data/kobako.wasm+. Every other keyword — the four caps
|
|
89
|
+
# (+stdout_limit+, +stderr_limit+, +timeout+, +memory_limit+) and the
|
|
90
|
+
# requested isolation profile (+profile+) — is forwarded verbatim to
|
|
91
|
+
# +Kobako::SandboxOptions+, which owns the DEFAULT fallbacks and
|
|
89
92
|
# normalisation. The constructed +SandboxOptions+ is exposed as
|
|
90
|
-
# +#options+ and
|
|
91
|
-
# +Forwardable+ delegation.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
# +#options+ and every option remains readable directly on Sandbox via
|
|
94
|
+
# +Forwardable+ delegation. The runtime builds the requested profile —
|
|
95
|
+
# +:hermetic+ (the default) denies the guest ambient time and entropy,
|
|
96
|
+
# +:permissive+ leaves them live — and construction refuses a runtime
|
|
97
|
+
# whose declared profile falls below the request, raising
|
|
98
|
+
# +Kobako::SetupError+ before any invocation entry point runs.
|
|
99
|
+
def initialize(wasm_path: nil, **)
|
|
95
100
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
96
|
-
@options = SandboxOptions.new(
|
|
97
|
-
stderr_limit: stderr_limit)
|
|
101
|
+
@options = SandboxOptions.new(**)
|
|
98
102
|
@handler = Catalog::Handles.new
|
|
99
103
|
@services = Kobako::Catalog::Namespaces.new(handler: @handler)
|
|
100
104
|
@snippets = Catalog::Snippets.new
|
|
101
|
-
@runtime =
|
|
102
|
-
@options.stdout_limit, @options.stderr_limit)
|
|
105
|
+
@runtime = build_runtime!
|
|
103
106
|
install_dispatch_proc!
|
|
104
107
|
reset_invocation_state!
|
|
105
108
|
end
|
|
@@ -207,17 +210,28 @@ module Kobako
|
|
|
207
210
|
|
|
208
211
|
private
|
|
209
212
|
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# +
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
# Construct the +Runtime+ with the requested isolation profile and
|
|
214
|
+
# refuse one whose declared posture falls below the request —
|
|
215
|
+
# +SandboxOptions#enforce_floor!+ owns the ladder comparison, so a
|
|
216
|
+
# runtime that cannot honor the request never runs guest code.
|
|
217
|
+
def build_runtime!
|
|
218
|
+
runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
219
|
+
@options.stdout_limit, @options.stderr_limit, profile)
|
|
220
|
+
@options.enforce_floor!(runtime.profile)
|
|
221
|
+
runtime
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
|
|
225
|
+
# dispatch +Proc+ that routes guest→host calls through the stateless
|
|
226
|
+
# +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
|
|
227
|
+
# closure. The ext hands the +Proc+ a per-dispatch +guest_yielder+ — a
|
|
228
|
+
# +String → String+ callable that re-enters the in-flight guest to run a
|
|
229
|
+
# yielded block — which the +Dispatcher+ forwards to the +Transport::Yielder+
|
|
230
|
+
# it builds for the call. Registered once at construction time so the
|
|
231
|
+
# wasm ext callback can fire without further setup.
|
|
217
232
|
def install_dispatch_proc!
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, yield_to_guest)
|
|
233
|
+
@runtime.on_dispatch = lambda do |request_bytes, guest_yielder|
|
|
234
|
+
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, guest_yielder)
|
|
221
235
|
end
|
|
222
236
|
end
|
|
223
237
|
|
|
@@ -226,8 +240,7 @@ module Kobako
|
|
|
226
240
|
# state — capture buffers, truncation predicates, and the
|
|
227
241
|
# +Catalog::Handles+ counter — before the guest runs. The
|
|
228
242
|
# +Catalog::Handles+ itself is held as +@handler+ and never exposed
|
|
229
|
-
# beyond this class
|
|
230
|
-
# Host App".
|
|
243
|
+
# beyond this class — it is not part of the Host App's surface.
|
|
231
244
|
def begin_invocation!
|
|
232
245
|
@services.seal!
|
|
233
246
|
@handler.reset!
|
|
@@ -239,9 +252,10 @@ module Kobako
|
|
|
239
252
|
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
240
253
|
# every outcome — value return, +Kobako::TrapError+ (including
|
|
241
254
|
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
242
|
-
# and +Kobako::ServiceError+.
|
|
243
|
-
#
|
|
244
|
-
#
|
|
255
|
+
# and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
|
|
256
|
+
# both paths: the figures are stashed in the ext on every outcome, so
|
|
257
|
+
# the readout here also covers the trap path, where +Runtime#eval+ /
|
|
258
|
+
# +#run+ raise instead of returning outcome bytes.
|
|
245
259
|
#
|
|
246
260
|
# The ext returns a positional 2-tuple +[wall_time, memory_peak]+
|
|
247
261
|
# whose order matches the +Kobako::Usage+ field order; the
|
|
@@ -267,33 +281,51 @@ module Kobako
|
|
|
267
281
|
end
|
|
268
282
|
end
|
|
269
283
|
|
|
284
|
+
# Read the per-last-invocation output captures from the ext and wrap
|
|
285
|
+
# them as +Kobako::Capture+ value objects. Runs in the +invoke!+
|
|
286
|
+
# +ensure+ block next to {#read_usage!} for the same reason: the ext
|
|
287
|
+
# stashes the captures on every outcome, so the readout also covers
|
|
288
|
+
# the trap path, where +Runtime#eval+ / +#run+ raise instead of
|
|
289
|
+
# returning outcome bytes — +#stdout+ / +#stderr+ keep the guest's
|
|
290
|
+
# partial output readable after a rescue.
|
|
291
|
+
#
|
|
292
|
+
# The ext returns a positional 4-tuple
|
|
293
|
+
# +[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]+;
|
|
294
|
+
# the destructure-then-kwargs handoff below is the explicit
|
|
295
|
+
# positional→keyword conversion point.
|
|
296
|
+
def read_captures!
|
|
297
|
+
stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures
|
|
298
|
+
@stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
|
|
299
|
+
@stderr_capture = Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
|
|
300
|
+
end
|
|
301
|
+
|
|
270
302
|
# Shared prologue / epilogue + trap-class translator for both
|
|
271
303
|
# invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
|
|
272
304
|
# TrapError message so the failing export is identifiable.
|
|
273
305
|
#
|
|
274
|
-
# The yielded block must return
|
|
275
|
-
# value of +Runtime#eval+ / +#run+
|
|
276
|
-
#
|
|
277
|
-
# +
|
|
278
|
-
#
|
|
306
|
+
# The yielded block must return the invocation's raw outcome bytes —
|
|
307
|
+
# i.e. the value of +Runtime#eval+ / +#run+ — which the success path
|
|
308
|
+
# feeds to +Outcome.decode+. Captures and usage are populated by the
|
|
309
|
+
# +ensure+ readouts ({#read_usage!} / {#read_captures!}) on every
|
|
310
|
+
# outcome, so +#stdout+ / +#stderr+ / +#usage+ stay readable after a
|
|
311
|
+
# rescued trap.
|
|
279
312
|
# The rescue chain is the single trap-translation boundary —
|
|
280
313
|
# configured-cap paths surface as named TrapError subclasses
|
|
281
314
|
# (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
|
|
282
315
|
# the base +TrapError+.
|
|
283
316
|
def invoke!(verb)
|
|
284
317
|
begin_invocation!
|
|
285
|
-
|
|
286
|
-
@stdout_capture = snapshot.stdout
|
|
287
|
-
@stderr_capture = snapshot.stderr
|
|
318
|
+
return_bytes = yield
|
|
288
319
|
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
289
320
|
# token; restore it to the host object the guest referenced before
|
|
290
321
|
# handing the value to the Host App. @handler still holds this
|
|
291
322
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
292
|
-
Codec::HandleWalk.deep_restore(Outcome.decode(
|
|
323
|
+
Codec::HandleWalk.deep_restore(Outcome.decode(return_bytes), @handler)
|
|
293
324
|
rescue Kobako::TrapError => e
|
|
294
325
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
295
326
|
ensure
|
|
296
327
|
read_usage!
|
|
328
|
+
read_captures!
|
|
297
329
|
end
|
|
298
330
|
end
|
|
299
331
|
end
|