kobako 0.27.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 +71 -0
- data/Cargo.lock +117 -140
- data/README.md +100 -125
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +31 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +4 -3
- 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 +28 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +21 -12
- 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 +44 -0
- data/crates/kobako-wasmtime/Cargo.toml +7 -5
- data/crates/kobako-wasmtime/README.md +2 -2
- 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 -15
|
@@ -8,39 +8,20 @@ require_relative "reflection"
|
|
|
8
8
|
require_relative "yielder"
|
|
9
9
|
|
|
10
10
|
module Kobako
|
|
11
|
-
# See lib/kobako/transport.rb for the umbrella module doc; this file
|
|
12
|
-
# owns the pure-function dispatcher that answers a routed Call.
|
|
13
11
|
module Transport
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
# or Catalog::Handles, decodes only the payload, invokes the method,
|
|
19
|
-
# and answers +[ok, bytes]+ — which the native side puts on the
|
|
20
|
-
# Reply's ok or fault arm. It never raises.
|
|
21
|
-
#
|
|
22
|
-
# The module is stateless — all mutable state is threaded through
|
|
23
|
-
# arguments so Dispatcher has no instance variables and no side
|
|
24
|
-
# effects beyond mutating the Catalog::Handles via +alloc+ when a
|
|
25
|
-
# non-wire-representable return value must be wrapped.
|
|
26
|
-
module Dispatcher
|
|
12
|
+
# Answers a routed Call with +[true, body, nil]+ or
|
|
13
|
+
# +[false, message, fault_type]+ and never raises, so every failure
|
|
14
|
+
# reaches the guest as a fault.
|
|
15
|
+
module Dispatcher # :nodoc:
|
|
27
16
|
# Throw tag for the Yielder's break unwind back to the
|
|
28
17
|
# dispatcher's +catch+ frame. +private_constant+ is a
|
|
29
18
|
# convention boundary — not a defence.
|
|
30
19
|
BREAK_THROW = :__kobako_break__
|
|
31
20
|
private_constant :BREAK_THROW
|
|
32
21
|
|
|
33
|
-
|
|
22
|
+
class UndefinedTargetError < StandardError; end # :nodoc:
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
# Fault with type="undefined". Contained at the wire boundary —
|
|
37
|
-
# not part of the public Kobako error taxonomy.
|
|
38
|
-
class UndefinedTargetError < StandardError; end
|
|
39
|
-
|
|
40
|
-
# The codec fault of a request that never became a call, restated in
|
|
41
|
-
# the vocabulary the guest can act on while staying inside the codec
|
|
42
|
-
# taxonomy the fault boundary sorts on.
|
|
43
|
-
class UnreadableRequestError < Kobako::Codec::Error; end
|
|
24
|
+
class UnreadableRequestError < Kobako::Codec::Error; end # :nodoc:
|
|
44
25
|
|
|
45
26
|
# The category kobako's own refusals answer under, keyed by the class
|
|
46
27
|
# each is raised as and ordered most specific first. A class absent
|
|
@@ -55,30 +36,18 @@ module Kobako
|
|
|
55
36
|
UndefinedTargetError => "undefined",
|
|
56
37
|
ArgumentError => "argument",
|
|
57
38
|
YieldValueError => "runtime",
|
|
39
|
+
UnsupportedError => "runtime",
|
|
58
40
|
Kobako::Codec::Error => "internal",
|
|
59
41
|
Kobako::SandboxError => "runtime"
|
|
60
42
|
}.freeze
|
|
61
43
|
private_constant :OWN_FAULTS
|
|
62
44
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# nor the Context needs to publish accessors for the per-invocation
|
|
70
|
-
# +Catalog::Handles+ or +Runtime+. +yield_to_guest+ is a +String → String+ callable
|
|
71
|
-
# (the ext's per-dispatch +Kobako::Runtime::GuestYielder+) used only
|
|
72
|
-
# when the Call carries +block_given: true+. Never raises — every
|
|
73
|
-
# failure path takes the fault arm instead, so the guest sees a
|
|
74
|
-
# transport error rather than a wasm trap.
|
|
75
|
-
#
|
|
76
|
-
# The decode runs inside +Codec.track_handles+ so #resolve_call_args
|
|
77
|
-
# can skip the argument walk when no Capability Handle crossed the
|
|
78
|
-
# wire.
|
|
79
|
-
def dispatch(call, resolver, handler, yield_to_guest)
|
|
80
|
-
yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if call.block_given
|
|
81
|
-
[true, encode_ok(run(call, resolver, handler, yielder), handler), nil] # : [bool, String, String?]
|
|
45
|
+
# The per-invocation state arrives as arguments so the Dispatcher
|
|
46
|
+
# stays stateless and neither the resolver nor the Context publishes
|
|
47
|
+
# accessors for it.
|
|
48
|
+
def self.dispatch(call, resolver, handles, guest_yielder)
|
|
49
|
+
yielder = Yielder.new(guest_yielder, BREAK_THROW, handles) if call.block_given
|
|
50
|
+
[true, encode_ok(run(call, resolver, handles, yielder), handles), nil] # : [bool, String, String?]
|
|
82
51
|
# StandardError is the boundary by intent: a Service method's
|
|
83
52
|
# application fault folds into a guest-rescuable fault, while a
|
|
84
53
|
# host-process failure (NoMemoryError, SignalException, a bare Exception)
|
|
@@ -90,172 +59,120 @@ module Kobako
|
|
|
90
59
|
yielder&.invalidate!
|
|
91
60
|
end
|
|
92
61
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# so the reply-shaping and the failure boundary stay one glance wide.
|
|
96
|
-
def run(call, resolver, handler, yielder)
|
|
97
|
-
arguments, carried_handle = decode_arguments(call.payload)
|
|
98
|
-
exposure = resolve_target(call.target, resolver, handler)
|
|
99
|
-
args, kwargs = resolve_call_args(arguments, handler, carried_handle)
|
|
100
|
-
catch(BREAK_THROW) { invoke(exposure, call.method_name, args, kwargs, yielder) }
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Decode the Call's payload into its arguments, reporting whether any
|
|
104
|
-
# Capability Handle crossed. A codec fault here is a request that
|
|
105
|
-
# never became a call, restated so it cannot read as an unwritable
|
|
106
|
-
# reply — the same restatement #encode_ok makes in the other
|
|
107
|
-
# direction.
|
|
108
|
-
def decode_arguments(payload)
|
|
109
|
-
Kobako::Codec.track_handles { Payload::Arguments.decode(payload) }
|
|
110
|
-
rescue Kobako::Codec::Error => e
|
|
111
|
-
raise UnreadableRequestError, "Sandbox could not read the request: #{e.message}"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Resolve positional and keyword arguments off the decoded payload in
|
|
115
|
-
# one step. +carried_handle+ reports whether the decode carried any
|
|
116
|
-
# Capability Handle; when it did not, every argument resolves to
|
|
117
|
-
# itself, so the decoded values pass straight through and the walk is
|
|
118
|
-
# skipped entirely. Otherwise both go through #resolve_arg so Handles
|
|
119
|
-
# round-trip back to the host-side Ruby object before the call reaches
|
|
120
|
-
# +public_send+.
|
|
121
|
-
def resolve_call_args(arguments, handler, carried_handle)
|
|
122
|
-
return [arguments.args, arguments.kwargs] unless carried_handle
|
|
62
|
+
class << self
|
|
63
|
+
private
|
|
123
64
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
65
|
+
def run(call, resolver, handles, yielder)
|
|
66
|
+
arguments, carried_handle = decode_arguments(call.payload)
|
|
67
|
+
exposure = resolve_target(call.target, resolver, handles)
|
|
68
|
+
args, kwargs = resolve_call_args(arguments, handles, carried_handle)
|
|
69
|
+
catch(BREAK_THROW) { invoke(exposure, call.method_name, args, kwargs, yielder) }
|
|
70
|
+
end
|
|
127
71
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# it is the +<class>: <message>+ shape a Host App is told to keep
|
|
136
|
-
# secrets out of, so wearing it says the Service raised. kobako's own
|
|
137
|
-
# refusals answer under their own wording instead of borrowing that
|
|
138
|
-
# shape.
|
|
139
|
-
#
|
|
140
|
-
# The guest's own block failing is not the Service's to report at
|
|
141
|
-
# all, so the Yielder that raised it is asked first — it recognises
|
|
142
|
-
# its own by identity and words the failure the guest's way.
|
|
143
|
-
def caught_fault(error, yielder)
|
|
144
|
-
block_failure = yielder&.fault_text(error)
|
|
145
|
-
return fault("block", block_failure) if block_failure
|
|
72
|
+
# A codec fault here is a request that never became a call, restated
|
|
73
|
+
# so it cannot read as an unwritable reply.
|
|
74
|
+
def decode_arguments(payload)
|
|
75
|
+
Kobako::Codec.track_handles { Payload::Arguments.decode(payload) }
|
|
76
|
+
rescue Kobako::Codec::Error => e
|
|
77
|
+
raise UnreadableRequestError, "Sandbox could not read the request: #{e.message}"
|
|
78
|
+
end
|
|
146
79
|
|
|
147
|
-
|
|
148
|
-
|
|
80
|
+
def resolve_call_args(arguments, handles, carried_handle)
|
|
81
|
+
return [arguments.args, arguments.kwargs] unless carried_handle
|
|
149
82
|
|
|
150
|
-
|
|
151
|
-
|
|
83
|
+
[arguments.args.map { |v| resolve_arg(v, handles) },
|
|
84
|
+
arguments.kwargs.transform_values { |v| resolve_arg(v, handles) }]
|
|
85
|
+
end
|
|
152
86
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if kwargs.empty?
|
|
171
|
-
target.public_send(name, *args, &block)
|
|
172
|
-
else
|
|
173
|
-
target.public_send(name, *args, **kwargs, &block)
|
|
87
|
+
# The class prefix marks a Service's own exception and nothing else:
|
|
88
|
+
# it is the +<class>: <message>+ shape a Host App is told to keep
|
|
89
|
+
# secrets out of, so wearing it says the Service raised. kobako's own
|
|
90
|
+
# refusals answer under their own wording instead of borrowing that
|
|
91
|
+
# shape.
|
|
92
|
+
#
|
|
93
|
+
# The guest's own block failing is not the Service's to report at
|
|
94
|
+
# all, so the Yielder that raised it is asked first — it recognises
|
|
95
|
+
# its own by identity and words the failure the guest's way.
|
|
96
|
+
def caught_fault(error, yielder)
|
|
97
|
+
block_failure = yielder&.fault_text(error)
|
|
98
|
+
return fault("block", block_failure) if block_failure
|
|
99
|
+
|
|
100
|
+
own = OWN_FAULTS.find { |klass, _| error.is_a?(klass) }
|
|
101
|
+
return fault(own.last, error.message) if own
|
|
102
|
+
|
|
103
|
+
fault("runtime", "#{error.class}: #{error.message}")
|
|
174
104
|
end
|
|
175
|
-
end
|
|
176
105
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
106
|
+
# The wire always carries a keyword map, so an empty one is left
|
|
107
|
+
# unsplatted for methods that take no keywords.
|
|
108
|
+
def invoke(exposure, method, args, kwargs, yielder)
|
|
109
|
+
name = method.to_sym
|
|
110
|
+
reject_unreachable!(exposure, name)
|
|
111
|
+
target = exposure.object
|
|
112
|
+
block = yielder&.to_proc
|
|
113
|
+
if kwargs.empty?
|
|
114
|
+
target.public_send(name, *args, &block)
|
|
115
|
+
else
|
|
116
|
+
target.public_send(name, *args, **kwargs, &block)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
186
119
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
raise UndefinedTargetError, e.message
|
|
195
|
-
end
|
|
120
|
+
# Both the ambient-surface floor and the reference's Exposure answer
|
|
121
|
+
# through Reflection, so a rejected name discloses nothing about
|
|
122
|
+
# which of the two refused.
|
|
123
|
+
def reject_unreachable!(exposure, name)
|
|
124
|
+
reason = Reflection.refusal(exposure, name)
|
|
125
|
+
raise UndefinedTargetError, reason if reason
|
|
126
|
+
end
|
|
196
127
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
# id. No else-branch is needed — the envelope layer is the system
|
|
202
|
-
# boundary that enforces the invariant.
|
|
203
|
-
def resolve_target(target, resolver, handler)
|
|
204
|
-
case target
|
|
205
|
-
when String
|
|
206
|
-
resolve_path(target, resolver)
|
|
207
|
-
when Integer
|
|
208
|
-
resolve_handle(target, handler)
|
|
128
|
+
def resolve_arg(value, handles)
|
|
129
|
+
Kobako::Codec::HandleWalk.deep_restore(value, handles)
|
|
130
|
+
rescue Kobako::SandboxError => e
|
|
131
|
+
raise UndefinedTargetError, e.message
|
|
209
132
|
end
|
|
210
|
-
end
|
|
211
133
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
134
|
+
# The envelope already discriminated the two target forms, so no
|
|
135
|
+
# else-branch is needed.
|
|
136
|
+
def resolve_target(target, resolver, handles)
|
|
137
|
+
case target
|
|
138
|
+
when String
|
|
139
|
+
resolve_path(target, resolver)
|
|
140
|
+
when Integer
|
|
141
|
+
resolve_handle(target, handles)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
217
144
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
raise UndefinedTargetError, e.message
|
|
224
|
-
end
|
|
145
|
+
def resolve_path(path, resolver)
|
|
146
|
+
resolver.lookup(path)
|
|
147
|
+
rescue KeyError => e
|
|
148
|
+
raise UndefinedTargetError, e.message
|
|
149
|
+
end
|
|
225
150
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# Capability Handle in place. The happy path encodes exactly once.
|
|
232
|
-
#
|
|
233
|
-
# Any other codec fault is the answer failing to encode rather than the
|
|
234
|
-
# request failing to decode, and a Service is the only side that can
|
|
235
|
-
# change what it returns — so it is named here, where the direction is
|
|
236
|
-
# known, instead of falling to the boundary's codec floor and reporting
|
|
237
|
-
# as an exchange that produced no Service outcome.
|
|
238
|
-
def encode_ok(value, handler)
|
|
239
|
-
Kobako::Codec::Nesting.assert_within_bound!(value)
|
|
240
|
-
Kobako::Codec::Encoder.encode(value)
|
|
241
|
-
rescue Kobako::Codec::UnsupportedTypeError
|
|
242
|
-
encode_ok(wrap_as_handle(value, handler), handler)
|
|
243
|
-
rescue Kobako::Codec::Error => e
|
|
244
|
-
raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.message}"
|
|
245
|
-
end
|
|
151
|
+
def resolve_handle(id, handles)
|
|
152
|
+
handles.exposure(id)
|
|
153
|
+
rescue Kobako::SandboxError => e
|
|
154
|
+
raise UndefinedTargetError, e.message
|
|
155
|
+
end
|
|
246
156
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
157
|
+
# A value with no wire form goes back as a Capability Handle. Any
|
|
158
|
+
# other codec fault is the answer failing to encode, which only the
|
|
159
|
+
# Service can change — so it is named here, where the direction is
|
|
160
|
+
# known, instead of falling to the boundary's codec floor.
|
|
161
|
+
def encode_ok(value, handles)
|
|
162
|
+
Kobako::Codec::Nesting.assert_within_bound!(value)
|
|
163
|
+
Kobako::Codec::Encoder.encode(value)
|
|
164
|
+
rescue Kobako::Codec::UnsupportedTypeError
|
|
165
|
+
encode_ok(handles.alloc(value), handles)
|
|
166
|
+
rescue Kobako::Codec::Error => e
|
|
167
|
+
raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.message}"
|
|
168
|
+
end
|
|
253
169
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
170
|
+
# Ruby core builds some exception messages as ASCII-8BIT (the arity
|
|
171
|
+
# ArgumentError, for one), and the envelope requires UTF-8 of the
|
|
172
|
+
# text fields it frames.
|
|
173
|
+
def fault(type, message)
|
|
174
|
+
[message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace), type] # : [String, String]
|
|
175
|
+
end
|
|
259
176
|
end
|
|
260
177
|
end
|
|
261
178
|
end
|
|
@@ -4,31 +4,17 @@ require_relative "../errors"
|
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Transport
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# decode. Distinct from a Wasm trap (engine signalled the guest
|
|
11
|
-
# runtime is unrecoverable) and from a normal sandbox-layer failure
|
|
12
|
-
# (the script raised but the protocol was respected): a
|
|
13
|
-
# +Transport::Error+ always indicates the guest runtime is corrupted —
|
|
14
|
-
# the only safe recovery is to discard the Sandbox and start a new
|
|
15
|
-
# invocation.
|
|
16
|
-
#
|
|
17
|
-
# Inherits from +Kobako::SandboxError+ so a single
|
|
18
|
-
# +rescue Kobako::SandboxError+ still catches it; callers that want
|
|
19
|
-
# to distinguish wire-violation paths from script failures can
|
|
20
|
-
# +rescue Kobako::Transport::Error+ directly.
|
|
7
|
+
# The guest broke the exchange with the host: it produced a value that
|
|
8
|
+
# could not be decoded, or a dispatch it made could not complete. Being a
|
|
9
|
+
# SandboxError, it is caught by rescuing that too.
|
|
21
10
|
class Error < Kobako::SandboxError
|
|
22
|
-
def initialize(message, diagnostic: nil, **)
|
|
11
|
+
def initialize(message, diagnostic: nil, **) # :nodoc:
|
|
23
12
|
super(message, **)
|
|
24
13
|
@diagnostic = diagnostic
|
|
25
14
|
end
|
|
26
15
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# be …" wording, so it stays out of #message; an operator
|
|
30
|
-
# triaging a corrupted runtime still needs it, and
|
|
31
|
-
# +detailed_message+ is where Ruby puts text of exactly that kind.
|
|
16
|
+
# Appends the codec's own reason, which an operator needs and a caller
|
|
17
|
+
# cannot act on, so #message leaves it out.
|
|
32
18
|
def detailed_message(...)
|
|
33
19
|
rendered = super
|
|
34
20
|
return rendered if @diagnostic.nil?
|
|
@@ -6,16 +6,11 @@ require_relative "reflection"
|
|
|
6
6
|
|
|
7
7
|
module Kobako
|
|
8
8
|
module Transport
|
|
9
|
-
# The methods
|
|
10
|
-
#
|
|
11
|
-
# is
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
# cannot foresee handing over are the ones it never wrote.
|
|
15
|
-
#
|
|
16
|
-
# Built on the +class X < Data.define(...)+ subclass form so the class
|
|
17
|
-
# body is fully Steep-visible; see +.rubocop.yml+ for the rationale.
|
|
18
|
-
class Exposure < Data.define(:object, :names)
|
|
9
|
+
# The methods one reference lets the guest call, fixed when the reference
|
|
10
|
+
# is made. Only what the object's own class and the object define in
|
|
11
|
+
# source is exposed, since a Host App cannot foresee handing over what it
|
|
12
|
+
# never wrote.
|
|
13
|
+
class Exposure < Data.define(:object, :names) # :nodoc:
|
|
19
14
|
PREDICATE = :respond_to_guest?
|
|
20
15
|
private_constant :PREDICATE
|
|
21
16
|
|
|
@@ -2,15 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Transport
|
|
5
|
-
|
|
6
|
-
# Dispatcher reaches +public_send+: which method names on a resolved
|
|
7
|
-
# target count as Service behaviour, and which are Ruby's ambient
|
|
8
|
-
# metaprogramming surface.
|
|
9
|
-
#
|
|
10
|
-
# Answers with a refusal reason rather than raising, so the error
|
|
11
|
-
# taxonomy stays with the Dispatcher and this module holds only the
|
|
12
|
-
# policy.
|
|
13
|
-
module Reflection
|
|
5
|
+
module Reflection # :nodoc:
|
|
14
6
|
# Modules whose instance methods are ambient Ruby reflection /
|
|
15
7
|
# metaprogramming surface (+send+, +public_send+, +instance_eval+,
|
|
16
8
|
# +method+, +tap+, +instance_variable_get+, ...) rather than Service
|
|
@@ -36,59 +28,56 @@ module Kobako
|
|
|
36
28
|
# callable exposes by default, so the floor and the Exposure agree.
|
|
37
29
|
CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
|
|
38
30
|
|
|
39
|
-
module_function
|
|
40
|
-
|
|
41
31
|
# The reason +name+ is unreachable through +exposure+, or +nil+ when
|
|
42
32
|
# the dispatch may proceed. Composes the ambient-surface floor with the
|
|
43
33
|
# reference's Exposure, in that order: the Exposure only narrows and
|
|
44
34
|
# can never re-open what the floor rejects.
|
|
45
|
-
def refusal(exposure, name)
|
|
35
|
+
def self.refusal(exposure, name)
|
|
46
36
|
ambient_refusal(exposure.object, name) ||
|
|
47
37
|
(exposure.exposes?(name) ? nil : "method #{name.inspect} is not exposed to the guest")
|
|
48
38
|
end
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# class, the owner of every class-level method (+File.popen+ /
|
|
53
|
-
# +Kernel.system+, unreachable via any fixed core-module list) — is
|
|
54
|
-
# rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
|
|
55
|
-
# stays invocable). A name with no concrete public method is allowed
|
|
56
|
-
# only when the target opts into it via +respond_to?+ (dynamic
|
|
57
|
-
# +method_missing+ Services), since the dangerous methods are all
|
|
58
|
-
# concretely defined and therefore never reach that branch.
|
|
59
|
-
#
|
|
60
|
-
# +method_missing+ named explicitly is refused whatever its owner:
|
|
61
|
-
# it is Ruby's dynamic-dispatch hook, and a public override (a
|
|
62
|
-
# +Delegator+) binds and calls the private method the guest passes as
|
|
63
|
-
# its first argument (+Kernel#system+). A dynamic +method_missing+
|
|
64
|
-
# Service is untouched — the guest reaches it by the virtual name,
|
|
65
|
-
# which resolves through the +NameError+ branch below, never by
|
|
66
|
-
# naming +method_missing+.
|
|
67
|
-
def ambient_refusal(target, name)
|
|
68
|
-
return "method #{name.inspect} is not a Service method" if name == :method_missing
|
|
40
|
+
class << self
|
|
41
|
+
private
|
|
69
42
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
43
|
+
# Guard against ambient reflection methods. A public method whose
|
|
44
|
+
# owner is a META_OWNERS or GADGET_OWNERS module — or a singleton
|
|
45
|
+
# class, the owner of every class-level method (+File.popen+ /
|
|
46
|
+
# +Kernel.system+, unreachable via any fixed core-module list) — is
|
|
47
|
+
# rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
|
|
48
|
+
# stays invocable). A name with no concrete public method is allowed
|
|
49
|
+
# only when the target opts into it via +respond_to?+ (dynamic
|
|
50
|
+
# +method_missing+ Services), since the dangerous methods are all
|
|
51
|
+
# concretely defined and therefore never reach that branch.
|
|
52
|
+
#
|
|
53
|
+
# +method_missing+ named explicitly is refused whatever its owner:
|
|
54
|
+
# it is Ruby's dynamic-dispatch hook, and a public override (a
|
|
55
|
+
# +Delegator+) binds and calls the private method the guest passes as
|
|
56
|
+
# its first argument (+Kernel#system+). A dynamic +method_missing+
|
|
57
|
+
# Service is untouched — the guest reaches it by the virtual name,
|
|
58
|
+
# which resolves through the +NameError+ branch below, never by
|
|
59
|
+
# naming +method_missing+.
|
|
60
|
+
def ambient_refusal(target, name)
|
|
61
|
+
return "method #{name.inspect} is not a Service method" if name == :method_missing
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
owner = target.public_method(name).owner
|
|
64
|
+
return nil unless ambient_owner?(owner, target)
|
|
65
|
+
return nil if GADGET_OWNERS.include?(owner) && CALLABLE_ALLOW.include?(name)
|
|
77
66
|
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
"method #{name.inspect} is not a Service method"
|
|
68
|
+
rescue NameError
|
|
69
|
+
return nil if target.respond_to?(name)
|
|
70
|
+
|
|
71
|
+
"no public method #{name.inspect} on target"
|
|
72
|
+
end
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def ambient_owner?(owner, target)
|
|
89
|
-
META_OWNERS.include?(owner) ||
|
|
90
|
-
GADGET_OWNERS.include?(owner) ||
|
|
91
|
-
(target.is_a?(Module) && owner.singleton_class?)
|
|
74
|
+
# A plain object's own singleton method (+def obj.x+) stays reachable,
|
|
75
|
+
# since only a Module target counts its singleton class as ambient.
|
|
76
|
+
def ambient_owner?(owner, target)
|
|
77
|
+
META_OWNERS.include?(owner) ||
|
|
78
|
+
GADGET_OWNERS.include?(owner) ||
|
|
79
|
+
(target.is_a?(Module) && owner.singleton_class?)
|
|
80
|
+
end
|
|
92
81
|
end
|
|
93
82
|
end
|
|
94
83
|
end
|