priv_sep

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

commit b98aa5f5f3dbb448eb474448455c94e7db56015e
parent 5047736d6f7a7724765d77699c7b1fb0b0ec3964
Author: Zack Newman <zack@philomathiclife.com>
Date:   Wed,  7 Feb 2024 09:27:04 -0700

add atomic ops. release as version 1.0.0

Diffstat:
MCargo.toml | 4++--
Msrc/lib.rs | 340++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 313 insertions(+), 31 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,14 +9,14 @@ license = "MIT OR Apache-2.0" name = "priv_sep" readme = "README.md" repository = "https://git.philomathiclife.com/repos/priv_sep/" -version = "0.8.1" +version = "1.0.0" [lib] name = "priv_sep" path = "src/lib.rs" [target.'cfg(target_os = "openbsd")'.dependencies] -libc = { version = "0.2.150", default-features = false, features = ["std"], optional = true } +libc = { version = "0.2.153", default-features = false, features = ["std"], optional = true } [build-dependencies] rustc_version = "0.4.0" diff --git a/src/lib.rs b/src/lib.rs @@ -36,11 +36,8 @@ clippy::suspicious )] #![allow( - clippy::arithmetic_side_effects, clippy::blanket_clippy_restriction_lints, - clippy::exhaustive_structs, clippy::implicit_return, - clippy::into_iter_on_ref, clippy::min_ident_chars, clippy::missing_trait_methods, clippy::single_char_lifetime_names, @@ -49,16 +46,15 @@ #![cfg(feature = "openbsd")] extern crate alloc; use alloc::ffi::{CString, NulError}; -use core::convert::AsRef; -use core::ffi::{c_char, c_int}; -use core::fmt::{self, Display, Formatter}; -use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Deref, Not}; -use core::ptr; -use core::slice::Iter; -use std::error; -use std::io; -use std::os::unix::ffi::OsStrExt; -use std::path::Path; +use core::{ + convert::AsRef, + ffi::{c_char, c_int}, + fmt::{self, Display, Formatter}, + ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Deref, Not}, + ptr, + slice::Iter, +}; +use std::{error, io, os::unix::ffi::OsStrExt, path::Path}; 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, @@ -184,6 +180,18 @@ impl Display for 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, @@ -192,6 +200,7 @@ impl Display for Promise { #[derive(Clone, Copy, Debug)] pub struct Promises<const CAP: usize> { /// The initial set of `Promise`s. + /// `Promise`s starting from `len` are ones that have been "removed". proms: [Promise; CAP], /// The length of `vals`. len: usize, @@ -219,32 +228,59 @@ fn pledge(promises: *const c_char) -> Result<(), io::Error> { } } impl<const CAP: usize> Promises<CAP> { - /// Returns a `Promises` containing the unique - /// `Promise`s in `initial`. + /// Returns a `Promises` containing the unique `Promise`s in `initial`. /// - /// Note that the order of `initial` may - /// not be retained. - #[allow(clippy::indexing_slicing)] + /// # Example + /// + /// ``` + /// use priv_sep::{Promise, Promises}; + /// assert!(Promises::new([Promise::Stdio]).len() == 1); + /// ``` + #[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)] #[inline] #[must_use] pub fn new(mut initial: [Promise; CAP]) -> Self { + // This is the first index where "removed" items begin. let mut len = CAP; + // We "remove" duplicates by putting them at the end of `initial`. for i in 0..CAP { + // We don't want to "remove" items that have been "removed" already. if len > i { + // We start at one past the current index. + // No fear of overflow since the max value of `i` is one less than `CAP`. let mut start = i + 1; + // We must check all items after `i`. When we find an item that is the same, + // we "remove" it by replacing it with the last active item. This is achieved by + // decrementing `len` (since we know we are removing one). By decrementing `len`, + // `len` becomes the last index of an active item which then allows us to swap it + // with the duplicate item. We keep the index the same in this situation since we need + // to check this item that has been swapped. loop { + // We don't want to "remove" items that have been "removed" already. if len > start { + // Check if the item at `start` is a duplicate. + // `i` is less than `CAP`, so indexing `initial` with it won't `panic`. + // `len` is always less than or equal to `CAP`, and `start` is less than it; + // so indexing `initial` with `start` won't `panic`. if initial[i] == initial[start] { + // We "remove" the item at `start` by swapping it with the last item. + // No fear of underflow since `start` is positive and `len` is greater than it. len -= 1; initial.swap(start, len); } else { + // The item at `start` is not the same as the item at `i`, so we check the next + // item. + // No fear of overflow since `start` is less than `len` which is less than or + // equal to `CAP`. start += 1; } } else { + // We have checked all items after `i`, so we move on to the next. break; } } } else { + // We have "removed" all duplicate items. break; } } @@ -254,24 +290,54 @@ impl<const CAP: usize> Promises<CAP> { } } /// Returns the number of [`Promise`]s. + /// + /// # Example + /// + /// ``` + /// use priv_sep::{Promise, Promises}; + /// assert!(Promises::new([Promise::Stdio]).len() == 1); + /// ``` #[inline] #[must_use] pub const fn len(&self) -> usize { self.len } /// Returns `true` iff there are no [`Promise`]s. + /// Returns the number of [`Promise`]s. + /// + /// # Example + /// + /// ``` + /// use priv_sep::Promises; + /// assert!(Promises::new([]).is_empty()); + /// ``` #[inline] #[must_use] pub const fn is_empty(&self) -> bool { self.len == 0 } /// Returns the set of `Promise`s. + /// + /// # Example + /// + /// ``` + /// use priv_sep::{Promise, Promises}; + /// assert!(Promises::new([Promise::Stdio]).vals().first().map_or(false, |promise| promise == Promise::Stdio)); + /// ``` #[inline] #[must_use] pub fn vals(&self) -> &[Promise] { &self.proms[..self.len] } /// Returns `true` iff `self` contains `promise`. + /// + /// # Examples + /// + /// ``` + /// use priv_sep::{Promise, Promises}; + /// assert!(Promises::new([Promise::Stdio]).contains(Promise::Stdio)); + /// assert!(!Promises::new([Promise::Stdio]).contains(Promise::Rpath)); + /// ``` #[inline] #[must_use] pub fn contains(&self, promise: Promise) -> bool { @@ -279,33 +345,95 @@ impl<const CAP: usize> Promises<CAP> { } /// Removes all [`Promise`]s except `promises`. /// - /// Note that the internal order of `Promise`s - /// may no longer be the same. + /// Note that the internal order of `Promise`s may no longer be the same. + /// + /// # Example + /// + /// ``` + /// use priv_sep::{Promise, Promises}; + /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]); + /// proms.retain(Promises::new([Promise::Stdio])); + /// assert!(proms.len() == 1 && proms[0] == Promise::Stdio); + /// ``` + #[allow(clippy::arithmetic_side_effects)] #[inline] pub fn retain<P: AsRef<[Promise]>>(&mut self, promises: P) { let other = promises.as_ref(); + // We "check" if an item is in `promises`. If it is not, we "remove" it by swapping it with the + // the last item. for i in 0..self.len { + // Since we swap items, we must re-check `i` with the item it was swapped with. loop { let Some(prom) = self.vals().get(i) else { + // We have no more items to check, so we are done. return; }; if other.contains(prom) { + // `prom` needs to be retained, so we move to the next item. break; } + // `prom` needs to be "removed", so we swap it with the last item and do the process again. + // `len` is at least 1; otherwise `self.vals().get(i)` would have returned `None` so this + // won't underflow. self.len -= 1; self.proms.swap(i, self.len); } } } + /// Same as [`Self::retain`] then [`Self::pledge`]; however this is "atomic" in that if an error occurs from `pledge`, + /// `self` is left unchanged. This is useful when one wants to "recover" from an error since there is no + /// way to add `Promise`s back forcing one to have to create a second `Promises`. + /// + /// Note that when `retain` doesn't change `self`, `pledge` is not called. + /// + /// # Errors + /// + /// Returns [`io::Error`] iff `pledge` does. + /// + /// # Example + /// + /// ```no_run + /// use priv_sep::{Promise, Promises}; + /// assert!(Promises::new([Promise::Rpath, Promise::Stdio]).retain_then_pledge([Promise::Rpath]).is_ok()); + /// ``` + #[inline] + pub fn retain_then_pledge<P: AsRef<[Promise]>>( + &mut self, + promises: P, + ) -> Result<(), 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.retain(promises); + if self.len < cur.len { + self.pledge().map_err(|err| { + *self = cur; + err + }) + } else { + Ok(()) + } + } /// Removes `promises` from `self`. /// - /// Note that the internal order of `Promise`s - /// may no longer be the same. + /// Note that the internal order of `Promise`s may no longer be the same. + /// + /// # Example + /// + /// ``` + /// use priv_sep::{Promise, Promises}; + /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]); + /// proms.remove_promises(Promises::new([Promise::Stdio])); + /// assert!(proms.len() == 1 && proms[0] == Promise::Rpath); + /// ``` + #[allow(clippy::arithmetic_side_effects, clippy::into_iter_on_ref)] #[inline] pub fn remove_promises<P: AsRef<[Promise]>>(&mut self, promises: P) { promises.as_ref().into_iter().fold((), |(), prom| { for (idx, prom2) in self.vals().into_iter().enumerate() { + // We must "remove" the item. if prom == prom2 { + // `len` is at least one; otherwise `self.vals().into_iter()` would have iterated `None`. self.len -= 1; self.proms.swap(idx, self.len); break; @@ -313,23 +441,109 @@ impl<const CAP: usize> Promises<CAP> { } }); } - /// Removes `promise` from `self` returning `true` iff - /// `promise` existed. + /// 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()); + /// ``` + #[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.remove_promises(promises); + if self.len() < cur.len { + self.pledge().map_err(|err| { + *self = cur; + err + }) + } else { + Ok(()) + } + } + /// Removes `promise` from `self` returning `true` iff `promise` existed. + /// + /// Note that the internal order of `Promise`s may no longer be the same. /// - /// Note that the internal order of `Promise`s - /// may no longer be the same. + /// # Example + /// + /// ``` + /// use priv_sep::{Promise, Promises}; + /// let mut proms = Promises::new([Promise::Rpath, Promise::Stdio]); + /// assert!(proms.remove(Promise::Stdio)); + /// assert!(proms.len() == 1 && proms[0] == Promise::Rpath); + /// ``` + #[allow(clippy::arithmetic_side_effects, clippy::into_iter_on_ref)] #[inline] pub fn remove(&mut self, promise: Promise) -> bool { for (idx, prom) in self.vals().into_iter().enumerate() { + // We must "remove" `prom`. if *prom == promise { + // `len` is at least one; otherwise `self.vals().into_iter()` would have iterated `None`. self.len -= 1; self.proms.swap(idx, self.len); + // Since `self` contains unique `Promise`s, we don't have to check any more. return true; } } false } + /// 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`. + /// + /// The contained `bool` in `Ok` is `true` iff `remove` returned `true`. Note that when `remove` returns + /// `false`, `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_then_pledge(Promise::Rpath).is_ok()); + /// ``` + #[inline] + pub fn remove_then_pledge(&mut self, promise: Promise) -> Result<bool, 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; + if self.remove(promise) { + self.pledge() + .map_err(|err| { + *self = cur; + err + }) + .map(|()| true) + } else { + Ok(false) + } + } /// Returns an [`Iterator`] without consuming `self`. + /// + /// # Example + /// + /// ``` + /// use priv_sep::{Promise, Promises}; + /// assert!(Promises::new([Promise::Rpath, Promise::Stdio]).iter().next().map_or(false, |prom| prom == Promise::Rpath)); + /// ``` + #[allow(clippy::into_iter_on_ref)] #[inline] pub fn iter(&self) -> Iter<'_, Promise> { self.vals().into_iter() @@ -340,8 +554,20 @@ impl<const CAP: usize> Promises<CAP> { /// # Errors /// /// Returns [`io::Error`] iff `pledge(2)` errors. + /// + /// # Example + /// + /// ```no_run + /// use priv_sep::{Promise, Promises}; + /// assert!(Promises::new([Promise::Stdio]).pledge().is_ok()); + /// ``` #[inline] - #[allow(unsafe_code, clippy::indexing_slicing)] + #[allow( + unsafe_code, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::into_iter_on_ref + )] pub fn pledge(&self) -> Result<(), io::Error> { let mut p = Vec::new(); self.vals().into_iter().fold((), |(), promise| { @@ -391,6 +617,7 @@ impl<const CAP: usize> Promises<CAP> { // All promises have a space after them which means // we must replace the last promise's space with // 0/nul byte. + // `idx` is at least 1 based on the above check. p[idx - 1] = 0; } // SAFETY: @@ -440,24 +667,31 @@ pub struct PromIter<const CAP: usize> { } impl<const CAP: usize> Iterator for PromIter<CAP> { type Item = Promise; + #[allow(clippy::arithmetic_side_effects)] #[inline] fn next(&mut self) -> Option<Self::Item> { self.prom.vals().get(self.idx).map(|prom| { + // `idx` must be less than `CAP`, since `get` returned `Some`. self.idx += 1; *prom }) } } impl<const CAP: usize> ExactSizeIterator for PromIter<CAP> { + #[allow(clippy::arithmetic_side_effects)] #[inline] fn len(&self) -> usize { + // `idx` is <= `len`. self.prom.len - self.idx } } impl<const CAP: usize> DoubleEndedIterator for PromIter<CAP> { + #[allow(clippy::arithmetic_side_effects)] #[inline] fn next_back(&mut self) -> Option<Self::Item> { self.prom.proms[self.idx..self.prom.len].last().map(|prom| { + // `len` is at least 1; otherwise the above slice would be empty which means `last` would + // have returned `None`. self.prom.len -= 1; *prom }) @@ -491,6 +725,13 @@ impl<const CAP: usize> From<[Promise; CAP]> for Promises<CAP> { /// # Errors /// /// Returns [`io::Error`] iff `pledge(2)` errors. +/// +/// # Example +/// +/// ```no_run +/// use priv_sep; +/// assert!(priv_sep::pledge_none().is_ok()); +/// ``` #[inline] pub fn pledge_none() -> Result<(), io::Error> { // `NULL` is always valid for `promises`. @@ -518,7 +759,7 @@ fn unveil(path: *const c_char, permissions: *const c_char) -> Result<(), io::Err } } /// `permissions` to [`unveil(2)`](https://man.openbsd.org/amd64/unveil.2). -#[allow(clippy::struct_excessive_bools)] +#[allow(clippy::exhaustive_structs, clippy::struct_excessive_bools)] #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Permissions { /// Consult `unveil(2)`. @@ -580,12 +821,34 @@ impl Permissions { /// /// Returns [`NulError`] iff [`CString::new`] does. /// Returns [`io::Error`] iff `unveil(2)` 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 + /// )); + /// ``` #[inline] - #[allow(unsafe_code, clippy::as_conversions, clippy::cast_lossless)] + #[allow( + unsafe_code, + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::cast_lossless + )] 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| { + // `true as usize` is guaranteed to return 1, and `false as usize` is guaranteed to return + // 0. This means the max sum is 5. let mut vec = Vec::with_capacity( self.create as usize + self.execute as usize @@ -794,6 +1057,18 @@ impl Not for &Permissions { !*self } } +impl PartialEq<&Self> for Permissions { + #[inline] + fn eq(&self, other: &&Self) -> bool { + *self == **other + } +} +impl PartialEq<Permissions> for &Permissions { + #[inline] + fn eq(&self, other: &Permissions) -> bool { + **self == *other + } +} /// Error returned by [`Permissions::unveil`]. #[allow(clippy::exhaustive_enums)] #[derive(Debug)] @@ -823,6 +1098,13 @@ impl error::Error for UnveilErr {} /// # 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`.