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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
use crate::{
fds::wopen_cloexec,
flog, flogf,
path::{path_remoteness, DirRemoteness},
prelude::*,
wutil::{file_id_for_file, file_id_for_path, wdirname, wunlink, FileId, INVALID_FILE_ID},
};
use fish_tempfile::random_filename;
use fish_widestring::{osstr2wcstring, wcs2bytes, wcs2osstring};
use libc::{c_int, LOCK_EX, LOCK_SH};
use nix::{fcntl::OFlag, sys::stat::Mode};
use std::{
ffi::OsString,
fs::{File, OpenOptions},
os::{
fd::AsRawFd as _,
unix::{ffi::OsStringExt as _, fs::MetadataExt as _},
},
path::PathBuf,
};
/// Creates a temporary file in the same directory as as `original_path`, meaning `original_path`
/// must be a valid file path. The filename will be created by appending random alphanumeric ASCII
/// chars to the `original_filename`.
fn create_temporary_file(original_path: &wstr) -> std::io::Result<(File, WString)> {
let original_path = PathBuf::from(OsString::from_vec(wcs2bytes(original_path)));
// original path must be a valid file path, so file_name should never return None.
let prefix = original_path.file_name().unwrap().to_owned();
let dir = original_path.parent().unwrap();
let (path, result) =
fish_tempfile::create_file_with_retry(|| dir.join(random_filename(prefix.clone())));
match result {
Ok(file) => Ok((file, osstr2wcstring(path))),
Err(e) => {
flog!(
error,
wgettext_fmt!(
"Unable to create temporary file '%s': %s",
// TODO(MSRV>=1.87): use OsString::display()
format!("{:?}", path),
e
)
);
Err(e)
}
}
}
/// Use this struct for all accesses to file which need mutual exclusion.
/// Otherwise, races on the file are possible.
/// The lock is released when this struct is dropped.
pub struct LockedFile {
/// This is the file which requires mutual exclusion.
/// It should only be accessed through this struct,
/// because the locks used here do not protect from other accesses to the file.
data_file: File,
/// The file descriptor of the parent directory, used for locking.
/// The lock is not placed on the data file directly due to issues with renaming.
/// If the data file is renamed after opening and before locking it,
/// There are two independent files around, whose locks do not interact.
/// In some cases this can be identified by checking file identifiers and timestamps,
/// but even with such checks races and corresponding file corruption can occur.
/// It is simpler to lock a different path, which does not change.
///
/// We may fail to lock (e.g. on lockless NFS - see issue #685.
/// In that case, we proceed as if locking succeeded.
/// This might result in corruption,
/// but the alternative of not being able to access the file at all is not desirable either.
_locked_fd: File,
}
pub const LOCKED_FILE_MODE: Mode = Mode::from_bits_truncate(0o600);
pub enum LockingMode {
Shared,
Exclusive(WriteMethod),
}
pub enum WriteMethod {
Append,
RenameIntoPlace,
}
impl LockingMode {
pub fn flock_op(&self) -> c_int {
match self {
Self::Shared => LOCK_SH,
Self::Exclusive(_) => LOCK_EX,
}
}
pub fn file_flags(&self) -> OFlag {
match self {
Self::Shared => OFlag::O_RDONLY,
Self::Exclusive(WriteMethod::Append) => {
OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT
}
Self::Exclusive(WriteMethod::RenameIntoPlace) => OFlag::O_RDWR | OFlag::O_CREAT,
}
}
}
impl LockedFile {
/// Creates a [`LockedFile`].
/// Use this for any access to a which requires mutual exclusion, as it ensures correct locking.
/// Use [`LockingMode::Exclusive`] if you want to modify the file in any way.
/// Otherwise you should use [`LockingMode::Shared`].
/// Two modes of modification are supported:
/// - Appending
/// - Writing to a temporary file which is then renamed into place.
/// File flags are derived from the [`LockingMode`].
/// `file_name` should just be a name, not a full path.
pub fn new(locking_mode: LockingMode, file_path: &wstr) -> std::io::Result<Self> {
let dir_path = wdirname(file_path);
if path_remoteness(dir_path) == DirRemoteness::Remote {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Directory considered remote. Locking is disabled on remote file systems.",
));
}
// We start by locking the directory.
// This is required to avoid racing modifications by other threads/processes.
let dir_fd = wopen_cloexec(dir_path, OFlag::O_RDONLY, Mode::empty())?;
{
// Cygwin's `flock` is currently not thread safe (#11933)
#[cfg(cygwin)]
static FLOCK_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(cygwin)]
let _lock = FLOCK_LOCK.lock().unwrap();
// Try locking the directory. Retry if locking was interrupted.
while unsafe { libc::flock(dir_fd.as_raw_fd(), locking_mode.flock_op()) } == -1 {
let err = std::io::Error::last_os_error();
if err.kind() != std::io::ErrorKind::Interrupted {
return Err(err);
}
}
}
// Open the data file
let data_file = wopen_cloexec(file_path, locking_mode.file_flags(), LOCKED_FILE_MODE)?;
Ok(Self {
data_file,
_locked_fd: dir_fd,
})
}
pub fn get(&self) -> &File {
&self.data_file
}
pub fn get_mut(&mut self) -> &mut File {
&mut self.data_file
}
/// Calling `fsync` and then closing the file is required for correctness on some file systems
/// before renaming the file.
/// <https://www.comp.nus.edu.sg/~lijl/papers/ferrite-asplos16.pdf>
/// <https://archive.kernel.org/oldwiki/btrfs.wiki.kernel.org/index.php/FAQ.html#What_are_the_crash_guarantees_of_overwrite-by-rename.3F>
/// Returns the lock file so that the lock can be kept.
pub fn fsync_close_and_keep_lock(self) -> std::io::Result<File> {
fsync(&self.data_file)?;
Ok(self._locked_fd)
}
}
/// Reimplementation of `std::sys::fs::unix::File::fsync` using publicly accessible functionality.
/// This function is used instead of `sync_all` due to concerns of that being too slow on macOS,
/// since there `libc::fcntl(fd, libc::F_FULLFSYNC)` is used internally.
/// This weakens our guarantees on macOS.
pub fn fsync(file: &File) -> std::io::Result<()> {
let fd = file.as_raw_fd();
loop {
match unsafe { libc::fsync(fd) } {
0 => return Ok(()),
-1 => {
let os_error = std::io::Error::last_os_error();
if os_error.kind() != std::io::ErrorKind::Interrupted {
flogf!(synced_file_access, "fsync failed: %s", os_error);
return Err(os_error);
}
}
_ => panic!("fsync should only ever return 0 or -1"),
}
}
}
/// Runs the `load` function, which should see a consistent state of the file at `path`.
/// To ensure a consistent state, we use locks to prevent modifications by others.
/// If locking is unavailable, the `load` function might be executed multiple times,
/// until it manages a run without the file being modified in the meantime, or until the maximum
/// number of allowed attempts is reached.
/// If the file does not exist this function will return an error.
pub fn lock_and_load<F, UserData>(path: &wstr, load: F) -> std::io::Result<(FileId, UserData)>
where
F: Fn(&File, FileId) -> std::io::Result<UserData>,
{
match LockedFile::new(LockingMode::Shared, path) {
Ok(locked_file) => {
let file_id = file_id_for_file(locked_file.get());
let user_data = load(locked_file.get(), file_id.clone())?;
return Ok((file_id, user_data));
}
Err(e) => {
flogf!(
synced_file_access,
"Error acquiring shared lock on the directory of '%s': %s",
path,
e,
);
// This function might be called when the file does not exist.
if e.kind() == std::io::ErrorKind::NotFound {
// There is no point in continuing in this function if the file does not
// exist.
return Err(e);
}
}
}
flog!(
synced_file_access,
"flock-based locking is disabled. Using fallback implementation."
);
// Fallback implementation for situations where locking is unavailable.
let max_attempts = 1000;
for _ in 0..max_attempts {
// If we cannot open the file, there is nothing we can do,
// so just return immediately.
let file = wopen_cloexec(path, OFlag::O_RDONLY, Mode::empty())?;
let initial_file_id = file_id_for_file(&file);
let loaded_data = match load(&file, initial_file_id.clone()) {
Ok(update_data) => update_data,
Err(_) => {
// Retry if load function failed. Because we do not hold a lock, this might be
// caused by concurrent modifications.
continue;
}
};
let final_file_id = file_id_for_path(path);
if initial_file_id != final_file_id {
continue;
}
// If the file id did not change, we assume that we loaded a consistent state.
return Ok((final_file_id, loaded_data));
}
Err(std::io::Error::other(
"Failed to update the file. Locking is disabled, and the fallback code did not succeed within the permissible number of attempts.",
))
}
pub struct PotentialUpdate<UserData> {
pub do_save: bool,
pub data: UserData,
}
/// Use this function for updating a file based on its current content,
/// for files which might be accessed at the same time by other threads/processes.
/// The basic principle is to create a temporary file, take a lock on the file to be rewritten,
/// do the rewrite (which might involve reading the old file contents),
/// and finally release the lock.
/// Error handling (especially the case where locking is not possible) makes the implementation
/// non-straightforward.
///
/// # Arguments
///
/// - `path`: The path to the file which should be updated.
/// - `rewrite`: The function which handles reading from the file and writing to a temporary file.
/// The first argument is for the file to read from, the second for the temporary file to
/// write to. On success, the value returned by `rewrite` is included in this functions
/// return value. Be careful about side effects of `rewrite`. It might get executed multiple
/// times. Try to avoid side effects and instead extract any data you might need and return
/// them on success. Then, apply the desired side effects once this function has returned
/// successfully.
///
/// # Return value
///
/// On success, the [`FileId`] of the rewritten file is returned, alongside the value returned by
/// `rewrite`. Note that if locking is unavailable, the [`FileId`] might be the id of a different
/// version of the file, which was written after this function renamed the temporary file to `path`
/// but before we obtained the [`FileId`] from `path`. This is a race condition we do not detect.
pub fn rewrite_via_temporary_file<F, UserData>(
path: &wstr,
rewrite: F,
) -> std::io::Result<(FileId, PotentialUpdate<UserData>)>
where
F: Fn(&File, &mut File) -> std::io::Result<PotentialUpdate<UserData>>,
{
/// Updates the metadata of the `new_file` to match the `old_file`.
/// Also updates the mtime of the `new_file` to the current time manually, to work around
/// operating systems which do not update the internal time used for such time stamps
/// frequently enough. This is important for [`FileId`] comparisons.
fn update_metadata(old_file: &File, new_file: &File) {
// Ensure we maintain the ownership and permissions of the original (#2355). If the
// stat fails, we assume (hope) our default permissions are correct. This
// corresponds to e.g. someone running sudo -E as the very first command. If they
// did, it would be tricky to set the permissions correctly. (bash doesn't get this
// case right either).
if let Ok(md) = old_file.metadata() {
if let Err(e) = std::os::unix::fs::fchown(new_file, Some(md.uid()), Some(md.gid())) {
flog!(
synced_file_access,
"Error when changing ownership of file:",
e
);
}
if let Err(e) = new_file.set_permissions(md.permissions()) {
flog!(synced_file_access, "Error when changing mode of file:", e);
}
} else {
flog!(synced_file_access, "Could not get metadata for file");
}
// Linux by default stores the mtime with low precision, low enough that updates that occur
// in quick succession may result in the same mtime (even the nanoseconds field). So
// manually set the mtime of the new file to a high-precision clock. Note that this is only
// necessary because Linux aggressively reuses inodes, causing the ABA problem; on other
// platforms we tend to notice the file has changed due to a different inode.
//
// The current time within the Linux kernel is cached, and generally only updated on a timer
// interrupt. So if the timer interrupt is running at 10 milliseconds, the cached time will
// only be updated once every 10 milliseconds.
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let mut times: [libc::timespec; 2] = unsafe { std::mem::zeroed() };
times[0].tv_nsec = libc::UTIME_OMIT; // don't change atime
if unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut times[1]) } == 0 {
unsafe {
// This accesses both times[0] and times[1]. Check `utimensat(2)` for details.
libc::futimens(new_file.as_raw_fd(), ×[0]);
}
}
}
}
/// Renames a file from `old_name` to `new_name`.
fn rename(old_name: &wstr, new_name: &wstr) -> std::io::Result<()> {
if let Err(e) = std::fs::rename(wcs2osstring(old_name), wcs2osstring(new_name)) {
flog!(
error,
wgettext_fmt!("Error when renaming file: %s", e.to_string())
);
return Err(e);
}
Ok(())
}
// This contains the main body of the surrounding function.
// It is wrapped as its own function to allow unlinking the tmpfile reliably.
// This can be achieved more concisely using a `ScopeGuard` which unlinks the file when it is
// closed, but this does not work if the file should be closed before renaming, which is
// necessary for the whole process to work reliably on some filesystems.
fn try_rewriting<F, UserData>(
path: &wstr,
rewrite: F,
tmp_name: &wstr,
mut tmp_file: File,
) -> std::io::Result<(FileId, PotentialUpdate<UserData>)>
where
F: Fn(&File, &mut File) -> std::io::Result<PotentialUpdate<UserData>>,
{
// We want to rewrite the file.
// To avoid issues with crashes during writing,
// we write to a temporary file and once we are done, this file is renamed such that it
// replaces the original file.
// To avoid races, we need to have exclusive access to the file for the entire
// duration, which we get via an exclusive lock on the parent directory.
// Taking a shared lock first and later upgrading to an exclusive one could result in a
// deadlock, so we take an exclusive one immediately.
match LockedFile::new(LockingMode::Exclusive(WriteMethod::RenameIntoPlace), path) {
Ok(locked_file) => {
let potential_update = rewrite(locked_file.get(), &mut tmp_file)?;
// In case the `LockedFile` is destroyed, this variable keeps the lock file in
// scope, to prevent releasing the lock too early.
let mut _lock_file = None;
if potential_update.do_save {
update_metadata(locked_file.get(), &tmp_file);
_lock_file = Some(locked_file.fsync_close_and_keep_lock()?);
rename(tmp_name, path)?;
}
return Ok((file_id_for_path(path), potential_update));
}
Err(e) => {
flogf!(
synced_file_access,
"Error acquiring exclusive lock on the directory of '%s': %s",
path,
e,
);
}
}
// If this is reached, we assume that locking is not available so we use a fallback
// implementation which tries to avoid race conditions, but in the case of contention it is
// possible that some writes are lost.
flog!(
synced_file_access,
"flock-based locking is disabled. Using fallback implementation."
);
// Give up after this many unsuccessful attempts.
// TODO: which value, should this be a constant shared with other retrying logic?
let max_attempts = 1000;
for _ in 0..max_attempts {
// Reopen the tmpfile. This is important because it might have been closed by
// explicitly dropping it.
tmp_file = OpenOptions::new()
.write(true)
.truncate(true)
.open(wcs2osstring(tmp_name))?;
// If the file does not exist yet, this will be `INVALID_FILE_ID`.
let initial_file_id = file_id_for_path(path);
// If we cannot open the file, there is nothing we can do,
// so just return immediately.
let old_file = wopen_cloexec(path, OFlag::O_RDONLY | OFlag::O_CREAT, LOCKED_FILE_MODE)?;
let opened_file_id = file_id_for_file(&old_file);
if initial_file_id != INVALID_FILE_ID && initial_file_id != opened_file_id {
// File ID changed (and not just because the file was created by us).
continue;
}
let Ok(potential_update) = rewrite(&old_file, &mut tmp_file) else {
// Retry if rewrite function failed. Because we do not hold a lock, this might be
// caused by concurrent modifications.
continue;
};
if potential_update.do_save {
update_metadata(&old_file, &tmp_file);
// fsync + close as described in
// https://archive.kernel.org/oldwiki/btrfs.wiki.kernel.org/index.php/FAQ.html#What_are_the_crash_guarantees_of_overwrite-by-rename.3F
fsync(&tmp_file)?;
std::mem::drop(tmp_file);
}
let mut final_file_id = file_id_for_path(path);
if opened_file_id != final_file_id {
continue;
}
// If we reach this point, the file ID did not change while we read the old file and wrote
// to `tmp_file`. Now we replace the old file with the `tmp_file`.
// Note that we cannot prevent races here.
// If the file is modified by someone else between the syscall for determining the [`FileId`]
// and the rename syscall, these modifications will be lost.
if potential_update.do_save {
// Do not retry on rename failures, as it is unlikely that these will disappear if we retry.
rename(tmp_name, path)?;
final_file_id = file_id_for_path(path);
}
// Note that this might not match the version of the file we just wrote.
// (If we did write.)
return Ok((final_file_id, potential_update));
}
Err(std::io::Error::other(
"Failed to update the file. Locking is disabled, and the fallback code did not succeed within the permissible number of attempts.",
))
}
let (tmp_file, tmp_name) = create_temporary_file(path)?;
let result = try_rewriting(path, rewrite, &tmp_name, tmp_file);
// Do not leave the tmpfile around.
// Note that we do not unlink when renaming succeeded.
// In that case, it would be unnecessary, because the file will no longer exist at the path,
// but it also enables a race condition, where after renaming succeeded, a different fish
// instance creates a tmpfile with the same name (unlikely but possible), which we would then
// delete here.
if result.is_err()
|| result
.as_ref()
.is_ok_and(|(_file_id, potential_update)| !potential_update.do_save)
{
let _ = wunlink(&tmp_name);
}
result
}