Skip to main content

reverie_process/seccomp/
bpf.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9#![allow(non_snake_case)]
10
11pub use libc::sock_filter;
12use syscalls::Errno;
13use syscalls::Sysno;
14
15use crate::fd::Fd;
16
17// See: /include/uapi/linux/bpf_common.h
18
19// Instruction classes
20pub const BPF_LD: u16 = 0x00;
21pub const BPF_ST: u16 = 0x02;
22pub const BPF_JMP: u16 = 0x05;
23pub const BPF_RET: u16 = 0x06;
24
25// ld/ldx fields
26pub const BPF_W: u16 = 0x00;
27
28pub const BPF_ABS: u16 = 0x20;
29pub const BPF_MEM: u16 = 0x60;
30
31pub const BPF_JEQ: u16 = 0x10;
32pub const BPF_JGT: u16 = 0x20;
33pub const BPF_JGE: u16 = 0x30;
34pub const BPF_K: u16 = 0x00;
35
36/// Maximum number of instructions.
37pub const BPF_MAXINSNS: usize = 4096;
38
39/// Defined in `/include/uapi/linux/seccomp.h`.
40const SECCOMP_SET_MODE_FILTER: u32 = 1;
41
42/// Offset of `seccomp_data::nr` in bytes.
43const SECCOMP_DATA_OFFSET_NR: u32 = 0;
44
45/// Offset of `seccomp_data::arch` in bytes.
46const SECCOMP_DATA_OFFSET_ARCH: u32 = 4;
47
48/// Offset of `seccomp_data::instruction_pointer` in bytes.
49const SECCOMP_DATA_OFFSET_IP: u32 = 8;
50
51/// Offset of `seccomp_data::args` in bytes.
52#[allow(unused)]
53const SECCOMP_DATA_OFFSET_ARGS: u32 = 16;
54
55#[cfg(target_endian = "little")]
56const SECCOMP_DATA_OFFSET_IP_HI: u32 = SECCOMP_DATA_OFFSET_IP + 4;
57#[cfg(target_endian = "little")]
58const SECCOMP_DATA_OFFSET_IP_LO: u32 = SECCOMP_DATA_OFFSET_IP;
59
60#[cfg(target_endian = "big")]
61const SECCOMP_DATA_OFFSET_IP_HI: u32 = SECCOMP_DATA_OFFSET_IP;
62#[cfg(target_endian = "big")]
63const SECCOMP_DATA_OFFSET_IP_LO: u32 = SECCOMP_DATA_OFFSET_IP + 4;
64
65// These are defined in `/include/uapi/linux/elf-em.h`.
66const EM_386: u32 = 3;
67const EM_MIPS: u32 = 8;
68const EM_PPC: u32 = 20;
69const EM_PPC64: u32 = 21;
70const EM_ARM: u32 = 40;
71const EM_X86_64: u32 = 62;
72const EM_AARCH64: u32 = 183;
73
74// These are defined in `/include/uapi/linux/audit.h`.
75const __AUDIT_ARCH_64BIT: u32 = 0x8000_0000;
76const __AUDIT_ARCH_LE: u32 = 0x4000_0000;
77
78// These are defined in `/include/uapi/linux/audit.h`.
79pub const AUDIT_ARCH_X86: u32 = EM_386 | __AUDIT_ARCH_LE;
80pub const AUDIT_ARCH_X86_64: u32 = EM_X86_64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE;
81pub const AUDIT_ARCH_ARM: u32 = EM_ARM | __AUDIT_ARCH_LE;
82pub const AUDIT_ARCH_AARCH64: u32 = EM_AARCH64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE;
83pub const AUDIT_ARCH_MIPS: u32 = EM_MIPS;
84pub const AUDIT_ARCH_PPC: u32 = EM_PPC;
85pub const AUDIT_ARCH_PPC64: u32 = EM_PPC64 | __AUDIT_ARCH_64BIT;
86
87bitflags::bitflags! {
88    #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
89    struct FilterFlags: u32 {
90        const TSYNC = 1 << 0;
91        const LOG = 1 << 1;
92        const SPEC_ALLOW = 1 << 2;
93        const NEW_LISTENER = 1 << 3;
94        const TSYNC_ESRCH = 1 << 4;
95    }
96}
97
98/// Seccomp-BPF program byte code.
99#[derive(Debug, Clone, Eq, PartialEq)]
100pub struct Filter {
101    // Since the limit is 4096 instructions, we *could* use a static array here
102    // instead. However, that would require bounds checks each time an
103    // instruction is appended and complicate the interface with `Result` types
104    // and error handling logic. It's cleaner to just check the size when the
105    // program is loaded.
106    filter: Vec<sock_filter>,
107}
108
109impl Default for Filter {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl Filter {
116    /// Creates a new, empty seccomp program. Note that empty BPF programs are not
117    /// valid and will fail to load.
118    pub const fn new() -> Self {
119        Self { filter: Vec::new() }
120    }
121
122    /// Appends a single instruction to the seccomp-BPF program.
123    pub fn push(&mut self, instruction: sock_filter) {
124        self.filter.push(instruction);
125    }
126
127    /// Returns the number of instructions in the BPF program.
128    pub fn len(&self) -> usize {
129        self.filter.len()
130    }
131
132    /// Returns true if the program is empty. Empty seccomp filters will result
133    /// in an error when loaded.
134    pub fn is_empty(&self) -> bool {
135        self.filter.is_empty()
136    }
137
138    /// Returns the program's instructions, in order, exactly as they would be
139    /// loaded.
140    pub fn instructions(&self) -> &[sock_filter] {
141        &self.filter
142    }
143
144    fn install(&self, flags: FilterFlags) -> Result<i32, Errno> {
145        let len = self.filter.len();
146
147        if len == 0 || len > BPF_MAXINSNS {
148            return Err(Errno::EINVAL);
149        }
150
151        let prog = libc::sock_fprog {
152            // Note: length is guaranteed to be less than `u16::MAX` because of
153            // the above check.
154            len: len as u16,
155            filter: self.filter.as_ptr() as *mut _,
156        };
157
158        let ptr = &prog as *const libc::sock_fprog;
159
160        let value = Errno::result(unsafe {
161            libc::syscall(
162                libc::SYS_seccomp,
163                SECCOMP_SET_MODE_FILTER,
164                flags.bits(),
165                ptr,
166            )
167        })?;
168
169        Ok(value as i32)
170    }
171
172    /// Loads the program via seccomp into the current process.
173    ///
174    /// Once loaded, the seccomp filter can never be removed. Additional seccomp
175    /// filters can be loaded, however, and they will chain together and be
176    /// executed in reverse order.
177    ///
178    /// NOTE: The maximum size of any single seccomp-bpf filter is 4096
179    /// instructions. The overall limit is 32768 instructions across all loaded
180    /// filters.
181    ///
182    /// See [`seccomp(2)`](https://man7.org/linux/man-pages/man2/seccomp.2.html)
183    /// for more details.
184    pub fn load(&self) -> Result<(), Errno> {
185        self.install(FilterFlags::empty())?;
186        Ok(())
187    }
188
189    /// This is the same as [`Filter::load`] except that it returns a file
190    /// descriptor. This is meant to be used with
191    /// [`seccomp_unotify(2)`](https://man7.org/linux/man-pages/man2/seccomp_unotify.2.html).
192    pub fn load_and_listen(&self) -> Result<Fd, Errno> {
193        let fd = self.install(FilterFlags::NEW_LISTENER)?;
194        Ok(Fd::new(fd))
195    }
196}
197
198impl Extend<sock_filter> for Filter {
199    fn extend<T: IntoIterator<Item = sock_filter>>(&mut self, iter: T) {
200        self.filter.extend(iter)
201    }
202}
203
204/// Trait for types that can emit BPF byte code.
205pub trait ByteCode {
206    /// Accumulates BPF instructions into the given filter.
207    fn into_bpf(self, filter: &mut Filter);
208}
209
210impl<F> ByteCode for F
211where
212    F: FnOnce(&mut Filter),
213{
214    fn into_bpf(self, filter: &mut Filter) {
215        self(filter)
216    }
217}
218
219impl ByteCode for sock_filter {
220    fn into_bpf(self, filter: &mut Filter) {
221        filter.push(self)
222    }
223}
224
225/// Returns a seccomp-bpf filter containing the given list of instructions.
226///
227/// This can be concatenated with other seccomp-BPF programs.
228///
229/// Note that this is not a true BPF program. Seccomp-bpf is a subset of BPF and
230/// so many instructions are not available.
231///
232/// When executing instructions, the BPF program operates on the syscall
233/// information made available as a (read-only) buffer of the following form:
234///
235/// ```no_compile
236/// struct seccomp_data {
237///     // The syscall number.
238///     nr: u32,
239///     // `AUDIT_ARCH_*` value (see `<linux/audit.h`).
240///     arch: u32,
241///     // CPU instruction pointer.
242///     instruction_pointer: u64,
243///     // Up to 6 syscall arguments.
244///     args: [u64; 8],
245/// }
246/// ```
247///
248/// # Example
249///
250/// This filter will allow only the specified syscalls.
251/// ```
252/// let _filter = seccomp_bpf![
253///     // Make sure the target process is using the x86-64 syscall ABI.
254///     VALIDATE_ARCH(AUDIT_ARCH_X86_64),
255///     // Load the current syscall number into `seccomp_data.nr`.
256///     LOAD_SYSCALL_NR,
257///     // Check if `seccomp_data.nr` matches the given syscalls. If so, then return
258///     // from the seccomp filter early, allowing the syscall to continue.
259///     SYSCALL(Sysno::open, ALLOW),
260///     SYSCALL(Sysno::close, ALLOW),
261///     SYSCALL(Sysno::write, ALLOW),
262///     SYSCALL(Sysno::read, ALLOW),
263///     // Deny all other syscalls by having the kernel kill the current thread with
264///     // `SIGSYS`.
265///     DENY,
266/// ];
267/// ```
268#[cfg(test)]
269macro_rules! seccomp_bpf {
270    ($($inst:expr),+ $(,)?) => {
271        {
272            let mut filter = Filter::new();
273            $(
274                $inst.into_bpf(&mut filter);
275            )+
276            filter
277        }
278    };
279}
280
281// See: /include/uapi/linux/filter.h
282pub const fn BPF_STMT(code: u16, k: u32) -> sock_filter {
283    sock_filter {
284        code,
285        jt: 0,
286        jf: 0,
287        k,
288    }
289}
290
291/// A BPF jump instruction.
292///
293/// # Arguments
294///
295/// * `code` is the operation code.
296/// * `k` is the value operated on for comparisons.
297/// * `jt` is the relative offset to jump to if the comparison is true.
298/// * `jf` is the relative offset to jump to if the comparison is false.
299///
300/// # Example
301///
302/// ```no_compile
303/// // Jump to the next instruction if the loaded value is equal to 42.
304/// BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 42, 1, 0);
305/// ```
306pub const fn BPF_JUMP(code: u16, k: u32, jt: u8, jf: u8) -> sock_filter {
307    sock_filter { code, jt, jf, k }
308}
309
310/// Loads the syscall number into `seccomp_data.nr`.
311pub const LOAD_SYSCALL_NR: sock_filter = BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_NR);
312
313/// Returns from the seccomp filter, allowing the syscall to pass through.
314#[allow(unused)]
315pub const ALLOW: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_ALLOW);
316
317/// Returns from the seccomp filter, instructing the kernel to kill the calling
318/// thread with `SIGSYS` before executing the syscall.
319#[allow(unused)]
320pub const DENY: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_THREAD);
321
322/// Returns from the seccomp filter, causing a `SIGSYS` to be sent to the calling
323/// thread skipping over the syscall without executing it. Unlike [`DENY`], this
324/// signal can be caught.
325#[allow(unused)]
326pub const TRAP: sock_filter = BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_TRAP);
327
328/// Returns from the seccomp filter, causing `PTRACE_EVENT_SECCOMP` to be
329/// generated for this syscall (if `PTRACE_O_TRACESECCOMP` is enabled). If no
330/// tracer is present, the syscall will not be executed and returns a `ENOSYS`
331/// instead.
332///
333/// `data` is made available to the tracer via `PTRACE_GETEVENTMSG`.
334#[allow(unused)]
335pub fn TRACE(data: u16) -> sock_filter {
336    BPF_STMT(
337        BPF_RET + BPF_K,
338        libc::SECCOMP_RET_TRACE | (data as u32 & libc::SECCOMP_RET_DATA),
339    )
340}
341
342/// Returns from the seccomp filter, returning the given error instead of
343/// executing the syscall.
344#[allow(unused)]
345pub fn ERRNO(err: Errno) -> sock_filter {
346    BPF_STMT(
347        BPF_RET + BPF_K,
348        libc::SECCOMP_RET_ERRNO | (err.into_raw() as u32 & libc::SECCOMP_RET_DATA),
349    )
350}
351
352macro_rules! instruction {
353    (
354        $(
355            $(#[$attrs:meta])*
356            $vis:vis fn $name:ident($($args:tt)*) {
357                $($instruction:expr;)*
358            }
359        )*
360    ) => {
361        $(
362            $vis fn $name($($args)*) -> impl ByteCode {
363                move |filter: &mut Filter| {
364                    $(
365                        $instruction.into_bpf(filter);
366                    )*
367                }
368            }
369        )*
370    };
371}
372
373instruction! {
374    /// Checks that architecture matches our target architecture. If it does not
375    /// match, kills the current process. This should be the first step for every
376    /// seccomp filter to ensure we're working with the syscall table we're
377    /// expecting. Each architecture has a slightly different syscall table and
378    /// we need to make sure the syscall numbers we're using are the right ones
379    /// for the architecture.
380    pub fn VALIDATE_ARCH(target_arch: u32) {
381        // Load `seccomp_data.arch`
382        BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_ARCH);
383        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, target_arch, 1, 0);
384        BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_PROCESS);
385    }
386
387    /// Like [`VALIDATE_ARCH`], except that a syscall of `alternate_arch` takes
388    /// `action` (which should be a `BPF_RET`) instead of killing the process.
389    /// A syscall of `target_arch` continues with the next instruction, and any
390    /// other architecture still kills the process.
391    pub fn VALIDATE_ARCH_OR_ALTERNATE(target_arch: u32, alternate_arch: u32, action: sock_filter) {
392        // Load `seccomp_data.arch`
393        BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_ARCH);
394        // if (arch == target_arch) goto CONTINUE;
395        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, target_arch, 3, 0);
396        // if (arch != alternate_arch) goto KILL;
397        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, alternate_arch, 0, 1);
398        action;
399        // KILL:
400        BPF_STMT(BPF_RET + BPF_K, libc::SECCOMP_RET_KILL_PROCESS);
401        // CONTINUE: the next instruction.
402    }
403
404    pub fn LOAD_SYSCALL_IP() {
405        BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_IP_LO);
406        // M[0] = lo
407        BPF_STMT(BPF_ST, 0);
408        BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_OFFSET_IP_HI);
409        // M[1] = hi
410        BPF_STMT(BPF_ST, 1);
411    }
412
413    /// Checks if `seccomp_data.nr` matches the given syscall. If so, then jumps
414    /// to `action`.
415    ///
416    /// # Example
417    /// ```no_compile
418    /// SYSCALL(Sysno::socket, DENY);
419    /// ```
420    pub fn SYSCALL(nr: Sysno, action: sock_filter) {
421        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr as i32 as u32, 0, 1);
422        action;
423    }
424
425    fn IP_RANGE64(blo: u32, bhi: u32, elo: u32, ehi: u32, action: sock_filter) {
426        // Most of the complexity below is caused by seccomp-bpf only being able
427        // to operate on `u32` values. We also can't reuse `JGE64` and `JLE64`
428        // because the jump offsets would be incorrect.
429        //
430        // On entry the accumulator holds `ip.hi` (see `LOAD_SYSCALL_IP`), and
431        // M[0] and M[1] hold `ip.lo` and `ip.hi`. Every exit restores
432        // `ip.hi` to the accumulator for the next rule. The instructions are
433        // numbered in the comments; a jump offset `n` from instruction `i`
434        // lands on `i + 1 + n`.
435
436        // STEP1: if (ip < begin) goto NOMATCH;
437
438        // 0: if (ip.hi > begin.hi) goto STEP2;
439        BPF_JUMP(BPF_JMP + BPF_JGT + BPF_K, bhi, 4 /* goto STEP2 */, 0);
440        // 1: if (ip.hi != begin.hi) goto NOMATCH; (ip.hi < begin.hi)
441        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, bhi, 0, 9 /* goto NOMATCH */);
442        // 2: Load M[0] to operate on the low bits of the IP.
443        BPF_STMT(BPF_LD + BPF_MEM, 0);
444        // 3: if (ip.lo < begin.lo) goto NOMATCH;
445        BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, blo, 0, 7 /* goto NOMATCH */);
446        // 4: Load M[1] because STEP2 expects the high bits of the IP.
447        BPF_STMT(BPF_LD + BPF_MEM, 1);
448
449        // STEP2: if (ip >= end) goto NOMATCH;
450
451        // 5: if (ip.hi > end.hi) goto NOMATCH;
452        BPF_JUMP(BPF_JMP + BPF_JGT + BPF_K, ehi, 5 /* goto NOMATCH */, 0);
453        // 6: if (ip.hi != end.hi) goto MATCH; (ip.hi < end.hi)
454        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ehi, 0, 3 /* goto MATCH */);
455        // 7: Load M[0]: the high halves are equal, so compare the low halves.
456        BPF_STMT(BPF_LD + BPF_MEM, 0);
457        // 8: if (ip.lo >= end.lo) goto NOMATCH;
458        BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, elo, 2 /* goto NOMATCH */, 0);
459        // 9: Load M[1] again after we loaded M[0].
460        BPF_STMT(BPF_LD + BPF_MEM, 1);
461
462        // 10: MATCH: Take the action.
463        action;
464
465        // 11: NOMATCH: Load M[1], the high bits of the IP, for the next rule.
466        BPF_STMT(BPF_LD + BPF_MEM, 1);
467    }
468}
469
470/// Checks if the instruction pointer equals `ip`. If so, executes `action`.
471/// Otherwise, falls through with the high 32 bits of the instruction pointer
472/// in the accumulator again.
473///
474/// Precondition: The instruction pointer must be loaded with [`LOAD_SYSCALL_IP`]
475/// first (so the accumulator holds its high 32 bits).
476pub fn IP_EQ(ip: u64, action: sock_filter) -> impl ByteCode {
477    IP_EQ64(ip as u32, (ip >> 32) as u32, action)
478}
479
480instruction! {
481    fn IP_EQ64(lo: u32, hi: u32, action: sock_filter) {
482        // if (arg.hi != hi) goto NOMATCH;
483        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, hi, 0, 3 /* goto NOMATCH */);
484        // Load M[0] to operate on the low bits of the IP.
485        BPF_STMT(BPF_LD + BPF_MEM, 0);
486        // if (arg.lo != lo) goto NOMATCH;
487        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, lo, 0, 1 /* goto NOMATCH */);
488        // MATCH: Take the action.
489        action;
490        // NOMATCH: Load M[1], the high bits of the IP, for the next rule.
491        BPF_STMT(BPF_LD + BPF_MEM, 1);
492    }
493}
494
495/// Checks if the instruction pointer is in the half-open interval
496/// `[begin, end)`, that is `begin <= ip && ip < end`, comparing all 64 bits.
497/// If so, executes `action`. Otherwise, falls through with the high 32 bits of
498/// the instruction pointer in the accumulator again.
499///
500/// Note that if `ip == end`, this will not match: the interval is open at the
501/// end, so `IP_RANGE(a, a + 1, ..)` matches only `a`.
502///
503/// Precondition: The instruction pointer must be loaded with [`LOAD_SYSCALL_IP`]
504/// first.
505pub fn IP_RANGE(begin: u64, end: u64, action: sock_filter) -> impl ByteCode {
506    let begin_lo = begin as u32;
507    let begin_hi = (begin >> 32) as u32;
508    let end_lo = end as u32;
509    let end_hi = (end >> 32) as u32;
510
511    IP_RANGE64(begin_lo, begin_hi, end_lo, end_hi, action)
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517
518    #[test]
519    fn smoke() {
520        let filter = seccomp_bpf![
521            VALIDATE_ARCH(AUDIT_ARCH_X86_64),
522            LOAD_SYSCALL_NR,
523            SYSCALL(Sysno::openat, DENY),
524            SYSCALL(Sysno::close, DENY),
525            SYSCALL(Sysno::write, DENY),
526            SYSCALL(Sysno::read, DENY),
527            ALLOW,
528        ];
529
530        assert_eq!(filter.len(), 13);
531    }
532
533    const RET_ALLOW: u32 = libc::SECCOMP_RET_ALLOW;
534    const RET_MATCH: u32 = libc::SECCOMP_RET_TRACE | 1;
535
536    /// Runs a seccomp-BPF program in user space on
537    /// `seccomp_data { nr, arch, instruction_pointer }` and returns its
538    /// verdict. Only the instructions this module emits are modelled; anything
539    /// else, and running off the end of the program, panics.
540    fn run(filter: &Filter, nr: u32, arch: u32, ip: u64) -> u32 {
541        const LD_ABS: u16 = BPF_LD + BPF_W + BPF_ABS;
542        const LD_MEM: u16 = BPF_LD + BPF_MEM;
543        const JEQ: u16 = BPF_JMP + BPF_JEQ + BPF_K;
544        const JGT: u16 = BPF_JMP + BPF_JGT + BPF_K;
545        const JGE: u16 = BPF_JMP + BPF_JGE + BPF_K;
546        const RET: u16 = BPF_RET + BPF_K;
547        let (mut acc, mut mem, mut pc) = (0u32, [0u32; 16], 0usize);
548        loop {
549            let insn = filter
550                .filter
551                .get(pc)
552                .unwrap_or_else(|| panic!("fell off the program at {pc}"));
553            pc += 1;
554            let jump = |taken: bool| usize::from(if taken { insn.jt } else { insn.jf });
555            match insn.code {
556                LD_ABS => {
557                    acc = match insn.k {
558                        SECCOMP_DATA_OFFSET_NR => nr,
559                        SECCOMP_DATA_OFFSET_ARCH => arch,
560                        SECCOMP_DATA_OFFSET_IP_LO => ip as u32,
561                        SECCOMP_DATA_OFFSET_IP_HI => (ip >> 32) as u32,
562                        k => panic!("load of unmodelled seccomp_data offset {k}"),
563                    }
564                }
565                BPF_ST => mem[insn.k as usize] = acc,
566                LD_MEM => acc = mem[insn.k as usize],
567                JEQ => pc += jump(acc == insn.k),
568                JGT => pc += jump(acc > insn.k),
569                JGE => pc += jump(acc >= insn.k),
570                RET => return insn.k,
571                code => panic!("unmodelled opcode {code:#x} at {}", pc - 1),
572            }
573        }
574    }
575
576    /// `LOAD_SYSCALL_IP; IP_RANGE(begin, end, RET_MATCH); ALLOW`.
577    fn range_filter(begin: u64, end: u64) -> Filter {
578        seccomp_bpf![
579            LOAD_SYSCALL_IP(),
580            IP_RANGE(begin, end, BPF_STMT(BPF_RET + BPF_K, RET_MATCH)),
581            ALLOW,
582        ]
583    }
584
585    fn matches(filter: &Filter, ip: u64) -> bool {
586        match run(filter, 0, AUDIT_ARCH_X86_64, ip) {
587            RET_MATCH => true,
588            RET_ALLOW => false,
589            other => panic!("unexpected verdict {other:#x} for ip {ip:#x}"),
590        }
591    }
592
593    /// The probe points named for this defect, for one range.
594    fn probes(begin: u64, end: u64) -> Vec<u64> {
595        let mut ips = vec![
596            begin.wrapping_sub(1),
597            begin,
598            end.wrapping_sub(1),
599            end,
600            end.wrapping_add(1),
601            0x7fff_ffff,
602            0xffff_ffff,
603            0x1_0000_0000u64.wrapping_add(begin),
604        ];
605        // The first and last addresses sharing the high half of each bound.
606        for bound in [begin, end] {
607            ips.push(bound & !0xffff_ffff);
608            ips.push(bound | 0xffff_ffff);
609        }
610        ips
611    }
612
613    /// `IP_RANGE(begin, end)` matches exactly `begin <= ip < end`, for ranges
614    /// inside one 4 GiB half and ranges crossing one or more 4 GiB
615    /// boundaries. The first row is the ptrace backend's untraced window.
616    #[test]
617    fn ip_range_matches_exactly_the_half_open_interval() {
618        let ranges: &[(u64, u64)] = &[
619            // Inside the low 4 GiB, the end's high half is zero.
620            (0x7100_0002, 0x7100_0003),
621            (0x1000, 0x2000),
622            (0x7fff_f000, 0x8000_1000),
623            (0xffff_f000, 0xffff_ffff),
624            // Inside a higher 4 GiB half.
625            (0x5_7100_0002, 0x5_7100_0003),
626            (0x7fff_0000_0000, 0x7fff_ffff_ffff),
627            // Crossing one 4 GiB boundary.
628            (0xffff_f000, 0x1_0000_1000),
629            (0x7100_0002, 0x1_7100_0003),
630            (0x1_ffff_ffff, 0x2_0000_0001),
631            // Crossing several.
632            (0x1000, 0x7_0000_0000),
633            (0x3_8000_0000, 0x7fff_ffff_f000),
634        ];
635        let mut wrong = Vec::new();
636        for &(begin, end) in ranges {
637            let filter = range_filter(begin, end);
638            for ip in probes(begin, end) {
639                let expected = begin <= ip && ip < end;
640                if matches(&filter, ip) != expected {
641                    wrong.push(format!(
642                        "[{begin:#x}, {end:#x}) ip {ip:#x}: expected match={expected}"
643                    ));
644                }
645            }
646        }
647        assert!(
648            wrong.is_empty(),
649            "{} wrong verdicts:\n{}",
650            wrong.len(),
651            wrong.join("\n")
652        );
653    }
654
655    /// The same property over every range whose bounds come from a grid of
656    /// high and low halves chosen at the comparison edges.
657    #[test]
658    fn ip_range_matches_the_half_open_interval_over_a_bound_grid() {
659        let his = [0u64, 1, 2, 0x7fff, 0xffff_fffe, 0xffff_ffff];
660        let los = [
661            0u64,
662            1,
663            0x7100_0002,
664            0x7100_0003,
665            0x7fff_ffff,
666            0x8000_0000,
667            0xffff_fffe,
668            0xffff_ffff,
669        ];
670        let points: Vec<u64> = his
671            .iter()
672            .flat_map(|hi| los.iter().map(move |lo| (hi << 32) | lo))
673            .collect();
674        let (mut checked, mut wrong) = (0usize, Vec::new());
675        for &begin in &points {
676            for &end in points.iter().filter(|&&end| end > begin) {
677                let filter = range_filter(begin, end);
678                for &ip in points.iter().chain(&probes(begin, end)) {
679                    checked += 1;
680                    let expected = begin <= ip && ip < end;
681                    if matches(&filter, ip) != expected {
682                        wrong.push((begin, end, ip, expected));
683                    }
684                }
685            }
686        }
687        assert!(checked > 60_000, "grid shrank to {checked} verdicts");
688        assert!(
689            wrong.is_empty(),
690            "{} of {checked} verdicts wrong; first: {:x?}",
691            wrong.len(),
692            &wrong[..wrong.len().min(8)]
693        );
694    }
695
696    /// Consecutive ranges and the syscall rules after them still see the
697    /// instruction pointer's high half after a range does not match.
698    #[test]
699    fn ip_ranges_chain_into_later_ranges_and_syscall_rules() {
700        use crate::seccomp::Action;
701        use crate::seccomp::FilterBuilder;
702
703        let filter = FilterBuilder::new()
704            .default_action(Action::Allow)
705            .target_arch(crate::seccomp::TargetArch::x86_64)
706            .ip_range(0x7100_0002, 0x7100_0003, Action::Trace(1))
707            .ip_range(0x1_0000_0000, 0x1_0000_1000, Action::Trace(2))
708            .syscall(Sysno::getppid, Action::Trace(3))
709            .build();
710        let nr = Sysno::getppid as u32;
711        let other = Sysno::getpid as u32;
712        let verdict = |nr, ip| run(&filter, nr, AUDIT_ARCH_X86_64, ip);
713        let trace = |data: u32| libc::SECCOMP_RET_TRACE | data;
714        for (nr, ip, expected) in [
715            (nr, 0x7100_0002, trace(1)),
716            (other, 0x7100_0002, trace(1)),
717            (nr, 0x7100_0003, trace(3)),
718            (nr, 0x7200_0002, trace(3)),
719            (other, 0x7200_0002, RET_ALLOW),
720            (nr, 0x1_0000_0000, trace(2)),
721            (nr, 0x1_0000_0fff, trace(2)),
722            (nr, 0x1_0000_1000, trace(3)),
723            (other, 0x1_0000_1000, RET_ALLOW),
724            (nr, 0x2_7100_0002, trace(3)),
725        ] {
726            assert_eq!(verdict(nr, ip), expected, "nr {nr} ip {ip:#x}");
727        }
728        assert_eq!(
729            run(&filter, nr, AUDIT_ARCH_X86_64 ^ 1, 0x7100_0002),
730            libc::SECCOMP_RET_KILL_PROCESS
731        );
732    }
733
734    /// The kernel agrees with `run` and with `begin <= ip < end`: a child
735    /// maps a `syscall; ret` stub so that the syscall's return address is
736    /// exactly `ip`, installs `ip_range(begin, end, Errno(EXDEV))` and calls
737    /// `getppid` through the stub.
738    #[cfg(target_arch = "x86_64")]
739    #[test]
740    fn kernel_ip_range_verdicts_match_the_interpreter() {
741        use crate::seccomp::Action;
742        use crate::seccomp::FilterBuilder;
743
744        const MATCHED: i32 = 10;
745        const UNMATCHED: i32 = 11;
746        const MAP_FAILED: i32 = 12;
747        const OTHER: i32 = 13;
748
749        fn kernel_matches(filter: &Filter, ip: u64) -> bool {
750            let page = 0x1000u64;
751            let first = (ip - 2) & !(page - 1);
752            let len = ((ip + 1 + page - 1) & !(page - 1)) - first;
753            // SAFETY: the child only makes raw syscalls on memory it maps
754            // itself, then exits without returning to the test harness.
755            match unsafe { libc::fork() } {
756                0 => unsafe {
757                    let base = libc::mmap(
758                        first as *mut libc::c_void,
759                        len as usize,
760                        libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
761                        libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED_NOREPLACE,
762                        -1,
763                        0,
764                    );
765                    if base as u64 != first {
766                        libc::_exit(MAP_FAILED);
767                    }
768                    // syscall; ret
769                    let stub = [0x0f, 0x05, 0xc3u8];
770                    std::ptr::copy_nonoverlapping(stub.as_ptr(), (ip - 2) as *mut u8, 3);
771                    if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
772                        || filter.load().is_err()
773                    {
774                        libc::_exit(OTHER);
775                    }
776                    let ret: i64;
777                    std::arch::asm!(
778                        "call {stub}",
779                        stub = in(reg) ip - 2,
780                        inlateout("rax") libc::SYS_getppid => ret,
781                        out("rcx") _,
782                        out("r11") _,
783                    );
784                    libc::_exit(if ret == -(libc::EXDEV as i64) {
785                        MATCHED
786                    } else if ret > 0 {
787                        UNMATCHED
788                    } else {
789                        OTHER
790                    });
791                },
792                -1 => panic!("fork failed: {}", std::io::Error::last_os_error()),
793                pid => {
794                    let mut status = 0;
795                    assert_eq!(unsafe { libc::waitpid(pid, &mut status, 0) }, pid);
796                    assert!(libc::WIFEXITED(status), "child status {status:#x}");
797                    match libc::WEXITSTATUS(status) {
798                        MATCHED => true,
799                        UNMATCHED => false,
800                        MAP_FAILED => panic!("could not map a stub page at {first:#x}"),
801                        code => panic!("child for ip {ip:#x} failed with {code}"),
802                    }
803                }
804            }
805        }
806
807        let (mut unfaithful, mut wrong) = (Vec::new(), Vec::new());
808        for (begin, end, ips) in [
809            (
810                0x7100_0002u64,
811                0x7100_0003u64,
812                &[
813                    0x7100_0001u64,
814                    0x7100_0002,
815                    0x7100_0003,
816                    0x7200_0002,
817                    0x7fff_ffff,
818                    0xffff_ffff,
819                    0x1_7100_0002,
820                ][..],
821            ),
822            (
823                0xffff_f000,
824                0x1_0000_1000,
825                &[0xffff_efff, 0xffff_f000, 0x1_0000_0fff, 0x1_0000_1000][..],
826            ),
827        ] {
828            let built = FilterBuilder::new()
829                .default_action(Action::Allow)
830                .ip_range(begin, end, Action::Errno(Errno::EXDEV))
831                .build();
832            for &ip in ips {
833                let expected = begin <= ip && ip < end;
834                let interpreted = run(&built, Sysno::getppid as u32, AUDIT_ARCH_X86_64, ip)
835                    == (libc::SECCOMP_RET_ERRNO | libc::EXDEV as u32);
836                let kernel = kernel_matches(&built, ip);
837                let row = format!("[{begin:#x}, {end:#x}) ip {ip:#x}: kernel match={kernel}");
838                if kernel != interpreted {
839                    unfaithful.push(format!("{row}, interpreter match={interpreted}"));
840                }
841                if kernel != expected {
842                    wrong.push(format!("{row}, interval match={expected}"));
843                }
844            }
845        }
846        assert!(
847            unfaithful.is_empty(),
848            "interpreter disagrees with the kernel:\n{}",
849            unfaithful.join("\n")
850        );
851        assert!(
852            wrong.is_empty(),
853            "kernel verdicts outside the interval:\n{}",
854            wrong.join("\n")
855        );
856    }
857}