Skip to main content

nucleus/security/
seccomp.rs

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
14/// Seccomp filter manager
15///
16/// Implements syscall whitelisting for the security state machine
17/// (NucleusSecurity_Seccomp_SeccompEnforcement.tla)
18pub 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            // File I/O
39            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            // Memory management
92            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            // SysV shared memory – used by PostgreSQL, Redis, and many databases
102            // for shared buffer pools. Safe in PID/IPC namespaces (isolated keyspace).
103            libc::SYS_shmget,
104            libc::SYS_shmat,
105            libc::SYS_shmdt,
106            libc::SYS_shmctl,
107            // POSIX semaphores (used by PostgreSQL for lightweight locking)
108            libc::SYS_semget,
109            libc::SYS_semop,
110            libc::SYS_semctl,
111            libc::SYS_semtimedop,
112            // Process management
113            // fork intentionally excluded – modern glibc/musl use clone(), which
114            // has namespace-flag filtering. Removing SYS_fork forces all forks
115            // through the filtered clone path (defense-in-depth against fork bombs
116            // and unfiltered namespace creation).
117            libc::SYS_execve,
118            // execveat is conditionally allowed below (AT_EMPTY_PATH blocked)
119            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            // Signals
135            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            // L7: kill/tgkill are safe when PID namespace is active (container
145            // can only signal its own processes). If PID namespace creation fails,
146            // the runtime aborts, so this is safe.
147            libc::SYS_kill,
148            libc::SYS_tgkill,
149            // Time and timers
150            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            // Directories
158            libc::SYS_getcwd,
159            libc::SYS_chdir,
160            libc::SYS_fchdir,
161            libc::SYS_mkdirat,
162            libc::SYS_getdents64,
163            // Misc
164            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            // L8: sysinfo removed – leaks host RAM, uptime, and process count.
171            // Applications needing this info should use /proc/meminfo instead.
172            libc::SYS_umask,
173            // prlimit64 moved to arg-filtered section (M3)
174            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            // Extended attributes – read-only queries, safe
183            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            // Ownership – safe after capability drop (CAP_CHOWN/CAP_FOWNER gone;
192            // operations on files not owned by the container UID will EPERM).
193            libc::SYS_fchown,
194            libc::SYS_fchownat,
195            // Legacy AIO – used by databases and storage engines. Operations are
196            // bounded by the process's existing fd permissions.
197            libc::SYS_io_setup,
198            libc::SYS_io_destroy,
199            libc::SYS_io_submit,
200            libc::SYS_io_getevents,
201            // NOTE: io_uring intentionally excluded from defaults – large kernel
202            // attack surface with a history of CVEs. Applications needing io_uring
203            // (e.g. PostgreSQL 18+ io_method=io_uring) should use a custom seccomp
204            // profile that adds io_uring_setup/io_uring_enter/io_uring_register.
205            // Process groups – safe in PID namespace (can only affect own pgrp).
206            libc::SYS_setpgid,
207            libc::SYS_getpgid,
208            // NOTE: memfd_create intentionally excluded – combined with execveat
209            // it enables fileless code execution bypassing all FS controls (SEC-02).
210            // Landlock bootstrap (runtime applies seccomp before Landlock)
211            libc::SYS_landlock_create_ruleset,
212            libc::SYS_landlock_add_rule,
213            libc::SYS_landlock_restrict_self,
214            // Socket/Network (safe introspection + local socketpair)
215            libc::SYS_getsockname,
216            libc::SYS_getpeername,
217            libc::SYS_socketpair,
218            libc::SYS_getsockopt,
219            // Safe with network mode disabled because socket(AF_INET/AF_INET6)
220            // remains blocked; this permits AF_UNIX probes such as nscd.
221            libc::SYS_connect,
222            // Poll/Select
223            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        // Legacy syscalls only available on x86_64 (aarch64 only has the *at variants)
236        #[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    /// Get minimal syscall whitelist for basic container operation
298    ///
299    /// This is a restrictive whitelist that blocks dangerous syscalls:
300    /// - ptrace (process tracing)
301    /// - kexec_load (kernel loading)
302    /// - add_key, request_key, keyctl (kernel keyring)
303    /// - bpf (eBPF programs)
304    /// - perf_event_open (performance monitoring)
305    /// - userfaultfd (user fault handling)
306    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        // Essential syscalls for basic operation
313        let allowed_syscalls = Self::base_allowed_syscalls();
314
315        // Allow all these syscalls unconditionally
316        for syscall in allowed_syscalls {
317            rules.insert(syscall, Vec::new());
318        }
319
320        // Add network-mode-specific syscalls
321        for syscall in Self::network_mode_syscalls(allow_network) {
322            rules.insert(syscall, Vec::new());
323        }
324
325        // Add user-requested extra syscalls (--seccomp-allow).
326        // - Already in default/arg-filtered: silently accepted (no-op).
327        // - In OPT_IN_SYSCALLS: added to allowlist.
328        // - Security-critical denied names: WARN and blocked even if later
329        //   accidentally added to OPT_IN_SYSCALLS.
330        // - Known but not opt-in: WARN and blocked (defense-in-depth).
331        // - Unknown name: WARN and blocked.
332        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        // Restrict socket() domains by network mode.
359        // none: AF_UNIX only; network-enabled: AF_UNIX/AF_INET/AF_INET6.
360        let mut socket_rules = Vec::new();
361        for domain in Self::allowed_socket_domains(allow_network) {
362            let condition = SeccompCondition::new(
363                0, // arg0 is socket(domain, type, protocol)
364                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        // ioctl: allow only safe terminal operations (arg0 = request code)
382        let ioctl_allowed: &[u64] = &[
383            0x5401,        // TCGETS
384            libc::TCGETS2, // termios2 read-only query used by modern shells
385            0x5402,        // TCSETS
386            0x5403,        // TCSETSW
387            0x5404,        // TCSETSF
388            0x540B,        // TCFLSH
389            0x540E,        // TIOCSCTTY
390            0x540F,        // TIOCGPGRP
391            0x5410,        // TIOCSPGRP
392            0x5413,        // TIOCGWINSZ
393            0x5429,        // TIOCGSID
394            0x541B,        // FIONREAD
395            0x5421,        // M12: FIONBIO – allowed because fcntl(F_SETFL, O_NONBLOCK)
396            // achieves the same result and is already permitted. Blocking
397            // FIONBIO only breaks tokio/mio for no security gain.
398            0x5451, // FIOCLEX
399            0x5450, // FIONCLEX
400        ];
401        let mut ioctl_rules = Vec::new();
402        for &request in ioctl_allowed {
403            let condition = SeccompCondition::new(
404                1, // arg1 is the request code for ioctl(fd, request, ...)
405                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        // prctl: allow only safe operations.
420        // Notably absent (hit default deny):
421        //   PR_CAPBSET_DROP (24) – could weaken the capability bounding set
422        //   PR_SET_SECUREBITS (28) – could disable secure-exec restrictions
423        //   PR_CAP_AMBIENT mutations – could activate retained inheritable caps
424        let prctl_allowed: &[u64] = &[
425            1,  // PR_SET_PDEATHSIG
426            2,  // PR_GET_PDEATHSIG
427            15, // PR_SET_NAME
428            16, // PR_GET_NAME
429            23, // PR_CAPBSET_READ – glibc probes this at startup to discover
430            // cap_last_cap when /proc/sys is masked. Read-only, harmless
431            // after capabilities have been dropped.
432            27, // PR_GET_SECUREBITS – read-only query of securebits flags
433            36, // PR_SET_CHILD_SUBREAPER – safe, only affects own descendants
434            37, // PR_GET_CHILD_SUBREAPER
435            38, // PR_SET_NO_NEW_PRIVS
436            40, // PR_GET_TID_ADDRESS – read-only, returns thread ID address
437            39, // PR_GET_NO_NEW_PRIVS
438            // Some Nix-packaged binaries attempt PR_SET_MM_ARG_START to update
439            // their visible argv range. The operation is kernel-gated by
440            // CAP_SYS_RESOURCE; Nucleus drops all capabilities before seccomp,
441            // so allowing this option lets the kernel return EPERM instead of
442            // killing otherwise ordinary workloads.
443            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, // arg0 is the option for prctl(option, ...)
449                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, // arg0 is the option for prctl(option, ...)
464            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, // arg1 is the PR_CAP_AMBIENT subcommand.
476            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        // M3: prlimit64 – only allow GET (new_limit == NULL, i.e. arg2 == 0).
497        // SET operations could raise RLIMIT_NPROC to bypass fork-bomb protection.
498        let prlimit_condition = SeccompCondition::new(
499            2, // arg2 = new_limit pointer for prlimit64(pid, resource, new_limit, old_limit)
500            seccompiler::SeccompCmpArgLen::Qword,
501            seccompiler::SeccompCmpOp::Eq,
502            0u64, // new_limit == NULL means GET-only
503        )
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        // mprotect: permit RW or RX transitions, but reject PROT_WRITE|PROT_EXEC.
513        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, // arg2 is prot for mprotect(addr, len, prot)
517                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        // preadv2/pwritev2: allow only flags == 0, which makes them equivalent
532        // to preadv/pwritev. Nonzero RWF_* flags include cache-observing
533        // behavior such as RWF_NOWAIT and future flags seccomp cannot review.
534        for (syscall, name) in [
535            (libc::SYS_preadv2, "preadv2"),
536            (libc::SYS_pwritev2, "pwritev2"),
537        ] {
538            let condition = SeccompCondition::new(
539                5, // arg5 = flags for preadv2/pwritev2(fd, iov, iovcnt, off_lo, off_hi, flags)
540                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        // clone3 is intentionally absent from the allow map. Its flags live in a
557        // user pointer (struct clone_args), which seccomp BPF cannot dereference,
558        // so it cannot be safely namespace-filtered. The BPF compiler adds an
559        // exact-match ENOSYS deny for clone3 so libc falls back to filtered clone.
560
561        // clone: allow but deny namespace-creating flags to prevent nested namespace creation
562        let clone_condition = SeccompCondition::new(
563            0, // arg0 = flags
564            seccompiler::SeccompCmpArgLen::Qword,
565            seccompiler::SeccompCmpOp::MaskedEq(DENIED_CLONE_NAMESPACE_FLAGS),
566            0, // (flags & ns_flags) == 0: none of the namespace flags set
567        )
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        // execveat: allow but block AT_EMPTY_PATH (0x1000) to prevent fileless
577        // execution. With AT_EMPTY_PATH, execveat can execute code from any open
578        // fd (e.g., open + unlink, or even a socket fd), bypassing filesystem
579        // controls – not just memfd_create. Blocking memfd_create alone is
580        // insufficient. Normal execveat with dirfd+pathname (no AT_EMPTY_PATH)
581        // remains allowed.
582        let execveat_condition = SeccompCondition::new(
583            4, // arg4 = flags for execveat(dirfd, pathname, argv, envp, flags)
584            seccompiler::SeccompCmpArgLen::Dword,
585            seccompiler::SeccompCmpOp::MaskedEq(libc::AT_EMPTY_PATH as u64),
586            0, // (flags & AT_EMPTY_PATH) == 0: AT_EMPTY_PATH not set
587        )
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    /// Validate `--seccomp-allow` entries for production mode.
600    ///
601    /// Development runs warn and ignore unsupported names at filter construction
602    /// time. Production mode rejects them early so operators cannot accidentally
603    /// believe a weakened or unsupported syscall fragment was applied.
604    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    /// Compile the minimal BPF filter without applying it
647    ///
648    /// This is useful for benchmarking filter compilation overhead
649    /// without the irreversible side effect of applying the filter.
650    ///
651    /// Uses bitmap-based BPF compilation for O(1) syscall dispatch.
652    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    /// Expose minimal_filter for tests in sibling modules.
667    #[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    /// Apply seccomp filter
681    ///
682    /// This implements the transition: no_filter -> whitelist_active
683    /// in the seccomp state machine (NucleusSecurity_Seccomp_SeccompEnforcement.tla)
684    ///
685    /// Once applied, the filter cannot be removed (irreversible property)
686    /// In rootless mode or if seccomp setup fails, this will warn and continue
687    pub fn apply_minimal_filter(&mut self) -> Result<bool> {
688        self.apply_minimal_filter_with_mode(false, false)
689    }
690
691    /// Apply seccomp filter with configurable failure behavior
692    ///
693    /// When `best_effort` is true, failures are logged and execution continues.
694    /// When false, seccomp setup is fail-closed.
695    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    /// Apply seccomp filter with network-mode-aware socket restrictions
704    ///
705    /// When `allow_network` is false, `SYS_socket` is restricted to AF_UNIX only,
706    /// preventing creation of network sockets (AF_INET, AF_INET6, etc.).
707    /// When `allow_network` is true, all socket domains are permitted.
708    ///
709    /// When `best_effort` is true, failures are logged and execution continues.
710    /// When false, seccomp setup is fail-closed.
711    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            /* gpu_mode */ false,
724        )
725    }
726
727    /// Apply the seccomp filter with an optional GPU relaxation.
728    ///
729    /// When `gpu_mode` is `true`, the restrictive terminal-only `ioctl`
730    /// allowlist is replaced with an unconditional allow. GPU drivers issue
731    /// dozens of vendor-specific ioctl request codes (DRM, NV_ESC_RM, KFD)
732    /// that cannot be enumerated, so the narrow allowlist is incompatible with
733    /// GPU passthrough. This is an audited, opt-in relaxation.
734    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        // GPU relaxation: replace the terminal-only ioctl allowlist with an
764        // unconditional allow. Vendor driver ioctl request codes are not
765        // enumerable, so the narrow filter would kill GPU workloads.
766        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        // Apply the filter
804        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    /// Apply a seccomp profile loaded from a JSON file.
828    ///
829    /// The profile format is a JSON object with:
830    /// ```json
831    /// {
832    ///   "defaultAction": "SCMP_ACT_ERRNO",
833    ///   "syscalls": [
834    ///     { "names": ["read", "write", "open", ...], "action": "SCMP_ACT_ALLOW" }
835    ///   ]
836    /// }
837    /// ```
838    ///
839    /// This is a subset of the OCI seccomp profile format. Only the syscall name
840    /// allowlist is used; argument-level filtering from the built-in profile is
841    /// not applied when using a custom profile.
842    ///
843    /// If `expected_sha256` is provided, the file's SHA-256 hash is verified
844    /// against it before loading. This prevents silent profile tampering.
845    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        // Read profile file
859        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        // Verify SHA-256 hash if expected
867        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        // Parse profile
879        let profile: SeccompProfile = serde_json::from_slice(&content).map_err(|e| {
880            NucleusError::SeccompError(format!("Failed to parse seccomp profile: {}", e))
881        })?;
882
883        // Warn when custom profile allows security-critical syscalls without
884        // argument-level filtering. The built-in filter restricts clone, ioctl,
885        // prctl, and socket at the argument level; a custom profile that allows
886        // them by name only silently removes all of that hardening.
887        Self::warn_missing_arg_filters(&profile);
888
889        // Build filter from profile
890        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        // SEC-01: Merge built-in argument filters for security-critical syscalls.
912        // Custom profiles that allow clone/ioctl/prctl/socket/mprotect by name
913        // without argument-level filters would silently remove all hardening.
914        // Overwrite their empty rules with the built-in argument-filtered rules.
915        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    /// Install an allow-all seccomp filter with SECCOMP_FILTER_FLAG_LOG.
957    ///
958    /// Used in trace mode: all syscalls are allowed but logged to the kernel
959    /// audit subsystem. A separate reader collects the logged syscalls.
960    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        // Apply with LOG flag so kernel audits every syscall.
971        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        // Create an empty rule set. SeccompAction::Log allows each syscall
979        // while requesting an audit/log record; SeccompFilter requires the
980        // unused match action to be distinct from the mismatch action.
981        let filter = SeccompFilter::new(
982            BTreeMap::new(),
983            SeccompAction::Log,   // default: allow everything and log it
984            SeccompAction::Allow, // match action (unused – no rules)
985            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    /// Syscalls that the built-in filter restricts at the argument level.
997    /// Custom profiles allowing these without argument filters weaken security.
998    const ARG_FILTERED_SYSCALLS: &'static [&'static str] = &[
999        "clone", "execveat", "ioctl", "mprotect", "preadv2", "prctl", "pwritev2", "socket",
1000    ];
1001
1002    /// Security-critical syscalls that must not be re-enabled by the
1003    /// convenience `--seccomp-allow` fragment mechanism.
1004    const SECURITY_CRITICAL_DENIED_SYSCALLS: &'static [&'static str] = &[
1005        // Namespace entry/creation bypasses clone namespace-flag filtering.
1006        "clone3",
1007        "unshare",
1008        "setns",
1009        // Kernel keyrings are deliberately outside the built-in policy.
1010        "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    /// Non-default syscalls that may be opted into via `--seccomp-allow`.
1020    ///
1021    /// Every syscall known to `syscall_name_to_number` but absent from both
1022    /// `base_allowed_syscalls` and `ARG_FILTERED_SYSCALLS` must appear here
1023    /// to be enableable. Requesting a known syscall that is NOT in this list
1024    /// emits a WARN and is silently dropped (defense-in-depth).
1025    const OPT_IN_SYSCALLS: &'static [&'static str] = &[
1026        // io_uring – large attack surface but needed by modern databases
1027        "io_uring_setup",
1028        "io_uring_enter",
1029        "io_uring_register",
1030        // SysV message queues
1031        "msgget",
1032        "msgsnd",
1033        "msgrcv",
1034        "msgctl",
1035        // POSIX message queues
1036        "mq_open",
1037        "mq_unlink",
1038        "mq_timedsend",
1039        "mq_timedreceive",
1040        "mq_notify",
1041        "mq_getsetattr",
1042        // POSIX timers
1043        "timer_create",
1044        "timer_settime",
1045        "timer_gettime",
1046        "timer_getoverrun",
1047        "timer_delete",
1048        // Inotify / fanotify
1049        "inotify_init",
1050        "inotify_init1",
1051        "inotify_add_watch",
1052        "inotify_rm_watch",
1053        "fanotify_init",
1054        "fanotify_mark",
1055        // Memory (non-default)
1056        "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        // File I/O (non-default)
1071        "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        // Extended attributes (write)
1090        "setxattr",
1091        "lsetxattr",
1092        "fsetxattr",
1093        "removexattr",
1094        "lremovexattr",
1095        "fremovexattr",
1096        "setxattrat",
1097        "getxattrat",
1098        "listxattrat",
1099        "removexattrat",
1100        // Network (non-default)
1101        "recvmmsg",
1102        "sendmmsg",
1103        // Scheduling (non-default)
1104        "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        // Resource limits / priority
1112        "setrlimit",
1113        "getpriority",
1114        "setpriority",
1115        "ioprio_set",
1116        "ioprio_get",
1117        // Process (non-default, low risk)
1118        "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        // UID/GID
1130        "setuid",
1131        "setgid",
1132        "setreuid",
1133        "setregid",
1134        "setresuid",
1135        "getresuid",
1136        "setresgid",
1137        "getresgid",
1138        "setfsuid",
1139        "setfsgid",
1140        "setgroups",
1141        "getsid",
1142        // Capabilities (read-only query)
1143        "capget",
1144        // Signals (non-default)
1145        "rt_tgsigqueueinfo",
1146        // Misc
1147        "mknod",
1148        "mknodat",
1149        "syslog",
1150        "clock_settime",
1151        "clock_adjtime",
1152        "adjtimex",
1153        "kcmp",
1154        "epoll_pwait2",
1155        // Futex (non-default)
1156        "futex_waitv",
1157        "futex_wake",
1158        "futex_wait",
1159        "futex_requeue",
1160        // Landlock (already in default but listed for completeness)
1161        "seccomp",
1162    ];
1163
1164    /// Warn when a custom seccomp profile allows security-critical syscalls
1165    /// without argument-level filtering.
1166    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    /// Check if seccomp filter has been applied
1185    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        // SAFETY: `prctl(PR_SET_NO_NEW_PRIVS, ...)` has no pointer arguments here
1221        // and only affects the current thread/process as required before seccomp.
1222        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        // SAFETY: `prog` points to a live BPF program buffer for the duration of
1233        // the syscall and the kernel copies the pointed-to filter immediately.
1234        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
1251// SeccompProfile and SeccompSyscallGroup are defined in seccomp_generate.rs
1252use crate::security::seccomp_generate::SeccompProfile;
1253
1254/// Map a syscall name (e.g. "read", "write") to its Linux syscall number.
1255///
1256/// Covers the most common syscalls. Unknown names return None.
1257fn syscall_name_to_number(name: &str) -> Option<i64> {
1258    match name {
1259        // File I/O
1260        "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        // Memory
1336        "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        // SysV IPC
1347        "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        // Process
1356        #[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        // Signals
1378        "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        // Time
1390        "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        // Directories
1396        "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        // Network
1408        "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        // Poll/Select
1425        #[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        // Misc
1448        "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        // Ownership
1469        "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
1476        "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        // Legacy AIO
1480        "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        // Timers
1485        "setitimer" => Some(libc::SYS_setitimer),
1486        "getitimer" => Some(libc::SYS_getitimer),
1487        // Process groups
1488        "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
1494        "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        // --- Additional syscalls (not in default allowlist, available via --seccomp-allow) ---
1498        // Memory
1499        "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        // Process
1517        #[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        // UID/GID
1535        "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        // Capabilities
1548        "capget" => Some(libc::SYS_capget),
1549        "capset" => Some(libc::SYS_capset),
1550        // Signals
1551        "rt_tgsigqueueinfo" => Some(libc::SYS_rt_tgsigqueueinfo),
1552        // SysV message queues
1553        "msgget" => Some(libc::SYS_msgget),
1554        "msgsnd" => Some(libc::SYS_msgsnd),
1555        "msgrcv" => Some(libc::SYS_msgrcv),
1556        "msgctl" => Some(libc::SYS_msgctl),
1557        // Timers
1558        "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        // File I/O (non-default)
1568        #[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        // Extended attributes (write)
1588        "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        // Network (non-default)
1599        "recvmmsg" => Some(libc::SYS_recvmmsg),
1600        "sendmmsg" => Some(libc::SYS_sendmmsg),
1601        // Inotify
1602        #[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
1608        "fanotify_init" => Some(libc::SYS_fanotify_init),
1609        "fanotify_mark" => Some(libc::SYS_fanotify_mark),
1610        // Epoll (non-default)
1611        "epoll_pwait2" => Some(libc::SYS_epoll_pwait2),
1612        // Scheduling (non-default)
1613        "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        // Resource limits
1622        #[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 (non-default)
1629        "futex_waitv" => Some(libc::SYS_futex_waitv),
1630        "futex_wake" => Some(454),
1631        "futex_wait" => Some(455),
1632        "futex_requeue" => Some(456),
1633        // Kernel modules
1634        "init_module" => Some(libc::SYS_init_module),
1635        "finit_module" => Some(libc::SYS_finit_module),
1636        "delete_module" => Some(libc::SYS_delete_module),
1637        // eBPF and performance
1638        "bpf" => Some(libc::SYS_bpf),
1639        "perf_event_open" => Some(libc::SYS_perf_event_open),
1640        // Seccomp
1641        "seccomp" => Some(libc::SYS_seccomp),
1642        // Userfaultfd
1643        "userfaultfd" => Some(libc::SYS_userfaultfd),
1644        // Mount (non-default)
1645        "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        // Misc (non-default)
1657        "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        // POSIX message queues
1689        "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        // Keyring
1696        "add_key" => Some(libc::SYS_add_key),
1697        "request_key" => Some(libc::SYS_request_key),
1698        "keyctl" => Some(libc::SYS_keyctl),
1699        // IO pgetevents
1700        "io_pgetevents" => Some(333),
1701        // LSM
1702        "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        // Note: We can't actually test application in unit tests
1732        // as it would affect the test process itself
1733        // This is tested in integration tests instead
1734        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        // i386 compat per syscall_32.tbl: socket..shutdown live at 359..373.
1851        // On x86_64 these numbers are outside our native allowlist surface.
1852        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        // Snapshot counts to catch unintended policy drift.
1867        // +9 accounts for conditional rules inserted in minimal_filter():
1868        // socket/ioctl/prctl/prlimit64/mprotect/preadv2/pwritev2/clone/execveat.
1869        // fork removed (forces through filtered clone path).
1870        // execveat removed from base (arg-filtered separately).
1871        // sysinfo removed (L8: leaks host info).
1872        // prlimit64 moved to arg-filtered (M3).
1873        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        // These syscalls must be in the arg-filtered list so custom profiles
1887        // get warnings when they allow them without filters.
1888        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        // clone3 carries flags through struct clone_args, which seccomp BPF
1902        // cannot inspect. It must not be unconditionally allowed; the compiler
1903        // adds an exact ENOSYS deny so libc falls back to filtered clone.
1904        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        // clone (not clone3) should still be in the rules with arg filtering
1920        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        // chown/fchown/lchown/fchownat: allowed – safe after CAP_CHOWN/CAP_FOWNER drop
1931        // mlock/munlock: allowed – needed by databases, bounded by RLIMIT_MEMLOCK
1932        let removed = [
1933            libc::SYS_sync,
1934            libc::SYS_syncfs,
1935            libc::SYS_mincore,
1936            libc::SYS_vfork,
1937            libc::SYS_tkill,
1938            // io_uring: large attack surface, many CVEs – require custom profile
1939            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        // SEC-01: Custom seccomp profiles that allow "clone" must still get
1956        // argument-level filtering to block namespace-creating flags.
1957        // Verify by inspecting the built-in filter rules that serve as the
1958        // merge source for apply_profile_from_file.
1959        let rules = SeccompManager::minimal_filter(true, &[]).unwrap();
1960
1961        // Every ARG_FILTERED_SYSCALLS entry must have non-empty argument-level
1962        // rules in the built-in filter so apply_profile_from_file can merge them.
1963        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        // SEC-02: memfd_create enables fileless code execution when combined with execveat.
1979        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        // Also verify it's not sneaked into the compiled filter rules
1985        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        // SEC-03: mprotect must have argument-level filtering to prevent W^X
1995        // (PROT_WRITE|PROT_EXEC) violations. Verify via runtime data structures.
1996
1997        // mprotect must NOT be in the unconditional base allowlist
1998        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        // mprotect must be present in the compiled filter with non-empty
2005        // argument conditions (the conditions enforce W^X)
2006        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        // v2 vectored I/O has an extra flags argument. The built-in profile
2021        // permits the v2 entrypoints only when flags == 0, leaving preadv/pwritev
2022        // available for ordinary positioned vectored I/O.
2023        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        // SEC-08: All unsafe blocks must have // SAFETY: documentation
2051        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            // Check that there's a SAFETY comment within 200 chars before the unsafe block
2056            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    // --- H-1: mprotect MaskedEq logic verification ---
2069    //
2070    // The mprotect filter uses MaskedEq((PROT_WRITE | PROT_EXEC), value) to
2071    // allow only combinations where the W|X bits match one of {0, W, X}.
2072    // These tests prove the logic is correct without installing a real
2073    // seccomp filter (which would affect the test process).
2074
2075    /// Helper: simulates the MaskedEq check that the seccomp BPF would perform.
2076    /// Returns true if the prot value would be ALLOWED by one of the rules.
2077    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    // --- Extra syscall allowlist tests ---
2146
2147    #[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        // If a user requests an arg-filtered syscall via extra_syscalls, the
2164        // version from the built-in filter should still be present (not replaced
2165        // with an unconditional allow).
2166        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        // Unknown syscall names emit a WARN and are skipped (not fatal)
2188        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        // Requesting a syscall that's already in the default allowlist should work fine
2206        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        // A known syscall that is NOT in OPT_IN_SYSCALLS must be blocked
2214        // (not added to the filter rules). E.g. kexec_load, bpf, ptrace.
2215        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        // Syscalls in OPT_IN_SYSCALLS must be added when requested
2302        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}