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
@@ -2,11 +2,11 @@
2
2
  //!
3
3
  //! The Ruby-visible classes are
4
4
  //!
5
- //! Kobako::Runtime — wraps a `kobako_wasmtime::Driver`
6
- //! Kobako::Runtime::Snapshot — one invocation's completion + captures + usage
5
+ //! Kobako::Runtime — wraps a `kobako_wasmtime::Driver`
6
+ //! Kobako::Snapshot — one invocation's completion + captures + usage
7
7
  //!
8
8
  //! `Kobako::Runtime` is constructed via `Kobako::Runtime.from_path(path,
9
- //! timeout, memory_limit, stdout_limit, stderr_limit, profile)`. Every
9
+ //! **options)`, the options keyed by the `Kobako::SandboxOptions` members. Every
10
10
  //! invocation (`#eval` / `#run`) takes that run's dispatch handler as a call
11
11
  //! argument, instantiates a fresh instance, and returns a `Snapshot` — the
12
12
  //! whole per-invocation result — so the Runtime holds no per-invocation
@@ -19,11 +19,16 @@
19
19
 
20
20
  mod bridge;
21
21
  mod errors;
22
+ mod escape;
23
+ mod filesystem;
22
24
  mod gvl;
25
+ mod pinned_value;
26
+ mod snapshot;
23
27
 
24
28
  use magnus::{
25
- function, method, prelude::*, typed_data::DataTypeFunctions, value::Opaque,
26
- Error as MagnusError, Exception, RArray, RModule, RString, Ruby, Symbol, TypedData, Value,
29
+ function, method, prelude::*, scan_args::get_kwargs, typed_data::DataTypeFunctions,
30
+ value::Opaque, Error as MagnusError, RArray, RHash, RModule, RString, Ruby, Symbol, TypedData,
31
+ Value,
27
32
  };
28
33
 
29
34
  use std::path::Path;
@@ -33,15 +38,10 @@ use std::time::Duration;
33
38
  use kobako_runtime::dispatch::DispatchHandler;
34
39
  use kobako_runtime::profile::Profile;
35
40
  use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
36
- use kobako_runtime::snapshot::{Completion, Snapshot as RuntimeSnapshot};
37
- use kobako_transport::envelope::{Bindings, Flag, Outcome, Preamble, Snippet, Snippets};
41
+ use kobako_transport::envelope::{Bindings, Flag, Preamble, Snippet, Snippets};
38
42
  use kobako_wasmtime::{Config, Driver};
39
43
 
40
- /// A Panic's fields as they cross to Ruby: origin, class, message,
41
- /// backtrace, and the names the invocation could have used in place of the
42
- /// one it named. Named so the Ruby side reads them positionally against one
43
- /// shape, matching the `Kobako::Outcome::panic_fields` alias.
44
- type PanicFields = (String, String, String, Vec<String>, Vec<String>);
44
+ use snapshot::Snapshot;
45
45
 
46
46
  /// The one place the boundary borrows an RString's bytes: the borrow does
47
47
  /// not outlive this call, so no Ruby allocation can move the RString
@@ -53,7 +53,7 @@ fn rstring_to_vec(s: RString) -> Vec<u8> {
53
53
 
54
54
  /// `kind` arrives as a Symbol so the registry never names the wire's
55
55
  /// discriminant; an off-ladder kind raises rather than defaulting.
56
- fn snippets_from(ruby: &Ruby, entries: RArray) -> Result<Snippets, MagnusError> {
56
+ fn to_snippets(ruby: &Ruby, entries: RArray) -> Result<Snippets, MagnusError> {
57
57
  let mut snippets = Snippets {
58
58
  entries: Vec::with_capacity(entries.len()),
59
59
  };
@@ -80,16 +80,16 @@ fn snippets_from(ruby: &Ruby, entries: RArray) -> Result<Snippets, MagnusError>
80
80
  }
81
81
 
82
82
  /// One owner for the Preamble every guest entry reads, so an invocation
83
- /// and a compile cannot drift in what they send.
84
- fn preamble_from(
85
- ruby: &Ruby,
86
- paths: Vec<String>,
87
- snippets: RArray,
88
- flags: RArray,
89
- ) -> Result<Preamble, MagnusError> {
83
+ /// and a compile cannot drift in what they send. An invocation's setup
84
+ /// and a compile's preamble both open with its paths, snippets, and flags.
85
+ fn to_preamble(ruby: &Ruby, parts: RArray) -> Result<Preamble, MagnusError> {
86
+ let paths: RArray = parts.entry(0)?;
87
+ let flags: RArray = parts.entry(2)?;
90
88
  let mut preamble = Preamble::default();
91
- preamble.bindings = Bindings { paths };
92
- preamble.snippets = snippets_from(ruby, snippets)?;
89
+ preamble.bindings = Bindings {
90
+ paths: paths.to_vec()?,
91
+ };
92
+ preamble.snippets = to_snippets(ruby, parts.entry(1)?)?;
93
93
  preamble.flags = flags
94
94
  .to_vec::<(String, String, bool)>()?
95
95
  .into_iter()
@@ -111,27 +111,32 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
111
111
  // directly into those classes through the constructors and mappers
112
112
  // in `runtime/errors.rs` — no intermediate hierarchy is registered.
113
113
 
114
+ // This extension is the only frontend in its process, so the first
115
+ // registration is the one that stands.
116
+ let _ = kobako_runtime::sched::register_blocking(Box::new(gvl::GvlFreeWait));
117
+
114
118
  let runtime = kobako.define_class("Runtime", ruby.class_object())?;
115
- runtime.define_singleton_method("from_path", function!(Runtime::from_path, 7))?;
116
- runtime.define_method("eval", method!(Runtime::eval, 5))?;
117
- runtime.define_method("run", method!(Runtime::run, 6))?;
119
+ runtime.define_singleton_method("from_path", function!(Runtime::from_path, 2))?;
120
+ runtime.define_method("eval", method!(Runtime::eval, 3))?;
121
+ runtime.define_method("run", method!(Runtime::run, 4))?;
118
122
  runtime.define_method("profile", method!(Runtime::profile, 0))?;
119
- runtime.define_method("compile", method!(Runtime::compile, 3))?;
123
+ runtime.define_method("compile", method!(Runtime::compile, 1))?;
124
+ runtime.define_singleton_method("validate_mount", function!(filesystem::validate_mount, 3))?;
120
125
  // The guest re-enters for a block yield through a frame-scoped
121
- // `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
122
- // method on Runtime.
123
- bridge::register(runtime)?;
126
+ // `Kobako::GuestYielder` the dispatcher hands the Proc, not a method on
127
+ // Runtime.
128
+ bridge::register(kobako)?;
129
+ filesystem::register(ruby, kobako)?;
124
130
 
125
131
  // Snapshot — the per-invocation result object each entry point returns.
126
- let snapshot = runtime.define_class("Snapshot", ruby.class_object())?;
127
- snapshot.define_method("outcome", method!(Snapshot::outcome, 0))?;
128
- snapshot.define_method("trap_error", method!(Snapshot::trap_error, 0))?;
129
- snapshot.define_method("wall_time", method!(Snapshot::wall_time, 0))?;
130
- snapshot.define_method("memory_peak", method!(Snapshot::memory_peak, 0))?;
131
- snapshot.define_method("stdout", method!(Snapshot::stdout, 0))?;
132
- snapshot.define_method("stdout_truncated?", method!(Snapshot::stdout_truncated, 0))?;
133
- snapshot.define_method("stderr", method!(Snapshot::stderr, 0))?;
134
- snapshot.define_method("stderr_truncated?", method!(Snapshot::stderr_truncated, 0))?;
132
+ snapshot::register(ruby, kobako)?;
133
+
134
+ // A wrapped class is otherwise looked up the first time an object is
135
+ // wrapped, through a Ruby call that a pending interrupt would turn into
136
+ // a panic; a signal arriving during a process's first run waits for
137
+ // exactly that moment.
138
+ Runtime::class(ruby);
139
+ Snapshot::class(ruby);
135
140
 
136
141
  Ok(())
137
142
  }
@@ -157,34 +162,61 @@ impl DataTypeFunctions for Runtime {}
157
162
 
158
163
  impl Runtime {
159
164
  /// The only Ruby-facing constructor, so Engine and Module are never
160
- /// visible to Ruby.
161
- fn from_path(
162
- path: String,
163
- timeout_seconds: Option<f64>,
164
- memory_limit: Option<usize>,
165
- stdout_limit: Option<usize>,
166
- stderr_limit: Option<usize>,
167
- profile: Symbol,
168
- gvl: Symbol,
169
- ) -> Result<Self, MagnusError> {
165
+ /// visible to Ruby. Every option is a required keyword: the defaults
166
+ /// belong to `Kobako::SandboxOptions`, which names the keys.
167
+ fn from_path(path: String, options: RHash) -> Result<Self, MagnusError> {
170
168
  let ruby = Ruby::get().expect("Ruby thread");
171
- let timeout = match timeout_seconds {
172
- None => None,
173
- Some(secs) if secs.is_finite() && secs > 0.0 => Some(Duration::from_secs_f64(secs)),
174
- Some(secs) => {
175
- // An invalid cap argument is a Host App
176
- // programming error and raises `ArgumentError`, outside the
177
- // construction-failure `SetupError` branch. `SandboxOptions`
178
- // is the primary guard (it never lets a bad timeout reach
179
- // here); this is defence-in-depth for direct `from_path` calls.
180
- return Err(MagnusError::new(
181
- ruby.exception_arg_error(),
182
- format!("timeout must be > 0 and finite, got {secs} seconds"),
183
- ));
184
- }
169
+ type Options = (
170
+ Option<f64>,
171
+ Option<f64>,
172
+ Option<usize>,
173
+ Option<usize>,
174
+ Option<usize>,
175
+ Option<usize>,
176
+ Symbol,
177
+ Symbol,
178
+ );
179
+ let options = get_kwargs::<_, Options, (), ()>(
180
+ options,
181
+ &[
182
+ "timeout",
183
+ "setup_timeout",
184
+ "memory_limit",
185
+ "stdout_limit",
186
+ "stderr_limit",
187
+ "open_file_limit",
188
+ "profile",
189
+ "gvl",
190
+ ],
191
+ &[],
192
+ )?;
193
+ let (
194
+ timeout_seconds,
195
+ setup_timeout_seconds,
196
+ memory_limit,
197
+ stdout_limit,
198
+ stderr_limit,
199
+ open_file_limit,
200
+ profile,
201
+ gvl,
202
+ ) = options.required;
203
+ // An invalid cap argument is a Host App programming error and
204
+ // raises `ArgumentError`, outside the construction-failure
205
+ // `SetupError` branch. `SandboxOptions` is the primary guard (it
206
+ // never lets a bad timeout reach here); this is defence-in-depth
207
+ // for direct `from_path` calls.
208
+ let to_duration = |seconds: Option<f64>, name: &str| match seconds {
209
+ None => Ok(None),
210
+ Some(secs) if secs.is_finite() && secs > 0.0 => Ok(Some(Duration::from_secs_f64(secs))),
211
+ Some(secs) => Err(MagnusError::new(
212
+ ruby.exception_arg_error(),
213
+ format!("{name} must be > 0 and finite, got {secs} seconds"),
214
+ )),
185
215
  };
216
+ let timeout = to_duration(timeout_seconds, "timeout")?;
217
+ let setup_timeout = to_duration(setup_timeout_seconds, "setup_timeout")?;
186
218
  // Fail closed on an off-ladder rung: an unrecognized posture must
187
- // never fall back to a grant. Same defence-in-depth posture as the
219
+ // never fall back to a weaker posture. Same defence in depth as the
188
220
  // timeout guard above — `SandboxOptions` is the primary validator.
189
221
  let profile = match profile.name()?.as_ref() {
190
222
  "hermetic" => Profile::Hermetic,
@@ -210,17 +242,16 @@ impl Runtime {
210
242
  }
211
243
  };
212
244
 
213
- let driver = Driver::new(
214
- Path::new(&path),
215
- Config {
216
- timeout,
217
- memory_limit,
218
- stdout_limit,
219
- stderr_limit,
220
- profile,
221
- },
222
- )
223
- .map_err(|e| errors::setup_to_magnus(&ruby, e))?;
245
+ let mut config = Config::default();
246
+ config.timeout = timeout;
247
+ config.setup_timeout = setup_timeout;
248
+ config.memory_limit = memory_limit;
249
+ config.stdout_limit = stdout_limit;
250
+ config.stderr_limit = stderr_limit;
251
+ config.open_file_limit = open_file_limit;
252
+ config.profile = profile;
253
+ let driver =
254
+ Driver::new(Path::new(&path), config).map_err(|e| errors::setup_to_magnus(&ruby, e))?;
224
255
  Ok(Self {
225
256
  driver,
226
257
  release_gvl,
@@ -233,40 +264,32 @@ impl Runtime {
233
264
  // success or trap alike. Only a could-not-start fault (a missing export
234
265
  // or a fault before the export call) raises directly, as the class
235
266
  // `errors::to_magnus` gives its cause, since it yields no `Snapshot`.
267
+ //
268
+ // `setup` is the invocation's `[paths, snippets, flags, mounts]`: the
269
+ // Preamble's three parts, then the Mounts as
270
+ // `filesystem::to_grants` reads them.
236
271
  // -----------------------------------------------------------------
237
272
 
238
273
  fn eval(
239
274
  &self,
240
275
  dispatch: Value,
241
- paths: RArray,
242
- snippets: RArray,
243
- flags: RArray,
276
+ setup: RArray,
244
277
  source: RString,
245
278
  ) -> Result<Snapshot, MagnusError> {
246
279
  let source = rstring_to_vec(source);
247
- self.invoke(
248
- dispatch,
249
- paths,
250
- snippets,
251
- flags,
252
- Entry::Eval { source: &source },
253
- )
280
+ self.invoke(dispatch, setup, Entry::Eval { source: &source })
254
281
  }
255
282
 
256
283
  fn run(
257
284
  &self,
258
285
  dispatch: Value,
259
- paths: RArray,
260
- snippets: RArray,
261
- flags: RArray,
286
+ setup: RArray,
262
287
  entrypoint: String,
263
288
  payload: RString,
264
289
  ) -> Result<Snapshot, MagnusError> {
265
290
  self.invoke(
266
291
  dispatch,
267
- paths,
268
- snippets,
269
- flags,
292
+ setup,
270
293
  Entry::Run {
271
294
  entrypoint: &entrypoint,
272
295
  payload: rstring_to_vec(payload),
@@ -274,14 +297,12 @@ impl Runtime {
274
297
  )
275
298
  }
276
299
 
277
- /// One owner for the wiring both verbs share, so a Preamble or handler
278
- /// change cannot drift between them.
300
+ /// One owner for the wiring both verbs share, so a Preamble, a Grant,
301
+ /// or a handler change cannot drift between them.
279
302
  fn invoke(
280
303
  &self,
281
304
  dispatch: Value,
282
- paths: RArray,
283
- snippets: RArray,
284
- flags: RArray,
305
+ setup: RArray,
285
306
  entry: Entry<'_>,
286
307
  ) -> Result<Snapshot, MagnusError> {
287
308
  let ruby = Ruby::get().expect("Ruby thread");
@@ -290,15 +311,17 @@ impl Runtime {
290
311
  Entry::Run { .. } => "run",
291
312
  };
292
313
  let handler = build_handler(dispatch);
293
- let preamble = preamble_from(&ruby, paths.to_vec()?, snippets, flags)?;
314
+ let preamble = to_preamble(&ruby, setup)?;
315
+ let (grants, _pins) = filesystem::to_grants(&ruby, setup.entry(3)?)?;
294
316
  // Release the GVL around the guest span iff this Sandbox asks for it;
295
317
  // the closure touches no Ruby VALUE (the driver is magnus-free, and a
296
- // guest→host dispatch re-acquires the GVL through the bridge).
318
+ // guest→host dispatch, or a mount's block, re-acquires the GVL).
297
319
  let result = gvl::region(self.release_gvl, || {
298
- self.driver.invoke(entry, &preamble, handler)
320
+ self.driver.invoke(entry, &preamble, handler, &grants)
299
321
  });
322
+ escape::resume()?;
300
323
  result
301
- .map(|snapshot| Snapshot(snapshot, verb))
324
+ .map(|snapshot| Snapshot::new(snapshot, verb))
302
325
  .map_err(|e| errors::to_magnus(&ruby, e))
303
326
  }
304
327
 
@@ -308,15 +331,11 @@ impl Runtime {
308
331
  /// invocation sends, `paths` included, so the driver can tell whether
309
332
  /// the bytecode still fits it; no dispatch handler is bound, since
310
333
  /// compiling runs no script.
311
- fn compile(
312
- &self,
313
- paths: RArray,
314
- snippets: RArray,
315
- flags: RArray,
316
- ) -> Result<Option<RArray>, MagnusError> {
334
+ fn compile(&self, preamble: RArray) -> Result<Option<RArray>, MagnusError> {
317
335
  let ruby = Ruby::get().expect("Ruby thread");
318
- let preamble = preamble_from(&ruby, paths.to_vec()?, snippets, flags)?;
336
+ let preamble = to_preamble(&ruby, preamble)?;
319
337
  let bytecodes = gvl::region(self.release_gvl, || self.driver.compile(&preamble));
338
+ escape::resume()?;
320
339
  Ok(bytecodes.map(|bytecodes| {
321
340
  ruby.ary_from_iter(
322
341
  bytecodes
@@ -349,96 +368,3 @@ fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
349
368
  as Arc<dyn DispatchHandler>,
350
369
  )
351
370
  }
352
-
353
- /// One invocation's result at the Ruby boundary — the whole `Snapshot` the
354
- /// driver produced, exposed as `Kobako::Runtime::Snapshot`. Usage and the
355
- /// two output captures are present on every outcome, so the trap path
356
- /// carries them just like the value path; the completion is read as either
357
- /// the Outcome (`#outcome`) or a trap (`#trap_error`). The verb names the
358
- /// invocation, so an unsupported one says which operation is lacking.
359
- #[derive(TypedData)]
360
- #[magnus(class = "Kobako::Runtime::Snapshot", free_immediately, size)]
361
- struct Snapshot(RuntimeSnapshot, &'static str);
362
-
363
- impl DataTypeFunctions for Snapshot {}
364
-
365
- impl Snapshot {
366
- /// Already split off the core envelope, so the Ruby side maps a failure
367
- /// onto its error taxonomy without decoding a payload byte: the ok
368
- /// arm's payload, or a Panic's fields beside an empty payload. A trap
369
- /// has no Outcome; `#trap_error` is the discriminator there.
370
- fn outcome(&self) -> Result<(RString, Option<PanicFields>), MagnusError> {
371
- let ruby = Ruby::get().expect("Ruby thread");
372
- match &self.0.completion {
373
- Completion::Outcome(Outcome::Ok(value)) => Ok((ruby.str_from_slice(value), None)),
374
- Completion::Outcome(Outcome::Panic(panic)) => Ok((
375
- ruby.str_from_slice(&[]),
376
- Some((
377
- panic.origin.name().to_owned(),
378
- panic.error.name.clone(),
379
- panic.error.message.clone(),
380
- panic.error.backtrace.clone(),
381
- panic.available.clone(),
382
- )),
383
- )),
384
- // The Guest Binary answered rather than broke, so it reads as
385
- // the Sandbox's failure, worded by the host.
386
- Completion::Outcome(Outcome::Unsupported(_)) => Ok((
387
- ruby.str_from_slice(&[]),
388
- Some((
389
- "sandbox".to_owned(),
390
- "Kobako::SandboxError".to_owned(),
391
- format!(
392
- "the Sandbox runtime does not support #{}; \
393
- rebuild it against the kobako version in use",
394
- self.1
395
- ),
396
- Vec::new(),
397
- Vec::new(),
398
- )),
399
- )),
400
- Completion::Trap(_) => Err(MagnusError::new(
401
- ruby.exception_runtime_error(),
402
- "a trapped run has no outcome",
403
- )),
404
- }
405
- }
406
-
407
- /// Unraised, so the Sandbox layer can attach the run's Execution before
408
- /// raising it.
409
- fn trap_error(&self) -> Result<Option<Exception>, MagnusError> {
410
- let Completion::Trap(trap) = &self.0.completion else {
411
- return Ok(None);
412
- };
413
- let ruby = Ruby::get().expect("Ruby thread");
414
- errors::trap_class(&ruby, trap)
415
- .new_instance((trap.to_string(),))
416
- .map(Some)
417
- }
418
-
419
- fn wall_time(&self) -> f64 {
420
- self.0.usage.wall_time
421
- }
422
-
423
- fn memory_peak(&self) -> usize {
424
- self.0.usage.memory_peak
425
- }
426
-
427
- fn stdout(&self) -> RString {
428
- let ruby = Ruby::get().expect("Ruby thread");
429
- ruby.str_from_slice(&self.0.stdout.bytes)
430
- }
431
-
432
- fn stdout_truncated(&self) -> bool {
433
- self.0.stdout.truncated
434
- }
435
-
436
- fn stderr(&self) -> RString {
437
- let ruby = Ruby::get().expect("Ruby thread");
438
- ruby.str_from_slice(&self.0.stderr.bytes)
439
- }
440
-
441
- fn stderr_truncated(&self) -> bool {
442
- self.0.stderr.truncated
443
- }
444
- }
@@ -16,10 +16,10 @@ module Kobako
16
16
  # The Extension readers are duck-typed, so a malformed +name+ or
17
17
  # +backend.path+ surfaces through the registry it routes to.
18
18
  def install(extension, snippets:, services:)
19
- validate!(extension)
19
+ validate(extension)
20
20
  snippets.register(code: extension.source, name: extension.name)
21
21
  backend = extension.backend
22
- services.bind(backend.path, install_object(backend)) if backend
22
+ services.bind(backend.path, object_of(backend)) if backend
23
23
  @entries << extension
24
24
  self
25
25
  end
@@ -27,7 +27,7 @@ module Kobako
27
27
  # The asserted flag flips only on success, so a check that failed
28
28
  # re-runs on the next invocation rather than silently passing a broken
29
29
  # Sandbox.
30
- def assert_dependencies!
30
+ def assert_dependencies
31
31
  return self if @asserted
32
32
 
33
33
  extension, dependency = unmet_dependency
@@ -52,7 +52,7 @@ module Kobako
52
52
 
53
53
  private
54
54
 
55
- def install_object(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
55
+ def object_of(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
56
56
 
57
57
  def resolve_backend(extension, resolved, by_provider)
58
58
  backend = extension.backend
@@ -64,7 +64,7 @@ module Kobako
64
64
  resolved[backend.path.to_s] = by_provider.fetch(provider) { by_provider[provider] = provider.call }
65
65
  end
66
66
 
67
- def validate!(extension)
67
+ def validate(extension)
68
68
  source = extension.source
69
69
  raise ArgumentError, "Extension #source must be a String, got #{source.class}" unless source.is_a?(String)
70
70
 
@@ -18,24 +18,12 @@ module Kobako
18
18
  end
19
19
  raise ArgumentError, "flag value must be a String, got #{value.class}" unless value.is_a?(String)
20
20
 
21
- key = utf8!(name.to_s, "flag name")
21
+ key = Catalog.to_utf8(name.to_s, "flag name")
22
22
  raise ArgumentError, "flag #{key.inspect} already set" if @entries.key?(key)
23
23
 
24
- @entries[key] = [utf8!(value, "flag value"), !advisory]
24
+ @entries[key] = [Catalog.to_utf8(value, "flag value"), !advisory]
25
25
  nil
26
26
  end
27
-
28
- private
29
-
30
- # The native side carries a flag only as UTF-8 text, so text read as
31
- # bytes is re-tagged, and bytes that are not UTF-8 are refused here
32
- # rather than failing every invocation.
33
- def utf8!(text, what)
34
- utf8 = text.dup.force_encoding(Encoding::UTF_8)
35
- raise ArgumentError, "#{what} must be UTF-8 text" unless utf8.valid_encoding?
36
-
37
- utf8
38
- end
39
27
  end
40
28
  end
41
29
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ module Catalog
5
+ class Grants # :nodoc:
6
+ def initialize
7
+ @entries = {} # : Hash[String, Mount]
8
+ end
9
+
10
+ # Each Grant decides what it cannot stand beside.
11
+ def register(grant)
12
+ reason = @entries.each_value.lazy.filter_map { |other| grant.conflict(other) }.first
13
+ raise ArgumentError, reason if reason
14
+
15
+ @entries[grant.key] = grant
16
+ nil
17
+ end
18
+
19
+ def include?(grant)
20
+ @entries.key?(grant.key)
21
+ end
22
+
23
+ # Each Grant as the native side reads it, an override standing in for
24
+ # the Grant it shares a key with.
25
+ def entries(overrides = {})
26
+ @entries.merge(overrides).values.map(&:to_native)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -23,8 +23,8 @@ module Kobako
23
23
  # call made through this Handle is authorized against the reference
24
24
  # the guest was given.
25
25
  def alloc(object)
26
- reject_unwrappable!(object)
27
- ensure_capacity!
26
+ reject_unwrappable(object)
27
+ ensure_capacity
28
28
  id = @next_id
29
29
  @entries[id] = Kobako::Transport::Exposure.of(object, @surfaces)
30
30
  @next_id = id + 1
@@ -36,7 +36,7 @@ module Kobako
36
36
  end
37
37
 
38
38
  def exposure(id)
39
- require_bound!(id)
39
+ require_bound(id)
40
40
  @entries[id]
41
41
  end
42
42
 
@@ -61,7 +61,7 @@ module Kobako
61
61
  # a surface the floor reads as ordinary Service behaviour. Raising here
62
62
  # keeps the rule at the single mint point, so it holds on both the
63
63
  # Service-return and the +#run+ host→guest auto-wrap paths.
64
- def reject_unwrappable!(object)
64
+ def reject_unwrappable(object)
65
65
  case object
66
66
  when Binding, Method, UnboundMethod, Module, Delegator
67
67
  # Delegator < BasicObject exposes no static +#class+, so name the
@@ -71,7 +71,7 @@ module Kobako
71
71
  end
72
72
  end
73
73
 
74
- def ensure_capacity!
74
+ def ensure_capacity
75
75
  cap = Kobako::Handle::MAX_ID
76
76
  return unless @next_id > cap
77
77
 
@@ -80,7 +80,7 @@ module Kobako
80
80
  "in a single invocation (limit #{cap})"
81
81
  end
82
82
 
83
- def require_bound!(id)
83
+ def require_bound(id)
84
84
  return if @entries.key?(id)
85
85
 
86
86
  raise SandboxError, "unknown Handle id: #{id.inspect}"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ module Catalog
5
+ class Mount < Data.define(:at, :access, :filesystem, :policy) # :nodoc:
6
+ # The path a Context replaces this Mount at.
7
+ def key = at
8
+
9
+ # Why +other+ cannot be mounted beside this Mount, or +nil+: a path
10
+ # means one Mount, as Docker refuses a duplicate mount point.
11
+ def conflict(other)
12
+ "path #{at.inspect} is already mounted" if other.at == at
13
+ end
14
+
15
+ # The Mount as the native side reads it.
16
+ def to_native = [at, access, filesystem, policy]
17
+ end
18
+ end
19
+ end
@@ -7,13 +7,20 @@ require_relative "../transport/exposure"
7
7
  module Kobako
8
8
  module Catalog
9
9
  class Services # :nodoc:
10
+ # The Guest Binary keeps this namespace for constants it may add later.
11
+ RESERVED_NAMESPACE = "Kobako"
12
+
10
13
  def initialize
11
14
  @bindings = {} # : Hash[String, Kobako::Transport::Exposure]
12
15
  end
13
16
 
14
17
  # The binding records +object+'s Exposure as it stands now.
15
18
  def bind(path, object)
16
- path_str = validate_path!(path)
19
+ path_str = validate_path(path)
20
+ if reserved?(path_str)
21
+ raise ArgumentError,
22
+ "Service path #{path_str} is in the reserved #{RESERVED_NAMESPACE} namespace"
23
+ end
17
24
  raise ArgumentError, "Service path #{path_str} conflicts with an existing binding" if collision?(path_str)
18
25
 
19
26
  @bindings[path_str] = Kobako::Transport::Exposure.of(object)
@@ -39,7 +46,7 @@ module Kobako
39
46
 
40
47
  private
41
48
 
42
- def validate_path!(path)
49
+ def validate_path(path)
43
50
  path_str = path.to_s
44
51
  segments = path_str.split("::", -1)
45
52
  return path_str if !segments.empty? && segments.all? { |seg| CONSTANT_NAME.match?(seg) }
@@ -48,6 +55,10 @@ module Kobako
48
55
  "bind path must be constant-form segments joined by '::' (got #{path.inspect})"
49
56
  end
50
57
 
58
+ def reserved?(path)
59
+ path.split("::").first == RESERVED_NAMESPACE
60
+ end
61
+
51
62
  # Keeps a name from being both a bound Service and a grouping prefix.
52
63
  def collision?(path)
53
64
  @bindings.each_key.any? do |existing|