Skip to main content

kernel_abi_tools/
filter.rs

1//! Classic-BPF seccomp filters built directly from the data, for the local
2//! runner: no container, no libseccomp, and no root.
3//!
4//! The program makes the same decisions as [`crate::seccomp_profile`]:
5//!
6//! 1. Any architecture other than x86_64 (e.g. `int 0x80` i386 calls) and any
7//!    x32 syscall number returns `ENOSYS`.
8//! 2. `madvise` returns `EINVAL` unless the low 32 bits of the advice (the
9//!    kernel reads an `int`) are a value the target kernel knows.
10//! 3. Syscalls the target provides are allowed; everything else, including
11//!    numbers this crate's data does not know, returns `ENOSYS`.
12//!
13//! Installing a filter needs no privilege once the process has set
14//! `PR_SET_NO_NEW_PRIVS`. Like the profiles, the filter is a testing aid and
15//! not a security boundary.
16
17use crate::{madvise_accepts, madvise_advice, present_syscalls, Target, EINVAL, ENOSYS};
18
19/// `AUDIT_ARCH_X86_64`: `EM_X86_64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE`.
20pub const AUDIT_ARCH_X86_64: u32 = 0xc000_003e;
21/// Syscall numbers at or above this bit belong to the x32 ABI.
22pub const X32_SYSCALL_BIT: u32 = 0x4000_0000;
23/// x86_64 `madvise` syscall number.
24pub const SYS_MADVISE: u32 = 28;
25
26/// `SECCOMP_RET_ALLOW`.
27pub const RET_ALLOW: u32 = 0x7fff_0000;
28/// `SECCOMP_RET_ERRNO`; the errno goes in the low 16 bits.
29pub const RET_ERRNO: u32 = 0x0005_0000;
30
31// Opcodes used by the program (linux/bpf_common.h).
32const LD_W_ABS: u16 = 0x20; // BPF_LD | BPF_W | BPF_ABS
33const JEQ_K: u16 = 0x15; // BPF_JMP | BPF_JEQ | BPF_K
34const JGT_K: u16 = 0x25; // BPF_JMP | BPF_JGT | BPF_K
35const JGE_K: u16 = 0x35; // BPF_JMP | BPF_JGE | BPF_K
36const RET_K: u16 = 0x06; // BPF_RET | BPF_K
37
38// Offsets into `struct seccomp_data` (x86_64 is little-endian, so the low
39// word of an argument comes first).
40const OFF_NR: u32 = 0;
41const OFF_ARCH: u32 = 4;
42const OFF_ARG2_LO: u32 = 16 + 2 * 8;
43
44/// The kernel's `BPF_MAXINSNS`.
45pub const MAX_INSNS: usize = 4096;
46
47/// One `struct sock_filter` instruction.
48#[repr(C)]
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct Insn {
51    /// The opcode (`BPF_LD | BPF_W | BPF_ABS` and so on).
52    pub code: u16,
53    /// Instructions to skip when a conditional jump is taken.
54    pub jt: u8,
55    /// Instructions to skip when a conditional jump is not taken.
56    pub jf: u8,
57    /// The constant operand: an offset, a value to compare, or a return value.
58    pub k: u32,
59}
60
61const fn stmt(code: u16, k: u32) -> Insn {
62    Insn {
63        code,
64        jt: 0,
65        jf: 0,
66        k,
67    }
68}
69
70const fn jump(code: u16, k: u32, jt: u8, jf: u8) -> Insn {
71    Insn { code, jt, jf, k }
72}
73
74/// Builds the filter program for `target`.
75///
76/// Allowed syscall numbers are merged into contiguous ranges; each range is
77/// three instructions with only short forward jumps, so the program needs no
78/// jump longer than the 255 instructions BPF allows.
79pub fn program(target: &Target) -> Vec<Insn> {
80    let enosys = RET_ERRNO | ENOSYS as u32;
81    let einval = RET_ERRNO | EINVAL as u32;
82    let mut p = vec![
83        stmt(LD_W_ABS, OFF_ARCH),
84        jump(JEQ_K, AUDIT_ARCH_X86_64, 1, 0),
85        stmt(RET_K, enosys),
86        stmt(LD_W_ABS, OFF_NR),
87        jump(JGE_K, X32_SYSCALL_BIT, 0, 1),
88        stmt(RET_K, enosys),
89    ];
90
91    let mut nrs: Vec<u32> = present_syscalls(target).iter().map(|s| s.nr).collect();
92    nrs.sort_unstable();
93    nrs.dedup();
94    if nrs.binary_search(&SYS_MADVISE).is_ok() {
95        let accepted: Vec<u32> = madvise_advice()
96            .iter()
97            .map(|a| a.value)
98            .filter(|&v| madvise_accepts(target.kernel, v))
99            .collect();
100        // Not madvise: skip the advice block (the load, one test per value,
101        // EINVAL, and ALLOW) and land on the reload of the number.
102        let skip = u8::try_from(accepted.len() + 3).expect("madvise block fits a BPF jump");
103        p.push(jump(JEQ_K, SYS_MADVISE, 0, skip));
104        p.push(stmt(LD_W_ABS, OFF_ARG2_LO));
105        let n = accepted.len();
106        for (i, v) in accepted.iter().enumerate() {
107            // Jump over the remaining tests and the EINVAL to the ALLOW.
108            let to_allow = u8::try_from(n - i).expect("madvise block fits a BPF jump");
109            p.push(jump(JEQ_K, *v, to_allow, 0));
110        }
111        p.push(stmt(RET_K, einval));
112        p.push(stmt(RET_K, RET_ALLOW));
113        // The accumulator holds the advice now; reload the number.
114        p.push(stmt(LD_W_ABS, OFF_NR));
115    }
116
117    for (lo, hi) in ranges(&nrs) {
118        p.push(jump(JGE_K, lo, 0, 2));
119        p.push(jump(JGT_K, hi, 1, 0));
120        p.push(stmt(RET_K, RET_ALLOW));
121    }
122    p.push(stmt(RET_K, enosys));
123    assert!(p.len() <= MAX_INSNS, "filter exceeds BPF_MAXINSNS");
124    p
125}
126
127fn ranges(sorted: &[u32]) -> Vec<(u32, u32)> {
128    let mut out: Vec<(u32, u32)> = Vec::new();
129    for &n in sorted {
130        match out.last_mut() {
131            Some((_, hi)) if *hi + 1 == n => *hi = n,
132            _ => out.push((n, n)),
133        }
134    }
135    out
136}
137
138/// Runs `prog` on one syscall the way the kernel does and returns the
139/// `SECCOMP_RET_*` value. Supports only the opcodes [`program`] emits.
140///
141/// `arch` is the `AUDIT_ARCH_*` value, `nr` the syscall number, and `args` its
142/// six arguments.
143///
144/// # Errors
145///
146/// Returns a message for an opcode or load offset [`program`] never emits, and
147/// when the program ends without returning.
148pub fn evaluate(prog: &[Insn], arch: u32, nr: u32, args: [u64; 6]) -> Result<u32, String> {
149    let word = |off: u32| -> Result<u32, String> {
150        match off {
151            OFF_NR => Ok(nr),
152            OFF_ARCH => Ok(arch),
153            16..=63 if off.is_multiple_of(4) => {
154                let arg = args[(off as usize - 16) / 8];
155                Ok(if off.is_multiple_of(8) {
156                    arg as u32
157                } else {
158                    (arg >> 32) as u32
159                })
160            }
161            _ => Err(format!("load from unsupported offset {off}")),
162        }
163    };
164    let mut acc = 0u32;
165    let mut pc = 0usize;
166    while let Some(insn) = prog.get(pc) {
167        pc += 1;
168        let taken = match insn.code {
169            LD_W_ABS => {
170                acc = word(insn.k)?;
171                continue;
172            }
173            RET_K => return Ok(insn.k),
174            JEQ_K => acc == insn.k,
175            JGT_K => acc > insn.k,
176            JGE_K => acc >= insn.k,
177            other => return Err(format!("unsupported opcode {other:#x}")),
178        };
179        pc += usize::from(if taken { insn.jt } else { insn.jf });
180    }
181    Err("program fell off the end".to_string())
182}
183
184/// Installs filters in the calling thread (and what it executes).
185#[cfg(target_os = "linux")]
186pub mod install {
187    use super::Insn;
188    use std::io;
189
190    const PR_SET_NO_NEW_PRIVS: i32 = 38;
191    const PR_SET_SECCOMP: i32 = 22;
192    const SECCOMP_MODE_FILTER: u64 = 2;
193
194    /// `struct sock_fprog`.
195    #[repr(C)]
196    pub struct Prog<'a> {
197        len: u16,
198        filter: *const Insn,
199        _insns: std::marker::PhantomData<&'a [Insn]>,
200    }
201
202    impl<'a> Prog<'a> {
203        /// Wraps `insns` for [`apply`].
204        ///
205        /// # Panics
206        ///
207        /// Panics if `insns` has more than 65535 instructions.
208        pub fn new(insns: &'a [Insn]) -> Self {
209            Prog {
210                len: u16::try_from(insns.len()).expect("filter fits sock_fprog"),
211                filter: insns.as_ptr(),
212                _insns: std::marker::PhantomData,
213            }
214        }
215    }
216
217    extern "C" {
218        fn prctl(option: i32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> i32;
219    }
220
221    /// Sets `no_new_privs` and installs `prog` for the calling thread. It
222    /// cannot be removed and is inherited across `fork` and `execve`.
223    ///
224    /// Only calls `prctl`, so it is async-signal-safe and may run between
225    /// `fork` and `exec` (for example from `CommandExt::pre_exec`).
226    ///
227    /// # Errors
228    ///
229    /// Returns the OS error when either `prctl` call fails, for example when the
230    /// kernel lacks seccomp filters or the program is too long.
231    pub fn apply(prog: &Prog<'_>) -> io::Result<()> {
232        // SAFETY: plain prctl calls; `prog` outlives them and points at
233        // `len` valid instructions.
234        unsafe {
235            if prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 {
236                return Err(io::Error::last_os_error());
237            }
238            let ptr = prog as *const Prog<'_> as u64;
239            if prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ptr, 0, 0) != 0 {
240                return Err(io::Error::last_os_error());
241            }
242        }
243        Ok(())
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::{syscalls, table_ceiling, KernelVersion};
251
252    fn kv(s: &str) -> KernelVersion {
253        KernelVersion::parse(s).unwrap()
254    }
255
256    fn eval(prog: &[Insn], nr: u32, a2: u64) -> u32 {
257        evaluate(prog, AUDIT_ARCH_X86_64, nr, [0, 0, a2, 0, 0, 0]).unwrap()
258    }
259
260    /// The filter must agree with the data (and so with the JSON profile)
261    /// for every known syscall, unknown numbers, and every advice value.
262    fn assert_matches_data(target: &Target) {
263        let prog = program(target);
264        let present: Vec<u32> = present_syscalls(target).iter().map(|s| s.nr).collect();
265        let max_nr = syscalls().iter().map(|s| s.nr).max().unwrap();
266        for nr in 0..=max_nr + 64 {
267            if nr == SYS_MADVISE {
268                continue;
269            }
270            let want = if present.contains(&nr) {
271                RET_ALLOW
272            } else {
273                RET_ERRNO | ENOSYS as u32
274            };
275            assert_eq!(eval(&prog, nr, 0), want, "{}: nr {nr}", target.label);
276        }
277        let max_advice = madvise_advice().iter().map(|a| a.value).max().unwrap();
278        for v in 0..=max_advice + 8 {
279            let want = if madvise_accepts(target.kernel, v) {
280                RET_ALLOW
281            } else {
282                RET_ERRNO | EINVAL as u32
283            };
284            assert_eq!(eval(&prog, SYS_MADVISE, v as u64), want, "advice {v}");
285            // Only the low 32 bits count, as with SCMP_CMP_MASKED_EQ.
286            let high = (0xffff_ffffu64 << 32) | v as u64;
287            assert_eq!(
288                eval(&prog, SYS_MADVISE, high),
289                want,
290                "advice {v} + high bits"
291            );
292        }
293    }
294
295    #[test]
296    fn agrees_with_the_data_for_generic_kernels() {
297        for k in crate::GENERIC_KERNELS {
298            assert_matches_data(&Target::kernel(kv(k)));
299        }
300        assert_matches_data(&Target::kernel(table_ceiling()));
301    }
302
303    #[test]
304    fn agrees_with_the_data_for_distro_backports() {
305        for d in crate::distros() {
306            assert_matches_data(&Target::distro(&d));
307        }
308    }
309
310    #[test]
311    fn blocks_other_architectures_and_x32() {
312        let prog = program(&Target::kernel(table_ceiling()));
313        let enosys = RET_ERRNO | ENOSYS as u32;
314        const AUDIT_ARCH_I386: u32 = 0x4000_0003;
315        assert_eq!(evaluate(&prog, AUDIT_ARCH_I386, 0, [0; 6]), Ok(enosys));
316        assert_eq!(eval(&prog, X32_SYSCALL_BIT, 0), enosys);
317        assert_eq!(eval(&prog, X32_SYSCALL_BIT | 1, 0), enosys);
318        assert_eq!(eval(&prog, u32::MAX, 0), enosys);
319        assert_eq!(eval(&prog, 0, 0), RET_ALLOW); // read
320    }
321
322    #[test]
323    fn stays_well_under_the_instruction_limit() {
324        let len = program(&Target::kernel(kv("3.10"))).len();
325        assert!(len < 512, "{len} instructions");
326    }
327
328    #[test]
329    fn instruction_layout_matches_sock_filter() {
330        assert_eq!(std::mem::size_of::<Insn>(), 8);
331    }
332}