kobako 0.9.2 → 0.11.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 +32 -0
- data/Cargo.lock +3 -1
- data/README.md +47 -19
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +12 -2
- data/ext/kobako/src/runtime/ambient.rs +1 -1
- data/ext/kobako/src/runtime/cache.rs +170 -6
- data/ext/kobako/src/runtime/capture.rs +1 -1
- data/ext/kobako/src/runtime/config.rs +3 -4
- data/ext/kobako/src/runtime/dispatch.rs +8 -8
- data/ext/kobako/src/runtime/exports.rs +32 -21
- data/ext/kobako/src/runtime/instance_pre.rs +97 -0
- data/ext/kobako/src/runtime/invocation.rs +36 -93
- data/ext/kobako/src/runtime/trap.rs +5 -5
- data/ext/kobako/src/runtime.rs +389 -403
- data/ext/kobako/src/snapshot.rs +2 -2
- data/lib/kobako/capture.rb +5 -7
- data/lib/kobako/catalog/handles.rb +28 -39
- data/lib/kobako/catalog/namespaces.rb +31 -20
- data/lib/kobako/catalog/snippets.rb +18 -16
- data/lib/kobako/codec/decoder.rb +5 -1
- data/lib/kobako/codec/utils.rb +6 -9
- data/lib/kobako/errors.rb +40 -36
- data/lib/kobako/handle.rb +2 -3
- data/lib/kobako/namespace.rb +17 -6
- data/lib/kobako/outcome.rb +12 -14
- data/lib/kobako/pool.rb +176 -0
- data/lib/kobako/sandbox.rb +68 -88
- data/lib/kobako/sandbox_options.rb +5 -9
- data/lib/kobako/snapshot.rb +2 -4
- data/lib/kobako/snippet/binary.rb +1 -3
- data/lib/kobako/snippet/source.rb +1 -2
- data/lib/kobako/snippet.rb +1 -2
- data/lib/kobako/transport/dispatcher.rb +39 -38
- data/lib/kobako/transport/request.rb +1 -1
- data/lib/kobako/transport/run.rb +23 -28
- data/lib/kobako/transport/yielder.rb +11 -17
- data/lib/kobako/transport.rb +2 -3
- data/lib/kobako/usage.rb +10 -13
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +16 -1
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/namespace.rbs +2 -0
- data/sig/kobako/pool.rbs +44 -0
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/transport/dispatcher.rbs +2 -0
- metadata +4 -1
data/ext/kobako/src/snapshot.rs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
//! Every successful `Kobako::Runtime#eval` / `#run` returns one of these.
|
|
4
4
|
//! It carries every observable the host needs to surface after a guest
|
|
5
5
|
//! invocation: the OUTCOME_BUFFER bytes (`return_bytes`), the captured
|
|
6
|
-
//! stdout / stderr byte slices with their truncation flags
|
|
7
|
-
//! the wall-clock + memory-peak figures from `Kobako::Usage
|
|
6
|
+
//! stdout / stderr byte slices with their truncation flags, and
|
|
7
|
+
//! the wall-clock + memory-peak figures from `Kobako::Usage`.
|
|
8
8
|
//!
|
|
9
9
|
//! Ruby callers see the seven raw readers registered below; the helper
|
|
10
10
|
//! methods that pack them into `Kobako::Capture` / `Kobako::Usage`
|
data/lib/kobako/capture.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Kobako
|
|
|
4
4
|
# Host-side captured prefix of guest stdout / stderr produced during a
|
|
5
5
|
# single +Kobako::Sandbox+ invocation, paired with the truncation flag
|
|
6
6
|
# the WASI pipe sets when the guest wrote past the configured per-channel
|
|
7
|
-
# cap
|
|
7
|
+
# cap.
|
|
8
8
|
#
|
|
9
9
|
# Immutable value object: the captured bytes and the truncation flag
|
|
10
10
|
# always travel together and the instance is frozen on construction.
|
|
@@ -30,14 +30,12 @@ module Kobako
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Returns +true+ iff the underlying capture channel exceeded its
|
|
33
|
-
# configured cap during the originating +Sandbox+ invocation
|
|
34
|
-
# ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
|
|
33
|
+
# configured cap during the originating +Sandbox+ invocation.
|
|
35
34
|
def truncated? = @truncated
|
|
36
35
|
|
|
37
|
-
# Pre-invocation sentinel
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# yet".
|
|
36
|
+
# Pre-invocation sentinel. Empty UTF-8 bytes and +truncated? == false+;
|
|
37
|
+
# reused by every fresh +Sandbox+ and by +Sandbox+ between invocations
|
|
38
|
+
# to denote "no capture yet".
|
|
41
39
|
EMPTY = new(bytes: "", truncated: false)
|
|
42
40
|
end
|
|
43
41
|
end
|
|
@@ -5,41 +5,29 @@ require_relative "../handle"
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Catalog
|
|
7
7
|
# Host-side mapping from opaque integer Handle IDs to Ruby objects.
|
|
8
|
-
# The table is owned by +Kobako::Sandbox+
|
|
9
|
-
# ({docs/behavior.md B-19}[link:../../../docs/behavior.md]) and injected
|
|
8
|
+
# The table is owned by +Kobako::Sandbox+ and injected
|
|
10
9
|
# into the per-Sandbox +Kobako::Catalog::Namespaces+ so guest→host dispatch
|
|
11
10
|
# resolves Handle targets and arguments against the same table that
|
|
12
|
-
# host→guest wire encoding allocates into
|
|
13
|
-
# ({docs/behavior.md B-14, B-34}[link:../../../docs/behavior.md]).
|
|
11
|
+
# host→guest wire encoding allocates into.
|
|
14
12
|
#
|
|
15
|
-
# Lifecycle invariants
|
|
13
|
+
# Lifecycle invariants:
|
|
16
14
|
#
|
|
17
|
-
# -
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# +#alloc+.
|
|
15
|
+
# - Handle IDs are allocated by a monotonically increasing counter
|
|
16
|
+
# scoped to a single invocation. The first ID issued in an
|
|
17
|
+
# invocation is 1; ID 0 is reserved as the invalid sentinel and is
|
|
18
|
+
# never returned by +#alloc+.
|
|
22
19
|
#
|
|
23
|
-
# -
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# allocation source (B-14 Service return or B-34 host-injected
|
|
20
|
+
# - At every invocation boundary (via +#reset!+), every Handle issued
|
|
21
|
+
# under the old state becomes invalid. Reset applies uniformly
|
|
22
|
+
# regardless of allocation source (Service return or host-injected
|
|
27
23
|
# argument).
|
|
28
24
|
#
|
|
29
|
-
# -
|
|
30
|
-
#
|
|
31
|
-
# immediately — no silent truncation, no wrap, no ID reuse.
|
|
25
|
+
# - The cap is +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap
|
|
26
|
+
# raises immediately — no silent truncation, no wrap, no ID reuse.
|
|
32
27
|
class Handles
|
|
33
|
-
# Reflective gadget types that are never minted as a Capability Handle
|
|
34
|
-
# ({docs/behavior.md B-43}[link:../../../docs/behavior.md]): wrapping a
|
|
35
|
-
# +Binding+ / +Method+ / +UnboundMethod+ would hand the guest a callable
|
|
36
|
-
# proxy onto host reflection (a returned +Binding+ reaches +Binding#eval+).
|
|
37
|
-
UNWRAPPABLE_TYPES = [Binding, Method, UnboundMethod].freeze
|
|
38
|
-
private_constant :UNWRAPPABLE_TYPES
|
|
39
|
-
|
|
40
28
|
# Build a fresh, empty table. +next_id+ is an internal seam that
|
|
41
|
-
# sets the starting value of the monotonic counter (defaults to 1
|
|
42
|
-
#
|
|
29
|
+
# sets the starting value of the monotonic counter (defaults to 1);
|
|
30
|
+
# tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
|
|
43
31
|
# the cap-exhaustion path without 2³¹ allocations.
|
|
44
32
|
def initialize(next_id: 1)
|
|
45
33
|
@entries = {} # : Hash[Integer, untyped]
|
|
@@ -52,8 +40,7 @@ module Kobako
|
|
|
52
40
|
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
53
41
|
# +Kobako::HandlerExhaustedError+ if the next ID would exceed the
|
|
54
42
|
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
55
|
-
# and the allocator share the same invariant
|
|
56
|
-
# ({docs/behavior.md B-21}[link:../../../docs/behavior.md]).
|
|
43
|
+
# and the allocator share the same invariant.
|
|
57
44
|
#
|
|
58
45
|
# Returning a Handle (rather than a bare Integer id) keeps the
|
|
59
46
|
# allocator's output a domain entity; +Kobako::Handle.restore+
|
|
@@ -76,9 +63,8 @@ module Kobako
|
|
|
76
63
|
@entries[id]
|
|
77
64
|
end
|
|
78
65
|
|
|
79
|
-
# Clear all entries AND reset the counter to 1. Called at the
|
|
80
|
-
# boundary by +Kobako::Sandbox
|
|
81
|
-
# {docs/behavior.md B-19}[link:../../../docs/behavior.md]. Returns +self+.
|
|
66
|
+
# Clear all entries AND reset the counter to 1. Called at the
|
|
67
|
+
# per-invocation boundary by +Kobako::Sandbox+. Returns +self+.
|
|
82
68
|
def reset!
|
|
83
69
|
@entries.clear
|
|
84
70
|
@next_id = 1
|
|
@@ -96,17 +82,20 @@ module Kobako
|
|
|
96
82
|
|
|
97
83
|
private
|
|
98
84
|
|
|
99
|
-
# Refuse to mint a Capability Handle for a reflective gadget
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
85
|
+
# Refuse to mint a Capability Handle for a reflective gadget:
|
|
86
|
+
# a +Binding+ / +Method+ / +UnboundMethod+ would hand the guest a
|
|
87
|
+
# callable proxy onto host reflection (a returned +Binding+ reaches
|
|
88
|
+
# +Binding#eval+). Raising here keeps the rule at the single mint
|
|
89
|
+
# point, so it holds on both the Service-return and the +#run+
|
|
90
|
+
# host→guest auto-wrap paths.
|
|
103
91
|
def reject_unwrappable!(object)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
case object
|
|
93
|
+
when Binding, Method, UnboundMethod
|
|
94
|
+
raise SandboxError, "a #{object.class} cannot cross as a Capability Handle"
|
|
95
|
+
end
|
|
107
96
|
end
|
|
108
97
|
|
|
109
|
-
# Guard {#alloc} against issuing an ID past the
|
|
98
|
+
# Guard {#alloc} against issuing an ID past the cap. Returns +nil+
|
|
110
99
|
# on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
|
|
111
100
|
def ensure_capacity!
|
|
112
101
|
cap = Kobako::Handle::MAX_ID
|
|
@@ -9,8 +9,7 @@ module Kobako
|
|
|
9
9
|
module Catalog
|
|
10
10
|
# Kobako::Catalog::Namespaces — per-Sandbox registry of
|
|
11
11
|
# +Kobako::Namespace+ entities. Holds the Namespace / Member bindings
|
|
12
|
-
# and the preamble emitted on Frame 1
|
|
13
|
-
# ({docs/behavior.md B-07..B-11}[link:../../../docs/behavior.md]).
|
|
12
|
+
# and the preamble emitted on Frame 1.
|
|
14
13
|
#
|
|
15
14
|
# Public API:
|
|
16
15
|
#
|
|
@@ -24,29 +23,27 @@ module Kobako
|
|
|
24
23
|
# +Kobako::Transport::Dispatcher+'s responsibility — the Dispatcher
|
|
25
24
|
# receives this registry and the +Catalog::Handles+ as arguments from
|
|
26
25
|
# the +Runtime#on_dispatch+ Proc that +Kobako::Sandbox#initialize+
|
|
27
|
-
# installs
|
|
28
|
-
# The registry holds an injected +Catalog::Handles+ reference so
|
|
26
|
+
# installs. The registry holds an injected +Catalog::Handles+ reference so
|
|
29
27
|
# dispatch target resolution and host→guest auto-wrap share the same
|
|
30
|
-
# Sandbox-owned allocator
|
|
28
|
+
# Sandbox-owned allocator.
|
|
31
29
|
class Namespaces
|
|
32
30
|
# Build a fresh registry. +handler+ is an internal seam that injects
|
|
33
31
|
# a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
|
|
34
|
-
# is pinned near +MAX_ID+ to exercise the
|
|
32
|
+
# is pinned near +MAX_ID+ to exercise the cap-exhaustion path
|
|
35
33
|
# without 2³¹ allocations. Production callers leave it at the default.
|
|
36
34
|
def initialize(handler: Catalog::Handles.new)
|
|
37
35
|
@namespaces = {} # : Hash[String, Kobako::Namespace]
|
|
38
36
|
@handler = handler
|
|
39
37
|
@sealed = false
|
|
38
|
+
@encoded = nil # : String?
|
|
40
39
|
end
|
|
41
40
|
|
|
42
|
-
# Declare or retrieve the Namespace named +name+ (idempotent
|
|
43
|
-
# {docs/behavior.md B-10}[link:../../../docs/behavior.md]).
|
|
41
|
+
# Declare or retrieve the Namespace named +name+ (idempotent).
|
|
44
42
|
# +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
|
|
45
43
|
# +Namespace::NAME_PATTERN+). Returns the +Kobako::Namespace+ for that
|
|
46
44
|
# name, creating it if it does not exist. Raises +ArgumentError+ when
|
|
47
45
|
# +name+ is malformed, or when called after the owning Sandbox has been
|
|
48
|
-
# sealed by its first invocation
|
|
49
|
-
# ({docs/behavior.md B-07}[link:../../../docs/behavior.md]).
|
|
46
|
+
# sealed by its first invocation.
|
|
50
47
|
def define(name)
|
|
51
48
|
raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
|
|
52
49
|
|
|
@@ -73,21 +70,35 @@ module Kobako
|
|
|
73
70
|
namespace.fetch(member_name)
|
|
74
71
|
end
|
|
75
72
|
|
|
76
|
-
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
73
|
+
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery.
|
|
74
|
+
# Routes through {Kobako::Codec::Encoder} like every other host-side
|
|
75
|
+
# wire encode so there is a single codec path; the preamble carries
|
|
76
|
+
# only Strings and Arrays, so none of the kobako ext types actually
|
|
77
|
+
# fire. Structure: +[["Namespace", ["MemberA", "MemberB"]], ...]+.
|
|
78
|
+
# Returns a binary +String+ of msgpack bytes.
|
|
79
|
+
#
|
|
80
|
+
# Once sealed, the bytes are computed once and reused for every
|
|
81
|
+
# subsequent invocation: sealing freezes Service registration at the
|
|
82
|
+
# first invocation, so the preamble is exactly the bindings that
|
|
83
|
+
# existed at that moment — a bind reaching a +Kobako::Namespace+
|
|
84
|
+
# after the seal raises +ArgumentError+ and never alters Frame 1.
|
|
83
85
|
def encode
|
|
84
|
-
|
|
86
|
+
return @encoded if @encoded
|
|
87
|
+
|
|
88
|
+
bytes = Codec::Encoder.encode(@namespaces.values.map(&:to_preamble)).freeze
|
|
89
|
+
@encoded = bytes if @sealed
|
|
90
|
+
bytes
|
|
85
91
|
end
|
|
86
92
|
|
|
87
|
-
# Mark the registry as sealed
|
|
88
|
-
#
|
|
93
|
+
# Mark the registry as sealed and propagate the seal to every
|
|
94
|
+
# declared +Kobako::Namespace+. Called by +Sandbox+ on the first
|
|
95
|
+
# invocation. After sealing, both #define and +Namespace#bind+
|
|
96
|
+
# raise ArgumentError. Idempotent.
|
|
89
97
|
def seal!
|
|
98
|
+
return self if @sealed
|
|
99
|
+
|
|
90
100
|
@sealed = true
|
|
101
|
+
@namespaces.each_value(&:seal!)
|
|
91
102
|
self
|
|
92
103
|
end
|
|
93
104
|
|
|
@@ -6,8 +6,7 @@ require_relative "../snippet"
|
|
|
6
6
|
module Kobako
|
|
7
7
|
module Catalog
|
|
8
8
|
# Kobako::Catalog::Snippets — per-Sandbox insertion-ordered registry
|
|
9
|
-
# of preloaded snippets
|
|
10
|
-
# ({docs/behavior.md B-32 / B-33}[link:../../../docs/behavior.md]).
|
|
9
|
+
# of preloaded snippets.
|
|
11
10
|
#
|
|
12
11
|
# Entries replay against the fresh +mrb_state+ before per-invocation
|
|
13
12
|
# source / entrypoint resolution. Each +Snippet::Source+ entry's +name+
|
|
@@ -15,22 +14,21 @@ module Kobako
|
|
|
15
14
|
# +debug_info+ that surfaces in every backtrace frame originating from
|
|
16
15
|
# the snippet as +(snippet:Name):line+. Duplicate names within the
|
|
17
16
|
# +code:+ form would produce ambiguous attribution and are rejected at
|
|
18
|
-
# registration time
|
|
19
|
-
# ({docs/behavior.md E-33}[link:../../../docs/behavior.md]).
|
|
17
|
+
# registration time.
|
|
20
18
|
# +Snippet::Binary+ entries carry no host-side name — their canonical
|
|
21
19
|
# name lives in the bytecode's +debug_info+ and is read by the guest at
|
|
22
20
|
# load time; the host does not extract it.
|
|
23
21
|
#
|
|
24
|
-
# Sealing
|
|
22
|
+
# Sealing is governed by the owning Sandbox — the registry itself
|
|
25
23
|
# is append-only and exposes no mutation API beyond +#register+; the
|
|
26
24
|
# Sandbox guards +#register+ behind the seal check before delegating.
|
|
27
25
|
class Snippets
|
|
28
|
-
# Ruby constant-name pattern enforced on snippet names
|
|
29
|
-
# ({docs/behavior.md E-34}[link:../../../docs/behavior.md]).
|
|
26
|
+
# Ruby constant-name pattern enforced on snippet names.
|
|
30
27
|
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
31
28
|
|
|
32
29
|
def initialize
|
|
33
30
|
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
|
|
31
|
+
@encoded = nil # : String?
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
# Serialize the registered snippets to wire bytes. Each entry
|
|
@@ -42,12 +40,17 @@ module Kobako
|
|
|
42
40
|
# carriers — this collection-tier method reads their attributes
|
|
43
41
|
# externally via +entry_payload+ rather than asking each entry to
|
|
44
42
|
# self-encode.
|
|
43
|
+
#
|
|
44
|
+
# The bytes are memoized — the table is replayed verbatim on every
|
|
45
|
+
# invocation after sealing, so Frame 3 never changes between
|
|
46
|
+
# encodes; {#register} drops the memo while the table is still open.
|
|
45
47
|
def encode
|
|
46
|
-
|
|
48
|
+
return @encoded if @encoded
|
|
49
|
+
|
|
50
|
+
@encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
|
|
47
51
|
end
|
|
48
52
|
|
|
49
|
-
# Register one preloaded snippet in either of two forms
|
|
50
|
-
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
53
|
+
# Register one preloaded snippet in either of two forms.
|
|
51
54
|
#
|
|
52
55
|
# * Source form +register(code: src, name: Name)+ — +src+ is the
|
|
53
56
|
# mruby source as a String; the bytes are re-encoded as UTF-8
|
|
@@ -58,16 +61,15 @@ module Kobako
|
|
|
58
61
|
# precompiled RITE bytecode as a String, duplicated and forced
|
|
59
62
|
# to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
|
|
60
63
|
# +nil+ — bytecode entries are anonymous on the host side; any
|
|
61
|
-
# structural validation
|
|
62
|
-
# ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
|
|
63
|
-
# is deferred to the guest at first replay.
|
|
64
|
+
# structural validation is deferred to the guest at first replay.
|
|
64
65
|
#
|
|
65
66
|
# The two forms are mutually exclusive: shape validation lives
|
|
66
67
|
# here so callers (chiefly +Kobako::Sandbox#preload+) collapse to
|
|
67
68
|
# a single delegation. Raises +ArgumentError+ on mixed forms,
|
|
68
|
-
# missing keywords, wrong types, malformed +name
|
|
69
|
-
# duplicate +code:+ +name
|
|
69
|
+
# missing keywords, wrong types, malformed +name+, or
|
|
70
|
+
# duplicate +code:+ +name+.
|
|
70
71
|
def register(code: nil, name: nil, binary: nil)
|
|
72
|
+
@encoded = nil
|
|
71
73
|
if binary
|
|
72
74
|
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
73
75
|
|
|
@@ -81,7 +83,7 @@ module Kobako
|
|
|
81
83
|
|
|
82
84
|
# Source-form register path. Delegates argument-shape checks to
|
|
83
85
|
# +ensure_source_args!+ (which returns the narrowed +[code, name]+
|
|
84
|
-
# pair), normalises +name+ to a Symbol, rejects duplicates
|
|
86
|
+
# pair), normalises +name+ to a Symbol, rejects duplicates,
|
|
85
87
|
# and appends the Source entry.
|
|
86
88
|
def register_source!(code, name)
|
|
87
89
|
code, name = ensure_source_args!(code, name)
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -64,7 +64,11 @@ module Kobako
|
|
|
64
64
|
case value
|
|
65
65
|
when String then Utils.assert_utf8!(value, "str payload") if value.encoding == Encoding::UTF_8
|
|
66
66
|
when Array then value.each { |v| validate_utf8!(v) }
|
|
67
|
-
when Hash
|
|
67
|
+
when Hash
|
|
68
|
+
value.each do |key, val|
|
|
69
|
+
validate_utf8!(key)
|
|
70
|
+
validate_utf8!(val)
|
|
71
|
+
end
|
|
68
72
|
end
|
|
69
73
|
end
|
|
70
74
|
end
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -19,8 +19,7 @@ module Kobako
|
|
|
19
19
|
# - Representability predicate ({representable?}) and the symmetric
|
|
20
20
|
# host→guest +#run+ argument walk ({deep_wrap}) used by
|
|
21
21
|
# +Kobako::Transport::Run#encode+ to route non-representable leaves
|
|
22
|
-
# through the Sandbox's +Kobako::Catalog::Handles
|
|
23
|
-
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]).
|
|
22
|
+
# through the Sandbox's +Kobako::Catalog::Handles+.
|
|
24
23
|
#
|
|
25
24
|
# All helpers are pure — they only inspect inputs, never mutate
|
|
26
25
|
# them — except {deep_wrap}, whose only side effect is allocating
|
|
@@ -84,8 +83,7 @@ module Kobako
|
|
|
84
83
|
|
|
85
84
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
86
85
|
# leaf that fails {representable?} with a +Kobako::Handle+
|
|
87
|
-
# allocated from +handler
|
|
88
|
-
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
|
|
86
|
+
# allocated from +handler+. The
|
|
89
87
|
# walk only descends through representable container shapes
|
|
90
88
|
# (Array, Hash) one structural level at a time; a non-representable
|
|
91
89
|
# leaf is wrapped as-is without inspecting its internal structure.
|
|
@@ -116,8 +114,7 @@ module Kobako
|
|
|
116
114
|
|
|
117
115
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
118
116
|
# +Kobako::Handle+ leaf with the host-side object +handler+ resolves
|
|
119
|
-
# it to
|
|
120
|
-
# The symmetric inverse of {deep_wrap}: that walk allocates objects
|
|
117
|
+
# it to. The symmetric inverse of {deep_wrap}: that walk allocates objects
|
|
121
118
|
# into Handles on the host→guest argument path; this walk resolves
|
|
122
119
|
# Handles back to their objects on every guest→host value path — the
|
|
123
120
|
# +#eval+ / +#run+ result and the yield-block result alike. The walk
|
|
@@ -126,11 +123,11 @@ module Kobako
|
|
|
126
123
|
# unchanged.
|
|
127
124
|
#
|
|
128
125
|
# +value+ is a decoded Ruby value (a Handle here is a wire-decoded
|
|
129
|
-
# +Kobako::Handle+, never a guest-forged one
|
|
126
|
+
# +Kobako::Handle+, never a guest-forged one); +handler+ must
|
|
130
127
|
# respond to +#fetch(id) -> object+ (a host-side
|
|
131
128
|
# +Kobako::Catalog::Handles+). +handler.fetch+ raises
|
|
132
|
-
# +Kobako::SandboxError+ for an id with no live binding,
|
|
133
|
-
# corrupted-runtime fallback
|
|
129
|
+
# +Kobako::SandboxError+ for an id with no live binding, the
|
|
130
|
+
# corrupted-runtime fallback.
|
|
134
131
|
def deep_restore(value, handler)
|
|
135
132
|
case value
|
|
136
133
|
when ::Array then value.map { |element| Utils.deep_restore(element, handler) }
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# Top-level Kobako namespace.
|
|
4
4
|
module Kobako
|
|
5
|
-
# Error taxonomy
|
|
5
|
+
# Error taxonomy.
|
|
6
6
|
#
|
|
7
7
|
# Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
|
|
8
8
|
# exactly one of three invocation-outcome classes. Attribution is decided after the
|
|
9
|
-
# guest binary returns control to the host
|
|
10
|
-
#
|
|
9
|
+
# guest binary returns control to the host: first the Wasm-trap layer, then
|
|
10
|
+
# the outcome-envelope tag.
|
|
11
11
|
#
|
|
12
12
|
# Three invocation-outcome branches:
|
|
13
13
|
#
|
|
@@ -21,20 +21,21 @@ module Kobako
|
|
|
21
21
|
# call that failed and was not rescued inside the
|
|
22
22
|
# script).
|
|
23
23
|
#
|
|
24
|
-
#
|
|
24
|
+
# Two further branches sit outside the invocation taxonomy:
|
|
25
25
|
#
|
|
26
26
|
# * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
|
|
27
27
|
# when the wasm runtime cannot be built from the
|
|
28
|
-
# configured +wasm_path+ before any invocation runs
|
|
29
|
-
# ({docs/behavior.md E-40 / E-41}[link:../../docs/behavior.md]).
|
|
28
|
+
# configured +wasm_path+ before any invocation runs.
|
|
30
29
|
# Not an invocation outcome, so it never passes
|
|
31
30
|
# through the two-step attribution decision.
|
|
31
|
+
# * {PoolTimeoutError} — pool checkout layer. Raised by `Kobako::Pool#with`
|
|
32
|
+
# when the checkout wait exceeds +checkout_timeout+.
|
|
32
33
|
#
|
|
33
|
-
#
|
|
34
|
+
# Named subclasses:
|
|
34
35
|
#
|
|
35
36
|
# * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
|
|
36
|
-
# at +wasm_path
|
|
37
|
-
# * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit
|
|
37
|
+
# at +wasm_path+.
|
|
38
|
+
# * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit.
|
|
38
39
|
|
|
39
40
|
# Base for all kobako-raised errors so callers that want to ignore the
|
|
40
41
|
# taxonomy can rescue a single class.
|
|
@@ -43,13 +44,13 @@ module Kobako
|
|
|
43
44
|
# Wasm engine layer. Raised when the Wasm execution engine crashed
|
|
44
45
|
# (trap, OOM, unreachable) or when the wire layer detected a structural
|
|
45
46
|
# violation that signals a corrupted guest execution environment
|
|
46
|
-
# (zero-length OUTCOME_BUFFER, unknown outcome tag
|
|
47
|
+
# (zero-length OUTCOME_BUFFER, unknown outcome tag).
|
|
47
48
|
#
|
|
48
|
-
# Two named subclasses cover the configured per-invocation caps
|
|
49
|
+
# Two named subclasses cover the configured per-invocation caps:
|
|
49
50
|
#
|
|
50
|
-
# * {TimeoutError} — wall-clock +timeout+ exceeded
|
|
51
|
+
# * {TimeoutError} — wall-clock +timeout+ exceeded.
|
|
51
52
|
# * {MemoryLimitError} — guest +memory.grow+ would exceed
|
|
52
|
-
# +memory_limit
|
|
53
|
+
# +memory_limit+.
|
|
53
54
|
#
|
|
54
55
|
# Host Apps that only care about "guest is unrecoverable, discard the
|
|
55
56
|
# Sandbox" can rescue +TrapError+ and ignore the subclass; Host Apps that
|
|
@@ -57,24 +58,23 @@ module Kobako
|
|
|
57
58
|
# first.
|
|
58
59
|
class TrapError < Error; end
|
|
59
60
|
|
|
60
|
-
# Wall-clock timeout cap exhausted
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
61
|
+
# Wall-clock timeout cap exhausted: the absolute deadline
|
|
62
|
+
# +entry_time + timeout+ passed and the next guest wasm safepoint
|
|
63
|
+
# trapped. The Sandbox is unrecoverable after this point; discard and
|
|
64
|
+
# recreate before another execution.
|
|
64
65
|
class TimeoutError < TrapError; end
|
|
65
66
|
|
|
66
|
-
# Linear-memory cap exhausted
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
67
|
+
# Linear-memory cap exhausted: a guest +memory.grow+ would have pushed
|
|
68
|
+
# linear memory past the configured +memory_limit+. The Sandbox is
|
|
69
|
+
# unrecoverable after this point; discard and recreate before another
|
|
70
|
+
# execution.
|
|
70
71
|
class MemoryLimitError < TrapError; end
|
|
71
72
|
|
|
72
73
|
# Construction-layer error raised by +Kobako::Sandbox.new+ /
|
|
73
74
|
# +Kobako::Runtime.from_path+ when the wasm runtime cannot be built
|
|
74
75
|
# from the configured +wasm_path+ before any invocation runs —
|
|
75
76
|
# an unreadable artifact, bytes that are not a valid Wasm module, or
|
|
76
|
-
# engine / linker / instantiation setup failure
|
|
77
|
-
# ({docs/behavior.md E-41}[link:../../docs/behavior.md]). Construction
|
|
77
|
+
# engine / linker / instantiation setup failure. Construction
|
|
78
78
|
# is not an invocation, so +SetupError+ sits beside the invocation
|
|
79
79
|
# taxonomy under +Kobako::Error+ rather than under +TrapError+: no
|
|
80
80
|
# Sandbox is produced, so the +TrapError+ "discard and recreate"
|
|
@@ -83,8 +83,7 @@ module Kobako
|
|
|
83
83
|
|
|
84
84
|
# The named +SetupError+ subclass for the common, actionable case:
|
|
85
85
|
# the Guest Binary artifact is absent at +wasm_path+ — the pre-build
|
|
86
|
-
# state on a fresh clone before +bundle exec rake compile
|
|
87
|
-
# ({docs/behavior.md E-40}[link:../../docs/behavior.md]). Host Apps
|
|
86
|
+
# state on a fresh clone before +bundle exec rake compile+. Host Apps
|
|
88
87
|
# that only need "the Sandbox could not be set up" rescue +SetupError+;
|
|
89
88
|
# those wanting to special-case the unbuilt-artifact state rescue
|
|
90
89
|
# +ModuleNotBuiltError+ first.
|
|
@@ -120,21 +119,26 @@ module Kobako
|
|
|
120
119
|
end
|
|
121
120
|
end
|
|
122
121
|
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
122
|
+
# HandlerExhaustedError is the canonical SandboxError subclass for the
|
|
123
|
+
# id-cap-hit path. Raised when the per-invocation Handle ID counter in
|
|
124
|
+
# Catalog::Handles reaches +0x7fff_ffff+ (2³¹ − 1) and further
|
|
125
|
+
# allocation would exceed the cap.
|
|
127
126
|
class HandlerExhaustedError < SandboxError; end
|
|
128
127
|
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
# preserved. Inherits from SandboxError so a single
|
|
128
|
+
# BytecodeError is the SandboxError subclass raised when a
|
|
129
|
+
# `#preload(binary:)` snippet fails structural validation during the
|
|
130
|
+
# first invocation's snippet replay against a fresh `mrb_state` (RITE
|
|
131
|
+
# version mismatch or corrupt body). Bytecode that loads cleanly and
|
|
132
|
+
# then raises at top level surfaces as plain `SandboxError` with the
|
|
133
|
+
# natural mruby class preserved. Inherits from SandboxError so a single
|
|
136
134
|
# `rescue Kobako::SandboxError` covers both source and bytecode
|
|
137
135
|
# snippet failures while callers wanting bytecode-specific handling
|
|
138
136
|
# can `rescue Kobako::BytecodeError` directly.
|
|
139
137
|
class BytecodeError < SandboxError; end
|
|
138
|
+
|
|
139
|
+
# Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
|
|
140
|
+
# wait exceeded the configured +checkout_timeout+ while every slot was
|
|
141
|
+
# held. No Sandbox state is touched — retrying succeeds as soon as a holder
|
|
142
|
+
# returns its Sandbox.
|
|
143
|
+
class PoolTimeoutError < Error; end
|
|
140
144
|
end
|
data/lib/kobako/handle.rb
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
# Wire-level value object for an ext-0x01 Capability Handle, used in both
|
|
5
5
|
# directions across the Sandbox boundary: as a Service method's return
|
|
6
|
-
# value (guest→host return path
|
|
7
|
-
#
|
|
8
|
-
# ({docs/behavior.md B-34}[link:../../docs/behavior.md]).
|
|
6
|
+
# value (guest→host return path) and as a +#run+ argument auto-wrapped
|
|
7
|
+
# by the host.
|
|
9
8
|
#
|
|
10
9
|
# SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
|
|
11
10
|
# payload ({docs/wire-codec.md}[link:../../docs/wire-codec.md]
|
data/lib/kobako/namespace.rb
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
|
-
# A named grouping of Members for one Sandbox
|
|
5
|
-
# ({docs/behavior.md B-07..B-11}[link:../../docs/behavior.md]).
|
|
4
|
+
# A named grouping of Members for one Sandbox.
|
|
6
5
|
# Returned by +Sandbox#define+. Each instance owns a flat name→object
|
|
7
6
|
# table of Members; member binding is validated against {NAME_PATTERN}.
|
|
8
7
|
class Namespace
|
|
9
|
-
# Ruby constant-name pattern shared by Namespace and Member names
|
|
10
|
-
# ({docs/behavior.md B-07/B-08 Notes}[link:../../docs/behavior.md]).
|
|
8
|
+
# Ruby constant-name pattern shared by Namespace and Member names.
|
|
11
9
|
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
12
10
|
|
|
13
11
|
attr_reader :name
|
|
@@ -18,15 +16,19 @@ module Kobako
|
|
|
18
16
|
def initialize(name)
|
|
19
17
|
@name = name
|
|
20
18
|
@members = {} # : Hash[String, untyped]
|
|
19
|
+
@sealed = false
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
# Bind +object+ under +member+ inside this Namespace. +member+ is a
|
|
24
23
|
# constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
|
|
25
24
|
# object that responds to the methods guest code will invoke. Returns
|
|
26
25
|
# +self+ for chaining. Raises +ArgumentError+ when +member+ does not
|
|
27
|
-
# match the constant pattern,
|
|
28
|
-
# bound
|
|
26
|
+
# match the constant pattern, when a Member of the same name is
|
|
27
|
+
# already bound, or when the owning Sandbox's first invocation has
|
|
28
|
+
# sealed Service registration.
|
|
29
29
|
def bind(member, object)
|
|
30
|
+
raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
|
|
31
|
+
|
|
30
32
|
member_str = validate_member_name!(member)
|
|
31
33
|
raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
|
|
32
34
|
|
|
@@ -34,6 +36,15 @@ module Kobako
|
|
|
34
36
|
self
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
# Mark this Namespace as sealed. Called by
|
|
40
|
+
# +Kobako::Catalog::Namespaces#seal!+ on the owning Sandbox's first
|
|
41
|
+
# invocation; afterwards {#bind} raises +ArgumentError+. Idempotent;
|
|
42
|
+
# returns +self+.
|
|
43
|
+
def seal!
|
|
44
|
+
@sealed = true
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
37
48
|
# Member lookup; raises +KeyError+ when no Member is registered
|
|
38
49
|
# under +member+.
|
|
39
50
|
def fetch(member)
|