kobako 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +18 -0
- data/Cargo.lock +129 -74
- data/README.md +1 -1
- data/SECURITY.md +35 -0
- 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 +1 -1
- data/ext/kobako/src/runtime/errors.rs +90 -0
- data/ext/kobako/src/runtime/frames.rs +188 -0
- data/ext/kobako/src/runtime/instance_pre.rs +2 -1
- data/ext/kobako/src/runtime/trap.rs +1 -1
- data/ext/kobako/src/runtime.rs +17 -276
- data/lib/kobako/catalog/handles.rb +4 -4
- data/lib/kobako/codec/handle_walk.rb +131 -0
- data/lib/kobako/codec/utils.rb +4 -117
- data/lib/kobako/codec.rb +1 -0
- data/lib/kobako/handle.rb +1 -1
- data/lib/kobako/outcome.rb +2 -10
- data/lib/kobako/sandbox.rb +1 -1
- data/lib/kobako/transport/dispatcher.rb +3 -7
- data/lib/kobako/transport/run.rb +4 -4
- data/lib/kobako/transport/yielder.rb +1 -1
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/codec/handle_walk.rbs +17 -0
- data/sig/kobako/codec/utils.rbs +0 -12
- metadata +6 -1
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -42,16 +42,15 @@ mod cache;
|
|
|
42
42
|
mod capture;
|
|
43
43
|
mod config;
|
|
44
44
|
mod dispatch;
|
|
45
|
+
mod errors;
|
|
45
46
|
mod exports;
|
|
47
|
+
mod frames;
|
|
46
48
|
mod guest_mem;
|
|
47
49
|
mod instance_pre;
|
|
48
50
|
mod invocation;
|
|
49
51
|
mod trap;
|
|
50
52
|
|
|
51
|
-
use magnus::
|
|
52
|
-
use magnus::{
|
|
53
|
-
function, method, prelude::*, Error as MagnusError, ExceptionClass, RModule, RString, Ruby,
|
|
54
|
-
};
|
|
53
|
+
use magnus::{function, method, prelude::*, Error as MagnusError, RModule, RString, Ruby};
|
|
55
54
|
|
|
56
55
|
use std::cell::Cell;
|
|
57
56
|
use std::path::Path;
|
|
@@ -61,11 +60,8 @@ use magnus::{gc, typed_data::DataTypeFunctions, value::Opaque, RArray, TypedData
|
|
|
61
60
|
|
|
62
61
|
use crate::snapshot::Snapshot;
|
|
63
62
|
use wasmtime::{
|
|
64
|
-
AsContextMut, InstancePre as WtInstancePre,
|
|
65
|
-
TypedFunc,
|
|
63
|
+
AsContextMut, InstancePre as WtInstancePre, ResourceLimiter, Store as WtStore, TypedFunc,
|
|
66
64
|
};
|
|
67
|
-
use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
|
|
68
|
-
use wasmtime_wasi::WasiCtxBuilder;
|
|
69
65
|
|
|
70
66
|
use self::cache::shared_engine;
|
|
71
67
|
use self::config::Config;
|
|
@@ -93,94 +89,6 @@ pub(crate) fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
|
93
89
|
unsafe { s.as_slice() }.to_vec()
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// Error classes (lazy-resolved from Ruby once the top-level Kobako error
|
|
98
|
-
// hierarchy is loaded by `lib/kobako/errors.rb`). The ext raises directly
|
|
99
|
-
// into the invocation-outcome taxonomy (`TrapError` and its subclasses)
|
|
100
|
-
// for run-path failures and into the construction-layer `SetupError`
|
|
101
|
-
// (and its `ModuleNotBuiltError` subclass) for `from_path` setup failures
|
|
102
|
-
// — no engine-specific intermediate layer; the Sandbox layer adds the
|
|
103
|
-
// verb prefix and lets the subclass identity flow through unchanged.
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
/// Resolve `Kobako::<name>` as an `ExceptionClass` — the shared body of
|
|
107
|
-
/// every error-class `Lazy` below, which differ only in the constant
|
|
108
|
-
/// name. The constants are guaranteed present by the time any of these
|
|
109
|
-
/// lazies first resolve (`lib/kobako/errors.rb` loads the hierarchy before
|
|
110
|
-
/// the ext raises into it), so a missing constant is a build / wiring bug
|
|
111
|
-
/// and the `unwrap` is the correct fail-fast.
|
|
112
|
-
fn kobako_error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
113
|
-
let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
|
|
114
|
-
kobako.const_get(name).unwrap()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
pub(crate) static SETUP_ERROR: Lazy<ExceptionClass> =
|
|
118
|
-
Lazy::new(|ruby| kobako_error_class(ruby, "SetupError"));
|
|
119
|
-
|
|
120
|
-
pub(crate) static MODULE_NOT_BUILT_ERROR: Lazy<ExceptionClass> =
|
|
121
|
-
Lazy::new(|ruby| kobako_error_class(ruby, "ModuleNotBuiltError"));
|
|
122
|
-
|
|
123
|
-
pub(crate) static TRAP_ERROR: Lazy<ExceptionClass> =
|
|
124
|
-
Lazy::new(|ruby| kobako_error_class(ruby, "TrapError"));
|
|
125
|
-
|
|
126
|
-
pub(crate) static TIMEOUT_ERROR: Lazy<ExceptionClass> =
|
|
127
|
-
Lazy::new(|ruby| kobako_error_class(ruby, "TimeoutError"));
|
|
128
|
-
|
|
129
|
-
pub(crate) static MEMORY_LIMIT_ERROR: Lazy<ExceptionClass> =
|
|
130
|
-
Lazy::new(|ruby| kobako_error_class(ruby, "MemoryLimitError"));
|
|
131
|
-
|
|
132
|
-
pub(crate) static SANDBOX_ERROR: Lazy<ExceptionClass> =
|
|
133
|
-
Lazy::new(|ruby| kobako_error_class(ruby, "SandboxError"));
|
|
134
|
-
|
|
135
|
-
/// Build a `MagnusError` in `class` carrying `msg` — the shared body of
|
|
136
|
-
/// the named `*_err` constructors below, which differ only in which
|
|
137
|
-
/// error-class `Lazy` they target.
|
|
138
|
-
fn error_in(ruby: &Ruby, class: &Lazy<ExceptionClass>, msg: impl Into<String>) -> MagnusError {
|
|
139
|
-
MagnusError::new(ruby.get_inner(class), msg.into())
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/// Construct a `Kobako::TrapError` magnus error. Used for every
|
|
143
|
-
/// invocation-time wasmtime engine failure that is not a configured-cap
|
|
144
|
-
/// trap — missing exports, allocation faults, memory write/read failures.
|
|
145
|
-
/// Construction-time setup failures use `setup_err`, not this.
|
|
146
|
-
pub(crate) fn trap_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
147
|
-
error_in(ruby, &TRAP_ERROR, msg)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/// Construct a `Kobako::SetupError` magnus error. Used for every
|
|
151
|
-
/// construction-time failure on the `Runtime.from_path` path before any
|
|
152
|
-
/// invocation runs — unreadable artifact, bytes that are not a valid Wasm
|
|
153
|
-
/// module, or engine / linker / instantiation setup failure. The
|
|
154
|
-
/// `ModuleNotBuiltError` subclass (artifact absent) is
|
|
155
|
-
/// raised through `MODULE_NOT_BUILT_ERROR` directly.
|
|
156
|
-
pub(crate) fn setup_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
157
|
-
error_in(ruby, &SETUP_ERROR, msg)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/// Construct a `Kobako::TimeoutError` magnus error. Surfaces the
|
|
161
|
-
/// wall-clock cap path with the verb prefix added
|
|
162
|
-
/// by `Kobako::Sandbox#invoke!`.
|
|
163
|
-
pub(crate) fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
164
|
-
error_in(ruby, &TIMEOUT_ERROR, msg)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/// Construct a `Kobako::MemoryLimitError` magnus error. Surfaces the
|
|
168
|
-
/// linear-memory cap path with the verb prefix
|
|
169
|
-
/// added by `Kobako::Sandbox#invoke!`.
|
|
170
|
-
pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
171
|
-
error_in(ruby, &MEMORY_LIMIT_ERROR, msg)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/// Construct a `Kobako::SandboxError` magnus error. Used for the
|
|
175
|
-
/// host-side pre-call faults the SPEC attributes to the sandbox / wire
|
|
176
|
-
/// layer rather than the Wasm engine — currently the `#run` invocation
|
|
177
|
-
/// envelope reservation failure (`__kobako_alloc` returns 0).
|
|
178
|
-
/// The runtime is intact, so this must not be a
|
|
179
|
-
/// `TrapError`: no discard-and-recreate recovery is owed to the caller.
|
|
180
|
-
pub(crate) fn sandbox_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
181
|
-
error_in(ruby, &SANDBOX_ERROR, msg)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
92
|
// ---------------------------------------------------------------------------
|
|
185
93
|
// Ruby init
|
|
186
94
|
// ---------------------------------------------------------------------------
|
|
@@ -327,7 +235,7 @@ impl Runtime {
|
|
|
327
235
|
/// `Invocation::wasi_mut`.
|
|
328
236
|
fn probe_abi_version(&self, ruby: &Ruby) -> Result<(), MagnusError> {
|
|
329
237
|
let mut store = self.new_store()?;
|
|
330
|
-
install_wasi_frames(&mut store, &self.config, &[])?;
|
|
238
|
+
frames::install_wasi_frames(&mut store, &self.config, &[])?;
|
|
331
239
|
let instance = self
|
|
332
240
|
.instance_pre
|
|
333
241
|
.instantiate(store.as_context_mut())
|
|
@@ -335,7 +243,7 @@ impl Runtime {
|
|
|
335
243
|
let probe = instance
|
|
336
244
|
.get_typed_func::<(), u32>(store.as_context_mut(), "__kobako_abi_version")
|
|
337
245
|
.map_err(|_| {
|
|
338
|
-
setup_err(
|
|
246
|
+
errors::setup_err(
|
|
339
247
|
ruby,
|
|
340
248
|
format!(
|
|
341
249
|
"the Guest Binary does not export __kobako_abi_version; \
|
|
@@ -344,13 +252,13 @@ impl Runtime {
|
|
|
344
252
|
)
|
|
345
253
|
})?;
|
|
346
254
|
let reported = probe.call(store.as_context_mut(), ()).map_err(|e| {
|
|
347
|
-
setup_err(
|
|
255
|
+
errors::setup_err(
|
|
348
256
|
ruby,
|
|
349
257
|
format!("failed to read the Guest Binary's ABI version: {e}"),
|
|
350
258
|
)
|
|
351
259
|
})?;
|
|
352
260
|
if reported != ABI_VERSION {
|
|
353
|
-
return Err(setup_err(
|
|
261
|
+
return Err(errors::setup_err(
|
|
354
262
|
ruby,
|
|
355
263
|
format!(
|
|
356
264
|
"the Guest Binary reports ABI version {reported}, but this host \
|
|
@@ -396,14 +304,14 @@ impl Runtime {
|
|
|
396
304
|
|
|
397
305
|
let bytes = rstring_to_vec(args_bytes);
|
|
398
306
|
let Some(caller) = dispatch::current_caller() else {
|
|
399
|
-
return Err(trap_err(
|
|
307
|
+
return Err(errors::trap_err(
|
|
400
308
|
&ruby,
|
|
401
309
|
"yield_to_active_invocation called outside an active Sandbox dispatch frame",
|
|
402
310
|
));
|
|
403
311
|
};
|
|
404
312
|
|
|
405
313
|
let resp_bytes =
|
|
406
|
-
guest_mem::drive_yield(caller, &bytes).map_err(|msg| trap_err(&ruby, msg))?;
|
|
314
|
+
guest_mem::drive_yield(caller, &bytes).map_err(|msg| errors::trap_err(&ruby, msg))?;
|
|
407
315
|
Ok(ruby.str_from_slice(&resp_bytes))
|
|
408
316
|
}
|
|
409
317
|
|
|
@@ -438,7 +346,7 @@ impl Runtime {
|
|
|
438
346
|
) -> Result<Snapshot, MagnusError> {
|
|
439
347
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
440
348
|
let mut store = self.new_store()?;
|
|
441
|
-
install_wasi_frames(
|
|
349
|
+
frames::install_wasi_frames(
|
|
442
350
|
&mut store,
|
|
443
351
|
&self.config,
|
|
444
352
|
&[
|
|
@@ -448,7 +356,7 @@ impl Runtime {
|
|
|
448
356
|
],
|
|
449
357
|
)?;
|
|
450
358
|
let exports = self.instantiate(&ruby, &mut store)?;
|
|
451
|
-
let eval = require_export(&ruby, exports.eval.as_ref())?;
|
|
359
|
+
let eval = frames::require_export(&ruby, exports.eval.as_ref())?;
|
|
452
360
|
self.call_with_caps(&mut store, &exports, eval, ())
|
|
453
361
|
.map_err(|e| trap::call_err(&ruby, e))?;
|
|
454
362
|
self.build_snapshot(&ruby, &mut store, &exports)
|
|
@@ -472,14 +380,14 @@ impl Runtime {
|
|
|
472
380
|
) -> Result<Snapshot, MagnusError> {
|
|
473
381
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
474
382
|
let mut store = self.new_store()?;
|
|
475
|
-
install_wasi_frames(
|
|
383
|
+
frames::install_wasi_frames(
|
|
476
384
|
&mut store,
|
|
477
385
|
&self.config,
|
|
478
386
|
&[rstring_to_vec(preamble), rstring_to_vec(snippets)],
|
|
479
387
|
)?;
|
|
480
388
|
let exports = self.instantiate(&ruby, &mut store)?;
|
|
481
|
-
let run = require_export(&ruby, exports.run.as_ref())?;
|
|
482
|
-
let (env_ptr, env_len) = write_envelope(&ruby, &mut store, &exports, envelope)?;
|
|
389
|
+
let run = frames::require_export(&ruby, exports.run.as_ref())?;
|
|
390
|
+
let (env_ptr, env_len) = frames::write_envelope(&ruby, &mut store, &exports, envelope)?;
|
|
483
391
|
self.call_with_caps(&mut store, &exports, run, (env_ptr, env_len))
|
|
484
392
|
.map_err(|e| trap::call_err(&ruby, e))?;
|
|
485
393
|
self.build_snapshot(&ruby, &mut store, &exports)
|
|
@@ -544,7 +452,7 @@ impl Runtime {
|
|
|
544
452
|
.instance_pre
|
|
545
453
|
.instantiate(store.as_context_mut())
|
|
546
454
|
.map_err(|e| {
|
|
547
|
-
trap_err(
|
|
455
|
+
errors::trap_err(
|
|
548
456
|
ruby,
|
|
549
457
|
format!("failed to instantiate the Sandbox runtime: {e}"),
|
|
550
458
|
)
|
|
@@ -630,7 +538,7 @@ impl Runtime {
|
|
|
630
538
|
store: &mut WtStore<Invocation>,
|
|
631
539
|
exports: &Exports,
|
|
632
540
|
) -> Result<Snapshot, MagnusError> {
|
|
633
|
-
let return_bytes = fetch_outcome_bytes(ruby, store, exports)?;
|
|
541
|
+
let return_bytes = frames::fetch_outcome_bytes(ruby, store, exports)?;
|
|
634
542
|
let data = store.data();
|
|
635
543
|
let (stdout_raw, stderr_raw, wall_time, memory_peak) = (
|
|
636
544
|
data.stdout_bytes(),
|
|
@@ -666,170 +574,3 @@ fn disarm_caps(store: &mut WtStore<Invocation>) {
|
|
|
666
574
|
store.data_mut().stop_wall_clock();
|
|
667
575
|
store.data_mut().disarm_memory_cap();
|
|
668
576
|
}
|
|
669
|
-
|
|
670
|
-
/// Return the resolved `memory` export handle, or raise
|
|
671
|
-
/// `Kobako::TrapError` when the loaded module exports no linear
|
|
672
|
-
/// memory — the "not a Kobako-shaped runtime" failure mode
|
|
673
|
-
/// (`SANDBOX_RUNTIME_NOT_KOBAKO`).
|
|
674
|
-
fn require_memory(ruby: &Ruby, exports: &Exports) -> Result<Memory, MagnusError> {
|
|
675
|
-
exports
|
|
676
|
-
.memory
|
|
677
|
-
.ok_or_else(|| trap_err(ruby, SANDBOX_RUNTIME_NOT_KOBAKO))
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/// Allocate a `len`-byte buffer in guest linear memory via
|
|
681
|
-
/// `__kobako_alloc`, copy `envelope` into it, and return `(ptr, len)`
|
|
682
|
-
/// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
|
|
683
|
-
/// Raises `Kobako::TrapError` when the allocation hook is missing or
|
|
684
|
-
/// itself traps, and `Kobako::SandboxError` when the hook runs but
|
|
685
|
-
/// cannot reserve the buffer (`__kobako_alloc` returns 0) — an
|
|
686
|
-
/// intact runtime, not an engine fault.
|
|
687
|
-
fn write_envelope(
|
|
688
|
-
ruby: &Ruby,
|
|
689
|
-
store: &mut WtStore<Invocation>,
|
|
690
|
-
exports: &Exports,
|
|
691
|
-
envelope: RString,
|
|
692
|
-
) -> Result<(i32, i32), MagnusError> {
|
|
693
|
-
let bytes = rstring_to_vec(envelope);
|
|
694
|
-
let len_i32 = guest_mem::checked_payload_len(bytes.len()).map_err(|msg| trap_err(ruby, msg))?;
|
|
695
|
-
|
|
696
|
-
let alloc = require_export(ruby, exports.alloc.as_ref())?;
|
|
697
|
-
let memory = require_memory(ruby, exports)?;
|
|
698
|
-
|
|
699
|
-
let ptr = alloc
|
|
700
|
-
.call(store.as_context_mut(), bytes.len() as u32)
|
|
701
|
-
.map_err(|e| trap_err(ruby, format!("failed to allocate input buffer: {}", e)))?;
|
|
702
|
-
if ptr == 0 {
|
|
703
|
-
return Err(sandbox_err(
|
|
704
|
-
ruby,
|
|
705
|
-
"could not allocate input buffer (out of memory)",
|
|
706
|
-
));
|
|
707
|
-
}
|
|
708
|
-
let data = memory.data_mut(store.as_context_mut());
|
|
709
|
-
let range = guest_mem::guest_buffer_range(ptr as usize, bytes.len(), data.len())
|
|
710
|
-
.map_err(|msg| trap_err(ruby, msg))?;
|
|
711
|
-
data[range].copy_from_slice(&bytes);
|
|
712
|
-
|
|
713
|
-
Ok((ptr as i32, len_i32))
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
/// Build the per-invocation WASI context with stdin carrying every frame
|
|
717
|
-
/// in `frames` (each prefixed by its 4-byte big-endian u32 length —
|
|
718
|
-
/// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
|
|
719
|
-
/// pipes, and install it on the invocation's Store. `#eval` passes three
|
|
720
|
-
/// frames (preamble, source, snippets), `#run` passes two (preamble,
|
|
721
|
-
/// snippets — the invocation envelope arrives via linear memory
|
|
722
|
-
/// instead). Each output pipe is sized at `cap + 1` so
|
|
723
|
-
/// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
|
|
724
|
-
/// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
|
|
725
|
-
/// rely on `memory_limit` for the real ceiling.
|
|
726
|
-
/// Raises `Kobako::TrapError` when any frame exceeds the 16 MiB cap that
|
|
727
|
-
/// keeps its `u32` length prefix from wrapping.
|
|
728
|
-
fn install_wasi_frames(
|
|
729
|
-
store: &mut WtStore<Invocation>,
|
|
730
|
-
config: &Config,
|
|
731
|
-
frames: &[Vec<u8>],
|
|
732
|
-
) -> Result<(), MagnusError> {
|
|
733
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
734
|
-
// Every frame carries the same 16 MiB cap as the `#run` envelope
|
|
735
|
-
// (`write_envelope`): the length prefix is a `u32`, so a frame past
|
|
736
|
-
// the cap would silently wrap and corrupt the stdin frame stream.
|
|
737
|
-
for frame in frames {
|
|
738
|
-
guest_mem::checked_payload_len(frame.len()).map_err(|msg| trap_err(&ruby, msg))?;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
let total: usize = frames.iter().map(|f| 4 + f.len()).sum();
|
|
742
|
-
let mut stdin_content: Vec<u8> = Vec::with_capacity(total);
|
|
743
|
-
for frame in frames {
|
|
744
|
-
stdin_content.extend_from_slice(&(frame.len() as u32).to_be_bytes());
|
|
745
|
-
stdin_content.extend_from_slice(frame);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
let stdin_pipe = MemoryInputPipe::new(stdin_content);
|
|
749
|
-
let stdout_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stdout_limit_bytes));
|
|
750
|
-
let stderr_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stderr_limit_bytes));
|
|
751
|
-
|
|
752
|
-
let mut builder = WasiCtxBuilder::new();
|
|
753
|
-
builder.stdin(stdin_pipe);
|
|
754
|
-
builder.stdout(stdout_pipe.clone());
|
|
755
|
-
builder.stderr(stderr_pipe.clone());
|
|
756
|
-
// Deny the preview1 ambient-authority imports the guest never legitimately
|
|
757
|
-
// reaches but the WASI layer would otherwise grant (see `ambient`).
|
|
758
|
-
builder.wall_clock(ambient::FrozenWallClock);
|
|
759
|
-
builder.monotonic_clock(ambient::FrozenMonotonicClock);
|
|
760
|
-
builder.secure_random(ambient::deterministic_rng());
|
|
761
|
-
let wasi = builder.build_p1();
|
|
762
|
-
|
|
763
|
-
store
|
|
764
|
-
.data_mut()
|
|
765
|
-
.install_wasi(wasi, stdout_pipe, stderr_pipe);
|
|
766
|
-
Ok(())
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
/// Invoke `__kobako_take_outcome`, decode the packed `(ptr<<32)|len`
|
|
770
|
-
/// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Raises
|
|
771
|
-
/// `Kobako::TrapError` when the export is missing, `len` exceeds the
|
|
772
|
-
/// 16 MiB single-dispatch cap, the `ptr`/`len` arithmetic overflows,
|
|
773
|
-
/// the slice falls outside live memory, or the `memory` export itself
|
|
774
|
-
/// is absent.
|
|
775
|
-
fn fetch_outcome_bytes(
|
|
776
|
-
ruby: &Ruby,
|
|
777
|
-
store: &mut WtStore<Invocation>,
|
|
778
|
-
exports: &Exports,
|
|
779
|
-
) -> Result<Vec<u8>, MagnusError> {
|
|
780
|
-
let take = require_export(ruby, exports.take_outcome.as_ref())?;
|
|
781
|
-
let mem = require_memory(ruby, exports)?;
|
|
782
|
-
|
|
783
|
-
let packed = take
|
|
784
|
-
.call(store.as_context_mut(), ())
|
|
785
|
-
.map_err(|e| trap_err(ruby, format!("failed to read the Sandbox result: {}", e)))?;
|
|
786
|
-
let (ptr, len) = guest_mem::unpack_outcome_packed(packed);
|
|
787
|
-
if len > guest_mem::MAX_DISPATCH_PAYLOAD {
|
|
788
|
-
return Err(trap_err(ruby, "result payload exceeds the 16 MiB limit"));
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
let data = mem.data(store.as_context_mut());
|
|
792
|
-
let range = guest_mem::guest_buffer_range(ptr, len, data.len()).map_err(|msg| {
|
|
793
|
-
trap_err(
|
|
794
|
-
ruby,
|
|
795
|
-
format!("the Sandbox result is out of bounds: {}", msg),
|
|
796
|
-
)
|
|
797
|
-
})?;
|
|
798
|
-
Ok(data[range].to_vec())
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/// User-facing message for the "Sandbox runtime is missing one of the
|
|
802
|
-
/// internal Kobako hooks" failure mode. Phrased in caller vocabulary —
|
|
803
|
-
/// the underlying ABI symbol names (`__kobako_alloc`, `__kobako_eval`,
|
|
804
|
-
/// `__kobako_take_outcome`) are not actionable to callers, and the
|
|
805
|
-
/// gem itself raises this error so a self-reference like "matches the
|
|
806
|
-
/// kobako gem version" reads as third-person. The actionable
|
|
807
|
-
/// diagnosis is "your data/kobako.wasm is out of sync; rebuild it".
|
|
808
|
-
const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required hooks; \
|
|
809
|
-
rebuild data/kobako.wasm against the installed version";
|
|
810
|
-
|
|
811
|
-
/// User-facing message for the "the loaded Wasm module is not a
|
|
812
|
-
/// Kobako-shaped runtime at all" failure mode (no linear memory
|
|
813
|
-
/// export). Same phrasing philosophy as
|
|
814
|
-
/// `SANDBOX_RUNTIME_MISSING_HOOKS`.
|
|
815
|
-
const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
|
|
816
|
-
"the loaded Wasm module is not a Kobako-compatible runtime";
|
|
817
|
-
|
|
818
|
-
/// Return the resolved `TypedFunc` for an ABI export, or raise
|
|
819
|
-
/// `Kobako::TrapError` when the option is `None`. Both run-path
|
|
820
|
-
/// methods (`#eval`, `#run`) plus the `build_snapshot` readout that
|
|
821
|
-
/// drains `OUTCOME_BUFFER` share the same "missing export → Ruby
|
|
822
|
-
/// error" boilerplate; this helper collapses those sites onto one
|
|
823
|
-
/// safe entry. The user-facing message is intentionally export-
|
|
824
|
-
/// agnostic (see `SANDBOX_RUNTIME_MISSING_HOOKS`) — the ABI symbol
|
|
825
|
-
/// name is not actionable to callers, so it is not threaded in.
|
|
826
|
-
fn require_export<'a, Params, Results>(
|
|
827
|
-
ruby: &Ruby,
|
|
828
|
-
export: Option<&'a TypedFunc<Params, Results>>,
|
|
829
|
-
) -> Result<&'a TypedFunc<Params, Results>, MagnusError>
|
|
830
|
-
where
|
|
831
|
-
Params: wasmtime::WasmParams,
|
|
832
|
-
Results: wasmtime::WasmResults,
|
|
833
|
-
{
|
|
834
|
-
export.ok_or_else(|| trap_err(ruby, SANDBOX_RUNTIME_MISSING_HOOKS))
|
|
835
|
-
}
|
|
@@ -43,9 +43,9 @@ module Kobako
|
|
|
43
43
|
# and the allocator share the same invariant.
|
|
44
44
|
#
|
|
45
45
|
# Returning a Handle (rather than a bare Integer id) keeps the
|
|
46
|
-
# allocator's output a domain entity
|
|
47
|
-
#
|
|
48
|
-
#
|
|
46
|
+
# allocator's output a domain entity. An id is the Handle's only
|
|
47
|
+
# content, so the same internal +Kobako::Handle.restore+ constructor
|
|
48
|
+
# serves both this allocator and the codec's wire-decode path.
|
|
49
49
|
def alloc(object)
|
|
50
50
|
reject_unwrappable!(object)
|
|
51
51
|
ensure_capacity!
|
|
@@ -72,7 +72,7 @@ module Kobako
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
# Number of currently-bound entries. Used by tests of the Dispatcher
|
|
75
|
-
# and Codec::
|
|
75
|
+
# and Codec::HandleWalk#deep_wrap to observe whether each path allocates
|
|
76
76
|
# exactly the Handle entries it should — the +Handles+ table itself never
|
|
77
77
|
# consults its own size, but the surrounding code's allocation
|
|
78
78
|
# contract is part of the observable boundary.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../handle"
|
|
4
|
+
|
|
5
|
+
module Kobako
|
|
6
|
+
module Codec
|
|
7
|
+
# Substitutes Capability Handles into and out of a Ruby value tree at
|
|
8
|
+
# the host↔guest boundary. {deep_wrap} allocates a +Kobako::Handle+ for
|
|
9
|
+
# each non-wire-representable leaf on the host→guest +#run+ argument
|
|
10
|
+
# path; {deep_restore} resolves each wire-decoded Handle back to its
|
|
11
|
+
# host object on every guest→host value path — the +#eval+ / +#run+
|
|
12
|
+
# result and the yield-block result alike. {representable?} is the
|
|
13
|
+
# by-value codec-type predicate that decides which leaves {deep_wrap}
|
|
14
|
+
# must wrap: the closed 12-entry wire type set
|
|
15
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
16
|
+
# Mapping).
|
|
17
|
+
#
|
|
18
|
+
# All helpers are pure except {deep_wrap}, whose only side effect is
|
|
19
|
+
# allocating new Handle ids into the supplied table.
|
|
20
|
+
module HandleWalk
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Inclusive Integer range the msgpack gem encodes without raising
|
|
24
|
+
# +RangeError+ at encode time — signed +int 64+ minimum through
|
|
25
|
+
# unsigned +uint 64+ maximum
|
|
26
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
27
|
+
# Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
|
|
28
|
+
# Anchored as a +Range+ so {primitive_type?} stays a single
|
|
29
|
+
# dispatch line. This is the codec's encode domain — not to
|
|
30
|
+
# be confused with the Handle id range, which lives on
|
|
31
|
+
# +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
|
|
32
|
+
# represents a different concept entirely.
|
|
33
|
+
MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
|
|
34
|
+
|
|
35
|
+
# Codec-type predicate
|
|
36
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
37
|
+
# Mapping). Returns +true+ when +value+ belongs to the closed
|
|
38
|
+
# 12-entry codec type set — +nil+, +TrueClass+, +FalseClass+,
|
|
39
|
+
# +Integer+ (in the +i64..u64+ value domain), +Float+, +String+,
|
|
40
|
+
# +Symbol+, +Kobako::Handle+, +Array+ whose every element is itself
|
|
41
|
+
# representable, or +Hash+ whose every key and value are
|
|
42
|
+
# representable. Integers outside the codec's signed-64 /
|
|
43
|
+
# unsigned-64 union are rejected so the predicate agrees with the
|
|
44
|
+
# msgpack gem's encode-time +RangeError+ behaviour the codec
|
|
45
|
+
# already surfaces as {UnsupportedType}.
|
|
46
|
+
def representable?(value)
|
|
47
|
+
primitive_type?(value) || container_representable?(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
51
|
+
# leaf that fails {representable?} with a +Kobako::Handle+
|
|
52
|
+
# allocated from +handler+. The
|
|
53
|
+
# walk only descends through representable container shapes
|
|
54
|
+
# (Array, Hash) one structural level at a time; a non-representable
|
|
55
|
+
# leaf is wrapped as-is without inspecting its internal structure.
|
|
56
|
+
# An existing +Kobako::Handle+ is representable and passes through
|
|
57
|
+
# unchanged — auto-wrap never re-wraps a Handle.
|
|
58
|
+
#
|
|
59
|
+
# +value+ may be any Ruby value; +handler+ must respond to
|
|
60
|
+
# +#alloc(object) -> Kobako::Handle+ (a host-side
|
|
61
|
+
# +Kobako::Catalog::Handles+). Returns a structurally equivalent value
|
|
62
|
+
# whose leaves are either representable or +Kobako::Handle+
|
|
63
|
+
# tokens.
|
|
64
|
+
#
|
|
65
|
+
# The block bodies spell +HandleWalk.deep_wrap+ explicitly rather
|
|
66
|
+
# than the unqualified +deep_wrap+ because +module_function+ makes
|
|
67
|
+
# the instance copy of these helpers private; an implicit receiver
|
|
68
|
+
# inside a block would resolve against the enclosing +self+
|
|
69
|
+
# (still +HandleWalk+ at definition time, but the qualified form
|
|
70
|
+
# keeps the dispatch readable when the recursive call sits inside a
|
|
71
|
+
# Proc captured from elsewhere).
|
|
72
|
+
def deep_wrap(value, handler)
|
|
73
|
+
case value
|
|
74
|
+
when ::Array then value.map { |element| HandleWalk.deep_wrap(element, handler) }
|
|
75
|
+
when ::Hash then value.transform_values { |val| HandleWalk.deep_wrap(val, handler) }
|
|
76
|
+
else
|
|
77
|
+
representable?(value) ? value : handler.alloc(value)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
82
|
+
# +Kobako::Handle+ leaf with the host-side object +handler+ resolves
|
|
83
|
+
# it to. The symmetric inverse of {deep_wrap}: that walk allocates objects
|
|
84
|
+
# into Handles on the host→guest argument path; this walk resolves
|
|
85
|
+
# Handles back to their objects on every guest→host value path — the
|
|
86
|
+
# +#eval+ / +#run+ result and the yield-block result alike. The walk
|
|
87
|
+
# descends through Array elements and Hash keys and values one
|
|
88
|
+
# structural level at a time; any non-Handle leaf passes through
|
|
89
|
+
# unchanged.
|
|
90
|
+
#
|
|
91
|
+
# +value+ is a decoded Ruby value (a Handle here is a wire-decoded
|
|
92
|
+
# +Kobako::Handle+, never a guest-forged one); +handler+ must
|
|
93
|
+
# respond to +#fetch(id) -> object+ (a host-side
|
|
94
|
+
# +Kobako::Catalog::Handles+). +handler.fetch+ raises
|
|
95
|
+
# +Kobako::SandboxError+ for an id with no live binding, the
|
|
96
|
+
# corrupted-runtime fallback.
|
|
97
|
+
def deep_restore(value, handler)
|
|
98
|
+
case value
|
|
99
|
+
when ::Array then value.map { |element| HandleWalk.deep_restore(element, handler) }
|
|
100
|
+
when ::Hash
|
|
101
|
+
value.to_h { |key, val| [HandleWalk.deep_restore(key, handler), HandleWalk.deep_restore(val, handler)] }
|
|
102
|
+
when Kobako::Handle then handler.fetch(value.id)
|
|
103
|
+
else value
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The non-container branch of {representable?}: returns +true+ for
|
|
108
|
+
# the scalar leaves and an existing Handle. Not part of the
|
|
109
|
+
# public surface; reach for {representable?} instead.
|
|
110
|
+
def primitive_type?(value)
|
|
111
|
+
case value
|
|
112
|
+
when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
|
|
113
|
+
when ::Integer then MSGPACK_INT_RANGE.cover?(value)
|
|
114
|
+
else false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The container branch of {representable?}: recurses into Array
|
|
119
|
+
# elements and Hash key+value pairs through the public
|
|
120
|
+
# {representable?}. Not part of the public surface; reach for
|
|
121
|
+
# {representable?} instead.
|
|
122
|
+
def container_representable?(value)
|
|
123
|
+
case value
|
|
124
|
+
when ::Array then value.all? { |element| HandleWalk.representable?(element) }
|
|
125
|
+
when ::Hash then value.all? { |key, val| HandleWalk.representable?(key) && HandleWalk.representable?(val) }
|
|
126
|
+
else false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|