Skip to main content

evalbox_sys/
check.rs

1//! System capability checking.
2//!
3//! Verifies at runtime that the kernel supports all required features for sandboxing.
4//! The check is performed once and cached in a static `OnceLock`.
5//!
6//! ## Required Features
7//!
8//! | Feature | Minimum | Check Method |
9//! |---------|---------|--------------|
10//! | Kernel | 5.13 | `uname` syscall |
11//! | Landlock | ABI 1 | `landlock_create_ruleset` with VERSION flag |
12//! | User NS | enabled | `/proc/sys/kernel/unprivileged_userns_clone` or fork+unshare test |
13//! | Seccomp | enabled | `prctl(PR_GET_SECCOMP)` |
14//!
15//! ## Usage
16//!
17//! ```ignore
18//! match check::check() {
19//!     Ok(info) => println!("Landlock ABI: {}", info.landlock_abi),
20//!     Err(e) => eprintln!("System not supported: {}", e),
21//! }
22//! ```
23//!
24//! ## User Namespaces
25//!
26//! User namespace support varies by distribution:
27//! - **Debian/Ubuntu**: `/proc/sys/kernel/unprivileged_userns_clone`
28//! - **NixOS/Fedora**: `/proc/sys/user/max_user_namespaces`
29//! - **Fallback**: Fork + unshare test
30
31use std::sync::OnceLock;
32
33use rustix::system::uname;
34use thiserror::Error;
35
36use crate::landlock;
37use crate::seccomp;
38
39/// Information about the system's sandboxing capabilities.
40#[derive(Debug, Clone)]
41pub struct SystemInfo {
42    pub kernel_version: (u32, u32, u32),
43    pub landlock_abi: u32,
44    pub user_ns_enabled: bool,
45    pub seccomp_enabled: bool,
46}
47
48/// Errors that can occur during system capability checking.
49#[derive(Debug, Clone, Error)]
50pub enum CheckError {
51    #[error("kernel version {}.{}.{} is too old, need at least {}.{}.{}", .found.0, .found.1, .found.2, .required.0, .required.1, .required.2)]
52    KernelTooOld { required: (u32, u32, u32), found: (u32, u32, u32) },
53
54    #[error("landlock is not available")]
55    LandlockNotAvailable,
56
57    #[error("user namespaces are disabled")]
58    UserNamespacesDisabled,
59
60    #[error("seccomp is not available")]
61    SeccompNotAvailable,
62
63    #[error("failed to read kernel version")]
64    KernelVersionReadFailed,
65}
66
67// Minimum kernel version: 5.13 (first with Landlock)
68const MIN_KERNEL_VERSION: (u32, u32, u32) = (5, 13, 0);
69
70static SYSTEM_INFO: OnceLock<Result<SystemInfo, CheckError>> = OnceLock::new();
71
72/// Check system capabilities and cache the result.
73///
74/// This function checks all required system capabilities for sandboxing
75/// and caches the result. Subsequent calls return the cached result.
76pub fn check() -> Result<&'static SystemInfo, &'static CheckError> {
77    SYSTEM_INFO.get_or_init(check_impl).as_ref()
78}
79
80fn check_impl() -> Result<SystemInfo, CheckError> {
81    let kernel_version = get_kernel_version()?;
82    if kernel_version < MIN_KERNEL_VERSION {
83        return Err(CheckError::KernelTooOld {
84            required: MIN_KERNEL_VERSION,
85            found: kernel_version,
86        });
87    }
88
89    let landlock_abi = landlock::landlock_abi_version().unwrap_or(0);
90    if landlock_abi == 0 {
91        return Err(CheckError::LandlockNotAvailable);
92    }
93
94    let user_ns_enabled = check_user_namespaces();
95    if !user_ns_enabled {
96        return Err(CheckError::UserNamespacesDisabled);
97    }
98
99    let seccomp_enabled = seccomp::seccomp_available();
100    if !seccomp_enabled {
101        return Err(CheckError::SeccompNotAvailable);
102    }
103
104    Ok(SystemInfo {
105        kernel_version,
106        landlock_abi,
107        user_ns_enabled,
108        seccomp_enabled,
109    })
110}
111
112fn get_kernel_version() -> Result<(u32, u32, u32), CheckError> {
113    let uts = uname();
114    let release = uts.release().to_str().map_err(|_| CheckError::KernelVersionReadFailed)?;
115    parse_kernel_version(release)
116}
117
118fn parse_kernel_version(release: &str) -> Result<(u32, u32, u32), CheckError> {
119    let parts: Vec<&str> = release.split('.').collect();
120    if parts.len() < 2 {
121        return Err(CheckError::KernelVersionReadFailed);
122    }
123
124    let major = parts[0]
125        .parse::<u32>()
126        .map_err(|_| CheckError::KernelVersionReadFailed)?;
127
128    let minor = parts[1]
129        .parse::<u32>()
130        .map_err(|_| CheckError::KernelVersionReadFailed)?;
131
132    // Patch might have additional suffix like "0-generic"
133    let patch = parts
134        .get(2)
135        .and_then(|p| p.split('-').next())
136        .and_then(|p| p.parse::<u32>().ok())
137        .unwrap_or(0);
138
139    Ok((major, minor, patch))
140}
141
142fn check_user_namespaces() -> bool {
143    // Check sysctl first (Debian/Ubuntu)
144    if let Ok(content) = std::fs::read_to_string("/proc/sys/kernel/unprivileged_userns_clone") {
145        return content.trim() == "1";
146    }
147
148    // Check max_user_namespaces (NixOS and others)
149    if let Ok(content) = std::fs::read_to_string("/proc/sys/user/max_user_namespaces")
150        && content.trim().parse::<u32>().unwrap_or(0) > 0
151    {
152        return true;
153    }
154
155    // Last resort: fork + unshare test (must fork to avoid polluting parent)
156    // SAFETY: fork/unshare/waitpid are safe when used correctly. Child exits immediately.
157    unsafe {
158        let pid = libc::fork();
159        if pid < 0 {
160            return false;
161        }
162        if pid == 0 {
163            let ret = libc::unshare(libc::CLONE_NEWUSER);
164            libc::_exit(if ret == 0 { 0 } else { 1 });
165        }
166        let mut status: i32 = 0;
167        libc::waitpid(pid, &mut status, 0);
168        libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_parse_kernel_version() {
178        assert_eq!(parse_kernel_version("5.15.0").unwrap(), (5, 15, 0));
179        assert_eq!(parse_kernel_version("6.1.0-generic").unwrap(), (6, 1, 0));
180        assert_eq!(parse_kernel_version("5.4.0-150-generic").unwrap(), (5, 4, 0));
181    }
182
183    #[test]
184    fn test_check() {
185        match check() {
186            Ok(info) => {
187                println!("Kernel version: {:?}", info.kernel_version);
188                println!("Landlock ABI: {}", info.landlock_abi);
189                println!("User NS enabled: {}", info.user_ns_enabled);
190                println!("Seccomp enabled: {}", info.seccomp_enabled);
191            }
192            Err(e) => {
193                println!("System check failed: {}", e);
194            }
195        }
196    }
197}