1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
//! What:
//! Directory-relative file operations that never follow a symbolic link or cross a mount: open a directory, a file or a
//! link below a held directory handle one plain name at a time (`openat2` with `RESOLVE_BENEATH`, `RESOLVE_NO_SYMLINKS`,
//! `RESOLVE_NO_XDEV` and `RESOLVE_NO_MAGICLINKS`), create entries there, set times on them, rename them, publish an open
//! file under a new name without replacing anything, and remove a lock file whose identity is the one that was recorded.
//! Why:
//! A path string is resolved again by every call that takes it, so a directory swapped for a link between two calls
//! redirects the second one. A name that was checked may also name another object when it is unlinked, and nothing in the
//! kernel makes the two one step, so no tree is deleted through this module: what the ladder made is kept and reported.
//! The clone rung and the snapshot's stub removal work on trees other processes can rename into; holding the handle of each
//! directory and naming one component at a time keeps every call on the object that was looked at.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const root = await openRoot('/canonical/path'); // throws when an ancestor is a link
//! const dir = await openDirectoryAt(root, 'a'); // throws on a link, a mount, '.' or '..'
//! await removeOwnedAt(dir, 'file', recordedIdentity); // keeps a replacement
//! ```
use std::ffi::{CString, OsStr, OsString};
use std::fs::File;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path, PathBuf};
/// `RESOLVE_NO_XDEV`: no mount point, bind mounts included, is crossed.
const RESOLVE_NO_XDEV: u64 = 0x01;
/// `RESOLVE_NO_MAGICLINKS`: no `/proc` style link is followed.
const RESOLVE_NO_MAGICLINKS: u64 = 0x02;
/// `RESOLVE_NO_SYMLINKS`: no symbolic link is followed, the last component included.
const RESOLVE_NO_SYMLINKS: u64 = 0x04;
/// `RESOLVE_BENEATH`: the lookup stays below the directory it starts from.
const RESOLVE_BENEATH: u64 = 0x08;
/// The arguments of `openat2`, laid out as the kernel's `struct open_how`.
#[repr(C)]
struct OpenHow {
/// The `open` flags.
flags: u64,
/// The mode of a created file.
mode: u64,
/// The `RESOLVE_*` bits.
resolve: u64,
}
/// The identity of a filesystem object: device, inode and, where the filesystem gives one, the birth time.
///
/// Why: a path or a name proves nothing about which object it names later; this does, up to the reuse of an inode number
/// within one birth-time tick.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ObjectId {
/// The device number.
pub device: u64,
/// The inode number.
pub inode: u64,
/// The birth time as seconds and nanoseconds, when the filesystem reports it.
pub birth: Option<(i64, u32)>,
}
impl ObjectId {
/// The identity as `device:inode:seconds:nanoseconds` (`-` for an unknown birth time).
pub fn encode(&self) -> String {
return match self.birth {
Some((seconds, nanos)) => format!("{}:{}:{seconds}:{nanos}", self.device, self.inode),
None => format!("{}:{}:-:-", self.device, self.inode),
};
}
/// The identity from its encoding.
pub fn decode(text: &str) -> Option<ObjectId> {
let mut parts = text.split(':');
let device: u64 = parts.next()?.parse().ok()?;
let inode: u64 = parts.next()?.parse().ok()?;
let seconds: &str = parts.next()?;
let nanos: &str = parts.next()?;
let birth: Option<(i64, u32)> = if seconds == "-" { None } else { Some((seconds.parse().ok()?, nanos.parse().ok()?)) };
return if parts.next().is_none() { Some(ObjectId { device, inode, birth }) } else { None };
}
}
/// The components of a relative path, each refused unless it is a plain name.
///
/// Why: `..`, `.`, a root or a prefix would leave the held directory or name it twice.
pub fn plain_components(relative: &Path) -> std::io::Result<Vec<OsString>> {
let mut names: Vec<OsString> = Vec::new();
for part in relative.components() {
match part {
Component::Normal(name) => names.push(name.to_os_string()),
_ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is not a plain relative path", relative.display()))),
}
}
return Ok(names);
}
/// A NUL-terminated copy of one name, refused when it is empty, `.`, `..`, holds a slash or a NUL byte.
fn name_of(name: &OsStr) -> std::io::Result<CString> {
let bytes: &[u8] = name.as_bytes();
if bytes.is_empty() || bytes == b"." || bytes == b".." || bytes.contains(&b'/') {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "a name must be one plain component"));
}
return CString::new(bytes).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a name holds a NUL byte"); });
}
/// Turn a C result that is `-1` on failure into the thread's error.
fn checked(result: libc::c_int) -> std::io::Result<libc::c_int> {
return if result < 0 { Err(std::io::Error::last_os_error()) } else { Ok(result) };
}
/// `openat2` of one plain name below `dir`, resolving nothing but the name itself.
fn open_beneath(dir: &File, name: &OsStr, flags: libc::c_int, mode: u32) -> std::io::Result<File> {
let c_name = name_of(name)?;
let how: OpenHow = OpenHow {
flags: (flags | libc::O_CLOEXEC) as u64,
mode: u64::from(mode),
resolve: RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS,
};
// SAFETY: `c_name` is a NUL-terminated string and `how` a live `open_how` of the size passed, both outliving the call;
// `dir` is an open descriptor.
let result: libc::c_long = unsafe { libc::syscall(libc::SYS_openat2, dir.as_raw_fd(), c_name.as_ptr(), &how as *const OpenHow, std::mem::size_of::<OpenHow>()) };
if result < 0 {
return Err(std::io::Error::last_os_error());
}
// SAFETY: `openat2` returned a new descriptor that nothing else owns.
return Ok(File::from(unsafe { OwnedFd::from_raw_fd(result as libc::c_int) }));
}
/// Open the directory at the canonical absolute path `path` as the root of a walk.
///
/// Why: the ancestors of a path are resolved by the kernel and may be links, so the open directory's own path, read back
/// from the descriptor, must equal the path asked for; a path with a link among its ancestors is refused.
pub fn open_root(path: &Path) -> std::io::Result<File> {
if !path.is_absolute() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is not absolute", path.display())));
}
let name = CString::new(path.as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a path holds a NUL byte"); })?;
// SAFETY: `name` is a NUL-terminated string that outlives the call.
let descriptor: libc::c_int = checked(unsafe { libc::open(name.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC) })?;
// SAFETY: `open` returned a new descriptor that nothing else owns.
let held: File = File::from(unsafe { OwnedFd::from_raw_fd(descriptor) });
let real: PathBuf = std::fs::read_link(handle_path(&held))?;
if real.as_os_str() != path.as_os_str() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is reached through a link: it is {}", path.display(), real.display())));
}
return Ok(held);
}
/// Open the subdirectory `name` of `parent`, refusing a link, a mount point and `.` or `..`.
pub fn open_directory_at(parent: &File, name: &OsStr) -> std::io::Result<File> {
return open_beneath(parent, name, libc::O_RDONLY | libc::O_DIRECTORY, 0);
}
/// Open the directory at `names` below `root`, one component at a time, refusing a link, a mount point or a directory on
/// another device (a subvolume of a btrfs filesystem has its own).
pub fn open_directory_beneath(root: &File, names: &[OsString]) -> std::io::Result<File> {
let device: u64 = device_of(root)?;
let mut current: File = root.try_clone()?;
for name in names {
let next: File = open_directory_at(¤t, name.as_os_str())?;
if device_of(&next)? != device {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is on another device", name.to_string_lossy())));
}
current = next;
}
return Ok(current);
}
/// The device number of an open file.
pub fn device_of(file: &File) -> std::io::Result<u64> {
use std::os::unix::fs::MetadataExt;
return Ok(file.metadata()?.dev());
}
/// Open the regular file `name` of `dir` for reading, refusing a link; a fifo or a device is refused later by its type.
pub fn open_file_at(dir: &File, name: &OsStr) -> std::io::Result<File> {
return open_beneath(dir, name, libc::O_RDONLY | libc::O_NONBLOCK, 0);
}
/// Create the file `name` in `dir` with `mode`, failing when anything is there, a link included.
pub fn create_file_at(dir: &File, name: &OsStr, mode: u32) -> std::io::Result<File> {
return open_beneath(dir, name, libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL, mode);
}
/// Create the directory `name` in `dir` with `mode`; `Ok(true)` when it was made, `Ok(false)` when a directory was
/// already there (a link or a file in the way is an error).
pub fn make_directory_at(dir: &File, name: &OsStr, mode: u32) -> std::io::Result<bool> {
let c_name = name_of(name)?;
// SAFETY: `c_name` outlives the call and `dir` is an open descriptor.
let result: libc::c_int = unsafe { libc::mkdirat(dir.as_raw_fd(), c_name.as_ptr(), mode as libc::mode_t) };
if result == 0 {
return Ok(true);
}
let error: std::io::Error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::AlreadyExists {
drop(open_directory_at(dir, name)?);
return Ok(false);
}
return Err(error);
}
/// Create the link `name` in `dir` pointing at `target`.
pub fn symlink_at(target: &Path, dir: &File, name: &OsStr) -> std::io::Result<()> {
let c_name = name_of(name)?;
let c_target = CString::new(target.as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a link target holds a NUL byte"); })?;
// SAFETY: both strings outlive the call and `dir` is an open descriptor.
checked(unsafe { libc::symlinkat(c_target.as_ptr(), dir.as_raw_fd(), c_name.as_ptr()) })?;
return Ok(());
}
/// The target of the link `name` in `dir`.
pub fn read_link_at(dir: &File, name: &OsStr) -> std::io::Result<PathBuf> {
use std::os::unix::ffi::OsStringExt;
let c_name = name_of(name)?;
let mut buffer: Vec<u8> = vec![0; 4096];
// SAFETY: `c_name` outlives the call, `buffer` has the length passed, and `dir` is an open descriptor.
let length: libc::ssize_t = unsafe { libc::readlinkat(dir.as_raw_fd(), c_name.as_ptr(), buffer.as_mut_ptr().cast::<libc::c_char>(), buffer.len()) };
if length < 0 {
return Err(std::io::Error::last_os_error());
}
buffer.truncate(usize::try_from(length).unwrap_or(0));
return Ok(PathBuf::from(OsString::from_vec(buffer)));
}
/// The path through which a held directory handle names that directory, for tools that take a path.
pub fn handle_path(directory: &File) -> PathBuf {
return Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string());
}
/// The metadata of the entry `name` in `dir`, without following a link.
pub fn entry_metadata_at(dir: &File, name: &OsStr) -> std::io::Result<std::fs::Metadata> {
// The name is one plain component, so the lookup below the descriptor's magic link resolves nothing else.
name_of(name)?;
return std::fs::symlink_metadata(handle_path(dir).join(name));
}
/// Set the access and modification times of the entry `name` in `dir`, without following a link.
pub fn set_times_at(dir: &File, name: &OsStr, times: [libc::timespec; 2]) -> std::io::Result<()> {
let c_name = name_of(name)?;
// SAFETY: `c_name` outlives the call, `times` holds the two timestamps the call reads, and `dir` is an open descriptor.
checked(unsafe { libc::utimensat(dir.as_raw_fd(), c_name.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW) })?;
return Ok(());
}
/// Remove the entry `name` from `dir`: a directory when `directory` is set (it must be empty), else a file or a link.
///
/// Why: this removes whatever the name holds now; use `remove_owned_at` for an object that was made earlier.
pub fn remove_at(dir: &File, name: &OsStr, directory: bool) -> std::io::Result<()> {
let c_name = name_of(name)?;
let flags: libc::c_int = if directory { libc::AT_REMOVEDIR } else { 0 };
// SAFETY: `c_name` outlives the call and `dir` is an open descriptor.
checked(unsafe { libc::unlinkat(dir.as_raw_fd(), c_name.as_ptr(), flags) })?;
return Ok(());
}
/// Rename `from` of `from_dir` to `to` of `to_dir`; with `replace` false an existing `to` is an error (`RENAME_NOREPLACE`).
pub fn rename_at(from_dir: &File, from: &OsStr, to_dir: &File, to: &OsStr, replace: bool) -> std::io::Result<()> {
let c_from = name_of(from)?;
let c_to = name_of(to)?;
let flags: libc::c_uint = if replace { 0 } else { libc::RENAME_NOREPLACE };
// SAFETY: both names outlive the call and both directories are open descriptors.
checked(unsafe { libc::renameat2(from_dir.as_raw_fd(), c_from.as_ptr(), to_dir.as_raw_fd(), c_to.as_ptr(), flags) })?;
return Ok(());
}
/// Exchange the entries `first` and `second` of `dir` atomically (`RENAME_EXCHANGE`).
pub fn exchange_at(dir: &File, first: &OsStr, second: &OsStr) -> std::io::Result<()> {
let c_first = name_of(first)?;
let c_second = name_of(second)?;
// SAFETY: both names outlive the call and `dir` is an open descriptor.
checked(unsafe { libc::renameat2(dir.as_raw_fd(), c_first.as_ptr(), dir.as_raw_fd(), c_second.as_ptr(), libc::RENAME_EXCHANGE) })?;
return Ok(());
}
/// Build an identity from the fields of `statx`.
fn id_of_statx(found: &libc::statx) -> ObjectId {
let birth: Option<(i64, u32)> = if found.stx_mask & libc::STATX_BTIME != 0 { Some((found.stx_btime.tv_sec, found.stx_btime.tv_nsec)) } else { None };
return ObjectId { device: libc::makedev(found.stx_dev_major, found.stx_dev_minor), inode: found.stx_ino, birth };
}
/// The identity of the entry `name` in `dir`, without following a link (`name` empty means `dir` itself).
fn identity_statx(dir: &File, name: Option<&OsStr>) -> std::io::Result<ObjectId> {
let (c_name, flags): (CString, libc::c_int) = match name {
Some(plain) => (name_of(plain)?, libc::AT_SYMLINK_NOFOLLOW),
None => (CString::default(), libc::AT_EMPTY_PATH),
};
// SAFETY: an all-zero `statx` is a valid value for the call to fill.
let mut found: libc::statx = unsafe { std::mem::zeroed() };
// SAFETY: `c_name` outlives the call, `found` is a live `statx`, and `dir` is an open descriptor.
checked(unsafe { libc::statx(dir.as_raw_fd(), c_name.as_ptr(), flags, libc::STATX_BASIC_STATS | libc::STATX_BTIME, &mut found) })?;
return Ok(id_of_statx(&found));
}
/// The identity of an open file or directory.
pub fn identity_of(file: &File) -> std::io::Result<ObjectId> {
return identity_statx(file, None);
}
/// The identity of the entry `name` in `dir`, a link not followed.
pub fn identity_at(dir: &File, name: &OsStr) -> std::io::Result<ObjectId> {
return identity_statx(dir, Some(name));
}
/// What a request to remove an entry that the caller made did.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Owned {
/// The entry was removed.
Removed,
/// Nothing was at the name.
Absent,
/// The name held another object and was left alone, for this reason.
Kept(String),
}
/// What:
/// Remove the entry `name` of `dir` if, at the moment of the look, it is the object with identity `expected`.
/// Why:
/// This is the one removal by name the ladder keeps, for the lock file it created and still holds open: it is what Git
/// itself does with its own lock. A name can be taken between the look and the unlink, and nothing in the kernel makes the
/// two one step, so the function is used only for a lock that no other writer can have replaced without breaking the lock
/// protocol, and never to remove a tree: a directory is refused. Everything else the ladder made stays and is reported.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await removeIfIdentical(dir, 'index.lock', expected);
/// ```
pub fn remove_if_identical(dir: &File, name: &OsStr, expected: ObjectId) -> std::io::Result<Owned> {
let found: ObjectId = match identity_at(dir, name) {
Ok(known) => known,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Owned::Absent),
Err(error) => return Err(error),
};
if found != expected {
return Ok(Owned::Kept(format!("{} is not the object that was made ({} against {})", name.to_string_lossy(), found.encode(), expected.encode())));
}
if entry_metadata_at(dir, name)?.is_dir() {
return Ok(Owned::Kept(format!("{} is a directory and is not removed", name.to_string_lossy())));
}
return match remove_at(dir, name, false) {
Ok(()) => Ok(Owned::Removed),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Owned::Absent),
Err(error) => Err(error),
};
}
/// What:
/// Make the open file `source` another name `name` of `dir`, failing if the name exists.
/// Why:
/// The object published is the one the descriptor holds, not whatever a separately checked name refers to at the moment of a
/// rename, and an existing entry is never replaced (`linkat` fails with `EEXIST`).
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await linkOpenFileAt(file, dir, 'index');
/// ```
pub fn link_open_file_at(source: &File, dir: &File, name: &OsStr) -> std::io::Result<()> {
let c_name = name_of(name)?;
let through = CString::new(handle_path(source).as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a path holds a NUL byte"); })?;
// SAFETY: both strings outlive the call, `dir` is an open descriptor, and `AT_SYMLINK_FOLLOW` makes the link to the file
// the descriptor's magic link names.
checked(unsafe { libc::linkat(libc::AT_FDCWD, through.as_ptr(), dir.as_raw_fd(), c_name.as_ptr(), libc::AT_SYMLINK_FOLLOW) })?;
return Ok(());
}
/// Controls stay out of the release executable.
#[cfg(test)]
#[path = "fs_beneath_tests.rs"]
mod tests;