From fc37a1bd24943a3c0a3b0c63e570cb901eb5c843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Roy=20Baron?= Date: Sat, 14 Sep 2024 14:58:46 +0200 Subject: [PATCH] Replace enumflags2 with a macro we define ourself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoid a proc macro, makes the api more ergonomic by removing the need for the BitFlags wrapper type, makes it impossible to construct an Access* value with invalid bits, avoids exposing all() to the user and makes it possible for us to define the exact api we want to expose to the user. Signed-off-by: Björn Roy Baron --- Cargo.toml | 1 - examples/sandboxer.rs | 6 +- src/access.rs | 179 ++++++++++++++++++++++++++++++------------ src/compat.rs | 1 - src/errors.rs | 25 ++---- src/fs.rs | 162 ++++++++++++++++++-------------------- src/lib.rs | 2 +- src/net.rs | 84 +++++++++----------- src/ruleset.rs | 36 ++++----- 9 files changed, 272 insertions(+), 224 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3a5da214..981a2535 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ exclude = [".gitignore"] readme = "README.md" [dependencies] -enumflags2 = "0.7" libc = "0.2.133" thiserror = "1.0" diff --git a/examples/sandboxer.rs b/examples/sandboxer.rs index dc106193..65e51590 100644 --- a/examples/sandboxer.rs +++ b/examples/sandboxer.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, bail, Context}; use landlock::{ - Access, AccessFs, AccessNet, BitFlags, NetPort, PathBeneath, PathFd, Ruleset, RulesetAttr, + Access, AccessFs, AccessNet, NetPort, PathBeneath, PathFd, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetStatus, ABI, }; use std::env; @@ -19,7 +19,7 @@ const ENV_TCP_CONNECT_NAME: &str = "LL_TCP_CONNECT"; struct PathEnv { paths: Vec, - access: BitFlags, + access: AccessFs, } impl PathEnv { @@ -31,7 +31,7 @@ impl PathEnv { /// allowed. Paths are separated with ":", e.g. "/bin:/lib:/usr:/proc". In case an empty /// string is provided, NO restrictions are applied. /// * `access`: Set of access-rights allowed for each of the parsed paths. - fn new<'a>(name: &'a str, access: BitFlags) -> anyhow::Result { + fn new<'a>(name: &'a str, access: AccessFs) -> anyhow::Result { Ok(Self { paths: env::var_os(name) .ok_or(anyhow!("missing environment variable {name}"))? diff --git a/src/access.rs b/src/access.rs index 24a42191..7237c388 100644 --- a/src/access.rs +++ b/src/access.rs @@ -1,21 +1,132 @@ +use std::ops::{BitAnd, BitOr, Not}; + use crate::{ - AccessError, AddRuleError, AddRulesError, BitFlags, CompatError, CompatResult, - HandleAccessError, HandleAccessesError, Ruleset, TailoredCompatLevel, TryCompat, ABI, + AccessError, AddRuleError, AddRulesError, CompatError, CompatResult, HandleAccessError, + HandleAccessesError, Ruleset, TailoredCompatLevel, TryCompat, ABI, }; -use enumflags2::BitFlag; #[cfg(test)] -use crate::{make_bitflags, AccessFs, CompatLevel, CompatState, Compatibility}; +use crate::{AccessFs, CompatLevel, CompatState, Compatibility}; + +#[macro_export] +macro_rules! make_bitflags { + ($bitflag_type:ident :: {$($flag:ident)|*}) => { + $bitflag_type::EMPTY $(.union($bitflag_type::$flag))* + }; +} + +macro_rules! bitflags_type { + ( + $(#[$bitflags_attr:meta])* + $vis:vis struct $bitflags_name:ident: $bitflags_type:ty { + $( + $(#[$flag_attr:meta])* + const $flag_name:ident = $flag_val:expr; + )* + } + ) => { + $(#[$bitflags_attr])* + #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] + $vis struct $bitflags_name($bitflags_type); + + impl $bitflags_name { + $( + #[allow(non_upper_case_globals)] + $(#[$flag_attr])* + $vis const $flag_name: Self = Self($flag_val); + )* + + $vis const EMPTY: Self = Self(0); + + $vis const fn is_empty(&self) -> bool { + self.0 == 0 + } + + $vis const fn union(self, rhs: Self) -> Self { + Self(self.0 | rhs.0) + } + + $vis const fn contains(self, rhs: Self) -> bool { + self.0 & rhs.0 == rhs.0 + } + + pub(crate) const fn all() -> Self { + Self(0 $(| $flag_val)*) + } + + pub(crate) const fn bits(self) -> $bitflags_type { + self.0 + } + } + + impl core::ops::BitAnd for $bitflags_name { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self { + Self(self.0 & rhs.0) + } + } + + impl core::ops::BitAndAssign for $bitflags_name { + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= rhs.0; + } + } + + impl core::ops::BitOr for $bitflags_name { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self { + Self(self.0 | rhs.0) + } + } + + impl core::ops::BitOrAssign for $bitflags_name { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } + } + + impl core::ops::BitXor for $bitflags_name { + type Output = Self; + + fn bitxor(self, rhs: Self) -> Self { + Self(self.0 ^ rhs.0) + } + } -pub trait Access: PrivateAccess { + impl core::ops::BitXorAssign for $bitflags_name { + fn bitxor_assign(&mut self, rhs: Self) { + self.0 ^= rhs.0; + } + } + + impl core::ops::Not for $bitflags_name { + type Output = Self; + + fn not(self) -> Self { + Self(!self.0) & Self::all() + } + } + }; +} +pub(crate) use bitflags_type; + +pub trait Access: PrivateAccess + TailoredCompatLevel { /// Gets the access rights defined by a specific [`ABI`]. - fn from_all(abi: ABI) -> BitFlags; + fn from_all(abi: ABI) -> Self; } -pub trait PrivateAccess: BitFlag { +pub trait PrivateAccess: + core::fmt::Debug + Copy + BitOr + BitAnd + Not +{ + fn is_empty(self) -> bool + where + Self: Access; + fn ruleset_handle_access( ruleset: &mut Ruleset, - access: BitFlags, + access: Self, ) -> Result<(), HandleAccessesError> where Self: Access; @@ -29,26 +140,16 @@ pub trait PrivateAccess: BitFlag { Self: Access; } -// Creates an illegal/overflowed BitFlags with all its bits toggled, including undefined ones. -fn full_negation(flags: BitFlags) -> BitFlags -where - T: Access, -{ - unsafe { BitFlags::::from_bits_unchecked(!flags.bits()) } -} - #[test] fn bit_flags_full_negation() { - let scoped_negation = !BitFlags::::all(); - assert_eq!(scoped_negation, BitFlags::::empty()); - // !BitFlags::::all() could be equal to full_negation(BitFlags::::all())) - // if all the 64-bits would be used, which is not currently the case. - assert_ne!(scoped_negation, full_negation(BitFlags::::all())); + let scoped_negation = !AccessFs::all(); + assert_eq!(scoped_negation, AccessFs::EMPTY); + // !AccessFs::all() could be equal to !AccessFs::all().bits() if + // all the 64-bits would be used, which is not currently the case. + assert_ne!(scoped_negation.bits(), !AccessFs::all().bits()); } -impl TailoredCompatLevel for BitFlags where A: Access {} - -impl TryCompat for BitFlags +impl TryCompat for A where A: Access, { @@ -56,24 +157,17 @@ where if self.is_empty() { // Empty access-rights would result to a runtime error. Err(AccessError::Empty.into()) - } else if !Self::all().contains(*self) { - // Unknown access-rights (at build time) would result to a runtime error. - // This can only be reached by using the unsafe BitFlags::from_bits_unchecked(). - Err(AccessError::Unknown { - access: *self, - unknown: *self & full_negation(Self::all()), - } - .into()) } else { let compat = *self & A::from_all(abi); + let incompatible_flags = *self & !A::from_all(abi); let ret = if compat.is_empty() { Ok(CompatResult::No( AccessError::Incompatible { access: *self }.into(), )) - } else if compat != *self { + } else if !incompatible_flags.is_empty() { let error = AccessError::PartiallyCompatible { access: *self, - incompatible: *self & full_negation(compat), + incompatible: incompatible_flags, } .into(); Ok(CompatResult::Partial(error)) @@ -103,7 +197,7 @@ fn compat_bit_flags() { ); assert!(compat.state == CompatState::Full); - let empty_access = BitFlags::::empty(); + let empty_access = AccessFs::EMPTY; assert!(matches!( empty_access .try_compat(compat.abi(), compat.level, &mut compat.state) @@ -111,21 +205,6 @@ fn compat_bit_flags() { CompatError::Access(AccessError::Empty) )); - let all_unknown_access = unsafe { BitFlags::::from_bits_unchecked(1 << 63) }; - assert!(matches!( - all_unknown_access.try_compat(compat.abi(), compat.level, &mut compat.state).unwrap_err(), - CompatError::Access(AccessError::Unknown { access, unknown }) if access == all_unknown_access && unknown == all_unknown_access - )); - // An error makes the state final. - assert!(compat.state == CompatState::Dummy); - - let some_unknown_access = unsafe { BitFlags::::from_bits_unchecked(1 << 63 | 1) }; - assert!(matches!( - some_unknown_access.try_compat(compat.abi(), compat.level, &mut compat.state).unwrap_err(), - CompatError::Access(AccessError::Unknown { access, unknown }) if access == some_unknown_access && unknown == all_unknown_access - )); - assert!(compat.state == CompatState::Dummy); - compat = ABI::Unsupported.into(); // Tests that the ruleset is marked as unsupported. diff --git a/src/compat.rs b/src/compat.rs index 86f115e2..f7a1570f 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -15,7 +15,6 @@ use strum_macros::{EnumCount as EnumCountMacro, EnumIter}; /// gets all the file system access rights defined by the first version. /// /// Without `ABI`, it would be hazardous to rely on the the full set of access flags -/// (e.g., `BitFlags::::all()` or `BitFlags::ALL`), /// a moving target that would change the semantics of your Landlock rule /// when migrating to a newer version of this crate. /// Indeed, a simple `cargo update` or `cargo install` run by any developer diff --git a/src/errors.rs b/src/errors.rs index de018741..25700227 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,4 +1,4 @@ -use crate::{Access, AccessFs, AccessNet, BitFlags}; +use crate::{Access, AccessFs, AccessNet}; use std::io; use std::path::PathBuf; use thiserror::Error; @@ -84,10 +84,7 @@ where AddRuleCall { source: io::Error }, /// The rule's access-rights are not all handled by the (requested) ruleset access-rights. #[error("access-rights not handled by the ruleset: {incompatible:?}")] - UnhandledAccess { - access: BitFlags, - incompatible: BitFlags, - }, + UnhandledAccess { access: T, incompatible: T }, #[error(transparent)] Compat(#[from] CompatError), } @@ -142,8 +139,8 @@ pub enum PathBeneathError { /// whereas the file descriptor doesn't point to a directory. #[error("incompatible directory-only access-rights: {incompatible:?}")] DirectoryAccess { - access: BitFlags, - incompatible: BitFlags, + access: AccessFs, + incompatible: AccessFs, }, } @@ -157,24 +154,14 @@ where /// kernel. #[error("empty access-right")] Empty, - /// The access-rights set was forged with the unsafe `BitFlags::from_bits_unchecked()` and it - /// contains unknown bits. - #[error("unknown access-rights (at build time): {unknown:?}")] - Unknown { - access: BitFlags, - unknown: BitFlags, - }, /// The best-effort approach was (deliberately) disabled and the requested access-rights are /// fully incompatible with the running kernel. #[error("fully incompatible access-rights: {access:?}")] - Incompatible { access: BitFlags }, + Incompatible { access: T }, /// The best-effort approach was (deliberately) disabled and the requested access-rights are /// partially incompatible with the running kernel. #[error("partially incompatible access-rights: {incompatible:?}")] - PartiallyCompatible { - access: BitFlags, - incompatible: BitFlags, - }, + PartiallyCompatible { access: T, incompatible: T }, } #[derive(Debug, Error)] diff --git a/src/fs.rs b/src/fs.rs index e1a4066d..8863d594 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -5,7 +5,6 @@ use crate::{ PrivateAccess, PrivateRule, Rule, Ruleset, RulesetCreated, RulesetError, TailoredCompatLevel, TryCompat, ABI, }; -use enumflags2::{bitflags, make_bitflags, BitFlags}; use std::fs::OpenOptions; use std::io::Error; use std::mem::zeroed; @@ -18,79 +17,70 @@ use crate::{RulesetAttr, RulesetCreatedAttr}; #[cfg(test)] use strum::IntoEnumIterator; -/// File system access right. -/// -/// Each variant of `AccessFs` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights) -/// for the file system. -/// A set of access rights can be created with [`BitFlags`](BitFlags). -/// -/// # Example -/// -/// ``` -/// use landlock::{ABI, Access, AccessFs, BitFlags, make_bitflags}; -/// -/// let exec = AccessFs::Execute; -/// -/// let exec_set: BitFlags = exec.into(); -/// -/// let file_content = make_bitflags!(AccessFs::{Execute | WriteFile | ReadFile}); -/// -/// let fs_v1 = AccessFs::from_all(ABI::V1); -/// -/// let without_exec = fs_v1 & !AccessFs::Execute; -/// -/// assert_eq!(fs_v1 | AccessFs::Refer, AccessFs::from_all(ABI::V2)); -/// ``` -/// -/// # Warning -/// -/// To avoid unknown restrictions **don't use `BitFlags::::all()` nor `BitFlags::ALL`**, -/// but use a version you tested and vetted instead, -/// for instance [`AccessFs::from_all(ABI::V1)`](Access::from_all). -/// Direct use of **the [`BitFlags`] API is deprecated**. -/// See [`ABI`] for the rationale and help to test it. -#[bitflags] -#[repr(u64)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum AccessFs { - /// Execute a file. - Execute = uapi::LANDLOCK_ACCESS_FS_EXECUTE as u64, - /// Open a file with write access. - WriteFile = uapi::LANDLOCK_ACCESS_FS_WRITE_FILE as u64, - /// Open a file with read access. - ReadFile = uapi::LANDLOCK_ACCESS_FS_READ_FILE as u64, - /// Open a directory or list its content. - ReadDir = uapi::LANDLOCK_ACCESS_FS_READ_DIR as u64, - /// Remove an empty directory or rename one. - RemoveDir = uapi::LANDLOCK_ACCESS_FS_REMOVE_DIR as u64, - /// Unlink (or rename) a file. - RemoveFile = uapi::LANDLOCK_ACCESS_FS_REMOVE_FILE as u64, - /// Create (or rename or link) a character device. - MakeChar = uapi::LANDLOCK_ACCESS_FS_MAKE_CHAR as u64, - /// Create (or rename) a directory. - MakeDir = uapi::LANDLOCK_ACCESS_FS_MAKE_DIR as u64, - /// Create (or rename or link) a regular file. - MakeReg = uapi::LANDLOCK_ACCESS_FS_MAKE_REG as u64, - /// Create (or rename or link) a UNIX domain socket. - MakeSock = uapi::LANDLOCK_ACCESS_FS_MAKE_SOCK as u64, - /// Create (or rename or link) a named pipe. - MakeFifo = uapi::LANDLOCK_ACCESS_FS_MAKE_FIFO as u64, - /// Create (or rename or link) a block device. - MakeBlock = uapi::LANDLOCK_ACCESS_FS_MAKE_BLOCK as u64, - /// Create (or rename or link) a symbolic link. - MakeSym = uapi::LANDLOCK_ACCESS_FS_MAKE_SYM as u64, - /// Link or rename a file from or to a different directory. - Refer = uapi::LANDLOCK_ACCESS_FS_REFER as u64, - /// Truncate a file with `truncate(2)`, `ftruncate(2)`, `creat(2)`, or `open(2)` with `O_TRUNC`. - Truncate = uapi::LANDLOCK_ACCESS_FS_TRUNCATE as u64, - /// Send IOCL commands to a device file. - IoctlDev = uapi::LANDLOCK_ACCESS_FS_IOCTL_DEV as u64, +crate::access::bitflags_type! { + /// File system access right. + /// + /// Each variant of `AccessFs` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights) + /// for the file system. + /// + /// # Example + /// + /// ``` + /// use landlock::{ABI, Access, AccessFs, make_bitflags}; + /// + /// let exec = AccessFs::Execute; + /// + /// let exec_set: AccessFs = exec.into(); + /// + /// let file_content = make_bitflags!(AccessFs::{Execute | WriteFile | ReadFile}); + /// + /// let fs_v1 = AccessFs::from_all(ABI::V1); + /// + /// let without_exec = fs_v1 & !AccessFs::Execute; + /// + /// assert_eq!(fs_v1 | AccessFs::Refer, AccessFs::from_all(ABI::V2)); + /// ``` + pub struct AccessFs: u64 { + /// Execute a file. + const Execute = uapi::LANDLOCK_ACCESS_FS_EXECUTE as u64; + /// Open a file with write access. + const WriteFile = uapi::LANDLOCK_ACCESS_FS_WRITE_FILE as u64; + /// Open a file with read access. + const ReadFile = uapi::LANDLOCK_ACCESS_FS_READ_FILE as u64; + /// Open a directory or list its content. + const ReadDir = uapi::LANDLOCK_ACCESS_FS_READ_DIR as u64; + /// Remove an empty directory or rename one. + const RemoveDir = uapi::LANDLOCK_ACCESS_FS_REMOVE_DIR as u64; + /// Unlink (or rename) a file. + const RemoveFile = uapi::LANDLOCK_ACCESS_FS_REMOVE_FILE as u64; + /// Create (or rename or link) a character device. + const MakeChar = uapi::LANDLOCK_ACCESS_FS_MAKE_CHAR as u64; + /// Create (or rename) a directory. + const MakeDir = uapi::LANDLOCK_ACCESS_FS_MAKE_DIR as u64; + /// Create (or rename or link) a regular file. + const MakeReg = uapi::LANDLOCK_ACCESS_FS_MAKE_REG as u64; + /// Create (or rename or link) a UNIX domain socket. + const MakeSock = uapi::LANDLOCK_ACCESS_FS_MAKE_SOCK as u64; + /// Create (or rename or link) a named pipe. + const MakeFifo = uapi::LANDLOCK_ACCESS_FS_MAKE_FIFO as u64; + /// Create (or rename or link) a block device. + const MakeBlock = uapi::LANDLOCK_ACCESS_FS_MAKE_BLOCK as u64; + /// Create (or rename or link) a symbolic link. + const MakeSym = uapi::LANDLOCK_ACCESS_FS_MAKE_SYM as u64; + /// Link or rename a file from or to a different directory. + const Refer = uapi::LANDLOCK_ACCESS_FS_REFER as u64; + /// Truncate a file with `truncate(2)`, `ftruncate(2)`, `creat(2)`, or `open(2)` with `O_TRUNC`. + const Truncate = uapi::LANDLOCK_ACCESS_FS_TRUNCATE as u64; + /// Send IOCL commands to a device file. + const IoctlDev = uapi::LANDLOCK_ACCESS_FS_IOCTL_DEV as u64; + } } +impl TailoredCompatLevel for AccessFs {} + impl Access for AccessFs { /// Union of [`from_read()`](AccessFs::from_read) and [`from_write()`](AccessFs::from_write). - fn from_all(abi: ABI) -> BitFlags { + fn from_all(abi: ABI) -> Self { // An empty access-right would be an error if passed to the kernel, but because the kernel // doesn't support Landlock, no Landlock syscall should be called. try_compat() should // also return RestrictionStatus::Unrestricted when called with unsupported/empty @@ -103,9 +93,9 @@ impl AccessFs { // Roughly read (i.e. not all FS actions are handled). /// Gets the access rights identified as read-only according to a specific ABI. /// Exclusive with [`from_write()`](AccessFs::from_write). - pub fn from_read(abi: ABI) -> BitFlags { + pub fn from_read(abi: ABI) -> Self { match abi { - ABI::Unsupported => BitFlags::EMPTY, + ABI::Unsupported => AccessFs::EMPTY, ABI::V1 | ABI::V2 | ABI::V3 | ABI::V4 | ABI::V5 => make_bitflags!(AccessFs::{ Execute | ReadFile @@ -117,9 +107,9 @@ impl AccessFs { // Roughly write (i.e. not all FS actions are handled). /// Gets the access rights identified as write-only according to a specific ABI. /// Exclusive with [`from_read()`](AccessFs::from_read). - pub fn from_write(abi: ABI) -> BitFlags { + pub fn from_write(abi: ABI) -> Self { match abi { - ABI::Unsupported => BitFlags::EMPTY, + ABI::Unsupported => AccessFs::EMPTY, ABI::V1 => make_bitflags!(AccessFs::{ WriteFile | RemoveDir @@ -139,7 +129,7 @@ impl AccessFs { } /// Gets the access rights legitimate for non-directory files. - pub fn from_file(abi: ABI) -> BitFlags { + pub fn from_file(abi: ABI) -> Self { Self::from_all(abi) & ACCESS_FILE } } @@ -156,9 +146,13 @@ fn consistent_access_fs_rw() { } impl PrivateAccess for AccessFs { + fn is_empty(self) -> bool { + AccessFs::is_empty(&self) + } + fn ruleset_handle_access( ruleset: &mut Ruleset, - access: BitFlags, + access: Self, ) -> Result<(), HandleAccessesError> { // We need to record the requested accesses for PrivateRule::check_consistency(). ruleset.requested_handled_fs |= access; @@ -187,8 +181,8 @@ impl PrivateAccess for AccessFs { // TODO: Make ACCESS_FILE a property of AccessFs. // TODO: Add tests for ACCESS_FILE. -const ACCESS_FILE: BitFlags = make_bitflags!(AccessFs::{ - ReadFile | WriteFile | Execute | Truncate | IoctlDev +const ACCESS_FILE: AccessFs = make_bitflags!(AccessFs::{ + ReadFile | WriteFile | Execute | Truncate }); // XXX: What should we do when a stat call failed? @@ -221,7 +215,7 @@ pub struct PathBeneath { attr: uapi::landlock_path_beneath_attr, // Ties the lifetime of a file descriptor to this object. parent_fd: F, - allowed_access: BitFlags, + allowed_access: AccessFs, compat_level: Option, } @@ -234,7 +228,7 @@ where /// The `parent` file descriptor will be automatically closed with the returned `PathBeneath`. pub fn new(parent: F, access: A) -> Self where - A: Into>, + A: Into, { PathBeneath { // Invalid access rights until as_ptr() is called. @@ -353,7 +347,7 @@ fn path_beneath_try_compat() { let mut compat_state = CompatState::Init; assert!(matches!( - PathBeneath::new(PathFd::new(file).unwrap(), BitFlags::EMPTY) + PathBeneath::new(PathFd::new(file).unwrap(), AccessFs::EMPTY) .try_compat(abi, CompatLevel::BestEffort, &mut compat_state) .unwrap_err(), CompatError::Access(AccessError::Empty) @@ -571,16 +565,14 @@ fn path_fd() { /// Ok(()) /// } /// ``` -pub fn path_beneath_rules( +pub fn path_beneath_rules( paths: I, - access: A, + access: AccessFs, ) -> impl Iterator, RulesetError>> where I: IntoIterator, P: AsRef, - A: Into>, { - let access = access.into(); paths.into_iter().filter_map(move |p| match PathFd::new(p) { Ok(f) => { let valid_access = match is_file(&f) { diff --git a/src/lib.rs b/src/lib.rs index d3eb8b95..ba88bca9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,7 +81,6 @@ extern crate lazy_static; pub use access::Access; pub use compat::{CompatLevel, Compatible, ABI}; -pub use enumflags2::{make_bitflags, BitFlags}; pub use errors::{ AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError, RulesetError, @@ -104,6 +103,7 @@ use errors::TestRulesetError; #[cfg(test)] use strum::IntoEnumIterator; +#[macro_use] mod access; mod compat; mod errors; diff --git a/src/net.rs b/src/net.rs index d2d9a923..803cbd45 100644 --- a/src/net.rs +++ b/src/net.rs @@ -4,66 +4,60 @@ use crate::{ Compatible, HandleAccessError, HandleAccessesError, PrivateAccess, PrivateRule, Rule, Ruleset, RulesetCreated, TailoredCompatLevel, TryCompat, ABI, }; -use enumflags2::{bitflags, BitFlags}; use std::mem::zeroed; -/// Network access right. -/// -/// Each variant of `AccessNet` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights) -/// for the network. -/// A set of access rights can be created with [`BitFlags`](BitFlags). -/// -/// # Example -/// -/// ``` -/// use landlock::{ABI, Access, AccessNet, BitFlags, make_bitflags}; -/// -/// let bind = AccessNet::BindTcp; -/// -/// let bind_set: BitFlags = bind.into(); -/// -/// let bind_connect = make_bitflags!(AccessNet::{BindTcp | ConnectTcp}); -/// -/// let net_v4 = AccessNet::from_all(ABI::V4); -/// -/// assert_eq!(bind_connect, net_v4); -/// ``` -/// -/// # Warning -/// -/// To avoid unknown restrictions **don't use `BitFlags::::all()` nor `BitFlags::ALL`**, -/// but use a version you tested and vetted instead, -/// for instance [`AccessNet::from_all(ABI::V4)`](Access::from_all). -/// Direct use of **the [`BitFlags`] API is deprecated**. -/// See [`ABI`] for the rationale and help to test it. -#[bitflags] -#[repr(u64)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum AccessNet { - /// Bind to a TCP port. - BindTcp = uapi::LANDLOCK_ACCESS_NET_BIND_TCP as u64, - /// Connect to a TCP port. - ConnectTcp = uapi::LANDLOCK_ACCESS_NET_CONNECT_TCP as u64, +crate::access::bitflags_type! { + /// Network access right. + /// + /// Each variant of `AccessNet` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights) + /// for the network. + /// + /// # Example + /// + /// ``` + /// use landlock::{ABI, Access, AccessNet, make_bitflags}; + /// + /// let bind = AccessNet::BindTcp; + /// + /// let bind_set: AccessNet = bind.into(); + /// + /// let bind_connect = make_bitflags!(AccessNet::{BindTcp | ConnectTcp}); + /// + /// let net_v4 = AccessNet::from_all(ABI::V4); + /// + /// assert_eq!(bind_connect, net_v4); + /// ``` + pub struct AccessNet: u64 { + /// Bind to a TCP port. + const BindTcp = uapi::LANDLOCK_ACCESS_NET_BIND_TCP as u64; + /// Connect to a TCP port. + const ConnectTcp = uapi::LANDLOCK_ACCESS_NET_CONNECT_TCP as u64; + } } +impl TailoredCompatLevel for AccessNet {} + /// # Warning /// -/// If `ABI <= ABI::V3`, `AccessNet::from_all()` returns an empty `BitFlags`, which +/// If `ABI <= ABI::V3`, `AccessNet::from_all()` returns an empty `AccessNet`, which /// makes `Ruleset::handle_access(AccessNet::from_all(ABI::V3))` return an error. impl Access for AccessNet { - fn from_all(abi: ABI) -> BitFlags { + fn from_all(abi: ABI) -> Self { match abi { - ABI::Unsupported | ABI::V1 | ABI::V2 | ABI::V3 => BitFlags::EMPTY, + ABI::Unsupported | ABI::V1 | ABI::V2 | ABI::V3 => AccessNet::EMPTY, ABI::V4 | ABI::V5 => AccessNet::BindTcp | AccessNet::ConnectTcp, } } } impl PrivateAccess for AccessNet { + fn is_empty(self) -> bool { + AccessNet::is_empty(&self) + } + fn ruleset_handle_access( ruleset: &mut Ruleset, - access: BitFlags, + access: Self, ) -> Result<(), HandleAccessesError> { // We need to record the requested accesses for PrivateRule::check_consistency(). ruleset.requested_handled_net |= access; @@ -106,7 +100,7 @@ pub struct NetPort { attr: uapi::landlock_net_port_attr, // Only 16-bit port make sense for now. port: u16, - allowed_access: BitFlags, + allowed_access: AccessNet, compat_level: Option, } @@ -119,7 +113,7 @@ impl NetPort { /// allowed for a port range defined by `/proc/sys/net/ipv4/ip_local_port_range`. pub fn new(port: u16, access: A) -> Self where - A: Into>, + A: Into, { NetPort { // Invalid access-rights until as_ptr() is called. diff --git a/src/ruleset.rs b/src/ruleset.rs index e7d3753e..4f18e768 100644 --- a/src/ruleset.rs +++ b/src/ruleset.rs @@ -1,8 +1,7 @@ use crate::compat::private::OptionCompatLevelMut; use crate::{ - uapi, Access, AccessFs, AccessNet, AddRuleError, AddRulesError, BitFlags, CompatLevel, - CompatState, Compatibility, Compatible, CreateRulesetError, RestrictSelfError, RulesetError, - TryCompat, + uapi, Access, AccessFs, AccessNet, AddRuleError, AddRulesError, CompatLevel, CompatState, + Compatibility, Compatible, CreateRulesetError, RestrictSelfError, RulesetError, TryCompat, }; use libc::close; use std::io::Error; @@ -171,10 +170,10 @@ fn support_no_new_privs() -> bool { /// ``` #[cfg_attr(test, derive(Debug))] pub struct Ruleset { - pub(crate) requested_handled_fs: BitFlags, - pub(crate) requested_handled_net: BitFlags, - pub(crate) actual_handled_fs: BitFlags, - pub(crate) actual_handled_net: BitFlags, + pub(crate) requested_handled_fs: AccessFs, + pub(crate) requested_handled_net: AccessNet, + pub(crate) actual_handled_fs: AccessFs, + pub(crate) actual_handled_net: AccessNet, pub(crate) compat: Compatibility, } @@ -336,12 +335,11 @@ pub trait RulesetAttr: Sized + AsMut + Compatible { /// /// On error, returns a wrapped [`HandleAccessesError`](crate::HandleAccessesError). /// E.g., `RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError))` - fn handle_access(mut self, access: T) -> Result + fn handle_access(mut self, access: T) -> Result where - T: Into>, - U: Access, + T: Access, { - U::ruleset_handle_access(self.as_mut(), access.into())?; + T::ruleset_handle_access(self.as_mut(), access)?; Ok(self) } } @@ -407,7 +405,7 @@ fn ruleset_created_handle_access_net_tcp() { // Tests AccessNet::ruleset_handle_access() with ABI that doesn't support TCP rights. let ruleset = Ruleset::from(ABI::V3).handle_access(access).unwrap(); assert_eq!(ruleset.requested_handled_net, access); - assert_eq!(ruleset.actual_handled_net, BitFlags::::EMPTY); + assert_eq!(ruleset.actual_handled_net, AccessNet::EMPTY); // Tests AccessNet::ruleset_handle_access() with ABI that supports TCP rights. let ruleset = Ruleset::from(ABI::V4).handle_access(access).unwrap(); @@ -493,7 +491,7 @@ pub trait RulesetCreatedAttr: Sized + AsMut + Compatible { /// /// ``` /// use landlock::{ - /// Access, AccessFs, BitFlags, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, + /// Access, AccessFs, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, /// RulesetAttr, RulesetCreatedAttr, RulesetError, ABI, /// }; /// use std::env; @@ -513,7 +511,7 @@ pub trait RulesetCreatedAttr: Sized + AsMut + Compatible { /// /// struct PathEnv { /// paths: Vec, - /// access: BitFlags, + /// access: AccessFs, /// } /// /// impl PathEnv { @@ -524,7 +522,7 @@ pub trait RulesetCreatedAttr: Sized + AsMut + Compatible { /// // no restrictions are applied. /// // `access` is the set of access rights allowed for each of the parsed paths. /// fn new<'a>( - /// env_var: &'a str, access: BitFlags + /// env_var: &'a str, access: AccessFs /// ) -> Result> { /// Ok(Self { /// paths: env::var_os(env_var) @@ -553,7 +551,7 @@ pub trait RulesetCreatedAttr: Sized + AsMut + Compatible { /// .handle_access(AccessFs::from_all(ABI::V1))? /// .create()? /// // In the shell: export EXECUTABLE_PATH="/usr:/bin:/sbin" - /// .add_rules(PathEnv::new("EXECUTABLE_PATH", AccessFs::Execute.into())?.iter())? + /// .add_rules(PathEnv::new("EXECUTABLE_PATH", AccessFs::Execute)?.iter())? /// .restrict_self()?) /// } /// ``` @@ -586,8 +584,8 @@ pub trait RulesetCreatedAttr: Sized + AsMut + Compatible { pub struct RulesetCreated { fd: RawFd, no_new_privs: bool, - pub(crate) requested_handled_fs: BitFlags, - pub(crate) requested_handled_net: BitFlags, + pub(crate) requested_handled_fs: AccessFs, + pub(crate) requested_handled_net: AccessNet, compat: Compatibility, } @@ -951,7 +949,7 @@ fn ruleset_unsupported() { // Tests inconsistency between the ruleset handled access-rights and the rule access-rights. for handled_access in &[ make_bitflags!(AccessFs::{Execute | WriteFile}), - AccessFs::Execute.into(), + AccessFs::Execute, ] { let ruleset = Ruleset::from(ABI::V1) .handle_access(*handled_access)