From b991d36493b36d805d7ba0f562aec70dced53231 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:25:04 +0200 Subject: [PATCH 1/4] The boot map reaches every address the CPU's physical width and the high half hold A machine whose firmware put the GOP framebuffer at 1010 GiB stopped in the loader: the boot map had one second-level table per view, so it reached 512 GiB and refused the scanout as PastPdpt. A second-level table is position-independent, and both views already share every page directory, so the plan now names one second-level table per 512 GiB it reaches, written at root slot ROOT_IDENTITY + r and ROOT_HIGH_HALF + r alike. Their count is derived, not fixed: the low map's, and one per directory past it (MAX_REGIONS), since a directory lies in exactly one 512 GiB. The pool shrinks by the two per-view tables of region 0 merged into one. The reach is the CPU's: the loader reads the physical address width (CPUID.80000008H:EAX[7:0] on x86-64, ID_AA64MMFR0_EL1.PARange on AArch64) and the plan refuses, by name and before it asserts, a scanout or loader byte past 1 << width (PastWidth) or past the 128 TiB the high half holds (PastWindow). A CPU that does not report a width stops the boot with that said on the console first. e64949905's rules stand: no misaligned base is rounded, the scanout stays uncacheable until PAT, and every refusal is printed before it is asserted. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- bootloader/src/arch/aarch64.rs | 11 ++ bootloader/src/arch/x86_64.rs | 11 ++ bootloader/src/main.rs | 29 ++++-- toyos-bootmap/src/aarch64.rs | 16 +++ toyos-bootmap/src/lib.rs | 111 ++++++++++++++++----- toyos-bootmap/tests/plan.rs | 177 ++++++++++++++++++++++++++------- 6 files changed, 282 insertions(+), 73 deletions(-) diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs index 94106d6dfb1..bd445190311 100644 --- a/bootloader/src/arch/aarch64.rs +++ b/bootloader/src/arch/aarch64.rs @@ -15,6 +15,17 @@ pub fn typing(write_back: &[(u64, u64)]) -> Typing<'_> { Typing::ByMap(write_back) } +/// The CPU's physical address width, from `ID_AA64MMFR0_EL1.PARange`, or why +/// it cannot be read. +pub fn physical_bits() -> Result { + let mmfr0: u64; + // SAFETY: reads an ID register EL1 and above may always read. + unsafe { core::arch::asm!("mrs {}, id_aa64mmfr0_el1", out(reg) mmfr0, options(nomem, nostack, preserves_flags)) }; + let parange = mmfr0 & 0xF; + toyos_bootmap::aarch64::physical_bits(parange) + .ok_or_else(|| alloc::format!("ID_AA64MMFR0_EL1.PARange reads {parange:#x}, an encoding the Arm ARM reserves")) +} + /// The exception level the loader runs at, from `CurrentEL`. fn current_el() -> u64 { let current: u64; diff --git a/bootloader/src/arch/x86_64.rs b/bootloader/src/arch/x86_64.rs index 78e2e25c615..380e9b4016a 100644 --- a/bootloader/src/arch/x86_64.rs +++ b/bootloader/src/arch/x86_64.rs @@ -14,6 +14,17 @@ pub fn typing(_write_back: &[(u64, u64)]) -> toyos_bootmap::Typing<'_> { toyos_bootmap::Typing::Firmware } +/// The CPU's physical address width, `CPUID.80000008H:EAX[7:0]` (SDM Vol. 2A, +/// CPUID; AMD APM Vol. 3, E.4.7), or why it cannot be read. +pub fn physical_bits() -> Result { + use core::arch::x86_64::__cpuid; + let max = __cpuid(0x8000_0000).eax; + if max < 0x8000_0008 { + return Err(alloc::format!("CPUID answers extended leaves to {max:#x}, short of 0x80000008's physical address width")); + } + Ok(__cpuid(0x8000_0008).eax & 0xFF) +} + /// The time-stamp counter, which counts from reset. pub fn counter() -> u64 { // SAFETY: RDTSC reads a counter and nothing else; every x86-64 has it. diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index f8c07cfc548..b1ae39feace 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -429,14 +429,18 @@ unsafe fn build_boot_page_tables(pt_mem: *mut u8, plan: &Plan) -> u64 { }; let root = alloc_page(); - let identity_pdpt = alloc_page(); - let high_pdpt = alloc_page(); + let mut pdpts = [core::ptr::null_mut::(); toyos_bootmap::MAX_REGIONS]; + for (at, region) in plan.regions().iter().enumerate() { + let pdpt = alloc_page(); + pdpts[at] = pdpt; + *root.add(ROOT_IDENTITY + *region as usize) = table(pdpt as u64); + *root.add(ROOT_HIGH_HALF + *region as usize) = table(pdpt as u64); + } let mut directories = [core::ptr::null_mut::(); toyos_bootmap::MAX_DIRECTORIES]; - for (slot, gib) in plan.directories().iter().enumerate() { + for (slot, (region, index)) in plan.directory_slots().enumerate() { let pd = alloc_page(); directories[slot] = pd; - *identity_pdpt.add(*gib as usize) = table(pd as u64); - *high_pdpt.add(*gib as usize) = table(pd as u64); + *pdpts[region].add(index) = table(pd as u64); } let mut fine = [core::ptr::null_mut::(); toyos_bootmap::MAX_PAGES]; @@ -455,9 +459,6 @@ unsafe fn build_boot_page_tables(pt_mem: *mut u8, plan: &Plan) -> u64 { } } - *root.add(ROOT_IDENTITY) = table(identity_pdpt as u64); - *root.add(ROOT_HIGH_HALF) = table(high_pdpt as u64); - root as u64 } @@ -540,18 +541,26 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v // pages by it. Before the exit, while the map can still be asked for; the // attributes a descriptor carries do not change across it. let write_back = write_back_memory(&system_table); + // Said before it is refused, for `report_reach`'s reason. + let physical_bits = arch::physical_bits().unwrap_or_else(|why| { + println!("Boot map: NO MAP HOLDS THIS MACHINE, {why}"); + panic!("{why}"); + }); + println!("Boot map: this CPU addresses {physical_bits} bits of physical memory"); let planned = Plan::new( gop.as_ref().map(|g| (g.framebuffer, g.framebuffer_size)), loader, arch::typing(&write_back), + physical_bits, ); match &planned { Ok(plan) => { match plan.scanout() { Some((at, len)) => println!( "Scanout: {at:#x}+{len:#x} mapped as the scanout in 2 MiB pages at identity and at \ - PHYS_OFFSET, in {} page directories", - plan.directories().len() + PHYS_OFFSET, in {} page directories under {} second-level tables", + plan.directories().len(), + plan.regions().len() ), None => println!("Scanout: this machine has none"), } diff --git a/toyos-bootmap/src/aarch64.rs b/toyos-bootmap/src/aarch64.rs index 7db15d8a091..2803b1a4f76 100644 --- a/toyos-bootmap/src/aarch64.rs +++ b/toyos-bootmap/src/aarch64.rs @@ -67,6 +67,22 @@ pub const fn page(phys: u64, cache: Cache) -> u64 { phys | PAGE | AF | attributes(cache) } +/// The physical address width `ID_AA64MMFR0_EL1.PARange` encodes (Arm ARM +/// K.a, D24.2.82), or `None` for an encoding it reserves. +pub const fn physical_bits(parange: u64) -> Option { + match parange { + 0b0000 => Some(32), + 0b0001 => Some(36), + 0b0010 => Some(40), + 0b0011 => Some(42), + 0b0100 => Some(44), + 0b0101 => Some(48), + 0b0110 => Some(52), + 0b0111 => Some(56), + _ => None, + } +} + /// 4 KiB pages in one 2 MiB page. const PAGES: u64 = PAGE_2M / PAGE_4K; diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs index 92f7237bfe4..830e427a186 100644 --- a/toyos-bootmap/src/lib.rs +++ b/toyos-bootmap/src/lib.rs @@ -12,9 +12,16 @@ //! is typed ([`Typing`]) and how an entry is encoded ([`x86_64`], //! [`aarch64`]). //! -//! 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. +//! A second-level table is position-independent, so one per 512 GiB the map +//! reaches serves both views: root slot [`ROOT_IDENTITY`] + r and +//! [`ROOT_HIGH_HALF`] + r name the same table, as both views already share +//! every directory. The map reaches as far as the CPU's physical addresses +//! do and the high half can hold, and refuses by name what lies past either. +//! +//! Pure: the scanout, the loader's image, the CPU's physical address width +//! 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 @@ -39,8 +46,7 @@ pub const PAGE_2M: u64 = 2 * 1024 * 1024; const GIB: u64 = 1 << 30; -/// One second-level table reaches 512 GiB, and the map has two: the identity -/// view at root slot 0 and the high-half view at root slot 256. +/// One second-level table reaches 512 GiB. const GIB_PER_PDPT: u64 = 512; /// Root slot 0: physical memory at its own address, which the switch to these @@ -132,6 +138,10 @@ const LOADER_DIRECTORIES: usize = 2; /// Every page directory a [`Plan`] can name. pub const MAX_DIRECTORIES: usize = LOW_DIRECTORIES + SCANOUT_DIRECTORIES + LOADER_DIRECTORIES; +/// Every second-level table a [`Plan`] can name: the low map's, and one per +/// directory past it, since a directory lies in exactly one 512 GiB. +pub const MAX_REGIONS: usize = 1 + SCANOUT_DIRECTORIES + LOADER_DIRECTORIES; + /// The small page a split 2 MiB page is mapped in. pub const PAGE_4K: u64 = 4096; @@ -141,9 +151,9 @@ const PAGES_PER_TABLE: u64 = PAGE_2M / PAGE_4K; /// The 2 MiB pages a scanout covers only in part: at most its first and its last. const FINE_TABLES: usize = 2; -/// The pool a builder needs: a root, a second-level table per view, every +/// The pool a builder needs: a root, every second-level table, every /// directory, and a table of 4 KiB leaves per split page. -pub const MAX_PAGES: usize = 3 + MAX_DIRECTORIES + FINE_TABLES; +pub const MAX_PAGES: usize = 1 + MAX_REGIONS + MAX_DIRECTORIES + FINE_TABLES; /// Why a machine's memory does not fit these tables. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -153,14 +163,15 @@ pub enum Refusal { Unaligned(u64), /// The range's own end does not fit an address. Extent { base: u64, len: u64 }, - /// Past the reach of the two second-level tables this map has. - PastPdpt(u64), + /// A range ending here, past the `bits` of physical address the CPU + /// reports: no table can name it. + PastWidth { end: u64, bits: u32 }, /// More directories than [`MAX_DIRECTORIES`]. Directories(usize), /// 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`]. + /// A range that ends here, past [`DIRECT_MAP_WINDOW`]. PastWindow(u64), /// A range of firmware's map that begins or ends here, off the 4 KiB page /// UEFI describes memory in. @@ -176,10 +187,9 @@ impl fmt::Display for Refusal { Self::Extent { base, len } => { write!(f, "{base:#x}+{len:#x} runs past the end of the address space") } - Self::PastPdpt(gib) => write!( - f, - "GiB {gib} is past the {GIB_PER_PDPT} this map's two second-level tables reach" - ), + Self::PastWidth { end, bits } => { + write!(f, "a range ends at {end:#x}, past the {bits} bits of physical address this CPU reports") + } Self::Directories(needed) => { write!(f, "{needed} page directories are needed and {MAX_DIRECTORIES} may be named") } @@ -190,7 +200,7 @@ impl fmt::Display for Refusal { ), Self::PastWindow(end) => write!( f, - "memory ends at {end:#x}, past the {DIRECT_MAP_WINDOW:#x} bytes a direct map can hold" + "a range ends at {end:#x}, past the {DIRECT_MAP_WINDOW:#x} bytes a direct map can hold" ), Self::OffPage(at) => write!(f, "a range of firmware's map is bounded at {at:#x}, off a {PAGE_4K:#x}-byte page"), } @@ -265,6 +275,9 @@ pub struct Entry { pub struct Plan<'a> { gibs: [u64; MAX_DIRECTORIES], directories: usize, + /// The 512 GiB each second-level table covers, by number. + spans: [u64; MAX_REGIONS], + regions: usize, /// The 2 MiB pages split into 4 KiB leaves, by base. fine: [u64; FINE_TABLES], fines: usize, @@ -291,17 +304,28 @@ impl<'a> Plan<'a> { /// x86-64 loader's switch to these tables runs from it, so it is mapped at /// identity or the first fetch after the switch faults. It is plain memory, /// so its pages are rounded out both ways and typed as the rest of memory. - pub fn new(scanout: Option<(u64, u64)>, loader: (u64, u64), typing: Typing<'a>) -> Result { + /// + /// `physical_bits` is the CPU's physical address width: a byte of either + /// at or past `1 << physical_bits` is [`Refusal::PastWidth`], and one past + /// [`DIRECT_MAP_WINDOW`] [`Refusal::PastWindow`]. + pub fn new( + scanout: Option<(u64, u64)>, + loader: (u64, u64), + typing: Typing<'a>, + physical_bits: u32, + ) -> Result { let mut plan = Self { gibs: [0; MAX_DIRECTORIES], directories: 0, + spans: [0; MAX_REGIONS], + regions: 0, fine: [0; FINE_TABLES], fines: 0, scanout: None, - loader: whole_pages(loader.0, loader.1)?, + loader: whole_pages(loader.0, loader.1, physical_bits)?, typing, }; - let scanout = scanout.map(|(base, len)| plan.scanout_extent(base, len)).transpose()?; + let scanout = scanout.map(|(base, len)| plan.scanout_extent(base, len, physical_bits)).transpose()?; let spans = [ scanout.map(|(base, end)| (base / PAGE_2M * PAGE_2M, end)), Some((plan.loader.0, plan.loader.0 + plan.loader.1)), @@ -320,6 +344,13 @@ impl<'a> Plan<'a> { plan.claim(gib); } } + for at in 0..plan.directories { + let region = plan.gibs[at] / GIB_PER_PDPT; + if !plan.spans[..plan.regions].contains(®ion) { + plan.spans[plan.regions] = region; + plan.regions += 1; + } + } if let Some((base, end)) = scanout { plan.split_scanout(base, end); } @@ -347,7 +378,7 @@ impl<'a> Plan<'a> { /// The scanout's extent as the map gives it, `(base, end)`, or why none can /// be given. - fn scanout_extent(&self, base: u64, len: u64) -> Result<(u64, u64), Refusal> { + fn scanout_extent(&self, base: u64, len: u64, physical_bits: u32) -> Result<(u64, u64), Refusal> { let granule = match self.typing { Typing::Firmware => PAGE_2M, Typing::ByMap(_) => PAGE_4K, @@ -357,9 +388,7 @@ impl<'a> Plan<'a> { } let end = base.checked_add(len).ok_or(Refusal::Extent { base, len })?; let end = end.checked_next_multiple_of(granule).ok_or(Refusal::Extent { base, len })?; - if (end - 1) / GIB >= GIB_PER_PDPT { - return Err(Refusal::PastPdpt((end - 1) / GIB)); - } + reachable(end, physical_bits)?; Ok((base, end)) } @@ -378,11 +407,31 @@ impl<'a> Plan<'a> { self.scanout = Some((base, end - base)); } + /// The 512 GiB each second-level table covers, by number `r`, in the order + /// a builder allocates them: each is named from root slots + /// [`ROOT_IDENTITY`] + `r` and [`ROOT_HIGH_HALF`] + `r`. + pub fn regions(&self) -> &[u64] { + &self.spans[..self.regions] + } + /// The GiB each directory covers, in the order a builder allocates them. pub fn directories(&self) -> &[u64] { &self.gibs[..self.directories] } + /// Where each of [`Plan::directories`] is named from: its second-level + /// table, by position in [`Plan::regions`], and its index there. + pub fn directory_slots(&self) -> impl Iterator + '_ { + self.directories().iter().map(move |&gib| { + let region = self + .regions() + .iter() + .position(|r| *r == gib / GIB_PER_PDPT) + .expect("every directory's 512 GiB was claimed"); + (region, (gib % GIB_PER_PDPT) as usize) + }) + } + /// The 2 MiB pages mapped by a table of 4 KiB leaves rather than one /// leaf, by base, in the order a builder allocates those tables. pub fn fine_tables(&self) -> &[u64] { @@ -491,19 +540,29 @@ impl<'a> Plan<'a> { /// `base..base + len` as whole 2 MiB pages, rounded out both ways: `(first /// page, bytes covered)`. An empty range has no page to be in and is refused. -fn whole_pages(base: u64, len: u64) -> Result<(u64, u64), Refusal> { +fn whole_pages(base: u64, len: u64, physical_bits: u32) -> Result<(u64, u64), Refusal> { if len == 0 { return Err(Refusal::Extent { base, len }); } let first = base / PAGE_2M * PAGE_2M; let end = base.checked_add(len).ok_or(Refusal::Extent { base, len })?; let end = end.checked_next_multiple_of(PAGE_2M).ok_or(Refusal::Extent { base, len })?; - if (end - 1) / GIB >= GIB_PER_PDPT { - return Err(Refusal::PastPdpt((end - 1) / GIB)); - } + reachable(end, physical_bits)?; Ok((first, end - first)) } +/// Whether a range ending at `end` is one this CPU can address and both views +/// can hold. +fn reachable(end: u64, physical_bits: u32) -> Result<(), Refusal> { + if physical_bits < u64::BITS && end > 1 << physical_bits { + return Err(Refusal::PastWidth { end, bits: physical_bits }); + } + if end > DIRECT_MAP_WINDOW { + return Err(Refusal::PastWindow(end)); + } + Ok(()) +} + /// The directories past the low map's that the ranges `(first, end)` need /// between them, each GiB counted once however many ranges fall in it. fn fresh_directories(spans: [Option<(u64, u64)>; 2]) -> usize { diff --git a/toyos-bootmap/tests/plan.rs b/toyos-bootmap/tests/plan.rs index fb98dfa746f..616fec4d4a4 100644 --- a/toyos-bootmap/tests/plan.rs +++ b/toyos-bootmap/tests/plan.rs @@ -3,8 +3,8 @@ //! and a loader firmware put below the map's end or above it. use toyos_bootmap::{ - aarch64, x86_64, Cache, Entry, Plan, Refusal, Slot, Typing, BOOT_MAP_BYTES, MAX_PAGES, PAGE_2M, - PAGE_4K, + aarch64, x86_64, Cache, Entry, Plan, Refusal, Slot, Typing, BOOT_MAP_BYTES, DIRECT_MAP_WINDOW, MAX_PAGES, + PAGE_2M, PAGE_4K, ROOT_HIGH_HALF, ROOT_IDENTITY, }; const GIB: u64 = 1 << 30; @@ -12,6 +12,9 @@ const GIB: u64 = 1 << 30; const PANEL: u64 = 0x7e9000; /// The loader as OVMF loads it on a 2 GiB guest: relocated low, and not on a page. const LOADER: (u64, u64) = (0x7e5f_0000, 0x3_5000); +/// A CPU of 48 physical address bits, wider than any address these tests name +/// but the ones that ask for the width itself. +const BITS: u32 = 48; /// Every leaf lands in a table the plan named, at an index inside it, no two /// land in the same place, and no leaf lands where a split page's table is @@ -35,7 +38,22 @@ fn is_consistent(plan: &Plan) { assert!(!seen.contains(&entry.slot), "{entry:?} twice"); seen.push(entry.slot); } - assert!(3 + plan.directories().len() + plan.fine_tables().len() <= MAX_PAGES); + // Every directory is named from the second-level table over its own GiB, + // at its own index, and no two from one place. + let slots: Vec<(usize, usize)> = plan.directory_slots().collect(); + for (&gib, &(region, index)) in plan.directories().iter().zip(&slots) { + assert_eq!(plan.regions()[region] * 512 + index as u64, gib, "directory {gib}"); + } + for (at, slot) in slots.iter().enumerate() { + assert!(!slots[..at].contains(slot), "{slot:?} twice"); + } + // Every table is named from an identity slot below the high half and a + // high-half slot inside the root. + for ®ion in plan.regions() { + assert!(ROOT_IDENTITY + (region as usize) < ROOT_HIGH_HALF, "region {region}"); + assert!(ROOT_HIGH_HALF + (region as usize) < 512, "region {region}"); + } + assert!(1 + plan.regions().len() + plan.directories().len() + plan.fine_tables().len() <= MAX_PAGES); } /// A 2 MiB leaf's directory, by position. @@ -48,7 +66,7 @@ fn directory(e: &Entry) -> usize { #[test] fn a_machine_with_no_framebuffer_is_the_low_map_and_nothing_else() { - let plan = Plan::new(None, LOADER, Typing::Firmware).expect("the low map alone"); + let plan = Plan::new(None, LOADER, Typing::Firmware, BITS).expect("the low map alone"); assert_eq!(plan.directories(), [0, 1, 2, 3]); assert_eq!(plan.scanout(), None); assert_eq!(plan.entries().count() as u64, BOOT_MAP_BYTES / PAGE_2M); @@ -60,7 +78,7 @@ fn a_machine_with_no_framebuffer_is_the_low_map_and_nothing_else() { /// retypes pages the low map already holds. #[test] fn a_framebuffer_inside_the_low_map_adds_no_directory() { - let plan = Plan::new(Some((0xc000_0000, PANEL)), LOADER, Typing::Firmware).expect("inside the low map"); + let plan = Plan::new(Some((0xc000_0000, PANEL)), LOADER, Typing::Firmware, BITS).expect("inside the low map"); assert_eq!(plan.directories(), [0, 1, 2, 3]); // Rounded up to whole pages, and up only. assert_eq!(plan.scanout(), Some((0xc000_0000, 0x80_0000))); @@ -74,7 +92,7 @@ fn a_framebuffer_inside_the_low_map_adds_no_directory() { /// the low map untouched. #[test] fn a_framebuffer_above_the_low_map_adds_its_own_directory() { - let plan = Plan::new(Some((256 * GIB, PANEL)), LOADER, Typing::Firmware).expect("above the low map"); + let plan = Plan::new(Some((256 * GIB, PANEL)), LOADER, Typing::Firmware, BITS).expect("above the low map"); assert_eq!(plan.directories(), [0, 1, 2, 3, 256]); assert_eq!(plan.scanout(), Some((256 * GIB, 0x80_0000))); let mine: Vec = plan @@ -94,13 +112,13 @@ fn a_framebuffer_above_the_low_map_adds_its_own_directory() { /// past it, not the last GiB of it. #[test] fn a_framebuffer_at_the_boundary_is_outside_the_low_map() { - let plan = Plan::new(Some((BOOT_MAP_BYTES, PANEL)), LOADER, Typing::Firmware).expect("at the boundary"); + let plan = Plan::new(Some((BOOT_MAP_BYTES, PANEL)), LOADER, Typing::Firmware, BITS).expect("at the boundary"); assert_eq!(plan.directories(), [0, 1, 2, 3, 4]); assert!(plan.entries().filter(|e| e.cache == Cache::Scanout).all(|e| directory(&e) == 4)); is_consistent(&plan); // One page below it is the low map's last page, and adds nothing. - let inside = Plan::new(Some((BOOT_MAP_BYTES - PAGE_2M, PAGE_2M)), LOADER, Typing::Firmware).expect("the last page"); + let inside = Plan::new(Some((BOOT_MAP_BYTES - PAGE_2M, PAGE_2M)), LOADER, Typing::Firmware, BITS).expect("the last page"); assert_eq!(inside.directories(), [0, 1, 2, 3]); is_consistent(&inside); } @@ -111,7 +129,7 @@ fn a_framebuffer_at_the_boundary_is_outside_the_low_map() { #[test] fn a_framebuffer_that_straddles_the_low_maps_end_is_mapped_from_both_arms() { let base = BOOT_MAP_BYTES - PAGE_2M; - let plan = Plan::new(Some((base, 4 * PAGE_2M)), LOADER, Typing::Firmware).expect("across the end"); + let plan = Plan::new(Some((base, 4 * PAGE_2M)), LOADER, Typing::Firmware, BITS).expect("across the end"); assert_eq!(plan.directories(), [0, 1, 2, 3, 4]); let mine: Vec<(u64, usize)> = plan .entries() @@ -137,7 +155,7 @@ fn a_framebuffer_that_straddles_the_low_maps_end_is_mapped_from_both_arms() { #[test] fn a_framebuffer_that_straddles_a_gib_claims_both() { let base = 8 * GIB - PAGE_2M; - let plan = Plan::new(Some((base, 4 * PAGE_2M)), LOADER, Typing::Firmware).expect("straddling"); + let plan = Plan::new(Some((base, 4 * PAGE_2M)), LOADER, Typing::Firmware, BITS).expect("straddling"); assert_eq!(plan.directories(), [0, 1, 2, 3, 7, 8]); assert!(plan.fine_tables().is_empty()); is_consistent(&plan); @@ -145,9 +163,9 @@ fn a_framebuffer_that_straddles_a_gib_claims_both() { // With a loader straddling another, every directory the budget has; the // two split-page tables are a map-typed plan's, and this one splits nothing. let loader = (12 * GIB - 0x1000, 0x3_5000); - let full = Plan::new(Some((base, 4 * PAGE_2M)), loader, Typing::Firmware).expect("both straddling"); + let full = Plan::new(Some((base, 4 * PAGE_2M)), loader, Typing::Firmware, BITS).expect("both straddling"); assert_eq!(full.directories(), [0, 1, 2, 3, 7, 8, 11, 12]); - assert_eq!(3 + full.directories().len() + 2, MAX_PAGES); + assert_eq!(full.regions(), [0]); is_consistent(&full); } @@ -162,22 +180,20 @@ fn the_high_half_slot_is_the_top_nine_bits_of_phys_offset() { #[test] fn a_base_off_the_page_is_refused_rather_than_rounded_down() { - assert_eq!(Plan::new(Some((0xc000_1000, PANEL)), LOADER, Typing::Firmware), Err(Refusal::Unaligned(0xc000_1000))); + assert_eq!(Plan::new(Some((0xc000_1000, PANEL)), LOADER, Typing::Firmware, BITS), Err(Refusal::Unaligned(0xc000_1000))); // The refusal names the address, because a machine owner reads it. assert!(Refusal::Unaligned(0xc000_1000).to_string().contains("0xc0001000")); } #[test] fn a_range_no_map_can_hold_is_refused_by_name() { - // Past the two PDPTs' 512 GiB. - assert_eq!(Plan::new(Some((512 * GIB, PANEL)), LOADER, Typing::Firmware), Err(Refusal::PastPdpt(512))); // Its own end does not fit an address, either as it is given... let base = u64::MAX - PAGE_2M + 1; - assert_eq!(Plan::new(Some((base, u64::MAX)), LOADER, Typing::Firmware), Err(Refusal::Extent { base, len: u64::MAX })); + assert_eq!(Plan::new(Some((base, u64::MAX)), LOADER, Typing::Firmware, BITS), Err(Refusal::Extent { base, len: u64::MAX })); // ...or once rounded up to the page it must end on. - assert_eq!(Plan::new(Some((base, 1)), LOADER, Typing::Firmware), Err(Refusal::Extent { base, len: 1 })); + assert_eq!(Plan::new(Some((base, 1)), LOADER, Typing::Firmware, BITS), Err(Refusal::Extent { base, len: 1 })); // Wider than the directories a plan may name, and told what it would need. - assert_eq!(Plan::new(Some((16 * GIB, 10 * GIB)), LOADER, Typing::Firmware), Err(Refusal::Directories(14))); + assert_eq!(Plan::new(Some((16 * GIB, 10 * GIB)), LOADER, Typing::Firmware, BITS), Err(Refusal::Directories(14))); assert!(Refusal::Directories(14).to_string().contains("14 page directories")); } @@ -204,7 +220,7 @@ fn uncacheable_is_pat_entry_three_and_plain_memory_is_entry_zero() { #[test] fn a_map_typed_plan_is_memory_where_firmware_says_write_back_and_a_device_elsewhere() { let ram = [(0x4000_0000, 0x4000_0000)]; - let plan = Plan::new(None, LOADER, Typing::ByMap(&ram)).expect("virt's map"); + let plan = Plan::new(None, LOADER, Typing::ByMap(&ram), BITS).expect("virt's map"); for entry in plan.entries() { let want = if (0x4000_0000..0x8000_0000).contains(&entry.phys) { Cache::Memory } else { Cache::Device }; assert_eq!(entry.cache, want, "{entry:?}"); @@ -216,26 +232,26 @@ fn a_map_typed_plan_is_memory_where_firmware_says_write_back_and_a_device_elsewh #[test] fn a_page_that_is_part_memory_is_refused_by_address() { let ram = [(0x4000_0000, 0x4000_0000 + 0x1000)]; - assert_eq!(Plan::new(None, LOADER, Typing::ByMap(&ram)), Err(Refusal::Mixed(0x8000_0000))); + assert_eq!(Plan::new(None, LOADER, Typing::ByMap(&ram), BITS), Err(Refusal::Mixed(0x8000_0000))); assert!(Refusal::Mixed(0x8000_0000).to_string().contains("0x80000000")); // Two adjacent ranges that meet inside a page make it whole. let split = [(0x4000_0000, 0x4010_0000 - 0x4000_0000), (0x4010_0000, 0x3ff0_0000)]; - assert!(Plan::new(None, LOADER, Typing::ByMap(&split)).is_ok()); + assert!(Plan::new(None, LOADER, Typing::ByMap(&split), BITS).is_ok()); } /// A scanout inside memory is the scanout's leaves, not a mixed page. #[test] fn a_scanout_inside_memory_is_typed_as_the_scanout() { let ram = [(0x4000_0000, 0x4000_0000)]; - let plan = Plan::new(Some((0x5000_0000, 0x10_0000)), LOADER, Typing::ByMap(&ram)).expect("inside"); + let plan = Plan::new(Some((0x5000_0000, 0x10_0000)), LOADER, Typing::ByMap(&ram), BITS).expect("inside"); let scanout: Vec = plan.entries().filter(|e| e.cache == Cache::Scanout).map(|e| e.phys).collect(); assert_eq!(scanout, (0..256).map(|page| 0x5000_0000 + page * PAGE_4K).collect::>()); is_consistent(&plan); // And a scanout over a page that is part memory takes that page whole. let part = [(0x4000_0000, 0x1000)]; - assert!(Plan::new(Some((0x4000_0000, PAGE_2M)), LOADER, Typing::ByMap(&part)).is_ok()); - assert_eq!(Plan::new(None, LOADER, Typing::ByMap(&part)), Err(Refusal::Mixed(0x4000_0000))); + assert!(Plan::new(Some((0x4000_0000, PAGE_2M)), LOADER, Typing::ByMap(&part), BITS).is_ok()); + assert_eq!(Plan::new(None, LOADER, Typing::ByMap(&part), BITS), Err(Refusal::Mixed(0x4000_0000))); } /// The AArch64 descriptors, against the Arm ARM's bit positions (K.a, D8.3.1 @@ -277,7 +293,7 @@ fn the_aarch64_descriptors_carry_the_attribute_index_the_mair_names() { fn a_scanout_carved_out_of_ram_splits_the_pages_it_shares() { let ram = [(0x4000_0000, 0x8000_0000)]; let (base, len) = (0xbc7a_0000, 0x30_0000); - let plan = Plan::new(Some((base, len)), LOADER, Typing::ByMap(&ram)).expect("ramfb"); + let plan = Plan::new(Some((base, len)), LOADER, Typing::ByMap(&ram), BITS).expect("ramfb"); assert_eq!(plan.fine_tables(), [0xbc60_0000, 0xbca0_0000]); let scanout: Vec = plan.entries().filter(|e| e.cache == Cache::Scanout).collect(); let bytes: u64 = scanout @@ -296,7 +312,7 @@ fn a_scanout_carved_out_of_ram_splits_the_pages_it_shares() { assert_eq!(at(0xbc80_0000), Cache::Scanout); is_consistent(&plan); // The same framebuffer under range-register typing is refused, as before. - assert_eq!(Plan::new(Some((base, len)), LOADER, Typing::Firmware), Err(Refusal::Unaligned(base))); + assert_eq!(Plan::new(Some((base, len)), LOADER, Typing::Firmware, BITS), Err(Refusal::Unaligned(base))); } /// A map-typed scanout must still start on a 4 KiB page. @@ -304,7 +320,7 @@ fn a_scanout_carved_out_of_ram_splits_the_pages_it_shares() { fn a_map_typed_scanout_off_a_small_page_is_refused() { let ram = [(0x4000_0000, 0x8000_0000)]; assert_eq!( - Plan::new(Some((0xbc7a_0800, 0x1000)), LOADER, Typing::ByMap(&ram)), + Plan::new(Some((0xbc7a_0800, 0x1000)), LOADER, Typing::ByMap(&ram), BITS), Err(Refusal::Unaligned(0xbc7a_0800)) ); } @@ -315,7 +331,7 @@ fn a_map_typed_scanout_off_a_small_page_is_refused() { #[test] fn a_loader_above_the_low_map_adds_its_own_directory_of_plain_memory() { let loader = (0x1_4000_0000, 0x3_4a00); - let plan = Plan::new(None, loader, Typing::Firmware).expect("above the low map"); + let plan = Plan::new(None, loader, Typing::Firmware, BITS).expect("above the low map"); assert_eq!(plan.directories(), [0, 1, 2, 3, 5]); assert_eq!(plan.loader(), (0x1_4000_0000, PAGE_2M)); let mine: Vec<(u64, usize, Cache)> = plan @@ -330,7 +346,7 @@ fn a_loader_above_the_low_map_adds_its_own_directory_of_plain_memory() { /// A loader inside the low map is already mapped, and adds nothing. #[test] fn a_loader_inside_the_low_map_adds_nothing() { - let plan = Plan::new(None, LOADER, Typing::Firmware).expect("inside"); + let plan = Plan::new(None, LOADER, Typing::Firmware, BITS).expect("inside"); assert_eq!(plan.directories(), [0, 1, 2, 3]); assert_eq!(plan.entries().count() as u64, BOOT_MAP_BYTES / PAGE_2M); assert_eq!(plan.loader(), (0x7e40_0000, 2 * PAGE_2M)); @@ -341,7 +357,7 @@ fn a_loader_inside_the_low_map_adds_nothing() { #[test] fn a_loader_across_a_page_is_both_pages() { let loader = (6 * GIB + PAGE_2M - 0x1000, 0x3_4a00); - let plan = Plan::new(None, loader, Typing::Firmware).expect("across a page"); + let plan = Plan::new(None, loader, Typing::Firmware, BITS).expect("across a page"); assert_eq!(plan.loader(), (6 * GIB, 2 * PAGE_2M)); let mine: Vec = plan.entries().filter(|e| e.phys >= BOOT_MAP_BYTES).map(|e| e.phys).collect(); assert_eq!(mine, [6 * GIB, 6 * GIB + PAGE_2M]); @@ -352,7 +368,7 @@ fn a_loader_across_a_page_is_both_pages() { /// scanout holds is not written twice. #[test] fn a_loader_beside_the_scanout_shares_its_directory() { - let plan = Plan::new(Some((256 * GIB, PANEL)), (256 * GIB + 0x7f_0000, 0x3_4a00), Typing::Firmware) + let plan = Plan::new(Some((256 * GIB, PANEL)), (256 * GIB + 0x7f_0000, 0x3_4a00), Typing::Firmware, BITS) .expect("one GiB for both"); assert_eq!(plan.directories(), [0, 1, 2, 3, 256]); let above: Vec<(u64, Cache)> = @@ -375,33 +391,120 @@ fn a_loader_beside_the_scanout_shares_its_directory() { #[test] fn a_map_typed_loader_is_memory() { let ram = [(0x4000_0000, 0x4000_0000), (5 * GIB, GIB)]; - let plan = Plan::new(None, (5 * GIB + 0x1000, 0x3_4a00), Typing::ByMap(&ram)).expect("in memory"); + let plan = Plan::new(None, (5 * GIB + 0x1000, 0x3_4a00), Typing::ByMap(&ram), BITS).expect("in memory"); let above: Vec<(u64, Cache)> = plan.entries().filter(|e| e.phys >= BOOT_MAP_BYTES).map(|e| (e.phys, e.cache)).collect(); assert_eq!(above, [(5 * GIB, Cache::Memory)]); is_consistent(&plan); let part = [(0x4000_0000, 0x4000_0000), (5 * GIB, 0x10_0000)]; assert_eq!( - Plan::new(None, (5 * GIB, 0x3_4a00), Typing::ByMap(&part)), + Plan::new(None, (5 * GIB, 0x3_4a00), Typing::ByMap(&part), BITS), Err(Refusal::Mixed(5 * GIB)) ); } #[test] fn a_loader_no_map_can_hold_is_refused_by_name() { - assert_eq!(Plan::new(None, (512 * GIB, 0x3_4a00), Typing::Firmware), Err(Refusal::PastPdpt(512))); assert_eq!( - Plan::new(None, (0x1_4000_0000, 0), Typing::Firmware), + Plan::new(None, (0x1_4000_0000, 0), Typing::Firmware, BITS), Err(Refusal::Extent { base: 0x1_4000_0000, len: 0 }) ); // Its directories count against the same budget as the scanout's, and a // GiB both need is counted once. assert_eq!( - Plan::new(Some((16 * GIB, 4 * GIB)), (32 * GIB, 0x3_4a00), Typing::Firmware), + Plan::new(Some((16 * GIB, 4 * GIB)), (32 * GIB, 0x3_4a00), Typing::Firmware, BITS), Err(Refusal::Directories(9)) ); - let shared = Plan::new(Some((16 * GIB, 4 * GIB)), (17 * GIB, 0x3_4a00), Typing::Firmware) + let shared = Plan::new(Some((16 * GIB, 4 * GIB)), (17 * GIB, 0x3_4a00), Typing::Firmware, BITS) .expect("the loader inside the scanout's GiBs"); assert_eq!(shared.directories(), [0, 1, 2, 3, 16, 17, 18, 19]); is_consistent(&shared); } + +/// A framebuffer firmware put at 1010 GiB, as an AMD laptop's does, and the +/// panel its GOP reports there: past the low map's 512 GiB, so the map adds a +/// second-level table for it, named from both views' second root slot. +#[test] +fn a_framebuffer_past_512_gib_adds_its_own_second_level_table() { + let base = 0xfc_9000_0000; + let plan = Plan::new(Some((base, 8_294_400)), LOADER, Typing::Firmware, BITS).expect("at 1010 GiB"); + assert_eq!(base / GIB, 1010); + assert_eq!(plan.directories(), [0, 1, 2, 3, 1010]); + assert_eq!(plan.regions(), [0, 1]); + assert_eq!(plan.directory_slots().last(), Some((1, 1010 - 512))); + let mine: Vec<(u64, usize)> = + plan.entries().filter(|e| e.cache == Cache::Scanout).map(|e| (e.phys, directory(&e))).collect(); + assert_eq!(mine, (0..4).map(|page| (base + page * PAGE_2M, 4)).collect::>()); + is_consistent(&plan); +} + +/// A scanout across the first 512 GiB boundary is two directories under two +/// second-level tables, and a loader in the same 512 GiB shares the second. +#[test] +fn a_framebuffer_across_512_gib_names_both_second_level_tables() { + let base = 512 * GIB - PAGE_2M; + let plan = Plan::new(Some((base, 4 * PAGE_2M)), (520 * GIB, 0x3_4a00), Typing::Firmware, BITS) + .expect("across 512 GiB"); + assert_eq!(plan.directories(), [0, 1, 2, 3, 511, 512, 520]); + assert_eq!(plan.regions(), [0, 1]); + assert_eq!(plan.directory_slots().skip(4).collect::>(), [(0, 511), (1, 0), (1, 8)]); + is_consistent(&plan); +} + +/// The most the map can be asked for: a scanout and a loader each straddling a +/// 512 GiB boundary, four second-level tables past the low map's, the last +/// below the high half's last root slot. +#[test] +fn every_second_level_table_the_budget_has_fits_the_pool() { + let scanout = (1024 * GIB - PAGE_2M, 4 * PAGE_2M); + let loader = (DIRECT_MAP_WINDOW - 512 * GIB - 0x1000, 0x3_4a00); + let plan = Plan::new(Some(scanout), loader, Typing::Firmware, BITS).expect("both straddling"); + assert_eq!(plan.regions(), [0, 1, 2, 254, 255]); + assert_eq!(1 + plan.regions().len() + plan.directories().len() + plan.fine_tables().len(), MAX_PAGES - 2); + is_consistent(&plan); +} + +/// The CPU's width is the map's reach: a range ending on its last byte fits, +/// and one a page past it is refused by name, for the scanout and the loader. +#[test] +fn a_range_past_the_physical_address_width_is_refused_by_name() { + let top = 1 << 40; + let last = Plan::new(Some((top - 4 * PAGE_2M, 4 * PAGE_2M)), LOADER, Typing::Firmware, 40).expect("the last bytes"); + assert_eq!(last.directories(), [0, 1, 2, 3, 1023]); + assert_eq!(last.regions(), [0, 1]); + is_consistent(&last); + assert_eq!( + Plan::new(Some((top - 2 * PAGE_2M, 4 * PAGE_2M)), LOADER, Typing::Firmware, 40), + Err(Refusal::PastWidth { end: top + 2 * PAGE_2M, bits: 40 }) + ); + assert_eq!( + Plan::new(None, (top - 0x1000, 0x3_4a00), Typing::Firmware, 40), + Err(Refusal::PastWidth { end: top + PAGE_2M, bits: 40 }) + ); + let said = Refusal::PastWidth { end: top + PAGE_2M, bits: 40 }.to_string(); + assert!(said.contains("0x10000200000") && said.contains("40 bits"), "{said}"); +} + +/// Past the high half's 128 TiB no view can hold a range, whatever the CPU's +/// width: the identity view would reach into the high half's slots. +#[test] +fn a_range_past_the_window_is_refused_however_wide_the_cpu() { + assert_eq!( + Plan::new(Some((DIRECT_MAP_WINDOW, PANEL)), LOADER, Typing::Firmware, 52), + Err(Refusal::PastWindow(DIRECT_MAP_WINDOW + 0x80_0000)) + ); + assert_eq!( + Plan::new(None, (DIRECT_MAP_WINDOW - 0x1000, 0x3_4a00), Typing::Firmware, 52), + Err(Refusal::PastWindow(DIRECT_MAP_WINDOW + PAGE_2M)) + ); + let last = Plan::new(Some((DIRECT_MAP_WINDOW - 0x80_0000, PANEL)), LOADER, Typing::Firmware, 52).expect("the window's last GiB"); + assert_eq!(last.regions(), [0, 255]); + is_consistent(&last); +} + +/// `PARange`'s encodings as the Arm ARM gives them, and a reserved one refused. +#[test] +fn parange_decodes_to_the_widths_the_arm_arm_names() { + let widths: Vec> = (0..9).map(aarch64::physical_bits).collect(); + assert_eq!(widths, [Some(32), Some(36), Some(40), Some(42), Some(44), Some(48), Some(52), Some(56), None]); +} From be194cd9d02c2f6867b18eb8488be13ca8cea5d5 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 15:05:05 +0200 Subject: [PATCH 2/4] The boot map's tables are written by toyos-bootmap and walked on the host as the SDM walks them; the width check goes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 1 of #839 found that no test reached the loop that names each second-level table from root slots ROOT_IDENTITY + r and ROOT_HIGH_HALF + r: the host tests stopped at the Plan, and every booted machine has one region. - Plan::write lays a plan into a pool of Tables (4096-aligned by type) at a physical address the caller gives, through an Encoding of the three entries each architecture's module exports as ENCODING. The loader's unsafe builder is gone; what is left unsafe there is the cast of its zeroed allocation to the pool. - tests/walk.rs walks the written tables as Intel SDM Vol. 3A §4.5 gives four-level paging, from the entries' bits, on the AMD laptop's numbers (scanout 0xfc90000000+8294400): the scanout at identity and at PHYS_OFFSET, GiB 0 at PHYS_OFFSET, nothing past what the plan names, and every leaf of three more plans in both views. - The physical-width check goes (arch::physical_bits on both architectures, the PARange decoder, Refusal::PastWidth, Plan::new's parameter and the loader's read and print): firmware that ran on the CPU cannot hand it a framebuffer or an image past its own width, and DIRECT_MAP_WINDOW is the map's one reach, refused by name as PastWindow. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- bootloader/src/arch/aarch64.rs | 11 -- bootloader/src/arch/x86_64.rs | 11 -- bootloader/src/main.rs | 75 ++----------- toyos-bootmap/Cargo.toml | 2 +- toyos-bootmap/src/aarch64.rs | 19 +--- toyos-bootmap/src/lib.rs | 102 ++++++++++++------ toyos-bootmap/src/x86_64.rs | 5 +- toyos-bootmap/tests/plan.rs | 109 +++++++------------ toyos-bootmap/tests/walk.rs | 185 +++++++++++++++++++++++++++++++++ 9 files changed, 309 insertions(+), 210 deletions(-) create mode 100644 toyos-bootmap/tests/walk.rs diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs index bd445190311..94106d6dfb1 100644 --- a/bootloader/src/arch/aarch64.rs +++ b/bootloader/src/arch/aarch64.rs @@ -15,17 +15,6 @@ pub fn typing(write_back: &[(u64, u64)]) -> Typing<'_> { Typing::ByMap(write_back) } -/// The CPU's physical address width, from `ID_AA64MMFR0_EL1.PARange`, or why -/// it cannot be read. -pub fn physical_bits() -> Result { - let mmfr0: u64; - // SAFETY: reads an ID register EL1 and above may always read. - unsafe { core::arch::asm!("mrs {}, id_aa64mmfr0_el1", out(reg) mmfr0, options(nomem, nostack, preserves_flags)) }; - let parange = mmfr0 & 0xF; - toyos_bootmap::aarch64::physical_bits(parange) - .ok_or_else(|| alloc::format!("ID_AA64MMFR0_EL1.PARange reads {parange:#x}, an encoding the Arm ARM reserves")) -} - /// The exception level the loader runs at, from `CurrentEL`. fn current_el() -> u64 { let current: u64; diff --git a/bootloader/src/arch/x86_64.rs b/bootloader/src/arch/x86_64.rs index 380e9b4016a..78e2e25c615 100644 --- a/bootloader/src/arch/x86_64.rs +++ b/bootloader/src/arch/x86_64.rs @@ -14,17 +14,6 @@ pub fn typing(_write_back: &[(u64, u64)]) -> toyos_bootmap::Typing<'_> { toyos_bootmap::Typing::Firmware } -/// The CPU's physical address width, `CPUID.80000008H:EAX[7:0]` (SDM Vol. 2A, -/// CPUID; AMD APM Vol. 3, E.4.7), or why it cannot be read. -pub fn physical_bits() -> Result { - use core::arch::x86_64::__cpuid; - let max = __cpuid(0x8000_0000).eax; - if max < 0x8000_0008 { - return Err(alloc::format!("CPUID answers extended leaves to {max:#x}, short of 0x80000008's physical address width")); - } - Ok(__cpuid(0x8000_0008).eax & 0xFF) -} - /// The time-stamp counter, which counts from reset. pub fn counter() -> u64 { // SAFETY: RDTSC reads a counter and nothing else; every x86-64 has it. diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index b1ae39feace..9c689d55cd0 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -20,7 +20,7 @@ use uefi::{ Event, }; use toyos_abi::boot::{KernelArgs, MemoryMapEntry, RootBridgeWindow, MAX_ROOT_BRIDGE_WINDOWS}; -use toyos_bootmap::{Plan, BOOT_MAP_BYTES, MAX_PAGES, ROOT_HIGH_HALF, ROOT_IDENTITY}; +use toyos_bootmap::{Plan, Table, BOOT_MAP_BYTES, MAX_PAGES}; use toyos_update::policy; use toyos_update::record::{self, Booted, Ended, Record}; @@ -413,55 +413,6 @@ fn query_gop(system_table: &SystemTable) -> Option { }) } -/// Write `plan` into `pt_mem` and return its root table's physical address. -/// -/// # Safety -/// `pt_mem` is [`toyos_bootmap::MAX_PAGES`] pages of zeroed memory, 4096-aligned. -unsafe fn build_boot_page_tables(pt_mem: *mut u8, plan: &Plan) -> u64 { - use arch::encoding::{block, page, table}; - use toyos_bootmap::Slot; - - let mut next_page = 0usize; - let mut alloc_page = || -> *mut u64 { - let page = pt_mem.add(next_page * 4096) as *mut u64; - next_page += 1; - page - }; - - let root = alloc_page(); - let mut pdpts = [core::ptr::null_mut::(); toyos_bootmap::MAX_REGIONS]; - for (at, region) in plan.regions().iter().enumerate() { - let pdpt = alloc_page(); - pdpts[at] = pdpt; - *root.add(ROOT_IDENTITY + *region as usize) = table(pdpt as u64); - *root.add(ROOT_HIGH_HALF + *region as usize) = table(pdpt as u64); - } - let mut directories = [core::ptr::null_mut::(); toyos_bootmap::MAX_DIRECTORIES]; - for (slot, (region, index)) in plan.directory_slots().enumerate() { - let pd = alloc_page(); - directories[slot] = pd; - *pdpts[region].add(index) = table(pd as u64); - } - - let mut fine = [core::ptr::null_mut::(); toyos_bootmap::MAX_PAGES]; - for (table_at, (directory, index)) in plan.fine_slots().enumerate() { - let leaves = alloc_page(); - fine[table_at] = leaves; - *directories[directory].add(index) = table(leaves as u64); - } - - for entry in plan.entries() { - match entry.slot { - Slot::Directory { directory, index } => { - *directories[directory].add(index) = block(entry.phys, entry.cache) - } - Slot::Fine { table, index } => *fine[table].add(index) = page(entry.phys, entry.cache), - } - } - - root as u64 -} - /// Every range firmware's map says is write-back memory, `(base, length)`: /// a descriptor carrying `EFI_MEMORY_WB` and not one of the two I/O types, /// which a firmware may give the attribute without meaning memory. @@ -519,12 +470,9 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v let free = gcd::free_mmio(&system_table, &mut root_bridge_windows[named..]); let root_bridge_window_count = (named + free) as u64; - // Pre-allocated before exiting boot services, and flat: `alloc_page` splits - // it into 512-entry pages. - let pt_layout = Layout::from_size_align(MAX_PAGES * 4096, 4096).unwrap(); - // SAFETY: `layout` has non-zero size and its 4096 alignment is what every - // page-table page below needs — the low 12 bits of an entry are flags, not - // address bits. + // Allocated before exiting boot services, which takes the allocator away. + let pt_layout = Layout::new::<[Table; MAX_PAGES]>(); + // SAFETY: `pt_layout` has non-zero size. let pt_mem = unsafe { alloc::alloc::alloc_zeroed(pt_layout) }; assert!(!pt_mem.is_null(), "page table allocation failed"); @@ -541,17 +489,10 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v // pages by it. Before the exit, while the map can still be asked for; the // attributes a descriptor carries do not change across it. let write_back = write_back_memory(&system_table); - // Said before it is refused, for `report_reach`'s reason. - let physical_bits = arch::physical_bits().unwrap_or_else(|why| { - println!("Boot map: NO MAP HOLDS THIS MACHINE, {why}"); - panic!("{why}"); - }); - println!("Boot map: this CPU addresses {physical_bits} bits of physical memory"); let planned = Plan::new( gop.as_ref().map(|g| (g.framebuffer, g.framebuffer_size)), loader, arch::typing(&write_back), - physical_bits, ); match &planned { Ok(plan) => { @@ -572,9 +513,11 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v let plan = planned .unwrap_or_else(|why| panic!("the boot map cannot hold the scanout and the loader: {why}")); - // SAFETY: `pt_mem` is the `MAX_PAGES * 4096`-byte, 4096-aligned, zeroed - // allocation above, and a `Plan` never names more pages than that. - let pml4_phys = unsafe { build_boot_page_tables(pt_mem, &plan) }; + // SAFETY: `pt_mem` is the zeroed allocation of `pt_layout` above, which + // nothing else holds; firmware's map is identity, so its address is the + // pool's physical one. + let pool = unsafe { &mut *pt_mem.cast::<[Table; MAX_PAGES]>() }; + let pml4_phys = plan.write(arch::encoding::ENCODING, pool, pt_mem as u64); println!("Boot map: root {pml4_phys:#x}, {BOOT_MAP_BYTES:#x} bytes at identity and at PHYS_OFFSET"); let kernel_phys = kernel.memory.as_ptr() as u64; diff --git a/toyos-bootmap/Cargo.toml b/toyos-bootmap/Cargo.toml index dc05967ef7e..201a83fb5db 100644 --- a/toyos-bootmap/Cargo.toml +++ b/toyos-bootmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toyos-bootmap" -description = "The bootloader's transient page tables, decided rather than built, pure." +description = "The bootloader's transient page tables, decided and written into pages the loader gives, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/toyos-bootmap/src/aarch64.rs b/toyos-bootmap/src/aarch64.rs index 2803b1a4f76..48a46d25ad5 100644 --- a/toyos-bootmap/src/aarch64.rs +++ b/toyos-bootmap/src/aarch64.rs @@ -6,7 +6,7 @@ use toyos_abi::boot::MemoryMapEntry; -use crate::{is_read_as_memory, Cache, DirectMapEnd, Refusal, DIRECT_MAP_WINDOW, PAGE_2M, PAGE_4K}; +use crate::{is_read_as_memory, Cache, DirectMapEnd, Encoding, Refusal, DIRECT_MAP_WINDOW, PAGE_2M, PAGE_4K}; /// `MAIR_EL1` index 0: Device-nGnRE, registers. pub const ATTR_DEVICE: u64 = 0; @@ -67,21 +67,8 @@ pub const fn page(phys: u64, cache: Cache) -> u64 { phys | PAGE | AF | attributes(cache) } -/// The physical address width `ID_AA64MMFR0_EL1.PARange` encodes (Arm ARM -/// K.a, D24.2.82), or `None` for an encoding it reserves. -pub const fn physical_bits(parange: u64) -> Option { - match parange { - 0b0000 => Some(32), - 0b0001 => Some(36), - 0b0010 => Some(40), - 0b0011 => Some(42), - 0b0100 => Some(44), - 0b0101 => Some(48), - 0b0110 => Some(52), - 0b0111 => Some(56), - _ => None, - } -} +/// The three, for [`Plan::write`](crate::Plan::write). +pub const ENCODING: Encoding = Encoding { table, block, page }; /// 4 KiB pages in one 2 MiB page. const PAGES: u64 = PAGE_2M / PAGE_4K; diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs index 830e427a186..260b732da9e 100644 --- a/toyos-bootmap/src/lib.rs +++ b/toyos-bootmap/src/lib.rs @@ -1,4 +1,5 @@ -//! The bootloader's transient page tables, decided rather than built. +//! The bootloader's transient page tables, decided, and written into pages +//! the loader gives. //! //! Between the loader's switch to these tables and the kernel's `mm::init` //! there is one mapping in the machine, and everything the kernel touches in @@ -15,13 +16,12 @@ //! A second-level table is position-independent, so one per 512 GiB the map //! reaches serves both views: root slot [`ROOT_IDENTITY`] + r and //! [`ROOT_HIGH_HALF`] + r name the same table, as both views already share -//! every directory. The map reaches as far as the CPU's physical addresses -//! do and the high half can hold, and refuses by name what lies past either. +//! every directory. The map reaches as far as the high half can hold, and +//! refuses by name what lies past it. //! -//! Pure: the scanout, the loader's image, the CPU's physical address width -//! 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. +//! 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, which +//! [`Plan::write`] lays into a pool of [`Table`]s the loader allocates. //! //! 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 @@ -155,6 +155,25 @@ const FINE_TABLES: usize = 2; /// directory, and a table of 4 KiB leaves per split page. pub const MAX_PAGES: usize = 1 + MAX_REGIONS + MAX_DIRECTORIES + FINE_TABLES; +/// One page of the map: 512 entries, on the page an entry names it by, since +/// an entry's low 12 bits are flags rather than address. +#[derive(Clone, Copy)] +#[repr(C, align(4096))] +pub struct Table(pub [u64; 512]); + +impl Table { + pub const EMPTY: Self = Self([0; 512]); +} + +/// How an architecture encodes the three entries a [`Plan`] writes: one naming +/// the table below it, a 2 MiB leaf, and a 4 KiB leaf. +#[derive(Clone, Copy)] +pub struct Encoding { + pub table: fn(u64) -> u64, + pub block: fn(u64, Cache) -> u64, + pub page: fn(u64, Cache) -> u64, +} + /// Why a machine's memory does not fit these tables. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Refusal { @@ -163,9 +182,6 @@ pub enum Refusal { Unaligned(u64), /// The range's own end does not fit an address. Extent { base: u64, len: u64 }, - /// A range ending here, past the `bits` of physical address the CPU - /// reports: no table can name it. - PastWidth { end: u64, bits: u32 }, /// More directories than [`MAX_DIRECTORIES`]. Directories(usize), /// A 2 MiB page of the low map that is write-back memory in part and not @@ -187,9 +203,6 @@ impl fmt::Display for Refusal { Self::Extent { base, len } => { write!(f, "{base:#x}+{len:#x} runs past the end of the address space") } - Self::PastWidth { end, bits } => { - write!(f, "a range ends at {end:#x}, past the {bits} bits of physical address this CPU reports") - } Self::Directories(needed) => { write!(f, "{needed} page directories are needed and {MAX_DIRECTORIES} may be named") } @@ -305,15 +318,8 @@ impl<'a> Plan<'a> { /// identity or the first fetch after the switch faults. It is plain memory, /// so its pages are rounded out both ways and typed as the rest of memory. /// - /// `physical_bits` is the CPU's physical address width: a byte of either - /// at or past `1 << physical_bits` is [`Refusal::PastWidth`], and one past - /// [`DIRECT_MAP_WINDOW`] [`Refusal::PastWindow`]. - pub fn new( - scanout: Option<(u64, u64)>, - loader: (u64, u64), - typing: Typing<'a>, - physical_bits: u32, - ) -> Result { + /// A byte of either past [`DIRECT_MAP_WINDOW`] is [`Refusal::PastWindow`]. + pub fn new(scanout: Option<(u64, u64)>, loader: (u64, u64), typing: Typing<'a>) -> Result { let mut plan = Self { gibs: [0; MAX_DIRECTORIES], directories: 0, @@ -322,10 +328,10 @@ impl<'a> Plan<'a> { fine: [0; FINE_TABLES], fines: 0, scanout: None, - loader: whole_pages(loader.0, loader.1, physical_bits)?, + loader: whole_pages(loader.0, loader.1)?, typing, }; - let scanout = scanout.map(|(base, len)| plan.scanout_extent(base, len, physical_bits)).transpose()?; + let scanout = scanout.map(|(base, len)| plan.scanout_extent(base, len)).transpose()?; let spans = [ scanout.map(|(base, end)| (base / PAGE_2M * PAGE_2M, end)), Some((plan.loader.0, plan.loader.0 + plan.loader.1)), @@ -378,7 +384,7 @@ impl<'a> Plan<'a> { /// The scanout's extent as the map gives it, `(base, end)`, or why none can /// be given. - fn scanout_extent(&self, base: u64, len: u64, physical_bits: u32) -> Result<(u64, u64), Refusal> { + fn scanout_extent(&self, base: u64, len: u64) -> Result<(u64, u64), Refusal> { let granule = match self.typing { Typing::Firmware => PAGE_2M, Typing::ByMap(_) => PAGE_4K, @@ -388,7 +394,7 @@ impl<'a> Plan<'a> { } let end = base.checked_add(len).ok_or(Refusal::Extent { base, len })?; let end = end.checked_next_multiple_of(granule).ok_or(Refusal::Extent { base, len })?; - reachable(end, physical_bits)?; + within_window(end)?; Ok((base, end)) } @@ -407,6 +413,38 @@ impl<'a> Plan<'a> { self.scanout = Some((base, end - base)); } + /// Write this map into `pool`, a zeroed pool whose first byte is at + /// physical address `at`, and return its root's: the root first, then + /// [`Plan::regions`]' tables, the directories and the split pages' tables, + /// each in its accessor's order. + pub fn write(&self, encoding: Encoding, pool: &mut [Table; MAX_PAGES], at: u64) -> u64 { + let first_directory = 1 + self.regions; + let first_fine = first_directory + self.directories; + let phys = |page: usize| at + page as u64 * PAGE_4K; + for (r, ®ion) in self.regions().iter().enumerate() { + let entry = (encoding.table)(phys(1 + r)); + pool[0].0[ROOT_IDENTITY + region as usize] = entry; + pool[0].0[ROOT_HIGH_HALF + region as usize] = entry; + } + for (d, (region, index)) in self.directory_slots().enumerate() { + pool[1 + region].0[index] = (encoding.table)(phys(first_directory + d)); + } + for (f, (directory, index)) in self.fine_slots().enumerate() { + pool[first_directory + directory].0[index] = (encoding.table)(phys(first_fine + f)); + } + for entry in self.entries() { + match entry.slot { + Slot::Directory { directory, index } => { + pool[first_directory + directory].0[index] = (encoding.block)(entry.phys, entry.cache) + } + Slot::Fine { table, index } => { + pool[first_fine + table].0[index] = (encoding.page)(entry.phys, entry.cache) + } + } + } + phys(0) + } + /// The 512 GiB each second-level table covers, by number `r`, in the order /// a builder allocates them: each is named from root slots /// [`ROOT_IDENTITY`] + `r` and [`ROOT_HIGH_HALF`] + `r`. @@ -540,23 +578,19 @@ impl<'a> Plan<'a> { /// `base..base + len` as whole 2 MiB pages, rounded out both ways: `(first /// page, bytes covered)`. An empty range has no page to be in and is refused. -fn whole_pages(base: u64, len: u64, physical_bits: u32) -> Result<(u64, u64), Refusal> { +fn whole_pages(base: u64, len: u64) -> Result<(u64, u64), Refusal> { if len == 0 { return Err(Refusal::Extent { base, len }); } let first = base / PAGE_2M * PAGE_2M; let end = base.checked_add(len).ok_or(Refusal::Extent { base, len })?; let end = end.checked_next_multiple_of(PAGE_2M).ok_or(Refusal::Extent { base, len })?; - reachable(end, physical_bits)?; + within_window(end)?; Ok((first, end - first)) } -/// Whether a range ending at `end` is one this CPU can address and both views -/// can hold. -fn reachable(end: u64, physical_bits: u32) -> Result<(), Refusal> { - if physical_bits < u64::BITS && end > 1 << physical_bits { - return Err(Refusal::PastWidth { end, bits: physical_bits }); - } +/// Whether a range ending at `end` is one both views can hold. +fn within_window(end: u64) -> Result<(), Refusal> { if end > DIRECT_MAP_WINDOW { return Err(Refusal::PastWindow(end)); } diff --git a/toyos-bootmap/src/x86_64.rs b/toyos-bootmap/src/x86_64.rs index eb942428f2e..e8ac240cb75 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, DirectMapEnd, Refusal, BOOT_MAP_BYTES, DIRECT_MAP_WINDOW, PAGE_2M}; +use crate::{is_read_as_memory, Cache, DirectMapEnd, Encoding, 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, @@ -56,3 +56,6 @@ pub const fn block(phys: u64, cache: Cache) -> u64 { pub const fn page(phys: u64, cache: Cache) -> u64 { block(phys, cache) & !PAGE_SIZE } + +/// The three, for [`Plan::write`](crate::Plan::write). +pub const ENCODING: Encoding = Encoding { table, block, page }; diff --git a/toyos-bootmap/tests/plan.rs b/toyos-bootmap/tests/plan.rs index 616fec4d4a4..c55492552b2 100644 --- a/toyos-bootmap/tests/plan.rs +++ b/toyos-bootmap/tests/plan.rs @@ -12,9 +12,6 @@ const GIB: u64 = 1 << 30; const PANEL: u64 = 0x7e9000; /// The loader as OVMF loads it on a 2 GiB guest: relocated low, and not on a page. const LOADER: (u64, u64) = (0x7e5f_0000, 0x3_5000); -/// A CPU of 48 physical address bits, wider than any address these tests name -/// but the ones that ask for the width itself. -const BITS: u32 = 48; /// Every leaf lands in a table the plan named, at an index inside it, no two /// land in the same place, and no leaf lands where a split page's table is @@ -66,7 +63,7 @@ fn directory(e: &Entry) -> usize { #[test] fn a_machine_with_no_framebuffer_is_the_low_map_and_nothing_else() { - let plan = Plan::new(None, LOADER, Typing::Firmware, BITS).expect("the low map alone"); + let plan = Plan::new(None, LOADER, Typing::Firmware).expect("the low map alone"); assert_eq!(plan.directories(), [0, 1, 2, 3]); assert_eq!(plan.scanout(), None); assert_eq!(plan.entries().count() as u64, BOOT_MAP_BYTES / PAGE_2M); @@ -78,7 +75,7 @@ fn a_machine_with_no_framebuffer_is_the_low_map_and_nothing_else() { /// retypes pages the low map already holds. #[test] fn a_framebuffer_inside_the_low_map_adds_no_directory() { - let plan = Plan::new(Some((0xc000_0000, PANEL)), LOADER, Typing::Firmware, BITS).expect("inside the low map"); + let plan = Plan::new(Some((0xc000_0000, PANEL)), LOADER, Typing::Firmware).expect("inside the low map"); assert_eq!(plan.directories(), [0, 1, 2, 3]); // Rounded up to whole pages, and up only. assert_eq!(plan.scanout(), Some((0xc000_0000, 0x80_0000))); @@ -92,7 +89,7 @@ fn a_framebuffer_inside_the_low_map_adds_no_directory() { /// the low map untouched. #[test] fn a_framebuffer_above_the_low_map_adds_its_own_directory() { - let plan = Plan::new(Some((256 * GIB, PANEL)), LOADER, Typing::Firmware, BITS).expect("above the low map"); + let plan = Plan::new(Some((256 * GIB, PANEL)), LOADER, Typing::Firmware).expect("above the low map"); assert_eq!(plan.directories(), [0, 1, 2, 3, 256]); assert_eq!(plan.scanout(), Some((256 * GIB, 0x80_0000))); let mine: Vec = plan @@ -112,13 +109,13 @@ fn a_framebuffer_above_the_low_map_adds_its_own_directory() { /// past it, not the last GiB of it. #[test] fn a_framebuffer_at_the_boundary_is_outside_the_low_map() { - let plan = Plan::new(Some((BOOT_MAP_BYTES, PANEL)), LOADER, Typing::Firmware, BITS).expect("at the boundary"); + let plan = Plan::new(Some((BOOT_MAP_BYTES, PANEL)), LOADER, Typing::Firmware).expect("at the boundary"); assert_eq!(plan.directories(), [0, 1, 2, 3, 4]); assert!(plan.entries().filter(|e| e.cache == Cache::Scanout).all(|e| directory(&e) == 4)); is_consistent(&plan); // One page below it is the low map's last page, and adds nothing. - let inside = Plan::new(Some((BOOT_MAP_BYTES - PAGE_2M, PAGE_2M)), LOADER, Typing::Firmware, BITS).expect("the last page"); + let inside = Plan::new(Some((BOOT_MAP_BYTES - PAGE_2M, PAGE_2M)), LOADER, Typing::Firmware).expect("the last page"); assert_eq!(inside.directories(), [0, 1, 2, 3]); is_consistent(&inside); } @@ -129,7 +126,7 @@ fn a_framebuffer_at_the_boundary_is_outside_the_low_map() { #[test] fn a_framebuffer_that_straddles_the_low_maps_end_is_mapped_from_both_arms() { let base = BOOT_MAP_BYTES - PAGE_2M; - let plan = Plan::new(Some((base, 4 * PAGE_2M)), LOADER, Typing::Firmware, BITS).expect("across the end"); + let plan = Plan::new(Some((base, 4 * PAGE_2M)), LOADER, Typing::Firmware).expect("across the end"); assert_eq!(plan.directories(), [0, 1, 2, 3, 4]); let mine: Vec<(u64, usize)> = plan .entries() @@ -155,7 +152,7 @@ fn a_framebuffer_that_straddles_the_low_maps_end_is_mapped_from_both_arms() { #[test] fn a_framebuffer_that_straddles_a_gib_claims_both() { let base = 8 * GIB - PAGE_2M; - let plan = Plan::new(Some((base, 4 * PAGE_2M)), LOADER, Typing::Firmware, BITS).expect("straddling"); + let plan = Plan::new(Some((base, 4 * PAGE_2M)), LOADER, Typing::Firmware).expect("straddling"); assert_eq!(plan.directories(), [0, 1, 2, 3, 7, 8]); assert!(plan.fine_tables().is_empty()); is_consistent(&plan); @@ -163,7 +160,7 @@ fn a_framebuffer_that_straddles_a_gib_claims_both() { // With a loader straddling another, every directory the budget has; the // two split-page tables are a map-typed plan's, and this one splits nothing. let loader = (12 * GIB - 0x1000, 0x3_5000); - let full = Plan::new(Some((base, 4 * PAGE_2M)), loader, Typing::Firmware, BITS).expect("both straddling"); + let full = Plan::new(Some((base, 4 * PAGE_2M)), loader, Typing::Firmware).expect("both straddling"); assert_eq!(full.directories(), [0, 1, 2, 3, 7, 8, 11, 12]); assert_eq!(full.regions(), [0]); is_consistent(&full); @@ -180,7 +177,7 @@ fn the_high_half_slot_is_the_top_nine_bits_of_phys_offset() { #[test] fn a_base_off_the_page_is_refused_rather_than_rounded_down() { - assert_eq!(Plan::new(Some((0xc000_1000, PANEL)), LOADER, Typing::Firmware, BITS), Err(Refusal::Unaligned(0xc000_1000))); + assert_eq!(Plan::new(Some((0xc000_1000, PANEL)), LOADER, Typing::Firmware), Err(Refusal::Unaligned(0xc000_1000))); // The refusal names the address, because a machine owner reads it. assert!(Refusal::Unaligned(0xc000_1000).to_string().contains("0xc0001000")); } @@ -189,11 +186,11 @@ fn a_base_off_the_page_is_refused_rather_than_rounded_down() { fn a_range_no_map_can_hold_is_refused_by_name() { // Its own end does not fit an address, either as it is given... let base = u64::MAX - PAGE_2M + 1; - assert_eq!(Plan::new(Some((base, u64::MAX)), LOADER, Typing::Firmware, BITS), Err(Refusal::Extent { base, len: u64::MAX })); + assert_eq!(Plan::new(Some((base, u64::MAX)), LOADER, Typing::Firmware), Err(Refusal::Extent { base, len: u64::MAX })); // ...or once rounded up to the page it must end on. - assert_eq!(Plan::new(Some((base, 1)), LOADER, Typing::Firmware, BITS), Err(Refusal::Extent { base, len: 1 })); + assert_eq!(Plan::new(Some((base, 1)), LOADER, Typing::Firmware), Err(Refusal::Extent { base, len: 1 })); // Wider than the directories a plan may name, and told what it would need. - assert_eq!(Plan::new(Some((16 * GIB, 10 * GIB)), LOADER, Typing::Firmware, BITS), Err(Refusal::Directories(14))); + assert_eq!(Plan::new(Some((16 * GIB, 10 * GIB)), LOADER, Typing::Firmware), Err(Refusal::Directories(14))); assert!(Refusal::Directories(14).to_string().contains("14 page directories")); } @@ -220,7 +217,7 @@ fn uncacheable_is_pat_entry_three_and_plain_memory_is_entry_zero() { #[test] fn a_map_typed_plan_is_memory_where_firmware_says_write_back_and_a_device_elsewhere() { let ram = [(0x4000_0000, 0x4000_0000)]; - let plan = Plan::new(None, LOADER, Typing::ByMap(&ram), BITS).expect("virt's map"); + let plan = Plan::new(None, LOADER, Typing::ByMap(&ram)).expect("virt's map"); for entry in plan.entries() { let want = if (0x4000_0000..0x8000_0000).contains(&entry.phys) { Cache::Memory } else { Cache::Device }; assert_eq!(entry.cache, want, "{entry:?}"); @@ -232,26 +229,26 @@ fn a_map_typed_plan_is_memory_where_firmware_says_write_back_and_a_device_elsewh #[test] fn a_page_that_is_part_memory_is_refused_by_address() { let ram = [(0x4000_0000, 0x4000_0000 + 0x1000)]; - assert_eq!(Plan::new(None, LOADER, Typing::ByMap(&ram), BITS), Err(Refusal::Mixed(0x8000_0000))); + assert_eq!(Plan::new(None, LOADER, Typing::ByMap(&ram)), Err(Refusal::Mixed(0x8000_0000))); assert!(Refusal::Mixed(0x8000_0000).to_string().contains("0x80000000")); // Two adjacent ranges that meet inside a page make it whole. let split = [(0x4000_0000, 0x4010_0000 - 0x4000_0000), (0x4010_0000, 0x3ff0_0000)]; - assert!(Plan::new(None, LOADER, Typing::ByMap(&split), BITS).is_ok()); + assert!(Plan::new(None, LOADER, Typing::ByMap(&split)).is_ok()); } /// A scanout inside memory is the scanout's leaves, not a mixed page. #[test] fn a_scanout_inside_memory_is_typed_as_the_scanout() { let ram = [(0x4000_0000, 0x4000_0000)]; - let plan = Plan::new(Some((0x5000_0000, 0x10_0000)), LOADER, Typing::ByMap(&ram), BITS).expect("inside"); + let plan = Plan::new(Some((0x5000_0000, 0x10_0000)), LOADER, Typing::ByMap(&ram)).expect("inside"); let scanout: Vec = plan.entries().filter(|e| e.cache == Cache::Scanout).map(|e| e.phys).collect(); assert_eq!(scanout, (0..256).map(|page| 0x5000_0000 + page * PAGE_4K).collect::>()); is_consistent(&plan); // And a scanout over a page that is part memory takes that page whole. let part = [(0x4000_0000, 0x1000)]; - assert!(Plan::new(Some((0x4000_0000, PAGE_2M)), LOADER, Typing::ByMap(&part), BITS).is_ok()); - assert_eq!(Plan::new(None, LOADER, Typing::ByMap(&part), BITS), Err(Refusal::Mixed(0x4000_0000))); + assert!(Plan::new(Some((0x4000_0000, PAGE_2M)), LOADER, Typing::ByMap(&part)).is_ok()); + assert_eq!(Plan::new(None, LOADER, Typing::ByMap(&part)), Err(Refusal::Mixed(0x4000_0000))); } /// The AArch64 descriptors, against the Arm ARM's bit positions (K.a, D8.3.1 @@ -293,7 +290,7 @@ fn the_aarch64_descriptors_carry_the_attribute_index_the_mair_names() { fn a_scanout_carved_out_of_ram_splits_the_pages_it_shares() { let ram = [(0x4000_0000, 0x8000_0000)]; let (base, len) = (0xbc7a_0000, 0x30_0000); - let plan = Plan::new(Some((base, len)), LOADER, Typing::ByMap(&ram), BITS).expect("ramfb"); + let plan = Plan::new(Some((base, len)), LOADER, Typing::ByMap(&ram)).expect("ramfb"); assert_eq!(plan.fine_tables(), [0xbc60_0000, 0xbca0_0000]); let scanout: Vec = plan.entries().filter(|e| e.cache == Cache::Scanout).collect(); let bytes: u64 = scanout @@ -312,7 +309,7 @@ fn a_scanout_carved_out_of_ram_splits_the_pages_it_shares() { assert_eq!(at(0xbc80_0000), Cache::Scanout); is_consistent(&plan); // The same framebuffer under range-register typing is refused, as before. - assert_eq!(Plan::new(Some((base, len)), LOADER, Typing::Firmware, BITS), Err(Refusal::Unaligned(base))); + assert_eq!(Plan::new(Some((base, len)), LOADER, Typing::Firmware), Err(Refusal::Unaligned(base))); } /// A map-typed scanout must still start on a 4 KiB page. @@ -320,7 +317,7 @@ fn a_scanout_carved_out_of_ram_splits_the_pages_it_shares() { fn a_map_typed_scanout_off_a_small_page_is_refused() { let ram = [(0x4000_0000, 0x8000_0000)]; assert_eq!( - Plan::new(Some((0xbc7a_0800, 0x1000)), LOADER, Typing::ByMap(&ram), BITS), + Plan::new(Some((0xbc7a_0800, 0x1000)), LOADER, Typing::ByMap(&ram)), Err(Refusal::Unaligned(0xbc7a_0800)) ); } @@ -331,7 +328,7 @@ fn a_map_typed_scanout_off_a_small_page_is_refused() { #[test] fn a_loader_above_the_low_map_adds_its_own_directory_of_plain_memory() { let loader = (0x1_4000_0000, 0x3_4a00); - let plan = Plan::new(None, loader, Typing::Firmware, BITS).expect("above the low map"); + let plan = Plan::new(None, loader, Typing::Firmware).expect("above the low map"); assert_eq!(plan.directories(), [0, 1, 2, 3, 5]); assert_eq!(plan.loader(), (0x1_4000_0000, PAGE_2M)); let mine: Vec<(u64, usize, Cache)> = plan @@ -346,7 +343,7 @@ fn a_loader_above_the_low_map_adds_its_own_directory_of_plain_memory() { /// A loader inside the low map is already mapped, and adds nothing. #[test] fn a_loader_inside_the_low_map_adds_nothing() { - let plan = Plan::new(None, LOADER, Typing::Firmware, BITS).expect("inside"); + let plan = Plan::new(None, LOADER, Typing::Firmware).expect("inside"); assert_eq!(plan.directories(), [0, 1, 2, 3]); assert_eq!(plan.entries().count() as u64, BOOT_MAP_BYTES / PAGE_2M); assert_eq!(plan.loader(), (0x7e40_0000, 2 * PAGE_2M)); @@ -357,7 +354,7 @@ fn a_loader_inside_the_low_map_adds_nothing() { #[test] fn a_loader_across_a_page_is_both_pages() { let loader = (6 * GIB + PAGE_2M - 0x1000, 0x3_4a00); - let plan = Plan::new(None, loader, Typing::Firmware, BITS).expect("across a page"); + let plan = Plan::new(None, loader, Typing::Firmware).expect("across a page"); assert_eq!(plan.loader(), (6 * GIB, 2 * PAGE_2M)); let mine: Vec = plan.entries().filter(|e| e.phys >= BOOT_MAP_BYTES).map(|e| e.phys).collect(); assert_eq!(mine, [6 * GIB, 6 * GIB + PAGE_2M]); @@ -368,7 +365,7 @@ fn a_loader_across_a_page_is_both_pages() { /// scanout holds is not written twice. #[test] fn a_loader_beside_the_scanout_shares_its_directory() { - let plan = Plan::new(Some((256 * GIB, PANEL)), (256 * GIB + 0x7f_0000, 0x3_4a00), Typing::Firmware, BITS) + let plan = Plan::new(Some((256 * GIB, PANEL)), (256 * GIB + 0x7f_0000, 0x3_4a00), Typing::Firmware) .expect("one GiB for both"); assert_eq!(plan.directories(), [0, 1, 2, 3, 256]); let above: Vec<(u64, Cache)> = @@ -391,14 +388,14 @@ fn a_loader_beside_the_scanout_shares_its_directory() { #[test] fn a_map_typed_loader_is_memory() { let ram = [(0x4000_0000, 0x4000_0000), (5 * GIB, GIB)]; - let plan = Plan::new(None, (5 * GIB + 0x1000, 0x3_4a00), Typing::ByMap(&ram), BITS).expect("in memory"); + let plan = Plan::new(None, (5 * GIB + 0x1000, 0x3_4a00), Typing::ByMap(&ram)).expect("in memory"); let above: Vec<(u64, Cache)> = plan.entries().filter(|e| e.phys >= BOOT_MAP_BYTES).map(|e| (e.phys, e.cache)).collect(); assert_eq!(above, [(5 * GIB, Cache::Memory)]); is_consistent(&plan); let part = [(0x4000_0000, 0x4000_0000), (5 * GIB, 0x10_0000)]; assert_eq!( - Plan::new(None, (5 * GIB, 0x3_4a00), Typing::ByMap(&part), BITS), + Plan::new(None, (5 * GIB, 0x3_4a00), Typing::ByMap(&part)), Err(Refusal::Mixed(5 * GIB)) ); } @@ -406,16 +403,16 @@ fn a_map_typed_loader_is_memory() { #[test] fn a_loader_no_map_can_hold_is_refused_by_name() { assert_eq!( - Plan::new(None, (0x1_4000_0000, 0), Typing::Firmware, BITS), + Plan::new(None, (0x1_4000_0000, 0), Typing::Firmware), Err(Refusal::Extent { base: 0x1_4000_0000, len: 0 }) ); // Its directories count against the same budget as the scanout's, and a // GiB both need is counted once. assert_eq!( - Plan::new(Some((16 * GIB, 4 * GIB)), (32 * GIB, 0x3_4a00), Typing::Firmware, BITS), + Plan::new(Some((16 * GIB, 4 * GIB)), (32 * GIB, 0x3_4a00), Typing::Firmware), Err(Refusal::Directories(9)) ); - let shared = Plan::new(Some((16 * GIB, 4 * GIB)), (17 * GIB, 0x3_4a00), Typing::Firmware, BITS) + let shared = Plan::new(Some((16 * GIB, 4 * GIB)), (17 * GIB, 0x3_4a00), Typing::Firmware) .expect("the loader inside the scanout's GiBs"); assert_eq!(shared.directories(), [0, 1, 2, 3, 16, 17, 18, 19]); is_consistent(&shared); @@ -427,7 +424,7 @@ fn a_loader_no_map_can_hold_is_refused_by_name() { #[test] fn a_framebuffer_past_512_gib_adds_its_own_second_level_table() { let base = 0xfc_9000_0000; - let plan = Plan::new(Some((base, 8_294_400)), LOADER, Typing::Firmware, BITS).expect("at 1010 GiB"); + let plan = Plan::new(Some((base, 8_294_400)), LOADER, Typing::Firmware).expect("at 1010 GiB"); assert_eq!(base / GIB, 1010); assert_eq!(plan.directories(), [0, 1, 2, 3, 1010]); assert_eq!(plan.regions(), [0, 1]); @@ -443,7 +440,7 @@ fn a_framebuffer_past_512_gib_adds_its_own_second_level_table() { #[test] fn a_framebuffer_across_512_gib_names_both_second_level_tables() { let base = 512 * GIB - PAGE_2M; - let plan = Plan::new(Some((base, 4 * PAGE_2M)), (520 * GIB, 0x3_4a00), Typing::Firmware, BITS) + let plan = Plan::new(Some((base, 4 * PAGE_2M)), (520 * GIB, 0x3_4a00), Typing::Firmware) .expect("across 512 GiB"); assert_eq!(plan.directories(), [0, 1, 2, 3, 511, 512, 520]); assert_eq!(plan.regions(), [0, 1]); @@ -458,53 +455,25 @@ fn a_framebuffer_across_512_gib_names_both_second_level_tables() { fn every_second_level_table_the_budget_has_fits_the_pool() { let scanout = (1024 * GIB - PAGE_2M, 4 * PAGE_2M); let loader = (DIRECT_MAP_WINDOW - 512 * GIB - 0x1000, 0x3_4a00); - let plan = Plan::new(Some(scanout), loader, Typing::Firmware, BITS).expect("both straddling"); + let plan = Plan::new(Some(scanout), loader, Typing::Firmware).expect("both straddling"); assert_eq!(plan.regions(), [0, 1, 2, 254, 255]); assert_eq!(1 + plan.regions().len() + plan.directories().len() + plan.fine_tables().len(), MAX_PAGES - 2); is_consistent(&plan); } -/// The CPU's width is the map's reach: a range ending on its last byte fits, -/// and one a page past it is refused by name, for the scanout and the loader. +/// Past the high half's 128 TiB no view can hold a range: the identity view +/// would reach into the high half's slots. #[test] -fn a_range_past_the_physical_address_width_is_refused_by_name() { - let top = 1 << 40; - let last = Plan::new(Some((top - 4 * PAGE_2M, 4 * PAGE_2M)), LOADER, Typing::Firmware, 40).expect("the last bytes"); - assert_eq!(last.directories(), [0, 1, 2, 3, 1023]); - assert_eq!(last.regions(), [0, 1]); - is_consistent(&last); - assert_eq!( - Plan::new(Some((top - 2 * PAGE_2M, 4 * PAGE_2M)), LOADER, Typing::Firmware, 40), - Err(Refusal::PastWidth { end: top + 2 * PAGE_2M, bits: 40 }) - ); +fn a_range_past_the_window_is_refused_by_name() { assert_eq!( - Plan::new(None, (top - 0x1000, 0x3_4a00), Typing::Firmware, 40), - Err(Refusal::PastWidth { end: top + PAGE_2M, bits: 40 }) - ); - let said = Refusal::PastWidth { end: top + PAGE_2M, bits: 40 }.to_string(); - assert!(said.contains("0x10000200000") && said.contains("40 bits"), "{said}"); -} - -/// Past the high half's 128 TiB no view can hold a range, whatever the CPU's -/// width: the identity view would reach into the high half's slots. -#[test] -fn a_range_past_the_window_is_refused_however_wide_the_cpu() { - assert_eq!( - Plan::new(Some((DIRECT_MAP_WINDOW, PANEL)), LOADER, Typing::Firmware, 52), + Plan::new(Some((DIRECT_MAP_WINDOW, PANEL)), LOADER, Typing::Firmware), Err(Refusal::PastWindow(DIRECT_MAP_WINDOW + 0x80_0000)) ); assert_eq!( - Plan::new(None, (DIRECT_MAP_WINDOW - 0x1000, 0x3_4a00), Typing::Firmware, 52), + Plan::new(None, (DIRECT_MAP_WINDOW - 0x1000, 0x3_4a00), Typing::Firmware), Err(Refusal::PastWindow(DIRECT_MAP_WINDOW + PAGE_2M)) ); - let last = Plan::new(Some((DIRECT_MAP_WINDOW - 0x80_0000, PANEL)), LOADER, Typing::Firmware, 52).expect("the window's last GiB"); + let last = Plan::new(Some((DIRECT_MAP_WINDOW - 0x80_0000, PANEL)), LOADER, Typing::Firmware).expect("the window's last GiB"); assert_eq!(last.regions(), [0, 255]); is_consistent(&last); } - -/// `PARange`'s encodings as the Arm ARM gives them, and a reserved one refused. -#[test] -fn parange_decodes_to_the_widths_the_arm_arm_names() { - let widths: Vec> = (0..9).map(aarch64::physical_bits).collect(); - assert_eq!(widths, [Some(32), Some(36), Some(40), Some(42), Some(44), Some(48), Some(52), Some(56), None]); -} diff --git a/toyos-bootmap/tests/walk.rs b/toyos-bootmap/tests/walk.rs new file mode 100644 index 00000000000..d2f533dbdaa --- /dev/null +++ b/toyos-bootmap/tests/walk.rs @@ -0,0 +1,185 @@ +//! The tables `Plan::write` lays down, walked as an x86-64 CPU walks them: Intel +//! SDM Vol. 3A §4.5, four-level paging, read from the entries' bits here rather +//! than from the encoder, so a table named from the wrong slot or the wrong +//! second-level table is an address that does not translate. + +use toyos_bootmap::{x86_64, Cache, Plan, Table, Typing, BOOT_MAP_BYTES, MAX_PAGES, PAGE_2M, PAGE_4K}; + +const GIB: u64 = 1 << 30; +const PHYS_OFFSET: u64 = 0xFFFF_8000_0000_0000; +/// Where the pool sits in physical memory: inside the low map, as the loader's +/// allocation does. +const AT: u64 = 0x7f80_0000; +/// The loader as OVMF loads it on a 2 GiB guest. +const LOADER: (u64, u64) = (0x7e5f_0000, 0x3_5000); +/// The AMD laptop's framebuffer, as its GOP reports it. +const LAPTOP: (u64, u64) = (0xfc_9000_0000, 8_294_400); + +/// Bits M-1:12 of an entry that names a page, for the widest M (52) §4.1.4 allows. +const ADDRESS: u64 = 0x000F_FFFF_FFFF_F000; +const P: u64 = 1 << 0; +const RW: u64 = 1 << 1; +const PWT: u64 = 1 << 3; +const PCD: u64 = 1 << 4; +const PS: u64 = 1 << 7; + +/// What a linear address translates to: the physical address, the PAT entry +/// its type comes from (§11.12.3: PAT·4 + PCD·2 + PWT), and whether a +/// supervisor write is allowed (R/W set at every level, §4.6.1). +#[derive(Debug, PartialEq, Eq)] +struct Translation { + phys: u64, + pat: u64, + writable: bool, +} + +/// The pool as physical memory: the 8 bytes at `phys`, which must lie in it. +fn read(pool: &[Table; MAX_PAGES], phys: u64) -> u64 { + assert!((AT..AT + (MAX_PAGES as u64) * PAGE_4K).contains(&phys), "an entry names {phys:#x}, outside the pool"); + let offset = phys - AT; + pool[(offset / PAGE_4K) as usize].0[((offset % PAGE_4K) / 8) as usize] +} + +/// SDM Vol. 3A §4.5.4, Figures 4-8 to 4-10: CR3, then a PML4E by bits 47:39, a +/// PDPTE by 38:30, a PDE by 29:21 and a PTE by 20:12, each found at the +/// address bits of the one above; a PS bit ends the walk at a 1 GiB or 2 MiB +/// page. `None` is a not-present entry at some level. +fn walk(pool: &[Table; MAX_PAGES], cr3: u64, linear: u64) -> Option { + // §3.3.7.1: bits 63:48 copy bit 47. + let high = linear >> 47; + assert!(high == 0 || high == 0x1_FFFF, "{linear:#x} is not canonical"); + let index = |shift: u32| (linear >> shift) & 0x1FF; + let pml4e = read(pool, (cr3 & ADDRESS) | index(39) << 3); + if pml4e & P == 0 { + return None; + } + let pdpte = read(pool, (pml4e & ADDRESS) | index(30) << 3); + if pdpte & P == 0 { + return None; + } + let mut writable = pml4e & pdpte & RW != 0; + if pdpte & PS != 0 { + // Table 4-16: a 1 GiB page, bits 51:30, its PAT bit 12. + let phys = (pdpte & ADDRESS & !(GIB - 1)) | (linear & (GIB - 1)); + return Some(Translation { phys, pat: pat(pdpte, pdpte >> 12 & 1), writable }); + } + let pde = read(pool, (pdpte & ADDRESS) | index(21) << 3); + if pde & P == 0 { + return None; + } + writable &= pde & RW != 0; + if pde & PS != 0 { + // Table 4-17: a 2 MiB page, bits 51:21; bits 20:13 are reserved. + assert_eq!(pde & 0x1F_E000, 0, "PDE {pde:#x} sets a reserved bit"); + let phys = (pde & ADDRESS & !(PAGE_2M - 1)) | (linear & (PAGE_2M - 1)); + return Some(Translation { phys, pat: pat(pde, pde >> 12 & 1), writable }); + } + let pte = read(pool, (pde & ADDRESS) | index(12) << 3); + if pte & P == 0 { + return None; + } + // Table 4-20: a 4 KiB page, bits 51:12, its PAT bit 7. + let phys = (pte & ADDRESS) | (linear & (PAGE_4K - 1)); + Some(Translation { phys, pat: pat(pte, pte >> 7 & 1), writable: writable && pte & RW != 0 }) +} + +fn pat(entry: u64, pat_bit: u64) -> u64 { + pat_bit << 2 | u64::from(entry & PCD != 0) << 1 | u64::from(entry & PWT != 0) +} + +/// `plan` written into a fresh pool at [`AT`], and its root. +fn written(plan: &Plan) -> (Box<[Table; MAX_PAGES]>, u64) { + let mut pool: Box<[Table; MAX_PAGES]> = + vec![Table::EMPTY; MAX_PAGES].into_boxed_slice().try_into().unwrap_or_else(|_| unreachable!()); + let root = plan.write(x86_64::ENCODING, &mut pool, AT); + (pool, root) +} + +/// The PAT entry a leaf of this kind must select: entry 0, whose type is the +/// MTRRs', for memory; entry 3, uncacheable under every MTRR and under the +/// power-on PAT, for a device and the scanout. +fn expected_pat(cache: Cache) -> u64 { + match cache { + Cache::Firmware | Cache::Memory => 0, + Cache::Device | Cache::Scanout => 3, + } +} + +/// Every leaf the plan names translates, at identity and at `PHYS_OFFSET`, its +/// first byte and its last, to itself, writable and of its kind's type. +fn every_leaf_translates_in_both_views(plan: &Plan, pool: &[Table; MAX_PAGES], root: u64) { + for entry in plan.entries() { + let size = match entry.slot { + toyos_bootmap::Slot::Directory { .. } => PAGE_2M, + toyos_bootmap::Slot::Fine { .. } => PAGE_4K, + }; + for phys in [entry.phys, entry.phys + size - 1] { + let want = Translation { phys, pat: expected_pat(entry.cache), writable: true }; + assert_eq!(walk(pool, root, phys).as_ref(), Some(&want), "{entry:?} at identity"); + assert_eq!(walk(pool, root, PHYS_OFFSET + phys).as_ref(), Some(&want), "{entry:?} at PHYS_OFFSET"); + } + } +} + +/// The laptop that stopped in the loader: its scanout at 1010 GiB, under the +/// second 512 GiB's table, reached from both views, while the high half's +/// first slot still reaches GiB 0. +#[test] +fn the_laptops_scanout_at_1010_gib_translates_in_both_views() { + let (base, len) = LAPTOP; + let plan = Plan::new(Some(LAPTOP), LOADER, Typing::Firmware).expect("the laptop's plan"); + let (pool, root) = written(&plan); + assert_eq!(root, AT, "the root is the pool's first page"); + + let scanout = |phys: u64| Some(Translation { phys, pat: 3, writable: true }); + let memory = |phys: u64| Some(Translation { phys, pat: 0, writable: true }); + for phys in [base, base + len - 1] { + assert_eq!(walk(&pool, root, phys), scanout(phys), "identity {phys:#x}"); + assert_eq!(walk(&pool, root, PHYS_OFFSET + phys), scanout(phys), "PHYS_OFFSET + {phys:#x}"); + } + for phys in [0, BOOT_MAP_BYTES - 1, LOADER.0] { + assert_eq!(walk(&pool, root, phys), memory(phys), "identity {phys:#x}"); + assert_eq!(walk(&pool, root, PHYS_OFFSET + phys), memory(phys), "PHYS_OFFSET + {phys:#x}"); + } + // Nothing but what the plan names: past the low map, past the scanout's + // last page in its GiB, and the 512 GiB below it. + let past_scanout = base + len.next_multiple_of(PAGE_2M); + for phys in [BOOT_MAP_BYTES, past_scanout, 512 * GIB, base - PAGE_2M] { + assert_eq!(walk(&pool, root, phys), None, "identity {phys:#x}"); + assert_eq!(walk(&pool, root, PHYS_OFFSET + phys), None, "PHYS_OFFSET + {phys:#x}"); + } + every_leaf_translates_in_both_views(&plan, &pool, root); +} + +/// A scanout across 512 GiB with the loader past it, and the whole budget of +/// second-level tables: each directory under its own 512 GiB's table. +#[test] +fn every_second_level_table_is_reached_from_its_own_root_slots() { + let across = Plan::new(Some((512 * GIB - PAGE_2M, 4 * PAGE_2M)), (520 * GIB, 0x3_4a00), Typing::Firmware) + .expect("across 512 GiB"); + let (pool, root) = written(&across); + every_leaf_translates_in_both_views(&across, &pool, root); + + let budget = Plan::new( + Some((1024 * GIB - PAGE_2M, 4 * PAGE_2M)), + (toyos_bootmap::DIRECT_MAP_WINDOW - 512 * GIB - 0x1000, 0x3_4a00), + Typing::Firmware, + ) + .expect("both straddling"); + assert_eq!(budget.regions(), [0, 1, 2, 254, 255]); + let (pool, root) = written(&budget); + every_leaf_translates_in_both_views(&budget, &pool, root); +} + +/// A scanout carved out of RAM, whose shared pages are split: the 4 KiB tables +/// hang under the right directory, and the bytes beside the scanout stay memory. +#[test] +fn a_split_page_translates_through_its_table_of_small_leaves() { + let ram = [(0x4000_0000, 0x8000_0000)]; + let (base, len) = (0xbc7a_0000, 0x30_0000); + let plan = Plan::new(Some((base, len)), LOADER, Typing::ByMap(&ram)).expect("ramfb"); + let (pool, root) = written(&plan); + assert_eq!(walk(&pool, root, base - 1), Some(Translation { phys: base - 1, pat: 0, writable: true })); + assert_eq!(walk(&pool, root, base), Some(Translation { phys: base, pat: 3, writable: true })); + every_leaf_translates_in_both_views(&plan, &pool, root); +} From d0b9b55c0b2c6d50b873d56cf1a0a0c4fc896674 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 16:02:39 +0200 Subject: [PATCH 3/4] Plan::write zeroes its own pool, and the loader's pool is a plain allocation A stale entry in the pool survived `write` as a mapping, so its result was correct only on a zeroed pool, a precondition a safe function could not enforce. `write` now fills the pool with empty tables before it names any, and the loader allocates it with `alloc`. walk.rs writes into a pool whose every entry is a present one naming memory outside it, so a `write` that leaves an entry it does not set is an entry the walk reads. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- bootloader/src/main.rs | 8 ++++---- toyos-bootmap/src/lib.rs | 5 +++-- toyos-bootmap/tests/walk.rs | 5 +++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index 9c689d55cd0..d35e5a28496 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -473,7 +473,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v // Allocated before exiting boot services, which takes the allocator away. let pt_layout = Layout::new::<[Table; MAX_PAGES]>(); // SAFETY: `pt_layout` has non-zero size. - let pt_mem = unsafe { alloc::alloc::alloc_zeroed(pt_layout) }; + let pt_mem = unsafe { alloc::alloc::alloc(pt_layout) }; assert!(!pt_mem.is_null(), "page table allocation failed"); // Where firmware loaded this image, which is where the x86-64 switch to @@ -513,9 +513,9 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v let plan = planned .unwrap_or_else(|why| panic!("the boot map cannot hold the scanout and the loader: {why}")); - // SAFETY: `pt_mem` is the zeroed allocation of `pt_layout` above, which - // nothing else holds; firmware's map is identity, so its address is the - // pool's physical one. + // SAFETY: `pt_mem` is the allocation of `pt_layout` above, which nothing + // else holds, and `write` stores every entry before it reads any; + // firmware's map is identity, so its address is the pool's physical one. let pool = unsafe { &mut *pt_mem.cast::<[Table; MAX_PAGES]>() }; let pml4_phys = plan.write(arch::encoding::ENCODING, pool, pt_mem as u64); println!("Boot map: root {pml4_phys:#x}, {BOOT_MAP_BYTES:#x} bytes at identity and at PHYS_OFFSET"); diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs index 260b732da9e..3f4f9a5fc41 100644 --- a/toyos-bootmap/src/lib.rs +++ b/toyos-bootmap/src/lib.rs @@ -413,14 +413,15 @@ impl<'a> Plan<'a> { self.scanout = Some((base, end - base)); } - /// Write this map into `pool`, a zeroed pool whose first byte is at - /// physical address `at`, and return its root's: the root first, then + /// Write this map over whatever `pool` holds, its first byte at physical + /// address `at`, and return its root's: the root first, then /// [`Plan::regions`]' tables, the directories and the split pages' tables, /// each in its accessor's order. pub fn write(&self, encoding: Encoding, pool: &mut [Table; MAX_PAGES], at: u64) -> u64 { let first_directory = 1 + self.regions; let first_fine = first_directory + self.directories; let phys = |page: usize| at + page as u64 * PAGE_4K; + pool.fill(Table::EMPTY); for (r, ®ion) in self.regions().iter().enumerate() { let entry = (encoding.table)(phys(1 + r)); pool[0].0[ROOT_IDENTITY + region as usize] = entry; diff --git a/toyos-bootmap/tests/walk.rs b/toyos-bootmap/tests/walk.rs index d2f533dbdaa..5fcaafcdf44 100644 --- a/toyos-bootmap/tests/walk.rs +++ b/toyos-bootmap/tests/walk.rs @@ -87,10 +87,11 @@ fn pat(entry: u64, pat_bit: u64) -> u64 { pat_bit << 2 | u64::from(entry & PCD != 0) << 1 | u64::from(entry & PWT != 0) } -/// `plan` written into a fresh pool at [`AT`], and its root. +/// `plan` written at [`AT`] into a pool whose every entry is a stale present +/// one naming memory outside it, and its root. fn written(plan: &Plan) -> (Box<[Table; MAX_PAGES]>, u64) { let mut pool: Box<[Table; MAX_PAGES]> = - vec![Table::EMPTY; MAX_PAGES].into_boxed_slice().try_into().unwrap_or_else(|_| unreachable!()); + vec![Table([u64::MAX; 512]); MAX_PAGES].into_boxed_slice().try_into().unwrap_or_else(|_| unreachable!()); let root = plan.write(x86_64::ENCODING, &mut pool, AT); (pool, root) } From 24132a8ac079ff6f411433bc9e6d5eea88b3e9dd Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 19:14:14 +0200 Subject: [PATCH 4/4] The loader allocates its boot map pool zeroed, so the reference it forms names valid tables alloc returned uninitialised memory and the loader then formed a &mut [Table; MAX_PAGES] over it, before write's own fill ran. Whether a reference to uninitialised integers is undefined is left open by the Rust reference; alloc_zeroed makes the pointee a valid array of empty tables (all zeroes is a valid u64), which settles it with no added line. Plan::write keeps its own fill as the safe function's contract. Review round 3, NOTE 1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- bootloader/src/main.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index d35e5a28496..c6dd069ee73 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -473,7 +473,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v // Allocated before exiting boot services, which takes the allocator away. let pt_layout = Layout::new::<[Table; MAX_PAGES]>(); // SAFETY: `pt_layout` has non-zero size. - let pt_mem = unsafe { alloc::alloc::alloc(pt_layout) }; + let pt_mem = unsafe { alloc::alloc::alloc_zeroed(pt_layout) }; assert!(!pt_mem.is_null(), "page table allocation failed"); // Where firmware loaded this image, which is where the x86-64 switch to @@ -513,9 +513,10 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v let plan = planned .unwrap_or_else(|why| panic!("the boot map cannot hold the scanout and the loader: {why}")); - // SAFETY: `pt_mem` is the allocation of `pt_layout` above, which nothing - // else holds, and `write` stores every entry before it reads any; - // firmware's map is identity, so its address is the pool's physical one. + // SAFETY: `pt_mem` is the zeroed allocation of `pt_layout` above, which + // nothing else holds, so it is a valid `[Table; MAX_PAGES]` of empty + // tables; firmware's map is identity, so its address is the pool's + // physical one. let pool = unsafe { &mut *pt_mem.cast::<[Table; MAX_PAGES]>() }; let pml4_phys = plan.write(arch::encoding::ENCODING, pool, pt_mem as u64); println!("Boot map: root {pml4_phys:#x}, {BOOT_MAP_BYTES:#x} bytes at identity and at PHYS_OFFSET");