letmein_seccomp/
lib.rs

1// -*- coding: utf-8 -*-
2//
3// Copyright (C) 2024 - 2026 Michael Büsch <m@bues.ch>
4//
5// Licensed under the Apache License version 2.0
6// or the MIT license, at your option.
7// SPDX-License-Identifier: Apache-2.0 OR MIT
8
9#![forbid(unsafe_code)]
10
11#[cfg(not(any(target_os = "linux", target_os = "android")))]
12std::compile_error!("letmeind server and letmein-seccomp do not support non-Linux platforms.");
13
14use anyhow::{self as ah, Context as _};
15use seccompiler::{apply_filter_all_threads, BpfProgram};
16use std::env::consts::ARCH;
17
18#[cfg(has_seccomp_support)]
19const NULL: u64 = 0;
20#[cfg(has_seccomp_support)]
21const PTR: u8 = 0xFF;
22
23#[cfg(has_seccomp_support)]
24macro_rules! sys {
25    ($ident:ident) => {{
26        #[allow(clippy::useless_conversion)]
27        let id: i64 = libc::$ident.into();
28        id
29    }};
30}
31
32#[cfg(has_seccomp_support)]
33fn seccomp_cond(idx: u8, value: u64, bit_width: u8) -> ah::Result<seccompiler::SeccompCondition> {
34    use seccompiler::{SeccompCmpArgLen, SeccompCmpOp, SeccompCondition};
35
36    let bit_width = match bit_width {
37        PTR => {
38            #[cfg(target_pointer_width = "32")]
39            let bit_width = 32;
40
41            #[cfg(target_pointer_width = "64")]
42            let bit_width = 64;
43
44            bit_width
45        }
46        bit_width => bit_width,
47    };
48
49    let arglen = match bit_width {
50        32 => {
51            assert_eq!(value & 0xFFFF_FFFF_0000_0000, 0);
52            SeccompCmpArgLen::Dword
53        }
54        64 => SeccompCmpArgLen::Qword,
55        bit_width => panic!("seccomp_cond: Invalid bit_width: {bit_width}"),
56    };
57
58    Ok(SeccompCondition::new(idx, arglen, SeccompCmpOp::Eq, value)?)
59}
60
61#[cfg(has_seccomp_support)]
62macro_rules! args {
63    ($([ $arg:literal ] ($bit_width:expr) == $value:expr),*) => {
64        SeccompRule::new(
65            vec![
66                $(
67                    seccomp_cond($arg, ($value) as _, $bit_width)?,
68                )*
69            ]
70        )?
71    };
72}
73
74/// Returns `true` if seccomp is supported on this platform.
75pub fn seccomp_supported() -> bool {
76    cfg!(any(has_seccomp_support))
77}
78
79/// Abstract allow-list features that map to one or more syscalls each.
80#[derive(Clone, Copy, Debug)]
81pub enum Allow {
82    Mmap,
83    Mprotect,
84    GetUidGid,
85    ArchPrctl { op: Option<u32> },
86    Dup,
87    Pipe,
88    Listen,
89    UnixAccept,
90    UnixConnect,
91    TcpAccept,
92    TcpConnect,
93    Netlink,
94    SetSockOpt { level_optname: Option<(i32, i32)> },
95    Access,
96    Open,
97    Read,
98    Write,
99    Ioctl { op: Option<u32> },
100    Fcntl { op: Option<u32> },
101    Stat,
102    Recv,
103    Send,
104    Signal,
105    SigAction,
106    Futex,
107    SetTidAddress,
108    Rseq,
109    Clone,
110    Exec,
111    Wait,
112    GetRlimit,
113    Uname,
114    Pidfd,
115}
116
117/// Action to be performed, if a syscall is executed that is not in the allow-list.
118#[derive(Clone, Copy, Debug)]
119pub enum Action {
120    /// Kill the process.
121    Kill,
122    /// Only log the event and keep running. See the kernel logs.
123    Log,
124}
125
126/// A compiled seccomp filter program.
127pub struct Filter(BpfProgram);
128
129impl Filter {
130    pub fn compile(allow: &[Allow], deny_action: Action) -> ah::Result<Self> {
131        Self::compile_for_arch(allow, deny_action, ARCH)
132    }
133
134    #[cfg(has_seccomp_support)]
135    pub fn compile_for_arch(allow: &[Allow], deny_action: Action, arch: &str) -> ah::Result<Self> {
136        assert!(!allow.is_empty());
137
138        use seccompiler::{SeccompAction, SeccompFilter, SeccompRule};
139        use std::collections::BTreeMap;
140
141        type RulesMap = BTreeMap<i64, Vec<SeccompRule>>;
142
143        fn add_sys(map: &mut RulesMap, sys: i64) {
144            let _rules = map.entry(sys).or_default();
145        }
146
147        fn add_sys_args_match(map: &mut RulesMap, sys: i64, rule: SeccompRule) {
148            let rules = map.entry(sys).or_default();
149            rules.push(rule);
150        }
151
152        let mut map: RulesMap = [].into();
153
154        add_sys(&mut map, sys!(SYS_brk));
155        add_sys(&mut map, sys!(SYS_close));
156        #[cfg(target_os = "linux")]
157        add_sys(&mut map, sys!(SYS_close_range));
158        add_sys(&mut map, sys!(SYS_exit));
159        add_sys(&mut map, sys!(SYS_exit_group));
160        add_sys(&mut map, sys!(SYS_getpid));
161        add_sys(&mut map, sys!(SYS_getrandom));
162        add_sys(&mut map, sys!(SYS_gettid));
163        add_sys(&mut map, sys!(SYS_madvise));
164        add_sys(&mut map, sys!(SYS_munmap));
165        add_sys(&mut map, sys!(SYS_sched_getaffinity));
166        add_sys(&mut map, sys!(SYS_sigaltstack));
167        add_sys(&mut map, sys!(SYS_nanosleep));
168        add_sys(&mut map, sys!(SYS_clock_gettime));
169        add_sys(&mut map, sys!(SYS_clock_getres));
170        add_sys(&mut map, sys!(SYS_clock_nanosleep));
171        add_sys(&mut map, sys!(SYS_gettimeofday));
172
173        fn add_read_write_rules(map: &mut RulesMap) {
174            add_sys(map, sys!(SYS_epoll_create1));
175            add_sys(map, sys!(SYS_epoll_ctl));
176            add_sys(map, sys!(SYS_epoll_pwait));
177            #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
178            add_sys(map, sys!(SYS_epoll_pwait2));
179            #[cfg(target_arch = "x86_64")]
180            add_sys(map, sys!(SYS_epoll_wait));
181            add_sys(map, sys!(SYS_lseek));
182            #[cfg(target_arch = "x86_64")]
183            add_sys(map, sys!(SYS_poll));
184            add_sys(map, sys!(SYS_ppoll));
185            add_sys(map, sys!(SYS_pselect6));
186        }
187
188        for allow in allow {
189            match *allow {
190                Allow::Mmap => {
191                    add_sys(&mut map, sys!(SYS_mmap));
192                    add_sys(&mut map, sys!(SYS_mremap));
193                    add_sys(&mut map, sys!(SYS_munmap));
194                }
195                Allow::Mprotect => {
196                    add_sys(&mut map, sys!(SYS_mprotect));
197                }
198                Allow::GetUidGid => {
199                    add_sys(&mut map, sys!(SYS_getuid));
200                    add_sys(&mut map, sys!(SYS_geteuid));
201                    add_sys(&mut map, sys!(SYS_getgid));
202                    add_sys(&mut map, sys!(SYS_getegid));
203                }
204                Allow::ArchPrctl { op: _ } => {
205                    //TODO restrict to op
206                    #[cfg(target_arch = "x86_64")]
207                    add_sys(&mut map, sys!(SYS_arch_prctl));
208                }
209                Allow::Dup => {
210                    add_sys(&mut map, sys!(SYS_dup));
211                    #[cfg(target_arch = "x86_64")]
212                    add_sys(&mut map, sys!(SYS_dup2));
213                    add_sys(&mut map, sys!(SYS_dup3));
214                }
215                Allow::Pipe => {
216                    #[cfg(target_arch = "x86_64")]
217                    add_sys(&mut map, sys!(SYS_pipe));
218                    add_sys(&mut map, sys!(SYS_pipe2));
219                }
220                Allow::Listen => {
221                    add_sys(&mut map, sys!(SYS_bind));
222                    add_sys(&mut map, sys!(SYS_listen));
223                }
224                Allow::UnixAccept => {
225                    add_sys(&mut map, sys!(SYS_accept4));
226                    add_sys_args_match(&mut map, sys!(SYS_socket), args!([0](32) == libc::AF_UNIX));
227                    add_sys(&mut map, sys!(SYS_getsockopt));
228                    add_sys(&mut map, sys!(SYS_getpeername));
229                }
230                Allow::UnixConnect => {
231                    add_sys(&mut map, sys!(SYS_connect));
232                    add_sys_args_match(&mut map, sys!(SYS_socket), args!([0](32) == libc::AF_UNIX));
233                    add_sys(&mut map, sys!(SYS_getsockopt));
234                    add_sys(&mut map, sys!(SYS_getpeername));
235                }
236                Allow::TcpAccept => {
237                    add_sys(&mut map, sys!(SYS_accept4));
238                    add_sys_args_match(&mut map, sys!(SYS_socket), args!([0](32) == libc::AF_INET));
239                    add_sys_args_match(
240                        &mut map,
241                        sys!(SYS_socket),
242                        args!([0](32) == libc::AF_INET6),
243                    );
244                    add_sys(&mut map, sys!(SYS_getsockopt));
245                    add_sys(&mut map, sys!(SYS_getpeername));
246                }
247                Allow::TcpConnect => {
248                    add_sys(&mut map, sys!(SYS_connect));
249                    add_sys_args_match(&mut map, sys!(SYS_socket), args!([0](32) == libc::AF_INET));
250                    add_sys_args_match(
251                        &mut map,
252                        sys!(SYS_socket),
253                        args!([0](32) == libc::AF_INET6),
254                    );
255                    add_sys(&mut map, sys!(SYS_getsockopt));
256                    add_sys(&mut map, sys!(SYS_getpeername));
257                }
258                Allow::Netlink => {
259                    add_sys(&mut map, sys!(SYS_connect));
260                    add_sys_args_match(
261                        &mut map,
262                        sys!(SYS_socket),
263                        args!([0](32) == libc::AF_NETLINK),
264                    );
265                    add_sys(&mut map, sys!(SYS_getsockopt));
266                }
267                Allow::SetSockOpt { level_optname } => {
268                    if let Some((level, optname)) = level_optname {
269                        add_sys_args_match(
270                            &mut map,
271                            sys!(SYS_setsockopt),
272                            args!([1](32) == level, [2](32) == optname),
273                        );
274                    } else {
275                        add_sys(&mut map, sys!(SYS_setsockopt));
276                    }
277                }
278                Allow::Access => {
279                    #[cfg(target_arch = "x86_64")]
280                    add_sys(&mut map, sys!(SYS_access));
281                    add_sys(&mut map, sys!(SYS_faccessat));
282                    #[cfg(target_os = "linux")]
283                    add_sys(&mut map, sys!(SYS_faccessat2));
284                }
285                Allow::Open => {
286                    //TODO: This should be restricted
287                    #[cfg(target_arch = "x86_64")]
288                    add_sys(&mut map, sys!(SYS_open));
289                    add_sys(&mut map, sys!(SYS_openat));
290                }
291                Allow::Read => {
292                    add_sys(&mut map, sys!(SYS_pread64));
293                    add_sys(&mut map, sys!(SYS_preadv2));
294                    add_sys(&mut map, sys!(SYS_read));
295                    add_sys(&mut map, sys!(SYS_readv));
296                    add_read_write_rules(&mut map);
297                }
298                Allow::Write => {
299                    add_sys(&mut map, sys!(SYS_fdatasync));
300                    add_sys(&mut map, sys!(SYS_fsync));
301                    add_sys(&mut map, sys!(SYS_pwrite64));
302                    add_sys(&mut map, sys!(SYS_pwritev2));
303                    add_sys(&mut map, sys!(SYS_write));
304                    add_sys(&mut map, sys!(SYS_writev));
305                    add_read_write_rules(&mut map);
306                }
307                Allow::Ioctl { op: _ } => {
308                    //TODO restrict to op
309                    add_sys(&mut map, sys!(SYS_ioctl));
310                }
311                Allow::Fcntl { op } => match op {
312                    Some(op) => {
313                        add_sys_args_match(&mut map, sys!(SYS_fcntl), args!([1](32) == op));
314                    }
315                    None => {
316                        add_sys(&mut map, sys!(SYS_fcntl));
317                    }
318                },
319                Allow::Stat => {
320                    add_sys(&mut map, sys!(SYS_fstat));
321                    add_sys(&mut map, sys!(SYS_statx));
322                    add_sys(&mut map, sys!(SYS_newfstatat));
323                }
324                Allow::Recv => {
325                    add_sys(&mut map, sys!(SYS_recvfrom));
326                    add_sys(&mut map, sys!(SYS_recvmsg));
327                    add_sys(&mut map, sys!(SYS_recvmmsg));
328                }
329                Allow::Send => {
330                    add_sys(&mut map, sys!(SYS_sendto));
331                    add_sys(&mut map, sys!(SYS_sendmsg));
332                    add_sys(&mut map, sys!(SYS_sendmmsg));
333                }
334                Allow::Signal => {
335                    add_sys(&mut map, sys!(SYS_rt_sigreturn));
336                    add_sys(&mut map, sys!(SYS_rt_sigprocmask));
337                }
338                Allow::SigAction => {
339                    add_sys(&mut map, sys!(SYS_rt_sigaction));
340                }
341                Allow::Futex => {
342                    add_sys(&mut map, sys!(SYS_futex));
343                    add_sys(&mut map, sys!(SYS_get_robust_list));
344                    add_sys(&mut map, sys!(SYS_set_robust_list));
345                    #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
346                    add_sys(&mut map, sys!(SYS_futex_waitv));
347                    //add_sys(&mut map, sys!(SYS_futex_wake));
348                    //add_sys(&mut map, sys!(SYS_futex_wait));
349                    //add_sys(&mut map, sys!(SYS_futex_requeue));
350                }
351                Allow::SetTidAddress => {
352                    add_sys(&mut map, sys!(SYS_set_tid_address));
353                }
354                Allow::Rseq => {
355                    #[cfg(target_os = "linux")]
356                    add_sys(&mut map, sys!(SYS_rseq));
357                }
358                Allow::Clone => {
359                    #[cfg(target_os = "linux")]
360                    add_sys(&mut map, sys!(SYS_clone3));
361                    #[cfg(target_arch = "aarch64")]
362                    add_sys(&mut map, sys!(SYS_clone));
363                }
364                Allow::Exec => {
365                    //TODO restrict the path
366                    add_sys(&mut map, sys!(SYS_execve));
367                }
368                Allow::Wait => {
369                    add_sys(&mut map, sys!(SYS_wait4));
370                }
371                Allow::GetRlimit => {
372                    add_sys_args_match(
373                        &mut map,
374                        sys!(SYS_prlimit64),
375                        args!([0](32) == 0, [2](PTR) == NULL),
376                    );
377                }
378                Allow::Uname => {
379                    add_sys(&mut map, sys!(SYS_uname));
380                }
381                Allow::Pidfd => {
382                    add_sys(&mut map, sys!(SYS_pidfd_open));
383                }
384            }
385        }
386
387        let filter = SeccompFilter::new(
388            map,
389            match deny_action {
390                Action::Kill => SeccompAction::KillProcess,
391                Action::Log => SeccompAction::Log,
392            },
393            SeccompAction::Allow,
394            arch.try_into().context("Unsupported CPU ARCH")?,
395        )
396        .context("Create seccomp filter")?;
397
398        let filter: BpfProgram = filter.try_into().context("Seccomp to BPF")?;
399
400        Ok(Self(filter))
401    }
402
403    #[cfg(not(has_seccomp_support))]
404    pub fn compile_for_arch(
405        _allow: &[Allow],
406        _deny_action: Action,
407        _arch: &str,
408    ) -> ah::Result<Self> {
409        Err(ah::format_err!("seccomp is not supported on this platform"))
410    }
411
412    pub fn install(&self) -> ah::Result<()> {
413        apply_filter_all_threads(&self.0).context("Apply seccomp filter")
414    }
415}
416
417// vim: ts=4 sw=4 expandtab