use std::{
io::Error,
mem::size_of_val,
os::{
fd::{AsRawFd, FromRawFd},
unix::io::{IntoRawFd, RawFd},
},
};
use bitflags::bitflags;
use libc::close;
#[cfg(test)]
use crate::landlock::*;
use crate::{
fd::SafeOwnedFd,
landlock::{
access::PrivateAccess,
compat::{private::OptionCompatLevelMut, LandlockStatus, ABI},
uapi, Access, AccessFs, AccessNet, AddRuleError, AddRulesError, CompatLevel, CompatState,
Compatibility, Compatible, CreateRulesetError, HandleAccessError, HandleAccessesError,
RestrictSelfError, RulesetError, Scope, TryCompat,
},
};
pub trait Rule<T>: PrivateRule<T>
where
T: Access,
{
}
pub trait PrivateRule<T>
where
Self: TryCompat<T> + Compatible,
T: Access,
{
const TYPE_ID: uapi::landlock_rule_type;
fn as_ptr(&mut self) -> *const libc::c_void;
fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError>;
}
#[derive(Debug, PartialEq, Eq)]
pub enum RulesetStatus {
FullyEnforced,
PartiallyEnforced,
NotEnforced,
}
impl From<CompatState> for RulesetStatus {
fn from(state: CompatState) -> Self {
match state {
CompatState::Init | CompatState::No | CompatState::Dummy => RulesetStatus::NotEnforced,
CompatState::Full => RulesetStatus::FullyEnforced,
CompatState::Partial => RulesetStatus::PartiallyEnforced,
}
}
}
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct RestrictionStatus {
pub ruleset: RulesetStatus,
pub no_new_privs: bool,
pub landlock: LandlockStatus,
}
fn prctl_set_no_new_privs() -> Result<(), Error> {
match unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } {
0 => Ok(()),
_ => Err(Error::last_os_error()),
}
}
fn support_no_new_privs() -> bool {
matches!(
unsafe { libc::prctl(libc::PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) },
0 | 1
)
}
#[cfg_attr(test, derive(Debug))]
pub struct Ruleset {
pub(crate) requested_handled_fs: AccessFs,
pub(crate) requested_handled_net: AccessNet,
pub(crate) requested_scoped: Scope,
pub(crate) actual_handled_fs: AccessFs,
pub(crate) actual_handled_net: AccessNet,
pub(crate) actual_scoped: Scope,
pub(crate) compat: Compatibility,
}
impl From<Compatibility> for Ruleset {
fn from(compat: Compatibility) -> Self {
Ruleset {
requested_handled_fs: Default::default(),
requested_handled_net: Default::default(),
requested_scoped: Default::default(),
actual_handled_fs: Default::default(),
actual_handled_net: Default::default(),
actual_scoped: Default::default(),
compat,
}
}
}
#[cfg(test)]
impl From<ABI> for Ruleset {
fn from(abi: ABI) -> Self {
Ruleset::from(Compatibility::from(abi))
}
}
#[test]
fn ruleset_add_rule_iter() {
assert!(matches!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.add_rule(PathBeneath::new(
PathFd::new("/").unwrap(),
AccessFs::ReadFile
))
.unwrap_err(),
RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { .. }))
));
}
impl Default for Ruleset {
fn default() -> Self {
Compatibility::new().into()
}
}
impl Ruleset {
#[deprecated(note = "Use Ruleset::default() instead")]
pub fn new() -> Self {
Ruleset::default()
}
pub fn create(mut self) -> Result<RulesetCreated, RulesetError> {
let body = || -> Result<RulesetCreated, CreateRulesetError> {
match self.compat.state {
CompatState::Init => {
Err(CreateRulesetError::MissingHandledAccess)
}
CompatState::No | CompatState::Dummy => {
#[cfg(test)]
assert!(
!self.requested_handled_fs.is_empty()
|| !self.requested_handled_net.is_empty()
|| !self.requested_scoped.is_empty()
);
self.compat.update(CompatState::Dummy);
match self.compat.level.into() {
CompatLevel::HardRequirement => {
Err(CreateRulesetError::MissingHandledAccess)
}
_ => Ok(RulesetCreated::new(self, None)),
}
}
CompatState::Full | CompatState::Partial => {
#[cfg(test)]
assert!(
!self.actual_handled_fs.is_empty()
|| !self.actual_handled_net.is_empty()
|| !self.actual_scoped.is_empty()
);
let attr = uapi::landlock_ruleset_attr {
handled_access_fs: self.actual_handled_fs.bits(),
handled_access_net: self.actual_handled_net.bits(),
scoped: self.actual_scoped.bits(),
};
match unsafe { uapi::landlock_create_ruleset(&attr, size_of_val(&attr), 0) } {
fd if fd >= 0 => Ok(RulesetCreated::new(
self,
Some(unsafe { SafeOwnedFd::from_raw_fd(fd) }),
)),
_ => Err(CreateRulesetError::CreateRulesetCall {
source: Error::last_os_error(),
}),
}
}
}
};
Ok(body()?)
}
}
impl OptionCompatLevelMut for Ruleset {
fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
&mut self.compat.level
}
}
impl OptionCompatLevelMut for &mut Ruleset {
fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
&mut self.compat.level
}
}
impl Compatible for Ruleset {}
impl Compatible for &mut Ruleset {}
impl AsMut<Ruleset> for Ruleset {
fn as_mut(&mut self) -> &mut Ruleset {
self
}
}
#[test]
fn ruleset_as_mut() {
let mut ruleset = Ruleset::from(ABI::Unsupported);
let _ = ruleset.as_mut();
let mut ruleset_created = Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap();
let _ = ruleset_created.as_mut();
}
pub trait RulesetAttr: Sized + AsMut<Ruleset> + Compatible {
fn handle_access<T>(mut self, access: T) -> Result<Self, RulesetError>
where
T: Access,
{
T::ruleset_handle_access(self.as_mut(), access)?;
Ok(self)
}
fn scope<T>(mut self, scope: T) -> Result<Self, RulesetError>
where
T: Into<Scope>,
{
let scope = scope.into();
let ruleset = self.as_mut();
ruleset.requested_scoped |= scope;
if let Some(a) = scope
.try_compat(
ruleset.compat.abi(),
ruleset.compat.level,
&mut ruleset.compat.state,
)
.map_err(|err| HandleAccessesError::Scope(HandleAccessError::Compat(err)))?
{
ruleset.actual_scoped |= a;
}
Ok(self)
}
}
impl RulesetAttr for Ruleset {}
impl RulesetAttr for &mut Ruleset {}
#[test]
fn ruleset_attr() {
let mut ruleset = Ruleset::from(ABI::Unsupported);
let ruleset_ref = &mut ruleset;
ruleset_ref
.set_compatibility(CompatLevel::BestEffort)
.handle_access(AccessFs::Execute)
.unwrap()
.handle_access(AccessFs::ReadFile)
.unwrap();
ruleset
.set_compatibility(CompatLevel::BestEffort)
.handle_access(AccessFs::Execute)
.unwrap()
.handle_access(AccessFs::WriteFile)
.unwrap()
.create()
.unwrap();
}
#[test]
fn ruleset_created_handle_access_fs() {
let access = make_bitflags!(AccessFs::{Execute | ReadDir});
let ruleset = Ruleset::from(ABI::V1).handle_access(access).unwrap();
assert_eq!(ruleset.requested_handled_fs, access);
assert_eq!(ruleset.actual_handled_fs, access);
let ruleset = Ruleset::from(ABI::V1)
.handle_access(AccessFs::Execute)
.unwrap()
.handle_access(AccessFs::ReadDir)
.unwrap()
.handle_access(AccessFs::Execute)
.unwrap();
assert_eq!(ruleset.requested_handled_fs, access);
assert_eq!(ruleset.actual_handled_fs, access);
assert!(matches!(Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::ReadDir)
.unwrap_err(),
RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat(
CompatError::Access(AccessError::Incompatible { access })
))) if access == AccessFs::ReadDir
));
}
#[test]
fn ruleset_created_handle_access_net_tcp() {
let access = make_bitflags!(AccessNet::{BindTcp | ConnectTcp});
let ruleset = Ruleset::from(ABI::V3).handle_access(access).unwrap();
assert_eq!(ruleset.requested_handled_net, access);
assert_eq!(ruleset.actual_handled_net, AccessNet::EMPTY);
let ruleset = Ruleset::from(ABI::V4).handle_access(access).unwrap();
assert_eq!(ruleset.requested_handled_net, access);
assert_eq!(ruleset.actual_handled_net, access);
let ruleset = Ruleset::from(ABI::V4)
.handle_access(AccessNet::BindTcp)
.unwrap()
.handle_access(AccessNet::ConnectTcp)
.unwrap()
.handle_access(AccessNet::BindTcp)
.unwrap();
assert_eq!(ruleset.requested_handled_net, access);
assert_eq!(ruleset.actual_handled_net, access);
assert!(matches!(Ruleset::from(ABI::Unsupported)
.handle_access(AccessNet::BindTcp)
.unwrap()
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessNet::ConnectTcp)
.unwrap_err(),
RulesetError::HandleAccesses(HandleAccessesError::Net(HandleAccessError::Compat(
CompatError::Access(AccessError::Incompatible { access })
))) if access == AccessNet::ConnectTcp
));
}
#[test]
fn ruleset_created_scope() {
let scopes = make_bitflags!(Scope::{AbstractUnixSocket | Signal});
let ruleset = Ruleset::from(ABI::V5).scope(scopes).unwrap();
assert_eq!(ruleset.requested_scoped, scopes);
assert_eq!(ruleset.actual_scoped, Scope::EMPTY);
let ruleset = Ruleset::from(ABI::V6).scope(scopes).unwrap();
assert_eq!(ruleset.requested_scoped, scopes);
assert_eq!(ruleset.actual_scoped, scopes);
let ruleset = Ruleset::from(ABI::V6)
.scope(Scope::AbstractUnixSocket)
.unwrap()
.scope(Scope::Signal)
.unwrap()
.scope(Scope::AbstractUnixSocket)
.unwrap();
assert_eq!(ruleset.requested_scoped, scopes);
assert_eq!(ruleset.actual_scoped, scopes);
assert!(matches!(Ruleset::from(ABI::Unsupported)
.scope(Scope::AbstractUnixSocket)
.unwrap()
.set_compatibility(CompatLevel::HardRequirement)
.scope(Scope::Signal)
.unwrap_err(),
RulesetError::HandleAccesses(HandleAccessesError::Scope(HandleAccessError::Compat(
CompatError::Access(AccessError::Incompatible { access })
))) if access == Scope::Signal
));
}
#[test]
fn ruleset_created_fs_net_scope() {
let access_fs = make_bitflags!(AccessFs::{Execute | ReadDir});
let access_net = make_bitflags!(AccessNet::{BindTcp | ConnectTcp});
let scopes = make_bitflags!(Scope::{AbstractUnixSocket | Signal});
let ruleset = Ruleset::from(ABI::V5)
.handle_access(access_fs)
.unwrap()
.scope(scopes)
.unwrap()
.handle_access(access_net)
.unwrap();
assert_eq!(ruleset.requested_handled_fs, access_fs);
assert_eq!(ruleset.actual_handled_fs, access_fs);
assert_eq!(ruleset.requested_handled_net, access_net);
assert_eq!(ruleset.actual_handled_net, access_net);
assert_eq!(ruleset.requested_scoped, scopes);
assert_eq!(ruleset.actual_scoped, Scope::EMPTY);
let ruleset = Ruleset::from(ABI::V6)
.handle_access(access_fs)
.unwrap()
.scope(scopes)
.unwrap()
.handle_access(access_net)
.unwrap();
assert_eq!(ruleset.requested_handled_fs, access_fs);
assert_eq!(ruleset.actual_handled_fs, access_fs);
assert_eq!(ruleset.requested_handled_net, access_net);
assert_eq!(ruleset.actual_handled_net, access_net);
assert_eq!(ruleset.requested_scoped, scopes);
assert_eq!(ruleset.actual_scoped, scopes);
}
impl OptionCompatLevelMut for RulesetCreated {
fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
&mut self.compat.level
}
}
impl OptionCompatLevelMut for &mut RulesetCreated {
fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
&mut self.compat.level
}
}
impl Compatible for RulesetCreated {}
impl Compatible for &mut RulesetCreated {}
pub trait RulesetCreatedAttr: Sized + AsMut<RulesetCreated> + Compatible {
fn add_rule<T, U>(mut self, rule: T) -> Result<Self, RulesetError>
where
T: Rule<U>,
U: Access,
{
let body = || -> Result<Self, AddRulesError> {
let self_ref = self.as_mut();
rule.check_consistency(self_ref)?;
let mut compat_rule = match rule
.try_compat(
self_ref.compat.abi(),
self_ref.compat.level,
&mut self_ref.compat.state,
)
.map_err(AddRuleError::Compat)?
{
Some(r) => r,
None => return Ok(self),
};
match self_ref.compat.state {
CompatState::Init | CompatState::No | CompatState::Dummy => Ok(self),
CompatState::Full | CompatState::Partial => match unsafe {
#[cfg(test)]
assert!(self_ref.fd.is_some());
let fd = self_ref.fd.as_ref().map(|f| f.as_raw_fd()).unwrap_or(-1);
uapi::landlock_add_rule(fd, T::TYPE_ID, compat_rule.as_ptr(), 0)
} {
0 => Ok(self),
_ => Err(AddRuleError::<U>::AddRuleCall {
source: Error::last_os_error(),
}
.into()),
},
}
};
Ok(body()?)
}
fn add_rules<I, T, U, E>(mut self, rules: I) -> Result<Self, E>
where
I: IntoIterator<Item = Result<T, E>>,
T: Rule<U>,
U: Access,
E: From<RulesetError>,
{
for rule in rules {
self = self.add_rule(rule?)?;
}
Ok(self)
}
fn set_no_new_privs(mut self, no_new_privs: bool) -> Self {
<Self as AsMut<RulesetCreated>>::as_mut(&mut self).no_new_privs = no_new_privs;
self
}
}
bitflags! {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub struct RestrictSelfFlags: u32 {
const LOG_SAME_EXEC_OFF = uapi::LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF;
const LOG_NEW_EXEC_ON = uapi::LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
const LOG_SUBDOMAINS_OFF = uapi::LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF;
const TSYNC = uapi::LANDLOCK_RESTRICT_SELF_TSYNC;
const MASK_V7 =
Self::LOG_SAME_EXEC_OFF.bits() |
Self::LOG_NEW_EXEC_ON.bits() |
Self::LOG_SUBDOMAINS_OFF.bits();
const MASK_V8 =
Self::MASK_V7.bits() |
Self::TSYNC.bits();
}
}
impl RestrictSelfFlags {
pub const fn supported(abi: ABI) -> Self {
match abi {
ABI::Unsupported | ABI::V1 | ABI::V2 | ABI::V3 | ABI::V4 | ABI::V5 | ABI::V6 => {
Self::empty()
}
ABI::V7 => Self::MASK_V7,
ABI::V8 => Self::MASK_V8,
}
}
#[inline]
pub fn retain_supported(self, abi: ABI) -> Self {
self & Self::supported(abi)
}
#[inline]
pub fn unsupported(self, abi: ABI) -> Self {
self & !Self::supported(abi)
}
}
impl std::fmt::Display for RestrictSelfFlags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
const FLAGS: &[(RestrictSelfFlags, &str)] = &[
(RestrictSelfFlags::LOG_SAME_EXEC_OFF, "log_same_exec_off"),
(RestrictSelfFlags::LOG_NEW_EXEC_ON, "log_new_exec_on"),
(RestrictSelfFlags::LOG_SUBDOMAINS_OFF, "log_subdomains_off"),
];
let mut first = true;
for (flag, name) in FLAGS {
if self.contains(*flag) {
if !first {
f.write_str(",")?;
}
f.write_str(name)?;
first = false;
}
}
Ok(())
}
}
#[cfg_attr(test, derive(Debug))]
pub struct RulesetCreated {
fd: Option<SafeOwnedFd>,
no_new_privs: bool,
pub(crate) requested_handled_fs: AccessFs,
pub(crate) requested_handled_net: AccessNet,
compat: Compatibility,
}
impl RulesetCreated {
pub(crate) fn new(ruleset: Ruleset, fd: Option<SafeOwnedFd>) -> Self {
#[cfg(test)]
assert!(!matches!(ruleset.compat.state, CompatState::Init));
RulesetCreated {
fd,
no_new_privs: true,
requested_handled_fs: ruleset.requested_handled_fs,
requested_handled_net: ruleset.requested_handled_net,
compat: ruleset.compat,
}
}
pub fn restrict_self(
mut self,
flags: RestrictSelfFlags,
) -> Result<RestrictionStatus, RulesetError> {
let mut body = || -> Result<RestrictionStatus, RestrictSelfError> {
let enforced_nnp = if self.no_new_privs {
if let Err(e) = prctl_set_no_new_privs() {
match self.compat.level.into() {
CompatLevel::BestEffort => {}
CompatLevel::SoftRequirement => {
self.compat.update(CompatState::Dummy);
}
CompatLevel::HardRequirement => {
return Err(RestrictSelfError::SetNoNewPrivsCall { source: e });
}
}
let support_nnp = support_no_new_privs();
match self.compat.state {
CompatState::Init | CompatState::No | CompatState::Dummy => {
if support_nnp {
return Err(RestrictSelfError::SetNoNewPrivsCall { source: e });
}
}
CompatState::Full | CompatState::Partial => {
return Err(RestrictSelfError::SetNoNewPrivsCall { source: e })
}
}
false
} else {
true
}
} else {
false
};
match self.compat.state {
CompatState::Init | CompatState::No | CompatState::Dummy => Ok(RestrictionStatus {
ruleset: self.compat.state.into(),
landlock: self.compat.status(),
no_new_privs: enforced_nnp,
}),
CompatState::Full | CompatState::Partial => {
#[cfg(test)]
assert!(self.fd.is_some());
let fd = self.fd.as_ref().map(|f| f.as_raw_fd()).unwrap_or(-1);
let flags = flags.retain_supported(self.compat.abi());
match unsafe { uapi::landlock_restrict_self(fd, flags.bits()) } {
0 => {
self.compat.update(CompatState::Full);
Ok(RestrictionStatus {
ruleset: self.compat.state.into(),
landlock: self.compat.status(),
no_new_privs: enforced_nnp,
})
}
_ => Err(RestrictSelfError::RestrictSelfCall {
source: Error::last_os_error(),
}),
}
}
}
};
Ok(body()?)
}
pub fn try_clone(&self) -> std::io::Result<Self> {
Ok(RulesetCreated {
fd: self.fd.as_ref().map(|f| f.try_clone()).transpose()?,
no_new_privs: self.no_new_privs,
requested_handled_fs: self.requested_handled_fs,
requested_handled_net: self.requested_handled_net,
compat: self.compat,
})
}
}
impl From<RulesetCreated> for Option<SafeOwnedFd> {
fn from(ruleset: RulesetCreated) -> Self {
ruleset.fd
}
}
#[test]
fn ruleset_created_ownedfd_none() {
let ruleset = Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap();
let fd: Option<SafeOwnedFd> = ruleset.into();
assert!(fd.is_none());
}
impl AsMut<RulesetCreated> for RulesetCreated {
fn as_mut(&mut self) -> &mut RulesetCreated {
self
}
}
impl RulesetCreatedAttr for RulesetCreated {}
impl RulesetCreatedAttr for &mut RulesetCreated {}
#[test]
fn ruleset_created_attr() {
let mut ruleset_created = Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap();
let ruleset_created_ref = &mut ruleset_created;
ruleset_created_ref
.set_compatibility(CompatLevel::BestEffort)
.add_rule(PathBeneath::new(
PathFd::new("/usr").unwrap(),
AccessFs::Execute,
))
.unwrap()
.add_rule(PathBeneath::new(
PathFd::new("/etc").unwrap(),
AccessFs::Execute,
))
.unwrap();
assert_eq!(
ruleset_created
.set_compatibility(CompatLevel::BestEffort)
.add_rule(PathBeneath::new(
PathFd::new("/tmp").unwrap(),
AccessFs::Execute,
))
.unwrap()
.add_rule(PathBeneath::new(
PathFd::new("/var").unwrap(),
AccessFs::Execute,
))
.unwrap()
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: true,
}
);
}
#[test]
fn ruleset_compat_dummy() {
for level in [CompatLevel::BestEffort, CompatLevel::SoftRequirement] {
println!("level: {:?}", level);
let ruleset = Ruleset::from(ABI::Unsupported);
assert_eq!(ruleset.compat.state, CompatState::Init);
let ruleset = ruleset.set_compatibility(level);
assert_eq!(ruleset.compat.state, CompatState::Init);
let ruleset = ruleset.handle_access(AccessFs::Execute).unwrap();
assert_eq!(
ruleset.compat.state,
match level {
CompatLevel::BestEffort => CompatState::No,
CompatLevel::SoftRequirement => CompatState::Dummy,
_ => unreachable!(),
}
);
let ruleset_created = ruleset.create().unwrap();
assert_eq!(ruleset_created.compat.state, CompatState::Dummy);
let ruleset_created = ruleset_created
.add_rule(PathBeneath::new(
PathFd::new("/usr").unwrap(),
AccessFs::Execute,
))
.unwrap();
assert_eq!(ruleset_created.compat.state, CompatState::Dummy);
}
}
#[test]
fn ruleset_compat_partial() {
let ruleset = Ruleset::from(ABI::V1);
assert_eq!(ruleset.compat.state, CompatState::Init);
let ruleset = ruleset.handle_access(AccessFs::Refer).unwrap();
assert_eq!(ruleset.compat.state, CompatState::No);
let ruleset = ruleset.handle_access(AccessFs::Execute).unwrap();
assert_eq!(ruleset.compat.state, CompatState::Partial);
let ruleset = ruleset.handle_access(AccessFs::Refer).unwrap();
assert_eq!(ruleset.compat.state, CompatState::Partial);
}
#[test]
fn ruleset_unsupported() {
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: true,
}
);
assert_eq!(
Ruleset::from(ABI::Unsupported)
.set_compatibility(CompatLevel::SoftRequirement)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: true,
}
);
matches!(
Ruleset::from(ABI::Unsupported)
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::Execute)
.unwrap_err(),
RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess)
);
matches!(
Ruleset::from(ABI::Unsupported)
.set_compatibility(CompatLevel::HardRequirement)
.scope(Scope::Signal)
.unwrap_err(),
RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess)
);
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.set_compatibility(CompatLevel::SoftRequirement)
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: true,
}
);
if compat::can_emulate(ABI::V1, ABI::V1, Some(ABI::V2)) {
assert_eq!(
Ruleset::from(ABI::V1)
.handle_access(make_bitflags!(AccessFs::{Execute | Refer}))
.unwrap()
.create()
.unwrap()
.set_compatibility(CompatLevel::SoftRequirement)
.add_rule(PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Refer))
.unwrap()
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::Available(ABI::V1, None),
no_new_privs: true,
}
);
}
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.set_no_new_privs(false)
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: false,
}
);
assert!(matches!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::from_all(ABI::Unsupported))
.unwrap_err(),
RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat(
CompatError::Access(AccessError::Empty)
)))
));
assert!(matches!(
Ruleset::from(ABI::Unsupported)
.create()
.unwrap_err(),
RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess)
));
assert!(matches!(
Ruleset::from(ABI::V1)
.handle_access(AccessFs::from_all(ABI::Unsupported))
.unwrap_err(),
RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat(
CompatError::Access(AccessError::Empty)
)))
));
assert!(matches!(
Ruleset::from(ABI::Unsupported)
.scope(Scope::from_all(ABI::Unsupported))
.unwrap_err(),
RulesetError::HandleAccesses(HandleAccessesError::Scope(HandleAccessError::Compat(
CompatError::Access(AccessError::Empty)
)))
));
assert!(matches!(
Ruleset::from(ABI::V1)
.scope(Scope::from_all(ABI::Unsupported))
.unwrap_err(),
RulesetError::HandleAccesses(HandleAccessesError::Scope(HandleAccessError::Compat(
CompatError::Access(AccessError::Empty)
)))
));
for handled_access in &[
make_bitflags!(AccessFs::{Execute | WriteFile}),
AccessFs::Execute,
] {
let ruleset = Ruleset::from(ABI::V1)
.handle_access(*handled_access)
.unwrap();
let ruleset_created = RulesetCreated::new(ruleset, None);
assert!(matches!(
ruleset_created
.add_rule(PathBeneath::new(
PathFd::new("/").unwrap(),
AccessFs::ReadFile
))
.unwrap_err(),
RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { .. }))
));
}
}
#[test]
fn ignore_abi_v2_with_abi_v1() {
assert_eq!(
Ruleset::from(ABI::V1)
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::from_all(ABI::V1))
.unwrap()
.set_compatibility(CompatLevel::SoftRequirement)
.handle_access(AccessFs::Refer)
.unwrap()
.create()
.unwrap()
.add_rule(PathBeneath::new(
PathFd::new("/tmp").unwrap(),
AccessFs::from_all(ABI::V2)
))
.unwrap()
.add_rule(PathBeneath::new(
PathFd::new("/usr").unwrap(),
make_bitflags!(AccessFs::{ReadFile | ReadDir})
))
.unwrap()
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::Available(ABI::V1, None),
no_new_privs: true,
}
);
}
#[test]
fn unsupported_handled_access() {
matches!(
Ruleset::from(ABI::V3)
.handle_access(AccessNet::from_all(ABI::V3))
.unwrap_err(),
RulesetError::HandleAccesses(HandleAccessesError::Net(HandleAccessError::Compat(
CompatError::Access(AccessError::Empty)
)))
);
}
#[test]
fn unsupported_handled_access_errno() {
assert_eq!(
Errno::from(
Ruleset::from(ABI::V3)
.handle_access(AccessNet::from_all(ABI::V3))
.unwrap_err()
),
Errno::new(libc::EINVAL)
);
}
#[test]
fn restrict_self_tsync_value() {
assert_eq!(RestrictSelfFlags::TSYNC.bits(), 1 << 3);
assert_eq!(RestrictSelfFlags::TSYNC.bits(), 8);
}
#[test]
fn restrict_self_flags_no_overlap() {
assert_eq!(RestrictSelfFlags::LOG_SAME_EXEC_OFF.bits(), 1);
assert_eq!(RestrictSelfFlags::LOG_NEW_EXEC_ON.bits(), 2);
assert_eq!(RestrictSelfFlags::LOG_SUBDOMAINS_OFF.bits(), 4);
assert_eq!(RestrictSelfFlags::TSYNC.bits(), 8);
let all = RestrictSelfFlags::LOG_SAME_EXEC_OFF
| RestrictSelfFlags::LOG_NEW_EXEC_ON
| RestrictSelfFlags::LOG_SUBDOMAINS_OFF
| RestrictSelfFlags::TSYNC;
assert_eq!(all.bits(), 0xf);
}
#[test]
fn restrict_self_mask_v7_and_v8() {
assert!(!RestrictSelfFlags::MASK_V7.contains(RestrictSelfFlags::TSYNC));
assert!(RestrictSelfFlags::MASK_V7.contains(RestrictSelfFlags::LOG_SAME_EXEC_OFF));
assert!(RestrictSelfFlags::MASK_V7.contains(RestrictSelfFlags::LOG_NEW_EXEC_ON));
assert!(RestrictSelfFlags::MASK_V7.contains(RestrictSelfFlags::LOG_SUBDOMAINS_OFF));
assert_eq!(RestrictSelfFlags::MASK_V7.bits(), 0x7);
assert!(RestrictSelfFlags::MASK_V8.contains(RestrictSelfFlags::TSYNC));
assert_eq!(RestrictSelfFlags::MASK_V8.bits(), 0xf);
}
#[test]
fn restrict_self_supported_abi_gating() {
for abi in [
ABI::Unsupported,
ABI::V1,
ABI::V2,
ABI::V3,
ABI::V4,
ABI::V5,
ABI::V6,
ABI::V7,
] {
assert!(
!RestrictSelfFlags::supported(abi).contains(RestrictSelfFlags::TSYNC),
"TSYNC should not be supported for {abi:?}"
);
}
for abi in [
ABI::Unsupported,
ABI::V1,
ABI::V2,
ABI::V3,
ABI::V4,
ABI::V5,
ABI::V6,
] {
assert!(
RestrictSelfFlags::supported(abi).is_empty(),
"No restrict_self flags should be supported for {abi:?}"
);
}
assert!(!RestrictSelfFlags::supported(ABI::V7).contains(RestrictSelfFlags::TSYNC));
assert!(RestrictSelfFlags::supported(ABI::V8).contains(RestrictSelfFlags::TSYNC));
}
#[test]
fn restrict_self_retain_supported_strips_tsync_below_v8() {
let flags = RestrictSelfFlags::TSYNC | RestrictSelfFlags::LOG_NEW_EXEC_ON;
assert_eq!(flags.retain_supported(ABI::V6), RestrictSelfFlags::empty());
assert_eq!(
flags.retain_supported(ABI::V7),
RestrictSelfFlags::LOG_NEW_EXEC_ON
);
assert_eq!(flags.retain_supported(ABI::V8), flags);
}
#[test]
fn restrict_self_tsync_unsupported_is_noop() {
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.restrict_self(RestrictSelfFlags::TSYNC)
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: true,
}
);
}
#[test]
fn restrict_self_tsync_combined_flags_unsupported() {
let flags = RestrictSelfFlags::TSYNC
| RestrictSelfFlags::LOG_SAME_EXEC_OFF
| RestrictSelfFlags::LOG_NEW_EXEC_ON;
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.restrict_self(flags)
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: true,
}
);
}
#[test]
fn test_ruleset_status_from_1() {
assert_eq!(
RulesetStatus::from(CompatState::Init),
RulesetStatus::NotEnforced
);
}
#[test]
fn test_ruleset_status_from_2() {
assert_eq!(
RulesetStatus::from(CompatState::No),
RulesetStatus::NotEnforced
);
}
#[test]
fn test_ruleset_status_from_3() {
assert_eq!(
RulesetStatus::from(CompatState::Dummy),
RulesetStatus::NotEnforced
);
}
#[test]
fn test_ruleset_status_from_4() {
assert_eq!(
RulesetStatus::from(CompatState::Full),
RulesetStatus::FullyEnforced
);
}
#[test]
fn test_ruleset_status_from_5() {
assert_eq!(
RulesetStatus::from(CompatState::Partial),
RulesetStatus::PartiallyEnforced
);
}
#[test]
fn test_restrict_self_flags_unsupported_1() {
let flags = RestrictSelfFlags::TSYNC | RestrictSelfFlags::LOG_NEW_EXEC_ON;
assert_eq!(flags.unsupported(ABI::V6), flags);
}
#[test]
fn test_restrict_self_flags_unsupported_2() {
let flags = RestrictSelfFlags::TSYNC | RestrictSelfFlags::LOG_NEW_EXEC_ON;
assert_eq!(flags.unsupported(ABI::V7), RestrictSelfFlags::TSYNC);
}
#[test]
fn test_restrict_self_flags_unsupported_3() {
assert_eq!(
RestrictSelfFlags::empty().unsupported(ABI::V7),
RestrictSelfFlags::empty()
);
}
#[test]
fn test_restrict_self_flags_display_1() {
let flags = RestrictSelfFlags::LOG_SAME_EXEC_OFF;
assert_eq!(format!("{flags}"), "log_same_exec_off");
}
#[test]
fn test_restrict_self_flags_display_2() {
let flags = RestrictSelfFlags::LOG_SAME_EXEC_OFF | RestrictSelfFlags::LOG_NEW_EXEC_ON;
assert_eq!(format!("{flags}"), "log_same_exec_off,log_new_exec_on");
}
#[test]
fn test_restrict_self_flags_display_3() {
let flags = RestrictSelfFlags::LOG_SAME_EXEC_OFF
| RestrictSelfFlags::LOG_NEW_EXEC_ON
| RestrictSelfFlags::LOG_SUBDOMAINS_OFF;
assert_eq!(
format!("{flags}"),
"log_same_exec_off,log_new_exec_on,log_subdomains_off"
);
}
#[test]
fn test_restrict_self_flags_display_4() {
let flags = RestrictSelfFlags::empty();
assert_eq!(format!("{flags}"), "");
}
#[test]
fn test_restrict_self_flags_default_1() {
assert_eq!(RestrictSelfFlags::default(), RestrictSelfFlags::empty());
}
#[test]
fn test_ruleset_created_try_clone_1() {
let ruleset_created = Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap();
let cloned = ruleset_created.try_clone().unwrap();
let fd: Option<SafeOwnedFd> = cloned.into();
assert!(fd.is_none());
}
#[test]
fn test_set_no_new_privs_1() {
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.set_no_new_privs(true)
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: true,
}
);
}
#[test]
fn test_set_no_new_privs_2() {
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.set_no_new_privs(false)
.restrict_self(RestrictSelfFlags::empty())
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
landlock: LandlockStatus::NotImplemented,
no_new_privs: false,
}
);
}
#[test]
fn test_ruleset_create_missing_access_1() {
assert!(matches!(
Ruleset::from(ABI::V1).create().unwrap_err(),
RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess)
));
}
#[test]
fn test_ruleset_create_hard_requirement_1() {
assert!(matches!(
Ruleset::from(ABI::Unsupported)
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::Execute)
.unwrap_err(),
RulesetError::HandleAccesses(_)
));
}
#[test]
fn test_ruleset_into_owned_fd_1() {
let ruleset = Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap();
let fd: Option<SafeOwnedFd> = ruleset.into();
assert!(fd.is_none());
}
#[test]
fn test_ruleset_created_new_1() {
let ruleset = Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap();
let created = RulesetCreated::new(ruleset, None);
assert!(created.no_new_privs);
assert_eq!(created.requested_handled_fs, AccessFs::Execute);
assert_eq!(created.requested_handled_net, AccessNet::EMPTY);
}