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 | 6.12 | `uname` syscall |
11//! | Landlock | ABI 5 | `landlock_create_ruleset` with VERSION flag |
12//! | Seccomp | enabled | `prctl(PR_GET_SECCOMP)` |
13//!
14//! ## Usage
15//!
16//! ```ignore
17//! match check::check() {
18//!     Ok(info) => println!("Landlock ABI: {}", info.landlock_abi),
19//!     Err(e) => eprintln!("System not supported: {}", e),
20//! }
21//! ```
22
23use std::sync::OnceLock;
24
25use rustix::system::uname;
26use thiserror::Error;
27
28use crate::landlock;
29use crate::seccomp;
30
31/// Information about the system's sandboxing capabilities.
32#[derive(Debug, Clone)]
33pub struct SystemInfo {
34    pub kernel_version: (u32, u32, u32),
35    pub landlock_abi: u32,
36    pub seccomp_enabled: bool,
37}
38
39/// Errors that can occur during system capability checking.
40#[derive(Debug, Clone, Error)]
41pub enum CheckError {
42    #[error("kernel version {}.{}.{} is too old, need at least {}.{}.{}", .found.0, .found.1, .found.2, .required.0, .required.1, .required.2)]
43    KernelTooOld {
44        required: (u32, u32, u32),
45        found: (u32, u32, u32),
46    },
47
48    #[error("landlock is not available")]
49    LandlockNotAvailable,
50
51    #[error("landlock ABI {found} is too old, need at least ABI {required}")]
52    LandlockAbiTooOld { required: u32, found: u32 },
53
54    #[error("seccomp is not available")]
55    SeccompNotAvailable,
56
57    #[error("failed to read kernel version")]
58    KernelVersionReadFailed,
59}
60
61// Minimum kernel version: 6.12 (Landlock ABI 5 with SCOPE_SIGNAL + SCOPE_ABSTRACT_UNIX_SOCKET)
62const MIN_KERNEL_VERSION: (u32, u32, u32) = (6, 12, 0);
63const MIN_LANDLOCK_ABI: u32 = 5;
64
65static SYSTEM_INFO: OnceLock<Result<SystemInfo, CheckError>> = OnceLock::new();
66
67/// Check system capabilities and cache the result.
68///
69/// This function checks all required system capabilities for sandboxing
70/// and caches the result. Subsequent calls return the cached result.
71pub fn check() -> Result<&'static SystemInfo, &'static CheckError> {
72    SYSTEM_INFO.get_or_init(check_impl).as_ref()
73}
74
75fn check_impl() -> Result<SystemInfo, CheckError> {
76    let kernel_version = get_kernel_version()?;
77    if kernel_version < MIN_KERNEL_VERSION {
78        return Err(CheckError::KernelTooOld {
79            required: MIN_KERNEL_VERSION,
80            found: kernel_version,
81        });
82    }
83
84    let landlock_abi = landlock::landlock_abi_version().unwrap_or(0);
85    if landlock_abi == 0 {
86        return Err(CheckError::LandlockNotAvailable);
87    }
88    if landlock_abi < MIN_LANDLOCK_ABI {
89        return Err(CheckError::LandlockAbiTooOld {
90            required: MIN_LANDLOCK_ABI,
91            found: landlock_abi,
92        });
93    }
94
95    let seccomp_enabled = seccomp::seccomp_available();
96    if !seccomp_enabled {
97        return Err(CheckError::SeccompNotAvailable);
98    }
99
100    Ok(SystemInfo {
101        kernel_version,
102        landlock_abi,
103        seccomp_enabled,
104    })
105}
106
107fn get_kernel_version() -> Result<(u32, u32, u32), CheckError> {
108    let uts = uname();
109    let release = uts
110        .release()
111        .to_str()
112        .map_err(|_| CheckError::KernelVersionReadFailed)?;
113    parse_kernel_version(release)
114}
115
116fn parse_kernel_version(release: &str) -> Result<(u32, u32, u32), CheckError> {
117    let parts: Vec<&str> = release.split('.').collect();
118    if parts.len() < 2 {
119        return Err(CheckError::KernelVersionReadFailed);
120    }
121
122    let major = parts[0]
123        .parse::<u32>()
124        .map_err(|_| CheckError::KernelVersionReadFailed)?;
125
126    let minor = parts[1]
127        .parse::<u32>()
128        .map_err(|_| CheckError::KernelVersionReadFailed)?;
129
130    // Patch might have additional suffix like "0-generic"
131    let patch = parts
132        .get(2)
133        .and_then(|p| p.split('-').next())
134        .and_then(|p| p.parse::<u32>().ok())
135        .unwrap_or(0);
136
137    Ok((major, minor, patch))
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_parse_kernel_version() {
146        assert_eq!(parse_kernel_version("5.15.0").unwrap(), (5, 15, 0));
147        assert_eq!(parse_kernel_version("6.1.0-generic").unwrap(), (6, 1, 0));
148        assert_eq!(
149            parse_kernel_version("5.4.0-150-generic").unwrap(),
150            (5, 4, 0)
151        );
152        assert_eq!(parse_kernel_version("6.12.0").unwrap(), (6, 12, 0));
153    }
154
155    #[test]
156    fn test_check() {
157        match check() {
158            Ok(info) => {
159                println!("Kernel version: {:?}", info.kernel_version);
160                println!("Landlock ABI: {}", info.landlock_abi);
161                println!("Seccomp enabled: {}", info.seccomp_enabled);
162            }
163            Err(e) => {
164                println!("System check failed: {e}");
165            }
166        }
167    }
168}