blob: d820ee1047d063503972265c804aa67e4724a5c5 [file]
//! The seccomp filter everything inside runs under.
//!
//! A network namespace leaves two ways out. An `AF_VSOCK` socket addresses the
//! hypervisor's host under any VM guest, WSL2 included, whatever namespace it is made in;
//! io_uring can make one without the `socket` call a filter sees. The filter refuses
//! both with `EPERM`, the answer Codex's filter gives
//! (`codex-rs/linux-sandbox/src/seccomp.rs`), and allows every other call.
//!
//! [`program`] compiles the filter before the fork; [`install`] puts it in place between
//! fork and exec, and allocates nothing.
use crate::Error;
use seccompiler::{
BpfProgram, SeccompAction, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompFilter,
SeccompRule, TargetArch,
};
use std::collections::BTreeMap;
/// What a refused call returns.
const REFUSED: libc::c_int = libc::EPERM;
/// `__X32_SYSCALL_BIT`: on x86_64 a call numbered with it is the x32 ABI's, under the
/// same audit architecture as the native call.
const X32_SYSCALL_BIT: i64 = 0x4000_0000;
/// Refused whatever their arguments.
const IO_URING: [i64; 3] = [
libc::SYS_io_uring_setup,
libc::SYS_io_uring_enter,
libc::SYS_io_uring_register,
];
/// Refused when the first argument, the address family, is `AF_VSOCK`.
const SOCKET: [i64; 2] = [libc::SYS_socket, libc::SYS_socketpair];
/// Each number the kernel takes for call `nr` under this architecture's audit value. The
/// filter's architecture check passes x32's numbers on x86_64, so they are refused too.
fn numbers(nr: i64) -> Vec<i64> {
if cfg!(target_arch = "x86_64") {
vec![nr, nr | X32_SYSCALL_BIT]
} else {
vec![nr]
}
}
fn compile(e: impl std::fmt::Display) -> Error {
Error::Confine(format!("seccomp: {e}"))
}
/// The filter, compiled for this architecture. Its first instructions check the audit
/// architecture and kill a process that calls under another one: an i386 call on
/// x86_64, or an aarch32 call on aarch64, has numbers of its own the rules do not name.
///
/// # Errors
///
/// When this architecture has no filter, or the filter does not compile.
pub(super) fn program() -> Result<BpfProgram, Error> {
let arch = TargetArch::try_from(std::env::consts::ARCH).map_err(compile)?;
let vsock = SeccompRule::new(vec![SeccompCondition::new(
0,
SeccompCmpArgLen::Dword,
SeccompCmpOp::Eq,
libc::AF_VSOCK as u64,
)
.map_err(compile)?])
.map_err(compile)?;
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
for nr in IO_URING.into_iter().flat_map(numbers) {
// no rule: the call is refused whatever its arguments
rules.insert(nr, Vec::new());
}
for nr in SOCKET.into_iter().flat_map(numbers) {
rules.insert(nr, vec![vsock.clone()]);
}
let filter = SeccompFilter::new(
rules,
SeccompAction::Allow,
SeccompAction::Errno(REFUSED as u32),
arch,
)
.map_err(compile)?;
BpfProgram::try_from(filter).map_err(compile)
}
/// Set `no_new_privs` and install `program` on the calling thread. Both hold across
/// every exec and fork after it. Allocates nothing, so it may run between fork and exec.
///
/// # Errors
///
/// The errno of the `prctl` or `seccomp` call that failed.
pub(super) fn install(program: &BpfProgram) -> std::io::Result<()> {
// an unprivileged process may install a filter only under no_new_privs
// SAFETY: prctl with integer arguments only.
if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
return Err(std::io::Error::last_os_error());
}
seccompiler::apply_filter(program).map_err(|e| match e {
seccompiler::Error::Prctl(io) | seccompiler::Error::Seccomp(io) => io,
_ => std::io::Error::from_raw_os_error(libc::EINVAL),
})
}