use crate::engine::error::{Cause, Error, Mechanism};
use crate::engine::policy::{AccessPolicy, ReadAccess};
use anyhow::Result;
use landlock::{
ABI, Access, AccessFs, AccessNet, BitFlags, LandlockStatus, NetPort, PathBeneath, Ruleset,
RulesetAttr, RulesetCreated, RulesetCreatedAttr, RulesetStatus,
};
use std::ffi::CString;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
fn setup_failed(source: impl Into<Cause>) -> Error {
Error::SandboxSetupFailed {
mechanism: Mechanism::Landlock,
source: source.into(),
}
}
const TARGET_ABI: ABI = ABI::V7;
pub(super) fn enforce_access_policy(policy: &AccessPolicy) -> Result<()> {
enforce_access_policy_with(policy, true)
}
pub(super) fn enforce_broker_access_policy(policy: &AccessPolicy) -> Result<()> {
enforce_access_policy_with(policy, false)
}
fn enforce_access_policy_with(policy: &AccessPolicy, restrict_read: bool) -> Result<()> {
let handled_access_fs = match &policy.read_access {
ReadAccess::AllowRoots(_) if restrict_read => {
AccessFs::from_write(TARGET_ABI) | read_access_fs()
}
ReadAccess::AllowRoots(_) | ReadAccess::Unrestricted => AccessFs::from_write(TARGET_ABI),
};
let mut handled_access_net = BitFlags::<AccessNet>::empty();
if policy.network_access.restrict_connect_tcp {
handled_access_net |= AccessNet::ConnectTcp;
}
if policy.network_access.restrict_bind_tcp {
handled_access_net |= AccessNet::BindTcp;
}
let mut ruleset = Ruleset::default()
.handle_access(handled_access_fs)
.map_err(setup_failed)?;
if !handled_access_net.is_empty() {
ruleset = ruleset
.handle_access(handled_access_net)
.map_err(setup_failed)?;
}
let mut created = ruleset.create().map_err(setup_failed)?;
created = add_path_rules(
created,
&policy.write_roots,
AccessFs::from_write(TARGET_ABI),
"write",
)?;
if restrict_read {
if let ReadAccess::AllowRoots(read_roots) = &policy.read_access {
created = add_path_rules(created, read_roots, read_access_fs(), "read")?;
}
}
if !handled_access_net.is_empty() {
created = add_network_rules(created, policy)?;
}
let status = created.restrict_self().map_err(setup_failed)?;
match status.ruleset {
RulesetStatus::FullyEnforced => {}
RulesetStatus::PartiallyEnforced => log::warn!(
"{}",
partial_enforcement_warning(status.landlock, handled_access_fs, handled_access_net,)
),
RulesetStatus::NotEnforced => {
return Err(setup_failed(
"not enforced by the kernel (Linux 5.13+ with CONFIG_SECURITY_LANDLOCK required, \
and not disabled via the lsm= boot parameter)",
)
.into());
}
}
Ok(())
}
fn partial_enforcement_warning(
status: LandlockStatus,
handled_access_fs: BitFlags<AccessFs>,
handled_access_net: BitFlags<AccessNet>,
) -> String {
let LandlockStatus::Available { effective_abi, .. } = status else {
return "landlock: kernel only partially enforced the policy".to_owned();
};
let supported_fs = AccessFs::from_all(effective_abi);
let supported_net = AccessNet::from_all(effective_abi);
let mut missing = Vec::new();
for (access, name) in [
(AccessFs::Refer, "filesystem REFER"),
(AccessFs::Truncate, "filesystem TRUNCATE"),
(AccessFs::IoctlDev, "filesystem IOCTL_DEV"),
] {
if handled_access_fs.contains(access) && !supported_fs.contains(access) {
missing.push(name);
}
}
for (access, name) in [
(AccessNet::BindTcp, "network BIND_TCP"),
(AccessNet::ConnectTcp, "network CONNECT_TCP"),
] {
if handled_access_net.contains(access) && !supported_net.contains(access) {
missing.push(name);
}
}
let details = if missing.is_empty() {
"some requested features".to_owned()
} else {
missing.join(", ")
};
format!(
"landlock: kernel ABI {effective_abi} only partially enforced the policy; unsupported \
access rights: {details}; supported rights remain enforced; Landlock ABI 5 or newer \
(upstream Linux 6.10+) is required for complete enforcement"
)
}
fn read_access_fs() -> BitFlags<AccessFs> {
AccessFs::from_read(TARGET_ABI) & !AccessFs::Execute
}
fn add_path_rules(
mut ruleset: RulesetCreated,
paths: &[PathBuf],
access: BitFlags<AccessFs>,
label: &str,
) -> Result<RulesetCreated> {
let mut seen_ancestors: Vec<PathBuf> = Vec::new();
for path in paths {
let fd = match open_path(path) {
Ok(fd) => fd,
Err(error)
if error.kind() == io::ErrorKind::NotFound
|| error.kind() == io::ErrorKind::PermissionDenied
|| error.raw_os_error() == Some(libc::EIO)
|| error.raw_os_error() == Some(libc::ENOTCONN) =>
{
let mut ancestor = path.clone();
loop {
match ancestor.parent() {
Some(parent) if !parent.as_os_str().is_empty() => {
match std::fs::symlink_metadata(parent) {
Ok(metadata) if metadata.is_dir() => {
ancestor = parent.to_path_buf();
break;
}
_ => ancestor = parent.to_path_buf(),
}
}
_ => break,
}
}
if seen_ancestors.iter().any(|seen| seen == &ancestor) {
continue;
}
let fd = match open_path(&ancestor) {
Ok(fd) => fd,
Err(error) => {
log::debug!(
"landlock: {label} ancestor {} unreachable: {error}",
ancestor.display()
);
continue;
}
};
seen_ancestors.push(ancestor);
fd
}
Err(error) => return Err(setup_failed(error).into()),
};
let path_access = if fd_is_dir(&fd)? {
access
} else {
access & AccessFs::from_file(TARGET_ABI)
};
if path_access.is_empty() {
continue;
}
ruleset = ruleset
.add_rule(PathBeneath::new(fd, path_access))
.map_err(setup_failed)?;
}
Ok(ruleset)
}
fn add_network_rules(mut ruleset: RulesetCreated, policy: &AccessPolicy) -> Result<RulesetCreated> {
if !policy.network_access.restrict_connect_tcp {
return Ok(ruleset);
}
for port in &policy.network_access.connect_tcp_ports {
ruleset = ruleset
.add_rule(NetPort::new(*port, AccessNet::ConnectTcp))
.map_err(setup_failed)?;
}
Ok(ruleset)
}
fn open_path(path: &Path) -> io::Result<OwnedFd> {
let path = CString::new(path.as_os_str().as_bytes())?;
let fd = unsafe { libc::open(path.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
fn fd_is_dir(fd: &OwnedFd) -> Result<bool> {
let mut stat = unsafe { std::mem::zeroed::<libc::stat>() };
let rc = unsafe { libc::fstat(fd.as_raw_fd(), &mut stat) };
if rc != 0 {
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(libc::EIO) || error.raw_os_error() == Some(libc::ENOTCONN) {
return Ok(false);
}
return Err(setup_failed(error).into());
}
Ok((stat.st_mode & libc::S_IFMT) == libc::S_IFDIR)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partial_enforcement_warning_identifies_missing_rights() {
let status = LandlockStatus::Available {
effective_abi: ABI::V4,
kernel_abi: None,
};
let warning = partial_enforcement_warning(
status,
AccessFs::from_write(TARGET_ABI),
BitFlags::empty(),
);
assert_eq!(
warning,
"landlock: kernel ABI 4 only partially enforced the policy; unsupported access rights: \
filesystem IOCTL_DEV; supported rights remain enforced; Landlock ABI 5 or newer \
(upstream Linux 6.10+) is required for complete enforcement"
);
}
}