priv_sep

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

commit 49e56b11b5fa96201214cfb342a97321d948ffdd
parent 40554cc7d568214ecd497018ab9df9c6d9c69354
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon, 19 May 2025 11:26:35 -0600

refactor. add chroot and setresuid

Diffstat:
Apriv_sep/.gitignore | 2++
Apriv_sep/Cargo.toml | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apriv_sep/LICENSE-APACHE | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apriv_sep/LICENSE-MIT | 20++++++++++++++++++++
Apriv_sep/README.md | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apriv_sep/src/c.rs | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apriv_sep/src/lib.rs | 1221++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apriv_sep/src/openbsd.rs | 1404+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 3146 insertions(+), 0 deletions(-)

diff --git a/priv_sep/.gitignore b/priv_sep/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/** diff --git a/priv_sep/Cargo.toml b/priv_sep/Cargo.toml @@ -0,0 +1,60 @@ +[package] +authors = ["Zack Newman <zack@philomathiclife.com>"] +categories = ["external-ffi-bindings", "os::unix-apis"] +description = "FFI for setresuid(2), setresgid(2), chroot(2), pledge(2), and unveil(2)." +documentation = "https://docs.rs/priv_sep/latest/priv_sep/" +edition = "2024" +keywords = ["ffi", "openbsd", "privsep", "security", "unix"] +license = "MIT OR Apache-2.0" +name = "priv_sep" +readme = "README.md" +repository = "https://git.philomathiclife.com/repos/priv_sep/" +rust-version = "1.86.0" +version = "3.0.0-alpha.1" + +[lints.rust] +unknown_lints = { level = "deny", priority = -1 } +future_incompatible = { level = "deny", priority = -1 } +let_underscore = { level = "deny", priority = -1 } +missing_docs = { level = "deny", priority = -1 } +nonstandard_style = { level = "deny", priority = -1 } +refining_impl_trait = { level = "deny", priority = -1 } +rust_2018_compatibility = { level = "deny", priority = -1 } +rust_2018_idioms = { level = "deny", priority = -1 } +rust_2021_compatibility = { level = "deny", priority = -1 } +rust_2024_compatibility = { level = "deny", priority = -1 } +unsafe_code = { level = "deny", priority = -1 } +unused = { level = "deny", priority = -1 } +warnings = { level = "deny", priority = -1 } + +[lints.clippy] +all = { level = "deny", priority = -1 } +cargo = { level = "deny", priority = -1 } +complexity = { level = "deny", priority = -1 } +correctness = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +restriction = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +# Noisy, opinionated, and likely don't prevent bugs or improve APIs. +arbitrary_source_item_ordering = "allow" +blanket_clippy_restriction_lints = "allow" +exhaustive_enums = "allow" +exhaustive_structs = "allow" +implicit_return = "allow" +min_ident_chars = "allow" +missing_trait_methods = "allow" +ref_patterns = "allow" +return_and_then = "allow" +single_call_fn = "allow" +single_char_lifetime_names = "allow" +unseparated_literal_suffix = "allow" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dev-dependencies] +tokio = { version = "1.44.2", default-features = false, features = ["macros", "net", "rt"] } diff --git a/priv_sep/LICENSE-APACHE b/priv_sep/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/priv_sep/LICENSE-MIT b/priv_sep/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright © 2024 Zack Newman + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/priv_sep/README.md b/priv_sep/README.md @@ -0,0 +1,125 @@ +# `priv_sep` + +[<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) +[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-priv_sep-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/priv_sep/latest/priv_sep/) + +`priv_sep` is a library that uses the system's libc to perform privilege separation and privilege reduction. + +## `priv_sep` in action for OpenBSD + +```rust +use core::convert::Infallible; +use priv_sep::{Permissions, PrivDropErr, Promise, Promises}; +use std::{ + fs, + io::Error, + net::{Ipv6Addr, SocketAddrV6}, +}; +use tokio::net::TcpListener; +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<Infallible, PrivDropErr<Error>> { + /// Config file. + 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`. + // Bind to TCP `[::1]:443` as root. + // `setresgid(2)` to the group ID associated with nobody. + // `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/", + [Promise::Inet, Promise::Rpath, Promise::Unveil], + false, + async || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).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)?; + // Remove file system access. + Permissions::NONE.unveil(CONFIG)?; + // Remove `rpath` and `unveil` from our `pledge(2)`d promises + // (i.e., only have `inet` and `stdio` abilities when we begin accepting TCP connections). + promises.remove_promises_then_pledge([Promise::Rpath, Promise::Unveil])?; + loop { + // Handle TCP connections. + if let Ok((_, ip)) = listener.accept().await { + assert!(ip.is_ipv6()); + } + } +} +``` + +## `priv_sep` in action for Unix-like OSes + +```rust +use core::convert::Infallible; +use priv_sep::{UserInfo, PrivDropErr}; +use std::{ + io::Error, + net::{Ipv6Addr, SocketAddrV6}, +}; +use tokio::net::TcpListener; +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<Infallible, PrivDropErr<Error>> { + // Get the user ID and group ID for nobody from `passwd(5)`. + // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. + // Bind to TCP `[::1]:443` as root. + // `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?; + // At this point, the process is running under nobody. + loop { + // Handle TCP connections. + if let Ok((_, ip)) = listener.accept().await { + assert!(ip.is_ipv6()); + } + } +} +``` + +## Minimum Supported Rust Version (MSRV) + +This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that +update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV +will be updated. + +MSRV changes will correspond to a SemVer minor version bump. + +## SemVer Policy + +* All on-by-default features of this library are covered by SemVer +* MSRV is considered exempt from SemVer as noted above + +## License + +Licensed under either of + +* Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT)) + +at your option. + +## Contribution + +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. Additionally +`RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be built. + +### Status + +This package will be actively maintained to stay in-sync with the latest version of OpenBSD. +The crate is only tested on the `x86_64-unknown-openbsd` and `x86_64-unknown-linux` targets. While OpenBSD supports +both the most recent -release/-stable release as well as the previous version, only the most recent version will +be supported by this library. If using -stable, it may be necessary to build the +[`rust` port](https://github.com/openbsd/ports/tree/master/lang/rust) from -current. diff --git a/priv_sep/src/c.rs b/priv_sep/src/c.rs @@ -0,0 +1,137 @@ +use super::{Gid, Uid, UserInfo}; +#[cfg(any(target_os = "espidf", target_os = "horizon", target_os = "vita"))] +use core::ffi::c_ushort; +use core::ffi::{c_char, c_int}; +/// Error code when a libc call is successful. +pub const SUCCESS: c_int = 0; +/// `uid_t` and `gid_t`. +#[cfg(any(target_os = "espidf", target_os = "horizon", target_os = "vita"))] +pub type IdT = c_ushort; +/// `uid_t` and `gid_t`. +#[cfg(target_os = "nto")] +pub type IdT = i32; +/// `uid_t` and `gid_t`. +#[cfg(not(any( + target_os = "espidf", + target_os = "horizon", + target_os = "nto", + target_os = "vita" +)))] +pub type IdT = u32; +/// `time_t`. +#[cfg(all( + any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ), + target_arch = "aarch64", + target_pointer_width = "32" +))] +type TimeT = i32; +/// `time_t`. +#[cfg(all( + any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ), + not(all(target_arch = "aarch64", target_pointer_width = "32")) +))] +type TimeT = i64; +/// [`passwd`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/pwd.h.html). +#[repr(C)] +pub struct Passwd { + /// Username. + name: *mut c_char, + /// User password. + pass: *mut c_char, + /// User ID. + uid: IdT, + /// Group ID. + gid: IdT, + /// Password change time. + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + 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" + ))] + class: *mut c_char, + /// User information. + gecos: *mut c_char, + /// Home directory. + dir: *mut c_char, + /// Shell program. + shell: *mut c_char, + /// Account expiration. + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + expire: TimeT, + /// Internal fields. + #[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "macos"))] + fields: c_int, +} +impl Passwd { + /// Returns `UserInfo` based on the contained user and group IDs. + pub const fn into_user_info(self) -> UserInfo { + UserInfo { + uid: Uid(self.uid), + gid: Gid(self.gid), + } + } +} +#[expect(unsafe_code, reason = "FFI requires unsafe")] +unsafe extern "C" { + /// [`getuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getuid.html). + pub safe fn getuid() -> IdT; + /// [`geteuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/geteuid.html). + pub safe fn geteuid() -> IdT; + /// [`getgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getgid.html). + pub safe fn getgid() -> IdT; + /// [`getegid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getegid.html). + pub safe fn getegid() -> IdT; + /// [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html). + pub safe fn setresuid(ruid: IdT, euid: IdT, suid: IdT) -> c_int; + /// [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html). + pub safe fn setresgid(rgid: IdT, egid: IdT, sgid: IdT) -> c_int; + /// [`chroot(2)`](https://manned.org/chroot.2). + pub fn chroot(path: *const c_char) -> c_int; + /// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). + pub fn chdir(path: *const c_char) -> c_int; + /// [`getpwnam_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwnam_r.html). + pub fn getpwnam_r( + name: *const c_char, + pwd: *mut Passwd, + buf: *mut c_char, + size: usize, + result: *mut *mut Passwd, + ) -> c_int; + /// [`getpwuid_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwuid_r.html). + pub fn getpwuid_r( + uid: IdT, + pwd: *mut Passwd, + buf: *mut c_char, + size: usize, + result: *mut *mut Passwd, + ) -> c_int; +} diff --git a/priv_sep/src/lib.rs b/priv_sep/src/lib.rs @@ -0,0 +1,1221 @@ +//! [![git]](https://git.philomathiclife.com/priv_sep/log.html)&ensp;[![crates-io]](https://crates.io/crates/priv_sep)&ensp;[![docs-rs]](crate) +//! +//! [git]: https://git.philomathiclife.com/git_badge.svg +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +//! +//! `priv_sep` is a library that uses the system's libc to perform privilege separation and privilege reduction. +//! +//! ## `priv_sep` in action for OpenBSD +//! +//! ```no_run +//! use core::convert::Infallible; +//! # #[cfg(target_os = "openbsd")] +//! use priv_sep::{Permissions, PrivDropErr, Promise, Promises}; +//! use std::{ +//! fs, +//! io::Error, +//! net::{Ipv6Addr, SocketAddrV6}, +//! }; +//! use tokio::net::TcpListener; +//! # #[cfg(not(target_os = "openbsd"))] +//! # fn main() {} +//! # #[cfg(target_os = "openbsd")] +//! #[tokio::main(flavor = "current_thread")] +//! async fn main() -> Result<Infallible, PrivDropErr<Error>> { +//! /// Config file. +//! 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`. +//! // Bind to TCP `[::1]:443` as root. +//! // `setresgid(2)` to the group ID associated with nobody. +//! // `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/", +//! [Promise::Inet, Promise::Rpath, Promise::Unveil], +//! false, +//! async || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).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)?; +//! // Remove file system access. +//! Permissions::NONE.unveil(CONFIG)?; +//! // Remove `rpath` and `unveil` from our `pledge(2)`d promises +//! // (i.e., only have `inet` and `stdio` abilities when we begin accepting TCP connections). +//! promises.remove_promises_then_pledge([Promise::Rpath, Promise::Unveil])?; +//! loop { +//! // Handle TCP connections. +//! if let Ok((_, ip)) = listener.accept().await { +//! assert!(ip.is_ipv6()); +//! } +//! } +//! } +//! ``` +//! +//! ## `priv_sep` in action for Unix-like OSes +//! +//! ```no_run +//! use core::convert::Infallible; +//! use priv_sep::{UserInfo, PrivDropErr}; +//! use std::{ +//! io::Error, +//! net::{Ipv6Addr, SocketAddrV6}, +//! }; +//! use tokio::net::TcpListener; +//! #[tokio::main(flavor = "current_thread")] +//! async fn main() -> Result<Infallible, PrivDropErr<Error>> { +//! // Get the user ID and group ID for nobody from `passwd(5)`. +//! // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. +//! // Bind to TCP `[::1]:443` as root. +//! // `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?; +//! // At this point, the process is running under nobody. +//! loop { +//! // Handle TCP connections. +//! if let Ok((_, ip)) = listener.accept().await { +//! assert!(ip.is_ipv6()); +//! } +//! } +//! } +//! ``` +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::pub_use, reason = "don't want openbsd types in a module")] +extern crate alloc; +/// C FFI. +mod c; +/// OpenBSD +#[cfg(any(doc, target_os = "openbsd"))] +mod openbsd; +use alloc::ffi::{CString, NulError}; +use c::{IdT, SUCCESS}; +use core::{ + error::Error as CoreErr, + ffi::{CStr, c_char, c_int}, + fmt::{self, Display, Formatter}, + mem::MaybeUninit, + ptr, +}; +#[cfg_attr(docsrs, doc(cfg(target_os = "openbsd")))] +#[cfg(any(doc, target_os = "openbsd"))] +pub use openbsd::{Permission, Permissions, Promise, Promises}; +use std::{io::Error, os::unix::ffi::OsStrExt as _, path::Path}; +/// [`uid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Uid(pub IdT); +impl Uid { + /// The root user ID (i.e., 0). + pub const ROOT: Self = Self(0); + /// Returns `true` iff `self` is [`Self::ROOT`]. + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::Uid; + /// assert!(Uid::ROOT.is_root()); + /// ``` + #[inline] + #[must_use] + pub const fn is_root(self) -> bool { + self.0 == Self::ROOT.0 + } + /// [`getuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getuid.html). + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::Uid; + /// assert_eq!(Uid::getuid(), 1000); + /// ``` + #[inline] + #[must_use] + pub fn getuid() -> Self { + Self(c::getuid()) + } + /// [`geteuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/geteuid.html). + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::Uid; + /// assert_eq!(Uid::geteuid(), 1000); + /// ``` + #[inline] + #[must_use] + pub fn geteuid() -> Self { + Self(c::geteuid()) + } + /// Calls [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html) + /// passing `self` for the real, effective, and saved user IDs. + /// + /// # Errors + /// + /// Errors iff `setresuid` does. + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::Uid; + /// assert!(Uid(1000).setresuid().is_ok()); + /// ``` + #[inline] + pub fn setresuid(self) -> Result<(), Error> { + if c::setresuid(self.0, self.0, self.0) == SUCCESS { + Ok(()) + } else { + Err(Error::last_os_error()) + } + } +} +impl PartialEq<&Self> for Uid { + #[inline] + fn eq(&self, other: &&Self) -> bool { + *self == **other + } +} +impl PartialEq<Uid> for &Uid { + #[inline] + fn eq(&self, other: &Uid) -> bool { + **self == *other + } +} +impl PartialEq<IdT> for Uid { + #[inline] + fn eq(&self, other: &IdT) -> bool { + self.0 == *other + } +} +impl From<Uid> for IdT { + #[inline] + fn from(value: Uid) -> Self { + value.0 + } +} +impl From<IdT> for Uid { + #[inline] + fn from(value: IdT) -> Self { + Self(value) + } +} +/// [`gid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Gid(pub IdT); +impl Gid { + /// [`getgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getgid.html). + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::Gid; + /// assert_eq!(Gid::getgid(), 1000); + /// ``` + #[inline] + #[must_use] + pub fn getgid() -> Self { + Self(c::getgid()) + } + /// [`getegid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getegid.html). + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::Gid; + /// assert_eq!(Gid::getegid(), 1000); + /// ``` + #[inline] + #[must_use] + pub fn getegid() -> Self { + Self(c::getegid()) + } + /// Calls [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html) + /// passing `self` for the real, effective, and saved group IDs. + /// + /// # Errors + /// + /// Errors iff `setresgid` does. + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::Gid; + /// assert!(Gid(1000).setresgid().is_ok()); + /// ``` + #[inline] + pub fn setresgid(self) -> Result<(), Error> { + if c::setresgid(self.0, self.0, self.0) == SUCCESS { + Ok(()) + } else { + Err(Error::last_os_error()) + } + } +} +impl PartialEq<&Self> for Gid { + #[inline] + fn eq(&self, other: &&Self) -> bool { + *self == **other + } +} +impl PartialEq<Gid> for &Gid { + #[inline] + fn eq(&self, other: &Gid) -> bool { + **self == *other + } +} +impl PartialEq<IdT> for Gid { + #[inline] + fn eq(&self, other: &IdT) -> bool { + self.0 == *other + } +} +impl From<Gid> for IdT { + #[inline] + fn from(value: Gid) -> Self { + value.0 + } +} +impl From<IdT> for Gid { + #[inline] + fn from(value: IdT) -> Self { + Self(value) + } +} +/// Error when [`CString::new`] errors or an I/O error occurs due to a libc call. +#[derive(Debug)] +pub enum NulOrIoErr { + /// Error returned from [`CString::new`]. + Nul(NulError), + /// Generic I/O error returned from a libc call. + Io(Error), +} +impl Display for NulOrIoErr { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Nul(ref err) => write!(f, "CString could not be created: {err}"), + Self::Io(ref err) => write!(f, "libc I/O error: {err}"), + } + } +} +impl CoreErr for NulOrIoErr {} +impl From<NulError> for NulOrIoErr { + #[inline] + fn from(value: NulError) -> Self { + Self::Nul(value) + } +} +impl From<Error> for NulOrIoErr { + #[inline] + fn from(value: Error) -> Self { + Self::Io(value) + } +} +/// [`chroot(2)`](https://manned.org/chroot.2). +/// +/// # Errors +/// +/// Returns [`NulError`] iff [`CString::new`] does. +/// Returns [`Error`] iff `chroot(2)` errors. +/// +/// # Examples +/// +/// ```no_run +/// assert!(priv_sep::chroot("./").is_ok()); +/// ``` +#[expect(unsafe_code, reason = "chroot(2) takes a pointer")] +#[inline] +pub fn chroot<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { + CString::new(path.as_ref().as_os_str().as_bytes()) + .map_err(NulOrIoErr::Nul) + .and_then(|c_path| { + let ptr = c_path.as_ptr(); + // SAFETY: + // `ptr` is valid and not null. + if unsafe { c::chroot(ptr) } == SUCCESS { + Ok(()) + } else { + Err(NulOrIoErr::Io(Error::last_os_error())) + } + }) +} +/// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). +/// +/// This function MUST only be called by `chdir` and `chroot_then_chdir`. +#[expect(unsafe_code, reason = "chdir(2) takes a pointer")] +fn private_chdir(path: *const c_char) -> Result<(), Error> { + // SAFETY: + // `path` is valid and not null as can be seen in the only functions that call this function: + // `chdir` and `chroot_then_chdir`. + if unsafe { c::chdir(path) } == SUCCESS { + Ok(()) + } else { + Err(Error::last_os_error()) + } +} +/// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). +/// +/// # Errors +/// +/// Returns [`NulError`] iff [`CString::new`] does. +/// Returns [`Error`] iff `chdir` errors. +/// +/// # Examples +/// +/// ```no_run +/// assert!(priv_sep::chdir("/").is_ok()); +/// ``` +#[inline] +pub fn chdir<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { + CString::new(path.as_ref().as_os_str().as_bytes()) + .map_err(NulOrIoErr::Nul) + .and_then(|c_path| private_chdir(c_path.as_ptr()).map_err(NulOrIoErr::Io)) +} +/// Calls [`chroot`] on `path` followed by a call to [`chdir`] on `"/"`. +/// +/// # Errors +/// +/// Errors iff `chroot` or `chdir` do. +/// +/// # Examples +/// +/// ```no_run +/// assert!(priv_sep::chroot_then_chdir("./").is_ok()); +/// ``` +#[inline] +pub fn chroot_then_chdir<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { + /// Root directory. + const ROOT: *const c_char = c"/".as_ptr(); + chroot(path).and_then(|()| private_chdir(ROOT).map_err(NulOrIoErr::Io)) +} +/// Error returned when dropping privileges. +#[derive(Debug)] +pub enum PrivDropErr<E> { + /// Error when [`CString::new`] errors. + Nul(NulError), + /// Error when an I/O error occurs from a libc call. + Io(Error), + /// Error when there is no entry in the user database corresponding to the passed username. + NoPasswdEntry, + /// Error when [`UserInfo::is_root`]. + RootEntry, + /// Error returned from the user-provided function that is invoked before calling [`UserInfo::setresid`]. + Other(E), +} +impl<E: Display> Display for PrivDropErr<E> { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Nul(ref err) => write!( + f, + "CString could not be created from the username to drop privileges to: {err}" + ), + Self::Io(ref err) => write!(f, "libc I/O error when dropping privileges: {err}"), + Self::NoPasswdEntry => f.write_str("no passwd(5) entry to drop privileges to"), + Self::RootEntry => f.write_str( + "setresuid(2) is not allowed to be called on uid 0 when dropping privileges", + ), + Self::Other(ref err) => write!( + f, + "error calling function before dropping privileges: {err}" + ), + } + } +} +impl<E: CoreErr> CoreErr for PrivDropErr<E> {} +impl<E> From<NulError> for PrivDropErr<E> { + #[inline] + fn from(value: NulError) -> Self { + Self::Nul(value) + } +} +impl<E> From<Error> for PrivDropErr<E> { + #[inline] + fn from(value: Error) -> Self { + Self::Io(value) + } +} +impl<E> From<NulOrIoErr> for PrivDropErr<E> { + #[inline] + fn from(value: NulOrIoErr) -> Self { + match value { + NulOrIoErr::Nul(e) => Self::Nul(e), + NulOrIoErr::Io(e) => Self::Io(e), + } + } +} +/// Error returned from [`UserInfo::setresid_if_valid`]. +#[derive(Debug)] +pub enum SetresidErr { + /// Error when an I/O error occurs from a libc call. + Io(Error), + /// Error when there is no entry in the user database corresponding to [`UserInfo::uid`]. + NoPasswdEntry, + /// Error when the entry in the user database has a different gid than [`UserInfo::gid`]. + GidMismatch, +} +impl Display for SetresidErr { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Io(ref err) => write!(f, "libc I/O error when dropping privileges: {err}"), + Self::NoPasswdEntry => f.write_str("no passwd(5) entry to drop privileges to"), + Self::GidMismatch => f.write_str("gid in passwd(5) does match the expected gid"), + } + } +} +impl CoreErr for SetresidErr {} +impl From<Error> for SetresidErr { + #[inline] + fn from(value: Error) -> Self { + Self::Io(value) + } +} +/// Used by [`UserInfo::getpw_entry`]. +trait PwEntry { + /// Calling code must uphold the following safety invariants: + /// * `buf` must be a valid, initialized, non-null pointer + /// * `size` must be the length of `buf` + /// * `result` must be a valid, initialized non-null pointer referencing a valid and initialized pointer that + /// is allowed to be null. + /// + /// Implementors MUST only _write_ to `pwd` and never read from it (i.e., `pwd` is allowed to be unitialized). + #[expect( + unsafe_code, + reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" + )] + unsafe fn getpw( + self, + pwd: *mut c::Passwd, + buf: *mut c_char, + size: usize, + result: *mut *mut c::Passwd, + ) -> c_int; +} +impl PwEntry for Uid { + #[expect(unsafe_code, reason = "getpwuid_r(3) take in pointers")] + unsafe fn getpw( + self, + pwd: *mut c::Passwd, + buf: *mut c_char, + size: usize, + result: *mut *mut c::Passwd, + ) -> c_int { + // SAFETY: + // Calling code must uphold safety invariants. + // `pwd` is never read from. + unsafe { c::getpwuid_r(self.0, pwd, buf, size, result) } + } +} +/// `newtype` around `CStr`. +#[derive(Clone, Copy)] +struct CStrWrapper<'a>(&'a CStr); +impl PwEntry for CStrWrapper<'_> { + #[expect(unsafe_code, reason = "getpwnam_r(3) takes in pointers")] + unsafe fn getpw( + self, + pwd: *mut c::Passwd, + buf: *mut c_char, + size: usize, + result: *mut *mut c::Passwd, + ) -> c_int { + let ptr = self.0.as_ptr(); + // SAFETY: + // Calling code must uphold safety invariants. + // `ptr` is valid, initialized, and not null. + // `pwd` is never read from. + unsafe { c::getpwnam_r(ptr, pwd, buf, size, result) } + } +} +/// User and group ID. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct UserInfo { + /// The user ID. + pub uid: Uid, + /// The group ID. + pub gid: Gid, +} +impl UserInfo { + /// Returns `true` iff [`Uid::is_root`]. + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::{Gid, Uid, UserInfo}; + /// assert!(UserInfo { uid: Uid::ROOT, gid: Gid(0), }.is_root()); + /// ``` + #[inline] + #[must_use] + pub const fn is_root(self) -> bool { + self.uid.is_root() + } + /// [`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 [`Error`] will + /// be returned. + /// + /// # Errors + /// + /// Returns [`NulError`] iff [`CString::new`] does. + /// Returns [`Error`] iff `getpwnam_r` errors. + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::{Uid, UserInfo}; + /// assert!(UserInfo::with_buffer("root", [0; 128].as_mut_slice())?.map_or(false, |info| info.is_root())); + /// # Ok::<_, priv_sep::NulOrIoErr>(()) + /// ``` + #[expect(unsafe_code, reason = "getpwnam_r(3) takes in pointers")] + #[inline] + pub fn with_buffer<T: Into<Vec<u8>>>( + name: T, + buffer: &mut [c_char], + ) -> Result<Option<Self>, NulOrIoErr> { + CString::new(name).map_err(NulOrIoErr::Nul).and_then(|n| { + let ptr = n.as_ptr(); + let mut pwd = MaybeUninit::<c::Passwd>::uninit(); + let pwd_ptr = pwd.as_mut_ptr(); + let buf_ptr = buffer.as_mut_ptr(); + let len = buffer.len(); + let mut result = ptr::null_mut(); + let res_ptr = &mut result; + // SAFETY: + // `pwd_ptr` is only written to; thus the fact `pwd` is unitialized is fine. + // `buf_ptr` is valid, initialized, and not null. + // `len` is the length of `buf_ptr`. + // `res_ptr` is valid, initialized, and not null. + // `result` is valid, initialized, and allowed to be null. + let code = unsafe { c::getpwnam_r(ptr, pwd_ptr, buf_ptr, len, res_ptr) }; + if code == SUCCESS { + if result.is_null() { + Ok(None) + } else { + // SAFETY: + // `c::getpwnam_r` writes to `pwd` iff `result` is not null. + Ok(Some(unsafe { pwd.assume_init() }.into_user_info())) + } + } else { + Err(NulOrIoErr::Io(Error::from_raw_os_error(code))) + } + }) + } + /// Helper for [`Self::new`] and [`Self::setresid_if_exists`]. + #[expect( + unsafe_code, + reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" + )] + fn getpw_entry<P: Copy + PwEntry>(u: P) -> Result<Option<Self>, Error> { + /// Initial buffer size. + const INIT_CAP: usize = 128; + // `2 * (MAX_CAP - 1) <= isize::MAX` MUST be true. + /// Maximum buffer size. + const MAX_CAP: usize = 0x4000; + /// [`ERANGE`](https://man.openbsd.org/errno#Result). + const ERANGE: c_int = 34; + let mut buffer = Vec::with_capacity(INIT_CAP); + let mut cap = buffer.capacity(); + let mut pwd = MaybeUninit::<c::Passwd>::uninit(); + let mut result = ptr::null_mut(); + let mut pwd_ptr; + let mut res_ptr; + let mut code; + let mut buf_ptr; + loop { + pwd_ptr = pwd.as_mut_ptr(); + res_ptr = &mut result; + buf_ptr = buffer.as_mut_ptr(); + // SAFETY: + // `pwd_ptr` is only written to; thus the fact `pwd` is unitialized is fine. + // `buf_ptr` is valid, initialized, and not null. + // `cap` is the length of `buf_ptr`. + // `res_ptr` is valid, initialized, and not null. + // `result` is valid, initialized, and allowed to be null. + code = unsafe { u.getpw(pwd_ptr, buf_ptr, cap, res_ptr) }; + if code == SUCCESS { + return Ok(if result.is_null() { + None + } else { + // SAFETY: + // `CStrWrapper::getpw` writes to `pwd` iff `result` is not null. + Some(unsafe { pwd.assume_init() }.into_user_info()) + }); + } else if code == ERANGE { + if cap >= MAX_CAP { + return Err(Error::from_raw_os_error(code)); + } + // `cap < MAX_CAP` and + // `2 * (MAX_CAP - 1) < isize::MAX`, so overflow is not possible. + buffer.reserve(cap << 1); + cap = buffer.capacity(); + } else { + return Err(Error::from_raw_os_error(code)); + } + } + } + /// Same as [`Self::with_buffer`] except repeated attempts are made with progressively larger buffers up to + /// 16 KiB. + /// + /// # Errors + /// + /// Errors iff [`Self::with_buffer`] does for a 16 KiB buffer. + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::UserInfo; + /// assert!(UserInfo::new("root")?.map_or(false, |info| info.is_root())); + /// # Ok::<_, priv_sep::NulOrIoErr>(()) + /// ``` + #[inline] + pub fn new<T: Into<Vec<u8>>>(name: T) -> Result<Option<Self>, NulOrIoErr> { + CString::new(name) + .map_err(NulOrIoErr::Nul) + .and_then(|n| Self::getpw_entry(CStrWrapper(n.as_c_str())).map_err(NulOrIoErr::Io)) + } + /// Calls [`Gid::setresgid`] and [`Uid::setresuid`]. + /// + /// # Errors + /// + /// Errors iff `Gid::setresgid` or `Uid::setresuid` error. + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::UserInfo; + /// if let Some(user) = UserInfo::new("nobody")? { + /// user.setresid()?; + /// } + /// # Ok::<_, priv_sep::NulOrIoErr>(()) + /// ``` + #[inline] + pub fn setresid(self) -> Result<(), Error> { + self.gid.setresgid().and_then(|()| self.uid.setresuid()) + } + /// Same as [`Self::setresid`] except + /// [`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`]. + /// + /// Like [`Self::new`], this will fail if the buffer needed exceeds 16 KiB. + /// + /// # Errors + /// + /// Errors iff `getpwuid_r` errors for a 16 KiB buffer, [`Self::uid`] and [`Self::gid`] don't exist in the user + /// database, [`Gid::setresgid`] errors, or [`Uid::setresuid`] errors. + /// + /// # Examples + /// + /// ```no_run + /// # use priv_sep::{Gid, Uid, UserInfo}; + /// UserInfo { uid: Uid(1000), gid: Gid(1000), }.setresid_if_valid()?; + /// # Ok::<_, priv_sep::SetresidErr>(()) + /// ``` + #[inline] + pub fn setresid_if_valid(self) -> Result<(), SetresidErr> { + Self::getpw_entry(self.uid) + .map_err(SetresidErr::Io) + .and_then(|opt| { + opt.ok_or(SetresidErr::NoPasswdEntry).and_then(|info| { + if info.gid == self.gid { + self.setresid().map_err(SetresidErr::Io) + } else { + Err(SetresidErr::GidMismatch) + } + }) + }) + } + /// Calls [`Self::new`], invokes `f`, then calls [`Self::setresid`]. + /// + /// Dropping privileges is necessary when needing to perform certain actions as root before no longer needing + /// such abilities; at which point, one calls + /// [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html) and + /// [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html) + /// using a lesser privileged gid and uid. + /// + /// # Errors + /// + /// Errors iff [`Self::new`], `f`, or [`Self::setresid`] do or there is no entry in the user database + /// corresponding to `name` or the entry has uid 0. + /// + /// # Examples + /// + /// ```no_run + /// # use core::net::{Ipv6Addr, SocketAddrV6}; + /// # use priv_sep::{PrivDropErr, UserInfo}; + /// # use std::{io::Error, net::TcpListener}; + /// let listener = UserInfo::priv_drop("nobody", || { + /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) + /// })?; + /// # Ok::<_, PrivDropErr<Error>>(()) + /// ``` + #[inline] + pub fn priv_drop<U: Into<Vec<u8>>, T, E, F: FnOnce() -> Result<T, E>>( + name: U, + f: F, + ) -> Result<T, PrivDropErr<E>> { + Self::new(name).map_err(PrivDropErr::from).and_then(|opt| { + opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) + .and_then(|info| { + if info.is_root() { + Err(PrivDropErr::RootEntry) + } else { + f().map_err(PrivDropErr::Other) + .and_then(|res| info.setresid().map_err(PrivDropErr::Io).map(|()| res)) + } + }) + }) + } + /// Same as [`Self::priv_drop`] except `f` is `async`. + /// + /// # Errors + /// + /// Read [`Self::priv_drop`]. + /// + /// # Examples + /// + /// ```no_run + /// # use core::net::{Ipv6Addr, SocketAddrV6}; + /// # use priv_sep::UserInfo; + /// # use tokio::net::TcpListener; + /// let listener_fut = UserInfo::priv_drop_async("nobody", async || { + /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await + /// }); + /// ``` + #[inline] + pub async fn priv_drop_async<U: Into<Vec<u8>>, T, E, F: AsyncFnOnce() -> Result<T, E>>( + name: U, + f: F, + ) -> Result<T, PrivDropErr<E>> { + match Self::new(name) { + Ok(opt) => match opt { + None => Err(PrivDropErr::NoPasswdEntry), + Some(info) => { + if info.is_root() { + Err(PrivDropErr::RootEntry) + } else { + f().await + .map_err(PrivDropErr::Other) + .and_then(|res| info.setresid().map_err(PrivDropErr::Io).map(|()| res)) + } + } + }, + Err(err) => Err(PrivDropErr::from(err)), + } + } + /// Same as [`Self::priv_drop`] except [`chroot_then_chdir`] is called before or after invoking `f` based on + /// `chroot_after_f`. + /// + /// # Errors + /// + /// Errors iff [`Self::priv_drop`] or [`chroot_then_chdir`] do. + /// + /// # Examples + /// + /// ```no_run + /// # use core::net::{Ipv6Addr, SocketAddrV6}; + /// # use priv_sep::{PrivDropErr, UserInfo}; + /// # use std::{io::Error, net::TcpListener}; + /// let listener = UserInfo::chroot_then_priv_drop("nobody", "./", false, || { + /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) + /// })?; + /// # Ok::<_, PrivDropErr<Error>>(()) + /// ``` + #[inline] + pub fn chroot_then_priv_drop< + U: Into<Vec<u8>>, + P: AsRef<Path>, + T, + E, + F: FnOnce() -> Result<T, E>, + >( + name: U, + path: P, + chroot_after_f: bool, + f: F, + ) -> Result<T, PrivDropErr<E>> { + Self::new(name).map_err(PrivDropErr::from).and_then(|opt| { + opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) + .and_then(|info| { + if info.is_root() { + Err(PrivDropErr::RootEntry) + } else if chroot_after_f { + f().map_err(PrivDropErr::Other).and_then(|res| { + chroot_then_chdir(path) + .map_err(PrivDropErr::from) + .map(|()| res) + }) + } else { + chroot_then_chdir(path) + .map_err(PrivDropErr::from) + .and_then(|()| f().map_err(PrivDropErr::Other)) + } + .and_then(|res| info.setresid().map_err(PrivDropErr::Io).map(|()| res)) + }) + }) + } + /// Same as [`Self::chroot_then_priv_drop`] except `f` is `async`. + /// + /// # Errors + /// + /// Read [`Self::chroot_then_priv_drop`]. + /// + /// # Examples + /// + /// ```no_run + /// # use core::net::{Ipv6Addr, SocketAddrV6}; + /// # use priv_sep::UserInfo; + /// # use tokio::net::TcpListener; + /// let listener_fut = UserInfo::chroot_then_priv_drop_async("nobody", "./", false, async || { + /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await + /// }); + /// ``` + #[inline] + pub async fn chroot_then_priv_drop_async< + U: Into<Vec<u8>>, + P: AsRef<Path>, + T, + E, + F: AsyncFnOnce() -> Result<T, E>, + >( + name: U, + path: P, + chroot_after_f: bool, + f: F, + ) -> Result<T, PrivDropErr<E>> { + match Self::new(name) { + Ok(opt) => match opt { + None => Err(PrivDropErr::NoPasswdEntry), + Some(info) => if info.is_root() { + Err(PrivDropErr::RootEntry) + } else if chroot_after_f { + f().await.map_err(PrivDropErr::Other).and_then(|res| { + chroot_then_chdir(path) + .map_err(PrivDropErr::from) + .map(|()| res) + }) + } else { + match chroot_then_chdir(path) { + Ok(()) => f().await.map_err(PrivDropErr::Other), + Err(err) => Err(PrivDropErr::from(err)), + } + } + .and_then(|res| info.setresid().map_err(PrivDropErr::Io).map(|()| res)), + }, + Err(err) => Err(PrivDropErr::from(err)), + } + } +} +impl PartialEq<&Self> for UserInfo { + #[inline] + fn eq(&self, other: &&Self) -> bool { + *self == **other + } +} +impl PartialEq<UserInfo> for &UserInfo { + #[inline] + fn eq(&self, other: &UserInfo) -> bool { + **self == *other + } +} +#[cfg(test)] +mod tests { + use super::{Gid, NulOrIoErr, PrivDropErr, SetresidErr, Uid, UserInfo}; + #[cfg(target_os = "openbsd")] + use super::{Permissions, Promise, Promises}; + use core::net::{Ipv6Addr, SocketAddrV6}; + use std::{fs, io::Error, net::TcpListener}; + const README: &str = "README.md"; + #[test] + fn test_getuid() { + _ = Uid::getuid(); + } + #[test] + fn test_geteuid() { + _ = Uid::geteuid(); + } + #[test] + fn test_getgid() { + _ = Gid::getgid(); + } + #[test] + fn test_getegid() { + _ = Gid::getegid(); + } + #[test] + fn test_setresuid() -> Result<(), Error> { + Uid::geteuid().setresuid() + } + #[test] + fn test_setresgid() -> Result<(), Error> { + Gid::getegid().setresgid() + } + #[test] + fn test_user_info_new() -> Result<(), NulOrIoErr> { + if let Some(user) = UserInfo::new("root")? { + assert!(user.is_root()); + } + Ok(()) + } + #[test] + fn test_user_info_with_buffer() -> Result<(), NulOrIoErr> { + if let Some(user) = UserInfo::with_buffer("root", [0; 512].as_mut_slice())? { + assert!(user.is_root()); + } + Ok(()) + } + #[test] + fn test_user_info_setresid() -> Result<(), Error> { + UserInfo { + uid: Uid::geteuid(), + gid: Gid::getegid(), + } + .setresid() + } + #[test] + fn test_user_info_setresid_if_exists() -> Result<(), SetresidErr> { + UserInfo { + uid: Uid::geteuid(), + gid: Gid::getegid(), + } + .setresid_if_valid() + } + #[test] + fn test_user_info_setresid_if_exists_failure() { + assert!( + UserInfo { + uid: Uid::geteuid(), + gid: Gid(u32::MAX), + } + .setresid_if_valid() + .map_or_else(|e| matches!(e, SetresidErr::GidMismatch), |_| false) + ); + } + #[test] + #[ignore] + fn test_priv_drop() -> Result<(), PrivDropErr<Error>> { + if Uid::geteuid().is_root() { + UserInfo::priv_drop("zack", || { + TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) + }) + .map(|_| { + assert!( + TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)).is_err() + ); + }) + } else { + assert!( + UserInfo::priv_drop("root", || Ok::<_, Error>(())) + .map_or_else(|e| matches!(e, PrivDropErr::RootEntry), |_| false) + ); + Ok(()) + } + } + #[test] + #[ignore] + fn test_chroot_priv_drop() -> Result<(), PrivDropErr<Error>> { + if Uid::geteuid().is_root() { + UserInfo::chroot_then_priv_drop("zack", "./", false, || { + TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) + }) + .and_then(|_| { + fs::exists(README).map_err(PrivDropErr::Io).map(|exists| { + assert!(exists); + assert!( + TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)) + .is_err() + ); + }) + }) + } else { + Ok(()) + } + } + #[cfg(target_os = "openbsd")] + #[test] + #[ignore] + fn test_pledge_unveil() { + const FILE_EXISTS: &str = "/home/zack/foo.txt"; + _ = fs::metadata(FILE_EXISTS) + .expect(format!("{FILE_EXISTS} does not exist, so unit testing cannot occur").as_str()); + const FILE_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23"; + drop(fs::metadata(FILE_NOT_EXISTS).expect_err( + format!("{FILE_NOT_EXISTS} exists, so unit testing cannot occur").as_str(), + )); + const DIR_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23/"; + 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!(Promises::pledge_none().is_ok()); + print!(""); + assert!(Promises::ALL.pledge().is_ok()); + // This tests that duplicates are ignored as well as the implementation of PartialEq. + let mut initial_promises = Promises::new([ + Promise::Stdio, + Promise::Unveil, + Promise::Rpath, + Promise::Stdio, + ]); + assert!(initial_promises.len() == 3); + assert!( + 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()); + // This tests unveil with no permissions. + assert!(Permissions::NONE.unveil(FILE_EXISTS).is_ok()); + assert!(fs::metadata(FILE_EXISTS).is_err()); + // 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).is_ok()); + // This tests that calls to unveil on missing files don't error. + assert!(Permissions::NONE.unveil(FILE_NOT_EXISTS).is_ok()); + // This tests that calls to unveil on missing directories error. + assert!(Permissions::NONE.unveil(DIR_NOT_EXISTS).is_err()); + // 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).is_ok()); + // The below tests that Promises can only be removed and not added. + initial_promises.remove_promises([Promise::Unveil]); + assert_eq!(initial_promises.len(), 2); + initial_promises.remove(Promise::Rpath); + 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()); + // 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)); + } + #[cfg(target_os = "openbsd")] + #[test] + #[ignore] + fn test_pledge_priv_drop() -> Result<(), PrivDropErr<Error>> { + if Uid::geteuid().is_root() { + Promises::new_priv_drop( + "zack", + [Promise::Inet, Promise::Rpath, Promise::Unveil], + false, + || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)), + ) + .and_then(|(_, mut promises)| { + Permissions::READ + .unveil(README) + .map_err(PrivDropErr::from) + .and_then(|()| { + fs::exists(README) + .map_err(PrivDropErr::Io) + .and_then(|exists| { + Permissions::NONE + .unveil(README) + .map_err(PrivDropErr::from) + .and_then(|()| { + promises + .remove_promises_then_pledge([ + Promise::Rpath, + Promise::Unveil, + ]) + .map_err(PrivDropErr::Io) + .map(|()| { + assert!(exists); + assert!( + TcpListener::bind(SocketAddrV6::new( + Ipv6Addr::LOCALHOST, + 80, + 0, + 0 + )) + .is_err() + ); + }) + }) + }) + }) + }) + } else { + Ok(()) + } + } + #[cfg(target_os = "openbsd")] + #[test] + #[ignore] + fn test_pledge_chroot_priv_drop() -> Result<(), PrivDropErr<Error>> { + if Uid::geteuid().is_root() { + Promises::new_chroot_then_priv_drop( + "zack", + "./", + [Promise::Inet, Promise::Rpath, Promise::Unveil], + false, + || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)), + ) + .and_then(|(_, mut promises)| { + Permissions::READ + .unveil(README) + .map_err(PrivDropErr::from) + .and_then(|()| { + fs::exists(README) + .map_err(PrivDropErr::Io) + .and_then(|exists| { + Permissions::NONE + .unveil(README) + .map_err(PrivDropErr::from) + .and_then(|()| { + promises + .remove_promises_then_pledge([ + Promise::Rpath, + Promise::Unveil, + ]) + .map_err(PrivDropErr::Io) + .map(|()| { + assert!(exists); + assert!( + TcpListener::bind(SocketAddrV6::new( + Ipv6Addr::LOCALHOST, + 80, + 0, + 0 + )) + .is_err() + ); + }) + }) + }) + }) + }) + } else { + Ok(()) + } + } +} diff --git a/priv_sep/src/openbsd.rs b/priv_sep/src/openbsd.rs @@ -0,0 +1,1404 @@ +#[cfg(doc)] +use super::chroot_then_chdir; +use super::{NulOrIoErr, PrivDropErr, SUCCESS, UserInfo}; +use Promise::{ + Audio, Bpf, Chown, Cpath, Disklabel, Dns, Dpath, Drm, Exec, Fattr, Flock, Getpw, Id, Inet, + Mcast, Pf, Proc, ProtExec, Ps, Recvfd, Route, Rpath, Sendfd, Settime, Stdio, Tape, Tmppath, + Tty, Unix, Unveil, Video, Vminfo, Vmm, Wpath, Wroute, +}; +use alloc::ffi::CString; +#[cfg(doc)] +use alloc::ffi::NulError; +use core::{ + convert::AsRef, + ffi::{c_char, c_int}, + fmt::{self, Display, Formatter}, + ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}, + ptr, +}; +use std::{io::Error, os::unix::ffi::OsStrExt as _, path::Path}; +#[expect(unsafe_code, reason = "FFI requires unsafe")] +unsafe extern "C" { + /// [`pledge(2)`](https://man.openbsd.org/pledge.2). + pub fn pledge(promises: *const c_char, execpromises: *const c_char) -> c_int; + /// [`unveil(2)`](https://man.openbsd.org/unveil.2). + pub fn unveil(path: *const c_char, permissions: *const c_char) -> c_int; +} +/// A `promise` to [`pledge(2)`](https://man.openbsd.org/pledge.2). +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[non_exhaustive] +pub enum Promise { + /// [`audio`](https://man.openbsd.org/pledge.2#audio). + Audio, + /// [`bpf`](https://man.openbsd.org/pledge.2#bpf). + Bpf, + /// [`chown`](https://man.openbsd.org/pledge.2#chown). + Chown, + /// [`cpath`](https://man.openbsd.org/pledge.2#cpath). + Cpath, + /// `disklabel`. + Disklabel, + /// [`dns`](https://man.openbsd.org/pledge.2#dns). + Dns, + /// [`dpath`](https://man.openbsd.org/pledge.2#dpath). + Dpath, + /// `drm`. + Drm, + /// [`error`](https://man.openbsd.org/pledge.2#error). + Error, + /// [`exec`](https://man.openbsd.org/pledge.2#exec). + Exec, + /// [`fattr`](https://man.openbsd.org/pledge.2#fattr). + Fattr, + /// [`flock`](https://man.openbsd.org/pledge.2#flock). + Flock, + /// [`getpw`](https://man.openbsd.org/pledge.2#getpw). + Getpw, + /// [`id`](https://man.openbsd.org/pledge.2#id). + Id, + /// [`inet`](https://man.openbsd.org/pledge.2#inet). + Inet, + /// [`mcast`](https://man.openbsd.org/pledge.2#mcast). + Mcast, + /// [`pf`](https://man.openbsd.org/pledge.2#pf). + Pf, + /// [`proc`](https://man.openbsd.org/pledge.2#proc). + Proc, + /// [`prot_exec`](https://man.openbsd.org/pledge.2#prot_exec). + ProtExec, + /// [`ps`](https://man.openbsd.org/pledge.2#ps). + Ps, + /// [`recvfd`](https://man.openbsd.org/pledge.2#recvfd). + Recvfd, + /// [`route`](https://man.openbsd.org/pledge.2#route). + Route, + /// [`rpath`](https://man.openbsd.org/pledge.2#rpath). + Rpath, + /// [`sendfd`](https://man.openbsd.org/pledge.2#sendfd). + Sendfd, + /// [`settime`](https://man.openbsd.org/pledge.2#settime). + Settime, + /// [`stdio`](https://man.openbsd.org/pledge.2#stdio). + Stdio, + /// [`tape`](https://man.openbsd.org/pledge.2#tape). + Tape, + /// [`tmppath`](https://man.openbsd.org/pledge.2#tmppath). + Tmppath, + /// [`tty`](https://man.openbsd.org/pledge.2#tty). + Tty, + /// [`unix`](https://man.openbsd.org/pledge.2#unix). + Unix, + /// [`unveil`](https://man.openbsd.org/pledge.2#unveil). + Unveil, + /// [`video`](https://man.openbsd.org/pledge.2#video). + Video, + /// [`vminfo`](https://man.openbsd.org/pledge.2#vminfo). + Vminfo, + /// `vmm`. + Vmm, + /// [`wpath`](https://man.openbsd.org/pledge.2#wpath). + Wpath, + /// [`wroute`](https://man.openbsd.org/pledge.2#wroute). + Wroute, +} +impl Promise { + /// Returns `self` as a `u64`. + const fn to_u64(self) -> u64 { + match self { + Audio => 0x1, + Bpf => 0x2, + Chown => 0x4, + Cpath => 0x8, + Disklabel => 0x10, + Dns => 0x20, + Dpath => 0x40, + Drm => 0x80, + Self::Error => 0x100, + Exec => 0x200, + Fattr => 0x400, + Flock => 0x800, + Getpw => 0x1000, + Id => 0x2000, + Inet => 0x4000, + Mcast => 0x8000, + Pf => 0x10000, + Proc => 0x20000, + ProtExec => 0x40000, + Ps => 0x80000, + Recvfd => 0x0010_0000, + Route => 0x0020_0000, + Rpath => 0x0040_0000, + Sendfd => 0x0080_0000, + Settime => 0x0100_0000, + Stdio => 0x0200_0000, + Tape => 0x0400_0000, + Tmppath => 0x0800_0000, + Tty => 0x1000_0000, + Unix => 0x2000_0000, + Unveil => 0x4000_0000, + Video => 0x8000_0000, + Vminfo => 0x0001_0000_0000, + Vmm => 0x0002_0000_0000, + Wpath => 0x0004_0000_0000, + Wroute => 0x0008_0000_0000, + } + } +} +impl Display for Promise { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Audio => f.write_str("pledge(2) 'audio' promise"), + Bpf => f.write_str("pledge(2) 'bpf' promise"), + Chown => f.write_str("pledge(2) 'chown' promise"), + Cpath => f.write_str("pledge(2) 'cpath' promise"), + Disklabel => f.write_str("pledge(2) 'disklabel' promise"), + Dns => f.write_str("pledge(2) 'dns' promise"), + Dpath => f.write_str("pledge(2) 'dpath' promise"), + Drm => f.write_str("pledge(2) 'drm' promise"), + Self::Error => f.write_str("pledge(2) 'error' promise"), + Exec => f.write_str("pledge(2) 'exec' promise"), + Fattr => f.write_str("pledge(2) 'fattr' promise"), + Flock => f.write_str("pledge(2) 'flock' promise"), + Getpw => f.write_str("pledge(2) 'getpw' promise"), + Id => f.write_str("pledge(2) 'id' promise"), + Inet => f.write_str("pledge(2) 'inet' promise"), + Mcast => f.write_str("pledge(2) 'mcast' promise"), + Pf => f.write_str("pledge(2) 'pf' promise"), + Proc => f.write_str("pledge(2) 'proc' promise"), + ProtExec => f.write_str("pledge(2) 'prot_exec' promise"), + Ps => f.write_str("pledge(2) 'ps' promise"), + Recvfd => f.write_str("pledge(2) 'recvfd' promise"), + Route => f.write_str("pledge(2) 'route' promise"), + Rpath => f.write_str("pledge(2) 'rpath' promise"), + Sendfd => f.write_str("pledge(2) 'sendfd' promise"), + Settime => f.write_str("pledge(2) 'settime' promise"), + Stdio => f.write_str("pledge(2) 'stdio' promise"), + Tape => f.write_str("pledge(2) 'tape' promise"), + Tmppath => f.write_str("pledge(2) 'tmppath' promise"), + Tty => f.write_str("pledge(2) 'tty' promise"), + Unix => f.write_str("pledge(2) 'unix' promise"), + Unveil => f.write_str("pledge(2) 'unveil' promise"), + Video => f.write_str("pledge(2) 'video' promise"), + Vminfo => f.write_str("pledge(2) 'vminfo' promise"), + Vmm => f.write_str("pledge(2) 'vmm' promise"), + Wpath => f.write_str("pledge(2) 'wpath' promise"), + Wroute => f.write_str("pledge(2) 'wroute' promise"), + } + } +} +impl PartialEq<&Self> for Promise { + #[inline] + fn eq(&self, other: &&Self) -> bool { + *self == **other + } +} +impl PartialEq<Promise> for &Promise { + #[inline] + fn eq(&self, other: &Promise) -> bool { + **self == *other + } +} +/// A set of [`Promise`]s that can only have `Promise`s removed after creation. +/// +/// Once a set of `promises` has been [`pledge(2)`](https://man.openbsd.org/pledge.2)d, +/// only a subset of those `promises` can be `pledge(2)`d again; as a result, this type can be used +/// to ensure that `Promise`s are never added but only removed from an initial set. +#[derive(Debug, Eq, PartialEq)] +pub struct Promises(u64); +impl Promises { + /// Empty `Promises`. + pub const NONE: Self = Self(0); + /// `Promises` containing all [`Promise`]s. + pub const ALL: Self = Self::NONE + .add(Promise::Audio) + .add(Promise::Bpf) + .add(Promise::Chown) + .add(Promise::Cpath) + .add(Promise::Disklabel) + .add(Promise::Dns) + .add(Promise::Dpath) + .add(Promise::Drm) + .add(Promise::Error) + .add(Promise::Exec) + .add(Promise::Fattr) + .add(Promise::Flock) + .add(Promise::Getpw) + .add(Promise::Id) + .add(Promise::Inet) + .add(Promise::Mcast) + .add(Promise::Pf) + .add(Promise::Proc) + .add(Promise::ProtExec) + .add(Promise::Ps) + .add(Promise::Recvfd) + .add(Promise::Route) + .add(Promise::Rpath) + .add(Promise::Sendfd) + .add(Promise::Settime) + .add(Promise::Stdio) + .add(Promise::Tape) + .add(Promise::Tmppath) + .add(Promise::Tty) + .add(Promise::Unix) + .add(Promise::Unveil) + .add(Promise::Video) + .add(Promise::Vminfo) + .add(Promise::Vmm) + .add(Promise::Wpath) + .add(Promise::Wroute); + /// Returns a `Promises` containing all `Promise`s in `self` and `promise`. + const fn add(self, promise: Promise) -> Self { + Self(self.0 | promise.to_u64()) + } + /// Returns a `Promises` containing the unique `Promise`s in `initial`. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// assert_eq!(Promises::new([Promise::Stdio]).len(), 1); + /// ``` + #[inline] + #[must_use] + pub fn new<P: AsRef<[Promise]>>(initial: P) -> Self { + initial + .as_ref() + .iter() + .fold(Self::NONE, |val, promise| val.add(*promise)) + } + /// Same as [`UserInfo::priv_drop`] except [`Self::pledge`] is called between [`UserInfo::new`] and + /// the invocation of `f`. + /// + /// Note [`Promise::Id`] and [`Promise::Stdio`] are automatically added to `initial`. `retain_id_promise` + /// dictates if [`Promise::Id`] should subsequently be retained after [`UserInfo::setresid`]. Callers must + /// ensure `initial` contains any necessary [`Promise`]s needed for `f`. + /// + /// # Errors + /// + /// Errors iff [`UserInfo::priv_drop`] or [`Self::pledge`] error. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # use core::net::{Ipv6Addr, SocketAddrV6}; + /// # use priv_sep::PrivDropErr; + /// # use std::{io::Error, net::TcpListener}; + /// # #[cfg(target_os = "openbsd")] + /// let (listener, promises) = Promises::new_priv_drop("nobody", [Promise::Inet], false, || { + /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) + /// })?; + /// # Ok::<_, PrivDropErr<Error>>(()) + /// ``` + #[inline] + pub fn new_priv_drop< + U: Into<Vec<u8>>, + Prom: AsRef<[Promise]>, + T, + E, + F: FnOnce() -> Result<T, E>, + >( + name: U, + initial: Prom, + retain_id_promise: bool, + f: F, + ) -> Result<(T, Self), PrivDropErr<E>> { + let mut promises = initial.as_ref().iter().fold( + Self::NONE.add(Promise::Id).add(Promise::Stdio), + |val, promise| val.add(*promise), + ); + UserInfo::new(name) + .map_err(PrivDropErr::from) + .and_then(|opt| { + opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) + .and_then(|info| { + if info.is_root() { + Err(PrivDropErr::RootEntry) + } else { + promises.pledge().map_err(PrivDropErr::Io).and_then(|()| { + f().map_err(PrivDropErr::Other).and_then(|res| { + info.setresid().map_err(PrivDropErr::Io).and_then(|()| { + if retain_id_promise { + Ok(()) + } else { + promises + .remove_then_pledge(Promise::Id) + .map_err(PrivDropErr::Io) + } + .map(|()| (res, promises)) + }) + }) + }) + } + }) + }) + } + /// Same as [`Self::new_priv_drop`] except `f` is `async`. + /// + /// # Errors + /// + /// Read [`Self::new_priv_drop`]. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # use core::net::{Ipv6Addr, SocketAddrV6}; + /// # use tokio::net::TcpListener; + /// # #[cfg(target_os = "openbsd")] + /// let listener_fut = Promises::new_priv_drop_async("nobody", [Promise::Inet], false, async || { + /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await + /// }); + /// ``` + #[inline] + pub async fn new_priv_drop_async< + U: Into<Vec<u8>>, + Prom: AsRef<[Promise]>, + T, + E, + F: AsyncFnOnce() -> Result<T, E>, + >( + name: U, + initial: Prom, + retain_id_promise: bool, + f: F, + ) -> Result<(T, Self), PrivDropErr<E>> { + let mut promises = initial.as_ref().iter().fold( + Self::NONE.add(Promise::Id).add(Promise::Stdio), + |val, promise| val.add(*promise), + ); + match UserInfo::new(name) { + Ok(opt) => match opt { + None => Err(PrivDropErr::NoPasswdEntry), + Some(info) => { + if info.is_root() { + Err(PrivDropErr::RootEntry) + } else { + match promises.pledge() { + Ok(()) => f().await.map_err(PrivDropErr::Other).and_then(|res| { + info.setresid().map_err(PrivDropErr::Io).and_then(|()| { + if retain_id_promise { + Ok(()) + } else { + promises + .remove_then_pledge(Promise::Id) + .map_err(PrivDropErr::Io) + } + .map(|()| (res, promises)) + }) + }), + Err(err) => Err(PrivDropErr::Io(err)), + } + } + } + }, + Err(err) => Err(PrivDropErr::from(err)), + } + } + /// Same as [`Self::new_priv_drop`] except [`chroot_then_chdir`] is called before [`Self::pledge`]ing `initial`. + /// + /// # Errors + /// + /// Errors iff [`Self::new_priv_drop`] or [`chroot_then_chdir`] do. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # use core::net::{Ipv6Addr, SocketAddrV6}; + /// # use priv_sep::PrivDropErr; + /// # use std::{io::Error, net::TcpListener}; + /// # #[cfg(target_os = "openbsd")] + /// let (listener, promises) = Promises::new_chroot_then_priv_drop("nobody", "./", [Promise::Inet], false, || { + /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) + /// })?; + /// # Ok::<_, PrivDropErr<Error>>(()) + /// ``` + #[inline] + pub fn new_chroot_then_priv_drop< + U: Into<Vec<u8>>, + P: AsRef<Path>, + Prom: AsRef<[Promise]>, + T, + E, + F: FnOnce() -> Result<T, E>, + >( + name: U, + path: P, + initial: Prom, + retain_id_promise: bool, + f: F, + ) -> Result<(T, Self), PrivDropErr<E>> { + let mut promises = initial.as_ref().iter().fold( + Self::NONE.add(Promise::Id).add(Promise::Stdio), + |val, promise| val.add(*promise), + ); + UserInfo::new(name) + .map_err(PrivDropErr::from) + .and_then(|opt| { + opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) + .and_then(|info| { + if info.is_root() { + Err(PrivDropErr::RootEntry) + } else { + super::chroot_then_chdir(path) + .map_err(PrivDropErr::from) + .and_then(|()| { + promises.pledge().map_err(PrivDropErr::Io).and_then(|()| { + f().map_err(PrivDropErr::Other).and_then(|res| { + info.setresid().map_err(PrivDropErr::Io).and_then( + |()| { + if retain_id_promise { + Ok(()) + } else { + promises + .remove_then_pledge(Promise::Id) + .map_err(PrivDropErr::Io) + } + .map(|()| (res, promises)) + }, + ) + }) + }) + }) + } + }) + }) + } + /// Same as [`Self::new_chroot_then_priv_drop`] except `f` is `async`. + /// + /// # Errors + /// + /// Read [`Self::new_chroot_then_priv_drop`]. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # use core::net::{Ipv6Addr, SocketAddrV6}; + /// # use tokio::net::TcpListener; + /// # #[cfg(target_os = "openbsd")] + /// let listener_fut = Promises::new_chroot_then_priv_drop_async("nobody", "./", [Promise::Inet], false, async || { + /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await + /// }); + /// ``` + #[inline] + pub async fn new_chroot_then_priv_drop_async< + U: Into<Vec<u8>>, + P: AsRef<Path>, + Prom: AsRef<[Promise]>, + T, + E, + F: AsyncFnOnce() -> Result<T, E>, + >( + name: U, + path: P, + initial: Prom, + retain_id_promise: bool, + f: F, + ) -> Result<(T, Self), PrivDropErr<E>> { + let mut promises = initial.as_ref().iter().fold( + Self::NONE.add(Promise::Id).add(Promise::Stdio), + |val, promise| val.add(*promise), + ); + match UserInfo::new(name) { + Ok(opt) => match opt { + None => Err(PrivDropErr::NoPasswdEntry), + Some(info) => { + if info.is_root() { + Err(PrivDropErr::RootEntry) + } else { + match super::chroot_then_chdir(path) { + Ok(()) => match promises.pledge() { + Ok(()) => f().await.map_err(PrivDropErr::Other).and_then(|res| { + info.setresid().map_err(PrivDropErr::Io).and_then(|()| { + if retain_id_promise { + Ok(()) + } else { + promises + .remove_then_pledge(Promise::Id) + .map_err(PrivDropErr::Io) + } + .map(|()| (res, promises)) + }) + }), + Err(err) => Err(PrivDropErr::Io(err)), + }, + Err(err) => Err(PrivDropErr::from(err)), + } + } + } + }, + Err(err) => Err(PrivDropErr::from(err)), + } + } + /// Returns the number of [`Promise`]s. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// assert_eq!(Promises::new([Promise::Stdio]).len(), 1); + /// ``` + #[inline] + #[must_use] + pub const fn len(&self) -> u32 { + self.0.count_ones() + } + /// Returns `true` iff there are no [`Promise`]s. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::Promises; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Promises::NONE.is_empty()); + /// ``` + #[inline] + #[must_use] + pub const fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Returns `true` iff `self` contains `promise`. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Promises::new([Promise::Stdio]).contains(Promise::Stdio)); + /// # #[cfg(target_os = "openbsd")] + /// assert!(!Promises::new([Promise::Stdio]).contains(Promise::Rpath)); + /// ``` + #[inline] + #[must_use] + pub const fn contains(&self, promise: Promise) -> bool { + let val = promise.to_u64(); + self.0 & val == val + } + /// Removes all `Promise`s _not_ in `promises` from `self`. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]); + /// # #[cfg(target_os = "openbsd")] + /// proms.retain([Promise::Stdio]); + /// # #[cfg(target_os = "openbsd")] + /// assert!(proms.len() == 1 && proms.contains(Promise::Stdio)); + /// ``` + #[inline] + pub fn retain<P: AsRef<[Promise]>>(&mut self, promises: P) { + self.0 = promises + .as_ref() + .iter() + .fold(Self::NONE, |val, promise| { + if self.contains(*promise) { + val.add(*promise) + } else { + val + } + }) + .0; + } + /// Same as [`Self::retain`] then [`Self::pledge`]; however this is "atomic" in that if an error occurs from `pledge`, + /// `self` is left unchanged. This is useful when one wants to "recover" from an error since there is no + /// way to add `Promise`s back forcing one to have to create a second `Promises`. + /// + /// Note that when `retain` doesn't change `self`, `pledge` is not called. + /// + /// # Errors + /// + /// Returns [`Error`] iff `pledge` does. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Promises::new([Promise::Rpath, Promise::Stdio]).retain_then_pledge([Promise::Rpath]).is_ok()); + /// ``` + #[inline] + pub fn retain_then_pledge<P: AsRef<[Promise]>>(&mut self, promises: P) -> Result<(), Error> { + // We opt for the easy way by copying `self`. This should be the fastest for the + // typical case since `self` likely has very few `Promise`s, and it makes for less code. + let cur = Self(self.0); + self.retain(promises); + if *self == cur { + Ok(()) + } else { + self.pledge().inspect_err(|_| *self = cur) + } + } + /// Removes all `Promise`s in `promises` from `self`. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]); + /// # #[cfg(target_os = "openbsd")] + /// proms.remove_promises([Promise::Stdio]); + /// # #[cfg(target_os = "openbsd")] + /// assert!(proms.len() == 1 && proms.contains(Promise::Rpath)); + /// ``` + #[inline] + pub fn remove_promises<P: AsRef<[Promise]>>(&mut self, promises: P) { + promises + .as_ref() + .iter() + .fold((), |(), promise| self.remove(*promise)); + } + /// Same as [`Self::remove_promises`] then [`Self::pledge`]; however this is "atomic" in that if an error occurs from + /// `pledge`, `self` is left unchanged. This is useful when one wants to "recover" from an error since there + /// is no way to add `Promise`s back forcing one to have to create a second `Promises`. + /// + /// Note that when `remove_promises` doesn't remove any, `pledge` is not called. + /// + /// # Errors + /// + /// Returns [`Error`] iff `pledge` does. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Promises::new([Promise::Rpath, Promise::Stdio]).remove_promises_then_pledge([Promise::Rpath]).is_ok()); + /// ``` + #[inline] + pub fn remove_promises_then_pledge<P: AsRef<[Promise]>>( + &mut self, + promises: P, + ) -> Result<(), Error> { + // We opt for the easy way by copying `self`. This should be the fastest for the + // typical case since `self` likely has very few `Promise`s, and it makes for less code. + let cur = Self(self.0); + self.remove_promises(promises); + if *self == cur { + Ok(()) + } else { + self.pledge().inspect_err(|_| *self = cur) + } + } + /// Removes `promise` from `self`. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]); + /// # #[cfg(target_os = "openbsd")] + /// proms.remove(Promise::Stdio); + /// # #[cfg(target_os = "openbsd")] + /// assert!(proms.len() == 1 && proms.contains(Promise::Rpath)); + /// ``` + #[inline] + pub const fn remove(&mut self, promise: Promise) { + self.0 &= !promise.to_u64(); + } + /// Same as [`Self::remove`] then [`Self::pledge`]; however this is "atomic" in that if an error occurs from `pledge`, + /// `self` is left unchanged. This is useful when one wants to "recover" from an error since there is no + /// way to add `Promise`s back forcing one to have to create a second `Promises`. + /// + /// # Errors + /// + /// Returns [`Error`] iff `pledge` does. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Promises::new([Promise::Rpath, Promise::Stdio]).remove_then_pledge(Promise::Rpath).is_ok()); + /// ``` + #[inline] + pub fn remove_then_pledge(&mut self, promise: Promise) -> Result<(), Error> { + // We opt for the easy way by copying `self`. This should be the fastest for the + // typical case since `self` likely has very few `Promise`s, and it makes for less code. + let cur = Self(self.0); + self.remove(promise); + if *self == cur { + Ok(()) + } else { + self.pledge().inspect_err(|_| *self = cur) + } + } + /// Invokes `pledge(2)` always passing `NULL` for `execpromises` and `promises` for `promises`. + /// + /// This function MUST only be called by [`Self::pledge`] and [`Self::pledge_none`]. + #[expect(unsafe_code, reason = "pledge(2) takes in pointers")] + fn inner_pledge(promises: *const c_char) -> Result<(), Error> { + // SAFETY: + // `promises` is either null or valid as can be seen in the only functions that call this + // function: + // `Self::pledge` and `Self::pledge_none`. + if unsafe { pledge(promises, ptr::null()) } == SUCCESS { + Ok(()) + } else { + Err(Error::last_os_error()) + } + } + /// Invokes [`pledge(2)`](https://man.openbsd.org/pledge.2) always passing in + /// `NULL` for `execpromises` and its contained [`Promise`]s for `promises`. + /// + /// # Errors + /// + /// Returns [`Error`] iff `pledge(2)` errors. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Promise, Promises}; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Promises::new([Promise::Stdio]).pledge().is_ok()); + /// ``` + #[expect( + unsafe_code, + reason = "we manually construct a valid CString and ensure its safety" + )] + #[expect( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + reason = "we replace a trailing space with 0 to ensure CString is correctly constructed" + )] + #[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "many if blocks to handle each Promise" + )] + #[inline] + pub fn pledge(&self) -> Result<(), Error> { + let mut p = Vec::new(); + if self.contains(Audio) { + p.extend_from_slice(b"audio "); + } + if self.contains(Bpf) { + p.extend_from_slice(b"bpf "); + } + if self.contains(Chown) { + p.extend_from_slice(b"chown "); + } + if self.contains(Cpath) { + p.extend_from_slice(b"cpath "); + } + if self.contains(Disklabel) { + p.extend_from_slice(b"disklabel "); + } + if self.contains(Dns) { + p.extend_from_slice(b"dns "); + } + if self.contains(Dpath) { + p.extend_from_slice(b"dpath "); + } + if self.contains(Drm) { + p.extend_from_slice(b"drm "); + } + if self.contains(Promise::Error) { + p.extend_from_slice(b"error "); + } + if self.contains(Exec) { + p.extend_from_slice(b"exec "); + } + if self.contains(Fattr) { + p.extend_from_slice(b"fattr "); + } + if self.contains(Flock) { + p.extend_from_slice(b"flock "); + } + if self.contains(Getpw) { + p.extend_from_slice(b"getpw "); + } + if self.contains(Id) { + p.extend_from_slice(b"id "); + } + if self.contains(Inet) { + p.extend_from_slice(b"inet "); + } + if self.contains(Mcast) { + p.extend_from_slice(b"mcast "); + } + if self.contains(Pf) { + p.extend_from_slice(b"pf "); + } + if self.contains(Proc) { + p.extend_from_slice(b"proc "); + } + if self.contains(ProtExec) { + p.extend_from_slice(b"prot_exec "); + } + if self.contains(Ps) { + p.extend_from_slice(b"ps "); + } + if self.contains(Recvfd) { + p.extend_from_slice(b"recvfd "); + } + if self.contains(Route) { + p.extend_from_slice(b"route "); + } + if self.contains(Rpath) { + p.extend_from_slice(b"rpath "); + } + if self.contains(Sendfd) { + p.extend_from_slice(b"sendfd "); + } + if self.contains(Settime) { + p.extend_from_slice(b"settime "); + } + if self.contains(Stdio) { + p.extend_from_slice(b"stdio "); + } + if self.contains(Tape) { + p.extend_from_slice(b"tape "); + } + if self.contains(Tmppath) { + p.extend_from_slice(b"tmppath "); + } + if self.contains(Tty) { + p.extend_from_slice(b"tty "); + } + if self.contains(Unix) { + p.extend_from_slice(b"unix "); + } + if self.contains(Unveil) { + p.extend_from_slice(b"unveil "); + } + if self.contains(Video) { + p.extend_from_slice(b"video "); + } + if self.contains(Vminfo) { + p.extend_from_slice(b"vminfo "); + } + if self.contains(Vmm) { + p.extend_from_slice(b"vmm "); + } + if self.contains(Wpath) { + p.extend_from_slice(b"wpath "); + } + if self.contains(Wroute) { + p.extend_from_slice(b"wroute "); + } + let idx = p.len(); + if idx == 0 { + p.push(0); + } else { + // All promises have a space after them which means + // we must replace the last promise's space with + // 0/nul byte. + // `idx` is at least 1 based on the above check. + p[idx - 1] = 0; + } + // SAFETY: + // `p` was populated above with correct ASCII-encoding of the literal + // values all of which do not have 0 bytes. + // `p` ends with a 0/nul byte. + let arg = unsafe { CString::from_vec_with_nul_unchecked(p) }; + Self::inner_pledge(arg.as_ptr()) + } + /// Invokes [`pledge(2)`](https://man.openbsd.org/pledge.2) with `NULL` for both `promises` and + /// `execpromises`. + /// + /// # Errors + /// + /// Returns [`Error`] iff `pledge(2)` errors. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::Promises; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Promises::pledge_none().is_ok()); + /// ``` + #[inline] + pub fn pledge_none() -> Result<(), Error> { + // `NULL` is always valid for `promises`. + Self::inner_pledge(ptr::null()) + } +} +impl PartialEq<&Self> for Promises { + #[inline] + fn eq(&self, other: &&Self) -> bool { + *self == **other + } +} +impl PartialEq<Promises> for &Promises { + #[inline] + fn eq(&self, other: &Promises) -> bool { + **self == *other + } +} +/// A permission in [`Permissions`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Permission { + /// [c](https://man.openbsd.org/unveil.2#c). + Create, + /// [x](https://man.openbsd.org/unveil.2#x). + Execute, + /// [r](https://man.openbsd.org/unveil.2#r). + Read, + /// [w](https://man.openbsd.org/unveil.2#w). + Write, +} +impl Permission { + /// Transforms `self` into a `u8`. + const fn to_u8(self) -> u8 { + match self { + Self::Create => 1, + Self::Execute => 2, + Self::Read => 4, + Self::Write => 8, + } + } +} +impl PartialEq<&Self> for Permission { + #[inline] + fn eq(&self, other: &&Self) -> bool { + *self == **other + } +} +impl PartialEq<Permission> for &Permission { + #[inline] + fn eq(&self, other: &Permission) -> bool { + **self == *other + } +} +/// `permissions` to [`unveil(2)`](https://man.openbsd.org/unveil.2). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Permissions(u8); +impl Permissions { + /// A `Permissions` with no [`Permission`]s enabled. + pub const NONE: Self = Self(0); + /// A `Permissions` with all [`Permission`]s enabled. + pub const ALL: Self = Self::NONE + .enable(Permission::Create) + .enable(Permission::Execute) + .enable(Permission::Read) + .enable(Permission::Write); + /// A `Permissions` with only [`Permission::Create`] enabled. + pub const CREATE: Self = Self::NONE.enable(Permission::Create); + /// A `Permissions` with only [`Permission::Execute`] enabled. + pub const EXECUTE: Self = Self::NONE.enable(Permission::Execute); + /// A `Permissions` with only [`Permission::Read`] enabled. + pub const READ: Self = Self::NONE.enable(Permission::Read); + /// A `Permissions` with only [`Permission::Write`] enabled. + pub const WRITE: Self = Self::NONE.enable(Permission::Write); + /// `const` version of [`Self::bitor`]. + #[inline] + #[must_use] + pub const fn enable(self, permission: Permission) -> Self { + Self(self.0 | permission.to_u8()) + } + /// Returns `true` iff `self` has `permission` enabled. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Permission, Permissions}; + /// # #[cfg(target_os = "openbsd")] + /// let perms = Permissions::CREATE; + /// # #[cfg(target_os = "openbsd")] + /// assert!(perms.is_enabled(Permission::Create)); + /// # #[cfg(target_os = "openbsd")] + /// assert!(!perms.is_enabled(Permission::Write)); + /// ``` + #[inline] + #[must_use] + pub const fn is_enabled(self, permission: Permission) -> bool { + let val = permission.to_u8(); + self.0 & val == val + } + /// Invokes `unveil(2)` passing `path` for `path` and `permissions` for `permissions`. + /// + /// This function MUST only be called by the functions [`Self::unveil`] and [`Self::unveil_no_more`]. + #[expect(unsafe_code, reason = "unveil(2) takes in pointers")] + fn inner_unveil(path: *const c_char, permissions: *const c_char) -> Result<(), Error> { + // SAFETY: + // `path` and `permissions` are either null or valid as can be seen in the only functions that call this + // function: + // `Self::unveil` and `Self::unveil_no_more`. + if unsafe { unveil(path, permissions) } == SUCCESS { + Ok(()) + } else { + Err(Error::last_os_error()) + } + } + /// Invokes [`unveil(2)`](https://man.openbsd.org/unveil.2) + /// passing `path` for `path` and the contained permissions for `permissions`. + /// + /// # Errors + /// + /// Returns [`NulError`] iff [`CString::new`] does. + /// Returns [`Error`] iff `unveil(2)` errors. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use std::io::ErrorKind; + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::{Permissions, NulOrIoErr}; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Permissions::READ.unveil("/path/to/read").is_ok()); + /// # #[cfg(target_os = "openbsd")] + /// assert!(Permissions::READ.unveil("/path/does/not/exist").map_or_else( + /// |err| match err { + /// NulOrIoErr::Io(e) => e.kind() == ErrorKind::NotFound, + /// NulOrIoErr::Nul(_) => false, + /// }, + /// |()| false + /// )); + /// ``` + #[expect( + unsafe_code, + reason = "we manually construct a valid CString and ensure its safety" + )] + #[expect( + clippy::arithmetic_side_effects, + reason = "we pre-allocate a Vec with the exact capacity which is capped at 5" + )] + #[inline] + pub fn unveil<P: AsRef<Path>>(self, path: P) -> Result<(), NulOrIoErr> { + CString::new(path.as_ref().as_os_str().as_bytes()).map_or_else( + |e| Err(NulOrIoErr::Nul(e)), + |path_c| { + // The max sum is 5, so overflow is not possible. + let mut vec = Vec::with_capacity( + usize::from(self.is_enabled(Permission::Create)) + + usize::from(self.is_enabled(Permission::Execute)) + + usize::from(self.is_enabled(Permission::Read)) + + usize::from(self.is_enabled(Permission::Write)) + + 1, + ); + if self.is_enabled(Permission::Create) { + vec.push(b'c'); + } + if self.is_enabled(Permission::Execute) { + vec.push(b'x'); + } + if self.is_enabled(Permission::Read) { + vec.push(b'r'); + } + if self.is_enabled(Permission::Write) { + vec.push(b'w'); + } + vec.push(0); + // SAFETY: + // `vec` was populated above with correct ASCII-encoding of the literal + // values all of which do not have 0 bytes. + // `vec` ends with a 0/nul byte. + let perm_c = unsafe { CString::from_vec_with_nul_unchecked(vec) }; + Self::inner_unveil(path_c.as_ptr(), perm_c.as_ptr()).map_err(NulOrIoErr::Io) + }, + ) + } + /// Invokes [`unveil(2)`](https://man.openbsd.org/unveil.2) by passing `NULL` for both `path` and + /// `permissions`. + /// + /// # Errors + /// + /// Returns [`Error`] when a problem occurs. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(target_os = "openbsd")] + /// # use priv_sep::Permissions; + /// # #[cfg(target_os = "openbsd")] + /// assert!(Permissions::unveil_no_more().is_ok()); + /// ``` + #[inline] + pub fn unveil_no_more() -> Result<(), Error> { + // `NULL` is valid for both `path` and `permissions`. + Self::inner_unveil(ptr::null(), ptr::null()) + } +} +impl Display for Permissions { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "unveil(2) '{}{}{}{}' permissions", + if self.is_enabled(Permission::Create) { + "c" + } else { + "" + }, + if self.is_enabled(Permission::Execute) { + "x" + } else { + "" + }, + if self.is_enabled(Permission::Read) { + "r" + } else { + "" + }, + if self.is_enabled(Permission::Write) { + "w" + } else { + "" + }, + ) + } +} +impl BitAnd<Permission> for Permissions { + type Output = Self; + #[inline] + fn bitand(self, rhs: Permission) -> Self::Output { + Self(self.0 & rhs.to_u8()) + } +} +impl BitAndAssign<Permission> for Permissions { + #[inline] + fn bitand_assign(&mut self, rhs: Permission) { + self.0 &= rhs.to_u8(); + } +} +impl BitOr<Permission> for Permissions { + type Output = Self; + #[inline] + fn bitor(self, rhs: Permission) -> Self::Output { + Self(self.0 | rhs.to_u8()) + } +} +impl BitOrAssign<Permission> for Permissions { + #[inline] + fn bitor_assign(&mut self, rhs: Permission) { + self.0 |= rhs.to_u8(); + } +} +impl BitXor<Permission> for Permissions { + type Output = Self; + #[inline] + fn bitxor(self, rhs: Permission) -> Self::Output { + Self(self.0 ^ rhs.to_u8()) + } +} +impl BitXorAssign<Permission> for Permissions { + #[inline] + fn bitxor_assign(&mut self, rhs: Permission) { + self.0 ^= rhs.to_u8(); + } +} +impl Not for Permissions { + type Output = Self; + #[inline] + fn not(self) -> Self::Output { + Self(Self::ALL.0 & !self.0) + } +} +impl PartialEq<&Self> for Permissions { + #[inline] + fn eq(&self, other: &&Self) -> bool { + *self == **other + } +} +impl PartialEq<Permissions> for &Permissions { + #[inline] + fn eq(&self, other: &Permissions) -> bool { + **self == *other + } +} +#[cfg(test)] +mod tests { + use super::{Permissions, Promise, Promises}; + use crate::{PrivDropErr, Uid}; + use core::net::{Ipv6Addr, SocketAddrV6}; + use std::{fs, io::Error, net::TcpListener}; + const README: &str = "README.md"; + #[test] + #[ignore] + fn test_pledge_unveil() { + const FILE_EXISTS: &str = "/home/zack/foo.txt"; + _ = fs::metadata(FILE_EXISTS) + .expect(format!("{FILE_EXISTS} does not exist, so unit testing cannot occur").as_str()); + const FILE_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23"; + drop(fs::metadata(FILE_NOT_EXISTS).expect_err( + format!("{FILE_NOT_EXISTS} exists, so unit testing cannot occur").as_str(), + )); + const DIR_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23/"; + 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!(Promises::pledge_none().is_ok()); + print!(""); + assert!(Promises::ALL.pledge().is_ok()); + // This tests that duplicates are ignored as well as the implementation of PartialEq. + let mut initial_promises = Promises::new([ + Promise::Stdio, + Promise::Unveil, + Promise::Rpath, + Promise::Stdio, + ]); + assert!(initial_promises.len() == 3); + assert!( + 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()); + // This tests unveil with no permissions. + assert!(Permissions::NONE.unveil(FILE_EXISTS).is_ok()); + assert!(fs::metadata(FILE_EXISTS).is_err()); + // 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).is_ok()); + // This tests that calls to unveil on missing files don't error. + assert!(Permissions::NONE.unveil(FILE_NOT_EXISTS).is_ok()); + // This tests that calls to unveil on missing directories error. + assert!(Permissions::NONE.unveil(DIR_NOT_EXISTS).is_err()); + // 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).is_ok()); + // The below tests that Promises can only be removed and not added. + initial_promises.remove_promises([Promise::Unveil]); + assert_eq!(initial_promises.len(), 2); + initial_promises.remove(Promise::Rpath); + 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()); + // 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)); + } + #[test] + #[ignore] + fn test_pledge_priv_drop() -> Result<(), PrivDropErr<Error>> { + if Uid::geteuid().is_root() { + Promises::new_priv_drop( + "zack", + [Promise::Inet, Promise::Rpath, Promise::Unveil], + false, + || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)), + ) + .and_then(|(_, mut promises)| { + Permissions::READ + .unveil(README) + .map_err(PrivDropErr::from) + .and_then(|()| { + fs::exists(README) + .map_err(PrivDropErr::Io) + .and_then(|exists| { + Permissions::NONE + .unveil(README) + .map_err(PrivDropErr::from) + .and_then(|()| { + promises + .remove_promises_then_pledge([ + Promise::Rpath, + Promise::Unveil, + ]) + .map_err(PrivDropErr::Io) + .map(|()| { + assert!(exists); + assert!( + TcpListener::bind(SocketAddrV6::new( + Ipv6Addr::LOCALHOST, + 80, + 0, + 0 + )) + .is_err() + ); + }) + }) + }) + }) + }) + } else { + Ok(()) + } + } + #[test] + #[ignore] + fn test_pledge_chroot_priv_drop() -> Result<(), PrivDropErr<Error>> { + if Uid::geteuid().is_root() { + Promises::new_chroot_then_priv_drop( + "zack", + "./", + [Promise::Inet, Promise::Rpath, Promise::Unveil], + false, + || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)), + ) + .and_then(|(_, mut promises)| { + Permissions::READ + .unveil(README) + .map_err(PrivDropErr::from) + .and_then(|()| { + fs::exists(README) + .map_err(PrivDropErr::Io) + .and_then(|exists| { + Permissions::NONE + .unveil(README) + .map_err(PrivDropErr::from) + .and_then(|()| { + promises + .remove_promises_then_pledge([ + Promise::Rpath, + Promise::Unveil, + ]) + .map_err(PrivDropErr::Io) + .map(|()| { + assert!(exists); + assert!( + TcpListener::bind(SocketAddrV6::new( + Ipv6Addr::LOCALHOST, + 80, + 0, + 0 + )) + .is_err() + ); + }) + }) + }) + }) + }) + } else { + Ok(()) + } + } +}