#![allow(non_snake_case)]
pub use libc::sock_filter;
use syscalls::Errno;
use syscalls::Sysno;
use crate::fd::Fd;
pub const BPF_LD: u16 = 0x00;
pub const BPF_ST: u16 = 0x02;
pub const BPF_JMP: u16 = 0x05;
pub const BPF_RET: u16 = 0x06;
pub const BPF_W: u16 = 0x00;
pub const BPF_ABS: u16 = 0x20;
pub const BPF_MEM: u16 = 0x60;
pub const BPF_JEQ: u16 = 0x10;
pub const BPF_JGT: u16 = 0x20;
pub const BPF_JGE: u16 = 0x30;
pub const BPF_K: u16 = 0x00;
pub const BPF_MAXINSNS: usize = 4096;
const SECCOMP_SET_MODE_FILTER: u32 = 1;
const SECCOMP_DATA_OFFSET_NR: u32 = 0;
const SECCOMP_DATA_OFFSET_ARCH: u32 = 4;
const SECCOMP_DATA_OFFSET_IP: u32 = 8;
#[allow(unused)]
const SECCOMP_DATA_OFFSET_ARGS: u32 = 16;
#[cfg(target_endian = "little")]
const SECCOMP_DATA_OFFSET_IP_HI: u32 = SECCOMP_DATA_OFFSET_IP + 4;
#[cfg(target_endian = "little")]
const SECCOMP_DATA_OFFSET_IP_LO: u32 = SECCOMP_DATA_OFFSET_IP;
#[cfg(target_endian = "big")]
const SECCOMP_DATA_OFFSET_IP_HI: u32 = SECCOMP_DATA_OFFSET_IP;
#[cfg(target_endian = "big")]
const SECCOMP_DATA_OFFSET_IP_LO: u32 = SECCOMP_DATA_OFFSET_IP + 4;
const EM_386: u32 = 3;
const EM_MIPS: u32 = 8;
const EM_PPC: u32 = 20;
const EM_PPC64: u32 = 21;
const EM_ARM: u32 = 40;
const EM_X86_64: u32 = 62;
const EM_AARCH64: u32 = 183;
const __AUDIT_ARCH_64BIT: u32 = 0x8000_0000;
const __AUDIT_ARCH_LE: u32 = 0x4000_0000;
pub const AUDIT_ARCH_X86: u32 = EM_386 | __AUDIT_ARCH_LE;
pub const AUDIT_ARCH_X86_64: u32 = EM_X86_64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE;
pub const AUDIT_ARCH_ARM: u32 = EM_ARM | __AUDIT_ARCH_LE;
pub const AUDIT_ARCH_AARCH64: u32 = EM_AARCH64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE;
pub const AUDIT_ARCH_MIPS: u32 = EM_MIPS;
pub const AUDIT_ARCH_PPC: u32 = EM_PPC;
pub const AUDIT_ARCH_PPC64: u32 = EM_PPC64 | __AUDIT_ARCH_64BIT;
bitflags::bitflags! {
#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
struct FilterFlags: u32 {
const TSYNC = 1 << 0;
const LOG = 1 << 1;
const SPEC_ALLOW = 1 << 2;
const NEW_LISTENER = 1 << 3;
const TSYNC_ESRCH = 1 << 4;
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Filter {
filter: Vec<sock_filter>,
}
impl Default for Filter {
fn default() -> Self {
Self::new()
}
}
impl Filter {
pub const fn new() -> Self {
Self { filter: Vec::new() }
}
pub fn push(&mut self, instruction: sock_filter) {
self.filter.push(instruction);
}
pub fn len(&self) -> usize {
self.filter.len()
}
pub fn is_empty(&self) -> bool {
self.filter.is_empty()
}
pub fn instructions(&self) -> &[sock_filter] {
&self.filter
}
fn install(&self, flags: FilterFlags) -> Result<i32, Errno> {
let len = self.filter.len();
if len == 0 || len > BPF_MAXINSNS {
return Err(Errno::EINVAL);
}
let prog = libc::sock_fprog {
len: len as u16,
filter: self.filter.as_ptr() as *mut _,
};
let ptr = &prog as *const libc::sock_fprog;
let value = Errno::result(unsafe {
libc::syscall(
libc::SYS_seccomp,
SECCOMP_SET_MODE_FILTER,
flags.bits(),
ptr,
)
})?;
Ok(value as i32)
}
pub fn load(&self) -> Result<(), Errno> {
self.install(FilterFlags::empty())?;
Ok(())
}
pub fn load_and_listen(&self) -> Result<Fd, Errno> {
let fd = self.install(FilterFlags::NEW_LISTENER)?;
Ok(Fd::new(fd))
}
}
impl Extend<sock_filter> for Filter {
fn extend<T: IntoIterator<Item = sock_filter>>(&mut self, iter: T) {
self.filter.extend(iter)
}
}
pub trait ByteCode {
fn into_bpf(self, filter: &mut Filter);
}
impl<F> ByteCode for F
where
F: FnOnce(&mut Filter),
{
fn into_bpf(self, filter: &mut Filter) {
self(filter)
}
}
impl ByteCode for sock_filter {
fn into_bpf(self, filter: &mut Filter) {
filter.push(self)
}
}
#[cfg(test)]
macro_rules! seccomp_bpf {
($($inst:expr),+ $(,)?) => {
{
let mut filter = Filter::new();
$(
$inst.into_bpf(&mut filter);
)+
filter
}
};
}
pub const fn BPF_STMT(code: u16, k: u32) -> sock_filter {
sock_filter {
code,
jt: 0,
jf: 0,
k,
}
}
pub const fn BPF_JUMP(code: u16, k: u32, jt: u8, jf: u8) -> sock_filter {
sock_filter { code, jt, jf, k }
}
pub const LOAD_SYSCALL_NR: sock_filter = BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_NR);
#[allow(unused)]
pub const ALLOW: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_ALLOW);
#[allow(unused)]
pub const DENY: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_THREAD);
#[allow(unused)]
pub const TRAP: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_TRAP);
#[allow(unused)]
pub fn TRACE(data: u16) -> sock_filter {
BPF_STMT(
BPF_RET + BPF_K,
libc::SECCOMP_RET_TRACE | (data as u32 & libc::SECCOMP_RET_DATA),
)
}
#[allow(unused)]
pub fn ERRNO(err: Errno) -> sock_filter {
BPF_STMT(
BPF_RET + BPF_K,
libc::SECCOMP_RET_ERRNO | (err.into_raw() as u32 & libc::SECCOMP_RET_DATA),
)
}
macro_rules! instruction {
(
$(
$(#[$attrs:meta])*
$vis:vis fn $name:ident($($args:tt)*) {
$($instruction:expr;)*
}
)*
) => {
$(
$vis fn $name($($args)*) -> impl ByteCode {
move |filter: &mut Filter| {
$(
$instruction.into_bpf(filter);
)*
}
}
)*
};
}
instruction! {
pub fn VALIDATE_ARCH(target_arch: u32) {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_ARCH);
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, target_arch, 1, 0);
BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_PROCESS);
}
pub fn VALIDATE_ARCH_OR_ALTERNATE(target_arch: u32, alternate_arch: u32, action: sock_filter) {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_ARCH);
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, target_arch, 3, 0);
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, alternate_arch, 0, 1);
action;
BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_PROCESS);
}
pub fn LOAD_SYSCALL_IP() {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_IP_LO);
BPF_STMT(BPF_ST, 0);
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_IP_HI);
BPF_STMT(BPF_ST, 1);
}
pub fn SYSCALL(nr: Sysno, action: sock_filter) {
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr as i32 as u32, 0, 1);
action;
}
fn IP_RANGE64(blo: u32, bhi: u32, elo: u32, ehi: u32, action: sock_filter) {
BPF_JUMP(BPF_JMP + BPF_JGT + BPF_K, bhi, 4 , 0);
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, bhi, 0, 9 );
BPF_STMT(BPF_LD + BPF_MEM, 0);
BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, blo, 0, 7 );
BPF_STMT(BPF_LD + BPF_MEM, 1);
BPF_JUMP(BPF_JMP + BPF_JGT + BPF_K, ehi, 5 , 0);
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ehi, 0, 3 );
BPF_STMT(BPF_LD + BPF_MEM, 0);
BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, elo, 2 , 0);
BPF_STMT(BPF_LD + BPF_MEM, 1);
action;
BPF_STMT(BPF_LD + BPF_MEM, 1);
}
}
pub fn IP_EQ(ip: u64, action: sock_filter) -> impl ByteCode {
IP_EQ64(ip as u32, (ip >> 32) as u32, action)
}
instruction! {
fn IP_EQ64(lo: u32, hi: u32, action: sock_filter) {
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, hi, 0, 3 );
BPF_STMT(BPF_LD + BPF_MEM, 0);
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, lo, 0, 1 );
action;
BPF_STMT(BPF_LD + BPF_MEM, 1);
}
}
pub fn IP_RANGE(begin: u64, end: u64, action: sock_filter) -> impl ByteCode {
let begin_lo = begin as u32;
let begin_hi = (begin >> 32) as u32;
let end_lo = end as u32;
let end_hi = (end >> 32) as u32;
IP_RANGE64(begin_lo, begin_hi, end_lo, end_hi, action)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn smoke() {
let filter = seccomp_bpf![
VALIDATE_ARCH(AUDIT_ARCH_X86_64),
LOAD_SYSCALL_NR,
SYSCALL(Sysno::openat, DENY),
SYSCALL(Sysno::close, DENY),
SYSCALL(Sysno::write, DENY),
SYSCALL(Sysno::read, DENY),
ALLOW,
];
assert_eq!(filter.len(), 13);
}
const RET_ALLOW: u32 = libc::SECCOMP_RET_ALLOW;
const RET_MATCH: u32 = libc::SECCOMP_RET_TRACE | 1;
fn run(filter: &Filter, nr: u32, arch: u32, ip: u64) -> u32 {
const LD_ABS: u16 = BPF_LD + BPF_W + BPF_ABS;
const LD_MEM: u16 = BPF_LD + BPF_MEM;
const JEQ: u16 = BPF_JMP + BPF_JEQ + BPF_K;
const JGT: u16 = BPF_JMP + BPF_JGT + BPF_K;
const JGE: u16 = BPF_JMP + BPF_JGE + BPF_K;
const RET: u16 = BPF_RET + BPF_K;
let (mut acc, mut mem, mut pc) = (0u32, [0u32; 16], 0usize);
loop {
let insn = filter
.filter
.get(pc)
.unwrap_or_else(|| panic!("fell off the program at {pc}"));
pc += 1;
let jump = |taken: bool| usize::from(if taken { insn.jt } else { insn.jf });
match insn.code {
LD_ABS => {
acc = match insn.k {
SECCOMP_DATA_OFFSET_NR => nr,
SECCOMP_DATA_OFFSET_ARCH => arch,
SECCOMP_DATA_OFFSET_IP_LO => ip as u32,
SECCOMP_DATA_OFFSET_IP_HI => (ip >> 32) as u32,
k => panic!("load of unmodelled seccomp_data offset {k}"),
}
}
BPF_ST => mem[insn.k as usize] = acc,
LD_MEM => acc = mem[insn.k as usize],
JEQ => pc += jump(acc == insn.k),
JGT => pc += jump(acc > insn.k),
JGE => pc += jump(acc >= insn.k),
RET => return insn.k,
code => panic!("unmodelled opcode {code:#x} at {}", pc - 1),
}
}
}
fn range_filter(begin: u64, end: u64) -> Filter {
seccomp_bpf![
LOAD_SYSCALL_IP(),
IP_RANGE(begin, end, BPF_STMT(BPF_RET + BPF_K, RET_MATCH)),
ALLOW,
]
}
fn matches(filter: &Filter, ip: u64) -> bool {
match run(filter, 0, AUDIT_ARCH_X86_64, ip) {
RET_MATCH => true,
RET_ALLOW => false,
other => panic!("unexpected verdict {other:#x} for ip {ip:#x}"),
}
}
fn probes(begin: u64, end: u64) -> Vec<u64> {
let mut ips = vec![
begin.wrapping_sub(1),
begin,
end.wrapping_sub(1),
end,
end.wrapping_add(1),
0x7fff_ffff,
0xffff_ffff,
0x1_0000_0000u64.wrapping_add(begin),
];
for bound in [begin, end] {
ips.push(bound & !0xffff_ffff);
ips.push(bound | 0xffff_ffff);
}
ips
}
#[test]
fn ip_range_matches_exactly_the_half_open_interval() {
let ranges: &[(u64, u64)] = &[
(0x7100_0002, 0x7100_0003),
(0x1000, 0x2000),
(0x7fff_f000, 0x8000_1000),
(0xffff_f000, 0xffff_ffff),
(0x5_7100_0002, 0x5_7100_0003),
(0x7fff_0000_0000, 0x7fff_ffff_ffff),
(0xffff_f000, 0x1_0000_1000),
(0x7100_0002, 0x1_7100_0003),
(0x1_ffff_ffff, 0x2_0000_0001),
(0x1000, 0x7_0000_0000),
(0x3_8000_0000, 0x7fff_ffff_f000),
];
let mut wrong = Vec::new();
for &(begin, end) in ranges {
let filter = range_filter(begin, end);
for ip in probes(begin, end) {
let expected = begin <= ip && ip < end;
if matches(&filter, ip) != expected {
wrong.push(format!(
"[{begin:#x}, {end:#x}) ip {ip:#x}: expected match={expected}"
));
}
}
}
assert!(
wrong.is_empty(),
"{} wrong verdicts:\n{}",
wrong.len(),
wrong.join("\n")
);
}
#[test]
fn ip_range_matches_the_half_open_interval_over_a_bound_grid() {
let his = [0u64, 1, 2, 0x7fff, 0xffff_fffe, 0xffff_ffff];
let los = [
0u64,
1,
0x7100_0002,
0x7100_0003,
0x7fff_ffff,
0x8000_0000,
0xffff_fffe,
0xffff_ffff,
];
let points: Vec<u64> = his
.iter()
.flat_map(|hi| los.iter().map(move |lo| (hi << 32) | lo))
.collect();
let (mut checked, mut wrong) = (0usize, Vec::new());
for &begin in &points {
for &end in points.iter().filter(|&&end| end > begin) {
let filter = range_filter(begin, end);
for &ip in points.iter().chain(&probes(begin, end)) {
checked += 1;
let expected = begin <= ip && ip < end;
if matches(&filter, ip) != expected {
wrong.push((begin, end, ip, expected));
}
}
}
}
assert!(checked > 60_000, "grid shrank to {checked} verdicts");
assert!(
wrong.is_empty(),
"{} of {checked} verdicts wrong; first: {:x?}",
wrong.len(),
&wrong[..wrong.len().min(8)]
);
}
#[test]
fn ip_ranges_chain_into_later_ranges_and_syscall_rules() {
use crate::seccomp::Action;
use crate::seccomp::FilterBuilder;
let filter = FilterBuilder::new()
.default_action(Action::Allow)
.target_arch(crate::seccomp::TargetArch::x86_64)
.ip_range(0x7100_0002, 0x7100_0003, Action::Trace(1))
.ip_range(0x1_0000_0000, 0x1_0000_1000, Action::Trace(2))
.syscall(Sysno::getppid, Action::Trace(3))
.build();
let nr = Sysno::getppid as u32;
let other = Sysno::getpid as u32;
let verdict = |nr, ip| run(&filter, nr, AUDIT_ARCH_X86_64, ip);
let trace = |data: u32| libc::SECCOMP_RET_TRACE | data;
for (nr, ip, expected) in [
(nr, 0x7100_0002, trace(1)),
(other, 0x7100_0002, trace(1)),
(nr, 0x7100_0003, trace(3)),
(nr, 0x7200_0002, trace(3)),
(other, 0x7200_0002, RET_ALLOW),
(nr, 0x1_0000_0000, trace(2)),
(nr, 0x1_0000_0fff, trace(2)),
(nr, 0x1_0000_1000, trace(3)),
(other, 0x1_0000_1000, RET_ALLOW),
(nr, 0x2_7100_0002, trace(3)),
] {
assert_eq!(verdict(nr, ip), expected, "nr {nr} ip {ip:#x}");
}
assert_eq!(
run(&filter, nr, AUDIT_ARCH_X86_64 ^ 1, 0x7100_0002),
libc::SECCOMP_RET_KILL_PROCESS
);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn kernel_ip_range_verdicts_match_the_interpreter() {
use crate::seccomp::Action;
use crate::seccomp::FilterBuilder;
const MATCHED: i32 = 10;
const UNMATCHED: i32 = 11;
const MAP_FAILED: i32 = 12;
const OTHER: i32 = 13;
fn kernel_matches(filter: &Filter, ip: u64) -> bool {
let page = 0x1000u64;
let first = (ip - 2) & !(page - 1);
let len = ((ip + 1 + page - 1) & !(page - 1)) - first;
match unsafe { libc::fork() } {
0 => unsafe {
let base = libc::mmap(
first as *mut libc::c_void,
len as usize,
libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED_NOREPLACE,
-1,
0,
);
if base as u64 != first {
libc::_exit(MAP_FAILED);
}
let stub = [0x0f, 0x05, 0xc3u8];
std::ptr::copy_nonoverlapping(stub.as_ptr(), (ip - 2) as *mut u8, 3);
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
|| filter.load().is_err()
{
libc::_exit(OTHER);
}
let ret: i64;
std::arch::asm!(
"call {stub}",
stub = in(reg) ip - 2,
inlateout("rax") libc::SYS_getppid => ret,
out("rcx") _,
out("r11") _,
);
libc::_exit(if ret == -(libc::EXDEV as i64) {
MATCHED
} else if ret > 0 {
UNMATCHED
} else {
OTHER
});
},
-1 => panic!("fork failed: {}", std::io::Error::last_os_error()),
pid => {
let mut status = 0;
assert_eq!(unsafe { libc::waitpid(pid, &mut status, 0) }, pid);
assert!(libc::WIFEXITED(status), "child status {status:#x}");
match libc::WEXITSTATUS(status) {
MATCHED => true,
UNMATCHED => false,
MAP_FAILED => panic!("could not map a stub page at {first:#x}"),
code => panic!("child for ip {ip:#x} failed with {code}"),
}
}
}
}
let (mut unfaithful, mut wrong) = (Vec::new(), Vec::new());
for (begin, end, ips) in [
(
0x7100_0002u64,
0x7100_0003u64,
&[
0x7100_0001u64,
0x7100_0002,
0x7100_0003,
0x7200_0002,
0x7fff_ffff,
0xffff_ffff,
0x1_7100_0002,
][..],
),
(
0xffff_f000,
0x1_0000_1000,
&[0xffff_efff, 0xffff_f000, 0x1_0000_0fff, 0x1_0000_1000][..],
),
] {
let built = FilterBuilder::new()
.default_action(Action::Allow)
.ip_range(begin, end, Action::Errno(Errno::EXDEV))
.build();
for &ip in ips {
let expected = begin <= ip && ip < end;
let interpreted = run(&built, Sysno::getppid as u32, AUDIT_ARCH_X86_64, ip)
== (libc::SECCOMP_RET_ERRNO | libc::EXDEV as u32);
let kernel = kernel_matches(&built, ip);
let row = format!("[{begin:#x}, {end:#x}) ip {ip:#x}: kernel match={kernel}");
if kernel != interpreted {
unfaithful.push(format!("{row}, interpreter match={interpreted}"));
}
if kernel != expected {
wrong.push(format!("{row}, interval match={expected}"));
}
}
}
assert!(
unfaithful.is_empty(),
"interpreter disagrees with the kernel:\n{}",
unfaithful.join("\n")
);
assert!(
wrong.is_empty(),
"kernel verdicts outside the interval:\n{}",
wrong.join("\n")
);
}
}