kobako 0.26.0 → 0.28.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 +103 -0
- data/Cargo.lock +117 -140
- data/README.md +156 -118
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +38 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +10 -12
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/lib.rs +1 -1
- data/crates/kobako-runtime/src/profile.rs +1 -1
- data/crates/kobako-runtime/src/runtime.rs +31 -17
- data/crates/kobako-runtime/src/snapshot.rs +13 -10
- data/crates/kobako-runtime/src/yielder.rs +9 -6
- data/crates/kobako-transport/CHANGELOG.md +35 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +43 -27
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +77 -53
- data/crates/kobako-transport/src/envelope/request.rs +295 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
- data/crates/kobako-wasmtime/Cargo.toml +7 -5
- data/crates/kobako-wasmtime/README.md +8 -10
- data/crates/kobako-wasmtime/src/abi.rs +5 -8
- data/crates/kobako-wasmtime/src/ambient.rs +2 -2
- data/crates/kobako-wasmtime/src/cache.rs +25 -59
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +11 -14
- data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
- data/crates/kobako-wasmtime/src/driver.rs +133 -86
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
- data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
- data/crates/kobako-wasmtime/src/invocation.rs +55 -372
- data/crates/kobako-wasmtime/src/io.rs +286 -0
- data/crates/kobako-wasmtime/src/lib.rs +3 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/trap.rs +32 -45
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/runtime/bridge.rs +38 -47
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/gvl.rs +4 -12
- data/ext/kobako/src/runtime.rs +169 -212
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +41 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/services.rb +7 -61
- data/lib/kobako/catalog/snippets.rb +42 -66
- data/lib/kobako/catalog.rb +2 -10
- data/lib/kobako/codec/decoder.rb +9 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +4 -10
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec.rb +3 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +51 -112
- data/lib/kobako/errors.rb +51 -127
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +23 -53
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +96 -133
- data/lib/kobako/sandbox_options.rb +38 -84
- data/lib/kobako/snippet/binary.rb +1 -16
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +1 -9
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +112 -195
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +5 -10
- data/lib/kobako/transport/reflection.rb +39 -50
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +21 -73
- data/lib/kobako/transport.rb +3 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +17 -0
- data/sig/kobako/catalog/services.rbs +0 -6
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec.rbs +2 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +2 -3
- data/sig/kobako/errors.rbs +6 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/runtime.rbs +12 -8
- data/sig/kobako/sandbox.rbs +4 -0
- data/sig/kobako/sandbox_options.rbs +2 -6
- data/sig/kobako/transport/dispatcher.rbs +14 -18
- data/sig/kobako/transport/reflection.rbs +5 -3
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +7 -6
- metadata +16 -4
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/rust-toolchain.toml +0 -11
data/lib/kobako/codec.rb
CHANGED
|
@@ -10,24 +10,7 @@ require_relative "codec/encoder"
|
|
|
10
10
|
require_relative "codec/decoder"
|
|
11
11
|
|
|
12
12
|
module Kobako
|
|
13
|
-
|
|
14
|
-
# byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
|
|
15
|
-
# This is the payload codec: the core envelope is framed natively, so
|
|
16
|
-
# what reaches here is one codec document — an invocation's arguments
|
|
17
|
-
# (+Kobako::Payload::Arguments+), a dispatch or yield value, or an
|
|
18
|
-
# invocation's value and a Panic's diagnostics
|
|
19
|
-
# (+Kobako::Outcome+). The ext-type leaves this layer
|
|
20
|
-
# carries — +Kobako::Handle+ (0x01) — lives at the kobako root so the
|
|
21
|
-
# codec can register it without depending upward on Transport.
|
|
22
|
-
#
|
|
23
|
-
# Backed by the official +msgpack+ gem: ExtTypes registers the two
|
|
24
|
-
# kobako-specific ext types (0x00 Symbol, 0x01 Capability Handle) on
|
|
25
|
-
# one process-wide +MessagePack::Factory+,
|
|
26
|
-
# and Encoder / Decoder are thin wrappers over it. The Rust side
|
|
27
|
-
# mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
|
|
28
|
-
# the ext-code constants live as module-private values on ExtTypes
|
|
29
|
-
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ on that side.
|
|
30
|
-
module Codec
|
|
13
|
+
module Codec # :nodoc:
|
|
31
14
|
# The maximum structural nesting depth the wire represents (the
|
|
32
15
|
# MessagePack ecosystem's bound), shared with the guest +kobako_codec+
|
|
33
16
|
# so both sides cap identically. The host refuses a value nesting past
|
|
@@ -35,10 +18,8 @@ module Kobako
|
|
|
35
18
|
# packer.
|
|
36
19
|
MAX_NESTING_DEPTH = 128
|
|
37
20
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# dispatch path uses to skip an all-identity Handle-resolution walk.
|
|
41
|
-
# The tracking state is codec-internal; this is its only readout.
|
|
21
|
+
# Answers the block's result with whether the decode carried a
|
|
22
|
+
# Capability Handle, so a caller can skip an all-identity restore walk.
|
|
42
23
|
def self.track_handles(&block)
|
|
43
24
|
State.current.track_handles(&block)
|
|
44
25
|
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# A bind-path segment, a snippet name and a #run entrypoint all take the
|
|
5
|
+
# constant-name form, so they are refused alike. The guest interns each
|
|
6
|
+
# one, and mruby holds a symbol of at most 65534 bytes.
|
|
7
|
+
CONSTANT_NAME = /\A[A-Z]\w{0,65533}\z/ # :nodoc:
|
|
8
|
+
end
|
data/lib/kobako/context.rb
CHANGED
|
@@ -11,41 +11,30 @@ require_relative "transport"
|
|
|
11
11
|
require_relative "catalog"
|
|
12
12
|
|
|
13
13
|
module Kobako
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# none and overlapping evals never touch each other's state.
|
|
14
|
+
# One invocation, as the block given to Sandbox#eval or Sandbox#run sees it.
|
|
15
|
+
# The block runs before the guest does, so it can choose the objects this
|
|
16
|
+
# invocation alone reaches:
|
|
17
|
+
#
|
|
18
|
+
# sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
|
|
20
19
|
class Context
|
|
21
|
-
|
|
22
|
-
# sealed +Catalog::Services+ / +Catalog::Snippets+ registries, and the
|
|
23
|
-
# +Catalog::Extensions+ whose callable backends this Context resolves for
|
|
24
|
-
# its own run. The +Catalog::Handles+ table is this invocation's own, so
|
|
25
|
-
# guest→host dispatch and host→guest auto-wrap share one allocator scoped
|
|
26
|
-
# to the run; the resolved provider map is likewise per-invocation, so
|
|
27
|
-
# concurrent invocations never share mutable state.
|
|
28
|
-
def initialize(runtime:, services:, snippets:, extensions:)
|
|
20
|
+
def initialize(runtime:, services:, snippets:, flags:, extensions:) # :nodoc:
|
|
29
21
|
@runtime = runtime
|
|
30
22
|
@services = services
|
|
31
23
|
@snippets = snippets
|
|
24
|
+
@flags = flags
|
|
32
25
|
@extensions = extensions
|
|
33
26
|
@resolved = {} # : Hash[String, Transport::Exposure]
|
|
34
27
|
@overrides = {} # : Hash[String, Transport::Exposure]
|
|
35
|
-
@
|
|
36
|
-
@handler = Catalog::Handles.new
|
|
28
|
+
@handles = Catalog::Handles.new
|
|
37
29
|
@stdout_capture = @stderr_capture = Capture::EMPTY
|
|
38
30
|
@usage = Usage::EMPTY
|
|
39
31
|
end
|
|
40
32
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# while the block runs — the Context is spent once the block returns, so a
|
|
47
|
-
# captured +ctx+ used afterward raises +ArgumentError+ too, putting both
|
|
48
|
-
# misuses on one rescuable channel. Returns +self+.
|
|
33
|
+
# Use +object+ for the Service at +path+ during this invocation only,
|
|
34
|
+
# filling a fillable Service or shadowing a bound one. Returns +self+.
|
|
35
|
+
#
|
|
36
|
+
# Raises ArgumentError when +path+ was never bound on the Sandbox, or when
|
|
37
|
+
# called after the block has returned.
|
|
49
38
|
def bind(path, object)
|
|
50
39
|
raise ArgumentError, "Kobako::Context is spent; ctx.bind is only valid inside the #eval / #run block" if @spent
|
|
51
40
|
|
|
@@ -56,16 +45,9 @@ module Kobako
|
|
|
56
45
|
self
|
|
57
46
|
end
|
|
58
47
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
# An unbound path raises +KeyError+; a fillable left unfilled resolves to
|
|
63
|
-
# +Kobako::Unresolved+ and is reported the same way, so an unresolved
|
|
64
|
-
# capability fails closed as an undefined target rather than dispatching to
|
|
65
|
-
# the sentinel. The Dispatcher maps either +KeyError+ to an
|
|
66
|
-
# undefined-target wire fault. Internal — the per-invocation dispatch
|
|
67
|
-
# handler is the sole caller.
|
|
68
|
-
def lookup(path)
|
|
48
|
+
# A fillable left unfilled raises like an unbound path, so the guest's
|
|
49
|
+
# call fails closed instead of reaching the sentinel.
|
|
50
|
+
def lookup(path) # :nodoc:
|
|
69
51
|
key = path.to_s
|
|
70
52
|
exposure = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } }
|
|
71
53
|
raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(exposure.object)
|
|
@@ -73,136 +55,93 @@ module Kobako
|
|
|
73
55
|
exposure
|
|
74
56
|
end
|
|
75
57
|
|
|
76
|
-
|
|
77
|
-
# decoded last expression. A given +block+ runs first, receiving this
|
|
78
|
-
# Context to collect +ctx.bind+ overrides before the guest drives.
|
|
79
|
-
def eval(code, &block)
|
|
58
|
+
def eval(code, &block) # :nodoc:
|
|
80
59
|
collect_overrides(&block) if block
|
|
81
60
|
invoke!(:eval) do
|
|
82
|
-
@runtime.eval(dispatch_handler, @services.paths,
|
|
61
|
+
@runtime.eval(dispatch_handler, @services.paths, @snippets.entries, @flags.entries, code.b)
|
|
83
62
|
end
|
|
84
63
|
end
|
|
85
64
|
|
|
86
|
-
|
|
87
|
-
# decoded result. A given +block+ runs first, receiving this Context to
|
|
88
|
-
# collect +ctx.bind+ overrides before the guest drives.
|
|
89
|
-
def run(request, &block)
|
|
65
|
+
def run(request, &block) # :nodoc:
|
|
90
66
|
collect_overrides(&block) if block
|
|
91
67
|
invoke!(:run, entrypoint: request.entrypoint) do
|
|
92
|
-
@runtime.run(dispatch_handler, @services.paths, @snippets.entries,
|
|
93
|
-
request.entrypoint.to_s, request.payload(@
|
|
68
|
+
@runtime.run(dispatch_handler, @services.paths, @snippets.entries, @flags.entries,
|
|
69
|
+
request.entrypoint.to_s, request.payload(@handles))
|
|
94
70
|
end
|
|
95
71
|
end
|
|
96
72
|
|
|
97
73
|
private
|
|
98
74
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
# the guest never runs and no Execution is produced.
|
|
75
|
+
# The Context is spent once the block returns, so a captured +ctx+ used
|
|
76
|
+
# later raises. A block that raises propagates before the guest drives,
|
|
77
|
+
# so the guest never runs and no Execution is produced.
|
|
103
78
|
def collect_overrides
|
|
104
79
|
yield self
|
|
105
80
|
ensure
|
|
106
81
|
@spent = true
|
|
107
82
|
end
|
|
108
83
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
# capturing this Context as the path resolver (its +#lookup+ layers the
|
|
112
|
-
# per-invocation providers over the static bindings) plus +@handler+. Handed to
|
|
113
|
-
# +Runtime#eval+ / +#run+ as a call argument, so the Runtime holds no
|
|
114
|
-
# dispatch state and the +Proc+ stays GC-rooted as a live argument for the
|
|
115
|
-
# synchronous call. The ext hands the +Proc+ a per-dispatch +guest_yielder+
|
|
116
|
-
# — a +String → String+ callable that re-enters the in-flight guest to run
|
|
117
|
-
# a yielded block — which the +Dispatcher+ forwards to the
|
|
118
|
-
# +Transport::Yielder+ it builds for the call.
|
|
84
|
+
# Handed to the Runtime as a call argument, so the Runtime holds no
|
|
85
|
+
# dispatch state and the Proc stays GC-rooted for the synchronous call.
|
|
119
86
|
def dispatch_handler
|
|
120
87
|
lambda do |target, method_name, block_given, payload, guest_yielder|
|
|
121
88
|
call = Transport::Call.new(target: target, method_name: method_name,
|
|
122
89
|
block_given: block_given, payload: payload)
|
|
123
|
-
Transport::Dispatcher.dispatch(call, self, @
|
|
90
|
+
Transport::Dispatcher.dispatch(call, self, @handles, guest_yielder)
|
|
124
91
|
end
|
|
125
92
|
end
|
|
126
93
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
# +#usage+ / +#stdout+ / +#stderr+ stay readable after a rescued trap.
|
|
94
|
+
# Every Snapshot carries usage and captures, trap or not, so they stay
|
|
95
|
+
# readable after a rescued trap.
|
|
130
96
|
def populate_observability!(snapshot)
|
|
131
97
|
@usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak)
|
|
132
98
|
@stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?)
|
|
133
99
|
@stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?)
|
|
134
100
|
end
|
|
135
101
|
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def trap_class_for(err)
|
|
141
|
-
case err
|
|
142
|
-
when TimeoutError then TimeoutError
|
|
143
|
-
when MemoryLimitError then MemoryLimitError
|
|
144
|
-
else TrapError
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Build the +TrapError+-family exception for a trapped +Snapshot+ from its
|
|
149
|
-
# neutral trap kind, tagged with the verb — the cap subclasses
|
|
150
|
-
# (+TimeoutError+ / +MemoryLimitError+) keep their identity, every other
|
|
151
|
-
# engine fault is the base +TrapError+.
|
|
152
|
-
def trap_error_for(snapshot, verb)
|
|
153
|
-
klass = case snapshot.trap_kind
|
|
154
|
-
when :timeout then TimeoutError
|
|
155
|
-
when :memory_limit then MemoryLimitError
|
|
156
|
-
else TrapError
|
|
157
|
-
end
|
|
158
|
-
klass.new("Sandbox##{verb} failed: #{snapshot.trap_message}")
|
|
102
|
+
# Every TrapError leaving an invocation names the verb that ran it; the
|
|
103
|
+
# cap subclasses keep their identity through the rebuild.
|
|
104
|
+
def with_verb(err, verb)
|
|
105
|
+
err.class.new("Sandbox##{verb} failed: #{err.message}")
|
|
159
106
|
end
|
|
160
107
|
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
# +failed+ records the two apart so a +nil+ +value+ from a successful run
|
|
164
|
-
# stays distinct from a failed one.
|
|
108
|
+
# +failed+ keeps a +nil+ value from a successful run apart from a failed
|
|
109
|
+
# run.
|
|
165
110
|
def build_execution(value, failed:)
|
|
166
111
|
Execution.new(value: value, usage: @usage, stdout: @stdout_capture, stderr: @stderr_capture, failed: failed)
|
|
167
112
|
end
|
|
168
113
|
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
# name +#run+ asked for, which the host knows and the wire never carries,
|
|
174
|
-
# so an unresolved one names itself on the error it raises.
|
|
114
|
+
# The settle sits in the rescue so a wire-violation trap or a Panic both
|
|
115
|
+
# attach this run's Execution, just like a guest-call trap does.
|
|
116
|
+
# +entrypoint+ is known to the host and never carried by the wire, so an
|
|
117
|
+
# unresolved one names itself on the error it raises.
|
|
175
118
|
def settle_outcome(snapshot, verb, entrypoint)
|
|
176
|
-
|
|
177
|
-
value, carried = Codec.track_handles { Outcome.reify(
|
|
178
|
-
value = Codec::HandleWalk.deep_restore(value, @
|
|
119
|
+
payload, panic = snapshot.outcome
|
|
120
|
+
value, carried = Codec.track_handles { Outcome.reify(payload, panic, entrypoint: entrypoint) }
|
|
121
|
+
value = Codec::HandleWalk.deep_restore(value, @handles) if carried
|
|
179
122
|
build_execution(value, failed: false)
|
|
180
123
|
rescue Kobako::TrapError => e
|
|
181
|
-
raise
|
|
182
|
-
failed: true))
|
|
124
|
+
raise with_verb(e, verb).with_execution(build_execution(nil, failed: true))
|
|
183
125
|
rescue Kobako::SandboxError, Kobako::ServiceError => e
|
|
184
126
|
raise e.with_execution(build_execution(nil, failed: true))
|
|
185
127
|
end
|
|
186
128
|
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# the guest unrun. Usage and captures are recorded before the trap check,
|
|
192
|
-
# so a trapped Snapshot's error carries them just like a completed run's
|
|
193
|
-
# return value. A could-not-start fault ran no invocation at all, so it
|
|
194
|
-
# carries no Execution and gains only the verb prefix.
|
|
129
|
+
# Extension backends are resolved before the guest runs, so a provider
|
|
130
|
+
# that raises propagates unwrapped and leaves the guest unrun. A
|
|
131
|
+
# could-not-start fault ran no invocation at all, so it carries no
|
|
132
|
+
# Execution and gains only the verb prefix.
|
|
195
133
|
def invoke!(verb, entrypoint: nil)
|
|
196
134
|
@resolved = @extensions.resolve.transform_values { |object| Transport::Exposure.of(object) }
|
|
197
135
|
begin
|
|
198
136
|
snapshot = yield
|
|
199
137
|
rescue Kobako::TrapError => e
|
|
200
|
-
raise
|
|
138
|
+
raise with_verb(e, verb)
|
|
201
139
|
end
|
|
202
140
|
populate_observability!(snapshot)
|
|
203
|
-
|
|
141
|
+
trap = snapshot.trap_error
|
|
142
|
+
return settle_outcome(snapshot, verb, entrypoint) unless trap
|
|
204
143
|
|
|
205
|
-
raise
|
|
144
|
+
raise with_verb(trap, verb).with_execution(build_execution(nil, failed: true))
|
|
206
145
|
end
|
|
207
146
|
end
|
|
208
147
|
end
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -1,103 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Top-level Kobako namespace.
|
|
4
3
|
module Kobako
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
#
|
|
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.
|
|
14
|
-
|
|
15
|
-
# Base for all kobako-raised errors so callers that want to ignore the
|
|
16
|
-
# taxonomy can rescue a single class.
|
|
4
|
+
# Every Sandbox#eval or Sandbox#run returns, or raises exactly one of
|
|
5
|
+
# TrapError, SandboxError, or ServiceError. SetupError and PoolTimeoutError
|
|
6
|
+
# arise outside any invocation.
|
|
7
|
+
|
|
8
|
+
# The base of every error kobako raises.
|
|
17
9
|
class Error < StandardError; end
|
|
18
10
|
|
|
19
|
-
#
|
|
20
|
-
# rescue reads its captures and usage
|
|
21
|
-
#
|
|
22
|
-
# classes (+TrapError+ / +SandboxError+ / +ServiceError+ and their
|
|
23
|
-
# subclasses); +SetupError+ and +PoolTimeoutError+ arise outside any
|
|
24
|
-
# invocation and carry no Execution. Defaults to +nil+ — a pre-flight
|
|
25
|
-
# failure that ran no invocation leaves it unset.
|
|
11
|
+
# Gives an invocation failure the Execution of the run that failed, so a
|
|
12
|
+
# rescue reads its captures and usage as a successful caller would.
|
|
13
|
+
# +execution+ is +nil+ when the failure came before the guest ran.
|
|
26
14
|
module CarriesExecution
|
|
15
|
+
# The Execution of the run that failed.
|
|
27
16
|
attr_reader :execution
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
# raise site reads +raise error.with_execution(execution)+.
|
|
31
|
-
def with_execution(execution)
|
|
18
|
+
def with_execution(execution) # :nodoc:
|
|
32
19
|
@execution = execution
|
|
33
20
|
self
|
|
34
21
|
end
|
|
35
22
|
end
|
|
36
23
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# violation that signals a corrupted guest execution environment (an
|
|
40
|
-
# outcome the core envelope cannot frame, an absent one included).
|
|
41
|
-
#
|
|
42
|
-
# Two named subclasses cover the configured per-invocation caps:
|
|
43
|
-
#
|
|
44
|
-
# * TimeoutError — wall-clock +timeout+ exceeded.
|
|
45
|
-
# * MemoryLimitError — guest +memory.grow+ would exceed
|
|
46
|
-
# +memory_limit+.
|
|
47
|
-
#
|
|
48
|
-
# Host Apps that only care about "guest is unrecoverable, discard the
|
|
49
|
-
# Sandbox" can rescue +TrapError+ and ignore the subclass; Host Apps that
|
|
50
|
-
# want to surface a specific reason to operators can rescue the subclass
|
|
51
|
-
# first.
|
|
24
|
+
# The engine stopped the guest: a trap, a cap reached, or an outcome that
|
|
25
|
+
# could not be read. The next invocation on the same Sandbox runs as usual.
|
|
52
26
|
class TrapError < Error
|
|
53
27
|
include CarriesExecution
|
|
54
28
|
end
|
|
55
29
|
|
|
56
|
-
#
|
|
57
|
-
# +entry_time + timeout+ passed and the next guest wasm safepoint
|
|
58
|
-
# trapped. The Sandbox is unrecoverable after this point; discard and
|
|
59
|
-
# recreate before another execution.
|
|
30
|
+
# The invocation ran past its +timeout+.
|
|
60
31
|
class TimeoutError < TrapError; end
|
|
61
32
|
|
|
62
|
-
#
|
|
63
|
-
# linear memory past the configured +memory_limit+. The Sandbox is
|
|
64
|
-
# unrecoverable after this point; discard and recreate before another
|
|
65
|
-
# execution.
|
|
33
|
+
# The invocation grew guest memory past its +memory_limit+.
|
|
66
34
|
class MemoryLimitError < TrapError; end
|
|
67
35
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# an unreadable artifact, bytes that are not a valid Wasm module, or
|
|
72
|
-
# engine / linker / instantiation setup failure. Construction
|
|
73
|
-
# is not an invocation, so +SetupError+ sits beside the invocation
|
|
74
|
-
# taxonomy under +Kobako::Error+ rather than under +TrapError+: no
|
|
75
|
-
# Sandbox is produced, so the +TrapError+ "discard and recreate"
|
|
76
|
-
# recovery contract does not apply.
|
|
36
|
+
# A Sandbox could not be built: the Guest Binary is unreadable or not a
|
|
37
|
+
# valid module, the runtime failed to start, or it provides less isolation
|
|
38
|
+
# than +profile+ asks for.
|
|
77
39
|
class SetupError < Error; end
|
|
78
40
|
|
|
79
|
-
# The
|
|
80
|
-
# the Guest Binary artifact is absent at +wasm_path+ — the pre-build
|
|
81
|
-
# state on a fresh clone before +bundle exec rake compile+. Host Apps
|
|
82
|
-
# that only need "the Sandbox could not be set up" rescue +SetupError+;
|
|
83
|
-
# those wanting to special-case the unbuilt-artifact state rescue
|
|
84
|
-
# +ModuleNotBuiltError+ first.
|
|
41
|
+
# The Guest Binary is missing at +wasm_path+.
|
|
85
42
|
class ModuleNotBuiltError < SetupError; end
|
|
86
43
|
|
|
87
|
-
#
|
|
88
|
-
# from a decoded guest exception — its +origin+, original +klass+, and
|
|
89
|
-
# +backtrace_lines+ — so a Host App can inspect a failure beyond its
|
|
90
|
-
# message. Mixed into both rather than promoted to a shared superclass
|
|
91
|
-
# because +SandboxError+ and +ServiceError+ sit in distinct branches of
|
|
92
|
-
# the invocation-outcome taxonomy under +Kobako::Error+.
|
|
93
|
-
#
|
|
94
|
-
# Data specific to one kind of failure rides a named reader on the
|
|
95
|
-
# subclass that failure raises, the way Ruby pairs NameError#name with
|
|
96
|
-
# +NameError#local_variables+ — see UndefinedEntrypointError.
|
|
44
|
+
# What the guest reported about a failure.
|
|
97
45
|
module Diagnosable
|
|
46
|
+
# Where the failure came from (<tt>"sandbox"</tt> or <tt>"service"</tt>),
|
|
47
|
+
# the name of the class that raised it, and its backtrace.
|
|
98
48
|
attr_reader :origin, :klass, :backtrace_lines
|
|
99
49
|
|
|
100
|
-
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil)
|
|
50
|
+
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil) # :nodoc:
|
|
101
51
|
super(message)
|
|
102
52
|
@origin = origin
|
|
103
53
|
@klass = klass
|
|
@@ -105,93 +55,67 @@ module Kobako
|
|
|
105
55
|
end
|
|
106
56
|
end
|
|
107
57
|
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
# payload the host's codec could not decode out of a well-framed
|
|
111
|
-
# outcome.
|
|
58
|
+
# The guest failed: the script raised or did not compile, the Guest Binary
|
|
59
|
+
# could not start the run, or what it produced could not be read.
|
|
112
60
|
class SandboxError < Error
|
|
113
61
|
include Diagnosable
|
|
114
62
|
include CarriesExecution
|
|
115
63
|
end
|
|
116
64
|
|
|
117
|
-
# Service
|
|
118
|
-
# script reported an application-level failure that the script did not
|
|
119
|
-
# rescue. The base class covers a Service that ran and raised; the two
|
|
120
|
-
# subclasses below cover the calls that never reached one, so a Host App
|
|
121
|
-
# routes them apart with +rescue+ instead of by reading the message.
|
|
65
|
+
# A Service call failed and the guest left it unrescued.
|
|
122
66
|
class ServiceError < Error
|
|
123
67
|
include Diagnosable
|
|
124
68
|
include CarriesExecution
|
|
125
69
|
end
|
|
126
70
|
|
|
127
|
-
# The
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
# surface. The causes stay indistinguishable — an opaque target must
|
|
131
|
-
# disclose nothing about which methods it defines — so what a Host App
|
|
132
|
-
# learns is that this call will not succeed by being retried.
|
|
71
|
+
# The call reached no Service method: nothing is bound there, the Handle
|
|
72
|
+
# is not live, or the guest may not call that method. The causes look the
|
|
73
|
+
# same on purpose, so a target discloses nothing about what it defines.
|
|
133
74
|
class NoServiceError < ServiceError; end
|
|
134
75
|
|
|
135
|
-
# The
|
|
136
|
-
# method but its arguments did not fit — an unknown keyword, or an
|
|
137
|
-
# arity mismatch.
|
|
76
|
+
# The call reached the Service method with arguments that did not fit.
|
|
138
77
|
class ServiceArgumentError < ServiceError; end
|
|
139
78
|
|
|
140
|
-
# Raised at a Service
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
# unrescued returns it to the guest, which re-raises the exception it
|
|
144
|
-
# raised in the first place. It therefore never reaches the Host App as
|
|
145
|
-
# an invocation outcome and carries no Execution. +klass+ names the
|
|
146
|
-
# guest-side class, which has no host counterpart to rebuild.
|
|
79
|
+
# Raised at a Service's +yield+ when the guest block raised; rescue it as
|
|
80
|
+
# you would the block's own exception. Left unrescued, the guest sees its
|
|
81
|
+
# own exception again, so it never reaches the Host App.
|
|
147
82
|
class BlockError < Error
|
|
148
83
|
include Diagnosable
|
|
149
84
|
end
|
|
150
85
|
|
|
151
|
-
# Raised at a Service
|
|
152
|
-
#
|
|
153
|
-
# block never ran. As BlockError, a Service rescues it at that site and
|
|
154
|
-
# may go on, so it never reaches the Host App as an invocation outcome;
|
|
155
|
-
# unrescued, the dispatch answers as that Service having failed.
|
|
86
|
+
# Raised at a Service's +yield+ when an argument cannot cross to the guest,
|
|
87
|
+
# so the block never ran. Left unrescued, the Service call fails.
|
|
156
88
|
class YieldValueError < Error; end
|
|
157
89
|
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
90
|
+
# Raised at a Service's +yield+ when the guest serves no block, so the
|
|
91
|
+
# block never ran. Left unrescued, the Service call fails.
|
|
92
|
+
class UnsupportedError < Error; end
|
|
93
|
+
|
|
94
|
+
# One invocation issued more Handles than an id can number (2**31 - 1).
|
|
162
95
|
class HandleExhaustedError < SandboxError; end
|
|
163
96
|
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
# first invocation's snippet replay against a fresh +mrb_state+ (RITE
|
|
167
|
-
# version mismatch or corrupt body). Bytecode that loads cleanly and
|
|
168
|
-
# then raises at top level surfaces as plain +SandboxError+ with the
|
|
169
|
-
# natural mruby class preserved. Inherits from SandboxError so a single
|
|
170
|
-
# +rescue Kobako::SandboxError+ covers both source and bytecode
|
|
171
|
-
# snippet failures while callers wanting bytecode-specific handling
|
|
172
|
-
# can +rescue Kobako::BytecodeError+ directly.
|
|
97
|
+
# A <tt>preload(binary:)</tt> snippet failed to load. Bytecode that loads
|
|
98
|
+
# and then raises is a plain SandboxError.
|
|
173
99
|
class BytecodeError < SandboxError; end
|
|
174
100
|
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
#
|
|
181
|
-
# +NameError#local_variables+ for the same reason.
|
|
101
|
+
# The Guest Binary does not support a flag the Sandbox set, so no script
|
|
102
|
+
# ran. Retrying without the flag, or on a Guest Binary that supports it,
|
|
103
|
+
# can succeed.
|
|
104
|
+
class UnsupportedFlagError < SandboxError; end
|
|
105
|
+
|
|
106
|
+
# Sandbox#run named no top-level constant.
|
|
182
107
|
class UndefinedEntrypointError < SandboxError
|
|
108
|
+
# The constant asked for, and the ones the preloaded snippets define.
|
|
183
109
|
attr_reader :name, :available
|
|
184
110
|
|
|
185
|
-
def initialize(message, name: nil, available: [], **)
|
|
111
|
+
def initialize(message, name: nil, available: [], **) # :nodoc:
|
|
186
112
|
super(message, **)
|
|
187
113
|
@name = name
|
|
188
114
|
@available = available
|
|
189
115
|
end
|
|
190
116
|
end
|
|
191
117
|
|
|
192
|
-
# Pool
|
|
193
|
-
#
|
|
194
|
-
# held. No Sandbox state is touched — retrying succeeds as soon as a holder
|
|
195
|
-
# returns its Sandbox.
|
|
118
|
+
# Pool#with waited past +checkout_timeout+ with every slot held. Retrying
|
|
119
|
+
# succeeds once a holder returns its Sandbox.
|
|
196
120
|
class PoolTimeoutError < Error; end
|
|
197
121
|
end
|
data/lib/kobako/execution.rb
CHANGED
|
@@ -4,25 +4,20 @@ require_relative "capture"
|
|
|
4
4
|
require_relative "usage"
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# Execution on the error's +#execution+, so a rescue reads the captures and
|
|
11
|
-
# usage exactly as a successful caller reads them off the return value. On a
|
|
12
|
-
# failed run +#value+ is +nil+ — only the captures and usage are meaningful.
|
|
7
|
+
# The record of one Sandbox#eval or Sandbox#run: the guest's #value, its
|
|
8
|
+
# output, and its #usage. A failed run raises instead, and the error carries
|
|
9
|
+
# the same record on +execution+, where #value is +nil+.
|
|
13
10
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# one). It holds no reference to the raising error, so the error carries the
|
|
17
|
-
# Execution but not the reverse.
|
|
11
|
+
# #failed? tells the two apart when both #value are +nil+: a script whose
|
|
12
|
+
# last expression was +nil+ versus one that failed.
|
|
18
13
|
class Execution
|
|
19
|
-
# The
|
|
14
|
+
# The guest value the run produced; +nil+ on a failed run.
|
|
20
15
|
attr_reader :value
|
|
21
16
|
|
|
22
|
-
#
|
|
17
|
+
# What the run spent against its caps, as a Usage.
|
|
23
18
|
attr_reader :usage
|
|
24
19
|
|
|
25
|
-
def initialize(value:, usage:, stdout:, stderr:, failed:)
|
|
20
|
+
def initialize(value:, usage:, stdout:, stderr:, failed:) # :nodoc:
|
|
26
21
|
@value = value
|
|
27
22
|
@usage = usage
|
|
28
23
|
@stdout_capture = stdout
|
|
@@ -31,22 +26,22 @@ module Kobako
|
|
|
31
26
|
freeze
|
|
32
27
|
end
|
|
33
28
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
29
|
+
# Whether the run failed: +false+ on the Execution #eval or #run
|
|
30
|
+
# returned, +true+ on the one a raised error carries.
|
|
36
31
|
def failed? = @failed
|
|
37
32
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
33
|
+
# What the guest wrote to stdout, up to +stdout_limit+ bytes: a UTF-8
|
|
34
|
+
# String, or a binary one when the bytes are not valid UTF-8. The text
|
|
35
|
+
# carries no sign it was cut; #stdout_truncated? says so.
|
|
41
36
|
def stdout = @stdout_capture.bytes
|
|
42
37
|
|
|
43
|
-
#
|
|
38
|
+
# What the guest wrote to stderr; see #stdout.
|
|
44
39
|
def stderr = @stderr_capture.bytes
|
|
45
40
|
|
|
46
|
-
#
|
|
41
|
+
# Whether stdout reached +stdout_limit+.
|
|
47
42
|
def stdout_truncated? = @stdout_capture.truncated?
|
|
48
43
|
|
|
49
|
-
#
|
|
44
|
+
# Whether stderr reached +stderr_limit+.
|
|
50
45
|
def stderr_truncated? = @stderr_capture.truncated?
|
|
51
46
|
end
|
|
52
47
|
end
|