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
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -7,28 +7,9 @@ require_relative "ext_types"
|
|
|
7
7
|
|
|
8
8
|
module Kobako
|
|
9
9
|
module Codec
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# The codec backbone is the official +msgpack+ gem: integers, floats,
|
|
14
|
-
# strings, arrays, and maps go through the gem's narrowest-encoding
|
|
15
|
-
# logic; the two kobako-specific ext types (0x00 Symbol, 0x01
|
|
16
|
-
# Capability Handle) are registered by ExtTypes on the process-wide
|
|
17
|
-
# factory.
|
|
18
|
-
#
|
|
19
|
-
# Public API is a single function — +.encode+. The codec is stateless;
|
|
20
|
-
# there is no buffer accumulator and no streaming write API. Callers
|
|
21
|
-
# that need to concatenate multiple encodings build the bytes
|
|
22
|
-
# themselves.
|
|
23
|
-
module Encoder
|
|
24
|
-
# Encode +value+ to wire bytes (binary-encoded String).
|
|
25
|
-
# SPEC's 11-entry type mapping is a closed set: a value outside it is
|
|
26
|
-
# rejected as +UnsupportedTypeError+ by the factory's +BasicObject+ guard
|
|
27
|
-
# (ExtTypes#register_unrepresentable), which raises before the msgpack
|
|
28
|
-
# gem can route the value through +to_msgpack+ — so a permissive
|
|
29
|
-
# +method_missing+ object cannot answer that probe and mis-encode. The
|
|
30
|
-
# rescue below maps the two violations the guard does not reach onto the
|
|
31
|
-
# same error: an integer outside i64..u64 (+RangeError+) and any
|
|
10
|
+
module Encoder # :nodoc:
|
|
11
|
+
# The rescue maps the two violations the factory's +BasicObject+ guard
|
|
12
|
+
# does not reach: an integer outside i64..u64 (+RangeError+) and any
|
|
32
13
|
# packer-internal +NoMethodError+.
|
|
33
14
|
#
|
|
34
15
|
# The caller bounds +value+'s nesting first (Nesting): the packer takes
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -2,34 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Codec
|
|
5
|
-
|
|
6
|
-
#
|
|
7
|
-
# The wire codec implements the binary contract pinned in
|
|
8
|
-
# {docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping.
|
|
9
|
-
# Every wire violation surfaces as a
|
|
10
|
-
# subclass of Error so callers can pattern-match on the specific
|
|
11
|
-
# fault while still rescuing all codec faults via this base class.
|
|
12
|
-
#
|
|
13
|
-
# Higher layers (e.g. the Sandbox dispatch loop) translate these into
|
|
14
|
-
# the public Kobako::SandboxError / Kobako::TrapError taxonomy.
|
|
15
|
-
class Error < StandardError; end
|
|
5
|
+
class Error < StandardError; end # :nodoc:
|
|
6
|
+
class TruncatedInputError < Error; end # :nodoc:
|
|
16
7
|
|
|
17
|
-
#
|
|
18
|
-
class
|
|
8
|
+
# Also raised on encode for a value nested past what the packer can walk.
|
|
9
|
+
class InvalidTypeError < Error; end # :nodoc:
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# or, on encode, a value nesting past what the packer can walk.
|
|
23
|
-
class InvalidTypeError < Error; end
|
|
24
|
-
|
|
25
|
-
# A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
|
|
26
|
-
# payload was not valid UTF-8 — both are wire violations per SPEC.
|
|
27
|
-
class InvalidEncodingError < Error; end
|
|
28
|
-
|
|
29
|
-
# The encoder was handed a Ruby object whose type has no wire
|
|
30
|
-
# representation (e.g. Range, Time). Higher layers may catch this
|
|
31
|
-
# and re-route the value through Handle allocation, but at the
|
|
32
|
-
# codec level it is a hard error.
|
|
33
|
-
class UnsupportedTypeError < Error; end
|
|
11
|
+
class InvalidEncodingError < Error; end # :nodoc:
|
|
12
|
+
class UnsupportedTypeError < Error; end # :nodoc:
|
|
34
13
|
end
|
|
35
14
|
end
|
|
@@ -8,21 +8,16 @@ require_relative "state"
|
|
|
8
8
|
require_relative "../handle"
|
|
9
9
|
|
|
10
10
|
module Kobako
|
|
11
|
-
module Codec
|
|
12
|
-
|
|
13
|
-
# ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types)
|
|
14
|
-
# as pure functions: per-operation decode state is threaded in as an
|
|
15
|
-
# argument, so the module itself holds nothing. #build_factory assembles
|
|
16
|
-
# the one +MessagePack::Factory+ these conversions are registered on.
|
|
17
|
-
module ExtTypes
|
|
11
|
+
module Codec # :nodoc:
|
|
12
|
+
module ExtTypes # :nodoc:
|
|
18
13
|
# MessagePack ext type code reserved for Symbol
|
|
19
|
-
# (
|
|
20
|
-
#
|
|
14
|
+
# (docs/wire/payload-msgpack.md § ext 0x00 — Symbol). Module-private —
|
|
15
|
+
# mirrors +codec::EXT_SYMBOL+ on the
|
|
21
16
|
# Rust side.
|
|
22
17
|
EXT_SYMBOL = 0x00
|
|
23
18
|
# MessagePack ext type code reserved for Capability Handle
|
|
24
|
-
# (
|
|
25
|
-
#
|
|
19
|
+
# (docs/wire/payload-msgpack.md § ext 0x01 — Handle). Module-private —
|
|
20
|
+
# mirrors +codec::EXT_HANDLE+ on the
|
|
26
21
|
# Rust side.
|
|
27
22
|
EXT_HANDLE = 0x01
|
|
28
23
|
private_constant :EXT_SYMBOL, :EXT_HANDLE
|
|
@@ -31,19 +26,13 @@ module Kobako
|
|
|
31
26
|
# never emitted (the guard's packer always raises) and never decoded
|
|
32
27
|
# (no unpacker is registered, so the id stays an UnknownExtTypeError on
|
|
33
28
|
# the wire), so it is not a wire ext type: deliberately not named
|
|
34
|
-
# +EXT_*+ like the two real ext codes, since it has no Rust-side mirror
|
|
35
|
-
# and must stay outside the wire-symmetry inventory.
|
|
29
|
+
# +EXT_*+ like the two real ext codes, since it has no Rust-side mirror.
|
|
36
30
|
UNREPRESENTABLE_GUARD_ID = 0x7F
|
|
37
31
|
private_constant :UNREPRESENTABLE_GUARD_ID
|
|
38
32
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# the unrepresentable-value guard registered, frozen because
|
|
43
|
-
# registration is its only mutation and happens exactly once. The
|
|
44
|
-
# stateful conversions resolve their per-operation state at call time,
|
|
45
|
-
# so one registered factory serves every thread.
|
|
46
|
-
def build_factory
|
|
33
|
+
# The stateful conversions resolve their per-operation state at call
|
|
34
|
+
# time, so one frozen factory serves every thread.
|
|
35
|
+
def self.build_factory
|
|
47
36
|
factory = MessagePack::Factory.new
|
|
48
37
|
register_symbol(factory)
|
|
49
38
|
register_handle(factory)
|
|
@@ -51,89 +40,78 @@ module Kobako
|
|
|
51
40
|
factory.freeze
|
|
52
41
|
end
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
symbol.name
|
|
57
|
-
end
|
|
43
|
+
class << self
|
|
44
|
+
private
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# would otherwise apply. The re-tag step lives here because the
|
|
63
|
-
# msgpack ext-type unpacker hands us binary bytes; the assertion
|
|
64
|
-
# itself is shared with Decoder via Utils.assert_utf8!. The
|
|
65
|
-
# +"Symbol"+ label keeps the error message in Ruby vocabulary
|
|
66
|
-
# rather than wire-ext-code vocabulary.
|
|
67
|
-
def unpack_symbol(payload)
|
|
68
|
-
name = payload.b.force_encoding(Encoding::UTF_8)
|
|
69
|
-
Utils.assert_utf8!(name, "Symbol payload")
|
|
70
|
-
name.to_sym
|
|
71
|
-
end
|
|
46
|
+
def pack_symbol(symbol)
|
|
47
|
+
symbol.name
|
|
48
|
+
end
|
|
72
49
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
50
|
+
# Refuses the binary-encoding fallback that msgpack-gem's default
|
|
51
|
+
# unpacker would otherwise apply to invalid bytes.
|
|
52
|
+
def unpack_symbol(payload)
|
|
53
|
+
name = payload.b.force_encoding(Encoding::UTF_8)
|
|
54
|
+
Utils.assert_utf8!(name, "Symbol payload")
|
|
55
|
+
name.to_sym
|
|
56
|
+
end
|
|
78
57
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# into a wire-layer +InvalidTypeError+ via Codec::Utils.with_boundary.
|
|
83
|
-
# The Value Object owns the id-range contract; this method only
|
|
84
|
-
# owns the frame shape. Records the Handle sighting on +state+ so a
|
|
85
|
-
# Handle-free decode can skip the downstream resolution walk.
|
|
86
|
-
def unpack_handle(payload, state)
|
|
87
|
-
state.record_handle!
|
|
88
|
-
bytes = payload.b
|
|
89
|
-
raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
58
|
+
def pack_handle(handle)
|
|
59
|
+
[handle.id].pack("N")
|
|
60
|
+
end
|
|
90
61
|
|
|
91
|
-
id
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
# Handle owns the id-range contract; this method owns only the frame
|
|
63
|
+
# shape. The sighting is recorded so a Handle-free decode can skip the
|
|
64
|
+
# downstream resolution walk.
|
|
65
|
+
def unpack_handle(payload, state)
|
|
66
|
+
state.record_handle!
|
|
67
|
+
bytes = payload.b
|
|
68
|
+
raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
packer: ->(symbol) { ExtTypes.pack_symbol(symbol) },
|
|
99
|
-
unpacker: ->(payload) { ExtTypes.unpack_symbol(payload) }
|
|
100
|
-
)
|
|
101
|
-
end
|
|
70
|
+
id = bytes.unpack1("N") # : Integer
|
|
71
|
+
Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
|
|
72
|
+
end
|
|
102
73
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
74
|
+
def register_symbol(factory)
|
|
75
|
+
factory.register_type(
|
|
76
|
+
EXT_SYMBOL, Symbol,
|
|
77
|
+
packer: ->(symbol) { pack_symbol(symbol) },
|
|
78
|
+
unpacker: ->(payload) { unpack_symbol(payload) }
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def register_handle(factory)
|
|
83
|
+
factory.register_type(
|
|
84
|
+
EXT_HANDLE, Kobako::Handle,
|
|
85
|
+
packer: ->(handle) { pack_handle(handle) },
|
|
86
|
+
unpacker: ->(payload) { unpack_handle(payload, State.current) }
|
|
87
|
+
)
|
|
88
|
+
end
|
|
110
89
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
90
|
+
# A catch-all packer that rejects any value with no wire representation
|
|
91
|
+
# as +UnsupportedTypeError+. Registered on +BasicObject+ so it also covers
|
|
92
|
+
# BasicObject-based proxies; the narrower Symbol / Handle
|
|
93
|
+
# registrations still win by most-specific match, and native types never
|
|
94
|
+
# reach it. Packer-only: the guard never writes bytes, so its id is inert
|
|
95
|
+
# and the decode surface stays fail-closed.
|
|
96
|
+
#
|
|
97
|
+
# This makes the host's non-wire detection a positive allowlist — a value
|
|
98
|
+
# outside the type set is rejected here rather than routed to +to_msgpack+
|
|
99
|
+
# — matching the guest's classname allowlist and the Rust codec's closed
|
|
100
|
+
# +Value+ enum. Without it, a value with a permissive +method_missing+
|
|
101
|
+
# answers the codec's +to_msgpack+ probe and mis-encodes as +nil+ instead
|
|
102
|
+
# of crossing as a Capability Handle.
|
|
103
|
+
def register_unrepresentable(factory)
|
|
104
|
+
factory.register_type(
|
|
105
|
+
UNREPRESENTABLE_GUARD_ID, BasicObject,
|
|
106
|
+
packer: ->(_value) { raise UnsupportedTypeError, "value has no wire representation" }
|
|
107
|
+
)
|
|
108
|
+
end
|
|
129
109
|
end
|
|
130
110
|
end
|
|
131
111
|
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
# work.
|
|
136
|
-
FACTORY = ExtTypes.build_factory
|
|
112
|
+
# Registered once at load and only read afterwards, so every thread
|
|
113
|
+
# shares it.
|
|
114
|
+
FACTORY = ExtTypes.build_factory # :nodoc:
|
|
137
115
|
private_constant :FACTORY
|
|
138
116
|
end
|
|
139
117
|
end
|
|
@@ -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
|