diff --git a/Cargo.lock b/Cargo.lock index 051264fc215..5186731c915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -954,6 +954,7 @@ name = "toyos-acpi" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-bootmap", ] [[package]] @@ -975,6 +976,9 @@ dependencies = [ [[package]] name = "toyos-bootmap" version = "0.1.0" +dependencies = [ + "toyos-abi", +] [[package]] name = "toyos-build" diff --git a/bootloader/Cargo.lock b/bootloader/Cargo.lock index f7dfd89b21e..e342c462790 100644 --- a/bootloader/Cargo.lock +++ b/bootloader/Cargo.lock @@ -264,6 +264,7 @@ name = "toyos-acpi" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-bootmap", ] [[package]] @@ -273,6 +274,9 @@ version = "0.1.0" [[package]] name = "toyos-bootmap" version = "0.1.0" +dependencies = [ + "toyos-abi", +] [[package]] name = "toyos-elf" diff --git a/bootloader/src/watchdog.rs b/bootloader/src/watchdog.rs index 798a0f15b18..4440cfd59b5 100644 --- a/bootloader/src/watchdog.rs +++ b/bootloader/src/watchdog.rs @@ -23,8 +23,7 @@ use toyos_tco::{ use uefi::prelude::*; use uefi::table::boot::{MemoryDescriptor, PAGE_SIZE}; -/// x86-64's 52-bit physical-address ceiling, as `kernel/src/drivers/acpi.rs` -/// bounds the same reads. +/// x86-64's 52-bit physical-address ceiling. const MAX_PHYS: u64 = 1 << 52; /// What of the ECAM window this reads: bus 0's thirty-two devices, eight diff --git a/issues/build/no-harness-test-boots-qemus-own-edk2.md b/issues/build/no-harness-test-boots-qemus-own-edk2.md new file mode 100644 index 00000000000..d71800896a9 --- /dev/null +++ b/issues/build/no-harness-test-boots-qemus-own-edk2.md @@ -0,0 +1,20 @@ +--- +status: open +kind: tooling +opened: 2026-09-28 +--- + +# No harness test boots QEMU's own edk2 + +Every harness boot uses the repository's `ovmf/`. QEMU's own +`edk2-x86_64-code.fd` hands an AMD vCPU a reserved range at +`0xfd00000000..0x10000000000`, which `ovmf/` never names, and a kernel whose +direct map reached every range in the map died there after `pmm:`, and a +survey that offered a 64-bit BAR only the space above that range handed the +NIC over nowhere. The host tests `toyos-bootmap/tests/direct_map.rs` and +`toyos-pci`'s `placement` hold the two rules; nothing boots the +firmware that broke them, so a regression outside those rules is seen by the +first person who boots QEMU's firmware and nobody else. + +Owner: orchestrator. Exit condition: a harness test boots QEMU's own edk2 on +the command line the release probe uses and reaches `compositor: ready` and `netd: DHCP: lease`. diff --git a/issues/kernel/map-mmio-takes-an-address-past-the-direct-map-window.md b/issues/kernel/map-mmio-takes-an-address-past-the-direct-map-window.md new file mode 100644 index 00000000000..99b8891817e --- /dev/null +++ b/issues/kernel/map-mmio-takes-an-address-past-the-direct-map-window.md @@ -0,0 +1,22 @@ +--- +status: open +kind: defect +opened: 2026-09-28 +--- + +# `map_mmio` takes an address past the direct map's window + +`paging::map_mmio` (`kernel/src/arch/x86_64/paging.rs`) maps `phys` at +`PHYS_OFFSET + phys` and bounds nothing. The direct map's window is +`toyos_bootmap::DIRECT_MAP_WINDOW`, `0x800000000000` (128 TiB): root slots +256 to 511. A firmware-named register base at or past it overflows that sum. +`vtd::window` (`kernel/src/arch/x86_64/vtd/mod.rs`) bounds a DMAR register base +by its own `MAX_PHYS`, `1 << 52`, so a base between the two reaches it; a BAR +in a firmware window past 128 TiB reaches it through `pcidev::probe_dword`. + +Nothing has been observed to reach it: every firmware this tree has booted +puts its registers far below. + +Owner: orchestrator. Exit condition: `map_mmio` refuses by name an address past +`DIRECT_MAP_WINDOW`, and each firmware-named base is checked against that bound +rather than against the architecture's width. diff --git a/issues/kernel/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md b/issues/kernel/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md new file mode 100644 index 00000000000..fdb6fa5f2d2 --- /dev/null +++ b/issues/kernel/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md @@ -0,0 +1,20 @@ +--- +status: open +kind: tooling +opened: 2026-09-28 +--- + +# Nothing reds when the BAR survey drops an assigned BAR or a forwarded range + +`pcidev::publish` (`kernel/src/pcidev/mod.rs`) feeds `toyos_pci::placement::free_runs` +three extents: the firmware map, every BAR firmware assigned (`taken.push` of +`memory.address()..end`), and every range a bridge forwards +(`taken.push(forwarded)`). The rule is host-tested; the two kernel pushes are +not. Deleting the BAR push still places the NIC at `0xc000200000` on QEMU's +edk2 and at `0x800200000` on `ovmf/`, because the 2 MiB alignment steps over +firmware's BARs there, and `alone_in_its_page` checks only BARs, not the ranges +bridges forward. No machine in reach puts a BAR it hands over behind a bridge. + +Owner: orchestrator. Exit condition: a guest test goes red when either push is +deleted — a machine whose firmware places a BAR, or a bridge's forwarded range, +where the first 2 MiB-aligned candidate would otherwise land. diff --git a/issues/kernel/the-direct-map-bound-on-a-firmware-address-is-52-bits.md b/issues/kernel/the-direct-map-bound-on-a-firmware-address-is-52-bits.md deleted file mode 100644 index 56bfccc4b8d..00000000000 --- a/issues/kernel/the-direct-map-bound-on-a-firmware-address-is-52-bits.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-03 ---- - -# A firmware address the direct map does not cover passes `readable` and faults on the read - -`DirectPhys::readable` (`kernel/src/drivers/acpi.rs:37-39`) accepts any physical -address below `MAX_PHYS`, which is `1 << 52` — x86-64's architectural ceiling, -not this machine's. The direct map covers installed RAM only: the bootloader -maps `size` bytes at `PHYS_OFFSET` in `build_boot_page_tables` -(`bootloader/src/main.rs:469-505`), so an XSDT entry naming an address above the -top of memory passes `readable`, reaches `read_volatile` in `DirectPhys::byte` -(`acpi.rs:41-43`), and faults in Ring 0 on firmware's word. - -Pre-existing: the shape this replaced bounded `table_at` by the same -`MAX_PHYS`. Nothing has been observed to reach it — every firmware this tree has -booted publishes its tables inside RAM — and the input is untrusted, which is -the whole reason the bound is supposed to be one. - -**Exit condition.** `readable` is bounded by the memory map's top rather than by -`MAX_PHYS`, and an address past it is refused by name like every other -`TableError`. The map is already in the kernel: `mm::init` takes -`&[MemoryMapEntry]`, so what is missing is a reader for its ceiling, not the -number. diff --git a/issues/kernel/the-direct-maps-page-directories-come-from-a-512-kib-heap.md b/issues/kernel/the-direct-maps-page-directories-come-from-a-512-kib-heap.md new file mode 100644 index 00000000000..02552f46620 --- /dev/null +++ b/issues/kernel/the-direct-maps-page-directories-come-from-a-512-kib-heap.md @@ -0,0 +1,23 @@ +--- +status: open +kind: defect +opened: 2026-09-28 +--- + +# The direct map's page directories come from a 512 KiB heap + +`paging::init` (`kernel/src/arch/x86_64/paging.rs`) builds the direct map +before `alloc::init`, so every table it takes is a `Box` from the early bump +heap (`EARLY_SIZE`, `kernel/src/mm/alloc.rs`): 512 KiB, 128 pages of 4 KiB. The +direct map takes one page directory per GiB it reaches, plus the root and one +second-level table per 512 GiB. A machine whose memory ends past what those +128 pages hold dies in `paging::init` with `memory allocation of 4096 bytes +failed`, the panic QEMU's edk2 produced when the map reached 1 TiB. + +The ceiling is an estimate, not a measurement: 128 pages less the root, one +second-level table, whatever the boot allocated before `mm::init`, and the +alignment padding each growth of `AddressSpace::children` leaves before the +next 4 KiB-aligned table. About 120 GiB of memory. + +Owner: orchestrator. Exit condition: no table of the direct map comes from the +early heap, whatever the map's end. diff --git a/issues/panic-path/a-panic-inside-mm-init-may-not-reach-the-panel.md b/issues/panic-path/a-panic-inside-mm-init-may-not-reach-the-panel.md new file mode 100644 index 00000000000..ce8a376e52d --- /dev/null +++ b/issues/panic-path/a-panic-inside-mm-init-may-not-reach-the-panel.md @@ -0,0 +1,24 @@ +--- +status: open +kind: defect +opened: 2026-09-28 +--- + +# A panic inside `mm::init` may not reach the panel + +From `paging::init`'s CR3 load to `drivers::panic_console::remap` +(`kernel/src/main.rs`), the panel is written through the kernel's own direct +map, which reaches `toyos_bootmap::x86_64::direct_map_end`: the boot map's +4 GiB, or the end of the highest memory range. A scanout past that end has no +mapping until `remap` maps it, so a panic in that span — `alloc::init`, and +`paging::seal_kernel_half`'s up to 255 table allocations — faults on the first +pixel instead of painting. The extent before it reached every descriptor in +the map, so a scanout the map describes was covered. + +Nothing has been observed to reach it: every firmware this tree has booted +puts its scanout below 4 GiB. + +Owner: orchestrator. Exit condition: the scanout is mapped in the kernel's +tables before the CR3 load that makes them live, and a boot actuator that +panics between `paging::init` and `remap` with the scanout above the direct map +paints its report. diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index e6d0370abcf..be62dcc5ee5 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -84,6 +84,7 @@ name = "toyos-acpi" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-bootmap", ] [[package]] @@ -97,6 +98,9 @@ version = "0.1.0" [[package]] name = "toyos-bootmap" version = "0.1.0" +dependencies = [ + "toyos-abi", +] [[package]] name = "toyos-dma" diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index 37fb11c9eab..c3fd3144c8d 100644 --- a/kernel/src/arch/aarch64/boot.rs +++ b/kernel/src/arch/aarch64/boot.rs @@ -20,7 +20,7 @@ use toyos_abi::boot::{KernelArgs, MemoryMapEntry}; use toyos_acpi::MadtEntry; use super::control_regs as regs; -use crate::drivers::acpi::DirectPhys; +use crate::drivers::acpi::direct_phys; use crate::log; use crate::mm::Region; @@ -179,7 +179,7 @@ pub fn after_console(args: &KernelArgs, maps: &[MemoryMapEntry]) { /// The MADT's GIC structures and the GTDT's timers, decoded and said: what /// the interrupt controller and the timer of stage 4 are built from. fn survey(rsdp_addr: u64) { - match toyos_acpi::find_table(DirectPhys, rsdp_addr, b"APIC", toyos_acpi::MADT_ENTRIES) { + match toyos_acpi::find_table(direct_phys(), rsdp_addr, b"APIC", toyos_acpi::MADT_ENTRIES) { Ok(madt) => { let (mut cpus, mut enabled) = (0u32, 0u32); for item in toyos_acpi::madt_entries(&madt) { @@ -221,7 +221,7 @@ fn survey(rsdp_addr: u64) { } Err(e) => log!("ACPI: MADT unusable: {e:?}"), } - match toyos_acpi::gtdt(DirectPhys, rsdp_addr) { + match toyos_acpi::gtdt(direct_phys(), rsdp_addr) { Ok(gtdt) => log!( "ACPI: GTDT timers: EL1 physical GSIV {}, EL1 virtual GSIV {}, EL2 GSIV {} ({})", gtdt.non_secure_el1.gsiv, diff --git a/kernel/src/arch/aarch64/console_uart.rs b/kernel/src/arch/aarch64/console_uart.rs index 2b30bad860e..e316a040308 100644 --- a/kernel/src/arch/aarch64/console_uart.rs +++ b/kernel/src/arch/aarch64/console_uart.rs @@ -8,7 +8,7 @@ use core::sync::atomic::{AtomicU64, Ordering}; use toyos_acpi::{SerialInterface, GAS_SYSTEM_MEMORY}; -use crate::drivers::acpi::DirectPhys; +use crate::drivers::acpi::direct_phys; use crate::log; use crate::mm::{DirectMap, Mmio}; @@ -35,7 +35,7 @@ fn regs() -> Mmio { /// Find the UART SPCR names and answer whether it is one this file drives. pub fn init(rsdp_addr: u64) -> bool { - let spcr = match toyos_acpi::spcr(DirectPhys, rsdp_addr) { + let spcr = match toyos_acpi::spcr(direct_phys(), rsdp_addr) { Ok(spcr) => spcr, Err(e) => { log!("serial: no console UART, because the SPCR is unusable: {e:?}"); diff --git a/kernel/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs index 29fa4e7bf8d..5f532fa2415 100644 --- a/kernel/src/arch/aarch64/paging.rs +++ b/kernel/src/arch/aarch64/paging.rs @@ -141,7 +141,11 @@ pub fn map_mmio(_phys: u64, _size: u64, _policy: MmioPolicy) -> crate::mm::Mmio owed!("the kernel's page tables", "stage 4") } -pub(crate) fn init(_memory_map: &[MemoryMapEntry]) { +pub(crate) fn init(_memory_map: &[MemoryMapEntry]) -> toyos_bootmap::DirectMapEnd { + owed!("the kernel's page tables", "stage 4") +} + +pub(crate) fn seal_kernel_half() { owed!("the kernel's page tables", "stage 4") } diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs index 33657f357fa..43991575b4a 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -20,6 +20,7 @@ use crate::arch::control_regs::PcidActive; use crate::arch::cpu::Invpcid; use crate::sync::Lock; use crate::vma::{self, Occupancy, Region, RegionKind}; +use toyos_bootmap::ROOT_HIGH_HALF; use toyos_userbound::PageSpan; use crate::MemoryMapEntry; @@ -381,10 +382,14 @@ impl AddressSpace { let kernel_as = kernel().lock(); let mut pml4 = Box::new(PageTablePage([0; 512])); - for i in 256..512 { - if kernel_as.root[i] & PAGE_PRESENT != 0 { - pml4.init_entry(i, kernel_as.root[i]); - } + for slot in ROOT_HIGH_HALF..512 { + let entry = kernel_as.root[slot]; + assert!( + entry & PAGE_PRESENT != 0, + "new_user: kernel root slot {slot} is absent, and this space would never see what is \ + mapped there: `seal_kernel_half` runs before the first user space" + ); + pml4.init_entry(slot, entry); } Some(Self { @@ -783,9 +788,8 @@ impl AddressSpace { flush_tlb_all(); } - /// Replaces whatever is there — the boot map covers every physical - /// address, so an MMIO window's target is pre-mapped by the time its - /// driver asks; a page `guard_4k` already split must not reach here. + /// Replaces whatever is there; a page `guard_4k` already split must not + /// reach here. fn map_2m(&mut self, phys: u64, flags: u64) { let virt = crate::mm::DirectMap::from_phys(phys).as_ptr::() as u64; let pd_idx = indices(virt).2; @@ -839,9 +843,17 @@ impl AddressSpace { // SAFETY: same argument as `pdpt` above, one level down. unsafe { PageTablePage::from_phys_mut(pdpt[pdpt_idx] & ADDR_MASK) } } -} -const MIN_PHYS_MAP: u64 = 4 * 1024 * 1024 * 1024; + /// An empty second-level table under the kernel's empty root slot `slot`. + fn install_root(&mut self, slot: usize) { + let child = Box::new(PageTablePage([0; 512])); + let va = crate::mm::PHYS_OFFSET + ((slot - ROOT_HIGH_HALF) as u64) * (1 << 39); + self.root + .write(slot, va, child.phys() | PAGE_PRESENT | PAGE_WRITE) + .expect_install("install_root"); + self.children.push(child); + } +} /// `Arc>`, not `Lock>`: a kernel thread names it /// as `KernelPayload.address_space` with no second answer. Leaked, since the @@ -910,15 +922,13 @@ pub fn guard_kernel_page(addr: u64) { kernel().lock().guard_4k(crate::mm::DirectMap::phys_of(addr as *const u8)); } -/// Build kernel page tables: map all physical memory in the high half using 2MB large pages. -pub(crate) fn init(memory_map: &[MemoryMapEntry]) { - let mut max_addr: u64 = MIN_PHYS_MAP; - for entry in memory_map { - if entry.end > max_addr { - max_addr = entry.end; - } - } - max_addr = (max_addr + PAGE_2M - 1) & !(PAGE_2M - 1); +/// Build kernel page tables: the direct map in the high half, in 2 MiB pages, +/// as far as [`toyos_bootmap::x86_64::direct_map_end`] reaches, and answer +/// that end. +pub(crate) fn init(memory_map: &[MemoryMapEntry]) -> toyos_bootmap::DirectMapEnd { + let extent = toyos_bootmap::x86_64::direct_map_end(memory_map) + .unwrap_or_else(|refusal| panic!("paging: firmware's memory map: {refusal}")); + let end = extent.get(); let mut kernel = AddressSpace { root: Box::new(PageTablePage([0; 512])), @@ -928,11 +938,17 @@ pub(crate) fn init(memory_map: &[MemoryMapEntry]) { pcid: PcidHandle::Kernel, }; + // Only the direct map's slots: these tables come from the early heap. + let last_slot = indices(crate::mm::DirectMap::from_phys(end - 1).as_ptr::() as u64).0; + for slot in ROOT_HIGH_HALF..=last_slot { + kernel.install_root(slot); + } let mut addr: u64 = 0; - while addr < max_addr { + while addr < end { kernel.map_2m(addr, PAGE_PRESENT | PAGE_WRITE); addr += PAGE_2M; } + crate::log!("paging: the direct map covers 0x0..{end:#x}"); let cr3 = kernel.root(); KERNEL_CR3.store(cr3.0, core::sync::atomic::Ordering::Release); @@ -950,6 +966,19 @@ pub(crate) fn init(memory_map: &[MemoryMapEntry]) { unsafe { cr3.load_flush(); } + extent +} + +/// Install a second-level table under every kernel root slot [`init`] left +/// empty, from the pmm's heap, before the first user space copies the slots: +/// one installed after a space was made would be missing from that space. +pub(crate) fn seal_kernel_half() { + let mut kernel = kernel().lock(); + for slot in ROOT_HIGH_HALF..512 { + if kernel.root[slot] & PAGE_PRESENT == 0 { + kernel.install_root(slot); + } + } } fn has(entry: u64, flag: u64) -> u8 { diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index 7c4f04417a7..7ea329f7195 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -26,7 +26,7 @@ use crate::drivers::xhci::stop; use crate::log; use crate::DirectMap; use toyos_acpi::{ - Century, MadtEntry, Phys, Reset, CMOS_RAM, MADT_ENTRIES, SDT_HEADER_LEN, SDT_REVISION, + Century, MadtEntry, Mapped, Memory, Reset, CMOS_RAM, MADT_ENTRIES, SDT_HEADER_LEN, SDT_REVISION, }; pub use toyos_acpi::{IoApicEntry, SourceOverride, TableError}; @@ -37,32 +37,32 @@ pub struct MadtInfo { pub source_overrides: Vec, } -/// x86-64's 52-bit physical-address ceiling; at or above this, `DirectMap`'s unchecked `+ PHYS_OFFSET` would wrap into the user half. -// CPUID's MAXPHYADDR may be smaller than 52 bits but never larger, so this is a safe bound on every real machine. -const MAX_PHYS: u64 = 1 << 52; - /// Firmware's physical addresses, read through the direct map. #[derive(Clone, Copy)] -pub struct DirectPhys; - -impl Phys for DirectPhys { - fn readable(self, phys: u64, len: usize) -> bool { - phys != 0 && phys.checked_add(len as u64).is_some_and(|end| end <= MAX_PHYS) - } +pub struct DirectMemory; +impl Memory for DirectMemory { fn byte(self, phys: u64) -> u8 { - // SAFETY: `readable` bounded `phys` below `MAX_PHYS`. + // SAFETY: `Mapped` asks only inside the direct map's extent it was + // made with, and the map never shrinks. unsafe { read_volatile(DirectMap::from_phys(phys).as_ptr::()) } } } +type DirectPhys = Mapped; + +/// The reader over the direct map as far as it reaches now. +pub fn direct_phys() -> DirectPhys { + Mapped::new(DirectMemory, crate::mm::direct_map_end()) +} + /// A firmware table whose declared length has been checked to cover the read bytes, and whose declared bytes sum to zero. #[derive(Clone, Copy)] pub struct Table(toyos_acpi::Table); impl Table { pub fn open(base: u64, signature: &[u8; 4], needed: usize) -> Result { - toyos_acpi::Table::open(DirectPhys, base, signature, needed).map(Table) + toyos_acpi::Table::open(direct_phys(), base, signature, needed).map(Table) } /// The declared length, already bounded by [`Table::open`]. @@ -83,14 +83,14 @@ impl Table { return None; } let at = self.0.base() + offset as u64; - // SAFETY: `Table::open` bounded the whole declared length below `MAX_PHYS`, and `end <= len` puts this read inside it. + // SAFETY: `Table::open` put the whole declared length inside the direct map, and `end <= len` puts this read inside it. Some(unsafe { read_unaligned(DirectMap::from_phys(at).as_ptr::().cast::()) }) } } /// The first table in the XSDT with this signature, validated for `needed` bytes. pub fn find_table(rsdp_addr: u64, signature: &[u8; 4], needed: usize) -> Result { - toyos_acpi::find_table(DirectPhys, rsdp_addr, signature, needed).map(Table) + toyos_acpi::find_table(direct_phys(), rsdp_addr, signature, needed).map(Table) } /// ACPI 6.5 §5.2.6, Table 5.4: OEM ID is six bytes at offset 10 of every table header. @@ -151,7 +151,7 @@ fn refuse(what: &str, error: TableError) -> Option { /// base address and the PCI segment group it serves. pub fn find_ecam_base(rsdp_addr: u64) -> Option<(u64, u16)> { log!("ACPI: RSDP at {rsdp_addr:#x}"); - let (mcfg, base) = match toyos_acpi::ecam_base(DirectPhys, rsdp_addr) { + let (mcfg, base) = match toyos_acpi::ecam_base(direct_phys(), rsdp_addr) { Ok(found) => found, Err(e) => return refuse("MCFG", e), }; @@ -226,13 +226,13 @@ pub fn init_power(rsdp_addr: u64) { /// FADT revision and the IA-PC boot architecture flags. // `Err` is not "absent" and must not be treated as one by the caller. pub fn iapc_boot_arch(rsdp_addr: u64) -> Result<(u8, u16), TableError> { - toyos_acpi::iapc_boot_arch(DirectPhys, rsdp_addr) + toyos_acpi::iapc_boot_arch(direct_phys(), rsdp_addr) } /// Which CMOS register holds the RTC's century, as the FADT names it. // `Ok(None)` is "no century register", distinct from `Err`, which the caller must not treat as one. pub fn rtc_century_register(rsdp_addr: u64) -> Result, TableError> { - let named = toyos_acpi::rtc_century(DirectPhys, rsdp_addr)?; + let named = toyos_acpi::rtc_century(direct_phys(), rsdp_addr)?; // The host can't vary what QEMU's FADT declares, so the actuator override forces "no century register" here. let named = if crate::actuator::rtc_no_century() { Century::Absent } else { named }; @@ -355,7 +355,7 @@ pub fn shutdown() -> ! { /// Given the RSDP address, parse XSDT -> HPET table -> return HPET MMIO base address. pub fn find_hpet_base(rsdp_addr: u64) -> Option { - let base = match toyos_acpi::hpet_base(DirectPhys, rsdp_addr) { + let base = match toyos_acpi::hpet_base(direct_phys(), rsdp_addr) { Ok(base) => base, Err(e) => return refuse("HPET", e), }; @@ -365,7 +365,7 @@ pub fn find_hpet_base(rsdp_addr: u64) -> Option { /// Parse MADT (signature "APIC") to discover per-CPU APIC IDs. pub fn parse_madt(rsdp_addr: u64) -> Option { - let madt = match toyos_acpi::find_table(DirectPhys, rsdp_addr, b"APIC", MADT_ENTRIES) { + let madt = match toyos_acpi::find_table(direct_phys(), rsdp_addr, b"APIC", MADT_ENTRIES) { Ok(table) => table, Err(e) => return refuse("MADT", e), }; diff --git a/kernel/src/drivers/panic_console/mod.rs b/kernel/src/drivers/panic_console/mod.rs index 94bd4eac608..bd46003575e 100644 --- a/kernel/src/drivers/panic_console/mod.rs +++ b/kernel/src/drivers/panic_console/mod.rs @@ -397,7 +397,7 @@ const fn framebuffer_is_reclaimed_ram( let mut i = 0; while i < maps.len() { let entry = &maps[i]; - if entry.start < end && phys < entry.end && mm::pmm::is_usable_type(entry.uefi_type) { + if entry.start < end && phys < entry.end && toyos_bootmap::is_usable_type(entry.uefi_type) { return Some(entry.uefi_type); } i += 1; diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index 897b341f94a..5e2737de802 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -441,21 +441,22 @@ impl PciDevice { } } - /// Every memory range this function forwards to its secondary bus, below - /// 4 GiB. Empty on a function that is not a bridge. + /// Every memory range this function forwards to its secondary bus. Empty on + /// a function that is not a bridge. /// /// **Read, never probed**: these are the ranges nothing above this bridge /// may hand out, and reading them costs the machine nothing — unlike /// `bar_size`, which takes memory decode off for the length of its probe. - pub fn forwarded_below_4g(&self) -> Vec { + pub fn forwarded(&self) -> Vec { if self.read_config_u8(HEADER_TYPE) & !MULTI_FUNCTION != bridge::HEADER_TYPE_BRIDGE { return Vec::new(); } let mut out = Vec::new(); out.extend(bridge::window(self.read_config_u32(bridge::MEMORY_BASE))); - out.extend(bridge::prefetch_below_4g( + out.extend(bridge::prefetch( self.read_config_u32(bridge::PREFETCH_BASE), self.read_config_u32(bridge::PREFETCH_BASE_UPPER), + self.read_config_u32(bridge::PREFETCH_LIMIT_UPPER), )); out } diff --git a/kernel/src/mm/mod.rs b/kernel/src/mm/mod.rs index e278f392230..304e0dd18bd 100644 --- a/kernel/src/mm/mod.rs +++ b/kernel/src/mm/mod.rs @@ -31,6 +31,7 @@ pub use pmm::Region; /// All physical memory is mapped at this virtual offset. pub const PHYS_OFFSET: u64 = 0xFFFF_8000_0000_0000; +const _: () = assert!(toyos_bootmap::DIRECT_MAP_WINDOW == 0u64.wrapping_sub(PHYS_OFFSET)); /// The kernel's one user page size and translation granularity. pub use toyos_userbound::PAGE_2M; @@ -107,6 +108,10 @@ impl core::fmt::LowerHex for UserAddr { } +/// One past the direct map's last byte: the boot map's until `paging::init` +/// builds the kernel's own, which never reaches less. +static DIRECT_MAP_END: toyos_bootmap::DirectMapEndCell = toyos_bootmap::DirectMapEndCell::boot(); + /// Converts between physical addresses and kernel virtual pointers; use only at that boundary, not for storing pointers. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DirectMap(u64); @@ -131,6 +136,11 @@ impl DirectMap { } } +/// One past the direct map's last byte now. +pub fn direct_map_end() -> toyos_bootmap::DirectMapEnd { + DIRECT_MAP_END.get() +} + impl core::fmt::Display for DirectMap { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{:#x}", self.0) @@ -143,12 +153,15 @@ impl core::fmt::Debug for DirectMap { } } -/// Call once at boot, in order: pmm (physical pages) → paging (direct map) → alloc (heap). +/// Call once at boot, in order: pmm (physical pages) → paging (direct map) → +/// alloc (heap) → every kernel root slot, before the first user space copies +/// them. pub fn init(memory_map: &[MemoryMapEntry], reserved: &[Region]) { alloc::init_early(); pmm::init(memory_map, reserved); - paging::init(memory_map); + DIRECT_MAP_END.set(paging::init(memory_map)); alloc::init(); + paging::seal_kernel_half(); } /// The two memory facts every crash report ends its contexts with: how deep diff --git a/kernel/src/mm/pmm.rs b/kernel/src/mm/pmm.rs index 7a93ddb17fc..fe23dfc66fe 100644 --- a/kernel/src/mm/pmm.rs +++ b/kernel/src/mm/pmm.rs @@ -388,22 +388,8 @@ pub fn stats() -> (u64, u64) { (total, used) } -const EFI_LOADER_CODE: u32 = 1; -pub const EFI_LOADER_DATA: u32 = 2; -const EFI_BOOT_SERVICES_CODE: u32 = 3; -const EFI_BOOT_SERVICES_DATA: u32 = 4; -const EFI_CONVENTIONAL_MEMORY: u32 = 7; - -/// Whether a UEFI memory type becomes free RAM the PMM will hand out. -pub const fn is_usable_type(uefi_type: u32) -> bool { - matches!(uefi_type, - EFI_LOADER_CODE | EFI_LOADER_DATA | - EFI_BOOT_SERVICES_CODE | EFI_BOOT_SERVICES_DATA | - EFI_CONVENTIONAL_MEMORY) -} - fn is_usable(entry: &MemoryMapEntry) -> bool { - is_usable_type(entry.uefi_type) + toyos_bootmap::is_usable_type(entry.uefi_type) } fn overlaps_reserved(start: u64, end: u64, reserved: &[Region]) -> bool { diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 404eecd2767..0fc518dadb6 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -140,18 +140,9 @@ const MAX_GRANT_BYTES: u64 = 8 * 1024 * 1024; /// window its lent regions are placed in ([`Space::lend`]). const MAX_GRANT_TOTAL: u64 = 32 * 1024 * 1024; -/// Where the fixed platform devices start on a PC: the I/O APIC, the HPET and -/// the LAPIC window are at and above this, so a 32-bit window may not reach it. -const PLATFORM_MMIO: u64 = 0xFEC0_0000; - /// The unit every run in this module's records is said in. const MIB: u64 = 1024 * 1024; -/// How much address space the windows may take, above what firmware assigned: -/// every BAR of every function this machine can hand out, at one 2 MiB page -/// each. -const WINDOW_SPAN: u64 = (MAX_FUNCTIONS * BARS) as u64 * PAGE_2M; - /// A function's own configuration space, which is all a claim may read of it /// (PCIe base spec §7.2.2: 4 KiB per function under ECAM). const CONFIG_BYTES: u64 = 4096; @@ -314,11 +305,10 @@ struct Machine { /// Requester ids the kernel's own drivers bound. A claim on one of them /// would be two drivers on one device. kernel_driven: Vec, - /// Where this module may ask the machine about an address: runs below - /// 4 GiB for a 32-bit BAR, and above everything firmware assigned for a - /// 64-bit one. Separate because a 32-bit BAR cannot hold an address a - /// 64-bit one can, and each shortens as [`placement::reserve`] hands an - /// address out of it. + /// Where this module may ask the machine about an address + /// ([`placement::free_runs`]), one list per BAR width. Separate because a + /// 32-bit BAR cannot hold an address a 64-bit one can, and each shortens as + /// [`placement::reserve`] hands an address out of it. /// /// **A run is where the machine may be asked, never where a BAR is put.** /// What the firmware map, this bus's assigned BARs and its bridges' @@ -460,19 +450,10 @@ pub fn note_kernel_driver(pci: &PciDevice) { /// **Before any driver `init`**, because the sizing probe below takes memory /// decode off the function it is probing for the length of the probe, and a /// driver mid-transfer must not meet that. -/// -/// **The high run is above everything firmware described**, which is every BAR -/// it assigned *and* every entry of the memory map it handed the loader — RAM, -/// its own runtime services, the ACPI regions and the fixed platform apertures -/// alike. Below 4 GiB there is no such address: the platform's fixed MMIO is at -/// [`PLATFORM_MMIO`] and the memory map reaches it, so the low runs are the -/// gaps *between* what those sources describe ([`free_runs_below_4g`]). pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry], firmware: &[RootBridgeWindow]) { - let mut wide_end = 0u64; let mut decoded = Vec::new(); - for entry in maps { - wide_end = wide_end.max(entry.end); - } + let mut taken: Vec = + maps.iter().map(|entry| Window { start: entry.start, end: entry.end }).collect(); for device in devices { // Bounded by the header's own declaration: a bridge has two BAR slots // and four registers past them that are not BARs, and `bar_size` below @@ -489,26 +470,39 @@ pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry], fir let size = device.bar_size(index).unwrap_or(0).max(1); let end = memory.address().saturating_add(size); decoded.push((requester(device), memory.address(), end)); - wide_end = wide_end.max(end); + taken.push(Window { start: memory.address(), end }); } // A 64-bit BAR's high half is the next register and is not a BAR: // decoding it would read an address out of address bits. index += if wide { 2 } else { 1 }; } + for forwarded in device.forwarded() { + log!( + "pcidev: PCI {:02x}:{:02x}.{} forwards {:#x}..{:#x} to its secondary bus", + device.bus, + device.dev, + device.func, + forwarded.start, + forwarded.end, + ); + taken.push(forwarded); + } } account_for(firmware, &decoded); - let low = free_runs_below_4g(devices, maps, &decoded); - let high = match window(wide_end, u64::MAX) { - (0, _) => Vec::new(), - (start, end) => alloc::vec![Window { start, end }], + let mut runs = |wide| -> Vec { + placement::free_runs(&mut taken, wide) + .filter(|run| run.end - run.start >= PAGE_2M) + .collect() }; + let (low, high) = (runs(false), runs(true)); log!( - "pcidev: {} functions; {} run(s) of {} MiB or more below {PLATFORM_MMIO:#x} and {} above \ - everything firmware described", + "pcidev: {} functions; {} run(s) of {} MiB or more below {:#x} and {} from {:#x}", devices.len(), low.len(), PAGE_2M / MIB, + placement::PLATFORM_MMIO, high.len(), + placement::WIDE_FLOOR, ); for run in low.iter().chain(high.iter()) { log!("pcidev: {:#x}..{:#x} ({} MiB)", run.start, run.end, (run.end - run.start) / MIB); @@ -556,79 +550,6 @@ fn account_for(firmware: &[RootBridgeWindow], decoded: &[(u16, u64, u64)]) { } } -/// What is left below 4 GiB, after everything this machine could be asked about -/// itself. -/// -/// **Below 4 GiB there is no address above everything firmware described** — -/// the platform's fixed MMIO is at [`PLATFORM_MMIO`] and the memory map reaches -/// it — so a 32-bit window is a run *between* things rather than a span above -/// them, and this is the subtraction that finds one. -/// -/// It accounts for exactly three things and each is *read*: the firmware memory -/// map, the BARs this bus has assigned, and every range a bridge forwards to a -/// secondary bus. None of the three says an address reaches the bus, which is -/// why a run here is only where [`place_bar`] *may* ask. -fn free_runs_below_4g( - devices: &[PciDevice], - maps: &[MemoryMapEntry], - decoded: &[(u16, u64, u64)], -) -> Vec { - let mut taken: Vec<(u64, u64)> = Vec::new(); - let mut note = |start: u64, end: u64| { - let (start, end) = (start.min(PLATFORM_MMIO), end.min(PLATFORM_MMIO)); - if start < end { - taken.push((start, end)); - } - }; - for entry in maps { - note(entry.start, entry.end); - } - for (_, start, end) in decoded { - note(*start, *end); - } - for device in devices { - for forwarded in device.forwarded_below_4g() { - log!( - "pcidev: PCI {:02x}:{:02x}.{} forwards {:#x}..{:#x} to its secondary bus", - device.bus, - device.dev, - device.func, - forwarded.start, - forwarded.end, - ); - note(forwarded.start, forwarded.end); - } - } - taken.sort_unstable(); - let mut free: Vec = Vec::new(); - let mut at = 0u64; - for (start, end) in taken { - if start > at { - free.push(Window { start: at, end: start }); - } - at = at.max(end); - } - if at < PLATFORM_MMIO { - free.push(Window { start: at, end: PLATFORM_MMIO }); - } - free.retain(|run| run.end - run.start >= PAGE_2M); - free -} - -/// The span above `assigned` this module may hand out, or an empty one where -/// there is no room under `ceiling`. -fn window(assigned: u64, ceiling: u64) -> (u64, u64) { - if assigned == 0 { - return (0, 0); - } - let base = align_2m(assigned as usize) as u64; - let top = base.saturating_add(WINDOW_SPAN); - if base >= ceiling || top > ceiling { - return (0, 0); - } - (base, top) -} - /// Why a function could not be handed over. Carried rather than collapsed: one /// message for all of them sends whoever reads the log looking in the wrong /// place. @@ -688,8 +609,8 @@ impl core::fmt::Display for Refusal { ), Self::NoRun { wide: true } => write!( f, - "this machine has no 2 MiB-aligned 64-bit address space both above what firmware \ - assigned and inside a window firmware declared to offer its BAR" + "nothing from 4 GiB up is both free of the firmware map, this bus's assigned \ + BARs and its bridges' forwarded ranges and inside a window firmware declared" ), Self::NoRun { wide: false } => write!( f, @@ -1284,11 +1205,6 @@ fn msix_bar(pci: &PciDevice) -> Option { /// /// **Every caller has named `at` routed first.** A load no bridge forwards does /// not come back on real hardware. -/// -/// **A refused candidate leaves the direct map's entries over its range -/// uncacheable, and that is the whole of what it leaves**: the boot map already -/// covers every physical address, so this takes no address space there is any -/// giving back of, and a run holds no memory the firmware map described. fn probe_dword(at: u64, span: u64, offset: u64) -> u32 { crate::mm::paging::map_mmio(at, span, MmioPolicy::Uncacheable).read_u32(offset) } diff --git a/kernel/src/rootfs.rs b/kernel/src/rootfs.rs index 1c82fcee304..507fb6c5be4 100644 --- a/kernel/src/rootfs.rs +++ b/kernel/src/rootfs.rs @@ -101,7 +101,7 @@ pub fn init(cmdline: &str, args: &KernelArgs, map: &[MemoryMapEntry]) -> Region let (at, len) = (args.root_image_addr, args.root_image_len); let descriptors = map.iter().map(|entry| Descriptor { ty: entry.uefi_type, start: entry.start, end: entry.end }); let none = Region { start: 0, end: 0 }; - let (handed, region) = match held(descriptors, crate::mm::pmm::EFI_LOADER_DATA, at, len, BLOCK as u64) { + let (handed, region) = match held(descriptors, toyos_bootmap::EFI_LOADER_DATA, at, len, BLOCK as u64) { _ if len == 0 => (Handed::Nothing, none), None => (Handed::Unheld { at, len }, none), Some(extent) => ( diff --git a/toyos-acpi/Cargo.toml b/toyos-acpi/Cargo.toml index c242e4d6663..2990ec42870 100644 --- a/toyos-acpi/Cargo.toml +++ b/toyos-acpi/Cargo.toml @@ -17,3 +17,4 @@ publish = false # The window type the bootloader hands the kernel: decoded here, carried in # `KernelArgs`, and one declaration rather than two that have to agree. toyos-abi = { path = "../toyos-abi" } +toyos-bootmap = { path = "../toyos-bootmap" } diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index 90ad4a8e3ba..48edc0db15e 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -20,6 +20,8 @@ mod madt; mod resource; mod spcr; +use toyos_bootmap::DirectMapEnd; + pub use fadt::{ century_of, dsdt_address, iapc_boot_arch, reset_register, rtc_century, Century, Reset, CMOS_RAM, FADT_FOR_RESET, FADT_PM1A_CNT_BLK, FADT_X_DSDT, @@ -43,6 +45,41 @@ pub trait Phys: Copy { fn byte(self, phys: u64) -> u8; } +/// Physical memory as a kernel reads it through its direct map, one byte at a +/// time. +/// +/// # Contract +/// [`Mapped`] calls [`byte`](Memory::byte) only inside a range its +/// [`readable`](Phys::readable) accepted. +pub trait Memory: Copy { + fn byte(self, phys: u64) -> u8; +} + +/// [`Memory`] bounded by the direct map it is read through: a range with a byte +/// at or past its [`DirectMapEnd`], or at address zero, is refused and never +/// read. +#[derive(Clone, Copy)] +pub struct Mapped { + memory: M, + end: DirectMapEnd, +} + +impl Mapped { + pub fn new(memory: M, end: DirectMapEnd) -> Self { + Self { memory, end } + } +} + +impl Phys for Mapped { + fn readable(self, phys: u64, len: usize) -> bool { + phys != 0 && phys.checked_add(len as u64).is_some_and(|last| last <= self.end.get()) + } + + fn byte(self, phys: u64) -> u8 { + self.memory.byte(phys) + } +} + /// Why a firmware table cannot be used; each variant is a distinct instruction to the caller. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TableError { diff --git a/toyos-acpi/tests/common/mod.rs b/toyos-acpi/tests/common/mod.rs index 213e4339f0b..428de7c47ac 100644 --- a/toyos-acpi/tests/common/mod.rs +++ b/toyos-acpi/tests/common/mod.rs @@ -3,7 +3,7 @@ //! A machine's physical memory as a list of regions, and the builders that lay //! crafted tables out in one. -use toyos_acpi::Phys; +use toyos_acpi::{Memory, Phys}; #[derive(Clone, Copy)] pub struct Machine<'a> { @@ -35,6 +35,12 @@ impl Phys for Machine<'_> { } } +impl Memory for Machine<'_> { + fn byte(self, phys: u64) -> u8 { + Phys::byte(self, phys) + } +} + /// Re-sum an SDT so its byte 9 makes the declared bytes add to zero. pub fn reseal(table: &mut [u8]) { let len = u32::from_le_bytes([table[4], table[5], table[6], table[7]]) as usize; diff --git a/toyos-acpi/tests/mapped.rs b/toyos-acpi/tests/mapped.rs new file mode 100644 index 00000000000..2f090601e86 --- /dev/null +++ b/toyos-acpi/tests/mapped.rs @@ -0,0 +1,64 @@ +//! The reader a kernel decodes firmware's tables through: bounded by the +//! direct map it reads them in, not by the architecture's physical-address +//! width. + +mod common; + +use common::{rsdp, sdt, xsdt, Machine}; +use toyos_acpi::{find_table, Mapped, Phys, Table, TableError}; +use toyos_bootmap::x86_64; + +/// What edk2's map on QEMU q35 with 2 GiB gives the direct map: the boot +/// map's, as `toyos-bootmap`'s `the_reserved_hole_below_1_tib_is_not_mapped` +/// holds. +fn end() -> u64 { + x86_64::direct_map_end(&[]).unwrap().get() +} + +/// The reserved hole that edk2 describes below 1 TiB, inside 52 bits. +const HOLE: u64 = 0xfd_0000_0000; + +const RSDP_AT: u64 = 0x1_0000; +const XSDT_AT: u64 = 0x2_0000; + +fn mapped<'a>(regions: &'a [(u64, &'a [u8])]) -> Mapped> { + Mapped::new(Machine { regions }, x86_64::direct_map_end(&[]).unwrap()) +} + +#[test] +fn a_table_past_the_direct_map_is_refused_before_a_byte_is_read() { + let hpet = sdt(b"HPET", 1, &[0u8; 20]); + let len = hpet.len(); + let end = end(); + for (at, refused) in [(HOLE, 36), (end, 36), (end - len as u64 + 1, len)] { + let regions: &[(u64, &[u8])] = &[(at, &hpet)]; + assert_eq!( + Table::open(mapped(regions), at, b"HPET", 0).err(), + Some(TableError::Unmapped { at, len: refused }), + "a table at {at:#x}" + ); + } + let at = end - len as u64; + let regions: &[(u64, &[u8])] = &[(at, &hpet)]; + assert!(Table::open(mapped(regions), at, b"HPET", 0).is_ok(), "a table whose last byte is the map's"); +} + +/// The machine holds the table and the walk reads it; through the direct map +/// the entry is one it cannot reach, and is skipped. +#[test] +fn an_xsdt_entry_past_the_direct_map_is_skipped() { + let head = rsdp(XSDT_AT, 2, 36); + let root = xsdt(&[HOLE]); + let hpet = sdt(b"HPET", 1, &[0u8; 20]); + let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (HOLE, &hpet)]; + assert!(find_table(Machine { regions }, RSDP_AT, b"HPET", 0).is_ok()); + assert_eq!(find_table(mapped(regions), RSDP_AT, b"HPET", 0).err(), Some(TableError::Absent)); +} + +#[test] +fn address_zero_and_a_range_past_every_address_are_refused() { + let m = mapped(&[]); + assert!(!m.readable(0, 1), "address zero"); + assert!(!m.readable(u64::MAX, 2), "a range whose end does not fit an address"); + assert!(m.readable(1, 1)); +} diff --git a/toyos-bootmap/Cargo.toml b/toyos-bootmap/Cargo.toml index e54f4983c20..dc05967ef7e 100644 --- a/toyos-bootmap/Cargo.toml +++ b/toyos-bootmap/Cargo.toml @@ -1,9 +1,3 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-gpt and -# toyos-tco: the bootloader depends on it by path and its tests run on the host. -# What lives here decides where a transient page table puts a machine's memory -# and its scanout — a decision with no instrument on the machine that gets it -# wrong, since a boot that mislays the panel says nothing about why. - [package] name = "toyos-bootmap" description = "The bootloader's transient page tables, decided rather than built, pure." @@ -11,3 +5,6 @@ version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" publish = false + +[dependencies] +toyos-abi = { path = "../toyos-abi" } diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs index 1ab42f24e56..ce22f5526ab 100644 --- a/toyos-bootmap/src/lib.rs +++ b/toyos-bootmap/src/lib.rs @@ -15,11 +15,21 @@ //! Pure: the scanout, the loader's image and, where the architecture types //! memory by firmware's map, the write-back ranges in; a [`Plan`] out. The //! loader allocates the pages and writes the entries. +//! +//! What the kernel takes from firmware's map when it builds its own tables is +//! here too, because it may never map less than this map did: which types the +//! pmm hands out ([`is_usable_type`]), and how far the direct map reaches +//! ([`x86_64::direct_map_end`], [`DirectMapEnd`]). The types are `EFI_MEMORY_TYPE`'s, +//! and what an OS may do with each after `ExitBootServices` is the UEFI +//! specification's table under `EFI_BOOT_SERVICES.AllocatePages()` (§7.2). +//! That table puts no bound on where a range the OS does not use may sit, and +//! UEFI does not order the map. #![no_std] #![forbid(unsafe_code)] use core::fmt; +use core::sync::atomic::{AtomicU64, Ordering}; pub mod aarch64; pub mod x86_64; @@ -45,6 +55,71 @@ pub const ROOT_HIGH_HALF: usize = 256; /// alike. Everything the entry jump needs, not everything `KernelArgs` names. pub const BOOT_MAP_BYTES: u64 = 4 * GIB; +/// The physical addresses a direct map at root slot [`ROOT_HIGH_HALF`] can +/// hold: every slot from there to the root's last. +pub const DIRECT_MAP_WINDOW: u64 = (512 - ROOT_HIGH_HALF as u64) * GIB_PER_PDPT * GIB; + +/// One past the kernel direct map's last byte. Made only by +/// [`x86_64::direct_map_end`]. +/// +/// ```compile_fail,E0603 +/// let _ = toyos_bootmap::DirectMapEnd(1 << 52); +/// ``` +#[derive(Clone, Copy)] +pub struct DirectMapEnd(u64); + +impl DirectMapEnd { + pub const fn get(self) -> u64 { + self.0 + } +} + +/// Where a [`DirectMapEnd`] is kept between the map that decided it and the +/// readers that ask; it holds no other number. +pub struct DirectMapEndCell(AtomicU64); + +impl DirectMapEndCell { + /// Holding [`BOOT_MAP_BYTES`], until the kernel's own map decides wider. + pub const fn boot() -> Self { + Self(AtomicU64::new(BOOT_MAP_BYTES)) + } + + pub fn set(&self, end: DirectMapEnd) { + self.0.store(end.0, Ordering::Release); + } + + pub fn get(&self) -> DirectMapEnd { + DirectMapEnd(self.0.load(Ordering::Acquire)) + } +} + +const EFI_LOADER_CODE: u32 = 1; +pub const EFI_LOADER_DATA: u32 = 2; +const EFI_BOOT_SERVICES_CODE: u32 = 3; +const EFI_BOOT_SERVICES_DATA: u32 = 4; +const EFI_CONVENTIONAL_MEMORY: u32 = 7; +const EFI_ACPI_RECLAIM_MEMORY: u32 = 9; +const EFI_ACPI_MEMORY_NVS: u32 = 10; + +/// Whether a UEFI memory type becomes free RAM the pmm hands out. +pub const fn is_usable_type(uefi_type: u32) -> bool { + matches!( + uefi_type, + EFI_LOADER_CODE + | EFI_LOADER_DATA + | EFI_BOOT_SERVICES_CODE + | EFI_BOOT_SERVICES_DATA + | EFI_CONVENTIONAL_MEMORY + ) +} + +/// 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 { + is_usable_type(uefi_type) || matches!(uefi_type, EFI_ACPI_RECLAIM_MEMORY | EFI_ACPI_MEMORY_NVS) +} + /// One page directory per GiB of [`BOOT_MAP_BYTES`]. const LOW_DIRECTORIES: usize = (BOOT_MAP_BYTES / GIB) as usize; @@ -86,6 +161,8 @@ pub enum Refusal { /// A 2 MiB page of the low map that is write-back memory in part and not /// in the rest: either type is wrong for some of it. Mixed(u64), + /// Memory that ends here, past [`DIRECT_MAP_WINDOW`]. + PastWindow(u64), } impl fmt::Display for Refusal { @@ -109,6 +186,10 @@ impl fmt::Display for Refusal { "the 2 MiB page at {phys:#x} is part write-back memory and part not, so no one \ memory type is right for all of it" ), + Self::PastWindow(end) => write!( + f, + "memory ends at {end:#x}, past the {DIRECT_MAP_WINDOW:#x} bytes a direct map can hold" + ), } } } diff --git a/toyos-bootmap/src/x86_64.rs b/toyos-bootmap/src/x86_64.rs index 6c138526df2..eb942428f2e 100644 --- a/toyos-bootmap/src/x86_64.rs +++ b/toyos-bootmap/src/x86_64.rs @@ -1,8 +1,28 @@ //! x86-64's encoding of a [`Plan`](crate::Plan): Intel SDM Vol. 3A §4.5, //! Tables 4-15 (a PML4 or PDPT entry naming a table) and 4-17 (a page -//! directory entry mapping a 2 MiB page). +//! directory entry mapping a 2 MiB page); and how far the kernel's own direct +//! map reaches. -use crate::Cache; +use toyos_abi::boot::MemoryMapEntry; + +use crate::{is_read_as_memory, Cache, DirectMapEnd, Refusal, BOOT_MAP_BYTES, DIRECT_MAP_WINDOW, PAGE_2M}; + +/// One past the kernel direct map's last byte: [`BOOT_MAP_BYTES`], or the end +/// of the highest range the kernel reads as memory in whole [`PAGE_2M`] pages, +/// whichever is higher. Memory past [`DIRECT_MAP_WINDOW`] is refused. +/// +/// The low [`BOOT_MAP_BYTES`] are mapped whole, registers and holes included, +/// because x86-64 types those pages by its MTRRs rather than by the entry, and +/// because the kernel goes on using addresses it took through the boot map. +pub fn direct_map_end(map: &[MemoryMapEntry]) -> Result { + map.iter().filter(|entry| is_read_as_memory(entry.uefi_type)).try_fold(BOOT_MAP_BYTES, |end, entry| { + if entry.end > DIRECT_MAP_WINDOW { + return Err(Refusal::PastWindow(entry.end)); + } + Ok(end.max(entry.end.next_multiple_of(PAGE_2M))) + }) + .map(DirectMapEnd) +} const PRESENT: u64 = 1 << 0; const WRITABLE: u64 = 1 << 1; diff --git a/toyos-bootmap/tests/direct_map.rs b/toyos-bootmap/tests/direct_map.rs new file mode 100644 index 00000000000..eb0767a8dbd --- /dev/null +++ b/toyos-bootmap/tests/direct_map.rs @@ -0,0 +1,258 @@ +//! How far the direct map reaches: to the end of the memory the kernel reads, +//! never to a range the map describes and does not call memory. + +use toyos_abi::boot::MemoryMapEntry; +use toyos_bootmap::{ + is_usable_type, x86_64, DirectMapEnd, Refusal, BOOT_MAP_BYTES, DIRECT_MAP_WINDOW, + EFI_LOADER_DATA, PAGE_2M, +}; + +const RESERVED: u32 = 0; +const CONVENTIONAL: u32 = 7; +const ACPI_RECLAIM: u32 = 9; +const ACPI_NVS: u32 = 10; + +const GIB: u64 = 1 << 30; + +fn direct_map_end(map: &[MemoryMapEntry]) -> Result { + x86_64::direct_map_end(map).map(DirectMapEnd::get) +} + +const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { + MemoryMapEntry { uefi_type, start, end } +} + +/// The map QEMU 11.1.1's edk2 (`edk2-x86_64-code.fd`) hands a 2 GiB q35 guest +/// with `-cpu qemu64`, every descriptor as a boot of this kernel printed it. The +/// last is QEMU's HyperTransport reservation below 1 TiB. +const EDK2_Q35_AMD: [MemoryMapEntry; 127] = [ + e(7, 0x0, 0x87000), + e(4, 0x87000, 0x88000), + e(7, 0x88000, 0xa0000), + e(7, 0x100000, 0x800000), + e(10, 0x800000, 0x808000), + e(7, 0x808000, 0x80b000), + e(10, 0x80b000, 0x80c000), + e(7, 0x80c000, 0x811000), + e(10, 0x811000, 0x900000), + e(4, 0x900000, 0x1780000), + e(7, 0x1780000, 0x8000000), + e(2, 0x8000000, 0x8004000), + e(7, 0x8004000, 0x7753c000), + e(2, 0x7753c000, 0x7bb3c000), + e(4, 0x7bb3c000, 0x7bb5c000), + e(7, 0x7bb5c000, 0x7bc1b000), + e(2, 0x7bc1b000, 0x7cd93000), + e(1, 0x7cd93000, 0x7cdea000), + e(7, 0x7cdea000, 0x7ce11000), + e(2, 0x7ce11000, 0x7ce14000), + e(7, 0x7ce14000, 0x7ce15000), + e(2, 0x7ce15000, 0x7ce24000), + e(4, 0x7ce24000, 0x7ce25000), + e(2, 0x7ce25000, 0x7ce27000), + e(4, 0x7ce27000, 0x7e180000), + e(2, 0x7e180000, 0x7e183000), + e(4, 0x7e183000, 0x7e189000), + e(2, 0x7e189000, 0x7e18a000), + e(4, 0x7e18a000, 0x7e2b5000), + e(3, 0x7e2b5000, 0x7e35c000), + e(4, 0x7e35c000, 0x7e3b4000), + e(3, 0x7e3b4000, 0x7e4a7000), + e(4, 0x7e4a7000, 0x7e4cc000), + e(3, 0x7e4cc000, 0x7e4e1000), + e(4, 0x7e4e1000, 0x7e4e5000), + e(3, 0x7e4e5000, 0x7e50e000), + e(4, 0x7e50e000, 0x7e515000), + e(3, 0x7e515000, 0x7e524000), + e(4, 0x7e524000, 0x7e526000), + e(3, 0x7e526000, 0x7e54a000), + e(4, 0x7e54a000, 0x7e54b000), + e(3, 0x7e54b000, 0x7e55f000), + e(4, 0x7e55f000, 0x7e561000), + e(3, 0x7e561000, 0x7e567000), + e(4, 0x7e567000, 0x7e56d000), + e(3, 0x7e56d000, 0x7e57a000), + e(4, 0x7e57a000, 0x7e587000), + e(3, 0x7e587000, 0x7e5b7000), + e(4, 0x7e5b7000, 0x7e5cb000), + e(3, 0x7e5cb000, 0x7e5e1000), + e(4, 0x7e5e1000, 0x7e5e4000), + e(3, 0x7e5e4000, 0x7e602000), + e(4, 0x7e602000, 0x7e604000), + e(3, 0x7e604000, 0x7e62d000), + e(4, 0x7e62d000, 0x7e638000), + e(3, 0x7e638000, 0x7e647000), + e(4, 0x7e647000, 0x7e64f000), + e(3, 0x7e64f000, 0x7e6a0000), + e(4, 0x7e6a0000, 0x7e6a6000), + e(3, 0x7e6a6000, 0x7e6b0000), + e(4, 0x7e6b0000, 0x7e6b2000), + e(3, 0x7e6b2000, 0x7e6bf000), + e(4, 0x7e6bf000, 0x7e6c1000), + e(3, 0x7e6c1000, 0x7e6e6000), + e(4, 0x7e6e6000, 0x7e6e8000), + e(3, 0x7e6e8000, 0x7e6eb000), + e(4, 0x7e6eb000, 0x7e6ee000), + e(3, 0x7e6ee000, 0x7e707000), + e(4, 0x7e707000, 0x7e70c000), + e(3, 0x7e70c000, 0x7e735000), + e(4, 0x7e735000, 0x7e736000), + e(3, 0x7e736000, 0x7e760000), + e(4, 0x7e760000, 0x7e762000), + e(3, 0x7e762000, 0x7e766000), + e(4, 0x7e766000, 0x7e76a000), + e(3, 0x7e76a000, 0x7e775000), + e(4, 0x7e775000, 0x7e778000), + e(3, 0x7e778000, 0x7e77c000), + e(4, 0x7e77c000, 0x7e780000), + e(3, 0x7e780000, 0x7e786000), + e(4, 0x7e786000, 0x7e78d000), + e(3, 0x7e78d000, 0x7e7e7000), + e(4, 0x7e7e7000, 0x7e7ec000), + e(3, 0x7e7ec000, 0x7e7ef000), + e(4, 0x7e7ef000, 0x7e7f4000), + e(3, 0x7e7f4000, 0x7e7fa000), + e(4, 0x7e7fa000, 0x7ea03000), + e(3, 0x7ea03000, 0x7ea06000), + e(4, 0x7ea06000, 0x7ea09000), + e(3, 0x7ea09000, 0x7ea2e000), + e(6, 0x7ea2e000, 0x7eaef000), + e(3, 0x7eaef000, 0x7eb17000), + e(4, 0x7eb17000, 0x7eb1e000), + e(3, 0x7eb1e000, 0x7eb2d000), + e(4, 0x7eb2d000, 0x7eb57000), + e(3, 0x7eb57000, 0x7eb74000), + e(4, 0x7eb74000, 0x7eb77000), + e(3, 0x7eb77000, 0x7eb7a000), + e(4, 0x7eb7a000, 0x7eb7d000), + e(3, 0x7eb7d000, 0x7eb86000), + e(4, 0x7eb86000, 0x7eb89000), + e(3, 0x7eb89000, 0x7eb8c000), + e(4, 0x7eb8c000, 0x7eb8f000), + e(3, 0x7eb8f000, 0x7eb90000), + e(4, 0x7eb90000, 0x7ef91000), + e(3, 0x7ef91000, 0x7efa2000), + e(4, 0x7efa2000, 0x7efa6000), + e(3, 0x7efa6000, 0x7efbd000), + e(4, 0x7efbd000, 0x7f4ed000), + e(6, 0x7f4ed000, 0x7f5ed000), + e(5, 0x7f5ed000, 0x7f6ed000), + e(0, 0x7f6ed000, 0x7f76d000), + e(9, 0x7f76d000, 0x7f77f000), + e(10, 0x7f77f000, 0x7f7ff000), + e(4, 0x7f7ff000, 0x7fe00000), + e(7, 0x7fe00000, 0x7fe16000), + e(4, 0x7fe16000, 0x7fe36000), + e(3, 0x7fe36000, 0x7fe80000), + e(0, 0x7fe80000, 0x7fe84000), + e(10, 0x7fe84000, 0x7fe86000), + e(3, 0x7fe86000, 0x7fe87000), + e(4, 0x7fe87000, 0x7feb0000), + e(3, 0x7feb0000, 0x7fedc000), + e(6, 0x7fedc000, 0x7ff60000), + e(10, 0x7ff60000, 0x80000000), + e(0, 0xe0000000, 0xf0000000), + e(0, 0xfd00000000, 0x10000000000), +]; + +#[test] +fn the_reserved_hole_below_1_tib_is_not_mapped() { + assert_eq!( + direct_map_end(&EDK2_Q35_AMD), + Ok(BOOT_MAP_BYTES), + "the direct map reaches past the boot map for a range the map calls reserved" + ); +} + +#[test] +fn an_unsorted_map_is_read_whole() { + let map = [ + e(RESERVED, 0xfd_0000_0000, 0x100_0000_0000), + e(CONVENTIONAL, 4 * GIB, 8 * GIB), + e(CONVENTIONAL, 0x10_0000, 2 * GIB), + ]; + assert_eq!(direct_map_end(&map), Ok(8 * GIB), "the highest memory is neither first nor last"); +} + +#[test] +fn memory_above_4_gib_is_mapped_to_its_end_in_whole_pages() { + let map = [ + e(CONVENTIONAL, 0x10_0000, 0x8000_0000), + e(CONVENTIONAL, 4 * GIB, 6 * GIB), + e(EFI_LOADER_DATA, 6 * GIB, 6 * GIB + 0x1000), + ]; + assert_eq!(direct_map_end(&map), Ok(6 * GIB + PAGE_2M)); +} + +#[test] +fn acpi_tables_above_the_last_ram_are_mapped() { + for ty in [ACPI_RECLAIM, ACPI_NVS] { + let map = [e(CONVENTIONAL, 0, 2 * GIB), e(ty, 8 * GIB, 8 * GIB + 0x3000)]; + assert_eq!(direct_map_end(&map), Ok(8 * GIB + PAGE_2M), "type {ty}"); + } +} + +/// UEFI §7.2's table: loader code and data, boot services code and data, and +/// conventional memory are the OS's after `ExitBootServices`, and nothing else is. +#[test] +fn the_pmm_hands_out_exactly_the_types_uefi_gives_the_os() { + let usable: Vec = (0..=255) + .chain([0x7000_0000, 0x7fff_ffff, 0x8000_0000, u32::MAX]) + .filter(|&ty| is_usable_type(ty)) + .collect(); + assert_eq!(usable, [1, 2, 3, 4, 7]); +} + +/// That boot's pmm counted 2140844032 usable bytes in 112 entries. +#[test] +fn the_captured_map_is_usable_where_its_boot_said() { + let usable: Vec<&MemoryMapEntry> = EDK2_Q35_AMD.iter().filter(|e| is_usable_type(e.uefi_type)).collect(); + assert_eq!(usable.len(), 112); + assert_eq!(usable.iter().map(|e| e.end - e.start).sum::(), 2_140_844_032); +} + +/// The containment the pmm rests on: it touches every frame it hands out +/// through the direct map. +#[test] +fn every_type_the_pmm_hands_out_is_mapped() { + for ty in (0..=255).filter(|&ty| is_usable_type(ty)) { + let map = [e(ty, 8 * GIB, 8 * GIB + PAGE_2M)]; + assert_eq!(direct_map_end(&map), Ok(8 * GIB + PAGE_2M), "type {ty}"); + } +} + +#[test] +fn no_other_type_reaches_past_the_boot_map() { + let others = [0, 5, 6, 8, 11, 12, 13, 14, 15, 0x7000_0000, 0x8000_0000, u32::MAX]; + for ty in others { + let map = [e(ty, 8 * GIB, 16 * GIB)]; + assert_eq!(direct_map_end(&map), Ok(BOOT_MAP_BYTES), "type {ty:#x}"); + } +} + +#[test] +fn an_empty_map_is_the_boot_map() { + assert_eq!(direct_map_end(&[]), Ok(BOOT_MAP_BYTES)); +} + +#[test] +fn memory_past_the_window_is_refused_by_name() { + let map = [e(CONVENTIONAL, DIRECT_MAP_WINDOW - PAGE_2M, DIRECT_MAP_WINDOW + 1)]; + assert_eq!( + direct_map_end(&map), + Err(Refusal::PastWindow(DIRECT_MAP_WINDOW + 1)), + "memory one byte past the window" + ); + let map = [e(CONVENTIONAL, DIRECT_MAP_WINDOW - PAGE_2M, DIRECT_MAP_WINDOW)]; + assert_eq!(direct_map_end(&map), Ok(DIRECT_MAP_WINDOW), "memory that ends at the window"); + for ty in (0..=255).filter(|&ty| is_usable_type(ty)) { + let map = [e(CONVENTIONAL, 0x10_0000, 2 * GIB), e(ty, 4 * GIB, u64::MAX)]; + assert_eq!(direct_map_end(&map), Err(Refusal::PastWindow(u64::MAX)), "type {ty}"); + } +} + +#[test] +fn a_range_past_the_window_that_is_not_memory_is_not_refused() { + let map = [e(CONVENTIONAL, 0x10_0000, 2 * GIB), e(RESERVED, 0, u64::MAX)]; + assert_eq!(direct_map_end(&map), Ok(BOOT_MAP_BYTES)); +} diff --git a/toyos-pci/src/bar.rs b/toyos-pci/src/bar.rs index 9f78f1c8a6f..00af5970d09 100644 --- a/toyos-pci/src/bar.rs +++ b/toyos-pci/src/bar.rs @@ -430,8 +430,7 @@ mod tests { #[test] fn a_placement_moves_the_address_and_keeps_the_devices_own_bits() { // The virtio NIC this machine has: BAR 4, 64-bit prefetchable, 16 KiB - // at 0x800000000, moved to the first 2 MiB window above what firmware - // assigned. + // at 0x800000000. let placed = placement(4, 0x0000_000C, 0x8_0020_0000, 0x4000).unwrap(); assert_eq!(placed.low, 0x0020_000C, "the low four bits are the device's"); assert_eq!(placed.high, Some(8)); diff --git a/toyos-pci/src/bridge.rs b/toyos-pci/src/bridge.rs index 15c21841c28..b6d4e240eec 100644 --- a/toyos-pci/src/bridge.rs +++ b/toyos-pci/src/bridge.rs @@ -4,9 +4,9 @@ //! **What a bridge forwards, nothing above it may hand out.** An address inside //! a bridge's window is routed to that bridge's secondary bus and answered by //! whatever is on it — or by nothing, which reads as ones and is not -//! distinguishable from unrouted space. So a module placing a window below -//! 4 GiB has to know these ranges before it can call any address free, and they -//! are readable from config space alone: no interpreter, no table. +//! distinguishable from unrouted space. So a module placing a window has to know +//! these ranges before it can call any address free, and they are readable from +//! config space alone: no interpreter, no table. //! //! The registers hold address bits 31:20 in their top twelve bits and hardwire //! the rest, so every window is a whole number of megabytes and a *limit* names @@ -40,28 +40,12 @@ pub struct Window { /// address; the low four are the type field for a prefetchable window and are /// reserved for a non-prefetchable one, and neither is part of the range. pub fn window(pair: u32) -> Option { - // The twelve bits at 15:4 of each half *are* address bits 31:20, so each - // half moves left by sixteen and not by the four its own field is offset by. - let base = u64::from(pair & 0xFFF0) << 16; - let limit = u64::from((pair >> 16) & 0xFFF0) << 16; - // A base above its limit is the encoding for a bridge that forwards - // nothing, and it is what firmware writes into a window it did not need — - // read as a range it would be `0x00100000..0x0`, which wraps. - if base > limit { - return None; - } - // The limit names the last megabyte, not the first free one. - Some(Window { start: base, end: limit + GRANULE }) + forwarded(pair, 0, 0) } /// Whether a prefetchable window's registers name a 64-bit range, in which case /// the upper dwords at 0x28 and 0x2C carry the rest of it. -/// -/// Answered rather than decoded, because a module that hands out only 32-bit -/// space needs to know that a window it read the low half of may reach far -/// above what it can see — and treating that as a 32-bit range would call -/// addresses free that the bridge forwards. -pub fn prefetch_is_64_bit(pair: u32) -> bool { +fn prefetch_is_64_bit(pair: u32) -> bool { pair & 0xF == 1 } @@ -70,20 +54,36 @@ pub fn prefetch_is_64_bit(pair: u32) -> bool { pub const PREFETCH_BASE_UPPER: u64 = 0x28; pub const PREFETCH_LIMIT_UPPER: u64 = 0x2C; -/// The part of a prefetchable window that lies below 4 GiB, or `None` where -/// none of it does. +/// The whole range a prefetchable window forwards, or `None` where it forwards +/// nothing. /// -/// **A survey of the low space may not count a window that is not in it.** A -/// 64-bit prefetchable window whose upper base is set begins above 4 GiB -/// entirely, and reading its low half as a range would call a megabyte of the -/// low space forwarded that no bridge forwards. Where the upper base is zero -/// the low half *is* the low part of the range, whatever the upper limit adds -/// above it. -pub fn prefetch_below_4g(pair: u32, base_upper: u32) -> Option { - if prefetch_is_64_bit(pair) && base_upper != 0 { +/// **A 64-bit window's upper dwords are half its address**: read without them +/// it is a megabyte of the low space no bridge forwards, and the range above +/// 4 GiB it does forward is called free. +pub fn prefetch(pair: u32, base_upper: u32, limit_upper: u32) -> Option { + // A 32-bit window's upper dwords are hardwired to zero and say nothing; a + // machine that answers otherwise must not move the window over it. + if !prefetch_is_64_bit(pair) { + return window(pair); + } + forwarded(pair, base_upper, limit_upper) +} + +fn forwarded(pair: u32, base_upper: u32, limit_upper: u32) -> Option { + // The twelve bits at 15:4 of each half *are* address bits 31:20, so each + // half moves left by sixteen and not by the four its own field is offset by. + let base = (u64::from(base_upper) << 32) | (u64::from(pair & 0xFFF0) << 16); + let limit = (u64::from(limit_upper) << 32) | (u64::from((pair >> 16) & 0xFFF0) << 16); + // A base above its limit is the encoding for a bridge that forwards + // nothing, and it is what firmware writes into a window it did not need — + // read as a range it would be `0x00100000..0x0`, which wraps. + if base > limit { return None; } - window(pair) + // The limit names the last megabyte, not the first free one. Saturating, + // because the one byte it drops is in no run: `placement::free_runs` stops + // below `u64::MAX`. + Some(Window { start: base, end: limit.saturating_add(GRANULE) }) } #[cfg(test)] @@ -132,21 +132,33 @@ mod tests { assert!(!prefetch_is_64_bit(0xbc30_bc20)); } - /// A 64-bit prefetchable window that starts above 4 GiB is not a low range, - /// and its low half is not one either. + /// **A 64-bit prefetchable window is its upper dwords too**, from a range + /// wholly above 4 GiB to one that crosses it; a 32-bit one is not moved by + /// upper dwords a machine answers anything in. #[test] - fn a_prefetchable_window_above_four_gigabytes_is_not_low_space() { - assert_eq!(prefetch_below_4g(0xbc31_bc21, 0x60), None); + fn a_64_bit_prefetchable_window_is_its_upper_dwords_too() { assert_eq!( - prefetch_below_4g(0xbc31_bc21, 0), + prefetch(0xbc31_bc21, 0x60, 0x60), + Some(Window { start: 0x60_bc20_0000, end: 0x60_bc40_0000 }) + ); + assert_eq!( + prefetch(0xfff1_c001, 0, 1), + Some(Window { start: 0xc000_0000, end: 0x2_0000_0000 }) + ); + assert_eq!( + prefetch(0xbc31_bc21, 0, 0), Some(Window { start: 0xbc20_0000, end: 0xbc40_0000 }) ); - // A 32-bit window's upper dwords are hardwired to zero and say nothing; - // a machine that answers otherwise must not lose the window over it. + // Disabled is decided on the whole address, not on its low half. + assert_eq!(prefetch(0xbc31_bc21, 0x61, 0x60), None); + assert_eq!( + prefetch(0xfff1_fff1, u32::MAX, u32::MAX), + Some(Window { start: 0xffff_ffff_fff0_0000, end: u64::MAX }) + ); assert_eq!( - prefetch_below_4g(0xbc30_bc20, 0x60), + prefetch(0xbc30_bc20, 0x60, 0x60), Some(Window { start: 0xbc20_0000, end: 0xbc40_0000 }) ); - assert_eq!(prefetch_below_4g(0x0000_0010, 0), None); + assert_eq!(prefetch(0x0000_0010, 0, 0), None); } } diff --git a/toyos-pci/src/placement.rs b/toyos-pci/src/placement.rs index 6aa4e071c58..07d2c35f8ba 100644 --- a/toyos-pci/src/placement.rs +++ b/toyos-pci/src/placement.rs @@ -12,6 +12,45 @@ use toyos_abi::boot::RootBridgeWindow; use crate::bridge::Window; +/// Where the fixed platform devices start on a PC: the I/O APIC, the HPET and +/// the LAPIC window are at and above this, so a 32-bit run may not reach it. +pub const PLATFORM_MMIO: u64 = 0xFEC0_0000; + +/// Where a 64-bit BAR's runs start. **Never below**, because the 32-bit runs +/// are there, and two lists holding one address would hand it out twice. +pub const WIDE_FLOOR: u64 = 1 << 32; + +/// What `taken` leaves free, lowest first: below [`PLATFORM_MMIO`] for a 32-bit +/// BAR, from [`WIDE_FLOOR`] up for a 64-bit one. +/// +/// `taken` is every extent the caller read something to decode — the firmware +/// memory map, the BARs firmware assigned, the ranges bridges forward — in any +/// order, overlapping as they may; it is sorted here. **A run is a gap between +/// them and never merely what lies above the highest**: firmware describes +/// extents outside every window it declared, and one of those says nothing +/// about the space free inside a window below it. +pub fn free_runs(taken: &mut [Window], wide: bool) -> impl Iterator + '_ { + let (floor, ceiling) = if wide { (WIDE_FLOOR, u64::MAX) } else { (0, PLATFORM_MMIO) }; + taken.sort_unstable_by_key(|extent| extent.start); + let mut at = floor; + let mut rest = taken.iter(); + core::iter::from_fn(move || { + while at < ceiling { + let Some(next) = rest.next() else { + let run = Window { start: at, end: ceiling }; + at = ceiling; + return Some(run); + }; + let run = Window { start: at, end: next.start.min(ceiling) }; + at = at.max(next.end); + if run.start < run.end { + return Some(run); + } + } + None + }) +} + /// One address a `span`-byte window may take, and the base of the root bridge /// window firmware declared it inside. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -100,6 +139,123 @@ mod tests { const MIB: u64 = 1024 * 1024; + /// QEMU q35 booted on its stock edk2 with 2 GiB: the extents its firmware + /// map covers, then its memory BARs. + const EDK2_TAKEN: [Window; 10] = [ + Window { start: 0, end: 0xa_0000 }, + Window { start: 0x10_0000, end: 0x8000_0000 }, + Window { start: 0xe000_0000, end: 0xf000_0000 }, + Window { start: 0xfd_0000_0000, end: 0x100_0000_0000 }, + Window { start: 0x8000_0000, end: 0x8104_0000 }, + Window { start: 0x8104_0000, end: 0x8104_1000 }, + Window { start: 0x8104_1000, end: 0x8104_2000 }, + Window { start: 0x8104_2000, end: 0x8104_3000 }, + Window { start: 0xc0_0000_0000, end: 0xc0_0000_4000 }, + Window { start: 0xc0_0000_4000, end: 0xc0_0000_8000 }, + ]; + /// The four windows that boot's firmware declared, in the order it did. + const EDK2_WINDOWS: [RootBridgeWindow; 4] = [ + RootBridgeWindow { base: 0x8000_0000, length: 0x110_0000 }, + RootBridgeWindow { base: 0xc0_0000_0000, length: 0x10_0000 }, + RootBridgeWindow { base: 0x8110_0000, length: 0x5ef0_0000 }, + RootBridgeWindow { base: 0xc0_0010_0000, length: 0x1f_fff0_0000 }, + ]; + + fn runs(taken: &[Window], wide: bool) -> std::vec::Vec { + let mut taken = taken.to_vec(); + free_runs(&mut taken, wide).collect() + } + + /// **edk2 offers a 64-bit BAR the window it declared for one.** The + /// reserved hole at 1 TiB is the highest extent it describes and lies + /// outside every window, so the space above it is inside none, and a run + /// found there alone offers the virtio NIC's BAR 4 no address. + #[test] + fn edk2_offers_a_64_bit_bar_the_window_it_declared() { + let mut high = runs(&EDK2_TAKEN, true); + assert_eq!( + reserve(&mut high, &EDK2_WINDOWS, 2 * MIB), + Some(Reservation { at: 0xc0_0020_0000, window: 0xc0_0010_0000, run: 1 }) + ); + } + + /// The 32-bit runs of 2 MiB or more are the two that boot printed. + #[test] + fn edk2s_32_bit_runs_are_the_ones_its_boot_printed() { + let low: std::vec::Vec = runs(&EDK2_TAKEN, false) + .into_iter() + .filter(|run| run.end - run.start >= 2 * MIB) + .collect(); + assert_eq!( + low, + [ + Window { start: 0x8104_3000, end: 0xe000_0000 }, + Window { start: 0xf000_0000, end: PLATFORM_MMIO }, + ] + ); + } + + /// The 64-bit runs are the three that boot printed. + #[test] + fn edk2s_64_bit_runs_are_the_ones_its_boot_printed() { + assert_eq!( + runs(&EDK2_TAKEN, true), + [ + Window { start: 0x1_0000_0000, end: 0xc0_0000_0000 }, + Window { start: 0xc0_0000_8000, end: 0xfd_0000_0000 }, + Window { start: 0x100_0000_0000, end: u64::MAX }, + ] + ); + } + + /// **An extent inside another opens no run inside it**: a bridge's + /// forwarded range holds the BARs behind it, and a map descriptor may hold + /// BARs. + #[test] + fn an_extent_inside_another_opens_no_run() { + const GIB: u64 = 1 << 30; + let nested = [ + Window { start: 5 * GIB, end: 8 * GIB }, + Window { start: 6 * GIB, end: 6 * GIB + 0x4000 }, + ]; + assert_eq!( + runs(&nested, true), + [Window { start: 4 * GIB, end: 5 * GIB }, Window { start: 8 * GIB, end: u64::MAX }] + ); + } + + /// No address is in both lists, none is in either twice, and none is + /// taken. + #[test] + fn a_run_is_free_and_in_one_list_once() { + let low = runs(&EDK2_TAKEN, false); + let high = runs(&EDK2_TAKEN, true); + assert!(low.iter().all(|l| high.iter().all(|h| l.end <= h.start || h.end <= l.start))); + for list in [&low, &high] { + assert!(list.windows(2).all(|pair| pair[0].end < pair[1].start)); + assert!(list.iter().all(|run| { + run.start < run.end + && EDK2_TAKEN.iter().all(|t| t.end <= run.start || t.start >= run.end) + })); + } + } + + /// Order and overlap in `taken` change nothing, and nothing taken leaves + /// each width its whole space. + #[test] + fn taken_is_read_in_any_order_and_overlapping() { + let mut shuffled = EDK2_TAKEN.to_vec(); + shuffled.reverse(); + shuffled.push(Window { start: 0xc0_0000_2000, end: 0xc0_0000_8000 }); + assert_eq!(runs(&shuffled, true), runs(&EDK2_TAKEN, true)); + assert_eq!(runs(&shuffled, false), runs(&EDK2_TAKEN, false)); + assert_eq!(runs(&[], false), [Window { start: 0, end: PLATFORM_MMIO }]); + assert_eq!(runs(&[], true), [Window { start: WIDE_FLOOR, end: u64::MAX }]); + let everything = [Window { start: 0, end: u64::MAX }]; + assert_eq!(runs(&everything, false), []); + assert_eq!(runs(&everything, true), []); + } + /// Every address `runs` hands out before it is empty, in order. fn drain( runs: &mut [Window],