lib.rs (45751B)
1 //! [![git]](https://git.philomathiclife.com/priv_sep/log.html) [![crates-io]](https://crates.io/crates/priv_sep) [![docs-rs]](crate) 2 //! 3 //! [git]: https://git.philomathiclife.com/git_badge.svg 4 //! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust 5 //! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs 6 //! 7 //! `priv_sep` is a library that uses the system's libc to perform privilege separation and privilege reduction 8 //! for Unix-like platforms. 9 //! 10 //! ## `priv_sep` in action for OpenBSD 11 //! 12 //! ```no_run 13 //! # #[cfg(target_os = "openbsd")] 14 //! use core::{convert::Infallible, ffi::CStr}; 15 //! # #[cfg(not(target_os = "openbsd"))] 16 //! # use core::convert::Infallible; 17 //! # #[cfg(target_os = "openbsd")] 18 //! use priv_sep::{Permissions, PrivDropErr, Promise, Promises}; 19 //! use std::{ 20 //! fs, 21 //! io::Error, 22 //! net::{Ipv6Addr, SocketAddrV6}, 23 //! }; 24 //! use tokio::net::TcpListener; 25 //! # #[cfg(not(target_os = "openbsd"))] 26 //! # fn main() {} 27 //! # #[cfg(target_os = "openbsd")] 28 //! #[tokio::main(flavor = "current_thread")] 29 //! async fn main() -> Result<Infallible, PrivDropErr<Error>> { 30 //! /// Config file. 31 //! const CONFIG: &CStr = c"config"; 32 //! /// Config file. 33 //! const CONFIG_STR: &str = match CONFIG.to_str() { 34 //! Ok(val) => val, 35 //! Err(_) => panic!("config is not a valid str"), 36 //! }; 37 //! // Get the user ID and group ID for nobody from `passwd(5)`. 38 //! // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. 39 //! // `pledge(2)` `id`, `inet`, `rpath`, `stdio`, and `unveil`. 40 //! // Bind to TCP `[::1]:443` as root. 41 //! // `setgroups(2)` to drop all supplementary groups. 42 //! // `setresgid(2)` to the group ID associated with nobody. 43 //! // `setresuid(2)` to the user ID associated with nobody. 44 //! // Remove `id` from our `pledge(2)`d promises. 45 //! let (listener, mut promises) = Promises::new_chroot_then_priv_drop_async( 46 //! c"nobody", 47 //! c"/path/chroot/", 48 //! [Promise::Inet, Promise::Rpath, Promise::Unveil], 49 //! false, 50 //! async || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await, 51 //! ) 52 //! .await?; 53 //! // At this point, the process is running under nobody. 54 //! // Only allow file system access to `config` and only allow read access to it. 55 //! Permissions::READ.unveil(CONFIG)?; 56 //! // Read `config`. 57 //! // This will of course fail if the file does not exist or nobody does not 58 //! // have read permissions. 59 //! let config = fs::read(CONFIG_STR).map_err(PrivDropErr::Other)?; 60 //! // Remove file system access. 61 //! Permissions::NONE.unveil(CONFIG)?; 62 //! // Remove `rpath` and `unveil` from our `pledge(2)`d promises 63 //! // (i.e., only have `inet` and `stdio` abilities when we begin accepting TCP connections). 64 //! promises.remove_promises_then_pledge([Promise::Rpath, Promise::Unveil])?; 65 //! loop { 66 //! // Handle TCP connections. 67 //! if let Ok((_, ip)) = listener.accept().await { 68 //! assert!(ip.is_ipv6()); 69 //! } 70 //! } 71 //! } 72 //! ``` 73 //! 74 //! ## `priv_sep` in action for Unix-like OSes 75 //! 76 //! ```no_run 77 //! use core::convert::Infallible; 78 //! use priv_sep::{PrivDropErr, UserInfo}; 79 //! use std::{ 80 //! io::Error, 81 //! net::{Ipv6Addr, SocketAddrV6}, 82 //! }; 83 //! use tokio::net::TcpListener; 84 //! #[tokio::main(flavor = "current_thread")] 85 //! async fn main() -> Result<Infallible, PrivDropErr<Error>> { 86 //! // Get the user ID and group ID for nobody from `passwd(5)`. 87 //! // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. 88 //! // Bind to TCP `[::1]:443` as root. 89 //! // `setgroups(2)` to drop all supplementary groups. 90 //! // `setresgid(2)` to the group ID associated with nobody. 91 //! // `setresuid(2)` to the user ID associated with nobody. 92 //! let listener = 93 //! UserInfo::chroot_then_priv_drop_async(c"nobody", c"/path/chroot/", false, async || { 94 //! TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await 95 //! }) 96 //! .await?; 97 //! // At this point, the process is running under nobody. 98 //! loop { 99 //! // Handle TCP connections. 100 //! if let Ok((_, ip)) = listener.accept().await { 101 //! assert!(ip.is_ipv6()); 102 //! } 103 //! } 104 //! } 105 //! ``` 106 #![cfg_attr(docsrs, feature(doc_cfg))] 107 #![no_std] 108 #![cfg(any( 109 target_os = "dragonfly", 110 target_os = "freebsd", 111 target_os = "linux", 112 target_os = "macos", 113 target_os = "netbsd", 114 target_os = "openbsd" 115 ))] 116 #![expect( 117 clippy::pub_use, 118 reason = "don't want Errno nor openbsd types in a module" 119 )] 120 #[cfg(feature = "std")] 121 extern crate std; 122 /// C FFI. 123 mod c; 124 /// Errno. 125 mod err; 126 /// OpenBSD 127 #[cfg(any(doc, target_os = "openbsd"))] 128 mod openbsd; 129 use c::SUCCESS; 130 use core::{ 131 error::Error as CoreErr, 132 ffi::{CStr, c_char, c_int}, 133 fmt::{self, Display, Formatter}, 134 mem::MaybeUninit, 135 ptr, 136 }; 137 pub use err::{Errno, OtherErrno}; 138 #[cfg_attr(docsrs, doc(cfg(target_os = "openbsd")))] 139 #[cfg(any(doc, target_os = "openbsd"))] 140 pub use openbsd::{Permission, Permissions, Promise, Promises}; 141 /// [`uid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). 142 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 143 #[repr(transparent)] 144 pub struct Uid(pub u32); 145 impl Uid { 146 /// The root user ID (i.e., 0). 147 pub const ROOT: Self = Self(0); 148 /// Returns `true` iff `self` is [`Self::ROOT`]. 149 /// 150 /// # Examples 151 /// 152 /// ```no_run 153 /// # use priv_sep::Uid; 154 /// assert!(Uid::ROOT.is_root()); 155 /// ``` 156 #[inline] 157 #[must_use] 158 pub const fn is_root(self) -> bool { 159 self.0 == Self::ROOT.0 160 } 161 /// [`getuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getuid.html). 162 /// 163 /// # Examples 164 /// 165 /// ```no_run 166 /// # use priv_sep::Uid; 167 /// assert_eq!(Uid::getuid(), 1000); 168 /// ``` 169 #[inline] 170 #[must_use] 171 pub fn getuid() -> Self { 172 Self(c::getuid()) 173 } 174 /// [`geteuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/geteuid.html). 175 /// 176 /// # Examples 177 /// 178 /// ```no_run 179 /// # use priv_sep::Uid; 180 /// assert_eq!(Uid::geteuid(), 1000); 181 /// ``` 182 #[inline] 183 #[must_use] 184 pub fn geteuid() -> Self { 185 Self(c::geteuid()) 186 } 187 /// Calls [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html) 188 /// passing `self` for the real, effective, and saved user IDs. 189 /// 190 /// Note on some platforms `setuid` is called using `self`. 191 /// 192 /// # Errors 193 /// 194 /// Errors iff `setresuid` does. 195 /// 196 /// # Examples 197 /// 198 /// ```no_run 199 /// # use priv_sep::Uid; 200 /// assert!(Uid(1000).setresuid().is_ok()); 201 /// ``` 202 #[inline] 203 pub fn setresuid(self) -> Result<(), Errno> { 204 #[cfg(any( 205 target_os = "dragonfly", 206 target_os = "freebsd", 207 target_os = "linux", 208 target_os = "openbsd" 209 ))] 210 let code = c::setresuid(self.0, self.0, self.0); 211 #[cfg(any(target_os = "macos", target_os = "netbsd"))] 212 let code = c::setuid(self.0); 213 if code == SUCCESS { 214 Ok(()) 215 } else { 216 Err(Errno::last()) 217 } 218 } 219 } 220 impl PartialEq<&Self> for Uid { 221 #[inline] 222 fn eq(&self, other: &&Self) -> bool { 223 *self == **other 224 } 225 } 226 impl PartialEq<Uid> for &Uid { 227 #[inline] 228 fn eq(&self, other: &Uid) -> bool { 229 **self == *other 230 } 231 } 232 impl PartialEq<u32> for Uid { 233 #[inline] 234 fn eq(&self, other: &u32) -> bool { 235 self.0 == *other 236 } 237 } 238 impl From<Uid> for u32 { 239 #[inline] 240 fn from(value: Uid) -> Self { 241 value.0 242 } 243 } 244 impl From<u32> for Uid { 245 #[inline] 246 fn from(value: u32) -> Self { 247 Self(value) 248 } 249 } 250 /// [`gid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). 251 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 252 #[repr(transparent)] 253 pub struct Gid(pub u32); 254 impl Gid { 255 /// [`getgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getgid.html). 256 /// 257 /// # Examples 258 /// 259 /// ```no_run 260 /// # use priv_sep::Gid; 261 /// assert_eq!(Gid::getgid(), 1000); 262 /// ``` 263 #[inline] 264 #[must_use] 265 pub fn getgid() -> Self { 266 Self(c::getgid()) 267 } 268 /// [`getegid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getegid.html). 269 /// 270 /// # Examples 271 /// 272 /// ```no_run 273 /// # use priv_sep::Gid; 274 /// assert_eq!(Gid::getegid(), 1000); 275 /// ``` 276 #[inline] 277 #[must_use] 278 pub fn getegid() -> Self { 279 Self(c::getegid()) 280 } 281 /// Calls [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html) 282 /// passing `self` for the real, effective, and saved group IDs. 283 /// 284 /// Note on some platforms `setgid` is called using `self`. 285 /// 286 /// # Errors 287 /// 288 /// Errors iff `setresgid` does. 289 /// 290 /// # Examples 291 /// 292 /// ```no_run 293 /// # use priv_sep::Gid; 294 /// assert!(Gid(1000).setresgid().is_ok()); 295 /// ``` 296 #[inline] 297 pub fn setresgid(self) -> Result<(), Errno> { 298 #[cfg(any( 299 target_os = "dragonfly", 300 target_os = "freebsd", 301 target_os = "linux", 302 target_os = "openbsd" 303 ))] 304 let code = c::setresgid(self.0, self.0, self.0); 305 #[cfg(any(target_os = "macos", target_os = "netbsd"))] 306 let code = c::setgid(self.0); 307 if code == SUCCESS { 308 Ok(()) 309 } else { 310 Err(Errno::last()) 311 } 312 } 313 } 314 impl PartialEq<&Self> for Gid { 315 #[inline] 316 fn eq(&self, other: &&Self) -> bool { 317 *self == **other 318 } 319 } 320 impl PartialEq<Gid> for &Gid { 321 #[inline] 322 fn eq(&self, other: &Gid) -> bool { 323 **self == *other 324 } 325 } 326 impl PartialEq<u32> for Gid { 327 #[inline] 328 fn eq(&self, other: &u32) -> bool { 329 self.0 == *other 330 } 331 } 332 impl From<Gid> for u32 { 333 #[inline] 334 fn from(value: Gid) -> Self { 335 value.0 336 } 337 } 338 impl From<u32> for Gid { 339 #[inline] 340 fn from(value: u32) -> Self { 341 Self(value) 342 } 343 } 344 /// [`chroot(2)`](https://manned.org/chroot.2). 345 /// 346 /// # Errors 347 /// 348 /// Errors iff `chroot(2)` does. 349 /// 350 /// # Examples 351 /// 352 /// ```no_run 353 /// assert!(priv_sep::chroot(c"./").is_ok()); 354 /// ``` 355 #[expect(unsafe_code, reason = "chroot(2) takes a pointer")] 356 #[inline] 357 pub fn chroot(path: &CStr) -> Result<(), Errno> { 358 let ptr = path.as_ptr(); 359 // SAFETY: 360 // `ptr` is valid and not null. 361 if unsafe { c::chroot(ptr) } == SUCCESS { 362 Ok(()) 363 } else { 364 Err(Errno::last()) 365 } 366 } 367 /// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). 368 /// 369 /// This function MUST only be called by `chdir` and `chroot_then_chdir`. 370 #[expect(unsafe_code, reason = "chdir(2) takes a pointer")] 371 fn private_chdir(path: *const c_char) -> Result<(), Errno> { 372 // SAFETY: 373 // `path` is valid and not null as can be seen in the only functions that call this function: 374 // `chdir` and `chroot_then_chdir`. 375 if unsafe { c::chdir(path) } == SUCCESS { 376 Ok(()) 377 } else { 378 Err(Errno::last()) 379 } 380 } 381 /// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). 382 /// 383 /// # Errors 384 /// 385 /// Errors iff `chdir` does. 386 /// 387 /// # Examples 388 /// 389 /// ```no_run 390 /// assert!(priv_sep::chdir(c"/").is_ok()); 391 /// ``` 392 #[inline] 393 pub fn chdir(path: &CStr) -> Result<(), Errno> { 394 private_chdir(path.as_ptr()) 395 } 396 /// Calls [`chroot`] on `path` followed by a call to [`chdir`] on `"/"`. 397 /// 398 /// # Errors 399 /// 400 /// Errors iff `chroot` or `chdir` do. 401 /// 402 /// # Examples 403 /// 404 /// ```no_run 405 /// assert!(priv_sep::chroot_then_chdir(c"./").is_ok()); 406 /// ``` 407 #[inline] 408 pub fn chroot_then_chdir(path: &CStr) -> Result<(), Errno> { 409 /// Root directory. 410 const ROOT: *const c_char = c"/".as_ptr(); 411 chroot(path).and_then(|()| private_chdir(ROOT)) 412 } 413 /// Error returned when dropping privileges. 414 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 415 pub enum PrivDropErr<E> { 416 /// Error when an error occurs from a libc call. 417 Libc(Errno), 418 /// Error when there is no entry in the user database corresponding to the passed username. 419 NoPasswdEntry, 420 /// Error when [`UserInfo::is_root`]. 421 RootEntry, 422 /// Error returned from the user-provided function that is invoked before calling [`UserInfo::setresid`]. 423 Other(E), 424 } 425 impl<E: Display> Display for PrivDropErr<E> { 426 #[inline] 427 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 428 match *self { 429 Self::Libc(err) => write!(f, "libc error when dropping privileges: {err}"), 430 Self::NoPasswdEntry => f.write_str("no passwd(5) entry to drop privileges to"), 431 Self::RootEntry => f.write_str( 432 "setresuid(2) is not allowed to be called on uid 0 when dropping privileges", 433 ), 434 Self::Other(ref err) => write!( 435 f, 436 "error calling function before dropping privileges: {err}" 437 ), 438 } 439 } 440 } 441 impl<E: CoreErr> CoreErr for PrivDropErr<E> {} 442 impl<E> From<Errno> for PrivDropErr<E> { 443 #[inline] 444 fn from(value: Errno) -> Self { 445 Self::Libc(value) 446 } 447 } 448 /// Error returned from [`UserInfo::setresid_if_valid`]. 449 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 450 pub enum SetresidErr { 451 /// Error when an error occurs from a libc call. 452 Libc(Errno), 453 /// Error when there is no entry in the user database corresponding to [`UserInfo::uid`]. 454 NoPasswdEntry, 455 /// Error when the entry in the user database has a different gid than [`UserInfo::gid`]. 456 GidMismatch, 457 } 458 impl Display for SetresidErr { 459 #[inline] 460 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 461 match *self { 462 Self::Libc(err) => write!(f, "libc error when dropping privileges: {err}"), 463 Self::NoPasswdEntry => f.write_str("no passwd(5) entry to drop privileges to"), 464 Self::GidMismatch => f.write_str("gid in passwd(5) does match the expected gid"), 465 } 466 } 467 } 468 impl CoreErr for SetresidErr {} 469 impl From<Errno> for SetresidErr { 470 #[inline] 471 fn from(value: Errno) -> Self { 472 Self::Libc(value) 473 } 474 } 475 /// Used by [`UserInfo::getpw_entry`]. 476 trait PwEntry { 477 /// Calling code must uphold the following safety invariants: 478 /// * `buf` must be a valid, initialized, non-null pointer 479 /// * `size` must be the length of `buf` 480 /// * `result` must be a valid, initialized non-null pointer referencing a valid and initialized pointer that 481 /// is allowed to be null. 482 /// 483 /// Implementors MUST only _write_ to `pwd` and never read from it (i.e., `pwd` is allowed to be unitialized). 484 #[expect( 485 unsafe_code, 486 reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" 487 )] 488 unsafe fn getpw( 489 self, 490 pwd: *mut c::Passwd, 491 buf: *mut c_char, 492 size: usize, 493 result: *mut *mut c::Passwd, 494 ) -> c_int; 495 } 496 impl PwEntry for Uid { 497 #[expect(unsafe_code, reason = "getpwuid_r(3) take in pointers")] 498 unsafe fn getpw( 499 self, 500 pwd: *mut c::Passwd, 501 buf: *mut c_char, 502 size: usize, 503 result: *mut *mut c::Passwd, 504 ) -> c_int { 505 // SAFETY: 506 // Calling code must uphold safety invariants. 507 // `pwd` is never read from. 508 unsafe { c::getpwuid_r(self.0, pwd, buf, size, result) } 509 } 510 } 511 /// `newtype` around `CStr`. 512 #[derive(Clone, Copy)] 513 struct CStrWrapper<'a>(&'a CStr); 514 impl PwEntry for CStrWrapper<'_> { 515 #[expect(unsafe_code, reason = "getpwnam_r(3) takes in pointers")] 516 unsafe fn getpw( 517 self, 518 pwd: *mut c::Passwd, 519 buf: *mut c_char, 520 size: usize, 521 result: *mut *mut c::Passwd, 522 ) -> c_int { 523 let ptr = self.0.as_ptr(); 524 // SAFETY: 525 // Calling code must uphold safety invariants. 526 // `ptr` is valid, initialized, and not null. 527 // `pwd` is never read from. 528 unsafe { c::getpwnam_r(ptr, pwd, buf, size, result) } 529 } 530 } 531 /// User and group ID. 532 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 533 pub struct UserInfo { 534 /// The user ID. 535 pub uid: Uid, 536 /// The group ID. 537 pub gid: Gid, 538 } 539 impl UserInfo { 540 /// The buffer size we use to read a `passwd` entry. 541 const PW_ENT_BUF_LEN: usize = 1024; 542 /// Returns `true` iff [`Uid::is_root`]. 543 /// 544 /// # Examples 545 /// 546 /// ```no_run 547 /// # use priv_sep::{Gid, Uid, UserInfo}; 548 /// assert!(UserInfo { uid: Uid::ROOT, gid: Gid(0), }.is_root()); 549 /// ``` 550 #[inline] 551 #[must_use] 552 pub const fn is_root(self) -> bool { 553 self.uid.is_root() 554 } 555 /// Helper for [`Self::with_buffer`], [`Self::new`], and [`Self::setresid_if_exists`]. 556 #[expect( 557 unsafe_code, 558 reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" 559 )] 560 fn getpw_entry<P: Copy + PwEntry>(u: P, buffer: &mut [c_char]) -> Result<Option<Self>, Errno> { 561 let mut pwd = MaybeUninit::<c::Passwd>::uninit(); 562 let pwd_ptr = pwd.as_mut_ptr(); 563 let buf_ptr = buffer.as_mut_ptr(); 564 let len = buffer.len(); 565 let mut result = ptr::null_mut(); 566 let res_ptr = &mut result; 567 // SAFETY: 568 // `pwd_ptr` is only written to; thus the fact `pwd` is unitialized is fine. 569 // `buf_ptr` is valid, initialized, and not null. 570 // `len` is the length of `buf_ptr`. 571 // `res_ptr` is valid, initialized, and not null. 572 // `result` is valid, initialized, and allowed to be null. 573 let code = unsafe { u.getpw(pwd_ptr, buf_ptr, len, res_ptr) }; 574 if code == SUCCESS { 575 if result.is_null() { 576 Ok(None) 577 } else { 578 debug_assert!( 579 result.is_aligned(), 580 "libc getpwnam_r or getpwuid_r result was not aligned. Something is terribly wrong with your system" 581 ); 582 // SAFETY: 583 // Verified above that `result` is not null and aligned. Note while we only verify the pointer 584 // is aligned on non-release builds, the situation is so dire that one could argue we are 585 // already in "undefined" territory. 586 // When `result` is not null, the platform is supposed to have written to `pwd`. 587 Ok(Some(unsafe { pwd.assume_init() }.into_user_info())) 588 } 589 } else { 590 Err(Errno::from_raw(code)) 591 } 592 } 593 /// [`getpwnam_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwnam_r.html). 594 /// 595 /// Uses `buffer` to write the user database entry into returning `None` iff there is no entry; otherwise 596 /// returns `Self`. 597 /// 598 /// Note it is the caller's responsibility to ensure `buffer` is large enough; otherwise an [`Errno`] will 599 /// be returned. 600 /// 601 /// # Errors 602 /// 603 /// Errors iff `getpwnam_r` does. 604 /// 605 /// # Examples 606 /// 607 /// ```no_run 608 /// # use priv_sep::{Uid, UserInfo}; 609 /// assert!(UserInfo::with_buffer(c"root", [0; 128].as_mut_slice())?.map_or(false, |info| info.is_root())); 610 /// # Ok::<_, priv_sep::Errno>(()) 611 /// ``` 612 #[inline] 613 pub fn with_buffer(name: &CStr, buffer: &mut [c_char]) -> Result<Option<Self>, Errno> { 614 Self::getpw_entry(CStrWrapper(name), buffer) 615 } 616 /// Same as [`Self::with_buffer`] except a stack-allocated buffer of 1024 bytes is used. 617 /// 618 /// # Errors 619 /// 620 /// Errors iff [`Self::with_buffer`] does for a 1024-byte buffer. 621 /// 622 /// # Examples 623 /// 624 /// ```no_run 625 /// # use priv_sep::UserInfo; 626 /// assert!(UserInfo::new(c"root")?.map_or(false, |info| info.is_root())); 627 /// # Ok::<_, priv_sep::Errno>(()) 628 /// ``` 629 #[inline] 630 pub fn new(name: &CStr) -> Result<Option<Self>, Errno> { 631 Self::with_buffer(name, &mut [0; Self::PW_ENT_BUF_LEN]) 632 } 633 /// Calls [`Gid::setresgid`] and [`Uid::setresuid`]. 634 /// 635 /// # Errors 636 /// 637 /// Errors iff `Gid::setresgid` or `Uid::setresuid` error. 638 /// 639 /// # Examples 640 /// 641 /// ```no_run 642 /// # use priv_sep::UserInfo; 643 /// if let Some(user) = UserInfo::new(c"nobody")? { 644 /// user.setresid()?; 645 /// } 646 /// # Ok::<_, priv_sep::Errno>(()) 647 /// ``` 648 #[inline] 649 pub fn setresid(self) -> Result<(), Errno> { 650 self.gid.setresgid().and_then(|()| self.uid.setresuid()) 651 } 652 /// Same as [`Self::setresid`] except 653 /// [`getpwuid_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwuid_r.html) 654 /// is used to first confirm the existence of [`Self::uid`] and [`Self::gid`]. 655 /// 656 /// Note this should rarely be used since most will rely on [`Self::new`], [`Self::with_buffer`], 657 /// [`Self::priv_drop`], or [`Self::chroot_then_priv_drop`]. 658 /// 659 /// Like [`Self::new`], this will fail if the buffer needed exceeds 16 KiB. 660 /// 661 /// # Errors 662 /// 663 /// Errors iff `getpwuid_r` errors for a 16 KiB buffer, [`Self::uid`] and [`Self::gid`] don't exist in the user 664 /// database, [`Gid::setresgid`] errors, or [`Uid::setresuid`] errors. 665 /// 666 /// # Examples 667 /// 668 /// ```no_run 669 /// # use priv_sep::{Gid, Uid, UserInfo}; 670 /// UserInfo { uid: Uid(1000), gid: Gid(1000), }.setresid_if_valid()?; 671 /// # Ok::<_, priv_sep::SetresidErr>(()) 672 /// ``` 673 #[inline] 674 pub fn setresid_if_valid(self) -> Result<(), SetresidErr> { 675 Self::getpw_entry(self.uid, &mut [0; Self::PW_ENT_BUF_LEN]) 676 .map_err(SetresidErr::Libc) 677 .and_then(|opt| { 678 opt.ok_or(SetresidErr::NoPasswdEntry).and_then(|info| { 679 if info.gid == self.gid { 680 self.setresid().map_err(SetresidErr::Libc) 681 } else { 682 Err(SetresidErr::GidMismatch) 683 } 684 }) 685 }) 686 } 687 /// Helper to unify targets that don't support `setgroups(2)`. 688 /// 689 /// No-op. 690 #[cfg(target_os = "macos")] 691 #[expect( 692 clippy::unnecessary_wraps, 693 reason = "unify with platforms that support `setgroups`" 694 )] 695 const fn drop_sup_groups<Never>() -> Result<(), Never> { 696 Ok(()) 697 } 698 /// Helper to unify targets that don't support `setgroups(2)`. 699 #[cfg(any( 700 target_os = "dragonfly", 701 target_os = "freebsd", 702 target_os = "linux", 703 target_os = "netbsd", 704 target_os = "openbsd" 705 ))] 706 fn drop_sup_groups() -> Result<(), Errno> { 707 drop_supplementary_groups() 708 } 709 /// Calls [`Self::new`], invokes `f`, calls [`drop_supplementary_groups`] (if the platform supports 710 /// `setgroups(2)`), then calls [`Self::setresid`]. 711 /// 712 /// Dropping privileges is necessary when needing to perform certain actions as root before no longer needing 713 /// such abilities; at which point, one calls 714 /// [`setgroups(2)`](https://www.man7.org/linux/man-pages/man2/setgroups.2.html), 715 /// [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html), and 716 /// [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html) 717 /// using a lesser privileged gid and uid. 718 /// 719 /// # Errors 720 /// 721 /// Errors iff [`Self::new`], `f`, [`drop_supplementary_groups`], or [`Self::setresid`] do or there is no entry in the user database 722 /// corresponding to `name` or the entry has uid 0. 723 /// 724 /// # Examples 725 /// 726 /// ```no_run 727 /// # use core::net::{Ipv6Addr, SocketAddrV6}; 728 /// # use priv_sep::{PrivDropErr, UserInfo}; 729 /// # use std::{io::Error, net::TcpListener}; 730 /// let listener = UserInfo::priv_drop(c"nobody", || { 731 /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) 732 /// })?; 733 /// # Ok::<_, PrivDropErr<Error>>(()) 734 /// ``` 735 #[inline] 736 pub fn priv_drop<T, E, F: FnOnce() -> Result<T, E>>( 737 name: &CStr, 738 f: F, 739 ) -> Result<T, PrivDropErr<E>> { 740 Self::new(name).map_err(PrivDropErr::Libc).and_then(|opt| { 741 opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) 742 .and_then(|info| { 743 if info.is_root() { 744 Err(PrivDropErr::RootEntry) 745 } else { 746 f().map_err(PrivDropErr::Other).and_then(|res| { 747 Self::drop_sup_groups() 748 .and_then(|()| info.setresid().map(|()| res)) 749 .map_err(PrivDropErr::Libc) 750 }) 751 } 752 }) 753 }) 754 } 755 /// Same as [`Self::priv_drop`] except `f` is `async`. 756 /// 757 /// # Errors 758 /// 759 /// Read [`Self::priv_drop`]. 760 /// 761 /// # Examples 762 /// 763 /// ```no_run 764 /// # use core::net::{Ipv6Addr, SocketAddrV6}; 765 /// # use priv_sep::UserInfo; 766 /// # use tokio::net::TcpListener; 767 /// let listener_fut = UserInfo::priv_drop_async(c"nobody", async || { 768 /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await 769 /// }); 770 /// ``` 771 #[inline] 772 pub async fn priv_drop_async<T, E, F: AsyncFnOnce() -> Result<T, E>>( 773 name: &CStr, 774 f: F, 775 ) -> Result<T, PrivDropErr<E>> { 776 match Self::new(name) { 777 Ok(opt) => match opt { 778 None => Err(PrivDropErr::NoPasswdEntry), 779 Some(info) => { 780 if info.is_root() { 781 Err(PrivDropErr::RootEntry) 782 } else { 783 f().await.map_err(PrivDropErr::Other).and_then(|res| { 784 Self::drop_sup_groups() 785 .and_then(|()| info.setresid().map(|()| res)) 786 .map_err(PrivDropErr::Libc) 787 }) 788 } 789 } 790 }, 791 Err(err) => Err(PrivDropErr::Libc(err)), 792 } 793 } 794 /// Same as [`Self::priv_drop`] except [`chroot_then_chdir`] is called before or after invoking `f` based on 795 /// `chroot_after_f`. 796 /// 797 /// # Errors 798 /// 799 /// Errors iff [`Self::priv_drop`] or [`chroot_then_chdir`] do. 800 /// 801 /// # Examples 802 /// 803 /// ```no_run 804 /// # use core::net::{Ipv6Addr, SocketAddrV6}; 805 /// # use priv_sep::{PrivDropErr, UserInfo}; 806 /// # use std::{io::Error, net::TcpListener}; 807 /// let listener = UserInfo::chroot_then_priv_drop(c"nobody", c"./", false, || { 808 /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) 809 /// })?; 810 /// # Ok::<_, PrivDropErr<Error>>(()) 811 /// ``` 812 #[inline] 813 pub fn chroot_then_priv_drop<T, E, F: FnOnce() -> Result<T, E>>( 814 name: &CStr, 815 path: &CStr, 816 chroot_after_f: bool, 817 f: F, 818 ) -> Result<T, PrivDropErr<E>> { 819 Self::new(name).map_err(PrivDropErr::Libc).and_then(|opt| { 820 opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) 821 .and_then(|info| { 822 if info.is_root() { 823 Err(PrivDropErr::RootEntry) 824 } else if chroot_after_f { 825 f().map_err(PrivDropErr::Other).and_then(|res| { 826 chroot_then_chdir(path) 827 .map_err(PrivDropErr::Libc) 828 .map(|()| res) 829 }) 830 } else { 831 chroot_then_chdir(path) 832 .map_err(PrivDropErr::Libc) 833 .and_then(|()| f().map_err(PrivDropErr::Other)) 834 } 835 .and_then(|res| { 836 Self::drop_sup_groups() 837 .and_then(|()| info.setresid().map(|()| res)) 838 .map_err(PrivDropErr::Libc) 839 }) 840 }) 841 }) 842 } 843 /// Same as [`Self::chroot_then_priv_drop`] except `f` is `async`. 844 /// 845 /// # Errors 846 /// 847 /// Read [`Self::chroot_then_priv_drop`]. 848 /// 849 /// # Examples 850 /// 851 /// ```no_run 852 /// # use core::net::{Ipv6Addr, SocketAddrV6}; 853 /// # use priv_sep::UserInfo; 854 /// # use tokio::net::TcpListener; 855 /// let listener_fut = UserInfo::chroot_then_priv_drop_async(c"nobody", c"./", false, async || { 856 /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await 857 /// }); 858 /// ``` 859 #[inline] 860 pub async fn chroot_then_priv_drop_async<T, E, F: AsyncFnOnce() -> Result<T, E>>( 861 name: &CStr, 862 path: &CStr, 863 chroot_after_f: bool, 864 f: F, 865 ) -> Result<T, PrivDropErr<E>> { 866 match Self::new(name) { 867 Ok(opt) => match opt { 868 None => Err(PrivDropErr::NoPasswdEntry), 869 Some(info) => if info.is_root() { 870 Err(PrivDropErr::RootEntry) 871 } else if chroot_after_f { 872 f().await.map_err(PrivDropErr::Other).and_then(|res| { 873 chroot_then_chdir(path) 874 .map_err(PrivDropErr::Libc) 875 .map(|()| res) 876 }) 877 } else { 878 match chroot_then_chdir(path) { 879 Ok(()) => f().await.map_err(PrivDropErr::Other), 880 Err(err) => Err(PrivDropErr::Libc(err)), 881 } 882 } 883 .and_then(|res| { 884 Self::drop_sup_groups() 885 .and_then(|()| info.setresid().map(|()| res)) 886 .map_err(PrivDropErr::Libc) 887 }), 888 }, 889 Err(err) => Err(PrivDropErr::Libc(err)), 890 } 891 } 892 } 893 impl PartialEq<&Self> for UserInfo { 894 #[inline] 895 fn eq(&self, other: &&Self) -> bool { 896 *self == **other 897 } 898 } 899 impl PartialEq<UserInfo> for &UserInfo { 900 #[inline] 901 fn eq(&self, other: &UserInfo) -> bool { 902 **self == *other 903 } 904 } 905 /// [`setgroups(2)`](https://www.man7.org/linux/man-pages/man2/setgroups.2.html). 906 /// 907 /// # Errors 908 /// 909 /// Errors iff `setgroups` does. 910 /// 911 /// # Examples 912 /// 913 /// ```no_run 914 /// # #[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux", target_os = "netbsd", target_os = "openbsd"))] 915 /// assert!(priv_sep::setgroups(&[]).is_ok()); 916 /// ``` 917 #[cfg_attr( 918 docsrs, 919 doc(cfg(any( 920 target_os = "dragonfly", 921 target_os = "freebsd", 922 target_os = "linux", 923 target_os = "netbsd", 924 target_os = "openbsd" 925 ))) 926 )] 927 #[cfg(any( 928 target_os = "dragonfly", 929 target_os = "freebsd", 930 target_os = "linux", 931 target_os = "netbsd", 932 target_os = "openbsd", 933 all(doc, target_os = "macos") 934 ))] 935 #[expect(unsafe_code, reason = "setgroups(2) takes a pointer")] 936 #[inline] 937 pub fn setgroups(groups: &[Gid]) -> Result<(), Errno> { 938 #[cfg(target_os = "linux")] 939 let size = groups.len(); 940 #[cfg(any( 941 target_os = "dragonfly", 942 target_os = "freebsd", 943 target_os = "netbsd", 944 target_os = "openbsd" 945 ))] 946 let size = c_int::try_from(groups.len()).map_err(|_e| Errno::EINVAL)?; 947 let gids = groups.as_ptr().cast(); 948 // SAFETY: 949 // `size` is the length of `gids`, and `gids` is a valid, non-null, alligned pointer to 950 // `u32`s. Note that `Gid` is a `newtype` around `u32` with `repr(transparent)`; thus the above 951 // pointer cast is fine. 952 let code = unsafe { c::setgroups(size, gids) }; 953 if code == SUCCESS { 954 Ok(()) 955 } else { 956 Err(Errno::last()) 957 } 958 } 959 /// [`setgroups(2)`](https://www.man7.org/linux/man-pages/man2/setgroups.2.html) passing in 960 /// 0 and a null pointer. 961 /// 962 /// If successful, this drops all supplementary groups. 963 /// 964 /// # Errors 965 /// 966 /// Errors iff `setgroups` does. 967 /// 968 /// # Examples 969 /// 970 /// ```no_run 971 /// # #[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux", target_os = "netbsd", target_os = "openbsd"))] 972 /// assert!(priv_sep::drop_supplementary_groups().is_ok()); 973 /// ``` 974 #[cfg_attr( 975 docsrs, 976 doc(cfg(any( 977 target_os = "dragonfly", 978 target_os = "freebsd", 979 target_os = "linux", 980 target_os = "netbsd", 981 target_os = "openbsd" 982 ))) 983 )] 984 #[cfg(any( 985 target_os = "dragonfly", 986 target_os = "freebsd", 987 target_os = "linux", 988 target_os = "netbsd", 989 target_os = "openbsd", 990 all(doc, target_os = "macos") 991 ))] 992 #[expect(unsafe_code, reason = "setgroups(2) takes a pointer")] 993 #[inline] 994 pub fn drop_supplementary_groups() -> Result<(), Errno> { 995 #[cfg(target_os = "linux")] 996 let size: usize = 0; 997 #[cfg(any( 998 target_os = "dragonfly", 999 target_os = "freebsd", 1000 target_os = "netbsd", 1001 target_os = "openbsd" 1002 ))] 1003 let size: c_int = 0; 1004 let gids = ptr::null(); 1005 // SAFETY: 1006 // Passing in 0 and a null pointer is valid and causes all supplementary groups to be dropped. 1007 let code = unsafe { c::setgroups(size, gids) }; 1008 if code == SUCCESS { 1009 Ok(()) 1010 } else { 1011 Err(Errno::last()) 1012 } 1013 } 1014 #[cfg(test)] 1015 mod tests { 1016 #[cfg(feature = "std")] 1017 use super::{CStr, PrivDropErr}; 1018 use super::{Errno, Gid, SetresidErr, Uid, UserInfo}; 1019 #[cfg(all(feature = "std", target_os = "openbsd"))] 1020 use super::{Permissions, Promise, Promises}; 1021 #[cfg(feature = "std")] 1022 use core::net::{Ipv6Addr, SocketAddrV6}; 1023 #[cfg(all(feature = "std", target_os = "openbsd"))] 1024 extern crate alloc; 1025 #[cfg(all(feature = "std", target_os = "openbsd"))] 1026 use alloc::format; 1027 #[cfg(all(feature = "std", target_os = "openbsd"))] 1028 use std::io::{self, Write as _}; 1029 #[cfg(feature = "std")] 1030 use std::{ 1031 fs, 1032 io::{Error, ErrorKind}, 1033 net::TcpListener, 1034 }; 1035 use tokio as _; 1036 #[cfg(feature = "std")] 1037 const README: &CStr = c"README.md"; 1038 #[cfg(feature = "std")] 1039 const README_STR: &str = match README.to_str() { 1040 Ok(val) => val, 1041 Err(_) => panic!("not possible"), 1042 }; 1043 #[test] 1044 fn getuid() { 1045 _ = Uid::getuid(); 1046 } 1047 #[test] 1048 fn geteuid() { 1049 _ = Uid::geteuid(); 1050 } 1051 #[test] 1052 fn getgid() { 1053 _ = Gid::getgid(); 1054 } 1055 #[test] 1056 fn getegid() { 1057 _ = Gid::getegid(); 1058 } 1059 #[test] 1060 fn setresuid() -> Result<(), Errno> { 1061 Uid::geteuid().setresuid() 1062 } 1063 #[test] 1064 fn setresgid() -> Result<(), Errno> { 1065 Gid::getegid().setresgid() 1066 } 1067 #[test] 1068 fn user_info_new() { 1069 assert_eq!( 1070 UserInfo::new(c"root"), 1071 Ok(Some(UserInfo { 1072 uid: Uid::ROOT, 1073 gid: Gid(0), 1074 })) 1075 ); 1076 } 1077 #[test] 1078 fn user_info_with_buffer() { 1079 assert_eq!( 1080 UserInfo::with_buffer(c"root", [0; 512].as_mut_slice()), 1081 Ok(Some(UserInfo { 1082 uid: Uid::ROOT, 1083 gid: Gid(0), 1084 })) 1085 ); 1086 } 1087 #[test] 1088 fn user_info_setresid() -> Result<(), Errno> { 1089 UserInfo { 1090 uid: Uid::geteuid(), 1091 gid: Gid::getegid(), 1092 } 1093 .setresid() 1094 } 1095 #[test] 1096 fn user_info_setresid_if_exists() -> Result<(), SetresidErr> { 1097 UserInfo { 1098 uid: Uid::geteuid(), 1099 gid: Gid::getegid(), 1100 } 1101 .setresid_if_valid() 1102 } 1103 #[test] 1104 fn user_info_setresid_if_exists_failure() { 1105 assert_eq!( 1106 UserInfo { 1107 uid: Uid::geteuid(), 1108 gid: Gid(u32::MAX), 1109 } 1110 .setresid_if_valid(), 1111 Err(SetresidErr::GidMismatch) 1112 ); 1113 } 1114 #[cfg(feature = "std")] 1115 #[test] 1116 #[ignore = "primarily useful for root and interferes with chroot_drop_priv"] 1117 fn priv_drop() { 1118 if Uid::geteuid().is_root() { 1119 assert!( 1120 UserInfo::priv_drop(c"nobody", || { 1121 TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) 1122 }) 1123 .is_ok_and(|_| true) 1124 ); 1125 assert!( 1126 TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)).map_or_else( 1127 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1128 |_| false 1129 ) 1130 ); 1131 } else { 1132 assert!( 1133 UserInfo::priv_drop(c"root", || Ok::<_, Error>(())) 1134 .map_or_else(|e| matches!(e, PrivDropErr::RootEntry), |()| false) 1135 ); 1136 } 1137 } 1138 #[cfg(feature = "std")] 1139 #[test] 1140 #[ignore = "primarily useful for root and interferes with priv_drop"] 1141 fn chroot_drop_priv() { 1142 if Uid::geteuid().is_root() { 1143 assert!( 1144 UserInfo::chroot_then_priv_drop(c"nobody", c"./", false, || { 1145 TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 987, 0, 0)) 1146 }) 1147 .is_ok_and(|_| { 1148 fs::exists(README_STR).is_ok_and(|exists| { 1149 exists 1150 && TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 82, 0, 0)) 1151 .map_or_else( 1152 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1153 |_| false, 1154 ) 1155 }) 1156 }) 1157 ); 1158 } 1159 } 1160 #[expect( 1161 clippy::panic, 1162 reason = "reasonable to expect files to not exist that need to in order to test pledge and unveil" 1163 )] 1164 #[expect( 1165 clippy::expect_used, 1166 reason = "kinda reasonable to expect files to exist that mustn't in order to test pledge and unveil" 1167 )] 1168 #[cfg(all(feature = "std", target_os = "openbsd"))] 1169 #[test] 1170 #[ignore = "interferes with other tests"] 1171 fn unveil_pledge() { 1172 const FILE_EXISTS: &CStr = c"/home/zack/foo.txt"; 1173 const FILE_EXISTS_STR: &str = match FILE_EXISTS.to_str() { 1174 Ok(val) => val, 1175 Err(_) => panic!("not possible"), 1176 }; 1177 const FILE_NOT_EXISTS: &CStr = c"/home/zack/aadkjfasj3s23"; 1178 const FILE_NOT_EXISTS_STR: &str = match FILE_NOT_EXISTS.to_str() { 1179 Ok(val) => val, 1180 Err(_) => panic!("not possible"), 1181 }; 1182 const DIR_NOT_EXISTS: &CStr = c"/home/zack/aadkjfasj3s23/"; 1183 const DIR_NOT_EXISTS_STR: &str = match DIR_NOT_EXISTS.to_str() { 1184 Ok(val) => val, 1185 Err(_) => panic!("not possible"), 1186 }; 1187 _ = fs::metadata(FILE_EXISTS_STR).unwrap_or_else(|_e| { 1188 panic!("{FILE_EXISTS_STR} does not exist, so unit testing cannot occur") 1189 }); 1190 drop(fs::metadata(FILE_NOT_EXISTS_STR).expect_err( 1191 format!("{FILE_NOT_EXISTS_STR} exists, so unit testing cannot occur").as_str(), 1192 )); 1193 drop(fs::metadata(DIR_NOT_EXISTS_STR).expect_err( 1194 format!("{DIR_NOT_EXISTS_STR} exists, so unit testing cannot occur").as_str(), 1195 )); 1196 // This tests that a NULL `promise` does nothing. 1197 assert_eq!(Promises::pledge_none(), Ok(())); 1198 assert!(writeln!(io::stdout()).is_ok_and(|()| true)); 1199 assert_eq!(Promises::ALL.pledge(), Ok(())); 1200 // This tests that duplicates are ignored as well as the implementation of PartialEq. 1201 let mut initial_promises = Promises::new([ 1202 Promise::Stdio, 1203 Promise::Unveil, 1204 Promise::Rpath, 1205 Promise::Stdio, 1206 ]); 1207 assert_eq!(initial_promises.len(), 3); 1208 assert_eq!( 1209 initial_promises, 1210 Promises::new([Promise::Rpath, Promise::Stdio, Promise::Unveil]) 1211 ); 1212 // Test retain. 1213 let mut vals = Promises::new([ 1214 Promise::Audio, 1215 Promise::Bpf, 1216 Promise::Chown, 1217 Promise::Cpath, 1218 Promise::Error, 1219 Promise::Exec, 1220 ]); 1221 vals.retain([Promise::Error, Promise::Chown]); 1222 assert_eq!(vals.len(), 2); 1223 assert!(vals.contains(Promise::Chown)); 1224 assert!(vals.contains(Promise::Error)); 1225 assert_eq!(initial_promises.pledge(), Ok(())); 1226 // This tests unveil with no permissions. 1227 assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Ok(())); 1228 assert!(fs::metadata(FILE_EXISTS_STR).map_or_else( 1229 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1230 |_| false 1231 )); 1232 // This tests unveil with read permissions, 1233 // and one can unveil more permissions (unlike pledge which can only remove promises). 1234 assert_eq!(Permissions::READ.unveil(FILE_EXISTS), Ok(())); 1235 assert!(fs::metadata(FILE_EXISTS_STR).is_ok_and(|_| true)); 1236 // This tests that calls to unveil on missing files don't error. 1237 assert_eq!(Permissions::NONE.unveil(FILE_NOT_EXISTS), Ok(())); 1238 // This tests that calls to unveil on missing directories error. 1239 assert_eq!(Permissions::NONE.unveil(DIR_NOT_EXISTS), Err(Errno::ENOENT)); 1240 // This tests that unveil can no longer be called. 1241 assert_eq!(Permissions::unveil_no_more(), Ok(())); 1242 assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Err(Errno::EPERM)); 1243 assert!(fs::metadata(FILE_EXISTS_STR).is_ok_and(|_| true)); 1244 // The below tests that Promises can only be removed and not added. 1245 initial_promises.remove_promises([Promise::Unveil]); 1246 assert_eq!(initial_promises.len(), 2); 1247 initial_promises.remove(Promise::Rpath); 1248 assert_eq!(initial_promises.len(), 1); 1249 initial_promises.remove(Promise::Rpath); 1250 assert_eq!(initial_promises.len(), 1); 1251 assert_eq!(initial_promises.pledge(), Ok(())); 1252 assert!(writeln!(io::stdout()).is_ok_and(|()| true)); 1253 assert_eq!(Promises::new([Promise::Rpath]).pledge(), Err(Errno::EPERM)); 1254 // If the below is uncommented, the program should crash since the above 1255 // call to pledge no longer allows access to the file system. 1256 // drop(fs::metadata(FILE_EXISTS_STR)); 1257 } 1258 #[cfg(all(feature = "std", target_os = "openbsd"))] 1259 #[test] 1260 #[ignore = "interferes with other tests when root"] 1261 fn pledge_inet_drop_priv() { 1262 if Uid::geteuid().is_root() { 1263 assert!( 1264 Promises::new_priv_drop( 1265 c"nobody", 1266 [Promise::Inet, Promise::Rpath, Promise::Unveil], 1267 false, 1268 || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 47, 0, 0)), 1269 ) 1270 .is_ok_and(|(_, _)| { 1271 Permissions::unveil_raw(README, c"r").is_ok_and(|()| { 1272 fs::exists(README_STR).is_ok_and(|exists| { 1273 Permissions::NONE.unveil(README).is_ok_and(|()| { 1274 Promises::pledge_raw(c"inet stdio").is_ok_and(|()| { 1275 exists 1276 && TcpListener::bind(SocketAddrV6::new( 1277 Ipv6Addr::LOCALHOST, 1278 792, 1279 0, 1280 0, 1281 )) 1282 .map_or_else( 1283 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1284 |_| false, 1285 ) 1286 }) 1287 }) 1288 }) 1289 }) 1290 }) 1291 ); 1292 } 1293 } 1294 #[cfg(all(feature = "std", target_os = "openbsd"))] 1295 #[test] 1296 #[ignore = "interferes with other tests when root"] 1297 fn inet_chroot_priv_pledge() { 1298 if Uid::geteuid().is_root() { 1299 assert!( 1300 Promises::new_chroot_then_priv_drop( 1301 c"nobody", 1302 c"./", 1303 [Promise::Inet, Promise::Rpath, Promise::Unveil], 1304 false, 1305 || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 382, 0, 0)) 1306 ) 1307 .is_ok_and(|(_, mut promises)| Permissions::READ 1308 .unveil(README) 1309 .is_ok_and( 1310 |()| fs::exists(README_STR).is_ok_and(|exists| Permissions::NONE 1311 .unveil(README) 1312 .is_ok_and(|()| promises 1313 .remove_promises_then_pledge([Promise::Rpath, Promise::Unveil]) 1314 .is_ok_and(|()| exists 1315 && TcpListener::bind(SocketAddrV6::new( 1316 Ipv6Addr::LOCALHOST, 1317 588, 1318 0, 1319 0 1320 )) 1321 .map_or_else( 1322 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1323 |_| false 1324 )))) 1325 )) 1326 ); 1327 } 1328 } 1329 }