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,2499 @@
1
+ //! Witness what a guest sees through a Mount: the preopen at the Mount's
2
+ //! path, the files a Filesystem holds, and the refusals the runtime
3
+ //! answers before the Filesystem is asked.
4
+
5
+ use std::sync::atomic::{AtomicUsize, Ordering};
6
+ use std::sync::Arc;
7
+
8
+ use kobako_runtime::filesystem::Access;
9
+ use kobako_runtime::filesystem::{
10
+ DirEntry, Directory, File, Filesystem, FilesystemError, Hold, Memory, Metadata, OpenOptions,
11
+ Overlay, Rename, Temporary,
12
+ };
13
+ use kobako_runtime::grant::Grant;
14
+ use kobako_runtime::mount::{Mount, Policy};
15
+ use kobako_runtime::profile::Profile;
16
+ use wasmtime::{Instance, Module, Store};
17
+
18
+ use crate::cache::shared_engine;
19
+ use crate::config::Config;
20
+ use crate::imports;
21
+ use crate::invocation::Invocation;
22
+ use crate::trap::NO_TIMEOUT_EPOCH_DELTA;
23
+ use crate::wasi::Wasi;
24
+
25
+ /// The answer of a test Filesystem holding no links.
26
+ macro_rules! holds_no_links {
27
+ () => {
28
+ fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
29
+ self.metadata(path)
30
+ }
31
+
32
+ fn canonicalize_beneath(&self, _base: &str, path: &str) -> Result<String, FilesystemError> {
33
+ Ok(path.to_string())
34
+ }
35
+
36
+ fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
37
+ None
38
+ }
39
+ };
40
+ }
41
+
42
+ /// The answer of a test Filesystem matching names byte for byte.
43
+ macro_rules! matches_names_exactly {
44
+ () => {
45
+ fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
46
+ Ok(path.to_string())
47
+ }
48
+ };
49
+ }
50
+
51
+ /// The answer of a test Filesystem whose identities never repeat.
52
+ macro_rules! never_repeats_identities {
53
+ () => {
54
+ fn hold_directory(&self, _path: &str) -> Result<Option<Hold>, FilesystemError> {
55
+ Ok(None)
56
+ }
57
+ };
58
+ }
59
+
60
+ /// The answer of a test Filesystem every Invocation shares.
61
+ macro_rules! is_shared {
62
+ () => {
63
+ fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
64
+ None
65
+ }
66
+ };
67
+ }
68
+
69
+ const ERRNO_ACCES: i32 = 2;
70
+ const ERRNO_BADF: i32 = 8;
71
+ const ERRNO_BUSY: i32 = 10;
72
+ const ERRNO_FBIG: i32 = 22;
73
+ const ERRNO_INVAL: i32 = 28;
74
+ const ERRNO_IO: i32 = 29;
75
+ const ERRNO_ISDIR: i32 = 31;
76
+ const ERRNO_LOOP: i32 = 32;
77
+ const ERRNO_NOENT: i32 = 44;
78
+ const ERRNO_NOTDIR: i32 = 54;
79
+ const ERRNO_NOTSUP: i32 = 58;
80
+ const ERRNO_ROFS: i32 = 69;
81
+ const ERRNO_XDEV: i32 = 75;
82
+ const ERRNO_NOTCAPABLE: i32 = 76;
83
+
84
+ /// Memory holds an `iovec` at 64 over 512..576, a `ciovec` at 80 over
85
+ /// "hello" at 600, the paths `file.txt` at 700 and `../file.txt` at 720,
86
+ /// and from 740 the paths the resolution rules turn on. An opened
87
+ /// descriptor lands at 128, a count at 132, a listing's count at 136, a
88
+ /// stat at 384, and a listing at 2048.
89
+ const PROBE_WAT: &str = r#"
90
+ (module
91
+ (import "wasi_snapshot_preview1" "fd_prestat_get" (func $prestat (param i32 i32) (result i32)))
92
+ (import "wasi_snapshot_preview1" "fd_prestat_dir_name" (func $prestat_name (param i32 i32 i32) (result i32)))
93
+ (import "wasi_snapshot_preview1" "path_open" (func $open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
94
+ (import "wasi_snapshot_preview1" "path_filestat_get" (func $stat (param i32 i32 i32 i32 i32) (result i32)))
95
+ (import "wasi_snapshot_preview1" "fd_read" (func $read (param i32 i32 i32 i32) (result i32)))
96
+ (import "wasi_snapshot_preview1" "fd_write" (func $write (param i32 i32 i32 i32) (result i32)))
97
+ (import "wasi_snapshot_preview1" "path_remove_directory" (func $rmdir (param i32 i32 i32) (result i32)))
98
+ (import "wasi_snapshot_preview1" "path_rename" (func $rename (param i32 i32 i32 i32 i32 i32) (result i32)))
99
+ (import "wasi_snapshot_preview1" "path_unlink_file" (func $unlink (param i32 i32 i32) (result i32)))
100
+ (import "wasi_snapshot_preview1" "fd_allocate" (func $allocate (param i32 i64 i64) (result i32)))
101
+ (import "wasi_snapshot_preview1" "fd_seek" (func $seek (param i32 i64 i32 i32) (result i32)))
102
+ (import "wasi_snapshot_preview1" "fd_readdir" (func $readdir (param i32 i32 i32 i64 i32) (result i32)))
103
+ (import "wasi_snapshot_preview1" "path_create_directory" (func $mkdir (param i32 i32 i32) (result i32)))
104
+ (import "wasi_snapshot_preview1" "fd_pread" (func $pread (param i32 i32 i32 i64 i32) (result i32)))
105
+ (import "wasi_snapshot_preview1" "fd_pwrite" (func $pwrite (param i32 i32 i32 i64 i32) (result i32)))
106
+ (import "wasi_snapshot_preview1" "fd_filestat_set_size" (func $set_size (param i32 i64) (result i32)))
107
+ (import "wasi_snapshot_preview1" "fd_advise" (func $advise (param i32 i64 i64 i32) (result i32)))
108
+ (import "wasi_snapshot_preview1" "fd_fdstat_get" (func $fdstat (param i32 i32) (result i32)))
109
+ (import "wasi_snapshot_preview1" "fd_fdstat_set_flags" (func $set_flags (param i32 i32) (result i32)))
110
+ (import "wasi_snapshot_preview1" "poll_oneoff" (func $poll (param i32 i32 i32 i32) (result i32)))
111
+ (memory (export "memory") 1)
112
+ (data (i32.const 740) ".")
113
+ (data (i32.const 760) "missing/../file.txt")
114
+ (data (i32.const 780) "file.txt/..")
115
+ (data (i32.const 800) "file.txt/")
116
+ (data (i32.const 820) "new.txt/")
117
+ (data (i32.const 840) "new.txt")
118
+ (data (i32.const 860) "file.txt/key")
119
+ (data (i32.const 880) "dir/.")
120
+ (data (i32.const 890) "dir/..")
121
+ (data (i32.const 900) "file.txt/x")
122
+ (data (i32.const 920) "moved")
123
+ (data (i32.const 930) "dir")
124
+ (data (i32.const 940) "missing/.")
125
+ (data (i32.const 960) "inner")
126
+ (data (i32.const 970) "file.txt/.")
127
+ (data (i32.const 990) "file.txt/../file.txt")
128
+ (data (i32.const 1010) "file.txt\00")
129
+ (data (i32.const 1030) "dir/file.txt")
130
+ (data (i32.const 1050) "dir/")
131
+ (data (i32.const 1060) "gone/")
132
+ (data (i32.const 1070) "up")
133
+ (data (i32.const 1080) "updir/x")
134
+ (data (i32.const 1090) "in/y")
135
+ (data (i32.const 1100) "twisty")
136
+ (data (i32.const 1110) "dangle")
137
+ (data (i32.const 1120) "dir/up")
138
+ (data (i32.const 1130) "nowhere")
139
+ (data (i32.const 1150) "pub/x")
140
+ ;; An iovec naming 16 bytes past the one page of memory.
141
+ (data (i32.const 1140) "\00\00\02\00\10\00\00\00")
142
+ (data (i32.const 64) "\00\02\00\00\40\00\00\00")
143
+ (data (i32.const 80) "\58\02\00\00\05\00\00\00")
144
+ (data (i32.const 600) "hello")
145
+ (data (i32.const 700) "file.txt")
146
+ (data (i32.const 720) "../file.txt")
147
+
148
+ (func (export "preopen_name") (result i32)
149
+ (local $errno i32)
150
+ (local.set $errno (call $prestat (i32.const 3) (i32.const 256)))
151
+ (if (local.get $errno) (then (return (local.get $errno))))
152
+ (call $prestat_name (i32.const 3) (i32.const 512) (i32.load (i32.const 260))))
153
+
154
+ ;; Open the path read-only and read it into 512.
155
+ (func (export "read_file") (param $path i32) (param $len i32) (result i32)
156
+ (local $errno i32)
157
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
158
+ (i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
159
+ (if (local.get $errno) (then (return (local.get $errno))))
160
+ (call $read (i32.load (i32.const 128)) (i32.const 64) (i32.const 1) (i32.const 132)))
161
+
162
+ ;; Open the path with read rights only, asking to truncate it.
163
+ (func (export "truncate_file") (param $path i32) (param $len i32) (result i32)
164
+ (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
165
+ (i32.const 8) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
166
+
167
+ ;; Create or truncate the path and write "hello" into it.
168
+ (func (export "write_file") (param $path i32) (param $len i32) (result i32)
169
+ (local $errno i32)
170
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
171
+ (i32.const 9) (i64.const 64) (i64.const 0) (i32.const 0) (i32.const 128)))
172
+ (if (local.get $errno) (then (return (local.get $errno))))
173
+ (call $write (i32.load (i32.const 128)) (i32.const 80) (i32.const 1) (i32.const 132)))
174
+
175
+ ;; Open the path read-only with the given oflags and fdflags.
176
+ (func (export "open_flagged") (param $path i32) (param $len i32) (param $oflags i32) (param $fdflags i32) (result i32)
177
+ (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
178
+ (local.get $oflags) (i64.const 2) (i64.const 0) (local.get $fdflags) (i32.const 128)))
179
+
180
+ ;; Stat the path with the given lookupflags into 384.
181
+ (func (export "stat_flagged") (param $path i32) (param $len i32) (param $flags i32) (result i32)
182
+ (call $stat (i32.const 3) (local.get $flags) (local.get $path) (local.get $len) (i32.const 384)))
183
+
184
+ ;; Change the root `.` at 740.
185
+ (func (export "remove_root") (result i32)
186
+ (call $rmdir (i32.const 3) (i32.const 740) (i32.const 1)))
187
+ (func (export "rename_root") (result i32)
188
+ (call $rename (i32.const 3) (i32.const 740) (i32.const 1) (i32.const 3) (i32.const 700) (i32.const 8)))
189
+ (func (export "rename_onto_root") (result i32)
190
+ (call $rename (i32.const 3) (i32.const 700) (i32.const 8) (i32.const 3) (i32.const 740) (i32.const 1)))
191
+ ;; Open the path to write and append, and write "hello" into it.
192
+ (func (export "append_file") (param $path i32) (param $len i32) (result i32)
193
+ (local $errno i32)
194
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
195
+ (i32.const 0) (i64.const 64) (i64.const 0) (i32.const 1) (i32.const 128)))
196
+ (if (local.get $errno) (then (return (local.get $errno))))
197
+ (call $write (i32.load (i32.const 128)) (i32.const 80) (i32.const 1) (i32.const 132)))
198
+
199
+ ;; Open the path to write and allocate 100 bytes from its start.
200
+ (func (export "allocate_file") (param $path i32) (param $len i32) (result i32)
201
+ (local $errno i32)
202
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (local.get $path) (local.get $len)
203
+ (i32.const 0) (i64.const 64) (i64.const 0) (i32.const 0) (i32.const 128)))
204
+ (if (local.get $errno) (then (return (local.get $errno))))
205
+ (call $allocate (i32.load (i32.const 128)) (i64.const 0) (i64.const 100)))
206
+
207
+ ;; Rename `file.txt` to `new.txt`.
208
+ (func (export "rename_to_new") (result i32)
209
+ (call $rename (i32.const 3) (i32.const 700) (i32.const 8) (i32.const 3) (i32.const 840) (i32.const 7)))
210
+ ;; Rename `file.txt` onto `file.txt/`, its own path as a directory.
211
+ (func (export "rename_onto_itself_as_directory") (result i32)
212
+ (call $rename (i32.const 3) (i32.const 700) (i32.const 8) (i32.const 3) (i32.const 800) (i32.const 9)))
213
+ (func (export "unlink_root") (result i32)
214
+ (call $unlink (i32.const 3) (i32.const 740) (i32.const 1)))
215
+ (func (export "unlink_path") (param $path i32) (param $len i32) (result i32)
216
+ (call $unlink (i32.const 3) (local.get $path) (local.get $len)))
217
+ (func (export "remove_path") (param $path i32) (param $len i32) (result i32)
218
+ (call $rmdir (i32.const 3) (local.get $path) (local.get $len)))
219
+ (func (export "make_dir") (param $path i32) (param $len i32) (result i32)
220
+ (call $mkdir (i32.const 3) (local.get $path) (local.get $len)))
221
+ (func (export "rename_paths") (param $from i32) (param $from_len i32) (param $to i32) (param $to_len i32) (result i32)
222
+ (call $rename (i32.const 3) (local.get $from) (local.get $from_len) (i32.const 3) (local.get $to) (local.get $to_len)))
223
+ ;; Open `file.txt`, seek it to the largest offset, then that far again.
224
+ (func (export "seek_past_the_largest_offset") (result i32)
225
+ (local $errno i32)
226
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
227
+ (i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
228
+ (if (local.get $errno) (then (return (local.get $errno))))
229
+ (local.set $errno (call $seek (i32.load (i32.const 128)) (i64.const 0x7fffffffffffffff) (i32.const 0) (i32.const 136)))
230
+ (if (local.get $errno) (then (return (local.get $errno))))
231
+ (call $seek (i32.load (i32.const 128)) (i64.const 0x7fffffffffffffff) (i32.const 1) (i32.const 136)))
232
+ ;; Open `file.txt` to read and write into 128.
233
+ (func $open_read_write (result i32)
234
+ (call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
235
+ (i32.const 0) (i64.const 66) (i64.const 0) (i32.const 0) (i32.const 128)))
236
+ ;; Read `file.txt` into 512 at `offset`.
237
+ (func (export "read_at") (param $offset i64) (result i32)
238
+ (local $errno i32)
239
+ (local.set $errno (call $open_read_write))
240
+ (if (local.get $errno) (then (return (local.get $errno))))
241
+ (call $pread (i32.load (i32.const 128)) (i32.const 64) (i32.const 1) (local.get $offset) (i32.const 132)))
242
+ ;; Write "hello" into `file.txt` at `offset`.
243
+ (func (export "write_at") (param $offset i64) (result i32)
244
+ (local $errno i32)
245
+ (local.set $errno (call $open_read_write))
246
+ (if (local.get $errno) (then (return (local.get $errno))))
247
+ (call $pwrite (i32.load (i32.const 128)) (i32.const 80) (i32.const 1) (local.get $offset) (i32.const 132)))
248
+ ;; Open `file.txt`, seek it to ten before the largest offset, and read
249
+ ;; the 64 bytes the iovec at 64 holds.
250
+ (func (export "read_near_the_largest_offset") (result i32)
251
+ (local $errno i32)
252
+ (local.set $errno (call $open_read_write))
253
+ (if (local.get $errno) (then (return (local.get $errno))))
254
+ (local.set $errno (call $seek (i32.load (i32.const 128)) (i64.const 0x7ffffffffffffff5) (i32.const 0) (i32.const 136)))
255
+ (if (local.get $errno) (then (return (local.get $errno))))
256
+ (call $read (i32.load (i32.const 128)) (i32.const 64) (i32.const 1) (i32.const 132)))
257
+ ;; Open `file.txt` with `rights`, then wait to write to it with the one
258
+ ;; subscription at 1200, its event landing at 1264.
259
+ (func (export "poll_write_with") (param $rights i64) (result i32)
260
+ (local $errno i32)
261
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
262
+ (i32.const 0) (local.get $rights) (i64.const 0) (i32.const 0) (i32.const 128)))
263
+ (if (local.get $errno) (then (return (local.get $errno))))
264
+ (i32.store8 (i32.const 1208) (i32.const 2))
265
+ (i32.store (i32.const 1216) (i32.load (i32.const 128)))
266
+ (call $poll (i32.const 1200) (i32.const 1264) (i32.const 1) (i32.const 1296)))
267
+ ;; Open `file.txt`, seek it to the largest offset, and write "hello".
268
+ (func (export "write_at_the_largest_offset") (result i32)
269
+ (local $errno i32)
270
+ (local.set $errno (call $open_read_write))
271
+ (if (local.get $errno) (then (return (local.get $errno))))
272
+ (local.set $errno (call $seek (i32.load (i32.const 128)) (i64.const 0x7fffffffffffffff) (i32.const 0) (i32.const 136)))
273
+ (if (local.get $errno) (then (return (local.get $errno))))
274
+ (call $write (i32.load (i32.const 128)) (i32.const 80) (i32.const 1) (i32.const 132)))
275
+ ;; Advise on `file.txt` from `offset`, and for `offset` bytes from 0.
276
+ (func (export "advise_from") (param $offset i64) (result i32)
277
+ (local $errno i32)
278
+ (local.set $errno (call $open_read_write))
279
+ (if (local.get $errno) (then (return (local.get $errno))))
280
+ (call $advise (i32.load (i32.const 128)) (local.get $offset) (i64.const 0) (i32.const 0)))
281
+ (func (export "advise_for") (param $len i64) (result i32)
282
+ (local $errno i32)
283
+ (local.set $errno (call $open_read_write))
284
+ (if (local.get $errno) (then (return (local.get $errno))))
285
+ (call $advise (i32.load (i32.const 128)) (i64.const 0) (local.get $len) (i32.const 0)))
286
+ ;; Open `file.txt`, then set it not to block.
287
+ (func (export "set_nonblocking") (result i32)
288
+ (local $errno i32)
289
+ (local.set $errno (call $open_read_write))
290
+ (if (local.get $errno) (then (return (local.get $errno))))
291
+ (call $set_flags (i32.load (i32.const 128)) (i32.const 4)))
292
+ ;; Open `dir` as a directory and read its fdstat into 384.
293
+ (func (export "dir_fdstat") (result i32)
294
+ (local $errno i32)
295
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
296
+ (i32.const 2) (i64.const 0) (i64.const 2) (i32.const 0) (i32.const 128)))
297
+ (if (local.get $errno) (then (return (local.get $errno))))
298
+ (call $fdstat (i32.load (i32.const 128)) (i32.const 384)))
299
+ ;; Size `file.txt` to `offset` bytes.
300
+ (func (export "size_at") (param $offset i64) (result i32)
301
+ (local $errno i32)
302
+ (local.set $errno (call $open_read_write))
303
+ (if (local.get $errno) (then (return (local.get $errno))))
304
+ (call $set_size (i32.load (i32.const 128)) (local.get $offset)))
305
+ ;; Open `file.txt` with `rights`, then read or write it through the
306
+ ;; iovec past memory at 1140.
307
+ (func $open_with (param $rights i64) (result i32)
308
+ (call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
309
+ (i32.const 0) (local.get $rights) (i64.const 0) (i32.const 0) (i32.const 128)))
310
+ (func (export "read_far_with") (param $rights i64) (result i32)
311
+ (local $errno i32)
312
+ (local.set $errno (call $open_with (local.get $rights)))
313
+ (if (local.get $errno) (then (return (local.get $errno))))
314
+ (call $read (i32.load (i32.const 128)) (i32.const 1140) (i32.const 1) (i32.const 132)))
315
+ (func (export "pread_far_with") (param $rights i64) (result i32)
316
+ (local $errno i32)
317
+ (local.set $errno (call $open_with (local.get $rights)))
318
+ (if (local.get $errno) (then (return (local.get $errno))))
319
+ (call $pread (i32.load (i32.const 128)) (i32.const 1140) (i32.const 1) (i64.const 0) (i32.const 132)))
320
+ (func (export "write_far_with") (param $rights i64) (result i32)
321
+ (local $errno i32)
322
+ (local.set $errno (call $open_with (local.get $rights)))
323
+ (if (local.get $errno) (then (return (local.get $errno))))
324
+ (call $write (i32.load (i32.const 128)) (i32.const 1140) (i32.const 1) (i32.const 132)))
325
+ (func (export "pwrite_far_with") (param $rights i64) (result i32)
326
+ (local $errno i32)
327
+ (local.set $errno (call $open_with (local.get $rights)))
328
+ (if (local.get $errno) (then (return (local.get $errno))))
329
+ (call $pwrite (i32.load (i32.const 128)) (i32.const 1140) (i32.const 1) (i64.const 0) (i32.const 132)))
330
+ ;; Open `file.txt` with `rights`, then size it to one byte.
331
+ (func (export "size_with") (param $rights i64) (result i32)
332
+ (local $errno i32)
333
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 700) (i32.const 8)
334
+ (i32.const 0) (local.get $rights) (i64.const 0) (i32.const 0) (i32.const 128)))
335
+ (if (local.get $errno) (then (return (local.get $errno))))
336
+ (call $set_size (i32.load (i32.const 128)) (i64.const 1)))
337
+ ;; List the root from `cookie` into the `len` bytes at 2048.
338
+ (func (export "list") (param $cookie i64) (param $len i32) (result i32)
339
+ (call $readdir (i32.const 3) (i32.const 2048) (local.get $len) (local.get $cookie) (i32.const 136)))
340
+ ;; Open `path` for reading, following a link standing at its end.
341
+ (func (export "open_following") (param $path i32) (param $len i32) (result i32)
342
+ (call $open (i32.const 3) (i32.const 1) (local.get $path) (local.get $len)
343
+ (i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
344
+ ;; Open `dir` as a directory, then the path beneath that descriptor.
345
+ (func (export "open_beneath_dir") (param $path i32) (param $len i32) (result i32)
346
+ (local $errno i32)
347
+ (local.set $errno (call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
348
+ (i32.const 2) (i64.const 0) (i64.const 2) (i32.const 0) (i32.const 128)))
349
+ (if (local.get $errno) (then (return (local.get $errno))))
350
+ (call $open (i32.load (i32.const 128)) (i32.const 0) (local.get $path) (local.get $len)
351
+ (i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 132)))
352
+ ;; As `open_beneath_dir`, following a link at the path's end.
353
+ (func (export "open_following_beneath_dir") (param $path i32) (param $len i32) (result i32)
354
+ (local $errno i32)
355
+ (local.set $errno (call $open_dir))
356
+ (if (local.get $errno) (then (return (local.get $errno))))
357
+ (call $open (i32.load (i32.const 128)) (i32.const 1) (local.get $path) (local.get $len)
358
+ (i32.const 0) (i64.const 2) (i64.const 0) (i32.const 0) (i32.const 132)))
359
+ ;; As `open_following_beneath_dir`, creating a file to write.
360
+ (func (export "create_following_beneath_dir") (param $path i32) (param $len i32) (result i32)
361
+ (local $errno i32)
362
+ (local.set $errno (call $open_dir))
363
+ (if (local.get $errno) (then (return (local.get $errno))))
364
+ (call $open (i32.load (i32.const 128)) (i32.const 1) (local.get $path) (local.get $len)
365
+ (i32.const 1) (i64.const 0x40) (i64.const 0) (i32.const 0) (i32.const 132)))
366
+ ;; Stat the path beneath `dir`, following a link at its end, into 384.
367
+ (func (export "stat_following_beneath_dir") (param $path i32) (param $len i32) (result i32)
368
+ (local $errno i32)
369
+ (local.set $errno (call $open_dir))
370
+ (if (local.get $errno) (then (return (local.get $errno))))
371
+ (call $stat (i32.load (i32.const 128)) (i32.const 1) (local.get $path) (local.get $len) (i32.const 384)))
372
+ ;; Open `dir` as a directory into 128.
373
+ (func $open_dir (export "open_dir") (result i32)
374
+ (call $open (i32.const 3) (i32.const 0) (i32.const 930) (i32.const 3)
375
+ (i32.const 2) (i64.const 0) (i64.const 2) (i32.const 0) (i32.const 128)))
376
+ ;; Stat `inner` beneath the directory opened into 128.
377
+ (func (export "stat_inner") (result i32)
378
+ (call $stat (i32.load (i32.const 128)) (i32.const 0) (i32.const 960) (i32.const 5) (i32.const 384))))
379
+ "#;
380
+
381
+ const FILE: (i32, i32) = (700, 8);
382
+ const ABOVE_ROOT: (i32, i32) = (720, 11);
383
+ const EMPTY: (i32, i32) = (700, 0);
384
+ const THROUGH_MISSING: (i32, i32) = (760, 19);
385
+ const OUT_OF_FILE: (i32, i32) = (780, 11);
386
+ const FILE_AS_DIRECTORY: (i32, i32) = (800, 9);
387
+ const NEW_AS_DIRECTORY: (i32, i32) = (820, 8);
388
+ const THROUGH_FILE: (i32, i32) = (860, 12);
389
+ const DIR_DOT: (i32, i32) = (880, 5);
390
+ const DIR_DOT_DOT: (i32, i32) = (890, 6);
391
+ const BENEATH_FILE: (i32, i32) = (900, 10);
392
+ const MOVED: (i32, i32) = (920, 5);
393
+ const DIR: (i32, i32) = (930, 3);
394
+ const MISSING_DOT: (i32, i32) = (940, 9);
395
+ const LINK_DOT: (i32, i32) = (970, 10);
396
+ const OUT_OF_LINK: (i32, i32) = (990, 20);
397
+ const HOLDING_NUL: (i32, i32) = (1010, 9);
398
+ const DIR_FILE: (i32, i32) = (1030, 12);
399
+ const DIR_AS_DIRECTORY: (i32, i32) = (1050, 4);
400
+ const GONE_AS_DIRECTORY: (i32, i32) = (1060, 5);
401
+ const ERRNO_OVERFLOW: i32 = 61;
402
+ const ERRNO_EXIST: i32 = 20;
403
+ const OFLAGS_EXCL: i32 = 1 << 2;
404
+
405
+ /// The default `OpenOptions` with `set` applied.
406
+ fn build_options(set: impl FnOnce(&mut OpenOptions)) -> OpenOptions {
407
+ let mut options = OpenOptions::default();
408
+ set(&mut options);
409
+ options
410
+ }
411
+
412
+ fn grant(
413
+ filesystem: Arc<dyn Filesystem>,
414
+ access: Access,
415
+ policy: Option<Arc<dyn Policy>>,
416
+ ) -> Grant {
417
+ let mount = Mount::new("/data", filesystem, access).expect("/data is a guest path");
418
+ Grant::Mount(match policy {
419
+ Some(policy) => mount.with_policy(policy),
420
+ None => mount,
421
+ })
422
+ }
423
+
424
+ /// One invocation's guest, handed `grants`.
425
+ struct Guest {
426
+ store: Store<Invocation>,
427
+ instance: Instance,
428
+ }
429
+
430
+ impl Guest {
431
+ fn new(grants: &[Grant]) -> Self {
432
+ let engine = shared_engine().expect("shared engine must be constructible");
433
+ let config = Config {
434
+ timeout: None,
435
+ setup_timeout: None,
436
+ memory_limit: None,
437
+ stdout_limit: None,
438
+ stderr_limit: None,
439
+ open_file_limit: None,
440
+ profile: Profile::Hermetic,
441
+ };
442
+ let wasi = Wasi::for_invocation(&config, &[], grants).expect("WASI context must build");
443
+ let mut store = Store::new(engine, Invocation::new(None, wasi));
444
+ store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
445
+ let module = Module::new(engine, PROBE_WAT).expect("probe module must compile");
446
+ let instance = imports::linker()
447
+ .expect("host imports must link")
448
+ .instantiate(&mut store, &module)
449
+ .expect("probe module must instantiate");
450
+ Self { store, instance }
451
+ }
452
+
453
+ fn call(&mut self, name: &str, path: (i32, i32)) -> i32 {
454
+ self.instance
455
+ .get_typed_func::<(i32, i32), i32>(&mut self.store, name)
456
+ .expect("probe export must resolve")
457
+ .call(&mut self.store, path)
458
+ .expect("probe call must not trap")
459
+ }
460
+
461
+ /// Open `path` read-only with `oflags` and `fdflags`.
462
+ fn call_flagged(&mut self, path: (i32, i32), oflags: i32, fdflags: i32) -> i32 {
463
+ self.instance
464
+ .get_typed_func::<(i32, i32, i32, i32), i32>(&mut self.store, "open_flagged")
465
+ .expect("probe export must resolve")
466
+ .call(&mut self.store, (path.0, path.1, oflags, fdflags))
467
+ .expect("probe call must not trap")
468
+ }
469
+
470
+ /// Call an export that takes two paths.
471
+ fn call_pair(&mut self, name: &str, first: (i32, i32), second: (i32, i32)) -> i32 {
472
+ self.instance
473
+ .get_typed_func::<(i32, i32, i32, i32), i32>(&mut self.store, name)
474
+ .expect("probe export must resolve")
475
+ .call(&mut self.store, (first.0, first.1, second.0, second.1))
476
+ .expect("probe call must not trap")
477
+ }
478
+
479
+ /// Call an export that takes an offset, `None` when it traps.
480
+ fn try_call_at(&mut self, name: &str, offset: u64) -> Option<i32> {
481
+ self.instance
482
+ .get_typed_func::<i64, i32>(&mut self.store, name)
483
+ .expect("probe export must resolve")
484
+ .call(&mut self.store, offset as i64)
485
+ .ok()
486
+ }
487
+
488
+ /// Call an export that takes an offset.
489
+ fn call_at(&mut self, name: &str, offset: u64) -> i32 {
490
+ self.instance
491
+ .get_typed_func::<i64, i32>(&mut self.store, name)
492
+ .expect("probe export must resolve")
493
+ .call(&mut self.store, offset as i64)
494
+ .expect("probe call must not trap")
495
+ }
496
+
497
+ /// Call an export that takes no path.
498
+ fn call_bare(&mut self, name: &str) -> i32 {
499
+ self.instance
500
+ .get_typed_func::<(), i32>(&mut self.store, name)
501
+ .expect("probe export must resolve")
502
+ .call(&mut self.store, ())
503
+ .expect("probe call must not trap")
504
+ }
505
+
506
+ /// The names a listing from `cookie` into `len` bytes holds whole.
507
+ fn list(&mut self, cookie: u64, len: i32) -> Vec<String> {
508
+ let errno = self
509
+ .instance
510
+ .get_typed_func::<(i64, i32), i32>(&mut self.store, "list")
511
+ .expect("probe export must resolve")
512
+ .call(&mut self.store, (cookie as i64, len))
513
+ .expect("probe call must not trap");
514
+ assert_eq!(errno, 0, "the listing must succeed");
515
+ let memory = self
516
+ .instance
517
+ .get_memory(&mut self.store, "memory")
518
+ .expect("probe exports its memory");
519
+ let data = memory.data(&self.store);
520
+ let used = u32::from_le_bytes(data[136..140].try_into().unwrap()) as usize;
521
+ let listed = &data[2048..2048 + used];
522
+ let mut names = Vec::new();
523
+ let mut at = 0;
524
+ while at + 24 <= listed.len() {
525
+ let len = u32::from_le_bytes(listed[at + 16..at + 20].try_into().unwrap()) as usize;
526
+ let Some(name) = listed.get(at + 24..at + 24 + len) else {
527
+ break;
528
+ };
529
+ names.push(String::from_utf8(name.to_vec()).expect("a listed name is UTF-8"));
530
+ at += 24 + len;
531
+ }
532
+ names
533
+ }
534
+
535
+ /// The bytes the last read placed at 512.
536
+ fn read_back(&mut self) -> Vec<u8> {
537
+ let memory = self
538
+ .instance
539
+ .get_memory(&mut self.store, "memory")
540
+ .expect("probe exports its memory");
541
+ let data = memory.data(&self.store);
542
+ let read = u32::from_le_bytes(data[132..136].try_into().unwrap()) as usize;
543
+ data[512..512 + read].to_vec()
544
+ }
545
+ }
546
+
547
+ fn contents(filesystem: &dyn Filesystem, path: &str) -> Option<Vec<u8>> {
548
+ let mut file = filesystem
549
+ .open(path, &build_options(|options| options.read = true))
550
+ .ok()?;
551
+ let mut buf = vec![0; 64];
552
+ let read = file.read_at(&mut buf, 0).ok()?;
553
+ buf.truncate(read);
554
+ Some(buf)
555
+ }
556
+
557
+ /// Make an empty file at `path`.
558
+ fn overwrite(filesystem: &dyn Filesystem, path: &str) {
559
+ filesystem
560
+ .open(
561
+ path,
562
+ &build_options(|options| (options.write, options.create) = (true, true)),
563
+ )
564
+ .expect("the file must be made");
565
+ }
566
+
567
+ struct Refuses(&'static str);
568
+
569
+ impl Policy for Refuses {
570
+ fn allows(&self, path: &str, _access: Access) -> bool {
571
+ path != self.0
572
+ }
573
+ }
574
+
575
+ struct AllowsAll;
576
+
577
+ impl Policy for AllowsAll {
578
+ fn allows(&self, _path: &str, _access: Access) -> bool {
579
+ true
580
+ }
581
+ }
582
+
583
+ /// A Memory storing every name in upper case while it reaches each by
584
+ /// any case, as a host that folds case does.
585
+ struct StoresUpperCase(Memory);
586
+
587
+ impl Filesystem for StoresUpperCase {
588
+ holds_no_links!();
589
+ never_repeats_identities!();
590
+ is_shared!();
591
+
592
+ fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
593
+ self.0.open(path, options)
594
+ }
595
+
596
+ fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
597
+ self.0.metadata(path)
598
+ }
599
+
600
+ fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
601
+ Ok(path.to_uppercase())
602
+ }
603
+
604
+ fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
605
+ self.0.read_dir(path)
606
+ }
607
+
608
+ fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
609
+ self.0.create_dir(path)
610
+ }
611
+
612
+ fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
613
+ self.0.remove_dir(path)
614
+ }
615
+
616
+ fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
617
+ self.0.remove_file(path)
618
+ }
619
+
620
+ fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
621
+ self.0.rename(from, to, rename)
622
+ }
623
+ }
624
+
625
+ // The Policy refuses only the stored spelling, so a Policy handed the
626
+ // spelling the guest asked for would let the read through.
627
+ // @behavior RT-120
628
+ #[test]
629
+ fn a_policy_judges_a_path_as_its_filesystem_stores_it() {
630
+ let memory: Memory = [("file.txt", "granted")].into_iter().collect();
631
+ let policy: Arc<dyn Policy> = Arc::new(Refuses("/data/FILE.TXT"));
632
+ let mut guest = Guest::new(&[grant(
633
+ Arc::new(StoresUpperCase(memory)),
634
+ Access::Read,
635
+ Some(policy),
636
+ )]);
637
+ assert_eq!(
638
+ guest.call("read_file", FILE),
639
+ ERRNO_ACCES,
640
+ "path_open of `file.txt` beneath a Mount whose Policy refuses `/data/FILE.TXT`, over a \
641
+ Filesystem storing that name as `FILE.TXT`, through preview1 must answer EACCES"
642
+ );
643
+ }
644
+
645
+ // @behavior RT-075
646
+ #[test]
647
+ fn a_mount_appears_as_a_directory_at_its_path() {
648
+ let mut guest = Guest::new(&[grant(Arc::new(Memory::new()), Access::Read, None)]);
649
+ assert_eq!(
650
+ guest.call_bare("preopen_name"),
651
+ 0,
652
+ "the first preopen must answer its name"
653
+ );
654
+ let memory = guest
655
+ .instance
656
+ .get_memory(&mut guest.store, "memory")
657
+ .expect("probe exports its memory");
658
+ assert_eq!(
659
+ &memory.data(&guest.store)[512..517],
660
+ b"/data",
661
+ "a Mount at /data through Runtime::invoke must preopen /data as descriptor 3"
662
+ );
663
+ }
664
+
665
+ // The first listing fills its buffer with `.` and `..` exactly, so it has
666
+ // not reached its end when the file is made.
667
+ // @behavior RT-107
668
+ #[test]
669
+ fn a_listing_goes_on_over_the_entries_it_began_with() {
670
+ let memory: Arc<Memory> = Arc::new([("a.txt", "")].into_iter().collect());
671
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
672
+ assert_eq!(
673
+ guest.list(0, 51),
674
+ [".", ".."],
675
+ "the first listing must hold the dots"
676
+ );
677
+ memory
678
+ .open(
679
+ "b.txt",
680
+ &build_options(|options| (options.write, options.create) = (true, true)),
681
+ )
682
+ .expect("the file must be made");
683
+ assert_eq!(
684
+ guest.list(2, 4096),
685
+ ["a.txt"],
686
+ "a listing going on through fd_readdir must read the entries it began with"
687
+ );
688
+ assert_eq!(
689
+ guest.list(0, 4096),
690
+ [".", "..", "a.txt", "b.txt"],
691
+ "a listing from the start through fd_readdir must read the directory as it stands"
692
+ );
693
+ }
694
+
695
+ // @behavior RT-076
696
+ #[test]
697
+ fn a_guest_reads_a_file_beneath_a_mount() {
698
+ let memory: Memory = [("file.txt", "granted")].into_iter().collect();
699
+ let mut guest = Guest::new(&[grant(Arc::new(memory), Access::Read, None)]);
700
+ assert_eq!(guest.call("read_file", FILE), 0, "the read must succeed");
701
+ assert_eq!(
702
+ guest.read_back(),
703
+ b"granted",
704
+ "a file read beneath a Mount through preview1 must read the Memory's content"
705
+ );
706
+ }
707
+
708
+ // @behavior RT-077
709
+ #[test]
710
+ fn a_guest_writes_a_file_beneath_a_read_write_mount() {
711
+ let memory = Arc::new(Memory::new());
712
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
713
+ assert_eq!(guest.call("write_file", FILE), 0, "the write must succeed");
714
+ assert_eq!(
715
+ contents(memory.as_ref(), "file.txt").as_deref(),
716
+ Some(&b"hello"[..]),
717
+ "a file written beneath a read-write Mount through preview1 must land in the Memory"
718
+ );
719
+ }
720
+
721
+ // @behavior RT-078
722
+ #[test]
723
+ fn a_change_beneath_a_read_only_mount_is_a_read_only_filesystem() {
724
+ let mut guest = Guest::new(&[grant(Arc::new(Memory::new()), Access::Read, None)]);
725
+ assert_eq!(
726
+ guest.call("write_file", FILE),
727
+ ERRNO_ROFS,
728
+ "a write opened beneath a read-only Mount through preview1 must answer EROFS"
729
+ );
730
+ }
731
+
732
+ // An open asking only to read can still truncate. wasmtime-wasi once let
733
+ // such an open past a read-only file permission (CVE-2026-47261), so the
734
+ // truncation alone must count as a change.
735
+ // @behavior RT-078
736
+ #[test]
737
+ fn a_truncating_open_beneath_a_read_only_mount_is_refused() {
738
+ let memory: Arc<Memory> = Arc::new([("file.txt", "granted")].into_iter().collect());
739
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::Read, None)]);
740
+ assert_eq!(
741
+ (
742
+ guest.call("truncate_file", FILE),
743
+ contents(memory.as_ref(), "file.txt")
744
+ ),
745
+ (ERRNO_ROFS, Some(b"granted".to_vec())),
746
+ "a read-rights open with O_TRUNC beneath a read-only Mount through preview1 must answer \
747
+ EROFS and leave the file whole"
748
+ );
749
+ }
750
+
751
+ // @behavior RT-079
752
+ #[test]
753
+ fn a_path_above_a_mounts_root_is_refused() {
754
+ let memory: Memory = [("file.txt", "granted")].into_iter().collect();
755
+ let mut guest = Guest::new(&[grant(Arc::new(memory), Access::Read, None)]);
756
+ assert_eq!(
757
+ guest.call("read_file", ABOVE_ROOT),
758
+ ERRNO_NOTCAPABLE,
759
+ "a path climbing above a Mount's root through preview1 must answer ENOTCAPABLE"
760
+ );
761
+ }
762
+
763
+ // A descriptor confines the guest to what lies beneath it, as cap-std
764
+ // confines wasmtime-wasi's, even where its Mount reaches further.
765
+ // @behavior RT-115
766
+ #[test]
767
+ fn a_path_above_the_directory_it_is_opened_beneath_is_refused() {
768
+ let memory: Memory = [("file.txt", "granted"), ("dir/inner.txt", "beneath")]
769
+ .into_iter()
770
+ .collect();
771
+ let mut guest = Guest::new(&[grant(Arc::new(memory), Access::Read, None)]);
772
+ assert_eq!(
773
+ guest.call("open_beneath_dir", ABOVE_ROOT),
774
+ ERRNO_NOTCAPABLE,
775
+ "a path climbing above a non-preopened directory descriptor through preview1 must answer \
776
+ ENOTCAPABLE, as wasmtime-wasi does"
777
+ );
778
+ }
779
+
780
+ /// What `stat_inner` answers through `dir` opened before `change` runs on
781
+ /// the Memory holding it.
782
+ fn stat_after(change: impl FnOnce(&Memory)) -> i32 {
783
+ let memory = file_and_directory();
784
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
785
+ assert_eq!(
786
+ guest.call_bare("open_dir"),
787
+ 0,
788
+ "dir must open as a directory"
789
+ );
790
+ change(&memory);
791
+ guest.call_bare("stat_inner")
792
+ }
793
+
794
+ // A directory made at the path holds an `inner` of its own, so only the
795
+ // identity check keeps the descriptor from reaching it.
796
+ // @behavior RT-117
797
+ #[test]
798
+ fn a_directory_made_in_an_opened_directorys_place_is_not_reached_through_it() {
799
+ let answers = (
800
+ stat_after(|memory| {
801
+ memory.rename("dir", "moved", Rename::Any).unwrap();
802
+ memory.create_dir("dir").unwrap();
803
+ overwrite(memory, "dir/inner");
804
+ }),
805
+ stat_after(|memory| {
806
+ memory.remove_file("dir/inner").unwrap();
807
+ memory.remove_dir("dir").unwrap();
808
+ memory.create_dir("dir").unwrap();
809
+ overwrite(memory, "dir/inner");
810
+ }),
811
+ );
812
+ assert_eq!(
813
+ answers,
814
+ (ERRNO_NOENT, ERRNO_NOENT),
815
+ "path_filestat_get through a directory descriptor beneath a Mount through preview1 must \
816
+ answer ENOENT once another directory stands at its path, moved in or made anew"
817
+ );
818
+ }
819
+
820
+ /// A Memory naming `dir` by one inode number for as long as nothing holds
821
+ /// it, as a host hands a removed directory's number to the next one made,
822
+ /// and by a fresh number once a held `dir` was removed.
823
+ struct ReusesInodes {
824
+ memory: Arc<Memory>,
825
+ holds: Arc<AtomicUsize>,
826
+ removed_while_held: AtomicUsize,
827
+ }
828
+
829
+ /// Counts as holding a directory until it drops.
830
+ struct Holding(Arc<AtomicUsize>);
831
+
832
+ impl Drop for Holding {
833
+ fn drop(&mut self) {
834
+ self.0.fetch_sub(1, Ordering::SeqCst);
835
+ }
836
+ }
837
+
838
+ impl Filesystem for ReusesInodes {
839
+ holds_no_links!();
840
+ matches_names_exactly!();
841
+ is_shared!();
842
+
843
+ fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
844
+ self.memory.open(path, options)
845
+ }
846
+
847
+ fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
848
+ let found = self.memory.metadata(path)?;
849
+ if path != "dir" {
850
+ return Ok(found);
851
+ }
852
+ let ino = 100 + self.removed_while_held.load(Ordering::SeqCst) as u64;
853
+ Ok(found.with_identity(1, ino))
854
+ }
855
+
856
+ fn hold_directory(&self, path: &str) -> Result<Option<Hold>, FilesystemError> {
857
+ if path != "dir" {
858
+ return Ok(None);
859
+ }
860
+ self.holds.fetch_add(1, Ordering::SeqCst);
861
+ Ok(Some(Hold::new(Holding(self.holds.clone()))))
862
+ }
863
+
864
+ fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
865
+ self.memory.read_dir(path)
866
+ }
867
+
868
+ fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
869
+ self.memory.create_dir(path)
870
+ }
871
+
872
+ fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
873
+ self.memory.remove_dir(path)?;
874
+ if self.holds.load(Ordering::SeqCst) > 0 {
875
+ self.removed_while_held.fetch_add(1, Ordering::SeqCst);
876
+ }
877
+ Ok(())
878
+ }
879
+
880
+ fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
881
+ self.memory.remove_file(path)
882
+ }
883
+
884
+ fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
885
+ self.memory.rename(from, to, rename)
886
+ }
887
+ }
888
+
889
+ // The Filesystem gives the new `dir` the old one's number unless a Hold
890
+ // on it lives, so only the descriptor keeping its Hold refuses the call.
891
+ // @behavior RT-121
892
+ #[test]
893
+ fn a_directory_descriptor_holds_its_directory_while_open() {
894
+ let memory = file_and_directory();
895
+ let reuses = Arc::new(ReusesInodes {
896
+ memory: memory.clone(),
897
+ holds: Arc::new(AtomicUsize::new(0)),
898
+ removed_while_held: AtomicUsize::new(0),
899
+ });
900
+ let mut guest = Guest::new(&[grant(reuses.clone(), Access::ReadWrite, None)]);
901
+ assert_eq!(
902
+ guest.call_bare("open_dir"),
903
+ 0,
904
+ "dir must open as a directory"
905
+ );
906
+ reuses.remove_file("dir/inner").unwrap();
907
+ reuses.remove_dir("dir").unwrap();
908
+ reuses.create_dir("dir").unwrap();
909
+ overwrite(memory.as_ref(), "dir/inner");
910
+ assert_eq!(
911
+ guest.call_bare("stat_inner"),
912
+ ERRNO_NOENT,
913
+ "path_filestat_get through a directory descriptor beneath a Mount through preview1, once \
914
+ its directory is removed and made again on a Filesystem reusing inode numbers, must \
915
+ answer ENOENT"
916
+ );
917
+ }
918
+
919
+ // The open looks the directory up without following a link at its end, so
920
+ // the identity it remembers comes from the Overlay's lstat, and every later
921
+ // call compares it with the Overlay's stat.
922
+ #[test]
923
+ fn a_directory_two_layers_hold_is_reached_through_its_descriptor() {
924
+ let upper: Arc<dyn Filesystem> = Arc::new([("dir/upper", "u")].into_iter().collect::<Memory>());
925
+ let lower: Arc<dyn Filesystem> = file_and_directory();
926
+ let overlay = Overlay::new()
927
+ .layer(upper, Access::ReadWrite)
928
+ .layer(lower, Access::Read);
929
+ let mut guest = Guest::new(&[grant(Arc::new(overlay), Access::ReadWrite, None)]);
930
+ assert_eq!(
931
+ (guest.call_bare("open_dir"), guest.call_bare("stat_inner")),
932
+ (0, 0),
933
+ "path_filestat_get through a directory descriptor of an Overlay whose two Layers hold the \
934
+ directory, beneath a Mount through preview1, must reach it"
935
+ );
936
+ }
937
+
938
+ // @behavior RT-118
939
+ #[test]
940
+ fn a_directory_moved_back_to_its_path_is_reached_through_its_descriptor_again() {
941
+ let answer = stat_after(|memory| {
942
+ memory.rename("dir", "moved", Rename::Any).unwrap();
943
+ memory.rename("moved", "dir", Rename::Any).unwrap();
944
+ });
945
+ assert_eq!(
946
+ answer, 0,
947
+ "path_filestat_get through a directory descriptor beneath a Mount through preview1 must \
948
+ reach the directory once it moves away and back"
949
+ );
950
+ }
951
+
952
+ // @behavior RT-080
953
+ #[test]
954
+ fn a_policy_refusing_an_open_answers_access_denied() {
955
+ let memory: Memory = [("file.txt", "granted")].into_iter().collect();
956
+ let policy: Arc<dyn Policy> = Arc::new(Refuses("/data/file.txt"));
957
+ let mut guest = Guest::new(&[grant(Arc::new(memory), Access::Read, Some(policy))]);
958
+ assert_eq!(
959
+ guest.call("read_file", FILE),
960
+ ERRNO_ACCES,
961
+ "an open a Mount's Policy refuses through preview1 must answer EACCES"
962
+ );
963
+ }
964
+
965
+ // @behavior RT-081
966
+ #[test]
967
+ fn a_policy_decides_in_the_place_of_a_mounts_access() {
968
+ let memory = Arc::new(Memory::new());
969
+ let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
970
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::Read, Some(policy))]);
971
+ assert_eq!(guest.call("write_file", FILE), 0, "the write must succeed");
972
+ assert_eq!(
973
+ contents(memory.as_ref(), "file.txt").as_deref(),
974
+ Some(&b"hello"[..]),
975
+ "a write a Policy allows beneath a read-only Mount through preview1 must land"
976
+ );
977
+ }
978
+
979
+ // @behavior RT-082
980
+ #[test]
981
+ fn each_invocation_is_handed_the_filesystem_it_starts_from() {
982
+ let grants = [grant(Arc::new(Temporary::new()), Access::ReadWrite, None)];
983
+ assert_eq!(
984
+ Guest::new(&grants).call("write_file", FILE),
985
+ 0,
986
+ "the write must succeed"
987
+ );
988
+ assert_eq!(
989
+ Guest::new(&grants).call("read_file", FILE),
990
+ ERRNO_NOENT,
991
+ "a file an earlier invocation wrote to a Temporary must not be found by the next one"
992
+ );
993
+ }
994
+
995
+ /// A Memory holding `file.txt` that counts every change it is asked for,
996
+ /// so a test can tell the runtime refused before asking it.
997
+ struct ChangeCounter {
998
+ memory: Memory,
999
+ changes: AtomicUsize,
1000
+ }
1001
+
1002
+ impl ChangeCounter {
1003
+ fn new() -> Arc<Self> {
1004
+ Arc::new(Self {
1005
+ memory: [("file.txt", "granted")].into_iter().collect(),
1006
+ changes: AtomicUsize::new(0),
1007
+ })
1008
+ }
1009
+
1010
+ fn change(&self) {
1011
+ self.changes.fetch_add(1, Ordering::Relaxed);
1012
+ }
1013
+ }
1014
+
1015
+ impl Filesystem for ChangeCounter {
1016
+ holds_no_links!();
1017
+ matches_names_exactly!();
1018
+ never_repeats_identities!();
1019
+ is_shared!();
1020
+
1021
+ fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
1022
+ self.memory.open(path, options)
1023
+ }
1024
+
1025
+ fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
1026
+ self.memory.metadata(path)
1027
+ }
1028
+
1029
+ fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
1030
+ self.memory.read_dir(path)
1031
+ }
1032
+
1033
+ fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
1034
+ self.change();
1035
+ self.memory.create_dir(path)
1036
+ }
1037
+
1038
+ fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
1039
+ self.change();
1040
+ self.memory.remove_dir(path)
1041
+ }
1042
+
1043
+ fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
1044
+ self.change();
1045
+ self.memory.remove_file(path)
1046
+ }
1047
+
1048
+ fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
1049
+ self.change();
1050
+ self.memory.rename(from, to, rename)
1051
+ }
1052
+ }
1053
+
1054
+ /// A Memory holding `file.txt` that, as a rename reaches it, replaces the
1055
+ /// file with a directory holding `secret`, as another invocation sharing
1056
+ /// the Memory could between the runtime's checks and the move.
1057
+ struct SwappedForADirectory(Memory);
1058
+
1059
+ impl SwappedForADirectory {
1060
+ fn new() -> Arc<Self> {
1061
+ Arc::new(Self([("file.txt", "granted")].into_iter().collect()))
1062
+ }
1063
+ }
1064
+
1065
+ impl Filesystem for SwappedForADirectory {
1066
+ holds_no_links!();
1067
+ matches_names_exactly!();
1068
+ never_repeats_identities!();
1069
+ is_shared!();
1070
+
1071
+ fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
1072
+ self.0.open(path, options)
1073
+ }
1074
+
1075
+ fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
1076
+ self.0.metadata(path)
1077
+ }
1078
+
1079
+ fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
1080
+ self.0.read_dir(path)
1081
+ }
1082
+
1083
+ fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
1084
+ self.0.create_dir(path)
1085
+ }
1086
+
1087
+ fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
1088
+ self.0.remove_dir(path)
1089
+ }
1090
+
1091
+ fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
1092
+ self.0.remove_file(path)
1093
+ }
1094
+
1095
+ fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
1096
+ self.0.remove_file(from)?;
1097
+ self.0.create_dir(from)?;
1098
+ drop(self.0.open(
1099
+ &format!("{from}/secret"),
1100
+ &build_options(|options| (options.write, options.create) = (true, true)),
1101
+ )?);
1102
+ self.0.rename(from, to, rename)
1103
+ }
1104
+ }
1105
+
1106
+ // The Policy allows everything, so only the rename's own refusal of a
1107
+ // directory, checked as the Filesystem moves it, keeps `secret` from
1108
+ // taking a path the Policy never judged.
1109
+ // @behavior RT-094
1110
+ #[test]
1111
+ fn a_directory_beneath_a_mount_with_a_policy_does_not_move() {
1112
+ let swapped = SwappedForADirectory::new();
1113
+ let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
1114
+ let mut guest = Guest::new(&[grant(swapped.clone(), Access::ReadWrite, Some(policy))]);
1115
+ assert_eq!(
1116
+ (
1117
+ guest.call_bare("rename_to_new"),
1118
+ swapped.metadata("new.txt/secret")
1119
+ ),
1120
+ (ERRNO_XDEV, Err(FilesystemError::NotFound)),
1121
+ "path_rename of what is a directory by the time it moves, beneath a Mount with a \
1122
+ Policy, through preview1 must answer EXDEV and move nothing"
1123
+ );
1124
+ }
1125
+
1126
+ /// A Memory whose files take another writer's `bbb` at their end as one
1127
+ /// is written, once the length the write would land at is known and
1128
+ /// before it lands, as another invocation sharing the file could.
1129
+ struct AppendedMeanwhile(Memory);
1130
+
1131
+ struct AppendedMeanwhileFile {
1132
+ file: Box<dyn File>,
1133
+ other: std::sync::Mutex<Box<dyn File>>,
1134
+ }
1135
+
1136
+ impl AppendedMeanwhileFile {
1137
+ fn other_appends(&self) -> Result<(), FilesystemError> {
1138
+ self.other
1139
+ .lock()
1140
+ .expect("the other writer's lock must not be poisoned")
1141
+ .append(b"bbb")
1142
+ .map(drop)
1143
+ }
1144
+ }
1145
+
1146
+ impl File for AppendedMeanwhileFile {
1147
+ fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
1148
+ self.file.read_at(buf, offset)
1149
+ }
1150
+
1151
+ fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
1152
+ self.file.write_at(buf, offset)
1153
+ }
1154
+
1155
+ /// The length is read before the other writer appends, so a write
1156
+ /// placed by it lands over what the other wrote.
1157
+ fn metadata(&self) -> Result<Metadata, FilesystemError> {
1158
+ let metadata = self.file.metadata()?;
1159
+ self.other_appends()?;
1160
+ Ok(metadata)
1161
+ }
1162
+
1163
+ fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
1164
+ self.file.set_len(len)
1165
+ }
1166
+
1167
+ fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
1168
+ self.other_appends()?;
1169
+ self.file.append(buf)
1170
+ }
1171
+ }
1172
+
1173
+ impl Filesystem for AppendedMeanwhile {
1174
+ holds_no_links!();
1175
+ matches_names_exactly!();
1176
+ never_repeats_identities!();
1177
+ is_shared!();
1178
+
1179
+ fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
1180
+ let writes = build_options(|options| options.write = true);
1181
+ Ok(Box::new(AppendedMeanwhileFile {
1182
+ file: self.0.open(path, options)?,
1183
+ other: std::sync::Mutex::new(self.0.open(path, &writes)?),
1184
+ }))
1185
+ }
1186
+
1187
+ fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
1188
+ self.0.metadata(path)
1189
+ }
1190
+
1191
+ fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
1192
+ self.0.read_dir(path)
1193
+ }
1194
+
1195
+ fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
1196
+ self.0.create_dir(path)
1197
+ }
1198
+
1199
+ fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
1200
+ self.0.remove_dir(path)
1201
+ }
1202
+
1203
+ fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
1204
+ self.0.remove_file(path)
1205
+ }
1206
+
1207
+ fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
1208
+ self.0.rename(from, to, rename)
1209
+ }
1210
+ }
1211
+
1212
+ // The other writer's `bbb` lands as the guest's append is made, so only
1213
+ // an append the file places itself keeps from writing over it.
1214
+ // @behavior RT-095
1215
+ #[test]
1216
+ fn an_append_lands_after_another_writers_append() {
1217
+ let memory: Memory = [("file.txt", "granted")].into_iter().collect();
1218
+ let shared = Arc::new(AppendedMeanwhile(memory));
1219
+ let mut guest = Guest::new(&[grant(shared.clone(), Access::ReadWrite, None)]);
1220
+ assert_eq!(
1221
+ (
1222
+ guest.call("append_file", FILE),
1223
+ contents(&shared.0, "file.txt")
1224
+ ),
1225
+ (0, Some(b"grantedbbbhello".to_vec())),
1226
+ "fd_write to a file opened to append beneath a Mount through preview1 must land after \
1227
+ what another writer appended meanwhile"
1228
+ );
1229
+ }
1230
+
1231
+ const FILETYPE_UNKNOWN: u8 = 0;
1232
+ const FILETYPE_DIRECTORY: u8 = 3;
1233
+ const FILETYPE_REGULAR_FILE: u8 = 4;
1234
+ const FILETYPE_SYMBOLIC_LINK: u8 = 7;
1235
+
1236
+ /// A host directory of its own holding `file.txt` as a link to `target`,
1237
+ /// made by `make_target`, and a guest handed it as a read-only Directory
1238
+ /// Mount.
1239
+ #[cfg(unix)]
1240
+ fn linked(name: &str, make_target: fn(&std::path::Path)) -> (std::path::PathBuf, Guest) {
1241
+ linked_under(name, make_target, Access::Read, None)
1242
+ }
1243
+
1244
+ /// As `linked`, the Mount taking `access` and `policy`.
1245
+ #[cfg(unix)]
1246
+ fn linked_under(
1247
+ name: &str,
1248
+ make_target: fn(&std::path::Path),
1249
+ access: Access,
1250
+ policy: Option<Arc<dyn Policy>>,
1251
+ ) -> (std::path::PathBuf, Guest) {
1252
+ let host = std::env::temp_dir().join(format!("kobako-granted-{name}-{}", std::process::id()));
1253
+ let _ = std::fs::remove_dir_all(&host);
1254
+ std::fs::create_dir_all(&host).expect("the host directory must be created");
1255
+ make_target(&host.join("target"));
1256
+ std::os::unix::fs::symlink("target", host.join("file.txt")).expect("the link must be made");
1257
+ let directory = Directory::new(&host).expect("the host directory must open");
1258
+ let guest = Guest::new(&[grant(Arc::new(directory), access, policy)]);
1259
+ (host, guest)
1260
+ }
1261
+
1262
+ /// What each stat of `path` with the given lookupflags answers, and the
1263
+ /// filetype it wrote, beneath a Directory Mount whose host directory
1264
+ /// holds `file.txt` as a link to `target`, made by `make_target`.
1265
+ #[cfg(unix)]
1266
+ fn stat_link(
1267
+ name: &str,
1268
+ make_target: fn(&std::path::Path),
1269
+ stats: &[((i32, i32), i32)],
1270
+ ) -> Vec<(i32, u8)> {
1271
+ let (host, mut guest) = linked(name, make_target);
1272
+ let answers = stats
1273
+ .iter()
1274
+ .map(|&(path, flags)| {
1275
+ let errno = guest
1276
+ .instance
1277
+ .get_typed_func::<(i32, i32, i32), i32>(&mut guest.store, "stat_flagged")
1278
+ .expect("probe export must resolve")
1279
+ .call(&mut guest.store, (path.0, path.1, flags))
1280
+ .expect("probe call must not trap");
1281
+ let memory = guest
1282
+ .instance
1283
+ .get_memory(&mut guest.store, "memory")
1284
+ .expect("probe exports its memory");
1285
+ (errno, memory.data(&guest.store)[384 + 16])
1286
+ })
1287
+ .collect();
1288
+ let _ = std::fs::remove_dir_all(&host);
1289
+ answers
1290
+ }
1291
+
1292
+ // A FIFO with no writer would hold an open that waits for one, so only
1293
+ // an open refused before it waits returns at all.
1294
+ // @behavior RT-127
1295
+ #[cfg(unix)]
1296
+ #[test]
1297
+ fn a_guest_sees_a_fifo_as_of_an_unknown_type_and_cannot_open_it() {
1298
+ let host = std::env::temp_dir().join(format!("kobako-granted-fifo-{}", std::process::id()));
1299
+ let _ = std::fs::remove_dir_all(&host);
1300
+ std::fs::create_dir_all(&host).expect("the host directory must be created");
1301
+ let made = std::process::Command::new("mkfifo")
1302
+ .arg(host.join("file.txt"))
1303
+ .status()
1304
+ .expect("mkfifo must run");
1305
+ assert!(made.success(), "the FIFO must be made");
1306
+ let directory = Directory::new(&host).expect("the host directory must open");
1307
+ let mut guest = Guest::new(&[grant(Arc::new(directory), Access::Read, None)]);
1308
+ let stat = guest
1309
+ .instance
1310
+ .get_typed_func::<(i32, i32, i32), i32>(&mut guest.store, "stat_flagged")
1311
+ .expect("probe export must resolve")
1312
+ .call(&mut guest.store, (FILE.0, FILE.1, 0))
1313
+ .expect("probe call must not trap");
1314
+ let filestat = guest
1315
+ .instance
1316
+ .get_memory(&mut guest.store, "memory")
1317
+ .expect("probe exports its memory")
1318
+ .data(&guest.store)[384..384 + 32]
1319
+ .to_vec();
1320
+ let nlink = u64::from_le_bytes(filestat[24..32].try_into().expect("nlink is 8 bytes"));
1321
+ let opened = guest.call("read_file", FILE);
1322
+ let _ = std::fs::remove_dir_all(&host);
1323
+ assert_eq!(
1324
+ (stat, filestat[16], nlink, opened),
1325
+ (0, FILETYPE_UNKNOWN, 1, ERRNO_ACCES),
1326
+ "path_filestat_get of a FIFO beneath a Directory Mount through preview1 must report an \
1327
+ unknown filetype named once, and path_open of it must answer EACCES"
1328
+ );
1329
+ }
1330
+
1331
+ // The link points at a regular file, so only a stat that follows none
1332
+ // can answer a link.
1333
+ // @behavior RT-097
1334
+ #[cfg(unix)]
1335
+ #[test]
1336
+ fn a_stat_following_no_link_reports_the_link_itself() {
1337
+ let answers = stat_link(
1338
+ "lstat",
1339
+ |target| std::fs::write(target, "granted").expect("the host file must be written"),
1340
+ &[(FILE, 1), (FILE, 0)],
1341
+ );
1342
+ assert_eq!(
1343
+ answers,
1344
+ [(0, FILETYPE_REGULAR_FILE), (0, FILETYPE_SYMBOLIC_LINK)],
1345
+ "path_filestat_get of a host link beneath a Directory Mount through preview1 must report \
1346
+ the file it points to when following links, and a symbolic link when following none"
1347
+ );
1348
+ }
1349
+
1350
+ // The stat follows no link, so only the trailing `/` can make it reach
1351
+ // the directory the link points at, as Linux's `lstat` does.
1352
+ // @behavior RT-098
1353
+ #[cfg(unix)]
1354
+ #[test]
1355
+ fn a_stat_of_a_link_named_with_a_trailing_slash_reports_its_directory() {
1356
+ let answers = stat_link(
1357
+ "lstat-slash",
1358
+ |target| std::fs::create_dir(target).expect("the host directory must be created"),
1359
+ &[(FILE_AS_DIRECTORY, 0), (FILE, 0)],
1360
+ );
1361
+ assert_eq!(
1362
+ answers,
1363
+ [(0, FILETYPE_DIRECTORY), (0, FILETYPE_SYMBOLIC_LINK)],
1364
+ "path_filestat_get following no link of a host link to a directory, named with a \
1365
+ trailing slash, beneath a Directory Mount through preview1 must report the directory, \
1366
+ and without the slash a symbolic link"
1367
+ );
1368
+ }
1369
+
1370
+ /// What `path_unlink_file` and `path_rename` answer for the link
1371
+ /// `file.txt/` to a directory beneath a read-write Directory Mount taking
1372
+ /// `policy`, and whether the link stays; `name` keeps its host directory
1373
+ /// its own.
1374
+ #[cfg(unix)]
1375
+ fn change_link_named_as_directory(name: &str, policy: Option<Arc<dyn Policy>>) -> ([i32; 2], bool) {
1376
+ let (host, mut guest) = linked_under(name, directory_with_key, Access::ReadWrite, policy);
1377
+ let answers = [
1378
+ guest.call("unlink_path", FILE_AS_DIRECTORY),
1379
+ guest.call_pair("rename_paths", FILE_AS_DIRECTORY, MOVED),
1380
+ ];
1381
+ let kept = host.join("file.txt").symlink_metadata().is_ok();
1382
+ let _ = std::fs::remove_dir_all(&host);
1383
+ (answers, kept)
1384
+ }
1385
+
1386
+ // Linux's `unlink` and `rename` both act on the link itself, which is no
1387
+ // directory, so a Policy, refusing only a link a call follows, changes
1388
+ // neither answer.
1389
+ // @behavior RT-123
1390
+ #[cfg(unix)]
1391
+ #[test]
1392
+ fn a_link_named_with_a_trailing_slash_is_neither_removed_nor_moved() {
1393
+ assert_eq!(
1394
+ [
1395
+ change_link_named_as_directory("link-slash", None),
1396
+ change_link_named_as_directory("policy-link-slash", Some(Arc::new(AllowsAll))),
1397
+ ],
1398
+ [([ERRNO_NOTDIR, ERRNO_NOTDIR], true); 2],
1399
+ "path_unlink_file and path_rename of a host link to a directory named with a trailing \
1400
+ slash, beneath a read-write Directory Mount with or without a Policy through preview1, \
1401
+ must answer ENOTDIR as Linux does and leave the link"
1402
+ );
1403
+ }
1404
+
1405
+ // The link points at nothing, so only an unlink that follows no link
1406
+ // finds it there to answer ENOTDIR rather than ENOENT.
1407
+ // @behavior RT-134
1408
+ #[cfg(unix)]
1409
+ #[test]
1410
+ fn an_unlink_with_a_trailing_slash_answers_for_the_name_itself() {
1411
+ let host = std::env::temp_dir().join(format!(
1412
+ "kobako-granted-unlink-slash-{}",
1413
+ std::process::id()
1414
+ ));
1415
+ let _ = std::fs::remove_dir_all(&host);
1416
+ std::fs::create_dir_all(host.join("dir")).expect("the host directory must be created");
1417
+ std::fs::write(host.join("file.txt"), "granted").expect("the host file must be written");
1418
+ std::os::unix::fs::symlink("nowhere", host.join("gone")).expect("the link must be made");
1419
+ let directory = Directory::new(&host).expect("the host directory must open");
1420
+ let mut guest = Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, None)]);
1421
+ let answers = [
1422
+ DIR_AS_DIRECTORY,
1423
+ FILE_AS_DIRECTORY,
1424
+ GONE_AS_DIRECTORY,
1425
+ NEW_AS_DIRECTORY,
1426
+ ]
1427
+ .map(|path| guest.call("unlink_path", path));
1428
+ let kept = ["dir", "file.txt", "gone"].map(|name| host.join(name).symlink_metadata().is_ok());
1429
+ let _ = std::fs::remove_dir_all(&host);
1430
+ assert_eq!(
1431
+ (answers, kept),
1432
+ (
1433
+ [ERRNO_ISDIR, ERRNO_NOTDIR, ERRNO_NOTDIR, ERRNO_NOENT],
1434
+ [true; 3]
1435
+ ),
1436
+ "path_unlink_file of a directory, a file, a link to nothing, and a missing name, each \
1437
+ named with a trailing slash beneath a read-write Directory Mount through preview1, must \
1438
+ answer EISDIR, ENOTDIR, ENOTDIR, and ENOENT as Linux does and remove nothing"
1439
+ );
1440
+ }
1441
+
1442
+ // @behavior RT-099
1443
+ #[test]
1444
+ fn an_allocation_for_a_file_beneath_a_mount_is_unsupported() {
1445
+ let memory: Arc<Memory> = Arc::new([("file.txt", "granted")].into_iter().collect());
1446
+ let answer =
1447
+ Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]).call("allocate_file", FILE);
1448
+ assert_eq!(
1449
+ (answer, contents(memory.as_ref(), "file.txt")),
1450
+ (ERRNO_NOTSUP, Some(b"granted".to_vec())),
1451
+ "fd_allocate of a file beneath a read-write Mount through preview1 must answer ENOTSUP \
1452
+ as wasmtime-wasi does, leaving the file as it was"
1453
+ );
1454
+ }
1455
+
1456
+ // The probe's opens follow no link, so only the trailing `/` can make one
1457
+ // reach the directory the link points at, as Linux's `open` does.
1458
+ // @behavior RT-100
1459
+ #[cfg(unix)]
1460
+ #[test]
1461
+ fn an_open_following_no_link_refuses_a_link_to_a_directory() {
1462
+ let (host, mut guest) = linked("open-nofollow", |target| {
1463
+ std::fs::create_dir(target).expect("the host directory must be created")
1464
+ });
1465
+ let answers = [
1466
+ guest.call_flagged(FILE, 0, 0),
1467
+ guest.call_flagged(FILE_AS_DIRECTORY, 0, 0),
1468
+ ];
1469
+ let _ = std::fs::remove_dir_all(&host);
1470
+ assert_eq!(
1471
+ answers,
1472
+ [ERRNO_LOOP, 0],
1473
+ "path_open following no link of a host link to a directory beneath a Directory Mount \
1474
+ through preview1 must answer ELOOP, and with a trailing slash open the directory"
1475
+ );
1476
+ }
1477
+
1478
+ /// A host directory linked in as `file.txt`, holding `key`.
1479
+ #[cfg(unix)]
1480
+ fn directory_with_key(target: &std::path::Path) {
1481
+ std::fs::create_dir(target).expect("the host directory must be created");
1482
+ std::fs::write(target.join("key"), "secret").expect("the host file must be written");
1483
+ }
1484
+
1485
+ // The Policy allows everything, so only the link on the way can refuse
1486
+ // these; the same calls beneath a Mount without a Policy go through.
1487
+ // @behavior RT-101
1488
+ #[cfg(unix)]
1489
+ #[test]
1490
+ fn a_path_through_a_link_beneath_a_mount_with_a_policy_answers_eloop() {
1491
+ let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
1492
+ let (host, mut guest) = linked_under(
1493
+ "policy-link",
1494
+ directory_with_key,
1495
+ Access::ReadWrite,
1496
+ Some(policy),
1497
+ );
1498
+ let refused = [
1499
+ guest.call("read_file", THROUGH_FILE),
1500
+ guest.call("unlink_path", THROUGH_FILE),
1501
+ guest.call("open_following", FILE),
1502
+ ];
1503
+ let kept = host.join("target/key").exists();
1504
+ let _ = std::fs::remove_dir_all(&host);
1505
+ let (host, mut guest) = linked_under("plain-link", directory_with_key, Access::Read, None);
1506
+ let followed = [
1507
+ guest.call("read_file", THROUGH_FILE),
1508
+ guest.call("open_following", FILE),
1509
+ ];
1510
+ let _ = std::fs::remove_dir_all(&host);
1511
+ assert_eq!(
1512
+ (refused, kept, followed),
1513
+ ([ERRNO_LOOP; 3], true, [0, 0]),
1514
+ "path_open and path_unlink_file through a host link, or path_open following one at the \
1515
+ end, beneath a Mount with a Policy through preview1 must answer ELOOP and change \
1516
+ nothing, while a Mount without a Policy follows the link"
1517
+ );
1518
+ }
1519
+
1520
+ // cap-std refuses to follow a link out of the host directory, and the
1521
+ // host refuses a loop, so each open is refused whichever answers first;
1522
+ // only ELOOP says the link is why. An unlink acts on the link itself,
1523
+ // which no Policy-free walk past it is needed for.
1524
+ // @behavior RT-125
1525
+ #[cfg(unix)]
1526
+ #[test]
1527
+ fn a_link_out_of_a_directory_or_into_a_loop_answers_eloop_beneath_a_policy() {
1528
+ let outside = std::env::temp_dir().join(format!("kobako-outside-{}", std::process::id()));
1529
+ directory_with_key(&outside);
1530
+ let targets = [
1531
+ outside.to_string_lossy().into_owned(),
1532
+ format!("../{}", outside.file_name().unwrap().to_string_lossy()),
1533
+ "file.txt".to_string(),
1534
+ ];
1535
+ let answers: Vec<_> = targets
1536
+ .iter()
1537
+ .enumerate()
1538
+ .map(|(index, target)| {
1539
+ let host = std::env::temp_dir().join(format!(
1540
+ "kobako-granted-escaping-{index}-{}",
1541
+ std::process::id()
1542
+ ));
1543
+ let _ = std::fs::remove_dir_all(&host);
1544
+ std::fs::create_dir_all(&host).expect("the host directory must be created");
1545
+ std::os::unix::fs::symlink(target, host.join("file.txt"))
1546
+ .expect("the link must be made");
1547
+ let directory = Directory::new(&host).expect("the host directory must open");
1548
+ let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
1549
+ let mut guest =
1550
+ Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, Some(policy))]);
1551
+ let answers = [
1552
+ guest.call("read_file", THROUGH_FILE),
1553
+ guest.call("unlink_path", FILE),
1554
+ ];
1555
+ let removed = host.join("file.txt").symlink_metadata().is_err();
1556
+ let _ = std::fs::remove_dir_all(&host);
1557
+ (answers, removed)
1558
+ })
1559
+ .collect();
1560
+ let kept = outside.join("key").exists();
1561
+ let _ = std::fs::remove_dir_all(&outside);
1562
+ assert_eq!(
1563
+ (answers, kept),
1564
+ (vec![([ERRNO_LOOP, 0], true); 3], true),
1565
+ "path_open through a host link out of a Directory, by an absolute or a relative target, \
1566
+ or into a loop, beneath a Mount with a Policy through preview1 must answer ELOOP, and \
1567
+ path_unlink_file of each link must remove it"
1568
+ );
1569
+ }
1570
+
1571
+ // The Filesystem fails only the lookup of `dir`, which the open needs
1572
+ // no answer from but the walk for links on its way does.
1573
+ // @behavior RT-126
1574
+ #[test]
1575
+ fn a_link_check_that_fails_fails_the_call_beneath_a_policy() {
1576
+ let failing = Arc::new(FailsLookingUpDir(
1577
+ [("dir/file.txt", "granted")].into_iter().collect(),
1578
+ ));
1579
+ let policy: Arc<dyn Policy> = Arc::new(AllowsAll);
1580
+ let mut guest = Guest::new(&[grant(failing, Access::Read, Some(policy))]);
1581
+ assert_eq!(
1582
+ guest.call("read_file", DIR_FILE),
1583
+ ERRNO_IO,
1584
+ "path_open of a file beneath a directory the Filesystem fails to look up, beneath a \
1585
+ Mount with a Policy through preview1, must answer that failure"
1586
+ );
1587
+ }
1588
+
1589
+ /// A Memory failing every lookup of `dir` itself, as a host directory
1590
+ /// that cannot be read could.
1591
+ struct FailsLookingUpDir(Memory);
1592
+
1593
+ impl Filesystem for FailsLookingUpDir {
1594
+ matches_names_exactly!();
1595
+ never_repeats_identities!();
1596
+ is_shared!();
1597
+
1598
+ fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
1599
+ self.0.open(path, options)
1600
+ }
1601
+
1602
+ fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
1603
+ self.0.metadata(path)
1604
+ }
1605
+
1606
+ fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
1607
+ match path {
1608
+ "dir" => Err(FilesystemError::Other("unreadable".into())),
1609
+ path => self.0.symlink_metadata(path),
1610
+ }
1611
+ }
1612
+
1613
+ fn canonicalize_beneath(&self, base: &str, path: &str) -> Result<String, FilesystemError> {
1614
+ self.0.canonicalize_beneath(base, path)
1615
+ }
1616
+
1617
+ fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
1618
+ None
1619
+ }
1620
+
1621
+ fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
1622
+ self.0.read_dir(path)
1623
+ }
1624
+
1625
+ fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
1626
+ self.0.create_dir(path)
1627
+ }
1628
+
1629
+ fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
1630
+ self.0.remove_dir(path)
1631
+ }
1632
+
1633
+ fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
1634
+ self.0.remove_file(path)
1635
+ }
1636
+
1637
+ fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
1638
+ self.0.rename(from, to, rename)
1639
+ }
1640
+ }
1641
+
1642
+ // The Mount has no Policy and follows links, so only the step out of
1643
+ // the link can refuse these.
1644
+ // @behavior RT-119
1645
+ #[cfg(unix)]
1646
+ #[test]
1647
+ fn a_step_out_of_a_link_answers_eloop() {
1648
+ assert_eq!(
1649
+ stat_link(
1650
+ "dot-link",
1651
+ directory_with_key,
1652
+ &[(LINK_DOT, 1), (OUT_OF_LINK, 0)]
1653
+ )
1654
+ .into_iter()
1655
+ .map(|(errno, _)| errno)
1656
+ .collect::<Vec<_>>(),
1657
+ [ERRNO_LOOP; 2],
1658
+ "path_filestat_get of `file.txt/.` and `file.txt/../file.txt`, where `file.txt` is a \
1659
+ host link to a directory, beneath a Directory Mount through preview1 must answer ELOOP"
1660
+ );
1661
+ }
1662
+
1663
+ fn file_and_directory() -> Arc<Memory> {
1664
+ Arc::new(
1665
+ [("file.txt", "granted"), ("dir/inner", "x")]
1666
+ .into_iter()
1667
+ .collect(),
1668
+ )
1669
+ }
1670
+
1671
+ // A `.` after a name resolves through it, so Linux and macOS both refuse
1672
+ // `missing/.` at the missing name rather than make it.
1673
+ // @behavior RT-109
1674
+ #[test]
1675
+ fn a_path_ending_in_a_dot_makes_nothing_where_no_directory_stands() {
1676
+ let memory = file_and_directory();
1677
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
1678
+ let answers = [
1679
+ guest.call("make_dir", MISSING_DOT),
1680
+ guest.call_flagged(MISSING_DOT, OFLAGS_CREAT, 0),
1681
+ ];
1682
+ assert_eq!(
1683
+ (answers, memory.metadata("missing").is_err()),
1684
+ ([ERRNO_NOENT, ERRNO_NOENT], true),
1685
+ "path_create_directory and path_open with O_CREAT of missing/. beneath a Mount through \
1686
+ preview1 must answer ENOENT, as Linux does, and make nothing named missing"
1687
+ );
1688
+ }
1689
+
1690
+ // Resolving `dir/.` reaches `dir` itself, so only refusing the path as
1691
+ // written keeps either call from taking `dir`.
1692
+ // @behavior RT-102
1693
+ #[test]
1694
+ fn a_path_ending_in_a_dot_is_neither_removed_nor_moved() {
1695
+ let memory = file_and_directory();
1696
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
1697
+ let answers = [
1698
+ guest.call("remove_path", DIR_DOT),
1699
+ guest.call("remove_path", DIR_DOT_DOT),
1700
+ guest.call_pair("rename_paths", DIR_DOT, MOVED),
1701
+ guest.call_pair("rename_paths", DIR_DOT_DOT, MOVED),
1702
+ guest.call("remove_path", OUT_OF_FILE),
1703
+ guest.call_pair("rename_paths", OUT_OF_FILE, MOVED),
1704
+ ];
1705
+ assert_eq!(
1706
+ (answers, memory.metadata("dir/inner").is_ok()),
1707
+ (
1708
+ [
1709
+ ERRNO_INVAL,
1710
+ ERRNO_INVAL,
1711
+ ERRNO_BUSY,
1712
+ ERRNO_BUSY,
1713
+ ERRNO_NOTDIR,
1714
+ ERRNO_NOTDIR
1715
+ ],
1716
+ true
1717
+ ),
1718
+ "path_remove_directory of a path ending in . or .., and path_rename of one, beneath a \
1719
+ Mount through preview1 must answer EINVAL and EBUSY as wasmtime-wasi does, or ENOTDIR \
1720
+ where a file stands before the dot, and leave the directory in place"
1721
+ );
1722
+ }
1723
+
1724
+ // The Mount only reads, so only judging a create of what already stands
1725
+ // as no change lets the open through.
1726
+ // @behavior RT-103
1727
+ #[test]
1728
+ fn a_create_of_what_already_stands_changes_nothing() {
1729
+ let mut guest = Guest::new(&[grant(file_and_directory(), Access::Read, None)]);
1730
+ let answers = [
1731
+ guest.call_flagged(FILE, OFLAGS_CREAT, 0),
1732
+ guest.call_flagged(DIR, OFLAGS_CREAT, 0),
1733
+ ];
1734
+ assert_eq!(
1735
+ answers,
1736
+ [0, ERRNO_ISDIR],
1737
+ "path_open with O_CREAT and no write of an existing file beneath a read-only Mount \
1738
+ through preview1 must open it, and of an existing directory answer EISDIR, as Linux does"
1739
+ );
1740
+ }
1741
+
1742
+ // The host file only reads, so a host open asking to create, and so to
1743
+ // write, would be refused where the guest asked only to read.
1744
+ // @behavior RT-103
1745
+ #[cfg(unix)]
1746
+ #[test]
1747
+ fn a_create_of_a_host_file_that_only_reads_opens_it() {
1748
+ use std::os::unix::fs::PermissionsExt;
1749
+ let host = std::env::temp_dir().join(format!("kobako-granted-creat-{}", std::process::id()));
1750
+ let _ = std::fs::remove_dir_all(&host);
1751
+ std::fs::create_dir_all(&host).expect("the host directory must be created");
1752
+ std::fs::write(host.join("file.txt"), "granted").expect("the host file must be written");
1753
+ std::fs::set_permissions(
1754
+ host.join("file.txt"),
1755
+ std::fs::Permissions::from_mode(0o444),
1756
+ )
1757
+ .expect("the host file must become read-only");
1758
+ let directory = Directory::new(&host).expect("the host directory must open");
1759
+ let answer = Guest::new(&[grant(Arc::new(directory), Access::ReadWrite, None)]).call_flagged(
1760
+ FILE,
1761
+ OFLAGS_CREAT,
1762
+ 0,
1763
+ );
1764
+ let _ = std::fs::remove_dir_all(&host);
1765
+ assert_eq!(
1766
+ answer, 0,
1767
+ "path_open with O_CREAT and no write of a read-only host file beneath a Directory Mount \
1768
+ through preview1 must open it"
1769
+ );
1770
+ }
1771
+
1772
+ // The second seek would land past what a signed offset holds, so only
1773
+ // the cap keeps the position from reaching it.
1774
+ // @behavior RT-104
1775
+ #[test]
1776
+ fn a_seek_past_the_largest_offset_overflows() {
1777
+ let answer = Guest::new(&[grant(file_and_directory(), Access::Read, None)])
1778
+ .call_bare("seek_past_the_largest_offset");
1779
+ assert_eq!(
1780
+ answer, ERRNO_OVERFLOW,
1781
+ "fd_seek past the largest signed 64-bit offset of a file beneath a Mount through \
1782
+ preview1 must answer EOVERFLOW, as Linux does"
1783
+ );
1784
+ }
1785
+
1786
+ // A Memory clamps a read past its end and tries to grow for a write or a
1787
+ // size, so only the check before the Filesystem answers each as Linux does.
1788
+ // @behavior RT-116
1789
+ #[test]
1790
+ fn an_offset_past_the_largest_signed_offset_is_invalid() {
1791
+ let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
1792
+ assert_eq!(
1793
+ (
1794
+ guest.call_at("read_at", 1 << 63),
1795
+ guest.call_at("write_at", 1 << 63),
1796
+ guest.call_at("size_at", 1 << 63)
1797
+ ),
1798
+ (ERRNO_INVAL, ERRNO_INVAL, ERRNO_INVAL),
1799
+ "fd_pread, fd_pwrite, and fd_filestat_set_size past the largest signed 64-bit offset of a \
1800
+ Memory file beneath a Mount through preview1 must answer EINVAL, as Linux does"
1801
+ );
1802
+ }
1803
+
1804
+ // The file is far shorter than either offset, so a read reaching the
1805
+ // Filesystem answers its end; only the refusal before it answers EINVAL.
1806
+ // @behavior RT-136
1807
+ #[test]
1808
+ fn a_read_whose_end_passes_the_largest_signed_offset_is_invalid() {
1809
+ let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
1810
+ assert_eq!(
1811
+ (
1812
+ guest.call_at("read_at", i64::MAX as u64 - 10),
1813
+ guest.call_bare("read_near_the_largest_offset"),
1814
+ guest.call_at("read_at", i64::MAX as u64 - 64),
1815
+ ),
1816
+ (ERRNO_INVAL, ERRNO_INVAL, 0),
1817
+ "fd_pread and fd_read of 64 bytes from ten before the largest signed 64-bit offset of a \
1818
+ file beneath a Mount through preview1 must answer EINVAL as Linux does, while a read \
1819
+ ending at it answers the file's end"
1820
+ );
1821
+ }
1822
+
1823
+ // Each file is opened only to read, so only its Mount's access can tell the
1824
+ // two answers apart.
1825
+ // @behavior RT-135
1826
+ #[test]
1827
+ fn a_wait_to_write_a_file_answers_by_what_its_mount_permits() {
1828
+ let wait = |access| {
1829
+ Guest::new(&[grant(file_and_directory(), access, None)])
1830
+ .call_at("poll_write_with", RIGHTS_FD_READ)
1831
+ };
1832
+ assert_eq!(
1833
+ (wait(Access::ReadWrite), wait(Access::Read)),
1834
+ (0, ERRNO_PERM),
1835
+ "poll_oneoff waiting to write a file opened only to read through preview1 must be ready \
1836
+ beneath a read-write Mount and answer EPERM beneath a read-only one, as wasmtime-wasi \
1837
+ answers by its preopen"
1838
+ );
1839
+ }
1840
+
1841
+ // The same file opened with the same rights, so only its Mount's access
1842
+ // tells the two refusals apart.
1843
+ // @behavior RT-137
1844
+ #[test]
1845
+ fn sizing_a_file_opened_only_to_read_answers_by_its_mount_then_as_linux_does() {
1846
+ let size = |access, rights| {
1847
+ Guest::new(&[grant(file_and_directory(), access, None)]).call_at("size_with", rights)
1848
+ };
1849
+ assert_eq!(
1850
+ (
1851
+ size(Access::Read, RIGHTS_FD_READ),
1852
+ size(Access::ReadWrite, RIGHTS_FD_READ),
1853
+ size(Access::ReadWrite, RIGHTS_FD_WRITE),
1854
+ ),
1855
+ (ERRNO_PERM, ERRNO_INVAL, 0),
1856
+ "fd_filestat_set_size on a file opened only to read through preview1 must answer EPERM \
1857
+ beneath a read-only Mount, as wasmtime-wasi does, and EINVAL beneath a read-write one, \
1858
+ as Linux's ftruncate does, while a file opened to write is sized"
1859
+ );
1860
+ }
1861
+
1862
+ /// A Policy allowing every change while keeping each one it is asked.
1863
+ #[derive(Default)]
1864
+ struct RecordsAsks(std::sync::Mutex<Vec<(String, Access)>>);
1865
+
1866
+ impl Policy for RecordsAsks {
1867
+ fn allows(&self, path: &str, access: Access) -> bool {
1868
+ self.0.lock().unwrap().push((path.to_owned(), access));
1869
+ true
1870
+ }
1871
+ }
1872
+
1873
+ // The Mount is made read-only, as a Ruby mount with a block is by default,
1874
+ // so only the Policy standing in its access's place lets either through.
1875
+ // Each Policy is asked only by the open before the call, so a wait or a size
1876
+ // asking it as well adds an ask the literal lists below do not hold.
1877
+ // @behavior RT-139
1878
+ #[test]
1879
+ fn a_mount_whose_policy_decides_may_be_written() {
1880
+ let call = |name, rights| {
1881
+ let policy = Arc::new(RecordsAsks::default());
1882
+ let errno = Guest::new(&[grant(
1883
+ file_and_directory(),
1884
+ Access::Read,
1885
+ Some(policy.clone() as Arc<dyn Policy>),
1886
+ )])
1887
+ .call_at(name, rights);
1888
+ let asked = policy.0.lock().unwrap().clone();
1889
+ (errno, asked)
1890
+ };
1891
+ assert_eq!(
1892
+ (
1893
+ call("poll_write_with", RIGHTS_FD_READ),
1894
+ call("size_with", RIGHTS_FD_WRITE),
1895
+ ),
1896
+ (
1897
+ (0, vec![("/data/file.txt".to_owned(), Access::Read)]),
1898
+ (0, vec![("/data/file.txt".to_owned(), Access::ReadWrite)]),
1899
+ ),
1900
+ "through preview1 beneath a read-only Mount whose Policy allows every change, \
1901
+ poll_oneoff waiting to write a file opened only to read must be ready, and \
1902
+ fd_filestat_set_size on a file opened to write must size it, the Policy asked \
1903
+ only by each open"
1904
+ );
1905
+ }
1906
+
1907
+ // Each open asks without the right to write, so only its CREAT, where
1908
+ // nothing stands yet, makes it a change.
1909
+ // @behavior RT-146
1910
+ #[test]
1911
+ fn a_policy_is_asked_once_for_an_open_that_may_create_as_what_it_does() {
1912
+ let open = |path| {
1913
+ let policy = Arc::new(RecordsAsks::default());
1914
+ let errno = Guest::new(&[grant(
1915
+ file_and_directory(),
1916
+ Access::Read,
1917
+ Some(policy.clone() as Arc<dyn Policy>),
1918
+ )])
1919
+ .call_flagged(path, OFLAGS_CREAT, 0);
1920
+ let asked = policy.0.lock().unwrap().clone();
1921
+ (errno, asked)
1922
+ };
1923
+ assert_eq!(
1924
+ (open(NEW), open(FILE)),
1925
+ (
1926
+ (0, vec![("/data/new.txt".to_owned(), Access::ReadWrite)]),
1927
+ (0, vec![("/data/file.txt".to_owned(), Access::Read)]),
1928
+ ),
1929
+ "path_open with O_CREAT and no write of a missing path and of an existing file beneath \
1930
+ a Mount whose Policy allows every change through preview1 must ask the Policy once for \
1931
+ each, as a change where it creates and as a read where the file stands"
1932
+ );
1933
+ }
1934
+
1935
+ // The open asks to write a path nothing stands at, so only the lookup
1936
+ // before the Mount judges it answers ENOENT, as Linux does on a read-only
1937
+ // mount, rather than the EROFS the same open of a file answers.
1938
+ // @behavior RT-147
1939
+ #[test]
1940
+ fn an_open_of_a_missing_path_that_makes_nothing_answers_enoent() {
1941
+ let read_only =
1942
+ Guest::new(&[grant(file_and_directory(), Access::Read, None)]).call("append_file", NEW);
1943
+ let policy = Arc::new(RecordsAsks::default());
1944
+ let policed = Guest::new(&[grant(
1945
+ file_and_directory(),
1946
+ Access::Read,
1947
+ Some(policy.clone() as Arc<dyn Policy>),
1948
+ )])
1949
+ .call("append_file", NEW);
1950
+ let asked = policy.0.lock().unwrap().clone();
1951
+ assert_eq!(
1952
+ (read_only, policed, asked),
1953
+ (ERRNO_NOENT, ERRNO_NOENT, Vec::new()),
1954
+ "path_open to write a missing path without O_CREAT beneath a read-only Mount and beneath \
1955
+ a Mount with a Policy through preview1 must answer ENOENT and ask the Policy nothing"
1956
+ );
1957
+ }
1958
+
1959
+ // The Mount is read-only, so an open judged before what stands at its
1960
+ // path is looked up would answer EROFS for both.
1961
+ // @behavior RT-148
1962
+ #[test]
1963
+ fn what_stands_at_a_path_answers_before_a_read_only_mount_refuses() {
1964
+ let mut guest = Guest::new(&[grant(file_and_directory(), Access::Read, None)]);
1965
+ assert_eq!(
1966
+ (
1967
+ guest.call_flagged(FILE, OFLAGS_CREAT | OFLAGS_EXCL, 0),
1968
+ guest.call("append_file", DIR),
1969
+ ),
1970
+ (ERRNO_EXIST, ERRNO_ISDIR),
1971
+ "path_open with O_CREAT | O_EXCL of a file, and to write a directory, beneath a read-only \
1972
+ Mount through preview1 must answer EEXIST and EISDIR, as Linux does"
1973
+ );
1974
+ }
1975
+
1976
+ const NEW: (i32, i32) = (840, 7);
1977
+ const RIGHTS_FD_READ: u64 = 1 << 1;
1978
+ const ERRNO_PERM: i32 = 63;
1979
+
1980
+ // A Memory would refuse the bytes for want of space, so only the refusal
1981
+ // before the Filesystem is asked answers EFBIG.
1982
+ // @behavior RT-133
1983
+ #[test]
1984
+ fn a_write_at_the_largest_signed_offset_is_too_large() {
1985
+ let memory = file_and_directory();
1986
+ let answer = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)])
1987
+ .call_bare("write_at_the_largest_offset");
1988
+ assert_eq!(
1989
+ (answer, contents(memory.as_ref(), "file.txt")),
1990
+ (ERRNO_FBIG, Some(b"granted".to_vec())),
1991
+ "fd_write at the largest signed 64-bit offset of a Memory file beneath a Mount through \
1992
+ preview1 must answer EFBIG, as Linux does, and leave the file as it was"
1993
+ );
1994
+ }
1995
+
1996
+ // @behavior RT-130
1997
+ #[test]
1998
+ fn an_advice_past_the_largest_signed_offset_is_invalid() {
1999
+ let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
2000
+ assert_eq!(
2001
+ (
2002
+ guest.call_at("advise_from", 1 << 63),
2003
+ guest.call_at("advise_for", 1 << 63),
2004
+ guest.call_at("advise_for", 0)
2005
+ ),
2006
+ (ERRNO_INVAL, ERRNO_INVAL, 0),
2007
+ "fd_advise from or for past the largest signed 64-bit offset of a Memory file beneath a \
2008
+ Mount through preview1 must answer EINVAL, as an advice within it succeeds"
2009
+ );
2010
+ }
2011
+
2012
+ // The open asks to read only, so nothing else the open could refuse
2013
+ // stands in for the refusal of not blocking.
2014
+ // @behavior RT-129
2015
+ #[test]
2016
+ fn a_descriptor_that_would_not_block_is_unsupported() {
2017
+ let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
2018
+ assert_eq!(
2019
+ (
2020
+ guest.call_flagged(FILE, 0, FDFLAGS_NONBLOCK),
2021
+ guest.call_bare("set_nonblocking"),
2022
+ guest.call_flagged(FILE, 0, 0)
2023
+ ),
2024
+ (ERRNO_NOTSUP, ERRNO_NOTSUP, 0),
2025
+ "path_open asking not to block and fd_fdstat_set_flags setting an open file not to \
2026
+ block, beneath a Mount through preview1, must answer ENOTSUP"
2027
+ );
2028
+ }
2029
+
2030
+ // @behavior RT-128
2031
+ #[test]
2032
+ fn a_directory_a_guest_opens_carries_no_right_to_write() {
2033
+ let mut guest = Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)]);
2034
+ let errno = guest.call_bare("dir_fdstat");
2035
+ let memory = guest
2036
+ .instance
2037
+ .get_memory(&mut guest.store, "memory")
2038
+ .expect("probe exports its memory");
2039
+ let rights = u64::from_le_bytes(
2040
+ memory.data(&guest.store)[392..400]
2041
+ .try_into()
2042
+ .expect("the rights are eight bytes"),
2043
+ );
2044
+ assert_eq!(
2045
+ (errno, rights & RIGHTS_FD_WRITE, rights & RIGHTS_FD_READDIR),
2046
+ (0, 0, RIGHTS_FD_READDIR),
2047
+ "fd_fdstat_get of a directory opened through path_open beneath a read-write Mount \
2048
+ through preview1 must report its rights without fd_write, as wasmtime-wasi does"
2049
+ );
2050
+ }
2051
+
2052
+ // @behavior RT-105
2053
+ #[test]
2054
+ fn a_file_moved_beneath_itself_is_no_directory() {
2055
+ let memory = file_and_directory();
2056
+ let answer = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]).call_pair(
2057
+ "rename_paths",
2058
+ FILE,
2059
+ BENEATH_FILE,
2060
+ );
2061
+ assert_eq!(
2062
+ (answer, contents(memory.as_ref(), "file.txt")),
2063
+ (ERRNO_NOTDIR, Some(b"granted".to_vec())),
2064
+ "path_rename of a file to a path beneath it, beneath a Mount through preview1, must \
2065
+ answer ENOTDIR as Linux does and leave the file in place"
2066
+ );
2067
+ }
2068
+
2069
+ // The link points at nothing, so only the link itself, not a file it
2070
+ // would follow to, can be what each open finds there.
2071
+ // @behavior RT-106
2072
+ #[cfg(unix)]
2073
+ #[test]
2074
+ fn an_open_following_no_link_finds_a_dangling_link_as_itself() {
2075
+ let (host, mut guest) = linked_under("dangling", |_| {}, Access::ReadWrite, None);
2076
+ let answers = [
2077
+ guest.call_flagged(FILE, 0, 0),
2078
+ guest.call_flagged(FILE, OFLAGS_CREAT | OFLAGS_EXCL, 0),
2079
+ ];
2080
+ let made = host.join("target").exists();
2081
+ let _ = std::fs::remove_dir_all(&host);
2082
+ assert_eq!(
2083
+ (answers, made),
2084
+ ([ERRNO_LOOP, ERRNO_EXIST], false),
2085
+ "path_open following no link of a dangling host link beneath a Directory Mount through \
2086
+ preview1 must answer ELOOP, and with O_CREAT and O_EXCL EEXIST, as Linux does, making \
2087
+ nothing where it points"
2088
+ );
2089
+ }
2090
+
2091
+ /// What each root-changing export answers, and how many changes the
2092
+ /// Filesystem was asked for along the way.
2093
+ fn change_root(exports: &[&str]) -> (Vec<i32>, usize) {
2094
+ let counted = ChangeCounter::new();
2095
+ let mut guest = Guest::new(&[grant(counted.clone(), Access::ReadWrite, None)]);
2096
+ let answers = exports.iter().map(|name| guest.call_bare(name)).collect();
2097
+ (answers, counted.changes.load(Ordering::Relaxed))
2098
+ }
2099
+
2100
+ // @behavior RT-083
2101
+ #[test]
2102
+ fn removing_a_mounts_root_is_refused_as_invalid() {
2103
+ assert_eq!(
2104
+ change_root(&["remove_root"]),
2105
+ (vec![ERRNO_INVAL], 0),
2106
+ "path_remove_directory of `.` beneath a Mount through preview1 must answer EINVAL \
2107
+ without asking the Filesystem"
2108
+ );
2109
+ }
2110
+
2111
+ // @behavior RT-084
2112
+ #[test]
2113
+ fn renaming_a_mounts_root_is_refused_as_busy() {
2114
+ assert_eq!(
2115
+ change_root(&["rename_root", "rename_onto_root"]),
2116
+ (vec![ERRNO_BUSY, ERRNO_BUSY], 0),
2117
+ "path_rename from or onto `.` beneath a Mount through preview1 must answer EBUSY \
2118
+ without asking the Filesystem"
2119
+ );
2120
+ }
2121
+
2122
+ // @behavior RT-085
2123
+ #[test]
2124
+ fn unlinking_a_mounts_root_answers_that_it_is_a_directory() {
2125
+ assert_eq!(
2126
+ change_root(&["unlink_root"]),
2127
+ (vec![ERRNO_ISDIR], 0),
2128
+ "path_unlink_file of `.` beneath a Mount through preview1 must answer EISDIR \
2129
+ without asking the Filesystem"
2130
+ );
2131
+ }
2132
+
2133
+ /// What reading `path` answers beneath a read-only Mount holding
2134
+ /// `file.txt`.
2135
+ fn read_beneath(path: (i32, i32)) -> i32 {
2136
+ let memory: Memory = [("file.txt", "granted")].into_iter().collect();
2137
+ Guest::new(&[grant(Arc::new(memory), Access::Read, None)]).call("read_file", path)
2138
+ }
2139
+
2140
+ // @behavior RT-086
2141
+ #[test]
2142
+ fn an_empty_path_names_nothing_beneath_a_mount() {
2143
+ assert_eq!(
2144
+ read_beneath(EMPTY),
2145
+ ERRNO_NOENT,
2146
+ "path_open of the empty path beneath a Mount through preview1 must answer ENOENT"
2147
+ );
2148
+ }
2149
+
2150
+ // @behavior RT-087
2151
+ #[test]
2152
+ fn a_dot_dot_after_a_missing_name_names_nothing() {
2153
+ assert_eq!(
2154
+ read_beneath(THROUGH_MISSING),
2155
+ ERRNO_NOENT,
2156
+ "path_open of `missing/../file.txt` beneath a Mount through preview1 must answer ENOENT"
2157
+ );
2158
+ }
2159
+
2160
+ // The Memory would hold `file.txt` and any other name, so only the NUL
2161
+ // refuses the open.
2162
+ // @behavior RT-122
2163
+ #[test]
2164
+ fn a_path_holding_a_nul_is_invalid() {
2165
+ assert_eq!(
2166
+ read_beneath(HOLDING_NUL),
2167
+ ERRNO_INVAL,
2168
+ "path_open of `file.txt\\0` beneath a Mount through preview1 must answer EINVAL"
2169
+ );
2170
+ }
2171
+
2172
+ // @behavior RT-088
2173
+ #[test]
2174
+ fn a_dot_dot_after_a_file_is_not_a_directory() {
2175
+ assert_eq!(
2176
+ read_beneath(OUT_OF_FILE),
2177
+ ERRNO_NOTDIR,
2178
+ "path_open of `file.txt/..` beneath a Mount through preview1 must answer ENOTDIR"
2179
+ );
2180
+ }
2181
+
2182
+ // @behavior RT-089
2183
+ #[test]
2184
+ fn a_path_ending_in_a_slash_names_only_a_directory() {
2185
+ assert_eq!(
2186
+ read_beneath(FILE_AS_DIRECTORY),
2187
+ ERRNO_NOTDIR,
2188
+ "path_open of `file.txt/` beneath a Mount through preview1 must answer ENOTDIR"
2189
+ );
2190
+ }
2191
+
2192
+ // The target is the source's own path, so only the slash can refuse it.
2193
+ // @behavior RT-093
2194
+ #[test]
2195
+ fn a_rename_of_a_file_onto_a_path_ending_in_a_slash_is_not_a_directory() {
2196
+ let memory: Arc<Memory> = Arc::new([("file.txt", "granted")].into_iter().collect());
2197
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
2198
+ assert_eq!(
2199
+ (
2200
+ guest.call_bare("rename_onto_itself_as_directory"),
2201
+ contents(memory.as_ref(), "file.txt")
2202
+ ),
2203
+ (ERRNO_NOTDIR, Some(b"granted".to_vec())),
2204
+ "path_rename of `file.txt` onto `file.txt/` beneath a Mount through preview1 must \
2205
+ answer ENOTDIR and leave the file"
2206
+ );
2207
+ }
2208
+
2209
+ // @behavior RT-090
2210
+ #[test]
2211
+ fn a_file_created_at_a_path_ending_in_a_slash_is_refused_as_a_directory() {
2212
+ let memory = Arc::new(Memory::new());
2213
+ let mut guest = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)]);
2214
+ assert_eq!(
2215
+ (
2216
+ guest.call("write_file", NEW_AS_DIRECTORY),
2217
+ memory.read_dir("").map(|entries| entries.len())
2218
+ ),
2219
+ (ERRNO_ISDIR, Ok(0)),
2220
+ "path_open creating `new.txt/` beneath a Mount through preview1 must answer EISDIR and \
2221
+ make no file"
2222
+ );
2223
+ }
2224
+
2225
+ const OFLAGS_CREAT: i32 = 1 << 0;
2226
+ const OFLAGS_DIRECTORY: i32 = 1 << 1;
2227
+ const OFLAGS_TRUNC: i32 = 1 << 3;
2228
+ const FDFLAGS_SYNC: i32 = 1 << 4;
2229
+ const FDFLAGS_NONBLOCK: i32 = 1 << 2;
2230
+ const RIGHTS_FD_WRITE: u64 = 1 << 6;
2231
+ const RIGHTS_FD_READDIR: u64 = 1 << 14;
2232
+
2233
+ /// What opening `path` with `oflags` and `fdflags` answers beneath a
2234
+ /// read-write Memory holding `file.txt`, and the paths it holds after.
2235
+ fn open_flagged(path: (i32, i32), oflags: i32, fdflags: i32) -> (i32, Vec<String>) {
2236
+ let memory: Arc<Memory> = Arc::new([("file.txt", "granted")].into_iter().collect());
2237
+ let errno = Guest::new(&[grant(memory.clone(), Access::ReadWrite, None)])
2238
+ .call_flagged(path, oflags, fdflags);
2239
+ let held = memory
2240
+ .read_dir("")
2241
+ .expect("the root must list")
2242
+ .into_iter()
2243
+ .map(|entry| entry.name)
2244
+ .collect();
2245
+ (errno, held)
2246
+ }
2247
+
2248
+ // @behavior RT-091
2249
+ #[test]
2250
+ fn an_open_asking_for_synchronized_writes_is_unsupported() {
2251
+ assert_eq!(
2252
+ open_flagged(FILE, 0, FDFLAGS_SYNC).0,
2253
+ ERRNO_NOTSUP,
2254
+ "path_open of `file.txt` with FDFLAGS_SYNC beneath a Mount through preview1 must answer \
2255
+ ENOTSUP"
2256
+ );
2257
+ }
2258
+
2259
+ // @behavior RT-092
2260
+ #[test]
2261
+ fn an_open_of_a_directory_that_creates_or_truncates_is_invalid() {
2262
+ let held = vec!["file.txt".to_string()];
2263
+ assert_eq!(
2264
+ (
2265
+ open_flagged(NEW_AS_DIRECTORY, OFLAGS_DIRECTORY | OFLAGS_CREAT, 0),
2266
+ open_flagged(FILE, OFLAGS_DIRECTORY | OFLAGS_TRUNC, 0)
2267
+ ),
2268
+ ((ERRNO_INVAL, held.clone()), (ERRNO_INVAL, held)),
2269
+ "path_open with OFLAGS_DIRECTORY and CREAT or TRUNC beneath a Mount through preview1 \
2270
+ must answer EINVAL and make nothing"
2271
+ );
2272
+ }
2273
+
2274
+ const UP: (i32, i32) = (1070, 2);
2275
+ const THROUGH_UPDIR: (i32, i32) = (1080, 7);
2276
+ const IN_LINKED: (i32, i32) = (1090, 4);
2277
+ const TWISTY: (i32, i32) = (1100, 6);
2278
+ const DANGLE: (i32, i32) = (1110, 6);
2279
+ const DIR_UP: (i32, i32) = (1120, 6);
2280
+ const NOWHERE: (i32, i32) = (1130, 7);
2281
+
2282
+ /// A host directory whose `dir` holds links staying within it (`in`,
2283
+ /// `twisty`), links leading out of it (`up`, `updir`), and links pointing
2284
+ /// at nothing out of it (`dangle`) and within it (`nowhere`).
2285
+ #[cfg(unix)]
2286
+ struct LinkedBeneath(std::path::PathBuf);
2287
+
2288
+ #[cfg(unix)]
2289
+ impl LinkedBeneath {
2290
+ fn new(name: &str) -> Self {
2291
+ let host =
2292
+ std::env::temp_dir().join(format!("kobako-beneath-{name}-{}", std::process::id()));
2293
+ let _ = std::fs::remove_dir_all(&host);
2294
+ std::fs::create_dir_all(host.join("dir/sub")).expect("the host directory must be created");
2295
+ std::fs::create_dir_all(host.join("other")).expect("the host directory must be created");
2296
+ for file in ["secret", "other/x", "dir/sub/y"] {
2297
+ std::fs::write(host.join(file), "x").expect("the host file must be written");
2298
+ }
2299
+ for (target, link) in [
2300
+ ("../secret", "dir/up"),
2301
+ ("../other", "dir/updir"),
2302
+ ("sub", "dir/in"),
2303
+ ("sub/../sub/y", "dir/twisty"),
2304
+ ("../escaped", "dir/dangle"),
2305
+ ("missing", "dir/nowhere"),
2306
+ ] {
2307
+ std::os::unix::fs::symlink(target, host.join(link)).expect("the link must be made");
2308
+ }
2309
+ Self(host)
2310
+ }
2311
+
2312
+ /// A guest over this host directory as a Directory Mount, or as an
2313
+ /// Overlay's writable Directory Layer above an empty Memory.
2314
+ fn guest(&self, overlay: bool) -> Guest {
2315
+ let directory = Arc::new(Directory::new(&self.0).expect("the host directory must open"));
2316
+ let filesystem: Arc<dyn Filesystem> = if overlay {
2317
+ Arc::new(
2318
+ Overlay::new()
2319
+ .layer(directory, Access::ReadWrite)
2320
+ .layer(Arc::new(Memory::new()), Access::Read),
2321
+ )
2322
+ } else {
2323
+ directory
2324
+ };
2325
+ Guest::new(&[grant(filesystem, Access::ReadWrite, None)])
2326
+ }
2327
+ }
2328
+
2329
+ #[cfg(unix)]
2330
+ impl Drop for LinkedBeneath {
2331
+ fn drop(&mut self) {
2332
+ let _ = std::fs::remove_dir_all(&self.0);
2333
+ }
2334
+ }
2335
+
2336
+ // Each link names a real file outside `dir` yet inside the Mount, so only
2337
+ // a link kept within the descriptor, not the Mount's root, is refused.
2338
+ // @behavior RT-140
2339
+ #[cfg(unix)]
2340
+ #[test]
2341
+ fn a_link_leading_out_of_an_opened_directory_is_not_permitted() {
2342
+ let host = LinkedBeneath::new("out");
2343
+ let answers = |overlay| {
2344
+ let mut guest = host.guest(overlay);
2345
+ [
2346
+ guest.call("open_following_beneath_dir", UP),
2347
+ guest.call("stat_following_beneath_dir", UP),
2348
+ guest.call("open_beneath_dir", THROUGH_UPDIR),
2349
+ guest.call("stat_following_beneath_dir", THROUGH_UPDIR),
2350
+ guest.call("create_following_beneath_dir", DANGLE),
2351
+ ]
2352
+ };
2353
+ assert_eq!(
2354
+ (
2355
+ answers(false),
2356
+ answers(true),
2357
+ host.0.join("escaped").exists()
2358
+ ),
2359
+ ([ERRNO_PERM; 5], [ERRNO_PERM; 5], false),
2360
+ "an open, a stat, or a create through preview1 of a path whose link leads out of a \
2361
+ directory opened beneath a Directory Mount, or an Overlay's Directory Layer, must answer \
2362
+ EPERM, as wasmtime-wasi does, and create nothing"
2363
+ );
2364
+ }
2365
+
2366
+ // @behavior RT-141
2367
+ #[cfg(unix)]
2368
+ #[test]
2369
+ fn a_link_staying_within_an_opened_directory_is_followed() {
2370
+ let host = LinkedBeneath::new("in");
2371
+ let answers = |overlay| {
2372
+ let mut guest = host.guest(overlay);
2373
+ [
2374
+ guest.call("open_following_beneath_dir", IN_LINKED),
2375
+ guest.call("open_following_beneath_dir", TWISTY),
2376
+ ]
2377
+ };
2378
+ assert_eq!(
2379
+ (answers(false), answers(true)),
2380
+ ([0; 2], [0; 2]),
2381
+ "an open through preview1 of a path whose links stay within a directory opened beneath \
2382
+ a Directory Mount, or an Overlay's Directory Layer, must reach the file they lead to"
2383
+ );
2384
+ }
2385
+
2386
+ // The link's target lies within `dir`, where Linux would create it, so only
2387
+ // the refusal to create through a link it cannot resolve answers ELOOP.
2388
+ // @behavior RT-142
2389
+ #[cfg(unix)]
2390
+ #[test]
2391
+ fn a_create_through_a_dangling_link_beneath_an_opened_directory_is_refused() {
2392
+ let host = LinkedBeneath::new("dangle");
2393
+ let answers = |overlay| {
2394
+ let errno = host
2395
+ .guest(overlay)
2396
+ .call("create_following_beneath_dir", NOWHERE);
2397
+ (errno, host.0.join("dir/missing").exists())
2398
+ };
2399
+ assert_eq!(
2400
+ (answers(false), answers(true)),
2401
+ ((ERRNO_LOOP, false), (ERRNO_LOOP, false)),
2402
+ "an open through preview1 creating a file through a link pointing at nothing beneath a \
2403
+ directory opened beneath a Directory Mount, or an Overlay's Directory Layer, must answer \
2404
+ ELOOP and create nothing"
2405
+ );
2406
+ }
2407
+
2408
+ // The link names a file outside the host directory, so cap-std refuses it
2409
+ // with no OS code, which wasmtime-wasi answers as EPERM.
2410
+ // @behavior RT-143
2411
+ #[cfg(unix)]
2412
+ #[test]
2413
+ fn a_link_leading_out_of_a_mount_is_not_permitted() {
2414
+ let host = LinkedBeneath::new("mount");
2415
+ let outside =
2416
+ std::env::temp_dir().join(format!("kobako-beneath-outside-{}", std::process::id()));
2417
+ std::fs::write(&outside, "host").expect("the outside file must be written");
2418
+ std::fs::remove_file(host.0.join("dir/up")).expect("the link must be removed");
2419
+ std::os::unix::fs::symlink(&outside, host.0.join("dir/up")).expect("the link must be made");
2420
+ let errno = host.guest(false).call("open_following", DIR_UP);
2421
+ let _ = std::fs::remove_file(&outside);
2422
+ assert_eq!(
2423
+ errno, ERRNO_PERM,
2424
+ "an open through preview1 from a Directory Mount's root of a link to a file outside its \
2425
+ host directory must answer EPERM, as wasmtime-wasi does"
2426
+ );
2427
+ }
2428
+
2429
+ // Each file is open but lacks the access its call needs, so only the order
2430
+ // of the bound and the access check decides between a trap and EBADF.
2431
+ // @behavior RT-144
2432
+ #[test]
2433
+ fn a_buffer_outside_memory_is_bounded_before_a_files_access_is_checked() {
2434
+ let call = |name, rights| {
2435
+ Guest::new(&[grant(file_and_directory(), Access::ReadWrite, None)])
2436
+ .try_call_at(name, rights)
2437
+ };
2438
+ assert_eq!(
2439
+ [
2440
+ call("read_far_with", RIGHTS_FD_WRITE),
2441
+ call("write_far_with", RIGHTS_FD_READ),
2442
+ call("pwrite_far_with", RIGHTS_FD_READ),
2443
+ call("pread_far_with", RIGHTS_FD_WRITE),
2444
+ ],
2445
+ [None, None, None, Some(ERRNO_BADF)],
2446
+ "through preview1 on a file opened without the access each call needs, with a buffer \
2447
+ past memory, fd_read, fd_write, and fd_pwrite must trap, and fd_pread must answer \
2448
+ EBADF, as wasmtime-wasi does"
2449
+ );
2450
+ }
2451
+
2452
+ const PUB_X: (i32, i32) = (1150, 5);
2453
+
2454
+ /// A Policy allowing `/data/pub/` alone, which swaps `pub` for a link to
2455
+ /// `secret` the first time it is asked, as another writer could between
2456
+ /// its judgment and the call.
2457
+ #[cfg(unix)]
2458
+ struct SwapsWhileAsked(std::path::PathBuf, std::sync::atomic::AtomicBool);
2459
+
2460
+ #[cfg(unix)]
2461
+ impl Policy for SwapsWhileAsked {
2462
+ fn allows(&self, path: &str, _access: Access) -> bool {
2463
+ if self.1.swap(false, Ordering::SeqCst) {
2464
+ std::fs::rename(self.0.join("pub"), self.0.join("pub_old")).expect("pub must move");
2465
+ std::os::unix::fs::symlink("secret", self.0.join("pub"))
2466
+ .expect("the link must be made");
2467
+ }
2468
+ path.starts_with("/data/pub/")
2469
+ }
2470
+ }
2471
+
2472
+ // The swap happens inside the Policy's own judgment, so only a call that
2473
+ // reaches the path without following the link swapped in meanwhile is
2474
+ // refused; a plain Directory would read `secret/x`.
2475
+ // @behavior RT-145
2476
+ #[cfg(unix)]
2477
+ #[test]
2478
+ fn a_link_swapped_in_after_a_policy_judges_a_path_is_not_followed() {
2479
+ let host = std::env::temp_dir().join(format!("kobako-swapped-{}", std::process::id()));
2480
+ let _ = std::fs::remove_dir_all(&host);
2481
+ for dir in ["pub", "secret"] {
2482
+ std::fs::create_dir_all(host.join(dir)).expect("the host directory must be created");
2483
+ std::fs::write(host.join(dir).join("x"), dir).expect("the host file must be written");
2484
+ }
2485
+ let policy = SwapsWhileAsked(host.clone(), std::sync::atomic::AtomicBool::new(true));
2486
+ let directory = Directory::new(&host).expect("the host directory must open");
2487
+ let errno = Guest::new(&[grant(
2488
+ Arc::new(directory),
2489
+ Access::Read,
2490
+ Some(Arc::new(policy)),
2491
+ )])
2492
+ .call("open_following", PUB_X);
2493
+ let _ = std::fs::remove_dir_all(&host);
2494
+ assert_eq!(
2495
+ errno, ERRNO_LOOP,
2496
+ "an open through preview1 beneath a Directory Mount with a Policy, of a path whose \
2497
+ directory another writer swaps for a link as the Policy judges it, must answer ELOOP"
2498
+ );
2499
+ }