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
@@ -1,4 +1,4 @@
1
- //! Per-`Driver` execution configuration.
1
+ //! The caps a `Driver` applies to every invocation.
2
2
  //!
3
3
  //! Every cap a frontend forwards into `Driver::new`. A plain value carrier
4
4
  //! owned by the `Driver` — distinct from the process-wide engine/module
@@ -17,24 +17,78 @@ use kobako_runtime::profile::Profile;
17
17
 
18
18
  /// The caps and requested isolation profile for one `Driver`. `None` on
19
19
  /// any cap field disables that cap.
20
+ #[non_exhaustive]
20
21
  pub struct Config {
21
- /// Wall-clock cap for one guest `#eval` / `#run`. Stamped into a
22
- /// per-run `Instant` deadline by `Driver::prime_caps`.
22
+ /// Wall-clock cap for one guest `#eval` / `#run`; the invocation traps
23
+ /// once it passes.
23
24
  pub timeout: Option<Duration>,
24
- /// Guest linear-memory cap, in bytes. Threaded into each fresh
25
- /// `Invocation`, where the wasmtime `ResourceLimiter` callback
26
- /// consumes it from inside the engine.
25
+ /// Wall-clock cap for setting the Guest Binary up the first time its
26
+ /// path is loaded; past it, construction fails.
27
+ pub setup_timeout: Option<Duration>,
28
+ /// Guest linear-memory cap, in bytes, charged to what one invocation
29
+ /// grows beyond the image it starts from.
27
30
  pub memory_limit: Option<usize>,
28
- /// Byte cap for guest stdout capture.
29
- /// Sizes the per-run `MemoryOutputPipe` and computes the truncation
30
- /// flag in `Driver::build_snapshot`.
31
+ /// Byte cap for guest stdout capture; output past it is dropped and
32
+ /// the capture marked truncated.
31
33
  pub stdout_limit: Option<usize>,
32
- /// Byte cap for guest stderr capture. Mirror of `stdout_limit`.
34
+ /// Byte cap for guest stderr capture, the same as `stdout_limit`.
33
35
  pub stderr_limit: Option<usize>,
34
- /// Isolation posture the frontend requested. The per-invocation
35
- /// WASI context is built to this rung — `Hermetic` freezes ambient
36
- /// time and entropy (`crate::ambient`), `Permissive` leaves the
37
- /// live WASI sources — and `Driver::profile` declares it back as
38
- /// the built posture.
36
+ /// Descriptors one invocation may hold open, the standard ones and
37
+ /// each Mount's preopen included; an open past it answers `MFILE`.
38
+ pub open_file_limit: Option<usize>,
39
+ /// Isolation posture the frontend requested. `Hermetic` freezes
40
+ /// ambient time and entropy, `Permissive` leaves the live WASI
41
+ /// sources, and `Driver::profile` declares it back as the built
42
+ /// posture.
39
43
  pub profile: Profile,
40
44
  }
45
+
46
+ impl Default for Config {
47
+ /// The caps every frontend defaults to: a 60-second deadline, 10
48
+ /// seconds to set the Guest Binary up, 4 MiB for memory, 1 MiB for
49
+ /// each output channel, 1024 open files, and the hermetic floor.
50
+ fn default() -> Self {
51
+ Config {
52
+ timeout: Some(Duration::from_secs(60)),
53
+ setup_timeout: Some(Duration::from_secs(10)),
54
+ memory_limit: Some(4 << 20),
55
+ stdout_limit: Some(1 << 20),
56
+ stderr_limit: Some(1 << 20),
57
+ open_file_limit: Some(1024),
58
+ profile: Profile::Hermetic,
59
+ }
60
+ }
61
+ }
62
+
63
+ #[cfg(test)]
64
+ mod tests {
65
+ use super::*;
66
+
67
+ // Literal values, so a drift from the defaults the SDK's Options and
68
+ // the Ruby frontend apply fails here.
69
+ #[test]
70
+ fn a_default_config_carries_every_frontends_default_caps() {
71
+ let config = Config::default();
72
+ assert_eq!(
73
+ (
74
+ config.timeout,
75
+ config.setup_timeout,
76
+ config.memory_limit,
77
+ config.stdout_limit,
78
+ config.stderr_limit,
79
+ config.open_file_limit,
80
+ config.profile
81
+ ),
82
+ (
83
+ Some(Duration::from_secs(60)),
84
+ Some(Duration::from_secs(10)),
85
+ Some(4 * 1024 * 1024),
86
+ Some(1024 * 1024),
87
+ Some(1024 * 1024),
88
+ Some(1024),
89
+ Profile::Hermetic
90
+ ),
91
+ "Config::default must carry the caps every frontend defaults to"
92
+ );
93
+ }
94
+ }
@@ -0,0 +1,167 @@
1
+ //! The guest's descriptor table: what each preview1 file descriptor
2
+ //! stands for within one invocation.
3
+ //!
4
+ //! kobako answers every preview1 call that reads this table, so the guest
5
+ //! sees one table and no call falls through to a second one that
6
+ //! disagrees. Numbering follows wasmtime-wasi's: a closed number is reused
7
+ //! before a fresh one is taken.
8
+
9
+ use std::collections::{BTreeMap, BTreeSet};
10
+ use std::sync::Arc;
11
+
12
+ use kobako_runtime::filesystem::{DirEntry, File, Hold, Metadata};
13
+ use kobako_runtime::mount::Mount;
14
+ use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
15
+
16
+ /// What one descriptor stands for.
17
+ pub(crate) enum Descriptor {
18
+ /// Descriptor 0, carrying the Preamble.
19
+ Stdin(MemoryInputPipe),
20
+ /// Descriptor 1 or 2, a capture pipe the guest writes a Channel into.
21
+ Channel(MemoryOutputPipe),
22
+ /// A directory beneath a Mount; a preopen is the Mount's own root.
23
+ Directory(OpenDirectory),
24
+ File(OpenFile),
25
+ }
26
+
27
+ /// What a directory is, apart from where it stands: the device and inode
28
+ /// numbers its Filesystem names it by.
29
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
30
+ pub(crate) struct Identity {
31
+ dev: u64,
32
+ ino: u64,
33
+ }
34
+
35
+ impl Identity {
36
+ /// The identity `metadata` names, or `None` where its inode of zero
37
+ /// reports none.
38
+ pub(crate) fn of(metadata: &Metadata) -> Option<Self> {
39
+ (metadata.ino != 0).then_some(Self {
40
+ dev: metadata.dev,
41
+ ino: metadata.ino,
42
+ })
43
+ }
44
+ }
45
+
46
+ pub(crate) struct OpenDirectory {
47
+ pub(crate) mount: Arc<Mount>,
48
+ /// The directory's path relative to the mount's root.
49
+ pub(crate) path: String,
50
+ /// The identity the directory had when it was opened, so one made in
51
+ /// its place is not taken for it; without one it follows its path.
52
+ pub(crate) identity: Option<Identity>,
53
+ /// What keeps that identity from passing to a directory made in this
54
+ /// one's place while the descriptor is open.
55
+ pub(crate) _hold: Option<Hold>,
56
+ pub(crate) preopen: bool,
57
+ /// The entries a listing began with, held until it reaches its end, so
58
+ /// each later call reads on without listing the directory again.
59
+ pub(crate) listing: Option<Vec<DirEntry>>,
60
+ }
61
+
62
+ pub(crate) struct OpenFile {
63
+ pub(crate) file: Box<dyn File>,
64
+ pub(crate) position: u64,
65
+ pub(crate) append: bool,
66
+ pub(crate) readable: bool,
67
+ pub(crate) writable: bool,
68
+ /// The Mount it was opened beneath, whose permission to write answers a
69
+ /// wait to write and a resize, as wasmtime-wasi answers both by its
70
+ /// preopen's permissions.
71
+ pub(crate) mount: Arc<Mount>,
72
+ }
73
+
74
+ pub(crate) struct Descriptors {
75
+ used: BTreeMap<u32, Descriptor>,
76
+ free: BTreeSet<u32>,
77
+ limit: Option<usize>,
78
+ }
79
+
80
+ impl Descriptors {
81
+ /// The table every invocation starts from: stdin, then the two
82
+ /// Channels, then one preopen per Mount from descriptor 3. It holds at
83
+ /// most `limit` descriptors, those included, as a process's
84
+ /// `RLIMIT_NOFILE` counts every one it holds.
85
+ pub(crate) fn new(
86
+ stdin: MemoryInputPipe,
87
+ stdout: MemoryOutputPipe,
88
+ stderr: MemoryOutputPipe,
89
+ mounts: Vec<Mount>,
90
+ limit: Option<usize>,
91
+ ) -> Self {
92
+ let mut used = BTreeMap::from([
93
+ (0, Descriptor::Stdin(stdin)),
94
+ (1, Descriptor::Channel(stdout)),
95
+ (2, Descriptor::Channel(stderr)),
96
+ ]);
97
+ for (fd, mount) in (3..).zip(mounts) {
98
+ // A root the Filesystem cannot stat or hold reports no
99
+ // identity, and the descriptor follows its path alone.
100
+ let filesystem = mount.filesystem();
101
+ let identity = filesystem
102
+ .metadata("")
103
+ .ok()
104
+ .and_then(|root| Identity::of(&root));
105
+ let hold = filesystem.hold_directory("").ok().flatten();
106
+ used.insert(
107
+ fd,
108
+ Descriptor::Directory(OpenDirectory {
109
+ mount: Arc::new(mount),
110
+ path: String::new(),
111
+ identity,
112
+ _hold: hold,
113
+ preopen: true,
114
+ listing: None,
115
+ }),
116
+ );
117
+ }
118
+ Self {
119
+ used,
120
+ free: BTreeSet::new(),
121
+ limit,
122
+ }
123
+ }
124
+
125
+ pub(crate) fn get(&self, fd: u32) -> Option<&Descriptor> {
126
+ self.used.get(&fd)
127
+ }
128
+
129
+ pub(crate) fn get_mut(&mut self, fd: u32) -> Option<&mut Descriptor> {
130
+ self.used.get_mut(&fd)
131
+ }
132
+
133
+ /// Take `fd` out of the table, freeing its number for reuse.
134
+ pub(crate) fn remove(&mut self, fd: u32) -> Option<Descriptor> {
135
+ let descriptor = self.used.remove(&fd)?;
136
+ self.free.insert(fd);
137
+ Some(descriptor)
138
+ }
139
+
140
+ /// Place `descriptor` at `fd`, a number the caller has just freed.
141
+ pub(crate) fn place(&mut self, fd: u32, descriptor: Descriptor) {
142
+ self.free.remove(&fd);
143
+ self.used.insert(fd, descriptor);
144
+ }
145
+
146
+ /// Whether the table holds as many descriptors as it may.
147
+ pub(crate) fn is_full(&self) -> bool {
148
+ self.limit.is_some_and(|limit| self.used.len() >= limit)
149
+ }
150
+
151
+ /// Add `descriptor` under the highest freed number, or the one past
152
+ /// the highest in use; `None` when the table is full.
153
+ pub(crate) fn push(&mut self, descriptor: Descriptor) -> Option<u32> {
154
+ if self.is_full() {
155
+ return None;
156
+ }
157
+ let fd = match self.free.pop_last() {
158
+ Some(fd) => fd,
159
+ None => match self.used.last_key_value() {
160
+ Some((last, _)) => last.checked_add(1)?,
161
+ None => 0,
162
+ },
163
+ };
164
+ self.used.insert(fd, descriptor);
165
+ Some(fd)
166
+ }
167
+ }
@@ -1,89 +1,99 @@
1
- //! Host-side dispatch for the `__kobako_dispatch` import.
1
+ //! Host side of the `__kobako_upcall` import: hand the guest's Call to
2
+ //! the bound `DispatchHandler` and write its Reply back into guest memory.
3
+ //! A request under a tag the host does not serve is answered as
4
+ //! unsupported, so a guest asking for a later operation can fall back.
2
5
  //!
3
- //! When the guest invokes the wasm import declared in
4
- //! `wasm/kobako-core/src/abi.rs`, wasmtime calls back into the host
5
- //! through the closure registered by `instance_pre::build_linker`.
6
- //! That closure delegates here. The dispatcher:
7
- //!
8
- //! 1. Reads the Call bytes from guest linear memory.
9
- //! 2. Invokes the bound `DispatchHandler` (the frontend's dispatch
10
- //! bridge, e.g. a Ruby Proc) and recovers Reply bytes.
11
- //! 3. Allocates a guest buffer via `__kobako_alloc(len)` invoked
12
- //! through `Caller::get_export`.
13
- //! 4. Writes the Reply bytes into the guest buffer.
14
- //! 5. Returns packed `(ptr<<32)|len` for the guest to decode.
15
- //!
16
- //! Returns 0 on any step failure. `Kobako::Sandbox#initialize` always
17
- //! installs the dispatch handler before any invocation, so reaching the
18
- //! dispatcher with no handler bound is itself a wire-layer fault; the
19
- //! guest maps a 0 return to a trap. Failures during normal dispatch
20
- //! take the Reply's fault arm from
21
- //! `Kobako::Transport::Dispatcher.dispatch` itself — they never reach
22
- //! this 0-return path.
23
- //!
24
- //! ## Why this module writes to `stderr`
25
- //!
26
- //! This file is the one place in the driver that deliberately prints
27
- //! through `eprintln!`. The host normally surfaces faults through the
28
- //! contract's error channels; the dispatcher contract is the exception
29
- //! — it must return a packed `i64` to the guest and cannot fail, so a
30
- //! 0 return is the only signal the wasm side receives. The guest collapses every 0 into the same trap, so the
31
- //! Ruby host has no way to attribute the failure to a specific step
32
- //! (missing `memory` export vs. no dispatch handler bound vs. the
33
- //! handler raised vs. `__kobako_alloc` returned 0 vs. `memory.write`
34
- //! rejected).
35
- //!
36
- //! `handle` writes a single `[kobako-dispatch] <reason>` line to
37
- //! `stderr` on each failure path so operators have a breadcrumb to
38
- //! correlate the trap with the actual cause. The line is emitted in
39
- //! both debug and release builds on purpose: dispatcher failures are
40
- //! wire-layer faults rather than expected error paths (`Kobako::Sandbox`
41
- //! always installs the handler, the handler is contracted never to
42
- //! raise, etc.), so the "release-build noise" cost is bounded — under
43
- //! normal operation the line is never written. Operators that need to
44
- //! silence the stream can redirect the host process's stderr, but the
45
- //! kobako convention is "ext never logs" plus this single, named
46
- //! exception.
6
+ //! A failure lands on whoever is answerable for it, and always says why.
7
+ //! A trap the guest raises during a callback into it ends the invocation
8
+ //! as that trap. A Call the host could not answer is answered as an
9
+ //! internal failure naming why, and an answer too large for a message as
10
+ //! the Service's own failure. Only an answer that cannot be written into
11
+ //! guest memory at all ends the invocation, as a trap naming why.
47
12
 
48
13
  use wasmtime::Caller;
49
14
 
50
- use kobako_transport::envelope::Call;
15
+ use kobako_runtime::error::Trap;
16
+ use kobako_runtime::yielder::Yielder;
17
+ use kobako_transport::abi::pack_ptr_len;
18
+ use kobako_transport::envelope::{Fault, FaultKind, Reply, Unsupported, Upcall, YieldReply};
51
19
 
20
+ use crate::guest_mem;
52
21
  use crate::invocation::Invocation;
53
22
 
54
- /// Drive a single `__kobako_dispatch` invocation end-to-end. Entry point
55
- /// from the wasmtime closure registered by `instance_pre::build_linker`.
56
- ///
57
- /// Returns the packed `(ptr<<32)|len` u64 on success, 0 on any
58
- /// wire-layer fault. Failure paths log a `[kobako-dispatch]` line to
59
- /// `stderr` so operators have a breadcrumb when the guest sees a 0
60
- /// return and traps. The bound dispatch handler is contracted never to
61
- /// raise (it folds Service exceptions onto the Reply's fault arm),
62
- /// so reaching the failure path is always a wiring bug or wire-layer
63
- /// fault rather than an expected path.
64
- pub(crate) fn handle(caller: &mut Caller<'_, Invocation>, req_ptr: i32, req_len: i32) -> i64 {
65
- match try_handle(caller, req_ptr, req_len) {
66
- Ok(packed) => packed,
67
- Err(reason) => {
68
- eprintln!("[kobako-dispatch] {reason}");
69
- 0
70
- }
23
+ /// The wasmtime-backed `Yielder`: built per `__kobako_upcall` frame over
24
+ /// that frame's `Caller`, so nested dispatch frames each carry their own and
25
+ /// stack on the Rust call stack with no shared slot.
26
+ struct CallerYielder<'a, 'c> {
27
+ caller: &'a mut Caller<'c, Invocation>,
28
+ frame: u32,
29
+ }
30
+
31
+ impl Yielder for CallerYielder<'_, '_> {
32
+ fn frame(&self) -> u32 {
33
+ self.frame
34
+ }
35
+
36
+ fn yield_to_block(&mut self, frame: u32, args: &[u8]) -> Result<YieldReply, Trap> {
37
+ let bytes = guest_mem::drive_yield(self.caller, frame, args)
38
+ .map_err(|msg| Trap::Other(msg.to_string()))?;
39
+ YieldReply::decode(&bytes).map_err(|err| Trap::Other(err.to_string()))
40
+ }
41
+ }
42
+
43
+ /// Answer one `__kobako_upcall` call with the packed `(ptr<<32)|len` of
44
+ /// the Reply, or the trap a callback into the guest raised.
45
+ pub(crate) fn handle(
46
+ caller: &mut Caller<'_, Invocation>,
47
+ req_ptr: i32,
48
+ req_len: i32,
49
+ ) -> wasmtime::Result<i64> {
50
+ let reply = reply_to(caller, req_ptr, req_len)
51
+ .unwrap_or_else(|reason| Reply::Fault(Fault::new(FaultKind::Internal, reason)));
52
+ let written = write_reply(caller, &fit_in_message(reply).encode());
53
+ if let Some(trap) = caller.data_mut().take_reentry_trap() {
54
+ return Err(trap);
55
+ }
56
+ if let Some(trap) = caller.data().abandonment() {
57
+ return Err(trap);
58
+ }
59
+ written.map_err(wasmtime::Error::msg)
60
+ }
61
+
62
+ /// An answer past the message cap is the Service's to change, as any other
63
+ /// answer the host cannot write is.
64
+ fn fit_in_message(reply: Reply) -> Reply {
65
+ let bytes = reply.encode().len();
66
+ match guest_mem::checked_payload_len(bytes) {
67
+ Ok(_) => reply,
68
+ Err(limit) => Reply::Fault(Fault::new(
69
+ FaultKind::Runtime,
70
+ format!("Sandbox could not write the Service's answer: {limit}"),
71
+ )),
71
72
  }
72
73
  }
73
74
 
74
- /// Result-returning core of `handle`. Pulled out so each early
75
- /// failure path carries a diagnostic string instead of an opaque 0.
76
- fn try_handle(
75
+ fn reply_to(
77
76
  caller: &mut Caller<'_, Invocation>,
78
77
  req_ptr: i32,
79
78
  req_len: i32,
80
- ) -> Result<i64, &'static str> {
81
- let req_bytes = crate::guest_mem::read(caller, req_ptr, req_len)?;
82
- // The driver decodes the core envelope so the frontend never sees a
83
- // frame; the payload inside it stays bytes the whole way through.
84
- let call = Call::decode(&req_bytes).map_err(|_| {
85
- "the guest sent a malformed Call envelope — please report this as a kobako bug"
86
- })?;
79
+ ) -> Result<Reply, &'static str> {
80
+ let req_bytes = guest_mem::read_request(caller, req_ptr, req_len)?;
81
+ // The driver decodes the core envelope, here and in `CallerYielder`, so
82
+ // the frontend never sees a frame; the payload inside it stays bytes
83
+ // the whole way through.
84
+ let call = match Upcall::decode(&req_bytes) {
85
+ Ok(Upcall::Call(call)) => call,
86
+ Ok(_) => {
87
+ return Ok(Reply::Unsupported(Unsupported::new(
88
+ "the host does not serve this request",
89
+ )))
90
+ }
91
+ Err(_) => {
92
+ return Err(
93
+ "the guest sent a malformed Call envelope — please report this as a kobako bug",
94
+ )
95
+ }
96
+ };
87
97
 
88
98
  // `Kobako::Sandbox` always installs the dispatch handler before
89
99
  // invoking the runtime, so reaching this branch indicates a misuse
@@ -91,27 +101,25 @@ fn try_handle(
91
101
  let handler = caller
92
102
  .data()
93
103
  .on_dispatch()
94
- .ok_or("a Sandbox callback fired outside an active Sandbox#run — please report this as a kobako bug")?;
104
+ .ok_or("a Sandbox callback fired outside an active invocation — please report this as a kobako bug")?;
95
105
 
96
106
  // Build a frame-scoped yielder over this Caller and hand it to the
97
107
  // handler. The borrow ends with the block, freeing the Caller for
98
- // `write_response`; nested dispatch frames each build their own, so
108
+ // `write_reply`; nested dispatch frames each build their own, so
99
109
  // the LIFO re-entry lives on the Rust stack — no shared slot.
110
+ let frame = caller.data_mut().open_call();
100
111
  let reply = {
101
- let mut yielder = crate::guest_mem::CallerYielder::new(caller);
112
+ let mut yielder = CallerYielder { caller, frame };
102
113
  handler.dispatch(call, &mut yielder)
103
- }
104
- .ok_or(
114
+ };
115
+ caller.data_mut().close_call();
116
+ reply.ok_or(
105
117
  "a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
106
- )?;
107
-
108
- write_response(caller, &reply.encode())
118
+ )
109
119
  }
110
120
 
111
- /// Allocate a guest-side buffer and copy the response bytes into it via
112
- /// `crate::guest_mem::alloc_and_write`, returning the packed
113
- /// `(ptr<<32)|len` u64 the guest's `__kobako_dispatch` import expects.
114
- fn write_response(caller: &mut Caller<'_, Invocation>, bytes: &[u8]) -> Result<i64, &'static str> {
115
- let ptr = crate::guest_mem::alloc_and_write(caller, bytes)?;
116
- Ok(((ptr as i64) << 32) | (bytes.len() as i64))
121
+ fn write_reply(caller: &mut Caller<'_, Invocation>, bytes: &[u8]) -> Result<i64, &'static str> {
122
+ let ptr = guest_mem::write_from_callback(caller, bytes)?;
123
+ // The write has already held the length to the payload cap.
124
+ Ok(pack_ptr_len(ptr, bytes.len() as u32) as i64)
117
125
  }