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
@@ -1,5 +1,5 @@
1
- //! The bundled guest recognises no flag, so every flag a host sets meets
2
- //! the unrecognised path: a critical one must refuse the invocation under
1
+ //! A flag the bundled guest does not recognise meets the unrecognised
2
+ //! path: a critical one must refuse the invocation under
3
3
  //! its own class, and an advisory one must leave the run untouched.
4
4
 
5
5
  mod support;
@@ -26,6 +26,7 @@ fn eval_with_flag(critical: bool) -> Option<(Outcome, Vec<u8>)> {
26
26
  },
27
27
  &preamble,
28
28
  None,
29
+ &[],
29
30
  )
30
31
  .expect("the invocation must start");
31
32
  let Completion::Outcome(outcome) = snapshot.completion else {
@@ -11,14 +11,14 @@ use kobako_transport::envelope::{Flag, Outcome, Preamble, Snippet, Snippets};
11
11
 
12
12
  use support::bundled_driver as driver;
13
13
 
14
- fn preamble_of(entries: Vec<Snippet>) -> Preamble {
14
+ fn preamble_with(entries: Vec<Snippet>) -> Preamble {
15
15
  let mut preamble = Preamble::default();
16
16
  preamble.snippets = Snippets { entries };
17
17
  preamble
18
18
  }
19
19
 
20
20
  fn greeter() -> Preamble {
21
- preamble_of(vec![Snippet::Source {
21
+ preamble_with(vec![Snippet::Source {
22
22
  name: "Greeter".into(),
23
23
  body: "module Greeter; def self.hi = 'hi'; end".into(),
24
24
  }])
@@ -33,7 +33,7 @@ fn compiled_snippets_replay_in_a_later_invocation() {
33
33
  let bytecodes = driver
34
34
  .compile(&greeter())
35
35
  .expect("a source snippet that parses must compile");
36
- let replay = preamble_of(
36
+ let replay = preamble_with(
37
37
  bytecodes
38
38
  .into_iter()
39
39
  .map(|body| Snippet::Bytecode { body })
@@ -47,6 +47,7 @@ fn compiled_snippets_replay_in_a_later_invocation() {
47
47
  },
48
48
  &replay,
49
49
  None,
50
+ &[],
50
51
  )
51
52
  .expect("the invocation must start");
52
53
 
@@ -18,17 +18,13 @@ pub fn bundled_driver() -> Option<Driver> {
18
18
  );
19
19
  return None;
20
20
  }
21
- Some(
22
- Driver::new(
23
- Path::new(WASM),
24
- Config {
25
- timeout: Some(Duration::from_secs(10)),
26
- memory_limit: None,
27
- stdout_limit: None,
28
- stderr_limit: None,
29
- profile: Profile::Hermetic,
30
- },
31
- )
32
- .expect("the Guest Binary must load"),
33
- )
21
+ let mut config = Config::default();
22
+ config.timeout = Some(Duration::from_secs(10));
23
+ config.setup_timeout = Some(Duration::from_secs(10));
24
+ config.memory_limit = None;
25
+ config.stdout_limit = None;
26
+ config.stderr_limit = None;
27
+ config.open_file_limit = None;
28
+ config.profile = Profile::Hermetic;
29
+ Some(Driver::new(Path::new(WASM), config).expect("the Guest Binary must load"))
34
30
  }
@@ -0,0 +1,86 @@
1
+ //! A Call the host could not answer as the Service meant still reaches the
2
+ //! guest as an answer it can rescue, and the answer says why. The default
3
+ //! frontends never hand the driver either failure, so only a handler
4
+ //! written here reaches them.
5
+
6
+ mod support;
7
+
8
+ use std::sync::Arc;
9
+
10
+ use kobako_runtime::{Completion, DispatchHandler, Entry, Runtime, Yielder};
11
+ use kobako_transport::envelope::{Bindings, Call, Outcome, Preamble, Reply};
12
+
13
+ /// Answers every Call with one byte more than a message carries.
14
+ struct AnswersTooMuch;
15
+
16
+ impl DispatchHandler for AnswersTooMuch {
17
+ fn dispatch(&self, _call: Call<'_>, _yielder: &mut dyn Yielder) -> Option<Reply> {
18
+ Some(Reply::Ok(vec![0xc0; (16 << 20) + 1]))
19
+ }
20
+ }
21
+
22
+ /// Breaks the handler contract by answering nothing.
23
+ struct AnswersNothing;
24
+
25
+ impl DispatchHandler for AnswersNothing {
26
+ fn dispatch(&self, _call: Call<'_>, _yielder: &mut dyn Yielder) -> Option<Reply> {
27
+ None
28
+ }
29
+ }
30
+
31
+ fn eval_against(handler: Arc<dyn DispatchHandler>, source: &[u8]) -> Option<Outcome> {
32
+ let driver = support::bundled_driver()?;
33
+ let mut preamble = Preamble::default();
34
+ preamble.bindings = Bindings {
35
+ paths: vec!["Svc".into()],
36
+ };
37
+ let snapshot = driver
38
+ .invoke(Entry::Eval { source }, &preamble, Some(handler), &[])
39
+ .expect("the invocation must start");
40
+ let Completion::Outcome(outcome) = snapshot.completion else {
41
+ panic!("the invocation must answer rather than trap");
42
+ };
43
+ Some(outcome)
44
+ }
45
+
46
+ // @behavior T-286
47
+ #[test]
48
+ fn an_answer_larger_than_a_message_raises_a_service_failure_naming_the_limit() {
49
+ let source = b"begin; Svc.go; rescue Kobako::ServiceError => e; e.message; end";
50
+ let Some(outcome) = eval_against(Arc::new(AnswersTooMuch), source) else {
51
+ return;
52
+ };
53
+
54
+ let mut expected = vec![0xd9, 0x4e];
55
+ expected.extend_from_slice(
56
+ b"Sandbox could not write the Service's answer: payload exceeds the 16 MiB limit",
57
+ );
58
+ assert_eq!(
59
+ outcome,
60
+ Outcome::Ok(expected),
61
+ "a Service answer past the message cap through Driver#invoke must raise a Service \
62
+ failure in the guest naming the limit"
63
+ );
64
+ }
65
+
66
+ // @behavior T-287
67
+ #[test]
68
+ fn a_call_the_host_could_not_answer_raises_a_transport_error_naming_why() {
69
+ let source = b"begin; Svc.go; rescue Kobako::Transport::Error => e; e.message; end";
70
+ let Some(outcome) = eval_against(Arc::new(AnswersNothing), source) else {
71
+ return;
72
+ };
73
+
74
+ let mut expected = vec![0xd9, 0x6a];
75
+ expected.extend_from_slice(
76
+ "a Sandbox callback raised an exception instead of returning a fault — please report \
77
+ this as a kobako bug"
78
+ .as_bytes(),
79
+ );
80
+ assert_eq!(
81
+ outcome,
82
+ Outcome::Ok(expected),
83
+ "a Call a handler answered with nothing through Driver#invoke must raise \
84
+ Kobako::Transport::Error in the guest naming why"
85
+ );
86
+ }
@@ -35,6 +35,7 @@ fn a_call_the_host_does_not_serve_raises_unsupported_error_in_the_guest() {
35
35
  Entry::Eval { source },
36
36
  &preamble,
37
37
  Some(Arc::new(ServesNoCall)),
38
+ &[],
38
39
  )
39
40
  .expect("the invocation must start");
40
41
 
data/data/kobako.wasm CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "kobako"
3
- version = "0.28.0"
3
+ version = "0.29.0"
4
4
  edition = "2021"
5
5
  authors = ["Aotokitsuruya <contact@aotoki.me>"]
6
6
  license = "Apache-2.0"
@@ -10,7 +10,9 @@ publish = false
10
10
  crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
- magnus = { version = "0.9.2", features = ["rb-sys"] }
13
+ # 0.9.3 keeps `$!` when Rust catches an exception, which a Filesystem or
14
+ # Service callback running while Ruby handles one would otherwise clear.
15
+ magnus = { version = "0.9.3", features = ["rb-sys"] }
14
16
  # Raw Ruby FFI for the GVL release/re-acquire calls magnus does not wrap
15
17
  # (`rb_thread_call_without_gvl` / `rb_thread_call_with_gvl`). Pinned to the
16
18
  # rb-sys magnus resolves so both share one binding instance.
@@ -5,113 +5,162 @@
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";
25
30
  const ARM_UNSUPPORTED: &str = "unsupported";
26
31
 
27
- /// Runs after `Kobako::Runtime` is defined, so the `#[magnus::wrap]` class
28
- /// 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_upcall` 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.
95
+ ///
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.
47
101
  ///
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)]
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_upcall`
60
- // frame on the one Ruby thread that owns the active Invocation; it is
61
- // 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
129
  /// Runs the instant the dispatch frame's `funcall` returns, so a guest
84
- /// block stashed beyond its frame raises instead of dereferencing freed
85
- /// stack.
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
135
  /// The arm is named here off the decoded Yield Reply, so the host
91
136
  /// `Transport::Yielder` decodes only a payload.
92
137
  fn call(&self, args: RString) -> Result<(Symbol, RString, Option<String>), MagnusError> {
93
138
  let ruby = Ruby::get().expect("Ruby handle unavailable in the block bridge");
94
- let Some(mut ptr) = self.yielder.get() else {
139
+ let innermost = self.open.innermost().filter(|_| self.live.get());
140
+ let Some(mut ptr) = innermost else {
95
141
  return Err(super::errors::trap_err(
96
142
  &ruby,
97
143
  "guest block invoked after the host dispatch frame returned",
98
144
  ));
99
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
+ }
100
152
  let bytes = super::rstring_to_vec(args);
101
- // SAFETY: `yielder` is `Some`, so `invalidate` has not run — the
102
- // dispatch frame that lent the `&mut dyn Yielder` is still on the
103
- // Rust stack, and each dispatch frame lends its own Yielder on the
104
- // one thread driving the invocation, so no other frame aliases it.
105
- // 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.
106
157
  let yielder: &mut dyn Yielder = unsafe { ptr.as_mut() };
107
158
  let reply = yielder
108
- .yield_to_block(&bytes)
159
+ .yield_to_block(self.frame, &bytes)
109
160
  .map_err(|t| super::errors::trap_to_magnus(&ruby, t))?;
110
161
  Ok(match reply {
111
162
  YieldReply::Ok(body) => (ruby.to_symbol(ARM_OK), ruby.str_from_slice(&body), None),
112
- YieldReply::Break(body) => {
113
- (ruby.to_symbol(ARM_BREAK), ruby.str_from_slice(&body), None)
114
- }
163
+ YieldReply::Break => (ruby.to_symbol(ARM_BREAK), ruby.str_from_slice(&[]), None),
115
164
  YieldReply::Error(record) => (
116
165
  ruby.to_symbol(ARM_ERROR),
117
166
  ruby.str_new(&record.message),
@@ -136,19 +185,24 @@ impl GuestYielder {
136
185
  /// `Opaque` copy of the same handle.
137
186
  pub(super) struct RubyDispatchHandler {
138
187
  on_dispatch: Opaque<Value>,
188
+ open: Arc<OpenFrames>,
139
189
  }
140
190
 
141
191
  impl RubyDispatchHandler {
142
192
  pub(super) fn new(on_dispatch: Opaque<Value>) -> Self {
143
- Self { on_dispatch }
193
+ Self {
194
+ on_dispatch,
195
+ open: Arc::default(),
196
+ }
144
197
  }
145
198
  }
146
199
 
147
200
  impl DispatchHandler for RubyDispatchHandler {
148
201
  /// Ruby decodes only the payload, through the codec it already owns,
149
202
  /// which keeps a large payload's strings shared with the buffer rather
150
- /// than copied. The Proc folds every dispatch failure into the Reply's
151
- /// fault arm, so a raise is a contract violation surfaced as `None`.
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`.
152
206
  fn dispatch(&self, call: Call<'_>, yielder: &mut dyn Yielder) -> Option<Reply> {
153
207
  // The guest may be running GVL-free (`gvl: :release`); re-acquire the
154
208
  // GVL for the whole callback before touching any Ruby VALUE. In hold
@@ -171,34 +225,53 @@ impl DispatchHandler for RubyDispatchHandler {
171
225
  };
172
226
  // Hand the Proc a frame-scoped yielder object as its last arg and
173
227
  // invalidate it the instant the Proc returns, so a guest block that
174
- // escapes the dispatch frame can never deref the freed stack
175
- // pointer. `guest_yielder` holds no Ruby Value, so it needs no GC
176
- // mark — the GC has nothing to trace through it.
177
- let guest_yielder = ruby.obj_wrap(GuestYielder::new(yielder));
178
- let answer: Result<(bool, RString, Option<RString>), magnus::Error> = proc_value
179
- .funcall(
180
- "call",
181
- (
182
- target,
183
- ruby.str_new(call.method),
184
- call.block_given,
185
- ruby.str_from_slice(call.payload),
186
- guest_yielder,
187
- ),
188
- );
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
+ });
189
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
+ };
190
255
  // The ok arm carries payload bytes the codec owns; the fault
191
256
  // arm carries the Fault's own two fields, which the envelope
192
257
  // types — so Ruby names a category rather than encoding one.
193
- // A category outside the closed set cannot come from the
194
- // public API, so it fails the dispatch rather than degrading.
195
- answer.ok().and_then(|(ok, body, kind)| match ok {
196
- true => Some(Reply::Ok(super::rstring_to_vec(body))),
197
- false => {
198
- let kind = FaultKind::from_name(&kind?.to_string().ok()?)?;
199
- Some(Reply::Fault(Fault::new(kind, body.to_string().ok()?)))
200
- }
201
- })
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
+ }
202
271
  })
203
272
  }
273
+
274
+ fn abandoned(&self) -> bool {
275
+ super::escape::pending()
276
+ }
204
277
  }
@@ -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
+ }