kobako 0.13.0 → 0.15.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 +81 -0
- data/Cargo.lock +3 -3
- data/README.md +107 -32
- data/ROADMAP.md +25 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/error.rs +8 -0
- data/crates/kobako-runtime/src/snapshot.rs +7 -3
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/capture.rs +17 -13
- data/crates/kobako-wasmtime/src/driver.rs +7 -8
- data/crates/kobako-wasmtime/src/frames.rs +3 -10
- data/crates/kobako-wasmtime/src/guest_mem.rs +8 -1
- data/crates/kobako-wasmtime/src/instance_pre.rs +7 -6
- data/crates/kobako-wasmtime/src/invocation.rs +4 -5
- data/crates/kobako-wasmtime/src/trap.rs +65 -71
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/runtime/errors.rs +9 -39
- data/ext/kobako/src/runtime.rs +5 -20
- data/lib/kobako/catalog/handles.rb +3 -3
- data/lib/kobako/catalog/services.rb +123 -0
- data/lib/kobako/catalog/snippets.rb +2 -2
- data/lib/kobako/catalog.rb +2 -2
- data/lib/kobako/codec/decoder.rb +10 -10
- data/lib/kobako/codec/encoder.rb +5 -5
- data/lib/kobako/codec/error.rb +3 -3
- data/lib/kobako/codec/ext_types.rb +169 -0
- data/lib/kobako/codec/handle_walk.rb +17 -21
- data/lib/kobako/codec/state.rb +98 -0
- data/lib/kobako/codec/utils.rb +9 -9
- data/lib/kobako/codec.rb +23 -6
- data/lib/kobako/errors.rb +14 -39
- data/lib/kobako/fault.rb +1 -1
- data/lib/kobako/handle.rb +6 -3
- data/lib/kobako/outcome.rb +14 -7
- data/lib/kobako/pool.rb +1 -1
- data/lib/kobako/sandbox.rb +30 -25
- data/lib/kobako/transport/dispatcher.rb +41 -31
- data/lib/kobako/transport/request.rb +16 -11
- data/lib/kobako/transport/response.rb +11 -4
- data/lib/kobako/transport/run.rb +1 -1
- data/lib/kobako/transport/yield.rb +7 -4
- data/lib/kobako/transport/yielder.rb +20 -12
- data/lib/kobako/version.rb +1 -1
- data/release-please-config.json +37 -7
- data/sig/kobako/catalog/services.rbs +25 -0
- data/sig/kobako/codec/ext_types.rbs +31 -0
- data/sig/kobako/codec/handle_walk.rbs +2 -2
- data/sig/kobako/codec/state.rbs +20 -0
- data/sig/kobako/codec.rbs +14 -0
- data/sig/kobako/handle.rbs +0 -2
- data/sig/kobako/sandbox.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +8 -8
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +8 -0
- metadata +8 -7
- data/lib/kobako/catalog/namespaces.rb +0 -115
- data/lib/kobako/codec/factory.rb +0 -187
- data/lib/kobako/namespace.rb +0 -78
- data/sig/kobako/catalog/namespaces.rbs +0 -17
- data/sig/kobako/codec/factory.rbs +0 -34
- data/sig/kobako/namespace.rbs +0 -21
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -10,18 +10,18 @@ module Kobako
|
|
|
10
10
|
# - UTF-8 assertion at the codec boundary
|
|
11
11
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
12
12
|
# § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
|
|
13
|
-
#
|
|
13
|
+
# Decoder when walking +str+ family payloads and by ExtTypes
|
|
14
14
|
# when validating the +ext 0x00+ Symbol payload.
|
|
15
15
|
# - +ArgumentError+ translation at the codec boundary
|
|
16
|
-
# (
|
|
17
|
-
#
|
|
16
|
+
# (#with_boundary) so the public taxonomy stays
|
|
17
|
+
# Kobako::Codec::Error.
|
|
18
18
|
#
|
|
19
19
|
# Both helpers are pure — they only inspect inputs, never mutate them.
|
|
20
|
-
# The host↔guest Handle substitution walk lives in
|
|
20
|
+
# The host↔guest Handle substitution walk lives in HandleWalk.
|
|
21
21
|
module Utils
|
|
22
22
|
module_function
|
|
23
23
|
|
|
24
|
-
# Raise
|
|
24
|
+
# Raise InvalidEncoding unless +string+'s bytes are valid under
|
|
25
25
|
# its current encoding tag. +label+ is the caller-supplied prefix
|
|
26
26
|
# for the error message (e.g. +"str payload"+, +"Symbol payload"+).
|
|
27
27
|
def assert_utf8!(string, label)
|
|
@@ -32,13 +32,13 @@ module Kobako
|
|
|
32
32
|
|
|
33
33
|
# Run +block+ at the codec boundary: a value object raises
|
|
34
34
|
# +ArgumentError+ when an invariant is violated at construction, and
|
|
35
|
-
# this helper surfaces that as
|
|
36
|
-
# stays
|
|
35
|
+
# this helper surfaces that as InvalidType so the public taxonomy
|
|
36
|
+
# stays Kobako::Codec::Error and never leaks +ArgumentError+ from
|
|
37
37
|
# the Ruby standard library.
|
|
38
38
|
#
|
|
39
39
|
# Reach for this only where a value object is constructed outside a
|
|
40
|
-
#
|
|
41
|
-
# mapping (worked example:
|
|
40
|
+
# Decoder.decode block, whose rescue already performs the same
|
|
41
|
+
# mapping (worked example: ExtTypes#unpack_handle building
|
|
42
42
|
# +Handle.restore+ from a raw fixext payload). Do not use it for
|
|
43
43
|
# general-purpose validation outside the codec boundary —
|
|
44
44
|
# host-layer +ArgumentError+ values should propagate unchanged.
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require_relative "codec/error"
|
|
4
4
|
require_relative "codec/utils"
|
|
5
5
|
require_relative "codec/handle_walk"
|
|
6
|
-
require_relative "codec/
|
|
6
|
+
require_relative "codec/state"
|
|
7
|
+
require_relative "codec/ext_types"
|
|
7
8
|
require_relative "codec/encoder"
|
|
8
9
|
require_relative "codec/decoder"
|
|
9
10
|
|
|
@@ -18,14 +19,30 @@ module Kobako
|
|
|
18
19
|
# the kobako root so the codec can register them without depending
|
|
19
20
|
# upward on Transport.
|
|
20
21
|
#
|
|
21
|
-
# Backed by the official +msgpack+ gem
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# Backed by the official +msgpack+ gem: ExtTypes registers the three
|
|
23
|
+
# kobako-specific ext types (0x00 Symbol, 0x01 Capability Handle,
|
|
24
|
+
# 0x02 Exception envelope) on one process-wide +MessagePack::Factory+,
|
|
25
|
+
# and Encoder / Decoder are thin wrappers over it. The Rust side
|
|
25
26
|
# mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
|
|
26
|
-
# the ext-code constants live as module-private values on
|
|
27
|
+
# the ext-code constants live as module-private values on ExtTypes
|
|
27
28
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
28
29
|
# +codec::EXT_ERRENV+ on that side.
|
|
29
30
|
module Codec
|
|
31
|
+
# Bracket a decode and return the block's result together with whether
|
|
32
|
+
# the decoded tree carried an ext 0x01 Capability Handle — the signal a
|
|
33
|
+
# dispatch path uses to skip an all-identity Handle-resolution walk.
|
|
34
|
+
# The tracking state is codec-internal; this is its only readout.
|
|
35
|
+
def self.track_handles(&block)
|
|
36
|
+
State.current.track_handles(&block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bracket a codec operation in a payload position: an ext 0x02 Fault
|
|
40
|
+
# envelope is only legal in the Response fault field, so the envelope
|
|
41
|
+
# layers open this bracket around every other encode / decode and the
|
|
42
|
+
# ext-type conversions refuse the envelope while it is open — a wire
|
|
43
|
+
# violation on decode, no wire representation on encode.
|
|
44
|
+
def self.forbid_faults(&block)
|
|
45
|
+
State.current.forbid_faults(&block)
|
|
46
|
+
end
|
|
30
47
|
end
|
|
31
48
|
end
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -4,38 +4,13 @@
|
|
|
4
4
|
module Kobako
|
|
5
5
|
# Error taxonomy.
|
|
6
6
|
#
|
|
7
|
-
# Every
|
|
8
|
-
# exactly one of
|
|
9
|
-
# guest binary returns control to the
|
|
10
|
-
# the outcome-envelope tag.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# * {TrapError} — Wasm engine layer (trap, OOM, unreachable, or a
|
|
15
|
-
# wire-violation fallback signalling a corrupted
|
|
16
|
-
# guest runtime).
|
|
17
|
-
# * {SandboxError} — sandbox / wire layer (mruby script error,
|
|
18
|
-
# wire-decode failure on an otherwise valid tag,
|
|
19
|
-
# Catalog::Handles exhaustion, output buffer overrun).
|
|
20
|
-
# * {ServiceError} — service / capability layer (a Service capability
|
|
21
|
-
# call that failed and was not rescued inside the
|
|
22
|
-
# script).
|
|
23
|
-
#
|
|
24
|
-
# Two further branches sit outside the invocation taxonomy:
|
|
25
|
-
#
|
|
26
|
-
# * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
|
|
27
|
-
# when the wasm runtime cannot be built from the
|
|
28
|
-
# configured +wasm_path+ before any invocation runs.
|
|
29
|
-
# Not an invocation outcome, so it never passes
|
|
30
|
-
# through the two-step attribution decision.
|
|
31
|
-
# * {PoolTimeoutError} — pool checkout layer. Raised by `Kobako::Pool#with`
|
|
32
|
-
# when the checkout wait exceeds +checkout_timeout+.
|
|
33
|
-
#
|
|
34
|
-
# Named subclasses:
|
|
35
|
-
#
|
|
36
|
-
# * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
|
|
37
|
-
# at +wasm_path+.
|
|
38
|
-
# * {HandleExhaustedError} < {SandboxError} — Handle id cap hit.
|
|
7
|
+
# Every +Kobako::Sandbox+ invocation (+#eval+ or +#run+) either returns a
|
|
8
|
+
# value or raises exactly one of TrapError, SandboxError, or ServiceError.
|
|
9
|
+
# Attribution is decided after the guest binary returns control to the
|
|
10
|
+
# host: first the Wasm-trap layer, then the outcome-envelope tag.
|
|
11
|
+
# SetupError and PoolTimeoutError sit outside the invocation taxonomy and
|
|
12
|
+
# never pass through that attribution decision. Each class below
|
|
13
|
+
# documents its own layer.
|
|
39
14
|
|
|
40
15
|
# Base for all kobako-raised errors so callers that want to ignore the
|
|
41
16
|
# taxonomy can rescue a single class.
|
|
@@ -48,8 +23,8 @@ module Kobako
|
|
|
48
23
|
#
|
|
49
24
|
# Two named subclasses cover the configured per-invocation caps:
|
|
50
25
|
#
|
|
51
|
-
# *
|
|
52
|
-
# *
|
|
26
|
+
# * TimeoutError — wall-clock +timeout+ exceeded.
|
|
27
|
+
# * MemoryLimitError — guest +memory.grow+ would exceed
|
|
53
28
|
# +memory_limit+.
|
|
54
29
|
#
|
|
55
30
|
# Host Apps that only care about "guest is unrecoverable, discard the
|
|
@@ -128,14 +103,14 @@ module Kobako
|
|
|
128
103
|
class HandleExhaustedError < SandboxError; end
|
|
129
104
|
|
|
130
105
|
# BytecodeError is the SandboxError subclass raised when a
|
|
131
|
-
#
|
|
132
|
-
# first invocation's snippet replay against a fresh
|
|
106
|
+
# +#preload(binary:)+ snippet fails structural validation during the
|
|
107
|
+
# first invocation's snippet replay against a fresh +mrb_state+ (RITE
|
|
133
108
|
# version mismatch or corrupt body). Bytecode that loads cleanly and
|
|
134
|
-
# then raises at top level surfaces as plain
|
|
109
|
+
# then raises at top level surfaces as plain +SandboxError+ with the
|
|
135
110
|
# natural mruby class preserved. Inherits from SandboxError so a single
|
|
136
|
-
#
|
|
111
|
+
# +rescue Kobako::SandboxError+ covers both source and bytecode
|
|
137
112
|
# snippet failures while callers wanting bytecode-specific handling
|
|
138
|
-
# can
|
|
113
|
+
# can +rescue Kobako::BytecodeError+ directly.
|
|
139
114
|
class BytecodeError < SandboxError; end
|
|
140
115
|
|
|
141
116
|
# Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
|
data/lib/kobako/fault.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Kobako
|
|
|
5
5
|
#
|
|
6
6
|
# Top-level shared wire primitive: like +Kobako::Handle+ (ext 0x01),
|
|
7
7
|
# +Fault+ is a MessagePack ext-type leaf registered by
|
|
8
|
-
# +Kobako::Codec::
|
|
8
|
+
# +Kobako::Codec::ExtTypes+ and rides nested inside other envelopes (a
|
|
9
9
|
# +Kobako::Transport::Response+ error payload, or another Fault's
|
|
10
10
|
# +details+). It lives at the kobako root rather than under +Transport+
|
|
11
11
|
# because the Codec layer must register it, and Codec must not depend
|
data/lib/kobako/handle.rb
CHANGED
|
@@ -14,9 +14,11 @@ module Kobako
|
|
|
14
14
|
# The constructor is internal to the Host Gem. +Kobako::Handle.new+ is
|
|
15
15
|
# privatised so Host App code cannot fabricate a Handle from a bare
|
|
16
16
|
# integer; legitimate Handle instances enter Host App code only as
|
|
17
|
-
# fields on raised error objects.
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# fields on raised error objects. +#with+ — Data's copy-with-changes
|
|
18
|
+
# constructor — is removed for the same reason: a legitimate Handle
|
|
19
|
+
# must not derive a sibling with a caller-chosen id. The Host Gem itself constructs
|
|
20
|
+
# Handles through +.restore+, which exists at exactly two call
|
|
21
|
+
# sites: +Kobako::Codec::ExtTypes#unpack_handle+ (wire decode) and
|
|
20
22
|
# +Kobako::Codec::HandleWalk.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
|
|
21
23
|
# (allocator paths). Both live inside +lib/kobako/+ and are not part
|
|
22
24
|
# of any public surface.
|
|
@@ -41,6 +43,7 @@ module Kobako
|
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
private_class_method :new
|
|
46
|
+
undef_method :with
|
|
44
47
|
|
|
45
48
|
# Host Gem–internal factory. Allocates the Data instance through
|
|
46
49
|
# +Class#allocate+ and dispatches +#initialize+ explicitly so the
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "codec"
|
|
3
4
|
require_relative "outcome/panic"
|
|
4
5
|
require_relative "transport/error"
|
|
5
6
|
|
|
@@ -69,7 +70,9 @@ module Kobako
|
|
|
69
70
|
# "Symbol payload must be …" wording, but operators triaging a
|
|
70
71
|
# corrupted Sandbox runtime still need it.
|
|
71
72
|
def decode_value(body)
|
|
72
|
-
|
|
73
|
+
# The Result envelope is a payload position: an ext 0x02 Fault in it
|
|
74
|
+
# is a wire violation routed into the invalid-result rescue below.
|
|
75
|
+
Kobako::Codec.forbid_faults { Kobako::Codec::Decoder.decode(body) }
|
|
73
76
|
rescue Kobako::Codec::Error => e
|
|
74
77
|
raise build_transport_error(
|
|
75
78
|
"Sandbox produced an invalid result value",
|
|
@@ -98,13 +101,17 @@ module Kobako
|
|
|
98
101
|
# through the decoder boundary; the message itself is never user-
|
|
99
102
|
# facing — it lands in +details+ via the rescue chain above.
|
|
100
103
|
def build_panic_record(body)
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
# The Panic envelope is a payload position: an ext 0x02 Fault in its
|
|
105
|
+
# +details+ is a wire violation routed into the invalid-panic rescue.
|
|
106
|
+
Kobako::Codec.forbid_faults do
|
|
107
|
+
Kobako::Codec::Decoder.decode(body) do |map|
|
|
108
|
+
raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
Panic.new(
|
|
111
|
+
origin: map["origin"], klass: map["class"], message: map["message"],
|
|
112
|
+
backtrace: map["backtrace"] || [], details: map["details"]
|
|
113
|
+
)
|
|
114
|
+
end
|
|
108
115
|
end
|
|
109
116
|
end
|
|
110
117
|
|
data/lib/kobako/pool.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Kobako
|
|
|
24
24
|
# seconds (+nil+ waits indefinitely); every other keyword is
|
|
25
25
|
# forwarded verbatim to +Kobako::Sandbox.new+. The optional block
|
|
26
26
|
# runs exactly once per constructed Sandbox — it is the setup window
|
|
27
|
-
# for +#
|
|
27
|
+
# for +#bind+ / +#preload+ before that Sandbox's first checkout.
|
|
28
28
|
# No Sandbox is constructed here. Raises +ArgumentError+ for an
|
|
29
29
|
# invalid +slots+ / +checkout_timeout+.
|
|
30
30
|
def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Kobako
|
|
|
17
17
|
#
|
|
18
18
|
# The Sandbox owns the +Kobako::Runtime+, the per-Sandbox
|
|
19
19
|
# +Kobako::Catalog::Handles+, the per-instance
|
|
20
|
-
# +Kobako::Catalog::
|
|
20
|
+
# +Kobako::Catalog::Services+ (which receives the +Catalog::Handles+ by
|
|
21
21
|
# injection so guest→host dispatch and host→guest auto-wrap share one
|
|
22
22
|
# allocator), and the dispatch +Proc+ / +yield_to_guest+ lambda installed
|
|
23
23
|
# on the Runtime via +Runtime#on_dispatch=+. The underlying wasmtime Engine
|
|
@@ -100,27 +100,33 @@ module Kobako
|
|
|
100
100
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
101
101
|
@options = SandboxOptions.new(**)
|
|
102
102
|
@handler = Catalog::Handles.new
|
|
103
|
-
@services = Kobako::Catalog::
|
|
103
|
+
@services = Kobako::Catalog::Services.new(handler: @handler)
|
|
104
104
|
@snippets = Catalog::Snippets.new
|
|
105
105
|
@runtime = build_runtime!
|
|
106
106
|
install_dispatch_proc!
|
|
107
107
|
reset_invocation_state!
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# +
|
|
110
|
+
# Bind +object+ as the Service reachable at +path+ — a Symbol or
|
|
111
|
+
# String of one or more +::+-separated constant-form segments
|
|
112
|
+
# (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
|
|
113
|
+
# chaining.
|
|
113
114
|
#
|
|
114
|
-
# Raises +ArgumentError+ when
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
# Raises +ArgumentError+ when a segment is malformed, when +path+
|
|
116
|
+
# collides with an existing binding (a name is a bound Service or a
|
|
117
|
+
# grouping prefix, never both), or when called after the first
|
|
118
|
+
# invocation has sealed Service registration.
|
|
119
|
+
def bind(path, object)
|
|
120
|
+
@services.bind(path, object)
|
|
121
|
+
self
|
|
118
122
|
end
|
|
119
123
|
|
|
120
124
|
# Register a snippet on this Sandbox in one of two forms:
|
|
121
125
|
#
|
|
122
126
|
# * +preload(code: source, name: Name)+ — +source+ is mruby source
|
|
123
|
-
# as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+.
|
|
127
|
+
# as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. Compile
|
|
128
|
+
# failures surface as +Kobako::SandboxError+ on the first
|
|
129
|
+
# invocation's replay. The +name+
|
|
124
130
|
# becomes the snippet's +(snippet:Name)+ backtrace filename and
|
|
125
131
|
# is the dedupe key that rejects a duplicate +code:+ snippet.
|
|
126
132
|
# * +preload(binary: bytes)+ — +bytes+ is precompiled RITE
|
|
@@ -173,15 +179,15 @@ module Kobako
|
|
|
173
179
|
#
|
|
174
180
|
# Source delivery uses the WASI stdin three-frame protocol
|
|
175
181
|
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
|
|
176
|
-
# Frame 1 carries the msgpack-encoded preamble (
|
|
177
|
-
#
|
|
182
|
+
# Frame 1 carries the msgpack-encoded preamble (Service registry
|
|
183
|
+
# snapshot), Frame 2 carries the user source UTF-8 bytes, and
|
|
178
184
|
# Frame 3 carries the snippet table registered via +#preload+.
|
|
179
185
|
# Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
|
|
180
186
|
# mandatory-presence — an empty snippet table sends an empty msgpack
|
|
181
187
|
# array, never an absent frame.
|
|
182
188
|
#
|
|
183
189
|
# The first invocation seals the Service registry and snippet table;
|
|
184
|
-
# subsequent +#
|
|
190
|
+
# subsequent +#bind+ / +#preload+ calls raise +ArgumentError+.
|
|
185
191
|
#
|
|
186
192
|
# Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
|
|
187
193
|
# +Kobako::SandboxError+ when the guest ran to completion but failed
|
|
@@ -216,7 +222,7 @@ module Kobako
|
|
|
216
222
|
# runtime that cannot honor the request never runs guest code.
|
|
217
223
|
def build_runtime!
|
|
218
224
|
runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
219
|
-
@options.stdout_limit, @options.stderr_limit, profile)
|
|
225
|
+
@options.stdout_limit, @options.stderr_limit, @options.profile)
|
|
220
226
|
@options.enforce_floor!(runtime.profile)
|
|
221
227
|
runtime
|
|
222
228
|
end
|
|
@@ -257,10 +263,8 @@ module Kobako
|
|
|
257
263
|
# the readout here also covers the trap path, where +Runtime#eval+ /
|
|
258
264
|
# +#run+ raise instead of returning outcome bytes.
|
|
259
265
|
#
|
|
260
|
-
# The ext
|
|
261
|
-
#
|
|
262
|
-
# destructure-then-kwargs handoff below is the explicit
|
|
263
|
-
# positional→keyword conversion point.
|
|
266
|
+
# The ext-side contract is positional: +Runtime#usage+ yields
|
|
267
|
+
# +[wall_time, memory_peak]+ in +Kobako::Usage+ field order.
|
|
264
268
|
def read_usage!
|
|
265
269
|
wall_time, memory_peak = @runtime.usage
|
|
266
270
|
@usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
@@ -283,16 +287,14 @@ module Kobako
|
|
|
283
287
|
|
|
284
288
|
# Read the per-last-invocation output captures from the ext and wrap
|
|
285
289
|
# them as +Kobako::Capture+ value objects. Runs in the +invoke!+
|
|
286
|
-
# +ensure+ block next to
|
|
290
|
+
# +ensure+ block next to #read_usage! for the same reason: the ext
|
|
287
291
|
# stashes the captures on every outcome, so the readout also covers
|
|
288
292
|
# the trap path, where +Runtime#eval+ / +#run+ raise instead of
|
|
289
293
|
# returning outcome bytes — +#stdout+ / +#stderr+ keep the guest's
|
|
290
294
|
# partial output readable after a rescue.
|
|
291
295
|
#
|
|
292
|
-
# The ext
|
|
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
|
+
# The ext-side contract is positional: +Runtime#captures+ yields
|
|
297
|
+
# +[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]+.
|
|
296
298
|
def read_captures!
|
|
297
299
|
stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures
|
|
298
300
|
@stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
|
|
@@ -306,7 +308,7 @@ module Kobako
|
|
|
306
308
|
# The yielded block must return the invocation's raw outcome bytes —
|
|
307
309
|
# i.e. the value of +Runtime#eval+ / +#run+ — which the success path
|
|
308
310
|
# feeds to +Outcome.decode+. Captures and usage are populated by the
|
|
309
|
-
# +ensure+ readouts (
|
|
311
|
+
# +ensure+ readouts (#read_usage! / #read_captures!) on every
|
|
310
312
|
# outcome, so +#stdout+ / +#stderr+ / +#usage+ stay readable after a
|
|
311
313
|
# rescued trap.
|
|
312
314
|
# The rescue chain is the single trap-translation boundary —
|
|
@@ -320,7 +322,10 @@ module Kobako
|
|
|
320
322
|
# token; restore it to the host object the guest referenced before
|
|
321
323
|
# handing the value to the Host App. @handler still holds this
|
|
322
324
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
323
|
-
|
|
325
|
+
# A Handle-free result resolves to itself, so the restoration walk is
|
|
326
|
+
# skipped when the decode carried none.
|
|
327
|
+
value, carried_handle = Codec.track_handles { Outcome.decode(return_bytes) }
|
|
328
|
+
carried_handle ? Codec::HandleWalk.deep_restore(value, @handler) : value
|
|
324
329
|
rescue Kobako::TrapError => e
|
|
325
330
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
326
331
|
ensure
|
|
@@ -13,7 +13,7 @@ module Kobako
|
|
|
13
13
|
module Transport
|
|
14
14
|
# Pure-function dispatcher for guest-initiated transport calls.
|
|
15
15
|
# Decodes a msgpack-encoded Request envelope, resolves the target
|
|
16
|
-
# object through the Catalog::
|
|
16
|
+
# object through the Catalog::Services (path lookup) or
|
|
17
17
|
# Catalog::Handles (Handle lookup), invokes the method, and returns
|
|
18
18
|
# a msgpack-encoded Response envelope.
|
|
19
19
|
#
|
|
@@ -24,10 +24,10 @@ module Kobako
|
|
|
24
24
|
#
|
|
25
25
|
# Entry point:
|
|
26
26
|
#
|
|
27
|
-
# Kobako::Transport::Dispatcher.dispatch(request_bytes,
|
|
27
|
+
# Kobako::Transport::Dispatcher.dispatch(request_bytes, services, handler, yield_to_guest)
|
|
28
28
|
# # => msgpack-encoded Response bytes (never raises)
|
|
29
29
|
module Dispatcher
|
|
30
|
-
# Throw tag for the
|
|
30
|
+
# Throw tag for the Yielder's break unwind back to the
|
|
31
31
|
# dispatcher's +catch+ frame. +private_constant+ is a
|
|
32
32
|
# convention boundary — not a defence.
|
|
33
33
|
BREAK_THROW = :__kobako_break__
|
|
@@ -54,12 +54,12 @@ module Kobako
|
|
|
54
54
|
# Callable gadget types whose own public methods are reflection surface
|
|
55
55
|
# (+Proc#binding+ reaches +Binding#eval+, +Method#receiver+ / +#unbind+
|
|
56
56
|
# hand back the underlying object) rather than Service behaviour. Only
|
|
57
|
-
#
|
|
57
|
+
# CALLABLE_ALLOW is reachable on a target of these types; a bound
|
|
58
58
|
# lambda stays invocable, its reflective surface does not.
|
|
59
59
|
GADGET_OWNERS = [Proc, Method, UnboundMethod, Binding].freeze
|
|
60
60
|
private_constant :GADGET_OWNERS
|
|
61
61
|
|
|
62
|
-
# The sole methods reachable on a
|
|
62
|
+
# The sole methods reachable on a GADGET_OWNERS target: invoking it
|
|
63
63
|
# (+call+ / +[]+ / +yield+) and the harmless +arity+ / +lambda?+
|
|
64
64
|
# describers that aid guest-side debugging.
|
|
65
65
|
CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
|
|
@@ -67,7 +67,7 @@ module Kobako
|
|
|
67
67
|
|
|
68
68
|
# Dispatch a single transport request and return the encoded
|
|
69
69
|
# Response bytes. Invoked from the +Runtime#on_dispatch+ Proc that
|
|
70
|
-
# +Kobako::Sandbox#initialize+ installs on the ext side; +
|
|
70
|
+
# +Kobako::Sandbox#initialize+ installs on the ext side; +services+,
|
|
71
71
|
# +handler+, and +yield_to_guest+ are captured in that Proc's
|
|
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+
|
|
@@ -77,31 +77,38 @@ module Kobako
|
|
|
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.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
#
|
|
81
|
+
# The decode runs inside +Codec.track_handles+ so #resolve_call_args
|
|
82
|
+
# can skip the argument walk when no Capability Handle crossed the
|
|
83
|
+
# wire.
|
|
84
|
+
def dispatch(request_bytes, services, handler, yield_to_guest)
|
|
85
|
+
request, carried_handle = Kobako::Codec.track_handles { Kobako::Transport::Request.decode(request_bytes) }
|
|
86
|
+
target = resolve_target(request.target, services, handler)
|
|
87
|
+
args, kwargs = resolve_call_args(request, handler, carried_handle)
|
|
84
88
|
yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if request.block_given
|
|
85
|
-
|
|
86
|
-
encode_ok(value, handler)
|
|
89
|
+
encode_ok(catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }, handler)
|
|
87
90
|
rescue StandardError => e
|
|
88
91
|
encode_caught_error(e)
|
|
89
92
|
ensure
|
|
90
93
|
yielder&.invalidate!
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
# Resolve positional and keyword arguments off +request+ in one
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
|
|
96
|
+
# Resolve positional and keyword arguments off +request+ in one step.
|
|
97
|
+
# +carried_handle+ reports whether the decode carried any Capability
|
|
98
|
+
# Handle; when it did not, every argument resolves to itself, so the
|
|
99
|
+
# decoded values pass straight through and the walk is skipped entirely.
|
|
100
|
+
# Otherwise both go through #resolve_arg so Handles round-trip back to
|
|
101
|
+
# the host-side Ruby object before the call reaches +public_send+.
|
|
102
|
+
def resolve_call_args(request, handler, carried_handle)
|
|
103
|
+
return [request.args, request.kwargs] unless carried_handle
|
|
104
|
+
|
|
98
105
|
[request.args.map { |v| resolve_arg(v, handler) },
|
|
99
106
|
request.kwargs.transform_values { |v| resolve_arg(v, handler) }]
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
# Map an error caught at the dispatch boundary to a +Response.error+
|
|
103
110
|
# envelope (binary msgpack). +error+ is the +StandardError+ caught by
|
|
104
|
-
#
|
|
111
|
+
# #dispatch's rescue; the +type+ field tells the guest which kind
|
|
105
112
|
# of failure it was so it can raise the matching proxy-side error.
|
|
106
113
|
def encode_caught_error(error)
|
|
107
114
|
case error
|
|
@@ -119,8 +126,8 @@ module Kobako
|
|
|
119
126
|
# reject calls to no-kwarg methods when the wire carries the
|
|
120
127
|
# uniform empty-map shape.
|
|
121
128
|
#
|
|
122
|
-
# +yielder+ is the host-side
|
|
123
|
-
# call site supplied a block; its
|
|
129
|
+
# +yielder+ is the host-side Yielder materialised when the guest
|
|
130
|
+
# call site supplied a block; its Yielder#to_proc
|
|
124
131
|
# rides the +&block+ slot. +&nil+ is a no-op block argument in Ruby,
|
|
125
132
|
# so the same call site handles both cases without an explicit
|
|
126
133
|
# conditional.
|
|
@@ -137,8 +144,8 @@ module Kobako
|
|
|
137
144
|
end
|
|
138
145
|
|
|
139
146
|
# Guard the +public_send+ below against ambient reflection methods.
|
|
140
|
-
# A public method whose owner is a
|
|
141
|
-
# rejected, except
|
|
147
|
+
# A public method whose owner is a META_OWNERS or GADGET_OWNERS module is
|
|
148
|
+
# rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
|
|
142
149
|
# stays invocable). A name with no concrete public method is allowed
|
|
143
150
|
# only when the target opts into it via +respond_to?+ (dynamic
|
|
144
151
|
# +method_missing+ Services), since the dangerous methods are all
|
|
@@ -159,7 +166,7 @@ module Kobako
|
|
|
159
166
|
# Consult the target's opt-in narrowing predicate. A bound object
|
|
160
167
|
# may define a private +respond_to_guest?(name)+ to restrict which of its
|
|
161
168
|
# methods the guest reaches; a falsy answer rejects the dispatch.
|
|
162
|
-
# The predicate composes beneath
|
|
169
|
+
# The predicate composes beneath #reject_meta_method! — it only narrows,
|
|
163
170
|
# never re-opening the reflection surface the floor rejects — and is
|
|
164
171
|
# consulted with the private surface included so the guest's +public_send+
|
|
165
172
|
# dispatch can never reach +respond_to_guest?+ itself.
|
|
@@ -187,17 +194,17 @@ module Kobako
|
|
|
187
194
|
# Target type is already validated by +Transport::Request.decode+
|
|
188
195
|
# before this method is reached, so no else-branch is needed here —
|
|
189
196
|
# the wire layer is the system boundary that enforces the invariant.
|
|
190
|
-
def resolve_target(target,
|
|
197
|
+
def resolve_target(target, services, handler)
|
|
191
198
|
case target
|
|
192
199
|
when String
|
|
193
|
-
resolve_path(target,
|
|
200
|
+
resolve_path(target, services)
|
|
194
201
|
when Kobako::Handle
|
|
195
202
|
require_live_object!(target.id, handler)
|
|
196
203
|
end
|
|
197
204
|
end
|
|
198
205
|
|
|
199
|
-
def resolve_path(path,
|
|
200
|
-
|
|
206
|
+
def resolve_path(path, services)
|
|
207
|
+
services.lookup(path)
|
|
201
208
|
rescue KeyError => e
|
|
202
209
|
raise UndefinedTargetError, e.message
|
|
203
210
|
end
|
|
@@ -213,7 +220,7 @@ module Kobako
|
|
|
213
220
|
# Encode +value+ as a +Response.ok+ envelope. When the value is not
|
|
214
221
|
# wire-representable per the codec's type mapping, the
|
|
215
222
|
# +UnsupportedType+ rescue routes it through the
|
|
216
|
-
# Catalog::Handles via
|
|
223
|
+
# Catalog::Handles via #wrap_as_handle and re-encodes with the Capability
|
|
217
224
|
# Handle in place. The happy path encodes exactly once.
|
|
218
225
|
def encode_ok(value, handler)
|
|
219
226
|
response = Kobako::Transport::Response.ok(value)
|
|
@@ -224,15 +231,18 @@ module Kobako
|
|
|
224
231
|
|
|
225
232
|
# Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
|
|
226
233
|
# that the wire codec can carry. Used as the fallback path of
|
|
227
|
-
#
|
|
234
|
+
# #encode_ok when +value+ has no wire representation.
|
|
228
235
|
def wrap_as_handle(value, handler)
|
|
229
236
|
handler.alloc(value)
|
|
230
237
|
end
|
|
231
238
|
|
|
239
|
+
# +message+ folds to UTF-8 first: Ruby core builds some exception
|
|
240
|
+
# messages as ASCII-8BIT (the arity ArgumentError, for one), and
|
|
241
|
+
# the codec would ride those as bin — a shape the guest refuses.
|
|
232
242
|
def encode_error(type, message)
|
|
243
|
+
message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
233
244
|
fault = Kobako::Fault.new(type: type, message: message)
|
|
234
|
-
|
|
235
|
-
response.encode
|
|
245
|
+
Kobako::Transport::Response.error(fault).encode
|
|
236
246
|
end
|
|
237
247
|
end
|
|
238
248
|
end
|
|
@@ -12,8 +12,8 @@ module Kobako
|
|
|
12
12
|
#
|
|
13
13
|
# 5-element msgpack array:
|
|
14
14
|
# +[target, method_name, args, kwargs, block_given]+. +target+ is
|
|
15
|
-
# either a +String+ (+"
|
|
16
|
-
# or a
|
|
15
|
+
# either a +String+ (+"MyService::KV"+, e.g. +"MyService::KV"+)
|
|
16
|
+
# or a Handle. SPEC pins +kwargs+ map keys to ext 0x00 Symbol;
|
|
17
17
|
# enforced at construction so the Value Object is the single source of
|
|
18
18
|
# truth. +block_given+ is a Boolean signalling whether the guest call
|
|
19
19
|
# site supplied a block; the block body itself never crosses the
|
|
@@ -43,17 +43,22 @@ module Kobako
|
|
|
43
43
|
Codec::Encoder.encode([target, method_name, args, kwargs, block_given])
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# Decode +bytes+ into a
|
|
47
|
-
# envelope is not the expected 5-element msgpack array,
|
|
48
|
-
#
|
|
46
|
+
# Decode +bytes+ into a Request. Raises +Codec::InvalidType+ when the
|
|
47
|
+
# envelope is not the expected 5-element msgpack array, when any
|
|
48
|
+
# position carries an ext 0x02 Fault envelope (a Request is a payload
|
|
49
|
+
# position; the Response fault field is the envelope's only home), or
|
|
50
|
+
# when the Value Object's construction invariants reject the decoded
|
|
51
|
+
# fields.
|
|
49
52
|
def self.decode(bytes)
|
|
50
|
-
Codec
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
Codec.forbid_faults do
|
|
54
|
+
Codec::Decoder.decode(bytes) do |arr|
|
|
55
|
+
unless arr.is_a?(Array) && arr.length == 5
|
|
56
|
+
raise Codec::InvalidType, "Request envelope is malformed (expected a 5-element array)"
|
|
57
|
+
end
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
target, method_name, args, kwargs, block_given = arr
|
|
60
|
+
new(target: target, method_name: method_name, args: args, kwargs: kwargs, block_given: block_given)
|
|
61
|
+
end
|
|
57
62
|
end
|
|
58
63
|
end
|
|
59
64
|
|
|
@@ -20,7 +20,7 @@ module Kobako
|
|
|
20
20
|
#
|
|
21
21
|
# 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
|
|
22
22
|
# (success) or 1 (fault). For success the second element is the return
|
|
23
|
-
# value; for fault it is a
|
|
23
|
+
# value; for fault it is a Fault (ext 0x02 envelope).
|
|
24
24
|
#
|
|
25
25
|
# Built on the +class X < Data.define(...)+ subclass form so the
|
|
26
26
|
# class body is fully Steep-visible; see +lib/kobako/outcome/panic.rb+
|
|
@@ -53,12 +53,19 @@ module Kobako
|
|
|
53
53
|
def error? = status == STATUS_ERROR
|
|
54
54
|
|
|
55
55
|
# Encode this Response to msgpack bytes as the 2-element
|
|
56
|
-
# +[status, payload]+ array.
|
|
56
|
+
# +[status, payload]+ array. The ok variant's value is a payload
|
|
57
|
+
# position, so a +Kobako::Fault+ inside it has no wire
|
|
58
|
+
# representation and surfaces as +UnsupportedType+ — the
|
|
59
|
+
# Dispatcher's auto-wrap rescue turns it into a Capability Handle.
|
|
60
|
+
# The error variant carries the Fault envelope in its one legal
|
|
61
|
+
# position and encodes it plainly.
|
|
57
62
|
def encode
|
|
58
|
-
Codec::Encoder.encode([status, payload])
|
|
63
|
+
return Codec::Encoder.encode([status, payload]) if error?
|
|
64
|
+
|
|
65
|
+
Codec.forbid_faults { Codec::Encoder.encode([status, payload]) }
|
|
59
66
|
end
|
|
60
67
|
|
|
61
|
-
# Decode +bytes+ into a
|
|
68
|
+
# Decode +bytes+ into a Response. Raises +Codec::InvalidType+ when the
|
|
62
69
|
# envelope is not the expected 2-element msgpack array, or when the
|
|
63
70
|
# Value Object's construction invariants reject the decoded fields.
|
|
64
71
|
def self.decode(bytes)
|