use std::ffi::{CString, OsString};
use std::io;
use std::mem;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::ptr;
use anyhow::{anyhow, bail, Context, Result};
use landlock::{
path_beneath_rules, AccessFs, CompatLevel, Compatible, Ruleset, RulesetAttr,
RulesetCreatedAttr, RulesetStatus, ABI,
};
use super::carve::Carve;
use super::{Availability, Backend, Plan};
const LANDLOCK_ABI: ABI = ABI::V3;
pub fn enforce_and_exec(plan: &Plan, command: &[OsString]) -> anyhow::Error {
let applied = match plan.backend {
Backend::Mount => apply_mount(&plan.pinned, &plan.protected),
Backend::Landlock => match &plan.carve {
Some(carve) => apply_landlock(carve),
None => Err(anyhow!("internal error: landlock plan was not built")),
},
Backend::Auto => Err(anyhow!("internal error: backend was not resolved")),
};
if let Err(error) = applied {
return error;
}
exec(command)
}
fn exec(command: &[OsString]) -> anyhow::Error {
let Some((program, arguments)) = command.split_first() else {
return anyhow!("no command given");
};
let error = Command::new(program).args(arguments).exec();
anyhow::Error::new(error).context(format!("failed to run `{}`", program.to_string_lossy()))
}
pub fn mount_availability() -> Availability {
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };
match probe(|| {
enter_namespaces(uid, gid)?;
enter_namespaces(uid, gid)
}) {
Ok(()) => Availability::Available {
detail: "read-only bind mounts in a locked namespace".to_string(),
},
Err(error) => Availability::Unavailable {
reason: format!("unprivileged user namespaces are not usable ({error})"),
},
}
}
fn apply_mount(pinned: &[PathBuf], protected: &[PathBuf]) -> Result<()> {
if protected.is_empty() {
return Ok(());
}
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };
enter_namespaces(uid, gid).context("failed to create a user and mount namespace")?;
check(unsafe {
libc::mount(
ptr::null(),
c"/".as_ptr(),
ptr::null(),
libc::MS_REC | libc::MS_PRIVATE,
ptr::null(),
)
})
.context("failed to make the mount tree private")?;
for path in pinned {
bind(path).with_context(|| format!("failed to pin {}", path.display()))?;
}
for path in protected {
bind_read_only(path)?;
}
if let Ok(directory) = std::env::current_dir() {
std::env::set_current_dir(&directory).with_context(|| {
format!("failed to re-enter {} after mounting", directory.display())
})?;
}
enter_namespaces(uid, gid).context("failed to lock the mount namespace")?;
Ok(())
}
fn enter_namespaces(uid: libc::uid_t, gid: libc::gid_t) -> io::Result<()> {
check(unsafe { libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) })?;
let _ = std::fs::write("/proc/self/setgroups", "deny");
std::fs::write("/proc/self/uid_map", format!("{uid} {uid} 1"))?;
std::fs::write("/proc/self/gid_map", format!("{gid} {gid} 1"))?;
Ok(())
}
fn bind(path: &Path) -> Result<()> {
let target = cstring(path)?;
check(unsafe {
libc::mount(
target.as_ptr(),
target.as_ptr(),
ptr::null(),
libc::MS_BIND | libc::MS_REC,
ptr::null(),
)
})
.with_context(|| format!("failed to bind mount {}", path.display()))
}
fn bind_read_only(path: &Path) -> Result<()> {
bind(path)?;
let target = cstring(path)?;
if mount_setattr_read_only(&target).is_ok() {
return Ok(());
}
let flags = libc::MS_REMOUNT | libc::MS_BIND | libc::MS_RDONLY | locked_flags(&target)?;
check(unsafe {
libc::mount(
ptr::null(),
target.as_ptr(),
ptr::null(),
flags,
ptr::null(),
)
})
.with_context(|| format!("failed to remount {} read-only", path.display()))
}
#[repr(C)]
#[derive(Default)]
struct MountAttr {
attr_set: u64,
attr_clr: u64,
propagation: u64,
userns_fd: u64,
}
fn mount_setattr_read_only(target: &CString) -> io::Result<()> {
const MOUNT_ATTR_RDONLY: u64 = 0x1;
const AT_RECURSIVE: libc::c_int = 0x8000;
let attributes = MountAttr {
attr_set: MOUNT_ATTR_RDONLY,
..MountAttr::default()
};
let result = unsafe {
libc::syscall(
libc::SYS_mount_setattr,
libc::AT_FDCWD,
target.as_ptr(),
AT_RECURSIVE,
&attributes as *const MountAttr,
mem::size_of::<MountAttr>(),
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
fn locked_flags(target: &CString) -> Result<libc::c_ulong> {
let mut stats: libc::statvfs = unsafe { mem::zeroed() };
check(unsafe { libc::statvfs(target.as_ptr(), &mut stats) })
.context("failed to read mount flags")?;
let pairs = [
(libc::ST_NOSUID, libc::MS_NOSUID),
(libc::ST_NODEV, libc::MS_NODEV),
(libc::ST_NOEXEC, libc::MS_NOEXEC),
(libc::ST_NOATIME, libc::MS_NOATIME),
(libc::ST_NODIRATIME, libc::MS_NODIRATIME),
(libc::ST_RELATIME, libc::MS_RELATIME),
];
Ok(pairs.iter().fold(0, |flags, (present, wanted)| {
if stats.f_flag & present != 0 {
flags | wanted
} else {
flags
}
}))
}
pub fn landlock_availability() -> Availability {
match landlock_abi() {
Some(abi) if abi >= 2 => Availability::Available {
detail: format!("kernel ABI v{abi}"),
},
Some(abi) => Availability::Available {
detail: format!("kernel ABI v{abi}, no cross-directory renames"),
},
None => Availability::Unavailable {
reason: "the kernel reports no Landlock support (needs Linux 5.13+ with landlock \
enabled, e.g. lsm=landlock,... on the kernel command line)"
.to_string(),
},
}
}
fn landlock_abi() -> Option<i64> {
const LANDLOCK_CREATE_RULESET_VERSION: libc::c_uint = 1;
let version = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
ptr::null::<libc::c_void>(),
0usize,
LANDLOCK_CREATE_RULESET_VERSION,
)
};
(version > 0).then_some(version)
}
fn apply_landlock(carve: &Carve) -> Result<()> {
if carve.restricted.is_empty() {
return Ok(());
}
let access = AccessFs::from_write(LANDLOCK_ABI);
let status = Ruleset::default()
.set_compatibility(CompatLevel::BestEffort)
.handle_access(access)?
.create()?
.no_new_privs(true)
.add_rules(path_beneath_rules(&carve.granted, access))?
.restrict_self()?;
match status.ruleset {
RulesetStatus::FullyEnforced => Ok(()),
RulesetStatus::PartiallyEnforced => {
eprintln!(
"ralon: warning: this kernel supports only part of the policy; \
run `ralon status` for details"
);
Ok(())
}
RulesetStatus::NotEnforced => {
bail!("the kernel accepted no part of the policy — nothing is protected")
}
}
}
fn cstring(path: &Path) -> Result<CString> {
CString::new(path.as_os_str().as_bytes())
.with_context(|| format!("path contains a null byte: {}", path.display()))
}
fn check(result: libc::c_int) -> io::Result<()> {
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
fn probe(action: impl Fn() -> io::Result<()>) -> io::Result<()> {
match unsafe { libc::fork() } {
-1 => Err(io::Error::last_os_error()),
0 => {
let errno = match action() {
Ok(()) => 0,
Err(error) => error.raw_os_error().unwrap_or(1),
};
unsafe { libc::_exit(errno.clamp(0, 126)) }
}
child => {
let mut status = 0;
if unsafe { libc::waitpid(child, &mut status, 0) } == -1 {
return Err(io::Error::last_os_error());
}
match libc::WEXITSTATUS(status) {
0 => Ok(()),
errno => Err(io::Error::from_raw_os_error(errno)),
}
}
}
}