use std::fs::File;
use std::io;
use std::os::fd::{AsFd, AsRawFd, OwnedFd, RawFd};
pub(crate) use landlock::{make_bitflags, AccessFs, BitFlags, ABI};
use landlock::{
Access, AccessNet, CompatLevel, Compatible, PathBeneath, Ruleset, RulesetAttr,
RulesetCreatedAttr, Scope,
};
use super::super::enforcement::MechanismLimit;
pub(crate) type FsAccess = BitFlags<AccessFs>;
const VOCABULARY: ABI = ABI::V5;
pub(crate) const NEWEST_DISTINGUISHED: ABI = ABI::V6;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct LandlockSupport {
abi: ABI,
}
impl LandlockSupport {
pub(crate) fn negotiate(ceiling: ABI) -> Option<Self> {
Self::negotiate_with(ceiling, kernel_creates_ruleset_for)
}
pub(crate) fn negotiate_with(ceiling: ABI, accepts: impl Fn(ABI) -> bool) -> Option<Self> {
[ABI::V6, ABI::V5, ABI::V4, ABI::V3, ABI::V2, ABI::V1]
.into_iter()
.filter(|abi| *abi <= ceiling)
.find(|abi| accepts(*abi))
.map(|abi| Self { abi })
}
#[cfg(test)]
pub(crate) fn assume(abi: ABI) -> Self {
Self { abi }
}
pub(crate) fn abi(self) -> ABI {
self.abi
}
pub(crate) fn handled_access(self) -> FsAccess {
AccessFs::from_all(self.abi.min(VOCABULARY))
}
pub(crate) fn bounds_truncation(self) -> bool {
self.abi >= ABI::V3
}
pub(crate) fn scopes_abstract_unix_sockets(self) -> bool {
self.abi >= ABI::V6
}
pub(crate) fn limits(self) -> Vec<MechanismLimit> {
let mut limits = Vec::new();
if self.abi < ABI::V2 {
limits.push(MechanismLimit::CrossDirectoryRenameRefused);
}
if !self.bounds_truncation() {
limits.push(MechanismLimit::TruncationFilteredBySyscall);
}
if self.abi < ABI::V5 {
limits.push(MechanismLimit::DeviceIoctlUnscoped);
}
if !self.scopes_abstract_unix_sockets() {
limits.push(MechanismLimit::UnixSocketGrantUnavailable);
}
limits
}
pub(crate) fn populated_ruleset<'a>(
self,
scope_abstract_unix_sockets: bool,
rules: impl IntoIterator<Item = (&'a File, FsAccess)>,
) -> Result<OwnedFd, String> {
let mut ruleset = Ruleset::default()
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(self.handled_access())
.map_err(|error| error.to_string())?;
if scope_abstract_unix_sockets {
ruleset = ruleset
.scope(Scope::AbstractUnixSocket)
.map_err(|error| error.to_string())?;
}
let mut created = ruleset.create().map_err(|error| error.to_string())?;
for (file, access) in rules {
created = created
.add_rule(PathBeneath::new(file.as_fd(), access))
.map_err(|error| error.to_string())?;
}
Option::<OwnedFd>::from(created)
.ok_or_else(|| "the kernel created no Landlock ruleset".to_string())
}
}
fn kernel_creates_ruleset_for(abi: ABI) -> bool {
let ruleset = Ruleset::default().set_compatibility(CompatLevel::HardRequirement);
let ruleset = match abi {
ABI::V1 => ruleset.handle_access(AccessFs::from_all(ABI::V1)),
ABI::V2 => ruleset.handle_access(AccessFs::Refer),
ABI::V3 => ruleset.handle_access(AccessFs::Truncate),
ABI::V4 => ruleset.handle_access(AccessNet::BindTcp),
ABI::V5 => ruleset.handle_access(AccessFs::IoctlDev),
ABI::V6 => ruleset
.handle_access(AccessFs::Execute)
.and_then(|ruleset| ruleset.scope(Scope::AbstractUnixSocket)),
_ => return false,
};
ruleset
.and_then(Ruleset::create)
.is_ok_and(|created| Option::<OwnedFd>::from(created).is_some())
}
pub(crate) fn filter_path_truncation(
rules: &mut std::collections::BTreeMap<libc::c_long, Vec<seccompiler::SeccompRule>>,
) -> Result<(), crate::value::VmError> {
use seccompiler::{SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompRule};
rules.remove(&libc::SYS_truncate);
rules.remove(&libc::SYS_openat2);
let flag_rules = |flags_index: u8| -> Result<Vec<SeccompRule>, String> {
let flags = |mask: u64, value: u64| {
SeccompCondition::new(
flags_index,
SeccompCmpArgLen::Dword,
SeccompCmpOp::MaskedEq(mask),
value,
)
.and_then(|condition| SeccompRule::new(vec![condition]))
.map_err(|error| error.to_string())
};
let access_mode = libc::O_ACCMODE as u64;
Ok(vec![
flags(libc::O_TRUNC as u64, 0)?,
flags(access_mode, libc::O_WRONLY as u64)?,
flags(access_mode, libc::O_RDWR as u64)?,
])
};
let mut opens = vec![(libc::SYS_openat, 2)];
#[cfg(target_arch = "x86_64")]
opens.push((libc::SYS_open, 1));
for (syscall, flags_index) in opens {
let filtered = flag_rules(flags_index).map_err(|error| {
super::sandbox_rejection(format!(
"failed to build the truncation filter for Landlock below ABI 3: {error}"
))
})?;
rules.insert(syscall, filtered);
}
Ok(())
}
pub(crate) fn enter_ruleset(ruleset: RawFd) -> io::Result<()> {
unsafe {
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 {
return Err(io::Error::last_os_error());
}
if libc::syscall(libc::SYS_landlock_restrict_self, ruleset, 0) < 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub(crate) fn host_support() -> Option<LandlockSupport> {
static SUPPORT: std::sync::OnceLock<Option<LandlockSupport>> = std::sync::OnceLock::new();
*SUPPORT.get_or_init(|| {
let support = LandlockSupport::negotiate(NEWEST_DISTINGUISHED)?;
restriction_holds(support).then_some(support)
})
}
fn restriction_holds(support: LandlockSupport) -> bool {
let Some(witness) = super::nesting::landlock_probe_witness() else {
return false;
};
let Ok(ruleset) = support.populated_ruleset(false, []) else {
return false;
};
let child = unsafe { libc::fork() };
if child == 0 {
let confined = enter_ruleset(ruleset.as_raw_fd()).is_ok();
let opened = if confined {
unsafe { libc::open(witness.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY) }
} else {
0
};
unsafe { libc::_exit(i32::from(!(confined && opened < 0))) };
}
if child < 0 {
return false;
}
let mut status = 0;
loop {
let waited = unsafe { libc::waitpid(child, &raw mut status, 0) };
if waited == child {
return libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0;
}
if waited < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
}
return false;
}
}