rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

config_loader.rs (24167B)


      1 //! Secure descriptor-bound configuration loading and create-new initialization.
      2 
      3 use core::fmt;
      4 use std::{error::Error, path::Path};
      5 
      6 use crate::{
      7     RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES, RhiConfigDocumentV1, RhiConfigProfile, RhiConfigV1Error,
      8     RhiRuntimeContext, parse_rhi_config_v1,
      9 };
     10 
     11 /// Stable source-free secure configuration I/O failure classification.
     12 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     13 pub enum RhiConfigLoadErrorKind {
     14     InvalidPath,
     15     Missing,
     16     AlreadyExists,
     17     InsecureParent,
     18     InsecureArtifact,
     19     TooLarge,
     20     Io,
     21     InvalidDocument,
     22     UnsupportedPlatform,
     23 }
     24 
     25 /// One path- and content-free secure configuration failure.
     26 #[derive(Clone, Copy, PartialEq, Eq)]
     27 pub struct RhiConfigLoadError {
     28     kind: RhiConfigLoadErrorKind,
     29 }
     30 
     31 impl RhiConfigLoadError {
     32     const fn new(kind: RhiConfigLoadErrorKind) -> Self {
     33         Self { kind }
     34     }
     35 
     36     #[must_use]
     37     pub const fn kind(self) -> RhiConfigLoadErrorKind {
     38         self.kind
     39     }
     40 }
     41 
     42 impl fmt::Debug for RhiConfigLoadError {
     43     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     44         formatter
     45             .debug_struct("RhiConfigLoadError")
     46             .field("kind", &self.kind)
     47             .finish()
     48     }
     49 }
     50 
     51 impl fmt::Display for RhiConfigLoadError {
     52     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     53         formatter.write_str(match self.kind {
     54             RhiConfigLoadErrorKind::InvalidPath => "configuration path is invalid",
     55             RhiConfigLoadErrorKind::Missing => "configuration document is missing",
     56             RhiConfigLoadErrorKind::AlreadyExists => "configuration document already exists",
     57             RhiConfigLoadErrorKind::InsecureParent => "configuration parent is insecure",
     58             RhiConfigLoadErrorKind::InsecureArtifact => "configuration artifact is insecure",
     59             RhiConfigLoadErrorKind::TooLarge => "configuration document exceeds its size limit",
     60             RhiConfigLoadErrorKind::Io => "configuration storage failed",
     61             RhiConfigLoadErrorKind::InvalidDocument => "configuration document is invalid",
     62             RhiConfigLoadErrorKind::UnsupportedPlatform => {
     63                 "secure configuration storage is unsupported"
     64             }
     65         })
     66     }
     67 }
     68 
     69 impl Error for RhiConfigLoadError {}
     70 
     71 /// Loads one selected configuration through the governed descriptor boundary.
     72 pub fn load_rhi_config_document(
     73     runtime: &RhiRuntimeContext,
     74 ) -> Result<RhiConfigDocumentV1, RhiConfigLoadError> {
     75     load_rhi_config_document_at(runtime.selected_config_path(), profile(runtime))
     76 }
     77 
     78 /// Loads one absolute candidate document without changing the selected path.
     79 pub fn load_rhi_config_candidate(
     80     runtime: &RhiRuntimeContext,
     81     candidate: &Path,
     82 ) -> Result<RhiConfigDocumentV1, RhiConfigLoadError> {
     83     load_rhi_config_document_at(candidate, profile(runtime))
     84 }
     85 
     86 /// Validates and creates the selected non-secret configuration exactly once.
     87 pub fn initialize_rhi_config_document(
     88     runtime: &RhiRuntimeContext,
     89     bytes: &[u8],
     90 ) -> Result<RhiConfigDocumentV1, RhiConfigLoadError> {
     91     if bytes.len() > RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES {
     92         return Err(RhiConfigLoadError::new(RhiConfigLoadErrorKind::TooLarge));
     93     }
     94     let document = parse_rhi_config_v1(bytes, profile(runtime)).map_err(map_document)?;
     95     persist_create_new(runtime.selected_config_path(), bytes)?;
     96     Ok(document)
     97 }
     98 
     99 fn profile(runtime: &RhiRuntimeContext) -> RhiConfigProfile {
    100     match runtime.profile() {
    101         crate::RhiBootstrapProfileV1::RepoLocal => RhiConfigProfile::RepoLocal,
    102         crate::RhiBootstrapProfileV1::ServiceHost | crate::RhiBootstrapProfileV1::Interactive => {
    103             RhiConfigProfile::Production
    104         }
    105     }
    106 }
    107 
    108 fn map_document(_: RhiConfigV1Error) -> RhiConfigLoadError {
    109     RhiConfigLoadError::new(RhiConfigLoadErrorKind::InvalidDocument)
    110 }
    111 
    112 #[cfg(any(target_os = "linux", target_os = "macos"))]
    113 fn load_rhi_config_document_at(
    114     path: &Path,
    115     profile: RhiConfigProfile,
    116 ) -> Result<RhiConfigDocumentV1, RhiConfigLoadError> {
    117     let bytes = native::read_existing(path, RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES)?;
    118     parse_rhi_config_v1(&bytes, profile).map_err(map_document)
    119 }
    120 
    121 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    122 fn load_rhi_config_document_at(
    123     _path: &Path,
    124     _profile: RhiConfigProfile,
    125 ) -> Result<RhiConfigDocumentV1, RhiConfigLoadError> {
    126     Err(RhiConfigLoadError::new(
    127         RhiConfigLoadErrorKind::UnsupportedPlatform,
    128     ))
    129 }
    130 
    131 #[cfg(any(target_os = "linux", target_os = "macos"))]
    132 fn persist_create_new(path: &Path, bytes: &[u8]) -> Result<(), RhiConfigLoadError> {
    133     native::persist_create_new(path, bytes)
    134 }
    135 
    136 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    137 fn persist_create_new(_path: &Path, _bytes: &[u8]) -> Result<(), RhiConfigLoadError> {
    138     Err(RhiConfigLoadError::new(
    139         RhiConfigLoadErrorKind::UnsupportedPlatform,
    140     ))
    141 }
    142 
    143 #[cfg(any(target_os = "linux", target_os = "macos"))]
    144 pub(crate) fn read_secure_bounded_file(
    145     path: &Path,
    146     maximum: usize,
    147 ) -> Result<Vec<u8>, RhiConfigLoadError> {
    148     native::read_existing(path, maximum)
    149 }
    150 
    151 #[cfg(any(target_os = "linux", target_os = "macos"))]
    152 mod native {
    153     use std::ffi::OsString;
    154     use std::fs::File;
    155     use std::io::{Read, Write};
    156     use std::os::unix::ffi::OsStrExt;
    157     use std::path::{Component, Path, PathBuf};
    158 
    159     use rustix::fs::{FileType, Mode, OFlags, fchmod, fstat, open, openat, unlinkat};
    160     use rustix::process::geteuid;
    161 
    162     use super::{RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES, RhiConfigLoadError, RhiConfigLoadErrorKind};
    163 
    164     #[derive(Clone, Copy, PartialEq, Eq)]
    165     struct Identity {
    166         device: u64,
    167         inode: u64,
    168     }
    169 
    170     struct SelectedPath {
    171         parent: PathBuf,
    172         name: OsString,
    173     }
    174 
    175     impl SelectedPath {
    176         fn parse(path: &Path) -> Result<Self, RhiConfigLoadError> {
    177             if !path.is_absolute()
    178                 || path.as_os_str().as_bytes().len() > 4_096
    179                 || path.components().any(|component| {
    180                     !matches!(component, Component::RootDir | Component::Normal(_))
    181                 })
    182             {
    183                 return Err(error(RhiConfigLoadErrorKind::InvalidPath));
    184             }
    185             let name = match path.components().next_back() {
    186                 Some(Component::Normal(name)) if !name.as_bytes().is_empty() => name.to_os_string(),
    187                 _ => return Err(error(RhiConfigLoadErrorKind::InvalidPath)),
    188             };
    189             let parent = path
    190                 .parent()
    191                 .filter(|parent| parent.is_absolute())
    192                 .ok_or_else(|| error(RhiConfigLoadErrorKind::InvalidPath))?;
    193             Ok(Self {
    194                 parent: parent.to_path_buf(),
    195                 name,
    196             })
    197         }
    198     }
    199 
    200     pub(super) fn read_existing(
    201         path: &Path,
    202         maximum: usize,
    203     ) -> Result<Vec<u8>, RhiConfigLoadError> {
    204         let selected = SelectedPath::parse(path)?;
    205         let parent = open_parent(&selected.parent)?;
    206         let parent_identity = directory_identity(&parent)?;
    207         let descriptor = openat(
    208             &parent,
    209             &selected.name,
    210             OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK,
    211             Mode::empty(),
    212         )
    213         .map_err(|source| {
    214             error(if source == rustix::io::Errno::NOENT {
    215                 RhiConfigLoadErrorKind::Missing
    216             } else {
    217                 RhiConfigLoadErrorKind::InsecureArtifact
    218             })
    219         })?;
    220         let mut file = File::from(descriptor);
    221         let status = fstat(&file).map_err(|_| error(RhiConfigLoadErrorKind::InsecureArtifact))?;
    222         let (identity, length) = file_identity(&status, maximum)?;
    223         let mut bytes = Vec::with_capacity(length);
    224         Read::by_ref(&mut file)
    225             .take(u64::try_from(length).unwrap_or(u64::MAX).saturating_add(1))
    226             .read_to_end(&mut bytes)
    227             .map_err(|_| error(RhiConfigLoadErrorKind::Io))?;
    228         if bytes.len() != length {
    229             return Err(error(RhiConfigLoadErrorKind::InsecureArtifact));
    230         }
    231         validate_current(
    232             &selected,
    233             &parent,
    234             parent_identity,
    235             &file,
    236             identity,
    237             length,
    238             maximum,
    239         )?;
    240         Ok(bytes)
    241     }
    242 
    243     pub(super) fn persist_create_new(path: &Path, bytes: &[u8]) -> Result<(), RhiConfigLoadError> {
    244         let selected = SelectedPath::parse(path)?;
    245         let parent = open_parent(&selected.parent)?;
    246         let parent_identity = directory_identity(&parent)?;
    247         let descriptor = openat(
    248             &parent,
    249             &selected.name,
    250             OFlags::WRONLY
    251                 | OFlags::CREATE
    252                 | OFlags::EXCL
    253                 | OFlags::NOFOLLOW
    254                 | OFlags::CLOEXEC
    255                 | OFlags::NONBLOCK,
    256             Mode::RUSR | Mode::WUSR,
    257         )
    258         .map_err(|source| {
    259             error(if source == rustix::io::Errno::EXIST {
    260                 RhiConfigLoadErrorKind::AlreadyExists
    261             } else {
    262                 RhiConfigLoadErrorKind::Io
    263             })
    264         })?;
    265         let mut file = File::from(descriptor);
    266         let status = fstat(&file).map_err(|_| error(RhiConfigLoadErrorKind::InsecureArtifact))?;
    267         let identity = status_identity(&status)?;
    268         let result = (|| {
    269             fchmod(&file, Mode::RUSR | Mode::WUSR)
    270                 .map_err(|_| error(RhiConfigLoadErrorKind::Io))?;
    271             file.write_all(bytes)
    272                 .and_then(|()| file.sync_all())
    273                 .map_err(|_| error(RhiConfigLoadErrorKind::Io))?;
    274             validate_current(
    275                 &selected,
    276                 &parent,
    277                 parent_identity,
    278                 &file,
    279                 identity,
    280                 bytes.len(),
    281                 RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES,
    282             )?;
    283             parent
    284                 .sync_all()
    285                 .map_err(|_| error(RhiConfigLoadErrorKind::Io))?;
    286             validate_current(
    287                 &selected,
    288                 &parent,
    289                 parent_identity,
    290                 &file,
    291                 identity,
    292                 bytes.len(),
    293                 RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES,
    294             )
    295         })();
    296         if result.is_err() {
    297             cleanup(&parent, &selected.name, identity);
    298         }
    299         result
    300     }
    301 
    302     fn open_parent(path: &Path) -> Result<File, RhiConfigLoadError> {
    303         let mut components = path.components();
    304         if !matches!(components.next(), Some(Component::RootDir)) {
    305             return Err(error(RhiConfigLoadErrorKind::InvalidPath));
    306         }
    307         let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
    308         let mut parent = File::from(
    309             open(Path::new("/"), flags, Mode::empty())
    310                 .map_err(|_| error(RhiConfigLoadErrorKind::InsecureParent))?,
    311         );
    312         for component in components {
    313             let Component::Normal(name) = component else {
    314                 return Err(error(RhiConfigLoadErrorKind::InvalidPath));
    315             };
    316             parent = File::from(
    317                 openat(&parent, name, flags, Mode::empty())
    318                     .map_err(|_| error(RhiConfigLoadErrorKind::InsecureParent))?,
    319             );
    320         }
    321         directory_identity(&parent)?;
    322         Ok(parent)
    323     }
    324 
    325     fn directory_identity(directory: &File) -> Result<Identity, RhiConfigLoadError> {
    326         let status = fstat(directory).map_err(|_| error(RhiConfigLoadErrorKind::InsecureParent))?;
    327         let mode = normalize_mode(status.st_mode);
    328         if !FileType::from_raw_mode(status.st_mode).is_dir()
    329             || status.st_uid != geteuid().as_raw()
    330             || mode & 0o022 != 0
    331         {
    332             return Err(error(RhiConfigLoadErrorKind::InsecureParent));
    333         }
    334         status_identity(&status)
    335     }
    336 
    337     fn file_identity(
    338         status: &rustix::fs::Stat,
    339         maximum: usize,
    340     ) -> Result<(Identity, usize), RhiConfigLoadError> {
    341         let mode = normalize_mode(status.st_mode);
    342         let length =
    343             usize::try_from(status.st_size).map_err(|_| error(RhiConfigLoadErrorKind::TooLarge))?;
    344         if !FileType::from_raw_mode(status.st_mode).is_file()
    345             || normalize_link_count(status.st_nlink) != 1
    346             || status.st_uid != geteuid().as_raw()
    347             || mode & 0o400 == 0
    348             || mode & 0o022 != 0
    349         {
    350             return Err(error(RhiConfigLoadErrorKind::InsecureArtifact));
    351         }
    352         if length > maximum {
    353             return Err(error(RhiConfigLoadErrorKind::TooLarge));
    354         }
    355         Ok((status_identity(status)?, length))
    356     }
    357 
    358     fn status_identity(status: &rustix::fs::Stat) -> Result<Identity, RhiConfigLoadError> {
    359         Ok(Identity {
    360             device: normalize_device(status.st_dev)
    361                 .map_err(|_| error(RhiConfigLoadErrorKind::InsecureArtifact))?,
    362             inode: status.st_ino,
    363         })
    364     }
    365 
    366     pub(super) fn normalize_mode<T: Into<u32>>(raw: T) -> u32 {
    367         raw.into()
    368     }
    369 
    370     pub(super) fn normalize_link_count<T: Into<u64>>(raw: T) -> u64 {
    371         raw.into()
    372     }
    373 
    374     pub(super) fn normalize_device<T: TryInto<u64>>(raw: T) -> Result<u64, T::Error> {
    375         raw.try_into()
    376     }
    377 
    378     fn validate_current(
    379         selected: &SelectedPath,
    380         parent: &File,
    381         parent_identity: Identity,
    382         held: &File,
    383         held_identity: Identity,
    384         length: usize,
    385         maximum: usize,
    386     ) -> Result<(), RhiConfigLoadError> {
    387         if directory_identity(parent)? != parent_identity {
    388             return Err(error(RhiConfigLoadErrorKind::InsecureParent));
    389         }
    390         let current_parent = open_parent(&selected.parent)?;
    391         if directory_identity(&current_parent)? != parent_identity {
    392             return Err(error(RhiConfigLoadErrorKind::InsecureParent));
    393         }
    394         let held_status =
    395             fstat(held).map_err(|_| error(RhiConfigLoadErrorKind::InsecureArtifact))?;
    396         let (current_held, current_length) = file_identity(&held_status, maximum)?;
    397         if current_held != held_identity || current_length != length {
    398             return Err(error(RhiConfigLoadErrorKind::InsecureArtifact));
    399         }
    400         let current = File::from(
    401             openat(
    402                 &current_parent,
    403                 &selected.name,
    404                 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK,
    405                 Mode::empty(),
    406             )
    407             .map_err(|_| error(RhiConfigLoadErrorKind::InsecureArtifact))?,
    408         );
    409         let status =
    410             fstat(&current).map_err(|_| error(RhiConfigLoadErrorKind::InsecureArtifact))?;
    411         let (current_identity, current_length) = file_identity(&status, maximum)?;
    412         if current_identity != held_identity || current_length != length {
    413             return Err(error(RhiConfigLoadErrorKind::InsecureArtifact));
    414         }
    415         Ok(())
    416     }
    417 
    418     fn cleanup(parent: &File, name: &std::ffi::OsStr, identity: Identity) {
    419         let current = openat(
    420             parent,
    421             name,
    422             OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK,
    423             Mode::empty(),
    424         )
    425         .ok()
    426         .map(File::from);
    427         if current.as_ref().is_some_and(|file| {
    428             fstat(file)
    429                 .ok()
    430                 .and_then(|status| status_identity(&status).ok())
    431                 == Some(identity)
    432         }) {
    433             let _ = unlinkat(parent, name, rustix::fs::AtFlags::empty());
    434             let _ = parent.sync_all();
    435         }
    436     }
    437 
    438     const fn error(kind: RhiConfigLoadErrorKind) -> RhiConfigLoadError {
    439         RhiConfigLoadError::new(kind)
    440     }
    441 
    442     #[cfg(test)]
    443     mod tests {
    444         use std::os::unix::fs::PermissionsExt as _;
    445 
    446         use super::*;
    447 
    448         #[test]
    449         fn validation_rejects_a_replaced_parent_path() {
    450             let root = tempfile::tempdir().expect("temporary root");
    451             let parent_path = root.path().join("selected");
    452             std::fs::create_dir(&parent_path).expect("selected parent");
    453             std::fs::set_permissions(&parent_path, std::fs::Permissions::from_mode(0o700))
    454                 .expect("secure selected parent");
    455             let path = parent_path.join("config.toml");
    456             std::fs::write(&path, b"config").expect("selected config");
    457             std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
    458                 .expect("secure selected config");
    459 
    460             let selected = SelectedPath::parse(&path).expect("selected path");
    461             let parent = open_parent(&selected.parent).expect("held parent");
    462             let parent_identity = directory_identity(&parent).expect("parent identity");
    463             let held = File::from(
    464                 openat(
    465                     &parent,
    466                     &selected.name,
    467                     OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK,
    468                     Mode::empty(),
    469                 )
    470                 .expect("held config"),
    471             );
    472             let status = fstat(&held).expect("held status");
    473             let (identity, length) = file_identity(&status, 16).expect("held identity");
    474 
    475             let moved = root.path().join("moved");
    476             std::fs::rename(&parent_path, &moved).expect("move held parent");
    477             std::fs::create_dir(&parent_path).expect("replacement parent");
    478             std::fs::set_permissions(&parent_path, std::fs::Permissions::from_mode(0o700))
    479                 .expect("secure replacement parent");
    480             std::fs::write(parent_path.join("config.toml"), b"config").expect("replacement config");
    481 
    482             assert_eq!(
    483                 validate_current(
    484                     &selected,
    485                     &parent,
    486                     parent_identity,
    487                     &held,
    488                     identity,
    489                     length,
    490                     16,
    491                 )
    492                 .expect_err("parent replacement")
    493                 .kind(),
    494                 RhiConfigLoadErrorKind::InsecureParent
    495             );
    496         }
    497     }
    498 }
    499 
    500 #[cfg(test)]
    501 mod tests {
    502     use super::*;
    503 
    504     #[test]
    505     fn errors_are_source_free_and_path_free() {
    506         for kind in [
    507             RhiConfigLoadErrorKind::InvalidPath,
    508             RhiConfigLoadErrorKind::Missing,
    509             RhiConfigLoadErrorKind::AlreadyExists,
    510             RhiConfigLoadErrorKind::InsecureParent,
    511             RhiConfigLoadErrorKind::InsecureArtifact,
    512             RhiConfigLoadErrorKind::TooLarge,
    513             RhiConfigLoadErrorKind::Io,
    514             RhiConfigLoadErrorKind::InvalidDocument,
    515             RhiConfigLoadErrorKind::UnsupportedPlatform,
    516         ] {
    517             let error = RhiConfigLoadError::new(kind);
    518             assert_eq!(error.kind(), kind);
    519             let rendered = format!("{error} {error:?}");
    520             assert!(!rendered.contains('/'));
    521             assert!(Error::source(&error).is_none());
    522         }
    523     }
    524 
    525     #[cfg(any(target_os = "linux", target_os = "macos"))]
    526     #[test]
    527     fn native_create_read_permissions_and_collision_are_exact() {
    528         use std::os::unix::fs::PermissionsExt as _;
    529 
    530         let directory = tempfile::tempdir().expect("temporary directory");
    531         std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700))
    532             .expect("secure directory");
    533         let path = directory.path().join("config.toml");
    534         let bytes = b"schema = \"radroots.rhi.config\"\n";
    535         native::persist_create_new(&path, bytes).expect("create-new config");
    536         assert_eq!(
    537             std::fs::metadata(&path)
    538                 .expect("metadata")
    539                 .permissions()
    540                 .mode()
    541                 & 0o777,
    542             0o600
    543         );
    544         assert_eq!(
    545             native::read_existing(&path, RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES).expect("secure read"),
    546             bytes
    547         );
    548         assert_eq!(
    549             native::persist_create_new(&path, bytes)
    550                 .expect_err("collision")
    551                 .kind(),
    552             RhiConfigLoadErrorKind::AlreadyExists
    553         );
    554     }
    555 
    556     #[cfg(any(target_os = "linux", target_os = "macos"))]
    557     #[test]
    558     fn native_reader_rejects_insecure_parent_artifact_links_and_oversize() {
    559         use std::os::unix::fs::{PermissionsExt as _, symlink};
    560 
    561         let directory = tempfile::tempdir().expect("temporary directory");
    562         std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700))
    563             .expect("secure directory");
    564         let path = directory.path().join("config.toml");
    565         std::fs::write(&path, b"config").expect("config");
    566         std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o620))
    567             .expect("insecure mode");
    568         assert_eq!(
    569             native::read_existing(&path, RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES)
    570                 .expect_err("group-write rejection")
    571                 .kind(),
    572             RhiConfigLoadErrorKind::InsecureArtifact
    573         );
    574 
    575         std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
    576             .expect("secure mode");
    577         let hardlink = directory.path().join("hardlink.toml");
    578         std::fs::hard_link(&path, &hardlink).expect("hard link");
    579         assert_eq!(
    580             native::read_existing(&path, RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES)
    581                 .expect_err("single-link rejection")
    582                 .kind(),
    583             RhiConfigLoadErrorKind::InsecureArtifact
    584         );
    585         std::fs::remove_file(&hardlink).expect("remove link");
    586 
    587         let symlink_path = directory.path().join("symlink.toml");
    588         symlink(&path, &symlink_path).expect("symlink");
    589         assert_eq!(
    590             native::read_existing(&symlink_path, RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES)
    591                 .expect_err("no-follow rejection")
    592                 .kind(),
    593             RhiConfigLoadErrorKind::InsecureArtifact
    594         );
    595 
    596         std::fs::write(&path, vec![b'x'; RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES + 1])
    597             .expect("oversize");
    598         assert_eq!(
    599             native::read_existing(&path, RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES)
    600                 .expect_err("oversize rejection")
    601                 .kind(),
    602             RhiConfigLoadErrorKind::TooLarge
    603         );
    604 
    605         std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o720))
    606             .expect("insecure parent");
    607         assert_eq!(
    608             native::read_existing(&path, RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES)
    609                 .expect_err("parent rejection")
    610                 .kind(),
    611             RhiConfigLoadErrorKind::InsecureParent
    612         );
    613     }
    614 
    615     #[cfg(any(target_os = "linux", target_os = "macos"))]
    616     #[test]
    617     fn native_metadata_normalization_preserves_width_and_signed_device_rejection() {
    618         assert_eq!(native::normalize_mode(0o600_u16), 0o600);
    619         assert_eq!(native::normalize_mode(0o700_u32), 0o700);
    620         assert_eq!(native::normalize_link_count(1_u16), 1);
    621         assert_eq!(native::normalize_link_count(1_u64), 1);
    622         assert_eq!(native::normalize_device(7_i32), Ok(7));
    623         assert!(native::normalize_device(-1_i32).is_err());
    624     }
    625 
    626     #[cfg(any(target_os = "linux", target_os = "macos"))]
    627     #[test]
    628     fn generic_secure_reader_enforces_the_callers_exact_bound() {
    629         use std::os::unix::fs::PermissionsExt as _;
    630 
    631         let directory = tempfile::tempdir().expect("temporary directory");
    632         std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700))
    633             .expect("secure directory");
    634         let path = directory.path().join("artifact");
    635         std::fs::write(&path, b"four").expect("artifact");
    636         std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
    637             .expect("secure artifact");
    638 
    639         assert_eq!(
    640             read_secure_bounded_file(&path, 4).expect("exact bound"),
    641             b"four"
    642         );
    643         assert_eq!(
    644             read_secure_bounded_file(&path, 3)
    645                 .expect_err("just over bound")
    646                 .kind(),
    647             RhiConfigLoadErrorKind::TooLarge
    648         );
    649     }
    650 }