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