use std::collections::BTreeMap;
use std::path::Path;
use std::process::Command;
use landlock::{
ABI, Access, AccessFs, CompatLevel, Compatible, PathBeneath, PathFd, Ruleset, RulesetAttr,
RulesetCreatedAttr, RulesetStatus,
};
use nix::libc;
use seccompiler::{BpfProgram, SeccompAction, SeccompFilter, SeccompRule, TargetArch};
use super::jail::{JailBackend, SpawnSpec};
use super::{Runner, RunnerError};
const LANDLOCK_ABI: ABI = ABI::V3;
const RUNTIME_READ_PATHS: &[&str] = &[
"/lib",
"/lib64",
"/usr/lib",
"/usr/lib64",
"/etc/ld.so.cache",
];
pub struct NamespaceJail;
impl JailBackend for NamespaceJail {
fn name(&self) -> &'static str {
"linux-namespace"
}
fn command(&self, spec: &SpawnSpec<'_>) -> Result<Command, RunnerError> {
let mut command = Command::new(spec.worker);
command
.arg("sandbox-helper")
.arg("--jail")
.arg(spec.jail)
.arg("--worker")
.arg(spec.worker)
.arg("--adapter")
.arg(spec.mode)
.arg("--rlimit-as")
.arg(spec.limits.address_space_bytes.to_string())
.arg("--rlimit-cpu")
.arg(spec.limits.cpu_seconds.to_string())
.arg("--rlimit-fsize")
.arg(spec.limits.file_size_bytes.to_string())
.arg("--rlimit-nproc")
.arg(spec.limits.max_processes.to_string())
.arg("--seccomp")
.arg(if spec.seccomp { "kill" } else { "off" });
Ok(command)
}
fn policy_material(&self) -> String {
format!(
"newuser+newnet landlock-abi-v3 runtime={} seccomp-allow={}",
RUNTIME_READ_PATHS.join(","),
allowed_syscalls()
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(",")
)
}
fn probe(&self, runner: &Runner) -> Result<(), RunnerError> {
super::jail::probe_net(runner, false)?;
match runner.run_unprobed("probe-seccomp", serde_json::json!({}), &[], true) {
Err(e) if e.code == "adapter_crash" => Ok(()),
Ok(_) => Err(RunnerError {
code: "sandbox_probe_failed",
message: "the syscall filter let a socket creation through".to_string(),
}),
Err(e) => Err(RunnerError {
code: "sandbox_probe_failed",
message: format!("the syscall filter probe failed to run: {e}"),
}),
}
}
}
pub(super) fn unshare_namespaces() -> Result<(), String> {
use std::os::unix::fs::MetadataExt;
let this = std::fs::metadata("/proc/self")
.map_err(|e| format!("cannot read process identity: {e}"))?;
let (uid, gid) = (this.uid(), this.gid());
nix::sched::unshare(
nix::sched::CloneFlags::CLONE_NEWUSER | nix::sched::CloneFlags::CLONE_NEWNET,
)
.map_err(|e| format!("unshare(newuser|newnet) failed: {e}"))?;
std::fs::write("/proc/self/setgroups", "deny")
.map_err(|e| format!("cannot deny setgroups: {e}"))?;
std::fs::write("/proc/self/uid_map", format!("{uid} {uid} 1\n"))
.map_err(|e| format!("cannot write the uid map: {e}"))?;
std::fs::write("/proc/self/gid_map", format!("{gid} {gid} 1\n"))
.map_err(|e| format!("cannot write the gid map: {e}"))?;
Ok(())
}
pub(super) fn apply_landlock(jail: &Path, worker: &Path) -> Result<(), String> {
let fail = |stage: &str, e: &dyn std::fmt::Display| format!("landlock {stage}: {e}");
let mut ruleset = Ruleset::default()
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::from_all(LANDLOCK_ABI))
.map_err(|e| fail("handle_access", &e))?
.create()
.map_err(|e| fail("create", &e))?;
ruleset = ruleset
.add_rule(PathBeneath::new(
PathFd::new(jail).map_err(|e| fail("open jail", &e))?,
AccessFs::from_all(LANDLOCK_ABI),
))
.map_err(|e| fail("jail rule", &e))?;
ruleset = ruleset
.add_rule(PathBeneath::new(
PathFd::new(worker).map_err(|e| fail("open worker", &e))?,
AccessFs::Execute | AccessFs::ReadFile,
))
.map_err(|e| fail("worker rule", &e))?;
for path in RUNTIME_READ_PATHS {
if !Path::new(path).exists() {
continue;
}
let access = if Path::new(path).is_dir() {
AccessFs::from_read(LANDLOCK_ABI)
} else {
AccessFs::ReadFile.into()
};
ruleset = ruleset
.add_rule(PathBeneath::new(
PathFd::new(path).map_err(|e| fail("open runtime path", &e))?,
access,
))
.map_err(|e| fail("runtime rule", &e))?;
}
let status = ruleset
.restrict_self()
.map_err(|e| fail("restrict_self", &e))?;
if status.ruleset != RulesetStatus::FullyEnforced {
return Err(format!(
"landlock policy is {:?}, full enforcement is required, the kernel floor is 6.2",
status.ruleset
));
}
Ok(())
}
pub(super) fn apply_seccomp() -> Result<(), String> {
let arch = TargetArch::try_from(std::env::consts::ARCH)
.map_err(|e| format!("seccomp does not support this architecture: {e:?}"))?;
let rules: BTreeMap<i64, Vec<SeccompRule>> = allowed_syscalls()
.iter()
.map(|syscall| (*syscall, Vec::new()))
.collect();
let filter = SeccompFilter::new(
rules,
SeccompAction::KillProcess,
SeccompAction::Allow,
arch,
)
.map_err(|e| format!("seccomp filter build failed: {e}"))?;
let program: BpfProgram = filter
.try_into()
.map_err(|e: seccompiler::BackendError| format!("seccomp compile failed: {e}"))?;
seccompiler::apply_filter(&program).map_err(|e| format!("seccomp install failed: {e}"))
}
fn allowed_syscalls() -> Vec<i64> {
let mut list = vec![
libc::SYS_read,
libc::SYS_write,
libc::SYS_readv,
libc::SYS_writev,
libc::SYS_pread64,
libc::SYS_pwrite64,
libc::SYS_close,
libc::SYS_fstat,
libc::SYS_newfstatat,
libc::SYS_statx,
libc::SYS_lseek,
libc::SYS_mmap,
libc::SYS_mprotect,
libc::SYS_munmap,
libc::SYS_mremap,
libc::SYS_madvise,
libc::SYS_brk,
libc::SYS_futex,
libc::SYS_sched_yield,
libc::SYS_sched_getaffinity,
libc::SYS_clock_gettime,
libc::SYS_clock_getres,
libc::SYS_clock_nanosleep,
libc::SYS_gettimeofday,
libc::SYS_getrandom,
libc::SYS_exit,
libc::SYS_exit_group,
libc::SYS_rt_sigaction,
libc::SYS_rt_sigprocmask,
libc::SYS_rt_sigreturn,
libc::SYS_sigaltstack,
libc::SYS_tgkill,
libc::SYS_tkill,
libc::SYS_getpid,
libc::SYS_gettid,
libc::SYS_getppid,
libc::SYS_getuid,
libc::SYS_geteuid,
libc::SYS_getgid,
libc::SYS_getegid,
libc::SYS_getrlimit,
libc::SYS_prlimit64,
libc::SYS_getrusage,
libc::SYS_sysinfo,
libc::SYS_times,
libc::SYS_uname,
libc::SYS_umask,
libc::SYS_openat,
libc::SYS_faccessat,
libc::SYS_faccessat2,
libc::SYS_getdents64,
libc::SYS_readlinkat,
libc::SYS_getcwd,
libc::SYS_chdir,
libc::SYS_fchdir,
libc::SYS_fcntl,
libc::SYS_dup,
libc::SYS_dup3,
libc::SYS_pipe2,
libc::SYS_ioctl,
libc::SYS_ppoll,
libc::SYS_pselect6,
libc::SYS_epoll_create1,
libc::SYS_epoll_ctl,
libc::SYS_epoll_pwait,
libc::SYS_eventfd2,
libc::SYS_mkdirat,
libc::SYS_unlinkat,
libc::SYS_renameat,
libc::SYS_ftruncate,
libc::SYS_fallocate,
libc::SYS_fsync,
libc::SYS_fdatasync,
libc::SYS_flock,
libc::SYS_utimensat,
libc::SYS_set_tid_address,
libc::SYS_set_robust_list,
libc::SYS_rseq,
libc::SYS_membarrier,
libc::SYS_prctl,
libc::SYS_clone,
libc::SYS_clone3,
libc::SYS_execve,
libc::SYS_wait4,
];
#[cfg(target_arch = "x86_64")]
list.extend_from_slice(&[
libc::SYS_open,
libc::SYS_stat,
libc::SYS_lstat,
libc::SYS_access,
libc::SYS_readlink,
libc::SYS_poll,
libc::SYS_select,
libc::SYS_dup2,
libc::SYS_pipe,
libc::SYS_epoll_wait,
libc::SYS_mkdir,
libc::SYS_unlink,
libc::SYS_rename,
libc::SYS_arch_prctl,
libc::SYS_nanosleep,
libc::SYS_time,
libc::SYS_getdents,
]);
list.sort_unstable();
list
}