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
@@ -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,
@@ -94,7 +94,7 @@ module Kobako
94
94
  done = true
95
95
  sandbox
96
96
  ensure
97
- release_capacity! unless done
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
@@ -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}"
@@ -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, :profile, :gvl
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,8 +41,9 @@ 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
- @runtime = build_runtime!
46
+ @runtime = build_runtime
45
47
  end
46
48
 
47
49
  # Make +object+ reachable from the guest as the constant at +path+, a
@@ -56,10 +58,11 @@ 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 that collides with an
60
- # existing binding, or a call after the first invocation.
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
- refuse_once_invoked!("bind")
65
+ refuse_once_invoked("bind")
63
66
  @services.bind(path, object)
64
67
  self
65
68
  end
@@ -71,12 +74,37 @@ module Kobako
71
74
  # Raises ArgumentError for a malformed Extension, a call after the first
72
75
  # invocation, or, at the first invocation, a missing dependency.
73
76
  def install(*extensions)
74
- refuse_once_invoked!("install")
77
+ refuse_once_invoked("install")
75
78
 
76
79
  extensions.each { |extension| @extensions.install(extension, snippets: @snippets, services: @services) }
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 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,
92
+ # or a StandardError, fails that call as <tt>Errno::EACCES</tt> in the
93
+ # guest and leaves the script running.
94
+ #
95
+ # A path may sit beneath another mount, and the guest reaches the
96
+ # innermost one.
97
+ #
98
+ # Raises TypeError for a path that is no String or something other than
99
+ # a Filesystem, and ArgumentError for a malformed or duplicate path, an
100
+ # unknown access, or a call after the first invocation.
101
+ def mount(at, filesystem, access: :read, &policy)
102
+ refuse_once_invoked("mount")
103
+ Runtime.validate_mount(at, access, filesystem)
104
+ @grants.register(Catalog::Mount.new(at: at, access: access, filesystem: filesystem, policy: policy))
105
+ self
106
+ end
107
+
80
108
  # Register a snippet that every invocation runs, in the order registered,
81
109
  # before its own code. Returns +self+.
82
110
  #
@@ -90,7 +118,7 @@ module Kobako
90
118
  # after the first invocation. A snippet that fails to load fails every
91
119
  # invocation with SandboxError, or BytecodeError for bytecode.
92
120
  def preload(code: nil, name: nil, binary: nil)
93
- refuse_once_invoked!("preload")
121
+ refuse_once_invoked("preload")
94
122
 
95
123
  @snippets.register(code: code, name: name, binary: binary)
96
124
  self
@@ -106,7 +134,7 @@ module Kobako
106
134
  # Raises ArgumentError for a malformed or duplicate flag, or a call after
107
135
  # the first invocation.
108
136
  def flag(name, value = "", advisory: false)
109
- refuse_once_invoked!("flag")
137
+ refuse_once_invoked("flag")
110
138
 
111
139
  @flags.register(name, value, advisory: advisory)
112
140
  self
@@ -121,7 +149,7 @@ module Kobako
121
149
  # Handle or a non-Symbol keyword. A failed run raises as #eval does.
122
150
  def run(target, *args, **kwargs, &block)
123
151
  request = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
124
- new_invocation.run(request, &block)
152
+ Codec.within_value_budget(@options.value_budget) { begin_invocation.run(request, &block) }
125
153
  end
126
154
 
127
155
  # Evaluate +code+, mruby source as a String, and return the Execution,
@@ -136,42 +164,42 @@ module Kobako
136
164
  def eval(code, &block)
137
165
  raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
138
166
 
139
- new_invocation.eval(code, &block)
167
+ Codec.within_value_budget(@options.value_budget) { begin_invocation.eval(code, &block) }
140
168
  end
141
169
 
142
170
  private
143
171
 
144
172
  # A runtime whose declared posture falls below the request never runs
145
173
  # guest code.
146
- def build_runtime!
147
- runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
148
- @options.stdout_limit, @options.stderr_limit, @options.profile,
149
- @options.gvl)
150
- @options.enforce_floor!(runtime.profile)
174
+ def build_runtime
175
+ runtime = Kobako::Runtime.from_path(@wasm_path, **@options.to_h)
176
+ @options.enforce_floor(runtime.profile)
151
177
  runtime
152
178
  end
153
179
 
154
- # Per-invocation state lives on the Context, so nothing an invocation
155
- # does is written back onto the shared config.
156
- def new_invocation
157
- begin_invocation!
158
- Context.new(runtime: @runtime, services: @services, snippets: @snippets, flags: @flags,
159
- extensions: @extensions)
180
+ # The one place the Preamble's parts are put together, for a compile
181
+ # and for every invocation alike.
182
+ def preamble(snippets)
183
+ [@services.paths, snippets, @flags.entries]
160
184
  end
161
185
 
162
186
  # Registration closes at the first invocation, so every invocation ships
163
- # the same Services, snippets, flags and Extensions.
164
- def refuse_once_invoked!(verb)
187
+ # the same Services, snippets, flags, Extensions and mounts.
188
+ def refuse_once_invoked(verb)
165
189
  raise ArgumentError, "cannot #{verb} after first Sandbox invocation" if @sealed
166
190
  end
167
191
 
168
192
  # Sealing before the dependency check keeps a Sandbox whose check failed
169
193
  # closed to further registration. The sealed snippets are compiled once,
170
- # before the first invocation replays them.
171
- 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
172
198
  @sealed = true
173
- @extensions.assert_dependencies!
174
- @snippets.compile { |entries| @runtime.compile(@services.paths, entries, @flags.entries) }
199
+ @extensions.assert_dependencies
200
+ @snippets.compile { |entries| @runtime.compile(preamble(entries)) }
201
+ Context.new(runtime: @runtime, services: @services, extensions: @extensions, grants: @grants,
202
+ preamble: preamble(@snippets.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, :profile, :gvl)
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 +memory_limit+: 1 MiB of growth past the guest's starting
24
- # memory.
25
- DEFAULT_MEMORY_LIMIT = 1 << 20
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,23 +55,36 @@ 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
- memory_limit = normalize_byte_limit(memory_limit, "memory_limit")
52
- stdout_limit = normalize_byte_limit(stdout_limit, "stdout_limit")
53
- stderr_limit = normalize_byte_limit(stderr_limit, "stderr_limit")
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
+ VALUE_BYTES = 16
79
+ private_constant :VALUE_BYTES
80
+
81
+ def value_budget # :nodoc:
82
+ memory_limit&./(VALUE_BYTES)
83
+ end
84
+
59
85
  # Both fallbacks fail closed: an unknown declaration ranks below every
60
86
  # request, and an unknown request refuses every declaration.
61
- def enforce_floor!(declared) # :nodoc:
87
+ def enforce_floor(declared) # :nodoc:
62
88
  return if (PROFILES.index(declared) || -1) >= (PROFILES.index(profile) || PROFILES.size)
63
89
 
64
90
  raise Kobako::SetupError, "runtime declares isolation profile #{declared.inspect}, " \
@@ -69,17 +95,17 @@ module Kobako
69
95
 
70
96
  # A zero or negative timeout would either fire instantly or never, both
71
97
  # more surprising than an early ArgumentError.
72
- def normalize_timeout(timeout)
98
+ def normalize_timeout(timeout, name)
73
99
  return nil if timeout.nil?
74
- raise ArgumentError, "timeout must be Numeric or nil, got #{timeout.class}" unless timeout.is_a?(Numeric)
100
+ raise ArgumentError, "#{name} must be Numeric or nil, got #{timeout.class}" unless timeout.is_a?(Numeric)
75
101
 
76
102
  seconds = timeout.to_f
77
- raise ArgumentError, "timeout must be > 0 (got #{timeout})" unless seconds.positive? && seconds.finite?
103
+ raise ArgumentError, "#{name} must be > 0 (got #{timeout})" unless seconds.positive? && seconds.finite?
78
104
 
79
105
  seconds
80
106
  end
81
107
 
82
- def normalize_byte_limit(limit, name)
108
+ def normalize_limit(limit, name)
83
109
  return nil if limit.nil?
84
110
  unless limit.is_a?(Integer) && limit.positive?
85
111
  raise ArgumentError, "#{name} must be a positive Integer or nil, got #{limit.inspect}"
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Kobako
4
4
  module Snippet
5
- class Binary < Data.define(:body) # :nodoc:
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "snippet/binary"
3
+ require_relative "snippet/bytecode"
4
4
  require_relative "snippet/source"
5
5
 
6
6
  module Kobako
@@ -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
@@ -3,18 +3,20 @@
3
3
  require_relative "../codec"
4
4
  require_relative "../errors"
5
5
  require_relative "../payload"
6
+ require_relative "block_failure"
6
7
  require_relative "call"
7
8
  require_relative "reflection"
8
9
  require_relative "yielder"
9
10
 
10
11
  module Kobako
11
12
  module Transport
12
- # Answers a routed Call with +[true, body, nil]+ or
13
- # +[false, message, fault_type]+ and never raises, so every failure
14
- # reaches the guest as a fault.
13
+ # Answers a routed Call with one arm per Reply variant: +[:ok, body, nil]+,
14
+ # +[:fault, message, fault_type]+, or +[:break, "", nil]+ — a block
15
+ # exit the guest kept carries nothing. It never raises, so every
16
+ # failure reaches the guest as a fault.
15
17
  module Dispatcher # :nodoc:
16
- # Throw tag for the Yielder's break unwind back to the
17
- # dispatcher's +catch+ frame. +private_constant+ is a
18
+ # Thrown rather than raised, so a Service unwound by a block exit runs
19
+ # its +ensure+ and cannot +rescue+ the exit. +private_constant+ is a
18
20
  # convention boundary — not a defence.
19
21
  BREAK_THROW = :__kobako_break__
20
22
  private_constant :BREAK_THROW
@@ -23,15 +25,18 @@ module Kobako
23
25
 
24
26
  class UnreadableRequestError < Kobako::Codec::Error; end # :nodoc:
25
27
 
26
- # The category kobako's own refusals answer under, keyed by the class
27
- # each is raised as and ordered most specific first. A class absent
28
- # here is the Service's own exception, which answers under +runtime+
29
- # wearing the +<class>: <message>+ shape that says so.
28
+ # The category each failure answering with its message alone takes,
29
+ # keyed by the class raised and ordered most specific first. kobako's
30
+ # own refusals are worded so none reads as a Service's exception, and
31
+ # +ArgumentError+, raised by a Service and by a call that will not
32
+ # bind alike, is named by +argument+ already. A class absent here is
33
+ # the Service's own exception, which answers under +runtime+ wearing
34
+ # the +<class>: <message>+ shape that names it.
30
35
  #
31
36
  # +Kobako::Codec::Error+ is the floor rather than a path of its own:
32
37
  # a codec fault reaching the boundary unnamed is the exchange failing,
33
38
  # and must not be dressed as something a Service raised.
34
- OWN_FAULTS = {
39
+ BARE_FAULTS = {
35
40
  HandleExhaustedError => "internal",
36
41
  UndefinedTargetError => "undefined",
37
42
  ArgumentError => "argument",
@@ -40,23 +45,24 @@ module Kobako
40
45
  Kobako::Codec::Error => "internal",
41
46
  Kobako::SandboxError => "runtime"
42
47
  }.freeze
43
- private_constant :OWN_FAULTS
48
+ private_constant :BARE_FAULTS
44
49
 
45
50
  # The per-invocation state arrives as arguments so the Dispatcher
46
51
  # stays stateless and neither the resolver nor the Context publishes
47
52
  # accessors for it.
48
- def self.dispatch(call, resolver, handles, guest_yielder)
49
- yielder = Yielder.new(guest_yielder, BREAK_THROW, handles) if call.block_given
50
- [true, encode_ok(run(call, resolver, handles, yielder), handles), nil] # : [bool, String, String?]
53
+ def self.dispatch(call, resolver, handles, failure, guest_yielder)
54
+ yielder = Yielder.new(guest_yielder, BREAK_THROW, handles, failure) if call.block_given
55
+ catch(BREAK_THROW) { return [:ok, encode_ok(run(call, resolver, handles, yielder), handles), nil] }
56
+ [:break, "", nil] # : [Symbol, String, String?]
51
57
  # StandardError is the boundary by intent: a Service method's
52
58
  # application fault folds into a guest-rescuable fault, while a
53
59
  # host-process failure (NoMemoryError, SignalException, a bare Exception)
54
60
  # stays uncaught and traps the invocation rather than being masked as a
55
61
  # rescuable fault.
56
62
  rescue StandardError => e
57
- [false, *caught_fault(e, yielder)] # : [bool, String, String?]
63
+ [:fault, *fault_of(e, failure)] # : [Symbol, String, String?]
58
64
  ensure
59
- yielder&.invalidate!
65
+ yielder&.invalidate
60
66
  end
61
67
 
62
68
  class << self
@@ -66,7 +72,7 @@ module Kobako
66
72
  arguments, carried_handle = decode_arguments(call.payload)
67
73
  exposure = resolve_target(call.target, resolver, handles)
68
74
  args, kwargs = resolve_call_args(arguments, handles, carried_handle)
69
- catch(BREAK_THROW) { invoke(exposure, call.method_name, args, kwargs, yielder) }
75
+ invoke(exposure, call.method_name, args, kwargs, yielder)
70
76
  end
71
77
 
72
78
  # A codec fault here is a request that never became a call, restated
@@ -84,21 +90,19 @@ module Kobako
84
90
  arguments.kwargs.transform_values { |v| resolve_arg(v, handles) }]
85
91
  end
86
92
 
87
- # The class prefix marks a Service's own exception and nothing else:
88
- # it is the +<class>: <message>+ shape a Host App is told to keep
89
- # secrets out of, so wearing it says the Service raised. kobako's own
90
- # refusals answer under their own wording instead of borrowing that
91
- # shape.
93
+ # The class prefix marks a Service's own exception that its category
94
+ # does not name: it is the +<class>: <message>+ shape a Host App is
95
+ # told to keep secrets out of, so wearing it says the Service raised.
92
96
  #
93
97
  # The guest's own block failing is not the Service's to report at
94
98
  # all, so the Yielder that raised it is asked first — it recognises
95
99
  # its own by identity and words the failure the guest's way.
96
- def caught_fault(error, yielder)
97
- block_failure = yielder&.fault_text(error)
100
+ def fault_of(error, failure)
101
+ block_failure = failure.fault_text(error)
98
102
  return fault("block", block_failure) if block_failure
99
103
 
100
- own = OWN_FAULTS.find { |klass, _| error.is_a?(klass) }
101
- return fault(own.last, error.message) if own
104
+ bare = BARE_FAULTS.find { |klass, _| error.is_a?(klass) }
105
+ return fault(bare.last, error.message) if bare
102
106
 
103
107
  fault("runtime", "#{error.class}: #{error.message}")
104
108
  end
@@ -107,7 +111,7 @@ module Kobako
107
111
  # unsplatted for methods that take no keywords.
108
112
  def invoke(exposure, method, args, kwargs, yielder)
109
113
  name = method.to_sym
110
- reject_unreachable!(exposure, name)
114
+ reject_unreachable(exposure, name)
111
115
  target = exposure.object
112
116
  block = yielder&.to_proc
113
117
  if kwargs.empty?
@@ -120,7 +124,7 @@ module Kobako
120
124
  # Both the ambient-surface floor and the reference's Exposure answer
121
125
  # through Reflection, so a rejected name discloses nothing about
122
126
  # which of the two refused.
123
- def reject_unreachable!(exposure, name)
127
+ def reject_unreachable(exposure, name)
124
128
  reason = Reflection.refusal(exposure, name)
125
129
  raise UndefinedTargetError, reason if reason
126
130
  end
@@ -159,7 +163,7 @@ module Kobako
159
163
  # Service can change — so it is named here, where the direction is
160
164
  # known, instead of falling to the boundary's codec floor.
161
165
  def encode_ok(value, handles)
162
- Kobako::Codec::Nesting.assert_within_bound!(value)
166
+ Kobako::Codec::Nesting.assert_within_bound(value)
163
167
  Kobako::Codec::Encoder.encode(value)
164
168
  rescue Kobako::Codec::UnsupportedTypeError
165
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
@@ -68,7 +68,7 @@ module Kobako
68
68
  end
69
69
 
70
70
  def class_names(object, klass)
71
- names = authored(klass)
71
+ names = authored_names(klass)
72
72
  case object
73
73
  when Struct, Data then names.concat(object.members)
74
74
  end
@@ -78,15 +78,13 @@ module Kobako
78
78
  def with_singleton(object, names)
79
79
  return names if SINGLETON_METHODS.bind_call(object, false).empty?
80
80
 
81
- names.union(authored(SINGLETON_CLASS_OF.bind_call(object))).freeze
81
+ names.union(authored_names(SINGLETON_CLASS_OF.bind_call(object))).freeze
82
82
  end
83
83
 
84
- # The public methods +owner+ itself defines in source. A method with
85
- # no source, or whose source is the platform's own, is built in.
86
- def authored(owner)
84
+ # The public methods +owner+ itself defines in Ruby source.
85
+ def authored_names(owner)
87
86
  owner.public_instance_methods(false).select do |name|
88
- file, = owner.instance_method(name).source_location
89
- file && !file.start_with?("<internal:")
87
+ Reflection.authored?(owner.instance_method(name))
90
88
  end
91
89
  end
92
90
  end