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