Skip to main content

pcie/
port.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Common PCIe port implementation shared between different port types.
5
6use anyhow::bail;
7use chipset_device::io::IoError;
8use chipset_device::io::IoResult;
9use chipset_device::mmio::RegisterMmioIntercept;
10use cxl_spec::CxlComponentRegisters;
11use cxl_spec::CxlFlexBusPortDvsecExtendedCapability;
12use cxl_spec::CxlPortDvsecExtendedCapability;
13use cxl_spec::CxlRegisterLocatorDvsecExtendedCapability;
14use cxl_spec::pci_registers::spec::flex_bus_port_dvsec::CxlFlexBusPortDvsecCapability;
15use cxl_spec::pci_registers::spec::register_locator_dvsec::CxlRegisterLocatorRegisterBir;
16use cxl_spec::pci_registers::spec::register_locator_dvsec::CxlRegisterLocatorRegisterBlockIdentifier;
17use cxl_spec::spec::CXL_COMPONENT_REGISTERS_SIZE_BYTES;
18use inspect::Inspect;
19use pci_bus::GenericPciBusDevice;
20use pci_core::bus_range::AssignedBusRange;
21use pci_core::capabilities::extended::PciExtendedCapability;
22use pci_core::capabilities::extended::acs::AcsExtendedCapability;
23use pci_core::capabilities::msi_cap::MsiCapability;
24use pci_core::capabilities::pci_express::PciExpressCapability;
25use pci_core::cfg_space_emu::BarMemoryKind;
26use pci_core::cfg_space_emu::ConfigSpaceType1Emulator;
27use pci_core::cfg_space_emu::DeviceBars;
28use pci_core::msi::MsiTarget;
29use pci_core::spec::caps::pci_express::DevicePortType;
30use pci_core::spec::hwid::HardwareIds;
31use std::sync::Arc;
32use vmcore::save_restore::RestoreError;
33use vmcore::save_restore::SaveError;
34use vmcore::save_restore::SaveRestore;
35
36const ACS_CAPABILITY_IMPLEMENTED_MASK: u16 = 0x00df;
37const ACS_CAPABILITY_ALLOWED_ROOT_OR_DSP_MASK: u16 = 0x00ff;
38const ACS_CAPABILITY_ALLOWED_USP_MASK: u16 = 0x0000;
39
40type CxlComponentRegistersSavedState = <CxlComponentRegisters as SaveRestore>::SavedState;
41const CXL_COMPONENT_ALLOWED_ACCESS_SIZES: [usize; 2] = [4, 8];
42
43fn validate_cxl_component_register_access(offset: u64, len: usize) -> Result<(), IoError> {
44    if !CXL_COMPONENT_ALLOWED_ACCESS_SIZES.contains(&len) {
45        return Err(IoError::InvalidAccessSize);
46    }
47
48    if !offset.is_multiple_of(len as u64) {
49        return Err(IoError::UnalignedAccess);
50    }
51
52    Ok(())
53}
54
55/// Express-level settings for a PCIe port.
56///
57/// Collects feature flags that apply uniformly to a port so that adding new
58/// capabilities does not require changing every constructor signature.
59#[derive(Debug, Default, Clone)]
60pub struct PciePortSettings {
61    /// ACS capability bits to advertise on this port. `0` means the ACS
62    /// extended capability is not present.
63    pub acs_capabilities_supported: u16,
64
65    /// Flex Bus Port capability bits used to advertise CXL support on ports.
66    ///
67    /// CXL DVSECs are added only when this is `Some` and either `cache_capable`
68    /// or `mem_capable` is set.
69    pub cxl_flex_bus_port_capability: Option<CxlFlexBusPortDvsecCapability>,
70}
71
72/// Generic PCIe port BAR definition.
73#[derive(Clone)]
74pub struct PortBarDefinition {
75    /// BAR index (Type-1 headers currently support BAR0/BAR1 only).
76    pub index: u8,
77    /// Total BAR size in bytes.
78    pub size_bytes: u64,
79    /// BAR subregions used to dispatch MMIO accesses.
80    pub subregions: Vec<PortBarSubregionDefinition>,
81}
82
83/// Generic PCIe port BAR subregion definition.
84#[derive(Clone)]
85pub struct PortBarSubregionDefinition {
86    /// Subregion semantic kind.
87    pub kind: PortBarSubregionKind,
88    /// Subregion offset within BAR aperture.
89    pub offset: u64,
90    /// Subregion length in bytes.
91    pub size_bytes: u64,
92}
93
94/// Generic PCIe port BAR subregion kind.
95#[derive(Copy, Clone, Eq, PartialEq, Debug)]
96pub enum PortBarSubregionKind {
97    /// CXL component register space.
98    CxlComponentRegisters,
99    /// MSI-X table subregion.
100    MsiXTable,
101    /// MSI-X pending bit array subregion.
102    MsiXPba,
103}
104
105pub(crate) fn build_cxl_register_locator_extended_capability(
106    register_bir: CxlRegisterLocatorRegisterBir,
107    register_block_offset: u64,
108) -> Option<Box<dyn PciExtendedCapability>> {
109    // Build the single-block CXL Register Locator DVSEC and drop invalid layouts.
110    let locator = CxlRegisterLocatorDvsecExtendedCapability::new()
111        .with_register_block(
112            register_bir,
113            CxlRegisterLocatorRegisterBlockIdentifier::COMPONENT_REGISTERS,
114            register_block_offset,
115        )
116        .ok()?;
117
118    Some(Box::new(locator))
119}
120
121/// Maps a Type-1 BAR index to the corresponding CXL Register Locator BIR enum.
122///
123/// Returns `None` for unsupported BAR numbers.
124fn register_bir_from_bar_index(index: u8) -> Option<CxlRegisterLocatorRegisterBir> {
125    match index {
126        0 => Some(CxlRegisterLocatorRegisterBir::BAR_10H),
127        1 => Some(CxlRegisterLocatorRegisterBir::BAR_14H),
128        2 => Some(CxlRegisterLocatorRegisterBir::BAR_18H),
129        3 => Some(CxlRegisterLocatorRegisterBir::BAR_1CH),
130        4 => Some(CxlRegisterLocatorRegisterBir::BAR_20H),
131        5 => Some(CxlRegisterLocatorRegisterBir::BAR_24H),
132        _ => None,
133    }
134}
135
136/// Extracts CXL Register Locator settings from the configured BAR layout.
137///
138/// The locator is only emitted when a CXL component-register subregion exists and
139/// the BAR index can be represented in CXL BIR encoding.
140fn cxl_register_locator_from_bar(
141    bar: Option<&PortBarDefinition>,
142) -> Option<(CxlRegisterLocatorRegisterBir, u64)> {
143    let bar_cfg = bar?;
144    let component_subregion = bar_cfg
145        .subregions
146        .iter()
147        .find(|region| region.kind == PortBarSubregionKind::CxlComponentRegisters)?;
148
149    let Some(register_bir) = register_bir_from_bar_index(bar_cfg.index) else {
150        tracelimit::warn_ratelimited!(
151            bar_index = bar_cfg.index,
152            "unsupported BAR index for CXL Register Locator"
153        );
154        return None;
155    };
156
157    Some((register_bir, component_subregion.offset))
158}
159
160fn has_cxl_component_register_subregion(bar: Option<&PortBarDefinition>) -> bool {
161    bar.is_some_and(|bar_cfg| {
162        bar_cfg
163            .subregions
164            .iter()
165            .any(|region| region.kind == PortBarSubregionKind::CxlComponentRegisters)
166    })
167}
168
169fn drop_cxl_component_register_subregions(bar: &mut Option<PortBarDefinition>) {
170    if let Some(bar_cfg) = bar {
171        bar_cfg
172            .subregions
173            .retain(|region| region.kind != PortBarSubregionKind::CxlComponentRegisters);
174
175        if bar_cfg.subregions.is_empty() {
176            *bar = None;
177        }
178    }
179}
180
181fn default_bar_from_settings(settings: &PciePortSettings) -> Option<PortBarDefinition> {
182    let cxl_requested = settings
183        .cxl_flex_bus_port_capability
184        .is_some_and(|capability| capability.cache_capable() || capability.mem_capable());
185
186    cxl_requested.then_some(PortBarDefinition {
187        index: 0,
188        size_bytes: CXL_COMPONENT_REGISTERS_SIZE_BYTES,
189        subregions: vec![PortBarSubregionDefinition {
190            kind: PortBarSubregionKind::CxlComponentRegisters,
191            offset: 0,
192            size_bytes: CXL_COMPONENT_REGISTERS_SIZE_BYTES,
193        }],
194    })
195}
196
197/// Filters requested ACS bits by both implementation support and port-type policy.
198pub(crate) fn filter_acs_capabilities_for_bridge(
199    port_type: &DevicePortType,
200    requested: u16,
201) -> u16 {
202    let type_mask = match port_type {
203        DevicePortType::RootPort | DevicePortType::DownstreamSwitchPort => {
204            ACS_CAPABILITY_ALLOWED_ROOT_OR_DSP_MASK
205        }
206        DevicePortType::UpstreamSwitchPort => ACS_CAPABILITY_ALLOWED_USP_MASK,
207        DevicePortType::Endpoint => 0,
208    };
209
210    requested & ACS_CAPABILITY_IMPLEMENTED_MASK & type_mask
211}
212
213/// A common PCIe downstream facing port implementation that handles device connections and configuration forwarding.
214///
215/// This struct contains the common functionality shared between RootPort and DownstreamSwitchPort,
216/// including device connection management and configuration space forwarding logic.
217#[derive(Inspect)]
218pub struct PcieDownstreamPort {
219    /// The name of this port.
220    pub name: String,
221
222    /// The configuration space emulator for this port.
223    pub cfg_space: ConfigSpaceType1Emulator,
224
225    /// The connected device, if any.
226    #[inspect(skip)]
227    pub link: Option<(Arc<str>, Box<dyn GenericPciBusDevice>)>,
228
229    /// Optional BAR layout for this port.
230    #[inspect(skip)]
231    bar: Option<PortBarDefinition>,
232
233    /// Optional CXL component-register backing for CXL BAR subregion emulation.
234    #[inspect(skip)]
235    cxl_component_registers: Option<CxlComponentRegisters>,
236}
237
238impl PcieDownstreamPort {
239    /// Creates a new PCIe port with the specified hardware configuration and optional multi-function flag.
240    ///
241    /// # Arguments
242    /// * `name` - The name for this port
243    /// * `hardware_ids` - Hardware identifiers for the port
244    /// * `port_type` - The PCIe port type (root port, downstream switch port, etc.)
245    /// * `multi_function` - Whether this port should have the multi-function flag set
246    /// * `hotplug_slot_number` - The slot number for hotplug support. `Some(slot_number)` enables hotplug, `None` disables it
247    /// * `msi_target` - MSI target for interrupt delivery
248    /// * `settings` - Express-level port settings (ACS, etc.)
249    pub fn new(
250        name: impl Into<String>,
251        hardware_ids: HardwareIds,
252        port_type: DevicePortType,
253        multi_function: bool,
254        hotplug_slot_number: Option<u32>,
255        msi_target: &MsiTarget,
256        settings: PciePortSettings,
257        register_mmio: Option<&mut dyn RegisterMmioIntercept>,
258        mut bar: Option<PortBarDefinition>,
259    ) -> Self {
260        let port_name = name.into();
261        let mut bars = DeviceBars::new();
262        let mut cxl_component_registers = None;
263        let mut cxl_locator_capability = None;
264        if bar.is_none() {
265            bar = default_bar_from_settings(&settings);
266        }
267
268        // CXL DVSECs are exposed only when the port advertises cache/mem capability.
269        let mut cxl_enabled = settings
270            .cxl_flex_bus_port_capability
271            .is_some_and(|capability| capability.cache_capable() || capability.mem_capable());
272
273        let requested_cxl_component_subregion = has_cxl_component_register_subregion(bar.as_ref());
274
275        if cxl_enabled && requested_cxl_component_subregion {
276            if let Some((register_bir, register_block_offset)) =
277                cxl_register_locator_from_bar(bar.as_ref())
278            {
279                if let Some(locator_capability) = build_cxl_register_locator_extended_capability(
280                    register_bir,
281                    register_block_offset,
282                ) {
283                    cxl_locator_capability = Some(locator_capability);
284                    cxl_component_registers = Some(CxlComponentRegisters::new());
285                } else {
286                    tracelimit::warn_ratelimited!(
287                        offset = register_block_offset,
288                        "invalid CXL Register Locator settings; disabling CXL BAR subregion"
289                    );
290                    drop_cxl_component_register_subregions(&mut bar);
291                    cxl_enabled = false;
292                }
293            } else {
294                tracelimit::warn_ratelimited!(
295                    "invalid CXL Register Locator BAR configuration; disabling CXL BAR subregion"
296                );
297                drop_cxl_component_register_subregions(&mut bar);
298                cxl_enabled = false;
299            }
300        }
301
302        // Materialize BAR intercept plumbing only when the caller provides BAR metadata.
303        if let Some(bar_cfg) = &bar {
304            if bar_cfg.index != 0 {
305                tracelimit::warn_ratelimited!(
306                    bar_index = bar_cfg.index,
307                    "ignoring unsupported BAR index; only BAR0 is supported"
308                );
309                bar = None;
310            } else if let Some(register_mmio) = register_mmio {
311                let region_name = format!("{}-bar{}", port_name, bar_cfg.index);
312                bars = bars.bar0(
313                    bar_cfg.size_bytes,
314                    BarMemoryKind::Intercept(
315                        register_mmio.new_io_region(&region_name, bar_cfg.size_bytes),
316                    ),
317                );
318            } else {
319                tracelimit::warn_ratelimited!(
320                    "ignoring BAR configuration because MMIO register context is missing"
321                );
322                bar = None;
323            }
324        }
325
326        // If CXL component-register emulation was requested but BAR MMIO interception could
327        // not be materialized, disable CXL DVSECs so config space matches emulation behavior.
328        if cxl_enabled && requested_cxl_component_subregion && bar.is_none() {
329            tracelimit::warn_ratelimited!(
330                "dropping CXL DVSECs because BAR MMIO interception is unavailable"
331            );
332            cxl_enabled = false;
333            cxl_locator_capability = None;
334            cxl_component_registers = None;
335        }
336
337        let (hotplug, slot_number) = match hotplug_slot_number {
338            Some(slot) => (true, Some(slot)),
339            None => (false, None),
340        };
341
342        let msi_capability = MsiCapability::new(0, true, false, msi_target);
343        let acs_supported =
344            filter_acs_capabilities_for_bridge(&port_type, settings.acs_capabilities_supported);
345
346        let pcie_cap = if hotplug {
347            let slot_num = slot_number.unwrap_or(0);
348            PciExpressCapability::new(port_type, None).with_hotplug_support(slot_num)
349        } else {
350            PciExpressCapability::new(port_type, None)
351        };
352
353        let extended_capabilities = if acs_supported != 0 {
354            vec![
355                Box::new(AcsExtendedCapability::with_capabilities(acs_supported))
356                    as Box<dyn PciExtendedCapability>,
357            ]
358        } else {
359            vec![]
360        };
361
362        let mut extended_capabilities = extended_capabilities;
363
364        if cxl_enabled {
365            // CXL Spec mandates that a CXL root port or downstream switch port must have CXL Port DVSEC
366            // and CXL Flex Bus Port DVSEC.
367            extended_capabilities.push(Box::new(CxlPortDvsecExtendedCapability::new()));
368            let mut flex_bus_dvsec = CxlFlexBusPortDvsecExtendedCapability::new();
369            if let Some(capability) = settings.cxl_flex_bus_port_capability {
370                flex_bus_dvsec = flex_bus_dvsec
371                    .with_cache_capable(capability.cache_capable())
372                    .with_mem_capable(capability.mem_capable())
373                    .with_optional_capabilities(
374                        capability.cache_capable(),
375                        capability.cxl_68b_flit_and_vh_capable(),
376                        capability.cxl_multi_logical_device_capable(),
377                        capability.cxl_latency_optimized_256b_flit_capable(),
378                        capability.cxl_pbr_flit_capable(),
379                    );
380            }
381            extended_capabilities.push(Box::new(flex_bus_dvsec));
382
383            if let Some(locator_capability) = cxl_locator_capability {
384                extended_capabilities.push(locator_capability);
385            }
386        }
387
388        let cfg_space = ConfigSpaceType1Emulator::new_with_bars(
389            hardware_ids,
390            vec![Box::new(pcie_cap), Box::new(msi_capability)],
391            extended_capabilities,
392            bars,
393        )
394        .with_multi_function_bit(multi_function);
395
396        Self {
397            name: port_name,
398            cfg_space,
399            link: None,
400            bar,
401            cxl_component_registers,
402        }
403    }
404
405    /// Resolves a guest physical address to a BAR index + BAR-relative offset.
406    pub(crate) fn find_bar(&self, addr: u64) -> Option<(u8, u64)> {
407        self.cfg_space.find_bar(addr)
408    }
409
410    /// Saves optional per-port CXL component-register state.
411    ///
412    /// Ports without CXL component-register backing return `None`.
413    pub(crate) fn save_cxl_component_registers_state(
414        &mut self,
415    ) -> Result<Option<CxlComponentRegistersSavedState>, SaveError> {
416        self.cxl_component_registers
417            .as_mut()
418            .map(|regs| regs.save())
419            .transpose()
420    }
421
422    /// Restores optional per-port CXL component-register state.
423    ///
424    /// State presence must match the current port topology, otherwise restore fails
425    /// with an invalid-saved-state error.
426    pub(crate) fn restore_cxl_component_registers_state(
427        &mut self,
428        state: Option<CxlComponentRegistersSavedState>,
429    ) -> Result<(), RestoreError> {
430        match (&mut self.cxl_component_registers, state) {
431            (Some(current), Some(saved)) => current.restore(saved)?,
432            (None, None) => {}
433            (Some(_), None) => {
434                return Err(RestoreError::InvalidSavedState(anyhow::anyhow!(
435                    "missing CXL component-register state"
436                )));
437            }
438            (None, Some(_)) => {
439                return Err(RestoreError::InvalidSavedState(anyhow::anyhow!(
440                    "unexpected CXL component-register state"
441                )));
442            }
443        }
444
445        Ok(())
446    }
447
448    /// Handles BAR reads for this port using subregion semantics.
449    ///
450    /// CXL component-register accesses are redirected into `CxlComponentRegisters`;
451    /// unsupported or out-of-window accesses are handled as reserved reads.
452    pub(crate) fn bar_mmio_read(&mut self, bar: u8, bar_offset: u64, data: &mut [u8]) -> IoResult {
453        if bar != 0 {
454            tracelimit::warn_ratelimited!(bar, "unsupported port BAR read");
455            data.fill(0xff);
456            return IoResult::Ok;
457        }
458
459        let Some(bar_cfg) = &self.bar else {
460            tracelimit::warn_ratelimited!(bar, "BAR read without BAR configuration");
461            data.fill(0xff);
462            return IoResult::Ok;
463        };
464
465        let access_end = match bar_offset.checked_add(data.len() as u64) {
466            Some(end) => end,
467            None => {
468                data.fill(0xff);
469                return IoResult::Ok;
470            }
471        };
472
473        let Some(subregion) = bar_cfg.subregions.iter().find(|subregion| {
474            let Some(subregion_end) = subregion.offset.checked_add(subregion.size_bytes) else {
475                return false;
476            };
477
478            bar_offset >= subregion.offset && access_end <= subregion_end
479        }) else {
480            tracelimit::warn_ratelimited!(
481                offset = bar_offset,
482                size = data.len(),
483                "BAR read outside configured subregions"
484            );
485            data.fill(0);
486            return IoResult::Ok;
487        };
488
489        match subregion.kind {
490            PortBarSubregionKind::CxlComponentRegisters => {
491                let Some(component_registers) = self.cxl_component_registers.as_ref() else {
492                    tracelimit::warn_ratelimited!(
493                        "CXL component register BAR read without component-register backing"
494                    );
495                    data.fill(0);
496                    return IoResult::Ok;
497                };
498
499                let Some(relative_offset) = bar_offset.checked_sub(subregion.offset) else {
500                    data.fill(0);
501                    return IoResult::Ok;
502                };
503
504                if let Err(err) =
505                    validate_cxl_component_register_access(relative_offset, data.len())
506                {
507                    return IoResult::Err(err);
508                }
509
510                let Ok(relative_offset) = u16::try_from(relative_offset) else {
511                    data.fill(0);
512                    return IoResult::Ok;
513                };
514
515                match component_registers.read(relative_offset, data) {
516                    IoResult::Err(IoError::InvalidRegister) => {
517                        data.fill(0);
518                        IoResult::Ok
519                    }
520                    res => res,
521                }
522            }
523            PortBarSubregionKind::MsiXTable | PortBarSubregionKind::MsiXPba => {
524                // MSI-X BAR subregions are not emulated by this port yet.
525                tracelimit::warn_ratelimited!(
526                    "read BAR subregion of kind {:?}, offset 0x{:x}, size 0x{:x}",
527                    subregion.kind,
528                    subregion.offset,
529                    subregion.size_bytes
530                );
531                data.fill(0);
532                IoResult::Ok
533            }
534        }
535    }
536
537    /// Handles BAR writes for this port using subregion semantics.
538    ///
539    /// CXL component-register accesses are redirected into `CxlComponentRegisters`;
540    /// unsupported or out-of-window accesses are treated as handled writes.
541    pub(crate) fn bar_mmio_write(&mut self, bar: u8, bar_offset: u64, data: &[u8]) -> IoResult {
542        if bar != 0 {
543            tracelimit::warn_ratelimited!(bar, "unsupported port BAR write");
544            return IoResult::Ok;
545        }
546
547        let Some(bar_cfg) = &self.bar else {
548            tracelimit::warn_ratelimited!(bar, "BAR write without BAR configuration");
549            return IoResult::Ok;
550        };
551
552        let access_end = match bar_offset.checked_add(data.len() as u64) {
553            Some(end) => end,
554            None => {
555                return IoResult::Ok;
556            }
557        };
558
559        let Some(subregion) = bar_cfg.subregions.iter().find(|subregion| {
560            let Some(subregion_end) = subregion.offset.checked_add(subregion.size_bytes) else {
561                return false;
562            };
563
564            bar_offset >= subregion.offset && access_end <= subregion_end
565        }) else {
566            tracelimit::warn_ratelimited!(
567                offset = bar_offset,
568                size = data.len(),
569                "BAR write outside configured subregions"
570            );
571            return IoResult::Ok;
572        };
573
574        match subregion.kind {
575            PortBarSubregionKind::CxlComponentRegisters => {
576                let Some(component_registers) = self.cxl_component_registers.as_mut() else {
577                    tracelimit::warn_ratelimited!(
578                        "CXL component register BAR write without component-register backing"
579                    );
580                    return IoResult::Ok;
581                };
582
583                let Some(relative_offset) = bar_offset.checked_sub(subregion.offset) else {
584                    return IoResult::Ok;
585                };
586
587                if let Err(err) =
588                    validate_cxl_component_register_access(relative_offset, data.len())
589                {
590                    return IoResult::Err(err);
591                }
592
593                let Ok(relative_offset) = u16::try_from(relative_offset) else {
594                    return IoResult::Ok;
595                };
596
597                match component_registers.write(relative_offset, data) {
598                    IoResult::Err(IoError::InvalidRegister) => IoResult::Ok,
599                    res => res,
600                }
601            }
602            PortBarSubregionKind::MsiXTable | PortBarSubregionKind::MsiXPba => {
603                // MSI-X BAR subregions are not emulated by this port yet.
604                tracelimit::warn_ratelimited!(
605                    "write BAR subregion of kind {:?}, offset 0x{:x}, size 0x{:x}",
606                    subregion.kind,
607                    subregion.offset,
608                    subregion.size_bytes
609                );
610                IoResult::Ok
611            }
612        }
613    }
614
615    /// Returns a clone of the config space emulator's shared bus range.
616    ///
617    /// The returned handle shares the same underlying atomic as the
618    /// emulator — writes, resets, and restores are reflected automatically.
619    pub fn bus_range(&self) -> AssignedBusRange {
620        self.cfg_space.bus_range()
621    }
622
623    /// Notify the guest of a hotplug event via MSI.
624    ///
625    /// Fires MSI if the guest has enabled hot_plug_interrupt_enable in
626    /// Slot Control. The caller must have already set the appropriate
627    /// status bits (via set_hotplug_state) before calling this.
628    fn fire_hotplug_msi(&self) {
629        let hotplug_enabled = self
630            .cfg_space
631            .capabilities()
632            .iter()
633            .find_map(|cap| cap.as_pci_express())
634            .is_some_and(|pcie| pcie.hot_plug_interrupt_enabled());
635
636        if hotplug_enabled {
637            if let Some(interrupt) = self
638                .cfg_space
639                .capabilities()
640                .iter()
641                .find_map(|cap| cap.as_msi_cap())
642                .and_then(|msi| msi.interrupt())
643            {
644                interrupt.deliver();
645            }
646        }
647    }
648
649    /// Forward a configuration space read to the connected device.
650    /// Supports routing components for multi-level hierarchies.
651    pub fn forward_cfg_read_with_routing(
652        &mut self,
653        bus: &u8,
654        function: &u8,
655        cfg_offset: u16,
656        value: &mut u32,
657    ) -> IoResult {
658        let bus_range = self.cfg_space.assigned_bus_range();
659
660        // If the bus range is 0..=0, this indicates invalid/uninitialized bus configuration
661        if bus_range == (0..=0) {
662            tracelimit::warn_ratelimited!("invalid access: port bus number range not configured");
663            return IoResult::Ok;
664        }
665
666        if bus_range.contains(bus) {
667            if let Some((_, device)) = &mut self.link {
668                let secondary_bus = *bus_range.start();
669                let result = device.pci_cfg_read_with_routing(
670                    secondary_bus,
671                    *bus,
672                    *function,
673                    cfg_offset,
674                    value,
675                );
676
677                if let Some(result) = result {
678                    match result {
679                        IoResult::Ok => (),
680                        res => return res,
681                    }
682                }
683            } else if *bus != *bus_range.start() {
684                tracelimit::warn_ratelimited!(
685                    "invalid access: bus number to access not within port's bus number range"
686                );
687            }
688        }
689
690        IoResult::Ok
691    }
692
693    /// Forward a configuration space write to the connected device.
694    /// Supports routing components for multi-level hierarchies.
695    pub fn forward_cfg_write_with_routing(
696        &mut self,
697        bus: &u8,
698        function: &u8,
699        cfg_offset: u16,
700        value: u32,
701    ) -> IoResult {
702        let bus_range = self.cfg_space.assigned_bus_range();
703
704        // If the bus range is 0..=0, this indicates invalid/uninitialized bus configuration
705        if bus_range == (0..=0) {
706            tracelimit::warn_ratelimited!("invalid access: port bus number range not configured");
707            return IoResult::Ok;
708        }
709
710        if bus_range.contains(bus) {
711            if let Some((_, device)) = &mut self.link {
712                let secondary_bus = *bus_range.start();
713                let result = device.pci_cfg_write_with_routing(
714                    secondary_bus,
715                    *bus,
716                    *function,
717                    cfg_offset,
718                    value,
719                );
720
721                if let Some(result) = result {
722                    match result {
723                        IoResult::Ok => (),
724                        res => return res,
725                    }
726                }
727            } else if *bus != *bus_range.start() {
728                tracelimit::warn_ratelimited!(
729                    "invalid access: bus number to access not within port's bus number range"
730                );
731            }
732        }
733
734        IoResult::Ok
735    }
736
737    /// Connect a device to this specific port by exact name match.
738    pub fn add_pcie_device(
739        &mut self,
740        port_name: &str,
741        device_name: &str,
742        device: Box<dyn GenericPciBusDevice>,
743    ) -> anyhow::Result<()> {
744        // Only connect if the name exactly matches this port's name
745        if port_name == self.name.as_str() {
746            // Check if there's already a device connected
747            if self.link.is_some() {
748                bail!("port is already occupied");
749            }
750
751            // Connect the device to this port
752            self.link = Some((device_name.into(), device));
753
754            // Set presence detect state to true when a device is connected
755            self.cfg_space.set_presence_detect_state(true);
756
757            return Ok(());
758        }
759
760        // If the name doesn't match, fail immediately (no forwarding)
761        bail!("port name does not match")
762    }
763
764    /// Hot-add a device to this port at runtime.
765    ///
766    /// Unlike `add_pcie_device`, this method verifies the port is hotplug-capable
767    /// and fires MSI to notify the guest's pciehp driver.
768    pub fn hotplug_add_device(
769        &mut self,
770        device_name: &str,
771        device: Box<dyn GenericPciBusDevice>,
772    ) -> anyhow::Result<()> {
773        let is_hotplug_capable = self
774            .cfg_space
775            .capabilities()
776            .iter()
777            .find_map(|cap| cap.as_pci_express())
778            .is_some_and(|pcie| pcie.slot_capabilities().hot_plug_capable());
779
780        if !is_hotplug_capable {
781            bail!("port '{}' is not hotplug capable", self.name);
782        }
783        if self.link.is_some() {
784            bail!("port '{}' is already occupied", self.name);
785        }
786
787        self.link = Some((device_name.into(), device));
788
789        // Atomically set presence + link active + changed bits, then fire MSI
790        for cap in self.cfg_space.capabilities().iter() {
791            if let Some(pcie) = cap.as_pci_express() {
792                pcie.set_hotplug_state(true);
793            }
794        }
795        self.fire_hotplug_msi();
796        Ok(())
797    }
798
799    /// Hot-remove the device from this port at runtime.
800    pub fn hotplug_remove_device(&mut self) -> anyhow::Result<()> {
801        let is_hotplug_capable = self
802            .cfg_space
803            .capabilities()
804            .iter()
805            .find_map(|cap| cap.as_pci_express())
806            .is_some_and(|pcie| pcie.slot_capabilities().hot_plug_capable());
807
808        if !is_hotplug_capable {
809            bail!("port '{}' is not hotplug capable", self.name);
810        }
811        if self.link.is_none() {
812            bail!("port '{}' is empty", self.name);
813        }
814
815        self.link = None;
816
817        // Atomically clear presence + link active + set changed bits, then fire MSI
818        for cap in self.cfg_space.capabilities().iter() {
819            if let Some(pcie) = cap.as_pci_express() {
820                pcie.set_hotplug_state(false);
821            }
822        }
823        self.fire_hotplug_msi();
824        Ok(())
825    }
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831    use crate::test_helpers::TestPcieMmioRegistration;
832    use chipset_device::io::IoResult;
833    use cxl_spec::pci_registers::spec::flex_bus_port_dvsec::CxlFlexBusPortDvsecCapability;
834    use parking_lot::Mutex;
835    use pci_bus::GenericPciBusDevice;
836    use pci_core::spec::hwid::HardwareIds;
837    use std::sync::Arc;
838
839    fn make_cxl_bar_port() -> PcieDownstreamPort {
840        use pci_core::spec::hwid::{ClassCode, ProgrammingInterface, Subclass};
841
842        let hardware_ids = HardwareIds {
843            vendor_id: 0x1234,
844            device_id: 0x5678,
845            revision_id: 0,
846            prog_if: ProgrammingInterface::NONE,
847            sub_class: Subclass::BRIDGE_PCI_TO_PCI,
848            base_class: ClassCode::BRIDGE,
849            type0_sub_vendor_id: 0,
850            type0_sub_system_id: 0,
851        };
852
853        let mut mmio = TestPcieMmioRegistration {};
854        let msi_target = MsiTarget::disconnected();
855        PcieDownstreamPort::new(
856            "cxl-bar-port",
857            hardware_ids,
858            DevicePortType::RootPort,
859            false,
860            None,
861            &msi_target,
862            PciePortSettings {
863                acs_capabilities_supported: 0,
864                cxl_flex_bus_port_capability: Some(
865                    CxlFlexBusPortDvsecCapability::new().with_mem_capable(true),
866                ),
867            },
868            Some(&mut mmio),
869            Some(PortBarDefinition {
870                index: 0,
871                size_bytes: 0x1000,
872                subregions: vec![PortBarSubregionDefinition {
873                    kind: PortBarSubregionKind::CxlComponentRegisters,
874                    offset: 0,
875                    size_bytes: 0x1000,
876                }],
877            }),
878        )
879    }
880
881    // Mock device for testing
882    struct MockDevice;
883
884    impl GenericPciBusDevice for MockDevice {
885        fn pci_cfg_read(&mut self, _offset: u16, _value: &mut u32) -> Option<IoResult> {
886            None
887        }
888
889        fn pci_cfg_write(&mut self, _offset: u16, _value: u32) -> Option<IoResult> {
890            None
891        }
892    }
893
894    #[derive(Default, Debug, Clone, PartialEq, Eq)]
895    struct RoutingStats {
896        direct_reads: usize,
897        forward_reads: Vec<(u8, u8, u16)>,
898        direct_writes: usize,
899        forward_writes: Vec<(u8, u8, u16, u32)>,
900    }
901
902    struct MultiFunctionMockDevice {
903        stats: Arc<Mutex<RoutingStats>>,
904    }
905
906    impl GenericPciBusDevice for MultiFunctionMockDevice {
907        fn pci_cfg_read(&mut self, _offset: u16, _value: &mut u32) -> Option<IoResult> {
908            self.stats.lock().direct_reads += 1;
909            Some(IoResult::Ok)
910        }
911
912        fn pci_cfg_write(&mut self, _offset: u16, _value: u32) -> Option<IoResult> {
913            self.stats.lock().direct_writes += 1;
914            Some(IoResult::Ok)
915        }
916
917        fn pci_cfg_read_with_routing(
918            &mut self,
919            _secondary_bus: u8,
920            target_bus: u8,
921            function: u8,
922            offset: u16,
923            value: &mut u32,
924        ) -> Option<IoResult> {
925            self.stats
926                .lock()
927                .forward_reads
928                .push((target_bus, function, offset));
929            *value = 0x1234_5678;
930            Some(IoResult::Ok)
931        }
932
933        fn pci_cfg_write_with_routing(
934            &mut self,
935            _secondary_bus: u8,
936            target_bus: u8,
937            function: u8,
938            offset: u16,
939            value: u32,
940        ) -> Option<IoResult> {
941            self.stats
942                .lock()
943                .forward_writes
944                .push((target_bus, function, offset, value));
945            Some(IoResult::Ok)
946        }
947    }
948
949    #[test]
950    fn test_add_pcie_device_sets_presence_detect_state() {
951        use pci_core::spec::hwid::{ClassCode, ProgrammingInterface, Subclass};
952
953        // Create a port with hotplug support
954        let hardware_ids = HardwareIds {
955            vendor_id: 0x1234,
956            device_id: 0x5678,
957            revision_id: 0,
958            prog_if: ProgrammingInterface::NONE,
959            sub_class: Subclass::BRIDGE_PCI_TO_PCI,
960            base_class: ClassCode::BRIDGE,
961            type0_sub_vendor_id: 0,
962            type0_sub_system_id: 0,
963        };
964
965        let msi_conn = pci_core::msi::MsiConnection::new(AssignedBusRange::new(), 0);
966        let mut port = PcieDownstreamPort::new(
967            "test-port",
968            hardware_ids,
969            DevicePortType::RootPort,
970            false,
971            Some(1), // Enable hotplug with slot number 1
972            msi_conn.target(),
973            PciePortSettings::default(),
974            None,
975            None,
976        );
977
978        // Initially, presence detect state should be 0
979        let mut slot_status_val = 0u32;
980        let result = port.cfg_space.read_u32(0x58, &mut slot_status_val); // 0x40 (cap start) + 0x18 (slot control/status)
981        assert!(matches!(result, IoResult::Ok));
982        let initial_presence_detect = (slot_status_val >> 22) & 0x1; // presence_detect_state is bit 6 of slot status
983        assert_eq!(
984            initial_presence_detect, 0,
985            "Initial presence detect state should be 0"
986        );
987
988        // Add a device to the port
989        let mock_device = Box::new(MockDevice);
990        let result = port.add_pcie_device("test-port", "mock-device", mock_device);
991        assert!(result.is_ok(), "Adding device should succeed");
992
993        // Check that presence detect state is now 1
994        let result = port.cfg_space.read_u32(0x58, &mut slot_status_val);
995        assert!(matches!(result, IoResult::Ok));
996        let present_presence_detect = (slot_status_val >> 22) & 0x1;
997        assert_eq!(
998            present_presence_detect, 1,
999            "Presence detect state should be 1 after adding device"
1000        );
1001    }
1002
1003    #[test]
1004    fn test_add_pcie_device_without_hotplug() {
1005        use pci_core::spec::hwid::{ClassCode, ProgrammingInterface, Subclass};
1006
1007        // Create a port without hotplug support
1008        let hardware_ids = HardwareIds {
1009            vendor_id: 0x1234,
1010            device_id: 0x5678,
1011            revision_id: 0,
1012            prog_if: ProgrammingInterface::NONE,
1013            sub_class: Subclass::BRIDGE_PCI_TO_PCI,
1014            base_class: ClassCode::BRIDGE,
1015            type0_sub_vendor_id: 0,
1016            type0_sub_system_id: 0,
1017        };
1018
1019        let msi_conn = pci_core::msi::MsiConnection::new(AssignedBusRange::new(), 0);
1020        let mut port = PcieDownstreamPort::new(
1021            "test-port",
1022            hardware_ids,
1023            DevicePortType::RootPort,
1024            false,
1025            None, // No hotplug
1026            msi_conn.target(),
1027            PciePortSettings::default(),
1028            None,
1029            None,
1030        );
1031
1032        // Add a device to the port (should not panic even without hotplug support)
1033        let mock_device = Box::new(MockDevice);
1034        let result = port.add_pcie_device("test-port", "mock-device", mock_device);
1035        assert!(
1036            result.is_ok(),
1037            "Adding device should succeed even without hotplug support"
1038        );
1039    }
1040
1041    #[test]
1042    fn test_direct_child_bus_reads_use_forward_for_multifunction_devices() {
1043        use pci_core::spec::hwid::{ClassCode, ProgrammingInterface, Subclass};
1044
1045        let hardware_ids = HardwareIds {
1046            vendor_id: 0x1234,
1047            device_id: 0x5678,
1048            revision_id: 0,
1049            prog_if: ProgrammingInterface::NONE,
1050            sub_class: Subclass::BRIDGE_PCI_TO_PCI,
1051            base_class: ClassCode::BRIDGE,
1052            type0_sub_vendor_id: 0,
1053            type0_sub_system_id: 0,
1054        };
1055
1056        let msi_target = MsiTarget::disconnected();
1057        let mut port = PcieDownstreamPort::new(
1058            "test-port",
1059            hardware_ids,
1060            DevicePortType::RootPort,
1061            false,
1062            None,
1063            &msi_target,
1064            PciePortSettings::default(),
1065            None,
1066            None,
1067        );
1068
1069        port.cfg_space
1070            .write_u32(0x18, (1u32 << 16) | (1u32 << 8))
1071            .unwrap();
1072
1073        let stats = Arc::new(Mutex::new(RoutingStats::default()));
1074        port.link = Some((
1075            "mf-device".into(),
1076            Box::new(MultiFunctionMockDevice {
1077                stats: Arc::clone(&stats),
1078            }),
1079        ));
1080
1081        let mut value = 0;
1082        // All accesses on the secondary bus go through
1083        // pci_cfg_read_with_routing — the linked device is responsible
1084        // for dispatching function 0 to its own config space.
1085        assert!(matches!(
1086            port.forward_cfg_read_with_routing(&1, &0, 0x10, &mut value),
1087            IoResult::Ok
1088        ));
1089        assert!(matches!(
1090            port.forward_cfg_read_with_routing(&1, &3, 0x14, &mut value),
1091            IoResult::Ok
1092        ));
1093
1094        let stats = stats.lock().clone();
1095        assert_eq!(stats.direct_reads, 0);
1096        assert_eq!(stats.forward_reads, vec![(1, 0, 0x10), (1, 3, 0x14)]);
1097    }
1098
1099    #[test]
1100    fn test_direct_child_bus_writes_use_forward_for_multifunction_devices() {
1101        use pci_core::spec::hwid::{ClassCode, ProgrammingInterface, Subclass};
1102
1103        let hardware_ids = HardwareIds {
1104            vendor_id: 0x1234,
1105            device_id: 0x5678,
1106            revision_id: 0,
1107            prog_if: ProgrammingInterface::NONE,
1108            sub_class: Subclass::BRIDGE_PCI_TO_PCI,
1109            base_class: ClassCode::BRIDGE,
1110            type0_sub_vendor_id: 0,
1111            type0_sub_system_id: 0,
1112        };
1113
1114        let msi_conn = pci_core::msi::MsiConnection::new(AssignedBusRange::new(), 0);
1115        let mut port = PcieDownstreamPort::new(
1116            "test-port",
1117            hardware_ids,
1118            DevicePortType::RootPort,
1119            false,
1120            None,
1121            msi_conn.target(),
1122            PciePortSettings::default(),
1123            None,
1124            None,
1125        );
1126
1127        port.cfg_space
1128            .write_u32(0x18, (1u32 << 16) | (1u32 << 8))
1129            .unwrap();
1130
1131        let stats = Arc::new(Mutex::new(RoutingStats::default()));
1132        port.link = Some((
1133            "mf-device".into(),
1134            Box::new(MultiFunctionMockDevice {
1135                stats: Arc::clone(&stats),
1136            }),
1137        ));
1138
1139        // All accesses on the secondary bus go through
1140        // pci_cfg_write_with_routing — the linked device is responsible
1141        // for dispatching function 0 to its own config space.
1142        assert!(matches!(
1143            port.forward_cfg_write_with_routing(&1, &0, 0x10, 0xAAAA_0000),
1144            IoResult::Ok
1145        ));
1146        assert!(matches!(
1147            port.forward_cfg_write_with_routing(&1, &2, 0x14, 0xBBBB_0000),
1148            IoResult::Ok
1149        ));
1150
1151        let stats = stats.lock().clone();
1152        assert_eq!(stats.direct_writes, 0);
1153        assert_eq!(
1154            stats.forward_writes,
1155            vec![(1, 0, 0x10, 0xAAAA_0000), (1, 2, 0x14, 0xBBBB_0000)]
1156        );
1157    }
1158
1159    #[test]
1160    fn test_port_cfg_space_save_restore() {
1161        use pci_core::spec::hwid::{ClassCode, ProgrammingInterface, Subclass};
1162        use vmcore::save_restore::SaveRestore;
1163
1164        let hardware_ids = HardwareIds {
1165            vendor_id: 0x1234,
1166            device_id: 0x5678,
1167            revision_id: 0,
1168            prog_if: ProgrammingInterface::NONE,
1169            sub_class: Subclass::BRIDGE_PCI_TO_PCI,
1170            base_class: ClassCode::BRIDGE,
1171            type0_sub_vendor_id: 0,
1172            type0_sub_system_id: 0,
1173        };
1174
1175        let msi_conn = pci_core::msi::MsiConnection::new(AssignedBusRange::new(), 0);
1176        let mut port = PcieDownstreamPort::new(
1177            "test-port",
1178            hardware_ids,
1179            DevicePortType::RootPort,
1180            false,
1181            None,
1182            msi_conn.target(),
1183            PciePortSettings::default(),
1184            None,
1185            None,
1186        );
1187
1188        // Program bridge bus numbers (Type1 register at offset 0x18).
1189        port.cfg_space.write_u32(0x18, 0x0012_1000).unwrap();
1190        assert_eq!(port.cfg_space.assigned_bus_range(), 0x10..=0x12);
1191
1192        let saved = port.cfg_space.save().expect("save should succeed");
1193
1194        // Change state away from saved values.
1195        port.cfg_space.write_u32(0x18, 0x0000_0000).unwrap();
1196        assert_eq!(port.cfg_space.assigned_bus_range(), 0..=0);
1197
1198        port.cfg_space
1199            .restore(saved)
1200            .expect("restore should succeed");
1201        assert_eq!(port.cfg_space.assigned_bus_range(), 0x10..=0x12);
1202    }
1203
1204    #[test]
1205    fn test_filter_acs_capabilities_for_bridge_type() {
1206        assert_eq!(
1207            filter_acs_capabilities_for_bridge(&DevicePortType::RootPort, 0x00ff),
1208            0x00df
1209        );
1210        assert_eq!(
1211            filter_acs_capabilities_for_bridge(&DevicePortType::DownstreamSwitchPort, 0x00ff),
1212            0x00df
1213        );
1214        assert_eq!(
1215            filter_acs_capabilities_for_bridge(&DevicePortType::UpstreamSwitchPort, 0x00ff),
1216            0
1217        );
1218        assert_eq!(
1219            filter_acs_capabilities_for_bridge(&DevicePortType::Endpoint, 0x00ff),
1220            0
1221        );
1222    }
1223
1224    #[test]
1225    fn test_root_port_adds_acs_only_when_non_zero() {
1226        use pci_core::spec::caps::ExtendedCapabilityId;
1227        use pci_core::spec::hwid::{ClassCode, ProgrammingInterface, Subclass};
1228
1229        let hardware_ids = HardwareIds {
1230            vendor_id: 0x1234,
1231            device_id: 0x5678,
1232            revision_id: 0,
1233            prog_if: ProgrammingInterface::NONE,
1234            sub_class: Subclass::BRIDGE_PCI_TO_PCI,
1235            base_class: ClassCode::BRIDGE,
1236            type0_sub_vendor_id: 0,
1237            type0_sub_system_id: 0,
1238        };
1239
1240        let msi_target = MsiTarget::disconnected();
1241        let with_acs = PcieDownstreamPort::new(
1242            "with-acs",
1243            hardware_ids,
1244            DevicePortType::RootPort,
1245            false,
1246            None,
1247            &msi_target,
1248            PciePortSettings {
1249                acs_capabilities_supported: 0x005f,
1250                ..Default::default()
1251            },
1252            None,
1253            None,
1254        );
1255        let mut value = 0u32;
1256        with_acs.cfg_space.read_u32(0x100, &mut value).unwrap();
1257        assert_eq!(value & 0xffff, ExtendedCapabilityId::ACS.0 as u32);
1258
1259        let without_acs = PcieDownstreamPort::new(
1260            "without-acs",
1261            hardware_ids,
1262            DevicePortType::RootPort,
1263            false,
1264            None,
1265            &msi_target,
1266            PciePortSettings::default(),
1267            None,
1268            None,
1269        );
1270        without_acs.cfg_space.read_u32(0x100, &mut value).unwrap();
1271        assert_eq!(value, 0xffff_ffff);
1272    }
1273
1274    #[test]
1275    fn test_invalid_cxl_component_register_locator_disables_cxl_exposure() {
1276        use cxl_spec::pci_registers::spec::flex_bus_port_dvsec::CxlFlexBusPortDvsecCapability;
1277        use pci_core::spec::hwid::{ClassCode, ProgrammingInterface, Subclass};
1278
1279        let hardware_ids = HardwareIds {
1280            vendor_id: 0x1234,
1281            device_id: 0x5678,
1282            revision_id: 0,
1283            prog_if: ProgrammingInterface::NONE,
1284            sub_class: Subclass::BRIDGE_PCI_TO_PCI,
1285            base_class: ClassCode::BRIDGE,
1286            type0_sub_vendor_id: 0,
1287            type0_sub_system_id: 0,
1288        };
1289
1290        let msi_target = MsiTarget::disconnected();
1291        let port = PcieDownstreamPort::new(
1292            "test-port",
1293            hardware_ids,
1294            DevicePortType::RootPort,
1295            false,
1296            None,
1297            &msi_target,
1298            PciePortSettings {
1299                acs_capabilities_supported: 0,
1300                cxl_flex_bus_port_capability: Some(
1301                    CxlFlexBusPortDvsecCapability::new().with_mem_capable(true),
1302                ),
1303            },
1304            None,
1305            Some(PortBarDefinition {
1306                index: 0,
1307                size_bytes: 0x1000,
1308                subregions: vec![PortBarSubregionDefinition {
1309                    kind: PortBarSubregionKind::CxlComponentRegisters,
1310                    offset: 0,
1311                    size_bytes: 0x1000,
1312                }],
1313            }),
1314        );
1315
1316        let mut value = 0u32;
1317        port.cfg_space.read_u32(0x100, &mut value).unwrap();
1318        assert_eq!(
1319            value, 0xffff_ffff,
1320            "CXL DVSECs should be absent when CXL component-register BAR backing is invalid"
1321        );
1322        assert!(
1323            port.cxl_component_registers.is_none(),
1324            "component-register backing should not be allocated"
1325        );
1326    }
1327
1328    #[test]
1329    fn test_cxl_component_register_bar_rejects_1_or_2_byte_reads() {
1330        let mut port = make_cxl_bar_port();
1331
1332        let mut read1 = [0u8; 1];
1333        assert!(matches!(
1334            port.bar_mmio_read(0, 0, &mut read1),
1335            IoResult::Err(IoError::InvalidAccessSize)
1336        ));
1337
1338        let mut read2 = [0u8; 2];
1339        assert!(matches!(
1340            port.bar_mmio_read(0, 0, &mut read2),
1341            IoResult::Err(IoError::InvalidAccessSize)
1342        ));
1343    }
1344
1345    #[test]
1346    fn test_cxl_component_register_bar_rejects_1_or_2_byte_writes() {
1347        let mut port = make_cxl_bar_port();
1348
1349        let write1 = [0u8; 1];
1350        assert!(matches!(
1351            port.bar_mmio_write(0, 0, &write1),
1352            IoResult::Err(IoError::InvalidAccessSize)
1353        ));
1354
1355        let write2 = [0u8; 2];
1356        assert!(matches!(
1357            port.bar_mmio_write(0, 0, &write2),
1358            IoResult::Err(IoError::InvalidAccessSize)
1359        ));
1360    }
1361}