From 6ebfbf240eedcd7cb09daa4792c2c76609ec773a Mon Sep 17 00:00:00 2001 From: Josen Date: Mon, 25 May 2026 15:38:33 +0800 Subject: [PATCH 1/6] feat(svm): add configuration for Linux guest VM and update QEMU smoke test settings --- .../vms/linux-x86_64-qemu-smp1-svm.toml | 79 +++ .../svm/qemu/build-x86_64-unknown-none.toml | 2 +- .../axvisor/svm/qemu/smoke/qemu-x86_64.toml | 22 +- virtualization/x86_vcpu/src/svm/vcpu.rs | 540 +++++++++++++++++- 4 files changed, 605 insertions(+), 38 deletions(-) create mode 100644 os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml diff --git a/os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml b/os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml new file mode 100644 index 0000000000..7f925824ed --- /dev/null +++ b/os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml @@ -0,0 +1,79 @@ +# Vm base info configs +# +[base] +# Guest vm id. +id = 1 +# Guest vm name. +name = "linux-x86_64-qemu-svm" +# Virtualization type. +vm_type = 1 +# The number of virtual CPUs. +cpu_num = 1 +# Guest vm physical cpu sets. +phys_cpu_sets = [1] + +# +# Vm kernel configs +# +[kernel] +# The entry point of the kernel image. +# Linux direct boot uses the Linux-specific boot stub at this GPA. +entry_point = 0x8000 +# The location of image: "memory" | "fs". +image_location = "fs" +# The file path of the kernel image. +kernel_path = "/guest/linux/linux-qemu" +# The load address of the kernel image. +kernel_load_addr = 0x20_0000 +# Whether to enable BIOS boot flow. +enable_bios = false +# SVM phase-1 smoke keeps a dedicated command line so later stages can +# adjust SVM bring-up independently from the VMX baseline. +cmdline = "console=ttyS0 root=/dev/vda rw rootwait devtmpfs.mount=1 init=/sbin/getty acpi=off pci=conf1 pci=nomsi nox2apic tsc=unstable initcall_blacklist=ahci_pci_driver_init,i8042_init -- -n -l /bin/sh -L 115200 ttyS0 dumb" + +## The path of the disk image. +# disk_path = "" + +# Memory regions with format (`base_paddr`, `size`, `flags`, `map_type`). +# For `map_type`, 0 means `MAP_ALLOC`, 1 means `MAP_IDENTICAL`, 2 means `MAP_RESERVED`. +memory_regions = [ + [0x0000_0000, 0x0800_0000, 0x7, 1], # System RAM 128M MAP_IDENTICAL for DMA-capable passthrough + [0x0000_0000, 0x0010_0000, 0x7, 0], # Low boot scratch for real-mode stub and boot_params +] + +# +# Device specifications +# +[devices] +# The interrupt mode. +interrupt_mode = "passthrough" + +# Emu_devices. +# Name Base-Ipa Ipa_len Alloc-Irq Emu-Type EmuConfig. +emu_devices = [ + ["x86-com1", 0x3f8, 0x8, 0x0, 0x2, []], + ["x86-ioapic", 0xfec0_0000, 0x1000, 0x0, 0x23, []], + ["x86-pit", 0x40, 0x22, 0x0, 0x24, []], +] + +# Pass-through devices. +# Name Base-Ipa Base-Pa Length Alloc-Irq. +passthrough_devices = [ + [ + "HPET", + 0xfed0_0000, + 0xfed0_0000, + 0x1000, + 0x1, + ], +] + +# Passthrough addresses. +# Base-GPA Length. +passthrough_addresses = [ + # QEMU q35 low MMIO window needed by PCI/virtio-blk probing. + [0xfe00_0000, 0x00c0_0000], +] + +# Devices that are not desired to be passed through to the guest +excluded_devices = [] diff --git a/test-suit/axvisor/svm/qemu/build-x86_64-unknown-none.toml b/test-suit/axvisor/svm/qemu/build-x86_64-unknown-none.toml index c160743f7c..780af23aa2 100644 --- a/test-suit/axvisor/svm/qemu/build-x86_64-unknown-none.toml +++ b/test-suit/axvisor/svm/qemu/build-x86_64-unknown-none.toml @@ -8,4 +8,4 @@ features = [ ] log = "Info" target = "x86_64-unknown-none" -vm_configs = ["os/axvisor/configs/vms/nimbos-x86_64-qemu-smp1.toml"] +vm_configs = ["os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml"] diff --git a/test-suit/axvisor/svm/qemu/smoke/qemu-x86_64.toml b/test-suit/axvisor/svm/qemu/smoke/qemu-x86_64.toml index 4f3e441239..1b7c3e32e7 100644 --- a/test-suit/axvisor/svm/qemu/smoke/qemu-x86_64.toml +++ b/test-suit/axvisor/svm/qemu/smoke/qemu-x86_64.toml @@ -1,28 +1,36 @@ args = [ - "-nographic", + "-nodefaults", + "-no-user-config", + "-display", + "none", + "-serial", + "stdio", + "-monitor", + "none", "-cpu", "host", "-machine", - "q35", + "q35,sata=off,smbus=off,i8042=off,usb=off,graphics=off", "-smp", "1", "-accel", "kvm", "-device", - "virtio-blk-pci,drive=disk0", + "virtio-blk-pci,drive=disk0,addr=03.0", "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", "-m", - "128M", + "512M", ] +timeout = 180 fail_regex = [ "(?i)\\bpanic(?:ked)?\\b", "(?i)kernel panic", "(?i)login incorrect", "(?i)permission denied", ] -success_regex = ["Hello world from user mode program!"] -shell_prefix = ">>" -shell_init_cmd = "hello_world" +success_regex = ["(?m)^guest linux test pass!\\s*$"] +shell_prefix = "~ #" +shell_init_cmd = "pwd && echo 'guest linux test pass!'" to_bin = false uefi = false diff --git a/virtualization/x86_vcpu/src/svm/vcpu.rs b/virtualization/x86_vcpu/src/svm/vcpu.rs index 8307714e8d..0e2cf2437d 100644 --- a/virtualization/x86_vcpu/src/svm/vcpu.rs +++ b/virtualization/x86_vcpu/src/svm/vcpu.rs @@ -1,3 +1,4 @@ +use alloc::collections::VecDeque; use core::{ arch::asm, fmt::{Debug, Formatter, Result as FmtResult}, @@ -5,16 +6,21 @@ use core::{ }; use ax_errno::{AxResult, ax_err, ax_err_type}; +use ax_memory_addr::AddrRange; use axaddrspace::{ - GuestPhysAddr, HostPhysAddr, MappingFlags, NestedPageFaultInfo, + GuestPhysAddr, GuestVirtAddr, HostPhysAddr, MappingFlags, NestedPageFaultInfo, device::{AccessWidth, Port, SysRegAddr, SysRegAddrRange}, }; use axdevice_base::BaseDeviceOps; use axvcpu::{AxArchVCpu, AxVCpuExitReason}; -use axvisor_api::vmm::{VCpuId, VMId}; +use axvisor_api::{ + memory::{self, PhysAddr}, + vmm::{VCpuId, VMId}, +}; use bit_field::BitField; use tock_registers::interfaces::{ReadWriteable, Readable, Writeable}; -use x86_64::registers::control::Cr0Flags; +use x86::controlregs::Xcr0; +use x86_64::registers::control::{Cr0Flags, Cr4Flags, EferFlags}; use x86_vlapic::EmulatedLocalApic; use super::{ @@ -29,7 +35,20 @@ use crate::{ const QEMU_EXIT_PORT: u16 = 0x604; const QEMU_EXIT_MAGIC: u64 = 0x2000; -const QEMU_RESET_PORT: u16 = 0xcf9; +const X86_PIT_PORT_BASE: u16 = 0x40; +const X86_PIT_PORT_COUNT: u32 = 4; +const X86_PIT_SPEAKER_PORT: u16 = 0x61; +const X86_COM1_PORT_BASE: u16 = 0x3f8; +const X86_COM1_PORT_COUNT: u32 = 8; +const X86_IOAPIC_BASE: usize = 0xfec0_0000; +const X86_IOAPIC_SIZE: usize = 0x1000; +const X86_LOCAL_APIC_BASE: usize = 0xfee0_0000; +const X86_LOCAL_APIC_SIZE: usize = 0x1000; +const X86_LOCAL_APIC_EOI_OFFSET: usize = 0xb0; + +const APIC_BASE_MSR: u32 = 0x1b; +const IA32_UMWAIT_CONTROL: u32 = 0xe1; +const AMD64_DE_CFG: u32 = 0xc001_1029; const EFER_SVME: u64 = 1 << 12; const EFER_LMA: u64 = 1 << 10; @@ -92,6 +111,13 @@ pub enum VmCpuMode { Mode64, } +#[derive(Clone, Copy, Debug)] +struct PendingEvent { + vector: u8, + err_code: Option, + level_triggered: bool, +} + /// Host save area used to restore CPU state touched by SVM VMLOAD/VMSAVE. pub struct VmLoadSaveStates { vmcb: VmcbFrame, @@ -147,6 +173,8 @@ pub struct SvmVcpu { iopm: IOPm, /// The MSR permission map used by SVM MSR intercepts. msrpm: MSRPm, + /// Pending events to be injected to the guest. + pending_events: VecDeque, /// Emulated Local APIC for x2APIC MSR accesses. vlapic: EmulatedLocalApic, /// The XState of the VCpu. Both host and guest. @@ -166,6 +194,7 @@ impl SvmVcpu { load_save_states: VmLoadSaveStates::new()?, iopm: IOPm::passthrough_all()?, msrpm: MSRPm::passthrough_all()?, + pending_events: VecDeque::with_capacity(8), vlapic: EmulatedLocalApic::new(vm_id, vcpu_id), xstate: XState::new(), }; @@ -173,8 +202,13 @@ impl SvmVcpu { Ok(vcpu) } - fn setup_vmcb(&mut self, entry: GuestPhysAddr, npt_root: HostPhysAddr) -> AxResult { - self.setup_io_bitmap()?; + fn setup_vmcb( + &mut self, + entry: GuestPhysAddr, + npt_root: HostPhysAddr, + config: X86VCpuSetupConfig, + ) -> AxResult { + self.setup_io_bitmap(config)?; self.setup_msr_bitmap()?; self.setup_vmcb_guest(entry)?; self.setup_vmcb_control(npt_root) @@ -229,7 +263,6 @@ impl SvmVcpu { control .tlb_control .modify(VmcbTlbControl::CONTROL::FlushGuestTlb); - control.int_control.set(1 << 24); control.intercept_cr.modify( InterceptCrRw::WRITE_CR0::SET + InterceptCrRw::WRITE_CR3::SET @@ -245,6 +278,7 @@ impl SvmVcpu { SvmIntercept::INTR, SvmIntercept::NMI, SvmIntercept::CPUID, + SvmIntercept::PAUSE, SvmIntercept::HLT, SvmIntercept::IOIO_PROT, SvmIntercept::MSR_PROT, @@ -256,6 +290,7 @@ impl SvmVcpu { SvmIntercept::STGI, SvmIntercept::CLGI, SvmIntercept::SKINIT, + SvmIntercept::XSETBV, ] { control.set_intercept(intercept); } @@ -271,10 +306,19 @@ impl SvmVcpu { } fn setup_msr_bitmap(&mut self) -> AxResult { + // Keep APIC state in the emulated local APIC instead of exposing the host APIC MSR. + self.msrpm.set_read_intercept(APIC_BASE_MSR, true); + self.msrpm.set_write_intercept(APIC_BASE_MSR, true); // Keep EFER under software control so the guest never observes or // clears the host-required SVME bit stored in the VMCB. self.msrpm.set_read_intercept(Msr::IA32_EFER as u32, true); self.msrpm.set_write_intercept(Msr::IA32_EFER as u32, true); + // Match VMX's Linux direct-boot path: UMWAIT and AMD64_DE_CFG are + // handled in software so guest probes do not leak host-specific state. + self.msrpm.set_read_intercept(IA32_UMWAIT_CONTROL, true); + self.msrpm.set_write_intercept(IA32_UMWAIT_CONTROL, true); + self.msrpm.set_read_intercept(AMD64_DE_CFG, true); + self.msrpm.set_write_intercept(AMD64_DE_CFG, true); // Route x2APIC MSRs through the emulated local APIC instead of the host APIC. for msr in X2APIC_MSR_BASE..=X2APIC_MSR_END { self.msrpm.set_read_intercept(msr, true); @@ -283,13 +327,17 @@ impl SvmVcpu { Ok(()) } - fn setup_io_bitmap(&mut self) -> AxResult { - // These ports are part of the x86 QEMU test contract: 0x604 reports - // test completion and 0xcf9 requests reset/poweroff. + fn setup_io_bitmap(&mut self, config: X86VCpuSetupConfig) -> AxResult { + // This port is part of the x86 QEMU test contract: 0x604 reports test completion. self.iopm .set_intercept_of_range(QEMU_EXIT_PORT as _, 2, true); self.iopm - .set_intercept_of_range(QEMU_RESET_PORT as _, 1, true); + .set_intercept_of_range(X86_PIT_PORT_BASE as u32, X86_PIT_PORT_COUNT, true); + self.iopm.set_intercept(X86_PIT_SPEAKER_PORT as u32, true); + if config.emulate_com1 { + self.iopm + .set_intercept_of_range(X86_COM1_PORT_BASE as u32, X86_COM1_PORT_COUNT, true); + } Ok(()) } @@ -401,6 +449,29 @@ impl SvmVcpu { Ok(()) } + /// Add a virtual interrupt or exception to the pending events list. + pub fn queue_event(&mut self, vector: u8, err_code: Option) { + self.pending_events.push_back(PendingEvent { + vector, + err_code, + level_triggered: false, + }); + } + + /// Add a virtual interrupt or exception with trigger mode metadata. + pub fn queue_event_with_trigger( + &mut self, + vector: u8, + err_code: Option, + level_triggered: bool, + ) { + self.pending_events.push_back(PendingEvent { + vector, + err_code, + level_triggered, + }); + } + fn flush_guest_tlb(&mut self) { unsafe { self.vmcb @@ -421,11 +492,14 @@ impl SvmVcpu { fn inner_run(&mut self) -> AxResult { loop { + self.inject_pending_events()?; unsafe { self.svm_run(); } self.launched = true; + self.clear_event_inj(); + let exit_info = self.exit_info()?; // Consume exits that are fully handled inside the architecture @@ -442,9 +516,13 @@ impl SvmVcpu { fn builtin_vmexit_handler(&mut self, exit_info: &super::vmcb::SvmExitInfo) -> Option { match exit_info.exit_code { Ok(SvmExitCode::CPUID) => Some(self.handle_cpuid()), + Ok(SvmExitCode::XSETBV) => Some(self.handle_xsetbv()), Ok(SvmExitCode::CR_WRITE(cr @ (0 | 3 | 4))) => { Some(self.handle_cr_write(cr as usize, exit_info)) } + Ok(SvmExitCode::MSR) if self.regs().rcx as u32 == APIC_BASE_MSR => { + Some(self.handle_apic_base_msr_access(exit_info)) + } Ok(SvmExitCode::MSR) if self.regs().rcx as u32 == Msr::IA32_EFER as u32 => { Some(self.handle_efer_msr(exit_info)) } @@ -453,6 +531,11 @@ impl SvmVcpu { { Some(self.handle_apic_msr_access(exit_info, self.regs().rcx as u32)) } + Ok(SvmExitCode::MSR) + if matches!(self.regs().rcx as u32, IA32_UMWAIT_CONTROL | AMD64_DE_CFG) => + { + Some(self.handle_zero_msr_access(exit_info)) + } _ => None, } } @@ -493,6 +576,17 @@ impl SvmVcpu { self.advance_rip(VM_EXIT_INSTR_LEN_MSR) } + fn handle_apic_base_msr_access(&mut self, exit_info: &super::vmcb::SvmExitInfo) -> AxResult { + const VM_EXIT_INSTR_LEN_MSR: u8 = 2; + + if exit_info.exit_info_1 == 0 { + self.write_edx_eax(self.vlapic.apic_base()); + } else { + self.vlapic.set_apic_base(self.read_edx_eax())?; + } + self.advance_rip(VM_EXIT_INSTR_LEN_MSR) + } + fn handle_apic_msr_access( &mut self, exit_info: &super::vmcb::SvmExitInfo, @@ -521,6 +615,15 @@ impl SvmVcpu { self.advance_rip(VM_EXIT_INSTR_LEN_MSR) } + fn handle_zero_msr_access(&mut self, exit_info: &super::vmcb::SvmExitInfo) -> AxResult { + const VM_EXIT_INSTR_LEN_MSR: u8 = 2; + + if exit_info.exit_info_1 == 0 { + self.write_edx_eax(0); + } + self.advance_rip(VM_EXIT_INSTR_LEN_MSR) + } + fn guest_visible_efer(&self) -> u64 { unsafe { self.vmcb.as_vmcb_ref().state.efer.get() & !EFER_SVME } } @@ -571,13 +674,29 @@ impl SvmVcpu { const FEATURE_VMX: u32 = 1 << 5; const FEATURE_HYPERVISOR: u32 = 1 << 31; const FEATURE_MCE: u32 = 1 << 7; + const FEATURE_X2APIC: u32 = 1 << 21; + const FEATURE_TSC_DEADLINE: u32 = 1 << 24; + const FEATURE_APIC: u32 = 1 << 9; + const MAX_LOGICAL_PROCESSORS_MASK: u32 = 0xff << 16; + const INITIAL_APIC_ID_MASK: u32 = 0xff << 24; let mut res = cpuid!(regs_clone.rax, regs_clone.rcx); // Do not expose nested hardware virtualization to the guest. res.ecx &= !FEATURE_VMX; + res.ecx |= FEATURE_X2APIC; + res.ecx &= !FEATURE_TSC_DEADLINE; res.ecx |= FEATURE_HYPERVISOR; res.edx &= !FEATURE_MCE; + res.edx |= FEATURE_APIC; + res.ebx &= !(MAX_LOGICAL_PROCESSORS_MASK | INITIAL_APIC_ID_MASK); + res.ebx |= 1 << 16; res } + 0xb | 0x1f => CpuIdResult { + eax: 0, + ebx: 0, + ecx: regs_clone.rcx as u32, + edx: 0, + }, LEAF_STRUCTURED_EXTENDED_FEATURE_FLAGS_ENUMERATION => { let mut res = cpuid!(regs_clone.rax, regs_clone.rcx); if regs_clone.rcx == 0 { @@ -642,6 +761,52 @@ impl SvmVcpu { self.advance_rip(VM_EXIT_INSTR_LEN_CPUID) } + fn handle_xsetbv(&mut self) -> AxResult { + const XCR_XCR0: u64 = 0; + const VM_EXIT_INSTR_LEN_XSETBV: u8 = 3; + + let index = self.guest_regs.rcx.get_bits(0..32); + let value = self.guest_regs.rdx.get_bits(0..32) << 32 | self.guest_regs.rax.get_bits(0..32); + + if index == XCR_XCR0 { + Xcr0::from_bits(value) + .and_then(|x| { + if !x.contains(Xcr0::XCR0_FPU_MMX_STATE) { + return None; + } + if x.contains(Xcr0::XCR0_AVX_STATE) && !x.contains(Xcr0::XCR0_SSE_STATE) { + return None; + } + if x.contains(Xcr0::XCR0_BNDCSR_STATE) ^ x.contains(Xcr0::XCR0_BNDREG_STATE) { + return None; + } + + let avx512_state = x.contains(Xcr0::XCR0_OPMASK_STATE) + || x.contains(Xcr0::XCR0_ZMM_HI256_STATE) + || x.contains(Xcr0::XCR0_HI16_ZMM_STATE); + let avx512_state_complete = x.contains(Xcr0::XCR0_OPMASK_STATE) + && x.contains(Xcr0::XCR0_ZMM_HI256_STATE) + && x.contains(Xcr0::XCR0_HI16_ZMM_STATE); + if avx512_state + && (!avx512_state_complete + || !x.contains(Xcr0::XCR0_AVX_STATE) + || !x.contains(Xcr0::XCR0_SSE_STATE)) + { + return None; + } + + Some(x) + }) + .ok_or_else(|| ax_err_type!(InvalidInput)) + .and_then(|x| { + self.xstate.guest_xcr0 = x.bits(); + self.advance_rip(VM_EXIT_INSTR_LEN_XSETBV) + }) + } else { + ax_err!(Unsupported, "only xcr0 is supported") + } + } + fn svm_io_exit_info( &self, exit_info: &super::vmcb::SvmExitInfo, @@ -667,6 +832,297 @@ impl SvmVcpu { self.regs_mut().rdx = val >> 32; } + fn allow_interrupt(&self) -> bool { + let vmcb = unsafe { self.vmcb.as_vmcb_ref() }; + vmcb.state.rflags.get() & x86_64::registers::rflags::RFlags::INTERRUPT_FLAG.bits() != 0 + && vmcb.control.int_state.get() == 0 + && !vmcb.control.event_inj.get().get_bit(31) + } + + fn inject_pending_events(&mut self) -> AxResult { + let Some(event) = self.pending_events.front().copied() else { + return Ok(()); + }; + if event.vector >= 32 && !self.allow_interrupt() { + return Ok(()); + } + + self.inject_event(event.vector, event.err_code)?; + if event.vector >= 32 { + self.vlapic + .accept_interrupt(event.vector, event.level_triggered); + } + self.pending_events.pop_front(); + Ok(()) + } + + fn clear_event_inj(&mut self) { + let vmcb = unsafe { self.vmcb.as_vmcb() }; + vmcb.control.event_inj.set(0); + vmcb.control.event_inj_err.set(0); + } + + fn inject_event(&mut self, vector: u8, err_code: Option) -> AxResult { + const SVM_EVENT_VALID: u32 = 1 << 31; + const SVM_EVENT_TYPE_EXCEPTION: u32 = 3 << 8; + const SVM_EVENT_ERROR_CODE_VALID: u32 = 1 << 11; + + let vmcb = unsafe { self.vmcb.as_vmcb() }; + let mut event = SVM_EVENT_VALID | vector as u32; + if vector < 32 { + event |= SVM_EVENT_TYPE_EXCEPTION; + } + if let Some(err_code) = err_code { + event |= SVM_EVENT_ERROR_CODE_VALID; + vmcb.control.event_inj_err.set(err_code); + } + vmcb.control.event_inj.set(event); + Ok(()) + } + + fn gla2gva(&self, guest_rip: GuestVirtAddr) -> GuestVirtAddr { + if self.get_cpu_mode() == VmCpuMode::Mode64 { + guest_rip + } else { + guest_rip + unsafe { self.vmcb.as_vmcb_ref().state.cs.base.get() as usize } + } + } + + fn decode_npt_mmio_access( + &mut self, + exit_info: &super::vmcb::SvmExitInfo, + addr: GuestPhysAddr, + write: bool, + ) -> AxResult> { + let addr_usize = addr.as_usize(); + let local_apic = + (X86_LOCAL_APIC_BASE..X86_LOCAL_APIC_BASE + X86_LOCAL_APIC_SIZE).contains(&addr_usize); + let ioapic = (X86_IOAPIC_BASE..X86_IOAPIC_BASE + X86_IOAPIC_SIZE).contains(&addr_usize); + if !local_apic && !ioapic { + return Ok(None); + } + + let start = self.gla2gva(GuestVirtAddr::from(exit_info.guest_rip as usize)); + let mut rip = start; + let mut rex = 0u8; + if let Err(err) = self.skip_simple_prefixes(&mut rip, &mut rex) { + debug!("failed to decode SVM NPF MMIO prefixes: {err:?}"); + return Ok(None); + } + + let opcode = self.read_guest_u8(rip)?; + rip += 1; + let modrm = self.read_guest_u8(rip)?; + rip += 1; + if modrm >> 6 == 0b11 { + debug!("SVM NPF MMIO access did not use a memory operand"); + return Ok(None); + } + + match (write, opcode) { + (true, 0x89) => { + let reg = ((modrm >> 3) & 0x7) | ((rex & 0x4) << 1); + let end = self.skip_modrm_memory_operand(rip, modrm, rex)?; + let data = self.guest_regs.get_reg_of_index(reg) as u32 as u64; + let exit = self.handle_decoded_npt_mmio_write(addr, data, local_apic)?; + Ok(Some((exit, (end.as_usize() - start.as_usize()) as u8))) + } + (true, 0xc7) if (modrm >> 3) & 0x7 == 0 => { + let imm_addr = self.skip_modrm_memory_operand(rip, modrm, rex)?; + let mut data = 0u32; + for i in 0..size_of::() { + data |= (self.read_guest_u8(imm_addr + i)? as u32) << (i * 8); + } + let exit = self.handle_decoded_npt_mmio_write(addr, data as u64, local_apic)?; + Ok(Some(( + exit, + (imm_addr.as_usize() + size_of::() - start.as_usize()) as u8, + ))) + } + (false, 0x8b) => { + let reg = (((modrm >> 3) & 0x7) | ((rex & 0x4) << 1)) as usize; + let end = self.skip_modrm_memory_operand(rip, modrm, rex)?; + let exit = if local_apic { + let val = + >>::handle_read( + &self.vlapic, + addr, + AccessWidth::Dword, + )?; + self.regs_mut() + .set_reg_of_index(reg as u8, val as u32 as u64); + AxVCpuExitReason::Nothing + } else { + AxVCpuExitReason::MmioRead { + addr, + width: AccessWidth::Dword, + reg, + reg_width: AccessWidth::Dword, + signed_ext: false, + } + }; + Ok(Some((exit, (end.as_usize() - start.as_usize()) as u8))) + } + _ => { + debug!("unsupported SVM NPF MMIO opcode {opcode:#x}, write={write}"); + Ok(None) + } + } + } + + fn handle_decoded_npt_mmio_write( + &mut self, + addr: GuestPhysAddr, + data: u64, + local_apic: bool, + ) -> AxResult { + if !local_apic { + return Ok(AxVCpuExitReason::MmioWrite { + addr, + width: AccessWidth::Dword, + data, + }); + } + + let offset = addr.as_usize() - X86_LOCAL_APIC_BASE; + if offset == X86_LOCAL_APIC_EOI_OFFSET { + return Ok(AxVCpuExitReason::InterruptEnd { + vector: self.vlapic.handle_eoi(), + }); + } + + >>::handle_write( + &self.vlapic, + addr, + AccessWidth::Dword, + data as usize, + )?; + Ok(AxVCpuExitReason::Nothing) + } + + fn skip_simple_prefixes(&self, rip: &mut GuestVirtAddr, rex: &mut u8) -> AxResult { + loop { + let byte = self.read_guest_u8(*rip)?; + if byte == 0x66 { + *rip += 1; + } else if (0x40..=0x4f).contains(&byte) { + *rex = byte; + *rip += 1; + } else { + return Ok(()); + } + } + } + + fn skip_modrm_memory_operand( + &self, + mut cursor: GuestVirtAddr, + modrm: u8, + rex: u8, + ) -> AxResult { + let mode = modrm >> 6; + let rm = modrm & 0x7; + + if rm == 0b100 { + let sib = self.read_guest_u8(cursor)?; + cursor += 1; + let base = sib & 0x7; + if mode == 0 && base == 0b101 { + cursor += size_of::(); + } + } else if mode == 0 && rm == 0b101 && rex & 0x1 == 0 { + cursor += size_of::(); + } + + match mode { + 0 => {} + 1 => cursor += size_of::(), + 2 => cursor += size_of::(), + _ => return ax_err!(InvalidInput, "ModRM register operand is not memory"), + } + + Ok(cursor) + } + + fn read_guest_u8(&self, gva: GuestVirtAddr) -> AxResult { + let gpa = self.translate_guest_linear(gva)?; + let hva = memory::phys_to_virt(PhysAddr::from(gpa.as_usize())); + Ok(unsafe { core::ptr::read_volatile(hva.as_ptr()) }) + } + + fn translate_guest_linear(&self, gva: GuestVirtAddr) -> AxResult { + let addr = gva.as_usize(); + match self.get_paging_level() { + 0 => Ok(GuestPhysAddr::from(addr)), + 4 => self.walk_guest_page_table_4level(addr), + level => ax_err!( + Unsupported, + format_args!("unsupported SVM MMIO decode paging level {level}") + ), + } + } + + fn get_paging_level(&self) -> usize { + let vmcb = unsafe { self.vmcb.as_vmcb_ref() }; + let mut level = 0; + let cr0 = vmcb.state.cr0.get(); + let cr4 = vmcb.state.cr4.get(); + let efer = vmcb.state.efer.get(); + if cr0 & Cr0Flags::PAGING.bits() != 0 { + if cr4 & Cr4Flags::PHYSICAL_ADDRESS_EXTENSION.bits() != 0 { + if efer & EferFlags::LONG_MODE_ACTIVE.bits() != 0 { + level = 4; + } else { + level = 3; + } + } else { + level = 2; + } + } + level + } + + fn walk_guest_page_table_4level(&self, gva: usize) -> AxResult { + const PRESENT: u64 = 1 << 0; + const HUGE_PAGE: u64 = 1 << 7; + const ADDR_MASK: u64 = 0x000f_ffff_ffff_f000; + const PAGE_4K_MASK: usize = 0xfff; + const PAGE_2M_MASK: usize = 0x1f_ffff; + const PAGE_1G_MASK: usize = 0x3fff_ffff; + + let mut table = unsafe { self.vmcb.as_vmcb_ref().state.cr3.get() } & ADDR_MASK; + let indexes = [ + (gva >> 39) & 0x1ff, + (gva >> 30) & 0x1ff, + (gva >> 21) & 0x1ff, + (gva >> 12) & 0x1ff, + ]; + + for (level, index) in indexes.into_iter().enumerate() { + let entry = read_guest_phys_u64(table as usize + index * size_of::()); + if entry & PRESENT == 0 { + return ax_err!( + InvalidInput, + format_args!("guest RIP page table entry is not present at level {level}") + ); + } + + let paddr = (entry & ADDR_MASK) as usize; + match level { + 1 if entry & HUGE_PAGE != 0 => { + return Ok(GuestPhysAddr::from(paddr + (gva & PAGE_1G_MASK))); + } + 2 if entry & HUGE_PAGE != 0 => { + return Ok(GuestPhysAddr::from(paddr + (gva & PAGE_2M_MASK))); + } + 3 => return Ok(GuestPhysAddr::from(paddr + (gva & PAGE_4K_MASK))), + _ => table = paddr as u64, + } + } + + ax_err!(InvalidInput, "failed to translate guest RIP") + } + fn before_vmrun(&mut self) { let rax = self.regs().rax; unsafe { @@ -762,14 +1218,14 @@ impl AxArchVCpu for SvmVcpu { Ok(()) } - fn setup(&mut self, _config: Self::SetupConfig) -> AxResult { + fn setup(&mut self, config: Self::SetupConfig) -> AxResult { let entry = self .entry .ok_or_else(|| ax_err_type!(InvalidInput, "SVM guest entry is not set"))?; let npt_root = self .npt_root .ok_or_else(|| ax_err_type!(InvalidInput, "SVM NPT root is not set"))?; - self.setup_vmcb(entry, npt_root) + self.setup_vmcb(entry, npt_root, config) } fn run(&mut self) -> AxResult { @@ -822,12 +1278,6 @@ impl AxArchVCpu for SvmVcpu { && self.regs().rax == QEMU_EXIT_MAGIC { AxVCpuExitReason::SystemDown - } else if port == Port(QEMU_RESET_PORT) { - warn!( - "SVM guest wrote QEMU reset port {port:#x} with data {:#x}", - self.regs().rax.get_bits(width.bits_range()) - ); - AxVCpuExitReason::SystemDown } else { AxVCpuExitReason::IoWrite { port, @@ -852,14 +1302,24 @@ impl AxArchVCpu for SvmVcpu { } SvmExitCode::NPF => { let info = self.nested_page_fault_info()?; - AxVCpuExitReason::NestedPageFault { - addr: info.fault_guest_paddr, - access_flags: info.access_flags, + let write = info.access_flags.contains(MappingFlags::WRITE); + let read = info.access_flags.contains(MappingFlags::READ); + if (read || write) + && let Some((mmio_exit, instr_len)) = + self.decode_npt_mmio_access(&exit_info, info.fault_guest_paddr, write)? + { + self.advance_rip(instr_len)?; + mmio_exit + } else { + AxVCpuExitReason::NestedPageFault { + addr: info.fault_guest_paddr, + access_flags: info.access_flags, + } } } - // SVM INTR exits do not provide a usable vector here; return to - // the scheduler and let normal host interrupt handling proceed. - SvmExitCode::INTR => AxVCpuExitReason::Nothing, + // SVM has no VMX-style preemption timer. Use host interrupt and + // guest cpu-relax exits as periodic VMM poll points for PIT/serial injection. + SvmExitCode::INTR | SvmExitCode::PAUSE => AxVCpuExitReason::VTimer, SvmExitCode::HLT => AxVCpuExitReason::Halt, SvmExitCode::SHUTDOWN => AxVCpuExitReason::SystemDown, _ => { @@ -883,14 +1343,34 @@ impl AxArchVCpu for SvmVcpu { self.regs_mut().set_reg_of_index(reg as u8, val as u64); } - fn inject_interrupt(&mut self, _vector: usize) -> AxResult { - ax_err!( - Unsupported, - "AMD SVM interrupt injection is not implemented yet" - ) + fn inject_interrupt(&mut self, vector: usize) -> AxResult { + if vector == 0 { + warn!("interrupt queued in inject_interrupt: vector 0"); + panic!() + } + self.queue_event(vector as u8, None); + Ok(()) + } + + fn inject_interrupt_with_trigger(&mut self, vector: usize, level_triggered: bool) -> AxResult { + if vector == 0 { + warn!("interrupt queued in inject_interrupt_with_trigger: vector 0"); + panic!() + } + self.queue_event_with_trigger(vector as u8, None, level_triggered); + Ok(()) + } + + fn handle_eoi(&mut self) -> Option { + self.vlapic.handle_eoi() } fn set_return_value(&mut self, val: usize) { self.regs_mut().rax = val as u64; } } + +fn read_guest_phys_u64(gpa: usize) -> u64 { + let hva = memory::phys_to_virt(PhysAddr::from(gpa)); + unsafe { core::ptr::read_unaligned(hva.as_ptr() as *const u64) } +} From 77407ba2817b0b29aa5e9b8c38b93fa25b85b252 Mon Sep 17 00:00:00 2001 From: Josen Date: Mon, 25 May 2026 17:11:23 +0800 Subject: [PATCH 2/6] feat(svm): implement event reinjection and enhance PIT channel handling for Linux guest support --- virtualization/x86_vcpu/src/svm/vcpu.rs | 50 +++++++--- virtualization/x86_vlapic/src/pit.rs | 122 ++++++++++++++++++------ 2 files changed, 132 insertions(+), 40 deletions(-) diff --git a/virtualization/x86_vcpu/src/svm/vcpu.rs b/virtualization/x86_vcpu/src/svm/vcpu.rs index 0e2cf2437d..f7c7ce63f9 100644 --- a/virtualization/x86_vcpu/src/svm/vcpu.rs +++ b/virtualization/x86_vcpu/src/svm/vcpu.rs @@ -25,6 +25,7 @@ use x86_vlapic::EmulatedLocalApic; use super::{ definitions::{SvmExitCode, SvmIntercept}, + flags::{InterruptType, VmcbIntInfo}, structs::{IOPm, MSRPm, VmcbFrame}, vmcb::{InterceptCrRw, InterceptExceptions, NestedCtl, VmcbTlbControl, set_vmcb_segment}, }; @@ -499,6 +500,7 @@ impl SvmVcpu { self.launched = true; self.clear_event_inj(); + self.reinject_interrupted_event(); let exit_info = self.exit_info()?; @@ -860,23 +862,43 @@ impl SvmVcpu { let vmcb = unsafe { self.vmcb.as_vmcb() }; vmcb.control.event_inj.set(0); vmcb.control.event_inj_err.set(0); + vmcb.control.clean_bits.set(0); } - fn inject_event(&mut self, vector: u8, err_code: Option) -> AxResult { - const SVM_EVENT_VALID: u32 = 1 << 31; - const SVM_EVENT_TYPE_EXCEPTION: u32 = 3 << 8; - const SVM_EVENT_ERROR_CODE_VALID: u32 = 1 << 11; - + fn reinject_interrupted_event(&mut self) { let vmcb = unsafe { self.vmcb.as_vmcb() }; - let mut event = SVM_EVENT_VALID | vector as u32; - if vector < 32 { - event |= SVM_EVENT_TYPE_EXCEPTION; + let exit_int_info = vmcb.control.exit_int_info.get(); + if !exit_int_info.get_bit(31) { + return; } + + vmcb.control.event_inj.set(exit_int_info); + vmcb.control + .event_inj_err + .set(if exit_int_info.get_bit(11) { + vmcb.control.exit_int_info_err.get() + } else { + 0 + }); + vmcb.control.clean_bits.set(0); + } + + fn inject_event(&mut self, vector: u8, err_code: Option) -> AxResult { + let vmcb = unsafe { self.vmcb.as_vmcb() }; + let int_type = if vector < 32 { + InterruptType::Exception + } else { + InterruptType::External + }; + let mut event = VmcbIntInfo::from(int_type, vector).bits(); if let Some(err_code) = err_code { - event |= SVM_EVENT_ERROR_CODE_VALID; + event |= VmcbIntInfo::ERROR_CODE.bits(); vmcb.control.event_inj_err.set(err_code); + } else { + vmcb.control.event_inj_err.set(0); } vmcb.control.event_inj.set(event); + vmcb.control.clean_bits.set(0); Ok(()) } @@ -1317,10 +1339,12 @@ impl AxArchVCpu for SvmVcpu { } } } - // SVM has no VMX-style preemption timer. Use host interrupt and - // guest cpu-relax exits as periodic VMM poll points for PIT/serial injection. - SvmExitCode::INTR | SvmExitCode::PAUSE => AxVCpuExitReason::VTimer, - SvmExitCode::HLT => AxVCpuExitReason::Halt, + // SVM has no VMX-style preemption timer. Use host interrupt, + // guest cpu-relax, and idle exits as periodic VMM poll points + // for PIT/serial injection. + SvmExitCode::INTR | SvmExitCode::PAUSE | SvmExitCode::HLT => { + AxVCpuExitReason::VTimer + } SvmExitCode::SHUTDOWN => AxVCpuExitReason::SystemDown, _ => { warn!("SVM unsupported VM-exit: {exit_info:#x?}"); diff --git a/virtualization/x86_vlapic/src/pit.rs b/virtualization/x86_vlapic/src/pit.rs index e30eacf529..17fb11167b 100644 --- a/virtualization/x86_vlapic/src/pit.rs +++ b/virtualization/x86_vlapic/src/pit.rs @@ -13,7 +13,6 @@ const PIT_PORT_END: u16 = PIT_SPEAKER_CONTROL; const PIT_BASE_FREQUENCY_HZ: u64 = 1_193_182; const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000; const MIN_PERIOD_NS: u64 = 1_000; - #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum AccessMode { LatchCount, @@ -22,6 +21,21 @@ enum AccessMode { LowThenHigh, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PitMode { + InterruptOnTerminalCount, + Other(u8), +} + +impl PitMode { + fn from_command(command: u8) -> Self { + match (command >> 1) & 0b111 { + 0 => Self::InterruptOnTerminalCount, + mode => Self::Other(mode), + } + } +} + impl AccessMode { fn from_command(command: u8) -> Self { match (command >> 4) & 0b11 { @@ -36,9 +50,12 @@ impl AccessMode { #[derive(Clone, Copy, Debug)] struct PitChannel { access_mode: AccessMode, + mode: PitMode, reload_value: u16, write_low_latched: Option, read_high_next: bool, + latched_count: Option, + start_ns: u64, period_ns: Option, next_deadline_ns: u64, } @@ -47,26 +64,35 @@ impl PitChannel { const fn new() -> Self { Self { access_mode: AccessMode::LowThenHigh, + mode: PitMode::Other(3), reload_value: 0, write_low_latched: None, read_high_next: false, + latched_count: None, + start_ns: 0, period_ns: None, next_deadline_ns: 0, } } - fn program_reload(&mut self, reload_value: u16, now_ns: u64) { - self.reload_value = reload_value; - let divisor = if reload_value == 0 { + fn divisor(&self) -> u64 { + if self.reload_value == 0 { 0x1_0000 } else { - reload_value as u64 - }; + self.reload_value as u64 + } + } + + fn program_reload(&mut self, reload_value: u16, now_ns: u64) { + self.reload_value = reload_value; + let divisor = self.divisor(); let period_ns = ((divisor * NANOSECONDS_PER_SECOND) / PIT_BASE_FREQUENCY_HZ).max(MIN_PERIOD_NS); + self.start_ns = now_ns; self.period_ns = Some(period_ns); self.next_deadline_ns = now_ns.saturating_add(period_ns); self.read_high_next = false; + self.latched_count = None; trace!("x86 PIT channel 0 programmed: reload={reload_value:#x}, period_ns={period_ns}"); } @@ -85,20 +111,57 @@ impl PitChannel { } } - fn read_count(&mut self) -> u8 { - let value = self.reload_value; + fn current_count(&self, now_ns: u64) -> u16 { + let Some(_) = self.period_ns else { + return self.reload_value; + }; + let divisor = self.divisor(); + let elapsed_ns = now_ns.saturating_sub(self.start_ns); + let elapsed_ticks = + elapsed_ns.saturating_mul(PIT_BASE_FREQUENCY_HZ) / NANOSECONDS_PER_SECOND; + + if self.mode == PitMode::InterruptOnTerminalCount && elapsed_ticks >= divisor { + return 0; + } + + let remaining = divisor - (elapsed_ticks % divisor); + if remaining == 0x1_0000 { + 0 + } else { + remaining as u16 + } + } + + fn latch_count(&mut self, now_ns: u64) { + if self.latched_count.is_none() { + self.latched_count = Some(self.current_count(now_ns)); + self.read_high_next = false; + } + } + + fn read_count(&mut self, now_ns: u64) -> u8 { + let value = self + .latched_count + .unwrap_or_else(|| self.current_count(now_ns)); match self.access_mode { - AccessMode::HighByte => (value >> 8) as u8, + AccessMode::HighByte => { + self.latched_count = None; + (value >> 8) as u8 + } AccessMode::LowThenHigh => { if self.read_high_next { self.read_high_next = false; + self.latched_count = None; (value >> 8) as u8 } else { self.read_high_next = true; value as u8 } } - AccessMode::LatchCount | AccessMode::LowByte => value as u8, + AccessMode::LatchCount | AccessMode::LowByte => { + self.latched_count = None; + value as u8 + } } } } @@ -155,26 +218,30 @@ impl EmulatedPit { true } - fn write_command(state: &mut PitState, command: u8) { + fn write_command(state: &mut PitState, command: u8, now_ns: u64) { let channel = (command >> 6) & 0b11; let access_mode = AccessMode::from_command(command); + let mode = PitMode::from_command(command); + let pit_channel = match channel { + 0 => Some(&mut state.channel0), + 2 => Some(&mut state.channel2), + _ => None, + }; + let Some(pit_channel) = pit_channel else { + debug!("x86 PIT command for unsupported channel {channel}: {command:#x}"); + return; + }; + if access_mode == AccessMode::LatchCount { + pit_channel.latch_count(now_ns); return; } - match channel { - 0 => { - state.channel0.access_mode = access_mode; - state.channel0.write_low_latched = None; - state.channel0.read_high_next = false; - } - 2 => { - state.channel2.access_mode = access_mode; - state.channel2.write_low_latched = None; - state.channel2.read_high_next = false; - } - _ => debug!("x86 PIT command for unsupported channel {channel}: {command:#x}"), - } + pit_channel.access_mode = access_mode; + pit_channel.mode = mode; + pit_channel.write_low_latched = None; + pit_channel.read_high_next = false; + pit_channel.latched_count = None; } } @@ -198,10 +265,11 @@ impl BaseDeviceOps for EmulatedPit { return ax_err!(Unsupported, "x86 PIT only supports byte port reads"); } + let now_ns = time::current_time_nanos() as u64; let mut state = self.state.lock(); let value = match port.0 { - PIT_CHANNEL0 => state.channel0.read_count(), - PIT_CHANNEL2 => state.channel2.read_count(), + PIT_CHANNEL0 => state.channel0.read_count(now_ns), + PIT_CHANNEL2 => state.channel2.read_count(now_ns), PIT_COMMAND => 0, PIT_SPEAKER_CONTROL => state.speaker_control, _ => return ax_err!(Unsupported, "unsupported x86 PIT read port"), @@ -219,7 +287,7 @@ impl BaseDeviceOps for EmulatedPit { match port.0 { PIT_CHANNEL0 => state.channel0.write_count(val as u8, now_ns), PIT_CHANNEL2 => state.channel2.write_count(val as u8, now_ns), - PIT_COMMAND => Self::write_command(&mut state, val as u8), + PIT_COMMAND => Self::write_command(&mut state, val as u8, now_ns), PIT_SPEAKER_CONTROL => state.speaker_control = val as u8, _ => return ax_err!(Unsupported, "unsupported x86 PIT write port"), } From 9dc25a75f9810311c252448a9c0d4419386ad5fd Mon Sep 17 00:00:00 2001 From: Josen Date: Tue, 26 May 2026 14:10:59 +0800 Subject: [PATCH 3/6] feat(pit): enhance PIT channel modes and add status latching for improved IRQ handling --- .../vms/linux-x86_64-qemu-smp1-svm.toml | 2 + os/axvisor/src/vmm/devices/x86.rs | 1 - os/axvisor/src/vmm/vcpus.rs | 1 + virtualization/x86_vcpu/src/svm/vcpu.rs | 112 ++++++++++--- virtualization/x86_vlapic/src/pit.rs | 150 +++++++++++++++--- 5 files changed, 226 insertions(+), 40 deletions(-) diff --git a/os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml b/os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml index 7f925824ed..47f56820fe 100644 --- a/os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml +++ b/os/axvisor/configs/vms/linux-x86_64-qemu-smp1-svm.toml @@ -73,6 +73,8 @@ passthrough_devices = [ passthrough_addresses = [ # QEMU q35 low MMIO window needed by PCI/virtio-blk probing. [0xfe00_0000, 0x00c0_0000], + # Linux probes a q35/ICH system MMIO page at 0xfed8_03c0 after the PIT/HLT path is fixed. + [0xfed8_0000, 0x1000], ] # Devices that are not desired to be passed through to the guest diff --git a/os/axvisor/src/vmm/devices/x86.rs b/os/axvisor/src/vmm/devices/x86.rs index 26ed393e21..da3304a2f3 100644 --- a/os/axvisor/src/vmm/devices/x86.rs +++ b/os/axvisor/src/vmm/devices/x86.rs @@ -13,7 +13,6 @@ const IOAPIC_VECTOR_END: usize = IOAPIC_VECTOR_BASE + IOAPIC_GSI_COUNT; const PIT_TIMER_GSI: usize = 0; const COM1_GSI: usize = 4; - static IOAPIC_IRQ_FORWARDING_ENABLED: AtomicBool = AtomicBool::new(false); static IOAPIC_IRQ_HOOK_REGISTERED: AtomicBool = AtomicBool::new(false); static IOAPIC_IRQ_FORWARD_VM_ID: AtomicUsize = AtomicUsize::new(usize::MAX); diff --git a/os/axvisor/src/vmm/vcpus.rs b/os/axvisor/src/vmm/vcpus.rs index ffc147ca34..c0506ebf62 100644 --- a/os/axvisor/src/vmm/vcpus.rs +++ b/os/axvisor/src/vmm/vcpus.rs @@ -467,6 +467,7 @@ fn vcpu_run() { } } AxVCpuExitReason::PreemptionTimer => { + super::timer::check_events(); #[cfg(target_arch = "x86_64")] super::devices::x86::inject_due_pit_irq0(&vm, &vcpu); #[cfg(target_arch = "x86_64")] diff --git a/virtualization/x86_vcpu/src/svm/vcpu.rs b/virtualization/x86_vcpu/src/svm/vcpu.rs index f7c7ce63f9..6f2dd4c0c8 100644 --- a/virtualization/x86_vcpu/src/svm/vcpu.rs +++ b/virtualization/x86_vcpu/src/svm/vcpu.rs @@ -60,6 +60,12 @@ const X2APIC_MSR_BASE: u32 = 0x800; // Match the current VMX/vLAPIC path, which handles x2APIC register offsets 0x00..=0x3f. const X2APIC_MSR_END: u32 = 0x83f; +const SVM_INT_CTL_V_IRQ: u32 = 1 << 8; +const SVM_INT_CTL_V_INTR_PRIO_SHIFT: u32 = 16; +const SVM_INT_CTL_V_INTR_PRIO_MASK: u32 = 0xf << SVM_INT_CTL_V_INTR_PRIO_SHIFT; +const SVM_INT_CTL_V_IGN_TPR: u32 = 1 << 20; +const SVM_INT_CTL_V_INTR_MASKING: u32 = 1 << 24; + macro_rules! save_regs_no_rax { () => { " @@ -278,6 +284,7 @@ impl SvmVcpu { for intercept in [ SvmIntercept::INTR, SvmIntercept::NMI, + SvmIntercept::RDTSC, SvmIntercept::CPUID, SvmIntercept::PAUSE, SvmIntercept::HLT, @@ -450,13 +457,27 @@ impl SvmVcpu { Ok(()) } + fn virtual_interrupt_vector(&self) -> Option { + let vmcb = unsafe { self.vmcb.as_vmcb_ref() }; + if vmcb.control.int_control.get() & SVM_INT_CTL_V_IRQ != 0 { + Some(vmcb.control.int_vector.get() as u8) + } else { + None + } + } + + fn has_pending_external_event(&self, vector: u8) -> bool { + vector >= 32 + && (self.virtual_interrupt_vector() == Some(vector) + || self + .pending_events + .iter() + .any(|event| event.vector == vector)) + } + /// Add a virtual interrupt or exception to the pending events list. pub fn queue_event(&mut self, vector: u8, err_code: Option) { - self.pending_events.push_back(PendingEvent { - vector, - err_code, - level_triggered: false, - }); + self.queue_event_with_trigger(vector, err_code, false); } /// Add a virtual interrupt or exception with trigger mode metadata. @@ -466,6 +487,10 @@ impl SvmVcpu { err_code: Option, level_triggered: bool, ) { + if self.has_pending_external_event(vector) { + return; + } + self.pending_events.push_back(PendingEvent { vector, err_code, @@ -501,6 +526,7 @@ impl SvmVcpu { self.clear_event_inj(); self.reinject_interrupted_event(); + self.sync_virtual_interrupt_delivery(); let exit_info = self.exit_info()?; @@ -834,26 +860,56 @@ impl SvmVcpu { self.regs_mut().rdx = val >> 32; } - fn allow_interrupt(&self) -> bool { - let vmcb = unsafe { self.vmcb.as_vmcb_ref() }; - vmcb.state.rflags.get() & x86_64::registers::rflags::RFlags::INTERRUPT_FLAG.bits() != 0 - && vmcb.control.int_state.get() == 0 - && !vmcb.control.event_inj.get().get_bit(31) + fn handle_rdtsc(&mut self) -> AxResult { + const VM_EXIT_INSTR_LEN_RDTSC: u8 = 2; + + let tsc = unsafe { core::arch::x86_64::_rdtsc() }; + let tsc_offset = unsafe { self.vmcb.as_vmcb_ref().control.tsc_offset.get() }; + self.write_edx_eax(tsc.wrapping_add(tsc_offset)); + self.advance_rip(VM_EXIT_INSTR_LEN_RDTSC) + } + + fn arm_virtual_interrupt(&mut self, event: PendingEvent) { + let vmcb = unsafe { self.vmcb.as_vmcb() }; + let priority = ((event.vector >> 4) as u32) << SVM_INT_CTL_V_INTR_PRIO_SHIFT; + let int_control = (vmcb.control.int_control.get() & !SVM_INT_CTL_V_INTR_PRIO_MASK) + | SVM_INT_CTL_V_IRQ + | SVM_INT_CTL_V_IGN_TPR + | SVM_INT_CTL_V_INTR_MASKING + | priority; + vmcb.control.int_vector.set(event.vector as u32); + vmcb.control.int_control.set(int_control); + vmcb.control.clean_bits.set(0); + } + + fn sync_virtual_interrupt_delivery(&mut self) { + let Some(event) = self.pending_events.front().copied() else { + return; + }; + if event.vector < 32 || self.virtual_interrupt_vector().is_some() { + return; + } + + self.vlapic + .accept_interrupt(event.vector, event.level_triggered); + self.pending_events.pop_front(); } fn inject_pending_events(&mut self) -> AxResult { let Some(event) = self.pending_events.front().copied() else { return Ok(()); }; - if event.vector >= 32 && !self.allow_interrupt() { + + if event.vector >= 32 { + if self.virtual_interrupt_vector() == Some(event.vector) { + return Ok(()); + } + + self.arm_virtual_interrupt(event); return Ok(()); } self.inject_event(event.vector, event.err_code)?; - if event.vector >= 32 { - self.vlapic - .accept_interrupt(event.vector, event.level_triggered); - } self.pending_events.pop_front(); Ok(()) } @@ -1283,6 +1339,10 @@ impl AxArchVCpu for SvmVcpu { ], } } + SvmExitCode::RDTSC => { + self.handle_rdtsc()?; + AxVCpuExitReason::PreemptionTimer + } SvmExitCode::IOIO => { let (is_in, is_string, is_repeat, width, port) = self.svm_io_exit_info(&exit_info)?; @@ -1342,8 +1402,14 @@ impl AxArchVCpu for SvmVcpu { // SVM has no VMX-style preemption timer. Use host interrupt, // guest cpu-relax, and idle exits as periodic VMM poll points // for PIT/serial injection. - SvmExitCode::INTR | SvmExitCode::PAUSE | SvmExitCode::HLT => { - AxVCpuExitReason::VTimer + SvmExitCode::INTR => AxVCpuExitReason::PreemptionTimer, + SvmExitCode::HLT => { + self.advance_rip(1)?; + AxVCpuExitReason::PreemptionTimer + } + SvmExitCode::PAUSE => { + self.advance_rip(2)?; + AxVCpuExitReason::PreemptionTimer } SvmExitCode::SHUTDOWN => AxVCpuExitReason::SystemDown, _ => { @@ -1376,12 +1442,20 @@ impl AxArchVCpu for SvmVcpu { Ok(()) } - fn inject_interrupt_with_trigger(&mut self, vector: usize, level_triggered: bool) -> AxResult { + fn inject_interrupt_with_trigger( + &mut self, + vector: usize, + trigger: axvcpu::InterruptTriggerMode, + ) -> AxResult { if vector == 0 { warn!("interrupt queued in inject_interrupt_with_trigger: vector 0"); panic!() } - self.queue_event_with_trigger(vector as u8, None, level_triggered); + self.queue_event_with_trigger( + vector as u8, + None, + trigger == axvcpu::InterruptTriggerMode::LevelTriggered, + ); Ok(()) } diff --git a/virtualization/x86_vlapic/src/pit.rs b/virtualization/x86_vlapic/src/pit.rs index 17fb11167b..11707a6a89 100644 --- a/virtualization/x86_vlapic/src/pit.rs +++ b/virtualization/x86_vlapic/src/pit.rs @@ -24,16 +24,39 @@ enum AccessMode { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum PitMode { InterruptOnTerminalCount, - Other(u8), + HardwareRetriggerableOneShot, + RateGenerator, + SquareWaveGenerator, + SoftwareTriggeredStrobe, + HardwareTriggeredStrobe, } impl PitMode { fn from_command(command: u8) -> Self { match (command >> 1) & 0b111 { 0 => Self::InterruptOnTerminalCount, - mode => Self::Other(mode), + 1 => Self::HardwareRetriggerableOneShot, + 2 | 6 => Self::RateGenerator, + 3 | 7 => Self::SquareWaveGenerator, + 4 => Self::SoftwareTriggeredStrobe, + _ => Self::HardwareTriggeredStrobe, } } + + const fn raw_bits(self) -> u8 { + match self { + Self::InterruptOnTerminalCount => 0, + Self::HardwareRetriggerableOneShot => 1, + Self::RateGenerator => 2, + Self::SquareWaveGenerator => 3, + Self::SoftwareTriggeredStrobe => 4, + Self::HardwareTriggeredStrobe => 5, + } + } + + const fn is_periodic_irq(self) -> bool { + matches!(self, Self::RateGenerator | Self::SquareWaveGenerator) + } } impl AccessMode { @@ -55,23 +78,29 @@ struct PitChannel { write_low_latched: Option, read_high_next: bool, latched_count: Option, + latched_status: Option, + null_count: bool, start_ns: u64, period_ns: Option, next_deadline_ns: u64, + irq_fired: bool, } impl PitChannel { const fn new() -> Self { Self { access_mode: AccessMode::LowThenHigh, - mode: PitMode::Other(3), + mode: PitMode::SquareWaveGenerator, reload_value: 0, write_low_latched: None, read_high_next: false, latched_count: None, + latched_status: None, + null_count: true, start_ns: 0, period_ns: None, next_deadline_ns: 0, + irq_fired: false, } } @@ -93,7 +122,9 @@ impl PitChannel { self.next_deadline_ns = now_ns.saturating_add(period_ns); self.read_high_next = false; self.latched_count = None; - trace!("x86 PIT channel 0 programmed: reload={reload_value:#x}, period_ns={period_ns}"); + self.latched_status = None; + self.null_count = false; + self.irq_fired = false; } fn write_count(&mut self, value: u8, now_ns: u64) { @@ -111,16 +142,19 @@ impl PitChannel { } } + fn elapsed_ticks(&self, now_ns: u64) -> u64 { + let elapsed_ns = now_ns.saturating_sub(self.start_ns); + elapsed_ns.saturating_mul(PIT_BASE_FREQUENCY_HZ) / NANOSECONDS_PER_SECOND + } + fn current_count(&self, now_ns: u64) -> u16 { let Some(_) = self.period_ns else { return self.reload_value; }; let divisor = self.divisor(); - let elapsed_ns = now_ns.saturating_sub(self.start_ns); - let elapsed_ticks = - elapsed_ns.saturating_mul(PIT_BASE_FREQUENCY_HZ) / NANOSECONDS_PER_SECOND; + let elapsed_ticks = self.elapsed_ticks(now_ns); - if self.mode == PitMode::InterruptOnTerminalCount && elapsed_ticks >= divisor { + if !self.mode.is_periodic_irq() && elapsed_ticks >= divisor { return 0; } @@ -132,6 +166,35 @@ impl PitChannel { } } + fn output_high(&self, now_ns: u64) -> bool { + let Some(_) = self.period_ns else { + return true; + }; + let divisor = self.divisor(); + let elapsed_ticks = self.elapsed_ticks(now_ns); + match self.mode { + PitMode::InterruptOnTerminalCount | PitMode::SoftwareTriggeredStrobe => { + elapsed_ticks >= divisor + } + PitMode::RateGenerator => elapsed_ticks % divisor != divisor.saturating_sub(1), + PitMode::SquareWaveGenerator => (elapsed_ticks % divisor) < divisor.div_ceil(2), + PitMode::HardwareRetriggerableOneShot | PitMode::HardwareTriggeredStrobe => true, + } + } + + fn latch_status(&mut self, now_ns: u64) { + let mut status = (self.output_high(now_ns) as u8) << 7; + status |= (self.null_count as u8) << 6; + status |= match self.access_mode { + AccessMode::LatchCount => 0, + AccessMode::LowByte => 1, + AccessMode::HighByte => 2, + AccessMode::LowThenHigh => 3, + } << 4; + status |= self.mode.raw_bits() << 1; + self.latched_status = Some(status); + } + fn latch_count(&mut self, now_ns: u64) { if self.latched_count.is_none() { self.latched_count = Some(self.current_count(now_ns)); @@ -140,6 +203,10 @@ impl PitChannel { } fn read_count(&mut self, now_ns: u64) -> u8 { + if let Some(status) = self.latched_status.take() { + return status; + } + let value = self .latched_count .unwrap_or_else(|| self.current_count(now_ns)); @@ -210,24 +277,39 @@ impl EmulatedPit { return false; } - let elapsed = now_ns.saturating_sub(channel.next_deadline_ns); - let missed_periods = elapsed / period_ns; - channel.next_deadline_ns = channel - .next_deadline_ns - .saturating_add((missed_periods + 1).saturating_mul(period_ns)); + if channel.mode.is_periodic_irq() { + let elapsed = now_ns.saturating_sub(channel.next_deadline_ns); + let missed_periods = elapsed / period_ns; + channel.next_deadline_ns = channel + .next_deadline_ns + .saturating_add((missed_periods + 1).saturating_mul(period_ns)); + } else { + if channel.irq_fired { + return false; + } + channel.irq_fired = true; + } true } + fn channel_mut(state: &mut PitState, channel: u8) -> Option<&mut PitChannel> { + match channel { + 0 => Some(&mut state.channel0), + 2 => Some(&mut state.channel2), + _ => None, + } + } + fn write_command(state: &mut PitState, command: u8, now_ns: u64) { let channel = (command >> 6) & 0b11; + if channel == 0b11 { + Self::write_read_back_command(state, command, now_ns); + return; + } + let access_mode = AccessMode::from_command(command); let mode = PitMode::from_command(command); - let pit_channel = match channel { - 0 => Some(&mut state.channel0), - 2 => Some(&mut state.channel2), - _ => None, - }; - let Some(pit_channel) = pit_channel else { + let Some(pit_channel) = Self::channel_mut(state, channel) else { debug!("x86 PIT command for unsupported channel {channel}: {command:#x}"); return; }; @@ -242,6 +324,31 @@ impl EmulatedPit { pit_channel.write_low_latched = None; pit_channel.read_high_next = false; pit_channel.latched_count = None; + pit_channel.latched_status = None; + pit_channel.null_count = true; + } + + fn write_read_back_command(state: &mut PitState, command: u8, now_ns: u64) { + let latch_count = command & (1 << 5) == 0; + let latch_status = command & (1 << 4) == 0; + let selected = command & 0b1110; + + if selected & (1 << 1) != 0 { + if latch_count { + state.channel0.latch_count(now_ns); + } + if latch_status { + state.channel0.latch_status(now_ns); + } + } + if selected & (1 << 3) != 0 { + if latch_count { + state.channel2.latch_count(now_ns); + } + if latch_status { + state.channel2.latch_status(now_ns); + } + } } } @@ -271,7 +378,10 @@ impl BaseDeviceOps for EmulatedPit { PIT_CHANNEL0 => state.channel0.read_count(now_ns), PIT_CHANNEL2 => state.channel2.read_count(now_ns), PIT_COMMAND => 0, - PIT_SPEAKER_CONTROL => state.speaker_control, + PIT_SPEAKER_CONTROL => { + let output = state.channel2.output_high(now_ns) as u8; + (state.speaker_control & !0x20) | (output << 5) + } _ => return ax_err!(Unsupported, "unsupported x86 PIT read port"), }; Ok(value as usize) From 82b71a6fba3c529904a2e05a6e4002dc24406910 Mon Sep 17 00:00:00 2001 From: Josen Date: Tue, 26 May 2026 15:21:47 +0800 Subject: [PATCH 4/6] feat(svm): enhance SVM Linux guest model by adding optional CR4 features and adjusting CPUID exposure --- virtualization/x86_vcpu/src/svm/vcpu.rs | 49 +++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/virtualization/x86_vcpu/src/svm/vcpu.rs b/virtualization/x86_vcpu/src/svm/vcpu.rs index 6f2dd4c0c8..b87458fa08 100644 --- a/virtualization/x86_vcpu/src/svm/vcpu.rs +++ b/virtualization/x86_vcpu/src/svm/vcpu.rs @@ -56,6 +56,28 @@ const EFER_LMA: u64 = 1 << 10; const EFER_LME: u64 = 1 << 8; const CR0_PG: u64 = 1 << 31; const CR0_PE: u64 = 1 << 0; +// Keep the first SVM Linux guest model conservative. These optional CR4 +// features are not required by the smoke path and can make nested SVM VMRUN +// validation fail on some hosted AMD/KVM runners when exposed directly from +// the host CPU model. +const CR4_UMIP: u64 = 1 << 11; +const CR4_LA57: u64 = 1 << 12; +const CR4_FSGSBASE: u64 = 1 << 16; +const CR4_PCIDE: u64 = 1 << 17; +const CR4_SMEP: u64 = 1 << 20; +const CR4_SMAP: u64 = 1 << 21; +const CR4_PKE: u64 = 1 << 22; +const CR4_CET: u64 = 1 << 23; +const CR4_PKS: u64 = 1 << 24; +const SVM_UNSUPPORTED_GUEST_CR4: u64 = CR4_UMIP + | CR4_LA57 + | CR4_FSGSBASE + | CR4_PCIDE + | CR4_SMEP + | CR4_SMAP + | CR4_PKE + | CR4_CET + | CR4_PKS; const X2APIC_MSR_BASE: u32 = 0x800; // Match the current VMX/vLAPIC path, which handles x2APIC register offsets 0x00..=0x3f. const X2APIC_MSR_END: u32 = 0x83f; @@ -449,7 +471,7 @@ impl SvmVcpu { self.flush_guest_tlb(); } 4 => { - vmcb.state.cr4.set(val); + vmcb.state.cr4.set(val & !SVM_UNSUPPORTED_GUEST_CR4); self.flush_guest_tlb(); } _ => return ax_err!(InvalidInput, format_args!("Unsupported CR{}", cr_idx)), @@ -700,6 +722,7 @@ impl SvmVcpu { let res = match function { LEAF_FEATURE_INFO => { const FEATURE_VMX: u32 = 1 << 5; + const FEATURE_PCID: u32 = 1 << 17; const FEATURE_HYPERVISOR: u32 = 1 << 31; const FEATURE_MCE: u32 = 1 << 7; const FEATURE_X2APIC: u32 = 1 << 21; @@ -710,6 +733,7 @@ impl SvmVcpu { let mut res = cpuid!(regs_clone.rax, regs_clone.rcx); // Do not expose nested hardware virtualization to the guest. res.ecx &= !FEATURE_VMX; + res.ecx &= !FEATURE_PCID; res.ecx |= FEATURE_X2APIC; res.ecx &= !FEATURE_TSC_DEADLINE; res.ecx |= FEATURE_HYPERVISOR; @@ -728,8 +752,27 @@ impl SvmVcpu { LEAF_STRUCTURED_EXTENDED_FEATURE_FLAGS_ENUMERATION => { let mut res = cpuid!(regs_clone.rax, regs_clone.rcx); if regs_clone.rcx == 0 { - res.ecx.set_bit(5, false); - res.ecx.set_bit(16, false); + const FEATURE_FSGSBASE: u32 = 1 << 0; + const FEATURE_SMEP: u32 = 1 << 7; + const FEATURE_SMAP: u32 = 1 << 20; + const FEATURE_UMIP: u32 = 1 << 2; + const FEATURE_PKU: u32 = 1 << 3; + const FEATURE_OSPKE: u32 = 1 << 4; + const FEATURE_WAITPKG: u32 = 1 << 5; + const FEATURE_CET_SS: u32 = 1 << 7; + const FEATURE_LA57: u32 = 1 << 16; + const FEATURE_PKS: u32 = 1 << 31; + const FEATURE_IBT: u32 = 1 << 20; + + res.ebx &= !(FEATURE_FSGSBASE | FEATURE_SMEP | FEATURE_SMAP); + res.ecx &= !(FEATURE_UMIP + | FEATURE_PKU + | FEATURE_OSPKE + | FEATURE_WAITPKG + | FEATURE_CET_SS + | FEATURE_LA57 + | FEATURE_PKS); + res.edx &= !FEATURE_IBT; } res } From 85984b0a8c32e5e0e96faa5a09f0ee5df0b32031 Mon Sep 17 00:00:00 2001 From: Josen-B Date: Thu, 28 May 2026 05:21:02 +0000 Subject: [PATCH 5/6] fix(svm): fix HLT RIP advancement and rename ignored MSR handler --- virtualization/x86_vcpu/src/svm/vcpu.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/virtualization/x86_vcpu/src/svm/vcpu.rs b/virtualization/x86_vcpu/src/svm/vcpu.rs index b87458fa08..eeed238943 100644 --- a/virtualization/x86_vcpu/src/svm/vcpu.rs +++ b/virtualization/x86_vcpu/src/svm/vcpu.rs @@ -251,6 +251,11 @@ impl SvmVcpu { state.cr0.set(cr0_val.bits()); state.cr3.set(0); + // CR4 is initialized to zero here which is always a subset of + // SVM_UNSUPPORTED_GUEST_CR4. If a non-zero CR4 baseline is ever + // needed, apply the mask: value & !SVM_UNSUPPORTED_GUEST_CR4. + // handle_cr_write(4) already masks unsupported bits on every + // guest CR4 write. state.cr4.set(0); state.cs.selector.set(0); @@ -584,7 +589,7 @@ impl SvmVcpu { Ok(SvmExitCode::MSR) if matches!(self.regs().rcx as u32, IA32_UMWAIT_CONTROL | AMD64_DE_CFG) => { - Some(self.handle_zero_msr_access(exit_info)) + Some(self.handle_ignored_msr_access(exit_info)) } _ => None, } @@ -665,9 +670,11 @@ impl SvmVcpu { self.advance_rip(VM_EXIT_INSTR_LEN_MSR) } - fn handle_zero_msr_access(&mut self, exit_info: &super::vmcb::SvmExitInfo) -> AxResult { + fn handle_ignored_msr_access(&mut self, exit_info: &super::vmcb::SvmExitInfo) -> AxResult { const VM_EXIT_INSTR_LEN_MSR: u8 = 2; + // Reads return zero, writes are silently discarded. Only call this + // for known-ignorable MSRs (UMWAIT_CONTROL, AMD64_DE_CFG). if exit_info.exit_info_1 == 0 { self.write_edx_eax(0); } @@ -752,9 +759,11 @@ impl SvmVcpu { LEAF_STRUCTURED_EXTENDED_FEATURE_FLAGS_ENUMERATION => { let mut res = cpuid!(regs_clone.rax, regs_clone.rcx); if regs_clone.rcx == 0 { + // EBX feature flags. const FEATURE_FSGSBASE: u32 = 1 << 0; const FEATURE_SMEP: u32 = 1 << 7; const FEATURE_SMAP: u32 = 1 << 20; + // ECX feature flags. const FEATURE_UMIP: u32 = 1 << 2; const FEATURE_PKU: u32 = 1 << 3; const FEATURE_OSPKE: u32 = 1 << 4; @@ -762,6 +771,7 @@ impl SvmVcpu { const FEATURE_CET_SS: u32 = 1 << 7; const FEATURE_LA57: u32 = 1 << 16; const FEATURE_PKS: u32 = 1 << 31; + // EDX feature flags. const FEATURE_IBT: u32 = 1 << 20; res.ebx &= !(FEATURE_FSGSBASE | FEATURE_SMEP | FEATURE_SMAP); From a78bc873fd111fe36891181e372dfb0134f356cc Mon Sep 17 00:00:00 2001 From: Josen-B Date: Fri, 29 May 2026 00:49:13 +0000 Subject: [PATCH 6/6] fix(pit): guard latch_status against overwriting existing latched value --- virtualization/x86_vlapic/src/pit.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/virtualization/x86_vlapic/src/pit.rs b/virtualization/x86_vlapic/src/pit.rs index 11707a6a89..06f95e9aa5 100644 --- a/virtualization/x86_vlapic/src/pit.rs +++ b/virtualization/x86_vlapic/src/pit.rs @@ -183,16 +183,18 @@ impl PitChannel { } fn latch_status(&mut self, now_ns: u64) { - let mut status = (self.output_high(now_ns) as u8) << 7; - status |= (self.null_count as u8) << 6; - status |= match self.access_mode { - AccessMode::LatchCount => 0, - AccessMode::LowByte => 1, - AccessMode::HighByte => 2, - AccessMode::LowThenHigh => 3, - } << 4; - status |= self.mode.raw_bits() << 1; - self.latched_status = Some(status); + if self.latched_status.is_none() { + let mut status = (self.output_high(now_ns) as u8) << 7; + status |= (self.null_count as u8) << 6; + status |= match self.access_mode { + AccessMode::LatchCount => 0, + AccessMode::LowByte => 1, + AccessMode::HighByte => 2, + AccessMode::LowThenHigh => 3, + } << 4; + status |= self.mode.raw_bits() << 1; + self.latched_status = Some(status); + } } fn latch_count(&mut self, now_ns: u64) {