kobako 0.26.0-aarch64-linux → 0.28.0-aarch64-linux
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/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/README.md +10 -12
- data/crates/kobako-transport/CHANGELOG.md +35 -0
- data/crates/kobako-transport/README.md +43 -27
- data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
- data/crates/kobako-wasmtime/README.md +8 -10
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- 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 +6 -3
- data/rust-toolchain.toml +0 -11
|
@@ -5,26 +5,11 @@ require_relative "../errors"
|
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
7
|
module Codec
|
|
8
|
-
|
|
9
|
-
# the host↔guest boundary. #deep_wrap allocates a +Kobako::Handle+ for
|
|
10
|
-
# each non-wire-representable leaf on the host→guest +#run+ argument
|
|
11
|
-
# path; #deep_restore resolves each wire-decoded Handle back to its
|
|
12
|
-
# host object on every guest→host value path. #representable? is the
|
|
13
|
-
# by-value codec-type predicate that decides which leaves #deep_wrap
|
|
14
|
-
# must wrap: the closed 11-entry wire type set
|
|
15
|
-
# ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type
|
|
16
|
-
# Mapping).
|
|
17
|
-
#
|
|
18
|
-
# All helpers are pure except #deep_wrap, whose only side effect is
|
|
19
|
-
# allocating new Handle ids into the supplied table.
|
|
20
|
-
module HandleWalk
|
|
21
|
-
module_function
|
|
22
|
-
|
|
8
|
+
module HandleWalk # :nodoc:
|
|
23
9
|
# Inclusive Integer range the msgpack gem encodes without raising
|
|
24
10
|
# +RangeError+ at encode time — signed +int 64+ minimum through
|
|
25
|
-
# unsigned +uint 64+ maximum
|
|
26
|
-
#
|
|
27
|
-
# Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
|
|
11
|
+
# unsigned +uint 64+ maximum (docs/wire/payload-msgpack.md § Integer
|
|
12
|
+
# Range).
|
|
28
13
|
# Anchored as a +Range+ so #primitive_type? stays a single
|
|
29
14
|
# dispatch line. This is the codec's encode domain — not to
|
|
30
15
|
# be confused with the Handle id range, which lives on
|
|
@@ -32,141 +17,91 @@ module Kobako
|
|
|
32
17
|
# represents a different concept entirely.
|
|
33
18
|
MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
|
|
34
19
|
|
|
35
|
-
#
|
|
36
|
-
# (
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
# representable, or +Hash+ whose every key and value are
|
|
42
|
-
# representable. Integers outside the codec's signed-64 /
|
|
43
|
-
# unsigned-64 union are rejected so the predicate agrees with the
|
|
44
|
-
# msgpack gem's encode-time +RangeError+ behaviour the codec
|
|
45
|
-
# already surfaces as UnsupportedTypeError.
|
|
46
|
-
#
|
|
47
|
-
# The optional +depth+ bounds the recursive descent at the wire's
|
|
48
|
-
# structural nesting cap so a self-referential container — reachable
|
|
49
|
-
# as a cyclic Hash key on the +#run+ argument path — reads as
|
|
50
|
-
# non-representable rather than looping, letting #deep_wrap_hash
|
|
51
|
-
# refuse it instead of overflowing this walk.
|
|
52
|
-
def representable?(value, depth = 0)
|
|
20
|
+
# Whether +value+ falls in the codec's type set
|
|
21
|
+
# (docs/wire/payload-msgpack.md § Type Mapping). The +depth+ bound
|
|
22
|
+
# makes a self-referential container —
|
|
23
|
+
# reachable as a cyclic Hash key on the +#run+ argument path — read as
|
|
24
|
+
# non-representable rather than looping.
|
|
25
|
+
def self.representable?(value, depth = 0)
|
|
53
26
|
return false if depth > MAX_NESTING_DEPTH
|
|
54
27
|
|
|
55
28
|
primitive_type?(value) || container_representable?(value, depth)
|
|
56
29
|
end
|
|
57
30
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
# (Array, Hash) one structural level at a time; a non-representable
|
|
63
|
-
# leaf is wrapped as-is without inspecting its internal structure.
|
|
64
|
-
# An existing +Kobako::Handle+ is representable and passes through
|
|
65
|
-
# unchanged — auto-wrap never re-wraps a Handle. Only Hash *values*
|
|
66
|
-
# are wrapped: a Hash *key* must already be wire-representable, since a
|
|
67
|
-
# key is not auto-wrapped the way a value is — a non-representable key
|
|
68
|
-
# raises +Kobako::SandboxError+ rather than crossing as an opaque token
|
|
69
|
-
# the guest→host restore walk could not round-trip.
|
|
70
|
-
#
|
|
71
|
-
# +value+ may be any Ruby value; +handler+ must respond to
|
|
72
|
-
# +#alloc(object) -> Kobako::Handle+ (a host-side
|
|
73
|
-
# +Kobako::Catalog::Handles+). Returns a structurally equivalent value
|
|
74
|
-
# whose leaves are either representable or +Kobako::Handle+
|
|
75
|
-
# tokens.
|
|
76
|
-
#
|
|
77
|
-
# Recursive calls spell the +HandleWalk.+ receiver so the dispatch
|
|
78
|
-
# stays valid even when a block is captured and run under a
|
|
79
|
-
# different +self+ (+module_function+ privatizes the instance copies).
|
|
80
|
-
def deep_wrap(value, handler, depth = 0)
|
|
31
|
+
# Only Hash values are wrapped: a non-representable key raises rather
|
|
32
|
+
# than crossing as a token the guest→host restore walk could not
|
|
33
|
+
# round-trip.
|
|
34
|
+
def self.deep_wrap(value, handles, depth = 0)
|
|
81
35
|
guard_nesting!(depth)
|
|
82
36
|
case value
|
|
83
|
-
when ::Array then value.map { |element|
|
|
84
|
-
when ::Hash then
|
|
37
|
+
when ::Array then value.map { |element| deep_wrap(element, handles, depth + 1) }
|
|
38
|
+
when ::Hash then deep_wrap_hash(value, handles, depth)
|
|
85
39
|
else
|
|
86
|
-
representable?(value) ? value :
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Refuse a wrap walk that has descended past the wire's structural
|
|
91
|
-
# nesting bound — a reference cycle necessarily trips it — so a +#run+
|
|
92
|
-
# argument fails as a clean +Kobako::SandboxError+ rather than an
|
|
93
|
-
# unbounded host recursion.
|
|
94
|
-
def guard_nesting!(depth)
|
|
95
|
-
return unless depth > MAX_NESTING_DEPTH
|
|
96
|
-
|
|
97
|
-
raise Kobako::SandboxError,
|
|
98
|
-
"a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels within the Run payload and " \
|
|
99
|
-
"cannot cross the sandbox boundary (possible reference cycle)"
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Wrap a Hash on the host→guest argument path: each value's
|
|
103
|
-
# non-representable leaves become Handles, while each key must be
|
|
104
|
-
# wire-representable as-is. A stateful object may cross the boundary as
|
|
105
|
-
# a Hash value but not as a key — the one deliberate asymmetry with the
|
|
106
|
-
# guest→host restore walk, which resolves Handle keys the guest built.
|
|
107
|
-
def deep_wrap_hash(hash, handler, depth)
|
|
108
|
-
wrapped = {} # : Hash[untyped, untyped]
|
|
109
|
-
hash.each do |key, val|
|
|
110
|
-
unless HandleWalk.representable?(key, depth + 1)
|
|
111
|
-
raise Kobako::SandboxError,
|
|
112
|
-
"a Hash passed to #run has a key that cannot cross the sandbox boundary " \
|
|
113
|
-
"(#{key.class}); only wire-representable values may be Hash keys"
|
|
114
|
-
end
|
|
115
|
-
wrapped[key] = HandleWalk.deep_wrap(val, handler, depth + 1)
|
|
40
|
+
representable?(value) ? value : handles.alloc(value)
|
|
116
41
|
end
|
|
117
|
-
wrapped
|
|
118
42
|
end
|
|
119
43
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
# Handles back to their objects wherever a guest→host payload carries
|
|
125
|
-
# one — an invocation result, a yield-block result, or a dispatch
|
|
126
|
-
# argument. The walk descends through Array elements and Hash keys and
|
|
127
|
-
# values one structural level at a time; any non-Handle leaf passes
|
|
128
|
-
# through unchanged.
|
|
129
|
-
#
|
|
130
|
-
# +value+ is a decoded Ruby value (a Handle here is a wire-decoded
|
|
131
|
-
# +Kobako::Handle+, never a guest-forged one); +handler+ must
|
|
132
|
-
# respond to +#fetch(id) -> object+ (a host-side
|
|
133
|
-
# +Kobako::Catalog::Handles+). +handler.fetch+ raises
|
|
134
|
-
# +Kobako::SandboxError+ for an id with no live binding, the
|
|
135
|
-
# corrupted-runtime fallback.
|
|
136
|
-
def deep_restore(value, handler)
|
|
44
|
+
# The inverse of #deep_wrap. A Handle here was decoded off the wire,
|
|
45
|
+
# never forged by the guest, and +handles.fetch+ refuses an id with no
|
|
46
|
+
# live binding.
|
|
47
|
+
def self.deep_restore(value, handles)
|
|
137
48
|
case value
|
|
138
|
-
when ::Array then value.map { |element|
|
|
49
|
+
when ::Array then value.map { |element| deep_restore(element, handles) }
|
|
139
50
|
when ::Hash
|
|
140
51
|
# Rebuilt with each key restored: two distinct Handle keys that
|
|
141
52
|
# resolve to equal host objects collapse to the later pair, as in
|
|
142
53
|
# any Ruby Hash. The guest authored this payload, so that collapse
|
|
143
54
|
# is its own concern, not a fidelity guarantee the host owes it.
|
|
144
|
-
value.to_h { |key, val| [
|
|
145
|
-
when Kobako::Handle then
|
|
55
|
+
value.to_h { |key, val| [deep_restore(key, handles), deep_restore(val, handles)] }
|
|
56
|
+
when Kobako::Handle then handles.fetch(value.id)
|
|
146
57
|
else value
|
|
147
58
|
end
|
|
148
59
|
end
|
|
149
60
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
61
|
+
class << self
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# A reference cycle necessarily trips the nesting bound, so a +#run+
|
|
65
|
+
# argument fails as a clean +Kobako::SandboxError+ rather than an
|
|
66
|
+
# unbounded host recursion.
|
|
67
|
+
def guard_nesting!(depth)
|
|
68
|
+
return unless depth > MAX_NESTING_DEPTH
|
|
69
|
+
|
|
70
|
+
raise Kobako::SandboxError,
|
|
71
|
+
"a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels within the Run payload and " \
|
|
72
|
+
"cannot cross the sandbox boundary (possible reference cycle)"
|
|
158
73
|
end
|
|
159
|
-
end
|
|
160
74
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
75
|
+
# A stateful object may cross the boundary as a Hash value but not as
|
|
76
|
+
# a key — the one deliberate asymmetry with the guest→host restore
|
|
77
|
+
# walk, which resolves Handle keys the guest built.
|
|
78
|
+
def deep_wrap_hash(hash, handles, depth)
|
|
79
|
+
wrapped = {} # : Hash[untyped, untyped]
|
|
80
|
+
hash.each do |key, val|
|
|
81
|
+
unless representable?(key, depth + 1)
|
|
82
|
+
raise Kobako::SandboxError,
|
|
83
|
+
"a Hash passed to #run has a key that cannot cross the sandbox boundary " \
|
|
84
|
+
"(#{key.class}); only wire-representable values may be Hash keys"
|
|
85
|
+
end
|
|
86
|
+
wrapped[key] = deep_wrap(val, handles, depth + 1)
|
|
87
|
+
end
|
|
88
|
+
wrapped
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def primitive_type?(value)
|
|
92
|
+
case value
|
|
93
|
+
when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
|
|
94
|
+
when ::Integer then MSGPACK_INT_RANGE.cover?(value)
|
|
95
|
+
else false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def container_representable?(value, depth)
|
|
100
|
+
case value
|
|
101
|
+
when ::Array then value.all? { |element| representable?(element, depth + 1) }
|
|
102
|
+
when ::Hash then value.all? { |pair| pair.all? { |member| representable?(member, depth + 1) } }
|
|
103
|
+
else false
|
|
104
|
+
end
|
|
170
105
|
end
|
|
171
106
|
end
|
|
172
107
|
end
|
data/lib/kobako/codec/nesting.rb
CHANGED
|
@@ -4,14 +4,9 @@ require_relative "error"
|
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Codec
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
# packer. The packer takes no depth limit and walks a list or map in
|
|
11
|
-
# frames that carry no stack guard, so a value it cannot finish — a
|
|
12
|
-
# reference cycle necessarily is one — must be refused before it is
|
|
13
|
-
# handed over.
|
|
14
|
-
module Nesting
|
|
7
|
+
# Refuses a value nested past the wire's bound before the packer sees it,
|
|
8
|
+
# since the packer walks without a stack guard and a cycle never ends.
|
|
9
|
+
module Nesting # :nodoc:
|
|
15
10
|
# Raise InvalidTypeError when +value+ nests past MAX_NESTING_DEPTH; a
|
|
16
11
|
# value nested exactly to the bound passes.
|
|
17
12
|
def self.assert_within_bound!(value, depth = 0)
|
data/lib/kobako/codec/state.rb
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Codec
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
# synchronously on their owning thread.
|
|
9
|
-
class State
|
|
5
|
+
# Whether a Capability Handle crossed the decode in flight, kept per
|
|
6
|
+
# thread so plain instance variables stay sound.
|
|
7
|
+
class State # :nodoc:
|
|
10
8
|
# Thread-local slot holding the calling thread's State.
|
|
11
9
|
STATE_KEY = :__kobako_codec_state__
|
|
12
10
|
private_constant :STATE_KEY
|
|
@@ -22,12 +20,8 @@ module Kobako
|
|
|
22
20
|
@carried_handle = false
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
# Bracket a decode and return the block's result together with
|
|
26
|
-
# whether the decoded tree carried an ext 0x01 Capability Handle.
|
|
27
23
|
# ExtTypes#unpack_handle is the sole chokepoint every Handle passes
|
|
28
|
-
# through, so one decode pass records the whole tree
|
|
29
|
-
# can skip an all-identity Handle-resolution walk when none was
|
|
30
|
-
# present.
|
|
24
|
+
# through, so one decode pass records the whole tree.
|
|
31
25
|
def track_handles
|
|
32
26
|
@carried_handle = false
|
|
33
27
|
result = yield
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -4,45 +4,17 @@ require_relative "error"
|
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Codec
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
# - UTF-8 assertion at the codec boundary
|
|
11
|
-
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
12
|
-
# § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
|
|
13
|
-
# Decoder when walking +str+ family payloads and by ExtTypes
|
|
14
|
-
# when validating the +ext 0x00+ Symbol payload.
|
|
15
|
-
# - +ArgumentError+ translation at the codec boundary
|
|
16
|
-
# (#with_boundary) so the public taxonomy stays
|
|
17
|
-
# Kobako::Codec::Error.
|
|
18
|
-
#
|
|
19
|
-
# Both helpers are pure — they only inspect inputs, never mutate them.
|
|
20
|
-
# The host↔guest Handle substitution walk lives in HandleWalk.
|
|
21
|
-
module Utils
|
|
22
|
-
module_function
|
|
23
|
-
|
|
24
|
-
# Raise InvalidEncodingError unless +string+'s bytes are valid under
|
|
25
|
-
# its current encoding tag. +label+ is the caller-supplied prefix
|
|
26
|
-
# for the error message (e.g. +"str payload"+, +"Symbol payload"+).
|
|
27
|
-
def assert_utf8!(string, label)
|
|
7
|
+
module Utils # :nodoc:
|
|
8
|
+
def self.assert_utf8!(string, label)
|
|
28
9
|
return if string.valid_encoding?
|
|
29
10
|
|
|
30
11
|
raise InvalidEncodingError, "#{label} is not valid UTF-8"
|
|
31
12
|
end
|
|
32
13
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
# the Ruby standard library.
|
|
38
|
-
#
|
|
39
|
-
# Reach for this only where a value object is constructed outside a
|
|
40
|
-
# Decoder.decode block, whose rescue already performs the same
|
|
41
|
-
# mapping (worked example: ExtTypes#unpack_handle building
|
|
42
|
-
# +Handle.restore+ from a raw fixext payload). Do not use it for
|
|
43
|
-
# general-purpose validation outside the codec boundary —
|
|
44
|
-
# host-layer +ArgumentError+ values should propagate unchanged.
|
|
45
|
-
def with_boundary
|
|
14
|
+
# Only for a value object built outside a Decoder.decode block, whose
|
|
15
|
+
# rescue already maps the same way; a host-layer +ArgumentError+
|
|
16
|
+
# elsewhere should propagate unchanged.
|
|
17
|
+
def self.with_boundary
|
|
46
18
|
yield
|
|
47
19
|
rescue ::ArgumentError => e
|
|
48
20
|
raise InvalidTypeError, e.message
|
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
|