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 MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES, MycConfigDocumentV1, MycConfigProfile, MycConfigV1Error, 8 MycRuntimeContext, parse_myc_config_v1, 9 }; 10 11 /// Stable source-free secure configuration I/O failure classification. 12 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 13 pub enum MycConfigLoadErrorKind { 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 MycConfigLoadError { 28 kind: MycConfigLoadErrorKind, 29 } 30 31 impl MycConfigLoadError { 32 const fn new(kind: MycConfigLoadErrorKind) -> Self { 33 Self { kind } 34 } 35 36 #[must_use] 37 pub const fn kind(self) -> MycConfigLoadErrorKind { 38 self.kind 39 } 40 } 41 42 impl fmt::Debug for MycConfigLoadError { 43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 44 formatter 45 .debug_struct("MycConfigLoadError") 46 .field("kind", &self.kind) 47 .finish() 48 } 49 } 50 51 impl fmt::Display for MycConfigLoadError { 52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 53 formatter.write_str(match self.kind { 54 MycConfigLoadErrorKind::InvalidPath => "configuration path is invalid", 55 MycConfigLoadErrorKind::Missing => "configuration document is missing", 56 MycConfigLoadErrorKind::AlreadyExists => "configuration document already exists", 57 MycConfigLoadErrorKind::InsecureParent => "configuration parent is insecure", 58 MycConfigLoadErrorKind::InsecureArtifact => "configuration artifact is insecure", 59 MycConfigLoadErrorKind::TooLarge => "configuration document exceeds its size limit", 60 MycConfigLoadErrorKind::Io => "configuration storage failed", 61 MycConfigLoadErrorKind::InvalidDocument => "configuration document is invalid", 62 MycConfigLoadErrorKind::UnsupportedPlatform => { 63 "secure configuration storage is unsupported" 64 } 65 }) 66 } 67 } 68 69 impl Error for MycConfigLoadError {} 70 71 /// Loads one selected configuration through the governed descriptor boundary. 72 pub fn load_myc_config_document( 73 runtime: &MycRuntimeContext, 74 ) -> Result<MycConfigDocumentV1, MycConfigLoadError> { 75 load_myc_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_myc_config_candidate( 80 runtime: &MycRuntimeContext, 81 candidate: &Path, 82 ) -> Result<MycConfigDocumentV1, MycConfigLoadError> { 83 load_myc_config_document_at(candidate, profile(runtime)) 84 } 85 86 /// Validates and creates the selected non-secret configuration exactly once. 87 pub fn initialize_myc_config_document( 88 runtime: &MycRuntimeContext, 89 bytes: &[u8], 90 ) -> Result<MycConfigDocumentV1, MycConfigLoadError> { 91 if bytes.len() > MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES { 92 return Err(MycConfigLoadError::new(MycConfigLoadErrorKind::TooLarge)); 93 } 94 let document = parse_myc_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: &MycRuntimeContext) -> MycConfigProfile { 100 match runtime.profile() { 101 crate::MycBootstrapProfileV1::RepoLocal => MycConfigProfile::RepoLocal, 102 crate::MycBootstrapProfileV1::ServiceHost | crate::MycBootstrapProfileV1::Interactive => { 103 MycConfigProfile::Production 104 } 105 } 106 } 107 108 fn map_document(_: MycConfigV1Error) -> MycConfigLoadError { 109 MycConfigLoadError::new(MycConfigLoadErrorKind::InvalidDocument) 110 } 111 112 #[cfg(any(target_os = "linux", target_os = "macos"))] 113 fn load_myc_config_document_at( 114 path: &Path, 115 profile: MycConfigProfile, 116 ) -> Result<MycConfigDocumentV1, MycConfigLoadError> { 117 let bytes = native::read_existing(path, MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES)?; 118 parse_myc_config_v1(&bytes, profile).map_err(map_document) 119 } 120 121 #[cfg(not(any(target_os = "linux", target_os = "macos")))] 122 fn load_myc_config_document_at( 123 _path: &Path, 124 _profile: MycConfigProfile, 125 ) -> Result<MycConfigDocumentV1, MycConfigLoadError> { 126 Err(MycConfigLoadError::new( 127 MycConfigLoadErrorKind::UnsupportedPlatform, 128 )) 129 } 130 131 #[cfg(any(target_os = "linux", target_os = "macos"))] 132 fn persist_create_new(path: &Path, bytes: &[u8]) -> Result<(), MycConfigLoadError> { 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<(), MycConfigLoadError> { 138 Err(MycConfigLoadError::new( 139 MycConfigLoadErrorKind::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>, MycConfigLoadError> { 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::{MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES, MycConfigLoadError, MycConfigLoadErrorKind}; 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, MycConfigLoadError> { 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(MycConfigLoadErrorKind::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(MycConfigLoadErrorKind::InvalidPath)), 188 }; 189 let parent = path 190 .parent() 191 .filter(|parent| parent.is_absolute()) 192 .ok_or_else(|| error(MycConfigLoadErrorKind::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>, MycConfigLoadError> { 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 MycConfigLoadErrorKind::Missing 216 } else { 217 MycConfigLoadErrorKind::InsecureArtifact 218 }) 219 })?; 220 let mut file = File::from(descriptor); 221 let status = fstat(&file).map_err(|_| error(MycConfigLoadErrorKind::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(MycConfigLoadErrorKind::Io))?; 228 if bytes.len() != length { 229 return Err(error(MycConfigLoadErrorKind::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<(), MycConfigLoadError> { 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 MycConfigLoadErrorKind::AlreadyExists 261 } else { 262 MycConfigLoadErrorKind::Io 263 }) 264 })?; 265 let mut file = File::from(descriptor); 266 let status = fstat(&file).map_err(|_| error(MycConfigLoadErrorKind::InsecureArtifact))?; 267 let identity = status_identity(&status)?; 268 let result = (|| { 269 fchmod(&file, Mode::RUSR | Mode::WUSR) 270 .map_err(|_| error(MycConfigLoadErrorKind::Io))?; 271 file.write_all(bytes) 272 .and_then(|()| file.sync_all()) 273 .map_err(|_| error(MycConfigLoadErrorKind::Io))?; 274 validate_current( 275 &selected, 276 &parent, 277 parent_identity, 278 &file, 279 identity, 280 bytes.len(), 281 MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES, 282 )?; 283 parent 284 .sync_all() 285 .map_err(|_| error(MycConfigLoadErrorKind::Io))?; 286 validate_current( 287 &selected, 288 &parent, 289 parent_identity, 290 &file, 291 identity, 292 bytes.len(), 293 MYC_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, MycConfigLoadError> { 303 let mut components = path.components(); 304 if !matches!(components.next(), Some(Component::RootDir)) { 305 return Err(error(MycConfigLoadErrorKind::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(MycConfigLoadErrorKind::InsecureParent))?, 311 ); 312 for component in components { 313 let Component::Normal(name) = component else { 314 return Err(error(MycConfigLoadErrorKind::InvalidPath)); 315 }; 316 parent = File::from( 317 openat(&parent, name, flags, Mode::empty()) 318 .map_err(|_| error(MycConfigLoadErrorKind::InsecureParent))?, 319 ); 320 } 321 directory_identity(&parent)?; 322 Ok(parent) 323 } 324 325 fn directory_identity(directory: &File) -> Result<Identity, MycConfigLoadError> { 326 let status = fstat(directory).map_err(|_| error(MycConfigLoadErrorKind::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(MycConfigLoadErrorKind::InsecureParent)); 333 } 334 status_identity(&status) 335 } 336 337 fn file_identity( 338 status: &rustix::fs::Stat, 339 maximum: usize, 340 ) -> Result<(Identity, usize), MycConfigLoadError> { 341 let mode = normalize_mode(status.st_mode); 342 let length = 343 usize::try_from(status.st_size).map_err(|_| error(MycConfigLoadErrorKind::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(MycConfigLoadErrorKind::InsecureArtifact)); 351 } 352 if length > maximum { 353 return Err(error(MycConfigLoadErrorKind::TooLarge)); 354 } 355 Ok((status_identity(status)?, length)) 356 } 357 358 fn status_identity(status: &rustix::fs::Stat) -> Result<Identity, MycConfigLoadError> { 359 Ok(Identity { 360 device: normalize_device(status.st_dev) 361 .map_err(|_| error(MycConfigLoadErrorKind::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<(), MycConfigLoadError> { 387 if directory_identity(parent)? != parent_identity { 388 return Err(error(MycConfigLoadErrorKind::InsecureParent)); 389 } 390 let current_parent = open_parent(&selected.parent)?; 391 if directory_identity(¤t_parent)? != parent_identity { 392 return Err(error(MycConfigLoadErrorKind::InsecureParent)); 393 } 394 let held_status = 395 fstat(held).map_err(|_| error(MycConfigLoadErrorKind::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(MycConfigLoadErrorKind::InsecureArtifact)); 399 } 400 let current = File::from( 401 openat( 402 ¤t_parent, 403 &selected.name, 404 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK, 405 Mode::empty(), 406 ) 407 .map_err(|_| error(MycConfigLoadErrorKind::InsecureArtifact))?, 408 ); 409 let status = 410 fstat(¤t).map_err(|_| error(MycConfigLoadErrorKind::InsecureArtifact))?; 411 let (current_identity, current_length) = file_identity(&status, maximum)?; 412 if current_identity != held_identity || current_length != length { 413 return Err(error(MycConfigLoadErrorKind::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: MycConfigLoadErrorKind) -> MycConfigLoadError { 439 MycConfigLoadError::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 MycConfigLoadErrorKind::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 MycConfigLoadErrorKind::InvalidPath, 508 MycConfigLoadErrorKind::Missing, 509 MycConfigLoadErrorKind::AlreadyExists, 510 MycConfigLoadErrorKind::InsecureParent, 511 MycConfigLoadErrorKind::InsecureArtifact, 512 MycConfigLoadErrorKind::TooLarge, 513 MycConfigLoadErrorKind::Io, 514 MycConfigLoadErrorKind::InvalidDocument, 515 MycConfigLoadErrorKind::UnsupportedPlatform, 516 ] { 517 let error = MycConfigLoadError::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.myc.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, MYC_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 MycConfigLoadErrorKind::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, MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES) 570 .expect_err("group-write rejection") 571 .kind(), 572 MycConfigLoadErrorKind::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, MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES) 581 .expect_err("single-link rejection") 582 .kind(), 583 MycConfigLoadErrorKind::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, MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES) 591 .expect_err("no-follow rejection") 592 .kind(), 593 MycConfigLoadErrorKind::InsecureArtifact 594 ); 595 596 std::fs::write(&path, vec![b'x'; MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES + 1]) 597 .expect("oversize"); 598 assert_eq!( 599 native::read_existing(&path, MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES) 600 .expect_err("oversize rejection") 601 .kind(), 602 MycConfigLoadErrorKind::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, MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES) 609 .expect_err("parent rejection") 610 .kind(), 611 MycConfigLoadErrorKind::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 MycConfigLoadErrorKind::TooLarge 648 ); 649 } 650 }