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