Skip to main content

sandbox_core/
capabilities.rs

1//! Runtime detection of available system capabilities
2//!
3//! Probes the running kernel and system configuration to determine which
4//! sandboxing features are available, allowing graceful degradation.
5
6use std::path::Path;
7
8/// Detected system capabilities for sandboxing
9#[derive(Debug, Clone)]
10pub struct SystemCapabilities {
11    /// Running as root (euid == 0)
12    pub has_root: bool,
13    /// Unprivileged user namespaces are available
14    pub has_user_namespaces: bool,
15    /// Seccomp BPF filtering is available
16    pub has_seccomp: bool,
17    /// Landlock LSM is available (Linux 5.13+)
18    pub has_landlock: bool,
19    /// Cgroup v2 unified hierarchy is mounted
20    pub has_cgroup_v2: bool,
21    /// Cgroup delegation is available for current user
22    pub has_cgroup_delegation: bool,
23}
24
25impl SystemCapabilities {
26    /// Detect all available capabilities on the current system
27    pub fn detect() -> Self {
28        Self {
29            has_root: detect_root(),
30            has_user_namespaces: detect_user_namespaces(),
31            has_seccomp: detect_seccomp(),
32            has_landlock: detect_landlock(),
33            has_cgroup_v2: detect_cgroup_v2(),
34            has_cgroup_delegation: detect_cgroup_delegation(),
35        }
36    }
37
38    /// Check if unprivileged sandboxing is possible (without root)
39    pub fn can_sandbox_unprivileged(&self) -> bool {
40        // At minimum we need seccomp (always available on modern kernels)
41        // User namespaces and landlock are bonuses
42        self.has_seccomp
43    }
44
45    /// Check if full privileged sandboxing is possible
46    pub fn can_sandbox_privileged(&self) -> bool {
47        self.has_root && self.has_cgroup_v2
48    }
49
50    /// Get a human-readable summary of capabilities
51    pub fn summary(&self) -> String {
52        let mut lines = Vec::new();
53        let check = |available: bool| if available { "[ok]" } else { "[--]" };
54
55        lines.push(format!("{} Root privileges", check(self.has_root)));
56        lines.push(format!(
57            "{} User namespaces",
58            check(self.has_user_namespaces)
59        ));
60        lines.push(format!("{} Seccomp BPF", check(self.has_seccomp)));
61        lines.push(format!("{} Landlock LSM", check(self.has_landlock)));
62        lines.push(format!("{} Cgroup v2", check(self.has_cgroup_v2)));
63        lines.push(format!(
64            "{} Cgroup delegation",
65            check(self.has_cgroup_delegation)
66        ));
67
68        lines.join("\n")
69    }
70}
71
72fn detect_root() -> bool {
73    unsafe { libc::geteuid() == 0 }
74}
75
76fn detect_user_namespaces() -> bool {
77    // Check if unprivileged user namespaces are enabled
78    if let Ok(content) = std::fs::read_to_string("/proc/sys/kernel/unprivileged_userns_clone")
79        && content.trim() == "0"
80    {
81        return false;
82    }
83
84    // Also check max_user_namespaces > 0
85    if let Ok(content) = std::fs::read_to_string("/proc/sys/user/max_user_namespaces")
86        && let Ok(max) = content.trim().parse::<u64>()
87    {
88        return max > 0;
89    }
90
91    // If we can't read the files, assume available on modern kernels
92    true
93}
94
95fn detect_seccomp() -> bool {
96    // Check if seccomp is available via prctl
97    let ret = unsafe { libc::prctl(libc::PR_GET_SECCOMP, 0, 0, 0, 0) };
98    // Returns 0 if seccomp mode is disabled (available but not active)
99    // Returns -1 with EINVAL if seccomp is not built into kernel
100    ret >= 0
101}
102
103fn detect_landlock() -> bool {
104    // Use LANDLOCK_CREATE_RULESET_VERSION to query ABI version.
105    // With flags=1 and NULL attrs, this returns the highest supported
106    // ABI version (an integer >= 1), NOT a file descriptor.
107    let ret = unsafe {
108        libc::syscall(
109            libc::SYS_landlock_create_ruleset,
110            std::ptr::null::<libc::c_void>(),
111            0usize,
112            1u32, // LANDLOCK_CREATE_RULESET_VERSION
113        )
114    };
115
116    if ret >= 0 {
117        // ret is the ABI version number, not a fd — do NOT close it
118        return true;
119    }
120
121    // ENOSYS means landlock syscall doesn't exist
122    let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
123    errno != libc::ENOSYS
124}
125
126fn detect_cgroup_v2() -> bool {
127    Path::new("/sys/fs/cgroup/cgroup.controllers").exists()
128}
129
130fn detect_cgroup_delegation() -> bool {
131    // Check if current user has a delegated cgroup slice
132    let uid = unsafe { libc::geteuid() };
133    if uid == 0 {
134        return true; // root always has access
135    }
136
137    let user_slice = format!("/sys/fs/cgroup/user.slice/user-{}.slice", uid);
138    let path = Path::new(&user_slice);
139
140    if !path.exists() {
141        return false;
142    }
143
144    // Check if we can write to the cgroup directory
145    let test_path = path.join("sandbox-test-probe");
146    match std::fs::create_dir(&test_path) {
147        Ok(()) => {
148            let _ = std::fs::remove_dir(&test_path);
149            true
150        }
151        Err(_) => false,
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn detect_returns_valid_capabilities() {
161        let caps = SystemCapabilities::detect();
162        // Just verify detection doesn't panic
163        let _ = caps.has_root;
164        let _ = caps.has_seccomp;
165        let _ = caps.has_user_namespaces;
166        let _ = caps.has_landlock;
167        let _ = caps.has_cgroup_v2;
168        let _ = caps.has_cgroup_delegation;
169    }
170
171    #[test]
172    fn summary_produces_output() {
173        let caps = SystemCapabilities::detect();
174        let summary = caps.summary();
175        assert!(!summary.is_empty());
176        assert!(summary.contains("Root privileges"));
177        assert!(summary.contains("Seccomp BPF"));
178    }
179
180    #[test]
181    fn seccomp_detection_works() {
182        // On any modern Linux kernel, seccomp should be available
183        let has = detect_seccomp();
184        // We can't assert true universally, but it shouldn't panic
185        let _ = has;
186    }
187
188    #[test]
189    fn root_detection_matches_euid() {
190        let detected = detect_root();
191        let actual = unsafe { libc::geteuid() == 0 };
192        assert_eq!(detected, actual);
193    }
194}