kobako 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +170 -0
  4. data/Cargo.lock +111 -82
  5. data/README.md +154 -36
  6. data/ROADMAP.md +5 -0
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +86 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +13 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  13. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  14. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  15. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  18. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  19. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  20. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  21. data/crates/kobako-runtime/src/grant.rs +20 -0
  22. data/crates/kobako-runtime/src/lib.rs +11 -1
  23. data/crates/kobako-runtime/src/mount.rs +178 -0
  24. data/crates/kobako-runtime/src/profile.rs +4 -4
  25. data/crates/kobako-runtime/src/runtime.rs +12 -0
  26. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  27. data/crates/kobako-runtime/src/yielder.rs +11 -6
  28. data/crates/kobako-transport/CHANGELOG.md +17 -0
  29. data/crates/kobako-transport/Cargo.toml +1 -1
  30. data/crates/kobako-transport/README.md +1 -1
  31. data/crates/kobako-transport/src/abi.rs +1 -1
  32. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  33. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  34. data/crates/kobako-transport/src/envelope/reply.rs +32 -14
  35. data/crates/kobako-transport/src/envelope/request.rs +18 -8
  36. data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
  37. data/crates/kobako-wasmtime/Cargo.toml +21 -16
  38. data/crates/kobako-wasmtime/README.md +5 -3
  39. data/crates/kobako-wasmtime/src/abi.rs +66 -32
  40. data/crates/kobako-wasmtime/src/ambient.rs +63 -4
  41. data/crates/kobako-wasmtime/src/cache.rs +185 -74
  42. data/crates/kobako-wasmtime/src/config.rs +58 -1
  43. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  44. data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
  45. data/crates/kobako-wasmtime/src/driver.rs +46 -44
  46. data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
  47. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  48. data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
  49. data/crates/kobako-wasmtime/src/invocation.rs +35 -46
  50. data/crates/kobako-wasmtime/src/io.rs +5 -202
  51. data/crates/kobako-wasmtime/src/lib.rs +4 -0
  52. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  53. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  54. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  55. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  56. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  57. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  58. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  59. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  60. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  61. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  62. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  63. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  64. data/crates/kobako-wasmtime/src/trap.rs +39 -19
  65. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  66. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  67. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  68. data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
  69. data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
  70. data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
  71. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  72. data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
  73. data/data/kobako.wasm +0 -0
  74. data/ext/kobako/Cargo.toml +4 -2
  75. data/ext/kobako/src/runtime/bridge.rs +154 -81
  76. data/ext/kobako/src/runtime/escape.rs +53 -0
  77. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  78. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  79. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  80. data/ext/kobako/src/runtime/gvl.rs +7 -1
  81. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  82. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  83. data/ext/kobako/src/runtime.rs +120 -199
  84. data/lib/kobako/catalog/flags.rb +2 -14
  85. data/lib/kobako/catalog/grants.rb +30 -0
  86. data/lib/kobako/catalog/mount.rb +27 -0
  87. data/lib/kobako/catalog/services.rb +11 -0
  88. data/lib/kobako/catalog/snippets.rb +5 -9
  89. data/lib/kobako/catalog.rb +11 -0
  90. data/lib/kobako/codec/decoder.rb +1 -0
  91. data/lib/kobako/codec/ext_types.rb +1 -1
  92. data/lib/kobako/codec/state.rb +19 -3
  93. data/lib/kobako/codec/value_budget.rb +86 -0
  94. data/lib/kobako/codec.rb +7 -0
  95. data/lib/kobako/context.rb +38 -11
  96. data/lib/kobako/errors.rb +15 -14
  97. data/lib/kobako/filesystem.rb +38 -0
  98. data/lib/kobako/pool.rb +2 -2
  99. data/lib/kobako/sandbox.rb +40 -12
  100. data/lib/kobako/sandbox_options.rb +39 -12
  101. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
  102. data/lib/kobako/snippet.rb +1 -1
  103. data/lib/kobako/transport/block_failure.rb +35 -0
  104. data/lib/kobako/transport/dispatcher.rb +30 -26
  105. data/lib/kobako/transport/exposure.rb +5 -7
  106. data/lib/kobako/transport/reflection.rb +79 -23
  107. data/lib/kobako/transport/yielder.rb +8 -30
  108. data/lib/kobako/transport.rb +1 -0
  109. data/lib/kobako/version.rb +1 -1
  110. data/lib/kobako.rb +1 -0
  111. data/sig/kobako/catalog/flags.rbs +0 -4
  112. data/sig/kobako/catalog/grants.rbs +13 -0
  113. data/sig/kobako/catalog/mount.rbs +28 -0
  114. data/sig/kobako/catalog/services.rbs +4 -0
  115. data/sig/kobako/catalog/snippets.rbs +1 -1
  116. data/sig/kobako/catalog.rbs +1 -0
  117. data/sig/kobako/codec/state.rbs +7 -1
  118. data/sig/kobako/codec/value_budget.rbs +23 -0
  119. data/sig/kobako/codec.rbs +2 -0
  120. data/sig/kobako/context.rbs +14 -6
  121. data/sig/kobako/errors.rbs +4 -4
  122. data/sig/kobako/filesystem.rbs +88 -0
  123. data/sig/kobako/guest_yielder.rbs +9 -0
  124. data/sig/kobako/pool.rbs +1 -1
  125. data/sig/kobako/runtime.rbs +21 -49
  126. data/sig/kobako/sandbox.rbs +7 -1
  127. data/sig/kobako/sandbox_options.rbs +24 -2
  128. data/sig/kobako/snapshot.rbs +20 -0
  129. data/sig/kobako/snippet/bytecode.rbs +12 -0
  130. data/sig/kobako/transport/block_failure.rbs +13 -0
  131. data/sig/kobako/transport/dispatcher.rbs +3 -3
  132. data/sig/kobako/transport/exposure.rbs +1 -1
  133. data/sig/kobako/transport/reflection.rbs +14 -1
  134. data/sig/kobako/transport/yielder.rbs +3 -7
  135. data/sig/kobako/transport.rbs +2 -2
  136. metadata +60 -7
  137. 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()
@@ -112,26 +112,27 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
112
112
  // in `runtime/errors.rs` — no intermediate hierarchy is registered.
113
113
 
114
114
  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))?;
115
+ runtime.define_singleton_method("from_path", function!(Runtime::from_path, 2))?;
116
+ runtime.define_method("eval", method!(Runtime::eval, 3))?;
117
+ runtime.define_method("run", method!(Runtime::run, 4))?;
118
118
  runtime.define_method("profile", method!(Runtime::profile, 0))?;
119
- runtime.define_method("compile", method!(Runtime::compile, 3))?;
119
+ runtime.define_method("compile", method!(Runtime::compile, 1))?;
120
+ runtime.define_singleton_method("validate_mount!", function!(filesystem::validate_mount, 3))?;
120
121
  // 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)?;
122
+ // `Kobako::GuestYielder` the dispatcher hands the Proc, not a method on
123
+ // Runtime.
124
+ bridge::register(kobako)?;
125
+ filesystem::register(ruby, kobako)?;
124
126
 
125
127
  // 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))?;
128
+ snapshot::register(ruby, kobako)?;
129
+
130
+ // A wrapped class is otherwise looked up the first time an object is
131
+ // wrapped, through a Ruby call that a pending interrupt would turn into
132
+ // a panic; a signal arriving during a process's first run waits for
133
+ // exactly that moment.
134
+ Runtime::class(ruby);
135
+ Snapshot::class(ruby);
135
136
 
136
137
  Ok(())
137
138
  }
@@ -157,34 +158,61 @@ impl DataTypeFunctions for Runtime {}
157
158
 
158
159
  impl Runtime {
159
160
  /// 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> {
161
+ /// visible to Ruby. Every option is a required keyword: the defaults
162
+ /// belong to `Kobako::SandboxOptions`, which names the keys.
163
+ fn from_path(path: String, options: RHash) -> Result<Self, MagnusError> {
170
164
  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
- }
165
+ type Options = (
166
+ Option<f64>,
167
+ Option<f64>,
168
+ Option<usize>,
169
+ Option<usize>,
170
+ Option<usize>,
171
+ Option<usize>,
172
+ Symbol,
173
+ Symbol,
174
+ );
175
+ let options = get_kwargs::<_, Options, (), ()>(
176
+ options,
177
+ &[
178
+ "timeout",
179
+ "setup_timeout",
180
+ "memory_limit",
181
+ "stdout_limit",
182
+ "stderr_limit",
183
+ "open_file_limit",
184
+ "profile",
185
+ "gvl",
186
+ ],
187
+ &[],
188
+ )?;
189
+ let (
190
+ timeout_seconds,
191
+ setup_timeout_seconds,
192
+ memory_limit,
193
+ stdout_limit,
194
+ stderr_limit,
195
+ open_file_limit,
196
+ profile,
197
+ gvl,
198
+ ) = options.required;
199
+ // An invalid cap argument is a Host App programming error and
200
+ // raises `ArgumentError`, outside the construction-failure
201
+ // `SetupError` branch. `SandboxOptions` is the primary guard (it
202
+ // never lets a bad timeout reach here); this is defence-in-depth
203
+ // for direct `from_path` calls.
204
+ let to_duration = |seconds: Option<f64>, name: &str| match seconds {
205
+ None => Ok(None),
206
+ Some(secs) if secs.is_finite() && secs > 0.0 => Ok(Some(Duration::from_secs_f64(secs))),
207
+ Some(secs) => Err(MagnusError::new(
208
+ ruby.exception_arg_error(),
209
+ format!("{name} must be > 0 and finite, got {secs} seconds"),
210
+ )),
185
211
  };
212
+ let timeout = to_duration(timeout_seconds, "timeout")?;
213
+ let setup_timeout = to_duration(setup_timeout_seconds, "setup_timeout")?;
186
214
  // 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
215
+ // never fall back to a weaker posture. Same defence in depth as the
188
216
  // timeout guard above — `SandboxOptions` is the primary validator.
189
217
  let profile = match profile.name()?.as_ref() {
190
218
  "hermetic" => Profile::Hermetic,
@@ -210,17 +238,16 @@ impl Runtime {
210
238
  }
211
239
  };
212
240
 
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))?;
241
+ let mut config = Config::default();
242
+ config.timeout = timeout;
243
+ config.setup_timeout = setup_timeout;
244
+ config.memory_limit = memory_limit;
245
+ config.stdout_limit = stdout_limit;
246
+ config.stderr_limit = stderr_limit;
247
+ config.open_file_limit = open_file_limit;
248
+ config.profile = profile;
249
+ let driver =
250
+ Driver::new(Path::new(&path), config).map_err(|e| errors::setup_to_magnus(&ruby, e))?;
224
251
  Ok(Self {
225
252
  driver,
226
253
  release_gvl,
@@ -233,40 +260,32 @@ impl Runtime {
233
260
  // success or trap alike. Only a could-not-start fault (a missing export
234
261
  // or a fault before the export call) raises directly, as the class
235
262
  // `errors::to_magnus` gives its cause, since it yields no `Snapshot`.
263
+ //
264
+ // `setup` is the invocation's `[paths, snippets, flags, mounts]`: the
265
+ // Preamble's three parts, then the Mounts as
266
+ // `filesystem::to_grants` reads them.
236
267
  // -----------------------------------------------------------------
237
268
 
238
269
  fn eval(
239
270
  &self,
240
271
  dispatch: Value,
241
- paths: RArray,
242
- snippets: RArray,
243
- flags: RArray,
272
+ setup: RArray,
244
273
  source: RString,
245
274
  ) -> Result<Snapshot, MagnusError> {
246
275
  let source = rstring_to_vec(source);
247
- self.invoke(
248
- dispatch,
249
- paths,
250
- snippets,
251
- flags,
252
- Entry::Eval { source: &source },
253
- )
276
+ self.invoke(dispatch, setup, Entry::Eval { source: &source })
254
277
  }
255
278
 
256
279
  fn run(
257
280
  &self,
258
281
  dispatch: Value,
259
- paths: RArray,
260
- snippets: RArray,
261
- flags: RArray,
282
+ setup: RArray,
262
283
  entrypoint: String,
263
284
  payload: RString,
264
285
  ) -> Result<Snapshot, MagnusError> {
265
286
  self.invoke(
266
287
  dispatch,
267
- paths,
268
- snippets,
269
- flags,
288
+ setup,
270
289
  Entry::Run {
271
290
  entrypoint: &entrypoint,
272
291
  payload: rstring_to_vec(payload),
@@ -274,14 +293,12 @@ impl Runtime {
274
293
  )
275
294
  }
276
295
 
277
- /// One owner for the wiring both verbs share, so a Preamble or handler
278
- /// change cannot drift between them.
296
+ /// One owner for the wiring both verbs share, so a Preamble, a Grant,
297
+ /// or a handler change cannot drift between them.
279
298
  fn invoke(
280
299
  &self,
281
300
  dispatch: Value,
282
- paths: RArray,
283
- snippets: RArray,
284
- flags: RArray,
301
+ setup: RArray,
285
302
  entry: Entry<'_>,
286
303
  ) -> Result<Snapshot, MagnusError> {
287
304
  let ruby = Ruby::get().expect("Ruby thread");
@@ -290,15 +307,17 @@ impl Runtime {
290
307
  Entry::Run { .. } => "run",
291
308
  };
292
309
  let handler = build_handler(dispatch);
293
- let preamble = preamble_from(&ruby, paths.to_vec()?, snippets, flags)?;
310
+ let preamble = to_preamble(&ruby, setup)?;
311
+ let (grants, _pins) = filesystem::to_grants(&ruby, setup.entry(3)?)?;
294
312
  // Release the GVL around the guest span iff this Sandbox asks for it;
295
313
  // the closure touches no Ruby VALUE (the driver is magnus-free, and a
296
- // guest→host dispatch re-acquires the GVL through the bridge).
314
+ // guest→host dispatch, or a mount's block, re-acquires the GVL).
297
315
  let result = gvl::region(self.release_gvl, || {
298
- self.driver.invoke(entry, &preamble, handler)
316
+ self.driver.invoke(entry, &preamble, handler, &grants)
299
317
  });
318
+ escape::resume()?;
300
319
  result
301
- .map(|snapshot| Snapshot(snapshot, verb))
320
+ .map(|snapshot| Snapshot::new(snapshot, verb))
302
321
  .map_err(|e| errors::to_magnus(&ruby, e))
303
322
  }
304
323
 
@@ -308,14 +327,9 @@ impl Runtime {
308
327
  /// invocation sends, `paths` included, so the driver can tell whether
309
328
  /// the bytecode still fits it; no dispatch handler is bound, since
310
329
  /// compiling runs no script.
311
- fn compile(
312
- &self,
313
- paths: RArray,
314
- snippets: RArray,
315
- flags: RArray,
316
- ) -> Result<Option<RArray>, MagnusError> {
330
+ fn compile(&self, preamble: RArray) -> Result<Option<RArray>, MagnusError> {
317
331
  let ruby = Ruby::get().expect("Ruby thread");
318
- let preamble = preamble_from(&ruby, paths.to_vec()?, snippets, flags)?;
332
+ let preamble = to_preamble(&ruby, preamble)?;
319
333
  let bytecodes = gvl::region(self.release_gvl, || self.driver.compile(&preamble));
320
334
  Ok(bytecodes.map(|bytecodes| {
321
335
  ruby.ary_from_iter(
@@ -349,96 +363,3 @@ fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
349
363
  as Arc<dyn DispatchHandler>,
350
364
  )
351
365
  }
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
- }
@@ -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
@@ -0,0 +1,27 @@
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, and never sits inside another.
11
+ def conflict(other)
12
+ return "path #{at.inspect} is already mounted" if other.at == at
13
+
14
+ "path #{at.inspect} nests with the mounted path #{other.at.inspect}" if nested?(other.at)
15
+ end
16
+
17
+ # The Mount as the native side reads it.
18
+ def to_native = [at, access, filesystem, policy]
19
+
20
+ private
21
+
22
+ def nested?(other)
23
+ [at, other].include?("/") || at.start_with?("#{other}/") || other.start_with?("#{at}/")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -7,6 +7,9 @@ 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
@@ -14,6 +17,10 @@ module Kobako
14
17
  # The binding records +object+'s Exposure as it stands now.
15
18
  def bind(path, object)
16
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)
@@ -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|
@@ -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
@@ -48,13 +48,9 @@ module Kobako
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
51
  def register_source!(code, name)
55
52
  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?
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 }
@@ -80,7 +76,7 @@ module Kobako
80
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,6 +13,7 @@ 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
18
  validate_utf8!(value)
18
19
  block_given? ? yield(value) : value
@@ -63,7 +63,7 @@ module Kobako
63
63
  # shape. The sighting is recorded so a Handle-free decode can skip the
64
64
  # downstream resolution walk.
65
65
  def unpack_handle(payload, state)
66
- state.record_handle!
66
+ state.record_handle
67
67
  bytes = payload.b
68
68
  raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
69
69