use crate::{error::CoreError, sandbox::BackendFeatures};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LandlockAbi {
Unsupported,
V1,
V2,
V3,
V4,
V5,
V6,
V7,
V8,
V9,
}
impl LandlockAbi {
pub fn supports_net_port_filter(self) -> bool {
self >= Self::V4
}
pub fn supports_truncate(self) -> bool {
self >= Self::V3
}
pub fn supports_ioctl_dev(self) -> bool {
self >= Self::V5
}
pub fn supports_scopes(self) -> bool {
self >= Self::V6
}
pub fn supports_unix_path_filter(self) -> bool {
self >= Self::V9
}
pub fn as_str(self) -> &'static str {
match self {
Self::Unsupported => "unsupported",
Self::V1 => "v1",
Self::V2 => "v2",
Self::V3 => "v3",
Self::V4 => "v4",
Self::V5 => "v5",
Self::V6 => "v6",
Self::V7 => "v7",
Self::V8 => "v8",
Self::V9 => "v9",
}
}
}
impl From<i64> for LandlockAbi {
fn from(value: i64) -> Self {
match value {
..=0 => Self::Unsupported,
1 => Self::V1,
2 => Self::V2,
3 => Self::V3,
4 => Self::V4,
5 => Self::V5,
6 => Self::V6,
7 => Self::V7,
8 => Self::V8,
_ => Self::V9,
}
}
}
#[derive(Debug, Clone)]
pub struct ProbeResult {
pub kernel: String,
pub abi: LandlockAbi,
}
impl ProbeResult {
pub fn features(&self) -> BackendFeatures {
BackendFeatures {
fs_write: self.abi >= LandlockAbi::V1,
fs_read: self.abi >= LandlockAbi::V1,
exec_allowlist: self.abi >= LandlockAbi::V1,
net_port_filter: self.abi.supports_net_port_filter(),
audit_stream: false,
}
}
}
pub fn run() -> Result<ProbeResult, CoreError> {
let kernel = kernel_version();
let abi = detect_abi();
if abi == LandlockAbi::Unsupported {
return Err(CoreError::BackendUnavailable {
reason: format!(
"Landlock LSM required (kernel ≥5.13). Detected: {kernel}. Build the kernel with \
CONFIG_SECURITY_LANDLOCK=y or run on a newer host."
),
});
}
Ok(ProbeResult { kernel, abi })
}
#[cfg(target_os = "linux")]
fn detect_abi() -> LandlockAbi {
const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1;
#[allow(unsafe_code)]
let raw = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
std::ptr::null::<libc::c_void>(),
0_usize,
LANDLOCK_CREATE_RULESET_VERSION,
)
};
LandlockAbi::from(raw as i64)
}
#[cfg(not(target_os = "linux"))]
fn detect_abi() -> LandlockAbi {
LandlockAbi::Unsupported
}
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
fn kernel_version() -> String {
if let Ok(output) = std::process::Command::new("uname").arg("-sr").output()
&& let Ok(text) = String::from_utf8(output.stdout)
{
let trimmed = text.trim();
if !trimmed.is_empty() {
return trimmed.to_owned();
}
}
"Linux (unknown)".to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_order_abi_tiers() {
assert!(LandlockAbi::V4 > LandlockAbi::V3);
assert!(LandlockAbi::V1 > LandlockAbi::Unsupported);
assert!(LandlockAbi::V9 >= LandlockAbi::V4);
}
#[test]
fn test_should_report_net_filter_capability() {
assert!(!LandlockAbi::V1.supports_net_port_filter());
assert!(!LandlockAbi::V3.supports_net_port_filter());
assert!(LandlockAbi::V4.supports_net_port_filter());
assert!(LandlockAbi::V9.supports_net_port_filter());
}
#[test]
fn test_should_report_scope_capability() {
assert!(!LandlockAbi::V5.supports_scopes());
assert!(LandlockAbi::V6.supports_scopes());
assert!(LandlockAbi::V9.supports_scopes());
}
#[test]
fn test_should_map_raw_abi_values() {
assert_eq!(LandlockAbi::from(0), LandlockAbi::Unsupported);
assert_eq!(LandlockAbi::from(-1), LandlockAbi::Unsupported);
assert_eq!(LandlockAbi::from(1), LandlockAbi::V1);
assert_eq!(LandlockAbi::from(4), LandlockAbi::V4);
assert_eq!(LandlockAbi::from(99), LandlockAbi::V9);
}
}