use super::report::*;
use std::path::PathBuf;
pub fn probe_system() -> DiagnosticReport {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let platform = format!("{} {}", os, arch);
let kernel = probe_kernel();
let lsms = probe_lsms();
let landlock = probe_landlock(&lsms);
let bpf_lsm = probe_bpf_lsm(&lsms);
let helper = probe_helper();
let (backend_impl, caps) = crate::backend::detect_backend();
let backend_name = backend_impl.name();
let mode = if caps.enforce_fs {
"Enforcement"
} else {
"Containment"
};
let status = if os != "linux" {
SystemStatus::Unsupported
} else {
match backend_name {
"BPF-LSM" => SystemStatus::Ready,
"Landlock" => SystemStatus::Degraded,
_ => SystemStatus::Degraded,
}
};
let reason = if backend_name == "Landlock" && !helper.exists {
"Landlock available; BPF helper not installed".to_string()
} else if backend_name == "NoopAudit" {
"No supported containment features".to_string()
} else {
match status {
SystemStatus::Ready => "System fully supported".to_string(),
SystemStatus::Degraded => "Running with reduced capabilities".to_string(),
SystemStatus::Unsupported => "Platform not supported".to_string(),
}
};
let backend = BackendSelection {
selected: backend_name.to_string(),
mode: mode.to_string(),
reason,
};
let mut suggestions = Vec::new();
if status == SystemStatus::Degraded && os == "linux" {
suggestions.push("Run `assay setup --apply` to install privileged helper".to_string());
}
DiagnosticReport {
schema: DOCTOR_REPORT_SCHEMA,
assay_version: env!("CARGO_PKG_VERSION").to_string(),
platform,
kernel,
lsms,
landlock,
bpf_lsm,
helper,
backend,
sandbox_features: SandboxFeatures::default(),
metrics: crate::metrics::get_all(),
status,
suggestions,
}
}
fn probe_kernel() -> Option<String> {
if cfg!(target_os = "linux") {
std::process::Command::new("uname")
.arg("-r")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
} else {
None
}
}
fn probe_lsms() -> Vec<String> {
if let Ok(c) = std::fs::read_to_string("/sys/kernel/security/lsm") {
c.trim().split(',').map(String::from).collect()
} else {
Vec::new()
}
}
fn probe_landlock(lsms: &[String]) -> LandlockStatus {
let available = lsms.contains(&"landlock".to_string());
let (abi_probe_status, abi_version, abi_probe_errno) = query_landlock_abi();
let net_connect_tcp_supported = net_connect_tcp_supported(abi_version);
let net_bind_tcp_supported = net_bind_tcp_supported(abi_version);
let no_new_privs_settable = probe_no_new_privs_settable();
let (net_connect_ruleset_probe, net_connect_ruleset_errno) =
super::landlock_net_smoke::probe_net_connect_ruleset(abi_version);
LandlockStatus {
available,
fs_enforce: available,
net_enforce: net_connect_tcp_supported,
abi_version,
abi_version_source: if abi_version.is_some() {
"landlock_create_ruleset_version"
} else {
"none"
},
abi_probe_status,
abi_probe_errno,
net_connect_tcp_supported,
net_bind_tcp_supported,
no_new_privs_settable,
net_connect_ruleset_probe,
net_connect_ruleset_errno,
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn classify_abi(ret: i64, errno: i32) -> (LandlockAbiProbeStatus, Option<u32>, Option<i32>) {
if ret >= 1 {
(LandlockAbiProbeStatus::Ok, Some(ret as u32), None)
} else if errno == libc::ENOSYS {
(LandlockAbiProbeStatus::Unsupported, None, Some(errno))
} else if errno == libc::EOPNOTSUPP {
(LandlockAbiProbeStatus::Disabled, None, Some(errno))
} else {
(LandlockAbiProbeStatus::Error, None, Some(errno))
}
}
fn net_connect_tcp_supported(abi: Option<u32>) -> bool {
abi.is_some_and(|v| v >= 4)
}
fn net_bind_tcp_supported(abi: Option<u32>) -> bool {
abi.is_some_and(|v| v >= 4)
}
#[cfg(target_os = "linux")]
fn query_landlock_abi() -> (LandlockAbiProbeStatus, Option<u32>, Option<i32>) {
const LANDLOCK_CREATE_RULESET_VERSION: libc::c_long = 1;
let ret = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
std::ptr::null::<libc::c_void>(),
0usize,
LANDLOCK_CREATE_RULESET_VERSION,
)
};
let errno = if ret < 0 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
} else {
0
};
classify_abi(ret as i64, errno)
}
#[cfg(not(target_os = "linux"))]
fn query_landlock_abi() -> (LandlockAbiProbeStatus, Option<u32>, Option<i32>) {
(LandlockAbiProbeStatus::Unsupported, None, None)
}
#[cfg(target_os = "linux")]
fn probe_no_new_privs_settable() -> bool {
const PR_SET_NO_NEW_PRIVS: libc::c_int = 38;
const PR_GET_NO_NEW_PRIVS: libc::c_int = 39;
unsafe {
let pid = libc::fork();
if pid == 0 {
let set = libc::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
let get = libc::prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0);
libc::_exit(if set == 0 && get == 1 { 0 } else { 1 });
} else if pid > 0 {
let mut status: libc::c_int = 0;
if libc::waitpid(pid, &mut status, 0) < 0 {
return false;
}
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0
} else {
false
}
}
}
#[cfg(not(target_os = "linux"))]
fn probe_no_new_privs_settable() -> bool {
false
}
fn probe_bpf_lsm(lsms: &[String]) -> BpfLsmStatus {
BpfLsmStatus {
available: lsms.contains(&"bpf".to_string()),
}
}
fn probe_helper() -> HelperStatus {
let path = PathBuf::from("/usr/local/bin/assay-bpf");
let socket = PathBuf::from("/run/assay/bpf.sock");
HelperStatus {
exists: path.exists(),
path,
version: None,
socket_exists: socket.exists(),
socket,
}
}
#[cfg(test)]
mod landlock_probe_tests {
use super::*;
#[test]
fn abi_ok_returns_version_and_no_errno() {
assert_eq!(
classify_abi(4, 0),
(LandlockAbiProbeStatus::Ok, Some(4), None)
);
assert_eq!(
classify_abi(1, 0),
(LandlockAbiProbeStatus::Ok, Some(1), None)
);
}
#[test]
fn abi_enosys_is_unsupported() {
assert_eq!(
classify_abi(-1, libc::ENOSYS),
(
LandlockAbiProbeStatus::Unsupported,
None,
Some(libc::ENOSYS)
)
);
}
#[test]
fn abi_eopnotsupp_is_disabled() {
assert_eq!(
classify_abi(-1, libc::EOPNOTSUPP),
(
LandlockAbiProbeStatus::Disabled,
None,
Some(libc::EOPNOTSUPP)
)
);
}
#[test]
fn abi_other_errno_is_error_with_errno() {
assert_eq!(
classify_abi(-1, libc::EPERM),
(LandlockAbiProbeStatus::Error, None, Some(libc::EPERM))
);
}
#[test]
fn net_caps_require_abi_4() {
assert!(!net_connect_tcp_supported(None));
assert!(!net_connect_tcp_supported(Some(3)));
assert!(net_connect_tcp_supported(Some(4)));
assert!(net_connect_tcp_supported(Some(5)));
assert!(!net_bind_tcp_supported(Some(3)));
assert!(net_bind_tcp_supported(Some(4)));
}
#[test]
fn no_new_privs_probe_returns_without_panic() {
let _: bool = probe_no_new_privs_settable();
}
#[test]
fn report_is_additive_and_keeps_old_fields() {
let status = probe_landlock(&[]);
let v = serde_json::to_value(&status).unwrap();
for k in ["available", "fs_enforce", "net_enforce", "abi_version"] {
assert!(v.get(k).is_some(), "missing pre-existing field {k}");
}
for k in [
"abi_version_source",
"abi_probe_status",
"abi_probe_errno",
"net_connect_tcp_supported",
"net_bind_tcp_supported",
"no_new_privs_settable",
"net_connect_ruleset_probe",
"net_connect_ruleset_errno",
] {
assert!(v.get(k).is_some(), "missing new field {k}");
}
}
#[test]
fn report_is_self_describing_with_schema_id() {
let report = probe_system();
let v = serde_json::to_value(&report).unwrap();
assert_eq!(
v.get("schema").and_then(|s| s.as_str()),
Some(DOCTOR_REPORT_SCHEMA),
"doctor report must carry its schema id so the shape is an explicit version, not silent drift"
);
}
}