//! The seccomp profile for job processes, compiled with the pure-Rust `seccompiler` crate. //! //! The profile is default-allow: [`DENIED_SYSCALLS`] fail with EPERM and everything else runs. //! seccompiler resolves syscall names only through its JSON frontend, and that frontend rejects //! names unknown on the target arch. So each name is probed on its own, and the unknown ones are //! skipped. The filter is built in the helper before libcontainer forks, and installed by //! [`super::exec::FilteredExecutor`] just before the job execs. use seccompiler::{BpfProgram, TargetArch}; use serde_json::json; use super::SandboxError; /// The errno returned for denied syscalls. pub const EPERM: u32 = 1; /// Syscalls denied with EPERM. Everything else is allowed. pub const DENIED_SYSCALLS: &[&str] = &[ "keyctl", "add_key", "request_key", "bpf", "perf_event_open", "ptrace", "mount", "umount2", "pivot_root", "swapon", "swapoff", "reboot", "kexec_load", "kexec_file_load", "init_module", "finit_module", "delete_module", "unshare", "setns", "open_by_handle_at", "userfaultfd", "acct", "settimeofday", "clock_settime", "syslog", "iopl", "ioperm", "quotactl", "name_to_handle_at", "lookup_dcookie", ]; /// The seccompiler arch for the CPU this binary runs on. pub fn native_arch() -> Result { if cfg!(target_arch = "x86_64") { Ok(TargetArch::x86_64) } else if cfg!(target_arch = "aarch64") { Ok(TargetArch::aarch64) } else if cfg!(target_arch = "riscv64") { Ok(TargetArch::riscv64) } else { Err(SandboxError::Setup( "seccomp: unsupported CPU architecture".to_owned(), )) } } /// The denied syscalls that seccompiler knows on `arch`, in the order of [`DENIED_SYSCALLS`]. pub fn resolved_names(arch: TargetArch) -> Vec<&'static str> { DENIED_SYSCALLS .iter() .copied() .filter(|name| compile(&[name], arch).is_ok()) .collect() } /// Builds the BPF program that denies [`DENIED_SYSCALLS`] with EPERM on this CPU. pub fn build_filter() -> Result { let arch = native_arch()?; let names = resolved_names(arch); if names.is_empty() { return Err(SandboxError::Setup( "seccomp: no denied syscall is known on this arch".to_owned(), )); } compile(&names, arch) } /// Compiles one filter that denies `names` with EPERM and allows everything else. fn compile(names: &[&str], arch: TargetArch) -> Result { const FILTER: &str = "forge_job"; let rules: Vec = names .iter() .map(|name| json!({ "syscall": name })) .collect(); let document = json!({ FILTER: { "mismatch_action": "allow", "match_action": { "errno": EPERM }, "filter": rules, } }); let text = document.to_string(); let mut programs = seccompiler::compile_from_json(text.as_bytes(), arch) .map_err(|e| SandboxError::Setup(format!("seccomp: cannot compile filter: {e}")))?; programs .remove(FILTER) .ok_or_else(|| SandboxError::Setup("seccomp: compiled filter is missing".to_owned())) } #[cfg(test)] mod tests { use super::*; #[test] fn native_arch_resolves_on_supported_cpus() { // Only the CPUs this crate builds for are expected to resolve. if cfg!(any( target_arch = "x86_64", target_arch = "aarch64", target_arch = "riscv64" )) { assert!(native_arch().is_ok()); } } #[test] fn the_whole_denied_list_is_known_on_x86_64() { // Every name in the contract resolves on x86_64, so none is silently dropped there. assert_eq!(resolved_names(TargetArch::x86_64), DENIED_SYSCALLS); } #[test] fn unknown_names_are_skipped_and_known_ones_kept() { let arch = native_arch().expect("arch"); let known = resolved_names(arch); for name in DENIED_SYSCALLS { // Names that resolve on this arch are kept, and the rest are dropped. assert_eq!( known.contains(name), compile(&[name], arch).is_ok(), "{name}" ); } assert!(compile(&["no_such_syscall_forge"], arch).is_err()); } #[test] fn filter_builds_and_is_a_bpf_program() { let program = build_filter().expect("filter"); // A BPF program is a sequence of 8-byte instructions. It is never empty for a deny list. assert!(!program.is_empty()); } #[test] fn denied_list_covers_the_contract() { for expected in [ "keyctl", "add_key", "request_key", "bpf", "perf_event_open", "ptrace", "mount", "umount2", "pivot_root", "swapon", "swapoff", "reboot", "kexec_load", "kexec_file_load", "init_module", "finit_module", "delete_module", "unshare", "setns", "open_by_handle_at", "userfaultfd", "acct", "settimeofday", "clock_settime", "syslog", "iopl", "ioperm", "quotactl", "name_to_handle_at", "lookup_dcookie", ] { assert!(DENIED_SYSCALLS.contains(&expected), "{expected} not denied"); } assert_eq!(DENIED_SYSCALLS.len(), 30); } }