diff --git a/Cargo.lock b/Cargo.lock index 4b0234cb7c..b5b0e06f71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9039,8 +9039,10 @@ dependencies = [ "ax-crate-interface", "ax-errno 0.6.0", "ax-memory-addr", + "ax-plat", "axdevice", "axdevice_base", + "axvm", "axvm-types", "x86_vlapic", ] diff --git a/virtualization/axvm/src/irq/mod.rs b/virtualization/axvm/src/irq/mod.rs new file mode 100644 index 0000000000..3d7c6f7ab3 --- /dev/null +++ b/virtualization/axvm/src/irq/mod.rs @@ -0,0 +1,102 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! VM-owned interrupt line routing. + +use alloc::sync::Arc; + +use ax_errno::{AxResult, ax_err}; +use axdevice::IrqResolver; +use axdevice_base::{InterruptTriggerMode, IrqLine, IrqLineId, IrqSink}; +use axvm_types::VMInterruptMode; + +/// Resolves device interrupt lines against one VM's interrupt backend. +/// +/// The fabric owns only the backend capability. It never owns or references the +/// containing VM, so devices can retain [`IrqLine`] objects without creating an +/// `AxVM -> device -> IRQ -> AxVM` reference cycle. +pub struct InterruptFabric { + mode: VMInterruptMode, + sink: Option>, +} + +impl InterruptFabric { + /// Creates a fabric without an interrupt backend. + pub const fn new(mode: VMInterruptMode) -> Self { + Self { mode, sink: None } + } + + /// Creates a fabric that routes lines to `sink`. + pub fn with_sink(mode: VMInterruptMode, sink: Arc) -> AxResult { + if mode == VMInterruptMode::NoIrq { + return ax_err!( + InvalidInput, + "a VM configured with interrupt_mode=no_irq cannot install an IRQ backend" + ); + } + Ok(Self { + mode, + sink: Some(sink), + }) + } + + /// Returns the VM interrupt mode associated with this fabric. + pub const fn mode(&self) -> VMInterruptMode { + self.mode + } + + /// Returns whether this fabric has an interrupt backend. + pub const fn has_backend(&self) -> bool { + self.sink.is_some() + } + + pub(crate) fn validate_mode(&self, mode: VMInterruptMode) -> AxResult { + if self.mode != mode { + return ax_err!( + InvalidInput, + format_args!( + "interrupt fabric mode {:?} does not match VM interrupt mode {:?}", + self.mode, mode + ) + ); + } + Ok(()) + } +} + +impl Default for InterruptFabric { + fn default() -> Self { + Self::new(VMInterruptMode::NoIrq) + } +} + +impl IrqResolver for InterruptFabric { + fn resolve_irq(&self, line: usize, trigger: InterruptTriggerMode) -> AxResult { + let Some(sink) = &self.sink else { + if self.mode == VMInterruptMode::NoIrq { + return ax_err!( + InvalidInput, + format_args!("cannot resolve IRQ line {line}: the VM interrupt mode is NoIrq") + ); + } + return ax_err!( + Unsupported, + format_args!( + "cannot resolve IRQ line {line}: no VM interrupt backend is installed" + ) + ); + }; + Ok(IrqLine::new(IrqLineId(line), trigger, sink.clone())) + } +} diff --git a/virtualization/axvm/src/lib.rs b/virtualization/axvm/src/lib.rs index 1f1f5fa4b9..31a63b254a 100644 --- a/virtualization/axvm/src/lib.rs +++ b/virtualization/axvm/src/lib.rs @@ -26,6 +26,7 @@ extern crate log; mod arch; mod cache; mod host; +pub mod irq; mod manager; mod percpu; mod runtime; @@ -45,6 +46,7 @@ pub(crate) use host::{ paging::HostPagingHandler, task::{AxTaskExt, AxTaskRef, TaskInner, WaitQueue, WaitQueueHandle as HostWaitQueueHandle}, }; +pub use irq::InterruptFabric; pub use manager::{ AxvmRuntime, current_vcpu_id, current_vm_id, get_vm_by_id, get_vm_list, inject_current_vcpu_interrupt, register_vm, diff --git a/virtualization/axvm/src/vm.rs b/virtualization/axvm/src/vm.rs index 7377b133d9..1e319869d9 100644 --- a/virtualization/axvm/src/vm.rs +++ b/virtualization/axvm/src/vm.rs @@ -20,7 +20,10 @@ use ax_errno::{AxError, AxResult, ax_err, ax_err_type}; use ax_kspin::SpinNoIrq as Mutex; use ax_memory_addr::{align_down_4k, align_up_4k}; use axaddrspace::{AddrSpace, MappingFlags}; -use axdevice::{AxVmDeviceConfig, AxVmDevices}; +use axdevice::{ + AxVmDeviceConfig, AxVmDevices, DeviceBuildContext, DeviceFactoryRegistry, + register_builtin_factories, +}; use axdevice_base::AccessWidth; use axvcpu::{AxVCpu, AxVCpuExitReason}; #[cfg(target_arch = "x86_64")] @@ -37,6 +40,7 @@ use crate::vcpu::get_sysreg_device; use crate::{ config::{AxVMConfig, PhysCpuList, VMInterruptMode}, host::paging::{HostPagingHandler, virt_to_phys}, + irq::InterruptFabric, vcpu::AxArchVCpuImpl, }; @@ -72,6 +76,7 @@ struct AxVMInnerConst { phys_cpu_ls: PhysCpuList, vcpu_list: Box<[AxVCpuRef]>, devices: AxVmDevices, + interrupt_fabric: InterruptFabric, } unsafe impl Send for AxVMInnerConst {} @@ -213,7 +218,19 @@ impl AxVM { /// Sets up the VM before booting. pub fn init(&self) -> AxResult { + let mut factories = DeviceFactoryRegistry::new(); + register_builtin_factories(&mut factories)?; + self.init_with_factories(&factories, InterruptFabric::new(self.interrupt_mode())) + } + + /// Sets up the VM with explicit device factories and an interrupt fabric. + pub fn init_with_factories( + &self, + factories: &DeviceFactoryRegistry, + interrupt_fabric: InterruptFabric, + ) -> AxResult { let mut inner_mut = self.inner_mut.lock(); + interrupt_fabric.validate_mode(inner_mut.config.interrupt_mode())?; 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(); @@ -327,9 +344,16 @@ impl AxVM { )?; #[cfg_attr(not(target_arch = "aarch64"), expect(unused_mut))] - let mut devices = axdevice::AxVmDevices::new(AxVmDeviceConfig { - emu_configs: inner_mut.config.emu_devices().to_vec(), - })?; + let mut devices = { + let build_context = DeviceBuildContext::new(&interrupt_fabric); + axdevice::AxVmDevices::build_with_factories( + AxVmDeviceConfig { + emu_configs: inner_mut.config.emu_devices().to_vec(), + }, + factories, + &build_context, + )? + }; #[cfg(target_arch = "aarch64")] { @@ -372,14 +396,8 @@ impl AxVM { } } - self.inner_const.call_once(|| AxVMInnerConst { - phys_cpu_ls: inner_mut.config.phys_cpu_ls.clone(), - vcpu_list: vcpu_list.into_boxed_slice(), - devices, - }); - // Setup VCpus. - for vcpu in self.vcpu_list() { + for vcpu in &vcpu_list { #[cfg(target_arch = "aarch64")] let setup_config = { let passthrough = inner_mut.config.interrupt_mode() == VMInterruptMode::Passthrough; @@ -426,6 +444,14 @@ impl AxVM { setup_config, )?; } + + self.inner_const.call_once(|| AxVMInnerConst { + phys_cpu_ls: inner_mut.config.phys_cpu_ls.clone(), + vcpu_list: vcpu_list.into_boxed_slice(), + devices, + interrupt_fabric, + }); + info!("VM setup: id={}", self.id()); Ok(()) } @@ -558,6 +584,11 @@ impl AxVM { &self.inner_const().devices } + /// Returns this VM's interrupt fabric. + pub fn interrupt_fabric(&self) -> &InterruptFabric { + &self.inner_const().interrupt_fabric + } + /// Run a vCPU according to the given vcpu_id. /// /// ## Arguments diff --git a/virtualization/test_crates/virtualization-tests/Cargo.toml b/virtualization/test_crates/virtualization-tests/Cargo.toml index 43fd02f193..0cf3b73e0e 100644 --- a/virtualization/test_crates/virtualization-tests/Cargo.toml +++ b/virtualization/test_crates/virtualization-tests/Cargo.toml @@ -12,7 +12,9 @@ repository.workspace = true ax-crate-interface.workspace = true ax-errno.workspace = true ax-memory-addr.workspace = true +ax-plat.workspace = true axdevice.workspace = true axdevice_base.workspace = true +axvm.workspace = true axvm-types.workspace = true x86_vlapic.workspace = true diff --git a/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs b/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs new file mode 100644 index 0000000000..aa5f3350e7 --- /dev/null +++ b/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs @@ -0,0 +1,304 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::{Arc, Mutex, Weak}; + +use ax_errno::{AxError, AxResult}; +use ax_plat::{console::ConsoleIf, time::TimeIf}; +use axdevice::{ + AxVmDeviceConfig, AxVmDevices, DeviceBuildContext, DeviceBundle, DeviceFactory, + DeviceFactoryRegistry, DeviceRegistration, IrqResolver, +}; +use axdevice_base::{ + AccessWidth, BaseDeviceOps, InterruptTriggerMode, IrqLine, IrqLineId, IrqSink, +}; +use axvm::InterruptFabric; +use axvm_types::{ + EmulatedDeviceConfig, EmulatedDeviceType, GuestPhysAddr, GuestPhysAddrRange, VMInterruptMode, +}; + +struct TestConsole; + +#[ax_plat::impl_plat_interface] +impl ConsoleIf for TestConsole { + fn write_bytes(_bytes: &[u8]) {} + + fn read_bytes(_bytes: &mut [u8]) -> usize { + 0 + } + + fn irq_num() -> Option { + None + } + + fn set_input_irq_enabled(_enabled: bool) {} + + fn handle_irq() -> ax_plat::console::ConsoleIrqEvent { + ax_plat::console::ConsoleIrqEvent::empty() + } +} + +struct TestTime; + +#[ax_plat::impl_plat_interface] +impl TimeIf for TestTime { + fn current_ticks() -> u64 { + 0 + } + + fn ticks_to_nanos(ticks: u64) -> u64 { + ticks + } + + fn nanos_to_ticks(nanos: u64) -> u64 { + nanos + } + + fn epochoffset_nanos() -> u64 { + 0 + } + + fn irq_num() -> usize { + 0 + } + + fn set_oneshot_timer(_deadline_ns: u64) {} +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum IrqEvent { + SetLevel(IrqLineId, bool), + Pulse(IrqLineId), +} + +#[derive(Default)] +struct RecordingIrqSink { + events: Mutex>, +} + +impl RecordingIrqSink { + fn events(&self) -> Vec { + self.events.lock().unwrap().clone() + } +} + +impl IrqSink for RecordingIrqSink { + fn set_level(&self, line: IrqLineId, asserted: bool) -> AxResult { + self.events + .lock() + .unwrap() + .push(IrqEvent::SetLevel(line, asserted)); + Ok(()) + } + + fn pulse(&self, line: IrqLineId) -> AxResult { + self.events.lock().unwrap().push(IrqEvent::Pulse(line)); + Ok(()) + } +} + +struct IrqMmioDevice { + range: GuestPhysAddrRange, + line: IrqLine, +} + +impl BaseDeviceOps for IrqMmioDevice { + fn address_range(&self) -> GuestPhysAddrRange { + self.range + } + + fn emu_type(&self) -> EmulatedDeviceType { + EmulatedDeviceType::VirtioNet + } + + fn handle_read(&self, _addr: GuestPhysAddr, _width: AccessWidth) -> AxResult { + Ok(0) + } + + fn handle_write(&self, _addr: GuestPhysAddr, _width: AccessWidth, _val: usize) -> AxResult { + self.line.pulse() + } +} + +struct IrqMmioFactory; + +impl DeviceFactory for IrqMmioFactory { + fn device_type(&self) -> EmulatedDeviceType { + EmulatedDeviceType::VirtioNet + } + + fn build( + &self, + config: &EmulatedDeviceConfig, + context: &DeviceBuildContext<'_>, + ) -> AxResult { + let Some(end) = config.base_gpa.checked_add(config.length) else { + return Err(AxError::InvalidInput); + }; + let line = context.resolve_irq(config.irq_id, InterruptTriggerMode::EdgeTriggered)?; + Ok(DeviceRegistration::Mmio(Arc::new(IrqMmioDevice { + range: GuestPhysAddrRange::new(config.base_gpa.into(), end.into()), + line, + })) + .into()) + } +} + +fn irq_device_config(base_gpa: usize, irq_id: usize) -> EmulatedDeviceConfig { + EmulatedDeviceConfig { + name: String::from("irq-mmio"), + base_gpa, + length: 0x1000, + irq_id, + emu_type: EmulatedDeviceType::VirtioNet, + cfg_list: vec![], + } +} + +fn irq_factory_registry() -> DeviceFactoryRegistry { + let mut factories = DeviceFactoryRegistry::new(); + factories.register(Arc::new(IrqMmioFactory)).unwrap(); + factories +} + +fn recording_fabric(mode: VMInterruptMode) -> (InterruptFabric, Weak) { + let sink = Arc::new(RecordingIrqSink::default()); + let weak = Arc::downgrade(&sink); + (InterruptFabric::with_sink(mode, sink).unwrap(), weak) +} + +#[test] +fn test_no_irq_fabric_rejects_backend_and_line_resolution() { + let sink = Arc::new(RecordingIrqSink::default()); + assert_eq!( + InterruptFabric::with_sink(VMInterruptMode::NoIrq, sink).err(), + Some(AxError::InvalidInput) + ); + + let fabric = InterruptFabric::new(VMInterruptMode::NoIrq); + let context = DeviceBuildContext::new(&fabric); + assert_eq!( + AxVmDevices::build_with_factories( + AxVmDeviceConfig::new(vec![irq_device_config(0x6_0000, 12)]), + &irq_factory_registry(), + &context, + ) + .err(), + Some(AxError::InvalidInput) + ); +} + +#[test] +fn test_interrupt_fabric_preserves_event_order() { + let sink = Arc::new(RecordingIrqSink::default()); + let fabric = InterruptFabric::with_sink(VMInterruptMode::Emulated, sink.clone()).unwrap(); + let level = fabric + .resolve_irq(13, InterruptTriggerMode::LevelTriggered) + .unwrap(); + let edge = fabric + .resolve_irq(14, InterruptTriggerMode::EdgeTriggered) + .unwrap(); + + assert_eq!(level.pulse(), Err(AxError::InvalidInput)); + assert_eq!(edge.raise(), Err(AxError::InvalidInput)); + level.raise().unwrap(); + level.lower().unwrap(); + edge.pulse().unwrap(); + + assert_eq!( + sink.events(), + vec![ + IrqEvent::SetLevel(IrqLineId(13), true), + IrqEvent::SetLevel(IrqLineId(13), false), + IrqEvent::Pulse(IrqLineId(14)), + ] + ); +} + +#[test] +fn test_factory_device_emits_irq_through_interrupt_fabric() { + let (fabric, sink) = recording_fabric(VMInterruptMode::Emulated); + let devices = { + let context = DeviceBuildContext::new(&fabric); + AxVmDevices::build_with_factories( + AxVmDeviceConfig::new(vec![irq_device_config(0x7_0000, 15)]), + &irq_factory_registry(), + &context, + ) + .unwrap() + }; + + devices + .handle_mmio_write(GuestPhysAddr::from(0x7_0000), AccessWidth::Dword, 1) + .unwrap(); + + assert_eq!( + sink.upgrade().unwrap().events(), + vec![IrqEvent::Pulse(IrqLineId(15))] + ); +} + +#[test] +fn test_dropping_devices_and_fabric_releases_irq_backend() { + let (fabric, sink) = recording_fabric(VMInterruptMode::Emulated); + let devices = { + let context = DeviceBuildContext::new(&fabric); + AxVmDevices::build_with_factories( + AxVmDeviceConfig::new(vec![irq_device_config(0x8_0000, 16)]), + &irq_factory_registry(), + &context, + ) + .unwrap() + }; + + drop(fabric); + assert!(sink.upgrade().is_some()); + drop(devices); + assert!(sink.upgrade().is_none()); +} + +#[test] +fn test_equal_irq_numbers_are_isolated_between_fabrics() { + let (fabric_a, sink_a) = recording_fabric(VMInterruptMode::Emulated); + let (fabric_b, sink_b) = recording_fabric(VMInterruptMode::Emulated); + let devices_a = { + let context = DeviceBuildContext::new(&fabric_a); + AxVmDevices::build_with_factories( + AxVmDeviceConfig::new(vec![irq_device_config(0x9_0000, 17)]), + &irq_factory_registry(), + &context, + ) + .unwrap() + }; + let devices_b = { + let context = DeviceBuildContext::new(&fabric_b); + AxVmDevices::build_with_factories( + AxVmDeviceConfig::new(vec![irq_device_config(0xa_0000, 17)]), + &irq_factory_registry(), + &context, + ) + .unwrap() + }; + + devices_a + .handle_mmio_write(GuestPhysAddr::from(0x9_0000), AccessWidth::Dword, 1) + .unwrap(); + + assert_eq!( + sink_a.upgrade().unwrap().events(), + vec![IrqEvent::Pulse(IrqLineId(17))] + ); + assert!(sink_b.upgrade().unwrap().events().is_empty()); + assert_eq!(devices_b.iter_mmio_dev().count(), 1); +}