kobako 0.12.2 → 0.13.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 +11 -0
- data/Cargo.lock +126 -137
- data/README.md +2 -0
- data/crates/kobako-runtime/CHANGELOG.md +8 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/lib.rs +4 -2
- data/crates/kobako-runtime/src/profile.rs +35 -0
- data/crates/kobako-runtime/src/runtime.rs +7 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +8 -0
- data/crates/kobako-wasmtime/Cargo.toml +4 -4
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/ambient.rs +5 -2
- data/crates/kobako-wasmtime/src/config.rs +10 -2
- data/crates/kobako-wasmtime/src/driver.rs +12 -0
- data/crates/kobako-wasmtime/src/frames.rs +105 -5
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +0 -2
- data/ext/kobako/src/runtime.rs +114 -38
- data/lib/kobako/codec.rb +1 -1
- data/lib/kobako/sandbox.rb +58 -28
- data/lib/kobako/sandbox_options.rb +51 -9
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +0 -1
- data/release-please-config.json +31 -1
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +11 -2
- metadata +2 -4
- data/ext/kobako/src/snapshot.rs +0 -75
- data/lib/kobako/snapshot.rb +0 -32
- data/sig/kobako/snapshot.rbs +0 -12
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//! Kobako::Runtime — wraps a `kobako_wasmtime::Driver` + the Ruby seams
|
|
6
6
|
//!
|
|
7
7
|
//! constructed via `Kobako::Runtime.from_path(path, timeout, memory_limit,
|
|
8
|
-
//! stdout_limit, stderr_limit)`. Every invocation (`#eval` / `#run`)
|
|
8
|
+
//! stdout_limit, stderr_limit, profile)`. Every invocation (`#eval` / `#run`)
|
|
9
9
|
//! instantiates a fresh instance and discards the whole Store afterwards —
|
|
10
10
|
//! the per-invocation instance discipline. The run mechanics —
|
|
11
11
|
//! engine/module caches, caps, trap classification — live in the
|
|
@@ -21,24 +21,25 @@
|
|
|
21
21
|
//!
|
|
22
22
|
//! This file owns the `Kobako::Runtime` magnus class itself — the Ruby
|
|
23
23
|
//! init() that registers the class, the byte↔`RString` shuttling, the
|
|
24
|
-
//! dispatch-Proc GC root, and the per-invocation usage
|
|
24
|
+
//! dispatch-Proc GC root, and the per-invocation usage / capture readouts.
|
|
25
25
|
|
|
26
26
|
mod bridge;
|
|
27
27
|
mod errors;
|
|
28
28
|
|
|
29
|
-
use magnus::{
|
|
29
|
+
use magnus::{
|
|
30
|
+
function, gc, method, prelude::*, typed_data::DataTypeFunctions, value::Opaque,
|
|
31
|
+
Error as MagnusError, RArray, RModule, RString, Ruby, Symbol, TypedData, Value,
|
|
32
|
+
};
|
|
30
33
|
|
|
31
|
-
use std::cell::Cell;
|
|
34
|
+
use std::cell::{Cell, RefCell};
|
|
32
35
|
use std::path::Path;
|
|
33
36
|
use std::sync::Arc;
|
|
34
37
|
use std::time::Duration;
|
|
35
38
|
|
|
36
|
-
use magnus::{gc, typed_data::DataTypeFunctions, value::Opaque, RArray, TypedData, Value};
|
|
37
|
-
|
|
38
|
-
use crate::snapshot::Snapshot;
|
|
39
39
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
40
|
+
use kobako_runtime::profile::Profile;
|
|
40
41
|
use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
|
|
41
|
-
use kobako_runtime::snapshot::{Completion, Snapshot as RuntimeSnapshot, Usage};
|
|
42
|
+
use kobako_runtime::snapshot::{Capture, Completion, Snapshot as RuntimeSnapshot, Usage};
|
|
42
43
|
use kobako_wasmtime::{Config, Driver};
|
|
43
44
|
|
|
44
45
|
/// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
|
|
@@ -52,6 +53,15 @@ fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
|
52
53
|
unsafe { s.as_slice() }.to_vec()
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/// The pre-invocation sentinel for one capture channel: no bytes, cap
|
|
57
|
+
/// not reached. Fresh `Vec`s per call because `Capture` owns its buffer.
|
|
58
|
+
fn empty_capture() -> Capture {
|
|
59
|
+
Capture {
|
|
60
|
+
bytes: Vec::new(),
|
|
61
|
+
truncated: false,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
// ---------------------------------------------------------------------------
|
|
56
66
|
// Ruby init
|
|
57
67
|
// ---------------------------------------------------------------------------
|
|
@@ -65,11 +75,13 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
65
75
|
// intermediate hierarchy is registered.
|
|
66
76
|
|
|
67
77
|
let runtime = kobako.define_class("Runtime", ruby.class_object())?;
|
|
68
|
-
runtime.define_singleton_method("from_path", function!(Runtime::from_path,
|
|
78
|
+
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 6))?;
|
|
69
79
|
runtime.define_method("on_dispatch=", method!(Runtime::set_on_dispatch, 1))?;
|
|
70
80
|
runtime.define_method("eval", method!(Runtime::eval, 3))?;
|
|
71
81
|
runtime.define_method("run", method!(Runtime::run, 3))?;
|
|
72
82
|
runtime.define_method("usage", method!(Runtime::usage, 0))?;
|
|
83
|
+
runtime.define_method("captures", method!(Runtime::captures, 0))?;
|
|
84
|
+
runtime.define_method("profile", method!(Runtime::profile, 0))?;
|
|
73
85
|
// The guest re-enters for a block yield through a frame-scoped
|
|
74
86
|
// `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
|
|
75
87
|
// method on Runtime.
|
|
@@ -93,11 +105,17 @@ struct Runtime {
|
|
|
93
105
|
// reference the one Proc this `Opaque` pins. `Cell` is sound under the
|
|
94
106
|
// GVL (see the `unsafe impl Sync` below).
|
|
95
107
|
on_dispatch: Cell<Option<Opaque<Value>>>,
|
|
96
|
-
// Usage of the most recent invocation,
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// invocation.
|
|
108
|
+
// Usage of the most recent invocation, stashed here so `#usage` reads
|
|
109
|
+
// survive the per-invocation Store teardown and the trap path's
|
|
110
|
+
// raise. Zeroed before the first invocation.
|
|
100
111
|
last_usage: Cell<Usage>,
|
|
112
|
+
// Output captures of the most recent invocation, stashed for the same
|
|
113
|
+
// reason as `last_usage`: the trap path raises, and this readout is
|
|
114
|
+
// what keeps the guest's partial output readable after a rescue.
|
|
115
|
+
// `RefCell` (not `Cell`) because `Capture` owns its byte buffer; the
|
|
116
|
+
// same GVL single-thread discipline applies (see the `unsafe impl
|
|
117
|
+
// Sync` below). Empty before the first invocation.
|
|
118
|
+
last_captures: RefCell<(Capture, Capture)>,
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
impl DataTypeFunctions for Runtime {
|
|
@@ -118,11 +136,11 @@ impl DataTypeFunctions for Runtime {
|
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
// SAFETY: magnus requires `Send + Sync` on TypedData types. The
|
|
121
|
-
// `on_dispatch` / `last_usage` `Cell`s
|
|
122
|
-
// unavailable, but every access to them
|
|
123
|
-
// thread at a time — Ruby method calls,
|
|
124
|
-
// holds the GVL. No cross-thread access to
|
|
125
|
-
// stays auto-derived.
|
|
139
|
+
// `on_dispatch` / `last_usage` `Cell`s and the `last_captures` `RefCell`
|
|
140
|
+
// make the auto-derived `Sync` unavailable, but every access to them
|
|
141
|
+
// happens under the GVL on a single thread at a time — Ruby method calls,
|
|
142
|
+
// and a GC `mark` pass that also holds the GVL. No cross-thread access to
|
|
143
|
+
// any of them can occur. `Send` stays auto-derived.
|
|
126
144
|
unsafe impl Sync for Runtime {}
|
|
127
145
|
|
|
128
146
|
impl Runtime {
|
|
@@ -135,15 +153,18 @@ impl Runtime {
|
|
|
135
153
|
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
136
154
|
/// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
|
|
137
155
|
/// are the per-channel output caps (`None`
|
|
138
|
-
/// disables)
|
|
139
|
-
/// (`
|
|
140
|
-
///
|
|
156
|
+
/// disables); `profile` is the isolation rung the driver builds
|
|
157
|
+
/// (`:permissive` / `:hermetic`). All five are validated by the
|
|
158
|
+
/// caller (`Kobako::Sandbox`); this method only refuses non-finite
|
|
159
|
+
/// or non-positive timeouts and off-ladder profiles as a defence in
|
|
160
|
+
/// depth.
|
|
141
161
|
fn from_path(
|
|
142
162
|
path: String,
|
|
143
163
|
timeout_seconds: Option<f64>,
|
|
144
164
|
memory_limit: Option<usize>,
|
|
145
165
|
stdout_limit_bytes: Option<usize>,
|
|
146
166
|
stderr_limit_bytes: Option<usize>,
|
|
167
|
+
profile: Symbol,
|
|
147
168
|
) -> Result<Self, MagnusError> {
|
|
148
169
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
149
170
|
let timeout = match timeout_seconds {
|
|
@@ -161,6 +182,19 @@ impl Runtime {
|
|
|
161
182
|
));
|
|
162
183
|
}
|
|
163
184
|
};
|
|
185
|
+
// Fail closed on an off-ladder rung: an unrecognized posture must
|
|
186
|
+
// never fall back to a grant. Same defence-in-depth posture as the
|
|
187
|
+
// timeout guard above — `SandboxOptions` is the primary validator.
|
|
188
|
+
let profile = match profile.name()?.as_ref() {
|
|
189
|
+
"hermetic" => Profile::Hermetic,
|
|
190
|
+
"permissive" => Profile::Permissive,
|
|
191
|
+
other => {
|
|
192
|
+
return Err(MagnusError::new(
|
|
193
|
+
ruby.exception_arg_error(),
|
|
194
|
+
format!("profile must be :permissive or :hermetic, got :{other}"),
|
|
195
|
+
));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
164
198
|
|
|
165
199
|
let driver = Driver::new(
|
|
166
200
|
Path::new(&path),
|
|
@@ -169,6 +203,7 @@ impl Runtime {
|
|
|
169
203
|
timeout,
|
|
170
204
|
stdout_limit_bytes,
|
|
171
205
|
stderr_limit_bytes,
|
|
206
|
+
profile,
|
|
172
207
|
},
|
|
173
208
|
)
|
|
174
209
|
.map_err(|e| errors::setup_to_magnus(&ruby, e))?;
|
|
@@ -179,6 +214,7 @@ impl Runtime {
|
|
|
179
214
|
wall_time: 0.0,
|
|
180
215
|
memory_peak: 0,
|
|
181
216
|
}),
|
|
217
|
+
last_captures: RefCell::new((empty_capture(), empty_capture())),
|
|
182
218
|
})
|
|
183
219
|
}
|
|
184
220
|
|
|
@@ -204,7 +240,7 @@ impl Runtime {
|
|
|
204
240
|
/// One-shot mruby source execution (`#eval`). The Ruby-facing entry:
|
|
205
241
|
/// builds the dispatch handler from the registered Proc, hands the
|
|
206
242
|
/// three stdin frames (`preamble`, `source`, `snippets`) and the source
|
|
207
|
-
/// to the driver, and settles the
|
|
243
|
+
/// to the driver, and settles the invocation through
|
|
208
244
|
/// `finish_invocation` — or maps a could-not-start `Error` onto its
|
|
209
245
|
/// `Kobako::*` exception. The run mechanics — frames, caps, trap
|
|
210
246
|
/// classification — live in `kobako_wasmtime::Driver`.
|
|
@@ -213,7 +249,7 @@ impl Runtime {
|
|
|
213
249
|
preamble: RString,
|
|
214
250
|
source: RString,
|
|
215
251
|
snippets: RString,
|
|
216
|
-
) -> Result<
|
|
252
|
+
) -> Result<RString, MagnusError> {
|
|
217
253
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
218
254
|
let handler = self.build_handler();
|
|
219
255
|
let preamble = rstring_to_vec(preamble);
|
|
@@ -233,8 +269,8 @@ impl Runtime {
|
|
|
233
269
|
self.finish_invocation(&ruby, snapshot)
|
|
234
270
|
}
|
|
235
271
|
|
|
236
|
-
/// Execute one entrypoint dispatch (`__kobako_run`) and return
|
|
237
|
-
///
|
|
272
|
+
/// Execute one entrypoint dispatch (`__kobako_run`) and return the
|
|
273
|
+
/// guest's raw outcome bytes.
|
|
238
274
|
///
|
|
239
275
|
/// The two-frame stdin protocol (preamble + snippets; no user source
|
|
240
276
|
/// frame — docs/wire-codec.md § Invocation channels) plus the
|
|
@@ -246,7 +282,7 @@ impl Runtime {
|
|
|
246
282
|
preamble: RString,
|
|
247
283
|
snippets: RString,
|
|
248
284
|
envelope: RString,
|
|
249
|
-
) -> Result<
|
|
285
|
+
) -> Result<RString, MagnusError> {
|
|
250
286
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
251
287
|
let handler = self.build_handler();
|
|
252
288
|
let preamble = rstring_to_vec(preamble);
|
|
@@ -268,20 +304,27 @@ impl Runtime {
|
|
|
268
304
|
self.finish_invocation(&ruby, snapshot)
|
|
269
305
|
}
|
|
270
306
|
|
|
271
|
-
/// Settle one invocation's `Snapshot` at the Ruby boundary: usage
|
|
272
|
-
///
|
|
273
|
-
///
|
|
274
|
-
/// is
|
|
307
|
+
/// Settle one invocation's `Snapshot` at the Ruby boundary: usage and
|
|
308
|
+
/// the two output captures are recorded on every outcome, so the
|
|
309
|
+
/// `#usage` / `#captures` readouts survive the trap path's raise —
|
|
310
|
+
/// that is what keeps the guest's partial output readable after the
|
|
311
|
+
/// Host App rescues the trap. A completed guest invocation returns
|
|
312
|
+
/// its raw outcome bytes; the Sandbox layer decodes them.
|
|
275
313
|
fn finish_invocation(
|
|
276
314
|
&self,
|
|
277
315
|
ruby: &Ruby,
|
|
278
316
|
snapshot: RuntimeSnapshot,
|
|
279
|
-
) -> Result<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
317
|
+
) -> Result<RString, MagnusError> {
|
|
318
|
+
let RuntimeSnapshot {
|
|
319
|
+
completion,
|
|
320
|
+
stdout,
|
|
321
|
+
stderr,
|
|
322
|
+
usage,
|
|
323
|
+
} = snapshot;
|
|
324
|
+
self.last_usage.set(usage);
|
|
325
|
+
self.last_captures.replace((stdout, stderr));
|
|
326
|
+
match completion {
|
|
327
|
+
Completion::Outcome(bytes) => Ok(ruby.str_from_slice(&bytes)),
|
|
285
328
|
Completion::Trap(trap) => Err(errors::trap_to_magnus(ruby, trap)),
|
|
286
329
|
}
|
|
287
330
|
}
|
|
@@ -313,8 +356,8 @@ impl Runtime {
|
|
|
313
356
|
/// size captured at invocation entry. `0` before the first
|
|
314
357
|
/// invocation.
|
|
315
358
|
///
|
|
316
|
-
/// Reads the `last_usage` Cell `finish_invocation` populated
|
|
317
|
-
///
|
|
359
|
+
/// Reads the `last_usage` Cell `finish_invocation` populated before
|
|
360
|
+
/// the per-invocation Store was discarded.
|
|
318
361
|
fn usage(&self) -> Result<RArray, MagnusError> {
|
|
319
362
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
320
363
|
let usage = self.last_usage.get();
|
|
@@ -323,4 +366,37 @@ impl Runtime {
|
|
|
323
366
|
arr.push(usage.memory_peak)?;
|
|
324
367
|
Ok(arr)
|
|
325
368
|
}
|
|
369
|
+
|
|
370
|
+
/// Return the per-last-invocation output captures as a Ruby 4-tuple
|
|
371
|
+
/// `[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]`
|
|
372
|
+
/// — the flat positional layout mirrors `#usage`, and the element
|
|
373
|
+
/// order matches the destructure in `Kobako::Sandbox#read_captures!`;
|
|
374
|
+
/// reorder both sides together.
|
|
375
|
+
///
|
|
376
|
+
/// Reads the `last_captures` pair `finish_invocation` stashed on
|
|
377
|
+
/// every outcome, so the readout also covers the trap path, where
|
|
378
|
+
/// `#eval` / `#run` raise instead of returning outcome bytes.
|
|
379
|
+
/// Empty bytes and `false` flags before the first invocation.
|
|
380
|
+
fn captures(&self) -> Result<RArray, MagnusError> {
|
|
381
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
382
|
+
let captures = self.last_captures.borrow();
|
|
383
|
+
let (stdout, stderr) = &*captures;
|
|
384
|
+
let arr = ruby.ary_new_capa(4);
|
|
385
|
+
arr.push(ruby.str_from_slice(&stdout.bytes))?;
|
|
386
|
+
arr.push(stdout.truncated)?;
|
|
387
|
+
arr.push(ruby.str_from_slice(&stderr.bytes))?;
|
|
388
|
+
arr.push(stderr.truncated)?;
|
|
389
|
+
Ok(arr)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// Return the isolation profile the driver built, as a Symbol
|
|
393
|
+
/// (`:hermetic` / `:permissive`) — the declaration the Sandbox
|
|
394
|
+
/// compares against the posture its `profile:` option requested.
|
|
395
|
+
fn profile(&self) -> Symbol {
|
|
396
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
397
|
+
match self.driver.profile() {
|
|
398
|
+
Profile::Hermetic => ruby.to_symbol("hermetic"),
|
|
399
|
+
Profile::Permissive => ruby.to_symbol("permissive"),
|
|
400
|
+
}
|
|
401
|
+
}
|
|
326
402
|
}
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Kobako
|
|
|
22
22
|
# {Decoder} are thin wrappers that register the three kobako-specific
|
|
23
23
|
# ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
|
|
24
24
|
# envelope) on a single +MessagePack::Factory+ instance. The Rust side
|
|
25
|
-
# mirrors this layer as the +codec+ module in the +kobako-
|
|
25
|
+
# mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
|
|
26
26
|
# the ext-code constants live as module-private values on {Factory}
|
|
27
27
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
28
28
|
# +codec::EXT_ERRENV+ on that side.
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -38,14 +38,16 @@ module Kobako
|
|
|
38
38
|
|
|
39
39
|
attr_reader :wasm_path, :options
|
|
40
40
|
|
|
41
|
-
# Per-
|
|
41
|
+
# Per-option accessors forward to the immutable +SandboxOptions+ Value
|
|
42
42
|
# Object so the Host App still reads them off Sandbox directly.
|
|
43
|
-
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit
|
|
43
|
+
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile
|
|
44
44
|
|
|
45
45
|
# Returns the bytes the guest wrote to stdout during the most recent
|
|
46
46
|
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
47
47
|
# any invocation; the byte content never contains a truncation sentinel,
|
|
48
|
-
# so use +#stdout_truncated?+ to observe overflow.
|
|
48
|
+
# so use +#stdout_truncated?+ to observe overflow. Populated on every
|
|
49
|
+
# outcome — including a rescued +TrapError+, after which it holds the
|
|
50
|
+
# bytes written before the trap fired — mirroring +#usage+.
|
|
49
51
|
def stdout
|
|
50
52
|
@stdout_capture.bytes
|
|
51
53
|
end
|
|
@@ -83,25 +85,24 @@ module Kobako
|
|
|
83
85
|
# Build a fresh Sandbox.
|
|
84
86
|
#
|
|
85
87
|
# +wasm_path+ is the absolute path to the Guest Binary; defaults to the
|
|
86
|
-
# gem-bundled +data/kobako.wasm+.
|
|
87
|
-
# +stderr_limit+, +timeout+, +memory_limit+)
|
|
88
|
-
# +
|
|
88
|
+
# gem-bundled +data/kobako.wasm+. Every other keyword — the four caps
|
|
89
|
+
# (+stdout_limit+, +stderr_limit+, +timeout+, +memory_limit+) and the
|
|
90
|
+
# requested isolation profile (+profile+) — is forwarded verbatim to
|
|
91
|
+
# +Kobako::SandboxOptions+, which owns the DEFAULT fallbacks and
|
|
89
92
|
# normalisation. The constructed +SandboxOptions+ is exposed as
|
|
90
|
-
# +#options+ and
|
|
91
|
-
# +Forwardable+ delegation.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
# +#options+ and every option remains readable directly on Sandbox via
|
|
94
|
+
# +Forwardable+ delegation. The runtime builds the requested profile —
|
|
95
|
+
# +:hermetic+ (the default) denies the guest ambient time and entropy,
|
|
96
|
+
# +:permissive+ leaves them live — and construction refuses a runtime
|
|
97
|
+
# whose declared profile falls below the request, raising
|
|
98
|
+
# +Kobako::SetupError+ before any invocation entry point runs.
|
|
99
|
+
def initialize(wasm_path: nil, **)
|
|
97
100
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
98
|
-
@options = SandboxOptions.new(
|
|
99
|
-
stderr_limit: stderr_limit)
|
|
101
|
+
@options = SandboxOptions.new(**)
|
|
100
102
|
@handler = Catalog::Handles.new
|
|
101
103
|
@services = Kobako::Catalog::Namespaces.new(handler: @handler)
|
|
102
104
|
@snippets = Catalog::Snippets.new
|
|
103
|
-
@runtime =
|
|
104
|
-
@options.stdout_limit, @options.stderr_limit)
|
|
105
|
+
@runtime = build_runtime!
|
|
105
106
|
install_dispatch_proc!
|
|
106
107
|
reset_invocation_state!
|
|
107
108
|
end
|
|
@@ -209,6 +210,17 @@ module Kobako
|
|
|
209
210
|
|
|
210
211
|
private
|
|
211
212
|
|
|
213
|
+
# Construct the +Runtime+ with the requested isolation profile and
|
|
214
|
+
# refuse one whose declared posture falls below the request —
|
|
215
|
+
# +SandboxOptions#enforce_floor!+ owns the ladder comparison, so a
|
|
216
|
+
# runtime that cannot honor the request never runs guest code.
|
|
217
|
+
def build_runtime!
|
|
218
|
+
runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
219
|
+
@options.stdout_limit, @options.stderr_limit, profile)
|
|
220
|
+
@options.enforce_floor!(runtime.profile)
|
|
221
|
+
runtime
|
|
222
|
+
end
|
|
223
|
+
|
|
212
224
|
# Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
|
|
213
225
|
# dispatch +Proc+ that routes guest→host calls through the stateless
|
|
214
226
|
# +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
|
|
@@ -242,8 +254,8 @@ module Kobako
|
|
|
242
254
|
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
243
255
|
# and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
|
|
244
256
|
# both paths: the figures are stashed in the ext on every outcome, so
|
|
245
|
-
#
|
|
246
|
-
#
|
|
257
|
+
# the readout here also covers the trap path, where +Runtime#eval+ /
|
|
258
|
+
# +#run+ raise instead of returning outcome bytes.
|
|
247
259
|
#
|
|
248
260
|
# The ext returns a positional 2-tuple +[wall_time, memory_peak]+
|
|
249
261
|
# whose order matches the +Kobako::Usage+ field order; the
|
|
@@ -269,33 +281,51 @@ module Kobako
|
|
|
269
281
|
end
|
|
270
282
|
end
|
|
271
283
|
|
|
284
|
+
# Read the per-last-invocation output captures from the ext and wrap
|
|
285
|
+
# them as +Kobako::Capture+ value objects. Runs in the +invoke!+
|
|
286
|
+
# +ensure+ block next to {#read_usage!} for the same reason: the ext
|
|
287
|
+
# stashes the captures on every outcome, so the readout also covers
|
|
288
|
+
# the trap path, where +Runtime#eval+ / +#run+ raise instead of
|
|
289
|
+
# returning outcome bytes — +#stdout+ / +#stderr+ keep the guest's
|
|
290
|
+
# partial output readable after a rescue.
|
|
291
|
+
#
|
|
292
|
+
# The ext returns a positional 4-tuple
|
|
293
|
+
# +[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]+;
|
|
294
|
+
# the destructure-then-kwargs handoff below is the explicit
|
|
295
|
+
# positional→keyword conversion point.
|
|
296
|
+
def read_captures!
|
|
297
|
+
stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures
|
|
298
|
+
@stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
|
|
299
|
+
@stderr_capture = Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
|
|
300
|
+
end
|
|
301
|
+
|
|
272
302
|
# Shared prologue / epilogue + trap-class translator for both
|
|
273
303
|
# invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
|
|
274
304
|
# TrapError message so the failing export is identifiable.
|
|
275
305
|
#
|
|
276
|
-
# The yielded block must return
|
|
277
|
-
# value of +Runtime#eval+ / +#run
|
|
278
|
-
#
|
|
279
|
-
# +
|
|
280
|
-
#
|
|
306
|
+
# The yielded block must return the invocation's raw outcome bytes —
|
|
307
|
+
# i.e. the value of +Runtime#eval+ / +#run+ — which the success path
|
|
308
|
+
# feeds to +Outcome.decode+. Captures and usage are populated by the
|
|
309
|
+
# +ensure+ readouts ({#read_usage!} / {#read_captures!}) on every
|
|
310
|
+
# outcome, so +#stdout+ / +#stderr+ / +#usage+ stay readable after a
|
|
311
|
+
# rescued trap.
|
|
281
312
|
# The rescue chain is the single trap-translation boundary —
|
|
282
313
|
# configured-cap paths surface as named TrapError subclasses
|
|
283
314
|
# (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
|
|
284
315
|
# the base +TrapError+.
|
|
285
316
|
def invoke!(verb)
|
|
286
317
|
begin_invocation!
|
|
287
|
-
|
|
288
|
-
@stdout_capture = snapshot.stdout
|
|
289
|
-
@stderr_capture = snapshot.stderr
|
|
318
|
+
return_bytes = yield
|
|
290
319
|
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
291
320
|
# token; restore it to the host object the guest referenced before
|
|
292
321
|
# handing the value to the Host App. @handler still holds this
|
|
293
322
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
294
|
-
Codec::HandleWalk.deep_restore(Outcome.decode(
|
|
323
|
+
Codec::HandleWalk.deep_restore(Outcome.decode(return_bytes), @handler)
|
|
295
324
|
rescue Kobako::TrapError => e
|
|
296
325
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
297
326
|
ensure
|
|
298
327
|
read_usage!
|
|
328
|
+
read_captures!
|
|
299
329
|
end
|
|
300
330
|
end
|
|
301
331
|
end
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
3
5
|
module Kobako
|
|
4
6
|
# Kobako::SandboxOptions — immutable Value Object holding the four
|
|
5
|
-
# per-Sandbox configuration caps
|
|
6
|
-
# Data.define(...)+ subclass form (the
|
|
7
|
-
# +lib/kobako/outcome/panic.rb+).
|
|
7
|
+
# per-Sandbox configuration caps and the requested isolation profile.
|
|
8
|
+
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
9
|
+
# Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
|
|
8
10
|
#
|
|
9
|
-
# The +initialize+ normalises every
|
|
11
|
+
# The +initialize+ normalises every option before delegating to Data's
|
|
10
12
|
# +super+: +timeout+ to Float seconds, +memory_limit+ / +stdout_limit+ /
|
|
11
13
|
# +stderr_limit+ to positive Integer bytes. Each cap is +nil+-disablable
|
|
12
14
|
# (an absent argument takes its DEFAULT; an explicit +nil+ leaves the
|
|
13
|
-
# bound off), so all four behave uniformly.
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
15
|
+
# bound off), so all four behave uniformly. +profile+ is the one
|
|
16
|
+
# non-cap: a Symbol on the PROFILES ladder naming the posture the
|
|
17
|
+
# runtime builds, which is also the weakest the Host App accepts —
|
|
18
|
+
# +nil+ is rejected because the weakest posture is requested as an
|
|
19
|
+
# explicit +:permissive+. Anything that survives +SandboxOptions.new+
|
|
20
|
+
# is a wire-ready bundle the +Kobako::Runtime+ constructor consumes
|
|
21
|
+
# as-is. The options also own the ladder comparison
|
|
22
|
+
# (+#enforce_floor!+) that +Kobako::Sandbox+ delegates its
|
|
23
|
+
# construction floor check to.
|
|
24
|
+
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile)
|
|
17
25
|
# Default wall-clock timeout for a single invocation: 60 seconds.
|
|
18
26
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
19
27
|
|
|
@@ -25,17 +33,41 @@ module Kobako
|
|
|
25
33
|
# Default per-channel capture ceiling: 1 MiB.
|
|
26
34
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
27
35
|
|
|
36
|
+
# The isolation ladder, weakest first — index order is rank order,
|
|
37
|
+
# so a floor check is an index comparison.
|
|
38
|
+
PROFILES = %i[permissive hermetic].freeze
|
|
39
|
+
|
|
40
|
+
# Default isolation profile: the strictest rung — opting down to
|
|
41
|
+
# +:permissive+ is the Host App's explicit trade.
|
|
42
|
+
DEFAULT_PROFILE = :hermetic
|
|
43
|
+
|
|
28
44
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
29
45
|
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
30
46
|
stdout_limit: DEFAULT_OUTPUT_LIMIT,
|
|
31
|
-
stderr_limit: DEFAULT_OUTPUT_LIMIT
|
|
47
|
+
stderr_limit: DEFAULT_OUTPUT_LIMIT,
|
|
48
|
+
profile: DEFAULT_PROFILE)
|
|
32
49
|
timeout = normalize_timeout(timeout)
|
|
33
50
|
memory_limit = normalize_memory_limit(memory_limit)
|
|
34
51
|
stdout_limit = normalize_output_limit(stdout_limit, "stdout_limit")
|
|
35
52
|
stderr_limit = normalize_output_limit(stderr_limit, "stderr_limit")
|
|
53
|
+
profile = normalize_profile(profile)
|
|
36
54
|
super
|
|
37
55
|
end
|
|
38
56
|
|
|
57
|
+
# Enforce the requested +profile+ as the floor against +declared+ —
|
|
58
|
+
# the posture a runtime reports having built — so a runtime that
|
|
59
|
+
# cannot honor the request fails construction with
|
|
60
|
+
# +Kobako::SetupError+ instead of weakening the posture silently.
|
|
61
|
+
# Both fallbacks fail closed: a declaration off the PROFILES ladder
|
|
62
|
+
# ranks below every request, and a request off the ladder
|
|
63
|
+
# (unreachable past +initialize+) refuses every declaration.
|
|
64
|
+
def enforce_floor!(declared)
|
|
65
|
+
return if (PROFILES.index(declared) || -1) >= (PROFILES.index(profile) || PROFILES.size)
|
|
66
|
+
|
|
67
|
+
raise Kobako::SetupError, "runtime declares isolation profile #{declared.inspect}, " \
|
|
68
|
+
"below the requested floor #{profile.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
39
71
|
private
|
|
40
72
|
|
|
41
73
|
# Coerce +timeout+ into the Float seconds the ext expects, or +nil+
|
|
@@ -78,5 +110,15 @@ module Kobako
|
|
|
78
110
|
|
|
79
111
|
limit
|
|
80
112
|
end
|
|
113
|
+
|
|
114
|
+
# Validate +profile+ against the PROFILES ladder. Unlike the caps
|
|
115
|
+
# there is no +nil+ form: the weakest posture is requested as an
|
|
116
|
+
# explicit +:permissive+, so anything off the ladder — +nil+
|
|
117
|
+
# included — is rejected.
|
|
118
|
+
def normalize_profile(profile)
|
|
119
|
+
return profile if PROFILES.include?(profile)
|
|
120
|
+
|
|
121
|
+
raise ArgumentError, "profile must be one of #{PROFILES.map(&:inspect).join(", ")}, got #{profile.inspect}"
|
|
122
|
+
end
|
|
81
123
|
end
|
|
82
124
|
end
|
data/lib/kobako/version.rb
CHANGED
data/lib/kobako.rb
CHANGED
data/release-please-config.json
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"path": "/wasm/Cargo.lock",
|
|
19
19
|
"jsonpath": "$.package[?(@.name=='kobako-core')].version"
|
|
20
20
|
},
|
|
21
|
+
{
|
|
22
|
+
"type": "toml",
|
|
23
|
+
"path": "/wasm/kobako-core/Cargo.toml",
|
|
24
|
+
"jsonpath": "$.dependencies['kobako-codec'].version"
|
|
25
|
+
},
|
|
21
26
|
{
|
|
22
27
|
"type": "generic",
|
|
23
28
|
"path": "/wasm/kobako-core/README.md"
|
|
@@ -38,6 +43,11 @@
|
|
|
38
43
|
"path": "/wasm/kobako/Cargo.toml",
|
|
39
44
|
"jsonpath": "$.dependencies['kobako-core'].version"
|
|
40
45
|
},
|
|
46
|
+
{
|
|
47
|
+
"type": "toml",
|
|
48
|
+
"path": "/wasm/kobako/Cargo.toml",
|
|
49
|
+
"jsonpath": "$.dependencies['kobako-codec'].version"
|
|
50
|
+
},
|
|
41
51
|
{
|
|
42
52
|
"type": "generic",
|
|
43
53
|
"path": "/wasm/kobako/README.md"
|
|
@@ -104,6 +114,26 @@
|
|
|
104
114
|
}
|
|
105
115
|
]
|
|
106
116
|
},
|
|
117
|
+
"crates/kobako-codec": {
|
|
118
|
+
"component": "kobako-codec",
|
|
119
|
+
"release-type": "rust",
|
|
120
|
+
"extra-files": [
|
|
121
|
+
{
|
|
122
|
+
"type": "toml",
|
|
123
|
+
"path": "/crates/Cargo.lock",
|
|
124
|
+
"jsonpath": "$.package[?(@.name=='kobako-codec')].version"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"type": "toml",
|
|
128
|
+
"path": "/wasm/Cargo.lock",
|
|
129
|
+
"jsonpath": "$.package[?(@.name=='kobako-codec')].version"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"type": "generic",
|
|
133
|
+
"path": "/crates/kobako-codec/README.md"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
107
137
|
"crates/kobako-runtime": {
|
|
108
138
|
"component": "kobako-runtime",
|
|
109
139
|
"release-type": "rust",
|
|
@@ -154,7 +184,7 @@
|
|
|
154
184
|
{
|
|
155
185
|
"type": "linked-versions",
|
|
156
186
|
"groupName": "kobako crates",
|
|
157
|
-
"components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-json", "kobako-regexp", "kobako-baker", "kobako-runtime", "kobako-wasmtime"]
|
|
187
|
+
"components": ["kobako-codec", "kobako-core", "kobako-rs", "kobako-io", "kobako-json", "kobako-regexp", "kobako-baker", "kobako-runtime", "kobako-wasmtime"]
|
|
158
188
|
}
|
|
159
189
|
],
|
|
160
190
|
"extra-files": [
|
data/sig/kobako/runtime.rbs
CHANGED
|
@@ -7,17 +7,22 @@ module Kobako
|
|
|
7
7
|
(Float | Integer)? timeout_seconds,
|
|
8
8
|
Integer? memory_limit,
|
|
9
9
|
Integer? stdout_limit_bytes,
|
|
10
|
-
Integer? stderr_limit_bytes
|
|
10
|
+
Integer? stderr_limit_bytes,
|
|
11
|
+
Symbol profile
|
|
11
12
|
) -> Runtime
|
|
12
13
|
|
|
13
14
|
def on_dispatch=: (^(String, Kobako::Transport::_GuestYielder) -> String dispatch_proc) -> void
|
|
14
15
|
|
|
15
|
-
def eval: (String preamble, String source, String snippets) ->
|
|
16
|
+
def eval: (String preamble, String source, String snippets) -> String
|
|
16
17
|
|
|
17
|
-
def run: (String preamble, String snippets, String envelope) ->
|
|
18
|
+
def run: (String preamble, String snippets, String envelope) -> String
|
|
18
19
|
|
|
19
20
|
def usage: () -> [Float, Integer]
|
|
20
21
|
|
|
22
|
+
def captures: () -> [String, bool, String, bool]
|
|
23
|
+
|
|
24
|
+
def profile: () -> Symbol
|
|
25
|
+
|
|
21
26
|
# Frame-scoped guest re-entry handle the ext hands the dispatch Proc as
|
|
22
27
|
# its second argument; stands in as the +String -> String+ callable the
|
|
23
28
|
# host +Transport::Yielder+ invokes for a block yield.
|