kobako 0.12.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +11 -0
- data/Cargo.lock +15 -2
- data/Cargo.toml +6 -2
- data/README.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +8 -0
- data/crates/kobako-runtime/Cargo.toml +23 -0
- data/crates/kobako-runtime/README.md +34 -0
- data/crates/kobako-runtime/src/dispatch.rs +22 -0
- data/crates/kobako-runtime/src/error.rs +64 -0
- data/crates/kobako-runtime/src/lib.rs +16 -0
- data/crates/kobako-runtime/src/runtime.rs +50 -0
- data/crates/kobako-runtime/src/snapshot.rs +46 -0
- data/crates/kobako-runtime/src/yielder.rs +22 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +8 -0
- data/crates/kobako-wasmtime/Cargo.toml +62 -0
- data/crates/kobako-wasmtime/README.md +32 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/ambient.rs +3 -3
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/cache.rs +30 -41
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/capture.rs +2 -2
- data/crates/kobako-wasmtime/src/config.rs +25 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
- data/crates/kobako-wasmtime/src/driver.rs +285 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/frames.rs +70 -82
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/guest_mem.rs +38 -15
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/instance_pre.rs +13 -21
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/invocation.rs +54 -49
- data/crates/kobako-wasmtime/src/lib.rs +47 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/trap.rs +29 -35
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +9 -32
- data/ext/kobako/src/runtime/bridge.rs +150 -0
- data/ext/kobako/src/runtime/errors.rs +45 -13
- data/ext/kobako/src/runtime.rs +156 -406
- data/ext/kobako/src/snapshot.rs +27 -62
- data/lib/kobako/catalog/handles.rb +3 -3
- data/lib/kobako/catalog/namespaces.rb +4 -0
- data/lib/kobako/catalog/snippets.rb +4 -0
- data/lib/kobako/codec/encoder.rb +5 -1
- data/lib/kobako/codec/factory.rb +41 -13
- data/lib/kobako/codec/handle_walk.rb +4 -0
- data/lib/kobako/errors.rb +18 -16
- data/lib/kobako/sandbox.rb +20 -18
- data/lib/kobako/sandbox_options.rb +25 -9
- data/lib/kobako/snapshot.rb +7 -13
- data/lib/kobako/transport/dispatcher.rb +2 -2
- data/lib/kobako/transport/response.rb +14 -14
- data/lib/kobako/transport/run.rb +2 -6
- data/lib/kobako/transport/yield.rb +1 -1
- data/lib/kobako/transport/yielder.rb +2 -2
- data/lib/kobako/version.rb +1 -1
- data/release-please-config.json +48 -3
- data/sig/kobako/codec/factory.rbs +3 -0
- data/sig/kobako/errors.rbs +7 -14
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/sandbox_options.rbs +4 -2
- data/sig/kobako/snapshot.rbs +0 -3
- data/sig/kobako/transport/dispatcher.rbs +1 -1
- data/sig/kobako/transport/run.rbs +2 -2
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -0
- metadata +27 -12
- data/ext/kobako/src/runtime/config.rs +0 -25
- data/ext/kobako/src/runtime/dispatch.rs +0 -211
data/ext/kobako/src/snapshot.rs
CHANGED
|
@@ -1,64 +1,39 @@
|
|
|
1
|
-
//! `Kobako::Snapshot` — per-invocation observable bundle.
|
|
1
|
+
//! `Kobako::Snapshot` — the Ruby-facing per-invocation observable bundle.
|
|
2
2
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//! stdout / stderr
|
|
7
|
-
//! the wall-clock + memory-peak figures from `Kobako::Usage`.
|
|
8
|
-
//!
|
|
9
|
-
//! Ruby callers see the seven raw readers registered below; the helper
|
|
10
|
-
//! methods that pack them into `Kobako::Capture` / `Kobako::Usage`
|
|
11
|
-
//! (`Kobako::Snapshot#stdout` / `#stderr` / `#usage`) live in
|
|
3
|
+
//! The success-path view of the engine-neutral snapshot: the outcome bytes
|
|
4
|
+
//! and the two captured output channels, exposed through five raw readers.
|
|
5
|
+
//! The helper methods that pack them into `Kobako::Capture`
|
|
6
|
+
//! (`Kobako::Snapshot#stdout` / `#stderr`) live in
|
|
12
7
|
//! `lib/kobako/snapshot.rb`. The split keeps the ext side a pure value
|
|
13
|
-
//! carrier and lets Ruby own the convenience surface.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
use std::time::Duration;
|
|
8
|
+
//! carrier and lets Ruby own the convenience surface. Usage is not on the
|
|
9
|
+
//! Snapshot — `Sandbox#usage` reads it from `Kobako::Runtime#usage`, which
|
|
10
|
+
//! survives the trap path where no `Kobako::Snapshot` is produced.
|
|
17
11
|
|
|
18
12
|
use magnus::{method, prelude::*, Error as MagnusError, RModule, RString, Ruby};
|
|
19
13
|
|
|
14
|
+
use kobako_runtime::snapshot::Capture;
|
|
15
|
+
|
|
20
16
|
/// Per-invocation snapshot value. Magnus wraps it so a single ext call
|
|
21
|
-
/// from `Runtime::eval` / `Runtime::run` returns the whole bundle —
|
|
22
|
-
///
|
|
23
|
-
///
|
|
24
|
-
/// in `init` read them out one by one.
|
|
25
|
-
/// held as a `Cell<Duration>` only because magnus' `#[magnus::wrap]`
|
|
26
|
-
/// macro requires interior mutability — every field is set once at
|
|
27
|
-
/// construction time and never mutated again.
|
|
17
|
+
/// from `Runtime::eval` / `Runtime::run` returns the whole bundle — the
|
|
18
|
+
/// Sandbox layer decomposes it without round-tripping into ext again. The
|
|
19
|
+
/// fields are set once at construction and never mutated; the five public
|
|
20
|
+
/// methods registered in `init` read them out one by one.
|
|
28
21
|
#[magnus::wrap(class = "Kobako::Snapshot", free_immediately, size)]
|
|
29
22
|
pub(crate) struct Snapshot {
|
|
30
23
|
return_bytes: Vec<u8>,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
stderr_bytes: Vec<u8>,
|
|
34
|
-
stderr_truncated: bool,
|
|
35
|
-
wall_time: Cell<Duration>,
|
|
36
|
-
memory_peak: usize,
|
|
24
|
+
stdout: Capture,
|
|
25
|
+
stderr: Capture,
|
|
37
26
|
}
|
|
38
27
|
|
|
39
28
|
impl Snapshot {
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
-
///
|
|
43
|
-
|
|
44
|
-
/// and the capture pipes have been clipped to their caps.
|
|
45
|
-
pub(crate) fn new(
|
|
46
|
-
return_bytes: Vec<u8>,
|
|
47
|
-
stdout_bytes: Vec<u8>,
|
|
48
|
-
stdout_truncated: bool,
|
|
49
|
-
stderr_bytes: Vec<u8>,
|
|
50
|
-
stderr_truncated: bool,
|
|
51
|
-
wall_time: Duration,
|
|
52
|
-
memory_peak: usize,
|
|
53
|
-
) -> Self {
|
|
29
|
+
/// Bundle the success outputs the Runtime collected once the guest
|
|
30
|
+
/// export returned with an outcome: the drained OUTCOME_BUFFER bytes
|
|
31
|
+
/// and the capture pipes clipped to their caps.
|
|
32
|
+
pub(crate) fn new(return_bytes: Vec<u8>, stdout: Capture, stderr: Capture) -> Self {
|
|
54
33
|
Self {
|
|
55
34
|
return_bytes,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
stderr_bytes,
|
|
59
|
-
stderr_truncated,
|
|
60
|
-
wall_time: Cell::new(wall_time),
|
|
61
|
-
memory_peak,
|
|
35
|
+
stdout,
|
|
36
|
+
stderr,
|
|
62
37
|
}
|
|
63
38
|
}
|
|
64
39
|
|
|
@@ -69,32 +44,24 @@ impl Snapshot {
|
|
|
69
44
|
|
|
70
45
|
fn stdout_bytes(&self) -> RString {
|
|
71
46
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
72
|
-
ruby.str_from_slice(&self.
|
|
47
|
+
ruby.str_from_slice(&self.stdout.bytes)
|
|
73
48
|
}
|
|
74
49
|
|
|
75
50
|
fn stdout_truncated(&self) -> bool {
|
|
76
|
-
self.
|
|
51
|
+
self.stdout.truncated
|
|
77
52
|
}
|
|
78
53
|
|
|
79
54
|
fn stderr_bytes(&self) -> RString {
|
|
80
55
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
81
|
-
ruby.str_from_slice(&self.
|
|
56
|
+
ruby.str_from_slice(&self.stderr.bytes)
|
|
82
57
|
}
|
|
83
58
|
|
|
84
59
|
fn stderr_truncated(&self) -> bool {
|
|
85
|
-
self.
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
fn wall_time(&self) -> f64 {
|
|
89
|
-
self.wall_time.get().as_secs_f64()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
fn memory_peak(&self) -> usize {
|
|
93
|
-
self.memory_peak
|
|
60
|
+
self.stderr.truncated
|
|
94
61
|
}
|
|
95
62
|
}
|
|
96
63
|
|
|
97
|
-
/// Register `Kobako::Snapshot` plus its
|
|
64
|
+
/// Register `Kobako::Snapshot` plus its five raw readers under the
|
|
98
65
|
/// `Kobako` module. Called from `crate::init` after `Kobako::Runtime`
|
|
99
66
|
/// is registered so the magnus wrap macro can resolve the class name.
|
|
100
67
|
pub(crate) fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
@@ -104,7 +71,5 @@ pub(crate) fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
104
71
|
snapshot.define_method("stdout_truncated", method!(Snapshot::stdout_truncated, 0))?;
|
|
105
72
|
snapshot.define_method("stderr_bytes", method!(Snapshot::stderr_bytes, 0))?;
|
|
106
73
|
snapshot.define_method("stderr_truncated", method!(Snapshot::stderr_truncated, 0))?;
|
|
107
|
-
snapshot.define_method("wall_time", method!(Snapshot::wall_time, 0))?;
|
|
108
|
-
snapshot.define_method("memory_peak", method!(Snapshot::memory_peak, 0))?;
|
|
109
74
|
Ok(())
|
|
110
75
|
}
|
|
@@ -38,7 +38,7 @@ module Kobako
|
|
|
38
38
|
# for it. +object+ is any host-side Ruby object to bind. Returns a
|
|
39
39
|
# freshly-allocated +Kobako::Handle+ whose +#id+ falls in
|
|
40
40
|
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
41
|
-
# +Kobako::
|
|
41
|
+
# +Kobako::HandleExhaustedError+ if the next ID would exceed the
|
|
42
42
|
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
43
43
|
# and the allocator share the same invariant.
|
|
44
44
|
#
|
|
@@ -96,12 +96,12 @@ module Kobako
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
# Guard {#alloc} against issuing an ID past the cap. Returns +nil+
|
|
99
|
-
# on success; raises +Kobako::
|
|
99
|
+
# on success; raises +Kobako::HandleExhaustedError+ at exhaustion.
|
|
100
100
|
def ensure_capacity!
|
|
101
101
|
cap = Kobako::Handle::MAX_ID
|
|
102
102
|
return unless @next_id > cap
|
|
103
103
|
|
|
104
|
-
raise
|
|
104
|
+
raise HandleExhaustedError,
|
|
105
105
|
"Out of handle allocations: too many host objects were referenced " \
|
|
106
106
|
"in a single invocation (limit #{cap})"
|
|
107
107
|
end
|
|
@@ -82,6 +82,10 @@ module Kobako
|
|
|
82
82
|
# first invocation, so the preamble is exactly the bindings that
|
|
83
83
|
# existed at that moment — a bind reaching a +Kobako::Namespace+
|
|
84
84
|
# after the seal raises +ArgumentError+ and never alters Frame 1.
|
|
85
|
+
# The memo is gated on the seal rather than dropped per mutation (the
|
|
86
|
+
# +Catalog::Snippets#encode+ approach) because a +Member+ bind lands
|
|
87
|
+
# on a child +Kobako::Namespace+, invisible to this collection; only
|
|
88
|
+
# the seal guarantees nothing further can change.
|
|
85
89
|
def encode
|
|
86
90
|
return @encoded if @encoded
|
|
87
91
|
|
|
@@ -44,6 +44,10 @@ module Kobako
|
|
|
44
44
|
# The bytes are memoized — the table is replayed verbatim on every
|
|
45
45
|
# invocation after sealing, so Frame 3 never changes between
|
|
46
46
|
# encodes; {#register} drops the memo while the table is still open.
|
|
47
|
+
# Unlike +Catalog::Namespaces#encode+, which gates its memo on the
|
|
48
|
+
# seal, this one can fill eagerly and invalidate in +#register+
|
|
49
|
+
# because every mutation funnels through that single method — there is
|
|
50
|
+
# no out-of-sight child object to change the result behind its back.
|
|
47
51
|
def encode
|
|
48
52
|
return @encoded if @encoded
|
|
49
53
|
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -26,7 +26,11 @@ module Kobako
|
|
|
26
26
|
# mapping is a closed set, and anything outside it is rejected by
|
|
27
27
|
# the msgpack gem itself (arbitrary objects raise +NoMethodError+
|
|
28
28
|
# from missing +to_msgpack+, integers outside i64..u64 raise
|
|
29
|
-
# +RangeError+).
|
|
29
|
+
# +RangeError+). The +NoMethodError+ catch is deliberately broad:
|
|
30
|
+
# MessagePack signals "no wire representation" only through that error,
|
|
31
|
+
# so there is no narrower discriminator — a packer-internal
|
|
32
|
+
# +NoMethodError+ is likewise reported as +UnsupportedType+ rather than
|
|
33
|
+
# propagating.
|
|
30
34
|
def self.encode(value)
|
|
31
35
|
Factory.dump(value)
|
|
32
36
|
rescue ::RangeError, ::NoMethodError => e
|
data/lib/kobako/codec/factory.rb
CHANGED
|
@@ -48,6 +48,16 @@ module Kobako
|
|
|
48
48
|
EXT_ERRENV = 0x02
|
|
49
49
|
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
50
50
|
|
|
51
|
+
# An ext 0x02 (Fault) envelope nests through its +details+ field, and
|
|
52
|
+
# each level re-enters the codec with a fresh +MessagePack+ unpacker
|
|
53
|
+
# whose built-in stack guard resets — so ext-envelope depth is tracked
|
|
54
|
+
# here instead. The cap matches the wire's overall nesting bound and
|
|
55
|
+
# keeps a nested chain from exhausting the native stack: an over-deep
|
|
56
|
+
# chain fails as a clean wire error, never a stack-level trap.
|
|
57
|
+
MAX_EXT_DEPTH = 128
|
|
58
|
+
EXT_DEPTH_KEY = :__kobako_codec_ext_depth__
|
|
59
|
+
private_constant :MAX_EXT_DEPTH, :EXT_DEPTH_KEY
|
|
60
|
+
|
|
51
61
|
# Instance-level pass-through onto the wrapped +MessagePack::Factory+.
|
|
52
62
|
# Spelled +def_instance_delegators+ rather than +def_delegators+ because
|
|
53
63
|
# the class also extends +SingleForwardable+ (see the +extend+ block
|
|
@@ -129,9 +139,13 @@ module Kobako
|
|
|
129
139
|
# Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
|
|
130
140
|
# the embedded payload flows through the same boundary as a top-level
|
|
131
141
|
# encode — nested kobako values (Handle, nested Fault) reach the
|
|
132
|
-
# registered ext-type packers via the cached singleton.
|
|
142
|
+
# registered ext-type packers via the cached singleton. A +details+
|
|
143
|
+
# chain nested past {MAX_EXT_DEPTH} has no wire representation and
|
|
144
|
+
# surfaces as +UnsupportedType+.
|
|
133
145
|
def pack_fault(fault)
|
|
134
|
-
|
|
146
|
+
within_ext_frame(UnsupportedType) do
|
|
147
|
+
Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
|
|
148
|
+
end
|
|
135
149
|
end
|
|
136
150
|
|
|
137
151
|
# Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
|
|
@@ -139,19 +153,33 @@ module Kobako
|
|
|
139
153
|
# +ArgumentError+ invariants surface as +InvalidType+ through the
|
|
140
154
|
# decoder boundary. Inner decode goes through {Decoder} (not
|
|
141
155
|
# +factory.load+) so the embedded +str+ payloads flow through the
|
|
142
|
-
# same UTF-8 validation as a top-level decode.
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
# singleton instance feeds +Decoder.decode+, which re-enters this
|
|
146
|
-
# method when a nested ext 0x02 appears inside +details+. The recursion
|
|
147
|
-
# is bounded by msgpack nesting depth — identical to nested Array /
|
|
148
|
-
# Hash payloads — so no extra guard is needed. Do not switch back to
|
|
149
|
-
# +factory.load+ to "simplify": that path bypasses UTF-8 validation.
|
|
156
|
+
# same UTF-8 validation as a top-level decode. A nested ext 0x02 in
|
|
157
|
+
# +details+ re-enters this method, so {#within_ext_frame} bounds the
|
|
158
|
+
# chain depth to keep it from exhausting the native stack.
|
|
150
159
|
def unpack_fault(payload)
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
within_ext_frame(InvalidType) do
|
|
161
|
+
Decoder.decode(payload) do |map|
|
|
162
|
+
raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
153
168
|
|
|
154
|
-
|
|
169
|
+
# Track ext-envelope re-entry depth and refuse a chain past
|
|
170
|
+
# {MAX_EXT_DEPTH}, raising +over_limit+ so the failure lands in the
|
|
171
|
+
# caller's existing wire-error class. The counter is thread-scoped and
|
|
172
|
+
# balanced by the +ensure+, so a raise mid-chain still unwinds it to
|
|
173
|
+
# its entry value.
|
|
174
|
+
def within_ext_frame(over_limit)
|
|
175
|
+
depth = (Thread.current[EXT_DEPTH_KEY] || 0) + 1
|
|
176
|
+
raise over_limit, "ext envelope nesting exceeds #{MAX_EXT_DEPTH} levels" if depth > MAX_EXT_DEPTH
|
|
177
|
+
|
|
178
|
+
Thread.current[EXT_DEPTH_KEY] = depth
|
|
179
|
+
begin
|
|
180
|
+
yield
|
|
181
|
+
ensure
|
|
182
|
+
Thread.current[EXT_DEPTH_KEY] = depth - 1
|
|
155
183
|
end
|
|
156
184
|
end
|
|
157
185
|
end
|
|
@@ -97,6 +97,10 @@ module Kobako
|
|
|
97
97
|
case value
|
|
98
98
|
when ::Array then value.map { |element| HandleWalk.deep_restore(element, handler) }
|
|
99
99
|
when ::Hash
|
|
100
|
+
# Rebuilt with each key restored: two distinct Handle keys that
|
|
101
|
+
# resolve to equal host objects collapse to the later pair, as in
|
|
102
|
+
# any Ruby Hash. The guest authored this payload, so that collapse
|
|
103
|
+
# is its own concern, not a fidelity guarantee the host owes it.
|
|
100
104
|
value.to_h { |key, val| [HandleWalk.deep_restore(key, handler), HandleWalk.deep_restore(val, handler)] }
|
|
101
105
|
when Kobako::Handle then handler.fetch(value.id)
|
|
102
106
|
else value
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Kobako
|
|
|
35
35
|
#
|
|
36
36
|
# * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
|
|
37
37
|
# at +wasm_path+.
|
|
38
|
-
# * {
|
|
38
|
+
# * {HandleExhaustedError} < {SandboxError} — Handle id cap hit.
|
|
39
39
|
|
|
40
40
|
# Base for all kobako-raised errors so callers that want to ignore the
|
|
41
41
|
# taxonomy can rescue a single class.
|
|
@@ -89,10 +89,13 @@ module Kobako
|
|
|
89
89
|
# +ModuleNotBuiltError+ first.
|
|
90
90
|
class ModuleNotBuiltError < SetupError; end
|
|
91
91
|
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
|
|
92
|
+
# The structured attribution the two invocation-failure classes carry
|
|
93
|
+
# from a decoded guest exception — its +origin+, original +klass+,
|
|
94
|
+
# +backtrace_lines+, and +details+ — so a Host App can inspect a failure
|
|
95
|
+
# beyond its message. Mixed into both rather than promoted to a shared
|
|
96
|
+
# superclass because +SandboxError+ and +ServiceError+ sit in distinct
|
|
97
|
+
# branches of the invocation-outcome taxonomy under +Kobako::Error+.
|
|
98
|
+
module StructuredError
|
|
96
99
|
attr_reader :origin, :klass, :backtrace_lines, :details
|
|
97
100
|
|
|
98
101
|
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
|
|
@@ -104,26 +107,25 @@ module Kobako
|
|
|
104
107
|
end
|
|
105
108
|
end
|
|
106
109
|
|
|
110
|
+
# Sandbox / wire layer. Raised when the guest ran to completion but
|
|
111
|
+
# execution failed due to a mruby script error, a protocol fault, or a
|
|
112
|
+
# host-side wire decode failure on an otherwise valid outcome tag.
|
|
113
|
+
class SandboxError < Error
|
|
114
|
+
include StructuredError
|
|
115
|
+
end
|
|
116
|
+
|
|
107
117
|
# Service layer. Raised when a Service capability call inside a mruby
|
|
108
118
|
# script reported an application-level failure that the script did not
|
|
109
119
|
# rescue.
|
|
110
120
|
class ServiceError < Error
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
|
|
114
|
-
super(message)
|
|
115
|
-
@origin = origin
|
|
116
|
-
@klass = klass
|
|
117
|
-
@backtrace_lines = backtrace_lines
|
|
118
|
-
@details = details
|
|
119
|
-
end
|
|
121
|
+
include StructuredError
|
|
120
122
|
end
|
|
121
123
|
|
|
122
|
-
#
|
|
124
|
+
# HandleExhaustedError is the canonical SandboxError subclass for the
|
|
123
125
|
# id-cap-hit path. Raised when the per-invocation Handle ID counter in
|
|
124
126
|
# Catalog::Handles reaches +0x7fff_ffff+ (2³¹ − 1) and further
|
|
125
127
|
# allocation would exceed the cap.
|
|
126
|
-
class
|
|
128
|
+
class HandleExhaustedError < SandboxError; end
|
|
127
129
|
|
|
128
130
|
# BytecodeError is the SandboxError subclass raised when a
|
|
129
131
|
# `#preload(binary:)` snippet fails structural validation during the
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -89,7 +89,9 @@ module Kobako
|
|
|
89
89
|
# normalisation. The constructed +SandboxOptions+ is exposed as
|
|
90
90
|
# +#options+ and the four caps remain readable directly on Sandbox via
|
|
91
91
|
# +Forwardable+ delegation.
|
|
92
|
-
def initialize(wasm_path: nil,
|
|
92
|
+
def initialize(wasm_path: nil,
|
|
93
|
+
stdout_limit: SandboxOptions::DEFAULT_OUTPUT_LIMIT,
|
|
94
|
+
stderr_limit: SandboxOptions::DEFAULT_OUTPUT_LIMIT,
|
|
93
95
|
timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
|
|
94
96
|
memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
|
|
95
97
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
@@ -207,17 +209,17 @@ module Kobako
|
|
|
207
209
|
|
|
208
210
|
private
|
|
209
211
|
|
|
210
|
-
# Configure the +Runtime+'s host↔guest dispatch wiring.
|
|
211
|
-
#
|
|
212
|
-
# +
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
#
|
|
212
|
+
# Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
|
|
213
|
+
# dispatch +Proc+ that routes guest→host calls through the stateless
|
|
214
|
+
# +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
|
|
215
|
+
# closure. The ext hands the +Proc+ a per-dispatch +guest_yielder+ — a
|
|
216
|
+
# +String → String+ callable that re-enters the in-flight guest to run a
|
|
217
|
+
# yielded block — which the +Dispatcher+ forwards to the +Transport::Yielder+
|
|
218
|
+
# it builds for the call. Registered once at construction time so the
|
|
219
|
+
# wasm ext callback can fire without further setup.
|
|
217
220
|
def install_dispatch_proc!
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, yield_to_guest)
|
|
221
|
+
@runtime.on_dispatch = lambda do |request_bytes, guest_yielder|
|
|
222
|
+
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, guest_yielder)
|
|
221
223
|
end
|
|
222
224
|
end
|
|
223
225
|
|
|
@@ -226,8 +228,7 @@ module Kobako
|
|
|
226
228
|
# state — capture buffers, truncation predicates, and the
|
|
227
229
|
# +Catalog::Handles+ counter — before the guest runs. The
|
|
228
230
|
# +Catalog::Handles+ itself is held as +@handler+ and never exposed
|
|
229
|
-
# beyond this class
|
|
230
|
-
# Host App".
|
|
231
|
+
# beyond this class — it is not part of the Host App's surface.
|
|
231
232
|
def begin_invocation!
|
|
232
233
|
@services.seal!
|
|
233
234
|
@handler.reset!
|
|
@@ -239,9 +240,10 @@ module Kobako
|
|
|
239
240
|
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
240
241
|
# every outcome — value return, +Kobako::TrapError+ (including
|
|
241
242
|
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
242
|
-
# and +Kobako::ServiceError+.
|
|
243
|
-
#
|
|
244
|
-
#
|
|
243
|
+
# and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
|
|
244
|
+
# both paths: the figures are stashed in the ext on every outcome, so
|
|
245
|
+
# unlike the +Snapshot+ (built only on success) the readout here also
|
|
246
|
+
# covers the trap path.
|
|
245
247
|
#
|
|
246
248
|
# The ext returns a positional 2-tuple +[wall_time, memory_peak]+
|
|
247
249
|
# whose order matches the +Kobako::Usage+ field order; the
|
|
@@ -272,8 +274,8 @@ module Kobako
|
|
|
272
274
|
# TrapError message so the failing export is identifiable.
|
|
273
275
|
#
|
|
274
276
|
# The yielded block must return a +Kobako::Snapshot+ — i.e. the
|
|
275
|
-
# value of +Runtime#eval+ / +#run
|
|
276
|
-
#
|
|
277
|
+
# value of +Runtime#eval+ / +#run+. The success path unpacks
|
|
278
|
+
# +#stdout+ / +#stderr+ into
|
|
277
279
|
# +Capture+ and feeds +#return_bytes+ to +Outcome.decode+; usage is
|
|
278
280
|
# populated by the +ensure+ readout ({#read_usage!}) on every outcome.
|
|
279
281
|
# The rescue chain is the single trap-translation boundary —
|
|
@@ -6,11 +6,13 @@ module Kobako
|
|
|
6
6
|
# Data.define(...)+ subclass form (the Steep-friendly shape — see
|
|
7
7
|
# +lib/kobako/outcome/panic.rb+).
|
|
8
8
|
#
|
|
9
|
-
# The +initialize+
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
9
|
+
# The +initialize+ normalises every cap before delegating to Data's
|
|
10
|
+
# +super+: +timeout+ to Float seconds, +memory_limit+ / +stdout_limit+ /
|
|
11
|
+
# +stderr_limit+ to positive Integer bytes. Each cap is +nil+-disablable
|
|
12
|
+
# (an absent argument takes its DEFAULT; an explicit +nil+ leaves the
|
|
13
|
+
# bound off), so all four behave uniformly. Anything that survives
|
|
14
|
+
# +SandboxOptions.new+ is a wire-ready cap bundle the +Kobako::Runtime+
|
|
15
|
+
# constructor consumes as-is.
|
|
14
16
|
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit)
|
|
15
17
|
# Default wall-clock timeout for a single invocation: 60 seconds.
|
|
16
18
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
@@ -25,12 +27,12 @@ module Kobako
|
|
|
25
27
|
|
|
26
28
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
27
29
|
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
28
|
-
stdout_limit:
|
|
29
|
-
stderr_limit:
|
|
30
|
+
stdout_limit: DEFAULT_OUTPUT_LIMIT,
|
|
31
|
+
stderr_limit: DEFAULT_OUTPUT_LIMIT)
|
|
30
32
|
timeout = normalize_timeout(timeout)
|
|
31
33
|
memory_limit = normalize_memory_limit(memory_limit)
|
|
32
|
-
stdout_limit
|
|
33
|
-
stderr_limit
|
|
34
|
+
stdout_limit = normalize_output_limit(stdout_limit, "stdout_limit")
|
|
35
|
+
stderr_limit = normalize_output_limit(stderr_limit, "stderr_limit")
|
|
34
36
|
super
|
|
35
37
|
end
|
|
36
38
|
|
|
@@ -62,5 +64,19 @@ module Kobako
|
|
|
62
64
|
|
|
63
65
|
memory_limit
|
|
64
66
|
end
|
|
67
|
+
|
|
68
|
+
# Coerce a per-channel output cap (+stdout_limit+ / +stderr_limit+)
|
|
69
|
+
# into the byte cap the ext expects, or +nil+ to leave the channel
|
|
70
|
+
# uncapped. Same shape as +normalize_memory_limit+: a positive Integer
|
|
71
|
+
# when set, Float / zero / negative rejected. +name+ tags the
|
|
72
|
+
# +ArgumentError+ with the offending keyword.
|
|
73
|
+
def normalize_output_limit(limit, name)
|
|
74
|
+
return nil if limit.nil?
|
|
75
|
+
unless limit.is_a?(Integer) && limit.positive?
|
|
76
|
+
raise ArgumentError, "#{name} must be a positive Integer or nil, got #{limit.inspect}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
limit
|
|
80
|
+
end
|
|
65
81
|
end
|
|
66
82
|
end
|
data/lib/kobako/snapshot.rb
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "capture"
|
|
4
|
-
require_relative "usage"
|
|
5
4
|
|
|
6
5
|
module Kobako
|
|
7
6
|
# Kobako::Snapshot — per-invocation observable bundle returned from
|
|
8
7
|
# +Kobako::Runtime#eval+ and +#run+.
|
|
9
8
|
#
|
|
10
|
-
# The magnus class (see ext/kobako/src/snapshot.rs) carries
|
|
9
|
+
# The magnus class (see ext/kobako/src/snapshot.rs) carries five raw
|
|
11
10
|
# readers: +return_bytes+, +stdout_bytes+, +stdout_truncated+,
|
|
12
|
-
# +stderr_bytes+, +stderr_truncated
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
11
|
+
# +stderr_bytes+, +stderr_truncated+. This file reopens the class to add
|
|
12
|
+
# the Ruby-side helpers that pack those raw fields into the user-facing
|
|
13
|
+
# value object +Kobako::Capture+ — the same shape +Kobako::Sandbox+
|
|
14
|
+
# exposes to the Host App. Usage is not on the Snapshot; +Sandbox#usage+
|
|
15
|
+
# reads it from +Kobako::Runtime#usage+, which also covers the trap path
|
|
16
|
+
# where no Snapshot is produced.
|
|
17
17
|
class Snapshot
|
|
18
18
|
# Wrap the stdout capture pair (+stdout_bytes+, +stdout_truncated+)
|
|
19
19
|
# as a +Kobako::Capture+ value object. The byte content never carries
|
|
@@ -28,11 +28,5 @@ module Kobako
|
|
|
28
28
|
def stderr
|
|
29
29
|
Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
|
|
30
30
|
end
|
|
31
|
-
|
|
32
|
-
# Wrap the per-last-invocation usage pair (+wall_time+,
|
|
33
|
-
# +memory_peak+) as a +Kobako::Usage+ value object.
|
|
34
|
-
def usage
|
|
35
|
-
Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
36
|
-
end
|
|
37
31
|
end
|
|
38
32
|
end
|
|
@@ -72,8 +72,8 @@ module Kobako
|
|
|
72
72
|
# closure so the Dispatcher stays stateless and the registry doesn't
|
|
73
73
|
# need to publish accessors for the Sandbox-owned +Catalog::Handles+
|
|
74
74
|
# or +Runtime+. +yield_to_guest+ is a +String → String+ callable
|
|
75
|
-
# (
|
|
76
|
-
#
|
|
75
|
+
# (the ext's per-dispatch +Kobako::Runtime::GuestYielder+) used only
|
|
76
|
+
# when the Request carries +block_given: true+. Always
|
|
77
77
|
# returns a binary String — every failure path is reified as a
|
|
78
78
|
# Response.error envelope so the guest sees a transport error rather
|
|
79
79
|
# than a wasm trap.
|
|
@@ -38,20 +38,6 @@ module Kobako
|
|
|
38
38
|
new(status: STATUS_ERROR, payload: fault)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# Decode +bytes+ into a {Response}. Raises +Codec::InvalidType+ when the
|
|
42
|
-
# envelope is not the expected 2-element msgpack array, or when the
|
|
43
|
-
# Value Object's construction invariants reject the decoded fields.
|
|
44
|
-
def self.decode(bytes)
|
|
45
|
-
Codec::Decoder.decode(bytes) do |arr|
|
|
46
|
-
unless arr.is_a?(Array) && arr.length == 2
|
|
47
|
-
raise Codec::InvalidType, "Response envelope is malformed (expected a 2-element array)"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
status, payload = arr
|
|
51
|
-
new(status: status, payload: payload)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
41
|
def initialize(status:, payload:)
|
|
56
42
|
unless [STATUS_OK, STATUS_ERROR].include?(status)
|
|
57
43
|
raise ArgumentError, "Response status must be 0 (ok) or 1 (error), got #{status.inspect}"
|
|
@@ -71,6 +57,20 @@ module Kobako
|
|
|
71
57
|
def encode
|
|
72
58
|
Codec::Encoder.encode([status, payload])
|
|
73
59
|
end
|
|
60
|
+
|
|
61
|
+
# Decode +bytes+ into a {Response}. Raises +Codec::InvalidType+ when the
|
|
62
|
+
# envelope is not the expected 2-element msgpack array, or when the
|
|
63
|
+
# Value Object's construction invariants reject the decoded fields.
|
|
64
|
+
def self.decode(bytes)
|
|
65
|
+
Codec::Decoder.decode(bytes) do |arr|
|
|
66
|
+
unless arr.is_a?(Array) && arr.length == 2
|
|
67
|
+
raise Codec::InvalidType, "Response envelope is malformed (expected a 2-element array)"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
status, payload = arr
|
|
71
|
+
new(status: status, payload: payload)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
end
|
data/lib/kobako/transport/run.rb
CHANGED
|
@@ -43,8 +43,8 @@ module Kobako
|
|
|
43
43
|
|
|
44
44
|
def initialize(entrypoint:, args: [], kwargs: {})
|
|
45
45
|
entrypoint = normalize_entrypoint(entrypoint)
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
validate_args!(args)
|
|
47
|
+
validate_kwargs!(kwargs)
|
|
48
48
|
super
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -100,8 +100,6 @@ module Kobako
|
|
|
100
100
|
def validate_args!(args)
|
|
101
101
|
raise ArgumentError, "arguments must be an Array" unless args.is_a?(Array)
|
|
102
102
|
raise ArgumentError, forged_handle_message("arguments") if args.any?(Kobako::Handle)
|
|
103
|
-
|
|
104
|
-
args
|
|
105
103
|
end
|
|
106
104
|
|
|
107
105
|
# Reject a non-Symbol kwargs key, and a +Kobako::Handle+ arriving
|
|
@@ -117,8 +115,6 @@ module Kobako
|
|
|
117
115
|
"keyword argument keys must be Symbols (got #{bad_keys.inspect})"
|
|
118
116
|
end
|
|
119
117
|
raise ArgumentError, forged_handle_message("keyword argument values") if kwargs.each_value.any?(Kobako::Handle)
|
|
120
|
-
|
|
121
|
-
kwargs
|
|
122
118
|
end
|
|
123
119
|
|
|
124
120
|
# Single source of truth for the forged-Handle reject message so the
|
|
@@ -67,7 +67,7 @@ module Kobako
|
|
|
67
67
|
raise Codec::InvalidType, "YieldResponse must carry at least one byte" if bytes.empty?
|
|
68
68
|
|
|
69
69
|
tag = bytes.getbyte(0) # : Integer
|
|
70
|
-
body = bytes.byteslice(1, bytes.bytesize - 1)
|
|
70
|
+
body = bytes.byteslice(1, bytes.bytesize - 1) # : String
|
|
71
71
|
|
|
72
72
|
reject_dead_tag!(tag)
|
|
73
73
|
new(tag: tag, value: Codec::Decoder.decode(body))
|
|
@@ -28,8 +28,8 @@ module Kobako
|
|
|
28
28
|
# dispatch completes; any later call to a stashed Yielder then raises
|
|
29
29
|
# +LocalJumpError+ — the observable shape of an escaped Yielder.
|
|
30
30
|
class Yielder
|
|
31
|
-
# +yield_to_guest+ is a +String → String+ callable (
|
|
32
|
-
# +Runtime
|
|
31
|
+
# +yield_to_guest+ is a +String → String+ callable (the ext's
|
|
32
|
+
# per-dispatch +Kobako::Runtime::GuestYielder+) that
|
|
33
33
|
# {#yield} invokes to re-enter the guest; +break_tag+ is the +catch+
|
|
34
34
|
# throw tag the Dispatcher matches against to unwind the Service on
|
|
35
35
|
# +tag 0x02+. +handler+ is the Sandbox's +Kobako::Catalog::Handles+,
|
data/lib/kobako/version.rb
CHANGED