Skip to main content

aa_security/policy/
syscall.rs

1//! Syscall-allowlist node for the canonical policy AST (AAASM-3624).
2//!
3//! A [`SyscallAllowlist`] is a per-workload set of permitted syscalls. It lives
4//! on the same [`PolicyDocument`](super::document::PolicyDocument) as the
5//! path/egress rules so the kernel-layer syscall allowlist is expressed in the
6//! ONE policy source — there is no second policy path. The AAASM-3635 lowering
7//! compiles this node into the `SYSCALL_ALLOWLIST` eBPF map entries the
8//! AAASM-3631 enforcement probe consumes.
9//!
10//! Naming mirrors the lowercase syscall names used elsewhere
11//! (`aa-ebpf::syscall::SyscallKind`) so a policy reads `read` / `write` /
12//! `close`, not raw numbers. Each known name carries its x86_64 syscall number
13//! for the lowering step. Unknown names are rejected at parse/validation time
14//! so a typo cannot silently widen the allowlist.
15
16use std::collections::BTreeSet;
17use std::fmt;
18use std::str::FromStr;
19
20/// A syscall permitted by a [`SyscallAllowlist`].
21///
22/// The variant set is deliberately the small, audited vocabulary a sandboxed
23/// workload legitimately needs; it carries the x86_64 syscall number so the
24/// eBPF lowering (AAASM-3635) can emit `SYSCALL_ALLOWLIST` map keys without a
25/// second name table. Adding a syscall is an explicit, reviewable change.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum Syscall {
29    /// `read` — read from a file descriptor.
30    Read,
31    /// `write` — write to a file descriptor.
32    Write,
33    /// `close` — close a file descriptor.
34    Close,
35    /// `openat` — open a file relative to a directory fd.
36    Openat,
37    /// `fstat` — get file status.
38    Fstat,
39    /// `lseek` — reposition a file offset.
40    Lseek,
41    /// `mmap` — map memory.
42    Mmap,
43    /// `munmap` — unmap memory.
44    Munmap,
45    /// `brk` — change the data segment size.
46    Brk,
47    /// `rt_sigaction` — examine/change a signal action.
48    RtSigaction,
49    /// `rt_sigprocmask` — examine/change blocked signals.
50    RtSigprocmask,
51    /// `exit` — terminate the calling thread.
52    Exit,
53    /// `exit_group` — terminate all threads in the process.
54    ExitGroup,
55    /// `clock_gettime` — read a clock.
56    ClockGettime,
57    /// `getrandom` — obtain random bytes.
58    Getrandom,
59}
60
61impl Syscall {
62    /// The x86_64 syscall number for this syscall.
63    ///
64    /// These are the stable Linux x86_64 (`__NR_*`) numbers, used by the
65    /// AAASM-3635 lowering to populate the `SYSCALL_ALLOWLIST` eBPF map.
66    pub fn number(self) -> u32 {
67        match self {
68            Syscall::Read => 0,
69            Syscall::Write => 1,
70            Syscall::Close => 3,
71            Syscall::Fstat => 5,
72            Syscall::Lseek => 8,
73            Syscall::Mmap => 9,
74            Syscall::Munmap => 11,
75            Syscall::Brk => 12,
76            Syscall::RtSigaction => 13,
77            Syscall::RtSigprocmask => 14,
78            Syscall::Openat => 257,
79            Syscall::ClockGettime => 228,
80            Syscall::ExitGroup => 231,
81            Syscall::Exit => 60,
82            Syscall::Getrandom => 318,
83        }
84    }
85
86    /// The lowercase policy name for this syscall.
87    pub fn name(self) -> &'static str {
88        match self {
89            Syscall::Read => "read",
90            Syscall::Write => "write",
91            Syscall::Close => "close",
92            Syscall::Openat => "openat",
93            Syscall::Fstat => "fstat",
94            Syscall::Lseek => "lseek",
95            Syscall::Mmap => "mmap",
96            Syscall::Munmap => "munmap",
97            Syscall::Brk => "brk",
98            Syscall::RtSigaction => "rt_sigaction",
99            Syscall::RtSigprocmask => "rt_sigprocmask",
100            Syscall::Exit => "exit",
101            Syscall::ExitGroup => "exit_group",
102            Syscall::ClockGettime => "clock_gettime",
103            Syscall::Getrandom => "getrandom",
104        }
105    }
106}
107
108impl fmt::Display for Syscall {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.write_str(self.name())
111    }
112}
113
114impl FromStr for Syscall {
115    type Err = String;
116
117    fn from_str(s: &str) -> Result<Self, Self::Err> {
118        match s {
119            "read" => Ok(Syscall::Read),
120            "write" => Ok(Syscall::Write),
121            "close" => Ok(Syscall::Close),
122            "openat" => Ok(Syscall::Openat),
123            "fstat" => Ok(Syscall::Fstat),
124            "lseek" => Ok(Syscall::Lseek),
125            "mmap" => Ok(Syscall::Mmap),
126            "munmap" => Ok(Syscall::Munmap),
127            "brk" => Ok(Syscall::Brk),
128            "rt_sigaction" => Ok(Syscall::RtSigaction),
129            "rt_sigprocmask" => Ok(Syscall::RtSigprocmask),
130            "exit" => Ok(Syscall::Exit),
131            "exit_group" => Ok(Syscall::ExitGroup),
132            "clock_gettime" => Ok(Syscall::ClockGettime),
133            "getrandom" => Ok(Syscall::Getrandom),
134            _ => Err(format!("unknown syscall: '{s}'")),
135        }
136    }
137}
138
139/// A per-workload kernel syscall allowlist node on the canonical policy AST.
140///
141/// Membership is the set of permitted syscalls; the enforcement probe
142/// default-denies any syscall *not* in this set for a monitored PID. The set
143/// is a [`BTreeSet`] so it is inherently de-duplicated and order-stable, which
144/// keeps the AAASM-3635 lowering deterministic.
145#[derive(Debug, Clone, PartialEq, Eq, Default)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147pub struct SyscallAllowlist {
148    /// The permitted syscalls.
149    pub syscalls: BTreeSet<Syscall>,
150}
151
152impl SyscallAllowlist {
153    /// Build an allowlist from syscall name strings, validating each name and
154    /// de-duplicating. Returns the first unknown name as an error so a typo
155    /// can never silently widen (or no-op) the allowlist.
156    pub fn from_names<I, S>(names: I) -> Result<Self, String>
157    where
158        I: IntoIterator<Item = S>,
159        S: AsRef<str>,
160    {
161        let mut syscalls = BTreeSet::new();
162        for name in names {
163            syscalls.insert(Syscall::from_str(name.as_ref())?);
164        }
165        Ok(Self { syscalls })
166    }
167
168    /// Whether the allowlist permits the given syscall.
169    pub fn permits(&self, syscall: Syscall) -> bool {
170        self.syscalls.contains(&syscall)
171    }
172
173    /// The permitted syscalls, ordered, as an iterator.
174    pub fn iter(&self) -> impl Iterator<Item = Syscall> + '_ {
175        self.syscalls.iter().copied()
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn known_names_parse_and_round_trip() {
185        for name in [
186            "read",
187            "write",
188            "close",
189            "openat",
190            "fstat",
191            "lseek",
192            "mmap",
193            "munmap",
194            "brk",
195            "rt_sigaction",
196            "rt_sigprocmask",
197            "exit",
198            "exit_group",
199            "clock_gettime",
200            "getrandom",
201        ] {
202            let sc = Syscall::from_str(name).unwrap();
203            assert_eq!(sc.name(), name);
204            assert_eq!(sc.to_string(), name);
205        }
206    }
207
208    #[test]
209    fn unknown_name_is_rejected() {
210        assert!(Syscall::from_str("ptrace").is_err());
211        assert!(SyscallAllowlist::from_names(["read", "execve"]).is_err());
212    }
213
214    #[test]
215    fn from_names_dedups_and_validates() {
216        let allow = SyscallAllowlist::from_names(["read", "write", "read", "close"]).unwrap();
217        assert_eq!(allow.syscalls.len(), 3);
218        assert!(allow.permits(Syscall::Read));
219        assert!(allow.permits(Syscall::Write));
220        assert!(allow.permits(Syscall::Close));
221        assert!(!allow.permits(Syscall::Openat));
222    }
223
224    #[test]
225    fn iter_is_order_stable() {
226        let allow = SyscallAllowlist::from_names(["write", "read", "close"]).unwrap();
227        // BTreeSet orders by enum declaration order (derive Ord): Read, Write, Close.
228        let order: Vec<Syscall> = allow.iter().collect();
229        assert_eq!(order, vec![Syscall::Read, Syscall::Write, Syscall::Close]);
230    }
231
232    #[test]
233    fn known_syscall_numbers_are_x86_64() {
234        assert_eq!(Syscall::Read.number(), 0);
235        assert_eq!(Syscall::Write.number(), 1);
236        assert_eq!(Syscall::Close.number(), 3);
237        assert_eq!(Syscall::Exit.number(), 60);
238        assert_eq!(Syscall::Openat.number(), 257);
239        assert_eq!(Syscall::ExitGroup.number(), 231);
240    }
241}