#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::util::HostDiagnostic;
use boxlite_shared::{BoxliteError, BoxliteResult};
pub(crate) trait HypervisorProbe: Send + Sync {
fn startup_check(&self) -> BoxliteResult<()>;
#[allow(dead_code)] fn diagnose_create_failure(&self, error: BoxliteError) -> BoxliteError;
}
pub struct SystemCheck {
#[cfg(target_os = "linux")]
_kvm: std::fs::File,
}
impl SystemCheck {
pub fn run() -> BoxliteResult<Self> {
#[cfg(target_os = "linux")]
{
let probe = KvmProbe;
probe.startup_check()?;
Ok(Self {
_kvm: probe.into_kvm_file(),
})
}
#[cfg(target_os = "macos")]
{
let probe = HvfProbe;
probe.startup_check()?;
Ok(Self {})
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err(BoxliteError::Unsupported(
"BoxLite only supports Linux and macOS".into(),
))
}
}
}
#[cfg(feature = "krun")]
pub(crate) fn hypervisor_probe() -> Box<dyn HypervisorProbe> {
#[cfg(target_os = "macos")]
{
Box::new(HvfProbe)
}
#[cfg(target_os = "linux")]
{
Box::new(KvmProbe)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Box::new(NoopProbe)
}
}
#[cfg(target_os = "linux")]
struct KvmProbe;
#[cfg(target_os = "linux")]
impl KvmProbe {
fn into_kvm_file(self) -> std::fs::File {
open_kvm().expect("KVM was validated in startup_check but re-open failed")
}
}
#[cfg(target_os = "linux")]
impl HypervisorProbe for KvmProbe {
fn startup_check(&self) -> BoxliteResult<()> {
let kvm = open_kvm()?;
smoke_test_kvm(&kvm)?;
Ok(())
}
fn diagnose_create_failure(&self, error: BoxliteError) -> BoxliteError {
error
}
}
#[cfg(target_os = "linux")]
fn open_kvm() -> BoxliteResult<std::fs::File> {
use std::path::Path;
const DEV: &str = "/dev/kvm";
if !Path::new(DEV).exists() {
let diagnostic = kvm_missing();
tracing::error!("{}", diagnostic.operator());
return Err(BoxliteError::Unsupported(diagnostic.into_client()));
}
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(DEV)
.map_err(|e| {
let diagnostic = match e.kind() {
std::io::ErrorKind::PermissionDenied => kvm_permission_denied(),
_ => kvm_open_failed(e.to_string()),
};
tracing::error!("{}", diagnostic.operator());
BoxliteError::Unsupported(diagnostic.into_client())
})
}
#[cfg(target_os = "linux")]
fn smoke_test_kvm(kvm: &std::fs::File) -> BoxliteResult<()> {
use std::os::fd::AsRawFd;
const KVM_EXIT_HLT: i32 = 5;
unsafe extern "C" {
fn boxlite_kvm_smoke_test(kvm_fd: libc::c_int) -> libc::c_int;
}
let exit_reason = unsafe { boxlite_kvm_smoke_test(kvm.as_raw_fd()) };
if exit_reason == KVM_EXIT_HLT {
return Ok(());
}
let kernel = std::fs::read_to_string("/proc/version")
.unwrap_or_default()
.split_whitespace()
.nth(2)
.unwrap_or("unknown")
.to_string();
let diagnostic = kvm_smoke_failed(exit_reason, KVM_EXIT_HLT, &kernel);
tracing::error!("{}", diagnostic.operator());
Err(BoxliteError::Unsupported(diagnostic.into_client()))
}
#[cfg(target_os = "linux")]
fn kvm_missing() -> HostDiagnostic {
let mut msg = String::from(
"/dev/kvm does not exist\n\n\
Suggestions:\n\
- Enable KVM in BIOS/UEFI (VT-x for Intel, AMD-V for AMD)\n\
- Load the KVM module: sudo modprobe kvm_intel # or kvm_amd\n\
- Check: lsmod | grep kvm",
);
if std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() {
msg.push_str(
"\n\nWSL2 detected:\n\
- Requires Windows 11 or Windows 10 build 21390+\n\
- Add 'nestedVirtualization=true' to .wslconfig\n\
- Restart WSL: wsl --shutdown",
);
}
HostDiagnostic::new("KVM is not available on this host.", msg)
}
#[cfg(target_os = "linux")]
fn kvm_permission_denied() -> HostDiagnostic {
HostDiagnostic::new(
"KVM is available but access is denied for the current user.",
"/dev/kvm: permission denied\n\n\
Fix:\n\
- sudo usermod -aG kvm $USER && newgrp kvm",
)
}
#[cfg(target_os = "linux")]
fn kvm_open_failed(error: String) -> HostDiagnostic {
HostDiagnostic::new("KVM could not be opened.", format!("/dev/kvm: {error}"))
}
#[cfg(target_os = "linux")]
fn kvm_smoke_failed(exit_reason: i32, expected: i32, kernel: &str) -> HostDiagnostic {
HostDiagnostic::new(
"KVM is present but cannot run a virtual machine on this host.",
format!(
"KVM smoke test failed: vCPU exit reason {exit_reason} (expected {expected})\n\n\
/dev/kvm exists but cannot execute guest code (host kernel: {kernel}).\n\n\
Suggestions:\n\
- Ensure nested virtualization is enabled (cloud instances need this explicitly)\n\
- Load the KVM module: sudo modprobe kvm_intel # or kvm_amd\n\
- Check: lsmod | grep kvm\n\
- See https://github.com/boxlite-ai/boxlite/blob/main/docs/faq.md"
),
)
}
#[cfg(target_os = "macos")]
#[allow(dead_code)] mod hvf_ffi {
pub const HV_SUCCESS: i32 = 0;
pub const HV_BUSY: i32 = -85377022;
pub const HV_NO_RESOURCES: i32 = -85377019;
pub const HV_DENIED: i32 = -85377017;
unsafe extern "C" {
pub fn hv_vm_create(config: *mut std::ffi::c_void) -> i32;
pub fn hv_vm_destroy() -> i32;
}
}
#[cfg(target_os = "macos")]
struct HvfProbe;
#[cfg(target_os = "macos")]
impl HypervisorProbe for HvfProbe {
fn startup_check(&self) -> BoxliteResult<()> {
check_hypervisor_framework()
}
fn diagnose_create_failure(&self, error: BoxliteError) -> BoxliteError {
let ret = unsafe { hvf_ffi::hv_vm_create(std::ptr::null_mut()) };
match ret {
hvf_ffi::HV_NO_RESOURCES => {
tracing::error!(
hvf_code = ret,
"HVF diagnostic: HV_NO_RESOURCES — VM address spaces exhausted \
(see sysctl kern.hv.max_address_spaces)"
);
BoxliteError::ResourceExhausted(
"macOS Hypervisor.framework VM address spaces are exhausted. \
Stop some running boxes and retry."
.into(),
)
}
hvf_ffi::HV_SUCCESS => {
unsafe {
hvf_ffi::hv_vm_destroy();
}
tracing::debug!(
"HVF diagnostic: hv_vm_create succeeded — failure is not HVF-related"
);
error
}
hvf_ffi::HV_BUSY => {
tracing::debug!("HVF diagnostic: HV_BUSY — VM exists, failure was post-creation");
error
}
hvf_ffi::HV_DENIED => {
tracing::error!(
hvf_code = ret,
"HVF diagnostic: HV_DENIED — missing entitlement \
(the shim binary needs com.apple.security.hypervisor)"
);
BoxliteError::Unsupported("Hypervisor.framework access is denied.".into())
}
_ => {
tracing::error!(
hvf_code = format!("{ret:#x}"),
"HVF diagnostic: unexpected error code (original error: {error})"
);
BoxliteError::Engine("The VM failed to start.".into())
}
}
}
}
#[cfg(target_os = "macos")]
fn check_hypervisor_framework() -> BoxliteResult<()> {
#[cfg(not(target_arch = "aarch64"))]
{
let diagnostic = hvf_unsupported_arch(std::env::consts::ARCH);
tracing::error!("{}", diagnostic.operator());
return Err(BoxliteError::Unsupported(diagnostic.into_client()));
}
#[cfg(target_arch = "aarch64")]
{
let output = std::process::Command::new("sysctl")
.arg("kern.hv_support")
.output()
.map_err(|e| {
let diagnostic = hvf_sysctl_failed(e.to_string());
tracing::error!("{}", diagnostic.operator());
BoxliteError::Unsupported(diagnostic.into_client())
})?;
if !output.status.success() {
let diagnostic = hvf_sysctl_failed("non-zero exit status".to_string());
tracing::error!("{}", diagnostic.operator());
return Err(BoxliteError::Unsupported(diagnostic.into_client()));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let value = stdout.split(':').nth(1).map(|s| s.trim()).unwrap_or("0");
if value == "1" {
Ok(())
} else {
let diagnostic = hvf_unavailable();
tracing::error!("{}", diagnostic.operator());
Err(BoxliteError::Unsupported(diagnostic.into_client()))
}
}
}
#[cfg(all(target_os = "macos", not(target_arch = "aarch64")))]
fn hvf_unsupported_arch(arch: &str) -> HostDiagnostic {
HostDiagnostic::new(
"BoxLite on macOS requires Apple Silicon (ARM64).",
format!(
"Unsupported architecture: {arch}\n\n\
BoxLite on macOS requires Apple Silicon (ARM64).\n\
Intel Macs are not supported."
),
)
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn hvf_sysctl_failed(error: String) -> HostDiagnostic {
HostDiagnostic::new(
"Hypervisor.framework could not be checked.",
format!(
"Failed to check Hypervisor.framework: {error}\n\n\
Check manually: sysctl kern.hv_support"
),
)
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn hvf_unavailable() -> HostDiagnostic {
HostDiagnostic::new(
"Hypervisor.framework is not available on this host.",
"Hypervisor.framework is not available\n\n\
Suggestions:\n\
- Verify macOS 10.10 or later\n\
- Check: sysctl kern.hv_support",
)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
struct NoopProbe;
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
impl HypervisorProbe for NoopProbe {
fn startup_check(&self) -> BoxliteResult<()> {
Err(BoxliteError::Unsupported(
"BoxLite only supports Linux and macOS".into(),
))
}
fn diagnose_create_failure(&self, error: BoxliteError) -> BoxliteError {
error
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_check_runs() {
match SystemCheck::run() {
Ok(_) => {} Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("kvm") || msg.contains("KVM") || msg.contains("Hypervisor"),
"Error should mention the hypervisor: {msg}"
);
}
}
}
#[test]
#[cfg(target_os = "linux")]
fn kvm_client_messages_are_safe() {
for (diag, box_id) in [
(kvm_missing(), "vIsLhQP34dQF"),
(kvm_permission_denied(), "vIsLhQP34dQF"),
(kvm_open_failed("EACCES".into()), "vIsLhQP34dQF"),
(kvm_smoke_failed(7, 5, "6.8.0"), "vIsLhQP34dQF"),
] {
crate::util::assert_client_safe(&diag.client, box_id);
assert!(
diag.operator().contains("/dev/kvm"),
"operator report lost the KVM detail"
);
}
}
#[test]
#[cfg(target_os = "linux")]
fn kvm_diagnostics_never_leak_host_internals() {
let cases: Vec<(HostDiagnostic, &str)> = vec![
(kvm_missing(), "missing"),
(kvm_permission_denied(), "permission denied"),
(kvm_open_failed("EACCES".to_string()), "open failed"),
(kvm_smoke_failed(3, 5, "6.8.0"), "smoke failed"),
];
for (diag, label) in cases {
crate::util::assert_client_safe(&diag.client, "test-box");
assert!(
diag.operator().contains("modprobe")
|| diag.operator().contains("lsmod")
|| diag.operator().contains("/dev/kvm")
|| diag.operator().contains("nested"),
"{label}: operator report lost the guidance"
);
}
}
#[test]
#[cfg(feature = "krun")]
fn hypervisor_probe_returns_original_on_passthrough() {
let probe = hypervisor_probe();
let original = BoxliteError::Engine("test error".into());
let result = probe.diagnose_create_failure(original);
let msg = result.to_string();
assert!(!msg.is_empty());
}
#[cfg(target_os = "macos")]
mod hvf_tests {
use super::super::*;
#[test]
fn hvf_probe_startup_check() {
let probe = HvfProbe;
match probe.startup_check() {
Ok(()) => {}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("Hypervisor") || msg.contains("architecture"),
"Error should mention HVF: {msg}"
);
}
}
}
#[test]
fn hvf_ffi_constants() {
assert_eq!(hvf_ffi::HV_SUCCESS, 0);
assert_eq!(hvf_ffi::HV_NO_RESOURCES, -85377019_i32);
assert_eq!(hvf_ffi::HV_BUSY, -85377022_i32);
assert_eq!(hvf_ffi::HV_DENIED, -85377017_i32);
}
}
}