kobako 0.28.0 → 0.29.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +170 -0
  4. data/Cargo.lock +111 -82
  5. data/README.md +154 -36
  6. data/ROADMAP.md +5 -0
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +86 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +13 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  13. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  14. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  15. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  18. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  19. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  20. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  21. data/crates/kobako-runtime/src/grant.rs +20 -0
  22. data/crates/kobako-runtime/src/lib.rs +11 -1
  23. data/crates/kobako-runtime/src/mount.rs +178 -0
  24. data/crates/kobako-runtime/src/profile.rs +4 -4
  25. data/crates/kobako-runtime/src/runtime.rs +12 -0
  26. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  27. data/crates/kobako-runtime/src/yielder.rs +11 -6
  28. data/crates/kobako-transport/CHANGELOG.md +17 -0
  29. data/crates/kobako-transport/Cargo.toml +1 -1
  30. data/crates/kobako-transport/README.md +1 -1
  31. data/crates/kobako-transport/src/abi.rs +1 -1
  32. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  33. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  34. data/crates/kobako-transport/src/envelope/reply.rs +32 -14
  35. data/crates/kobako-transport/src/envelope/request.rs +18 -8
  36. data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
  37. data/crates/kobako-wasmtime/Cargo.toml +21 -16
  38. data/crates/kobako-wasmtime/README.md +5 -3
  39. data/crates/kobako-wasmtime/src/abi.rs +66 -32
  40. data/crates/kobako-wasmtime/src/ambient.rs +63 -4
  41. data/crates/kobako-wasmtime/src/cache.rs +185 -74
  42. data/crates/kobako-wasmtime/src/config.rs +58 -1
  43. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  44. data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
  45. data/crates/kobako-wasmtime/src/driver.rs +46 -44
  46. data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
  47. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  48. data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
  49. data/crates/kobako-wasmtime/src/invocation.rs +35 -46
  50. data/crates/kobako-wasmtime/src/io.rs +5 -202
  51. data/crates/kobako-wasmtime/src/lib.rs +4 -0
  52. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  53. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  54. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  55. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  56. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  57. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  58. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  59. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  60. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  61. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  62. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  63. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  64. data/crates/kobako-wasmtime/src/trap.rs +39 -19
  65. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  66. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  67. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  68. data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
  69. data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
  70. data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
  71. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  72. data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
  73. data/data/kobako.wasm +0 -0
  74. data/ext/kobako/Cargo.toml +4 -2
  75. data/ext/kobako/src/runtime/bridge.rs +154 -81
  76. data/ext/kobako/src/runtime/escape.rs +53 -0
  77. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  78. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  79. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  80. data/ext/kobako/src/runtime/gvl.rs +7 -1
  81. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  82. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  83. data/ext/kobako/src/runtime.rs +120 -199
  84. data/lib/kobako/catalog/flags.rb +2 -14
  85. data/lib/kobako/catalog/grants.rb +30 -0
  86. data/lib/kobako/catalog/mount.rb +27 -0
  87. data/lib/kobako/catalog/services.rb +11 -0
  88. data/lib/kobako/catalog/snippets.rb +5 -9
  89. data/lib/kobako/catalog.rb +11 -0
  90. data/lib/kobako/codec/decoder.rb +1 -0
  91. data/lib/kobako/codec/ext_types.rb +1 -1
  92. data/lib/kobako/codec/state.rb +19 -3
  93. data/lib/kobako/codec/value_budget.rb +86 -0
  94. data/lib/kobako/codec.rb +7 -0
  95. data/lib/kobako/context.rb +38 -11
  96. data/lib/kobako/errors.rb +15 -14
  97. data/lib/kobako/filesystem.rb +38 -0
  98. data/lib/kobako/pool.rb +2 -2
  99. data/lib/kobako/sandbox.rb +40 -12
  100. data/lib/kobako/sandbox_options.rb +39 -12
  101. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
  102. data/lib/kobako/snippet.rb +1 -1
  103. data/lib/kobako/transport/block_failure.rb +35 -0
  104. data/lib/kobako/transport/dispatcher.rb +30 -26
  105. data/lib/kobako/transport/exposure.rb +5 -7
  106. data/lib/kobako/transport/reflection.rb +79 -23
  107. data/lib/kobako/transport/yielder.rb +8 -30
  108. data/lib/kobako/transport.rb +1 -0
  109. data/lib/kobako/version.rb +1 -1
  110. data/lib/kobako.rb +1 -0
  111. data/sig/kobako/catalog/flags.rbs +0 -4
  112. data/sig/kobako/catalog/grants.rbs +13 -0
  113. data/sig/kobako/catalog/mount.rbs +28 -0
  114. data/sig/kobako/catalog/services.rbs +4 -0
  115. data/sig/kobako/catalog/snippets.rbs +1 -1
  116. data/sig/kobako/catalog.rbs +1 -0
  117. data/sig/kobako/codec/state.rbs +7 -1
  118. data/sig/kobako/codec/value_budget.rbs +23 -0
  119. data/sig/kobako/codec.rbs +2 -0
  120. data/sig/kobako/context.rbs +14 -6
  121. data/sig/kobako/errors.rbs +4 -4
  122. data/sig/kobako/filesystem.rbs +88 -0
  123. data/sig/kobako/guest_yielder.rbs +9 -0
  124. data/sig/kobako/pool.rbs +1 -1
  125. data/sig/kobako/runtime.rbs +21 -49
  126. data/sig/kobako/sandbox.rbs +7 -1
  127. data/sig/kobako/sandbox_options.rbs +24 -2
  128. data/sig/kobako/snapshot.rbs +20 -0
  129. data/sig/kobako/snippet/bytecode.rbs +12 -0
  130. data/sig/kobako/transport/block_failure.rbs +13 -0
  131. data/sig/kobako/transport/dispatcher.rbs +3 -3
  132. data/sig/kobako/transport/exposure.rbs +1 -1
  133. data/sig/kobako/transport/reflection.rbs +14 -1
  134. data/sig/kobako/transport/yielder.rbs +3 -7
  135. data/sig/kobako/transport.rbs +2 -2
  136. metadata +60 -7
  137. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -0,0 +1,672 @@
1
+ //! The 21 `fd_*` calls over kobako's descriptor table.
2
+ //!
3
+ //! For stdio each answers what wasmtime-wasi's preview1 answers for the
4
+ //! same descriptor; the differential probe in `preview1` holds that. For a
5
+ //! directory or a file beneath a Mount, the Filesystem answers
6
+ //! and the descriptor's position and rights stay here.
7
+
8
+ use bytes::Bytes;
9
+ use kobako_runtime::filesystem::{FileType, Metadata};
10
+ use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
11
+ use wasmtime_wasi::p2::{InputStream, OutputStream, StreamError};
12
+
13
+ use super::args::{
14
+ self, FDFLAGS_APPEND, FDFLAGS_NONBLOCK, FDFLAGS_SYNCHRONIZED, RIGHTS_FD_READ, RIGHTS_FD_WRITE,
15
+ };
16
+ use super::beneath::confirm;
17
+ use super::errno::{Answer, Errno};
18
+ use super::memory::GuestMemory;
19
+ use crate::descriptors::{Descriptor, Descriptors, OpenFile};
20
+
21
+ /// `fdstat` / `filestat` filetypes.
22
+ const FILETYPE_UNKNOWN: u8 = 0;
23
+ const FILETYPE_DIRECTORY: u8 = 3;
24
+ const FILETYPE_REGULAR_FILE: u8 = 4;
25
+ const FILETYPE_SYMBOLIC_LINK: u8 = 7;
26
+
27
+ const RIGHTS_FD_DATASYNC: u64 = 1 << 0;
28
+ const RIGHTS_FD_SEEK: u64 = 1 << 2;
29
+ const RIGHTS_FD_FDSTAT_SET_FLAGS: u64 = 1 << 3;
30
+ const RIGHTS_FD_SYNC: u64 = 1 << 4;
31
+ const RIGHTS_FD_TELL: u64 = 1 << 5;
32
+ const RIGHTS_FD_ADVISE: u64 = 1 << 7;
33
+ const RIGHTS_FD_ALLOCATE: u64 = 1 << 8;
34
+ const RIGHTS_FD_READDIR: u64 = 1 << 14;
35
+ const RIGHTS_PATH_FILESTAT_SET_SIZE: u64 = 1 << 19;
36
+ const RIGHTS_FD_FILESTAT_GET: u64 = 1 << 21;
37
+ const RIGHTS_FD_FILESTAT_SET_SIZE: u64 = 1 << 22;
38
+ const RIGHTS_FD_FILESTAT_SET_TIMES: u64 = 1 << 23;
39
+ const RIGHTS_POLL_FD_READWRITE: u64 = 1 << 27;
40
+
41
+ /// What a preopen allows itself: every path call, as wasmtime-wasi hands
42
+ /// most userlands.
43
+ const PREOPEN_RIGHTS: u64 = (1 << 9)
44
+ | (1 << 10)
45
+ | (1 << 11)
46
+ | (1 << 12)
47
+ | (1 << 13)
48
+ | RIGHTS_FD_READDIR
49
+ | (1 << 15)
50
+ | (1 << 16)
51
+ | (1 << 17)
52
+ | (1 << 18)
53
+ | (1 << 20)
54
+ | RIGHTS_FD_FILESTAT_GET
55
+ | RIGHTS_FD_FILESTAT_SET_TIMES
56
+ | (1 << 24)
57
+ | (1 << 25)
58
+ | (1 << 26);
59
+
60
+ /// What a file opened beneath a preopen may hold. Reading and writing are
61
+ /// both inherited whatever the Mount's access, since wasi-libc intersects
62
+ /// them before asking: a read-only Mount answers a write itself, as `ROFS`.
63
+ const PREOPEN_INHERITING: u64 = PREOPEN_RIGHTS
64
+ | RIGHTS_FD_DATASYNC
65
+ | RIGHTS_FD_READ
66
+ | RIGHTS_FD_SEEK
67
+ | RIGHTS_FD_FDSTAT_SET_FLAGS
68
+ | RIGHTS_FD_SYNC
69
+ | RIGHTS_FD_TELL
70
+ | RIGHTS_FD_WRITE
71
+ | RIGHTS_FD_ADVISE
72
+ | RIGHTS_FD_ALLOCATE
73
+ | RIGHTS_FD_FILESTAT_SET_SIZE
74
+ | RIGHTS_POLL_FD_READWRITE;
75
+
76
+ /// The most bytes one write moves into a capture pipe at a time, the
77
+ /// preview2 `blocking-write-and-flush` limit wasmtime-wasi writes under.
78
+ const WRITE_CHUNK: usize = 4096;
79
+
80
+ /// The size of one `dirent` header ahead of its name.
81
+ const DIRENT_SIZE: u32 = 24;
82
+
83
+ fn descriptor(table: &Descriptors, fd: u32) -> Answer<&Descriptor> {
84
+ table.get(fd).ok_or_else(|| Errno::Badf.into())
85
+ }
86
+
87
+ fn descriptor_mut(table: &mut Descriptors, fd: u32) -> Answer<&mut Descriptor> {
88
+ table.get_mut(fd).ok_or_else(|| Errno::Badf.into())
89
+ }
90
+
91
+ /// The open file `fd` stands for; any other descriptor is `BADF`.
92
+ fn file(table: &mut Descriptors, fd: u32) -> Answer<&mut OpenFile> {
93
+ match descriptor_mut(table, fd)? {
94
+ Descriptor::File(file) => Ok(file),
95
+ Descriptor::Stdin(_) | Descriptor::Channel(_) | Descriptor::Directory(_) => {
96
+ Err(Errno::Badf.into())
97
+ }
98
+ }
99
+ }
100
+
101
+ /// A call that moves a stream position: a file has one, stdio is a pipe
102
+ /// with none, and a directory is no stream at all.
103
+ fn seekable(table: &mut Descriptors, fd: u32) -> Answer<&mut OpenFile> {
104
+ match descriptor_mut(table, fd)? {
105
+ Descriptor::File(file) => Ok(file),
106
+ Descriptor::Stdin(_) | Descriptor::Channel(_) => Err(Errno::Spipe.into()),
107
+ Descriptor::Directory(_) => Err(Errno::Badf.into()),
108
+ }
109
+ }
110
+
111
+ /// An offset or size `pread`, `pwrite`, or `ftruncate` names, which Linux
112
+ /// reads as a signed `off_t` and refuses once negative.
113
+ fn refuse_negative(offset: u64) -> Answer<u64> {
114
+ if offset > i64::MAX as u64 {
115
+ return Err(Errno::Inval.into());
116
+ }
117
+ Ok(offset)
118
+ }
119
+
120
+ /// A read of `len` bytes from `offset`, refused as Linux refuses one whose
121
+ /// end would pass the largest signed offset.
122
+ fn refuse_reading_past_largest_offset(offset: u64, len: u32) -> Answer<u64> {
123
+ if offset.saturating_add(u64::from(len)) > i64::MAX as u64 {
124
+ return Err(Errno::Inval.into());
125
+ }
126
+ Ok(offset)
127
+ }
128
+
129
+ /// The part of `buf` a write at `offset` lands, as Linux cuts a write
130
+ /// short at the largest signed offset and refuses one starting there as
131
+ /// too large.
132
+ fn trim_to_largest_offset(buf: &[u8], offset: u64) -> Answer<&[u8]> {
133
+ let room = (i64::MAX as u64).saturating_sub(offset);
134
+ if room == 0 && !buf.is_empty() {
135
+ return Err(Errno::Fbig.into());
136
+ }
137
+ Ok(&buf[..buf.len().min(usize::try_from(room).unwrap_or(usize::MAX))])
138
+ }
139
+
140
+ /// A call any file or directory answers, which preview1 refuses for
141
+ /// stdio.
142
+ fn file_or_directory(table: &Descriptors, fd: u32) -> Answer {
143
+ match descriptor(table, fd)? {
144
+ Descriptor::Directory(_) | Descriptor::File(_) => Ok(()),
145
+ Descriptor::Stdin(_) | Descriptor::Channel(_) => Err(Errno::Badf.into()),
146
+ }
147
+ }
148
+
149
+ /// A `filestat` of `filetype`, named `nlink` times and `size` bytes long.
150
+ fn write_filestat(mem: &mut GuestMemory, out: u32, filetype: u8, nlink: u64, size: u64) -> Answer {
151
+ GuestMemory::check_aligned(out, 8)?;
152
+ mem.write(out, &[0; 64])?;
153
+ mem.write_u8(GuestMemory::field(out, 16)?, filetype)?;
154
+ mem.write_u64(GuestMemory::field(out, 24)?, nlink)?;
155
+ mem.write_u64(GuestMemory::field(out, 32)?, size)
156
+ }
157
+
158
+ fn to_filetype(file_type: FileType) -> u8 {
159
+ match file_type {
160
+ FileType::Directory => FILETYPE_DIRECTORY,
161
+ FileType::Symlink => FILETYPE_SYMBOLIC_LINK,
162
+ FileType::File => FILETYPE_REGULAR_FILE,
163
+ _ => FILETYPE_UNKNOWN,
164
+ }
165
+ }
166
+
167
+ /// A `filestat` for what a Filesystem reports a path names, named once
168
+ /// since `Metadata` carries no count of names.
169
+ pub(crate) fn write_metadata(mem: &mut GuestMemory, out: u32, metadata: Metadata) -> Answer {
170
+ write_filestat(mem, out, to_filetype(metadata.file_type), 1, metadata.len)
171
+ }
172
+
173
+ /// The hints preview1 offers about a file. A Filesystem that ignores them
174
+ /// is still correct, so they succeed over a range Linux takes.
175
+ pub(crate) fn advise(
176
+ table: &mut Descriptors,
177
+ fd: u32,
178
+ offset: u64,
179
+ len: u64,
180
+ advice: u32,
181
+ ) -> Answer {
182
+ args::variant(advice, args::ADVICE_MAX)?;
183
+ file(table, fd)?;
184
+ refuse_negative(offset)?;
185
+ refuse_negative(len).map(drop)
186
+ }
187
+
188
+ /// A Filesystem has no room to reserve ahead of a write, so allocating it
189
+ /// is unsupported rather than answered as done.
190
+ pub(crate) fn allocate(table: &mut Descriptors, fd: u32) -> Answer {
191
+ file(table, fd)?;
192
+ Err(Errno::Notsup.into())
193
+ }
194
+
195
+ pub(crate) fn close(table: &mut Descriptors, fd: u32) -> Answer {
196
+ match table.remove(fd).ok_or(Errno::Badf)? {
197
+ Descriptor::File(open) => Ok(open.file.close()?),
198
+ _ => Ok(()),
199
+ }
200
+ }
201
+
202
+ pub(crate) fn sync(table: &mut Descriptors, fd: u32) -> Answer {
203
+ Ok(file(table, fd)?.file.sync()?)
204
+ }
205
+
206
+ pub(crate) fn fdstat_get(table: &Descriptors, mem: &mut GuestMemory, fd: u32, out: u32) -> Answer {
207
+ let (filetype, flags, base, inheriting) = match descriptor(table, fd)? {
208
+ Descriptor::Stdin(_) => (FILETYPE_UNKNOWN, 0, RIGHTS_FD_READ, RIGHTS_FD_READ),
209
+ Descriptor::Channel(_) => (FILETYPE_UNKNOWN, 0, RIGHTS_FD_WRITE, RIGHTS_FD_WRITE),
210
+ Descriptor::Directory(dir) if dir.preopen => {
211
+ (FILETYPE_DIRECTORY, 0, PREOPEN_RIGHTS, PREOPEN_INHERITING)
212
+ }
213
+ Descriptor::Directory(_) => {
214
+ let rights = args::RIGHTS
215
+ & !RIGHTS_FD_WRITE
216
+ & !RIGHTS_FD_SEEK
217
+ & !RIGHTS_FD_FILESTAT_SET_SIZE
218
+ & !RIGHTS_PATH_FILESTAT_SET_SIZE;
219
+ (FILETYPE_DIRECTORY, 0, rights, rights)
220
+ }
221
+ Descriptor::File(file) => {
222
+ let mut rights = args::RIGHTS;
223
+ if !file.readable {
224
+ rights &= !RIGHTS_FD_READ & !RIGHTS_FD_READDIR;
225
+ }
226
+ if !file.writable {
227
+ rights &= !RIGHTS_FD_WRITE;
228
+ }
229
+ let flags = if file.append {
230
+ FDFLAGS_APPEND as u16
231
+ } else {
232
+ 0
233
+ };
234
+ (FILETYPE_REGULAR_FILE, flags, rights, rights)
235
+ }
236
+ };
237
+ GuestMemory::check_aligned(out, 8)?;
238
+ mem.write_u8(out, filetype)?;
239
+ mem.write_u16(GuestMemory::field(out, 2)?, flags)?;
240
+ mem.write_u64(GuestMemory::field(out, 8)?, base)?;
241
+ mem.write_u64(GuestMemory::field(out, 16)?, inheriting)
242
+ }
243
+
244
+ /// Only appending can change once a file is open; the sync flags are
245
+ /// `INVAL`, as in wasmtime-wasi, and a file never stops blocking, so
246
+ /// asking it to is unsupported.
247
+ pub(crate) fn fdstat_set_flags(table: &mut Descriptors, fd: u32, flags: u32) -> Answer {
248
+ args::flags(flags, args::FDFLAGS)?;
249
+ let file = file(table, fd)?;
250
+ if flags & FDFLAGS_SYNCHRONIZED != 0 {
251
+ return Err(Errno::Inval.into());
252
+ }
253
+ if flags & FDFLAGS_NONBLOCK != 0 {
254
+ return Err(Errno::Notsup.into());
255
+ }
256
+ file.append = flags & FDFLAGS_APPEND != 0;
257
+ Ok(())
258
+ }
259
+
260
+ /// Rights only narrow at open, so a later change is unsupported.
261
+ pub(crate) fn fdstat_set_rights(
262
+ table: &Descriptors,
263
+ fd: u32,
264
+ base: u64,
265
+ inheriting: u64,
266
+ ) -> Answer {
267
+ args::flags(base, args::RIGHTS)?;
268
+ args::flags(inheriting, args::RIGHTS)?;
269
+ file_or_directory(table, fd)?;
270
+ Err(Errno::Notsup.into())
271
+ }
272
+
273
+ pub(crate) fn filestat_get(
274
+ table: &Descriptors,
275
+ mem: &mut GuestMemory,
276
+ fd: u32,
277
+ out: u32,
278
+ ) -> Answer {
279
+ match descriptor(table, fd)? {
280
+ Descriptor::Stdin(_) | Descriptor::Channel(_) => {
281
+ write_filestat(mem, out, FILETYPE_UNKNOWN, 0, 0)
282
+ }
283
+ Descriptor::Directory(dir) => {
284
+ confirm(dir)?;
285
+ write_metadata(mem, out, dir.mount.filesystem().metadata(&dir.path)?)
286
+ }
287
+ Descriptor::File(file) => write_metadata(mem, out, file.file.metadata()?),
288
+ }
289
+ }
290
+
291
+ /// A Mount that refuses the write answers first, as wasmtime-wasi answers
292
+ /// by its preopen; a file opened only to read then answers as Linux's
293
+ /// `ftruncate` does.
294
+ pub(crate) fn filestat_set_size(table: &mut Descriptors, fd: u32, size: u64) -> Answer {
295
+ let file = file(table, fd)?;
296
+ if !file.mount.allows_writes() {
297
+ return Err(Errno::Perm.into());
298
+ }
299
+ if !file.writable {
300
+ return Err(Errno::Inval.into());
301
+ }
302
+ Ok(file.file.set_len(refuse_negative(size)?)?)
303
+ }
304
+
305
+ /// A Filesystem keeps no timestamps of the guest's choosing, so setting
306
+ /// them is unsupported rather than silently ignored.
307
+ pub(crate) fn filestat_set_times(table: &Descriptors, fd: u32, fst_flags: u32) -> Answer {
308
+ args::fstflags(fst_flags)?;
309
+ file_or_directory(table, fd)?;
310
+ Err(Errno::Notsup.into())
311
+ }
312
+
313
+ pub(crate) fn pread(
314
+ table: &mut Descriptors,
315
+ mem: &mut GuestMemory,
316
+ fd: u32,
317
+ iovs: (u32, u32),
318
+ offset: u64,
319
+ nread: u32,
320
+ ) -> Answer {
321
+ let (ptr, len) = mem.first_non_empty_buffer(iovs.0, iovs.1)?;
322
+ let file = match descriptor_mut(table, fd)? {
323
+ Descriptor::Stdin(_) => return Err(Errno::Spipe.into()),
324
+ Descriptor::Channel(_) | Descriptor::Directory(_) => return Err(Errno::Badf.into()),
325
+ Descriptor::File(file) => file,
326
+ };
327
+ if !file.readable {
328
+ return Err(Errno::Badf.into());
329
+ }
330
+ let offset = refuse_reading_past_largest_offset(refuse_negative(offset)?, len)?;
331
+ let read = file.file.read_at(mem.bytes_mut(ptr, len)?, offset)?;
332
+ mem.write_u32(nread, read as u32)
333
+ }
334
+
335
+ pub(crate) fn prestat_get(table: &Descriptors, mem: &mut GuestMemory, fd: u32, out: u32) -> Answer {
336
+ match descriptor(table, fd)? {
337
+ Descriptor::Directory(dir) if dir.preopen => {
338
+ GuestMemory::check_aligned(out, 4)?;
339
+ mem.write_u8(out, 0)?;
340
+ mem.write_u32(GuestMemory::field(out, 4)?, dir.mount.at().len() as u32)
341
+ }
342
+ _ => Err(Errno::Badf.into()),
343
+ }
344
+ }
345
+
346
+ pub(crate) fn prestat_dir_name(
347
+ table: &Descriptors,
348
+ mem: &mut GuestMemory,
349
+ fd: u32,
350
+ path: u32,
351
+ path_len: u32,
352
+ ) -> Answer {
353
+ match descriptor(table, fd)? {
354
+ Descriptor::Directory(dir) if dir.preopen => {
355
+ let name = dir.mount.at().as_bytes();
356
+ if name.len() > path_len as usize {
357
+ return Err(Errno::Nametoolong.into());
358
+ }
359
+ mem.write(path, name)
360
+ }
361
+ _ => Err(Errno::Notdir.into()),
362
+ }
363
+ }
364
+
365
+ pub(crate) fn pwrite(
366
+ table: &mut Descriptors,
367
+ mem: &mut GuestMemory,
368
+ fd: u32,
369
+ ciovs: (u32, u32),
370
+ offset: u64,
371
+ nwritten: u32,
372
+ ) -> Answer {
373
+ let (ptr, len) = mem.first_non_empty_buffer(ciovs.0, ciovs.1)?;
374
+ let file = match descriptor_mut(table, fd)? {
375
+ Descriptor::Channel(_) => return Err(Errno::Spipe.into()),
376
+ Descriptor::Stdin(_) | Descriptor::Directory(_) => return Err(Errno::Badf.into()),
377
+ Descriptor::File(file) => file,
378
+ };
379
+ // The buffer is bounded before the access is checked, as wasmtime-wasi
380
+ // bounds it before the host refuses the write.
381
+ let buf = mem.bytes(ptr, len)?;
382
+ if !file.writable {
383
+ return Err(Errno::Badf.into());
384
+ }
385
+ let offset = refuse_negative(offset)?;
386
+ // Linux's `pwrite` ignores the offset of a file opened to append.
387
+ let written = if file.append {
388
+ file.file.append(buf)?.1
389
+ } else {
390
+ file.file
391
+ .write_at(trim_to_largest_offset(buf, offset)?, offset)?
392
+ };
393
+ mem.write_u32(nwritten, written as u32)
394
+ }
395
+
396
+ pub(crate) fn read(
397
+ table: &mut Descriptors,
398
+ mem: &mut GuestMemory,
399
+ fd: u32,
400
+ iovs: (u32, u32),
401
+ nread: u32,
402
+ ) -> Answer {
403
+ let (ptr, len) = mem.first_non_empty_buffer(iovs.0, iovs.1)?;
404
+ let read = match descriptor_mut(table, fd)? {
405
+ Descriptor::Stdin(pipe) => {
406
+ // Bounded before stdin is drained, so a trap takes no bytes.
407
+ mem.bytes(ptr, len)?;
408
+ let bytes = read_stdin(pipe, len as usize)?;
409
+ mem.write(ptr, &bytes)?;
410
+ bytes.len()
411
+ }
412
+ Descriptor::File(file) => {
413
+ // Bounded before the access is checked, as wasmtime-wasi bounds
414
+ // it before the host refuses the read.
415
+ let buf = mem.bytes_mut(ptr, len)?;
416
+ if !file.readable {
417
+ return Err(Errno::Badf.into());
418
+ }
419
+ let at = refuse_reading_past_largest_offset(file.position, len)?;
420
+ let read = file.file.read_at(buf, at)?;
421
+ file.position += read as u64;
422
+ read
423
+ }
424
+ Descriptor::Channel(_) | Descriptor::Directory(_) => return Err(Errno::Badf.into()),
425
+ };
426
+ mem.write_u32(nread, read as u32)
427
+ }
428
+
429
+ /// A directory's entries as preview1 `dirent`s, `.` and `..` first, from
430
+ /// the `cookie`th on, cut off where the buffer ends. A listing from the
431
+ /// start reads the directory; one going on reads the entries that listing
432
+ /// began with, so a guest reading in small steps costs one listing, not one
433
+ /// per step.
434
+ pub(crate) fn readdir(
435
+ table: &mut Descriptors,
436
+ mem: &mut GuestMemory,
437
+ fd: u32,
438
+ buf: (u32, u32),
439
+ cookie: u64,
440
+ bufused: u32,
441
+ ) -> Answer {
442
+ let dir = match descriptor_mut(table, fd)? {
443
+ Descriptor::Directory(dir) => dir,
444
+ _ => return Err(Errno::Badf.into()),
445
+ };
446
+ let skip = usize::try_from(cookie).map_err(|_| Errno::Overflow)?;
447
+ confirm(dir)?;
448
+ let listing = match dir.listing.take() {
449
+ Some(listing) if cookie != 0 => listing,
450
+ _ => dir.mount.filesystem().read_dir(&dir.path)?,
451
+ };
452
+ let entries = [(".", FILETYPE_DIRECTORY), ("..", FILETYPE_DIRECTORY)]
453
+ .into_iter()
454
+ .chain(
455
+ listing
456
+ .iter()
457
+ .map(|entry| (entry.name.as_str(), to_filetype(entry.file_type))),
458
+ );
459
+ let (start, capacity) = buf;
460
+ let mut used: u32 = 0;
461
+ let mut full = false;
462
+ 'entries: for (index, (name, filetype)) in entries.enumerate().skip(skip) {
463
+ let mut dirent = [0u8; DIRENT_SIZE as usize];
464
+ dirent[0..8].copy_from_slice(&(index as u64 + 1).to_le_bytes());
465
+ dirent[16..20].copy_from_slice(&(name.len() as u32).to_le_bytes());
466
+ dirent[20] = filetype;
467
+ for part in [&dirent[..], name.as_bytes()] {
468
+ let room = (capacity - used) as usize;
469
+ let take = part.len().min(room);
470
+ if take > 0 {
471
+ mem.write(GuestMemory::field(start, used)?, &part[..take])?;
472
+ }
473
+ used += take as u32;
474
+ if used == capacity {
475
+ full = true;
476
+ break 'entries;
477
+ }
478
+ }
479
+ }
480
+ if full {
481
+ dir.listing = Some(listing);
482
+ }
483
+ mem.write_u32(bufused, used)
484
+ }
485
+
486
+ /// preview1 renumbering closes `to` as `fd_close` does, answering a close
487
+ /// its Filesystem refuses, and moves `from` onto its number; both must
488
+ /// already be open.
489
+ pub(crate) fn renumber(table: &mut Descriptors, from: u32, to: u32) -> Answer {
490
+ if table.get(from).is_none() || table.get(to).is_none() {
491
+ return Err(Errno::Badf.into());
492
+ }
493
+ if from == to {
494
+ return Ok(());
495
+ }
496
+ close(table, to)?;
497
+ let moved = table.remove(from).ok_or(Errno::Badf)?;
498
+ table.place(to, moved);
499
+ Ok(())
500
+ }
501
+
502
+ pub(crate) fn seek(
503
+ table: &mut Descriptors,
504
+ mem: &mut GuestMemory,
505
+ fd: u32,
506
+ offset: i64,
507
+ whence: u32,
508
+ out: u32,
509
+ ) -> Answer {
510
+ args::variant(whence, args::WHENCE_MAX)?;
511
+ let file = seekable(table, fd)?;
512
+ let base = match whence {
513
+ 0 => 0,
514
+ 1 => file.position,
515
+ _ => file.file.metadata()?.len,
516
+ };
517
+ // Linux holds an offset in a signed 64-bit `off_t`, past which a seek
518
+ // is `OVERFLOW` rather than a position a write could wrap around.
519
+ let position = base.checked_add_signed(offset).ok_or(Errno::Inval)?;
520
+ if position > i64::MAX as u64 {
521
+ return Err(Errno::Overflow.into());
522
+ }
523
+ file.position = position;
524
+ mem.write_u64(out, position)
525
+ }
526
+
527
+ pub(crate) fn tell(table: &mut Descriptors, mem: &mut GuestMemory, fd: u32, out: u32) -> Answer {
528
+ let position = seekable(table, fd)?.position;
529
+ mem.write_u64(out, position)
530
+ }
531
+
532
+ pub(crate) fn write(
533
+ table: &mut Descriptors,
534
+ mem: &mut GuestMemory,
535
+ fd: u32,
536
+ ciovs: (u32, u32),
537
+ nwritten: u32,
538
+ ) -> Answer {
539
+ let (ptr, len) = mem.first_non_empty_buffer(ciovs.0, ciovs.1)?;
540
+ let written = match descriptor_mut(table, fd)? {
541
+ Descriptor::Channel(pipe) => write_channel(pipe, mem.bytes(ptr, len)?)?,
542
+ Descriptor::File(file) => {
543
+ // Bounded before the access is checked, as in `pwrite`.
544
+ let buf = mem.bytes(ptr, len)?;
545
+ if !file.writable {
546
+ return Err(Errno::Badf.into());
547
+ }
548
+ let (at, written) = if file.append {
549
+ file.file.append(buf)?
550
+ } else {
551
+ let buf = trim_to_largest_offset(buf, file.position)?;
552
+ (file.position, file.file.write_at(buf, file.position)?)
553
+ };
554
+ file.position = at.checked_add(written as u64).ok_or(Errno::Fbig)?;
555
+ written
556
+ }
557
+ Descriptor::Stdin(_) | Descriptor::Directory(_) => return Err(Errno::Badf.into()),
558
+ };
559
+ mem.write_u32(nwritten, written as u32)
560
+ }
561
+
562
+ /// A zero-length read of a stdin that still holds bytes is `INTR`, and one
563
+ /// of a drained stdin reads nothing, as wasmtime-wasi's blocking read
564
+ /// answers them.
565
+ fn read_stdin(pipe: &mut MemoryInputPipe, len: usize) -> Answer<Vec<u8>> {
566
+ match pipe.read(len) {
567
+ Ok(bytes) if bytes.is_empty() => Err(Errno::Intr.into()),
568
+ Ok(bytes) => Ok(bytes.to_vec()),
569
+ Err(StreamError::Closed) => Ok(Vec::new()),
570
+ Err(_) => Err(Errno::Io.into()),
571
+ }
572
+ }
573
+
574
+ /// Fill the capture pipe as far as it holds. A pipe that fills before the
575
+ /// bytes run out answers `IO`, though what fit is kept: the cap one past
576
+ /// the configured limit is how a capture learns it was truncated.
577
+ fn write_channel(pipe: &mut MemoryOutputPipe, bytes: &[u8]) -> Answer<usize> {
578
+ for chunk in bytes.chunks(WRITE_CHUNK) {
579
+ let mut rest = chunk;
580
+ while !rest.is_empty() {
581
+ let permit = pipe.check_write().map_err(|_| Errno::Io)?;
582
+ let (now, later) = rest.split_at(rest.len().min(permit));
583
+ pipe.write(Bytes::copy_from_slice(now))
584
+ .map_err(|_| Errno::Io)?;
585
+ rest = later;
586
+ }
587
+ }
588
+ Ok(bytes.len())
589
+ }
590
+
591
+ #[cfg(test)]
592
+ mod tests {
593
+ //! Witness that renumbering onto an open file closes it as `fd_close`
594
+ //! does, so its Filesystem learns of the close and may refuse it.
595
+ use std::sync::Arc;
596
+
597
+ use kobako_runtime::filesystem::{Access, File, FilesystemError, Memory, Metadata};
598
+ use kobako_runtime::mount::Mount;
599
+ use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
600
+
601
+ use super::renumber;
602
+ use crate::descriptors::{Descriptor, Descriptors, OpenFile};
603
+ use crate::preview1::errno::{Errno, Failure};
604
+
605
+ /// A file whose Filesystem refuses to release it.
606
+ struct RefusesClose;
607
+
608
+ impl File for RefusesClose {
609
+ fn read_at(&mut self, _buf: &mut [u8], _offset: u64) -> Result<usize, FilesystemError> {
610
+ Ok(0)
611
+ }
612
+
613
+ fn write_at(&mut self, buf: &[u8], _offset: u64) -> Result<usize, FilesystemError> {
614
+ Ok(buf.len())
615
+ }
616
+
617
+ fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
618
+ Ok((0, buf.len()))
619
+ }
620
+
621
+ fn metadata(&self) -> Result<Metadata, FilesystemError> {
622
+ Err(FilesystemError::Unsupported)
623
+ }
624
+
625
+ fn set_len(&mut self, _len: u64) -> Result<(), FilesystemError> {
626
+ Ok(())
627
+ }
628
+
629
+ fn close(self: Box<Self>) -> Result<(), FilesystemError> {
630
+ Err(FilesystemError::PermissionDenied)
631
+ }
632
+ }
633
+
634
+ fn open_file() -> Descriptor {
635
+ Descriptor::File(OpenFile {
636
+ file: Box::new(RefusesClose),
637
+ position: 0,
638
+ append: false,
639
+ readable: true,
640
+ writable: false,
641
+ mount: Arc::new(
642
+ Mount::new("/data", Arc::new(Memory::new()), Access::Read)
643
+ .expect("/data is a guest path"),
644
+ ),
645
+ })
646
+ }
647
+
648
+ // @behavior RT-114
649
+ #[test]
650
+ fn renumbering_onto_an_open_file_closes_it_first() {
651
+ let mut table = Descriptors::new(
652
+ MemoryInputPipe::new(Vec::new()),
653
+ MemoryOutputPipe::new(1),
654
+ MemoryOutputPipe::new(1),
655
+ Vec::new(),
656
+ None,
657
+ );
658
+ let from = table.push(open_file()).expect("a descriptor to move");
659
+ let to = table.push(open_file()).expect("a descriptor to move onto");
660
+ assert!(
661
+ matches!(
662
+ renumber(&mut table, from, to),
663
+ Err(Failure::Errno(Errno::Acces))
664
+ ),
665
+ "fd_renumber onto a file whose Filesystem refuses the close must answer that refusal"
666
+ );
667
+ assert!(
668
+ table.get(to).is_none() && table.get(from).is_some(),
669
+ "a refused close through fd_renumber must leave the target closed and the source in place"
670
+ );
671
+ }
672
+ }