kobako 0.1.2 → 0.2.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 +95 -60
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/wasm/cache.rs +39 -1
- data/ext/kobako/src/wasm/dispatch.rs +20 -20
- data/ext/kobako/src/wasm/host_state.rs +261 -34
- data/ext/kobako/src/wasm/instance.rs +467 -272
- data/ext/kobako/src/wasm.rs +50 -19
- data/lib/kobako/capture.rb +46 -0
- data/lib/kobako/codec/decoder.rb +66 -0
- data/lib/kobako/codec/encoder.rb +37 -0
- data/lib/kobako/codec/error.rb +33 -0
- data/lib/kobako/codec/factory.rb +155 -0
- data/lib/kobako/codec/utils.rb +55 -0
- data/lib/kobako/codec.rb +27 -0
- data/lib/kobako/errors.rb +24 -1
- data/lib/kobako/outcome/panic.rb +42 -0
- data/lib/kobako/outcome.rb +133 -0
- data/lib/kobako/rpc/dispatcher.rb +169 -0
- data/lib/kobako/rpc/envelope.rb +118 -0
- data/lib/kobako/{wire/exception.rb → rpc/fault.rb} +6 -4
- data/lib/kobako/{wire → rpc}/handle.rb +4 -2
- data/lib/kobako/{registry → rpc}/handle_table.rb +9 -9
- data/lib/kobako/{registry/service_group.rb → rpc/namespace.rb} +20 -11
- data/lib/kobako/rpc/server.rb +156 -0
- data/lib/kobako/rpc.rb +11 -0
- data/lib/kobako/sandbox.rb +149 -69
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako/wasm.rb +6 -16
- data/lib/kobako.rb +2 -0
- data/sig/kobako/capture.rbs +13 -0
- data/sig/kobako/codec/decoder.rbs +11 -0
- data/sig/kobako/codec/encoder.rbs +7 -0
- data/sig/kobako/codec/error.rbs +18 -0
- data/sig/kobako/codec/factory.rbs +31 -0
- data/sig/kobako/codec/utils.rbs +9 -0
- data/sig/kobako/errors.rbs +52 -0
- data/sig/kobako/outcome/panic.rbs +34 -0
- data/sig/kobako/outcome.rbs +24 -0
- data/sig/kobako/rpc/dispatcher.rbs +33 -0
- data/sig/kobako/rpc/envelope.rbs +51 -0
- data/sig/kobako/rpc/fault.rbs +20 -0
- data/sig/kobako/rpc/handle.rbs +19 -0
- data/sig/kobako/rpc/handle_table.rbs +25 -0
- data/sig/kobako/rpc/namespace.rbs +24 -0
- data/sig/kobako/rpc/server.rbs +37 -0
- data/sig/kobako/rpc.rbs +4 -0
- data/sig/kobako/sandbox.rbs +53 -0
- data/sig/kobako/wasm.rbs +37 -0
- data/sig/kobako.rbs +0 -1
- metadata +37 -17
- data/lib/kobako/registry/dispatcher.rb +0 -168
- data/lib/kobako/registry.rb +0 -160
- data/lib/kobako/sandbox/outcome_decoder.rb +0 -100
- data/lib/kobako/sandbox/output_buffer.rb +0 -79
- data/lib/kobako/wire/codec/decoder.rb +0 -87
- data/lib/kobako/wire/codec/encoder.rb +0 -41
- data/lib/kobako/wire/codec/error.rb +0 -35
- data/lib/kobako/wire/codec/factory.rb +0 -136
- data/lib/kobako/wire/codec.rb +0 -44
- data/lib/kobako/wire/envelope/payloads.rb +0 -145
- data/lib/kobako/wire/envelope.rb +0 -147
- data/lib/kobako/wire.rb +0 -40
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "msgpack"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
require_relative "envelope"
|
|
6
|
+
require_relative "namespace"
|
|
7
|
+
require_relative "handle_table"
|
|
8
|
+
require_relative "dispatcher"
|
|
9
|
+
|
|
10
|
+
module Kobako
|
|
11
|
+
module RPC
|
|
12
|
+
# Kobako::RPC::Server — per-Sandbox host-side RPC coordinator. Maintains
|
|
13
|
+
# the Namespace / Member registry, owns the HandleTable, and routes
|
|
14
|
+
# incoming Requests to the resolved Service object
|
|
15
|
+
# ({SPEC.md B-07..B-21}[link:../../../SPEC.md]).
|
|
16
|
+
#
|
|
17
|
+
# Public API:
|
|
18
|
+
#
|
|
19
|
+
# server = Kobako::RPC::Server.new
|
|
20
|
+
# namespace = server.define(:MyService) # => Kobako::RPC::Namespace
|
|
21
|
+
# namespace.bind(:KV, kv_object) # => namespace (chainable)
|
|
22
|
+
# server.to_preamble # => array for Frame 1
|
|
23
|
+
# server.dispatch(request_bytes) # => msgpack bytes (delegated to Dispatcher)
|
|
24
|
+
#
|
|
25
|
+
# Namespaces live at +Kobako::RPC::Namespace+
|
|
26
|
+
# (lib/kobako/rpc/namespace.rb). The opaque Handle allocator lives at
|
|
27
|
+
# +Kobako::RPC::HandleTable+
|
|
28
|
+
# (lib/kobako/rpc/handle_table.rb). Dispatch helpers live at
|
|
29
|
+
# +Kobako::RPC::Dispatcher+
|
|
30
|
+
# (lib/kobako/rpc/dispatcher.rb).
|
|
31
|
+
class Server
|
|
32
|
+
# Build a fresh Server. +handle_table+ is an internal seam that
|
|
33
|
+
# injects a pre-configured +HandleTable+; tests pass one whose +next_id+
|
|
34
|
+
# is pinned near +MAX_ID+ to exercise the B-21 cap-exhaustion path
|
|
35
|
+
# without 2³¹ allocations. Production callers leave it at the default.
|
|
36
|
+
def initialize(handle_table: HandleTable.new)
|
|
37
|
+
@namespaces = {} # : Hash[String, Kobako::RPC::Namespace]
|
|
38
|
+
@handle_table = handle_table
|
|
39
|
+
@sealed = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Declare or retrieve the Namespace named +name+ (idempotent — SPEC.md B-10).
|
|
43
|
+
# +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
|
|
44
|
+
# +Namespace::NAME_PATTERN+). Returns the +Kobako::RPC::Namespace+ for
|
|
45
|
+
# that name, creating it if it does not exist. Raises +ArgumentError+
|
|
46
|
+
# when +name+ is malformed, or when called after the owning Sandbox has
|
|
47
|
+
# been sealed by +#run+.
|
|
48
|
+
def define(name)
|
|
49
|
+
raise ArgumentError, "cannot define after Sandbox#run has been invoked" if @sealed
|
|
50
|
+
|
|
51
|
+
name_str = name.to_s
|
|
52
|
+
unless Namespace::NAME_PATTERN.match?(name_str)
|
|
53
|
+
raise ArgumentError,
|
|
54
|
+
"Namespace name must match #{Namespace::NAME_PATTERN.inspect} (got #{name.inspect})"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@namespaces[name_str] ||= Namespace.new(name_str)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Resolve a +target+ path of the form +"Namespace::Member"+ to the
|
|
61
|
+
# bound Host object. +target+ is a two-level path using the +::+
|
|
62
|
+
# separator. Returns the bound Host object. Raises +KeyError+ when the
|
|
63
|
+
# namespace or the member is not bound.
|
|
64
|
+
def lookup(target)
|
|
65
|
+
namespace, member_name, namespace_name = parse_target(target)
|
|
66
|
+
raise KeyError, "no namespace named #{namespace_name.inspect}" if namespace.nil?
|
|
67
|
+
raise KeyError, "no member #{target.inspect} bound on server" unless member_name
|
|
68
|
+
|
|
69
|
+
namespace.fetch(member_name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns +true+ when +target+ (a +"Namespace::Member"+ path) resolves
|
|
73
|
+
# to a bound member, +false+ otherwise.
|
|
74
|
+
def bound?(target)
|
|
75
|
+
namespace, member_name, = parse_target(target)
|
|
76
|
+
!namespace.nil? && !member_name.nil? && !namespace[member_name].nil?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns all declared +Kobako::RPC::Namespace+ instances as an +Array+.
|
|
80
|
+
def namespaces
|
|
81
|
+
@namespaces.values
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns the number of declared namespaces as an +Integer+.
|
|
85
|
+
def size
|
|
86
|
+
@namespaces.size
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns +true+ when no namespaces have been declared, +false+ otherwise.
|
|
90
|
+
def empty?
|
|
91
|
+
@namespaces.empty?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Structured Frame 1 description. Called by +Sandbox#run+ when assembling
|
|
95
|
+
# stdin Frame 1 ({SPEC.md B-02}[link:../../../SPEC.md]). Returns an
|
|
96
|
+
# unencoded preamble array — an +Array+ of two-element +[name, members]+
|
|
97
|
+
# arrays, one per declared namespace.
|
|
98
|
+
def to_preamble
|
|
99
|
+
@namespaces.values.map(&:to_preamble)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery
|
|
103
|
+
# ({SPEC.md B-02}[link:../../../SPEC.md]). Uses plain MessagePack (no
|
|
104
|
+
# kobako ext types) because the preamble contains only strings — no
|
|
105
|
+
# Handles or Fault envelopes. Structure:
|
|
106
|
+
# +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
|
|
107
|
+
# +String+ of msgpack bytes.
|
|
108
|
+
def encoded_preamble
|
|
109
|
+
MessagePack.pack(to_preamble)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Mark the Server as sealed. Called by +Sandbox#run+ on first run.
|
|
113
|
+
# After sealing, #define raises ArgumentError. Idempotent.
|
|
114
|
+
def seal!
|
|
115
|
+
@sealed = true
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns +true+ when {#seal!} has been called, +false+ otherwise.
|
|
120
|
+
def sealed?
|
|
121
|
+
@sealed
|
|
122
|
+
end
|
|
123
|
+
|
|
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
|
+
# Dispatch a single RPC request and return the encoded response bytes
|
|
131
|
+
# ({SPEC.md B-12}[link:../../../SPEC.md]). +request_bytes+ is a
|
|
132
|
+
# msgpack-encoded Request envelope. Called by the Rust ext from inside
|
|
133
|
+
# +__kobako_dispatch+. Always returns a binary +String+ — never raises.
|
|
134
|
+
# Delegates to +Dispatcher.dispatch+ which reifies any failure as a
|
|
135
|
+
# +Response.error+ envelope so the guest sees the failure as a normal RPC
|
|
136
|
+
# error rather than a wasm trap.
|
|
137
|
+
def dispatch(request_bytes)
|
|
138
|
+
Dispatcher.dispatch(request_bytes, self)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Expose the +HandleTable+ for tests and wire-layer Handle wrapping.
|
|
142
|
+
attr_reader :handle_table
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Split +target+ on the +::+ separator and resolve the namespace half.
|
|
147
|
+
# Returns +[namespace_or_nil, member_str_or_nil, namespace_name_str]+ so
|
|
148
|
+
# each public method ({#lookup} / {#bound?}) only owns its boundary
|
|
149
|
+
# semantics (raise vs predicate).
|
|
150
|
+
def parse_target(target)
|
|
151
|
+
namespace_name, member_name = target.to_s.split("::", 2)
|
|
152
|
+
[@namespaces[namespace_name], member_name, namespace_name]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
data/lib/kobako/rpc.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Kobako::RPC — protocol namespace for host↔guest RPC. Houses the value
|
|
5
|
+
# objects that travel on the wire (+Handle+, +Request+, +Response+,
|
|
6
|
+
# +Fault+) and the host-side Server coordinator. See
|
|
7
|
+
# {SPEC.md Refinement → Internal Concepts}[link:../../SPEC.md] for the
|
|
8
|
+
# RPC role split.
|
|
9
|
+
module RPC
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -1,73 +1,105 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "capture"
|
|
3
4
|
require_relative "errors"
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "sandbox/outcome_decoder"
|
|
5
|
+
require_relative "outcome"
|
|
6
|
+
require_relative "rpc/server"
|
|
7
|
+
require_relative "rpc/envelope"
|
|
8
8
|
|
|
9
9
|
module Kobako
|
|
10
10
|
# Kobako::Sandbox — the user-facing entry point for executing guest mruby
|
|
11
11
|
# scripts inside a wasmtime-hosted Wasm module
|
|
12
12
|
# ({SPEC.md B-01}[link:../../SPEC.md]).
|
|
13
13
|
#
|
|
14
|
-
# The Sandbox owns the +Kobako::Wasm::Instance+, the per-instance
|
|
15
|
-
# (which itself owns the per-run HandleTable), and
|
|
16
|
-
# capture
|
|
17
|
-
# cached at process scope by the native ext and
|
|
18
|
-
# constructing many Sandboxes amortises both costs
|
|
14
|
+
# The Sandbox owns the +Kobako::Wasm::Instance+, the per-instance RPC Server
|
|
15
|
+
# (which itself owns the per-run HandleTable), and the per-channel byte
|
|
16
|
+
# caches for guest stdout / stderr capture. The underlying wasmtime Engine
|
|
17
|
+
# and compiled Module are cached at process scope by the native ext and
|
|
18
|
+
# never surface to Ruby — constructing many Sandboxes amortises both costs
|
|
19
|
+
# automatically.
|
|
19
20
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
21
|
+
# Output capture policy ({SPEC.md B-04}[link:../../SPEC.md]): the
|
|
22
|
+
# per-channel cap (+stdout_limit+ / +stderr_limit+) is enforced inside the
|
|
23
|
+
# WASI pipe — the host buffer stops growing at the cap, subsequent guest
|
|
24
|
+
# writes on that channel fail or are dropped, and +#run+ still returns
|
|
25
|
+
# normally. +#stdout+ / +#stderr+ return the captured prefix as a UTF-8
|
|
26
|
+
# String; the byte content never carries a truncation sentinel.
|
|
27
|
+
# +#stdout_truncated?+ / +#stderr_truncated?+ are the only way to observe
|
|
28
|
+
# that the cap was hit.
|
|
26
29
|
class Sandbox
|
|
27
30
|
# Default per-channel capture ceiling: 1 MiB
|
|
28
|
-
# ({SPEC.md B-01
|
|
31
|
+
# ({SPEC.md B-01}[link:../../SPEC.md]).
|
|
29
32
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
30
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
|
|
41
|
+
|
|
31
42
|
attr_reader :wasm_path, :instance,
|
|
32
|
-
:
|
|
33
|
-
:
|
|
43
|
+
:stdout_limit, :stderr_limit,
|
|
44
|
+
:timeout, :memory_limit, :services
|
|
34
45
|
|
|
35
|
-
# Returns the
|
|
36
|
-
#
|
|
37
|
-
# call. {SPEC.md B-04}[link:../../SPEC.md]
|
|
38
|
-
#
|
|
46
|
+
# Returns the bytes the guest wrote to stdout during the most recent
|
|
47
|
+
# +#run+ as a UTF-8 String, clipped at +stdout_limit+. Empty before any
|
|
48
|
+
# +#run+ call. {SPEC.md B-04}[link:../../SPEC.md] — the byte content
|
|
49
|
+
# never contains a truncation sentinel; use +#stdout_truncated?+ to
|
|
50
|
+
# observe overflow.
|
|
39
51
|
def stdout
|
|
40
|
-
@
|
|
52
|
+
@stdout_capture.bytes
|
|
41
53
|
end
|
|
42
54
|
|
|
43
|
-
# Returns the
|
|
44
|
-
#
|
|
45
|
-
# call.
|
|
46
|
-
# marker when the cap was hit.
|
|
55
|
+
# Returns the bytes the guest wrote to stderr during the most recent
|
|
56
|
+
# +#run+ as a UTF-8 String, clipped at +stderr_limit+. Empty before any
|
|
57
|
+
# +#run+ call. Mirror of +#stdout+.
|
|
47
58
|
def stderr
|
|
48
|
-
@
|
|
59
|
+
@stderr_capture.bytes
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns +true+ iff stdout capture during the most recent +#run+
|
|
63
|
+
# exceeded +stdout_limit+ ({SPEC.md B-04}[link:../../SPEC.md]). Resets
|
|
64
|
+
# to +false+ at the start of the next +#run+ ({SPEC.md
|
|
65
|
+
# B-03}[link:../../SPEC.md]).
|
|
66
|
+
def stdout_truncated?
|
|
67
|
+
@stdout_capture.truncated?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns +true+ iff stderr capture during the most recent +#run+
|
|
71
|
+
# exceeded +stderr_limit+. Mirror of +#stdout_truncated?+.
|
|
72
|
+
def stderr_truncated?
|
|
73
|
+
@stderr_capture.truncated?
|
|
49
74
|
end
|
|
50
75
|
|
|
51
76
|
# Build a fresh Sandbox.
|
|
52
77
|
#
|
|
53
78
|
# +wasm_path+ is the absolute path to the Guest Binary; defaults to the
|
|
54
79
|
# gem-bundled +data/kobako.wasm+. +stdout_limit+ and +stderr_limit+ cap
|
|
55
|
-
# the per-run byte ceiling for each capture channel (default 1 MiB
|
|
56
|
-
|
|
80
|
+
# the per-run byte ceiling for each capture channel (default 1 MiB;
|
|
81
|
+
# +nil+ disables the cap). +timeout+ is the wall-clock cap on a single
|
|
82
|
+
# +#run+ in seconds ({SPEC.md B-01}[link:../../SPEC.md]; default 60.0,
|
|
83
|
+
# +nil+ disables); +memory_limit+ caps guest linear memory growth in
|
|
84
|
+
# bytes ({SPEC.md B-01, E-20}[link:../../SPEC.md]; default 5 MiB,
|
|
85
|
+
# +nil+ disables).
|
|
86
|
+
def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
|
|
87
|
+
timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
88
|
+
memory_limit: DEFAULT_MEMORY_LIMIT)
|
|
57
89
|
@wasm_path = wasm_path || Kobako::Wasm.default_path
|
|
58
90
|
@stdout_limit = stdout_limit || DEFAULT_OUTPUT_LIMIT
|
|
59
91
|
@stderr_limit = stderr_limit || DEFAULT_OUTPUT_LIMIT
|
|
60
|
-
@
|
|
61
|
-
@
|
|
62
|
-
@
|
|
63
|
-
@
|
|
64
|
-
@instance.
|
|
92
|
+
@timeout = normalize_timeout(timeout)
|
|
93
|
+
@memory_limit = normalize_memory_limit(memory_limit)
|
|
94
|
+
@services = Kobako::RPC::Server.new
|
|
95
|
+
@instance = Kobako::Wasm::Instance.from_path(@wasm_path, @timeout, @memory_limit, @stdout_limit, @stderr_limit)
|
|
96
|
+
@instance.server = @services
|
|
97
|
+
clear_captures!
|
|
65
98
|
end
|
|
66
99
|
|
|
67
|
-
# Declare or retrieve the
|
|
100
|
+
# Declare or retrieve the Namespace named +name+ on this Sandbox
|
|
68
101
|
# ({SPEC.md B-07, B-09, B-10}[link:../../SPEC.md]). +name+ must be a
|
|
69
|
-
# Symbol or String in constant form. Returns the
|
|
70
|
-
# +Kobako::Registry::ServiceGroup+.
|
|
102
|
+
# Symbol or String in constant form. Returns the +Kobako::RPC::Namespace+.
|
|
71
103
|
#
|
|
72
104
|
# Raises +ArgumentError+ when called after +#run+, or when +name+ does
|
|
73
105
|
# not match the constant-name pattern.
|
|
@@ -82,7 +114,7 @@ module Kobako
|
|
|
82
114
|
#
|
|
83
115
|
# Source delivery uses the WASI stdin two-frame protocol
|
|
84
116
|
# ({SPEC.md ABI Signatures}[link:../../SPEC.md]): Frame 1 carries the
|
|
85
|
-
# msgpack-encoded preamble (
|
|
117
|
+
# msgpack-encoded preamble (Namespace / Member registry snapshot) and Frame 2
|
|
86
118
|
# carries the user script UTF-8 bytes. Each frame is prefixed by a
|
|
87
119
|
# 4-byte big-endian u32 length.
|
|
88
120
|
#
|
|
@@ -94,53 +126,101 @@ module Kobako
|
|
|
94
126
|
|
|
95
127
|
@services.seal!
|
|
96
128
|
reset_run_state!
|
|
97
|
-
preamble = @services.guest_preamble
|
|
98
|
-
@instance.setup_wasi_pipes(@stdout_limit, @stderr_limit, preamble, source.b)
|
|
99
129
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
OutcomeDecoder.decode(outcome_bytes)
|
|
130
|
+
run_guest(@services.encoded_preamble, source.b)
|
|
131
|
+
read_captures!
|
|
132
|
+
take_result!
|
|
104
133
|
end
|
|
105
134
|
|
|
106
135
|
private
|
|
107
136
|
|
|
137
|
+
# Coerce +timeout+ into the Float seconds the ext expects, or +nil+ to
|
|
138
|
+
# mean the cap is disabled ({SPEC.md B-01}[link:../../SPEC.md]). Any
|
|
139
|
+
# finite non-positive value is rejected — a zero or negative timeout
|
|
140
|
+
# would either fire instantly or never, both of which would surprise
|
|
141
|
+
# callers more than an early +ArgumentError+.
|
|
142
|
+
def normalize_timeout(timeout)
|
|
143
|
+
return nil if timeout.nil?
|
|
144
|
+
raise ArgumentError, "timeout must be Numeric or nil, got #{timeout.class}" unless timeout.is_a?(Numeric)
|
|
145
|
+
|
|
146
|
+
seconds = timeout.to_f
|
|
147
|
+
raise ArgumentError, "timeout must be > 0 (got #{timeout})" unless seconds.positive? && seconds.finite?
|
|
148
|
+
|
|
149
|
+
seconds
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Coerce +memory_limit+ into the byte cap the ext expects, or +nil+ to
|
|
153
|
+
# mean unbounded ({SPEC.md B-01, E-20}[link:../../SPEC.md]). Must be a
|
|
154
|
+
# positive Integer when set; +Float+ or zero/negative values are
|
|
155
|
+
# rejected.
|
|
156
|
+
def normalize_memory_limit(memory_limit)
|
|
157
|
+
return nil if memory_limit.nil?
|
|
158
|
+
unless memory_limit.is_a?(Integer) && memory_limit.positive?
|
|
159
|
+
raise ArgumentError, "memory_limit must be a positive Integer or nil, got #{memory_limit.inspect}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
memory_limit
|
|
163
|
+
end
|
|
164
|
+
|
|
108
165
|
# Per-run state reset ({SPEC.md B-03}[link:../../SPEC.md]). Capture
|
|
109
|
-
# buffers and the HandleTable counter are
|
|
166
|
+
# buffers, truncation predicates, and the HandleTable counter are
|
|
167
|
+
# zeroed before the guest runs.
|
|
110
168
|
def reset_run_state!
|
|
111
169
|
@services.reset_handles!
|
|
112
|
-
|
|
113
|
-
@stderr_buffer.clear
|
|
170
|
+
clear_captures!
|
|
114
171
|
end
|
|
115
172
|
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
# (
|
|
119
|
-
#
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@stdout_buffer << stdout_bytes unless stdout_bytes.empty?
|
|
124
|
-
@stderr_buffer << stderr_bytes unless stderr_bytes.empty?
|
|
173
|
+
# Reset both per-channel captures to the pre-run sentinel
|
|
174
|
+
# ({SPEC.md B-05}[link:../../SPEC.md]). Shared by +#initialize+
|
|
175
|
+
# (first-run setup) and +#reset_run_state!+ (between-run reset) so
|
|
176
|
+
# both paths agree on what "empty capture" means.
|
|
177
|
+
def clear_captures!
|
|
178
|
+
@stdout_capture = Capture::EMPTY
|
|
179
|
+
@stderr_capture = Capture::EMPTY
|
|
125
180
|
end
|
|
126
181
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
182
|
+
# Read the per-channel capture pairs (+[bytes, truncated]+) from the
|
|
183
|
+
# ext after a guest run completes and wrap each as a +Capture+ value
|
|
184
|
+
# object. The ext clips +bytes+ to the configured cap and sets
|
|
185
|
+
# +truncated+ when the guest produced strictly more than +cap+ bytes
|
|
186
|
+
# ({SPEC.md B-04}[link:../../SPEC.md]). Mirror of {#clear_captures!}
|
|
187
|
+
# at the post-run boundary.
|
|
188
|
+
def read_captures!
|
|
189
|
+
out_bytes, out_truncated = @instance.stdout
|
|
190
|
+
err_bytes, err_truncated = @instance.stderr
|
|
191
|
+
@stdout_capture = Capture.from_ext(out_bytes, out_truncated)
|
|
192
|
+
@stderr_capture = Capture.from_ext(err_bytes, err_truncated)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Drive +Instance#run+ with the two stdin frames (preamble + source).
|
|
196
|
+
# Wraps wasmtime / wire errors in TrapError so the Sandbox layer maps
|
|
197
|
+
# cleanly to the three-class taxonomy. The configured-cap paths
|
|
198
|
+
# (SPEC.md E-19 / E-20) are routed to the named TrapError subclasses
|
|
199
|
+
# so callers that want to surface a specific reason can rescue them;
|
|
200
|
+
# everything else falls through to the base TrapError.
|
|
201
|
+
def run_guest(preamble, source)
|
|
202
|
+
@instance.run(preamble, source)
|
|
203
|
+
rescue Kobako::Wasm::TimeoutError => e
|
|
204
|
+
raise TimeoutError, "guest exceeded timeout: #{e.message}"
|
|
205
|
+
rescue Kobako::Wasm::MemoryLimitError => e
|
|
206
|
+
raise MemoryLimitError, "guest exceeded memory_limit: #{e.message}"
|
|
133
207
|
rescue Kobako::Wasm::Error => e
|
|
134
208
|
raise TrapError, "guest __kobako_run trapped: #{e.message}"
|
|
135
209
|
end
|
|
136
210
|
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
211
|
+
# Take OUTCOME_BUFFER bytes from guest memory via +Instance#outcome!+
|
|
212
|
+
# and decode them into the Sandbox-level result — the unwrapped mruby
|
|
213
|
+
# return value, or a raised three-layer
|
|
214
|
+
# ({SPEC.md "Error Scenarios"}[link:../../SPEC.md]) exception. A zero-
|
|
215
|
+
# length outcome is delivered to +Kobako::Outcome+ as an empty String
|
|
216
|
+
# so a single boundary attributes every wire-violation outcome
|
|
140
217
|
# ({SPEC.md ABI Signatures}[link:../../SPEC.md]).
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
218
|
+
#
|
|
219
|
+
# The bang reflects the destructive ext call beneath: the underlying
|
|
220
|
+
# +__kobako_take_outcome+ export invalidates the buffer pointer, so this
|
|
221
|
+
# method must be called at most once per +#run+.
|
|
222
|
+
def take_result!
|
|
223
|
+
Outcome.decode(@instance.outcome!)
|
|
144
224
|
rescue Kobako::Wasm::Error => e
|
|
145
225
|
raise TrapError, "failed to read OUTCOME_BUFFER: #{e.message}"
|
|
146
226
|
end
|
data/lib/kobako/version.rb
CHANGED
data/lib/kobako/wasm.rb
CHANGED
|
@@ -5,10 +5,9 @@ module Kobako
|
|
|
5
5
|
# (see ext/kobako/src/wasm.rs). This module is the foundational binding
|
|
6
6
|
# layer for Sandbox (#14), the run path (#16) and RPC dispatch (#18).
|
|
7
7
|
#
|
|
8
|
-
# The classes themselves (
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# reason to live in Rust.
|
|
8
|
+
# The classes themselves (Instance) and the error hierarchy (Error /
|
|
9
|
+
# ModuleNotBuiltError) are defined from Rust at ext load time; this file
|
|
10
|
+
# only adds the pure-Ruby helpers that have no reason to live in Rust.
|
|
12
11
|
module Wasm
|
|
13
12
|
# Absolute path to the gem-bundled `data/kobako.wasm` artifact. Computed
|
|
14
13
|
# from this file's location so it works for both `bundle exec` (running
|
|
@@ -16,20 +15,11 @@ module Kobako
|
|
|
16
15
|
#
|
|
17
16
|
# Returns a String regardless of whether the file currently exists —
|
|
18
17
|
# call sites that need the file to be present should pass this through
|
|
19
|
-
#
|
|
18
|
+
# +Kobako::Wasm::Instance.from_path+, which raises +ModuleNotBuiltError+
|
|
20
19
|
# with a clear remediation message.
|
|
21
20
|
def self.default_path
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# Unpack the +(ptr << 32) | len+ u64 produced by the Rust ext's
|
|
26
|
-
# +__kobako_take_outcome+ export. Returns +[ptr, len]+ as 32-bit
|
|
27
|
-
# unsigned integers. Pure-Ruby helper kept near the ABI surface so
|
|
28
|
-
# Sandbox does not have to carry bit-level wire layout.
|
|
29
|
-
def self.unpack_outcome_ptr_len(packed)
|
|
30
|
-
ptr = (packed >> 32) & 0xffff_ffff
|
|
31
|
-
len = packed & 0xffff_ffff
|
|
32
|
-
[ptr, len]
|
|
21
|
+
dir = __dir__ or raise Error, "Kobako::Wasm.default_path requires __dir__"
|
|
22
|
+
File.expand_path("../../data/kobako.wasm", dir)
|
|
33
23
|
end
|
|
34
24
|
end
|
|
35
25
|
end
|
data/lib/kobako.rb
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Kobako
|
|
2
|
+
module Codec
|
|
3
|
+
class Factory
|
|
4
|
+
include Singleton
|
|
5
|
+
extend Forwardable
|
|
6
|
+
extend SingleForwardable
|
|
7
|
+
|
|
8
|
+
EXT_SYMBOL: Integer
|
|
9
|
+
EXT_HANDLE: Integer
|
|
10
|
+
EXT_ERRENV: Integer
|
|
11
|
+
|
|
12
|
+
def dump: (untyped value) -> String
|
|
13
|
+
def load: (String bytes) -> untyped
|
|
14
|
+
|
|
15
|
+
def self.dump: (untyped value) -> String
|
|
16
|
+
def self.load: (String bytes) -> untyped
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def initialize: () -> void
|
|
21
|
+
def register_symbol: () -> void
|
|
22
|
+
def pack_symbol: (Symbol symbol) -> String
|
|
23
|
+
def unpack_symbol: (String payload) -> Symbol
|
|
24
|
+
def register_handle: () -> void
|
|
25
|
+
def register_fault: () -> void
|
|
26
|
+
def unpack_handle: (String payload) -> Kobako::RPC::Handle
|
|
27
|
+
def pack_fault: (Kobako::RPC::Fault fault) -> String
|
|
28
|
+
def unpack_fault: (String payload) -> Kobako::RPC::Fault
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Kobako
|
|
2
|
+
class Error < StandardError
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
class TrapError < Error
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class TimeoutError < TrapError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class MemoryLimitError < TrapError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class SandboxError < Error
|
|
15
|
+
attr_reader origin: String?
|
|
16
|
+
attr_reader klass: String?
|
|
17
|
+
attr_reader backtrace_lines: Array[String]?
|
|
18
|
+
attr_reader details: untyped
|
|
19
|
+
|
|
20
|
+
def initialize: (
|
|
21
|
+
String message,
|
|
22
|
+
?origin: String?,
|
|
23
|
+
?klass: String?,
|
|
24
|
+
?backtrace_lines: Array[String]?,
|
|
25
|
+
?details: untyped
|
|
26
|
+
) -> void
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class ServiceError < Error
|
|
30
|
+
attr_reader origin: String?
|
|
31
|
+
attr_reader klass: String?
|
|
32
|
+
attr_reader backtrace_lines: Array[String]?
|
|
33
|
+
attr_reader details: untyped
|
|
34
|
+
|
|
35
|
+
def initialize: (
|
|
36
|
+
String message,
|
|
37
|
+
?origin: String?,
|
|
38
|
+
?klass: String?,
|
|
39
|
+
?backtrace_lines: Array[String]?,
|
|
40
|
+
?details: untyped
|
|
41
|
+
) -> void
|
|
42
|
+
|
|
43
|
+
class Disconnected < ServiceError
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class HandleTableError < SandboxError
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class HandleTableExhausted < HandleTableError
|
|
51
|
+
end
|
|
52
|
+
end
|