#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Instruction {
pub(crate) code: u16,
pub(crate) jt: u8,
pub(crate) jf: u8,
pub(crate) k: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Abi {
pub(crate) audit_arch: u32,
pub(crate) fork: Option<u32>,
pub(crate) vfork: Option<u32>,
pub(crate) clone: u32,
pub(crate) clone3: u32,
pub(crate) foreign_syscall_floor: Option<u32>,
}
pub(crate) const X86_64: Abi = Abi {
audit_arch: 0xc000_003e,
fork: Some(57),
vfork: Some(58),
clone: 56,
clone3: 435,
foreign_syscall_floor: Some(0x4000_0000),
};
pub(crate) const AARCH64: Abi = Abi {
audit_arch: 0xc000_00b7,
fork: None,
vfork: None,
clone: 220,
clone3: 435,
foreign_syscall_floor: None,
};
impl Abi {
#[cfg(all(feature = "sandbox-linux", target_os = "linux"))]
pub(crate) fn native() -> Option<&'static Abi> {
if cfg!(target_arch = "x86_64") {
Some(&X86_64)
} else if cfg!(target_arch = "aarch64") {
Some(&AARCH64)
} else {
None
}
}
}
const BPF_LD_W_ABS: u16 = 0x20;
const BPF_JMP_JEQ_K: u16 = 0x15;
const BPF_JMP_JGE_K: u16 = 0x35;
const BPF_JMP_JSET_K: u16 = 0x45;
const BPF_RET_K: u16 = 0x06;
const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
const SECCOMP_RET_ERRNO: u32 = 0x0005_0000;
const EPERM: u32 = 1;
const ENOSYS: u32 = 38;
const CLONE_THREAD: u32 = 0x0001_0000;
const OFFSET_NR: u32 = 0;
const OFFSET_ARCH: u32 = 4;
const OFFSET_ARG0_LOW: u32 = 16;
#[derive(Debug, Clone, Copy)]
enum Target {
Next,
Allow,
Deny,
NoSys,
}
pub(crate) fn deny_process_creation(abi: &Abi) -> Vec<Instruction> {
let mut body: Vec<(u16, u32, Target, Target)> = vec![
(BPF_LD_W_ABS, OFFSET_ARCH, Target::Next, Target::Next),
(BPF_JMP_JEQ_K, abi.audit_arch, Target::Next, Target::Deny),
(BPF_LD_W_ABS, OFFSET_NR, Target::Next, Target::Next),
];
if let Some(floor) = abi.foreign_syscall_floor {
body.push((BPF_JMP_JGE_K, floor, Target::Deny, Target::Next));
}
for number in [abi.fork, abi.vfork].into_iter().flatten() {
body.push((BPF_JMP_JEQ_K, number, Target::Deny, Target::Next));
}
body.push((BPF_JMP_JEQ_K, abi.clone3, Target::NoSys, Target::Next));
body.push((BPF_JMP_JEQ_K, abi.clone, Target::Next, Target::Allow));
body.push((BPF_LD_W_ABS, OFFSET_ARG0_LOW, Target::Next, Target::Next));
body.push((BPF_JMP_JSET_K, CLONE_THREAD, Target::Allow, Target::Deny));
let allow = body.len();
let resolve = |index: usize, target: Target| -> u8 {
let destination = match target {
Target::Next => index + 1,
Target::Allow => allow,
Target::Deny => allow + 1,
Target::NoSys => allow + 2,
};
u8::try_from(destination - (index + 1)).expect("jump offset fits in u8")
};
let mut program: Vec<Instruction> = body
.iter()
.enumerate()
.map(|(index, &(code, k, jt, jf))| Instruction {
code,
jt: resolve(index, jt),
jf: resolve(index, jf),
k,
})
.collect();
for k in [SECCOMP_RET_ALLOW, SECCOMP_RET_ERRNO | EPERM, SECCOMP_RET_ERRNO | ENOSYS] {
program.push(Instruction { code: BPF_RET_K, jt: 0, jf: 0, k });
}
program
}
pub(crate) fn encode(program: &[Instruction]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(program.len() * 8);
for instruction in program {
bytes.extend_from_slice(&instruction.code.to_ne_bytes());
bytes.push(instruction.jt);
bytes.push(instruction.jf);
bytes.extend_from_slice(&instruction.k.to_ne_bytes());
}
bytes
}
#[cfg(test)]
mod tests {
use super::*;
const AUDIT_ARCH_I386: u32 = 0x4000_0003;
const SIGCHLD: u64 = 17;
const CLONE_VM: u64 = 0x100;
const CLONE_VFORK: u64 = 0x4000;
const THREAD_FLAGS: u64 = 0x003d_0f00;
fn run(program: &[Instruction], arch: u32, nr: u32, arg0: u64) -> u32 {
let mut data = [0u8; 64];
data[0..4].copy_from_slice(&nr.to_le_bytes());
data[4..8].copy_from_slice(&arch.to_le_bytes());
data[16..24].copy_from_slice(&arg0.to_le_bytes());
let mut accumulator = 0u32;
let mut pc = 0usize;
loop {
let instruction = program.get(pc).expect("the program ran off its end");
let k = instruction.k;
let jump = |taken: bool| {
pc + 1 + usize::from(if taken { instruction.jt } else { instruction.jf })
};
pc = match instruction.code {
BPF_LD_W_ABS => {
let offset = usize::try_from(k).unwrap();
accumulator = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
pc + 1
}
BPF_JMP_JEQ_K => jump(accumulator == k),
BPF_JMP_JGE_K => jump(accumulator >= k),
BPF_JMP_JSET_K => jump(accumulator & k != 0),
BPF_RET_K => return k,
other => panic!("unexpected opcode {other:#x}"),
};
}
}
const ALLOW: u32 = SECCOMP_RET_ALLOW;
const DENY: u32 = SECCOMP_RET_ERRNO | EPERM;
const NOSYS: u32 = SECCOMP_RET_ERRNO | ENOSYS;
#[test]
fn x86_64_denies_every_process_creation_path() {
let program = deny_process_creation(&X86_64);
let arch = X86_64.audit_arch;
assert_eq!(run(&program, arch, 57, 0), DENY, "fork");
assert_eq!(run(&program, arch, 58, 0), DENY, "vfork");
assert_eq!(run(&program, arch, 56, SIGCHLD), DENY, "clone as fork");
assert_eq!(run(&program, arch, 56, CLONE_VM | CLONE_VFORK | SIGCHLD), DENY, "posix_spawn");
assert_eq!(run(&program, arch, 435, 0), NOSYS, "clone3");
}
#[test]
fn x86_64_keeps_threads_exec_and_ordinary_syscalls() {
let program = deny_process_creation(&X86_64);
let arch = X86_64.audit_arch;
assert_eq!(run(&program, arch, 56, THREAD_FLAGS), ALLOW, "pthread_create");
assert_eq!(run(&program, arch, 59, 0), ALLOW, "execve");
assert_eq!(run(&program, arch, 39, 0), ALLOW, "getpid");
assert_eq!(run(&program, arch, 0, 0), ALLOW, "read");
}
#[test]
fn x86_64_rejects_other_abis() {
let program = deny_process_creation(&X86_64);
assert_eq!(run(&program, AUDIT_ARCH_I386, 2, 0), DENY, "i386 fork via int 0x80");
assert_eq!(run(&program, AUDIT_ARCH_I386, 20, 0), DENY, "i386 getpid");
assert_eq!(run(&program, X86_64.audit_arch, 0x4000_0000 | 57, 0), DENY, "x32 fork");
assert_eq!(run(&program, X86_64.audit_arch, 0x4000_0000 | 39, 0), DENY, "x32 getpid");
}
#[test]
fn aarch64_denies_process_creation_and_keeps_threads() {
let program = deny_process_creation(&AARCH64);
let arch = AARCH64.audit_arch;
assert_eq!(run(&program, arch, 220, SIGCHLD), DENY, "clone as fork");
assert_eq!(run(&program, arch, 220, CLONE_VM | CLONE_VFORK | SIGCHLD), DENY, "posix_spawn");
assert_eq!(run(&program, arch, 435, 0), NOSYS, "clone3");
assert_eq!(run(&program, arch, 220, THREAD_FLAGS), ALLOW, "pthread_create");
assert_eq!(run(&program, arch, 221, 0), ALLOW, "execve");
assert_eq!(run(&program, arch, 172, 0), ALLOW, "getpid");
assert_eq!(run(&program, X86_64.audit_arch, 172, 0), DENY, "foreign arch");
}
#[test]
fn every_jump_lands_inside_the_program() {
for abi in [&X86_64, &AARCH64] {
let program = deny_process_creation(abi);
for (index, instruction) in program.iter().enumerate() {
if instruction.code == BPF_RET_K {
continue;
}
for offset in [instruction.jt, instruction.jf] {
assert!(index + 1 + usize::from(offset) < program.len(), "{abi:?} @ {index}");
}
}
assert_eq!(program.last().map(|i| i.code), Some(BPF_RET_K));
}
}
#[test]
fn encoding_matches_the_sock_filter_layout() {
let program = [Instruction { code: 0x0015, jt: 1, jf: 2, k: 0xc000_003e }];
let mut expected = Vec::new();
expected.extend_from_slice(&0x0015u16.to_ne_bytes());
expected.extend_from_slice(&[1, 2]);
expected.extend_from_slice(&0xc000_003eu32.to_ne_bytes());
assert_eq!(encode(&program), expected);
assert_eq!(encode(&deny_process_creation(&X86_64)).len() % 8, 0);
}
}