kobako 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +217 -10
- data/Cargo.lock +139 -141
- data/README.md +96 -574
- data/ROADMAP.md +2 -1
- data/crates/kobako-runtime/CHANGELOG.md +269 -1
- data/crates/kobako-runtime/Cargo.toml +25 -15
- data/crates/kobako-runtime/README.md +7 -3
- data/crates/kobako-runtime/src/dispatch.rs +1 -1
- data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
- data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
- data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
- data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
- data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
- data/crates/kobako-runtime/src/endpoint.rs +166 -0
- data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
- data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
- data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
- data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
- data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
- data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
- data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
- data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
- data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
- data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
- data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
- data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
- data/crates/kobako-runtime/src/filesystem.rs +21 -387
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +11 -6
- data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
- data/crates/kobako-runtime/src/locking.rs +218 -0
- data/crates/kobako-runtime/src/mount.rs +31 -104
- data/crates/kobako-runtime/src/profile.rs +137 -1
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
- data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
- data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
- data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
- data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
- data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
- data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
- data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
- data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
- data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
- data/crates/kobako-runtime/src/vfs/path.rs +64 -0
- data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
- data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
- data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
- data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
- data/crates/kobako-runtime/src/vfs.rs +60 -0
- data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
- data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
- data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
- data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
- data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
- data/crates/kobako-transport/CHANGELOG.md +8 -2
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
- data/crates/kobako-transport/src/envelope/call.rs +5 -5
- data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
- data/crates/kobako-transport/src/envelope/fault.rs +2 -2
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/request.rs +2 -2
- data/crates/kobako-transport/src/envelope/run.rs +1 -1
- data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
- data/crates/kobako-transport/src/envelope.rs +2 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
- data/crates/kobako-wasmtime/Cargo.toml +15 -14
- data/crates/kobako-wasmtime/README.md +9 -5
- data/crates/kobako-wasmtime/src/abi.rs +12 -20
- data/crates/kobako-wasmtime/src/ambient.rs +1 -2
- data/crates/kobako-wasmtime/src/cache.rs +5 -5
- data/crates/kobako-wasmtime/src/config.rs +22 -7
- data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
- data/crates/kobako-wasmtime/src/driver.rs +29 -96
- data/crates/kobako-wasmtime/src/exports.rs +5 -16
- data/crates/kobako-wasmtime/src/guest.rs +38 -0
- data/crates/kobako-wasmtime/src/imports.rs +14 -8
- data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
- data/crates/kobako-wasmtime/src/invocation.rs +26 -46
- data/crates/kobako-wasmtime/src/lib.rs +1 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
- data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
- data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
- data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
- data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
- data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
- data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
- data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
- data/crates/kobako-wasmtime/src/preview1.rs +464 -413
- data/crates/kobako-wasmtime/src/trap.rs +9 -9
- data/crates/kobako-wasmtime/src/wasi.rs +53 -37
- data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
- data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
- data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/runtime/bridge.rs +1 -1
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +28 -14
- data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
- data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
- data/ext/kobako/src/runtime/filesystem.rs +109 -137
- data/ext/kobako/src/runtime/gvl.rs +43 -8
- data/ext/kobako/src/runtime/snapshot.rs +1 -1
- data/ext/kobako/src/runtime.rs +6 -1
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -2
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +2 -10
- data/lib/kobako/catalog/services.rb +2 -2
- data/lib/kobako/catalog/snippets.rb +7 -7
- data/lib/kobako/catalog.rb +1 -1
- data/lib/kobako/codec/decoder.rb +10 -10
- data/lib/kobako/codec/ext_types.rb +3 -5
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +3 -3
- data/lib/kobako/context.rb +5 -5
- data/lib/kobako/filesystem.rb +44 -13
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +4 -4
- data/lib/kobako/sandbox.rb +28 -28
- data/lib/kobako/sandbox_options.rb +2 -3
- data/lib/kobako/snippet/bytecode.rb +1 -1
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/transport/dispatcher.rb +5 -5
- data/lib/kobako/transport/exposure.rb +2 -2
- data/lib/kobako/transport/reflection.rb +7 -7
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +1 -1
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +34 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +0 -4
- data/sig/kobako/catalog/services.rbs +1 -1
- data/sig/kobako/catalog/snippets.rbs +3 -3
- data/sig/kobako/catalog.rbs +1 -1
- data/sig/kobako/codec/decoder.rbs +1 -1
- data/sig/kobako/codec/handle_walk.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +2 -2
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +1 -1
- data/sig/kobako/codec.rbs +1 -2
- data/sig/kobako/context.rbs +1 -1
- data/sig/kobako/filesystem.rbs +19 -14
- data/sig/kobako/guest_yielder.rbs +1 -1
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +2 -2
- data/sig/kobako/runtime.rbs +1 -1
- data/sig/kobako/sandbox.rbs +3 -5
- data/sig/kobako/sandbox_options.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +2 -2
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +2 -2
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport.rbs +1 -1
- metadata +67 -19
- data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
- data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
- data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
- data/crates/kobako-wasmtime/src/io.rs +0 -89
- data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
- /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
- /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
|
@@ -1,421 +1,302 @@
|
|
|
1
|
-
//! The 10 `path_*` calls. Each
|
|
2
|
-
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
1
|
+
//! The 10 `path_*` calls. Each hands the call to the runtime's VFS, which
|
|
2
|
+
//! resolves the path, asks the Mount, and asks the Filesystem. What a Linux
|
|
3
|
+
//! syscall entry checks before the VFS stays here, such as the order of an
|
|
4
|
+
//! open's errors.
|
|
5
5
|
|
|
6
|
-
use
|
|
6
|
+
use kobako_runtime::filesystem::{FilesystemError, Iattr};
|
|
7
|
+
use kobako_runtime::vfs::{self, OpenHow, Start};
|
|
8
|
+
use wiggle::GuestPtr;
|
|
7
9
|
|
|
8
|
-
use kobako_runtime::filesystem::{
|
|
9
|
-
Access, FileType, Filesystem, FilesystemError, OpenOptions, Rename,
|
|
10
|
-
};
|
|
11
|
-
use kobako_runtime::mount::Mount;
|
|
12
|
-
|
|
13
|
-
use super::args::{
|
|
14
|
-
self, FDFLAGS_APPEND, FDFLAGS_NONBLOCK, FDFLAGS_SYNCHRONIZED, RIGHTS_FD_READ, RIGHTS_FD_WRITE,
|
|
15
|
-
};
|
|
16
|
-
use super::beneath::{resolve, ResolvedPath};
|
|
17
10
|
use super::errno::{Answer, Errno};
|
|
18
|
-
use super::fd::
|
|
11
|
+
use super::fd::to_filestat;
|
|
19
12
|
use super::memory::GuestMemory;
|
|
20
|
-
use
|
|
21
|
-
|
|
22
|
-
const LOOKUPFLAGS_SYMLINK_FOLLOW: u32 = 1 << 0;
|
|
23
|
-
const OFLAGS_CREAT: u32 = 1 << 0;
|
|
24
|
-
const OFLAGS_DIRECTORY: u32 = 1 << 1;
|
|
25
|
-
const OFLAGS_EXCL: u32 = 1 << 2;
|
|
26
|
-
const OFLAGS_TRUNC: u32 = 1 << 3;
|
|
13
|
+
use super::types;
|
|
14
|
+
use crate::descriptors::{Descriptor, Descriptors};
|
|
27
15
|
|
|
28
|
-
/// The
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
pub(crate)
|
|
33
|
-
pub(crate)
|
|
34
|
-
pub(crate)
|
|
16
|
+
/// The arguments `path_open` takes besides where it starts and the path, as
|
|
17
|
+
/// Linux's `openat2` takes a `struct open_how`; they become the `OpenHow` the
|
|
18
|
+
/// VFS opens with.
|
|
19
|
+
pub(crate) struct PathOpen {
|
|
20
|
+
pub(crate) dirflags: types::Lookupflags,
|
|
21
|
+
pub(crate) oflags: types::Oflags,
|
|
22
|
+
pub(crate) rights_base: types::Rights,
|
|
23
|
+
pub(crate) fdflags: types::Fdflags,
|
|
35
24
|
}
|
|
36
25
|
|
|
37
|
-
///
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Descriptor::Stdin(_) | Descriptor::Channel(_) | Descriptor::File(_) => {
|
|
42
|
-
Err(Errno::Badf.into())
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
26
|
+
/// How many times a call is made before a guest is answered EAGAIN, as cap-std
|
|
27
|
+
/// makes `openat2` four times: any move in the process answers a `..` walking
|
|
28
|
+
/// past it so, and nothing bounds how often one lands.
|
|
29
|
+
const MAX_ATTEMPTS: usize = 4;
|
|
46
30
|
|
|
47
|
-
///
|
|
48
|
-
///
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
/// What `call` answers once no move landed during its walk, made again at most
|
|
32
|
+
/// `MAX_ATTEMPTS` times in all, as cap-std retries `openat2` on EAGAIN.
|
|
33
|
+
pub(super) fn retry_eagain<T>(
|
|
34
|
+
mut call: impl FnMut() -> Result<T, FilesystemError>,
|
|
35
|
+
) -> Result<T, FilesystemError> {
|
|
36
|
+
let mut answer = call();
|
|
37
|
+
for _ in 1..MAX_ATTEMPTS {
|
|
38
|
+
if !matches!(answer, Err(FilesystemError::WouldBlock)) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
answer = call();
|
|
52
42
|
}
|
|
53
|
-
|
|
43
|
+
answer
|
|
54
44
|
}
|
|
55
45
|
|
|
56
|
-
///
|
|
57
|
-
///
|
|
58
|
-
///
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Ok(_) => {}
|
|
67
|
-
// What is missing holds no link; the call answers for it.
|
|
68
|
-
Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => break,
|
|
69
|
-
Err(err) => return Err(err.into()),
|
|
46
|
+
/// Where `fd` stands, for a path to be walked from, once the guard answered for a
|
|
47
|
+
/// directory there, found when the VFS has taken the name, as Linux's
|
|
48
|
+
/// `path_init` takes the descriptor. What is no directory the walk itself
|
|
49
|
+
/// refuses, and a move the guard meets is one a walk meets, so `retry_eagain`
|
|
50
|
+
/// makes the call again.
|
|
51
|
+
fn start_of(table: &Descriptors, fd: u32) -> impl vfs::Start + '_ {
|
|
52
|
+
move || {
|
|
53
|
+
let file = &table.get(fd).ok_or(FilesystemError::BadDescriptor)?.file;
|
|
54
|
+
if file.is_directory() {
|
|
55
|
+
table.walk_guard().check(file.path())?;
|
|
70
56
|
}
|
|
57
|
+
Ok(file.path().clone())
|
|
71
58
|
}
|
|
72
|
-
Ok(())
|
|
73
59
|
}
|
|
74
60
|
|
|
75
|
-
/// A path beneath `fd`, relative to its mount's root, for a call acting
|
|
76
|
-
/// on what stands there rather than where a link leads: refused when it
|
|
77
|
-
/// ends in `/` and names a file or a link.
|
|
78
|
-
fn beneath<'a>(
|
|
79
|
-
table: &'a Descriptors,
|
|
80
|
-
mem: &GuestMemory,
|
|
81
|
-
fd: u32,
|
|
82
|
-
path: (u32, u32),
|
|
83
|
-
) -> Answer<(&'a OpenDirectory, String)> {
|
|
84
|
-
let dir = directory(table, fd)?;
|
|
85
|
-
let path = resolve(dir, &mem.path(path.0, path.1)?)?.check_directory(
|
|
86
|
-
dir.mount.filesystem().as_ref(),
|
|
87
|
-
<dyn Filesystem>::symlink_metadata,
|
|
88
|
-
)?;
|
|
89
|
-
Ok((dir, path))
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/// A trailing `/` asks for the directory being made, so it is no
|
|
93
|
-
/// refusal here, as `mkdir(2)` takes it.
|
|
94
61
|
pub(crate) fn create_directory(
|
|
95
62
|
table: &Descriptors,
|
|
96
|
-
mem: &GuestMemory,
|
|
63
|
+
mem: &GuestMemory<'_, '_>,
|
|
97
64
|
fd: u32,
|
|
98
|
-
path:
|
|
65
|
+
path: GuestPtr<str>,
|
|
99
66
|
) -> Answer {
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
67
|
+
let path = mem.path(path)?;
|
|
68
|
+
Ok(retry_eagain(|| {
|
|
69
|
+
vfs::filename_mkdirat(table.cred(), &start_of(table, fd), &path)
|
|
70
|
+
})?)
|
|
104
71
|
}
|
|
105
72
|
|
|
106
73
|
pub(crate) fn filestat_get(
|
|
107
74
|
table: &Descriptors,
|
|
108
|
-
mem: &
|
|
75
|
+
mem: &GuestMemory<'_, '_>,
|
|
109
76
|
fd: u32,
|
|
110
|
-
flags:
|
|
111
|
-
path:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
let follow = flags & LOOKUPFLAGS_SYMLINK_FOLLOW != 0 || resolved.names_directory;
|
|
119
|
-
let resolved = if follow {
|
|
120
|
-
resolved.follow_last(dir, false)?
|
|
121
|
-
} else {
|
|
122
|
-
resolved
|
|
123
|
-
};
|
|
124
|
-
let filesystem = dir.mount.filesystem();
|
|
125
|
-
let path = resolved.check_directory(filesystem.as_ref(), <dyn Filesystem>::metadata)?;
|
|
126
|
-
let metadata = if follow {
|
|
127
|
-
filesystem.metadata(&path)?
|
|
128
|
-
} else {
|
|
129
|
-
filesystem.symlink_metadata(&path)?
|
|
130
|
-
};
|
|
131
|
-
write_metadata(mem, out, metadata)
|
|
77
|
+
flags: types::Lookupflags,
|
|
78
|
+
path: GuestPtr<str>,
|
|
79
|
+
) -> Answer<types::Filestat> {
|
|
80
|
+
let path = mem.path(path)?;
|
|
81
|
+
let follow = flags.contains(types::Lookupflags::SYMLINK_FOLLOW);
|
|
82
|
+
let stat =
|
|
83
|
+
retry_eagain(|| vfs::vfs_fstatat(table.cred(), &start_of(table, fd), &path, follow))?;
|
|
84
|
+
Ok(to_filestat(table, stat))
|
|
132
85
|
}
|
|
133
86
|
|
|
134
|
-
/// A Filesystem keeps no timestamps of the guest's choosing.
|
|
135
87
|
pub(crate) fn filestat_set_times(
|
|
136
88
|
table: &Descriptors,
|
|
137
|
-
mem: &GuestMemory,
|
|
89
|
+
mem: &GuestMemory<'_, '_>,
|
|
138
90
|
fd: u32,
|
|
139
|
-
flags:
|
|
140
|
-
path:
|
|
141
|
-
|
|
91
|
+
flags: types::Lookupflags,
|
|
92
|
+
path: GuestPtr<str>,
|
|
93
|
+
times: &Iattr,
|
|
142
94
|
) -> Answer {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
95
|
+
let path = mem.path(path)?;
|
|
96
|
+
let follow = flags.contains(types::Lookupflags::SYMLINK_FOLLOW);
|
|
97
|
+
Ok(retry_eagain(|| {
|
|
98
|
+
vfs::do_utimes_path(table.cred(), &start_of(table, fd), &path, follow, times)
|
|
99
|
+
})?)
|
|
147
100
|
}
|
|
148
101
|
|
|
149
|
-
/// A Filesystem holds no links, and answering as if one were made would
|
|
150
|
-
/// leave the guest believing it was.
|
|
151
102
|
pub(crate) fn link(
|
|
152
103
|
table: &Descriptors,
|
|
153
|
-
mem: &GuestMemory,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
new_fd: u32,
|
|
158
|
-
new_path: (u32, u32),
|
|
104
|
+
mem: &GuestMemory<'_, '_>,
|
|
105
|
+
old: (u32, GuestPtr<str>),
|
|
106
|
+
new: (u32, GuestPtr<str>),
|
|
107
|
+
flags: types::Lookupflags,
|
|
159
108
|
) -> Answer {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
109
|
+
let (old_path, new_path) = (mem.path(old.1)?, mem.path(new.1)?);
|
|
110
|
+
let follow = flags.contains(types::Lookupflags::SYMLINK_FOLLOW);
|
|
111
|
+
Ok(retry_eagain(|| {
|
|
112
|
+
let (from, to) = (start_of(table, old.0), start_of(table, new.0));
|
|
113
|
+
vfs::filename_linkat(table.cred(), &from, &old_path, &to, &new_path, follow)
|
|
114
|
+
})?)
|
|
166
115
|
}
|
|
167
116
|
|
|
168
117
|
pub(crate) fn open(
|
|
169
118
|
table: &mut Descriptors,
|
|
170
|
-
mem: &
|
|
119
|
+
mem: &GuestMemory<'_, '_>,
|
|
171
120
|
fd: u32,
|
|
172
|
-
path:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
resolved.follow_last(dir, oflag(OFLAGS_CREAT))?
|
|
207
|
-
} else {
|
|
208
|
-
resolved
|
|
209
|
-
};
|
|
210
|
-
let path =
|
|
211
|
-
resolved.check_directory(dir.mount.filesystem().as_ref(), <dyn Filesystem>::metadata)?;
|
|
212
|
-
let mount = Arc::clone(&dir.mount);
|
|
213
|
-
if table.is_full() {
|
|
214
|
-
return Err(Errno::Mfile.into());
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
let read = flags.rights_base & RIGHTS_FD_READ != 0;
|
|
218
|
-
let write = flags.rights_base & RIGHTS_FD_WRITE != 0;
|
|
219
|
-
if !mount.follows_links() {
|
|
220
|
-
refuse_links(&mount, &path, follow)?;
|
|
221
|
-
}
|
|
222
|
-
let looked_up = if follow {
|
|
223
|
-
mount.filesystem().metadata(&path)
|
|
224
|
-
} else {
|
|
225
|
-
mount.filesystem().symlink_metadata(&path)
|
|
226
|
-
};
|
|
227
|
-
let (existing, identity) = match looked_up {
|
|
228
|
-
Ok(metadata) => (Some(metadata.file_type), Identity::of(&metadata)),
|
|
229
|
-
Err(FilesystemError::NotFound) => (None, None),
|
|
230
|
-
Err(err) => return Err(err.into()),
|
|
231
|
-
};
|
|
232
|
-
// What stands at the path answers before any access is judged, as
|
|
233
|
-
// Linux's `open` does on a read-only mount.
|
|
234
|
-
let refusal = match existing {
|
|
235
|
-
None if !oflag(OFLAGS_CREAT) => Some(Errno::Noent),
|
|
236
|
-
Some(_) if oflag(OFLAGS_CREAT) && oflag(OFLAGS_EXCL) => Some(Errno::Exist),
|
|
237
|
-
Some(FileType::Directory) if write || oflag(OFLAGS_TRUNC | OFLAGS_CREAT) => {
|
|
238
|
-
Some(Errno::Isdir)
|
|
239
|
-
}
|
|
240
|
-
Some(FileType::Directory) | None => None,
|
|
241
|
-
Some(_) if oflag(OFLAGS_DIRECTORY) => Some(Errno::Notdir),
|
|
242
|
-
Some(_) => None,
|
|
243
|
-
};
|
|
244
|
-
if let Some(errno) = refusal {
|
|
245
|
-
return Err(errno.into());
|
|
246
|
-
}
|
|
247
|
-
// Creating changes the Mount only where nothing stands yet, so the
|
|
248
|
-
// Mount judges the open once, knowing what it does.
|
|
249
|
-
let changes = write || oflag(OFLAGS_TRUNC) || (oflag(OFLAGS_CREAT) && existing.is_none());
|
|
250
|
-
let access = if changes {
|
|
251
|
-
Access::ReadWrite
|
|
252
|
-
} else {
|
|
253
|
-
Access::Read
|
|
254
|
-
};
|
|
255
|
-
mount.check(&path, access)?;
|
|
256
|
-
let descriptor = match existing {
|
|
257
|
-
Some(FileType::Directory) => Descriptor::Directory(OpenDirectory {
|
|
258
|
-
_hold: mount.filesystem().hold_directory(&path)?,
|
|
259
|
-
mount,
|
|
260
|
-
path,
|
|
261
|
-
identity,
|
|
262
|
-
preopen: false,
|
|
263
|
-
listing: None,
|
|
264
|
-
}),
|
|
265
|
-
Some(_) | None => {
|
|
266
|
-
let mut options = OpenOptions::default();
|
|
267
|
-
options.read = read;
|
|
268
|
-
options.write = write;
|
|
269
|
-
// A file already there is opened as it is, so a host that opens
|
|
270
|
-
// only to create never asks for more than the guest.
|
|
271
|
-
options.create = oflag(OFLAGS_CREAT) && existing.is_none();
|
|
272
|
-
options.create_new = oflag(OFLAGS_CREAT) && oflag(OFLAGS_EXCL);
|
|
273
|
-
options.truncate = oflag(OFLAGS_TRUNC);
|
|
274
|
-
options.nofollow = !follow;
|
|
275
|
-
Descriptor::File(OpenFile {
|
|
276
|
-
file: mount.filesystem().open(&path, &options)?,
|
|
277
|
-
position: 0,
|
|
278
|
-
append: flags.fdflags & FDFLAGS_APPEND != 0,
|
|
279
|
-
readable: read,
|
|
280
|
-
writable: write,
|
|
281
|
-
mount,
|
|
282
|
-
})
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
let opened = table.push(descriptor).ok_or(Errno::Mfile)?;
|
|
286
|
-
mem.write_u32(out, opened)
|
|
121
|
+
path: GuestPtr<str>,
|
|
122
|
+
call: &PathOpen,
|
|
123
|
+
) -> Answer<types::Fd> {
|
|
124
|
+
let oflags = call.oflags;
|
|
125
|
+
let mut how = OpenHow::default();
|
|
126
|
+
// wasi-libc asks `fd_read` and `fd_readdir` for `O_RDONLY` and neither for
|
|
127
|
+
// `O_SEARCH`, which glibc spells `O_PATH`; wasi-testsuite lists a
|
|
128
|
+
// directory opened with `fd_readdir` alone, and makes a file asking no
|
|
129
|
+
// right at all, which only an `O_RDONLY` open does.
|
|
130
|
+
how.create = oflags.contains(types::Oflags::CREAT);
|
|
131
|
+
how.exclusive = oflags.contains(types::Oflags::EXCL);
|
|
132
|
+
how.truncate = oflags.contains(types::Oflags::TRUNC);
|
|
133
|
+
how.write = call.rights_base.contains(types::Rights::FD_WRITE);
|
|
134
|
+
how.read = call
|
|
135
|
+
.rights_base
|
|
136
|
+
.intersects(types::Rights::FD_READ | types::Rights::FD_READDIR)
|
|
137
|
+
|| (!how.write && (how.create || how.truncate));
|
|
138
|
+
how.directory = oflags.contains(types::Oflags::DIRECTORY);
|
|
139
|
+
how.follow = call.dirflags.contains(types::Lookupflags::SYMLINK_FOLLOW);
|
|
140
|
+
how.append = call.fdflags.contains(types::Fdflags::APPEND);
|
|
141
|
+
how.nonblock = call.fdflags.contains(types::Fdflags::NONBLOCK);
|
|
142
|
+
how.dsync = call.fdflags.contains(types::Fdflags::DSYNC);
|
|
143
|
+
how.sync = call
|
|
144
|
+
.fdflags
|
|
145
|
+
.intersects(types::Fdflags::RSYNC | types::Fdflags::SYNC);
|
|
146
|
+
// Linux's `do_sys_openat2` builds the flags, then reserves a descriptor
|
|
147
|
+
// before `do_file_open` answers for the name, so a full table answers
|
|
148
|
+
// whatever the path names.
|
|
149
|
+
let op = vfs::build_open_flags(&how)?;
|
|
150
|
+
let new = table.lowest_unused().ok_or(Errno::Mfile)?;
|
|
151
|
+
let path = mem.path(path)?;
|
|
152
|
+
let file = retry_eagain(|| vfs::do_file_open(table.cred(), &start_of(table, fd), &path, &op))?;
|
|
153
|
+
table.place(new, Descriptor::for_file(file));
|
|
154
|
+
Ok(new.into())
|
|
287
155
|
}
|
|
288
156
|
|
|
289
|
-
/// A Filesystem holds no links to read.
|
|
290
157
|
pub(crate) fn readlink(
|
|
291
158
|
table: &Descriptors,
|
|
292
|
-
mem: &GuestMemory,
|
|
159
|
+
mem: &mut GuestMemory<'_, '_>,
|
|
293
160
|
fd: u32,
|
|
294
|
-
path:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
/// Refuse a guest path ending in `.` or `..` as `refusal`, since it names
|
|
301
|
-
/// no entry a call may remove or move. Linux looks up what stands before
|
|
302
|
-
/// it first, so a missing or non-directory parent answers as itself.
|
|
303
|
-
fn refuse_dot_end(dir: &OpenDirectory, path: &str, refusal: Errno) -> Answer {
|
|
304
|
-
let trimmed = path.trim_end_matches('/');
|
|
305
|
-
let (parent, last) = trimmed.rsplit_once('/').unwrap_or(("", trimmed));
|
|
306
|
-
if !matches!(last, "." | "..") {
|
|
307
|
-
return Ok(());
|
|
308
|
-
}
|
|
309
|
-
if !parent.is_empty() {
|
|
310
|
-
let parent = resolve(dir, parent)?.path;
|
|
311
|
-
if dir.mount.filesystem().metadata(&parent)?.file_type != FileType::Directory {
|
|
312
|
-
return Err(Errno::Notdir.into());
|
|
313
|
-
}
|
|
161
|
+
path: GuestPtr<str>,
|
|
162
|
+
buf: GuestPtr<u8>,
|
|
163
|
+
buf_len: types::Size,
|
|
164
|
+
) -> Answer<types::Size> {
|
|
165
|
+
if buf_len == 0 {
|
|
166
|
+
return Err(Errno::Inval.into());
|
|
314
167
|
}
|
|
315
|
-
|
|
168
|
+
let path = mem.path(path)?;
|
|
169
|
+
let target = retry_eagain(|| vfs::do_readlinkat(table.cred(), &start_of(table, fd), &path))?;
|
|
170
|
+
write_link(mem, &target, buf, buf_len)
|
|
316
171
|
}
|
|
317
172
|
|
|
318
|
-
///
|
|
319
|
-
///
|
|
320
|
-
fn
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
173
|
+
/// As much of where a link points as the buffer holds, as `readlink` cuts it
|
|
174
|
+
/// off there without saying so.
|
|
175
|
+
fn write_link(
|
|
176
|
+
mem: &mut GuestMemory<'_, '_>,
|
|
177
|
+
target: &str,
|
|
178
|
+
buf: GuestPtr<u8>,
|
|
179
|
+
buf_len: types::Size,
|
|
180
|
+
) -> Answer<types::Size> {
|
|
181
|
+
let take = target.len().min(buf_len as usize);
|
|
182
|
+
mem.write_bytes(buf, &target.as_bytes()[..take])?;
|
|
183
|
+
Ok(take as u32)
|
|
325
184
|
}
|
|
326
185
|
|
|
327
186
|
pub(crate) fn remove_directory(
|
|
328
187
|
table: &Descriptors,
|
|
329
|
-
mem: &GuestMemory,
|
|
188
|
+
mem: &GuestMemory<'_, '_>,
|
|
330
189
|
fd: u32,
|
|
331
|
-
path:
|
|
190
|
+
path: GuestPtr<str>,
|
|
332
191
|
) -> Answer {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
)?;
|
|
338
|
-
let (dir, path) = beneath(table, mem, fd, path)?;
|
|
339
|
-
refuse_root(&path, Errno::Inval)?;
|
|
340
|
-
judge(&dir.mount, &path, Access::ReadWrite, false)?;
|
|
341
|
-
Ok(dir.mount.filesystem().remove_dir(&path)?)
|
|
192
|
+
let path = mem.path(path)?;
|
|
193
|
+
Ok(retry_eagain(|| {
|
|
194
|
+
vfs::filename_rmdir(table.cred(), &start_of(table, fd), &path)
|
|
195
|
+
})?)
|
|
342
196
|
}
|
|
343
197
|
|
|
344
|
-
/// A rename stays within one Mount; across two it is `XDEV`, as
|
|
345
|
-
/// across two mounted devices. A directory moves only beneath a Mount
|
|
346
|
-
/// that allows it, the Filesystem checking what it moves as it moves it.
|
|
347
198
|
pub(crate) fn rename(
|
|
348
199
|
table: &Descriptors,
|
|
349
|
-
mem: &GuestMemory,
|
|
350
|
-
old: (u32,
|
|
351
|
-
new: (u32,
|
|
200
|
+
mem: &GuestMemory<'_, '_>,
|
|
201
|
+
old: (u32, GuestPtr<str>),
|
|
202
|
+
new: (u32, GuestPtr<str>),
|
|
352
203
|
) -> Answer {
|
|
353
|
-
let
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
let from = resolve(from_dir, &from)?;
|
|
359
|
-
let to = resolve(to_dir, &to)?;
|
|
360
|
-
refuse_root(&from.path, Errno::Busy)?;
|
|
361
|
-
refuse_root(&to.path, Errno::Busy)?;
|
|
362
|
-
// A trailing `/` on either side asks that the source be a directory,
|
|
363
|
-
// which `rename(2)` checks before it finds a rename onto its own path
|
|
364
|
-
// leaves everything as it was. A rename moves a link rather than what
|
|
365
|
-
// it points at, so each side is checked as it stands.
|
|
366
|
-
let names_directory = from.names_directory || to.names_directory;
|
|
367
|
-
let from = ResolvedPath {
|
|
368
|
-
names_directory,
|
|
369
|
-
..from
|
|
370
|
-
}
|
|
371
|
-
.check_directory(
|
|
372
|
-
from_dir.mount.filesystem().as_ref(),
|
|
373
|
-
<dyn Filesystem>::symlink_metadata,
|
|
374
|
-
)?;
|
|
375
|
-
let to = match to {
|
|
376
|
-
to if to.path == from => to.path,
|
|
377
|
-
to => to.check_directory(
|
|
378
|
-
to_dir.mount.filesystem().as_ref(),
|
|
379
|
-
<dyn Filesystem>::symlink_metadata,
|
|
380
|
-
)?,
|
|
381
|
-
};
|
|
382
|
-
if !Arc::ptr_eq(&from_dir.mount, &to_dir.mount) {
|
|
383
|
-
return Err(Errno::Xdev.into());
|
|
384
|
-
}
|
|
385
|
-
judge(&from_dir.mount, &from, Access::ReadWrite, false)?;
|
|
386
|
-
judge(&to_dir.mount, &to, Access::ReadWrite, false)?;
|
|
387
|
-
let rename = match (from_dir.mount.moves_directories(), names_directory) {
|
|
388
|
-
(false, _) => Rename::File,
|
|
389
|
-
(true, true) => Rename::Directory,
|
|
390
|
-
(true, false) => Rename::Any,
|
|
391
|
-
};
|
|
392
|
-
Ok(from_dir.mount.filesystem().rename(&from, &to, rename)?)
|
|
204
|
+
let (from_path, to_path) = (mem.path(old.1)?, mem.path(new.1)?);
|
|
205
|
+
Ok(retry_eagain(|| {
|
|
206
|
+
let (from, to) = (start_of(table, old.0), start_of(table, new.0));
|
|
207
|
+
vfs::filename_renameat2(table.cred(), &from, &from_path, &to, &to_path)
|
|
208
|
+
})?)
|
|
393
209
|
}
|
|
394
210
|
|
|
395
|
-
/// A Filesystem holds no links to make.
|
|
396
211
|
pub(crate) fn symlink(
|
|
397
212
|
table: &Descriptors,
|
|
398
|
-
mem: &GuestMemory,
|
|
399
|
-
old_path:
|
|
213
|
+
mem: &GuestMemory<'_, '_>,
|
|
214
|
+
old_path: GuestPtr<str>,
|
|
400
215
|
fd: u32,
|
|
401
|
-
new_path:
|
|
216
|
+
new_path: GuestPtr<str>,
|
|
402
217
|
) -> Answer {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
218
|
+
let (target, path) = (mem.path(old_path)?, mem.path(new_path)?);
|
|
219
|
+
// wasi-testsuite's symlink_create has a guest make no link to an absolute
|
|
220
|
+
// path, where Linux would make one; a guest walks none of them anyway. It
|
|
221
|
+
// is refused once the start is found, where the link would be made.
|
|
222
|
+
let start = || -> Result<vfs::Path, FilesystemError> {
|
|
223
|
+
let start = start_of(table, fd).path_init()?;
|
|
224
|
+
match target.starts_with('/') {
|
|
225
|
+
true => Err(FilesystemError::NotPermitted),
|
|
226
|
+
false => Ok(start),
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
Ok(retry_eagain(|| {
|
|
230
|
+
vfs::filename_symlinkat(table.cred(), &target, &start, &path)
|
|
231
|
+
})?)
|
|
407
232
|
}
|
|
408
233
|
|
|
409
234
|
pub(crate) fn unlink_file(
|
|
410
235
|
table: &Descriptors,
|
|
411
|
-
mem: &GuestMemory,
|
|
236
|
+
mem: &GuestMemory<'_, '_>,
|
|
412
237
|
fd: u32,
|
|
413
|
-
path:
|
|
238
|
+
path: GuestPtr<str>,
|
|
414
239
|
) -> Answer {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
240
|
+
let path = mem.path(path)?;
|
|
241
|
+
Ok(retry_eagain(|| {
|
|
242
|
+
vfs::filename_unlinkat(table.cred(), &start_of(table, fd), &path)
|
|
243
|
+
})?)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[cfg(test)]
|
|
247
|
+
mod tests {
|
|
248
|
+
use super::*;
|
|
249
|
+
|
|
250
|
+
// Every Invocation holds to the hook refusing a readlink, so no guest call
|
|
251
|
+
// reaches this; only the encoding itself can witness it.
|
|
252
|
+
#[test]
|
|
253
|
+
fn a_link_is_cut_off_where_the_buffer_ends() {
|
|
254
|
+
let mut bytes = [0u8; 8];
|
|
255
|
+
let answered = {
|
|
256
|
+
let mut memory = wiggle::GuestMemory::Unshared(&mut bytes);
|
|
257
|
+
let mut mem = GuestMemory::new(&mut memory, 64);
|
|
258
|
+
write_link(&mut mem, "target", GuestPtr::new(2), 3)
|
|
259
|
+
.unwrap_or_else(|_| panic!("the link must write"))
|
|
260
|
+
};
|
|
261
|
+
assert_eq!(
|
|
262
|
+
(answered, &bytes),
|
|
263
|
+
(3, b"\0\0tar\0\0\0"),
|
|
264
|
+
"where a link points through path_readlink must fill the buffer and answer what fit"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// What `retry_eagain` answers when the first `moves` calls each had a move
|
|
269
|
+
/// land in them, and how many calls it made.
|
|
270
|
+
fn retry_after_moves(moves: usize) -> (Result<(), FilesystemError>, usize) {
|
|
271
|
+
let mut made = 0;
|
|
272
|
+
let answer = retry_eagain(|| {
|
|
273
|
+
made += 1;
|
|
274
|
+
match made <= moves {
|
|
275
|
+
true => Err(FilesystemError::WouldBlock),
|
|
276
|
+
false => Ok(()),
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
(answer, made)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A move lands during a walk only between two of its steps, which no guest
|
|
283
|
+
// call can time, so the retry itself witnesses it.
|
|
284
|
+
// @behavior RT-256
|
|
285
|
+
#[test]
|
|
286
|
+
fn a_path_call_a_move_landed_in_is_made_again_four_times_at_most() {
|
|
287
|
+
assert_eq!(
|
|
288
|
+
[
|
|
289
|
+
retry_after_moves(0),
|
|
290
|
+
retry_after_moves(3),
|
|
291
|
+
retry_after_moves(4)
|
|
292
|
+
],
|
|
293
|
+
[
|
|
294
|
+
(Ok(()), 1),
|
|
295
|
+
(Ok(()), 4),
|
|
296
|
+
(Err(FilesystemError::WouldBlock), 4),
|
|
297
|
+
],
|
|
298
|
+
"a path_* call answered EAGAIN must be made again until it answers otherwise, four \
|
|
299
|
+
times in all at most, as cap-std makes openat2"
|
|
300
|
+
);
|
|
301
|
+
}
|
|
421
302
|
}
|