kobako 0.2.1 → 0.3.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 +123 -57
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/wasm/cache.rs +3 -3
- data/ext/kobako/src/wasm/dispatch.rs +87 -36
- data/ext/kobako/src/wasm/host_state.rs +189 -52
- data/ext/kobako/src/wasm/instance.rs +367 -152
- data/ext/kobako/src/wasm.rs +19 -5
- data/lib/kobako/capture.rb +12 -10
- data/lib/kobako/codec/decoder.rb +3 -2
- data/lib/kobako/codec/encoder.rb +1 -1
- data/lib/kobako/codec/error.rb +3 -2
- data/lib/kobako/codec/factory.rb +11 -7
- data/lib/kobako/codec/utils.rb +3 -2
- data/lib/kobako/codec.rb +2 -1
- data/lib/kobako/errors.rb +22 -10
- data/lib/kobako/invocation.rb +112 -0
- data/lib/kobako/outcome/panic.rb +2 -2
- data/lib/kobako/outcome.rb +20 -13
- data/lib/kobako/rpc/dispatcher.rb +9 -9
- data/lib/kobako/rpc/envelope.rb +3 -3
- data/lib/kobako/rpc/fault.rb +3 -2
- data/lib/kobako/rpc/handle.rb +3 -2
- data/lib/kobako/rpc/handle_table.rb +7 -7
- data/lib/kobako/rpc/namespace.rb +3 -3
- data/lib/kobako/rpc/server.rb +14 -12
- data/lib/kobako/sandbox.rb +147 -125
- 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/version.rb +1 -1
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/invocation.rbs +23 -0
- data/sig/kobako/sandbox.rbs +17 -18
- 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/wasm.rbs +3 -1
- metadata +13 -1
|
@@ -7,20 +7,20 @@ module Kobako
|
|
|
7
7
|
# Host-side mapping from opaque integer Handle IDs to Ruby objects
|
|
8
8
|
# (capability proxies). One table is owned per +Kobako::RPC::Server+
|
|
9
9
|
# instance (and therefore per +Kobako::Sandbox+ instance). See
|
|
10
|
-
# {
|
|
10
|
+
# {docs/behavior.md B-15}[link:../../../docs/behavior.md].
|
|
11
11
|
#
|
|
12
|
-
# Lifecycle invariants ({
|
|
12
|
+
# Lifecycle invariants ({docs/behavior.md}[link:../../../docs/behavior.md]):
|
|
13
13
|
#
|
|
14
|
-
# - {
|
|
14
|
+
# - {docs/behavior.md B-15}[link:../../../docs/behavior.md] — Handle IDs are allocated by
|
|
15
15
|
# a monotonically increasing counter scoped to a single `#run`. The
|
|
16
16
|
# first ID issued in a run is 1; ID 0 is reserved as the invalid
|
|
17
17
|
# sentinel and is never returned by #alloc.
|
|
18
18
|
#
|
|
19
|
-
# - {
|
|
19
|
+
# - {docs/behavior.md B-19}[link:../../../docs/behavior.md] — When between `#run`
|
|
20
20
|
# invocations (via `#reset!`), every Handle issued under the old state
|
|
21
21
|
# becomes invalid.
|
|
22
22
|
#
|
|
23
|
-
# - {
|
|
23
|
+
# - {docs/behavior.md B-21}[link:../../../docs/behavior.md] — The cap is `0x7fff_ffff`
|
|
24
24
|
# (2³¹ − 1). Allocation beyond the cap raises immediately — no silent
|
|
25
25
|
# truncation, no wrap, no ID reuse.
|
|
26
26
|
class HandleTable
|
|
@@ -38,7 +38,7 @@ module Kobako
|
|
|
38
38
|
# allocated Handle ID in +[1, RPC::Handle::MAX_ID]+. Raises
|
|
39
39
|
# +Kobako::HandleTableExhausted+ if the next ID would exceed the cap.
|
|
40
40
|
# The cap is anchored on +RPC::Handle+ — the wire codec and the
|
|
41
|
-
# allocator share the same invariant ({
|
|
41
|
+
# allocator share the same invariant ({docs/behavior.md B-21}[link:../../../docs/behavior.md]).
|
|
42
42
|
def alloc(object)
|
|
43
43
|
id = @next_id
|
|
44
44
|
cap = RPC::Handle::MAX_ID
|
|
@@ -66,7 +66,7 @@ module Kobako
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# Clear all entries AND reset the counter to 1. Called at the per-run
|
|
69
|
-
# boundary — see {
|
|
69
|
+
# boundary — see {docs/behavior.md B-19}[link:../../../docs/behavior.md].
|
|
70
70
|
# Returns +self+.
|
|
71
71
|
def reset!
|
|
72
72
|
@entries.clear
|
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
|
@@ -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
|
#
|
|
@@ -39,14 +39,14 @@ module Kobako
|
|
|
39
39
|
@sealed = false
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
# Declare or retrieve the Namespace named +name+ (idempotent —
|
|
42
|
+
# Declare or retrieve the Namespace named +name+ (idempotent — docs/behavior.md B-10).
|
|
43
43
|
# +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
|
|
44
44
|
# +Namespace::NAME_PATTERN+). Returns the +Kobako::RPC::Namespace+ for
|
|
45
45
|
# that name, creating it if it does not exist. Raises +ArgumentError+
|
|
46
46
|
# when +name+ is malformed, or when called after the owning Sandbox has
|
|
47
|
-
# been sealed by
|
|
47
|
+
# been sealed by its first invocation ({docs/behavior.md B-07}[link:../../../docs/behavior.md]).
|
|
48
48
|
def define(name)
|
|
49
|
-
raise ArgumentError, "cannot define after Sandbox
|
|
49
|
+
raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
|
|
50
50
|
|
|
51
51
|
name_str = name.to_s
|
|
52
52
|
unless Namespace::NAME_PATTERN.match?(name_str)
|
|
@@ -91,8 +91,9 @@ module Kobako
|
|
|
91
91
|
@namespaces.empty?
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
# Structured Frame 1 description. Called by +Sandbox#
|
|
95
|
-
# stdin Frame 1
|
|
94
|
+
# Structured Frame 1 description. Called by +Sandbox#eval+ when
|
|
95
|
+
# assembling stdin Frame 1
|
|
96
|
+
# ({docs/behavior.md B-02}[link:../../../docs/behavior.md]). Returns an
|
|
96
97
|
# unencoded preamble array — an +Array+ of two-element +[name, members]+
|
|
97
98
|
# arrays, one per declared namespace.
|
|
98
99
|
def to_preamble
|
|
@@ -100,7 +101,7 @@ module Kobako
|
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery
|
|
103
|
-
# ({
|
|
104
|
+
# ({docs/behavior.md B-02}[link:../../../docs/behavior.md]). Uses plain MessagePack (no
|
|
104
105
|
# kobako ext types) because the preamble contains only strings — no
|
|
105
106
|
# Handles or Fault envelopes. Structure:
|
|
106
107
|
# +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
|
|
@@ -109,8 +110,8 @@ module Kobako
|
|
|
109
110
|
MessagePack.pack(to_preamble)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
# Mark the Server as sealed. Called by +Sandbox
|
|
113
|
-
# After sealing, #define raises ArgumentError. Idempotent.
|
|
113
|
+
# Mark the Server as sealed. Called by +Sandbox+ on the first
|
|
114
|
+
# invocation. After sealing, #define raises ArgumentError. Idempotent.
|
|
114
115
|
def seal!
|
|
115
116
|
@sealed = true
|
|
116
117
|
self
|
|
@@ -121,14 +122,15 @@ module Kobako
|
|
|
121
122
|
@sealed
|
|
122
123
|
end
|
|
123
124
|
|
|
124
|
-
# Reset the HandleTable for a new
|
|
125
|
-
# before each invocation
|
|
125
|
+
# Reset the HandleTable for a new invocation boundary. Called by
|
|
126
|
+
# +Sandbox+ before each invocation
|
|
127
|
+
# ({docs/behavior.md B-19}[link:../../../docs/behavior.md]).
|
|
126
128
|
def reset_handles!
|
|
127
129
|
@handle_table.reset!
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
# Dispatch a single RPC request and return the encoded response bytes
|
|
131
|
-
# ({
|
|
133
|
+
# ({docs/behavior.md B-12}[link:../../../docs/behavior.md]). +request_bytes+ is a
|
|
132
134
|
# msgpack-encoded Request envelope. Called by the Rust ext from inside
|
|
133
135
|
# +__kobako_dispatch+. Always returns a binary +String+ — never raises.
|
|
134
136
|
# Delegates to +Dispatcher.dispatch+ which reifies any failure as a
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
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 "invocation"
|
|
5
8
|
require_relative "outcome"
|
|
6
9
|
require_relative "rpc/server"
|
|
7
10
|
require_relative "rpc/envelope"
|
|
11
|
+
require_relative "sandbox_options"
|
|
12
|
+
require_relative "snippet"
|
|
8
13
|
|
|
9
14
|
module Kobako
|
|
10
15
|
# Kobako::Sandbox — the user-facing entry point for executing guest mruby
|
|
11
16
|
# scripts inside a wasmtime-hosted Wasm module
|
|
12
|
-
# ({
|
|
17
|
+
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
13
18
|
#
|
|
14
19
|
# The Sandbox owns the +Kobako::Wasm::Instance+, the per-instance RPC Server
|
|
15
20
|
# (which itself owns the per-run HandleTable), and the per-channel byte
|
|
@@ -18,7 +23,7 @@ module Kobako
|
|
|
18
23
|
# never surface to Ruby — constructing many Sandboxes amortises both costs
|
|
19
24
|
# automatically.
|
|
20
25
|
#
|
|
21
|
-
# Output capture policy ({
|
|
26
|
+
# Output capture policy ({docs/behavior.md B-04}[link:../../docs/behavior.md]): the
|
|
22
27
|
# per-channel cap (+stdout_limit+ / +stderr_limit+) is enforced inside the
|
|
23
28
|
# WASI pipe — the host buffer stops growing at the cap, subsequent guest
|
|
24
29
|
# writes on that channel fail or are dropped, and +#run+ still returns
|
|
@@ -27,47 +32,41 @@ module Kobako
|
|
|
27
32
|
# +#stdout_truncated?+ / +#stderr_truncated?+ are the only way to observe
|
|
28
33
|
# that the cap was hit.
|
|
29
34
|
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
|
|
35
|
+
extend Forwardable
|
|
41
36
|
|
|
42
37
|
attr_reader :wasm_path, :instance,
|
|
43
|
-
:
|
|
44
|
-
:
|
|
38
|
+
:options,
|
|
39
|
+
:services, :snippets
|
|
40
|
+
|
|
41
|
+
# Per-cap accessors forward to the immutable +SandboxOptions+ Value
|
|
42
|
+
# Object so the Host App still reads them off Sandbox directly.
|
|
43
|
+
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit
|
|
45
44
|
|
|
46
45
|
# Returns the bytes the guest wrote to stdout during the most recent
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# never contains a truncation sentinel; use +#stdout_truncated?+ to
|
|
46
|
+
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
47
|
+
# any invocation. {docs/behavior.md B-04}[link:../../docs/behavior.md] — the byte
|
|
48
|
+
# content never contains a truncation sentinel; use +#stdout_truncated?+ to
|
|
50
49
|
# observe overflow.
|
|
51
50
|
def stdout
|
|
52
51
|
@stdout_capture.bytes
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
# Returns the bytes the guest wrote to stderr during the most recent
|
|
56
|
-
#
|
|
57
|
-
#
|
|
55
|
+
# invocation as a UTF-8 String, clipped at +stderr_limit+. Empty before
|
|
56
|
+
# any invocation. Mirror of +#stdout+.
|
|
58
57
|
def stderr
|
|
59
58
|
@stderr_capture.bytes
|
|
60
59
|
end
|
|
61
60
|
|
|
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:../../
|
|
61
|
+
# Returns +true+ iff stdout capture during the most recent invocation
|
|
62
|
+
# exceeded +stdout_limit+ ({docs/behavior.md B-04}[link:../../docs/behavior.md]). Resets
|
|
63
|
+
# to +false+ at the start of the next invocation
|
|
64
|
+
# ({docs/behavior.md B-03}[link:../../docs/behavior.md]).
|
|
66
65
|
def stdout_truncated?
|
|
67
66
|
@stdout_capture.truncated?
|
|
68
67
|
end
|
|
69
68
|
|
|
70
|
-
# Returns +true+ iff stderr capture during the most recent
|
|
69
|
+
# Returns +true+ iff stderr capture during the most recent invocation
|
|
71
70
|
# exceeded +stderr_limit+. Mirror of +#stdout_truncated?+.
|
|
72
71
|
def stderr_truncated?
|
|
73
72
|
@stderr_capture.truncated?
|
|
@@ -76,115 +75,148 @@ module Kobako
|
|
|
76
75
|
# Build a fresh Sandbox.
|
|
77
76
|
#
|
|
78
77
|
# +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).
|
|
78
|
+
# gem-bundled +data/kobako.wasm+. The four caps (+stdout_limit+,
|
|
79
|
+
# +stderr_limit+, +timeout+, +memory_limit+) are forwarded verbatim to
|
|
80
|
+
# +Kobako::SandboxOptions+, which owns their DEFAULT fallback and
|
|
81
|
+
# normalisation. The constructed +SandboxOptions+ is exposed as
|
|
82
|
+
# +#options+ and the four caps remain readable directly on Sandbox via
|
|
83
|
+
# +Forwardable+ delegation.
|
|
86
84
|
def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
|
|
87
|
-
timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
88
|
-
memory_limit: DEFAULT_MEMORY_LIMIT)
|
|
85
|
+
timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
|
|
86
|
+
memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
|
|
89
87
|
@wasm_path = wasm_path || Kobako::Wasm.default_path
|
|
90
|
-
@
|
|
91
|
-
|
|
92
|
-
@timeout = normalize_timeout(timeout)
|
|
93
|
-
@memory_limit = normalize_memory_limit(memory_limit)
|
|
88
|
+
@options = SandboxOptions.new(timeout: timeout, memory_limit: memory_limit, stdout_limit: stdout_limit,
|
|
89
|
+
stderr_limit: stderr_limit)
|
|
94
90
|
@services = Kobako::RPC::Server.new
|
|
95
|
-
@
|
|
91
|
+
@snippets = Snippet::Table.new
|
|
92
|
+
@instance = Kobako::Wasm::Instance.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
93
|
+
@options.stdout_limit, @options.stderr_limit)
|
|
96
94
|
@instance.server = @services
|
|
97
95
|
clear_captures!
|
|
98
96
|
end
|
|
99
97
|
|
|
100
98
|
# Declare or retrieve the Namespace named +name+ on this Sandbox
|
|
101
|
-
# ({
|
|
99
|
+
# ({docs/behavior.md B-07, B-09, B-10}[link:../../docs/behavior.md]). +name+ must be a
|
|
102
100
|
# Symbol or String in constant form. Returns the +Kobako::RPC::Namespace+.
|
|
103
101
|
#
|
|
104
|
-
# Raises +ArgumentError+ when called after
|
|
105
|
-
# not match the constant-name pattern.
|
|
102
|
+
# Raises +ArgumentError+ when called after the first invocation, or
|
|
103
|
+
# when +name+ does not match the constant-name pattern.
|
|
106
104
|
def define(name)
|
|
107
105
|
@services.define(name)
|
|
108
106
|
end
|
|
109
107
|
|
|
110
|
-
#
|
|
111
|
-
# ({
|
|
112
|
-
# source code as a UTF-8 String. Returns the deserialized last
|
|
113
|
-
# expression of the script.
|
|
108
|
+
# Register a snippet on this Sandbox in one of two forms
|
|
109
|
+
# ({docs/behavior.md B-32}[link:../../docs/behavior.md]):
|
|
114
110
|
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
111
|
+
# * +preload(code: source, name: Name)+ — +source+ is mruby source
|
|
112
|
+
# as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. The +name+
|
|
113
|
+
# becomes the snippet's +(snippet:Name)+ backtrace filename and
|
|
114
|
+
# is the dedupe key for E-33.
|
|
115
|
+
# * +preload(binary: bytes)+ — +bytes+ is precompiled RITE
|
|
116
|
+
# bytecode as a +String+. The canonical name, when present,
|
|
117
|
+
# lives in the bytecode's embedded +debug_info+ and is resolved
|
|
118
|
+
# by the guest at load time; the host treats the bytes as
|
|
119
|
+
# opaque. Structural failures
|
|
120
|
+
# ({docs/behavior.md E-37 / E-38}[link:../../docs/behavior.md])
|
|
121
|
+
# surface as +Kobako::BytecodeError+ on the first invocation.
|
|
120
122
|
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
# Subsequent invocations (+#eval+ or +#run+) replay every registered
|
|
124
|
+
# snippet — in insertion order — against the fresh +mrb_state+
|
|
125
|
+
# before per-invocation source or entrypoint resolution.
|
|
126
|
+
#
|
|
127
|
+
# Returns +self+ to allow chaining.
|
|
128
|
+
#
|
|
129
|
+
# Raises +ArgumentError+ when neither form's keyword set is
|
|
130
|
+
# supplied, when both forms are mixed (e.g., +code:+ and +binary:+
|
|
131
|
+
# together, or +binary:+ paired with +name:+), when +code+ / +bytes+
|
|
132
|
+
# is not a +String+, when +name+ does not match the constant
|
|
133
|
+
# pattern ({docs/behavior.md E-34}[link:../../docs/behavior.md]),
|
|
134
|
+
# when +name+ duplicates an already-registered +code:+ form snippet
|
|
135
|
+
# ({docs/behavior.md E-33}[link:../../docs/behavior.md]), or when
|
|
136
|
+
# called after the first invocation
|
|
137
|
+
# ({docs/behavior.md E-35, B-33}[link:../../docs/behavior.md]).
|
|
138
|
+
def preload(code: nil, name: nil, binary: nil)
|
|
139
|
+
raise ArgumentError, "cannot preload after first Sandbox invocation" if @services.sealed?
|
|
140
|
+
|
|
141
|
+
@snippets.register(code: code, name: name, binary: binary)
|
|
142
|
+
self
|
|
133
143
|
end
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
seconds
|
|
145
|
+
# Dispatch into a preloaded entrypoint constant
|
|
146
|
+
# ({docs/behavior.md B-31}[link:../../docs/behavior.md]). Delegates host
|
|
147
|
+
# pre-flight (E-24 / E-25 / E-29 / E-30) and wire encoding to
|
|
148
|
+
# +Kobako::Invocation+ / +Kobako::Invocation#encode+; the guest
|
|
149
|
+
# resolves +target+ as a top-level constant, calls +#call+ on it
|
|
150
|
+
# with +args+ / +kwargs+, and returns the deserialized result. The
|
|
151
|
+
# first invocation seals the Service registry and snippet table
|
|
152
|
+
# (B-07 / B-33). Runtime errors follow the same three-class taxonomy
|
|
153
|
+
# as +#eval+.
|
|
154
|
+
def run(target, *args, **kwargs)
|
|
155
|
+
invocation = Invocation.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
156
|
+
invoke!(:run) do
|
|
157
|
+
@instance.run(@services.encoded_preamble, @snippets.encode, invocation.encode)
|
|
158
|
+
end
|
|
150
159
|
end
|
|
151
160
|
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
# Execute a guest mruby source string in a fresh +mrb_state+
|
|
162
|
+
# ({docs/behavior.md B-02 / B-03 / B-06}[link:../../docs/behavior.md]). +code+ is the
|
|
163
|
+
# mruby source as a UTF-8 String. Returns the deserialized last
|
|
164
|
+
# expression of the source.
|
|
165
|
+
#
|
|
166
|
+
# Source delivery uses the WASI stdin three-frame protocol
|
|
167
|
+
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
|
|
168
|
+
# Frame 1 carries the msgpack-encoded preamble (Namespace / Member
|
|
169
|
+
# registry snapshot), Frame 2 carries the user source UTF-8 bytes, and
|
|
170
|
+
# Frame 3 carries the snippet table registered via +#preload+ (B-32).
|
|
171
|
+
# Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
|
|
172
|
+
# mandatory-presence — an empty snippet table sends an empty msgpack
|
|
173
|
+
# array, never an absent frame.
|
|
174
|
+
#
|
|
175
|
+
# The first invocation seals the Service registry and snippet table
|
|
176
|
+
# ({docs/behavior.md B-07 / B-33}[link:../../docs/behavior.md]); subsequent
|
|
177
|
+
# +#define+ / +#preload+ calls raise +ArgumentError+.
|
|
178
|
+
#
|
|
179
|
+
# Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
|
|
180
|
+
# +Kobako::SandboxError+ when the guest ran to completion but failed
|
|
181
|
+
# (including when +code+ is +nil+ or not a String, or when a preloaded
|
|
182
|
+
# snippet's replay raises — E-36);
|
|
183
|
+
# +Kobako::ServiceError+ on an unrescued Service capability failure.
|
|
184
|
+
def eval(code)
|
|
185
|
+
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
161
186
|
|
|
162
|
-
|
|
187
|
+
invoke!(:eval) do
|
|
188
|
+
@instance.eval(@services.encoded_preamble, code.b, @snippets.encode)
|
|
189
|
+
end
|
|
163
190
|
end
|
|
164
191
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
#
|
|
168
|
-
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
# Per-invocation prologue ({docs/behavior.md B-03 / B-07 /
|
|
195
|
+
# B-33}[link:../../docs/behavior.md]). Seals the Service / snippet
|
|
196
|
+
# registries on first call (idempotent) and zeros the per-invocation
|
|
197
|
+
# capability state — capture buffers, truncation predicates, and the
|
|
198
|
+
# HandleTable counter — before the guest runs.
|
|
199
|
+
def begin_invocation!
|
|
200
|
+
@services.seal!
|
|
169
201
|
@services.reset_handles!
|
|
170
202
|
clear_captures!
|
|
171
203
|
end
|
|
172
204
|
|
|
173
|
-
# Reset both per-channel captures to the pre-
|
|
174
|
-
# ({
|
|
175
|
-
# (first-
|
|
176
|
-
# both paths agree on what "empty capture" means.
|
|
205
|
+
# Reset both per-channel captures to the pre-invocation sentinel
|
|
206
|
+
# ({docs/behavior.md B-05}[link:../../docs/behavior.md]). Shared by +#initialize+
|
|
207
|
+
# (first-time setup) and +#begin_invocation!+ (between-invocation
|
|
208
|
+
# reset) so both paths agree on what "empty capture" means.
|
|
177
209
|
def clear_captures!
|
|
178
210
|
@stdout_capture = Capture::EMPTY
|
|
179
211
|
@stderr_capture = Capture::EMPTY
|
|
180
212
|
end
|
|
181
213
|
|
|
182
214
|
# Read the per-channel capture pairs (+[bytes, truncated]+) from the
|
|
183
|
-
# ext after
|
|
215
|
+
# ext after an invocation completes and wrap each as a +Capture+ value
|
|
184
216
|
# object. The ext clips +bytes+ to the configured cap and sets
|
|
185
217
|
# +truncated+ when the guest produced strictly more than +cap+ bytes
|
|
186
|
-
# ({
|
|
187
|
-
# at the post-
|
|
218
|
+
# ({docs/behavior.md B-04}[link:../../docs/behavior.md]). Mirror of {#clear_captures!}
|
|
219
|
+
# at the post-invocation boundary.
|
|
188
220
|
def read_captures!
|
|
189
221
|
out_bytes, out_truncated = @instance.stdout
|
|
190
222
|
err_bytes, err_truncated = @instance.stderr
|
|
@@ -192,37 +224,27 @@ module Kobako
|
|
|
192
224
|
@stderr_capture = Capture.from_ext(err_bytes, err_truncated)
|
|
193
225
|
end
|
|
194
226
|
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
227
|
+
# Shared prologue / epilogue + trap-class translator for both
|
|
228
|
+
# invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
|
|
229
|
+
# TrapError message so the failing export is identifiable. The
|
|
230
|
+
# rescue chain is the single trap-translation boundary — wasmtime /
|
|
231
|
+
# wire failures from the guest call and from the subsequent
|
|
232
|
+
# +Instance#outcome!+ read both flow through here, so an
|
|
233
|
+
# OUTCOME_BUFFER read failure attributes to the same export name as
|
|
234
|
+
# the guest call itself. Configured-cap paths
|
|
235
|
+
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md]) surface as
|
|
236
|
+
# named TrapError subclasses.
|
|
237
|
+
def invoke!(verb)
|
|
238
|
+
begin_invocation!
|
|
239
|
+
yield
|
|
240
|
+
read_captures!
|
|
241
|
+
Outcome.decode(@instance.outcome!)
|
|
203
242
|
rescue Kobako::Wasm::TimeoutError => e
|
|
204
243
|
raise TimeoutError, "guest exceeded timeout: #{e.message}"
|
|
205
244
|
rescue Kobako::Wasm::MemoryLimitError => e
|
|
206
245
|
raise MemoryLimitError, "guest exceeded memory_limit: #{e.message}"
|
|
207
246
|
rescue Kobako::Wasm::Error => e
|
|
208
|
-
raise TrapError, "guest
|
|
209
|
-
end
|
|
210
|
-
|
|
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
|
|
217
|
-
# ({SPEC.md ABI Signatures}[link:../../SPEC.md]).
|
|
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!)
|
|
224
|
-
rescue Kobako::Wasm::Error => e
|
|
225
|
-
raise TrapError, "failed to read OUTCOME_BUFFER: #{e.message}"
|
|
247
|
+
raise TrapError, "guest __kobako_#{verb} trapped: #{e.message}"
|
|
226
248
|
end
|
|
227
249
|
end
|
|
228
250
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Kobako::SandboxOptions — immutable Value Object holding the four
|
|
5
|
+
# per-Sandbox configuration caps ({docs/behavior.md B-01,
|
|
6
|
+
# E-20}[link:../../docs/behavior.md]). Built on the +class X <
|
|
7
|
+
# Data.define(...)+ subclass form (the Steep-friendly shape — see
|
|
8
|
+
# +lib/kobako/outcome/panic.rb+).
|
|
9
|
+
#
|
|
10
|
+
# The +initialize+ method does double duty: it applies DEFAULT fallback
|
|
11
|
+
# for absent values and normalises (timeout to Float seconds,
|
|
12
|
+
# memory_limit to positive Integer bytes) before delegating to Data's
|
|
13
|
+
# +super+. Anything that survives +SandboxOptions.new+ is a wire-ready
|
|
14
|
+
# cap bundle the +Kobako::Wasm::Instance+ constructor consumes as-is.
|
|
15
|
+
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit)
|
|
16
|
+
# Default wall-clock timeout for a single invocation: 60 seconds
|
|
17
|
+
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
18
|
+
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
19
|
+
|
|
20
|
+
# Default cap on the per-invocation guest linear-memory delta:
|
|
21
|
+
# 1 MiB ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
22
|
+
# The mruby image's initial allocation and prior invocations'
|
|
23
|
+
# watermark sit outside this budget — see B-01 Notes.
|
|
24
|
+
DEFAULT_MEMORY_LIMIT = 1 << 20
|
|
25
|
+
|
|
26
|
+
# Default per-channel capture ceiling: 1 MiB
|
|
27
|
+
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
28
|
+
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
29
|
+
|
|
30
|
+
# steep:ignore:start
|
|
31
|
+
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
32
|
+
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
33
|
+
stdout_limit: nil,
|
|
34
|
+
stderr_limit: nil)
|
|
35
|
+
super(
|
|
36
|
+
timeout: normalize_timeout(timeout),
|
|
37
|
+
memory_limit: normalize_memory_limit(memory_limit),
|
|
38
|
+
stdout_limit: stdout_limit || DEFAULT_OUTPUT_LIMIT,
|
|
39
|
+
stderr_limit: stderr_limit || DEFAULT_OUTPUT_LIMIT
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Coerce +timeout+ into the Float seconds the ext expects, or +nil+
|
|
46
|
+
# to mean the cap is disabled. Any finite non-positive value is
|
|
47
|
+
# rejected — a zero or negative timeout would either fire instantly
|
|
48
|
+
# or never, both of which would surprise callers more than an early
|
|
49
|
+
# +ArgumentError+.
|
|
50
|
+
def normalize_timeout(timeout)
|
|
51
|
+
return nil if timeout.nil?
|
|
52
|
+
raise ArgumentError, "timeout must be Numeric or nil, got #{timeout.class}" unless timeout.is_a?(Numeric)
|
|
53
|
+
|
|
54
|
+
seconds = timeout.to_f
|
|
55
|
+
raise ArgumentError, "timeout must be > 0 (got #{timeout})" unless seconds.positive? && seconds.finite?
|
|
56
|
+
|
|
57
|
+
seconds
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Coerce +memory_limit+ into the byte cap the ext expects, or +nil+
|
|
61
|
+
# to mean unbounded. Must be a positive Integer when set; Float or
|
|
62
|
+
# zero/negative values are rejected.
|
|
63
|
+
def normalize_memory_limit(memory_limit)
|
|
64
|
+
return nil if memory_limit.nil?
|
|
65
|
+
unless memory_limit.is_a?(Integer) && memory_limit.positive?
|
|
66
|
+
raise ArgumentError, "memory_limit must be a positive Integer or nil, got #{memory_limit.inspect}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
memory_limit
|
|
70
|
+
end
|
|
71
|
+
# steep:ignore:end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Snippet
|
|
5
|
+
# Kobako::Snippet::Binary — value object representing a single
|
|
6
|
+
# +#preload(binary:)+ entry held by +Kobako::Snippet::Table+
|
|
7
|
+
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
8
|
+
#
|
|
9
|
+
# The +body+ is RITE bytecode (as emitted by +mrbc+) carried as an
|
|
10
|
+
# +ASCII_8BIT+ String so msgpack-ruby encodes it as +bin+ family on
|
|
11
|
+
# the wire ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
|
|
12
|
+
# The host treats the bytes as opaque — the snippet's canonical
|
|
13
|
+
# name, when present, lives in the bytecode's embedded
|
|
14
|
+
# +debug_info+ and is resolved by the guest at load time;
|
|
15
|
+
# structural validation
|
|
16
|
+
# ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
|
|
17
|
+
# is deferred to the first invocation's guest replay.
|
|
18
|
+
#
|
|
19
|
+
# The class is a +Data.define+ subclass — frozen and value-equal.
|
|
20
|
+
# Callers (chiefly +Table+) construct instances via keyword form
|
|
21
|
+
# +Binary.new(body: ...)+. Wire-form construction is the +Table+'s
|
|
22
|
+
# responsibility.
|
|
23
|
+
class Binary < Data.define(:body)
|
|
24
|
+
# The +kind+ field value carried by bytecode snippets in their
|
|
25
|
+
# Frame 3 wire envelope entry
|
|
26
|
+
# ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
|
|
27
|
+
KIND = "bytecode"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|