use std::ffi::OsString;
use std::io;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use landlock::{
AccessFs, PathBeneath, PathFd, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetStatus, ABI,
};
use lex_extension::schema::Capabilities;
use seccompiler::{BpfProgram, SeccompAction, SeccompFilter, TargetArch};
use super::{Sandbox, SandboxError};
#[derive(Debug, Default, Clone, Copy)]
pub struct LinuxSandbox;
impl Sandbox for LinuxSandbox {
fn apply_to(
&self,
cmd: &mut std::process::Command,
caps: Capabilities,
) -> Result<(), SandboxError> {
if !caps.is_pure() {
return Err(SandboxError::new(format!(
"LinuxSandbox only enforces pure capabilities (fs=false, net=false); got {caps:?}"
)));
}
let program: OsString = cmd.get_program().to_owned();
unsafe {
cmd.pre_exec(move || install_pure_policy(&program));
}
Ok(())
}
fn supports(&self, caps: Capabilities) -> bool {
caps.is_pure()
}
}
fn install_pure_policy(program: &std::ffi::OsStr) -> io::Result<()> {
set_no_new_privs().inspect_err(|e| write_diag("PR_SET_NO_NEW_PRIVS", e))?;
install_landlock_fs_allowlist(program).inspect_err(|e| write_diag("landlock", e))?;
install_seccomp_network_deny().inspect_err(|e| write_diag("seccomp", e))?;
Ok(())
}
fn write_diag(stage: &str, err: &io::Error) {
let msg = format!("lex-extension-host sandbox: {stage} failed: {err}\n");
let bytes = msg.as_bytes();
unsafe {
libc::write(2, bytes.as_ptr() as *const _, bytes.len());
}
}
fn set_no_new_privs() -> io::Result<()> {
let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if ret != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn install_landlock_fs_allowlist(program: &std::ffi::OsStr) -> io::Result<()> {
let abi = ABI::V1;
let read_access = AccessFs::from_read(abi);
let mut allowed: Vec<PathBuf> = [
"/lib",
"/lib64",
"/usr/lib",
"/usr/lib64",
"/usr/bin",
"/bin",
"/etc/ld.so.cache",
"/etc/ld.so.preload",
"/proc/self",
]
.iter()
.map(PathBuf::from)
.collect();
allowed.push(PathBuf::from(program));
let ruleset = Ruleset::default()
.handle_access(read_access)
.map_err(landlock_err)?
.create()
.map_err(landlock_err)?
.add_rules(allowed.iter().filter_map(|p| {
PathFd::new(p)
.ok()
.map(|fd| Ok::<_, landlock::RulesetError>(PathBeneath::new(fd, read_access)))
}))
.map_err(landlock_err)?;
let status = ruleset.restrict_self().map_err(landlock_err)?;
if status.ruleset == RulesetStatus::NotEnforced {
return Err(io::Error::other(format!(
"landlock not enforced (status: {:?}); kernel missing landlock support",
status.ruleset
)));
}
Ok(())
}
fn install_seccomp_network_deny() -> io::Result<()> {
use std::collections::BTreeMap;
let deny = SeccompAction::Errno(libc::EPERM as u32);
let denied_syscalls = [
libc::SYS_socket,
libc::SYS_connect,
libc::SYS_bind,
libc::SYS_listen,
libc::SYS_accept,
libc::SYS_accept4,
libc::SYS_sendto,
libc::SYS_recvfrom,
libc::SYS_sendmsg,
libc::SYS_recvmsg,
libc::SYS_sendmmsg,
libc::SYS_recvmmsg,
];
let mut rules: BTreeMap<i64, Vec<seccompiler::SeccompRule>> = BTreeMap::new();
for syscall in denied_syscalls {
rules.insert(syscall, Vec::new());
}
let arch: TargetArch = std::env::consts::ARCH
.try_into()
.map_err(|e| io::Error::other(format!("seccomp arch: {e}")))?;
let filter = SeccompFilter::new(rules, SeccompAction::Allow, deny, arch)
.map_err(|e| io::Error::other(format!("seccomp filter build: {e}")))?;
let program: BpfProgram = filter
.try_into()
.map_err(|e| io::Error::other(format!("seccomp compile: {e}")))?;
seccompiler::apply_filter(&program)
.map_err(|e| io::Error::other(format!("seccomp apply: {e}")))?;
Ok(())
}
fn landlock_err<E: std::fmt::Display>(e: E) -> io::Error {
io::Error::other(format!("landlock: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn supports_returns_true_only_for_pure_capabilities() {
let s = LinuxSandbox;
assert!(s.supports(Capabilities::default()));
assert!(!s.supports(Capabilities {
fs: true,
net: false,
}));
assert!(!s.supports(Capabilities {
fs: false,
net: true,
}));
assert!(!s.supports(Capabilities {
fs: true,
net: true,
}));
}
#[test]
fn apply_to_rejects_non_pure_capabilities() {
let s = LinuxSandbox;
let mut cmd = std::process::Command::new("/bin/true");
let err = s
.apply_to(
&mut cmd,
Capabilities {
fs: true,
net: false,
},
)
.expect_err("non-pure caps must be rejected before spawn");
assert!(
err.to_string().contains("pure capabilities"),
"unexpected error message: {err}"
);
}
}