lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }