kobako 0.28.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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +377 -0
  4. data/Cargo.lock +181 -154
  5. data/README.md +99 -459
  6. data/ROADMAP.md +7 -1
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +354 -0
  9. data/crates/kobako-runtime/Cargo.toml +38 -7
  10. data/crates/kobako-runtime/README.md +17 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +9 -1
  12. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  13. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  14. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  15. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  16. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  17. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  18. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  19. data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
  20. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  21. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  29. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
  30. data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
  31. data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
  32. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  33. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  37. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  38. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  39. data/crates/kobako-runtime/src/filesystem.rs +26 -0
  40. data/crates/kobako-runtime/src/grant.rs +20 -0
  41. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  42. data/crates/kobako-runtime/src/lib.rs +16 -1
  43. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  44. data/crates/kobako-runtime/src/locking.rs +218 -0
  45. data/crates/kobako-runtime/src/mount.rs +105 -0
  46. data/crates/kobako-runtime/src/profile.rs +140 -4
  47. data/crates/kobako-runtime/src/runtime.rs +12 -0
  48. data/crates/kobako-runtime/src/sched.rs +210 -0
  49. data/crates/kobako-runtime/src/security.rs +696 -0
  50. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  51. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  52. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  53. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  54. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  55. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  56. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  57. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  58. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  59. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  60. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  61. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  62. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  63. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  64. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  65. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  66. data/crates/kobako-runtime/src/vfs.rs +60 -0
  67. data/crates/kobako-runtime/src/yielder.rs +11 -6
  68. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  70. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  71. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  72. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  73. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  74. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  75. data/crates/kobako-transport/CHANGELOG.md +23 -0
  76. data/crates/kobako-transport/Cargo.toml +3 -3
  77. data/crates/kobako-transport/README.md +6 -5
  78. data/crates/kobako-transport/src/abi.rs +1 -1
  79. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  80. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  81. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  83. data/crates/kobako-transport/src/envelope/fault.rs +4 -3
  84. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  85. data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
  86. data/crates/kobako-transport/src/envelope/reply.rs +34 -16
  87. data/crates/kobako-transport/src/envelope/request.rs +19 -9
  88. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  89. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  90. data/crates/kobako-transport/src/envelope.rs +2 -2
  91. data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
  92. data/crates/kobako-wasmtime/Cargo.toml +25 -19
  93. data/crates/kobako-wasmtime/README.md +13 -7
  94. data/crates/kobako-wasmtime/src/abi.rs +75 -49
  95. data/crates/kobako-wasmtime/src/ambient.rs +64 -6
  96. data/crates/kobako-wasmtime/src/cache.rs +188 -77
  97. data/crates/kobako-wasmtime/src/config.rs +77 -5
  98. data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
  99. data/crates/kobako-wasmtime/src/driver.rs +70 -135
  100. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  101. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  102. data/crates/kobako-wasmtime/src/imports.rs +52 -0
  103. data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
  104. data/crates/kobako-wasmtime/src/invocation.rs +43 -74
  105. data/crates/kobako-wasmtime/src/lib.rs +5 -4
  106. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  107. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
  108. data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
  109. data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
  110. data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
  111. data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
  112. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  113. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
  114. data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
  115. data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
  116. data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
  117. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  118. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  119. data/crates/kobako-wasmtime/src/preview1.rs +512 -0
  120. data/crates/kobako-wasmtime/src/trap.rs +45 -25
  121. data/crates/kobako-wasmtime/src/wasi.rs +333 -0
  122. data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
  123. data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
  124. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  125. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
  126. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
  127. data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
  128. data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
  129. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
  130. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  131. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  132. data/data/kobako.wasm +0 -0
  133. data/ext/kobako/Cargo.toml +4 -2
  134. data/ext/kobako/src/runtime/bridge.rs +154 -81
  135. data/ext/kobako/src/runtime/errors.rs +1 -1
  136. data/ext/kobako/src/runtime/escape.rs +67 -0
  137. data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
  138. data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
  139. data/ext/kobako/src/runtime/filesystem.rs +208 -0
  140. data/ext/kobako/src/runtime/gvl.rs +49 -8
  141. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  142. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  143. data/ext/kobako/src/runtime.rs +125 -199
  144. data/lib/kobako/catalog/extensions.rb +5 -5
  145. data/lib/kobako/catalog/flags.rb +2 -14
  146. data/lib/kobako/catalog/grants.rb +30 -0
  147. data/lib/kobako/catalog/handles.rb +6 -6
  148. data/lib/kobako/catalog/mount.rb +19 -0
  149. data/lib/kobako/catalog/services.rb +13 -2
  150. data/lib/kobako/catalog/snippets.rb +11 -15
  151. data/lib/kobako/catalog.rb +11 -0
  152. data/lib/kobako/codec/decoder.rb +10 -9
  153. data/lib/kobako/codec/ext_types.rb +4 -6
  154. data/lib/kobako/codec/handle_walk.rb +4 -6
  155. data/lib/kobako/codec/nesting.rb +7 -7
  156. data/lib/kobako/codec/state.rb +19 -3
  157. data/lib/kobako/codec/utils.rb +1 -1
  158. data/lib/kobako/codec/value_budget.rb +86 -0
  159. data/lib/kobako/codec.rb +7 -0
  160. data/lib/kobako/context.rb +40 -13
  161. data/lib/kobako/errors.rb +15 -14
  162. data/lib/kobako/filesystem.rb +69 -0
  163. data/lib/kobako/payload/arguments.rb +2 -2
  164. data/lib/kobako/pool.rb +6 -6
  165. data/lib/kobako/sandbox.rb +55 -27
  166. data/lib/kobako/sandbox_options.rb +39 -13
  167. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
  168. data/lib/kobako/snippet/source.rb +1 -1
  169. data/lib/kobako/snippet.rb +1 -1
  170. data/lib/kobako/transport/block_failure.rb +35 -0
  171. data/lib/kobako/transport/dispatcher.rb +33 -29
  172. data/lib/kobako/transport/exposure.rb +7 -9
  173. data/lib/kobako/transport/reflection.rb +79 -23
  174. data/lib/kobako/transport/run.rb +5 -5
  175. data/lib/kobako/transport/yielder.rb +9 -31
  176. data/lib/kobako/transport.rb +1 -0
  177. data/lib/kobako/version.rb +1 -1
  178. data/lib/kobako.rb +35 -0
  179. data/release-please-config.json +1 -0
  180. data/sig/kobako/catalog/extensions.rbs +3 -3
  181. data/sig/kobako/catalog/flags.rbs +0 -4
  182. data/sig/kobako/catalog/grants.rbs +13 -0
  183. data/sig/kobako/catalog/handles.rbs +3 -3
  184. data/sig/kobako/catalog/mount.rbs +24 -0
  185. data/sig/kobako/catalog/services.rbs +5 -1
  186. data/sig/kobako/catalog/snippets.rbs +4 -4
  187. data/sig/kobako/catalog.rbs +1 -0
  188. data/sig/kobako/codec/decoder.rbs +1 -1
  189. data/sig/kobako/codec/handle_walk.rbs +1 -1
  190. data/sig/kobako/codec/nesting.rbs +2 -2
  191. data/sig/kobako/codec/state.rbs +7 -1
  192. data/sig/kobako/codec/utils.rbs +1 -1
  193. data/sig/kobako/codec/value_budget.rbs +23 -0
  194. data/sig/kobako/codec.rbs +3 -2
  195. data/sig/kobako/context.rbs +14 -6
  196. data/sig/kobako/errors.rbs +4 -4
  197. data/sig/kobako/filesystem.rbs +93 -0
  198. data/sig/kobako/guest_yielder.rbs +9 -0
  199. data/sig/kobako/payload/arguments.rbs +1 -1
  200. data/sig/kobako/pool.rbs +3 -3
  201. data/sig/kobako/runtime.rbs +21 -49
  202. data/sig/kobako/sandbox.rbs +9 -5
  203. data/sig/kobako/sandbox_options.rbs +25 -3
  204. data/sig/kobako/snapshot.rbs +20 -0
  205. data/sig/kobako/snippet/bytecode.rbs +12 -0
  206. data/sig/kobako/transport/block_failure.rbs +13 -0
  207. data/sig/kobako/transport/dispatcher.rbs +4 -4
  208. data/sig/kobako/transport/exposure.rbs +2 -2
  209. data/sig/kobako/transport/reflection.rbs +14 -1
  210. data/sig/kobako/transport/run.rbs +3 -3
  211. data/sig/kobako/transport/yielder.rbs +3 -7
  212. data/sig/kobako/transport.rbs +3 -3
  213. metadata +116 -15
  214. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  215. data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
  216. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
  217. data/crates/kobako-wasmtime/src/io.rs +0 -286
  218. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -0,0 +1,194 @@
1
+ //! Per-invocation I/O between the host and the guest: it frames what the guest
2
+ //! is handed, writes the entry's downcall into a freshly allocated guest buffer,
3
+ //! and reads the Outcome the downcall answered with.
4
+
5
+ use kobako_transport::abi::FRAME_LEN_SIZE;
6
+ use kobako_transport::envelope::{CompileReply, Downcall, Outcome, Preamble, Run, Snippet};
7
+
8
+ use super::buffer::{
9
+ alloc_and_write, checked_payload_len, read_buffer, unpack_guest_buffer, ReadError, WriteError,
10
+ };
11
+ use super::{GuestInstance, SANDBOX_RUNTIME_NOT_KOBAKO};
12
+ use crate::error::{InvokeError, SetupError, Trap};
13
+ use crate::runtime::Entry;
14
+
15
+ /// What the guest reads from stdin: the Preamble behind its length prefix. It
16
+ /// carries the same 16 MiB cap as a downcall request: past it, the prefix
17
+ /// would silently wrap.
18
+ pub fn stdin_content(preamble: &Preamble) -> Result<Vec<u8>, &'static str> {
19
+ let preamble = preamble.encode();
20
+ checked_payload_len(preamble.len())?;
21
+ let mut content = Vec::with_capacity(FRAME_LEN_SIZE + preamble.len());
22
+ content.extend_from_slice(&(preamble.len() as u32).to_be_bytes());
23
+ content.extend_from_slice(&preamble);
24
+ Ok(content)
25
+ }
26
+
27
+ /// The downcall `entry` is.
28
+ pub fn request_of(entry: Entry<'_>) -> Vec<u8> {
29
+ match entry {
30
+ Entry::Eval { source } => Downcall::Eval(source.to_vec()),
31
+ Entry::Run {
32
+ entrypoint,
33
+ payload,
34
+ } => Downcall::Run(Run {
35
+ entrypoint: entrypoint.to_owned(),
36
+ payload,
37
+ }),
38
+ }
39
+ .encode()
40
+ }
41
+
42
+ /// The downcall that asks the guest to compile its Preamble.
43
+ pub fn compile_request() -> Vec<u8> {
44
+ Downcall::Compile.encode()
45
+ }
46
+
47
+ /// Write a request into a guest buffer, answering where. A missing or trapping
48
+ /// allocator is an engine fault, a `Trap`; one that runs but returns 0 leaves
49
+ /// the runtime intact, so it is a `SetupError`.
50
+ pub fn write_request<G: GuestInstance>(
51
+ guest: &mut G,
52
+ envelope: &[u8],
53
+ ) -> Result<(i32, i32), InvokeError> {
54
+ let trap = |message: String| -> InvokeError { Trap::Other(message).into() };
55
+ let ptr = alloc_and_write(guest, envelope).map_err(|err| match err {
56
+ WriteError::OutOfMemory => {
57
+ SetupError::Intact("could not allocate input buffer (out of memory)".to_string()).into()
58
+ }
59
+ WriteError::TooLarge => trap("payload exceeds the 16 MiB limit".to_string()),
60
+ WriteError::Incompatible(message) => trap(message.to_string()),
61
+ WriteError::Trapped(e) => G::cap_trap(&e)
62
+ .unwrap_or_else(|| Trap::Other(format!("failed to allocate input buffer: {e}")))
63
+ .into(),
64
+ WriteError::OutOfBounds => {
65
+ trap("the input buffer lies outside the Sandbox's memory".to_string())
66
+ }
67
+ })?;
68
+ // The write has already held the length to the payload cap.
69
+ Ok((ptr as i32, envelope.len() as i32))
70
+ }
71
+
72
+ /// A guest that answered without an Outcome the envelope can frame — an
73
+ /// empty answer included — has nothing left to attribute, so it is a
74
+ /// `Trap` like every other way of failing to read the result.
75
+ pub fn read_outcome<G: GuestInstance>(guest: &mut G, packed: u64) -> Result<Outcome, Trap> {
76
+ let bytes = read_answer(guest, packed)?;
77
+ Outcome::decode(&bytes).map_err(|_| {
78
+ Trap::Other(
79
+ if bytes.is_empty() {
80
+ "Sandbox exited without producing a result"
81
+ } else {
82
+ "Sandbox produced an unrecognised result"
83
+ }
84
+ .to_string(),
85
+ )
86
+ })
87
+ }
88
+
89
+ /// Held to the same 16 MiB bound as an Outcome; an answer that does not
90
+ /// frame is a `Trap` the caller treats like any other failed compile.
91
+ pub fn read_compile_reply<G: GuestInstance>(
92
+ guest: &mut G,
93
+ packed: u64,
94
+ ) -> Result<CompileReply, Trap> {
95
+ let bytes = read_answer(guest, packed)?;
96
+ CompileReply::decode(&bytes)
97
+ .map_err(|_| Trap::Other("Sandbox produced an unrecognised compile answer".to_string()))
98
+ }
99
+
100
+ fn read_answer<G: GuestInstance>(guest: &mut G, packed: u64) -> Result<Vec<u8>, Trap> {
101
+ let memory = guest
102
+ .memory()
103
+ .ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_NOT_KOBAKO.to_string()))?;
104
+ let (ptr, len) = unpack_guest_buffer(packed);
105
+ read_buffer(memory, ptr, len).map_err(|err| {
106
+ Trap::Other(
107
+ match err {
108
+ ReadError::TooLarge => "result payload exceeds the 16 MiB limit",
109
+ ReadError::OutOfBounds => "the Sandbox result is out of bounds",
110
+ }
111
+ .to_string(),
112
+ )
113
+ })
114
+ }
115
+
116
+ /// What a compile answered, to replay in place of `preamble`'s source
117
+ /// snippets, or `None` where nothing can be replayed.
118
+ pub fn compiled_bytecodes(preamble: &Preamble, reply: CompileReply) -> Option<Vec<Vec<u8>>> {
119
+ match reply {
120
+ CompileReply::Ok(bytecodes) => {
121
+ one_per_source(preamble, bytecodes).filter(|bytecodes| replay_fits(preamble, bytecodes))
122
+ }
123
+ CompileReply::Error(_) | CompileReply::Unsupported(_) => None,
124
+ }
125
+ }
126
+
127
+ /// Each invocation sends the Preamble with every bytecode in place of its
128
+ /// source, so an answer that would carry it past the payload cap would trap
129
+ /// every invocation the source alone fits.
130
+ fn replay_fits(preamble: &Preamble, bytecodes: &[Vec<u8>]) -> bool {
131
+ let mut replay = preamble.clone();
132
+ let sources = replay
133
+ .snippets
134
+ .entries
135
+ .iter_mut()
136
+ .filter(|entry| matches!(entry, Snippet::Source { .. }));
137
+ for (entry, body) in sources.zip(bytecodes) {
138
+ *entry = Snippet::Bytecode { body: body.clone() };
139
+ }
140
+ checked_payload_len(replay.encode().len()).is_ok()
141
+ }
142
+
143
+ /// A frontend replays each bytecode in place of one source snippet, in
144
+ /// order, so an answer of any other length leaves nothing to replay.
145
+ fn one_per_source(preamble: &Preamble, bytecodes: Vec<Vec<u8>>) -> Option<Vec<Vec<u8>>> {
146
+ let sources = preamble
147
+ .snippets
148
+ .entries
149
+ .iter()
150
+ .filter(|entry| matches!(entry, Snippet::Source { .. }))
151
+ .count();
152
+ (bytecodes.len() == sources).then_some(bytecodes)
153
+ }
154
+
155
+ #[cfg(test)]
156
+ mod tests {
157
+ use kobako_transport::envelope::Snippets;
158
+
159
+ use super::*;
160
+
161
+ /// One source between two bytecode snippets: a compile answers one
162
+ /// bytecode for it.
163
+ fn one_source() -> Preamble {
164
+ let mut preamble = Preamble::default();
165
+ preamble.snippets = Snippets {
166
+ entries: vec![
167
+ Snippet::Bytecode { body: vec![0xaa] },
168
+ Snippet::Source {
169
+ name: "Only".into(),
170
+ body: "1".into(),
171
+ },
172
+ Snippet::Bytecode { body: vec![0xbb] },
173
+ ],
174
+ };
175
+ preamble
176
+ }
177
+
178
+ // @behavior WE-089
179
+ #[test]
180
+ fn a_compiled_answer_of_another_length_leaves_nothing_compiled() {
181
+ let preamble = one_source();
182
+
183
+ assert_eq!(
184
+ [
185
+ one_per_source(&preamble, Vec::new()),
186
+ one_per_source(&preamble, vec![vec![0x01], vec![0x02]]),
187
+ one_per_source(&preamble, vec![vec![0x01]]),
188
+ ],
189
+ [None, None, Some(vec![vec![0x01]])],
190
+ "a compiled answer through Driver#compile must be kept only when it carries one \
191
+ bytecode per source snippet"
192
+ );
193
+ }
194
+ }
@@ -0,0 +1,238 @@
1
+ //! The per-invocation memory cap: only the growth past the linear-memory size
2
+ //! captured at invocation entry is charged, so the image's initial allocation
3
+ //! is outside the budget.
4
+
5
+ /// The budget one invocation's memory grows against.
6
+ ///
7
+ /// `memory_limit` bounds per-invocation growth (`None` disables it).
8
+ /// `baseline` is the linear-memory size captured at invocation entry by
9
+ /// `MemoryCap::activate`; only the growth past `baseline` is charged against
10
+ /// `memory_limit`, so the mruby image's initial allocation and any high-water
11
+ /// mark left by prior invocations on the same Sandbox do not consume the
12
+ /// budget. An Engine also asks about the module's declared initial allocation
13
+ /// at instantiation time, so the cap stays dormant until `MemoryCap::activate`
14
+ /// arms it for one invocation.
15
+ #[derive(Debug, Clone, Copy)]
16
+ pub struct MemoryCap {
17
+ memory_limit: Option<usize>,
18
+ baseline: usize,
19
+ cap_active: bool,
20
+ peak: usize,
21
+ }
22
+
23
+ impl MemoryCap {
24
+ pub fn new(memory_limit: Option<usize>) -> Self {
25
+ Self {
26
+ memory_limit,
27
+ baseline: 0,
28
+ cap_active: false,
29
+ peak: 0,
30
+ }
31
+ }
32
+
33
+ /// The cap stays dormant until armed, because the module's declared
34
+ /// initial memory is allocated during instantiation and must pass.
35
+ pub fn activate(&mut self, baseline: usize) {
36
+ self.baseline = baseline;
37
+ self.cap_active = true;
38
+ self.peak = 0;
39
+ }
40
+
41
+ pub fn deactivate(&mut self) {
42
+ self.cap_active = false;
43
+ }
44
+
45
+ /// A grow the cap rejects never updates the peak, so the reported value
46
+ /// never exceeds `memory_limit`.
47
+ pub fn peak(&self) -> usize {
48
+ self.peak
49
+ }
50
+
51
+ /// Charge a growth to `desired` bytes, refusing one past `memory_limit`.
52
+ pub fn charge(&mut self, desired: usize) -> Result<(), MemoryLimitExceeded> {
53
+ if !self.cap_active {
54
+ return Ok(());
55
+ }
56
+ let delta = desired.saturating_sub(self.baseline);
57
+ if let Some(limit) = self.memory_limit {
58
+ if delta > limit {
59
+ return Err(MemoryLimitExceeded::new(desired, limit));
60
+ }
61
+ }
62
+ if delta > self.peak {
63
+ self.peak = delta;
64
+ }
65
+ Ok(())
66
+ }
67
+ }
68
+
69
+ /// A growth past the per-invocation memory cap. An Engine ends the invocation
70
+ /// with it, and a Wasm Driver finds it again to classify the failure as a
71
+ /// memory-limit `Trap`. Callers use the `Display` impl below — no field is
72
+ /// read directly — so the inner state stays private.
73
+ #[derive(Debug)]
74
+ pub struct MemoryLimitExceeded {
75
+ desired: usize,
76
+ limit: usize,
77
+ }
78
+
79
+ impl MemoryLimitExceeded {
80
+ pub fn new(desired: usize, limit: usize) -> Self {
81
+ Self { desired, limit }
82
+ }
83
+ }
84
+
85
+ impl std::fmt::Display for MemoryLimitExceeded {
86
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87
+ write!(
88
+ f,
89
+ "memory usage exceeded memory_limit: \
90
+ requested={} bytes, limit={} bytes",
91
+ self.desired, self.limit
92
+ )
93
+ }
94
+ }
95
+
96
+ impl std::error::Error for MemoryLimitExceeded {}
97
+
98
+ #[cfg(test)]
99
+ mod tests {
100
+ //! The pure delta arithmetic, so a regression that breaks the baseline
101
+ //! accounting (dropping the `baseline` subtraction, or letting `activate`
102
+ //! carry stale state across invocations) is caught without an Engine.
103
+ use super::MemoryCap;
104
+
105
+ fn assert_charged(cap: &mut MemoryCap, desired: usize, what: &str) {
106
+ assert!(
107
+ cap.charge(desired).is_ok(),
108
+ "{what} through MemoryCap::charge must be allowed"
109
+ );
110
+ }
111
+
112
+ fn assert_refused(cap: &mut MemoryCap, desired: usize, what: &str) {
113
+ assert!(
114
+ cap.charge(desired).is_err(),
115
+ "{what} through MemoryCap::charge must be refused"
116
+ );
117
+ }
118
+
119
+ // @behavior S-113
120
+ #[test]
121
+ fn a_growth_before_the_cap_is_armed_is_not_bounded() {
122
+ let mut cap = MemoryCap::new(Some(1 << 20));
123
+ assert_charged(&mut cap, 100 << 20, "a 100 MiB growth before activate");
124
+ }
125
+
126
+ // @behavior S-114
127
+ #[test]
128
+ fn a_growth_within_the_limit_is_allowed() {
129
+ let mut cap = MemoryCap::new(Some(1 << 20));
130
+ cap.activate(2 << 20);
131
+ assert_charged(
132
+ &mut cap,
133
+ (2 << 20) + (1 << 19),
134
+ "a 512 KiB growth past a 2 MiB baseline under a 1 MiB limit",
135
+ );
136
+ }
137
+
138
+ // @behavior S-121
139
+ #[test]
140
+ fn a_growth_past_the_limit_is_refused() {
141
+ let mut cap = MemoryCap::new(Some(1 << 20));
142
+ cap.activate(2 << 20);
143
+ assert_refused(
144
+ &mut cap,
145
+ 4 << 20,
146
+ "a 2 MiB growth past a 2 MiB baseline under a 1 MiB limit",
147
+ );
148
+ }
149
+
150
+ // @behavior S-008
151
+ #[test]
152
+ fn each_activation_charges_from_its_own_baseline() {
153
+ let mut cap = MemoryCap::new(Some(1 << 20));
154
+ cap.activate(2 << 20);
155
+ assert_charged(&mut cap, 3 << 20, "a 1 MiB growth in the first invocation");
156
+ cap.activate(3 << 20);
157
+ assert_charged(
158
+ &mut cap,
159
+ 4 << 20,
160
+ "a 1 MiB growth in the second invocation, past the first one's residue",
161
+ );
162
+ }
163
+
164
+ // @behavior S-009
165
+ #[test]
166
+ fn a_cap_without_a_limit_allows_any_growth() {
167
+ let mut cap = MemoryCap::new(None);
168
+ cap.activate(0);
169
+ assert_charged(&mut cap, 100 << 20, "a 100 MiB growth with no limit");
170
+ }
171
+
172
+ // @behavior S-116
173
+ #[test]
174
+ fn the_peak_is_zero_before_any_growth() {
175
+ let cap = MemoryCap::new(Some(1 << 20));
176
+ assert_eq!(
177
+ cap.peak(),
178
+ 0,
179
+ "a new cap through MemoryCap::peak must report 0"
180
+ );
181
+ }
182
+
183
+ // @behavior S-115
184
+ #[test]
185
+ fn the_peak_is_the_highest_growth_past_the_baseline() {
186
+ let mut cap = MemoryCap::new(Some(1 << 20));
187
+ cap.activate(2 << 20);
188
+ assert_charged(&mut cap, (2 << 20) + (1 << 18), "a 256 KiB growth");
189
+ assert_charged(&mut cap, (2 << 20) + (1 << 19), "a 512 KiB growth");
190
+ assert_charged(&mut cap, (2 << 20) + (1 << 17), "a 128 KiB growth");
191
+ assert_eq!(
192
+ cap.peak(),
193
+ 1 << 19,
194
+ "growths of 256, 512, and 128 KiB through MemoryCap::peak must report 512 KiB"
195
+ );
196
+ }
197
+
198
+ // @behavior S-062
199
+ #[test]
200
+ fn a_refused_growth_leaves_the_peak_alone() {
201
+ let mut cap = MemoryCap::new(Some(1 << 20));
202
+ cap.activate(2 << 20);
203
+ assert_charged(&mut cap, (2 << 20) + (1 << 19), "a 512 KiB growth");
204
+ assert_refused(&mut cap, 4 << 20, "a 2 MiB growth under a 1 MiB limit");
205
+ assert_eq!(
206
+ cap.peak(),
207
+ 1 << 19,
208
+ "a refused 2 MiB growth through MemoryCap::peak must leave 512 KiB reported"
209
+ );
210
+ }
211
+
212
+ // @behavior S-117
213
+ #[test]
214
+ fn each_activation_resets_the_peak() {
215
+ let mut cap = MemoryCap::new(Some(1 << 20));
216
+ cap.activate(2 << 20);
217
+ assert_charged(&mut cap, (2 << 20) + (1 << 19), "a 512 KiB growth");
218
+ cap.activate(3 << 20);
219
+ assert_eq!(
220
+ cap.peak(),
221
+ 0,
222
+ "a second activate through MemoryCap::peak must report 0"
223
+ );
224
+ }
225
+
226
+ // @behavior S-118
227
+ #[test]
228
+ fn a_cap_without_a_limit_still_reports_its_peak() {
229
+ let mut cap = MemoryCap::new(None);
230
+ cap.activate(1 << 20);
231
+ assert_charged(&mut cap, 5 << 20, "a 4 MiB growth with no limit");
232
+ assert_eq!(
233
+ cap.peak(),
234
+ 4 << 20,
235
+ "a 4 MiB growth with no limit through MemoryCap::peak must report 4 MiB"
236
+ );
237
+ }
238
+ }
@@ -0,0 +1,166 @@
1
+ //! The host ABI endpoint: everything the kobako ABI asks of a host that no
2
+ //! Engine decides, so a Wasm Driver for any Engine implements `GuestInstance`
3
+ //! over its own memory and exports, and links `__kobako_upcall` to
4
+ //! `dispatch::handle`.
5
+ //!
6
+ //! Only this endpoint and its guest counterpart read or write an envelope's
7
+ //! bytes.
8
+
9
+ mod buffer;
10
+ mod capture;
11
+ mod dispatch;
12
+ mod invoke;
13
+ mod memory_cap;
14
+
15
+ use std::fmt::Display;
16
+
17
+ use kobako_transport::abi::ABI_VERSION;
18
+
19
+ use crate::error::{SetupError, Trap};
20
+
21
+ pub use capture::{clip_capture, pipe_capacity};
22
+ pub use dispatch::{handle, Calls, UpcallEnd, ABANDONED};
23
+ pub use invoke::{
24
+ compile_request, compiled_bytecodes, read_compile_reply, read_outcome, request_of,
25
+ stdin_content, write_request,
26
+ };
27
+ pub use memory_cap::{MemoryCap, MemoryLimitExceeded};
28
+
29
+ /// For a required guest export that is absent or mistyped, wherever it is
30
+ /// looked up. Names neither the export nor a frontend's artifact path;
31
+ /// what a caller can do is rebuild the runtime.
32
+ pub const RUNTIME_INCOMPATIBLE: &str =
33
+ "the Sandbox runtime is incompatible; rebuild it against the kobako version in use";
34
+
35
+ /// Accept a Guest Binary only when the ABI version it reports is the host's;
36
+ /// `None` means it exports none. Only rebuilding the artifact fixes either
37
+ /// refusal, so both are `Dead` and say so.
38
+ pub fn verify_abi_version(reported: Option<u32>) -> Result<(), SetupError> {
39
+ match reported {
40
+ None => Err(SetupError::Dead(format!(
41
+ "the Guest Binary does not export __kobako_abi_version; \
42
+ rebuild it against ABI version {ABI_VERSION}"
43
+ ))),
44
+ Some(version) if version != ABI_VERSION => Err(SetupError::Dead(format!(
45
+ "the Guest Binary reports ABI version {version}, but this host \
46
+ implements ABI version {ABI_VERSION}; rebuild the Guest Binary \
47
+ against the host's version"
48
+ ))),
49
+ Some(_) => Ok(()),
50
+ }
51
+ }
52
+
53
+ /// For the "the loaded Wasm module is not a Kobako-shaped runtime at all"
54
+ /// failure mode, such as an instance with no `memory` export.
55
+ const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
56
+ "the loaded Wasm module is not a Kobako-compatible runtime";
57
+
58
+ /// One guest instance as the endpoint drives it: what a Wasm Driver gives it of an
59
+ /// Engine, from the invocation body and from inside `__kobako_upcall` alike.
60
+ /// A method answering `None` names an export the module does not have.
61
+ pub trait GuestInstance {
62
+ /// What a call into the guest raises, kept as it is so an invocation ends
63
+ /// with its own kind.
64
+ type Fault: Display;
65
+
66
+ /// The state of the Calls open on this instance.
67
+ fn calls(&mut self) -> &mut Calls<Self::Fault>;
68
+
69
+ /// The guest's linear memory.
70
+ fn memory(&mut self) -> Option<&mut [u8]>;
71
+
72
+ /// Allocate `len` bytes through `__kobako_alloc`, answering the address,
73
+ /// or 0 where the guest has none to give.
74
+ fn alloc(&mut self, len: u32) -> Option<Result<u32, Self::Fault>>;
75
+
76
+ /// Call `__kobako_downcall` with a request at `ptr`, answering the packed
77
+ /// `(ptr, len)` of its answer.
78
+ fn downcall(&mut self, ptr: i32, len: i32) -> Option<Result<u64, Self::Fault>>;
79
+
80
+ /// The cap `fault` carries, if one of the invocation's caps fired, which
81
+ /// names itself rather than the step it fired during.
82
+ fn cap_trap(fault: &Self::Fault) -> Option<Trap>;
83
+ }
84
+
85
+ #[cfg(test)]
86
+ mod tests {
87
+ //! Witness that the endpoint needs nothing of an Engine: an instance made
88
+ //! of a plain buffer drives a whole request and upcall through it.
89
+ use kobako_transport::abi::unpack_ptr_len;
90
+ use kobako_transport::envelope::Reply;
91
+
92
+ use super::*;
93
+
94
+ /// An instance holding a buffer and a bump allocator.
95
+ struct InMemory {
96
+ memory: Vec<u8>,
97
+ next: u32,
98
+ calls: Calls<String>,
99
+ }
100
+
101
+ impl GuestInstance for InMemory {
102
+ type Fault = String;
103
+
104
+ fn calls(&mut self) -> &mut Calls<String> {
105
+ &mut self.calls
106
+ }
107
+
108
+ fn memory(&mut self) -> Option<&mut [u8]> {
109
+ Some(&mut self.memory)
110
+ }
111
+
112
+ fn alloc(&mut self, len: u32) -> Option<Result<u32, String>> {
113
+ let ptr = self.next;
114
+ self.next += len;
115
+ Some(Ok(ptr))
116
+ }
117
+
118
+ fn downcall(&mut self, _ptr: i32, _len: i32) -> Option<Result<u64, String>> {
119
+ None
120
+ }
121
+
122
+ fn cap_trap(_fault: &String) -> Option<Trap> {
123
+ None
124
+ }
125
+ }
126
+
127
+ fn instance() -> InMemory {
128
+ InMemory {
129
+ memory: vec![0; 1024],
130
+ next: 16,
131
+ calls: Calls::default(),
132
+ }
133
+ }
134
+
135
+ // @behavior RT-191
136
+ #[test]
137
+ fn a_request_lands_in_guest_memory_through_any_instance() {
138
+ let mut guest = instance();
139
+ let answered = write_request(&mut guest, b"request").ok();
140
+ assert_eq!(
141
+ (answered, &guest.memory[16..23]),
142
+ (Some((16, 7)), &b"request"[..]),
143
+ "endpoint::write_request through an instance of a plain buffer must write the \
144
+ request at the address its allocator answers"
145
+ );
146
+ }
147
+
148
+ // A tag the host does not serve needs no handler bound to be answered.
149
+ // @behavior RT-191
150
+ #[test]
151
+ fn an_upcall_answers_through_any_instance() {
152
+ let mut guest = instance();
153
+ let request = b"not a call";
154
+ guest.memory[..request.len()].copy_from_slice(request);
155
+ let packed = handle(&mut guest, 0, request.len() as i32)
156
+ .ok()
157
+ .expect("the upcall must answer");
158
+ let (ptr, len) = unpack_ptr_len(packed as u64);
159
+ let reply = Reply::decode(&guest.memory[ptr as usize..(ptr + len) as usize]);
160
+ assert!(
161
+ matches!(reply, Ok(Reply::Unsupported(_))),
162
+ "endpoint::handle through an instance of a plain buffer must write the answer to a \
163
+ request under an unserved tag back into guest memory"
164
+ );
165
+ }
166
+ }
@@ -0,0 +1,82 @@
1
+ //! The host directories a Bind keeps open, each a descriptor of the host's
2
+ //! own. A dentry holds the one above it, so keeping one for every dentry would
3
+ //! spend a descriptor per level of depth, which no table of a guest's counts.
4
+ //! A directory the guest holds open keeps its own; the rest are kept up to a
5
+ //! bound, the oldest let go and walked to again when asked, as 9p walks to a
6
+ //! dentry it holds no fid for.
7
+
8
+ use std::num::NonZeroUsize;
9
+ use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
10
+
11
+ use cap_std::fs::Dir;
12
+ use lru::LruCache;
13
+
14
+ use crate::vfs::Dentry;
15
+
16
+ /// The most directories a Bind keeps open for walks.
17
+ const MAX_CACHED_FIDS: NonZeroUsize = NonZeroUsize::new(64).expect("the bound is not zero");
18
+
19
+ /// A directory, and the dentry it was walked to for.
20
+ type CachedFid = (Weak<Dentry>, Arc<Dir>);
21
+
22
+ /// Directories a Bind walked to, by the address of the dentry naming each.
23
+ /// Each entry holds that dentry's allocation, so no other can take its address.
24
+ pub(super) struct FidCache(Mutex<LruCache<usize, CachedFid>>);
25
+
26
+ impl Default for FidCache {
27
+ fn default() -> Self {
28
+ Self(Mutex::new(LruCache::new(MAX_CACHED_FIDS)))
29
+ }
30
+ }
31
+
32
+ fn key(dentry: &Dentry) -> usize {
33
+ std::ptr::from_ref(dentry) as usize
34
+ }
35
+
36
+ impl FidCache {
37
+ fn entries(&self) -> MutexGuard<'_, LruCache<usize, CachedFid>> {
38
+ self.0.lock().unwrap_or_else(PoisonError::into_inner)
39
+ }
40
+
41
+ /// The directory kept for `dentry`, now the most recently asked.
42
+ pub(super) fn get(&self, dentry: &Dentry) -> Option<Arc<Dir>> {
43
+ self.entries().get(&key(dentry)).map(|(_, dir)| dir.clone())
44
+ }
45
+
46
+ /// Keep `dir` for `dentry`, letting go of the oldest past the bound.
47
+ pub(super) fn put(&self, dentry: &Dentry, dir: &Arc<Dir>) {
48
+ let entry = (Arc::downgrade(&dentry.get()), dir.clone());
49
+ self.entries().put(key(dentry), entry);
50
+ }
51
+
52
+ /// How many directories are kept.
53
+ #[cfg(all(test, unix))]
54
+ pub(super) fn len(&self) -> usize {
55
+ self.entries().len()
56
+ }
57
+ }
58
+
59
+ /// The host directory a guest holds open on a dentry, kept on it for as long
60
+ /// as the guest holds it, as 9p keeps a fid in `d_fsdata`.
61
+ #[derive(Default)]
62
+ pub(super) struct OpenDirFid(Mutex<Weak<Dir>>);
63
+
64
+ impl OpenDirFid {
65
+ /// The directory on `dentry`, while a guest holds it open.
66
+ pub(super) fn get(dentry: &Dentry) -> Option<Arc<Dir>> {
67
+ let kept = dentry.fsdata().get()?.clone().downcast::<Self>().ok()?;
68
+ let dir = kept.0.lock().unwrap_or_else(PoisonError::into_inner);
69
+ dir.upgrade()
70
+ }
71
+
72
+ /// `dir` is held open on `dentry` from now on.
73
+ pub(super) fn set(dentry: &Dentry, dir: &Arc<Dir>) {
74
+ let kept = dentry
75
+ .fsdata()
76
+ .get_or_init(|| Arc::new(Self::default()))
77
+ .clone();
78
+ if let Ok(kept) = kept.downcast::<Self>() {
79
+ *kept.0.lock().unwrap_or_else(PoisonError::into_inner) = Arc::downgrade(dir);
80
+ }
81
+ }
82
+ }