1
#![cfg_attr(docsrs, feature(doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![warn(missing_docs)]
7
#![warn(noop_method_call)]
8
#![warn(unreachable_pub)]
9
#![warn(clippy::all)]
10
#![deny(clippy::await_holding_lock)]
11
#![deny(clippy::cargo_common_metadata)]
12
#![deny(clippy::cast_lossless)]
13
#![deny(clippy::checked_conversions)]
14
#![allow(clippy::cognitive_complexity)] // See arti#2556
15
#![deny(clippy::debug_assert_with_mut_call)]
16
#![deny(clippy::exhaustive_enums)]
17
#![deny(clippy::exhaustive_structs)]
18
#![deny(clippy::expl_impl_clone_on_copy)]
19
#![deny(clippy::fallible_impl_from)]
20
#![deny(clippy::implicit_clone)]
21
#![deny(clippy::large_stack_arrays)]
22
#![warn(clippy::manual_ok_or)]
23
#![deny(clippy::missing_docs_in_private_items)]
24
#![warn(clippy::needless_borrow)]
25
#![warn(clippy::needless_pass_by_value)]
26
#![warn(clippy::option_option)]
27
#![warn(clippy::or_fun_call)]
28
#![deny(clippy::print_stderr)]
29
#![deny(clippy::print_stdout)]
30
#![warn(clippy::rc_buffer)]
31
#![deny(clippy::ref_option_ref)]
32
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![warn(clippy::trait_duplication_in_bounds)]
34
#![deny(clippy::unchecked_time_subtraction)]
35
#![deny(clippy::unnecessary_wraps)]
36
#![warn(clippy::unseparated_literal_suffix)]
37
#![deny(clippy::unwrap_used)]
38
#![deny(clippy::mod_module_files)]
39
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
40
#![allow(clippy::uninlined_format_args)]
41
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
42
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
43
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
44
#![allow(clippy::needless_lifetimes)] // See arti#1765
45
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
46
#![allow(clippy::collapsible_if)] // See arti#2342
47
#![deny(clippy::unused_async)]
48
#![deny(clippy::string_slice)] // See arti#2571
49
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
50
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
51

            
52
use std::{fs, path::Path};
53

            
54
/// A lock-file for which we hold the lock.
55
///
56
/// So long as this object exists, we hold the lock on this file.
57
/// When it is dropped, we will release the lock.
58
///
59
/// # Semantics
60
///
61
///  * Only one `LockFileGuard` can exist at one time
62
///    for any particular `path`.
63
///  * This applies across all tasks and threads in all programs;
64
///    other acquisitions of the lock in the same process are prevented.
65
///  * This applies across even separate machines, if `path` is on a shared filesystem.
66
///
67
/// # Restrictions
68
///
69
///  * **`path` must only be deleted (or renamed) via the APIs in this module**
70
///  * This restriction applies to all programs on the computer,
71
///    so for example automatic file cleaning with `find` and `rm` is forbidden.
72
///  * Cross-filesystem locking is broken on Linux before 2.6.12.
73
#[derive(Debug)]
74
pub struct LockFileGuard {
75
    /// A [`File`](fs::File) with its exclusive lock held.
76
    ///
77
    /// This `File` instance will remain locked for as long as this
78
    /// LockFileGuard exists.
79
    locked_file: fs::File,
80
}
81

            
82
impl LockFileGuard {
83
    /// Try to open `path` with options suitable for using it as a lockfile,
84
    /// creating it as necessary.
85
12841
    fn open<P>(path: P) -> Result<fs::File, std::io::Error>
86
12841
    where
87
12841
        P: AsRef<Path>,
88
    {
89
12841
        fs::OpenOptions::new()
90
12841
            .read(true)
91
12841
            .write(true)
92
12841
            .create(true)
93
12841
            .truncate(false)
94
12841
            .open(&path)
95
12841
    }
96

            
97
    /// Try to construct a new [`LockFileGuard`] representing a lock we hold on
98
    /// the file `path`.
99
    ///
100
    /// Blocks until we can get the lock.
101
3
    pub fn lock<P>(path: P) -> Result<Self, std::io::Error>
102
3
    where
103
3
        P: AsRef<Path>,
104
    {
105
3
        let path = path.as_ref();
106
        loop {
107
3
            let file = Self::open(path)?;
108
3
            do_lock(&file)?;
109

            
110
3
            if os::lockfile_has_path(&file, path)? {
111
3
                return Ok(Self { locked_file: file });
112
            }
113
        }
114
3
    }
115

            
116
    /// Try to construct a new [`LockFileGuard`] representing a lock we hold on
117
    /// the file `path`.
118
    ///
119
    /// Does not block; returns Ok(None) if somebody else holds the lock.
120
12838
    pub fn try_lock<P>(path: P) -> Result<Option<Self>, std::io::Error>
121
12838
    where
122
12838
        P: AsRef<Path>,
123
    {
124
12838
        let path = path.as_ref();
125
12838
        let file = Self::open(path)?;
126
12838
        match do_try_lock(&file) {
127
            Ok(()) => {
128
12836
                if os::lockfile_has_path(&file, path)? {
129
12836
                    Ok(Some(Self { locked_file: file }))
130
                } else {
131
                    Ok(None)
132
                }
133
            }
134
2
            Err(fs::TryLockError::WouldBlock) => Ok(None),
135
            Err(fs::TryLockError::Error(e)) => Err(e),
136
        }
137
12838
    }
138

            
139
    /// Try to delete the lock file that we hold.
140
    ///
141
    /// The provided `path` must be the same as was passed to `lock`.
142
38
    pub fn delete_lock_file<P>(self, path: P) -> Result<(), std::io::Error>
143
38
    where
144
38
        P: AsRef<Path>,
145
    {
146
38
        let path = path.as_ref();
147
38
        if os::lockfile_has_path(&self.locked_file, path)? {
148
38
            std::fs::remove_file(path)
149
        } else {
150
            Err(std::io::Error::other(MismatchedPathError {}))
151
        }
152
38
    }
153
}
154

            
155
impl Drop for LockFileGuard {
156
    // We pro-actively unlock the file rather than relying on drop of the File closing it.
157
    //
158
    // This is necessary on Unix because otherwise the following scenario is possible:
159
    //   0. The process has multiple threads
160
    //   1. Thread A executes fork (eg as part of spawn), and the child gets a copy of the fd,
161
    //   2. Thread B drops the `LockFileGuard` and calls close() on its copy of the fd
162
    //   3. Thread B tries to re-acquire the same lock with try_lock and fails
163
    //   4. Thread A closes the fd (via exec, or otherwise)
164
    // We want to prevent the error in step 3, which arises from a race which is possible
165
    // due to us violating the expected semantics of a guard (namely, that the lock is
166
    // synchronously released when the guard is dropped).
167
    #[allow(clippy::unnecessary_lazy_evaluations)] // we want to write the discarded error type
168
16948
    fn drop(&mut self) {
169
16948
        self.locked_file
170
16948
            .unlock()
171
            // Ignore errors from unlock.  There shouldn't be any, but if there are we
172
            // don't have anything sensible we could do with them.
173
16948
            .unwrap_or_else(|_: std::io::Error| ());
174
16948
    }
175
}
176

            
177
/// Try to lock `f`, blocking if need be.
178
///
179
/// On non-android, this just calls [`fs::File::lock`].
180
#[cfg(not(target_os = "android"))]
181
3
fn do_lock(f: &fs::File) -> std::io::Result<()> {
182
3
    f.lock()
183
3
}
184

            
185
/// Try to lock `f`, without blocking.
186
///
187
/// On non-android, this just calls [`fs::File::try_lock`].
188
#[cfg(not(target_os = "android"))]
189
17505
fn do_try_lock(f: &fs::File) -> Result<(), std::fs::TryLockError> {
190
17505
    f.try_lock()
191
17505
}
192

            
193
/// Try to lock `f`, blocking if need be.
194
///
195
/// On android, we need to use flock manually, since Rust (as of May 2026)
196
/// always returns "not implemented" for `lock()` and `try_lock()`.
197
///
198
/// See <https://github.com/rust-lang/rust/issues/148325>.
199
/// Apparently,
200
/// although there are filesystems (specifically FUSE filesystems)
201
/// where flock won't work, it will correctly report ENOSYS
202
/// on those filesystems.
203
//
204
// TODO MSRV ????: we can remove this once Rust supports file locking on Android
205
// at our MSRV.  As of May 2026, https://github.com/rust-lang/rust/pull/157038/
206
// seems like the likeliest MR for that, but it has not been merged.
207
#[cfg(target_os = "android")]
208
fn do_lock(f: &fs::File) -> std::io::Result<()> {
209
    use std::os::fd::AsRawFd;
210

            
211
    let fd = f.as_raw_fd();
212
    // SAFETY: Since `f` is a file, it has a valid fd.
213
    let success = unsafe { libc::flock(fd, libc::LOCK_EX) } == 0;
214

            
215
    if success {
216
        Ok(())
217
    } else {
218
        Err(std::io::Error::last_os_error())
219
    }
220
}
221

            
222
/// Try to lock `f`, without blocking.
223
///
224
/// On android, we need to use flock manually, since Rust (as of May 2026)
225
/// always returns "not implemented" for `lock()` and `try_lock()`.
226
///
227
/// See <https://github.com/rust-lang/rust/issues/148325>.
228
/// Apparently,
229
/// although there are filesystems (specifically FUSE filesystems)
230
/// where flock won't work, it will correctly report ENOSYS
231
/// on those filesystems.
232
//
233
// TODO MSRV ????: See 'TODO MSRV' on do_lock above.
234
#[cfg(target_os = "android")]
235
fn do_try_lock(f: &fs::File) -> Result<(), std::fs::TryLockError> {
236
    use std::os::fd::AsRawFd;
237

            
238
    let fd = f.as_raw_fd();
239
    // SAFETY: Since `f` is a file, it has a valid fd.
240
    let success = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) } == 0;
241

            
242
    if success {
243
        Ok(())
244
    } else {
245
        let err = std::io::Error::last_os_error();
246
        if err.kind() == std::io::ErrorKind::WouldBlock {
247
            Err(std::fs::TryLockError::WouldBlock)
248
        } else {
249
            Err(std::fs::TryLockError::Error(err))
250
        }
251
    }
252
}
253

            
254
/// An error that we return when the path given to `delete_lock_file` does not
255
/// match the file we have.
256
///
257
/// Since we wrap this in an `io::Error`, it doesn't need to be public or fancy.
258
#[derive(thiserror::Error, Debug, Clone)]
259
#[error("Called delete_lock_file with a mismatched path.")]
260
struct MismatchedPathError {}
261

            
262
/// Platform module for locking protocol on Unix.
263
///
264
/// ### Locking protocol on Unix
265
///
266
/// The lock is held by an open-file iff:
267
///
268
///  * that open-file holds an `flock` `LOCK_EX` lock; and
269
///  * the directory entry for `path` refers to the same file as the open-file
270
///
271
/// `path` may only refer to a plain file, or `ENOENT`.
272
/// If `path` refers to a file,
273
/// only the lockholder may cause it to no longer refer to that file.
274
///
275
/// In principle the open-file might be shared with subprocesses.
276
/// Even a naive program can safely and correctly inherit and hold the lock,
277
/// since the lockholder only needs to not close an fd.
278
/// However uncontrolled leaking of the fd into other processes is undesirable,
279
/// as it might cause delays or even deadlocks, if those processes' inheritors live too long.
280
/// In our Rust implementation we don't support sharing the held lock
281
/// with subprocesses or different process images (ie across exec);
282
/// we use `O_CLOEXEC`.
283
///
284
/// #### Locking algorithm
285
///
286
///  1. open the file with `O_CREAT|O_RDWR`
287
///  2. `flock LOCK_EX`
288
///  3. `fstat` the open-file and `lstat` the path
289
///  4. If the inode and device numbers don't match,
290
///     close the fd and go back to the start.
291
///  5. Now we hold the lock.
292
///
293
/// Proof sketch:
294
///
295
/// If we get to point 5, we see that at point 3, we had the lock.
296
/// No-one else could cause the conditions to become false
297
/// in the meantime:
298
/// no-one else ~~can~~ may make `path` refer to a different file
299
/// since they don't hold the lock.
300
/// And, no-one else can `flock` it since the kernel prevents
301
/// a conflicting lock.
302
/// So at step 5 we must still hold the lock.
303
///
304
/// #### Unlocking algorithm
305
///
306
///  1. Close the fd.
307
///  2. Now we no longer hold the lock and others can acquire it.
308
///
309
/// This drops the open-file and
310
/// leaves the lock available for another caller.
311
///
312
/// #### Deletion algorithm
313
///
314
///  0. The lock must already be held
315
///  1. `unlink` the file
316
///  2. close the fd
317
///  3. Now we no longer hold the lock and others can acquire it.
318
///
319
/// Step 1 atomically falsifies the lock-holding condition.
320
/// We are allowed to perform it because we hold the lock.
321
///
322
/// Concurrent lockers might open the old file,
323
/// which we are about to delete.
324
/// They will acquire their `flock` (locking step 2)
325
/// after we close (deletion step 2)
326
/// and then see that they have a stale file.
327
#[cfg(unix)]
328
mod os {
329
    use std::{fs::File, os::unix::fs::MetadataExt as _, path::Path};
330

            
331
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
332
19469
    pub(crate) fn lockfile_has_path(lf: &File, path: &Path) -> std::io::Result<bool> {
333
19469
        let m1 = std::fs::metadata(path)?;
334
19469
        let m2 = lf.metadata()?;
335

            
336
19469
        Ok(m1.ino() == m2.ino() && m1.dev() == m2.dev())
337
19469
    }
338
}
339

            
340
/// Platform module for locking protocol on Windows.
341
///
342
/// The argument for correctness on Windows proceeds as for Unix, but with a
343
/// higher degree of uncertainty, since we are not sufficient Windows experts to
344
/// determine if our assumptions hold.
345
///
346
/// Here we assume as follows:
347
/// * When `File::open` calls `CreateFileW`, it gets a `HANDLE` to an open file.
348
///   As we use them, the `HANDLE` behaves
349
///   similarly to the "fd" in the Unix argument above,
350
///   and the open file behaves similarly to the "open-file".
351
///   * We assume that any differences that exist in their behavior do not
352
///     affect our correctness above.
353
/// * When `File::lock` calls `LockFileEx`, and it completes successfully,
354
///   we now have a lock on the file.
355
///   Only one lock can exist on a file at a time.
356
/// * When we compare members of `handle.metadata()` and `path.metadata()`,
357
///   the comparison will return equal if ~~and only if~~
358
///   the two files are truly the same.
359
///   * We rely on the property that a file cannot change its file_index while it is
360
///     open.
361
/// * Deleting the lock file will actually work, since `File::open` opened it with
362
///   FILE_SHARE_DELETE.  (This is the default according to the documentation
363
///   for `OpenOptionsExt::share_mode`.)
364
/// * When we delete the lock file, possibly-asynchronous ("deferred") deletion
365
///   definitely won't mean that the OS kernel violates our rule that no-one but the lockholder
366
///   is allowed to delete the file.
367
/// * The above is true even if someone with read
368
///   access to the file - eg the human user - opens it without the FILE_SHARE options.
369
/// * The same is true even if there is a virus scanner.
370
/// * The same is true even on a remote filesystem.
371
/// * If someone with read access to the file - eg the human user - opens it for reading
372
///   without FILE_SHARE options, the algorithm will still work and not fail
373
///   with a file sharing violation io error.
374
///   (Or, every program the user might use to randomly peer at files in arti's
375
///   state directory, including the equivalents of `grep -R` and backup programs,
376
///   will use suitable FILE_SHARE options.)
377
///   (If this assumption is false, the consequence is not data loss;
378
///   rather, arti would fall over.  So that would be tolerable if we don't
379
///   know how to do better, or if doing better is hard.)
380
#[cfg(windows)]
381
mod os {
382
    use std::{fs::File, mem::MaybeUninit, os::windows::io::AsRawHandle, path::Path};
383
    use windows_sys::Win32::{
384
        Foundation::HANDLE,
385
        Storage::FileSystem::{FILE_ID_INFO, FileIdInfo, GetFileInformationByHandleEx},
386
    };
387

            
388
    /// Use `GetFileInformationByHandleEx` to return a FILE_ID_INFO data for `f`.
389
    ///
390
    /// `GetFileInformationByHandleEx` is supported in Vista and later, so it
391
    /// should be fine here.  Unlike GetFileInformationByHandle, it gives
392
    /// 128-bit identifiers which are supposedly even more unique.
393
    fn get_id_info(f: &File) -> std::io::Result<FILE_ID_INFO> {
394
        let handle = f.as_raw_handle() as HANDLE;
395
        let mut info: MaybeUninit<FILE_ID_INFO> = MaybeUninit::uninit();
396
        let buffersize: u32 = std::mem::size_of::<FILE_ID_INFO>()
397
            .try_into()
398
            .expect("sizeof(FILE_ID_INFO) is ridiculously large");
399

            
400
        let info = unsafe {
401
            // SAFETY: Since `size` is the size of info, this will not write to
402
            // uninitialized memory.
403
            let rv = GetFileInformationByHandleEx(
404
                handle,
405
                FileIdInfo,
406
                info.as_mut_ptr() as _,
407
                buffersize,
408
            );
409

            
410
            if rv == 0 {
411
                return Err(std::io::Error::last_os_error());
412
            }
413

            
414
            // SAFETY: since rv was nonzero, this value is initialized.
415
            info.assume_init()
416
        };
417
        Ok(info)
418
    }
419

            
420
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
421
    pub(crate) fn lockfile_has_path(lf: &File, path: &Path) -> std::io::Result<bool> {
422
        let f2 = File::open(path)?;
423

            
424
        // Note: we would like to just use the MetadataExt methods for index and
425
        // volume serial number, but they are currently available only on
426
        // nightly: https://github.com/rust-lang/rust/issues/63010
427
        //
428
        // If they stabilize at our MSRV, _and_ the file ID is expanded to the
429
        // 128-bit version, we can use them here instead.
430

            
431
        let i1 = get_id_info(lf)?;
432
        let i2 = get_id_info(&f2)?;
433

            
434
        // This comparison is about the best we can do on Windows,
435
        // though there are caveats.
436
        //
437
        // See Raymond Chen's writeup at
438
        //   https://devblogs.microsoft.com/oldnewthing/20220128-00/?p=106201
439
        // and also see BurntSushi's caveats at
440
        //   https://github.com/BurntSushi/same-file/blob/master/src/win.rs
441
        Ok(i1.VolumeSerialNumber == i2.VolumeSerialNumber
442
            && i1.FileId.Identifier == i2.FileId.Identifier)
443
    }
444
}
445

            
446
/// Non-windows, non-unix implementation for lockfile_has_path.
447
///
448
/// For now, this implementation always reports an error.
449
/// It exists so that we can build (but not run) on wasm.
450
#[cfg(all(not(windows), not(unix)))]
451
mod os {
452
    use std::path::Path;
453

            
454
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
455
    pub(crate) fn lockfile_has_path(_lf: &std::fs::File, _path: &Path) -> std::io::Result<bool> {
456
        Err(std::io::Error::other(
457
            "fslock-guard does not support this operating system".to_string(),
458
        ))
459
    }
460
}
461

            
462
#[cfg(test)]
463
mod tests {
464
    // @@ begin test lint list maintained by maint/add_warning @@
465
    #![allow(clippy::bool_assert_comparison)]
466
    #![allow(clippy::clone_on_copy)]
467
    #![allow(clippy::dbg_macro)]
468
    #![allow(clippy::mixed_attributes_style)]
469
    #![allow(clippy::print_stderr)]
470
    #![allow(clippy::print_stdout)]
471
    #![allow(clippy::single_char_pattern)]
472
    #![allow(clippy::unwrap_used)]
473
    #![allow(clippy::unchecked_time_subtraction)]
474
    #![allow(clippy::useless_vec)]
475
    #![allow(clippy::needless_pass_by_value)]
476
    #![allow(clippy::string_slice)] // See arti#2571
477
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
478

            
479
    use crate::LockFileGuard;
480
    use std::sync::Arc;
481
    use std::thread;
482
    use test_temp_dir::test_temp_dir;
483

            
484
    #[test]
485
    fn keep_lock_file_after_drop() {
486
        test_temp_dir!().used_by(|dir| {
487
            let file = dir.join("file");
488
            let flock_guard = LockFileGuard::lock(&file).unwrap();
489
            assert!(file.try_exists().unwrap());
490
            drop(flock_guard);
491
            assert!(file.try_exists().unwrap());
492
        });
493
    }
494

            
495
    #[test]
496
    fn delete_lock_file_if_requested() {
497
        test_temp_dir!().used_by(|dir| {
498
            let file = dir.join("file");
499
            let flock_guard = LockFileGuard::lock(&file).unwrap();
500
            assert!(file.try_exists().unwrap());
501
            assert!(flock_guard.delete_lock_file(&file).is_ok());
502
            assert!(!file.try_exists().unwrap());
503
        });
504
    }
505

            
506
    #[test]
507
    fn tight_loop() {
508
        let tmp = Arc::new(test_temp_dir!());
509

            
510
        // We make several threads in case there are any cross-thread interactions
511
        // that we're not aware of.  There shouldn't be.
512
        let threads = (0..10)
513
            .map(|i| {
514
                let tmp = tmp.clone();
515
                thread::spawn(move || {
516
                    tmp.used_by(|dir| {
517
                        let file = dir.join(format!("{i}"));
518
                        for _ in 0..1000 {
519
                            // Test that the lock is immediately re-requirable after drop.
520
                            let lock: LockFileGuard =
521
                                LockFileGuard::try_lock(&file).unwrap().unwrap();
522
                            drop(lock);
523
                        }
524
                    });
525
                })
526
            })
527
            .collect::<Vec<_>>();
528

            
529
        for t in threads {
530
            t.join().unwrap_or_else(|e| std::panic::resume_unwind(e));
531
        }
532
    }
533

            
534
    #[test]
535
    #[cfg(unix)]
536
    fn fork_leak_fds() {
537
        use std::ffi::c_int;
538

            
539
        let tmp = test_temp_dir!();
540

            
541
        tmp.used_by(|tmp| {
542
            let file = tmp.join("lock");
543
            let lock = LockFileGuard::lock(&file).unwrap();
544

            
545
            let child = unsafe {
546
                // It would be nicer to do this with std's Command, but
547
                // we'd have to use the unsafe pre-exec hook for synchronisation
548
                // and anyway that runs after Command's impl has closed "unwanted" fds.
549
                match libc::fork() {
550
                    -1 => panic!("fork failed"),
551
                    0 => {
552
                        libc::usleep(10_000);
553
                        libc::_exit(0);
554
                    }
555
                    child => child,
556
                }
557
            };
558

            
559
            drop(lock);
560
            let _lock: LockFileGuard = LockFileGuard::try_lock(&file).unwrap().unwrap();
561

            
562
            unsafe {
563
                let mut status: c_int = 0;
564
                let got = libc::waitpid(child, (&mut status) as *mut _, 0);
565
                assert_eq!(got, child);
566
                assert_eq!(status, 0, "{status}");
567
            }
568
        });
569
    }
570
}