kobako 0.2.1 → 0.4.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/Cargo.lock +1 -1
- data/README.md +205 -59
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/wasm/cache.rs +15 -7
- data/ext/kobako/src/wasm/dispatch.rs +88 -36
- data/ext/kobako/src/wasm/host_state.rs +298 -55
- data/ext/kobako/src/wasm/instance.rs +477 -160
- data/ext/kobako/src/wasm.rs +20 -5
- data/lib/kobako/capture.rb +12 -10
- data/lib/kobako/codec/decoder.rb +3 -4
- data/lib/kobako/codec/encoder.rb +1 -1
- data/lib/kobako/codec/error.rb +3 -2
- data/lib/kobako/codec/factory.rb +24 -17
- data/lib/kobako/codec/utils.rb +105 -12
- data/lib/kobako/codec.rb +2 -1
- data/lib/kobako/errors.rb +22 -10
- data/lib/kobako/handle.rb +62 -0
- data/lib/kobako/handle_table.rb +119 -0
- data/lib/kobako/invocation.rb +143 -0
- data/lib/kobako/outcome/panic.rb +2 -2
- data/lib/kobako/outcome.rb +61 -24
- data/lib/kobako/rpc/dispatcher.rb +30 -28
- data/lib/kobako/rpc/envelope.rb +10 -10
- data/lib/kobako/rpc/fault.rb +4 -3
- data/lib/kobako/rpc/namespace.rb +3 -3
- data/lib/kobako/rpc/server.rb +23 -33
- data/lib/kobako/rpc/wire_error.rb +23 -0
- data/lib/kobako/sandbox.rb +211 -136
- data/lib/kobako/sandbox_options.rb +73 -0
- data/lib/kobako/snippet/binary.rb +30 -0
- data/lib/kobako/snippet/source.rb +28 -0
- data/lib/kobako/snippet/table.rb +174 -0
- data/lib/kobako/snippet.rb +20 -0
- data/lib/kobako/usage.rb +41 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/codec/factory.rbs +1 -1
- data/sig/kobako/codec/utils.rbs +10 -0
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/handle.rbs +19 -0
- data/sig/kobako/handle_table.rbs +23 -0
- data/sig/kobako/invocation.rbs +25 -0
- data/sig/kobako/outcome.rbs +1 -1
- data/sig/kobako/rpc/dispatcher.rbs +7 -7
- data/sig/kobako/rpc/envelope.rbs +3 -3
- data/sig/kobako/rpc/server.rbs +1 -7
- data/sig/kobako/rpc/wire_error.rbs +6 -0
- data/sig/kobako/sandbox.rbs +22 -17
- data/sig/kobako/sandbox_options.rbs +32 -0
- data/sig/kobako/snippet/binary.rbs +12 -0
- data/sig/kobako/snippet/source.rbs +13 -0
- data/sig/kobako/snippet/table.rbs +36 -0
- data/sig/kobako/snippet.rbs +4 -0
- data/sig/kobako/usage.rbs +11 -0
- data/sig/kobako/wasm.rbs +5 -1
- metadata +21 -5
- data/lib/kobako/rpc/handle.rb +0 -38
- data/lib/kobako/rpc/handle_table.rb +0 -107
- data/sig/kobako/rpc/handle.rbs +0 -19
- data/sig/kobako/rpc/handle_table.rbs +0 -25
data/lib/kobako/rpc/namespace.rb
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module RPC
|
|
5
5
|
# A named grouping of Members for one Sandbox
|
|
6
|
-
# ({
|
|
6
|
+
# ({docs/behavior.md B-07..B-11}[link:../../../docs/behavior.md]). Returned by
|
|
7
7
|
# +Sandbox#define+. Each instance owns a flat name→object table of
|
|
8
8
|
# Members; member binding is validated against {NAME_PATTERN}.
|
|
9
9
|
class Namespace
|
|
10
10
|
# Ruby constant-name pattern shared by Namespace and Member names
|
|
11
|
-
# ({
|
|
11
|
+
# ({docs/behavior.md B-07/B-08 Notes}[link:../../../docs/behavior.md]).
|
|
12
12
|
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
13
13
|
|
|
14
14
|
attr_reader :name, :members
|
|
@@ -26,7 +26,7 @@ module Kobako
|
|
|
26
26
|
# object that responds to the methods guest code will invoke. Returns
|
|
27
27
|
# +self+ for chaining. Raises +ArgumentError+ when +member+ does not
|
|
28
28
|
# match the constant pattern, or a Member of the same name is already
|
|
29
|
-
# bound ({
|
|
29
|
+
# bound ({docs/behavior.md B-11}[link:../../../docs/behavior.md]).
|
|
30
30
|
def bind(member, object)
|
|
31
31
|
member_str = validate_member_name!(member)
|
|
32
32
|
raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
|
data/lib/kobako/rpc/server.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "msgpack"
|
|
|
4
4
|
require_relative "../errors"
|
|
5
5
|
require_relative "envelope"
|
|
6
6
|
require_relative "namespace"
|
|
7
|
-
require_relative "handle_table"
|
|
7
|
+
require_relative "../handle_table"
|
|
8
8
|
require_relative "dispatcher"
|
|
9
9
|
|
|
10
10
|
module Kobako
|
|
@@ -12,7 +12,7 @@ module Kobako
|
|
|
12
12
|
# Kobako::RPC::Server — per-Sandbox host-side RPC coordinator. Maintains
|
|
13
13
|
# the Namespace / Member registry, owns the HandleTable, and routes
|
|
14
14
|
# incoming Requests to the resolved Service object
|
|
15
|
-
# ({
|
|
15
|
+
# ({docs/behavior.md B-07..B-21}[link:../../../docs/behavior.md]).
|
|
16
16
|
#
|
|
17
17
|
# Public API:
|
|
18
18
|
#
|
|
@@ -24,10 +24,11 @@ module Kobako
|
|
|
24
24
|
#
|
|
25
25
|
# Namespaces live at +Kobako::RPC::Namespace+
|
|
26
26
|
# (lib/kobako/rpc/namespace.rb). The opaque Handle allocator lives at
|
|
27
|
-
# +Kobako::
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# (
|
|
27
|
+
# +Kobako::HandleTable+ (lib/kobako/handle_table.rb) and is owned by
|
|
28
|
+
# the Sandbox — the Server only holds an injected reference so RPC
|
|
29
|
+
# dispatch resolves against the same table the wire layer allocates
|
|
30
|
+
# into (docs/behavior.md B-19). Dispatch helpers live at
|
|
31
|
+
# +Kobako::RPC::Dispatcher+ (lib/kobako/rpc/dispatcher.rb).
|
|
31
32
|
class Server
|
|
32
33
|
# Build a fresh Server. +handle_table+ is an internal seam that
|
|
33
34
|
# injects a pre-configured +HandleTable+; tests pass one whose +next_id+
|
|
@@ -39,14 +40,14 @@ module Kobako
|
|
|
39
40
|
@sealed = false
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
# Declare or retrieve the Namespace named +name+ (idempotent —
|
|
43
|
+
# Declare or retrieve the Namespace named +name+ (idempotent — docs/behavior.md B-10).
|
|
43
44
|
# +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
|
|
44
45
|
# +Namespace::NAME_PATTERN+). Returns the +Kobako::RPC::Namespace+ for
|
|
45
46
|
# that name, creating it if it does not exist. Raises +ArgumentError+
|
|
46
47
|
# when +name+ is malformed, or when called after the owning Sandbox has
|
|
47
|
-
# been sealed by
|
|
48
|
+
# been sealed by its first invocation ({docs/behavior.md B-07}[link:../../../docs/behavior.md]).
|
|
48
49
|
def define(name)
|
|
49
|
-
raise ArgumentError, "cannot define after Sandbox
|
|
50
|
+
raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
|
|
50
51
|
|
|
51
52
|
name_str = name.to_s
|
|
52
53
|
unless Namespace::NAME_PATTERN.match?(name_str)
|
|
@@ -76,11 +77,6 @@ module Kobako
|
|
|
76
77
|
!namespace.nil? && !member_name.nil? && !namespace[member_name].nil?
|
|
77
78
|
end
|
|
78
79
|
|
|
79
|
-
# Returns all declared +Kobako::RPC::Namespace+ instances as an +Array+.
|
|
80
|
-
def namespaces
|
|
81
|
-
@namespaces.values
|
|
82
|
-
end
|
|
83
|
-
|
|
84
80
|
# Returns the number of declared namespaces as an +Integer+.
|
|
85
81
|
def size
|
|
86
82
|
@namespaces.size
|
|
@@ -91,8 +87,9 @@ module Kobako
|
|
|
91
87
|
@namespaces.empty?
|
|
92
88
|
end
|
|
93
89
|
|
|
94
|
-
# Structured Frame 1 description. Called by +Sandbox#
|
|
95
|
-
# stdin Frame 1
|
|
90
|
+
# Structured Frame 1 description. Called by +Sandbox#eval+ when
|
|
91
|
+
# assembling stdin Frame 1
|
|
92
|
+
# ({docs/behavior.md B-02}[link:../../../docs/behavior.md]). Returns an
|
|
96
93
|
# unencoded preamble array — an +Array+ of two-element +[name, members]+
|
|
97
94
|
# arrays, one per declared namespace.
|
|
98
95
|
def to_preamble
|
|
@@ -100,7 +97,7 @@ module Kobako
|
|
|
100
97
|
end
|
|
101
98
|
|
|
102
99
|
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery
|
|
103
|
-
# ({
|
|
100
|
+
# ({docs/behavior.md B-02}[link:../../../docs/behavior.md]). Uses plain MessagePack (no
|
|
104
101
|
# kobako ext types) because the preamble contains only strings — no
|
|
105
102
|
# Handles or Fault envelopes. Structure:
|
|
106
103
|
# +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
|
|
@@ -109,8 +106,8 @@ module Kobako
|
|
|
109
106
|
MessagePack.pack(to_preamble)
|
|
110
107
|
end
|
|
111
108
|
|
|
112
|
-
# Mark the Server as sealed. Called by +Sandbox
|
|
113
|
-
# After sealing, #define raises ArgumentError. Idempotent.
|
|
109
|
+
# Mark the Server as sealed. Called by +Sandbox+ on the first
|
|
110
|
+
# invocation. After sealing, #define raises ArgumentError. Idempotent.
|
|
114
111
|
def seal!
|
|
115
112
|
@sealed = true
|
|
116
113
|
self
|
|
@@ -121,26 +118,19 @@ module Kobako
|
|
|
121
118
|
@sealed
|
|
122
119
|
end
|
|
123
120
|
|
|
124
|
-
# Reset the HandleTable for a new +#run+ boundary. Called by +Sandbox#run+
|
|
125
|
-
# before each invocation ({SPEC.md B-19}[link:../../../SPEC.md]).
|
|
126
|
-
def reset_handles!
|
|
127
|
-
@handle_table.reset!
|
|
128
|
-
end
|
|
129
|
-
|
|
130
121
|
# Dispatch a single RPC request and return the encoded response bytes
|
|
131
|
-
# ({
|
|
122
|
+
# ({docs/behavior.md B-12}[link:../../../docs/behavior.md]). +request_bytes+ is a
|
|
132
123
|
# msgpack-encoded Request envelope. Called by the Rust ext from inside
|
|
133
124
|
# +__kobako_dispatch+. Always returns a binary +String+ — never raises.
|
|
134
|
-
#
|
|
135
|
-
# +
|
|
136
|
-
#
|
|
125
|
+
# Forwards both the Server (for namespace lookup) and the injected
|
|
126
|
+
# +HandleTable+ (for Handle resolution / return-value wrapping) to
|
|
127
|
+
# +Dispatcher.dispatch+. The Server holds the HandleTable as an
|
|
128
|
+
# injected reference, not an owned resource — the Sandbox owns it
|
|
129
|
+
# (B-19) — so the table is not exposed via accessors.
|
|
137
130
|
def dispatch(request_bytes)
|
|
138
|
-
Dispatcher.dispatch(request_bytes, self)
|
|
131
|
+
Dispatcher.dispatch(request_bytes, self, @handle_table)
|
|
139
132
|
end
|
|
140
133
|
|
|
141
|
-
# Expose the +HandleTable+ for tests and wire-layer Handle wrapping.
|
|
142
|
-
attr_reader :handle_table
|
|
143
|
-
|
|
144
134
|
private
|
|
145
135
|
|
|
146
136
|
# Split +target+ on the +::+ separator and resolve the namespace half.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Kobako
|
|
6
|
+
module RPC
|
|
7
|
+
# +Kobako::SandboxError+ subclass raised when the host detects a
|
|
8
|
+
# structural violation of the wire contract while decoding bytes
|
|
9
|
+
# produced by the guest (a malformed Outcome envelope, a result body
|
|
10
|
+
# that fails msgpack decode, a Panic envelope missing required
|
|
11
|
+
# fields). Distinct from a Wasm trap (engine signalled the guest
|
|
12
|
+
# runtime is unrecoverable) and from a normal sandbox-layer failure
|
|
13
|
+
# (the script raised but the protocol was respected): a +WireError+
|
|
14
|
+
# always indicates the guest runtime is corrupted — the only safe
|
|
15
|
+
# recovery is to discard the Sandbox and start a new invocation.
|
|
16
|
+
#
|
|
17
|
+
# Inherits from +Kobako::SandboxError+ so a single
|
|
18
|
+
# +rescue Kobako::SandboxError+ still catches it; callers that want
|
|
19
|
+
# to distinguish wire-violation paths from script failures can
|
|
20
|
+
# +rescue Kobako::RPC::WireError+ directly.
|
|
21
|
+
class WireError < Kobako::SandboxError; end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
3
5
|
require_relative "capture"
|
|
4
6
|
require_relative "errors"
|
|
7
|
+
require_relative "handle_table"
|
|
8
|
+
require_relative "invocation"
|
|
5
9
|
require_relative "outcome"
|
|
6
10
|
require_relative "rpc/server"
|
|
7
11
|
require_relative "rpc/envelope"
|
|
12
|
+
require_relative "sandbox_options"
|
|
13
|
+
require_relative "snippet"
|
|
14
|
+
require_relative "usage"
|
|
8
15
|
|
|
9
16
|
module Kobako
|
|
10
17
|
# Kobako::Sandbox — the user-facing entry point for executing guest mruby
|
|
11
18
|
# scripts inside a wasmtime-hosted Wasm module
|
|
12
|
-
# ({
|
|
19
|
+
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
13
20
|
#
|
|
14
|
-
# The Sandbox owns the +Kobako::Wasm::Instance+, the per-
|
|
15
|
-
# (
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
21
|
+
# The Sandbox owns the +Kobako::Wasm::Instance+, the per-Sandbox
|
|
22
|
+
# +Kobako::HandleTable+ ({docs/behavior.md B-19}[link:../../docs/behavior.md]),
|
|
23
|
+
# the per-instance RPC Server (which receives the HandleTable by
|
|
24
|
+
# injection so guest→host dispatch and host→guest auto-wrap share one
|
|
25
|
+
# allocator), and the per-channel byte caches for guest stdout / stderr
|
|
26
|
+
# capture. The underlying wasmtime Engine and compiled Module are cached
|
|
27
|
+
# at process scope by the native ext and never surface to Ruby —
|
|
28
|
+
# constructing many Sandboxes amortises both costs automatically.
|
|
20
29
|
#
|
|
21
|
-
# Output capture policy ({
|
|
30
|
+
# Output capture policy ({docs/behavior.md B-04}[link:../../docs/behavior.md]): the
|
|
22
31
|
# per-channel cap (+stdout_limit+ / +stderr_limit+) is enforced inside the
|
|
23
32
|
# WASI pipe — the host buffer stops growing at the cap, subsequent guest
|
|
24
33
|
# writes on that channel fail or are dropped, and +#run+ still returns
|
|
@@ -27,164 +36,211 @@ module Kobako
|
|
|
27
36
|
# +#stdout_truncated?+ / +#stderr_truncated?+ are the only way to observe
|
|
28
37
|
# that the cap was hit.
|
|
29
38
|
class Sandbox
|
|
30
|
-
|
|
31
|
-
# ({SPEC.md B-01}[link:../../SPEC.md]).
|
|
32
|
-
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
33
|
-
|
|
34
|
-
# Default wall-clock timeout for a single +#run+: 60 seconds
|
|
35
|
-
# ({SPEC.md B-01}[link:../../SPEC.md]).
|
|
36
|
-
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
37
|
-
|
|
38
|
-
# Default cap on guest linear memory growth: 5 MiB
|
|
39
|
-
# ({SPEC.md B-01}[link:../../SPEC.md]).
|
|
40
|
-
DEFAULT_MEMORY_LIMIT = 5 << 20
|
|
39
|
+
extend Forwardable
|
|
41
40
|
|
|
42
41
|
attr_reader :wasm_path, :instance,
|
|
43
|
-
:
|
|
44
|
-
:
|
|
42
|
+
:options,
|
|
43
|
+
:services, :snippets
|
|
44
|
+
|
|
45
|
+
# Per-cap accessors forward to the immutable +SandboxOptions+ Value
|
|
46
|
+
# Object so the Host App still reads them off Sandbox directly.
|
|
47
|
+
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit
|
|
45
48
|
|
|
46
49
|
# Returns the bytes the guest wrote to stdout during the most recent
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# never contains a truncation sentinel; use +#stdout_truncated?+ to
|
|
50
|
+
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
51
|
+
# any invocation. {docs/behavior.md B-04}[link:../../docs/behavior.md] — the byte
|
|
52
|
+
# content never contains a truncation sentinel; use +#stdout_truncated?+ to
|
|
50
53
|
# observe overflow.
|
|
51
54
|
def stdout
|
|
52
55
|
@stdout_capture.bytes
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
# Returns the bytes the guest wrote to stderr during the most recent
|
|
56
|
-
#
|
|
57
|
-
#
|
|
59
|
+
# invocation as a UTF-8 String, clipped at +stderr_limit+. Empty before
|
|
60
|
+
# any invocation. Mirror of +#stdout+.
|
|
58
61
|
def stderr
|
|
59
62
|
@stderr_capture.bytes
|
|
60
63
|
end
|
|
61
64
|
|
|
62
|
-
# Returns +true+ iff stdout capture during the most recent
|
|
63
|
-
# exceeded +stdout_limit+ ({
|
|
64
|
-
# to +false+ at the start of the next
|
|
65
|
-
# B-03}[link:../../
|
|
65
|
+
# Returns +true+ iff stdout capture during the most recent invocation
|
|
66
|
+
# exceeded +stdout_limit+ ({docs/behavior.md B-04}[link:../../docs/behavior.md]). Resets
|
|
67
|
+
# to +false+ at the start of the next invocation
|
|
68
|
+
# ({docs/behavior.md B-03}[link:../../docs/behavior.md]).
|
|
66
69
|
def stdout_truncated?
|
|
67
70
|
@stdout_capture.truncated?
|
|
68
71
|
end
|
|
69
72
|
|
|
70
|
-
# Returns +true+ iff stderr capture during the most recent
|
|
73
|
+
# Returns +true+ iff stderr capture during the most recent invocation
|
|
71
74
|
# exceeded +stderr_limit+. Mirror of +#stdout_truncated?+.
|
|
72
75
|
def stderr_truncated?
|
|
73
76
|
@stderr_capture.truncated?
|
|
74
77
|
end
|
|
75
78
|
|
|
79
|
+
# Returns the +Kobako::Usage+ value object for the most recent
|
|
80
|
+
# invocation ({docs/behavior.md B-35}[link:../../docs/behavior.md]).
|
|
81
|
+
# Carries +wall_time+ (Float seconds the guest export call spent
|
|
82
|
+
# inside wasmtime) and +memory_peak+ (Integer bytes, high-water of
|
|
83
|
+
# the per-invocation +memory.grow+ delta past the entry-time
|
|
84
|
+
# baseline). Returns +Kobako::Usage::EMPTY+ before any invocation;
|
|
85
|
+
# populated on every outcome — including +TrapError+ — so the Host
|
|
86
|
+
# App can read it after rescuing a trap to diagnose budget
|
|
87
|
+
# consumption.
|
|
88
|
+
attr_reader :usage
|
|
89
|
+
|
|
76
90
|
# Build a fresh Sandbox.
|
|
77
91
|
#
|
|
78
92
|
# +wasm_path+ is the absolute path to the Guest Binary; defaults to the
|
|
79
|
-
# gem-bundled +data/kobako.wasm+.
|
|
80
|
-
#
|
|
81
|
-
# +
|
|
82
|
-
#
|
|
83
|
-
# +
|
|
84
|
-
#
|
|
85
|
-
# +nil+ disables).
|
|
93
|
+
# gem-bundled +data/kobako.wasm+. The four caps (+stdout_limit+,
|
|
94
|
+
# +stderr_limit+, +timeout+, +memory_limit+) are forwarded verbatim to
|
|
95
|
+
# +Kobako::SandboxOptions+, which owns their DEFAULT fallback and
|
|
96
|
+
# normalisation. The constructed +SandboxOptions+ is exposed as
|
|
97
|
+
# +#options+ and the four caps remain readable directly on Sandbox via
|
|
98
|
+
# +Forwardable+ delegation.
|
|
86
99
|
def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
|
|
87
|
-
timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
88
|
-
memory_limit: DEFAULT_MEMORY_LIMIT)
|
|
100
|
+
timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
|
|
101
|
+
memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
|
|
89
102
|
@wasm_path = wasm_path || Kobako::Wasm.default_path
|
|
90
|
-
@
|
|
91
|
-
|
|
92
|
-
@
|
|
93
|
-
@
|
|
94
|
-
@
|
|
95
|
-
@instance = Kobako::Wasm::Instance.from_path(@wasm_path, @timeout, @memory_limit,
|
|
103
|
+
@options = SandboxOptions.new(timeout: timeout, memory_limit: memory_limit, stdout_limit: stdout_limit,
|
|
104
|
+
stderr_limit: stderr_limit)
|
|
105
|
+
@handle_table = HandleTable.new
|
|
106
|
+
@services = Kobako::RPC::Server.new(handle_table: @handle_table)
|
|
107
|
+
@snippets = Snippet::Table.new
|
|
108
|
+
@instance = Kobako::Wasm::Instance.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
109
|
+
@options.stdout_limit, @options.stderr_limit)
|
|
96
110
|
@instance.server = @services
|
|
97
|
-
|
|
111
|
+
reset_invocation_state!
|
|
98
112
|
end
|
|
99
113
|
|
|
100
114
|
# Declare or retrieve the Namespace named +name+ on this Sandbox
|
|
101
|
-
# ({
|
|
115
|
+
# ({docs/behavior.md B-07, B-09, B-10}[link:../../docs/behavior.md]). +name+ must be a
|
|
102
116
|
# Symbol or String in constant form. Returns the +Kobako::RPC::Namespace+.
|
|
103
117
|
#
|
|
104
|
-
# Raises +ArgumentError+ when called after
|
|
105
|
-
# not match the constant-name pattern.
|
|
118
|
+
# Raises +ArgumentError+ when called after the first invocation, or
|
|
119
|
+
# when +name+ does not match the constant-name pattern.
|
|
106
120
|
def define(name)
|
|
107
121
|
@services.define(name)
|
|
108
122
|
end
|
|
109
123
|
|
|
110
|
-
#
|
|
111
|
-
# ({
|
|
112
|
-
# source code as a UTF-8 String. Returns the deserialized last
|
|
113
|
-
# expression of the script.
|
|
124
|
+
# Register a snippet on this Sandbox in one of two forms
|
|
125
|
+
# ({docs/behavior.md B-32}[link:../../docs/behavior.md]):
|
|
114
126
|
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
127
|
+
# * +preload(code: source, name: Name)+ — +source+ is mruby source
|
|
128
|
+
# as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. The +name+
|
|
129
|
+
# becomes the snippet's +(snippet:Name)+ backtrace filename and
|
|
130
|
+
# is the dedupe key for E-33.
|
|
131
|
+
# * +preload(binary: bytes)+ — +bytes+ is precompiled RITE
|
|
132
|
+
# bytecode as a +String+. The canonical name, when present,
|
|
133
|
+
# lives in the bytecode's embedded +debug_info+ and is resolved
|
|
134
|
+
# by the guest at load time; the host treats the bytes as
|
|
135
|
+
# opaque. Structural failures
|
|
136
|
+
# ({docs/behavior.md E-37 / E-38}[link:../../docs/behavior.md])
|
|
137
|
+
# surface as +Kobako::BytecodeError+ on the first invocation.
|
|
120
138
|
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
# Subsequent invocations (+#eval+ or +#run+) replay every registered
|
|
140
|
+
# snippet — in insertion order — against the fresh +mrb_state+
|
|
141
|
+
# before per-invocation source or entrypoint resolution.
|
|
142
|
+
#
|
|
143
|
+
# Returns +self+ to allow chaining.
|
|
144
|
+
#
|
|
145
|
+
# Raises +ArgumentError+ when neither form's keyword set is
|
|
146
|
+
# supplied, when both forms are mixed (e.g., +code:+ and +binary:+
|
|
147
|
+
# together, or +binary:+ paired with +name:+), when +code+ / +bytes+
|
|
148
|
+
# is not a +String+, when +name+ does not match the constant
|
|
149
|
+
# pattern ({docs/behavior.md E-34}[link:../../docs/behavior.md]),
|
|
150
|
+
# when +name+ duplicates an already-registered +code:+ form snippet
|
|
151
|
+
# ({docs/behavior.md E-33}[link:../../docs/behavior.md]), or when
|
|
152
|
+
# called after the first invocation
|
|
153
|
+
# ({docs/behavior.md E-35, B-33}[link:../../docs/behavior.md]).
|
|
154
|
+
def preload(code: nil, name: nil, binary: nil)
|
|
155
|
+
raise ArgumentError, "cannot preload after first Sandbox invocation" if @services.sealed?
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
take_result!
|
|
157
|
+
@snippets.register(code: code, name: name, binary: binary)
|
|
158
|
+
self
|
|
133
159
|
end
|
|
134
160
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
seconds
|
|
161
|
+
# Dispatch into a preloaded entrypoint constant
|
|
162
|
+
# ({docs/behavior.md B-31}[link:../../docs/behavior.md]). Delegates host
|
|
163
|
+
# pre-flight (E-24 / E-25 / E-29 / E-30) and wire encoding to
|
|
164
|
+
# +Kobako::Invocation+ / +Kobako::Invocation#encode+; the guest
|
|
165
|
+
# resolves +target+ as a top-level constant, calls +#call+ on it
|
|
166
|
+
# with +args+ / +kwargs+, and returns the deserialized result. The
|
|
167
|
+
# first invocation seals the Service registry and snippet table
|
|
168
|
+
# (B-07 / B-33). Runtime errors follow the same three-class taxonomy
|
|
169
|
+
# as +#eval+.
|
|
170
|
+
def run(target, *args, **kwargs)
|
|
171
|
+
invocation = Invocation.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
172
|
+
invoke!(:run) do
|
|
173
|
+
@instance.run(@services.encoded_preamble, @snippets.encode, invocation.encode(@handle_table))
|
|
174
|
+
end
|
|
150
175
|
end
|
|
151
176
|
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
# Execute a guest mruby source string in a fresh +mrb_state+
|
|
178
|
+
# ({docs/behavior.md B-02 / B-03 / B-06}[link:../../docs/behavior.md]). +code+ is the
|
|
179
|
+
# mruby source as a UTF-8 String. Returns the deserialized last
|
|
180
|
+
# expression of the source.
|
|
181
|
+
#
|
|
182
|
+
# Source delivery uses the WASI stdin three-frame protocol
|
|
183
|
+
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
|
|
184
|
+
# Frame 1 carries the msgpack-encoded preamble (Namespace / Member
|
|
185
|
+
# registry snapshot), Frame 2 carries the user source UTF-8 bytes, and
|
|
186
|
+
# Frame 3 carries the snippet table registered via +#preload+ (B-32).
|
|
187
|
+
# Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
|
|
188
|
+
# mandatory-presence — an empty snippet table sends an empty msgpack
|
|
189
|
+
# array, never an absent frame.
|
|
190
|
+
#
|
|
191
|
+
# The first invocation seals the Service registry and snippet table
|
|
192
|
+
# ({docs/behavior.md B-07 / B-33}[link:../../docs/behavior.md]); subsequent
|
|
193
|
+
# +#define+ / +#preload+ calls raise +ArgumentError+.
|
|
194
|
+
#
|
|
195
|
+
# Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
|
|
196
|
+
# +Kobako::SandboxError+ when the guest ran to completion but failed
|
|
197
|
+
# (including when +code+ is +nil+ or not a String, or when a preloaded
|
|
198
|
+
# snippet's replay raises — E-36);
|
|
199
|
+
# +Kobako::ServiceError+ on an unrescued Service capability failure.
|
|
200
|
+
def eval(code)
|
|
201
|
+
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
161
202
|
|
|
162
|
-
|
|
203
|
+
invoke!(:eval) do
|
|
204
|
+
@instance.eval(@services.encoded_preamble, code.b, @snippets.encode)
|
|
205
|
+
end
|
|
163
206
|
end
|
|
164
207
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
# Per-invocation prologue ({docs/behavior.md B-03 / B-07 /
|
|
211
|
+
# B-33}[link:../../docs/behavior.md]). Seals the Service / snippet
|
|
212
|
+
# registries on first call (idempotent) and zeros the per-invocation
|
|
213
|
+
# capability state — capture buffers, truncation predicates, and the
|
|
214
|
+
# HandleTable counter — before the guest runs. The HandleTable
|
|
215
|
+
# itself is held as +@handle_table+ and never exposed beyond
|
|
216
|
+
# this class: SPEC.md Terminology pins it as "Not exposed to the
|
|
217
|
+
# Host App" (B-19 / B-20 / E-29).
|
|
218
|
+
def begin_invocation!
|
|
219
|
+
@services.seal!
|
|
220
|
+
@handle_table.reset!
|
|
221
|
+
reset_invocation_state!
|
|
171
222
|
end
|
|
172
223
|
|
|
173
|
-
# Reset
|
|
174
|
-
#
|
|
175
|
-
# (
|
|
176
|
-
#
|
|
177
|
-
|
|
224
|
+
# Reset all per-invocation observable state to its pre-invocation
|
|
225
|
+
# sentinels — both per-channel captures
|
|
226
|
+
# ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
|
|
227
|
+
# per-last-invocation usage record
|
|
228
|
+
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
|
|
229
|
+
# +#initialize+ (first-time setup) and +#begin_invocation!+
|
|
230
|
+
# (between-invocation reset) so both paths agree on what
|
|
231
|
+
# "pre-invocation state" means.
|
|
232
|
+
def reset_invocation_state!
|
|
178
233
|
@stdout_capture = Capture::EMPTY
|
|
179
234
|
@stderr_capture = Capture::EMPTY
|
|
235
|
+
@usage = Usage::EMPTY
|
|
180
236
|
end
|
|
181
237
|
|
|
182
238
|
# Read the per-channel capture pairs (+[bytes, truncated]+) from the
|
|
183
|
-
# ext after
|
|
239
|
+
# ext after an invocation completes and wrap each as a +Capture+ value
|
|
184
240
|
# object. The ext clips +bytes+ to the configured cap and sets
|
|
185
241
|
# +truncated+ when the guest produced strictly more than +cap+ bytes
|
|
186
|
-
# ({
|
|
187
|
-
# at the post-
|
|
242
|
+
# ({docs/behavior.md B-04}[link:../../docs/behavior.md]). Mirror of
|
|
243
|
+
# {#reset_invocation_state!} at the post-invocation boundary.
|
|
188
244
|
def read_captures!
|
|
189
245
|
out_bytes, out_truncated = @instance.stdout
|
|
190
246
|
err_bytes, err_truncated = @instance.stderr
|
|
@@ -192,37 +248,56 @@ module Kobako
|
|
|
192
248
|
@stderr_capture = Capture.from_ext(err_bytes, err_truncated)
|
|
193
249
|
end
|
|
194
250
|
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
251
|
+
# Read the per-last-invocation +wall_time+ and +memory_peak+ from
|
|
252
|
+
# the ext and wrap them as a +Kobako::Usage+ value object
|
|
253
|
+
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
|
|
254
|
+
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
255
|
+
# every outcome — value return, +Kobako::TrapError+ (including
|
|
256
|
+
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
257
|
+
# and +Kobako::ServiceError+.
|
|
258
|
+
#
|
|
259
|
+
# The ext returns a positional 2-tuple +[wall_time, memory_peak]+
|
|
260
|
+
# whose order matches the +Kobako::Usage+ field order; the
|
|
261
|
+
# destructure-then-kwargs handoff below is the explicit
|
|
262
|
+
# positional→keyword conversion point, mirroring
|
|
263
|
+
# +#read_captures!+'s +Capture.from_ext(bytes, truncated)+ shape.
|
|
264
|
+
def read_usage!
|
|
265
|
+
wall_time, memory_peak = @instance.usage
|
|
266
|
+
@usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
209
267
|
end
|
|
210
268
|
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
269
|
+
# Map a wasmtime trap class to the matching three-layer Ruby
|
|
270
|
+
# exception class. Cap-trap subclasses
|
|
271
|
+
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
|
|
272
|
+
# select their named +TrapError+ subclass; everything else
|
|
273
|
+
# collapses to the base +Kobako::TrapError+.
|
|
274
|
+
def trap_class_for(err)
|
|
275
|
+
case err
|
|
276
|
+
when Kobako::Wasm::TimeoutError then TimeoutError
|
|
277
|
+
when Kobako::Wasm::MemoryLimitError then MemoryLimitError
|
|
278
|
+
else TrapError
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Shared prologue / epilogue + trap-class translator for both
|
|
283
|
+
# invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
|
|
284
|
+
# TrapError message so the failing export is identifiable. The
|
|
285
|
+
# rescue chain is the single trap-translation boundary — wasmtime /
|
|
286
|
+
# wire failures from the guest call and from the subsequent
|
|
287
|
+
# +Instance#outcome!+ read both flow through here, so an
|
|
288
|
+
# OUTCOME_BUFFER read failure attributes to the same export name as
|
|
289
|
+
# the guest call itself. Configured-cap paths
|
|
290
|
+
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md]) surface as
|
|
291
|
+
# named TrapError subclasses.
|
|
292
|
+
def invoke!(verb)
|
|
293
|
+
begin_invocation!
|
|
294
|
+
yield
|
|
295
|
+
read_captures!
|
|
223
296
|
Outcome.decode(@instance.outcome!)
|
|
224
297
|
rescue Kobako::Wasm::Error => e
|
|
225
|
-
raise
|
|
298
|
+
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
299
|
+
ensure
|
|
300
|
+
read_usage!
|
|
226
301
|
end
|
|
227
302
|
end
|
|
228
303
|
end
|