use std::ffi::CStr;
use std::fs::OpenOptions;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use super::host::{Check, Fix, FixCommand, Problem, Section};
const KVM_DEVICE: &str = "/dev/kvm";
const KVM_AMD_AVIC_PARAMETER: &str = "/sys/module/kvm_amd/parameters/avic";
const KVM_INTEL_APICV_PARAMETER: &str = "/sys/module/kvm_intel/parameters/enable_apicv";
#[derive(Debug, Clone)]
struct DeviceGroup {
name: Option<String>,
gid: libc::gid_t,
grants_read_write: bool,
}
#[derive(Debug, Clone)]
struct UserInfo {
name: String,
primary_gid: libc::gid_t,
}
pub(super) fn host_section() -> (Section, Vec<Problem>) {
let mut checks = Vec::new();
let mut problems = Vec::new();
let arch = std::env::consts::ARCH;
checks.push(Check::info("Platform", &format!("Linux {arch}")));
if arch == "x86_64" {
match cpu_virt_flag() {
Some(flag) => checks.push(Check::pass("CPU virt", flag)),
None => {
checks.push(Check::fail("CPU virt", "not found"));
problems.push(Problem::new(
"CPU virtualization is not available",
vec![
"no vmx (Intel) or svm (AMD) flag in /proc/cpuinfo".to_string(),
"enable virtualization (VT-x / AMD-V) in your BIOS or UEFI firmware"
.to_string(),
"inside a VM, enable nested virtualization on the host".to_string(),
],
));
}
}
}
let kvm = Path::new(KVM_DEVICE);
if !kvm.exists() {
checks.push(Check::fail("KVM device", "missing"));
let mut problem = Problem::new(
format!("{KVM_DEVICE} is not present"),
vec![
"the KVM kernel module is not loaded".to_string(),
"in containers or CI, the host must expose /dev/kvm to this environment"
.to_string(),
],
);
if let Some(module) = kvm_module() {
problem = problem.with_fix(Fix::new(
format!("load the {module} kernel module"),
vec![FixCommand::sudo(&["modprobe", module])],
));
}
problems.push(problem);
return (section(checks), problems);
}
checks.push(Check::pass("KVM device", KVM_DEVICE));
match OpenOptions::new().read(true).write(true).open(kvm) {
Ok(_) => checks.push(Check::pass("KVM access", "read/write")),
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
checks.push(Check::fail("KVM access", "permission denied"));
problems.push(kvm_permission_problem());
}
Err(err) => {
checks.push(Check::fail("KVM access", "unavailable"));
problems.push(Problem::new(
format!("{KVM_DEVICE} could not be opened"),
vec![format!("opening {KVM_DEVICE} failed: {err}")],
));
}
}
if arch == "x86_64"
&& let Some(check) = kvm_apic_acceleration_check()
{
checks.push(check);
}
(section(checks), problems)
}
fn section(checks: Vec<Check>) -> Section {
Section {
title: "Host".to_string(),
checks,
}
}
fn cpu_virt_flag() -> Option<&'static str> {
let info = std::fs::read_to_string("/proc/cpuinfo").ok()?;
cpu_virt_flag_from(&info)
}
fn cpu_virt_flag_from(info: &str) -> Option<&'static str> {
for line in info.lines() {
if line.starts_with("flags") {
let flags = line.split(':').nth(1).unwrap_or("");
if flags.split_whitespace().any(|f| f == "vmx") {
return Some("vmx");
}
if flags.split_whitespace().any(|f| f == "svm") {
return Some("svm");
}
}
}
None
}
fn kvm_apic_acceleration_check() -> Option<Check> {
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").ok()?;
match cpu_virt_flag_from(&cpuinfo) {
Some("svm") => {
let parameter = std::fs::read_to_string(KVM_AMD_AVIC_PARAMETER).ok();
kvm_avic_check_from(&cpuinfo, parameter.as_deref())
}
Some("vmx") => {
let parameter = std::fs::read_to_string(KVM_INTEL_APICV_PARAMETER).ok();
kvm_apicv_check_from(&cpuinfo, parameter.as_deref())
}
_ => None,
}
}
fn kvm_avic_check_from(cpuinfo: &str, parameter: Option<&str>) -> Option<Check> {
if cpu_virt_flag_from(cpuinfo) != Some("svm") || !cpu_flag_present(cpuinfo, "avic") {
return None;
}
let x2avic = cpu_flag_present(cpuinfo, "x2avic");
match parse_module_bool(parameter?) {
Some(true) if x2avic => Some(Check::pass("KVM AVIC", "enabled (x2AVIC available)")),
Some(true) => Some(Check::pass("KVM AVIC", "enabled")),
Some(false) if x2avic => Some(Check::warn("KVM AVIC", "disabled — x2AVIC available")),
Some(false) => Some(Check::warn("KVM AVIC", "disabled — optional acceleration")),
None => None,
}
}
fn kvm_apicv_check_from(cpuinfo: &str, parameter: Option<&str>) -> Option<Check> {
if cpu_virt_flag_from(cpuinfo) != Some("vmx") {
return None;
}
match parse_module_bool(parameter?) {
Some(true) => Some(Check::pass("KVM APICv", "enabled by KVM policy")),
Some(false) => Some(Check::warn("KVM APICv", "disabled by KVM policy")),
None => None,
}
}
fn cpu_flag_present(info: &str, expected: &str) -> bool {
info.lines().any(|line| {
line.starts_with("flags")
&& line
.split_once(':')
.is_some_and(|(_, flags)| flags.split_whitespace().any(|flag| flag == expected))
})
}
fn parse_module_bool(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "on" | "y" | "yes" => Some(true),
"0" | "off" | "n" | "no" => Some(false),
_ => None,
}
}
fn kvm_permission_problem() -> Problem {
let device_group = device_group(KVM_DEVICE);
let user = current_user();
let process_member = device_group
.as_ref()
.map(|group| process_has_group(group.gid))
.unwrap_or(false);
let persistent_member = match (&user, &device_group) {
(Some(user), Some(group)) => persistent_user_in_group(user, group.gid),
_ => false,
};
let cause = match &device_group {
Some(group) if persistent_member && !process_member => {
let label = group_label(group);
format!("you are in the '{label}' group, but this login session predates the change")
}
Some(group) if process_member => {
let label = group_label(group);
format!(
"your process belongs to the '{label}' group, but {KVM_DEVICE} still rejects read/write access"
)
}
Some(group) if group.grants_read_write => {
let label = group_label(group);
format!("{KVM_DEVICE} is owned by group '{label}', which your user is not in")
}
Some(group) => {
let label = group_label(group);
format!(
"{KVM_DEVICE} is owned by group '{label}', but group permissions do not grant read/write access"
)
}
None => format!("your user lacks read/write access to {KVM_DEVICE}"),
};
let mut problem = Problem::new(
format!("{KVM_DEVICE} is not accessible by your user"),
vec![cause],
);
let Some(user) = user else {
return problem;
};
let acl = format!("u:{}:rw", user.name);
if persistent_member || process_member || !can_offer_group_fix(device_group.as_ref()) {
problem = problem.with_fix(Fix::new(
format!(
"grant {} access to {KVM_DEVICE} for the current session",
user.name
),
vec![FixCommand::sudo(&[
"setfacl",
"-m",
acl.as_str(),
KVM_DEVICE,
])],
));
} else {
let group = device_group
.as_ref()
.and_then(|group| group.name.as_deref())
.expect("safe KVM group fixes require a named device group");
problem = problem.with_fix(
Fix::new(
format!(
"add {} to the '{group}' group and grant access now",
user.name
),
vec![
FixCommand::sudo(&["usermod", "-aG", group, user.name.as_str()]),
FixCommand::sudo(&["setfacl", "-m", acl.as_str(), KVM_DEVICE]),
],
)
.requires_relogin(),
);
}
problem
}
fn kvm_module() -> Option<&'static str> {
match cpu_virt_flag() {
Some("vmx") => Some("kvm_intel"),
Some("svm") => Some("kvm_amd"),
_ => None,
}
}
fn current_user() -> Option<UserInfo> {
unsafe {
let entry = libc::getpwuid(libc::geteuid());
if entry.is_null() {
return None;
}
Some(UserInfo {
name: CStr::from_ptr((*entry).pw_name)
.to_string_lossy()
.into_owned(),
primary_gid: (*entry).pw_gid,
})
}
}
fn device_group(path: &str) -> Option<DeviceGroup> {
let metadata = std::fs::metadata(path).ok()?;
let gid = metadata.gid();
Some(DeviceGroup {
name: group_name(gid),
gid,
grants_read_write: metadata.mode() & 0o060 == 0o060,
})
}
fn group_name(gid: libc::gid_t) -> Option<String> {
unsafe {
let entry = libc::getgrgid(gid);
if entry.is_null() {
return None;
}
Some(
CStr::from_ptr((*entry).gr_name)
.to_string_lossy()
.into_owned(),
)
}
}
fn process_has_group(gid: libc::gid_t) -> bool {
if unsafe { libc::getegid() } == gid {
return true;
}
let count = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
if count <= 0 {
return false;
}
let mut gids = vec![0 as libc::gid_t; count as usize];
let filled = unsafe { libc::getgroups(count, gids.as_mut_ptr()) };
if filled < 0 {
return false;
}
gids.truncate(filled as usize);
gids.into_iter().any(|g| g == gid)
}
fn persistent_user_in_group(user: &UserInfo, gid: libc::gid_t) -> bool {
user.primary_gid == gid || group_has_member(gid, &user.name)
}
fn group_has_member(gid: libc::gid_t, user: &str) -> bool {
unsafe {
let entry = libc::getgrgid(gid);
if entry.is_null() {
return false;
}
let mut member = (*entry).gr_mem;
while !member.is_null() && !(*member).is_null() {
if CStr::from_ptr(*member).to_string_lossy() == user {
return true;
}
member = member.add(1);
}
}
false
}
fn can_offer_group_fix(group: Option<&DeviceGroup>) -> bool {
matches!(
group,
Some(DeviceGroup {
name: Some(name),
grants_read_write: true,
..
}) if name == "kvm"
)
}
fn group_label(group: &DeviceGroup) -> String {
group
.name
.clone()
.unwrap_or_else(|| format!("gid {}", group.gid))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpu_flags_are_read_from_the_flags_field() {
let cpuinfo = "processor: 0\nflags: fpu svm avic x2avic\n";
assert_eq!(cpu_virt_flag_from(cpuinfo), Some("svm"));
assert!(cpu_flag_present(cpuinfo, "avic"));
assert!(cpu_flag_present(cpuinfo, "x2avic"));
assert!(!cpu_flag_present(cpuinfo, "vmx"));
}
#[test]
fn module_boolean_parser_accepts_sysfs_shapes() {
assert_eq!(parse_module_bool("Y\n"), Some(true));
assert_eq!(parse_module_bool("1"), Some(true));
assert_eq!(parse_module_bool("N\n"), Some(false));
assert_eq!(parse_module_bool("0"), Some(false));
assert_eq!(parse_module_bool("auto"), None);
}
#[test]
fn avic_check_distinguishes_enabled_disabled_and_irrelevant_hosts() {
let amd = "processor: 0\nflags: fpu svm avic x2avic\n";
let amd_without_avic = "processor: 0\nflags: fpu svm\n";
let intel = "processor: 0\nflags: fpu vmx\n";
let enabled = kvm_avic_check_from(amd, Some("Y\n")).unwrap();
assert_eq!(enabled.state, super::super::host::CheckState::Pass);
assert_eq!(enabled.value, "enabled (x2AVIC available)");
let disabled = kvm_avic_check_from(amd, Some("N\n")).unwrap();
assert_eq!(disabled.state, super::super::host::CheckState::Warn);
assert_eq!(disabled.value, "disabled — x2AVIC available");
assert!(kvm_avic_check_from(intel, None).is_none());
assert!(kvm_avic_check_from(amd_without_avic, Some("Y\n")).is_none());
assert!(kvm_avic_check_from(amd, None).is_none());
assert!(kvm_avic_check_from(amd, Some("unknown")).is_none());
}
#[test]
fn apicv_check_reports_intel_kvm_policy_only_when_known() {
let intel = "processor: 0\nflags: fpu vmx\n";
let amd = "processor: 0\nflags: fpu svm avic x2avic\n";
let enabled = kvm_apicv_check_from(intel, Some("Y\n")).unwrap();
assert_eq!(enabled.state, super::super::host::CheckState::Pass);
assert_eq!(enabled.value, "enabled by KVM policy");
let disabled = kvm_apicv_check_from(intel, Some("N\n")).unwrap();
assert_eq!(disabled.state, super::super::host::CheckState::Warn);
assert_eq!(disabled.value, "disabled by KVM policy");
assert!(kvm_apicv_check_from(amd, Some("Y\n")).is_none());
assert!(kvm_apicv_check_from(intel, None).is_none());
assert!(kvm_apicv_check_from(intel, Some("unknown")).is_none());
}
}