diff --git a/Cargo.toml b/Cargo.toml index a07a8e19..233c8d40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ exclude = [".gitignore"] readme = "README.md" [dependencies] -enumflags2 = "0.7" +bitflags = "2.6.0" libc = "0.2.133" thiserror = "1.0" diff --git a/examples/sandboxer.rs b/examples/sandboxer.rs index d069ab16..e9d9ebed 100644 --- a/examples/sandboxer.rs +++ b/examples/sandboxer.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, bail}; 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..e7de2f53 100644 --- a/src/access.rs +++ b/src/access.rs @@ -1,21 +1,41 @@ +use std::ops::{BitAnd, BitOr}; + 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))* + }; +} -pub trait Access: PrivateAccess { +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: Copy + Eq + BitOr + BitAnd { + fn is_empty_flags(self) -> bool + where + Self: Access; + + fn all() -> Self + where + Self: Access; + + fn known_unknown_flags(self, all: Self) -> (Self, Self) + where + Self: Access; + fn ruleset_handle_access( ruleset: &mut Ruleset, - access: BitFlags, + access: Self, ) -> Result<(), HandleAccessesError> where Self: Access; @@ -29,58 +49,49 @@ 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, { fn try_compat_inner(&mut self, abi: ABI) -> Result, CompatError> { - if self.is_empty() { + let (_known_flags, unknown_flags) = self.known_unknown_flags(Self::all()); + if self.is_empty_flags() { // Empty access-rights would result to a runtime error. Err(AccessError::Empty.into()) - } else if !Self::all().contains(*self) { + } else if !unknown_flags.is_empty_flags() { // 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(). + // This can only be reached by using Access*::from_bits_retain(). Err(AccessError::Unknown { access: *self, - unknown: *self & full_negation(Self::all()), + unknown: unknown_flags, } .into()) } else { - let compat = *self & A::from_all(abi); - let ret = if compat.is_empty() { + let (compatible_flags, incompatible_flags) = self.known_unknown_flags(A::from_all(abi)); + let ret = if compatible_flags.is_empty_flags() { Ok(CompatResult::No( AccessError::Incompatible { access: *self }.into(), )) - } else if compat != *self { + } else if !incompatible_flags.is_empty_flags() { let error = AccessError::PartiallyCompatible { access: *self, - incompatible: *self & full_negation(compat), + incompatible: incompatible_flags, } .into(); Ok(CompatResult::Partial(error)) } else { Ok(CompatResult::Full) }; - *self = compat; + *self = compatible_flags; ret } } @@ -103,7 +114,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,7 +122,7 @@ fn compat_bit_flags() { CompatError::Access(AccessError::Empty) )); - let all_unknown_access = unsafe { BitFlags::::from_bits_unchecked(1 << 63) }; + let all_unknown_access = AccessFs::from_bits_retain(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 @@ -119,7 +130,7 @@ fn compat_bit_flags() { // An error makes the state final. assert!(compat.state == CompatState::Dummy); - let some_unknown_access = unsafe { BitFlags::::from_bits_unchecked(1 << 63 | 1) }; + let some_unknown_access = AccessFs::from_bits_retain(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 diff --git a/src/errors.rs b/src/errors.rs index de018741..0493f3c4 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,18 @@ where /// kernel. #[error("empty access-right")] Empty, - /// The access-rights set was forged with the unsafe `BitFlags::from_bits_unchecked()` and it + /// The access-rights set was forged with `Access*::from_bits_retain()` and it /// contains unknown bits. #[error("unknown access-rights (at build time): {unknown:?}")] - Unknown { - access: BitFlags, - unknown: BitFlags, - }, + Unknown { access: T, unknown: T }, /// 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 09fa65be..6bfe9076 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,3 +1,5 @@ +#![allow(non_upper_case_globals)] + use crate::compat::private::OptionCompatLevelMut; use crate::{ uapi, Access, AddRuleError, AddRulesError, CompatError, CompatLevel, CompatResult, CompatState, @@ -5,7 +7,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,77 +19,76 @@ 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, +bitflags::bitflags! { + /// 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)); + /// ``` + /// + /// # Warning + /// + /// To avoid unknown restrictions **don't use `AccessFs::all()`**, + /// but use a version you tested and vetted instead, + /// for instance [`AccessFs::from_all(ABI::V1)`](Access::from_all). + /// See [`ABI`] for the rationale and help to test it. + #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] + 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; + } } +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 @@ -101,9 +101,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 => make_bitflags!(AccessFs::{ Execute | ReadFile @@ -115,9 +115,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 @@ -136,7 +136,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 } } @@ -153,9 +153,27 @@ fn consistent_access_fs_rw() { } impl PrivateAccess for AccessFs { + fn is_empty_flags(self) -> bool { + AccessFs::is_empty(&self) + } + + fn all() -> Self { + Self::all() + } + + fn known_unknown_flags(self, all: Self) -> (Self, Self) + where + Self: Access, + { + ( + self & all, + AccessFs::from_bits_retain(self.bits() & !all.bits()), + ) + } + 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; @@ -184,7 +202,7 @@ 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::{ +const ACCESS_FILE: AccessFs = make_bitflags!(AccessFs::{ ReadFile | WriteFile | Execute | Truncate }); @@ -218,7 +236,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, } @@ -231,7 +249,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. @@ -350,7 +368,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) @@ -568,16 +586,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 317ed058..dbbacd38 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 9db80207..ba457297 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,69 +1,87 @@ +#![allow(non_upper_case_globals)] + use crate::compat::private::OptionCompatLevelMut; use crate::{ uapi, Access, AddRuleError, AddRulesError, CompatError, CompatLevel, CompatResult, CompatState, 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 file system. -/// 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, +bitflags::bitflags! { + /// 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 file system. + /// + /// # 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); + /// ``` + /// + /// # Warning + /// + /// To avoid unknown restrictions **don't use `AccessNet::all()`, + /// but use a version you tested and vetted instead, + /// for instance [`AccessNet::from_all(ABI::V4)`](Access::from_all). + /// See [`ABI`] for the rationale and help to test it. + #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] + 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 => AccessNet::BindTcp | AccessNet::ConnectTcp, } } } impl PrivateAccess for AccessNet { + fn is_empty_flags(self) -> bool { + AccessNet::is_empty(&self) + } + + fn all() -> Self { + Self::all() + } + + fn known_unknown_flags(self, all: Self) -> (Self, Self) + where + Self: Access, + { + ( + self & all, + AccessNet::from_bits_retain(self.bits() & !all.bits()), + ) + } + 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 +124,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 +137,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..e69dda25 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)