use std::any::Any;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use crate::device::DeviceManager;
use crate::error::{Result, VmmError};
use crate::event::EventLoop;
use crate::irq::{Gsi, IrqChip, IrqTriggerCallback};
use crate::memory::MemoryManager;
use crate::vcpu::VcpuManager;
use arcbox_hypervisor::VirtioDeviceConfig;
use arcbox_hypervisor::VmConfig;
#[cfg(target_os = "macos")]
mod darwin;
#[cfg(target_os = "linux")]
mod linux;
type ManagedVm = Box<dyn Any + Send + Sync>;
#[derive(Debug, Clone)]
pub struct SharedDirConfig {
pub host_path: PathBuf,
pub tag: String,
pub read_only: bool,
}
#[derive(Debug, Clone)]
pub struct BlockDeviceConfig {
pub path: PathBuf,
pub read_only: bool,
}
#[derive(Debug, Clone)]
pub struct VmmConfig {
pub vcpu_count: u32,
pub memory_size: u64,
pub kernel_path: PathBuf,
pub kernel_cmdline: String,
pub initrd_path: Option<PathBuf>,
pub enable_rosetta: bool,
pub serial_console: bool,
pub virtio_console: bool,
pub shared_dirs: Vec<SharedDirConfig>,
pub networking: bool,
pub vsock: bool,
pub guest_cid: Option<u32>,
pub balloon: bool,
pub block_devices: Vec<BlockDeviceConfig>,
}
impl Default for VmmConfig {
fn default() -> Self {
Self {
vcpu_count: 1,
memory_size: 512 * 1024 * 1024, kernel_path: PathBuf::new(),
kernel_cmdline: String::new(),
initrd_path: None,
enable_rosetta: false,
serial_console: true,
virtio_console: true,
shared_dirs: Vec::new(),
networking: true,
vsock: true,
guest_cid: None,
balloon: true, block_devices: Vec::new(),
}
}
}
impl VmmConfig {
fn to_vm_config(&self) -> VmConfig {
let mut builder = VmConfig::builder()
.vcpu_count(self.vcpu_count)
.memory_size(self.memory_size)
.kernel_path(self.kernel_path.to_string_lossy())
.kernel_cmdline(&self.kernel_cmdline)
.enable_rosetta(self.enable_rosetta);
if let Some(initrd_path) = &self.initrd_path {
builder = builder.initrd_path(initrd_path.to_string_lossy());
}
builder.build()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VmmState {
Created,
Initializing,
Running,
Paused,
Stopping,
Stopped,
Failed,
}
pub struct Vmm {
config: VmmConfig,
state: VmmState,
running: Arc<AtomicBool>,
vcpu_manager: Option<VcpuManager>,
memory_manager: Option<MemoryManager>,
device_manager: Option<DeviceManager>,
irq_chip: Option<Arc<IrqChip>>,
event_loop: Option<EventLoop>,
managed_execution: bool,
managed_vm: Option<ManagedVm>,
#[cfg(target_os = "macos")]
net_cancel: Option<tokio_util::sync::CancellationToken>,
#[cfg(target_os = "macos")]
net_vz_fd: Option<OwnedFd>,
#[cfg(target_os = "macos")]
inbound_listener_manager: Option<arcbox_net::darwin::inbound_relay::InboundListenerManager>,
}
impl Vmm {
pub fn new(config: VmmConfig) -> Result<Self> {
if config.vcpu_count == 0 {
return Err(VmmError::config("vcpu_count must be > 0".to_string()));
}
if config.memory_size < 64 * 1024 * 1024 {
return Err(VmmError::config("memory_size must be >= 64MB".to_string()));
}
if !config.kernel_path.as_os_str().is_empty() && !config.kernel_path.exists() {
return Err(VmmError::config(format!(
"kernel not found: {}",
config.kernel_path.display()
)));
}
if config.vsock && config.guest_cid.is_none() {
return Err(VmmError::config(
"guest_cid must be set when vsock is enabled".to_string(),
));
}
tracing::info!(
"Creating VMM: vcpus={}, memory={}MB",
config.vcpu_count,
config.memory_size / (1024 * 1024)
);
Ok(Self {
config,
state: VmmState::Created,
running: Arc::new(AtomicBool::new(false)),
vcpu_manager: None,
memory_manager: None,
device_manager: None,
irq_chip: None,
event_loop: None,
managed_execution: false,
managed_vm: None,
#[cfg(target_os = "macos")]
net_cancel: None,
#[cfg(target_os = "macos")]
net_vz_fd: None,
#[cfg(target_os = "macos")]
inbound_listener_manager: None,
})
}
#[must_use]
pub const fn state(&self) -> VmmState {
self.state
}
#[must_use]
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
#[must_use]
pub fn running_flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.running)
}
pub fn initialize(&mut self) -> Result<()> {
if self.state != VmmState::Created {
return Err(VmmError::invalid_state(format!(
"cannot initialize from state {:?}",
self.state
)));
}
self.state = VmmState::Initializing;
tracing::info!("Initializing VMM");
#[cfg(target_os = "macos")]
{
self.initialize_darwin()?;
}
#[cfg(target_os = "linux")]
{
self.initialize_linux()?;
}
tracing::info!("VMM initialized successfully");
Ok(())
}
pub fn start(&mut self) -> Result<()> {
if self.state == VmmState::Created {
self.initialize()?;
}
if self.state != VmmState::Initializing && self.state != VmmState::Stopped {
return Err(VmmError::invalid_state(format!(
"cannot start from state {:?}",
self.state
)));
}
tracing::info!("Starting VMM");
if self.managed_execution {
#[cfg(target_os = "macos")]
{
self.start_managed_vm()?;
}
} else {
if let Some(ref mut vcpu_manager) = self.vcpu_manager {
vcpu_manager.start()?;
}
}
if let Some(ref mut event_loop) = self.event_loop {
event_loop.start()?;
}
self.running.store(true, Ordering::SeqCst);
self.state = VmmState::Running;
tracing::info!("VMM started");
Ok(())
}
pub fn pause(&mut self) -> Result<()> {
if self.state != VmmState::Running {
return Err(VmmError::invalid_state(format!(
"cannot pause from state {:?}",
self.state
)));
}
tracing::info!("Pausing VMM");
if self.managed_execution {
#[cfg(target_os = "macos")]
{
self.pause_managed_vm()?;
}
} else if let Some(ref mut vcpu_manager) = self.vcpu_manager {
vcpu_manager.pause()?;
}
self.state = VmmState::Paused;
tracing::info!("VMM paused");
Ok(())
}
pub fn resume(&mut self) -> Result<()> {
if self.state != VmmState::Paused {
return Err(VmmError::invalid_state(format!(
"cannot resume from state {:?}",
self.state
)));
}
tracing::info!("Resuming VMM");
if self.managed_execution {
#[cfg(target_os = "macos")]
{
self.resume_managed_vm()?;
}
} else if let Some(ref mut vcpu_manager) = self.vcpu_manager {
vcpu_manager.resume()?;
}
self.state = VmmState::Running;
tracing::info!("VMM resumed");
Ok(())
}
pub fn stop(&mut self) -> Result<()> {
if self.state == VmmState::Stopped {
return Ok(());
}
tracing::info!("Stopping VMM");
self.state = VmmState::Stopping;
self.running.store(false, Ordering::SeqCst);
if let Some(ref mut event_loop) = self.event_loop {
event_loop.stop();
}
#[cfg(target_os = "macos")]
if self.managed_execution {
self.stop_managed_vm()?;
}
if !self.managed_execution {
if let Some(ref mut vcpu_manager) = self.vcpu_manager {
vcpu_manager.stop()?;
}
}
#[cfg(target_os = "macos")]
self.stop_network();
self.state = VmmState::Stopped;
tracing::info!("VMM stopped");
Ok(())
}
#[must_use]
pub const fn configured_memory(&self) -> u64 {
self.config.memory_size
}
#[must_use]
pub const fn has_balloon(&self) -> bool {
self.config.balloon
}
pub fn capture_snapshot_context(&self) -> Result<crate::snapshot::VmSnapshotContext> {
if self.state != VmmState::Running && self.state != VmmState::Paused {
return Err(VmmError::invalid_state(format!(
"cannot capture snapshot from state {:?}",
self.state
)));
}
#[cfg(target_os = "linux")]
if let Some(result) = self.capture_snapshot_linux() {
return result;
}
#[cfg(target_os = "macos")]
if let Some(result) = self.capture_snapshot_darwin() {
return result;
}
Err(VmmError::invalid_state(
"hypervisor VM handle is unavailable for snapshot".to_string(),
))
}
pub fn restore_from_snapshot_data(
&mut self,
restore_data: &crate::snapshot::VmRestoreData,
) -> Result<()> {
if self.state != VmmState::Running && self.state != VmmState::Paused {
return Err(VmmError::invalid_state(format!(
"cannot restore snapshot from state {:?}",
self.state
)));
}
let expected_memory_len = usize::try_from(restore_data.memory_size()).map_err(|_| {
VmmError::Memory(format!(
"snapshot memory size {} does not fit in usize",
restore_data.memory_size()
))
})?;
if restore_data.memory().len() != expected_memory_len {
return Err(VmmError::Memory(format!(
"snapshot memory length mismatch: expected {}, got {}",
expected_memory_len,
restore_data.memory().len()
)));
}
#[cfg(target_os = "linux")]
if let Some(result) = self.restore_snapshot_linux(restore_data) {
return result;
}
#[cfg(target_os = "macos")]
if let Some(result) = self.restore_snapshot_darwin(restore_data) {
return result;
}
Err(VmmError::invalid_state(
"hypervisor VM handle is unavailable for restore".to_string(),
))
}
pub async fn run(&mut self) -> Result<()> {
if self.state != VmmState::Running {
self.start()?;
}
tracing::info!("VMM running, waiting for exit");
while self.is_running() {
if let Some(ref mut event_loop) = self.event_loop {
if let Some(event) = event_loop.poll().await {
self.handle_event(event)?;
}
}
tokio::task::yield_now().await;
}
tracing::info!("VMM exited");
Ok(())
}
fn handle_event(&mut self, event: crate::event::VmmEvent) -> Result<()> {
use crate::event::VmmEvent;
match event {
VmmEvent::VcpuExit { vcpu_id, exit } => {
self.handle_vcpu_exit(vcpu_id, exit)?;
}
VmmEvent::DeviceIo {
device_id,
is_read,
addr,
data,
} => {
self.handle_device_io(device_id, is_read, addr, data)?;
}
VmmEvent::Timer { id } => {
tracing::trace!("Timer {} fired", id);
}
VmmEvent::Shutdown => {
tracing::info!("Shutdown requested");
self.running.store(false, Ordering::SeqCst);
}
}
Ok(())
}
fn handle_vcpu_exit(&mut self, vcpu_id: u32, exit: arcbox_hypervisor::VcpuExit) -> Result<()> {
use arcbox_hypervisor::VcpuExit;
match exit {
VcpuExit::Halt => {
tracing::debug!("vCPU {} halted", vcpu_id);
}
VcpuExit::IoOut { port, size, data } => {
tracing::trace!(
"vCPU {} I/O out: port={:#x}, size={}, data={:#x}",
vcpu_id,
port,
size,
data
);
self.handle_io_out(port, size, data)?;
}
VcpuExit::IoIn { port, size } => {
tracing::trace!("vCPU {} I/O in: port={:#x}, size={}", vcpu_id, port, size);
let _value = self.handle_io_in(port, size)?;
}
VcpuExit::MmioRead { addr, size } => {
tracing::trace!(
"vCPU {} MMIO read: addr={:#x}, size={}",
vcpu_id,
addr,
size
);
if let Some(ref device_manager) = self.device_manager {
match device_manager.handle_mmio_read(addr, size as usize) {
Ok(value) => {
tracing::trace!("MMIO read returned: {:#x}", value);
}
Err(e) => {
tracing::warn!("MMIO read failed at {:#x}: {}", addr, e);
}
}
}
}
VcpuExit::MmioWrite { addr, size, data } => {
tracing::trace!(
"vCPU {} MMIO write: addr={:#x}, size={}, data={:#x}",
vcpu_id,
addr,
size,
data
);
if let Some(ref device_manager) = self.device_manager {
if let Err(e) = device_manager.handle_mmio_write(addr, size as usize, data) {
tracing::warn!("MMIO write failed at {:#x}: {}", addr, e);
}
}
}
VcpuExit::Hypercall { nr, args } => {
tracing::debug!("vCPU {} hypercall: nr={}, args={:?}", vcpu_id, nr, args);
self.handle_hypercall(vcpu_id, nr, args)?;
}
VcpuExit::SystemReset => {
tracing::info!("vCPU {} requested system reset", vcpu_id);
self.running.store(false, Ordering::SeqCst);
}
VcpuExit::Shutdown => {
tracing::info!("vCPU {} requested shutdown", vcpu_id);
self.running.store(false, Ordering::SeqCst);
}
VcpuExit::Unknown(code) => {
tracing::warn!("vCPU {} unknown exit: {}", vcpu_id, code);
}
_ => {
tracing::debug!("vCPU {} unhandled exit: {:?}", vcpu_id, exit);
}
}
Ok(())
}
fn handle_device_io(
&mut self,
device_id: u32,
is_read: bool,
addr: u64,
data: Option<u64>,
) -> Result<()> {
if let Some(ref device_manager) = self.device_manager {
if is_read {
match device_manager.handle_mmio_read(addr, 4) {
Ok(value) => {
tracing::trace!("Device {} read at {:#x}: {:#x}", device_id, addr, value);
}
Err(e) => {
tracing::warn!("Device {} read failed: {}", device_id, e);
}
}
} else if let Some(value) = data {
if let Err(e) = device_manager.handle_mmio_write(addr, 4, value) {
tracing::warn!("Device {} write failed: {}", device_id, e);
}
}
}
Ok(())
}
fn handle_io_out(&self, port: u16, size: u8, data: u64) -> Result<()> {
match port {
0x3F8..=0x3FF => {
if port == 0x3F8 {
let ch = (data & 0xFF) as u8;
if ch.is_ascii() && (ch.is_ascii_graphic() || ch.is_ascii_whitespace()) {
tracing::trace!("Serial output: '{}'", ch as char);
}
}
}
0x2F8..=0x2FF => {
}
0x402 => {
let ch = (data & 0xFF) as u8;
if ch.is_ascii() {
tracing::debug!("Debug port: '{}'", ch as char);
}
}
0x604 => {
if data & 0x2000 != 0 {
tracing::info!("ACPI shutdown requested");
}
}
0x20 | 0x21 | 0xA0 | 0xA1 => {
tracing::trace!("PIC write: port={:#x}, data={:#x}", port, data);
}
0x40..=0x43 => {
tracing::trace!("PIT write: port={:#x}, data={:#x}", port, data);
}
_ => {
tracing::trace!(
"Unhandled I/O out: port={:#x}, size={}, data={:#x}",
port,
size,
data
);
}
}
Ok(())
}
fn handle_io_in(&self, port: u16, size: u8) -> Result<u64> {
let value = match port {
0x3FD => {
0x60
}
0x70 | 0x71 => 0,
0x64 => {
0x00
}
0x20 | 0x21 | 0xA0 | 0xA1 => 0xFF,
_ => {
tracing::trace!("Unhandled I/O in: port={:#x}, size={}", port, size);
0xFF }
};
Ok(value)
}
fn handle_hypercall(&self, vcpu_id: u32, nr: u64, args: [u64; 6]) -> Result<()> {
match nr {
0 => {
tracing::trace!("vCPU {} hypercall: VAPIC_POLL_IRQ", vcpu_id);
}
2 => {
tracing::trace!("vCPU {} hypercall: GET_FEATURES", vcpu_id);
}
4 => {
let target_vcpu = args[0] as u32;
tracing::trace!(
"vCPU {} hypercall: KICK_CPU target={}",
vcpu_id,
target_vcpu
);
}
_ => {
tracing::debug!(
"vCPU {} unhandled hypercall: nr={}, args={:?}",
vcpu_id,
nr,
args
);
}
}
Ok(())
}
}
fn placeholder_vcpu_snapshots(vcpu_count: u32) -> Vec<arcbox_hypervisor::VcpuSnapshot> {
#[cfg(target_arch = "aarch64")]
{
(0..vcpu_count)
.map(|id| {
arcbox_hypervisor::VcpuSnapshot::new_arm64(
id,
arcbox_hypervisor::Arm64Registers::default(),
)
})
.collect()
}
#[cfg(not(target_arch = "aarch64"))]
{
(0..vcpu_count)
.map(|id| {
arcbox_hypervisor::VcpuSnapshot::new_x86(
id,
arcbox_hypervisor::Registers::default(),
)
})
.collect()
}
}
impl Drop for Vmm {
fn drop(&mut self) {
if self.state != VmmState::Stopped && self.state != VmmState::Created {
let _ = self.stop();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vmm_creation() {
let config = VmmConfig {
guest_cid: Some(3),
..Default::default()
};
let vmm = Vmm::new(config).unwrap();
assert_eq!(vmm.state(), VmmState::Created);
}
#[test]
fn test_vmm_invalid_config() {
let config = VmmConfig {
vcpu_count: 0,
guest_cid: Some(3),
..Default::default()
};
assert!(Vmm::new(config).is_err());
let config = VmmConfig {
memory_size: 1024, guest_cid: Some(3),
..Default::default()
};
assert!(Vmm::new(config).is_err());
}
#[test]
fn test_vmm_requires_guest_cid_when_vsock_enabled() {
let config = VmmConfig {
guest_cid: None,
..Default::default()
};
assert!(Vmm::new(config).is_err());
let config = VmmConfig {
vsock: false,
guest_cid: None,
..Default::default()
};
assert!(Vmm::new(config).is_ok());
}
#[test]
fn test_vmm_state_transitions() {
let config = VmmConfig {
guest_cid: Some(3),
..Default::default()
};
let mut vmm = Vmm::new(config).unwrap();
assert!(vmm.pause().is_err());
assert!(vmm.resume().is_err());
}
}