// Copyright © 2019 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause // use std::any::Any; use std::collections::{BTreeMap, HashMap}; use std::os::fd::BorrowedFd; use std::os::unix::io::AsRawFd; use std::path::PathBuf; use std::sync::{Arc, Barrier, Mutex}; use std::{cmp, io, result}; use anyhow::anyhow; use byteorder::{ByteOrder, LittleEndian}; use hypervisor::HypervisorVmError; use libc::{_SC_PAGESIZE, sysconf}; use log::{error, info}; use serde::{Deserialize, Serialize}; use thiserror::Error; use vfio_bindings::bindings::vfio::*; use vfio_ioctls::{VfioDevice, VfioIrq, VfioOps, VfioRegionInfoCap, VfioRegionSparseMmapArea}; use vm_allocator::page_size::{ align_page_size_down, align_page_size_up, get_page_size, is_4k_aligned, is_4k_multiple, is_page_size_aligned, }; use vm_allocator::{AddressAllocator, MemorySlotAllocator, SystemAllocator}; use vm_device::dma_mapping::ExternalDmaMapping; use vm_device::interrupt::{ InterruptIndex, InterruptManager, InterruptSourceGroup, MsiIrqGroupConfig, }; use vm_device::{BusDevice, Resource}; use vm_memory::{Address, GuestAddress, GuestAddressSpace, GuestMemory, GuestUsize}; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use vmm_sys_util::eventfd::EventFd; use crate::configuration::{COMMAND_REG, COMMAND_REG_MEMORY_SPACE_MASK}; use crate::mmap::MmapRegion; use crate::msi::{MSI_CONFIG_ID, MsiConfigState}; use crate::msix::{MaybeMutInterruptSourceGroup, MsixConfigState}; use crate::{ BarReprogrammingParams, MSIX_CONFIG_ID, MSIX_TABLE_ENTRY_SIZE, MsiCap, MsiConfig, MsixCap, MsixConfig, PCI_CONFIGURATION_ID, PciBarConfiguration, PciBarPrefetchable, PciBarRegionType, PciBdf, PciCapabilityId, PciClassCode, PciConfiguration, PciDevice, PciDeviceError, PciExpressCapabilityId, PciHeaderType, PciSubclass, msi_num_enabled_vectors, }; pub(crate) const VFIO_COMMON_ID: &str = "vfio_common"; #[derive(Debug, Error)] pub enum VfioPciError { #[error("Failed to create user memory region")] CreateUserMemoryRegion(#[source] HypervisorVmError), #[error("Failed to DMA map: {0} for device {1} (guest BDF: {2})")] DmaMap(#[source] vfio_ioctls::VfioError, PathBuf, PciBdf), #[error("Failed to DMA unmap: {0} for device {1} (guest BDF: {2})")] DmaUnmap(#[source] vfio_ioctls::VfioError, PathBuf, PciBdf), #[error("Failed to enable INTx")] EnableIntx(#[source] VfioError), #[error("Failed to enable MSI")] EnableMsi(#[source] VfioError), #[error("Failed to enable MSI-x")] EnableMsix(#[source] VfioError), #[error("Failed to mmap the area")] MmapArea, #[error("Failed to notifier's eventfd")] MissingNotifier, #[error("Invalid region alignment")] RegionAlignment, #[error("Invalid region size")] RegionSize, #[error("Failed to retrieve MsiConfigState")] RetrieveMsiConfigState(#[source] anyhow::Error), #[error("Failed to retrieve MsixConfigState")] RetrieveMsixConfigState(#[source] anyhow::Error), #[error("Failed to retrieve PciConfigurationState")] RetrievePciConfigurationState(#[source] anyhow::Error), #[error("Failed to retrieve VfioCommonState")] RetrieveVfioCommonState(#[source] anyhow::Error), } #[derive(Copy, Clone)] enum PciVfioSubclass { VfioSubclass = 0xff, } impl PciSubclass for PciVfioSubclass { fn get_register_value(&self) -> u8 { *self as u8 } } enum InterruptUpdateAction { EnableMsi, DisableMsi, EnableMsix, DisableMsix, } #[derive(Serialize, Deserialize)] struct IntxState { enabled: bool, } pub(crate) struct VfioIntx { interrupt_source_group: Arc, enabled: bool, } #[derive(Serialize, Deserialize)] struct MsiState { cap: MsiCap, cap_offset: u32, } pub(crate) struct VfioMsi { pub(crate) cfg: MsiConfig, cap_offset: u32, interrupt_source_group: Arc, } impl VfioMsi { fn update(&mut self, offset: u64, data: &[u8]) -> Option { let old_enabled = self.cfg.enabled(); self.cfg.update(offset, data); let new_enabled = self.cfg.enabled(); if !old_enabled && new_enabled { return Some(InterruptUpdateAction::EnableMsi); } if old_enabled && !new_enabled { return Some(InterruptUpdateAction::DisableMsi); } None } } #[derive(Serialize, Deserialize)] struct MsixState { cap: MsixCap, cap_offset: u32, bdf: u32, } pub(crate) struct VfioMsix { pub(crate) bar: MsixConfig, cap: MsixCap, cap_offset: u32, interrupt_source_group: Arc, } impl VfioMsix { fn update(&mut self, offset: u64, data: &[u8]) -> Option { let old_enabled = self.bar.enabled(); // Update "Message Control" word if offset == 2 && data.len() == 2 { let data = LittleEndian::read_u16(data); self.bar.set_msg_ctl(data); self.cap.set_msg_ctl(data); } else if offset == 0 && data.len() == 4 { // Some guests update MSI-X control through the dword config write path. let data = (LittleEndian::read_u32(data) >> 16) as u16; self.bar.set_msg_ctl(data); self.cap.set_msg_ctl(data); } let new_enabled = self.bar.enabled(); if !old_enabled && new_enabled { return Some(InterruptUpdateAction::EnableMsix); } if old_enabled && !new_enabled { return Some(InterruptUpdateAction::DisableMsix); } None } fn table_accessed(&self, bar_index: u32, offset: u64) -> bool { let table_offset: u64 = u64::from(self.cap.table_offset()); let table_size: u64 = u64::from(self.cap.table_size()) * (MSIX_TABLE_ENTRY_SIZE as u64); let table_bir: u32 = self.cap.table_bir(); bar_index == table_bir && offset >= table_offset && offset < table_offset + table_size } } pub(crate) struct Interrupt { pub(crate) intx: Option, pub(crate) msi: Option, pub(crate) msix: Option, } impl Interrupt { fn update_msi(&mut self, offset: u64, data: &[u8]) -> Option { if let Some(msi) = &mut self.msi { let action = msi.update(offset, data); return action; } None } fn update_msix(&mut self, offset: u64, data: &[u8]) -> Option { if let Some(msix) = &mut self.msix { let action = msix.update(offset, data); return action; } None } fn accessed(&self, offset: u64) -> Option<(PciCapabilityId, u64)> { if let Some(msi) = &self.msi && offset >= u64::from(msi.cap_offset) && offset < u64::from(msi.cap_offset) + msi.cfg.size() { return Some(( PciCapabilityId::MessageSignalledInterrupts, u64::from(msi.cap_offset), )); } if let Some(msix) = &self.msix && offset == u64::from(msix.cap_offset) { return Some((PciCapabilityId::MsiX, u64::from(msix.cap_offset))); } None } fn msix_table_accessed(&self, bar_index: u32, offset: u64) -> bool { if let Some(msix) = &self.msix { return msix.table_accessed(bar_index, offset); } false } fn msix_write_table(&mut self, offset: u64, data: &[u8]) { if let Some(msix) = &mut self.msix { let offset = offset - u64::from(msix.cap.table_offset()); msix.bar.write_table(offset, data); } } fn msix_read_table(&self, offset: u64, data: &mut [u8]) { if let Some(msix) = &self.msix { let offset = offset - u64::from(msix.cap.table_offset()); msix.bar.read_table(offset, data); } } pub(crate) fn intx_in_use(&self) -> bool { if let Some(intx) = &self.intx { return intx.enabled; } false } } #[derive(Clone)] pub struct UserMemoryRegion { pub slot: u32, pub start: u64, pub mapping: Arc, } #[derive(Clone)] pub struct MmioRegion { pub start: GuestAddress, pub length: GuestUsize, pub(crate) type_: PciBarRegionType, pub(crate) index: u32, pub(crate) user_memory_regions: Vec, } impl MmioRegion { /// Returns true if this region has the exact same memory slots as the other region. pub fn has_matching_slots(&self, other: &MmioRegion) -> bool { self.user_memory_regions.len() == other.user_memory_regions.len() && self .user_memory_regions .iter() .all(|u| other.user_memory_regions.iter().any(|o| o.slot == u.slot)) } } /// # Safety /// /// [`Self::find_user_address`] must always either return `Err` /// or a pointer to `size` bytes of valid memory. unsafe trait MmioRegionRange { fn check_range(&self, guest_addr: u64, size: u64) -> bool; fn find_user_address(&self, guest_addr: u64, size: u64) -> Result<*mut u8, io::Error>; } // SAFETY: See the comment in `find_user_address`. unsafe impl MmioRegionRange for Vec { // Check if a guest address is within the range of mmio regions fn check_range(&self, guest_addr: u64, size: u64) -> bool { let Some(guest_addr_end) = guest_addr.checked_add(size) else { return false; }; for region in self.iter() { let Some(region_end) = region.start.raw_value().checked_add(region.length) else { return false; }; if guest_addr >= region.start.raw_value() && guest_addr_end <= region_end { return true; } } false } // Locate the user region address for a guest address within all mmio regions fn find_user_address(&self, guest_addr: u64, size: u64) -> Result<*mut u8, io::Error> { for region in self.iter() { for user_region in region.user_memory_regions.iter() { let mapping: &MmapRegion = &user_region.mapping; let start: u64 = user_region.start; let len: u64 = mapping.len().try_into().unwrap(); // See if the guest address is inside the region. let Some(offset_from_start) = guest_addr.checked_sub(start) else { continue; }; if offset_from_start >= len { continue; } // Check that the size is in bounds. // This enforces the invariant promised by implementing MmioRegionRange. assert!( size <= len - offset_from_start, "Attempt to read {size} bytes at offset {offset_from_start} into \ a region of size {len}" ); // SAFETY: MmapRegion guarantees that mapping.addr points to at least // mapping.len() bytes of valid memory. offset_from_start is equal // to guest_addr - start, which was checked to be less than mapping.len(). // Therefore, the returned pointer is still in the range of valid memory. // Also, since mapping.len() fit in usize, offset_from_start must as well, // so the cast is safe. return Ok(unsafe { mapping.addr().add(offset_from_start as usize) }); } } Err(io::Error::other(format!( "unable to find user address: 0x{guest_addr:x}" ))) } } #[derive(Debug, Error)] pub enum VfioError { #[error("Kernel VFIO error")] KernelVfio(#[source] vfio_ioctls::VfioError), #[error("VFIO user error")] VfioUser(#[source] vfio_user::Error), } pub(crate) trait Vfio: Send + Sync { fn read_config_byte(&self, offset: u32) -> u8 { let mut data: [u8; 1] = [0]; self.read_config(offset, &mut data); data[0] } fn read_config_word(&self, offset: u32) -> u16 { let mut data: [u8; 2] = [0, 0]; self.read_config(offset, &mut data); u16::from_le_bytes(data) } fn read_config_dword(&self, offset: u32) -> u32 { let mut data: [u8; 4] = [0, 0, 0, 0]; self.read_config(offset, &mut data); u32::from_le_bytes(data) } fn write_config_dword(&self, offset: u32, buf: u32) { let data: [u8; 4] = buf.to_le_bytes(); self.write_config(offset, &data); } fn read_config(&self, offset: u32, data: &mut [u8]) { self.region_read(VFIO_PCI_CONFIG_REGION_INDEX, offset.into(), data.as_mut()); } fn write_config(&self, offset: u32, data: &[u8]) { self.region_write(VFIO_PCI_CONFIG_REGION_INDEX, offset.into(), data); } fn enable_msi(&self, fds: Vec<&EventFd>) -> Result<(), VfioError> { self.enable_irq(VFIO_PCI_MSI_IRQ_INDEX, fds) } fn disable_msi(&self) -> Result<(), VfioError> { self.disable_irq(VFIO_PCI_MSI_IRQ_INDEX) } fn enable_msix(&self, fds: Vec<&EventFd>) -> Result<(), VfioError> { self.enable_irq(VFIO_PCI_MSIX_IRQ_INDEX, fds) } fn disable_msix(&self) -> Result<(), VfioError> { self.disable_irq(VFIO_PCI_MSIX_IRQ_INDEX) } fn region_read(&self, _index: u32, _offset: u64, _data: &mut [u8]) { unimplemented!() } fn region_write(&self, _index: u32, _offset: u64, _data: &[u8]) { unimplemented!() } fn get_irq_info(&self, _irq_index: u32) -> Option { unimplemented!() } fn enable_irq(&self, _irq_index: u32, _event_fds: Vec<&EventFd>) -> Result<(), VfioError> { unimplemented!() } fn disable_irq(&self, _irq_index: u32) -> Result<(), VfioError> { unimplemented!() } fn unmask_irq(&self, _irq_index: u32) -> Result<(), VfioError> { unimplemented!() } } struct VfioDeviceWrapper { device: Arc, } impl VfioDeviceWrapper { fn new(device: Arc) -> Self { Self { device } } } impl Vfio for VfioDeviceWrapper { fn region_read(&self, index: u32, offset: u64, data: &mut [u8]) { self.device.region_read(index, data, offset); } fn region_write(&self, index: u32, offset: u64, data: &[u8]) { self.device.region_write(index, data, offset); } fn get_irq_info(&self, irq_index: u32) -> Option { self.device.get_irq_info(irq_index).copied() } fn enable_irq(&self, irq_index: u32, event_fds: Vec<&EventFd>) -> Result<(), VfioError> { self.device .enable_irq(irq_index, event_fds) .map_err(VfioError::KernelVfio) } fn disable_irq(&self, irq_index: u32) -> Result<(), VfioError> { self.device .disable_irq(irq_index) .map_err(VfioError::KernelVfio) } fn unmask_irq(&self, irq_index: u32) -> Result<(), VfioError> { self.device .unmask_irq(irq_index) .map_err(VfioError::KernelVfio) } } #[derive(Serialize, Deserialize)] struct VfioCommonState { intx_state: Option, msi_state: Option, msix_state: Option, } pub(crate) struct ConfigPatch { mask: u32, patch: u32, } pub(crate) struct VfioCommon { pub(crate) configuration: PciConfiguration, pub(crate) mmio_regions: Vec, pub(crate) interrupt: Interrupt, pub(crate) msi_interrupt_manager: Arc>, pub(crate) legacy_interrupt_group: Option>, pub(crate) vfio_wrapper: Arc, pub(crate) patches: HashMap, x_nv_gpudirect_clique: Option, x_exclude_mmap_bars: Vec, } #[derive(Default)] pub(crate) struct VfioCommonConfig { pub(crate) x_nv_gpudirect_clique: Option, pub(crate) x_exclude_mmap_bars: Vec, } impl VfioCommon { pub(crate) fn new( msi_interrupt_manager: Arc>, legacy_interrupt_group: Option>, vfio_wrapper: Arc, subclass: &dyn PciSubclass, bdf: PciBdf, snapshot: Option<&Snapshot>, config: VfioCommonConfig, ) -> Result { let pci_configuration_state = vm_migration::state_from_id(snapshot, PCI_CONFIGURATION_ID) .map_err(|e| { VfioPciError::RetrievePciConfigurationState(anyhow!( "Failed to get PciConfigurationState from Snapshot: {e}" )) })?; let configuration = PciConfiguration::new( 0, 0, 0, PciClassCode::Other, subclass, None, PciHeaderType::Device, 0, 0, None, pci_configuration_state, ); let mut vfio_common = VfioCommon { mmio_regions: Vec::new(), configuration, interrupt: Interrupt { intx: None, msi: None, msix: None, }, msi_interrupt_manager, legacy_interrupt_group, vfio_wrapper, patches: HashMap::new(), x_nv_gpudirect_clique: config.x_nv_gpudirect_clique, x_exclude_mmap_bars: config.x_exclude_mmap_bars, }; let state: Option = snapshot .as_ref() .map(|s| s.to_state()) .transpose() .map_err(|e| { VfioPciError::RetrieveVfioCommonState(anyhow!( "Failed to get VfioCommonState from Snapshot: {e}" )) })?; let msi_state = vm_migration::state_from_id(snapshot, MSI_CONFIG_ID).map_err(|e| { VfioPciError::RetrieveMsiConfigState(anyhow!( "Failed to get MsiConfigState from Snapshot: {e}" )) })?; let msix_state = vm_migration::state_from_id(snapshot, MSIX_CONFIG_ID).map_err(|e| { VfioPciError::RetrieveMsixConfigState(anyhow!( "Failed to get MsixConfigState from Snapshot: {e}" )) })?; if let Some(state) = state.as_ref() { vfio_common.set_state(state, msi_state, msix_state)?; } else { vfio_common.parse_capabilities(bdf); vfio_common.initialize_legacy_interrupt()?; } Ok(vfio_common) } /// In case msix table offset is not page size aligned, we need do some fixup to achieve it. /// Because we don't want the MMIO RW region and trap region overlap each other. fn fixup_msix_region(&mut self, bar_id: u32, region_size: u64) -> u64 { if let Some(msix) = self.interrupt.msix.as_mut() { let msix_cap = &mut msix.cap; // Suppose table_bir equals to pba_bir here. Am I right? let (table_offset, table_size) = msix_cap.table_range(); if is_page_size_aligned(table_offset) || msix_cap.table_bir() != bar_id { return region_size; } let (pba_offset, pba_size) = msix_cap.pba_range(); let msix_sz = align_page_size_up(table_size + pba_size); // Expand region to hold RW and trap region which both page size aligned let size = cmp::max(region_size * 2, msix_sz * 2); // let table starts from the middle of the region msix_cap.table_set_offset((size / 2) as u32); msix_cap.pba_set_offset((size / 2 + pba_offset - table_offset) as u32); size } else { // MSI-X not supported for this device region_size } } pub(crate) fn allocate_bars( &mut self, allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, mmio64_allocator: &mut AddressAllocator, resources: Option<&[Resource]>, ) -> Result, PciDeviceError> { let mut bars = Vec::new(); let mut bar_id = VFIO_PCI_BAR0_REGION_INDEX; // Going through all regular regions to compute the BAR size. // We're not saving the BAR address to restore it, because we // are going to allocate a guest address for each BAR and write // that new address back. while bar_id < VFIO_PCI_CONFIG_REGION_INDEX { let mut region_size: u64 = 0; let mut region_type = PciBarRegionType::Memory32BitRegion; let mut prefetchable = PciBarPrefetchable::NotPrefetchable; let mut flags: u32 = 0; let mut restored_bar_addr = None; if let Some(resources) = resources { for resource in resources { if let Resource::PciBar { index, base, size, type_, .. } = resource && *index == bar_id as usize { restored_bar_addr = Some(GuestAddress(*base)); region_size = *size; region_type = PciBarRegionType::from(*type_); break; } } if restored_bar_addr.is_none() { bar_id += 1; continue; } } else { let bar_offset = if bar_id == VFIO_PCI_ROM_REGION_INDEX { (PCI_ROM_EXP_BAR_INDEX * 4) as u32 } else { PCI_CONFIG_BAR_OFFSET + bar_id * 4 }; // First read flags flags = self.vfio_wrapper.read_config_dword(bar_offset); // Is this an IO BAR? let io_bar = if bar_id == VFIO_PCI_ROM_REGION_INDEX { false } else { matches!(flags & PCI_CONFIG_IO_BAR, PCI_CONFIG_IO_BAR) }; // Is this a 64-bit BAR? let is_64bit_bar = if bar_id == VFIO_PCI_ROM_REGION_INDEX { false } else { matches!( flags & PCI_CONFIG_MEMORY_BAR_64BIT, PCI_CONFIG_MEMORY_BAR_64BIT ) }; if matches!( flags & PCI_CONFIG_BAR_PREFETCHABLE, PCI_CONFIG_BAR_PREFETCHABLE ) { prefetchable = PciBarPrefetchable::Prefetchable; } // To get size write all 1s self.vfio_wrapper .write_config_dword(bar_offset, 0xffff_ffff); // And read back BAR value. The device will write zeros for bits it doesn't care about let mut lower = self.vfio_wrapper.read_config_dword(bar_offset); if io_bar { // Mask flag bits (lowest 2 for I/O bars) lower &= !0b11; // BAR is not enabled if lower == 0 { bar_id += 1; continue; } // IO BAR region_type = PciBarRegionType::IoRegion; // Invert bits and add 1 to calculate size region_size = (!lower + 1) as u64; } else if is_64bit_bar { // 64 bits Memory BAR region_type = PciBarRegionType::Memory64BitRegion; // Query size of upper BAR of 64-bit BAR let upper_offset: u32 = PCI_CONFIG_BAR_OFFSET + (bar_id + 1) * 4; self.vfio_wrapper .write_config_dword(upper_offset, 0xffff_ffff); let upper = self.vfio_wrapper.read_config_dword(upper_offset); let mut combined_size = (u64::from(upper) << 32) | u64::from(lower); // Mask out flag bits (lowest 4 for memory bars) combined_size &= !0b1111; // BAR is not enabled if combined_size == 0 { bar_id += 1; continue; } // Invert and add 1 to to find size region_size = !combined_size + 1; } else { region_type = PciBarRegionType::Memory32BitRegion; // Mask out flag bits (lowest 4 for memory bars) lower &= !0b1111; if lower == 0 { bar_id += 1; continue; } // Invert and add 1 to to find size region_size = (!lower + 1) as u64; } } let bar_addr = match region_type { PciBarRegionType::IoRegion => { // The address needs to be 4 bytes aligned. allocator .allocate_io_addresses(restored_bar_addr, region_size, Some(0x4)) .ok_or(PciDeviceError::IoAllocationFailed(region_size))? } PciBarRegionType::Memory32BitRegion => { // BAR allocation must be naturally aligned mmio32_allocator .allocate(restored_bar_addr, region_size, Some(region_size)) .ok_or(PciDeviceError::IoAllocationFailed(region_size))? } PciBarRegionType::Memory64BitRegion => { // We need do some fixup to keep MMIO RW region and msix cap region page size // aligned. region_size = self.fixup_msix_region(bar_id, region_size); mmio64_allocator .allocate( restored_bar_addr, region_size, Some(cmp::max( // SAFETY: FFI call. Trivially safe. unsafe { sysconf(_SC_PAGESIZE) as GuestUsize }, region_size, )), ) .ok_or(PciDeviceError::IoAllocationFailed(region_size))? } }; // We can now build our BAR configuration block. let bar = PciBarConfiguration::default() .set_index(bar_id as usize) .set_address(bar_addr.raw_value()) .set_size(region_size) .set_region_type(region_type) .set_prefetchable(prefetchable); if bar_id == VFIO_PCI_ROM_REGION_INDEX { self.configuration .add_pci_rom_bar(&bar, flags & 0x1) .map_err(|e| PciDeviceError::IoRegistrationFailed(bar_addr.raw_value(), e))?; } else { self.configuration .add_pci_bar(&bar) .map_err(|e| PciDeviceError::IoRegistrationFailed(bar_addr.raw_value(), e))?; } bars.push(bar); self.mmio_regions.push(MmioRegion { start: bar_addr, length: region_size, type_: region_type, index: bar_id, user_memory_regions: Vec::new(), }); bar_id += 1; if region_type == PciBarRegionType::Memory64BitRegion { bar_id += 1; } } Ok(bars) } pub(crate) fn free_bars( &mut self, allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, mmio64_allocator: &mut AddressAllocator, ) -> Result<(), PciDeviceError> { for region in self.mmio_regions.iter() { match region.type_ { PciBarRegionType::IoRegion => { allocator.free_io_addresses(region.start, region.length); } PciBarRegionType::Memory32BitRegion => { mmio32_allocator.free(region.start, region.length); } PciBarRegionType::Memory64BitRegion => { mmio64_allocator.free(region.start, region.length); } } } Ok(()) } fn parse_msix_capabilities(&mut self, cap: u8) -> MsixCap { let msg_ctl = self.vfio_wrapper.read_config_word((cap + 2).into()); let table = self.vfio_wrapper.read_config_dword((cap + 4).into()); let pba = self.vfio_wrapper.read_config_dword((cap + 8).into()); MsixCap { msg_ctl, table, pba, } } fn initialize_msix( &mut self, msix_cap: MsixCap, cap_offset: u32, bdf: PciBdf, state: Option, ) { let interrupt_source_group = self .msi_interrupt_manager .create_group(MsiIrqGroupConfig { base: 0, count: msix_cap.table_size() as InterruptIndex, }) .unwrap(); let msix_config = MsixConfig::new( msix_cap.table_size(), MaybeMutInterruptSourceGroup::Immutable(interrupt_source_group.clone()), bdf.into(), state, ) .unwrap(); self.interrupt.msix = Some(VfioMsix { bar: msix_config, cap: msix_cap, cap_offset, interrupt_source_group, }); } fn parse_msi_capabilities(&mut self, cap: u8) -> u16 { self.vfio_wrapper.read_config_word((cap + 2).into()) } fn initialize_msi(&mut self, msg_ctl: u16, cap_offset: u32, state: Option) { let interrupt_source_group = self .msi_interrupt_manager .create_group(MsiIrqGroupConfig { base: 0, count: msi_num_enabled_vectors(msg_ctl) as InterruptIndex, }) .unwrap(); let msi_config = MsiConfig::new(msg_ctl, interrupt_source_group.clone(), state).unwrap(); self.interrupt.msi = Some(VfioMsi { cfg: msi_config, cap_offset, interrupt_source_group, }); } /// Returns true, if the device claims to have a PCI capability list. fn has_capabilities(&self) -> bool { let status = self.vfio_wrapper.read_config_word(PCI_CONFIG_STATUS_OFFSET); status & PCI_CONFIG_STATUS_CAPABILITIES_LIST != 0 } fn get_msix_cap_idx(&self) -> Option { if !self.has_capabilities() { return None; } let mut cap_next = self .vfio_wrapper .read_config_byte(PCI_CONFIG_CAPABILITY_OFFSET) & PCI_CONFIG_CAPABILITY_PTR_MASK; while cap_next != 0 { let cap_id = self.vfio_wrapper.read_config_byte(cap_next.into()); if PciCapabilityId::from(cap_id) == PciCapabilityId::MsiX { return Some(cap_next as usize); } let cap_ptr = self.vfio_wrapper.read_config_byte((cap_next + 1).into()) & PCI_CONFIG_CAPABILITY_PTR_MASK; // See parse_capabilities below for an explanation. if cap_ptr == cap_next { break; } cap_next = cap_ptr; } None } fn parse_capabilities(&mut self, bdf: PciBdf) { if !self.has_capabilities() { return; } let mut cap_iter = self .vfio_wrapper .read_config_byte(PCI_CONFIG_CAPABILITY_OFFSET) & PCI_CONFIG_CAPABILITY_PTR_MASK; let mut pci_express_cap_found = false; let mut power_management_cap_found = false; while cap_iter != 0 { let cap_id = self.vfio_wrapper.read_config_byte(cap_iter.into()); match PciCapabilityId::from(cap_id) { PciCapabilityId::MessageSignalledInterrupts => { if let Some(irq_info) = self.vfio_wrapper.get_irq_info(VFIO_PCI_MSI_IRQ_INDEX) && irq_info.count > 0 { // Parse capability only if the VFIO device // supports MSI. let msg_ctl = self.parse_msi_capabilities(cap_iter); self.initialize_msi(msg_ctl, cap_iter as u32, None); } } PciCapabilityId::MsiX => { if let Some(irq_info) = self.vfio_wrapper.get_irq_info(VFIO_PCI_MSIX_IRQ_INDEX) && irq_info.count > 0 { // Parse capability only if the VFIO device // supports MSI-X. let msix_cap = self.parse_msix_capabilities(cap_iter); self.initialize_msix(msix_cap, cap_iter as u32, bdf, None); } } PciCapabilityId::PciExpress => pci_express_cap_found = true, PciCapabilityId::PowerManagement => power_management_cap_found = true, _ => {} } let cap_next = self.vfio_wrapper.read_config_byte((cap_iter + 1).into()) & PCI_CONFIG_CAPABILITY_PTR_MASK; // Break out of the loop, if we either find the end or we have a broken device. This // doesn't handle all cases where a device might send us in a loop here, but it // handles case of a device returning 0xFF instead of implementing a real // capabilities list. if cap_next == 0 || cap_next == cap_iter { break; } cap_iter = cap_next; } if let Some(clique_id) = self.x_nv_gpudirect_clique { self.add_nv_gpudirect_clique_cap(cap_iter, clique_id); } if pci_express_cap_found && power_management_cap_found { self.parse_extended_capabilities(); } } fn add_nv_gpudirect_clique_cap(&mut self, cap_iter: u8, clique_id: u8) { // Turing, Ampere, Hopper, and Lovelace GPUs have dedicated space // at 0xD4 for this capability. let cap_offset = 0xd4u32; let reg_idx = (cap_iter / 4) as usize; self.patches.insert( reg_idx, ConfigPatch { mask: 0x0000_ff00, patch: cap_offset << 8, }, ); let reg_idx = (cap_offset / 4) as usize; self.patches.insert( reg_idx, ConfigPatch { mask: 0xffff_ffff, patch: 0x50080009u32, }, ); self.patches.insert( reg_idx + 1, ConfigPatch { mask: 0xffff_ffff, patch: (u32::from(clique_id) << 19) | 0x5032, }, ); } fn parse_extended_capabilities(&mut self) { let mut current_offset = PCI_CONFIG_EXTENDED_CAPABILITY_OFFSET; loop { let ext_cap_hdr = self.vfio_wrapper.read_config_dword(current_offset); let cap_id: u16 = (ext_cap_hdr & 0xffff) as u16; let cap_next: u16 = ((ext_cap_hdr >> 20) & 0xfff) as u16; match PciExpressCapabilityId::from(cap_id) { PciExpressCapabilityId::AlternativeRoutingIdentificationInterpretation | PciExpressCapabilityId::ResizeableBar | PciExpressCapabilityId::SingleRootIoVirtualization => { let reg_idx = (current_offset / 4) as usize; self.patches.insert( reg_idx, ConfigPatch { mask: 0x0000_ffff, patch: PciExpressCapabilityId::NullCapability as u32, }, ); } _ => {} } if cap_next == 0 { break; } current_offset = cap_next.into(); } } pub(crate) fn enable_intx(&mut self) -> Result<(), VfioPciError> { if let Some(intx) = &mut self.interrupt.intx && !intx.enabled { if let Some(eventfd) = intx.interrupt_source_group.notifier(0) { self.vfio_wrapper .enable_irq(VFIO_PCI_INTX_IRQ_INDEX, vec![&eventfd]) .map_err(VfioPciError::EnableIntx)?; intx.enabled = true; } else { return Err(VfioPciError::MissingNotifier); } } Ok(()) } pub(crate) fn disable_intx(&mut self) { if let Some(intx) = &mut self.interrupt.intx && intx.enabled { if let Err(e) = self.vfio_wrapper.disable_irq(VFIO_PCI_INTX_IRQ_INDEX) { error!("Could not disable INTx: {e}"); } else { intx.enabled = false; } } } pub(crate) fn enable_msi(&self) -> Result<(), VfioPciError> { if let Some(msi) = &self.interrupt.msi { let mut irq_fds: Vec = Vec::new(); for i in 0..msi.cfg.num_enabled_vectors() { if let Some(eventfd) = msi.interrupt_source_group.notifier(i as InterruptIndex) { irq_fds.push(eventfd); } else { return Err(VfioPciError::MissingNotifier); } } self.vfio_wrapper .enable_msi(irq_fds.iter().collect()) .map_err(VfioPciError::EnableMsi)?; } Ok(()) } pub(crate) fn disable_msi(&self) { if let Err(e) = self.vfio_wrapper.disable_msi() { error!("Could not disable MSI: {e}"); } } pub(crate) fn enable_msix(&self) -> Result<(), VfioPciError> { if let Some(msix) = &self.interrupt.msix { let mut irq_fds: Vec = Vec::new(); for i in 0..msix.bar.table_entries.len() { if let Some(eventfd) = msix.interrupt_source_group.notifier(i as InterruptIndex) { irq_fds.push(eventfd); } else { return Err(VfioPciError::MissingNotifier); } } self.vfio_wrapper .enable_msix(irq_fds.iter().collect()) .map_err(VfioPciError::EnableMsix)?; } Ok(()) } pub(crate) fn disable_msix(&self) { if let Err(e) = self.vfio_wrapper.disable_msix() { error!("Could not disable MSI-X: {e}"); } } fn initialize_legacy_interrupt(&mut self) -> Result<(), VfioPciError> { if let Some(irq_info) = self.vfio_wrapper.get_irq_info(VFIO_PCI_INTX_IRQ_INDEX) && irq_info.count == 0 { // A count of 0 means the INTx IRQ is not supported, therefore // it shouldn't be initialized. return Ok(()); } if let Some(interrupt_source_group) = self.legacy_interrupt_group.clone() { self.interrupt.intx = Some(VfioIntx { interrupt_source_group, enabled: false, }); self.enable_intx()?; } Ok(()) } fn update_msi_capabilities(&mut self, offset: u64, data: &[u8]) -> Result<(), VfioPciError> { match self.interrupt.update_msi(offset, data) { Some(InterruptUpdateAction::EnableMsi) => { // Disable INTx before we can enable MSI self.disable_intx(); self.enable_msi()?; } Some(InterruptUpdateAction::DisableMsi) => { // Fallback onto INTx when disabling MSI self.disable_msi(); self.enable_intx()?; } _ => {} } Ok(()) } fn update_msix_capabilities(&mut self, offset: u64, data: &[u8]) -> Result<(), VfioPciError> { match self.interrupt.update_msix(offset, data) { Some(InterruptUpdateAction::EnableMsix) => { // Disable INTx before we can enable MSI-X self.disable_intx(); self.enable_msix()?; } Some(InterruptUpdateAction::DisableMsix) => { // Fallback onto INTx when disabling MSI-X self.disable_msix(); self.enable_intx()?; } _ => {} } Ok(()) } fn find_region(&self, addr: u64) -> Option { for region in self.mmio_regions.iter() { if addr >= region.start.raw_value() && addr < region.start.unchecked_add(region.length).raw_value() { return Some(region.clone()); } } None } pub(crate) fn read_bar(&mut self, base: u64, offset: u64, data: &mut [u8]) { let addr = base + offset; if let Some(region) = self.find_region(addr) { let offset = addr - region.start.raw_value(); if self.interrupt.msix_table_accessed(region.index, offset) { self.interrupt.msix_read_table(offset, data); } else { self.vfio_wrapper.region_read(region.index, offset, data); } } // INTx EOI // The guest reading from the BAR potentially means the interrupt has // been received and can be acknowledged. if self.interrupt.intx_in_use() && let Err(e) = self.vfio_wrapper.unmask_irq(VFIO_PCI_INTX_IRQ_INDEX) { error!("Failed unmasking INTx IRQ: {e}"); } } pub(crate) fn write_bar( &mut self, base: u64, offset: u64, data: &[u8], ) -> Option> { let addr = base + offset; if let Some(region) = self.find_region(addr) { let offset = addr - region.start.raw_value(); // If the MSI-X table is written to, we need to update our cache. if self.interrupt.msix_table_accessed(region.index, offset) { self.interrupt.msix_write_table(offset, data); } else { self.vfio_wrapper.region_write(region.index, offset, data); } } // INTx EOI // The guest writing to the BAR potentially means the interrupt has // been received and can be acknowledged. if self.interrupt.intx_in_use() && let Err(e) = self.vfio_wrapper.unmask_irq(VFIO_PCI_INTX_IRQ_INDEX) { error!("Failed unmasking INTx IRQ: {e}"); } None } pub(crate) fn write_config_register( &mut self, reg_idx: usize, offset: u64, data: &[u8], ) -> (Vec, Option>) { // When the guest wants to write to a BAR, we trap it into // our local configuration space. We're not reprogramming // VFIO device. if (PCI_CONFIG_BAR0_INDEX..PCI_CONFIG_BAR0_INDEX + BAR_NUMS).contains(®_idx) || reg_idx == PCI_ROM_EXP_BAR_INDEX { // We keep our local cache updated with the BARs. // We'll read it back from there when the guest is asking // for BARs (see read_config_register()). return ( self.configuration .write_config_register(reg_idx, offset, data), None, ); } let reg = (reg_idx * PCI_CONFIG_REGISTER_SIZE) as u64; // If the MSI or MSI-X capabilities are accessed, we need to // update our local cache accordingly. // Depending on how the capabilities are modified, this could // trigger a VFIO MSI or MSI-X toggle. if let Some((cap_id, cap_base)) = self.interrupt.accessed(reg) { let cap_offset: u64 = reg - cap_base + offset; match cap_id { PciCapabilityId::MessageSignalledInterrupts => { if let Err(e) = self.update_msi_capabilities(cap_offset, data) { error!("Could not update MSI capabilities: {e}"); } } PciCapabilityId::MsiX => { if let Err(e) = self.update_msix_capabilities(cap_offset, data) { error!("Could not update MSI-X capabilities: {e}"); } } _ => {} } } // Make sure to write to the device's PCI config space after MSI/MSI-X // interrupts have been enabled/disabled. In case of MSI, when the // interrupts are enabled through VFIO (using VFIO_DEVICE_SET_IRQS), // the MSI Enable bit in the MSI capability structure found in the PCI // config space is disabled by default. That's why when the guest is // enabling this bit, we first need to enable the MSI interrupts with // VFIO through VFIO_DEVICE_SET_IRQS ioctl, and only after we can write // to the device region to update the MSI Enable bit. self.vfio_wrapper.write_config((reg + offset) as u32, data); // Return pending BAR repgrogramming if MSE bit is set let mut ret_param = self.configuration.pending_bar_reprogram(); if !ret_param.is_empty() { if self.read_config_register(COMMAND_REG) & COMMAND_REG_MEMORY_SPACE_MASK == COMMAND_REG_MEMORY_SPACE_MASK { info!("BAR reprogramming parameter is returned: {ret_param:x?}"); self.configuration.clear_pending_bar_reprogram(); } else { info!( "MSE bit is disabled. No BAR reprogramming parameter is returned: {ret_param:x?}" ); ret_param = Vec::new(); } } (ret_param, None) } pub(crate) fn read_config_register(&mut self, reg_idx: usize) -> u32 { // When reading the BARs, we trap it and return what comes // from our local configuration space. We want the guest to // use that and not the VFIO device BARs as it does not map // with the guest address space. if (PCI_CONFIG_BAR0_INDEX..PCI_CONFIG_BAR0_INDEX + BAR_NUMS).contains(®_idx) || reg_idx == PCI_ROM_EXP_BAR_INDEX { return self.configuration.read_reg(reg_idx); } if let Some(id) = self.get_msix_cap_idx() { let msix = self.interrupt.msix.as_mut().unwrap(); if reg_idx * 4 == id + 4 { return msix.cap.table; } else if reg_idx * 4 == id + 8 { return msix.cap.pba; } } // Since we don't support passing multi-functions devices, we should // mask the multi-function bit, bit 7 of the Header Type byte on the // register 3. let mask = if reg_idx == PCI_HEADER_TYPE_REG_INDEX { 0xff7f_ffff } else { 0xffff_ffff }; // The config register read comes from the VFIO device itself. let mut value = self.vfio_wrapper.read_config_dword((reg_idx * 4) as u32) & mask; if let Some(config_patch) = self.patches.get(®_idx) { value = (value & !config_patch.mask) | config_patch.patch; } value } fn state(&self) -> VfioCommonState { let intx_state = self.interrupt.intx.as_ref().map(|intx| IntxState { enabled: intx.enabled, }); let msi_state = self.interrupt.msi.as_ref().map(|msi| MsiState { cap: msi.cfg.cap, cap_offset: msi.cap_offset, }); let msix_state = self.interrupt.msix.as_ref().map(|msix| MsixState { cap: msix.cap, cap_offset: msix.cap_offset, bdf: msix.bar.devid, }); VfioCommonState { intx_state, msi_state, msix_state, } } fn set_state( &mut self, state: &VfioCommonState, msi_state: Option, msix_state: Option, ) -> Result<(), VfioPciError> { if let (Some(intx), Some(interrupt_source_group)) = (&state.intx_state, self.legacy_interrupt_group.clone()) { self.interrupt.intx = Some(VfioIntx { interrupt_source_group, enabled: false, }); if intx.enabled { self.enable_intx()?; } } if let Some(msi) = &state.msi_state { self.initialize_msi(msi.cap.msg_ctl, msi.cap_offset, msi_state); } if let Some(msix) = &state.msix_state { self.initialize_msix(msix.cap, msix.cap_offset, msix.bdf.into(), msix_state); } Ok(()) } } impl Pausable for VfioCommon {} impl Snapshottable for VfioCommon { fn id(&self) -> String { String::from(VFIO_COMMON_ID) } fn snapshot(&mut self) -> result::Result { let mut vfio_common_snapshot = Snapshot::new_from_state(&self.state())?; // Snapshot PciConfiguration vfio_common_snapshot.add_snapshot(self.configuration.id(), self.configuration.snapshot()?); // Snapshot MSI if let Some(msi) = &mut self.interrupt.msi { vfio_common_snapshot.add_snapshot(msi.cfg.id(), msi.cfg.snapshot()?); } // Snapshot MSI-X if let Some(msix) = &mut self.interrupt.msix { vfio_common_snapshot.add_snapshot(msix.bar.id(), msix.bar.snapshot()?); } Ok(vfio_common_snapshot) } } /// VfioPciDevice represents a VFIO PCI device. /// This structure implements the BusDevice and PciDevice traits. /// /// A VfioPciDevice is bound to a VfioDevice and is also a PCI device. /// The VMM creates a VfioDevice, then assigns it to a VfioPciDevice, /// which then gets added to the PCI bus. pub struct VfioPciDevice { id: String, vm: Arc, device: Arc, vfio_ops: Arc, common: VfioCommon, iommu_attached: bool, // Whether to map VFIO device MMIO BARs into the host IOMMU address space. // Required for peer-to-peer DMA between VFIO devices. p2p_dma: bool, memory_slot_allocator: MemorySlotAllocator, bdf: PciBdf, device_path: PathBuf, } impl VfioPciDevice { /// Constructs a new Vfio Pci device for the given Vfio device #[expect(clippy::too_many_arguments)] pub fn new( id: String, vm: Arc, device: VfioDevice, vfio_ops: Arc, msi_interrupt_manager: Arc>, legacy_interrupt_group: Option>, iommu_attached: bool, p2p_dma: bool, bdf: PciBdf, memory_slot_allocator: MemorySlotAllocator, snapshot: Option<&Snapshot>, x_nv_gpudirect_clique: Option, x_exclude_mmap_bars: Vec, device_path: PathBuf, ) -> Result { let device = Arc::new(device); device.reset(); let vfio_wrapper = VfioDeviceWrapper::new(Arc::clone(&device)); let common = VfioCommon::new( msi_interrupt_manager, legacy_interrupt_group, Arc::new(vfio_wrapper) as Arc, &PciVfioSubclass::VfioSubclass, bdf, vm_migration::snapshot_from_id(snapshot, VFIO_COMMON_ID), VfioCommonConfig { x_nv_gpudirect_clique, x_exclude_mmap_bars, }, )?; let vfio_pci_device = VfioPciDevice { id, vm, device, vfio_ops, common, iommu_attached, p2p_dma, memory_slot_allocator, bdf, device_path, }; Ok(vfio_pci_device) } pub fn iommu_attached(&self) -> bool { self.iommu_attached } fn generate_sparse_areas( caps: &[VfioRegionInfoCap], region_index: u32, region_start: u64, region_size: u64, vfio_msix: Option<&VfioMsix>, ) -> Result, VfioPciError> { for cap in caps { match cap { VfioRegionInfoCap::SparseMmap(sparse_mmap) => return Ok(sparse_mmap.areas.clone()), VfioRegionInfoCap::MsixMappable => { if !is_4k_aligned(region_start) { error!( "Region start address 0x{region_start:x} must be at least aligned on 4KiB" ); return Err(VfioPciError::RegionAlignment); } if !is_4k_multiple(region_size) { error!("Region size 0x{region_size:x} must be at least a multiple of 4KiB"); return Err(VfioPciError::RegionSize); } // In case the region contains the MSI-X vectors table or // the MSI-X PBA table, we must calculate the subregions // around them, leading to a list of sparse areas. // We want to make sure we will still trap MMIO accesses // to these MSI-X specific ranges. If these region don't align // with pagesize, we can achieve it by enlarging its range. // // Using a BtreeMap as the list provided through the iterator is sorted // by key. This ensures proper split of the whole region. let mut inter_ranges = BTreeMap::new(); if let Some(msix) = vfio_msix { if region_index == msix.cap.table_bir() { let (offset, size) = msix.cap.table_range(); let offset = align_page_size_down(offset); let size = align_page_size_up(size); // MSI-X mmap region safety: when a device has a non page // aligned MSI-X offset, fixup_msix_region() relocates MSI-X // to the upper half of an enlarged virtual BAR, causing the // offsets in msix.cap to exceed the physical BAR size. This // check skips carving a hole, preventing invalid offsets from // reaching the mmap path. With no holes, // generate_sparse_areas() returns a single sparse region // covering the entire physical BAR. The relocated MSI-X in // the virtual BAR remains trapped because its upper half has // no mmap backing. Exposing the physical MSI-X region through // mmap is safe when the kernel advertises // VFIO_REGION_INFO_CAP_MSIX_MAPPABLE. When MSI-X offsets are // already page aligned, fixup_msix_region() does not relocate // and this check is satisfied, so a hole is carved at the // intended offset as before. if offset < region_size { inter_ranges.insert(offset, size); } } if region_index == msix.cap.pba_bir() { let (offset, size) = msix.cap.pba_range(); let offset = align_page_size_down(offset); let size = align_page_size_up(size); // See MSI-X mmap safety comment above. if offset < region_size { inter_ranges.insert(offset, size); } } } let mut sparse_areas = Vec::new(); let mut current_offset = 0; for (range_offset, range_size) in inter_ranges { if range_offset > current_offset { sparse_areas.push(VfioRegionSparseMmapArea { offset: current_offset, size: range_offset - current_offset, }); } current_offset = align_page_size_down(range_offset + range_size); } if region_size > current_offset { sparse_areas.push(VfioRegionSparseMmapArea { offset: current_offset, size: region_size - current_offset, }); } return Ok(sparse_areas); } _ => {} } } // In case no relevant capabilities have been found, create a single // sparse area corresponding to the entire MMIO region. Ok(vec![VfioRegionSparseMmapArea { offset: 0, size: region_size, }]) } /// Map MMIO regions into the guest, and avoid VM exits when the guest tries /// to reach those regions. /// /// # Arguments /// /// * `vm` - The VM object. It is used to set the VFIO MMIO regions /// as user memory regions. /// * `mem_slot` - The closure to return a memory slot. pub fn map_mmio_regions(&mut self) -> Result<(), VfioPciError> { let fd = self.device.as_raw_fd(); // SAFETY: fd is guaranteed valid let fd = unsafe { BorrowedFd::borrow_raw(fd) }; for region in self.common.mmio_regions.iter_mut() { if self .common .x_exclude_mmap_bars .contains(&(region.index as u8)) { info!( "Skipping VFIO BAR mmap and P2P DMA mapping for device {} at {} BAR {} (size = 0x{:x})", self.bdf, self.device_path.display(), region.index, region.length ); continue; } let region_flags = self.device.get_region_flags(region.index); if region_flags & VFIO_REGION_INFO_FLAG_MMAP != 0 { let mut prot = 0; if region_flags & VFIO_REGION_INFO_FLAG_READ != 0 { prot |= libc::PROT_READ; } if region_flags & VFIO_REGION_INFO_FLAG_WRITE != 0 { prot |= libc::PROT_WRITE; } // Retrieve the list of capabilities found on the region let caps = if region_flags & VFIO_REGION_INFO_FLAG_CAPS != 0 { self.device.get_region_caps(region.index) } else { Vec::new() }; // Don't try to mmap the region if it contains MSI-X table or // MSI-X PBA subregion, and if we couldn't find MSIX_MAPPABLE // in the list of supported capabilities. if let Some(msix) = self.common.interrupt.msix.as_ref() && (region.index == msix.cap.table_bir() || region.index == msix.cap.pba_bir()) && !caps.contains(&VfioRegionInfoCap::MsixMappable) { continue; } let mmap_size = self.device.get_region_size(region.index); let mmap_offset = self.device.get_region_offset(region.index); let sparse_areas = Self::generate_sparse_areas( &caps, region.index, region.start.0, mmap_size, self.common.interrupt.msix.as_ref(), )?; let page_size = get_page_size(); for area in sparse_areas.iter() { // KVM_SET_USER_MEMORY_REGION requires memory_size to be a // multiple of the host page size. On aarch64 with 64K pages // a device BAR can be smaller than a page (e.g. 16K NVMe // BAR). // // The kernel only sets VFIO_REGION_INFO_FLAG_MMAP on sub-page // BARs after verifying the physical BAR start is page-aligned // and reserving the rest of the page. Expansion is only safe // at offset 0 where the kernel reservation applies. // // fixup_msix_region() ensures MSI-X relocation at >= page_size // offset, so the expanded mmap cannot overlap the trap region. let mmap_len = if area.size < page_size { if area.offset != 0 { error!( "BAR {}: sub-page sparse area at non-zero offset 0x{:x} \ cannot be safely expanded to page size", region.index, area.offset, ); return Err(VfioPciError::MmapArea); } info!( "BAR {}: expanding sub-page sparse area mmap from 0x{:x} to \ page size 0x{:x}", region.index, area.size, page_size, ); page_size } else { area.size }; let mapping = match MmapRegion::mmap( mmap_len, prot, fd, mmap_offset, area.offset, ) { Ok(mapping) => mapping, Err(_) => { error!( "Could not mmap sparse area (offset = 0x{:x}, size = 0x{:x}): {}", mmap_offset, mmap_len, io::Error::last_os_error() ); return Err(VfioPciError::MmapArea); } }; let user_memory_region = UserMemoryRegion { slot: self.memory_slot_allocator.next_memory_slot(), start: region.start.0 + area.offset, mapping: Arc::new(mapping), }; // SAFETY: MmapRegion invariants guarantee that // user_memory_region.mapping.addr() points to // user_memory_region.mapping.len() bytes of // valid memory that will only be unmapped with munmap(). unsafe { self.vm.create_user_memory_region( user_memory_region.slot, user_memory_region.start, user_memory_region.mapping.len(), user_memory_region.mapping.addr(), false, false, ) } .map_err(VfioPciError::CreateUserMemoryRegion)?; // Map the MMIO BAR into the host IOMMU address space via VfioOps // Only needed if p2p_dma is enabled. if !self.iommu_attached && self.p2p_dma { // vfio_dma_map should be unsafe but isn't. // SAFETY: MmapRegion invariants guarantee that // user_memory_region.mapping.addr() points to // user_memory_region.mapping.len() bytes of // valid memory that will only be unmapped with munmap(). unsafe { self.vfio_ops.vfio_dma_map( user_memory_region.start, user_memory_region.mapping.len(), user_memory_region.mapping.addr(), ) } .map_err(|e| VfioPciError::DmaMap(e, self.device_path.clone(), self.bdf))?; } region.user_memory_regions.push(user_memory_region); } } } Ok(()) } pub fn unmap_mmio_regions(&mut self) { for region in self.common.mmio_regions.iter_mut() { for user_memory_region in region.user_memory_regions.drain(..) { let len = user_memory_region.mapping.len(); let host_addr = user_memory_region.mapping.addr(); // Unmap MMIO region from the host IOMMU address space via VfioOps // Only needed if p2p_dma is enabled. if !self.iommu_attached && self.p2p_dma && let Err(e) = self .vfio_ops .vfio_dma_unmap(user_memory_region.start, len) .map_err(|e| VfioPciError::DmaUnmap(e, self.device_path.clone(), self.bdf)) { error!( "Could not unmap MMIO region from the host IOMMU address space: \ iova 0x{:x}, size 0x{:x}: {}, ", user_memory_region.start, len, e ); } // Remove region // SAFETY: only valid entries are added to the user_memory_regions field // of the entries of self.common.mmio_regions. // Also, host_addr..host_addr + len is valid by the MmapRegion invariants. if let Err(e) = unsafe { self.vm.remove_user_memory_region( user_memory_region.slot, user_memory_region.start, len, host_addr, false, false, ) } { error!("Could not remove the userspace memory region: {e}"); } self.memory_slot_allocator .free_memory_slot(user_memory_region.slot); } } } pub fn mmio_regions(&self) -> Vec { self.common.mmio_regions.clone() } } impl Drop for VfioPciDevice { fn drop(&mut self) { self.unmap_mmio_regions(); if let Some(msix) = &self.common.interrupt.msix && msix.bar.enabled() { self.common.disable_msix(); } if let Some(msi) = &self.common.interrupt.msi && msi.cfg.enabled() { self.common.disable_msi(); } if self.common.interrupt.intx_in_use() { self.common.disable_intx(); } } } impl BusDevice for VfioPciDevice { fn read(&mut self, base: u64, offset: u64, data: &mut [u8]) { self.read_bar(base, offset, data); } fn write(&mut self, base: u64, offset: u64, data: &[u8]) -> Option> { self.write_bar(base, offset, data) } } // Offset of the 16-bit status register in the PCI configuration space. const PCI_CONFIG_STATUS_OFFSET: u32 = 0x06; // Status bit indicating the presence of a capabilities list. const PCI_CONFIG_STATUS_CAPABILITIES_LIST: u16 = 1 << 4; // First BAR offset in the PCI config space. const PCI_CONFIG_BAR_OFFSET: u32 = 0x10; // Capability register offset in the PCI config space. const PCI_CONFIG_CAPABILITY_OFFSET: u32 = 0x34; // The valid bits for the capabilities pointer. const PCI_CONFIG_CAPABILITY_PTR_MASK: u8 = !0b11; // Extended capabilities register offset in the PCI config space. const PCI_CONFIG_EXTENDED_CAPABILITY_OFFSET: u32 = 0x100; // IO BAR when first BAR bit is 1. const PCI_CONFIG_IO_BAR: u32 = 0x1; // 64-bit memory bar flag. const PCI_CONFIG_MEMORY_BAR_64BIT: u32 = 0x4; // Prefetchable BAR bit const PCI_CONFIG_BAR_PREFETCHABLE: u32 = 0x8; // PCI config register size (4 bytes). const PCI_CONFIG_REGISTER_SIZE: usize = 4; // Number of BARs for a PCI device const BAR_NUMS: usize = 6; // PCI Header Type register index const PCI_HEADER_TYPE_REG_INDEX: usize = 3; // First BAR register index const PCI_CONFIG_BAR0_INDEX: usize = 4; // PCI ROM expansion BAR register index const PCI_ROM_EXP_BAR_INDEX: usize = 12; impl PciDevice for VfioPciDevice { fn allocate_bars( &mut self, allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, mmio64_allocator: &mut AddressAllocator, resources: Option>, ) -> Result, PciDeviceError> { self.common.allocate_bars( allocator, mmio32_allocator, mmio64_allocator, resources.as_deref(), ) } fn free_bars( &mut self, allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, mmio64_allocator: &mut AddressAllocator, ) -> Result<(), PciDeviceError> { self.common .free_bars(allocator, mmio32_allocator, mmio64_allocator) } fn write_config_register( &mut self, reg_idx: usize, offset: u64, data: &[u8], ) -> (Vec, Option>) { self.common.write_config_register(reg_idx, offset, data) } fn read_config_register(&mut self, reg_idx: usize) -> u32 { self.common.read_config_register(reg_idx) } fn read_bar(&mut self, base: u64, offset: u64, data: &mut [u8]) { self.common.read_bar(base, offset, data); } fn write_bar(&mut self, base: u64, offset: u64, data: &[u8]) -> Option> { self.common.write_bar(base, offset, data) } fn move_bar(&mut self, old_base: u64, new_base: u64) -> Result<(), io::Error> { for region in self.common.mmio_regions.iter_mut() { if region.start.raw_value() == old_base { region.start = GuestAddress(new_base); for user_memory_region in region.user_memory_regions.iter_mut() { let len = user_memory_region.mapping.len(); let host_addr = user_memory_region.mapping.addr(); // Unmap the old MMIO region from the host IOMMU address space via VfioOps // Only needed if p2p_dma is enabled. if !self.iommu_attached && self.p2p_dma && let Err(e) = self .vfio_ops .vfio_dma_unmap(user_memory_region.start, len) .map_err(|e| { VfioPciError::DmaUnmap(e, self.device_path.clone(), self.bdf) }) { error!( "Could not unmap MMIO region from the host IOMMU address space: \ iova 0x{:x}, size 0x{:x}: {}, ", user_memory_region.start, len, e ); } // Remove old region // SAFETY: MmapRegion invariants guarantee that // host_addr points to len bytes of // valid memory that will only be unmapped with munmap(). unsafe { self.vm.remove_user_memory_region( user_memory_region.slot, user_memory_region.start, len, host_addr, false, false, ) } .map_err(io::Error::other)?; // Update the user memory region with the correct start address. if new_base > old_base { user_memory_region.start += new_base - old_base; } else { user_memory_region.start -= old_base - new_base; } // Insert new region // SAFETY: MmapRegion invariants guarantee that // host_addr points to len bytes of // valid memory that will only be unmapped with munmap(). unsafe { self.vm.create_user_memory_region( user_memory_region.slot, user_memory_region.start, len, host_addr, false, false, ) } .map_err(io::Error::other)?; // Map the moved MMIO region into the host IOMMU address space via VfioOps // Only needed if p2p_dma is enabled. if !self.iommu_attached && self.p2p_dma { // vfio_dma_map is unsound and ought to be marked as unsafe // SAFETY: MmapRegion invariants guarantee that // host_addr points to len bytes of // valid memory that will only be unmapped with munmap(). unsafe { self.vfio_ops .vfio_dma_map(user_memory_region.start, len, host_addr) } .map_err(|e| VfioPciError::DmaMap(e, self.device_path.clone(), self.bdf)) .map_err(|e| { io::Error::other(format!( "Could not map MMIO region into the host IOMMU address space: \ iova 0x{:x}, size 0x{:x}: {}, ", user_memory_region.start, len, e )) })?; } } } } Ok(()) } fn restore_bar_addr(&mut self, params: &BarReprogrammingParams) { self.common.configuration.restore_bar_addr(params); } fn as_any_mut(&mut self) -> &mut dyn Any { self } fn id(&self) -> Option { Some(self.id.clone()) } } impl Pausable for VfioPciDevice {} impl Snapshottable for VfioPciDevice { fn id(&self) -> String { self.id.clone() } fn snapshot(&mut self) -> result::Result { let mut vfio_pci_dev_snapshot = Snapshot::default(); // Snapshot VfioCommon vfio_pci_dev_snapshot.add_snapshot(self.common.id(), self.common.snapshot()?); Ok(vfio_pci_dev_snapshot) } } impl Transportable for VfioPciDevice {} impl Migratable for VfioPciDevice {} /// This structure implements the ExternalDmaMapping trait. It is meant to /// be used when the caller tries to provide a way to update the mappings /// associated with a specific VfioOps instance. pub struct VfioDmaMapping { vfio_ops: Arc, memory: Arc, mmio_regions: Arc>>, } impl VfioDmaMapping { /// Create a DmaMapping object. /// # Parameters /// * `vfio_ops`: VfioOps instance. /// * `memory`: guest memory to mmap. /// * `mmio_regions`: mmio_regions to mmap. pub fn new( vfio_ops: Arc, memory: Arc, mmio_regions: Arc>>, ) -> Self { VfioDmaMapping { vfio_ops, memory, mmio_regions, } } } impl ExternalDmaMapping for VfioDmaMapping { fn map(&self, iova: u64, gpa: u64, size: u64) -> result::Result<(), io::Error> { let Ok(usize_size): Result = size.try_into() else { return Err(io::Error::other(format!("size {size} overflows usize"))); }; let mem = self.memory.memory(); let guest_addr = GuestAddress(gpa); let user_addr = if mem.check_range(guest_addr, usize_size) { match mem.get_slice(guest_addr, usize_size) { Ok(t) => { assert!(t.len() >= usize_size); Ok(t.ptr_guard_mut()) } Err(e) => { return Err(io::Error::other(format!( "unable to retrieve user address for gpa 0x{gpa:x} from guest memory region: {e}" ))); } } } else if self.mmio_regions.lock().unwrap().check_range(gpa, size) { Err(self .mmio_regions .lock() .unwrap() .find_user_address(gpa, size)?) } else { return Err(io::Error::other(format!( "failed to locate guest address 0x{gpa:x} in guest memory" ))); }; let user_addr = match user_addr { Ok(p) => p.as_ptr(), Err(p) => p, }; // vfio_dma_map is unsound and ought to be marked as unsafe // SAFETY: find_user_address and GuestMemory::get_slice() guarantee that // the returned pointer is valid for up to `usize_size` bytes. // `usize_size` is always equal to `size` due to the above `try_into()` call. unsafe { self.vfio_ops.vfio_dma_map(iova, size as usize, user_addr) }.map_err(|e| { io::Error::other(format!( "failed to map memory into the host IOMMU address space, \ iova 0x{iova:x}, gpa 0x{gpa:x}, size 0x{size:x}: {e:?}" )) }) } fn unmap(&self, iova: u64, size: u64) -> result::Result<(), io::Error> { self.vfio_ops .vfio_dma_unmap(iova, size as usize) .map_err(|e| { io::Error::other(format!( "failed to unmap memory from the host IOMMU address space, \ iova 0x{iova:x}, size 0x{size:x}: {e:?}" )) }) } }