From 1650ff20f09b1971a1dca08ec8e27e9b0286980f Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:18:04 +0200 Subject: [PATCH 1/3] PCI is walked only over the buses each MCFG allocation decodes, and only into buses a bridge forwards The kernel mapped 256 buses from the first MCFG allocation's base and read every one of them. On an AMD laptop whose MCFG decodes buses 0 to 0x3f, the addresses past bus 0x3f are other hardware's (the IOAPIC, the HPET, flash, then DRAM): the walk read thousands of phantom functions out of them, filled its 256-function cap, and remapped what lay there uncached. - toyos-acpi decodes every MCFG allocation structure (PCI Firmware Specification 3.3, Table 4-3) with its segment group and start and end bus, and refuses by name one that is inverted, off a bus boundary, wraps the address space, is cut short by the table's end, sits on a segment group other than the first accepted one's, or decodes a bus an earlier window already does. `ecam_base`, which read the first base and nothing else, is gone. - toyos-pci's `buses` decides which buses of a window a walk enters: the window's first bus, and each bridge's secondary inside what the bus above it is forwarded, each at most once and always above the bus that named it, so the walk ends however the bridges are numbered. - The kernel maps each window over its own buses only, walks it with that, and answers `function_window` only for a bus a window decodes; the ACPI holder's mediated window and the loader's watchdog read the same decode. An unusable MCFG is a boot with no PCI function, said by name, instead of a panic on firmware input. issues/the-mcfg-entrys-start-bus-is-never-read.md is closed: the decode returns the range beside the base and no caller indexes a bus outside it. Its claim that the base is the start bus's was wrong: the base is bus 0's (Linux's `pci_mmcfg` reads it so, and `toyos_userbound::firmware::Ecam` already did). issues/pci-is-walked-from-one-root-bus-per-ecam-window.md records what the walk still does not enter: a root bus other than a window's first, and a segment group other than the first window's. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- bootloader/src/watchdog.rs | 10 +- ...alked-from-one-root-bus-per-ecam-window.md | 25 ++ ...the-mcfg-entrys-start-bus-is-never-read.md | 23 -- kernel/pci/src/buses.rs | 235 ++++++++++++++++++ kernel/pci/src/lib.rs | 1 + kernel/src/arch/x86_64/acpi_mode.rs | 19 +- kernel/src/drivers/acpi.rs | 36 ++- kernel/src/drivers/pci.rs | 114 ++++++--- kernel/src/main.rs | 10 +- tests/common/qemu.rs | 17 +- tests/toyos.rs | 45 +++- toyos-acpi/src/lib.rs | 18 +- toyos-acpi/src/mcfg.rs | 156 ++++++++++++ toyos-acpi/tests/corpus.rs | 11 +- toyos-acpi/tests/fixtures.rs | 14 +- toyos-acpi/tests/mcfg.rs | 130 ++++++++++ 16 files changed, 747 insertions(+), 117 deletions(-) create mode 100644 issues/pci-is-walked-from-one-root-bus-per-ecam-window.md delete mode 100644 issues/the-mcfg-entrys-start-bus-is-never-read.md create mode 100644 kernel/pci/src/buses.rs create mode 100644 toyos-acpi/src/mcfg.rs create mode 100644 toyos-acpi/tests/mcfg.rs diff --git a/bootloader/src/watchdog.rs b/bootloader/src/watchdog.rs index 4440cfd59b5..a8b96205010 100644 --- a/bootloader/src/watchdog.rs +++ b/bootloader/src/watchdog.rs @@ -56,10 +56,16 @@ pub fn arm(system_table: &SystemTable, rsdp_addr: u64, cmdline: &str) { if !crate::arch::pio::EXISTS { return refused(format_args!("this architecture has no I/O port space, where a TCO block answers")); } - let ecam = match toyos_acpi::ecam_base(Identity, rsdp_addr) { - Ok((_, base)) => base, + let allocations = match toyos_acpi::ecam_allocations(Identity, rsdp_addr) { + Ok(allocations) => allocations, Err(e) => return refused(format_args!("this machine's tables name no ECAM ({e:?})")), }; + // Bus 0 of segment group 0 is where the chipset is, and only a window + // that decodes it has bus 0's address at its base. + let Some(ecam) = allocations.flatten().find(|window| window.segment() == 0 && window.holds(0)).map(|window| window.base()) + else { + return refused(format_args!("no ECAM window of the MCFG's decodes bus 0 of segment group 0")); + }; // The MCFG's word for where configuration space is, checked against // firmware's own map before anything dereferences it. if !described(system_table, ecam, BUS_ZERO_BYTES) { diff --git a/issues/pci-is-walked-from-one-root-bus-per-ecam-window.md b/issues/pci-is-walked-from-one-root-bus-per-ecam-window.md new file mode 100644 index 00000000000..0b04522ed97 --- /dev/null +++ b/issues/pci-is-walked-from-one-root-bus-per-ecam-window.md @@ -0,0 +1,25 @@ +--- +status: open +kind: defect +opened: 2026-10-10 +--- + +# PCI is walked from one root bus per ECAM window, on one segment group + +`pci::enumerate` (`kernel/src/drivers/pci.rs`) enters each ECAM window at its +start bus and from there only the buses a bridge forwards +(`toyos_pci::buses`). A host bridge whose root bus is any other bus of the +window — a second root complex in one segment group, as on multi-socket and +many-die parts, or QEMU's `pxb-pcie` — is never entered, and its functions are +not enumerated. Functions on a segment group other than the first window's are +not enumerated either: `toyos_acpi::ecam_allocations` refuses those windows +`OtherSegment`, and `PciDevice` carries no segment. + +What names every root bus is each host bridge's bus range: `_BBN` and `_CRS` +in AML, or the bus descriptor of `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL`'s +`Configuration()`, which the loader already reads for memory windows +(`bootloader/src/rootbridge.rs`) and with its segment number beside it. q35's +and `virt`'s guests have one root bus, at their window's start. + +Exit: the kernel enters every root bus firmware names, on every segment group +it names, and a guest with a `pxb-pcie` enumerates the functions behind it. diff --git a/issues/the-mcfg-entrys-start-bus-is-never-read.md b/issues/the-mcfg-entrys-start-bus-is-never-read.md deleted file mode 100644 index 49e8ef4d465..00000000000 --- a/issues/the-mcfg-entrys-start-bus-is-never-read.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-06 ---- - -# The MCFG entry's start bus is never read - -`toyos_acpi::ecam_base` (`toyos-acpi/src/lib.rs:256-263`) takes the first MCFG -allocation structure's base address and nothing else from it. The structure -carries a PCI segment group, a start bus and an end bus as well (PCI Firmware -Specification 3.3, Table 4-3), and the base is the address of *that entry's -start bus*, not of bus 0: on a machine whose first entry begins at a bus above -zero, or whose bus 0 lives in a later entry, `base + (bus << 20)` names another -segment's configuration space or nothing at all. Nothing downstream can notice, -because the range is not returned: `kernel/src/drivers/acpi.rs:96-107` logs the -base and hands it on, and every caller then indexes it as though bus 0 sat at -offset zero. Every machine this tree has booted has one entry starting at bus -0, so the defect is unobserved rather than absent. - -Exit: the decode returns the entry's segment and bus range beside its base, and -every caller refuses a bus outside that range by name rather than computing an -address from it. diff --git a/kernel/pci/src/buses.rs b/kernel/pci/src/buses.rs new file mode 100644 index 00000000000..db8147db84b --- /dev/null +++ b/kernel/pci/src/buses.rs @@ -0,0 +1,235 @@ +//! Which buses of one ECAM window a walk of configuration space enters +//! (PCI-to-PCI Bridge Architecture Specification 1.2 §3.2.5.3 to §3.2.5.5). +//! +//! **A bus is entered only where something forwards configuration cycles to +//! it**: the window's first bus, which its host bridge decodes, and each +//! bridge's secondary bus, inside what the bus above that bridge is forwarded. +//! A bus number nothing forwards is one no cycle reaches, and what answers at +//! its address in the window is not a function. +//! +//! A bridge forwards a cycle for a bus from its secondary through its +//! subordinate, so a bus below it is forwarded what both bounds allow: the +//! narrower of the two subordinates. Every bus is entered at most once and +//! only above the bus whose bridge named it, so a walk ends however a machine +//! numbered its bridges, and it enters buses in ascending order. + +use core::ops::RangeInclusive; + +/// The dword of a Type 1 header holding its primary, secondary and +/// subordinate bus numbers, in that byte order from its low end. +pub const BUS_NUMBERS: u64 = 0x18; + +/// Why a bridge's secondary bus is not entered. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Unforwarded { + /// Its secondary is not above the bus the bridge sits on: unconfigured + /// (zero) or numbered against the tree. + NotBelow { secondary: u8 }, + /// Its subordinate is below its secondary, so it forwards no bus. + Inverted { secondary: u8, subordinate: u8 }, + /// Its secondary is past the last bus the bridge itself is forwarded. + Outside { secondary: u8, limit: u8 }, + /// Another bridge already forwards its secondary. + Taken { secondary: u8 }, +} + +impl core::fmt::Display for Unforwarded { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match *self { + Self::NotBelow { secondary } => write!(f, "its secondary bus {secondary:#04x} is not below it"), + Self::Inverted { secondary, subordinate } => { + write!(f, "its subordinate bus {subordinate:#04x} is below its secondary {secondary:#04x}") + } + Self::Outside { secondary, limit } => { + write!(f, "its secondary bus {secondary:#04x} is past {limit:#04x}, the last bus it is forwarded") + } + Self::Taken { secondary } => write!(f, "another bridge already forwards bus {secondary:#04x}"), + } + } +} + +/// The walk's state over one window. +pub struct Buses { + /// Buses forwarded and not yet entered, one bit each. + pending: [u64; 4], + /// Buses forwarded at all, entered or not. + forwarded: [u64; 4], + /// For each forwarded bus, the last bus a cycle through it reaches. + limit: [u8; 256], +} + +fn bit(set: &[u64; 4], bus: u8) -> bool { + set[usize::from(bus / 64)] & 1 << (bus % 64) != 0 +} + +fn set(set: &mut [u64; 4], bus: u8, on: bool) { + let word = &mut set[usize::from(bus / 64)]; + *word = if on { *word | 1 << (bus % 64) } else { *word & !(1 << (bus % 64)) }; +} + +impl Buses { + /// A walk of the window over `buses`, its host bridge forwarding the first. + pub fn new(buses: RangeInclusive) -> Self { + let mut walk = Self { pending: [0; 4], forwarded: [0; 4], limit: [0; 256] }; + let (first, last) = buses.into_inner(); + if first <= last { + walk.forward(first, last); + } + walk + } + + fn forward(&mut self, bus: u8, limit: u8) { + set(&mut self.pending, bus, true); + set(&mut self.forwarded, bus, true); + self.limit[usize::from(bus)] = limit; + } + + /// The lowest bus forwarded and not yet entered, now entered. + pub fn enter(&mut self) -> Option { + let (word, bits) = self.pending.iter().enumerate().find(|(_, bits)| **bits != 0)?; + let bus = (word * 64) as u8 + bits.trailing_zeros() as u8; + set(&mut self.pending, bus, false); + Some(bus) + } + + /// A bridge on entered bus `bus`, its [`BUS_NUMBERS`] dword `numbers`: + /// the buses it forwards, its secondary to be entered, or why not. + pub fn bridge(&mut self, bus: u8, numbers: u32) -> Result, Unforwarded> { + let [_, secondary, subordinate, _] = numbers.to_le_bytes(); + let limit = self.limit[usize::from(bus)]; + if secondary <= bus { + return Err(Unforwarded::NotBelow { secondary }); + } + if subordinate < secondary { + return Err(Unforwarded::Inverted { secondary, subordinate }); + } + if secondary > limit { + return Err(Unforwarded::Outside { secondary, limit }); + } + if bit(&self.forwarded, secondary) { + return Err(Unforwarded::Taken { secondary }); + } + let reached = subordinate.min(limit); + self.forward(secondary, reached); + Ok(secondary..=reached) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::vec::Vec; + + /// A bridge's bus-number dword: primary, secondary, subordinate. + fn numbers(primary: u8, secondary: u8, subordinate: u8) -> u32 { + u32::from_le_bytes([primary, secondary, subordinate, 0x00]) + } + + /// A machine as `(bus, bridges on it as (secondary, subordinate))`, walked + /// as the kernel walks it: every bus entered, in the order entered. + fn entered(window: RangeInclusive, machine: &[(u8, &[(u8, u8)])]) -> Vec { + let mut walk = Buses::new(window); + let mut out = Vec::new(); + while let Some(bus) = walk.enter() { + out.push(bus); + for (_, bridges) in machine.iter().filter(|(on, _)| *on == bus) { + for &(secondary, subordinate) in *bridges { + let _ = walk.bridge(bus, numbers(bus, secondary, subordinate)); + } + } + } + out + } + + /// Root ports on bus 0 forwarding 4, 9, 0x0a and a hot-plug range from + /// 0x20, each secondary entered once and in ascending order whatever + /// order the ports were read in. + #[test] + fn the_buses_bridges_forward_are_entered_and_no_other() { + let machine: &[(u8, &[(u8, u8)])] = &[(0, &[(4, 4), (0x20, 0x49), (9, 9), (0x0a, 0x0a)])]; + assert_eq!(entered(0..=0xff, machine), [0, 4, 9, 0x0a, 0x20]); + } + + /// The defect's shape: a window of 0..=0x3f with one bridge forwarding 1. + /// No bus past it is entered — the 0x40 onwards a 256-bus walk read as + /// functions — and none between that nothing forwards. + #[test] + fn no_bus_past_the_window_and_none_nothing_forwards_is_entered() { + assert_eq!(entered(0..=0x3f, &[(0, &[(1, 1)])]), [0, 1]); + assert_eq!(entered(0..=0x3f, &[]), [0]); + } + + /// A window that starts above bus 0 enters its own first bus. + #[test] + fn a_window_is_entered_at_its_first_bus() { + assert_eq!(entered(0x10..=0x1f, &[(0x10, &[(0x11, 0x12)]), (0x11, &[(0x12, 0x12)])]), [0x10, 0x11, 0x12]); + } + + /// A nested bridge is forwarded only what the bridge above it is: a + /// subordinate past its parent's is narrowed to it, and a secondary past + /// it is not entered at all. + #[test] + fn a_bridge_below_another_is_forwarded_only_what_that_one_is() { + let mut walk = Buses::new(0..=0xff); + assert_eq!(walk.enter(), Some(0)); + assert_eq!(walk.bridge(0, numbers(0, 1, 3)), Ok(1..=3)); + assert_eq!(walk.enter(), Some(1)); + assert_eq!(walk.bridge(1, numbers(1, 2, 9)), Ok(2..=3)); + assert_eq!(walk.bridge(1, numbers(1, 4, 4)), Err(Unforwarded::Outside { secondary: 4, limit: 3 })); + assert_eq!(walk.enter(), Some(2)); + assert_eq!(walk.bridge(2, numbers(2, 3, 3)), Ok(3..=3)); + assert_eq!(walk.enter(), Some(3)); + assert_eq!(walk.enter(), None); + } + + /// The window's own last bus bounds a bridge on its first. + #[test] + fn a_bridge_is_not_forwarded_past_the_window() { + let mut walk = Buses::new(0..=0x3f); + walk.enter(); + assert_eq!(walk.bridge(0, numbers(0, 0x40, 0x40)), Err(Unforwarded::Outside { secondary: 0x40, limit: 0x3f })); + assert_eq!(walk.bridge(0, numbers(0, 0x3f, 0xff)), Ok(0x3f..=0x3f)); + } + + /// Every bridge that could make the walk go round, or enter one bus + /// twice, is refused: an unconfigured one, one pointing at its own bus or + /// above it in the tree, one forwarding nothing, and a second claiming a + /// bus a first already forwards. + #[test] + fn a_bridge_that_would_loop_or_repeat_a_bus_is_refused() { + let mut walk = Buses::new(0..=0xff); + assert_eq!(walk.enter(), Some(0)); + assert_eq!(walk.bridge(0, numbers(0, 0, 0)), Err(Unforwarded::NotBelow { secondary: 0 })); + assert_eq!(walk.bridge(0, numbers(0, 5, 4)), Err(Unforwarded::Inverted { secondary: 5, subordinate: 4 })); + assert_eq!(walk.bridge(0, numbers(0, 2, 8)), Ok(2..=8)); + assert_eq!(walk.bridge(0, numbers(0, 2, 2)), Err(Unforwarded::Taken { secondary: 2 })); + assert_eq!(walk.enter(), Some(2)); + assert_eq!(walk.bridge(2, numbers(2, 2, 2)), Err(Unforwarded::NotBelow { secondary: 2 })); + assert_eq!(walk.bridge(2, numbers(2, 1, 8)), Err(Unforwarded::NotBelow { secondary: 1 })); + assert_eq!(walk.enter(), None); + } + + /// Every possible bus-number dword, on every bus of a full window: the + /// walk ends, and enters no bus twice and none outside the window. + #[test] + fn no_bridge_numbering_makes_the_walk_repeat_or_leave_the_window() { + for first in [0u8, 0x10] { + let mut walk = Buses::new(first..=0x3f); + let mut seen = [false; 256]; + let mut steps = 0u32; + while let Some(bus) = walk.enter() { + assert!((first..=0x3f).contains(&bus), "{bus:#x} is outside the window"); + assert!(!seen[usize::from(bus)], "{bus:#x} entered twice"); + seen[usize::from(bus)] = true; + steps += 1; + assert!(steps <= 256); + for secondary in 0..=0xffu8 { + for subordinate in [0u8, secondary, secondary.saturating_add(3), 0xff] { + let _ = walk.bridge(bus, numbers(bus, secondary, subordinate)); + } + } + } + assert_eq!(steps, 0x40 - u32::from(first)); + } + } +} diff --git a/kernel/pci/src/lib.rs b/kernel/pci/src/lib.rs index 79570482638..74b98df1b06 100644 --- a/kernel/pci/src/lib.rs +++ b/kernel/pci/src/lib.rs @@ -31,6 +31,7 @@ pub mod aperture; pub mod af; pub mod bar; pub mod bridge; +pub mod buses; pub mod caps; pub mod express; pub mod msi; diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index 893c82976f2..77f7e44d670 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -285,24 +285,17 @@ pub fn init(rsdp_addr: u64) { if cpu::inw(control.port(0)) & SCI_EN != 0 { "ACPI" } else { "legacy" }, ); isa::fill(ROW, Function { name: "the ACPI fixed hardware", runs, irqs: vec![], wires: vec![sci] }); - let (ecam, lock) = (ecam(rsdp_addr), global_lock(facs)); + let (ecam, lock) = (ecam(), global_lock(facs)); let hardware = Hardware { fixed, control, ec, rsdp: rsdp_addr, ecam, lock }; let was = HARDWARE.swap(Box::into_raw(Box::new(hardware)), Ordering::Release); assert!(was.is_null(), "acpi: init ran twice"); } -/// The ECAM window as the MCFG's first allocation bounds it (PCI Firmware -/// Specification 3.3, Table 4-3: the segment group at +8 of the entry, the -/// first and last bus at +10 and +11). -fn ecam(rsdp_addr: u64) -> Option { - let (mcfg, base) = toyos_acpi::ecam_base(crate::drivers::acpi::direct_phys(), rsdp_addr).ok()?; - let entry = toyos_acpi::MCFG_FIRST_ENTRY; - let (segment, first_bus, last_bus) = (mcfg.u16_at(entry + 8)?, mcfg.byte(entry + 10)?, mcfg.byte(entry + 11)?); - if first_bus > last_bus { - log!("acpi: the MCFG's window ends at bus {last_bus:#x}, before its first, {first_bus:#x}: no configuration access is mediated"); - return None; - } - Some(Ecam { base, segment, first_bus, last_bus }) +/// The first ECAM window PCI enumeration walked. +fn ecam() -> Option { + let window = *crate::drivers::pci::windows().first()?; + let (first_bus, last_bus) = window.buses().into_inner(); + Some(Ecam { base: window.base(), segment: window.segment(), first_bus, last_bus }) } /// The Global Lock of the FACS the FADT names, said by name where there is diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index 87db44ac6a5..43ed55aaa2c 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -121,22 +121,32 @@ fn refuse(what: &str, error: TableError) -> Option { None } -/// Given the RSDP address from UEFI, parse XSDT -> MCFG -> return the ECAM -/// base address and the PCI segment group it serves. -pub fn find_ecam_base(rsdp_addr: u64) -> Option<(u64, u16)> { +/// Every ECAM window the MCFG names that this kernel walks, each structure it +/// refuses said by name; none where the MCFG is unusable. +pub fn ecam_windows(rsdp_addr: u64) -> Vec { log!("ACPI: RSDP at {rsdp_addr:#x}"); - let (mcfg, base) = match toyos_acpi::ecam_base(direct_phys(), rsdp_addr) { + let allocations = match toyos_acpi::ecam_allocations(direct_phys(), rsdp_addr) { Ok(found) => found, - Err(e) => return refuse("MCFG", e), + Err(e) => return refuse("MCFG", e).unwrap_or_default(), }; - // PCI Firmware Specification 3.3, Table 4-3: the entry's segment group - // follows its base, inside the entry `ecam_base` already bounded. - let segment = mcfg - .u16_at(toyos_acpi::MCFG_FIRST_ENTRY + 8) - .expect("ecam_base bounded the whole first allocation structure"); - log!("ACPI: MCFG found at {:#x}", mcfg.base()); - log!("ACPI: ECAM base address: {base:#x}"); - Some((base, segment)) + log!("ACPI: MCFG found at {:#x}", allocations.table_base()); + let mut windows = Vec::new(); + for (index, item) in allocations.enumerate() { + match item { + Ok(window) => { + log!( + "ACPI: ECAM window: segment {} buses {:#04x}..={:#04x}, bus 0 at {:#x}", + window.segment(), + window.buses().start(), + window.buses().end(), + window.base() + ); + windows.push(window); + } + Err(why) => log!("ACPI: MCFG allocation {index} refused: {why:?}"), + } + } + windows } /// FADT revision and the IA-PC boot architecture flags. diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index adfa2773c63..5b7bc6e868b 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -1,6 +1,7 @@ use alloc::vec::Vec; -use toyos_pci::{bar, bridge, caps, msi, msix}; +use toyos_acpi::Allocation; +use toyos_pci::{bar, bridge, buses, caps, msi, msix}; use crate::mm::Mmio; use crate::mm::policy::MmioPolicy; @@ -104,11 +105,12 @@ pub fn stop_bus_mastering(config: Mmio) { /// One function's ECAM window: PCIe extended config space, PCI 3.0 §7.2.2. /// -/// Declared once because two readers deal in it — `PciDevice::new` carves it and +/// Declared once because two readers deal in it — an ECAM window carves it and /// the reset-time xHCI stop rebuilds one from a bare address. pub const CONFIG_BYTES: u64 = 4096; -/// PCI device identified by ECAM base + Bus/Device/Function. +/// A PCI function, by its bus, device and function and the configuration +/// space its ECAM window gives it. #[derive(Clone, Copy)] pub struct PciDevice { mmio: Mmio, @@ -118,13 +120,6 @@ pub struct PciDevice { } impl PciDevice { - fn new(ecam: &crate::mm::Mmio, bus: u8, dev: u8, func: u8) -> Self { - let offset = ((bus as u64) << 20) - | ((dev as u64) << 15) - | ((func as u64) << 12); - Self { mmio: ecam.subregion(offset, CONFIG_BYTES), bus, dev, func } - } - /// A function over a caller-owned config-space window, for the cap self-test to drive the real walk over lists no hardware in reach produces. #[cfg(feature = "boot-actuators")] pub(crate) fn over_config(mmio: crate::mm::Mmio) -> Self { @@ -533,41 +528,94 @@ impl<'a> Iterator for CapabilityIter<'a> { } } -/// The window [`enumerate`] walked. -static ECAM: crate::sync::Lock> = crate::sync::Lock::new(None); +/// One ECAM window, mapped over exactly the buses its allocation decodes. +#[derive(Clone, Copy)] +struct Window { + allocation: Allocation, + mmio: Mmio, +} + +impl Window { + /// The configuration space of a function on a bus this window decodes. + fn function(&self, bus: u8, dev: u8, func: u8) -> Option { + if !self.allocation.holds(bus) { + return None; + } + let offset = (u64::from(bus - self.allocation.buses().start()) << 20) + | (u64::from(dev) << 15) + | (u64::from(func) << 12); + Some(self.mmio.subregion(offset, CONFIG_BYTES)) + } +} + +/// The windows [`enumerate`] walked. +static WINDOWS: crate::sync::Lock> = crate::sync::Lock::new(Vec::new()); /// The configuration space of the function at this address, whether or not -/// one answers there: an absent function reads all ones. +/// one answers there: an absent function reads all ones. `None` on a bus no +/// window decodes. pub fn function_window(bus: u8, dev: u8, func: u8) -> Option { - (*ECAM.lock()).map(|ecam| PciDevice::new(&ecam, bus, dev, func).mmio) + WINDOWS.lock().iter().find_map(|window| window.function(bus, dev, func)) +} + +/// The ECAM windows [`enumerate`] walked, in the order it walked them. +pub fn windows() -> Vec { + WINDOWS.lock().iter().map(|window| window.allocation).collect() } /// The most functions [`enumerate`] will hand back; the rest are logged, not enumerated. const MAX_DEVICES: usize = 256; -/// Every PCIe function ECAM decodes, in bus/device/function order; drivers must select all matches, not the first. -pub fn enumerate(ecam: &crate::mm::Mmio) -> Vec { +/// Every PCIe function on a bus something forwards configuration cycles to, +/// window by window and in bus/device/function order within one; drivers +/// must select all matches, not the first. +/// +/// **Only the buses each window decodes are mapped, and only those a bridge +/// forwards are read** ([`buses`]): past a window's end bus its addresses are +/// other hardware's, which reads as functions that are not there. +pub fn enumerate(allocations: &[Allocation]) -> Vec { log!("PCI: Enumerating devices..."); - *ECAM.lock() = Some(*ecam); + let windows: Vec = allocations + .iter() + .map(|&allocation| { + let (start, bytes) = allocation.decoded(); + Window { allocation, mmio: crate::mm::paging::map_mmio(start, bytes, MmioPolicy::Uncacheable) } + }) + .collect(); + *WINDOWS.lock() = windows.clone(); let mut found: Vec = Vec::new(); - 'scan: for bus in 0..=255u16 { - for dev in 0..32u8 { - let root = PciDevice::new(ecam, bus as u8, dev, 0); - if root.vendor_id() == INVALID_VENDOR { continue; } - - let funcs = if root.read_config_u8(HEADER_TYPE) & MULTI_FUNCTION != 0 { 8 } else { 1 }; - for func in 0..funcs { - let pci = PciDevice::new(ecam, bus as u8, dev, func); - if pci.vendor_id() == INVALID_VENDOR { continue; } - - print_device(&pci); - if found.len() == MAX_DEVICES { - log!("PCI: more than {} functions decoded; the rest are not enumerated", - MAX_DEVICES); - break 'scan; + 'scan: for window in &windows { + let function = |bus: u8, dev: u8, func: u8| { + let mmio = window.function(bus, dev, func).expect("the walk enters only buses its window decodes"); + PciDevice { mmio, bus, dev, func } + }; + let mut walk = buses::Buses::new(window.allocation.buses()); + while let Some(bus) = walk.enter() { + for dev in 0..32u8 { + let root = function(bus, dev, 0); + if root.vendor_id() == INVALID_VENDOR { continue; } + + let funcs = if root.read_config_u8(HEADER_TYPE) & MULTI_FUNCTION != 0 { 8 } else { 1 }; + for func in 0..funcs { + let pci = function(bus, dev, func); + if pci.vendor_id() == INVALID_VENDOR { continue; } + + print_device(&pci); + if found.len() == MAX_DEVICES { + log!("PCI: more than {} functions decoded; the rest are not enumerated", + MAX_DEVICES); + break 'scan; + } + if pci.read_config_u8(HEADER_TYPE) & !MULTI_FUNCTION == bridge::HEADER_TYPE_BRIDGE { + match walk.bridge(bus, pci.read_config_u32(buses::BUS_NUMBERS)) { + Ok(reached) => log!("PCI {bus:02x}:{dev:02x}.{func}: forwards buses {:02x}..={:02x}", + reached.start(), reached.end()), + Err(why) => log!("PCI {bus:02x}:{dev:02x}.{func}: no bus below it is read: {why}"), + } + } + found.push(pci); } - found.push(pci); } } } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index cbf0cf818a5..6bf47b05586 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -108,7 +108,6 @@ mod late_panic { } } -use crate::mm::policy::MmioPolicy; use alloc::boxed::Box; use arch::{cpu, percpu}; use drivers::{acpi, gop, pci, serial, virtio_console, virtio_gpu, virtio_sound, xhci}; @@ -433,10 +432,11 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { let t_periph = clock::nanos_since_boot(); - let (ecam_base, pci_segment) = acpi::find_ecam_base(kernel_args.rsdp_addr) - .expect("ACPI: failed to find ECAM base address"); - let ecam = mm::paging::map_mmio(ecam_base, 256 * 32 * 8 * 4096, MmioPolicy::Uncacheable); - let pci_devices = pci::enumerate(&ecam); + let ecam_windows = acpi::ecam_windows(kernel_args.rsdp_addr); + let pci_devices = pci::enumerate(&ecam_windows); + // Every window is on one segment group; a machine with no window has no + // function to name one for. + let pci_segment = ecam_windows.first().map_or(0, |window| window.segment()); // Before any driver `init`: this sizes every BAR on the machine, and the // spec's probe takes memory decode off the function it is sizing for the // length of it. Nothing has bound yet, so nothing is mid-transfer. diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 448a000089d..f5d3f6dba50 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -736,13 +736,25 @@ pub enum Profile { /// [`Profile::Virt`] with its SMMUv3 and two of QEMU's `iommu-testdev`, a /// function that writes where it is told to through the unit. VirtSmmu, + /// [`Profile::Virt`] with its ECAM in low memory (`highmem-ecam=off`): a + /// 16 MiB window whose MCFG decodes buses 0 to 0x0f, with RAM right past + /// it. The one machine QEMU makes whose MCFG decodes fewer than 256 + /// buses: q35's is as long as firmware programs `PCIEXBAR`, which OVMF + /// sets to 256 MiB and QEMU offers no option to change. + VirtLowEcam, } impl Profile { /// The architecture this machine is. pub fn arch(self) -> Arch { match self { - Self::Virt | Self::VirtNoRng | Self::VirtEl2 | Self::VirtEl2NoVhe | Self::VirtTcg | Self::VirtSmmu => { + Self::Virt + | Self::VirtNoRng + | Self::VirtEl2 + | Self::VirtEl2NoVhe + | Self::VirtTcg + | Self::VirtSmmu + | Self::VirtLowEcam => { Arch::Aarch64 } Self::Headless @@ -939,7 +951,7 @@ pub const NVME_SMALL: u64 = 128 * 1024 * 1024; impl Profile { fn shape(self) -> Shape { match self { - Self::VirtEl2 | Self::VirtEl2NoVhe | Self::VirtTcg => Self::Virt.shape(), + Self::VirtEl2 | Self::VirtEl2NoVhe | Self::VirtTcg | Self::VirtLowEcam => Self::Virt.shape(), Self::VirtNoRng => Shape { rng: false, ..Self::Virt.shape() }, Self::VirtSmmu => Shape { smmu: Smmu::WithTestdev, ..Self::Virt.shape() }, Self::Virt => Shape { @@ -2138,6 +2150,7 @@ fn qemu_command( Profile::VirtEl2 | Profile::VirtEl2NoVhe => { format!("{},gic-version=3,virtualization=on", arch.machine()) } + Profile::VirtLowEcam => format!("{},gic-version=3,highmem-ecam=off", arch.machine()), _ => format!("{},gic-version=3", arch.machine()), }; match shape.smmu { diff --git a/tests/toyos.rs b/tests/toyos.rs index 5dc34455148..bff47270963 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -298,6 +298,7 @@ const SCREEN_TESTS: &[(&str, qemu::Profile)] = &[ ("virt_random_differs", qemu::Profile::Virt), ("virt_smmu", qemu::Profile::VirtSmmu), ("virt_no_seed_refused", qemu::Profile::VirtNoRng), + ("virt_low_ecam", qemu::Profile::VirtLowEcam), ]; /// The tests whose machine shape *is* the test, each on a boot of its own. @@ -2067,6 +2068,47 @@ fn virt_no_seed_refused(profile: qemu::Profile, test_config: &Path) -> Result<() Ok(()) } +/// A machine whose MCFG decodes sixteen buses, with RAM past them: the kernel +/// maps those sixteen buses' megabytes and no more, and enumerates the +/// functions QEMU put there and nothing else, and the boot goes on. +fn virt_low_ecam(profile: qemu::Profile, test_config: &Path) -> Result<(), String> { + const WINDOW: &str = "ACPI: ECAM window: segment 0 buses 0x00..=0x0f, bus 0 at 0x3f000000"; + const MAPPED: &str = "mmio: 0x3f000000+0x1000000 "; + const SPAWNED: &str = "spawn: /system/bin/logkeeper pid="; + /// What QEMU puts on bus 0 of this profile: the host bridge, the xHCI and + /// the virtio-rng, as `vendor=… device=…`. + const FUNCTIONS: &[&str] = &[ + "00:00.0 [0600] vendor=1b36 device=0008", + "00:01.0 [0c03] vendor=1033 device=0194", + "00:02.0 [00ff] vendor=1af4 device=1005", + ]; + let options = BootOptions { profile, ready_marker: "control registers: SCTLR_EL1=", ..Default::default() }; + let argv = qemu::profile_argv(&options); + if !argv.iter().any(|a| a.contains("highmem-ecam=off")) { + return Err(format!("the machine keeps its ECAM high: {argv:?}")); + } + let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); + let rest = qemu.drain_until(Duration::from_secs(30), |l| l.contains(SPAWNED)); + let serial = format!("{}\n{rest}", qemu.boot_log()); + // What was read and what was mapped before what was said about it. + pci_inventory(&serial).map_err(|why| format!("{why}\nserial:\n{serial}"))?; + let rows: Vec<&str> = serial.lines().filter_map(|l| l.split(" PCI ").nth(1)).collect(); + let found: Vec<&str> = rows.iter().map(|row| row.split(" prog_if=").next().unwrap_or(row)).collect(); + if found != FUNCTIONS { + return Err(format!("the kernel enumerated {found:#?}, and QEMU put {FUNCTIONS:#?} on the window's buses")); + } + if let Some(wider) = serial.lines().find(|l| l.contains("mmio: 0x3f000000+") && !l.contains(MAPPED)) { + return Err(format!("the window was mapped wider than its buses: {wider:?}\nserial:\n{serial}")); + } + for want in [MAPPED, WINDOW, SPAWNED] { + if !serial.contains(want) { + return Err(format!("{want:?} not on the PL011\nserial:\n{serial}")); + } + } + eprintln!(" [virt] {WINDOW}; {} functions: {found:?}", found.len()); + Ok(()) +} + /// The SMMUv3 armed from the IORT, judged by what two of QEMU's /// `iommu-testdev` can and cannot write through it /// (`kernel/src/arch/aarch64/smmu/selftest.rs`): `GBPA` read back aborting, @@ -2937,6 +2979,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re "virt_random_differs" => virt_random_differs(profile), "virt_no_seed_refused" => virt_no_seed_refused(profile, test_config), "virt_smmu" => virt_smmu(profile, test_config), + "virt_low_ecam" => virt_low_ecam(profile, test_config), "virt_mask_windows" => virt_mask_windows(profile), "virt_irq_storm" => { // The CPU floods itself with SGIs until the timer has fired a @@ -5463,7 +5506,7 @@ fn acpi_table_inventory(log: &str) -> Result<(), String> { ("APIC", "ACPI: MADT cpus="), ("FACP", "ACPI: reset register "), ("HPET", "clock: HPET at "), - ("MCFG", "ACPI: ECAM base address: "), + ("MCFG", "ACPI: ECAM window: "), ]; for (signature, decoded) in NEEDED { if !log.lines().any(|l| l.contains(&format!("ACPI: {signature} at ")) && l.contains("checksummed")) diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index 794c4089665..90a738c0183 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -21,6 +21,7 @@ mod fadt; mod gtdt; mod iort; mod madt; +mod mcfg; mod resource; mod spcr; @@ -41,6 +42,7 @@ pub use madt::{ Polarity, SourceOverride, Trigger, MADT_ENTRIES, }; pub use gtdt::{gtdt, Gtdt, TimerInterrupt, GTDT_NEEDED}; +pub use mcfg::{ecam_allocations, Allocation, AllocationRefused, Allocations}; pub use resource::{memory_windows, ResourceError, MAX_LIST_BYTES}; pub use spcr::{spcr, Gas, SerialInterface, Spcr, GAS_SYSTEM_MEMORY, SPCR_NEEDED}; @@ -364,22 +366,6 @@ impl Iterator for DefinitionBlocks

{ } } -/// PCI Firmware Specification 3.3, Table 4-3: the first allocation structure -/// sits one 8-byte reserved field past the header, and its base address is the -/// ECAM window's. -pub const MCFG_FIRST_ENTRY: usize = SDT_HEADER_LEN + 8; -const MCFG_ENTRY_LEN: usize = 16; - -/// The ECAM base address the MCFG's first allocation structure names. -pub fn ecam_base(phys: P, rsdp_addr: u64) -> Result<(Table

, u64), TableError> { - let needed = MCFG_FIRST_ENTRY + MCFG_ENTRY_LEN; - let mcfg = find_table(phys, rsdp_addr, b"MCFG", needed)?; - let base = mcfg - .u64_at(MCFG_FIRST_ENTRY) - .ok_or(TableError::Length { declared: mcfg.len as u32, needed })?; - Ok((mcfg, base)) -} - /// IA-PC HPET Specification 1.0a, Table 3: the event timer block's Generic /// Address Structure starts at 40 and its 64-bit address at 44. const HPET_BASE_ADDRESS: usize = 44; diff --git a/toyos-acpi/src/mcfg.rs b/toyos-acpi/src/mcfg.rs new file mode 100644 index 00000000000..99849eeda30 --- /dev/null +++ b/toyos-acpi/src/mcfg.rs @@ -0,0 +1,156 @@ +//! The MCFG (PCI Firmware Specification 3.3, §4.1.2, Table 4-3): where each +//! PCI segment group's configuration space is, and which of its buses that +//! window decodes. +//! +//! **A window decodes its start bus through its end bus and nothing past +//! either.** Its base is bus 0's address whatever bus it starts at, so a bus +//! outside the range still computes an address — one the platform gave to +//! something else: an interrupt controller, a timer, flash, DRAM, each of which +//! reads as functions that do not exist. What this hands back is the range +//! alongside the base, and every reader indexes only inside it. +//! +//! **One segment group is served**: the first allocation accepted names it, +//! an allocation on another is refused by name, and each bus of the served +//! group is decoded by at most one window. + +use core::ops::RangeInclusive; + +use crate::{find_table, Phys, Table, TableError, SDT_HEADER_LEN}; + +/// The first allocation structure, one 8-byte reserved field past the header. +const FIRST_ENTRY: usize = SDT_HEADER_LEN + 8; +/// One allocation structure: base at 0, segment group at 8, start and end bus +/// at 10 and 11, four reserved bytes. +const ENTRY_LEN: usize = 16; +/// Each bus is 32 devices of 8 functions of 4 KiB: one megabyte, so the bus +/// number is address bits 27:20 (PCIe Base 6.0 §7.2.2). +const BUS_SHIFT: u32 = 20; + +/// One allocation structure, accepted: its window ends inside the address +/// space and starts on a bus boundary, and its start bus is not past its end. +/// Only [`Allocations`] makes one. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Allocation { + base: u64, + segment: u16, + start_bus: u8, + end_bus: u8, +} + +impl Allocation { + /// Bus 0's configuration space, whatever bus the window starts at. + pub fn base(&self) -> u64 { + self.base + } + + pub fn segment(&self) -> u16 { + self.segment + } + + /// The buses the window decodes, never empty. + pub fn buses(&self) -> RangeInclusive { + self.start_bus..=self.end_bus + } + + /// The bytes the window decodes, as `(address of the start bus, length)`. + pub fn decoded(&self) -> (u64, u64) { + let start = self.base + (u64::from(self.start_bus) << BUS_SHIFT); + let buses = u64::from(self.end_bus - self.start_bus) + 1; + (start, buses << BUS_SHIFT) + } + + pub fn holds(&self, bus: u8) -> bool { + self.buses().contains(&bus) + } +} + +/// Why one allocation structure is not a window this decode hands out. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum AllocationRefused { + /// The end bus is below the start bus: a window of no bus. + Inverted { start_bus: u8, end_bus: u8 }, + /// A base off a bus boundary, where no bus number is an address bit. + Misaligned { base: u64 }, + /// The window's last byte is past the end of the address space. + Wraps { base: u64, end_bus: u8 }, + /// The table ends inside this structure. + Partial { bytes: usize }, + /// On a segment group other than the one served. + OtherSegment { segment: u16, served: u16 }, + /// Buses an earlier window of the served group already decodes. + Overlaps { start_bus: u8, end_bus: u8 }, +} + +/// Every allocation structure of an MCFG, in table order. +#[derive(Clone, Copy)] +pub struct Allocations

{ + table: Table

, + next: usize, + served: Option, + /// One bit per bus of the served group that a window already decodes. + claimed: [u64; 4], +} + +impl

Allocations

{ + /// Where firmware put the MCFG. + pub fn table_base(&self) -> u64 { + self.table.base + } +} + +/// The MCFG at `rsdp_addr`, opened for its first allocation structure. +pub fn ecam_allocations(phys: P, rsdp_addr: u64) -> Result, TableError> { + let table = find_table(phys, rsdp_addr, b"MCFG", FIRST_ENTRY + ENTRY_LEN)?; + Ok(Allocations { table, next: FIRST_ENTRY, served: None, claimed: [0; 4] }) +} + +impl Iterator for Allocations

{ + type Item = Result; + + fn next(&mut self) -> Option { + let at = self.next; + let left = self.table.len.checked_sub(at).filter(|&left| left > 0)?; + if left < ENTRY_LEN { + self.next = self.table.len; + return Some(Err(AllocationRefused::Partial { bytes: left })); + } + self.next = at + ENTRY_LEN; + // Inside the table: `left` covers the whole structure. + let (base, segment, start_bus, end_bus) = ( + self.table.u64_at(at)?, + self.table.u16_at(at + 8)?, + self.table.byte(at + 10)?, + self.table.byte(at + 11)?, + ); + Some(self.accept(Allocation { base, segment, start_bus, end_bus })) + } +} + +impl

Allocations

{ + fn accept(&mut self, window: Allocation) -> Result { + let Allocation { base, segment, start_bus, end_bus } = window; + if end_bus < start_bus { + return Err(AllocationRefused::Inverted { start_bus, end_bus }); + } + if base & ((1 << BUS_SHIFT) - 1) != 0 { + return Err(AllocationRefused::Misaligned { base }); + } + // The last byte of the end bus, which `decoded` then sums without a check. + let last = (u64::from(end_bus) + 1) << BUS_SHIFT; + if base.checked_add(last - 1).is_none() { + return Err(AllocationRefused::Wraps { base, end_bus }); + } + let served = *self.served.get_or_insert(segment); + if segment != served { + return Err(AllocationRefused::OtherSegment { segment, served }); + } + let buses = start_bus..=end_bus; + if buses.clone().any(|bus| self.claimed[usize::from(bus / 64)] & 1 << (bus % 64) != 0) { + return Err(AllocationRefused::Overlaps { start_bus, end_bus }); + } + for bus in buses { + self.claimed[usize::from(bus / 64)] |= 1 << (bus % 64); + } + Ok(window) + } +} diff --git a/toyos-acpi/tests/corpus.rs b/toyos-acpi/tests/corpus.rs index 1c17895b268..7dc6e13fd68 100644 --- a/toyos-acpi/tests/corpus.rs +++ b/toyos-acpi/tests/corpus.rs @@ -12,7 +12,7 @@ use common::{declare_len, entry, madt, rsdp, sdt, t14_root_bridge, xsdt, Machine use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ - definition_blocks, dsdt_address, ecam_base, ecdt, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, + definition_blocks, dsdt_address, ecam_allocations, ecdt, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, EcRefused, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, Register, Reset, SourceOverride, Table, TableError, Trigger, ECDT_NEEDED, @@ -33,7 +33,7 @@ fn a_length_shorter_than_the_fixed_part_is_refused_with_both_numbers() { let m = Machine { regions }; assert_eq!( - ecam_base(m, RSDP_AT).err(), + ecam_allocations(m, RSDP_AT).err(), Some(TableError::Length { declared: 40, needed: 60 }), "the walk stops at a table of the right signature it cannot use, and says why" ); @@ -351,7 +351,7 @@ const FIXTURES: &[(&str, &[u8], u64)] = &[ /// **No panic and no unbounded walk, over every byte of every table this crate /// decodes.** Each byte of each fixture takes each of its 255 other values in -/// turn, and `ecam_base`, `hpet_base`, `rtc_century`, `iapc_boot_arch` and the +/// turn, and the MCFG walk, `hpet_base`, `rtc_century`, `iapc_boot_arch` and the /// MADT walk to exhaustion all run over it. A panic anywhere — including the /// reader's own, which fires on a read no bound accepted — reds this. /// @@ -392,7 +392,10 @@ fn no_single_byte_mutation_of_a_real_table_panics_or_runs_away() { .collect(); let m = Machine { regions: ®ions }; - let _ = ecam_base(m, rsdp_at); + if let Ok(allocations) = ecam_allocations(m, rsdp_at) { + // One item per 16 bytes, and a partial one at the end. + assert!(allocations.count() <= MAX_TABLE_LEN / 16 + 1, "{which}[{offset}]={value:#04x}: the MCFG walk is not ending"); + } let _ = hpet_base(m, rsdp_at); let _ = rtc_century(m, rsdp_at); let _ = iapc_boot_arch(m, rsdp_at); diff --git a/toyos-acpi/tests/fixtures.rs b/toyos-acpi/tests/fixtures.rs index ac97851a0e0..39a7396dcb5 100644 --- a/toyos-acpi/tests/fixtures.rs +++ b/toyos-acpi/tests/fixtures.rs @@ -7,7 +7,7 @@ use common::{t14_root_bridge, Machine, OVMF_ROOT_BRIDGE}; use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ - century_of, definition_blocks, dsdt_address, ecam_base, find_table, fixed_hardware, hpet_base, iapc_boot_arch, + century_of, definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, FixedHardware, IoApicEntry, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, SmiCmd, SourceOverride, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, @@ -80,12 +80,16 @@ fn the_madt_names_the_two_cpus_that_boot_and_the_chip_that_interrupts_them() { ); } -/// `ACPI: MCFG found at 0x7fb76000` and `ACPI: ECAM base address: 0xb0000000`. +/// `ACPI: MCFG found at 0x7fb76000` and `ACPI: ECAM window: segment 0 buses +/// 0x00..=0xff, bus 0 at 0xb0000000`: q35 publishes one allocation, of every bus. #[test] fn the_mcfg_names_the_ecam_window_the_pci_walk_used() { - let (mcfg, base) = ecam_base(machine(), RSDP).expect("MCFG"); - assert_eq!(mcfg.base(), 0x7fb7_6000); - assert_eq!(base, 0xb000_0000); + let allocations = ecam_allocations(machine(), RSDP).expect("MCFG"); + assert_eq!(allocations.table_base(), 0x7fb7_6000); + let windows: Vec<_> = allocations.collect(); + let [Ok(window)] = windows[..] else { panic!("q35 publishes one allocation, and the walk found {windows:?}") }; + assert_eq!((window.base(), window.segment(), window.buses()), (0xb000_0000, 0, 0..=0xff)); + assert_eq!(window.decoded(), (0xb000_0000, 0x1000_0000)); } /// `ACPI: HPET at 0xfed00000`. diff --git a/toyos-acpi/tests/mcfg.rs b/toyos-acpi/tests/mcfg.rs new file mode 100644 index 00000000000..b885d3a06b0 --- /dev/null +++ b/toyos-acpi/tests/mcfg.rs @@ -0,0 +1,130 @@ +//! The MCFG's allocations: the window each decodes, and every structure that +//! is not a window refused by name while the walk goes on to the next. + +mod common; + +use common::{declare_len, rsdp, sdt, xsdt, Machine}; +use toyos_acpi::{ecam_allocations, Allocation, AllocationRefused}; + +const RSDP_AT: u64 = 0x1_0000; +const XSDT_AT: u64 = 0x2_0000; +const MCFG_AT: u64 = 0x3_0000; + +/// An MCFG over `(base, segment, start bus, end bus)` structures. +fn mcfg(allocations: &[(u64, u16, u8, u8)]) -> Vec { + let mut body = vec![0u8; 8]; + for (base, segment, start, end) in allocations { + body.extend_from_slice(&base.to_le_bytes()); + body.extend_from_slice(&segment.to_le_bytes()); + body.extend_from_slice(&[*start, *end, 0, 0, 0, 0]); + } + sdt(b"MCFG", 1, &body) +} + +/// Every item the walk answers over `table`. +fn allocations(table: &[u8]) -> Vec> { + let head = rsdp(XSDT_AT, 2, 36); + let root = xsdt(&[MCFG_AT]); + let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (MCFG_AT, table)]; + ecam_allocations(Machine { regions }, RSDP_AT).expect("an MCFG with one structure at least").collect() +} + +/// A window as `(base, segment, first bus, last bus)`. +type Window = (u64, u16, u8, u8); + +fn window(allocation: &Allocation) -> Window { + (allocation.base(), allocation.segment(), *allocation.buses().start(), *allocation.buses().end()) +} + +/// [`allocations`], each accepted one as a [`Window`]. +fn walk(table: &[u8]) -> Vec> { + allocations(table).iter().map(|item| item.as_ref().map(window).map_err(|why| *why)).collect() +} + +/// The laptop's shape: one allocation of buses 0 to 0x3f. Its window is +/// 64 MiB, and the 192 MiB past it that 256 buses would name is not in it. +#[test] +fn a_window_of_sixty_four_buses_decodes_sixty_four_megabytes() { + let table = mcfg(&[(0xf800_0000, 0, 0, 0x3f)]); + assert_eq!(walk(&table), [Ok((0xf800_0000, 0, 0, 0x3f))]); + let accepted = allocations(&table)[0].unwrap(); + assert_eq!(accepted.decoded(), (0xf800_0000, 0x0400_0000)); + assert!(accepted.holds(0x3f)); + assert!(!accepted.holds(0x40)); +} + +/// The base is bus 0's whatever bus the window starts at, so the window's +/// first byte is its start bus's and no bus below it is held. +#[test] +fn a_window_starting_above_bus_zero_starts_at_its_start_bus() { + let accepted = allocations(&mcfg(&[(0xe000_0000, 0, 0x10, 0x1f)]))[0].unwrap(); + assert_eq!(accepted.decoded(), (0xe100_0000, 0x0100_0000)); + assert!(!accepted.holds(0)); + assert!(!accepted.holds(0x0f)); + assert!(accepted.holds(0x10)); + assert!(!accepted.holds(0x20)); +} + +/// Each malformed structure is refused by name, and the walk goes on to the +/// next: a structure the walk refused names no served segment either. +#[test] +fn a_malformed_structure_is_refused_and_the_next_is_still_read() { + let found = walk(&mcfg(&[ + (0xe000_0000, 1, 0x20, 0x1f), + (0xe008_0000, 1, 0, 0xff), + (0xffff_ffff_fff0_0000, 1, 0, 1), + (0xe000_0000, 0, 0, 0xff), + ])); + assert_eq!( + found, + [ + Err(AllocationRefused::Inverted { start_bus: 0x20, end_bus: 0x1f }), + Err(AllocationRefused::Misaligned { base: 0xe008_0000 }), + Err(AllocationRefused::Wraps { base: 0xffff_ffff_fff0_0000, end_bus: 1 }), + Ok((0xe000_0000, 0, 0, 0xff)), + ] + ); +} + +/// A window whose last byte is the address space's last byte ends inside it; +/// one bus further does not. +#[test] +fn a_window_ending_on_the_last_byte_is_accepted_and_one_bus_more_wraps() { + let top = 0xffff_ffff_fff0_0000; + let accepted = allocations(&mcfg(&[(top, 0, 0, 0)]))[0].unwrap(); + assert_eq!(accepted.decoded(), (top, 0x10_0000)); + let base = 0xffff_ffff_f000_0000; + assert_eq!(walk(&mcfg(&[(base, 0, 0xff, 0xff)])), [Ok((base, 0, 0xff, 0xff))]); + assert_eq!(walk(&mcfg(&[(top, 0, 1, 1)])), [Err(AllocationRefused::Wraps { base: top, end_bus: 1 })]); +} + +/// The first window accepted names the served segment group; a structure on +/// another is refused, and so is one naming a bus a window already decodes. +#[test] +fn another_segment_and_an_overlap_are_refused() { + let found = walk(&mcfg(&[ + (0xe000_0000, 0, 0, 0x7f), + (0x1_0000_0000, 1, 0, 0xff), + (0xf000_0000, 0, 0x7f, 0x80), + (0xe000_0000, 0, 0x80, 0xff), + ])); + assert_eq!( + found, + [ + Ok((0xe000_0000, 0, 0, 0x7f)), + Err(AllocationRefused::OtherSegment { segment: 1, served: 0 }), + Err(AllocationRefused::Overlaps { start_bus: 0x7f, end_bus: 0x80 }), + Ok((0xe000_0000, 0, 0x80, 0xff)), + ] + ); +} + +/// A table that ends inside a structure ends the walk on a refusal of that +/// structure, after every whole one. +#[test] +fn a_table_ending_inside_a_structure_refuses_the_part() { + let mut table = mcfg(&[(0xe000_0000, 0, 0, 0xff), (0xf000_0000, 1, 0, 0xff)]); + let declared = table.len() as u32 - 7; + declare_len(&mut table, declared); + assert_eq!(walk(&table), [Ok((0xe000_0000, 0, 0, 0xff)), Err(AllocationRefused::Partial { bytes: 9 })]); +} From ec8fce3d1a7295c9e5b0b1d5a4059ebb2de9c526 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 15:42:35 +0200 Subject: [PATCH 2/3] Each MCFG window is one decoded type, scanned linearly over its own buses and refused where it cannot be mapped exactly The review of #840 sent back the bridge walk: inside a window the host bridge decodes every bus, and a bus nothing routes reads all ones, so the in-window scan was never the defect; reading and mapping past end_bus was. `toyos_pci::buses` and the bridge branch of `enumerate` are deleted, and each window's start..=end buses are scanned in order. That restores peer root buses inside a window, which the walk had lost. `toyos_acpi::EcamWindow` replaces both `toyos_acpi::Allocation` and `toyos_userbound::firmware::Ecam`. `EcamWindow::decode` is its one constructor. It owns the arithmetic both ways: `offset` (bus, device, function to a byte offset from the start bus) and `locate` (address to register), which the kernel's scan and the `acpi` claim's policy now read instead of each doing its own. The policy takes every window, not the first. Segment group 0 alone is served, wherever its windows sit in the table, instead of whichever group the first window named. The window carries no segment; `toyos_acpi::SEGMENT_GROUP` names it. `EcamWindow::mappable` refuses by name a window the kernel cannot map exactly. That is one off the map grain (x86 `map_mmio` maps whole 2 MiB pages, and `map_2m` would silently remap a neighbouring cached MiB uncached), one reaching `DIRECT_MAP_WINDOW`, and one over memory firmware's map lists as memory (on AArch64 that was a panic on firmware input). Refusals are logged eight at a time and then counted. Issues: `pci-is-walked-from-one-root-bus-per-ecam-window` shrinks to `pci-is-enumerated-on-segment-group-0-alone`, and `an-ecam-window-past-the-cpus-physical-address-width-is-mapped` is filed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- Cargo.lock | 1 + bootloader/src/watchdog.rs | 8 +- ...e-cpus-physical-address-width-is-mapped.md | 23 ++ ...-is-enumerated-on-segment-group-0-alone.md | 24 ++ ...alked-from-one-root-bus-per-ecam-window.md | 25 -- ...oes-only-what-must-precede-the-handover.md | 2 +- kernel/pci/src/buses.rs | 235 ------------------ kernel/pci/src/lib.rs | 1 - kernel/src/arch/aarch64/paging.rs | 3 + kernel/src/arch/x86_64/acpi_mode.rs | 24 +- kernel/src/arch/x86_64/paging.rs | 4 + kernel/src/drivers/acpi.rs | 30 ++- kernel/src/drivers/pci.rs | 54 ++-- kernel/src/main.rs | 5 +- toyos-acpi/src/lib.rs | 2 +- toyos-acpi/src/mcfg.rs | 185 +++++++++----- toyos-acpi/tests/fixtures.rs | 2 +- toyos-acpi/tests/mcfg.rs | 158 +++++++++--- toyos-bootmap/src/lib.rs | 2 +- toyos-userbound/Cargo.toml | 3 + toyos-userbound/src/firmware.rs | 50 ++-- toyos-userbound/tests/firmware.rs | 48 ++-- 22 files changed, 411 insertions(+), 478 deletions(-) create mode 100644 issues/an-ecam-window-past-the-cpus-physical-address-width-is-mapped.md create mode 100644 issues/pci-is-enumerated-on-segment-group-0-alone.md delete mode 100644 issues/pci-is-walked-from-one-root-bus-per-ecam-window.md delete mode 100644 kernel/pci/src/buses.rs diff --git a/Cargo.lock b/Cargo.lock index 7977733927d..23a50bf9824 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6029,6 +6029,7 @@ name = "toyos-userbound" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-acpi", "toyos-bootmap", ] diff --git a/bootloader/src/watchdog.rs b/bootloader/src/watchdog.rs index a8b96205010..2a4d9dbc6a0 100644 --- a/bootloader/src/watchdog.rs +++ b/bootloader/src/watchdog.rs @@ -60,11 +60,9 @@ pub fn arm(system_table: &SystemTable, rsdp_addr: u64, cmdline: &str) { Ok(allocations) => allocations, Err(e) => return refused(format_args!("this machine's tables name no ECAM ({e:?})")), }; - // Bus 0 of segment group 0 is where the chipset is, and only a window - // that decodes it has bus 0's address at its base. - let Some(ecam) = allocations.flatten().find(|window| window.segment() == 0 && window.holds(0)).map(|window| window.base()) - else { - return refused(format_args!("no ECAM window of the MCFG's decodes bus 0 of segment group 0")); + // Bus 0 is where the chipset is. + let Some(ecam) = allocations.flatten().find(|window| window.holds(0)).map(|window| window.base()) else { + return refused(format_args!("no ECAM window of the MCFG's decodes bus 0")); }; // The MCFG's word for where configuration space is, checked against // firmware's own map before anything dereferences it. diff --git a/issues/an-ecam-window-past-the-cpus-physical-address-width-is-mapped.md b/issues/an-ecam-window-past-the-cpus-physical-address-width-is-mapped.md new file mode 100644 index 00000000000..d66b5406a51 --- /dev/null +++ b/issues/an-ecam-window-past-the-cpus-physical-address-width-is-mapped.md @@ -0,0 +1,23 @@ +--- +status: open +kind: defect +opened: 2026-10-10 +--- + +# An ECAM window past the CPU's physical address width is mapped + +`acpi::ecam_windows` (`kernel/src/drivers/acpi.rs`) bounds each MCFG window by +`toyos_bootmap::DIRECT_MAP_WINDOW` (128 TiB), the first address the direct +map cannot hold, and not by the physical address width the CPU implements. A +window an MCFG puts between the two is mapped: on x86-64 the page-table entry +then sets bits past `MAXPHYADDR`, which are reserved, and the scan's first +read of it is a page fault in the kernel, which panics on firmware input. + +**Evidence:** the code. Every MCFG this tree has read puts its window below +4 GiB. + +Owner: orchestrator. Exit: the limit `ecam_windows` passes to +`EcamWindow::mappable` is the lower of `DIRECT_MAP_WINDOW` and the width the +CPU reports (CPUID leaf `0x8000_0008` `EAX[7:0]` on x86-64, +`ID_AA64MMFR0_EL1.PARange` on AArch64), so such a window is refused +`PastLimit` by name. diff --git a/issues/pci-is-enumerated-on-segment-group-0-alone.md b/issues/pci-is-enumerated-on-segment-group-0-alone.md new file mode 100644 index 00000000000..a0b18c8a2d1 --- /dev/null +++ b/issues/pci-is-enumerated-on-segment-group-0-alone.md @@ -0,0 +1,24 @@ +--- +status: open +kind: defect +opened: 2026-10-10 +--- + +# PCI is enumerated on segment group 0 alone + +`toyos_acpi::ecam_allocations` (`toyos-acpi/src/mcfg.rs`) refuses an MCFG +window on any segment group but 0 (`AllocationRefused::OtherSegment`), so +`pci::enumerate` (`kernel/src/drivers/pci.rs`) never reads its functions, and +`PciDevice` and `pcidev`'s `Machine` carry one segment for every function. A +machine with more than one segment group has functions this kernel does not +see. The `acpi` claim's configuration accesses are bounded to segment group 0 +too (`toyos_userbound::firmware::config`). + +**Evidence:** the code. Every machine this tree has booted publishes segment +group 0 alone: QEMU's q35 and `virt` MCFGs (`toyos-acpi/tests/fixtures.rs`, +`virt_low_ecam`) and the T14's. + +Owner: orchestrator. Exit: a function carries its segment group from +enumeration to `pcidev`'s inventory, every window the MCFG names is +enumerated whatever its group, and a host test of the decode accepts two +groups' windows. diff --git a/issues/pci-is-walked-from-one-root-bus-per-ecam-window.md b/issues/pci-is-walked-from-one-root-bus-per-ecam-window.md deleted file mode 100644 index 0b04522ed97..00000000000 --- a/issues/pci-is-walked-from-one-root-bus-per-ecam-window.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-10 ---- - -# PCI is walked from one root bus per ECAM window, on one segment group - -`pci::enumerate` (`kernel/src/drivers/pci.rs`) enters each ECAM window at its -start bus and from there only the buses a bridge forwards -(`toyos_pci::buses`). A host bridge whose root bus is any other bus of the -window — a second root complex in one segment group, as on multi-socket and -many-die parts, or QEMU's `pxb-pcie` — is never entered, and its functions are -not enumerated. Functions on a segment group other than the first window's are -not enumerated either: `toyos_acpi::ecam_allocations` refuses those windows -`OtherSegment`, and `PciDevice` carries no segment. - -What names every root bus is each host bridge's bus range: `_BBN` and `_CRS` -in AML, or the bus descriptor of `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL`'s -`Configuration()`, which the loader already reads for memory windows -(`bootloader/src/rootbridge.rs`) and with its segment number beside it. q35's -and `virt`'s guests have one root bus, at their window's start. - -Exit: the kernel enters every root bus firmware names, on every segment group -it names, and a guest with a `pxb-pcie` enumerates the functions behind it. diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index 9c05b2a802f..00657eb9855 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -375,7 +375,7 @@ Each stage lands on its own, in this order. `boot-deadline=` boots, and to `arch::watchdog::init` on the others: through `mm::init`, ACPI, interrupts, HPET calibration, the RTC read, PCI enumeration, `pcidev::publish` and `iommu::init`. - - The kernel arms as its first act, before `mm::init`: `toyos_acpi::ecam_base` + - The kernel arms as its first act, before `mm::init`: `toyos_acpi::ecam_allocations` and the bus-0 scan `bootloader/src/watchdog.rs` does, through a boot map that maps bus 0's ECAM window. What stays unbounded is the loader's code from `ExitBootServices` to the jump. diff --git a/kernel/pci/src/buses.rs b/kernel/pci/src/buses.rs deleted file mode 100644 index db8147db84b..00000000000 --- a/kernel/pci/src/buses.rs +++ /dev/null @@ -1,235 +0,0 @@ -//! Which buses of one ECAM window a walk of configuration space enters -//! (PCI-to-PCI Bridge Architecture Specification 1.2 §3.2.5.3 to §3.2.5.5). -//! -//! **A bus is entered only where something forwards configuration cycles to -//! it**: the window's first bus, which its host bridge decodes, and each -//! bridge's secondary bus, inside what the bus above that bridge is forwarded. -//! A bus number nothing forwards is one no cycle reaches, and what answers at -//! its address in the window is not a function. -//! -//! A bridge forwards a cycle for a bus from its secondary through its -//! subordinate, so a bus below it is forwarded what both bounds allow: the -//! narrower of the two subordinates. Every bus is entered at most once and -//! only above the bus whose bridge named it, so a walk ends however a machine -//! numbered its bridges, and it enters buses in ascending order. - -use core::ops::RangeInclusive; - -/// The dword of a Type 1 header holding its primary, secondary and -/// subordinate bus numbers, in that byte order from its low end. -pub const BUS_NUMBERS: u64 = 0x18; - -/// Why a bridge's secondary bus is not entered. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Unforwarded { - /// Its secondary is not above the bus the bridge sits on: unconfigured - /// (zero) or numbered against the tree. - NotBelow { secondary: u8 }, - /// Its subordinate is below its secondary, so it forwards no bus. - Inverted { secondary: u8, subordinate: u8 }, - /// Its secondary is past the last bus the bridge itself is forwarded. - Outside { secondary: u8, limit: u8 }, - /// Another bridge already forwards its secondary. - Taken { secondary: u8 }, -} - -impl core::fmt::Display for Unforwarded { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match *self { - Self::NotBelow { secondary } => write!(f, "its secondary bus {secondary:#04x} is not below it"), - Self::Inverted { secondary, subordinate } => { - write!(f, "its subordinate bus {subordinate:#04x} is below its secondary {secondary:#04x}") - } - Self::Outside { secondary, limit } => { - write!(f, "its secondary bus {secondary:#04x} is past {limit:#04x}, the last bus it is forwarded") - } - Self::Taken { secondary } => write!(f, "another bridge already forwards bus {secondary:#04x}"), - } - } -} - -/// The walk's state over one window. -pub struct Buses { - /// Buses forwarded and not yet entered, one bit each. - pending: [u64; 4], - /// Buses forwarded at all, entered or not. - forwarded: [u64; 4], - /// For each forwarded bus, the last bus a cycle through it reaches. - limit: [u8; 256], -} - -fn bit(set: &[u64; 4], bus: u8) -> bool { - set[usize::from(bus / 64)] & 1 << (bus % 64) != 0 -} - -fn set(set: &mut [u64; 4], bus: u8, on: bool) { - let word = &mut set[usize::from(bus / 64)]; - *word = if on { *word | 1 << (bus % 64) } else { *word & !(1 << (bus % 64)) }; -} - -impl Buses { - /// A walk of the window over `buses`, its host bridge forwarding the first. - pub fn new(buses: RangeInclusive) -> Self { - let mut walk = Self { pending: [0; 4], forwarded: [0; 4], limit: [0; 256] }; - let (first, last) = buses.into_inner(); - if first <= last { - walk.forward(first, last); - } - walk - } - - fn forward(&mut self, bus: u8, limit: u8) { - set(&mut self.pending, bus, true); - set(&mut self.forwarded, bus, true); - self.limit[usize::from(bus)] = limit; - } - - /// The lowest bus forwarded and not yet entered, now entered. - pub fn enter(&mut self) -> Option { - let (word, bits) = self.pending.iter().enumerate().find(|(_, bits)| **bits != 0)?; - let bus = (word * 64) as u8 + bits.trailing_zeros() as u8; - set(&mut self.pending, bus, false); - Some(bus) - } - - /// A bridge on entered bus `bus`, its [`BUS_NUMBERS`] dword `numbers`: - /// the buses it forwards, its secondary to be entered, or why not. - pub fn bridge(&mut self, bus: u8, numbers: u32) -> Result, Unforwarded> { - let [_, secondary, subordinate, _] = numbers.to_le_bytes(); - let limit = self.limit[usize::from(bus)]; - if secondary <= bus { - return Err(Unforwarded::NotBelow { secondary }); - } - if subordinate < secondary { - return Err(Unforwarded::Inverted { secondary, subordinate }); - } - if secondary > limit { - return Err(Unforwarded::Outside { secondary, limit }); - } - if bit(&self.forwarded, secondary) { - return Err(Unforwarded::Taken { secondary }); - } - let reached = subordinate.min(limit); - self.forward(secondary, reached); - Ok(secondary..=reached) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::vec::Vec; - - /// A bridge's bus-number dword: primary, secondary, subordinate. - fn numbers(primary: u8, secondary: u8, subordinate: u8) -> u32 { - u32::from_le_bytes([primary, secondary, subordinate, 0x00]) - } - - /// A machine as `(bus, bridges on it as (secondary, subordinate))`, walked - /// as the kernel walks it: every bus entered, in the order entered. - fn entered(window: RangeInclusive, machine: &[(u8, &[(u8, u8)])]) -> Vec { - let mut walk = Buses::new(window); - let mut out = Vec::new(); - while let Some(bus) = walk.enter() { - out.push(bus); - for (_, bridges) in machine.iter().filter(|(on, _)| *on == bus) { - for &(secondary, subordinate) in *bridges { - let _ = walk.bridge(bus, numbers(bus, secondary, subordinate)); - } - } - } - out - } - - /// Root ports on bus 0 forwarding 4, 9, 0x0a and a hot-plug range from - /// 0x20, each secondary entered once and in ascending order whatever - /// order the ports were read in. - #[test] - fn the_buses_bridges_forward_are_entered_and_no_other() { - let machine: &[(u8, &[(u8, u8)])] = &[(0, &[(4, 4), (0x20, 0x49), (9, 9), (0x0a, 0x0a)])]; - assert_eq!(entered(0..=0xff, machine), [0, 4, 9, 0x0a, 0x20]); - } - - /// The defect's shape: a window of 0..=0x3f with one bridge forwarding 1. - /// No bus past it is entered — the 0x40 onwards a 256-bus walk read as - /// functions — and none between that nothing forwards. - #[test] - fn no_bus_past_the_window_and_none_nothing_forwards_is_entered() { - assert_eq!(entered(0..=0x3f, &[(0, &[(1, 1)])]), [0, 1]); - assert_eq!(entered(0..=0x3f, &[]), [0]); - } - - /// A window that starts above bus 0 enters its own first bus. - #[test] - fn a_window_is_entered_at_its_first_bus() { - assert_eq!(entered(0x10..=0x1f, &[(0x10, &[(0x11, 0x12)]), (0x11, &[(0x12, 0x12)])]), [0x10, 0x11, 0x12]); - } - - /// A nested bridge is forwarded only what the bridge above it is: a - /// subordinate past its parent's is narrowed to it, and a secondary past - /// it is not entered at all. - #[test] - fn a_bridge_below_another_is_forwarded_only_what_that_one_is() { - let mut walk = Buses::new(0..=0xff); - assert_eq!(walk.enter(), Some(0)); - assert_eq!(walk.bridge(0, numbers(0, 1, 3)), Ok(1..=3)); - assert_eq!(walk.enter(), Some(1)); - assert_eq!(walk.bridge(1, numbers(1, 2, 9)), Ok(2..=3)); - assert_eq!(walk.bridge(1, numbers(1, 4, 4)), Err(Unforwarded::Outside { secondary: 4, limit: 3 })); - assert_eq!(walk.enter(), Some(2)); - assert_eq!(walk.bridge(2, numbers(2, 3, 3)), Ok(3..=3)); - assert_eq!(walk.enter(), Some(3)); - assert_eq!(walk.enter(), None); - } - - /// The window's own last bus bounds a bridge on its first. - #[test] - fn a_bridge_is_not_forwarded_past_the_window() { - let mut walk = Buses::new(0..=0x3f); - walk.enter(); - assert_eq!(walk.bridge(0, numbers(0, 0x40, 0x40)), Err(Unforwarded::Outside { secondary: 0x40, limit: 0x3f })); - assert_eq!(walk.bridge(0, numbers(0, 0x3f, 0xff)), Ok(0x3f..=0x3f)); - } - - /// Every bridge that could make the walk go round, or enter one bus - /// twice, is refused: an unconfigured one, one pointing at its own bus or - /// above it in the tree, one forwarding nothing, and a second claiming a - /// bus a first already forwards. - #[test] - fn a_bridge_that_would_loop_or_repeat_a_bus_is_refused() { - let mut walk = Buses::new(0..=0xff); - assert_eq!(walk.enter(), Some(0)); - assert_eq!(walk.bridge(0, numbers(0, 0, 0)), Err(Unforwarded::NotBelow { secondary: 0 })); - assert_eq!(walk.bridge(0, numbers(0, 5, 4)), Err(Unforwarded::Inverted { secondary: 5, subordinate: 4 })); - assert_eq!(walk.bridge(0, numbers(0, 2, 8)), Ok(2..=8)); - assert_eq!(walk.bridge(0, numbers(0, 2, 2)), Err(Unforwarded::Taken { secondary: 2 })); - assert_eq!(walk.enter(), Some(2)); - assert_eq!(walk.bridge(2, numbers(2, 2, 2)), Err(Unforwarded::NotBelow { secondary: 2 })); - assert_eq!(walk.bridge(2, numbers(2, 1, 8)), Err(Unforwarded::NotBelow { secondary: 1 })); - assert_eq!(walk.enter(), None); - } - - /// Every possible bus-number dword, on every bus of a full window: the - /// walk ends, and enters no bus twice and none outside the window. - #[test] - fn no_bridge_numbering_makes_the_walk_repeat_or_leave_the_window() { - for first in [0u8, 0x10] { - let mut walk = Buses::new(first..=0x3f); - let mut seen = [false; 256]; - let mut steps = 0u32; - while let Some(bus) = walk.enter() { - assert!((first..=0x3f).contains(&bus), "{bus:#x} is outside the window"); - assert!(!seen[usize::from(bus)], "{bus:#x} entered twice"); - seen[usize::from(bus)] = true; - steps += 1; - assert!(steps <= 256); - for secondary in 0..=0xffu8 { - for subordinate in [0u8, secondary, secondary.saturating_add(3), 0xff] { - let _ = walk.bridge(bus, numbers(bus, secondary, subordinate)); - } - } - } - assert_eq!(steps, 0x40 - u32::from(first)); - } - } -} diff --git a/kernel/pci/src/lib.rs b/kernel/pci/src/lib.rs index 74b98df1b06..79570482638 100644 --- a/kernel/pci/src/lib.rs +++ b/kernel/pci/src/lib.rs @@ -31,7 +31,6 @@ pub mod aperture; pub mod af; pub mod bar; pub mod bridge; -pub mod buses; pub mod caps; pub mod express; pub mod msi; diff --git a/kernel/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs index 2943851f5ee..639bcff9d09 100644 --- a/kernel/src/arch/aarch64/paging.rs +++ b/kernel/src/arch/aarch64/paging.rs @@ -612,6 +612,9 @@ pub fn activate_kernel() { unsafe { kernel_root().activate() }; } +/// What [`map_mmio`] maps at: a 4 KiB page. +pub const MMIO_GRAIN: u64 = PAGE_4K; + /// Map a device's registers, or the scanout, into the direct map: 4 KiB pages /// exactly over `[phys, phys + size)`, a block where a whole 2 MiB page is /// asked for and free. No invalidation is owed, since only an invalid entry is diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index 77f7e44d670..19ad1db2b5f 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -76,15 +76,16 @@ use alloc::boxed::Box; use alloc::format; use alloc::string::String; use alloc::vec; +use alloc::vec::Vec; use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use core::sync::atomic::AtomicU32; use toyos_abi::acpi::{Access, AcpiInfo, Block, Refused, Space, Width, FIXED_POWER_BUTTON}; use toyos_abi::syscall::SyscallError; -use toyos_acpi::{Ec, FixedHardware, LegacyMode, PowerButton}; +use toyos_acpi::{Ec, EcamWindow, FixedHardware, LegacyMode, PowerButton}; use toyos_userbound::firmware::{ - self, CallRate, Ecam, FirmwareCall, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict, + self, CallRate, FirmwareCall, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict, }; use toyos_userbound::Ports; @@ -119,8 +120,8 @@ struct Hardware { /// Or why it is none a holder can be handed. ec: Result, rsdp: u64, - /// The window configuration space is reached through, as the MCFG bounds it. - ecam: Option, + /// The windows configuration space is reached through, as the MCFG bounds them. + ecam: Vec, lock: GlobalLock, } @@ -285,18 +286,12 @@ pub fn init(rsdp_addr: u64) { if cpu::inw(control.port(0)) & SCI_EN != 0 { "ACPI" } else { "legacy" }, ); isa::fill(ROW, Function { name: "the ACPI fixed hardware", runs, irqs: vec![], wires: vec![sci] }); - let (ecam, lock) = (ecam(), global_lock(facs)); + let (ecam, lock) = (crate::drivers::pci::windows(), global_lock(facs)); let hardware = Hardware { fixed, control, ec, rsdp: rsdp_addr, ecam, lock }; let was = HARDWARE.swap(Box::into_raw(Box::new(hardware)), Ordering::Release); assert!(was.is_null(), "acpi: init ran twice"); } -/// The first ECAM window PCI enumeration walked. -fn ecam() -> Option { - let window = *crate::drivers::pci::windows().first()?; - let (first_bus, last_bus) = window.buses().into_inner(); - Some(Ecam { base: window.base(), segment: window.segment(), first_bus, last_bus }) -} /// The Global Lock of the FACS the FADT names, said by name where there is /// none or it is none this kernel takes: a FACS that does not decode, and one @@ -698,7 +693,7 @@ fn write_port(passed: &PortAt, value: u64) { /// One configuration access, decided and, where it is a read, made through /// the window the MCFG names, which the policy bounded the function by. fn config(hardware: &Hardware, _acting: &Holder, segment: u16, function: PciFunction, offset: u16, width: Width, write: bool) -> Result { - let at = firmware::config(hardware.ecam, segment, function, offset, width, write)?; + let at = firmware::config(&hardware.ecam, segment, function, offset, width, write)?; let PciFunction { bus, device, function } = at.function(); let space = crate::drivers::pci::function_window(bus, device, function).expect("a machine with a claimable ACPI row enumerated its PCI functions"); let offset = u64::from(at.offset()); @@ -734,7 +729,7 @@ fn memory(hardware: &Hardware, acting: &Holder, request: &mut Access, width: Wid let memory = Memory { map, mapped_end: crate::mm::direct_map_end().get(), - ecam: hardware.ecam, + ecam: &hardware.ecam, devices: driven.iter().copied().chain(bars), facs: hardware.lock.facs().map(|facs| facs.span), uncached, @@ -750,8 +745,7 @@ fn memory(hardware: &Hardware, acting: &Holder, request: &mut Access, width: Wid Ok(0) } (MemoryVerdict::AsConfig(function, offset), _) => { - let segment = hardware.ecam.expect("the policy answered a configuration access from an ECAM window").segment; - config(hardware, acting, segment, function, offset, width, write.is_some()) + config(hardware, acting, toyos_acpi::SEGMENT_GROUP, function, offset, width, write.is_some()) } (MemoryVerdict::Refused(refused), _) => Err(refused), } diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs index 38525436e6e..29798570c88 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -876,6 +876,10 @@ pub fn with_driven_windows(f: impl FnOnce(&[(u64, u64)]) -> T) -> T { f(&DRIVEN.lock()) } +/// What [`map_mmio`] maps at: whole 2 MiB pages, so a window off this grain +/// takes its neighbours' bytes with it. +pub const MMIO_GRAIN: u64 = PAGE_2M; + /// Free function (not a method): the lock and the shootdown are separate /// statements. Not optional — `map_2m` may change memory type under a /// sibling's stale entry, which is SDM Vol. 3A §11.12.4 undefined behaviour. diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index 43ed55aaa2c..dbc02af51d8 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -121,31 +121,47 @@ fn refuse(what: &str, error: TableError) -> Option { None } -/// Every ECAM window the MCFG names that this kernel walks, each structure it -/// refuses said by name; none where the MCFG is unusable. -pub fn ecam_windows(rsdp_addr: u64) -> Vec { +/// The most refused MCFG allocation structures said one by one: a table of +/// up to [`toyos_acpi::MAX_TABLE_LEN`] bytes holds tens of thousands. +const REFUSALS_SAID: usize = 8; + +/// Every ECAM window the MCFG names that this kernel maps exactly, each +/// structure it refuses said by name; none where the MCFG is unusable. +pub fn ecam_windows(rsdp_addr: u64) -> Vec { log!("ACPI: RSDP at {rsdp_addr:#x}"); let allocations = match toyos_acpi::ecam_allocations(direct_phys(), rsdp_addr) { Ok(found) => found, Err(e) => return refuse("MCFG", e).unwrap_or_default(), }; log!("ACPI: MCFG found at {:#x}", allocations.table_base()); - let mut windows = Vec::new(); + let (mut windows, mut refused) = (Vec::new(), 0usize); for (index, item) in allocations.enumerate() { - match item { + let mappable = |window: toyos_acpi::EcamWindow| { + let limit = toyos_bootmap::DIRECT_MAP_WINDOW; + window.mappable(crate::mm::paging::MMIO_GRAIN, limit, crate::mm::firmware_map()).map(|()| window) + }; + match item.and_then(mappable) { Ok(window) => { log!( "ACPI: ECAM window: segment {} buses {:#04x}..={:#04x}, bus 0 at {:#x}", - window.segment(), + toyos_acpi::SEGMENT_GROUP, window.buses().start(), window.buses().end(), window.base() ); windows.push(window); } - Err(why) => log!("ACPI: MCFG allocation {index} refused: {why:?}"), + Err(why) => { + refused += 1; + if refused <= REFUSALS_SAID { + log!("ACPI: MCFG allocation {index} refused: {why:?}"); + } + } } } + if refused > REFUSALS_SAID { + log!("ACPI: {} more MCFG allocations refused", refused - REFUSALS_SAID); + } windows } diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index 5b7bc6e868b..c6aedfee9de 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; -use toyos_acpi::Allocation; -use toyos_pci::{bar, bridge, buses, caps, msi, msix}; +use toyos_acpi::EcamWindow; +use toyos_pci::{bar, bridge, caps, msi, msix}; use crate::mm::Mmio; use crate::mm::policy::MmioPolicy; @@ -528,23 +528,17 @@ impl<'a> Iterator for CapabilityIter<'a> { } } -/// One ECAM window, mapped over exactly the buses its allocation decodes. +/// One ECAM window, mapped over exactly the buses it decodes. #[derive(Clone, Copy)] struct Window { - allocation: Allocation, + ecam: EcamWindow, mmio: Mmio, } impl Window { /// The configuration space of a function on a bus this window decodes. fn function(&self, bus: u8, dev: u8, func: u8) -> Option { - if !self.allocation.holds(bus) { - return None; - } - let offset = (u64::from(bus - self.allocation.buses().start()) << 20) - | (u64::from(dev) << 15) - | (u64::from(func) << 12); - Some(self.mmio.subregion(offset, CONFIG_BYTES)) + Some(self.mmio.subregion(self.ecam.offset(bus, dev, func)?, CONFIG_BYTES)) } } @@ -559,27 +553,27 @@ pub fn function_window(bus: u8, dev: u8, func: u8) -> Option { } /// The ECAM windows [`enumerate`] walked, in the order it walked them. -pub fn windows() -> Vec { - WINDOWS.lock().iter().map(|window| window.allocation).collect() +pub fn windows() -> Vec { + WINDOWS.lock().iter().map(|window| window.ecam).collect() } /// The most functions [`enumerate`] will hand back; the rest are logged, not enumerated. const MAX_DEVICES: usize = 256; -/// Every PCIe function on a bus something forwards configuration cycles to, -/// window by window and in bus/device/function order within one; drivers -/// must select all matches, not the first. +/// Every PCIe function the windows decode, window by window and in +/// bus/device/function order within one; drivers must select all matches, +/// not the first. /// -/// **Only the buses each window decodes are mapped, and only those a bridge -/// forwards are read** ([`buses`]): past a window's end bus its addresses are -/// other hardware's, which reads as functions that are not there. -pub fn enumerate(allocations: &[Allocation]) -> Vec { +/// **Each window is mapped and read over the buses it decodes and no +/// further**: past its end bus the addresses are other hardware's, which +/// reads as functions that are not there. +pub fn enumerate(ecam: &[EcamWindow]) -> Vec { log!("PCI: Enumerating devices..."); - let windows: Vec = allocations + let windows: Vec = ecam .iter() - .map(|&allocation| { - let (start, bytes) = allocation.decoded(); - Window { allocation, mmio: crate::mm::paging::map_mmio(start, bytes, MmioPolicy::Uncacheable) } + .map(|&ecam| { + let (start, bytes) = ecam.decoded(); + Window { ecam, mmio: crate::mm::paging::map_mmio(start, bytes, MmioPolicy::Uncacheable) } }) .collect(); *WINDOWS.lock() = windows.clone(); @@ -587,11 +581,10 @@ pub fn enumerate(allocations: &[Allocation]) -> Vec { let mut found: Vec = Vec::new(); 'scan: for window in &windows { let function = |bus: u8, dev: u8, func: u8| { - let mmio = window.function(bus, dev, func).expect("the walk enters only buses its window decodes"); + let mmio = window.function(bus, dev, func).expect("the scan reads only buses its window decodes"); PciDevice { mmio, bus, dev, func } }; - let mut walk = buses::Buses::new(window.allocation.buses()); - while let Some(bus) = walk.enter() { + for bus in window.ecam.buses() { for dev in 0..32u8 { let root = function(bus, dev, 0); if root.vendor_id() == INVALID_VENDOR { continue; } @@ -607,13 +600,6 @@ pub fn enumerate(allocations: &[Allocation]) -> Vec { MAX_DEVICES); break 'scan; } - if pci.read_config_u8(HEADER_TYPE) & !MULTI_FUNCTION == bridge::HEADER_TYPE_BRIDGE { - match walk.bridge(bus, pci.read_config_u32(buses::BUS_NUMBERS)) { - Ok(reached) => log!("PCI {bus:02x}:{dev:02x}.{func}: forwards buses {:02x}..={:02x}", - reached.start(), reached.end()), - Err(why) => log!("PCI {bus:02x}:{dev:02x}.{func}: no bus below it is read: {why}"), - } - } found.push(pci); } } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 6bf47b05586..0c0ff395ddd 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -434,13 +434,10 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { let ecam_windows = acpi::ecam_windows(kernel_args.rsdp_addr); let pci_devices = pci::enumerate(&ecam_windows); - // Every window is on one segment group; a machine with no window has no - // function to name one for. - let pci_segment = ecam_windows.first().map_or(0, |window| window.segment()); // Before any driver `init`: this sizes every BAR on the machine, and the // spec's probe takes memory decode off the function it is sizing for the // length of it. Nothing has bound yet, so nothing is mid-transfer. - pcidev::publish(&pci_devices, pci_segment, maps, kernel_args.root_bridge_windows()); + pcidev::publish(&pci_devices, toyos_acpi::SEGMENT_GROUP, maps, kernel_args.root_bridge_windows()); #[cfg(feature = "boot-actuators")] if actuator::pci_cap_selftest() { drivers::virtio::cap_selftest(); diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index 90a738c0183..0b3b03c0c28 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -42,7 +42,7 @@ pub use madt::{ Polarity, SourceOverride, Trigger, MADT_ENTRIES, }; pub use gtdt::{gtdt, Gtdt, TimerInterrupt, GTDT_NEEDED}; -pub use mcfg::{ecam_allocations, Allocation, AllocationRefused, Allocations}; +pub use mcfg::{ecam_allocations, AllocationRefused, Allocations, ConfigRegister, EcamWindow, SEGMENT_GROUP}; pub use resource::{memory_windows, ResourceError, MAX_LIST_BYTES}; pub use spcr::{spcr, Gas, SerialInterface, Spcr, GAS_SYSTEM_MEMORY, SPCR_NEEDED}; diff --git a/toyos-acpi/src/mcfg.rs b/toyos-acpi/src/mcfg.rs index 99849eeda30..a34042a5ecf 100644 --- a/toyos-acpi/src/mcfg.rs +++ b/toyos-acpi/src/mcfg.rs @@ -1,20 +1,22 @@ -//! The MCFG (PCI Firmware Specification 3.3, §4.1.2, Table 4-3): where each -//! PCI segment group's configuration space is, and which of its buses that +//! The MCFG (PCI Firmware Specification 3.3, §4.1.2, Table 4-3): where PCI +//! segment group 0's configuration space is, and which of its buses each //! window decodes. //! //! **A window decodes its start bus through its end bus and nothing past //! either.** Its base is bus 0's address whatever bus it starts at, so a bus //! outside the range still computes an address — one the platform gave to //! something else: an interrupt controller, a timer, flash, DRAM, each of which -//! reads as functions that do not exist. What this hands back is the range -//! alongside the base, and every reader indexes only inside it. +//! reads as functions that do not exist. [`EcamWindow`] is the only reader of +//! that arithmetic, and it answers only inside the range. //! -//! **One segment group is served**: the first allocation accepted names it, -//! an allocation on another is refused by name, and each bus of the served -//! group is decoded by at most one window. +//! **Segment group 0 alone is served** ([`SEGMENT_GROUP`]): it is the one every +//! machine has, and a window on another is refused by name. Each of its buses +//! is decoded by at most one window. use core::ops::RangeInclusive; +use toyos_abi::boot::MemoryMapEntry; + use crate::{find_table, Phys, Table, TableError, SDT_HEADER_LEN}; /// The first allocation structure, one 8-byte reserved field past the header. @@ -25,33 +27,71 @@ const ENTRY_LEN: usize = 16; /// Each bus is 32 devices of 8 functions of 4 KiB: one megabyte, so the bus /// number is address bits 27:20 (PCIe Base 6.0 §7.2.2). const BUS_SHIFT: u32 = 20; - -/// One allocation structure, accepted: its window ends inside the address -/// space and starts on a bus boundary, and its start bus is not past its end. -/// Only [`Allocations`] makes one. +const DEVICE_SHIFT: u32 = 15; +const FUNCTION_SHIFT: u32 = 12; +/// One function's configuration space. +const FUNCTION_BYTES: u64 = 1 << FUNCTION_SHIFT; + +/// The PCI segment group every window this decode accepts is on. +pub const SEGMENT_GROUP: u16 = 0; + +/// One window of segment group 0's configuration space, as an allocation +/// structure names it: it starts on a bus boundary, its start bus is not past +/// its end, and its last byte is inside the address space. Only +/// [`EcamWindow::decode`] makes one. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Allocation { +pub struct EcamWindow { base: u64, - segment: u16, start_bus: u8, end_bus: u8, } -impl Allocation { +/// A function's configuration register, as an address in a window names it. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct ConfigRegister { + pub bus: u8, + pub device: u8, + pub function: u8, + pub offset: u16, +} + +impl EcamWindow { + /// One allocation structure's bytes, accepted or refused by name. + pub fn decode(entry: &[u8; ENTRY_LEN]) -> Result { + let [b0, b1, b2, b3, b4, b5, b6, b7, s0, s1, start_bus, end_bus, ..] = *entry; + let base = u64::from_le_bytes([b0, b1, b2, b3, b4, b5, b6, b7]); + let segment = u16::from_le_bytes([s0, s1]); + if segment != SEGMENT_GROUP { + return Err(AllocationRefused::OtherSegment { segment }); + } + if end_bus < start_bus { + return Err(AllocationRefused::Inverted { start_bus, end_bus }); + } + if base & ((1 << BUS_SHIFT) - 1) != 0 { + return Err(AllocationRefused::Misaligned { base }); + } + // The last byte of the end bus: what every other method then sums + // without a check. + if base.checked_add(((u64::from(end_bus) + 1) << BUS_SHIFT) - 1).is_none() { + return Err(AllocationRefused::Wraps { base, end_bus }); + } + Ok(Self { base, start_bus, end_bus }) + } + /// Bus 0's configuration space, whatever bus the window starts at. pub fn base(&self) -> u64 { self.base } - pub fn segment(&self) -> u16 { - self.segment - } - /// The buses the window decodes, never empty. pub fn buses(&self) -> RangeInclusive { self.start_bus..=self.end_bus } + pub fn holds(&self, bus: u8) -> bool { + self.buses().contains(&bus) + } + /// The bytes the window decodes, as `(address of the start bus, length)`. pub fn decoded(&self) -> (u64, u64) { let start = self.base + (u64::from(self.start_bus) << BUS_SHIFT); @@ -59,14 +99,59 @@ impl Allocation { (start, buses << BUS_SHIFT) } - pub fn holds(&self, bus: u8) -> bool { - self.buses().contains(&bus) + /// Where a function's configuration space is, from the window's first + /// byte ([`Self::decoded`]'s start); `None` for a bus the window does not + /// decode, or a device or function no address names. + pub fn offset(&self, bus: u8, device: u8, function: u8) -> Option { + if !self.holds(bus) || device > 31 || function > 7 { + return None; + } + Some( + u64::from(bus - self.start_bus) << BUS_SHIFT + | u64::from(device) << DEVICE_SHIFT + | u64::from(function) << FUNCTION_SHIFT, + ) + } + + /// The register an address names, if the window decodes it. + pub fn locate(&self, at: u64) -> Option { + let (start, bytes) = self.decoded(); + let into = at.checked_sub(start).filter(|&into| into < bytes)?; + Some(ConfigRegister { + bus: self.start_bus + (into >> BUS_SHIFT) as u8, + device: (into >> DEVICE_SHIFT & 0x1F) as u8, + function: (into >> FUNCTION_SHIFT & 7) as u8, + offset: (into % FUNCTION_BYTES) as u16, + }) + } + + /// Whether the kernel can map exactly this window, uncached: on its + /// mapping's grain, below `limit`, and over no memory firmware's `map` + /// lists as memory, which the kernel already maps cached. + pub fn mappable(&self, grain: u64, limit: u64, map: &[MemoryMapEntry]) -> Result<(), AllocationRefused> { + let (start, bytes) = self.decoded(); + if start % grain != 0 || bytes % grain != 0 { + return Err(AllocationRefused::OffGrain { start, bytes, grain }); + } + // At most `u64::MAX`: `decode` bounded the last byte. + let last = start + (bytes - 1); + if last >= limit { + return Err(AllocationRefused::PastLimit { last, limit }); + } + match map.iter().find(|entry| { + toyos_bootmap::is_read_as_memory(entry.uefi_type) && entry.start <= last && start < entry.end + }) { + Some(entry) => Err(AllocationRefused::OverMemory { uefi_type: entry.uefi_type, start: entry.start }), + None => Ok(()), + } } } -/// Why one allocation structure is not a window this decode hands out. +/// Why one allocation structure is not a window this kernel uses. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum AllocationRefused { + /// On a segment group other than [`SEGMENT_GROUP`]. + OtherSegment { segment: u16 }, /// The end bus is below the start bus: a window of no bus. Inverted { start_bus: u8, end_bus: u8 }, /// A base off a bus boundary, where no bus number is an address bit. @@ -75,10 +160,15 @@ pub enum AllocationRefused { Wraps { base: u64, end_bus: u8 }, /// The table ends inside this structure. Partial { bytes: usize }, - /// On a segment group other than the one served. - OtherSegment { segment: u16, served: u16 }, - /// Buses an earlier window of the served group already decodes. + /// Buses an earlier window already decodes. Overlaps { start_bus: u8, end_bus: u8 }, + /// Its bytes start or end off the grain the kernel maps at, so a mapping + /// of it would take the neighbouring bytes too. + OffGrain { start: u64, bytes: u64, grain: u64 }, + /// Its last byte is at or past the first address the kernel can map. + PastLimit { last: u64, limit: u64 }, + /// Firmware's map lists memory, of this type and from this address, in it. + OverMemory { uefi_type: u32, start: u64 }, } /// Every allocation structure of an MCFG, in table order. @@ -86,8 +176,7 @@ pub enum AllocationRefused { pub struct Allocations

{ table: Table

, next: usize, - served: Option, - /// One bit per bus of the served group that a window already decodes. + /// One bit per bus a window already decodes. claimed: [u64; 4], } @@ -101,11 +190,11 @@ impl

Allocations

{ /// The MCFG at `rsdp_addr`, opened for its first allocation structure. pub fn ecam_allocations(phys: P, rsdp_addr: u64) -> Result, TableError> { let table = find_table(phys, rsdp_addr, b"MCFG", FIRST_ENTRY + ENTRY_LEN)?; - Ok(Allocations { table, next: FIRST_ENTRY, served: None, claimed: [0; 4] }) + Ok(Allocations { table, next: FIRST_ENTRY, claimed: [0; 4] }) } impl Iterator for Allocations

{ - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { let at = self.next; @@ -115,40 +204,22 @@ impl Iterator for Allocations

{ return Some(Err(AllocationRefused::Partial { bytes: left })); } self.next = at + ENTRY_LEN; - // Inside the table: `left` covers the whole structure. - let (base, segment, start_bus, end_bus) = ( - self.table.u64_at(at)?, - self.table.u16_at(at + 8)?, - self.table.byte(at + 10)?, - self.table.byte(at + 11)?, - ); - Some(self.accept(Allocation { base, segment, start_bus, end_bus })) + let mut entry = [0u8; ENTRY_LEN]; + for (i, byte) in entry.iter_mut().enumerate() { + // Inside the table: `left` covers the whole structure. + *byte = self.table.byte(at + i)?; + } + Some(EcamWindow::decode(&entry).and_then(|window| self.claim(window))) } } impl

Allocations

{ - fn accept(&mut self, window: Allocation) -> Result { - let Allocation { base, segment, start_bus, end_bus } = window; - if end_bus < start_bus { - return Err(AllocationRefused::Inverted { start_bus, end_bus }); - } - if base & ((1 << BUS_SHIFT) - 1) != 0 { - return Err(AllocationRefused::Misaligned { base }); - } - // The last byte of the end bus, which `decoded` then sums without a check. - let last = (u64::from(end_bus) + 1) << BUS_SHIFT; - if base.checked_add(last - 1).is_none() { - return Err(AllocationRefused::Wraps { base, end_bus }); - } - let served = *self.served.get_or_insert(segment); - if segment != served { - return Err(AllocationRefused::OtherSegment { segment, served }); - } - let buses = start_bus..=end_bus; - if buses.clone().any(|bus| self.claimed[usize::from(bus / 64)] & 1 << (bus % 64) != 0) { - return Err(AllocationRefused::Overlaps { start_bus, end_bus }); + fn claim(&mut self, window: EcamWindow) -> Result { + let claimed = |bus: u8| self.claimed[usize::from(bus / 64)] & 1 << (bus % 64) != 0; + if window.buses().any(claimed) { + return Err(AllocationRefused::Overlaps { start_bus: window.start_bus, end_bus: window.end_bus }); } - for bus in buses { + for bus in window.buses() { self.claimed[usize::from(bus / 64)] |= 1 << (bus % 64); } Ok(window) diff --git a/toyos-acpi/tests/fixtures.rs b/toyos-acpi/tests/fixtures.rs index 39a7396dcb5..6f49d646aad 100644 --- a/toyos-acpi/tests/fixtures.rs +++ b/toyos-acpi/tests/fixtures.rs @@ -88,7 +88,7 @@ fn the_mcfg_names_the_ecam_window_the_pci_walk_used() { assert_eq!(allocations.table_base(), 0x7fb7_6000); let windows: Vec<_> = allocations.collect(); let [Ok(window)] = windows[..] else { panic!("q35 publishes one allocation, and the walk found {windows:?}") }; - assert_eq!((window.base(), window.segment(), window.buses()), (0xb000_0000, 0, 0..=0xff)); + assert_eq!((window.base(), window.buses()), (0xb000_0000, 0..=0xff)); assert_eq!(window.decoded(), (0xb000_0000, 0x1000_0000)); } diff --git a/toyos-acpi/tests/mcfg.rs b/toyos-acpi/tests/mcfg.rs index b885d3a06b0..92a15215814 100644 --- a/toyos-acpi/tests/mcfg.rs +++ b/toyos-acpi/tests/mcfg.rs @@ -1,14 +1,17 @@ -//! The MCFG's allocations: the window each decodes, and every structure that -//! is not a window refused by name while the walk goes on to the next. +//! The MCFG's allocations: the window each decodes, the address each of its +//! functions is at, and every structure that is not a window refused by name +//! while the walk goes on to the next. mod common; use common::{declare_len, rsdp, sdt, xsdt, Machine}; -use toyos_acpi::{ecam_allocations, Allocation, AllocationRefused}; +use toyos_abi::boot::MemoryMapEntry; +use toyos_acpi::{ecam_allocations, AllocationRefused, ConfigRegister, EcamWindow}; const RSDP_AT: u64 = 0x1_0000; const XSDT_AT: u64 = 0x2_0000; const MCFG_AT: u64 = 0x3_0000; +const MIB: u64 = 1 << 20; /// An MCFG over `(base, segment, start bus, end bus)` structures. fn mcfg(allocations: &[(u64, u16, u8, u8)]) -> Vec { @@ -22,66 +25,91 @@ fn mcfg(allocations: &[(u64, u16, u8, u8)]) -> Vec { } /// Every item the walk answers over `table`. -fn allocations(table: &[u8]) -> Vec> { +fn allocations(table: &[u8]) -> Vec> { let head = rsdp(XSDT_AT, 2, 36); let root = xsdt(&[MCFG_AT]); let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (MCFG_AT, table)]; ecam_allocations(Machine { regions }, RSDP_AT).expect("an MCFG with one structure at least").collect() } -/// A window as `(base, segment, first bus, last bus)`. -type Window = (u64, u16, u8, u8); +/// A window as `(base, first bus, last bus)`. +type Window = (u64, u8, u8); -fn window(allocation: &Allocation) -> Window { - (allocation.base(), allocation.segment(), *allocation.buses().start(), *allocation.buses().end()) +fn shape(window: &EcamWindow) -> Window { + (window.base(), *window.buses().start(), *window.buses().end()) } /// [`allocations`], each accepted one as a [`Window`]. fn walk(table: &[u8]) -> Vec> { - allocations(table).iter().map(|item| item.as_ref().map(window).map_err(|why| *why)).collect() + allocations(table).iter().map(|item| item.as_ref().map(shape).map_err(|why| *why)).collect() +} + +/// The one window an MCFG of one structure decodes. +fn window(base: u64, start_bus: u8, end_bus: u8) -> EcamWindow { + allocations(&mcfg(&[(base, 0, start_bus, end_bus)]))[0].expect("a well-formed window") } /// The laptop's shape: one allocation of buses 0 to 0x3f. Its window is /// 64 MiB, and the 192 MiB past it that 256 buses would name is not in it. #[test] fn a_window_of_sixty_four_buses_decodes_sixty_four_megabytes() { - let table = mcfg(&[(0xf800_0000, 0, 0, 0x3f)]); - assert_eq!(walk(&table), [Ok((0xf800_0000, 0, 0, 0x3f))]); - let accepted = allocations(&table)[0].unwrap(); - assert_eq!(accepted.decoded(), (0xf800_0000, 0x0400_0000)); + let accepted = window(0xf800_0000, 0, 0x3f); + assert_eq!(accepted.decoded(), (0xf800_0000, 64 * MIB)); assert!(accepted.holds(0x3f)); assert!(!accepted.holds(0x40)); + assert_eq!(accepted.offset(0x3f, 31, 7), Some(64 * MIB - 0x1000)); + assert_eq!(accepted.offset(0x40, 0, 0), None); + assert_eq!(accepted.locate(0xf800_0000 + 64 * MIB), None, "the first byte past the end bus"); } /// The base is bus 0's whatever bus the window starts at, so the window's -/// first byte is its start bus's and no bus below it is held. +/// first byte is its start bus's: a function's offset into the window counts +/// from that bus, and no bus below it or past its end has one. #[test] -fn a_window_starting_above_bus_zero_starts_at_its_start_bus() { - let accepted = allocations(&mcfg(&[(0xe000_0000, 0, 0x10, 0x1f)]))[0].unwrap(); - assert_eq!(accepted.decoded(), (0xe100_0000, 0x0100_0000)); - assert!(!accepted.holds(0)); - assert!(!accepted.holds(0x0f)); - assert!(accepted.holds(0x10)); - assert!(!accepted.holds(0x20)); +fn a_window_starting_at_bus_0x10_counts_its_functions_from_bus_0x10() { + let accepted = window(0xe000_0000, 0x10, 0x1f); + assert_eq!(accepted.decoded(), (0xe100_0000, 16 * MIB)); + assert_eq!(accepted.offset(0x10, 0, 0), Some(0)); + assert_eq!(accepted.offset(0x11, 2, 3), Some(MIB | 2 << 15 | 3 << 12)); + assert_eq!(accepted.offset(0x1f, 31, 7), Some(16 * MIB - 0x1000), "the window's last function"); + for (bus, device, function) in [(0x0f, 0, 0), (0x20, 0, 0), (0, 0, 0), (0x10, 32, 0), (0x10, 0, 8)] { + assert_eq!(accepted.offset(bus, device, function), None, "{bus:#x}:{device:#x}.{function}"); + } + assert_eq!(accepted.locate(0xe100_0000), Some(ConfigRegister { bus: 0x10, device: 0, function: 0, offset: 0 })); + assert_eq!(accepted.locate(0xe0ff_ffff), None, "bus 0x0f's last byte"); + assert_eq!(accepted.locate(0xe200_0000), None, "bus 0x20's first byte"); + // Each function's every register is where `offset` puts it, and nowhere + // else does `locate` name it. + let (start, _) = accepted.decoded(); + for bus in accepted.buses() { + for (device, function) in (0..32).flat_map(|device| (0..8).map(move |function| (device, function))) { + let at = start + accepted.offset(bus, device, function).expect("a function the window decodes"); + for offset in [0, 0x44, 0xfff] { + assert_eq!(accepted.locate(at + u64::from(offset)), Some(ConfigRegister { bus, device, function, offset })); + } + } + } } /// Each malformed structure is refused by name, and the walk goes on to the -/// next: a structure the walk refused names no served segment either. +/// next. #[test] fn a_malformed_structure_is_refused_and_the_next_is_still_read() { let found = walk(&mcfg(&[ - (0xe000_0000, 1, 0x20, 0x1f), - (0xe008_0000, 1, 0, 0xff), - (0xffff_ffff_fff0_0000, 1, 0, 1), + (0xe000_0000, 1, 0, 0xff), + (0xe000_0000, 0, 0x20, 0x1f), + (0xe008_0000, 0, 0, 0xff), + (0xffff_ffff_fff0_0000, 0, 0, 1), (0xe000_0000, 0, 0, 0xff), ])); assert_eq!( found, [ + Err(AllocationRefused::OtherSegment { segment: 1 }), Err(AllocationRefused::Inverted { start_bus: 0x20, end_bus: 0x1f }), Err(AllocationRefused::Misaligned { base: 0xe008_0000 }), Err(AllocationRefused::Wraps { base: 0xffff_ffff_fff0_0000, end_bus: 1 }), - Ok((0xe000_0000, 0, 0, 0xff)), + Ok((0xe000_0000, 0, 0xff)), ] ); } @@ -91,30 +119,32 @@ fn a_malformed_structure_is_refused_and_the_next_is_still_read() { #[test] fn a_window_ending_on_the_last_byte_is_accepted_and_one_bus_more_wraps() { let top = 0xffff_ffff_fff0_0000; - let accepted = allocations(&mcfg(&[(top, 0, 0, 0)]))[0].unwrap(); - assert_eq!(accepted.decoded(), (top, 0x10_0000)); + let accepted = window(top, 0, 0); + assert_eq!(accepted.decoded(), (top, MIB)); + assert_eq!(accepted.locate(u64::MAX), Some(ConfigRegister { bus: 0, device: 31, function: 7, offset: 0xfff })); let base = 0xffff_ffff_f000_0000; - assert_eq!(walk(&mcfg(&[(base, 0, 0xff, 0xff)])), [Ok((base, 0, 0xff, 0xff))]); + assert_eq!(walk(&mcfg(&[(base, 0, 0xff, 0xff)])), [Ok((base, 0xff, 0xff))]); assert_eq!(walk(&mcfg(&[(top, 0, 1, 1)])), [Err(AllocationRefused::Wraps { base: top, end_bus: 1 })]); } -/// The first window accepted names the served segment group; a structure on -/// another is refused, and so is one naming a bus a window already decodes. +/// Segment group 0 is served wherever its windows sit in the table: one on +/// another group is refused, and so is one naming a bus a window already +/// decodes. #[test] fn another_segment_and_an_overlap_are_refused() { let found = walk(&mcfg(&[ - (0xe000_0000, 0, 0, 0x7f), (0x1_0000_0000, 1, 0, 0xff), + (0xe000_0000, 0, 0, 0x7f), (0xf000_0000, 0, 0x7f, 0x80), (0xe000_0000, 0, 0x80, 0xff), ])); assert_eq!( found, [ - Ok((0xe000_0000, 0, 0, 0x7f)), - Err(AllocationRefused::OtherSegment { segment: 1, served: 0 }), + Err(AllocationRefused::OtherSegment { segment: 1 }), + Ok((0xe000_0000, 0, 0x7f)), Err(AllocationRefused::Overlaps { start_bus: 0x7f, end_bus: 0x80 }), - Ok((0xe000_0000, 0, 0x80, 0xff)), + Ok((0xe000_0000, 0x80, 0xff)), ] ); } @@ -126,5 +156,61 @@ fn a_table_ending_inside_a_structure_refuses_the_part() { let mut table = mcfg(&[(0xe000_0000, 0, 0, 0xff), (0xf000_0000, 1, 0, 0xff)]); let declared = table.len() as u32 - 7; declare_len(&mut table, declared); - assert_eq!(walk(&table), [Ok((0xe000_0000, 0, 0, 0xff)), Err(AllocationRefused::Partial { bytes: 9 })]); + assert_eq!(walk(&table), [Ok((0xe000_0000, 0, 0xff)), Err(AllocationRefused::Partial { bytes: 9 })]); +} + +const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { + MemoryMapEntry { uefi_type, start, end } +} + +const GRAIN_2M: u64 = 2 * MIB; +const GRAIN_4K: u64 = 0x1000; +const LIMIT: u64 = 1 << 47; + +/// A window that starts or ends on an odd megabyte is off a 2 MiB grain, +/// where mapping it would take the neighbouring megabyte too; on a 4 KiB +/// grain it is mapped exactly. +#[test] +fn a_window_off_the_grain_is_refused_and_on_it_is_mapped() { + let cases = [((0x11, 0x1f), Some((0xe110_0000, 15 * MIB))), ((0x10, 0x10), Some((0xe100_0000, MIB))), ((0x10, 0x1f), None)]; + for ((start_bus, end_bus), off) in cases { + let accepted = window(0xe000_0000, start_bus, end_bus); + let found = accepted.mappable(GRAIN_2M, LIMIT, &[]); + match off { + Some((start, bytes)) => assert_eq!(found, Err(AllocationRefused::OffGrain { start, bytes, grain: GRAIN_2M })), + None => assert_eq!(found, Ok(())), + } + assert_eq!(accepted.mappable(GRAIN_4K, LIMIT, &[]), Ok(())); + } + // A base on an odd megabyte: aligned to a bus, not to the grain. + assert_eq!( + window(0xe010_0000, 0, 1).mappable(GRAIN_2M, LIMIT, &[]), + Err(AllocationRefused::OffGrain { start: 0xe010_0000, bytes: 2 * MIB, grain: GRAIN_2M }) + ); +} + +/// The window's last byte must be below the first address the kernel cannot +/// map. +#[test] +fn a_window_reaching_the_limit_is_refused() { + let below = window(LIMIT - 16 * MIB, 0, 0x0f); + assert_eq!(below.mappable(GRAIN_2M, LIMIT, &[]), Ok(())); + let reaching = window(LIMIT - 16 * MIB, 0, 0x11); + assert_eq!(reaching.mappable(GRAIN_2M, LIMIT, &[]), Err(AllocationRefused::PastLimit { last: LIMIT + 2 * MIB - 1, limit: LIMIT })); + let top = window(0xffff_ffff_fff0_0000, 0, 0); + assert_eq!(top.mappable(GRAIN_4K, LIMIT, &[]), Err(AllocationRefused::PastLimit { last: u64::MAX, limit: LIMIT })); +} + +/// Over memory firmware's map lists as memory — what the kernel hands out and +/// what ACPI's tables live in — a window is refused; over reserved or +/// memory-mapped I/O ranges, or beside memory, it is mapped. +#[test] +fn a_window_over_memory_is_refused() { + let accepted = window(0x3f00_0000, 0, 0x0f); + let beside = [e(7, 0x4000_0000, 0x8000_0000), e(7, 0x3000_0000, 0x3f00_0000), e(0, 0x3f00_0000, 0x4000_0000), e(11, 0x3f00_0000, 0x3f10_0000)]; + assert_eq!(accepted.mappable(GRAIN_4K, LIMIT, &beside), Ok(())); + for (uefi_type, start, end) in [(7, 0x3fff_f000, 0x4000_1000), (7, 0x3000_0000, 0x3f00_1000), (4, 0x3f80_0000, 0x3f80_1000), (9, 0x3f00_0000, 0x3f00_1000), (10, 0x3ff0_0000, 0x4000_0000)] { + let map = [e(0, 0x3f00_0000, 0x4000_0000), e(uefi_type, start, end)]; + assert_eq!(accepted.mappable(GRAIN_4K, LIMIT, &map), Err(AllocationRefused::OverMemory { uefi_type, start }), "{uefi_type} at {start:#x}"); + } } diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs index 92f7237bfe4..1c539ac3af7 100644 --- a/toyos-bootmap/src/lib.rs +++ b/toyos-bootmap/src/lib.rs @@ -115,7 +115,7 @@ pub const fn is_usable_type(uefi_type: u32) -> bool { /// Whether the kernel reads a range of this type as memory: what the pmm hands /// out, and the two types ACPI's tables live in. Any other type, one this list /// does not know included, is not. -const fn is_read_as_memory(uefi_type: u32) -> bool { +pub const fn is_read_as_memory(uefi_type: u32) -> bool { is_usable_type(uefi_type) || matches!(uefi_type, EFI_ACPI_RECLAIM_MEMORY | EFI_ACPI_MEMORY_NVS) } diff --git a/toyos-userbound/Cargo.toml b/toyos-userbound/Cargo.toml index dc24dd17a49..da6066eef56 100644 --- a/toyos-userbound/Cargo.toml +++ b/toyos-userbound/Cargo.toml @@ -23,3 +23,6 @@ license = "MIT OR Apache-2.0" toyos-abi = { path = "../toyos-abi" } # Which memory types the kernel hands out as RAM. toyos-bootmap = { path = "../toyos-bootmap" } +# The ECAM window, as the MCFG decode made it: the one declaration of which +# addresses are configuration space, read by the policy and by the kernel. +toyos-acpi = { path = "../toyos-acpi" } diff --git a/toyos-userbound/src/firmware.rs b/toyos-userbound/src/firmware.rs index 870ae111c53..b16215ef70c 100644 --- a/toyos-userbound/src/firmware.rs +++ b/toyos-userbound/src/firmware.rs @@ -40,7 +40,7 @@ //! nothing ToyOS put there. The read may have an effect in the device that //! nothing here knows of. Inside that, every page a device the kernel knows //! of decodes in is refused, whatever firmware types it and whoever drives -//! the device, and an address in the ECAM window is a configuration access +//! the device, and an address in an ECAM window is a configuration access //! and is decided as one. //! //! **What the allocator hands out is refused wherever the map lists it.** A @@ -71,6 +71,7 @@ use toyos_abi::acpi::{Refused, Width, UNLISTED}; use toyos_abi::boot::MemoryMapEntry; +use toyos_acpi::{ConfigRegister, EcamWindow, SEGMENT_GROUP}; use crate::port::{KeptCommands, Mediated, IO_PORTS}; use crate::span::PAGE_4K; @@ -96,26 +97,6 @@ pub const FIXED_RANGE_END: u64 = 0x10_0000; /// Bytes of configuration space a function has. const CONFIG_BYTES: u16 = 0x1000; -/// The window configuration space is reached through, as the MCFG names it -/// (PCI Firmware Specification 3.3, Table 4-3): `base` is bus 0's, whatever -/// the first bus the window decodes. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Ecam { - pub base: u64, - pub segment: u16, - pub first_bus: u8, - pub last_bus: u8, -} - -impl Ecam { - fn holds(&self, at: u64) -> bool { - // Saturating: the base is firmware's word. - let start = self.base.saturating_add(u64::from(self.first_bus) << 20); - let end = self.base.saturating_add((u64::from(self.last_bus) + 1) << 20); - (start..end).contains(&at) - } -} - /// One PCI function on the segment group the kernel enumerated. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Function { @@ -133,7 +114,9 @@ pub struct Memory<'a, D> { pub map: &'a [MemoryMapEntry], /// One past the last byte the kernel maps. pub mapped_end: u64, - pub ecam: Option, + /// The windows configuration space is reached through, as the MCFG + /// bounds them. + pub ecam: &'a [EcamWindow], pub devices: D, /// The FACS, as `(start, end)`. pub facs: Option<(u64, u64)>, @@ -212,15 +195,13 @@ impl> Memory<'_, D> { pub fn decide(self, at: u64, width: Width, write: bool) -> MemoryVerdict { use MemoryVerdict::Refused as No; let Some(last) = at.checked_add(width.bytes() - 1) else { return No(Refused::Unmapped) }; - if let Some(ecam) = self.ecam { - match (ecam.holds(at), ecam.holds(last)) { - (true, true) => { - let offset = at - ecam.base; - let function = - Function { bus: (offset >> 20) as u8, device: (offset >> 15 & 0x1F) as u8, function: (offset >> 12 & 7) as u8 }; - return MemoryVerdict::AsConfig(function, (offset & 0xFFF) as u16); + for window in self.ecam { + match (window.locate(at), window.locate(last)) { + (Some(register), Some(_)) => { + let ConfigRegister { bus, device, function, offset } = register; + return MemoryVerdict::AsConfig(Function { bus, device, function }, offset); } - (false, false) => {} + (None, None) => {} _ => return No(Refused::Straddles), } } @@ -475,14 +456,13 @@ impl ConfigAt { /// configuration from and what moves the ports it declared. A read is held to /// its shape: a function the window reaches, and at most a dword that crosses /// no dword boundary, the unit a configuration register is defined in. -pub fn config(ecam: Option, segment: u16, function: Function, offset: u16, width: Width, write: bool) -> Result { +pub fn config(ecam: &[EcamWindow], segment: u16, function: Function, offset: u16, width: Width, write: bool) -> Result { if write { return Err(Refused::ConfigWrite); } - let reached = ecam.is_some_and(|ecam| { - ecam.segment == segment && (ecam.first_bus..=ecam.last_bus).contains(&function.bus) - }); - if !reached || function.device > 31 || function.function > 7 { + let Function { bus, device, function: number } = function; + let reached = segment == SEGMENT_GROUP && ecam.iter().any(|window| window.offset(bus, device, number).is_some()); + if !reached { return Err(Refused::ConfigUnreachable); } let bytes = width.bytes() as u16; diff --git a/toyos-userbound/tests/firmware.rs b/toyos-userbound/tests/firmware.rs index aec6232f63a..3c14e744fcb 100644 --- a/toyos-userbound/tests/firmware.rs +++ b/toyos-userbound/tests/firmware.rs @@ -5,11 +5,21 @@ use toyos_abi::acpi::{Refused, Width}; use toyos_abi::boot::MemoryMapEntry; use toyos_userbound::firmware::{ - config, lock_word, port, sleep_type, type_word, CallRate, Ecam, Function, Memory, MemoryVerdict, NoLockWord, PortVerdict, Standing, CALLS, + config, lock_word, port, sleep_type, type_word, CallRate, Function, Memory, MemoryVerdict, NoLockWord, PortVerdict, Standing, CALLS, CALL_PERIOD_NS, FIXED_RANGE_END, }; +use toyos_acpi::EcamWindow; use toyos_userbound::{KeptCommands, Mediated}; +/// One window as an MCFG allocation structure of segment group 0 names it, +/// through the decode that is the only maker of one. +fn ecam(base: u64, first_bus: u8, last_bus: u8) -> &'static [EcamWindow] { + let mut entry = [0u8; 16]; + entry[..8].copy_from_slice(&base.to_le_bytes()); + (entry[10], entry[11]) = (first_bus, last_bus); + Box::leak(Box::new([EcamWindow::decode(&entry).expect("a well-formed window")])) +} + const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { MemoryMapEntry { uefi_type, start, end } } @@ -31,7 +41,6 @@ const Q35: [MemoryMapEntry; 9] = [ e(0, 0xe0000000, 0xf0000000), e(0, 0xfd00000000, 0x10000000000), ]; -const Q35_ECAM: Ecam = Ecam { base: 0xe000_0000, segment: 0, first_bus: 0, last_bus: 0xFF }; /// The I/O APIC and the HPET, as the kernel maps them on q35. const Q35_DRIVEN: &[(u64, u64)] = &[(0xfec0_0000, 0xfec0_0020), (0xfed0_0000, 0xfed0_1000)]; const Q35_FACS: (u64, u64) = (0x7ff7_7000, 0x7ff7_7040); @@ -46,7 +55,7 @@ fn devices(decoded: &'static [(u64, u64)]) -> Devices { /// A machine of one map and nothing else: no ECAM window, no FACS. fn bare(map: &'static [MemoryMapEntry], decoded: &'static [(u64, u64)]) -> Mem { - Memory { map, mapped_end: 4 * GIB, ecam: None, devices: devices(decoded), facs: None, uncached: registers, registers_differ: false } + Memory { map, mapped_end: 4 * GIB, ecam: &[], devices: devices(decoded), facs: None, uncached: registers, registers_differ: false } } /// What the range registers type uncacheable on these machines: the hole @@ -58,7 +67,7 @@ fn registers(at: u64, len: u64) -> bool { } fn q35() -> Mem { - Memory { map: &Q35, mapped_end: 4 * GIB, ecam: Some(Q35_ECAM), devices: devices(Q35_DRIVEN), facs: Some(Q35_FACS), uncached: registers, registers_differ: false } + Memory { map: &Q35, mapped_end: 4 * GIB, ecam: ecam(0xe000_0000, 0, 0xFF), devices: devices(Q35_DRIVEN), facs: Some(Q35_FACS), uncached: registers, registers_differ: false } } /// A map shaped as a laptop's is, at addresses of this test's own: RAM, a @@ -78,8 +87,7 @@ const LAPTOP: [MemoryMapEntry; 9] = [ ]; fn laptop() -> Mem { - let ecam = Ecam { base: 0xc000_0000, segment: 0, first_bus: 0, last_bus: 0xFF }; - Memory { map: &LAPTOP, mapped_end: 4 * GIB, ecam: Some(ecam), devices: devices(&[]), facs: Some((0x7400_0040, 0x7400_0080)), uncached: registers, registers_differ: false } + Memory { map: &LAPTOP, mapped_end: 4 * GIB, ecam: ecam(0xc000_0000, 0, 0xFF), devices: devices(&[]), facs: Some((0x7400_0040, 0x7400_0080)), uncached: registers, registers_differ: false } } fn passes(memory: &Mem, at: u64, width: Width, write: bool) -> bool { @@ -267,8 +275,8 @@ fn memory_any_usable_range_holds_is_refused_whatever_lists_it_first() { // A usable range over the firmware range's second page and beyond. let over = [e(firmware, 0x1000, 0x3000), e(handed_out, 0x2000, 0x4000)]; for write in [false, true] { - let twice = Memory { map: &twice, mapped_end: 4 * GIB, ecam: None, devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; - let over = Memory { map: &over, mapped_end: 4 * GIB, ecam: None, devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; + let twice = Memory { map: &twice, mapped_end: 4 * GIB, ecam: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; + let over = Memory { map: &over, mapped_end: 4 * GIB, ecam: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; let why = MemoryVerdict::Refused(Refused::UsableMemory); assert_eq!(twice.clone().decide(0x1000, Width::Byte, write), why, "type {firmware} listed before {handed_out}"); assert_eq!(twice.decide(0x2ff8, Width::QWord, write), why); @@ -316,7 +324,7 @@ fn every_other_type_and_an_unlisted_address_is_refused_with_its_type() { // Every type but the four the policy names, as the only range of a map. for ty in (0..=0x20u32).chain([0x7000_0000, 0x8000_0000, u32::MAX]) { let map = [e(ty, 0x1000, 0x2000)]; - let memory = Memory { map: &map, mapped_end: 4 * GIB, ecam: None, devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; + let memory = Memory { map: &map, mapped_end: 4 * GIB, ecam: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; let read = memory.clone().decide(0x1000, Width::Byte, false); let write = memory.decide(0x1000, Width::Byte, true); let through = |verdict| matches!(verdict, MemoryVerdict::Through(_)); @@ -424,7 +432,7 @@ fn the_facs_is_read_and_its_bytes_are_never_written() { fn an_address_in_the_ecam_window_is_a_configuration_access_whatever_the_map_types_it() { // Typed reserved on q35 and memory-mapped I/O on the laptop's shape. for memory in [q35(), laptop()] { - let base = memory.ecam.expect("an ECAM window").base; + let base = memory.ecam[0].base(); for write in [false, true] { assert_eq!( memory.clone().decide(base + (3 << 20 | 0x1c << 15 | 5 << 12 | 0x48), Width::DWord, write), @@ -441,15 +449,15 @@ fn an_address_in_the_ecam_window_is_a_configuration_access_whatever_the_map_type } // A window that begins at a later bus holds nothing below it. const WINDOW: &[MemoryMapEntry] = &[e(0, 0xe000_0000, 0xf000_0000)]; - let ecam = Ecam { base: 0xe000_0000, segment: 0, first_bus: 0x10, last_bus: 0x1F }; - let memory = Memory { ecam: Some(ecam), ..bare(WINDOW, &[]) }; + let memory = Memory { ecam: ecam(0xe000_0000, 0x10, 0x1F), ..bare(WINDOW, &[]) }; assert!(passes(&memory, 0xe000_0000, Width::Byte, false), "below the first bus is plain reserved memory"); assert_eq!(memory.clone().decide(0xe100_0000, Width::Byte, false), MemoryVerdict::AsConfig(Function { bus: 0x10, device: 0, function: 0 }, 0)); assert!(passes(&memory, 0xe200_0000, Width::Byte, false), "past the last bus too"); - // A base firmware put at the top of the address space decides without overflow. - let ecam = Ecam { base: u64::MAX - 0xFFF, segment: 0, first_bus: 0, last_bus: 0xFF }; - let memory = Memory { ecam: Some(ecam), ..bare(WINDOW, &[]) }; - assert!(passes(&memory, 0xe000_0000, Width::Byte, false)); + // A window ending on the address space's last byte decides its last byte + // without overflow. + let top = Memory { ecam: ecam(0xffff_ffff_fff0_0000, 0, 0), ..bare(WINDOW, &[]) }; + assert_eq!(top.clone().decide(u64::MAX, Width::Byte, false), MemoryVerdict::AsConfig(Function { bus: 0, device: 0x1f, function: 7 }, 0xFFF)); + assert_eq!(refused(&top, u64::MAX, Width::Word, false), Refused::Unmapped); } #[test] @@ -647,7 +655,7 @@ const HOST_BRIDGE: Function = Function { bus: 0, device: 0, function: 0 }; #[test] fn a_configuration_read_is_held_to_the_window_and_to_one_register() { - let ecam = Some(Ecam { base: 0xe000_0000, segment: 0, first_bus: 0, last_bus: 0x7F }); + let ecam = ecam(0xe000_0000, 0, 0x7F); for (offset, width) in [(0, Width::DWord), (0xE, Width::Byte), (0x19, Width::Byte), (0x4A, Width::Word), (0xFFC, Width::DWord), (0xFFF, Width::Byte)] { let at = config(ecam, 0, HOST_BRIDGE, offset, width, false).expect("one register of a reachable function"); assert_eq!((at.function(), at.offset(), at.width()), (HOST_BRIDGE, offset, width)); @@ -661,12 +669,12 @@ fn a_configuration_read_is_held_to_the_window_and_to_one_register() { assert_eq!(config(ecam, 0, Function { bus: 0x80, device: 0, function: 0 }, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); assert_eq!(config(ecam, 0, Function { bus: 0, device: 32, function: 0 }, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); assert_eq!(config(ecam, 0, Function { bus: 0, device: 0, function: 8 }, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); - assert_eq!(config(None, 0, HOST_BRIDGE, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); + assert_eq!(config(&[], 0, HOST_BRIDGE, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); } #[test] fn every_configuration_write_is_refused_by_one_name() { - let ecam = Some(Ecam { base: 0xe000_0000, segment: 0, first_bus: 0, last_bus: 0x7F }); + let ecam = ecam(0xe000_0000, 0, 0x7F); // The header, a capability's place, the registers past them, extended // space: every register a read reaches. for offset in (0..0x1000u16).step_by(4) { @@ -678,7 +686,7 @@ fn every_configuration_write_is_refused_by_one_name() { // And what no read reaches is a write all the same. assert_eq!(config(ecam, 0, HOST_BRIDGE, 0, Width::QWord, true), Err(Refused::ConfigWrite)); assert_eq!(config(ecam, 1, HOST_BRIDGE, 0, Width::DWord, true), Err(Refused::ConfigWrite)); - assert_eq!(config(None, 0, HOST_BRIDGE, 0x44, Width::Byte, true), Err(Refused::ConfigWrite)); + assert_eq!(config(&[], 0, HOST_BRIDGE, 0x44, Width::Byte, true), Err(Refused::ConfigWrite)); } /// `SLP_TYPx` is bits 12:10 of PM1 control and `SLP_EN` bit 13 (ACPI 6.5 From 916b5e16cc3e1d22d7ebd010ed66d7f32ad9553f Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 16:31:51 +0200 Subject: [PATCH 3/3] A later MCFG window is held to the acpi claim's policy as the first is, a window over an earlier one's bytes is refused, and the off-grain refusal is filed The policy's two readers of the window list, `Memory::decide` and `config`, were tested only over one-window lists, so a reader that took the first window alone stayed green. `a_later_window_is_held_as_the_first_is` puts buses 0x40..=0x7f at their own base after 0..=0x3f, both typed reserved, and asserts that an address there is a configuration access with its absolute bus, that a write there is refused `ConfigWrite`, and that a read reaches both windows' buses and not past them. Each review patch (`.iter().take(1)` in either reader) and the two together turn it red. `Allocations::claim` refused only buses an earlier window decodes. Two windows of disjoint buses over the same bytes would be mapped twice and enumerated under two bus numbers each, and the policy would decide by the first. Every bus is one megabyte on a megabyte boundary, so the claim keeps each claimed bus's address and refuses a window holding one, by name: `SharesBytes`. Deleting the check reds `a_window_over_another_windows_bytes_is_refused`. `OffGrain` is a compromise on x86-64, whose `map_mmio` maps 2 MiB pages: filed as issues/an-ecam-window-off-the-2-mib-grain-is-not-enumerated-on-x86.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...he-2-mib-grain-is-not-enumerated-on-x86.md | 25 ++++++++++ toyos-acpi/src/mcfg.rs | 22 +++++++-- toyos-acpi/tests/mcfg.rs | 28 +++++++++++ toyos-userbound/tests/firmware.rs | 47 ++++++++++++++++--- 4 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 issues/an-ecam-window-off-the-2-mib-grain-is-not-enumerated-on-x86.md diff --git a/issues/an-ecam-window-off-the-2-mib-grain-is-not-enumerated-on-x86.md b/issues/an-ecam-window-off-the-2-mib-grain-is-not-enumerated-on-x86.md new file mode 100644 index 00000000000..fbff1569ffe --- /dev/null +++ b/issues/an-ecam-window-off-the-2-mib-grain-is-not-enumerated-on-x86.md @@ -0,0 +1,25 @@ +--- +status: open +kind: defect +opened: 2026-10-10 +--- + +# An ECAM window off the 2 MiB grain is not enumerated on x86 + +x86-64's `map_mmio` maps whole 2 MiB pages (`paging::MMIO_GRAIN`, +`kernel/src/arch/x86_64/paging.rs`), so `EcamWindow::mappable` +(`toyos-acpi/src/mcfg.rs`) refuses a window whose first decoded byte or length +is off that grain, `AllocationRefused::OffGrain`, rather than map the +neighbouring megabyte uncached with it. A well-formed MCFG window that starts +or ends on an odd bus, or whose start bus sits on an odd megabyte, is then +never enumerated (`kernel/src/drivers/acpi.rs`, `ecam_windows`); where it is +the only window, the machine boots with no PCI function at all. AArch64 maps +at 4 KiB and takes every such window. + +**Evidence:** the code, and `a_window_off_the_grain_is_refused_and_on_it_is_mapped` +in `toyos-acpi/tests/mcfg.rs`. No MCFG read so far is off the grain: q35's +decodes buses 0x00..=0xff, the T14's 0x00..=0x79 from 0xc0000000, and the target +laptop's 0x00..=0x3f, each an even number of megabytes from a 2 MiB boundary. + +Owner: orchestrator. Exit: x86-64 maps an MMIO window exactly, to the 4 KiB +page, and a host test accepts a window of an odd bus count at x86-64's grain. diff --git a/toyos-acpi/src/mcfg.rs b/toyos-acpi/src/mcfg.rs index a34042a5ecf..7f4caccde0b 100644 --- a/toyos-acpi/src/mcfg.rs +++ b/toyos-acpi/src/mcfg.rs @@ -10,8 +10,8 @@ //! that arithmetic, and it answers only inside the range. //! //! **Segment group 0 alone is served** ([`SEGMENT_GROUP`]): it is the one every -//! machine has, and a window on another is refused by name. Each of its buses -//! is decoded by at most one window. +//! machine has, and a window on another is refused by name. Each of its buses, +//! and each byte of its configuration space, is decoded by at most one window. use core::ops::RangeInclusive; @@ -162,8 +162,12 @@ pub enum AllocationRefused { Partial { bytes: usize }, /// Buses an earlier window already decodes. Overlaps { start_bus: u8, end_bus: u8 }, + /// Bytes an earlier window already decodes as other buses: one function + /// would answer under two bus numbers. + SharesBytes { start: u64, bytes: u64 }, /// Its bytes start or end off the grain the kernel maps at, so a mapping - /// of it would take the neighbouring bytes too. + /// of it would take the neighbouring bytes too + /// (`issues/an-ecam-window-off-the-2-mib-grain-is-not-enumerated-on-x86.md`). OffGrain { start: u64, bytes: u64, grain: u64 }, /// Its last byte is at or past the first address the kernel can map. PastLimit { last: u64, limit: u64 }, @@ -178,6 +182,8 @@ pub struct Allocations

{ next: usize, /// One bit per bus a window already decodes. claimed: [u64; 4], + /// Where each claimed bus's configuration space starts. + bus_at: [u64; 256], } impl

Allocations

{ @@ -190,7 +196,7 @@ impl

Allocations

{ /// The MCFG at `rsdp_addr`, opened for its first allocation structure. pub fn ecam_allocations(phys: P, rsdp_addr: u64) -> Result, TableError> { let table = find_table(phys, rsdp_addr, b"MCFG", FIRST_ENTRY + ENTRY_LEN)?; - Ok(Allocations { table, next: FIRST_ENTRY, claimed: [0; 4] }) + Ok(Allocations { table, next: FIRST_ENTRY, claimed: [0; 4], bus_at: [0; 256] }) } impl Iterator for Allocations

{ @@ -219,8 +225,16 @@ impl

Allocations

{ if window.buses().any(claimed) { return Err(AllocationRefused::Overlaps { start_bus: window.start_bus, end_bus: window.end_bus }); } + // Every bus is one megabyte on a megabyte boundary, so two windows + // share a byte exactly where a claimed bus starts inside this one. + let (start, bytes) = window.decoded(); + let decodes = start..=start + (bytes - 1); + if (0..=u8::MAX).any(|bus| claimed(bus) && decodes.contains(&self.bus_at[usize::from(bus)])) { + return Err(AllocationRefused::SharesBytes { start, bytes }); + } for bus in window.buses() { self.claimed[usize::from(bus / 64)] |= 1 << (bus % 64); + self.bus_at[usize::from(bus)] = window.base + (u64::from(bus) << BUS_SHIFT); } Ok(window) } diff --git a/toyos-acpi/tests/mcfg.rs b/toyos-acpi/tests/mcfg.rs index 92a15215814..551869a4bae 100644 --- a/toyos-acpi/tests/mcfg.rs +++ b/toyos-acpi/tests/mcfg.rs @@ -149,6 +149,34 @@ fn another_segment_and_an_overlap_are_refused() { ); } +/// Buses no window decodes, at bytes one already does, are refused: the same +/// functions would answer under two bus numbers. A window that only touches +/// one, either side, is accepted. +#[test] +fn a_window_over_another_windows_bytes_is_refused() { + // The base that puts `bus` at `at`. + let base = |at: u64, bus: u8| at - u64::from(bus) * MIB; + let found = walk(&mcfg(&[ + (0xe000_0000, 0, 0, 0x3f), + (base(0xe000_0000, 0x40), 0, 0x40, 0x7f), + (base(0xe3f0_0000, 0x80), 0, 0x80, 0x80), + (base(0xdfe0_0000, 0xa0), 0, 0xa0, 0xa7), + (base(0xdff0_0000, 0x81), 0, 0x81, 0x81), + (base(0xe400_0000, 0x82), 0, 0x82, 0x82), + ])); + assert_eq!( + found, + [ + Ok((0xe000_0000, 0, 0x3f)), + Err(AllocationRefused::SharesBytes { start: 0xe000_0000, bytes: 64 * MIB }), + Err(AllocationRefused::SharesBytes { start: 0xe3f0_0000, bytes: MIB }), + Err(AllocationRefused::SharesBytes { start: 0xdfe0_0000, bytes: 8 * MIB }), + Ok((base(0xdff0_0000, 0x81), 0x81, 0x81)), + Ok((base(0xe400_0000, 0x82), 0x82, 0x82)), + ] + ); +} + /// A table that ends inside a structure ends the walk on a refusal of that /// structure, after every whole one. #[test] diff --git a/toyos-userbound/tests/firmware.rs b/toyos-userbound/tests/firmware.rs index 3c14e744fcb..85625194a5a 100644 --- a/toyos-userbound/tests/firmware.rs +++ b/toyos-userbound/tests/firmware.rs @@ -11,13 +11,21 @@ use toyos_userbound::firmware::{ use toyos_acpi::EcamWindow; use toyos_userbound::{KeptCommands, Mediated}; -/// One window as an MCFG allocation structure of segment group 0 names it, -/// through the decode that is the only maker of one. +/// Windows as MCFG allocation structures of segment group 0 name them, each +/// `(base, first bus, last bus)`, through the decode that is the only maker of +/// one. +fn windows(named: &[(u64, u8, u8)]) -> &'static [EcamWindow] { + let decode = |&(base, first_bus, last_bus): &(u64, u8, u8)| { + let mut entry = [0u8; 16]; + entry[..8].copy_from_slice(&base.to_le_bytes()); + (entry[10], entry[11]) = (first_bus, last_bus); + EcamWindow::decode(&entry).expect("a well-formed window") + }; + named.iter().map(decode).collect::>().leak() +} + fn ecam(base: u64, first_bus: u8, last_bus: u8) -> &'static [EcamWindow] { - let mut entry = [0u8; 16]; - entry[..8].copy_from_slice(&base.to_le_bytes()); - (entry[10], entry[11]) = (first_bus, last_bus); - Box::leak(Box::new([EcamWindow::decode(&entry).expect("a well-formed window")])) + windows(&[(base, first_bus, last_bus)]) } const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { @@ -460,6 +468,33 @@ fn an_address_in_the_ecam_window_is_a_configuration_access_whatever_the_map_type assert_eq!(refused(&top, u64::MAX, Width::Word, false), Refused::Unmapped); } +/// Every window is read for every access, the first no more than a later one: +/// an address in the second is a configuration access with its own bus, +/// refused as a write whatever the map types it, and a read reaches its buses. +#[test] +fn a_later_window_is_held_as_the_first_is() { + // Buses 0x40..=0x7f at their own base, after the laptop's 0..=0x3f; both + // typed reserved, as a plain write would pass. + const SECOND: u64 = 0xd000_0000; + const WINDOWS: &[MemoryMapEntry] = &[e(0, 0xc000_0000, 0xe000_0000)]; + let memory = Memory { ecam: windows(&[(0xc000_0000, 0, 0x3f), (SECOND, 0x40, 0x7f)]), ..bare(WINDOWS, &[]) }; + let function = Function { bus: 0x42, device: 3, function: 1 }; + let at = SECOND + (0x42 << 20 | 3 << 15 | 1 << 12 | 0x10); + for write in [false, true] { + let verdict = memory.clone().decide(at, Width::DWord, write); + assert_eq!(verdict, MemoryVerdict::AsConfig(function, 0x10), "write={write}"); + // What the kernel does with that verdict. + let MemoryVerdict::AsConfig(function, offset) = verdict else { unreachable!() }; + let made = config(memory.ecam, 0, function, offset, Width::DWord, write).map(|at| (at.function(), at.offset())); + assert_eq!(made, if write { Err(Refused::ConfigWrite) } else { Ok((function, 0x10)) }, "write={write}"); + } + for bus in [0x3f, 0x40, 0x7f] { + let function = Function { bus, device: 0, function: 0 }; + assert!(config(memory.ecam, 0, function, 0, Width::DWord, false).is_ok(), "bus {bus:#x}"); + } + assert_eq!(config(memory.ecam, 0, Function { bus: 0x80, device: 0, function: 0 }, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); +} + #[test] fn the_lock_word_is_exchanged_only_where_all_four_bytes_are_the_firmwares_own() { // The FACS of the q35 boot and of the laptop's shape, both in ACPI NVS.