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
@@ -2,7 +2,7 @@
2
2
  //!
3
3
  //! The surface where a wasm engine implementation and a host frontend
4
4
  //! meet: the `Runtime` trait, the isolation `Profile` a runtime
5
- //! declares, the Grants it presents to the guest and the `Filesystem` a
5
+ //! declares, the Grants it presents to the guest and the Filesystem a
6
6
  //! Mount places, the neutral per-invocation value types, and the dispatch /
7
7
  //! yield re-entry traits a frontend supplies. The messages these carry
8
8
  //! are `kobako-transport`'s, which this crate takes as given.
@@ -13,27 +13,32 @@
13
13
  //! so the engine stays swappable.
14
14
 
15
15
  pub mod dispatch;
16
+ pub mod endpoint;
16
17
  pub mod error;
17
18
  pub mod filesystem;
18
19
  pub mod grant;
20
+ mod hashtable;
21
+ mod locking;
19
22
  pub mod mount;
20
23
  pub mod profile;
21
24
  pub mod runtime;
25
+ pub mod sched;
26
+ pub mod security;
22
27
  pub mod snapshot;
28
+ pub mod vfs;
23
29
  pub mod yielder;
24
30
 
25
31
  // Every name an engine implementation writes into its own signatures, at
26
32
  // the crate root: the module path repeats the crate name and adds nothing
27
33
  // a reader of `kobako_runtime::Runtime` was missing.
28
34
  pub use dispatch::DispatchHandler;
35
+ pub use endpoint::{Calls, GuestInstance};
29
36
  pub use error::{InvokeError, SetupError, Trap};
30
- pub use filesystem::{
31
- Access, Blocking, DirEntry, Directory, File, FileType, Filesystem, FilesystemError, Hold,
32
- Memory, Metadata, OpenOptions, Overlay, Rename, SeedError, Temporary,
33
- };
37
+ pub use filesystem::{Bind, DirEntry, FileType, FilesystemError, Kstat, Overlay, SeedError, Tmpfs};
34
38
  pub use grant::{Grant, GrantKind};
35
- pub use mount::{Denial, Mount, MountPathError, Policy};
39
+ pub use mount::{Access, Mount, MountPathError, Policy};
36
40
  pub use profile::Profile;
37
41
  pub use runtime::{Entry, Runtime};
42
+ pub use sched::Blocking;
38
43
  pub use snapshot::{Capture, Completion, Snapshot, Usage};
39
44
  pub use yielder::Yielder;
@@ -0,0 +1,66 @@
1
+ //! A lock its readers never wait on, as Linux's `seqlock_t`: a writer
2
+ //! counts each write, and a reader that a write overlapped reads again.
3
+
4
+ use std::sync::atomic::{AtomicUsize, Ordering};
5
+ use std::sync::{Mutex, PoisonError};
6
+
7
+ pub(crate) struct SeqLock {
8
+ /// Holds one writer at a time, as `write_seqlock` takes the lock's
9
+ /// spinlock.
10
+ writer: Mutex<()>,
11
+ /// How many times a write began or ended, odd while one is under way.
12
+ sequence: AtomicUsize,
13
+ }
14
+
15
+ impl SeqLock {
16
+ pub(crate) const fn new() -> Self {
17
+ Self {
18
+ writer: Mutex::new(()),
19
+ sequence: AtomicUsize::new(0),
20
+ }
21
+ }
22
+
23
+ /// Run `write` with every reader overlapping it told to read again, as
24
+ /// `write_seqlock` and `write_sequnlock` bracket a write.
25
+ pub(crate) fn write<T>(&self, write: impl FnOnce() -> T) -> T {
26
+ let _held = self.writer.lock().unwrap_or_else(PoisonError::into_inner);
27
+ self.sequence.fetch_add(1, Ordering::SeqCst);
28
+ let written = write();
29
+ self.sequence.fetch_add(1, Ordering::SeqCst);
30
+ written
31
+ }
32
+
33
+ /// Where the writes stand once none is under way, to ask `read_seqretry`
34
+ /// about later, as `__read_seqcount_begin` spins past an odd count. Holding
35
+ /// the writer's lock a moment waits out the one write it guards.
36
+ pub(crate) fn read_seqbegin(&self) -> usize {
37
+ loop {
38
+ let sequence = self.sequence.load(Ordering::SeqCst);
39
+ if sequence.is_multiple_of(2) {
40
+ return sequence;
41
+ }
42
+ drop(self.writer.lock().unwrap_or_else(PoisonError::into_inner));
43
+ }
44
+ }
45
+
46
+ /// Whether a write began since `read_seqbegin` answered `begun`.
47
+ pub(crate) fn read_seqretry(&self, begun: usize) -> bool {
48
+ self.sequence.load(Ordering::SeqCst) != begun
49
+ }
50
+
51
+ /// What `read` answers with no write overlapping it, as
52
+ /// `read_seqbegin_or_lock` reads: first without waiting, then, once a
53
+ /// write overlapped, holding writers off, so writes one after another
54
+ /// never keep a reader reading again.
55
+ pub(crate) fn read<T>(&self, mut read: impl FnMut() -> T) -> T {
56
+ let begun = self.sequence.load(Ordering::SeqCst);
57
+ if begun.is_multiple_of(2) {
58
+ let answer = read();
59
+ if self.sequence.load(Ordering::SeqCst) == begun {
60
+ return answer;
61
+ }
62
+ }
63
+ let _held = self.writer.lock().unwrap_or_else(PoisonError::into_inner);
64
+ read()
65
+ }
66
+ }
@@ -0,0 +1,218 @@
1
+ //! The lock a directory's calls run under, as Linux's `rw_semaphore` in
2
+ //! `kernel/locking`: shared while a name in it is looked up, exclusive while
3
+ //! one is made, removed, or moved. The other locks beneath the VFS sit
4
+ //! beside it.
5
+
6
+ pub(crate) mod seqlock;
7
+
8
+ use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
9
+ use std::thread::ThreadId;
10
+
11
+ use crate::sched;
12
+
13
+ /// Why a hold was not taken.
14
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
15
+ pub(crate) enum DownError {
16
+ /// A wait no release could ever end, since the thread asking holds what it
17
+ /// asks for. A thread must never hang on itself, so it is answered, as
18
+ /// Linux's `rt_mutex` answers `-EDEADLK`.
19
+ Deadlock,
20
+ /// The wait's run was to stop, past its deadline or interrupted, as
21
+ /// `down_read_killable` answers `-EINTR` on a fatal signal.
22
+ Interrupted,
23
+ }
24
+
25
+ /// Held shared by any number of threads or exclusive by one. A thread
26
+ /// asking again for a hold it could only get once it let go of its own is
27
+ /// refused as a Deadlock; a thread already holding it shared may hold it
28
+ /// shared again, even past a waiting writer, which waits for that thread.
29
+ /// Every wait for it ends once its run is to stop, as each wait of Linux's
30
+ /// is `_killable`.
31
+ #[derive(Default)]
32
+ pub(crate) struct RwSemaphore {
33
+ holders: Mutex<Holders>,
34
+ freed: Condvar,
35
+ }
36
+
37
+ #[derive(Default)]
38
+ struct Holders {
39
+ writer: Option<ThreadId>,
40
+ /// One entry for each shared hold, a thread holding it shared twice
41
+ /// standing twice.
42
+ readers: Vec<ThreadId>,
43
+ /// How many threads wait for a release, so one nobody waits for wakes
44
+ /// no one.
45
+ waiting: usize,
46
+ /// How many threads wait to hold it exclusive, which a thread arriving to hold
47
+ /// it shared goes behind, as `rwsem_down_read_slowpath` queues behind a waiting
48
+ /// writer, so readers one after another never keep a writer out.
49
+ writers_waiting: usize,
50
+ }
51
+
52
+ /// The semaphore while one call holds it, freed as the call returns. It keeps
53
+ /// the semaphore alive itself, so whatever a lock was reached through may go
54
+ /// first.
55
+ pub(crate) struct RwSemaphoreGuard {
56
+ semaphore: Arc<RwSemaphore>,
57
+ holder: ThreadId,
58
+ exclusive: bool,
59
+ }
60
+
61
+ impl RwSemaphore {
62
+ fn holders(&self) -> MutexGuard<'_, Holders> {
63
+ self.holders.lock().unwrap_or_else(PoisonError::into_inner)
64
+ }
65
+
66
+ /// Hold it shared, waiting while a writer holds it.
67
+ pub(crate) fn down_read(self: &Arc<Self>) -> Result<RwSemaphoreGuard, DownError> {
68
+ let current = sched::current();
69
+ self.down(current, false, |holders| match holders.writer {
70
+ Some(writer) if writer == current => Err(DownError::Deadlock),
71
+ Some(_) => Ok(false),
72
+ None if holders.writers_waiting > 0 && !holders.readers.contains(&current) => Ok(false),
73
+ None => {
74
+ holders.readers.push(current);
75
+ Ok(true)
76
+ }
77
+ })
78
+ }
79
+
80
+ /// Hold it exclusive, waiting while anyone holds it.
81
+ pub(crate) fn down_write(self: &Arc<Self>) -> Result<RwSemaphoreGuard, DownError> {
82
+ let current = sched::current();
83
+ self.down(current, true, |holders| {
84
+ if holders.writer == Some(current) || holders.readers.contains(&current) {
85
+ return Err(DownError::Deadlock);
86
+ }
87
+ if holders.writer.is_some() || !holders.readers.is_empty() {
88
+ return Ok(false);
89
+ }
90
+ holders.writer = Some(current);
91
+ Ok(true)
92
+ })
93
+ }
94
+
95
+ /// Hold it shared where nobody holds or waits to hold it exclusive, without
96
+ /// waiting, as `down_read_trylock` fails on a writer or a waiter.
97
+ pub(crate) fn down_read_trylock(self: &Arc<Self>) -> Option<RwSemaphoreGuard> {
98
+ let current = sched::current();
99
+ let mut holders = self.holders();
100
+ if holders.writer.is_some() || holders.writers_waiting > 0 {
101
+ return None;
102
+ }
103
+ holders.readers.push(current);
104
+ Some(self.guard(current, false))
105
+ }
106
+
107
+ /// Hold it exclusive where nobody holds it, without waiting, as
108
+ /// `down_write_trylock`.
109
+ pub(crate) fn down_write_trylock(self: &Arc<Self>) -> Option<RwSemaphoreGuard> {
110
+ let current = sched::current();
111
+ let mut holders = self.holders();
112
+ if holders.writer.is_some() || !holders.readers.is_empty() {
113
+ return None;
114
+ }
115
+ holders.writer = Some(current);
116
+ Some(self.guard(current, true))
117
+ }
118
+
119
+ fn guard(self: &Arc<Self>, holder: ThreadId, exclusive: bool) -> RwSemaphoreGuard {
120
+ RwSemaphoreGuard {
121
+ semaphore: self.clone(),
122
+ holder,
123
+ exclusive,
124
+ }
125
+ }
126
+
127
+ /// Whether a thread waits to hold it exclusive, for a test to see one queued.
128
+ #[cfg(test)]
129
+ pub(crate) fn writer_waits(&self) -> bool {
130
+ self.holders().writers_waiting > 0
131
+ }
132
+
133
+ /// Take the hold `take` grants, or wait for it.
134
+ fn down(
135
+ self: &Arc<Self>,
136
+ holder: ThreadId,
137
+ exclusive: bool,
138
+ mut take: impl FnMut(&mut Holders) -> Result<bool, DownError>,
139
+ ) -> Result<RwSemaphoreGuard, DownError> {
140
+ let mut holders = self.holders();
141
+ if !take(&mut holders)? {
142
+ drop(holders);
143
+ self.down_slowpath(exclusive, &mut take)?;
144
+ }
145
+ Ok(self.guard(holder, exclusive))
146
+ }
147
+
148
+ /// Wait for a release and ask again, as `rwsem_down_read_slowpath` and
149
+ /// `rwsem_down_write_slowpath` call `schedule`, until the run is to stop, as
150
+ /// a killable wait looks at `fatal_signal_pending` each time it wakes.
151
+ fn down_slowpath(
152
+ &self,
153
+ exclusive: bool,
154
+ take: &mut impl FnMut(&mut Holders) -> Result<bool, DownError>,
155
+ ) -> Result<(), DownError> {
156
+ let mut taken = Ok(());
157
+ sched::schedule(&mut || {
158
+ let mut holders = self.holders();
159
+ holders.writers_waiting += usize::from(exclusive);
160
+ loop {
161
+ match take(&mut holders) {
162
+ Ok(true) => break,
163
+ Ok(false) if sched::fatal_signal_pending() => {
164
+ taken = Err(DownError::Interrupted);
165
+ break;
166
+ }
167
+ Ok(false) => {
168
+ holders.waiting += 1;
169
+ holders = self
170
+ .freed
171
+ .wait_timeout(holders, sched::sleep_slice())
172
+ .unwrap_or_else(PoisonError::into_inner)
173
+ .0;
174
+ holders.waiting -= 1;
175
+ }
176
+ Err(err) => {
177
+ taken = Err(err);
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ holders.writers_waiting -= usize::from(exclusive);
183
+ // A reader may have stood behind this writer, which now takes it or
184
+ // gave up; one that took it wakes them again once it lets go.
185
+ if holders.waiting > 0 && taken.is_err() {
186
+ self.freed.notify_all();
187
+ }
188
+ });
189
+ taken
190
+ }
191
+ }
192
+
193
+ impl RwSemaphoreGuard {
194
+ /// Whether this holds `semaphore`.
195
+ pub(crate) fn holds(&self, semaphore: &Arc<RwSemaphore>) -> bool {
196
+ Arc::ptr_eq(&self.semaphore, semaphore)
197
+ }
198
+ }
199
+
200
+ impl Drop for RwSemaphoreGuard {
201
+ fn drop(&mut self) {
202
+ let mut holders = self.semaphore.holders();
203
+ if self.exclusive {
204
+ holders.writer = None;
205
+ } else if let Some(at) = holders
206
+ .readers
207
+ .iter()
208
+ .position(|reader| *reader == self.holder)
209
+ {
210
+ holders.readers.swap_remove(at);
211
+ }
212
+ let waiting = holders.waiting > 0;
213
+ drop(holders);
214
+ if waiting {
215
+ self.semaphore.freed.notify_all();
216
+ }
217
+ }
218
+ }
@@ -1,38 +1,37 @@
1
1
  //! A Mount: the Grant handing a Filesystem to the guest at an absolute
2
- //! guest path, with the access it allows there. The Mount decides each
3
- //! open and each change itself, so a runtime asks it rather than reading
4
- //! its access, and a Filesystem of a third party's checks nothing.
2
+ //! guest path, with the access it allows there and the Policy that may refuse
3
+ //! more, which the VFS asks through its Cred, so a Filesystem of a third
4
+ //! party's checks nothing.
5
5
 
6
6
  use std::fmt;
7
7
  use std::sync::Arc;
8
8
 
9
- use crate::filesystem::{Access, Filesystem};
9
+ use crate::filesystem::Filesystem;
10
+ use crate::vfs::VfsMount;
10
11
 
11
- /// The Host App's decision for one path a guest opens or changes, made in
12
- /// a Mount's access's place.
12
+ /// What a Mount allows a guest beneath it, and what a Policy is asked to allow
13
+ /// for one path.
14
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
15
+ pub enum Access {
16
+ Read,
17
+ ReadWrite,
18
+ }
19
+
20
+ /// The Host App's decision for one path a guest opens or changes, made after
21
+ /// a Mount's access allows it, so it can only refuse more.
13
22
  pub trait Policy: Send + Sync {
14
23
  /// Whether the guest may reach `path`, the absolute guest path, with
15
24
  /// `access`.
16
25
  fn allows(&self, path: &str, access: Access) -> bool;
17
26
  }
18
27
 
19
- /// Why a Mount refused a call.
20
- #[derive(Clone, Copy, Debug, PartialEq, Eq)]
21
- pub enum Denial {
22
- /// A change beneath a Mount allowing only reads.
23
- ReadOnly,
24
- /// The Mount's Policy said no.
25
- Refused,
26
- }
27
-
28
- /// A Filesystem at an absolute guest path. Its path bounds what a Policy
29
- /// can allow either way.
28
+ /// A Filesystem instance at an absolute guest path. Its path bounds what a
29
+ /// Policy can allow either way.
30
30
  #[derive(Clone)]
31
31
  pub struct Mount {
32
32
  at: String,
33
- access: Access,
34
- filesystem: Arc<dyn Filesystem>,
35
33
  policy: Option<Arc<dyn Policy>>,
34
+ mnt: Arc<VfsMount>,
36
35
  }
37
36
 
38
37
  /// A path no Mount can stand at: one that is not an absolute guest path.
@@ -66,7 +65,7 @@ impl Mount {
66
65
  /// guest path, which every path the Mount answers for is built on.
67
66
  pub fn new(
68
67
  at: impl Into<String>,
69
- filesystem: Arc<dyn Filesystem>,
68
+ filesystem: Arc<Filesystem>,
70
69
  access: Access,
71
70
  ) -> Result<Self, MountPathError> {
72
71
  let at = at.into();
@@ -75,14 +74,13 @@ impl Mount {
75
74
  }
76
75
  Ok(Self {
77
76
  at,
78
- access,
79
- filesystem,
77
+ mnt: VfsMount::new(filesystem, access == Access::Read),
80
78
  policy: None,
81
79
  })
82
80
  }
83
81
 
84
- /// This Mount with `policy` deciding each open and each change in its
85
- /// access's place.
82
+ /// This Mount with `policy` also judging each open and each change its
83
+ /// access allows; it can only refuse more, as an LSM can.
86
84
  pub fn with_policy(self, policy: Arc<dyn Policy>) -> Self {
87
85
  Self {
88
86
  policy: Some(policy),
@@ -90,89 +88,18 @@ impl Mount {
90
88
  }
91
89
  }
92
90
 
93
- /// The absolute guest path the Filesystem appears at.
94
- pub fn at(&self) -> &str {
95
- &self.at
96
- }
97
-
98
- /// The Filesystem an engine hands each call beneath this Mount.
99
- pub fn filesystem(&self) -> &Arc<dyn Filesystem> {
100
- &self.filesystem
101
- }
102
-
103
- /// Whether a change beneath this Mount may be allowed at all: it was made
104
- /// to be written, or a Policy decides each change in its access's place.
105
- /// The Policy itself is not asked.
106
- pub fn allows_writes(&self) -> bool {
107
- self.policy.is_some() || self.access == Access::ReadWrite
108
- }
109
-
110
- /// The absolute guest path `path`, relative to this Mount's root,
111
- /// stands for.
112
- pub fn guest_path(&self, path: &str) -> String {
113
- match (self.at.as_str(), path) {
114
- (at, "") => at.to_string(),
115
- ("/", path) => format!("/{path}"),
116
- (at, path) => format!("{at}/{path}"),
117
- }
118
- }
119
-
120
- /// Whether the guest may reach `path`, relative to this Mount's root,
121
- /// with `access`. A Policy judges the path as the Filesystem stores
122
- /// its names, so a host folding case or Unicode form cannot hand it
123
- /// one spelling of an entry it refuses by another; a Filesystem that
124
- /// cannot spell it refuses.
125
- pub fn check(&self, path: &str, access: Access) -> Result<(), Denial> {
126
- match &self.policy {
127
- Some(policy) => {
128
- let stored = self
129
- .filesystem
130
- .stored_path(path)
131
- .map_err(|_| Denial::Refused)?;
132
- if policy.allows(&self.guest_path(&stored), access) {
133
- Ok(())
134
- } else {
135
- Err(Denial::Refused)
136
- }
137
- }
138
- None if access == Access::ReadWrite && self.access == Access::Read => {
139
- Err(Denial::ReadOnly)
140
- }
141
- None => Ok(()),
142
- }
91
+ /// The Filesystem as the VFS walks it.
92
+ pub fn mnt(&self) -> &Arc<VfsMount> {
93
+ &self.mnt
143
94
  }
144
95
 
145
- /// Whether a directory may move beneath this Mount. Under a Policy it
146
- /// may not: everything beneath it would take paths the Policy never
147
- /// judged, so a move is refused as one across devices and the guest
148
- /// copies entry by entry instead, as Landlock refuses such a move.
149
- pub fn moves_directories(&self) -> bool {
150
- self.policy.is_none()
96
+ /// The Policy deciding beneath this Mount, once one was given.
97
+ pub(crate) fn policy(&self) -> Option<&Arc<dyn Policy>> {
98
+ self.policy.as_ref()
151
99
  }
152
100
 
153
- /// Whether a path beneath this Mount may pass through a link the
154
- /// Filesystem holds. Under a Policy it may not: the link leads to a
155
- /// path the Policy never judged, so the call is refused as one past
156
- /// too many links.
157
- pub fn follows_links(&self) -> bool {
158
- self.policy.is_none()
159
- }
160
-
161
- /// This Mount as a new Invocation starts from it, over the Filesystem
162
- /// that Invocation receives; beneath a Policy, the one that follows no
163
- /// link, so each call reaches the path the Policy judged.
164
- pub fn for_invocation(&self) -> Self {
165
- let filesystem = self
166
- .filesystem
167
- .for_invocation()
168
- .unwrap_or_else(|| self.filesystem.clone());
169
- let filesystem = match self.policy {
170
- Some(_) => filesystem.for_policy().unwrap_or(filesystem),
171
- None => filesystem,
172
- };
173
- Self {
174
- filesystem,
175
- ..self.clone()
176
- }
101
+ /// The absolute guest path the Filesystem appears at.
102
+ pub fn at(&self) -> &str {
103
+ &self.at
177
104
  }
178
105
  }
@@ -6,13 +6,20 @@
6
6
  //! actually built, and the frontend refuses a declaration below the
7
7
  //! request — so the request is also the floor.
8
8
 
9
+ use std::hash::{BuildHasher, RandomState};
10
+ use std::sync::LazyLock;
11
+ use std::time::{SystemTime, UNIX_EPOCH};
12
+
13
+ use crate::vfs::{FileType, Identity, Kstat};
14
+
9
15
  /// The ordered isolation ladder a runtime builds one rung of.
10
16
  ///
11
17
  /// `Hermetic` is the full ambient-denial posture: ambient time and
12
18
  /// entropy denied at the WASI boundary, no ambient filesystem /
13
19
  /// environment / network reachability, and no host import beyond the wire ABI's
14
20
  /// `__kobako_upcall`. `Permissive` differs only in the ambient
15
- /// authority it grants — live time and entropy at the WASI boundary. Ordering
21
+ /// authority it grants — live time and entropy at the WASI boundary — and in
22
+ /// the times and inode numbers it shows of an entry. Ordering
16
23
  /// follows strength (`Permissive < Hermetic`), so a floor check is a
17
24
  /// plain comparison.
18
25
  ///
@@ -29,10 +36,139 @@ pub enum Profile {
29
36
  Hermetic,
30
37
  }
31
38
 
39
+ impl Profile {
40
+ /// The inode number a guest on this posture sees for `identity`. A
41
+ /// hermetic guest sees none, as a Filesystem's device number depends on
42
+ /// how many were made before it in the process. Any other sees a hash of
43
+ /// it under a key the process draws once, so it can tell entries apart
44
+ /// and cannot work back to a host's inode number.
45
+ pub fn guest_ino(self, identity: Option<Identity>) -> u64 {
46
+ static KEY: LazyLock<RandomState> = LazyLock::new(RandomState::new);
47
+ match (self, identity) {
48
+ (Self::Hermetic, _) | (_, None) => 0,
49
+ (Self::Permissive, Some(identity)) => KEY.hash_one(identity),
50
+ }
51
+ }
52
+
53
+ /// `stat` as a guest on this posture sees it, whichever ABI a Wasm
54
+ /// Driver encodes it into. A Filesystem records the host's real time whatever
55
+ /// the invocation writing to it runs under, so one shared across
56
+ /// invocations keeps one record; a hermetic guest sees every time at the
57
+ /// Unix epoch, where its wall clock stands, so a time a write left behind
58
+ /// is no clock it can read. A link reports no size, so nothing of where it
59
+ /// points reaches the guest, and an entry is told apart only by what
60
+ /// `guest_ino` answers.
61
+ pub fn guest_kstat(self, stat: Kstat) -> Kstat {
62
+ let ino = self.guest_ino(Identity::of(&stat));
63
+ let masked = match self {
64
+ Self::Permissive => stat,
65
+ Self::Hermetic => {
66
+ let epoch = |time: Option<SystemTime>| time.map(|_| UNIX_EPOCH);
67
+ stat.with_times(epoch(stat.atime), epoch(stat.mtime), epoch(stat.ctime))
68
+ }
69
+ };
70
+ Kstat {
71
+ size: match masked.file_type {
72
+ FileType::Symlink => 0,
73
+ _ => masked.size,
74
+ },
75
+ ..masked.with_identity(0, ino)
76
+ }
77
+ }
78
+ }
79
+
32
80
  #[cfg(test)]
33
81
  mod tests {
34
82
  use super::*;
35
83
 
84
+ // The Wasm Driver only encodes this, so another one shows the same, and a
85
+ // guest reads a link's size and a time here or nowhere.
86
+ // @behavior RT-173 RT-183
87
+ #[test]
88
+ fn a_guest_sees_no_size_for_a_link_and_only_what_its_posture_allows() {
89
+ let reported = Kstat::new(FileType::Symlink, 42)
90
+ .with_identity(7, 9)
91
+ .with_times(Some(SystemTime::now()), None, None);
92
+ let (hermetic, permissive) = (
93
+ Profile::Hermetic.guest_kstat(reported),
94
+ Profile::Permissive.guest_kstat(reported),
95
+ );
96
+ assert_eq!(
97
+ (hermetic.size, permissive.size, hermetic.ino, hermetic.atime),
98
+ (0, 0, 0, Some(UNIX_EPOCH)),
99
+ "Profile::guest_kstat of a link must report no size on any posture, and at the \
100
+ hermetic rung no inode number and every time at the Unix epoch"
101
+ );
102
+ assert!(
103
+ permissive.ino != 0 && permissive.ino != 9 && permissive.atime == reported.atime,
104
+ "Profile::guest_kstat at the permissive rung must hash the inode number and keep \
105
+ the time"
106
+ );
107
+ }
108
+
109
+ // A device number depends on how many Filesystems the process made before,
110
+ // so the inode number a guest reads folds it in and the device stays out.
111
+ // @behavior RT-283
112
+ #[test]
113
+ fn a_guest_sees_one_device_for_every_entry() {
114
+ let reported = Kstat::new(FileType::RegFile, 1).with_identity(7, 9);
115
+ assert_eq!(
116
+ [Profile::Hermetic, Profile::Permissive]
117
+ .map(|profile| profile.guest_kstat(reported).dev),
118
+ [0, 0],
119
+ "Profile::guest_kstat of an entry a Filesystem reports on device 7 must report \
120
+ device 0 on either posture"
121
+ );
122
+ }
123
+
124
+ // An unkeyed hash answers what anyone can compute from a host's numbers,
125
+ // so the hash a guest reads must differ from it.
126
+ // @behavior RT-194
127
+ #[test]
128
+ fn a_guest_cannot_compute_the_inode_number_it_reads() {
129
+ use std::collections::hash_map::DefaultHasher;
130
+ use std::hash::{Hash, Hasher};
131
+
132
+ let reported = Kstat::new(FileType::RegFile, 0).with_identity(7, 9);
133
+ let identity = Identity::of(&reported);
134
+ let mut unkeyed = DefaultHasher::new();
135
+ identity
136
+ .expect("an inode number names an identity")
137
+ .hash(&mut unkeyed);
138
+ let seen = Profile::Permissive.guest_ino(identity);
139
+ assert_eq!(
140
+ seen,
141
+ Profile::Permissive.guest_ino(identity),
142
+ "Profile::guest_ino of one identity must answer one number within a process"
143
+ );
144
+ assert_ne!(
145
+ seen,
146
+ unkeyed.finish(),
147
+ "Profile::guest_ino at the permissive rung must not answer a hash anyone can compute \
148
+ from the device and inode numbers"
149
+ );
150
+ }
151
+
152
+ // A guest reads the count on any ABI, so the count starts in the runtime
153
+ // rather than in what a Wasm Driver encodes.
154
+ // @behavior RT-193
155
+ #[test]
156
+ fn an_entry_whose_filesystem_keeps_no_count_is_named_once() {
157
+ let reported = Kstat::new(FileType::RegFile, 0);
158
+ assert_eq!(
159
+ (
160
+ Profile::Hermetic.guest_kstat(reported).nlink,
161
+ Profile::Permissive.guest_kstat(reported).nlink,
162
+ Profile::Permissive
163
+ .guest_kstat(reported.with_nlink(3))
164
+ .nlink,
165
+ ),
166
+ (1, 1, 3),
167
+ "Profile::guest_kstat of an entry reporting no count must show one name on each \
168
+ posture, and of one reporting three must show three"
169
+ );
170
+ }
171
+
36
172
  // @behavior RT-014
37
173
  #[test]
38
174
  fn a_declaration_satisfies_any_floor_at_or_below_it() {