kobako 0.27.0 → 0.29.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 +241 -0
- data/Cargo.lock +132 -126
- data/README.md +240 -147
- data/ROADMAP.md +28 -22
- data/SECURITY.md +2 -2
- data/crates/kobako-runtime/CHANGELOG.md +117 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +16 -10
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +12 -2
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +43 -17
- data/crates/kobako-runtime/src/snapshot.rs +33 -15
- data/crates/kobako-runtime/src/yielder.rs +14 -6
- data/crates/kobako-transport/CHANGELOG.md +45 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +21 -12
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +103 -61
- data/crates/kobako-transport/src/envelope/request.rs +305 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
- data/crates/kobako-wasmtime/Cargo.toml +23 -16
- data/crates/kobako-wasmtime/README.md +6 -4
- data/crates/kobako-wasmtime/src/abi.rs +67 -36
- data/crates/kobako-wasmtime/src/ambient.rs +64 -5
- data/crates/kobako-wasmtime/src/cache.rs +204 -127
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +69 -15
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
- data/crates/kobako-wasmtime/src/driver.rs +174 -125
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
- data/crates/kobako-wasmtime/src/invocation.rs +84 -412
- data/crates/kobako-wasmtime/src/io.rs +89 -0
- data/crates/kobako-wasmtime/src/lib.rs +7 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +66 -59
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +177 -113
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +11 -13
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +190 -312
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +29 -0
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +17 -60
- data/lib/kobako/catalog/snippets.rb +41 -69
- data/lib/kobako/catalog.rb +13 -10
- data/lib/kobako/codec/decoder.rb +10 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +21 -11
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +10 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +83 -117
- data/lib/kobako/errors.rb +57 -132
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +25 -55
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +128 -137
- data/lib/kobako/sandbox_options.rb +71 -90
- data/lib/kobako/snippet/bytecode.rb +11 -0
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +2 -10
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +127 -206
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +10 -17
- data/lib/kobako/transport/reflection.rb +108 -63
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +20 -94
- data/lib/kobako/transport.rb +4 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +13 -0
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +3 -5
- data/sig/kobako/catalog/snippets.rbs +5 -5
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +4 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +15 -8
- data/sig/kobako/errors.rbs +10 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -45
- data/sig/kobako/sandbox.rbs +11 -1
- data/sig/kobako/sandbox_options.rbs +25 -7
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +15 -19
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +19 -4
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +5 -9
- data/sig/kobako/transport.rbs +9 -8
- metadata +75 -10
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/lib/kobako/snippet/binary.rb +0 -26
- data/rust-toolchain.toml +0 -15
- data/sig/kobako/snippet/binary.rbs +0 -12
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! The Ruby-visible classes are
|
|
4
4
|
//!
|
|
5
|
-
//! Kobako::Runtime
|
|
6
|
-
//! Kobako::
|
|
5
|
+
//! Kobako::Runtime — wraps a `kobako_wasmtime::Driver`
|
|
6
|
+
//! Kobako::Snapshot — one invocation's completion + captures + usage
|
|
7
7
|
//!
|
|
8
8
|
//! `Kobako::Runtime` is constructed via `Kobako::Runtime.from_path(path,
|
|
9
|
-
//!
|
|
9
|
+
//! **options)`, the options keyed by the `Kobako::SandboxOptions` members. Every
|
|
10
10
|
//! invocation (`#eval` / `#run`) takes that run's dispatch handler as a call
|
|
11
11
|
//! argument, instantiates a fresh instance, and returns a `Snapshot` — the
|
|
12
12
|
//! whole per-invocation result — so the Runtime holds no per-invocation
|
|
@@ -15,21 +15,20 @@
|
|
|
15
15
|
//! `kobako-wasmtime` crate behind the `kobako_runtime` contract; no wasm
|
|
16
16
|
//! engine type reaches this crate or the Host App.
|
|
17
17
|
//!
|
|
18
|
-
//!
|
|
19
|
-
//!
|
|
20
|
-
//! * `bridge` — the magnus dispatch bridge: `RubyDispatchHandler` plus the
|
|
21
|
-
//! frame-scoped `GuestYielder` Ruby class.
|
|
22
|
-
//! * `errors` — the single boundary mapping the neutral `Trap` /
|
|
23
|
-
//! `SetupError` channels onto the `Kobako::*` classes for a failure that
|
|
24
|
-
//! never produced a `Snapshot` (a could-not-start fault).
|
|
18
|
+
//! Each module holds one responsibility and opens with its own doc.
|
|
25
19
|
|
|
26
20
|
mod bridge;
|
|
27
21
|
mod errors;
|
|
22
|
+
mod escape;
|
|
23
|
+
mod filesystem;
|
|
28
24
|
mod gvl;
|
|
25
|
+
mod pinned_value;
|
|
26
|
+
mod snapshot;
|
|
29
27
|
|
|
30
28
|
use magnus::{
|
|
31
|
-
function, method, prelude::*,
|
|
32
|
-
Error as MagnusError, RArray, RModule, RString, Ruby, Symbol, TypedData,
|
|
29
|
+
function, method, prelude::*, scan_args::get_kwargs, typed_data::DataTypeFunctions,
|
|
30
|
+
value::Opaque, Error as MagnusError, RArray, RHash, RModule, RString, Ruby, Symbol, TypedData,
|
|
31
|
+
Value,
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
use std::path::Path;
|
|
@@ -37,53 +36,31 @@ use std::sync::Arc;
|
|
|
37
36
|
use std::time::Duration;
|
|
38
37
|
|
|
39
38
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
40
|
-
use kobako_runtime::error::Trap;
|
|
41
39
|
use kobako_runtime::profile::Profile;
|
|
42
|
-
use kobako_runtime::runtime::{Entry,
|
|
43
|
-
use
|
|
44
|
-
use kobako_transport::envelope::{Bindings, Outcome, Run, Snippet, Snippets};
|
|
40
|
+
use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
|
|
41
|
+
use kobako_transport::envelope::{Bindings, Flag, Preamble, Snippet, Snippets};
|
|
45
42
|
use kobako_wasmtime::{Config, Driver};
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
/// backtrace, and the names the invocation could have used in place of the
|
|
49
|
-
/// one it named. Named so the Ruby side reads them positionally against one
|
|
50
|
-
/// shape, matching the `Kobako::Outcome::panic_fields` alias.
|
|
51
|
-
type PanicFields = (String, String, String, Vec<String>, Vec<String>);
|
|
44
|
+
use snapshot::Snapshot;
|
|
52
45
|
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
/// does not outlive this call, so no Ruby allocation can move the
|
|
57
|
-
/// underlying RString between the borrow and the copy — the safety
|
|
58
|
-
/// invariant the inline form relied on is established once here.
|
|
46
|
+
/// The one place the boundary borrows an RString's bytes: the borrow does
|
|
47
|
+
/// not outlive this call, so no Ruby allocation can move the RString
|
|
48
|
+
/// between the borrow and the copy.
|
|
59
49
|
fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
60
50
|
// SAFETY: see item doc.
|
|
61
51
|
unsafe { s.as_slice() }.to_vec()
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
///
|
|
65
|
-
///
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Ok(Bindings {
|
|
69
|
-
paths: paths.to_vec()?,
|
|
70
|
-
}
|
|
71
|
-
.encode())
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/// Frame the Frame 3 snippet table from the registry's entries — one
|
|
75
|
-
/// `[kind, name, body]` triple each, `kind` a Symbol naming the form so
|
|
76
|
-
/// the wire's discriminant byte stays here. An off-ladder kind raises
|
|
77
|
-
/// rather than defaulting, the same fail-closed posture `from_path`
|
|
78
|
-
/// takes on its Symbol options.
|
|
79
|
-
fn frame_snippets(ruby: &Ruby, entries: RArray) -> Result<Vec<u8>, MagnusError> {
|
|
80
|
-
let mut frame = Snippets {
|
|
54
|
+
/// `kind` arrives as a Symbol so the registry never names the wire's
|
|
55
|
+
/// discriminant; an off-ladder kind raises rather than defaulting.
|
|
56
|
+
fn to_snippets(ruby: &Ruby, entries: RArray) -> Result<Snippets, MagnusError> {
|
|
57
|
+
let mut snippets = Snippets {
|
|
81
58
|
entries: Vec::with_capacity(entries.len()),
|
|
82
59
|
};
|
|
83
60
|
for index in 0..entries.len() as isize {
|
|
84
61
|
let entry: RArray = entries.entry(index)?;
|
|
85
62
|
let kind: Symbol = entry.entry(0)?;
|
|
86
|
-
|
|
63
|
+
snippets.entries.push(match kind.name()?.as_ref() {
|
|
87
64
|
"source" => Snippet::Source {
|
|
88
65
|
name: entry.entry(1)?,
|
|
89
66
|
body: entry.entry(2)?,
|
|
@@ -99,7 +76,30 @@ fn frame_snippets(ruby: &Ruby, entries: RArray) -> Result<Vec<u8>, MagnusError>
|
|
|
99
76
|
}
|
|
100
77
|
});
|
|
101
78
|
}
|
|
102
|
-
Ok(
|
|
79
|
+
Ok(snippets)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// One owner for the Preamble every guest entry reads, so an invocation
|
|
83
|
+
/// and a compile cannot drift in what they send. An invocation's setup
|
|
84
|
+
/// and a compile's preamble both open with its paths, snippets, and flags.
|
|
85
|
+
fn to_preamble(ruby: &Ruby, parts: RArray) -> Result<Preamble, MagnusError> {
|
|
86
|
+
let paths: RArray = parts.entry(0)?;
|
|
87
|
+
let flags: RArray = parts.entry(2)?;
|
|
88
|
+
let mut preamble = Preamble::default();
|
|
89
|
+
preamble.bindings = Bindings {
|
|
90
|
+
paths: paths.to_vec()?,
|
|
91
|
+
};
|
|
92
|
+
preamble.snippets = to_snippets(ruby, parts.entry(1)?)?;
|
|
93
|
+
preamble.flags = flags
|
|
94
|
+
.to_vec::<(String, String, bool)>()?
|
|
95
|
+
.into_iter()
|
|
96
|
+
.map(|(name, value, critical)| Flag {
|
|
97
|
+
name,
|
|
98
|
+
value,
|
|
99
|
+
critical,
|
|
100
|
+
})
|
|
101
|
+
.collect();
|
|
102
|
+
Ok(preamble)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// ---------------------------------------------------------------------------
|
|
@@ -112,27 +112,27 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
112
112
|
// in `runtime/errors.rs` — no intermediate hierarchy is registered.
|
|
113
113
|
|
|
114
114
|
let runtime = kobako.define_class("Runtime", ruby.class_object())?;
|
|
115
|
-
runtime.define_singleton_method("from_path", function!(Runtime::from_path,
|
|
116
|
-
runtime.define_method("eval", method!(Runtime::eval,
|
|
117
|
-
runtime.define_method("run", method!(Runtime::run,
|
|
115
|
+
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 2))?;
|
|
116
|
+
runtime.define_method("eval", method!(Runtime::eval, 3))?;
|
|
117
|
+
runtime.define_method("run", method!(Runtime::run, 4))?;
|
|
118
118
|
runtime.define_method("profile", method!(Runtime::profile, 0))?;
|
|
119
|
+
runtime.define_method("compile", method!(Runtime::compile, 1))?;
|
|
120
|
+
runtime.define_singleton_method("validate_mount!", function!(filesystem::validate_mount, 3))?;
|
|
119
121
|
// The guest re-enters for a block yield through a frame-scoped
|
|
120
|
-
// `Kobako::
|
|
121
|
-
//
|
|
122
|
-
bridge::register(
|
|
122
|
+
// `Kobako::GuestYielder` the dispatcher hands the Proc, not a method on
|
|
123
|
+
// Runtime.
|
|
124
|
+
bridge::register(kobako)?;
|
|
125
|
+
filesystem::register(ruby, kobako)?;
|
|
123
126
|
|
|
124
127
|
// Snapshot — the per-invocation result object each entry point returns.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
snapshot.define_method("stdout_truncated?", method!(Snapshot::stdout_truncated, 0))?;
|
|
134
|
-
snapshot.define_method("stderr", method!(Snapshot::stderr, 0))?;
|
|
135
|
-
snapshot.define_method("stderr_truncated?", method!(Snapshot::stderr_truncated, 0))?;
|
|
128
|
+
snapshot::register(ruby, kobako)?;
|
|
129
|
+
|
|
130
|
+
// A wrapped class is otherwise looked up the first time an object is
|
|
131
|
+
// wrapped, through a Ruby call that a pending interrupt would turn into
|
|
132
|
+
// a panic; a signal arriving during a process's first run waits for
|
|
133
|
+
// exactly that moment.
|
|
134
|
+
Runtime::class(ruby);
|
|
135
|
+
Snapshot::class(ruby);
|
|
136
136
|
|
|
137
137
|
Ok(())
|
|
138
138
|
}
|
|
@@ -157,49 +157,62 @@ struct Runtime {
|
|
|
157
157
|
impl DataTypeFunctions for Runtime {}
|
|
158
158
|
|
|
159
159
|
impl Runtime {
|
|
160
|
-
///
|
|
161
|
-
///
|
|
162
|
-
///
|
|
163
|
-
|
|
164
|
-
///
|
|
165
|
-
/// `timeout_seconds` is the wall-clock cap in seconds
|
|
166
|
-
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
167
|
-
/// bytes (`None` disables); `stdout_limit` / `stderr_limit`
|
|
168
|
-
/// are the per-channel output caps (`None`
|
|
169
|
-
/// disables); `profile` is the isolation rung the driver builds
|
|
170
|
-
/// (`:permissive` / `:hermetic`); `gvl` is the scheduling mode
|
|
171
|
-
/// (`:hold` / `:release`) deciding whether each invocation releases
|
|
172
|
-
/// the GVL for its guest span. All six are validated by the caller
|
|
173
|
-
/// (`Kobako::Sandbox`); this method only refuses non-finite or
|
|
174
|
-
/// non-positive timeouts, off-ladder profiles, and unrecognized gvl
|
|
175
|
-
/// modes as a defence in depth.
|
|
176
|
-
fn from_path(
|
|
177
|
-
path: String,
|
|
178
|
-
timeout_seconds: Option<f64>,
|
|
179
|
-
memory_limit: Option<usize>,
|
|
180
|
-
stdout_limit: Option<usize>,
|
|
181
|
-
stderr_limit: Option<usize>,
|
|
182
|
-
profile: Symbol,
|
|
183
|
-
gvl: Symbol,
|
|
184
|
-
) -> Result<Self, MagnusError> {
|
|
160
|
+
/// The only Ruby-facing constructor, so Engine and Module are never
|
|
161
|
+
/// visible to Ruby. Every option is a required keyword: the defaults
|
|
162
|
+
/// belong to `Kobako::SandboxOptions`, which names the keys.
|
|
163
|
+
fn from_path(path: String, options: RHash) -> Result<Self, MagnusError> {
|
|
185
164
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
165
|
+
type Options = (
|
|
166
|
+
Option<f64>,
|
|
167
|
+
Option<f64>,
|
|
168
|
+
Option<usize>,
|
|
169
|
+
Option<usize>,
|
|
170
|
+
Option<usize>,
|
|
171
|
+
Option<usize>,
|
|
172
|
+
Symbol,
|
|
173
|
+
Symbol,
|
|
174
|
+
);
|
|
175
|
+
let options = get_kwargs::<_, Options, (), ()>(
|
|
176
|
+
options,
|
|
177
|
+
&[
|
|
178
|
+
"timeout",
|
|
179
|
+
"setup_timeout",
|
|
180
|
+
"memory_limit",
|
|
181
|
+
"stdout_limit",
|
|
182
|
+
"stderr_limit",
|
|
183
|
+
"open_file_limit",
|
|
184
|
+
"profile",
|
|
185
|
+
"gvl",
|
|
186
|
+
],
|
|
187
|
+
&[],
|
|
188
|
+
)?;
|
|
189
|
+
let (
|
|
190
|
+
timeout_seconds,
|
|
191
|
+
setup_timeout_seconds,
|
|
192
|
+
memory_limit,
|
|
193
|
+
stdout_limit,
|
|
194
|
+
stderr_limit,
|
|
195
|
+
open_file_limit,
|
|
196
|
+
profile,
|
|
197
|
+
gvl,
|
|
198
|
+
) = options.required;
|
|
199
|
+
// An invalid cap argument is a Host App programming error and
|
|
200
|
+
// raises `ArgumentError`, outside the construction-failure
|
|
201
|
+
// `SetupError` branch. `SandboxOptions` is the primary guard (it
|
|
202
|
+
// never lets a bad timeout reach here); this is defence-in-depth
|
|
203
|
+
// for direct `from_path` calls.
|
|
204
|
+
let to_duration = |seconds: Option<f64>, name: &str| match seconds {
|
|
205
|
+
None => Ok(None),
|
|
206
|
+
Some(secs) if secs.is_finite() && secs > 0.0 => Ok(Some(Duration::from_secs_f64(secs))),
|
|
207
|
+
Some(secs) => Err(MagnusError::new(
|
|
208
|
+
ruby.exception_arg_error(),
|
|
209
|
+
format!("{name} must be > 0 and finite, got {secs} seconds"),
|
|
210
|
+
)),
|
|
200
211
|
};
|
|
212
|
+
let timeout = to_duration(timeout_seconds, "timeout")?;
|
|
213
|
+
let setup_timeout = to_duration(setup_timeout_seconds, "setup_timeout")?;
|
|
201
214
|
// Fail closed on an off-ladder rung: an unrecognized posture must
|
|
202
|
-
// never fall back to a
|
|
215
|
+
// never fall back to a weaker posture. Same defence in depth as the
|
|
203
216
|
// timeout guard above — `SandboxOptions` is the primary validator.
|
|
204
217
|
let profile = match profile.name()?.as_ref() {
|
|
205
218
|
"hermetic" => Profile::Hermetic,
|
|
@@ -225,17 +238,16 @@ impl Runtime {
|
|
|
225
238
|
}
|
|
226
239
|
};
|
|
227
240
|
|
|
228
|
-
let
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
.map_err(|e| errors::setup_to_magnus(&ruby, e))?;
|
|
241
|
+
let mut config = Config::default();
|
|
242
|
+
config.timeout = timeout;
|
|
243
|
+
config.setup_timeout = setup_timeout;
|
|
244
|
+
config.memory_limit = memory_limit;
|
|
245
|
+
config.stdout_limit = stdout_limit;
|
|
246
|
+
config.stderr_limit = stderr_limit;
|
|
247
|
+
config.open_file_limit = open_file_limit;
|
|
248
|
+
config.profile = profile;
|
|
249
|
+
let driver =
|
|
250
|
+
Driver::new(Path::new(&path), config).map_err(|e| errors::setup_to_magnus(&ruby, e))?;
|
|
239
251
|
Ok(Self {
|
|
240
252
|
driver,
|
|
241
253
|
release_gvl,
|
|
@@ -246,90 +258,90 @@ impl Runtime {
|
|
|
246
258
|
// Run-path methods. Each takes the run's dispatch handler as its first
|
|
247
259
|
// argument and returns a `Snapshot` for any completed invocation —
|
|
248
260
|
// success or trap alike. Only a could-not-start fault (a missing export
|
|
249
|
-
// or a fault before the export call) raises
|
|
250
|
-
//
|
|
261
|
+
// or a fault before the export call) raises directly, as the class
|
|
262
|
+
// `errors::to_magnus` gives its cause, since it yields no `Snapshot`.
|
|
263
|
+
//
|
|
264
|
+
// `setup` is the invocation's `[paths, snippets, flags, mounts]`: the
|
|
265
|
+
// Preamble's three parts, then the Mounts as
|
|
266
|
+
// `filesystem::to_grants` reads them.
|
|
251
267
|
// -----------------------------------------------------------------
|
|
252
268
|
|
|
253
|
-
/// One-shot mruby source execution (`#eval`). Builds the dispatch
|
|
254
|
-
/// handler from `dispatch` (the per-invocation Proc), frames the two
|
|
255
|
-
/// stdin invocation frames from the registry state (`paths`,
|
|
256
|
-
/// `snippets`), hands them and the source to the driver, and returns
|
|
257
|
-
/// the run's `Snapshot`.
|
|
258
269
|
fn eval(
|
|
259
270
|
&self,
|
|
260
271
|
dispatch: Value,
|
|
261
|
-
|
|
272
|
+
setup: RArray,
|
|
262
273
|
source: RString,
|
|
263
|
-
snippets: RArray,
|
|
264
274
|
) -> Result<Snapshot, MagnusError> {
|
|
265
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
266
|
-
let handler = build_handler(dispatch);
|
|
267
|
-
let preamble = frame_preamble(paths)?;
|
|
268
275
|
let source = rstring_to_vec(source);
|
|
269
|
-
|
|
270
|
-
// Release the GVL around the guest span iff this Sandbox asks for it;
|
|
271
|
-
// the closure touches no Ruby VALUE (the driver is magnus-free, and a
|
|
272
|
-
// guest→host dispatch re-acquires the GVL through the bridge).
|
|
273
|
-
let result = gvl::region(self.release_gvl, || {
|
|
274
|
-
self.driver.invoke(
|
|
275
|
-
Entry::Eval { source: &source },
|
|
276
|
-
Frames {
|
|
277
|
-
preamble: &preamble,
|
|
278
|
-
snippets: &snippets,
|
|
279
|
-
},
|
|
280
|
-
handler,
|
|
281
|
-
)
|
|
282
|
-
});
|
|
283
|
-
let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
|
|
284
|
-
Ok(Snapshot::from(snapshot))
|
|
276
|
+
self.invoke(dispatch, setup, Entry::Eval { source: &source })
|
|
285
277
|
}
|
|
286
278
|
|
|
287
|
-
/// Execute one entrypoint dispatch (`__kobako_run`) and return its
|
|
288
|
-
/// `Snapshot`.
|
|
289
|
-
///
|
|
290
|
-
/// The two-frame stdin protocol (preamble + snippets; no user source
|
|
291
|
-
/// frame — docs/wire-codec.md § Invocation channels) plus the Run
|
|
292
|
-
/// envelope copied into guest linear memory — `entrypoint` routes it,
|
|
293
|
-
/// `payload` feeds it, and this side frames the two together. Cap
|
|
294
|
-
/// semantics match `#eval`.
|
|
295
279
|
fn run(
|
|
296
280
|
&self,
|
|
297
281
|
dispatch: Value,
|
|
298
|
-
|
|
299
|
-
snippets: RArray,
|
|
282
|
+
setup: RArray,
|
|
300
283
|
entrypoint: String,
|
|
301
284
|
payload: RString,
|
|
285
|
+
) -> Result<Snapshot, MagnusError> {
|
|
286
|
+
self.invoke(
|
|
287
|
+
dispatch,
|
|
288
|
+
setup,
|
|
289
|
+
Entry::Run {
|
|
290
|
+
entrypoint: &entrypoint,
|
|
291
|
+
payload: rstring_to_vec(payload),
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// One owner for the wiring both verbs share, so a Preamble, a Grant,
|
|
297
|
+
/// or a handler change cannot drift between them.
|
|
298
|
+
fn invoke(
|
|
299
|
+
&self,
|
|
300
|
+
dispatch: Value,
|
|
301
|
+
setup: RArray,
|
|
302
|
+
entry: Entry<'_>,
|
|
302
303
|
) -> Result<Snapshot, MagnusError> {
|
|
303
304
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
305
|
+
let verb = match entry {
|
|
306
|
+
Entry::Eval { .. } => "eval",
|
|
307
|
+
Entry::Run { .. } => "run",
|
|
308
|
+
};
|
|
304
309
|
let handler = build_handler(dispatch);
|
|
305
|
-
let preamble =
|
|
306
|
-
let
|
|
307
|
-
let envelope = Run {
|
|
308
|
-
entrypoint,
|
|
309
|
-
payload: rstring_to_vec(payload),
|
|
310
|
-
}
|
|
311
|
-
.encode();
|
|
310
|
+
let preamble = to_preamble(&ruby, setup)?;
|
|
311
|
+
let (grants, _pins) = filesystem::to_grants(&ruby, setup.entry(3)?)?;
|
|
312
312
|
// Release the GVL around the guest span iff this Sandbox asks for it;
|
|
313
|
-
//
|
|
313
|
+
// the closure touches no Ruby VALUE (the driver is magnus-free, and a
|
|
314
|
+
// guest→host dispatch, or a mount's block, re-acquires the GVL).
|
|
314
315
|
let result = gvl::region(self.release_gvl, || {
|
|
315
|
-
self.driver.invoke(
|
|
316
|
-
Entry::Run {
|
|
317
|
-
envelope: &envelope,
|
|
318
|
-
},
|
|
319
|
-
Frames {
|
|
320
|
-
preamble: &preamble,
|
|
321
|
-
snippets: &snippets,
|
|
322
|
-
},
|
|
323
|
-
handler,
|
|
324
|
-
)
|
|
316
|
+
self.driver.invoke(entry, &preamble, handler, &grants)
|
|
325
317
|
});
|
|
326
|
-
|
|
327
|
-
|
|
318
|
+
escape::resume()?;
|
|
319
|
+
result
|
|
320
|
+
.map(|snapshot| Snapshot::new(snapshot, verb))
|
|
321
|
+
.map_err(|e| errors::to_magnus(&ruby, e))
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// The bytecode of the source snippets, one binary String each in
|
|
325
|
+
/// order, or `nil` when the guest compiled nothing; no failure raises,
|
|
326
|
+
/// since the Sandbox keeps the source either way. Takes the Preamble an
|
|
327
|
+
/// invocation sends, `paths` included, so the driver can tell whether
|
|
328
|
+
/// the bytecode still fits it; no dispatch handler is bound, since
|
|
329
|
+
/// compiling runs no script.
|
|
330
|
+
fn compile(&self, preamble: RArray) -> Result<Option<RArray>, MagnusError> {
|
|
331
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
332
|
+
let preamble = to_preamble(&ruby, preamble)?;
|
|
333
|
+
let bytecodes = gvl::region(self.release_gvl, || self.driver.compile(&preamble));
|
|
334
|
+
Ok(bytecodes.map(|bytecodes| {
|
|
335
|
+
ruby.ary_from_iter(
|
|
336
|
+
bytecodes
|
|
337
|
+
.iter()
|
|
338
|
+
.map(|bytecode| ruby.str_from_slice(bytecode)),
|
|
339
|
+
)
|
|
340
|
+
}))
|
|
328
341
|
}
|
|
329
342
|
|
|
330
|
-
///
|
|
331
|
-
///
|
|
332
|
-
/// compares against the posture its `profile:` option requested.
|
|
343
|
+
/// The driver's declaration, which the Sandbox checks against the
|
|
344
|
+
/// posture its `profile:` option requested.
|
|
333
345
|
fn profile(&self) -> Symbol {
|
|
334
346
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
335
347
|
match self.driver.profile() {
|
|
@@ -339,11 +351,9 @@ impl Runtime {
|
|
|
339
351
|
}
|
|
340
352
|
}
|
|
341
353
|
|
|
342
|
-
///
|
|
343
|
-
///
|
|
344
|
-
///
|
|
345
|
-
/// argument on the Ruby stack, so the driver only borrows it (the safety
|
|
346
|
-
/// contract on `kobako_runtime::runtime::Runtime`).
|
|
354
|
+
/// The Proc stays GC-rooted for the synchronous `#eval` / `#run` call as a
|
|
355
|
+
/// live method argument on the Ruby stack, so the driver only borrows it
|
|
356
|
+
/// (the safety contract on `kobako_runtime::runtime::Runtime`).
|
|
347
357
|
fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
|
|
348
358
|
if dispatch.is_nil() {
|
|
349
359
|
return None;
|
|
@@ -353,135 +363,3 @@ fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
|
|
|
353
363
|
as Arc<dyn DispatchHandler>,
|
|
354
364
|
)
|
|
355
365
|
}
|
|
356
|
-
|
|
357
|
-
/// One invocation's result at the Ruby boundary — the whole `Snapshot` the
|
|
358
|
-
/// driver produced, exposed as `Kobako::Runtime::Snapshot`. Usage and the
|
|
359
|
-
/// two output captures are present on every outcome, so the trap path
|
|
360
|
-
/// carries them just like the value path; the completion is read as either
|
|
361
|
-
/// the outcome bytes (`#outcome`) or a trap (`#trapped?` / `#trap_kind` /
|
|
362
|
-
/// `#trap_message`), and the Sandbox layer maps a trap onto its
|
|
363
|
-
/// `Kobako::TrapError` family.
|
|
364
|
-
#[derive(TypedData)]
|
|
365
|
-
#[magnus(class = "Kobako::Runtime::Snapshot", free_immediately, size)]
|
|
366
|
-
struct Snapshot {
|
|
367
|
-
completion: Completion,
|
|
368
|
-
stdout: Capture,
|
|
369
|
-
stderr: Capture,
|
|
370
|
-
usage: Usage,
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
impl DataTypeFunctions for Snapshot {}
|
|
374
|
-
|
|
375
|
-
impl From<RuntimeSnapshot> for Snapshot {
|
|
376
|
-
fn from(snapshot: RuntimeSnapshot) -> Self {
|
|
377
|
-
let RuntimeSnapshot {
|
|
378
|
-
completion,
|
|
379
|
-
stdout,
|
|
380
|
-
stderr,
|
|
381
|
-
usage,
|
|
382
|
-
} = snapshot;
|
|
383
|
-
Self {
|
|
384
|
-
completion,
|
|
385
|
-
stdout,
|
|
386
|
-
stderr,
|
|
387
|
-
usage,
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
impl Snapshot {
|
|
393
|
-
/// One completed run's outcome, already split off the core envelope:
|
|
394
|
-
/// `[kind, payload, panic]`. `kind` names the arm — `:ok`,
|
|
395
|
-
/// `:panic`, `:absent` (nothing written), or `:malformed` (bytes the
|
|
396
|
-
/// envelope cannot frame). `payload` is the invocation's
|
|
397
|
-
/// codec-encoded value, carried only by `:ok`; every other arm
|
|
398
|
-
/// answers empty. `panic` carries the Panic's own fields on `:panic`
|
|
399
|
-
/// and is `nil` otherwise, so the Ruby side maps a failure onto its
|
|
400
|
-
/// error taxonomy without decoding a payload byte.
|
|
401
|
-
///
|
|
402
|
-
/// A trap answers `:absent` — `#trapped?` is the authoritative
|
|
403
|
-
/// discriminator there and this is never read.
|
|
404
|
-
fn outcome(&self) -> (Symbol, RString, Option<PanicFields>) {
|
|
405
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
406
|
-
let empty = || ruby.str_from_slice(&[]);
|
|
407
|
-
let Completion::Outcome(bytes) = &self.completion else {
|
|
408
|
-
return (ruby.to_symbol("absent"), empty(), None);
|
|
409
|
-
};
|
|
410
|
-
match Outcome::decode(bytes) {
|
|
411
|
-
Ok(Outcome::Ok(value)) => (ruby.to_symbol("ok"), ruby.str_from_slice(&value), None),
|
|
412
|
-
Ok(Outcome::Panic(panic)) => (
|
|
413
|
-
ruby.to_symbol("panic"),
|
|
414
|
-
empty(),
|
|
415
|
-
Some((
|
|
416
|
-
panic.origin.name().to_owned(),
|
|
417
|
-
panic.error.name,
|
|
418
|
-
panic.error.message,
|
|
419
|
-
panic.error.backtrace,
|
|
420
|
-
panic.available,
|
|
421
|
-
)),
|
|
422
|
-
),
|
|
423
|
-
Err(_) if bytes.is_empty() => (ruby.to_symbol("absent"), empty(), None),
|
|
424
|
-
Err(_) => (ruby.to_symbol("malformed"), empty(), None),
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/// `true` iff the invocation completed via an engine trap.
|
|
429
|
-
fn trapped(&self) -> bool {
|
|
430
|
-
matches!(self.completion, Completion::Trap(_))
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/// The trap's neutral kind as a Symbol (`:timeout` / `:memory_limit` /
|
|
434
|
-
/// `:trap`), or `nil` on a completed run. The Sandbox maps this onto the
|
|
435
|
-
/// named `Kobako::TrapError` subclass.
|
|
436
|
-
fn trap_kind(&self) -> Option<Symbol> {
|
|
437
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
438
|
-
match &self.completion {
|
|
439
|
-
Completion::Trap(Trap::Timeout(_)) => Some(ruby.to_symbol("timeout")),
|
|
440
|
-
Completion::Trap(Trap::MemoryLimit(_)) => Some(ruby.to_symbol("memory_limit")),
|
|
441
|
-
// A cap this frontend has no named subclass for is still a
|
|
442
|
-
// trap; the base class is what the taxonomy owes it.
|
|
443
|
-
Completion::Trap(_) => Some(ruby.to_symbol("trap")),
|
|
444
|
-
Completion::Outcome(_) => None,
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/// The trap's message, or `nil` on a completed run.
|
|
449
|
-
fn trap_message(&self) -> Option<String> {
|
|
450
|
-
match &self.completion {
|
|
451
|
-
Completion::Trap(trap) => Some(trap.to_string()),
|
|
452
|
-
Completion::Outcome(_) => None,
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/// Wall-clock seconds the guest export call spent inside wasmtime.
|
|
457
|
-
fn wall_time(&self) -> f64 {
|
|
458
|
-
self.usage.wall_time
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/// High-water `memory.grow` delta in bytes past the entry-time baseline.
|
|
462
|
-
fn memory_peak(&self) -> usize {
|
|
463
|
-
self.usage.memory_peak
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/// Bytes captured on the guest's stdout channel, clipped to the cap.
|
|
467
|
-
fn stdout(&self) -> RString {
|
|
468
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
469
|
-
ruby.str_from_slice(&self.stdout.bytes)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/// `true` iff the stdout channel reached its cap during this run.
|
|
473
|
-
fn stdout_truncated(&self) -> bool {
|
|
474
|
-
self.stdout.truncated
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/// Bytes captured on the guest's stderr channel, clipped to the cap.
|
|
478
|
-
fn stderr(&self) -> RString {
|
|
479
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
480
|
-
ruby.str_from_slice(&self.stderr.bytes)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/// `true` iff the stderr channel reached its cap during this run.
|
|
484
|
-
fn stderr_truncated(&self) -> bool {
|
|
485
|
-
self.stderr.truncated
|
|
486
|
-
}
|
|
487
|
-
}
|
data/lib/kobako/capture.rb
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
|
-
|
|
5
|
-
# cap, together with whether it wrote past the cap. Frozen, so the bytes
|
|
6
|
-
# and the flag always travel together.
|
|
7
|
-
class Capture
|
|
4
|
+
class Capture # :nodoc:
|
|
8
5
|
attr_reader :bytes
|
|
9
6
|
|
|
10
7
|
# The bytes read as UTF-8 when they are valid UTF-8, and as binary
|