kobako 0.28.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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +377 -0
  4. data/Cargo.lock +181 -154
  5. data/README.md +99 -459
  6. data/ROADMAP.md +7 -1
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +354 -0
  9. data/crates/kobako-runtime/Cargo.toml +38 -7
  10. data/crates/kobako-runtime/README.md +17 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +9 -1
  12. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  13. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  14. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  15. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  16. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  17. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  18. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  19. data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
  20. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  21. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  29. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
  30. data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
  31. data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
  32. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  33. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  37. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  38. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  39. data/crates/kobako-runtime/src/filesystem.rs +26 -0
  40. data/crates/kobako-runtime/src/grant.rs +20 -0
  41. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  42. data/crates/kobako-runtime/src/lib.rs +16 -1
  43. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  44. data/crates/kobako-runtime/src/locking.rs +218 -0
  45. data/crates/kobako-runtime/src/mount.rs +105 -0
  46. data/crates/kobako-runtime/src/profile.rs +140 -4
  47. data/crates/kobako-runtime/src/runtime.rs +12 -0
  48. data/crates/kobako-runtime/src/sched.rs +210 -0
  49. data/crates/kobako-runtime/src/security.rs +696 -0
  50. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  51. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  52. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  53. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  54. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  55. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  56. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  57. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  58. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  59. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  60. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  61. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  62. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  63. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  64. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  65. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  66. data/crates/kobako-runtime/src/vfs.rs +60 -0
  67. data/crates/kobako-runtime/src/yielder.rs +11 -6
  68. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  70. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  71. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  72. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  73. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  74. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  75. data/crates/kobako-transport/CHANGELOG.md +23 -0
  76. data/crates/kobako-transport/Cargo.toml +3 -3
  77. data/crates/kobako-transport/README.md +6 -5
  78. data/crates/kobako-transport/src/abi.rs +1 -1
  79. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  80. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  81. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  83. data/crates/kobako-transport/src/envelope/fault.rs +4 -3
  84. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  85. data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
  86. data/crates/kobako-transport/src/envelope/reply.rs +34 -16
  87. data/crates/kobako-transport/src/envelope/request.rs +19 -9
  88. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  89. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  90. data/crates/kobako-transport/src/envelope.rs +2 -2
  91. data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
  92. data/crates/kobako-wasmtime/Cargo.toml +25 -19
  93. data/crates/kobako-wasmtime/README.md +13 -7
  94. data/crates/kobako-wasmtime/src/abi.rs +75 -49
  95. data/crates/kobako-wasmtime/src/ambient.rs +64 -6
  96. data/crates/kobako-wasmtime/src/cache.rs +188 -77
  97. data/crates/kobako-wasmtime/src/config.rs +77 -5
  98. data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
  99. data/crates/kobako-wasmtime/src/driver.rs +70 -135
  100. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  101. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  102. data/crates/kobako-wasmtime/src/imports.rs +52 -0
  103. data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
  104. data/crates/kobako-wasmtime/src/invocation.rs +43 -74
  105. data/crates/kobako-wasmtime/src/lib.rs +5 -4
  106. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  107. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
  108. data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
  109. data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
  110. data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
  111. data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
  112. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  113. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
  114. data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
  115. data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
  116. data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
  117. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  118. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  119. data/crates/kobako-wasmtime/src/preview1.rs +512 -0
  120. data/crates/kobako-wasmtime/src/trap.rs +45 -25
  121. data/crates/kobako-wasmtime/src/wasi.rs +333 -0
  122. data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
  123. data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
  124. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  125. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
  126. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
  127. data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
  128. data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
  129. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
  130. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  131. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  132. data/data/kobako.wasm +0 -0
  133. data/ext/kobako/Cargo.toml +4 -2
  134. data/ext/kobako/src/runtime/bridge.rs +154 -81
  135. data/ext/kobako/src/runtime/errors.rs +1 -1
  136. data/ext/kobako/src/runtime/escape.rs +67 -0
  137. data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
  138. data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
  139. data/ext/kobako/src/runtime/filesystem.rs +208 -0
  140. data/ext/kobako/src/runtime/gvl.rs +49 -8
  141. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  142. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  143. data/ext/kobako/src/runtime.rs +125 -199
  144. data/lib/kobako/catalog/extensions.rb +5 -5
  145. data/lib/kobako/catalog/flags.rb +2 -14
  146. data/lib/kobako/catalog/grants.rb +30 -0
  147. data/lib/kobako/catalog/handles.rb +6 -6
  148. data/lib/kobako/catalog/mount.rb +19 -0
  149. data/lib/kobako/catalog/services.rb +13 -2
  150. data/lib/kobako/catalog/snippets.rb +11 -15
  151. data/lib/kobako/catalog.rb +11 -0
  152. data/lib/kobako/codec/decoder.rb +10 -9
  153. data/lib/kobako/codec/ext_types.rb +4 -6
  154. data/lib/kobako/codec/handle_walk.rb +4 -6
  155. data/lib/kobako/codec/nesting.rb +7 -7
  156. data/lib/kobako/codec/state.rb +19 -3
  157. data/lib/kobako/codec/utils.rb +1 -1
  158. data/lib/kobako/codec/value_budget.rb +86 -0
  159. data/lib/kobako/codec.rb +7 -0
  160. data/lib/kobako/context.rb +40 -13
  161. data/lib/kobako/errors.rb +15 -14
  162. data/lib/kobako/filesystem.rb +69 -0
  163. data/lib/kobako/payload/arguments.rb +2 -2
  164. data/lib/kobako/pool.rb +6 -6
  165. data/lib/kobako/sandbox.rb +55 -27
  166. data/lib/kobako/sandbox_options.rb +39 -13
  167. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
  168. data/lib/kobako/snippet/source.rb +1 -1
  169. data/lib/kobako/snippet.rb +1 -1
  170. data/lib/kobako/transport/block_failure.rb +35 -0
  171. data/lib/kobako/transport/dispatcher.rb +33 -29
  172. data/lib/kobako/transport/exposure.rb +7 -9
  173. data/lib/kobako/transport/reflection.rb +79 -23
  174. data/lib/kobako/transport/run.rb +5 -5
  175. data/lib/kobako/transport/yielder.rb +9 -31
  176. data/lib/kobako/transport.rb +1 -0
  177. data/lib/kobako/version.rb +1 -1
  178. data/lib/kobako.rb +35 -0
  179. data/release-please-config.json +1 -0
  180. data/sig/kobako/catalog/extensions.rbs +3 -3
  181. data/sig/kobako/catalog/flags.rbs +0 -4
  182. data/sig/kobako/catalog/grants.rbs +13 -0
  183. data/sig/kobako/catalog/handles.rbs +3 -3
  184. data/sig/kobako/catalog/mount.rbs +24 -0
  185. data/sig/kobako/catalog/services.rbs +5 -1
  186. data/sig/kobako/catalog/snippets.rbs +4 -4
  187. data/sig/kobako/catalog.rbs +1 -0
  188. data/sig/kobako/codec/decoder.rbs +1 -1
  189. data/sig/kobako/codec/handle_walk.rbs +1 -1
  190. data/sig/kobako/codec/nesting.rbs +2 -2
  191. data/sig/kobako/codec/state.rbs +7 -1
  192. data/sig/kobako/codec/utils.rbs +1 -1
  193. data/sig/kobako/codec/value_budget.rbs +23 -0
  194. data/sig/kobako/codec.rbs +3 -2
  195. data/sig/kobako/context.rbs +14 -6
  196. data/sig/kobako/errors.rbs +4 -4
  197. data/sig/kobako/filesystem.rbs +93 -0
  198. data/sig/kobako/guest_yielder.rbs +9 -0
  199. data/sig/kobako/payload/arguments.rbs +1 -1
  200. data/sig/kobako/pool.rbs +3 -3
  201. data/sig/kobako/runtime.rbs +21 -49
  202. data/sig/kobako/sandbox.rbs +9 -5
  203. data/sig/kobako/sandbox_options.rbs +25 -3
  204. data/sig/kobako/snapshot.rbs +20 -0
  205. data/sig/kobako/snippet/bytecode.rbs +12 -0
  206. data/sig/kobako/transport/block_failure.rbs +13 -0
  207. data/sig/kobako/transport/dispatcher.rbs +4 -4
  208. data/sig/kobako/transport/exposure.rbs +2 -2
  209. data/sig/kobako/transport/reflection.rbs +14 -1
  210. data/sig/kobako/transport/run.rbs +3 -3
  211. data/sig/kobako/transport/yielder.rbs +3 -7
  212. data/sig/kobako/transport.rbs +3 -3
  213. metadata +116 -15
  214. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  215. data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
  216. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
  217. data/crates/kobako-wasmtime/src/io.rs +0 -286
  218. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -7,7 +7,7 @@ module Kobako
7
7
  module Catalog
8
8
  class Snippets # :nodoc:
9
9
  def initialize
10
- @entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
10
+ @entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Bytecode]
11
11
  @compiled = {} # : Hash[Kobako::Snippet::Source, String]
12
12
  @compile_tried = false
13
13
  end
@@ -40,21 +40,17 @@ module Kobako
40
40
  if binary
41
41
  raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
42
42
 
43
- register_binary!(binary)
43
+ register_binary(binary)
44
44
  else
45
- register_source!(code, name)
45
+ register_source(code, name)
46
46
  end
47
47
  end
48
48
 
49
49
  private
50
50
 
51
- # The native side frames a source body only as UTF-8 text, so source
52
- # read as bytes is re-tagged, and bytes that are not UTF-8 are refused
53
- # here rather than failing every invocation.
54
- def register_source!(code, name)
55
- code, name = ensure_source_args!(code, name)
56
- body = code.dup.force_encoding(Encoding::UTF_8)
57
- raise ArgumentError, "code must be UTF-8 text" unless body.valid_encoding?
51
+ def register_source(code, name)
52
+ code, name = ensure_source_args(code, name)
53
+ body = Catalog.to_utf8(code, "code")
58
54
 
59
55
  name_sym = normalize_name(name)
60
56
  if @entries.any? { |e| e.is_a?(Snippet::Source) && e.name == name_sym }
@@ -67,7 +63,7 @@ module Kobako
67
63
 
68
64
  # The +code:+ type check runs first so an explicit +code: nil+ reads
69
65
  # as a type error rather than a missing keyword.
70
- def ensure_source_args!(code, name)
66
+ def ensure_source_args(code, name)
71
67
  raise ArgumentError, "missing keyword: code: + name:, or binary:" if code.nil? && name.nil?
72
68
  raise ArgumentError, "code must be a String, got #{code.class}" unless code.is_a?(String)
73
69
  raise ArgumentError, "missing keyword: name:" if name.nil?
@@ -77,10 +73,10 @@ module Kobako
77
73
 
78
74
  # Kept as bytes whatever encoding the caller tagged them with, since
79
75
  # the guest reads them as bytecode rather than text.
80
- def register_binary!(bytes)
76
+ def register_binary(bytes)
81
77
  raise ArgumentError, "binary must be a String, got #{bytes.class}" unless bytes.is_a?(String)
82
78
 
83
- @entries << Snippet::Binary.new(body: bytes.dup.force_encoding(Encoding::ASCII_8BIT))
79
+ @entries << Snippet::Bytecode.new(body: bytes.dup.force_encoding(Encoding::ASCII_8BIT))
84
80
  nil
85
81
  end
86
82
 
@@ -90,9 +86,9 @@ module Kobako
90
86
  case entry
91
87
  when Snippet::Source
92
88
  compiled = @compiled[entry]
93
- compiled ? [Snippet::Binary::KIND, nil, compiled] : [Snippet::Source::KIND, entry.name.to_s, entry.body]
89
+ compiled ? [Snippet::Bytecode::KIND, nil, compiled] : [Snippet::Source::KIND, entry.name.to_s, entry.body]
94
90
  else
95
- [Snippet::Binary::KIND, nil, entry.body]
91
+ [Snippet::Bytecode::KIND, nil, entry.body]
96
92
  end
97
93
  end
98
94
 
@@ -5,8 +5,19 @@ require_relative "catalog/services"
5
5
  require_relative "catalog/snippets"
6
6
  require_relative "catalog/flags"
7
7
  require_relative "catalog/extensions"
8
+ require_relative "catalog/mount"
9
+ require_relative "catalog/grants"
8
10
 
9
11
  module Kobako
10
12
  module Catalog # :nodoc:
13
+ # The native side carries a registered text only as UTF-8, so text read
14
+ # as bytes is re-tagged, and bytes that are not UTF-8 are refused at
15
+ # registration rather than failing every invocation.
16
+ def self.to_utf8(text, what)
17
+ utf8 = text.dup.force_encoding(Encoding::UTF_8)
18
+ raise ArgumentError, "#{what} must be UTF-8 text" unless utf8.valid_encoding?
19
+
20
+ utf8
21
+ end
11
22
  end
12
23
  end
@@ -13,8 +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
17
  value = FACTORY.load(bytes.b)
17
- validate_utf8!(value)
18
+ validate_utf8(value)
18
19
  block_given? ? yield(value) : value
19
20
  # msgpack gem raises the format/type errors below; +ArgumentError+
20
21
  # covers a yielded block's Value Object invariants — a wire violation
@@ -29,23 +30,23 @@ module Kobako
29
30
  raise TruncatedInputError, e.message
30
31
  end
31
32
 
32
- # +str+ family payloads must be UTF-8
33
- # (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
34
35
  # returns UTF-8-tagged Strings for
35
36
  # str family but does not validate the bytes; +bin+ family decodes
36
37
  # to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
37
- # str-typed leaf via Utils.assert_utf8!.
38
+ # str-typed leaf via Utils.assert_utf8.
38
39
  class << self
39
40
  private
40
41
 
41
- def validate_utf8!(value)
42
+ def validate_utf8(value)
42
43
  case value
43
- when String then Utils.assert_utf8!(value, "str payload") if value.encoding == Encoding::UTF_8
44
- 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) }
45
46
  when Hash
46
47
  value.each do |key, val|
47
- validate_utf8!(key)
48
- validate_utf8!(val)
48
+ validate_utf8(key)
49
+ validate_utf8(val)
49
50
  end
50
51
  end
51
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
 
@@ -63,7 +61,7 @@ module Kobako
63
61
  # shape. The sighting is recorded so a Handle-free decode can skip the
64
62
  # downstream resolution walk.
65
63
  def unpack_handle(payload, state)
66
- state.record_handle!
64
+ state.record_handle
67
65
  bytes = payload.b
68
66
  raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
69
67
 
@@ -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
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Kobako
4
4
  module Codec
5
- # Whether a Capability Handle crossed the decode in flight, kept per
6
- # thread so plain instance variables stay sound.
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
@@ -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"
@@ -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 it could hold in
9
+ # its own memory. 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
@@ -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:, snippets:, flags:, extensions:) # :nodoc:
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) do
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
- invoke!(:run, entrypoint: request.entrypoint) do
68
- @runtime.run(dispatch_handler, @services.paths, @snippets.entries, @flags.entries,
69
- 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))
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!(snapshot)
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?)
@@ -130,14 +157,14 @@ module Kobako
130
157
  # that raises propagates unwrapped and leaves the guest unrun. A
131
158
  # could-not-start fault ran no invocation at all, so it carries no
132
159
  # Execution and gains only the verb prefix.
133
- def invoke!(verb, entrypoint: nil)
160
+ def invoke(verb, entrypoint: nil)
134
161
  @resolved = @extensions.resolve.transform_values { |object| Transport::Exposure.of(object) }
135
162
  begin
136
163
  snapshot = yield
137
164
  rescue Kobako::TrapError => e
138
165
  raise with_verb(e, verb)
139
166
  end
140
- populate_observability!(snapshot)
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
- # Gives an invocation failure the Execution of the run that failed, so a
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 CarriesExecution
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
- # What the guest reported about a failure.
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 CarriesExecution
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 CarriesExecution
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 call reached the Service method with arguments that did not fit.
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,69 @@
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::Tmpfs.new("config.yml" => yaml))
9
+ #
10
+ # [<tt>Tmpfs.new(files = {}, size: 64 MiB, nr_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; one that
13
+ # should start empty mounts a new Tmpfs through Context#mount.
14
+ # [<tt>Bind.new(path)</tt>]
15
+ # A host directory and everything beneath it. Nothing outside it is
16
+ # reachable, a symbolic link pointing out included.
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
+ #
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
+ # directories, its root among them; +nil+ lifts either bound. Without
26
+ # +nr_inodes+ it holds one for each 4 KiB of +size+, 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
+ #
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.
67
+ module Filesystem
68
+ end
69
+ end