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:
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) [![crates-io]](https://crates.io/crates/priv_sep) [![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(())
+ }
+ }
+}