Repository navigation
The boot map reaches every address the high half holds: one second-level table per 512 GiB, shared by both views, written by toyos-bootmap and walked on the host as the SDM walks it - #839
Conversation
…igh 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. e649499'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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo test, the whole guest suite (EXIT=0) at |
|
cargo test, the whole guest suite (EXIT=0) at |
|
cargo run -- --build-only (EXIT=0) at |
|
cargo run -- --build-only --arch aarch64 (EXIT=0) at |
|
cargo test -p toyos-bootmap (EXIT=0) at |
|
metal readback staging, boot:metaldevicecase (EXIT=2: staged, machine untouched) at |
|
Mutations and the negative control at m1 — EXIT=101 (red)diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs
index 830e427..7ffae8a 100644
--- a/toyos-bootmap/src/lib.rs
+++ b/toyos-bootmap/src/lib.rs
@@ -345,7 +345,7 @@ impl<'a> Plan<'a> {
}
}
for at in 0..plan.directories {
- let region = plan.gibs[at] / GIB_PER_PDPT;
+ let region = plan.gibs[at] / GIB_PER_PDPT * 0;
if !plan.spans[..plan.regions].contains(®ion) {
plan.spans[plan.regions] = region;
plan.regions += 1;logm2 — EXIT=101 (red)diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs
index 830e427..20edaa4 100644
--- a/toyos-bootmap/src/lib.rs
+++ b/toyos-bootmap/src/lib.rs
@@ -554,7 +554,7 @@ fn whole_pages(base: u64, len: u64, physical_bits: u32) -> Result<(u64, u64), Re
/// 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 {
+ if false && physical_bits < u64::BITS && end > 1 << physical_bits {
return Err(Refusal::PastWidth { end, bits: physical_bits });
}
if end > DIRECT_MAP_WINDOW {logm3 — EXIT=101 (red)diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs
index 830e427..a19b1e7 100644
--- a/toyos-bootmap/src/lib.rs
+++ b/toyos-bootmap/src/lib.rs
@@ -554,7 +554,7 @@ fn whole_pages(base: u64, len: u64, physical_bits: u32) -> Result<(u64, u64), Re
/// 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 {
+ if physical_bits < u64::BITS && end >= 1 << physical_bits {
return Err(Refusal::PastWidth { end, bits: physical_bits });
}
if end > DIRECT_MAP_WINDOW {logm4 — EXIT=101 (red)diff --git a/toyos-bootmap/src/lib.rs b/toyos-bootmap/src/lib.rs
index 830e427..27b10d7 100644
--- a/toyos-bootmap/src/lib.rs
+++ b/toyos-bootmap/src/lib.rs
@@ -557,7 +557,7 @@ 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 {
+ if false && end > DIRECT_MAP_WINDOW {
return Err(Refusal::PastWindow(end));
}
Ok(())logNegative control: the whole change reverted — EXIT=101 (red)
use toyos_bootmap::{Plan, Typing};
#[test]
fn the_laptops_framebuffer() {
let got = Plan::new(Some((0xfc_9000_0000, 8_294_400)), (0x7e5f_0000, 0x3_5000), Typing::Firmware);
println!("{:?}", got.as_ref().map(|p| p.directories().to_vec()).map_err(|e| e.to_string()));
assert!(got.is_ok(), "{}", got.unwrap_err());
}reverse patchdiff --git a/toyos-bootmap/src/aarch64.rs b/toyos-bootmap/src/aarch64.rs
index 7db15d8a0..2803b1a4f 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<u32> {
+ 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 92f7237bf..830e427a1 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<Self, Refusal> {
+ ///
+ /// `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<Self, Refusal> {
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<Item = (usize, usize)> + '_ {
+ 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 {logbaseline log (EXIT=0) |
|
T14 reading at The staged
Each hash was taken from the staged file and checked in the same command that flashed it. Judge ( The T14's scanout at 256 GiB is still mapped: |
|
Review of #839 at Net lines ( On the QEMU argument: I accept it for the guest tier. QEMU cannot place a GOP framebuffer above 4 GiB, and a loader above 512 GiB would need more than 508 GiB of guest RAM. So no guest test can carry this. The argument does not reach the builder, though. The host negative control shows that the old plan refuses the laptop's numbers. It says nothing about whether the new tables translate BLOCKER
NOTE
SEND BACK |
|
The T14 judge's whole log at |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…host as the SDM walks them; the width check goes 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
cargo test -p toyos-bootmap (EXIT=0) at |
|
cargo run -- --build-only (EXIT=0) at |
|
cargo run -- --build-only --arch aarch64 (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
cargo run -- --ci host (EXIT=0) at |
|
Mutation m4 at
--- a/toyos-bootmap/src/lib.rs
+++ b/toyos-bootmap/src/lib.rs
@@ -421,7 +421,6 @@
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;
set -u
R=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/amdlaptop/highmap/r3
cd /Users/jan/Dev/jan/toyos-highmap
git apply --check $R/m4.patch; echo "CHECK=$?"
git apply $R/m4.patch && git diff
cargo test -p toyos-bootmap; echo "M4 EXIT=$?"
git apply -R $R/m4.patch; echo "RESTORE=$?"
git status --porcelain --ignore-submodules=none; echo "STATUS-END" |
|
cargo test, the whole guest suite (EXIT=0) at |
|
cargo test, the whole guest suite (EXIT=0) at |
|
The target laptop's reading (BLOCKER: hardware). It is from the orchestrator's test image v6 on the AMD Zen 2 laptop: the measurement-only branch |
|
Review of #839 at Earlier BLOCKERs
Merge against current origin/main
BLOCKERNone. NOTE
Net lines ( LAND AFTER NAMED CHANGES |
…rms 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
|
Round 4 gate log at |
#846, #843, #849, #850, #840, #839, #837) into the batch: the icons' and wallpaper's digests hash with toyos-sha2-hw, and the loader's wall clock reads through its own UEFI bindings The batch's merge of main at f72d53d moved #813's wallpaper and #814's icon digest tests onto `toyos_sha2`. #833, already on main, had replaced the root package's `toyos-sha2` dependency with `toyos-sha2-hw`, so CI's merge of the two compiled no `toyos-build` lib test and both the build system's tests and clippy went red with E0432. Both tests now hash through `toyos_sha2_hw`, as every other SHA-256 the build takes does. #842's `bootloader/src/wallclock.rs` was written on the `uefi` crate, which #815 removes; it now calls `efi`'s `RuntimeServices::get_time`, whose `Time` fields are plain and whose error is the `Status` itself. `start_kernel` takes main's `wall_clock` and the batch's `SystemTable`; the batch's `armed_at` goes, as main replaced it with `wallclock::now`. Cargo.lock takes main's `ntapi`; `nonempty` goes with `gix`, which the batch removed and which was its only user (`cargo metadata --offline`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
, #836, #839, #840, #842 through #847, #849 and #850, into consent system.toml: sshserver and shell start both toyfetch (#843) and grants. tests/common/qemu.rs: Profile carries both Desktop and MetalAmdVi (#837). tests/toyos.rs: SCREEN_TESTS carries consent_prompt beside virt_wall_clock_utc (#842) and virt_low_ecam (#840). Beyond the conflict lines: #833 replaced the build's toyos-sha2 dependency with toyos-sha2-hw, so consent_prompt digests its job with toyos_sha2_hw::sha256_digest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
On an AMD laptop, firmware puts the GOP framebuffer near 1 TiB (
fb=0xfc90000000, 1010 GiB). That machine stopped in the loader withBoot map: NO MAP HOLDS THIS MACHINE, GiB 1010 is past the 512 this map's two second-level tables reach. The boot map now reaches every address the high half holds and refuses by name anything past it.toyos-bootmapnow writes the map's tables as well as planning them, and a host test walks them as the CPU does.What changed, per decision
toyos-bootmap/src/lib.rs). The map used to have one PDPT per view, each indexed by GiB, so it reached 512 GiB. A PDPT is position-independent, and both views already shared every page directory. So the plan now names one PDPT for each 512 GiB it reaches (Plan::regions). Each PDPT is named from root slotROOT_IDENTITY + rand fromROOT_HIGH_HALF + r. Each directory is named from its own region's table (Plan::directory_slots). In region 0, the two identical PDPTs become one.MAX_REGIONS = 1 + SCANOUT_DIRECTORIES + LOADER_DIRECTORIES: one for the low map, plus one per directory past it, because a directory lies in exactly one 512 GiB.MAX_PAGES = 1 + MAX_REGIONS + MAX_DIRECTORIES + FINE_TABLES, which is 16 (it was 15).toyos-bootmapwrites the tables (review round 1, BLOCKER 1).Plan::write(encoding, pool, at)lays the plan into a[Table; MAX_PAGES]whose first byte is at physical addressat, and returns the root's address. The order is: the root, then the regions' tables, the directories, and the split pages' tables.Tableis#[repr(C, align(4096))], so the type itself holds the 4 KiB alignment an entry's address bits need. Each architecture's module exports its three encoders asENCODING. The loader'sunsafe fn build_boot_page_tablesis deleted. What stays unsafe in the loader is one cast: itsalloc_zeroedofLayout::new::<[Table; MAX_PAGES]>()to the pool. The crate keeps#![forbid(unsafe_code)], and itsdescriptionnow says it writes the tables.Plan::writezeroes its own pool (review round 2, NOTE). A stale entry in the pool used to survivewriteas a mapping, so a safe function's result was correct only on a zeroed pool.writenow begins withpool.fill(Table::EMPTY).walk.rsnow writes into a pool whose every entry is a stale present one naming memory outside the pool (Table([u64::MAX; 512])), so an entrywriteleaves behind is one the walk reads and refuses.arch::physical_bitson both architectures, the PARange decoder and its test,Refusal::PastWidthand its test, the parameter toPlan::new, and the loader's read and print. Firmware that ran on the CPU cannot hand it a framebuffer or an image past the CPU's own width. A byte past the 128 TiB the high half holds is refused asRefusal::PastWindow, because the identity view would otherwise run into the high half's slots.PastPdptis gone.allocleft the loader forming&mut [Table; MAX_PAGES]over uninitialised memory beforewrite's fill ran, and whether such a reference is undefined is left open by the Rust reference.alloc_zeroedmakes the pointee a valid array of empty tables, since all zeroes is a validu64.write's own fill stays as the safe function's contract, and the SAFETY comment says the pool is zeroedTables.… in N page directories under M second-level tables.The kernel: nothing assumes 512 GiB, so nothing changed and nothing is filed
kernel/src/arch/x86_64/paging.rs::init): installs root slotsROOT_HIGH_HALF..=last_slotfor memory up todirect_map_end, bounded byDIRECT_MAP_WINDOW(128 TiB).seal_kernel_halffills every other high slot. The framebuffer at 1010 GiB and the MMIO ranges are not memory types, so they do not movedirect_map_end. The early-heap ceiling for memory past about 120 GiB is already tracked (issues/the-direct-maps-page-directories-come-from-a-512-kib-heap.md), and a laptop's RAM does not reach it.panic_console::remap→paging::map_mmio):map_2m→ensure_tablecreates the root and PDPT entries for any slot, soPHYS_OFFSET + 0xfc90000000(root slot 257) maps the same way the T14's 256 GiB framebuffer does. Beforemm::init, the kernel draws through the boot map's high-half view, which this change now holds at slot 257.0xfca0200000+0xfe00000on this laptop): mapped into the 128 TiB direct map by the samemap_mmio. AArch64'smap_directbuilds tables on demand the same way.Gates at
07811d977origin/mainwas merged at07811d977(26b3684b9, #843). Every command below ran from a clean tree at07811d977.cargo run -- --ci hostcargo run -- --build-onlycargo run -- --build-only --arch aarch64cargo test -p toyos-bootmap(plan 26, walk 3, direct_map 11, aarch64_direct_map 9, doctest 1)The whole guest suite last ran at
967227d8b, before this round's merge andalloc_zeroed:cargo test, 55 of 55, EXIT=0 (part 1/2 part 2/2). The merge queue'sguest / suiteruns it on the merged tree.The T14 judge ran at
be194cd9d, before this round:cargo test --test toyos-build -- --metal --metal-readback <dir> boot:metalcase, EXIT=0,4 passed, 0 failed, 2 boot(s), imagesmetalcase904f91fb…andmetaldevicecaseed0f1e44…(the orchestrator's reading). This round changes the pool's zeroing and the merge, not the tables a plan writes.The independent oracle, the mutations and the negative control
Oracle: Intel SDM Vol. 3A §4.5, four-level paging (
toyos-bootmap/tests/walk.rs). The test writes a plan withx86_64::ENCODINGinto a pool placed at0x7f800000, then walks it from CR3 the way §4.5.4 does: PML4E by bits 47:39, PDPTE by bits 38:30, PDE by bits 29:21, PTE by bits 20:12. A PS bit ends the walk at a 1 GiB or 2 MiB page, and the PDE's reserved bits 20:13 are checked. The memory type is the PAT index from §11.12.3 (PAT·4 + PCD·2 + PWT), and writability is R/W set at every level (§4.6.1). Every one of these is read from the entries' bits by the test itself, not taken from the encoder. On the laptop's numbers (scanout0xfc90000000+8294400, the OVMF loader):PHYS_OFFSET, through PAT entry 3;0,BOOT_MAP_BYTES - 1and the loader translate to themselves in both views, through PAT entry 0 — soPHYS_OFFSET + 0still reaches GiB 0;The same every-leaf walk runs on a scanout across 512 GiB with the loader past it, on the whole budget (regions
[0, 1, 2, 254, 255]), and on a map-typed scanout whose split pages go through 4 KiB tables.Mutations (patches and whole log). Each was applied as a checked patch to
Plan::write, built, tested withcargo test -p toyos-bootmap, and restored in the same script, which left the tree clean:+ region): EXIT=101. The laptop'sPHYS_OFFSET + 0xfc90000000does not translate. Across 512 GiB,PHYS_OFFSET + 0reaches 512 GiB, with PAT entry 3.+ region: EXIT=101.967227d8b,Plan::writewithout itspool.fill(Table::EMPTY)(patch and script, whole log): EXIT=101.the_laptops_scanout_at_1010_gib_translates_in_both_viewspanics atwalk.rs:149, where identity0x100000000, past the low map, translates through a stale 1 GiB entry to0xfffffc0000000with PAT entry 7 when it must not be present. The other two walk tests and all 26 plan tests stay green.m1 to m3 each turn red only in
tests/walk.rs. The 26 plan tests stay green under all three, which bears out the review: nothing before this change reached the writer.Negative control (whole log).
toyos-bootmap/srcwas checked out asorigin/main(e3e21e421) has it, and a scratch test asked it to plan the laptop's scanout. It goes red, EXIT=101, withGiB 1010 is past the 512 this map's two second-level tables reach— the laptop's own line. With the change in place, the same test is green, EXIT=0. This reproduces the bug. It is not the oracle: the SDM walk above is what checks the new tables.Tests
Host, in
toyos-bootmap/tests/plan.rs:[0,1,2,3,1010], regions[0,1], slot(1, 498);is_consistentalso holds every directory to its region's table and index, and keeps each region's two root slots inside their halves.tests/walk.rsis described above.No guest test, because QEMU cannot place what this fixes. Measured with
info pcion QEMU 11.1.1 q35: BAR0 ofVGAandbochs-display, and the VGA BAR0 ofvirtio-vga, are all32 bit prefetchable memory, so OVMF's GOP framebuffer always lies below 4 GiB.ramfband VirtioGpuDxe draw into guest RAM. Putting the loader or a RAM framebuffer past 512 GiB would take more than 508 GiB of guest RAM. A profile with 64-bit BARs placed above 512 GiB would boot through the kernel'smap_mmio, which this branch does not change. Such a test would stay green with this change reverted, so it could not be its negative control. The host walk carries the tables, and the hardware readings below carry the boot.What I am unsure of
wt/toyos-yoga-tpat100e61b9e, which merges967227d8bwith other branches. In order, it shows the loader'sStarting kernel..., theScanout: 0xfc90000000+0x800000 … in 6 page directories under 2 second-level tablesline, the panic console armed at0xfc90000000, and the kernel'sGOP: … at 0xfc90000000withGOP: scanout memory type WC. The kernel logging at all means the CPU loaded this map's root and ran the switch and the kernel's entry through it. The review checked the provenance:967227d8bis an ancestor of100e61b9e, and the image changestoyos-bootmapby one line,is_read_as_memorymadepub, and nothing in the loader's plan, pool,writeorenter_kernellines. The comment's "nothing beyond this branch touches toyos-bootmap" is therefore not exact. The reading is an excerpt: it lacks the whole stick log, the full sha256 and the command that read the stick. This head differs from967227d8bin the pool's allocation (alloc_zeroed) and the merge of26b3684b9; the laptop has not booted it.Plan::writewithaarch64::ENCODING. Its descriptors are checked bythe_aarch64_descriptors_carry_the_attribute_index_the_mair_names, but no VMSAv8 walk runs over its tables.Lines (
git diff --numstat origin/main...07811d977): production +137/−84 (net +53), tests +265/−7. Production grows by the per-region bookkeeping and byPlan::write,TableandEncoding. The loader's builder (48 lines) and the width check are deleted.🤖 Generated with Claude Code
https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C