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,200 @@
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 Linux's `alloc_fd`: the lowest closed number
7
+ //! is reused before a fresh one is taken.
8
+
9
+ use std::collections::{BTreeMap, BTreeSet};
10
+ use std::sync::Arc;
11
+
12
+ use kobako_runtime::filesystem::FilesystemError;
13
+ use kobako_runtime::mount::Mount;
14
+ use kobako_runtime::profile::Profile;
15
+ use kobako_runtime::security::{restrict_self, WalkGuard};
16
+ use kobako_runtime::vfs::{Cred, File};
17
+
18
+ /// What one descriptor stands for, as Linux's fd table holds a `struct file`.
19
+ pub(crate) struct Descriptor {
20
+ pub(crate) file: File,
21
+ /// The guest path a preopen names; a Mount's own root has one.
22
+ pub(crate) preopen_path: Option<String>,
23
+ }
24
+
25
+ impl Descriptor {
26
+ /// The descriptor of `file`, naming no preopen.
27
+ pub(crate) fn for_file(file: File) -> Self {
28
+ Self {
29
+ file,
30
+ preopen_path: None,
31
+ }
32
+ }
33
+ }
34
+
35
+ pub(crate) struct Descriptors {
36
+ used: BTreeMap<u32, Descriptor>,
37
+ free: BTreeSet<u32>,
38
+ limit: Option<usize>,
39
+ /// Who the guest's calls are made for, as a task holds its `cred`.
40
+ cred: Arc<Cred>,
41
+ /// What a descriptor's call asks before it starts.
42
+ walk_guard: WalkGuard,
43
+ /// The isolation the guest runs under, which decides what it sees of
44
+ /// the times a Filesystem reports.
45
+ profile: Profile,
46
+ }
47
+
48
+ impl Descriptors {
49
+ /// The table every invocation starts from: stdin, then the two
50
+ /// Channels, then one preopen per Mount from descriptor 3. It holds at
51
+ /// most `limit` descriptors, those included, as a process's
52
+ /// `RLIMIT_NOFILE` counts every one it holds. Each Mount's root opens as
53
+ /// `opener`, the Cred the Driver prepared, before its Policy applies.
54
+ pub(crate) fn new(
55
+ stdin: File,
56
+ stdout: File,
57
+ stderr: File,
58
+ mounts: Vec<Mount>,
59
+ limit: Option<usize>,
60
+ opener: Arc<Cred>,
61
+ profile: Profile,
62
+ ) -> Result<Self, FilesystemError> {
63
+ let mut used = BTreeMap::from([
64
+ (0, Descriptor::for_file(stdin)),
65
+ (1, Descriptor::for_file(stdout)),
66
+ (2, Descriptor::for_file(stderr)),
67
+ ]);
68
+ // Each Mount's root opens before its Policy applies, as a Linux task
69
+ // opens what it keeps and then calls `landlock_restrict_self`, so a
70
+ // Policy judges only what the guest asks for.
71
+ for (fd, mount) in (3..).zip(&mounts) {
72
+ used.insert(
73
+ fd,
74
+ Descriptor {
75
+ file: File::root(mount.mnt(), &opener)?,
76
+ preopen_path: Some(mount.at().to_string()),
77
+ },
78
+ );
79
+ }
80
+ let cred = restrict_self(&opener, &mounts);
81
+ let walk_guard = WalkGuard::new(&mounts);
82
+ Ok(Self {
83
+ used,
84
+ free: BTreeSet::new(),
85
+ limit,
86
+ cred,
87
+ walk_guard,
88
+ profile,
89
+ })
90
+ }
91
+
92
+ /// Who the guest's calls are made for.
93
+ pub(crate) fn cred(&self) -> &Arc<Cred> {
94
+ &self.cred
95
+ }
96
+
97
+ /// What every call through a descriptor asks first, so a Policy judges the
98
+ /// path it stands at.
99
+ pub(crate) fn walk_guard(&self) -> &WalkGuard {
100
+ &self.walk_guard
101
+ }
102
+
103
+ /// The isolation the guest runs under.
104
+ pub(crate) fn profile(&self) -> Profile {
105
+ self.profile
106
+ }
107
+
108
+ pub(crate) fn get(&self, fd: u32) -> Option<&Descriptor> {
109
+ self.used.get(&fd)
110
+ }
111
+
112
+ pub(crate) fn get_mut(&mut self, fd: u32) -> Option<&mut Descriptor> {
113
+ self.used.get_mut(&fd)
114
+ }
115
+
116
+ /// Take `fd` out of the table, freeing its number for reuse.
117
+ pub(crate) fn remove(&mut self, fd: u32) -> Option<Descriptor> {
118
+ let descriptor = self.used.remove(&fd)?;
119
+ self.free.insert(fd);
120
+ Some(descriptor)
121
+ }
122
+
123
+ /// Place `descriptor` at `fd`, a number the caller has just freed or
124
+ /// `lowest_unused` found, as Linux's `fd_install` does.
125
+ pub(crate) fn place(&mut self, fd: u32, descriptor: Descriptor) {
126
+ self.free.remove(&fd);
127
+ self.used.insert(fd, descriptor);
128
+ }
129
+
130
+ /// The number the next descriptor takes, the lowest freed one or the one
131
+ /// past the highest in use, as Linux's `get_unused_fd_flags` finds it;
132
+ /// `None` when the table holds as many as it may. Nothing is placed
133
+ /// until `place`, as `fd_install` fills what was found.
134
+ pub(crate) fn lowest_unused(&self) -> Option<u32> {
135
+ if self.limit.is_some_and(|limit| self.used.len() >= limit) {
136
+ return None;
137
+ }
138
+ match self.free.first() {
139
+ Some(fd) => Some(*fd),
140
+ None => match self.used.last_key_value() {
141
+ Some((last, _)) => last.checked_add(1),
142
+ None => Some(0),
143
+ },
144
+ }
145
+ }
146
+
147
+ /// Add `descriptor` under the number `lowest_unused` finds; `None` when
148
+ /// the table is full.
149
+ #[cfg(test)]
150
+ pub(crate) fn push(&mut self, descriptor: Descriptor) -> Option<u32> {
151
+ let fd = self.lowest_unused()?;
152
+ self.place(fd, descriptor);
153
+ Some(fd)
154
+ }
155
+ }
156
+
157
+ #[cfg(test)]
158
+ mod tests {
159
+ use super::*;
160
+ use kobako_runtime::filesystem::{create_pipe_files, PipeInodeInfo};
161
+ use kobako_runtime::security::prepare_creds_within;
162
+
163
+ fn pipe_end() -> File {
164
+ create_pipe_files(&PipeInodeInfo::new(1), &prepare_creds_within(&[], None)).1
165
+ }
166
+
167
+ // @behavior RT-189
168
+ #[test]
169
+ fn a_new_descriptor_takes_the_lowest_closed_number() {
170
+ let mut table = Descriptors::new(
171
+ pipe_end(),
172
+ pipe_end(),
173
+ pipe_end(),
174
+ Vec::new(),
175
+ None,
176
+ prepare_creds_within(&[], None),
177
+ Profile::Hermetic,
178
+ )
179
+ .unwrap_or_else(|_| panic!("a table holding no Mount must make"));
180
+ let mut open = || {
181
+ table
182
+ .push(Descriptor::for_file(pipe_end()))
183
+ .expect("a table with no limit takes every descriptor")
184
+ };
185
+ let numbers = [open(), open(), open()];
186
+ assert_eq!(numbers, [3, 4, 5], "descriptors must number from 3");
187
+ table.remove(3);
188
+ table.remove(4);
189
+ assert_eq!(
190
+ (
191
+ table.push(Descriptor::for_file(pipe_end())),
192
+ table.push(Descriptor::for_file(pipe_end())),
193
+ table.push(Descriptor::for_file(pipe_end())),
194
+ ),
195
+ (Some(3), Some(4), Some(6)),
196
+ "path_open's descriptors must take the lowest closed number first, as Linux's alloc_fd \
197
+ does, and then the one past the highest"
198
+ );
199
+ }
200
+ }
@@ -9,29 +9,32 @@
9
9
 
10
10
  use std::path::Path;
11
11
  use std::sync::Arc;
12
- use std::time::Instant;
13
12
 
14
13
  use wasmtime::{
15
14
  AsContextMut, InstancePre as WtInstancePre, ResourceLimiter, Store as WtStore, TypedFunc,
16
15
  };
17
16
 
18
- use crate::cache::shared_engine;
17
+ use crate::cache::load_engine;
19
18
  use crate::config::Config;
20
19
  use crate::exports::{self, Exports};
20
+ use crate::guest::Guest;
21
21
  use crate::invocation::Invocation;
22
- use crate::{capture, guest_mem, instance_pre, io, trap};
22
+ use crate::wasi::Wasi;
23
+ use crate::{instance_pre, trap};
23
24
  use kobako_runtime::dispatch::DispatchHandler;
25
+ use kobako_runtime::endpoint;
24
26
  use kobako_runtime::error::{InvokeError, SetupError, Trap};
27
+ use kobako_runtime::grant::{Grant, GrantKind};
25
28
  use kobako_runtime::profile::Profile;
26
29
  use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
27
30
  use kobako_runtime::snapshot::{Capture, Completion, Snapshot, Usage};
28
- use kobako_transport::envelope::{CompileReply, Downcall, Preamble, Run, Snippet};
31
+ use kobako_transport::envelope::Preamble;
29
32
 
30
33
  /// The wasmtime execution unit behind one sandbox runtime.
31
34
  pub struct Driver {
32
35
  // Pre-linked instantiation template (import wiring, type checks,
33
36
  // and the ABI version check done once in
34
- // `instance_pre::cached_instance_pre`). Every invocation
37
+ // `instance_pre::load_instance_pre`). Every invocation
35
38
  // instantiates a fresh instance from it and discards the whole
36
39
  // Store afterwards — the per-invocation instance discipline.
37
40
  instance_pre: WtInstancePre<Invocation>,
@@ -45,15 +48,22 @@ impl Driver {
45
48
  /// neither leaves the driver.
46
49
  pub fn new(path: &Path, config: Config) -> Result<Self, SetupError> {
47
50
  Ok(Self {
48
- instance_pre: instance_pre::cached_instance_pre(path)?,
51
+ instance_pre: instance_pre::load_instance_pre(path, config.setup_timeout)?,
49
52
  config,
50
53
  })
51
54
  }
52
55
 
53
- fn new_store(&self) -> Result<WtStore<Invocation>, SetupError> {
54
- let mut store = WtStore::new(shared_engine()?, Invocation::new(self.config.memory_limit));
56
+ /// The deadline and the `wall_time` bracket open with the Store, so
57
+ /// instantiating the guest and writing its request count against the
58
+ /// `timeout` as the export call does.
59
+ fn new_store(&self, wasi: Wasi) -> Result<WtStore<Invocation>, SetupError> {
60
+ let mut store = WtStore::new(
61
+ load_engine()?,
62
+ Invocation::new(self.config.memory_limit, wasi),
63
+ );
55
64
  store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
56
- store.epoch_deadline_callback(trap::epoch_deadline_callback);
65
+ trap::arm_deadline(&mut store, self.config.timeout);
66
+ store.data_mut().start_wall_clock();
57
67
  Ok(store)
58
68
  }
59
69
 
@@ -63,7 +73,11 @@ impl Driver {
63
73
  let instance = self
64
74
  .instance_pre
65
75
  .instantiate(store.as_context_mut())
66
- .map_err(|e| Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}")))?;
76
+ .map_err(|e| {
77
+ trap::cap_trap(&e).unwrap_or_else(|| {
78
+ Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}"))
79
+ })
80
+ })?;
67
81
  let exports = Exports::resolve(&instance, store.as_context_mut());
68
82
  store.data_mut().set_exports(exports.clone());
69
83
  Ok(exports)
@@ -83,58 +97,46 @@ impl Driver {
83
97
  Params: wasmtime::WasmParams,
84
98
  Results: wasmtime::WasmResults,
85
99
  {
86
- self.prime_caps(store, exports);
87
- let result = export.call(store.as_context_mut(), params);
100
+ self.arm_memory_cap(store, exports);
101
+ // A wait for a lock the call meets ends at the same deadline the guest's
102
+ // code does, so another Sandbox holding it cannot keep this one past it.
103
+ let deadline = store.data().deadline();
104
+ let result = kobako_runtime::sched::run_task(deadline, || {
105
+ export.call(store.as_context_mut(), params)
106
+ });
88
107
  disarm_caps(store);
89
108
  result
90
109
  }
91
110
 
92
111
  /// The pre-initialized image's allocation is folded into the memory
93
- /// baseline rather than the budget.
94
- fn prime_caps(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
95
- match self.config.timeout {
96
- Some(timeout) => {
97
- let deadline = Instant::now() + timeout;
98
- store.data_mut().set_deadline(Some(deadline));
99
- store.set_epoch_deadline(1);
100
- }
101
- None => {
102
- store.data_mut().set_deadline(None);
103
- store.set_epoch_deadline(trap::NO_TIMEOUT_EPOCH_DELTA);
104
- }
105
- }
112
+ /// baseline rather than the budget, so the cap arms only once the
113
+ /// image is in place.
114
+ fn arm_memory_cap(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
106
115
  let baseline = match exports.memory {
107
116
  Some(m) => m.data_size(store.as_context_mut()),
108
117
  None => 0,
109
118
  };
110
119
  store.data_mut().arm_memory_cap(baseline);
111
- store.data_mut().start_wall_clock();
112
120
  }
113
121
 
114
122
  /// Built the same for every `completion`: captures and usage must
115
123
  /// survive a trap just as they do an outcome.
116
124
  fn build_snapshot(&self, store: &WtStore<Invocation>, completion: Completion) -> Snapshot {
117
125
  let data = store.data();
118
- let usage = Usage {
119
- wall_time: data.wall_time().as_secs_f64(),
120
- memory_peak: data.memory_peak(),
126
+ let mut usage = Usage::default();
127
+ usage.wall_time = data.wall_time().as_secs_f64();
128
+ usage.memory_peak = data.memory_peak();
129
+ let clip = |bytes, limit| {
130
+ let mut capture = Capture::default();
131
+ (capture.bytes, capture.truncated) = endpoint::clip_capture(bytes, limit);
132
+ capture
121
133
  };
122
- let (stdout_bytes, stdout_truncated) =
123
- capture::clip_capture(data.stdout_bytes(), self.config.stdout_limit);
124
- let (stderr_bytes, stderr_truncated) =
125
- capture::clip_capture(data.stderr_bytes(), self.config.stderr_limit);
126
- Snapshot {
134
+ Snapshot::new(
127
135
  completion,
128
- stdout: Capture {
129
- bytes: stdout_bytes,
130
- truncated: stdout_truncated,
131
- },
132
- stderr: Capture {
133
- bytes: stderr_bytes,
134
- truncated: stderr_truncated,
135
- },
136
+ clip(data.wasi().stdout_bytes(), self.config.stdout_limit),
137
+ clip(data.wasi().stderr_bytes(), self.config.stderr_limit),
136
138
  usage,
137
- }
139
+ )
138
140
  }
139
141
  }
140
142
 
@@ -148,135 +150,68 @@ impl ContractRuntime for Driver {
148
150
  entry: Entry<'_>,
149
151
  preamble: &Preamble,
150
152
  handler: Option<Arc<dyn DispatchHandler>>,
153
+ grants: &[Grant],
151
154
  ) -> Result<Snapshot, InvokeError> {
152
- let mut store = self.new_store()?;
155
+ let stdin =
156
+ endpoint::stdin_content(preamble).map_err(|msg| Trap::Other(msg.to_string()))?;
157
+ let wasi = Wasi::for_invocation(&self.config, stdin, grants)?;
158
+ let mut store = self.new_store(wasi)?;
153
159
  if let Some(handler) = handler {
154
160
  store.data_mut().bind_on_dispatch(handler);
155
161
  }
156
- io::install_wasi(&mut store, &self.config, &preamble.encode())?;
157
162
  let exports = self.instantiate(&mut store)?;
158
163
  let downcall = exports::require(exports.downcall.as_ref())?;
159
- let request = match entry {
160
- Entry::Eval { source } => Downcall::Eval(source.to_vec()),
161
- Entry::Run {
162
- entrypoint,
163
- payload,
164
- } => Downcall::Run(Run {
165
- entrypoint: entrypoint.to_owned(),
166
- payload,
167
- }),
168
- }
169
- .encode();
170
- let (ptr, len) = io::write_request(&mut store, &exports, &request)?;
164
+ let request = endpoint::request_of(entry);
165
+ let (ptr, len) = endpoint::write_request(&mut guest_of(&mut store), &request)?;
171
166
  let called = self.call_with_caps(&mut store, &exports, downcall, (ptr, len));
172
167
  let completion = match called {
173
- Ok(packed) => match io::read_outcome(&store, &exports, packed) {
168
+ Ok(packed) => match endpoint::read_outcome(&mut guest_of(&mut store), packed) {
174
169
  Ok(outcome) => Completion::Outcome(outcome),
175
170
  Err(t) => Completion::Trap(t),
176
171
  },
177
- Err(e) => Completion::Trap(trap::trap_from(e)),
172
+ Err(e) => Completion::Trap(trap::to_trap(e)),
178
173
  };
179
174
  Ok(self.build_snapshot(&store, completion))
180
175
  }
181
176
 
182
177
  /// Exactly the rung `Config` requested. `Hermetic` freezes ambient time
183
- /// and entropy; both rungs wire no filesystem, environment, or network,
178
+ /// and entropy; both rungs wire no ambient filesystem, environment, or network,
184
179
  /// and the linker adds only `__kobako_upcall` beyond that WASI surface.
185
180
  fn profile(&self) -> Profile {
186
181
  self.config.profile
187
182
  }
188
183
 
184
+ /// A Mount becomes a preopen of kobako's own descriptor table, which
185
+ /// answers every preview1 call beneath it.
186
+ fn supports(&self, kind: GrantKind) -> bool {
187
+ matches!(kind, GrantKind::Mount)
188
+ }
189
+
189
190
  /// Bounded by the same caps as an invocation, on an instance of its
190
191
  /// own that no script ever runs on, so what it answers carries
191
192
  /// nothing a script could have written.
192
193
  fn compile(&self, preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
193
- let mut store = self.new_store().ok()?;
194
- io::install_wasi(&mut store, &self.config, &preamble.encode()).ok()?;
194
+ let wasi = Wasi::for_compile(&self.config, endpoint::stdin_content(preamble).ok()?).ok()?;
195
+ let mut store = self.new_store(wasi).ok()?;
195
196
  let exports = self.instantiate(&mut store).ok()?;
196
197
  let downcall = exports::require(exports.downcall.as_ref()).ok()?;
197
198
  let (ptr, len) =
198
- io::write_request(&mut store, &exports, &Downcall::Compile.encode()).ok()?;
199
+ endpoint::write_request(&mut guest_of(&mut store), &endpoint::compile_request())
200
+ .ok()?;
199
201
  let packed = self
200
202
  .call_with_caps(&mut store, &exports, downcall, (ptr, len))
201
203
  .ok()?;
202
- match io::read_compile_reply(&store, &exports, packed).ok()? {
203
- CompileReply::Ok(bytecodes) => one_per_source(preamble, bytecodes)
204
- .filter(|bytecodes| replay_fits(preamble, bytecodes)),
205
- CompileReply::Error(_) | CompileReply::Unsupported(_) => None,
206
- }
207
- }
208
- }
209
-
210
- /// Each invocation sends the Preamble with every bytecode in place of its
211
- /// source, so an answer that would carry it past the payload cap would trap
212
- /// every invocation the source alone fits.
213
- fn replay_fits(preamble: &Preamble, bytecodes: &[Vec<u8>]) -> bool {
214
- let mut replay = preamble.clone();
215
- let sources = replay
216
- .snippets
217
- .entries
218
- .iter_mut()
219
- .filter(|entry| matches!(entry, Snippet::Source { .. }));
220
- for (entry, body) in sources.zip(bytecodes) {
221
- *entry = Snippet::Bytecode { body: body.clone() };
204
+ let reply = endpoint::read_compile_reply(&mut guest_of(&mut store), packed).ok()?;
205
+ endpoint::compiled_bytecodes(preamble, reply)
222
206
  }
223
- guest_mem::checked_payload_len(replay.encode().len()).is_ok()
224
207
  }
225
208
 
226
- /// A frontend replays each bytecode in place of one source snippet, in
227
- /// order, so an answer of any other length leaves nothing to replay.
228
- fn one_per_source(preamble: &Preamble, bytecodes: Vec<Vec<u8>>) -> Option<Vec<Vec<u8>>> {
229
- let sources = preamble
230
- .snippets
231
- .entries
232
- .iter()
233
- .filter(|entry| matches!(entry, Snippet::Source { .. }))
234
- .count();
235
- (bytecodes.len() == sources).then_some(bytecodes)
209
+ /// The instance the runtime's host ABI endpoint drives, through `store`.
210
+ fn guest_of(store: &mut WtStore<Invocation>) -> Guest<'_> {
211
+ Guest(store.as_context_mut())
236
212
  }
237
213
 
238
214
  fn disarm_caps(store: &mut WtStore<Invocation>) {
239
215
  store.data_mut().stop_wall_clock();
240
216
  store.data_mut().disarm_memory_cap();
241
217
  }
242
-
243
- #[cfg(test)]
244
- mod tests {
245
- use kobako_transport::envelope::Snippets;
246
-
247
- use super::*;
248
-
249
- /// One source between two bytecode snippets: a compile answers one
250
- /// bytecode for it.
251
- fn one_source() -> Preamble {
252
- let mut preamble = Preamble::default();
253
- preamble.snippets = Snippets {
254
- entries: vec![
255
- Snippet::Bytecode { body: vec![0xaa] },
256
- Snippet::Source {
257
- name: "Only".into(),
258
- body: "1".into(),
259
- },
260
- Snippet::Bytecode { body: vec![0xbb] },
261
- ],
262
- };
263
- preamble
264
- }
265
-
266
- // @behavior WE-089
267
- #[test]
268
- fn a_compiled_answer_of_another_length_leaves_nothing_compiled() {
269
- let preamble = one_source();
270
-
271
- assert_eq!(
272
- [
273
- one_per_source(&preamble, Vec::new()),
274
- one_per_source(&preamble, vec![vec![0x01], vec![0x02]]),
275
- one_per_source(&preamble, vec![vec![0x01]]),
276
- ],
277
- [None, None, Some(vec![vec![0x01]])],
278
- "a compiled answer through Driver#compile must be kept only when it carries one \
279
- bytecode per source snippet"
280
- );
281
- }
282
- }
@@ -10,19 +10,13 @@
10
10
 
11
11
  use wasmtime::{AsContextMut, Instance as WtInstance, Memory, TypedFunc};
12
12
 
13
- use crate::guest_mem::SANDBOX_RUNTIME_NOT_KOBAKO;
13
+ use kobako_runtime::endpoint::RUNTIME_INCOMPATIBLE;
14
14
  use kobako_runtime::error::Trap;
15
15
 
16
- /// For a required guest export that is absent or mistyped, wherever it is
17
- /// looked up. Names neither the export nor a frontend's artifact path;
18
- /// what a caller can do is rebuild the runtime.
19
- pub(crate) const RUNTIME_INCOMPATIBLE: &str =
20
- "the Sandbox runtime is incompatible; rebuild it against the kobako version in use";
21
-
22
16
  /// The resolved host-driven export handles. Each is `Option` because test
23
17
  /// fixtures (a minimal "ping" module) need not provide them; real
24
- /// `kobako.wasm` always does, and the run path surfaces a `Trap` (via
25
- /// `require` / `Exports::require_memory`) when a handle is `None`.
18
+ /// `kobako.wasm` always does, and the run path surfaces a `Trap` when a
19
+ /// handle is `None`.
26
20
  ///
27
21
  /// The handles are indices into the owning Store, not borrows of the
28
22
  /// `Instance` — they stay valid for the Store's lifetime, which is why
@@ -49,11 +43,6 @@ impl Exports {
49
43
  memory: instance.get_memory(&mut ctx, "memory"),
50
44
  }
51
45
  }
52
-
53
- pub(crate) fn require_memory(&self) -> Result<Memory, Trap> {
54
- self.memory
55
- .ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_NOT_KOBAKO.to_string()))
56
- }
57
46
  }
58
47
 
59
48
  pub(crate) fn require<Params, Results>(
@@ -77,7 +66,7 @@ mod tests {
77
66
 
78
67
  use wasmtime::ExternType;
79
68
 
80
- use crate::cache::cached_module;
69
+ use crate::cache::load_module;
81
70
 
82
71
  const WASM: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../data/kobako.wasm");
83
72
 
@@ -98,7 +87,7 @@ mod tests {
98
87
  );
99
88
  return;
100
89
  }
101
- let module = cached_module(path).expect("the Guest Binary must load");
90
+ let module = load_module(path).expect("the Guest Binary must load");
102
91
 
103
92
  let mut functions: Vec<&str> = module
104
93
  .exports()
@@ -0,0 +1,38 @@
1
+ //! One wasmtime instance as the runtime's host ABI endpoint drives it, from the
2
+ //! invocation body's `Store` and from a host import's `Caller` alike.
3
+
4
+ use kobako_runtime::endpoint::{Calls, GuestInstance};
5
+ use kobako_runtime::error::Trap;
6
+ use wasmtime::StoreContextMut;
7
+
8
+ use crate::invocation::Invocation;
9
+ use crate::trap;
10
+
11
+ pub(crate) struct Guest<'a>(pub(crate) StoreContextMut<'a, Invocation>);
12
+
13
+ impl GuestInstance for Guest<'_> {
14
+ type Fault = wasmtime::Error;
15
+
16
+ fn calls(&mut self) -> &mut Calls<wasmtime::Error> {
17
+ self.0.data_mut().calls_mut()
18
+ }
19
+
20
+ fn memory(&mut self) -> Option<&mut [u8]> {
21
+ let memory = self.0.data().exports()?.memory?;
22
+ Some(memory.data_mut(&mut self.0))
23
+ }
24
+
25
+ fn alloc(&mut self, len: u32) -> Option<wasmtime::Result<u32>> {
26
+ let alloc = self.0.data().exports()?.alloc?;
27
+ Some(alloc.call(&mut self.0, len))
28
+ }
29
+
30
+ fn downcall(&mut self, ptr: i32, len: i32) -> Option<wasmtime::Result<u64>> {
31
+ let downcall = self.0.data().exports()?.downcall?;
32
+ Some(downcall.call(&mut self.0, (ptr, len)))
33
+ }
34
+
35
+ fn cap_trap(fault: &wasmtime::Error) -> Option<Trap> {
36
+ trap::cap_trap(fault)
37
+ }
38
+ }
@@ -0,0 +1,52 @@
1
+ //! The host imports a Guest Binary resolves: the WASI preview1 set and
2
+ //! the wire ABI's `__kobako_upcall`.
3
+ //!
4
+ //! Both closures read all their state from the `Invocation` inside the
5
+ //! calling Store, never from a Runtime, so the wiring is the same for
6
+ //! every Sandbox and `crate::instance_pre` can cache what it resolves.
7
+
8
+ use wasmtime::{AsContextMut, Caller, Linker};
9
+ use wasmtime_wasi::p1;
10
+
11
+ use crate::cache::load_engine;
12
+ use crate::guest::Guest;
13
+ use crate::invocation::Invocation;
14
+ use crate::preview1;
15
+ use kobako_runtime::endpoint::{self, UpcallEnd, ABANDONED};
16
+ use kobako_runtime::error::SetupError;
17
+
18
+ pub(crate) fn linker() -> Result<Linker<Invocation>, SetupError> {
19
+ let mut linker: Linker<Invocation> = Linker::new(load_engine()?);
20
+
21
+ // Wire the wasmtime-wasi preview1 WASI imports against each
22
+ // invocation's own `Wasi` context, then answer every call that reads
23
+ // a descriptor from kobako's own table, which routes guest fd 1/2 to
24
+ // its capture pipes.
25
+ p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| {
26
+ state.wasi_mut().ctx_mut()
27
+ })
28
+ .and_then(|_| preview1::shadow(&mut linker))
29
+ .map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
30
+
31
+ // `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` in the ABI;
32
+ // `endpoint::handle` owns what it answers.
33
+ linker
34
+ .func_wrap(
35
+ "env",
36
+ "__kobako_upcall",
37
+ |mut caller: Caller<'_, Invocation>,
38
+ req_ptr: i32,
39
+ req_len: i32|
40
+ -> wasmtime::Result<i64> {
41
+ let mut guest = Guest(caller.as_context_mut());
42
+ endpoint::handle(&mut guest, req_ptr, req_len).map_err(|end| match end {
43
+ UpcallEnd::Fault(trap) => trap,
44
+ UpcallEnd::Abandoned => wasmtime::Error::msg(ABANDONED),
45
+ UpcallEnd::Unwritable(reason) => wasmtime::Error::msg(reason),
46
+ })
47
+ },
48
+ )
49
+ .map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
50
+
51
+ Ok(linker)
52
+ }