priv_sep

Privilege separation library.
git clone https://git.philomathiclife.com/repos/priv_sep
Log | Files | Refs | README

commit 6caca4961a51443ea5ee79d32d5d1a80061663dc
parent 3aaf25aaec21a4157b812ab63a9a1f16e5d0a850
Author: Zack Newman <zack@philomathiclife.com>
Date:   Fri,  9 Jan 2026 16:15:00 -0700

add cstrhelper trait

Diffstat:
MCargo.toml | 6+++++-
MREADME.md | 11+++--------
Msrc/err.rs | 2+-
Msrc/lib.rs | 505+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/openbsd.rs | 43+++++++++++++++++++++++++++++--------------
5 files changed, 405 insertions(+), 162 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -86,6 +86,7 @@ question_mark_used = "allow" redundant_pub_crate = "allow" ref_patterns = "allow" return_and_then = "allow" +single_call_fn = "allow" single_char_lifetime_names = "allow" semicolon_inside_block = "allow" @@ -115,5 +116,8 @@ tokio = { version = "1.49.0", default-features = false, features = ["macros", "n [features] default = ["std"] +# Provide alloc support. +alloc = [] + # Provide std support. -std = [] +std = ["alloc"] diff --git a/README.md b/README.md @@ -52,7 +52,7 @@ async fn main() -> Result<Infallible, PrivDropErr<Error>> { <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> ```rust -use core::{convert::Infallible, ffi::CStr}; +use core::convert::Infallible; use priv_sep::{Permissions, PrivDropErr, Promise, Promises}; use std::{ fs, @@ -63,12 +63,7 @@ use tokio::net::TcpListener; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<Infallible, PrivDropErr<Error>> { /// Config file. - const CONFIG: &CStr = c"config"; - /// Config file. - const CONFIG_STR: &str = match CONFIG.to_str() { - Ok(val) => val, - Err(_) => panic!("config is not a valid str"), - }; + const CONFIG: &str = "config"; // Get the user ID and group ID for nobody from `passwd(5)`. // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. // `pledge(2)` `id`, `inet`, `rpath`, `stdio`, and `unveil`. @@ -91,7 +86,7 @@ async fn main() -> Result<Infallible, PrivDropErr<Error>> { // Read `config`. // This will of course fail if the file does not exist or nobody does not // have read permissions. - let config = fs::read(CONFIG_STR).map_err(PrivDropErr::Other)?; + let config = fs::read(CONFIG).map_err(PrivDropErr::Other)?; // Remove file system access. Permissions::NONE.unveil(CONFIG)?; // Remove `rpath` and `unveil` from our `pledge(2)`d promises diff --git a/src/err.rs b/src/err.rs @@ -555,7 +555,7 @@ pub enum Errno { docsrs, doc(cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))) )] - #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))] + #[cfg(any(doc, target_arch = "powerpc", target_arch = "powerpc64"))] EDEADLOCK, /// Bad font file format. EBFONT = 59, diff --git a/src/lib.rs b/src/lib.rs @@ -52,7 +52,7 @@ //! //! ```no_run //! # #[cfg(target_os = "openbsd")] -//! use core::{convert::Infallible, ffi::CStr}; +//! use core::convert::Infallible; //! # #[cfg(target_os = "openbsd")] //! use priv_sep::{Permissions, PrivDropErr, Promise, Promises}; //! # #[cfg(target_os = "openbsd")] @@ -69,12 +69,7 @@ //! #[tokio::main(flavor = "current_thread")] //! async fn main() -> Result<Infallible, PrivDropErr<Error>> { //! /// Config file. -//! const CONFIG: &CStr = c"config"; -//! /// Config file. -//! const CONFIG_STR: &str = match CONFIG.to_str() { -//! Ok(val) => val, -//! Err(_) => panic!("config is not a valid str"), -//! }; +//! const CONFIG: &str = "config"; //! // Get the user ID and group ID for nobody from `passwd(5)`. //! // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. //! // `pledge(2)` `id`, `inet`, `rpath`, `stdio`, and `unveil`. @@ -97,7 +92,7 @@ //! // Read `config`. //! // This will of course fail if the file does not exist or nobody does not //! // have read permissions. -//! let config = fs::read(CONFIG_STR).map_err(PrivDropErr::Other)?; +//! let config = fs::read(CONFIG).map_err(PrivDropErr::Other)?; //! // Remove file system access. //! Permissions::NONE.unveil(CONFIG)?; //! // Remove `rpath` and `unveil` from our `pledge(2)`d promises @@ -127,6 +122,8 @@ clippy::pub_use, reason = "don't want Errno nor openbsd types in a module" )] +#[cfg(feature = "alloc")] +extern crate alloc; #[cfg(feature = "std")] extern crate std; /// C FFI. @@ -134,20 +131,29 @@ mod c; /// Errno. mod err; /// OpenBSD -#[cfg(target_os = "openbsd")] +#[cfg(any(doc, target_os = "openbsd"))] mod openbsd; +#[cfg(feature = "std")] +use alloc::borrow::Cow; +#[cfg(feature = "alloc")] +use alloc::{ffi::CString, string::String, vec}; use c::SUCCESS; use core::{ error::Error as CoreErr, ffi::{CStr, c_char, c_int}, fmt::{self, Display, Formatter}, mem::MaybeUninit, - ptr, + ptr, slice, }; pub use err::Errno; #[cfg_attr(docsrs, doc(cfg(target_os = "openbsd")))] -#[cfg(target_os = "openbsd")] +#[cfg(any(doc, target_os = "openbsd"))] pub use openbsd::{Permission, Permissions, Promise, Promises}; +#[cfg(feature = "std")] +use std::{ + ffi::{OsStr, OsString}, + path::{Component, Components, Iter, Path, PathBuf}, +}; /// [`uid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(transparent)] @@ -351,6 +357,212 @@ impl From<u32> for Gid { Self(value) } } +/// Primarily an internal `trait` that allows one to use other types in lieu of +/// [`CStr`](https://doc.rust-lang.org/stable/core/ffi/struct.CStr.html). +pub trait CStrHelper { + /// First converts `self` into a `CStr` before passing it into `f`. + /// + /// # Errors + /// + /// Errors whenever necessary. + /// + /// If an error occurs due to insufficient buffer space (e.g., when the `alloc` feature is not enabled and + /// a `str` is used with length greater than 1023), then [`Errno::ERANGE`] must be returned. + /// + /// If an error occurs from converting `self` into a `CStr` due to nul bytes, then [`Errno::EINVAL`] must be + /// returned. + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>(&self, f: F) + -> Result<T, Errno>; +} +impl CStrHelper for CStr { + #[inline] + fn convert_then_apply<T, F: FnOnce(&Self) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + f(self) + } +} +/// Converts `val` into a `CStr` via heap allocation before applying `f` to it. +#[cold] +#[inline(never)] +#[cfg(feature = "alloc")] +fn c_str_allocating<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + bytes: &[u8], + f: F, +) -> Result<T, Errno> { + CString::new(bytes) + .map_err(|_e| Errno::EINVAL) + .and_then(|c| f(&c)) +} +impl CStrHelper for [u8] { + #[expect(unsafe_code, reason = "comments justify correctness")] + #[expect( + clippy::arithmetic_side_effects, + reason = "comment justifies correctness" + )] + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + /// Maximum stack allocation for `CStr` conversion. + const C_STR_MAX_STACK_ALLOCATION: usize = 1024; + let len = self.len(); + if len < C_STR_MAX_STACK_ALLOCATION { + let mut buf = MaybeUninit::<[u8; C_STR_MAX_STACK_ALLOCATION]>::uninit(); + let buf_ptr = buf.as_mut_ptr().cast(); + let slice_ptr = self.as_ptr(); + // SAFETY: + // `slice_ptr` was created from `self` which has length `len` thus is valid for `len` bytes. + // `buf_ptr` was created from `buf` which has length `C_STR_MAX_STACK_ALLOCATION > len`; thus + // it too is valid for `len` bytes. + // `buf`, while unitialized, is only written to before ever being read from. + // `slice_ptr` and `buf_ptr` are properly aligned. + // `slice_ptr` and `buf_ptr` point to completely separate allocations. + unsafe { ptr::copy_nonoverlapping(slice_ptr, buf_ptr, len) }; + // SAFETY: + // We just wrote `len` bytes into `buf` (which `buf_ptr` points to). + let nul_pos = unsafe { buf_ptr.add(len) }; + // SAFETY: + // `buf.len() > len`; thus we know we have at least one byte of space to write `0` to. + unsafe { nul_pos.write(0) }; + // `len <= isize::MAX`; thus this cannot overflow `usize::MAX`. + let final_len = len + 1; + // SAFETY: + // The first `final_len` bytes of `buf` (which `buf_ptr` points to) is initialized, aligned, valid, and + // not null. + // `CStr::from_bytes_with_nul` doesn't mutate `raw_slice`. + let raw_slice = unsafe { slice::from_raw_parts(buf_ptr, final_len) }; + CStr::from_bytes_with_nul(raw_slice) + .map_err(|_e| Errno::EINVAL) + .and_then(f) + } else { + #[cfg(not(feature = "alloc"))] + let res = Err(Errno::ERANGE); + #[cfg(feature = "alloc")] + let res = c_str_allocating(self, f); + res + } + } +} +impl CStrHelper for str { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_bytes().convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg(feature = "alloc")] +impl CStrHelper for String { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_str().convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl CStrHelper for OsStr { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_encoded_bytes().convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl CStrHelper for OsString { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_os_str().convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl CStrHelper for Cow<'_, OsStr> { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + (**self).convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl CStrHelper for Path { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_os_str().convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl CStrHelper for PathBuf { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_os_str().convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl CStrHelper for Component<'_> { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_os_str().convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl CStrHelper for Components<'_> { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_path().convert_then_apply(f) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl CStrHelper for Iter<'_> { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + self.as_path().convert_then_apply(f) + } +} +impl<C: CStrHelper + ?Sized> CStrHelper for &C { + #[inline] + fn convert_then_apply<T, F: FnOnce(&CStr) -> Result<T, Errno>>( + &self, + f: F, + ) -> Result<T, Errno> { + (**self).convert_then_apply(f) + } +} /// [`chroot(2)`](https://manned.org/chroot.2). /// /// # Errors @@ -364,15 +576,18 @@ impl From<u32> for Gid { /// ``` #[expect(unsafe_code, reason = "chroot(2) takes a pointer")] #[inline] -pub fn chroot(path: &CStr) -> Result<(), Errno> { - let ptr = path.as_ptr(); - // SAFETY: - // `ptr` is valid and not null. - if unsafe { c::chroot(ptr) } == SUCCESS { - Ok(()) - } else { - Err(Errno::last()) +pub fn chroot<P: CStrHelper>(path: P) -> Result<(), Errno> { + fn f(path: &CStr) -> Result<(), Errno> { + let ptr = path.as_ptr(); + // SAFETY: + // `ptr` is valid and not null. + if unsafe { c::chroot(ptr) } == SUCCESS { + Ok(()) + } else { + Err(Errno::last()) + } } + path.convert_then_apply(f) } /// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). /// @@ -400,8 +615,11 @@ fn private_chdir(path: *const c_char) -> Result<(), Errno> { /// assert!(priv_sep::chdir(c"/").is_ok()); /// ``` #[inline] -pub fn chdir(path: &CStr) -> Result<(), Errno> { - private_chdir(path.as_ptr()) +pub fn chdir<P: CStrHelper>(path: P) -> Result<(), Errno> { + fn f(path: &CStr) -> Result<(), Errno> { + private_chdir(path.as_ptr()) + } + path.convert_then_apply(f) } /// Calls [`chroot`] on `path` followed by a call to [`chdir`] on `"/"`. /// @@ -415,7 +633,7 @@ pub fn chdir(path: &CStr) -> Result<(), Errno> { /// assert!(priv_sep::chroot_then_chdir(c"./").is_ok()); /// ``` #[inline] -pub fn chroot_then_chdir(path: &CStr) -> Result<(), Errno> { +pub fn chroot_then_chdir<P: CStrHelper>(path: P) -> Result<(), Errno> { /// Root directory. const ROOT: *const c_char = c"/".as_ptr(); chroot(path).and_then(|()| private_chdir(ROOT)) @@ -547,8 +765,6 @@ pub struct UserInfo { pub gid: Gid, } impl UserInfo { - /// The buffer size we use to read a `passwd` entry. - const PW_ENT_BUF_LEN: usize = 1024; /// Returns `true` iff [`Uid::is_root`]. /// /// # Examples @@ -562,7 +778,7 @@ impl UserInfo { pub const fn is_root(self) -> bool { self.uid.is_root() } - /// Helper for [`Self::with_buffer`], [`Self::new`], and [`Self::setresid_if_exists`]. + /// Helper for [`Self::new`] and [`Self::setresid_if_valid`]. #[expect( unsafe_code, reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" @@ -602,32 +818,20 @@ impl UserInfo { } /// [`getpwnam_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwnam_r.html). /// - /// Uses `buffer` to write the user database entry into returning `None` iff there is no entry; otherwise - /// returns `Self`. - /// - /// Note it is the caller's responsibility to ensure `buffer` is large enough; otherwise an [`Errno`] will - /// be returned. - /// - /// # Errors - /// - /// Errors iff `getpwnam_r` does. + /// Obtains the user database entry returning `None` iff there is no entry; otherwise returns `Self`. /// - /// # Examples + /// A 1024-byte stack-allocated buffer is used to write the database entry into. If [`Errno::ERANGE`] + /// is returned, then the following will occur: /// - /// ```no_run - /// # use priv_sep::{Uid, UserInfo}; - /// assert!(UserInfo::with_buffer(c"root", [0; 128].as_mut_slice())?.map_or(false, |info| info.is_root())); - /// # Ok::<_, priv_sep::Errno>(()) - /// ``` - #[inline] - pub fn with_buffer(name: &CStr, buffer: &mut [c_char]) -> Result<Option<Self>, Errno> { - Self::getpw_entry(CStrWrapper(name), buffer) - } - /// Same as [`Self::with_buffer`] except a stack-allocated buffer of 1024 bytes is used. + /// * If `alloc` is not enabled, then the error is returned. + /// * If `alloc` is enabled and a 16-bit architecture is used, then a heap-allocated buffer of 16 KiB + /// is used. If this errors, then the error is returned. + /// * If `alloc` is enabled and a non-16-bit architecture is used, then a heap-allocated buffer of 1 MiB + /// is used. If this errors, then the error is returned. /// /// # Errors /// - /// Errors iff [`Self::with_buffer`] does for a 1024-byte buffer. + /// Errors iff `getpwnam_r` does. /// /// # Examples /// @@ -637,8 +841,31 @@ impl UserInfo { /// # Ok::<_, priv_sep::Errno>(()) /// ``` #[inline] - pub fn new(name: &CStr) -> Result<Option<Self>, Errno> { - Self::with_buffer(name, &mut [0; Self::PW_ENT_BUF_LEN]) + pub fn new<C: CStrHelper>(name: C) -> Result<Option<Self>, Errno> { + fn f(name: &CStr) -> Result<Option<UserInfo>, Errno> { + let wrapper = CStrWrapper(name); + let res = UserInfo::getpw_entry(wrapper, &mut [0; 0x400]); + #[cfg(not(feature = "alloc"))] + let res_final = res; + #[cfg(all(target_pointer_width = "16", feature = "alloc"))] + let res_final = res.or_else(|e| { + if matches!(e, Errno::ERANGE) { + Self::getpw_entry(wrapper, vec![0; 0x4000].as_mut_slice()) + } else { + Err(e) + } + }); + #[cfg(all(not(target_pointer_width = "16"), feature = "alloc"))] + let res_final = res.or_else(|e| { + if matches!(e, Errno::ERANGE) { + UserInfo::getpw_entry(wrapper, vec![0; 0x10_0000].as_mut_slice()) + } else { + Err(e) + } + }); + res_final + } + name.convert_then_apply(f) } /// Calls [`Gid::setresgid`] and [`Uid::setresuid`]. /// @@ -663,14 +890,14 @@ impl UserInfo { /// [`getpwuid_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwuid_r.html) /// is used to first confirm the existence of [`Self::uid`] and [`Self::gid`]. /// - /// Note this should rarely be used since most will rely on [`Self::new`], [`Self::with_buffer`], - /// [`Self::priv_drop`], or [`Self::chroot_then_priv_drop`]. + /// Note this should rarely be used since most will rely on [`Self::new`], [`Self::priv_drop`], or + /// [`Self::chroot_then_priv_drop`]. /// - /// Like [`Self::new`], this will fail if the buffer needed exceeds 16 KiB. + /// Read [`Self::new`] more information on the buffering strategy. /// /// # Errors /// - /// Errors iff `getpwuid_r` errors for a 16 KiB buffer, [`Self::uid`] and [`Self::gid`] don't exist in the user + /// Errors iff `getpwuid_r` errors, [`Self::uid`] and [`Self::gid`] don't exist in the user /// database, [`Gid::setresgid`] errors, or [`Uid::setresuid`] errors. /// /// # Examples @@ -682,17 +909,34 @@ impl UserInfo { /// ``` #[inline] pub fn setresid_if_valid(self) -> Result<(), SetresidErr> { - Self::getpw_entry(self.uid, &mut [0; Self::PW_ENT_BUF_LEN]) - .map_err(SetresidErr::Libc) - .and_then(|opt| { - opt.ok_or(SetresidErr::NoPasswdEntry).and_then(|info| { - if info.gid == self.gid { - self.setresid().map_err(SetresidErr::Libc) - } else { - Err(SetresidErr::GidMismatch) - } - }) + let res = Self::getpw_entry(self.uid, &mut [0; 0x400]); + #[cfg(not(feature = "alloc"))] + let res_final = res; + #[cfg(all(target_pointer_width = "16", feature = "alloc"))] + let res_final = res.or_else(|e| { + if matches!(e, Errno::ERANGE) { + Self::getpw_entry(self.uid, vec![0; 0x4000].as_mut_slice()) + } else { + Err(e) + } + }); + #[cfg(all(not(target_pointer_width = "16"), feature = "alloc"))] + let res_final = res.or_else(|e| { + if matches!(e, Errno::ERANGE) { + Self::getpw_entry(self.uid, vec![0; 0x10_0000].as_mut_slice()) + } else { + Err(e) + } + }); + res_final.map_err(SetresidErr::Libc).and_then(|opt| { + opt.ok_or(SetresidErr::NoPasswdEntry).and_then(|info| { + if info.gid == self.gid { + self.setresid().map_err(SetresidErr::Libc) + } else { + Err(SetresidErr::GidMismatch) + } }) + }) } /// Helper to unify targets that don't support `setgroups(2)`. /// @@ -743,8 +987,8 @@ impl UserInfo { /// # Ok::<_, PrivDropErr<Error>>(()) /// ``` #[inline] - pub fn priv_drop<T, E, F: FnOnce() -> Result<T, E>>( - name: &CStr, + pub fn priv_drop<C: CStrHelper, T, E, F: FnOnce() -> Result<T, E>>( + name: C, f: F, ) -> Result<T, PrivDropErr<E>> { Self::new(name).map_err(PrivDropErr::Libc).and_then(|opt| { @@ -779,8 +1023,8 @@ impl UserInfo { /// }); /// ``` #[inline] - pub async fn priv_drop_async<T, E, F: AsyncFnOnce() -> Result<T, E>>( - name: &CStr, + pub async fn priv_drop_async<C: CStrHelper, T, E, F: AsyncFnOnce() -> Result<T, E>>( + name: C, f: F, ) -> Result<T, PrivDropErr<E>> { match Self::new(name) { @@ -820,9 +1064,15 @@ impl UserInfo { /// # Ok::<_, PrivDropErr<Error>>(()) /// ``` #[inline] - pub fn chroot_then_priv_drop<T, E, F: FnOnce() -> Result<T, E>>( - name: &CStr, - path: &CStr, + pub fn chroot_then_priv_drop< + N: CStrHelper, + P: CStrHelper, + T, + E, + F: FnOnce() -> Result<T, E>, + >( + name: N, + path: P, chroot_after_f: bool, f: F, ) -> Result<T, PrivDropErr<E>> { @@ -867,9 +1117,15 @@ impl UserInfo { /// }); /// ``` #[inline] - pub async fn chroot_then_priv_drop_async<T, E, F: AsyncFnOnce() -> Result<T, E>>( - name: &CStr, - path: &CStr, + pub async fn chroot_then_priv_drop_async< + N: CStrHelper, + P: CStrHelper, + T, + E, + F: AsyncFnOnce() -> Result<T, E>, + >( + name: N, + path: P, chroot_after_f: bool, f: F, ) -> Result<T, PrivDropErr<E>> { @@ -924,7 +1180,7 @@ impl PartialEq<UserInfo> for &UserInfo { /// assert!(priv_sep::setgroups(&[]).is_ok()); /// ``` #[cfg_attr(docsrs, doc(cfg(not(target_os = "macos"))))] -#[cfg(not(target_os = "macos"))] +#[cfg(any(doc, not(target_os = "macos")))] #[expect(unsafe_code, reason = "setgroups(2) takes a pointer")] #[inline] pub fn setgroups(groups: &[Gid]) -> Result<(), Errno> { @@ -964,7 +1220,7 @@ pub fn setgroups(groups: &[Gid]) -> Result<(), Errno> { /// assert!(priv_sep::drop_supplementary_groups().is_ok()); /// ``` #[cfg_attr(docsrs, doc(cfg(not(target_os = "macos"))))] -#[cfg(not(target_os = "macos"))] +#[cfg(any(doc, not(target_os = "macos")))] #[expect(unsafe_code, reason = "setgroups(2) takes a pointer")] #[inline] pub fn drop_supplementary_groups() -> Result<(), Errno> { @@ -990,7 +1246,7 @@ pub fn drop_supplementary_groups() -> Result<(), Errno> { #[cfg(test)] mod tests { #[cfg(feature = "std")] - use super::{CStr, PrivDropErr}; + use super::PrivDropErr; use super::{Errno, Gid, SetresidErr, Uid, UserInfo}; #[cfg(all(feature = "std", target_os = "openbsd"))] use super::{Permissions, Promise, Promises}; @@ -1010,12 +1266,7 @@ mod tests { }; use tokio as _; #[cfg(feature = "std")] - const README: &CStr = c"README.md"; - #[cfg(feature = "std")] - const README_STR: &str = match README.to_str() { - Ok(val) => val, - Err(_) => panic!("not possible"), - }; + const README: &str = "README.md"; #[test] fn getuid() { _ = Uid::getuid(); @@ -1051,16 +1302,6 @@ mod tests { ); } #[test] - fn user_info_with_buffer() { - assert_eq!( - UserInfo::with_buffer(c"root", [0; 512].as_mut_slice()), - Ok(Some(UserInfo { - uid: Uid::ROOT, - gid: Gid(0), - })) - ); - } - #[test] fn user_info_setresid() -> Result<(), Errno> { UserInfo { uid: Uid::geteuid(), @@ -1069,7 +1310,7 @@ mod tests { .setresid() } #[test] - fn user_info_setresid_if_exists() -> Result<(), SetresidErr> { + fn user_info_setresid_if_valid() -> Result<(), SetresidErr> { UserInfo { uid: Uid::geteuid(), gid: Gid::getegid(), @@ -1077,7 +1318,7 @@ mod tests { .setresid_if_valid() } #[test] - fn user_info_setresid_if_exists_failure() { + fn user_info_setresid_if_valid_failure() { assert_eq!( UserInfo { uid: Uid::geteuid(), @@ -1121,7 +1362,7 @@ mod tests { TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 987, 0, 0)) }) .is_ok_and(|_| { - fs::exists(README_STR).is_ok_and(|exists| { + fs::exists(README).is_ok_and(|exists| { exists && TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 82, 0, 0)) .map_or_else( @@ -1145,30 +1386,20 @@ mod tests { #[test] #[ignore = "interferes with other tests"] fn unveil_pledge() { - const FILE_EXISTS: &CStr = c"/home/zack/foo.txt"; - const FILE_EXISTS_STR: &str = match FILE_EXISTS.to_str() { - Ok(val) => val, - Err(_) => panic!("not possible"), - }; - const FILE_NOT_EXISTS: &CStr = c"/home/zack/aadkjfasj3s23"; - const FILE_NOT_EXISTS_STR: &str = match FILE_NOT_EXISTS.to_str() { - Ok(val) => val, - Err(_) => panic!("not possible"), - }; - const DIR_NOT_EXISTS: &CStr = c"/home/zack/aadkjfasj3s23/"; - const DIR_NOT_EXISTS_STR: &str = match DIR_NOT_EXISTS.to_str() { - Ok(val) => val, - Err(_) => panic!("not possible"), - }; - _ = fs::metadata(FILE_EXISTS_STR).unwrap_or_else(|_e| { - panic!("{FILE_EXISTS_STR} does not exist, so unit testing cannot occur") + const FILE_EXISTS: &str = "/home/zack/foo.txt"; + const FILE_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23"; + const DIR_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23/"; + _ = fs::metadata(FILE_EXISTS).unwrap_or_else(|_e| { + panic!("{FILE_EXISTS} does not exist, so unit testing cannot occur") }); - drop(fs::metadata(FILE_NOT_EXISTS_STR).expect_err( - format!("{FILE_NOT_EXISTS_STR} exists, so unit testing cannot occur").as_str(), - )); - drop(fs::metadata(DIR_NOT_EXISTS_STR).expect_err( - format!("{DIR_NOT_EXISTS_STR} exists, so unit testing cannot occur").as_str(), + drop(fs::metadata(FILE_NOT_EXISTS).expect_err( + format!("{FILE_NOT_EXISTS} exists, so unit testing cannot occur").as_str(), )); + drop( + fs::metadata(DIR_NOT_EXISTS).expect_err( + format!("{DIR_NOT_EXISTS} exists, so unit testing cannot occur").as_str(), + ), + ); // This tests that a NULL `promise` does nothing. assert_eq!(Promises::pledge_none(), Ok(())); assert!(writeln!(io::stdout()).is_ok_and(|()| true)); @@ -1201,14 +1432,14 @@ mod tests { assert_eq!(initial_promises.pledge(), Ok(())); // This tests unveil with no permissions. assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Ok(())); - assert!(fs::metadata(FILE_EXISTS_STR).map_or_else( + assert!(fs::metadata(FILE_EXISTS).map_or_else( |e| matches!(e.kind(), ErrorKind::PermissionDenied), |_| false )); // This tests unveil with read permissions, // and one can unveil more permissions (unlike pledge which can only remove promises). assert_eq!(Permissions::READ.unveil(FILE_EXISTS), Ok(())); - assert!(fs::metadata(FILE_EXISTS_STR).is_ok_and(|_| true)); + assert!(fs::metadata(FILE_EXISTS).is_ok_and(|_| true)); // This tests that calls to unveil on missing files don't error. assert_eq!(Permissions::NONE.unveil(FILE_NOT_EXISTS), Ok(())); // This tests that calls to unveil on missing directories error. @@ -1216,7 +1447,7 @@ mod tests { // This tests that unveil can no longer be called. assert_eq!(Permissions::unveil_no_more(), Ok(())); assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Err(Errno::EPERM)); - assert!(fs::metadata(FILE_EXISTS_STR).is_ok_and(|_| true)); + assert!(fs::metadata(FILE_EXISTS).is_ok_and(|_| true)); // The below tests that Promises can only be removed and not added. initial_promises.remove_promises([Promise::Unveil]); assert_eq!(initial_promises.len(), 2); @@ -1229,7 +1460,7 @@ mod tests { assert_eq!(Promises::new([Promise::Rpath]).pledge(), Err(Errno::EPERM)); // If the below is uncommented, the program should crash since the above // call to pledge no longer allows access to the file system. - // drop(fs::metadata(FILE_EXISTS_STR)); + // drop(fs::metadata(FILE_EXISTS)); } #[cfg(all(feature = "std", target_os = "openbsd"))] #[test] @@ -1245,7 +1476,7 @@ mod tests { ) .is_ok_and(|(_, _)| { Permissions::unveil_raw(README, c"r").is_ok_and(|()| { - fs::exists(README_STR).is_ok_and(|exists| { + fs::exists(README).is_ok_and(|exists| { Permissions::NONE.unveil(README).is_ok_and(|()| { Promises::pledge_raw(c"inet stdio").is_ok_and(|()| { exists @@ -1282,23 +1513,21 @@ mod tests { ) .is_ok_and(|(_, mut promises)| Permissions::READ .unveil(README) - .is_ok_and( - |()| fs::exists(README_STR).is_ok_and(|exists| Permissions::NONE - .unveil(README) - .is_ok_and(|()| promises - .remove_promises_then_pledge([Promise::Rpath, Promise::Unveil]) - .is_ok_and(|()| exists - && TcpListener::bind(SocketAddrV6::new( - Ipv6Addr::LOCALHOST, - 588, - 0, - 0 - )) - .map_or_else( - |e| matches!(e.kind(), ErrorKind::PermissionDenied), - |_| false - )))) - )) + .is_ok_and(|()| fs::exists(README).is_ok_and(|exists| Permissions::NONE + .unveil(README) + .is_ok_and(|()| promises + .remove_promises_then_pledge([Promise::Rpath, Promise::Unveil]) + .is_ok_and(|()| exists + && TcpListener::bind(SocketAddrV6::new( + Ipv6Addr::LOCALHOST, + 588, + 0, + 0 + )) + .map_or_else( + |e| matches!(e.kind(), ErrorKind::PermissionDenied), + |_| false + )))))) ); } } diff --git a/src/openbsd.rs b/src/openbsd.rs @@ -1,6 +1,6 @@ #[cfg(doc)] use super::chroot_then_chdir; -use super::{Errno, PrivDropErr, SUCCESS, UserInfo}; +use super::{CStrHelper, Errno, PrivDropErr, SUCCESS, UserInfo}; use core::{ ffi::{CStr, c_char, c_int}, fmt::{self, Display, Formatter}, @@ -288,8 +288,14 @@ impl Promises { /// # Ok::<_, PrivDropErr<Error>>(()) /// ``` #[inline] - pub fn new_priv_drop<Prom: AsRef<[Promise]>, T, E, F: FnOnce() -> Result<T, E>>( - name: &CStr, + pub fn new_priv_drop< + C: CStrHelper, + Prom: AsRef<[Promise]>, + T, + E, + F: FnOnce() -> Result<T, E>, + >( + name: C, initial: Prom, retain_id_promise: bool, f: F, @@ -344,12 +350,13 @@ impl Promises { /// ``` #[inline] pub async fn new_priv_drop_async< + C: CStrHelper, Prom: AsRef<[Promise]>, T, E, F: AsyncFnOnce() -> Result<T, E>, >( - name: &CStr, + name: C, initial: Prom, retain_id_promise: bool, f: F, @@ -407,9 +414,16 @@ impl Promises { /// # Ok::<_, PrivDropErr<Error>>(()) /// ``` #[inline] - pub fn new_chroot_then_priv_drop<Prom: AsRef<[Promise]>, T, E, F: FnOnce() -> Result<T, E>>( - name: &CStr, - path: &CStr, + pub fn new_chroot_then_priv_drop< + N: CStrHelper, + P: CStrHelper, + Prom: AsRef<[Promise]>, + T, + E, + F: FnOnce() -> Result<T, E>, + >( + name: N, + path: P, initial: Prom, retain_id_promise: bool, f: F, @@ -470,13 +484,15 @@ impl Promises { /// ``` #[inline] pub async fn new_chroot_then_priv_drop_async< + N: CStrHelper, + P: CStrHelper, Prom: AsRef<[Promise]>, T, E, F: AsyncFnOnce() -> Result<T, E>, >( - name: &CStr, - path: &CStr, + name: N, + path: P, initial: Prom, retain_id_promise: bool, f: F, @@ -1248,7 +1264,7 @@ impl Permissions { /// )); /// ``` #[inline] - pub fn unveil(self, path: &CStr) -> Result<(), Errno> { + pub fn unveil<P: CStrHelper>(self, path: P) -> Result<(), Errno> { let perms = if self.is_enabled(Permission::Create) { if self.is_enabled(Permission::Execute) { if self.is_enabled(Permission::Read) { @@ -1296,7 +1312,7 @@ impl Permissions { } else { c"" }; - Self::inner_unveil(path.as_ptr(), perms.as_ptr()) + path.convert_then_apply(|p| Self::inner_unveil(p.as_ptr(), perms.as_ptr())) } /// Invokes [`unveil(2)`](https://man.openbsd.org/unveil.2) by passing `NULL` for both `path` and /// `permissions`. @@ -1341,9 +1357,8 @@ impl Permissions { /// )); /// ``` #[inline] - pub fn unveil_raw(path: &CStr, permissions: &CStr) -> Result<(), Errno> { - // `NULL` is valid for both `path` and `permissions`. - Self::inner_unveil(path.as_ptr(), permissions.as_ptr()) + pub fn unveil_raw<P: CStrHelper>(path: P, permissions: &CStr) -> Result<(), Errno> { + path.convert_then_apply(|p| Self::inner_unveil(p.as_ptr(), permissions.as_ptr())) } } impl Display for Permissions {