diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index c7976463980..683f25255a5 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,54 +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 identity_pdpt = alloc_page(); - let high_pdpt = alloc_page(); - let mut directories = [core::ptr::null_mut::(); toyos_bootmap::MAX_DIRECTORIES]; - for (slot, gib) in plan.directories().iter().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); - } - - 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.add(ROOT_IDENTITY) = table(identity_pdpt as u64); - *root.add(ROOT_HIGH_HALF) = table(high_pdpt as u64); - - 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. @@ -518,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"); @@ -550,8 +499,9 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v 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"), } @@ -563,9 +513,12 @@ 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, 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"); 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 7db15d8a091..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,6 +67,9 @@ pub const fn page(phys: u64, cache: Cache) -> u64 { phys | PAGE | AF | attributes(cache) } +/// 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 92f7237bfe4..3f4f9a5fc41 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 @@ -12,9 +13,15 @@ //! is typed ([`Typing`]) and how an entry is encoded ([`x86_64`], //! [`aarch64`]). //! +//! 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 high half can hold, and +//! refuses by name what lies past it. +//! //! 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. +//! 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 @@ -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,28 @@ 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; + +/// 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)] @@ -153,14 +182,12 @@ 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), /// 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 +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::PastPdpt(gib) => write!( - f, - "GiB {gib} is past the {GIB_PER_PDPT} this map's two second-level tables reach" - ), Self::Directories(needed) => { write!(f, "{needed} page directories are needed and {MAX_DIRECTORIES} may be named") } @@ -190,7 +213,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 +288,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,10 +317,14 @@ 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. + /// + /// 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, + spans: [0; MAX_REGIONS], + regions: 0, fine: [0; FINE_TABLES], fines: 0, scanout: None, @@ -320,6 +350,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); } @@ -357,9 +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 })?; - if (end - 1) / GIB >= GIB_PER_PDPT { - return Err(Refusal::PastPdpt((end - 1) / GIB)); - } + within_window(end)?; Ok((base, end)) } @@ -378,11 +413,64 @@ impl<'a> Plan<'a> { self.scanout = Some((base, end - base)); } + /// 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; + 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`. + 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] { @@ -498,12 +586,18 @@ fn whole_pages(base: u64, len: u64) -> Result<(u64, u64), Refusal> { 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)); - } + within_window(end)?; Ok((first, end - first)) } +/// 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)); + } + 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/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 fb98dfa746f..c55492552b2 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; @@ -35,7 +35,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. @@ -147,7 +162,7 @@ fn a_framebuffer_that_straddles_a_gib_claims_both() { let loader = (12 * GIB - 0x1000, 0x3_5000); 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!(3 + full.directories().len() + 2, MAX_PAGES); + assert_eq!(full.regions(), [0]); is_consistent(&full); } @@ -169,8 +184,6 @@ fn a_base_off_the_page_is_refused_rather_than_rounded_down() { #[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 })); @@ -389,7 +402,6 @@ fn a_map_typed_loader_is_memory() { #[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), Err(Refusal::Extent { base: 0x1_4000_0000, len: 0 }) @@ -405,3 +417,63 @@ fn a_loader_no_map_can_hold_is_refused_by_name() { 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).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) + .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).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); +} + +/// 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_window_is_refused_by_name() { + assert_eq!( + 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), + Err(Refusal::PastWindow(DIRECT_MAP_WINDOW + PAGE_2M)) + ); + 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); +} diff --git a/toyos-bootmap/tests/walk.rs b/toyos-bootmap/tests/walk.rs new file mode 100644 index 00000000000..5fcaafcdf44 --- /dev/null +++ b/toyos-bootmap/tests/walk.rs @@ -0,0 +1,186 @@ +//! 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 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([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) +} + +/// 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); +}