axvm 0.5.26

Virtual Machine resource management crate for ArceOS's hypervisor variant.
//! x86_64 VM resource creation and initialization.

use ax_memory_addr::PAGE_SIZE_4K;
use axdevice::{DeviceFirmwareBinding, DeviceNodeId, DeviceNodeSpec};

use super::*;
use crate::{
    config::*,
    layout::*,
    vm::{
        prepare::{device_plan::*, devices::*, vcpus::*, *},
        *,
    },
};

pub(crate) type X86VmPlan = SimpleVmPlan;

const ARCH_OWNED_REGIONS: [GuestOwnedRegion; 1] = [GuestOwnedRegion::new(
    X86_LOCAL_APIC_GPA,
    X86_LOCAL_APIC_SIZE,
    crate::layout::VmRegionKind::Reserved,
)];

impl X86_64Arch {
    pub(crate) fn create_vm_resources(
        config: AxVMConfig,
        fw_cfg_payload: std::sync::Arc<axdevice::FwCfgPayloadSlot>,
    ) -> AxVmResult<AxVMResources> {
        #[cfg(feature = "host-fs")]
        let config = {
            let mut config = config;
            apply_host_serial(&mut config)?;
            config
        };
        let device_plan = plan_devices(&config, fw_cfg_payload)?;
        let placements = config.phys_cpu_ls.get_vcpu_affinities_pcpu_ids();
        let levels = guest_page_table_levels(&placements)?;
        let page_table = nested_paging::NestedPageTable::new(levels)?;
        AxVMResources::from_page_table(config, page_table, device_plan, |root_paddr| {
            let gpa_bits = match levels {
                3 => 39,
                4 => 48,
                _ => {
                    return ax_err!(InvalidInput, "unsupported x86 nested page-table levels");
                }
            };
            Ok(NestedPagingConfig::new(root_paddr, levels, gpa_bits, 0))
        })
    }

    pub(crate) fn init_vm(vm: &AxVM) -> AxVmResult {
        vm.prepare_resources_with(|resources| {
            let placements = resources.vcpu_placements();
            let vcpus = PreparedVcpus::create(vm.id(), &placements, |_| Ok(X86VcpuCreateConfig))?;
            let devices = PreparedDevices::build_planned(resources, vm.device_access_ports())?;
            let interrupt_controller = devices
                .devices()
                .interrupt_controller(axdevice_base::InterruptControllerId::new(0))?;
            resources.prepare_guest_address_space(vm.id(), &ARCH_OWNED_REGIONS)?;
            resources.map_arch_address_space()?;
            let intercepted_ports = resources.resolved_port_intercepts()?;
            vcpus.setup(resources, |config, memory_regions| {
                build_vcpu_setup_config(config, memory_regions, &intercepted_ports)
            })?;

            Ok(PreparedVm::new(vcpus, devices, interrupt_controller))
        })
    }
}

#[cfg(feature = "host-fs")]
fn apply_host_serial(config: &mut AxVMConfig) -> AxVmResult {
    let Some(serial) = ax_driver::probe::acpi::with_acpi(|acpi| acpi.serial_console()) else {
        return Ok(());
    };
    let Some(serial) = serial.map_err(|error| {
        AxVmError::invalid_config(std::format!(
            "failed to parse host ACPI serial console: {error}"
        ))
    })?
    else {
        return Ok(());
    };
    let snapshot = crate::machine::host_serial_from_acpi(serial, config.serial_profile())?;
    config.replace_machine_serial(snapshot.profile, Some(snapshot.identity))
}

fn plan_devices(
    config: &AxVMConfig,
    fw_cfg_payload: std::sync::Arc<axdevice::FwCfgPayloadSlot>,
) -> AxVmResult<X86VmPlan> {
    let low_memory_size = super::cmos::guest_low_memory_size(config)?;
    let controller_id = DeviceNodeId::new("ioapic")?;
    let mut nodes = std::vec![
        DeviceNodeSpec::virtual_device(
            controller_id.clone(),
            super::ioapic_model(config.id(), 0xfec0_0000, 0x1000),
        )
        .with_firmware_binding(DeviceFirmwareBinding::AcpiDevice("IOAPIC".into())),
        DeviceNodeSpec::virtual_device(
            DeviceNodeId::new("fw-cfg")?,
            std::sync::Arc::new(axdevice::FwCfgPayloadFactory::deferred_pio(
                GuestPhysAddr::from(0x510),
                0x0c,
                fw_cfg_payload,
            )),
        )
        .with_firmware_binding(DeviceFirmwareBinding::AcpiDevice("\\_SB.FWCF".into())),
        DeviceNodeSpec::virtual_device(DeviceNodeId::new("pit")?, super::pit_model(config.id()),),
        DeviceNodeSpec::virtual_device(
            DeviceNodeId::new("pic")?,
            std::sync::Arc::new(super::pic::X86PicModel),
        ),
        DeviceNodeSpec::virtual_device(
            DeviceNodeId::new("cmos")?,
            std::sync::Arc::new(super::cmos::X86CmosModel::new(low_memory_size)),
        ),
        DeviceNodeSpec::virtual_device(
            DeviceNodeId::new("pci-config")?,
            std::sync::Arc::new(super::pci_config::X86PciConfigModel),
        ),
        DeviceNodeSpec::virtual_device(
            DeviceNodeId::new("acpi-pm-timer")?,
            std::sync::Arc::new(super::acpi_pm_timer::X86AcpiPmTimerModel),
        )
        .with_dependency(controller_id.clone()),
    ];
    for port in config.pass_through_ports() {
        let id = DeviceNodeId::new(std::format!("host-port-{:x}", port.base))?;
        nodes.push(DeviceNodeSpec::virtual_device(
            id,
            std::sync::Arc::new(super::port::HostPortPassthroughDeviceModel::new(
                port.base,
                port.length,
            )),
        ));
    }
    crate::configured::append_configured_devices(
        config,
        &mut nodes,
        &controller_id,
        axdevice_base::InterruptControllerId::new(0),
    )?;
    Ok(SimpleVmPlan::new(VmDevicePlan::with_pools_for_vm(
        config,
        nodes,
        &[],
        super::resource_pools::create(config)?,
    )?))
}

fn build_vcpu_setup_config(
    _config: &AxVMConfig,
    memory_regions: &[crate::vm::VMMemoryRegion],
    intercepted_ports: &[(u16, u16)],
) -> AxVmResult<<super::AxvmX86Vcpu as VmArchVcpuOps>::SetupConfig> {
    let mut setup_config = X86VcpuSetupConfig {
        guest_memory_regions: memory_regions
            .iter()
            .map(|region| X86GuestMemoryRegion {
                gpa: X86GuestPhysAddr::from_usize(region.gpa.as_usize()),
                hva: X86HostVirtAddr::from_usize(region.hva.as_usize()),
                size: region.size(),
            })
            .collect(),
        ..Default::default()
    };
    for &(base, size) in intercepted_ports {
        x86_result(setup_config.add_intercepted_port_range(base, size))
            .map_err(|error| AxVmError::vcpu("configure resolved device port intercept", error))?;
    }
    Ok(setup_config)
}

impl AxVMResources {
    fn resolved_port_intercepts(&self) -> AxVmResult<std::vec::Vec<(u16, u16)>> {
        let graph = self.planned_devices().graph();
        let mut ranges = std::vec::Vec::new();
        for node in graph.nodes() {
            ranges.extend(
                graph
                    .resources_for(node.id())?
                    .pio_ranges()
                    .map(|(_, base, size)| (base, size)),
            );
        }
        Ok(ranges)
    }

    fn map_arch_address_space(&mut self) -> AxVmResult {
        if x86_requires_apic_access_page()? {
            let gpa = x86_apic_access_page_gpa()?;
            self.address_space
                .map_linear(
                    gpa,
                    x86_apic_access_page_addr()?,
                    PAGE_SIZE_4K,
                    MappingFlags::DEVICE | MappingFlags::READ | MappingFlags::WRITE,
                )
                .map_err(|error| AxVmError::memory("map x86 APIC access page", error))?;
        }
        Ok(())
    }
}

fn guest_page_table_levels(vcpu_mappings: &[(usize, Option<usize>, usize)]) -> AxVmResult<usize> {
    crate::architecture::minimum_recorded_target_cpu_capability(
        "x86 nested page-table levels",
        vcpu_mappings,
        |cpu_id| {
            crate::percpu::select_cpu_virtualization_capability(cpu_id, |levels, _, _| {
                levels as u64
            })
        },
    )
    .map(|levels| levels as usize)
    .map_err(|error| {
        crate::architecture::unsupported_target_cpu_capability(
            "select x86 target CPU capability",
            error,
        )
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn svm_reserves_the_local_apic_trap_region() {
        let layout = crate::layout::build_address_layout(
            axvm_types::AddressSpacePolicy::Passthrough,
            0,
            0x1_0000_0000,
            &[],
            &[],
            &ARCH_OWNED_REGIONS,
            &[],
        )
        .unwrap();
        assert_eq!(
            layout
                .mappings()
                .iter()
                .map(|mapping| (mapping.gpa.as_usize(), mapping.size))
                .collect::<std::vec::Vec<_>>(),
            [
                (0, X86_LOCAL_APIC_GPA),
                (
                    X86_LOCAL_APIC_GPA + X86_LOCAL_APIC_SIZE,
                    0x1_0000_0000 - X86_LOCAL_APIC_GPA - X86_LOCAL_APIC_SIZE,
                ),
            ]
        );
    }
}