kobako 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +217 -10
  4. data/Cargo.lock +139 -141
  5. data/README.md +96 -574
  6. data/ROADMAP.md +2 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +269 -1
  8. data/crates/kobako-runtime/Cargo.toml +25 -15
  9. data/crates/kobako-runtime/README.md +7 -3
  10. data/crates/kobako-runtime/src/dispatch.rs +1 -1
  11. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  12. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  13. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  14. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  15. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  16. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  17. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  18. data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
  19. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  20. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  21. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
  29. data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
  30. data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
  31. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  32. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  33. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  37. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  38. data/crates/kobako-runtime/src/filesystem.rs +21 -387
  39. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  40. data/crates/kobako-runtime/src/lib.rs +11 -6
  41. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  42. data/crates/kobako-runtime/src/locking.rs +218 -0
  43. data/crates/kobako-runtime/src/mount.rs +31 -104
  44. data/crates/kobako-runtime/src/profile.rs +137 -1
  45. data/crates/kobako-runtime/src/sched.rs +210 -0
  46. data/crates/kobako-runtime/src/security.rs +696 -0
  47. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  48. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  49. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  50. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  51. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  52. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  53. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  54. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  55. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  56. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  57. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  58. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  59. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  60. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  61. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  62. data/crates/kobako-runtime/src/vfs.rs +60 -0
  63. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  64. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  65. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  66. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  67. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  68. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  70. data/crates/kobako-transport/CHANGELOG.md +8 -2
  71. data/crates/kobako-transport/Cargo.toml +3 -3
  72. data/crates/kobako-transport/README.md +6 -5
  73. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  74. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  75. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  76. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  77. data/crates/kobako-transport/src/envelope/fault.rs +2 -2
  78. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  79. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  80. data/crates/kobako-transport/src/envelope/reply.rs +2 -2
  81. data/crates/kobako-transport/src/envelope/request.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  83. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  84. data/crates/kobako-transport/src/envelope.rs +2 -2
  85. data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
  86. data/crates/kobako-wasmtime/Cargo.toml +15 -14
  87. data/crates/kobako-wasmtime/README.md +9 -5
  88. data/crates/kobako-wasmtime/src/abi.rs +12 -20
  89. data/crates/kobako-wasmtime/src/ambient.rs +1 -2
  90. data/crates/kobako-wasmtime/src/cache.rs +5 -5
  91. data/crates/kobako-wasmtime/src/config.rs +22 -7
  92. data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
  93. data/crates/kobako-wasmtime/src/driver.rs +29 -96
  94. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  95. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  96. data/crates/kobako-wasmtime/src/imports.rs +14 -8
  97. data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
  98. data/crates/kobako-wasmtime/src/invocation.rs +26 -46
  99. data/crates/kobako-wasmtime/src/lib.rs +1 -4
  100. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  101. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
  102. data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
  103. data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
  104. data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
  105. data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
  106. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  107. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
  108. data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
  109. data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
  110. data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
  111. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  112. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  113. data/crates/kobako-wasmtime/src/preview1.rs +464 -413
  114. data/crates/kobako-wasmtime/src/trap.rs +9 -9
  115. data/crates/kobako-wasmtime/src/wasi.rs +53 -37
  116. data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
  117. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  118. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
  119. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
  120. data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
  121. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
  122. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  123. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  124. data/data/kobako.wasm +0 -0
  125. data/ext/kobako/Cargo.toml +1 -1
  126. data/ext/kobako/src/runtime/bridge.rs +1 -1
  127. data/ext/kobako/src/runtime/errors.rs +1 -1
  128. data/ext/kobako/src/runtime/escape.rs +28 -14
  129. data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
  130. data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
  131. data/ext/kobako/src/runtime/filesystem.rs +109 -137
  132. data/ext/kobako/src/runtime/gvl.rs +43 -8
  133. data/ext/kobako/src/runtime/snapshot.rs +1 -1
  134. data/ext/kobako/src/runtime.rs +6 -1
  135. data/lib/kobako/catalog/extensions.rb +5 -5
  136. data/lib/kobako/catalog/flags.rb +2 -2
  137. data/lib/kobako/catalog/handles.rb +6 -6
  138. data/lib/kobako/catalog/mount.rb +2 -10
  139. data/lib/kobako/catalog/services.rb +2 -2
  140. data/lib/kobako/catalog/snippets.rb +7 -7
  141. data/lib/kobako/catalog.rb +1 -1
  142. data/lib/kobako/codec/decoder.rb +10 -10
  143. data/lib/kobako/codec/ext_types.rb +3 -5
  144. data/lib/kobako/codec/handle_walk.rb +4 -6
  145. data/lib/kobako/codec/nesting.rb +7 -7
  146. data/lib/kobako/codec/utils.rb +1 -1
  147. data/lib/kobako/codec/value_budget.rb +3 -3
  148. data/lib/kobako/context.rb +5 -5
  149. data/lib/kobako/filesystem.rb +44 -13
  150. data/lib/kobako/payload/arguments.rb +2 -2
  151. data/lib/kobako/pool.rb +4 -4
  152. data/lib/kobako/sandbox.rb +28 -28
  153. data/lib/kobako/sandbox_options.rb +2 -3
  154. data/lib/kobako/snippet/bytecode.rb +1 -1
  155. data/lib/kobako/snippet/source.rb +1 -1
  156. data/lib/kobako/transport/dispatcher.rb +5 -5
  157. data/lib/kobako/transport/exposure.rb +2 -2
  158. data/lib/kobako/transport/reflection.rb +7 -7
  159. data/lib/kobako/transport/run.rb +5 -5
  160. data/lib/kobako/transport/yielder.rb +1 -1
  161. data/lib/kobako/version.rb +1 -1
  162. data/lib/kobako.rb +34 -0
  163. data/release-please-config.json +1 -0
  164. data/sig/kobako/catalog/extensions.rbs +3 -3
  165. data/sig/kobako/catalog/handles.rbs +3 -3
  166. data/sig/kobako/catalog/mount.rbs +0 -4
  167. data/sig/kobako/catalog/services.rbs +1 -1
  168. data/sig/kobako/catalog/snippets.rbs +3 -3
  169. data/sig/kobako/catalog.rbs +1 -1
  170. data/sig/kobako/codec/decoder.rbs +1 -1
  171. data/sig/kobako/codec/handle_walk.rbs +1 -1
  172. data/sig/kobako/codec/nesting.rbs +2 -2
  173. data/sig/kobako/codec/utils.rbs +1 -1
  174. data/sig/kobako/codec/value_budget.rbs +1 -1
  175. data/sig/kobako/codec.rbs +1 -2
  176. data/sig/kobako/context.rbs +1 -1
  177. data/sig/kobako/filesystem.rbs +19 -14
  178. data/sig/kobako/guest_yielder.rbs +1 -1
  179. data/sig/kobako/payload/arguments.rbs +1 -1
  180. data/sig/kobako/pool.rbs +2 -2
  181. data/sig/kobako/runtime.rbs +1 -1
  182. data/sig/kobako/sandbox.rbs +3 -5
  183. data/sig/kobako/sandbox_options.rbs +1 -1
  184. data/sig/kobako/transport/dispatcher.rbs +2 -2
  185. data/sig/kobako/transport/exposure.rbs +1 -1
  186. data/sig/kobako/transport/reflection.rbs +2 -2
  187. data/sig/kobako/transport/run.rbs +3 -3
  188. data/sig/kobako/transport.rbs +1 -1
  189. metadata +67 -19
  190. data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
  191. data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
  192. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
  193. data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
  194. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  195. data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
  196. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
  197. data/crates/kobako-wasmtime/src/io.rs +0 -89
  198. data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
  199. data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
  200. /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
  201. /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
@@ -0,0 +1,192 @@
1
+ //! Guest linear-memory I/O for the ABI's buffer handoffs, whether the
2
+ //! invocation body or an upcall drives one.
3
+ //!
4
+ //! The two shared steps answer an error kind, and each caller words it,
5
+ //! because depth decides what the same problem means: before the export
6
+ //! call it is a `Trap` or an intact runtime, inside a callback it is a
7
+ //! wire failure the guest receives.
8
+
9
+ use kobako_transport::abi::{unpack_ptr_len, MAX_DISPATCH_PAYLOAD};
10
+
11
+ use super::{GuestInstance, RUNTIME_INCOMPATIBLE, SANDBOX_RUNTIME_NOT_KOBAKO};
12
+
13
+ // The size-limit messages here and in the endpoint name the cap as 16 MiB, so
14
+ // moving the cap fails the build until they are reworded with it.
15
+ const _: () = assert!(MAX_DISPATCH_PAYLOAD == 16 << 20);
16
+
17
+ /// Why a host write into guest memory failed.
18
+ pub(super) enum WriteError<F> {
19
+ TooLarge,
20
+ /// The allocator or the memory is missing or mistyped; carries the
21
+ /// message for it.
22
+ Incompatible(&'static str),
23
+ Trapped(F),
24
+ /// The allocator ran and answered 0.
25
+ OutOfMemory,
26
+ OutOfBounds,
27
+ }
28
+
29
+ /// Why a host read of a guest-named buffer failed.
30
+ pub(super) enum ReadError {
31
+ TooLarge,
32
+ OutOfBounds,
33
+ }
34
+
35
+ /// Allocate a guest buffer and copy `bytes` into it, answering the buffer's
36
+ /// address.
37
+ pub(super) fn alloc_and_write<G: GuestInstance>(
38
+ guest: &mut G,
39
+ bytes: &[u8],
40
+ ) -> Result<u32, WriteError<G::Fault>> {
41
+ let len = checked_payload_len(bytes.len()).map_err(|_| WriteError::TooLarge)?;
42
+ if guest.memory().is_none() {
43
+ return Err(WriteError::Incompatible(SANDBOX_RUNTIME_NOT_KOBAKO));
44
+ }
45
+ let ptr = guest
46
+ .alloc(len as u32)
47
+ .ok_or(WriteError::Incompatible(RUNTIME_INCOMPATIBLE))?
48
+ .map_err(WriteError::Trapped)?;
49
+ if ptr == 0 {
50
+ return Err(WriteError::OutOfMemory);
51
+ }
52
+ let memory = guest
53
+ .memory()
54
+ .ok_or(WriteError::Incompatible(SANDBOX_RUNTIME_NOT_KOBAKO))?;
55
+ let range = guest_buffer_range(ptr as usize, bytes.len(), memory.len())
56
+ .map_err(|_| WriteError::OutOfBounds)?;
57
+ memory[range].copy_from_slice(bytes);
58
+ Ok(ptr)
59
+ }
60
+
61
+ /// Copy out the buffer the guest names; a length past the 16 MiB cap is
62
+ /// refused before memory is touched.
63
+ pub(super) fn read_buffer(data: &[u8], ptr: usize, len: usize) -> Result<Vec<u8>, ReadError> {
64
+ if len > MAX_DISPATCH_PAYLOAD {
65
+ return Err(ReadError::TooLarge);
66
+ }
67
+ let range = guest_buffer_range(ptr, len, data.len()).map_err(|_| ReadError::OutOfBounds)?;
68
+ Ok(data[range].to_vec())
69
+ }
70
+
71
+ /// Every host write boundary routes its length through here, so the
72
+ /// wire-violation reason is uniform.
73
+ pub(super) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
74
+ if len > MAX_DISPATCH_PAYLOAD {
75
+ return Err("payload exceeds the 16 MiB limit");
76
+ }
77
+ // The cap above sits below `i32::MAX`, so this conversion cannot wrap.
78
+ i32::try_from(len).map_err(|_| "payload exceeds the 16 MiB limit")
79
+ }
80
+
81
+ /// The bytes of guest memory a `(ptr, len)` names.
82
+ fn guest_buffer_range(
83
+ ptr: usize,
84
+ len: usize,
85
+ mem_size: usize,
86
+ ) -> Result<core::ops::Range<usize>, &'static str> {
87
+ let end = ptr.checked_add(len).ok_or("ptr + len overflow")?;
88
+ if end > mem_size {
89
+ return Err("range exceeds Sandbox memory size");
90
+ }
91
+ Ok(ptr..end)
92
+ }
93
+
94
+ /// The `(ptr, len)` the guest packs for any answer it hands back, the
95
+ /// outcome and a block's result alike.
96
+ pub(super) fn unpack_guest_buffer(packed: u64) -> (usize, usize) {
97
+ let (ptr, len) = unpack_ptr_len(packed);
98
+ (ptr as usize, len as usize)
99
+ }
100
+
101
+ #[cfg(test)]
102
+ mod tests {
103
+ use super::{
104
+ checked_payload_len, guest_buffer_range, unpack_guest_buffer, MAX_DISPATCH_PAYLOAD,
105
+ };
106
+
107
+ // @behavior WE-059
108
+ #[test]
109
+ fn a_transfer_of_nothing_or_at_the_cap_is_admitted() {
110
+ assert_eq!(
111
+ (
112
+ checked_payload_len(0),
113
+ checked_payload_len(MAX_DISPATCH_PAYLOAD)
114
+ ),
115
+ (Ok(0), Ok(MAX_DISPATCH_PAYLOAD as i32)),
116
+ "0 bytes and MAX_DISPATCH_PAYLOAD bytes through checked_payload_len must be admitted"
117
+ );
118
+ }
119
+
120
+ // @behavior WE-060
121
+ #[test]
122
+ fn a_transfer_past_the_cap_is_refused() {
123
+ assert!(
124
+ checked_payload_len(MAX_DISPATCH_PAYLOAD + 1).is_err()
125
+ && checked_payload_len(usize::MAX).is_err(),
126
+ "one byte past MAX_DISPATCH_PAYLOAD and usize::MAX through checked_payload_len must be \
127
+ refused"
128
+ );
129
+ }
130
+
131
+ // @behavior WE-061
132
+ #[test]
133
+ fn a_guest_buffer_is_read_half_open() {
134
+ assert_eq!(
135
+ guest_buffer_range(10, 5, 100),
136
+ Ok(10..15),
137
+ "5 bytes at 10 in 100 bytes of memory through guest_buffer_range must answer 10..15"
138
+ );
139
+ }
140
+
141
+ // @behavior WE-062
142
+ #[test]
143
+ fn a_buffer_of_no_length_is_read_at_any_pointer_inside_memory() {
144
+ assert_eq!(
145
+ (guest_buffer_range(0, 0, 0), guest_buffer_range(42, 0, 100)),
146
+ (Ok(0..0), Ok(42..42)),
147
+ "no bytes at 0 in no memory and at 42 in 100 bytes through guest_buffer_range must \
148
+ answer an empty range there"
149
+ );
150
+ }
151
+
152
+ // @behavior WE-063
153
+ #[test]
154
+ fn a_pointer_and_length_that_overflow_are_refused() {
155
+ assert!(
156
+ guest_buffer_range(usize::MAX, 1, usize::MAX).is_err(),
157
+ "1 byte at usize::MAX through guest_buffer_range must be refused"
158
+ );
159
+ }
160
+
161
+ // @behavior WE-064
162
+ #[test]
163
+ fn a_buffer_ending_past_memory_is_refused() {
164
+ assert_eq!(
165
+ (guest_buffer_range(10, 100, 50).is_err(), guest_buffer_range(0, 50, 50)),
166
+ (true, Ok(0..50)),
167
+ "100 bytes at 10 in 50 bytes of memory through guest_buffer_range must be refused, and \
168
+ 50 bytes at 0 admitted"
169
+ );
170
+ }
171
+
172
+ // @behavior WE-001
173
+ #[test]
174
+ fn a_packed_answer_carries_the_pointer_high_and_the_length_low() {
175
+ assert_eq!(
176
+ unpack_guest_buffer(0xAABB_CCDD_1122_3344),
177
+ (0xAABB_CCDD, 0x1122_3344),
178
+ "0xAABB_CCDD_1122_3344 through unpack_guest_buffer must answer pointer 0xAABB_CCDD and \
179
+ length 0x1122_3344"
180
+ );
181
+ }
182
+
183
+ // @behavior WE-065
184
+ #[test]
185
+ fn a_packed_answer_of_nothing_is_a_zero_pair() {
186
+ assert_eq!(
187
+ unpack_guest_buffer(0),
188
+ (0, 0),
189
+ "0 through unpack_guest_buffer must answer pointer 0 and length 0"
190
+ );
191
+ }
192
+ }
@@ -0,0 +1,115 @@
1
+ //! Per-channel stdout / stderr capture sizing and clipping.
2
+ //!
3
+ //! Two pure helpers shared by the run path: one sizes the per-run pipe, the
4
+ //! other clips a captured snapshot back to the configured limit and reports
5
+ //! whether the limit was passed. Kept channel-agnostic (a function of `limit`,
6
+ //! not of which channel) so a regression that only breaks one channel cannot
7
+ //! sneak through the test that pins them.
8
+
9
+ /// One byte past the limit, so "wrote exactly the limit" and "passed the
10
+ /// limit" stay distinguishable.
11
+ pub fn pipe_capacity(limit: Option<usize>) -> usize {
12
+ match limit {
13
+ Some(limit) => limit.saturating_add(1),
14
+ None => usize::MAX,
15
+ }
16
+ }
17
+
18
+ /// Takes the snapshot by value and truncates in place, so the readback
19
+ /// costs one copy out of the pipe rather than two.
20
+ pub fn clip_capture(mut raw: Vec<u8>, limit: Option<usize>) -> (Vec<u8>, bool) {
21
+ match limit {
22
+ Some(limit) if raw.len() > limit => {
23
+ raw.truncate(limit);
24
+ (raw, true)
25
+ }
26
+ _ => (raw, false),
27
+ }
28
+ }
29
+
30
+ #[cfg(test)]
31
+ mod tests {
32
+ use super::{clip_capture, pipe_capacity};
33
+
34
+ // @behavior S-108
35
+ #[test]
36
+ fn a_pipe_holds_one_byte_past_its_limit() {
37
+ assert_eq!(
38
+ (pipe_capacity(Some(5)), pipe_capacity(Some(0))),
39
+ (6, 1),
40
+ "a limit of 5 and of 0 through pipe_capacity must size the pipe 6 and 1"
41
+ );
42
+ }
43
+
44
+ // @behavior S-109
45
+ #[test]
46
+ fn a_pipe_without_a_limit_holds_usize_max() {
47
+ assert_eq!(
48
+ pipe_capacity(None),
49
+ usize::MAX,
50
+ "no limit through pipe_capacity must size the pipe usize::MAX"
51
+ );
52
+ }
53
+
54
+ // @behavior S-110
55
+ #[test]
56
+ fn a_pipe_past_usize_max_saturates() {
57
+ assert_eq!(
58
+ pipe_capacity(Some(usize::MAX)),
59
+ usize::MAX,
60
+ "a limit of usize::MAX through pipe_capacity must saturate at usize::MAX"
61
+ );
62
+ }
63
+
64
+ // @behavior S-111
65
+ #[test]
66
+ fn a_capture_under_its_limit_stays_whole() {
67
+ assert_eq!(
68
+ clip_capture(b"abc".to_vec(), Some(5)),
69
+ (b"abc".to_vec(), false),
70
+ "3 bytes under a limit of 5 through clip_capture must stay whole and untruncated"
71
+ );
72
+ }
73
+
74
+ // @behavior S-112
75
+ #[test]
76
+ fn a_capture_at_its_limit_is_not_truncated() {
77
+ assert_eq!(
78
+ clip_capture(b"abcde".to_vec(), Some(5)),
79
+ (b"abcde".to_vec(), false),
80
+ "5 bytes at a limit of 5 through clip_capture must stay whole and untruncated"
81
+ );
82
+ }
83
+
84
+ // @behavior S-024
85
+ #[test]
86
+ fn a_capture_past_its_limit_is_clipped_and_truncated() {
87
+ // The pipe holds one byte past the limit, so 6 bytes is the most a
88
+ // limit of 5 can capture.
89
+ assert_eq!(
90
+ clip_capture(b"abcdef".to_vec(), Some(5)),
91
+ (b"abcde".to_vec(), true),
92
+ "6 bytes past a limit of 5 through clip_capture must clip to 5 and flag truncation"
93
+ );
94
+ }
95
+
96
+ // @behavior S-033
97
+ #[test]
98
+ fn a_capture_without_a_limit_stays_whole() {
99
+ assert_eq!(
100
+ clip_capture(b"abcdef".to_vec(), None),
101
+ (b"abcdef".to_vec(), false),
102
+ "6 bytes with no limit through clip_capture must stay whole and untruncated"
103
+ );
104
+ }
105
+
106
+ // @behavior S-100 S-101
107
+ #[test]
108
+ fn an_empty_capture_stays_empty() {
109
+ assert_eq!(
110
+ clip_capture(Vec::new(), Some(5)),
111
+ (Vec::new(), false),
112
+ "no bytes under a limit of 5 through clip_capture must stay empty and untruncated"
113
+ );
114
+ }
115
+ }
@@ -0,0 +1,253 @@
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.
5
+ //!
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.
12
+
13
+ use std::sync::Arc;
14
+
15
+ use kobako_transport::abi::pack_ptr_len;
16
+ use kobako_transport::envelope::{
17
+ Downcall, Fault, FaultKind, Reply, Unsupported, Upcall, YieldReply,
18
+ };
19
+
20
+ use super::buffer::{
21
+ alloc_and_write, checked_payload_len, read_buffer, unpack_guest_buffer, ReadError, WriteError,
22
+ };
23
+ use super::{GuestInstance, RUNTIME_INCOMPATIBLE, SANDBOX_RUNTIME_NOT_KOBAKO};
24
+ use crate::dispatch::DispatchHandler;
25
+ use crate::error::Trap;
26
+ use crate::yielder::Yielder;
27
+
28
+ /// The message of the trap ending an invocation its host gave up on.
29
+ pub const ABANDONED: &str = "the host abandoned the invocation";
30
+
31
+ /// The state of the Calls open on one instance: who answers them, how many
32
+ /// stand open, and the first trap a callback into the guest raised, as the
33
+ /// guest counts the Calls it has open.
34
+ pub struct Calls<F> {
35
+ on_dispatch: Option<Arc<dyn DispatchHandler>>,
36
+ open: u32,
37
+ reentry_trap: Option<F>,
38
+ }
39
+
40
+ impl<F> Default for Calls<F> {
41
+ fn default() -> Self {
42
+ Self {
43
+ on_dispatch: None,
44
+ open: 0,
45
+ reentry_trap: None,
46
+ }
47
+ }
48
+ }
49
+
50
+ impl<F> Calls<F> {
51
+ /// Have `handler` answer the Calls on this instance.
52
+ pub fn bind(&mut self, handler: Arc<dyn DispatchHandler>) {
53
+ self.on_dispatch = Some(handler);
54
+ }
55
+
56
+ /// Whether its host gave up on the invocation, which ends at the next
57
+ /// host call it answers.
58
+ pub fn abandoned(&self) -> bool {
59
+ self.on_dispatch
60
+ .as_ref()
61
+ .is_some_and(|handler| handler.abandoned())
62
+ }
63
+ }
64
+
65
+ /// How an upcall ends an invocation instead of answering.
66
+ pub enum UpcallEnd<F> {
67
+ /// The guest trapped during a callback into it.
68
+ Fault(F),
69
+ /// The host gave the invocation up.
70
+ Abandoned,
71
+ /// The answer could not be written into guest memory, naming why.
72
+ Unwritable(&'static str),
73
+ }
74
+
75
+ /// The Yielder of one upcall's frame, built over that frame's guest, so nested
76
+ /// dispatch frames each carry their own and stack on the call stack with no
77
+ /// shared slot.
78
+ struct FrameYielder<'a, G> {
79
+ guest: &'a mut G,
80
+ frame: u32,
81
+ }
82
+
83
+ impl<G: GuestInstance> Yielder for FrameYielder<'_, G> {
84
+ fn frame(&self) -> u32 {
85
+ self.frame
86
+ }
87
+
88
+ fn yield_to_block(&mut self, frame: u32, args: &[u8]) -> Result<YieldReply, Trap> {
89
+ let bytes =
90
+ drive_yield(self.guest, frame, args).map_err(|msg| Trap::Other(msg.to_string()))?;
91
+ YieldReply::decode(&bytes).map_err(|err| Trap::Other(err.to_string()))
92
+ }
93
+ }
94
+
95
+ /// Answer one `__kobako_upcall` call with the packed `(ptr<<32)|len` of
96
+ /// the Reply, or what ends the invocation instead.
97
+ pub fn handle<G: GuestInstance>(
98
+ guest: &mut G,
99
+ req_ptr: i32,
100
+ req_len: i32,
101
+ ) -> Result<i64, UpcallEnd<G::Fault>> {
102
+ let reply = reply_to(guest, req_ptr, req_len)
103
+ .unwrap_or_else(|reason| Reply::Fault(Fault::new(FaultKind::Internal, reason)));
104
+ let written = write_reply(guest, &fit_in_message(reply).encode());
105
+ if let Some(trap) = guest.calls().reentry_trap.take() {
106
+ return Err(UpcallEnd::Fault(trap));
107
+ }
108
+ if guest.calls().abandoned() {
109
+ return Err(UpcallEnd::Abandoned);
110
+ }
111
+ written.map_err(UpcallEnd::Unwritable)
112
+ }
113
+
114
+ /// An answer past the message cap is the Service's to change, as any other
115
+ /// answer the host cannot write is.
116
+ fn fit_in_message(reply: Reply) -> Reply {
117
+ let bytes = reply.encode().len();
118
+ match checked_payload_len(bytes) {
119
+ Ok(_) => reply,
120
+ Err(limit) => Reply::Fault(Fault::new(
121
+ FaultKind::Runtime,
122
+ format!("Sandbox could not write the Service's answer: {limit}"),
123
+ )),
124
+ }
125
+ }
126
+
127
+ fn reply_to<G: GuestInstance>(
128
+ guest: &mut G,
129
+ req_ptr: i32,
130
+ req_len: i32,
131
+ ) -> Result<Reply, &'static str> {
132
+ let req_bytes = read_request(guest, req_ptr, req_len)?;
133
+ // The endpoint decodes the Envelope, here and in `FrameYielder`, so
134
+ // the frontend never sees a frame; the payload inside it stays bytes
135
+ // the whole way through.
136
+ let call = match Upcall::decode(&req_bytes) {
137
+ Ok(Upcall::Call(call)) => call,
138
+ Ok(_) => {
139
+ return Ok(Reply::Unsupported(Unsupported::new(
140
+ "the host does not serve this request",
141
+ )))
142
+ }
143
+ Err(_) => {
144
+ return Err(
145
+ "the guest sent a malformed Call envelope — please report this as a kobako bug",
146
+ )
147
+ }
148
+ };
149
+
150
+ // A frontend always installs the dispatch handler before invoking the
151
+ // runtime, so reaching this branch indicates a misuse rather than a
152
+ // normal control path.
153
+ let handler = guest
154
+ .calls()
155
+ .on_dispatch
156
+ .clone()
157
+ .ok_or("a Sandbox callback fired outside an active invocation — please report this as a kobako bug")?;
158
+
159
+ // A frame-scoped yielder lends the guest to the handler. The borrow ends
160
+ // with the block, freeing it for `write_reply`; nested dispatch frames
161
+ // each build their own, so the LIFO re-entry lives on the stack.
162
+ let frame = {
163
+ let calls = guest.calls();
164
+ calls.open += 1;
165
+ calls.open - 1
166
+ };
167
+ let reply = {
168
+ let mut yielder = FrameYielder { guest, frame };
169
+ handler.dispatch(call, &mut yielder)
170
+ };
171
+ guest.calls().open -= 1;
172
+ reply.ok_or(
173
+ "a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
174
+ )
175
+ }
176
+
177
+ fn write_reply<G: GuestInstance>(guest: &mut G, bytes: &[u8]) -> Result<i64, &'static str> {
178
+ let ptr = write_from_callback(guest, bytes)?;
179
+ // The write has already held the length to the payload cap.
180
+ Ok(pack_ptr_len(ptr, bytes.len() as u32) as i64)
181
+ }
182
+
183
+ /// The request the guest hands the upcall import. Its `(ptr, len)` are
184
+ /// guest addresses, so they are read unsigned.
185
+ fn read_request<G: GuestInstance>(
186
+ guest: &mut G,
187
+ req_ptr: i32,
188
+ req_len: i32,
189
+ ) -> Result<Vec<u8>, &'static str> {
190
+ let memory = guest.memory().ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
191
+ read_buffer(memory, req_ptr as u32 as usize, req_len as u32 as usize).map_err(|err| match err {
192
+ ReadError::TooLarge => "request payload exceeds the 16 MiB limit",
193
+ ReadError::OutOfBounds => "the Sandbox produced an out-of-bounds request",
194
+ })
195
+ }
196
+
197
+ /// Keep a trap the guest raised during a callback into it for the upcall to
198
+ /// end the invocation with, and answer the callback's own reason.
199
+ fn keep_trap<G: GuestInstance>(
200
+ guest: &mut G,
201
+ trap: G::Fault,
202
+ reason: &'static str,
203
+ ) -> &'static str {
204
+ guest.calls().reentry_trap.get_or_insert(trap);
205
+ reason
206
+ }
207
+
208
+ /// Write `bytes` into the guest from inside a callback. A guest that has
209
+ /// already trapped is not called again.
210
+ fn write_from_callback<G: GuestInstance>(guest: &mut G, bytes: &[u8]) -> Result<u32, &'static str> {
211
+ if guest.calls().reentry_trap.is_some() {
212
+ return Err("the Sandbox already trapped during this invocation");
213
+ }
214
+ alloc_and_write(guest, bytes).map_err(|err| match err {
215
+ WriteError::TooLarge => "payload exceeds the 16 MiB limit",
216
+ WriteError::Incompatible(message) => message,
217
+ WriteError::Trapped(trap) => keep_trap(
218
+ guest,
219
+ trap,
220
+ "the Sandbox trapped while allocating memory for the request",
221
+ ),
222
+ WriteError::OutOfMemory => "the Sandbox ran out of memory while preparing the request",
223
+ WriteError::OutOfBounds => "could not write the request into the Sandbox's memory",
224
+ })
225
+ }
226
+
227
+ fn drive_yield<G: GuestInstance>(
228
+ guest: &mut G,
229
+ frame: u32,
230
+ args: &[u8],
231
+ ) -> Result<Vec<u8>, &'static str> {
232
+ let request = Downcall::Yield {
233
+ frame,
234
+ args: args.to_vec(),
235
+ }
236
+ .encode();
237
+ let len_i32 = checked_payload_len(request.len())?;
238
+ let req_ptr = write_from_callback(guest, &request)? as i32;
239
+
240
+ let packed = guest
241
+ .downcall(req_ptr, len_i32)
242
+ .ok_or(RUNTIME_INCOMPATIBLE)?
243
+ .map_err(|trap| keep_trap(guest, trap, "the Sandbox trapped while invoking a block"))?;
244
+ let (reply_ptr, reply_len) = unpack_guest_buffer(packed);
245
+ if reply_len == 0 {
246
+ return Err("the Sandbox returned an empty block result");
247
+ }
248
+ let memory = guest.memory().ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
249
+ read_buffer(memory, reply_ptr, reply_len).map_err(|err| match err {
250
+ ReadError::TooLarge => "block result payload exceeds the 16 MiB limit",
251
+ ReadError::OutOfBounds => "the Sandbox returned an out-of-bounds block result",
252
+ })
253
+ }