kobako 0.12.2 → 0.14.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 +41 -0
- data/Cargo.lock +126 -137
- data/README.md +5 -3
- data/ROADMAP.md +26 -0
- data/crates/kobako-runtime/CHANGELOG.md +15 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/error.rs +8 -0
- data/crates/kobako-runtime/src/lib.rs +4 -2
- data/crates/kobako-runtime/src/profile.rs +35 -0
- data/crates/kobako-runtime/src/runtime.rs +7 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +15 -0
- data/crates/kobako-wasmtime/Cargo.toml +4 -4
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/ambient.rs +5 -2
- data/crates/kobako-wasmtime/src/config.rs +10 -2
- data/crates/kobako-wasmtime/src/driver.rs +13 -1
- data/crates/kobako-wasmtime/src/frames.rs +105 -5
- data/crates/kobako-wasmtime/src/instance_pre.rs +7 -6
- data/crates/kobako-wasmtime/src/invocation.rs +4 -5
- data/crates/kobako-wasmtime/src/trap.rs +42 -21
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +0 -2
- data/ext/kobako/src/runtime.rs +114 -38
- data/lib/kobako/catalog/handles.rb +2 -2
- data/lib/kobako/catalog/namespaces.rb +2 -2
- data/lib/kobako/catalog/snippets.rb +1 -1
- data/lib/kobako/codec/decoder.rb +7 -7
- data/lib/kobako/codec/encoder.rb +2 -2
- data/lib/kobako/codec/error.rb +3 -3
- data/lib/kobako/codec/factory.rb +10 -10
- data/lib/kobako/codec/handle_walk.rb +17 -21
- data/lib/kobako/codec/utils.rb +9 -9
- data/lib/kobako/codec.rb +4 -4
- data/lib/kobako/errors.rb +14 -39
- data/lib/kobako/handle.rb +5 -2
- data/lib/kobako/namespace.rb +3 -3
- data/lib/kobako/outcome.rb +1 -0
- data/lib/kobako/sandbox.rb +61 -33
- data/lib/kobako/sandbox_options.rb +51 -9
- data/lib/kobako/transport/dispatcher.rb +17 -14
- data/lib/kobako/transport/request.rb +2 -2
- data/lib/kobako/transport/response.rb +2 -2
- data/lib/kobako/transport/run.rb +1 -1
- data/lib/kobako/transport/yield.rb +3 -3
- data/lib/kobako/transport/yielder.rb +6 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +0 -1
- data/release-please-config.json +66 -6
- data/sig/kobako/codec/handle_walk.rbs +2 -2
- data/sig/kobako/codec.rbs +11 -0
- data/sig/kobako/handle.rbs +0 -2
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +11 -2
- data/sig/kobako/transport/dispatcher.rbs +8 -8
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -0
- metadata +3 -4
- data/ext/kobako/src/snapshot.rs +0 -75
- data/lib/kobako/snapshot.rb +0 -32
- data/sig/kobako/snapshot.rbs +0 -12
data/lib/kobako/errors.rb
CHANGED
|
@@ -4,38 +4,13 @@
|
|
|
4
4
|
module Kobako
|
|
5
5
|
# Error taxonomy.
|
|
6
6
|
#
|
|
7
|
-
# Every
|
|
8
|
-
# exactly one of
|
|
9
|
-
# guest binary returns control to the
|
|
10
|
-
# the outcome-envelope tag.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# * {TrapError} — Wasm engine layer (trap, OOM, unreachable, or a
|
|
15
|
-
# wire-violation fallback signalling a corrupted
|
|
16
|
-
# guest runtime).
|
|
17
|
-
# * {SandboxError} — sandbox / wire layer (mruby script error,
|
|
18
|
-
# wire-decode failure on an otherwise valid tag,
|
|
19
|
-
# Catalog::Handles exhaustion, output buffer overrun).
|
|
20
|
-
# * {ServiceError} — service / capability layer (a Service capability
|
|
21
|
-
# call that failed and was not rescued inside the
|
|
22
|
-
# script).
|
|
23
|
-
#
|
|
24
|
-
# Two further branches sit outside the invocation taxonomy:
|
|
25
|
-
#
|
|
26
|
-
# * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
|
|
27
|
-
# when the wasm runtime cannot be built from the
|
|
28
|
-
# configured +wasm_path+ before any invocation runs.
|
|
29
|
-
# Not an invocation outcome, so it never passes
|
|
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+.
|
|
33
|
-
#
|
|
34
|
-
# Named subclasses:
|
|
35
|
-
#
|
|
36
|
-
# * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
|
|
37
|
-
# at +wasm_path+.
|
|
38
|
-
# * {HandleExhaustedError} < {SandboxError} — Handle id cap hit.
|
|
7
|
+
# Every +Kobako::Sandbox+ invocation (+#eval+ or +#run+) either returns a
|
|
8
|
+
# value or raises exactly one of TrapError, SandboxError, or ServiceError.
|
|
9
|
+
# Attribution is decided after the guest binary returns control to the
|
|
10
|
+
# host: first the Wasm-trap layer, then the outcome-envelope tag.
|
|
11
|
+
# SetupError and PoolTimeoutError sit outside the invocation taxonomy and
|
|
12
|
+
# never pass through that attribution decision. Each class below
|
|
13
|
+
# documents its own layer.
|
|
39
14
|
|
|
40
15
|
# Base for all kobako-raised errors so callers that want to ignore the
|
|
41
16
|
# taxonomy can rescue a single class.
|
|
@@ -48,8 +23,8 @@ module Kobako
|
|
|
48
23
|
#
|
|
49
24
|
# Two named subclasses cover the configured per-invocation caps:
|
|
50
25
|
#
|
|
51
|
-
# *
|
|
52
|
-
# *
|
|
26
|
+
# * TimeoutError — wall-clock +timeout+ exceeded.
|
|
27
|
+
# * MemoryLimitError — guest +memory.grow+ would exceed
|
|
53
28
|
# +memory_limit+.
|
|
54
29
|
#
|
|
55
30
|
# Host Apps that only care about "guest is unrecoverable, discard the
|
|
@@ -128,14 +103,14 @@ module Kobako
|
|
|
128
103
|
class HandleExhaustedError < SandboxError; end
|
|
129
104
|
|
|
130
105
|
# BytecodeError is the SandboxError subclass raised when a
|
|
131
|
-
#
|
|
132
|
-
# first invocation's snippet replay against a fresh
|
|
106
|
+
# +#preload(binary:)+ snippet fails structural validation during the
|
|
107
|
+
# first invocation's snippet replay against a fresh +mrb_state+ (RITE
|
|
133
108
|
# version mismatch or corrupt body). Bytecode that loads cleanly and
|
|
134
|
-
# then raises at top level surfaces as plain
|
|
109
|
+
# then raises at top level surfaces as plain +SandboxError+ with the
|
|
135
110
|
# natural mruby class preserved. Inherits from SandboxError so a single
|
|
136
|
-
#
|
|
111
|
+
# +rescue Kobako::SandboxError+ covers both source and bytecode
|
|
137
112
|
# snippet failures while callers wanting bytecode-specific handling
|
|
138
|
-
# can
|
|
113
|
+
# can +rescue Kobako::BytecodeError+ directly.
|
|
139
114
|
class BytecodeError < SandboxError; end
|
|
140
115
|
|
|
141
116
|
# Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
|
data/lib/kobako/handle.rb
CHANGED
|
@@ -14,8 +14,10 @@ module Kobako
|
|
|
14
14
|
# The constructor is internal to the Host Gem. +Kobako::Handle.new+ is
|
|
15
15
|
# privatised so Host App code cannot fabricate a Handle from a bare
|
|
16
16
|
# integer; legitimate Handle instances enter Host App code only as
|
|
17
|
-
# fields on raised error objects.
|
|
18
|
-
#
|
|
17
|
+
# fields on raised error objects. +#with+ — Data's copy-with-changes
|
|
18
|
+
# constructor — is removed for the same reason: a legitimate Handle
|
|
19
|
+
# must not derive a sibling with a caller-chosen id. The Host Gem itself constructs
|
|
20
|
+
# Handles through +.restore+, which exists at exactly two call
|
|
19
21
|
# sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
|
|
20
22
|
# +Kobako::Codec::HandleWalk.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
|
|
21
23
|
# (allocator paths). Both live inside +lib/kobako/+ and are not part
|
|
@@ -41,6 +43,7 @@ module Kobako
|
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
private_class_method :new
|
|
46
|
+
undef_method :with
|
|
44
47
|
|
|
45
48
|
# Host Gem–internal factory. Allocates the Data instance through
|
|
46
49
|
# +Class#allocate+ and dispatches +#initialize+ explicitly so the
|
data/lib/kobako/namespace.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
# A named grouping of Members for one Sandbox.
|
|
5
5
|
# Returned by +Sandbox#define+. Each instance owns a flat name→object
|
|
6
|
-
# table of Members; member binding is validated against
|
|
6
|
+
# table of Members; member binding is validated against NAME_PATTERN.
|
|
7
7
|
class Namespace
|
|
8
8
|
# Ruby constant-name pattern shared by Namespace and Member names.
|
|
9
9
|
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
@@ -11,7 +11,7 @@ module Kobako
|
|
|
11
11
|
attr_reader :name
|
|
12
12
|
|
|
13
13
|
# Build a new Namespace. +name+ is an already-validated Namespace
|
|
14
|
-
# name (must satisfy
|
|
14
|
+
# name (must satisfy NAME_PATTERN; validation is the caller's
|
|
15
15
|
# responsibility).
|
|
16
16
|
def initialize(name)
|
|
17
17
|
@name = name
|
|
@@ -38,7 +38,7 @@ module Kobako
|
|
|
38
38
|
|
|
39
39
|
# Mark this Namespace as sealed. Called by
|
|
40
40
|
# +Kobako::Catalog::Namespaces#seal!+ on the owning Sandbox's first
|
|
41
|
-
# invocation; afterwards
|
|
41
|
+
# invocation; afterwards #bind raises +ArgumentError+. Idempotent;
|
|
42
42
|
# returns +self+.
|
|
43
43
|
def seal!
|
|
44
44
|
@sealed = true
|
data/lib/kobako/outcome.rb
CHANGED
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -38,14 +38,16 @@ module Kobako
|
|
|
38
38
|
|
|
39
39
|
attr_reader :wasm_path, :options
|
|
40
40
|
|
|
41
|
-
# Per-
|
|
41
|
+
# Per-option accessors forward to the immutable +SandboxOptions+ Value
|
|
42
42
|
# Object so the Host App still reads them off Sandbox directly.
|
|
43
|
-
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit
|
|
43
|
+
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile
|
|
44
44
|
|
|
45
45
|
# Returns the bytes the guest wrote to stdout during the most recent
|
|
46
46
|
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
47
47
|
# any invocation; the byte content never contains a truncation sentinel,
|
|
48
|
-
# so use +#stdout_truncated?+ to observe overflow.
|
|
48
|
+
# so use +#stdout_truncated?+ to observe overflow. Populated on every
|
|
49
|
+
# outcome — including a rescued +TrapError+, after which it holds the
|
|
50
|
+
# bytes written before the trap fired — mirroring +#usage+.
|
|
49
51
|
def stdout
|
|
50
52
|
@stdout_capture.bytes
|
|
51
53
|
end
|
|
@@ -83,25 +85,24 @@ module Kobako
|
|
|
83
85
|
# Build a fresh Sandbox.
|
|
84
86
|
#
|
|
85
87
|
# +wasm_path+ is the absolute path to the Guest Binary; defaults to the
|
|
86
|
-
# gem-bundled +data/kobako.wasm+.
|
|
87
|
-
# +stderr_limit+, +timeout+, +memory_limit+)
|
|
88
|
-
# +
|
|
88
|
+
# gem-bundled +data/kobako.wasm+. Every other keyword — the four caps
|
|
89
|
+
# (+stdout_limit+, +stderr_limit+, +timeout+, +memory_limit+) and the
|
|
90
|
+
# requested isolation profile (+profile+) — is forwarded verbatim to
|
|
91
|
+
# +Kobako::SandboxOptions+, which owns the DEFAULT fallbacks and
|
|
89
92
|
# normalisation. The constructed +SandboxOptions+ is exposed as
|
|
90
|
-
# +#options+ and
|
|
91
|
-
# +Forwardable+ delegation.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
# +#options+ and every option remains readable directly on Sandbox via
|
|
94
|
+
# +Forwardable+ delegation. The runtime builds the requested profile —
|
|
95
|
+
# +:hermetic+ (the default) denies the guest ambient time and entropy,
|
|
96
|
+
# +:permissive+ leaves them live — and construction refuses a runtime
|
|
97
|
+
# whose declared profile falls below the request, raising
|
|
98
|
+
# +Kobako::SetupError+ before any invocation entry point runs.
|
|
99
|
+
def initialize(wasm_path: nil, **)
|
|
97
100
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
98
|
-
@options = SandboxOptions.new(
|
|
99
|
-
stderr_limit: stderr_limit)
|
|
101
|
+
@options = SandboxOptions.new(**)
|
|
100
102
|
@handler = Catalog::Handles.new
|
|
101
103
|
@services = Kobako::Catalog::Namespaces.new(handler: @handler)
|
|
102
104
|
@snippets = Catalog::Snippets.new
|
|
103
|
-
@runtime =
|
|
104
|
-
@options.stdout_limit, @options.stderr_limit)
|
|
105
|
+
@runtime = build_runtime!
|
|
105
106
|
install_dispatch_proc!
|
|
106
107
|
reset_invocation_state!
|
|
107
108
|
end
|
|
@@ -119,7 +120,9 @@ module Kobako
|
|
|
119
120
|
# Register a snippet on this Sandbox in one of two forms:
|
|
120
121
|
#
|
|
121
122
|
# * +preload(code: source, name: Name)+ — +source+ is mruby source
|
|
122
|
-
# as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+.
|
|
123
|
+
# as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. Compile
|
|
124
|
+
# failures surface as +Kobako::SandboxError+ on the first
|
|
125
|
+
# invocation's replay. The +name+
|
|
123
126
|
# becomes the snippet's +(snippet:Name)+ backtrace filename and
|
|
124
127
|
# is the dedupe key that rejects a duplicate +code:+ snippet.
|
|
125
128
|
# * +preload(binary: bytes)+ — +bytes+ is precompiled RITE
|
|
@@ -209,6 +212,17 @@ module Kobako
|
|
|
209
212
|
|
|
210
213
|
private
|
|
211
214
|
|
|
215
|
+
# Construct the +Runtime+ with the requested isolation profile and
|
|
216
|
+
# refuse one whose declared posture falls below the request —
|
|
217
|
+
# +SandboxOptions#enforce_floor!+ owns the ladder comparison, so a
|
|
218
|
+
# runtime that cannot honor the request never runs guest code.
|
|
219
|
+
def build_runtime!
|
|
220
|
+
runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
221
|
+
@options.stdout_limit, @options.stderr_limit, @options.profile)
|
|
222
|
+
@options.enforce_floor!(runtime.profile)
|
|
223
|
+
runtime
|
|
224
|
+
end
|
|
225
|
+
|
|
212
226
|
# Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
|
|
213
227
|
# dispatch +Proc+ that routes guest→host calls through the stateless
|
|
214
228
|
# +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
|
|
@@ -242,13 +256,11 @@ module Kobako
|
|
|
242
256
|
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
243
257
|
# and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
|
|
244
258
|
# both paths: the figures are stashed in the ext on every outcome, so
|
|
245
|
-
#
|
|
246
|
-
#
|
|
259
|
+
# the readout here also covers the trap path, where +Runtime#eval+ /
|
|
260
|
+
# +#run+ raise instead of returning outcome bytes.
|
|
247
261
|
#
|
|
248
|
-
# The ext
|
|
249
|
-
#
|
|
250
|
-
# destructure-then-kwargs handoff below is the explicit
|
|
251
|
-
# positional→keyword conversion point.
|
|
262
|
+
# The ext-side contract is positional: +Runtime#usage+ yields
|
|
263
|
+
# +[wall_time, memory_peak]+ in +Kobako::Usage+ field order.
|
|
252
264
|
def read_usage!
|
|
253
265
|
wall_time, memory_peak = @runtime.usage
|
|
254
266
|
@usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
@@ -269,33 +281,49 @@ module Kobako
|
|
|
269
281
|
end
|
|
270
282
|
end
|
|
271
283
|
|
|
284
|
+
# Read the per-last-invocation output captures from the ext and wrap
|
|
285
|
+
# them as +Kobako::Capture+ value objects. Runs in the +invoke!+
|
|
286
|
+
# +ensure+ block next to #read_usage! for the same reason: the ext
|
|
287
|
+
# stashes the captures on every outcome, so the readout also covers
|
|
288
|
+
# the trap path, where +Runtime#eval+ / +#run+ raise instead of
|
|
289
|
+
# returning outcome bytes — +#stdout+ / +#stderr+ keep the guest's
|
|
290
|
+
# partial output readable after a rescue.
|
|
291
|
+
#
|
|
292
|
+
# The ext-side contract is positional: +Runtime#captures+ yields
|
|
293
|
+
# +[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]+.
|
|
294
|
+
def read_captures!
|
|
295
|
+
stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures
|
|
296
|
+
@stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
|
|
297
|
+
@stderr_capture = Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
|
|
298
|
+
end
|
|
299
|
+
|
|
272
300
|
# Shared prologue / epilogue + trap-class translator for both
|
|
273
301
|
# invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
|
|
274
302
|
# TrapError message so the failing export is identifiable.
|
|
275
303
|
#
|
|
276
|
-
# The yielded block must return
|
|
277
|
-
# value of +Runtime#eval+ / +#run
|
|
278
|
-
#
|
|
279
|
-
# +
|
|
280
|
-
#
|
|
304
|
+
# The yielded block must return the invocation's raw outcome bytes —
|
|
305
|
+
# i.e. the value of +Runtime#eval+ / +#run+ — which the success path
|
|
306
|
+
# feeds to +Outcome.decode+. Captures and usage are populated by the
|
|
307
|
+
# +ensure+ readouts (#read_usage! / #read_captures!) on every
|
|
308
|
+
# outcome, so +#stdout+ / +#stderr+ / +#usage+ stay readable after a
|
|
309
|
+
# rescued trap.
|
|
281
310
|
# The rescue chain is the single trap-translation boundary —
|
|
282
311
|
# configured-cap paths surface as named TrapError subclasses
|
|
283
312
|
# (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
|
|
284
313
|
# the base +TrapError+.
|
|
285
314
|
def invoke!(verb)
|
|
286
315
|
begin_invocation!
|
|
287
|
-
|
|
288
|
-
@stdout_capture = snapshot.stdout
|
|
289
|
-
@stderr_capture = snapshot.stderr
|
|
316
|
+
return_bytes = yield
|
|
290
317
|
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
291
318
|
# token; restore it to the host object the guest referenced before
|
|
292
319
|
# handing the value to the Host App. @handler still holds this
|
|
293
320
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
294
|
-
Codec::HandleWalk.deep_restore(Outcome.decode(
|
|
321
|
+
Codec::HandleWalk.deep_restore(Outcome.decode(return_bytes), @handler)
|
|
295
322
|
rescue Kobako::TrapError => e
|
|
296
323
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
297
324
|
ensure
|
|
298
325
|
read_usage!
|
|
326
|
+
read_captures!
|
|
299
327
|
end
|
|
300
328
|
end
|
|
301
329
|
end
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
3
5
|
module Kobako
|
|
4
6
|
# Kobako::SandboxOptions — immutable Value Object holding the four
|
|
5
|
-
# per-Sandbox configuration caps
|
|
6
|
-
# Data.define(...)+ subclass form (the
|
|
7
|
-
# +lib/kobako/outcome/panic.rb+).
|
|
7
|
+
# per-Sandbox configuration caps and the requested isolation profile.
|
|
8
|
+
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
9
|
+
# Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
|
|
8
10
|
#
|
|
9
|
-
# The +initialize+ normalises every
|
|
11
|
+
# The +initialize+ normalises every option before delegating to Data's
|
|
10
12
|
# +super+: +timeout+ to Float seconds, +memory_limit+ / +stdout_limit+ /
|
|
11
13
|
# +stderr_limit+ to positive Integer bytes. Each cap is +nil+-disablable
|
|
12
14
|
# (an absent argument takes its DEFAULT; an explicit +nil+ leaves the
|
|
13
|
-
# bound off), so all four behave uniformly.
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
15
|
+
# bound off), so all four behave uniformly. +profile+ is the one
|
|
16
|
+
# non-cap: a Symbol on the PROFILES ladder naming the posture the
|
|
17
|
+
# runtime builds, which is also the weakest the Host App accepts —
|
|
18
|
+
# +nil+ is rejected because the weakest posture is requested as an
|
|
19
|
+
# explicit +:permissive+. Anything that survives +SandboxOptions.new+
|
|
20
|
+
# is a wire-ready bundle the +Kobako::Runtime+ constructor consumes
|
|
21
|
+
# as-is. The options also own the ladder comparison
|
|
22
|
+
# (+#enforce_floor!+) that +Kobako::Sandbox+ delegates its
|
|
23
|
+
# construction floor check to.
|
|
24
|
+
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile)
|
|
17
25
|
# Default wall-clock timeout for a single invocation: 60 seconds.
|
|
18
26
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
19
27
|
|
|
@@ -25,17 +33,41 @@ module Kobako
|
|
|
25
33
|
# Default per-channel capture ceiling: 1 MiB.
|
|
26
34
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
27
35
|
|
|
36
|
+
# The isolation ladder, weakest first — index order is rank order,
|
|
37
|
+
# so a floor check is an index comparison.
|
|
38
|
+
PROFILES = %i[permissive hermetic].freeze
|
|
39
|
+
|
|
40
|
+
# Default isolation profile: the strictest rung — opting down to
|
|
41
|
+
# +:permissive+ is the Host App's explicit trade.
|
|
42
|
+
DEFAULT_PROFILE = :hermetic
|
|
43
|
+
|
|
28
44
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
29
45
|
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
30
46
|
stdout_limit: DEFAULT_OUTPUT_LIMIT,
|
|
31
|
-
stderr_limit: DEFAULT_OUTPUT_LIMIT
|
|
47
|
+
stderr_limit: DEFAULT_OUTPUT_LIMIT,
|
|
48
|
+
profile: DEFAULT_PROFILE)
|
|
32
49
|
timeout = normalize_timeout(timeout)
|
|
33
50
|
memory_limit = normalize_memory_limit(memory_limit)
|
|
34
51
|
stdout_limit = normalize_output_limit(stdout_limit, "stdout_limit")
|
|
35
52
|
stderr_limit = normalize_output_limit(stderr_limit, "stderr_limit")
|
|
53
|
+
profile = normalize_profile(profile)
|
|
36
54
|
super
|
|
37
55
|
end
|
|
38
56
|
|
|
57
|
+
# Enforce the requested +profile+ as the floor against +declared+ —
|
|
58
|
+
# the posture a runtime reports having built — so a runtime that
|
|
59
|
+
# cannot honor the request fails construction with
|
|
60
|
+
# +Kobako::SetupError+ instead of weakening the posture silently.
|
|
61
|
+
# Both fallbacks fail closed: a declaration off the PROFILES ladder
|
|
62
|
+
# ranks below every request, and a request off the ladder
|
|
63
|
+
# (unreachable past +initialize+) refuses every declaration.
|
|
64
|
+
def enforce_floor!(declared)
|
|
65
|
+
return if (PROFILES.index(declared) || -1) >= (PROFILES.index(profile) || PROFILES.size)
|
|
66
|
+
|
|
67
|
+
raise Kobako::SetupError, "runtime declares isolation profile #{declared.inspect}, " \
|
|
68
|
+
"below the requested floor #{profile.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
39
71
|
private
|
|
40
72
|
|
|
41
73
|
# Coerce +timeout+ into the Float seconds the ext expects, or +nil+
|
|
@@ -78,5 +110,15 @@ module Kobako
|
|
|
78
110
|
|
|
79
111
|
limit
|
|
80
112
|
end
|
|
113
|
+
|
|
114
|
+
# Validate +profile+ against the PROFILES ladder. Unlike the caps
|
|
115
|
+
# there is no +nil+ form: the weakest posture is requested as an
|
|
116
|
+
# explicit +:permissive+, so anything off the ladder — +nil+
|
|
117
|
+
# included — is rejected.
|
|
118
|
+
def normalize_profile(profile)
|
|
119
|
+
return profile if PROFILES.include?(profile)
|
|
120
|
+
|
|
121
|
+
raise ArgumentError, "profile must be one of #{PROFILES.map(&:inspect).join(", ")}, got #{profile.inspect}"
|
|
122
|
+
end
|
|
81
123
|
end
|
|
82
124
|
end
|
|
@@ -27,7 +27,7 @@ module Kobako
|
|
|
27
27
|
# Kobako::Transport::Dispatcher.dispatch(request_bytes, namespaces, handler, yield_to_guest)
|
|
28
28
|
# # => msgpack-encoded Response bytes (never raises)
|
|
29
29
|
module Dispatcher
|
|
30
|
-
# Throw tag for the
|
|
30
|
+
# Throw tag for the Yielder's break unwind back to the
|
|
31
31
|
# dispatcher's +catch+ frame. +private_constant+ is a
|
|
32
32
|
# convention boundary — not a defence.
|
|
33
33
|
BREAK_THROW = :__kobako_break__
|
|
@@ -54,12 +54,12 @@ module Kobako
|
|
|
54
54
|
# Callable gadget types whose own public methods are reflection surface
|
|
55
55
|
# (+Proc#binding+ reaches +Binding#eval+, +Method#receiver+ / +#unbind+
|
|
56
56
|
# hand back the underlying object) rather than Service behaviour. Only
|
|
57
|
-
#
|
|
57
|
+
# CALLABLE_ALLOW is reachable on a target of these types; a bound
|
|
58
58
|
# lambda stays invocable, its reflective surface does not.
|
|
59
59
|
GADGET_OWNERS = [Proc, Method, UnboundMethod, Binding].freeze
|
|
60
60
|
private_constant :GADGET_OWNERS
|
|
61
61
|
|
|
62
|
-
# The sole methods reachable on a
|
|
62
|
+
# The sole methods reachable on a GADGET_OWNERS target: invoking it
|
|
63
63
|
# (+call+ / +[]+ / +yield+) and the harmless +arity+ / +lambda?+
|
|
64
64
|
# describers that aid guest-side debugging.
|
|
65
65
|
CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
|
|
@@ -91,7 +91,7 @@ module Kobako
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# Resolve positional and keyword arguments off +request+ in one
|
|
94
|
-
# step. Both pass through
|
|
94
|
+
# step. Both pass through #resolve_arg so Capability Handles
|
|
95
95
|
# round-trip back to the host-side Ruby object before the call
|
|
96
96
|
# reaches +public_send+.
|
|
97
97
|
def resolve_call_args(request, handler)
|
|
@@ -101,7 +101,7 @@ module Kobako
|
|
|
101
101
|
|
|
102
102
|
# Map an error caught at the dispatch boundary to a +Response.error+
|
|
103
103
|
# envelope (binary msgpack). +error+ is the +StandardError+ caught by
|
|
104
|
-
#
|
|
104
|
+
# #dispatch's rescue; the +type+ field tells the guest which kind
|
|
105
105
|
# of failure it was so it can raise the matching proxy-side error.
|
|
106
106
|
def encode_caught_error(error)
|
|
107
107
|
case error
|
|
@@ -119,8 +119,8 @@ module Kobako
|
|
|
119
119
|
# reject calls to no-kwarg methods when the wire carries the
|
|
120
120
|
# uniform empty-map shape.
|
|
121
121
|
#
|
|
122
|
-
# +yielder+ is the host-side
|
|
123
|
-
# call site supplied a block; its
|
|
122
|
+
# +yielder+ is the host-side Yielder materialised when the guest
|
|
123
|
+
# call site supplied a block; its Yielder#to_proc
|
|
124
124
|
# rides the +&block+ slot. +&nil+ is a no-op block argument in Ruby,
|
|
125
125
|
# so the same call site handles both cases without an explicit
|
|
126
126
|
# conditional.
|
|
@@ -137,8 +137,8 @@ module Kobako
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
# Guard the +public_send+ below against ambient reflection methods.
|
|
140
|
-
# A public method whose owner is a
|
|
141
|
-
# rejected, except
|
|
140
|
+
# A public method whose owner is a META_OWNERS or GADGET_OWNERS module is
|
|
141
|
+
# rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
|
|
142
142
|
# stays invocable). A name with no concrete public method is allowed
|
|
143
143
|
# only when the target opts into it via +respond_to?+ (dynamic
|
|
144
144
|
# +method_missing+ Services), since the dangerous methods are all
|
|
@@ -159,7 +159,7 @@ module Kobako
|
|
|
159
159
|
# Consult the target's opt-in narrowing predicate. A bound object
|
|
160
160
|
# may define a private +respond_to_guest?(name)+ to restrict which of its
|
|
161
161
|
# methods the guest reaches; a falsy answer rejects the dispatch.
|
|
162
|
-
# The predicate composes beneath
|
|
162
|
+
# The predicate composes beneath #reject_meta_method! — it only narrows,
|
|
163
163
|
# never re-opening the reflection surface the floor rejects — and is
|
|
164
164
|
# consulted with the private surface included so the guest's +public_send+
|
|
165
165
|
# dispatch can never reach +respond_to_guest?+ itself.
|
|
@@ -213,7 +213,7 @@ module Kobako
|
|
|
213
213
|
# Encode +value+ as a +Response.ok+ envelope. When the value is not
|
|
214
214
|
# wire-representable per the codec's type mapping, the
|
|
215
215
|
# +UnsupportedType+ rescue routes it through the
|
|
216
|
-
# Catalog::Handles via
|
|
216
|
+
# Catalog::Handles via #wrap_as_handle and re-encodes with the Capability
|
|
217
217
|
# Handle in place. The happy path encodes exactly once.
|
|
218
218
|
def encode_ok(value, handler)
|
|
219
219
|
response = Kobako::Transport::Response.ok(value)
|
|
@@ -224,15 +224,18 @@ module Kobako
|
|
|
224
224
|
|
|
225
225
|
# Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
|
|
226
226
|
# that the wire codec can carry. Used as the fallback path of
|
|
227
|
-
#
|
|
227
|
+
# #encode_ok when +value+ has no wire representation.
|
|
228
228
|
def wrap_as_handle(value, handler)
|
|
229
229
|
handler.alloc(value)
|
|
230
230
|
end
|
|
231
231
|
|
|
232
|
+
# +message+ folds to UTF-8 first: Ruby core builds some exception
|
|
233
|
+
# messages as ASCII-8BIT (the arity ArgumentError, for one), and
|
|
234
|
+
# the codec would ride those as bin — a shape the guest refuses.
|
|
232
235
|
def encode_error(type, message)
|
|
236
|
+
message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
233
237
|
fault = Kobako::Fault.new(type: type, message: message)
|
|
234
|
-
|
|
235
|
-
response.encode
|
|
238
|
+
Kobako::Transport::Response.error(fault).encode
|
|
236
239
|
end
|
|
237
240
|
end
|
|
238
241
|
end
|
|
@@ -13,7 +13,7 @@ module Kobako
|
|
|
13
13
|
# 5-element msgpack array:
|
|
14
14
|
# +[target, method_name, args, kwargs, block_given]+. +target+ is
|
|
15
15
|
# either a +String+ (+"<Namespace>::<Member>"+, e.g. +"MyService::KV"+)
|
|
16
|
-
# or a
|
|
16
|
+
# or a Handle. SPEC pins +kwargs+ map keys to ext 0x00 Symbol;
|
|
17
17
|
# enforced at construction so the Value Object is the single source of
|
|
18
18
|
# truth. +block_given+ is a Boolean signalling whether the guest call
|
|
19
19
|
# site supplied a block; the block body itself never crosses the
|
|
@@ -43,7 +43,7 @@ module Kobako
|
|
|
43
43
|
Codec::Encoder.encode([target, method_name, args, kwargs, block_given])
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# Decode +bytes+ into a
|
|
46
|
+
# Decode +bytes+ into a Request. Raises +Codec::InvalidType+ when the
|
|
47
47
|
# envelope is not the expected 5-element msgpack array, or when the
|
|
48
48
|
# Value Object's construction invariants reject the decoded fields.
|
|
49
49
|
def self.decode(bytes)
|
|
@@ -20,7 +20,7 @@ module Kobako
|
|
|
20
20
|
#
|
|
21
21
|
# 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
|
|
22
22
|
# (success) or 1 (fault). For success the second element is the return
|
|
23
|
-
# value; for fault it is a
|
|
23
|
+
# value; for fault it is a Fault (ext 0x02 envelope).
|
|
24
24
|
#
|
|
25
25
|
# Built on the +class X < Data.define(...)+ subclass form so the
|
|
26
26
|
# class body is fully Steep-visible; see +lib/kobako/outcome/panic.rb+
|
|
@@ -58,7 +58,7 @@ module Kobako
|
|
|
58
58
|
Codec::Encoder.encode([status, payload])
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# Decode +bytes+ into a
|
|
61
|
+
# Decode +bytes+ into a Response. Raises +Codec::InvalidType+ when the
|
|
62
62
|
# envelope is not the expected 2-element msgpack array, or when the
|
|
63
63
|
# Value Object's construction invariants reject the decoded fields.
|
|
64
64
|
def self.decode(bytes)
|
data/lib/kobako/transport/run.rb
CHANGED
|
@@ -51,7 +51,7 @@ module Kobako
|
|
|
51
51
|
# Encode this Run to the msgpack bytes the guest's +__kobako_run+
|
|
52
52
|
# entry point consumes as its command-buffer payload
|
|
53
53
|
# ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
|
|
54
|
-
# Walks +args+ / +kwargs+ through
|
|
54
|
+
# Walks +args+ / +kwargs+ through Codec::HandleWalk.deep_wrap so
|
|
55
55
|
# any non-wire-representable leaf is allocated into +handler+ and
|
|
56
56
|
# replaced with a +Kobako::Handle+; the
|
|
57
57
|
# +handler+ argument is the Sandbox's table, sharing the same
|
|
@@ -10,9 +10,9 @@ module Kobako
|
|
|
10
10
|
# First byte of the YieldResponse for the success branch — body is
|
|
11
11
|
# the block's return value encoded as a single msgpack value.
|
|
12
12
|
TAG_OK = 0x01
|
|
13
|
-
# First byte for
|
|
13
|
+
# First byte for +break val+ — body is the break value.
|
|
14
14
|
TAG_BREAK = 0x02
|
|
15
|
-
# Reserved for future
|
|
15
|
+
# Reserved for future +return val+ support; both sides reject this
|
|
16
16
|
# tag as a wire violation (YieldResponse envelope contract).
|
|
17
17
|
TAG_RESERVED = 0x03
|
|
18
18
|
# First byte for an error / fault outcome — body is a
|
|
@@ -58,7 +58,7 @@ module Kobako
|
|
|
58
58
|
[tag].pack("C") + Codec::Encoder.encode(value)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# Decode +bytes+ into a
|
|
61
|
+
# Decode +bytes+ into a Yield. Rejects empty input, the reserved
|
|
62
62
|
# tag 0x03, and any tag outside +LIVE_TAGS+ by raising
|
|
63
63
|
# +Kobako::Codec::InvalidType+ — these are wire violations per the
|
|
64
64
|
# SPEC's YieldResponse envelope contract.
|
|
@@ -13,7 +13,7 @@ module Kobako
|
|
|
13
13
|
# Each guest call that carries +block_given: true+ gets a Yielder
|
|
14
14
|
# that the Dispatcher hands to the Service method as +&block+. The
|
|
15
15
|
# Service method observes it as an ordinary Ruby Proc through
|
|
16
|
-
#
|
|
16
|
+
# #to_proc; +yield val+ / +block.call(val)+ invokes #yield, which
|
|
17
17
|
# serialises the positional args, re-enters the guest via the injected
|
|
18
18
|
# +yield_to_guest+ lambda, and reifies the +YieldResponse+ into Ruby
|
|
19
19
|
# control flow:
|
|
@@ -24,13 +24,13 @@ module Kobako
|
|
|
24
24
|
# * +tag 0x04+ error — raise the +{class, message}+ payload at the
|
|
25
25
|
# Service's yield site
|
|
26
26
|
#
|
|
27
|
-
# The Dispatcher calls
|
|
27
|
+
# The Dispatcher calls #invalidate! from its +ensure+ block once
|
|
28
28
|
# dispatch completes; any later call to a stashed Yielder then raises
|
|
29
29
|
# +LocalJumpError+ — the observable shape of an escaped Yielder.
|
|
30
30
|
class Yielder
|
|
31
31
|
# +yield_to_guest+ is a +String → String+ callable (the ext's
|
|
32
32
|
# per-dispatch +Kobako::Runtime::GuestYielder+) that
|
|
33
|
-
#
|
|
33
|
+
# #yield invokes to re-enter the guest; +break_tag+ is the +catch+
|
|
34
34
|
# throw tag the Dispatcher matches against to unwind the Service on
|
|
35
35
|
# +tag 0x02+. +handler+ is the Sandbox's +Kobako::Catalog::Handles+,
|
|
36
36
|
# used to restore a Capability Handle in the block's ok value back to
|
|
@@ -44,7 +44,7 @@ module Kobako
|
|
|
44
44
|
|
|
45
45
|
# Re-enter the guest with +args+ and reify the YieldResponse into
|
|
46
46
|
# Ruby control flow. Raises +LocalJumpError+ if called after
|
|
47
|
-
#
|
|
47
|
+
# #invalidate!. The ok value is consumed by the host Service
|
|
48
48
|
# method, so a Capability Handle in it is restored to its host object.
|
|
49
49
|
# The break value unwinds past the Service back to the guest
|
|
50
50
|
# Member call, so it passes through verbatim — a Handle stays a
|
|
@@ -60,14 +60,14 @@ module Kobako
|
|
|
60
60
|
raise yield_failure(response.value, default: "yield error")
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# The Proc the Dispatcher passes as +&block+, binding
|
|
63
|
+
# The Proc the Dispatcher passes as +&block+, binding #yield so a
|
|
64
64
|
# Service method's +yield+ / +block.call+ drives the round-trip.
|
|
65
65
|
def to_proc
|
|
66
66
|
method(:yield).to_proc
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Mark this Yielder dead. Called by the Dispatcher's +ensure+ block
|
|
70
|
-
# when the originating dispatch frame returns; any later
|
|
70
|
+
# when the originating dispatch frame returns; any later #yield
|
|
71
71
|
# call then raises +LocalJumpError+.
|
|
72
72
|
def invalidate!
|
|
73
73
|
@active = false
|
data/lib/kobako/version.rb
CHANGED
data/lib/kobako.rb
CHANGED