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,71 +1,81 @@
|
|
|
1
1
|
//! How a host directory spells a name it holds. A host that folds case or
|
|
2
|
-
//! Unicode form reaches one entry by many names, so
|
|
3
|
-
//! name as asked would
|
|
2
|
+
//! Unicode form reaches one entry by many names, so the runtime keeping a
|
|
3
|
+
//! dentry per name as asked would hold one entry under several.
|
|
4
4
|
//!
|
|
5
5
|
//! macOS answers the stored name for any spelling. Elsewhere a listing
|
|
6
6
|
//! finds the entry by its inode, skipped on a Linux filesystem known to
|
|
7
7
|
//! match names exactly; without an inode, a spelling the listing lacks is
|
|
8
8
|
//! refused.
|
|
9
9
|
|
|
10
|
+
#[cfg(not(target_os = "macos"))]
|
|
10
11
|
use std::io;
|
|
11
12
|
|
|
12
13
|
use cap_std::fs::Dir;
|
|
13
14
|
|
|
15
|
+
use super::{to_error, FilesystemError};
|
|
16
|
+
|
|
14
17
|
/// The name `parent` stores for the entry `name` reaches, a link reached
|
|
15
|
-
/// as itself
|
|
16
|
-
pub(super) fn
|
|
18
|
+
/// as itself; `name` as given when nothing is there.
|
|
19
|
+
pub(super) fn lookup(parent: &Dir, name: &str) -> Result<String, FilesystemError> {
|
|
17
20
|
#[cfg(target_os = "macos")]
|
|
18
21
|
{
|
|
19
|
-
macos::
|
|
22
|
+
macos::lookup(parent, name).map_err(to_error)
|
|
20
23
|
}
|
|
21
24
|
#[cfg(not(target_os = "macos"))]
|
|
22
25
|
{
|
|
23
26
|
#[cfg(target_os = "linux")]
|
|
24
27
|
if linux::matches_exactly(parent) {
|
|
25
|
-
return
|
|
26
|
-
Ok(_) => Ok(Some(name.to_string())),
|
|
27
|
-
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
|
|
28
|
-
Err(err) => Err(err),
|
|
29
|
-
};
|
|
28
|
+
return Ok(name.to_string());
|
|
30
29
|
}
|
|
31
|
-
|
|
30
|
+
lookup_by_listing(parent, name)
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/// The name a listing of `parent` holds for `name`: itself when listed,
|
|
36
|
-
/// else the entry sharing its inode.
|
|
35
|
+
/// else the entry sharing its inode. A spelling the listing lacks and no
|
|
36
|
+
/// inode ties to a listed name is refused as `EACCES`.
|
|
37
37
|
#[cfg(not(target_os = "macos"))]
|
|
38
|
-
fn
|
|
38
|
+
fn lookup_by_listing(parent: &Dir, name: &str) -> Result<String, FilesystemError> {
|
|
39
39
|
let found = match parent.symlink_metadata(name) {
|
|
40
40
|
Ok(found) => found,
|
|
41
|
-
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(
|
|
42
|
-
Err(err) => return Err(err),
|
|
41
|
+
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(name.to_string()),
|
|
42
|
+
Err(err) => return Err(to_error(err)),
|
|
43
43
|
};
|
|
44
44
|
let mut names = Vec::new();
|
|
45
|
-
for entry in parent.entries()? {
|
|
46
|
-
let entry = entry?;
|
|
45
|
+
for entry in parent.entries().map_err(to_error)? {
|
|
46
|
+
let entry = entry.map_err(to_error)?;
|
|
47
47
|
if entry.file_name() == name {
|
|
48
|
-
return Ok(
|
|
48
|
+
return Ok(name.to_string());
|
|
49
49
|
}
|
|
50
50
|
names.push(entry);
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
stored_by_inode(names, &found)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The name among `names` sharing the inode of `found`.
|
|
56
|
+
#[cfg(all(unix, not(target_os = "macos")))]
|
|
57
|
+
fn stored_by_inode(
|
|
58
|
+
names: Vec<cap_std::fs::DirEntry>,
|
|
59
|
+
found: &cap_std::fs::Metadata,
|
|
60
|
+
) -> Result<String, FilesystemError> {
|
|
61
|
+
use cap_std::fs::MetadataExt;
|
|
62
|
+
use std::os::unix::fs::DirEntryExt;
|
|
63
|
+
names
|
|
64
|
+
.into_iter()
|
|
65
|
+
.find(|entry| entry.ino() == found.ino())
|
|
66
|
+
.ok_or(FilesystemError::PermissionDenied)?
|
|
67
|
+
.file_name()
|
|
68
|
+
.into_string()
|
|
69
|
+
.map_err(|_| to_error(io::ErrorKind::InvalidData.into()))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Without an inode nothing ties a spelling to a listed name.
|
|
73
|
+
#[cfg(not(unix))]
|
|
74
|
+
fn stored_by_inode(
|
|
75
|
+
_names: Vec<cap_std::fs::DirEntry>,
|
|
76
|
+
_found: &cap_std::fs::Metadata,
|
|
77
|
+
) -> Result<String, FilesystemError> {
|
|
78
|
+
Err(FilesystemError::PermissionDenied)
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
#[cfg(target_os = "macos")]
|
|
@@ -85,7 +95,7 @@ mod macos {
|
|
|
85
95
|
data: [u8; 1024],
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
pub(super) fn
|
|
98
|
+
pub(super) fn lookup(parent: &Dir, name: &str) -> io::Result<String> {
|
|
89
99
|
let path = CString::new(name).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
|
|
90
100
|
let mut request = libc::attrlist {
|
|
91
101
|
bitmapcount: libc::ATTR_BIT_MAP_COUNT,
|
|
@@ -119,7 +129,7 @@ mod macos {
|
|
|
119
129
|
if answered != 0 {
|
|
120
130
|
let err = io::Error::last_os_error();
|
|
121
131
|
return match err.kind() {
|
|
122
|
-
io::ErrorKind::NotFound => Ok(
|
|
132
|
+
io::ErrorKind::NotFound => Ok(name.to_string()),
|
|
123
133
|
_ => Err(err),
|
|
124
134
|
};
|
|
125
135
|
}
|
|
@@ -129,20 +139,18 @@ mod macos {
|
|
|
129
139
|
.ok()
|
|
130
140
|
.and_then(|offset| offset.checked_sub(size_of::<libc::attrreference_t>()));
|
|
131
141
|
let len = (reply.name.attr_length as usize).checked_sub(1);
|
|
132
|
-
let
|
|
142
|
+
let stored_name = start
|
|
133
143
|
.zip(len)
|
|
134
144
|
.and_then(|(start, len)| reply.data.get(start..start.checked_add(len)?))
|
|
135
145
|
.ok_or(io::ErrorKind::InvalidData)?;
|
|
136
|
-
String::from_utf8(
|
|
137
|
-
.map(Some)
|
|
138
|
-
.map_err(|_| io::ErrorKind::InvalidData.into())
|
|
146
|
+
String::from_utf8(stored_name.to_vec()).map_err(|_| io::ErrorKind::InvalidData.into())
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
#[cfg(target_os = "linux")]
|
|
143
151
|
mod linux {
|
|
144
152
|
use cap_std::fs::Dir;
|
|
145
|
-
use rustix::fs::{fstatfs, ioctl_getflags};
|
|
153
|
+
use rustix::fs::{fstatfs, ioctl_getflags, openat, Mode, OFlags};
|
|
146
154
|
|
|
147
155
|
/// The 32-bit magic numbers of filesystems matching names byte for
|
|
148
156
|
/// byte unless a directory is marked to fold case: ext4, Btrfs, tmpfs,
|
|
@@ -154,17 +162,51 @@ mod linux {
|
|
|
154
162
|
/// Linux's `FS_CASEFOLD_FL`.
|
|
155
163
|
const CASEFOLD: u32 = 0x4000_0000;
|
|
156
164
|
|
|
165
|
+
/// cap-std holds `parent` for its path alone, which answers no flags,
|
|
166
|
+
/// so they are asked of the same directory opened to read.
|
|
157
167
|
pub(super) fn matches_exactly(parent: &Dir) -> bool {
|
|
158
168
|
let exact = fstatfs(parent).is_ok_and(|stats| EXACT.contains(&(stats.f_type as u32)));
|
|
159
|
-
exact
|
|
169
|
+
exact
|
|
170
|
+
&& openat(
|
|
171
|
+
parent,
|
|
172
|
+
".",
|
|
173
|
+
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
|
174
|
+
Mode::empty(),
|
|
175
|
+
)
|
|
176
|
+
.and_then(|readable| ioctl_getflags(&readable))
|
|
177
|
+
.is_ok_and(|flags| flags.bits() & CASEFOLD == 0)
|
|
160
178
|
}
|
|
161
179
|
|
|
162
180
|
#[cfg(test)]
|
|
163
181
|
mod tests {
|
|
164
182
|
use cap_std::ambient_authority;
|
|
165
183
|
|
|
166
|
-
use super::super::
|
|
167
|
-
use super::Dir;
|
|
184
|
+
use super::super::lookup_by_listing;
|
|
185
|
+
use super::{fstatfs, matches_exactly, Dir, EXACT};
|
|
186
|
+
|
|
187
|
+
// A Directory opens its directories as cap-std does, for a path
|
|
188
|
+
// alone, and a filesystem known to match exactly still spells
|
|
189
|
+
// without listing. A host on another filesystem has nothing here
|
|
190
|
+
// to witness.
|
|
191
|
+
// @behavior FS-081
|
|
192
|
+
#[test]
|
|
193
|
+
fn a_directory_opened_for_its_path_alone_is_known_to_match_exactly() {
|
|
194
|
+
let path = std::env::temp_dir().join(format!("kobako-exact-{}", std::process::id()));
|
|
195
|
+
let _ = std::fs::remove_dir_all(&path);
|
|
196
|
+
std::fs::create_dir_all(&path).expect("the host directory must be created");
|
|
197
|
+
let dir = Dir::open_ambient_dir(&path, ambient_authority())
|
|
198
|
+
.expect("the host directory must open");
|
|
199
|
+
let known = fstatfs(&dir).is_ok_and(|stats| EXACT.contains(&(stats.f_type as u32)));
|
|
200
|
+
let exact = matches_exactly(&dir);
|
|
201
|
+
let _ = std::fs::remove_dir_all(&path);
|
|
202
|
+
if known {
|
|
203
|
+
assert!(
|
|
204
|
+
exact,
|
|
205
|
+
"a directory cap-std opened on a filesystem known to match names exactly, \
|
|
206
|
+
asked through matches_exactly, must answer that it matches exactly"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
168
210
|
|
|
169
211
|
// A Linux host whose filesystem is not known to match exactly,
|
|
170
212
|
// XFS among them, spells by listing; the host here matches
|
|
@@ -179,15 +221,15 @@ mod linux {
|
|
|
179
221
|
let dir = Dir::open_ambient_dir(&path, ambient_authority())
|
|
180
222
|
.expect("the host directory must open");
|
|
181
223
|
let answers = (
|
|
182
|
-
|
|
183
|
-
|
|
224
|
+
lookup_by_listing(&dir, "file").ok(),
|
|
225
|
+
lookup_by_listing(&dir, "missing").ok(),
|
|
184
226
|
);
|
|
185
227
|
let _ = std::fs::remove_dir_all(&path);
|
|
186
228
|
assert_eq!(
|
|
187
229
|
answers,
|
|
188
|
-
(Some(
|
|
230
|
+
(Some("file".to_string()), Some("missing".to_string())),
|
|
189
231
|
"a name the host holds, spelled by listing its directory, must come back as \
|
|
190
|
-
given, and a name it lacks as
|
|
232
|
+
given, and a name it lacks as given too"
|
|
191
233
|
);
|
|
192
234
|
}
|
|
193
235
|
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
//! Opening a directory several names beneath one held open, in one call
|
|
2
|
+
//! where the host's kernel confines a path itself: Linux's `openat2` with
|
|
3
|
+
//! `RESOLVE_BENEATH` and `RESOLVE_NO_SYMLINKS`, and from macOS 26
|
|
4
|
+
//! `O_RESOLVE_BENEATH` with `O_NOFOLLOW_ANY`. Elsewhere each name opens in
|
|
5
|
+
//! turn without following a link, which answers the same.
|
|
6
|
+
|
|
7
|
+
use cap_std::fs::Dir;
|
|
8
|
+
|
|
9
|
+
use super::{open_dir_nofollow, FilesystemError};
|
|
10
|
+
|
|
11
|
+
/// The directory `names` reach beneath `dir`, a link on the way refused as
|
|
12
|
+
/// a loop, walked in one call as 9p's `Twalk` walks several names. A path past
|
|
13
|
+
/// what the host takes in one call is no name it refuses, so it goes name by
|
|
14
|
+
/// name, where only a name too long for the host is refused.
|
|
15
|
+
pub(super) fn walk(dir: &Dir, names: &[String]) -> Result<Dir, FilesystemError> {
|
|
16
|
+
match kernel::walk(dir, names) {
|
|
17
|
+
None | Some(Err(FilesystemError::InvalidFilename)) => by_name(dir, names),
|
|
18
|
+
Some(opened) => opened,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Each name opened in turn, the way every host can.
|
|
23
|
+
fn by_name(dir: &Dir, names: &[String]) -> Result<Dir, FilesystemError> {
|
|
24
|
+
let (first, rest) = names.split_first().ok_or(FilesystemError::InvalidInput)?;
|
|
25
|
+
rest.iter()
|
|
26
|
+
.try_fold(open_dir_nofollow(dir, first)?, |dir, name| {
|
|
27
|
+
open_dir_nofollow(&dir, name)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// What the host's errno names, a link on the way being a loop. `names` hold no
|
|
32
|
+
/// `..`, so a path leading out of `dir` is one a move carried out as it was
|
|
33
|
+
/// walked, as `complete_walk` answers EXDEV, and nothing stands there now.
|
|
34
|
+
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
35
|
+
fn to_error(errno: i32) -> FilesystemError {
|
|
36
|
+
const LOOP: i32 = rustix::io::Errno::LOOP.raw_os_error();
|
|
37
|
+
#[cfg(target_os = "linux")]
|
|
38
|
+
const LEADS_OUT: i32 = rustix::io::Errno::XDEV.raw_os_error();
|
|
39
|
+
#[cfg(target_os = "macos")]
|
|
40
|
+
const LEADS_OUT: i32 = libc::ENOTCAPABLE;
|
|
41
|
+
match errno {
|
|
42
|
+
LOOP => FilesystemError::FilesystemLoop,
|
|
43
|
+
LEADS_OUT => FilesystemError::NotFound,
|
|
44
|
+
_ => super::to_error(std::io::Error::from_raw_os_error(errno)),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[cfg(target_os = "linux")]
|
|
49
|
+
mod kernel {
|
|
50
|
+
use std::fs::File;
|
|
51
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
52
|
+
|
|
53
|
+
use cap_std::fs::Dir;
|
|
54
|
+
use rustix::fs::{openat2, Mode, OFlags, ResolveFlags};
|
|
55
|
+
use rustix::io::Errno;
|
|
56
|
+
|
|
57
|
+
use super::{to_error, FilesystemError};
|
|
58
|
+
|
|
59
|
+
/// Whether this kernel has `openat2`, which arrived in Linux 5.6.
|
|
60
|
+
static SUPPORTED: AtomicBool = AtomicBool::new(true);
|
|
61
|
+
|
|
62
|
+
/// A directory holds its path alone, as cap-std's own opens do, so one a
|
|
63
|
+
/// host lets a walk search but not read still opens. `names` hold no `..`,
|
|
64
|
+
/// the one thing `RESOLVE_BENEATH` asks a caller to retry.
|
|
65
|
+
pub(super) fn walk(dir: &Dir, names: &[String]) -> Option<Result<Dir, FilesystemError>> {
|
|
66
|
+
if !SUPPORTED.load(Ordering::Relaxed) {
|
|
67
|
+
return None;
|
|
68
|
+
}
|
|
69
|
+
match openat2(
|
|
70
|
+
dir,
|
|
71
|
+
names.join("/"),
|
|
72
|
+
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
|
73
|
+
Mode::empty(),
|
|
74
|
+
ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
|
|
75
|
+
) {
|
|
76
|
+
Ok(fd) => Some(Ok(Dir::from_std_file(File::from(fd)))),
|
|
77
|
+
Err(Errno::NOSYS) => {
|
|
78
|
+
SUPPORTED.store(false, Ordering::Relaxed);
|
|
79
|
+
None
|
|
80
|
+
}
|
|
81
|
+
Err(errno) => Some(Err(to_error(errno.raw_os_error()))),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[cfg(target_os = "macos")]
|
|
87
|
+
mod kernel {
|
|
88
|
+
use std::ffi::CString;
|
|
89
|
+
use std::fs::File;
|
|
90
|
+
use std::io;
|
|
91
|
+
use std::os::fd::{AsRawFd, FromRawFd};
|
|
92
|
+
use std::sync::OnceLock;
|
|
93
|
+
|
|
94
|
+
use cap_std::fs::Dir;
|
|
95
|
+
|
|
96
|
+
use super::{to_error, FilesystemError};
|
|
97
|
+
|
|
98
|
+
/// `O_RESOLVE_BENEATH` as `sys/fcntl.h` numbers it; the libc crate
|
|
99
|
+
/// names no such flag for macOS.
|
|
100
|
+
const O_RESOLVE_BENEATH: libc::c_int = 0x1000;
|
|
101
|
+
|
|
102
|
+
/// The first Darwin release of macOS 26, below which an older kernel
|
|
103
|
+
/// could ignore a flag it does not know and follow a link.
|
|
104
|
+
const DARWIN_OF_MACOS_26: u32 = 25;
|
|
105
|
+
|
|
106
|
+
fn is_supported() -> bool {
|
|
107
|
+
static SUPPORTED: OnceLock<bool> = OnceLock::new();
|
|
108
|
+
*SUPPORTED.get_or_init(|| {
|
|
109
|
+
rustix::system::uname()
|
|
110
|
+
.release()
|
|
111
|
+
.to_str()
|
|
112
|
+
.ok()
|
|
113
|
+
.and_then(|release| release.split('.').next()?.parse::<u32>().ok())
|
|
114
|
+
.is_some_and(|major| major >= DARWIN_OF_MACOS_26)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pub(super) fn walk(dir: &Dir, names: &[String]) -> Option<Result<Dir, FilesystemError>> {
|
|
119
|
+
if !is_supported() {
|
|
120
|
+
return None;
|
|
121
|
+
}
|
|
122
|
+
let Ok(path) = CString::new(names.join("/")) else {
|
|
123
|
+
return Some(Err(FilesystemError::InvalidInput));
|
|
124
|
+
};
|
|
125
|
+
// SAFETY: `path` is a NUL-terminated string that outlives the call,
|
|
126
|
+
// and `dir` holds its descriptor open across it.
|
|
127
|
+
let fd = unsafe {
|
|
128
|
+
libc::openat(
|
|
129
|
+
dir.as_raw_fd(),
|
|
130
|
+
path.as_ptr(),
|
|
131
|
+
libc::O_RDONLY
|
|
132
|
+
| libc::O_DIRECTORY
|
|
133
|
+
| libc::O_CLOEXEC
|
|
134
|
+
| libc::O_NOFOLLOW_ANY
|
|
135
|
+
| O_RESOLVE_BENEATH,
|
|
136
|
+
)
|
|
137
|
+
};
|
|
138
|
+
if fd < 0 {
|
|
139
|
+
let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
|
|
140
|
+
return Some(Err(to_error(errno)));
|
|
141
|
+
}
|
|
142
|
+
// SAFETY: `openat` just returned `fd`, which nothing else owns.
|
|
143
|
+
Some(Ok(Dir::from_std_file(unsafe { File::from_raw_fd(fd) })))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
148
|
+
mod kernel {
|
|
149
|
+
use cap_std::fs::Dir;
|
|
150
|
+
|
|
151
|
+
use super::FilesystemError;
|
|
152
|
+
|
|
153
|
+
pub(super) fn walk(_dir: &Dir, _names: &[String]) -> Option<Result<Dir, FilesystemError>> {
|
|
154
|
+
None
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[cfg(test)]
|
|
159
|
+
mod tests {
|
|
160
|
+
use std::fs;
|
|
161
|
+
use std::path::PathBuf;
|
|
162
|
+
|
|
163
|
+
use cap_std::ambient_authority;
|
|
164
|
+
|
|
165
|
+
use super::*;
|
|
166
|
+
|
|
167
|
+
/// A host tree of the test's own: `a/b/c`, a file `a/file`, and a link
|
|
168
|
+
/// `a/link` to `b`, removed when it drops.
|
|
169
|
+
struct Tree(PathBuf);
|
|
170
|
+
|
|
171
|
+
impl Tree {
|
|
172
|
+
fn new(name: &str) -> Self {
|
|
173
|
+
let path =
|
|
174
|
+
std::env::temp_dir().join(format!("kobako-beneath-{name}-{}", std::process::id()));
|
|
175
|
+
let _ = fs::remove_dir_all(&path);
|
|
176
|
+
fs::create_dir_all(path.join("a/b/c")).expect("the tree must be made");
|
|
177
|
+
fs::write(path.join("a/file"), "x").expect("the file must be made");
|
|
178
|
+
#[cfg(unix)]
|
|
179
|
+
std::os::unix::fs::symlink("b", path.join("a/link")).expect("the link must be made");
|
|
180
|
+
Self(path)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn dir(&self) -> Dir {
|
|
184
|
+
Dir::open_ambient_dir(&self.0, ambient_authority()).expect("the tree must open")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
impl Drop for Tree {
|
|
189
|
+
fn drop(&mut self) {
|
|
190
|
+
let _ = fs::remove_dir_all(&self.0);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn names(path: &str) -> Vec<String> {
|
|
195
|
+
path.split('/').map(str::to_string).collect()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// What `open` answers for `path`, only whether it opened kept.
|
|
199
|
+
fn answer(
|
|
200
|
+
open: impl Fn(&Dir, &[String]) -> Result<Dir, FilesystemError>,
|
|
201
|
+
dir: &Dir,
|
|
202
|
+
path: &str,
|
|
203
|
+
) -> Result<(), FilesystemError> {
|
|
204
|
+
open(dir, &names(path)).map(drop)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The host's kernel path and the one-name-at-a-time path must answer
|
|
208
|
+
// alike, since which one runs depends on the host.
|
|
209
|
+
#[test]
|
|
210
|
+
fn the_kernel_and_each_name_answer_alike() {
|
|
211
|
+
let tree = Tree::new("alike");
|
|
212
|
+
let dir = tree.dir();
|
|
213
|
+
let mut paths = vec!["a/b/c", "a/missing/c", "a/file/c", "a/file"];
|
|
214
|
+
if cfg!(unix) {
|
|
215
|
+
paths.extend(["a/link/c", "a/link"]);
|
|
216
|
+
}
|
|
217
|
+
for path in paths {
|
|
218
|
+
assert_eq!(
|
|
219
|
+
answer(walk, &dir, path),
|
|
220
|
+
answer(by_name, &dir, path),
|
|
221
|
+
"{path} opened beneath a directory by the kernel must answer as each name opened \
|
|
222
|
+
in turn answers"
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn a_link_on_the_way_is_a_loop() {
|
|
229
|
+
if !cfg!(unix) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
let tree = Tree::new("link");
|
|
233
|
+
assert_eq!(
|
|
234
|
+
answer(walk, &tree.dir(), "a/link/c"),
|
|
235
|
+
Err(FilesystemError::FilesystemLoop),
|
|
236
|
+
"a/link/c opened beneath a directory, through a link, must be refused as a loop"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn a_path_reaches_its_directory() {
|
|
242
|
+
let tree = Tree::new("reach");
|
|
243
|
+
let opened = walk(&tree.dir(), &names("a/b")).expect("a/b must open");
|
|
244
|
+
assert!(
|
|
245
|
+
opened.open_dir("c").is_ok(),
|
|
246
|
+
"a/b opened beneath a directory must be the directory holding c"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|