Skip to main content

statime_seccomp/
lib.rs

1//! Our seccomp bindings consist of two parts:
2//! - A high-level interface written in C which ensures that the platform compiler will insert all the correct sycall numbers.
3//! - A lightweight Rust wrapper around this interface.
4//!
5//! Example:
6//! ```
7//! statime_seccomp::KillProcess.enable().expect("could not enable seccomp filter");
8//! ```
9//!
10use std::io::{Error, Result};
11
12#[cfg(not(target_os = "linux"))]
13compile_error!("seccomp filtering is only for Linux targets");
14
15#[link(name = "seccomp")]
16unsafe extern "C" {
17    static statime_kill_thread: u32;
18    static statime_kill_process: u32;
19    static statime_trap: u32;
20    static statime_err: u32;
21    static statime_log: u32;
22
23    fn statime_sandbox(def_action: u32) -> std::ffi::c_int;
24}
25
26/// What action should be taken for system calls that are not explicitly allowed. The options are:
27pub enum Seccomp {
28    /// Kill the calling thread
29    KillThread,
30    /// Kill the entire process
31    KillProcess,
32    /// Trigger a trap (that can be caught)
33    Trap,
34    /// Make the syscall result in an EPERM error
35    Fail,
36    /// Allow the call but log it
37    Log,
38}
39
40pub use Seccomp::*;
41
42impl Seccomp {
43    fn into_u32(self) -> u32 {
44        //SAFETY: The referenced extern static's are declared as "const" and never
45        //have their address taken, so data races and aliasing does not occur.
46        unsafe {
47            match self {
48                Seccomp::KillThread => statime_kill_thread,
49                Seccomp::KillProcess => statime_kill_process,
50                Seccomp::Trap => statime_trap,
51                Seccomp::Fail => statime_err,
52                Seccomp::Log => statime_log,
53            }
54        }
55    }
56
57    /// Load the statime seccomp filter into the kernel.
58    ///
59    /// # Panics
60    ///
61    /// Panics if any of the libseccomp functions do not adhere to their specification.
62    ///
63    /// # Errors
64    ///
65    /// Will contain the the first error that is returned from a libseccomp function;
66    /// this is fatal and means the seccomp filter has not been loaded.
67    pub fn enable(self) -> Result<()> {
68        let scmp_action = self.into_u32();
69
70        //SAFETY: the statime_sandbox function is always safe to call.
71        match unsafe { statime_sandbox(scmp_action) } {
72            0 => Ok(()),
73
74            x if x < 0 => Err(Error::from_raw_os_error(-x)),
75
76            _ => panic!("libseccomp returned a positive error"),
77        }
78    }
79}
80
81#[cfg(test)]
82mod test {
83    use super::*;
84
85    fn not_allowed() -> std::io::Result<()> {
86        std::env::set_current_dir(".").map(|_| ())
87    }
88
89    fn allowed() -> std::io::Result<()> {
90        std::net::UdpSocket::bind("0.0.0.0:0").map(|_| ())
91    }
92
93    #[test]
94    fn cannot_downgrade() {
95        Fail.enable().unwrap();
96        assert!(Log.enable().is_err());
97    }
98
99    #[test]
100    fn allowlisted_functions_are_fine() {
101        Fail.enable().unwrap();
102        assert!(allowed().is_ok());
103    }
104
105    #[test]
106    fn blocklisted_functions_are_stopped() {
107        Fail.enable().unwrap();
108        assert!(not_allowed().is_err());
109    }
110
111    #[test]
112    fn blocklisted_functions_are_fine_when_logged() {
113        Log.enable().unwrap();
114        assert!(not_allowed().is_ok());
115    }
116
117    #[test]
118    fn thread_is_stopped() {
119        // Rust doesn't expect a thread to terminate suddenly; this causes a panic in the std lib;
120        // catch that panic to demonstrate that the thread is stopped but the entire process isn't.
121        let result = std::panic::catch_unwind(move || {
122            let handle = std::thread::spawn(|| {
123                KillThread.enable().unwrap();
124                let _ = not_allowed();
125            });
126            let _ = handle.join();
127        });
128
129        assert!(result.is_err());
130    }
131
132    #[allow(clippy::undocumented_unsafe_blocks)]
133    #[test]
134    fn process_is_stopped() {
135        for catchable in [true, false] {
136            unsafe {
137                //NOTE on async-signal safety: yes, there will be threads running, but:
138                // - not_allowed() is async-signal-safe
139                // - see https://github.com/seccomp/libseccomp/pull/390 for seccomp
140                // - this is test code
141                match libc::fork() {
142                    0 => {
143                        // we are the child
144                        unsafe extern "C" fn exit42(_: std::ffi::c_int) {
145                            unsafe { libc::_exit(42) }
146                        }
147                        libc::signal(libc::SIGSYS, exit42 as *const () as libc::sighandler_t);
148
149                        if catchable { Trap } else { KillProcess }.enable().unwrap();
150                        let _ = not_allowed();
151
152                        unreachable!();
153                    }
154                    child_pid => {
155                        // we are the parent
156                        let mut status = 0;
157                        assert_eq!(libc::wait(&mut status), child_pid);
158
159                        if catchable {
160                            assert!(libc::WIFEXITED(status));
161                            assert_eq!(libc::WEXITSTATUS(status), 42);
162                        } else {
163                            assert!(libc::WIFSIGNALED(status));
164                            assert_eq!(libc::WTERMSIG(status), libc::SIGSYS);
165                        }
166                    }
167                };
168            }
169        }
170    }
171}