commit 4d9d228407d2ebc3a045af3409aa6d58dba61ef6
parent 49e56b11b5fa96201214cfb342a97321d948ffdd
Author: Zack Newman <zack@philomathiclife.com>
Date: Mon, 19 May 2025 13:01:54 -0600
fix repo files
Diffstat:
11 files changed, 1108 insertions(+), 2608 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,29 +1,60 @@
[package]
authors = ["Zack Newman <zack@philomathiclife.com>"]
-categories = ["development-tools::ffi", "external-ffi-bindings", "os"]
-description = "FFI for pledge(2) and unveil(2) on OpenBSD."
+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"]
+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.85.0"
-version = "2.2.0"
+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"]
-[target.'cfg(target_os = "openbsd")'.dependencies]
-libc = { version = "0.2.170", default-features = false, features = ["std"], optional = true }
-
-
-### FEATURES #################################################################
-
-[features]
-default = ["openbsd"]
-
-# Provide pledge and unveil support for OpenBSD.
-openbsd = ["dep:libc"]
+[dev-dependencies]
+tokio = { version = "1.44.2", default-features = false, features = ["macros", "net", "rt"] }
diff --git a/README.md b/README.md
@@ -4,25 +4,87 @@
[<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 for privilege separation.
-It is currently designed around [`pledge(2)`](https://man.openbsd.org/amd64/pledge.2) and
-[`unveil(2)`](https://man.openbsd.org/amd64/unveil.2) for OpenBSD, but
-in the future may contain functionality for Linux's
-[`seccomp(2)`](https://man7.org/linux/man-pages/man2/seccomp.2.html).
-
-## Pledge
-
-Calls to `pledge(2)` are done via `Promises::pledge` and `pledge_none`.
-
-Note that since the use of `execpromises` is quite rare, `NULL` is always used for it.
-
-## Unveil
-
-Calls to `unveil(2)` are done via `Permissions::unveil` and `unveil_no_more`.
-
-## Errors
-
-Any error returned from the underlying system call is propagated via `Error`.
+`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)
@@ -51,14 +113,13 @@ at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you,
as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
-Before any PR is sent, `cargo clippy` and `cargo t` should be run for both `--no-default-features` and
-`--all-features`. Additionally `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to
-ensure documentation can be built.
+Before any PR is sent, `cargo clippy` 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; as a result,
-the crate is only tested on the `x86_64-unknown-openbsd` target. 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
+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/.gitignore b/priv_sep/.gitignore
@@ -1,2 +0,0 @@
-Cargo.lock
-target/**
diff --git a/priv_sep/Cargo.toml b/priv_sep/Cargo.toml
@@ -1,60 +0,0 @@
-[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
@@ -1,177 +0,0 @@
-
- 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
@@ -1,20 +0,0 @@
-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
@@ -1,125 +0,0 @@
-# `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/lib.rs b/priv_sep/src/lib.rs
@@ -1,1221 +0,0 @@
-//! [![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/c.rs b/src/c.rs
diff --git a/src/lib.rs b/src/lib.rs
@@ -4,1136 +4,1050 @@
//! [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 for privilege separation.
-//! It is currently designed around [`pledge(2)`](https://man.openbsd.org/amd64/pledge.2) and
-//! [`unveil(2)`](https://man.openbsd.org/amd64/unveil.2) for OpenBSD, but
-//! in the future may contain functionality for Linux's
-//! [`seccomp(2)`](https://man7.org/linux/man-pages/man2/seccomp.2.html).
+//! `priv_sep` is a library that uses the system's libc to perform privilege separation and privilege reduction.
//!
-//! ## Pledge
+//! ## `priv_sep` in action for OpenBSD
//!
-//! Calls to `pledge(2)` are done via [`Promises::pledge`] and [`pledge_none`].
-//! Note that since the use of `execpromises` is quite rare, `NULL` is always
-//! used for it.
+//! ```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());
+//! }
+//! }
+//! }
+//! ```
//!
-//! ## Unveil
+//! ## `priv_sep` in action for Unix-like OSes
//!
-//! Calls to `unveil(2)` are done via [`Permissions::unveil`] and [`unveil_no_more`].
-//!
-//! ## Errors
-//!
-//! Any error returned from the underlying system call is propagated via [`io::Error`].
+//! ```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))]
-#![deny(
- unknown_lints,
- future_incompatible,
- let_underscore,
- missing_docs,
- nonstandard_style,
- refining_impl_trait,
- rust_2018_compatibility,
- rust_2018_idioms,
- rust_2021_compatibility,
- rust_2024_compatibility,
- unsafe_code,
- unused,
- warnings,
- clippy::all,
- clippy::cargo,
- clippy::complexity,
- clippy::correctness,
- clippy::nursery,
- clippy::pedantic,
- clippy::perf,
- clippy::restriction,
- clippy::style,
- clippy::suspicious
-)]
-#![expect(
- clippy::blanket_clippy_restriction_lints,
- reason = "same reason as below"
-)]
-#![cfg_attr(docsrs, doc(cfg(feature = "openbsd")))]
-#![cfg(feature = "openbsd")]
-#![expect(
- clippy::arbitrary_source_item_ordering,
- clippy::exhaustive_enums,
- clippy::implicit_return,
- clippy::min_ident_chars,
- clippy::missing_trait_methods,
- clippy::ref_patterns,
- clippy::unseparated_literal_suffix,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
+#![allow(clippy::pub_use, reason = "don't want openbsd types in a module")]
extern crate alloc;
-use Promise::{
- Audio, Bpf, Chown, Cpath, Disklabel, Dns, Dpath, Drm, Error, 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,
-};
+/// C FFI.
+mod c;
+/// OpenBSD
+#[cfg(any(doc, target_os = "openbsd"))]
+mod openbsd;
use alloc::ffi::{CString, NulError};
+use c::{IdT, SUCCESS};
use core::{
- convert::AsRef,
- error,
- ffi::{c_char, c_int},
+ error::Error as CoreErr,
+ ffi::{CStr, c_char, c_int},
fmt::{self, Display, Formatter},
- ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not},
+ mem::MaybeUninit,
ptr,
};
-use std::{io, os::unix::ffi::OsStrExt as _, path::Path};
-/// A `promise` to [`pledge(2)`](https://man.openbsd.org/amd64/pledge.2).
-#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
-#[non_exhaustive]
-pub enum Promise {
- /// Consult `pledge(2)`.
- Audio,
- /// Consult `pledge(2)`.
- Bpf,
- /// Consult `pledge(2)`.
- Chown,
- /// Consult `pledge(2)`.
- Cpath,
- /// Consult `pledge(2)`.
- Disklabel,
- /// Consult `pledge(2)`.
- Dns,
- /// Consult `pledge(2)`.
- Dpath,
- /// Consult `pledge(2)`.
- Drm,
- /// Consult `pledge(2)`.
- Error,
- /// Consult `pledge(2)`.
- Exec,
- /// Consult `pledge(2)`.
- Fattr,
- /// Consult `pledge(2)`.
- Flock,
- /// Consult `pledge(2)`.
- Getpw,
- /// Consult `pledge(2)`.
- Id,
- /// Consult `pledge(2)`.
- Inet,
- /// Consult `pledge(2)`.
- Mcast,
- /// Consult `pledge(2)`.
- Pf,
- /// Consult `pledge(2)`.
- Proc,
- /// Consult `pledge(2)`.
- ProtExec,
- /// Consult `pledge(2)`.
- Ps,
- /// Consult `pledge(2)`.
- Recvfd,
- /// Consult `pledge(2)`.
- Route,
- /// Consult `pledge(2)`.
- Rpath,
- /// Consult `pledge(2)`.
- Sendfd,
- /// Consult `pledge(2)`.
- Settime,
- /// Consult `pledge(2)`.
- Stdio,
- /// Consult `pledge(2)`.
- Tape,
- /// Consult `pledge(2)`.
- Tmppath,
- /// Consult `pledge(2)`.
- Tty,
- /// Consult `pledge(2)`.
- Unix,
- /// Consult `pledge(2)`.
- Unveil,
- /// Consult `pledge(2)`.
- Video,
- /// Consult `pledge(2)`.
- Vminfo,
- /// Consult `pledge(2)`.
- Vmm,
- /// Consult `pledge(2)`.
- Wpath,
- /// Consult `pledge(2)`.
- 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,
- 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"),
- 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/amd64/pledge.2)d,
-/// only a subset of those `promises` can be `pledge(2)`d again, so 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);
-/// Invokes `pledge(2)` always passing `NULL` for `execpromises` and
-/// `promises` for `promises`.
-///
-/// This function MUST only be called by `Promises::pledge` and
-/// `pledge_none`.
-#[expect(unsafe_code, reason = "FFI requires unsafe code")]
-fn pledge(promises: *const c_char) -> Result<(), io::Error> {
- unsafe extern "C" {
- fn pledge(promises: *const c_char, execpromises: *const c_char) -> c_int;
- }
- // SAFETY:
- // `pledge` is an FFI binding; thus requires unsafe code.
- // `NULL` is always valid for `execpromises`, and `promises` meets the requirements of the `pledge(2)` call
- // as can be verified in the only two functions that call this function:
- // `Promises::pledge` and `pledge_none`.
- if unsafe { pledge(promises, ptr::null()) } == 0i32 {
- Ok(())
- } else {
- Err(io::Error::last_os_error())
- }
-}
-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
- ///
- /// ```
- /// # use priv_sep::{Promise, Promises};
- /// 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))
- }
- /// Returns the number of [`Promise`]s.
+#[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
///
- /// ```
- /// # use priv_sep::{Promise, Promises};
- /// assert_eq!(Promises::new([Promise::Stdio]).len(), 1);
+ /// ```no_run
+ /// # use priv_sep::Uid;
+ /// assert!(Uid::ROOT.is_root());
/// ```
#[inline]
#[must_use]
- pub const fn len(&self) -> u32 {
- self.0.count_ones()
+ pub const fn is_root(self) -> bool {
+ self.0 == Self::ROOT.0
}
- /// Returns `true` iff there are no [`Promise`]s.
+ /// [`getuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getuid.html).
///
/// # Examples
///
- /// ```
- /// # use priv_sep::Promises;
- /// assert!(Promises::NONE.is_empty());
+ /// ```no_run
+ /// # use priv_sep::Uid;
+ /// assert_eq!(Uid::getuid(), 1000);
/// ```
#[inline]
#[must_use]
- pub const fn is_empty(&self) -> bool {
- self.len() == 0
+ pub fn getuid() -> Self {
+ Self(c::getuid())
}
- /// Returns `true` iff `self` contains `promise`.
+ /// [`geteuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/geteuid.html).
///
/// # Examples
///
- /// ```
- /// # use priv_sep::{Promise, Promises};
- /// assert!(Promises::new([Promise::Stdio]).contains(Promise::Stdio));
- /// assert!(!Promises::new([Promise::Stdio]).contains(Promise::Rpath));
+ /// ```no_run
+ /// # use priv_sep::Uid;
+ /// assert_eq!(Uid::geteuid(), 1000);
/// ```
#[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
- ///
- /// ```
- /// # use priv_sep::{Promise, Promises};
- /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]);
- /// proms.retain([Promise::Stdio]);
- /// 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;
+ pub fn geteuid() -> Self {
+ Self(c::geteuid())
}
- /// 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.
+ /// Calls [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html)
+ /// passing `self` for the real, effective, and saved user IDs.
///
/// # Errors
///
- /// Returns [`io::Error`] iff `pledge` does.
+ /// Errors iff `setresuid` does.
///
- /// # Example
+ /// # Examples
///
/// ```no_run
- /// # use priv_sep::{Promise, Promises};
- /// assert!(Promises::new([Promise::Rpath, Promise::Stdio]).retain_then_pledge([Promise::Rpath]).is_ok());
+ /// # use priv_sep::Uid;
+ /// assert!(Uid(1000).setresuid().is_ok());
/// ```
#[inline]
- pub fn retain_then_pledge<P: AsRef<[Promise]>>(
- &mut self,
- promises: P,
- ) -> Result<(), io::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 {
+ pub fn setresuid(self) -> Result<(), Error> {
+ if c::setresuid(self.0, self.0, self.0) == SUCCESS {
Ok(())
} else {
- self.pledge().inspect_err(|_| *self = cur)
+ Err(Error::last_os_error())
}
}
- /// Removes all `Promise`s in `promises` from `self`.
- ///
- /// # Examples
- ///
- /// ```
- /// # use priv_sep::{Promise, Promises};
- /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]);
- /// proms.remove_promises([Promise::Stdio]);
- /// assert!(proms.len() == 1 && proms.contains(Promise::Rpath));
- /// ```
+}
+impl PartialEq<&Self> for Uid {
#[inline]
- pub fn remove_promises<P: AsRef<[Promise]>>(&mut self, promises: P) {
- promises
- .as_ref()
- .iter()
- .fold((), |(), promise| self.remove(*promise));
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
}
- /// 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 [`io::Error`] iff `pledge` does.
- ///
- /// # Example
- ///
- /// ```no_run
- /// # use priv_sep::{Promise, Promises};
- /// assert!(Promises::new([Promise::Rpath, Promise::Stdio]).remove_promises_then_pledge([Promise::Rpath]).is_ok());
- /// ```
+}
+impl PartialEq<Uid> for &Uid {
#[inline]
- pub fn remove_promises_then_pledge<P: AsRef<[Promise]>>(
- &mut self,
- promises: P,
- ) -> Result<(), io::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)
- }
+ fn eq(&self, other: &Uid) -> bool {
+ **self == *other
}
- /// Removes `promise` from `self`.
+}
+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
///
- /// ```
- /// # use priv_sep::{Promise, Promises};
- /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]);
- /// proms.remove(Promise::Stdio);
- /// assert!(proms.len() == 1 && proms.contains(Promise::Rpath));
+ /// ```no_run
+ /// # use priv_sep::Gid;
+ /// assert_eq!(Gid::getgid(), 1000);
/// ```
#[inline]
- pub const fn remove(&mut self, promise: Promise) {
- self.0 &= !promise.to_u64();
+ #[must_use]
+ pub fn getgid() -> Self {
+ Self(c::getgid())
}
- /// 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 [`io::Error`] iff `pledge` does.
+ /// [`getegid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getegid.html).
///
- /// # Example
+ /// # Examples
///
/// ```no_run
- /// # use priv_sep::{Promise, Promises};
- /// assert!(Promises::new([Promise::Rpath, Promise::Stdio]).remove_then_pledge(Promise::Rpath).is_ok());
+ /// # use priv_sep::Gid;
+ /// assert_eq!(Gid::getegid(), 1000);
/// ```
#[inline]
- pub fn remove_then_pledge(&mut self, promise: Promise) -> Result<(), io::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)
- }
+ #[must_use]
+ pub fn getegid() -> Self {
+ Self(c::getegid())
}
- /// Invokes [`pledge(2)`](https://man.openbsd.org/amd64/pledge.2) always passing in
- /// `NULL` for `execpromises` and its contained [`Promise`]s for `promises`.
+ /// Calls [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html)
+ /// passing `self` for the real, effective, and saved group IDs.
///
/// # Errors
///
- /// Returns [`io::Error`] iff `pledge(2)` errors.
+ /// Errors iff `setresgid` does.
///
/// # Examples
///
/// ```no_run
- /// # use priv_sep::{Promise, Promises};
- /// assert!(Promises::new([Promise::Stdio]).pledge().is_ok());
+ /// # use priv_sep::Gid;
+ /// assert!(Gid(1000).setresgid().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<(), io::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(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);
+ pub fn setresgid(self) -> Result<(), Error> {
+ if c::setresgid(self.0, self.0, self.0) == SUCCESS {
+ Ok(())
} 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;
+ Err(Error::last_os_error())
}
- // 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) };
- pledge(arg.as_ptr())
}
}
-impl PartialEq<&Self> for Promises {
+impl PartialEq<&Self> for Gid {
#[inline]
fn eq(&self, other: &&Self) -> bool {
*self == **other
}
}
-impl PartialEq<Promises> for &Promises {
+impl PartialEq<Gid> for &Gid {
#[inline]
- fn eq(&self, other: &Promises) -> bool {
+ fn eq(&self, other: &Gid) -> bool {
**self == *other
}
}
-/// Invokes [`pledge(2)`](https://man.openbsd.org/amd64/pledge.2) with `NULL` for
-/// both `promises` and `execpromises`.
+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 [`io::Error`] iff `pledge(2)` errors.
+/// Returns [`NulError`] iff [`CString::new`] does.
+/// Returns [`Error`] iff `chroot(2)` errors.
///
-/// # Example
+/// # Examples
///
/// ```no_run
-/// use priv_sep;
-/// assert!(priv_sep::pledge_none().is_ok());
+/// assert!(priv_sep::chroot("./").is_ok());
/// ```
+#[expect(unsafe_code, reason = "chroot(2) takes a pointer")]
#[inline]
-pub fn pledge_none() -> Result<(), io::Error> {
- // `NULL` is always valid for `promises`.
- pledge(ptr::null())
+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()))
+ }
+ })
}
-/// Invokes `unveil(2)` passing `path` for `path` and `permissions` for `permissions`.
+/// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html).
///
-/// This function MUST only be called by the functions `Permissions::unveil` and
-/// `unveil_no_more`.
-#[expect(unsafe_code, reason = "FFI requires unsafe code")]
-fn unveil(path: *const c_char, permissions: *const c_char) -> Result<(), io::Error> {
- unsafe extern "C" {
- fn unveil(path: *const c_char, permissions: *const c_char) -> c_int;
- }
+/// 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:
- // `unveil` is an FFI binding; thus requires unsafe code.
- // `path` and `permissions` meet the requirements of the `unveil(2)` call
- // as can be seen in the only functions that call this function:
- // `Permissions::unveil` and `unveil_no_more`.
- if unsafe { unveil(path, permissions) } == 0i32 {
+ // `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(io::Error::last_os_error())
+ Err(Error::last_os_error())
}
}
-/// A permission in [`Permissions`].
-#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
-pub enum Permission {
- /// [c](https://man.openbsd.org/amd64/unveil.2#c).
- Create,
- /// [x](https://man.openbsd.org/amd64/unveil.2#x).
- Execute,
- /// [r](https://man.openbsd.org/amd64/unveil.2#r).
- Read,
- /// [w](https://man.openbsd.org/amd64/unveil.2#w).
- Write,
+/// [`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))
}
-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,
+/// 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 PartialEq<&Self> for Permission {
+impl<E: CoreErr> CoreErr for PrivDropErr<E> {}
+impl<E> From<NulError> for PrivDropErr<E> {
#[inline]
- fn eq(&self, other: &&Self) -> bool {
- *self == **other
+ fn from(value: NulError) -> Self {
+ Self::Nul(value)
}
}
-impl PartialEq<Permission> for &Permission {
+impl<E> From<Error> for PrivDropErr<E> {
#[inline]
- fn eq(&self, other: &Permission) -> bool {
- **self == *other
+ fn from(value: Error) -> Self {
+ Self::Io(value)
}
}
-/// `permissions` to [`unveil(2)`](https://man.openbsd.org/amd64/unveil.2).
-#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
-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_inner(Permission::Create)
- .enable_inner(Permission::Execute)
- .enable_inner(Permission::Read)
- .enable_inner(Permission::Write);
- /// A `Permissions` with only [`Permission::Create`] enabled.
- pub const CREATE: Self = Self::NONE.enable_inner(Permission::Create);
- /// A `Permissions` with only [`Permission::Execute`] enabled.
- pub const EXECUTE: Self = Self::NONE.enable_inner(Permission::Execute);
- /// A `Permissions` with only [`Permission::Read`] enabled.
- pub const READ: Self = Self::NONE.enable_inner(Permission::Read);
- /// A `Permissions` with only [`Permission::Write`] enabled.
- pub const WRITE: Self = Self::NONE.enable_inner(Permission::Write);
- /// Same as [`enable`] but returns a new instance instead of mutating `self`.
- const fn enable_inner(self, permission: Permission) -> Self {
- Self(self.0 | permission.to_u8())
+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),
+ }
}
- /// Enables `permission` in `self`.
- ///
- /// # Examples
- ///
- /// ```
- /// # use priv_sep::{Permission, Permissions};
- /// let mut perms = Permissions::NONE;
- /// perms.enable(Permission::Read);
- /// assert_eq!(perms, Permissions::READ);
- /// ```
+}
+/// 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]
- pub const fn enable(&mut self, permission: Permission) {
- self.0 |= permission.to_u8();
+ 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"),
+ }
}
- /// Disables `permission` in `self`.
- ///
- /// # Examples
- ///
- /// ```
- /// # use priv_sep::{Permission, Permissions};
- /// let mut perms = Permissions::ALL;
- /// perms.disable(Permission::Execute);
- /// assert!(
- /// perms.is_enabled(Permission::Create)
- /// && perms.is_enabled(Permission::Read)
- /// && perms.is_enabled(Permission::Write)
- /// && !perms.is_enabled(Permission::Execute)
- /// );
- /// assert!(perms.is_enabled(Permission::Create) && perms.is_enabled(Permission::Read) && perms.is_enabled(Permission::Write) && !perms.is_enabled(Permission::Execute));
- /// ```
+}
+impl CoreErr for SetresidErr {}
+impl From<Error> for SetresidErr {
#[inline]
- pub const fn disable(&mut self, permission: Permission) {
- self.0 &= !permission.to_u8();
+ 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) }
}
- /// Returns `true` iff `self` has `permission` enabled.
+}
+/// `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
///
- /// ```
- /// # use priv_sep::{Permission, Permissions};
- /// let perms = Permissions::CREATE;
- /// assert!(perms.is_enabled(Permission::Create));
- /// assert!(!perms.is_enabled(Permission::Write));
+ /// ```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_enabled(self, permission: Permission) -> bool {
- let val = permission.to_u8();
- self.0 & val == val
+ pub const fn is_root(self) -> bool {
+ self.uid.is_root()
}
- /// Invokes [`unveil(2)`](https://man.openbsd.org/amd64/unveil.2)
- /// passing `path` for `path` and the contained permissions for `permissions`.
+ /// [`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 [`io::Error`] iff `unveil(2)` errors.
+ /// Returns [`Error`] iff `getpwnam_r` errors.
///
/// # Examples
///
/// ```no_run
- /// # use std::io::ErrorKind;
- /// # use priv_sep::{Permissions, UnveilErr};
- /// assert!(Permissions::READ.unveil("/path/to/read").is_ok());
- /// assert!(Permissions::READ.unveil("/path/does/not/exist").map_or_else(
- /// |err| match err {
- /// UnveilErr::Io(e) => e.kind() == ErrorKind::NotFound,
- /// UnveilErr::Nul(_) => false,
- /// },
- /// |()| false
- /// ));
+ /// # 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 = "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"
+ reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers"
)]
- #[inline]
- pub fn unveil<P: AsRef<Path>>(self, path: P) -> Result<(), UnveilErr> {
- CString::new(path.as_ref().as_os_str().as_bytes()).map_or_else(
- |e| Err(UnveilErr::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');
+ 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));
}
- 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) };
- let path_ptr = path_c.as_ptr();
- let perm_ptr = perm_c.as_ptr();
- unveil(path_ptr, perm_ptr).map_err(UnveilErr::Io)
- },
- )
- }
-}
-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"
+ // `cap < MAX_CAP` and
+ // `2 * (MAX_CAP - 1) < isize::MAX`, so overflow is not possible.
+ buffer.reserve(cap << 1);
+ cap = buffer.capacity();
} else {
- ""
- },
- )
- }
-}
-impl BitAnd for Permissions {
- type Output = Self;
- #[inline]
- fn bitand(self, rhs: Self) -> Self::Output {
- Self(self.0 & rhs.0)
+ return Err(Error::from_raw_os_error(code));
+ }
+ }
}
-}
-impl BitAnd<&Self> for Permissions {
- type Output = Self;
+ /// 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]
- fn bitand(self, rhs: &Self) -> Self::Output {
- self & *rhs
+ 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))
}
-}
-impl BitAnd<&Permissions> for &Permissions {
- type Output = Permissions;
+ /// 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]
- fn bitand(self, rhs: &Permissions) -> Self::Output {
- *self & *rhs
+ pub fn setresid(self) -> Result<(), Error> {
+ self.gid.setresgid().and_then(|()| self.uid.setresuid())
}
-}
-impl BitAnd<Permissions> for &Permissions {
- type Output = Permissions;
+ /// 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]
- fn bitand(self, rhs: Permissions) -> Self::Output {
- *self & rhs
+ 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)
+ }
+ })
+ })
}
-}
-impl BitAndAssign for Permissions {
+ /// 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]
- fn bitand_assign(&mut self, rhs: Self) {
- self.0 &= rhs.0;
+ 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))
+ }
+ })
+ })
}
-}
-impl BitAndAssign<&Self> for Permissions {
+ /// 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]
- fn bitand_assign(&mut self, rhs: &Self) {
- *self &= *rhs;
+ 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)),
+ }
}
-}
-impl BitOr for Permissions {
- type Output = Self;
+ /// 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]
- fn bitor(self, rhs: Self) -> Self::Output {
- Self(self.0 | rhs.0)
+ 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))
+ })
+ })
}
-}
-impl BitOr<&Self> for Permissions {
- type Output = Self;
+ /// 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]
- fn bitor(self, rhs: &Self) -> Self::Output {
- self | *rhs
+ 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 BitOr<&Permissions> for &Permissions {
- type Output = Permissions;
+impl PartialEq<&Self> for UserInfo {
#[inline]
- fn bitor(self, rhs: &Permissions) -> Self::Output {
- *self | *rhs
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
}
}
-impl BitOr<Permissions> for &Permissions {
- type Output = Permissions;
+impl PartialEq<UserInfo> for &UserInfo {
#[inline]
- fn bitor(self, rhs: Permissions) -> Self::Output {
- *self | rhs
+ fn eq(&self, other: &UserInfo) -> bool {
+ **self == *other
}
}
-impl BitOrAssign for Permissions {
- #[inline]
- fn bitor_assign(&mut self, rhs: Self) {
- self.0 |= rhs.0;
+#[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();
}
-}
-impl BitOrAssign<&Self> for Permissions {
- #[inline]
- fn bitor_assign(&mut self, rhs: &Self) {
- *self |= *rhs;
+ #[test]
+ fn test_geteuid() {
+ _ = Uid::geteuid();
}
-}
-impl BitXor for Permissions {
- type Output = Self;
- #[inline]
- fn bitxor(self, rhs: Self) -> Self::Output {
- Self(self.0 ^ rhs.0)
+ #[test]
+ fn test_getgid() {
+ _ = Gid::getgid();
}
-}
-impl BitXor<&Self> for Permissions {
- type Output = Self;
- #[inline]
- fn bitxor(self, rhs: &Self) -> Self::Output {
- self ^ *rhs
+ #[test]
+ fn test_getegid() {
+ _ = Gid::getegid();
}
-}
-impl BitXor<&Permissions> for &Permissions {
- type Output = Permissions;
- #[inline]
- fn bitxor(self, rhs: &Permissions) -> Self::Output {
- *self ^ *rhs
+ #[test]
+ fn test_setresuid() -> Result<(), Error> {
+ Uid::geteuid().setresuid()
}
-}
-impl BitXor<Permissions> for &Permissions {
- type Output = Permissions;
- #[inline]
- fn bitxor(self, rhs: Permissions) -> Self::Output {
- *self ^ rhs
+ #[test]
+ fn test_setresgid() -> Result<(), Error> {
+ Gid::getegid().setresgid()
}
-}
-impl BitXorAssign for Permissions {
- #[inline]
- fn bitxor_assign(&mut self, rhs: Self) {
- self.0 ^= rhs.0;
+ #[test]
+ fn test_user_info_new() -> Result<(), NulOrIoErr> {
+ if let Some(user) = UserInfo::new("root")? {
+ assert!(user.is_root());
+ }
+ Ok(())
}
-}
-impl BitXorAssign<&Self> for Permissions {
- #[inline]
- fn bitxor_assign(&mut self, rhs: &Self) {
- *self ^= *rhs;
+ #[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(())
}
-}
-impl Not for Permissions {
- type Output = Self;
- #[inline]
- fn not(self) -> Self::Output {
- Self(Self::ALL.0 & !self.0)
+ #[test]
+ fn test_user_info_setresid() -> Result<(), Error> {
+ UserInfo {
+ uid: Uid::geteuid(),
+ gid: Gid::getegid(),
+ }
+ .setresid()
}
-}
-impl Not for &Permissions {
- type Output = Permissions;
- #[inline]
- fn not(self) -> Self::Output {
- !*self
+ #[test]
+ fn test_user_info_setresid_if_exists() -> Result<(), SetresidErr> {
+ UserInfo {
+ uid: Uid::geteuid(),
+ gid: Gid::getegid(),
+ }
+ .setresid_if_valid()
}
-}
-impl PartialEq<&Self> for Permissions {
- #[inline]
- fn eq(&self, other: &&Self) -> bool {
- *self == **other
+ #[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)
+ );
}
-}
-impl PartialEq<Permissions> for &Permissions {
- #[inline]
- fn eq(&self, other: &Permissions) -> bool {
- **self == *other
+ #[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(())
+ }
}
-}
-/// Error returned by [`Permissions::unveil`].
-#[derive(Debug)]
-pub enum UnveilErr {
- /// Error propagated from [`unveil(2)`](https://man.openbsd.org/amd64/unveil.2).
- Io(io::Error),
- /// Error when a path cannot be converted into a
- /// [`CString`].
- Nul(NulError),
-}
-impl Display for UnveilErr {
- #[inline]
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- match *self {
- Self::Io(ref err) => err.fmt(f),
- Self::Nul(ref e) => write!(
- f,
- "The path passed to 'unveil(2)' was unable to be converted to a CString: {e}"
- ),
+ #[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(())
}
}
-}
-impl error::Error for UnveilErr {}
-/// Invokes [`unveil(2)`](https://man.openbsd.org/amd64/unveil.2) by passing `NULL` for both `path` and `permissions`.
-///
-/// # Errors
-///
-/// Returns [`io::Error`] when a problem occurs.
-///
-/// # Example
-///
-/// ```no_run
-/// use priv_sep;
-/// assert!(priv_sep::unveil_no_more().is_ok());
-/// ```
-#[inline]
-pub fn unveil_no_more() -> Result<(), io::Error> {
- // `NULL` is valid for both `path` and `permissions`.
- unveil(ptr::null(), ptr::null())
-}
-#[cfg(all(test, target_os = "openbsd"))]
-mod tests {
- use crate::{Permissions, Promise, Promises};
- use std::fs;
- // We only have one test since we must force the order of pledge/unveil calls.
+ #[cfg(target_os = "openbsd")]
#[test]
#[ignore]
- fn test() {
+ 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());
@@ -1148,7 +1062,7 @@ mod tests {
),
);
// This tests that a NULL `promise` does nothing.
- assert!(crate::pledge_none().is_ok());
+ 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.
@@ -1188,7 +1102,7 @@ mod tests {
// 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!(crate::unveil_no_more().is_ok());
+ 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.
@@ -1205,4 +1119,103 @@ mod tests {
// 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/src/openbsd.rs