From 492c2cbac972dfa445a80a7021b4cff5c75223c2 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 23:37:12 +0200 Subject: [PATCH 1/8] The direct map reaches memory, not every range the firmware map names Under the edk2 firmware Homebrew's QEMU 11.1.1 ships, the kernel died right after `pmm:` with `memory allocation of 4096 bytes failed`. `paging::init` mapped physical memory up to the highest `end` of every descriptor in the UEFI map, whatever its type, and took the page tables from the 512 KiB early bump heap (128 pages). QEMU gives an AMD vCPU (`-cpu qemu64`) with 40 physical address bits a reserved e820 range for the HyperTransport hole, 0xfd00000000..0x10000000000, and edk2 carries it into the UEFI map as EfiReservedMemoryType. The loader's `GCD:` line shows edk2 saw it: its 64-bit PCI window sits at 0xc000000000..0xe000000000, below that reservation, where the repo's `ovmf/` puts its own at 0x800000000. Mapping to 1 TiB takes 1024 page directories, eight times the early heap, so a 4096-byte table is the allocation that fails. The repo's `ovmf/` never names the range, so it boots. The UEFI specification's usage table after ExitBootServices says a reserved range is not usable and an MMIO range is not used by the OS; a conforming map may still put either anywhere. So the extent is now one rule in a new pure crate, `toyos-memmap::direct_map_end`: the low 4 GiB, and above it the end of the highest range the kernel reads as memory (what the pmm hands out, and ACPI reclaim and NVS), rounded up to 2 MiB. MMIO past it is mapped on demand by `map_mmio`, as every 64-bit BAR already was. The pmm's usable-type rule moves into the same crate, so the one list of UEFI types holds both answers and a host test holds the containment the pmm rests on: every type it hands out is inside the direct map. `paging::init` logs the extent it built. Filed: issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md, the kernel root entries `map_mmio` can add after a user space copied them. This change leaves it as it was. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 7 ++ Cargo.toml | 1 + ...-a-user-space-exists-is-missing-from-it.md | 31 +++++++ kernel/Cargo.lock | 8 ++ kernel/Cargo.toml | 1 + kernel/src/arch/x86_64/paging.rs | 21 ++--- kernel/src/drivers/panic_console/mod.rs | 2 +- kernel/src/mm/pmm.rs | 16 +--- kernel/src/rootfs.rs | 2 +- toyos-memmap/Cargo.toml | 16 ++++ toyos-memmap/src/lib.rs | 62 ++++++++++++++ toyos-memmap/tests/direct_map.rs | 84 +++++++++++++++++++ 12 files changed, 220 insertions(+), 31 deletions(-) create mode 100644 issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md create mode 100644 toyos-memmap/Cargo.toml create mode 100644 toyos-memmap/src/lib.rs create mode 100644 toyos-memmap/tests/direct_map.rs diff --git a/Cargo.lock b/Cargo.lock index 3d51a3aa6a..89cc40cb29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1349,6 +1349,13 @@ dependencies = [ "toyos-dns", ] +[[package]] +name = "toyos-memmap" +version = "0.1.0" +dependencies = [ + "toyos-abi", +] + [[package]] name = "toyos-mixer" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e7de9c08ab..416f9acbc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "toyos-logstream", "toyos-manifest", "toyos-mdns", + "toyos-memmap", "toyos-mixer", "toyos-net-wire", "toyos-pci", diff --git a/issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md b/issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md new file mode 100644 index 0000000000..42917e47e7 --- /dev/null +++ b/issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md @@ -0,0 +1,31 @@ +--- +status: open +kind: defect +opened: 2026-09-27 +--- + +# A kernel mapping made after a user space exists is missing from that space + +`AddressSpace::new_user` (`kernel/src/arch/x86_64/paging.rs`) copies the kernel +root's present entries 256..512 once, at creation. `paging::init` creates only +the entries the direct map reaches (`toyos_memmap::direct_map_end`: the low +4 GiB and the memory above it), and `map_mmio` creates any other through +`ensure_table`. An entry created after a user space was made is absent from +that space, so a kernel access through it under that space's CR3 is a +not-present fault in Ring 0. + +`pcidev::place_bar` reaches it: a claim is a syscall on the claimant's CR3, +and `probe_dword` maps and reads the BAR where firmware put it. Firmware puts a +64-bit BAR in its 64-bit window. Nothing orders that window below 512 GiB: +under Homebrew QEMU 11.1.1's edk2 with `-cpu qemu64` the window is +`0xc000100000+0x1ffff00000` (the loader's `GCD:` line), which is root entry +257. The repository's `ovmf/` puts the kernel's own xHCI BAR in its 64-bit +window too (`xHCI: BAR0=0x800004000`); if edk2 does the same, the kernel maps +entry 257 at boot, before any user space, and hides this. Unmeasured: no boot +here has printed edk2's BARs. A machine with no kernel-driven function in that +entry faults on netd's claim. `probe_dword`'s comment says "the boot +map already covers every physical address", which is false. + +**Exit condition**: every kernel root entry a physical address this CPU can +name reaches through the direct map exists before the first user space copies +them, and a later install of one is a named panic. diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index e6d0370abc..39e3e62e31 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -48,6 +48,7 @@ dependencies = [ "toyos-fat32", "toyos-gpt", "toyos-hda", + "toyos-memmap", "toyos-pci", "toyos-pcid", "toyos-proclife", @@ -125,6 +126,13 @@ version = "0.1.0" name = "toyos-hda" version = "0.1.0" +[[package]] +name = "toyos-memmap" +version = "0.1.0" +dependencies = [ + "toyos-abi", +] + [[package]] name = "toyos-pci" version = "0.1.0" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 70caaf2665..fc8fc9bef1 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -404,6 +404,7 @@ toyos-fat32 = { path = "../toyos-fat32" } toyos-elf = { path = "../toyos-elf" } toyos-gpt = { path = "../toyos-gpt" } toyos-hda = { path = "../toyos-hda" } +toyos-memmap = { path = "../toyos-memmap" } toyos-pci = { path = "../toyos-pci" } toyos-pcid = { path = "../toyos-pcid" } toyos-tco = { path = "../toyos-tco" } diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs index d9f00b95bd..79eab50a7a 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -783,9 +783,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; @@ -841,8 +840,6 @@ impl AddressSpace { } } -const MIN_PHYS_MAP: u64 = 4 * 1024 * 1024 * 1024; - /// `Arc>`, not `Lock>`: a kernel thread names it /// as `KernelPayload.address_space` with no second answer. Leaked, since the /// kernel address space outlives every task by construction. @@ -910,15 +907,10 @@ 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. +/// Build kernel page tables: the direct map in the high half, in 2 MiB pages, +/// as far as [`toyos_memmap::direct_map_end`] reaches. 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); + let end = toyos_memmap::direct_map_end(memory_map); let mut kernel = AddressSpace { root: Box::new(PageTablePage([0; 512])), @@ -929,10 +921,11 @@ pub(crate) fn init(memory_map: &[MemoryMapEntry]) { }; 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); diff --git a/kernel/src/drivers/panic_console/mod.rs b/kernel/src/drivers/panic_console/mod.rs index 71ed2178f6..a5ffdc72dc 100644 --- a/kernel/src/drivers/panic_console/mod.rs +++ b/kernel/src/drivers/panic_console/mod.rs @@ -399,7 +399,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_memmap::is_usable_type(entry.uefi_type) { return Some(entry.uefi_type); } i += 1; diff --git a/kernel/src/mm/pmm.rs b/kernel/src/mm/pmm.rs index 7a93ddb17f..152bc65535 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_memmap::is_usable_type(entry.uefi_type) } fn overlaps_reserved(start: u64, end: u64, reserved: &[Region]) -> bool { diff --git a/kernel/src/rootfs.rs b/kernel/src/rootfs.rs index 1c82fcee30..83339cae33 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_memmap::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-memmap/Cargo.toml b/toyos-memmap/Cargo.toml new file mode 100644 index 0000000000..eefa150597 --- /dev/null +++ b/toyos-memmap/Cargo.toml @@ -0,0 +1,16 @@ +# A member of the host workspace (root `Cargo.toml`), like toyos-rootimage: the +# kernel depends on it by path and its tests run on the host. +# What lives here is what the kernel takes from firmware's memory map by UEFI +# memory type — which frames the pmm hands out and how far the direct map +# reaches — decisions whose only other instrument is a firmware nobody here +# chose. + +[package] +name = "toyos-memmap" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +toyos-abi = { path = "../toyos-abi" } diff --git a/toyos-memmap/src/lib.rs b/toyos-memmap/src/lib.rs new file mode 100644 index 0000000000..f13631c45e --- /dev/null +++ b/toyos-memmap/src/lib.rs @@ -0,0 +1,62 @@ +//! What the kernel takes from firmware's memory map, by UEFI memory type, pure. +//! +//! The types are `EFI_MEMORY_TYPE`'s, and what an OS may do with each after +//! `ExitBootServices` is the UEFI specification's "Memory Type Usage after +//! ExitBootServices()" table (`EFI_BOOT_SERVICES.AllocatePages()`, §7.2). A +//! conforming map may describe any address, memory or not: a range the table +//! calls "not usable" or "not used by the OS" is still in the map, anywhere in +//! the physical address space. + +#![no_std] +#![forbid(unsafe_code)] + +use toyos_abi::boot::MemoryMapEntry; + +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; + +/// The direct map's leaf. +pub const PAGE_2M: u64 = 2 * 1024 * 1024; + +/// The low 4 GiB, mapped whatever the map says: the platform's registers and +/// firmware's own tables are there, and neither is described as memory. +pub const DIRECT_MAP_FLOOR: u64 = 4 * 1024 * 1024 * 1024; + +/// 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 + ) +} + +/// 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. +/// +/// Every other type is left out, a type this list does not know included: +/// reserved and I/O ranges are nothing a write-back mapping may cover, unusable +/// memory is memory with errors, and runtime services code, persistent and +/// unaccepted memory are nothing this kernel reads. +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 past the kernel direct map's last byte: [`DIRECT_MAP_FLOOR`], or the +/// end of the highest range the kernel reads as memory in whole [`PAGE_2M`] +/// pages, whichever is higher. +pub fn direct_map_end(map: &[MemoryMapEntry]) -> u64 { + map.iter() + .filter(|entry| is_read_as_memory(entry.uefi_type)) + .map(|entry| entry.end.saturating_add(PAGE_2M - 1) & !(PAGE_2M - 1)) + .fold(DIRECT_MAP_FLOOR, u64::max) +} diff --git a/toyos-memmap/tests/direct_map.rs b/toyos-memmap/tests/direct_map.rs new file mode 100644 index 0000000000..03ff516300 --- /dev/null +++ b/toyos-memmap/tests/direct_map.rs @@ -0,0 +1,84 @@ +//! 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_memmap::{direct_map_end, is_usable_type, DIRECT_MAP_FLOOR, EFI_LOADER_DATA, PAGE_2M}; + +const RESERVED: u32 = 0; +const CONVENTIONAL: u32 = 7; +const ACPI_RECLAIM: u32 = 9; +const ACPI_NVS: u32 = 10; +const MMIO: u32 = 11; + +const GIB: u64 = 1 << 30; + +const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { + MemoryMapEntry { uefi_type, start, end } +} + +/// The map edk2 hands a 2 GiB q35 guest with an AMD vCPU of 40 physical +/// address bits: the kernel's image and the RSDP where that boot put them, the +/// flash as runtime MMIO, and QEMU's HyperTransport reservation below 1 TiB as +/// reserved memory. +const EDK2_Q35_AMD: [MemoryMapEntry; 9] = [ + e(CONVENTIONAL, 0, 0xa_0000), + e(CONVENTIONAL, 0x10_0000, 0x7be0_0000), + e(EFI_LOADER_DATA, 0x7be0_0000, 0x7ca2_c000), + e(CONVENTIONAL, 0x7ca2_c000, 0x7f77_e000), + e(ACPI_RECLAIM, 0x7f77_e000, 0x7f77_f000), + e(ACPI_NVS, 0x7f77_f000, 0x7f80_0000), + e(RESERVED, 0x7f80_0000, 0x8000_0000), + e(MMIO, 0xffc0_0000, 0x1_0000_0000), + e(RESERVED, 0xfd_0000_0000, 0x100_0000_0000), +]; + +#[test] +fn the_reserved_hole_below_1_tib_is_not_mapped() { + assert_eq!( + direct_map_end(&EDK2_Q35_AMD), + DIRECT_MAP_FLOOR, + "the direct map reaches past the low 4 GiB for a range the map calls reserved" + ); +} + +#[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), 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), 8 * GIB + PAGE_2M, "type {ty}"); + } +} + +/// 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..=15).filter(|&ty| is_usable_type(ty)) { + let map = [e(ty, 8 * GIB, 8 * GIB + PAGE_2M)]; + assert_eq!(direct_map_end(&map), 8 * GIB + PAGE_2M, "type {ty}"); + } +} + +#[test] +fn no_other_type_reaches_past_the_floor() { + 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), DIRECT_MAP_FLOOR, "type {ty:#x}"); + } +} + +#[test] +fn an_empty_map_is_the_floor() { + assert_eq!(direct_map_end(&[]), DIRECT_MAP_FLOOR); +} From bbac417a9bfb13e5e572ce0285d0b1f8e022d85a Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 00:02:20 +0200 Subject: [PATCH 2/8] The direct map's rule lives in toyos-bootmap; the ACPI reader and every kernel root slot follow it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toyos-memmap is folded into toyos-bootmap: the kernel's direct map may never reach less than the boot map, and two crates with two constants could not hold that. `x86_64::direct_map_end` floors at `BOOT_MAP_BYTES`, sits behind the architecture boundary because mapping the low 4 GiB whole is safe only where the MTRRs type it, and refuses by name memory past `DIRECT_MAP_WINDOW` (root slots 256..511, 128 TiB) rather than saturating to an end below the range it was asked to cover. `DirectPhys::readable` is bounded by the direct map's extent through `toyos_bootmap::reaches`: the boot map's until `paging::init` stores its own, instead of x86-64's 52-bit width. `MAX_PHYS` goes from the ACPI reader and `the-direct-map-bound-on-a-firmware-address-is-52-bits` is closed. `paging::init` installs the root slots its direct map needs, and `seal_kernel_half` installs the rest once the heap is the pmm's; a user space asserts every kernel slot is present when it copies them, and `ensure_table` never creates one. That closes `a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it`. Tests: an unsorted map with a reserved entry first gives 8 GiB, memory ending at u64::MAX is refused by name, the usable set is exactly UEFI §7.2's {1,2,3,4,7}, and an address past the direct map is not read. The fixture no longer claims to be edk2's map. Filed: the early heap's ceiling on the direct map, map_mmio's unbounded address, and the missing edk2 boot in the harness. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 10 +- Cargo.toml | 1 - bootloader/Cargo.lock | 3 + bootloader/src/watchdog.rs | 3 +- .../no-harness-test-boots-qemus-own-edk2.md | 18 +++ ...-a-user-space-exists-is-missing-from-it.md | 31 ---- ...s-an-address-past-the-direct-map-window.md | 22 +++ ...-bound-on-a-firmware-address-is-52-bits.md | 26 ---- ...ge-directories-come-from-a-512-kib-heap.md | 23 +++ kernel/Cargo.lock | 11 +- kernel/Cargo.toml | 1 - kernel/src/arch/aarch64/paging.rs | 6 +- kernel/src/arch/x86_64/paging.rs | 54 ++++++- kernel/src/drivers/acpi.rs | 13 +- kernel/src/drivers/panic_console/mod.rs | 2 +- kernel/src/mm/mod.rs | 17 ++- kernel/src/mm/pmm.rs | 2 +- kernel/src/pcidev/mod.rs | 5 - kernel/src/rootfs.rs | 2 +- toyos-bootmap/Cargo.toml | 11 +- toyos-bootmap/src/lib.rs | 55 +++++++ toyos-bootmap/src/x86_64.rs | 23 ++- toyos-bootmap/tests/direct_map.rs | 144 ++++++++++++++++++ toyos-memmap/Cargo.toml | 16 -- toyos-memmap/src/lib.rs | 62 -------- toyos-memmap/tests/direct_map.rs | 84 ---------- 26 files changed, 376 insertions(+), 269 deletions(-) create mode 100644 issues/build/no-harness-test-boots-qemus-own-edk2.md delete mode 100644 issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md create mode 100644 issues/kernel/map-mmio-takes-an-address-past-the-direct-map-window.md delete mode 100644 issues/kernel/the-direct-map-bound-on-a-firmware-address-is-52-bits.md create mode 100644 issues/kernel/the-direct-maps-page-directories-come-from-a-512-kib-heap.md create mode 100644 toyos-bootmap/tests/direct_map.rs delete mode 100644 toyos-memmap/Cargo.toml delete mode 100644 toyos-memmap/src/lib.rs delete mode 100644 toyos-memmap/tests/direct_map.rs diff --git a/Cargo.lock b/Cargo.lock index 89cc40cb29..2881621da1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1195,6 +1195,9 @@ dependencies = [ [[package]] name = "toyos-bootmap" version = "0.1.0" +dependencies = [ + "toyos-abi", +] [[package]] name = "toyos-build" @@ -1349,13 +1352,6 @@ dependencies = [ "toyos-dns", ] -[[package]] -name = "toyos-memmap" -version = "0.1.0" -dependencies = [ - "toyos-abi", -] - [[package]] name = "toyos-mixer" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 416f9acbc4..e7de9c08ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,6 @@ members = [ "toyos-logstream", "toyos-manifest", "toyos-mdns", - "toyos-memmap", "toyos-mixer", "toyos-net-wire", "toyos-pci", diff --git a/bootloader/Cargo.lock b/bootloader/Cargo.lock index f7dfd89b21..09cb3f6226 100644 --- a/bootloader/Cargo.lock +++ b/bootloader/Cargo.lock @@ -273,6 +273,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 798a0f15b1..4440cfd59b 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 0000000000..89f8ae360e --- /dev/null +++ b/issues/build/no-harness-test-boots-qemus-own-edk2.md @@ -0,0 +1,18 @@ +--- +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:`. The host +test `toyos-bootmap/tests/direct_map.rs` holds the rule; nothing boots the +firmware that broke it, so a regression outside that rule 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`. diff --git a/issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md b/issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md deleted file mode 100644 index 42917e47e7..0000000000 --- a/issues/kernel/a-kernel-mapping-made-after-a-user-space-exists-is-missing-from-it.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A kernel mapping made after a user space exists is missing from that space - -`AddressSpace::new_user` (`kernel/src/arch/x86_64/paging.rs`) copies the kernel -root's present entries 256..512 once, at creation. `paging::init` creates only -the entries the direct map reaches (`toyos_memmap::direct_map_end`: the low -4 GiB and the memory above it), and `map_mmio` creates any other through -`ensure_table`. An entry created after a user space was made is absent from -that space, so a kernel access through it under that space's CR3 is a -not-present fault in Ring 0. - -`pcidev::place_bar` reaches it: a claim is a syscall on the claimant's CR3, -and `probe_dword` maps and reads the BAR where firmware put it. Firmware puts a -64-bit BAR in its 64-bit window. Nothing orders that window below 512 GiB: -under Homebrew QEMU 11.1.1's edk2 with `-cpu qemu64` the window is -`0xc000100000+0x1ffff00000` (the loader's `GCD:` line), which is root entry -257. The repository's `ovmf/` puts the kernel's own xHCI BAR in its 64-bit -window too (`xHCI: BAR0=0x800004000`); if edk2 does the same, the kernel maps -entry 257 at boot, before any user space, and hides this. Unmeasured: no boot -here has printed edk2's BARs. A machine with no kernel-driven function in that -entry faults on netd's claim. `probe_dword`'s comment says "the boot -map already covers every physical address", which is false. - -**Exit condition**: every kernel root entry a physical address this CPU can -name reaches through the direct map exists before the first user space copies -them, and a later install of one is a named panic. 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 0000000000..99b8891817 --- /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/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 56bfccc4b8..0000000000 --- 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 0000000000..02552f4662 --- /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/kernel/Cargo.lock b/kernel/Cargo.lock index 39e3e62e31..69f958fcab 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -48,7 +48,6 @@ dependencies = [ "toyos-fat32", "toyos-gpt", "toyos-hda", - "toyos-memmap", "toyos-pci", "toyos-pcid", "toyos-proclife", @@ -98,6 +97,9 @@ version = "0.1.0" [[package]] name = "toyos-bootmap" version = "0.1.0" +dependencies = [ + "toyos-abi", +] [[package]] name = "toyos-dma" @@ -126,13 +128,6 @@ version = "0.1.0" name = "toyos-hda" version = "0.1.0" -[[package]] -name = "toyos-memmap" -version = "0.1.0" -dependencies = [ - "toyos-abi", -] - [[package]] name = "toyos-pci" version = "0.1.0" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index fc8fc9bef1..70caaf2665 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -404,7 +404,6 @@ toyos-fat32 = { path = "../toyos-fat32" } toyos-elf = { path = "../toyos-elf" } toyos-gpt = { path = "../toyos-gpt" } toyos-hda = { path = "../toyos-hda" } -toyos-memmap = { path = "../toyos-memmap" } toyos-pci = { path = "../toyos-pci" } toyos-pcid = { path = "../toyos-pcid" } toyos-tco = { path = "../toyos-tco" } diff --git a/kernel/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs index b287a4449c..4e9b87c687 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]) -> u64 { + 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 79eab50a7a..6d22a1f184 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 { @@ -811,6 +816,11 @@ impl AddressSpace { let target = self.root(); if self.root[pml4_idx] & PAGE_PRESENT == 0 { + assert!( + pml4_idx < ROOT_HIGH_HALF, + "ensure_table: kernel root slot {pml4_idx} is absent at {va:#x}, and a user space \ + copies the kernel's slots once: `init` and `seal_kernel_half` install every one" + ); let child = Box::new(PageTablePage([0; 512])); self.root .write(pml4_idx, va, child.phys() | flags) @@ -838,6 +848,16 @@ impl AddressSpace { // SAFETY: same argument as `pdpt` above, one level down. unsafe { PageTablePage::from_phys_mut(pdpt[pdpt_idx] & ADDR_MASK) } } + + /// 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 @@ -908,9 +928,11 @@ pub fn guard_kernel_page(addr: u64) { } /// Build kernel page tables: the direct map in the high half, in 2 MiB pages, -/// as far as [`toyos_memmap::direct_map_end`] reaches. -pub(crate) fn init(memory_map: &[MemoryMapEntry]) { - let end = toyos_memmap::direct_map_end(memory_map); +/// as far as [`toyos_bootmap::x86_64::direct_map_end`] reaches, and answer +/// that end. +pub(crate) fn init(memory_map: &[MemoryMapEntry]) -> u64 { + let end = toyos_bootmap::x86_64::direct_map_end(memory_map) + .unwrap_or_else(|refusal| panic!("paging: firmware's memory map: {refusal}")); let mut kernel = AddressSpace { root: Box::new(PageTablePage([0; 512])), @@ -920,6 +942,11 @@ 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 < end { kernel.map_2m(addr, PAGE_PRESENT | PAGE_WRITE); @@ -943,6 +970,19 @@ pub(crate) fn init(memory_map: &[MemoryMapEntry]) { unsafe { cr3.load_flush(); } + end +} + +/// 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 7c4f04417a..24c76d66f9 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -37,21 +37,18 @@ 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. +/// Firmware's physical addresses, read through the direct map; one it does not +/// reach is refused, never read. #[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) + phys != 0 && DirectMap::reaches(phys, len as u64) } fn byte(self, phys: u64) -> u8 { - // SAFETY: `readable` bounded `phys` below `MAX_PHYS`. + // SAFETY: `readable` put `phys` inside the direct map. unsafe { read_volatile(DirectMap::from_phys(phys).as_ptr::()) } } } @@ -83,7 +80,7 @@ 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::()) }) } } diff --git a/kernel/src/drivers/panic_console/mod.rs b/kernel/src/drivers/panic_console/mod.rs index a5ffdc72dc..954a57f1a0 100644 --- a/kernel/src/drivers/panic_console/mod.rs +++ b/kernel/src/drivers/panic_console/mod.rs @@ -399,7 +399,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 && toyos_memmap::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/mm/mod.rs b/kernel/src/mm/mod.rs index e278f39223..47094934a5 100644 --- a/kernel/src/mm/mod.rs +++ b/kernel/src/mm/mod.rs @@ -107,6 +107,11 @@ 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: core::sync::atomic::AtomicU64 = + core::sync::atomic::AtomicU64::new(toyos_bootmap::BOOT_MAP_BYTES); + /// 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); @@ -129,6 +134,11 @@ impl DirectMap { pub fn phys_of(ptr: *const T) -> u64 { ptr as u64 - PHYS_OFFSET } + + /// Whether every byte of `phys..phys + len` lies inside the direct map. + pub fn reaches(phys: u64, len: u64) -> bool { + toyos_bootmap::reaches(DIRECT_MAP_END.load(core::sync::atomic::Ordering::Acquire), phys, len) + } } impl core::fmt::Display for DirectMap { @@ -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.store(paging::init(memory_map), core::sync::atomic::Ordering::Release); 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 152bc65535..fe23dfc66f 100644 --- a/kernel/src/mm/pmm.rs +++ b/kernel/src/mm/pmm.rs @@ -389,7 +389,7 @@ pub fn stats() -> (u64, u64) { } fn is_usable(entry: &MemoryMapEntry) -> bool { - toyos_memmap::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 498c99ffa3..c9afb1b209 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -1284,11 +1284,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 83339cae33..507fb6c5be 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, toyos_memmap::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-bootmap/Cargo.toml b/toyos-bootmap/Cargo.toml index 98c8f99ae7..e7044d2099 100644 --- a/toyos-bootmap/Cargo.toml +++ b/toyos-bootmap/Cargo.toml @@ -1,8 +1,10 @@ # 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. +# toyos-tco: the bootloader and the kernel depend 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. +# and its scanout, and how far the kernel's own direct map reaches — decisions +# with no instrument on the machine that gets them wrong, since a boot that +# mislays the panel says nothing about why. [package] name = "toyos-bootmap" @@ -10,3 +12,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 1ab42f24e5..8270933758 100644 --- a/toyos-bootmap/src/lib.rs +++ b/toyos-bootmap/src/lib.rs @@ -15,6 +15,15 @@ //! 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`], [`reaches`]). 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)] @@ -45,6 +54,46 @@ 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; + +/// Whether every byte of `phys..phys + len` lies inside a direct map of +/// `0..end`. +pub const fn reaches(end: u64, phys: u64, len: u64) -> bool { + match phys.checked_add(len) { + Some(last) => last <= end, + None => false, + } +} + +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 +135,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 +160,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 6c138526df..3511b5eb6d 100644 --- a/toyos-bootmap/src/x86_64.rs +++ b/toyos-bootmap/src/x86_64.rs @@ -1,8 +1,27 @@ //! 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, 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))) + }) +} 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 0000000000..e71da1eaac --- /dev/null +++ b/toyos-bootmap/tests/direct_map.rs @@ -0,0 +1,144 @@ +//! 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; and what a +//! firmware address the kernel reads through it must lie inside. + +use toyos_abi::boot::MemoryMapEntry; +use toyos_bootmap::x86_64::direct_map_end; +use toyos_bootmap::{ + is_usable_type, reaches, 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 MMIO: u32 = 11; + +const GIB: u64 = 1 << 30; + +const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { + MemoryMapEntry { uefi_type, start, end } +} + +/// 2 GiB of memory, and a reserved range just below 1 TiB. +const RESERVED_HOLE: [MemoryMapEntry; 9] = [ + e(CONVENTIONAL, 0, 0xa_0000), + e(CONVENTIONAL, 0x10_0000, 0x7be0_0000), + e(EFI_LOADER_DATA, 0x7be0_0000, 0x7ca2_c000), + e(CONVENTIONAL, 0x7ca2_c000, 0x7f77_e000), + e(ACPI_RECLAIM, 0x7f77_e000, 0x7f77_f000), + e(ACPI_NVS, 0x7f77_f000, 0x7f80_0000), + e(RESERVED, 0x7f80_0000, 0x8000_0000), + e(MMIO, 0xffc0_0000, 0x1_0000_0000), + e(RESERVED, 0xfd_0000_0000, 0x100_0000_0000), +]; + +#[test] +fn the_reserved_hole_below_1_tib_is_not_mapped() { + assert_eq!( + direct_map_end(&RESERVED_HOLE), + 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]); +} + +/// 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)); +} + +/// Root slots 256 to 511 at `PHYS_OFFSET`: everything from there to the top of +/// the address space. +#[test] +fn the_window_is_what_phys_offset_leaves() { + const PHYS_OFFSET: u64 = 0xFFFF_8000_0000_0000; + assert_eq!(DIRECT_MAP_WINDOW, 0u64.wrapping_sub(PHYS_OFFSET)); +} + +#[test] +fn memory_past_the_window_is_refused_by_name() { + 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}"); + } + 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))); + let map = [e(CONVENTIONAL, DIRECT_MAP_WINDOW - PAGE_2M, DIRECT_MAP_WINDOW)]; + assert_eq!(direct_map_end(&map), Ok(DIRECT_MAP_WINDOW)); +} + +#[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)); +} + +/// What the ACPI reader asks of a firmware address: the direct map's extent, +/// not the architecture's physical-address width. +#[test] +fn a_firmware_address_past_the_direct_map_is_not_read() { + let end = direct_map_end(&RESERVED_HOLE).unwrap(); + assert!(!reaches(end, 0xfd_0000_0000, 36), "a table in the reserved hole"); + assert!(!reaches(BOOT_MAP_BYTES, 1 << 40, 1), "before the kernel's own map, past the boot map"); + assert!(reaches(end, end - 36, 36)); + assert!(!reaches(end, end - 35, 36), "a table whose last byte is past the map"); + assert!(!reaches(end, u64::MAX, 2), "a range whose end does not fit an address"); +} diff --git a/toyos-memmap/Cargo.toml b/toyos-memmap/Cargo.toml deleted file mode 100644 index eefa150597..0000000000 --- a/toyos-memmap/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-rootimage: the -# kernel depends on it by path and its tests run on the host. -# What lives here is what the kernel takes from firmware's memory map by UEFI -# memory type — which frames the pmm hands out and how far the direct map -# reaches — decisions whose only other instrument is a firmware nobody here -# chose. - -[package] -name = "toyos-memmap" -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" -publish = false - -[dependencies] -toyos-abi = { path = "../toyos-abi" } diff --git a/toyos-memmap/src/lib.rs b/toyos-memmap/src/lib.rs deleted file mode 100644 index f13631c45e..0000000000 --- a/toyos-memmap/src/lib.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! What the kernel takes from firmware's memory map, by UEFI memory type, pure. -//! -//! The types are `EFI_MEMORY_TYPE`'s, and what an OS may do with each after -//! `ExitBootServices` is the UEFI specification's "Memory Type Usage after -//! ExitBootServices()" table (`EFI_BOOT_SERVICES.AllocatePages()`, §7.2). A -//! conforming map may describe any address, memory or not: a range the table -//! calls "not usable" or "not used by the OS" is still in the map, anywhere in -//! the physical address space. - -#![no_std] -#![forbid(unsafe_code)] - -use toyos_abi::boot::MemoryMapEntry; - -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; - -/// The direct map's leaf. -pub const PAGE_2M: u64 = 2 * 1024 * 1024; - -/// The low 4 GiB, mapped whatever the map says: the platform's registers and -/// firmware's own tables are there, and neither is described as memory. -pub const DIRECT_MAP_FLOOR: u64 = 4 * 1024 * 1024 * 1024; - -/// 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 - ) -} - -/// 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. -/// -/// Every other type is left out, a type this list does not know included: -/// reserved and I/O ranges are nothing a write-back mapping may cover, unusable -/// memory is memory with errors, and runtime services code, persistent and -/// unaccepted memory are nothing this kernel reads. -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 past the kernel direct map's last byte: [`DIRECT_MAP_FLOOR`], or the -/// end of the highest range the kernel reads as memory in whole [`PAGE_2M`] -/// pages, whichever is higher. -pub fn direct_map_end(map: &[MemoryMapEntry]) -> u64 { - map.iter() - .filter(|entry| is_read_as_memory(entry.uefi_type)) - .map(|entry| entry.end.saturating_add(PAGE_2M - 1) & !(PAGE_2M - 1)) - .fold(DIRECT_MAP_FLOOR, u64::max) -} diff --git a/toyos-memmap/tests/direct_map.rs b/toyos-memmap/tests/direct_map.rs deleted file mode 100644 index 03ff516300..0000000000 --- a/toyos-memmap/tests/direct_map.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! 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_memmap::{direct_map_end, is_usable_type, DIRECT_MAP_FLOOR, EFI_LOADER_DATA, PAGE_2M}; - -const RESERVED: u32 = 0; -const CONVENTIONAL: u32 = 7; -const ACPI_RECLAIM: u32 = 9; -const ACPI_NVS: u32 = 10; -const MMIO: u32 = 11; - -const GIB: u64 = 1 << 30; - -const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { - MemoryMapEntry { uefi_type, start, end } -} - -/// The map edk2 hands a 2 GiB q35 guest with an AMD vCPU of 40 physical -/// address bits: the kernel's image and the RSDP where that boot put them, the -/// flash as runtime MMIO, and QEMU's HyperTransport reservation below 1 TiB as -/// reserved memory. -const EDK2_Q35_AMD: [MemoryMapEntry; 9] = [ - e(CONVENTIONAL, 0, 0xa_0000), - e(CONVENTIONAL, 0x10_0000, 0x7be0_0000), - e(EFI_LOADER_DATA, 0x7be0_0000, 0x7ca2_c000), - e(CONVENTIONAL, 0x7ca2_c000, 0x7f77_e000), - e(ACPI_RECLAIM, 0x7f77_e000, 0x7f77_f000), - e(ACPI_NVS, 0x7f77_f000, 0x7f80_0000), - e(RESERVED, 0x7f80_0000, 0x8000_0000), - e(MMIO, 0xffc0_0000, 0x1_0000_0000), - e(RESERVED, 0xfd_0000_0000, 0x100_0000_0000), -]; - -#[test] -fn the_reserved_hole_below_1_tib_is_not_mapped() { - assert_eq!( - direct_map_end(&EDK2_Q35_AMD), - DIRECT_MAP_FLOOR, - "the direct map reaches past the low 4 GiB for a range the map calls reserved" - ); -} - -#[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), 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), 8 * GIB + PAGE_2M, "type {ty}"); - } -} - -/// 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..=15).filter(|&ty| is_usable_type(ty)) { - let map = [e(ty, 8 * GIB, 8 * GIB + PAGE_2M)]; - assert_eq!(direct_map_end(&map), 8 * GIB + PAGE_2M, "type {ty}"); - } -} - -#[test] -fn no_other_type_reaches_past_the_floor() { - 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), DIRECT_MAP_FLOOR, "type {ty:#x}"); - } -} - -#[test] -fn an_empty_map_is_the_floor() { - assert_eq!(direct_map_end(&[]), DIRECT_MAP_FLOOR); -} From 50caa3764ae55c8d8102a1877efebe229714ee9f Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 00:04:24 +0200 Subject: [PATCH 3/8] toyos-bootmap: the window test asserts before any arithmetic can overflow The case one byte past the window runs first, so a direct_map_end with no window check fails on the assertion that names it, whatever the profile's overflow checks. Co-Authored-By: Claude Opus 5.5 --- toyos-bootmap/tests/direct_map.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/toyos-bootmap/tests/direct_map.rs b/toyos-bootmap/tests/direct_map.rs index e71da1eaac..fff015ebd9 100644 --- a/toyos-bootmap/tests/direct_map.rs +++ b/toyos-bootmap/tests/direct_map.rs @@ -115,14 +115,18 @@ fn the_window_is_what_phys_offset_leaves() { #[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}"); } - 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))); - let map = [e(CONVENTIONAL, DIRECT_MAP_WINDOW - PAGE_2M, DIRECT_MAP_WINDOW)]; - assert_eq!(direct_map_end(&map), Ok(DIRECT_MAP_WINDOW)); } #[test] From 07bc8789721c741699b29d3dd27ca2a992dbd79c Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 00:06:29 +0200 Subject: [PATCH 4/8] acpi: the firmware reader carries the direct map's extent it was made under `DirectPhys` holds the end `mm::direct_map_end` answered when it was made, and `readable` bounds by it through `toyos_bootmap::reaches`. A `readable` that stops consulting the extent leaves the field unread, which the kernel's clippy gate denies. Co-Authored-By: Claude Opus 5.5 --- kernel/src/arch/aarch64/boot.rs | 4 ++-- kernel/src/arch/aarch64/console_uart.rs | 2 +- kernel/src/drivers/acpi.rs | 30 ++++++++++++++++--------- kernel/src/mm/mod.rs | 8 +++---- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index 37fb11c9ea..e83fa70619 100644 --- a/kernel/src/arch/aarch64/boot.rs +++ b/kernel/src/arch/aarch64/boot.rs @@ -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(DirectPhys::now(), 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(DirectPhys::now(), 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 2b30bad860..0ca9be84a7 100644 --- a/kernel/src/arch/aarch64/console_uart.rs +++ b/kernel/src/arch/aarch64/console_uart.rs @@ -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(DirectPhys::now(), rsdp_addr) { Ok(spcr) => spcr, Err(e) => { log!("serial: no console UART, because the SPCR is unusable: {e:?}"); diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index 24c76d66f9..a95c8e27cf 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -37,14 +37,22 @@ pub struct MadtInfo { pub source_overrides: Vec, } -/// Firmware's physical addresses, read through the direct map; one it does not -/// reach is refused, never read. +/// Firmware's physical addresses, read through the direct map as far as it +/// reached when this reader was made; one past that is refused, never read. #[derive(Clone, Copy)] -pub struct DirectPhys; +pub struct DirectPhys { + end: u64, +} + +impl DirectPhys { + pub fn now() -> Self { + Self { end: crate::mm::direct_map_end() } + } +} impl Phys for DirectPhys { fn readable(self, phys: u64, len: usize) -> bool { - phys != 0 && DirectMap::reaches(phys, len as u64) + phys != 0 && toyos_bootmap::reaches(self.end, phys, len as u64) } fn byte(self, phys: u64) -> u8 { @@ -59,7 +67,7 @@ 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(DirectPhys::now(), base, signature, needed).map(Table) } /// The declared length, already bounded by [`Table::open`]. @@ -87,7 +95,7 @@ impl Table { /// 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(DirectPhys::now(), 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. @@ -148,7 +156,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(DirectPhys::now(), rsdp_addr) { Ok(found) => found, Err(e) => return refuse("MCFG", e), }; @@ -223,13 +231,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(DirectPhys::now(), 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(DirectPhys::now(), 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 }; @@ -352,7 +360,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(DirectPhys::now(), rsdp_addr) { Ok(base) => base, Err(e) => return refuse("HPET", e), }; @@ -362,7 +370,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(DirectPhys::now(), rsdp_addr, b"APIC", MADT_ENTRIES) { Ok(table) => table, Err(e) => return refuse("MADT", e), }; diff --git a/kernel/src/mm/mod.rs b/kernel/src/mm/mod.rs index 47094934a5..83af53b706 100644 --- a/kernel/src/mm/mod.rs +++ b/kernel/src/mm/mod.rs @@ -134,11 +134,11 @@ impl DirectMap { pub fn phys_of(ptr: *const T) -> u64 { ptr as u64 - PHYS_OFFSET } +} - /// Whether every byte of `phys..phys + len` lies inside the direct map. - pub fn reaches(phys: u64, len: u64) -> bool { - toyos_bootmap::reaches(DIRECT_MAP_END.load(core::sync::atomic::Ordering::Acquire), phys, len) - } +/// One past the direct map's last byte now. +pub fn direct_map_end() -> u64 { + DIRECT_MAP_END.load(core::sync::atomic::Ordering::Acquire) } impl core::fmt::Display for DirectMap { From 9e49e08e1c6ede221f8d7250d722959e8bebc743 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 00:10:14 +0200 Subject: [PATCH 5/8] toyos-bootmap: the edk2 fixture is the map a boot printed The 127 descriptors QEMU 11.1.1's edk2 handed a 2 GiB q35 guest with `-cpu qemu64`, as the kernel printed them (diag boot, edk2-r1/diag.log). The last is type 0 at 0xfd00000000..0x10000000000, the HyperTransport reservation the reconstructed fixture assumed. The fixture's usable bytes and entries are what that boot's pmm counted, and the RSDP and the five tables the boot read are inside the direct map the fixture gives. Co-Authored-By: Claude Opus 5.5 --- toyos-bootmap/tests/direct_map.rs | 166 +++++++++++++++++++++++++++--- 1 file changed, 152 insertions(+), 14 deletions(-) diff --git a/toyos-bootmap/tests/direct_map.rs b/toyos-bootmap/tests/direct_map.rs index fff015ebd9..02052ddb91 100644 --- a/toyos-bootmap/tests/direct_map.rs +++ b/toyos-bootmap/tests/direct_map.rs @@ -12,7 +12,6 @@ const RESERVED: u32 = 0; const CONVENTIONAL: u32 = 7; const ACPI_RECLAIM: u32 = 9; const ACPI_NVS: u32 = 10; -const MMIO: u32 = 11; const GIB: u64 = 1 << 30; @@ -20,23 +19,143 @@ const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { MemoryMapEntry { uefi_type, start, end } } -/// 2 GiB of memory, and a reserved range just below 1 TiB. -const RESERVED_HOLE: [MemoryMapEntry; 9] = [ - e(CONVENTIONAL, 0, 0xa_0000), - e(CONVENTIONAL, 0x10_0000, 0x7be0_0000), - e(EFI_LOADER_DATA, 0x7be0_0000, 0x7ca2_c000), - e(CONVENTIONAL, 0x7ca2_c000, 0x7f77_e000), - e(ACPI_RECLAIM, 0x7f77_e000, 0x7f77_f000), - e(ACPI_NVS, 0x7f77_f000, 0x7f80_0000), - e(RESERVED, 0x7f80_0000, 0x8000_0000), - e(MMIO, 0xffc0_0000, 0x1_0000_0000), - e(RESERVED, 0xfd_0000_0000, 0x100_0000_0000), +/// 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(&RESERVED_HOLE), + 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" ); @@ -81,6 +200,14 @@ fn the_pmm_hands_out_exactly_the_types_uefi_gives_the_os() { 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] @@ -139,7 +266,18 @@ fn a_range_past_the_window_that_is_not_memory_is_not_refused() { /// not the architecture's physical-address width. #[test] fn a_firmware_address_past_the_direct_map_is_not_read() { - let end = direct_map_end(&RESERVED_HOLE).unwrap(); + let end = direct_map_end(&EDK2_Q35_AMD).unwrap(); + // The RSDP and the five tables the same boot read, where it found them. + for (at, len) in [ + (0x7f77e014, 36), + (0x7f778000, 144), + (0x7f779000, 244), + (0x7f777000, 56), + (0x7f776000, 60), + (0x7f775000, 128), + ] { + assert!(reaches(end, at, len), "{at:#x}+{len}"); + } assert!(!reaches(end, 0xfd_0000_0000, 36), "a table in the reserved hole"); assert!(!reaches(BOOT_MAP_BYTES, 1 << 40, 1), "before the kernel's own map, past the boot map"); assert!(reaches(end, end - 36, 36)); From 3f1edf10fe9c8e3fe29baec585739ceb377731d2 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 00:31:59 +0200 Subject: [PATCH 6/8] pcidev: a free run is a gap inside the space of its width, never what lies above the highest extent On QEMU's stock edk2 the virtio NIC was not handed over: the 64-bit run was the 48 MiB above the highest extent firmware described, and that extent is the reserved range 0xfd00000000..0x10000000000, outside every window firmware declared, so the run at 0x10000000000 was inside none and BAR 4 had no address to be placed at. The window firmware declared for 64-bit BARs, 0xc000100000..0xe000000000, was never looked at. The rule was in pcidev on main and unchanged by this branch; main's kernel died at pmm: on this firmware before reaching it. One rule now makes both lists: toyos_pci::placement::free_runs, the gaps between the firmware map, the assigned BARs and the bridges' forwarded ranges, below PLATFORM_MMIO for a 32-bit BAR and from 4 GiB for a 64-bit one. A gap rule above 4 GiB has to know what bridges forward there, so bridge::prefetch decodes a 64-bit prefetchable window whole, upper dwords included, where prefetch_below_4g dropped it. Co-Authored-By: Claude Opus 5.5 --- .../no-harness-test-boots-qemus-own-edk2.md | 10 +- kernel/src/drivers/pci.rs | 9 +- kernel/src/pcidev/mod.rs | 139 +++++------------- toyos-pci/src/bridge.rs | 94 ++++++------ toyos-pci/src/placement.rs | 129 ++++++++++++++++ 5 files changed, 226 insertions(+), 155 deletions(-) diff --git a/issues/build/no-harness-test-boots-qemus-own-edk2.md b/issues/build/no-harness-test-boots-qemus-own-edk2.md index 89f8ae360e..d71800896a 100644 --- a/issues/build/no-harness-test-boots-qemus-own-edk2.md +++ b/issues/build/no-harness-test-boots-qemus-own-edk2.md @@ -9,10 +9,12 @@ opened: 2026-09-28 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:`. The host -test `toyos-bootmap/tests/direct_map.rs` holds the rule; nothing boots the -firmware that broke it, so a regression outside that rule is seen by the +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`. +the command line the release probe uses and reaches `compositor: ready` and `netd: DHCP: lease`. diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index 0deb9bf9a4..eecf6b74f5 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/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index c9afb1b209..0333b0b624 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' @@ -461,18 +451,15 @@ pub fn note_kernel_driver(pci: &PciDevice) { /// 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`]). +/// The runs are what three things this machine could be asked about leave over +/// ([`placement::free_runs`]), 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 is only where [`place_bar`] *may* ask. 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 +476,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 +556,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 +615,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, diff --git a/toyos-pci/src/bridge.rs b/toyos-pci/src/bridge.rs index 15c21841c2..b6d4e240ee 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 6aa4e071c5..b76f515bb5 100644 --- a/toyos-pci/src/placement.rs +++ b/toyos-pci/src/placement.rs @@ -12,6 +12,46 @@ 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 (q35's edk2 reserves +/// `0xfd00000000..0x10000000000`), 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 +140,95 @@ mod tests { const MIB: u64 = 1024 * 1024; + /// QEMU q35 booted on its stock edk2 with 2 GiB, as that boot printed it: + /// the extents its firmware map covers, then its memory BARs, each running + /// to the next one's address (xHCI's to the end its `mmio:` line names). + 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_0001_4000 }, + ]; + /// 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 }, + ] + ); + } + + /// 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], From 58660e78abbf3af24c7e98f524b9c7ff3f0bb431 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 07:41:59 +0200 Subject: [PATCH 7/8] acpi: the direct map's bound is toyos-acpi's reader, over an end only firmware's map can make The kernel's ACPI reader implemented `Phys::readable` itself, so putting it back to the architecture's 52-bit ceiling passed every gate. The bound now lives in `toyos_acpi::Mapped`, which the kernel wraps its byte read in (`toyos_acpi::Memory`), and `tests/mapped.rs` pins it: a table in edk2's reserved hole at 0xfd00000000, at the map's end, or one byte over it is `Unmapped`; an XSDT entry pointing there is skipped; address zero and a range past every address are refused. `toyos_bootmap::reaches` goes with it. The end it bounds by is `toyos_bootmap::DirectMapEnd`, whose constructor is private (E0603, pinned by a compile_fail doctest): only `x86_64::direct_map_end` and `DirectMapEnd::BOOT` make one, and the kernel keeps it in a `DirectMapEndCell`, which holds no other number. `DIRECT_MAP_WINDOW` is asserted against the kernel's own `PHYS_OFFSET` at compile time instead of against a copy in a test. `ensure_table`'s assert repeated `new_user`'s and goes. pci: a nested extent opens no run inside the one that holds it (`an_extent_inside_another_opens_no_run`), and the edk2 fixture's xHCI extent is its decoded 16 KiB, so the 64-bit runs are asserted as the three that boot printed. Filed: a panic inside `mm::init` may not reach the panel; nothing reds when the BAR survey drops an assigned BAR or a forwarded range. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j --- Cargo.lock | 1 + bootloader/Cargo.lock | 1 + ...en-the-bar-survey-drops-an-assigned-bar.md | 20 +++++ ...-inside-mm-init-may-not-reach-the-panel.md | 24 ++++++ kernel/Cargo.lock | 1 + kernel/src/arch/aarch64/boot.rs | 6 +- kernel/src/arch/aarch64/console_uart.rs | 4 +- kernel/src/arch/aarch64/paging.rs | 2 +- kernel/src/arch/x86_64/paging.rs | 12 +-- kernel/src/drivers/acpi.rs | 43 +++++----- kernel/src/mm/mod.rs | 10 +-- kernel/src/pcidev/mod.rs | 6 -- toyos-acpi/Cargo.toml | 1 + toyos-acpi/src/lib.rs | 37 +++++++++ toyos-acpi/tests/common/mod.rs | 8 +- toyos-acpi/tests/mapped.rs | 78 +++++++++++++++++++ toyos-bootmap/Cargo.toml | 8 -- toyos-bootmap/src/lib.rs | 44 +++++++++-- toyos-bootmap/src/x86_64.rs | 5 +- toyos-bootmap/tests/direct_map.rs | 42 ++-------- toyos-pci/src/bar.rs | 3 +- toyos-pci/src/placement.rs | 41 ++++++++-- 22 files changed, 286 insertions(+), 111 deletions(-) create mode 100644 issues/kernel/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md create mode 100644 issues/panic-path/a-panic-inside-mm-init-may-not-reach-the-panel.md create mode 100644 toyos-acpi/tests/mapped.rs diff --git a/Cargo.lock b/Cargo.lock index 85349a8402..d27c976526 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1174,6 +1174,7 @@ name = "toyos-acpi" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-bootmap", ] [[package]] diff --git a/bootloader/Cargo.lock b/bootloader/Cargo.lock index 09cb3f6226..e342c46279 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]] 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 0000000000..fdb6fa5f2d --- /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/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 0000000000..ce8a376e52 --- /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 69f958fcab..be62dcc5ee 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]] diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index e83fa70619..c3fd3144c8 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::now(), 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::now(), 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 0ca9be84a7..e316a04030 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::now(), 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 4e9b87c687..34f6e5e0ba 100644 --- a/kernel/src/arch/aarch64/paging.rs +++ b/kernel/src/arch/aarch64/paging.rs @@ -141,7 +141,7 @@ 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]) -> u64 { +pub(crate) fn init(_memory_map: &[MemoryMapEntry]) -> toyos_bootmap::DirectMapEnd { 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 6d22a1f184..bebdc87077 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -816,11 +816,6 @@ impl AddressSpace { let target = self.root(); if self.root[pml4_idx] & PAGE_PRESENT == 0 { - assert!( - pml4_idx < ROOT_HIGH_HALF, - "ensure_table: kernel root slot {pml4_idx} is absent at {va:#x}, and a user space \ - copies the kernel's slots once: `init` and `seal_kernel_half` install every one" - ); let child = Box::new(PageTablePage([0; 512])); self.root .write(pml4_idx, va, child.phys() | flags) @@ -930,9 +925,10 @@ pub fn guard_kernel_page(addr: u64) { /// 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]) -> u64 { - let end = toyos_bootmap::x86_64::direct_map_end(memory_map) +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])), @@ -970,7 +966,7 @@ pub(crate) fn init(memory_map: &[MemoryMapEntry]) -> u64 { unsafe { cr3.load_flush(); } - end + extent } /// Install a second-level table under every kernel root slot [`init`] left diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index a95c8e27cf..7ea329f719 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,28 +37,23 @@ pub struct MadtInfo { pub source_overrides: Vec, } -/// Firmware's physical addresses, read through the direct map as far as it -/// reached when this reader was made; one past that is refused, never read. +/// Firmware's physical addresses, read through the direct map. #[derive(Clone, Copy)] -pub struct DirectPhys { - end: u64, -} +pub struct DirectMemory; -impl DirectPhys { - pub fn now() -> Self { - Self { end: crate::mm::direct_map_end() } +impl Memory for DirectMemory { + fn byte(self, phys: u64) -> u8 { + // 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::()) } } } -impl Phys for DirectPhys { - fn readable(self, phys: u64, len: usize) -> bool { - phys != 0 && toyos_bootmap::reaches(self.end, phys, len as u64) - } +type DirectPhys = Mapped; - fn byte(self, phys: u64) -> u8 { - // SAFETY: `readable` put `phys` inside the direct map. - unsafe { read_volatile(DirectMap::from_phys(phys).as_ptr::()) } - } +/// 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. @@ -67,7 +62,7 @@ pub struct Table(toyos_acpi::Table); impl Table { pub fn open(base: u64, signature: &[u8; 4], needed: usize) -> Result { - toyos_acpi::Table::open(DirectPhys::now(), base, signature, needed).map(Table) + toyos_acpi::Table::open(direct_phys(), base, signature, needed).map(Table) } /// The declared length, already bounded by [`Table::open`]. @@ -95,7 +90,7 @@ impl Table { /// 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::now(), 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. @@ -156,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::now(), rsdp_addr) { + let (mcfg, base) = match toyos_acpi::ecam_base(direct_phys(), rsdp_addr) { Ok(found) => found, Err(e) => return refuse("MCFG", e), }; @@ -231,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::now(), 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::now(), 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 }; @@ -360,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::now(), rsdp_addr) { + let base = match toyos_acpi::hpet_base(direct_phys(), rsdp_addr) { Ok(base) => base, Err(e) => return refuse("HPET", e), }; @@ -370,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::now(), 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/mm/mod.rs b/kernel/src/mm/mod.rs index 83af53b706..304e0dd18b 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; @@ -109,8 +110,7 @@ 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: core::sync::atomic::AtomicU64 = - core::sync::atomic::AtomicU64::new(toyos_bootmap::BOOT_MAP_BYTES); +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)] @@ -137,8 +137,8 @@ impl DirectMap { } /// One past the direct map's last byte now. -pub fn direct_map_end() -> u64 { - DIRECT_MAP_END.load(core::sync::atomic::Ordering::Acquire) +pub fn direct_map_end() -> toyos_bootmap::DirectMapEnd { + DIRECT_MAP_END.get() } impl core::fmt::Display for DirectMap { @@ -159,7 +159,7 @@ impl core::fmt::Debug for DirectMap { pub fn init(memory_map: &[MemoryMapEntry], reserved: &[Region]) { alloc::init_early(); pmm::init(memory_map, reserved); - DIRECT_MAP_END.store(paging::init(memory_map), core::sync::atomic::Ordering::Release); + DIRECT_MAP_END.set(paging::init(memory_map)); alloc::init(); paging::seal_kernel_half(); } diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 0333b0b624..c9d3e943c7 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -450,12 +450,6 @@ 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 runs are what three things this machine could be asked about leave over -/// ([`placement::free_runs`]), 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 is only where [`place_bar`] *may* ask. pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry], firmware: &[RootBridgeWindow]) { let mut decoded = Vec::new(); let mut taken: Vec = diff --git a/toyos-acpi/Cargo.toml b/toyos-acpi/Cargo.toml index bf29d25f0c..061351cd2e 100644 --- a/toyos-acpi/Cargo.toml +++ b/toyos-acpi/Cargo.toml @@ -16,3 +16,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 90ad4a8e3b..48edc0db15 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 213e4339f0..428de7c47a 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 0000000000..15b918a6d7 --- /dev/null +++ b/toyos-acpi/tests/mapped.rs @@ -0,0 +1,78 @@ +//! 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::DirectMapEnd; + +/// 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. +const END: u64 = DirectMapEnd::BOOT.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 }, DirectMapEnd::BOOT) +} + +/// The RSDP and the five tables a boot of this kernel on that firmware read, +/// where it found them. +#[test] +fn every_table_edk2_published_is_inside_the_map() { + let m = mapped(&[]); + for (at, len) in [ + (0x7f77e014, 36), + (0x7f778000, 144), + (0x7f779000, 244), + (0x7f777000, 56), + (0x7f776000, 60), + (0x7f775000, 128), + ] { + assert!(m.readable(at, len), "{at:#x}+{len}"); + } +} + +#[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(); + 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 e7044d2099..6d4cc77c7a 100644 --- a/toyos-bootmap/Cargo.toml +++ b/toyos-bootmap/Cargo.toml @@ -1,11 +1,3 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-gpt and -# toyos-tco: the bootloader and the kernel depend 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, and how far the kernel's own direct map reaches — decisions -# with no instrument on the machine that gets them wrong, since a boot that -# mislays the panel says nothing about why. - [package] name = "toyos-bootmap" version = "0.1.0" diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs index 8270933758..37db4dfb87 100644 --- a/toyos-bootmap/src/lib.rs +++ b/toyos-bootmap/src/lib.rs @@ -19,7 +19,7 @@ //! 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`], [`reaches`]). The types are `EFI_MEMORY_TYPE`'s, +//! ([`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 @@ -29,6 +29,7 @@ #![forbid(unsafe_code)] use core::fmt; +use core::sync::atomic::{AtomicU64, Ordering}; pub mod aarch64; pub mod x86_64; @@ -58,12 +59,41 @@ pub const BOOT_MAP_BYTES: u64 = 4 * GIB; /// 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; -/// Whether every byte of `phys..phys + len` lies inside a direct map of -/// `0..end`. -pub const fn reaches(end: u64, phys: u64, len: u64) -> bool { - match phys.checked_add(len) { - Some(last) => last <= end, - None => false, +/// One past the kernel direct map's last byte. Made only here, by +/// [`x86_64::direct_map_end`] or as [`DirectMapEnd::BOOT`], so no reader can +/// be handed a wider map than one that was built. +/// +/// ```compile_fail,E0603 +/// let _ = toyos_bootmap::DirectMapEnd(1 << 52); +/// ``` +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct DirectMapEnd(u64); + +impl DirectMapEnd { + /// The boot map's, which the kernel's own never reaches less than. + pub const BOOT: Self = Self(BOOT_MAP_BYTES); + + 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 [`DirectMapEnd::BOOT`]. + 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)) } } diff --git a/toyos-bootmap/src/x86_64.rs b/toyos-bootmap/src/x86_64.rs index 3511b5eb6d..eb942428f2 100644 --- a/toyos-bootmap/src/x86_64.rs +++ b/toyos-bootmap/src/x86_64.rs @@ -5,7 +5,7 @@ use toyos_abi::boot::MemoryMapEntry; -use crate::{is_read_as_memory, Cache, Refusal, BOOT_MAP_BYTES, DIRECT_MAP_WINDOW, PAGE_2M}; +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, @@ -14,13 +14,14 @@ use crate::{is_read_as_memory, Cache, Refusal, BOOT_MAP_BYTES, DIRECT_MAP_WINDOW /// 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 { +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; diff --git a/toyos-bootmap/tests/direct_map.rs b/toyos-bootmap/tests/direct_map.rs index 02052ddb91..eb0767a8db 100644 --- a/toyos-bootmap/tests/direct_map.rs +++ b/toyos-bootmap/tests/direct_map.rs @@ -1,11 +1,10 @@ //! 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; and what a -//! firmware address the kernel reads through it must lie inside. +//! never to a range the map describes and does not call memory. use toyos_abi::boot::MemoryMapEntry; -use toyos_bootmap::x86_64::direct_map_end; use toyos_bootmap::{ - is_usable_type, reaches, Refusal, BOOT_MAP_BYTES, DIRECT_MAP_WINDOW, EFI_LOADER_DATA, PAGE_2M, + is_usable_type, x86_64, DirectMapEnd, Refusal, BOOT_MAP_BYTES, DIRECT_MAP_WINDOW, + EFI_LOADER_DATA, PAGE_2M, }; const RESERVED: u32 = 0; @@ -15,6 +14,10 @@ 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 } } @@ -232,14 +235,6 @@ fn an_empty_map_is_the_boot_map() { assert_eq!(direct_map_end(&[]), Ok(BOOT_MAP_BYTES)); } -/// Root slots 256 to 511 at `PHYS_OFFSET`: everything from there to the top of -/// the address space. -#[test] -fn the_window_is_what_phys_offset_leaves() { - const PHYS_OFFSET: u64 = 0xFFFF_8000_0000_0000; - assert_eq!(DIRECT_MAP_WINDOW, 0u64.wrapping_sub(PHYS_OFFSET)); -} - #[test] fn memory_past_the_window_is_refused_by_name() { let map = [e(CONVENTIONAL, DIRECT_MAP_WINDOW - PAGE_2M, DIRECT_MAP_WINDOW + 1)]; @@ -261,26 +256,3 @@ 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)); } - -/// What the ACPI reader asks of a firmware address: the direct map's extent, -/// not the architecture's physical-address width. -#[test] -fn a_firmware_address_past_the_direct_map_is_not_read() { - let end = direct_map_end(&EDK2_Q35_AMD).unwrap(); - // The RSDP and the five tables the same boot read, where it found them. - for (at, len) in [ - (0x7f77e014, 36), - (0x7f778000, 144), - (0x7f779000, 244), - (0x7f777000, 56), - (0x7f776000, 60), - (0x7f775000, 128), - ] { - assert!(reaches(end, at, len), "{at:#x}+{len}"); - } - assert!(!reaches(end, 0xfd_0000_0000, 36), "a table in the reserved hole"); - assert!(!reaches(BOOT_MAP_BYTES, 1 << 40, 1), "before the kernel's own map, past the boot map"); - assert!(reaches(end, end - 36, 36)); - assert!(!reaches(end, end - 35, 36), "a table whose last byte is past the map"); - assert!(!reaches(end, u64::MAX, 2), "a range whose end does not fit an address"); -} diff --git a/toyos-pci/src/bar.rs b/toyos-pci/src/bar.rs index 9f78f1c8a6..00af5970d0 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/placement.rs b/toyos-pci/src/placement.rs index b76f515bb5..07d2c35f8b 100644 --- a/toyos-pci/src/placement.rs +++ b/toyos-pci/src/placement.rs @@ -27,9 +27,8 @@ pub const WIDE_FLOOR: u64 = 1 << 32; /// 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 (q35's edk2 reserves -/// `0xfd00000000..0x10000000000`), and one of those says nothing about the -/// space free inside a window below it. +/// 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); @@ -140,9 +139,8 @@ mod tests { const MIB: u64 = 1024 * 1024; - /// QEMU q35 booted on its stock edk2 with 2 GiB, as that boot printed it: - /// the extents its firmware map covers, then its memory BARs, each running - /// to the next one's address (xHCI's to the end its `mmio:` line names). + /// 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 }, @@ -153,7 +151,7 @@ mod tests { 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_0001_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] = [ @@ -197,6 +195,35 @@ mod tests { ); } + /// 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] From 562242b17bcfeedcff80df7935b55e4a4b92e7dd Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 08:39:41 +0200 Subject: [PATCH 8/8] Review r4: DirectMapEnd::BOOT goes since only tests named it, and the test that pinned constants instead of the map goes too `DirectMapEndCell::boot()` already built its own `BOOT_MAP_BYTES`; `DirectMapEnd::BOOT` had no production caller, only `toyos-acpi/tests/mapped.rs`. Its tests now take `x86_64::direct_map_end(&[]).unwrap()`, the same value, from the function that is the type's only real constructor. The unused `PartialEq, Eq, Debug` derives on `DirectMapEnd` go with it. `every_table_edk2_published_is_inside_the_map` asserted six addresses were nonzero and below a constant 4 GiB end; no mutation of `Mapped::readable` reds it without also redding another test in the file, so it tested nothing and is deleted with its doc. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j --- toyos-acpi/tests/mapped.rs | 30 ++++++++---------------------- toyos-bootmap/src/lib.rs | 12 ++++-------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/toyos-acpi/tests/mapped.rs b/toyos-acpi/tests/mapped.rs index 15b918a6d7..2f090601e8 100644 --- a/toyos-acpi/tests/mapped.rs +++ b/toyos-acpi/tests/mapped.rs @@ -6,12 +6,14 @@ mod common; use common::{rsdp, sdt, xsdt, Machine}; use toyos_acpi::{find_table, Mapped, Phys, Table, TableError}; -use toyos_bootmap::DirectMapEnd; +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. -const END: u64 = DirectMapEnd::BOOT.get(); +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; @@ -20,31 +22,15 @@ 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 }, DirectMapEnd::BOOT) -} - -/// The RSDP and the five tables a boot of this kernel on that firmware read, -/// where it found them. -#[test] -fn every_table_edk2_published_is_inside_the_map() { - let m = mapped(&[]); - for (at, len) in [ - (0x7f77e014, 36), - (0x7f778000, 144), - (0x7f779000, 244), - (0x7f777000, 56), - (0x7f776000, 60), - (0x7f775000, 128), - ] { - assert!(m.readable(at, len), "{at:#x}+{len}"); - } + 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(); - for (at, refused) in [(HOLE, 36), (END, 36), (END - len as u64 + 1, 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(), @@ -52,7 +38,7 @@ fn a_table_past_the_direct_map_is_refused_before_a_byte_is_read() { "a table at {at:#x}" ); } - let at = END - len as u64; + 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"); } diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs index 37db4dfb87..ce22f5526a 100644 --- a/toyos-bootmap/src/lib.rs +++ b/toyos-bootmap/src/lib.rs @@ -59,20 +59,16 @@ pub const BOOT_MAP_BYTES: u64 = 4 * GIB; /// 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 here, by -/// [`x86_64::direct_map_end`] or as [`DirectMapEnd::BOOT`], so no reader can -/// be handed a wider map than one that was built. +/// 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, PartialEq, Eq, Debug)] +#[derive(Clone, Copy)] pub struct DirectMapEnd(u64); impl DirectMapEnd { - /// The boot map's, which the kernel's own never reaches less than. - pub const BOOT: Self = Self(BOOT_MAP_BYTES); - pub const fn get(self) -> u64 { self.0 } @@ -83,7 +79,7 @@ impl DirectMapEnd { pub struct DirectMapEndCell(AtomicU64); impl DirectMapEndCell { - /// Holding [`DirectMapEnd::BOOT`]. + /// Holding [`BOOT_MAP_BYTES`], until the kernel's own map decides wider. pub const fn boot() -> Self { Self(AtomicU64::new(BOOT_MAP_BYTES)) }