#[cfg(test)]
#[macro_use]
extern crate lazy_static;
pub use access::{Access, HandledAccess};
pub use compat::{CompatLevel, Compatible, LandlockStatus, ABI};
pub use enumflags2::{make_bitflags, BitFlags};
pub use errata::Erratum;
pub use errors::{
AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno,
HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError,
RulesetError, ScopeError, SyscallFlagError,
};
pub use flags::{RestrictSelfFlag, SyscallFlag};
pub use fs::{path_beneath_rules, AccessFs, PathBeneath, PathFd};
pub use net::{AccessNet, NetPort};
pub use restrict_self::{RestrictSelf, RestrictSelfAttr, RestrictSelfStatus};
pub use ruleset::{
RestrictionStatus, Rule, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr,
RulesetStatus,
};
pub use scope::Scope;
use access::PrivateHandledAccess;
use compat::{CompatResult, CompatState, Compatibility, TailoredCompatLevel, TryCompat};
use ruleset::PrivateRule;
#[cfg(test)]
use compat::{can_emulate, get_errno_from_landlock_status};
#[cfg(test)]
use errors::TestRulesetError;
#[cfg(test)]
use strum::IntoEnumIterator;
mod access;
mod compat;
mod errata;
mod errors;
mod flags;
mod fs;
mod net;
mod prctl;
mod restrict_self;
mod ruleset;
mod scope;
mod uapi;
mod private {
pub trait Sealed {}
impl Sealed for crate::AccessFs {}
impl Sealed for crate::AccessNet {}
impl Sealed for crate::Scope {}
impl Sealed for crate::RestrictSelfFlag {}
}
#[cfg(test)]
mod tests {
use crate::*;
fn check_support<B, S, F, OkFn, ErrFn>(
partial: ABI,
full: Option<ABI>,
check: F,
assert_ok: OkFn,
assert_err: ErrFn,
) where
B: From<ABI> + Send + 'static,
F: Fn(B) -> Result<S, TestRulesetError> + Send + Copy + 'static,
S: std::fmt::Debug + Send + 'static,
OkFn: Fn(ABI, Result<S, TestRulesetError>),
ErrFn: Fn(Result<S, TestRulesetError>),
{
assert!(partial <= full.unwrap_or(partial));
for abi in ABI::iter() {
let ret = std::thread::spawn(move || check(B::from(abi)))
.join()
.unwrap();
println!("Checking ABI {abi:?}: received {ret:#?}");
if can_emulate(abi, partial, full) {
assert_ok(abi, ret);
} else {
assert_err(ret);
}
}
}
fn check_ruleset_support<F>(
partial: ABI,
full: Option<ABI>,
check: F,
error_if_abi_lt_partial: bool,
) where
F: Fn(Ruleset) -> Result<RestrictionStatus, TestRulesetError> + Send + Copy + 'static,
{
check_support(
partial,
full,
check,
|abi, ret| {
if abi < partial && error_if_abi_lt_partial {
assert!(matches!(ret, Err(TestRulesetError::Ruleset(_))));
} else {
let full_support = if let Some(full_inner) = full {
abi >= full_inner
} else {
false
};
let ruleset_status = if full_support {
RulesetStatus::FullyEnforced
} else if abi >= partial {
RulesetStatus::PartiallyEnforced
} else {
RulesetStatus::NotEnforced
};
let landlock_status = abi.into();
println!("Expecting ruleset status {ruleset_status:?}");
println!("Expecting Landlock status {landlock_status:?}");
assert!(matches!(
ret,
Ok(RestrictionStatus {
ruleset,
landlock,
no_new_privs: true,
..
}) if ruleset == ruleset_status && landlock == landlock_status
))
}
},
|ret| {
let errno = get_errno_from_landlock_status();
println!("Expecting error {errno:?}");
match ret {
Err(
ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset(
CreateRulesetError::CreateRulesetCall { ref source },
)),
) => {
assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
match (source.raw_os_error(), errno) {
(Some(e1), Some(e2)) => assert_eq!(e1, e2),
(Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
_ => unreachable!(),
}
}
Err(TestRulesetError::Ruleset(RulesetError::RestrictSelf(
RestrictSelfError::RestrictSelfCall { ref source },
))) => {
assert_eq!(source.raw_os_error(), Some(libc::EINVAL));
}
_ => unreachable!(),
}
},
);
}
fn check_restrict_self_support<F>(partial: ABI, full: Option<ABI>, check: F)
where
F: Fn(RestrictSelf) -> Result<RestrictSelfStatus, TestRulesetError> + Send + Copy + 'static,
{
check_support(
partial,
full,
check,
|abi, ret| {
let landlock_status: LandlockStatus = abi.into();
println!("Expecting Landlock status {landlock_status:?}");
assert!(matches!(
ret,
Ok(RestrictSelfStatus { landlock, .. }) if landlock == landlock_status
));
},
|ret| {
let errno = get_errno_from_landlock_status();
println!("Expecting error {errno:?}");
match ret {
Err(
ref error @ TestRulesetError::Ruleset(RulesetError::RestrictSelf(
RestrictSelfError::RestrictSelfCall { ref source },
)),
) => {
assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
match (source.raw_os_error(), errno) {
(Some(e1), Some(e2)) => assert_eq!(e1, e2),
(Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
_ => unreachable!(),
}
}
_ => unreachable!(),
}
},
);
}
#[test]
fn allow_root_compat() {
let abi = ABI::V1;
check_ruleset_support(
abi,
Some(abi),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::from_all(abi))?
.create()?
.add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
.restrict_self()?)
},
false,
);
}
#[test]
fn too_much_access_rights_for_a_file() {
let abi = ABI::V1;
check_ruleset_support(
abi,
Some(abi),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::from_all(abi))?
.create()?
.add_rule(PathBeneath::new(
PathFd::new("/etc/passwd")?,
AccessFs::from_file(abi),
))?
.restrict_self()?)
},
false,
);
check_ruleset_support(
abi,
None,
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::from_all(abi))?
.create()?
.add_rule(PathBeneath::new(
PathFd::new("/etc/passwd")?,
AccessFs::from_all(abi),
))?
.restrict_self()?)
},
false,
);
}
#[test]
fn path_beneath_rules_with_too_much_access_rights_for_a_file() {
let abi = ABI::V1;
check_ruleset_support(
abi,
Some(abi),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::from_all(ABI::V1))?
.create()?
.add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))?
.restrict_self()?)
},
false,
);
}
#[test]
fn allow_root_fragile() {
let abi = ABI::V1;
check_ruleset_support(
abi,
Some(abi),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::Execute)?
.set_compatibility(CompatLevel::BestEffort)
.handle_access(AccessFs::from_all(abi))?
.create()?
.no_new_privs(true)
.add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
.restrict_self()?)
},
true,
);
}
#[test]
fn ruleset_enforced() {
let abi = ABI::V1;
check_ruleset_support(
abi,
Some(abi),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::Execute)?
.create()?
.restrict_self()?)
},
false,
);
}
#[test]
fn abi_v2_exec_refer() {
check_ruleset_support(
ABI::V1,
Some(ABI::V2),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::Execute)?
.handle_access(AccessFs::Refer)?
.create()?
.restrict_self()?)
},
false,
);
}
#[test]
fn abi_v2_refer_only() {
check_ruleset_support(
ABI::V2,
Some(ABI::V2),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::Refer)?
.create()?
.restrict_self()?)
},
false,
);
}
#[test]
fn abi_v3_truncate() {
check_ruleset_support(
ABI::V2,
Some(ABI::V3),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::Refer)?
.handle_access(AccessFs::Truncate)?
.create()?
.add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))?
.restrict_self()?)
},
false,
);
}
#[test]
fn ruleset_created_try_clone() {
check_ruleset_support(
ABI::V1,
Some(ABI::V1),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::Execute)?
.create()?
.add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))?
.try_clone()?
.restrict_self()?)
},
false,
);
}
#[test]
fn abi_v4_tcp() {
check_ruleset_support(
ABI::V3,
Some(ABI::V4),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::Truncate)?
.handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?
.create()?
.add_rule(NetPort::new(1, AccessNet::ConnectTcp))?
.restrict_self()?)
},
false,
);
}
#[test]
fn abi_v5_ioctl_dev() {
check_ruleset_support(
ABI::V4,
Some(ABI::V5),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessNet::BindTcp)?
.handle_access(AccessFs::IoctlDev)?
.create()?
.add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))?
.restrict_self()?)
},
false,
);
}
#[test]
fn abi_v6_scope_mix() {
check_ruleset_support(
ABI::V5,
Some(ABI::V6),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::IoctlDev)?
.scope(Scope::AbstractUnixSocket | Scope::Signal)?
.create()?
.restrict_self()?)
},
false,
);
}
#[test]
fn abi_v6_scope_only() {
check_ruleset_support(
ABI::V6,
Some(ABI::V6),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.scope(Scope::AbstractUnixSocket | Scope::Signal)?
.create()?
.restrict_self()?)
},
false,
);
}
#[test]
fn abi_v7_log_flags() {
check_ruleset_support(
ABI::V6,
Some(ABI::V7),
move |ruleset: Ruleset| -> _ {
let status = ruleset
.scope(Scope::Signal)?
.create()?
.log_same_exec(false)?
.log_new_exec(true)?
.log_subdomains(false)?
.restrict_self()?;
if status.ruleset == RulesetStatus::FullyEnforced {
assert!(!status.log_same_exec);
assert!(status.log_new_exec);
assert!(!status.log_subdomains);
} else {
assert!(status.log_same_exec);
assert!(!status.log_new_exec);
assert!(status.log_subdomains);
}
Ok(status)
},
false,
);
}
#[test]
fn ruleset_created_try_clone_ownedfd() {
use std::os::unix::io::{AsRawFd, OwnedFd};
let abi = ABI::V1;
check_ruleset_support(
abi,
Some(abi),
move |ruleset: Ruleset| -> _ {
let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?;
let ruleset2 = ruleset1.try_clone().unwrap();
let ruleset3 = ruleset2.try_clone().unwrap();
let some1: Option<OwnedFd> = ruleset1.into();
if let Some(fd1) = some1 {
assert!(fd1.as_raw_fd() >= 0);
let some2: Option<OwnedFd> = ruleset2.into();
let fd2 = some2.unwrap();
assert!(fd2.as_raw_fd() >= 0);
assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd());
}
Ok(ruleset3.restrict_self()?)
},
false,
);
}
#[test]
fn restrict_self_log_subdomains() {
check_restrict_self_support(ABI::V7, Some(ABI::V7), move |rs: RestrictSelf| -> _ {
Ok(rs.log_subdomains(false)?.apply()?)
});
}
#[test]
fn abi_v8_all_threads() {
use crate::compat::TEST_ABI;
use std::sync::{Arc, Barrier};
use std::thread;
const OK: i32 = 0;
const MAIN_BEFORE_DENIED: i32 = 1 << 0;
const SIBLING_BEFORE_DENIED: i32 = 1 << 1;
const MAIN_AFTER_MISMATCH: i32 = 1 << 2;
const SIBLING_AFTER_MISMATCH: i32 = 1 << 3;
const STATUS_RULESET_MISMATCH: i32 = 1 << 4;
const STATUS_TSYNC_MISMATCH: i32 = 1 << 5;
const PANICKED: i32 = 1 << 6;
fn can_list_root() -> bool {
std::fs::read_dir("/").is_ok()
}
let landlock_supported = *TEST_ABI != ABI::Unsupported;
let tsync_supported = *TEST_ABI >= ABI::V8;
for all_threads in [false, true] {
let expect_sibling_denied = all_threads && tsync_supported;
match unsafe { libc::fork() } {
-1 => panic!("fork() failed: {}", std::io::Error::last_os_error()),
0 => {
let code = std::panic::catch_unwind(|| {
let barrier = Arc::new(Barrier::new(2));
let sibling = {
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
let before = can_list_root();
barrier.wait();
barrier.wait();
(before, can_list_root())
})
};
let main_before = can_list_root();
barrier.wait();
let status = Ruleset::default()
.handle_access(AccessFs::from_all(ABI::V1))
.unwrap()
.create()
.unwrap()
.all_threads(all_threads)
.unwrap()
.restrict_self()
.unwrap();
barrier.wait();
let main_denied = !can_list_root();
let (sibling_before, sibling_after) = sibling.join().unwrap();
let sibling_denied = !sibling_after;
let ruleset_enforced = status.ruleset != RulesetStatus::NotEnforced;
let mut code = OK;
if !main_before {
code |= MAIN_BEFORE_DENIED;
}
if !sibling_before {
code |= SIBLING_BEFORE_DENIED;
}
if main_denied != landlock_supported {
code |= MAIN_AFTER_MISMATCH;
}
if sibling_denied != expect_sibling_denied {
code |= SIBLING_AFTER_MISMATCH;
}
if ruleset_enforced != landlock_supported {
code |= STATUS_RULESET_MISMATCH;
}
if status.all_threads != expect_sibling_denied {
code |= STATUS_TSYNC_MISMATCH;
}
code
})
.unwrap_or(PANICKED);
unsafe { libc::_exit(code) };
}
pid => {
let mut wstatus: libc::c_int = 0;
let ret = unsafe { libc::waitpid(pid, &mut wstatus, 0) };
assert_eq!(ret, pid);
assert!(libc::WIFEXITED(wstatus));
assert_eq!(libc::WEXITSTATUS(wstatus), OK, "all_threads({all_threads})");
}
}
}
}
#[test]
fn abi_v9_resolve_unix() {
check_ruleset_support(
ABI::V9,
Some(ABI::V9),
move |ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::ResolveUnix)?
.create()?
.add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::ResolveUnix))?
.restrict_self()?)
},
false,
);
}
}