unix.rs (33731B)
1 //! Secure Unix-domain admin listener ownership. 2 3 use core::fmt; 4 use fs2::FileExt; 5 use std::error::Error; 6 use std::ffi::OsStr; 7 use std::fs::{self, File}; 8 use std::io; 9 use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; 10 use std::os::unix::net::UnixListener; 11 use std::path::{Path, PathBuf}; 12 use std::time::Duration; 13 14 use rustix::fs::{ 15 AtFlags, FileType, Gid, Mode, OFlags, chownat, fchmod, fchown, fstat, open, openat, 16 }; 17 use rustix::process::geteuid; 18 19 use super::peer::{AdminPeerAuthorizationPolicy, PeerAuthorizer}; 20 #[cfg(test)] 21 use super::test_support; 22 23 const WRITER_LOCK_FILE_NAME: &str = ".radroots-admin-writer.lock"; 24 /// Final owner-only mode for the runtime directory. 25 pub const UNIX_ADMIN_OWNER_DIRECTORY_MODE: u32 = 0o700; 26 /// Final owner-only mode for the socket path. 27 pub const UNIX_ADMIN_OWNER_SOCKET_MODE: u32 = 0o600; 28 /// Final Linux mode for a runtime directory shared with an admin group. 29 pub const UNIX_ADMIN_GROUP_DIRECTORY_MODE: u32 = 0o750; 30 /// Final Linux mode for a socket shared with an admin group. 31 pub const UNIX_ADMIN_GROUP_SOCKET_MODE: u32 = 0o660; 32 33 /// Maximum time spent proving that an existing Unix socket has a live listener. 34 pub const UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT: Duration = Duration::from_secs(1); 35 36 /// A safe, path-redacted failure from Unix admin socket ownership or binding. 37 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 38 pub enum UnixAdminSocketError { 39 RuntimeDirectoryNotAbsolute, 40 RuntimeDirectoryUnavailable { kind: io::ErrorKind }, 41 RuntimeDirectoryNotDirectory, 42 RuntimeDirectoryWrongOwner, 43 RuntimeDirectoryChanged, 44 RuntimeDirectoryPermissions { kind: io::ErrorKind }, 45 RuntimeDirectoryGroup { kind: io::ErrorKind }, 46 InvalidSocketPath, 47 WriterLockUnavailable { kind: io::ErrorKind }, 48 WriterLockInvalidType, 49 WriterLockWrongOwner, 50 WriterAlreadyActive, 51 SocketPathUnavailable { kind: io::ErrorKind }, 52 SocketPathWrongType, 53 SocketPathWrongOwner, 54 SocketActive, 55 SocketLivenessUnproven, 56 StaleSocketCleanup { kind: io::ErrorKind }, 57 SocketBind { kind: io::ErrorKind }, 58 SocketPermissions { kind: io::ErrorKind }, 59 SocketGroup { kind: io::ErrorKind }, 60 ListenerConfiguration { kind: io::ErrorKind }, 61 } 62 63 impl fmt::Display for UnixAdminSocketError { 64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 65 formatter.write_str(match self { 66 Self::RuntimeDirectoryNotAbsolute => "admin runtime directory must be absolute", 67 Self::RuntimeDirectoryUnavailable { .. } => "admin runtime directory is unavailable", 68 Self::RuntimeDirectoryNotDirectory => "admin runtime path is not a directory", 69 Self::RuntimeDirectoryWrongOwner => "admin runtime directory has the wrong owner", 70 Self::RuntimeDirectoryChanged => "admin runtime directory identity changed", 71 Self::RuntimeDirectoryPermissions { .. } => { 72 "admin runtime directory permissions could not be secured" 73 } 74 Self::RuntimeDirectoryGroup { .. } => { 75 "admin runtime directory group could not be secured" 76 } 77 Self::InvalidSocketPath => "admin socket path is outside its runtime directory", 78 Self::WriterLockUnavailable { .. } => "admin writer lock is unavailable", 79 Self::WriterLockInvalidType => "admin writer lock path has an unsafe type", 80 Self::WriterLockWrongOwner => "admin writer lock has the wrong owner", 81 Self::WriterAlreadyActive => "another admin socket writer is active", 82 Self::SocketPathUnavailable { .. } => "admin socket path could not be inspected", 83 Self::SocketPathWrongType => "admin socket path has an unsafe type", 84 Self::SocketPathWrongOwner => "admin socket path has the wrong owner", 85 Self::SocketActive => "an admin socket listener is already active", 86 Self::SocketLivenessUnproven => "admin socket liveness could not be proven", 87 Self::StaleSocketCleanup { .. } => "stale admin socket could not be removed", 88 Self::SocketBind { .. } => "admin socket could not be bound", 89 Self::SocketPermissions { .. } => "admin socket permissions could not be secured", 90 Self::SocketGroup { .. } => "admin socket group could not be secured", 91 Self::ListenerConfiguration { .. } => "admin listener could not be configured", 92 }) 93 } 94 } 95 96 impl Error for UnixAdminSocketError {} 97 98 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 99 struct FileIdentity { 100 device: u64, 101 inode: u64, 102 } 103 104 impl FileIdentity { 105 fn from_metadata(metadata: &fs::Metadata) -> Self { 106 Self { 107 device: metadata.dev(), 108 inode: metadata.ino(), 109 } 110 } 111 } 112 113 /// Exclusive authority required before inspecting or replacing an admin socket. 114 /// 115 /// The persistent lock sidecar is opened without following symlinks and held for 116 /// this value's entire lifetime. It is deliberately not exposed as a raw file. 117 pub struct UnixAdminSocketWriterAuthority { 118 runtime_directory: PathBuf, 119 _directory: File, 120 directory_identity: FileIdentity, 121 expected_uid: u32, 122 expected_gid: Option<u32>, 123 directory_mode: u32, 124 peer_authorization: AdminPeerAuthorizationPolicy, 125 _writer_lock: File, 126 } 127 128 impl fmt::Debug for UnixAdminSocketWriterAuthority { 129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 130 formatter 131 .debug_struct("UnixAdminSocketWriterAuthority") 132 .field("runtime_directory", &"[redacted]") 133 .field("writer_lock", &"held") 134 .finish() 135 } 136 } 137 138 impl UnixAdminSocketWriterAuthority { 139 /// Acquires owner-only writer authority for one existing runtime directory. 140 pub fn acquire(runtime_directory: impl AsRef<Path>) -> Result<Self, UnixAdminSocketError> { 141 Self::acquire_with_peer_authorization( 142 runtime_directory, 143 AdminPeerAuthorizationPolicy::owner_only(), 144 ) 145 } 146 147 /// Acquires writer authority using one policy for permissions and peer admission. 148 pub fn acquire_with_peer_authorization( 149 runtime_directory: impl AsRef<Path>, 150 peer_authorization: AdminPeerAuthorizationPolicy, 151 ) -> Result<Self, UnixAdminSocketError> { 152 let runtime_directory = runtime_directory.as_ref().to_path_buf(); 153 if !runtime_directory.is_absolute() { 154 return Err(UnixAdminSocketError::RuntimeDirectoryNotAbsolute); 155 } 156 let expected_uid = geteuid().as_raw(); 157 let directory = open_secure_directory(&runtime_directory, expected_uid)?; 158 let writer_lock = open_writer_lock(&directory, expected_uid)?; 159 FileExt::try_lock_exclusive(&writer_lock).map_err(|error| { 160 if error.kind() == io::ErrorKind::WouldBlock { 161 UnixAdminSocketError::WriterAlreadyActive 162 } else { 163 UnixAdminSocketError::WriterLockUnavailable { kind: error.kind() } 164 } 165 })?; 166 let expected_gid = peer_authorization.admin_gid(); 167 if let Some(admin_gid) = expected_gid { 168 fchown(&directory, None, Some(Gid::from_raw(admin_gid))).map_err(|error| { 169 UnixAdminSocketError::RuntimeDirectoryGroup { 170 kind: errno_kind(error), 171 } 172 })?; 173 } 174 let directory_permissions = if expected_gid.is_some() { 175 Mode::RWXU | Mode::RGRP | Mode::XGRP 176 } else { 177 Mode::RWXU 178 }; 179 let directory_mode = normalize_mode(directory_permissions.bits()); 180 fchmod(&directory, directory_permissions).map_err(|error| { 181 UnixAdminSocketError::RuntimeDirectoryPermissions { 182 kind: errno_kind(error), 183 } 184 })?; 185 let directory_metadata = directory.metadata().map_err(|error| { 186 UnixAdminSocketError::RuntimeDirectoryUnavailable { kind: error.kind() } 187 })?; 188 189 let authority = Self { 190 runtime_directory, 191 _directory: directory, 192 directory_identity: FileIdentity::from_metadata(&directory_metadata), 193 expected_uid, 194 expected_gid, 195 directory_mode, 196 peer_authorization, 197 _writer_lock: writer_lock, 198 }; 199 authority.ensure_directory_identity()?; 200 Ok(authority) 201 } 202 203 fn resolve_socket_path(&self, requested: &Path) -> Result<PathBuf, UnixAdminSocketError> { 204 let Some(file_name) = requested.file_name() else { 205 return Err(UnixAdminSocketError::InvalidSocketPath); 206 }; 207 if requested.parent() != Some(self.runtime_directory.as_path()) 208 || file_name == OsStr::new(WRITER_LOCK_FILE_NAME) 209 { 210 return Err(UnixAdminSocketError::InvalidSocketPath); 211 } 212 Ok(self.runtime_directory.join(file_name)) 213 } 214 215 fn ensure_directory_identity(&self) -> Result<(), UnixAdminSocketError> { 216 let metadata = fs::symlink_metadata(&self.runtime_directory).map_err(|error| { 217 UnixAdminSocketError::RuntimeDirectoryUnavailable { kind: error.kind() } 218 })?; 219 if metadata.file_type().is_symlink() || !metadata.is_dir() { 220 return Err(UnixAdminSocketError::RuntimeDirectoryChanged); 221 } 222 if metadata.uid() != self.expected_uid 223 || self.expected_gid.is_some_and(|gid| metadata.gid() != gid) 224 || metadata.permissions().mode() & 0o777 != self.directory_mode 225 || FileIdentity::from_metadata(&metadata) != self.directory_identity 226 { 227 return Err(UnixAdminSocketError::RuntimeDirectoryChanged); 228 } 229 Ok(()) 230 } 231 } 232 233 fn normalize_mode<T>(raw: T) -> u32 234 where 235 T: Into<u32>, 236 { 237 raw.into() 238 } 239 240 /// A bound Unix admin listener with policy-aligned modes and identity-safe cleanup. 241 pub struct UnixAdminSocketBinding { 242 listener: UnixListener, 243 socket_path: PathBuf, 244 socket_identity: FileIdentity, 245 authority: UnixAdminSocketWriterAuthority, 246 } 247 248 impl fmt::Debug for UnixAdminSocketBinding { 249 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 250 formatter 251 .debug_struct("UnixAdminSocketBinding") 252 .field("socket_path", &"[redacted]") 253 .field("writer_authority", &"held") 254 .finish_non_exhaustive() 255 } 256 } 257 258 impl UnixAdminSocketBinding { 259 /// Validates, probes, and binds one direct child of the authorized runtime directory. 260 pub async fn bind( 261 authority: UnixAdminSocketWriterAuthority, 262 socket_path: impl AsRef<Path>, 263 ) -> Result<Self, UnixAdminSocketError> { 264 authority.ensure_directory_identity()?; 265 let socket_path = authority.resolve_socket_path(socket_path.as_ref())?; 266 prepare_socket_path(&authority, &socket_path).await?; 267 authority.ensure_directory_identity()?; 268 269 let listener = UnixListener::bind(&socket_path) 270 .map_err(|error| UnixAdminSocketError::SocketBind { kind: error.kind() })?; 271 let socket_identity = match inspect_socket(&socket_path, authority.expected_uid) { 272 Ok(Some(identity)) => identity, 273 Ok(None) => { 274 drop(listener); 275 return Err(UnixAdminSocketError::SocketPathUnavailable { 276 kind: io::ErrorKind::NotFound, 277 }); 278 } 279 Err(error) => { 280 drop(listener); 281 return Err(error); 282 } 283 }; 284 if let Some(admin_gid) = authority.expected_gid 285 && let Err(error) = chownat( 286 &authority._directory, 287 socket_path 288 .file_name() 289 .expect("resolved socket path always has a file name"), 290 None, 291 Some(Gid::from_raw(admin_gid)), 292 AtFlags::SYMLINK_NOFOLLOW, 293 ) 294 { 295 drop(listener); 296 remove_matching_socket(&authority, &socket_path, socket_identity); 297 return Err(UnixAdminSocketError::SocketGroup { 298 kind: errno_kind(error), 299 }); 300 } 301 let socket_mode = if authority.expected_gid.is_some() { 302 UNIX_ADMIN_GROUP_SOCKET_MODE 303 } else { 304 UNIX_ADMIN_OWNER_SOCKET_MODE 305 }; 306 if let Err(error) = 307 fs::set_permissions(&socket_path, fs::Permissions::from_mode(socket_mode)) 308 { 309 drop(listener); 310 remove_matching_socket(&authority, &socket_path, socket_identity); 311 return Err(UnixAdminSocketError::SocketPermissions { kind: error.kind() }); 312 } 313 if let Err(error) = verify_bound_socket( 314 &socket_path, 315 socket_identity, 316 authority.expected_uid, 317 authority.expected_gid, 318 socket_mode, 319 ) { 320 drop(listener); 321 remove_matching_socket(&authority, &socket_path, socket_identity); 322 return Err(error); 323 } 324 if let Err(error) = listener.set_nonblocking(true) { 325 drop(listener); 326 remove_matching_socket(&authority, &socket_path, socket_identity); 327 return Err(UnixAdminSocketError::ListenerConfiguration { kind: error.kind() }); 328 } 329 330 Ok(Self { 331 listener, 332 socket_path, 333 socket_identity, 334 authority, 335 }) 336 } 337 338 pub(crate) fn listener(&self) -> &UnixListener { 339 &self.listener 340 } 341 342 /// Returns the policy shared by socket permissions and peer admission. 343 #[must_use] 344 pub const fn peer_authorization(&self) -> AdminPeerAuthorizationPolicy { 345 self.authority.peer_authorization 346 } 347 348 pub(crate) const fn peer_authorizer(&self) -> PeerAuthorizer { 349 PeerAuthorizer::new( 350 self.authority.peer_authorization, 351 self.authority.expected_uid, 352 ) 353 } 354 } 355 356 impl Drop for UnixAdminSocketBinding { 357 fn drop(&mut self) { 358 remove_matching_socket(&self.authority, &self.socket_path, self.socket_identity); 359 } 360 } 361 362 fn open_secure_directory(path: &Path, expected_uid: u32) -> Result<File, UnixAdminSocketError> { 363 let directory = open( 364 path, 365 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC, 366 Mode::empty(), 367 ) 368 .map_err(|error| UnixAdminSocketError::RuntimeDirectoryUnavailable { 369 kind: errno_kind(error), 370 })?; 371 let status = 372 fstat(&directory).map_err(|error| UnixAdminSocketError::RuntimeDirectoryUnavailable { 373 kind: errno_kind(error), 374 })?; 375 if FileType::from_raw_mode(status.st_mode) != FileType::Directory { 376 return Err(UnixAdminSocketError::RuntimeDirectoryNotDirectory); 377 } 378 validate_owner(status.st_uid, expected_uid)?; 379 Ok(File::from(directory)) 380 } 381 382 fn validate_owner(actual_uid: u32, expected_uid: u32) -> Result<(), UnixAdminSocketError> { 383 if actual_uid == expected_uid { 384 Ok(()) 385 } else { 386 Err(UnixAdminSocketError::RuntimeDirectoryWrongOwner) 387 } 388 } 389 390 fn open_writer_lock(directory: &File, expected_uid: u32) -> Result<File, UnixAdminSocketError> { 391 let descriptor = openat( 392 directory, 393 WRITER_LOCK_FILE_NAME, 394 OFlags::RDWR | OFlags::CREATE | OFlags::NOFOLLOW | OFlags::CLOEXEC, 395 Mode::RUSR | Mode::WUSR, 396 ) 397 .map_err(|error| UnixAdminSocketError::WriterLockUnavailable { 398 kind: errno_kind(error), 399 })?; 400 let status = 401 fstat(&descriptor).map_err(|error| UnixAdminSocketError::WriterLockUnavailable { 402 kind: errno_kind(error), 403 })?; 404 if FileType::from_raw_mode(status.st_mode) != FileType::RegularFile || status.st_nlink != 1 { 405 return Err(UnixAdminSocketError::WriterLockInvalidType); 406 } 407 if status.st_uid != expected_uid { 408 return Err(UnixAdminSocketError::WriterLockWrongOwner); 409 } 410 fchmod(&descriptor, Mode::RUSR | Mode::WUSR).map_err(|error| { 411 UnixAdminSocketError::WriterLockUnavailable { 412 kind: errno_kind(error), 413 } 414 })?; 415 Ok(File::from(descriptor)) 416 } 417 418 async fn prepare_socket_path( 419 authority: &UnixAdminSocketWriterAuthority, 420 socket_path: &Path, 421 ) -> Result<(), UnixAdminSocketError> { 422 let before = match inspect_socket(socket_path, authority.expected_uid)? { 423 Some(identity) => identity, 424 None => return Ok(()), 425 }; 426 427 let probe = tokio::time::timeout( 428 UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT, 429 tokio::net::UnixStream::connect(socket_path), 430 ) 431 .await; 432 match probe { 433 Ok(Ok(stream)) => { 434 drop(stream); 435 Err(UnixAdminSocketError::SocketActive) 436 } 437 Ok(Err(error)) if error.kind() == io::ErrorKind::ConnectionRefused => { 438 authority.ensure_directory_identity()?; 439 if inspect_socket(socket_path, authority.expected_uid)? != Some(before) { 440 return Err(UnixAdminSocketError::SocketLivenessUnproven); 441 } 442 fs::remove_file(socket_path) 443 .map_err(|error| UnixAdminSocketError::StaleSocketCleanup { kind: error.kind() })?; 444 Ok(()) 445 } 446 Ok(Err(error)) if error.kind() == io::ErrorKind::NotFound => { 447 if inspect_socket(socket_path, authority.expected_uid)?.is_none() { 448 Ok(()) 449 } else { 450 Err(UnixAdminSocketError::SocketLivenessUnproven) 451 } 452 } 453 Ok(Err(_)) | Err(_) => Err(UnixAdminSocketError::SocketLivenessUnproven), 454 } 455 } 456 457 fn inspect_socket( 458 socket_path: &Path, 459 expected_uid: u32, 460 ) -> Result<Option<FileIdentity>, UnixAdminSocketError> { 461 match fs::symlink_metadata(socket_path) { 462 Ok(metadata) => { 463 if !metadata.file_type().is_socket() { 464 return Err(UnixAdminSocketError::SocketPathWrongType); 465 } 466 if metadata.uid() != expected_uid { 467 return Err(UnixAdminSocketError::SocketPathWrongOwner); 468 } 469 Ok(Some(FileIdentity::from_metadata(&metadata))) 470 } 471 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), 472 Err(error) => Err(UnixAdminSocketError::SocketPathUnavailable { kind: error.kind() }), 473 } 474 } 475 476 fn verify_bound_socket( 477 socket_path: &Path, 478 expected_identity: FileIdentity, 479 expected_uid: u32, 480 expected_gid: Option<u32>, 481 expected_mode: u32, 482 ) -> Result<(), UnixAdminSocketError> { 483 let metadata = fs::symlink_metadata(socket_path) 484 .map_err(|error| UnixAdminSocketError::SocketPathUnavailable { kind: error.kind() })?; 485 if !metadata.file_type().is_socket() 486 || FileIdentity::from_metadata(&metadata) != expected_identity 487 { 488 return Err(UnixAdminSocketError::SocketPathWrongType); 489 } 490 if metadata.uid() != expected_uid { 491 return Err(UnixAdminSocketError::SocketPathWrongOwner); 492 } 493 if expected_gid.is_some_and(|gid| metadata.gid() != gid) { 494 return Err(UnixAdminSocketError::SocketGroup { 495 kind: io::ErrorKind::PermissionDenied, 496 }); 497 } 498 if metadata.permissions().mode() & 0o777 != expected_mode { 499 return Err(UnixAdminSocketError::SocketPermissions { 500 kind: io::ErrorKind::PermissionDenied, 501 }); 502 } 503 Ok(()) 504 } 505 506 fn remove_matching_socket( 507 authority: &UnixAdminSocketWriterAuthority, 508 socket_path: &Path, 509 expected_identity: FileIdentity, 510 ) { 511 if authority.ensure_directory_identity().is_err() { 512 return; 513 } 514 let Ok(Some(current_identity)) = inspect_socket(socket_path, authority.expected_uid) else { 515 return; 516 }; 517 if expected_identity == current_identity { 518 let _ = fs::remove_file(socket_path); 519 } 520 } 521 522 fn errno_kind(error: rustix::io::Errno) -> io::ErrorKind { 523 io::Error::from_raw_os_error(error.raw_os_error()).kind() 524 } 525 526 #[cfg(test)] 527 mod tests { 528 use super::*; 529 use std::os::unix::fs::{MetadataExt, symlink}; 530 531 fn mode(path: &Path) -> u32 { 532 fs::symlink_metadata(path) 533 .expect("path metadata") 534 .permissions() 535 .mode() 536 & 0o777 537 } 538 539 #[tokio::test] 540 async fn binds_owner_only_socket_sets_modes_and_cleans_up() { 541 let directory = super::test_support::short_tempdir(); 542 let socket = directory.path().join("admin.sock"); 543 let authority = 544 UnixAdminSocketWriterAuthority::acquire(directory.path()).expect("writer authority"); 545 let binding = UnixAdminSocketBinding::bind(authority, &socket) 546 .await 547 .expect("admin binding"); 548 549 assert_eq!(mode(directory.path()), UNIX_ADMIN_OWNER_DIRECTORY_MODE); 550 assert_eq!(mode(&socket), UNIX_ADMIN_OWNER_SOCKET_MODE); 551 assert_eq!( 552 mode(&directory.path().join(WRITER_LOCK_FILE_NAME)), 553 UNIX_ADMIN_OWNER_SOCKET_MODE 554 ); 555 assert!( 556 !binding 557 .listener() 558 .local_addr() 559 .expect("local address") 560 .is_unnamed() 561 ); 562 assert!(!format!("{binding:?}").contains(directory.path().to_string_lossy().as_ref())); 563 564 drop(binding); 565 assert!(!socket.exists()); 566 UnixAdminSocketWriterAuthority::acquire(directory.path()) 567 .expect("writer authority released"); 568 } 569 570 #[test] 571 fn owner_only_mode_inventory_is_literal_and_stable() { 572 assert_eq!(UNIX_ADMIN_OWNER_DIRECTORY_MODE, 0o700); 573 assert_eq!(UNIX_ADMIN_OWNER_SOCKET_MODE, 0o600); 574 assert_eq!(UNIX_ADMIN_GROUP_DIRECTORY_MODE, 0o750); 575 assert_eq!(UNIX_ADMIN_GROUP_SOCKET_MODE, 0o660); 576 assert_eq!(normalize_mode(Mode::RWXU.bits()), 0o700); 577 } 578 579 #[cfg(target_os = "linux")] 580 #[tokio::test] 581 async fn configured_admin_group_sets_group_access_modes_and_identity() { 582 let directory = super::test_support::short_tempdir(); 583 let socket = directory.path().join("admin.sock"); 584 let admin_gid = rustix::process::getegid().as_raw(); 585 let policy = AdminPeerAuthorizationPolicy::with_admin_gid(admin_gid) 586 .expect("current Linux group is valid"); 587 let authority = UnixAdminSocketWriterAuthority::acquire_with_peer_authorization( 588 directory.path(), 589 policy, 590 ) 591 .expect("group writer authority"); 592 let binding = UnixAdminSocketBinding::bind(authority, &socket) 593 .await 594 .expect("group admin binding"); 595 596 assert_eq!(mode(directory.path()), UNIX_ADMIN_GROUP_DIRECTORY_MODE); 597 assert_eq!(mode(&socket), UNIX_ADMIN_GROUP_SOCKET_MODE); 598 assert_eq!( 599 fs::symlink_metadata(directory.path()) 600 .expect("directory metadata") 601 .gid(), 602 admin_gid 603 ); 604 assert_eq!( 605 fs::symlink_metadata(&socket) 606 .expect("socket metadata") 607 .gid(), 608 admin_gid 609 ); 610 assert_eq!(binding.peer_authorization(), policy); 611 612 let error = UnixAdminSocketWriterAuthority::acquire(directory.path()) 613 .expect_err("active group-authorized writer excludes owner-only reconfiguration"); 614 assert_eq!(error, UnixAdminSocketError::WriterAlreadyActive); 615 assert_eq!(mode(directory.path()), UNIX_ADMIN_GROUP_DIRECTORY_MODE); 616 assert_eq!(mode(&socket), UNIX_ADMIN_GROUP_SOCKET_MODE); 617 } 618 619 #[tokio::test] 620 async fn refuses_a_live_socket_owned_outside_the_writer_guard() { 621 let directory = super::test_support::short_tempdir(); 622 let socket = directory.path().join("admin.sock"); 623 let live = UnixListener::bind(&socket).expect("live listener"); 624 let authority = 625 UnixAdminSocketWriterAuthority::acquire(directory.path()).expect("writer authority"); 626 627 let error = UnixAdminSocketBinding::bind(authority, &socket) 628 .await 629 .expect_err("live listener must be retained"); 630 assert_eq!(error, UnixAdminSocketError::SocketActive); 631 assert!(socket.exists()); 632 drop(live); 633 } 634 635 #[tokio::test] 636 async fn recovers_only_a_proven_stale_socket() { 637 let directory = super::test_support::short_tempdir(); 638 let socket = directory.path().join("admin.sock"); 639 drop(UnixListener::bind(&socket).expect("stale listener")); 640 assert!(socket.exists()); 641 642 let authority = 643 UnixAdminSocketWriterAuthority::acquire(directory.path()).expect("writer authority"); 644 let binding = UnixAdminSocketBinding::bind(authority, &socket) 645 .await 646 .expect("stale socket recovery"); 647 assert!(socket.exists()); 648 drop(binding); 649 assert!(!socket.exists()); 650 } 651 652 #[tokio::test] 653 async fn refuses_paths_outside_the_authorized_runtime_directory() { 654 let directory = super::test_support::short_tempdir(); 655 let outside = super::test_support::short_tempdir(); 656 let authority = 657 UnixAdminSocketWriterAuthority::acquire(directory.path()).expect("writer authority"); 658 659 let error = UnixAdminSocketBinding::bind(authority, outside.path().join("admin.sock")) 660 .await 661 .expect_err("outside path must fail"); 662 assert_eq!(error, UnixAdminSocketError::InvalidSocketPath); 663 } 664 665 #[tokio::test] 666 async fn refuses_non_socket_entries_without_unlinking_them() { 667 let directory = super::test_support::short_tempdir(); 668 let socket = directory.path().join("admin.sock"); 669 fs::write(&socket, b"not a socket").expect("sentinel file"); 670 let authority = 671 UnixAdminSocketWriterAuthority::acquire(directory.path()).expect("writer authority"); 672 673 let error = UnixAdminSocketBinding::bind(authority, &socket) 674 .await 675 .expect_err("non-socket path must fail"); 676 assert_eq!(error, UnixAdminSocketError::SocketPathWrongType); 677 assert_eq!( 678 fs::read(&socket).expect("sentinel retained"), 679 b"not a socket" 680 ); 681 } 682 683 #[test] 684 fn one_writer_authority_excludes_a_second_writer() { 685 let directory = super::test_support::short_tempdir(); 686 let first = UnixAdminSocketWriterAuthority::acquire(directory.path()) 687 .expect("first writer authority"); 688 let error = UnixAdminSocketWriterAuthority::acquire(directory.path()) 689 .expect_err("second writer must fail"); 690 assert_eq!(error, UnixAdminSocketError::WriterAlreadyActive); 691 drop(first); 692 } 693 694 #[test] 695 fn refuses_a_symlink_runtime_directory_and_wrong_owner_identity() { 696 let target = super::test_support::short_tempdir(); 697 let link_parent = super::test_support::short_tempdir(); 698 let link = link_parent.path().join("runtime"); 699 symlink(target.path(), &link).expect("runtime symlink"); 700 assert!(matches!( 701 UnixAdminSocketWriterAuthority::acquire(&link), 702 Err(UnixAdminSocketError::RuntimeDirectoryUnavailable { .. }) 703 )); 704 assert_eq!( 705 validate_owner(41, 42), 706 Err(UnixAdminSocketError::RuntimeDirectoryWrongOwner) 707 ); 708 } 709 710 #[test] 711 fn refuses_relative_runtime_directory_before_any_file_operation() { 712 assert_eq!( 713 UnixAdminSocketWriterAuthority::acquire("relative/runtime") 714 .expect_err("relative runtime directory must fail"), 715 UnixAdminSocketError::RuntimeDirectoryNotAbsolute 716 ); 717 } 718 719 #[tokio::test] 720 async fn cleanup_never_unlinks_a_replacement_socket() { 721 let directory = super::test_support::short_tempdir(); 722 let socket = directory.path().join("admin.sock"); 723 let authority = 724 UnixAdminSocketWriterAuthority::acquire(directory.path()).expect("writer authority"); 725 let binding = UnixAdminSocketBinding::bind(authority, &socket) 726 .await 727 .expect("admin binding"); 728 729 fs::remove_file(&socket).expect("unlink original name"); 730 let replacement = UnixListener::bind(&socket).expect("replacement listener"); 731 drop(binding); 732 assert!(socket.exists(), "replacement identity must survive cleanup"); 733 drop(replacement); 734 fs::remove_file(&socket).expect("remove replacement"); 735 } 736 737 #[test] 738 fn public_errors_and_authority_debug_never_reveal_runtime_paths() { 739 let directory = super::test_support::short_tempdir(); 740 let authority = 741 UnixAdminSocketWriterAuthority::acquire(directory.path()).expect("writer authority"); 742 let debug = format!("{authority:?}"); 743 assert!(!debug.contains(directory.path().to_string_lossy().as_ref())); 744 745 let error = UnixAdminSocketError::SocketBind { 746 kind: io::ErrorKind::PermissionDenied, 747 }; 748 assert!(!format!("{error:?}").contains('/')); 749 assert!(!error.to_string().contains('/')); 750 } 751 752 #[test] 753 fn helper_admission_checks_bind_every_identity_and_mode_dimension() { 754 let directory = super::test_support::short_tempdir(); 755 let uid = geteuid().as_raw(); 756 assert!(validate_owner(uid, uid).is_ok()); 757 assert_eq!( 758 validate_owner(uid, uid.wrapping_add(1)), 759 Err(UnixAdminSocketError::RuntimeDirectoryWrongOwner) 760 ); 761 762 let held_directory = open_secure_directory(directory.path(), uid).unwrap(); 763 let wrong_owner = open_writer_lock(&held_directory, uid.wrapping_add(1)).unwrap_err(); 764 assert_eq!(wrong_owner, UnixAdminSocketError::WriterLockWrongOwner); 765 drop(held_directory); 766 fs::remove_file(directory.path().join(WRITER_LOCK_FILE_NAME)).unwrap(); 767 768 let lock = directory.path().join(WRITER_LOCK_FILE_NAME); 769 fs::write(&lock, b"").unwrap(); 770 let alias = directory.path().join("writer-lock-alias"); 771 fs::hard_link(&lock, &alias).unwrap(); 772 let held_directory = open_secure_directory(directory.path(), uid).unwrap(); 773 assert_eq!( 774 open_writer_lock(&held_directory, uid).unwrap_err(), 775 UnixAdminSocketError::WriterLockInvalidType 776 ); 777 drop(held_directory); 778 fs::remove_file(alias).unwrap(); 779 fs::remove_file(lock).unwrap(); 780 781 let socket = directory.path().join("admin.sock"); 782 let listener = UnixListener::bind(&socket).unwrap(); 783 let metadata = fs::symlink_metadata(&socket).unwrap(); 784 let identity = FileIdentity::from_metadata(&metadata); 785 assert_eq!(inspect_socket(&socket, uid).unwrap(), Some(identity)); 786 assert_eq!( 787 inspect_socket(&directory.path().join("missing"), uid).unwrap(), 788 None 789 ); 790 assert_eq!( 791 inspect_socket(&socket, uid.wrapping_add(1)).unwrap_err(), 792 UnixAdminSocketError::SocketPathWrongOwner 793 ); 794 assert!(verify_bound_socket(&socket, identity, uid, None, mode(&socket)).is_ok()); 795 assert_eq!( 796 verify_bound_socket( 797 &socket, 798 FileIdentity { 799 device: identity.device, 800 inode: identity.inode.wrapping_add(1), 801 }, 802 uid, 803 None, 804 mode(&socket), 805 ), 806 Err(UnixAdminSocketError::SocketPathWrongType) 807 ); 808 assert_eq!( 809 verify_bound_socket(&socket, identity, uid.wrapping_add(1), None, mode(&socket)), 810 Err(UnixAdminSocketError::SocketPathWrongOwner) 811 ); 812 assert!(matches!( 813 verify_bound_socket( 814 &socket, 815 identity, 816 uid, 817 Some(metadata.gid().wrapping_add(1)), 818 mode(&socket), 819 ), 820 Err(UnixAdminSocketError::SocketGroup { .. }) 821 )); 822 assert!(matches!( 823 verify_bound_socket(&socket, identity, uid, None, mode(&socket) ^ 0o100), 824 Err(UnixAdminSocketError::SocketPermissions { .. }) 825 )); 826 drop(listener); 827 fs::remove_file(socket).unwrap(); 828 } 829 830 #[test] 831 fn live_directory_revalidation_rejects_mode_identity_and_path_drift() { 832 let directory = super::test_support::short_tempdir(); 833 let authority = UnixAdminSocketWriterAuthority::acquire(directory.path()).unwrap(); 834 assert!(authority.ensure_directory_identity().is_ok()); 835 assert_eq!( 836 authority.resolve_socket_path(Path::new("/")).unwrap_err(), 837 UnixAdminSocketError::InvalidSocketPath 838 ); 839 840 fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o755)).unwrap(); 841 assert_eq!( 842 authority.ensure_directory_identity().unwrap_err(), 843 UnixAdminSocketError::RuntimeDirectoryChanged 844 ); 845 fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o700)).unwrap(); 846 847 let moved = directory.path().with_extension("held"); 848 fs::rename(directory.path(), &moved).unwrap(); 849 fs::create_dir(directory.path()).unwrap(); 850 fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o700)).unwrap(); 851 assert_eq!( 852 authority.ensure_directory_identity().unwrap_err(), 853 UnixAdminSocketError::RuntimeDirectoryChanged 854 ); 855 fs::remove_dir(directory.path()).unwrap(); 856 fs::rename(&moved, directory.path()).unwrap(); 857 assert!(authority.ensure_directory_identity().is_ok()); 858 859 let socket = directory.path().join("missing.sock"); 860 remove_matching_socket( 861 &authority, 862 &socket, 863 FileIdentity { 864 device: 0, 865 inode: 0, 866 }, 867 ); 868 assert!(!socket.exists()); 869 } 870 }