From e64d7e357585d812ac5c6cb76e258cafe51c7b7c Mon Sep 17 00:00:00 2001 From: Xuan <37977109+AprilNEA@users.noreply.github.com> Date: Wed, 8 Jul 2026 05:56:07 +0000 Subject: [PATCH] feat(sandbox): expose nested-virt capability and pre-check create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SystemService.GetSandboxCapability so clients learn whether sandboxes can run (nested-virt microVMs) without booting one into an opaque KVM failure. The host computes it from the System VM backend plus a static host nested-virt probe (arcbox_hypervisor::host_supports_nested_virt), so arcbox sandbox create fails fast with an actionable message on unsupported hardware/backends — no round-trip into the guest. Refactor the Linux nested-virt detection out of KvmHypervisor into a reusable module function backing the new cross-platform helper. Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com> --- app/arcbox-api/src/system.rs | 19 +++++- app/arcbox-cli/src/commands/sandbox.rs | 31 ++++++++++ app/arcbox-core/src/lib.rs | 2 +- app/arcbox-core/src/runtime.rs | 62 +++++++++++++++++++ app/arcbox-core/src/runtime/tests.rs | 25 ++++++++ rpc/arcbox-protocol/proto/api.proto | 16 +++++ .../src/generated/arcbox.v1.rs | 15 +++++ virt/arcbox-hypervisor/src/lib.rs | 25 ++++++++ .../arcbox-hypervisor/src/linux/hypervisor.rs | 27 +------- virt/arcbox-hypervisor/src/linux/mod.rs | 29 +++++++++ 10 files changed, 222 insertions(+), 29 deletions(-) diff --git a/app/arcbox-api/src/system.rs b/app/arcbox-api/src/system.rs index 76e2db4e1..795be8f8b 100644 --- a/app/arcbox-api/src/system.rs +++ b/app/arcbox-api/src/system.rs @@ -9,9 +9,9 @@ use std::sync::Arc; use arcbox_grpc::SystemService; use arcbox_protocol::v1::{ Empty, ResolveContainerFsRequest, ResolveContainerFsResponse, ResolveImageFsRequest, - ResolveImageFsResponse, SetSystemVmBackendRequest, SetupStatus, SystemVmBackend, - SystemVmBackendInfo, VcpuDebug, VirtioDebugInfo, VirtioDeviceDebug, VirtioQueueDebug, - setup_status, + ResolveImageFsResponse, SandboxCapability, SetSystemVmBackendRequest, SetupStatus, + SystemVmBackend, SystemVmBackendInfo, VcpuDebug, VirtioDebugInfo, VirtioDeviceDebug, + VirtioQueueDebug, setup_status, }; use tokio::sync::watch; use tokio_stream::Stream; @@ -265,6 +265,19 @@ impl SystemService for SystemServiceImpl { })) } + async fn get_sandbox_capability( + &self, + _request: Request, + ) -> Result, Status> { + let runtime = self.runtime.ready()?; + let cap = runtime.sandbox_capability(); + Ok(Response::new(SandboxCapability { + supported: cap.supported, + reason: cap.reason, + backend: backend_to_proto(cap.backend) as i32, + })) + } + async fn get_virtio_debug( &self, _request: Request, diff --git a/app/arcbox-cli/src/commands/sandbox.rs b/app/arcbox-cli/src/commands/sandbox.rs index 28f0fcf4a..61e5864fd 100644 --- a/app/arcbox-cli/src/commands/sandbox.rs +++ b/app/arcbox-cli/src/commands/sandbox.rs @@ -37,6 +37,33 @@ async fn sandbox_channel() -> Result { }) } +/// Fails fast when the daemon reports that sandboxes cannot run on this host +/// or System VM backend, instead of booting a microVM that would land in +/// `failed` with an opaque KVM error. This is a host-side check — no round-trip +/// into the guest. Transport errors (e.g. an older daemon without the RPC) fall +/// through so the create still proceeds, where the guest agent remains the +/// backstop. +async fn ensure_sandbox_supported(channel: &Channel) -> Result<()> { + use arcbox_grpc::SystemServiceClient; + + let mut client = SystemServiceClient::new(channel.clone()); + match client + .get_sandbox_capability(tonic::Request::new(arcbox_protocol::v1::Empty {})) + .await + { + Ok(resp) => { + let cap = resp.into_inner(); + if !cap.supported { + anyhow::bail!("{}", cap.reason); + } + Ok(()) + } + // The capability RPC is unavailable (older daemon, not ready); let the + // create proceed rather than blocking on a missing pre-check. + Err(_) => Ok(()), + } +} + /// Attaches the default `x-machine` metadata header to a tonic request for /// daemon-side routing to the guest VM agent. fn attach_machine(mut request: tonic::Request) -> tonic::Request { @@ -294,6 +321,10 @@ fn parse_labels(raw: &[String]) -> Result> { async fn execute_create(args: CreateArgs) -> Result<()> { let channel = sandbox_channel().await?; + + // Reject unsupported hosts/backends up front with an actionable message. + ensure_sandbox_supported(&channel).await?; + let mut client = SandboxServiceClient::new(channel); let labels = parse_labels(&args.label)?; diff --git a/app/arcbox-core/src/lib.rs b/app/arcbox-core/src/lib.rs index 412a3e097..d26310471 100644 --- a/app/arcbox-core/src/lib.rs +++ b/app/arcbox-core/src/lib.rs @@ -69,7 +69,7 @@ pub use macos::{ #[cfg(feature = "macos-ipsw-install")] pub use macos::{PullPhase, PullSource}; pub use migration::MigrationManager; -pub use runtime::{Runtime, SandboxPortExposure}; +pub use runtime::{Runtime, SandboxCapability, SandboxPortExposure}; pub use vm::{SharedDirConfig, VmConfig, VmManager}; pub use vm_lifecycle::{ ActivityScope, DEFAULT_MACHINE_NAME, DefaultVmConfig, HealthMonitor, VmLifecycleConfig, diff --git a/app/arcbox-core/src/runtime.rs b/app/arcbox-core/src/runtime.rs index bc6764c93..add868e08 100644 --- a/app/arcbox-core/src/runtime.rs +++ b/app/arcbox-core/src/runtime.rs @@ -145,6 +145,49 @@ pub struct SandboxPortExposure { pub guest_port: u16, } +/// Whether this host and System VM backend can run sandboxes. +/// +/// Produced by [`Runtime::sandbox_capability`]. +#[derive(Debug, Clone)] +pub struct SandboxCapability { + /// Whether sandboxes are runnable on the current host and backend. + pub supported: bool, + /// Actionable reason when unsupported; empty when supported. + pub reason: String, + /// The System VM backend the capability was evaluated against. + pub backend: arcbox_vmm::VmBackend, +} + +/// Pure decision behind [`Runtime::sandbox_capability`], split out for testing. +/// +/// Host nested-virt support is the hard gate (M3+ / macOS 15+ hardware), so it +/// is checked first: on hardware that lacks it, switching backends cannot help. +/// Only when the hardware is capable does the HV backend become the actionable +/// blocker (nested virtualization is unavailable under Hypervisor.framework). +fn evaluate_sandbox_capability( + backend: arcbox_vmm::VmBackend, + host_nested_virt: bool, +) -> (bool, String) { + if !host_nested_virt { + return ( + false, + "sandbox requires nested virtualization: the VZ backend on Apple \ + Silicon M3 or newer with macOS 15+; this host does not support it" + .to_string(), + ); + } + if backend == arcbox_vmm::VmBackend::Hv { + return ( + false, + "sandbox requires nested virtualization, which is unavailable under \ + the HV backend (current backend: HV); switch with \ + `arcbox system backend vz`" + .to_string(), + ); + } + (true, String::new()) +} + impl Runtime { /// Creates a new runtime with the given configuration. /// @@ -415,6 +458,25 @@ impl Runtime { self.vm_lifecycle.backend() } + /// Reports whether this host and System VM backend can run sandboxes. + /// + /// Sandboxes are nested Firecracker microVMs; they need `/dev/kvm` inside + /// the System VM, which the host exposes only with nested virtualization + /// enabled (VZ backend on Apple Silicon M3+ with macOS 15+). Computing this + /// on the host lets `arcbox sandbox create` and clients fail fast without a + /// round-trip into the guest. + #[must_use] + pub fn sandbox_capability(&self) -> SandboxCapability { + let backend = self.system_vm_backend(); + let (supported, reason) = + evaluate_sandbox_capability(backend, arcbox_hypervisor::host_supports_nested_virt()); + SandboxCapability { + supported, + reason, + backend, + } + } + /// Switches the System VM's hypervisor backend (HV <-> VZ) and restarts the /// VM so it takes effect. /// diff --git a/app/arcbox-core/src/runtime/tests.rs b/app/arcbox-core/src/runtime/tests.rs index fdd94fda4..bfa3ed96d 100644 --- a/app/arcbox-core/src/runtime/tests.rs +++ b/app/arcbox-core/src/runtime/tests.rs @@ -238,3 +238,28 @@ fn resolve_bind_ip_defaults_and_loopback() { ); assert_eq!(super::resolve_bind_ip("not-an-ip"), None); } + +#[test] +fn sandbox_capability_gates_on_nested_virt_and_backend() { + use arcbox_vmm::VmBackend; + + // VZ backend on nested-virt-capable hardware: sandboxes run. + let (supported, reason) = super::evaluate_sandbox_capability(VmBackend::Vz, true); + assert!(supported); + assert!(reason.is_empty()); + + // HV backend on capable hardware: actionable "switch to VZ" message. + let (supported, reason) = super::evaluate_sandbox_capability(VmBackend::Hv, true); + assert!(!supported); + assert!(reason.contains("HV")); + assert!(reason.contains("backend vz")); + + // No host nested-virt support: hardware message regardless of backend, + // since switching backends cannot help. + for backend in [VmBackend::Vz, VmBackend::Hv] { + let (supported, reason) = super::evaluate_sandbox_capability(backend, false); + assert!(!supported); + assert!(reason.contains("nested virtualization")); + assert!(reason.contains("M3")); + } +} diff --git a/rpc/arcbox-protocol/proto/api.proto b/rpc/arcbox-protocol/proto/api.proto index 6a493aaf4..776c4ad7f 100644 --- a/rpc/arcbox-protocol/proto/api.proto +++ b/rpc/arcbox-protocol/proto/api.proto @@ -194,6 +194,12 @@ service SystemService { // Gets the System VM's current hypervisor backend. rpc GetSystemVmBackend(Empty) returns (SystemVmBackendInfo); + // Reports whether this host and System VM backend can run sandboxes + // (nested-virtualization microVMs). Lets clients pre-check and grey out + // sandbox features without booting a microVM that would fail with an + // opaque KVM error. + rpc GetSandboxCapability(Empty) returns (SandboxCapability); + // Switches the System VM's hypervisor backend (HV <-> VZ). The choice is // persisted; the System VM is restarted so it takes effect, which stops // running containers. Returns the resulting backend. @@ -268,6 +274,16 @@ message ResolveImageFsResponse { repeated string lower_dirs = 1; } +// Whether sandboxes (nested-virtualization microVMs) can run on this host. +message SandboxCapability { + // Whether sandboxes are runnable on the current host and backend. + bool supported = 1; + // Actionable reason when unsupported; empty when supported. + string reason = 2; + // The System VM backend the capability was evaluated against. + SystemVmBackend backend = 3; +} + // Diagnostic snapshot of the System VM's virtio devices and vCPUs. message VirtioDebugInfo { repeated VirtioDeviceDebug devices = 1; diff --git a/rpc/arcbox-protocol/src/generated/arcbox.v1.rs b/rpc/arcbox-protocol/src/generated/arcbox.v1.rs index b58bf3f4e..3f0387434 100644 --- a/rpc/arcbox-protocol/src/generated/arcbox.v1.rs +++ b/rpc/arcbox-protocol/src/generated/arcbox.v1.rs @@ -344,6 +344,21 @@ pub struct ResolveImageFsResponse { #[prost(string, repeated, tag = "1")] pub lower_dirs: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } +/// Whether sandboxes (nested-virtualization microVMs) can run on this host. +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SandboxCapability { + /// Whether sandboxes are runnable on the current host and backend. + #[prost(bool, tag = "1")] + pub supported: bool, + /// Actionable reason when unsupported; empty when supported. + #[prost(string, tag = "2")] + pub reason: ::prost::alloc::string::String, + /// The System VM backend the capability was evaluated against. + #[prost(enumeration = "SystemVmBackend", tag = "3")] + pub backend: i32, +} /// Diagnostic snapshot of the System VM's virtio devices and vCPUs. #[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/virt/arcbox-hypervisor/src/lib.rs b/virt/arcbox-hypervisor/src/lib.rs index be9f63e06..71659a431 100644 --- a/virt/arcbox-hypervisor/src/lib.rs +++ b/virt/arcbox-hypervisor/src/lib.rs @@ -86,3 +86,28 @@ pub fn create_hypervisor() -> Result { compile_error!("Unsupported platform: only macOS and Linux are supported") } } + +/// Reports whether the host supports nested virtualization for guest VMs. +/// +/// This gates ArcBox sandboxes: nested Firecracker microVMs need `/dev/kvm` +/// inside the System VM, which the host exposes only when it enables nested +/// virtualization — Apple Silicon M3 or newer on macOS 15+ under the VZ +/// backend. Detection is a cheap static query and does not construct a +/// hypervisor. +#[must_use] +pub fn host_supports_nested_virt() -> bool { + #[cfg(target_os = "macos")] + { + arcbox_vz::GenericPlatform::is_nested_virt_supported() + } + + #[cfg(target_os = "linux")] + { + linux::host_supports_nested_virt() + } + + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + false + } +} diff --git a/virt/arcbox-hypervisor/src/linux/hypervisor.rs b/virt/arcbox-hypervisor/src/linux/hypervisor.rs index 203188365..a085d7982 100644 --- a/virt/arcbox-hypervisor/src/linux/hypervisor.rs +++ b/virt/arcbox-hypervisor/src/linux/hypervisor.rs @@ -97,11 +97,8 @@ impl KvmHypervisor { // Determine supported architectures let supported_archs = vec![CpuArch::native()]; - // Check for nested virtualization support - #[cfg(target_arch = "x86_64")] - let nested_virt = Self::check_nested_virt(); - #[cfg(not(target_arch = "x86_64"))] - let nested_virt = false; + // Check for nested virtualization support. + let nested_virt = super::host_supports_nested_virt(); Ok(PlatformCapabilities { supported_archs, @@ -112,26 +109,6 @@ impl KvmHypervisor { }) } - /// Checks if nested virtualization is supported (x86 only). - #[cfg(target_arch = "x86_64")] - fn check_nested_virt() -> bool { - // Check Intel VMX nested support - if let Ok(content) = std::fs::read_to_string("/sys/module/kvm_intel/parameters/nested") { - if content.trim() == "Y" || content.trim() == "1" { - return true; - } - } - - // Check AMD SVM nested support - if let Ok(content) = std::fs::read_to_string("/sys/module/kvm_amd/parameters/nested") { - if content.trim() == "Y" || content.trim() == "1" { - return true; - } - } - - false - } - /// Returns the KVM system handle. pub(crate) fn kvm(&self) -> &Arc { &self.kvm diff --git a/virt/arcbox-hypervisor/src/linux/mod.rs b/virt/arcbox-hypervisor/src/linux/mod.rs index 94df7db04..cfecd0a48 100644 --- a/virt/arcbox-hypervisor/src/linux/mod.rs +++ b/virt/arcbox-hypervisor/src/linux/mod.rs @@ -24,3 +24,32 @@ pub use hypervisor::KvmHypervisor; pub use memory::KvmMemory; pub use vcpu::KvmVcpu; pub use vm::{KvmVm, VirtioDeviceInfo}; + +/// Whether the Linux host has nested virtualization enabled for KVM. +/// +/// Reads the `nested` module parameter of the Intel/AMD KVM drivers. x86-only; +/// other architectures always report `false`. +#[must_use] +pub(crate) fn host_supports_nested_virt() -> bool { + #[cfg(target_arch = "x86_64")] + { + // Intel VMX and AMD SVM expose nesting via a module parameter that + // reads "Y"/"1" when enabled. + for path in [ + "/sys/module/kvm_intel/parameters/nested", + "/sys/module/kvm_amd/parameters/nested", + ] { + if let Ok(content) = std::fs::read_to_string(path) { + let value = content.trim(); + if value == "Y" || value == "1" { + return true; + } + } + } + false + } + #[cfg(not(target_arch = "x86_64"))] + { + false + } +}