1use crate::error::{NucleusError, Result};
2use crate::security::policy::sha256_hex;
3#[cfg(any(
4 target_arch = "x86_64",
5 target_arch = "aarch64",
6 target_arch = "riscv64"
7))]
8use crate::security::syscall_numbers::{SYS_FADVISE64, SYS_SENDFILE};
9use seccompiler::{BpfProgram, SeccompAction, SeccompCondition, SeccompFilter, SeccompRule};
10use std::collections::BTreeMap;
11use std::path::Path;
12use tracing::{debug, info, warn};
13
14pub struct SeccompManager {
19 applied: bool,
20}
21
22const DENIED_CLONE_NAMESPACE_FLAGS: u64 = (libc::CLONE_NEWUSER
23 | libc::CLONE_NEWNS
24 | libc::CLONE_NEWNET
25 | libc::CLONE_NEWIPC
26 | libc::CLONE_NEWUTS
27 | libc::CLONE_NEWPID
28 | libc::CLONE_NEWCGROUP
29 | libc::CLONE_NEWTIME) as u64;
30
31impl SeccompManager {
32 pub fn new() -> Self {
33 Self { applied: false }
34 }
35
36 fn base_allowed_syscalls() -> Vec<i64> {
37 let mut syscalls = vec![
38 libc::SYS_read,
40 libc::SYS_write,
41 libc::SYS_openat,
42 libc::SYS_close,
43 libc::SYS_fstat,
44 libc::SYS_lseek,
45 libc::SYS_fcntl,
46 libc::SYS_readv,
47 libc::SYS_writev,
48 libc::SYS_preadv,
49 libc::SYS_pwritev,
50 libc::SYS_pread64,
51 libc::SYS_pwrite64,
52 libc::SYS_readlinkat,
53 libc::SYS_newfstatat,
54 libc::SYS_statx,
55 libc::SYS_faccessat,
56 libc::SYS_faccessat2,
57 libc::SYS_dup,
58 libc::SYS_dup3,
59 libc::SYS_pipe2,
60 libc::SYS_unlinkat,
61 libc::SYS_renameat,
62 libc::SYS_renameat2,
63 libc::SYS_linkat,
64 libc::SYS_symlinkat,
65 libc::SYS_fchmod,
66 libc::SYS_fchmodat,
67 libc::SYS_truncate,
68 libc::SYS_ftruncate,
69 libc::SYS_fallocate,
70 #[cfg(any(
71 target_arch = "x86_64",
72 target_arch = "aarch64",
73 target_arch = "riscv64"
74 ))]
75 SYS_FADVISE64,
76 libc::SYS_fsync,
77 libc::SYS_fdatasync,
78 libc::SYS_sync_file_range,
79 libc::SYS_flock,
80 libc::SYS_fstatfs,
81 libc::SYS_statfs,
82 #[cfg(any(
83 target_arch = "x86_64",
84 target_arch = "aarch64",
85 target_arch = "riscv64"
86 ))]
87 SYS_SENDFILE,
88 libc::SYS_copy_file_range,
89 libc::SYS_splice,
90 libc::SYS_tee,
91 libc::SYS_mmap,
93 libc::SYS_munmap,
94 libc::SYS_brk,
95 libc::SYS_mremap,
96 libc::SYS_madvise,
97 libc::SYS_msync,
98 libc::SYS_mlock,
99 libc::SYS_munlock,
100 libc::SYS_mlock2,
101 libc::SYS_shmget,
104 libc::SYS_shmat,
105 libc::SYS_shmdt,
106 libc::SYS_shmctl,
107 libc::SYS_semget,
109 libc::SYS_semop,
110 libc::SYS_semctl,
111 libc::SYS_semtimedop,
112 libc::SYS_execve,
118 libc::SYS_wait4,
120 libc::SYS_waitid,
121 libc::SYS_exit,
122 libc::SYS_exit_group,
123 libc::SYS_getpid,
124 libc::SYS_gettid,
125 libc::SYS_getuid,
126 libc::SYS_getgid,
127 libc::SYS_geteuid,
128 libc::SYS_getegid,
129 libc::SYS_getresuid,
130 libc::SYS_getresgid,
131 libc::SYS_getppid,
132 libc::SYS_setsid,
133 libc::SYS_getgroups,
134 libc::SYS_rt_sigaction,
136 libc::SYS_rt_sigprocmask,
137 libc::SYS_rt_sigreturn,
138 libc::SYS_rt_sigsuspend,
139 libc::SYS_rt_sigtimedwait,
140 libc::SYS_rt_sigpending,
141 libc::SYS_rt_sigqueueinfo,
142 libc::SYS_sigaltstack,
143 libc::SYS_restart_syscall,
144 libc::SYS_kill,
148 libc::SYS_tgkill,
149 libc::SYS_clock_gettime,
151 libc::SYS_clock_getres,
152 libc::SYS_clock_nanosleep,
153 libc::SYS_gettimeofday,
154 libc::SYS_nanosleep,
155 libc::SYS_setitimer,
156 libc::SYS_getitimer,
157 libc::SYS_getcwd,
159 libc::SYS_chdir,
160 libc::SYS_fchdir,
161 libc::SYS_mkdirat,
162 libc::SYS_getdents64,
163 libc::SYS_uname,
165 libc::SYS_getrandom,
166 libc::SYS_futex,
167 libc::SYS_set_tid_address,
168 libc::SYS_set_robust_list,
169 libc::SYS_get_robust_list,
170 libc::SYS_umask,
173 libc::SYS_getrusage,
175 libc::SYS_times,
176 libc::SYS_sched_yield,
177 libc::SYS_sched_getaffinity,
178 libc::SYS_sched_setaffinity,
179 libc::SYS_sched_getparam,
180 libc::SYS_sched_getscheduler,
181 libc::SYS_getcpu,
182 libc::SYS_getxattr,
184 libc::SYS_lgetxattr,
185 libc::SYS_fgetxattr,
186 libc::SYS_listxattr,
187 libc::SYS_llistxattr,
188 libc::SYS_flistxattr,
189 libc::SYS_rseq,
190 libc::SYS_close_range,
191 libc::SYS_fchown,
194 libc::SYS_fchownat,
195 libc::SYS_io_setup,
198 libc::SYS_io_destroy,
199 libc::SYS_io_submit,
200 libc::SYS_io_getevents,
201 libc::SYS_setpgid,
207 libc::SYS_getpgid,
208 libc::SYS_landlock_create_ruleset,
212 libc::SYS_landlock_add_rule,
213 libc::SYS_landlock_restrict_self,
214 libc::SYS_getsockname,
216 libc::SYS_getpeername,
217 libc::SYS_socketpair,
218 libc::SYS_getsockopt,
219 libc::SYS_connect,
222 libc::SYS_ppoll,
224 libc::SYS_pselect6,
225 libc::SYS_epoll_create1,
226 libc::SYS_epoll_ctl,
227 libc::SYS_epoll_pwait,
228 libc::SYS_eventfd2,
229 libc::SYS_signalfd4,
230 libc::SYS_timerfd_create,
231 libc::SYS_timerfd_settime,
232 libc::SYS_timerfd_gettime,
233 ];
234
235 #[cfg(target_arch = "x86_64")]
237 syscalls.extend_from_slice(&[
238 libc::SYS_open,
239 libc::SYS_stat,
240 libc::SYS_lstat,
241 libc::SYS_access,
242 libc::SYS_readlink,
243 libc::SYS_dup2,
244 libc::SYS_pipe,
245 libc::SYS_unlink,
246 libc::SYS_rename,
247 libc::SYS_link,
248 libc::SYS_symlink,
249 libc::SYS_chmod,
250 libc::SYS_mkdir,
251 libc::SYS_rmdir,
252 libc::SYS_getdents,
253 libc::SYS_getpgrp,
254 libc::SYS_chown,
255 libc::SYS_fchown,
256 libc::SYS_lchown,
257 libc::SYS_arch_prctl,
258 libc::SYS_getrlimit,
259 libc::SYS_poll,
260 libc::SYS_select,
261 libc::SYS_epoll_create,
262 libc::SYS_epoll_wait,
263 libc::SYS_eventfd,
264 libc::SYS_signalfd,
265 ]);
266
267 syscalls
268 }
269
270 fn allowed_socket_domains(allow_network: bool) -> Vec<i32> {
271 if allow_network {
272 vec![libc::AF_UNIX, libc::AF_INET, libc::AF_INET6]
273 } else {
274 vec![libc::AF_UNIX]
275 }
276 }
277
278 fn network_mode_syscalls(allow_network: bool) -> Vec<i64> {
279 if allow_network {
280 vec![
281 libc::SYS_sendto,
282 libc::SYS_recvfrom,
283 libc::SYS_sendmsg,
284 libc::SYS_recvmsg,
285 libc::SYS_shutdown,
286 libc::SYS_bind,
287 libc::SYS_listen,
288 libc::SYS_accept,
289 libc::SYS_accept4,
290 libc::SYS_setsockopt,
291 ]
292 } else {
293 Vec::new()
294 }
295 }
296
297 fn minimal_filter(
307 allow_network: bool,
308 extra_syscalls: &[String],
309 ) -> Result<BTreeMap<i64, Vec<SeccompRule>>> {
310 let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
311
312 let allowed_syscalls = Self::base_allowed_syscalls();
314
315 for syscall in allowed_syscalls {
317 rules.insert(syscall, Vec::new());
318 }
319
320 for syscall in Self::network_mode_syscalls(allow_network) {
322 rules.insert(syscall, Vec::new());
323 }
324
325 for name in extra_syscalls {
333 if Self::ARG_FILTERED_SYSCALLS.contains(&name.as_str()) {
334 continue;
335 }
336
337 if let Some(nr) = syscall_name_to_number(name) {
338 if let std::collections::btree_map::Entry::Vacant(entry) = rules.entry(nr) {
339 if Self::SECURITY_CRITICAL_DENIED_SYSCALLS.contains(&name.as_str()) {
340 warn!(
341 "--seccomp-allow: security-critical syscall '{}' is always blocked",
342 name
343 );
344 } else if Self::OPT_IN_SYSCALLS.contains(&name.as_str()) {
345 entry.insert(Vec::new());
346 } else {
347 warn!(
348 "--seccomp-allow: syscall '{}' is not in the opt-in allowlist – blocked",
349 name
350 );
351 }
352 }
353 } else {
354 warn!("--seccomp-allow: unknown syscall '{}' – blocked", name);
355 }
356 }
357
358 let mut socket_rules = Vec::new();
361 for domain in Self::allowed_socket_domains(allow_network) {
362 let condition = SeccompCondition::new(
363 0, seccompiler::SeccompCmpArgLen::Dword,
365 seccompiler::SeccompCmpOp::Eq,
366 domain as u64,
367 )
368 .map_err(|e| {
369 NucleusError::SeccompError(format!(
370 "Failed to create socket domain condition: {}",
371 e
372 ))
373 })?;
374 let rule = SeccompRule::new(vec![condition]).map_err(|e| {
375 NucleusError::SeccompError(format!("Failed to create socket rule: {}", e))
376 })?;
377 socket_rules.push(rule);
378 }
379 rules.insert(libc::SYS_socket, socket_rules);
380
381 let ioctl_allowed: &[u64] = &[
383 0x5401, libc::TCGETS2, 0x5402, 0x5403, 0x5404, 0x540B, 0x540E, 0x540F, 0x5410, 0x5413, 0x5429, 0x541B, 0x5421, 0x5451, 0x5450, ];
401 let mut ioctl_rules = Vec::new();
402 for &request in ioctl_allowed {
403 let condition = SeccompCondition::new(
404 1, seccompiler::SeccompCmpArgLen::Dword,
406 seccompiler::SeccompCmpOp::Eq,
407 request,
408 )
409 .map_err(|e| {
410 NucleusError::SeccompError(format!("Failed to create ioctl condition: {}", e))
411 })?;
412 let rule = SeccompRule::new(vec![condition]).map_err(|e| {
413 NucleusError::SeccompError(format!("Failed to create ioctl rule: {}", e))
414 })?;
415 ioctl_rules.push(rule);
416 }
417 rules.insert(libc::SYS_ioctl, ioctl_rules);
418
419 let prctl_allowed: &[u64] = &[
425 1, 2, 15, 16, 23, 27, 36, 37, 38, 40, 39, libc::PR_SET_MM as u64,
444 ];
445 let mut prctl_rules = Vec::new();
446 for &option in prctl_allowed {
447 let condition = SeccompCondition::new(
448 0, seccompiler::SeccompCmpArgLen::Dword,
450 seccompiler::SeccompCmpOp::Eq,
451 option,
452 )
453 .map_err(|e| {
454 NucleusError::SeccompError(format!("Failed to create prctl condition: {}", e))
455 })?;
456 let rule = SeccompRule::new(vec![condition]).map_err(|e| {
457 NucleusError::SeccompError(format!("Failed to create prctl rule: {}", e))
458 })?;
459 prctl_rules.push(rule);
460 }
461
462 let ambient_option = SeccompCondition::new(
463 0, seccompiler::SeccompCmpArgLen::Dword,
465 seccompiler::SeccompCmpOp::Eq,
466 libc::PR_CAP_AMBIENT as u64,
467 )
468 .map_err(|e| {
469 NucleusError::SeccompError(format!(
470 "Failed to create PR_CAP_AMBIENT prctl condition: {}",
471 e
472 ))
473 })?;
474 let ambient_is_set = SeccompCondition::new(
475 1, seccompiler::SeccompCmpArgLen::Dword,
477 seccompiler::SeccompCmpOp::Eq,
478 libc::PR_CAP_AMBIENT_IS_SET as u64,
479 )
480 .map_err(|e| {
481 NucleusError::SeccompError(format!(
482 "Failed to create PR_CAP_AMBIENT_IS_SET prctl condition: {}",
483 e
484 ))
485 })?;
486 let ambient_probe_rule =
487 SeccompRule::new(vec![ambient_option, ambient_is_set]).map_err(|e| {
488 NucleusError::SeccompError(format!(
489 "Failed to create PR_CAP_AMBIENT_IS_SET prctl rule: {}",
490 e
491 ))
492 })?;
493 prctl_rules.push(ambient_probe_rule);
494 rules.insert(libc::SYS_prctl, prctl_rules);
495
496 let prlimit_condition = SeccompCondition::new(
499 2, seccompiler::SeccompCmpArgLen::Qword,
501 seccompiler::SeccompCmpOp::Eq,
502 0u64, )
504 .map_err(|e| {
505 NucleusError::SeccompError(format!("Failed to create prlimit64 condition: {}", e))
506 })?;
507 let prlimit_rule = SeccompRule::new(vec![prlimit_condition]).map_err(|e| {
508 NucleusError::SeccompError(format!("Failed to create prlimit64 rule: {}", e))
509 })?;
510 rules.insert(libc::SYS_prlimit64, vec![prlimit_rule]);
511
512 let mut mprotect_rules = Vec::new();
514 for allowed in [0, libc::PROT_WRITE as u64, libc::PROT_EXEC as u64] {
515 let condition = SeccompCondition::new(
516 2, seccompiler::SeccompCmpArgLen::Dword,
518 seccompiler::SeccompCmpOp::MaskedEq((libc::PROT_WRITE | libc::PROT_EXEC) as u64),
519 allowed,
520 )
521 .map_err(|e| {
522 NucleusError::SeccompError(format!("Failed to create mprotect condition: {}", e))
523 })?;
524 let rule = SeccompRule::new(vec![condition]).map_err(|e| {
525 NucleusError::SeccompError(format!("Failed to create mprotect rule: {}", e))
526 })?;
527 mprotect_rules.push(rule);
528 }
529 rules.insert(libc::SYS_mprotect, mprotect_rules);
530
531 for (syscall, name) in [
535 (libc::SYS_preadv2, "preadv2"),
536 (libc::SYS_pwritev2, "pwritev2"),
537 ] {
538 let condition = SeccompCondition::new(
539 5, seccompiler::SeccompCmpArgLen::Qword,
541 seccompiler::SeccompCmpOp::Eq,
542 0,
543 )
544 .map_err(|e| {
545 NucleusError::SeccompError(format!(
546 "Failed to create {} flags condition: {}",
547 name, e
548 ))
549 })?;
550 let rule = SeccompRule::new(vec![condition]).map_err(|e| {
551 NucleusError::SeccompError(format!("Failed to create {} rule: {}", name, e))
552 })?;
553 rules.insert(syscall, vec![rule]);
554 }
555
556 let clone_condition = SeccompCondition::new(
563 0, seccompiler::SeccompCmpArgLen::Qword,
565 seccompiler::SeccompCmpOp::MaskedEq(DENIED_CLONE_NAMESPACE_FLAGS),
566 0, )
568 .map_err(|e| {
569 NucleusError::SeccompError(format!("Failed to create clone condition: {}", e))
570 })?;
571 let clone_rule = SeccompRule::new(vec![clone_condition]).map_err(|e| {
572 NucleusError::SeccompError(format!("Failed to create clone rule: {}", e))
573 })?;
574 rules.insert(libc::SYS_clone, vec![clone_rule]);
575
576 let execveat_condition = SeccompCondition::new(
583 4, seccompiler::SeccompCmpArgLen::Dword,
585 seccompiler::SeccompCmpOp::MaskedEq(libc::AT_EMPTY_PATH as u64),
586 0, )
588 .map_err(|e| {
589 NucleusError::SeccompError(format!("Failed to create execveat condition: {}", e))
590 })?;
591 let execveat_rule = SeccompRule::new(vec![execveat_condition]).map_err(|e| {
592 NucleusError::SeccompError(format!("Failed to create execveat rule: {}", e))
593 })?;
594 rules.insert(libc::SYS_execveat, vec![execveat_rule]);
595
596 Ok(rules)
597 }
598
599 pub fn validate_extra_syscalls_for_production(
605 allow_network: bool,
606 extra_syscalls: &[String],
607 ) -> Result<()> {
608 let base_syscalls = Self::base_allowed_syscalls();
609 let network_syscalls = Self::network_mode_syscalls(allow_network);
610
611 for name in extra_syscalls {
612 let Some(nr) = syscall_name_to_number(name) else {
613 return Err(NucleusError::ConfigError(format!(
614 "Production mode rejects unknown --seccomp-allow syscall '{}'",
615 name
616 )));
617 };
618
619 if base_syscalls.contains(&nr)
620 || network_syscalls.contains(&nr)
621 || Self::ARG_FILTERED_SYSCALLS.contains(&name.as_str())
622 {
623 continue;
624 }
625
626 if Self::SECURITY_CRITICAL_DENIED_SYSCALLS.contains(&name.as_str()) {
627 return Err(NucleusError::ConfigError(format!(
628 "Production mode forbids --seccomp-allow for security-critical syscall '{}'",
629 name
630 )));
631 }
632
633 if Self::OPT_IN_SYSCALLS.contains(&name.as_str()) {
634 continue;
635 }
636
637 return Err(NucleusError::ConfigError(format!(
638 "Production mode rejects unsupported --seccomp-allow syscall '{}'",
639 name
640 )));
641 }
642
643 Ok(())
644 }
645
646 pub fn compile_minimal_filter() -> Result<BpfProgram> {
653 let rules = Self::minimal_filter(true, &[])?;
654 let target_arch = std::env::consts::ARCH.try_into().map_err(|e| {
655 NucleusError::SeccompError(format!("Unsupported architecture: {:?}", e))
656 })?;
657 super::seccomp_bpf::compile_bitmap_bpf_with_errno_syscalls(
658 rules,
659 Self::errno_denied_syscalls(),
660 SeccompAction::KillProcess,
661 SeccompAction::Allow,
662 target_arch,
663 )
664 }
665
666 #[cfg(test)]
668 pub(crate) fn minimal_filter_for_test(
669 allow_network: bool,
670 extra_syscalls: &[String],
671 ) -> BTreeMap<i64, Vec<SeccompRule>> {
672 Self::minimal_filter(allow_network, extra_syscalls).unwrap()
673 }
674
675 #[cfg(test)]
676 pub(crate) fn errno_denied_syscalls_for_test() -> &'static [(i64, u32)] {
677 Self::errno_denied_syscalls()
678 }
679
680 pub fn apply_minimal_filter(&mut self) -> Result<bool> {
688 self.apply_minimal_filter_with_mode(false, false)
689 }
690
691 pub fn apply_minimal_filter_with_mode(
696 &mut self,
697 best_effort: bool,
698 log_denied: bool,
699 ) -> Result<bool> {
700 self.apply_filter_for_network_mode(true, best_effort, log_denied, &[])
701 }
702
703 pub fn apply_filter_for_network_mode(
712 &mut self,
713 allow_network: bool,
714 best_effort: bool,
715 log_denied: bool,
716 extra_syscalls: &[String],
717 ) -> Result<bool> {
718 self.apply_filter_with_options(
719 allow_network,
720 best_effort,
721 log_denied,
722 extra_syscalls,
723 false,
724 )
725 }
726
727 pub fn apply_filter_with_options(
735 &mut self,
736 allow_network: bool,
737 best_effort: bool,
738 log_denied: bool,
739 extra_syscalls: &[String],
740 gpu_mode: bool,
741 ) -> Result<bool> {
742 if self.applied {
743 debug!("Seccomp filter already applied, skipping");
744 return Ok(true);
745 }
746
747 info!(allow_network, gpu_mode, "Applying seccomp filter");
748
749 let mut rules = match Self::minimal_filter(allow_network, extra_syscalls) {
750 Ok(r) => r,
751 Err(e) => {
752 if best_effort {
753 warn!(
754 "Failed to create seccomp rules: {} (continuing without seccomp)",
755 e
756 );
757 return Ok(false);
758 }
759 return Err(e);
760 }
761 };
762
763 if gpu_mode {
767 rules.insert(libc::SYS_ioctl, Vec::new());
768 info!("GPU mode: relaxing seccomp ioctl filter to allow-all");
769 }
770
771 let target_arch = match std::env::consts::ARCH.try_into() {
772 Ok(a) => a,
773 Err(e) => {
774 let msg = format!("Unsupported architecture: {:?}", e);
775 if best_effort {
776 warn!("{} (continuing without seccomp)", msg);
777 return Ok(false);
778 }
779 return Err(NucleusError::SeccompError(msg));
780 }
781 };
782
783 let bpf_prog: BpfProgram = match super::seccomp_bpf::compile_bitmap_bpf_with_errno_syscalls(
784 rules,
785 Self::errno_denied_syscalls(),
786 SeccompAction::KillProcess,
787 SeccompAction::Allow,
788 target_arch,
789 ) {
790 Ok(p) => p,
791 Err(e) => {
792 if best_effort {
793 warn!(
794 "Failed to compile BPF program: {} (continuing without seccomp)",
795 e
796 );
797 return Ok(false);
798 }
799 return Err(e);
800 }
801 };
802
803 match Self::apply_bpf_program(&bpf_prog, log_denied) {
805 Ok(_) => {
806 self.applied = true;
807 info!("Successfully applied seccomp filter");
808 Ok(true)
809 }
810 Err(e) => {
811 if best_effort {
812 warn!(
813 "Failed to apply seccomp filter: {} (continuing without seccomp)",
814 e
815 );
816 Ok(false)
817 } else {
818 Err(NucleusError::SeccompError(format!(
819 "Failed to apply seccomp filter: {}",
820 e
821 )))
822 }
823 }
824 }
825 }
826
827 pub fn apply_profile_from_file(
846 &mut self,
847 profile_path: &Path,
848 expected_sha256: Option<&str>,
849 audit_mode: bool,
850 ) -> Result<bool> {
851 if self.applied {
852 debug!("Seccomp filter already applied, skipping");
853 return Ok(true);
854 }
855
856 info!("Loading seccomp profile from {:?}", profile_path);
857
858 let content = std::fs::read(profile_path).map_err(|e| {
860 NucleusError::SeccompError(format!(
861 "Failed to read seccomp profile {:?}: {}",
862 profile_path, e
863 ))
864 })?;
865
866 if let Some(expected) = expected_sha256 {
868 let actual = sha256_hex(&content);
869 if actual != expected {
870 return Err(NucleusError::SeccompError(format!(
871 "Seccomp profile hash mismatch: expected {}, got {}",
872 expected, actual
873 )));
874 }
875 info!("Seccomp profile hash verified: {}", actual);
876 }
877
878 let profile: SeccompProfile = serde_json::from_slice(&content).map_err(|e| {
880 NucleusError::SeccompError(format!("Failed to parse seccomp profile: {}", e))
881 })?;
882
883 Self::warn_missing_arg_filters(&profile);
888
889 let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
891
892 for syscall_group in &profile.syscalls {
893 if syscall_group.action == "SCMP_ACT_ALLOW" {
894 for name in &syscall_group.names {
895 if name == "clone3" {
896 warn!(
897 "Custom seccomp profile requested clone3; ignoring it and returning \
898 ENOSYS because clone3 namespace flags cannot be argument-filtered"
899 );
900 continue;
901 }
902 if let Some(nr) = syscall_name_to_number(name) {
903 rules.insert(nr, Vec::new());
904 } else {
905 warn!("Unknown syscall in profile: {} (skipping)", name);
906 }
907 }
908 }
909 }
910
911 let builtin_rules = Self::minimal_filter(true, &[])?;
916 for syscall_name in Self::ARG_FILTERED_SYSCALLS {
917 if let Some(nr) = syscall_name_to_number(syscall_name) {
918 if let std::collections::btree_map::Entry::Occupied(mut entry) = rules.entry(nr) {
919 if let Some(builtin) = builtin_rules.get(&nr) {
920 if !builtin.is_empty() {
921 info!(
922 "Merging built-in argument filters for '{}' into custom profile",
923 syscall_name
924 );
925 entry.insert(builtin.clone());
926 }
927 }
928 }
929 }
930 }
931 let target_arch = std::env::consts::ARCH.try_into().map_err(|e| {
932 NucleusError::SeccompError(format!("Unsupported architecture: {:?}", e))
933 })?;
934
935 let bpf_prog: BpfProgram = super::seccomp_bpf::compile_bitmap_bpf_with_errno_syscalls(
936 rules,
937 Self::errno_denied_syscalls(),
938 SeccompAction::KillProcess,
939 SeccompAction::Allow,
940 target_arch,
941 )?;
942
943 match Self::apply_bpf_program(&bpf_prog, audit_mode) {
944 Ok(_) => {
945 self.applied = true;
946 info!(
947 "Seccomp profile applied from {:?} (log_denied={})",
948 profile_path, audit_mode
949 );
950 Ok(true)
951 }
952 Err(e) => Err(e),
953 }
954 }
955
956 pub fn apply_trace_filter(&mut self) -> Result<bool> {
961 if self.applied {
962 debug!("Seccomp filter already applied, skipping trace filter");
963 return Ok(true);
964 }
965
966 info!("Applying seccomp trace filter (allow-all + LOG)");
967
968 let bpf_prog = Self::compile_trace_filter()?;
969
970 Self::apply_bpf_program(&bpf_prog, true)?;
972 self.applied = true;
973 info!("Seccomp trace filter applied (all syscalls allowed + logged)");
974 Ok(true)
975 }
976
977 fn compile_trace_filter() -> Result<BpfProgram> {
978 let filter = SeccompFilter::new(
982 BTreeMap::new(),
983 SeccompAction::Log, SeccompAction::Allow, std::env::consts::ARCH.try_into().map_err(|e| {
986 NucleusError::SeccompError(format!("Unsupported architecture: {:?}", e))
987 })?,
988 )
989 .map_err(|e| NucleusError::SeccompError(format!("Failed to create trace filter: {}", e)))?;
990
991 filter
992 .try_into()
993 .map_err(|e| NucleusError::SeccompError(format!("Failed to compile trace BPF: {}", e)))
994 }
995
996 const ARG_FILTERED_SYSCALLS: &'static [&'static str] = &[
999 "clone", "execveat", "ioctl", "mprotect", "preadv2", "prctl", "pwritev2", "socket",
1000 ];
1001
1002 const SECURITY_CRITICAL_DENIED_SYSCALLS: &'static [&'static str] = &[
1005 "clone3",
1007 "unshare",
1008 "setns",
1009 "add_key",
1011 "request_key",
1012 "keyctl",
1013 ];
1014
1015 fn errno_denied_syscalls() -> &'static [(i64, u32)] {
1016 &[(libc::SYS_clone3, libc::ENOSYS as u32)]
1017 }
1018
1019 const OPT_IN_SYSCALLS: &'static [&'static str] = &[
1026 "io_uring_setup",
1028 "io_uring_enter",
1029 "io_uring_register",
1030 "msgget",
1032 "msgsnd",
1033 "msgrcv",
1034 "msgctl",
1035 "mq_open",
1037 "mq_unlink",
1038 "mq_timedsend",
1039 "mq_timedreceive",
1040 "mq_notify",
1041 "mq_getsetattr",
1042 "timer_create",
1044 "timer_settime",
1045 "timer_gettime",
1046 "timer_getoverrun",
1047 "timer_delete",
1048 "inotify_init",
1050 "inotify_init1",
1051 "inotify_add_watch",
1052 "inotify_rm_watch",
1053 "fanotify_init",
1054 "fanotify_mark",
1055 "mincore",
1057 "mlockall",
1058 "munlockall",
1059 "membarrier",
1060 "process_madvise",
1061 "mbind",
1062 "set_mempolicy",
1063 "get_mempolicy",
1064 "set_mempolicy_home_node",
1065 "pkey_mprotect",
1066 "pkey_alloc",
1067 "pkey_free",
1068 "cachestat",
1069 "remap_file_pages",
1070 "sync",
1072 "syncfs",
1073 "sync_file_range",
1074 "readahead",
1075 "vmsplice",
1076 "openat2",
1077 "name_to_handle_at",
1078 "open_by_handle_at",
1079 "io_cancel",
1080 "io_pgetevents",
1081 "creat",
1082 "fchmodat2",
1083 "statmount",
1084 "listmount",
1085 "utimensat",
1086 "utimes",
1087 "utime",
1088 "futimesat",
1089 "setxattr",
1091 "lsetxattr",
1092 "fsetxattr",
1093 "removexattr",
1094 "lremovexattr",
1095 "fremovexattr",
1096 "setxattrat",
1097 "getxattrat",
1098 "listxattrat",
1099 "removexattrat",
1100 "recvmmsg",
1102 "sendmmsg",
1103 "sched_setparam",
1105 "sched_setscheduler",
1106 "sched_get_priority_max",
1107 "sched_get_priority_min",
1108 "sched_rr_get_interval",
1109 "sched_setattr",
1110 "sched_getattr",
1111 "setrlimit",
1113 "getpriority",
1114 "setpriority",
1115 "ioprio_set",
1116 "ioprio_get",
1117 "vfork",
1119 "pause",
1120 "alarm",
1121 "tkill",
1122 "sysinfo",
1123 "personality",
1124 "vhangup",
1125 "time",
1126 "pidfd_open",
1127 "pidfd_send_signal",
1128 "pidfd_getfd",
1129 "setuid",
1131 "setgid",
1132 "setreuid",
1133 "setregid",
1134 "setresuid",
1135 "getresuid",
1136 "setresgid",
1137 "getresgid",
1138 "setfsuid",
1139 "setfsgid",
1140 "setgroups",
1141 "getsid",
1142 "capget",
1144 "rt_tgsigqueueinfo",
1146 "mknod",
1148 "mknodat",
1149 "syslog",
1150 "clock_settime",
1151 "clock_adjtime",
1152 "adjtimex",
1153 "kcmp",
1154 "epoll_pwait2",
1155 "futex_waitv",
1157 "futex_wake",
1158 "futex_wait",
1159 "futex_requeue",
1160 "seccomp",
1162 ];
1163
1164 fn warn_missing_arg_filters(profile: &SeccompProfile) {
1167 for group in &profile.syscalls {
1168 if group.action != "SCMP_ACT_ALLOW" {
1169 continue;
1170 }
1171 for name in &group.names {
1172 if Self::ARG_FILTERED_SYSCALLS.contains(&name.as_str()) && group.args.is_empty() {
1173 warn!(
1174 "Custom seccomp profile allows '{}' without argument filters. \
1175 The built-in filter restricts this syscall at the argument level. \
1176 This profile weakens security compared to the default.",
1177 name
1178 );
1179 }
1180 }
1181 }
1182 }
1183
1184 pub fn is_applied(&self) -> bool {
1186 self.applied
1187 }
1188
1189 fn apply_bpf_program(bpf_prog: &BpfProgram, log_denied: bool) -> Result<()> {
1190 let mut flags: libc::c_ulong = 0;
1191 if log_denied {
1192 flags |= libc::SECCOMP_FILTER_FLAG_LOG as libc::c_ulong;
1193 }
1194
1195 match Self::apply_bpf_program_with_flags(bpf_prog, flags) {
1196 Ok(()) => Ok(()),
1197 Err(err)
1198 if log_denied
1199 && err.raw_os_error() == Some(libc::EINVAL)
1200 && libc::SECCOMP_FILTER_FLAG_LOG != 0 =>
1201 {
1202 warn!(
1203 "Kernel rejected SECCOMP_FILTER_FLAG_LOG; continuing with seccomp \
1204 enforcement without deny logging"
1205 );
1206 Self::apply_bpf_program_with_flags(bpf_prog, 0)?;
1207 Ok(())
1208 }
1209 Err(err) => Err(NucleusError::SeccompError(format!(
1210 "Failed to apply seccomp filter: {}",
1211 err
1212 ))),
1213 }
1214 }
1215
1216 fn apply_bpf_program_with_flags(
1217 bpf_prog: &BpfProgram,
1218 flags: libc::c_ulong,
1219 ) -> std::io::Result<()> {
1220 let rc = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
1223 if rc != 0 {
1224 return Err(std::io::Error::last_os_error());
1225 }
1226
1227 let prog = libc::sock_fprog {
1228 len: bpf_prog.len() as u16,
1229 filter: bpf_prog.as_ptr() as *mut libc::sock_filter,
1230 };
1231
1232 let rc = unsafe {
1235 libc::syscall(
1236 libc::SYS_seccomp,
1237 libc::SECCOMP_SET_MODE_FILTER,
1238 flags,
1239 &prog as *const libc::sock_fprog,
1240 )
1241 };
1242
1243 if rc < 0 {
1244 return Err(std::io::Error::last_os_error());
1245 }
1246
1247 Ok(())
1248 }
1249}
1250
1251use crate::security::seccomp_generate::SeccompProfile;
1253
1254fn syscall_name_to_number(name: &str) -> Option<i64> {
1258 match name {
1259 "read" => Some(libc::SYS_read),
1261 "write" => Some(libc::SYS_write),
1262 #[cfg(target_arch = "x86_64")]
1263 "open" => Some(libc::SYS_open),
1264 "openat" => Some(libc::SYS_openat),
1265 "close" => Some(libc::SYS_close),
1266 #[cfg(target_arch = "x86_64")]
1267 "stat" => Some(libc::SYS_stat),
1268 "fstat" => Some(libc::SYS_fstat),
1269 #[cfg(target_arch = "x86_64")]
1270 "lstat" => Some(libc::SYS_lstat),
1271 "lseek" => Some(libc::SYS_lseek),
1272 #[cfg(target_arch = "x86_64")]
1273 "access" => Some(libc::SYS_access),
1274 "fcntl" => Some(libc::SYS_fcntl),
1275 "readv" => Some(libc::SYS_readv),
1276 "writev" => Some(libc::SYS_writev),
1277 "preadv" => Some(libc::SYS_preadv),
1278 "pwritev" => Some(libc::SYS_pwritev),
1279 "preadv2" => Some(libc::SYS_preadv2),
1280 "pwritev2" => Some(libc::SYS_pwritev2),
1281 "pread64" => Some(libc::SYS_pread64),
1282 "pwrite64" => Some(libc::SYS_pwrite64),
1283 #[cfg(target_arch = "x86_64")]
1284 "readlink" => Some(libc::SYS_readlink),
1285 "readlinkat" => Some(libc::SYS_readlinkat),
1286 "newfstatat" => Some(libc::SYS_newfstatat),
1287 "statx" => Some(libc::SYS_statx),
1288 "faccessat" => Some(libc::SYS_faccessat),
1289 "faccessat2" => Some(libc::SYS_faccessat2),
1290 "dup" => Some(libc::SYS_dup),
1291 #[cfg(target_arch = "x86_64")]
1292 "dup2" => Some(libc::SYS_dup2),
1293 "dup3" => Some(libc::SYS_dup3),
1294 #[cfg(target_arch = "x86_64")]
1295 "pipe" => Some(libc::SYS_pipe),
1296 "pipe2" => Some(libc::SYS_pipe2),
1297 #[cfg(target_arch = "x86_64")]
1298 "unlink" => Some(libc::SYS_unlink),
1299 "unlinkat" => Some(libc::SYS_unlinkat),
1300 #[cfg(target_arch = "x86_64")]
1301 "rename" => Some(libc::SYS_rename),
1302 "renameat" => Some(libc::SYS_renameat),
1303 "renameat2" => Some(libc::SYS_renameat2),
1304 #[cfg(target_arch = "x86_64")]
1305 "link" => Some(libc::SYS_link),
1306 "linkat" => Some(libc::SYS_linkat),
1307 #[cfg(target_arch = "x86_64")]
1308 "symlink" => Some(libc::SYS_symlink),
1309 "symlinkat" => Some(libc::SYS_symlinkat),
1310 #[cfg(target_arch = "x86_64")]
1311 "chmod" => Some(libc::SYS_chmod),
1312 "fchmod" => Some(libc::SYS_fchmod),
1313 "fchmodat" => Some(libc::SYS_fchmodat),
1314 "truncate" => Some(libc::SYS_truncate),
1315 "ftruncate" => Some(libc::SYS_ftruncate),
1316 "fallocate" => Some(libc::SYS_fallocate),
1317 #[cfg(any(
1318 target_arch = "x86_64",
1319 target_arch = "aarch64",
1320 target_arch = "riscv64"
1321 ))]
1322 "fadvise64" => Some(SYS_FADVISE64),
1323 "fsync" => Some(libc::SYS_fsync),
1324 "fdatasync" => Some(libc::SYS_fdatasync),
1325 "flock" => Some(libc::SYS_flock),
1326 #[cfg(any(
1327 target_arch = "x86_64",
1328 target_arch = "aarch64",
1329 target_arch = "riscv64"
1330 ))]
1331 "sendfile" => Some(SYS_SENDFILE),
1332 "copy_file_range" => Some(libc::SYS_copy_file_range),
1333 "splice" => Some(libc::SYS_splice),
1334 "tee" => Some(libc::SYS_tee),
1335 "mmap" => Some(libc::SYS_mmap),
1337 "munmap" => Some(libc::SYS_munmap),
1338 "mprotect" => Some(libc::SYS_mprotect),
1339 "brk" => Some(libc::SYS_brk),
1340 "mremap" => Some(libc::SYS_mremap),
1341 "madvise" => Some(libc::SYS_madvise),
1342 "msync" => Some(libc::SYS_msync),
1343 "mlock" => Some(libc::SYS_mlock),
1344 "mlock2" => Some(libc::SYS_mlock2),
1345 "munlock" => Some(libc::SYS_munlock),
1346 "shmget" => Some(libc::SYS_shmget),
1348 "shmat" => Some(libc::SYS_shmat),
1349 "shmdt" => Some(libc::SYS_shmdt),
1350 "shmctl" => Some(libc::SYS_shmctl),
1351 "semget" => Some(libc::SYS_semget),
1352 "semop" => Some(libc::SYS_semop),
1353 "semctl" => Some(libc::SYS_semctl),
1354 "semtimedop" => Some(libc::SYS_semtimedop),
1355 #[cfg(target_arch = "x86_64")]
1357 "fork" => Some(libc::SYS_fork),
1358 "clone" => Some(libc::SYS_clone),
1359 "clone3" => Some(libc::SYS_clone3),
1360 "execve" => Some(libc::SYS_execve),
1361 "execveat" => Some(libc::SYS_execveat),
1362 "wait4" => Some(libc::SYS_wait4),
1363 "waitid" => Some(libc::SYS_waitid),
1364 "exit" => Some(libc::SYS_exit),
1365 "exit_group" => Some(libc::SYS_exit_group),
1366 "getpid" => Some(libc::SYS_getpid),
1367 "gettid" => Some(libc::SYS_gettid),
1368 "getuid" => Some(libc::SYS_getuid),
1369 "getgid" => Some(libc::SYS_getgid),
1370 "geteuid" => Some(libc::SYS_geteuid),
1371 "getegid" => Some(libc::SYS_getegid),
1372 "getppid" => Some(libc::SYS_getppid),
1373 #[cfg(target_arch = "x86_64")]
1374 "getpgrp" => Some(libc::SYS_getpgrp),
1375 "setsid" => Some(libc::SYS_setsid),
1376 "getgroups" => Some(libc::SYS_getgroups),
1377 "rt_sigaction" => Some(libc::SYS_rt_sigaction),
1379 "rt_sigprocmask" => Some(libc::SYS_rt_sigprocmask),
1380 "rt_sigreturn" => Some(libc::SYS_rt_sigreturn),
1381 "rt_sigsuspend" => Some(libc::SYS_rt_sigsuspend),
1382 "rt_sigtimedwait" => Some(libc::SYS_rt_sigtimedwait),
1383 "rt_sigpending" => Some(libc::SYS_rt_sigpending),
1384 "rt_sigqueueinfo" => Some(libc::SYS_rt_sigqueueinfo),
1385 "sigaltstack" => Some(libc::SYS_sigaltstack),
1386 "restart_syscall" => Some(libc::SYS_restart_syscall),
1387 "kill" => Some(libc::SYS_kill),
1388 "tgkill" => Some(libc::SYS_tgkill),
1389 "clock_gettime" => Some(libc::SYS_clock_gettime),
1391 "clock_getres" => Some(libc::SYS_clock_getres),
1392 "clock_nanosleep" => Some(libc::SYS_clock_nanosleep),
1393 "gettimeofday" => Some(libc::SYS_gettimeofday),
1394 "nanosleep" => Some(libc::SYS_nanosleep),
1395 "getcwd" => Some(libc::SYS_getcwd),
1397 "chdir" => Some(libc::SYS_chdir),
1398 "fchdir" => Some(libc::SYS_fchdir),
1399 #[cfg(target_arch = "x86_64")]
1400 "mkdir" => Some(libc::SYS_mkdir),
1401 "mkdirat" => Some(libc::SYS_mkdirat),
1402 #[cfg(target_arch = "x86_64")]
1403 "rmdir" => Some(libc::SYS_rmdir),
1404 #[cfg(target_arch = "x86_64")]
1405 "getdents" => Some(libc::SYS_getdents),
1406 "getdents64" => Some(libc::SYS_getdents64),
1407 "socket" => Some(libc::SYS_socket),
1409 "connect" => Some(libc::SYS_connect),
1410 "sendto" => Some(libc::SYS_sendto),
1411 "recvfrom" => Some(libc::SYS_recvfrom),
1412 "sendmsg" => Some(libc::SYS_sendmsg),
1413 "recvmsg" => Some(libc::SYS_recvmsg),
1414 "shutdown" => Some(libc::SYS_shutdown),
1415 "bind" => Some(libc::SYS_bind),
1416 "listen" => Some(libc::SYS_listen),
1417 "accept" => Some(libc::SYS_accept),
1418 "accept4" => Some(libc::SYS_accept4),
1419 "setsockopt" => Some(libc::SYS_setsockopt),
1420 "getsockopt" => Some(libc::SYS_getsockopt),
1421 "getsockname" => Some(libc::SYS_getsockname),
1422 "getpeername" => Some(libc::SYS_getpeername),
1423 "socketpair" => Some(libc::SYS_socketpair),
1424 #[cfg(target_arch = "x86_64")]
1426 "poll" => Some(libc::SYS_poll),
1427 "ppoll" => Some(libc::SYS_ppoll),
1428 #[cfg(target_arch = "x86_64")]
1429 "select" => Some(libc::SYS_select),
1430 "pselect6" => Some(libc::SYS_pselect6),
1431 #[cfg(target_arch = "x86_64")]
1432 "epoll_create" => Some(libc::SYS_epoll_create),
1433 "epoll_create1" => Some(libc::SYS_epoll_create1),
1434 "epoll_ctl" => Some(libc::SYS_epoll_ctl),
1435 #[cfg(target_arch = "x86_64")]
1436 "epoll_wait" => Some(libc::SYS_epoll_wait),
1437 "epoll_pwait" => Some(libc::SYS_epoll_pwait),
1438 #[cfg(target_arch = "x86_64")]
1439 "eventfd" => Some(libc::SYS_eventfd),
1440 "eventfd2" => Some(libc::SYS_eventfd2),
1441 #[cfg(target_arch = "x86_64")]
1442 "signalfd" => Some(libc::SYS_signalfd),
1443 "signalfd4" => Some(libc::SYS_signalfd4),
1444 "timerfd_create" => Some(libc::SYS_timerfd_create),
1445 "timerfd_settime" => Some(libc::SYS_timerfd_settime),
1446 "timerfd_gettime" => Some(libc::SYS_timerfd_gettime),
1447 "uname" => Some(libc::SYS_uname),
1449 "getrandom" => Some(libc::SYS_getrandom),
1450 "futex" => Some(libc::SYS_futex),
1451 "set_tid_address" => Some(libc::SYS_set_tid_address),
1452 "set_robust_list" => Some(libc::SYS_set_robust_list),
1453 "get_robust_list" => Some(libc::SYS_get_robust_list),
1454 #[cfg(target_arch = "x86_64")]
1455 "arch_prctl" => Some(libc::SYS_arch_prctl),
1456 "sysinfo" => Some(libc::SYS_sysinfo),
1457 "umask" => Some(libc::SYS_umask),
1458 #[cfg(target_arch = "x86_64")]
1459 "getrlimit" => Some(libc::SYS_getrlimit),
1460 "prlimit64" => Some(libc::SYS_prlimit64),
1461 "getrusage" => Some(libc::SYS_getrusage),
1462 "times" => Some(libc::SYS_times),
1463 "sched_yield" => Some(libc::SYS_sched_yield),
1464 "sched_getaffinity" => Some(libc::SYS_sched_getaffinity),
1465 "getcpu" => Some(libc::SYS_getcpu),
1466 "rseq" => Some(libc::SYS_rseq),
1467 "close_range" => Some(libc::SYS_close_range),
1468 "fchown" => Some(libc::SYS_fchown),
1470 "fchownat" => Some(libc::SYS_fchownat),
1471 #[cfg(target_arch = "x86_64")]
1472 "chown" => Some(libc::SYS_chown),
1473 #[cfg(target_arch = "x86_64")]
1474 "lchown" => Some(libc::SYS_lchown),
1475 "io_uring_setup" => Some(libc::SYS_io_uring_setup),
1477 "io_uring_enter" => Some(libc::SYS_io_uring_enter),
1478 "io_uring_register" => Some(libc::SYS_io_uring_register),
1479 "io_setup" => Some(libc::SYS_io_setup),
1481 "io_destroy" => Some(libc::SYS_io_destroy),
1482 "io_submit" => Some(libc::SYS_io_submit),
1483 "io_getevents" => Some(libc::SYS_io_getevents),
1484 "setitimer" => Some(libc::SYS_setitimer),
1486 "getitimer" => Some(libc::SYS_getitimer),
1487 "setpgid" => Some(libc::SYS_setpgid),
1489 "getpgid" => Some(libc::SYS_getpgid),
1490 "memfd_create" => Some(libc::SYS_memfd_create),
1491 "ioctl" => Some(libc::SYS_ioctl),
1492 "prctl" => Some(libc::SYS_prctl),
1493 "landlock_create_ruleset" => Some(libc::SYS_landlock_create_ruleset),
1495 "landlock_add_rule" => Some(libc::SYS_landlock_add_rule),
1496 "landlock_restrict_self" => Some(libc::SYS_landlock_restrict_self),
1497 "mincore" => Some(libc::SYS_mincore),
1500 "mlockall" => Some(libc::SYS_mlockall),
1501 "munlockall" => Some(libc::SYS_munlockall),
1502 "mbind" => Some(libc::SYS_mbind),
1503 "set_mempolicy" => Some(libc::SYS_set_mempolicy),
1504 "get_mempolicy" => Some(libc::SYS_get_mempolicy),
1505 "memfd_secret" => Some(libc::SYS_memfd_secret),
1506 "membarrier" => Some(libc::SYS_membarrier),
1507 "process_madvise" => Some(libc::SYS_process_madvise),
1508 "pkey_mprotect" => Some(libc::SYS_pkey_mprotect),
1509 "pkey_alloc" => Some(libc::SYS_pkey_alloc),
1510 "pkey_free" => Some(libc::SYS_pkey_free),
1511 "mseal" => Some(libc::SYS_mseal),
1512 "map_shadow_stack" => Some(453),
1513 "remap_file_pages" => Some(libc::SYS_remap_file_pages),
1514 "set_mempolicy_home_node" => Some(libc::SYS_set_mempolicy_home_node),
1515 "cachestat" => Some(451),
1516 #[cfg(target_arch = "x86_64")]
1518 "vfork" => Some(libc::SYS_vfork),
1519 #[cfg(target_arch = "x86_64")]
1520 "pause" => Some(libc::SYS_pause),
1521 #[cfg(target_arch = "x86_64")]
1522 "alarm" => Some(libc::SYS_alarm),
1523 "tkill" => Some(libc::SYS_tkill),
1524 "ptrace" => Some(libc::SYS_ptrace),
1525 "process_vm_readv" => Some(libc::SYS_process_vm_readv),
1526 "process_vm_writev" => Some(libc::SYS_process_vm_writev),
1527 "process_mrelease" => Some(libc::SYS_process_mrelease),
1528 "kcmp" => Some(libc::SYS_kcmp),
1529 "unshare" => Some(libc::SYS_unshare),
1530 "setns" => Some(libc::SYS_setns),
1531 "pidfd_open" => Some(libc::SYS_pidfd_open),
1532 "pidfd_send_signal" => Some(libc::SYS_pidfd_send_signal),
1533 "pidfd_getfd" => Some(libc::SYS_pidfd_getfd),
1534 "setuid" => Some(libc::SYS_setuid),
1536 "setgid" => Some(libc::SYS_setgid),
1537 "setreuid" => Some(libc::SYS_setreuid),
1538 "setregid" => Some(libc::SYS_setregid),
1539 "setresuid" => Some(libc::SYS_setresuid),
1540 "getresuid" => Some(libc::SYS_getresuid),
1541 "setresgid" => Some(libc::SYS_setresgid),
1542 "getresgid" => Some(libc::SYS_getresgid),
1543 "setfsuid" => Some(libc::SYS_setfsuid),
1544 "setfsgid" => Some(libc::SYS_setfsgid),
1545 "setgroups" => Some(libc::SYS_setgroups),
1546 "getsid" => Some(libc::SYS_getsid),
1547 "capget" => Some(libc::SYS_capget),
1549 "capset" => Some(libc::SYS_capset),
1550 "rt_tgsigqueueinfo" => Some(libc::SYS_rt_tgsigqueueinfo),
1552 "msgget" => Some(libc::SYS_msgget),
1554 "msgsnd" => Some(libc::SYS_msgsnd),
1555 "msgrcv" => Some(libc::SYS_msgrcv),
1556 "msgctl" => Some(libc::SYS_msgctl),
1557 "timer_create" => Some(libc::SYS_timer_create),
1559 "timer_settime" => Some(libc::SYS_timer_settime),
1560 "timer_gettime" => Some(libc::SYS_timer_gettime),
1561 "timer_getoverrun" => Some(libc::SYS_timer_getoverrun),
1562 "timer_delete" => Some(libc::SYS_timer_delete),
1563 "clock_settime" => Some(libc::SYS_clock_settime),
1564 "clock_adjtime" => Some(libc::SYS_clock_adjtime),
1565 #[cfg(target_arch = "x86_64")]
1566 "time" => Some(libc::SYS_time),
1567 #[cfg(target_arch = "x86_64")]
1569 "creat" => Some(libc::SYS_creat),
1570 "readahead" => Some(libc::SYS_readahead),
1571 "sync" => Some(libc::SYS_sync),
1572 "syncfs" => Some(libc::SYS_syncfs),
1573 "vmsplice" => Some(libc::SYS_vmsplice),
1574 "utimensat" => Some(libc::SYS_utimensat),
1575 #[cfg(target_arch = "x86_64")]
1576 "utimes" => Some(libc::SYS_utimes),
1577 #[cfg(target_arch = "x86_64")]
1578 "utime" => Some(libc::SYS_utime),
1579 #[cfg(target_arch = "x86_64")]
1580 "futimesat" => Some(libc::SYS_futimesat),
1581 "openat2" => Some(libc::SYS_openat2),
1582 "name_to_handle_at" => Some(libc::SYS_name_to_handle_at),
1583 "open_by_handle_at" => Some(libc::SYS_open_by_handle_at),
1584 "fchmodat2" => Some(libc::SYS_fchmodat2),
1585 "statmount" => Some(457),
1586 "listmount" => Some(458),
1587 "setxattr" => Some(libc::SYS_setxattr),
1589 "lsetxattr" => Some(libc::SYS_lsetxattr),
1590 "fsetxattr" => Some(libc::SYS_fsetxattr),
1591 "removexattr" => Some(libc::SYS_removexattr),
1592 "lremovexattr" => Some(libc::SYS_lremovexattr),
1593 "fremovexattr" => Some(libc::SYS_fremovexattr),
1594 "setxattrat" => Some(463),
1595 "getxattrat" => Some(464),
1596 "listxattrat" => Some(465),
1597 "removexattrat" => Some(466),
1598 "recvmmsg" => Some(libc::SYS_recvmmsg),
1600 "sendmmsg" => Some(libc::SYS_sendmmsg),
1601 #[cfg(target_arch = "x86_64")]
1603 "inotify_init" => Some(libc::SYS_inotify_init),
1604 "inotify_init1" => Some(libc::SYS_inotify_init1),
1605 "inotify_add_watch" => Some(libc::SYS_inotify_add_watch),
1606 "inotify_rm_watch" => Some(libc::SYS_inotify_rm_watch),
1607 "fanotify_init" => Some(libc::SYS_fanotify_init),
1609 "fanotify_mark" => Some(libc::SYS_fanotify_mark),
1610 "epoll_pwait2" => Some(libc::SYS_epoll_pwait2),
1612 "sched_setparam" => Some(libc::SYS_sched_setparam),
1614 "sched_setscheduler" => Some(libc::SYS_sched_setscheduler),
1615 "sched_get_priority_max" => Some(libc::SYS_sched_get_priority_max),
1616 "sched_get_priority_min" => Some(libc::SYS_sched_get_priority_min),
1617 "sched_rr_get_interval" => Some(libc::SYS_sched_rr_get_interval),
1618 "sched_setattr" => Some(libc::SYS_sched_setattr),
1619 "sched_getattr" => Some(libc::SYS_sched_getattr),
1620 "sched_setaffinity" => Some(libc::SYS_sched_setaffinity),
1621 #[cfg(target_arch = "x86_64")]
1623 "setrlimit" => Some(libc::SYS_setrlimit),
1624 "getpriority" => Some(libc::SYS_getpriority),
1625 "setpriority" => Some(libc::SYS_setpriority),
1626 "ioprio_set" => Some(libc::SYS_ioprio_set),
1627 "ioprio_get" => Some(libc::SYS_ioprio_get),
1628 "futex_waitv" => Some(libc::SYS_futex_waitv),
1630 "futex_wake" => Some(454),
1631 "futex_wait" => Some(455),
1632 "futex_requeue" => Some(456),
1633 "init_module" => Some(libc::SYS_init_module),
1635 "finit_module" => Some(libc::SYS_finit_module),
1636 "delete_module" => Some(libc::SYS_delete_module),
1637 "bpf" => Some(libc::SYS_bpf),
1639 "perf_event_open" => Some(libc::SYS_perf_event_open),
1640 "seccomp" => Some(libc::SYS_seccomp),
1642 "userfaultfd" => Some(libc::SYS_userfaultfd),
1644 "mount" => Some(libc::SYS_mount),
1646 "umount2" => Some(libc::SYS_umount2),
1647 "pivot_root" => Some(libc::SYS_pivot_root),
1648 "mount_setattr" => Some(libc::SYS_mount_setattr),
1649 "open_tree" => Some(libc::SYS_open_tree),
1650 "open_tree_attr" => Some(467),
1651 "move_mount" => Some(libc::SYS_move_mount),
1652 "fsopen" => Some(libc::SYS_fsopen),
1653 "fsconfig" => Some(libc::SYS_fsconfig),
1654 "fsmount" => Some(libc::SYS_fsmount),
1655 "fspick" => Some(libc::SYS_fspick),
1656 "syslog" => Some(libc::SYS_syslog),
1658 "reboot" => Some(libc::SYS_reboot),
1659 "swapon" => Some(libc::SYS_swapon),
1660 "swapoff" => Some(libc::SYS_swapoff),
1661 "chroot" => Some(libc::SYS_chroot),
1662 "acct" => Some(libc::SYS_acct),
1663 "settimeofday" => Some(libc::SYS_settimeofday),
1664 "sethostname" => Some(libc::SYS_sethostname),
1665 "setdomainname" => Some(libc::SYS_setdomainname),
1666 "adjtimex" => Some(libc::SYS_adjtimex),
1667 #[cfg(target_arch = "x86_64")]
1668 "modify_ldt" => Some(libc::SYS_modify_ldt),
1669 #[cfg(target_arch = "x86_64")]
1670 "iopl" => Some(libc::SYS_iopl),
1671 #[cfg(target_arch = "x86_64")]
1672 "ioperm" => Some(libc::SYS_ioperm),
1673 "quotactl" => Some(libc::SYS_quotactl),
1674 "quotactl_fd" => Some(libc::SYS_quotactl_fd),
1675 "personality" => Some(libc::SYS_personality),
1676 "vhangup" => Some(libc::SYS_vhangup),
1677 #[cfg(target_arch = "x86_64")]
1678 "ustat" => Some(libc::SYS_ustat),
1679 #[cfg(target_arch = "x86_64")]
1680 "sysfs" => Some(libc::SYS_sysfs),
1681 "mknod" => Some(libc::SYS_mknod),
1682 "mknodat" => Some(libc::SYS_mknodat),
1683 "migrate_pages" => Some(libc::SYS_migrate_pages),
1684 "move_pages" => Some(libc::SYS_move_pages),
1685 #[cfg(target_arch = "x86_64")]
1686 "kexec_load" => Some(libc::SYS_kexec_load),
1687 "kexec_file_load" => Some(libc::SYS_kexec_file_load),
1688 "mq_open" => Some(libc::SYS_mq_open),
1690 "mq_unlink" => Some(libc::SYS_mq_unlink),
1691 "mq_timedsend" => Some(libc::SYS_mq_timedsend),
1692 "mq_timedreceive" => Some(libc::SYS_mq_timedreceive),
1693 "mq_notify" => Some(libc::SYS_mq_notify),
1694 "mq_getsetattr" => Some(libc::SYS_mq_getsetattr),
1695 "add_key" => Some(libc::SYS_add_key),
1697 "request_key" => Some(libc::SYS_request_key),
1698 "keyctl" => Some(libc::SYS_keyctl),
1699 "io_pgetevents" => Some(333),
1701 "lsm_get_self_attr" => Some(459),
1703 "lsm_set_self_attr" => Some(460),
1704 "lsm_list_modules" => Some(461),
1705 #[cfg(target_arch = "x86_64")]
1706 "lookup_dcookie" => Some(libc::SYS_lookup_dcookie),
1707 "uretprobe" => Some(335),
1708 _ => None,
1709 }
1710}
1711
1712impl Default for SeccompManager {
1713 fn default() -> Self {
1714 Self::new()
1715 }
1716}
1717
1718#[cfg(test)]
1719mod tests {
1720 use super::*;
1721
1722 #[test]
1723 fn test_seccomp_manager_initial_state() {
1724 let mgr = SeccompManager::new();
1725 assert!(!mgr.is_applied());
1726 }
1727
1728 #[test]
1729 fn test_apply_idempotent() {
1730 let mgr = SeccompManager::new();
1731 assert!(!mgr.is_applied());
1735 }
1736
1737 #[test]
1738 fn test_clone_denied_flags_include_newcgroup() {
1739 assert_ne!(
1740 DENIED_CLONE_NAMESPACE_FLAGS & libc::CLONE_NEWCGROUP as u64,
1741 0
1742 );
1743 }
1744
1745 #[test]
1746 fn test_clone_denied_flags_include_newtime() {
1747 assert_ne!(
1748 DENIED_CLONE_NAMESPACE_FLAGS & libc::CLONE_NEWTIME as u64,
1749 0,
1750 "CLONE_NEWTIME must be in denied clone namespace flags"
1751 );
1752 }
1753
1754 #[test]
1755 fn test_network_none_socket_domains_are_unix_only() {
1756 let domains = SeccompManager::allowed_socket_domains(false);
1757 assert_eq!(domains, vec![libc::AF_UNIX]);
1758 }
1759
1760 #[test]
1761 fn test_network_enabled_socket_domains_exclude_netlink() {
1762 let domains = SeccompManager::allowed_socket_domains(true);
1763 assert!(domains.contains(&libc::AF_UNIX));
1764 assert!(domains.contains(&libc::AF_INET));
1765 assert!(domains.contains(&libc::AF_INET6));
1766 assert!(!domains.contains(&libc::AF_NETLINK));
1767 }
1768
1769 #[test]
1770 fn test_network_mode_syscalls_only_enabled_when_network_allowed() {
1771 let none = SeccompManager::network_mode_syscalls(false);
1772 assert!(none.is_empty());
1773
1774 let enabled = SeccompManager::network_mode_syscalls(true);
1775 assert!(enabled.contains(&libc::SYS_bind));
1776 assert!(enabled.contains(&libc::SYS_listen));
1777 assert!(enabled.contains(&libc::SYS_accept));
1778 assert!(enabled.contains(&libc::SYS_setsockopt));
1779 assert!(
1780 SeccompManager::base_allowed_syscalls().contains(&libc::SYS_connect),
1781 "connect is allowed for AF_UNIX probes; network sockets are blocked by socket() domain filtering"
1782 );
1783 }
1784
1785 #[test]
1786 fn test_landlock_bootstrap_syscalls_present_in_base_allowlist() {
1787 let base = SeccompManager::base_allowed_syscalls();
1788 assert!(base.contains(&libc::SYS_landlock_create_ruleset));
1789 assert!(base.contains(&libc::SYS_landlock_add_rule));
1790 assert!(base.contains(&libc::SYS_landlock_restrict_self));
1791 }
1792
1793 #[test]
1794 fn test_process_identity_syscalls_present_in_base_allowlist() {
1795 let base = SeccompManager::base_allowed_syscalls();
1796 assert!(base.contains(&libc::SYS_getuid));
1797 assert!(base.contains(&libc::SYS_getgid));
1798 assert!(base.contains(&libc::SYS_geteuid));
1799 assert!(base.contains(&libc::SYS_getegid));
1800 assert!(base.contains(&libc::SYS_getresuid));
1801 assert!(base.contains(&libc::SYS_getresgid));
1802 }
1803
1804 #[cfg(any(
1805 target_arch = "x86_64",
1806 target_arch = "aarch64",
1807 target_arch = "riscv64"
1808 ))]
1809 #[test]
1810 fn test_generic_file_syscalls_present_in_base_allowlist() {
1811 let base = SeccompManager::base_allowed_syscalls();
1812 assert!(
1813 base.contains(&SYS_FADVISE64),
1814 "fadvise64 must be allowed on architectures with a generic syscall table"
1815 );
1816 assert!(
1817 base.contains(&SYS_SENDFILE),
1818 "sendfile must be allowed on architectures with a generic syscall table"
1819 );
1820 }
1821
1822 #[cfg(any(
1823 target_arch = "x86_64",
1824 target_arch = "aarch64",
1825 target_arch = "riscv64"
1826 ))]
1827 #[test]
1828 fn test_generic_file_syscall_names_resolve_for_profiles() {
1829 assert_eq!(syscall_name_to_number("fadvise64"), Some(SYS_FADVISE64));
1830 assert_eq!(syscall_name_to_number("sendfile"), Some(SYS_SENDFILE));
1831 }
1832
1833 #[test]
1834 fn test_x32_legacy_range_not_allowlisted() {
1835 let base = SeccompManager::base_allowed_syscalls();
1836 let net = SeccompManager::network_mode_syscalls(true);
1837 for nr in 512_i64..=547_i64 {
1838 assert!(
1839 !base.contains(&nr) && !net.contains(&nr),
1840 "x32 syscall number {} unexpectedly allowlisted",
1841 nr
1842 );
1843 }
1844 }
1845
1846 #[test]
1847 fn test_i386_compat_socketcall_range_not_allowlisted() {
1848 let base = SeccompManager::base_allowed_syscalls();
1849 let net = SeccompManager::network_mode_syscalls(true);
1850 for nr in 359_i64..=373_i64 {
1853 assert!(
1854 !base.contains(&nr) && !net.contains(&nr),
1855 "i386 compat syscall number {} unexpectedly allowlisted",
1856 nr
1857 );
1858 }
1859 }
1860
1861 #[test]
1862 fn test_minimal_filter_allowlist_counts_are_stable() {
1863 let base = SeccompManager::base_allowed_syscalls();
1864 let net = SeccompManager::network_mode_syscalls(true);
1865
1866 assert_eq!(base.len(), 174);
1874 assert_eq!(net.len(), 10);
1875 assert_eq!(base.len() + 9, 183);
1876 assert_eq!(base.len() + net.len() + 9, 193);
1877 }
1878
1879 #[test]
1880 fn test_trace_filter_compiles() {
1881 SeccompManager::compile_trace_filter().expect("trace filter must compile");
1882 }
1883
1884 #[test]
1885 fn test_arg_filtered_syscalls_list_includes_critical_syscalls() {
1886 for name in &[
1889 "clone", "execveat", "ioctl", "preadv2", "prctl", "pwritev2", "socket",
1890 ] {
1891 assert!(
1892 SeccompManager::ARG_FILTERED_SYSCALLS.contains(name),
1893 "'{}' must be in ARG_FILTERED_SYSCALLS",
1894 name
1895 );
1896 }
1897 }
1898
1899 #[test]
1900 fn test_clone3_not_allowlisted_in_minimal_filter() {
1901 let rules = SeccompManager::minimal_filter(true, &[]).unwrap();
1905 assert!(
1906 !rules.contains_key(&libc::SYS_clone3),
1907 "clone3 must not be in the seccomp allowlist"
1908 );
1909 assert!(
1910 SeccompManager::errno_denied_syscalls()
1911 .iter()
1912 .any(|(nr, errno)| *nr == libc::SYS_clone3 && *errno == libc::ENOSYS as u32),
1913 "clone3 must be denied with ENOSYS to trigger libc fallback"
1914 );
1915 }
1916
1917 #[test]
1918 fn test_clone_is_allowed_with_arg_filter() {
1919 let rules = SeccompManager::minimal_filter(true, &[]).unwrap();
1921 assert!(
1922 rules.contains_key(&libc::SYS_clone),
1923 "clone must be in the seccomp allowlist with arg filters"
1924 );
1925 }
1926
1927 #[test]
1928 fn test_high_risk_syscalls_removed_from_base_allowlist() {
1929 let base = SeccompManager::base_allowed_syscalls();
1930 let removed = [
1933 libc::SYS_sync,
1934 libc::SYS_syncfs,
1935 libc::SYS_mincore,
1936 libc::SYS_vfork,
1937 libc::SYS_tkill,
1938 libc::SYS_io_uring_setup,
1940 libc::SYS_io_uring_enter,
1941 libc::SYS_io_uring_register,
1942 ];
1943
1944 for syscall in removed {
1945 assert!(
1946 !base.contains(&syscall),
1947 "syscall {} unexpectedly present in base allowlist",
1948 syscall
1949 );
1950 }
1951 }
1952
1953 #[test]
1954 fn test_custom_profile_preserves_clone_arg_filters() {
1955 let rules = SeccompManager::minimal_filter(true, &[]).unwrap();
1960
1961 for name in SeccompManager::ARG_FILTERED_SYSCALLS {
1964 if let Some(nr) = syscall_name_to_number(name) {
1965 let entry = rules.get(&nr);
1966 assert!(
1967 entry.is_some() && !entry.unwrap().is_empty(),
1968 "built-in filter must have argument-level rules for '{}' \
1969 so apply_profile_from_file can merge them into custom profiles",
1970 name
1971 );
1972 }
1973 }
1974 }
1975
1976 #[test]
1977 fn test_memfd_create_not_in_default_allowlist() {
1978 let base = SeccompManager::base_allowed_syscalls();
1980 assert!(
1981 !base.contains(&libc::SYS_memfd_create),
1982 "memfd_create must not be in the default seccomp allowlist (fileless exec risk)"
1983 );
1984 let rules = SeccompManager::minimal_filter(true, &[]).unwrap();
1986 assert!(
1987 !rules.contains_key(&libc::SYS_memfd_create),
1988 "memfd_create must not be in the compiled seccomp filter rules"
1989 );
1990 }
1991
1992 #[test]
1993 fn test_mprotect_has_arg_filtering() {
1994 let base = SeccompManager::base_allowed_syscalls();
1999 assert!(
2000 !base.contains(&libc::SYS_mprotect),
2001 "SYS_mprotect must not be unconditionally allowed - needs arg filtering"
2002 );
2003
2004 let rules = SeccompManager::minimal_filter(true, &[]).unwrap();
2007 let mprotect_rules = rules.get(&libc::SYS_mprotect);
2008 assert!(
2009 mprotect_rules.is_some(),
2010 "mprotect must be present in the seccomp filter rules"
2011 );
2012 assert!(
2013 !mprotect_rules.unwrap().is_empty(),
2014 "mprotect must have argument-level conditions to prevent W^X violations"
2015 );
2016 }
2017
2018 #[test]
2019 fn test_preadv2_pwritev2_have_flags_arg_filtering() {
2020 let base = SeccompManager::base_allowed_syscalls();
2024 let rules = SeccompManager::minimal_filter(true, &[]).unwrap();
2025
2026 for (name, syscall) in [
2027 ("preadv2", libc::SYS_preadv2),
2028 ("pwritev2", libc::SYS_pwritev2),
2029 ] {
2030 assert!(
2031 !base.contains(&syscall),
2032 "{} must not be unconditionally allowed",
2033 name
2034 );
2035 assert!(
2036 rules.get(&syscall).is_some_and(|chain| !chain.is_empty()),
2037 "{} must have argument-level conditions",
2038 name
2039 );
2040 assert!(
2041 SeccompManager::ARG_FILTERED_SYSCALLS.contains(&name),
2042 "{} must be listed as argument-filtered for custom profiles",
2043 name
2044 );
2045 }
2046 }
2047
2048 #[test]
2049 fn test_unsafe_blocks_have_safety_comments() {
2050 let source = include_str!("seccomp.rs");
2052 let mut pos = 0;
2053 while let Some(idx) = source[pos..].find("unsafe {") {
2054 let abs_idx = pos + idx;
2055 let start = abs_idx.saturating_sub(200);
2057 let context = &source[start..abs_idx];
2058 assert!(
2059 context.contains("SAFETY:"),
2060 "unsafe block at byte {} must have a // SAFETY: comment. Context: ...{}...",
2061 abs_idx,
2062 &source[abs_idx.saturating_sub(80)..abs_idx + 10]
2063 );
2064 pos = abs_idx + 1;
2065 }
2066 }
2067
2068 fn mprotect_would_allow(prot: u64) -> bool {
2078 let mask = (libc::PROT_WRITE | libc::PROT_EXEC) as u64;
2079 let allowed_values: &[u64] = &[0, libc::PROT_WRITE as u64, libc::PROT_EXEC as u64];
2080 let masked = prot & mask;
2081 allowed_values.contains(&masked)
2082 }
2083
2084 #[test]
2085 fn test_mprotect_allows_prot_none() {
2086 assert!(mprotect_would_allow(0), "PROT_NONE must be allowed");
2087 }
2088
2089 #[test]
2090 fn test_mprotect_allows_prot_read_only() {
2091 assert!(
2092 mprotect_would_allow(libc::PROT_READ as u64),
2093 "PROT_READ must be allowed (W|X bits are 0)"
2094 );
2095 }
2096
2097 #[test]
2098 fn test_mprotect_allows_prot_read_write() {
2099 assert!(
2100 mprotect_would_allow((libc::PROT_READ | libc::PROT_WRITE) as u64),
2101 "PROT_READ|PROT_WRITE must be allowed"
2102 );
2103 }
2104
2105 #[test]
2106 fn test_mprotect_allows_prot_read_exec() {
2107 assert!(
2108 mprotect_would_allow((libc::PROT_READ | libc::PROT_EXEC) as u64),
2109 "PROT_READ|PROT_EXEC must be allowed"
2110 );
2111 }
2112
2113 #[test]
2114 fn test_mprotect_rejects_prot_write_exec() {
2115 assert!(
2116 !mprotect_would_allow((libc::PROT_WRITE | libc::PROT_EXEC) as u64),
2117 "PROT_WRITE|PROT_EXEC (W^X violation) must be REJECTED"
2118 );
2119 }
2120
2121 #[test]
2122 fn test_mprotect_rejects_prot_read_write_exec() {
2123 assert!(
2124 !mprotect_would_allow((libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC) as u64),
2125 "PROT_READ|PROT_WRITE|PROT_EXEC (W^X violation) must be REJECTED"
2126 );
2127 }
2128
2129 #[test]
2130 fn test_mprotect_allows_prot_write_alone() {
2131 assert!(
2132 mprotect_would_allow(libc::PROT_WRITE as u64),
2133 "PROT_WRITE alone must be allowed"
2134 );
2135 }
2136
2137 #[test]
2138 fn test_mprotect_allows_prot_exec_alone() {
2139 assert!(
2140 mprotect_would_allow(libc::PROT_EXEC as u64),
2141 "PROT_EXEC alone must be allowed"
2142 );
2143 }
2144
2145 #[test]
2148 fn test_extra_syscalls_are_merged_into_filter() {
2149 let extra = vec!["io_uring_setup".to_string(), "sysinfo".to_string()];
2150 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2151 assert!(
2152 rules.contains_key(&libc::SYS_io_uring_setup),
2153 "io_uring_setup must be in filter when requested via extra_syscalls"
2154 );
2155 assert!(
2156 rules.contains_key(&libc::SYS_sysinfo),
2157 "sysinfo must be in filter when requested via extra_syscalls"
2158 );
2159 }
2160
2161 #[test]
2162 fn test_extra_syscalls_do_not_override_arg_filtered() {
2163 let extra = vec![
2167 "clone".to_string(),
2168 "preadv2".to_string(),
2169 "pwritev2".to_string(),
2170 ];
2171 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2172 for (name, syscall) in [
2173 ("clone", libc::SYS_clone),
2174 ("preadv2", libc::SYS_preadv2),
2175 ("pwritev2", libc::SYS_pwritev2),
2176 ] {
2177 assert!(
2178 rules.get(&syscall).is_some_and(|chain| !chain.is_empty()),
2179 "{} must retain argument-level filtering even when in extra_syscalls",
2180 name
2181 );
2182 }
2183 }
2184
2185 #[test]
2186 fn test_extra_syscalls_unknown_name_is_warned_and_skipped() {
2187 let extra = vec!["not_a_real_syscall".to_string()];
2189 let result = SeccompManager::minimal_filter(true, &extra);
2190 assert!(
2191 result.is_ok(),
2192 "Unknown syscall name should warn and skip, not error"
2193 );
2194 }
2195
2196 #[test]
2197 fn test_extra_syscalls_empty_is_noop() {
2198 let rules_without = SeccompManager::minimal_filter(true, &[]).unwrap();
2199 let rules_with = SeccompManager::minimal_filter(true, &[]).unwrap();
2200 assert_eq!(rules_without.len(), rules_with.len());
2201 }
2202
2203 #[test]
2204 fn test_extra_syscalls_duplicate_of_default_is_harmless() {
2205 let extra = vec!["read".to_string()];
2207 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2208 assert!(rules.contains_key(&libc::SYS_read));
2209 }
2210
2211 #[test]
2212 fn test_extra_syscalls_blocked_known_syscall_not_added() {
2213 let extra = vec!["kexec_load".to_string()];
2216 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2217 assert!(
2218 !rules.contains_key(&libc::SYS_kexec_load),
2219 "kexec_load must be blocked even when requested via --seccomp-allow"
2220 );
2221 }
2222
2223 #[test]
2224 fn test_extra_syscalls_unshare_remains_blocked() {
2225 let extra = vec!["unshare".to_string()];
2226 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2227 assert!(
2228 !rules.contains_key(&libc::SYS_unshare),
2229 "unshare must stay blocked even when requested via --seccomp-allow"
2230 );
2231 }
2232
2233 #[test]
2234 fn test_extra_syscalls_keyring_remain_blocked() {
2235 let extra = vec![
2236 "add_key".to_string(),
2237 "request_key".to_string(),
2238 "keyctl".to_string(),
2239 ];
2240 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2241
2242 for (name, syscall) in [
2243 ("add_key", libc::SYS_add_key),
2244 ("request_key", libc::SYS_request_key),
2245 ("keyctl", libc::SYS_keyctl),
2246 ] {
2247 assert!(
2248 !rules.contains_key(&syscall),
2249 "{} must stay blocked even when requested via --seccomp-allow",
2250 name
2251 );
2252 assert!(
2253 !SeccompManager::OPT_IN_SYSCALLS.contains(&name),
2254 "{} must not be in the seccomp opt-in allowlist",
2255 name
2256 );
2257 }
2258 }
2259
2260 #[test]
2261 fn test_security_critical_syscalls_remain_absent_from_filter() {
2262 let extra = SeccompManager::SECURITY_CRITICAL_DENIED_SYSCALLS
2263 .iter()
2264 .map(|name| (*name).to_string())
2265 .collect::<Vec<_>>();
2266 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2267
2268 for name in SeccompManager::SECURITY_CRITICAL_DENIED_SYSCALLS {
2269 let syscall = syscall_name_to_number(name).unwrap();
2270 assert!(
2271 !rules.contains_key(&syscall),
2272 "{} must not appear in the built-in filter even when requested via --seccomp-allow",
2273 name
2274 );
2275 assert!(
2276 !SeccompManager::OPT_IN_SYSCALLS.contains(name),
2277 "{} must not be in the seccomp opt-in allowlist",
2278 name
2279 );
2280 }
2281 }
2282
2283 #[test]
2284 fn test_extra_syscalls_clone3_remains_blocked() {
2285 let extra = vec!["clone3".to_string()];
2286 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2287 assert!(
2288 !rules.contains_key(&libc::SYS_clone3),
2289 "clone3 must stay out of the allowlist even when requested via --seccomp-allow"
2290 );
2291 assert!(
2292 SeccompManager::errno_denied_syscalls()
2293 .iter()
2294 .any(|(nr, _)| *nr == libc::SYS_clone3),
2295 "clone3 must remain covered by the exact ENOSYS deny"
2296 );
2297 }
2298
2299 #[test]
2300 fn test_extra_syscalls_opt_in_syscall_is_added() {
2301 let extra = vec!["io_uring_setup".to_string()];
2303 let rules = SeccompManager::minimal_filter(true, &extra).unwrap();
2304 assert!(
2305 rules.contains_key(&libc::SYS_io_uring_setup),
2306 "io_uring_setup is in OPT_IN_SYSCALLS and must be added"
2307 );
2308 }
2309
2310 #[test]
2311 fn test_production_validation_rejects_security_critical_extra_syscalls() {
2312 for name in [
2313 "clone3",
2314 "unshare",
2315 "setns",
2316 "add_key",
2317 "request_key",
2318 "keyctl",
2319 ] {
2320 let extra = vec![name.to_string()];
2321 let err =
2322 SeccompManager::validate_extra_syscalls_for_production(true, &extra).unwrap_err();
2323 assert!(err.to_string().contains("security-critical"));
2324 assert!(err.to_string().contains(name));
2325 }
2326 }
2327
2328 #[test]
2329 fn test_production_validation_rejects_unsupported_extra_syscalls() {
2330 let extra = vec!["kexec_load".to_string()];
2331 let err = SeccompManager::validate_extra_syscalls_for_production(true, &extra).unwrap_err();
2332 assert!(err.to_string().contains("unsupported"));
2333 assert!(err.to_string().contains("kexec_load"));
2334 }
2335
2336 #[test]
2337 fn test_production_validation_allows_supported_extra_syscalls() {
2338 let extra = vec![
2339 "read".to_string(),
2340 "clone".to_string(),
2341 "connect".to_string(),
2342 "io_uring_setup".to_string(),
2343 ];
2344 SeccompManager::validate_extra_syscalls_for_production(true, &extra).unwrap();
2345 }
2346}