Skip to main content

arcbox_hypervisor/
capability.rs

1//! Host capability probes that answer without constructing a hypervisor.
2//!
3//! `Hypervisor::capabilities` is the richer surface, but reaching it means
4//! opening `/dev/kvm` (or a VZ configuration) first. Callers that must
5//! answer before any VM exists — the sandbox capability gate, which
6//! `GetCapabilities` and the `Create` fail-fast path both consult — use
7//! these instead. The concrete backends read the same probes when they
8//! fill in `PlatformCapabilities`, so the two never drift.
9
10/// Whether this host can nest a VM, and the platform's own reason when it
11/// cannot.
12///
13/// The reason is platform knowledge, not product copy: it names the
14/// hardware or kernel requirement that failed, so a caller can surface it
15/// without carrying a `#[cfg]` of its own.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct NestedVirtSupport {
18    /// True when the hardware and kernel allow a nested guest.
19    pub supported: bool,
20    /// Why not, when `supported` is false; empty otherwise.
21    ///
22    /// Phrased as the requirement that failed, with no leading subject, so
23    /// a caller can prefix its own context — `arcbox-api` renders it as
24    /// `"sandboxes cannot run on this host: {reason}"` — without ending up
25    /// with two stapled-together clauses.
26    pub reason: &'static str,
27}
28
29/// Probes nested-virtualization support on this host.
30///
31/// Cached on macOS, where this is a fixed hardware/OS property that
32/// cannot change while the process lives. Re-probed on every call
33/// elsewhere: on Linux the answer depends on the `kvm_intel`/`kvm_amd`
34/// module being loaded, and this function is reachable — via the sandbox
35/// capability gate — before any VM has ever booted (in particular under
36/// `--no-linux-vm`, where nothing in this process ever opens `/dev/kvm`).
37/// A query that lands before the module autoloads would otherwise cache a
38/// false negative for the rest of the process's life.
39#[must_use]
40pub fn host_nested_virt() -> NestedVirtSupport {
41    let supported = cached_or_reprobed();
42    NestedVirtSupport {
43        supported,
44        reason: if supported { "" } else { UNSUPPORTED_REASON },
45    }
46}
47
48#[cfg(target_os = "macos")]
49fn cached_or_reprobed() -> bool {
50    static SUPPORTED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
51    *SUPPORTED.get_or_init(probe_nested_virt)
52}
53
54#[cfg(not(target_os = "macos"))]
55fn cached_or_reprobed() -> bool {
56    probe_nested_virt()
57}
58
59#[cfg(target_os = "macos")]
60const UNSUPPORTED_REASON: &str = concat!(
61    "nested virtualization requires Apple Silicon M3 or newer ",
62    "with macOS 15 or newer",
63);
64
65/// `VZGenericPlatformConfiguration.isNestedVirtualizationSupported`.
66#[cfg(target_os = "macos")]
67fn probe_nested_virt() -> bool {
68    arcbox_vz::GenericPlatform::is_nested_virt_supported()
69}
70
71#[cfg(target_os = "linux")]
72const UNSUPPORTED_REASON: &str = concat!(
73    "the host kernel does not expose nested KVM ",
74    "(/sys/module/kvm_{intel,amd}/parameters/nested)",
75);
76
77/// Nested-KVM module parameters.
78///
79/// Not gated on x86_64: the files simply do not exist on other
80/// architectures, so the read fails and the answer is false — which is
81/// also the honest answer for ARM until the kernel grows a probe for
82/// ARMv8.4 nested virtualization.
83#[cfg(target_os = "linux")]
84fn probe_nested_virt() -> bool {
85    ["kvm_intel", "kvm_amd"].iter().any(|module| {
86        std::fs::read_to_string(format!("/sys/module/{module}/parameters/nested"))
87            .is_ok_and(|content| matches!(content.trim(), "1" | "Y" | "y"))
88    })
89}
90
91#[cfg(not(any(target_os = "macos", target_os = "linux")))]
92const UNSUPPORTED_REASON: &str = "nested virtualization is not supported on this platform";
93
94#[cfg(not(any(target_os = "macos", target_os = "linux")))]
95fn probe_nested_virt() -> bool {
96    false
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    // The reason is user-facing: `arcbox-computer` surfaces it verbatim in
104    // the `NESTED_VIRT_UNSUPPORTED` error a failing `Create` returns, so
105    // the hardware requirement it names is contract, not an internal
106    // string. Pin it here, where the constant lives.
107    #[cfg(target_os = "macos")]
108    #[test]
109    fn the_macos_reason_names_the_hardware_requirement() {
110        let reason = super::UNSUPPORTED_REASON;
111        assert!(reason.contains("M3"), "{reason}");
112        assert!(reason.contains("macOS 15"), "{reason}");
113    }
114
115    #[test]
116    fn an_unsupported_host_always_carries_a_reason() {
117        let support = host_nested_virt();
118        assert_eq!(support.supported, support.reason.is_empty());
119    }
120
121    #[test]
122    fn the_probe_is_stable_across_repeated_calls() {
123        // Cached on macOS (a fixed hardware property); re-probed every call
124        // on Linux (see `cached_or_reprobed`). Either way, two calls back to
125        // back must agree — nothing in this process changes kernel module
126        // state between them.
127        assert_eq!(host_nested_virt(), host_nested_virt());
128    }
129}