Skip to main content

aria_primitives/
local_socket.rs

1//! Exclusive ownership of a private Unix listener and its filesystem paths.
2
3use std::fs::{self, File, Metadata, Permissions, TryLockError};
4use std::io;
5use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
6use std::os::unix::net::UnixListener;
7use std::path::{Path, PathBuf};
8
9use rustix::fs::{Mode, OFlags};
10
11use crate::private_fs::{self, PRIVATE_FILE_MODE, PrivateFileError};
12
13type Result<T> = std::result::Result<T, LocalSocketError>;
14
15/// A private Unix listener held under an advisory ownership lock.
16///
17/// The lock is acquired before a stale socket is removed and remains held for
18/// the listener's lifetime. Cleanup removes the socket path only while it still
19/// identifies this listener.
20pub struct ExclusiveListener {
21    listener: UnixListener,
22    socket_path: PathBuf,
23    socket_identity: SocketIdentity,
24    _lock: File,
25}
26
27impl ExclusiveListener {
28    pub fn bind(socket_path: impl AsRef<Path>, lock_path: impl AsRef<Path>) -> Result<Self> {
29        let lock = acquire_ownership(lock_path.as_ref())?;
30        let socket_path = socket_path.as_ref().to_path_buf();
31        remove_stale_socket(&socket_path)?;
32        let listener = bind_listener(&socket_path)?;
33        let socket_identity =
34            socket_identity(&socket_path)?.ok_or_else(|| LocalSocketError::SocketDisappeared {
35                path: socket_path.clone(),
36            })?;
37        let listener = Self {
38            listener,
39            socket_path,
40            socket_identity,
41            _lock: lock,
42        };
43
44        listener.configure()?;
45        listener.secure()?;
46        listener.verify_identity()?;
47        Ok(listener)
48    }
49
50    /// The bound listener, for a caller that accepts through its own reactor.
51    /// This handle keeps owning the lock and the socket path.
52    pub fn listener(&self) -> &UnixListener {
53        &self.listener
54    }
55
56    pub fn path(&self) -> &Path {
57        &self.socket_path
58    }
59
60    fn configure(&self) -> Result<()> {
61        self.listener
62            .set_nonblocking(true)
63            .map_err(|source| LocalSocketError::ConfigureSocket {
64                path: self.socket_path.clone(),
65                source,
66            })
67    }
68
69    fn secure(&self) -> Result<()> {
70        fs::set_permissions(&self.socket_path, Permissions::from_mode(PRIVATE_FILE_MODE)).map_err(
71            |source| LocalSocketError::SecureSocket {
72                path: self.socket_path.clone(),
73                source,
74            },
75        )
76    }
77
78    fn verify_identity(&self) -> Result<()> {
79        if socket_identity(&self.socket_path)? != Some(self.socket_identity) {
80            return Err(LocalSocketError::SocketChanged {
81                path: self.socket_path.clone(),
82            });
83        }
84        Ok(())
85    }
86}
87
88fn acquire_ownership(path: &Path) -> Result<File> {
89    let file = open_lock(path)?;
90    private_fs::prepare_file(&file).map_err(|source| LocalSocketError::UnusableLock {
91        path: path.to_path_buf(),
92        source,
93    })?;
94    file.try_lock().map_err(|error| match error {
95        TryLockError::WouldBlock => LocalSocketError::AlreadyOwned,
96        TryLockError::Error(error) => LocalSocketError::AcquireLock(error),
97    })?;
98    write_pid(&file, path)?;
99    Ok(file)
100}
101
102/// The lock doubles as a pidfile: a controller that wants the owner gone
103/// reads the PID here and signals it. Only meaningful while the lock is
104/// held — after a crash the contents are stale until the next owner writes.
105fn write_pid(mut file: &File, path: &Path) -> Result<()> {
106    let error = |source: io::Error| LocalSocketError::WritePid {
107        path: path.to_path_buf(),
108        source,
109    };
110    file.set_len(0).map_err(error)?;
111    io::Write::write_all(&mut file, std::process::id().to_string().as_bytes()).map_err(error)
112}
113
114/// The PID recorded by [`ExclusiveListener::bind`], or `None` when the lock
115/// is missing or holds no number. Only meaningful while the socket answers;
116/// the caller establishes that first.
117pub fn owner_pid(lock_path: &Path) -> Option<u32> {
118    fs::read_to_string(lock_path).ok()?.trim().parse().ok()
119}
120
121fn open_lock(path: &Path) -> Result<File> {
122    let flags =
123        OFlags::WRONLY | OFlags::CREATE | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK;
124    let descriptor = rustix::fs::open(path, flags, Mode::RUSR | Mode::WUSR).map_err(|source| {
125        LocalSocketError::OpenLock {
126            path: path.to_path_buf(),
127            source: source.into(),
128        }
129    })?;
130    Ok(File::from(descriptor))
131}
132
133fn remove_stale_socket(path: &Path) -> Result<()> {
134    if socket_identity(path)?.is_none() {
135        return Ok(());
136    }
137
138    fs::remove_file(path).map_err(|source| LocalSocketError::RemoveStaleSocket {
139        path: path.to_path_buf(),
140        source,
141    })
142}
143
144fn bind_listener(path: &Path) -> Result<UnixListener> {
145    UnixListener::bind(path).map_err(|source| LocalSocketError::BindSocket {
146        path: path.to_path_buf(),
147        source,
148    })
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152struct SocketIdentity {
153    device: u64,
154    inode: u64,
155}
156
157impl SocketIdentity {
158    fn from_metadata(metadata: &Metadata) -> Self {
159        Self {
160            device: metadata.dev(),
161            inode: metadata.ino(),
162        }
163    }
164}
165
166fn socket_identity(path: &Path) -> Result<Option<SocketIdentity>> {
167    let metadata = match fs::symlink_metadata(path) {
168        Ok(metadata) => metadata,
169        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
170        Err(source) => {
171            return Err(LocalSocketError::InspectSocket {
172                path: path.to_path_buf(),
173                source,
174            });
175        }
176    };
177
178    validate_socket(path, &metadata)?;
179    Ok(Some(SocketIdentity::from_metadata(&metadata)))
180}
181
182fn validate_socket(path: &Path, metadata: &Metadata) -> Result<()> {
183    if !metadata.file_type().is_socket() {
184        return Err(LocalSocketError::NotASocket {
185            path: path.to_path_buf(),
186        });
187    }
188
189    let expected_uid = rustix::process::getuid().as_raw();
190    if metadata.uid() != expected_uid {
191        return Err(LocalSocketError::WrongSocketOwner {
192            path: path.to_path_buf(),
193            expected_uid,
194            actual_uid: metadata.uid(),
195        });
196    }
197    Ok(())
198}
199
200impl Drop for ExclusiveListener {
201    fn drop(&mut self) {
202        match socket_identity(&self.socket_path) {
203            Ok(Some(identity)) if identity == self.socket_identity => {
204                if let Err(error) = fs::remove_file(&self.socket_path) {
205                    tracing::warn!(
206                        path = %self.socket_path.display(),
207                        %error,
208                        "failed to remove local socket"
209                    );
210                }
211            }
212            Ok(None) => {}
213            Ok(Some(_)) => {
214                tracing::warn!(
215                    path = %self.socket_path.display(),
216                    "leaving local socket path because its identity changed"
217                );
218            }
219            Err(error) => {
220                tracing::warn!(
221                    path = %self.socket_path.display(),
222                    %error,
223                    "leaving local socket path because it could not be verified"
224                );
225            }
226        }
227    }
228}
229
230// Two paths are in play: the lock file that grants ownership, and the socket.
231// Every variant names which of them it means.
232#[derive(Debug, thiserror::Error)]
233pub enum LocalSocketError {
234    #[error("failed to open local socket lock file {}", path.display())]
235    OpenLock {
236        path: PathBuf,
237        #[source]
238        source: io::Error,
239    },
240
241    #[error("local socket lock file {} is not usable", path.display())]
242    UnusableLock {
243        path: PathBuf,
244        #[source]
245        source: PrivateFileError,
246    },
247
248    #[error("local socket endpoint is already owned by another process")]
249    AlreadyOwned,
250
251    #[error("failed to acquire local socket ownership lock")]
252    AcquireLock(#[source] io::Error),
253
254    #[error("failed to record the owner PID in lock file {}", path.display())]
255    WritePid {
256        path: PathBuf,
257        #[source]
258        source: io::Error,
259    },
260
261    #[error("failed to inspect local socket path {}", path.display())]
262    InspectSocket {
263        path: PathBuf,
264        #[source]
265        source: io::Error,
266    },
267
268    #[error("local socket path is not a Unix socket: {}", path.display())]
269    NotASocket { path: PathBuf },
270
271    #[error(
272        "local socket {} belongs to uid {actual_uid}, expected uid {expected_uid}",
273        path.display()
274    )]
275    WrongSocketOwner {
276        path: PathBuf,
277        expected_uid: u32,
278        actual_uid: u32,
279    },
280
281    #[error("failed to remove stale local socket {}", path.display())]
282    RemoveStaleSocket {
283        path: PathBuf,
284        #[source]
285        source: io::Error,
286    },
287
288    #[error("failed to bind local socket {}", path.display())]
289    BindSocket {
290        path: PathBuf,
291        #[source]
292        source: io::Error,
293    },
294
295    #[error("failed to configure local socket {}", path.display())]
296    ConfigureSocket {
297        path: PathBuf,
298        #[source]
299        source: io::Error,
300    },
301
302    #[error("local socket disappeared while binding: {}", path.display())]
303    SocketDisappeared { path: PathBuf },
304
305    #[error("failed to secure local socket permissions at {}", path.display())]
306    SecureSocket {
307        path: PathBuf,
308        #[source]
309        source: io::Error,
310    },
311
312    #[error("local socket path changed while binding: {}", path.display())]
313    SocketChanged { path: PathBuf },
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn the_lock_file_records_the_owner_pid() {
322        let directory = tempfile::tempdir().expect("test directory is created");
323        let lock_path = directory.path().join("daemon.lock");
324        let _listener = ExclusiveListener::bind(directory.path().join("daemon.sock"), &lock_path)
325            .expect("the listener should bind");
326
327        let recorded = fs::read_to_string(&lock_path).expect("the lock file should be readable");
328
329        assert_eq!(recorded, std::process::id().to_string());
330        assert_eq!(owner_pid(&lock_path), Some(std::process::id()));
331    }
332
333    #[test]
334    fn a_missing_or_malformed_lock_has_no_owner() {
335        let directory = tempfile::tempdir().expect("test directory is created");
336        let lock_path = directory.path().join("daemon.lock");
337
338        assert_eq!(owner_pid(&lock_path), None);
339
340        fs::write(&lock_path, "not a pid\n").expect("the lock file should be writable");
341        assert_eq!(owner_pid(&lock_path), None);
342
343        fs::write(&lock_path, " 4242\n").expect("the lock file should be writable");
344        assert_eq!(owner_pid(&lock_path), Some(4242));
345    }
346}