diff --git a/Cargo.lock b/Cargo.lock index 9d1b32f5256..f91fd3b5a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6086,6 +6086,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 4440cfd59b5..2a4d9dbc6a0 100644 --- a/bootloader/src/watchdog.rs +++ b/bootloader/src/watchdog.rs @@ -56,10 +56,14 @@ 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 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. if !described(system_table, ecam, BUS_ZERO_BYTES) { 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/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/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/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs index 2e91a0993cf..d1676cb8690 100644 --- a/kernel/src/arch/aarch64/paging.rs +++ b/kernel/src/arch/aarch64/paging.rs @@ -624,6 +624,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 893c82976f2..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,25 +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(rsdp_addr), 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 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 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 @@ -705,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()); @@ -741,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, @@ -757,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 87db44ac6a5..dbc02af51d8 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -121,22 +121,48 @@ 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)> { +/// 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 (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, mut refused) = (Vec::new(), 0usize); + for (index, item) in allocations.enumerate() { + 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}", + toyos_acpi::SEGMENT_GROUP, + window.buses().start(), + window.buses().end(), + window.base() + ); + windows.push(window); + } + 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 } /// FADT revision and the IA-PC boot architecture flags. diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index 78f795f6742..c0b035dff05 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -1,5 +1,6 @@ use alloc::vec::Vec; +use toyos_acpi::EcamWindow; use toyos_pci::{bar, bridge, caps, msi, msix}; use crate::mm::Mmio; @@ -100,11 +101,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, @@ -114,13 +116,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 { @@ -519,41 +514,80 @@ 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 it decodes. +#[derive(Clone, Copy)] +struct Window { + 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 { + Some(self.mmio.subregion(self.ecam.offset(bus, dev, func)?, 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.ecam).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 the windows decode, window by window and in +/// bus/device/function order within one; drivers must select all matches, +/// not the first. +/// +/// **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..."); - *ECAM.lock() = Some(*ecam); + let windows: Vec = ecam + .iter() + .map(|&ecam| { + let (start, bytes) = ecam.decoded(); + Window { ecam, 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 scan reads only buses its window decodes"); + PciDevice { mmio, bus, dev, func } + }; + for bus in window.ecam.buses() { + 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; + } + found.push(pci); } - found.push(pci); } } } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 5aef1ee13a4..800ba26daf7 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, xhci}; @@ -433,14 +432,12 @@ 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); // 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/tests/common/qemu.rs b/tests/common/qemu.rs index f539f39b59b..bffda1991c5 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -740,6 +740,12 @@ 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, /// [`Profile::Virt`] with its SMMUv3, QEMU's emulated GICv3 and its ITS, /// QEMU's `edu`, a function that sends an MSI when told to, and an /// `e1000e` the IORT routes past the SMMUv3. @@ -750,7 +756,14 @@ 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::VirtIts => { + Self::Virt + | Self::VirtNoRng + | Self::VirtEl2 + | Self::VirtEl2NoVhe + | Self::VirtTcg + | Self::VirtSmmu + | Self::VirtLowEcam + | Self::VirtIts => { Arch::Aarch64 } Self::Headless @@ -956,7 +969,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::VirtIts => Shape { smmu: Smmu::WithIts, ..Self::Virt.shape() }, @@ -2164,6 +2177,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 5774e30e097..8e6243a7972 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -316,6 +316,7 @@ const SCREEN_TESTS: &[(&str, qemu::Profile)] = &[ ("virt_claim_lpi", qemu::Profile::VirtIts), ("virt_no_seed_refused", qemu::Profile::VirtNoRng), ("virt_wall_clock_utc", qemu::Profile::Virt), + ("virt_low_ecam", qemu::Profile::VirtLowEcam), ]; /// The tests whose machine shape *is* the test, each on a boot of its own. @@ -2157,6 +2158,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, @@ -3163,6 +3205,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re "virt_no_seed_refused" => virt_no_seed_refused(profile, test_config), "virt_smmu" => virt_smmu(profile, test_config), "virt_claim_lpi" => virt_claim_lpi(profile), + "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 @@ -5812,7 +5855,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..0b3b03c0c28 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, 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}; @@ -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..7f4caccde0b --- /dev/null +++ b/toyos-acpi/src/mcfg.rs @@ -0,0 +1,241 @@ +//! 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. [`EcamWindow`] is the only reader of +//! 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, +//! and each byte of its configuration space, 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. +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; +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 EcamWindow { + base: u64, + start_bus: u8, + end_bus: u8, +} + +/// 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 + } + + /// 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); + let buses = u64::from(self.end_bus - self.start_bus) + 1; + (start, buses << BUS_SHIFT) + } + + /// 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 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. + 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 }, + /// 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 + /// (`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 }, + /// 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. +#[derive(Clone, Copy)] +pub struct Allocations

{ + table: Table

, + 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

{ + /// 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, claimed: [0; 4], bus_at: [0; 256] }) +} + +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; + 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 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 }); + } + // 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/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..6f49d646aad 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.buses()), (0xb000_0000, 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..551869a4bae --- /dev/null +++ b/toyos-acpi/tests/mcfg.rs @@ -0,0 +1,244 @@ +//! 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_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 { + 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, first bus, last bus)`. +type Window = (u64, u8, u8); + +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(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 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: 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_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. +#[test] +fn a_malformed_structure_is_refused_and_the_next_is_still_read() { + let found = walk(&mcfg(&[ + (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, 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 = 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, 0xff, 0xff))]); + assert_eq!(walk(&mcfg(&[(top, 0, 1, 1)])), [Err(AllocationRefused::Wraps { base: top, end_bus: 1 })]); +} + +/// 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(&[ + (0x1_0000_0000, 1, 0, 0xff), + (0xe000_0000, 0, 0, 0x7f), + (0xf000_0000, 0, 0x7f, 0x80), + (0xe000_0000, 0, 0x80, 0xff), + ])); + assert_eq!( + found, + [ + Err(AllocationRefused::OtherSegment { segment: 1 }), + Ok((0xe000_0000, 0, 0x7f)), + Err(AllocationRefused::Overlaps { start_bus: 0x7f, end_bus: 0x80 }), + Ok((0xe000_0000, 0x80, 0xff)), + ] + ); +} + +/// 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] +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, 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..85625194a5a 100644 --- a/toyos-userbound/tests/firmware.rs +++ b/toyos-userbound/tests/firmware.rs @@ -5,11 +5,29 @@ 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}; +/// 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] { + windows(&[(base, first_bus, last_bus)]) +} + const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { MemoryMapEntry { uefi_type, start, end } } @@ -31,7 +49,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 +63,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 +75,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 +95,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 +283,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 +332,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 +440,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 +457,42 @@ 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); +} + +/// 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] @@ -647,7 +690,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 +704,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 +721,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