Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions bootloader/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions bootloader/src/watchdog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ use toyos_tco::{
use uefi::prelude::*;
use uefi::table::boot::{MemoryDescriptor, PAGE_SIZE};

/// x86-64's 52-bit physical-address ceiling, as `kernel/src/drivers/acpi.rs`
/// bounds the same reads.
/// x86-64's 52-bit physical-address ceiling.
const MAX_PHYS: u64 = 1 << 52;

/// What of the ECAM window this reads: bus 0's thirty-two devices, eight
Expand Down
20 changes: 20 additions & 0 deletions issues/build/no-harness-test-boots-qemus-own-edk2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
status: open
kind: tooling
opened: 2026-09-28
---

# No harness test boots QEMU's own edk2

Every harness boot uses the repository's `ovmf/`. QEMU's own
`edk2-x86_64-code.fd` hands an AMD vCPU a reserved range at
`0xfd00000000..0x10000000000`, which `ovmf/` never names, and a kernel whose
direct map reached every range in the map died there after `pmm:`, and a
survey that offered a 64-bit BAR only the space above that range handed the
NIC over nowhere. The host tests `toyos-bootmap/tests/direct_map.rs` and
`toyos-pci`'s `placement` hold the two rules; nothing boots the
firmware that broke them, so a regression outside those rules is seen by the
first person who boots QEMU's firmware and nobody else.

Owner: orchestrator. Exit condition: a harness test boots QEMU's own edk2 on
the command line the release probe uses and reaches `compositor: ready` and `netd: DHCP: lease`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
status: open
kind: defect
opened: 2026-09-28
---

# `map_mmio` takes an address past the direct map's window

`paging::map_mmio` (`kernel/src/arch/x86_64/paging.rs`) maps `phys` at
`PHYS_OFFSET + phys` and bounds nothing. The direct map's window is
`toyos_bootmap::DIRECT_MAP_WINDOW`, `0x800000000000` (128 TiB): root slots
256 to 511. A firmware-named register base at or past it overflows that sum.
`vtd::window` (`kernel/src/arch/x86_64/vtd/mod.rs`) bounds a DMAR register base
by its own `MAX_PHYS`, `1 << 52`, so a base between the two reaches it; a BAR
in a firmware window past 128 TiB reaches it through `pcidev::probe_dword`.

Nothing has been observed to reach it: every firmware this tree has booted
puts its registers far below.

Owner: orchestrator. Exit condition: `map_mmio` refuses by name an address past
`DIRECT_MAP_WINDOW`, and each firmware-named base is checked against that bound
rather than against the architecture's width.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
status: open
kind: tooling
opened: 2026-09-28
---

# Nothing reds when the BAR survey drops an assigned BAR or a forwarded range

`pcidev::publish` (`kernel/src/pcidev/mod.rs`) feeds `toyos_pci::placement::free_runs`
three extents: the firmware map, every BAR firmware assigned (`taken.push` of
`memory.address()..end`), and every range a bridge forwards
(`taken.push(forwarded)`). The rule is host-tested; the two kernel pushes are
not. Deleting the BAR push still places the NIC at `0xc000200000` on QEMU's
edk2 and at `0x800200000` on `ovmf/`, because the 2 MiB alignment steps over
firmware's BARs there, and `alone_in_its_page` checks only BARs, not the ranges
bridges forward. No machine in reach puts a BAR it hands over behind a bridge.

Owner: orchestrator. Exit condition: a guest test goes red when either push is
deleted — a machine whose firmware places a BAR, or a bridge's forwarded range,
where the first 2 MiB-aligned candidate would otherwise land.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
status: open
kind: defect
opened: 2026-09-28
---

# The direct map's page directories come from a 512 KiB heap

`paging::init` (`kernel/src/arch/x86_64/paging.rs`) builds the direct map
before `alloc::init`, so every table it takes is a `Box` from the early bump
heap (`EARLY_SIZE`, `kernel/src/mm/alloc.rs`): 512 KiB, 128 pages of 4 KiB. The
direct map takes one page directory per GiB it reaches, plus the root and one
second-level table per 512 GiB. A machine whose memory ends past what those
128 pages hold dies in `paging::init` with `memory allocation of 4096 bytes
failed`, the panic QEMU's edk2 produced when the map reached 1 TiB.

The ceiling is an estimate, not a measurement: 128 pages less the root, one
second-level table, whatever the boot allocated before `mm::init`, and the
alignment padding each growth of `AddressSpace::children` leaves before the
next 4 KiB-aligned table. About 120 GiB of memory.

Owner: orchestrator. Exit condition: no table of the direct map comes from the
early heap, whatever the map's end.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: defect
opened: 2026-09-28
---

# A panic inside `mm::init` may not reach the panel

From `paging::init`'s CR3 load to `drivers::panic_console::remap`
(`kernel/src/main.rs`), the panel is written through the kernel's own direct
map, which reaches `toyos_bootmap::x86_64::direct_map_end`: the boot map's
4 GiB, or the end of the highest memory range. A scanout past that end has no
mapping until `remap` maps it, so a panic in that span — `alloc::init`, and
`paging::seal_kernel_half`'s up to 255 table allocations — faults on the first
pixel instead of painting. The extent before it reached every descriptor in
the map, so a scanout the map describes was covered.

Nothing has been observed to reach it: every firmware this tree has booted
puts its scanout below 4 GiB.

Owner: orchestrator. Exit condition: the scanout is mapped in the kernel's
tables before the CR3 load that makes them live, and a boot actuator that
panics between `paging::init` and `remap` with the scanout above the direct map
paints its report.
4 changes: 4 additions & 0 deletions kernel/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions kernel/src/arch/aarch64/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use toyos_abi::boot::{KernelArgs, MemoryMapEntry};
use toyos_acpi::MadtEntry;

use super::control_regs as regs;
use crate::drivers::acpi::DirectPhys;
use crate::drivers::acpi::direct_phys;
use crate::log;
use crate::mm::Region;

Expand Down Expand Up @@ -179,7 +179,7 @@ pub fn after_console(args: &KernelArgs, maps: &[MemoryMapEntry]) {
/// The MADT's GIC structures and the GTDT's timers, decoded and said: what
/// the interrupt controller and the timer of stage 4 are built from.
fn survey(rsdp_addr: u64) {
match toyos_acpi::find_table(DirectPhys, rsdp_addr, b"APIC", toyos_acpi::MADT_ENTRIES) {
match toyos_acpi::find_table(direct_phys(), rsdp_addr, b"APIC", toyos_acpi::MADT_ENTRIES) {
Ok(madt) => {
let (mut cpus, mut enabled) = (0u32, 0u32);
for item in toyos_acpi::madt_entries(&madt) {
Expand Down Expand Up @@ -221,7 +221,7 @@ fn survey(rsdp_addr: u64) {
}
Err(e) => log!("ACPI: MADT unusable: {e:?}"),
}
match toyos_acpi::gtdt(DirectPhys, rsdp_addr) {
match toyos_acpi::gtdt(direct_phys(), rsdp_addr) {
Ok(gtdt) => log!(
"ACPI: GTDT timers: EL1 physical GSIV {}, EL1 virtual GSIV {}, EL2 GSIV {} ({})",
gtdt.non_secure_el1.gsiv,
Expand Down
4 changes: 2 additions & 2 deletions kernel/src/arch/aarch64/console_uart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use core::sync::atomic::{AtomicU64, Ordering};

use toyos_acpi::{SerialInterface, GAS_SYSTEM_MEMORY};

use crate::drivers::acpi::DirectPhys;
use crate::drivers::acpi::direct_phys;
use crate::log;
use crate::mm::{DirectMap, Mmio};

Expand All @@ -35,7 +35,7 @@ fn regs() -> Mmio {

/// Find the UART SPCR names and answer whether it is one this file drives.
pub fn init(rsdp_addr: u64) -> bool {
let spcr = match toyos_acpi::spcr(DirectPhys, rsdp_addr) {
let spcr = match toyos_acpi::spcr(direct_phys(), rsdp_addr) {
Ok(spcr) => spcr,
Err(e) => {
log!("serial: no console UART, because the SPCR is unusable: {e:?}");
Expand Down
6 changes: 5 additions & 1 deletion kernel/src/arch/aarch64/paging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,11 @@ pub fn map_mmio(_phys: u64, _size: u64, _policy: MmioPolicy) -> crate::mm::Mmio
owed!("the kernel's page tables", "stage 4")
}

pub(crate) fn init(_memory_map: &[MemoryMapEntry]) {
pub(crate) fn init(_memory_map: &[MemoryMapEntry]) -> toyos_bootmap::DirectMapEnd {
owed!("the kernel's page tables", "stage 4")
}

pub(crate) fn seal_kernel_half() {
owed!("the kernel's page tables", "stage 4")
}

Expand Down
67 changes: 48 additions & 19 deletions kernel/src/arch/x86_64/paging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::arch::control_regs::PcidActive;
use crate::arch::cpu::Invpcid;
use crate::sync::Lock;
use crate::vma::{self, Occupancy, Region, RegionKind};
use toyos_bootmap::ROOT_HIGH_HALF;
use toyos_userbound::PageSpan;
use crate::MemoryMapEntry;

Expand Down Expand Up @@ -381,10 +382,14 @@ impl AddressSpace {
let kernel_as = kernel().lock();
let mut pml4 = Box::new(PageTablePage([0; 512]));

for i in 256..512 {
if kernel_as.root[i] & PAGE_PRESENT != 0 {
pml4.init_entry(i, kernel_as.root[i]);
}
for slot in ROOT_HIGH_HALF..512 {
let entry = kernel_as.root[slot];
assert!(
entry & PAGE_PRESENT != 0,
"new_user: kernel root slot {slot} is absent, and this space would never see what is \
mapped there: `seal_kernel_half` runs before the first user space"
);
pml4.init_entry(slot, entry);
}

Some(Self {
Expand Down Expand Up @@ -783,9 +788,8 @@ impl AddressSpace {
flush_tlb_all();
}

/// Replaces whatever is there — the boot map covers every physical
/// address, so an MMIO window's target is pre-mapped by the time its
/// driver asks; a page `guard_4k` already split must not reach here.
/// Replaces whatever is there; a page `guard_4k` already split must not
/// reach here.
fn map_2m(&mut self, phys: u64, flags: u64) {
let virt = crate::mm::DirectMap::from_phys(phys).as_ptr::<u8>() as u64;
let pd_idx = indices(virt).2;
Expand Down Expand Up @@ -839,9 +843,17 @@ impl AddressSpace {
// SAFETY: same argument as `pdpt` above, one level down.
unsafe { PageTablePage::from_phys_mut(pdpt[pdpt_idx] & ADDR_MASK) }
}
}

const MIN_PHYS_MAP: u64 = 4 * 1024 * 1024 * 1024;
/// An empty second-level table under the kernel's empty root slot `slot`.
fn install_root(&mut self, slot: usize) {
let child = Box::new(PageTablePage([0; 512]));
let va = crate::mm::PHYS_OFFSET + ((slot - ROOT_HIGH_HALF) as u64) * (1 << 39);
self.root
.write(slot, va, child.phys() | PAGE_PRESENT | PAGE_WRITE)
.expect_install("install_root");
self.children.push(child);
}
}

/// `Arc<Lock<AddressSpace>>`, not `Lock<Option<_>>`: a kernel thread names it
/// as `KernelPayload.address_space` with no second answer. Leaked, since the
Expand Down Expand Up @@ -910,15 +922,13 @@ pub fn guard_kernel_page(addr: u64) {
kernel().lock().guard_4k(crate::mm::DirectMap::phys_of(addr as *const u8));
}

/// Build kernel page tables: map all physical memory in the high half using 2MB large pages.
pub(crate) fn init(memory_map: &[MemoryMapEntry]) {
let mut max_addr: u64 = MIN_PHYS_MAP;
for entry in memory_map {
if entry.end > max_addr {
max_addr = entry.end;
}
}
max_addr = (max_addr + PAGE_2M - 1) & !(PAGE_2M - 1);
/// Build kernel page tables: the direct map in the high half, in 2 MiB pages,
/// as far as [`toyos_bootmap::x86_64::direct_map_end`] reaches, and answer
/// that end.
pub(crate) fn init(memory_map: &[MemoryMapEntry]) -> toyos_bootmap::DirectMapEnd {
let extent = toyos_bootmap::x86_64::direct_map_end(memory_map)
.unwrap_or_else(|refusal| panic!("paging: firmware's memory map: {refusal}"));
let end = extent.get();

let mut kernel = AddressSpace {
root: Box::new(PageTablePage([0; 512])),
Expand All @@ -928,11 +938,17 @@ pub(crate) fn init(memory_map: &[MemoryMapEntry]) {
pcid: PcidHandle::Kernel,
};

// Only the direct map's slots: these tables come from the early heap.
let last_slot = indices(crate::mm::DirectMap::from_phys(end - 1).as_ptr::<u8>() as u64).0;
for slot in ROOT_HIGH_HALF..=last_slot {
kernel.install_root(slot);
}
let mut addr: u64 = 0;
while addr < max_addr {
while addr < end {
kernel.map_2m(addr, PAGE_PRESENT | PAGE_WRITE);
addr += PAGE_2M;
}
crate::log!("paging: the direct map covers 0x0..{end:#x}");

let cr3 = kernel.root();
KERNEL_CR3.store(cr3.0, core::sync::atomic::Ordering::Release);
Expand All @@ -950,6 +966,19 @@ pub(crate) fn init(memory_map: &[MemoryMapEntry]) {
unsafe {
cr3.load_flush();
}
extent
}

/// Install a second-level table under every kernel root slot [`init`] left
/// empty, from the pmm's heap, before the first user space copies the slots:
/// one installed after a space was made would be missing from that space.
pub(crate) fn seal_kernel_half() {
let mut kernel = kernel().lock();
for slot in ROOT_HIGH_HALF..512 {
if kernel.root[slot] & PAGE_PRESENT == 0 {
kernel.install_root(slot);
}
}
}

fn has(entry: u64, flag: u64) -> u8 {
Expand Down
Loading
Loading