返回 CodeWhale
seccomp.rs
根目录 / crates / tui / src / sandbox / seccomp.rs
1 //! Linux seccomp (Secure Computing) filter layer (#2182).
2 //!
3 //! This module is dormant in v0.9.1: command execution does not install this
4 //! filter. Kernel support therefore does not mean that a command is sandboxed.
5 //!
6 //! Seccomp BPF (Berkeley Packet Filter) is a kernel facility that allows a
7 //! process to restrict the system calls it (and its descendants) can make.
8 //! Once wired, the implementation can complement a filesystem sandbox by
9 //! blocking entire *classes* of dangerous syscalls like `ptrace`, `mount`,
10 //! and `kexec_load`.
11 //!
12 //! # Architecture
13 //!
14 //! The filter is written as a raw BPF program (array of `sock_filter`
15 //! instructions) and loaded via `prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER)`.
16 //! This avoids any dependency on external crates like `libseccomp-sys` or
17 //! `seccompiler` — we use only the `libc` crate already in the dependency
18 //! tree.
19 //!
20 //! # Whitelisted syscalls
21 //!
22 //! The filter uses a whitelist approach: only syscalls that are known to be
23 //! safe for a development/shell workload are allowed. Everything else is
24 //! killed with `SECCOMP_RET_KILL_PROCESS`. The whitelist includes:
25 //!
26 //! - File I/O: read, write, open, openat, close, stat, fstat, lstat, newfstatat
27 //! - Directory: getdents, getdents64, getcwd, chdir
28 //! - Memory: mmap, mprotect, munmap, brk, mremap, madvise
29 //! - Process: clone, clone3, fork, vfork, execve, execveat, exit, exit_group
30 //! - IPC: pipe, pipe2, socket, socketpair, connect, bind, listen, accept, accept4
31 //! - Synchronization: futex, nanosleep, clock_nanosleep
32 //! - Signals: rt_sigaction, rt_sigprocmask, rt_sigreturn, kill, tkill, tgkill
33 //! - Resource: getrlimit, setrlimit, prlimit64, getrusage
34 //! - Time: clock_gettime, gettimeofday, time
35 //! - Misc: getpid, gettid, getuid, geteuid, getgid, getegid, uname, arch_prctl
36 //!
37 //! # Explicitly denied
38 //!
39 //! - ptrace (process hijacking)
40 //! - mount, umount2 (filesystem manipulation)
41 //! - kexec_load, kexec_file_load (kernel execution)
42 //! - init_module, finit_module, delete_module (kernel module loading)
43 //! - bpf (loading BPF programs — would bypass seccomp!)
44 //! - reboot
45 //! - swapon, swapoff
46 //! - pivot_root
47 //! - setuid, setgid, setreuid, setregid, setresuid, setresgid
48 //! - personality
49 //!
50 //! # Safety
51 //!
52 //! Once the seccomp filter is installed, it is **irreversible** — even
53 //! `prctl(PR_SET_SECCOMP, ...)` is denied. This is by design.
54
55 /// Check if seccomp is available on this system.
56 ///
57 /// Returns true if `/proc/sys/kernel/seccomp/actions_avail` exists and
58 /// contains "kill_process", indicating the kernel supports seccomp BPF.
59 #[cfg(target_os = "linux")]
60 pub fn is_available() -> bool {
61 std::path::Path::new("/proc/sys/kernel/seccomp/actions_avail").exists()
62 }
63
64 #[cfg(not(target_os = "linux"))]
65 pub fn is_available() -> bool {
66 false
67 }
68
69 /// Detect if a failure was caused by seccomp denial.
70 ///
71 /// Seccomp kills the process with SIGSYS (or the thread with SECCOMP_RET_KILL_THREAD),
72 /// and the exit code is typically SIGSYS (31) or the process may be killed with
73 /// "Bad system call" on stderr.
74 ///
75 /// Additionally, seccomp violations may produce EPERM for filtered syscalls
76 /// if using SECCOMP_RET_ERRNO.
77 #[cfg(target_os = "linux")]
78 pub fn detect_denial(exit_code: i32, stderr: &str) -> bool {
79 // SIGSYS = 31
80 if exit_code == 31 {
81 return true;
82 }
83 // Check for seccomp denial patterns in stderr
84 stderr.contains("Bad system call")
85 || stderr.contains("bad system call")
86 || stderr.contains("SIGSYS")
87 || stderr.contains("seccomp")
88 || stderr.contains("invalid argument") && exit_code == 159
89 // 159 = 128 + 31 (died from SIGSYS with core dump disabled)
90 }
91
92 #[cfg(not(target_os = "linux"))]
93 pub fn detect_denial(_exit_code: i32, _stderr: &str) -> bool {
94 false
95 }
96
97 /// Apply the seccomp filter to the calling thread.
98 ///
99 /// This installs a BPF program that whitelists safe syscalls and kills the
100 /// process on any disallowed syscall.
101 ///
102 /// # Errors
103 ///
104 /// Returns an error if the prctl call fails (e.g., seccomp already enabled
105 /// or kernel too old).
106 #[cfg(target_os = "linux")]
107 pub fn apply_seccomp_filter() -> std::io::Result<()> {
108 // ── Build the BPF filter program ─────────────────────────────────────
109 //
110 // BPF for seccomp works as follows:
111 // 1. Load the architecture (4 bytes at offset 4 in seccomp_data)
112 // 2. Validate architecture matches AUDIT_ARCH_X86_64 (0xC000003E)
113 // 3. Load the syscall number (4 bytes at offset 0)
114 // 4. Compare against whitelist, return ALLOW on match
115 // 5. Return KILL on no match
116 //
117 // The filter uses a linear search over the whitelist. While not optimal,
118 // it's simple, auditable, and has no external dependencies. The BPF
119 // program is at most a few hundred instructions, which is well within
120 // the kernel's 4096-instruction limit.
121
122 #[repr(C)]
123 struct sock_filter {
124 code: u16,
125 jt: u8,
126 jf: u8,
127 k: u32,
128 }
129
130 const BPF_LD: u16 = 0x00;
131 const BPF_JMP: u16 = 0x05;
132 const BPF_RET: u16 = 0x06;
133
134 const BPF_W: u16 = 0x00;
135 const BPF_ABS: u16 = 0x20;
136
137 const BPF_JEQ: u16 = 0x10;
138 const BPF_JGE: u16 = 0x30;
139 const BPF_JA: u16 = 0x00;
140
141 const SECCOMP_RET_KILL_PROCESS: u32 = 0x8000_0000;
142 const SECCOMP_RET_ALLOW: u32 = 0x7FFF_0000;
143
144 // Audit arch for x86_64
145 const AUDIT_ARCH_X86_64: u32 = 0xC000_003E;
146
147 // Helper to build a BPF instruction compactly.
148 // Pattern from openai/codex codex-rs/codex-sandbox/src/linux/seccomp.rs; reimplemented.
149
150 // Whitelist of safe syscall numbers (x86_64).
151 // These are the syscalls most commonly used by shell commands, compilers,
152 // and developer tools. Any syscall NOT on this list causes immediate SIGSYS.
153 let allowed_syscalls: &[u32] = &[
154 0, // read
155 1, // write
156 2, // open
157 3, // close
158 4, // stat
159 5, // fstat
160 6, // lstat
161 7, // poll
162 8, // lseek
163 9, // mmap
164 10, // mprotect
165 11, // munmap
166 12, // brk
167 13, // rt_sigaction
168 14, // rt_sigprocmask
169 15, // rt_sigreturn
170 16, // ioctl
171 17, // pread64
172 18, // pwrite64
173 19, // readv
174 20, // writev
175 21, // access
176 22, // pipe
177 23, // select
178 24, // sched_yield
179 25, // mremap
180 27, // mincore
181 28, // madvise
182 29, // shmget
183 30, // shmat
184 32, // dup
185 33, // dup2
186 35, // nanosleep
187 39, // getpid
188 41, // socket
189 42, // connect
190 43, // accept
191 44, // sendto
192 45, // recvfrom
193 46, // sendmsg
194 47, // recvmsg
195 48, // shutdown
196 49, // bind
197 50, // listen
198 51, // getsockname
199 52, // getpeername
200 53, // socketpair
201 54, // setsockopt
202 55, // getsockopt
203 56, // clone
204 57, // fork
205 58, // vfork
206 59, // execve
207 60, // exit
208 61, // wait4
209 62, // kill
210 63, // uname
211 72, // fcntl
212 73, // flock
213 74, // fsync
214 75, // fdatasync
215 76, // truncate
216 77, // ftruncate
217 78, // getdents
218 79, // getcwd
219 80, // chdir
220 81, // fchdir
221 82, // rename
222 83, // mkdir
223 84, // rmdir
224 85, // creat
225 86, // link
226 87, // unlink
227 88, // symlink
228 89, // readlink
229 90, // chmod
230 91, // fchmod
231 92, // chown
232 93, // fchown
233 94, // lchown
234 95, // umask
235 96, // gettimeofday
236 97, // getrlimit
237 98, // getrusage
238 99, // sysinfo
239 100, // times
240 102, // getuid
241 104, // getgid
242 107, // geteuid
243 108, // getegid
244 110, // getppid
245 111, // getpgrp
246 112, // setsid
247 116, // syslog
248 131, // sigaltstack
249 137, // statfs
250 138, // fstatfs
251 157, // prctl
252 158, // arch_prctl
253 186, // gettid
254 201, // time
255 202, // futex
256 204, // sched_getaffinity
257 217, // getdents64
258 218, // set_tid_address
259 228, // clock_gettime
260 230, // clock_nanosleep
261 231, // exit_group
262 232, // epoll_wait
263 233, // epoll_ctl
264 234, // tgkill
265 235, // utimes
266 257, // openat
267 262, // newfstatat
268 273, // set_robust_list
269 281, // epoll_pwait
270 291, // epoll_create1
271 292, // dup3
272 293, // pipe2
273 302, // prlimit64
274 318, // getrandom
275 332, // statx
276 334, // rseq
277 435, // clone3
278 ];
279
280 // Build the BPF program.
281 let mut filter = vec![
282 // Instruction 0: load architecture from seccomp_data.arch
283 sock_filter {
284 code: BPF_LD | BPF_W | BPF_ABS,
285 jt: 0,
286 jf: 0,
287 k: 4, // offset of arch in seccomp_data
288 },
289 // Instruction 1: compare with AUDIT_ARCH_X86_64
290 // If match, jump to next instruction; if not, kill process
291 sock_filter {
292 code: BPF_JMP | BPF_JEQ,
293 jt: 0,
294 jf: 1, // jump 1 forward (to KILL) if arch doesn't match
295 k: AUDIT_ARCH_X86_64,
296 },
297 // Instruction 2: KILL (wrong architecture)
298 sock_filter {
299 code: BPF_RET,
300 jt: 0,
301 jf: 0,
302 k: SECCOMP_RET_KILL_PROCESS,
303 },
304 // Instruction 3: load syscall number from seccomp_data.nr
305 sock_filter {
306 code: BPF_LD | BPF_W | BPF_ABS,
307 jt: 0,
308 jf: 0,
309 k: 0, // offset of nr in seccomp_data
310 },
311 ];
312
313 // For each allowed syscall, add a compare+jump to ALLOW.
314 // We use a linear scan for simplicity: each JEQ instruction jumps
315 // forward over the remaining checks + KILL to reach ALLOW.
316 for &syscall in allowed_syscalls {
317 let remaining = (allowed_syscalls.len() as u8).saturating_sub(
318 allowed_syscalls
319 .iter()
320 .position(|&s| s == syscall)
321 .unwrap_or(0) as u8,
322 );
323 // If syscall == this one, jump to allow_target; otherwise fall through
324 filter.push(sock_filter {
325 code: BPF_JMP | BPF_JEQ,
326 jt: remaining, // jump forward to ALLOW
327 jf: 0, // fall through to next check
328 k: syscall,
329 });
330 }
331
332 // Instruction N: KILL PROCESS for any unmatched syscall
333 filter.push(sock_filter {
334 code: BPF_RET,
335 jt: 0,
336 jf: 0,
337 k: SECCOMP_RET_KILL_PROCESS,
338 });
339
340 // Instruction N+1: ALLOW
341 filter.push(sock_filter {
342 code: BPF_RET,
343 jt: 0,
344 jf: 0,
345 k: SECCOMP_RET_ALLOW,
346 });
347
348 // ── Load the filter into the kernel ───────────────────────────────────
349
350 #[repr(C)]
351 struct sock_fprog {
352 len: u16,
353 filter: *const sock_filter,
354 }
355
356 let prog = sock_fprog {
357 len: filter.len() as u16,
358 filter: filter.as_ptr(),
359 };
360
361 // Safety: prctl with PR_SET_SECCOMP installs a seccomp BPF filter.
362 // The filter is a valid array of sock_filter instructions that lives
363 // for the duration of the prctl call.
364 let result = unsafe {
365 libc::prctl(
366 libc::PR_SET_SECCOMP,
367 libc::SECCOMP_MODE_FILTER,
368 &raw const prog,
369 0i64,
370 0i64,
371 )
372 };
373
374 if result != 0 {
375 return Err(std::io::Error::last_os_error());
376 }
377
378 Ok(())
379 }
380
381 #[cfg(test)]
382 mod tests {
383 use super::*;
384
385 #[test]
386 fn test_is_available_does_not_panic() {
387 let _ = is_available();
388 }
389
390 #[test]
391 #[cfg(target_os = "linux")]
392 fn test_detect_denial() {
393 assert!(detect_denial(31, ""));
394 assert!(detect_denial(1, "Bad system call"));
395 assert!(detect_denial(1, "SIGSYS"));
396 assert!(!detect_denial(0, "Success"));
397 assert!(!detect_denial(1, "File not found"));
398 }
399
400 #[test]
401 fn test_detect_denial_non_linux() {
402 #[cfg(not(target_os = "linux"))]
403 {
404 assert!(!detect_denial(31, "Bad system call"));
405 }
406 }
407 }
408
408 lines RUST