kobako 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +377 -0
- data/Cargo.lock +181 -154
- data/README.md +99 -459
- data/ROADMAP.md +7 -1
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +354 -0
- data/crates/kobako-runtime/Cargo.toml +38 -7
- data/crates/kobako-runtime/README.md +17 -8
- data/crates/kobako-runtime/src/dispatch.rs +9 -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/bind/lookup.rs +236 -0
- 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 +301 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
- 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 +26 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +16 -1
- 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 +105 -0
- data/crates/kobako-runtime/src/profile.rs +140 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- 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/src/yielder.rs +11 -6
- 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 +23 -0
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/abi.rs +1 -1
- 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 +4 -3
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
- data/crates/kobako-transport/src/envelope/reply.rs +34 -16
- data/crates/kobako-transport/src/envelope/request.rs +19 -9
- 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 +265 -0
- data/crates/kobako-wasmtime/Cargo.toml +25 -19
- data/crates/kobako-wasmtime/README.md +13 -7
- data/crates/kobako-wasmtime/src/abi.rs +75 -49
- data/crates/kobako-wasmtime/src/ambient.rs +64 -6
- data/crates/kobako-wasmtime/src/cache.rs +188 -77
- data/crates/kobako-wasmtime/src/config.rs +77 -5
- data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
- data/crates/kobako-wasmtime/src/driver.rs +70 -135
- 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 +52 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
- data/crates/kobako-wasmtime/src/invocation.rs +43 -74
- data/crates/kobako-wasmtime/src/lib.rs +5 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
- 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 +512 -0
- data/crates/kobako-wasmtime/src/trap.rs +45 -25
- data/crates/kobako-wasmtime/src/wasi.rs +333 -0
- data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
- data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -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 +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +67 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
- data/ext/kobako/src/runtime/filesystem.rs +208 -0
- data/ext/kobako/src/runtime/gvl.rs +49 -8
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +125 -199
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +19 -0
- data/lib/kobako/catalog/services.rb +13 -2
- data/lib/kobako/catalog/snippets.rb +11 -15
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +10 -9
- data/lib/kobako/codec/ext_types.rb +4 -6
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +40 -13
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +69 -0
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +6 -6
- data/lib/kobako/sandbox.rb +55 -27
- data/lib/kobako/sandbox_options.rb +39 -13
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +33 -29
- data/lib/kobako/transport/exposure.rb +7 -9
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +9 -31
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +35 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +24 -0
- data/sig/kobako/catalog/services.rbs +5 -1
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/catalog.rbs +1 -0
- 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/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +3 -2
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +93 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +3 -3
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +25 -3
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +4 -4
- data/sig/kobako/transport/exposure.rbs +2 -2
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +3 -3
- metadata +116 -15
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
- data/crates/kobako-wasmtime/src/io.rs +0 -286
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
//! The security modules an Invocation registers: kobako's own rules, and the
|
|
2
|
+
//! Host App's Policy for each Mount that has one. Linux has neither, so they
|
|
3
|
+
//! answer through the hooks it leaves for such modules, and the VFS never
|
|
4
|
+
//! learns what they decide.
|
|
5
|
+
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
|
|
8
|
+
use crate::mount::{Access, Mount, Policy};
|
|
9
|
+
use crate::vfs::{
|
|
10
|
+
d_revalidate, Cred, Dentry, FMode, FileSecurity, FileType, FilesystemError, OpenHow, Path,
|
|
11
|
+
SecurityOperations, SecurityPath, VfsMount, VmCommitted, PATH_MAX,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/// What a Mount's Policy allowed a file as it opened, kept on the open file, as
|
|
15
|
+
/// Landlock's file blob keeps `allowed_access`.
|
|
16
|
+
struct AllowedAccess(Access);
|
|
17
|
+
|
|
18
|
+
/// Whether each name from the root down to `path` still names what the runtime
|
|
19
|
+
/// recorded, as gVisor's `revalidatePath` walks one. Linux asks only names a walk
|
|
20
|
+
/// passes through, so a `WalkGuard` asks this of where a call starts. A name moved
|
|
21
|
+
/// while it was asked answers EAGAIN, as a scoped walk's `..` answers a move it
|
|
22
|
+
/// met, and the caller makes the call again.
|
|
23
|
+
fn revalidate_path(path: &Path) -> Result<bool, FilesystemError> {
|
|
24
|
+
let mut chain = vec![path.dentry().clone()];
|
|
25
|
+
while let Some(at) = chain.last().filter(|at| !at.is_root()) {
|
|
26
|
+
let above = at.parent();
|
|
27
|
+
chain.push(above);
|
|
28
|
+
}
|
|
29
|
+
for pair in chain.windows(2).rev() {
|
|
30
|
+
let [name, above] = pair else { continue };
|
|
31
|
+
let _held = above.inode_lock_shared()?;
|
|
32
|
+
if !Arc::ptr_eq(&name.parent(), above) {
|
|
33
|
+
return Err(FilesystemError::WouldBlock);
|
|
34
|
+
}
|
|
35
|
+
if !d_revalidate(path.mnt().sb(), above, name)? {
|
|
36
|
+
return Ok(false);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
Ok(true)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// What every Invocation holds to, whatever its Mounts allow.
|
|
43
|
+
struct KobakoSecurity;
|
|
44
|
+
|
|
45
|
+
impl SecurityOperations for KobakoSecurity {
|
|
46
|
+
/// Only a file or a directory opens: the runtime provides no peer for a
|
|
47
|
+
/// FIFO or a socket, and a device would act on being opened.
|
|
48
|
+
fn file_open(
|
|
49
|
+
&self,
|
|
50
|
+
_path: SecurityPath<'_>,
|
|
51
|
+
kind: FileType,
|
|
52
|
+
_mode: FMode,
|
|
53
|
+
_how: &OpenHow,
|
|
54
|
+
) -> Result<FileSecurity, FilesystemError> {
|
|
55
|
+
match kind {
|
|
56
|
+
FileType::RegFile | FileType::Dir => Ok(None),
|
|
57
|
+
_ => Err(FilesystemError::PermissionDenied),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// A guest never reads where a link points.
|
|
62
|
+
fn inode_readlink(&self, _dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
63
|
+
Err(FilesystemError::Unsupported)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// A Mount's Policy, answering only for that Mount.
|
|
68
|
+
struct MountPolicy {
|
|
69
|
+
mnt: Arc<VfsMount>,
|
|
70
|
+
at: String,
|
|
71
|
+
policy: Arc<dyn Policy>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl MountPolicy {
|
|
75
|
+
fn guards(&self, mnt: &Arc<VfsMount>) -> bool {
|
|
76
|
+
Arc::ptr_eq(&self.mnt, mnt)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// The absolute guest path `path`, from the Mount's root, stands for.
|
|
80
|
+
fn guest_path(&self, path: &str) -> String {
|
|
81
|
+
match (self.at.as_str(), path) {
|
|
82
|
+
(at, "/") => at.to_string(),
|
|
83
|
+
("/", path) => path.to_string(),
|
|
84
|
+
(at, path) => format!("{at}{path}"),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Judge `path` as AppArmor's `aa_path_perm` judges a whole path once, each
|
|
89
|
+
/// hook below reaching it as AppArmor's do. The Policy judges the path as the
|
|
90
|
+
/// Filesystem stores its names, so a host folding case or Unicode form cannot
|
|
91
|
+
/// hand it one spelling of an entry it refuses by another. One a Policy could
|
|
92
|
+
/// not be handed within `PATH_MAX` is refused as too long, as AppArmor's
|
|
93
|
+
/// `aa_path_name` refuses one past its buffer.
|
|
94
|
+
fn path_perm(&self, path: SecurityPath<'_>, access: Access) -> Result<(), FilesystemError> {
|
|
95
|
+
if !self.guards(path.mnt) {
|
|
96
|
+
return Ok(());
|
|
97
|
+
}
|
|
98
|
+
let path = self.guest_path(&(path.name)());
|
|
99
|
+
if path.len() >= PATH_MAX {
|
|
100
|
+
return Err(FilesystemError::InvalidFilename);
|
|
101
|
+
}
|
|
102
|
+
match self.policy.allows(&path, access) {
|
|
103
|
+
true => Ok(()),
|
|
104
|
+
false => Err(FilesystemError::PermissionDenied),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl SecurityOperations for MountPolicy {
|
|
110
|
+
fn path_mknod(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
|
|
111
|
+
self.path_perm(path, Access::ReadWrite)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn path_mkdir(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
|
|
115
|
+
self.path_perm(path, Access::ReadWrite)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn path_rmdir(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
|
|
119
|
+
self.path_perm(path, Access::ReadWrite)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn path_unlink(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
|
|
123
|
+
self.path_perm(path, Access::ReadWrite)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn path_symlink(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
|
|
127
|
+
self.path_perm(path, Access::ReadWrite)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn path_link(
|
|
131
|
+
&self,
|
|
132
|
+
old: SecurityPath<'_>,
|
|
133
|
+
new: SecurityPath<'_>,
|
|
134
|
+
) -> Result<(), FilesystemError> {
|
|
135
|
+
self.path_perm(old, Access::ReadWrite)?;
|
|
136
|
+
self.path_perm(new, Access::ReadWrite)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Asked once for each open, for what the open may change, and kept on the
|
|
140
|
+
/// file, as Landlock keeps the rights it allowed. A file the open made was
|
|
141
|
+
/// judged as a change already, through `path_mknod`.
|
|
142
|
+
fn file_open(
|
|
143
|
+
&self,
|
|
144
|
+
path: SecurityPath<'_>,
|
|
145
|
+
_kind: FileType,
|
|
146
|
+
mode: FMode,
|
|
147
|
+
how: &OpenHow,
|
|
148
|
+
) -> Result<FileSecurity, FilesystemError> {
|
|
149
|
+
if !self.guards(path.mnt) {
|
|
150
|
+
return Ok(None);
|
|
151
|
+
}
|
|
152
|
+
let created = mode.contains(FMode::CREATED);
|
|
153
|
+
let access = match created || mode.contains(FMode::WRITE) || how.truncate {
|
|
154
|
+
true => Access::ReadWrite,
|
|
155
|
+
false => Access::Read,
|
|
156
|
+
};
|
|
157
|
+
if !created {
|
|
158
|
+
self.path_perm(path, access)?;
|
|
159
|
+
}
|
|
160
|
+
Ok(Some(Arc::new(AllowedAccess(access))))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// A truncate follows what the Policy allowed as the file opened, wherever
|
|
164
|
+
/// the file stands now, as Landlock's `hook_file_truncate` reads the file's
|
|
165
|
+
/// rights.
|
|
166
|
+
fn file_truncate(&self, kept: &FileSecurity) -> Result<(), FilesystemError> {
|
|
167
|
+
match kept
|
|
168
|
+
.as_deref()
|
|
169
|
+
.and_then(|kept| kept.downcast_ref::<AllowedAccess>())
|
|
170
|
+
{
|
|
171
|
+
Some(AllowedAccess(Access::Read)) => Err(FilesystemError::PermissionDenied),
|
|
172
|
+
_ => Ok(()),
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn path_utimes(&self, path: SecurityPath<'_>) -> Result<(), FilesystemError> {
|
|
177
|
+
self.path_perm(path, Access::ReadWrite)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Both paths are judged. Everything beneath a directory would take paths the
|
|
181
|
+
/// Policy never judged, and a Policy answers for one path, not a tree, so its
|
|
182
|
+
/// move is refused as one across devices and the guest copies entry by entry
|
|
183
|
+
/// instead, as Landlock answers a move it cannot show keeps each right beneath.
|
|
184
|
+
/// A move onto its own name takes no new path.
|
|
185
|
+
fn path_rename(
|
|
186
|
+
&self,
|
|
187
|
+
old: SecurityPath<'_>,
|
|
188
|
+
new: SecurityPath<'_>,
|
|
189
|
+
(old_dentry, new_dentry): (&Arc<Dentry>, &Arc<Dentry>),
|
|
190
|
+
) -> Result<(), FilesystemError> {
|
|
191
|
+
self.path_perm(old, Access::ReadWrite)?;
|
|
192
|
+
self.path_perm(new, Access::ReadWrite)?;
|
|
193
|
+
let moves_dir =
|
|
194
|
+
old_dentry.kind() == Ok(FileType::Dir) && !Arc::ptr_eq(old_dentry, new_dentry);
|
|
195
|
+
match moves_dir && self.guards(old.mnt) {
|
|
196
|
+
true => Err(FilesystemError::CrossesDevices),
|
|
197
|
+
false => Ok(()),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// What keeps a Policy judging the path a descriptor stands at, kobako's own
|
|
203
|
+
/// guarantee: Linux asks nothing of the dentry a walk starts from, so nothing in
|
|
204
|
+
/// its VFS can ask this, and a Wasm Driver asks it wherever a descriptor becomes where
|
|
205
|
+
/// a call starts.
|
|
206
|
+
pub struct WalkGuard {
|
|
207
|
+
/// The mounts a Policy judges; no other holds anything to keep true.
|
|
208
|
+
guarded: Vec<Arc<VfsMount>>,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
impl WalkGuard {
|
|
212
|
+
/// What keeps each Policy over `mounts` judging the path it means to.
|
|
213
|
+
pub fn new<'a>(mounts: impl IntoIterator<Item = &'a Mount>) -> Self {
|
|
214
|
+
let guarded = mounts
|
|
215
|
+
.into_iter()
|
|
216
|
+
.filter(|mount| mount.policy().is_some())
|
|
217
|
+
.map(|mount| mount.mnt().clone())
|
|
218
|
+
.collect();
|
|
219
|
+
Self { guarded }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Refuse a call through a descriptor unless its dentry still names what the
|
|
223
|
+
/// runtime opened there, so the Policy judges the path it stands at. A dentry
|
|
224
|
+
/// dropped or removed is refused as Linux's AppArmor refuses an unlinked one,
|
|
225
|
+
/// and one whose identity the runtime never learned cannot be told from another,
|
|
226
|
+
/// so it is refused too. This keeps a host or another Filesystem instance moving
|
|
227
|
+
/// a directory behind the runtime's back from having a Policy judge a path it no
|
|
228
|
+
/// longer stands at.
|
|
229
|
+
pub fn check(&self, path: &Path) -> Result<(), FilesystemError> {
|
|
230
|
+
if !self.guarded.iter().any(|mnt| Arc::ptr_eq(mnt, path.mnt())) {
|
|
231
|
+
return Ok(());
|
|
232
|
+
}
|
|
233
|
+
let dentry = path.dentry();
|
|
234
|
+
if dentry.is_root() {
|
|
235
|
+
return Ok(());
|
|
236
|
+
}
|
|
237
|
+
let told_apart = dentry
|
|
238
|
+
.inode()
|
|
239
|
+
.is_some_and(|inode| inode.identity().is_some());
|
|
240
|
+
if path.mnt().sb().dentries().unlinked(dentry) || !told_apart || !revalidate_path(path)? {
|
|
241
|
+
return Err(FilesystemError::NotFound);
|
|
242
|
+
}
|
|
243
|
+
Ok(())
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// The Cred an Invocation opens its Mounts' roots with, its Filesystems holding at
|
|
248
|
+
/// most `memory_limit` bytes on its behalf, or without bound for `None`: kobako's
|
|
249
|
+
/// own rules, then each of `modules` in order, before any Mount's Policy applies,
|
|
250
|
+
/// as a Linux task opens the files it keeps before `landlock_restrict_self`.
|
|
251
|
+
/// The modules are those the Wasm Driver was built with, as Linux stacks the LSMs
|
|
252
|
+
/// `lsm=` names at boot after the capability module.
|
|
253
|
+
pub fn prepare_creds_within(
|
|
254
|
+
modules: &[Arc<dyn SecurityOperations>],
|
|
255
|
+
memory_limit: Option<usize>,
|
|
256
|
+
) -> Arc<Cred> {
|
|
257
|
+
let mut security: Vec<Arc<dyn SecurityOperations>> = vec![Arc::new(KobakoSecurity)];
|
|
258
|
+
security.extend(modules.iter().cloned());
|
|
259
|
+
Cred::within(security, memory_limit.map(VmCommitted::new))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// `cred` with each Mount's Policy added, the modules it held and the memory
|
|
263
|
+
/// held on its behalf shared, so what either Cred charges counts against one
|
|
264
|
+
/// limit, as `landlock_restrict_self` adds a domain to the task's Cred.
|
|
265
|
+
pub fn restrict_self<'a>(cred: &Cred, mounts: impl IntoIterator<Item = &'a Mount>) -> Arc<Cred> {
|
|
266
|
+
cred.with_modules(policies_of(mounts))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn policies_of<'a>(
|
|
270
|
+
mounts: impl IntoIterator<Item = &'a Mount>,
|
|
271
|
+
) -> Vec<Arc<dyn SecurityOperations>> {
|
|
272
|
+
mounts
|
|
273
|
+
.into_iter()
|
|
274
|
+
.filter_map(|mount| {
|
|
275
|
+
let policy = mount.policy()?;
|
|
276
|
+
Some(Arc::new(MountPolicy {
|
|
277
|
+
mnt: mount.mnt().clone(),
|
|
278
|
+
at: mount.at().to_string(),
|
|
279
|
+
policy: policy.clone(),
|
|
280
|
+
}) as Arc<dyn SecurityOperations>)
|
|
281
|
+
})
|
|
282
|
+
.collect()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[cfg(test)]
|
|
286
|
+
mod tests {
|
|
287
|
+
use super::*;
|
|
288
|
+
use crate::filesystem::wrap::{Hook, Wrap};
|
|
289
|
+
use crate::filesystem::{DentryOperations, Filesystem, InodeOperations, Tmpfs};
|
|
290
|
+
|
|
291
|
+
/// A Policy allowing everything, counting how often it is asked.
|
|
292
|
+
#[derive(Default)]
|
|
293
|
+
struct Counts(std::sync::atomic::AtomicUsize);
|
|
294
|
+
|
|
295
|
+
impl Policy for Counts {
|
|
296
|
+
fn allows(&self, _path: &str, _access: Access) -> bool {
|
|
297
|
+
self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
298
|
+
true
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// A Policy refusing every change beneath `/guarded/q`.
|
|
303
|
+
struct RefusesChangesInQ;
|
|
304
|
+
|
|
305
|
+
impl Policy for RefusesChangesInQ {
|
|
306
|
+
fn allows(&self, path: &str, access: Access) -> bool {
|
|
307
|
+
access == Access::Read || !path.starts_with("/guarded/q/")
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The Policy refuses a change where the file stands by the truncate, so only
|
|
312
|
+
// a decision kept from the open lets it through, as Landlock keeps the
|
|
313
|
+
// truncate right it allowed on the file.
|
|
314
|
+
// @behavior RT-272
|
|
315
|
+
#[test]
|
|
316
|
+
fn a_truncate_follows_what_the_policy_allowed_as_the_file_opened() {
|
|
317
|
+
let filesystem = Filesystem::new(Arc::new(
|
|
318
|
+
[("p/f", "x"), ("q/keep", "x")]
|
|
319
|
+
.into_iter()
|
|
320
|
+
.collect::<Tmpfs>(),
|
|
321
|
+
))
|
|
322
|
+
.expect("the instance must stand");
|
|
323
|
+
let guarded = Mount::new("/guarded", filesystem.clone(), Access::ReadWrite)
|
|
324
|
+
.expect("/guarded is a guest path")
|
|
325
|
+
.with_policy(Arc::new(RefusesChangesInQ));
|
|
326
|
+
let plain =
|
|
327
|
+
Mount::new("/plain", filesystem, Access::ReadWrite).expect("/plain is a guest path");
|
|
328
|
+
let cred = restrict_self(&prepare_creds_within(&[], None), [&guarded]);
|
|
329
|
+
let write = OpenHow {
|
|
330
|
+
write: true,
|
|
331
|
+
..Default::default()
|
|
332
|
+
};
|
|
333
|
+
let mut held =
|
|
334
|
+
crate::vfs::kern_path::filp_open(&cred, &Path::root(guarded.mnt()), "p/f", &write)
|
|
335
|
+
.unwrap_or_else(|_| panic!("the file must open"));
|
|
336
|
+
let root = Path::root(plain.mnt());
|
|
337
|
+
crate::vfs::filename_renameat2(
|
|
338
|
+
&restrict_self(&prepare_creds_within(&[], None), []),
|
|
339
|
+
&root,
|
|
340
|
+
"p/f",
|
|
341
|
+
&root,
|
|
342
|
+
"q/f",
|
|
343
|
+
)
|
|
344
|
+
.expect("the move must land");
|
|
345
|
+
assert_eq!(
|
|
346
|
+
held.truncate(0),
|
|
347
|
+
Ok(()),
|
|
348
|
+
"File::truncate of a file opened to write beneath a Policy, moved since to where the \
|
|
349
|
+
Policy refuses changes, must follow what the Policy allowed as it opened"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// A Policy refusing nothing still counts each ask, so only an open asking
|
|
354
|
+
// no module leaves it at zero.
|
|
355
|
+
// @behavior RT-302
|
|
356
|
+
#[test]
|
|
357
|
+
fn an_open_standing_for_a_path_asks_no_policy() {
|
|
358
|
+
let policy = Arc::new(Counts::default());
|
|
359
|
+
let filesystem = Filesystem::new(Arc::new([("f", "x")].into_iter().collect::<Tmpfs>()))
|
|
360
|
+
.expect("the instance must stand");
|
|
361
|
+
let mount = Mount::new("/data", filesystem, Access::ReadWrite)
|
|
362
|
+
.expect("/data is a guest path")
|
|
363
|
+
.with_policy(policy.clone());
|
|
364
|
+
let cred = restrict_self(&prepare_creds_within(&[], None), [&mount]);
|
|
365
|
+
let opened = crate::vfs::kern_path::filp_open(
|
|
366
|
+
&cred,
|
|
367
|
+
&Path::root(mount.mnt()),
|
|
368
|
+
"f",
|
|
369
|
+
&OpenHow::default(),
|
|
370
|
+
);
|
|
371
|
+
assert_eq!(
|
|
372
|
+
(
|
|
373
|
+
opened.map(|file| file.is_path()),
|
|
374
|
+
policy.0.load(std::sync::atomic::Ordering::SeqCst)
|
|
375
|
+
),
|
|
376
|
+
(Ok(true), 0),
|
|
377
|
+
"vfs::do_file_open of a file asking neither to read nor to write, beneath a Policy, \
|
|
378
|
+
must stand for the file without asking the Policy, as Linux's do_o_path asks no \
|
|
379
|
+
security module"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// What making an entry answers through a directory moved past
|
|
384
|
+
/// `PATH_MAX`, beneath a mount `prepare` finishes.
|
|
385
|
+
fn made_past_the_limit(prepare: impl FnOnce(Mount) -> Mount) -> Result<(), FilesystemError> {
|
|
386
|
+
let (deep, seed) = (vec!["a"; 2000].join("/"), "x");
|
|
387
|
+
let tmpfs: Tmpfs = [
|
|
388
|
+
(format!("c0/{deep}/file"), seed),
|
|
389
|
+
(format!("c1/{deep}/file"), seed),
|
|
390
|
+
]
|
|
391
|
+
.into_iter()
|
|
392
|
+
.collect();
|
|
393
|
+
let mount = Mount::new(
|
|
394
|
+
"/data",
|
|
395
|
+
Filesystem::new(Arc::new(tmpfs)).expect("the instance must stand"),
|
|
396
|
+
Access::ReadWrite,
|
|
397
|
+
)
|
|
398
|
+
.expect("/data is a guest path");
|
|
399
|
+
let mount = prepare(mount);
|
|
400
|
+
let cred = restrict_self(&prepare_creds_within(&[], None), [&mount]);
|
|
401
|
+
let root = Path::root(mount.mnt());
|
|
402
|
+
let how = crate::vfs::OpenHow {
|
|
403
|
+
read: true,
|
|
404
|
+
directory: true,
|
|
405
|
+
..Default::default()
|
|
406
|
+
};
|
|
407
|
+
let held = crate::vfs::kern_path::filp_open(&cred, &root, &format!("c0/{deep}"), &how)
|
|
408
|
+
.unwrap_or_else(|_| panic!("the directory must open"));
|
|
409
|
+
crate::vfs::filename_renameat2(
|
|
410
|
+
&Arc::default(),
|
|
411
|
+
&root,
|
|
412
|
+
"c0",
|
|
413
|
+
&root,
|
|
414
|
+
&format!("c1/{deep}/z"),
|
|
415
|
+
)
|
|
416
|
+
.expect("the move must land");
|
|
417
|
+
crate::vfs::filename_mkdirat(&cred, held.path(), "made")
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Linux's walk sets no limit on where a directory stands, so a walk
|
|
421
|
+
// without a Policy has no reason to.
|
|
422
|
+
// @behavior RT-203
|
|
423
|
+
#[test]
|
|
424
|
+
fn a_path_past_the_limit_is_made_where_no_policy_needs_it() {
|
|
425
|
+
assert_eq!(
|
|
426
|
+
made_past_the_limit(|mount| mount),
|
|
427
|
+
Ok(()),
|
|
428
|
+
"vfs::filename_mkdirat through a directory moved past PATH_MAX, beneath a Mount with \
|
|
429
|
+
no Policy, must make the entry"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// @behavior RT-204
|
|
434
|
+
#[test]
|
|
435
|
+
fn a_path_a_policy_cannot_be_handed_is_too_long() {
|
|
436
|
+
let policy = Arc::new(Counts::default());
|
|
437
|
+
let asked = policy.clone();
|
|
438
|
+
let made = made_past_the_limit(move |mount| mount.with_policy(policy));
|
|
439
|
+
let asked_past_the_limit = asked.0.load(std::sync::atomic::Ordering::SeqCst);
|
|
440
|
+
assert_eq!(
|
|
441
|
+
(made, asked_past_the_limit),
|
|
442
|
+
(Err(FilesystemError::InvalidFilename), 1),
|
|
443
|
+
"vfs::filename_mkdirat through a directory moved past PATH_MAX, beneath a Policy, \
|
|
444
|
+
must answer ENAMETOOLONG, the Policy asked only of the open before the move"
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/// Holds the first ask of `revalidate` about `d` once it reached the
|
|
449
|
+
/// Filesystem, so a move of `d` racing it either lands first or waits for
|
|
450
|
+
/// the answer.
|
|
451
|
+
struct PausesRevalidate(std::sync::Mutex<Option<std::sync::mpsc::Sender<()>>>);
|
|
452
|
+
|
|
453
|
+
impl Hook for PausesRevalidate {
|
|
454
|
+
fn revalidate(
|
|
455
|
+
&self,
|
|
456
|
+
ops: &dyn DentryOperations,
|
|
457
|
+
dir: &dyn InodeOperations,
|
|
458
|
+
dentry: &Dentry,
|
|
459
|
+
) -> Result<bool, FilesystemError> {
|
|
460
|
+
let reached = match dentry.name() == "d" {
|
|
461
|
+
true => self.0.lock().expect("the sender").take(),
|
|
462
|
+
false => None,
|
|
463
|
+
};
|
|
464
|
+
if let Some(reached) = reached {
|
|
465
|
+
reached.send(()).expect("the test waits for the ask");
|
|
466
|
+
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
467
|
+
}
|
|
468
|
+
ops.revalidate(dir, dentry)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/// One Filesystem with a Mount under an allow-all Policy holding `p/d` open,
|
|
473
|
+
/// and a Mount with none to move it through.
|
|
474
|
+
fn held_beneath_a_policy(
|
|
475
|
+
filesystem: Arc<Filesystem>,
|
|
476
|
+
) -> (Arc<Cred>, crate::vfs::File, Mount, WalkGuard) {
|
|
477
|
+
let guarded = Mount::new("/guarded", filesystem.clone(), Access::ReadWrite)
|
|
478
|
+
.expect("/guarded is a guest path")
|
|
479
|
+
.with_policy(Arc::new(Counts::default()));
|
|
480
|
+
let plain =
|
|
481
|
+
Mount::new("/plain", filesystem, Access::ReadWrite).expect("/plain is a guest path");
|
|
482
|
+
let cred = restrict_self(&prepare_creds_within(&[], None), [&guarded]);
|
|
483
|
+
let how = crate::vfs::OpenHow {
|
|
484
|
+
read: true,
|
|
485
|
+
directory: true,
|
|
486
|
+
..Default::default()
|
|
487
|
+
};
|
|
488
|
+
let held = crate::vfs::kern_path::filp_open(&cred, &Path::root(guarded.mnt()), "p/d", &how)
|
|
489
|
+
.unwrap_or_else(|_| panic!("the directory must open"));
|
|
490
|
+
(cred, held, plain, WalkGuard::new([&guarded]))
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// The ask of `revalidate` and a move through the other Mount meet only if
|
|
494
|
+
// the walk holds no lock: then the Filesystem has moved the entry when it is
|
|
495
|
+
// asked, the runtime lets go of a dentry still in use, and the move finds
|
|
496
|
+
// nothing to carry along.
|
|
497
|
+
// @behavior RT-234
|
|
498
|
+
#[test]
|
|
499
|
+
fn a_directory_moved_while_a_call_starts_from_it_stays_reachable_beneath_a_policy() {
|
|
500
|
+
let tmpfs: Tmpfs = [("p/d/f", "x"), ("q/keep", "x")].into_iter().collect();
|
|
501
|
+
let (reached, asked) = std::sync::mpsc::channel();
|
|
502
|
+
let hook = Arc::new(PausesRevalidate(std::sync::Mutex::new(None)));
|
|
503
|
+
let filesystem = Filesystem::new(Arc::new(Wrap::new(Arc::new(tmpfs), hook.clone())))
|
|
504
|
+
.expect("the instance must stand");
|
|
505
|
+
let (cred, held, plain, guard) = held_beneath_a_policy(filesystem);
|
|
506
|
+
*hook.0.lock().expect("the sender") = Some(reached);
|
|
507
|
+
let start = held.path().clone();
|
|
508
|
+
let first = std::thread::scope(|scope| {
|
|
509
|
+
let first = scope.spawn(|| guard.check(&start));
|
|
510
|
+
asked
|
|
511
|
+
.recv_timeout(crate::sched::testing::MAX_WAIT)
|
|
512
|
+
.expect("the call must ask revalidate");
|
|
513
|
+
let root = Path::root(plain.mnt());
|
|
514
|
+
crate::vfs::filename_renameat2(
|
|
515
|
+
&restrict_self(&prepare_creds_within(&[], None), []),
|
|
516
|
+
&root,
|
|
517
|
+
"p/d",
|
|
518
|
+
&root,
|
|
519
|
+
"q/d",
|
|
520
|
+
)
|
|
521
|
+
.expect("the move must land");
|
|
522
|
+
crate::sched::testing::join_scoped("the call", first)
|
|
523
|
+
});
|
|
524
|
+
assert!(
|
|
525
|
+
first.is_ok()
|
|
526
|
+
&& guard.check(held.path()).is_ok()
|
|
527
|
+
&& crate::vfs::vfs_fstatat(&cred, held.path(), "f", false).is_ok()
|
|
528
|
+
&& held.path().dentry().path_raw() == "/q/d",
|
|
529
|
+
"WalkGuard::check of a directory moved by another Mount while revalidate is asked, \
|
|
530
|
+
beneath a Policy, must let a call through both then and afterwards, at /q/d"
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/// Moves `d` between `p` and `q` through `plain` each time the guarding thread
|
|
535
|
+
/// asks either whether it still stands, so every pass from the root meets a
|
|
536
|
+
/// move, until it has moved `MOVES` times.
|
|
537
|
+
struct MovesEachPass {
|
|
538
|
+
guarding: std::sync::OnceLock<std::thread::ThreadId>,
|
|
539
|
+
plain: std::sync::OnceLock<Path>,
|
|
540
|
+
moved: std::sync::atomic::AtomicUsize,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
impl MovesEachPass {
|
|
544
|
+
const MOVES: usize = 100;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
impl Hook for MovesEachPass {
|
|
548
|
+
fn revalidate(
|
|
549
|
+
&self,
|
|
550
|
+
ops: &dyn crate::vfs::DentryOperations,
|
|
551
|
+
dir: &dyn crate::filesystem::InodeOperations,
|
|
552
|
+
dentry: &Dentry,
|
|
553
|
+
) -> Result<bool, FilesystemError> {
|
|
554
|
+
use std::sync::atomic::Ordering;
|
|
555
|
+
let guarding = self.guarding.get() == Some(&std::thread::current().id());
|
|
556
|
+
let from = dentry.name();
|
|
557
|
+
if guarding
|
|
558
|
+
&& (from == "p" || from == "q")
|
|
559
|
+
&& self.moved.load(Ordering::SeqCst) < Self::MOVES
|
|
560
|
+
{
|
|
561
|
+
let to = if from == "p" { "q" } else { "p" };
|
|
562
|
+
let root = self.plain.get().expect("the plain Mount stands").clone();
|
|
563
|
+
std::thread::spawn(move || {
|
|
564
|
+
crate::vfs::filename_renameat2(
|
|
565
|
+
&restrict_self(&prepare_creds_within(&[], None), []),
|
|
566
|
+
&root,
|
|
567
|
+
&format!("{from}/d"),
|
|
568
|
+
&root,
|
|
569
|
+
&format!("{to}/d"),
|
|
570
|
+
)
|
|
571
|
+
})
|
|
572
|
+
.join()
|
|
573
|
+
.expect("the move must not panic")
|
|
574
|
+
.expect("the move must land");
|
|
575
|
+
self.moved.fetch_add(1, Ordering::SeqCst);
|
|
576
|
+
}
|
|
577
|
+
ops.revalidate(dir, dentry)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Each pass from the root meets a move another Mount makes, so a guard asking
|
|
582
|
+
// again would make the call wait while the moves go on; one pass answers,
|
|
583
|
+
// and the caller makes the call again, as a scoped walk's `..` does.
|
|
584
|
+
// @behavior RT-266
|
|
585
|
+
#[test]
|
|
586
|
+
fn a_start_moved_while_it_is_asked_answers_eagain_beneath_a_policy() {
|
|
587
|
+
use std::sync::atomic::Ordering;
|
|
588
|
+
|
|
589
|
+
let tmpfs: Tmpfs = [("p/d/f", "x"), ("q/keep", "x")].into_iter().collect();
|
|
590
|
+
let hook = Arc::new(MovesEachPass {
|
|
591
|
+
guarding: std::sync::OnceLock::new(),
|
|
592
|
+
plain: std::sync::OnceLock::new(),
|
|
593
|
+
moved: std::sync::atomic::AtomicUsize::new(0),
|
|
594
|
+
});
|
|
595
|
+
let filesystem = Filesystem::new(Arc::new(Wrap::new(Arc::new(tmpfs), hook.clone())))
|
|
596
|
+
.expect("the instance must stand");
|
|
597
|
+
let (_cred, held, plain, guard) = held_beneath_a_policy(filesystem);
|
|
598
|
+
hook.plain.set(Path::root(plain.mnt())).ok();
|
|
599
|
+
hook.guarding.set(std::thread::current().id()).ok();
|
|
600
|
+
assert_eq!(
|
|
601
|
+
(guard.check(held.path()), hook.moved.load(Ordering::SeqCst)),
|
|
602
|
+
(Err(FilesystemError::WouldBlock), 1),
|
|
603
|
+
"WalkGuard::check of a directory another Mount moves while it is asked, beneath a \
|
|
604
|
+
Policy, must answer EAGAIN from the pass that met the move"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
mod stress {
|
|
609
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
610
|
+
|
|
611
|
+
use super::*;
|
|
612
|
+
|
|
613
|
+
// Nothing refuses a directory that stands, so a single refusal while it
|
|
614
|
+
// moves between two names is a dentry the walk let go of. A move met
|
|
615
|
+
// while the guard asks answers EAGAIN for the caller to ask again, which
|
|
616
|
+
// refuses nothing.
|
|
617
|
+
// @behavior RT-234
|
|
618
|
+
#[test]
|
|
619
|
+
#[ignore = "a stress witness; run by rake crates:stress"]
|
|
620
|
+
fn a_directory_moving_while_calls_start_from_it_stays_reachable_beneath_a_policy() {
|
|
621
|
+
let tmpfs: Tmpfs = [("p/d/f", "x"), ("q/keep", "x")].into_iter().collect();
|
|
622
|
+
let (cred, held, plain, guard) = held_beneath_a_policy(
|
|
623
|
+
Filesystem::new(Arc::new(tmpfs)).expect("the instance must stand"),
|
|
624
|
+
);
|
|
625
|
+
let stop = AtomicBool::new(false);
|
|
626
|
+
let start = held.path().clone();
|
|
627
|
+
let refused = std::thread::scope(|scope| {
|
|
628
|
+
scope.spawn(|| {
|
|
629
|
+
let (root, cred) = (
|
|
630
|
+
Path::root(plain.mnt()),
|
|
631
|
+
restrict_self(&prepare_creds_within(&[], None), []),
|
|
632
|
+
);
|
|
633
|
+
while !stop.load(Ordering::SeqCst) {
|
|
634
|
+
for (from, to) in [("p/d", "q/d"), ("q/d", "p/d")] {
|
|
635
|
+
crate::vfs::filename_renameat2(&cred, &root, from, &root, to)
|
|
636
|
+
.expect("the move must land");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
let call = || {
|
|
641
|
+
guard.check(&start)?;
|
|
642
|
+
crate::vfs::vfs_fstatat(&cred, &start, "f", false)
|
|
643
|
+
};
|
|
644
|
+
let refused = (0..200_000)
|
|
645
|
+
.filter(|_| call().is_err_and(|error| error != FilesystemError::WouldBlock))
|
|
646
|
+
.count();
|
|
647
|
+
stop.store(true, Ordering::SeqCst);
|
|
648
|
+
refused
|
|
649
|
+
});
|
|
650
|
+
assert_eq!(
|
|
651
|
+
refused, 0,
|
|
652
|
+
"vfs::vfs_fstatat through a directory another Mount keeps moving, beneath a \
|
|
653
|
+
Policy, must always find its file"
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// A Bind refuses these itself, so only asking the rule directly shows it
|
|
659
|
+
// holds for a Filesystem that would open them.
|
|
660
|
+
// @behavior RT-197
|
|
661
|
+
#[test]
|
|
662
|
+
fn only_a_file_or_a_directory_opens() {
|
|
663
|
+
let mount = Mount::new(
|
|
664
|
+
"/data",
|
|
665
|
+
crate::filesystem::Filesystem::new(Arc::new(crate::filesystem::Tmpfs::new()))
|
|
666
|
+
.expect("the instance must stand"),
|
|
667
|
+
Access::Read,
|
|
668
|
+
)
|
|
669
|
+
.expect("/data is a guest path");
|
|
670
|
+
let judged = |kind| {
|
|
671
|
+
let path = SecurityPath {
|
|
672
|
+
mnt: mount.mnt(),
|
|
673
|
+
name: &|| "/".to_owned(),
|
|
674
|
+
};
|
|
675
|
+
KobakoSecurity
|
|
676
|
+
.file_open(path, kind, FMode::READ, &OpenHow::default())
|
|
677
|
+
.is_ok()
|
|
678
|
+
};
|
|
679
|
+
assert_eq!(
|
|
680
|
+
[
|
|
681
|
+
FileType::RegFile,
|
|
682
|
+
FileType::Dir,
|
|
683
|
+
FileType::Symlink,
|
|
684
|
+
FileType::Fifo,
|
|
685
|
+
FileType::Sock,
|
|
686
|
+
FileType::Blkdev,
|
|
687
|
+
FileType::Chrdev,
|
|
688
|
+
FileType::Unknown,
|
|
689
|
+
]
|
|
690
|
+
.map(judged),
|
|
691
|
+
[true, true, false, false, false, false, false, false],
|
|
692
|
+
"KobakoSecurity::file_open must pass a file and a directory, and refuse every other \
|
|
693
|
+
kind"
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|