Skip to main content

vtcode_safety/sandboxing/policy/
seccomp.rs

1use serde::{Deserialize, Serialize};
2
3/// Version of the seccomp blocklist schema compiled into this binary.
4///
5/// Bump when `BLOCKED_SYSCALLS` gains or loses an entry so launcher logs
6/// (see `linux_seccomp::apply_seccomp_filter`) can distinguish which policy
7/// a trace came from.
8pub const SECCOMP_PROFILE_VERSION: u32 = 1;
9
10/// Syscalls that should be blocked in seccomp-bpf profiles.
11///
12/// Following the field guide: "A tight seccomp profile blocks syscalls that expand
13/// kernel attack surface or enable escalation."
14///
15/// This is a blocklist (deny-list), not a whitelist. Per the sandboxing-basics
16/// analysis (Emilua 2025), blocklists are inherently fragile: new kernel syscalls,
17/// multiarch numberings, and the x32 ABI (`__X32_SYSCALL_BIT`) can bypass naive
18/// filters. `linux_seccomp::primary_rules` therefore installs both the native
19/// number and its x32-aliased variant on x86_64, and the launcher kills
20/// mismatched architectures outright (`SECCOMP_RET_KILL_PROCESS` via
21/// seccompiler arch validation). Prefer Landlock for filesystem policy and keep
22/// this list focused on escalation/escape primitives.
23///
24/// Group labels mirror the Kafel-inspired families from that analysis
25/// (`Debug`, `FilesystemHandle`, `IoUring`, `ProcessVm`, ...) so future
26/// whitelist work can promote one family at a time.
27pub const BLOCKED_SYSCALLS: &[&str] = &[
28    // Debug/inspection - can be used to escape sandboxes or leak process state
29    "ptrace",
30    "kcmp",
31    "pidfd_getfd",
32    "process_madvise",
33    "process_mrelease",
34    // Mounting - can change filesystem namespace
35    "mount",
36    "umount",
37    "umount2",
38    // FilesystemHandle - file-handle escapes around path-based policy
39    "open_by_handle_at",
40    "name_to_handle_at",
41    // Kernel module loading
42    "init_module",
43    "finit_module",
44    "delete_module",
45    // Kernel replacement
46    "kexec_load",
47    "kexec_file_load",
48    // BPF - can be used for sandbox escape
49    "bpf",
50    // Performance events - information leakage risk
51    "perf_event_open",
52    // Userfaultfd - can be used for race conditions
53    "userfaultfd",
54    // IoUring - widely distrusted for untrusted code; see
55    // https://security.googleblog.com/2023/06/learnings-from-kctf-vrps-42-linux.html
56    "io_uring_setup",
57    "io_uring_enter",
58    "io_uring_register",
59    // ProcessVm - cross-process memory inspection/writes
60    "process_vm_readv",
61    "process_vm_writev",
62    // Reboot/power
63    "reboot",
64    // Swap manipulation
65    "swapon",
66    "swapoff",
67    // System time manipulation
68    "settimeofday",
69    "clock_settime",
70    "adjtimex",
71    // Keyring manipulation
72    "add_key",
73    "request_key",
74    "keyctl",
75    // IO permission
76    "ioperm",
77    "iopl",
78    // Acct - process accounting manipulation
79    "acct",
80    // Quota manipulation
81    "quotactl",
82    // Namespace creation (can bypass restrictions)
83    "unshare",
84    "setns",
85    // Personality - can enable legacy modes
86    "personality",
87];
88
89/// Syscalls that require argument filtering (not fully blocked).
90pub const FILTERED_SYSCALLS: &[&str] = &[
91    // clone/clone3: filter to prevent new namespaces
92    "clone", "clone3", // ioctl: filter to block dangerous device ioctls
93    "ioctl",  // prctl: filter to block dangerous operations
94    "prctl",  // socket: filter to enforce network policy
95    "socket",
96];
97
98/// Seccomp profile configuration for Linux sandboxing.
99///
100/// Used alongside Landlock for defense-in-depth.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct SeccompProfile {
103    /// Syscalls to block entirely.
104    #[serde(default = "default_blocked_syscalls")]
105    blocked_syscalls: Vec<String>,
106
107    /// Whether to allow new namespace creation (usually false for sandboxes).
108    #[serde(default)]
109    pub(super) allow_namespaces: bool,
110
111    /// Whether to allow network socket creation (controlled separately).
112    #[serde(default)]
113    pub(super) allow_network_sockets: bool,
114
115    /// Whether to log blocked syscalls instead of killing the process.
116    #[serde(default)]
117    log_only: bool,
118}
119
120fn default_blocked_syscalls() -> Vec<String> {
121    BLOCKED_SYSCALLS.iter().map(|s| s.to_string()).collect()
122}
123
124impl Default for SeccompProfile {
125    fn default() -> Self {
126        Self {
127            blocked_syscalls: default_blocked_syscalls(),
128            allow_namespaces: false,
129            allow_network_sockets: false,
130            log_only: false,
131        }
132    }
133}
134
135impl SeccompProfile {
136    /// Syscalls this profile blocks outright.
137    #[must_use]
138    pub(crate) fn blocked_syscalls(&self) -> &[String] {
139        &self.blocked_syscalls
140    }
141
142    /// Whether new namespace creation is permitted.
143    #[must_use]
144    pub(crate) fn allow_namespaces(&self) -> bool {
145        self.allow_namespaces
146    }
147
148    /// Whether network socket creation is permitted.
149    #[must_use]
150    pub(crate) fn allow_network_sockets(&self) -> bool {
151        self.allow_network_sockets
152    }
153
154    /// Whether the profile is log-only (no filter installed).
155    #[must_use]
156    pub(crate) fn log_only(&self) -> bool {
157        self.log_only
158    }
159
160    /// Create a strict profile blocking all dangerous syscalls.
161    #[must_use]
162    pub fn strict() -> Self {
163        Self {
164            blocked_syscalls: default_blocked_syscalls(),
165            allow_namespaces: false,
166            allow_network_sockets: false,
167            log_only: false,
168        }
169    }
170
171    /// Create a permissive profile for semi-trusted code.
172    #[must_use]
173    pub fn permissive() -> Self {
174        Self {
175            blocked_syscalls: vec![
176                "ptrace".to_string(),
177                "kexec_load".to_string(),
178                "kexec_file_load".to_string(),
179                "reboot".to_string(),
180            ],
181            allow_namespaces: false,
182            allow_network_sockets: true,
183            log_only: false,
184        }
185    }
186
187    /// Create a logging-only profile for debugging.
188    #[must_use]
189    pub fn logging() -> Self {
190        Self {
191            blocked_syscalls: default_blocked_syscalls(),
192            allow_namespaces: false,
193            allow_network_sockets: false,
194            log_only: true,
195        }
196    }
197
198    /// Builder: add a syscall to block.
199    #[must_use]
200    pub fn block_syscall(mut self, syscall: impl Into<String>) -> Self {
201        let syscall = syscall.into();
202        if !self.blocked_syscalls.contains(&syscall) {
203            self.blocked_syscalls.push(syscall);
204        }
205        self
206    }
207
208    /// Builder: allow network sockets.
209    #[must_use]
210    pub fn with_network(mut self) -> Self {
211        self.allow_network_sockets = true;
212        self
213    }
214
215    /// Builder: enable log-only mode.
216    #[must_use]
217    pub fn with_logging(mut self) -> Self {
218        self.log_only = true;
219        self
220    }
221
222    /// Check if a syscall is blocked by this profile.
223    #[inline]
224    #[must_use]
225    pub(super) fn is_blocked(&self, syscall: &str) -> bool {
226        self.blocked_syscalls.iter().any(|s| s == syscall)
227    }
228
229    /// Generate a JSON representation for the sandbox helper.
230    pub(crate) fn to_json(&self) -> Result<String, serde_json::Error> {
231        serde_json::to_string(self)
232    }
233}