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