use crate::compat::private::OptionCompatLevelMut;
use crate::{
uapi, Access, AccessFs, AddRuleError, AddRulesError, BitFlags, CompatLevel, CompatState,
Compatibility, Compatible, CreateRulesetError, RestrictSelfError, RulesetError, TryCompat,
};
use libc::close;
use std::io::Error;
use std::mem::size_of_val;
use std::os::unix::io::RawFd;
#[cfg(test)]
use crate::*;
pub trait Rule<T>: PrivateRule<T>
where
T: Access,
{
}
pub trait PrivateRule<T>
where
Self: TryCompat<T> + Compatible,
T: Access,
{
fn as_ptr(&self) -> *const libc::c_void;
fn get_type_id(&self) -> uapi::landlock_rule_type;
fn get_flags(&self) -> u32;
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,
}
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: BitFlags<AccessFs>,
pub(crate) actual_handled_fs: BitFlags<AccessFs>,
pub(crate) compat: Compatibility,
}
impl From<Compatibility> for Ruleset {
fn from(compat: Compatibility) -> Self {
Ruleset {
requested_handled_fs: Default::default(),
actual_handled_fs: 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 {
#[allow(clippy::new_without_default)]
#[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> {
if self.requested_handled_fs.is_empty() {
return Err(CreateRulesetError::MissingHandledAccess);
}
#[cfg(test)]
assert!(!matches!(self.compat.state, CompatState::Init));
if self.compat.state == CompatState::Init {
return Err(CreateRulesetError::MissingHandledAccess);
}
if self.actual_handled_fs.is_empty() {
match self.compat.level.into() {
CompatLevel::BestEffort => {
self.compat.update(CompatState::No);
}
CompatLevel::SoftRequirement => {
self.compat.update(CompatState::Dummy);
}
CompatLevel::HardRequirement => {
return Err(CreateRulesetError::MissingHandledAccess);
}
}
}
let attr = uapi::landlock_ruleset_attr {
handled_access_fs: self.actual_handled_fs.bits(),
handled_access_net: 0,
};
match self.compat.state {
CompatState::Init | CompatState::No | CompatState::Dummy => {
Ok(RulesetCreated::new(self, -1))
}
CompatState::Full | CompatState::Partial => {
match unsafe { uapi::landlock_create_ruleset(&attr, size_of_val(&attr), 0) } {
fd if fd >= 0 => Ok(RulesetCreated::new(self, 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, U>(mut self, access: T) -> Result<Self, RulesetError>
where
T: Into<BitFlags<U>>,
U: Access,
{
U::ruleset_handle_access(self.as_mut(), access.into())?;
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_or() {
let ruleset = Ruleset::from(ABI::V1)
.handle_access(AccessFs::Execute)
.unwrap()
.handle_access(AccessFs::ReadDir)
.unwrap();
let access = make_bitflags!(AccessFs::{Execute | ReadDir});
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
));
}
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 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 {
uapi::landlock_add_rule(
self_ref.fd,
compat_rule.get_type_id(),
compat_rule.as_ptr(),
compat_rule.get_flags(),
)
} {
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
}
}
#[cfg_attr(test, derive(Debug))]
pub struct RulesetCreated {
fd: RawFd,
no_new_privs: bool,
pub(crate) requested_handled_fs: BitFlags<AccessFs>,
compat: Compatibility,
}
impl RulesetCreated {
fn new(ruleset: Ruleset, fd: RawFd) -> Self {
#[cfg(test)]
assert!(!matches!(ruleset.compat.state, CompatState::Init));
RulesetCreated {
fd,
no_new_privs: true,
requested_handled_fs: ruleset.requested_handled_fs,
compat: ruleset.compat,
}
}
pub fn restrict_self(mut self) -> Result<RestrictionStatus, RulesetError> {
let mut body = || -> Result<RestrictionStatus, RestrictSelfError> {
let enforced_nnp = if self.compat.state != CompatState::Dummy && 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(),
no_new_privs: enforced_nnp,
}),
CompatState::Full | CompatState::Partial => {
match unsafe { uapi::landlock_restrict_self(self.fd, 0) } {
0 => {
self.compat.update(CompatState::Full);
Ok(RestrictionStatus {
ruleset: self.compat.state.into(),
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: match self.fd {
-1 => -1,
self_fd => match unsafe { libc::fcntl(self_fd, libc::F_DUPFD_CLOEXEC, 0) } {
dup_fd if dup_fd >= 0 => dup_fd,
_ => return Err(Error::last_os_error()),
},
},
no_new_privs: self.no_new_privs,
requested_handled_fs: self.requested_handled_fs,
compat: self.compat,
})
}
}
impl Drop for RulesetCreated {
fn drop(&mut self) {
if self.fd >= 0 {
unsafe { close(self.fd) };
}
}
}
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()
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
no_new_privs: true,
}
);
}
#[test]
fn ruleset_unsupported() {
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.restrict_self()
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
no_new_privs: true,
}
);
assert_eq!(
Ruleset::from(ABI::Unsupported)
.set_compatibility(CompatLevel::SoftRequirement)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.restrict_self()
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
no_new_privs: false,
}
);
matches!(
Ruleset::from(ABI::Unsupported)
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::Execute)
.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()
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
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()
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
no_new_privs: false,
}
);
}
assert_eq!(
Ruleset::from(ABI::Unsupported)
.handle_access(AccessFs::Execute)
.unwrap()
.create()
.unwrap()
.set_no_new_privs(false)
.restrict_self()
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
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)
)))
));
for handled_access in &[
make_bitflags!(AccessFs::{Execute | WriteFile}),
AccessFs::Execute.into(),
] {
let ruleset = Ruleset::from(ABI::V1)
.handle_access(*handled_access)
.unwrap();
let ruleset_created = RulesetCreated::new(ruleset, -1);
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()
.unwrap(),
RestrictionStatus {
ruleset: RulesetStatus::NotEnforced,
no_new_privs: false,
}
);
}