kobako 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/Cargo.lock +1 -1
- data/README.md +14 -7
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/runtime/ambient.rs +1 -1
- data/ext/kobako/src/runtime/cache.rs +3 -3
- data/ext/kobako/src/runtime/capture.rs +1 -1
- data/ext/kobako/src/runtime/config.rs +3 -4
- data/ext/kobako/src/runtime/dispatch.rs +6 -6
- data/ext/kobako/src/runtime/exports.rs +1 -1
- data/ext/kobako/src/runtime/instance_pre.rs +1 -1
- data/ext/kobako/src/runtime/invocation.rs +27 -27
- data/ext/kobako/src/runtime/trap.rs +5 -5
- data/ext/kobako/src/runtime.rs +44 -45
- data/ext/kobako/src/snapshot.rs +2 -2
- data/lib/kobako/capture.rb +5 -7
- data/lib/kobako/catalog/handles.rb +24 -31
- data/lib/kobako/catalog/namespaces.rb +19 -27
- data/lib/kobako/catalog/snippets.rb +10 -16
- data/lib/kobako/codec/utils.rb +6 -9
- data/lib/kobako/errors.rb +33 -39
- data/lib/kobako/handle.rb +2 -3
- data/lib/kobako/namespace.rb +8 -11
- data/lib/kobako/outcome.rb +12 -14
- data/lib/kobako/pool.rb +18 -24
- data/lib/kobako/sandbox.rb +61 -83
- 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/sig/kobako/transport/dispatcher.rbs +2 -0
- metadata +1 -1
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// constructed via `Kobako::Runtime.from_path(path, timeout, memory_limit,
|
|
8
8
|
// stdout_limit, stderr_limit)`. Every invocation (`#eval` / `#run`)
|
|
9
9
|
// instantiates a fresh instance from the InstancePre and discards the
|
|
10
|
-
// whole Store afterwards — the
|
|
11
|
-
//
|
|
10
|
+
// whole Store afterwards — the per-invocation instance discipline
|
|
11
|
+
// (ABI v2). The underlying wasmtime Engine and
|
|
12
12
|
// compiled Module live in a process-scope cache (see the `cache`
|
|
13
13
|
// submodule) and never surface to Ruby (SPEC.md "Code Organization":
|
|
14
14
|
// `ext/` "exposes no Wasm engine types to the Host App or downstream
|
|
@@ -74,9 +74,10 @@ use self::invocation::Invocation;
|
|
|
74
74
|
|
|
75
75
|
/// The wire ABI version this host implements (docs/wire-codec.md § ABI
|
|
76
76
|
/// Version). A Guest Binary is accepted only when its
|
|
77
|
-
/// `__kobako_abi_version` export reports the same value
|
|
78
|
-
///
|
|
79
|
-
///
|
|
77
|
+
/// `__kobako_abi_version` export reports the same value; a mismatch
|
|
78
|
+
/// is a deterministic artifact fault. The guest-side mirror is
|
|
79
|
+
/// `kobako_core::abi::ABI_VERSION`. Version 2
|
|
80
|
+
/// carries the per-invocation instance discipline: the host
|
|
80
81
|
/// drives every invocation on a fresh instance, so the guest may leave
|
|
81
82
|
/// its VM state dirty at exit.
|
|
82
83
|
const ABI_VERSION: u32 = 2;
|
|
@@ -149,22 +150,22 @@ pub(crate) fn trap_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
|
149
150
|
/// Construct a `Kobako::SetupError` magnus error. Used for every
|
|
150
151
|
/// construction-time failure on the `Runtime.from_path` path before any
|
|
151
152
|
/// invocation runs — unreadable artifact, bytes that are not a valid Wasm
|
|
152
|
-
/// module, or engine / linker / instantiation setup failure
|
|
153
|
-
///
|
|
153
|
+
/// module, or engine / linker / instantiation setup failure. The
|
|
154
|
+
/// `ModuleNotBuiltError` subclass (artifact absent) is
|
|
154
155
|
/// raised through `MODULE_NOT_BUILT_ERROR` directly.
|
|
155
156
|
pub(crate) fn setup_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
156
157
|
error_in(ruby, &SETUP_ERROR, msg)
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/// Construct a `Kobako::TimeoutError` magnus error. Surfaces the
|
|
160
|
-
///
|
|
161
|
+
/// wall-clock cap path with the verb prefix added
|
|
161
162
|
/// by `Kobako::Sandbox#invoke!`.
|
|
162
163
|
pub(crate) fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
163
164
|
error_in(ruby, &TIMEOUT_ERROR, msg)
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
/// Construct a `Kobako::MemoryLimitError` magnus error. Surfaces the
|
|
167
|
-
///
|
|
168
|
+
/// linear-memory cap path with the verb prefix
|
|
168
169
|
/// added by `Kobako::Sandbox#invoke!`.
|
|
169
170
|
pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
170
171
|
error_in(ruby, &MEMORY_LIMIT_ERROR, msg)
|
|
@@ -173,8 +174,8 @@ pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusErr
|
|
|
173
174
|
/// Construct a `Kobako::SandboxError` magnus error. Used for the
|
|
174
175
|
/// host-side pre-call faults the SPEC attributes to the sandbox / wire
|
|
175
176
|
/// layer rather than the Wasm engine — currently the `#run` invocation
|
|
176
|
-
/// envelope reservation failure (`__kobako_alloc` returns 0
|
|
177
|
-
///
|
|
177
|
+
/// envelope reservation failure (`__kobako_alloc` returns 0).
|
|
178
|
+
/// The runtime is intact, so this must not be a
|
|
178
179
|
/// `TrapError`: no discard-and-recreate recovery is owed to the caller.
|
|
179
180
|
pub(crate) fn sandbox_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
180
181
|
error_in(ruby, &SANDBOX_ERROR, msg)
|
|
@@ -212,10 +213,9 @@ pub(crate) struct Runtime {
|
|
|
212
213
|
// Pre-linked instantiation template (import wiring + type checks
|
|
213
214
|
// done once in `instance_pre::cached_instance_pre`). Every
|
|
214
215
|
// invocation instantiates a fresh instance from it and discards the
|
|
215
|
-
// whole Store afterwards — the
|
|
216
|
-
// instance discipline.
|
|
216
|
+
// whole Store afterwards — the per-invocation instance discipline.
|
|
217
217
|
instance_pre: WtInstancePre<Invocation>,
|
|
218
|
-
// Per-invocation linear-memory cap
|
|
218
|
+
// Per-invocation linear-memory cap,
|
|
219
219
|
// threaded into each fresh `Invocation`; lives apart from `Config`
|
|
220
220
|
// because the wasmtime `ResourceLimiter` callback consumes it from
|
|
221
221
|
// inside the wasm engine.
|
|
@@ -223,7 +223,7 @@ pub(crate) struct Runtime {
|
|
|
223
223
|
// Wall-clock + per-channel capture caps forwarded from the Sandbox;
|
|
224
224
|
// see `Config`.
|
|
225
225
|
config: Config,
|
|
226
|
-
// The host-side dispatch Proc
|
|
226
|
+
// The host-side dispatch Proc, held here only
|
|
227
227
|
// to give `DataTypeFunctions::mark` a read path so it can pin the
|
|
228
228
|
// Proc across GC. The copy the `__kobako_dispatch` import actually
|
|
229
229
|
// calls is bound onto each per-invocation `Invocation` by
|
|
@@ -231,7 +231,7 @@ pub(crate) struct Runtime {
|
|
|
231
231
|
// pinned Proc. `Cell` is sound under the GVL (see the `unsafe impl
|
|
232
232
|
// Sync` below).
|
|
233
233
|
on_dispatch: Cell<Option<Opaque<Value>>>,
|
|
234
|
-
//
|
|
234
|
+
// Usage of the most recent invocation —
|
|
235
235
|
// `(wall_time_seconds, memory_peak_bytes)` — captured by
|
|
236
236
|
// `build_snapshot` before the per-invocation Store is discarded so
|
|
237
237
|
// `#usage` reads survive the teardown. `(0.0, 0)` before the first
|
|
@@ -245,7 +245,7 @@ impl DataTypeFunctions for Runtime {
|
|
|
245
245
|
/// copy on `Invocation` for the duration of a guest invocation.
|
|
246
246
|
/// `gc::Marker::mark` maps to `rb_gc_mark`, which pins: required because
|
|
247
247
|
/// the Invocation copy is a cached `VALUE` that compaction would
|
|
248
|
-
/// otherwise leave dangling
|
|
248
|
+
/// otherwise leave dangling. Without
|
|
249
249
|
/// this the Proc has no GC root at all — sweep collects it (SIGSEGV on
|
|
250
250
|
/// the next dispatch) and compaction relocates it (dispatch lands on
|
|
251
251
|
/// the wrong receiver).
|
|
@@ -270,10 +270,10 @@ impl Runtime {
|
|
|
270
270
|
/// Ruby-facing constructor for `Kobako::Runtime` — Engine and Module
|
|
271
271
|
/// are never visible to Ruby.
|
|
272
272
|
///
|
|
273
|
-
/// `timeout_seconds` is the
|
|
273
|
+
/// `timeout_seconds` is the wall-clock cap in seconds
|
|
274
274
|
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
275
275
|
/// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
|
|
276
|
-
/// are the per-channel output caps (
|
|
276
|
+
/// are the per-channel output caps (`None`
|
|
277
277
|
/// disables). All four are validated by the caller
|
|
278
278
|
/// (`Kobako::Sandbox`); this method only refuses non-finite or
|
|
279
279
|
/// non-positive timeouts as a defence in depth.
|
|
@@ -289,7 +289,7 @@ impl Runtime {
|
|
|
289
289
|
None => None,
|
|
290
290
|
Some(secs) if secs.is_finite() && secs > 0.0 => Some(Duration::from_secs_f64(secs)),
|
|
291
291
|
Some(secs) => {
|
|
292
|
-
//
|
|
292
|
+
// An invalid cap argument is a Host App
|
|
293
293
|
// programming error and raises `ArgumentError`, outside the
|
|
294
294
|
// construction-failure `SetupError` branch. `SandboxOptions`
|
|
295
295
|
// is the primary guard (it never lets a bad timeout reach
|
|
@@ -318,12 +318,12 @@ impl Runtime {
|
|
|
318
318
|
|
|
319
319
|
/// Instantiate a throwaway probe instance at construction and require
|
|
320
320
|
/// the guest's `__kobako_abi_version` export to equal `ABI_VERSION`
|
|
321
|
-
///
|
|
322
|
-
///
|
|
321
|
+
/// An absent export or a non-equal value is
|
|
322
|
+
/// a deterministic artifact fault raised as
|
|
323
323
|
/// `Kobako::SetupError`. The probe Store drops here; invocation
|
|
324
324
|
/// instances are created per `#eval` / `#run`. The frameless WASI
|
|
325
325
|
/// context keeps a third-party guest whose start section touches
|
|
326
|
-
/// WASI on the
|
|
326
|
+
/// WASI on the `SetupError` path instead of panicking in
|
|
327
327
|
/// `Invocation::wasi_mut`.
|
|
328
328
|
fn probe_abi_version(&self, ruby: &Ruby) -> Result<(), MagnusError> {
|
|
329
329
|
let mut store = self.new_store()?;
|
|
@@ -362,7 +362,7 @@ impl Runtime {
|
|
|
362
362
|
Ok(())
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
/// Register the Ruby-side dispatch `Proc
|
|
365
|
+
/// Register the Ruby-side dispatch `Proc`.
|
|
366
366
|
/// Bound to Ruby as `Kobako::Runtime#on_dispatch=`. The handle is
|
|
367
367
|
/// pinned by `DataTypeFunctions::mark` and copied onto every
|
|
368
368
|
/// per-invocation `Invocation` by `Runtime::new_store`, where the
|
|
@@ -374,7 +374,7 @@ impl Runtime {
|
|
|
374
374
|
|
|
375
375
|
/// Synchronously re-enter the guest's `__kobako_yield_to_block`
|
|
376
376
|
/// export with `args_bytes` as the yield-arguments payload, and
|
|
377
|
-
/// return the YieldResponse bytes the guest produced
|
|
377
|
+
/// return the YieldResponse bytes the guest produced.
|
|
378
378
|
///
|
|
379
379
|
/// Bound to Ruby as `Kobako::Runtime#yield_to_active_invocation`.
|
|
380
380
|
/// Recovers the dispatcher's `&mut Caller` from the per-thread
|
|
@@ -382,7 +382,7 @@ impl Runtime {
|
|
|
382
382
|
/// already inside a `__kobako_dispatch` callback, so the Caller
|
|
383
383
|
/// parked on the Rust stack is the same one the Sandbox-level
|
|
384
384
|
/// `#eval` / `#run` is driving. Invoked from the host-side yield
|
|
385
|
-
/// proxy that the dispatcher hands to Service methods
|
|
385
|
+
/// proxy that the dispatcher hands to Service methods;
|
|
386
386
|
/// raises `Kobako::TrapError` when called outside an active dispatch
|
|
387
387
|
/// frame, or when any of the underlying allocation / write / call /
|
|
388
388
|
/// read steps fails.
|
|
@@ -417,10 +417,10 @@ impl Runtime {
|
|
|
417
417
|
/// Execute one guest invocation (`__kobako_eval` — one-shot source)
|
|
418
418
|
/// and return a `Snapshot` bundling every per-invocation observable.
|
|
419
419
|
///
|
|
420
|
-
/// Builds a fresh Store + instance
|
|
420
|
+
/// Builds a fresh Store + instance whose WASI context carries
|
|
421
421
|
/// the three-frame stdin protocol (`preamble`, `source`, `snippets`
|
|
422
422
|
/// — docs/wire-codec.md § Invocation channels), then invokes
|
|
423
|
-
/// `__kobako_eval`. Per-invocation caps
|
|
423
|
+
/// `__kobako_eval`. Per-invocation caps are
|
|
424
424
|
/// primed here: the wall-clock deadline is stamped into `Invocation`
|
|
425
425
|
/// and the epoch deadline is set to fire at the next ticker tick;
|
|
426
426
|
/// the memory-cap limiter is already wired.
|
|
@@ -429,7 +429,7 @@ impl Runtime {
|
|
|
429
429
|
/// `Kobako::TimeoutError` / `Kobako::MemoryLimitError`; everything
|
|
430
430
|
/// else raises `Kobako::TrapError`. On success the Snapshot carries
|
|
431
431
|
/// the OUTCOME_BUFFER bytes, the per-channel stdout / stderr captures
|
|
432
|
-
/// with their truncation flags, and the
|
|
432
|
+
/// with their truncation flags, and the usage figures.
|
|
433
433
|
pub(crate) fn eval(
|
|
434
434
|
&self,
|
|
435
435
|
preamble: RString,
|
|
@@ -457,14 +457,13 @@ impl Runtime {
|
|
|
457
457
|
/// Execute one entrypoint dispatch (`__kobako_run`) and return a
|
|
458
458
|
/// `Snapshot` bundling every per-invocation observable.
|
|
459
459
|
///
|
|
460
|
-
/// Builds a fresh Store + instance
|
|
460
|
+
/// Builds a fresh Store + instance whose WASI context carries
|
|
461
461
|
/// the two-frame stdin protocol (preamble + snippets; no user source
|
|
462
462
|
/// frame — docs/wire-codec.md § Invocation channels), copies
|
|
463
463
|
/// `envelope` bytes into guest linear memory via `__kobako_alloc`,
|
|
464
464
|
/// and calls `__kobako_run(env_ptr, env_len)`. Per-invocation cap
|
|
465
465
|
/// semantics match `Runtime::eval`. Raises `Kobako::TrapError`
|
|
466
|
-
/// ("alloc returned 0") when guest allocation fails
|
|
467
|
-
/// (docs/behavior.md E-31).
|
|
466
|
+
/// ("alloc returned 0") when guest allocation fails.
|
|
468
467
|
pub(crate) fn run(
|
|
469
468
|
&self,
|
|
470
469
|
preamble: RString,
|
|
@@ -486,7 +485,7 @@ impl Runtime {
|
|
|
486
485
|
self.build_snapshot(&ruby, &mut store, &exports)
|
|
487
486
|
}
|
|
488
487
|
|
|
489
|
-
/// Return the
|
|
488
|
+
/// Return the per-last-invocation usage as a
|
|
490
489
|
/// Ruby 2-tuple `[wall_time, memory_peak]`. The element order
|
|
491
490
|
/// matches the `Kobako::Usage` field order declared in
|
|
492
491
|
/// `lib/kobako/usage.rb`; reorder both sides together if the field
|
|
@@ -520,7 +519,7 @@ impl Runtime {
|
|
|
520
519
|
|
|
521
520
|
/// Build the per-invocation Store: a fresh `Invocation` wired with
|
|
522
521
|
/// the memory limiter, the epoch-deadline callback, and the
|
|
523
|
-
/// registered dispatch Proc
|
|
522
|
+
/// registered dispatch Proc.
|
|
524
523
|
fn new_store(&self) -> Result<WtStore<Invocation>, MagnusError> {
|
|
525
524
|
let mut store = WtStore::new(shared_engine()?, Invocation::new(self.memory_limit));
|
|
526
525
|
store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
|
|
@@ -532,10 +531,10 @@ impl Runtime {
|
|
|
532
531
|
}
|
|
533
532
|
|
|
534
533
|
/// Instantiate the per-invocation instance from the pre-linked
|
|
535
|
-
/// template
|
|
534
|
+
/// template and resolve its host-driven export handles. An
|
|
536
535
|
/// instantiation failure at invocation time is an engine fault —
|
|
537
536
|
/// `Kobako::TrapError` — unlike the construction-time probe, whose
|
|
538
|
-
/// failure is
|
|
537
|
+
/// failure is `SetupError`.
|
|
539
538
|
fn instantiate(
|
|
540
539
|
&self,
|
|
541
540
|
ruby: &Ruby,
|
|
@@ -557,7 +556,7 @@ impl Runtime {
|
|
|
557
556
|
/// `Runtime::prime_caps` before, `disarm_caps` after — the shared
|
|
558
557
|
/// bracket for both run-path exports (`__kobako_eval` /
|
|
559
558
|
/// `__kobako_run`). Disarm runs whether the call returns or traps, so
|
|
560
|
-
/// the
|
|
559
|
+
/// the `wall_time` bracket and the memory
|
|
561
560
|
/// cap always close — that close-on-trap guarantee is the reason this
|
|
562
561
|
/// bracket lives in one place rather than inline at each call site.
|
|
563
562
|
/// The wasmtime trap is returned unmapped; each caller wraps it
|
|
@@ -576,7 +575,7 @@ impl Runtime {
|
|
|
576
575
|
self.prime_caps(store, exports);
|
|
577
576
|
let result = export.call(store.as_context_mut(), params);
|
|
578
577
|
disarm_caps(store);
|
|
579
|
-
// Stash the
|
|
578
|
+
// Stash the usage figures on every outcome — including the
|
|
580
579
|
// trap paths, where `build_snapshot` never runs and the Store is
|
|
581
580
|
// about to be discarded with the error.
|
|
582
581
|
let data = store.data();
|
|
@@ -592,10 +591,10 @@ impl Runtime {
|
|
|
592
591
|
/// effectively never fires.
|
|
593
592
|
///
|
|
594
593
|
/// Also captures the current linear-memory size as the baseline
|
|
595
|
-
/// for the
|
|
594
|
+
/// for the per-invocation memory delta cap —
|
|
596
595
|
/// the pre-initialized image's allocation is folded into the
|
|
597
596
|
/// baseline rather than the budget — and stamps the wall-clock
|
|
598
|
-
/// entry instant for the
|
|
597
|
+
/// entry instant for the `wall_time`
|
|
599
598
|
/// measurement. The bracket closes in `disarm_caps` so it matches
|
|
600
599
|
/// the `timeout` deadline window and excludes `OUTCOME_BUFFER`
|
|
601
600
|
/// decoding and stdout / stderr capture readout.
|
|
@@ -623,7 +622,7 @@ impl Runtime {
|
|
|
623
622
|
/// Called from the run-path methods after the guest export returns
|
|
624
623
|
/// successfully: drains OUTCOME_BUFFER via `__kobako_take_outcome`
|
|
625
624
|
/// and snapshots the per-channel stdout / stderr pipes (clipped to
|
|
626
|
-
/// their caps). The
|
|
625
|
+
/// their caps). The usage figures were already stashed by
|
|
627
626
|
/// `call_with_caps`.
|
|
628
627
|
fn build_snapshot(
|
|
629
628
|
&self,
|
|
@@ -660,7 +659,7 @@ impl Runtime {
|
|
|
660
659
|
/// Drop the memory cap as soon as the guest call returns so that
|
|
661
660
|
/// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
|
|
662
661
|
/// which can grow guest memory transiently) is not attributed to
|
|
663
|
-
/// the user script. Also closes the
|
|
662
|
+
/// the user script. Also closes the
|
|
664
663
|
/// `wall_time` bracket opened by `Runtime::prime_caps`. Paired
|
|
665
664
|
/// with `Runtime::prime_caps`.
|
|
666
665
|
fn disarm_caps(store: &mut WtStore<Invocation>) {
|
|
@@ -683,8 +682,8 @@ fn require_memory(ruby: &Ruby, exports: &Exports) -> Result<Memory, MagnusError>
|
|
|
683
682
|
/// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
|
|
684
683
|
/// Raises `Kobako::TrapError` when the allocation hook is missing or
|
|
685
684
|
/// itself traps, and `Kobako::SandboxError` when the hook runs but
|
|
686
|
-
/// cannot reserve the buffer (`__kobako_alloc` returns 0
|
|
687
|
-
///
|
|
685
|
+
/// cannot reserve the buffer (`__kobako_alloc` returns 0) — an
|
|
686
|
+
/// intact runtime, not an engine fault.
|
|
688
687
|
fn write_envelope(
|
|
689
688
|
ruby: &Ruby,
|
|
690
689
|
store: &mut WtStore<Invocation>,
|
|
@@ -723,7 +722,7 @@ fn write_envelope(
|
|
|
723
722
|
/// instead). Each output pipe is sized at `cap + 1` so
|
|
724
723
|
/// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
|
|
725
724
|
/// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
|
|
726
|
-
/// rely on `memory_limit`
|
|
725
|
+
/// rely on `memory_limit` for the real ceiling.
|
|
727
726
|
/// Raises `Kobako::TrapError` when any frame exceeds the 16 MiB cap that
|
|
728
727
|
/// keeps its `u32` length prefix from wrapping.
|
|
729
728
|
fn install_wasi_frames(
|
data/ext/kobako/src/snapshot.rs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
//! Every successful `Kobako::Runtime#eval` / `#run` returns one of these.
|
|
4
4
|
//! It carries every observable the host needs to surface after a guest
|
|
5
5
|
//! invocation: the OUTCOME_BUFFER bytes (`return_bytes`), the captured
|
|
6
|
-
//! stdout / stderr byte slices with their truncation flags
|
|
7
|
-
//! the wall-clock + memory-peak figures from `Kobako::Usage
|
|
6
|
+
//! stdout / stderr byte slices with their truncation flags, and
|
|
7
|
+
//! the wall-clock + memory-peak figures from `Kobako::Usage`.
|
|
8
8
|
//!
|
|
9
9
|
//! Ruby callers see the seven raw readers registered below; the helper
|
|
10
10
|
//! methods that pack them into `Kobako::Capture` / `Kobako::Usage`
|
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,34 +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
28
|
# Build a fresh, empty table. +next_id+ is an internal seam that
|
|
34
|
-
# sets the starting value of the monotonic counter (defaults to 1
|
|
35
|
-
#
|
|
29
|
+
# sets the starting value of the monotonic counter (defaults to 1);
|
|
30
|
+
# tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
|
|
36
31
|
# the cap-exhaustion path without 2³¹ allocations.
|
|
37
32
|
def initialize(next_id: 1)
|
|
38
33
|
@entries = {} # : Hash[Integer, untyped]
|
|
@@ -45,8 +40,7 @@ module Kobako
|
|
|
45
40
|
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
46
41
|
# +Kobako::HandlerExhaustedError+ if the next ID would exceed the
|
|
47
42
|
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
48
|
-
# and the allocator share the same invariant
|
|
49
|
-
# ({docs/behavior.md B-21}[link:../../../docs/behavior.md]).
|
|
43
|
+
# and the allocator share the same invariant.
|
|
50
44
|
#
|
|
51
45
|
# Returning a Handle (rather than a bare Integer id) keeps the
|
|
52
46
|
# allocator's output a domain entity; +Kobako::Handle.restore+
|
|
@@ -69,9 +63,8 @@ module Kobako
|
|
|
69
63
|
@entries[id]
|
|
70
64
|
end
|
|
71
65
|
|
|
72
|
-
# Clear all entries AND reset the counter to 1. Called at the
|
|
73
|
-
# boundary by +Kobako::Sandbox
|
|
74
|
-
# {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+.
|
|
75
68
|
def reset!
|
|
76
69
|
@entries.clear
|
|
77
70
|
@next_id = 1
|
|
@@ -89,12 +82,12 @@ module Kobako
|
|
|
89
82
|
|
|
90
83
|
private
|
|
91
84
|
|
|
92
|
-
# Refuse to mint a Capability Handle for a reflective gadget
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
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.
|
|
98
91
|
def reject_unwrappable!(object)
|
|
99
92
|
case object
|
|
100
93
|
when Binding, Method, UnboundMethod
|
|
@@ -102,7 +95,7 @@ module Kobako
|
|
|
102
95
|
end
|
|
103
96
|
end
|
|
104
97
|
|
|
105
|
-
# Guard {#alloc} against issuing an ID past the
|
|
98
|
+
# Guard {#alloc} against issuing an ID past the cap. Returns +nil+
|
|
106
99
|
# on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
|
|
107
100
|
def ensure_capacity!
|
|
108
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,14 +23,13 @@ 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]
|
|
@@ -40,14 +38,12 @@ module Kobako
|
|
|
40
38
|
@encoded = nil # : String?
|
|
41
39
|
end
|
|
42
40
|
|
|
43
|
-
# Declare or retrieve the Namespace named +name+ (idempotent
|
|
44
|
-
# {docs/behavior.md B-10}[link:../../../docs/behavior.md]).
|
|
41
|
+
# Declare or retrieve the Namespace named +name+ (idempotent).
|
|
45
42
|
# +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
|
|
46
43
|
# +Namespace::NAME_PATTERN+). Returns the +Kobako::Namespace+ for that
|
|
47
44
|
# name, creating it if it does not exist. Raises +ArgumentError+ when
|
|
48
45
|
# +name+ is malformed, or when called after the owning Sandbox has been
|
|
49
|
-
# sealed by its first invocation
|
|
50
|
-
# ({docs/behavior.md B-07}[link:../../../docs/behavior.md]).
|
|
46
|
+
# sealed by its first invocation.
|
|
51
47
|
def define(name)
|
|
52
48
|
raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
|
|
53
49
|
|
|
@@ -74,20 +70,18 @@ module Kobako
|
|
|
74
70
|
namespace.fetch(member_name)
|
|
75
71
|
end
|
|
76
72
|
|
|
77
|
-
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# +String+ of msgpack bytes.
|
|
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.
|
|
84
79
|
#
|
|
85
80
|
# Once sealed, the bytes are computed once and reused for every
|
|
86
|
-
# subsequent invocation:
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
# and never alters Frame 1.
|
|
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.
|
|
91
85
|
def encode
|
|
92
86
|
return @encoded if @encoded
|
|
93
87
|
|
|
@@ -97,11 +91,9 @@ module Kobako
|
|
|
97
91
|
end
|
|
98
92
|
|
|
99
93
|
# Mark the registry as sealed and propagate the seal to every
|
|
100
|
-
# declared +Kobako::Namespace+
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# raises ArgumentError (E-18) and +Namespace#bind+ raises
|
|
104
|
-
# ArgumentError (E-45). Idempotent.
|
|
94
|
+
# declared +Kobako::Namespace+. Called by +Sandbox+ on the first
|
|
95
|
+
# invocation. After sealing, both #define and +Namespace#bind+
|
|
96
|
+
# raise ArgumentError. Idempotent.
|
|
105
97
|
def seal!
|
|
106
98
|
return self if @sealed
|
|
107
99
|
|
|
@@ -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,18 +14,16 @@ 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
|
|
@@ -45,7 +42,7 @@ module Kobako
|
|
|
45
42
|
# self-encode.
|
|
46
43
|
#
|
|
47
44
|
# The bytes are memoized — the table is replayed verbatim on every
|
|
48
|
-
# invocation after
|
|
45
|
+
# invocation after sealing, so Frame 3 never changes between
|
|
49
46
|
# encodes; {#register} drops the memo while the table is still open.
|
|
50
47
|
def encode
|
|
51
48
|
return @encoded if @encoded
|
|
@@ -53,8 +50,7 @@ module Kobako
|
|
|
53
50
|
@encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
|
|
54
51
|
end
|
|
55
52
|
|
|
56
|
-
# Register one preloaded snippet in either of two forms
|
|
57
|
-
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
53
|
+
# Register one preloaded snippet in either of two forms.
|
|
58
54
|
#
|
|
59
55
|
# * Source form +register(code: src, name: Name)+ — +src+ is the
|
|
60
56
|
# mruby source as a String; the bytes are re-encoded as UTF-8
|
|
@@ -65,15 +61,13 @@ module Kobako
|
|
|
65
61
|
# precompiled RITE bytecode as a String, duplicated and forced
|
|
66
62
|
# to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
|
|
67
63
|
# +nil+ — bytecode entries are anonymous on the host side; any
|
|
68
|
-
# structural validation
|
|
69
|
-
# ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
|
|
70
|
-
# is deferred to the guest at first replay.
|
|
64
|
+
# structural validation is deferred to the guest at first replay.
|
|
71
65
|
#
|
|
72
66
|
# The two forms are mutually exclusive: shape validation lives
|
|
73
67
|
# here so callers (chiefly +Kobako::Sandbox#preload+) collapse to
|
|
74
68
|
# a single delegation. Raises +ArgumentError+ on mixed forms,
|
|
75
|
-
# missing keywords, wrong types, malformed +name
|
|
76
|
-
# duplicate +code:+ +name
|
|
69
|
+
# missing keywords, wrong types, malformed +name+, or
|
|
70
|
+
# duplicate +code:+ +name+.
|
|
77
71
|
def register(code: nil, name: nil, binary: nil)
|
|
78
72
|
@encoded = nil
|
|
79
73
|
if binary
|
|
@@ -89,7 +83,7 @@ module Kobako
|
|
|
89
83
|
|
|
90
84
|
# Source-form register path. Delegates argument-shape checks to
|
|
91
85
|
# +ensure_source_args!+ (which returns the narrowed +[code, name]+
|
|
92
|
-
# pair), normalises +name+ to a Symbol, rejects duplicates
|
|
86
|
+
# pair), normalises +name+ to a Symbol, rejects duplicates,
|
|
93
87
|
# and appends the Source entry.
|
|
94
88
|
def register_source!(code, name)
|
|
95
89
|
code, name = ensure_source_args!(code, name)
|