diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 882ba93..fda9a32 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -87,13 +87,30 @@ jobs: env: RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links -D missing-docs run: | + # Build documentation cargo doc --no-deps --all-features - printf '' $(cargo tree | head -1 | cut -d' ' -f1) > target/doc/index.html + + # Auto-detect documentation directory + # Check if doc exists in target/doc or target/*/doc + if [ -d "target/doc" ]; then + DOC_DIR="target/doc" + else + # Find doc directory under target/*/doc pattern + DOC_DIR=$(find target -type d -name doc -path "target/*/doc" | head -n 1) + if [ -z "$DOC_DIR" ]; then + echo "Error: Could not find documentation directory" + exit 1 + fi + fi + + echo "Documentation found in: $DOC_DIR" + printf '' $(cargo tree | head -1 | cut -d' ' -f1) > "${DOC_DIR}/index.html" + echo "DOC_DIR=${DOC_DIR}" >> $GITHUB_ENV - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: target/doc + path: ${{ env.DOC_DIR }} deploy: name: Deploy to GitHub Pages diff --git a/Cargo.toml b/Cargo.toml index 506b227..4f1ed96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "axvm" authors = ["aarkegz "] -version = "0.2.0" +version = "0.2.3" edition = "2024" categories = ["virtualization", "no-std"] description = "Virtual Machine resource management crate for ArceOS's hypervisor variant." @@ -12,7 +12,7 @@ license = "Apache-2.0" [features] default = ["vmx"] vmx = [] -4-level-ept = ["arm_vcpu/4-level-ept"] # TODO: Realize 4-level-ept on x86_64 and riscv64. +4-level-ept = ["axaddrspace/4-level-ept"] # TODO: Realize 4-level-ept on x86_64 and riscv64. [dependencies] log = "0.4" @@ -20,27 +20,27 @@ cfg-if = "1.0" spin = "0.9" # System independent crates provided by ArceOS. -axerrno = "0.1.0" +axerrno = "0.2" cpumask = "0.1.0" # kspin = "0.1.0" memory_addr = "0.4" -page_table_entry = { version = "0.5", features = ["arm-el2"] } -page_table_multiarch = "0.5" -percpu = { version = "0.2.0", features = ["arm-el2"] } +page_table_entry = { version = "0.6", features = ["arm-el2"] } +page_table_multiarch = "0.6" +percpu = { version = "0.2.3-preview.1", features = ["arm-el2"] } # System dependent modules provided by ArceOS-Hypervisor. -axvcpu = "0.1" -axaddrspace = "0.1" -axdevice = "0.2" -axdevice_base = "0.1" -axvmconfig = { version = "0.1", default-features = false } +axvcpu = "0.2.2" +axaddrspace = "0.1.5" +axdevice = "0.2.1" +axdevice_base = "=0.2.1" +axvmconfig = { version = "0.2", default-features = false } [target.'cfg(target_arch = "x86_64")'.dependencies] -x86_vcpu = "0.1" +x86_vcpu = "0.2.1" [target.'cfg(target_arch = "riscv64")'.dependencies] -riscv_vcpu = "0.1" +riscv_vcpu = "0.2.1" [target.'cfg(target_arch = "aarch64")'.dependencies] -arm_vcpu = "0.1" -arm_vgic = { version = "0.1", features = ["vgicv3"] } +arm_vcpu = "0.2.1" +arm_vgic = { version = "0.2.1", features = ["vgicv3"] } diff --git a/src/config.rs b/src/config.rs index afb35d1..73e23c3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,18 +17,17 @@ use alloc::string::String; use alloc::vec::Vec; -use core::ops::Range; use axaddrspace::GuestPhysAddr; pub use axvmconfig::{ - AxVMCrateConfig, EmulatedDeviceConfig, PassThroughDeviceConfig, VMInterruptMode, VMType, - VmMemConfig, VmMemMappingType, + AxVMCrateConfig, EmulatedDeviceConfig, PassThroughAddressConfig, PassThroughDeviceConfig, + VMInterruptMode, VMType, VmMemConfig, VmMemMappingType, }; -/// A part of `AxVCpuConfig`, which represents an architecture-dependent `VCpu`. -/// -/// The concrete type of configuration is defined in `AxArchVCpuImpl`. +// /// A part of `AxVCpuConfig`, which represents an architecture-dependent `VCpu`. +// /// +// /// The concrete type of configuration is defined in `AxArchVCpuImpl`. // #[derive(Clone, Copy, Debug, Default)] // pub struct AxArchVCpuConfig { // pub create_config: as AxArchVCpu>::CreateConfig, @@ -45,7 +44,7 @@ pub struct AxVCpuConfig { } /// A part of `AxVMConfig`, which stores configuration attributes related to the load address of VM images. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct VMImageConfig { /// The load address in GPA for the kernel image. pub kernel_load_gpa: GuestPhysAddr, @@ -64,14 +63,15 @@ pub struct AxVMConfig { name: String, #[allow(dead_code)] vm_type: VMType, - cpu_num: usize, - phys_cpu_ids: Option>, - phys_cpu_sets: Option>, - cpu_config: AxVCpuConfig, - image_config: VMImageConfig, - memory_regions: Vec, + pub(crate) phys_cpu_ls: PhysCpuList, + /// vCPU configuration. + pub cpu_config: AxVCpuConfig, + /// VM image configuration. + pub image_config: VMImageConfig, emu_devices: Vec, pass_through_devices: Vec, + excluded_devices: Vec>, + pass_through_addresses: Vec, // TODO: improve interrupt passthrough spi_list: Vec, interrupt_mode: VMInterruptMode, @@ -83,9 +83,11 @@ impl From for AxVMConfig { id: cfg.base.id, name: cfg.base.name, vm_type: VMType::from(cfg.base.vm_type), - cpu_num: cfg.base.cpu_num, - phys_cpu_ids: cfg.base.phys_cpu_ids, - phys_cpu_sets: cfg.base.phys_cpu_sets, + phys_cpu_ls: PhysCpuList { + cpu_num: cfg.base.cpu_num, + phys_cpu_ids: cfg.base.phys_cpu_ids, + phys_cpu_sets: cfg.base.phys_cpu_sets, + }, cpu_config: AxVCpuConfig { bsp_entry: GuestPhysAddr::from(cfg.kernel.entry_point), ap_entry: GuestPhysAddr::from(cfg.kernel.entry_point), @@ -96,9 +98,11 @@ impl From for AxVMConfig { dtb_load_gpa: cfg.kernel.dtb_load_addr.map(GuestPhysAddr::from), ramdisk_load_gpa: cfg.kernel.ramdisk_load_addr.map(GuestPhysAddr::from), }, - memory_regions: cfg.kernel.memory_regions, + // memory_regions: cfg.kernel.memory_regions, emu_devices: cfg.devices.emu_devices, pass_through_devices: cfg.devices.passthrough_devices, + excluded_devices: cfg.devices.excluded_devices, + pass_through_addresses: cfg.devices.passthrough_addresses, spi_list: Vec::new(), interrupt_mode: cfg.devices.interrupt_mode, } @@ -116,32 +120,6 @@ impl AxVMConfig { self.name.clone() } - /// Returns vCpu id list and its corresponding pCpu affinity list, as well as its physical id. - /// If the pCpu affinity is None, it means the vCpu will be allocated to any available pCpu randomly. - /// if the pCPU id is not provided, the vCpu's physical id will be set as vCpu id. - /// - /// Returns a vector of tuples, each tuple contains: - /// - The vCpu id. - /// - The pCpu affinity mask, `None` if not set. - /// - The physical id of the vCpu, equal to vCpu id if not provided. - pub fn get_vcpu_affinities_pcpu_ids(&self) -> Vec<(usize, Option, usize)> { - let mut vcpu_pcpu_tuples = Vec::new(); - for vcpu_id in 0..self.cpu_num { - vcpu_pcpu_tuples.push((vcpu_id, None, vcpu_id)); - } - if let Some(phys_cpu_sets) = &self.phys_cpu_sets { - for (vcpu_id, pcpu_mask_bitmap) in phys_cpu_sets.iter().enumerate() { - vcpu_pcpu_tuples[vcpu_id].1 = Some(*pcpu_mask_bitmap); - } - } - if let Some(phys_cpu_ids) = &self.phys_cpu_ids { - for (vcpu_id, phys_id) in phys_cpu_ids.iter().enumerate() { - vcpu_pcpu_tuples[vcpu_id].2 = *phys_id; - } - } - vcpu_pcpu_tuples - } - /// Returns configurations related to VM image load addresses. pub fn image_config(&self) -> &VMImageConfig { &self.image_config @@ -159,22 +137,36 @@ impl AxVMConfig { self.cpu_config.ap_entry } - /// Returns configurations related to VM memory regions. - pub fn memory_regions(&self) -> &Vec { - &self.memory_regions + /// Returns a mutable reference to the physical CPU list. + pub fn phys_cpu_ls_mut(&mut self) -> &mut PhysCpuList { + &mut self.phys_cpu_ls } - /// Adds a new memory region to the VM configuration. - pub fn add_memory_region(&mut self, region: VmMemConfig) { - self.memory_regions.push(region); + /// Returns the list of excluded devices. + pub fn excluded_devices(&self) -> &Vec> { + &self.excluded_devices } - /// Checks if the VM memory regions contain a specific range. - pub fn contains_memory_range(&self, range: &Range) -> bool { - self.memory_regions - .iter() - .any(|region| region.gpa <= range.start && region.gpa + region.size >= range.end) + /// Returns the list of passthrough address configurations. + pub fn pass_through_addresses(&self) -> &Vec { + &self.pass_through_addresses } + // /// Returns configurations related to VM memory regions. + // pub fn memory_regions(&self) -> Vec { + // &self.memory_regions + // } + + // /// Adds a new memory region to the VM configuration. + // pub fn add_memory_region(&mut self, region: VmMemConfig) { + // self.memory_regions.push(region); + // } + + // /// Checks if the VM memory regions contain a specific range. + // pub fn contains_memory_range(&self, range: &Range) -> bool { + // self.memory_regions + // .iter() + // .any(|region| region.gpa <= range.start && region.gpa + region.size >= range.end) + // } /// Returns configurations related to VM emulated devices. pub fn emu_devices(&self) -> &Vec { @@ -191,6 +183,16 @@ impl AxVMConfig { self.pass_through_devices.push(device); } + /// Removes passthrough device from the VM configuration. + pub fn remove_pass_through_device(&mut self, device: PassThroughDeviceConfig) { + self.pass_through_devices.retain(|d| d == &device); + } + + /// Clears all passthrough devices from the VM configuration. + pub fn clear_pass_through_devices(&mut self) { + self.pass_through_devices.clear(); + } + /// Adds a passthrough SPI to the VM configuration. pub fn add_pass_through_spi(&mut self, spi: u32) { self.spi_list.push(spi); @@ -206,3 +208,89 @@ impl AxVMConfig { self.interrupt_mode } } + +/// Represents the list of physical CPUs available for the VM. +#[derive(Debug, Default, Clone)] +pub struct PhysCpuList { + cpu_num: usize, + phys_cpu_ids: Option>, + phys_cpu_sets: Option>, +} + +impl PhysCpuList { + /// Returns vCpu id list and its corresponding pCpu affinity list, as well as its physical id. + /// If the pCpu affinity is None, it means the vCpu will be allocated to any available pCpu randomly. + /// if the pCPU id is not provided, the vCpu's physical id will be set as vCpu id. + /// + /// Returns a vector of tuples, each tuple contains: + /// - The vCpu id. + /// - The pCpu affinity mask, `None` if not set. + /// - The physical id of the vCpu, equal to vCpu id if not provided. + pub fn get_vcpu_affinities_pcpu_ids(&self) -> Vec<(usize, Option, usize)> { + let mut vcpu_pcpu_tuples = Vec::new(); + #[cfg(target_arch = "riscv64")] + let mut pcpu_mask_flag = false; + + if let Some(phys_cpu_ids) = &self.phys_cpu_ids + && self.cpu_num != phys_cpu_ids.len() + { + error!( + "ERROR!!!: cpu_num: {}, phys_cpu_ids: {:?}", + self.cpu_num, self.phys_cpu_ids + ); + } + + for vcpu_id in 0..self.cpu_num { + vcpu_pcpu_tuples.push((vcpu_id, None, vcpu_id)); + } + + #[cfg(target_arch = "riscv64")] + if let Some(phys_cpu_sets) = &self.phys_cpu_sets { + pcpu_mask_flag = true; + for (vcpu_id, pcpu_mask_bitmap) in phys_cpu_sets.iter().enumerate() { + vcpu_pcpu_tuples[vcpu_id].1 = Some(*pcpu_mask_bitmap); + } + } + + #[cfg(not(target_arch = "riscv64"))] + if let Some(phys_cpu_sets) = &self.phys_cpu_sets { + for (vcpu_id, pcpu_mask_bitmap) in phys_cpu_sets.iter().enumerate() { + vcpu_pcpu_tuples[vcpu_id].1 = Some(*pcpu_mask_bitmap); + } + } + + if let Some(phys_cpu_ids) = &self.phys_cpu_ids { + for (vcpu_id, phys_id) in phys_cpu_ids.iter().enumerate() { + vcpu_pcpu_tuples[vcpu_id].2 = *phys_id; + #[cfg(target_arch = "riscv64")] + { + if !pcpu_mask_flag { + // if don't assign pcpu mask yet, assign it manually + vcpu_pcpu_tuples[vcpu_id].1 = Some(1 << (*phys_id)); + } + } + } + } + vcpu_pcpu_tuples + } + + /// Returns the number of CPUs. + pub fn cpu_num(&self) -> usize { + self.cpu_num + } + + /// Returns the physical CPU IDs. + pub fn phys_cpu_ids(&self) -> &Option> { + &self.phys_cpu_ids + } + + /// Returns the physical CPU sets. + pub fn phys_cpu_sets(&self) -> &Option> { + &self.phys_cpu_sets + } + + /// Sets the guest CPU sets. + pub fn set_guest_cpu_sets(&mut self, phys_cpu_sets: Vec) { + self.phys_cpu_sets = Some(phys_cpu_sets); + } +} diff --git a/src/hal.rs b/src/hal.rs index 22e2496..787c074 100644 --- a/src/hal.rs +++ b/src/hal.rs @@ -20,14 +20,6 @@ pub trait AxVMHal: Sized { /// The low-level **OS-dependent** helpers that must be provided for physical address management. type PagingHandler: page_table_multiarch::PagingHandler; - /// Allocates a memory region at the specified physical address. - /// - /// Returns `true` if the memory region is successfully allocated. - fn alloc_memory_region_at(base: HostPhysAddr, size: usize) -> bool; - - /// Deallocates a memory region at the specified physical address. - fn dealloc_memory_region_at(base: HostPhysAddr, size: usize); - /// Converts a virtual address to the corresponding physical address. fn virt_to_phys(vaddr: HostVirtAddr) -> HostPhysAddr; diff --git a/src/lib.rs b/src/lib.rs index 4ff0b60..e4e9d5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,10 +13,6 @@ // limitations under the License. #![no_std] -#![feature(new_range_api)] -// #![feature(concat_idents)] -// #![feature(naked_functions)] -// #![feature(const_trait_impl)] //! This crate provides a minimal VM monitor (VMM) for running guest VMs. //! @@ -37,6 +33,8 @@ pub use hal::AxVMHal; pub use vm::AxVCpuRef; pub use vm::AxVM; pub use vm::AxVMRef; +pub use vm::VMMemoryRegion; +pub use vm::VMStatus; /// The architecture-independent per-CPU type. pub type AxVMPerCpu = axvcpu::AxPerCpu>; diff --git a/src/vm.rs b/src/vm.rs index 264b1a2..12f62d9 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -16,30 +16,26 @@ use alloc::boxed::Box; use alloc::format; use alloc::sync::Arc; use alloc::vec::Vec; -#[cfg(target_arch = "aarch64")] -use axvmconfig::VMInterruptMode; -use core::sync::atomic::{AtomicBool, Ordering}; +use axaddrspace::HostVirtAddr; +use axerrno::{AxError, AxResult, ax_err, ax_err_type}; +use core::alloc::Layout; +use core::fmt; use memory_addr::{align_down_4k, align_up_4k}; - -use axerrno::{AxResult, ax_err, ax_err_type}; -use spin::Mutex; +use spin::{Mutex, Once}; use axaddrspace::{AddrSpace, GuestPhysAddr, HostPhysAddr, MappingFlags, device::AccessWidth}; use axdevice::{AxVmDeviceConfig, AxVmDevices}; use axvcpu::{AxVCpu, AxVCpuExitReason, AxVCpuHal}; - use cpumask::CpuMask; -use crate::config::{AxVMConfig, VmMemMappingType}; +use crate::config::{AxVMConfig, PhysCpuList}; use crate::vcpu::AxArchVCpuImpl; - -#[cfg(not(target_arch = "x86_64"))] -use crate::vcpu::AxVCpuCreateConfig; - use crate::{AxVMHal, has_hardware_support}; +#[cfg(target_arch = "riscv64")] +use crate::vcpu::AxVCpuCreateConfig; #[cfg(target_arch = "aarch64")] -use crate::vcpu::get_sysreg_device; +use crate::vcpu::{AxVCpuCreateConfig, get_sysreg_device}; const VM_ASPACE_BASE: usize = 0x0; const VM_ASPACE_SIZE: usize = 0x7fff_ffff_f000; @@ -55,8 +51,7 @@ pub type AxVCpuRef = Arc>; pub type AxVMRef = Arc>; // we know the bound is not enforced here, we keep it for clarity struct AxVMInnerConst { - id: usize, - config: AxVMConfig, + phys_cpu_ls: PhysCpuList, vcpu_list: Box<[AxVCpuRef]>, devices: AxVmDevices, } @@ -64,20 +59,96 @@ struct AxVMInnerConst { unsafe impl Send for AxVMInnerConst {} unsafe impl Sync for AxVMInnerConst {} +/// Represents a memory region in a virtual machine. +#[derive(Debug, Clone)] +pub struct VMMemoryRegion { + /// Guest physical address. + pub gpa: GuestPhysAddr, + /// Host virtual address. + pub hva: HostVirtAddr, + /// Memory layout of the region. + pub layout: Layout, + /// Whether this region was allocated by the allocator and needs to be deallocated + pub needs_dealloc: bool, +} + +impl VMMemoryRegion { + /// Returns the size of the memory region. + pub fn size(&self) -> usize { + self.layout.size() + } + + /// Returns `true` if the guest physical address is identical to the host virtual address. + pub fn is_identical(&self) -> bool { + self.gpa.as_usize() == self.hva.as_usize() + } +} + struct AxVMInnerMut { // Todo: use more efficient lock. - address_space: Mutex>, + address_space: AddrSpace, + memory_regions: Vec, + config: AxVMConfig, + vm_status: VMStatus, _marker: core::marker::PhantomData, } +/// VM status enumeration representing the lifecycle states of a virtual machine +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VMStatus { + /// VM is being created/loaded + Loading, + /// VM is loaded but not yet started + Loaded, + /// VM is currently running + Running, + /// VM is suspended (paused but can be resumed) + Suspended, + /// VM is in the process of shutting down + Stopping, + /// VM is stopped + Stopped, +} + +impl VMStatus { + /// Get status as a string (lowercase) + pub fn as_str(&self) -> &'static str { + match self { + VMStatus::Loading => "loading", + VMStatus::Loaded => "loaded", + VMStatus::Running => "running", + VMStatus::Suspended => "suspended", + VMStatus::Stopping => "stopping", + VMStatus::Stopped => "stopped", + } + } + + /// Get status with emoji icon + pub fn as_str_with_icon(&self) -> &'static str { + match self { + VMStatus::Loading => "🔄 loading", + VMStatus::Loaded => "đŸ“Ļ loaded", + VMStatus::Running => "🚀 running", + VMStatus::Suspended => "🛑 suspended", + VMStatus::Stopping => "âšī¸ stopping", + VMStatus::Stopped => "💤 stopped", + } + } +} + +impl fmt::Display for VMStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + const TEMP_MAX_VCPU_NUM: usize = 64; /// A Virtual Machine. pub struct AxVM { - running: AtomicBool, - shutting_down: AtomicBool, - inner_const: AxVMInnerConst, - inner_mut: AxVMInnerMut, + id: usize, + inner_const: Once>, + inner_mut: Mutex>, } impl AxVM { @@ -85,122 +156,71 @@ impl AxVM { /// Returns an error if the configuration is invalid. /// The VM is not started until `boot` is called. pub fn new(config: AxVMConfig) -> AxResult> { - let vcpu_id_pcpu_sets = config.get_vcpu_affinities_pcpu_ids(); + let address_space = + AddrSpace::new_empty(GuestPhysAddr::from(VM_ASPACE_BASE), VM_ASPACE_SIZE)?; - debug!( - "id: {}, VCpuIdPCpuSets: {:#x?}", - config.id(), - vcpu_id_pcpu_sets - ); + let result = Arc::new(Self { + id: config.id(), + inner_const: Once::new(), + inner_mut: Mutex::new(AxVMInnerMut { + address_space, + config, + memory_regions: Vec::new(), + vm_status: VMStatus::Loading, + _marker: core::marker::PhantomData, + }), + }); + + info!("VM created: id={}", result.id()); + + Ok(result) + } + + /// Returns the VM id. + #[inline] + pub fn id(&self) -> usize { + self.id + } + + /// Sets up the VM before booting. + pub fn init(&self) -> AxResult { + let mut inner_mut = self.inner_mut.lock(); + + let dtb_addr = inner_mut.config.image_config().dtb_load_gpa; + let vcpu_id_pcpu_sets = inner_mut.config.phys_cpu_ls.get_vcpu_affinities_pcpu_ids(); + + info!("dtb_load_gpa: {:?}", dtb_addr); + debug!("id: {}, VCpuIdPCpuSets: {vcpu_id_pcpu_sets:#x?}", self.id()); let mut vcpu_list = Vec::with_capacity(vcpu_id_pcpu_sets.len()); for (vcpu_id, phys_cpu_set, _pcpu_id) in vcpu_id_pcpu_sets { #[cfg(target_arch = "aarch64")] let arch_config = AxVCpuCreateConfig { mpidr_el1: _pcpu_id as _, - dtb_addr: config - .image_config() - .dtb_load_gpa - .unwrap_or_default() - .as_usize(), + dtb_addr: dtb_addr.unwrap_or_default().as_usize(), }; #[cfg(target_arch = "riscv64")] let arch_config = AxVCpuCreateConfig { hart_id: vcpu_id as _, - dtb_addr: config - .image_config() - .dtb_load_gpa - .unwrap_or(GuestPhysAddr::from_usize(0x9000_0000)), + dtb_addr: dtb_addr.unwrap_or_default().as_usize(), }; - #[cfg(not(target_arch = "x86_64"))] vcpu_list.push(Arc::new(VCpu::new( - config.id(), + self.id(), vcpu_id, 0, // Currently not used. phys_cpu_set, + #[cfg(target_arch = "aarch64")] arch_config, - )?)); - - #[cfg(target_arch = "x86_64")] - vcpu_list.push(Arc::new(VCpu::new( - config.id(), - vcpu_id, - 0, // Currently not used. - phys_cpu_set, + #[cfg(target_arch = "riscv64")] + arch_config, + #[cfg(target_arch = "x86_64")] (), )?)); } - let mut address_space = - AddrSpace::new_empty(GuestPhysAddr::from(VM_ASPACE_BASE), VM_ASPACE_SIZE)?; - - for mem_region in config.memory_regions() { - let mapping_flags = MappingFlags::from_bits(mem_region.flags).ok_or_else(|| { - ax_err_type!( - InvalidInput, - format!("Illegal flags {:?}", mem_region.flags) - ) - })?; - - // Check mapping flags. - if mapping_flags.contains(MappingFlags::DEVICE) { - warn!( - "Do not include DEVICE flag in memory region flags, it should be configured in pass_through_devices" - ); - continue; - } - - info!( - "Setting up memory region: [{:#x}~{:#x}] {:?}", - mem_region.gpa, - mem_region.gpa + mem_region.size, - mapping_flags - ); - - // Handle ram region. - match mem_region.map_type { - VmMemMappingType::MapIdentical => { - if H::alloc_memory_region_at( - HostPhysAddr::from(mem_region.gpa), - mem_region.size, - ) { - } else { - address_space.map_linear( - GuestPhysAddr::from(mem_region.gpa), - HostPhysAddr::from(mem_region.gpa), - mem_region.size, - mapping_flags, - )?; - warn!( - "Failed to allocate memory region at {:#x} for VM [{}]", - mem_region.gpa, - config.id() - ); - } - - address_space.map_linear( - GuestPhysAddr::from(mem_region.gpa), - HostPhysAddr::from(mem_region.gpa), - mem_region.size, - mapping_flags, - )?; - } - VmMemMappingType::MapAlloc => { - // Note: currently we use `map_alloc`, - // which allocates real physical memory in units of physical page frames, - // which may not be contiguous!!! - address_space.map_alloc( - GuestPhysAddr::from(mem_region.gpa), - mem_region.size, - mapping_flags, - true, - )?; - } - } - } let mut pt_dev_region = Vec::new(); - for pt_device in config.pass_through_devices() { + for pt_device in inner_mut.config.pass_through_devices() { trace!( "PT dev {:?} region: [{:#x}~{:#x}] -> [{:#x}~{:#x}]", pt_device.name, @@ -216,6 +236,16 @@ impl AxVM { )); } + for pt_addr in inner_mut.config.pass_through_addresses() { + debug!( + "PT addr region: [{:#x}~{:#x}]", + pt_addr.base_gpa, + pt_addr.base_gpa + pt_addr.length, + ); + // Align the base address and length to 4K boundaries. + pt_dev_region.push((align_down_4k(pt_addr.base_gpa), align_up_4k(pt_addr.length))); + } + pt_dev_region.sort_by_key(|(gpa, _)| *gpa); // Merge overlapping regions. @@ -237,7 +267,7 @@ impl AxVM { }); for (gpa, len) in &pt_dev_region { - address_space.map_linear( + inner_mut.address_space.map_linear( GuestPhysAddr::from(*gpa), HostPhysAddr::from(*gpa), *len, @@ -250,21 +280,20 @@ impl AxVM { #[cfg(target_arch = "aarch64")] let mut devices = axdevice::AxVmDevices::new(AxVmDeviceConfig { - emu_configs: config.emu_devices().to_vec(), + emu_configs: inner_mut.config.emu_devices().to_vec(), }); - #[cfg(target_arch = "aarch64")] - let passthrough = config.interrupt_mode() == VMInterruptMode::Passthrough; - #[cfg(not(target_arch = "aarch64"))] let devices = axdevice::AxVmDevices::new(AxVmDeviceConfig { - emu_configs: config.emu_devices().to_vec(), + emu_configs: inner_mut.config.emu_devices().to_vec(), }); #[cfg(target_arch = "aarch64")] { + let passthrough = + inner_mut.config.interrupt_mode() == axvmconfig::VMInterruptMode::Passthrough; if passthrough { - let spis = config.pass_through_spis(); - let cpu_id = config.id() - 1; // FIXME: get the real CPU id. + let spis = inner_mut.config.pass_through_spis(); + let cpu_id = self.id() - 1; // FIXME: get the real CPU id. let mut gicd_found = false; for device in devices.iter_mmio_dev() { @@ -277,7 +306,7 @@ impl AxVM { gicd.assign_irq(*spi + 32, cpu_id, (0, 0, 0, cpu_id as _)) } - Ok(()) + AxResult::Ok(()) }, ) { result?; @@ -300,58 +329,55 @@ impl AxVM { } } - let result = Arc::new(Self { - running: AtomicBool::new(false), - shutting_down: AtomicBool::new(false), - inner_const: AxVMInnerConst { - id: config.id(), - config, - vcpu_list: vcpu_list.into_boxed_slice(), - devices, - }, - inner_mut: AxVMInnerMut { - address_space: Mutex::new(address_space), - _marker: core::marker::PhantomData, - }, + self.inner_const.call_once(|| AxVMInnerConst { + phys_cpu_ls: inner_mut.config.phys_cpu_ls.clone(), + vcpu_list: vcpu_list.into_boxed_slice(), + devices, }); - info!("VM created: id={}", result.id()); - // Setup VCpus. - #[cfg(target_arch = "aarch64")] - for vcpu in result.vcpu_list() { - let setup_config = crate::vcpu::AxVCpuSetupConfig { - passthrough_interrupt: passthrough, - passthrough_timer: passthrough, + for vcpu in self.vcpu_list() { + #[cfg(target_arch = "aarch64")] + let setup_config = { + let passthrough = + inner_mut.config.interrupt_mode() == axvmconfig::VMInterruptMode::Passthrough; + crate::vcpu::AxVCpuSetupConfig { + passthrough_interrupt: passthrough, + passthrough_timer: passthrough, + } }; let entry = if vcpu.id() == 0 { - result.inner_const.config.bsp_entry() + inner_mut.config.bsp_entry() } else { - result.inner_const.config.ap_entry() + inner_mut.config.ap_entry() }; - vcpu.setup(entry, result.ept_root(), setup_config)?; - } - #[cfg(not(target_arch = "aarch64"))] - for vcpu in result.vcpu_list() { - let entry = if vcpu.id() == 0 { - result.inner_const.config.bsp_entry() - } else { - result.inner_const.config.ap_entry() - }; - vcpu.setup(entry, result.ept_root(), ())?; - } + debug!("Setting up vCPU[{}] entry at {:#x}", vcpu.id(), entry); - info!("VM setup: id={}", result.id()); + vcpu.setup( + entry, + inner_mut.address_space.page_table_root(), + #[cfg(target_arch = "aarch64")] + setup_config, + #[cfg(not(target_arch = "aarch64"))] + (), + )?; + } + info!("VM setup: id={}", self.id()); + Ok(()) + } - Ok(result) + /// Sets the VM status. + pub fn set_vm_status(&self, status: VMStatus) { + let mut inner_mut = self.inner_mut.lock(); + inner_mut.vm_status = status; } - /// Returns the VM id. - #[inline] - pub const fn id(&self) -> usize { - self.inner_const.id + /// Returns the current VM status. + pub fn vm_status(&self) -> VMStatus { + let inner_mut = self.inner_mut.lock(); + inner_mut.vm_status } /// Retrieves the vCPU corresponding to the given vcpu_id for the VM. @@ -363,19 +389,34 @@ impl AxVM { /// Returns the number of vCPUs corresponding to the VM. #[inline] - pub const fn vcpu_num(&self) -> usize { - self.inner_const.vcpu_list.len() + pub fn vcpu_num(&self) -> usize { + self.inner_const().vcpu_list.len() + } + + fn inner_const(&self) -> &AxVMInnerConst { + self.inner_const + .get() + .expect("VM inner_const not initialized") } /// Returns a reference to the list of vCPUs corresponding to the VM. #[inline] pub fn vcpu_list(&self) -> &[AxVCpuRef] { - &self.inner_const.vcpu_list + &self.inner_const().vcpu_list } /// Returns the base address of the two-stage address translation page table for the VM. pub fn ept_root(&self) -> HostPhysAddr { - self.inner_mut.address_space.lock().page_table_root() + self.inner_mut.lock().address_space.page_table_root() + } + + /// Returns to the VM's configuration. + pub fn with_config(&self, f: F) -> R + where + F: FnOnce(&mut AxVMConfig) -> R, + { + let mut g = self.inner_mut.lock(); + f(&mut g.config) } /// Returns guest VM image load region in `Vec<&'static mut [u8]>`, @@ -391,19 +432,15 @@ impl AxVM { image_load_gpa: GuestPhysAddr, image_size: usize, ) -> AxResult> { - let addr_space = self.inner_mut.address_space.lock(); - let image_load_hva = addr_space + let g = self.inner_mut.lock(); + let image_load_hva = g + .address_space .translated_byte_buffer(image_load_gpa, image_size) .expect("Failed to translate kernel image load address"); Ok(image_load_hva) } - /// Returns if the VM is running. - pub fn running(&self) -> bool { - self.running.load(Ordering::Relaxed) - } - - /// Boots the VM by setting the running flag as true. + /// Boots the VM by transitioning to Running state. pub fn boot(&self) -> AxResult { if !has_hardware_support() { ax_err!(Unsupported, "Hardware does not support virtualization") @@ -411,29 +448,44 @@ impl AxVM { ax_err!(BadState, format!("VM[{}] is already running", self.id())) } else { info!("Booting VM[{}]", self.id()); - self.running.store(true, Ordering::Relaxed); + self.set_vm_status(VMStatus::Running); Ok(()) } } - /// Returns if the VM is shutting down. - pub fn shutting_down(&self) -> bool { - self.shutting_down.load(Ordering::Relaxed) + /// Returns if the VM is running. + pub fn running(&self) -> bool { + self.vm_status() == VMStatus::Running } - /// Shuts down the VM by setting the shutting_down flag as true. + /// Returns if the VM is shutting down (in Stopping state). + pub fn stopping(&self) -> bool { + self.vm_status() == VMStatus::Stopping + } + + /// Returns if the VM is suspended. + pub fn suspending(&self) -> bool { + self.vm_status() == VMStatus::Suspended + } + + /// Returns if the VM is stopped. + pub fn stopped(&self) -> bool { + self.vm_status() == VMStatus::Stopped + } + + /// Shuts down the VM by transitioning to Stopping state. /// + /// This method sets the VM status to Stopping, which signals all vCPUs to exit. /// Currently, the "re-init" process of the VM is not implemented. Therefore, a VM can only be /// booted once. And after the VM is shut down, it cannot be booted again. pub fn shutdown(&self) -> AxResult { - if self.shutting_down() { - ax_err!( - BadState, - format!("VM[{}] is already shutting down", self.id()) - ) + if self.stopping() { + ax_err!(BadState, format!("VM[{}] is already stopping", self.id())) + } else if self.stopped() { + ax_err!(BadState, format!("VM[{}] is already stopped", self.id())) } else { info!("Shutting down VM[{}]", self.id()); - self.shutting_down.store(true, Ordering::Relaxed); + self.set_vm_status(VMStatus::Stopping); Ok(()) } } @@ -443,7 +495,7 @@ impl AxVM { /// Returns this VM's emulated devices. pub fn get_devices(&self) -> &AxVmDevices { - &self.inner_const.devices + &self.inner_const().devices } /// Run a vCPU according to the given vcpu_id. @@ -483,8 +535,12 @@ impl AxVM { } AxVCpuExitReason::IoRead { port, width } => { let val = self.get_devices().handle_port_read(*port, *width)?; + #[cfg(not(target_arch = "riscv64"))] vcpu.set_gpr(0, val); // The target is always eax/ax/al, todo: handle access_width correctly + #[cfg(target_arch = "riscv64")] + vcpu.set_gpr(riscv_vcpu::GprIndex::A0 as usize, val); + true } AxVCpuExitReason::IoWrite { port, width, data } => { @@ -512,8 +568,8 @@ impl AxVM { } AxVCpuExitReason::NestedPageFault { addr, access_flags } => self .inner_mut - .address_space .lock() + .address_space .handle_page_fault(*addr, *access_flags), _ => false, }; @@ -549,11 +605,25 @@ impl AxVM { Ok(()) } - /// Returns a reference to the VM's configuration. - pub fn config(&self) -> &AxVMConfig { - &self.inner_const.config + /// Returns vCpu id list and its corresponding pCpu affinity list, as well as its physical id. + /// If the pCpu affinity is None, it means the vCpu will be allocated to any available pCpu randomly. + /// if the pCPU id is not provided, the vCpu's physical id will be set as vCpu id. + /// + /// Returns a vector of tuples, each tuple contains: + /// - The vCpu id. + /// - The pCpu affinity mask, `None` if not set. + /// - The physical id of the vCpu, equal to vCpu id if not provided. + pub fn get_vcpu_affinities_pcpu_ids(&self) -> Vec<(usize, Option, usize)> { + self.inner_const() + .phys_cpu_ls + .get_vcpu_affinities_pcpu_ids() } + // /// Returns a reference to the VM's configuration. + // pub fn config(&self) -> &AxVMConfig { + // &self.inner_const.config + // } + /// Maps a region of host physical memory to guest physical memory. pub fn map_region( &self, @@ -561,16 +631,18 @@ impl AxVM { hpa: HostPhysAddr, size: usize, flags: MappingFlags, - ) -> AxResult<()> { + ) -> AxResult { self.inner_mut - .address_space .lock() - .map_linear(gpa, hpa, size, flags) + .address_space + .map_linear(gpa, hpa, size, flags)?; + Ok(()) } /// Unmaps a region of guest physical memory. - pub fn unmap_region(&self, gpa: GuestPhysAddr, size: usize) -> AxResult<()> { - self.inner_mut.address_space.lock().unmap(gpa, size) + pub fn unmap_region(&self, gpa: GuestPhysAddr, size: usize) -> AxResult { + self.inner_mut.lock().address_space.unmap(gpa, size)?; + Ok(()) } /// Reads an object of type `T` from the guest physical address. @@ -585,8 +657,8 @@ impl AxVM { return ax_err!(InvalidInput, "Unaligned guest physical address"); } - let addr_space = self.inner_mut.address_space.lock(); - match addr_space.translated_byte_buffer(gpa_ptr, size) { + let g = self.inner_mut.lock(); + match g.address_space.translated_byte_buffer(gpa_ptr, size) { Some(buffers) => { let mut data_bytes = Vec::with_capacity(size); for chunk in buffers { @@ -618,9 +690,12 @@ impl AxVM { /// Writes an object of type `T` to the guest physical address. pub fn write_to_guest_of(&self, gpa_ptr: GuestPhysAddr, data: &T) -> AxResult { - let addr_space = self.inner_mut.address_space.lock(); - - match addr_space.translated_byte_buffer(gpa_ptr, core::mem::size_of::()) { + match self + .inner_mut + .lock() + .address_space + .translated_byte_buffer(gpa_ptr, core::mem::size_of::()) + { Some(mut buffer) => { let bytes = unsafe { core::slice::from_raw_parts( @@ -649,7 +724,7 @@ impl AxVM { pub fn alloc_ivc_channel(&self, expected_size: usize) -> AxResult<(GuestPhysAddr, usize)> { // Ensure the expected size is aligned to 4K. let size = align_up_4k(expected_size); - let gpa = self.inner_const.devices.alloc_ivc_channel(size)?; + let gpa = self.inner_const().devices.alloc_ivc_channel(size)?; Ok((gpa, size)) } @@ -660,6 +735,201 @@ impl AxVM { /// ## Returns /// * `AxResult<()>` - An empty result indicating success or failure. pub fn release_ivc_channel(&self, gpa: GuestPhysAddr, size: usize) -> AxResult { - self.inner_const.devices.release_ivc_channel(gpa, size) + self.inner_const().devices.release_ivc_channel(gpa, size)?; + Ok(()) + } + + /// Allocates a new memory region for the VM. + pub fn alloc_memory_region( + &self, + layout: Layout, + gpa: Option, + ) -> AxResult<&[u8]> { + assert!( + layout.size() > 0, + "Cannot allocate zero-sized memory region" + ); + + let hva = unsafe { alloc::alloc::alloc_zeroed(layout) }; + if hva.is_null() { + return Err(AxError::NoMemory); + } + let s = unsafe { core::slice::from_raw_parts_mut(hva, layout.size()) }; + let hva = HostVirtAddr::from_mut_ptr_of(hva); + + let hpa = H::virt_to_phys(hva); + + let gpa = gpa.unwrap_or_else(|| hpa.as_usize().into()); + + let mut g = self.inner_mut.lock(); + g.address_space.map_linear( + gpa, + hpa, + layout.size(), + MappingFlags::READ | MappingFlags::WRITE | MappingFlags::EXECUTE | MappingFlags::USER, + )?; + g.memory_regions.push(VMMemoryRegion { + gpa, + hva, + layout, + needs_dealloc: true, // This region was allocated and needs to be freed + }); + + Ok(s) + } + + /// Returns a list of all memory regions in the VM. + pub fn memory_regions(&self) -> Vec { + self.inner_mut.lock().memory_regions.clone() + } + + /// Maps a reserved memory region for the VM. + pub fn map_reserved_memory_region( + &self, + layout: Layout, + gpa: Option, + ) -> AxResult<&[u8]> { + assert!( + layout.size() > 0, + "Cannot allocate zero-sized memory region" + ); + let mut g = self.inner_mut.lock(); + g.address_space.map_linear( + gpa.unwrap(), + gpa.unwrap().as_usize().into(), + layout.size(), + MappingFlags::READ | MappingFlags::WRITE | MappingFlags::EXECUTE | MappingFlags::USER, + )?; + let hva = gpa.unwrap().as_usize().into(); + let tem_hva = gpa.unwrap().as_usize() as *mut u8; + let s = unsafe { core::slice::from_raw_parts_mut(tem_hva, layout.size()) }; + let gpa = gpa.unwrap(); + g.memory_regions.push(VMMemoryRegion { + gpa, + hva, + layout, + needs_dealloc: false, // This is a reserved region, not allocated + }); + Ok(s) + } + + /// Cleanup resources for the VM before drop. + /// This is called internally by the Drop implementation. + fn cleanup_resources(&self) { + info!("Cleaning up VM[{}] resources...", self.id()); + + // 1. Ensure the VM is in Stopping or Stopped state + let current_status = self.vm_status(); + if !matches!(current_status, VMStatus::Stopping | VMStatus::Stopped) { + warn!( + "VM[{}] is being dropped without explicit shutdown (status: {:?}), marking as stopping", + self.id(), + current_status + ); + self.set_vm_status(VMStatus::Stopping); + } + + let mut inner_mut = self.inner_mut.lock(); + + // First, collect all memory regions to clean up + // We need to clone the regions to avoid borrowing issues + let regions_to_cleanup: Vec = inner_mut.memory_regions.clone(); + + // Unmap all memory regions from the address space + // This must be done BEFORE deallocating memory to avoid use-after-free + for region in ®ions_to_cleanup { + debug!( + "VM[{}] unmapping memory region: GPA={:#x}, size={:#x}", + self.id(), + region.gpa.as_usize(), + region.size() + ); + // Unmap the region from guest physical address space + if let Err(e) = inner_mut.address_space.unmap(region.gpa, region.size()) { + warn!( + "VM[{}] failed to unmap region at GPA={:#x}: {:?}", + self.id(), + region.gpa.as_usize(), + e + ); + } + } + + // Now it's safe to deallocate the memory + for region in ®ions_to_cleanup { + // Only deallocate memory regions that were allocated by the allocator + if region.needs_dealloc { + debug!( + "VM[{}] deallocating memory region: HVA={:#x}, size={:#x}", + self.id(), + region.hva.as_usize(), + region.size() + ); + unsafe { + alloc::alloc::dealloc(region.hva.as_mut_ptr(), region.layout); + } + } else { + debug!( + "VM[{}] skipping dealloc for reserved memory region: GPA={:#x}, HVA={:#x}, size={:#x}", + self.id(), + region.gpa.as_usize(), + region.hva.as_usize(), + region.size() + ); + } + } + inner_mut.memory_regions.clear(); + + // Clear remaining address space mappings + // This includes: + // - Passthrough device MMIO mappings + // - Emulated device MMIO mappings + // - Reserved memory mappings + // - All other page table entries + debug!( + "VM[{}] clearing remaining address space mappings", + self.id() + ); + inner_mut.address_space.clear(); + + // Release the lock before accessing inner_const + drop(inner_mut); + + // Device cleanup + // Although devices will be automatically dropped when inner_const is dropped, + // we should perform explicit cleanup if devices hold resources like: + // - Hardware interrupt registrations + // - DMA mappings + // - Background threads or timers + if let Some(inner_const) = self.inner_const.get() { + debug!( + "VM[{}] devices cleanup: {} MMIO devices, {} SysReg devices", + self.id(), + inner_const.devices.iter_mmio_dev().count(), + inner_const.devices.iter_sys_reg_dev().count() + ); + + // TODO: Add device-specific cleanup if needed + // For example: + // - Stop device background tasks + // - Unregister interrupts + // - Release device-specific resources + + // Note: Device Arc references will be dropped automatically when + // inner_const is dropped at the end of AxVM's drop + } + + info!("VM[{}] resources cleanup completed", self.id()); + } +} + +impl Drop for AxVM { + fn drop(&mut self) { + info!("Dropping VM[{}]", self.id()); + + // Clean up all allocated resources + self.cleanup_resources(); + + info!("VM[{}] dropped", self.id()); } }