kobako 0.28.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 +170 -0
- data/Cargo.lock +111 -82
- data/README.md +154 -36
- data/ROADMAP.md +5 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +86 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +13 -8
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- 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 +11 -1
- 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 +12 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-transport/CHANGELOG.md +17 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +32 -14
- data/crates/kobako-transport/src/envelope/request.rs +18 -8
- data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
- data/crates/kobako-wasmtime/Cargo.toml +21 -16
- data/crates/kobako-wasmtime/README.md +5 -3
- data/crates/kobako-wasmtime/src/abi.rs +66 -32
- data/crates/kobako-wasmtime/src/ambient.rs +63 -4
- data/crates/kobako-wasmtime/src/cache.rs +185 -74
- data/crates/kobako-wasmtime/src/config.rs +58 -1
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
- data/crates/kobako-wasmtime/src/driver.rs +46 -44
- data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
- data/crates/kobako-wasmtime/src/invocation.rs +35 -46
- data/crates/kobako-wasmtime/src/io.rs +5 -202
- data/crates/kobako-wasmtime/src/lib.rs +4 -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 +39 -19
- 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 +3 -2
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
- data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- 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 +7 -1
- 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 +120 -199
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +11 -0
- data/lib/kobako/catalog/snippets.rb +5 -9
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +1 -0
- data/lib/kobako/codec/ext_types.rb +1 -1
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +38 -11
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/pool.rb +2 -2
- data/lib/kobako/sandbox.rb +40 -12
- data/lib/kobako/sandbox_options.rb +39 -12
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +30 -26
- data/lib/kobako/transport/exposure.rb +5 -7
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/yielder.rb +8 -30
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +4 -0
- data/sig/kobako/catalog/snippets.rbs +1 -1
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +2 -0
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +7 -1
- data/sig/kobako/sandbox_options.rbs +24 -2
- 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 +3 -3
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +2 -2
- metadata +60 -7
- 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
|
|
@@ -19,11 +19,16 @@
|
|
|
19
19
|
|
|
20
20
|
mod bridge;
|
|
21
21
|
mod errors;
|
|
22
|
+
mod escape;
|
|
23
|
+
mod filesystem;
|
|
22
24
|
mod gvl;
|
|
25
|
+
mod pinned_value;
|
|
26
|
+
mod snapshot;
|
|
23
27
|
|
|
24
28
|
use magnus::{
|
|
25
|
-
function, method, prelude::*,
|
|
26
|
-
Error as MagnusError,
|
|
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,
|
|
27
32
|
};
|
|
28
33
|
|
|
29
34
|
use std::path::Path;
|
|
@@ -33,15 +38,10 @@ use std::time::Duration;
|
|
|
33
38
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
34
39
|
use kobako_runtime::profile::Profile;
|
|
35
40
|
use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
|
|
36
|
-
use
|
|
37
|
-
use kobako_transport::envelope::{Bindings, Flag, Outcome, Preamble, Snippet, Snippets};
|
|
41
|
+
use kobako_transport::envelope::{Bindings, Flag, Preamble, Snippet, Snippets};
|
|
38
42
|
use kobako_wasmtime::{Config, Driver};
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
/// backtrace, and the names the invocation could have used in place of the
|
|
42
|
-
/// one it named. Named so the Ruby side reads them positionally against one
|
|
43
|
-
/// shape, matching the `Kobako::Outcome::panic_fields` alias.
|
|
44
|
-
type PanicFields = (String, String, String, Vec<String>, Vec<String>);
|
|
44
|
+
use snapshot::Snapshot;
|
|
45
45
|
|
|
46
46
|
/// The one place the boundary borrows an RString's bytes: the borrow does
|
|
47
47
|
/// not outlive this call, so no Ruby allocation can move the RString
|
|
@@ -53,7 +53,7 @@ fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
|
53
53
|
|
|
54
54
|
/// `kind` arrives as a Symbol so the registry never names the wire's
|
|
55
55
|
/// discriminant; an off-ladder kind raises rather than defaulting.
|
|
56
|
-
fn
|
|
56
|
+
fn to_snippets(ruby: &Ruby, entries: RArray) -> Result<Snippets, MagnusError> {
|
|
57
57
|
let mut snippets = Snippets {
|
|
58
58
|
entries: Vec::with_capacity(entries.len()),
|
|
59
59
|
};
|
|
@@ -80,16 +80,16 @@ fn snippets_from(ruby: &Ruby, entries: RArray) -> Result<Snippets, MagnusError>
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/// One owner for the Preamble every guest entry reads, so an invocation
|
|
83
|
-
/// and a compile cannot drift in what they send.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
paths:
|
|
87
|
-
|
|
88
|
-
flags: RArray,
|
|
89
|
-
) -> Result<Preamble, MagnusError> {
|
|
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)?;
|
|
90
88
|
let mut preamble = Preamble::default();
|
|
91
|
-
preamble.bindings = Bindings {
|
|
92
|
-
|
|
89
|
+
preamble.bindings = Bindings {
|
|
90
|
+
paths: paths.to_vec()?,
|
|
91
|
+
};
|
|
92
|
+
preamble.snippets = to_snippets(ruby, parts.entry(1)?)?;
|
|
93
93
|
preamble.flags = flags
|
|
94
94
|
.to_vec::<(String, String, bool)>()?
|
|
95
95
|
.into_iter()
|
|
@@ -112,26 +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,
|
|
119
|
+
runtime.define_method("compile", method!(Runtime::compile, 1))?;
|
|
120
|
+
runtime.define_singleton_method("validate_mount!", function!(filesystem::validate_mount, 3))?;
|
|
120
121
|
// The guest re-enters for a block yield through a frame-scoped
|
|
121
|
-
// `Kobako::
|
|
122
|
-
//
|
|
123
|
-
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)?;
|
|
124
126
|
|
|
125
127
|
// Snapshot — the per-invocation result object each entry point returns.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
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);
|
|
135
136
|
|
|
136
137
|
Ok(())
|
|
137
138
|
}
|
|
@@ -157,34 +158,61 @@ impl DataTypeFunctions for Runtime {}
|
|
|
157
158
|
|
|
158
159
|
impl Runtime {
|
|
159
160
|
/// The only Ruby-facing constructor, so Engine and Module are never
|
|
160
|
-
/// visible to Ruby.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
timeout_seconds: Option<f64>,
|
|
164
|
-
memory_limit: Option<usize>,
|
|
165
|
-
stdout_limit: Option<usize>,
|
|
166
|
-
stderr_limit: Option<usize>,
|
|
167
|
-
profile: Symbol,
|
|
168
|
-
gvl: Symbol,
|
|
169
|
-
) -> Result<Self, MagnusError> {
|
|
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> {
|
|
170
164
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
)),
|
|
185
211
|
};
|
|
212
|
+
let timeout = to_duration(timeout_seconds, "timeout")?;
|
|
213
|
+
let setup_timeout = to_duration(setup_timeout_seconds, "setup_timeout")?;
|
|
186
214
|
// Fail closed on an off-ladder rung: an unrecognized posture must
|
|
187
|
-
// never fall back to a
|
|
215
|
+
// never fall back to a weaker posture. Same defence in depth as the
|
|
188
216
|
// timeout guard above — `SandboxOptions` is the primary validator.
|
|
189
217
|
let profile = match profile.name()?.as_ref() {
|
|
190
218
|
"hermetic" => Profile::Hermetic,
|
|
@@ -210,17 +238,16 @@ impl Runtime {
|
|
|
210
238
|
}
|
|
211
239
|
};
|
|
212
240
|
|
|
213
|
-
let
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.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))?;
|
|
224
251
|
Ok(Self {
|
|
225
252
|
driver,
|
|
226
253
|
release_gvl,
|
|
@@ -233,40 +260,32 @@ impl Runtime {
|
|
|
233
260
|
// success or trap alike. Only a could-not-start fault (a missing export
|
|
234
261
|
// or a fault before the export call) raises directly, as the class
|
|
235
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.
|
|
236
267
|
// -----------------------------------------------------------------
|
|
237
268
|
|
|
238
269
|
fn eval(
|
|
239
270
|
&self,
|
|
240
271
|
dispatch: Value,
|
|
241
|
-
|
|
242
|
-
snippets: RArray,
|
|
243
|
-
flags: RArray,
|
|
272
|
+
setup: RArray,
|
|
244
273
|
source: RString,
|
|
245
274
|
) -> Result<Snapshot, MagnusError> {
|
|
246
275
|
let source = rstring_to_vec(source);
|
|
247
|
-
self.invoke(
|
|
248
|
-
dispatch,
|
|
249
|
-
paths,
|
|
250
|
-
snippets,
|
|
251
|
-
flags,
|
|
252
|
-
Entry::Eval { source: &source },
|
|
253
|
-
)
|
|
276
|
+
self.invoke(dispatch, setup, Entry::Eval { source: &source })
|
|
254
277
|
}
|
|
255
278
|
|
|
256
279
|
fn run(
|
|
257
280
|
&self,
|
|
258
281
|
dispatch: Value,
|
|
259
|
-
|
|
260
|
-
snippets: RArray,
|
|
261
|
-
flags: RArray,
|
|
282
|
+
setup: RArray,
|
|
262
283
|
entrypoint: String,
|
|
263
284
|
payload: RString,
|
|
264
285
|
) -> Result<Snapshot, MagnusError> {
|
|
265
286
|
self.invoke(
|
|
266
287
|
dispatch,
|
|
267
|
-
|
|
268
|
-
snippets,
|
|
269
|
-
flags,
|
|
288
|
+
setup,
|
|
270
289
|
Entry::Run {
|
|
271
290
|
entrypoint: &entrypoint,
|
|
272
291
|
payload: rstring_to_vec(payload),
|
|
@@ -274,14 +293,12 @@ impl Runtime {
|
|
|
274
293
|
)
|
|
275
294
|
}
|
|
276
295
|
|
|
277
|
-
/// One owner for the wiring both verbs share, so a Preamble
|
|
278
|
-
/// change cannot drift between them.
|
|
296
|
+
/// One owner for the wiring both verbs share, so a Preamble, a Grant,
|
|
297
|
+
/// or a handler change cannot drift between them.
|
|
279
298
|
fn invoke(
|
|
280
299
|
&self,
|
|
281
300
|
dispatch: Value,
|
|
282
|
-
|
|
283
|
-
snippets: RArray,
|
|
284
|
-
flags: RArray,
|
|
301
|
+
setup: RArray,
|
|
285
302
|
entry: Entry<'_>,
|
|
286
303
|
) -> Result<Snapshot, MagnusError> {
|
|
287
304
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
@@ -290,15 +307,17 @@ impl Runtime {
|
|
|
290
307
|
Entry::Run { .. } => "run",
|
|
291
308
|
};
|
|
292
309
|
let handler = build_handler(dispatch);
|
|
293
|
-
let preamble =
|
|
310
|
+
let preamble = to_preamble(&ruby, setup)?;
|
|
311
|
+
let (grants, _pins) = filesystem::to_grants(&ruby, setup.entry(3)?)?;
|
|
294
312
|
// Release the GVL around the guest span iff this Sandbox asks for it;
|
|
295
313
|
// the closure touches no Ruby VALUE (the driver is magnus-free, and a
|
|
296
|
-
// guest→host dispatch re-acquires the GVL
|
|
314
|
+
// guest→host dispatch, or a mount's block, re-acquires the GVL).
|
|
297
315
|
let result = gvl::region(self.release_gvl, || {
|
|
298
|
-
self.driver.invoke(entry, &preamble, handler)
|
|
316
|
+
self.driver.invoke(entry, &preamble, handler, &grants)
|
|
299
317
|
});
|
|
318
|
+
escape::resume()?;
|
|
300
319
|
result
|
|
301
|
-
.map(|snapshot| Snapshot(snapshot, verb))
|
|
320
|
+
.map(|snapshot| Snapshot::new(snapshot, verb))
|
|
302
321
|
.map_err(|e| errors::to_magnus(&ruby, e))
|
|
303
322
|
}
|
|
304
323
|
|
|
@@ -308,14 +327,9 @@ impl Runtime {
|
|
|
308
327
|
/// invocation sends, `paths` included, so the driver can tell whether
|
|
309
328
|
/// the bytecode still fits it; no dispatch handler is bound, since
|
|
310
329
|
/// compiling runs no script.
|
|
311
|
-
fn compile(
|
|
312
|
-
&self,
|
|
313
|
-
paths: RArray,
|
|
314
|
-
snippets: RArray,
|
|
315
|
-
flags: RArray,
|
|
316
|
-
) -> Result<Option<RArray>, MagnusError> {
|
|
330
|
+
fn compile(&self, preamble: RArray) -> Result<Option<RArray>, MagnusError> {
|
|
317
331
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
318
|
-
let preamble =
|
|
332
|
+
let preamble = to_preamble(&ruby, preamble)?;
|
|
319
333
|
let bytecodes = gvl::region(self.release_gvl, || self.driver.compile(&preamble));
|
|
320
334
|
Ok(bytecodes.map(|bytecodes| {
|
|
321
335
|
ruby.ary_from_iter(
|
|
@@ -349,96 +363,3 @@ fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
|
|
|
349
363
|
as Arc<dyn DispatchHandler>,
|
|
350
364
|
)
|
|
351
365
|
}
|
|
352
|
-
|
|
353
|
-
/// One invocation's result at the Ruby boundary — the whole `Snapshot` the
|
|
354
|
-
/// driver produced, exposed as `Kobako::Runtime::Snapshot`. Usage and the
|
|
355
|
-
/// two output captures are present on every outcome, so the trap path
|
|
356
|
-
/// carries them just like the value path; the completion is read as either
|
|
357
|
-
/// the Outcome (`#outcome`) or a trap (`#trap_error`). The verb names the
|
|
358
|
-
/// invocation, so an unsupported one says which operation is lacking.
|
|
359
|
-
#[derive(TypedData)]
|
|
360
|
-
#[magnus(class = "Kobako::Runtime::Snapshot", free_immediately, size)]
|
|
361
|
-
struct Snapshot(RuntimeSnapshot, &'static str);
|
|
362
|
-
|
|
363
|
-
impl DataTypeFunctions for Snapshot {}
|
|
364
|
-
|
|
365
|
-
impl Snapshot {
|
|
366
|
-
/// Already split off the core envelope, so the Ruby side maps a failure
|
|
367
|
-
/// onto its error taxonomy without decoding a payload byte: the ok
|
|
368
|
-
/// arm's payload, or a Panic's fields beside an empty payload. A trap
|
|
369
|
-
/// has no Outcome; `#trap_error` is the discriminator there.
|
|
370
|
-
fn outcome(&self) -> Result<(RString, Option<PanicFields>), MagnusError> {
|
|
371
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
372
|
-
match &self.0.completion {
|
|
373
|
-
Completion::Outcome(Outcome::Ok(value)) => Ok((ruby.str_from_slice(value), None)),
|
|
374
|
-
Completion::Outcome(Outcome::Panic(panic)) => Ok((
|
|
375
|
-
ruby.str_from_slice(&[]),
|
|
376
|
-
Some((
|
|
377
|
-
panic.origin.name().to_owned(),
|
|
378
|
-
panic.error.name.clone(),
|
|
379
|
-
panic.error.message.clone(),
|
|
380
|
-
panic.error.backtrace.clone(),
|
|
381
|
-
panic.available.clone(),
|
|
382
|
-
)),
|
|
383
|
-
)),
|
|
384
|
-
// The Guest Binary answered rather than broke, so it reads as
|
|
385
|
-
// the Sandbox's failure, worded by the host.
|
|
386
|
-
Completion::Outcome(Outcome::Unsupported(_)) => Ok((
|
|
387
|
-
ruby.str_from_slice(&[]),
|
|
388
|
-
Some((
|
|
389
|
-
"sandbox".to_owned(),
|
|
390
|
-
"Kobako::SandboxError".to_owned(),
|
|
391
|
-
format!(
|
|
392
|
-
"the Sandbox runtime does not support #{}; \
|
|
393
|
-
rebuild it against the kobako version in use",
|
|
394
|
-
self.1
|
|
395
|
-
),
|
|
396
|
-
Vec::new(),
|
|
397
|
-
Vec::new(),
|
|
398
|
-
)),
|
|
399
|
-
)),
|
|
400
|
-
Completion::Trap(_) => Err(MagnusError::new(
|
|
401
|
-
ruby.exception_runtime_error(),
|
|
402
|
-
"a trapped run has no outcome",
|
|
403
|
-
)),
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/// Unraised, so the Sandbox layer can attach the run's Execution before
|
|
408
|
-
/// raising it.
|
|
409
|
-
fn trap_error(&self) -> Result<Option<Exception>, MagnusError> {
|
|
410
|
-
let Completion::Trap(trap) = &self.0.completion else {
|
|
411
|
-
return Ok(None);
|
|
412
|
-
};
|
|
413
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
414
|
-
errors::trap_class(&ruby, trap)
|
|
415
|
-
.new_instance((trap.to_string(),))
|
|
416
|
-
.map(Some)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
fn wall_time(&self) -> f64 {
|
|
420
|
-
self.0.usage.wall_time
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
fn memory_peak(&self) -> usize {
|
|
424
|
-
self.0.usage.memory_peak
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
fn stdout(&self) -> RString {
|
|
428
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
429
|
-
ruby.str_from_slice(&self.0.stdout.bytes)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
fn stdout_truncated(&self) -> bool {
|
|
433
|
-
self.0.stdout.truncated
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
fn stderr(&self) -> RString {
|
|
437
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
438
|
-
ruby.str_from_slice(&self.0.stderr.bytes)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
fn stderr_truncated(&self) -> bool {
|
|
442
|
-
self.0.stderr.truncated
|
|
443
|
-
}
|
|
444
|
-
}
|
data/lib/kobako/catalog/flags.rb
CHANGED
|
@@ -18,24 +18,12 @@ module Kobako
|
|
|
18
18
|
end
|
|
19
19
|
raise ArgumentError, "flag value must be a String, got #{value.class}" unless value.is_a?(String)
|
|
20
20
|
|
|
21
|
-
key =
|
|
21
|
+
key = Catalog.to_utf8!(name.to_s, "flag name")
|
|
22
22
|
raise ArgumentError, "flag #{key.inspect} already set" if @entries.key?(key)
|
|
23
23
|
|
|
24
|
-
@entries[key] = [
|
|
24
|
+
@entries[key] = [Catalog.to_utf8!(value, "flag value"), !advisory]
|
|
25
25
|
nil
|
|
26
26
|
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
# The native side carries a flag only as UTF-8 text, so text read as
|
|
31
|
-
# bytes is re-tagged, and bytes that are not UTF-8 are refused here
|
|
32
|
-
# rather than failing every invocation.
|
|
33
|
-
def utf8!(text, what)
|
|
34
|
-
utf8 = text.dup.force_encoding(Encoding::UTF_8)
|
|
35
|
-
raise ArgumentError, "#{what} must be UTF-8 text" unless utf8.valid_encoding?
|
|
36
|
-
|
|
37
|
-
utf8
|
|
38
|
-
end
|
|
39
27
|
end
|
|
40
28
|
end
|
|
41
29
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Catalog
|
|
5
|
+
class Grants # :nodoc:
|
|
6
|
+
def initialize
|
|
7
|
+
@entries = {} # : Hash[String, Mount]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Each Grant decides what it cannot stand beside.
|
|
11
|
+
def register(grant)
|
|
12
|
+
reason = @entries.each_value.lazy.filter_map { |other| grant.conflict(other) }.first
|
|
13
|
+
raise ArgumentError, reason if reason
|
|
14
|
+
|
|
15
|
+
@entries[grant.key] = grant
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def include?(grant)
|
|
20
|
+
@entries.key?(grant.key)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Each Grant as the native side reads it, an override standing in for
|
|
24
|
+
# the Grant it shares a key with.
|
|
25
|
+
def entries(overrides = {})
|
|
26
|
+
@entries.merge(overrides).values.map(&:to_native)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Catalog
|
|
5
|
+
class Mount < Data.define(:at, :access, :filesystem, :policy) # :nodoc:
|
|
6
|
+
# The path a Context replaces this Mount at.
|
|
7
|
+
def key = at
|
|
8
|
+
|
|
9
|
+
# Why +other+ cannot be mounted beside this Mount, or +nil+: a path
|
|
10
|
+
# means one Mount, and never sits inside another.
|
|
11
|
+
def conflict(other)
|
|
12
|
+
return "path #{at.inspect} is already mounted" if other.at == at
|
|
13
|
+
|
|
14
|
+
"path #{at.inspect} nests with the mounted path #{other.at.inspect}" if nested?(other.at)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The Mount as the native side reads it.
|
|
18
|
+
def to_native = [at, access, filesystem, policy]
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def nested?(other)
|
|
23
|
+
[at, other].include?("/") || at.start_with?("#{other}/") || other.start_with?("#{at}/")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -7,6 +7,9 @@ require_relative "../transport/exposure"
|
|
|
7
7
|
module Kobako
|
|
8
8
|
module Catalog
|
|
9
9
|
class Services # :nodoc:
|
|
10
|
+
# The Guest Binary keeps this namespace for constants it may add later.
|
|
11
|
+
RESERVED_NAMESPACE = "Kobako"
|
|
12
|
+
|
|
10
13
|
def initialize
|
|
11
14
|
@bindings = {} # : Hash[String, Kobako::Transport::Exposure]
|
|
12
15
|
end
|
|
@@ -14,6 +17,10 @@ module Kobako
|
|
|
14
17
|
# The binding records +object+'s Exposure as it stands now.
|
|
15
18
|
def bind(path, object)
|
|
16
19
|
path_str = validate_path!(path)
|
|
20
|
+
if reserved?(path_str)
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"Service path #{path_str} is in the reserved #{RESERVED_NAMESPACE} namespace"
|
|
23
|
+
end
|
|
17
24
|
raise ArgumentError, "Service path #{path_str} conflicts with an existing binding" if collision?(path_str)
|
|
18
25
|
|
|
19
26
|
@bindings[path_str] = Kobako::Transport::Exposure.of(object)
|
|
@@ -48,6 +55,10 @@ module Kobako
|
|
|
48
55
|
"bind path must be constant-form segments joined by '::' (got #{path.inspect})"
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
def reserved?(path)
|
|
59
|
+
path.split("::").first == RESERVED_NAMESPACE
|
|
60
|
+
end
|
|
61
|
+
|
|
51
62
|
# Keeps a name from being both a bound Service and a grouping prefix.
|
|
52
63
|
def collision?(path)
|
|
53
64
|
@bindings.each_key.any? do |existing|
|
|
@@ -7,7 +7,7 @@ module Kobako
|
|
|
7
7
|
module Catalog
|
|
8
8
|
class Snippets # :nodoc:
|
|
9
9
|
def initialize
|
|
10
|
-
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::
|
|
10
|
+
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Bytecode]
|
|
11
11
|
@compiled = {} # : Hash[Kobako::Snippet::Source, String]
|
|
12
12
|
@compile_tried = false
|
|
13
13
|
end
|
|
@@ -48,13 +48,9 @@ module Kobako
|
|
|
48
48
|
|
|
49
49
|
private
|
|
50
50
|
|
|
51
|
-
# The native side frames a source body only as UTF-8 text, so source
|
|
52
|
-
# read as bytes is re-tagged, and bytes that are not UTF-8 are refused
|
|
53
|
-
# here rather than failing every invocation.
|
|
54
51
|
def register_source!(code, name)
|
|
55
52
|
code, name = ensure_source_args!(code, name)
|
|
56
|
-
body =
|
|
57
|
-
raise ArgumentError, "code must be UTF-8 text" unless body.valid_encoding?
|
|
53
|
+
body = Catalog.to_utf8!(code, "code")
|
|
58
54
|
|
|
59
55
|
name_sym = normalize_name(name)
|
|
60
56
|
if @entries.any? { |e| e.is_a?(Snippet::Source) && e.name == name_sym }
|
|
@@ -80,7 +76,7 @@ module Kobako
|
|
|
80
76
|
def register_binary!(bytes)
|
|
81
77
|
raise ArgumentError, "binary must be a String, got #{bytes.class}" unless bytes.is_a?(String)
|
|
82
78
|
|
|
83
|
-
@entries << Snippet::
|
|
79
|
+
@entries << Snippet::Bytecode.new(body: bytes.dup.force_encoding(Encoding::ASCII_8BIT))
|
|
84
80
|
nil
|
|
85
81
|
end
|
|
86
82
|
|
|
@@ -90,9 +86,9 @@ module Kobako
|
|
|
90
86
|
case entry
|
|
91
87
|
when Snippet::Source
|
|
92
88
|
compiled = @compiled[entry]
|
|
93
|
-
compiled ? [Snippet::
|
|
89
|
+
compiled ? [Snippet::Bytecode::KIND, nil, compiled] : [Snippet::Source::KIND, entry.name.to_s, entry.body]
|
|
94
90
|
else
|
|
95
|
-
[Snippet::
|
|
91
|
+
[Snippet::Bytecode::KIND, nil, entry.body]
|
|
96
92
|
end
|
|
97
93
|
end
|
|
98
94
|
|
data/lib/kobako/catalog.rb
CHANGED
|
@@ -5,8 +5,19 @@ require_relative "catalog/services"
|
|
|
5
5
|
require_relative "catalog/snippets"
|
|
6
6
|
require_relative "catalog/flags"
|
|
7
7
|
require_relative "catalog/extensions"
|
|
8
|
+
require_relative "catalog/mount"
|
|
9
|
+
require_relative "catalog/grants"
|
|
8
10
|
|
|
9
11
|
module Kobako
|
|
10
12
|
module Catalog # :nodoc:
|
|
13
|
+
# The native side carries a registered text only as UTF-8, so text read
|
|
14
|
+
# as bytes is re-tagged, and bytes that are not UTF-8 are refused at
|
|
15
|
+
# registration rather than failing every invocation.
|
|
16
|
+
def self.to_utf8!(text, what)
|
|
17
|
+
utf8 = text.dup.force_encoding(Encoding::UTF_8)
|
|
18
|
+
raise ArgumentError, "#{what} must be UTF-8 text" unless utf8.valid_encoding?
|
|
19
|
+
|
|
20
|
+
utf8
|
|
21
|
+
end
|
|
11
22
|
end
|
|
12
23
|
end
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Kobako
|
|
|
13
13
|
# from the decoded payload reports a broken invariant as
|
|
14
14
|
# InvalidTypeError without its own Utils.with_boundary.
|
|
15
15
|
def self.decode(bytes)
|
|
16
|
+
ValueBudget.assert_within!(bytes, State.current.value_budget)
|
|
16
17
|
value = FACTORY.load(bytes.b)
|
|
17
18
|
validate_utf8!(value)
|
|
18
19
|
block_given? ? yield(value) : value
|
|
@@ -63,7 +63,7 @@ module Kobako
|
|
|
63
63
|
# shape. The sighting is recorded so a Handle-free decode can skip the
|
|
64
64
|
# downstream resolution walk.
|
|
65
65
|
def unpack_handle(payload, state)
|
|
66
|
-
state.record_handle
|
|
66
|
+
state.record_handle
|
|
67
67
|
bytes = payload.b
|
|
68
68
|
raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
69
69
|
|