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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
//! Holds calls racing on one shared instance to the Linux they align with. No
|
|
2
|
+
//! interleaving repeats, so answers are compared as sets: what kobako answers a
|
|
3
|
+
//! kind of call, Linux must have answered it too over the same sequences. What
|
|
4
|
+
//! stands once every call returned is held to rules any interleaving keeps.
|
|
5
|
+
|
|
6
|
+
use std::collections::BTreeSet;
|
|
7
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
8
|
+
use std::sync::Barrier;
|
|
9
|
+
use std::thread;
|
|
10
|
+
|
|
11
|
+
use crate::calls::{Call, Rng, SLOTS};
|
|
12
|
+
use crate::kobako::Kobako;
|
|
13
|
+
use crate::linux::Linux;
|
|
14
|
+
use crate::Snapshot;
|
|
15
|
+
|
|
16
|
+
/// How many threads race, each with descriptors of its own.
|
|
17
|
+
pub const THREADS: usize = 4;
|
|
18
|
+
|
|
19
|
+
/// What a sequence races over: plain names, so a path names one entry whichever
|
|
20
|
+
/// descriptor or link would otherwise lead there.
|
|
21
|
+
const PATHS: [&str; 12] = [
|
|
22
|
+
"a", "b", "c", "d", "d/f", "d/g", "d/e", "d/e/h", "m", "m/s", "m/s/t", "m/x",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/// One side of the comparison, one instance per racing thread.
|
|
26
|
+
pub trait Side: Send {
|
|
27
|
+
fn run(&mut self, call: &Call) -> String;
|
|
28
|
+
fn snapshot(&self) -> Snapshot;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Side for Kobako {
|
|
32
|
+
fn run(&mut self, call: &Call) -> String {
|
|
33
|
+
Kobako::run(self, call)
|
|
34
|
+
}
|
|
35
|
+
fn snapshot(&self) -> Snapshot {
|
|
36
|
+
Kobako::snapshot(self)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl Side for Linux {
|
|
41
|
+
fn run(&mut self, call: &Call) -> String {
|
|
42
|
+
Linux::run(self, call)
|
|
43
|
+
}
|
|
44
|
+
fn snapshot(&self) -> Snapshot {
|
|
45
|
+
Linux::snapshot(self)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// One call as it happened, between two stamps of one clock all threads share.
|
|
50
|
+
struct Event {
|
|
51
|
+
call: Call,
|
|
52
|
+
answer: String,
|
|
53
|
+
began: u64,
|
|
54
|
+
ended: u64,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl Event {
|
|
58
|
+
fn succeeded(&self) -> bool {
|
|
59
|
+
self.answer.starts_with("ok")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// The name this call removed.
|
|
63
|
+
fn removed(&self) -> Option<&str> {
|
|
64
|
+
match &self.call {
|
|
65
|
+
Call::Unlink(_, path) | Call::Rmdir(_, path) if self.succeeded() => Some(path),
|
|
66
|
+
_ => None,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Whether this call may have put an entry at or beneath `name`.
|
|
71
|
+
fn may_make(&self, name: &str) -> bool {
|
|
72
|
+
let related = |path: &str| {
|
|
73
|
+
path == name
|
|
74
|
+
|| path.starts_with(&format!("{name}/"))
|
|
75
|
+
|| name.starts_with(&format!("{path}/"))
|
|
76
|
+
};
|
|
77
|
+
if !self.succeeded() {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
match &self.call {
|
|
81
|
+
Call::Open(open) => open.create && related(&open.path),
|
|
82
|
+
Call::Mkdir(_, path) => related(path),
|
|
83
|
+
Call::Rename(_, from, to) => related(from) || related(to),
|
|
84
|
+
_ => false,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// A call and its answer by what it says of the call, not its data: a read is
|
|
90
|
+
/// only `ok`, a stat only the kind it found, and what an open asked is kept where
|
|
91
|
+
/// it decides the answer.
|
|
92
|
+
fn class(event: &Event) -> String {
|
|
93
|
+
let answer = first_word(&event.answer);
|
|
94
|
+
let asked = match &event.call {
|
|
95
|
+
Call::Open(open) => format!(" create={} exclusive={}", open.create, open.exclusive),
|
|
96
|
+
_ => String::new(),
|
|
97
|
+
};
|
|
98
|
+
format!("{}{asked} {answer}", event.call.kind())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// One thread's sequence.
|
|
102
|
+
fn sequence(seed: u64, thread: usize, longest: u64) -> Vec<Call> {
|
|
103
|
+
let paths = PATHS.map(str::to_string);
|
|
104
|
+
let mut rng = Rng::new(seed.wrapping_mul(31).wrapping_add(thread as u64 + 1));
|
|
105
|
+
let length = 1 + rng.random_below(longest);
|
|
106
|
+
(0..length)
|
|
107
|
+
.map(|_| {
|
|
108
|
+
let mut call = rng.call(&paths);
|
|
109
|
+
match &mut call {
|
|
110
|
+
Call::Open(open) => open.base = None,
|
|
111
|
+
Call::Stat { base, .. }
|
|
112
|
+
| Call::Mkdir(base, _)
|
|
113
|
+
| Call::Rmdir(base, _)
|
|
114
|
+
| Call::Unlink(base, _)
|
|
115
|
+
| Call::Rename(base, ..) => *base = None,
|
|
116
|
+
_ => {}
|
|
117
|
+
}
|
|
118
|
+
call
|
|
119
|
+
})
|
|
120
|
+
.collect()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// The kind each descriptor reported when it opened, so a thread can tell one that
|
|
124
|
+
/// came to report another.
|
|
125
|
+
#[derive(Default)]
|
|
126
|
+
struct HeldKinds {
|
|
127
|
+
kinds: [Option<String>; SLOTS],
|
|
128
|
+
changed: BTreeSet<String>,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
impl HeldKinds {
|
|
132
|
+
/// Keep what `call`, answered `answer`, says of the descriptors, asking `side`
|
|
133
|
+
/// what one just opened is.
|
|
134
|
+
fn note<S: Side>(&mut self, side: &mut S, call: &Call, answer: &str) {
|
|
135
|
+
match call {
|
|
136
|
+
Call::Open(open) if answer.starts_with("ok") => {
|
|
137
|
+
let kind = side.run(&Call::Fstat(open.slot));
|
|
138
|
+
self.kinds[open.slot] = Some(first_word(&kind).to_string());
|
|
139
|
+
}
|
|
140
|
+
Call::Close(slot) => self.kinds[*slot] = None,
|
|
141
|
+
Call::Fstat(slot) => {
|
|
142
|
+
if let Some(kind) = &self.kinds[*slot] {
|
|
143
|
+
if kind != first_word(answer) {
|
|
144
|
+
self.changed
|
|
145
|
+
.insert(format!("a {kind} descriptor came to report {answer}"));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
_ => {}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn first_word(answer: &str) -> &str {
|
|
155
|
+
answer.split([':', ' ']).next().unwrap_or("")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Race `sequences` over one side, one thread each, answering what each call did,
|
|
159
|
+
/// the thread's side to look at afterwards, and what a held descriptor changed to.
|
|
160
|
+
fn race<S: Side>(sides: Vec<S>, sequences: &[Vec<Call>]) -> (Vec<S>, Vec<Event>, BTreeSet<String>) {
|
|
161
|
+
let clock = AtomicU64::new(0);
|
|
162
|
+
let start = Barrier::new(sides.len());
|
|
163
|
+
let (mut left, mut events, mut changed) = (Vec::new(), Vec::new(), BTreeSet::new());
|
|
164
|
+
thread::scope(|scope| {
|
|
165
|
+
let threads: Vec<_> = sides
|
|
166
|
+
.into_iter()
|
|
167
|
+
.zip(sequences)
|
|
168
|
+
.map(|(mut side, calls)| {
|
|
169
|
+
let (clock, start) = (&clock, &start);
|
|
170
|
+
scope.spawn(move || {
|
|
171
|
+
start.wait();
|
|
172
|
+
let mut events = Vec::with_capacity(calls.len());
|
|
173
|
+
let mut held = HeldKinds::default();
|
|
174
|
+
for call in calls {
|
|
175
|
+
let began = clock.fetch_add(1, Ordering::SeqCst);
|
|
176
|
+
let answer = side.run(call);
|
|
177
|
+
let ended = clock.fetch_add(1, Ordering::SeqCst);
|
|
178
|
+
held.note(&mut side, call, &answer);
|
|
179
|
+
events.push(Event {
|
|
180
|
+
call: call.clone(),
|
|
181
|
+
answer,
|
|
182
|
+
began,
|
|
183
|
+
ended,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for slot in 0..SLOTS {
|
|
187
|
+
let call = Call::Fstat(slot);
|
|
188
|
+
let answer = side.run(&call);
|
|
189
|
+
held.note(&mut side, &call, &answer);
|
|
190
|
+
}
|
|
191
|
+
(side, events, held.changed)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
.collect();
|
|
195
|
+
for thread in threads {
|
|
196
|
+
let (side, done, held) = thread.join().expect("a racing thread must not panic");
|
|
197
|
+
left.push(side);
|
|
198
|
+
events.extend(done);
|
|
199
|
+
changed.extend(held);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
(left, events, changed)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// What no interleaving allows of what stands once every call returned: an entry
|
|
206
|
+
/// its own stat refuses, a descriptor that no longer answers, and a name removed
|
|
207
|
+
/// that stands with nothing having made it since the removal began.
|
|
208
|
+
fn broken_rules<S: Side>(sides: &mut [S], events: &[Event]) -> BTreeSet<String> {
|
|
209
|
+
let mut broken = BTreeSet::new();
|
|
210
|
+
for side in sides.iter_mut() {
|
|
211
|
+
for slot in 0..SLOTS {
|
|
212
|
+
let answer = side.run(&Call::Fstat(slot));
|
|
213
|
+
if answer.starts_with('E') && answer != "EBADF" {
|
|
214
|
+
broken.insert(format!("a held descriptor answers {answer} at rest"));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let tree = sides[0].snapshot();
|
|
219
|
+
for (path, seen) in &tree {
|
|
220
|
+
if seen.starts_with('E') || seen.contains("unreadable") {
|
|
221
|
+
broken.insert(format!("{path} is listed but answers {seen}"));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for removal in events {
|
|
225
|
+
let Some(name) = removal.removed() else {
|
|
226
|
+
continue;
|
|
227
|
+
};
|
|
228
|
+
let remade = events
|
|
229
|
+
.iter()
|
|
230
|
+
.any(|event| event.ended >= removal.began && event.may_make(name));
|
|
231
|
+
let stands = tree
|
|
232
|
+
.keys()
|
|
233
|
+
.find(|path| *path == name || path.starts_with(&format!("{name}/")));
|
|
234
|
+
if let (false, Some(path)) = (remade, stands) {
|
|
235
|
+
broken.insert(format!(
|
|
236
|
+
"{path} stands after its removal, with nothing making it"
|
|
237
|
+
));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
broken
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// What one setup found over its rounds.
|
|
244
|
+
#[derive(Default)]
|
|
245
|
+
pub struct Found {
|
|
246
|
+
pub kobako: BTreeSet<String>,
|
|
247
|
+
pub linux: BTreeSet<String>,
|
|
248
|
+
pub broken_by_kobako: BTreeSet<String>,
|
|
249
|
+
pub broken_by_linux: BTreeSet<String>,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Race one round's sequences through a side of each.
|
|
253
|
+
pub fn round(
|
|
254
|
+
seed: u64,
|
|
255
|
+
longest: u64,
|
|
256
|
+
kobako: Vec<Kobako>,
|
|
257
|
+
linux: Vec<Linux>,
|
|
258
|
+
found: &mut Found,
|
|
259
|
+
) -> (Vec<Kobako>, Vec<Linux>) {
|
|
260
|
+
let sequences: Vec<Vec<Call>> = (0..kobako.len())
|
|
261
|
+
.map(|thread| sequence(seed, thread, longest))
|
|
262
|
+
.collect();
|
|
263
|
+
let (mut kobako, ours, changed) = race(kobako, &sequences);
|
|
264
|
+
found.broken_by_kobako.extend(changed);
|
|
265
|
+
found
|
|
266
|
+
.broken_by_kobako
|
|
267
|
+
.extend(broken_rules(&mut kobako, &ours));
|
|
268
|
+
found.kobako.extend(ours.iter().map(class));
|
|
269
|
+
let (mut linux, theirs, changed) = race(linux, &sequences);
|
|
270
|
+
found.broken_by_linux.extend(changed);
|
|
271
|
+
found
|
|
272
|
+
.broken_by_linux
|
|
273
|
+
.extend(broken_rules(&mut linux, &theirs));
|
|
274
|
+
found.linux.extend(theirs.iter().map(class));
|
|
275
|
+
(kobako, linux)
|
|
276
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
//! A sequence's calls through kobako's VFS.
|
|
2
|
+
|
|
3
|
+
use std::collections::BTreeMap;
|
|
4
|
+
use std::fmt::Debug;
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
7
|
+
|
|
8
|
+
use kobako_runtime::filesystem::{Filesystem, FilesystemError, Iattr, SetTime};
|
|
9
|
+
use kobako_runtime::mount::{Access, Mount, Policy};
|
|
10
|
+
use kobako_runtime::security::{prepare_creds_within, restrict_self};
|
|
11
|
+
use kobako_runtime::vfs::{
|
|
12
|
+
self, Cred, DirContext, DirEmit, File, FileType, OpenFlags, OpenHow, VfsMount, Whence,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
use crate::calls::{payload, Call, SLOTS, UTIME_SECONDS};
|
|
16
|
+
use crate::{describe, Snapshot, Times};
|
|
17
|
+
|
|
18
|
+
fn errno(error: &FilesystemError) -> String {
|
|
19
|
+
error
|
|
20
|
+
.posix_name()
|
|
21
|
+
.map_or_else(|| format!("EIO({error:?})"), str::to_string)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn answer<T>(result: Result<T, FilesystemError>, ok: impl FnOnce(T) -> String) -> String {
|
|
25
|
+
result.map_or_else(|error| errno(&error), ok)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn ok_or_errno<T: Debug>(result: Result<T, FilesystemError>) -> String {
|
|
29
|
+
answer(result, |_| "ok".to_string())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn kind_name(kind: FileType) -> &'static str {
|
|
33
|
+
match kind {
|
|
34
|
+
FileType::RegFile => "reg",
|
|
35
|
+
FileType::Dir => "dir",
|
|
36
|
+
FileType::Symlink => "lnk",
|
|
37
|
+
_ => "oth",
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn directory_flags() -> OpenFlags {
|
|
42
|
+
let mut how = OpenHow::default();
|
|
43
|
+
how.read = true;
|
|
44
|
+
how.directory = true;
|
|
45
|
+
vfs::build_open_flags(&how).expect("a directory read means an open")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// The names `file` lists from its start, kinds included, `.` and `..` among
|
|
49
|
+
/// them, as the Linux side seeks a directory back before `getdents64`.
|
|
50
|
+
fn entries(file: &mut File) -> Result<Vec<(String, FileType)>, FilesystemError> {
|
|
51
|
+
if file.is_directory() {
|
|
52
|
+
file.llseek(0, Whence::Set)?;
|
|
53
|
+
}
|
|
54
|
+
let mut names = Vec::new();
|
|
55
|
+
let mut actor = |emit: &DirEmit<'_>| {
|
|
56
|
+
names.push((emit.name.to_string(), emit.file_type));
|
|
57
|
+
true
|
|
58
|
+
};
|
|
59
|
+
file.iterate_dir(&mut DirContext::new(0, &mut actor))?;
|
|
60
|
+
Ok(names)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub struct Kobako {
|
|
64
|
+
cred: Arc<Cred>,
|
|
65
|
+
mnt: Arc<VfsMount>,
|
|
66
|
+
root: File,
|
|
67
|
+
slots: Vec<Option<File>>,
|
|
68
|
+
merged: bool,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl Kobako {
|
|
72
|
+
/// The root of `filesystem` mounted with `access`. `merged` leaves out what
|
|
73
|
+
/// an Overlay and overlayfs each count of a directory their own way.
|
|
74
|
+
pub fn new(filesystem: Arc<Filesystem>, access: Access, merged: bool) -> Self {
|
|
75
|
+
let mount = Mount::new("/baseline", filesystem, access).expect("/baseline is a guest path");
|
|
76
|
+
let cred = restrict_self(&prepare_creds_within(&[], None), [&mount]);
|
|
77
|
+
let root = File::root(mount.mnt(), &cred)
|
|
78
|
+
.unwrap_or_else(|error| panic!("the root must open: {error:?}"));
|
|
79
|
+
Self {
|
|
80
|
+
cred,
|
|
81
|
+
mnt: mount.mnt().clone(),
|
|
82
|
+
root,
|
|
83
|
+
slots: std::iter::repeat_with(|| None).take(SLOTS).collect(),
|
|
84
|
+
merged,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// The root of `filesystem` mounted to change, beneath `policy`.
|
|
89
|
+
pub fn with_policy(filesystem: Arc<Filesystem>, policy: Arc<dyn Policy>) -> Self {
|
|
90
|
+
let mount = Mount::new("/baseline", filesystem, Access::ReadWrite)
|
|
91
|
+
.expect("/baseline is a guest path")
|
|
92
|
+
.with_policy(policy);
|
|
93
|
+
let cred = restrict_self(&prepare_creds_within(&[], None), [&mount]);
|
|
94
|
+
let root = File::root(mount.mnt(), &cred)
|
|
95
|
+
.unwrap_or_else(|error| panic!("the root must open: {error:?}"));
|
|
96
|
+
Self {
|
|
97
|
+
cred,
|
|
98
|
+
mnt: mount.mnt().clone(),
|
|
99
|
+
root,
|
|
100
|
+
slots: std::iter::repeat_with(|| None).take(SLOTS).collect(),
|
|
101
|
+
merged: false,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Another caller of the same mount, with descriptors of its own.
|
|
106
|
+
pub fn another(&self) -> Self {
|
|
107
|
+
let root = File::root(&self.mnt, &self.cred)
|
|
108
|
+
.unwrap_or_else(|error| panic!("the root must open: {error:?}"));
|
|
109
|
+
Self {
|
|
110
|
+
cred: self.cred.clone(),
|
|
111
|
+
mnt: self.mnt.clone(),
|
|
112
|
+
root,
|
|
113
|
+
slots: std::iter::repeat_with(|| None).take(SLOTS).collect(),
|
|
114
|
+
merged: self.merged,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn start(&self, base: Option<usize>) -> Result<&vfs::Path, String> {
|
|
119
|
+
match base {
|
|
120
|
+
None => Ok(self.root.path()),
|
|
121
|
+
Some(slot) => match &self.slots[slot] {
|
|
122
|
+
None => Err("EBADF".to_string()),
|
|
123
|
+
Some(file) => Ok(file.path()),
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn stat(&self, stat: vfs::Kstat) -> String {
|
|
129
|
+
describe(
|
|
130
|
+
kind_name(stat.file_type),
|
|
131
|
+
stat.size,
|
|
132
|
+
stat.nlink,
|
|
133
|
+
self.merged,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub fn run(&mut self, call: &Call) -> String {
|
|
138
|
+
macro_rules! start {
|
|
139
|
+
($base:expr) => {
|
|
140
|
+
match self.start($base) {
|
|
141
|
+
Ok(path) => path,
|
|
142
|
+
Err(answer) => return answer,
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
macro_rules! file {
|
|
147
|
+
($slot:expr) => {
|
|
148
|
+
match self.slots[$slot].as_mut() {
|
|
149
|
+
Some(file) => file,
|
|
150
|
+
None => return "EBADF".to_string(),
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
match call {
|
|
155
|
+
Call::Open(open) => {
|
|
156
|
+
let mut how = OpenHow::default();
|
|
157
|
+
how.read = open.read || !open.write;
|
|
158
|
+
how.write = open.write;
|
|
159
|
+
how.create = open.create;
|
|
160
|
+
how.exclusive = open.exclusive;
|
|
161
|
+
how.truncate = open.truncate;
|
|
162
|
+
how.append = open.append;
|
|
163
|
+
how.directory = open.directory;
|
|
164
|
+
how.follow = !open.nofollow;
|
|
165
|
+
let start = start!(open.base);
|
|
166
|
+
let opened = vfs::build_open_flags(&how)
|
|
167
|
+
.and_then(|op| vfs::do_file_open(&self.cred, start, &open.path, &op));
|
|
168
|
+
answer(opened, |file| {
|
|
169
|
+
self.slots[open.slot] = Some(file);
|
|
170
|
+
"ok".to_string()
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
Call::Close(slot) => {
|
|
174
|
+
file!(*slot);
|
|
175
|
+
let file = self.slots[*slot].take().expect("the slot holds a file");
|
|
176
|
+
ok_or_errno(file.close())
|
|
177
|
+
}
|
|
178
|
+
Call::Read(slot, len) => {
|
|
179
|
+
let mut buf = vec![0; *len];
|
|
180
|
+
answer(file!(*slot).read(&mut buf), |read| bytes(&buf[..read]))
|
|
181
|
+
}
|
|
182
|
+
Call::Write(slot, len) => answer(file!(*slot).write(payload(*len)), bytes_written),
|
|
183
|
+
Call::Pread(slot, len, pos) => {
|
|
184
|
+
let mut buf = vec![0; *len];
|
|
185
|
+
answer(file!(*slot).pread(&mut buf, *pos), |read| {
|
|
186
|
+
bytes(&buf[..read])
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
Call::Pwrite(slot, len, pos) => {
|
|
190
|
+
answer(file!(*slot).pwrite(payload(*len), *pos), bytes_written)
|
|
191
|
+
}
|
|
192
|
+
Call::Seek(slot, offset, whence) => {
|
|
193
|
+
let file = file!(*slot);
|
|
194
|
+
if file.is_directory() {
|
|
195
|
+
return "directory".to_string();
|
|
196
|
+
}
|
|
197
|
+
let whence = [Whence::Set, Whence::Cur, Whence::End][*whence as usize];
|
|
198
|
+
answer(file.llseek(*offset, whence), |pos| format!("ok:{pos}"))
|
|
199
|
+
}
|
|
200
|
+
Call::Truncate(slot, len) => ok_or_errno(file!(*slot).truncate(*len)),
|
|
201
|
+
Call::Fallocate(slot, offset, len) => {
|
|
202
|
+
ok_or_errno(file!(*slot).fallocate(*offset, *len))
|
|
203
|
+
}
|
|
204
|
+
Call::Utimes { base, path, follow } => {
|
|
205
|
+
let time = UNIX_EPOCH + Duration::from_secs(UTIME_SECONDS as u64);
|
|
206
|
+
let times = Iattr::times(None, Some(SetTime::At(time)));
|
|
207
|
+
ok_or_errno(vfs::do_utimes_path(
|
|
208
|
+
&self.cred,
|
|
209
|
+
start!(*base),
|
|
210
|
+
path,
|
|
211
|
+
*follow,
|
|
212
|
+
×,
|
|
213
|
+
))
|
|
214
|
+
}
|
|
215
|
+
Call::Fstat(slot) => {
|
|
216
|
+
let got = file!(*slot).getattr();
|
|
217
|
+
answer(got, |metadata| self.stat(metadata))
|
|
218
|
+
}
|
|
219
|
+
Call::Fsync(slot) => ok_or_errno(file!(*slot).fsync(false)),
|
|
220
|
+
Call::Readdir(slot) => answer(entries(file!(*slot)), |names| {
|
|
221
|
+
let mut names: Vec<_> = names
|
|
222
|
+
.into_iter()
|
|
223
|
+
.map(|(name, kind)| format!("{name}:{}", kind_name(kind)))
|
|
224
|
+
.collect();
|
|
225
|
+
names.sort();
|
|
226
|
+
format!("ok:{names:?}")
|
|
227
|
+
}),
|
|
228
|
+
Call::Stat { base, path, follow } => {
|
|
229
|
+
let got = vfs::vfs_fstatat(&self.cred, start!(*base), path, *follow);
|
|
230
|
+
answer(got, |metadata| self.stat(metadata))
|
|
231
|
+
}
|
|
232
|
+
Call::Mkdir(base, path) => {
|
|
233
|
+
ok_or_errno(vfs::filename_mkdirat(&self.cred, start!(*base), path))
|
|
234
|
+
}
|
|
235
|
+
Call::Rmdir(base, path) => {
|
|
236
|
+
ok_or_errno(vfs::filename_rmdir(&self.cred, start!(*base), path))
|
|
237
|
+
}
|
|
238
|
+
Call::Unlink(base, path) => {
|
|
239
|
+
ok_or_errno(vfs::filename_unlinkat(&self.cred, start!(*base), path))
|
|
240
|
+
}
|
|
241
|
+
Call::Rename(base, from, to) => ok_or_errno(vfs::filename_renameat2(
|
|
242
|
+
&self.cred,
|
|
243
|
+
start!(*base),
|
|
244
|
+
from,
|
|
245
|
+
self.root.path(),
|
|
246
|
+
to,
|
|
247
|
+
)),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Visit every entry beneath the root, directories before what they hold.
|
|
252
|
+
fn walk(&self, dir: &vfs::Path, prefix: &str, visit: &mut dyn FnMut(&vfs::Path, &str, &str)) {
|
|
253
|
+
let Ok(mut opened) = vfs::do_file_open(&self.cred, dir, ".", &directory_flags()) else {
|
|
254
|
+
return;
|
|
255
|
+
};
|
|
256
|
+
for (name, kind) in entries(&mut opened).unwrap_or_default() {
|
|
257
|
+
if name == "." || name == ".." {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
let full = format!("{prefix}{name}");
|
|
261
|
+
visit(dir, &name, &full);
|
|
262
|
+
if kind == FileType::Dir {
|
|
263
|
+
if let Ok(sub) = vfs::do_file_open(&self.cred, dir, &name, &directory_flags()) {
|
|
264
|
+
self.walk(sub.path(), &format!("{full}/"), visit);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Every entry beneath the root, with a file's content.
|
|
271
|
+
pub fn snapshot(&self) -> Snapshot {
|
|
272
|
+
let mut out = BTreeMap::new();
|
|
273
|
+
self.walk(self.root.path(), "", &mut |dir, name, full| {
|
|
274
|
+
let seen = match vfs::vfs_fstatat(&self.cred, dir, name, false) {
|
|
275
|
+
Err(error) => errno(&error),
|
|
276
|
+
Ok(metadata) => {
|
|
277
|
+
let kind = metadata.file_type;
|
|
278
|
+
let mut seen = self.stat(metadata);
|
|
279
|
+
if kind == FileType::RegFile {
|
|
280
|
+
seen += &self.content(dir, name);
|
|
281
|
+
}
|
|
282
|
+
seen
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
out.insert(full.to_string(), seen);
|
|
286
|
+
});
|
|
287
|
+
out
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn content(&self, dir: &vfs::Path, name: &str) -> String {
|
|
291
|
+
let mut how = OpenHow::default();
|
|
292
|
+
how.read = true;
|
|
293
|
+
let op = vfs::build_open_flags(&how).expect("a read means an open");
|
|
294
|
+
let Ok(mut file) = vfs::do_file_open(&self.cred, dir, name, &op) else {
|
|
295
|
+
return " unreadable".to_string();
|
|
296
|
+
};
|
|
297
|
+
let mut buf = [0; 64];
|
|
298
|
+
let read = file.read(&mut buf).unwrap_or(0);
|
|
299
|
+
format!(" {:?}", String::from_utf8_lossy(&buf[..read]))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// When each entry beneath the root was last modified and changed.
|
|
303
|
+
pub fn times(&self) -> Times<Option<SystemTime>> {
|
|
304
|
+
let mut out = BTreeMap::new();
|
|
305
|
+
if let Ok(stat) = vfs::vfs_fstatat(&self.cred, self.root.path(), ".", false) {
|
|
306
|
+
out.insert(".".to_string(), (stat.mtime, stat.ctime));
|
|
307
|
+
}
|
|
308
|
+
self.walk(self.root.path(), "", &mut |dir, name, full| {
|
|
309
|
+
if let Ok(stat) = vfs::vfs_fstatat(&self.cred, dir, name, false) {
|
|
310
|
+
out.insert(full.to_string(), (stat.mtime, stat.ctime));
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
out
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn bytes(read: &[u8]) -> String {
|
|
318
|
+
format!("ok:{:?}", String::from_utf8_lossy(read))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn bytes_written(count: usize) -> String {
|
|
322
|
+
format!("ok:{count}")
|
|
323
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# The differences from Linux known so far, one per line: the setup, then the
|
|
2
|
+
# class a failure of tests/linux_baseline names, with what causes it after a `#`.
|
|
3
|
+
# A line records a difference and settles nothing about it.
|
|
4
|
+
overlay Rename from=g kobako=EXDEV linux=ok # a lower Layer's link is not copied up
|
|
5
|
+
overlay Rename from=l kobako=EXDEV linux=ok # a lower Layer's link is not copied up
|
|
6
|
+
overlay Rename from=n kobako=EXDEV linux=ok # a lower Layer's link is not copied up
|
|
7
|
+
bind Utimes kobako=ENOTSUP linux=ok # a link keeps its own times
|
|
8
|
+
overlay Utimes kobako=ENOTSUP linux=ok # a link keeps its own times
|
|
9
|
+
policy Rename from=d kobako=EXDEV linux=ENOTDIR # a Policy answers for one path, not a tree
|
|
10
|
+
policy Rename from=d kobako=EXDEV linux=ok # a Policy answers for one path, not a tree
|
|
11
|
+
policy Rename from=n kobako=EXDEV linux=ENOTDIR # a Policy answers for one path, not a tree
|
|
12
|
+
policy Rename from=n kobako=EXDEV linux=ok # a Policy answers for one path, not a tree
|
|
13
|
+
policy Utimes kobako=EACCES linux=ok # a Policy judges a time as a change, where Landlock leaves it
|
|
14
|
+
another-user Open kobako=EACCES linux=ok # a directory only readable or only searched: spelling a name and reading through it take more
|
|
15
|
+
another-user Rename from=nor kobako=EACCES linux=ok # spelling a name in a directory only searched takes reading it
|
|
16
|
+
another-user Stat kobako=EACCES linux=reg # spelling a name in a directory only searched takes reading it
|