kobako 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +22 -0
- data/Cargo.lock +138 -136
- data/Cargo.toml +6 -2
- data/README.md +3 -1
- data/crates/kobako-runtime/CHANGELOG.md +16 -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 +18 -0
- data/crates/kobako-runtime/src/profile.rs +35 -0
- data/crates/kobako-runtime/src/runtime.rs +57 -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 +16 -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 +8 -5
- 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 +33 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
- data/crates/kobako-wasmtime/src/driver.rs +297 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
- data/crates/kobako-wasmtime/src/frames.rs +276 -0
- 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/lib.rs +0 -2
- data/ext/kobako/src/runtime/bridge.rs +150 -0
- data/ext/kobako/src/runtime/errors.rs +45 -13
- data/ext/kobako/src/runtime.rs +246 -420
- 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/codec.rb +1 -1
- data/lib/kobako/errors.rb +18 -16
- data/lib/kobako/sandbox.rb +71 -39
- data/lib/kobako/sandbox_options.rb +71 -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/lib/kobako.rb +0 -1
- data/release-please-config.json +78 -3
- data/sig/kobako/codec/factory.rbs +3 -0
- data/sig/kobako/errors.rbs +7 -14
- data/sig/kobako/runtime.rbs +16 -6
- data/sig/kobako/sandbox.rbs +11 -7
- data/sig/kobako/sandbox_options.rbs +15 -4
- 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 +28 -15
- data/ext/kobako/src/runtime/config.rs +0 -25
- data/ext/kobako/src/runtime/dispatch.rs +0 -211
- data/ext/kobako/src/runtime/frames.rs +0 -188
- data/ext/kobako/src/snapshot.rs +0 -110
- data/lib/kobako/snapshot.rb +0 -38
- data/sig/kobako/snapshot.rbs +0 -15
|
@@ -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/codec.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Kobako
|
|
|
22
22
|
# {Decoder} are thin wrappers that register the three kobako-specific
|
|
23
23
|
# ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
|
|
24
24
|
# envelope) on a single +MessagePack::Factory+ instance. The Rust side
|
|
25
|
-
# mirrors this layer as the +codec+ module in the +kobako-
|
|
25
|
+
# mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
|
|
26
26
|
# the ext-code constants live as module-private values on {Factory}
|
|
27
27
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
28
28
|
# +codec::EXT_ERRENV+ on that side.
|
data/lib/kobako/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
|
@@ -38,14 +38,16 @@ module Kobako
|
|
|
38
38
|
|
|
39
39
|
attr_reader :wasm_path, :options
|
|
40
40
|
|
|
41
|
-
# Per-
|
|
41
|
+
# Per-option accessors forward to the immutable +SandboxOptions+ Value
|
|
42
42
|
# Object so the Host App still reads them off Sandbox directly.
|
|
43
|
-
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit
|
|
43
|
+
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile
|
|
44
44
|
|
|
45
45
|
# Returns the bytes the guest wrote to stdout during the most recent
|
|
46
46
|
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
47
47
|
# any invocation; the byte content never contains a truncation sentinel,
|
|
48
|
-
# so use +#stdout_truncated?+ to observe overflow.
|
|
48
|
+
# so use +#stdout_truncated?+ to observe overflow. Populated on every
|
|
49
|
+
# outcome — including a rescued +TrapError+, after which it holds the
|
|
50
|
+
# bytes written before the trap fired — mirroring +#usage+.
|
|
49
51
|
def stdout
|
|
50
52
|
@stdout_capture.bytes
|
|
51
53
|
end
|
|
@@ -83,23 +85,24 @@ module Kobako
|
|
|
83
85
|
# Build a fresh Sandbox.
|
|
84
86
|
#
|
|
85
87
|
# +wasm_path+ is the absolute path to the Guest Binary; defaults to the
|
|
86
|
-
# gem-bundled +data/kobako.wasm+.
|
|
87
|
-
# +stderr_limit+, +timeout+, +memory_limit+)
|
|
88
|
-
# +
|
|
88
|
+
# gem-bundled +data/kobako.wasm+. Every other keyword — the four caps
|
|
89
|
+
# (+stdout_limit+, +stderr_limit+, +timeout+, +memory_limit+) and the
|
|
90
|
+
# requested isolation profile (+profile+) — is forwarded verbatim to
|
|
91
|
+
# +Kobako::SandboxOptions+, which owns the DEFAULT fallbacks and
|
|
89
92
|
# normalisation. The constructed +SandboxOptions+ is exposed as
|
|
90
|
-
# +#options+ and
|
|
91
|
-
# +Forwardable+ delegation.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
# +#options+ and every option remains readable directly on Sandbox via
|
|
94
|
+
# +Forwardable+ delegation. The runtime builds the requested profile —
|
|
95
|
+
# +:hermetic+ (the default) denies the guest ambient time and entropy,
|
|
96
|
+
# +:permissive+ leaves them live — and construction refuses a runtime
|
|
97
|
+
# whose declared profile falls below the request, raising
|
|
98
|
+
# +Kobako::SetupError+ before any invocation entry point runs.
|
|
99
|
+
def initialize(wasm_path: nil, **)
|
|
95
100
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
96
|
-
@options = SandboxOptions.new(
|
|
97
|
-
stderr_limit: stderr_limit)
|
|
101
|
+
@options = SandboxOptions.new(**)
|
|
98
102
|
@handler = Catalog::Handles.new
|
|
99
103
|
@services = Kobako::Catalog::Namespaces.new(handler: @handler)
|
|
100
104
|
@snippets = Catalog::Snippets.new
|
|
101
|
-
@runtime =
|
|
102
|
-
@options.stdout_limit, @options.stderr_limit)
|
|
105
|
+
@runtime = build_runtime!
|
|
103
106
|
install_dispatch_proc!
|
|
104
107
|
reset_invocation_state!
|
|
105
108
|
end
|
|
@@ -207,17 +210,28 @@ module Kobako
|
|
|
207
210
|
|
|
208
211
|
private
|
|
209
212
|
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# +
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
# Construct the +Runtime+ with the requested isolation profile and
|
|
214
|
+
# refuse one whose declared posture falls below the request —
|
|
215
|
+
# +SandboxOptions#enforce_floor!+ owns the ladder comparison, so a
|
|
216
|
+
# runtime that cannot honor the request never runs guest code.
|
|
217
|
+
def build_runtime!
|
|
218
|
+
runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
219
|
+
@options.stdout_limit, @options.stderr_limit, profile)
|
|
220
|
+
@options.enforce_floor!(runtime.profile)
|
|
221
|
+
runtime
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
|
|
225
|
+
# dispatch +Proc+ that routes guest→host calls through the stateless
|
|
226
|
+
# +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
|
|
227
|
+
# closure. The ext hands the +Proc+ a per-dispatch +guest_yielder+ — a
|
|
228
|
+
# +String → String+ callable that re-enters the in-flight guest to run a
|
|
229
|
+
# yielded block — which the +Dispatcher+ forwards to the +Transport::Yielder+
|
|
230
|
+
# it builds for the call. Registered once at construction time so the
|
|
231
|
+
# wasm ext callback can fire without further setup.
|
|
217
232
|
def install_dispatch_proc!
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, yield_to_guest)
|
|
233
|
+
@runtime.on_dispatch = lambda do |request_bytes, guest_yielder|
|
|
234
|
+
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, guest_yielder)
|
|
221
235
|
end
|
|
222
236
|
end
|
|
223
237
|
|
|
@@ -226,8 +240,7 @@ module Kobako
|
|
|
226
240
|
# state — capture buffers, truncation predicates, and the
|
|
227
241
|
# +Catalog::Handles+ counter — before the guest runs. The
|
|
228
242
|
# +Catalog::Handles+ itself is held as +@handler+ and never exposed
|
|
229
|
-
# beyond this class
|
|
230
|
-
# Host App".
|
|
243
|
+
# beyond this class — it is not part of the Host App's surface.
|
|
231
244
|
def begin_invocation!
|
|
232
245
|
@services.seal!
|
|
233
246
|
@handler.reset!
|
|
@@ -239,9 +252,10 @@ module Kobako
|
|
|
239
252
|
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
240
253
|
# every outcome — value return, +Kobako::TrapError+ (including
|
|
241
254
|
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
242
|
-
# and +Kobako::ServiceError+.
|
|
243
|
-
#
|
|
244
|
-
#
|
|
255
|
+
# and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
|
|
256
|
+
# both paths: the figures are stashed in the ext on every outcome, so
|
|
257
|
+
# the readout here also covers the trap path, where +Runtime#eval+ /
|
|
258
|
+
# +#run+ raise instead of returning outcome bytes.
|
|
245
259
|
#
|
|
246
260
|
# The ext returns a positional 2-tuple +[wall_time, memory_peak]+
|
|
247
261
|
# whose order matches the +Kobako::Usage+ field order; the
|
|
@@ -267,33 +281,51 @@ module Kobako
|
|
|
267
281
|
end
|
|
268
282
|
end
|
|
269
283
|
|
|
284
|
+
# Read the per-last-invocation output captures from the ext and wrap
|
|
285
|
+
# them as +Kobako::Capture+ value objects. Runs in the +invoke!+
|
|
286
|
+
# +ensure+ block next to {#read_usage!} for the same reason: the ext
|
|
287
|
+
# stashes the captures on every outcome, so the readout also covers
|
|
288
|
+
# the trap path, where +Runtime#eval+ / +#run+ raise instead of
|
|
289
|
+
# returning outcome bytes — +#stdout+ / +#stderr+ keep the guest's
|
|
290
|
+
# partial output readable after a rescue.
|
|
291
|
+
#
|
|
292
|
+
# The ext returns a positional 4-tuple
|
|
293
|
+
# +[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]+;
|
|
294
|
+
# the destructure-then-kwargs handoff below is the explicit
|
|
295
|
+
# positional→keyword conversion point.
|
|
296
|
+
def read_captures!
|
|
297
|
+
stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures
|
|
298
|
+
@stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
|
|
299
|
+
@stderr_capture = Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
|
|
300
|
+
end
|
|
301
|
+
|
|
270
302
|
# Shared prologue / epilogue + trap-class translator for both
|
|
271
303
|
# invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
|
|
272
304
|
# TrapError message so the failing export is identifiable.
|
|
273
305
|
#
|
|
274
|
-
# The yielded block must return
|
|
275
|
-
# value of +Runtime#eval+ / +#run+
|
|
276
|
-
#
|
|
277
|
-
# +
|
|
278
|
-
#
|
|
306
|
+
# The yielded block must return the invocation's raw outcome bytes —
|
|
307
|
+
# i.e. the value of +Runtime#eval+ / +#run+ — which the success path
|
|
308
|
+
# feeds to +Outcome.decode+. Captures and usage are populated by the
|
|
309
|
+
# +ensure+ readouts ({#read_usage!} / {#read_captures!}) on every
|
|
310
|
+
# outcome, so +#stdout+ / +#stderr+ / +#usage+ stay readable after a
|
|
311
|
+
# rescued trap.
|
|
279
312
|
# The rescue chain is the single trap-translation boundary —
|
|
280
313
|
# configured-cap paths surface as named TrapError subclasses
|
|
281
314
|
# (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
|
|
282
315
|
# the base +TrapError+.
|
|
283
316
|
def invoke!(verb)
|
|
284
317
|
begin_invocation!
|
|
285
|
-
|
|
286
|
-
@stdout_capture = snapshot.stdout
|
|
287
|
-
@stderr_capture = snapshot.stderr
|
|
318
|
+
return_bytes = yield
|
|
288
319
|
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
289
320
|
# token; restore it to the host object the guest referenced before
|
|
290
321
|
# handing the value to the Host App. @handler still holds this
|
|
291
322
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
292
|
-
Codec::HandleWalk.deep_restore(Outcome.decode(
|
|
323
|
+
Codec::HandleWalk.deep_restore(Outcome.decode(return_bytes), @handler)
|
|
293
324
|
rescue Kobako::TrapError => e
|
|
294
325
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
295
326
|
ensure
|
|
296
327
|
read_usage!
|
|
328
|
+
read_captures!
|
|
297
329
|
end
|
|
298
330
|
end
|
|
299
331
|
end
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
3
5
|
module Kobako
|
|
4
6
|
# Kobako::SandboxOptions — immutable Value Object holding the four
|
|
5
|
-
# per-Sandbox configuration caps
|
|
6
|
-
# Data.define(...)+ subclass form (the
|
|
7
|
-
# +lib/kobako/outcome/panic.rb+).
|
|
7
|
+
# per-Sandbox configuration caps and the requested isolation profile.
|
|
8
|
+
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
9
|
+
# Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
|
|
8
10
|
#
|
|
9
|
-
# The +initialize+
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
11
|
+
# The +initialize+ normalises every option before delegating to Data's
|
|
12
|
+
# +super+: +timeout+ to Float seconds, +memory_limit+ / +stdout_limit+ /
|
|
13
|
+
# +stderr_limit+ to positive Integer bytes. Each cap is +nil+-disablable
|
|
14
|
+
# (an absent argument takes its DEFAULT; an explicit +nil+ leaves the
|
|
15
|
+
# bound off), so all four behave uniformly. +profile+ is the one
|
|
16
|
+
# non-cap: a Symbol on the PROFILES ladder naming the posture the
|
|
17
|
+
# runtime builds, which is also the weakest the Host App accepts —
|
|
18
|
+
# +nil+ is rejected because the weakest posture is requested as an
|
|
19
|
+
# explicit +:permissive+. Anything that survives +SandboxOptions.new+
|
|
20
|
+
# is a wire-ready bundle the +Kobako::Runtime+ constructor consumes
|
|
21
|
+
# as-is. The options also own the ladder comparison
|
|
22
|
+
# (+#enforce_floor!+) that +Kobako::Sandbox+ delegates its
|
|
23
|
+
# construction floor check to.
|
|
24
|
+
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile)
|
|
15
25
|
# Default wall-clock timeout for a single invocation: 60 seconds.
|
|
16
26
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
17
27
|
|
|
@@ -23,17 +33,41 @@ module Kobako
|
|
|
23
33
|
# Default per-channel capture ceiling: 1 MiB.
|
|
24
34
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
25
35
|
|
|
36
|
+
# The isolation ladder, weakest first — index order is rank order,
|
|
37
|
+
# so a floor check is an index comparison.
|
|
38
|
+
PROFILES = %i[permissive hermetic].freeze
|
|
39
|
+
|
|
40
|
+
# Default isolation profile: the strictest rung — opting down to
|
|
41
|
+
# +:permissive+ is the Host App's explicit trade.
|
|
42
|
+
DEFAULT_PROFILE = :hermetic
|
|
43
|
+
|
|
26
44
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
27
45
|
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
28
|
-
stdout_limit:
|
|
29
|
-
stderr_limit:
|
|
46
|
+
stdout_limit: DEFAULT_OUTPUT_LIMIT,
|
|
47
|
+
stderr_limit: DEFAULT_OUTPUT_LIMIT,
|
|
48
|
+
profile: DEFAULT_PROFILE)
|
|
30
49
|
timeout = normalize_timeout(timeout)
|
|
31
50
|
memory_limit = normalize_memory_limit(memory_limit)
|
|
32
|
-
stdout_limit
|
|
33
|
-
stderr_limit
|
|
51
|
+
stdout_limit = normalize_output_limit(stdout_limit, "stdout_limit")
|
|
52
|
+
stderr_limit = normalize_output_limit(stderr_limit, "stderr_limit")
|
|
53
|
+
profile = normalize_profile(profile)
|
|
34
54
|
super
|
|
35
55
|
end
|
|
36
56
|
|
|
57
|
+
# Enforce the requested +profile+ as the floor against +declared+ —
|
|
58
|
+
# the posture a runtime reports having built — so a runtime that
|
|
59
|
+
# cannot honor the request fails construction with
|
|
60
|
+
# +Kobako::SetupError+ instead of weakening the posture silently.
|
|
61
|
+
# Both fallbacks fail closed: a declaration off the PROFILES ladder
|
|
62
|
+
# ranks below every request, and a request off the ladder
|
|
63
|
+
# (unreachable past +initialize+) refuses every declaration.
|
|
64
|
+
def enforce_floor!(declared)
|
|
65
|
+
return if (PROFILES.index(declared) || -1) >= (PROFILES.index(profile) || PROFILES.size)
|
|
66
|
+
|
|
67
|
+
raise Kobako::SetupError, "runtime declares isolation profile #{declared.inspect}, " \
|
|
68
|
+
"below the requested floor #{profile.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
37
71
|
private
|
|
38
72
|
|
|
39
73
|
# Coerce +timeout+ into the Float seconds the ext expects, or +nil+
|
|
@@ -62,5 +96,29 @@ module Kobako
|
|
|
62
96
|
|
|
63
97
|
memory_limit
|
|
64
98
|
end
|
|
99
|
+
|
|
100
|
+
# Coerce a per-channel output cap (+stdout_limit+ / +stderr_limit+)
|
|
101
|
+
# into the byte cap the ext expects, or +nil+ to leave the channel
|
|
102
|
+
# uncapped. Same shape as +normalize_memory_limit+: a positive Integer
|
|
103
|
+
# when set, Float / zero / negative rejected. +name+ tags the
|
|
104
|
+
# +ArgumentError+ with the offending keyword.
|
|
105
|
+
def normalize_output_limit(limit, name)
|
|
106
|
+
return nil if limit.nil?
|
|
107
|
+
unless limit.is_a?(Integer) && limit.positive?
|
|
108
|
+
raise ArgumentError, "#{name} must be a positive Integer or nil, got #{limit.inspect}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
limit
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Validate +profile+ against the PROFILES ladder. Unlike the caps
|
|
115
|
+
# there is no +nil+ form: the weakest posture is requested as an
|
|
116
|
+
# explicit +:permissive+, so anything off the ladder — +nil+
|
|
117
|
+
# included — is rejected.
|
|
118
|
+
def normalize_profile(profile)
|
|
119
|
+
return profile if PROFILES.include?(profile)
|
|
120
|
+
|
|
121
|
+
raise ArgumentError, "profile must be one of #{PROFILES.map(&:inspect).join(", ")}, got #{profile.inspect}"
|
|
122
|
+
end
|
|
65
123
|
end
|
|
66
124
|
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
data/lib/kobako.rb
CHANGED