Skip to main content

inferd_daemon/
lock.rs

1//! Single-instance lock for the daemon.
2//!
3//! See `THREAT_MODEL.md` F-2: pre-existing symlinks at the lock path are
4//! rejected before any open is attempted. The lock itself is acquired via
5//! `std::fs::File::try_lock` (stable since Rust 1.89), which uses
6//! `flock(LOCK_EX|LOCK_NB)` on Unix and `LockFileEx` with
7//! `LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY` on Windows.
8
9use std::fs::{File, OpenOptions};
10use std::io;
11use std::path::{Path, PathBuf};
12
13/// Errors produced when acquiring the single-instance lock.
14#[derive(Debug, thiserror::Error)]
15pub enum LockError {
16    /// The lock path pre-exists as a symlink. Refused per F-2.
17    #[error("lock path is a symlink (refused): {0}")]
18    Symlink(PathBuf),
19    /// The lock path exists and is not a regular file (e.g. a directory).
20    #[error("lock path is not a regular file: {0}")]
21    NotARegularFile(PathBuf),
22    /// Another daemon already holds the lock.
23    #[error("another inferd-daemon already holds the lock at {0}")]
24    AlreadyHeld(PathBuf),
25    /// Underlying I/O error.
26    #[error("io: {0}")]
27    Io(#[from] io::Error),
28}
29
30/// An acquired single-instance lock.
31///
32/// The lock is released when this value is dropped (closing the file
33/// descriptor releases the OS-level lock). The lock file itself is **not**
34/// deleted on drop — leaving stale lock files on disk after a crash is
35/// normal and harmless because the lock is held by the open fd, not by the
36/// file's mere existence.
37#[derive(Debug)]
38pub struct Lock {
39    _file: File,
40    path: PathBuf,
41}
42
43impl Lock {
44    /// Acquire the single-instance lock at `path`.
45    ///
46    /// The function:
47    /// 1. Calls `symlink_metadata` and refuses if the path is a symlink (F-2).
48    /// 2. Opens the file with `O_NOFOLLOW` semantics (via fresh metadata
49    ///    check then create-or-open).
50    /// 3. Calls `try_lock_exclusive`. If another process holds the lock,
51    ///    returns `LockError::AlreadyHeld`.
52    pub fn acquire(path: impl AsRef<Path>) -> Result<Self, LockError> {
53        let path = path.as_ref();
54
55        if let Ok(meta) = std::fs::symlink_metadata(path) {
56            if meta.file_type().is_symlink() {
57                return Err(LockError::Symlink(path.to_path_buf()));
58            }
59            if !meta.file_type().is_file() {
60                return Err(LockError::NotARegularFile(path.to_path_buf()));
61            }
62        }
63
64        let file = OpenOptions::new()
65            .read(true)
66            .write(true)
67            .create(true)
68            .truncate(false)
69            .open(path)?;
70
71        match file.try_lock() {
72            Ok(()) => Ok(Lock {
73                _file: file,
74                path: path.to_path_buf(),
75            }),
76            Err(std::fs::TryLockError::WouldBlock) => {
77                Err(LockError::AlreadyHeld(path.to_path_buf()))
78            }
79            Err(std::fs::TryLockError::Error(e)) => Err(LockError::Io(e)),
80        }
81    }
82
83    /// Path of the lock file (for diagnostics and tests).
84    pub fn path(&self) -> &Path {
85        &self.path
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use tempfile::tempdir;
93
94    #[test]
95    fn acquire_then_release_succeeds() {
96        let dir = tempdir().unwrap();
97        let path = dir.path().join("inferd.lock");
98        let lock = Lock::acquire(&path).unwrap();
99        assert_eq!(lock.path(), &path);
100        drop(lock);
101        // Re-acquire after drop must succeed.
102        let _again = Lock::acquire(&path).unwrap();
103    }
104
105    #[test]
106    fn second_acquire_while_held_fails() {
107        let dir = tempdir().unwrap();
108        let path = dir.path().join("inferd.lock");
109        let _first = Lock::acquire(&path).unwrap();
110        let err = Lock::acquire(&path).unwrap_err();
111        assert!(matches!(err, LockError::AlreadyHeld(_)));
112    }
113
114    // F-2: pre-existing symlinks are refused.
115    //
116    // Skipped on Windows because creating a symlink requires either developer
117    // mode or elevated privileges; the underlying invariant is identical
118    // (the same `is_symlink()` check runs on every platform).
119    #[cfg(unix)]
120    #[test]
121    fn pre_existing_symlink_is_refused() {
122        let dir = tempdir().unwrap();
123        let target = dir.path().join("target.bin");
124        std::fs::write(&target, b"x").unwrap();
125        let symlink = dir.path().join("inferd.lock");
126        std::os::unix::fs::symlink(&target, &symlink).unwrap();
127
128        let err = Lock::acquire(&symlink).unwrap_err();
129        assert!(matches!(err, LockError::Symlink(_)));
130    }
131
132    #[test]
133    fn directory_at_lock_path_refused() {
134        let dir = tempdir().unwrap();
135        let bad = dir.path().join("inferd.lock");
136        std::fs::create_dir(&bad).unwrap();
137        let err = Lock::acquire(&bad).unwrap_err();
138        assert!(matches!(err, LockError::NotARegularFile(_)));
139    }
140}