Skip to content
Merged
10 changes: 9 additions & 1 deletion issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
status: open
status: assigned
kind: defect
opened: 2026-10-13
---
Expand Down Expand Up @@ -31,6 +31,14 @@ on the next one, and the agent itself exits on "no virtio-serial function
was handed to this program". Its handshake on aarch64 rests on the code
that passes on q35.

The windows placement offers come from the loader's list, which takes every
memory range firmware's map types MMIO and nobody owns, ECAM and flash among
them; the root's own `_CRS` on this machine declares `0x10000000..0x3eff0000`
and `0x8000000000..0x10000000000` and neither. Taken up by #916: its stage A
holds the declaration as a kernel state, and its stage B makes acpiserver
declare each root's `_CRS` on both architectures, which is what hands these
four claims over and closes this file.

Owner: the arm64 track, `issues/toyos-runs-on-arm64.md`, whose stage 7
exit (`ssh` from the host, the desktop on virtio-gpu) a `cargo run` boot
cannot meet until this is fixed.
Expand Down
102 changes: 99 additions & 3 deletions kernel/pci/src/aperture.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Whether an address reaches the bus at all.
//!
//! The windows are the ones firmware named
//! ([`toyos_abi::boot::KernelArgs::root_bridge_windows`]); what is decided here
//! is where one extent stands against them, for the BARs firmware assigned and
//! The windows are a declaration of the memory the root bridges decode
//! ([`check`] decides whether one can be held at all); what is decided here is
//! where one extent stands against them, for the BARs firmware assigned and
//! for the address space this kernel would put one in.

use toyos_abi::boot::RootBridgeWindow;
Expand All @@ -18,6 +18,66 @@ pub enum Decode {
Unrouted,
}

/// Why a declaration of the root bridges' memory is refused whole.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Refused {
/// It names no window, so nothing on the machine is known to be routed.
Empty,
/// A window of no length, or one whose end is past the address space.
Malformed(RootBridgeWindow),
/// Two windows name one address: whichever is wrong, the declaration does
/// not say what the bridges decode.
Overlap(RootBridgeWindow, RootBridgeWindow),
}

impl core::fmt::Display for Refused {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Empty => write!(f, "it names no window"),
Self::Malformed(w) => write!(f, "its window {:#x}+{:#x} is no extent of the address space", w.base, w.length),
Self::Overlap(a, b) => write!(
f,
"its windows {:#x}..{:#x} and {:#x}..{:#x} share an address",
a.base,
a.end(),
b.base,
b.end()
),
}
}
}

/// The root bridges' memory, held whole: at least one window, each an extent
/// that ends inside the address space, no two sharing an address. Only
/// [`check`] makes one.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Declaration<W>(W);

impl<W: AsRef<[RootBridgeWindow]>> Declaration<W> {
pub fn windows(&self) -> &[RootBridgeWindow] {
self.0.as_ref()
}
}

/// `windows` as a [`Declaration`], or why it cannot be one. Refused whole,
/// because a declaration with one window dropped is one with a hole a BAR
/// would be placed in.
pub fn check<W: AsRef<[RootBridgeWindow]>>(windows: W) -> Result<Declaration<W>, Refused> {
let list = windows.as_ref();
if list.is_empty() {
return Err(Refused::Empty);
}
for (at, window) in list.iter().enumerate() {
if window.length == 0 || window.base.checked_add(window.length).is_none() {
return Err(Refused::Malformed(*window));
}
if let Some(other) = list[..at].iter().find(|o| o.base < window.end() && window.base < o.end()) {
return Err(Refused::Overlap(*other, *window));
}
}
Ok(Declaration(windows))
}

/// Where `base .. end` stands against `windows`.
///
/// **An empty window list decodes nothing.** A machine whose firmware would not
Expand Down Expand Up @@ -55,6 +115,42 @@ mod tests {
assert_eq!(decode(&T14, 0xfe01_0000, 0xfe01_1000), Decode::Unrouted);
}

/// The T14's and the AMD bench's recorded declarations, seven windows
/// each, some meeting end to end, are held; an empty list, a window of no
/// length or past the address space, and two windows sharing an address
/// are each refused by name.
#[test]
fn a_declaration_is_held_only_whole_and_disjoint() {
let w = |base, length| RootBridgeWindow { base, length };
let recorded = [
w(0xa200_0000, 0x1b00_0000),
w(0x40_0000_0000, 0x20_3dc0_0000),
w(0xa080_0000, 0x180_0000),
w(0xbd00_0000, 0x300_0000),
w(0xff00_0000, 0xb8_0000),
w(0xffd3_a070, 0x2c_5f90),
w(0x60_3dc0_0000, 0x1f_c240_0000),
];
assert_eq!(check(&recorded).map(|d| d.windows().len()), Ok(7));
let amd_bench = [
w(0xd000_0000, 0x70_0000),
w(0xfc_9000_0000, 0x1020_0000),
w(0xd070_0000, 0x2790_0000),
w(0xfc00_0000, 0x1c0_0000),
w(0xff00_0000, 0xea_b000),
w(0x4_5020_0000, 0xf8_3fe0_0000),
w(0xfc_a020_0000, 0xfe0_0000),
];
assert_eq!(check(&amd_bench).map(|d| d.windows().len()), Ok(7));
assert_eq!(check(&[]), Err(Refused::Empty));
assert_eq!(check(&[w(0x1000, 0)]), Err(Refused::Malformed(w(0x1000, 0))));
assert_eq!(check(&[w(u64::MAX, 2)]), Err(Refused::Malformed(w(u64::MAX, 2))));
assert_eq!(
check(&[w(0x1000_0000, 0x100_0000), w(0x10ff_f000, 0x2000)]),
Err(Refused::Overlap(w(0x1000_0000, 0x100_0000), w(0x10ff_f000, 0x2000)))
);
}

/// A machine whose firmware named nothing, and a span of no length: neither
/// is an address anything decodes.
#[test]
Expand Down
20 changes: 20 additions & 0 deletions kernel/src/arch/aarch64/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

use core::arch::asm;

/// How many bits wide a physical address this CPU issues is:
/// `ID_AA64MMFR0_EL1.PARange` (Arm ARM DDI 0487): 0b0111, 56 bits, is
/// FEAT_D128's, and the encodings past it are reserved.
pub fn physical_address_bits() -> u8 {
let mmfr0: u64;
// SAFETY: reads an EL1 ID register, which EL1 may always read.
unsafe { asm!("mrs {}, id_aa64mmfr0_el1", out(reg) mmfr0, options(nomem, nostack, preserves_flags)) };
match mmfr0 & 0xF {
0 => 32,
1 => 36,
2 => 40,
3 => 42,
4 => 44,
5 => 48,
6 => 52,
7 => 56,
reserved => panic!("ID_AA64MMFR0_EL1.PARange holds the reserved value {reserved:#x}"),
}
}

/// The CPU's free-running counter: the generic timer's virtual count,
/// `CNTVCT_EL0`. The `ISB` keeps the read from being taken early, ahead of
/// the code it times (Arm ARM K.a, D12.2.2).
Expand Down
6 changes: 3 additions & 3 deletions kernel/src/arch/aarch64/smmu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ fn window(phys: u64, bytes: u64) -> Mmio {

/// The SMMUv3, armed; a refusal leaves every unit the IORT names aborting
/// what it is given, or says why one could not be told to.
pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[toyos_abi::boot::RootBridgeWindow]) {
pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[toyos_abi::boot::RootBridgeWindow]>) {
let iort = match toyos_acpi::iort(direct_phys(), rsdp_addr) {
Ok(iort) => iort,
Err(IortRefused::Table(TableError::Absent)) => {
Expand Down Expand Up @@ -268,7 +268,7 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[toyos_abi::boot::R
Err(why) => log!("IOMMU: {function}'s IORT route is refused: {why:?}"),
}
}
let reserved = windows.iter().map(|w| (w.base, w.end())).collect();
let reserved = windows.map(|w| w.iter().map(|w| (w.base, w.end())).collect());
let Some(live) = program(regs, smmu.base, smmu.coherent_override, smmu.event, routes, reserved) else {
return;
};
Expand Down Expand Up @@ -326,7 +326,7 @@ fn program(
coherent_override: bool,
event: Option<core::num::NonZeroU32>,
routes: Vec<(StreamId, u32, Option<ItsDevice>)>,
reserved: Vec<(u64, u64)>,
reserved: Option<Vec<(u64, u64)>>,
) -> Option<Live> {
let refused = |why: core::fmt::Arguments<'_>| log!("IOMMU: the SMMUv3 at {base:#x} is left aborting: {why}");
let (idr0, idr1, idr5) = (regs.read(reg::IDR0), regs.read(reg::IDR1), regs.read(reg::IDR5));
Expand Down
3 changes: 2 additions & 1 deletion kernel/src/arch/x86_64/amdvi/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ static DOMAINS: Lock<Option<Amd>> = Lock::new(None);
/// Before any domain is made, once a unit will translate: every one is built
/// clear of `reserved`, `kept`'s requesters stay on the identity domain, and
/// only a requester in `translated` is given one.
pub(super) fn start(reserved: Vec<(u64, u64)>, settings: Vec<Setting>, kept: Vec<Requesters>, translated: Vec<(u16, u16)>) {
pub(super) fn start(reserved: Option<Vec<(u64, u64)>>, settings: Vec<Setting>, kept: Vec<Requesters>, translated: Vec<(u16, u16)>) {
let mut domains = Domains::new(IDENTITY + 1);
domains.avoid(reserved);
*DOMAINS.lock() = Some(Amd { domains, settings, kept, translated });
Expand Down Expand Up @@ -83,6 +83,7 @@ pub fn untranslated(stream: StreamId) -> Option<&'static str> {
pub fn create(room: u64) -> Result<(DomainId, Iova), IommuError> {
let mut held = DOMAINS.lock();
let amd = held.as_mut().ok_or(IommuError::NoUnit)?;
// Mode-4 host tables translate 48 bits of device address (48882 §2.2.3).
let next = amd.domains.prepare(9 * ROOT_LEVEL + 12, 1 << 16, room)?;
let space = Space { root: TABLES.lock().alloc() };
Ok(amd.domains.add(next, space, alloc::format!("root={:#x} mode={ROOT_LEVEL}", space.root.get())))
Expand Down
9 changes: 6 additions & 3 deletions kernel/src/arch/x86_64/amdvi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ struct Ready {
/// Every unit the IVRS describes, switched off, and each this kernel can use
/// programmed; `false` where firmware published no IVRS. `windows` and every
/// IVMD range are what no domain's addresses reach.
pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow]) -> bool {
pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeWindow]>) -> bool {
let ivrs = match toyos_acpi::ivrs(crate::drivers::acpi::direct_phys(), rsdp_addr) {
Ok(ivrs) => ivrs,
Err(IvrsRefused::Table(TableError::Absent)) => return false,
Expand Down Expand Up @@ -259,8 +259,11 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow])
);
}
let identity = build(devices, &settings);
let reserved = windows.iter().map(|w| (w.base, w.end())).chain(ivmds.iter().map(|m| (m.start, m.start + m.length)));
domain::start(reserved.collect(), settings, ivmds.iter().map(|m| m.requesters).collect(), translated);
// Nothing where no declaration is held, as `vtd` treats its RMRRs.
let reserved = windows.map(|w| {
w.iter().map(|w| (w.base, w.end())).chain(ivmds.iter().map(|m| (m.start, m.start + m.length))).collect()
});
domain::start(reserved, settings, ivmds.iter().map(|m| m.requesters).collect(), translated);
log!("iommu: amdvi identity domain covers 0x0..{top:#x}, root={:#x}", identity.get());
for unit in ready {
program(unit, devices);
Expand Down
9 changes: 9 additions & 0 deletions kernel/src/arch/x86_64/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ pub fn leaf_d(subleaf: u32) -> (u32, u32, u32, u32) {
if cpuid(0, 0).0 >= 0xD { cpuid(0xD, subleaf) } else { (0, 0, 0, 0) }
}

/// How many bits wide a physical address this CPU issues is: CPUID
/// 8000_0008H `EAX[7:0]` (Intel SDM Vol. 2A, CPUID; AMD APM Vol. 3,
/// Fn8000_0008_EAX `PhysAddrSize`), a leaf every x86-64 CPU of this
/// project's range implements.
pub fn physical_address_bits() -> u8 {
assert!(cpuid(0x8000_0000, 0).0 >= 0x8000_0008, "CPUID does not reach leaf 8000_0008H");
cpuid(0x8000_0008, 0).0 as u8
}

/// CPUID with both index registers, as `(eax, ebx, ecx, edx)`.
///
/// `core`'s intrinsic and not an `asm!` of our own: `rbx` is CPUID's output
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/arch/x86_64/iommu_unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn backend() -> Backend {
const MAX_PHYS: u64 = 1 << 52;

/// The units of whichever table firmware published.
pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow]) {
pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeWindow]>) {
let found = if super::vtd::init(rsdp_addr, devices, windows) {
Backend::Vtd
} else if super::amdvi::init(rsdp_addr, devices, windows) {
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/arch/x86_64/vtd/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ static DOMAINS: Lock<Vtd> =
Lock::new(Vtd { agreement: Agreement::None, domains: Domains::new(table::KERNEL_DOMAIN + 1) });

/// Before any domain is made: every one is built clear of these.
pub fn avoid(reserved: Vec<(u64, u64)>) {
pub fn avoid(reserved: Option<Vec<(u64, u64)>>) {
DOMAINS.lock().domains.avoid(reserved);
}

Expand Down
11 changes: 7 additions & 4 deletions kernel/src/arch/x86_64/vtd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ pub(super) fn invalidate_interrupt_entries() {

/// `windows` and every region firmware reserved are what no domain's addresses
/// reach. `false` where firmware published no DMAR.
pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow]) -> bool {
pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeWindow]>) -> bool {
let dmar = match Dmar::open(rsdp_addr) {
Ok(dmar) => dmar,
Err(TableError::Absent) => return false,
Expand Down Expand Up @@ -151,8 +151,9 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow])
// Described and planned before any unit is armed: whether sources may move
// to the remappable format is one decision, taken before the first `IRE`.
let mut ready: Vec<(Unit, Plan)> = Vec::new();
// What no domain's addresses may reach, RMRRs added below as the walk meets them.
let mut reserved: Vec<(u64, u64)> = windows.iter().map(|w| (w.base, w.end())).collect();
// What no domain's addresses may reach, RMRRs added below as the walk
// meets them; nothing where no declaration is held.
let mut reserved: Option<Vec<(u64, u64)>> = windows.map(|w| w.iter().map(|w| (w.base, w.end())).collect());
for structure in dmar.structures() {
match structure {
Ok(Structure::Drhd(drhd)) => {
Expand Down Expand Up @@ -199,7 +200,9 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow])
identity domain 0x0..{identity_top:#x}"
);
}
reserved.push((base, limit.saturating_add(1)));
if let Some(reserved) = reserved.as_mut() {
reserved.push((base, limit.saturating_add(1)));
}
describe_scopes("rmrr", regions, rmrr.scopes());
regions += 1;
held_regions += usize::from(held);
Expand Down
13 changes: 7 additions & 6 deletions kernel/src/iommu/domains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ pub struct Domains<S> {
/// By id minus `first`; never shrinks, since a released id would name a
/// domain some unit may still have cached.
live: Vec<(S, Window)>,
/// What no domain's addresses may reach: the root bridges' windows and
/// the regions firmware reserved, as `(start, end)`.
reserved: Vec<(u64, u64)>,
/// What no domain's addresses may reach: the root bridges' declared
/// windows and the regions firmware reserved, as `(start, end)`; `None`
/// where no declaration is held ([`Window::new`]).
reserved: Option<Vec<(u64, u64)>>,
}

/// A domain [`Domains::prepare`] admitted, not yet made.
Expand All @@ -30,11 +31,11 @@ pub struct Next {

impl<S: Copy> Domains<S> {
pub const fn new(first: u16) -> Self {
Self { first, live: Vec::new(), reserved: Vec::new() }
Self { first, live: Vec::new(), reserved: None }
}

/// Before any domain is made: every one is built clear of these.
pub fn avoid(&mut self, reserved: Vec<(u64, u64)>) {
pub fn avoid(&mut self, reserved: Option<Vec<(u64, u64)>>) {
self.reserved = reserved;
}

Expand All @@ -46,7 +47,7 @@ impl<S: Copy> Domains<S> {
.ok()
.filter(|id| u32::from(*id) < ceiling)
.ok_or(IommuError::DomainsExhausted(ceiling))?;
let (addresses, first) = Window::new(translatable, &self.reserved, room)?;
let (addresses, first) = Window::new(translatable, self.reserved.as_deref(), room)?;
Ok(Next { id, addresses, first })
}

Expand Down
18 changes: 15 additions & 3 deletions kernel/src/iommu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ pub enum IommuError {
/// What this machine's units translate does not reach above its memory, so
/// a device window has nowhere to sit that a stale descriptor would miss.
WindowBelowMemory { translatable: u8, floor: u64, top: u64 },
/// The units translate no further than the CPU's physical addresses, so a
/// domain has to end under the root bridges' windows, and no declaration
/// of them is held.
Undeclared { translatable: u8, cpu: u8 },
/// From the floor to the first root-bridge window or reserved region above
/// it is less than a new domain was asked to hand out.
NoRoom { floor: u64, ceiling: u64, room: u64 },
Expand All @@ -147,6 +151,12 @@ impl core::fmt::Display for IommuError {
"this machine's units translate {translatable} bits, whose device window would \
start at {floor:#x}, at or below the {top:#x} its memory reaches"
),
Self::Undeclared { translatable, cpu } => write!(
f,
"this machine's units translate {translatable} bits and its CPUs issue {cpu}, so a \
domain has to end under the root bridges' windows, and no declaration of them \
is held"
),
Self::NoRoom { floor, ceiling, room } => write!(
f,
"a domain's addresses from {floor:#x} end at {ceiling:#x}, where a root bridge's \
Expand Down Expand Up @@ -277,13 +287,15 @@ impl core::fmt::Display for StreamId {
///
/// The device list must be the complete enumeration: enabling translation with an unenumerated device left off it can brick the machine's own boot disk.
///
/// No domain's addresses reach into one of `windows`, the memory firmware says the root bridges decode.
/// No domain's addresses reach into one of `windows`, the root bridges'
/// declared memory, or `None` where no declaration is held ([`window`]).
pub fn init(
rsdp_addr: u64,
devices: &[crate::drivers::pci::PciDevice],
windows: &[toyos_abi::boot::RootBridgeWindow],
windows: Option<&toyos_pci::aperture::Declaration<alloc::vec::Vec<toyos_abi::boot::RootBridgeWindow>>>,
) {
unit::init(rsdp_addr, devices, windows);
log!("iommu: the CPUs issue {}-bit physical addresses", crate::arch::cpu::physical_address_bits());
unit::init(rsdp_addr, devices, windows.map(|declared| declared.windows()));
}

/// Why a source could not be given a message: the architecture's own reasons.
Expand Down
Loading