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}