kobako 0.28.0 → 0.29.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 +170 -0
- data/Cargo.lock +111 -82
- data/README.md +154 -36
- data/ROADMAP.md +5 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +86 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +13 -8
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +11 -1
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-transport/CHANGELOG.md +17 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +32 -14
- data/crates/kobako-transport/src/envelope/request.rs +18 -8
- data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
- data/crates/kobako-wasmtime/Cargo.toml +21 -16
- data/crates/kobako-wasmtime/README.md +5 -3
- data/crates/kobako-wasmtime/src/abi.rs +66 -32
- data/crates/kobako-wasmtime/src/ambient.rs +63 -4
- data/crates/kobako-wasmtime/src/cache.rs +185 -74
- data/crates/kobako-wasmtime/src/config.rs +58 -1
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
- data/crates/kobako-wasmtime/src/driver.rs +46 -44
- data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
- data/crates/kobako-wasmtime/src/invocation.rs +35 -46
- data/crates/kobako-wasmtime/src/io.rs +5 -202
- data/crates/kobako-wasmtime/src/lib.rs +4 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +39 -19
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
- data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +7 -1
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +120 -199
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +11 -0
- data/lib/kobako/catalog/snippets.rb +5 -9
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +1 -0
- data/lib/kobako/codec/ext_types.rb +1 -1
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +38 -11
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/pool.rb +2 -2
- data/lib/kobako/sandbox.rb +40 -12
- data/lib/kobako/sandbox_options.rb +39 -12
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +30 -26
- data/lib/kobako/transport/exposure.rb +5 -7
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/yielder.rb +8 -30
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +4 -0
- data/sig/kobako/catalog/snippets.rbs +1 -1
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +2 -0
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +7 -1
- data/sig/kobako/sandbox_options.rbs +24 -2
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +3 -3
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +2 -2
- metadata +60 -7
- data/sig/kobako/snippet/binary.rbs +0 -12
data/lib/kobako/codec/state.rb
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Codec
|
|
5
|
-
# Whether a Capability Handle crossed the decode in flight,
|
|
6
|
-
# thread so plain instance
|
|
5
|
+
# Whether a Capability Handle crossed the decode in flight, and the
|
|
6
|
+
# value budget a decode is held to, kept per thread so plain instance
|
|
7
|
+
# variables stay sound.
|
|
7
8
|
class State # :nodoc:
|
|
8
9
|
# Thread-local slot holding the calling thread's State.
|
|
9
10
|
STATE_KEY = :__kobako_codec_state__
|
|
@@ -18,6 +19,21 @@ module Kobako
|
|
|
18
19
|
|
|
19
20
|
def initialize
|
|
20
21
|
@carried_handle = false
|
|
22
|
+
@value_budget = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The most values a decode on this thread may build, or nil for no
|
|
26
|
+
# budget.
|
|
27
|
+
attr_reader :value_budget
|
|
28
|
+
|
|
29
|
+
# A nested invocation on the same thread restores the outer one's
|
|
30
|
+
# budget when it returns.
|
|
31
|
+
def within_value_budget(budget)
|
|
32
|
+
outer = @value_budget
|
|
33
|
+
@value_budget = budget
|
|
34
|
+
yield
|
|
35
|
+
ensure
|
|
36
|
+
@value_budget = outer
|
|
21
37
|
end
|
|
22
38
|
|
|
23
39
|
# ExtTypes#unpack_handle is the sole chokepoint every Handle passes
|
|
@@ -30,7 +46,7 @@ module Kobako
|
|
|
30
46
|
|
|
31
47
|
# Record that an ext 0x01 Capability Handle crossed the current
|
|
32
48
|
# decode; #track_handles reports it to the bracketing caller.
|
|
33
|
-
def record_handle
|
|
49
|
+
def record_handle
|
|
34
50
|
@carried_handle = true
|
|
35
51
|
end
|
|
36
52
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "error"
|
|
4
|
+
|
|
5
|
+
module Kobako
|
|
6
|
+
module Codec
|
|
7
|
+
# Counts the values a payload holds before the decoder builds any, so a
|
|
8
|
+
# guest cannot make the host build more objects than its budget allows
|
|
9
|
+
# (docs/wire/payload-msgpack.md § Value Budget). Every value takes at
|
|
10
|
+
# least one byte, so a payload no longer than the budget needs no count.
|
|
11
|
+
#
|
|
12
|
+
# The count stops at the first byte it cannot frame and leaves that
|
|
13
|
+
# payload for the decoder to refuse in its own words.
|
|
14
|
+
module ValueBudget # :nodoc:
|
|
15
|
+
# Marker to the body width of a scalar with no length field.
|
|
16
|
+
SCALARS = {
|
|
17
|
+
0xc0 => 0, 0xc2 => 0, 0xc3 => 0, 0xca => 4, 0xcb => 8,
|
|
18
|
+
0xcc => 1, 0xcd => 2, 0xce => 4, 0xcf => 8,
|
|
19
|
+
0xd0 => 1, 0xd1 => 2, 0xd2 => 4, 0xd3 => 8,
|
|
20
|
+
0xd4 => 2, 0xd5 => 3, 0xd6 => 5, 0xd7 => 9, 0xd8 => 17
|
|
21
|
+
}.freeze
|
|
22
|
+
# Marker to a container's count width and the values each entry holds.
|
|
23
|
+
COUNTS = { 0xdc => [2, 1], 0xdd => [4, 1], 0xde => [2, 2], 0xdf => [4, 2] }.freeze
|
|
24
|
+
# Marker to a str, bin, or ext's length width and type tag width.
|
|
25
|
+
LENGTHS = {
|
|
26
|
+
0xc4 => [1, 0], 0xc5 => [2, 0], 0xc6 => [4, 0],
|
|
27
|
+
0xd9 => [1, 0], 0xda => [2, 0], 0xdb => [4, 0],
|
|
28
|
+
0xc7 => [1, 1], 0xc8 => [2, 1], 0xc9 => [4, 1]
|
|
29
|
+
}.freeze
|
|
30
|
+
UINT = { 1 => "C", 2 => "n", 4 => "N" }.freeze
|
|
31
|
+
private_constant :SCALARS, :COUNTS, :LENGTHS, :UINT
|
|
32
|
+
|
|
33
|
+
def self.assert_within!(bytes, budget)
|
|
34
|
+
return if budget.nil? || bytes.bytesize <= budget
|
|
35
|
+
|
|
36
|
+
remaining = budget
|
|
37
|
+
pending = 1
|
|
38
|
+
pos = 0
|
|
39
|
+
while pending.positive?
|
|
40
|
+
pos, children = step(bytes, pos)
|
|
41
|
+
return if pos.nil?
|
|
42
|
+
raise InvalidTypeError, "payload holds more than #{budget} values" if (remaining -= 1).negative?
|
|
43
|
+
|
|
44
|
+
pending += children - 1
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The position after the value at +pos+, nil for one the payload
|
|
49
|
+
# cannot frame, and the values it holds directly.
|
|
50
|
+
def self.step(bytes, pos)
|
|
51
|
+
marker = bytes.getbyte(pos) or return [nil, 0]
|
|
52
|
+
pos += 1
|
|
53
|
+
case marker
|
|
54
|
+
when 0x00..0x7f, 0xe0..0xff then [pos, 0]
|
|
55
|
+
when 0x80..0x8f then [pos, (marker & 0x0f) * 2]
|
|
56
|
+
when 0x90..0x9f then [pos, marker & 0x0f]
|
|
57
|
+
when 0xa0..0xbf then [pos + (marker & 0x1f), 0]
|
|
58
|
+
else step_framed(bytes, pos, marker)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.step_framed(bytes, pos, marker)
|
|
63
|
+
return [pos + SCALARS.fetch(marker), 0] if SCALARS.key?(marker)
|
|
64
|
+
|
|
65
|
+
width, per = COUNTS[marker]
|
|
66
|
+
return [pos + width, uint(bytes, pos, width) * per] if width && per
|
|
67
|
+
|
|
68
|
+
width, tag = LENGTHS[marker]
|
|
69
|
+
return [nil, 0] unless width && tag
|
|
70
|
+
|
|
71
|
+
[pos + width + tag + uint(bytes, pos, width), 0]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A length cut short reads as zero, so the count runs off the end and
|
|
75
|
+
# stops there.
|
|
76
|
+
def self.uint(bytes, pos, width)
|
|
77
|
+
slice = bytes.byteslice(pos, width).to_s
|
|
78
|
+
return 0 if slice.bytesize < width
|
|
79
|
+
|
|
80
|
+
slice.unpack1(UINT.fetch(width)) # : Integer
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private_class_method :step, :step_framed, :uint
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "codec/utils"
|
|
|
5
5
|
require_relative "codec/handle_walk"
|
|
6
6
|
require_relative "codec/nesting"
|
|
7
7
|
require_relative "codec/state"
|
|
8
|
+
require_relative "codec/value_budget"
|
|
8
9
|
require_relative "codec/ext_types"
|
|
9
10
|
require_relative "codec/encoder"
|
|
10
11
|
require_relative "codec/decoder"
|
|
@@ -23,5 +24,11 @@ module Kobako
|
|
|
23
24
|
def self.track_handles(&block)
|
|
24
25
|
State.current.track_handles(&block)
|
|
25
26
|
end
|
|
27
|
+
|
|
28
|
+
# Holds every decode the block runs on this thread to +budget+ values,
|
|
29
|
+
# or to none when +budget+ is nil.
|
|
30
|
+
def self.within_value_budget(budget, &block)
|
|
31
|
+
State.current.within_value_budget(budget, &block)
|
|
32
|
+
end
|
|
26
33
|
end
|
|
27
34
|
end
|
data/lib/kobako/context.rb
CHANGED
|
@@ -17,12 +17,12 @@ module Kobako
|
|
|
17
17
|
#
|
|
18
18
|
# sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
|
|
19
19
|
class Context
|
|
20
|
-
def initialize(runtime:, services:,
|
|
20
|
+
def initialize(runtime:, services:, extensions:, grants:, preamble:) # :nodoc:
|
|
21
21
|
@runtime = runtime
|
|
22
22
|
@services = services
|
|
23
|
-
@snippets = snippets
|
|
24
|
-
@flags = flags
|
|
25
23
|
@extensions = extensions
|
|
24
|
+
@grants = grants
|
|
25
|
+
@preamble = preamble
|
|
26
26
|
@resolved = {} # : Hash[String, Transport::Exposure]
|
|
27
27
|
@overrides = {} # : Hash[String, Transport::Exposure]
|
|
28
28
|
@handles = Catalog::Handles.new
|
|
@@ -45,6 +45,26 @@ module Kobako
|
|
|
45
45
|
self
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Use +filesystem+ for the mount at +at+ during this invocation only,
|
|
49
|
+
# with its own +access+ and block, as Sandbox#mount takes them. Returns
|
|
50
|
+
# +self+.
|
|
51
|
+
#
|
|
52
|
+
# Raises TypeError for a path that is no String or something other than
|
|
53
|
+
# a Filesystem, and ArgumentError when nothing was mounted at +at+ on the
|
|
54
|
+
# Sandbox, for a malformed path or an unknown access, or when called
|
|
55
|
+
# after the block has returned.
|
|
56
|
+
def mount(at, filesystem, access: :read, &policy)
|
|
57
|
+
raise ArgumentError, "Kobako::Context is spent; ctx.mount is only valid inside the #eval / #run block" if @spent
|
|
58
|
+
|
|
59
|
+
Runtime.validate_mount!(at, access, filesystem)
|
|
60
|
+
mount = Catalog::Mount.new(at: at, access: access, filesystem: filesystem, policy: policy)
|
|
61
|
+
raise ArgumentError, "cannot override unmounted path #{at.inspect}" unless @grants.include?(mount)
|
|
62
|
+
|
|
63
|
+
mounts = @mounts ||= {} # : Hash[String, Catalog::Mount]
|
|
64
|
+
mounts[mount.key] = mount
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
48
68
|
# A fillable left unfilled raises like an unbound path, so the guest's
|
|
49
69
|
# call fails closed instead of reaching the sentinel.
|
|
50
70
|
def lookup(path) # :nodoc:
|
|
@@ -57,21 +77,25 @@ module Kobako
|
|
|
57
77
|
|
|
58
78
|
def eval(code, &block) # :nodoc:
|
|
59
79
|
collect_overrides(&block) if block
|
|
60
|
-
invoke!(:eval)
|
|
61
|
-
@runtime.eval(dispatch_handler, @services.paths, @snippets.entries, @flags.entries, code.b)
|
|
62
|
-
end
|
|
80
|
+
invoke!(:eval) { @runtime.eval(dispatch_handler, setup, code.b) }
|
|
63
81
|
end
|
|
64
82
|
|
|
65
83
|
def run(request, &block) # :nodoc:
|
|
66
84
|
collect_overrides(&block) if block
|
|
67
85
|
invoke!(:run, entrypoint: request.entrypoint) do
|
|
68
|
-
@runtime.run(dispatch_handler,
|
|
69
|
-
request.entrypoint.to_s, request.payload(@handles))
|
|
86
|
+
@runtime.run(dispatch_handler, setup, request.entrypoint.to_s, request.payload(@handles))
|
|
70
87
|
end
|
|
71
88
|
end
|
|
72
89
|
|
|
73
90
|
private
|
|
74
91
|
|
|
92
|
+
# What the Runtime builds the invocation from: the Preamble's parts,
|
|
93
|
+
# then this invocation's mounts.
|
|
94
|
+
def setup
|
|
95
|
+
paths, snippets, flags = @preamble
|
|
96
|
+
[paths, snippets, flags, @grants.entries(@mounts || {})]
|
|
97
|
+
end
|
|
98
|
+
|
|
75
99
|
# The Context is spent once the block returns, so a captured +ctx+ used
|
|
76
100
|
# later raises. A block that raises propagates before the guest drives,
|
|
77
101
|
# so the guest never runs and no Execution is produced.
|
|
@@ -83,17 +107,20 @@ module Kobako
|
|
|
83
107
|
|
|
84
108
|
# Handed to the Runtime as a call argument, so the Runtime holds no
|
|
85
109
|
# dispatch state and the Proc stays GC-rooted for the synchronous call.
|
|
110
|
+
# Built once per invocation, so every Call it answers shares one
|
|
111
|
+
# BlockFailure.
|
|
86
112
|
def dispatch_handler
|
|
113
|
+
failure = Transport::BlockFailure.new
|
|
87
114
|
lambda do |target, method_name, block_given, payload, guest_yielder|
|
|
88
115
|
call = Transport::Call.new(target: target, method_name: method_name,
|
|
89
116
|
block_given: block_given, payload: payload)
|
|
90
|
-
Transport::Dispatcher.dispatch(call, self, @handles, guest_yielder)
|
|
117
|
+
Transport::Dispatcher.dispatch(call, self, @handles, failure, guest_yielder)
|
|
91
118
|
end
|
|
92
119
|
end
|
|
93
120
|
|
|
94
121
|
# Every Snapshot carries usage and captures, trap or not, so they stay
|
|
95
122
|
# readable after a rescued trap.
|
|
96
|
-
def populate_observability
|
|
123
|
+
def populate_observability(snapshot)
|
|
97
124
|
@usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak)
|
|
98
125
|
@stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?)
|
|
99
126
|
@stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?)
|
|
@@ -137,7 +164,7 @@ module Kobako
|
|
|
137
164
|
rescue Kobako::TrapError => e
|
|
138
165
|
raise with_verb(e, verb)
|
|
139
166
|
end
|
|
140
|
-
populate_observability
|
|
167
|
+
populate_observability(snapshot)
|
|
141
168
|
trap = snapshot.trap_error
|
|
142
169
|
return settle_outcome(snapshot, verb, entrypoint) unless trap
|
|
143
170
|
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -8,11 +8,7 @@ module Kobako
|
|
|
8
8
|
# The base of every error kobako raises.
|
|
9
9
|
class Error < StandardError; end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
# rescue reads its captures and usage as a successful caller would.
|
|
13
|
-
# +execution+ is +nil+ when the failure came before the guest ran.
|
|
14
|
-
module CarriesExecution
|
|
15
|
-
# The Execution of the run that failed.
|
|
11
|
+
module Recorded # :nodoc:
|
|
16
12
|
attr_reader :execution
|
|
17
13
|
|
|
18
14
|
def with_execution(execution) # :nodoc:
|
|
@@ -23,8 +19,11 @@ module Kobako
|
|
|
23
19
|
|
|
24
20
|
# The engine stopped the guest: a trap, a cap reached, or an outcome that
|
|
25
21
|
# could not be read. The next invocation on the same Sandbox runs as usual.
|
|
22
|
+
# +execution+ is the failed run's Execution, so a rescue reads its captures
|
|
23
|
+
# and usage as a successful caller would; it is +nil+ when the failure came
|
|
24
|
+
# before the guest ran.
|
|
26
25
|
class TrapError < Error
|
|
27
|
-
include
|
|
26
|
+
include Recorded
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
# The invocation ran past its +timeout+.
|
|
@@ -41,10 +40,7 @@ module Kobako
|
|
|
41
40
|
# The Guest Binary is missing at +wasm_path+.
|
|
42
41
|
class ModuleNotBuiltError < SetupError; end
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
module Diagnosable
|
|
46
|
-
# Where the failure came from (<tt>"sandbox"</tt> or <tt>"service"</tt>),
|
|
47
|
-
# the name of the class that raised it, and its backtrace.
|
|
43
|
+
module Diagnosable # :nodoc:
|
|
48
44
|
attr_reader :origin, :klass, :backtrace_lines
|
|
49
45
|
|
|
50
46
|
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil) # :nodoc:
|
|
@@ -57,15 +53,19 @@ module Kobako
|
|
|
57
53
|
|
|
58
54
|
# The guest failed: the script raised or did not compile, the Guest Binary
|
|
59
55
|
# could not start the run, or what it produced could not be read.
|
|
56
|
+
# +origin+ (<tt>"sandbox"</tt> or <tt>"service"</tt>), +klass+ and
|
|
57
|
+
# +backtrace_lines+ say where it came from, which class raised it, and its
|
|
58
|
+
# backtrace. +execution+ is the failed run's Execution, as on TrapError.
|
|
60
59
|
class SandboxError < Error
|
|
61
60
|
include Diagnosable
|
|
62
|
-
include
|
|
61
|
+
include Recorded
|
|
63
62
|
end
|
|
64
63
|
|
|
65
|
-
# A Service call failed and the guest left it unrescued.
|
|
64
|
+
# A Service call failed and the guest left it unrescued. It carries
|
|
65
|
+
# +origin+, +klass+, +backtrace_lines+ and +execution+ as SandboxError does.
|
|
66
66
|
class ServiceError < Error
|
|
67
67
|
include Diagnosable
|
|
68
|
-
include
|
|
68
|
+
include Recorded
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
# The call reached no Service method: nothing is bound there, the Handle
|
|
@@ -73,7 +73,8 @@ module Kobako
|
|
|
73
73
|
# same on purpose, so a target discloses nothing about what it defines.
|
|
74
74
|
class NoServiceError < ServiceError; end
|
|
75
75
|
|
|
76
|
-
# The
|
|
76
|
+
# The Service method refused its arguments: the call would not bind, or
|
|
77
|
+
# the method raised +ArgumentError+ over what it was given.
|
|
77
78
|
class ServiceArgumentError < ServiceError; end
|
|
78
79
|
|
|
79
80
|
# Raised at a Service's +yield+ when the guest block raised; rescue it as
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# The Filesystems a Sandbox can mount for the guest's +File+, each
|
|
5
|
+
# answered in Rust by the kind of the same name. Hand one to
|
|
6
|
+
# Sandbox#mount:
|
|
7
|
+
#
|
|
8
|
+
# sandbox.mount("/data", Kobako::Filesystem::Memory.new("config.yml" => yaml))
|
|
9
|
+
#
|
|
10
|
+
# [<tt>Memory.new(files = {}, limit: 64 MiB, inodes:)</tt>]
|
|
11
|
+
# Files held in memory for as long as the object lives, seeded from a
|
|
12
|
+
# Hash of path to contents. Every invocation shares them.
|
|
13
|
+
# [<tt>Temporary.new(limit: 64 MiB, inodes:)</tt>]
|
|
14
|
+
# Files held in memory that every invocation starts empty.
|
|
15
|
+
# [<tt>Directory.new(path)</tt>]
|
|
16
|
+
# A host directory and everything beneath it. Nothing outside it is
|
|
17
|
+
# reachable, a symbolic link pointing out included.
|
|
18
|
+
# [<tt>Overlay.new.layer(filesystem, access: :read)</tt>]
|
|
19
|
+
# Layers stacked into one view, the first added on top, each +:read+
|
|
20
|
+
# or +:read_write+ on its own. A change lands in a writable Layer, and
|
|
21
|
+
# a read-only one stays as it was. Each +layer+ answers a new Overlay.
|
|
22
|
+
#
|
|
23
|
+
# A Memory or Temporary is bounded as a tmpfs mount is: its files hold
|
|
24
|
+
# at most +limit+ bytes together, and it holds at most +inodes+ files and
|
|
25
|
+
# directories, its root among them; +nil+ lifts either bound. Without
|
|
26
|
+
# +inodes+ it holds one for each 4 KiB of +limit+, and the root. Past
|
|
27
|
+
# either, the guest raises Errno::ENOSPC.
|
|
28
|
+
#
|
|
29
|
+
# Any object answering +open+ and +stat+ mounts too, as a Filesystem
|
|
30
|
+
# written in Ruby; its methods take the names of +File+, +Dir+, and +IO+.
|
|
31
|
+
# Its +stat+ answers +directory?+, +size+, and +ino+, an +ino+ naming one
|
|
32
|
+
# directory for as long as the guest holds it open. Its names match
|
|
33
|
+
# exactly, as a mount's block judges them as given. A name may hold a
|
|
34
|
+
# <tt>\\</tt>, which the guest's paths never separate on, so one joined
|
|
35
|
+
# to a Windows host path is the Filesystem's to refuse.
|
|
36
|
+
module Filesystem
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/kobako/pool.rb
CHANGED
|
@@ -94,7 +94,7 @@ module Kobako
|
|
|
94
94
|
done = true
|
|
95
95
|
sandbox
|
|
96
96
|
ensure
|
|
97
|
-
release_capacity
|
|
97
|
+
release_capacity unless done
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def checkin(sandbox)
|
|
@@ -104,7 +104,7 @@ module Kobako
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
def release_capacity
|
|
107
|
+
def release_capacity
|
|
108
108
|
@mutex.synchronize do
|
|
109
109
|
@constructed -= 1
|
|
110
110
|
@slot_freed.signal
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -24,7 +24,8 @@ module Kobako
|
|
|
24
24
|
# readable on the Sandbox itself, as in #timeout.
|
|
25
25
|
attr_reader :options
|
|
26
26
|
|
|
27
|
-
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit,
|
|
27
|
+
def_delegators :@options, :timeout, :setup_timeout, :memory_limit, :stdout_limit, :stderr_limit,
|
|
28
|
+
:open_file_limit, :profile, :gvl
|
|
28
29
|
|
|
29
30
|
# Build a Sandbox on the Guest Binary at +wasm_path+, the one bundled
|
|
30
31
|
# with the gem by default. The other keywords are the options
|
|
@@ -40,6 +41,7 @@ module Kobako
|
|
|
40
41
|
@snippets = Catalog::Snippets.new
|
|
41
42
|
@flags = Catalog::Flags.new
|
|
42
43
|
@extensions = Catalog::Extensions.new
|
|
44
|
+
@grants = Catalog::Grants.new
|
|
43
45
|
@sealed = false
|
|
44
46
|
@runtime = build_runtime!
|
|
45
47
|
end
|
|
@@ -56,8 +58,9 @@ module Kobako
|
|
|
56
58
|
# the constant, and Context#bind supplies the object for each invocation.
|
|
57
59
|
# A call to one left unfilled fails as a ServiceError.
|
|
58
60
|
#
|
|
59
|
-
# Raises ArgumentError for a malformed path, a path
|
|
60
|
-
#
|
|
61
|
+
# Raises ArgumentError for a malformed path, a path under the reserved
|
|
62
|
+
# +Kobako+ namespace, a path that collides with an existing binding, or a
|
|
63
|
+
# call after the first invocation.
|
|
61
64
|
def bind(path, object = Unresolved)
|
|
62
65
|
refuse_once_invoked!("bind")
|
|
63
66
|
@services.bind(path, object)
|
|
@@ -77,6 +80,27 @@ module Kobako
|
|
|
77
80
|
self
|
|
78
81
|
end
|
|
79
82
|
|
|
83
|
+
# Mount +filesystem+, a Kobako::Filesystem, at the absolute guest path
|
|
84
|
+
# +at+, where the guest's +File+ reaches it. +access+ is +:read+ or
|
|
85
|
+
# +:read_write+; a write beneath a read-only mount fails as
|
|
86
|
+
# <tt>Errno::EROFS</tt> in the guest. Returns +self+.
|
|
87
|
+
#
|
|
88
|
+
# A given block decides, in +access+'s place, each open and each change
|
|
89
|
+
# that reaches the mount, asked at most once for each with the guest path
|
|
90
|
+
# and +:read+ or +:read_write+. A falsy answer,
|
|
91
|
+
# or a StandardError, fails that call as <tt>Errno::EACCES</tt> in the
|
|
92
|
+
# guest and leaves the script running.
|
|
93
|
+
#
|
|
94
|
+
# Raises TypeError for a path that is no String or something other than
|
|
95
|
+
# a Filesystem, and ArgumentError for a malformed, duplicate, or nested
|
|
96
|
+
# path, an unknown access, or a call after the first invocation.
|
|
97
|
+
def mount(at, filesystem, access: :read, &policy)
|
|
98
|
+
refuse_once_invoked!("mount")
|
|
99
|
+
Runtime.validate_mount!(at, access, filesystem)
|
|
100
|
+
@grants.register(Catalog::Mount.new(at: at, access: access, filesystem: filesystem, policy: policy))
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
80
104
|
# Register a snippet that every invocation runs, in the order registered,
|
|
81
105
|
# before its own code. Returns +self+.
|
|
82
106
|
#
|
|
@@ -121,7 +145,7 @@ module Kobako
|
|
|
121
145
|
# Handle or a non-Symbol keyword. A failed run raises as #eval does.
|
|
122
146
|
def run(target, *args, **kwargs, &block)
|
|
123
147
|
request = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
124
|
-
new_invocation.run(request, &block)
|
|
148
|
+
Codec.within_value_budget(@options.value_budget) { new_invocation.run(request, &block) }
|
|
125
149
|
end
|
|
126
150
|
|
|
127
151
|
# Evaluate +code+, mruby source as a String, and return the Execution,
|
|
@@ -136,7 +160,7 @@ module Kobako
|
|
|
136
160
|
def eval(code, &block)
|
|
137
161
|
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
138
162
|
|
|
139
|
-
new_invocation.eval(code, &block)
|
|
163
|
+
Codec.within_value_budget(@options.value_budget) { new_invocation.eval(code, &block) }
|
|
140
164
|
end
|
|
141
165
|
|
|
142
166
|
private
|
|
@@ -144,9 +168,7 @@ module Kobako
|
|
|
144
168
|
# A runtime whose declared posture falls below the request never runs
|
|
145
169
|
# guest code.
|
|
146
170
|
def build_runtime!
|
|
147
|
-
runtime = Kobako::Runtime.from_path(@wasm_path,
|
|
148
|
-
@options.stdout_limit, @options.stderr_limit, @options.profile,
|
|
149
|
-
@options.gvl)
|
|
171
|
+
runtime = Kobako::Runtime.from_path(@wasm_path, **@options.to_h)
|
|
150
172
|
@options.enforce_floor!(runtime.profile)
|
|
151
173
|
runtime
|
|
152
174
|
end
|
|
@@ -155,12 +177,18 @@ module Kobako
|
|
|
155
177
|
# does is written back onto the shared config.
|
|
156
178
|
def new_invocation
|
|
157
179
|
begin_invocation!
|
|
158
|
-
Context.new(runtime: @runtime, services: @services,
|
|
159
|
-
|
|
180
|
+
Context.new(runtime: @runtime, services: @services, extensions: @extensions, grants: @grants,
|
|
181
|
+
preamble: preamble(@snippets.entries))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The one place the Preamble's parts are put together, for a compile
|
|
185
|
+
# and for every invocation alike.
|
|
186
|
+
def preamble(snippets)
|
|
187
|
+
[@services.paths, snippets, @flags.entries]
|
|
160
188
|
end
|
|
161
189
|
|
|
162
190
|
# Registration closes at the first invocation, so every invocation ships
|
|
163
|
-
# the same Services, snippets, flags and
|
|
191
|
+
# the same Services, snippets, flags, Extensions and mounts.
|
|
164
192
|
def refuse_once_invoked!(verb)
|
|
165
193
|
raise ArgumentError, "cannot #{verb} after first Sandbox invocation" if @sealed
|
|
166
194
|
end
|
|
@@ -171,7 +199,7 @@ module Kobako
|
|
|
171
199
|
def begin_invocation!
|
|
172
200
|
@sealed = true
|
|
173
201
|
@extensions.assert_dependencies!
|
|
174
|
-
@snippets.compile { |entries| @runtime.compile(
|
|
202
|
+
@snippets.compile { |entries| @runtime.compile(preamble(entries)) }
|
|
175
203
|
end
|
|
176
204
|
end
|
|
177
205
|
end
|
|
@@ -8,25 +8,38 @@ module Kobako
|
|
|
8
8
|
# default, and +nil+ switches it off.
|
|
9
9
|
#
|
|
10
10
|
# [+timeout+] Float seconds one invocation may run.
|
|
11
|
+
# [+setup_timeout+] Float seconds the Guest Binary may take to be set up
|
|
12
|
+
# the first time its path is loaded.
|
|
11
13
|
# [+memory_limit+] Integer bytes one invocation may grow guest memory by.
|
|
12
14
|
# [+stdout_limit+, +stderr_limit+]
|
|
13
15
|
# Integer bytes kept from each output channel.
|
|
16
|
+
# [+open_file_limit+]
|
|
17
|
+
# Integer descriptors one invocation may hold open, the
|
|
18
|
+
# standard ones and each Mount's included.
|
|
14
19
|
# [+profile+] The isolation the runtime must provide, one of PROFILES.
|
|
15
20
|
# [+gvl+] How an invocation holds Ruby's GVL, one of GVL_MODES.
|
|
16
21
|
#
|
|
17
22
|
# Raises ArgumentError for a value outside these shapes, and SetupError
|
|
18
23
|
# when the runtime provides less isolation than +profile+ asks for.
|
|
19
|
-
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit,
|
|
24
|
+
class SandboxOptions < Data.define(:timeout, :setup_timeout, :memory_limit, :stdout_limit, :stderr_limit,
|
|
25
|
+
:open_file_limit, :profile, :gvl)
|
|
20
26
|
# The default +timeout+: 60 seconds.
|
|
21
27
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
22
28
|
|
|
23
|
-
# The default +
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
# The default +setup_timeout+: 10 seconds.
|
|
30
|
+
DEFAULT_SETUP_TIMEOUT_SECONDS = 10.0
|
|
31
|
+
|
|
32
|
+
# The default +memory_limit+: 4 MiB of growth past the guest's starting
|
|
33
|
+
# memory, sized for a guest whose every value takes 16 bytes.
|
|
34
|
+
DEFAULT_MEMORY_LIMIT = 4 << 20
|
|
26
35
|
|
|
27
36
|
# The default +stdout_limit+ and +stderr_limit+: 1 MiB each.
|
|
28
37
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
29
38
|
|
|
39
|
+
# The default +open_file_limit+: 1024, the soft limit Linux gives a
|
|
40
|
+
# process.
|
|
41
|
+
DEFAULT_OPEN_FILE_LIMIT = 1024
|
|
42
|
+
|
|
30
43
|
# The isolation profiles, weakest first. +:hermetic+ denies the guest the
|
|
31
44
|
# clock and entropy; +:permissive+ leaves them live.
|
|
32
45
|
PROFILES = %i[permissive hermetic].freeze
|
|
@@ -42,20 +55,34 @@ module Kobako
|
|
|
42
55
|
DEFAULT_GVL = :hold
|
|
43
56
|
|
|
44
57
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
58
|
+
setup_timeout: DEFAULT_SETUP_TIMEOUT_SECONDS,
|
|
45
59
|
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
46
60
|
stdout_limit: DEFAULT_OUTPUT_LIMIT,
|
|
47
61
|
stderr_limit: DEFAULT_OUTPUT_LIMIT,
|
|
62
|
+
open_file_limit: DEFAULT_OPEN_FILE_LIMIT,
|
|
48
63
|
profile: DEFAULT_PROFILE,
|
|
49
64
|
gvl: DEFAULT_GVL)
|
|
50
|
-
timeout = normalize_timeout(timeout)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
timeout = normalize_timeout(timeout, "timeout")
|
|
66
|
+
setup_timeout = normalize_timeout(setup_timeout, "setup_timeout")
|
|
67
|
+
memory_limit = normalize_limit(memory_limit, "memory_limit")
|
|
68
|
+
stdout_limit = normalize_limit(stdout_limit, "stdout_limit")
|
|
69
|
+
stderr_limit = normalize_limit(stderr_limit, "stderr_limit")
|
|
70
|
+
open_file_limit = normalize_limit(open_file_limit, "open_file_limit")
|
|
54
71
|
profile = normalize_choice(profile, PROFILES, "profile")
|
|
55
72
|
gvl = normalize_choice(gvl, GVL_MODES, "gvl")
|
|
56
73
|
super
|
|
57
74
|
end
|
|
58
75
|
|
|
76
|
+
# The least guest memory one value occupies, so a guest payload may hold
|
|
77
|
+
# one value for each of these bytes of +memory_limit+
|
|
78
|
+
# (docs/wire/payload-msgpack.md § Value Budget).
|
|
79
|
+
VALUE_BYTES = 16
|
|
80
|
+
private_constant :VALUE_BYTES
|
|
81
|
+
|
|
82
|
+
def value_budget # :nodoc:
|
|
83
|
+
memory_limit&./(VALUE_BYTES)
|
|
84
|
+
end
|
|
85
|
+
|
|
59
86
|
# Both fallbacks fail closed: an unknown declaration ranks below every
|
|
60
87
|
# request, and an unknown request refuses every declaration.
|
|
61
88
|
def enforce_floor!(declared) # :nodoc:
|
|
@@ -69,17 +96,17 @@ module Kobako
|
|
|
69
96
|
|
|
70
97
|
# A zero or negative timeout would either fire instantly or never, both
|
|
71
98
|
# more surprising than an early ArgumentError.
|
|
72
|
-
def normalize_timeout(timeout)
|
|
99
|
+
def normalize_timeout(timeout, name)
|
|
73
100
|
return nil if timeout.nil?
|
|
74
|
-
raise ArgumentError, "
|
|
101
|
+
raise ArgumentError, "#{name} must be Numeric or nil, got #{timeout.class}" unless timeout.is_a?(Numeric)
|
|
75
102
|
|
|
76
103
|
seconds = timeout.to_f
|
|
77
|
-
raise ArgumentError, "
|
|
104
|
+
raise ArgumentError, "#{name} must be > 0 (got #{timeout})" unless seconds.positive? && seconds.finite?
|
|
78
105
|
|
|
79
106
|
seconds
|
|
80
107
|
end
|
|
81
108
|
|
|
82
|
-
def
|
|
109
|
+
def normalize_limit(limit, name)
|
|
83
110
|
return nil if limit.nil?
|
|
84
111
|
unless limit.is_a?(Integer) && limit.positive?
|
|
85
112
|
raise ArgumentError, "#{name} must be a positive Integer or nil, got #{limit.inspect}"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
|
-
class
|
|
5
|
+
class Bytecode < Data.define(:body) # :nodoc:
|
|
6
6
|
# Names the snippet form the guest replays this entry as. The wire's
|
|
7
7
|
# discriminant byte is assigned by the core envelope, not here.
|
|
8
8
|
KIND = :bytecode
|
data/lib/kobako/snippet.rb
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Transport
|
|
5
|
+
# The newest failure a guest block raised to a Service during one
|
|
6
|
+
# invocation, whichever open Call carried that block: a Service may run
|
|
7
|
+
# a block stored from an earlier Call, and its failure is still the
|
|
8
|
+
# guest's own.
|
|
9
|
+
class BlockFailure # :nodoc:
|
|
10
|
+
def initialize
|
|
11
|
+
@raised = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Only the newest is kept: a Service that rescued an earlier one and
|
|
15
|
+
# yielded again has already handled it.
|
|
16
|
+
def remember(error)
|
|
17
|
+
@raised = error
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Identity rather than class: a Service that rescued the block's
|
|
21
|
+
# failure and raised its own has reported something else, and the
|
|
22
|
+
# guest must hear about that instead.
|
|
23
|
+
#
|
|
24
|
+
# The class travels with the message because not every block failure
|
|
25
|
+
# is an exception the guest holds — a block value the guest refused
|
|
26
|
+
# has a class to raise under and no object to continue.
|
|
27
|
+
def fault_text(error)
|
|
28
|
+
raised = @raised
|
|
29
|
+
return if raised.nil? || !error.equal?(raised)
|
|
30
|
+
|
|
31
|
+
"#{raised.klass}: #{raised.message}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|