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
@@ -3,123 +3,106 @@
3
3
  //!
4
4
  //! kobako answers every preview1 call that reads this table, so the guest
5
5
  //! sees one table and no call falls through to a second one that
6
- //! disagrees. Numbering follows wasmtime-wasi's: a closed number is reused
7
- //! before a fresh one is taken.
6
+ //! disagrees. Numbering follows Linux's `alloc_fd`: the lowest closed number
7
+ //! is reused before a fresh one is taken.
8
8
 
9
9
  use std::collections::{BTreeMap, BTreeSet};
10
10
  use std::sync::Arc;
11
11
 
12
- use kobako_runtime::filesystem::{DirEntry, File, Hold, Metadata};
12
+ use kobako_runtime::filesystem::FilesystemError;
13
13
  use kobako_runtime::mount::Mount;
14
- use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
15
-
16
- /// What one descriptor stands for.
17
- pub(crate) enum Descriptor {
18
- /// Descriptor 0, carrying the Preamble.
19
- Stdin(MemoryInputPipe),
20
- /// Descriptor 1 or 2, a capture pipe the guest writes a Channel into.
21
- Channel(MemoryOutputPipe),
22
- /// A directory beneath a Mount; a preopen is the Mount's own root.
23
- Directory(OpenDirectory),
24
- File(OpenFile),
25
- }
14
+ use kobako_runtime::profile::Profile;
15
+ use kobako_runtime::security::{restrict_self, WalkGuard};
16
+ use kobako_runtime::vfs::{Cred, File};
26
17
 
27
- /// What a directory is, apart from where it stands: the device and inode
28
- /// numbers its Filesystem names it by.
29
- #[derive(Clone, Copy, Debug, PartialEq, Eq)]
30
- pub(crate) struct Identity {
31
- dev: u64,
32
- ino: u64,
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>,
33
23
  }
34
24
 
35
- impl Identity {
36
- /// The identity `metadata` names, or `None` where its inode of zero
37
- /// reports none.
38
- pub(crate) fn of(metadata: &Metadata) -> Option<Self> {
39
- (metadata.ino != 0).then_some(Self {
40
- dev: metadata.dev,
41
- ino: metadata.ino,
42
- })
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
+ }
43
32
  }
44
33
  }
45
34
 
46
- pub(crate) struct OpenDirectory {
47
- pub(crate) mount: Arc<Mount>,
48
- /// The directory's path relative to the mount's root.
49
- pub(crate) path: String,
50
- /// The identity the directory had when it was opened, so one made in
51
- /// its place is not taken for it; without one it follows its path.
52
- pub(crate) identity: Option<Identity>,
53
- /// What keeps that identity from passing to a directory made in this
54
- /// one's place while the descriptor is open.
55
- pub(crate) _hold: Option<Hold>,
56
- pub(crate) preopen: bool,
57
- /// The entries a listing began with, held until it reaches its end, so
58
- /// each later call reads on without listing the directory again.
59
- pub(crate) listing: Option<Vec<DirEntry>>,
60
- }
61
-
62
- pub(crate) struct OpenFile {
63
- pub(crate) file: Box<dyn File>,
64
- pub(crate) position: u64,
65
- pub(crate) append: bool,
66
- pub(crate) readable: bool,
67
- pub(crate) writable: bool,
68
- /// The Mount it was opened beneath, whose permission to write answers a
69
- /// wait to write and a resize, as wasmtime-wasi answers both by its
70
- /// preopen's permissions.
71
- pub(crate) mount: Arc<Mount>,
72
- }
73
-
74
35
  pub(crate) struct Descriptors {
75
36
  used: BTreeMap<u32, Descriptor>,
76
37
  free: BTreeSet<u32>,
77
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,
78
46
  }
79
47
 
80
48
  impl Descriptors {
81
49
  /// The table every invocation starts from: stdin, then the two
82
50
  /// Channels, then one preopen per Mount from descriptor 3. It holds at
83
51
  /// most `limit` descriptors, those included, as a process's
84
- /// `RLIMIT_NOFILE` counts every one it holds.
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.
85
54
  pub(crate) fn new(
86
- stdin: MemoryInputPipe,
87
- stdout: MemoryOutputPipe,
88
- stderr: MemoryOutputPipe,
55
+ stdin: File,
56
+ stdout: File,
57
+ stderr: File,
89
58
  mounts: Vec<Mount>,
90
59
  limit: Option<usize>,
91
- ) -> Self {
60
+ opener: Arc<Cred>,
61
+ profile: Profile,
62
+ ) -> Result<Self, FilesystemError> {
92
63
  let mut used = BTreeMap::from([
93
- (0, Descriptor::Stdin(stdin)),
94
- (1, Descriptor::Channel(stdout)),
95
- (2, Descriptor::Channel(stderr)),
64
+ (0, Descriptor::for_file(stdin)),
65
+ (1, Descriptor::for_file(stdout)),
66
+ (2, Descriptor::for_file(stderr)),
96
67
  ]);
97
- for (fd, mount) in (3..).zip(mounts) {
98
- // A root the Filesystem cannot stat or hold reports no
99
- // identity, and the descriptor follows its path alone.
100
- let filesystem = mount.filesystem();
101
- let identity = filesystem
102
- .metadata("")
103
- .ok()
104
- .and_then(|root| Identity::of(&root));
105
- let hold = filesystem.hold_directory("").ok().flatten();
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) {
106
72
  used.insert(
107
73
  fd,
108
- Descriptor::Directory(OpenDirectory {
109
- mount: Arc::new(mount),
110
- path: String::new(),
111
- identity,
112
- _hold: hold,
113
- preopen: true,
114
- listing: None,
115
- }),
74
+ Descriptor {
75
+ file: File::root(mount.mnt(), &opener)?,
76
+ preopen_path: Some(mount.at().to_string()),
77
+ },
116
78
  );
117
79
  }
118
- Self {
80
+ let cred = restrict_self(&opener, &mounts);
81
+ let walk_guard = WalkGuard::new(&mounts);
82
+ Ok(Self {
119
83
  used,
120
84
  free: BTreeSet::new(),
121
85
  limit,
122
- }
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
123
106
  }
124
107
 
125
108
  pub(crate) fn get(&self, fd: u32) -> Option<&Descriptor> {
@@ -137,31 +120,81 @@ impl Descriptors {
137
120
  Some(descriptor)
138
121
  }
139
122
 
140
- /// Place `descriptor` at `fd`, a number the caller has just freed.
123
+ /// Place `descriptor` at `fd`, a number the caller has just freed or
124
+ /// `lowest_unused` found, as Linux's `fd_install` does.
141
125
  pub(crate) fn place(&mut self, fd: u32, descriptor: Descriptor) {
142
126
  self.free.remove(&fd);
143
127
  self.used.insert(fd, descriptor);
144
128
  }
145
129
 
146
- /// Whether the table holds as many descriptors as it may.
147
- pub(crate) fn is_full(&self) -> bool {
148
- self.limit.is_some_and(|limit| self.used.len() >= limit)
149
- }
150
-
151
- /// Add `descriptor` under the highest freed number, or the one past
152
- /// the highest in use; `None` when the table is full.
153
- pub(crate) fn push(&mut self, descriptor: Descriptor) -> Option<u32> {
154
- if self.is_full() {
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) {
155
136
  return None;
156
137
  }
157
- let fd = match self.free.pop_last() {
158
- Some(fd) => fd,
138
+ match self.free.first() {
139
+ Some(fd) => Some(*fd),
159
140
  None => match self.used.last_key_value() {
160
- Some((last, _)) => last.checked_add(1)?,
161
- None => 0,
141
+ Some((last, _)) => last.checked_add(1),
142
+ None => Some(0),
162
143
  },
163
- };
164
- self.used.insert(fd, descriptor);
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);
165
153
  Some(fd)
166
154
  }
167
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
+ }
@@ -14,25 +14,27 @@ use wasmtime::{
14
14
  AsContextMut, InstancePre as WtInstancePre, ResourceLimiter, Store as WtStore, TypedFunc,
15
15
  };
16
16
 
17
- use crate::cache::shared_engine;
17
+ use crate::cache::load_engine;
18
18
  use crate::config::Config;
19
19
  use crate::exports::{self, Exports};
20
+ use crate::guest::Guest;
20
21
  use crate::invocation::Invocation;
21
22
  use crate::wasi::Wasi;
22
- use crate::{capture, guest_mem, instance_pre, io, trap};
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};
25
27
  use kobako_runtime::grant::{Grant, GrantKind};
26
28
  use kobako_runtime::profile::Profile;
27
29
  use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
28
30
  use kobako_runtime::snapshot::{Capture, Completion, Snapshot, Usage};
29
- use kobako_transport::envelope::{CompileReply, Downcall, Preamble, Run, Snippet};
31
+ use kobako_transport::envelope::Preamble;
30
32
 
31
33
  /// The wasmtime execution unit behind one sandbox runtime.
32
34
  pub struct Driver {
33
35
  // Pre-linked instantiation template (import wiring, type checks,
34
36
  // and the ABI version check done once in
35
- // `instance_pre::cached_instance_pre`). Every invocation
37
+ // `instance_pre::load_instance_pre`). Every invocation
36
38
  // instantiates a fresh instance from it and discards the whole
37
39
  // Store afterwards — the per-invocation instance discipline.
38
40
  instance_pre: WtInstancePre<Invocation>,
@@ -46,7 +48,7 @@ impl Driver {
46
48
  /// neither leaves the driver.
47
49
  pub fn new(path: &Path, config: Config) -> Result<Self, SetupError> {
48
50
  Ok(Self {
49
- instance_pre: instance_pre::cached_instance_pre(path, config.setup_timeout)?,
51
+ instance_pre: instance_pre::load_instance_pre(path, config.setup_timeout)?,
50
52
  config,
51
53
  })
52
54
  }
@@ -56,7 +58,7 @@ impl Driver {
56
58
  /// `timeout` as the export call does.
57
59
  fn new_store(&self, wasi: Wasi) -> Result<WtStore<Invocation>, SetupError> {
58
60
  let mut store = WtStore::new(
59
- shared_engine()?,
61
+ load_engine()?,
60
62
  Invocation::new(self.config.memory_limit, wasi),
61
63
  );
62
64
  store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
@@ -96,7 +98,12 @@ impl Driver {
96
98
  Results: wasmtime::WasmResults,
97
99
  {
98
100
  self.arm_memory_cap(store, exports);
99
- let result = export.call(store.as_context_mut(), params);
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
+ });
100
107
  disarm_caps(store);
101
108
  result
102
109
  }
@@ -121,7 +128,7 @@ impl Driver {
121
128
  usage.memory_peak = data.memory_peak();
122
129
  let clip = |bytes, limit| {
123
130
  let mut capture = Capture::default();
124
- (capture.bytes, capture.truncated) = capture::clip_capture(bytes, limit);
131
+ (capture.bytes, capture.truncated) = endpoint::clip_capture(bytes, limit);
125
132
  capture
126
133
  };
127
134
  Snapshot::new(
@@ -145,28 +152,20 @@ impl ContractRuntime for Driver {
145
152
  handler: Option<Arc<dyn DispatchHandler>>,
146
153
  grants: &[Grant],
147
154
  ) -> Result<Snapshot, InvokeError> {
148
- let wasi = Wasi::for_invocation(&self.config, &preamble.encode(), grants)?;
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)?;
149
158
  let mut store = self.new_store(wasi)?;
150
159
  if let Some(handler) = handler {
151
160
  store.data_mut().bind_on_dispatch(handler);
152
161
  }
153
162
  let exports = self.instantiate(&mut store)?;
154
163
  let downcall = exports::require(exports.downcall.as_ref())?;
155
- let request = match entry {
156
- Entry::Eval { source } => Downcall::Eval(source.to_vec()),
157
- Entry::Run {
158
- entrypoint,
159
- payload,
160
- } => Downcall::Run(Run {
161
- entrypoint: entrypoint.to_owned(),
162
- payload,
163
- }),
164
- }
165
- .encode();
166
- 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)?;
167
166
  let called = self.call_with_caps(&mut store, &exports, downcall, (ptr, len));
168
167
  let completion = match called {
169
- Ok(packed) => match io::read_outcome(&store, &exports, packed) {
168
+ Ok(packed) => match endpoint::read_outcome(&mut guest_of(&mut store), packed) {
170
169
  Ok(outcome) => Completion::Outcome(outcome),
171
170
  Err(t) => Completion::Trap(t),
172
171
  },
@@ -192,93 +191,27 @@ impl ContractRuntime for Driver {
192
191
  /// own that no script ever runs on, so what it answers carries
193
192
  /// nothing a script could have written.
194
193
  fn compile(&self, preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
195
- let wasi = Wasi::for_compile(&self.config, &preamble.encode()).ok()?;
194
+ let wasi = Wasi::for_compile(&self.config, endpoint::stdin_content(preamble).ok()?).ok()?;
196
195
  let mut store = self.new_store(wasi).ok()?;
197
196
  let exports = self.instantiate(&mut store).ok()?;
198
197
  let downcall = exports::require(exports.downcall.as_ref()).ok()?;
199
198
  let (ptr, len) =
200
- io::write_request(&mut store, &exports, &Downcall::Compile.encode()).ok()?;
199
+ endpoint::write_request(&mut guest_of(&mut store), &endpoint::compile_request())
200
+ .ok()?;
201
201
  let packed = self
202
202
  .call_with_caps(&mut store, &exports, downcall, (ptr, len))
203
203
  .ok()?;
204
- match io::read_compile_reply(&store, &exports, packed).ok()? {
205
- CompileReply::Ok(bytecodes) => one_per_source(preamble, bytecodes)
206
- .filter(|bytecodes| replay_fits(preamble, bytecodes)),
207
- CompileReply::Error(_) | CompileReply::Unsupported(_) => None,
208
- }
209
- }
210
- }
211
-
212
- /// Each invocation sends the Preamble with every bytecode in place of its
213
- /// source, so an answer that would carry it past the payload cap would trap
214
- /// every invocation the source alone fits.
215
- fn replay_fits(preamble: &Preamble, bytecodes: &[Vec<u8>]) -> bool {
216
- let mut replay = preamble.clone();
217
- let sources = replay
218
- .snippets
219
- .entries
220
- .iter_mut()
221
- .filter(|entry| matches!(entry, Snippet::Source { .. }));
222
- for (entry, body) in sources.zip(bytecodes) {
223
- *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)
224
206
  }
225
- guest_mem::checked_payload_len(replay.encode().len()).is_ok()
226
207
  }
227
208
 
228
- /// A frontend replays each bytecode in place of one source snippet, in
229
- /// order, so an answer of any other length leaves nothing to replay.
230
- fn one_per_source(preamble: &Preamble, bytecodes: Vec<Vec<u8>>) -> Option<Vec<Vec<u8>>> {
231
- let sources = preamble
232
- .snippets
233
- .entries
234
- .iter()
235
- .filter(|entry| matches!(entry, Snippet::Source { .. }))
236
- .count();
237
- (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())
238
212
  }
239
213
 
240
214
  fn disarm_caps(store: &mut WtStore<Invocation>) {
241
215
  store.data_mut().stop_wall_clock();
242
216
  store.data_mut().disarm_memory_cap();
243
217
  }
244
-
245
- #[cfg(test)]
246
- mod tests {
247
- use kobako_transport::envelope::Snippets;
248
-
249
- use super::*;
250
-
251
- /// One source between two bytecode snippets: a compile answers one
252
- /// bytecode for it.
253
- fn one_source() -> Preamble {
254
- let mut preamble = Preamble::default();
255
- preamble.snippets = Snippets {
256
- entries: vec![
257
- Snippet::Bytecode { body: vec![0xaa] },
258
- Snippet::Source {
259
- name: "Only".into(),
260
- body: "1".into(),
261
- },
262
- Snippet::Bytecode { body: vec![0xbb] },
263
- ],
264
- };
265
- preamble
266
- }
267
-
268
- // @behavior WE-089
269
- #[test]
270
- fn a_compiled_answer_of_another_length_leaves_nothing_compiled() {
271
- let preamble = one_source();
272
-
273
- assert_eq!(
274
- [
275
- one_per_source(&preamble, Vec::new()),
276
- one_per_source(&preamble, vec![vec![0x01], vec![0x02]]),
277
- one_per_source(&preamble, vec![vec![0x01]]),
278
- ],
279
- [None, None, Some(vec![vec![0x01]])],
280
- "a compiled answer through Driver#compile must be kept only when it carries one \
281
- bytecode per source snippet"
282
- );
283
- }
284
- }
@@ -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
+ }
@@ -5,16 +5,18 @@
5
5
  //! calling Store, never from a Runtime, so the wiring is the same for
6
6
  //! every Sandbox and `crate::instance_pre` can cache what it resolves.
7
7
 
8
- use wasmtime::{Caller, Linker};
8
+ use wasmtime::{AsContextMut, Caller, Linker};
9
9
  use wasmtime_wasi::p1;
10
10
 
11
- use crate::cache::shared_engine;
11
+ use crate::cache::load_engine;
12
+ use crate::guest::Guest;
12
13
  use crate::invocation::Invocation;
13
- use crate::{dispatch, preview1};
14
+ use crate::preview1;
15
+ use kobako_runtime::endpoint::{self, UpcallEnd, ABANDONED};
14
16
  use kobako_runtime::error::SetupError;
15
17
 
16
18
  pub(crate) fn linker() -> Result<Linker<Invocation>, SetupError> {
17
- let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
19
+ let mut linker: Linker<Invocation> = Linker::new(load_engine()?);
18
20
 
19
21
  // Wire the wasmtime-wasi preview1 WASI imports against each
20
22
  // invocation's own `Wasi` context, then answer every call that reads
@@ -26,9 +28,8 @@ pub(crate) fn linker() -> Result<Linker<Invocation>, SetupError> {
26
28
  .and_then(|_| preview1::shadow(&mut linker))
27
29
  .map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
28
30
 
29
- // `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` per
30
- // docs/wire/abi.md § ABI Signatures; `dispatch::handle` owns what it
31
- // answers.
31
+ // `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` in the ABI;
32
+ // `endpoint::handle` owns what it answers.
32
33
  linker
33
34
  .func_wrap(
34
35
  "env",
@@ -37,7 +38,12 @@ pub(crate) fn linker() -> Result<Linker<Invocation>, SetupError> {
37
38
  req_ptr: i32,
38
39
  req_len: i32|
39
40
  -> wasmtime::Result<i64> {
40
- dispatch::handle(&mut caller, req_ptr, req_len)
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
+ })
41
47
  },
42
48
  )
43
49
  .map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
@@ -16,7 +16,7 @@ use std::time::Duration;
16
16
 
17
17
  use wasmtime::InstancePre;
18
18
 
19
- use crate::cache::cached_module;
19
+ use crate::cache::load_module;
20
20
  use crate::invocation::Invocation;
21
21
  use crate::{abi, imports, trap};
22
22
  use kobako_runtime::error::SetupError;
@@ -26,7 +26,7 @@ static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocatio
26
26
 
27
27
  /// The probe runs only on the first load of a path, so only that load's
28
28
  /// `setup_timeout` bounds it; a path already verified is not set up again.
29
- pub(crate) fn cached_instance_pre(
29
+ pub(crate) fn load_instance_pre(
30
30
  path: &Path,
31
31
  setup_timeout: Option<Duration>,
32
32
  ) -> Result<InstancePre<Invocation>, SetupError> {
@@ -41,7 +41,7 @@ pub(crate) fn cached_instance_pre(
41
41
  return Ok(pre);
42
42
  }
43
43
 
44
- let module = cached_module(path)?;
44
+ let module = load_module(path)?;
45
45
  let linker = imports::linker()?;
46
46
  let pre = linker
47
47
  .instantiate_pre(&module)