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,302 @@
1
+ //! The 10 `path_*` calls. Each hands the call to the runtime's VFS, which
2
+ //! resolves the path, asks the Mount, and asks the Filesystem. What a Linux
3
+ //! syscall entry checks before the VFS stays here, such as the order of an
4
+ //! open's errors.
5
+
6
+ use kobako_runtime::filesystem::{FilesystemError, Iattr};
7
+ use kobako_runtime::vfs::{self, OpenHow, Start};
8
+ use wiggle::GuestPtr;
9
+
10
+ use super::errno::{Answer, Errno};
11
+ use super::fd::to_filestat;
12
+ use super::memory::GuestMemory;
13
+ use super::types;
14
+ use crate::descriptors::{Descriptor, Descriptors};
15
+
16
+ /// The arguments `path_open` takes besides where it starts and the path, as
17
+ /// Linux's `openat2` takes a `struct open_how`; they become the `OpenHow` the
18
+ /// VFS opens with.
19
+ pub(crate) struct PathOpen {
20
+ pub(crate) dirflags: types::Lookupflags,
21
+ pub(crate) oflags: types::Oflags,
22
+ pub(crate) rights_base: types::Rights,
23
+ pub(crate) fdflags: types::Fdflags,
24
+ }
25
+
26
+ /// How many times a call is made before a guest is answered EAGAIN, as cap-std
27
+ /// makes `openat2` four times: any move in the process answers a `..` walking
28
+ /// past it so, and nothing bounds how often one lands.
29
+ const MAX_ATTEMPTS: usize = 4;
30
+
31
+ /// What `call` answers once no move landed during its walk, made again at most
32
+ /// `MAX_ATTEMPTS` times in all, as cap-std retries `openat2` on EAGAIN.
33
+ pub(super) fn retry_eagain<T>(
34
+ mut call: impl FnMut() -> Result<T, FilesystemError>,
35
+ ) -> Result<T, FilesystemError> {
36
+ let mut answer = call();
37
+ for _ in 1..MAX_ATTEMPTS {
38
+ if !matches!(answer, Err(FilesystemError::WouldBlock)) {
39
+ break;
40
+ }
41
+ answer = call();
42
+ }
43
+ answer
44
+ }
45
+
46
+ /// Where `fd` stands, for a path to be walked from, once the guard answered for a
47
+ /// directory there, found when the VFS has taken the name, as Linux's
48
+ /// `path_init` takes the descriptor. What is no directory the walk itself
49
+ /// refuses, and a move the guard meets is one a walk meets, so `retry_eagain`
50
+ /// makes the call again.
51
+ fn start_of(table: &Descriptors, fd: u32) -> impl vfs::Start + '_ {
52
+ move || {
53
+ let file = &table.get(fd).ok_or(FilesystemError::BadDescriptor)?.file;
54
+ if file.is_directory() {
55
+ table.walk_guard().check(file.path())?;
56
+ }
57
+ Ok(file.path().clone())
58
+ }
59
+ }
60
+
61
+ pub(crate) fn create_directory(
62
+ table: &Descriptors,
63
+ mem: &GuestMemory<'_, '_>,
64
+ fd: u32,
65
+ path: GuestPtr<str>,
66
+ ) -> Answer {
67
+ let path = mem.path(path)?;
68
+ Ok(retry_eagain(|| {
69
+ vfs::filename_mkdirat(table.cred(), &start_of(table, fd), &path)
70
+ })?)
71
+ }
72
+
73
+ pub(crate) fn filestat_get(
74
+ table: &Descriptors,
75
+ mem: &GuestMemory<'_, '_>,
76
+ fd: u32,
77
+ flags: types::Lookupflags,
78
+ path: GuestPtr<str>,
79
+ ) -> Answer<types::Filestat> {
80
+ let path = mem.path(path)?;
81
+ let follow = flags.contains(types::Lookupflags::SYMLINK_FOLLOW);
82
+ let stat =
83
+ retry_eagain(|| vfs::vfs_fstatat(table.cred(), &start_of(table, fd), &path, follow))?;
84
+ Ok(to_filestat(table, stat))
85
+ }
86
+
87
+ pub(crate) fn filestat_set_times(
88
+ table: &Descriptors,
89
+ mem: &GuestMemory<'_, '_>,
90
+ fd: u32,
91
+ flags: types::Lookupflags,
92
+ path: GuestPtr<str>,
93
+ times: &Iattr,
94
+ ) -> Answer {
95
+ let path = mem.path(path)?;
96
+ let follow = flags.contains(types::Lookupflags::SYMLINK_FOLLOW);
97
+ Ok(retry_eagain(|| {
98
+ vfs::do_utimes_path(table.cred(), &start_of(table, fd), &path, follow, times)
99
+ })?)
100
+ }
101
+
102
+ pub(crate) fn link(
103
+ table: &Descriptors,
104
+ mem: &GuestMemory<'_, '_>,
105
+ old: (u32, GuestPtr<str>),
106
+ new: (u32, GuestPtr<str>),
107
+ flags: types::Lookupflags,
108
+ ) -> Answer {
109
+ let (old_path, new_path) = (mem.path(old.1)?, mem.path(new.1)?);
110
+ let follow = flags.contains(types::Lookupflags::SYMLINK_FOLLOW);
111
+ Ok(retry_eagain(|| {
112
+ let (from, to) = (start_of(table, old.0), start_of(table, new.0));
113
+ vfs::filename_linkat(table.cred(), &from, &old_path, &to, &new_path, follow)
114
+ })?)
115
+ }
116
+
117
+ pub(crate) fn open(
118
+ table: &mut Descriptors,
119
+ mem: &GuestMemory<'_, '_>,
120
+ fd: u32,
121
+ path: GuestPtr<str>,
122
+ call: &PathOpen,
123
+ ) -> Answer<types::Fd> {
124
+ let oflags = call.oflags;
125
+ let mut how = OpenHow::default();
126
+ // wasi-libc asks `fd_read` and `fd_readdir` for `O_RDONLY` and neither for
127
+ // `O_SEARCH`, which glibc spells `O_PATH`; wasi-testsuite lists a
128
+ // directory opened with `fd_readdir` alone, and makes a file asking no
129
+ // right at all, which only an `O_RDONLY` open does.
130
+ how.create = oflags.contains(types::Oflags::CREAT);
131
+ how.exclusive = oflags.contains(types::Oflags::EXCL);
132
+ how.truncate = oflags.contains(types::Oflags::TRUNC);
133
+ how.write = call.rights_base.contains(types::Rights::FD_WRITE);
134
+ how.read = call
135
+ .rights_base
136
+ .intersects(types::Rights::FD_READ | types::Rights::FD_READDIR)
137
+ || (!how.write && (how.create || how.truncate));
138
+ how.directory = oflags.contains(types::Oflags::DIRECTORY);
139
+ how.follow = call.dirflags.contains(types::Lookupflags::SYMLINK_FOLLOW);
140
+ how.append = call.fdflags.contains(types::Fdflags::APPEND);
141
+ how.nonblock = call.fdflags.contains(types::Fdflags::NONBLOCK);
142
+ how.dsync = call.fdflags.contains(types::Fdflags::DSYNC);
143
+ how.sync = call
144
+ .fdflags
145
+ .intersects(types::Fdflags::RSYNC | types::Fdflags::SYNC);
146
+ // Linux's `do_sys_openat2` builds the flags, then reserves a descriptor
147
+ // before `do_file_open` answers for the name, so a full table answers
148
+ // whatever the path names.
149
+ let op = vfs::build_open_flags(&how)?;
150
+ let new = table.lowest_unused().ok_or(Errno::Mfile)?;
151
+ let path = mem.path(path)?;
152
+ let file = retry_eagain(|| vfs::do_file_open(table.cred(), &start_of(table, fd), &path, &op))?;
153
+ table.place(new, Descriptor::for_file(file));
154
+ Ok(new.into())
155
+ }
156
+
157
+ pub(crate) fn readlink(
158
+ table: &Descriptors,
159
+ mem: &mut GuestMemory<'_, '_>,
160
+ fd: u32,
161
+ path: GuestPtr<str>,
162
+ buf: GuestPtr<u8>,
163
+ buf_len: types::Size,
164
+ ) -> Answer<types::Size> {
165
+ if buf_len == 0 {
166
+ return Err(Errno::Inval.into());
167
+ }
168
+ let path = mem.path(path)?;
169
+ let target = retry_eagain(|| vfs::do_readlinkat(table.cred(), &start_of(table, fd), &path))?;
170
+ write_link(mem, &target, buf, buf_len)
171
+ }
172
+
173
+ /// As much of where a link points as the buffer holds, as `readlink` cuts it
174
+ /// off there without saying so.
175
+ fn write_link(
176
+ mem: &mut GuestMemory<'_, '_>,
177
+ target: &str,
178
+ buf: GuestPtr<u8>,
179
+ buf_len: types::Size,
180
+ ) -> Answer<types::Size> {
181
+ let take = target.len().min(buf_len as usize);
182
+ mem.write_bytes(buf, &target.as_bytes()[..take])?;
183
+ Ok(take as u32)
184
+ }
185
+
186
+ pub(crate) fn remove_directory(
187
+ table: &Descriptors,
188
+ mem: &GuestMemory<'_, '_>,
189
+ fd: u32,
190
+ path: GuestPtr<str>,
191
+ ) -> Answer {
192
+ let path = mem.path(path)?;
193
+ Ok(retry_eagain(|| {
194
+ vfs::filename_rmdir(table.cred(), &start_of(table, fd), &path)
195
+ })?)
196
+ }
197
+
198
+ pub(crate) fn rename(
199
+ table: &Descriptors,
200
+ mem: &GuestMemory<'_, '_>,
201
+ old: (u32, GuestPtr<str>),
202
+ new: (u32, GuestPtr<str>),
203
+ ) -> Answer {
204
+ let (from_path, to_path) = (mem.path(old.1)?, mem.path(new.1)?);
205
+ Ok(retry_eagain(|| {
206
+ let (from, to) = (start_of(table, old.0), start_of(table, new.0));
207
+ vfs::filename_renameat2(table.cred(), &from, &from_path, &to, &to_path)
208
+ })?)
209
+ }
210
+
211
+ pub(crate) fn symlink(
212
+ table: &Descriptors,
213
+ mem: &GuestMemory<'_, '_>,
214
+ old_path: GuestPtr<str>,
215
+ fd: u32,
216
+ new_path: GuestPtr<str>,
217
+ ) -> Answer {
218
+ let (target, path) = (mem.path(old_path)?, mem.path(new_path)?);
219
+ // wasi-testsuite's symlink_create has a guest make no link to an absolute
220
+ // path, where Linux would make one; a guest walks none of them anyway. It
221
+ // is refused once the start is found, where the link would be made.
222
+ let start = || -> Result<vfs::Path, FilesystemError> {
223
+ let start = start_of(table, fd).path_init()?;
224
+ match target.starts_with('/') {
225
+ true => Err(FilesystemError::NotPermitted),
226
+ false => Ok(start),
227
+ }
228
+ };
229
+ Ok(retry_eagain(|| {
230
+ vfs::filename_symlinkat(table.cred(), &target, &start, &path)
231
+ })?)
232
+ }
233
+
234
+ pub(crate) fn unlink_file(
235
+ table: &Descriptors,
236
+ mem: &GuestMemory<'_, '_>,
237
+ fd: u32,
238
+ path: GuestPtr<str>,
239
+ ) -> Answer {
240
+ let path = mem.path(path)?;
241
+ Ok(retry_eagain(|| {
242
+ vfs::filename_unlinkat(table.cred(), &start_of(table, fd), &path)
243
+ })?)
244
+ }
245
+
246
+ #[cfg(test)]
247
+ mod tests {
248
+ use super::*;
249
+
250
+ // Every Invocation holds to the hook refusing a readlink, so no guest call
251
+ // reaches this; only the encoding itself can witness it.
252
+ #[test]
253
+ fn a_link_is_cut_off_where_the_buffer_ends() {
254
+ let mut bytes = [0u8; 8];
255
+ let answered = {
256
+ let mut memory = wiggle::GuestMemory::Unshared(&mut bytes);
257
+ let mut mem = GuestMemory::new(&mut memory, 64);
258
+ write_link(&mut mem, "target", GuestPtr::new(2), 3)
259
+ .unwrap_or_else(|_| panic!("the link must write"))
260
+ };
261
+ assert_eq!(
262
+ (answered, &bytes),
263
+ (3, b"\0\0tar\0\0\0"),
264
+ "where a link points through path_readlink must fill the buffer and answer what fit"
265
+ );
266
+ }
267
+
268
+ /// What `retry_eagain` answers when the first `moves` calls each had a move
269
+ /// land in them, and how many calls it made.
270
+ fn retry_after_moves(moves: usize) -> (Result<(), FilesystemError>, usize) {
271
+ let mut made = 0;
272
+ let answer = retry_eagain(|| {
273
+ made += 1;
274
+ match made <= moves {
275
+ true => Err(FilesystemError::WouldBlock),
276
+ false => Ok(()),
277
+ }
278
+ });
279
+ (answer, made)
280
+ }
281
+
282
+ // A move lands during a walk only between two of its steps, which no guest
283
+ // call can time, so the retry itself witnesses it.
284
+ // @behavior RT-256
285
+ #[test]
286
+ fn a_path_call_a_move_landed_in_is_made_again_four_times_at_most() {
287
+ assert_eq!(
288
+ [
289
+ retry_after_moves(0),
290
+ retry_after_moves(3),
291
+ retry_after_moves(4)
292
+ ],
293
+ [
294
+ (Ok(()), 1),
295
+ (Ok(()), 4),
296
+ (Err(FilesystemError::WouldBlock), 4),
297
+ ],
298
+ "a path_* call answered EAGAIN must be made again until it answers otherwise, four \
299
+ times in all at most, as cap-std makes openat2"
300
+ );
301
+ }
302
+ }
@@ -0,0 +1,381 @@
1
+ //! `poll_oneoff`, the guest's wait: for a clock, or for a descriptor to be
2
+ //! ready.
3
+ //!
4
+ //! Every descriptor kobako holds is ready at once, so only a clock ever
5
+ //! waits. A wait passes on the guest's own clocks: the hermetic rung's
6
+ //! never advance, so its waits end at once, and a permissive wait takes
7
+ //! host time but never past the invocation's deadline — the host is the
8
+ //! one waiting, where no epoch check can interrupt it.
9
+
10
+ use std::thread;
11
+ use std::time::{Duration, Instant};
12
+
13
+ use kobako_runtime::vfs::PollMask;
14
+ use wiggle::{GuestPtr, GuestType};
15
+
16
+ use super::errno::{Answer, Errno};
17
+ use super::fd::file;
18
+ use super::memory::GuestMemory;
19
+ use super::types::{self, Clockid, Eventrwflags, Eventtype, Subclockflags, SubscriptionU};
20
+ use crate::invocation::Invocation;
21
+ use crate::trap::TimeoutTrap;
22
+
23
+ /// One subscription as the guest wrote it, and the error its event reports.
24
+ struct Subscription {
25
+ userdata: u64,
26
+ kind: Kind,
27
+ error: Errno,
28
+ }
29
+
30
+ enum Kind {
31
+ /// A clock, as the nanoseconds left before it fires.
32
+ Clock(u64),
33
+ FdRead,
34
+ FdWrite,
35
+ }
36
+
37
+ pub(crate) fn poll_oneoff(
38
+ invocation: &Invocation,
39
+ mem: &mut GuestMemory<'_, '_>,
40
+ subs: GuestPtr<types::Subscription>,
41
+ events: GuestPtr<types::Event>,
42
+ count: u32,
43
+ ) -> Answer<types::Size> {
44
+ if count == 0 {
45
+ return Err(Errno::Inval.into());
46
+ }
47
+ mem.consume(count, types::Subscription::guest_size())?;
48
+ mem.consume(count, types::Event::guest_size())?;
49
+ let subscriptions = subs
50
+ .as_array(count)
51
+ .iter()
52
+ .map(|sub| read_subscription(invocation, mem, sub?))
53
+ .collect::<Answer<Vec<_>>>()?;
54
+
55
+ let clocks = subscriptions.iter().filter_map(|sub| match sub.kind {
56
+ Kind::Clock(left) => Some(left),
57
+ Kind::FdRead | Kind::FdWrite => None,
58
+ });
59
+ let waited = if clocks.clone().count() == subscriptions.len() {
60
+ let soonest = clocks.min().unwrap_or(0);
61
+ wait(invocation, soonest)?;
62
+ soonest
63
+ } else {
64
+ 0
65
+ };
66
+
67
+ let mut written = 0;
68
+ for sub in &subscriptions {
69
+ let (ready, nbytes) = match sub.kind {
70
+ Kind::Clock(left) => (left <= waited, 0),
71
+ Kind::FdRead | Kind::FdWrite => (true, u64::from(sub.error == Errno::Success)),
72
+ };
73
+ if ready {
74
+ mem.write(events.add(written)?, to_event(sub, nbytes))?;
75
+ written += 1;
76
+ }
77
+ }
78
+ Ok(written)
79
+ }
80
+
81
+ fn read_subscription(
82
+ invocation: &Invocation,
83
+ mem: &GuestMemory<'_, '_>,
84
+ sub: GuestPtr<types::Subscription>,
85
+ ) -> Answer<Subscription> {
86
+ let sub = mem.read(sub)?;
87
+ let (kind, error) = match sub.u {
88
+ SubscriptionU::Clock(clock) => (read_clock(invocation, &clock)?, Errno::Success),
89
+ SubscriptionU::FdRead(subscribed) => (
90
+ Kind::FdRead,
91
+ poll_descriptor(invocation, subscribed, PollMask::IN)?,
92
+ ),
93
+ SubscriptionU::FdWrite(subscribed) => (
94
+ Kind::FdWrite,
95
+ poll_descriptor(invocation, subscribed, PollMask::OUT)?,
96
+ ),
97
+ };
98
+ Ok(Subscription {
99
+ userdata: sub.userdata,
100
+ kind,
101
+ error,
102
+ })
103
+ }
104
+
105
+ /// Hold a subscription to what its descriptor would find ready, answering the
106
+ /// error its event reports. A number no descriptor holds, or one standing only
107
+ /// for its path, is reported in its event, as Linux's `do_pollfd` marks it
108
+ /// `POLLNVAL` when `fdget` refuses it and the wait goes on. A
109
+ /// pipe's end is never ready for what the other end does, and nothing here
110
+ /// would wake a wait on it, so that is `BADF` rather than a wait that never
111
+ /// ends.
112
+ fn poll_descriptor(
113
+ invocation: &Invocation,
114
+ subscribed: types::SubscriptionFdReadwrite,
115
+ event: PollMask,
116
+ ) -> Answer<Errno> {
117
+ let descriptors = invocation.wasi().descriptors();
118
+ let Ok(file) = file(descriptors, subscribed.file_descriptor.into()) else {
119
+ return Ok(Errno::Badf);
120
+ };
121
+ match file.poll().contains(event) {
122
+ true => Ok(Errno::Success),
123
+ false => Err(Errno::Badf.into()),
124
+ }
125
+ }
126
+
127
+ /// A clock, read against the guest's own clocks. Only the realtime and
128
+ /// monotonic clocks can be waited on, alone or among other
129
+ /// subscriptions, as in wasmtime-wasi.
130
+ fn read_clock(invocation: &Invocation, clock: &types::SubscriptionClock) -> Answer<Kind> {
131
+ let absolute = clock
132
+ .flags
133
+ .contains(Subclockflags::SUBSCRIPTION_CLOCK_ABSTIME);
134
+ let clocks = invocation.wasi().clocks();
135
+ let now = match (clock.id, absolute) {
136
+ (Clockid::Realtime | Clockid::Monotonic, false) => 0,
137
+ (Clockid::Monotonic, true) => clocks.monotonic_now(),
138
+ (Clockid::Realtime, true) => clocks.wall_now(),
139
+ _ => return Err(Errno::Inval.into()),
140
+ };
141
+ Ok(Kind::Clock(clock.timeout.saturating_sub(now)))
142
+ }
143
+
144
+ /// Wait `nanos` of the guest's time: none on frozen clocks, and on live
145
+ /// ones no further than the invocation's deadline, which ends it as a
146
+ /// timeout.
147
+ fn wait(invocation: &Invocation, nanos: u64) -> Answer {
148
+ if nanos == 0 || !invocation.wasi().clocks().advances() {
149
+ return Ok(());
150
+ }
151
+ let wanted = Duration::from_nanos(nanos);
152
+ if let Some(deadline) = invocation.deadline() {
153
+ let left = deadline.saturating_duration_since(Instant::now());
154
+ if wanted >= left {
155
+ thread::sleep(left);
156
+ return Err(types::Error::trap(wasmtime::Error::new(TimeoutTrap)));
157
+ }
158
+ }
159
+ thread::sleep(wanted);
160
+ Ok(())
161
+ }
162
+
163
+ fn to_event(sub: &Subscription, nbytes: u64) -> types::Event {
164
+ types::Event {
165
+ userdata: sub.userdata,
166
+ error: sub.error,
167
+ type_: match sub.kind {
168
+ Kind::Clock(_) => Eventtype::Clock,
169
+ Kind::FdRead => Eventtype::FdRead,
170
+ Kind::FdWrite => Eventtype::FdWrite,
171
+ },
172
+ fd_readwrite: types::EventFdReadwrite {
173
+ nbytes,
174
+ flags: Eventrwflags::empty(),
175
+ },
176
+ }
177
+ }
178
+
179
+ #[cfg(test)]
180
+ mod tests {
181
+ //! Witness that a guest's clock wait passes on its posture's clocks:
182
+ //! at once on the frozen ones, and on the live ones no further than
183
+ //! the deadline.
184
+ use std::time::{Duration, Instant};
185
+
186
+ use wasmtime::{Module, Store};
187
+
188
+ use crate::cache::load_engine;
189
+ use crate::config::Config;
190
+ use crate::imports;
191
+ use crate::invocation::Invocation;
192
+ use crate::trap::{to_trap, NO_TIMEOUT_EPOCH_DELTA};
193
+ use crate::wasi::Wasi;
194
+ use kobako_runtime::error::Trap;
195
+ use kobako_runtime::profile::Profile;
196
+
197
+ /// One relative monotonic-clock subscription of ten seconds at 0,
198
+ /// its event written at 64 and the event count at 96.
199
+ const WAIT_WAT: &str = r#"
200
+ (module
201
+ (import "wasi_snapshot_preview1" "poll_oneoff"
202
+ (func $poll (param i32 i32 i32 i32) (result i32)))
203
+ (memory (export "memory") 1)
204
+ (func (export "wait_ten_seconds") (result i32)
205
+ (i32.store8 (i32.const 8) (i32.const 0))
206
+ (i32.store (i32.const 16) (i32.const 1))
207
+ (i64.store (i32.const 24) (i64.const 10000000000))
208
+ (call $poll (i32.const 0) (i32.const 64) (i32.const 1) (i32.const 96))))
209
+ "#;
210
+
211
+ /// Run the ten-second wait at `profile` under `deadline`, answering
212
+ /// how it ended and how long the host spent on it.
213
+ fn wait_ten_seconds(
214
+ profile: Profile,
215
+ deadline: Option<Duration>,
216
+ ) -> (wasmtime::Result<i32>, Duration) {
217
+ let engine = load_engine().expect("shared engine must be constructible");
218
+ let config = Config {
219
+ timeout: None,
220
+ setup_timeout: None,
221
+ memory_limit: None,
222
+ stdout_limit: None,
223
+ stderr_limit: None,
224
+ open_file_limit: None,
225
+ profile,
226
+ security: Vec::new(),
227
+ };
228
+ let wasi = Wasi::for_invocation(&config, Vec::new(), &[]).expect("WASI context must build");
229
+ let mut store = Store::new(engine, Invocation::new(None, wasi));
230
+ store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
231
+ let linker = imports::linker().expect("host imports must link");
232
+ let module = Module::new(engine, WAIT_WAT).expect("wait module must compile");
233
+ let instance = linker
234
+ .instantiate(&mut store, &module)
235
+ .expect("wait module must instantiate");
236
+ let wait = instance
237
+ .get_typed_func::<(), i32>(&mut store, "wait_ten_seconds")
238
+ .expect("wait export must resolve");
239
+ let started = Instant::now();
240
+ store
241
+ .data_mut()
242
+ .set_deadline(deadline.map(|left| started + left));
243
+ let ended = wait.call(&mut store, ());
244
+ (ended, started.elapsed())
245
+ }
246
+
247
+ // @behavior RT-073
248
+ #[test]
249
+ fn a_hermetic_wait_ends_without_the_host_waiting() {
250
+ let (ended, spent) = wait_ten_seconds(Profile::Hermetic, None);
251
+ assert_eq!(
252
+ ended.ok(),
253
+ Some(0),
254
+ "a ten-second clock wait through a hermetic guest's poll_oneoff must succeed"
255
+ );
256
+ assert!(
257
+ spent < Duration::from_secs(1),
258
+ "a ten-second clock wait through a hermetic guest's poll_oneoff must return at once, \
259
+ not after {spent:?}"
260
+ );
261
+ }
262
+
263
+ /// What a lone relative wait of no time on clock `id` answers.
264
+ fn wait_alone_on(id: i32) -> i32 {
265
+ const WAIT_ON_WAT: &str = r#"
266
+ (module
267
+ (import "wasi_snapshot_preview1" "poll_oneoff"
268
+ (func $poll (param i32 i32 i32 i32) (result i32)))
269
+ (memory (export "memory") 1)
270
+ (func (export "wait_on") (param $id i32) (result i32)
271
+ (i32.store (i32.const 16) (local.get $id))
272
+ (call $poll (i32.const 0) (i32.const 64) (i32.const 1) (i32.const 96))))
273
+ "#;
274
+ let engine = load_engine().expect("shared engine must be constructible");
275
+ let config = Config {
276
+ timeout: None,
277
+ setup_timeout: None,
278
+ memory_limit: None,
279
+ stdout_limit: None,
280
+ stderr_limit: None,
281
+ open_file_limit: None,
282
+ profile: Profile::Hermetic,
283
+ security: Vec::new(),
284
+ };
285
+ let wasi = Wasi::for_invocation(&config, Vec::new(), &[]).expect("WASI context must build");
286
+ let mut store = Store::new(engine, Invocation::new(None, wasi));
287
+ store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
288
+ let linker = imports::linker().expect("host imports must link");
289
+ let module = Module::new(engine, WAIT_ON_WAT).expect("wait module must compile");
290
+ linker
291
+ .instantiate(&mut store, &module)
292
+ .expect("wait module must instantiate")
293
+ .get_typed_func::<i32, i32>(&mut store, "wait_on")
294
+ .expect("wait export must resolve")
295
+ .call(&mut store, id)
296
+ .expect("the wait must not trap")
297
+ }
298
+
299
+ // @behavior RT-131
300
+ #[test]
301
+ fn a_lone_wait_on_a_cpu_time_clock_is_invalid() {
302
+ assert_eq!(
303
+ [0, 1, 2, 3].map(wait_alone_on),
304
+ [0, 0, ERRNO_INVAL, ERRNO_INVAL],
305
+ "a lone relative wait through poll_oneoff on the process or thread CPU-time clock \
306
+ must answer EINVAL as wasmtime-wasi does, while the realtime and monotonic clocks wait"
307
+ );
308
+ }
309
+
310
+ const ERRNO_INVAL: i32 = 28;
311
+
312
+ /// What a lone wait on standard output answers for event `tag`, 1 to read
313
+ /// and 2 to write.
314
+ fn wait_on_stdout(tag: i32) -> i32 {
315
+ const WAIT_ON_FD_WAT: &str = r#"
316
+ (module
317
+ (import "wasi_snapshot_preview1" "poll_oneoff"
318
+ (func $poll (param i32 i32 i32 i32) (result i32)))
319
+ (memory (export "memory") 1)
320
+ (func (export "wait_on") (param $tag i32) (result i32)
321
+ (i32.store8 (i32.const 8) (local.get $tag))
322
+ (i32.store (i32.const 16) (i32.const 1))
323
+ (call $poll (i32.const 0) (i32.const 64) (i32.const 1) (i32.const 96))))
324
+ "#;
325
+ let engine = load_engine().expect("shared engine must be constructible");
326
+ let config = Config {
327
+ timeout: None,
328
+ setup_timeout: None,
329
+ memory_limit: None,
330
+ stdout_limit: None,
331
+ stderr_limit: None,
332
+ open_file_limit: None,
333
+ profile: Profile::Hermetic,
334
+ security: Vec::new(),
335
+ };
336
+ let wasi = Wasi::for_invocation(&config, Vec::new(), &[]).expect("WASI context must build");
337
+ let mut store = Store::new(engine, Invocation::new(None, wasi));
338
+ store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
339
+ let linker = imports::linker().expect("host imports must link");
340
+ let module = Module::new(engine, WAIT_ON_FD_WAT).expect("wait module must compile");
341
+ linker
342
+ .instantiate(&mut store, &module)
343
+ .expect("wait module must instantiate")
344
+ .get_typed_func::<i32, i32>(&mut store, "wait_on")
345
+ .expect("wait export must resolve")
346
+ .call(&mut store, tag)
347
+ .expect("the wait must not trap")
348
+ }
349
+
350
+ const ERRNO_BADF: i32 = 8;
351
+
352
+ // Nothing ever makes standard output ready to read, so without the refusal
353
+ // the wait would only end with the invocation.
354
+ // @behavior RT-285
355
+ #[test]
356
+ fn a_wait_on_what_a_descriptor_never_does_answers_ebadf() {
357
+ assert_eq!(
358
+ [1, 2].map(wait_on_stdout),
359
+ [ERRNO_BADF, 0],
360
+ "poll_oneoff on standard output must answer EBADF waiting to read and be ready to \
361
+ write"
362
+ );
363
+ }
364
+
365
+ // @behavior RT-074
366
+ #[test]
367
+ fn a_permissive_wait_ends_at_the_deadline_as_a_timeout() {
368
+ let (ended, spent) = wait_ten_seconds(Profile::Permissive, Some(Duration::from_millis(50)));
369
+ let trap = ended.expect_err("a wait past the deadline must not return to the guest");
370
+ assert!(
371
+ matches!(to_trap(trap), Trap::Timeout(_)),
372
+ "a clock wait past the deadline through a permissive guest's poll_oneoff must end \
373
+ as a timeout"
374
+ );
375
+ assert!(
376
+ spent >= Duration::from_millis(50) && spent < Duration::from_secs(5),
377
+ "a clock wait past the deadline through a permissive guest's poll_oneoff must end \
378
+ at the deadline, not after {spent:?}"
379
+ );
380
+ }
381
+ }
@@ -0,0 +1,13 @@
1
+ //! The 4 `sock_*` calls. The table holds no socket, so every open
2
+ //! descriptor answers `NOTSOCK`, as wasmtime-wasi's preview1 does, and one
3
+ //! standing only for its path `BADF`, as Linux's socket calls take theirs
4
+ //! through `fdget`.
5
+
6
+ use super::errno::{Answer, Errno};
7
+ use super::fd::file;
8
+ use crate::descriptors::Descriptors;
9
+
10
+ pub(crate) fn socket<T>(table: &Descriptors, fd: u32) -> Answer<T> {
11
+ file(table, fd)?;
12
+ Err(Errno::Notsock.into())
13
+ }