Skip to main content

sandlock_core/seccomp/
syscall.rs

1//! Syscall identity: name-to-number resolution and the checked `Syscall`
2//! number newtype.
3//!
4//! The newtype closes the footgun where `add_handler(-5, h)` would compile but
5//! silently never fire because the cBPF filter cannot emit a JEQ for an
6//! architecture-unknown syscall number.
7
8use thiserror::Error;
9
10/// Map a syscall name to its number on the current architecture.
11///
12/// Returns `None` for names that are not syscalls on this architecture (for
13/// example legacy `open`/`stat` on the generic-ABI arches) or are not syscall
14/// names at all. Backed by the `syscalls` crate's kernel-ABI tables, so this
15/// covers every syscall, not a curated subset.
16///
17/// Sandlock's public API and presets use libc's `SYS_*` spellings; where the
18/// crate's per-arch table spells a syscall differently, [`libc_name_alias`]
19/// bridges the gap so those names keep resolving.
20pub fn syscall_name_to_nr(name: &str) -> Option<u32> {
21    name.parse::<syscalls::Sysno>()
22        .ok()
23        .or_else(|| libc_name_alias(name).and_then(|aka| aka.parse::<syscalls::Sysno>().ok()))
24        .map(|s| s.id() as u32)
25}
26
27/// Maps a libc `SYS_*` syscall name to the `syscalls` crate's name where the
28/// two diverge. Sandlock callers spell syscalls the libc way, but the crate's
29/// tables use the kernel-canonical name on some architectures.
30///
31/// Currently only `newfstatat`: the crate spells syscall 79 `fstatat` on
32/// aarch64 (libc, and the crate's own x86_64 and riscv64 tables, use
33/// `newfstatat`). Returns `None` when no alias is needed.
34fn libc_name_alias(name: &str) -> Option<&'static str> {
35    match name {
36        "newfstatat" => Some("fstatat"),
37        _ => None,
38    }
39}
40
41#[derive(Debug, Error, PartialEq, Eq)]
42pub enum SyscallError {
43    #[error("syscall number {0} is negative")]
44    Negative(i64),
45    #[error("syscall number {0} is unknown for the current architecture")]
46    UnknownForArch(i64),
47}
48
49#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
50pub struct Syscall(i64);
51
52impl Syscall {
53    /// Validates that `nr` is non-negative and known on the current architecture.
54    pub fn checked(nr: i64) -> Result<Self, SyscallError> {
55        if nr < 0 {
56            return Err(SyscallError::Negative(nr));
57        }
58        if !crate::arch::is_known_syscall(nr) {
59            return Err(SyscallError::UnknownForArch(nr));
60        }
61        Ok(Self(nr))
62    }
63
64    pub fn raw(self) -> i64 {
65        self.0
66    }
67}
68
69impl TryFrom<i64> for Syscall {
70    type Error = SyscallError;
71    fn try_from(nr: i64) -> Result<Self, Self::Error> {
72        Self::checked(nr)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn checked_accepts_valid_openat() {
82        let s = Syscall::checked(libc::SYS_openat).expect("openat is valid");
83        assert_eq!(s.raw(), libc::SYS_openat);
84    }
85
86    #[test]
87    fn checked_rejects_negative() {
88        match Syscall::checked(-5) {
89            Err(SyscallError::Negative(-5)) => {}
90            other => panic!("expected Negative(-5), got {:?}", other),
91        }
92    }
93
94    #[test]
95    fn checked_rejects_arch_unknown() {
96        // 99_999 is not a real syscall number on any supported arch.
97        match Syscall::checked(99_999) {
98            Err(SyscallError::UnknownForArch(99_999)) => {}
99            other => panic!("expected UnknownForArch(99_999), got {:?}", other),
100        }
101    }
102
103    #[test]
104    fn try_from_i64_delegates_to_checked() {
105        let s: Syscall = libc::SYS_openat.try_into().expect("openat valid");
106        assert_eq!(s.raw(), libc::SYS_openat);
107        let bad: Result<Syscall, _> = (-1i64).try_into();
108        assert!(matches!(bad, Err(SyscallError::Negative(-1))));
109    }
110
111    /// Independent cross-check that the crate's ABI tables agree with the
112    /// system `libc::SYS_*` constants. Only names libc and the crate spell
113    /// identically on every target arch belong here; `newfstatat` (which the
114    /// crate spells `fstatat` on aarch64) resolves through the alias path and
115    /// is covered by `name_to_nr_resolves_newfstatat_alias` instead.
116    #[test]
117    fn name_to_nr_matches_libc_for_stable_names() {
118        let cases: &[(&str, i64)] = &[
119            ("mount", libc::SYS_mount),
120            ("openat", libc::SYS_openat),
121            ("connect", libc::SYS_connect),
122            ("clone", libc::SYS_clone),
123            ("clone3", libc::SYS_clone3),
124            ("execve", libc::SYS_execve),
125            ("ioctl", libc::SYS_ioctl),
126            ("ptrace", libc::SYS_ptrace),
127            ("userfaultfd", libc::SYS_userfaultfd),
128            ("bpf", libc::SYS_bpf),
129            ("statx", libc::SYS_statx),
130            ("getrandom", libc::SYS_getrandom),
131            ("io_uring_setup", libc::SYS_io_uring_setup),
132        ];
133        for &(name, expected) in cases {
134            assert_eq!(
135                syscall_name_to_nr(name),
136                Some(expected as u32),
137                "{name} should resolve to libc::SYS_{name} = {expected}"
138            );
139        }
140    }
141
142    #[test]
143    fn name_to_nr_rejects_non_syscall_names() {
144        assert_eq!(syscall_name_to_nr("definitely_not_a_syscall"), None);
145        assert_eq!(syscall_name_to_nr(""), None);
146    }
147
148    /// `newfstatat` must resolve on every arch even though the crate spells it
149    /// `fstatat` on aarch64. Regression guard: the `COMMON_PATH_SYSCALLS`
150    /// preset and other callers pass the libc name through the FFI.
151    #[test]
152    fn name_to_nr_resolves_newfstatat_alias() {
153        assert_eq!(
154            syscall_name_to_nr("newfstatat"),
155            Some(libc::SYS_newfstatat as u32)
156        );
157    }
158}