1
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![cfg_attr(not(ci_arti_stable), allow(renamed_and_removed_lints))]
5
#![cfg_attr(not(ci_arti_nightly), allow(unknown_lints))]
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
#![warn(clippy::cognitive_complexity)]
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
#![deny(clippy::print_stderr)]
28
#![deny(clippy::print_stdout)]
29
#![warn(clippy::rc_buffer)]
30
#![deny(clippy::ref_option_ref)]
31
#![warn(clippy::semicolon_if_nothing_returned)]
32
#![warn(clippy::trait_duplication_in_bounds)]
33
#![deny(clippy::unchecked_duration_subtraction)]
34
#![deny(clippy::unnecessary_wraps)]
35
#![warn(clippy::unseparated_literal_suffix)]
36
#![deny(clippy::unwrap_used)]
37
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
38
#![allow(clippy::uninlined_format_args)]
39
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
40
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
41
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
42
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
43

            
44
use std::path::Path;
45

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

            
74
impl LockFileGuard {
75
    /// Try to construct a new [`LockFileGuard`] representing a lock we hold on
76
    /// the file `path`.
77
    ///
78
    /// Blocks until we can get the lock.
79
4
    pub fn lock<P>(path: P) -> Result<Self, fslock::Error>
80
4
    where
81
4
        P: AsRef<Path>,
82
4
    {
83
4
        let path = path.as_ref();
84
        loop {
85
4
            let mut lockfile = fslock::LockFile::open(path)?;
86
4
            lockfile.lock()?;
87

            
88
4
            if os::lockfile_has_path(&lockfile, path)? {
89
4
                return Ok(Self { locked: lockfile });
90
            }
91
        }
92
4
    }
93

            
94
    /// Try to construct a new [`LockFileGuard`] representing a lock we hold on
95
    /// the file `path`.
96
    ///
97
    /// Does not block; returns Ok(None) if somebody else holds the lock.
98
688
    pub fn try_lock<P>(path: P) -> Result<Option<Self>, fslock::Error>
99
688
    where
100
688
        P: AsRef<Path>,
101
688
    {
102
688
        let path = path.as_ref();
103
688
        let mut lockfile = fslock::LockFile::open(path)?;
104
688
        if lockfile.try_lock()? && os::lockfile_has_path(&lockfile, path)? {
105
686
            return Ok(Some(Self { locked: lockfile }));
106
2
        }
107
2
        Ok(None)
108
688
    }
109

            
110
    /// Try to delete the lock file that we hold.
111
    ///
112
    /// The provided `path` must be the same as was passed to `lock`.
113
76
    pub fn delete_lock_file<P>(self, path: P) -> Result<(), std::io::Error>
114
76
    where
115
76
        P: AsRef<Path>,
116
76
    {
117
76
        let path = path.as_ref();
118
76
        if os::lockfile_has_path(&self.locked, path)? {
119
76
            std::fs::remove_file(path)
120
        } else {
121
            Err(std::io::Error::new(
122
                std::io::ErrorKind::Other,
123
                MismatchedPathError {},
124
            ))
125
        }
126
76
    }
127
}
128

            
129
/// An error that we return when the path given to `delete_lock_file` does not
130
/// match the file we have.
131
///
132
/// Since we wrap this in an `io::Error`, it doesn't need to be public or fancy.
133
#[derive(thiserror::Error, Debug, Clone)]
134
#[error("Called delete_lock_file with a mismatched path.")]
135
struct MismatchedPathError {}
136

            
137
// Note: This requires AsFd and AsHandle implementations for `LockFile`.
138
//  See https://github.com/brunoczim/fslock/pull/15
139
// This is why we are using fslock-arti-fork in place of fslock.
140

            
141
/// Platform module for locking protocol on Unix.
142
///
143
/// ### Locking protocol on Unix
144
///
145
/// The lock is held by an open-file iff:
146
///
147
///  * that open-file holds an `flock` `LOCK_EX` lock; and
148
///  * the directory entry for `path` refers to the same file as the open-file
149
///
150
/// `path` may only refer to a plain file, or `ENOENT`.
151
/// If `path` refers to a file,
152
/// only the lockholder may cause it to no longer refer to that file.
153
///
154
/// In principle the open-file might be shared with subprocesses.
155
/// Even a naive program can safely and correctly inherit and hold the lock,
156
/// since the lockholder only needs to not close an fd.
157
/// However uncontrolled leaking of the fd into other processes is undesirable,
158
/// as it might cause delays or even deadlocks, if those processes' inheritors live too long.
159
/// In our Rust implementation we don't support sharing the held lock
160
/// with subprocesses or different process images (ie across exec);
161
/// we use `O_CLOEXEC`.
162
///
163
/// #### Locking algorithm
164
///
165
///  1. open the file with `O_CREAT|O_RDWR`
166
///  2. `flock LOCK_EX`
167
///  3. `fstat` the open-file and `lstat` the path
168
///  4. If the inode and device numbers don't match,
169
///     close the fd and go back to the start.
170
///  5. Now we hold the lock.
171
///
172
/// Proof sketch:
173
///
174
/// If we get to point 5, we see that at point 3, we had the lock.
175
/// No-one else could cause the conditions to become false
176
/// in the meantime:
177
/// no-one else ~~can~~ may make `path` refer to a different file
178
/// since they don't hold the lock.
179
/// And, no-one else can `flock` it since the kernel prevents
180
/// a conflicting lock.
181
/// So at step 5 we must still hold the lock.
182
///
183
/// #### Unlocking algorithm
184
///
185
///  1. Close the fd.
186
///  2. Now we no longer hold the lock and others can acquire it.
187
///
188
/// This drops the open-file and
189
/// leaves the lock available for another caller.
190
///
191
/// #### Deletion algorithm
192
///
193
///  0. The lock must already be held
194
///  1. `unlink` the file
195
///  2. close the fd
196
///  3. Now we no longer hold the lock and others can acquire it.
197
///
198
/// Step 1 atomically falsifies the lock-holding condition.
199
/// We are allowed to perform it because we hold the lock.
200
///
201
/// Concurrent lockers might open the old file,
202
/// which we are about to delete.
203
/// They will acquire their `flock` (locking step 2)
204
/// after we close (deletion step 2)
205
/// and then see that they have a stale file.
206
#[cfg(unix)]
207
mod os {
208
    use std::{fs::File, os::fd::AsFd, os::unix::fs::MetadataExt as _, path::Path};
209

            
210
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
211
4631
    pub(crate) fn lockfile_has_path(lf: &fslock::LockFile, path: &Path) -> std::io::Result<bool> {
212
4631
        let m1 = std::fs::metadata(path)?;
213
        // TODO: This does an unnecessary dup().
214
4631
        let f_dup = File::from(lf.as_fd().try_clone_to_owned()?);
215
4631
        let m2 = f_dup.metadata()?;
216

            
217
4631
        Ok(m1.ino() == m2.ino() && m1.dev() == m2.dev())
218
4631
    }
219
}
220

            
221
/// Platform module for locking protocol on Windows.
222
///
223
/// The argument for correctness on Windows proceeds as for Unix, but with a
224
/// higher degree of uncertainty, since we are not sufficient Windows experts to
225
/// determine if our assumptions hold.
226
///
227
/// Here we assume as follows:
228
/// * When `fslock` calls `CreateFileW`, it gets a `HANDLE` to an open file.
229
///   As we use them, the `HANDLE` behaves
230
///   similarly to the "fd" in the Unix argument above,
231
///   and the open file behaves similarly to the "open-file".
232
///   * We assume that any differences that exist in their behavior do not
233
///     affect our correctness above.
234
/// * When `fslock` calls `LockFileEx`, and it completes successfully,
235
///   we now have a lock on the file.
236
///   Only one lock can exist on a file at a time.
237
/// * When we compare members of `handle.metadata()` and `path.metadata()`,
238
///   the comparison will return equal if ~~and only if~~
239
///   the two files are truly the same.
240
///   * We rely on the property that a file cannot change its file_index while it is
241
///     open.
242
/// * Deleting the lock file will actually work, since `fslock` opened it with
243
///   FILE_SHARE_DELETE.
244
/// * When we delete the lock file, possibly-asynchronous ("deferred") deletion
245
///   definitely won't mean that the OS kernel violates our rule that no-one but the lockholder
246
///   is allowed to delete the file.
247
/// * The above is true even if someone with read
248
///   access to the file - eg the human user - opens it without the FILE_SHARE options.
249
/// * The same is true even if there is a virus scanner.
250
/// * The same is true even on a remote filesystem.
251
/// * If someone with read access to the file - eg the human user - opens it for reading
252
///   without FILE_SHARE options, the algorithm will still work and not fail
253
///   with a file sharing violation io error.
254
///   (Or, every program the user might use to randomly peer at files in arti's
255
///   state directory, including the equivalents of `grep -R` and backup programs,
256
///   will use suitable FILE_SHARE options.)
257
///   (If this assumption is false, the consequence is not data loss;
258
///   rather, arti would fall over.  So that would be tolerable if we don't
259
///   know how to do better, or if doing better is hard.)
260
#[cfg(windows)]
261
mod os {
262
    use std::{fs::File, mem::MaybeUninit, os::windows::io::AsRawHandle, path::Path};
263
    use winapi::um::fileapi::{GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION as Info};
264

            
265
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
266
    pub(crate) fn lockfile_has_path(lf: &fslock::LockFile, path: &Path) -> std::io::Result<bool> {
267
        let mut m1: MaybeUninit<Info> = MaybeUninit::uninit();
268
        let mut m2: MaybeUninit<Info> = MaybeUninit::uninit();
269

            
270
        let f2 = File::open(path)?;
271

            
272
        let (i1, i2) = unsafe {
273
            if GetFileInformationByHandle(lf.as_raw_handle() as _, m1.as_mut_ptr()) == 0 {
274
                return Err(std::io::Error::last_os_error());
275
            }
276
            if GetFileInformationByHandle(f2.as_raw_handle() as _, m2.as_mut_ptr()) == 0 {
277
                return Err(std::io::Error::last_os_error());
278
            }
279
            (m1.assume_init(), m2.assume_init())
280
        };
281

            
282
        // This comparison is about the best we can do on Windows,
283
        // though there are caveats.
284
        //
285
        // See Raymond Chen's writeup at
286
        //   https://devblogs.microsoft.com/oldnewthing/20220128-00/?p=106201
287
        // and also see BurntSushi's caveats at
288
        //   https://github.com/BurntSushi/same-file/blob/master/src/win.rs
289
        Ok(i1.nFileIndexHigh == i2.nFileIndexHigh
290
            && i1.nFileIndexLow == i2.nFileIndexLow
291
            && i1.dwVolumeSerialNumber == i2.dwVolumeSerialNumber)
292
    }
293
}
294

            
295
#[cfg(test)]
296
mod tests {
297
    // @@ begin test lint list maintained by maint/add_warning @@
298
    #![allow(clippy::bool_assert_comparison)]
299
    #![allow(clippy::clone_on_copy)]
300
    #![allow(clippy::dbg_macro)]
301
    #![allow(clippy::mixed_attributes_style)]
302
    #![allow(clippy::print_stderr)]
303
    #![allow(clippy::print_stdout)]
304
    #![allow(clippy::single_char_pattern)]
305
    #![allow(clippy::unwrap_used)]
306
    #![allow(clippy::unchecked_duration_subtraction)]
307
    #![allow(clippy::useless_vec)]
308
    #![allow(clippy::needless_pass_by_value)]
309
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
310

            
311
    use crate::LockFileGuard;
312
    use test_temp_dir::test_temp_dir;
313

            
314
    #[test]
315
    fn keep_lock_file_after_drop() {
316
        test_temp_dir!().used_by(|dir| {
317
            let file = dir.join("file");
318
            let flock_guard = LockFileGuard::lock(&file).unwrap();
319
            assert!(file.exists());
320
            drop(flock_guard);
321
            assert!(file.exists());
322
        });
323
    }
324

            
325
    #[test]
326
    fn delete_lock_file_if_requested() {
327
        test_temp_dir!().used_by(|dir| {
328
            let file = dir.join("file");
329
            let flock_guard = LockFileGuard::lock(&file).unwrap();
330
            assert!(file.exists());
331
            assert!(flock_guard.delete_lock_file(&file).is_ok());
332
            assert!(!file.exists());
333
        });
334
    }
335
}