kobako 0.9.2-x86_64-linux → 0.11.0-x86_64-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 +32 -0
- data/README.md +47 -19
- 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/capture.rb +5 -7
- data/lib/kobako/catalog/handles.rb +28 -39
- data/lib/kobako/catalog/namespaces.rb +31 -20
- data/lib/kobako/catalog/snippets.rb +18 -16
- data/lib/kobako/codec/decoder.rb +5 -1
- data/lib/kobako/codec/utils.rb +6 -9
- data/lib/kobako/errors.rb +40 -36
- data/lib/kobako/handle.rb +2 -3
- data/lib/kobako/namespace.rb +17 -6
- data/lib/kobako/outcome.rb +12 -14
- data/lib/kobako/pool.rb +176 -0
- data/lib/kobako/sandbox.rb +68 -88
- data/lib/kobako/sandbox_options.rb +5 -9
- data/lib/kobako/snapshot.rb +2 -4
- data/lib/kobako/snippet/binary.rb +1 -3
- data/lib/kobako/snippet/source.rb +1 -2
- data/lib/kobako/snippet.rb +1 -2
- data/lib/kobako/transport/dispatcher.rb +39 -38
- data/lib/kobako/transport/request.rb +1 -1
- data/lib/kobako/transport/run.rb +23 -28
- data/lib/kobako/transport/yielder.rb +11 -17
- data/lib/kobako/transport.rb +2 -3
- data/lib/kobako/usage.rb +10 -13
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +16 -1
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/namespace.rbs +2 -0
- data/sig/kobako/pool.rbs +44 -0
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/transport/dispatcher.rbs +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88cfe63025113640b86313e0db4764e075df080db27c2d46a41f3437a9665748
|
|
4
|
+
data.tar.gz: 2fea087aa65fbf8a56ab9ac6d4cb040c02c920095e2e024894419f9094df3a62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eba4996867c874cfbbe5170420637f9fb8743cd68e7a5e0fc5bc63af5afbb9b8303cb3388f4808c1d6c699357a2f411e55040ef1ab23b4878c4fac3e18403063
|
|
7
|
+
data.tar.gz: e4ab4f98eff567fb86415365099d6d11e7857be4146c5fa6ac3260ca8fca1c1c9de9e1b0cfde77a3aa2a1781409ccf76d0df3116505e22f81ecc75a2069dcb9a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.11.0","wasm/kobako-core":"0.5.0","wasm/kobako":"0.5.0","wasm/kobako-io":"0.5.0","wasm/kobako-regexp":"0.5.0","wasm/kobako-baker":"0.5.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0](https://github.com/elct9620/kobako/compare/v0.10.0...v0.11.0) (2026-06-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **transport:** narrow guest-reachable methods via respond_to_guest? ([7a25fe3](https://github.com/elct9620/kobako/commit/7a25fe3b440b523b8a692b7601d08877b1305b0d))
|
|
9
|
+
|
|
10
|
+
## [0.10.0](https://github.com/elct9620/kobako/compare/v0.9.2...v0.10.0) (2026-06-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **catalog:** reject member binding after the seal (E-45) ([5193ed6](https://github.com/elct9620/kobako/commit/5193ed64100fa8ac05a2ba18cfa00634b4f40e6f))
|
|
16
|
+
* **guest:** bake the canonical boot state and instantiate per invocation (B-49) ([ee9ae6e](https://github.com/elct9620/kobako/commit/ee9ae6e09eab30f54dba0eeec00a5a2c80da819f))
|
|
17
|
+
* **pool:** add Kobako::Pool warm-Sandbox checkout (B-46..B-48) ([abf9bf8](https://github.com/elct9620/kobako/commit/abf9bf8d3c725c0ca0b8f2ab8b2ddd6f71ee6de4))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **ext:** give the ABI probe a WASI context ([18e21ea](https://github.com/elct9620/kobako/commit/18e21eac8b160ade2724578aeacd86170403ee2c))
|
|
23
|
+
* **ext:** trust the artifact disk cache only in an exclusively writable directory ([17679cc](https://github.com/elct9620/kobako/commit/17679cc0d38d2a1b605e2faaeed762477a718c18))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Performance Improvements
|
|
27
|
+
|
|
28
|
+
* **bench:** re-bless the anchor onto the post-0.9.2 performance round ([195224d](https://github.com/elct9620/kobako/commit/195224d5d48e53dc2be17752e6d6af6382a0c1ec))
|
|
29
|
+
* **catalog:** drop the alloc-path block iteration from the gadget refusal ([542fe59](https://github.com/elct9620/kobako/commit/542fe59464bde57283d8e91984b82e82592bc3ab))
|
|
30
|
+
* **ext:** amortise module compilation across processes via .cwasm cache ([2e688bc](https://github.com/elct9620/kobako/commit/2e688bc4a1cdf1d0d4d5a0bce2efb314a5b8d1f7))
|
|
31
|
+
* **ext:** bound and harden the compiled-artifact cache ([949f222](https://github.com/elct9620/kobako/commit/949f2227af7cdf7d1913dcae58df683912a7dbd5))
|
|
32
|
+
* **ext:** cache ABI export handles and per-path InstancePre ([47573d0](https://github.com/elct9620/kobako/commit/47573d022233c788ce94413d1a2901ee9d62fc2e))
|
|
33
|
+
* **lib:** cache sealed frame encodings and cut decode-walk allocations ([e599573](https://github.com/elct9620/kobako/commit/e599573e37531b363baca83a1aa5833930100320))
|
|
34
|
+
|
|
3
35
|
## [0.9.2](https://github.com/elct9620/kobako/compare/v0.9.1...v0.9.2) (2026-06-11)
|
|
4
36
|
|
|
5
37
|
|
data/README.md
CHANGED
|
@@ -69,7 +69,7 @@ The script executes inside the Wasm guest. It cannot read your filesystem, open
|
|
|
69
69
|
|
|
70
70
|
### Services
|
|
71
71
|
|
|
72
|
-
Declare a Namespace, then `bind` any Ruby object as a Member; the guest reaches it as a `<Namespace>::<Member>` proxy and invokes its public methods through the Transport wire. See [`docs/behavior.md`](docs/behavior.md) B-07..B-12.
|
|
72
|
+
Declare a Namespace, then `bind` any Ruby object as a Member; the guest reaches it as a `<Namespace>::<Member>` proxy and invokes its public methods through the Transport wire. See [`docs/behavior/registration.md`](docs/behavior/registration.md) B-07..B-12.
|
|
73
73
|
|
|
74
74
|
```ruby
|
|
75
75
|
class User
|
|
@@ -93,7 +93,7 @@ Names must match `/\A[A-Z]\w*\z/`. Symbol kwargs travel transparently to the hos
|
|
|
93
93
|
|
|
94
94
|
### Output Capture
|
|
95
95
|
|
|
96
|
-
Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and exposed independently of the return value ([`docs/behavior.md`](docs/behavior.md) B-04). Buffers clear at the start of each invocation; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
|
|
96
|
+
Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and exposed independently of the return value ([`docs/behavior/lifecycle.md`](docs/behavior/lifecycle.md) B-04). Buffers clear at the start of each invocation; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
|
|
97
97
|
|
|
98
98
|
```ruby
|
|
99
99
|
result = sandbox.eval(<<~RUBY)
|
|
@@ -134,7 +134,7 @@ end
|
|
|
134
134
|
|
|
135
135
|
### Resource Limits
|
|
136
136
|
|
|
137
|
-
Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Sandbox#usage`](lib/kobako/sandbox.rb) after the call — populated on every outcome including traps — for actual consumption ([`docs/behavior.md`](docs/behavior.md) B-35).
|
|
137
|
+
Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Sandbox#usage`](lib/kobako/sandbox.rb) after the call — populated on every outcome including traps — for actual consumption ([`docs/behavior/lifecycle.md`](docs/behavior/lifecycle.md) B-35).
|
|
138
138
|
|
|
139
139
|
```ruby
|
|
140
140
|
sandbox = Kobako::Sandbox.new(
|
|
@@ -179,7 +179,8 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
179
179
|
|
|
180
180
|
──────────────── invocation N ───────────────────
|
|
181
181
|
|
|
182
|
-
1.
|
|
182
|
+
1. start from the canonical boot state
|
|
183
|
+
(mruby pre-initialized into the artifact at build time)
|
|
183
184
|
|
|
184
185
|
2. replay snippets (in insertion order):
|
|
185
186
|
:Adder → defines Adder
|
|
@@ -189,7 +190,7 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
189
190
|
|
|
190
191
|
4. return value to host
|
|
191
192
|
|
|
192
|
-
5. discard
|
|
193
|
+
5. discard the instance; reset per-invocation state:
|
|
193
194
|
· Handles invalidated
|
|
194
195
|
· stdout / stderr buffers cleared
|
|
195
196
|
· memory delta zeroed
|
|
@@ -199,9 +200,30 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
199
200
|
|
|
200
201
|
For workloads that must be isolated from each other (one Sandbox per tenant, per student submission, per agent session), construct a fresh `Kobako::Sandbox` per scope — wasmtime's Engine and the compiled Module are cached at process scope, so additional Sandboxes amortize cold-start cost automatically.
|
|
201
202
|
|
|
203
|
+
### Pooling
|
|
204
|
+
|
|
205
|
+
For hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes and hands each one to a single exclusive holder at a time ([`docs/behavior/runtime.md`](docs/behavior/runtime.md) B-46..B-48). Construction forwards every `Sandbox.new` keyword verbatim; the optional block is the per-Sandbox setup window and runs exactly once per constructed Sandbox.
|
|
206
|
+
|
|
207
|
+
`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. B-49 bakes the shared boot state into the artifact and every dynamic script still compiles and runs per invocation, so all a pool actually saves is the ~30 µs host-side `Sandbox.new`. For the workload kobako is built for — many small, short-lived Sandboxes running dynamic scripts — that is not a significant gain (~4-5% in the [serverless example](examples/serverless/README.md), and proportionally less once the script itself does real work).
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
pool = Kobako::Pool.new(slots: 4) do |sandbox|
|
|
211
|
+
sandbox.define(:KV).bind(:Lookup, ->(key) { redis.get(key) })
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))) }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| Option | Meaning | Default |
|
|
218
|
+
|--------|---------|---------|
|
|
219
|
+
| `slots:` | Upper bound on constructed Sandboxes | required |
|
|
220
|
+
| `checkout_timeout:` | Seconds `#with` waits for a free Sandbox; `nil` waits indefinitely | 5.0 |
|
|
221
|
+
|
|
222
|
+
Sandboxes construct lazily on first demand. `#with` yields a Sandbox with empty output buffers and returns the block's value; at block exit the Sandbox returns to the pool, except a block that raises `Kobako::TrapError` discards its Sandbox and the slot refills by a fresh construction on next demand. A checkout that waits past `checkout_timeout` raises `Kobako::PoolTimeoutError`. There is no teardown verb — a Pool releases everything with its own reachability.
|
|
223
|
+
|
|
202
224
|
### Service Blocks
|
|
203
225
|
|
|
204
|
-
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest; `break` / `next` / exceptions follow normal Ruby semantics, scoped to the single dispatch. See [`docs/behavior.md`](docs/behavior.md) B-23..B-30.
|
|
226
|
+
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest; `break` / `next` / exceptions follow normal Ruby semantics, scoped to the single dispatch. See [`docs/behavior/yield.md`](docs/behavior/yield.md) B-23..B-30.
|
|
205
227
|
|
|
206
228
|
```ruby
|
|
207
229
|
sandbox.define(:Seq).bind(:Map, ->(items, &blk) { items.map(&blk) })
|
|
@@ -212,7 +234,7 @@ sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }')
|
|
|
212
234
|
|
|
213
235
|
### Handle Management
|
|
214
236
|
|
|
215
|
-
A non-wire-representable host object — returned from a Service (B-14), passed to `#run` (B-34), or handed back from the guest (B-37) — crosses the boundary as an opaque `Kobako::Handle` proxy and is restored to the original object before host code sees it; any other unrepresentable value raises `Kobako::SandboxError`. Handles are scoped to a single invocation ([`docs/behavior.md`](docs/behavior.md) B-13..B-21, B-34, B-37).
|
|
237
|
+
A non-wire-representable host object — returned from a Service (B-14), passed to `#run` (B-34), or handed back from the guest (B-37) — crosses the boundary as an opaque `Kobako::Handle` proxy and is restored to the original object before host code sees it; any other unrepresentable value raises `Kobako::SandboxError`. Handles are scoped to a single invocation ([`docs/behavior/dispatch.md`](docs/behavior/dispatch.md) B-13..B-21, B-34, B-37).
|
|
216
238
|
|
|
217
239
|
```ruby
|
|
218
240
|
class Greeter
|
|
@@ -248,7 +270,7 @@ This is deliberate, not a leak. Handle IDs run to 2³¹ − 1 per invocation and
|
|
|
248
270
|
|
|
249
271
|
### Snippets & Entrypoints
|
|
250
272
|
|
|
251
|
-
`Sandbox#preload` registers named mruby snippets that replay
|
|
273
|
+
`Sandbox#preload` registers named mruby snippets that replay into every invocation's canonical boot state; `Sandbox#run(:Target, *args, **kwargs)` dispatches into a top-level `Object` constant defined by those snippets ([`docs/behavior/invocation.md`](docs/behavior/invocation.md) B-31..B-33).
|
|
252
274
|
|
|
253
275
|
```ruby
|
|
254
276
|
sandbox = Kobako::Sandbox.new
|
|
@@ -262,7 +284,7 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
|
|
|
262
284
|
```
|
|
263
285
|
per-invocation replay (every #eval / #run, snippets in insertion order):
|
|
264
286
|
|
|
265
|
-
|
|
287
|
+
canonical boot state
|
|
266
288
|
│
|
|
267
289
|
├──▶ replay :Adder (defines Adder)
|
|
268
290
|
│
|
|
@@ -271,7 +293,7 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
|
|
|
271
293
|
└──▶ eval(source) -or- run(:Target, *args, **kwargs)
|
|
272
294
|
│
|
|
273
295
|
▼
|
|
274
|
-
return value, then
|
|
296
|
+
return value, then instance discarded
|
|
275
297
|
```
|
|
276
298
|
|
|
277
299
|
`#preload` accepts two payload forms:
|
|
@@ -300,6 +322,11 @@ sandbox.define(:Cfg).bind(:Settings, ThemeReader.new) # not: bind(:Settings, Ap
|
|
|
300
322
|
sandbox.eval('Cfg::Settings.color') # => "#3366ff" — every other method raises NoMethodError
|
|
301
323
|
```
|
|
302
324
|
|
|
325
|
+
When a purpose-built wrapper is more than you need, an object can gate its own surface in
|
|
326
|
+
place: a private `respond_to_guest?(name)` answers, per method, whether the guest may call
|
|
327
|
+
it. Returning `false` for every name makes the object opaque — a credential the guest
|
|
328
|
+
forwards to another Service but never reads — while a named subset becomes an allow-list.
|
|
329
|
+
|
|
303
330
|
Guest code can name any `<Namespace>::<Member>` path, but a forged name only resolves to
|
|
304
331
|
something you bound — the real authorization gate is this host-side allowlist. Give each
|
|
305
332
|
trust context its own Sandbox, and see [`docs/security.md`](docs/security.md) for the rest
|
|
@@ -312,15 +339,16 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
|
|
|
312
339
|
|
|
313
340
|
| Phase | Cost |
|
|
314
341
|
|--------------------------------------------------------------|-----------------------|
|
|
315
|
-
| First `Sandbox.new`
|
|
316
|
-
|
|
|
317
|
-
|
|
|
318
|
-
| Warm `#
|
|
319
|
-
|
|
|
320
|
-
|
|
|
321
|
-
|
|
|
322
|
-
|
|
323
|
-
|
|
342
|
+
| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
|
|
343
|
+
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~5 ms one-time |
|
|
344
|
+
| Subsequent `Sandbox.new` (caches warm) | ~30 µs |
|
|
345
|
+
| Warm `#eval("nil")` on a reused Sandbox | ~73 µs |
|
|
346
|
+
| Warm `#run(:Entrypoint, ...)` dispatch | ~104 µs |
|
|
347
|
+
| Service call amortized inside one invocation | ~6.8 µs |
|
|
348
|
+
| Snippet replay per invocation | ~8 µs each |
|
|
349
|
+
| Per additional idle Sandbox (RSS) | ~1 KB |
|
|
350
|
+
|
|
351
|
+
The Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance (the canonical boot state is baked into the artifact and instantiated per invocation), which is why a thousand idle tenants cost ~32 MB total. `ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput stays around 16k `#eval`/s regardless of Thread count, though Ruby-side `#eval` setup still overlaps. A +10% regression on any of the six SPEC-mandated benchmarks blocks release.
|
|
324
352
|
|
|
325
353
|
Regexp is an opt-in capability gem, excluded from the default binary and the gated set; its throughput is tracked in a separate non-gated characterization (`#10` in [`benchmark/README.md`](benchmark/README.md)). There `=~` (~5 µs/match) costs about 4× `match?` (~1.2 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.
|
|
326
354
|
|
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
|
data/lib/kobako/capture.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Kobako
|
|
|
4
4
|
# Host-side captured prefix of guest stdout / stderr produced during a
|
|
5
5
|
# single +Kobako::Sandbox+ invocation, paired with the truncation flag
|
|
6
6
|
# the WASI pipe sets when the guest wrote past the configured per-channel
|
|
7
|
-
# cap
|
|
7
|
+
# cap.
|
|
8
8
|
#
|
|
9
9
|
# Immutable value object: the captured bytes and the truncation flag
|
|
10
10
|
# always travel together and the instance is frozen on construction.
|
|
@@ -30,14 +30,12 @@ module Kobako
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Returns +true+ iff the underlying capture channel exceeded its
|
|
33
|
-
# configured cap during the originating +Sandbox+ invocation
|
|
34
|
-
# ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
|
|
33
|
+
# configured cap during the originating +Sandbox+ invocation.
|
|
35
34
|
def truncated? = @truncated
|
|
36
35
|
|
|
37
|
-
# Pre-invocation sentinel
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# yet".
|
|
36
|
+
# Pre-invocation sentinel. Empty UTF-8 bytes and +truncated? == false+;
|
|
37
|
+
# reused by every fresh +Sandbox+ and by +Sandbox+ between invocations
|
|
38
|
+
# to denote "no capture yet".
|
|
41
39
|
EMPTY = new(bytes: "", truncated: false)
|
|
42
40
|
end
|
|
43
41
|
end
|
|
@@ -5,41 +5,29 @@ require_relative "../handle"
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Catalog
|
|
7
7
|
# Host-side mapping from opaque integer Handle IDs to Ruby objects.
|
|
8
|
-
# The table is owned by +Kobako::Sandbox+
|
|
9
|
-
# ({docs/behavior.md B-19}[link:../../../docs/behavior.md]) and injected
|
|
8
|
+
# The table is owned by +Kobako::Sandbox+ and injected
|
|
10
9
|
# into the per-Sandbox +Kobako::Catalog::Namespaces+ so guest→host dispatch
|
|
11
10
|
# resolves Handle targets and arguments against the same table that
|
|
12
|
-
# host→guest wire encoding allocates into
|
|
13
|
-
# ({docs/behavior.md B-14, B-34}[link:../../../docs/behavior.md]).
|
|
11
|
+
# host→guest wire encoding allocates into.
|
|
14
12
|
#
|
|
15
|
-
# Lifecycle invariants
|
|
13
|
+
# Lifecycle invariants:
|
|
16
14
|
#
|
|
17
|
-
# -
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# +#alloc+.
|
|
15
|
+
# - Handle IDs are allocated by a monotonically increasing counter
|
|
16
|
+
# scoped to a single invocation. The first ID issued in an
|
|
17
|
+
# invocation is 1; ID 0 is reserved as the invalid sentinel and is
|
|
18
|
+
# never returned by +#alloc+.
|
|
22
19
|
#
|
|
23
|
-
# -
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# allocation source (B-14 Service return or B-34 host-injected
|
|
20
|
+
# - At every invocation boundary (via +#reset!+), every Handle issued
|
|
21
|
+
# under the old state becomes invalid. Reset applies uniformly
|
|
22
|
+
# regardless of allocation source (Service return or host-injected
|
|
27
23
|
# argument).
|
|
28
24
|
#
|
|
29
|
-
# -
|
|
30
|
-
#
|
|
31
|
-
# immediately — no silent truncation, no wrap, no ID reuse.
|
|
25
|
+
# - The cap is +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap
|
|
26
|
+
# raises immediately — no silent truncation, no wrap, no ID reuse.
|
|
32
27
|
class Handles
|
|
33
|
-
# Reflective gadget types that are never minted as a Capability Handle
|
|
34
|
-
# ({docs/behavior.md B-43}[link:../../../docs/behavior.md]): wrapping a
|
|
35
|
-
# +Binding+ / +Method+ / +UnboundMethod+ would hand the guest a callable
|
|
36
|
-
# proxy onto host reflection (a returned +Binding+ reaches +Binding#eval+).
|
|
37
|
-
UNWRAPPABLE_TYPES = [Binding, Method, UnboundMethod].freeze
|
|
38
|
-
private_constant :UNWRAPPABLE_TYPES
|
|
39
|
-
|
|
40
28
|
# Build a fresh, empty table. +next_id+ is an internal seam that
|
|
41
|
-
# sets the starting value of the monotonic counter (defaults to 1
|
|
42
|
-
#
|
|
29
|
+
# sets the starting value of the monotonic counter (defaults to 1);
|
|
30
|
+
# tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
|
|
43
31
|
# the cap-exhaustion path without 2³¹ allocations.
|
|
44
32
|
def initialize(next_id: 1)
|
|
45
33
|
@entries = {} # : Hash[Integer, untyped]
|
|
@@ -52,8 +40,7 @@ module Kobako
|
|
|
52
40
|
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
53
41
|
# +Kobako::HandlerExhaustedError+ if the next ID would exceed the
|
|
54
42
|
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
55
|
-
# and the allocator share the same invariant
|
|
56
|
-
# ({docs/behavior.md B-21}[link:../../../docs/behavior.md]).
|
|
43
|
+
# and the allocator share the same invariant.
|
|
57
44
|
#
|
|
58
45
|
# Returning a Handle (rather than a bare Integer id) keeps the
|
|
59
46
|
# allocator's output a domain entity; +Kobako::Handle.restore+
|
|
@@ -76,9 +63,8 @@ module Kobako
|
|
|
76
63
|
@entries[id]
|
|
77
64
|
end
|
|
78
65
|
|
|
79
|
-
# Clear all entries AND reset the counter to 1. Called at the
|
|
80
|
-
# boundary by +Kobako::Sandbox
|
|
81
|
-
# {docs/behavior.md B-19}[link:../../../docs/behavior.md]. Returns +self+.
|
|
66
|
+
# Clear all entries AND reset the counter to 1. Called at the
|
|
67
|
+
# per-invocation boundary by +Kobako::Sandbox+. Returns +self+.
|
|
82
68
|
def reset!
|
|
83
69
|
@entries.clear
|
|
84
70
|
@next_id = 1
|
|
@@ -96,17 +82,20 @@ module Kobako
|
|
|
96
82
|
|
|
97
83
|
private
|
|
98
84
|
|
|
99
|
-
# Refuse to mint a Capability Handle for a reflective gadget
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
85
|
+
# Refuse to mint a Capability Handle for a reflective gadget:
|
|
86
|
+
# a +Binding+ / +Method+ / +UnboundMethod+ would hand the guest a
|
|
87
|
+
# callable proxy onto host reflection (a returned +Binding+ reaches
|
|
88
|
+
# +Binding#eval+). Raising here keeps the rule at the single mint
|
|
89
|
+
# point, so it holds on both the Service-return and the +#run+
|
|
90
|
+
# host→guest auto-wrap paths.
|
|
103
91
|
def reject_unwrappable!(object)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
case object
|
|
93
|
+
when Binding, Method, UnboundMethod
|
|
94
|
+
raise SandboxError, "a #{object.class} cannot cross as a Capability Handle"
|
|
95
|
+
end
|
|
107
96
|
end
|
|
108
97
|
|
|
109
|
-
# Guard {#alloc} against issuing an ID past the
|
|
98
|
+
# Guard {#alloc} against issuing an ID past the cap. Returns +nil+
|
|
110
99
|
# on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
|
|
111
100
|
def ensure_capacity!
|
|
112
101
|
cap = Kobako::Handle::MAX_ID
|
|
@@ -9,8 +9,7 @@ module Kobako
|
|
|
9
9
|
module Catalog
|
|
10
10
|
# Kobako::Catalog::Namespaces — per-Sandbox registry of
|
|
11
11
|
# +Kobako::Namespace+ entities. Holds the Namespace / Member bindings
|
|
12
|
-
# and the preamble emitted on Frame 1
|
|
13
|
-
# ({docs/behavior.md B-07..B-11}[link:../../../docs/behavior.md]).
|
|
12
|
+
# and the preamble emitted on Frame 1.
|
|
14
13
|
#
|
|
15
14
|
# Public API:
|
|
16
15
|
#
|
|
@@ -24,29 +23,27 @@ module Kobako
|
|
|
24
23
|
# +Kobako::Transport::Dispatcher+'s responsibility — the Dispatcher
|
|
25
24
|
# receives this registry and the +Catalog::Handles+ as arguments from
|
|
26
25
|
# the +Runtime#on_dispatch+ Proc that +Kobako::Sandbox#initialize+
|
|
27
|
-
# installs
|
|
28
|
-
# The registry holds an injected +Catalog::Handles+ reference so
|
|
26
|
+
# installs. The registry holds an injected +Catalog::Handles+ reference so
|
|
29
27
|
# dispatch target resolution and host→guest auto-wrap share the same
|
|
30
|
-
# Sandbox-owned allocator
|
|
28
|
+
# Sandbox-owned allocator.
|
|
31
29
|
class Namespaces
|
|
32
30
|
# Build a fresh registry. +handler+ is an internal seam that injects
|
|
33
31
|
# a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
|
|
34
|
-
# is pinned near +MAX_ID+ to exercise the
|
|
32
|
+
# is pinned near +MAX_ID+ to exercise the cap-exhaustion path
|
|
35
33
|
# without 2³¹ allocations. Production callers leave it at the default.
|
|
36
34
|
def initialize(handler: Catalog::Handles.new)
|
|
37
35
|
@namespaces = {} # : Hash[String, Kobako::Namespace]
|
|
38
36
|
@handler = handler
|
|
39
37
|
@sealed = false
|
|
38
|
+
@encoded = nil # : String?
|
|
40
39
|
end
|
|
41
40
|
|
|
42
|
-
# Declare or retrieve the Namespace named +name+ (idempotent
|
|
43
|
-
# {docs/behavior.md B-10}[link:../../../docs/behavior.md]).
|
|
41
|
+
# Declare or retrieve the Namespace named +name+ (idempotent).
|
|
44
42
|
# +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
|
|
45
43
|
# +Namespace::NAME_PATTERN+). Returns the +Kobako::Namespace+ for that
|
|
46
44
|
# name, creating it if it does not exist. Raises +ArgumentError+ when
|
|
47
45
|
# +name+ is malformed, or when called after the owning Sandbox has been
|
|
48
|
-
# sealed by its first invocation
|
|
49
|
-
# ({docs/behavior.md B-07}[link:../../../docs/behavior.md]).
|
|
46
|
+
# sealed by its first invocation.
|
|
50
47
|
def define(name)
|
|
51
48
|
raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
|
|
52
49
|
|
|
@@ -73,21 +70,35 @@ module Kobako
|
|
|
73
70
|
namespace.fetch(member_name)
|
|
74
71
|
end
|
|
75
72
|
|
|
76
|
-
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
73
|
+
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery.
|
|
74
|
+
# Routes through {Kobako::Codec::Encoder} like every other host-side
|
|
75
|
+
# wire encode so there is a single codec path; the preamble carries
|
|
76
|
+
# only Strings and Arrays, so none of the kobako ext types actually
|
|
77
|
+
# fire. Structure: +[["Namespace", ["MemberA", "MemberB"]], ...]+.
|
|
78
|
+
# Returns a binary +String+ of msgpack bytes.
|
|
79
|
+
#
|
|
80
|
+
# Once sealed, the bytes are computed once and reused for every
|
|
81
|
+
# subsequent invocation: sealing freezes Service registration at the
|
|
82
|
+
# first invocation, so the preamble is exactly the bindings that
|
|
83
|
+
# existed at that moment — a bind reaching a +Kobako::Namespace+
|
|
84
|
+
# after the seal raises +ArgumentError+ and never alters Frame 1.
|
|
83
85
|
def encode
|
|
84
|
-
|
|
86
|
+
return @encoded if @encoded
|
|
87
|
+
|
|
88
|
+
bytes = Codec::Encoder.encode(@namespaces.values.map(&:to_preamble)).freeze
|
|
89
|
+
@encoded = bytes if @sealed
|
|
90
|
+
bytes
|
|
85
91
|
end
|
|
86
92
|
|
|
87
|
-
# Mark the registry as sealed
|
|
88
|
-
#
|
|
93
|
+
# Mark the registry as sealed and propagate the seal to every
|
|
94
|
+
# declared +Kobako::Namespace+. Called by +Sandbox+ on the first
|
|
95
|
+
# invocation. After sealing, both #define and +Namespace#bind+
|
|
96
|
+
# raise ArgumentError. Idempotent.
|
|
89
97
|
def seal!
|
|
98
|
+
return self if @sealed
|
|
99
|
+
|
|
90
100
|
@sealed = true
|
|
101
|
+
@namespaces.each_value(&:seal!)
|
|
91
102
|
self
|
|
92
103
|
end
|
|
93
104
|
|
|
@@ -6,8 +6,7 @@ require_relative "../snippet"
|
|
|
6
6
|
module Kobako
|
|
7
7
|
module Catalog
|
|
8
8
|
# Kobako::Catalog::Snippets — per-Sandbox insertion-ordered registry
|
|
9
|
-
# of preloaded snippets
|
|
10
|
-
# ({docs/behavior.md B-32 / B-33}[link:../../../docs/behavior.md]).
|
|
9
|
+
# of preloaded snippets.
|
|
11
10
|
#
|
|
12
11
|
# Entries replay against the fresh +mrb_state+ before per-invocation
|
|
13
12
|
# source / entrypoint resolution. Each +Snippet::Source+ entry's +name+
|
|
@@ -15,22 +14,21 @@ module Kobako
|
|
|
15
14
|
# +debug_info+ that surfaces in every backtrace frame originating from
|
|
16
15
|
# the snippet as +(snippet:Name):line+. Duplicate names within the
|
|
17
16
|
# +code:+ form would produce ambiguous attribution and are rejected at
|
|
18
|
-
# registration time
|
|
19
|
-
# ({docs/behavior.md E-33}[link:../../../docs/behavior.md]).
|
|
17
|
+
# registration time.
|
|
20
18
|
# +Snippet::Binary+ entries carry no host-side name — their canonical
|
|
21
19
|
# name lives in the bytecode's +debug_info+ and is read by the guest at
|
|
22
20
|
# load time; the host does not extract it.
|
|
23
21
|
#
|
|
24
|
-
# Sealing
|
|
22
|
+
# Sealing is governed by the owning Sandbox — the registry itself
|
|
25
23
|
# is append-only and exposes no mutation API beyond +#register+; the
|
|
26
24
|
# Sandbox guards +#register+ behind the seal check before delegating.
|
|
27
25
|
class Snippets
|
|
28
|
-
# Ruby constant-name pattern enforced on snippet names
|
|
29
|
-
# ({docs/behavior.md E-34}[link:../../../docs/behavior.md]).
|
|
26
|
+
# Ruby constant-name pattern enforced on snippet names.
|
|
30
27
|
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
31
28
|
|
|
32
29
|
def initialize
|
|
33
30
|
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
|
|
31
|
+
@encoded = nil # : String?
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
# Serialize the registered snippets to wire bytes. Each entry
|
|
@@ -42,12 +40,17 @@ module Kobako
|
|
|
42
40
|
# carriers — this collection-tier method reads their attributes
|
|
43
41
|
# externally via +entry_payload+ rather than asking each entry to
|
|
44
42
|
# self-encode.
|
|
43
|
+
#
|
|
44
|
+
# The bytes are memoized — the table is replayed verbatim on every
|
|
45
|
+
# invocation after sealing, so Frame 3 never changes between
|
|
46
|
+
# encodes; {#register} drops the memo while the table is still open.
|
|
45
47
|
def encode
|
|
46
|
-
|
|
48
|
+
return @encoded if @encoded
|
|
49
|
+
|
|
50
|
+
@encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
|
|
47
51
|
end
|
|
48
52
|
|
|
49
|
-
# Register one preloaded snippet in either of two forms
|
|
50
|
-
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
53
|
+
# Register one preloaded snippet in either of two forms.
|
|
51
54
|
#
|
|
52
55
|
# * Source form +register(code: src, name: Name)+ — +src+ is the
|
|
53
56
|
# mruby source as a String; the bytes are re-encoded as UTF-8
|
|
@@ -58,16 +61,15 @@ module Kobako
|
|
|
58
61
|
# precompiled RITE bytecode as a String, duplicated and forced
|
|
59
62
|
# to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
|
|
60
63
|
# +nil+ — bytecode entries are anonymous on the host side; any
|
|
61
|
-
# structural validation
|
|
62
|
-
# ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
|
|
63
|
-
# is deferred to the guest at first replay.
|
|
64
|
+
# structural validation is deferred to the guest at first replay.
|
|
64
65
|
#
|
|
65
66
|
# The two forms are mutually exclusive: shape validation lives
|
|
66
67
|
# here so callers (chiefly +Kobako::Sandbox#preload+) collapse to
|
|
67
68
|
# a single delegation. Raises +ArgumentError+ on mixed forms,
|
|
68
|
-
# missing keywords, wrong types, malformed +name
|
|
69
|
-
# duplicate +code:+ +name
|
|
69
|
+
# missing keywords, wrong types, malformed +name+, or
|
|
70
|
+
# duplicate +code:+ +name+.
|
|
70
71
|
def register(code: nil, name: nil, binary: nil)
|
|
72
|
+
@encoded = nil
|
|
71
73
|
if binary
|
|
72
74
|
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
73
75
|
|
|
@@ -81,7 +83,7 @@ module Kobako
|
|
|
81
83
|
|
|
82
84
|
# Source-form register path. Delegates argument-shape checks to
|
|
83
85
|
# +ensure_source_args!+ (which returns the narrowed +[code, name]+
|
|
84
|
-
# pair), normalises +name+ to a Symbol, rejects duplicates
|
|
86
|
+
# pair), normalises +name+ to a Symbol, rejects duplicates,
|
|
85
87
|
# and appends the Source entry.
|
|
86
88
|
def register_source!(code, name)
|
|
87
89
|
code, name = ensure_source_args!(code, name)
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -64,7 +64,11 @@ module Kobako
|
|
|
64
64
|
case value
|
|
65
65
|
when String then Utils.assert_utf8!(value, "str payload") if value.encoding == Encoding::UTF_8
|
|
66
66
|
when Array then value.each { |v| validate_utf8!(v) }
|
|
67
|
-
when Hash
|
|
67
|
+
when Hash
|
|
68
|
+
value.each do |key, val|
|
|
69
|
+
validate_utf8!(key)
|
|
70
|
+
validate_utf8!(val)
|
|
71
|
+
end
|
|
68
72
|
end
|
|
69
73
|
end
|
|
70
74
|
end
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -19,8 +19,7 @@ module Kobako
|
|
|
19
19
|
# - Representability predicate ({representable?}) and the symmetric
|
|
20
20
|
# host→guest +#run+ argument walk ({deep_wrap}) used by
|
|
21
21
|
# +Kobako::Transport::Run#encode+ to route non-representable leaves
|
|
22
|
-
# through the Sandbox's +Kobako::Catalog::Handles
|
|
23
|
-
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]).
|
|
22
|
+
# through the Sandbox's +Kobako::Catalog::Handles+.
|
|
24
23
|
#
|
|
25
24
|
# All helpers are pure — they only inspect inputs, never mutate
|
|
26
25
|
# them — except {deep_wrap}, whose only side effect is allocating
|
|
@@ -84,8 +83,7 @@ module Kobako
|
|
|
84
83
|
|
|
85
84
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
86
85
|
# leaf that fails {representable?} with a +Kobako::Handle+
|
|
87
|
-
# allocated from +handler
|
|
88
|
-
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
|
|
86
|
+
# allocated from +handler+. The
|
|
89
87
|
# walk only descends through representable container shapes
|
|
90
88
|
# (Array, Hash) one structural level at a time; a non-representable
|
|
91
89
|
# leaf is wrapped as-is without inspecting its internal structure.
|
|
@@ -116,8 +114,7 @@ module Kobako
|
|
|
116
114
|
|
|
117
115
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
118
116
|
# +Kobako::Handle+ leaf with the host-side object +handler+ resolves
|
|
119
|
-
# it to
|
|
120
|
-
# The symmetric inverse of {deep_wrap}: that walk allocates objects
|
|
117
|
+
# it to. The symmetric inverse of {deep_wrap}: that walk allocates objects
|
|
121
118
|
# into Handles on the host→guest argument path; this walk resolves
|
|
122
119
|
# Handles back to their objects on every guest→host value path — the
|
|
123
120
|
# +#eval+ / +#run+ result and the yield-block result alike. The walk
|
|
@@ -126,11 +123,11 @@ module Kobako
|
|
|
126
123
|
# unchanged.
|
|
127
124
|
#
|
|
128
125
|
# +value+ is a decoded Ruby value (a Handle here is a wire-decoded
|
|
129
|
-
# +Kobako::Handle+, never a guest-forged one
|
|
126
|
+
# +Kobako::Handle+, never a guest-forged one); +handler+ must
|
|
130
127
|
# respond to +#fetch(id) -> object+ (a host-side
|
|
131
128
|
# +Kobako::Catalog::Handles+). +handler.fetch+ raises
|
|
132
|
-
# +Kobako::SandboxError+ for an id with no live binding,
|
|
133
|
-
# corrupted-runtime fallback
|
|
129
|
+
# +Kobako::SandboxError+ for an id with no live binding, the
|
|
130
|
+
# corrupted-runtime fallback.
|
|
134
131
|
def deep_restore(value, handler)
|
|
135
132
|
case value
|
|
136
133
|
when ::Array then value.map { |element| Utils.deep_restore(element, handler) }
|