priv_sep

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

commit 4d9d228407d2ebc3a045af3409aa6d58dba61ef6
parent 49e56b11b5fa96201214cfb342a97321d948ffdd
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon, 19 May 2025 13:01:54 -0600

fix repo files

Diffstat:
MCargo.toml | 63+++++++++++++++++++++++++++++++++++++++++++++++----------------
MREADME.md | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Dpriv_sep/.gitignore | 2--
Dpriv_sep/Cargo.toml | 60------------------------------------------------------------
Dpriv_sep/LICENSE-APACHE | 177-------------------------------------------------------------------------------
Dpriv_sep/LICENSE-MIT | 20--------------------
Dpriv_sep/README.md | 125-------------------------------------------------------------------------------
Dpriv_sep/src/lib.rs | 1221------------------------------------------------------------------------------
Rpriv_sep/src/c.rs -> src/c.rs | 0
Msrc/lib.rs | 1935++++++++++++++++++++++++++++++++++++++++---------------------------------------
Rpriv_sep/src/openbsd.rs -> src/openbsd.rs | 0
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)&ensp;[![crates-io]](https://crates.io/crates/priv_sep)&ensp;[![docs-rs]](crate) -//! -//! [git]: https://git.philomathiclife.com/git_badge.svg -//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust -//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs -//! -//! `priv_sep` is a library that uses the system's libc to perform privilege separation and privilege reduction. -//! -//! ## `priv_sep` in action for OpenBSD -//! -//! ```no_run -//! use core::convert::Infallible; -//! # #[cfg(target_os = "openbsd")] -//! use priv_sep::{Permissions, PrivDropErr, Promise, Promises}; -//! use std::{ -//! fs, -//! io::Error, -//! net::{Ipv6Addr, SocketAddrV6}, -//! }; -//! use tokio::net::TcpListener; -//! # #[cfg(not(target_os = "openbsd"))] -//! # fn main() {} -//! # #[cfg(target_os = "openbsd")] -//! #[tokio::main(flavor = "current_thread")] -//! async fn main() -> Result<Infallible, PrivDropErr<Error>> { -//! /// Config file. -//! const CONFIG: &str = "config"; -//! // Get the user ID and group ID for nobody from `passwd(5)`. -//! // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. -//! // `pledge(2)` `id`, `inet`, `rpath`, `stdio`, and `unveil`. -//! // Bind to TCP `[::1]:443` as root. -//! // `setresgid(2)` to the group ID associated with nobody. -//! // `setresuid(2)` to the user ID associated with nobody. -//! // Remove `id` from our `pledge(2)`d promises. -//! let (listener, mut promises) = Promises::new_chroot_then_priv_drop_async( -//! "nobody", -//! "/path/chroot/", -//! [Promise::Inet, Promise::Rpath, Promise::Unveil], -//! false, -//! async || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await, -//! ).await?; -//! // At this point, the process is running under nobody. -//! // Only allow file system access to `config` and only allow read access to it. -//! Permissions::READ.unveil(CONFIG)?; -//! // Read `config`. -//! // This will of course fail if the file does not exist or nobody does not -//! // have read permissions. -//! let config = fs::read(CONFIG)?; -//! // Remove file system access. -//! Permissions::NONE.unveil(CONFIG)?; -//! // Remove `rpath` and `unveil` from our `pledge(2)`d promises -//! // (i.e., only have `inet` and `stdio` abilities when we begin accepting TCP connections). -//! promises.remove_promises_then_pledge([Promise::Rpath, Promise::Unveil])?; -//! loop { -//! // Handle TCP connections. -//! if let Ok((_, ip)) = listener.accept().await { -//! assert!(ip.is_ipv6()); -//! } -//! } -//! } -//! ``` -//! -//! ## `priv_sep` in action for Unix-like OSes -//! -//! ```no_run -//! use core::convert::Infallible; -//! use priv_sep::{UserInfo, PrivDropErr}; -//! use std::{ -//! io::Error, -//! net::{Ipv6Addr, SocketAddrV6}, -//! }; -//! use tokio::net::TcpListener; -//! #[tokio::main(flavor = "current_thread")] -//! async fn main() -> Result<Infallible, PrivDropErr<Error>> { -//! // Get the user ID and group ID for nobody from `passwd(5)`. -//! // `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`. -//! // Bind to TCP `[::1]:443` as root. -//! // `setresgid(2)` to the group ID associated with nobody. -//! // `setresuid(2)` to the user ID associated with nobody. -//! let listener = UserInfo::chroot_then_priv_drop_async("nobody", "/path/chroot/", false, async || { -//! TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await -//! }).await?; -//! // At this point, the process is running under nobody. -//! loop { -//! // Handle TCP connections. -//! if let Ok((_, ip)) = listener.accept().await { -//! assert!(ip.is_ipv6()); -//! } -//! } -//! } -//! ``` -#![cfg_attr(docsrs, feature(doc_cfg))] -#![allow(clippy::pub_use, reason = "don't want openbsd types in a module")] -extern crate alloc; -/// C FFI. -mod c; -/// OpenBSD -#[cfg(any(doc, target_os = "openbsd"))] -mod openbsd; -use alloc::ffi::{CString, NulError}; -use c::{IdT, SUCCESS}; -use core::{ - error::Error as CoreErr, - ffi::{CStr, c_char, c_int}, - fmt::{self, Display, Formatter}, - mem::MaybeUninit, - ptr, -}; -#[cfg_attr(docsrs, doc(cfg(target_os = "openbsd")))] -#[cfg(any(doc, target_os = "openbsd"))] -pub use openbsd::{Permission, Permissions, Promise, Promises}; -use std::{io::Error, os::unix::ffi::OsStrExt as _, path::Path}; -/// [`uid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct Uid(pub IdT); -impl Uid { - /// The root user ID (i.e., 0). - pub const ROOT: Self = Self(0); - /// Returns `true` iff `self` is [`Self::ROOT`]. - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::Uid; - /// assert!(Uid::ROOT.is_root()); - /// ``` - #[inline] - #[must_use] - pub const fn is_root(self) -> bool { - self.0 == Self::ROOT.0 - } - /// [`getuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getuid.html). - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::Uid; - /// assert_eq!(Uid::getuid(), 1000); - /// ``` - #[inline] - #[must_use] - pub fn getuid() -> Self { - Self(c::getuid()) - } - /// [`geteuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/geteuid.html). - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::Uid; - /// assert_eq!(Uid::geteuid(), 1000); - /// ``` - #[inline] - #[must_use] - pub fn geteuid() -> Self { - Self(c::geteuid()) - } - /// Calls [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html) - /// passing `self` for the real, effective, and saved user IDs. - /// - /// # Errors - /// - /// Errors iff `setresuid` does. - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::Uid; - /// assert!(Uid(1000).setresuid().is_ok()); - /// ``` - #[inline] - pub fn setresuid(self) -> Result<(), Error> { - if c::setresuid(self.0, self.0, self.0) == SUCCESS { - Ok(()) - } else { - Err(Error::last_os_error()) - } - } -} -impl PartialEq<&Self> for Uid { - #[inline] - fn eq(&self, other: &&Self) -> bool { - *self == **other - } -} -impl PartialEq<Uid> for &Uid { - #[inline] - fn eq(&self, other: &Uid) -> bool { - **self == *other - } -} -impl PartialEq<IdT> for Uid { - #[inline] - fn eq(&self, other: &IdT) -> bool { - self.0 == *other - } -} -impl From<Uid> for IdT { - #[inline] - fn from(value: Uid) -> Self { - value.0 - } -} -impl From<IdT> for Uid { - #[inline] - fn from(value: IdT) -> Self { - Self(value) - } -} -/// [`gid_t`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/sys_types.h.html). -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct Gid(pub IdT); -impl Gid { - /// [`getgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getgid.html). - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::Gid; - /// assert_eq!(Gid::getgid(), 1000); - /// ``` - #[inline] - #[must_use] - pub fn getgid() -> Self { - Self(c::getgid()) - } - /// [`getegid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getegid.html). - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::Gid; - /// assert_eq!(Gid::getegid(), 1000); - /// ``` - #[inline] - #[must_use] - pub fn getegid() -> Self { - Self(c::getegid()) - } - /// Calls [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html) - /// passing `self` for the real, effective, and saved group IDs. - /// - /// # Errors - /// - /// Errors iff `setresgid` does. - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::Gid; - /// assert!(Gid(1000).setresgid().is_ok()); - /// ``` - #[inline] - pub fn setresgid(self) -> Result<(), Error> { - if c::setresgid(self.0, self.0, self.0) == SUCCESS { - Ok(()) - } else { - Err(Error::last_os_error()) - } - } -} -impl PartialEq<&Self> for Gid { - #[inline] - fn eq(&self, other: &&Self) -> bool { - *self == **other - } -} -impl PartialEq<Gid> for &Gid { - #[inline] - fn eq(&self, other: &Gid) -> bool { - **self == *other - } -} -impl PartialEq<IdT> for Gid { - #[inline] - fn eq(&self, other: &IdT) -> bool { - self.0 == *other - } -} -impl From<Gid> for IdT { - #[inline] - fn from(value: Gid) -> Self { - value.0 - } -} -impl From<IdT> for Gid { - #[inline] - fn from(value: IdT) -> Self { - Self(value) - } -} -/// Error when [`CString::new`] errors or an I/O error occurs due to a libc call. -#[derive(Debug)] -pub enum NulOrIoErr { - /// Error returned from [`CString::new`]. - Nul(NulError), - /// Generic I/O error returned from a libc call. - Io(Error), -} -impl Display for NulOrIoErr { - #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match *self { - Self::Nul(ref err) => write!(f, "CString could not be created: {err}"), - Self::Io(ref err) => write!(f, "libc I/O error: {err}"), - } - } -} -impl CoreErr for NulOrIoErr {} -impl From<NulError> for NulOrIoErr { - #[inline] - fn from(value: NulError) -> Self { - Self::Nul(value) - } -} -impl From<Error> for NulOrIoErr { - #[inline] - fn from(value: Error) -> Self { - Self::Io(value) - } -} -/// [`chroot(2)`](https://manned.org/chroot.2). -/// -/// # Errors -/// -/// Returns [`NulError`] iff [`CString::new`] does. -/// Returns [`Error`] iff `chroot(2)` errors. -/// -/// # Examples -/// -/// ```no_run -/// assert!(priv_sep::chroot("./").is_ok()); -/// ``` -#[expect(unsafe_code, reason = "chroot(2) takes a pointer")] -#[inline] -pub fn chroot<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { - CString::new(path.as_ref().as_os_str().as_bytes()) - .map_err(NulOrIoErr::Nul) - .and_then(|c_path| { - let ptr = c_path.as_ptr(); - // SAFETY: - // `ptr` is valid and not null. - if unsafe { c::chroot(ptr) } == SUCCESS { - Ok(()) - } else { - Err(NulOrIoErr::Io(Error::last_os_error())) - } - }) -} -/// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). -/// -/// This function MUST only be called by `chdir` and `chroot_then_chdir`. -#[expect(unsafe_code, reason = "chdir(2) takes a pointer")] -fn private_chdir(path: *const c_char) -> Result<(), Error> { - // SAFETY: - // `path` is valid and not null as can be seen in the only functions that call this function: - // `chdir` and `chroot_then_chdir`. - if unsafe { c::chdir(path) } == SUCCESS { - Ok(()) - } else { - Err(Error::last_os_error()) - } -} -/// [`chdir`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/chdir.html). -/// -/// # Errors -/// -/// Returns [`NulError`] iff [`CString::new`] does. -/// Returns [`Error`] iff `chdir` errors. -/// -/// # Examples -/// -/// ```no_run -/// assert!(priv_sep::chdir("/").is_ok()); -/// ``` -#[inline] -pub fn chdir<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { - CString::new(path.as_ref().as_os_str().as_bytes()) - .map_err(NulOrIoErr::Nul) - .and_then(|c_path| private_chdir(c_path.as_ptr()).map_err(NulOrIoErr::Io)) -} -/// Calls [`chroot`] on `path` followed by a call to [`chdir`] on `"/"`. -/// -/// # Errors -/// -/// Errors iff `chroot` or `chdir` do. -/// -/// # Examples -/// -/// ```no_run -/// assert!(priv_sep::chroot_then_chdir("./").is_ok()); -/// ``` -#[inline] -pub fn chroot_then_chdir<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { - /// Root directory. - const ROOT: *const c_char = c"/".as_ptr(); - chroot(path).and_then(|()| private_chdir(ROOT).map_err(NulOrIoErr::Io)) -} -/// Error returned when dropping privileges. -#[derive(Debug)] -pub enum PrivDropErr<E> { - /// Error when [`CString::new`] errors. - Nul(NulError), - /// Error when an I/O error occurs from a libc call. - Io(Error), - /// Error when there is no entry in the user database corresponding to the passed username. - NoPasswdEntry, - /// Error when [`UserInfo::is_root`]. - RootEntry, - /// Error returned from the user-provided function that is invoked before calling [`UserInfo::setresid`]. - Other(E), -} -impl<E: Display> Display for PrivDropErr<E> { - #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match *self { - Self::Nul(ref err) => write!( - f, - "CString could not be created from the username to drop privileges to: {err}" - ), - Self::Io(ref err) => write!(f, "libc I/O error when dropping privileges: {err}"), - Self::NoPasswdEntry => f.write_str("no passwd(5) entry to drop privileges to"), - Self::RootEntry => f.write_str( - "setresuid(2) is not allowed to be called on uid 0 when dropping privileges", - ), - Self::Other(ref err) => write!( - f, - "error calling function before dropping privileges: {err}" - ), - } - } -} -impl<E: CoreErr> CoreErr for PrivDropErr<E> {} -impl<E> From<NulError> for PrivDropErr<E> { - #[inline] - fn from(value: NulError) -> Self { - Self::Nul(value) - } -} -impl<E> From<Error> for PrivDropErr<E> { - #[inline] - fn from(value: Error) -> Self { - Self::Io(value) - } -} -impl<E> From<NulOrIoErr> for PrivDropErr<E> { - #[inline] - fn from(value: NulOrIoErr) -> Self { - match value { - NulOrIoErr::Nul(e) => Self::Nul(e), - NulOrIoErr::Io(e) => Self::Io(e), - } - } -} -/// Error returned from [`UserInfo::setresid_if_valid`]. -#[derive(Debug)] -pub enum SetresidErr { - /// Error when an I/O error occurs from a libc call. - Io(Error), - /// Error when there is no entry in the user database corresponding to [`UserInfo::uid`]. - NoPasswdEntry, - /// Error when the entry in the user database has a different gid than [`UserInfo::gid`]. - GidMismatch, -} -impl Display for SetresidErr { - #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match *self { - Self::Io(ref err) => write!(f, "libc I/O error when dropping privileges: {err}"), - Self::NoPasswdEntry => f.write_str("no passwd(5) entry to drop privileges to"), - Self::GidMismatch => f.write_str("gid in passwd(5) does match the expected gid"), - } - } -} -impl CoreErr for SetresidErr {} -impl From<Error> for SetresidErr { - #[inline] - fn from(value: Error) -> Self { - Self::Io(value) - } -} -/// Used by [`UserInfo::getpw_entry`]. -trait PwEntry { - /// Calling code must uphold the following safety invariants: - /// * `buf` must be a valid, initialized, non-null pointer - /// * `size` must be the length of `buf` - /// * `result` must be a valid, initialized non-null pointer referencing a valid and initialized pointer that - /// is allowed to be null. - /// - /// Implementors MUST only _write_ to `pwd` and never read from it (i.e., `pwd` is allowed to be unitialized). - #[expect( - unsafe_code, - reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" - )] - unsafe fn getpw( - self, - pwd: *mut c::Passwd, - buf: *mut c_char, - size: usize, - result: *mut *mut c::Passwd, - ) -> c_int; -} -impl PwEntry for Uid { - #[expect(unsafe_code, reason = "getpwuid_r(3) take in pointers")] - unsafe fn getpw( - self, - pwd: *mut c::Passwd, - buf: *mut c_char, - size: usize, - result: *mut *mut c::Passwd, - ) -> c_int { - // SAFETY: - // Calling code must uphold safety invariants. - // `pwd` is never read from. - unsafe { c::getpwuid_r(self.0, pwd, buf, size, result) } - } -} -/// `newtype` around `CStr`. -#[derive(Clone, Copy)] -struct CStrWrapper<'a>(&'a CStr); -impl PwEntry for CStrWrapper<'_> { - #[expect(unsafe_code, reason = "getpwnam_r(3) takes in pointers")] - unsafe fn getpw( - self, - pwd: *mut c::Passwd, - buf: *mut c_char, - size: usize, - result: *mut *mut c::Passwd, - ) -> c_int { - let ptr = self.0.as_ptr(); - // SAFETY: - // Calling code must uphold safety invariants. - // `ptr` is valid, initialized, and not null. - // `pwd` is never read from. - unsafe { c::getpwnam_r(ptr, pwd, buf, size, result) } - } -} -/// User and group ID. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct UserInfo { - /// The user ID. - pub uid: Uid, - /// The group ID. - pub gid: Gid, -} -impl UserInfo { - /// Returns `true` iff [`Uid::is_root`]. - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::{Gid, Uid, UserInfo}; - /// assert!(UserInfo { uid: Uid::ROOT, gid: Gid(0), }.is_root()); - /// ``` - #[inline] - #[must_use] - pub const fn is_root(self) -> bool { - self.uid.is_root() - } - /// [`getpwnam_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwnam_r.html). - /// - /// Uses `buffer` to write the user database entry into returning `None` iff there is no entry; otherwise - /// returns `Self`. - /// - /// Note it is the caller's responsibility to ensure `buffer` is large enough; otherwise an [`Error`] will - /// be returned. - /// - /// # Errors - /// - /// Returns [`NulError`] iff [`CString::new`] does. - /// Returns [`Error`] iff `getpwnam_r` errors. - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::{Uid, UserInfo}; - /// assert!(UserInfo::with_buffer("root", [0; 128].as_mut_slice())?.map_or(false, |info| info.is_root())); - /// # Ok::<_, priv_sep::NulOrIoErr>(()) - /// ``` - #[expect(unsafe_code, reason = "getpwnam_r(3) takes in pointers")] - #[inline] - pub fn with_buffer<T: Into<Vec<u8>>>( - name: T, - buffer: &mut [c_char], - ) -> Result<Option<Self>, NulOrIoErr> { - CString::new(name).map_err(NulOrIoErr::Nul).and_then(|n| { - let ptr = n.as_ptr(); - let mut pwd = MaybeUninit::<c::Passwd>::uninit(); - let pwd_ptr = pwd.as_mut_ptr(); - let buf_ptr = buffer.as_mut_ptr(); - let len = buffer.len(); - let mut result = ptr::null_mut(); - let res_ptr = &mut result; - // SAFETY: - // `pwd_ptr` is only written to; thus the fact `pwd` is unitialized is fine. - // `buf_ptr` is valid, initialized, and not null. - // `len` is the length of `buf_ptr`. - // `res_ptr` is valid, initialized, and not null. - // `result` is valid, initialized, and allowed to be null. - let code = unsafe { c::getpwnam_r(ptr, pwd_ptr, buf_ptr, len, res_ptr) }; - if code == SUCCESS { - if result.is_null() { - Ok(None) - } else { - // SAFETY: - // `c::getpwnam_r` writes to `pwd` iff `result` is not null. - Ok(Some(unsafe { pwd.assume_init() }.into_user_info())) - } - } else { - Err(NulOrIoErr::Io(Error::from_raw_os_error(code))) - } - }) - } - /// Helper for [`Self::new`] and [`Self::setresid_if_exists`]. - #[expect( - unsafe_code, - reason = "getpwnam_r(3) and getpwuid_r(3) take in pointers" - )] - fn getpw_entry<P: Copy + PwEntry>(u: P) -> Result<Option<Self>, Error> { - /// Initial buffer size. - const INIT_CAP: usize = 128; - // `2 * (MAX_CAP - 1) <= isize::MAX` MUST be true. - /// Maximum buffer size. - const MAX_CAP: usize = 0x4000; - /// [`ERANGE`](https://man.openbsd.org/errno#Result). - const ERANGE: c_int = 34; - let mut buffer = Vec::with_capacity(INIT_CAP); - let mut cap = buffer.capacity(); - let mut pwd = MaybeUninit::<c::Passwd>::uninit(); - let mut result = ptr::null_mut(); - let mut pwd_ptr; - let mut res_ptr; - let mut code; - let mut buf_ptr; - loop { - pwd_ptr = pwd.as_mut_ptr(); - res_ptr = &mut result; - buf_ptr = buffer.as_mut_ptr(); - // SAFETY: - // `pwd_ptr` is only written to; thus the fact `pwd` is unitialized is fine. - // `buf_ptr` is valid, initialized, and not null. - // `cap` is the length of `buf_ptr`. - // `res_ptr` is valid, initialized, and not null. - // `result` is valid, initialized, and allowed to be null. - code = unsafe { u.getpw(pwd_ptr, buf_ptr, cap, res_ptr) }; - if code == SUCCESS { - return Ok(if result.is_null() { - None - } else { - // SAFETY: - // `CStrWrapper::getpw` writes to `pwd` iff `result` is not null. - Some(unsafe { pwd.assume_init() }.into_user_info()) - }); - } else if code == ERANGE { - if cap >= MAX_CAP { - return Err(Error::from_raw_os_error(code)); - } - // `cap < MAX_CAP` and - // `2 * (MAX_CAP - 1) < isize::MAX`, so overflow is not possible. - buffer.reserve(cap << 1); - cap = buffer.capacity(); - } else { - return Err(Error::from_raw_os_error(code)); - } - } - } - /// Same as [`Self::with_buffer`] except repeated attempts are made with progressively larger buffers up to - /// 16 KiB. - /// - /// # Errors - /// - /// Errors iff [`Self::with_buffer`] does for a 16 KiB buffer. - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::UserInfo; - /// assert!(UserInfo::new("root")?.map_or(false, |info| info.is_root())); - /// # Ok::<_, priv_sep::NulOrIoErr>(()) - /// ``` - #[inline] - pub fn new<T: Into<Vec<u8>>>(name: T) -> Result<Option<Self>, NulOrIoErr> { - CString::new(name) - .map_err(NulOrIoErr::Nul) - .and_then(|n| Self::getpw_entry(CStrWrapper(n.as_c_str())).map_err(NulOrIoErr::Io)) - } - /// Calls [`Gid::setresgid`] and [`Uid::setresuid`]. - /// - /// # Errors - /// - /// Errors iff `Gid::setresgid` or `Uid::setresuid` error. - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::UserInfo; - /// if let Some(user) = UserInfo::new("nobody")? { - /// user.setresid()?; - /// } - /// # Ok::<_, priv_sep::NulOrIoErr>(()) - /// ``` - #[inline] - pub fn setresid(self) -> Result<(), Error> { - self.gid.setresgid().and_then(|()| self.uid.setresuid()) - } - /// Same as [`Self::setresid`] except - /// [`getpwuid_r`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/getpwuid_r.html) - /// is used to first confirm the existence of [`Self::uid`] and [`Self::gid`]. - /// - /// Note this should rarely be used since most will rely on [`Self::new`], [`Self::with_buffer`], - /// [`Self::priv_drop`], or [`Self::chroot_then_priv_drop`]. - /// - /// Like [`Self::new`], this will fail if the buffer needed exceeds 16 KiB. - /// - /// # Errors - /// - /// Errors iff `getpwuid_r` errors for a 16 KiB buffer, [`Self::uid`] and [`Self::gid`] don't exist in the user - /// database, [`Gid::setresgid`] errors, or [`Uid::setresuid`] errors. - /// - /// # Examples - /// - /// ```no_run - /// # use priv_sep::{Gid, Uid, UserInfo}; - /// UserInfo { uid: Uid(1000), gid: Gid(1000), }.setresid_if_valid()?; - /// # Ok::<_, priv_sep::SetresidErr>(()) - /// ``` - #[inline] - pub fn setresid_if_valid(self) -> Result<(), SetresidErr> { - Self::getpw_entry(self.uid) - .map_err(SetresidErr::Io) - .and_then(|opt| { - opt.ok_or(SetresidErr::NoPasswdEntry).and_then(|info| { - if info.gid == self.gid { - self.setresid().map_err(SetresidErr::Io) - } else { - Err(SetresidErr::GidMismatch) - } - }) - }) - } - /// Calls [`Self::new`], invokes `f`, then calls [`Self::setresid`]. - /// - /// Dropping privileges is necessary when needing to perform certain actions as root before no longer needing - /// such abilities; at which point, one calls - /// [`setresgid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresgid.html) and - /// [`setresuid`](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/setresuid.html) - /// using a lesser privileged gid and uid. - /// - /// # Errors - /// - /// Errors iff [`Self::new`], `f`, or [`Self::setresid`] do or there is no entry in the user database - /// corresponding to `name` or the entry has uid 0. - /// - /// # Examples - /// - /// ```no_run - /// # use core::net::{Ipv6Addr, SocketAddrV6}; - /// # use priv_sep::{PrivDropErr, UserInfo}; - /// # use std::{io::Error, net::TcpListener}; - /// let listener = UserInfo::priv_drop("nobody", || { - /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) - /// })?; - /// # Ok::<_, PrivDropErr<Error>>(()) - /// ``` - #[inline] - pub fn priv_drop<U: Into<Vec<u8>>, T, E, F: FnOnce() -> Result<T, E>>( - name: U, - f: F, - ) -> Result<T, PrivDropErr<E>> { - Self::new(name).map_err(PrivDropErr::from).and_then(|opt| { - opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) - .and_then(|info| { - if info.is_root() { - Err(PrivDropErr::RootEntry) - } else { - f().map_err(PrivDropErr::Other) - .and_then(|res| info.setresid().map_err(PrivDropErr::Io).map(|()| res)) - } - }) - }) - } - /// Same as [`Self::priv_drop`] except `f` is `async`. - /// - /// # Errors - /// - /// Read [`Self::priv_drop`]. - /// - /// # Examples - /// - /// ```no_run - /// # use core::net::{Ipv6Addr, SocketAddrV6}; - /// # use priv_sep::UserInfo; - /// # use tokio::net::TcpListener; - /// let listener_fut = UserInfo::priv_drop_async("nobody", async || { - /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await - /// }); - /// ``` - #[inline] - pub async fn priv_drop_async<U: Into<Vec<u8>>, T, E, F: AsyncFnOnce() -> Result<T, E>>( - name: U, - f: F, - ) -> Result<T, PrivDropErr<E>> { - match Self::new(name) { - Ok(opt) => match opt { - None => Err(PrivDropErr::NoPasswdEntry), - Some(info) => { - if info.is_root() { - Err(PrivDropErr::RootEntry) - } else { - f().await - .map_err(PrivDropErr::Other) - .and_then(|res| info.setresid().map_err(PrivDropErr::Io).map(|()| res)) - } - } - }, - Err(err) => Err(PrivDropErr::from(err)), - } - } - /// Same as [`Self::priv_drop`] except [`chroot_then_chdir`] is called before or after invoking `f` based on - /// `chroot_after_f`. - /// - /// # Errors - /// - /// Errors iff [`Self::priv_drop`] or [`chroot_then_chdir`] do. - /// - /// # Examples - /// - /// ```no_run - /// # use core::net::{Ipv6Addr, SocketAddrV6}; - /// # use priv_sep::{PrivDropErr, UserInfo}; - /// # use std::{io::Error, net::TcpListener}; - /// let listener = UserInfo::chroot_then_priv_drop("nobody", "./", false, || { - /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) - /// })?; - /// # Ok::<_, PrivDropErr<Error>>(()) - /// ``` - #[inline] - pub fn chroot_then_priv_drop< - U: Into<Vec<u8>>, - P: AsRef<Path>, - T, - E, - F: FnOnce() -> Result<T, E>, - >( - name: U, - path: P, - chroot_after_f: bool, - f: F, - ) -> Result<T, PrivDropErr<E>> { - Self::new(name).map_err(PrivDropErr::from).and_then(|opt| { - opt.ok_or_else(|| PrivDropErr::NoPasswdEntry) - .and_then(|info| { - if info.is_root() { - Err(PrivDropErr::RootEntry) - } else if chroot_after_f { - f().map_err(PrivDropErr::Other).and_then(|res| { - chroot_then_chdir(path) - .map_err(PrivDropErr::from) - .map(|()| res) - }) - } else { - chroot_then_chdir(path) - .map_err(PrivDropErr::from) - .and_then(|()| f().map_err(PrivDropErr::Other)) - } - .and_then(|res| info.setresid().map_err(PrivDropErr::Io).map(|()| res)) - }) - }) - } - /// Same as [`Self::chroot_then_priv_drop`] except `f` is `async`. - /// - /// # Errors - /// - /// Read [`Self::chroot_then_priv_drop`]. - /// - /// # Examples - /// - /// ```no_run - /// # use core::net::{Ipv6Addr, SocketAddrV6}; - /// # use priv_sep::UserInfo; - /// # use tokio::net::TcpListener; - /// let listener_fut = UserInfo::chroot_then_priv_drop_async("nobody", "./", false, async || { - /// TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await - /// }); - /// ``` - #[inline] - pub async fn chroot_then_priv_drop_async< - U: Into<Vec<u8>>, - P: AsRef<Path>, - T, - E, - F: AsyncFnOnce() -> Result<T, E>, - >( - name: U, - path: P, - chroot_after_f: bool, - f: F, - ) -> Result<T, PrivDropErr<E>> { - match Self::new(name) { - Ok(opt) => match opt { - None => Err(PrivDropErr::NoPasswdEntry), - Some(info) => if info.is_root() { - Err(PrivDropErr::RootEntry) - } else if chroot_after_f { - f().await.map_err(PrivDropErr::Other).and_then(|res| { - chroot_then_chdir(path) - .map_err(PrivDropErr::from) - .map(|()| res) - }) - } else { - match chroot_then_chdir(path) { - Ok(()) => f().await.map_err(PrivDropErr::Other), - Err(err) => Err(PrivDropErr::from(err)), - } - } - .and_then(|res| info.setresid().map_err(PrivDropErr::Io).map(|()| res)), - }, - Err(err) => Err(PrivDropErr::from(err)), - } - } -} -impl PartialEq<&Self> for UserInfo { - #[inline] - fn eq(&self, other: &&Self) -> bool { - *self == **other - } -} -impl PartialEq<UserInfo> for &UserInfo { - #[inline] - fn eq(&self, other: &UserInfo) -> bool { - **self == *other - } -} -#[cfg(test)] -mod tests { - use super::{Gid, NulOrIoErr, PrivDropErr, SetresidErr, Uid, UserInfo}; - #[cfg(target_os = "openbsd")] - use super::{Permissions, Promise, Promises}; - use core::net::{Ipv6Addr, SocketAddrV6}; - use std::{fs, io::Error, net::TcpListener}; - const README: &str = "README.md"; - #[test] - fn test_getuid() { - _ = Uid::getuid(); - } - #[test] - fn test_geteuid() { - _ = Uid::geteuid(); - } - #[test] - fn test_getgid() { - _ = Gid::getgid(); - } - #[test] - fn test_getegid() { - _ = Gid::getegid(); - } - #[test] - fn test_setresuid() -> Result<(), Error> { - Uid::geteuid().setresuid() - } - #[test] - fn test_setresgid() -> Result<(), Error> { - Gid::getegid().setresgid() - } - #[test] - fn test_user_info_new() -> Result<(), NulOrIoErr> { - if let Some(user) = UserInfo::new("root")? { - assert!(user.is_root()); - } - Ok(()) - } - #[test] - fn test_user_info_with_buffer() -> Result<(), NulOrIoErr> { - if let Some(user) = UserInfo::with_buffer("root", [0; 512].as_mut_slice())? { - assert!(user.is_root()); - } - Ok(()) - } - #[test] - fn test_user_info_setresid() -> Result<(), Error> { - UserInfo { - uid: Uid::geteuid(), - gid: Gid::getegid(), - } - .setresid() - } - #[test] - fn test_user_info_setresid_if_exists() -> Result<(), SetresidErr> { - UserInfo { - uid: Uid::geteuid(), - gid: Gid::getegid(), - } - .setresid_if_valid() - } - #[test] - fn test_user_info_setresid_if_exists_failure() { - assert!( - UserInfo { - uid: Uid::geteuid(), - gid: Gid(u32::MAX), - } - .setresid_if_valid() - .map_or_else(|e| matches!(e, SetresidErr::GidMismatch), |_| false) - ); - } - #[test] - #[ignore] - fn test_priv_drop() -> Result<(), PrivDropErr<Error>> { - if Uid::geteuid().is_root() { - UserInfo::priv_drop("zack", || { - TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) - }) - .map(|_| { - assert!( - TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)).is_err() - ); - }) - } else { - assert!( - UserInfo::priv_drop("root", || Ok::<_, Error>(())) - .map_or_else(|e| matches!(e, PrivDropErr::RootEntry), |_| false) - ); - Ok(()) - } - } - #[test] - #[ignore] - fn test_chroot_priv_drop() -> Result<(), PrivDropErr<Error>> { - if Uid::geteuid().is_root() { - UserInfo::chroot_then_priv_drop("zack", "./", false, || { - TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)) - }) - .and_then(|_| { - fs::exists(README).map_err(PrivDropErr::Io).map(|exists| { - assert!(exists); - assert!( - TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)) - .is_err() - ); - }) - }) - } else { - Ok(()) - } - } - #[cfg(target_os = "openbsd")] - #[test] - #[ignore] - fn test_pledge_unveil() { - const FILE_EXISTS: &str = "/home/zack/foo.txt"; - _ = fs::metadata(FILE_EXISTS) - .expect(format!("{FILE_EXISTS} does not exist, so unit testing cannot occur").as_str()); - const FILE_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23"; - drop(fs::metadata(FILE_NOT_EXISTS).expect_err( - format!("{FILE_NOT_EXISTS} exists, so unit testing cannot occur").as_str(), - )); - const DIR_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23/"; - drop( - fs::metadata(DIR_NOT_EXISTS).expect_err( - format!("{DIR_NOT_EXISTS} exists, so unit testing cannot occur").as_str(), - ), - ); - // This tests that a NULL `promise` does nothing. - assert!(Promises::pledge_none().is_ok()); - print!(""); - assert!(Promises::ALL.pledge().is_ok()); - // This tests that duplicates are ignored as well as the implementation of PartialEq. - let mut initial_promises = Promises::new([ - Promise::Stdio, - Promise::Unveil, - Promise::Rpath, - Promise::Stdio, - ]); - assert!(initial_promises.len() == 3); - assert!( - initial_promises == Promises::new([Promise::Rpath, Promise::Stdio, Promise::Unveil]) - ); - // Test retain. - assert!({ - let mut vals = Promises::new([ - Promise::Audio, - Promise::Bpf, - Promise::Chown, - Promise::Cpath, - Promise::Error, - Promise::Exec, - ]); - vals.retain([Promise::Error, Promise::Chown]); - vals.len() == 2 && vals.contains(Promise::Chown) && vals.contains(Promise::Error) - }); - assert!(initial_promises.pledge().is_ok()); - // This tests unveil with no permissions. - assert!(Permissions::NONE.unveil(FILE_EXISTS).is_ok()); - assert!(fs::metadata(FILE_EXISTS).is_err()); - // This tests unveil with read permissions, - // and one can unveil more permissions (unlike pledge which can only remove promises). - assert!(Permissions::READ.unveil(FILE_EXISTS).is_ok()); - assert!(fs::metadata(FILE_EXISTS).is_ok()); - // This tests that calls to unveil on missing files don't error. - assert!(Permissions::NONE.unveil(FILE_NOT_EXISTS).is_ok()); - // This tests that calls to unveil on missing directories error. - assert!(Permissions::NONE.unveil(DIR_NOT_EXISTS).is_err()); - // This tests that unveil can no longer be called. - assert!(Permissions::unveil_no_more().is_ok()); - assert!(Permissions::NONE.unveil(FILE_EXISTS).is_err()); - assert!(fs::metadata(FILE_EXISTS).is_ok()); - // The below tests that Promises can only be removed and not added. - initial_promises.remove_promises([Promise::Unveil]); - assert_eq!(initial_promises.len(), 2); - initial_promises.remove(Promise::Rpath); - assert_eq!(initial_promises.len(), 1); - initial_promises.remove(Promise::Rpath); - assert_eq!(initial_promises.len(), 1); - assert!(initial_promises.pledge().is_ok()); - print!(""); - assert!(Promises::new([Promise::Rpath]).pledge().is_err()); - // If the below is uncommented, the program should crash since the above - // call to pledge no longer allows access to the file system. - // drop(fs::metadata(FILE_EXISTS)); - } - #[cfg(target_os = "openbsd")] - #[test] - #[ignore] - fn test_pledge_priv_drop() -> Result<(), PrivDropErr<Error>> { - if Uid::geteuid().is_root() { - Promises::new_priv_drop( - "zack", - [Promise::Inet, Promise::Rpath, Promise::Unveil], - false, - || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)), - ) - .and_then(|(_, mut promises)| { - Permissions::READ - .unveil(README) - .map_err(PrivDropErr::from) - .and_then(|()| { - fs::exists(README) - .map_err(PrivDropErr::Io) - .and_then(|exists| { - Permissions::NONE - .unveil(README) - .map_err(PrivDropErr::from) - .and_then(|()| { - promises - .remove_promises_then_pledge([ - Promise::Rpath, - Promise::Unveil, - ]) - .map_err(PrivDropErr::Io) - .map(|()| { - assert!(exists); - assert!( - TcpListener::bind(SocketAddrV6::new( - Ipv6Addr::LOCALHOST, - 80, - 0, - 0 - )) - .is_err() - ); - }) - }) - }) - }) - }) - } else { - Ok(()) - } - } - #[cfg(target_os = "openbsd")] - #[test] - #[ignore] - fn test_pledge_chroot_priv_drop() -> Result<(), PrivDropErr<Error>> { - if Uid::geteuid().is_root() { - Promises::new_chroot_then_priv_drop( - "zack", - "./", - [Promise::Inet, Promise::Rpath, Promise::Unveil], - false, - || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)), - ) - .and_then(|(_, mut promises)| { - Permissions::READ - .unveil(README) - .map_err(PrivDropErr::from) - .and_then(|()| { - fs::exists(README) - .map_err(PrivDropErr::Io) - .and_then(|exists| { - Permissions::NONE - .unveil(README) - .map_err(PrivDropErr::from) - .and_then(|()| { - promises - .remove_promises_then_pledge([ - Promise::Rpath, - Promise::Unveil, - ]) - .map_err(PrivDropErr::Io) - .map(|()| { - assert!(exists); - assert!( - TcpListener::bind(SocketAddrV6::new( - Ipv6Addr::LOCALHOST, - 80, - 0, - 0 - )) - .is_err() - ); - }) - }) - }) - }) - }) - } else { - Ok(()) - } - } -} diff --git a/priv_sep/src/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