use crate::compat::private::OptionCompatLevelMut;
use crate::{
uapi, Access, AddRuleError, AddRulesError, CompatError, CompatLevel, CompatResult, CompatState,
Compatible, HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError,
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;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
use std::path::Path;
#[cfg(test)]
use crate::{RulesetAttr, RulesetCreatedAttr};
#[cfg(test)]
use strum::IntoEnumIterator;
#[bitflags]
#[repr(u64)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccessFs {
Execute = uapi::LANDLOCK_ACCESS_FS_EXECUTE as u64,
WriteFile = uapi::LANDLOCK_ACCESS_FS_WRITE_FILE as u64,
ReadFile = uapi::LANDLOCK_ACCESS_FS_READ_FILE as u64,
ReadDir = uapi::LANDLOCK_ACCESS_FS_READ_DIR as u64,
RemoveDir = uapi::LANDLOCK_ACCESS_FS_REMOVE_DIR as u64,
RemoveFile = uapi::LANDLOCK_ACCESS_FS_REMOVE_FILE as u64,
MakeChar = uapi::LANDLOCK_ACCESS_FS_MAKE_CHAR as u64,
MakeDir = uapi::LANDLOCK_ACCESS_FS_MAKE_DIR as u64,
MakeReg = uapi::LANDLOCK_ACCESS_FS_MAKE_REG as u64,
MakeSock = uapi::LANDLOCK_ACCESS_FS_MAKE_SOCK as u64,
MakeFifo = uapi::LANDLOCK_ACCESS_FS_MAKE_FIFO as u64,
MakeBlock = uapi::LANDLOCK_ACCESS_FS_MAKE_BLOCK as u64,
MakeSym = uapi::LANDLOCK_ACCESS_FS_MAKE_SYM as u64,
Refer = uapi::LANDLOCK_ACCESS_FS_REFER as u64,
Truncate = uapi::LANDLOCK_ACCESS_FS_TRUNCATE as u64,
}
impl Access for AccessFs {
fn from_read(abi: ABI) -> BitFlags<Self> {
match abi {
ABI::Unsupported => BitFlags::EMPTY,
ABI::V1 | ABI::V2 | ABI::V3 => make_bitflags!(AccessFs::{
Execute
| ReadFile
| ReadDir
}),
}
}
fn from_write(abi: ABI) -> BitFlags<Self> {
match abi {
ABI::Unsupported => BitFlags::EMPTY,
ABI::V1 => make_bitflags!(AccessFs::{
WriteFile
| RemoveDir
| RemoveFile
| MakeChar
| MakeDir
| MakeReg
| MakeSock
| MakeFifo
| MakeBlock
| MakeSym
}),
ABI::V2 => Self::from_write(ABI::V1) | AccessFs::Refer,
ABI::V3 => Self::from_write(ABI::V2) | AccessFs::Truncate,
}
}
}
#[test]
fn consistent_access_fs_rw() {
for abi in ABI::iter() {
let access_all = AccessFs::from_all(abi);
let access_read = AccessFs::from_read(abi);
let access_write = AccessFs::from_write(abi);
assert_eq!(access_read, !access_write & access_all);
assert_eq!(access_read | access_write, access_all);
}
}
impl AccessFs {
pub fn from_file(abi: ABI) -> BitFlags<Self> {
Self::from_all(abi) & ACCESS_FILE
}
}
impl PrivateAccess for AccessFs {
fn ruleset_handle_access(
ruleset: &mut Ruleset,
access: BitFlags<Self>,
) -> Result<(), HandleAccessesError> {
ruleset.requested_handled_fs |= access;
ruleset.actual_handled_fs |= match access
.try_compat(
ruleset.compat.abi(),
ruleset.compat.level,
&mut ruleset.compat.state,
)
.map_err(HandleAccessError::Compat)?
{
Some(a) => a,
None => return Ok(()),
};
Ok(())
}
fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
AddRulesError::Fs(error)
}
fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
HandleAccessesError::Fs(error)
}
}
const ACCESS_FILE: BitFlags<AccessFs> = make_bitflags!(AccessFs::{
ReadFile | WriteFile | Execute | Truncate
});
fn is_file<F>(fd: F) -> Result<bool, Error>
where
F: AsFd,
{
unsafe {
let mut stat = zeroed();
match libc::fstat(fd.as_fd().as_raw_fd(), &mut stat) {
0 => Ok((stat.st_mode & libc::S_IFMT) != libc::S_IFDIR),
_ => Err(Error::last_os_error()),
}
}
}
#[cfg_attr(test, derive(Debug))]
pub struct PathBeneath<F> {
attr: uapi::landlock_path_beneath_attr,
parent_fd: F,
allowed_access: BitFlags<AccessFs>,
compat_level: Option<CompatLevel>,
}
impl<F> PathBeneath<F>
where
F: AsFd,
{
pub fn new<A>(parent: F, access: A) -> Self
where
A: Into<BitFlags<AccessFs>>,
{
PathBeneath {
attr: uapi::landlock_path_beneath_attr {
allowed_access: 0,
parent_fd: parent.as_fd().as_raw_fd(),
},
parent_fd: parent,
allowed_access: access.into(),
compat_level: None,
}
}
fn sync_attr(mut self) -> Self {
self.attr.allowed_access = self.allowed_access.bits();
self
}
}
impl<F> TryCompat<AccessFs> for PathBeneath<F>
where
F: AsFd,
{
fn try_compat_children<L>(
mut self,
abi: ABI,
parent_level: L,
compat_state: &mut CompatState,
) -> Result<Option<Self>, CompatError<AccessFs>>
where
L: Into<CompatLevel>,
{
self.allowed_access = match self.allowed_access.try_compat(
abi,
self.tailored_compat_level(parent_level),
compat_state,
)? {
Some(a) => a,
None => return Ok(None),
};
Ok(Some(self))
}
fn try_compat_inner(
mut self,
_abi: ABI,
) -> Result<CompatResult<Self, AccessFs>, CompatError<AccessFs>> {
let valid_access =
if is_file(&self.parent_fd).map_err(|e| PathBeneathError::StatCall { source: e })? {
self.allowed_access & ACCESS_FILE
} else {
self.allowed_access
};
if self.allowed_access != valid_access {
let error = PathBeneathError::DirectoryAccess {
access: self.allowed_access,
incompatible: self.allowed_access ^ valid_access,
}
.into();
self.allowed_access = valid_access;
Ok(CompatResult::Partial(self.sync_attr(), error))
} else {
Ok(CompatResult::Full(self.sync_attr()))
}
}
}
#[test]
fn path_beneath_try_compat() {
use crate::*;
let abi = ABI::V1;
for file in &["/etc/passwd", "/dev/null"] {
let mut compat_state = CompatState::Init;
let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
assert!(matches!(
PathBeneath::new(PathFd::new(file).unwrap(), ro_access)
.try_compat(abi, CompatLevel::HardRequirement, &mut compat_state)
.unwrap_err(),
CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
if access == ro_access && incompatible == AccessFs::ReadDir
));
let mut compat_state = CompatState::Init;
assert!(matches!(
PathBeneath::new(PathFd::new(file).unwrap(), BitFlags::EMPTY)
.try_compat(abi, CompatLevel::BestEffort, &mut compat_state)
.unwrap_err(),
CompatError::Access(AccessError::Empty)
));
}
let full_access = AccessFs::from_all(ABI::V1);
for compat_level in &[
CompatLevel::BestEffort,
CompatLevel::SoftRequirement,
CompatLevel::HardRequirement,
] {
let mut compat_state = CompatState::Init;
let raw_access = PathBeneath::new(PathFd::new("/").unwrap(), full_access)
.try_compat(abi, *compat_level, &mut compat_state)
.unwrap()
.unwrap()
.attr
.allowed_access;
assert_eq!(raw_access, full_access.bits());
assert_eq!(compat_state, CompatState::Full);
}
}
impl<F> OptionCompatLevelMut for PathBeneath<F> {
fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
&mut self.compat_level
}
}
impl<F> OptionCompatLevelMut for &mut PathBeneath<F> {
fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
&mut self.compat_level
}
}
impl<F> Compatible for PathBeneath<F> {}
impl<F> Compatible for &mut PathBeneath<F> {}
#[test]
fn path_beneath_compatibility() {
let mut path = PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::from_all(ABI::V1));
let path_ref = &mut path;
let level = path_ref.as_option_compat_level_mut();
assert_eq!(level, &None);
assert_eq!(
<Option<CompatLevel> as Into<CompatLevel>>::into(*level),
CompatLevel::BestEffort
);
path_ref.set_compatibility(CompatLevel::SoftRequirement);
assert_eq!(
path_ref.as_option_compat_level_mut(),
&Some(CompatLevel::SoftRequirement)
);
path.set_compatibility(CompatLevel::HardRequirement);
}
impl<F> Rule<AccessFs> for PathBeneath<F> where F: AsFd {}
impl<F> PrivateRule<AccessFs> for PathBeneath<F>
where
F: AsFd,
{
fn as_ptr(&self) -> *const libc::c_void {
&self.attr as *const _ as _
}
fn get_type_id(&self) -> uapi::landlock_rule_type {
uapi::landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH
}
fn get_flags(&self) -> u32 {
0
}
fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
if ruleset.requested_handled_fs.contains(self.allowed_access) {
Ok(())
} else {
Err(AddRuleError::UnhandledAccess {
access: self.allowed_access,
incompatible: self.allowed_access & !ruleset.requested_handled_fs,
}
.into())
}
}
}
#[test]
fn path_beneath_check_consistency() {
use crate::*;
let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
let rx_access = AccessFs::Execute | AccessFs::ReadFile;
assert!(matches!(
Ruleset::from(ABI::Unsupported)
.handle_access(ro_access)
.unwrap()
.create()
.unwrap()
.add_rule(PathBeneath::new(PathFd::new("/").unwrap(), rx_access))
.unwrap_err(),
RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access, incompatible }))
if access == rx_access && incompatible == AccessFs::Execute
));
}
#[cfg_attr(test, derive(Debug))]
pub struct PathFd {
fd: OwnedFd,
}
impl PathFd {
pub fn new<T>(path: T) -> Result<Self, PathFdError>
where
T: AsRef<Path>,
{
Ok(PathFd {
fd: OpenOptions::new()
.read(true)
.custom_flags(libc::O_PATH | libc::O_CLOEXEC)
.open(path.as_ref())
.map_err(|e| PathFdError::OpenCall {
source: e,
path: path.as_ref().into(),
})?
.into(),
})
}
}
impl AsFd for PathFd {
fn as_fd(&self) -> BorrowedFd<'_> {
self.fd.as_fd()
}
}
#[test]
fn path_fd() {
use std::fs::File;
use std::io::Read;
PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute);
PathBeneath::new(File::open("/").unwrap(), AccessFs::Execute);
let mut buffer = [0; 1];
File::from(PathFd::new("/etc/passwd").unwrap().fd)
.read(&mut buffer)
.unwrap_err();
}
pub fn path_beneath_rules<I, P, A>(
paths: I,
access: A,
) -> impl Iterator<Item = Result<PathBeneath<PathFd>, RulesetError>>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
A: Into<BitFlags<AccessFs>>,
{
let access = access.into();
paths.into_iter().filter_map(move |p| match PathFd::new(p) {
Ok(f) => {
let valid_access = match is_file(&f) {
Ok(true) => access & ACCESS_FILE,
Err(_) | Ok(false) => access,
};
Some(Ok(PathBeneath::new(f, valid_access)))
}
Err(_) => None,
})
}
#[test]
fn path_beneath_rules_iter() {
let _ = Ruleset::default()
.handle_access(AccessFs::from_all(ABI::V1))
.unwrap()
.create()
.unwrap()
.add_rules(path_beneath_rules(
&["/usr", "/opt", "/does-not-exist", "/root"],
AccessFs::Execute,
))
.unwrap();
}