kobako 0.27.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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +241 -0
  4. data/Cargo.lock +132 -126
  5. data/README.md +240 -147
  6. data/ROADMAP.md +28 -22
  7. data/SECURITY.md +2 -2
  8. data/crates/kobako-runtime/CHANGELOG.md +117 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +16 -10
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/error.rs +8 -9
  13. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  14. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  15. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  18. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  19. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  20. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  21. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  22. data/crates/kobako-runtime/src/grant.rs +20 -0
  23. data/crates/kobako-runtime/src/lib.rs +12 -2
  24. data/crates/kobako-runtime/src/mount.rs +178 -0
  25. data/crates/kobako-runtime/src/profile.rs +4 -4
  26. data/crates/kobako-runtime/src/runtime.rs +43 -17
  27. data/crates/kobako-runtime/src/snapshot.rs +33 -15
  28. data/crates/kobako-runtime/src/yielder.rs +14 -6
  29. data/crates/kobako-transport/CHANGELOG.md +45 -0
  30. data/crates/kobako-transport/Cargo.toml +1 -1
  31. data/crates/kobako-transport/README.md +21 -12
  32. data/crates/kobako-transport/src/abi.rs +11 -14
  33. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  34. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  35. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  36. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  37. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  38. data/crates/kobako-transport/src/envelope/reply.rs +103 -61
  39. data/crates/kobako-transport/src/envelope/request.rs +305 -0
  40. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  41. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  42. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  43. data/crates/kobako-transport/src/envelope.rs +19 -12
  44. data/crates/kobako-transport/src/lib.rs +0 -2
  45. data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
  46. data/crates/kobako-wasmtime/Cargo.toml +23 -16
  47. data/crates/kobako-wasmtime/README.md +6 -4
  48. data/crates/kobako-wasmtime/src/abi.rs +67 -36
  49. data/crates/kobako-wasmtime/src/ambient.rs +64 -5
  50. data/crates/kobako-wasmtime/src/cache.rs +204 -127
  51. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  52. data/crates/kobako-wasmtime/src/config.rs +69 -15
  53. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  54. data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
  55. data/crates/kobako-wasmtime/src/driver.rs +174 -125
  56. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  57. data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
  58. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  59. data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
  60. data/crates/kobako-wasmtime/src/invocation.rs +84 -412
  61. data/crates/kobako-wasmtime/src/io.rs +89 -0
  62. data/crates/kobako-wasmtime/src/lib.rs +7 -24
  63. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  64. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  65. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  66. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  67. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  68. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  69. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  70. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  71. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  72. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  73. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  74. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  75. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  76. data/crates/kobako-wasmtime/src/trap.rs +66 -59
  77. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  78. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  79. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  80. data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
  81. data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
  82. data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
  83. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  84. data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
  85. data/data/kobako.wasm +0 -0
  86. data/ext/kobako/Cargo.toml +4 -2
  87. data/ext/kobako/src/runtime/bridge.rs +177 -113
  88. data/ext/kobako/src/runtime/errors.rs +26 -32
  89. data/ext/kobako/src/runtime/escape.rs +53 -0
  90. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  91. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  92. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  93. data/ext/kobako/src/runtime/gvl.rs +11 -13
  94. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  95. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  96. data/ext/kobako/src/runtime.rs +190 -312
  97. data/lib/kobako/capture.rb +1 -4
  98. data/lib/kobako/catalog/extensions.rb +21 -59
  99. data/lib/kobako/catalog/flags.rb +29 -0
  100. data/lib/kobako/catalog/grants.rb +30 -0
  101. data/lib/kobako/catalog/handles.rb +7 -54
  102. data/lib/kobako/catalog/mount.rb +27 -0
  103. data/lib/kobako/catalog/services.rb +17 -60
  104. data/lib/kobako/catalog/snippets.rb +41 -69
  105. data/lib/kobako/catalog.rb +13 -10
  106. data/lib/kobako/codec/decoder.rb +10 -28
  107. data/lib/kobako/codec/encoder.rb +3 -22
  108. data/lib/kobako/codec/error.rb +6 -27
  109. data/lib/kobako/codec/ext_types.rb +72 -94
  110. data/lib/kobako/codec/handle_walk.rb +65 -130
  111. data/lib/kobako/codec/nesting.rb +3 -8
  112. data/lib/kobako/codec/state.rb +21 -11
  113. data/lib/kobako/codec/utils.rb +6 -34
  114. data/lib/kobako/codec/value_budget.rb +86 -0
  115. data/lib/kobako/codec.rb +10 -22
  116. data/lib/kobako/constant_name.rb +8 -0
  117. data/lib/kobako/context.rb +83 -117
  118. data/lib/kobako/errors.rb +57 -132
  119. data/lib/kobako/execution.rb +16 -21
  120. data/lib/kobako/extension.rb +23 -40
  121. data/lib/kobako/filesystem.rb +38 -0
  122. data/lib/kobako/handle.rb +7 -12
  123. data/lib/kobako/outcome.rb +51 -80
  124. data/lib/kobako/payload/arguments.rb +1 -16
  125. data/lib/kobako/payload.rb +4 -12
  126. data/lib/kobako/pool.rb +25 -55
  127. data/lib/kobako/runtime.rb +7 -21
  128. data/lib/kobako/sandbox.rb +128 -137
  129. data/lib/kobako/sandbox_options.rb +71 -90
  130. data/lib/kobako/snippet/bytecode.rb +11 -0
  131. data/lib/kobako/snippet/source.rb +1 -16
  132. data/lib/kobako/snippet.rb +2 -10
  133. data/lib/kobako/transport/block_failure.rb +35 -0
  134. data/lib/kobako/transport/call.rb +1 -14
  135. data/lib/kobako/transport/dispatcher.rb +127 -206
  136. data/lib/kobako/transport/error.rb +6 -20
  137. data/lib/kobako/transport/exposure.rb +10 -17
  138. data/lib/kobako/transport/reflection.rb +108 -63
  139. data/lib/kobako/transport/run.rb +17 -71
  140. data/lib/kobako/transport/yielder.rb +20 -94
  141. data/lib/kobako/transport.rb +4 -15
  142. data/lib/kobako/unresolved.rb +4 -11
  143. data/lib/kobako/usage.rb +2 -6
  144. data/lib/kobako/version.rb +1 -1
  145. data/lib/kobako.rb +1 -0
  146. data/sig/kobako/catalog/extensions.rbs +2 -2
  147. data/sig/kobako/catalog/flags.rbs +13 -0
  148. data/sig/kobako/catalog/grants.rbs +13 -0
  149. data/sig/kobako/catalog/mount.rbs +28 -0
  150. data/sig/kobako/catalog/services.rbs +3 -5
  151. data/sig/kobako/catalog/snippets.rbs +5 -5
  152. data/sig/kobako/catalog.rbs +1 -0
  153. data/sig/kobako/codec/ext_types.rbs +10 -8
  154. data/sig/kobako/codec/handle_walk.rbs +9 -7
  155. data/sig/kobako/codec/state.rbs +7 -1
  156. data/sig/kobako/codec/utils.rbs +2 -2
  157. data/sig/kobako/codec/value_budget.rbs +23 -0
  158. data/sig/kobako/codec.rbs +4 -2
  159. data/sig/kobako/constant_name.rbs +3 -0
  160. data/sig/kobako/context.rbs +15 -8
  161. data/sig/kobako/errors.rbs +10 -4
  162. data/sig/kobako/filesystem.rbs +88 -0
  163. data/sig/kobako/guest_yielder.rbs +9 -0
  164. data/sig/kobako/outcome.rbs +6 -8
  165. data/sig/kobako/pool.rbs +1 -1
  166. data/sig/kobako/runtime.rbs +21 -45
  167. data/sig/kobako/sandbox.rbs +11 -1
  168. data/sig/kobako/sandbox_options.rbs +25 -7
  169. data/sig/kobako/snapshot.rbs +20 -0
  170. data/sig/kobako/snippet/bytecode.rbs +12 -0
  171. data/sig/kobako/transport/block_failure.rbs +13 -0
  172. data/sig/kobako/transport/dispatcher.rbs +15 -19
  173. data/sig/kobako/transport/exposure.rbs +1 -1
  174. data/sig/kobako/transport/reflection.rbs +19 -4
  175. data/sig/kobako/transport/run.rbs +1 -3
  176. data/sig/kobako/transport/yielder.rbs +5 -9
  177. data/sig/kobako/transport.rbs +9 -8
  178. metadata +75 -10
  179. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  180. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  181. data/lib/kobako/snippet/binary.rb +0 -26
  182. data/rust-toolchain.toml +0 -15
  183. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -5,128 +5,175 @@
5
5
  //! dispatch seam touches `magnus`. The wasm-side dispatch path
6
6
  //! (`kobako_wasmtime`'s dispatch module) sees only the contract traits.
7
7
 
8
- use core::cell::Cell;
8
+ use core::cell::{Cell, RefCell};
9
9
  use core::ptr::NonNull;
10
+ use std::sync::Arc;
10
11
 
12
+ use magnus::rb_sys::AsRawValue;
11
13
  use magnus::value::{Opaque, ReprValue};
12
- use magnus::{method, prelude::*, Error as MagnusError, RClass, RString, Ruby, Symbol, Value};
14
+ use magnus::{
15
+ method, prelude::*, Error as MagnusError, RModule, RString, Ruby, Symbol, TypedData, Value,
16
+ };
13
17
 
14
18
  use kobako_runtime::dispatch::DispatchHandler;
15
19
  use kobako_runtime::yielder::Yielder;
16
20
  use kobako_transport::envelope::{Call, Fault, FaultKind, Reply, Target, YieldReply};
17
21
 
18
- /// The names the host `Transport::Yielder` branches on — one per Yield
19
- /// Reply arm. Neutral Symbols rather than the envelope's tag bytes, the
20
- /// same way `Snapshot#outcome` names its arms: the core envelope is this
21
- /// side's to read, so no byte value of it reaches Ruby.
22
+ /// The names each side of this seam branches on — one per Reply and
23
+ /// Yield Reply arm. Neutral Symbols rather than the envelope's tag bytes,
24
+ /// the same way `Snapshot#outcome` names its arms: the core envelope is
25
+ /// this side's to read, so no byte value of it reaches Ruby.
22
26
  const ARM_OK: &str = "ok";
27
+ const ARM_FAULT: &str = "fault";
23
28
  const ARM_BREAK: &str = "break";
24
29
  const ARM_ERROR: &str = "error";
30
+ const ARM_UNSUPPORTED: &str = "unsupported";
25
31
 
26
- /// Register the `Kobako::Runtime::GuestYielder` Ruby class. Called from
27
- /// `crate::runtime::init` after `Kobako::Runtime` is defined so the
28
- /// `#[magnus::wrap]` class name resolves before any object is wrapped.
29
- pub(super) fn register(runtime_class: RClass) -> Result<(), MagnusError> {
32
+ /// Defines the class the `#[magnus::wrap]` name resolves to, and looks it
33
+ /// up once, before any object is wrapped.
34
+ pub(super) fn register(kobako: RModule) -> Result<(), MagnusError> {
30
35
  let ruby = Ruby::get().expect("Ruby thread");
31
- let class = runtime_class.define_class("GuestYielder", ruby.class_object())?;
36
+ let class = kobako.define_class("GuestYielder", ruby.class_object())?;
32
37
  class.define_method("call", method!(GuestYielder::call, 1))?;
38
+ GuestYielder::class(&ruby);
33
39
  Ok(())
34
40
  }
35
41
 
36
- /// Frame-scoped Ruby handle that lets the dispatch `Proc` re-enter the
37
- /// guest to run a yielded block. It wraps the active `&mut dyn Yielder`
38
- /// for exactly one `__kobako_dispatch` frame: the bridge builds one, hands
39
- /// it to the `Proc` as the second argument, and `invalidate`s it the
42
+ /// The Yielders one invocation's open Calls lent, innermost last. A yield
43
+ /// re-enters the guest only through the innermost, whatever Call's block
44
+ /// it runs, so no Yielder is reached while a later Call holds the guest.
45
+ ///
46
+ /// This is the one place the host↔guest re-entry still costs raw
47
+ /// pointers: `magnus`' `funcall` sits between two Rust frames, so the
48
+ /// typed `&mut dyn Yielder` cannot cross it and is erased here. Each
49
+ /// pointer is pushed before its frame's `funcall` and popped after it, so
50
+ /// every pointer on the stack belongs to a frame still on the Rust stack.
51
+ #[derive(Default)]
52
+ struct OpenFrames(RefCell<Vec<NonNull<dyn Yielder>>>);
53
+
54
+ // SAFETY: the stack is pushed and popped by the dispatch frames on the
55
+ // fiber driving the invocation, and read only by a `GuestYielder` that
56
+ // refuses every other fiber, so it is never reached from another thread.
57
+ unsafe impl Send for OpenFrames {}
58
+ unsafe impl Sync for OpenFrames {}
59
+
60
+ impl OpenFrames {
61
+ /// Lend `yielder` for the frame `run` answers, popping it however
62
+ /// `run` returns.
63
+ fn lend<T>(&self, yielder: &mut dyn Yielder, run: impl FnOnce() -> T) -> T {
64
+ struct Pop<'a>(&'a OpenFrames);
65
+ impl Drop for Pop<'_> {
66
+ fn drop(&mut self) {
67
+ self.0 .0.borrow_mut().pop();
68
+ }
69
+ }
70
+ // SAFETY: the lifetime is erased only for as long as `Pop` keeps
71
+ // the pointer on the stack, which ends before `yielder` does.
72
+ let ptr: NonNull<dyn Yielder> = unsafe {
73
+ std::mem::transmute::<NonNull<dyn Yielder + '_>, NonNull<dyn Yielder + 'static>>(
74
+ NonNull::from(yielder),
75
+ )
76
+ };
77
+ self.0.borrow_mut().push(ptr);
78
+ let _pop = Pop(self);
79
+ run()
80
+ }
81
+
82
+ fn innermost(&self) -> Option<NonNull<dyn Yielder>> {
83
+ self.0.borrow().last().copied()
84
+ }
85
+ }
86
+
87
+ /// Frame-scoped Ruby handle that lets the dispatch `Proc` run the block
88
+ /// one Call carried. The bridge builds one per `__kobako_upcall` frame,
89
+ /// hands it to the `Proc` as the last argument, and `invalidate`s it the
40
90
  /// instant the `Proc` returns. A guest block stashed and called after that
41
91
  /// frame normally raises `LocalJumpError` at the Ruby
42
92
  /// `Transport::Yielder` net — invalidated in the dispatcher's `ensure`,
43
93
  /// which fires before this handle is reached. This inner invalidation is
44
- /// the backstop behind that outer net: it keeps `call`'s `unsafe`
45
- /// `NonNull` deref from touching freed stack should the outer net ever be
46
- /// bypassed, so neither net is redundant.
94
+ /// the backstop behind that outer net, so neither net is redundant.
47
95
  ///
48
- /// This is the single, explicit, frame-scoped FFI pointer the host↔guest
49
- /// re-entry still costs: `magnus`' `funcall` sits between two Rust frames,
50
- /// so the typed `&mut dyn Yielder` cannot cross it and is erased to a raw
51
- /// pointer here. Unlike the dispatch `Proc`, this handle holds **no Ruby
52
- /// `Value`**, so GC has nothing to trace through it — it needs no `mark`.
53
- #[magnus::wrap(class = "Kobako::Runtime::GuestYielder", free_immediately, size)]
96
+ /// The guest's Store is entered from the fiber that drives the
97
+ /// invocation, and only from it: another fiber runs on its own native
98
+ /// stack, which wasmtime's stack limit and the value budget never saw. A
99
+ /// block run from any other fiber, including one on another thread, is
100
+ /// refused before it reaches the guest.
101
+ ///
102
+ /// It holds **no Ruby `Value`**, so GC has nothing to trace through it —
103
+ /// it needs no `mark`. The driving fiber is kept as its raw identity,
104
+ /// compared only while the frame is live and so while that fiber is.
105
+ #[magnus::wrap(class = "Kobako::GuestYielder", free_immediately, size)]
54
106
  struct GuestYielder {
55
- yielder: Cell<Option<NonNull<dyn Yielder>>>,
107
+ frame: u32,
108
+ open: Arc<OpenFrames>,
109
+ live: Cell<bool>,
110
+ fiber: rb_sys::VALUE,
56
111
  }
57
112
 
58
- // SAFETY: magnus requires `Send + Sync` on wrapped types. The raw pointer
59
- // is created, used, and invalidated within a single `__kobako_dispatch`
60
- // frame on the one Ruby thread that owns the active Invocation (SPEC.md
61
- // Single-Invocation Slot); it is never read from another thread.
113
+ // SAFETY: magnus requires `Send + Sync` on wrapped types. `call` refuses
114
+ // every fiber but the one driving the invocation, so the frames it reaches
115
+ // are only ever read on that fiber's thread.
62
116
  unsafe impl Send for GuestYielder {}
63
117
  unsafe impl Sync for GuestYielder {}
64
118
 
65
119
  impl GuestYielder {
66
- /// Erase the frame-scoped `&mut dyn Yielder` into a Ruby-owned handle.
67
- /// Safety contract for the caller: `invalidate` MUST run before the
68
- /// borrow this pointer came from ends (i.e. before the dispatch frame
69
- /// returns).
70
- fn new(yielder: &mut dyn Yielder) -> Self {
71
- let ptr = NonNull::from(yielder);
72
- // Erase the borrow's lifetime to `'static`; the pointer is only
73
- // ever dereferenced while it is still `Some` (i.e. `invalidate`
74
- // has not run), so the referent is guaranteed live.
75
- let ptr: NonNull<dyn Yielder> = unsafe {
76
- std::mem::transmute::<NonNull<dyn Yielder + '_>, NonNull<dyn Yielder + 'static>>(ptr)
77
- };
120
+ fn new(ruby: &Ruby, frame: u32, open: Arc<OpenFrames>) -> Self {
78
121
  Self {
79
- yielder: Cell::new(Some(ptr)),
122
+ frame,
123
+ open,
124
+ live: Cell::new(true),
125
+ fiber: ruby.fiber_current().as_raw(),
80
126
  }
81
127
  }
82
128
 
83
- /// Mark this handle dead. Called the instant the dispatch frame's
84
- /// `funcall` returns, so a guest block stashed beyond its frame raises
85
- /// instead of dereferencing freed stack.
129
+ /// Runs the instant the dispatch frame's `funcall` returns, so a guest
130
+ /// block stashed beyond its frame raises instead of re-entering.
86
131
  fn invalidate(&self) {
87
- self.yielder.set(None);
132
+ self.live.set(false);
88
133
  }
89
134
 
90
- /// Ruby-visible `call(args_payload) -> [arm, body, class]`: drive one
91
- /// yield round-trip and hand back the reply already split. The arm is
92
- /// named here off the core envelope, so the host `Transport::Yielder`
93
- /// decodes only a payload — and only on the two arms that carry one,
94
- /// where `body` is the block's value; on the error arm it is the
95
- /// message, and `class` names what raised.
96
- /// Raises `Kobako::TrapError` when the handle has been invalidated
97
- /// (escaped guest block), when the re-entry itself traps, or when the
98
- /// guest answers with bytes the envelope cannot frame.
135
+ /// The arm is named here off the decoded Yield Reply, so the host
136
+ /// `Transport::Yielder` decodes only a payload.
99
137
  fn call(&self, args: RString) -> Result<(Symbol, RString, Option<String>), MagnusError> {
100
- let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_yield");
101
- let Some(mut ptr) = self.yielder.get() else {
138
+ let ruby = Ruby::get().expect("Ruby handle unavailable in the block bridge");
139
+ let innermost = self.open.innermost().filter(|_| self.live.get());
140
+ let Some(mut ptr) = innermost else {
102
141
  return Err(super::errors::trap_err(
103
142
  &ruby,
104
143
  "guest block invoked after the host dispatch frame returned",
105
144
  ));
106
145
  };
146
+ if ruby.fiber_current().as_raw() != self.fiber {
147
+ return Err(MagnusError::new(
148
+ ruby.exception_thread_error(),
149
+ "guest block called from a thread or fiber other than its invocation's",
150
+ ));
151
+ }
107
152
  let bytes = super::rstring_to_vec(args);
108
- // SAFETY: `yielder` is `Some`, so `invalidate` has not run — the
109
- // dispatch frame that lent the `&mut dyn Yielder` is still on the
110
- // Rust stack, and the Single-Invocation Slot guarantees no other
111
- // frame aliases it. The borrow ends with this method.
153
+ // SAFETY: this frame is live, so the innermost frame — this one or
154
+ // a later Call — is still on the Rust stack with its Yielder lent.
155
+ // Only the innermost frame's Ruby code runs, so no other borrow of
156
+ // that Yielder is live; the borrow ends with this method.
112
157
  let yielder: &mut dyn Yielder = unsafe { ptr.as_mut() };
113
- let resp = yielder
114
- .yield_to_block(&bytes)
158
+ let reply = yielder
159
+ .yield_to_block(self.frame, &bytes)
115
160
  .map_err(|t| super::errors::trap_to_magnus(&ruby, t))?;
116
- match YieldReply::decode(&resp) {
117
- Ok(YieldReply::Ok(body)) => {
118
- Ok((ruby.to_symbol(ARM_OK), ruby.str_from_slice(&body), None))
119
- }
120
- Ok(YieldReply::Break(body)) => {
121
- Ok((ruby.to_symbol(ARM_BREAK), ruby.str_from_slice(&body), None))
122
- }
123
- Ok(YieldReply::Error(record)) => Ok((
161
+ Ok(match reply {
162
+ YieldReply::Ok(body) => (ruby.to_symbol(ARM_OK), ruby.str_from_slice(&body), None),
163
+ YieldReply::Break => (ruby.to_symbol(ARM_BREAK), ruby.str_from_slice(&[]), None),
164
+ YieldReply::Error(record) => (
124
165
  ruby.to_symbol(ARM_ERROR),
125
166
  ruby.str_new(&record.message),
126
167
  Some(record.name),
127
- )),
128
- Err(err) => Err(super::errors::trap_err(&ruby, err.to_string())),
129
- }
168
+ ),
169
+ // The guest's own message stays behind: the host words this
170
+ // refusal itself.
171
+ YieldReply::Unsupported(_) => (
172
+ ruby.to_symbol(ARM_UNSUPPORTED),
173
+ ruby.str_from_slice(&[]),
174
+ None,
175
+ ),
176
+ })
130
177
  }
131
178
  }
132
179
 
@@ -138,37 +185,35 @@ impl GuestYielder {
138
185
  /// `Opaque` copy of the same handle.
139
186
  pub(super) struct RubyDispatchHandler {
140
187
  on_dispatch: Opaque<Value>,
188
+ open: Arc<OpenFrames>,
141
189
  }
142
190
 
143
191
  impl RubyDispatchHandler {
144
192
  pub(super) fn new(on_dispatch: Opaque<Value>) -> Self {
145
- Self { on_dispatch }
193
+ Self {
194
+ on_dispatch,
195
+ open: Arc::default(),
196
+ }
146
197
  }
147
198
  }
148
199
 
149
200
  impl DispatchHandler for RubyDispatchHandler {
150
- /// Call the Ruby Proc with the routed Call and return its Reply. The
151
- /// envelope is already decoded, so Ruby receives the target, method,
152
- /// and block flag as ordinary values and decodes only the payload —
153
- /// through the MessagePack codec it already owns, which keeps a
154
- /// large payload's strings shared with the buffer rather than copied.
155
- ///
156
- /// The Proc is contracted to fold every dispatch failure into the
157
- /// Reply's fault arm (see `Kobako::Transport::Dispatcher.dispatch`),
158
- /// so a raise is a contract violation surfaced as `None` — the
159
- /// dispatcher then walks the 0-return wire-fault path.
201
+ /// Ruby decodes only the payload, through the codec it already owns,
202
+ /// which keeps a large payload's strings shared with the buffer rather
203
+ /// than copied. The Proc folds every `StandardError` into the Reply's
204
+ /// fault arm, so one escaping is a contract violation surfaced as
205
+ /// `None`.
160
206
  fn dispatch(&self, call: Call<'_>, yielder: &mut dyn Yielder) -> Option<Reply> {
161
207
  // The guest may be running GVL-free (`gvl: :release`); re-acquire the
162
208
  // GVL for the whole callback before touching any Ruby VALUE. In hold
163
209
  // mode `reenter` runs the body inline — the GVL is already held.
164
210
  super::gvl::reenter(|| {
165
211
  // The wasmtime callback runs on the same Ruby thread that called
166
- // the active Sandbox invocation (#eval or #run) — the invariant
167
- // SPEC Implementation Standards Architecture pins for the host gem
168
- // — so `Ruby::get()` is available here once the GVL is held.
212
+ // the active Sandbox invocation (#eval or #run), so `Ruby::get()`
213
+ // is available here once the GVL is held.
169
214
  // Panicking with `expect` localises the violation rather than
170
215
  // letting a nonsense error propagate.
171
- let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_dispatch");
216
+ let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_upcall");
172
217
  let proc_value: Value = ruby.get_inner(self.on_dispatch);
173
218
  // The target crosses in one slot, already discriminated: a
174
219
  // String for a bound constant's path, an Integer for a Handle
@@ -180,34 +225,53 @@ impl DispatchHandler for RubyDispatchHandler {
180
225
  };
181
226
  // Hand the Proc a frame-scoped yielder object as its last arg and
182
227
  // invalidate it the instant the Proc returns, so a guest block that
183
- // escapes the dispatch frame can never deref the freed stack
184
- // pointer. `guest_yielder` holds no Ruby Value, so it needs no GC
185
- // mark — the GC has nothing to trace through it.
186
- let guest_yielder = ruby.obj_wrap(GuestYielder::new(yielder));
187
- let answer: Result<(bool, RString, Option<RString>), magnus::Error> = proc_value
188
- .funcall(
189
- "call",
190
- (
191
- target,
192
- ruby.str_new(call.method),
193
- call.block_given,
194
- ruby.str_from_slice(call.payload),
195
- guest_yielder,
196
- ),
197
- );
228
+ // escapes the dispatch frame never re-enters the guest.
229
+ let guest_yielder =
230
+ ruby.obj_wrap(GuestYielder::new(&ruby, yielder.frame(), self.open.clone()));
231
+ let answer: Result<(Symbol, RString, Option<RString>), magnus::Error> =
232
+ self.open.lend(yielder, || {
233
+ proc_value.funcall(
234
+ "call",
235
+ (
236
+ target,
237
+ ruby.str_new(call.method),
238
+ call.block_given,
239
+ ruby.str_from_slice(call.payload),
240
+ guest_yielder,
241
+ ),
242
+ )
243
+ });
198
244
  guest_yielder.invalidate();
245
+ // The Proc rescues only StandardError, so what else escapes it
246
+ // is the host's own and is held for the host; a StandardError
247
+ // escaping breaks the Proc's contract.
248
+ let (arm, body, kind) = match answer {
249
+ Ok(answer) => answer,
250
+ Err(error) => {
251
+ let _ = super::escape::hold(&ruby, error);
252
+ return None;
253
+ }
254
+ };
199
255
  // The ok arm carries payload bytes the codec owns; the fault
200
256
  // arm carries the Fault's own two fields, which the envelope
201
257
  // types — so Ruby names a category rather than encoding one.
202
- // A category outside the closed set cannot come from the
203
- // public API, so it fails the dispatch rather than degrading.
204
- answer.ok().and_then(|(ok, body, kind)| match ok {
205
- true => Some(Reply::Ok(super::rstring_to_vec(body))),
206
- false => {
207
- let kind = FaultKind::from_name(&kind?.to_string().ok()?)?;
208
- Some(Reply::Fault(Fault::new(kind, body.to_string().ok()?)))
209
- }
210
- })
258
+ // A block exit carries nothing, since nothing of it crosses.
259
+ // An arm or category outside the closed set cannot come from
260
+ // the public API, so it fails the dispatch rather than
261
+ // degrading.
262
+ match arm.name().ok()?.as_ref() {
263
+ ARM_OK => Some(Reply::Ok(super::rstring_to_vec(body))),
264
+ ARM_BREAK => Some(Reply::Break),
265
+ ARM_FAULT => Some(Reply::Fault(Fault::new(
266
+ FaultKind::from_name(&kind?.to_string().ok()?)?,
267
+ body.to_string().ok()?,
268
+ ))),
269
+ _ => None,
270
+ }
211
271
  })
212
272
  }
273
+
274
+ fn abandoned(&self) -> bool {
275
+ super::escape::pending()
276
+ }
213
277
  }
@@ -13,12 +13,8 @@ use magnus::{prelude::*, Error as MagnusError, ExceptionClass, RModule, Ruby};
13
13
 
14
14
  use kobako_runtime::error::{InvokeError, SetupError, Trap};
15
15
 
16
- /// Resolve `Kobako::<name>` as an `ExceptionClass` — the shared body of
17
- /// every error-class `Lazy` below, which differ only in the constant
18
- /// name. The constants are guaranteed present by the time any of these
19
- /// lazies first resolve (`lib/kobako/errors.rb` loads the hierarchy before
20
- /// the ext raises into it), so a missing constant is a build / wiring bug
21
- /// and the `unwrap` is the correct fail-fast.
16
+ /// `lib/kobako/errors.rb` loads the hierarchy before the ext raises into
17
+ /// it, so a missing constant is a wiring bug and the `unwrap` fails fast.
22
18
  fn kobako_error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
23
19
  let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
24
20
  kobako.const_get(name).unwrap()
@@ -40,45 +36,46 @@ static MEMORY_LIMIT_ERROR: Lazy<ExceptionClass> =
40
36
  static SANDBOX_ERROR: Lazy<ExceptionClass> =
41
37
  Lazy::new(|ruby| kobako_error_class(ruby, "SandboxError"));
42
38
 
43
- /// Build a `MagnusError` in `class` carrying `msg` — the shared body of
44
- /// the named `*_err` constructors below, which differ only in which
45
- /// error-class `Lazy` they target.
46
39
  fn error_in(ruby: &Ruby, class: &Lazy<ExceptionClass>, msg: impl Into<String>) -> MagnusError {
47
40
  MagnusError::new(ruby.get_inner(class), msg.into())
48
41
  }
49
42
 
50
- /// Construct a `Kobako::TrapError` magnus error. Used for every
51
- /// invocation-time wasmtime engine failure that is not a configured-cap
52
- /// trap — missing exports, allocation faults, memory write/read failures.
53
- /// Construction-time setup failures use `setup_err`, not this.
43
+ /// For an invocation-time engine failure that is not a configured cap;
44
+ /// construction-time failures use `setup_to_magnus`.
54
45
  pub(super) fn trap_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
55
46
  error_in(ruby, &TRAP_ERROR, msg)
56
47
  }
57
48
 
58
- /// Map a neutral `Trap` onto its `Kobako::TrapError`-family Ruby exception.
59
- /// The boundary between the magnus-free run mechanics and the Ruby surface:
60
- /// the run path classifies a fault into a `Trap`, and this is where it
61
- /// becomes a raised exception. The verb prefix (`Sandbox#eval` / `#run`)
62
- /// is added by `Kobako::Sandbox#invoke!`.
63
- pub(super) fn trap_to_magnus(ruby: &Ruby, trap: Trap) -> MagnusError {
49
+ /// The one place a trap meets the `Kobako::TrapError` family, whether it
50
+ /// stopped the run before the guest started or while it ran.
51
+ pub(super) fn trap_class(ruby: &Ruby, trap: &Trap) -> ExceptionClass {
64
52
  match trap {
65
- Trap::Timeout(msg) => error_in(ruby, &TIMEOUT_ERROR, msg),
66
- Trap::MemoryLimit(msg) => error_in(ruby, &MEMORY_LIMIT_ERROR, msg),
53
+ Trap::Timeout(_) => ruby.get_inner(&TIMEOUT_ERROR),
54
+ Trap::MemoryLimit(_) => ruby.get_inner(&MEMORY_LIMIT_ERROR),
67
55
  // A cap with no named subclass here is still an engine fault, so it
68
56
  // takes the base class rather than borrowing another cap's name.
69
- other => trap_err(ruby, other.to_string()),
57
+ _ => ruby.get_inner(&TRAP_ERROR),
70
58
  }
71
59
  }
72
60
 
73
- /// Map a neutral `SetupError` onto the `Kobako::*` class the SPEC assigns
74
- /// to each runtime state — artifact-absent, runtime-dead, runtime-intact.
61
+ /// The verb prefix is left to `Kobako::Context#invoke!`.
62
+ pub(super) fn trap_to_magnus(ruby: &Ruby, trap: Trap) -> MagnusError {
63
+ MagnusError::new(trap_class(ruby, &trap), trap.to_string())
64
+ }
65
+
75
66
  pub(super) fn setup_to_magnus(ruby: &Ruby, err: SetupError) -> MagnusError {
76
67
  match err {
77
- SetupError::ModuleNotBuilt(msg) => error_in(ruby, &MODULE_NOT_BUILT_ERROR, msg),
68
+ // The engine serves every frontend, so the gem adds its own way to
69
+ // build the artifact.
70
+ SetupError::ModuleNotBuilt(msg) => error_in(
71
+ ruby,
72
+ &MODULE_NOT_BUILT_ERROR,
73
+ format!("{msg}; run `bundle exec rake wasm:build` to build it"),
74
+ ),
78
75
  SetupError::Dead(msg) => error_in(ruby, &SETUP_ERROR, msg),
79
- // Runtime intact means a host-side pre-call fault the SPEC
80
- // attributes to the sandbox / wire layer, not the engine — no
81
- // discard-and-recreate recovery is owed, so never a TrapError.
76
+ // Runtime intact means a host-side pre-call fault attributed to
77
+ // the sandbox / wire layer: the engine never ran, so never a
78
+ // TrapError.
82
79
  SetupError::Intact(msg) => error_in(ruby, &SANDBOX_ERROR, msg),
83
80
  // An unrecognised state is one this frontend cannot promise is
84
81
  // recoverable, so it takes the same class as `Dead`.
@@ -86,14 +83,11 @@ pub(super) fn setup_to_magnus(ruby: &Ruby, err: SetupError) -> MagnusError {
86
83
  }
87
84
  }
88
85
 
89
- /// Map either run-path channel onto its Ruby exception. The single
90
- /// translation point the run-path entry methods funnel their `Result`
91
- /// through.
92
86
  pub(super) fn to_magnus(ruby: &Ruby, err: InvokeError) -> MagnusError {
93
87
  match err {
94
88
  InvokeError::Trap(trap) => trap_to_magnus(ruby, trap),
95
89
  InvokeError::Setup(err) => setup_to_magnus(ruby, err),
96
- // A channel this frontend does not know is still a run that never
90
+ // A failure this frontend does not know is still a run that never
97
91
  // started, which the taxonomy attributes to the engine.
98
92
  other => trap_err(ruby, other.to_string()),
99
93
  }
@@ -0,0 +1,53 @@
1
+ //! What a Host App's callback raises that is not a `StandardError`:
2
+ //! `Interrupt`, `exit`, `throw`, `Timeout`. These belong to the host, so
3
+ //! the callback holds one here instead of answering the guest with it,
4
+ //! the invocation ends at its next host call, and the run raises it again
5
+ //! once control is back in Ruby.
6
+ //!
7
+ //! The slot is per thread, as each invocation runs on its caller's
8
+ //! thread. A nested invocation takes what its own run held before the
9
+ //! outer callback sees it, so the outer one holds it next.
10
+
11
+ use std::cell::RefCell;
12
+
13
+ use magnus::value::BoxValue;
14
+ use magnus::{Error as MagnusError, Ruby, Value};
15
+
16
+ thread_local! {
17
+ static HELD: RefCell<Option<Held>> = const { RefCell::new(None) };
18
+ }
19
+
20
+ /// The error and, when it carries one, its exception kept rooted while
21
+ /// the guest finishes unwinding.
22
+ struct Held {
23
+ error: MagnusError,
24
+ _rooted: Option<BoxValue<Value>>,
25
+ }
26
+
27
+ /// Hold `error` unless it is a `StandardError`, which comes back for the
28
+ /// callback to answer the guest with; the first one held wins.
29
+ pub(super) fn hold(ruby: &Ruby, error: MagnusError) -> Result<(), MagnusError> {
30
+ if error.is_kind_of(ruby.exception_standard_error()) {
31
+ return Err(error);
32
+ }
33
+ HELD.with(|held| {
34
+ held.borrow_mut().get_or_insert_with(|| Held {
35
+ _rooted: error.value().map(BoxValue::new),
36
+ error,
37
+ });
38
+ });
39
+ Ok(())
40
+ }
41
+
42
+ /// Whether a callback on this thread holds an error for the host.
43
+ pub(super) fn pending() -> bool {
44
+ HELD.with(|held| held.borrow().is_some())
45
+ }
46
+
47
+ /// Raise what a callback held, once the run is back in Ruby.
48
+ pub(super) fn resume() -> Result<(), MagnusError> {
49
+ match HELD.with(|held| held.borrow_mut().take()) {
50
+ Some(held) => Err(held.error),
51
+ None => Ok(()),
52
+ }
53
+ }
@@ -0,0 +1,91 @@
1
+ //! A Sandbox's Mounts as contract Grants: refused at `mount` time and
2
+ //! built afresh for every invocation by the same rules, each mount's
3
+ //! block standing in for its access.
4
+
5
+ use std::sync::Arc;
6
+
7
+ use magnus::value::{Opaque, ReprValue};
8
+ use magnus::{prelude::*, Error as MagnusError, RArray, RString, Ruby, Value};
9
+
10
+ use kobako_runtime::filesystem::Access;
11
+ use kobako_runtime::grant::Grant;
12
+ use kobako_runtime::mount::{Mount, Policy};
13
+
14
+ use super::{to_access, to_filesystem};
15
+ use crate::runtime::pinned_value::PinnedValue;
16
+ use crate::runtime::{escape, gvl};
17
+
18
+ /// Refuse a Mount at `mount` time by the same rules an invocation builds
19
+ /// it with, so a mistake raises where it was made.
20
+ pub(in crate::runtime) fn validate_mount(
21
+ at: Value,
22
+ access: Value,
23
+ filesystem: Value,
24
+ ) -> Result<(), MagnusError> {
25
+ to_mount(at, access, filesystem).map(drop)
26
+ }
27
+
28
+ /// The Mount a Sandbox's `[at, access, filesystem]` stands for: one owner
29
+ /// of the path, access, and Filesystem rules.
30
+ fn to_mount(at: Value, access: Value, filesystem: Value) -> Result<Mount, MagnusError> {
31
+ let ruby = Ruby::get().expect("Ruby thread");
32
+ let at = RString::try_convert(at)?.to_string()?;
33
+ let access = to_access(access)?;
34
+ Mount::new(at, to_filesystem(filesystem)?, access)
35
+ .map_err(|err| MagnusError::new(ruby.exception_arg_error(), err.to_string()))
36
+ }
37
+
38
+ /// A mount's block, asked in its access's place. A block raising a
39
+ /// StandardError refuses, so the guest's open fails and its script goes
40
+ /// on; anything else it raises is held for the host.
41
+ struct RubyPolicy {
42
+ block: Opaque<Value>,
43
+ }
44
+
45
+ impl Policy for RubyPolicy {
46
+ fn allows(&self, path: &str, access: Access) -> bool {
47
+ gvl::reenter(|| {
48
+ let ruby = Ruby::get().expect("Ruby handle unavailable in a mount's block");
49
+ let block: Value = ruby.get_inner(self.block);
50
+ let access = ruby.to_symbol(match access {
51
+ Access::Read => "read",
52
+ Access::ReadWrite => "read_write",
53
+ });
54
+ block
55
+ .funcall::<_, _, Value>("call", (ruby.str_new(path), access))
56
+ .map(|answer| answer.to_bool())
57
+ .unwrap_or_else(|error| {
58
+ let _ = escape::hold(&ruby, error);
59
+ false
60
+ })
61
+ })
62
+ }
63
+ }
64
+
65
+ /// The Grants one invocation carries, from the Sandbox's Mounts as
66
+ /// `[at, access, filesystem, block]` entries, beside the pins keeping
67
+ /// each Mount's filesystem and block in place while the guest may reach
68
+ /// them; the caller holds both until the invocation returns.
69
+ pub(in crate::runtime) fn to_grants(
70
+ ruby: &Ruby,
71
+ mounts: RArray,
72
+ ) -> Result<(Vec<Grant>, Vec<PinnedValue>), MagnusError> {
73
+ let mut grants = Vec::with_capacity(mounts.len());
74
+ let mut pins = Vec::with_capacity(mounts.len() * 2);
75
+ for index in 0..mounts.len() as isize {
76
+ let entry: RArray = mounts.entry(index)?;
77
+ let filesystem: Value = entry.entry(2)?;
78
+ pins.push(PinnedValue::new(ruby, filesystem));
79
+ let mount = to_mount(entry.entry(0)?, entry.entry(1)?, filesystem)?;
80
+ let block: Value = entry.entry(3)?;
81
+ grants.push(Grant::Mount(if block.is_nil() {
82
+ mount
83
+ } else {
84
+ pins.push(PinnedValue::new(ruby, block));
85
+ mount.with_policy(Arc::new(RubyPolicy {
86
+ block: Opaque::from(block),
87
+ }))
88
+ }));
89
+ }
90
+ Ok((grants, pins))
91
+ }