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 mut msg = format!(
"{DEV} 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 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",
);
}
return Err(BoxliteError::Unsupported(msg));
}
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(DEV)
.map_err(|e| match e.kind() {
std::io::ErrorKind::PermissionDenied => BoxliteError::Unsupported(format!(
"{DEV}: permission denied\n\n\
Fix:\n\
- sudo usermod -aG kvm $USER && newgrp kvm"
)),
_ => BoxliteError::Unsupported(format!("{DEV}: {e}")),
})
}
#[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();
Err(BoxliteError::Unsupported(format!(
"KVM smoke test failed: vCPU exit reason {exit_reason} (expected {KVM_EXIT_HLT})\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"
);
BoxliteError::ResourceExhausted(
"macOS Hypervisor.framework VM address spaces exhausted \
(kern.hv.max_address_spaces limit reached). \
Stop some boxes with `boxlite stop` and retry. \
See: sysctl kern.hv.max_address_spaces"
.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"
);
BoxliteError::Unsupported(
"Hypervisor.framework access denied. \
The boxlite-shim binary needs the \
com.apple.security.hypervisor entitlement."
.into(),
)
}
_ => {
tracing::error!(
hvf_code = format!("{ret:#x}"),
"HVF diagnostic: unexpected error code"
);
BoxliteError::Engine(format!(
"{}. HVF diagnostic probe returned error code {ret:#x}",
error
))
}
}
}
}
#[cfg(target_os = "macos")]
fn check_hypervisor_framework() -> BoxliteResult<()> {
#[cfg(not(target_arch = "aarch64"))]
return Err(BoxliteError::Unsupported(format!(
"Unsupported architecture: {}\n\n\
BoxLite on macOS requires Apple Silicon (ARM64).\n\
Intel Macs are not supported.",
std::env::consts::ARCH
)));
#[cfg(target_arch = "aarch64")]
{
let output = std::process::Command::new("sysctl")
.arg("kern.hv_support")
.output()
.map_err(|e| {
BoxliteError::Unsupported(format!(
"Failed to check Hypervisor.framework: {e}\n\n\
Check manually: sysctl kern.hv_support"
))
})?;
if !output.status.success() {
return Err(BoxliteError::Unsupported(
"sysctl kern.hv_support failed".into(),
));
}
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 {
Err(BoxliteError::Unsupported(
"Hypervisor.framework is not available\n\n\
Suggestions:\n\
- Verify macOS 10.10 or later\n\
- Check: sysctl kern.hv_support"
.into(),
))
}
}
}
#[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(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);
}
}
}