#[macro_use]
mod bpf;
#[allow(unused)]
mod notif;
use std::collections::BTreeMap;
pub use bpf::Filter;
use bpf::*;
pub use notif::*;
use syscalls::Errno;
use syscalls::Sysno;
#[derive(Debug, Clone)]
pub struct FilterBuilder {
target_arch: TargetArch,
default_action: Action,
syscalls: BTreeMap<Sysno, Action>,
ip_ranges: Vec<(u64, u64, Action)>,
instruction_pointers: Vec<(u64, Action)>,
alternate_arch: Option<(TargetArch, Action)>,
}
#[allow(non_camel_case_types, missing_docs)]
#[derive(Debug, Copy, Clone)]
#[repr(u32)]
pub enum TargetArch {
x86 = AUDIT_ARCH_X86,
x86_64 = AUDIT_ARCH_X86_64,
mips = AUDIT_ARCH_MIPS,
powerpc = AUDIT_ARCH_PPC,
powerpc64 = AUDIT_ARCH_PPC64,
arm = AUDIT_ARCH_ARM,
aarch64 = AUDIT_ARCH_AARCH64,
}
#[derive(Debug, Copy, Clone)]
pub enum Action {
Allow,
Errno(Errno),
KillThread,
KillProcess,
Log,
Trace(u16),
Trap,
Notify,
}
impl From<Action> for u32 {
fn from(action: Action) -> u32 {
match action {
Action::Allow => libc::SECCOMP_RET_ALLOW,
Action::Errno(x) => {
libc::SECCOMP_RET_ERRNO | (x.into_raw() as u32 & libc::SECCOMP_RET_DATA)
}
Action::KillThread => libc::SECCOMP_RET_KILL_THREAD,
Action::KillProcess => libc::SECCOMP_RET_KILL_PROCESS,
Action::Log => libc::SECCOMP_RET_LOG,
Action::Trace(x) => libc::SECCOMP_RET_TRACE | (x as u32 & libc::SECCOMP_RET_DATA),
Action::Trap => libc::SECCOMP_RET_TRAP,
Action::Notify => 0x7fc00000u32,
}
}
}
impl From<Action> for sock_filter {
fn from(action: Action) -> sock_filter {
BPF_STMT(BPF_RET + BPF_K, u32::from(action))
}
}
impl TargetArch {
#![allow(missing_docs)]
#[cfg(target_arch = "x86")]
pub const CURRENT: TargetArch = Self::x86;
#[cfg(target_arch = "x86_64")]
pub const CURRENT: TargetArch = Self::x86_64;
#[cfg(target_arch = "mips")]
pub const CURRENT: TargetArch = Self::mips;
#[cfg(target_arch = "powerpc")]
pub const CURRENT: TargetArch = Self::powerpc;
#[cfg(target_arch = "powerpc64")]
pub const CURRENT: TargetArch = Self::powerpc64;
#[cfg(target_arch = "arm")]
pub const CURRENT: TargetArch = Self::arm;
#[cfg(target_arch = "aarch64")]
pub const CURRENT: TargetArch = Self::aarch64;
}
impl Default for TargetArch {
fn default() -> Self {
Self::CURRENT
}
}
impl Default for FilterBuilder {
fn default() -> Self {
Self::new()
}
}
impl FilterBuilder {
pub fn new() -> Self {
Self {
target_arch: TargetArch::default(),
default_action: Action::KillThread,
syscalls: Default::default(),
ip_ranges: Default::default(),
instruction_pointers: Default::default(),
alternate_arch: None,
}
}
pub fn target_arch(&mut self, target_arch: TargetArch) -> &mut Self {
self.target_arch = target_arch;
self
}
pub fn default_action(&mut self, action: Action) -> &mut Self {
self.default_action = action;
self
}
pub fn syscall(&mut self, syscall: Sysno, action: Action) -> &mut Self {
self.syscalls.insert(syscall, action);
self
}
pub fn syscalls<I>(&mut self, table: I) -> &mut Self
where
I: IntoIterator<Item = (Sysno, Action)>,
{
self.syscalls.extend(table);
self
}
pub fn alternate_arch(&mut self, arch: TargetArch, action: Action) -> &mut Self {
self.alternate_arch = Some((arch, action));
self
}
pub fn instruction_pointer(&mut self, ip: u64, action: Action) -> &mut Self {
self.instruction_pointers.push((ip, action));
self
}
pub fn ip_range(&mut self, begin: u64, end: u64, action: Action) -> &mut Self {
self.ip_ranges.push((begin, end, action));
self
}
pub fn ip_ranges<I>(&mut self, ranges: I) -> &mut Self
where
I: IntoIterator<Item = (u64, u64, Action)>,
{
self.ip_ranges.extend(ranges);
self
}
pub fn build(&self) -> Filter {
let mut filter = Filter::new();
match self.alternate_arch {
None => VALIDATE_ARCH(self.target_arch as u32).into_bpf(&mut filter),
Some((arch, action)) => {
VALIDATE_ARCH_OR_ALTERNATE(self.target_arch as u32, arch as u32, action.into())
.into_bpf(&mut filter)
}
}
if !self.instruction_pointers.is_empty() || !self.ip_ranges.is_empty() {
LOAD_SYSCALL_IP().into_bpf(&mut filter);
for (ip, action) in &self.instruction_pointers {
IP_EQ(*ip, (*action).into()).into_bpf(&mut filter);
}
for (begin, end, action) in &self.ip_ranges {
IP_RANGE(*begin, *end, (*action).into()).into_bpf(&mut filter);
}
}
if !self.syscalls.is_empty() {
LOAD_SYSCALL_NR.into_bpf(&mut filter);
for (syscall, action) in &self.syscalls {
SYSCALL(*syscall, (*action).into()).into_bpf(&mut filter);
}
}
sock_filter::from(self.default_action).into_bpf(&mut filter);
filter
}
}
#[cfg(test)]
mod tests {
use super::*;
fn words(filter: &Filter) -> Vec<(u16, u8, u8, u32)> {
filter
.instructions()
.iter()
.map(|insn| (insn.code, insn.jt, insn.jf, insn.k))
.collect()
}
const LD_ARCH: (u16, u8, u8, u32) = (0x20, 0, 0, 4);
const RET_KILL_PROCESS: (u16, u8, u8, u32) = (0x06, 0, 0, 0x8000_0000);
#[test]
fn alternate_arch_prologue_is_exact_and_unset_emits_nothing_new() {
let mut plain = FilterBuilder::new();
plain
.target_arch(TargetArch::x86_64)
.default_action(Action::Allow)
.syscall(Sysno::getpid, Action::Trace(0));
let mut routed = plain.clone();
routed.alternate_arch(TargetArch::x86, Action::Trace(0x7101));
let plain = words(&plain.build());
let routed = words(&routed.build());
assert_eq!(
plain[..3],
[LD_ARCH, (0x15, 1, 0, AUDIT_ARCH_X86_64), RET_KILL_PROCESS]
);
assert_eq!(
routed[..5],
[
LD_ARCH,
(0x15, 3, 0, AUDIT_ARCH_X86_64),
(0x15, 0, 1, AUDIT_ARCH_X86),
(0x06, 0, 0, 0x7ff0_0000 | 0x7101),
RET_KILL_PROCESS,
]
);
assert_eq!(plain[3..], routed[5..]);
}
#[test]
fn instruction_pointer_is_an_exact_match_checked_before_ranges() {
let mut builder = FilterBuilder::new();
builder
.default_action(Action::Allow)
.ip_range(0x7100_0002, 0x7100_0003, Action::Allow)
.instruction_pointer(0x1234_5678_7100_0006, Action::Trace(0x7102));
let words = words(&builder.build());
let ld_mem = |slot| (0x60, 0, 0, slot);
assert_eq!(
words[3..7],
[
(0x20, 0, 0, 8),
(0x02, 0, 0, 0),
(0x20, 0, 0, 12),
(0x02, 0, 0, 1)
],
"LOAD_SYSCALL_IP"
);
assert_eq!(
words[7..12],
[
(0x15, 0, 3, 0x1234_5678),
ld_mem(0),
(0x15, 0, 1, 0x7100_0006),
(0x06, 0, 0, 0x7ff0_0000 | 0x7102),
ld_mem(1),
]
);
let mut range_only = FilterBuilder::new();
range_only
.default_action(Action::Allow)
.ip_range(0x7100_0002, 0x7100_0003, Action::Allow);
let range_only = self::words(&range_only.build());
assert_eq!(words[12..], range_only[7..]);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn smoke() {
assert_eq!(
FilterBuilder::new()
.default_action(Action::Allow)
.target_arch(TargetArch::x86_64)
.syscalls([
(Sysno::read, Action::KillThread),
(Sysno::write, Action::KillThread),
(Sysno::open, Action::KillThread),
(Sysno::close, Action::KillThread),
(Sysno::write, Action::KillThread),
])
.build(),
seccomp_bpf![
VALIDATE_ARCH(AUDIT_ARCH_X86_64),
LOAD_SYSCALL_NR,
SYSCALL(Sysno::read, DENY),
SYSCALL(Sysno::write, DENY),
SYSCALL(Sysno::open, DENY),
SYSCALL(Sysno::close, DENY),
ALLOW,
]
);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn smoke() {
assert_eq!(
FilterBuilder::new()
.default_action(Action::Allow)
.target_arch(TargetArch::aarch64)
.syscalls([
(Sysno::read, Action::KillThread),
(Sysno::write, Action::KillThread),
(Sysno::openat, Action::KillThread),
(Sysno::close, Action::KillThread),
(Sysno::write, Action::KillThread),
])
.build(),
seccomp_bpf![
VALIDATE_ARCH(AUDIT_ARCH_AARCH64),
LOAD_SYSCALL_NR,
SYSCALL(Sysno::openat, DENY),
SYSCALL(Sysno::close, DENY),
SYSCALL(Sysno::read, DENY),
SYSCALL(Sysno::write, DENY),
ALLOW,
]
);
}
}