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
1 change: 1 addition & 0 deletions Cargo.lock

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

8 changes: 6 additions & 2 deletions bootloader/src/watchdog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,14 @@ pub fn arm(system_table: &SystemTable<Boot>, rsdp_addr: u64, cmdline: &str) {
if !crate::arch::pio::EXISTS {
return refused(format_args!("this architecture has no I/O port space, where a TCO block answers"));
}
let ecam = match toyos_acpi::ecam_base(Identity, rsdp_addr) {
Ok((_, base)) => base,
let allocations = match toyos_acpi::ecam_allocations(Identity, rsdp_addr) {
Ok(allocations) => allocations,
Err(e) => return refused(format_args!("this machine's tables name no ECAM ({e:?})")),
};
// Bus 0 is where the chipset is.
let Some(ecam) = allocations.flatten().find(|window| window.holds(0)).map(|window| window.base()) else {
return refused(format_args!("no ECAM window of the MCFG's decodes bus 0"));
};
// The MCFG's word for where configuration space is, checked against
// firmware's own map before anything dereferences it.
if !described(system_table, ecam, BUS_ZERO_BYTES) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
status: open
kind: defect
opened: 2026-10-10
---

# An ECAM window off the 2 MiB grain is not enumerated on x86

x86-64's `map_mmio` maps whole 2 MiB pages (`paging::MMIO_GRAIN`,
`kernel/src/arch/x86_64/paging.rs`), so `EcamWindow::mappable`
(`toyos-acpi/src/mcfg.rs`) refuses a window whose first decoded byte or length
is off that grain, `AllocationRefused::OffGrain`, rather than map the
neighbouring megabyte uncached with it. A well-formed MCFG window that starts
or ends on an odd bus, or whose start bus sits on an odd megabyte, is then
never enumerated (`kernel/src/drivers/acpi.rs`, `ecam_windows`); where it is
the only window, the machine boots with no PCI function at all. AArch64 maps
at 4 KiB and takes every such window.

**Evidence:** the code, and `a_window_off_the_grain_is_refused_and_on_it_is_mapped`
in `toyos-acpi/tests/mcfg.rs`. No MCFG read so far is off the grain: q35's
decodes buses 0x00..=0xff, the T14's 0x00..=0x79 from 0xc0000000, and the target
laptop's 0x00..=0x3f, each an even number of megabytes from a 2 MiB boundary.

Owner: orchestrator. Exit: x86-64 maps an MMIO window exactly, to the 4 KiB
page, and a host test accepts a window of an odd bus count at x86-64's grain.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
status: open
kind: defect
opened: 2026-10-10
---

# An ECAM window past the CPU's physical address width is mapped

`acpi::ecam_windows` (`kernel/src/drivers/acpi.rs`) bounds each MCFG window by
`toyos_bootmap::DIRECT_MAP_WINDOW` (128 TiB), the first address the direct
map cannot hold, and not by the physical address width the CPU implements. A
window an MCFG puts between the two is mapped: on x86-64 the page-table entry
then sets bits past `MAXPHYADDR`, which are reserved, and the scan's first
read of it is a page fault in the kernel, which panics on firmware input.

**Evidence:** the code. Every MCFG this tree has read puts its window below
4 GiB.

Owner: orchestrator. Exit: the limit `ecam_windows` passes to
`EcamWindow::mappable` is the lower of `DIRECT_MAP_WINDOW` and the width the
CPU reports (CPUID leaf `0x8000_0008` `EAX[7:0]` on x86-64,
`ID_AA64MMFR0_EL1.PARange` on AArch64), so such a window is refused
`PastLimit` by name.
24 changes: 24 additions & 0 deletions issues/pci-is-enumerated-on-segment-group-0-alone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: defect
opened: 2026-10-10
---

# PCI is enumerated on segment group 0 alone

`toyos_acpi::ecam_allocations` (`toyos-acpi/src/mcfg.rs`) refuses an MCFG
window on any segment group but 0 (`AllocationRefused::OtherSegment`), so
`pci::enumerate` (`kernel/src/drivers/pci.rs`) never reads its functions, and
`PciDevice` and `pcidev`'s `Machine` carry one segment for every function. A
machine with more than one segment group has functions this kernel does not
see. The `acpi` claim's configuration accesses are bounded to segment group 0
too (`toyos_userbound::firmware::config`).

**Evidence:** the code. Every machine this tree has booted publishes segment
group 0 alone: QEMU's q35 and `virt` MCFGs (`toyos-acpi/tests/fixtures.rs`,
`virt_low_ecam`) and the T14's.

Owner: orchestrator. Exit: a function carries its segment group from
enumeration to `pcidev`'s inventory, every window the MCFG names is
enumerated whatever its group, and a host test of the decode accepts two
groups' windows.
23 changes: 0 additions & 23 deletions issues/the-mcfg-entrys-start-bus-is-never-read.md

This file was deleted.

3 changes: 3 additions & 0 deletions kernel/src/arch/aarch64/paging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,9 @@ pub fn activate_kernel() {
unsafe { kernel_root().activate() };
}

/// What [`map_mmio`] maps at: a 4 KiB page.
pub const MMIO_GRAIN: u64 = PAGE_4K;

/// Map a device's registers, or the scanout, into the direct map: 4 KiB pages
/// exactly over `[phys, phys + size)`, a block where a whole 2 MiB page is
/// asked for and free. No invalidation is owed, since only an invalid entry is
Expand Down
31 changes: 9 additions & 22 deletions kernel/src/arch/x86_64/acpi_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,16 @@ use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};

use core::sync::atomic::AtomicU32;

use toyos_abi::acpi::{Access, AcpiInfo, Block, Refused, Space, Width, FIXED_POWER_BUTTON};
use toyos_abi::syscall::SyscallError;
use toyos_acpi::{Ec, FixedHardware, LegacyMode, PowerButton};
use toyos_acpi::{Ec, EcamWindow, FixedHardware, LegacyMode, PowerButton};
use toyos_userbound::firmware::{
self, CallRate, Ecam, FirmwareCall, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict,
self, CallRate, FirmwareCall, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict,
};
use toyos_userbound::Ports;

Expand Down Expand Up @@ -119,8 +120,8 @@ struct Hardware {
/// Or why it is none a holder can be handed.
ec: Result<Ec, String>,
rsdp: u64,
/// The window configuration space is reached through, as the MCFG bounds it.
ecam: Option<Ecam>,
/// The windows configuration space is reached through, as the MCFG bounds them.
ecam: Vec<EcamWindow>,
lock: GlobalLock,
}

Expand Down Expand Up @@ -285,25 +286,12 @@ pub fn init(rsdp_addr: u64) {
if cpu::inw(control.port(0)) & SCI_EN != 0 { "ACPI" } else { "legacy" },
);
isa::fill(ROW, Function { name: "the ACPI fixed hardware", runs, irqs: vec![], wires: vec![sci] });
let (ecam, lock) = (ecam(rsdp_addr), global_lock(facs));
let (ecam, lock) = (crate::drivers::pci::windows(), global_lock(facs));
let hardware = Hardware { fixed, control, ec, rsdp: rsdp_addr, ecam, lock };
let was = HARDWARE.swap(Box::into_raw(Box::new(hardware)), Ordering::Release);
assert!(was.is_null(), "acpi: init ran twice");
}

/// The ECAM window as the MCFG's first allocation bounds it (PCI Firmware
/// Specification 3.3, Table 4-3: the segment group at +8 of the entry, the
/// first and last bus at +10 and +11).
fn ecam(rsdp_addr: u64) -> Option<Ecam> {
let (mcfg, base) = toyos_acpi::ecam_base(crate::drivers::acpi::direct_phys(), rsdp_addr).ok()?;
let entry = toyos_acpi::MCFG_FIRST_ENTRY;
let (segment, first_bus, last_bus) = (mcfg.u16_at(entry + 8)?, mcfg.byte(entry + 10)?, mcfg.byte(entry + 11)?);
if first_bus > last_bus {
log!("acpi: the MCFG's window ends at bus {last_bus:#x}, before its first, {first_bus:#x}: no configuration access is mediated");
return None;
}
Some(Ecam { base, segment, first_bus, last_bus })
}

/// The Global Lock of the FACS the FADT names, said by name where there is
/// none or it is none this kernel takes: a FACS that does not decode, and one
Expand Down Expand Up @@ -705,7 +693,7 @@ fn write_port(passed: &PortAt, value: u64) {
/// One configuration access, decided and, where it is a read, made through
/// the window the MCFG names, which the policy bounded the function by.
fn config(hardware: &Hardware, _acting: &Holder, segment: u16, function: PciFunction, offset: u16, width: Width, write: bool) -> Result<u64, Refused> {
let at = firmware::config(hardware.ecam, segment, function, offset, width, write)?;
let at = firmware::config(&hardware.ecam, segment, function, offset, width, write)?;
let PciFunction { bus, device, function } = at.function();
let space = crate::drivers::pci::function_window(bus, device, function).expect("a machine with a claimable ACPI row enumerated its PCI functions");
let offset = u64::from(at.offset());
Expand Down Expand Up @@ -741,7 +729,7 @@ fn memory(hardware: &Hardware, acting: &Holder, request: &mut Access, width: Wid
let memory = Memory {
map,
mapped_end: crate::mm::direct_map_end().get(),
ecam: hardware.ecam,
ecam: &hardware.ecam,
devices: driven.iter().copied().chain(bars),
facs: hardware.lock.facs().map(|facs| facs.span),
uncached,
Expand All @@ -757,8 +745,7 @@ fn memory(hardware: &Hardware, acting: &Holder, request: &mut Access, width: Wid
Ok(0)
}
(MemoryVerdict::AsConfig(function, offset), _) => {
let segment = hardware.ecam.expect("the policy answered a configuration access from an ECAM window").segment;
config(hardware, acting, segment, function, offset, width, write.is_some())
config(hardware, acting, toyos_acpi::SEGMENT_GROUP, function, offset, width, write.is_some())
}
(MemoryVerdict::Refused(refused), _) => Err(refused),
}
Expand Down
4 changes: 4 additions & 0 deletions kernel/src/arch/x86_64/paging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,10 @@ pub fn with_driven_windows<T>(f: impl FnOnce(&[(u64, u64)]) -> T) -> T {
f(&DRIVEN.lock())
}

/// What [`map_mmio`] maps at: whole 2 MiB pages, so a window off this grain
/// takes its neighbours' bytes with it.
pub const MMIO_GRAIN: u64 = PAGE_2M;

/// Free function (not a method): the lock and the shootdown are separate
/// statements. Not optional — `map_2m` may change memory type under a
/// sibling's stale entry, which is SDM Vol. 3A §11.12.4 undefined behaviour.
Expand Down
52 changes: 39 additions & 13 deletions kernel/src/drivers/acpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,22 +121,48 @@ fn refuse<T>(what: &str, error: TableError) -> Option<T> {
None
}

/// Given the RSDP address from UEFI, parse XSDT -> MCFG -> return the ECAM
/// base address and the PCI segment group it serves.
pub fn find_ecam_base(rsdp_addr: u64) -> Option<(u64, u16)> {
/// The most refused MCFG allocation structures said one by one: a table of
/// up to [`toyos_acpi::MAX_TABLE_LEN`] bytes holds tens of thousands.
const REFUSALS_SAID: usize = 8;

/// Every ECAM window the MCFG names that this kernel maps exactly, each
/// structure it refuses said by name; none where the MCFG is unusable.
pub fn ecam_windows(rsdp_addr: u64) -> Vec<toyos_acpi::EcamWindow> {
log!("ACPI: RSDP at {rsdp_addr:#x}");
let (mcfg, base) = match toyos_acpi::ecam_base(direct_phys(), rsdp_addr) {
let allocations = match toyos_acpi::ecam_allocations(direct_phys(), rsdp_addr) {
Ok(found) => found,
Err(e) => return refuse("MCFG", e),
Err(e) => return refuse("MCFG", e).unwrap_or_default(),
};
// PCI Firmware Specification 3.3, Table 4-3: the entry's segment group
// follows its base, inside the entry `ecam_base` already bounded.
let segment = mcfg
.u16_at(toyos_acpi::MCFG_FIRST_ENTRY + 8)
.expect("ecam_base bounded the whole first allocation structure");
log!("ACPI: MCFG found at {:#x}", mcfg.base());
log!("ACPI: ECAM base address: {base:#x}");
Some((base, segment))
log!("ACPI: MCFG found at {:#x}", allocations.table_base());
let (mut windows, mut refused) = (Vec::new(), 0usize);
for (index, item) in allocations.enumerate() {
let mappable = |window: toyos_acpi::EcamWindow| {
let limit = toyos_bootmap::DIRECT_MAP_WINDOW;
window.mappable(crate::mm::paging::MMIO_GRAIN, limit, crate::mm::firmware_map()).map(|()| window)
};
match item.and_then(mappable) {
Ok(window) => {
log!(
"ACPI: ECAM window: segment {} buses {:#04x}..={:#04x}, bus 0 at {:#x}",
toyos_acpi::SEGMENT_GROUP,
window.buses().start(),
window.buses().end(),
window.base()
);
windows.push(window);
}
Err(why) => {
refused += 1;
if refused <= REFUSALS_SAID {
log!("ACPI: MCFG allocation {index} refused: {why:?}");
}
}
}
}
if refused > REFUSALS_SAID {
log!("ACPI: {} more MCFG allocations refused", refused - REFUSALS_SAID);
}
windows
}

/// FADT revision and the IA-PC boot architecture flags.
Expand Down
Loading
Loading