lib.rs (54607B)
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; 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: &str = "config"; 73 //! // Get the user ID and group ID for nobody from `passwd(5)`. 74 //! // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. 75 //! // `pledge(2)` `id`, `inet`, `rpath`, `stdio`, and `unveil`. 76 //! // Bind to TCP `[::1]:443` as root. 77 //! // `setgroups(2)` to drop all supplementary groups. 78 //! // `setresgid(2)` to the group ID associated with nobody. 79 //! // `setresuid(2)` to the user ID associated with nobody. 80 //! // Remove `id` from our `pledge(2)`d promises. 81 //! let (listener, mut promises) = Promises::new_chroot_then_priv_drop_async( 82 //! c"nobody", 83 //! c"/path/chroot/", 84 //! [Promise::Inet, Promise::Rpath, Promise::Unveil], 85 //! false, 86 //! async || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await, 87 //! ) 88 //! .await?; 89 //! // At this point, the process is running under nobody. 90 //! // Only allow file system access to `config` and only allow read access to it. 91 //! Permissions::READ.unveil(CONFIG)?; 92 //! // Read `config`. 93 //! // This will of course fail if the file does not exist or nobody does not 94 //! // have read permissions. 95 //! let config = fs::read(CONFIG).map_err(PrivDropErr::Other)?; 96 //! // Remove file system access. 97 //! Permissions::NONE.unveil(CONFIG)?; 98 //! // Remove `rpath` and `unveil` from our `pledge(2)`d promises 99 //! // (i.e., only have `inet` and `stdio` abilities when we begin accepting TCP connections). 100 //! promises.remove_promises_then_pledge([Promise::Rpath, Promise::Unveil])?; 101 //! loop { 102 //! // Handle TCP connections. 103 //! if let Ok((_, ip)) = listener.accept().await { 104 //! assert!(ip.is_ipv6()); 105 //! } 106 //! } 107 //! } 108 //! ``` 109 //! </details> 110 //! 111 //! ## Cargo "features" 112 //! 113 //! ### `alloc` 114 //! 115 //! Enables [`alloc`](https://doc.rust-lang.org/stable/alloc/) support. While "typical" use of `priv_sep` 116 //! should work without `alloc`, there are cases where one may desire heap allocation. For example if a 117 //! database entry associated with a user requires more than 1 KiB of space, [`UserInfo::new`] will error 118 //! with [`Errno::ERANGE`] when `alloc` is not enabled. 119 //! 120 //! Additional [`CStrHelper`] `impl`s are exposed as well (e.g., 121 //! [`String`](./trait.CStrHelper.html#impl-CStrHelper-for-String)). 122 //! 123 //! ### `std` 124 //! 125 //! Enables [`std`](https://doc.rust-lang.org/stable/std/) support. This is useful for additional [`CStrHelper`] 126 //! `impl`s (e.g., [`OsStr`](./trait.CStrHelper.html#impl-CStrHelper-for-OsStr)) as well as 127 //! [`TryFrom<Error>`](./enum.Errno.html#impl-TryFrom%3CError%3E-for-Errno) and 128 //! [`From<Errno>`](./enum.Errno.html#impl-From%3CErrno%3E-for-Error). 129 //! 130 //! This feature implies [`alloc`](#alloc) and is enabled by default via the `default` feature. 131 #![expect( 132 clippy::doc_paragraphs_missing_punctuation, 133 reason = "false positive for crate documentation having image links" 134 )] 135 #![cfg_attr(docsrs, feature(doc_cfg))] 136 #![cfg_attr(docsrs, doc(auto_cfg = false))] 137 #![no_std] 138 #![cfg(any( 139 target_os = "dragonfly", 140 target_os = "freebsd", 141 target_os = "linux", 142 target_os = "macos", 143 target_os = "netbsd", 144 target_os = "openbsd" 145 ))] 146 #![expect( 147 clippy::pub_use, 148 reason = "don't want Errno nor openbsd types in a module" 149 )] 150 #[cfg(feature = "alloc")] 151 extern crate alloc; 152 #[cfg(feature = "std")] 153 extern crate std; 154 /// C FFI. 155 mod c; 156 /// Errno. 157 mod err; 158 /// OpenBSD 159 #[cfg(any(doc, target_os = "openbsd"))] 160 mod openbsd; 161 #[cfg(feature = "std")] 162 use alloc::borrow::Cow; 163 #[cfg(feature = "alloc")] 164 use alloc::{ffi::CString, string::String, vec}; 165 use c::SUCCESS; 166 use core::{ 167 error::Error as CoreErr, 168 ffi::{CStr, c_char, c_int}, 169 fmt::{self, Display, Formatter}, 170 mem::MaybeUninit, 171 ptr, slice, 172 }; 173 pub use err::Errno; 174 #[cfg_attr(docsrs, doc(cfg(target_os = "openbsd")))] 175 #[cfg(any(doc, target_os = "openbsd"))] 176 pub use openbsd::{Permission, Permissions, Promise, Promises}; 177 #[cfg(feature = "std")] 178 use std::{ 179 ffi::{OsStr, OsString}, 180 path::{Component, Components, Iter, Path, PathBuf}, 181 }; 182 /// [`uid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). 183 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 184 #[repr(transparent)] 185 pub struct Uid(pub u32); 186 impl Uid { 187 /// The root user ID (i.e., 0). 188 pub const ROOT: Self = Self(0); 189 /// Returns `true` iff `self` is [`Self::ROOT`]. 190 /// 191 /// # Examples 192 /// 193 /// ```no_run 194 /// # use priv_sep::Uid; 195 /// assert!(Uid::ROOT.is_root()); 196 /// ``` 197 #[inline] 198 #[must_use] 199 pub const fn is_root(self) -> bool { 200 self.0 == Self::ROOT.0 201 } 202 /// [`getuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getuid.html). 203 /// 204 /// # Examples 205 /// 206 /// ```no_run 207 /// # use priv_sep::Uid; 208 /// assert_eq!(Uid::getuid(), 1000); 209 /// ``` 210 #[inline] 211 #[must_use] 212 pub fn getuid() -> Self { 213 Self(c::getuid()) 214 } 215 /// [`geteuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/geteuid.html). 216 /// 217 /// # Examples 218 /// 219 /// ```no_run 220 /// # use priv_sep::Uid; 221 /// assert_eq!(Uid::geteuid(), 1000); 222 /// ``` 223 #[inline] 224 #[must_use] 225 pub fn geteuid() -> Self { 226 Self(c::geteuid()) 227 } 228 /// Calls [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html) 229 /// passing `self` for the real, effective, and saved user IDs. 230 /// 231 /// Note on some platforms `setuid` is called using `self`. 232 /// 233 /// # Errors 234 /// 235 /// Errors iff `setresuid` does. 236 /// 237 /// # Examples 238 /// 239 /// ```no_run 240 /// # use priv_sep::Uid; 241 /// assert!(Uid(1000).setresuid().is_ok()); 242 /// ``` 243 #[inline] 244 pub fn setresuid(self) -> Result<(), Errno> { 245 #[cfg(any( 246 target_os = "dragonfly", 247 target_os = "freebsd", 248 target_os = "linux", 249 target_os = "openbsd" 250 ))] 251 let code = c::setresuid(self.0, self.0, self.0); 252 #[cfg(any(target_os = "macos", target_os = "netbsd"))] 253 let code = c::setuid(self.0); 254 if code == SUCCESS { 255 Ok(()) 256 } else { 257 Err(Errno::last()) 258 } 259 } 260 } 261 impl PartialEq<&Self> for Uid { 262 #[inline] 263 fn eq(&self, other: &&Self) -> bool { 264 *self == **other 265 } 266 } 267 impl PartialEq<Uid> for &Uid { 268 #[inline] 269 fn eq(&self, other: &Uid) -> bool { 270 **self == *other 271 } 272 } 273 impl PartialEq<u32> for Uid { 274 #[inline] 275 fn eq(&self, other: &u32) -> bool { 276 self.0 == *other 277 } 278 } 279 impl From<Uid> for u32 { 280 #[inline] 281 fn from(value: Uid) -> Self { 282 value.0 283 } 284 } 285 impl From<u32> for Uid { 286 #[inline] 287 fn from(value: u32) -> Self { 288 Self(value) 289 } 290 } 291 /// [`gid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). 292 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 293 #[repr(transparent)] 294 pub struct Gid(pub u32); 295 impl Gid { 296 /// [`getgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getgid.html). 297 /// 298 /// # Examples 299 /// 300 /// ```no_run 301 /// # use priv_sep::Gid; 302 /// assert_eq!(Gid::getgid(), 1000); 303 /// ``` 304 #[inline] 305 #[must_use] 306 pub fn getgid() -> Self { 307 Self(c::getgid()) 308 } 309 /// [`getegid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getegid.html). 310 /// 311 /// # Examples 312 /// 313 /// ```no_run 314 /// # use priv_sep::Gid; 315 /// assert_eq!(Gid::getegid(), 1000); 316 /// ``` 317 #[inline] 318 #[must_use] 319 pub fn getegid() -> Self { 320 Self(c::getegid()) 321 } 322 /// Calls [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html) 323 /// passing `self` for the real, effective, and saved group IDs. 324 /// 325 /// Note on some platforms `setgid` is called using `self`. 326 /// 327 /// # Errors 328 /// 329 /// Errors iff `setresgid` does. 330 /// 331 /// # Examples 332 /// 333 /// ```no_run 334 /// # use priv_sep::Gid; 335 /// assert!(Gid(1000).setresgid().is_ok()); 336 /// ``` 337 #[inline] 338 pub fn setresgid(self) -> Result<(), Errno> { 339 #[cfg(any( 340 target_os = "dragonfly", 341 target_os = "freebsd", 342 target_os = "linux", 343 target_os = "openbsd" 344 ))] 345 let code = c::setresgid(self.0, self.0, self.0); 346 #[cfg(any(target_os = "macos", target_os = "netbsd"))] 347 let code = c::setgid(self.0); 348 if code == SUCCESS { 349 Ok(()) 350 } else { 351 Err(Errno::last()) 352 } 353 } 354 } 355 impl PartialEq<&Self> for Gid { 356 #[inline] 357 fn eq(&self, other: &&Self) -> bool { 358 *self == **other 359 } 360 } 361 impl PartialEq<Gid> for &Gid { 362 #[inline] 363 fn eq(&self, other: &Gid) -> bool { 364 **self == *other 365 } 366 } 367 impl PartialEq<u32> for Gid { 368 #[inline] 369 fn eq(&self, other: &u32) -> bool { 370 self.0 == *other 371 } 372 } 373 impl From<Gid> for u32 { 374 #[inline] 375 fn from(value: Gid) -> Self { 376 value.0 377 } 378 } 379 impl From<u32> for Gid { 380 #[inline] 381 fn from(value: u32) -> Self { 382 Self(value) 383 } 384 } 385 /// Primarily an internal `trait` that allows one to use a variety of types in lieu of 386 /// [`CStr`](https://doc.rust-lang.org/stable/core/ffi/struct.CStr.html). 387 pub trait CStrHelper { 388 /// First converts `self` into a 389 /// [`CStr`](https://doc.rust-lang.org/stable/core/ffi/struct.CStr.html) 390 /// before passing it into `f`. 391 /// 392 /// # Errors 393 /// 394 /// Errors whenever necessary. 395 /// 396 /// If an error occurs due to insufficient buffer space (e.g., when [`alloc`](./index.html#alloc) is not 397 /// enabled and a `str` is used with length greater than 1023), then [`Errno::ERANGE`] must be returned. 398 /// 399 /// If an error occurs from converting `self` into a `CStr` due to nul bytes, then [`Errno::EINVAL`] 400 /// must be returned. 401 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>(&self, f: F) 402 -> Result<T, Errno>; 403 } 404 impl CStrHelper for CStr { 405 #[inline] 406 fn convert_then_apply<T, F: FnOnce(&Self) -> Result<T, Errno>>( 407 &self, 408 f: F, 409 ) -> Result<T, Errno> { 410 f(self) 411 } 412 } 413 #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] 414 #[cfg(feature = "alloc")] 415 impl CStrHelper for CString { 416 #[inline] 417 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 418 &self, 419 f: F, 420 ) -> Result<T, Errno> { 421 self.as_c_str().convert_then_apply(f) 422 } 423 } 424 /// Converts `val` into a `CStr` via heap allocation before applying `f` to it. 425 #[cold] 426 #[inline(never)] 427 #[cfg(feature = "alloc")] 428 fn c_str_allocating<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 429 bytes: &[u8], 430 f: F, 431 ) -> Result<T, Errno> { 432 CString::new(bytes) 433 .map_err(|_e| Errno::EINVAL) 434 .and_then(|c| f(&c)) 435 } 436 impl CStrHelper for [u8] { 437 #[expect(unsafe_code, reason = "comments justify correctness")] 438 #[expect( 439 clippy::arithmetic_side_effects, 440 reason = "comment justifies correctness" 441 )] 442 #[inline] 443 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 444 &self, 445 f: F, 446 ) -> Result<T, Errno> { 447 /// Maximum stack allocation for `CStr` conversion. 448 const C_STR_MAX_STACK_ALLOCATION: usize = 0x400; 449 let len = self.len(); 450 if len < C_STR_MAX_STACK_ALLOCATION { 451 let mut buf = MaybeUninit::<[u8; C_STR_MAX_STACK_ALLOCATION]>::uninit(); 452 let buf_ptr = buf.as_mut_ptr().cast(); 453 let slice_ptr = self.as_ptr(); 454 // SAFETY: 455 // `slice_ptr` was created from `self` which has length `len` thus is valid for `len` bytes. 456 // `buf_ptr` was created from `buf` which has length `C_STR_MAX_STACK_ALLOCATION > len`; thus 457 // it too is valid for `len` bytes. 458 // `buf`, while unitialized, is only written to before ever being read from. 459 // `slice_ptr` and `buf_ptr` are properly aligned. 460 // `slice_ptr` and `buf_ptr` point to completely separate allocations. 461 unsafe { ptr::copy_nonoverlapping(slice_ptr, buf_ptr, len) }; 462 // SAFETY: 463 // We just wrote `len` bytes into `buf` (which `buf_ptr` points to). 464 let nul_pos = unsafe { buf_ptr.add(len) }; 465 // SAFETY: 466 // `buf.len() > len`; thus we know we have at least one byte of space to write `0` to. 467 unsafe { nul_pos.write(0) }; 468 // `len <= isize::MAX`; thus this cannot overflow `usize::MAX`. 469 let final_len = len + 1; 470 // SAFETY: 471 // The first `final_len` bytes of `buf` (which `buf_ptr` points to) is initialized, aligned, valid, and 472 // not null. 473 // `CStr::from_bytes_with_nul` doesn't mutate `raw_slice`. 474 let raw_slice = unsafe { slice::from_raw_parts(buf_ptr, final_len) }; 475 CStr::from_bytes_with_nul(raw_slice) 476 .map_err(|_e| Errno::EINVAL) 477 .and_then(f) 478 } else { 479 #[cfg(not(feature = "alloc"))] 480 let res = Err(Errno::ERANGE); 481 #[cfg(feature = "alloc")] 482 let res = c_str_allocating(self, f); 483 res 484 } 485 } 486 } 487 impl CStrHelper for str { 488 #[inline] 489 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 490 &self, 491 f: F, 492 ) -> Result<T, Errno> { 493 self.as_bytes().convert_then_apply(f) 494 } 495 } 496 #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] 497 #[cfg(feature = "alloc")] 498 impl CStrHelper for String { 499 #[inline] 500 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 501 &self, 502 f: F, 503 ) -> Result<T, Errno> { 504 self.as_str().convert_then_apply(f) 505 } 506 } 507 #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 508 #[cfg(feature = "std")] 509 impl CStrHelper for OsStr { 510 #[inline] 511 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 512 &self, 513 f: F, 514 ) -> Result<T, Errno> { 515 self.as_encoded_bytes().convert_then_apply(f) 516 } 517 } 518 #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 519 #[cfg(feature = "std")] 520 impl CStrHelper for OsString { 521 #[inline] 522 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 523 &self, 524 f: F, 525 ) -> Result<T, Errno> { 526 self.as_os_str().convert_then_apply(f) 527 } 528 } 529 #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 530 #[cfg(feature = "std")] 531 impl CStrHelper for Cow<'_, OsStr> { 532 #[inline] 533 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 534 &self, 535 f: F, 536 ) -> Result<T, Errno> { 537 (**self).convert_then_apply(f) 538 } 539 } 540 #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 541 #[cfg(feature = "std")] 542 impl CStrHelper for Path { 543 #[inline] 544 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 545 &self, 546 f: F, 547 ) -> Result<T, Errno> { 548 self.as_os_str().convert_then_apply(f) 549 } 550 } 551 #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 552 #[cfg(feature = "std")] 553 impl CStrHelper for PathBuf { 554 #[inline] 555 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 556 &self, 557 f: F, 558 ) -> Result<T, Errno> { 559 self.as_os_str().convert_then_apply(f) 560 } 561 } 562 #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 563 #[cfg(feature = "std")] 564 impl CStrHelper for Component<'_> { 565 #[inline] 566 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 567 &self, 568 f: F, 569 ) -> Result<T, Errno> { 570 self.as_os_str().convert_then_apply(f) 571 } 572 } 573 #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 574 #[cfg(feature = "std")] 575 impl CStrHelper for Components<'_> { 576 #[inline] 577 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 578 &self, 579 f: F, 580 ) -> Result<T, Errno> { 581 self.as_path().convert_then_apply(f) 582 } 583 } 584 #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 585 #[cfg(feature = "std")] 586 impl CStrHelper for Iter<'_> { 587 #[inline] 588 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 589 &self, 590 f: F, 591 ) -> Result<T, Errno> { 592 self.as_path().convert_then_apply(f) 593 } 594 } 595 impl<C: CStrHelper + ?Sized> CStrHelper for &C { 596 #[inline] 597 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 598 &self, 599 f: F, 600 ) -> Result<T, Errno> { 601 (**self).convert_then_apply(f) 602 } 603 } 604 impl<C: CStrHelper + ?Sized> CStrHelper for &mut C { 605 #[inline] 606 fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( 607 &self, 608 f: F, 609 ) -> Result<T, Errno> { 610 (**self).convert_then_apply(f) 611 } 612 } 613 /// [`chroot(2)`](https://manned.org/chroot.2). 614 /// 615 /// # Errors 616 /// 617 /// Errors iff `chroot(2)` does. 618 /// 619 /// # Examples 620 /// 621 /// ```no_run 622 /// assert!(priv_sep::chroot(c"./").is_ok()); 623 /// ``` 624 #[expect(unsafe_code, reason = "chroot(2) takes a pointer")] 625 #[inline] 626 pub fn chroot<P: CStrHelper>(path: P) -> Result<(), Errno> { 627 fn f(path: &CStr) -> Result<(), Errno> { 628 let ptr = path.as_ptr(); 629 // SAFETY: 630 // `ptr` is valid and not null. 631 if unsafe { c::chroot(ptr) } == SUCCESS { 632 Ok(()) 633 } else { 634 Err(Errno::last()) 635 } 636 } 637 path.convert_then_apply(f) 638 } 639 /// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). 640 /// 641 /// This function MUST only be called by `chdir` and `chroot_then_chdir`. 642 #[expect(unsafe_code, reason = "chdir(2) takes a pointer")] 643 fn private_chdir(path: *const c_char) -> Result<(), Errno> { 644 // SAFETY: 645 // `path` is valid and not null as can be seen in the only functions that call this function: 646 // `chdir` and `chroot_then_chdir`. 647 if unsafe { c::chdir(path) } == SUCCESS { 648 Ok(()) 649 } else { 650 Err(Errno::last()) 651 } 652 } 653 /// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). 654 /// 655 /// # Errors 656 /// 657 /// Errors iff `chdir` does. 658 /// 659 /// # Examples 660 /// 661 /// ```no_run 662 /// assert!(priv_sep::chdir(c"/").is_ok()); 663 /// ``` 664 #[inline] 665 pub fn chdir<P: CStrHelper>(path: P) -> Result<(), Errno> { 666 fn f(path: &CStr) -> Result<(), Errno> { 667 private_chdir(path.as_ptr()) 668 } 669 path.convert_then_apply(f) 670 } 671 /// Calls [`chroot`] on `path` followed by a call to [`chdir`] on `"/"`. 672 /// 673 /// # Errors 674 /// 675 /// Errors iff `chroot` or `chdir` do. 676 /// 677 /// # Examples 678 /// 679 /// ```no_run 680 /// assert!(priv_sep::chroot_then_chdir(c"./").is_ok()); 681 /// ``` 682 #[inline] 683 pub fn chroot_then_chdir<P: CStrHelper>(path: P) -> Result<(), Errno> { 684 /// Root directory. 685 const ROOT: *const c_char = c"/".as_ptr(); 686 chroot(path).and_then(|()| private_chdir(ROOT)) 687 } 688 /// Error returned when dropping privileges. 689 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 690 pub enum PrivDropErr<E> { 691 /// Error when an error occurs from a libc call. 692 Libc(Errno), 693 /// Error when there is no entry in the user database corresponding to the passed username. 694 NoPasswdEntry, 695 /// Error when [`UserInfo::is_root`]. 696 RootEntry, 697 /// Error returned from the user-provided function that is invoked before calling [`UserInfo::setresid`]. 698 Other(E), 699 } 700 impl<E: Display> Display for PrivDropErr<E> { 701 #[inline] 702 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 703 match *self { 704 Self::Libc(err) => write!(f, "libc error when dropping privileges: {err}"), 705 Self::NoPasswdEntry => f.write_str("no passwd(5) entry to drop privileges to"), 706 Self::RootEntry => f.write_str( 707 "setresuid(2) is not allowed to be called on uid 0 when dropping privileges", 708 ), 709 Self::Other(ref err) => write!( 710 f, 711 "error calling function before dropping privileges: {err}" 712 ), 713 } 714 } 715 } 716 impl<E: CoreErr> CoreErr for PrivDropErr<E> {} 717 impl<E> From<Errno> for PrivDropErr<E> { 718 #[inline] 719 fn from(value: Errno) -> Self { 720 Self::Libc(value) 721 } 722 } 723 /// Error returned from [`UserInfo::setresid_if_valid`]. 724 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 725 pub enum SetresidErr { 726 /// Error when an error occurs from a libc call. 727 Libc(Errno), 728 /// Error when there is no entry in the user database corresponding to [`UserInfo::uid`]. 729 NoPasswdEntry, 730 /// Error when the entry in the user database has a different gid than [`UserInfo::gid`]. 731 GidMismatch, 732 } 733 impl Display for SetresidErr { 734 #[inline] 735 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 736 match *self { 737 Self::Libc(err) => write!(f, "libc error when dropping privileges: {err}"), 738 Self::NoPasswdEntry => f.write_str("no passwd(5) entry to drop privileges to"), 739 Self::GidMismatch => f.write_str("gid in passwd(5) does match the expected gid"), 740 } 741 } 742 } 743 impl CoreErr for SetresidErr {} 744 impl From<Errno> for SetresidErr { 745 #[inline] 746 fn from(value: Errno) -> Self { 747 Self::Libc(value) 748 } 749 } 750 /// Used by [`UserInfo::getpw_entry`]. 751 trait PwEntry { 752 /// Calling code must uphold the following safety invariants: 753 /// * `buf` must be a valid, initialized, non-null pointer 754 /// * `size` must be the length of `buf` 755 /// * `result` must be a valid, initialized non-null pointer referencing a valid and initialized pointer that 756 /// is allowed to be null. 757 /// 758 /// Implementors MUST only _write_ to `pwd` and never read from it (i.e., `pwd` is allowed to be unitialized). 759 #[expect( 760 unsafe_code, 761 reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" 762 )] 763 unsafe fn getpw( 764 self, 765 pwd: *mut c::Passwd, 766 buf: *mut c_char, 767 size: usize, 768 result: *mut *mut c::Passwd, 769 ) -> c_int; 770 } 771 impl PwEntry for Uid { 772 #[expect(unsafe_code, reason = "getpwuid_r(3) take in pointers")] 773 unsafe fn getpw( 774 self, 775 pwd: *mut c::Passwd, 776 buf: *mut c_char, 777 size: usize, 778 result: *mut *mut c::Passwd, 779 ) -> c_int { 780 // SAFETY: 781 // Calling code must uphold safety invariants. 782 // `pwd` is never read from. 783 unsafe { c::getpwuid_r(self.0, pwd, buf, size, result) } 784 } 785 } 786 /// `newtype` around `CStr`. 787 #[derive(Clone, Copy)] 788 struct CStrWrapper<'a>(&'a CStr); 789 impl PwEntry for CStrWrapper<'_> { 790 #[expect(unsafe_code, reason = "getpwnam_r(3) takes in pointers")] 791 unsafe fn getpw( 792 self, 793 pwd: *mut c::Passwd, 794 buf: *mut c_char, 795 size: usize, 796 result: *mut *mut c::Passwd, 797 ) -> c_int { 798 let ptr = self.0.as_ptr(); 799 // SAFETY: 800 // Calling code must uphold safety invariants. 801 // `ptr` is valid, initialized, and not null. 802 // `pwd` is never read from. 803 unsafe { c::getpwnam_r(ptr, pwd, buf, size, result) } 804 } 805 } 806 /// User and group ID. 807 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 808 pub struct UserInfo { 809 /// The user ID. 810 pub uid: Uid, 811 /// The group ID. 812 pub gid: Gid, 813 } 814 impl UserInfo { 815 /// Returns `true` iff [`Uid::is_root`]. 816 /// 817 /// # Examples 818 /// 819 /// ```no_run 820 /// # use priv_sep::{Gid, Uid, UserInfo}; 821 /// assert!(UserInfo { uid: Uid::ROOT, gid: Gid(0), }.is_root()); 822 /// ``` 823 #[inline] 824 #[must_use] 825 pub const fn is_root(self) -> bool { 826 self.uid.is_root() 827 } 828 /// Helper for [`Self::new`] and [`Self::setresid_if_valid`]. 829 #[expect( 830 unsafe_code, 831 reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" 832 )] 833 fn getpw_entry<P: Copy + PwEntry>(u: P, buffer: &mut [c_char]) -> Result<Option<Self>, Errno> { 834 let mut pwd = MaybeUninit::<c::Passwd>::uninit(); 835 let pwd_ptr = pwd.as_mut_ptr(); 836 let buf_ptr = buffer.as_mut_ptr(); 837 let len = buffer.len(); 838 let mut result = ptr::null_mut(); 839 let res_ptr = &mut result; 840 // SAFETY: 841 // `pwd_ptr` is only written to; thus the fact `pwd` is unitialized is fine. 842 // `buf_ptr` is valid, initialized, and not null. 843 // `len` is the length of `buf_ptr`. 844 // `res_ptr` is valid, initialized, and not null. 845 // `result` is valid, initialized, and allowed to be null. 846 let code = unsafe { u.getpw(pwd_ptr, buf_ptr, len, res_ptr) }; 847 if code == SUCCESS { 848 if result.is_null() { 849 Ok(None) 850 } else { 851 debug_assert!( 852 result.is_aligned(), 853 "libc getpwnam_r or getpwuid_r result was not aligned. Something is terribly wrong with your system" 854 ); 855 // SAFETY: 856 // Verified above that `result` is not null and aligned. Note while we only verify the pointer 857 // is aligned on non-release builds, the situation is so dire that one could argue we are 858 // already in "undefined" territory. 859 // When `result` is not null, the platform is supposed to have written to `pwd`. 860 Ok(Some(unsafe { pwd.assume_init() }.into_user_info())) 861 } 862 } else { 863 Err(Errno::from_raw(code)) 864 } 865 } 866 /// [`getpwnam_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwnam_r.html). 867 /// 868 /// Obtains the user database entry returning `None` iff there is no entry; otherwise returns `Self`. 869 /// 870 /// A 1 KiB stack-allocated buffer is used to write the database entry into. If [`Errno::ERANGE`] 871 /// is returned, then the following will occur: 872 /// 873 /// * If [`alloc`](./index.html#alloc) is not enabled, then the error is returned. 874 /// * If [`alloc`](./index.html#alloc) is enabled and a 16-bit architecture is used, then a heap-allocated buffer of 16 KiB 875 /// is used. If this errors, then the error is returned. 876 /// * If [`alloc`](./index.html#alloc) is enabled and a non-16-bit architecture is used, then a heap-allocated buffer of 1 MiB 877 /// is used. If this errors, then the error is returned. 878 /// 879 /// # Errors 880 /// 881 /// Errors iff `getpwnam_r` does. 882 /// 883 /// # Examples 884 /// 885 /// ```no_run 886 /// # use priv_sep::UserInfo; 887 /// assert!(UserInfo::new(c"root")?.map_or(false, |info| info.is_root())); 888 /// # Ok::<_, priv_sep::Errno>(()) 889 /// ``` 890 #[inline] 891 pub fn new<C: CStrHelper>(name: C) -> Result<Option<Self>, Errno> { 892 fn f(name: &CStr) -> Result<Option<UserInfo>, Errno> { 893 let wrapper = CStrWrapper(name); 894 let res = UserInfo::getpw_entry(wrapper, &mut [0; 0x400]); 895 #[cfg(not(feature = "alloc"))] 896 let res_final = res; 897 #[cfg(all(target_pointer_width = "16", feature = "alloc"))] 898 let res_final = res.or_else(|e| { 899 if matches!(e, Errno::ERANGE) { 900 Self::getpw_entry(wrapper, vec![0; 0x4000].as_mut_slice()) 901 } else { 902 Err(e) 903 } 904 }); 905 #[cfg(all(not(target_pointer_width = "16"), feature = "alloc"))] 906 let res_final = res.or_else(|e| { 907 if matches!(e, Errno::ERANGE) { 908 UserInfo::getpw_entry(wrapper, vec![0; 0x10_0000].as_mut_slice()) 909 } else { 910 Err(e) 911 } 912 }); 913 res_final 914 } 915 name.convert_then_apply(f) 916 } 917 /// Calls [`Gid::setresgid`] and [`Uid::setresuid`]. 918 /// 919 /// # Errors 920 /// 921 /// Errors iff `Gid::setresgid` or `Uid::setresuid` error. 922 /// 923 /// # Examples 924 /// 925 /// ```no_run 926 /// # use priv_sep::UserInfo; 927 /// if let Some(user) = UserInfo::new(c"nobody")? { 928 /// user.setresid()?; 929 /// } 930 /// # Ok::<_, priv_sep::Errno>(()) 931 /// ``` 932 #[inline] 933 pub fn setresid(self) -> Result<(), Errno> { 934 self.gid.setresgid().and_then(|()| self.uid.setresuid()) 935 } 936 /// Same as [`Self::setresid`] except 937 /// [`getpwuid_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwuid_r.html) 938 /// is used to first confirm the existence of [`Self::uid`] and [`Self::gid`]. 939 /// 940 /// Note this should rarely be used since most will rely on [`Self::new`], [`Self::priv_drop`], or 941 /// [`Self::chroot_then_priv_drop`]. 942 /// 943 /// Read [`Self::new`] for more information on the buffering strategy. 944 /// 945 /// # Errors 946 /// 947 /// Errors iff `getpwuid_r` errors, [`Self::uid`] and [`Self::gid`] don't exist in the user 948 /// database, [`Gid::setresgid`] errors, or [`Uid::setresuid`] errors. 949 /// 950 /// # Examples 951 /// 952 /// ```no_run 953 /// # use priv_sep::{Gid, Uid, UserInfo}; 954 /// UserInfo { uid: Uid(1000), gid: Gid(1000), }.setresid_if_valid()?; 955 /// # Ok::<_, priv_sep::SetresidErr>(()) 956 /// ``` 957 #[inline] 958 pub fn setresid_if_valid(self) -> Result<(), SetresidErr> { 959 let res = Self::getpw_entry(self.uid, &mut [0; 0x400]); 960 #[cfg(not(feature = "alloc"))] 961 let res_final = res; 962 #[cfg(all(target_pointer_width = "16", feature = "alloc"))] 963 let res_final = res.or_else(|e| { 964 if matches!(e, Errno::ERANGE) { 965 Self::getpw_entry(self.uid, vec![0; 0x4000].as_mut_slice()) 966 } else { 967 Err(e) 968 } 969 }); 970 #[cfg(all(not(target_pointer_width = "16"), feature = "alloc"))] 971 let res_final = res.or_else(|e| { 972 if matches!(e, Errno::ERANGE) { 973 Self::getpw_entry(self.uid, vec![0; 0x10_0000].as_mut_slice()) 974 } else { 975 Err(e) 976 } 977 }); 978 res_final.map_err(SetresidErr::Libc).and_then(|opt| { 979 opt.ok_or(SetresidErr::NoPasswdEntry).and_then(|info| { 980 if info.gid == self.gid { 981 self.setresid().map_err(SetresidErr::Libc) 982 } else { 983 Err(SetresidErr::GidMismatch) 984 } 985 }) 986 }) 987 } 988 /// Helper to unify targets that don't support `setgroups(2)`. 989 /// 990 /// No-op. 991 #[cfg(target_os = "macos")] 992 #[expect( 993 clippy::unnecessary_wraps, 994 reason = "unify with platforms that support `setgroups`" 995 )] 996 const fn drop_sup_groups<Never>() -> Result<(), Never> { 997 Ok(()) 998 } 999 /// Helper to unify targets that don't support `setgroups(2)`. 1000 #[cfg(any( 1001 target_os = "dragonfly", 1002 target_os = "freebsd", 1003 target_os = "linux", 1004 target_os = "netbsd", 1005 target_os = "openbsd" 1006 ))] 1007 fn drop_sup_groups() -> Result<(), Errno> { 1008 drop_supplementary_groups() 1009 } 1010 /// Calls [`Self::new`], invokes `f`, calls [`drop_supplementary_groups`] (if the platform supports 1011 /// `setgroups(2)`), then calls [`Self::setresid`]. 1012 /// 1013 /// Dropping privileges is necessary when needing to perform certain actions as root before no longer needing 1014 /// such abilities; at which point, one calls 1015 /// [`setgroups(2)`](https://www.man7.org/linux/man-pages/man2/setgroups.2.html), 1016 /// [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html), and 1017 /// [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html) 1018 /// using a lesser privileged gid and uid. 1019 /// 1020 /// # Errors 1021 /// 1022 /// Errors iff [`Self::new`], `f`, [`drop_supplementary_groups`], or [`Self::setresid`] do or there is no entry in the user database 1023 /// corresponding to `name` or the entry has uid 0. 1024 /// 1025 /// # Examples 1026 /// 1027 /// ```no_run 1028 /// # use core::net::{Ipv6Addr, SocketAddrV6}; 1029 /// # use priv_sep::{PrivDropErr, UserInfo}; 1030 /// # use std::{io::Error, net::TcpListener}; 1031 /// let listener = UserInfo::priv_drop(c"nobody", || { 1032 /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) 1033 /// })?; 1034 /// # Ok::<_, PrivDropErr<Error>>(()) 1035 /// ``` 1036 #[inline] 1037 pub fn priv_drop<C: CStrHelper, T, E, F: FnOnce() -> Result<T, E>>( 1038 name: C, 1039 f: F, 1040 ) -> Result<T, PrivDropErr<E>> { 1041 Self::new(name).map_err(PrivDropErr::Libc).and_then(|opt| { 1042 opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) 1043 .and_then(|info| { 1044 if info.is_root() { 1045 Err(PrivDropErr::RootEntry) 1046 } else { 1047 f().map_err(PrivDropErr::Other).and_then(|res| { 1048 Self::drop_sup_groups() 1049 .and_then(|()| info.setresid().map(|()| res)) 1050 .map_err(PrivDropErr::Libc) 1051 }) 1052 } 1053 }) 1054 }) 1055 } 1056 /// Same as [`Self::priv_drop`] except `f` is `async`. 1057 /// 1058 /// # Errors 1059 /// 1060 /// Read [`Self::priv_drop`]. 1061 /// 1062 /// # Examples 1063 /// 1064 /// ```no_run 1065 /// # use core::net::{Ipv6Addr, SocketAddrV6}; 1066 /// # use priv_sep::UserInfo; 1067 /// # use tokio::net::TcpListener; 1068 /// let listener_fut = UserInfo::priv_drop_async(c"nobody", async || { 1069 /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await 1070 /// }); 1071 /// ``` 1072 #[inline] 1073 pub async fn priv_drop_async<C: CStrHelper, T, E, F: AsyncFnOnce() -> Result<T, E>>( 1074 name: C, 1075 f: F, 1076 ) -> Result<T, PrivDropErr<E>> { 1077 match Self::new(name) { 1078 Ok(opt) => match opt { 1079 None => Err(PrivDropErr::NoPasswdEntry), 1080 Some(info) => { 1081 if info.is_root() { 1082 Err(PrivDropErr::RootEntry) 1083 } else { 1084 f().await.map_err(PrivDropErr::Other).and_then(|res| { 1085 Self::drop_sup_groups() 1086 .and_then(|()| info.setresid().map(|()| res)) 1087 .map_err(PrivDropErr::Libc) 1088 }) 1089 } 1090 } 1091 }, 1092 Err(err) => Err(PrivDropErr::Libc(err)), 1093 } 1094 } 1095 /// Same as [`Self::priv_drop`] except [`chroot_then_chdir`] is called before or after invoking `f` based on 1096 /// `chroot_after_f`. 1097 /// 1098 /// # Errors 1099 /// 1100 /// Errors iff [`Self::priv_drop`] or [`chroot_then_chdir`] do. 1101 /// 1102 /// # Examples 1103 /// 1104 /// ```no_run 1105 /// # use core::net::{Ipv6Addr, SocketAddrV6}; 1106 /// # use priv_sep::{PrivDropErr, UserInfo}; 1107 /// # use std::{io::Error, net::TcpListener}; 1108 /// let listener = UserInfo::chroot_then_priv_drop(c"nobody", c"./", false, || { 1109 /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) 1110 /// })?; 1111 /// # Ok::<_, PrivDropErr<Error>>(()) 1112 /// ``` 1113 #[inline] 1114 pub fn chroot_then_priv_drop< 1115 N: CStrHelper, 1116 P: CStrHelper, 1117 T, 1118 E, 1119 F: FnOnce() -> Result<T, E>, 1120 >( 1121 name: N, 1122 path: P, 1123 chroot_after_f: bool, 1124 f: F, 1125 ) -> Result<T, PrivDropErr<E>> { 1126 Self::new(name).map_err(PrivDropErr::Libc).and_then(|opt| { 1127 opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) 1128 .and_then(|info| { 1129 if info.is_root() { 1130 Err(PrivDropErr::RootEntry) 1131 } else if chroot_after_f { 1132 f().map_err(PrivDropErr::Other).and_then(|res| { 1133 chroot_then_chdir(path) 1134 .map_err(PrivDropErr::Libc) 1135 .map(|()| res) 1136 }) 1137 } else { 1138 chroot_then_chdir(path) 1139 .map_err(PrivDropErr::Libc) 1140 .and_then(|()| f().map_err(PrivDropErr::Other)) 1141 } 1142 .and_then(|res| { 1143 Self::drop_sup_groups() 1144 .and_then(|()| info.setresid().map(|()| res)) 1145 .map_err(PrivDropErr::Libc) 1146 }) 1147 }) 1148 }) 1149 } 1150 /// Same as [`Self::chroot_then_priv_drop`] except `f` is `async`. 1151 /// 1152 /// # Errors 1153 /// 1154 /// Read [`Self::chroot_then_priv_drop`]. 1155 /// 1156 /// # Examples 1157 /// 1158 /// ```no_run 1159 /// # use core::net::{Ipv6Addr, SocketAddrV6}; 1160 /// # use priv_sep::UserInfo; 1161 /// # use tokio::net::TcpListener; 1162 /// let listener_fut = UserInfo::chroot_then_priv_drop_async(c"nobody", c"./", false, async || { 1163 /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await 1164 /// }); 1165 /// ``` 1166 #[inline] 1167 pub async fn chroot_then_priv_drop_async< 1168 N: CStrHelper, 1169 P: CStrHelper, 1170 T, 1171 E, 1172 F: AsyncFnOnce() -> Result<T, E>, 1173 >( 1174 name: N, 1175 path: P, 1176 chroot_after_f: bool, 1177 f: F, 1178 ) -> Result<T, PrivDropErr<E>> { 1179 match Self::new(name) { 1180 Ok(opt) => match opt { 1181 None => Err(PrivDropErr::NoPasswdEntry), 1182 Some(info) => if info.is_root() { 1183 Err(PrivDropErr::RootEntry) 1184 } else if chroot_after_f { 1185 f().await.map_err(PrivDropErr::Other).and_then(|res| { 1186 chroot_then_chdir(path) 1187 .map_err(PrivDropErr::Libc) 1188 .map(|()| res) 1189 }) 1190 } else { 1191 match chroot_then_chdir(path) { 1192 Ok(()) => f().await.map_err(PrivDropErr::Other), 1193 Err(err) => Err(PrivDropErr::Libc(err)), 1194 } 1195 } 1196 .and_then(|res| { 1197 Self::drop_sup_groups() 1198 .and_then(|()| info.setresid().map(|()| res)) 1199 .map_err(PrivDropErr::Libc) 1200 }), 1201 }, 1202 Err(err) => Err(PrivDropErr::Libc(err)), 1203 } 1204 } 1205 } 1206 impl PartialEq<&Self> for UserInfo { 1207 #[inline] 1208 fn eq(&self, other: &&Self) -> bool { 1209 *self == **other 1210 } 1211 } 1212 impl PartialEq<UserInfo> for &UserInfo { 1213 #[inline] 1214 fn eq(&self, other: &UserInfo) -> bool { 1215 **self == *other 1216 } 1217 } 1218 /// [`setgroups(2)`](https://www.man7.org/linux/man-pages/man2/setgroups.2.html). 1219 /// 1220 /// # Errors 1221 /// 1222 /// Errors iff `setgroups` does. 1223 /// 1224 /// # Examples 1225 /// 1226 /// ```no_run 1227 /// assert!(priv_sep::setgroups(&[]).is_ok()); 1228 /// ``` 1229 #[cfg_attr(docsrs, doc(cfg(not(target_os = "macos"))))] 1230 #[cfg(any(doc, not(target_os = "macos")))] 1231 #[expect(unsafe_code, reason = "setgroups(2) takes a pointer")] 1232 #[inline] 1233 pub fn setgroups(groups: &[Gid]) -> Result<(), Errno> { 1234 #[cfg(target_os = "linux")] 1235 let size = groups.len(); 1236 #[cfg(any( 1237 target_os = "dragonfly", 1238 target_os = "freebsd", 1239 target_os = "netbsd", 1240 target_os = "openbsd" 1241 ))] 1242 let size = c_int::try_from(groups.len()).map_err(|_e| Errno::EINVAL)?; 1243 let gids = groups.as_ptr().cast(); 1244 // SAFETY: 1245 // `size` is the length of `gids`, and `gids` is a valid, non-null, aligned pointer to 1246 // `u32`s. Note that `Gid` is a `newtype` around `u32` with `repr(transparent)`; thus the above 1247 // pointer cast is fine. 1248 let code = unsafe { c::setgroups(size, gids) }; 1249 if code == SUCCESS { 1250 Ok(()) 1251 } else { 1252 Err(Errno::last()) 1253 } 1254 } 1255 /// [`setgroups(2)`](https://www.man7.org/linux/man-pages/man2/setgroups.2.html) passing in 1256 /// 0 and a null pointer. 1257 /// 1258 /// If successful, this drops all supplementary groups. 1259 /// 1260 /// # Errors 1261 /// 1262 /// Errors iff `setgroups` does. 1263 /// 1264 /// # Examples 1265 /// 1266 /// ```no_run 1267 /// assert!(priv_sep::drop_supplementary_groups().is_ok()); 1268 /// ``` 1269 #[cfg_attr(docsrs, doc(cfg(not(target_os = "macos"))))] 1270 #[cfg(any(doc, not(target_os = "macos")))] 1271 #[expect(unsafe_code, reason = "setgroups(2) takes a pointer")] 1272 #[inline] 1273 pub fn drop_supplementary_groups() -> Result<(), Errno> { 1274 #[cfg(target_os = "linux")] 1275 let size: usize = 0; 1276 #[cfg(any( 1277 target_os = "dragonfly", 1278 target_os = "freebsd", 1279 target_os = "netbsd", 1280 target_os = "openbsd" 1281 ))] 1282 let size: c_int = 0; 1283 let gids = ptr::null(); 1284 // SAFETY: 1285 // Passing in 0 and a null pointer is valid and causes all supplementary groups to be dropped. 1286 let code = unsafe { c::setgroups(size, gids) }; 1287 if code == SUCCESS { 1288 Ok(()) 1289 } else { 1290 Err(Errno::last()) 1291 } 1292 } 1293 #[cfg(test)] 1294 mod tests { 1295 #[cfg(feature = "std")] 1296 use super::PrivDropErr; 1297 use super::{Errno, Gid, SetresidErr, Uid, UserInfo}; 1298 #[cfg(all(feature = "std", target_os = "openbsd"))] 1299 use super::{Permissions, Promise, Promises}; 1300 #[cfg(feature = "std")] 1301 use core::net::{Ipv6Addr, SocketAddrV6}; 1302 #[cfg(all(feature = "std", target_os = "openbsd"))] 1303 extern crate alloc; 1304 #[cfg(all(feature = "std", target_os = "openbsd"))] 1305 use alloc::format; 1306 #[cfg(all(feature = "std", target_os = "openbsd"))] 1307 use std::io::{self, Write as _}; 1308 #[cfg(feature = "std")] 1309 use std::{ 1310 fs, 1311 io::{Error, ErrorKind}, 1312 net::TcpListener, 1313 }; 1314 use tokio as _; 1315 #[cfg(feature = "std")] 1316 const README: &str = "README.md"; 1317 #[test] 1318 fn getuid() { 1319 _ = Uid::getuid(); 1320 } 1321 #[test] 1322 fn geteuid() { 1323 _ = Uid::geteuid(); 1324 } 1325 #[test] 1326 fn getgid() { 1327 _ = Gid::getgid(); 1328 } 1329 #[test] 1330 fn getegid() { 1331 _ = Gid::getegid(); 1332 } 1333 #[test] 1334 fn setresuid() -> Result<(), Errno> { 1335 Uid::geteuid().setresuid() 1336 } 1337 #[test] 1338 fn setresgid() -> Result<(), Errno> { 1339 Gid::getegid().setresgid() 1340 } 1341 #[test] 1342 fn user_info_new() { 1343 assert_eq!( 1344 UserInfo::new(c"root"), 1345 Ok(Some(UserInfo { 1346 uid: Uid::ROOT, 1347 gid: Gid(0), 1348 })) 1349 ); 1350 } 1351 #[test] 1352 fn user_info_setresid() -> Result<(), Errno> { 1353 UserInfo { 1354 uid: Uid::geteuid(), 1355 gid: Gid::getegid(), 1356 } 1357 .setresid() 1358 } 1359 #[test] 1360 fn user_info_setresid_if_valid() -> Result<(), SetresidErr> { 1361 UserInfo { 1362 uid: Uid::geteuid(), 1363 gid: Gid::getegid(), 1364 } 1365 .setresid_if_valid() 1366 } 1367 #[test] 1368 fn user_info_setresid_if_valid_failure() { 1369 assert_eq!( 1370 UserInfo { 1371 uid: Uid::geteuid(), 1372 gid: Gid(u32::MAX), 1373 } 1374 .setresid_if_valid(), 1375 Err(SetresidErr::GidMismatch) 1376 ); 1377 } 1378 #[cfg(feature = "std")] 1379 #[test] 1380 #[ignore = "primarily useful for root and interferes with chroot_drop_priv"] 1381 fn priv_drop() { 1382 if Uid::geteuid().is_root() { 1383 assert!( 1384 UserInfo::priv_drop(c"nobody", || { 1385 TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) 1386 }) 1387 .is_ok_and(|_| true) 1388 ); 1389 assert!( 1390 TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)).map_or_else( 1391 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1392 |_| false 1393 ) 1394 ); 1395 } else { 1396 assert!( 1397 UserInfo::priv_drop(c"root", || Ok::<_, Error>(())) 1398 .map_or_else(|e| matches!(e, PrivDropErr::RootEntry), |()| false) 1399 ); 1400 } 1401 } 1402 #[cfg(feature = "std")] 1403 #[test] 1404 #[ignore = "primarily useful for root and interferes with priv_drop"] 1405 fn chroot_drop_priv() { 1406 if Uid::geteuid().is_root() { 1407 assert!( 1408 UserInfo::chroot_then_priv_drop(c"nobody", c"./", false, || { 1409 TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 987, 0, 0)) 1410 }) 1411 .is_ok_and(|_| { 1412 fs::exists(README).is_ok_and(|exists| { 1413 exists 1414 && TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 82, 0, 0)) 1415 .map_or_else( 1416 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1417 |_| false, 1418 ) 1419 }) 1420 }) 1421 ); 1422 } 1423 } 1424 #[expect( 1425 clippy::panic, 1426 reason = "reasonable to expect files to not exist that need to in order to test pledge and unveil" 1427 )] 1428 #[expect( 1429 clippy::expect_used, 1430 reason = "kinda reasonable to expect files to exist that mustn't in order to test pledge and unveil" 1431 )] 1432 #[cfg(all(feature = "std", target_os = "openbsd"))] 1433 #[test] 1434 #[ignore = "interferes with other tests"] 1435 fn unveil_pledge() { 1436 const FILE_EXISTS: &str = "/home/zack/foo.txt"; 1437 const FILE_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23"; 1438 const DIR_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23/"; 1439 _ = fs::metadata(FILE_EXISTS).unwrap_or_else(|_e| { 1440 panic!("{FILE_EXISTS} does not exist, so unit testing cannot occur") 1441 }); 1442 drop(fs::metadata(FILE_NOT_EXISTS).expect_err( 1443 format!("{FILE_NOT_EXISTS} exists, so unit testing cannot occur").as_str(), 1444 )); 1445 drop( 1446 fs::metadata(DIR_NOT_EXISTS).expect_err( 1447 format!("{DIR_NOT_EXISTS} exists, so unit testing cannot occur").as_str(), 1448 ), 1449 ); 1450 // This tests that a NULL `promise` does nothing. 1451 assert_eq!(Promises::pledge_none(), Ok(())); 1452 assert!(writeln!(io::stdout()).is_ok_and(|()| true)); 1453 assert_eq!(Promises::ALL.pledge(), Ok(())); 1454 // This tests that duplicates are ignored as well as the implementation of PartialEq. 1455 let mut initial_promises = Promises::new([ 1456 Promise::Stdio, 1457 Promise::Unveil, 1458 Promise::Rpath, 1459 Promise::Stdio, 1460 ]); 1461 assert_eq!(initial_promises.len(), 3); 1462 assert_eq!( 1463 initial_promises, 1464 Promises::new([Promise::Rpath, Promise::Stdio, Promise::Unveil]) 1465 ); 1466 // Test retain. 1467 let mut vals = Promises::new([ 1468 Promise::Audio, 1469 Promise::Bpf, 1470 Promise::Chown, 1471 Promise::Cpath, 1472 Promise::Error, 1473 Promise::Exec, 1474 ]); 1475 vals.retain([Promise::Error, Promise::Chown]); 1476 assert_eq!(vals.len(), 2); 1477 assert!(vals.contains(Promise::Chown)); 1478 assert!(vals.contains(Promise::Error)); 1479 assert_eq!(initial_promises.pledge(), Ok(())); 1480 // This tests unveil with no permissions. 1481 assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Ok(())); 1482 assert!(fs::metadata(FILE_EXISTS).map_or_else( 1483 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1484 |_| false 1485 )); 1486 // This tests unveil with read permissions, 1487 // and one can unveil more permissions (unlike pledge which can only remove promises). 1488 assert_eq!(Permissions::READ.unveil(FILE_EXISTS), Ok(())); 1489 assert!(fs::metadata(FILE_EXISTS).is_ok_and(|_| true)); 1490 // This tests that calls to unveil on missing files don't error. 1491 assert_eq!(Permissions::NONE.unveil(FILE_NOT_EXISTS), Ok(())); 1492 // This tests that calls to unveil on missing directories error. 1493 assert_eq!(Permissions::NONE.unveil(DIR_NOT_EXISTS), Err(Errno::ENOENT)); 1494 // This tests that unveil can no longer be called. 1495 assert_eq!(Permissions::unveil_no_more(), Ok(())); 1496 assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Err(Errno::EPERM)); 1497 assert!(fs::metadata(FILE_EXISTS).is_ok_and(|_| true)); 1498 // The below tests that Promises can only be removed and not added. 1499 initial_promises.remove_promises([Promise::Unveil]); 1500 assert_eq!(initial_promises.len(), 2); 1501 initial_promises.remove(Promise::Rpath); 1502 assert_eq!(initial_promises.len(), 1); 1503 initial_promises.remove(Promise::Rpath); 1504 assert_eq!(initial_promises.len(), 1); 1505 assert_eq!(initial_promises.pledge(), Ok(())); 1506 assert!(writeln!(io::stdout()).is_ok_and(|()| true)); 1507 assert_eq!(Promises::new([Promise::Rpath]).pledge(), Err(Errno::EPERM)); 1508 // If the below is uncommented, the program should crash since the above 1509 // call to pledge no longer allows access to the file system. 1510 // drop(fs::metadata(FILE_EXISTS)); 1511 } 1512 #[cfg(all(feature = "std", target_os = "openbsd"))] 1513 #[test] 1514 #[ignore = "interferes with other tests when root"] 1515 fn pledge_inet_drop_priv() { 1516 if Uid::geteuid().is_root() { 1517 assert!( 1518 Promises::new_priv_drop( 1519 c"nobody", 1520 [Promise::Inet, Promise::Rpath, Promise::Unveil], 1521 false, 1522 || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 47, 0, 0)), 1523 ) 1524 .is_ok_and(|(_, _)| { 1525 Permissions::unveil_raw(README, c"r").is_ok_and(|()| { 1526 fs::exists(README).is_ok_and(|exists| { 1527 Permissions::NONE.unveil(README).is_ok_and(|()| { 1528 Promises::pledge_raw(c"inet stdio").is_ok_and(|()| { 1529 exists 1530 && TcpListener::bind(SocketAddrV6::new( 1531 Ipv6Addr::LOCALHOST, 1532 792, 1533 0, 1534 0, 1535 )) 1536 .map_or_else( 1537 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1538 |_| false, 1539 ) 1540 }) 1541 }) 1542 }) 1543 }) 1544 }) 1545 ); 1546 } 1547 } 1548 #[cfg(all(feature = "std", target_os = "openbsd"))] 1549 #[test] 1550 #[ignore = "interferes with other tests when root"] 1551 fn inet_chroot_priv_pledge() { 1552 if Uid::geteuid().is_root() { 1553 assert!( 1554 Promises::new_chroot_then_priv_drop( 1555 c"nobody", 1556 c"./", 1557 [Promise::Inet, Promise::Rpath, Promise::Unveil], 1558 false, 1559 || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 382, 0, 0)) 1560 ) 1561 .is_ok_and(|(_, mut promises)| Permissions::READ 1562 .unveil(README) 1563 .is_ok_and(|()| fs::exists(README).is_ok_and(|exists| Permissions::NONE 1564 .unveil(README) 1565 .is_ok_and(|()| promises 1566 .remove_promises_then_pledge([Promise::Rpath, Promise::Unveil]) 1567 .is_ok_and(|()| exists 1568 && TcpListener::bind(SocketAddrV6::new( 1569 Ipv6Addr::LOCALHOST, 1570 588, 1571 0, 1572 0 1573 )) 1574 .map_or_else( 1575 |e| matches!(e.kind(), ErrorKind::PermissionDenied), 1576 |_| false 1577 )))))) 1578 ); 1579 } 1580 } 1581 }