commit 4637e015b5ac42b6ff866422c6d0b50531c4541a
parent 6806c11be86bce6457e7c788ce32c8f068ce2b7d
Author: Zack Newman <zack@philomathiclife.com>
Date: Tue, 7 Oct 2025 18:57:45 -0600
clean up tests. improve docs
Diffstat:
6 files changed, 252 insertions(+), 240 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -10,7 +10,7 @@ name = "priv_sep"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/priv_sep/"
rust-version = "1.86.0"
-version = "3.0.0-alpha.2.0"
+version = "3.0.0-alpha.2.1"
[lints.rust]
ambiguous_negative_literals = { level = "deny", priority = -1 }
@@ -91,15 +91,12 @@ semicolon_inside_block = "allow"
[package.metadata.docs.rs]
all-features = true
-cargo-args = ["-Zbuild-std=std"]
-default-target = "x86_64-unknown-openbsd"
+default-target = "x86_64-unknown-linux-gnu"
targets = [
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"i686-unknown-linux-gnu",
- "x86_64-unknown-dragonfly",
"x86_64-unknown-freebsd",
- "x86_64-unknown-linux-gnu",
"x86_64-unknown-netbsd"
]
diff --git a/README.md b/README.md
@@ -1,4 +1,5 @@
-# `priv_sep`
+Privilege separation library for Unix-likes OSes
+================================================
[<img alt="git" src="https://git.philomathiclife.com/badges/priv_sep.svg" height="20">](https://git.philomathiclife.com/priv_sep/log.html)
[<img alt="crates.io" src="https://img.shields.io/crates/v/priv_sep.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/priv_sep)
@@ -12,15 +13,14 @@ Note the only platforms that are currently supported are platforms that correspo
* `dragonfly`
* `freebsd`
* `linux`
+* `macos`
* `netbsd`
* `openbsd`
-or the `apple` `target_vendor`.
-
## `priv_sep` in action for OpenBSD
```rust
-use core::convert::Infallible;
+use core::{convert::Infallible, ffi::CStr};
use priv_sep::{Permissions, PrivDropErr, Promise, Promises};
use std::{
fs,
@@ -31,7 +31,12 @@ use tokio::net::TcpListener;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<Infallible, PrivDropErr<Error>> {
/// Config file.
- const CONFIG: &str = "config";
+ 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"),
+ };
// 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`.
@@ -41,19 +46,20 @@ async fn main() -> Result<Infallible, PrivDropErr<Error>> {
// `setresuid(2)` to the user ID associated with nobody.
// Remove `id` from our `pledge(2)`d promises.
let (listener, mut promises) = Promises::new_chroot_then_priv_drop_async(
- "nobody",
- "/path/chroot/",
+ c"nobody",
+ c"/path/chroot/",
[Promise::Inet, Promise::Rpath, Promise::Unveil],
false,
async || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await,
- ).await?;
+ )
+ .await?;
// At this point, the process is running under nobody.
// Only allow file system access to `config` and only allow read access to it.
Permissions::READ.unveil(CONFIG)?;
// 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)?;
+ let config = fs::read(CONFIG_STR).map_err(PrivDropErr::Other)?;
// Remove file system access.
Permissions::NONE.unveil(CONFIG)?;
// Remove `rpath` and `unveil` from our `pledge(2)`d promises
@@ -72,7 +78,7 @@ async fn main() -> Result<Infallible, PrivDropErr<Error>> {
```rust
use core::convert::Infallible;
-use priv_sep::{UserInfo, PrivDropErr};
+use priv_sep::{PrivDropErr, UserInfo};
use std::{
io::Error,
net::{Ipv6Addr, SocketAddrV6},
@@ -86,9 +92,11 @@ async fn main() -> Result<Infallible, PrivDropErr<Error>> {
// `setgroups(2)` to drop all supplementary groups.
// `setresgid(2)` to the group ID associated with nobody.
// `setresuid(2)` to the user ID associated with nobody.
- let listener = UserInfo::chroot_then_priv_drop_async("nobody", "/path/chroot/", false, async || {
- TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await
- }).await?;
+ let listener =
+ UserInfo::chroot_then_priv_drop_async(c"nobody", c"/path/chroot/", false, async || {
+ TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await
+ })
+ .await?;
// At this point, the process is running under nobody.
loop {
// Handle TCP connections.
@@ -126,12 +134,15 @@ at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you,
as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
-Before any PR is sent, `cargo clippy` and `cargo t` should be run _for each possible combination of "features"_
-using the stable and MSRV toolchains. One easy way to achieve this is by invoking
-[`ci-cargo`](https://crates.io/crates/ci-cargo) in the `priv_sep` directory. Additionally,
-`RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be built.
+Before any PR is sent, `cargo clippy --all-targets` and `cargo t` should be run _for each possible combination of
+"features"_ using the stable and MSRV toolchains. One easy way to achieve this is by invoking
+[`ci-cargo`](https://crates.io/crates/ci-cargo) with the `--all-targets` option in the `priv_sep` directory.
+Additionally, one should test all `ignore` tests as both root and non-root. When run as root or on OpenBSD,
+`ignore` tests should be run separately. Last, `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features`
+should be run to ensure documentation can be built on non-OpenBSD platforms; otherwise `cargo doc --all-features`
+should be run.
### Status
The crate is only tested on the `x86_64-unknown-linux-gnu`, `x86_64-unknown-openbsd`, and `aarch64-apple-darwin`
-targets; but it should work on most platforms.
+targets; but it should work on most of the supported platforms.
diff --git a/src/c.rs b/src/c.rs
@@ -7,9 +7,9 @@ pub(crate) const SUCCESS: c_int = 0;
any(
target_os = "dragonfly",
target_os = "freebsd",
+ target_os = "macos",
target_os = "netbsd",
- target_os = "openbsd",
- target_vendor = "apple"
+ target_os = "openbsd"
),
target_arch = "aarch64",
target_pointer_width = "32"
@@ -20,9 +20,9 @@ type TimeT = i32;
any(
target_os = "dragonfly",
target_os = "freebsd",
+ target_os = "macos",
target_os = "netbsd",
- target_os = "openbsd",
- target_vendor = "apple"
+ target_os = "openbsd"
),
not(all(target_arch = "aarch64", target_pointer_width = "32"))
))]
@@ -42,18 +42,18 @@ pub(crate) struct Passwd {
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
+ target_os = "macos",
target_os = "netbsd",
- target_os = "openbsd",
- target_vendor = "apple"
+ target_os = "openbsd"
))]
change: TimeT,
/// User access class.
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
+ target_os = "macos",
target_os = "netbsd",
- target_os = "openbsd",
- target_vendor = "apple"
+ target_os = "openbsd"
))]
class: *mut c_char,
/// User information.
@@ -66,9 +66,9 @@ pub(crate) struct Passwd {
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
+ target_os = "macos",
target_os = "netbsd",
- target_os = "openbsd",
- target_vendor = "apple"
+ target_os = "openbsd"
))]
expire: TimeT,
/// Internal fields.
@@ -103,7 +103,7 @@ unsafe extern "C" {
))]
pub(crate) safe fn setresuid(ruid: u32, euid: u32, suid: u32) -> c_int;
/// [`setuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setuid.html#).
- #[cfg(any(target_os = "netbsd", target_vendor = "apple"))]
+ #[cfg(any(target_os = "macos", target_os = "netbsd"))]
pub(crate) safe fn setuid(uid: u32) -> c_int;
/// [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html).
#[cfg(any(
@@ -114,7 +114,7 @@ unsafe extern "C" {
))]
pub(crate) safe fn setresgid(rgid: u32, egid: u32, sgid: u32) -> c_int;
/// [`setgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setgid.html#).
- #[cfg(any(target_os = "netbsd", target_vendor = "apple"))]
+ #[cfg(any(target_os = "macos", target_os = "netbsd"))]
pub(crate) safe fn setgid(gid: u32) -> c_int;
/// [`chroot(2)`](https://manned.org/chroot.2).
pub(crate) fn chroot(path: *const c_char) -> c_int;
@@ -151,7 +151,7 @@ unsafe extern "C" {
#[cfg(any(target_os = "netbsd", target_os = "openbsd"))]
pub(crate) fn __errno() -> *mut c_int;
/// [`__error`](https://github.com/freebsd/freebsd-src/blob/main/sys/sys/errno.h#L43).
- #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
+ #[cfg(any(target_os = "freebsd", target_os = "macos"))]
pub(crate) fn __error() -> *mut c_int;
/// [`__errno_location`](https://sourceware.org/git/?p=glibc.git;a=blob;f=include/errno.h;h=f0ccaa74dd8bebed83e4adfbf301339efeae3cdb;hb=HEAD#l37).
#[cfg(any(target_os = "dragonfly", target_os = "linux"))]
diff --git a/src/err.rs b/src/err.rs
@@ -563,7 +563,7 @@ impl Errno {
97 => Self::EINTEGRITY,
_ => Self::Other(OtherErrno(code)),
}
- #[cfg(target_vendor = "apple")]
+ #[cfg(target_os = "macos")]
match code {
1 => Self::EPERM,
2 => Self::ENOENT,
@@ -1346,7 +1346,7 @@ impl Errno {
| Self::ENOPOLICY
| Self::EQFULL => impossible!(),
}
- #[cfg(target_vendor = "apple")]
+ #[cfg(target_os = "macos")]
match self {
Self::EPERM => 1,
Self::ENOENT => 2,
@@ -1992,7 +1992,7 @@ impl Errno {
unsafe {
c::__errno()
}
- #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
+ #[cfg(any(target_os = "freebsd", target_os = "macos"))]
// SAFETY:
// Safe because errno is a thread-local variable.
unsafe {
diff --git a/src/lib.rs b/src/lib.rs
@@ -7,23 +7,13 @@
//! `priv_sep` is a library that uses the system's libc to perform privilege separation and privilege reduction
//! for Unix-like platforms.
//!
-//! Note the only platforms that are currently supported are platforms that correspond to the following
-//! `target_os` values:
-//!
-//! * `dragonfly`
-//! * `freebsd`
-//! * `linux`
-//! * `netbsd`
-//! * `openbsd`
-//!
-//! or the `apple` `target_vendor`.
-//!
//! ## `priv_sep` in action for OpenBSD
//!
//! ```no_run
//! # #[cfg(target_os = "openbsd")]
-//! use core::ffi::CStr;
-//! use core::convert::Infallible;
+//! use core::{convert::Infallible, ffi::CStr};
+//! # #[cfg(not(target_os = "openbsd"))]
+//! # use core::convert::Infallible;
//! # #[cfg(target_os = "openbsd")]
//! use priv_sep::{Permissions, PrivDropErr, Promise, Promises};
//! use std::{
@@ -39,6 +29,11 @@
//! 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"),
+//! };
//! // 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`.
@@ -53,14 +48,15 @@
//! [Promise::Inet, Promise::Rpath, Promise::Unveil],
//! false,
//! async || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await,
-//! ).await?;
+//! )
+//! .await?;
//! // At this point, the process is running under nobody.
//! // Only allow file system access to `config` and only allow read access to it.
//! Permissions::READ.unveil(CONFIG)?;
//! // 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.to_str().unwrap_or_else(|_e| unreachable!("only contains UTF-8"))).map_err(PrivDropErr::Other)?;
+//! let config = fs::read(CONFIG_STR).map_err(PrivDropErr::Other)?;
//! // Remove file system access.
//! Permissions::NONE.unveil(CONFIG)?;
//! // Remove `rpath` and `unveil` from our `pledge(2)`d promises
@@ -79,7 +75,7 @@
//!
//! ```no_run
//! use core::convert::Infallible;
-//! use priv_sep::{UserInfo, PrivDropErr};
+//! use priv_sep::{PrivDropErr, UserInfo};
//! use std::{
//! io::Error,
//! net::{Ipv6Addr, SocketAddrV6},
@@ -93,9 +89,11 @@
//! // `setgroups(2)` to drop all supplementary groups.
//! // `setresgid(2)` to the group ID associated with nobody.
//! // `setresuid(2)` to the user ID associated with nobody.
-//! let listener = UserInfo::chroot_then_priv_drop_async(c"nobody", c"/path/chroot/", false, async || {
-//! TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await
-//! }).await?;
+//! let listener =
+//! UserInfo::chroot_then_priv_drop_async(c"nobody", c"/path/chroot/", false, async || {
+//! TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await
+//! })
+//! .await?;
//! // At this point, the process is running under nobody.
//! loop {
//! // Handle TCP connections.
@@ -111,11 +109,11 @@
target_os = "dragonfly",
target_os = "freebsd",
target_os = "linux",
+ target_os = "macos",
target_os = "netbsd",
- target_os = "openbsd",
- target_vendor = "apple"
+ target_os = "openbsd"
))]
-#![allow(
+#![expect(
clippy::pub_use,
reason = "don't want Errno nor openbsd types in a module"
)]
@@ -210,7 +208,7 @@ impl Uid {
target_os = "openbsd"
))]
let code = c::setresuid(self.0, self.0, self.0);
- #[cfg(any(target_os = "netbsd", target_vendor = "apple"))]
+ #[cfg(any(target_os = "macos", target_os = "netbsd"))]
let code = c::setuid(self.0);
if code == SUCCESS {
Ok(())
@@ -304,7 +302,7 @@ impl Gid {
target_os = "openbsd"
))]
let code = c::setresgid(self.0, self.0, self.0);
- #[cfg(any(target_os = "netbsd", target_vendor = "apple"))]
+ #[cfg(any(target_os = "macos", target_os = "netbsd"))]
let code = c::setgid(self.0);
if code == SUCCESS {
Ok(())
@@ -413,7 +411,7 @@ pub fn chroot_then_chdir(path: &CStr) -> Result<(), Errno> {
chroot(path).and_then(|()| private_chdir(ROOT))
}
/// Error returned when dropping privileges.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PrivDropErr<E> {
/// Error when an error occurs from a libc call.
Libc(Errno),
@@ -448,7 +446,7 @@ impl<E> From<Errno> for PrivDropErr<E> {
}
}
/// Error returned from [`UserInfo::setresid_if_valid`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SetresidErr {
/// Error when an error occurs from a libc call.
Libc(Errno),
@@ -689,7 +687,7 @@ impl UserInfo {
/// Helper to unify targets that don't support `setgroups(2)`.
///
/// No-op.
- #[cfg(target_vendor = "apple")]
+ #[cfg(target_os = "macos")]
#[expect(
clippy::unnecessary_wraps,
reason = "unify with platforms that support `setgroups`"
@@ -981,7 +979,7 @@ pub fn setgroups(groups: &[Gid]) -> Result<(), Errno> {
target_os = "linux",
target_os = "netbsd",
target_os = "openbsd",
- all(doc, target_vendor = "apple")
+ all(doc, target_os = "macos")
))]
#[expect(unsafe_code, reason = "setgroups(2) takes a pointer")]
#[inline]
@@ -1015,9 +1013,17 @@ mod tests {
#[cfg(feature = "std")]
use core::net::{Ipv6Addr, SocketAddrV6};
#[cfg(all(feature = "std", target_os = "openbsd"))]
- use std::{format, print};
+ extern crate alloc;
+ #[cfg(all(feature = "std", target_os = "openbsd"))]
+ use alloc::format;
+ #[cfg(all(feature = "std", target_os = "openbsd"))]
+ use std::io::{self, Write as _};
#[cfg(feature = "std")]
- use std::{fs, io::Error, net::TcpListener};
+ use std::{
+ fs,
+ io::{Error, ErrorKind},
+ net::TcpListener,
+ };
use tokio as _;
#[cfg(feature = "std")]
const README: &CStr = c"README.md";
@@ -1027,45 +1033,51 @@ mod tests {
Err(_) => panic!("not possible"),
};
#[test]
- fn test_getuid() {
+ fn getuid() {
_ = Uid::getuid();
}
#[test]
- fn test_geteuid() {
+ fn geteuid() {
_ = Uid::geteuid();
}
#[test]
- fn test_getgid() {
+ fn getgid() {
_ = Gid::getgid();
}
#[test]
- fn test_getegid() {
+ fn getegid() {
_ = Gid::getegid();
}
#[test]
- fn test_setresuid() -> Result<(), Errno> {
+ fn setresuid() -> Result<(), Errno> {
Uid::geteuid().setresuid()
}
#[test]
- fn test_setresgid() -> Result<(), Errno> {
+ fn setresgid() -> Result<(), Errno> {
Gid::getegid().setresgid()
}
#[test]
- fn test_user_info_new() -> Result<(), Errno> {
- if let Some(user) = UserInfo::new(c"root")? {
- assert!(user.is_root());
- }
- Ok(())
+ fn user_info_new() {
+ assert_eq!(
+ UserInfo::new(c"root"),
+ Ok(Some(UserInfo {
+ uid: Uid::ROOT,
+ gid: Gid(0),
+ }))
+ );
}
#[test]
- fn test_user_info_with_buffer() -> Result<(), Errno> {
- if let Some(user) = UserInfo::with_buffer(c"root", [0; 512].as_mut_slice())? {
- assert!(user.is_root());
- }
- Ok(())
+ 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 test_user_info_setresid() -> Result<(), Errno> {
+ fn user_info_setresid() -> Result<(), Errno> {
UserInfo {
uid: Uid::geteuid(),
gid: Gid::getegid(),
@@ -1073,7 +1085,7 @@ mod tests {
.setresid()
}
#[test]
- fn test_user_info_setresid_if_exists() -> Result<(), SetresidErr> {
+ fn user_info_setresid_if_exists() -> Result<(), SetresidErr> {
UserInfo {
uid: Uid::geteuid(),
gid: Gid::getegid(),
@@ -1081,92 +1093,102 @@ mod tests {
.setresid_if_valid()
}
#[test]
- fn test_user_info_setresid_if_exists_failure() {
- assert!(
+ fn user_info_setresid_if_exists_failure() {
+ assert_eq!(
UserInfo {
uid: Uid::geteuid(),
gid: Gid(u32::MAX),
}
- .setresid_if_valid()
- .map_or_else(|e| matches!(e, SetresidErr::GidMismatch), |_| false)
+ .setresid_if_valid(),
+ Err(SetresidErr::GidMismatch)
);
}
#[cfg(feature = "std")]
#[test]
- #[ignore]
- fn test_priv_drop() -> Result<(), PrivDropErr<Error>> {
+ #[ignore = "primarily useful for root and interferes with chroot_drop_priv"]
+ fn priv_drop() {
if Uid::geteuid().is_root() {
- UserInfo::priv_drop(c"nobody", || {
- TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0))
- })
- .map(|_| {
- assert!(
- TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)).is_err()
- );
- })
+ assert!(
+ UserInfo::priv_drop(c"nobody", || {
+ TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0))
+ })
+ .is_ok_and(|_| true)
+ );
+ assert!(
+ TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)).map_or_else(
+ |e| matches!(e.kind(), ErrorKind::PermissionDenied),
+ |_| false
+ )
+ );
} else {
assert!(
UserInfo::priv_drop(c"root", || Ok::<_, Error>(()))
- .map_or_else(|e| matches!(e, PrivDropErr::RootEntry), |_| false)
+ .map_or_else(|e| matches!(e, PrivDropErr::RootEntry), |()| false)
);
- Ok(())
}
}
#[cfg(feature = "std")]
#[test]
- #[ignore]
- fn test_chroot_priv_drop() -> Result<(), PrivDropErr<Error>> {
+ #[ignore = "primarily useful for root and interferes with priv_drop"]
+ fn chroot_drop_priv() {
if Uid::geteuid().is_root() {
- UserInfo::chroot_then_priv_drop(c"nobody", c"./", false, || {
- TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 987, 0, 0))
- })
- .and_then(|_| {
- fs::exists(README_STR)
- .map_err(PrivDropErr::Other)
- .map(|exists| {
- assert!(exists);
- assert!(
- TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 82, 0, 0))
- .is_err()
- );
+ assert!(
+ UserInfo::chroot_then_priv_drop(c"nobody", c"./", false, || {
+ TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 987, 0, 0))
+ })
+ .is_ok_and(|_| {
+ fs::exists(README_STR).is_ok_and(|exists| {
+ exists
+ && TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 82, 0, 0))
+ .map_or_else(
+ |e| matches!(e.kind(), ErrorKind::PermissionDenied),
+ |_| false,
+ )
})
- })
- } else {
- Ok(())
+ })
+ );
}
}
+ #[expect(
+ clippy::panic,
+ reason = "reasonable to expect files to not exist that need to in order to test pledge and unveil"
+ )]
+ #[expect(
+ clippy::expect_used,
+ reason = "kinda reasonable to expect files to exist that mustn't in order to test pledge and unveil"
+ )]
#[cfg(all(feature = "std", target_os = "openbsd"))]
#[test]
- #[ignore]
- fn test_pledge_unveil() {
+ #[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"),
};
- _ = fs::metadata(FILE_EXISTS_STR).expect(
- format!("{FILE_EXISTS_STR} does not exist, so unit testing cannot occur").as_str(),
- );
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"),
};
- drop(fs::metadata(FILE_NOT_EXISTS_STR).expect_err(
- format!("{FILE_NOT_EXISTS_STR} exists, so unit testing cannot occur").as_str(),
- ));
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")
+ });
+ 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(),
));
// This tests that a NULL `promise` does nothing.
- assert!(Promises::pledge_none().is_ok());
- print!("");
- assert!(Promises::ALL.pledge().is_ok());
+ assert_eq!(Promises::pledge_none(), Ok(()));
+ assert!(writeln!(io::stdout()).is_ok_and(|()| true));
+ assert_eq!(Promises::ALL.pledge(), Ok(()));
// This tests that duplicates are ignored as well as the implementation of PartialEq.
let mut initial_promises = Promises::new([
Promise::Stdio,
@@ -1174,39 +1196,43 @@ mod tests {
Promise::Rpath,
Promise::Stdio,
]);
- assert!(initial_promises.len() == 3);
- assert!(
- initial_promises == Promises::new([Promise::Rpath, Promise::Stdio, Promise::Unveil])
+ assert_eq!(initial_promises.len(), 3);
+ assert_eq!(
+ initial_promises,
+ Promises::new([Promise::Rpath, Promise::Stdio, Promise::Unveil])
);
// Test retain.
- assert!({
- let mut vals = Promises::new([
- Promise::Audio,
- Promise::Bpf,
- Promise::Chown,
- Promise::Cpath,
- Promise::Error,
- Promise::Exec,
- ]);
- vals.retain([Promise::Error, Promise::Chown]);
- vals.len() == 2 && vals.contains(Promise::Chown) && vals.contains(Promise::Error)
- });
- assert!(initial_promises.pledge().is_ok());
+ let mut vals = Promises::new([
+ Promise::Audio,
+ Promise::Bpf,
+ Promise::Chown,
+ Promise::Cpath,
+ Promise::Error,
+ Promise::Exec,
+ ]);
+ vals.retain([Promise::Error, Promise::Chown]);
+ assert_eq!(vals.len(), 2);
+ assert!(vals.contains(Promise::Chown));
+ assert!(vals.contains(Promise::Error));
+ assert_eq!(initial_promises.pledge(), Ok(()));
// This tests unveil with no permissions.
- assert!(Permissions::NONE.unveil(FILE_EXISTS).is_ok());
- assert!(fs::metadata(FILE_EXISTS_STR).is_err());
+ assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Ok(()));
+ assert!(fs::metadata(FILE_EXISTS_STR).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!(Permissions::READ.unveil(FILE_EXISTS).is_ok());
- assert!(fs::metadata(FILE_EXISTS_STR).is_ok());
+ assert_eq!(Permissions::READ.unveil(FILE_EXISTS), Ok(()));
+ assert!(fs::metadata(FILE_EXISTS_STR).is_ok_and(|_| true));
// This tests that calls to unveil on missing files don't error.
- assert!(Permissions::NONE.unveil(FILE_NOT_EXISTS).is_ok());
+ assert_eq!(Permissions::NONE.unveil(FILE_NOT_EXISTS), Ok(()));
// This tests that calls to unveil on missing directories error.
- assert!(Permissions::NONE.unveil(DIR_NOT_EXISTS).is_err());
+ assert_eq!(Permissions::NONE.unveil(DIR_NOT_EXISTS), Err(Errno::ENOENT));
// This tests that unveil can no longer be called.
- assert!(Permissions::unveil_no_more().is_ok());
- assert!(Permissions::NONE.unveil(FILE_EXISTS).is_err());
- assert!(fs::metadata(FILE_EXISTS_STR).is_ok());
+ 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));
// The below tests that Promises can only be removed and not added.
initial_promises.remove_promises([Promise::Unveil]);
assert_eq!(initial_promises.len(), 2);
@@ -1214,105 +1240,82 @@ mod tests {
assert_eq!(initial_promises.len(), 1);
initial_promises.remove(Promise::Rpath);
assert_eq!(initial_promises.len(), 1);
- assert!(initial_promises.pledge().is_ok());
- print!("");
- assert!(Promises::new([Promise::Rpath]).pledge().is_err());
+ assert_eq!(initial_promises.pledge(), Ok(()));
+ assert!(writeln!(io::stdout()).is_ok_and(|()| true));
+ 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));
}
#[cfg(all(feature = "std", target_os = "openbsd"))]
#[test]
- #[ignore]
- fn test_pledge_priv_drop() -> Result<(), PrivDropErr<Error>> {
+ #[ignore = "interferes with other tests when root"]
+ fn pledge_inet_drop_priv() {
if Uid::geteuid().is_root() {
- Promises::new_priv_drop(
- c"nobody",
- [Promise::Inet, Promise::Rpath, Promise::Unveil],
- false,
- || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 47, 0, 0)),
- )
- .and_then(|(_, _)| {
- Permissions::unveil_raw(README, c"r")
- .map_err(PrivDropErr::Libc)
- .and_then(|()| {
- fs::exists(README_STR)
- .map_err(PrivDropErr::Other)
- .and_then(|exists| {
- Permissions::NONE
- .unveil(README)
- .map_err(PrivDropErr::Libc)
- .and_then(|()| {
- Promises::pledge_raw(c"inet stdio")
- .map_err(PrivDropErr::Libc)
- .map(|()| {
- assert!(exists);
- assert!(
- TcpListener::bind(SocketAddrV6::new(
- Ipv6Addr::LOCALHOST,
- 792,
- 0,
- 0
- ))
- .is_err()
- );
- })
- })
+ assert!(
+ Promises::new_priv_drop(
+ c"nobody",
+ [Promise::Inet, Promise::Rpath, Promise::Unveil],
+ false,
+ || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 47, 0, 0)),
+ )
+ .is_ok_and(|(_, _)| {
+ Permissions::unveil_raw(README, c"r").is_ok_and(|()| {
+ fs::exists(README_STR).is_ok_and(|exists| {
+ Permissions::NONE.unveil(README).is_ok_and(|()| {
+ Promises::pledge_raw(c"inet stdio").is_ok_and(|()| {
+ exists
+ && TcpListener::bind(SocketAddrV6::new(
+ Ipv6Addr::LOCALHOST,
+ 792,
+ 0,
+ 0,
+ ))
+ .map_or_else(
+ |e| matches!(e.kind(), ErrorKind::PermissionDenied),
+ |_| false,
+ )
+ })
})
+ })
})
- })
- } else {
- Ok(())
+ })
+ );
}
}
#[cfg(all(feature = "std", target_os = "openbsd"))]
#[test]
- #[ignore]
- fn test_pledge_chroot_priv_drop() -> Result<(), PrivDropErr<Error>> {
+ #[ignore = "interferes with other tests when root"]
+ fn inet_chroot_priv_pledge() {
if Uid::geteuid().is_root() {
- Promises::new_chroot_then_priv_drop(
- c"nobody",
- c"./",
- [Promise::Inet, Promise::Rpath, Promise::Unveil],
- false,
- || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 382, 0, 0)),
- )
- .and_then(|(_, mut promises)| {
- Permissions::READ
+ assert!(
+ Promises::new_chroot_then_priv_drop(
+ c"nobody",
+ c"./",
+ [Promise::Inet, Promise::Rpath, Promise::Unveil],
+ false,
+ || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 382, 0, 0))
+ )
+ .is_ok_and(|(_, mut promises)| Permissions::READ
.unveil(README)
- .map_err(PrivDropErr::Libc)
- .and_then(|()| {
- fs::exists(README_STR)
- .map_err(PrivDropErr::Other)
- .and_then(|exists| {
- Permissions::NONE
- .unveil(README)
- .map_err(PrivDropErr::Libc)
- .and_then(|()| {
- promises
- .remove_promises_then_pledge([
- Promise::Rpath,
- Promise::Unveil,
- ])
- .map_err(PrivDropErr::Libc)
- .map(|()| {
- assert!(exists);
- assert!(
- TcpListener::bind(SocketAddrV6::new(
- Ipv6Addr::LOCALHOST,
- 588,
- 0,
- 0
- ))
- .is_err()
- );
- })
- })
- })
- })
- })
- } else {
- Ok(())
+ .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
+ ))))
+ ))
+ );
}
}
}
diff --git a/src/openbsd.rs b/src/openbsd.rs
@@ -1,3 +1,4 @@
+#![cfg_attr(docsrs, doc(cfg(target_os = "openbsd")))]
#[cfg(doc)]
use super::chroot_then_chdir;
use super::{Errno, PrivDropErr, SUCCESS, UserInfo};