kobako 0.29.0 → 0.30.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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +217 -10
  4. data/Cargo.lock +139 -141
  5. data/README.md +96 -574
  6. data/ROADMAP.md +2 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +269 -1
  8. data/crates/kobako-runtime/Cargo.toml +25 -15
  9. data/crates/kobako-runtime/README.md +7 -3
  10. data/crates/kobako-runtime/src/dispatch.rs +1 -1
  11. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  12. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  13. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  14. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  15. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  16. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  17. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  18. data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
  19. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  20. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  21. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
  29. data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
  30. data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
  31. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  32. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  33. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  37. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  38. data/crates/kobako-runtime/src/filesystem.rs +21 -387
  39. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  40. data/crates/kobako-runtime/src/lib.rs +11 -6
  41. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  42. data/crates/kobako-runtime/src/locking.rs +218 -0
  43. data/crates/kobako-runtime/src/mount.rs +31 -104
  44. data/crates/kobako-runtime/src/profile.rs +137 -1
  45. data/crates/kobako-runtime/src/sched.rs +210 -0
  46. data/crates/kobako-runtime/src/security.rs +696 -0
  47. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  48. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  49. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  50. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  51. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  52. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  53. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  54. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  55. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  56. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  57. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  58. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  59. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  60. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  61. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  62. data/crates/kobako-runtime/src/vfs.rs +60 -0
  63. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  64. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  65. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  66. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  67. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  68. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  70. data/crates/kobako-transport/CHANGELOG.md +8 -2
  71. data/crates/kobako-transport/Cargo.toml +3 -3
  72. data/crates/kobako-transport/README.md +6 -5
  73. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  74. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  75. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  76. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  77. data/crates/kobako-transport/src/envelope/fault.rs +2 -2
  78. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  79. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  80. data/crates/kobako-transport/src/envelope/reply.rs +2 -2
  81. data/crates/kobako-transport/src/envelope/request.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  83. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  84. data/crates/kobako-transport/src/envelope.rs +2 -2
  85. data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
  86. data/crates/kobako-wasmtime/Cargo.toml +15 -14
  87. data/crates/kobako-wasmtime/README.md +9 -5
  88. data/crates/kobako-wasmtime/src/abi.rs +12 -20
  89. data/crates/kobako-wasmtime/src/ambient.rs +1 -2
  90. data/crates/kobako-wasmtime/src/cache.rs +5 -5
  91. data/crates/kobako-wasmtime/src/config.rs +22 -7
  92. data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
  93. data/crates/kobako-wasmtime/src/driver.rs +29 -96
  94. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  95. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  96. data/crates/kobako-wasmtime/src/imports.rs +14 -8
  97. data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
  98. data/crates/kobako-wasmtime/src/invocation.rs +26 -46
  99. data/crates/kobako-wasmtime/src/lib.rs +1 -4
  100. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  101. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
  102. data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
  103. data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
  104. data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
  105. data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
  106. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  107. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
  108. data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
  109. data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
  110. data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
  111. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  112. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  113. data/crates/kobako-wasmtime/src/preview1.rs +464 -413
  114. data/crates/kobako-wasmtime/src/trap.rs +9 -9
  115. data/crates/kobako-wasmtime/src/wasi.rs +53 -37
  116. data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
  117. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  118. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
  119. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
  120. data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
  121. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
  122. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  123. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  124. data/data/kobako.wasm +0 -0
  125. data/ext/kobako/Cargo.toml +1 -1
  126. data/ext/kobako/src/runtime/bridge.rs +1 -1
  127. data/ext/kobako/src/runtime/errors.rs +1 -1
  128. data/ext/kobako/src/runtime/escape.rs +28 -14
  129. data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
  130. data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
  131. data/ext/kobako/src/runtime/filesystem.rs +109 -137
  132. data/ext/kobako/src/runtime/gvl.rs +43 -8
  133. data/ext/kobako/src/runtime/snapshot.rs +1 -1
  134. data/ext/kobako/src/runtime.rs +6 -1
  135. data/lib/kobako/catalog/extensions.rb +5 -5
  136. data/lib/kobako/catalog/flags.rb +2 -2
  137. data/lib/kobako/catalog/handles.rb +6 -6
  138. data/lib/kobako/catalog/mount.rb +2 -10
  139. data/lib/kobako/catalog/services.rb +2 -2
  140. data/lib/kobako/catalog/snippets.rb +7 -7
  141. data/lib/kobako/catalog.rb +1 -1
  142. data/lib/kobako/codec/decoder.rb +10 -10
  143. data/lib/kobako/codec/ext_types.rb +3 -5
  144. data/lib/kobako/codec/handle_walk.rb +4 -6
  145. data/lib/kobako/codec/nesting.rb +7 -7
  146. data/lib/kobako/codec/utils.rb +1 -1
  147. data/lib/kobako/codec/value_budget.rb +3 -3
  148. data/lib/kobako/context.rb +5 -5
  149. data/lib/kobako/filesystem.rb +44 -13
  150. data/lib/kobako/payload/arguments.rb +2 -2
  151. data/lib/kobako/pool.rb +4 -4
  152. data/lib/kobako/sandbox.rb +28 -28
  153. data/lib/kobako/sandbox_options.rb +2 -3
  154. data/lib/kobako/snippet/bytecode.rb +1 -1
  155. data/lib/kobako/snippet/source.rb +1 -1
  156. data/lib/kobako/transport/dispatcher.rb +5 -5
  157. data/lib/kobako/transport/exposure.rb +2 -2
  158. data/lib/kobako/transport/reflection.rb +7 -7
  159. data/lib/kobako/transport/run.rb +5 -5
  160. data/lib/kobako/transport/yielder.rb +1 -1
  161. data/lib/kobako/version.rb +1 -1
  162. data/lib/kobako.rb +34 -0
  163. data/release-please-config.json +1 -0
  164. data/sig/kobako/catalog/extensions.rbs +3 -3
  165. data/sig/kobako/catalog/handles.rbs +3 -3
  166. data/sig/kobako/catalog/mount.rbs +0 -4
  167. data/sig/kobako/catalog/services.rbs +1 -1
  168. data/sig/kobako/catalog/snippets.rbs +3 -3
  169. data/sig/kobako/catalog.rbs +1 -1
  170. data/sig/kobako/codec/decoder.rbs +1 -1
  171. data/sig/kobako/codec/handle_walk.rbs +1 -1
  172. data/sig/kobako/codec/nesting.rbs +2 -2
  173. data/sig/kobako/codec/utils.rbs +1 -1
  174. data/sig/kobako/codec/value_budget.rbs +1 -1
  175. data/sig/kobako/codec.rbs +1 -2
  176. data/sig/kobako/context.rbs +1 -1
  177. data/sig/kobako/filesystem.rbs +19 -14
  178. data/sig/kobako/guest_yielder.rbs +1 -1
  179. data/sig/kobako/payload/arguments.rbs +1 -1
  180. data/sig/kobako/pool.rbs +2 -2
  181. data/sig/kobako/runtime.rbs +1 -1
  182. data/sig/kobako/sandbox.rbs +3 -5
  183. data/sig/kobako/sandbox_options.rbs +1 -1
  184. data/sig/kobako/transport/dispatcher.rbs +2 -2
  185. data/sig/kobako/transport/exposure.rbs +1 -1
  186. data/sig/kobako/transport/reflection.rbs +2 -2
  187. data/sig/kobako/transport/run.rbs +3 -3
  188. data/sig/kobako/transport.rbs +1 -1
  189. metadata +67 -19
  190. data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
  191. data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
  192. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
  193. data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
  194. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  195. data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
  196. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
  197. data/crates/kobako-wasmtime/src/io.rs +0 -89
  198. data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
  199. data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
  200. /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
  201. /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
@@ -13,9 +13,9 @@ module Kobako
13
13
  # from the decoded payload reports a broken invariant as
14
14
  # InvalidTypeError without its own Utils.with_boundary.
15
15
  def self.decode(bytes)
16
- ValueBudget.assert_within!(bytes, State.current.value_budget)
16
+ ValueBudget.assert_within(bytes, State.current.value_budget)
17
17
  value = FACTORY.load(bytes.b)
18
- validate_utf8!(value)
18
+ validate_utf8(value)
19
19
  block_given? ? yield(value) : value
20
20
  # msgpack gem raises the format/type errors below; +ArgumentError+
21
21
  # covers a yielded block's Value Object invariants — a wire violation
@@ -30,23 +30,23 @@ module Kobako
30
30
  raise TruncatedInputError, e.message
31
31
  end
32
32
 
33
- # +str+ family payloads must be UTF-8
34
- # (docs/wire/payload-msgpack.md § Text and Bytes). The msgpack gem
33
+ # +str+ family payloads must be UTF-8, since a sender carries bytes
34
+ # that are not UTF-8 as +bin+. The msgpack gem
35
35
  # returns UTF-8-tagged Strings for
36
36
  # str family but does not validate the bytes; +bin+ family decodes
37
37
  # to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
38
- # str-typed leaf via Utils.assert_utf8!.
38
+ # str-typed leaf via Utils.assert_utf8.
39
39
  class << self
40
40
  private
41
41
 
42
- def validate_utf8!(value)
42
+ def validate_utf8(value)
43
43
  case value
44
- when String then Utils.assert_utf8!(value, "str payload") if value.encoding == Encoding::UTF_8
45
- when Array then value.each { |v| validate_utf8!(v) }
44
+ when String then Utils.assert_utf8(value, "str payload") if value.encoding == Encoding::UTF_8
45
+ when Array then value.each { |v| validate_utf8(v) }
46
46
  when Hash
47
47
  value.each do |key, val|
48
- validate_utf8!(key)
49
- validate_utf8!(val)
48
+ validate_utf8(key)
49
+ validate_utf8(val)
50
50
  end
51
51
  end
52
52
  end
@@ -10,13 +10,11 @@ require_relative "../handle"
10
10
  module Kobako
11
11
  module Codec # :nodoc:
12
12
  module ExtTypes # :nodoc:
13
- # MessagePack ext type code reserved for Symbol
14
- # (docs/wire/payload-msgpack.md § ext 0x00 — Symbol). Module-private —
13
+ # MessagePack ext type code reserved for Symbol. Module-private —
15
14
  # mirrors +codec::EXT_SYMBOL+ on the
16
15
  # Rust side.
17
16
  EXT_SYMBOL = 0x00
18
- # MessagePack ext type code reserved for Capability Handle
19
- # (docs/wire/payload-msgpack.md § ext 0x01 — Handle). Module-private —
17
+ # MessagePack ext type code reserved for Capability Handle. Module-private —
20
18
  # mirrors +codec::EXT_HANDLE+ on the
21
19
  # Rust side.
22
20
  EXT_HANDLE = 0x01
@@ -51,7 +49,7 @@ module Kobako
51
49
  # unpacker would otherwise apply to invalid bytes.
52
50
  def unpack_symbol(payload)
53
51
  name = payload.b.force_encoding(Encoding::UTF_8)
54
- Utils.assert_utf8!(name, "Symbol payload")
52
+ Utils.assert_utf8(name, "Symbol payload")
55
53
  name.to_sym
56
54
  end
57
55
 
@@ -8,8 +8,7 @@ module Kobako
8
8
  module HandleWalk # :nodoc:
9
9
  # Inclusive Integer range the msgpack gem encodes without raising
10
10
  # +RangeError+ at encode time — signed +int 64+ minimum through
11
- # unsigned +uint 64+ maximum (docs/wire/payload-msgpack.md § Integer
12
- # Range).
11
+ # unsigned +uint 64+ maximum, the range every wire Integer fits.
13
12
  # Anchored as a +Range+ so #primitive_type? stays a single
14
13
  # dispatch line. This is the codec's encode domain — not to
15
14
  # be confused with the Handle id range, which lives on
@@ -17,8 +16,7 @@ module Kobako
17
16
  # represents a different concept entirely.
18
17
  MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
19
18
 
20
- # Whether +value+ falls in the codec's type set
21
- # (docs/wire/payload-msgpack.md § Type Mapping). The +depth+ bound
19
+ # Whether +value+ falls in the codec's closed type set. The +depth+ bound
22
20
  # makes a self-referential container —
23
21
  # reachable as a cyclic Hash key on the +#run+ argument path — read as
24
22
  # non-representable rather than looping.
@@ -32,7 +30,7 @@ module Kobako
32
30
  # than crossing as a token the guest→host restore walk could not
33
31
  # round-trip.
34
32
  def self.deep_wrap(value, handles, depth = 0)
35
- guard_nesting!(depth)
33
+ guard_nesting(depth)
36
34
  case value
37
35
  when ::Array then value.map { |element| deep_wrap(element, handles, depth + 1) }
38
36
  when ::Hash then deep_wrap_hash(value, handles, depth)
@@ -64,7 +62,7 @@ module Kobako
64
62
  # A reference cycle necessarily trips the nesting bound, so a +#run+
65
63
  # argument fails as a clean +Kobako::SandboxError+ rather than an
66
64
  # unbounded host recursion.
67
- def guard_nesting!(depth)
65
+ def guard_nesting(depth)
68
66
  return unless depth > MAX_NESTING_DEPTH
69
67
 
70
68
  raise Kobako::SandboxError,
@@ -9,12 +9,12 @@ module Kobako
9
9
  module Nesting # :nodoc:
10
10
  # Raise InvalidTypeError when +value+ nests past MAX_NESTING_DEPTH; a
11
11
  # value nested exactly to the bound passes.
12
- def self.assert_within_bound!(value, depth = 0)
12
+ def self.assert_within_bound(value, depth = 0)
13
13
  case value
14
- when ::Array then assert_members_within_bound!(value, depth)
14
+ when ::Array then assert_members_within_bound(value, depth)
15
15
  when ::Hash
16
- assert_members_within_bound!(value.keys, depth)
17
- assert_members_within_bound!(value.values, depth)
16
+ assert_members_within_bound(value.keys, depth)
17
+ assert_members_within_bound(value.values, depth)
18
18
  end
19
19
  end
20
20
 
@@ -22,7 +22,7 @@ module Kobako
22
22
  # level deeper. A member is matched by class rather than asked about
23
23
  # itself — it may be a BasicObject — and only one that could be a
24
24
  # container is walked into.
25
- def self.assert_members_within_bound!(members, depth)
25
+ def self.assert_members_within_bound(members, depth)
26
26
  return if members.empty?
27
27
  if depth >= MAX_NESTING_DEPTH
28
28
  raise InvalidTypeError,
@@ -30,9 +30,9 @@ module Kobako
30
30
  end
31
31
  return unless members.any?(::Enumerable)
32
32
 
33
- members.grep(::Enumerable) { |member| assert_within_bound!(member, depth + 1) }
33
+ members.grep(::Enumerable) { |member| assert_within_bound(member, depth + 1) }
34
34
  end
35
- private_class_method :assert_members_within_bound!
35
+ private_class_method :assert_members_within_bound
36
36
  end
37
37
  end
38
38
  end
@@ -5,7 +5,7 @@ require_relative "error"
5
5
  module Kobako
6
6
  module Codec
7
7
  module Utils # :nodoc:
8
- def self.assert_utf8!(string, label)
8
+ def self.assert_utf8(string, label)
9
9
  return if string.valid_encoding?
10
10
 
11
11
  raise InvalidEncodingError, "#{label} is not valid UTF-8"
@@ -5,8 +5,8 @@ require_relative "error"
5
5
  module Kobako
6
6
  module Codec
7
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
8
+ # guest cannot make the host build more objects than it could hold in
9
+ # its own memory. Every value takes at
10
10
  # least one byte, so a payload no longer than the budget needs no count.
11
11
  #
12
12
  # The count stops at the first byte it cannot frame and leaves that
@@ -30,7 +30,7 @@ module Kobako
30
30
  UINT = { 1 => "C", 2 => "n", 4 => "N" }.freeze
31
31
  private_constant :SCALARS, :COUNTS, :LENGTHS, :UINT
32
32
 
33
- def self.assert_within!(bytes, budget)
33
+ def self.assert_within(bytes, budget)
34
34
  return if budget.nil? || bytes.bytesize <= budget
35
35
 
36
36
  remaining = budget
@@ -56,7 +56,7 @@ module Kobako
56
56
  def mount(at, filesystem, access: :read, &policy)
57
57
  raise ArgumentError, "Kobako::Context is spent; ctx.mount is only valid inside the #eval / #run block" if @spent
58
58
 
59
- Runtime.validate_mount!(at, access, filesystem)
59
+ Runtime.validate_mount(at, access, filesystem)
60
60
  mount = Catalog::Mount.new(at: at, access: access, filesystem: filesystem, policy: policy)
61
61
  raise ArgumentError, "cannot override unmounted path #{at.inspect}" unless @grants.include?(mount)
62
62
 
@@ -77,13 +77,13 @@ module Kobako
77
77
 
78
78
  def eval(code, &block) # :nodoc:
79
79
  collect_overrides(&block) if block
80
- invoke!(:eval) { @runtime.eval(dispatch_handler, setup, code.b) }
80
+ invoke(:eval) { @runtime.eval(dispatch_handler, setup, code.b) }
81
81
  end
82
82
 
83
83
  def run(request, &block) # :nodoc:
84
84
  collect_overrides(&block) if block
85
- invoke!(:run, entrypoint: request.entrypoint) do
86
- @runtime.run(dispatch_handler, setup, request.entrypoint.to_s, request.payload(@handles))
85
+ invoke(:run, entrypoint: request.entrypoint) do
86
+ @runtime.run(dispatch_handler, setup, request.entrypoint.to_s, request.encode_payload(@handles))
87
87
  end
88
88
  end
89
89
 
@@ -157,7 +157,7 @@ module Kobako
157
157
  # that raises propagates unwrapped and leaves the guest unrun. A
158
158
  # could-not-start fault ran no invocation at all, so it carries no
159
159
  # Execution and gains only the verb prefix.
160
- def invoke!(verb, entrypoint: nil)
160
+ def invoke(verb, entrypoint: nil)
161
161
  @resolved = @extensions.resolve.transform_values { |object| Transport::Exposure.of(object) }
162
162
  begin
163
163
  snapshot = yield
@@ -5,25 +5,25 @@ module Kobako
5
5
  # answered in Rust by the kind of the same name. Hand one to
6
6
  # Sandbox#mount:
7
7
  #
8
- # sandbox.mount("/data", Kobako::Filesystem::Memory.new("config.yml" => yaml))
8
+ # sandbox.mount("/data", Kobako::Filesystem::Tmpfs.new("config.yml" => yaml))
9
9
  #
10
- # [<tt>Memory.new(files = {}, limit: 64 MiB, inodes:)</tt>]
10
+ # [<tt>Tmpfs.new(files = {}, size: 64 MiB, nr_inodes:)</tt>]
11
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>]
12
+ # Hash of path to contents. Every invocation shares them; one that
13
+ # should start empty mounts a new Tmpfs through Context#mount.
14
+ # [<tt>Bind.new(path)</tt>]
16
15
  # A host directory and everything beneath it. Nothing outside it is
17
16
  # 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.
17
+ # [<tt>Overlay.new(lower: [filesystem, ...], upper: tmpfs)</tt>]
18
+ # Read-only lower Layers, topmost first, under an upper Tmpfs that
19
+ # takes every change, as Linux's overlayfs stacks them. Without
20
+ # +upper+ the view is read-only and needs two lower Layers. It takes
21
+ # at most 500 lower Layers and stacks at most two Overlays deep.
22
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
23
+ # A Tmpfs is bounded as a tmpfs mount is: its files hold
24
+ # at most +size+ bytes together, and it holds at most +nr_inodes+ files and
25
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
26
+ # +nr_inodes+ it holds one for each 4 KiB of +size+, and the root. Past
27
27
  # either, the guest raises Errno::ENOSPC.
28
28
  #
29
29
  # Any object answering +open+ and +stat+ mounts too, as a Filesystem
@@ -33,6 +33,37 @@ module Kobako
33
33
  # exactly, as a mount's block judges them as given. A name may hold a
34
34
  # <tt>\\</tt>, which the guest's paths never separate on, so one joined
35
35
  # to a Windows host path is the Filesystem's to refuse.
36
+ #
37
+ # Each method beyond +open+ and +stat+ is optional, and a change it lacks
38
+ # raises <tt>Errno::EROFS</tt> in the guest:
39
+ #
40
+ # [<tt>children(path)</tt>]
41
+ # Entry names, as Dir.children gives them.
42
+ # [<tt>mkdir(path)</tt>, <tt>rmdir(path)</tt>, <tt>unlink(path)</tt>, <tt>rename(from, to)</tt>]
43
+ # The changes a writable mount passes on.
44
+ #
45
+ # A +stat+ may also answer +dev+, else the Filesystem stands for one
46
+ # device, and +ftype+, which says a FIFO, a socket, or a device is one
47
+ # so the guest's open is refused without asking +open+.
48
+ #
49
+ # The file +open+ answers takes the guest's reads and writes, each named
50
+ # as +IO+ names it:
51
+ #
52
+ # [<tt>pread(length, offset)</tt>, +size+]
53
+ # Required: the bytes there, +nil+ or EOFError at the end, and the
54
+ # length.
55
+ # [<tt>pwrite(data, offset)</tt>, <tt>truncate(length)</tt>]
56
+ # The bytes written, and nothing; without them a write raises
57
+ # <tt>Errno::EROFS</tt>.
58
+ # [<tt>append(data)</tt>]
59
+ # Writes all of +data+ at the end in one step, so another writer's
60
+ # append never lands over it, and answers where the file then ends;
61
+ # without it an append raises <tt>Errno::ENOTSUP</tt>.
62
+ # [+fsync+, +close+]
63
+ # Nothing; without them there is nothing to flush or release.
64
+ # [+stat+]
65
+ # What <tt>stat(path)</tt> answers, for the file's times and count of
66
+ # names; without it the file has one name and no time.
36
67
  module Filesystem
37
68
  end
38
69
  end
@@ -8,7 +8,7 @@ module Kobako
8
8
  def initialize(args: [], kwargs: {})
9
9
  raise ArgumentError, "payload args must be Array" unless args.is_a?(Array)
10
10
 
11
- validate_kwargs!(kwargs)
11
+ validate_kwargs(kwargs)
12
12
  super
13
13
  end
14
14
 
@@ -32,7 +32,7 @@ module Kobako
32
32
 
33
33
  private
34
34
 
35
- def validate_kwargs!(kwargs)
35
+ def validate_kwargs(kwargs)
36
36
  raise ArgumentError, "payload kwargs must be Hash" unless kwargs.is_a?(Hash)
37
37
 
38
38
  kwargs.each_key do |key|
data/lib/kobako/pool.rb CHANGED
@@ -20,7 +20,7 @@ module Kobako
20
20
  # Raises ArgumentError when +slots+ is not a positive Integer or
21
21
  # +checkout_timeout+ is not a positive finite number.
22
22
  def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
23
- validate_slots!(slots)
23
+ validate_slots(slots)
24
24
  @slots = slots
25
25
  @checkout_timeout = normalize_checkout_timeout(checkout_timeout)
26
26
  @sandbox_options = sandbox_options
@@ -69,13 +69,13 @@ module Kobako
69
69
  return [:build, nil]
70
70
  end
71
71
 
72
- await_slot!(deadline)
72
+ await_slot(deadline)
73
73
  [:retry, nil]
74
74
  end
75
75
  end
76
76
 
77
77
  # Must run while holding +@mutex+.
78
- def await_slot!(deadline)
78
+ def await_slot(deadline)
79
79
  remaining = deadline && (deadline - monotonic_now)
80
80
  if remaining && remaining <= 0
81
81
  raise PoolTimeoutError,
@@ -117,7 +117,7 @@ module Kobako
117
117
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
118
118
  end
119
119
 
120
- def validate_slots!(slots)
120
+ def validate_slots(slots)
121
121
  return if slots.is_a?(Integer) && slots.positive?
122
122
 
123
123
  raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
@@ -43,7 +43,7 @@ module Kobako
43
43
  @extensions = Catalog::Extensions.new
44
44
  @grants = Catalog::Grants.new
45
45
  @sealed = false
46
- @runtime = build_runtime!
46
+ @runtime = build_runtime
47
47
  end
48
48
 
49
49
  # Make +object+ reachable from the guest as the constant at +path+, a
@@ -62,7 +62,7 @@ module Kobako
62
62
  # +Kobako+ namespace, a path that collides with an existing binding, or a
63
63
  # call after the first invocation.
64
64
  def bind(path, object = Unresolved)
65
- refuse_once_invoked!("bind")
65
+ refuse_once_invoked("bind")
66
66
  @services.bind(path, object)
67
67
  self
68
68
  end
@@ -74,7 +74,7 @@ module Kobako
74
74
  # Raises ArgumentError for a malformed Extension, a call after the first
75
75
  # invocation, or, at the first invocation, a missing dependency.
76
76
  def install(*extensions)
77
- refuse_once_invoked!("install")
77
+ refuse_once_invoked("install")
78
78
 
79
79
  extensions.each { |extension| @extensions.install(extension, snippets: @snippets, services: @services) }
80
80
  self
@@ -85,18 +85,22 @@ module Kobako
85
85
  # +:read_write+; a write beneath a read-only mount fails as
86
86
  # <tt>Errno::EROFS</tt> in the guest. Returns +self+.
87
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,
88
+ # A given block judges each open and each change +access+ allows, asked
89
+ # with the guest path and +:read+ or +:read_write+, and can only refuse
90
+ # more; an open that may create asks +:read_write+ first, then what it
91
+ # opens for. A falsy answer,
91
92
  # or a StandardError, fails that call as <tt>Errno::EACCES</tt> in the
92
93
  # guest and leaves the script running.
93
94
  #
95
+ # A path may sit beneath another mount, and the guest reaches the
96
+ # innermost one.
97
+ #
94
98
  # 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.
99
+ # a Filesystem, and ArgumentError for a malformed or duplicate path, an
100
+ # unknown access, or a call after the first invocation.
97
101
  def mount(at, filesystem, access: :read, &policy)
98
- refuse_once_invoked!("mount")
99
- Runtime.validate_mount!(at, access, filesystem)
102
+ refuse_once_invoked("mount")
103
+ Runtime.validate_mount(at, access, filesystem)
100
104
  @grants.register(Catalog::Mount.new(at: at, access: access, filesystem: filesystem, policy: policy))
101
105
  self
102
106
  end
@@ -114,7 +118,7 @@ module Kobako
114
118
  # after the first invocation. A snippet that fails to load fails every
115
119
  # invocation with SandboxError, or BytecodeError for bytecode.
116
120
  def preload(code: nil, name: nil, binary: nil)
117
- refuse_once_invoked!("preload")
121
+ refuse_once_invoked("preload")
118
122
 
119
123
  @snippets.register(code: code, name: name, binary: binary)
120
124
  self
@@ -130,7 +134,7 @@ module Kobako
130
134
  # Raises ArgumentError for a malformed or duplicate flag, or a call after
131
135
  # the first invocation.
132
136
  def flag(name, value = "", advisory: false)
133
- refuse_once_invoked!("flag")
137
+ refuse_once_invoked("flag")
134
138
 
135
139
  @flags.register(name, value, advisory: advisory)
136
140
  self
@@ -145,7 +149,7 @@ module Kobako
145
149
  # Handle or a non-Symbol keyword. A failed run raises as #eval does.
146
150
  def run(target, *args, **kwargs, &block)
147
151
  request = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
148
- Codec.within_value_budget(@options.value_budget) { new_invocation.run(request, &block) }
152
+ Codec.within_value_budget(@options.value_budget) { begin_invocation.run(request, &block) }
149
153
  end
150
154
 
151
155
  # Evaluate +code+, mruby source as a String, and return the Execution,
@@ -160,27 +164,19 @@ module Kobako
160
164
  def eval(code, &block)
161
165
  raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
162
166
 
163
- Codec.within_value_budget(@options.value_budget) { new_invocation.eval(code, &block) }
167
+ Codec.within_value_budget(@options.value_budget) { begin_invocation.eval(code, &block) }
164
168
  end
165
169
 
166
170
  private
167
171
 
168
172
  # A runtime whose declared posture falls below the request never runs
169
173
  # guest code.
170
- def build_runtime!
174
+ def build_runtime
171
175
  runtime = Kobako::Runtime.from_path(@wasm_path, **@options.to_h)
172
- @options.enforce_floor!(runtime.profile)
176
+ @options.enforce_floor(runtime.profile)
173
177
  runtime
174
178
  end
175
179
 
176
- # Per-invocation state lives on the Context, so nothing an invocation
177
- # does is written back onto the shared config.
178
- def new_invocation
179
- begin_invocation!
180
- Context.new(runtime: @runtime, services: @services, extensions: @extensions, grants: @grants,
181
- preamble: preamble(@snippets.entries))
182
- end
183
-
184
180
  # The one place the Preamble's parts are put together, for a compile
185
181
  # and for every invocation alike.
186
182
  def preamble(snippets)
@@ -189,17 +185,21 @@ module Kobako
189
185
 
190
186
  # Registration closes at the first invocation, so every invocation ships
191
187
  # the same Services, snippets, flags, Extensions and mounts.
192
- def refuse_once_invoked!(verb)
188
+ def refuse_once_invoked(verb)
193
189
  raise ArgumentError, "cannot #{verb} after first Sandbox invocation" if @sealed
194
190
  end
195
191
 
196
192
  # Sealing before the dependency check keeps a Sandbox whose check failed
197
193
  # closed to further registration. The sealed snippets are compiled once,
198
- # before the first invocation replays them.
199
- def begin_invocation!
194
+ # before the first invocation replays them. Per-invocation state lives on
195
+ # the Context it answers, so nothing an invocation does is written back
196
+ # onto the shared config.
197
+ def begin_invocation
200
198
  @sealed = true
201
- @extensions.assert_dependencies!
199
+ @extensions.assert_dependencies
202
200
  @snippets.compile { |entries| @runtime.compile(preamble(entries)) }
201
+ Context.new(runtime: @runtime, services: @services, extensions: @extensions, grants: @grants,
202
+ preamble: preamble(@snippets.entries))
203
203
  end
204
204
  end
205
205
  end
@@ -74,8 +74,7 @@ module Kobako
74
74
  end
75
75
 
76
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).
77
+ # one value for each of these bytes of +memory_limit+.
79
78
  VALUE_BYTES = 16
80
79
  private_constant :VALUE_BYTES
81
80
 
@@ -85,7 +84,7 @@ module Kobako
85
84
 
86
85
  # Both fallbacks fail closed: an unknown declaration ranks below every
87
86
  # request, and an unknown request refuses every declaration.
88
- def enforce_floor!(declared) # :nodoc:
87
+ def enforce_floor(declared) # :nodoc:
89
88
  return if (PROFILES.index(declared) || -1) >= (PROFILES.index(profile) || PROFILES.size)
90
89
 
91
90
  raise Kobako::SetupError, "runtime declares isolation profile #{declared.inspect}, " \
@@ -4,7 +4,7 @@ module Kobako
4
4
  module Snippet
5
5
  class Bytecode < Data.define(:body) # :nodoc:
6
6
  # Names the snippet form the guest replays this entry as. The wire's
7
- # discriminant byte is assigned by the core envelope, not here.
7
+ # discriminant byte is assigned by the Envelope, not here.
8
8
  KIND = :bytecode
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Kobako
4
4
  module Snippet
5
5
  class Source < Data.define(:name, :body) # :nodoc:
6
6
  # Names the snippet form the guest replays this entry as. The wire's
7
- # discriminant byte is assigned by the core envelope, not here.
7
+ # discriminant byte is assigned by the Envelope, not here.
8
8
  KIND = :source
9
9
  end
10
10
  end
@@ -60,7 +60,7 @@ module Kobako
60
60
  # stays uncaught and traps the invocation rather than being masked as a
61
61
  # rescuable fault.
62
62
  rescue StandardError => e
63
- [:fault, *caught_fault(e, failure)] # : [Symbol, String, String?]
63
+ [:fault, *fault_of(e, failure)] # : [Symbol, String, String?]
64
64
  ensure
65
65
  yielder&.invalidate
66
66
  end
@@ -97,7 +97,7 @@ module Kobako
97
97
  # The guest's own block failing is not the Service's to report at
98
98
  # all, so the Yielder that raised it is asked first — it recognises
99
99
  # its own by identity and words the failure the guest's way.
100
- def caught_fault(error, failure)
100
+ def fault_of(error, failure)
101
101
  block_failure = failure.fault_text(error)
102
102
  return fault("block", block_failure) if block_failure
103
103
 
@@ -111,7 +111,7 @@ module Kobako
111
111
  # unsplatted for methods that take no keywords.
112
112
  def invoke(exposure, method, args, kwargs, yielder)
113
113
  name = method.to_sym
114
- reject_unreachable!(exposure, name)
114
+ reject_unreachable(exposure, name)
115
115
  target = exposure.object
116
116
  block = yielder&.to_proc
117
117
  if kwargs.empty?
@@ -124,7 +124,7 @@ module Kobako
124
124
  # Both the ambient-surface floor and the reference's Exposure answer
125
125
  # through Reflection, so a rejected name discloses nothing about
126
126
  # which of the two refused.
127
- def reject_unreachable!(exposure, name)
127
+ def reject_unreachable(exposure, name)
128
128
  reason = Reflection.refusal(exposure, name)
129
129
  raise UndefinedTargetError, reason if reason
130
130
  end
@@ -163,7 +163,7 @@ module Kobako
163
163
  # Service can change — so it is named here, where the direction is
164
164
  # known, instead of falling to the boundary's codec floor.
165
165
  def encode_ok(value, handles)
166
- Kobako::Codec::Nesting.assert_within_bound!(value)
166
+ Kobako::Codec::Nesting.assert_within_bound(value)
167
167
  Kobako::Codec::Encoder.encode(value)
168
168
  rescue Kobako::Codec::UnsupportedTypeError
169
169
  encode_ok(handles.alloc(value), handles)
@@ -32,7 +32,7 @@ module Kobako
32
32
  # once (a Handle table), so two objects of one class enumerate it once.
33
33
  def self.of(object, surfaces = {})
34
34
  klass = CLASS_OF.bind_call(object)
35
- names = narrows_itself?(object, klass) ? nil : default_names(object, klass, surfaces)
35
+ names = narrows_itself?(object, klass) ? nil : resolve_default_names(object, klass, surfaces)
36
36
  new(object: object, names: names)
37
37
  end
38
38
 
@@ -59,7 +59,7 @@ module Kobako
59
59
 
60
60
  # A callable keeps its callable names; a class-level surface or a
61
61
  # forwarder has no surface of its own to expose.
62
- def default_names(object, klass, surfaces)
62
+ def resolve_default_names(object, klass, surfaces)
63
63
  case object
64
64
  when Proc, Method then CALLABLE
65
65
  when Module, Delegator then NOTHING
@@ -77,20 +77,20 @@ module Kobako
77
77
  # A name with no method never reaches +instance_method+, which would
78
78
  # keep a guest-chosen name as a Symbol for the life of the process.
79
79
  def ambient_refusal(target, name)
80
- return refused(name) if name == :method_missing
80
+ return service_refusal(name) if name == :method_missing
81
81
 
82
82
  klass = lookup_class(target)
83
- return defined_refusal(target, name, klass) if klass.public_method_defined?(name)
83
+ return public_method_refusal(target, name, klass) if klass.public_method_defined?(name)
84
84
  return "no public method #{name.inspect} on target" unless target.respond_to?(name)
85
- return refused(name) if klass.method_defined?(name) || klass.private_method_defined?(name)
85
+ return service_refusal(name) if klass.method_defined?(name) || klass.private_method_defined?(name)
86
86
 
87
87
  owner_refusal(target, name, klass)
88
88
  end
89
89
 
90
- def defined_refusal(target, name, klass)
90
+ def public_method_refusal(target, name, klass)
91
91
  method = klass.instance_method(name)
92
92
  owner_refusal(target, name, method.owner) ||
93
- (authored_for?(target, name, method, klass) ? nil : refused(name))
93
+ (authored_for?(target, name, method, klass) ? nil : service_refusal(name))
94
94
  end
95
95
 
96
96
  # A bound callable keeps its callable names and a record its member
@@ -102,7 +102,7 @@ module Kobako
102
102
  (klass <= Struct || klass <= Data) && target.members.include?(name)
103
103
  end
104
104
 
105
- def refused(name)
105
+ def service_refusal(name)
106
106
  "method #{name.inspect} is not a Service method"
107
107
  end
108
108
 
@@ -110,7 +110,7 @@ module Kobako
110
110
  return nil unless ambient_owner?(owner, target)
111
111
  return nil if GADGET_OWNERS.include?(owner) && CALLABLE_ALLOW.include?(name)
112
112
 
113
- refused(name)
113
+ service_refusal(name)
114
114
  end
115
115
 
116
116
  # The class a method lookup on +target+ starts at. A plain object's