lib

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

provision.rs (40361B)


      1 //! Explicit provisioning for one canonical service-instance state directory.
      2 
      3 use core::fmt;
      4 use std::{
      5     error::Error,
      6     ffi::{OsStr, OsString},
      7     path::{Component, Path, PathBuf},
      8 };
      9 
     10 use crate::{InstanceId, RadrootsPathProfile, RuntimeContext, ServiceId};
     11 
     12 #[cfg(any(target_os = "linux", target_os = "macos"))]
     13 use std::fs::File;
     14 
     15 #[cfg(any(target_os = "linux", target_os = "macos"))]
     16 use rustix::{
     17     fs::{AtFlags, FileType, Mode, OFlags, fstat, mkdirat, open, openat, statat, unlinkat},
     18     process::geteuid,
     19 };
     20 
     21 const SERVICES_COMPONENT: &str = "services";
     22 
     23 /// A sealed plan for validating or creating one canonical state directory.
     24 ///
     25 /// Construction is available only from [`RuntimeContext::state_directory_plan`].
     26 /// The plan owns no ambient-environment lookup and performs no filesystem I/O
     27 /// until [`Self::provision`] is called.
     28 ///
     29 /// ```compile_fail
     30 /// use radroots_runtime_paths::{RadrootsPathProfile, RuntimeStateDirectoryPlan};
     31 ///
     32 /// let _ = RuntimeStateDirectoryPlan {
     33 ///     profile: RadrootsPathProfile::RepoLocal,
     34 ///     state_root: "/tmp/alternate".into(),
     35 ///     service: todo!(),
     36 ///     instance: todo!(),
     37 /// };
     38 /// ```
     39 #[derive(Clone, PartialEq, Eq)]
     40 pub struct RuntimeStateDirectoryPlan {
     41     profile: RadrootsPathProfile,
     42     state_root: PathBuf,
     43     service: ServiceId,
     44     instance: InstanceId,
     45 }
     46 
     47 impl RuntimeStateDirectoryPlan {
     48     pub(crate) fn from_context(
     49         context: &RuntimeContext,
     50     ) -> Result<Self, StateDirectoryProvisionError> {
     51         let state_root = context.paths().state_root();
     52         validate_absolute_root(state_root)?;
     53         let expected = state_root
     54             .join(SERVICES_COMPONENT)
     55             .join(context.service().as_str())
     56             .join(context.instance().as_str());
     57         if expected != context.paths().state() {
     58             return Err(StateDirectoryProvisionError::InvalidPlan);
     59         }
     60         Ok(Self {
     61             profile: context.profile(),
     62             state_root: state_root.to_path_buf(),
     63             service: context.service().clone(),
     64             instance: context.instance().clone(),
     65         })
     66     }
     67 
     68     /// Returns the path profile whose creation policy is frozen by this plan.
     69     #[must_use]
     70     pub fn profile(&self) -> RadrootsPathProfile {
     71         self.profile
     72     }
     73 
     74     /// Validates or creates the exact `services/<service>/<instance>` suffix.
     75     ///
     76     /// `InteractiveUser` and `RepoLocal` plans may create missing suffix
     77     /// directories. `ServiceHost` plans validate an already-provisioned suffix
     78     /// and never create it. Existing directories are never permission-repaired.
     79     /// Every traversal and creation is descriptor-relative and rejects symlinks.
     80     pub fn provision(&self) -> Result<(), StateDirectoryProvisionError> {
     81         provision_supported(self)
     82     }
     83 
     84     fn components(&self) -> [&OsStr; 3] {
     85         [
     86             OsStr::new(SERVICES_COMPONENT),
     87             OsStr::new(self.service.as_str()),
     88             OsStr::new(self.instance.as_str()),
     89         ]
     90     }
     91 
     92     fn permits_creation(&self) -> Result<bool, StateDirectoryProvisionError> {
     93         match self.profile {
     94             RadrootsPathProfile::InteractiveUser | RadrootsPathProfile::RepoLocal => Ok(true),
     95             RadrootsPathProfile::ServiceHost => Ok(false),
     96             RadrootsPathProfile::MobileNative => {
     97                 Err(StateDirectoryProvisionError::UnsupportedProfile)
     98             }
     99         }
    100     }
    101 }
    102 
    103 impl fmt::Debug for RuntimeStateDirectoryPlan {
    104     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    105         formatter
    106             .debug_struct("RuntimeStateDirectoryPlan")
    107             .field("profile", &self.profile)
    108             .field("state_root", &"[redacted]")
    109             .field("service", &"[redacted]")
    110             .field("instance", &"[redacted]")
    111             .finish()
    112     }
    113 }
    114 
    115 /// Stable path-free failures from state-directory planning or provisioning.
    116 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    117 pub enum StateDirectoryProvisionError {
    118     InvalidPlan,
    119     UnsupportedPlatform,
    120     UnsupportedProfile,
    121     StateRootUnavailable,
    122     MissingDirectory,
    123     DirectoryConflict,
    124     UnsafeDirectory,
    125     Filesystem,
    126     Cleanup,
    127 }
    128 
    129 impl fmt::Display for StateDirectoryProvisionError {
    130     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    131         formatter.write_str(match self {
    132             Self::InvalidPlan => "runtime state-directory plan is invalid",
    133             Self::UnsupportedPlatform => {
    134                 "runtime state-directory provisioning is unsupported on this platform"
    135             }
    136             Self::UnsupportedProfile => {
    137                 "runtime state-directory provisioning is unsupported for this profile"
    138             }
    139             Self::StateRootUnavailable => "runtime state-directory root is unavailable or unsafe",
    140             Self::MissingDirectory => "runtime state directory must already exist for this profile",
    141             Self::DirectoryConflict => {
    142                 "runtime state-directory entry conflicts with the canonical plan"
    143             }
    144             Self::UnsafeDirectory => "runtime state directory failed security validation",
    145             Self::Filesystem => "runtime state-directory filesystem operation failed",
    146             Self::Cleanup => "runtime state-directory cleanup could not be proven complete",
    147         })
    148     }
    149 }
    150 
    151 impl Error for StateDirectoryProvisionError {}
    152 
    153 fn validate_absolute_root(root: &Path) -> Result<(), StateDirectoryProvisionError> {
    154     if !root.is_absolute() || root.parent().is_none() {
    155         return Err(StateDirectoryProvisionError::InvalidPlan);
    156     }
    157     let mut saw_root = false;
    158     let mut saw_normal = false;
    159     for component in root.components() {
    160         match component {
    161             Component::RootDir if !saw_root && !saw_normal => saw_root = true,
    162             Component::Normal(_) if saw_root => saw_normal = true,
    163             Component::Prefix(_)
    164             | Component::CurDir
    165             | Component::ParentDir
    166             | Component::RootDir => {
    167                 return Err(StateDirectoryProvisionError::InvalidPlan);
    168             }
    169             Component::Normal(_) => return Err(StateDirectoryProvisionError::InvalidPlan),
    170         }
    171     }
    172     if saw_normal {
    173         Ok(())
    174     } else {
    175         Err(StateDirectoryProvisionError::InvalidPlan)
    176     }
    177 }
    178 
    179 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    180 fn provision_supported(
    181     _plan: &RuntimeStateDirectoryPlan,
    182 ) -> Result<(), StateDirectoryProvisionError> {
    183     Err(StateDirectoryProvisionError::UnsupportedPlatform)
    184 }
    185 
    186 #[cfg(any(target_os = "linux", target_os = "macos"))]
    187 fn provision_supported(
    188     plan: &RuntimeStateDirectoryPlan,
    189 ) -> Result<(), StateDirectoryProvisionError> {
    190     provision_with_operations(plan, &SystemProvisionOperations)
    191 }
    192 
    193 #[cfg(any(target_os = "linux", target_os = "macos"))]
    194 trait ProvisionOperations {
    195     fn after_create(&self, _component_index: usize) -> Result<(), StateDirectoryProvisionError> {
    196         Ok(())
    197     }
    198 }
    199 
    200 #[cfg(any(target_os = "linux", target_os = "macos"))]
    201 struct SystemProvisionOperations;
    202 
    203 #[cfg(any(target_os = "linux", target_os = "macos"))]
    204 impl ProvisionOperations for SystemProvisionOperations {}
    205 
    206 #[cfg(any(target_os = "linux", target_os = "macos"))]
    207 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    208 struct DirectoryIdentity {
    209     device: u64,
    210     inode: u64,
    211 }
    212 
    213 #[cfg(any(target_os = "linux", target_os = "macos"))]
    214 struct CreatedDirectory {
    215     parent: File,
    216     name: OsString,
    217     held: File,
    218     identity: DirectoryIdentity,
    219 }
    220 
    221 #[cfg(any(target_os = "linux", target_os = "macos"))]
    222 struct CreationJournal {
    223     state_root_path: PathBuf,
    224     state_root: File,
    225     state_root_identity: DirectoryIdentity,
    226     entries: Vec<CreatedDirectory>,
    227     committed: bool,
    228 }
    229 
    230 #[cfg(any(target_os = "linux", target_os = "macos"))]
    231 impl CreationJournal {
    232     fn new(
    233         state_root_path: PathBuf,
    234         state_root: File,
    235         state_root_identity: DirectoryIdentity,
    236     ) -> Self {
    237         Self {
    238             state_root_path,
    239             state_root,
    240             state_root_identity,
    241             entries: Vec::new(),
    242             committed: false,
    243         }
    244     }
    245 
    246     fn fail(
    247         &mut self,
    248         failure: StateDirectoryProvisionError,
    249     ) -> Result<(), StateDirectoryProvisionError> {
    250         match self.cleanup() {
    251             Ok(()) => Err(failure),
    252             Err(()) => Err(StateDirectoryProvisionError::Cleanup),
    253         }
    254     }
    255 
    256     fn cleanup(&mut self) -> Result<(), ()> {
    257         if validate_absolute_directory_binding(
    258             &self.state_root_path,
    259             &self.state_root,
    260             self.state_root_identity,
    261         )
    262         .is_err()
    263         {
    264             return Err(());
    265         }
    266         let mut clean = true;
    267         for entry in self.entries.iter().rev() {
    268             if cleanup_created_directory(entry).is_err() {
    269                 clean = false;
    270                 break;
    271             }
    272         }
    273         if clean {
    274             self.entries.clear();
    275             self.committed = true;
    276             Ok(())
    277         } else {
    278             Err(())
    279         }
    280     }
    281 }
    282 
    283 #[cfg(any(target_os = "linux", target_os = "macos"))]
    284 impl Drop for CreationJournal {
    285     fn drop(&mut self) {
    286         if !self.committed {
    287             let _ = self.cleanup();
    288         }
    289     }
    290 }
    291 
    292 #[cfg(any(target_os = "linux", target_os = "macos"))]
    293 fn provision_with_operations(
    294     plan: &RuntimeStateDirectoryPlan,
    295     operations: &dyn ProvisionOperations,
    296 ) -> Result<(), StateDirectoryProvisionError> {
    297     let permits_creation = plan.permits_creation()?;
    298     let state_root = open_absolute_directory(&plan.state_root)?;
    299     let state_root_identity = validate_secure_directory(&state_root, false)
    300         .map_err(|_| StateDirectoryProvisionError::StateRootUnavailable)?;
    301     let mut current = state_root
    302         .try_clone()
    303         .map_err(|_| StateDirectoryProvisionError::Filesystem)?;
    304     let mut journal =
    305         CreationJournal::new(plan.state_root.clone(), state_root, state_root_identity);
    306     let mut bindings = Vec::with_capacity(3);
    307 
    308     for (component_index, component) in plan.components().into_iter().enumerate() {
    309         if validate_absolute_directory_binding(
    310             &journal.state_root_path,
    311             &journal.state_root,
    312             journal.state_root_identity,
    313         )
    314         .is_err()
    315         {
    316             return journal.fail(StateDirectoryProvisionError::StateRootUnavailable);
    317         }
    318         let parent = match current.try_clone() {
    319             Ok(parent) => parent,
    320             Err(_) => return journal.fail(StateDirectoryProvisionError::Filesystem),
    321         };
    322         match open_directory_at(&current, component) {
    323             Ok(next) => {
    324                 let identity = match validate_secure_directory(&next, false) {
    325                     Ok(identity) => identity,
    326                     Err(failure) => return journal.fail(failure),
    327                 };
    328                 let held = match next.try_clone() {
    329                     Ok(held) => held,
    330                     Err(_) => return journal.fail(StateDirectoryProvisionError::Filesystem),
    331                 };
    332                 bindings.push(DirectoryBinding {
    333                     parent,
    334                     name: component.to_os_string(),
    335                     held,
    336                     identity,
    337                     exact_owner_mode: false,
    338                 });
    339                 current = next;
    340             }
    341             Err(rustix::io::Errno::NOENT) if !permits_creation => {
    342                 return journal.fail(StateDirectoryProvisionError::MissingDirectory);
    343             }
    344             Err(rustix::io::Errno::NOENT) => {
    345                 match mkdirat(&parent, component, Mode::RUSR | Mode::WUSR | Mode::XUSR) {
    346                     Ok(()) => {}
    347                     Err(rustix::io::Errno::EXIST) => match open_directory_at(&parent, component) {
    348                         Ok(next) => {
    349                             let identity = match validate_secure_directory(&next, false) {
    350                                 Ok(identity) => identity,
    351                                 Err(failure) => return journal.fail(failure),
    352                             };
    353                             let held = match next.try_clone() {
    354                                 Ok(held) => held,
    355                                 Err(_) => {
    356                                     return journal.fail(StateDirectoryProvisionError::Filesystem);
    357                                 }
    358                             };
    359                             bindings.push(DirectoryBinding {
    360                                 parent,
    361                                 name: component.to_os_string(),
    362                                 held,
    363                                 identity,
    364                                 exact_owner_mode: false,
    365                             });
    366                             current = next;
    367                             continue;
    368                         }
    369                         Err(_) => {
    370                             return journal.fail(StateDirectoryProvisionError::DirectoryConflict);
    371                         }
    372                     },
    373                     Err(_) => return journal.fail(StateDirectoryProvisionError::Filesystem),
    374                 }
    375 
    376                 let created_identity = match created_directory_identity(&parent, component) {
    377                     Ok(identity) => identity,
    378                     Err(failure) => return journal.fail(failure),
    379                 };
    380                 let held = match open_directory_at(&parent, component) {
    381                     Ok(held) => held,
    382                     Err(_) => return journal.fail(StateDirectoryProvisionError::DirectoryConflict),
    383                 };
    384                 let opened_identity = match validate_secure_directory(&held, true) {
    385                     Ok(identity) => identity,
    386                     Err(failure) => return journal.fail(failure),
    387                 };
    388                 if opened_identity != created_identity {
    389                     return journal.fail(StateDirectoryProvisionError::DirectoryConflict);
    390                 }
    391                 journal.entries.push(CreatedDirectory {
    392                     parent,
    393                     name: component.to_os_string(),
    394                     held,
    395                     identity: created_identity,
    396                 });
    397                 let Some(created) = journal.entries.last() else {
    398                     return journal.fail(StateDirectoryProvisionError::Filesystem);
    399                 };
    400                 let next = match created.held.try_clone() {
    401                     Ok(next) => next,
    402                     Err(_) => return journal.fail(StateDirectoryProvisionError::Filesystem),
    403                 };
    404                 let binding_parent = match created.parent.try_clone() {
    405                     Ok(binding_parent) => binding_parent,
    406                     Err(_) => return journal.fail(StateDirectoryProvisionError::Filesystem),
    407                 };
    408                 let binding_held = match created.held.try_clone() {
    409                     Ok(binding_held) => binding_held,
    410                     Err(_) => return journal.fail(StateDirectoryProvisionError::Filesystem),
    411                 };
    412                 if created.parent.sync_all().is_err() {
    413                     return journal.fail(StateDirectoryProvisionError::Filesystem);
    414                 }
    415                 if let Err(failure) = operations.after_create(component_index) {
    416                     return journal.fail(failure);
    417                 }
    418                 bindings.push(DirectoryBinding {
    419                     parent: binding_parent,
    420                     name: component.to_os_string(),
    421                     held: binding_held,
    422                     identity: created_identity,
    423                     exact_owner_mode: true,
    424                 });
    425                 current = next;
    426             }
    427             Err(_) => return journal.fail(StateDirectoryProvisionError::DirectoryConflict),
    428         }
    429     }
    430 
    431     if validate_absolute_directory_binding(
    432         &journal.state_root_path,
    433         &journal.state_root,
    434         journal.state_root_identity,
    435     )
    436     .is_err()
    437     {
    438         return journal.fail(StateDirectoryProvisionError::StateRootUnavailable);
    439     }
    440     for binding in &bindings {
    441         if validate_directory_binding(binding).is_err() {
    442             return journal.fail(StateDirectoryProvisionError::DirectoryConflict);
    443         }
    444     }
    445 
    446     journal.committed = true;
    447     Ok(())
    448 }
    449 
    450 #[cfg(any(target_os = "linux", target_os = "macos"))]
    451 struct DirectoryBinding {
    452     parent: File,
    453     name: OsString,
    454     held: File,
    455     identity: DirectoryIdentity,
    456     exact_owner_mode: bool,
    457 }
    458 
    459 #[cfg(any(target_os = "linux", target_os = "macos"))]
    460 fn validate_directory_binding(binding: &DirectoryBinding) -> Result<(), ()> {
    461     let current = open_directory_at(&binding.parent, &binding.name).map_err(|_| ())?;
    462     let held_identity =
    463         validate_secure_directory(&binding.held, binding.exact_owner_mode).map_err(|_| ())?;
    464     let current_identity =
    465         validate_secure_directory(&current, binding.exact_owner_mode).map_err(|_| ())?;
    466     if held_identity == binding.identity && current_identity == binding.identity {
    467         Ok(())
    468     } else {
    469         Err(())
    470     }
    471 }
    472 
    473 #[cfg(any(target_os = "linux", target_os = "macos"))]
    474 fn validate_absolute_directory_binding(
    475     path: &Path,
    476     held: &File,
    477     expected: DirectoryIdentity,
    478 ) -> Result<(), ()> {
    479     let current = open_absolute_directory(path).map_err(|_| ())?;
    480     let held_identity = validate_secure_directory(held, false).map_err(|_| ())?;
    481     let current_identity = validate_secure_directory(&current, false).map_err(|_| ())?;
    482     if held_identity == expected && current_identity == expected {
    483         Ok(())
    484     } else {
    485         Err(())
    486     }
    487 }
    488 
    489 #[cfg(any(target_os = "linux", target_os = "macos"))]
    490 fn open_absolute_directory(root: &Path) -> Result<File, StateDirectoryProvisionError> {
    491     let mut current = File::from(
    492         open(
    493             Path::new("/"),
    494             OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
    495             Mode::empty(),
    496         )
    497         .map_err(|_| StateDirectoryProvisionError::StateRootUnavailable)?,
    498     );
    499     for component in root.components() {
    500         match component {
    501             Component::RootDir => {}
    502             Component::Normal(name) => {
    503                 current = open_directory_at(&current, name)
    504                     .map_err(|_| StateDirectoryProvisionError::StateRootUnavailable)?;
    505             }
    506             Component::Prefix(_) | Component::CurDir | Component::ParentDir => {
    507                 return Err(StateDirectoryProvisionError::InvalidPlan);
    508             }
    509         }
    510     }
    511     Ok(current)
    512 }
    513 
    514 #[cfg(any(target_os = "linux", target_os = "macos"))]
    515 fn open_directory_at(parent: &File, name: &OsStr) -> Result<File, rustix::io::Errno> {
    516     openat(
    517         parent,
    518         name,
    519         OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK,
    520         Mode::empty(),
    521     )
    522     .map(File::from)
    523 }
    524 
    525 #[cfg(any(target_os = "linux", target_os = "macos"))]
    526 fn created_directory_identity(
    527     parent: &File,
    528     name: &OsStr,
    529 ) -> Result<DirectoryIdentity, StateDirectoryProvisionError> {
    530     let status = statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
    531         .map_err(|_| StateDirectoryProvisionError::DirectoryConflict)?;
    532     validate_directory_status(
    533         FileType::from_raw_mode(status.st_mode).is_dir(),
    534         status.st_uid,
    535         normalize_mode(status.st_mode),
    536         true,
    537     )?;
    538     Ok(DirectoryIdentity {
    539         device: normalize_device(status.st_dev)
    540             .map_err(|_| StateDirectoryProvisionError::UnsafeDirectory)?,
    541         inode: status.st_ino,
    542     })
    543 }
    544 
    545 #[cfg(any(target_os = "linux", target_os = "macos"))]
    546 fn validate_secure_directory(
    547     directory: &File,
    548     exact_owner_mode: bool,
    549 ) -> Result<DirectoryIdentity, StateDirectoryProvisionError> {
    550     let status = fstat(directory).map_err(|_| StateDirectoryProvisionError::Filesystem)?;
    551     validate_directory_status(
    552         FileType::from_raw_mode(status.st_mode).is_dir(),
    553         status.st_uid,
    554         normalize_mode(status.st_mode),
    555         exact_owner_mode,
    556     )?;
    557     Ok(DirectoryIdentity {
    558         device: normalize_device(status.st_dev)
    559             .map_err(|_| StateDirectoryProvisionError::UnsafeDirectory)?,
    560         inode: status.st_ino,
    561     })
    562 }
    563 
    564 #[cfg(any(target_os = "linux", target_os = "macos"))]
    565 fn validate_directory_status(
    566     is_directory: bool,
    567     owner: u32,
    568     mode: u32,
    569     exact_owner_mode: bool,
    570 ) -> Result<(), StateDirectoryProvisionError> {
    571     let permissions = mode & 0o777;
    572     let permissions_valid = if exact_owner_mode {
    573         permissions == 0o700
    574     } else {
    575         permissions & 0o022 == 0 && permissions & 0o500 == 0o500
    576     };
    577     if is_directory && owner == geteuid().as_raw() && permissions_valid {
    578         Ok(())
    579     } else {
    580         Err(StateDirectoryProvisionError::UnsafeDirectory)
    581     }
    582 }
    583 
    584 #[cfg(any(target_os = "linux", target_os = "macos"))]
    585 fn cleanup_created_directory(entry: &CreatedDirectory) -> Result<(), ()> {
    586     let current = open_directory_at(&entry.parent, &entry.name).map_err(|_| ())?;
    587     let held_identity = validate_secure_directory(&entry.held, true).map_err(|_| ())?;
    588     let current_identity = validate_secure_directory(&current, true).map_err(|_| ())?;
    589     if held_identity != entry.identity || current_identity != entry.identity {
    590         return Err(());
    591     }
    592     unlinkat(&entry.parent, &entry.name, AtFlags::REMOVEDIR).map_err(|_| ())?;
    593     entry.parent.sync_all().map_err(|_| ())
    594 }
    595 
    596 #[cfg(any(target_os = "linux", target_os = "macos"))]
    597 fn normalize_mode<T: Into<u32>>(raw: T) -> u32 {
    598     raw.into()
    599 }
    600 
    601 #[cfg(any(target_os = "linux", target_os = "macos"))]
    602 fn normalize_device<T: TryInto<u64>>(raw: T) -> Result<u64, T::Error> {
    603     raw.try_into()
    604 }
    605 
    606 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
    607 mod tests {
    608     use std::{
    609         error::Error as _,
    610         ffi::OsString,
    611         fs,
    612         os::unix::fs::{MetadataExt, PermissionsExt, symlink},
    613         path::{Path, PathBuf},
    614         sync::Mutex,
    615     };
    616 
    617     use tempfile::TempDir;
    618 
    619     use super::{
    620         ProvisionOperations, RuntimeStateDirectoryPlan, StateDirectoryProvisionError,
    621         provision_with_operations,
    622     };
    623     use crate::{
    624         InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
    625         RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
    626     };
    627 
    628     fn context(
    629         platform: RadrootsPlatform,
    630         profile: RadrootsPathProfile,
    631         root: &Path,
    632     ) -> RuntimeContext {
    633         let environment = match platform {
    634             RadrootsPlatform::Linux => RadrootsHostEnvironment {
    635                 xdg_data_home: Some(root.to_path_buf()),
    636                 xdg_config_home: Some(root.join("config-root")),
    637                 xdg_state_home: Some(root.join("state-root")),
    638                 xdg_cache_home: Some(root.join("cache-root")),
    639                 xdg_runtime_dir: Some(root.join("runtime-root")),
    640                 ..RadrootsHostEnvironment::default()
    641             },
    642             RadrootsPlatform::Macos => RadrootsHostEnvironment {
    643                 home_dir: Some(root.to_path_buf()),
    644                 ..RadrootsHostEnvironment::default()
    645             },
    646             _ => RadrootsHostEnvironment::default(),
    647         };
    648         let repo_local_root =
    649             matches!(profile, RadrootsPathProfile::RepoLocal).then(|| root.to_path_buf());
    650         let bootstrap = RuntimeContextBootstrap::new(
    651             profile,
    652             repo_local_root,
    653             if matches!(profile, RadrootsPathProfile::RepoLocal) {
    654                 RuntimeContextSource::BootstrapCli
    655             } else {
    656                 RuntimeContextSource::SafeDefault
    657             },
    658             RuntimeContextSource::BootstrapCli,
    659         )
    660         .expect("bootstrap");
    661         RuntimeContext::resolve(
    662             &RadrootsPathResolver::new(platform, environment),
    663             bootstrap,
    664             ServiceId::new("myc").expect("service"),
    665             InstanceId::new("primary").expect("instance"),
    666         )
    667         .expect("context")
    668     }
    669 
    670     fn prepare_state_root(context: &RuntimeContext) -> PathBuf {
    671         let root = context.paths().state_root().to_path_buf();
    672         fs::create_dir_all(&root).expect("state root");
    673         fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).expect("root mode");
    674         root
    675     }
    676 
    677     #[test]
    678     fn root_and_owner_validation_rejects_invalid_inputs() {
    679         for root in ["relative", "/", "/valid/../other"] {
    680             assert_eq!(
    681                 super::validate_absolute_root(Path::new(root)),
    682                 Err(StateDirectoryProvisionError::InvalidPlan)
    683             );
    684         }
    685         super::validate_absolute_root(Path::new("/valid/root")).unwrap();
    686         let owner = rustix::process::geteuid().as_raw();
    687         for (directory, uid) in [(false, owner), (true, owner.wrapping_add(1))] {
    688             assert_eq!(
    689                 super::validate_directory_status(directory, uid, 0o700, true),
    690                 Err(StateDirectoryProvisionError::UnsafeDirectory)
    691             );
    692         }
    693     }
    694 
    695     #[test]
    696     fn retained_directory_checks_reject_a_descriptor_for_another_directory() {
    697         let temporary = TempDir::new().unwrap();
    698         let root = temporary.path().canonicalize().unwrap();
    699         for name in ["expected", "other"] {
    700             fs::create_dir(root.join(name)).unwrap();
    701             fs::set_permissions(root.join(name), fs::Permissions::from_mode(0o700)).unwrap();
    702         }
    703         let parent = super::open_absolute_directory(&root).unwrap();
    704         let held = super::open_directory_at(&parent, "expected".as_ref()).unwrap();
    705         let expected = super::validate_secure_directory(&held, true).unwrap();
    706         let mut binding = super::DirectoryBinding {
    707             parent,
    708             name: "expected".into(),
    709             held,
    710             identity: expected,
    711             exact_owner_mode: true,
    712         };
    713         super::validate_directory_binding(&binding).unwrap();
    714         super::validate_absolute_directory_binding(&root.join("expected"), &binding.held, expected)
    715             .unwrap();
    716         binding.held = super::open_directory_at(&binding.parent, "other".as_ref()).unwrap();
    717         assert!(super::validate_directory_binding(&binding).is_err());
    718         assert!(
    719             super::validate_absolute_directory_binding(
    720                 &root.join("expected"),
    721                 &binding.held,
    722                 expected
    723             )
    724             .is_err()
    725         );
    726         assert!(root.join("expected").is_dir());
    727         assert!(root.join("other").is_dir());
    728     }
    729 
    730     struct ReplaceAfterCreation {
    731         component: usize,
    732         original: PathBuf,
    733         displaced: PathBuf,
    734     }
    735 
    736     impl ProvisionOperations for ReplaceAfterCreation {
    737         fn after_create(&self, component_index: usize) -> Result<(), StateDirectoryProvisionError> {
    738             if component_index == self.component {
    739                 fs::rename(&self.original, &self.displaced).unwrap();
    740                 fs::create_dir(&self.original).unwrap();
    741                 fs::set_permissions(&self.original, fs::Permissions::from_mode(0o700)).unwrap();
    742             }
    743             Ok(())
    744         }
    745     }
    746 
    747     #[test]
    748     fn successful_creation_hooks_cannot_hide_root_or_suffix_replacement() {
    749         for (component, replace_root) in [(0, true), (2, true), (2, false)] {
    750             let temporary = TempDir::new().unwrap();
    751             let context = context(
    752                 RadrootsPlatform::Linux,
    753                 RadrootsPathProfile::RepoLocal,
    754                 temporary.path(),
    755             );
    756             let root = prepare_state_root(&context);
    757             let original = if replace_root {
    758                 root.clone()
    759             } else {
    760                 root.join("services")
    761             };
    762             let displaced = temporary.path().join("displaced");
    763             let operations = ReplaceAfterCreation {
    764                 component,
    765                 original: original.clone(),
    766                 displaced: displaced.clone(),
    767             };
    768             assert_eq!(
    769                 provision_with_operations(&context.state_directory_plan().unwrap(), &operations),
    770                 Err(StateDirectoryProvisionError::Cleanup)
    771             );
    772             assert!(original.is_dir());
    773             assert!(displaced.is_dir());
    774             assert_ne!(
    775                 fs::metadata(&original).unwrap().ino(),
    776                 fs::metadata(&displaced).unwrap().ino()
    777             );
    778         }
    779     }
    780 
    781     #[test]
    782     fn repo_local_creates_only_the_exact_canonical_suffix() {
    783         let temporary = TempDir::new().expect("temporary root");
    784         let context = context(
    785             RadrootsPlatform::Linux,
    786             RadrootsPathProfile::RepoLocal,
    787             temporary.path(),
    788         );
    789         let state_root = prepare_state_root(&context);
    790 
    791         context
    792             .state_directory_plan()
    793             .expect("plan")
    794             .provision()
    795             .expect("provision");
    796 
    797         let expected = state_root.join("services/myc/primary");
    798         assert_eq!(context.paths().state(), expected);
    799         for directory in [
    800             state_root.join("services"),
    801             state_root.join("services/myc"),
    802             expected,
    803         ] {
    804             let metadata = fs::metadata(directory).expect("created directory");
    805             assert!(metadata.is_dir());
    806             assert_eq!(metadata.permissions().mode() & 0o777, 0o700);
    807         }
    808         assert_eq!(
    809             fs::read_dir(temporary.path())
    810                 .expect("base inventory")
    811                 .map(|entry| entry.expect("entry").file_name())
    812                 .collect::<Vec<_>>(),
    813             vec![OsString::from("data")],
    814         );
    815     }
    816 
    817     #[test]
    818     fn linux_and_macos_interactive_profiles_create_the_exact_suffix() {
    819         let linux = TempDir::new().expect("linux root");
    820         let linux_context = context(
    821             RadrootsPlatform::Linux,
    822             RadrootsPathProfile::InteractiveUser,
    823             linux.path(),
    824         );
    825         let linux_root = prepare_state_root(&linux_context);
    826         linux_context
    827             .state_directory_plan()
    828             .expect("linux plan")
    829             .provision()
    830             .expect("linux provision");
    831         assert_eq!(
    832             linux_context.paths().state(),
    833             linux_root.join("services/myc/primary")
    834         );
    835 
    836         let macos = TempDir::new().expect("macOS home");
    837         let macos_context = context(
    838             RadrootsPlatform::Macos,
    839             RadrootsPathProfile::InteractiveUser,
    840             macos.path(),
    841         );
    842         let macos_root = prepare_state_root(&macos_context);
    843         macos_context
    844             .state_directory_plan()
    845             .expect("macOS plan")
    846             .provision()
    847             .expect("macOS provision");
    848         assert_eq!(
    849             macos_context.paths().state(),
    850             macos_root.join("services/myc/primary")
    851         );
    852     }
    853 
    854     #[test]
    855     fn existing_directories_are_validated_without_permission_repair() {
    856         let temporary = TempDir::new().expect("temporary root");
    857         let context = context(
    858             RadrootsPlatform::Linux,
    859             RadrootsPathProfile::RepoLocal,
    860             temporary.path(),
    861         );
    862         let root = prepare_state_root(&context);
    863         fs::create_dir_all(context.paths().state()).expect("existing suffix");
    864         fs::set_permissions(root.join("services/myc"), fs::Permissions::from_mode(0o755))
    865             .expect("safe existing mode");
    866         fs::set_permissions(context.paths().state(), fs::Permissions::from_mode(0o770))
    867             .expect("unsafe mode");
    868 
    869         assert_eq!(
    870             context.state_directory_plan().expect("plan").provision(),
    871             Err(StateDirectoryProvisionError::UnsafeDirectory)
    872         );
    873         assert_eq!(
    874             fs::metadata(context.paths().state())
    875                 .expect("state metadata")
    876                 .permissions()
    877                 .mode()
    878                 & 0o777,
    879             0o770
    880         );
    881         assert_eq!(
    882             fs::metadata(root.join("services/myc"))
    883                 .expect("service metadata")
    884                 .permissions()
    885                 .mode()
    886                 & 0o777,
    887             0o755
    888         );
    889     }
    890 
    891     #[test]
    892     fn symlink_entries_are_rejected_without_following_or_repairing() {
    893         let temporary = TempDir::new().expect("temporary root");
    894         let context = context(
    895             RadrootsPlatform::Linux,
    896             RadrootsPathProfile::RepoLocal,
    897             temporary.path(),
    898         );
    899         let root = prepare_state_root(&context);
    900         let outside = temporary.path().join("outside");
    901         fs::create_dir(&outside).expect("outside");
    902         symlink(&outside, root.join("services")).expect("symlink");
    903 
    904         assert_eq!(
    905             context.state_directory_plan().expect("plan").provision(),
    906             Err(StateDirectoryProvisionError::DirectoryConflict)
    907         );
    908         assert!(
    909             outside
    910                 .read_dir()
    911                 .expect("outside inventory")
    912                 .next()
    913                 .is_none()
    914         );
    915     }
    916 
    917     #[test]
    918     fn service_host_is_existing_only() {
    919         let temporary = TempDir::new().expect("temporary root");
    920         let plan = RuntimeStateDirectoryPlan {
    921             profile: RadrootsPathProfile::ServiceHost,
    922             state_root: temporary.path().join("state-root"),
    923             service: ServiceId::new("myc").expect("service"),
    924             instance: InstanceId::new("primary").expect("instance"),
    925         };
    926         fs::create_dir(&plan.state_root).expect("state root");
    927         fs::set_permissions(&plan.state_root, fs::Permissions::from_mode(0o700))
    928             .expect("root mode");
    929 
    930         assert_eq!(
    931             plan.provision(),
    932             Err(StateDirectoryProvisionError::MissingDirectory)
    933         );
    934         assert!(!plan.state_root.join("services").exists());
    935 
    936         fs::create_dir_all(plan.state_root.join("services/myc/primary"))
    937             .expect("preprovisioned suffix");
    938         for directory in [
    939             plan.state_root.join("services"),
    940             plan.state_root.join("services/myc"),
    941             plan.state_root.join("services/myc/primary"),
    942         ] {
    943             fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).expect("suffix mode");
    944         }
    945         plan.provision().expect("existing-only validation");
    946     }
    947 
    948     struct FailingOperations {
    949         fail_after: usize,
    950     }
    951 
    952     impl ProvisionOperations for FailingOperations {
    953         fn after_create(&self, component_index: usize) -> Result<(), StateDirectoryProvisionError> {
    954             if component_index == self.fail_after {
    955                 Err(StateDirectoryProvisionError::Filesystem)
    956             } else {
    957                 Ok(())
    958             }
    959         }
    960     }
    961 
    962     #[test]
    963     fn partial_creation_failure_removes_only_exact_created_identities() {
    964         let temporary = TempDir::new().expect("temporary root");
    965         let context = context(
    966             RadrootsPlatform::Linux,
    967             RadrootsPathProfile::RepoLocal,
    968             temporary.path(),
    969         );
    970         let root = prepare_state_root(&context);
    971         let plan = context.state_directory_plan().expect("plan");
    972 
    973         assert_eq!(
    974             provision_with_operations(&plan, &FailingOperations { fail_after: 1 }),
    975             Err(StateDirectoryProvisionError::Filesystem)
    976         );
    977         assert!(!root.join("services").exists());
    978     }
    979 
    980     struct ReplacingOperations {
    981         created: PathBuf,
    982         displaced: PathBuf,
    983         ran: Mutex<bool>,
    984     }
    985 
    986     impl ProvisionOperations for ReplacingOperations {
    987         fn after_create(&self, component_index: usize) -> Result<(), StateDirectoryProvisionError> {
    988             if component_index == 0 {
    989                 fs::rename(&self.created, &self.displaced).expect("displace created directory");
    990                 fs::create_dir(&self.created).expect("replacement directory");
    991                 fs::set_permissions(&self.created, fs::Permissions::from_mode(0o700))
    992                     .expect("replacement mode");
    993                 *self.ran.lock().expect("replacement flag") = true;
    994                 return Err(StateDirectoryProvisionError::Filesystem);
    995             }
    996             Ok(())
    997         }
    998     }
    999 
   1000     #[test]
   1001     fn cleanup_preserves_a_replacement_with_an_unmatched_identity() {
   1002         let temporary = TempDir::new().expect("temporary root");
   1003         let context = context(
   1004             RadrootsPlatform::Linux,
   1005             RadrootsPathProfile::RepoLocal,
   1006             temporary.path(),
   1007         );
   1008         let root = prepare_state_root(&context);
   1009         let operations = ReplacingOperations {
   1010             created: root.join("services"),
   1011             displaced: root.join("displaced-services"),
   1012             ran: Mutex::new(false),
   1013         };
   1014 
   1015         assert_eq!(
   1016             provision_with_operations(&context.state_directory_plan().expect("plan"), &operations,),
   1017             Err(StateDirectoryProvisionError::Cleanup)
   1018         );
   1019         assert!(*operations.ran.lock().expect("replacement flag"));
   1020         assert!(operations.created.is_dir());
   1021         assert_ne!(
   1022             fs::metadata(&operations.created)
   1023                 .expect("replacement")
   1024                 .ino(),
   1025             fs::metadata(&operations.displaced)
   1026                 .expect("displaced")
   1027                 .ino(),
   1028         );
   1029     }
   1030 
   1031     struct ReplacingRootOperations {
   1032         root: PathBuf,
   1033         displaced: PathBuf,
   1034     }
   1035 
   1036     impl ProvisionOperations for ReplacingRootOperations {
   1037         fn after_create(&self, component_index: usize) -> Result<(), StateDirectoryProvisionError> {
   1038             if component_index == 0 {
   1039                 fs::rename(&self.root, &self.displaced).expect("displace state root");
   1040                 fs::create_dir(&self.root).expect("replacement state root");
   1041                 fs::set_permissions(&self.root, fs::Permissions::from_mode(0o700))
   1042                     .expect("replacement root mode");
   1043                 return Err(StateDirectoryProvisionError::Filesystem);
   1044             }
   1045             Ok(())
   1046         }
   1047     }
   1048 
   1049     #[test]
   1050     fn state_root_replacement_blocks_cleanup_and_preserves_both_identities() {
   1051         let temporary = TempDir::new().expect("temporary root");
   1052         let context = context(
   1053             RadrootsPlatform::Linux,
   1054             RadrootsPathProfile::RepoLocal,
   1055             temporary.path(),
   1056         );
   1057         let root = prepare_state_root(&context);
   1058         let displaced = temporary.path().join("displaced-data");
   1059         let operations = ReplacingRootOperations {
   1060             root: root.clone(),
   1061             displaced: displaced.clone(),
   1062         };
   1063 
   1064         assert_eq!(
   1065             provision_with_operations(&context.state_directory_plan().expect("plan"), &operations,),
   1066             Err(StateDirectoryProvisionError::Cleanup)
   1067         );
   1068         assert!(root.is_dir());
   1069         assert!(displaced.join("services").is_dir());
   1070         assert_ne!(
   1071             fs::metadata(root).expect("replacement root").ino(),
   1072             fs::metadata(displaced).expect("displaced root").ino(),
   1073         );
   1074     }
   1075 
   1076     #[test]
   1077     fn plan_and_errors_do_not_render_paths_or_identities() {
   1078         let temporary = TempDir::new().expect("temporary root");
   1079         let context = context(
   1080             RadrootsPlatform::Linux,
   1081             RadrootsPathProfile::RepoLocal,
   1082             temporary.path(),
   1083         );
   1084         let plan = context.state_directory_plan().expect("plan");
   1085         let rendered = format!("{plan:?}");
   1086         assert!(!rendered.contains(temporary.path().to_string_lossy().as_ref()));
   1087         assert!(!rendered.contains("myc"));
   1088         assert!(!rendered.contains("primary"));
   1089         for failure in [
   1090             StateDirectoryProvisionError::InvalidPlan,
   1091             StateDirectoryProvisionError::UnsupportedPlatform,
   1092             StateDirectoryProvisionError::UnsupportedProfile,
   1093             StateDirectoryProvisionError::StateRootUnavailable,
   1094             StateDirectoryProvisionError::MissingDirectory,
   1095             StateDirectoryProvisionError::DirectoryConflict,
   1096             StateDirectoryProvisionError::UnsafeDirectory,
   1097             StateDirectoryProvisionError::Filesystem,
   1098             StateDirectoryProvisionError::Cleanup,
   1099         ] {
   1100             assert!(failure.source().is_none());
   1101             assert!(!format!("{failure:?} {failure}").contains("/"));
   1102         }
   1103     }
   1104 }