1use 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
15pub 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 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
102fn 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
114pub 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#[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}