From 98ae5a74005516792d72750bfc34eb2ca1059d8d Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 20:54:06 +0200 Subject: [PATCH 1/9] WIP: stage B in progress (declaration ABI, _CRS reader, acpiserver declare, pcidev state) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- build/licence.rs | 6 +- kernel/pci/Cargo.toml | 4 +- kernel/pci/src/aperture.rs | 160 ++++++++++++-- kernel/pci/src/placement.rs | 2 +- kernel/src/pcidev/mod.rs | 148 ++++++++----- lib/acpi/fixtures/ovmf-pure-efi/SOURCE | 12 - .../fixtures/ovmf-pure-efi/root-bridge-0.bin | Bin 186 -> 0 bytes lib/acpi/fixtures/qemu-11.1.1-virt/SOURCE | 6 + lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin | Bin 0 -> 5406 bytes lib/acpi/src/resource.rs | 170 ++++++++------- lib/acpi/tests/common/mod.rs | 30 +-- lib/acpi/tests/corpus.rs | 43 +--- lib/acpi/tests/fixtures.rs | 42 +--- lib/acpi/tests/resource.rs | 80 +++++-- sdk/abi/src/acpi.rs | 123 +++++++++++ sdk/abi/src/boot.rs | 177 ++------------- sdk/abi/src/syscall.rs | 11 +- sdk/toyos/src/device.rs | 6 + system/acpiserver/Cargo.toml | 2 +- system/acpiserver/src/aml.rs | 13 +- system/acpiserver/src/apertures.rs | 206 ++++++++++++++++++ system/acpiserver/src/host.rs | 48 +++- system/acpiserver/src/init.rs | 10 +- system/acpiserver/src/main.rs | 45 +++- 24 files changed, 879 insertions(+), 465 deletions(-) delete mode 100644 lib/acpi/fixtures/ovmf-pure-efi/SOURCE delete mode 100644 lib/acpi/fixtures/ovmf-pure-efi/root-bridge-0.bin create mode 100644 lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin create mode 100644 system/acpiserver/src/apertures.rs diff --git a/build/licence.rs b/build/licence.rs index 40d1d697072..cef91882136 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -406,9 +406,9 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ Terms::Spdx("MIT OR Apache-2.0"), ), ( - "lib/acpi/fixtures/ovmf-pure-efi/root-bridge-0.bin", - "eb00e68be746a09ac7f0ce1ca492ce8c858e3af1152112b49ffb4708883acbfb", - "ours: OVMF's answer on a q35 guest, read off that boot's own loader log", + "lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin", + "908d068d6d70db725bd139ac5a8fe9511373ab59a0570a0f285c3c59d60cd227", + "ours: QEMU `virt`'s own DSDT, captured by the commit that declares its root bridge's `_CRS`", Terms::Spdx("MIT OR Apache-2.0"), ), // A bcachefs volume upstream's own tools wrote, gzipped. The bytes inside diff --git a/kernel/pci/Cargo.toml b/kernel/pci/Cargo.toml index 1b735bbfe8c..6ae807041fc 100644 --- a/kernel/pci/Cargo.toml +++ b/kernel/pci/Cargo.toml @@ -14,7 +14,7 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -# The window type the bootloader hands the kernel, declared once in the ABI and -# read here rather than mirrored: a second spelling of a base and a length is +# The window type an ACPI server declares to the kernel, declared once in the +# ABI and read here rather than mirrored: a second spelling of a base and a length is # two readers that can disagree about one machine's aperture. toyos-abi = { path = "../../sdk/abi" } diff --git a/kernel/pci/src/aperture.rs b/kernel/pci/src/aperture.rs index 1d58b75e61e..eedf9fded2e 100644 --- a/kernel/pci/src/aperture.rs +++ b/kernel/pci/src/aperture.rs @@ -5,7 +5,7 @@ //! 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; +use toyos_abi::acpi::RootBridgeWindow; /// Where an extent stands against the memory windows the root bridges decode. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -28,6 +28,10 @@ pub enum Refused { /// Two windows name one address: whichever is wrong, the declaration does /// not say what the bridges decode. Overlap(RootBridgeWindow, RootBridgeWindow), + /// A window over memory: a root bridge's window is MMIO and never RAM, which + /// is what lets a device address equal to a RAM address go unrouted + /// peer-to-peer, so a declaration saying otherwise is wrong about one. + Memory(RootBridgeWindow), } impl core::fmt::Display for Refused { @@ -43,13 +47,14 @@ impl core::fmt::Display for Refused { b.base, b.end() ), + Self::Memory(w) => write!(f, "its window {:#x}..{:#x} lies over memory", w.base, w.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. +/// that ends inside the address space and lies over no memory, no two sharing +/// an address. Only [`check`] makes one. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Declaration(W); @@ -59,10 +64,11 @@ impl> Declaration { } } -/// `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>(windows: W) -> Result, Refused> { +/// `windows` as a [`Declaration`] on a machine whose memory is `memory`, as +/// `(start, end)`, 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>(windows: W, memory: &[(u64, u64)]) -> Result, Refused> { let list = windows.as_ref(); if list.is_empty() { return Err(Refused::Empty); @@ -74,10 +80,90 @@ pub fn check>(windows: W) -> Result, if let Some(other) = list[..at].iter().find(|o| o.base < window.end() && window.base < o.end()) { return Err(Refused::Overlap(*other, *window)); } + if memory.iter().any(|&(start, end)| start < window.end() && window.base < end) { + return Err(Refused::Memory(*window)); + } } Ok(Declaration(windows)) } +/// What the kernel holds of the root bridges' memory: nothing yet, a +/// declaration, or why there is none. It leaves [`Self::Unknown`] once and +/// never returns: a declaration is never replaced, and a refusal stands. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Held { + Unknown, + Declared(Declaration), + Withheld(Withheld), +} + +/// Why no declaration is held. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Withheld { + /// The declaration made was refused by [`check`]. + Refused(Refused), + /// The only holder who may declare let go of its authority without + /// declaring. + Ended, + /// A claim waited the whole of its deadline and nothing was declared. + Unanswered, +} + +impl core::fmt::Display for Withheld { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Refused(why) => write!(f, "the declaration was refused: {why}"), + Self::Ended => write!(f, "the acpi claim ended without declaring"), + Self::Unanswered => write!(f, "nothing was declared before a claim's deadline"), + } + } +} + +/// Why a declaration was not taken. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Declined { + /// Refused whole, and now held as the answer. + Refused(Refused), + /// An answer is already held: a second declaration replaces nothing. + Late, +} + +impl> Held { + /// Take `windows` as the declaration on a machine whose memory is + /// `memory`, from [`Self::Unknown`] alone; refused whole, the refusal is + /// the answer. + pub fn declare(&mut self, windows: W, memory: &[(u64, u64)]) -> Result<(), Declined> { + if !matches!(self, Self::Unknown) { + return Err(Declined::Late); + } + let (held, answer) = match check(windows, memory) { + Ok(declaration) => (Self::Declared(declaration), Ok(())), + Err(why) => (Self::Withheld(Withheld::Refused(why)), Err(Declined::Refused(why))), + }; + *self = held; + answer + } + + /// Nothing will be declared, for `why`: an answer where there was none, + /// and nothing changed where there was one. Whether it was the answer. + pub fn withhold(&mut self, why: Withheld) -> bool { + let unknown = matches!(self, Self::Unknown); + if unknown { + *self = Self::Withheld(why); + } + unknown + } + + /// The answer, or `None` while there is none to wait for yet. + pub fn answer(&self) -> Option, Withheld>> { + match self { + Self::Unknown => None, + Self::Declared(declaration) => Some(Ok(declaration)), + Self::Withheld(why) => Some(Err(*why)), + } + } +} + /// Where `base .. end` stands against `windows`. /// /// **An empty window list decodes nothing.** A machine whose firmware would not @@ -131,7 +217,7 @@ mod tests { w(0xffd3_a070, 0x2c_5f90), w(0x60_3dc0_0000, 0x1f_c240_0000), ]; - assert_eq!(check(&recorded).map(|d| d.windows().len()), Ok(7)); + 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), @@ -141,16 +227,33 @@ mod tests { 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(&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)]), + check(&[w(0x1000_0000, 0x100_0000), w(0x10ff_f000, 0x2000)], &[]), Err(Refused::Overlap(w(0x1000_0000, 0x100_0000), w(0x10ff_f000, 0x2000))) ); } + /// `virt`'s two windows (what its root's `_CRS` declares) beside its + /// memory at 0x40000000 are held; a window reaching one byte into that + /// memory, and one wholly over it, are refused by name, whatever their + /// place in the list. + #[test] + fn a_window_over_memory_is_refused_by_name() { + let w = |base, length| RootBridgeWindow { base, length }; + const MEMORY: [(u64, u64); 1] = [(0x4000_0000, 0x5000_0000)]; + let virt = [w(0x1000_0000, 0x2eff_0000), w(0x80_0000_0000, 0x80_0000_0000)]; + assert_eq!(check(&virt, &MEMORY).map(|d| d.windows().len()), Ok(2)); + let into = w(0x3f00_0000, 0x100_0001); + assert_eq!(check(&[virt[1], into], &MEMORY), Err(Refused::Memory(into))); + let over = w(0x4800_0000, 0x1000); + assert_eq!(check(&[over, virt[0]], &MEMORY), Err(Refused::Memory(over))); + assert_eq!(check(&[w(0x5000_0000, 0x1000)], &MEMORY).map(|d| d.windows().len()), Ok(1)); + } + /// A machine whose firmware named nothing, and a span of no length: neither /// is an address anything decodes. #[test] @@ -159,4 +262,35 @@ mod tests { assert_eq!(decode(&T14, 0, 0), Decode::Empty); assert_eq!(decode(&[], 0, 0), Decode::Empty); } + + /// The state leaves `Unknown` once: a declaration is held and a second + /// one, whatever it says, is late; a refused declaration is the answer + /// and the next one is late too; the holder's end and a claim's deadline + /// answer only where nothing had, and change no answer held. + #[test] + fn the_declaration_is_answered_once_and_never_replaced() { + let w = |base, length| RootBridgeWindow { base, length }; + let virt = [w(0x1000_0000, 0x2eff_0000), w(0x80_0000_0000, 0x80_0000_0000)]; + + let mut held = Held::Unknown; + assert_eq!(held.answer(), None); + assert_eq!(held.declare(virt, &[]), Ok(())); + assert_eq!(held.declare([w(0x1000, 0x1000); 2], &[]), Err(Declined::Late)); + assert!(!held.withhold(Withheld::Ended)); + assert!(!held.withhold(Withheld::Unanswered)); + assert_eq!(held.answer().map(|a| a.map(|d| d.windows().to_vec())), Some(Ok(virt.to_vec()))); + + let mut refused = Held::Unknown; + assert_eq!(refused.declare([w(0, 0); 2], &[]), Err(Declined::Refused(Refused::Malformed(w(0, 0))))); + assert_eq!(refused.declare(virt, &[]), Err(Declined::Late)); + assert_eq!(refused.answer().map(|a| a.err()), Some(Some(Withheld::Refused(Refused::Malformed(w(0, 0)))))); + + for why in [Withheld::Ended, Withheld::Unanswered] { + let mut ended = Held::<[RootBridgeWindow; 2]>::Unknown; + assert!(ended.withhold(why)); + assert!(!ended.withhold(Withheld::Unanswered)); + assert_eq!(ended.declare(virt, &[]), Err(Declined::Late)); + assert_eq!(ended.answer().map(|a| a.err()), Some(Some(why))); + } + } } diff --git a/kernel/pci/src/placement.rs b/kernel/pci/src/placement.rs index 07d2c35f8ba..578cf0f151d 100644 --- a/kernel/pci/src/placement.rs +++ b/kernel/pci/src/placement.rs @@ -8,7 +8,7 @@ //! [`toyos_abi::boot::KernelArgs::root_bridge_windows`], and an address inside //! none of those windows is never offered. -use toyos_abi::boot::RootBridgeWindow; +use toyos_abi::acpi::RootBridgeWindow; use crate::bridge::Window; diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 1f4486a75a9..08c1a1316ca 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -30,10 +30,14 @@ //! **Nothing here reads an address this kernel cannot name as routed**, because //! a load no bridge forwards does not answer all-ones on real hardware: it does //! not complete, and the CPU cannot be interrupted out of it. The name is a -//! window of the declaration [`Apertures`] holds: the address firmware put in -//! the BAR is held against those windows here, no other address is ever -//! offered ([`placement::reserve`]), and with no declaration held no function -//! is handed over. +//! window of the declaration the holder of the `acpi` claim makes, once, out +//! of each root's `_CRS` ([`declare`]): the address firmware put in the BAR is +//! held against those windows here, no other address is ever offered +//! ([`placement::reserve`]), and with no declaration held no function is +//! handed over. **A claim made before one waits for it**, on [`DECLARATION`], +//! for at most [`DECLARATION_DEADLINE`]; a claim that waits it out, the +//! holder's claim ending without one, and a declaration refused each leave +//! every claim of the boot refused by name. //! //! **Inside one, the function itself is the proof.** [`place_bar`] reads one //! dword through the BAR where firmware put it, moves the BAR onto the @@ -123,7 +127,8 @@ use core::fmt::Write; use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use record::Interrupt; -use toyos_abi::boot::{MemoryMapEntry, RootBridgeWindow}; +use toyos_abi::acpi::RootBridgeWindow; +use toyos_abi::boot::MemoryMapEntry; use toyos_abi::inventory::Arrival; use toyos_abi::pci::{DeviceIrqRecord, PciFunctionInfo, BARS, LEND_WINDOW}; use toyos_abi::syscall::{PciId, RegWidth, SyscallError}; @@ -139,7 +144,7 @@ use crate::mm::{align_2m, DirectMap, Mmio, PAGE_2M}; use crate::object::shm::{Region, SharedMemObject}; use crate::sync::Lock; use crate::inbox::PollEntry; -use crate::watch::IrqWatch; +use crate::watch::{IrqWatch, Watch}; /// The most one grant may be. A driver's rings and buffers are kilobytes to a /// megabyte; this is room for a queue depth nothing in reach uses, and a bound @@ -371,8 +376,11 @@ struct Machine { low: Vec, high: Vec, /// What this kernel holds of the memory the root bridges decode: the - /// necessary condition for any read this module issues. - apertures: Apertures, + /// necessary condition for any read this module issues. **Only a + /// declaration is ever read from**, so an address inside none of its + /// windows is never touched, and a function is handed over on no other + /// state. + apertures: aperture::Held>, /// Windows this module has cut, as `(requester, BAR index, at, span)`. /// /// **A window belongs to the BAR it was cut for, not to the claim that @@ -389,18 +397,14 @@ struct Machine { resetting: Vec<(u16, Resetting, Kept)>, } -/// The memory the root bridges decode, as far as this kernel has been told. -/// -/// **Only a declaration is ever read from**, so an address inside none of its -/// windows is never touched, and a function is handed over on no other state: -/// a machine that has not declared, or whose declaration was refused, is one -/// where nothing is known to be routed. -enum Apertures { - Unknown, - /// Never replaced. - Declared(aperture::Declaration>), - Refused(aperture::Refused), -} +/// Posted once, when [`Machine::apertures`] leaves `Unknown`. +static DECLARATION: Watch = Watch::new(); + +/// How long a claim made before the declaration waits for it. A ceiling and no +/// expectation: the server that declares is started before every claimant, and +/// its tables' load and `_CRS` took 73 ms on the ThinkPad T14 and some 2 s on +/// q35 under TCG. +const DECLARATION_DEADLINE: crate::time::Duration = crate::time::Duration::from_secs(30); static MACHINE: Lock = Lock::new(Machine { functions: Vec::new(), @@ -410,7 +414,7 @@ static MACHINE: Lock = Lock::new(Machine { kernel_driven: Vec::new(), low: Vec::new(), high: Vec::new(), - apertures: Apertures::Unknown, + apertures: aperture::Held::Unknown, windows: Vec::new(), resetting: Vec::new(), }); @@ -537,7 +541,7 @@ pub fn note_kernel_driver(pci: &PciDevice) { /// **Before any driver `init`**, because the sizing probe below takes memory /// decode off the function it is probing for the length of the probe, and a /// driver mid-transfer must not meet that. -pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry], firmware: &[RootBridgeWindow]) { +pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry]) { let mut decoded = Vec::new(); let mut taken: Vec = maps.iter().map(|entry| Window { start: entry.start, end: entry.end }).collect(); @@ -575,16 +579,6 @@ pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry], fir taken.push(forwarded); } } - let apertures = match aperture::check(firmware.to_vec()) { - Ok(declaration) => { - account_for(declaration.windows(), &decoded); - Apertures::Declared(declaration) - } - Err(why) => { - log!("pcidev: the root bridges' memory is refused as declared: {why}, so no function is handed over"); - Apertures::Refused(why) - } - }; let mut runs = |wide| -> Vec { placement::free_runs(&mut taken, wide) .filter(|run| run.end - run.start >= PAGE_2M) @@ -633,20 +627,70 @@ pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry], fir .collect(); machine.segment = segment; machine.decoded = decoded; - machine.apertures = apertures; machine.low = low; machine.high = high; } -/// The root bridges' memory, where a declaration of it is held. -pub fn declared() -> Option>> { - match &MACHINE.lock().apertures { - Apertures::Declared(declaration) => Some(declaration.clone()), - Apertures::Unknown | Apertures::Refused(_) => None, +/// Hold `windows` as the root bridges' memory: the `acpi` claim holder's +/// declaration, from `Unknown` alone. A window over memory firmware's map +/// gives the kernel is refused with the rest ([`aperture::check`]). +pub fn declare(windows: &[RootBridgeWindow]) -> Result<(), aperture::Declined> { + let memory: Vec<(u64, u64)> = crate::mm::firmware_map() + .iter() + .filter(|entry| toyos_bootmap::is_read_as_memory(entry.uefi_type)) + .map(|entry| (entry.start, entry.end)) + .collect(); + let mut machine = MACHINE.lock(); + let declared = machine.apertures.declare(windows.to_vec(), &memory); + match (&declared, machine.apertures.answer()) { + (Ok(()), Some(Ok(declaration))) => account_for(declaration.windows(), &machine.decoded), + (Err(aperture::Declined::Refused(why)), _) => { + log!("pcidev: the root bridges' memory is refused as declared: {why}, so no function is handed over") + } + (Err(aperture::Declined::Late), _) => log!("pcidev: a declaration of the root bridges' memory after the first answer is refused"), + (Ok(()), _) => unreachable!("a declaration taken is the answer"), + } + drop(machine); + DECLARATION.post(); + declared +} + +/// The `acpi` claim went: where it declared nothing, nothing will be. +pub fn declarer_gone() { + withhold(aperture::Withheld::Ended); +} + +fn withhold(why: aperture::Withheld) { + if MACHINE.lock().apertures.withhold(why) { + log!("pcidev: no declaration of the root bridges' memory will be held: {why}, so no function is handed over"); + DECLARATION.post(); + } +} + +/// The root bridges' memory, or why none is held; waited for, up to +/// [`DECLARATION_DEADLINE`], where nothing is held yet. `None` is a +/// claimant killed while it waited. +fn answered() -> Option>, aperture::Withheld>> { + let answer = || MACHINE.lock().apertures.answer().map(|answer| answer.cloned()); + if let Some(answer) = answer() { + return Some(answer); + } + let began = crate::clock::now(); + let deadline = crate::time::Deadline::at(began + DECLARATION_DEADLINE); + let parkable = crate::scheduler::Parkable::at_entry(); + let waited = crate::watch::wait_until(&parkable, &DECLARATION, 0, kernel::sched::task::WaitClass::Other, deadline, || { + MACHINE.lock().apertures.answer().is_some() + }); + waited.ok()?; + if answer().is_none() { + withhold(aperture::Withheld::Unanswered); } + let answer = answer().expect("pcidev: the declaration is answered or withheld"); + log!("pcidev: a claim waited {} for the root bridges' memory", crate::clock::now() - began); + Some(answer) } -/// Say what memory firmware declared the root bridges decode, and which +/// Say what memory the root bridges were declared to decode, and which /// assigned BAR lies inside none of it. /// /// **What this answers about is the necessary condition every read below is @@ -659,7 +703,7 @@ fn account_for(firmware: &[RootBridgeWindow], decoded: &[(u16, u64, u64)]) { } let _ = write!(said, "mem {:#x}..{:#x}", window.base, window.end()); } - log!("pcidev: firmware declared root bridge memory: {said}"); + log!("pcidev: the acpi claim declared root bridge memory: {said}"); for (who, base, end) in decoded { if aperture::decode(firmware, *base, *end) == aperture::Decode::Unrouted { @@ -676,10 +720,8 @@ fn account_for(firmware: &[RootBridgeWindow], decoded: &[(u16, u64, u64)]) { /// place. #[derive(Clone, Copy)] enum Refusal { - /// Nothing has declared the memory the root bridges decode yet. - Undeclared, - /// The declaration of the memory the root bridges decode was refused. - AperturesRefused(aperture::Refused), + /// No declaration of the memory the root bridges decode is held. + Withheld(aperture::Withheld), NotRemapped, /// The machine translates messages, and none could be made for this function. NoMessage(Refused), @@ -715,14 +757,9 @@ enum Refusal { impl core::fmt::Display for Refusal { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::Undeclared => write!( + Self::Withheld(why) => write!( f, - "nothing has declared the memory the root bridges decode, so no address on this \ - machine is known to be routed" - ), - Self::AperturesRefused(why) => write!( - f, - "the declaration of the memory the root bridges decode was refused ({why}), so no \ + "no declaration of the memory the root bridges decode is held ({why}), so no \ address on this machine is known to be routed" ), Self::NotRemapped => write!( @@ -821,15 +858,10 @@ pub fn claim(arrival: Arrival) -> Result<(PciFunctionInfo, Claim), ClaimError> { // Before anything is taken or touched: with no declaration held, no // read this claim would make is known to reach the bus. - let declared = match &MACHINE.lock().apertures { - Apertures::Declared(declaration) => Ok(declaration.clone()), - Apertures::Unknown => Err(Refusal::Undeclared), - Apertures::Refused(why) => Err(Refusal::AperturesRefused(*why)), - }; - let declaration = match declared { + let declaration = match answered().ok_or(ClaimError::Unusable)? { Ok(declaration) => declaration, Err(why) => { - log!("pcidev: PCI {:02x}:{:02x}.{} NOT HANDED OVER — {why}", pci.bus, pci.dev, pci.func); + log!("pcidev: PCI {:02x}:{:02x}.{} NOT HANDED OVER — {}", pci.bus, pci.dev, pci.func, Refusal::Withheld(why)); return Err(ClaimError::Unusable); } }; diff --git a/lib/acpi/fixtures/ovmf-pure-efi/SOURCE b/lib/acpi/fixtures/ovmf-pure-efi/SOURCE deleted file mode 100644 index 9d3bcd81714..00000000000 --- a/lib/acpi/fixtures/ovmf-pure-efi/SOURCE +++ /dev/null @@ -1,12 +0,0 @@ -The ACPI resource descriptors `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL::Configuration` -answered with on the one root bridge of a QEMU 11.1.0 q35 guest, under the OVMF -build sha256 -9de33971d47958f42af86584b502f83256120b2482e4f7ed14db32fd68e92922. - - root-bridge-0.bin 186 bytes, off the `Root bridge: 0 (segment 0) 186 bytes:` - line the bootloader writes to the 16550 before the kernel - handoff, on a `Profile::Headless` boot of `tests/netcase`. - -The bootloader printed the bytes and nothing decoded them on the way. What says -the length was right is the file itself: four descriptors whose own declared -lengths tile it exactly, then the End Tag. diff --git a/lib/acpi/fixtures/ovmf-pure-efi/root-bridge-0.bin b/lib/acpi/fixtures/ovmf-pure-efi/root-bridge-0.bin deleted file mode 100644 index ed8d1f5d4e2727761ac05be3f5b6a3653babdd7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 186 zcmeCuW?*DMfCLEhe?CG8%o2d`yR<>H0)zu14}ggO|M|f*hz$V{YF|SIvYF!^MI*wvqt=>tG97 diff --git a/lib/acpi/fixtures/qemu-11.1.1-virt/SOURCE b/lib/acpi/fixtures/qemu-11.1.1-virt/SOURCE index df4becb2b96..5dee691e01b 100644 --- a/lib/acpi/fixtures/qemu-11.1.1-virt/SOURCE +++ b/lib/acpi/fixtures/qemu-11.1.1-virt/SOURCE @@ -26,3 +26,9 @@ one file each: Those are `tests/iort.rs`'s regions, so the XSDT's own entries point at them and the walk under test is the real one. The XSDT's other six entries (FACP, PPTT, GTDT, MCFG, SPCR, DBG2) name tables that are not here. + +dsdt.bin is the DSDT of a boot of the same QEMU and firmware under `-M +virt,gic-version=3,its=on,iommu=smmuv3,kernel-irqchip=off -m 256`, written out +by `pmemsave` of 0x4c000000+64 MiB and found there by its signature and +checksum: 5406 bytes at 0x4cb41018, summing to zero. Its PCI root's `_CRS` is +what `acpiserver` declares on that machine. diff --git a/lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin b/lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin new file mode 100644 index 0000000000000000000000000000000000000000..a51a869cfbc452d7f15e2c887f808308c4ba864b GIT binary patch literal 5406 zcmZvg%WoT16o;>0u|0mouRQEHaSEm48QF33XlWt#*lE(niLuiL$(8I1foRLZ5*1QG zNNFmS(jAFlZKV7mNUYegKw`m~4gUZ;%o!)qjNI;i&59w}uK5fiZ6yuDADwkGYr#7mPjqLuvxn4` z^UI!Po?qNje)r;Kig5RXBlKloLHN}LCu`V(q5+g+z z>y&XtWJD!KhA^%WM#cSby!5_U5+g?#S1Cg*Q~n|GN`0Cz-lq(?PUF%#W(ngOWyp1! zkQfERxK0^zohBtlkuW}>47pAxCB_0_d`KB`ou(wlB4KP$hFqskn1!nF)k5?O&H5^o#rIQn}pGz47pCHB*xo>(WDHyPNyZtyM(bx8FHP@NQ@=I zxIr0moz6;(GGS~{hFqt4iLp!=+ms>KX+dIKCXAbuA=l}g#8@SaTa+Q!X;EUV6UGi@ z$aOj|F|HCui!iEkoi0d>YlN{&8FHPLB*q7XvF9?jgF9z!R(rQyw#%Dm%|{zOPtuAx zea`F7d{AFGmz(H!J6$wh4$55)L!k+wsEY#1oI69^)T?qoJq)Fr5z2Z)V3~8A<$j7d z3rz`ST@qO4+&SyGZEw5pnr79?>)0(DEvkvCy60=&z+=6>d$y0={Z6`M72C>I+v-!b zsLJ?j;MG^^zWM~uN<*ABt=-wr?tEz-W@5qEc&zk$qs(g`URP8muFqS3y**wKKu}eg zL@+)c`;^YXJU7x=n3yzUP9COuq0R)~nTwv%%ylE3=}kq$sIQ2rP9{Q~>0Kq@WO^$I zD4pp|4VX+!butm^OsvScO3=ylRuFVDy{Tw0T}vkuA@vL~&ybVpd4^1;w|IoqbAoxI zg~xilsc1;4Cqn8OW}ac@8Rnh{sb_?FMwllWSn7$8dPbQiZa&QJIa?Cto(QR@#XK$M ziH4VYBBY)%<{4w2G46?wdd8V&oOz-_rk)6?XM%Yqm}i1}BBY*4=9y%kXsD?tLh3om zJSUmwB=gs3$_|InO-jnddzBL`XdsnCAlX#0?GgL`XeL%(KKiOWYHo@yw79RJnKfq%m|#u)Xf z7w?c9XZTQ$>BEW6AN2XhDz_8v?{)3qi-*c?98^ZTp_9+kSu53R{Z;+9RNVOK;8E?D z>wkXs;-MA9gk?NO@LV10hru@kt27v;j!*9UzSZf@SY?zwK5>T4xs)Cm4)mFyEZ_x#sU{zx~}^`MUMP%s;LCuXbw`J$ax`(*d7(=pD6!oo1IbH@5Q= zmTvf}R{0fkXLsRU+BlciukhVyei5@HcbD`pc{iGkzWMFJ{ES!8O%i^_OJG3vJ!cJN z!1tC(J#2Q+L*c!OM6F=&Nlb@!v)kNR%q{hsclM)09bjiputT~xdDcGY)U88v0(ZEJ z&%kb2hrOe`9x+!py`OC^j$iEZdJY+?y6aM@X4tG;_j Option { + match tag { + WORD_ADDRESS_SPACE => Some(Self { first: 6, width: 2 }), + DWORD_ADDRESS_SPACE => Some(Self { first: 6, width: 4 }), + QWORD_ADDRESS_SPACE => Some(Self { first: 6, width: 8 }), + // A revision and a reserved byte before the granularity. + EXTENDED_ADDRESS_SPACE => Some(Self { first: 8, width: 8 }), + _ => None, + } + } + + /// The `n`th of the five fields, from the descriptor's first byte. + const fn field(&self, n: usize) -> usize { + self.first + n * self.width + } + + /// The bytes the five fields need. + const fn bytes(&self) -> usize { + self.field(5) + } +} /// ACPI 6.5 Table 6.44: what a resource type byte names. Every other value is /// reserved or vendor-defined. const TYPE_MEMORY: u8 = 0; const TYPE_IO: u8 = 1; const TYPE_BUS: u8 = 2; -/// ACPI 6.5 Table 6.43, bit 0 of the General Flags: set where the device -/// consumes the range itself rather than forwarding it downstream. +/// ACPI 6.5 Table 6.47, bit 0 of the General Flags: set where the device +/// consumes the range itself rather than producing it for what lies below. const CONSUMER: u8 = 1; /// Why a list of resource descriptors cannot be used. @@ -65,12 +87,14 @@ pub enum ResourceError { /// A window whose two accounts of its extent disagree: firmware named a /// maximum that is not its minimum plus its length. Inconsistent { min: u64, max: u64, length: u64 }, - /// A window whose address on the bridge's two sides differs. What this - /// answers is which addresses a *CPU* may issue, and translating one is a - /// machine nothing here has read. + /// A memory window whose address on the bridge's two sides differs: a + /// non-zero `_TRA`. A window is declared in the addresses a CPU issues, + /// and a device's above the CPU's own rest on the two being one. Translated { min: u64, offset: u64 }, - /// A memory range the bridge consumes rather than forwards — its own - /// registers, not a range anything behind it decodes. + /// A memory range the bridge consumes rather than produces — its own + /// registers, not a range anything behind it decodes. Refused rather than + /// stepped over: a root bridge's `_CRS` that names one says something + /// about its memory this decoder does not read. Consumed { min: u64 }, /// More memory windows, or I/O runs, than the caller has room for. TooMany { room: usize }, @@ -148,15 +172,21 @@ fn item(phys: P, at: u64, offset: usize) -> Result Ok(Item { tag, len }) } -fn u64le(phys: P, at: u64, offset: usize) -> u64 { - let mut v = 0u64; - for i in 0..8 { - v |= u64::from(phys.byte(at + offset as u64 + i)) << (8 * i); - } - v +/// The `width`-byte little-endian field `offset` bytes into the descriptor at `at`. +fn field(phys: P, at: u64, offset: usize, width: usize) -> u64 { + (0..width).fold(0u64, |v, i| v | u64::from(phys.byte(at + (offset + i) as u64)) << (8 * i)) } -/// Every memory window the list at `at` names, written into `out`, and how many. +/// Every memory window the root bridge's `_CRS` list at `at` says it +/// forwards, written into `out`, and how many. +/// +/// **A window is a memory range an Address Space Descriptor produces** +/// (ACPI 6.5 §6.4.3.5). Its bus numbers and I/O ranges, in those descriptors +/// and in the small I/O Port and Fixed I/O Port descriptors (§6.4.2.5, +/// §6.4.2.6), are stepped over; every other descriptor is refused by name, +/// as is a memory range whose two accounts of its extent disagree, that the +/// bridge consumes, or that a non-zero `_TRA` translates. A range of no +/// length is none. /// /// **A refusal carries no windows at all**: a caller handed the ones decoded /// before the refusal would be holding an aperture with a hole in it, and would @@ -170,50 +200,38 @@ pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) - } let item = item(phys, at, offset)?; let head = at + offset as u64; - let through = offset + item.len; - if item.tag == END_TAG { - return Ok(found); + offset += item.len; + let layout = match item.tag { + END_TAG => return Ok(found), + IO_PORT | FIXED_IO_PORT => continue, + tag => Layout::of(tag).ok_or(ResourceError::UnknownTag { tag })?, + }; + if item.len < layout.bytes() { + return Err(ResourceError::Short { tag: item.tag, whole: item.len, needed: layout.bytes() }); } - if item.tag != QWORD_ADDRESS_SPACE { - return Err(ResourceError::UnknownTag { tag: item.tag }); + let kind = phys.byte(head + RESOURCE_TYPE as u64); + match kind { + TYPE_MEMORY => {} + TYPE_IO | TYPE_BUS => continue, + kind => return Err(ResourceError::UnknownResourceType { kind }), } - if item.len < QWORD_BYTES { - return Err(ResourceError::Short { tag: item.tag, whole: item.len, needed: QWORD_BYTES }); + let read = |n| field(phys, head, layout.field(n), layout.width); + let (min, max, translation, length) = (read(1), read(2), read(3), read(4)); + if length == 0 { + continue; } - let kind = phys.byte(head + RESOURCE_TYPE as u64); - if !matches!(kind, TYPE_MEMORY | TYPE_IO | TYPE_BUS) { - return Err(ResourceError::UnknownResourceType { kind }); + if min.checked_add(length - 1) != Some(max) { + return Err(ResourceError::Inconsistent { min, max, length }); } - if kind == TYPE_MEMORY { - let min = u64le(phys, head, QWORD_MINIMUM); - let max = u64le(phys, head, QWORD_MAXIMUM); - let length = u64le(phys, head, QWORD_LENGTH); - let translation = u64le(phys, head, QWORD_TRANSLATION); - let flags = phys.byte(head + GENERAL_FLAGS as u64); - // A window of no length decodes nothing, and firmware emits one - // where a range is declared and unused. - if length != 0 { - let refusal = if min.checked_add(length - 1) != Some(max) { - Some(ResourceError::Inconsistent { min, max, length }) - } else if translation != 0 { - Some(ResourceError::Translated { min, offset: translation }) - } else if flags & CONSUMER != 0 { - Some(ResourceError::Consumed { min }) - } else { - None - }; - if let Some(why) = refusal { - return Err(why); - } - let room = out.len(); - match out.get_mut(found) { - Some(slot) => *slot = RootBridgeWindow { base: min, length }, - None => return Err(ResourceError::TooMany { room }), - } - found += 1; - } + if translation != 0 { + return Err(ResourceError::Translated { min, offset: translation }); + } + if phys.byte(head + GENERAL_FLAGS as u64) & CONSUMER != 0 { + return Err(ResourceError::Consumed { min }); } - offset = through; + let room = out.len(); + *out.get_mut(found).ok_or(ResourceError::TooMany { room })? = RootBridgeWindow { base: min, length }; + found += 1; } } diff --git a/lib/acpi/tests/common/mod.rs b/lib/acpi/tests/common/mod.rs index 7df0a8162df..b77d57ec64f 100644 --- a/lib/acpi/tests/common/mod.rs +++ b/lib/acpi/tests/common/mod.rs @@ -125,9 +125,17 @@ pub const BUS: u8 = 2; /// One QWORD Address Space Descriptor (ACPI 6.5 §6.4.3.5.1), field by field, /// its General and Type Specific Flags clear. pub fn qword_descriptor(kind: u8, granularity: u64, min: u64, max: u64, translation: u64, length: u64) -> Vec { - let mut d = vec![0x8A, 0x2B, 0x00, kind, 0x00, 0x00]; - for field in [granularity, min, max, translation, length] { - d.extend_from_slice(&field.to_le_bytes()); + address_space(0x8A, 8, kind, [granularity, min, max, translation, length]) +} + +/// One Address Space Descriptor of `tag` whose five fields are `width` bytes +/// each (ACPI 6.5 §6.4.3.5): `0x88` Word, `0x87` DWord, `0x8A` QWord, its +/// General and Type Specific Flags clear. +pub fn address_space(tag: u8, width: usize, kind: u8, fields: [u64; 5]) -> Vec { + let body = 3 + 5 * width; + let mut d = vec![tag, body as u8, 0x00, kind, 0x00, 0x00]; + for field in fields { + d.extend_from_slice(&field.to_le_bytes()[..width]); } d } @@ -138,19 +146,3 @@ pub fn resource_list(descriptors: &[Vec]) -> Vec { bytes.extend_from_slice(&[0x79, 0x00]); bytes } - -/// What OVMF's firmware answered for its root bridge 0 through -/// `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL::Configuration`, as captured. -pub const OVMF_ROOT_BRIDGE: &[u8] = include_bytes!("../../fixtures/ovmf-pure-efi/root-bridge-0.bin"); - -/// What the ThinkPad T14's firmware answers for its root bridge 0 through -/// `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL::Configuration`, decoded: its I/O range, -/// two memory windows and its bus range. -pub fn t14_root_bridge() -> Vec { - resource_list(&[ - qword_descriptor(IO, 0, 0x3000, 0x3fff, 0, 0x1000), - qword_descriptor(MEMORY, 0x20, 0xa200_0000, 0xbcff_ffff, 0, 0x1b00_0000), - qword_descriptor(MEMORY, 0x40, 0x40_0000_0000, 0x60_3dbf_ffff, 0, 0x20_3dc0_0000), - qword_descriptor(BUS, 0, 0, 0x79, 0, 0x7a), - ]) -} diff --git a/lib/acpi/tests/corpus.rs b/lib/acpi/tests/corpus.rs index 64ea5b48ec9..f5efffe4364 100644 --- a/lib/acpi/tests/corpus.rs +++ b/lib/acpi/tests/corpus.rs @@ -8,12 +8,11 @@ mod common; -use common::{declare_len, entry, madt, rsdp, sdt, t14_root_bridge, xsdt, Machine, OVMF_ROOT_BRIDGE}; -use toyos_abi::boot::RootBridgeWindow; +use common::{declare_len, entry, madt, rsdp, sdt, xsdt, Machine}; use toyos_abi::acpi::Block; use toyos_acpi::{ definition_blocks, dsdt_address, ecam_allocations, find_by_id, find_table, fixed_hardware, hpet_base, isa_line, - madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, + madt_entries, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, Reset, SourceOverride, Table, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, MAX_TABLE_LEN, @@ -716,44 +715,6 @@ fn a_fadt_that_ends_before_arm_boot_arch_is_short() { } } -/// Where the two firmwares' descriptor lists sit for the sweep below. -const ROOT_BRIDGE_AT: u64 = 0x4_0000; - -/// **No panic and no unbounded walk, over every byte of both firmwares' real -/// descriptor lists.** Each byte takes each of its 255 other values in turn and -/// the whole list is decoded over it. A resource list carries no checksum, -/// unlike the tables above, so a single byte reaches the decode directly. -#[test] -fn no_single_byte_mutation_of_a_firmwares_descriptor_list_panics_or_runs_away() { - let mut mutations = 0u64; - let mut refused = 0u64; - for (which, original) in [("ovmf", OVMF_ROOT_BRIDGE.to_vec()), ("thinkpad-t14", t14_root_bridge())] { - for offset in 0..original.len() { - for value in 0..=255u8 { - if original[offset] == value { - continue; - } - let mut mutated = original.to_vec(); - mutated[offset] = value; - let regions: &[(u64, &[u8])] = &[(ROOT_BRIDGE_AT, &mutated)]; - let mut out = [RootBridgeWindow::default(); 8]; - match memory_windows(Machine { regions }, ROOT_BRIDGE_AT, &mut out) { - Ok(count) => assert!( - out[..count].iter().all(|w| w.length != 0), - "{which} byte {offset} as {value:#04x}: a window of no length was carried" - ), - Err(_) => refused += 1, - } - mutations += 1; - } - } - } - // Both arms are reached: a sweep that only ever refused, or only ever - // decoded, would be measuring one path. - assert_eq!(mutations, 2 * 186 * 255); - assert!(refused > 0 && refused < mutations, "{refused} of {mutations} refused"); -} - /// A revision-6 FADT of 276 bytes naming q35's fixed hardware in both forms, /// for a test to break one field of. fn crafted(edit: impl FnOnce(&mut [u8])) -> Vec { diff --git a/lib/acpi/tests/fixtures.rs b/lib/acpi/tests/fixtures.rs index 7626d970fbe..2a5936728e6 100644 --- a/lib/acpi/tests/fixtures.rs +++ b/lib/acpi/tests/fixtures.rs @@ -3,12 +3,11 @@ mod common; -use common::{t14_root_bridge, Machine, OVMF_ROOT_BRIDGE}; -use toyos_abi::boot::RootBridgeWindow; +use common::Machine; use toyos_abi::acpi::Block; use toyos_acpi::{ century_of, definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, - isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, + isa_line, madt_entries, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, FixedHardware, IoApicEntry, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, SmiCmd, SourceOverride, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, MADT_ENTRIES, @@ -169,43 +168,6 @@ fn the_xsdt_walk_reaches_every_entry() { assert_eq!(find_table(m, RSDP, b"SSDT", 36).err(), Some(TableError::Absent)); } -const ROOT_BRIDGE: u64 = 0x7f00_0000; - -fn bridge_windows(bytes: &[u8]) -> Vec { - let regions: &[(u64, &[u8])] = &[(ROOT_BRIDGE, bytes)]; - let mut out = [RootBridgeWindow::default(); 8]; - let count = memory_windows(Machine { regions }, ROOT_BRIDGE, &mut out).expect("bytes a firmware answered with"); - out[..count].to_vec() -} - -/// The I/O and bus ranges in the same list are what make this more than a -/// count: a decoder that took every descriptor would answer four windows here. -#[test] -fn the_windows_a_boot_printed_are_what_that_firmwares_own_bytes_say() { - assert_eq!( - bridge_windows(OVMF_ROOT_BRIDGE), - [ - RootBridgeWindow { base: 0xc000_0000, length: 0x0010_0000 }, - RootBridgeWindow { base: 0x8_0000_0000, length: 0x0010_0000 }, - ] - ); -} - -/// The same decode against a second firmware, whose list is the same four -/// descriptors carrying entirely different numbers. The two together are what -/// say the decoder reads fields rather than offsets that happen to hold the -/// right values on one machine. -#[test] -fn a_second_firmwares_bytes_decode_to_that_machines_own_windows() { - assert_eq!( - bridge_windows(&t14_root_bridge()), - [ - RootBridgeWindow { base: 0xa200_0000, length: 0x1b00_0000 }, - RootBridgeWindow { base: 0x40_0000_0000, length: 0x20_3dc0_0000 }, - ] - ); -} - /// q35's ICH9 power-management block at the base QEMU's firmware put it, /// 0x600, as the FADT names it: the blocks the `acpi_press_is_one_sci` guest test /// takes its SCI through, and the 32-bit fields and their `X_` twins agreeing. diff --git a/lib/acpi/tests/resource.rs b/lib/acpi/tests/resource.rs index 5143196b739..096b4ff1d8b 100644 --- a/lib/acpi/tests/resource.rs +++ b/lib/acpi/tests/resource.rs @@ -3,8 +3,8 @@ mod common; -use common::{qword_descriptor, resource_list as list, Machine, BUS, IO, MEMORY}; -use toyos_abi::boot::RootBridgeWindow; +use common::{address_space, qword_descriptor, resource_list as list, Machine, BUS, IO, MEMORY}; +use toyos_abi::acpi::RootBridgeWindow; use toyos_acpi::{io_ports, memory_windows, ResourceError, MAX_LIST_BYTES}; /// Where a firmware pool allocation sits in the crafted machines below. @@ -27,9 +27,8 @@ fn windows(bytes: &[u8], room: usize) -> Result, ResourceE Ok(out) } -/// The shape the T14's firmware answers in, as Linux's own journal reports it: -/// two memory windows and the I/O and bus ranges beside them, of which only the -/// memory ones are windows a BAR could be placed in. +/// Two memory windows and the I/O and bus ranges beside them, of which only +/// the memory ones are windows a BAR could be placed in. #[test] fn only_the_memory_ranges_of_a_root_bridge_become_windows() { let bytes = list(&[ @@ -89,9 +88,9 @@ fn a_maximum_that_is_not_the_minimum_plus_the_length_is_refused() { assert!(matches!(windows(&list(&[swapped]), 8), Err(ResourceError::Inconsistent { .. }))); } -/// A window whose addresses differ on the two sides of the bridge. What this -/// answers is which addresses a CPU may issue, and no machine in reach -/// translates one — so it is refused rather than carried untranslated. +/// A window whose addresses differ on the two sides of the bridge: the +/// declaration is in the addresses a CPU issues, so it is refused rather +/// than carried untranslated. #[test] fn a_translated_window_is_refused_rather_than_taken_at_its_minimum() { let bytes = list(&[qword(MEMORY, 0x1000_0000, 0x1000, 0x4000_0000)]); @@ -125,16 +124,69 @@ fn a_range_the_bridge_consumes_is_not_a_window_it_forwards() { } } -/// The narrower address space descriptors, and anything else: refused by the -/// tag rather than stepped over. A window silently dropped is an aperture the -/// kernel would then place a BAR outside of. +/// A root bridge's `_CRS` as QEMU's `virt` publishes it (measured: its DSDT +/// evaluated, `lib/acpi/fixtures/qemu-11.1.1-virt/SOURCE`), descriptor for +/// descriptor: a Word bus range, a DWord memory window, a DWord I/O range +/// translated to its CPU address, and a QWord memory window. The two memory +/// windows are the declaration; the I/O range's translation is no refusal, +/// since it is no memory. +#[test] +fn every_width_of_address_space_descriptor_is_read() { + let crs = list(&[ + address_space(0x88, 2, BUS, [0, 0, 0xff, 0, 0x100]), + address_space(0x87, 4, MEMORY, [0, 0x1000_0000, 0x3efe_ffff, 0, 0x2eff_0000]), + address_space(0x87, 4, IO, [0, 0, 0xffff, 0x3eff_0000, 0x1_0000]), + address_space(0x8A, 8, MEMORY, [0, 0x80_0000_0000, 0xff_ffff_ffff, 0, 0x80_0000_0000]), + ]); + assert_eq!( + windows(&crs, 8).expect("virt's _CRS"), + [ + RootBridgeWindow { base: 0x1000_0000, length: 0x2eff_0000 }, + RootBridgeWindow { base: 0x80_0000_0000, length: 0x80_0000_0000 }, + ] + ); + // The Extended descriptor (§6.4.3.5.4): its fields two bytes later. + let mut extended = vec![0x8B, 0x35, 0x00, MEMORY, 0x00, 0x00, 0x01, 0x00]; + for field in [0u64, 0xc000_0000, 0xcfff_ffff, 0, 0x1000_0000, 0] { + extended.extend_from_slice(&field.to_le_bytes()); + } + assert_eq!(windows(&list(&[extended]), 8), Ok(vec![RootBridgeWindow { base: 0xc000_0000, length: 0x1000_0000 }])); +} + +/// q35's `_CRS` carries the configuration ports beside its ranges as a small +/// I/O Port Descriptor: stepped over with the I/O ranges, and what follows it +/// still read. +#[test] +fn the_small_io_descriptors_are_stepped_over() { + let crs = list(&[ + vec![0x47, 0x01, 0xf8, 0x0c, 0xf8, 0x0c, 0x01, 0x08], + vec![0x4B, 0x60, 0x00, 0x01], + address_space(0x87, 4, MEMORY, [0, 0x8000_0000, 0xafff_ffff, 0, 0x3000_0000]), + ]); + assert_eq!(windows(&crs, 8), Ok(vec![RootBridgeWindow { base: 0x8000_0000, length: 0x3000_0000 }])); +} + +/// Every other descriptor is refused by its tag rather than stepped over: a +/// memory descriptor this decoder does not read (Memory24, Memory32, Fixed +/// Memory32, §6.4.3.1-3) may be a window, and an IRQ or DMA descriptor names +/// something no root bridge's `_CRS` should. #[test] fn a_descriptor_this_decoder_does_not_read_is_refused_by_its_tag() { - for tag in [0x87u8, 0x88, 0x8B] { - let mut d = vec![tag, 0x17, 0x00]; - d.resize(3 + 0x17, 0); + for tag in [0x81u8, 0x85, 0x86] { + let mut d = vec![tag, 0x09, 0x00]; + d.resize(3 + 0x09, 0); assert_eq!(windows(&list(&[d]), 8), Err(ResourceError::UnknownTag { tag })); } + assert_eq!(windows(&list(&[vec![0x22, 0x01, 0x00]]), 8), Err(ResourceError::UnknownTag { tag: 0x22 })); +} + +/// A non-zero `_TRA` on a memory window is refused by name, in each width. +#[test] +fn a_translated_window_is_refused_in_every_width() { + for (tag, width) in [(0x88u8, 2usize), (0x87, 4), (0x8A, 8)] { + let d = address_space(tag, width, MEMORY, [0, 0x1000, 0x1fff, 0x100, 0x1000]); + assert_eq!(windows(&list(&[d]), 8), Err(ResourceError::Translated { min: 0x1000, offset: 0x100 }), "{tag:#x}"); + } } /// And the resource type inside a descriptor this decoder does read: reserved diff --git a/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index 304d9355ed7..94caeea8306 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -29,6 +29,17 @@ //! ([`op::S5`]): what `\_S5` evaluates to is in the firmware's AML, and //! until a holder has supplied it [`crate::syscall::SYS_SHUTDOWN`] is refused //! [`crate::syscall::SyscallError::NotSupported`]. +//! +//! **The memory the PCI root bridges decode is the holder's to declare, once** +//! ([`op::DECLARE`], [`Apertures`]): each root's `_CRS` is AML. Until a +//! declaration is held the kernel hands over no PCI function, and a claim +//! made meanwhile waits for it; a declaration the kernel refuses, or a claim +//! that ends without one, leaves every PCI claim refused for the boot. +//! +//! **A hardware-reduced machine** ([`HARDWARE_REDUCED`]; ACPI 6.5 §4.1) has +//! no fixed hardware: its claim names no block and raises no SCI, and its +//! holder reaches the tables, firmware's memory and configuration space +//! through [`Access`] alone. crate::user_safe! { /// A run of ports a register block occupies; `len` 0 is no block. @@ -53,6 +64,10 @@ impl Block { /// `PWRBTN_EN` in the PM1 event block. pub const FIXED_POWER_BUTTON: u16 = 1 << 0; +/// The FADT's `HW_REDUCED_ACPI` (ACPI 6.5 Table 5.10): the claim's two +/// blocks are [`Block::NONE`], and no SCI is raised on it. +pub const HARDWARE_REDUCED: u16 = 1 << 1; + crate::user_safe! { /// The claim's description. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -88,6 +103,10 @@ pub mod op { /// control to power the machine off. Once under a claim, and kept until /// the next claim's holder supplies its own. pub const S5: u64 = 3; + /// Declare the memory the PCI root bridges decode: an + /// [`Apertures`](super::Apertures), in through the caller's struct. Once + /// a boot, from the claim's holder. + pub const DECLARE: u64 = 4; pub const TAKEN: u64 = 0; pub const PENDING: u64 = 1; @@ -224,6 +243,12 @@ pub enum Refused { /// package's power limit, or what places the IOMMU or redirects /// interrupts. KernelRegister = 23, + /// A register's access on a machine whose kernel maps no register for + /// AML: an AArch64 kernel's direct map holds memory alone, and no range + /// register types an address a register there. + NoRegisters = 24, + /// A port access on a machine with no port space. + NoPorts = 25, } impl Refused { @@ -251,6 +276,8 @@ impl Refused { 21 => Self::HeldFunction, 22 => Self::AddressMap, 23 => Self::KernelRegister, + 24 => Self::NoRegisters, + 25 => Self::NoPorts, _ => return None, }) } @@ -291,3 +318,99 @@ impl Access { Self { address, value, space: space as u8, width: width as u8, write: 1, refused: 0, memory_type: UNLISTED, reserved: [0; 3] } } } + +crate::user_safe! { + /// One memory window a PCI root bridge decodes, in the addresses a CPU + /// issues. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + pub struct RootBridgeWindow { + pub base: u64, + pub length: u64, + } +} + +impl RootBridgeWindow { + /// The first address past the window, saturating: a firmware-named length + /// is untrusted input and an extent that would wrap is one this cannot + /// contain anything past. + pub fn end(&self) -> u64 { + self.base.saturating_add(self.length) + } + + /// Whether every address of the `length` bytes at `base` is one this window + /// decodes. + /// + /// The whole extent and not its first address: a BAR that starts inside a + /// window and runs past it decodes addresses the bridge does not. An extent + /// of no length carries no address for a window to decode and is inside + /// none of them. + pub fn holds(&self, base: u64, length: u64) -> bool { + length != 0 + && base >= self.base + && base.checked_add(length).is_some_and(|end| end <= self.end()) + } +} + +/// The most windows one declaration carries. +pub const MAX_WINDOWS: usize = 32; + +crate::user_safe! { + /// What [`op::DECLARE`] carries: the first `count` of `windows`, every + /// root bridge's together. + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub struct Apertures { + pub count: u64, + pub windows: [RootBridgeWindow; MAX_WINDOWS], + } +} + +impl Apertures { + /// `windows` as a declaration, or `None` past [`MAX_WINDOWS`]. + pub fn new(windows: &[RootBridgeWindow]) -> Option { + let mut declared = Self { count: windows.len() as u64, windows: [RootBridgeWindow::default(); MAX_WINDOWS] }; + declared.windows.get_mut(..windows.len())?.copy_from_slice(windows); + Some(declared) + } + + /// The windows declared, or `None` for a count past the array. + pub fn windows(&self) -> Option<&[RootBridgeWindow]> { + self.windows.get(..usize::try_from(self.count).ok()?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_window_holds_an_extent_and_not_merely_its_first_address() { + let window = RootBridgeWindow { base: 0xa080_0000, length: 0x1f80_0000 }; + assert_eq!(window.end(), 0xc000_0000); + + assert!(window.holds(0xbcf0_0000, 0x2_0000)); + assert!(window.holds(0xa080_0000, 0x2_0000)); + assert!(window.holds(0xa080_0000, 0x1f80_0000)); + assert!(window.holds(0xbfff_f000, 0x1000)); + assert!(!window.holds(0xa07f_f000, 0x2_0000)); + assert!(!window.holds(0x9920_0000, 0x1000)); + assert!(!window.holds(0xbfff_f000, 0x1001)); + assert!(!window.holds(0xbfff_0000, 0x1001_0000)); + assert!(!window.holds(0xc000_0000, 0)); + assert!(!window.holds(0xa080_0000, 0)); + + let whole = RootBridgeWindow { base: 1, length: u64::MAX }; + assert_eq!(whole.end(), u64::MAX); + assert!(!whole.holds(2, u64::MAX)); + } + + /// A list that fits is carried whole and read back as itself; one past the + /// array is no declaration, and a count past it reads back as none. + #[test] + fn a_declaration_carries_its_windows_and_no_count_past_them() { + let w = RootBridgeWindow { base: 0x1000_0000, length: 0x2eff_0000 }; + let declared = Apertures::new(&[w]).expect("one window fits"); + assert_eq!(declared.windows(), Some(&[w][..])); + assert_eq!(Apertures::new(&[w; MAX_WINDOWS + 1]), None); + assert_eq!(Apertures { count: MAX_WINDOWS as u64 + 1, ..declared }.windows(), None); + } +} diff --git a/sdk/abi/src/boot.rs b/sdk/abi/src/boot.rs index 12d0feff027..6dcea7dae60 100644 --- a/sdk/abi/src/boot.rs +++ b/sdk/abi/src/boot.rs @@ -77,17 +77,6 @@ pub struct KernelArgs { /// left to protect. pub cmdline_addr: u64, pub cmdline_len: u64, - /// How many of [`Self::root_bridge_windows`] firmware named. - pub root_bridge_window_count: u64, - /// The memory windows the platform's root bridges decode, as the addresses - /// a CPU issues. - /// - /// **An address outside every one of these is not free space, it is - /// unrouted**: a read of it answers all-ones, which is what an absent - /// device answers too. So this is what a BAR the kernel re-places has to - /// land inside, and there is no deriving it from the memory map — the map - /// says what firmware *used*, not what the bridge would decode. - pub root_bridge_windows: [RootBridgeWindow; MAX_ROOT_BRIDGE_WINDOWS], /// Where the loader put ROOT: its filesystem, read through the firmware's /// block I/O into `LoaderData` pages, which the kernel mounts from memory and /// keeps out of its allocator by this address, as it keeps the black box's. @@ -174,39 +163,6 @@ pub const SLOT_PARAM: &str = "boot-slot="; /// log. pub const SLOT_REFUSED_PARAM: &str = "slot-refused="; -/// The most windows the loader will carry. -pub const MAX_ROOT_BRIDGE_WINDOWS: usize = 64; - -/// One memory window a root bridge decodes. -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct RootBridgeWindow { - pub base: u64, - pub length: u64, -} - -impl RootBridgeWindow { - /// The first address past the window, saturating: a firmware-named length - /// is untrusted input and an extent that would wrap is one this cannot - /// contain anything past. - pub fn end(&self) -> u64 { - self.base.saturating_add(self.length) - } - - /// Whether every address of the `length` bytes at `base` is one this window - /// decodes. - /// - /// The whole extent and not its first address: a BAR that starts inside a - /// window and runs past it decodes addresses the bridge does not. An extent - /// of no length carries no address for a window to decode and is inside - /// none of them. - pub fn holds(&self, base: u64, length: u64) -> bool { - length != 0 - && base >= self.base - && base.checked_add(length).is_some_and(|end| end <= self.end()) - } -} - /// The token naming the filesystem the kernel mounts as root. const ROOT_PARAM: &str = "root="; @@ -226,14 +182,6 @@ pub fn actuators(cmdline: &str) -> impl Iterator { cmdline.split(',').filter(|t| !t.is_empty() && !t.starts_with(ROOT_PARAM)) } -impl KernelArgs { - /// The windows firmware named, and none of the array behind them. The - /// loader is the only writer of the count, so one past the array panics - /// here rather than clamping. - pub fn root_bridge_windows(&self) -> &[RootBridgeWindow] { - &self.root_bridge_windows[..self.root_bridge_window_count as usize] - } -} /// The kernel's `_start` reads three of these fields out of `rdi` by hardcoded /// byte offset, before Rust code runs and before there is a stack to call a @@ -256,28 +204,24 @@ const _: () = { assert!(offset_of!(KernelArgs, layout) == 164); assert!(offset_of!(KernelArgs, cmdline_addr) == 168); assert!(offset_of!(KernelArgs, cmdline_len) == 176); - assert!(offset_of!(KernelArgs, root_bridge_window_count) == 184); - assert!(offset_of!(KernelArgs, root_bridge_windows) == 192); - assert!(offset_of!(KernelArgs, root_image_addr) == 1216); - assert!(offset_of!(KernelArgs, root_image_len) == 1224); - assert!(offset_of!(KernelArgs, root_partition_guid) == 1232); - assert!(offset_of!(KernelArgs, loader_entry_counter) == 1248); - assert!(offset_of!(KernelArgs, loader_handoff_counter) == 1256); - assert!(offset_of!(KernelArgs, root_read_ticks) == 1264); - assert!(offset_of!(KernelArgs, loader_seed) == 1272); - assert!(offset_of!(KernelArgs, loader_seed_len) == 1304); - assert!(offset_of!(KernelArgs, wall_clock_secs) == 1312); - assert!(offset_of!(KernelArgs, wall_clock_counter) == 1320); - assert!(offset_of!(KernelArgs, wall_clock_known) == 1328); - assert!(offset_of!(KernelArgs, gop_edid) == 1336); - assert!(offset_of!(KernelArgs, gop_edid_len) == 1464); - assert!(offset_of!(KernelArgs, microcode_addr) == 1472); - assert!(offset_of!(KernelArgs, microcode_len) == 1480); - assert!(size_of::() == 1488); + assert!(offset_of!(KernelArgs, root_image_addr) == 184); + assert!(offset_of!(KernelArgs, root_image_len) == 192); + assert!(offset_of!(KernelArgs, root_partition_guid) == 200); + assert!(offset_of!(KernelArgs, loader_entry_counter) == 216); + assert!(offset_of!(KernelArgs, loader_handoff_counter) == 224); + assert!(offset_of!(KernelArgs, root_read_ticks) == 232); + assert!(offset_of!(KernelArgs, loader_seed) == 240); + assert!(offset_of!(KernelArgs, loader_seed_len) == 272); + assert!(offset_of!(KernelArgs, wall_clock_secs) == 280); + assert!(offset_of!(KernelArgs, wall_clock_counter) == 288); + assert!(offset_of!(KernelArgs, wall_clock_known) == 296); + assert!(offset_of!(KernelArgs, gop_edid) == 304); + assert!(offset_of!(KernelArgs, gop_edid_len) == 432); + assert!(offset_of!(KernelArgs, microcode_addr) == 440); + assert!(offset_of!(KernelArgs, microcode_len) == 448); + assert!(size_of::() == 456); assert!(LAYOUT as i32 > 1440 || (LAYOUT as i32) < -1440); assert!(align_of::() == 8); - assert!(size_of::() == 16); - assert!(align_of::() == 8); }; #[repr(C)] @@ -312,93 +256,4 @@ mod tests { assert_eq!(root_uuid(""), None); assert_eq!(actuators("").count(), 0); } - - #[test] - fn a_window_holds_an_extent_and_not_merely_its_first_address() { - let window = RootBridgeWindow { base: 0xa080_0000, length: 0x1f80_0000 }; - assert_eq!(window.end(), 0xc000_0000); - - assert!(window.holds(0xbcf0_0000, 0x2_0000)); - assert!(window.holds(0xa080_0000, 0x2_0000)); - assert!(window.holds(0xa080_0000, 0x1f80_0000)); - assert!(window.holds(0xbfff_f000, 0x1000)); - assert!(!window.holds(0xa07f_f000, 0x2_0000)); - assert!(!window.holds(0x9920_0000, 0x1000)); - assert!(!window.holds(0xbfff_f000, 0x1001)); - assert!(!window.holds(0xbfff_0000, 0x1001_0000)); - assert!(!window.holds(0xc000_0000, 0)); - assert!(!window.holds(0xa080_0000, 0)); - - let whole = RootBridgeWindow { base: 1, length: u64::MAX }; - assert_eq!(whole.end(), u64::MAX); - assert!(!whole.holds(2, u64::MAX)); - } - - const ZEROED: KernelArgs = KernelArgs { - memory_map_addr: 0, - memory_map_size: 0, - kernel_memory_addr: 0, - kernel_memory_size: 0, - kernel_stack_addr: 0, - kernel_stack_size: 0, - rsdp_addr: 0, - kernel_elf_addr: 0, - kernel_elf_size: 0, - gop_framebuffer: 0, - gop_framebuffer_size: 0, - gop_width: 0, - gop_height: 0, - gop_stride: 0, - gop_pixel_format: 0, - boot_pml4_addr: 0, - boot_partition_start_lba: 0, - boot_partition_blocks: 0, - boot_partition_guid: [0; 16], - boot_partition_present: 0, - log_partition_guid: [0; 16], - layout: LAYOUT, - cmdline_addr: 0, - cmdline_len: 0, - root_bridge_window_count: 0, - root_bridge_windows: [RootBridgeWindow { base: 0, length: 0 }; - MAX_ROOT_BRIDGE_WINDOWS], - root_image_addr: 0, - root_image_len: 0, - root_partition_guid: [0; 16], - loader_entry_counter: 0, - loader_handoff_counter: 0, - root_read_ticks: 0, - loader_seed: [0; SEED_LEN], - loader_seed_len: 0, - wall_clock_secs: 0, - wall_clock_counter: 0, - wall_clock_known: 0, - gop_edid: [0; EDID_BLOCK], - gop_edid_len: 0, - microcode_addr: 0, - microcode_len: 0, - }; - - #[test] - fn the_kernel_is_handed_the_windows_firmware_named_and_none_of_the_array_behind_them() { - assert!(ZEROED.root_bridge_windows().is_empty()); - - let low = RootBridgeWindow { base: 0xa080_0000, length: 0x1f80_0000 }; - let high = RootBridgeWindow { base: 0x40_0000_0000, length: 0x20_0000_0000 }; - let behind = RootBridgeWindow { base: 0xdead_0000, length: 0x1000 }; - let mut args = KernelArgs { root_bridge_window_count: 2, ..ZEROED }; - args.root_bridge_windows[0] = low; - args.root_bridge_windows[1] = high; - args.root_bridge_windows[2] = behind; - - assert_eq!(args.root_bridge_windows(), &[low, high][..]); - } - - #[test] - #[should_panic(expected = "out of range")] - fn a_count_past_the_array_is_refused_rather_than_clamped() { - let args = - KernelArgs { root_bridge_window_count: MAX_ROOT_BRIDGE_WINDOWS as u64 + 1, ..ZEROED }; - let _windows = args.root_bridge_windows(); - } } diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 47746e7daa1..294a83641f5 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -282,7 +282,7 @@ pub const SYS_TRACE_READ: u64 = 127; /// One operation on the `acpi` claim ([`crate::acpi::op`]): an access the /// kernel mediates, the firmware's Global Lock, or the power-off's sleep type. -/// See [`acpi_access`], [`acpi_lock_take`] and [`acpi_s5`]. +/// See [`acpi_access`], [`acpi_lock_take`], [`acpi_s5`] and [`acpi_declare`]. pub const SYS_ACPI: u64 = 128; /// Bind the `hda-audio` claim's stream to the listed controller it names. See @@ -2111,6 +2111,15 @@ pub fn acpi_s5(claim: RawHandle, slp_typ_a: u64) -> Result<(), SyscallError> { check_unit(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::S5, slp_typ_a, 0)) } +/// Declare the memory the PCI root bridges decode ([`crate::acpi::op::DECLARE`]). +/// `Ok` is the declaration held; [`SyscallError::InvalidArgument`] one the +/// kernel refused, said in its log by name and leaving every PCI claim refused; +/// [`SyscallError::AlreadyExists`] a declaration made after the first, or after +/// the kernel stopped waiting for one. +pub fn acpi_declare(claim: RawHandle, declared: &crate::acpi::Apertures) -> Result<(), SyscallError> { + check_unit(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::DECLARE, declared as *const crate::acpi::Apertures as u64, 0)) +} + /// Sleep for the given number of nanoseconds. pub fn nanosleep(nanos: u64) { syscall(SYS_NANOSLEEP, nanos, 0, 0, 0); diff --git a/sdk/toyos/src/device.rs b/sdk/toyos/src/device.rs index 901a62a6fa6..d38ae5c25c7 100644 --- a/sdk/toyos/src/device.rs +++ b/sdk/toyos/src/device.rs @@ -233,6 +233,12 @@ impl AcpiDev { pub fn s5(&self, slp_typ_a: u64) -> Result<(), SyscallError> { syscall::acpi_s5(self.as_handle(), slp_typ_a) } + + /// Declare the memory the PCI root bridges decode + /// ([`syscall::acpi_declare`]). + pub fn declare(&self, declared: &toyos_abi::acpi::Apertures) -> Result<(), SyscallError> { + syscall::acpi_declare(self.as_handle(), declared) + } } impl AsHandle for AcpiDev { diff --git a/system/acpiserver/Cargo.toml b/system/acpiserver/Cargo.toml index 6680ac7df1c..fe699b3c833 100644 --- a/system/acpiserver/Cargo.toml +++ b/system/acpiserver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "acpiserver" -description = "The machine's ACPI fixed hardware, served from userland: the SCI, the power button and the embedded controller's events, and its AML loaded." +description = "The machine's ACPI, served from userland: its AML loaded, the PCI root bridges' memory declared, and the SCI, the power button and the embedded controller's events." version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index 49d25f71e0c..f40071426aa 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -40,7 +40,7 @@ use std::time::Instant; use toyos_acpi::TableError; use toyos_aml::{Error, Host, Interpreter, Value}; -use crate::host::{Firmware, Kernel, Refusal, Stopping, Unmade, OWN}; +use crate::host::{Firmware, Handed, Kernel, Refusal, Stopping, Unmade, OWN}; use crate::tables::Tables; /// A machine's namespace, and the host every evaluation in it is answered by. @@ -199,17 +199,22 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi host.notifies ); + let mut unused = false; if loaded.blocks.first().is_some_and(|dsdt| dsdt.is_ok()) { match s5(interpreter, host) { Ok((a, b)) => { println!("acpiserver: \\_S5 evaluated: SLP_TYPa={a} SLP_TYPb={b}"); loaded.s5 = Some((a, b)); match kernel.s5(a) { - Ok(true) => { + Ok(Handed::Kept) => { println!("acpiserver: \\_S5 handed to the kernel: SLP_TYPa={a}"); loaded.handed = true; } - Ok(false) => println!("acpiserver: \\_S5 refused: the kernel keeps no SLP_TYPa wider than the register's three bits"), + Ok(Handed::TooWide) => println!("acpiserver: \\_S5 refused: the kernel keeps no SLP_TYPa wider than the register's three bits"), + Ok(Handed::Unused) => { + println!("acpiserver: \\_S5 not handed: this machine is hardware-reduced, and its power-off writes no sleep type"); + unused = true; + } Err(Stopping) => { println!("{STOPPING}"); return loaded; @@ -223,7 +228,7 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi Err(kind) => println!("acpiserver: \\_S5 refused: {kind}"), } } - if !loaded.handed { + if !loaded.handed && !unused { toyos::error!("{NO_S5_HANDED}"); } if !host.refused.is_empty() { diff --git a/system/acpiserver/src/apertures.rs b/system/acpiserver/src/apertures.rs new file mode 100644 index 00000000000..e1c57d152cf --- /dev/null +++ b/system/acpiserver/src/apertures.rs @@ -0,0 +1,206 @@ +//! The memory the PCI root bridges decode, declared to the kernel once +//! ([`Kernel::declare`]): every PCI claim on the machine waits for it, so it +//! is read as soon as the namespace is initialised (ACPI 6.5 §6.5.1). +//! +//! **A root is a PCI host bridge by its `_HID` or a `_CID`** (PNP0A03 or +//! PNP0A08, §6.1.5, §6.1.2) **below no other one**; a root whose `_STA` says +//! it is not present decodes nothing and is not read. Its windows are what +//! `toyos_acpi::memory_windows` reads of its `_CRS` (§6.2.2). +//! +//! **The declaration is every root's windows together, or none at all**: a +//! root whose `_STA` or `_CRS` is refused, a list the decoder refuses, more +//! windows than one declaration carries, and a namespace with no root are +//! each said by name and declared as no window, which the kernel refuses — +//! and with it every PCI claim, rather than one placed by a list with a hole +//! in it. A machine with no namespace declares the same. + +use toyos_abi::acpi::{RootBridgeWindow, MAX_WINDOWS}; +use toyos_aml::{Kind, Value}; + +use crate::aml::{kind, Aml}; +use crate::host::{Declared, Kernel, Stopping, OWN}; +use crate::init::{evaluate, host_bridge, Census, Names, PRESENT}; + +/// Every root bridge's windows, in walk order, or why there is no +/// declaration. +pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) -> Result, String> { + let devices = names.objects.iter().filter(|named| named.kind == Kind::Device).map(|named| named.path.as_str()); + let mut bridges: Vec<&str> = Vec::new(); + for device in devices { + if !bridges.iter().any(|above| below(device, above)) && host_bridge(aml, names, device, census) { + bridges.push(device); + } + } + if bridges.is_empty() { + return Err("the namespace names no PCI host bridge".into()); + } + let mut windows = Vec::new(); + for root in bridges { + if !present(aml, names, root, census)? { + println!("{OWN}{root} is not present, so it decodes nothing"); + continue; + } + let path = format!("{root}._CRS"); + let crs = match aml.interpreter.evaluate(&mut aml.host, &path, &[]) { + Ok(Value::Buffer(crs)) => crs, + Ok(_) => return Err(said(&path, "a root bridge's _CRS is no buffer")), + Err(why) => return Err(said(&path, &format!("a root bridge's _CRS was refused: {}", kind(&why)))), + }; + let mut out = [RootBridgeWindow::default(); MAX_WINDOWS]; + let room = MAX_WINDOWS - windows.len(); + match toyos_acpi::memory_windows(&crs[..], 0, &mut out[..room]) { + Ok(count) => windows.extend_from_slice(&out[..count]), + Err(why) => return Err(said(&path, &format!("a root bridge's _CRS is no list of windows: {why}"))), + } + } + Ok(windows) +} + +/// Declare what [`read`] answers, or no window where it refused, saying +/// which and what the kernel did with it. +pub fn declare(kernel: &K, read: Result, String>) -> Result<(), Stopping> { + let windows = match read { + Ok(windows) => windows, + Err(why) => { + println!("acpiserver: no window declared, so every PCI claim is refused: {why}"); + Vec::new() + } + }; + let listed: Vec = windows.iter().map(|w| format!("mem {:#x}..{:#x}", w.base, w.end())).collect(); + match kernel.declare(&windows)? { + Declared::Held => println!("acpiserver: the root bridges' memory declared: {}", listed.join(", ")), + Declared::Refused if windows.is_empty() => {} + Declared::Refused => println!("acpiserver: the kernel refused the root bridges' memory as declared ({}); its log says why", listed.join(", ")), + Declared::Late => println!("acpiserver: the kernel already holds an answer for the root bridges' memory, so nothing is declared"), + } + Ok(()) +} + +/// Whether `device` lies in `above`'s scope. +fn below(device: &str, above: &str) -> bool { + device.strip_prefix(above).is_some_and(|rest| rest.starts_with('.')) +} + +/// Whether `root` is present: bit 0 of its `_STA`, where it has one; a +/// refused or non-integer `_STA` says nothing, and refuses the declaration. +fn present(aml: &mut Aml<'_, K>, names: &Names, root: &str, census: &mut Census) -> Result { + let path = format!("{root}._STA"); + if !names.paths.contains(&path) { + return Ok(true); + } + match evaluate(aml, census, "_STA", &path, &[]) { + Some(Value::Integer(status)) => Ok(status & PRESENT != 0), + _ => Err(said(&path, "a root bridge's _STA says nothing")), + } +} + +/// `why`, with the path the firmware chose said on a line of its own. +fn said(path: &str, why: &str) -> String { + println!("{OWN}{path}: {why}"); + why.into() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aml::load; + use crate::host::tests::Scripted; + + const Q35_DSDT: &[u8] = include_bytes!("../../../lib/acpi/fixtures/qemu-11.1.1/dsdt.bin"); + const VIRT_DSDT: &[u8] = include_bytes!("../../../lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin"); + + /// The windows a DSDT's roots declare, read as the server reads them. + fn declared(dsdt: &[u8]) -> Result, String> { + let kernel = crate::aml::tests::crafted(dsdt, &[]); + let (_, kept) = load(&kernel, crate::aml::tests::CRAFTED_RSDP); + let mut aml = kept.expect("the DSDT loads"); + let names = Names::read(&aml.interpreter).expect("its namespace walks"); + read(&mut aml, &names, &mut Census::default()) + } + + /// QEMU's `virt` (the measured case): its one root, `\_SB.PCI0` + /// (PNP0A08), declares its 32-bit and 64-bit memory windows and nothing + /// of its bus numbers, its I/O window translated to 0x3eff0000, ECAM or + /// flash. + #[test] + fn virts_root_declares_its_two_memory_windows() { + assert_eq!( + declared(VIRT_DSDT), + Ok(vec![ + RootBridgeWindow { base: 0x1000_0000, length: 0x2eff_0000 }, + RootBridgeWindow { base: 0x80_0000_0000, length: 0x80_0000_0000 }, + ]) + ); + } + + /// q35: one root whose `_CRS` names the VGA window, two 32-bit windows + /// and the 64-bit window above memory, and steps over its configuration + /// ports and I/O ranges. + #[test] + fn q35s_root_declares_its_memory_windows() { + assert_eq!( + declared(Q35_DSDT), + Ok(vec![ + RootBridgeWindow { base: 0xa_0000, length: 0x2_0000 }, + RootBridgeWindow { base: 0x8000_0000, length: 0x6000_0000 }, + RootBridgeWindow { base: 0xf000_0000, length: 0xec0_0000 }, + RootBridgeWindow { base: 0xc0_0000_0000, length: 0x8_0000_0000 }, + ]) + ); + } + + /// No byte of either `_CRS` as evaluated panics the decoder or runs it + /// away: each takes each of its 255 other values in turn, and both arms, + /// windows and refusal, are reached. + #[test] + fn no_single_byte_mutation_of_a_roots_crs_panics_or_runs_away() { + for dsdt in [Q35_DSDT, VIRT_DSDT] { + let kernel = crate::aml::tests::crafted(dsdt, &[]); + let (_, kept) = load(&kernel, crate::aml::tests::CRAFTED_RSDP); + let mut aml = kept.expect("the DSDT loads"); + let Ok(Value::Buffer(crs)) = aml.interpreter.evaluate(&mut aml.host, "\\_SB_.PCI0._CRS", &[]) else { + panic!("PCI0._CRS is a buffer"); + }; + let (mut refused, mut mutations) = (0u64, 0u64); + for offset in 0..crs.len() { + for value in (0..=255u8).filter(|&v| v != crs[offset]) { + let mut mutated = crs.clone(); + mutated[offset] = value; + let mut out = [RootBridgeWindow::default(); MAX_WINDOWS]; + match toyos_acpi::memory_windows(&mutated[..], 0, &mut out) { + Ok(count) => assert!(out[..count].iter().all(|w| w.length != 0), "byte {offset} as {value:#04x}"), + Err(_) => refused += 1, + } + mutations += 1; + } + } + assert!(refused > 0 && refused < mutations, "{refused} of {mutations} refused"); + } + } + + /// A root that is not a list of windows, one whose `_CRS` is refused, and + /// a namespace with none: no declaration, each said by name; and what the + /// server declares then is no window at all. + #[test] + fn a_root_that_declares_nothing_usable_refuses_the_whole_declaration() { + use crate::aml::tests::{cat, sealed}; + // Device (PCI0) { Name (_HID, "PNP0A08") Name (_CRS, Buffer { IRQ }) } + let device = |crs: &[u8]| { + let body = cat(&[&[0x08], b"_HID", &[0x0D], b"PNP0A08", &[0x00], &[0x08], b"_CRS", crs]); + let mut d = vec![0x5B, 0x82, (body.len() + 5) as u8]; + d.extend_from_slice(b"PCI0"); + d.extend_from_slice(&body); + d + }; + let irq = [0x11, 0x08, 0x0A, 0x05, 0x22, 0x01, 0x00, 0x79, 0x00]; + let refused = declared(&sealed(b"DSDT", &device(&irq))); + assert_eq!(refused, Err("a root bridge's _CRS is no list of windows: descriptor tag 0x22 is not decoded here".into())); + let integer = declared(&sealed(b"DSDT", &device(&[0x0A, 0x05]))); + assert_eq!(integer, Err("a root bridge's _CRS is no buffer".into())); + assert_eq!(declared(&sealed(b"DSDT", &[0x08, b'A', b'A', b'A', b'A', 0x00])), Err("the namespace names no PCI host bridge".into())); + + let kernel = Scripted::default(); + declare(&kernel, Err("the namespace names no PCI host bridge".into())).expect("not stopping"); + assert_eq!(*kernel.declared.borrow(), Some(Vec::new())); + } +} diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index f7b9dcca432..fa876ac56b2 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -92,7 +92,7 @@ use std::collections::BTreeMap; use std::fmt; use std::time::{Duration, Instant}; -use toyos_abi::acpi::{pci_address, Access, Refused, Space, Width, UNLISTED}; +use toyos_abi::acpi::{pci_address, Access, Refused, RootBridgeWindow, Space, Width, UNLISTED}; use toyos_aml::{Address, Denied, Host, RuntimeLoad, Transfer}; use crate::devices::Ec; @@ -145,9 +145,11 @@ pub trait Kernel { /// `false` is the deadline. fn released(&self, until: Instant) -> bool; /// Hand the kernel `\_S5`'s `SLP_TYPa` for its power-off, which it takes - /// once under a claim: `false` where it is wider than the register's - /// field, and the kernel kept nothing. - fn s5(&self, slp_typ_a: u64) -> Result; + /// once under a claim. + fn s5(&self, slp_typ_a: u64) -> Result; + /// Declare the memory the PCI root bridges decode, once + /// ([`crate::apertures`]). + fn declare(&self, windows: &[RootBridgeWindow]) -> Result; /// Waits until `until`, serving the SCI as `sci::await_sleep` does for /// the fixed power button where the claim serves it and the controller's /// GPE `ec_gpe`: what latched and was taken. @@ -162,6 +164,26 @@ pub trait Kernel { fn power_off(&self) -> toyos::power::Refused; } +/// What the kernel did with `\_S5`'s `SLP_TYPa`. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Handed { + Kept, + /// Wider than the register's field: the kernel kept nothing. + TooWide, + /// A hardware-reduced machine's power-off writes no sleep type. + Unused, +} + +/// What the kernel did with a declaration of the root bridges' memory. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Declared { + Held, + /// Refused whole, said in the kernel's log by name. + Refused, + /// The kernel already holds an answer: a declaration, or its refusal. + Late, +} + /// Why an access to one of the controller's ports was not made. pub enum Unmade { Stopping, @@ -758,6 +780,8 @@ pub mod tests { pub stops_after: Cell>, /// The `SLP_TYPa` it was handed and kept. pub handed: Cell>, + /// The windows it was declared, once. + pub declared: RefCell>>, /// What each Sleep's wait takes, in turn; nothing once it runs out. /// Each served set a Sleep was asked to serve. pub sleeps: RefCell>, @@ -873,15 +897,25 @@ pub mod tests { self.sleeps.borrow_mut().pop_front().unwrap_or_default() } - fn s5(&self, slp_typ_a: u64) -> Result { + fn s5(&self, slp_typ_a: u64) -> Result { self.stopping()?; assert!(!self.held.get(), "the sleep type was handed over under the Global Lock"); // ACPI 6.5 Table 4.16: `SLP_TYPx` is three bits. if slp_typ_a > 7 { - return Ok(false); + return Ok(Handed::TooWide); } assert_eq!(self.handed.replace(Some(slp_typ_a)), None, "a second sleep type under one claim, which the kernel refuses"); - Ok(true) + Ok(Handed::Kept) + } + + fn declare(&self, windows: &[RootBridgeWindow]) -> Result { + self.stopping()?; + let mut declared = self.declared.borrow_mut(); + if declared.is_some() { + return Ok(Declared::Late); + } + *declared = Some(windows.to_vec()); + Ok(if windows.is_empty() { Declared::Refused } else { Declared::Held }) } fn latched(&self) -> Option { diff --git a/system/acpiserver/src/init.rs b/system/acpiserver/src/init.rs index 9795bd518f1..70a98b8825f 100644 --- a/system/acpiserver/src/init.rs +++ b/system/acpiserver/src/init.rs @@ -83,17 +83,17 @@ impl Names { } } -fn is_device(kind: Kind) -> bool { +pub fn is_device(kind: Kind) -> bool { matches!(kind, Kind::Device | Kind::Processor) } /// The scope an object's path is in. -fn parent(path: &str) -> &str { +pub fn parent(path: &str) -> &str { path.rsplit_once('.').map_or("\\", |(scope, _)| scope) } /// `_STA`'s Present and Functional bits (§6.3.7). -const PRESENT: u64 = 1 << 0; +pub const PRESENT: u64 = 1 << 0; const FUNCTIONAL: u64 = 1 << 3; /// What the initialisation ran, and what of it was refused. @@ -141,7 +141,7 @@ impl Census { } /// Evaluate `path`, counted in `census` under `method` where refused. -fn evaluate(aml: &mut Aml<'_, K>, census: &mut Census, method: &str, path: &str, args: &[Value]) -> Option { +pub fn evaluate(aml: &mut Aml<'_, K>, census: &mut Census, method: &str, path: &str, args: &[Value]) -> Option { let answer = aml.interpreter.evaluate(&mut aml.host, path, args); census.longest_wait_us = census.longest_wait_us.max(aml.interpreter.usage().waited_us); match answer { @@ -241,7 +241,7 @@ fn announce_config(aml: &mut Aml<'_, K>, names: &Names, since: u32, c /// Whether `device` is a PCI host bridge by its `_HID` or a `_CID` (§6.1.5, /// §6.1.2): PNP0A03 or PNP0A08. -fn host_bridge(aml: &mut Aml<'_, K>, names: &Names, device: &str, census: &mut Census) -> bool { +pub fn host_bridge(aml: &mut Aml<'_, K>, names: &Names, device: &str, census: &mut Census) -> bool { for id in ["_HID", "_CID"] { if !names.has(device, id) { continue; diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 7bdc5f49ab5..305a0b19612 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -10,7 +10,9 @@ //! **At start** every fixed and general-purpose event is disabled and its //! status cleared, then the fixed power button is enabled where the FADT //! says the machine has one, and the SCI acknowledged: a press after the -//! clear latches and is served, one before it is lost. +//! clear latches and is served, one before it is lost. A hardware-reduced +//! machine (`toyos_abi::acpi::HARDWARE_REDUCED`) has no blocks and no SCI: +//! its tables are loaded and its namespace served, and no event. //! //! **Then the machine's tables are loaded** ([`aml::load`]), through the //! kernel's mediated access ([`Claim`]): after the arming, so a press during @@ -30,6 +32,9 @@ //! device at or above an `_INI` by Table 6.67, again over whatever a table //! loaded meanwhile added. //! +//! **Then the root bridges' memory is declared to the kernel** +//! ([`apertures`]), every PCI claim on the machine waiting for it. +//! //! **Then the embedded controller is found in the namespace and armed** //! ([`devices::find`], [`devices::attach`]): attached to the AML host in the ECDT's place, so its //! space reaches it, under the Global Lock where its `_GLK` asks for it @@ -69,6 +74,7 @@ //! of every one at [`COUNTS`] intervals, never a line per event. mod aml; +mod apertures; mod battery; mod devices; mod ec; @@ -89,7 +95,7 @@ use toyos::ioport::{in16, in8, out16, out8}; use toyos::poller::{Poller, READABLE}; use toyos::power::{self, Stop}; use toyos::AcpiDev; -use toyos_abi::acpi::{Access, AcpiInfo, Block, FIXED_POWER_BUTTON}; +use toyos_abi::acpi::{Access, AcpiInfo, Apertures, Block, RootBridgeWindow, FIXED_POWER_BUTTON, HARDWARE_REDUCED}; use toyos_abi::syscall::{DeviceType, SyscallError}; use acpiserver_api::{CONTROLLER_NONE, CONTROLLER_SERVED}; @@ -97,7 +103,7 @@ use aml::Aml; use battery::Power; use devices::{Ec, Queried}; use ec::Transaction; -use host::{port_in, Answer, Kernel, Stopping, Take, Unmade, OWN}; +use host::{port_in, Answer, Declared, Handed, Kernel, Stopping, Take, Unmade, OWN}; use init::{Census, Names}; use sci::{Bits, Event, Fixed, Served, Unserved, Unstopped, PM1_STATUS, PWRBTN}; @@ -151,9 +157,12 @@ fn main() { let info = dev.describe().expect("acpiserver: the claim's first read is its description"); let claim = Claim { dev: &dev, info, poller: Poller::new(1), watching: Cell::new(false) }; let mut server = Server::new(&claim, Machine, info); - server.arm(&dev); + if info.flags & HARDWARE_REDUCED == 0 { + server.arm(&dev); + } let (loaded, kept) = aml::load(&claim, server.info.rsdp); let names = server.loaded(loaded.handed, kept); + server.declare(names.as_ref()); let (batteries, adapters) = server.arm_controller(names.as_ref()); if let Some(names) = server.negotiate(names) { println!("{}", server.census.line()); @@ -255,10 +264,20 @@ impl Kernel for Claim<'_> { sci::await_sleep(&mut Waiting(self), until, &served) } - fn s5(&self, slp_typ_a: u64) -> Result { + fn s5(&self, slp_typ_a: u64) -> Result { match self.dev.s5(slp_typ_a) { - Err(SyscallError::InvalidArgument) => Ok(false), - answer => Self::answered("the power-off's sleep type", answer).map(|()| true), + Err(SyscallError::InvalidArgument) => Ok(Handed::TooWide), + Err(SyscallError::NotSupported) if self.info.flags & HARDWARE_REDUCED != 0 => Ok(Handed::Unused), + answer => Self::answered("the power-off's sleep type", answer).map(|()| Handed::Kept), + } + } + + fn declare(&self, windows: &[RootBridgeWindow]) -> Result { + let declared = Apertures::new(windows).expect("acpiserver: the windows were read into a declaration's room"); + match self.dev.declare(&declared) { + Err(SyscallError::InvalidArgument) => Ok(Declared::Refused), + Err(SyscallError::AlreadyExists) => Ok(Declared::Late), + answer => Self::answered("the root bridges' memory", answer).map(|()| Declared::Held), } } @@ -392,6 +411,18 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { self.kernel.ack(); } + /// Declare the root bridges' memory ([`apertures`]) out of the namespace + /// as initialised; with no namespace, no window. + fn declare(&mut self, names: Option<&Names>) { + let read = match (self.aml.as_mut(), names) { + (Some(aml), Some(names)) => apertures::read(aml, names, &mut self.census), + _ => Err("no namespace was kept".into()), + }; + if apertures::declare(self.kernel, read).is_err() { + println!("acpiserver: the machine is stopping, so the root bridges' memory is not declared"); + } + } + /// Initialise the namespace the load kept ([`init`]), and again whatever /// a table its initialisation loaded added, until none did; what comes /// back is the namespace as it then stands. From cd2c246faadf0410e1c32f03c663b9e64de33c10 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 15 Oct 2026 00:31:35 +0200 Subject: [PATCH 2/9] Stage B: acpiserver declares each PCI root's _CRS windows; kernel drivers map at identity Work in progress before review; the PR body states the change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- build/build.rs | 1 + ...-cargo-run-boot-hands-over-no-pci-claim.md | 48 ----- ...hat-undoes-a-placement-nothing-answered.md | 16 +- kernel/pci/src/placement.rs | 6 +- kernel/src/arch/aarch64/acpi_mode.rs | 201 ++++++++++++++++-- kernel/src/arch/aarch64/boot.rs | 8 +- kernel/src/arch/aarch64/pio.rs | 15 +- kernel/src/arch/aarch64/smmu/domain.rs | 15 +- kernel/src/arch/aarch64/smmu/mod.rs | 7 +- kernel/src/arch/aarch64/smmu/selftest.rs | 4 +- kernel/src/arch/x86_64/amdvi/domain.rs | 12 +- kernel/src/arch/x86_64/amdvi/mod.rs | 12 +- kernel/src/arch/x86_64/amdvi/selftest.rs | 4 +- kernel/src/arch/x86_64/iommu_unit.rs | 14 +- kernel/src/arch/x86_64/vtd/domain.rs | 14 +- kernel/src/arch/x86_64/vtd/mod.rs | 15 +- kernel/src/device.rs | 9 +- kernel/src/iommu/domains.rs | 82 ++++--- kernel/src/iommu/mod.rs | 47 ++-- kernel/src/iommu/window.rs | 89 +++++--- kernel/src/main.rs | 4 +- kernel/src/pcidev/mod.rs | 21 +- kernel/src/syscall/device.rs | 15 ++ loader/src/efi/mod.rs | 2 +- loader/src/efi/proto.rs | 45 ---- loader/src/gcd.rs | 174 --------------- loader/src/main.rs | 16 +- loader/src/rootbridge.rs | 93 -------- system/acpiserver/src/aml.rs | 21 +- system/acpiserver/src/host.rs | 11 +- system/acpiserver/src/main.rs | 5 +- system/supervisor/src/main.rs | 39 +++- tests/checks/claims.rs | 21 +- tests/common/claims.rs | 94 +++++--- .../guest/rust-tests/src/bin/acpi_mediated.rs | 20 +- tests/images/acpicase.toml | 20 +- tests/images/clipboardcase.toml | 9 +- tests/images/drivescase.toml | 9 +- tests/images/jobcase.toml | 9 +- tests/images/latencycase.toml | 9 +- tests/images/metalcase.toml | 9 +- tests/images/metaldevicecase.toml | 9 +- tests/images/netcase.toml | 9 +- tests/images/proctreecase.toml | 9 +- tests/images/undeclaredcase.toml | 14 ++ tests/images/virtclaimcase.toml | 9 +- tests/toyos.rs | 78 ++++++- 47 files changed, 745 insertions(+), 648 deletions(-) delete mode 100644 issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md delete mode 100644 loader/src/gcd.rs delete mode 100644 loader/src/rootbridge.rs create mode 100644 tests/images/undeclaredcase.toml diff --git a/build/build.rs b/build/build.rs index ef81113491c..e75838be4f2 100644 --- a/build/build.rs +++ b/build/build.rs @@ -3063,6 +3063,7 @@ mod tests { "tests/images/panelcase.toml", "tests/images/proctreecase.toml", "tests/images/testcases.toml", + "tests/images/undeclaredcase.toml", "tests/images/virtclaimcase.toml", "tests/images/virtjobcase.toml", "tests/images/virtpaniccase.toml", diff --git a/issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md b/issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md deleted file mode 100644 index be1878885b7..00000000000 --- a/issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -status: assigned -kind: defect -opened: 2026-10-13 ---- - -# An aarch64 `cargo run` boot hands over no PCI claim - -`cargo run -- --arch aarch64` launches `virt` with the GICv3's ITS, on -QEMU's own GIC under HVF (`Arch::irqchip`), which arms it and maps each -claim's DeviceID to its slot's LPI: the same launch by hand with -`its=on,kernel-irqchip=off` (QEMU 11.1.1) showed it. It still hands over -none: diskserver's NVMe, soundserver, netstack and vdagent are each -refused with `its BAR was moved onto 8 64-bit address(es) inside the -windows firmware declared and the function answered at none of them` (lines 343, 418, 450, 487 of -[the boot's log](https://github.com/ToyOSOrg/evidence/blob/e626cced7ab73c04efdbbfd0b98a2f0a0ff8d86b/fix2-60f08b937/aarch64-its-boot.log)). -Each answers at its firmware BAR: `0x0f0107ff`, `0x79000000`, -`0x30bf8024` and `0x00000103` (lines 327, 386, 433, 470). At every -address it is moved onto the read is `0xffffffff`, except the first, -`0x4010000000`, the ECAM base (line 212), which line 225 lists among the -windows firmware declared: there the NVMe's read is `0x00081b36` -(line 327) and the other three's `0x00000000`. -The kernel's own xHCI driver is given no LPI there (`not armed — a driver -in this kernel is given no LPI`) and initialises no controller, so no USB -keyboard reaches the compositor, which panics on "the manifest gives this -program the keyboard". - -So on aarch64 the clipboard agent's claim and its handshake are unbooted: -its function is declined by the console driver and the console comes up -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. - -**Exit condition.** A `cargo run -- --arch aarch64` boot hands over a -claim: its console shows diskserver serving its NVMe partitions and -`vdagent: port 1 open`. diff --git a/issues/no-boot-reaches-the-retry-that-undoes-a-placement-nothing-answered.md b/issues/no-boot-reaches-the-retry-that-undoes-a-placement-nothing-answered.md index 4c9ee8de769..fd947562fac 100644 --- a/issues/no-boot-reaches-the-retry-that-undoes-a-placement-nothing-answered.md +++ b/issues/no-boot-reaches-the-retry-that-undoes-a-placement-nothing-answered.md @@ -30,12 +30,10 @@ pure and tested, `a_released_address_is_offered_again` pins the give-back arithmetic, and `https_tls13_e1000e` asserts the count of BARs kept back by name, so deleting the `BarReferenceEmpty` refusal reds. -**Exit condition.** A guest whose claimed function sits behind a `pcie-root-port` -given a memory window of a few MiB. **The port's window is the thing to arrange**: -it produces the case only because `KernelArgs::root_bridge_windows` carries the -loader's free GCD MMIO ranges, which are the host bridge's whole aperture and not -the port's slice of it — on q35 that range is `0xc1100000+0x3af00000` — so a -candidate past the port's window is an address this kernel may name and the -function does not answer at. Booted with an arm asserting the `left {at:#x}` -record, that the BAR ended at a later candidate, and that deleting the comparison -reds that arm. +**Exit condition.** A guest that offers a claimed function a candidate it does +not answer at, booted with an arm asserting the `left {at:#x}` record, that the +BAR ended at a later candidate, and that deleting the comparison reds that arm. +The declared windows are now each root's `_CRS`, and a function behind a bridge +is offered only what every bridge on its path forwards, so the loader's +over-wide list this once relied on is gone and a new way to arrange the case is +owed. diff --git a/kernel/pci/src/placement.rs b/kernel/pci/src/placement.rs index c280bb70d09..ef2468b6acf 100644 --- a/kernel/pci/src/placement.rs +++ b/kernel/pci/src/placement.rs @@ -4,9 +4,9 @@ //! [`bridge::Window`], the same pair of addresses that crate already declares. //! That the caller found nothing there is a statement about what it could //! *read*, and none of its sources says whether an address reaches the bus at -//! all; what says that is -//! [`toyos_abi::boot::KernelArgs::root_bridge_windows`], and an address inside -//! none of those windows is never offered. +//! all; what says that is the declaration of the root bridges' windows +//! ([`crate::aperture`]), and an address inside none of them is never +//! offered. use toyos_abi::acpi::RootBridgeWindow; diff --git a/kernel/src/arch/aarch64/acpi_mode.rs b/kernel/src/arch/aarch64/acpi_mode.rs index bc75642c8ab..7cd5828ed84 100644 --- a/kernel/src/arch/aarch64/acpi_mode.rs +++ b/kernel/src/arch/aarch64/acpi_mode.rs @@ -1,37 +1,202 @@ -//! The machine's ACPI mode: an AArch64 machine is hardware-reduced, with no -//! fixed hardware and no legacy mode to leave, so there is no row to claim. +//! The `acpi` claim on a hardware-reduced machine (ACPI 6.5 §4.1), which +//! every ACPI Arm machine is: no fixed hardware, no SCI and no legacy mode to +//! leave, so the claim's row names no port and no line. +//! +//! **What the firmware's AML names, this kernel reads and writes for the +//! claim's holder, one access at a time** ([`access`]), with the witness +//! `toyos_userbound::firmware` answers: a function's configuration space +//! through ECAM, and firmware's own memory — ACPI reclaim to read, ACPI NVS +//! both ways — through the direct map, which holds those and RAM and no +//! register. A register's address is refused `NoRegisters`: no range register +//! types one here, and the direct map holds none. There is no port space +//! (`NoPorts`), no Global Lock this kernel exchanges and no sleep type, since +//! PSCI powers the machine off. -use toyos_abi::acpi::AcpiInfo; +use alloc::vec; +use alloc::vec::Vec; +use core::sync::atomic::{AtomicPtr, Ordering}; + +use toyos_abi::acpi::{Access, AcpiInfo, Block, Refused, Space, Width, HARDWARE_REDUCED}; +use toyos_abi::syscall::SyscallError; +use toyos_acpi::EcamWindow; +use toyos_userbound::firmware::{self, Function as PciFunction, HostBridge, Memory, MemoryVerdict}; use crate::device::ClaimError; +use crate::isa::{self, Function}; + +/// `isa`'s row for the claim. +const ROW: usize = 1; -pub fn claim() -> Result<(crate::isa::Row, AcpiInfo), ClaimError> { - Err(ClaimError::Absent) +struct Hardware { + rsdp: u64, + ecam: Vec, } -/// Never called: nothing is released where nothing can be claimed. -pub fn release() { - unreachable!("AArch64 has no ACPI row") +/// Written once, by [`init`]. +static HARDWARE: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + +fn hardware() -> Option<&'static Hardware> { + let at = HARDWARE.load(Ordering::Acquire); + // SAFETY: `init` stored a leaked `Box` once, and nothing frees it. + (!at.is_null()).then(|| unsafe { &*at }) } -/// Never called, as [`release`]: the four are reached only with a claim. -pub fn access(_row: &crate::isa::Row, _request: &mut toyos_abi::acpi::Access) -> Result<(), toyos_abi::syscall::SyscallError> { - unreachable!("AArch64 has no ACPI row") +/// Fill the claim's row: no port and no line. Before userland runs. +pub fn init(rsdp_addr: u64) { + isa::fill(ROW, Function { name: "the ACPI tables", runs: vec![], irqs: vec![], wires: vec![] }); + let hardware = Hardware { rsdp: rsdp_addr, ecam: crate::drivers::pci::windows() }; + let was = HARDWARE.swap(alloc::boxed::Box::into_raw(alloc::boxed::Box::new(hardware)), Ordering::Release); + assert!(was.is_null(), "acpi: init ran twice"); + crate::log!("acpi: the ACPI row: hardware-reduced, its AML's accesses mediated"); } -pub fn lock_take(_row: &crate::isa::Row) -> Result { - unreachable!("AArch64 has no ACPI row") +pub fn claim() -> Result<(isa::Row, AcpiInfo), ClaimError> { + let hardware = hardware().ok_or(ClaimError::Absent)?; + let row = isa::claim_row(ROW)?; + let info = AcpiInfo { rsdp: hardware.rsdp, pm1_event: Block::NONE, gpe0: Block::NONE, flags: HARDWARE_REDUCED, reserved: [0; 3] }; + Ok((row, info)) } -pub fn lock_release(_row: &crate::isa::Row) -> Result<(), toyos_abi::syscall::SyscallError> { - unreachable!("AArch64 has no ACPI row") +/// Nothing to give back: no mode was entered and no lock taken. +pub fn release() {} + +/// No Global Lock this kernel exchanges. +pub fn lock_take(_row: &isa::Row) -> Result { + Err(SyscallError::NotSupported) } -pub fn s5(_row: &crate::isa::Row, _word: u64) -> Result<(), toyos_abi::syscall::SyscallError> { - unreachable!("AArch64 has no ACPI row") +pub fn lock_release(_row: &isa::Row) -> Result<(), SyscallError> { + Err(SyscallError::NotSupported) +} + +/// No sleep type: PSCI powers the machine off. +pub fn s5(_row: &isa::Row, _word: u64) -> Result<(), SyscallError> { + Err(SyscallError::NotSupported) } #[cfg(feature = "test-actuators")] pub fn debug_firmware_lock(_act: u64) -> u64 { - toyos_abi::syscall::SyscallError::NotSupported.to_u64() + SyscallError::NotSupported.to_u64() +} + +/// Make the access `request` names for the claim's holder, or refuse it by +/// name, as the x86 claim's `access` does. +pub fn access(_row: &isa::Row, request: &mut Access) -> Result<(), SyscallError> { + let hardware = hardware().expect("a claimed row has its hardware"); + let (Some(space), Some(width)) = (Space::from_raw(request.space), Width::from_raw(request.width)) else { + return Err(SyscallError::InvalidArgument); + }; + let write = match request.write { + 0 => None, + 1 if request.value <= width.max_value() => Some(request.value), + _ => return Err(SyscallError::InvalidArgument), + }; + if request.reserved != [0; 3] { + return Err(SyscallError::InvalidArgument); + } + if crate::quiesce::begun() { + return Err(SyscallError::Gone); + } + request.memory_type = toyos_abi::acpi::UNLISTED; + let made = match space { + Space::SystemMemory => memory(hardware, request, width, write), + Space::SystemIo => Err(Refused::NoPorts), + Space::PciConfig => { + let at = request.address; + let function = PciFunction { bus: (at >> 24) as u8, device: (at >> 19 & 0x1F) as u8, function: (at >> 16 & 7) as u8 }; + match at >> 48 { + 0 => config(hardware, (at >> 32) as u16, function, at as u16, width, write), + _ => Err(Refused::ConfigUnreachable), + } + } + }; + match made { + Ok(value) => { + request.refused = 0; + if write.is_none() { + request.value = value; + } + } + Err(refused) => request.refused = refused as u8, + } + Ok(()) +} + +/// Bytes at an address whatever its alignment, moved by one access. +#[repr(C, packed)] +struct Unaligned(T); + +fn memory(hardware: &Hardware, request: &mut Access, width: Width, write: Option) -> Result { + let map = crate::mm::firmware_map(); + request.memory_type = firmware::type_word(map, request.address); + let at = request.address; + let verdict = crate::pcidev::with_bar_memory(|bars| { + let memory = Memory { + map, + mapped_end: crate::mm::direct_map_end().get(), + ecam: &hardware.ecam, + devices: bars, + facs: None, + // No register is passed: see the module header. + uncached: |_, _| false, + registers_differ: false, + host_bridge: HostBridge::ABSENT, + }; + memory.decide(at, width, write.is_some()) + }); + let passed = match verdict { + MemoryVerdict::AsConfig(function, offset) => return config(hardware, toyos_acpi::SEGMENT_GROUP, function, offset, width, write), + MemoryVerdict::Refused(Refused::RegisterCached) => return Err(Refused::NoRegisters), + MemoryVerdict::Refused(refused) => return Err(refused), + MemoryVerdict::Through(passed) => passed, + }; + // The policy passed firmware's memory of one type; the direct map holds + // only the types the kernel reads as memory. + let mapped = map.iter().find(|e| (e.start..e.end).contains(&at)).is_some_and(|e| toyos_bootmap::is_read_as_memory(e.uefi_type)); + if !mapped { + return Err(Refused::MemoryType); + } + let base = crate::mm::DirectMap::from_phys(passed.at()); + // SAFETY: firmware's own memory, which the direct map holds for the + // machine's life and no Rust object occupies; the policy passed it, a + // write only where firmware's map lets a holder write. + unsafe { + match (passed.width(), write) { + (Width::Byte, None) => return Ok(u64::from(base.as_ptr::().read_volatile())), + (Width::Word, None) => return Ok(u64::from(base.as_ptr::>().read_volatile().0)), + (Width::DWord, None) => return Ok(u64::from(base.as_ptr::>().read_volatile().0)), + (Width::QWord, None) => return Ok(base.as_ptr::>().read_volatile().0), + (Width::Byte, Some(v)) => base.as_mut_ptr::().write_volatile(v as u8), + (Width::Word, Some(v)) => base.as_mut_ptr::>().write_volatile(Unaligned(v as u16)), + (Width::DWord, Some(v)) => base.as_mut_ptr::>().write_volatile(Unaligned(v as u32)), + (Width::QWord, Some(v)) => base.as_mut_ptr::>().write_volatile(Unaligned(v)), + } + } + Ok(0) +} + +/// One configuration access through the MCFG's window, as the x86 claim's +/// `config` makes it. +fn config(hardware: &Hardware, segment: u16, function: PciFunction, offset: u16, width: Width, write: Option) -> Result { + let at = firmware::config(&hardware.ecam, segment, function, offset, width)?; + let PciFunction { bus, device, function } = at.function(); + let space = crate::drivers::pci::function_window(bus, device, function).expect("a function the MCFG's window reaches is mapped"); + let Some(value) = write else { + let offset = u64::from(at.offset()); + return Ok(match at.width() { + Width::Byte => u64::from(space.read_u8(offset)), + Width::Word => u64::from(space.read_u16(offset)), + Width::DWord => u64::from(space.read_u32(offset)), + Width::QWord => unreachable!("the policy passes no qword configuration access"), + }); + }; + let passed = at.write(crate::pcidev::enumerated(bus, device, function))?.at(); + let offset = u64::from(passed.offset()); + match passed.width() { + Width::Byte => space.write_u8(offset, value as u8), + Width::Word => space.write_u16(offset, value as u16), + Width::DWord => space.write_u32(offset, value as u32), + Width::QWord => unreachable!("the policy passes no qword configuration access"), + } + Ok(0) } diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index e1a6b11703a..1a38d9cbd05 100644 --- a/kernel/src/arch/aarch64/boot.rs +++ b/kernel/src/arch/aarch64/boot.rs @@ -364,9 +364,11 @@ pub fn timer() { log!("timer: the EL1 virtual timer, PPI {}, stopped until the scheduler arms it", super::irqchip::timer_intid()); } -/// The platform's own devices that are not PCI functions: none this kernel -/// drives on an ACPI Arm machine. -pub fn platform_devices(_platform: &Platform, _rsdp_addr: u64) {} +/// The platform's own devices that are not PCI functions: on an ACPI Arm +/// machine, the `acpi` claim's row alone. +pub fn platform_devices(_platform: &Platform, rsdp_addr: u64) { + super::acpi_mode::init(rsdp_addr); +} /// Every other CPU, running. pub fn start_other_cpus(platform: &Platform, _args: &KernelArgs) { diff --git a/kernel/src/arch/aarch64/pio.rs b/kernel/src/arch/aarch64/pio.rs index 7312d1a1ab8..adddcd26e5b 100644 --- a/kernel/src/arch/aarch64/pio.rs +++ b/kernel/src/arch/aarch64/pio.rs @@ -1,5 +1,5 @@ -//! The I/O port space: AArch64 has none, and so no row to fill and no port to -//! declare. +//! The I/O port space: AArch64 has none, so no port to declare or open, and +//! no line a row raises; the `acpi` claim's row names neither. use core::convert::Infallible; @@ -7,10 +7,10 @@ use toyos_userbound::Ports; use crate::process::Pid; -/// No row is filled, so no wire is resolved. +/// No row raises a line, so no wire is resolved. pub type Wire = Infallible; -/// No line is routed where no row exists. +/// No line is routed where no row raises one. pub type Line = Infallible; pub fn route(_row: usize, wire: Wire) -> Result { @@ -38,7 +38,6 @@ pub fn holder(_ports: Ports) -> Option<&'static str> { None } -/// Never called: nothing is bound where nothing can be claimed. -pub fn switch_to(_pid: Option) { - unreachable!("AArch64 has no I/O permission bitmap") -} +/// Nothing: there is no I/O permission bitmap, and a row's binding opens no +/// port. +pub fn switch_to(_pid: Option) {} diff --git a/kernel/src/arch/aarch64/smmu/domain.rs b/kernel/src/arch/aarch64/smmu/domain.rs index 6860760f02d..e93f62f1651 100644 --- a/kernel/src/arch/aarch64/smmu/domain.rs +++ b/kernel/src/arch/aarch64/smmu/domain.rs @@ -20,7 +20,8 @@ use toyos_smmu::table::{entry, next, plan, Access, Entry, Leaf, INPUT_BITS}; use toyos_smmu::Asid; use super::{window, Live, UNIT}; -use crate::iommu::domains; +use crate::iommu::domains::{self, Placement}; +use crate::iommu::window::Addresses; use crate::iommu::{DomainId, IommuError, Iova, StreamId}; use crate::log; use crate::mm::PAGE_SIZE; @@ -92,14 +93,14 @@ pub fn untranslated(_function: StreamId) -> Option<&'static str> { None } -/// A new domain, with `room` bytes of it handed out first: refused, with no id -/// spent, where it would have less than that. -pub fn create(room: u64) -> Result<(DomainId, Iova), IommuError> { +/// A new domain placed as `placement` says: refused, with no id spent, where +/// it would not hold what was asked. +pub fn create(placement: Placement) -> Result<(DomainId, Option), IommuError> { let doorbell = super::super::irqchip::its::doorbell_page(); let mut held = UNIT.lock(); let live = held.as_mut().ok_or(IommuError::NoUnit)?; let ceiling = if live.unit.asid(0x100).is_some() { 1 << 16 } else { 1 << 8 }; - let next = live.domains.prepare(INPUT_BITS as u8, ceiling, room)?; + let next = live.domains.prepare(INPUT_BITS as u8, ceiling, placement)?; let asid = live.unit.asid(next.id).expect("SMMU: an id below the unit's ASIDs"); let root = Phys::new(live.memory.alloc(PAGE_SIZE)).expect("SMMU: a table below the unit's output size"); let context = Phys::new(live.memory.alloc(64)).expect("SMMU: a descriptor below the unit's output size"); @@ -111,7 +112,9 @@ pub fn create(room: u64) -> Result<(DomainId, Iova), IommuError> { } descriptor.write_u64(0, words[0]); if let Some(page) = doorbell { - assert!(page < next.addresses.floor(), "SMMU: the doorbell at {page:#x} is inside the addresses a domain hands out"); + if let Addresses::Window(window) = &next.addresses { + assert!(page < window.floor(), "SMMU: the doorbell at {page:#x} is inside the addresses a domain hands out"); + } live.put(root, page, Leaf::Doorbell(Phys::new(page).expect("SMMU: a 4 KiB page below 2^48"))); } let what = alloc::format!( diff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs index fc3ffcca30b..e9be6d7c7da 100644 --- a/kernel/src/arch/aarch64/smmu/mod.rs +++ b/kernel/src/arch/aarch64/smmu/mod.rs @@ -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: Option<&[toyos_abi::boot::RootBridgeWindow]>) { +pub fn init(rsdp_addr: u64, devices: &[PciDevice]) { let iort = match toyos_acpi::iort(direct_phys(), rsdp_addr) { Ok(iort) => iort, Err(IortRefused::Table(TableError::Absent)) => { @@ -268,8 +268,7 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[toyos_abi:: Err(why) => log!("IOMMU: {function}'s IORT route is refused: {why:?}"), } } - 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 { + let Some(live) = program(regs, smmu.base, smmu.coherent_override, smmu.event, routes) else { return; }; *UNIT.lock() = Some(live); @@ -326,7 +325,6 @@ fn program( coherent_override: bool, event: Option, routes: Vec<(StreamId, u32, Option)>, - reserved: Option>, ) -> Option { 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)); @@ -401,7 +399,6 @@ fn program( routes, domains: crate::iommu::domains::Domains::new(domain::FIRST), }; - live.domains.avoid(reserved); live.issue(&[Command::ForgetAll, Command::InvalidateAll]); regs.write64(reg::EVENTQ_BASE, events_base); diff --git a/kernel/src/arch/aarch64/smmu/selftest.rs b/kernel/src/arch/aarch64/smmu/selftest.rs index 8d3db5c07e8..0771a1b800f 100644 --- a/kernel/src/arch/aarch64/smmu/selftest.rs +++ b/kernel/src/arch/aarch64/smmu/selftest.rs @@ -59,7 +59,9 @@ pub(super) fn run(devices: &[PciDevice]) { verdict(aborted, REFUSED) ); - let (space, at) = OwnSpace::create(PAGE_2M).expect("smmu-selftest: a domain on the armed unit"); + // At boot, before any declaration: on its guest the unit translates past + // the CPU's addresses, where no window bounds a claimed function's domain. + let (space, at) = OwnSpace::create(PAGE_2M, &[]).expect("smmu-selftest: a domain on the armed unit"); space.place(at, phys, PAGE_2M).expect("smmu-selftest: the domain's own first leaf"); space.attach(device.bus, device.dev, device.func); let landed = write(regs, at + 0x40, phys + 0x40); diff --git a/kernel/src/arch/x86_64/amdvi/domain.rs b/kernel/src/arch/x86_64/amdvi/domain.rs index 46570fc4b1e..a5779d5dffa 100644 --- a/kernel/src/arch/x86_64/amdvi/domain.rs +++ b/kernel/src/arch/x86_64/amdvi/domain.rs @@ -15,7 +15,7 @@ use alloc::vec::Vec; -use crate::iommu::domains::{self, Domains}; +use crate::iommu::domains::{self, Domains, Placement}; use crate::iommu::{DomainId, IommuError, Iova, StreamId}; use crate::log; use crate::sync::Lock; @@ -55,7 +55,7 @@ static DOMAINS: Lock> = 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: Option>, settings: Vec, kept: Vec, translated: Vec<(u16, u16)>) { +pub(super) fn start(reserved: Vec<(u64, u64)>, settings: Vec, kept: Vec, translated: Vec<(u16, u16)>) { let mut domains = Domains::new(IDENTITY + 1); domains.avoid(reserved); *DOMAINS.lock() = Some(Amd { domains, settings, kept, translated }); @@ -78,13 +78,13 @@ pub fn untranslated(stream: StreamId) -> Option<&'static str> { } } -/// A new domain, with `room` bytes of it handed out first: refused, with no id -/// spent, where the domain would have less than that. -pub fn create(room: u64) -> Result<(DomainId, Iova), IommuError> { +/// A new domain placed as `placement` says: refused, with no id spent, where +/// it would not hold what was asked. +pub fn create(placement: Placement) -> Result<(DomainId, Option), 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 next = amd.domains.prepare(9 * ROOT_LEVEL + 12, 1 << 16, placement)?; let space = Space { root: TABLES.lock().alloc() }; Ok(amd.domains.add(next, space, alloc::format!("root={:#x} mode={ROOT_LEVEL}", space.root.get()))) } diff --git a/kernel/src/arch/x86_64/amdvi/mod.rs b/kernel/src/arch/x86_64/amdvi/mod.rs index bd3a12f6152..29e0c755425 100644 --- a/kernel/src/arch/x86_64/amdvi/mod.rs +++ b/kernel/src/arch/x86_64/amdvi/mod.rs @@ -29,7 +29,6 @@ use crate::mm::pmm::{self, PhysPage}; use crate::mm::{DirectMap, Mmio, PAGE_2M, PAGE_SIZE}; use crate::sync::Lock; use crate::time::{Duration, Tripwire}; -use toyos_abi::boot::RootBridgeWindow; use toyos_amdvi::dte::{Dte, Setting, DEVICE_IDS, ENTRY_BYTES}; use toyos_amdvi::queue::{Command, ENTRIES}; use toyos_amdvi::reg::{self, control, status, Features}; @@ -132,9 +131,9 @@ 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: Option<&[RootBridgeWindow]>) -> bool { +/// programmed; `false` where firmware published no IVRS. Every IVMD range is +/// what no claimed function's addresses reach. +pub fn init(rsdp_addr: u64, devices: &[PciDevice]) -> bool { let ivrs = match toyos_acpi::ivrs(crate::drivers::acpi::direct_phys(), rsdp_addr) { Ok(ivrs) => ivrs, Err(IvrsRefused::Table(TableError::Absent)) => return false, @@ -259,10 +258,7 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeW ); } let identity = build(devices, &settings); - // 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() - }); + let reserved = 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 { diff --git a/kernel/src/arch/x86_64/amdvi/selftest.rs b/kernel/src/arch/x86_64/amdvi/selftest.rs index 96fe0d7b873..161cb175875 100644 --- a/kernel/src/arch/x86_64/amdvi/selftest.rs +++ b/kernel/src/arch/x86_64/amdvi/selftest.rs @@ -41,7 +41,9 @@ pub fn run(devices: &[PciDevice]) { let (on_own, on_identity) = (StreamId::pci(moved.bus, moved.dev, moved.func), StreamId::pci(kept.bus, kept.dev, kept.func)); - let (space, at) = OwnSpace::create(PAGE_2M).expect("amdvi-selftest: a domain on the programmed unit"); + // At boot, before any declaration: on its guest the unit translates past + // the CPU's addresses, where no window bounds a claimed function's domain. + let (space, at) = OwnSpace::create(PAGE_2M, &[]).expect("amdvi-selftest: a domain on the programmed unit"); space.place(at, phys, PAGE_2M).expect("amdvi-selftest: the domain's own first leaf"); space.attach(moved.bus, moved.dev, moved.func); let landed = write(moved_regs, at + 0x40, phys + 0x40); diff --git a/kernel/src/arch/x86_64/iommu_unit.rs b/kernel/src/arch/x86_64/iommu_unit.rs index 202e145d9ae..002c382c472 100644 --- a/kernel/src/arch/x86_64/iommu_unit.rs +++ b/kernel/src/arch/x86_64/iommu_unit.rs @@ -9,7 +9,6 @@ use crate::drivers::pci::PciDevice; use crate::log; use crate::mm::policy::MmioPolicy; use crate::mm::Mmio; -use toyos_abi::boot::RootBridgeWindow; pub use super::vtd::interrupt; @@ -38,10 +37,10 @@ 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: Option<&[RootBridgeWindow]>) { - let found = if super::vtd::init(rsdp_addr, devices, windows) { +pub fn init(rsdp_addr: u64, devices: &[PciDevice]) { + let found = if super::vtd::init(rsdp_addr, devices) { Backend::Vtd - } else if super::amdvi::init(rsdp_addr, devices, windows) { + } else if super::amdvi::init(rsdp_addr, devices) { Backend::AmdVi } else { Backend::None @@ -127,12 +126,13 @@ pub(super) fn yn(v: bool) -> char { pub mod domain { use super::{backend, Backend}; use crate::arch::x86_64::{amdvi, vtd}; + use crate::iommu::domains::Placement; use crate::iommu::{DomainId, IommuError, Iova, StreamId}; - pub fn create(room: u64) -> Result<(DomainId, Iova), IommuError> { + pub fn create(placement: Placement) -> Result<(DomainId, Option), IommuError> { match backend() { - Backend::Vtd => vtd::domain::create(room), - Backend::AmdVi => amdvi::domain::create(room), + Backend::Vtd => vtd::domain::create(placement), + Backend::AmdVi => amdvi::domain::create(placement), Backend::None => Err(IommuError::NoUnit), } } diff --git a/kernel/src/arch/x86_64/vtd/domain.rs b/kernel/src/arch/x86_64/vtd/domain.rs index d31fe481c9a..276d3ceed58 100644 --- a/kernel/src/arch/x86_64/vtd/domain.rs +++ b/kernel/src/arch/x86_64/vtd/domain.rs @@ -18,7 +18,7 @@ use crate::log; use alloc::vec::Vec; -use crate::iommu::domains::{self, Domains}; +use crate::iommu::domains::{self, Domains, Placement}; use crate::iommu::{AddressWidth, DomainId, IommuError, Iova, StreamId}; use crate::sync::Lock; @@ -43,8 +43,8 @@ struct Vtd { static DOMAINS: Lock = 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: Option>) { +/// Before any domain is made: every claimed function's is built clear of these. +pub fn avoid(reserved: Vec<(u64, u64)>) { DOMAINS.lock().domains.avoid(reserved); } @@ -59,16 +59,16 @@ pub fn unit_agrees(width: AddressWidth, ceiling: u32, mgaw: u8) { }; } -/// A new domain, with `room` bytes of it handed out first: refused, with no id -/// spent, where the domain would have less than that. -pub fn create(room: u64) -> Result<(DomainId, Iova), IommuError> { +/// A new domain placed as `placement` says: refused, with no id spent, where +/// it would not hold what was asked. +pub fn create(placement: Placement) -> Result<(DomainId, Option), IommuError> { let mut held = DOMAINS.lock(); let (width, ceiling, mgaw) = match held.agreement { Agreement::None => return Err(IommuError::NoUnit), Agreement::Split => return Err(IommuError::WidthsDisagree), Agreement::One(width, ceiling, mgaw) => (width, ceiling, mgaw), }; - let next = held.domains.prepare(table::translatable_bits(width, mgaw), ceiling, room)?; + let next = held.domains.prepare(table::translatable_bits(width, mgaw), ceiling, placement)?; let space = Space { root: TABLES.lock().alloc(), width }; let what = alloc::format!("root={:#x} aw={} mgaw={mgaw}", space.root.phys(), width.bits()); Ok(held.domains.add(next, space, what)) diff --git a/kernel/src/arch/x86_64/vtd/mod.rs b/kernel/src/arch/x86_64/vtd/mod.rs index 5f62d517b37..ab8d1c369de 100644 --- a/kernel/src/arch/x86_64/vtd/mod.rs +++ b/kernel/src/arch/x86_64/vtd/mod.rs @@ -20,7 +20,6 @@ use alloc::vec::Vec; use crate::drivers::acpi::TableError; use crate::drivers::pci::PciDevice; -use toyos_abi::boot::RootBridgeWindow; use crate::iommu::{AddressWidth, StreamId}; use crate::mm::Mmio; use crate::sync::Lock; @@ -117,9 +116,9 @@ pub(super) fn invalidate_interrupt_entries() { } } -/// `windows` and every region firmware reserved are what no domain's addresses +/// Every region firmware reserved is what no claimed function's addresses /// reach. `false` where firmware published no DMAR. -pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeWindow]>) -> bool { +pub fn init(rsdp_addr: u64, devices: &[PciDevice]) -> bool { let dmar = match Dmar::open(rsdp_addr) { Ok(dmar) => dmar, Err(TableError::Absent) => return false, @@ -151,9 +150,9 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeW // 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; nothing where no declaration is held. - let mut reserved: Option> = windows.map(|w| w.iter().map(|w| (w.base, w.end())).collect()); + // What no claimed function's addresses may reach, RMRRs added as the walk + // meets them. + let mut reserved: Vec<(u64, u64)> = Vec::new(); for structure in dmar.structures() { match structure { Ok(Structure::Drhd(drhd)) => { @@ -200,9 +199,7 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeW identity domain 0x0..{identity_top:#x}" ); } - if let Some(reserved) = reserved.as_mut() { - reserved.push((base, limit.saturating_add(1))); - } + reserved.push((base, limit.saturating_add(1))); describe_scopes("rmrr", regions, rmrr.scopes()); regions += 1; held_regions += usize::from(held); diff --git a/kernel/src/device.rs b/kernel/src/device.rs index bd47ebb5c6e..8bc07237a27 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -131,8 +131,13 @@ impl Drop for Claim { Claimed::Partition(_) => {} // The row's drop is its release. Claimed::Isa(_) => {} - // Back to the mode the firmware handed over, and then the row's drop. - Claimed::Acpi(_) => crate::arch::acpi_mode::release(), + // Back to the mode the firmware handed over, and then the row's + // drop; and no declaration of the root bridges' memory will + // come where its holder made none. + Claimed::Acpi(_) => { + crate::arch::acpi_mode::release(); + crate::pcidev::declarer_gone(); + } } } } diff --git a/kernel/src/iommu/domains.rs b/kernel/src/iommu/domains.rs index fef333fc4f2..39fbdf203ba 100644 --- a/kernel/src/iommu/domains.rs +++ b/kernel/src/iommu/domains.rs @@ -5,7 +5,9 @@ use alloc::vec::Vec; -use crate::iommu::window::Window; +use toyos_abi::acpi::RootBridgeWindow; + +use crate::iommu::window::{Addresses, Window}; use crate::iommu::{DomainId, IommuError, Iova}; use crate::mm::PAGE_2M; @@ -15,50 +17,73 @@ pub struct Domains { first: u16, /// 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' declared - /// windows and the regions firmware reserved, as `(start, end)`; `None` - /// where no declaration is held ([`Window::new`]). - reserved: Option>, + live: Vec<(S, Addresses)>, + /// The regions firmware reserved for a device, as `(start, end)`, which no + /// claimed function's addresses reach ([`Window::new`]). + reserved: Vec<(u64, u64)>, +} + +/// Where a new domain's addresses come from ([`Addresses`]). +#[derive(Clone, Copy)] +pub enum Placement<'a> { + /// A kernel driver's: at identity. + Identity, + /// A claimed function's: `room` bytes handed out first, clear of the + /// declared `windows`. + Claimed { room: u64, windows: &'a [RootBridgeWindow] }, } /// A domain [`Domains::prepare`] admitted, not yet made. pub struct Next { pub id: u16, - pub addresses: Window, - first: Iova, + pub addresses: Addresses, + first: Option, } impl Domains { pub const fn new(first: u16) -> Self { - Self { first, live: Vec::new(), reserved: None } + Self { first, live: Vec::new(), reserved: Vec::new() } } - /// Before any domain is made: every one is built clear of these. - pub fn avoid(&mut self, reserved: Option>) { + /// Before any domain is made: every claimed function's is built clear of these. + pub fn avoid(&mut self, reserved: Vec<(u64, u64)>) { self.reserved = reserved; } - /// The next domain's id and its window over `translatable` bits, with - /// `room` bytes handed out first; refused, with no id spent, where ids - /// reach `ceiling` or the window would hold less. - pub fn prepare(&self, translatable: u8, ceiling: u32, room: u64) -> Result { + /// The next domain's id and its addresses over `translatable` bits, placed + /// as `placement` says; refused, with no id spent, where ids reach + /// `ceiling` or the addresses would not hold what was asked. + pub fn prepare(&self, translatable: u8, ceiling: u32, placement: Placement) -> Result { let id = u16::try_from(usize::from(self.first) + self.live.len()) .ok() .filter(|id| u32::from(*id) < ceiling) .ok_or(IommuError::DomainsExhausted(ceiling))?; - let (addresses, first) = Window::new(translatable, self.reserved.as_deref(), room)?; + let (addresses, first) = match placement { + Placement::Identity => (Addresses::identity(translatable)?, None), + Placement::Claimed { room, windows } => { + let reserved: Vec<(u64, u64)> = + windows.iter().map(|w| (w.base, w.end())).chain(self.reserved.iter().copied()).collect(); + let (window, first) = Window::new(translatable, &reserved, room)?; + (Addresses::Window(window), Some(first)) + } + }; Ok(Next { id, addresses, first }) } - /// `next`, made over `space`, which `what` describes in its record. - pub fn add(&mut self, next: Next, space: S, what: impl core::fmt::Display) -> (DomainId, Iova) { - log!( - "iommu: domain{} {what} addresses from {:#x} to {:#x}", - next.id, - next.addresses.floor(), - next.addresses.ceiling() - ); + /// `next`, made over `space`, which `what` describes in its record; and + /// the room a claimed placement asked for. + pub fn add(&mut self, next: Next, space: S, what: impl core::fmt::Display) -> (DomainId, Option) { + match &next.addresses { + Addresses::Identity { translatable } => { + log!("iommu: domain{} {what} maps each pool at identity, below {:#x}", next.id, 1u64 << translatable) + } + Addresses::Window(window) => log!( + "iommu: domain{} {what} addresses from {:#x} to {:#x}", + next.id, + window.floor(), + window.ceiling() + ), + } self.live.push((space, next.addresses)); (DomainId::new(next.id), next.first) } @@ -72,8 +97,8 @@ impl Domains { pub fn map(&mut self, id: DomainId, phys: u64, bytes: u64) -> Result<(S, Iova), IommuError> { aligned(phys)?; // A `DomainId` only `add` mints, so the index is always in range. - let (space, window) = &mut self.live[usize::from(id.raw() - self.first)]; - let at = window.reserve(bytes).ok_or(IommuError::AddressesExhausted(window.ceiling()))?; + let (space, addresses) = &mut self.live[usize::from(id.raw() - self.first)]; + let at = addresses.map(phys, bytes)?; log!("iommu: domain{} maps {phys:#x}..{:#x} at {:#x}", id.raw(), phys + bytes.next_multiple_of(PAGE_2M), at.raw()); Ok((*space, at)) } @@ -83,8 +108,9 @@ impl Domains { /// since [`Self::map`] may yet hand it out. pub fn place(&self, id: DomainId, at: Iova, phys: u64, bytes: u64) -> Result { aligned(phys)?; - let (space, window) = &self.live[usize::from(id.raw() - self.first)]; - assert!(window.handed_out(at, bytes), "iommu: domain{} never handed out {:#x}+{bytes:#x}", id.raw(), at.raw()); + let (space, addresses) = &self.live[usize::from(id.raw() - self.first)]; + let handed_out = matches!(addresses, Addresses::Window(window) if window.handed_out(at, bytes)); + assert!(handed_out, "iommu: domain{} never handed out {:#x}+{bytes:#x}", id.raw(), at.raw()); Ok(*space) } } diff --git a/kernel/src/iommu/mod.rs b/kernel/src/iommu/mod.rs index 3d0dee9160b..6245e831e49 100644 --- a/kernel/src/iommu/mod.rs +++ b/kernel/src/iommu/mod.rs @@ -121,10 +121,9 @@ 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 }, + /// Memory reaches `top`, past what the units translate, so a kernel + /// driver's pool may have no device address equal to its own. + MemoryPastTranslation { translatable: u8, top: u64 }, /// 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 }, @@ -151,11 +150,10 @@ 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!( + Self::MemoryPastTranslation { translatable, top } => 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" + "this machine's units translate {translatable} bits and its memory reaches {top:#x}, \ + so a pool may have no device address equal to its own" ), Self::NoRoom { floor, ceiling, room } => write!( f, @@ -179,10 +177,12 @@ pub enum DeviceSpace { } impl DeviceSpace { - /// One for whichever of `functions` is attached to it, or the machine's - /// own with the reason: the machine's own where any of them keeps it, so - /// the one attached reaches what its device addresses name. For a driver - /// **in this kernel**, whose addresses are the kernel's either way. + /// One for whichever of `functions` is attached to it, mapping each pool + /// at its own physical address ([`window::Addresses::Identity`]), or the + /// machine's own with the reason: the machine's own where any of them + /// keeps it, so the one attached reaches what its device addresses name. + /// For a driver **in this kernel**, whose addresses are the kernel's + /// either way and which no claim ever takes over. pub fn create(functions: &[crate::drivers::pci::PciDevice]) -> Self { for function in functions { let stream = StreamId::pci(function.bus, function.dev, function.func); @@ -191,7 +191,7 @@ impl DeviceSpace { return Self::Untranslated; } } - match unit::domain::create(0) { + match unit::domain::create(domains::Placement::Identity) { Ok((id, _)) => Self::Own(id), Err(why) => { log!("iommu: no domain of its own for a device: {why}"); @@ -238,11 +238,12 @@ impl DeviceSpace { pub struct OwnSpace(DomainId); impl OwnSpace { - /// One with `room` bytes of it handed out at the address answered, or the - /// reason there is none: a machine with no unit and a machine out of - /// domains are both refusals. - pub fn create(room: u64) -> Result<(Self, u64), IommuError> { - unit::domain::create(room).map(|(id, at)| (Self(id), at.raw())) + /// One with `room` bytes of it handed out at the address answered, clear + /// of the root bridges' declared `windows`, or the reason there is none: a + /// machine with no unit and a machine out of domains are both refusals. + pub fn create(room: u64, windows: &[toyos_abi::acpi::RootBridgeWindow]) -> Result<(Self, u64), IommuError> { + let (id, at) = unit::domain::create(domains::Placement::Claimed { room, windows })?; + Ok((Self(id), at.expect("iommu: a claimed placement hands out its room").raw())) } /// [`DeviceSpace::map`]. @@ -287,15 +288,9 @@ 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 root bridges' -/// declared memory, or `None` where no declaration is held ([`window`]). -pub fn init( - rsdp_addr: u64, - devices: &[crate::drivers::pci::PciDevice], - windows: Option<&toyos_pci::aperture::Declaration>>, -) { +pub fn init(rsdp_addr: u64, devices: &[crate::drivers::pci::PciDevice]) { log!("iommu: the CPUs issue {}-bit physical addresses", crate::arch::cpu::physical_address_bits()); - unit::init(rsdp_addr, devices, windows.map(|declared| declared.windows())); + unit::init(rsdp_addr, devices); } /// Why a source could not be given a message: the architecture's own reasons. diff --git a/kernel/src/iommu/window.rs b/kernel/src/iommu/window.rs index 4392aeec9e5..f2970a51b47 100644 --- a/kernel/src/iommu/window.rs +++ b/kernel/src/iommu/window.rs @@ -1,18 +1,23 @@ //! A domain's device addresses — where they start, where they end and which //! of them are handed out — for every backend. //! -//! They lie above all memory, so a descriptor still carrying one names -//! nothing a domain maps and faults rather than landing on a page, and clear -//! of every root-bridge window: a bridge may route a window peer-to-peer -//! before the unit sees the request (PCIe Base §2.4). Where the unit -//! translates past every address the CPU issues, they lie above every such -//! address, and nothing needs declaring. Where it translates no more bits -//! than the CPU's physical addresses, whatever the backend, they start a -//! quarter of the way up and end under the first declared window or region -//! firmware reserved for a device (an RMRR or IVMD) over that start, and with -//! no declaration accepted there is no domain. An address is handed out once, -//! unmapped or not: a device holding a stale one would reach whatever took -//! its place. +//! **A kernel driver's domain maps each pool at its own physical address** +//! ([`Addresses::Identity`]): a root bridge's window is MMIO and never RAM +//! (`toyos_pci::aperture::check` refuses a declaration saying otherwise), so a +//! device address equal to a RAM address is never routed peer-to-peer before +//! the unit sees it (PCIe Base §2.4), and no declaration is needed. The +//! function is never released to a claim, so no later holder inherits an +//! address it was aimed at. +//! +//! **A claimed function's domain hands its addresses out above all memory** +//! ([`Window`]), so a descriptor a holder left behind names nothing the next +//! holder's domain maps, and clear of every declared root-bridge window. Where +//! the unit translates past every address the CPU issues, they lie above every +//! such address. Where it translates no more bits than the CPU's physical +//! addresses, whatever the backend, they start a quarter of the way up and end +//! under the first declared window or region firmware reserved for a device +//! (an RMRR or IVMD) over that start. An address is handed out once, unmapped +//! or not: a device holding a stale one would reach whatever took its place. use crate::iommu::{IommuError, Iova}; use crate::mm::PAGE_2M; @@ -49,15 +54,45 @@ const fn ceiling(translatable: u8, floor: u64, reserved: &[(u64, u64)]) -> u64 { /// Where a domain over `translatable` bits begins and ends on a CPU issuing /// `cpu`-bit physical addresses, `reserved` the declared root-bridge windows -/// and firmware's reserved regions, or `None` where no declaration is held. -const fn bounds(translatable: u8, cpu: u8, reserved: Option<&[(u64, u64)]>) -> Result<(u64, u64), IommuError> { +/// and firmware's reserved regions. +const fn bounds(translatable: u8, cpu: u8, reserved: &[(u64, u64)]) -> (u64, u64) { if translatable > cpu { - return Ok((1 << cpu, 1 << translatable)); + return (1 << cpu, 1 << translatable); } let floor = first_address(translatable); - match reserved { - Some(reserved) => Ok((floor, ceiling(translatable, floor, reserved))), - None => Err(IommuError::Undeclared { translatable, cpu }), + (floor, ceiling(translatable, floor, reserved)) +} + +/// Where one domain's device addresses come from. +#[derive(Clone, Copy)] +pub enum Addresses { + /// A kernel driver's: each mapping at its own physical address, below + /// `1 << translatable`. + Identity { translatable: u8 }, + /// A claimed function's. + Window(Window), +} + +impl Addresses { + /// A kernel driver's, refused where memory reaches past what the unit + /// translates: a pool there would have no address equal to its own. + pub fn identity(translatable: u8) -> Result { + let top = crate::mm::pmm::top(); + if top > 1 << translatable { + return Err(IommuError::MemoryPastTranslation { translatable, top }); + } + Ok(Self::Identity { translatable }) + } + + /// Where `bytes` at `phys` goes in this domain, never handed out before. + pub fn map(&mut self, phys: u64, bytes: u64) -> Result { + match self { + Self::Identity { translatable } => match phys.checked_add(bytes.next_multiple_of(PAGE_2M)) { + Some(end) if end <= 1 << *translatable => Ok(Iova::translated(phys)), + _ => Err(IommuError::MemoryPastTranslation { translatable: *translatable, top: phys.saturating_add(bytes) }), + }, + Self::Window(window) => window.reserve(bytes).ok_or(IommuError::AddressesExhausted(window.ceiling())), + } } } @@ -65,8 +100,8 @@ impl Window { /// A new domain's window over `translatable` bits ([`bounds`]), with /// `room` bytes of it handed out first; refused where it would start at or /// below memory or hold less than that, and at least one leaf. - pub fn new(translatable: u8, reserved: Option<&[(u64, u64)]>, room: u64) -> Result<(Self, Iova), IommuError> { - let (floor, ceiling) = bounds(translatable, crate::arch::cpu::physical_address_bits(), reserved)?; + pub fn new(translatable: u8, reserved: &[(u64, u64)], room: u64) -> Result<(Self, Iova), IommuError> { + let (floor, ceiling) = bounds(translatable, crate::arch::cpu::physical_address_bits(), reserved); let top = crate::mm::pmm::top(); if floor <= top { return Err(IommuError::WindowBelowMemory { translatable, floor, top }); @@ -173,16 +208,14 @@ const _: () = { }; /// [`bounds`] on each kind of machine: a unit wider than the CPU's addresses -/// places above them, declared or not; a unit no wider places under its -/// first declared window, and with nothing declared nowhere. +/// places above them, whatever is declared; a unit no wider places under its +/// first declared window. const _: () = { const T14: [(u64, u64); 1] = [(0x40_0000_0000, 0x60_3DC0_0000)]; - assert!(matches!(bounds(48, 40, None), Ok((0x100_0000_0000, 0x1_0000_0000_0000)))); - assert!(matches!(bounds(48, 40, Some(&T14)), Ok((0x100_0000_0000, 0x1_0000_0000_0000)))); - assert!(matches!(bounds(39, 39, Some(&T14)), Ok((0x20_0000_0000, 0x40_0000_0000)))); - assert!(matches!(bounds(39, 39, None), Err(IommuError::Undeclared { translatable: 39, cpu: 39 }))); - assert!(matches!(bounds(39, 46, None), Err(IommuError::Undeclared { .. }))); + assert!(matches!(bounds(48, 40, &[]), (0x100_0000_0000, 0x1_0000_0000_0000))); + assert!(matches!(bounds(48, 40, &T14), (0x100_0000_0000, 0x1_0000_0000_0000))); + assert!(matches!(bounds(39, 39, &T14), (0x20_0000_0000, 0x40_0000_0000))); // The AMD bench: 48-bit host tables on a CPU of 48, all its windows below // the quarter floor, so the whole of the upper quarter. - assert!(matches!(bounds(48, 48, Some(&[(0x4_5020_0000, 0xfc_9000_0000)])), Ok((0x4000_0000_0000, 0x1_0000_0000_0000)))); + assert!(matches!(bounds(48, 48, &[(0x4_5020_0000, 0xfc_9000_0000)]), (0x4000_0000_0000, 0x1_0000_0000_0000))); }; diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 9da33092c47..5e91c83f291 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -442,14 +442,14 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { // Before any driver `init`: this sizes every BAR on the machine, and the // spec's probe takes memory decode off the function it is sizing for the // length of it. Nothing has bound yet, so nothing is mid-transfer. - pcidev::publish(&pci_devices, toyos_acpi::SEGMENT_GROUP, maps, kernel_args.root_bridge_windows()); + pcidev::publish(&pci_devices, toyos_acpi::SEGMENT_GROUP, maps); #[cfg(feature = "boot-actuators")] if actuator::pci_cap_selftest() { drivers::virtio::cap_selftest(); } // After ACPI is readable and PCI is enumerable, before any driver `init`: each enumerated device needs a context entry before it can DMA. // Refuses nothing — a machine with no usable IOMMU boots exactly as one without it. - iommu::init(kernel_args.rsdp_addr, &pci_devices, pcidev::declared().as_ref()); + iommu::init(kernel_args.rsdp_addr, &pci_devices); // Before storage and everything under it: what it covers is the rest of this // boot, and a wedge down there is the reason to have one. arch::watchdog::init(&pci_devices); diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 28fd8a4e5aa..60ae491c706 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -413,10 +413,10 @@ struct Machine { /// Posted once, when [`Machine::apertures`] leaves `Unknown`. static DECLARATION: Watch = Watch::new(); -/// How long a claim made before the declaration waits for it. A ceiling and no -/// expectation: the server that declares is started before every claimant, and -/// its tables' load and `_CRS` took 73 ms on the ThinkPad T14 and some 2 s on -/// q35 under TCG. +/// How long a claim made before the declaration waits for it: a failure +/// ceiling, not an expectation, since the server that declares is started +/// before every claimant. A claim that waits it out answers every claim of +/// the boot, so no later one waits again. const DECLARATION_DEADLINE: crate::time::Duration = crate::time::Duration::from_secs(30); static MACHINE: Lock = Lock::new(Machine { @@ -670,7 +670,9 @@ pub fn declare(windows: &[RootBridgeWindow]) -> Result<(), aperture::Declined> { (Ok(()), _) => unreachable!("a declaration taken is the answer"), } drop(machine); - DECLARATION.post(); + if declared != Err(aperture::Declined::Late) { + DECLARATION.post(); + } declared } @@ -967,7 +969,7 @@ fn bring_up(pci: PciDevice, id: PciId, number: usize, windows: &[RootBridgeWindo // descriptors. It is asked for here rather than after the BARs because a // refusal that had already armed a vector and moved a function's BARs would // leave the machine changed by a hand-over that did not happen. - let Space { space, lend } = function_space(number).map_err(Refusal::Untranslated)?; + let Space { space, lend } = function_space(number, windows).map_err(Refusal::Untranslated)?; // The table's own BAR, so it can be left where it is and kept out of what // the holder maps. @@ -1090,13 +1092,14 @@ fn place_bars( Ok((bar_at, bar_bytes)) } -/// Function `number`'s address space, made on its first claim. -fn function_space(number: usize) -> Result { +/// Function `number`'s address space, made on its first claim clear of the +/// declared `windows`. +fn function_space(number: usize, windows: &[RootBridgeWindow]) -> Result { let mut held = function(number).space.lock(); match *held { Some(space) => Ok(space), None => { - let (space, lend) = OwnSpace::create(LEND_WINDOW)?; + let (space, lend) = OwnSpace::create(LEND_WINDOW, windows)?; *held = Some(Space { space, lend }); Ok(Space { space, lend }) } diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 40082bd954a..1faac7338db 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -158,6 +158,13 @@ pub(super) fn sys_acpi(ctx: &SyscallContext, handle: RawHandle, op: u64, at: u64 }, _ => None, }; + let declared = match op { + ops::DECLARE => match ctx.copy_in::(UserAddr::new(at)) { + Ok(declared) => Some(declared), + Err(e) => return e.to_u64(), + }, + _ => None, + }; let pid = process::current_process(); // The holder's check and the act are one borrow of the row, as a read's are. let done = claim.isa(|row| { @@ -171,6 +178,14 @@ pub(super) fn sys_acpi(ctx: &SyscallContext, handle: RawHandle, op: u64, at: u64 } (ops::LOCK_RELEASE, _) => crate::arch::acpi_mode::lock_release(row).map(|()| 0), (ops::S5, _) => crate::arch::acpi_mode::s5(row, at).map(|()| 0), + (ops::DECLARE, _) => { + let windows = declared.as_ref().and_then(|d| d.windows()).ok_or(SyscallError::InvalidArgument)?; + match crate::pcidev::declare(windows) { + Ok(()) => Ok(0), + Err(toyos_pci::aperture::Declined::Refused(_)) => Err(SyscallError::InvalidArgument), + Err(toyos_pci::aperture::Declined::Late) => Err(SyscallError::AlreadyExists), + } + } _ => Err(SyscallError::InvalidArgument), } }); diff --git a/loader/src/efi/mod.rs b/loader/src/efi/mod.rs index 071393bf230..3709d832052 100644 --- a/loader/src/efi/mod.rs +++ b/loader/src/efi/mod.rs @@ -26,7 +26,7 @@ use alloc::vec::Vec; pub use boot::{AllocateType, BootServices, MemoryDescriptor, MemoryMap, Scoped, PAGE_SIZE}; pub use proto::{ - BlockIo, DevicePath, EdidActive, File, Gop, GopModeInfo, HardDrive, LoadedImage, Mode, PartitionInfo, PciRootBridgeIo, + BlockIo, DevicePath, EdidActive, File, Gop, GopModeInfo, HardDrive, LoadedImage, Mode, PartitionInfo, PixelFormat, Rng, SimpleFileSystem, }; pub use runtime::{ResetType, RuntimeServices, Time, VariableAttributes, GLOBAL_VARIABLE}; diff --git a/loader/src/efi/proto.rs b/loader/src/efi/proto.rs index 3d22054bd05..d58d91dcc68 100644 --- a/loader/src/efi/proto.rs +++ b/loader/src/efi/proto.rs @@ -647,48 +647,3 @@ impl super::Scoped<'_, Rng> { unsafe { (self.get_rng)(self.this(), ptr::null(), into.len(), into.as_mut_ptr()) }.ok() } } - -/// `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL` (UEFI 2.10 §14.2.1). -#[repr(C)] -pub struct PciRootBridgeIo { - parent_handle: Unused, - poll_mem: Unused, - poll_io: Unused, - mem_read: Unused, - mem_write: Unused, - io_read: Unused, - io_write: Unused, - pci_read: Unused, - pci_write: Unused, - copy_mem: Unused, - map: Unused, - unmap: Unused, - allocate_buffer: Unused, - free_buffer: Unused, - flush: Unused, - get_attributes: Unused, - set_attributes: Unused, - configuration: unsafe extern "efiapi" fn(this: *mut PciRootBridgeIo, resources: *mut *const c_void) -> Status, - pub segment_number: u32, -} - -const _: () = { - assert!(size_of::() == 18 * 8 + 8); - assert!(offset_of!(PciRootBridgeIo, configuration) == 17 * 8); - assert!(offset_of!(PciRootBridgeIo, segment_number) == 18 * 8); -}; - -// SAFETY: §14.2.1's layout, under its GUID. -unsafe impl Protocol for PciRootBridgeIo { - const GUID: Guid = Guid::new(0x2f707ebb, 0x4a1a, 0x11d4, [0x9a, 0x38, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d]); -} - -impl super::Scoped<'_, PciRootBridgeIo> { - /// `Configuration()` (§14.2.13): the bridge's ACPI resource descriptors, - /// in memory firmware owns. - pub fn configuration(&self) -> Result<*const c_void, Status> { - let mut resources = ptr::null(); - // SAFETY: the open interface, and a live out parameter. - unsafe { (self.configuration)(self.this(), &mut resources) }.ok().map(|()| resources) - } -} diff --git a/loader/src/gcd.rs b/loader/src/gcd.rs deleted file mode 100644 index e33da445725..00000000000 --- a/loader/src/gcd.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Which memory-mapped address space this platform declared, and which of it -//! nothing owns. -//! -//! **The firmware's account is read; the address space is never probed**, -//! because a load no bridge forwards does not come back on this hardware. -//! -//! The Global Coherency Domain is DXE's own map of the physical address space -//! (PI 1.8 Vol. 2 §7.2). EDK2's `PciHostBridgeDxe` adds each root bridge's -//! Mem, PMem and MemAbove4G apertures to it as -//! [`MEMORY_MAPPED_IO`] and allocates every BAR it assigns out of them, so an -//! MMIO descriptor with no owner is aperture the bridges decode and nothing -//! has taken. That is the necessary condition -//! `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL`'s `Configuration()` cannot give: it -//! answers a bridge's *current* settings, which a BAR may lie outside. -//! -//! It has to be asked here, like the root bridge protocol: DXE services die -//! with boot services. - -use core::ffi::c_void; - -use toyos_abi::boot::RootBridgeWindow; -use crate::efi::{Guid, Status, SystemTable}; - -const HEAD: &str = "GCD:"; - -/// The configuration table entry carrying [`DxeServices`] (PI 1.8 Vol. 2 §7.1). -const DXE_SERVICES_TABLE_GUID: Guid = - Guid::new(0x05ad34ba, 0x6f02, 0x4214, [0x95, 0x2e, 0x4d, 0xa0, 0x39, 0x8e, 0x2b, 0xb9]); - -/// `EFI_DXE_SERVICES_TABLE_SIGNATURE`: the eight bytes `DXE_SERV`, read as a -/// little-endian word. Checked because a configuration table entry is a pointer -/// firmware wrote and this loader calls through it. -const SIGNATURE: u64 = u64::from_le_bytes(*b"DXE_SERV"); - -/// `EfiGcdMemoryTypeMemoryMappedIo` of `EFI_GCD_MEMORY_TYPE` (PI 1.8 Vol. 2 -/// §7.2.1). -const MEMORY_MAPPED_IO: u32 = 3; - -/// `EFI_GCD_MEMORY_SPACE_DESCRIPTOR` (PI 1.8 Vol. 2 §7.2.1), in the spec's own -/// field order. -#[repr(C)] -#[derive(Clone, Copy)] -struct MemorySpace { - base: u64, - length: u64, - capabilities: u64, - attributes: u64, - /// `EFI_GCD_MEMORY_TYPE`, which the descriptor carries as a C enum and - /// therefore as four bytes. - kind: u32, - /// The image that allocated this range, and the device it was allocated - /// for. **Both null is the whole of what "free" means here** (PI 1.8 - /// Vol. 2 §7.2.3: `AllocateMemorySpace` sets them and `FreeMemorySpace` - /// clears them). - image: *mut c_void, - device: *mut c_void, -} - -/// The enum is four bytes and the two handles are eight each, so the u32 is -/// followed by four bytes of padding; a struct the compiler laid out otherwise -/// would read every field past `kind` off by that much. -const _: () = assert!(core::mem::size_of::() == 56); -const _: () = assert!(core::mem::offset_of!(MemorySpace, image) == 40); - -/// `DXE_SERVICES` (PI 1.8 Vol. 2 §7.1), in the spec's own field order. Only -/// `get_memory_space_map` is called; the rest are here because their order is -/// what puts it where it is. -#[repr(C)] -struct DxeServices { - signature: u64, - revision: u32, - header_size: u32, - crc32: u32, - reserved: u32, - add_memory_space: *mut c_void, - allocate_memory_space: *mut c_void, - free_memory_space: *mut c_void, - remove_memory_space: *mut c_void, - get_memory_space_descriptor: *mut c_void, - set_memory_space_attributes: *mut c_void, - get_memory_space_map: unsafe extern "efiapi" fn( - count: *mut usize, - map: *mut *mut MemorySpace, - ) -> Status, - add_io_space: *mut c_void, - allocate_io_space: *mut c_void, - free_io_space: *mut c_void, - remove_io_space: *mut c_void, - get_io_space_descriptor: *mut c_void, - get_io_space_map: *mut c_void, - dispatch: *mut c_void, - schedule: *mut c_void, - trust: *mut c_void, - process_firmware_volume: *mut c_void, - set_memory_space_capabilities: *mut c_void, -} - -/// A field added or dropped above moves `get_memory_space_map`, and the symptom -/// is firmware being asked to add memory space instead of describing it: the -/// 24-byte table header and eighteen pointers. -const _: () = assert!(core::mem::size_of::() == 24 + 18 * 8); -const _: () = assert!(core::mem::offset_of!(DxeServices, get_memory_space_map) == 24 + 6 * 8); - -/// The smallest run worth handing the kernel: the only page it maps. -const GRANULE: u64 = 2 * 1024 * 1024; - -/// Append every memory-mapped range this platform declared and nothing owns to -/// `out`, and answer how many were added. -pub fn free_mmio(system_table: &SystemTable, out: &mut [RootBridgeWindow]) -> usize { - let Some(entry) = - system_table.config_table().iter().find(|e| e.guid == DXE_SERVICES_TABLE_GUID) - else { - println!("{HEAD} no configuration table carries DXE services, so none of this machine's declared address space is known"); - return 0; - }; - // SAFETY: the entry firmware installed under the DXE services GUID points - // at that table for the life of boot services, and the signature below is - // what says the pointer is the table rather than a coincidence of GUIDs. - let services = unsafe { &*(entry.address as *const DxeServices) }; - if services.signature != SIGNATURE { - println!( - "{HEAD} the DXE services table signature is {:#018x} and not {SIGNATURE:#018x}, so it is not called", - services.signature - ); - return 0; - } - - let mut count: usize = 0; - let mut map: *mut MemorySpace = core::ptr::null_mut(); - // SAFETY: the call is the spec's — two out parameters, which firmware fills - // with a count and a pool buffer it allocated and this loader frees below. - let status = unsafe { (services.get_memory_space_map)(&mut count, &mut map) }; - if !status.is_success() || map.is_null() { - println!("{HEAD} GetMemorySpaceMap() answered {status:?}, so none of this machine's declared address space is known"); - return 0; - } - - let mut added = 0usize; - let mut refused = 0usize; - for index in 0..count { - // SAFETY: firmware answered `count` descriptors at `map`, and `index` - // is inside that count. - let space = unsafe { *map.add(index) }; - let free = space.image.is_null() && space.device.is_null(); - if space.kind != MEMORY_MAPPED_IO || !free || space.length < GRANULE { - continue; - } - // Every range this function found, whether it fits or not: one the - // kernel has no room for is aperture this machine has and the - // placement will not use. - let slot = out.get_mut(added); - println!( - "{HEAD} {:#x}+{:#x} mmio cap={:#x} attr={:#x} free{}", - space.base, - space.length, - space.capabilities, - space.attributes, - if slot.is_some() { "" } else { ", past the room the kernel is handed" }, - ); - match slot { - Some(slot) => { - *slot = RootBridgeWindow { base: space.base, length: space.length }; - added += 1; - } - None => refused += 1, - } - } - - // SAFETY: `map` is the pool buffer `GetMemorySpaceMap` allocated, nothing - // else holds it, and every descriptor has been copied out of it above. - let _ = unsafe { system_table.boot_services().free_pool(map as *mut u8) }; - println!("{HEAD} {count} descriptor(s); {added} free mmio range(s) of {GRANULE:#x} bytes or more handed to the kernel, {refused} past its room"); - added -} diff --git a/loader/src/main.rs b/loader/src/main.rs index d389a5da272..87b1a5adc86 100644 --- a/loader/src/main.rs +++ b/loader/src/main.rs @@ -13,7 +13,7 @@ use efi::{ cstr16, CStr16, DevicePath, EdidActive, Gop, Handle, HardDrive, LoadedImage, MemoryDescriptor, Mode, PixelFormat, ResetType, SimpleFileSystem, Status, SystemTable, ACPI2_GUID, PAGE_SIZE, }; -use toyos_abi::boot::{KernelArgs, MemoryMapEntry, RootBridgeWindow, EDID_BLOCK, MAX_ROOT_BRIDGE_WINDOWS}; +use toyos_abi::boot::{KernelArgs, MemoryMapEntry, EDID_BLOCK}; use toyos_bootmap::{Plan, Table, BOOT_MAP_BYTES, MAX_PAGES}; use toyos_update::policy; use toyos_update::record::{self, Booted, Ended, Record}; @@ -33,10 +33,8 @@ mod blackbox; mod bootnext; mod efi; mod floor; -mod gcd; mod loaderlog; mod microcode; -mod rootbridge; mod rootimage; mod seed; mod slot; @@ -527,16 +525,6 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v } } - // The last of the firmware questions, and asked here for the same reason - // the GOP's was asked before this: the protocol dies with boot services. - // - // Both readers answer memory the root bridges decode, which is what this - // array carries and the only thing the kernel asks of it. - let mut root_bridge_windows = [RootBridgeWindow::default(); MAX_ROOT_BRIDGE_WINDOWS]; - let named = rootbridge::windows(&system_table, &mut root_bridge_windows); - let free = gcd::free_mmio(&system_table, &mut root_bridge_windows[named..]); - let root_bridge_window_count = (named + free) as u64; - // 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. @@ -646,8 +634,6 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v layout: toyos_abi::boot::LAYOUT, cmdline_addr: cmdline.as_ptr() as u64, cmdline_len: cmdline.len() as u64, - root_bridge_window_count, - root_bridge_windows, root_image_addr, root_image_len, root_partition_guid, diff --git a/loader/src/rootbridge.rs b/loader/src/rootbridge.rs deleted file mode 100644 index f6d3060e8d9..00000000000 --- a/loader/src/rootbridge.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Where the platform's PCI root bridges decode memory, asked of the firmware -//! that assigned every BAR on the machine. -//! -//! `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL` (UEFI 2.10 §14.2) is installed on one -//! handle per root bridge, and its `Configuration()` answers with the ACPI -//! resource descriptors describing that bridge's current windows — the same -//! data an operating system reads out of `_CRS`, from the same firmware, and -//! reachable here without an AML interpreter. It has to be asked here: the -//! protocol dies with boot services. -//! -//! **A refusal hands the kernel nothing rather than something partial**, for -//! the reason [`toyos_abi::boot::KernelArgs::root_bridge_windows`] states: a -//! list missing one window is worse than no list. - -use core::ptr::read_volatile; - -use toyos_abi::boot::RootBridgeWindow; -use toyos_acpi::{memory_windows, Phys, MAX_LIST_BYTES}; -use crate::efi::{PciRootBridgeIo, Status, SystemTable}; - -const HEAD: &str = "Root bridge:"; - -/// The descriptor list firmware answered with. -/// -/// Boot services identity-map physical memory, so the pointer is the address -/// this loader reads. The bound is the decoder's own, because `Configuration()` -/// answers with a pointer and no length: what says a byte belongs to the list is -/// how far the walk may go. -#[derive(Clone, Copy)] -struct List { - at: u64, -} - -impl Phys for List { - fn readable(self, phys: u64, len: usize) -> bool { - let Some(ceiling) = self.at.checked_add(MAX_LIST_BYTES as u64) else { return false }; - phys >= self.at && phys.checked_add(len as u64).is_some_and(|e| e <= ceiling) - } - - fn byte(self, phys: u64) -> u8 { - // SAFETY: `readable` bounded `phys` to the list, and every caller in - // `toyos-acpi` asks it before asking this. - unsafe { read_volatile(phys as *const u8) } - } -} - -/// Every memory window this machine's root bridges decode, written into `out`; -/// the count, or zero on a machine that would not say. -pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) -> usize { - let bs = system_table.boot_services(); - let handles = match bs.handles::() { - Ok(handles) => handles, - Err(e) => { - println!("{HEAD} no handle carries the PCI Root Bridge I/O protocol ({e}), so the kernel is handed no window"); - return 0; - } - }; - - let mut found = 0usize; - for (index, handle) in handles.iter().enumerate() { - let bridge = match bs.get::(*handle) { - Ok(bridge) => bridge, - Err(e) => { - println!("{HEAD} handle {index} would not open ({e}), so the kernel is handed no window"); - return 0; - } - }; - - let answer = bridge.configuration(); - let Some(&resources) = answer.as_ref().ok().filter(|resources| !resources.is_null()) else { - println!( - "{HEAD} {index} (segment {}) answered {:?} to Configuration(), so the kernel is handed no window", - bridge.segment_number, - answer.err().unwrap_or(Status::SUCCESS), - ); - return 0; - }; - - let list = List { at: resources as u64 }; - match memory_windows(list, list.at, &mut out[found..]) { - Ok(count) => found += count, - Err(why) => { - println!("{HEAD} {index} (segment {}) {why}, so the kernel is handed no window", bridge.segment_number); - return 0; - } - } - } - - if found == 0 { - println!("{HEAD} {} bridge(s) name no memory window, so the kernel is handed none", handles.len()); - } - found -} diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index f40071426aa..a6452f7b5ed 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -199,8 +199,9 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi host.notifies ); - let mut unused = false; - if loaded.blocks.first().is_some_and(|dsdt| dsdt.is_ok()) { + if kernel.hardware_reduced() { + println!("acpiserver: \\_S5 not evaluated: this machine is hardware-reduced, and its power-off writes no sleep type"); + } else if loaded.blocks.first().is_some_and(|dsdt| dsdt.is_ok()) { match s5(interpreter, host) { Ok((a, b)) => { println!("acpiserver: \\_S5 evaluated: SLP_TYPa={a} SLP_TYPb={b}"); @@ -211,10 +212,6 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi loaded.handed = true; } Ok(Handed::TooWide) => println!("acpiserver: \\_S5 refused: the kernel keeps no SLP_TYPa wider than the register's three bits"), - Ok(Handed::Unused) => { - println!("acpiserver: \\_S5 not handed: this machine is hardware-reduced, and its power-off writes no sleep type"); - unused = true; - } Err(Stopping) => { println!("{STOPPING}"); return loaded; @@ -228,7 +225,7 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi Err(kind) => println!("acpiserver: \\_S5 refused: {kind}"), } } - if !loaded.handed && !unused { + if !loaded.handed && !kernel.hardware_reduced() { toyos::error!("{NO_S5_HANDED}"); } if !host.refused.is_empty() { @@ -534,6 +531,16 @@ pub mod tests { assert_eq!(kernel.handed.get(), None); } + /// A hardware-reduced machine powers off by no sleep type: `\_S5` is + /// not evaluated, and nothing is handed over or owed. + #[test] + fn a_hardware_reduced_machine_hands_over_no_sleep_type() { + let mut kernel = crafted(&sealed(b"DSDT", &s5_package(5, 0)), &[]); + kernel.reduced = true; + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); + assert_eq!(kernel.handed.get(), None); + } + #[test] fn a_machine_that_stops_mid_load_ends_it_with_nothing_refused() { for answered in [0, 3, 40] { diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index fa876ac56b2..c513f7fac43 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -147,6 +147,9 @@ pub trait Kernel { /// Hand the kernel `\_S5`'s `SLP_TYPa` for its power-off, which it takes /// once under a claim. fn s5(&self, slp_typ_a: u64) -> Result; + /// The machine is hardware-reduced (`toyos_abi::acpi::HARDWARE_REDUCED`): + /// no fixed hardware, and a power-off that writes no sleep type. + fn hardware_reduced(&self) -> bool; /// Declare the memory the PCI root bridges decode, once /// ([`crate::apertures`]). fn declare(&self, windows: &[RootBridgeWindow]) -> Result; @@ -170,8 +173,6 @@ pub enum Handed { Kept, /// Wider than the register's field: the kernel kept nothing. TooWide, - /// A hardware-reduced machine's power-off writes no sleep type. - Unused, } /// What the kernel did with a declaration of the root bridges' memory. @@ -780,6 +781,8 @@ pub mod tests { pub stops_after: Cell>, /// The `SLP_TYPa` it was handed and kept. pub handed: Cell>, + /// The machine is hardware-reduced. + pub reduced: bool, /// The windows it was declared, once. pub declared: RefCell>>, /// What each Sleep's wait takes, in turn; nothing once it runs out. @@ -908,6 +911,10 @@ pub mod tests { Ok(Handed::Kept) } + fn hardware_reduced(&self) -> bool { + self.reduced + } + fn declare(&self, windows: &[RootBridgeWindow]) -> Result { self.stopping()?; let mut declared = self.declared.borrow_mut(); diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 305a0b19612..5df62a2cfb1 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -267,11 +267,14 @@ impl Kernel for Claim<'_> { fn s5(&self, slp_typ_a: u64) -> Result { match self.dev.s5(slp_typ_a) { Err(SyscallError::InvalidArgument) => Ok(Handed::TooWide), - Err(SyscallError::NotSupported) if self.info.flags & HARDWARE_REDUCED != 0 => Ok(Handed::Unused), answer => Self::answered("the power-off's sleep type", answer).map(|()| Handed::Kept), } } + fn hardware_reduced(&self) -> bool { + self.info.flags & HARDWARE_REDUCED != 0 + } + fn declare(&self, windows: &[RootBridgeWindow]) -> Result { let declared = Apertures::new(windows).expect("acpiserver: the windows were read into a declaration's room"); match self.dev.declare(&declared) { diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 309f2ae2530..b93a9b7f4b3 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -585,6 +585,12 @@ fn is_storage(program: &Program) -> bool { !program.roles.is_empty() || is_block(program) } +/// A row holding the `acpi` claim, whose holder declares the memory every PCI +/// claim waits for: started before every other row. +fn declares(program: &Program) -> bool { + program.devices.iter().any(|device| device == "acpi") +} + /// A row serving the block port. fn is_block(program: &Program) -> bool { program.serves.iter().any(|s| s == toyos_blockring::PORT) @@ -801,10 +807,13 @@ impl Flight { impl<'a> Supervisor<'a> { /// Start `[boot] start`, kept for the machine's life: the supervisor is the only /// thing that can kill a daemon, and there is no other way back to a - /// process it started. The storage rows go first, the block service - /// before the file servers its grants are minted on — every other start - /// may make a directory, and a directory is a file server's — and the - /// session's home is made before the first row that is not one. + /// process it started. The `acpi` claim's holder goes first: it declares + /// the memory the PCI root bridges decode, which every PCI claim minted + /// after it waits for. Then the storage rows, the block service before + /// the file servers its grants are minted on — every other start may make + /// a directory, and a directory is a file server's — and the session's + /// home is made before the first row that is neither; the ACPI holder's + /// own home with it, which it is started without. /// /// **Every service holds its acceptors before any is started**, so what a /// start mints on another service's port — a role's directories, a @@ -814,9 +823,10 @@ impl<'a> Supervisor<'a> { fn boot(&mut self, role_acceptors: &mut BTreeMap<&str, Acceptor>) { let system = self.system; let rank = |name: &&String| match system.program(name) { - Some(p) if is_block(p) => 0, - Some(p) if is_storage(p) => 1, - _ => 2, + Some(p) if declares(p) => 0, + Some(p) if is_block(p) => 1, + Some(p) if is_storage(p) => 2, + _ => 3, }; let mut storage_first: Vec<&String> = system.start.iter().collect(); storage_first.sort_by_key(rank); @@ -864,11 +874,16 @@ impl<'a> Supervisor<'a> { let mut homes_made = false; for at in 0..self.services.len() { let program = self.services[at].program; - if !is_storage(program) && !homes_made { + if !is_storage(program) && !declares(program) && !homes_made { self.make_session_home(); homes_made = true; + for early in self.services[..at].iter().map(|s| s.program).filter(|p| declares(p)).collect::>() { + self.make_home(early); + } + } + if !declares(program) { + self.make_home(program); } - self.make_home(program); let service = &mut self.services[at]; let started = service.spawn(&program.path, &[], system, self.syscap, &self.connectors, &self.grants, &self.minted, &mut self.log); @@ -883,6 +898,9 @@ impl<'a> Supervisor<'a> { } if !homes_made { self.make_session_home(); + for early in self.services.iter().map(|s| s.program).filter(|p| declares(p)).collect::>() { + self.make_home(early); + } } self.load_grants(); self.drive(); @@ -1035,7 +1053,8 @@ impl<'a> Supervisor<'a> { } } - /// A service's own `HOME`, made before it first runs. + /// A service's own `HOME`, made before it first runs; the `acpi` claim's + /// holder's once storage is up, after it ([`Supervisor::boot`]). fn make_home(&mut self, program: &Program) { if !program.service || is_storage(program) { return; diff --git a/tests/checks/claims.rs b/tests/checks/claims.rs index 55261db24b9..343308841ec 100644 --- a/tests/checks/claims.rs +++ b/tests/checks/claims.rs @@ -6,10 +6,11 @@ use super::*; /// The T14's records of one boot, verbatim but for the readback's `stream|` -/// prefix: the I219 enumerated, firmware's root-bridge windows, a domain, and -/// the I219 claimed, released and claimed again. +/// prefix and the declaration's record, which names its declarer as the +/// kernel now does: the I219 enumerated, the root-bridge windows, a domain, +/// and the I219 claimed, released and claimed again. const RECORDED: &str = r"[2026-09-29 11:54:39 0.065 cpu0 kernel] PCI 00:1f.6 [0200] vendor=8086 device=15fc prog_if=00 bars=[bar0=0xbcf00000] -[2026-09-29 11:54:39 0.149 cpu0 kernel] pcidev: firmware declared root bridge memory: mem 0xa2000000..0xbd000000, mem 0x4000000000..0x603dc00000, mem 0xa0800000..0xa2000000, mem 0xbd000000..0xc0000000, mem 0xff000000..0xffb80000, mem 0xffd3a070..0x100000000, mem 0x603dc00000..0x8000000000 +[2026-09-29 11:54:39 0.149 cpu0 kernel] pcidev: the acpi claim declared root bridge memory: mem 0xa2000000..0xbd000000, mem 0x4000000000..0x603dc00000, mem 0xa0800000..0xa2000000, mem 0xbd000000..0xc0000000, mem 0xff000000..0xffb80000, mem 0xffd3a070..0x100000000, mem 0x603dc00000..0x8000000000 [2026-09-29 11:54:39 0.250 cpu0 kernel] iommu: domain2 root=0xa31000 aw=48 mgaw=39 addresses from 0x2000000000 to 0x8000000000 [2026-09-29 12:10:34 1.191 cpu0 kernel] iommu: irte5 source=00:1f.6 p=1 sid=0x00fe svt=1 sq=0 vector=0x28 apic=0x0 dst=0x0 trigger=edge [2026-09-29 12:10:34 1.192 cpu0 kernel] pcidev: PCI 00:1f.6 [8086:15fc] handed over on slot 0, vector 0x28 @@ -71,9 +72,23 @@ pub fn one_entry_per_slot() { } /// The recorded domain reaches into `0x4000000000..`; capped there, it passes. +/// A kernel driver's domain passes where each pool is at its own address +/// outside every window, and reds where one is not. pub fn domains_end_below_the_windows() { let why = claims::clear_of_host_bridges(&log(RECORDED)).expect_err("the recorded domain"); assert!(why.contains("inside the root-bridge window 0x4000000000..0x603dc00000"), "{why}"); let capped = RECORDED.replace("to 0x8000000000", "to 0x4000000000"); assert_eq!(claims::clear_of_host_bridges(&log(&capped)), Ok(())); + + let identity = |maps: &str| { + format!( + "{capped}[2026-09-29 11:54:39 0.260 cpu0 kernel] iommu: domain3 root=0xa32000 aw=48 mgaw=39 maps each pool at identity, below 0x8000000000\n\ + [2026-09-29 11:54:39 0.261 cpu0 kernel] iommu: domain3 maps {maps}\n" + ) + }; + assert_eq!(claims::clear_of_host_bridges(&log(&identity("0x4c00000..0x4e00000 at 0x4c00000"))), Ok(())); + let moved = claims::clear_of_host_bridges(&log(&identity("0x4c00000..0x4e00000 at 0x2000000000"))).expect_err("a pool off identity"); + assert!(moved.contains("maps 0x4c00000..0x4e00000 at 0x2000000000"), "{moved}"); + let inside = claims::clear_of_host_bridges(&log(&identity("0xa2000000..0xa2200000 at 0xa2000000"))).expect_err("a pool in a window"); + assert!(inside.contains("inside the root-bridge window 0xa2000000..0xbd000000"), "{inside}"); } diff --git a/tests/common/claims.rs b/tests/common/claims.rs index 028bf4d0ce8..c4abdc74457 100644 --- a/tests/common/claims.rs +++ b/tests/common/claims.rs @@ -122,41 +122,87 @@ pub fn reuses_its_entry(log: &Serial) -> Result<(), String> { Ok(()) } -/// Every domain's addresses lie outside every root-bridge window firmware -/// declared: a bridge may route a request in one of those before the unit -/// sees it. -pub fn clear_of_host_bridges(log: &Serial) -> Result<(), String> { - const DECLARED: &str = "pcidev: firmware declared root bridge memory: "; +/// What the `acpi` claim's holder declared the root bridges decode. +pub const DECLARED: &str = "pcidev: the acpi claim declared root bridge memory: "; + +/// `a..b` in hex, as the kernel's records write a span. +fn span(text: &str) -> Option<(u64, u64)> { + let (from, to) = text.split_once("..")?; + let hex = |t: &str| u64::from_str_radix(t.trim().strip_prefix("0x")?, 16).ok(); + Some((hex(from)?, hex(to)?)) +} + +/// The declared windows, as `(start, end)`. +fn declared(log: &Serial) -> Result, String> { let declared = records(log) .find_map(|m| m.strip_prefix(DECLARED)) .ok_or_else(|| format!("no `{DECLARED}` record:\n{}", log.text()))?; - let range = |text: &str| -> Option<(u64, u64)> { - let (from, to) = text.split_once("..")?; - let hex = |t: &str| u64::from_str_radix(t.trim().strip_prefix("0x")?, 16).ok(); - Some((hex(from)?, hex(to)?)) - }; - let windows = declared + declared .split(", ") - .map(|w| w.strip_prefix("mem ").and_then(range).ok_or_else(|| format!("{w:?} in {declared:?}"))) - .collect::, _>>()?; - let mut domains = 0; + .map(|w| w.strip_prefix("mem ").and_then(span).ok_or_else(|| format!("{w:?} in {declared:?}"))) + .collect() +} + +/// Each mapping domain `id`'s records say it made, as `(physical start, +/// physical end, device address)`. +pub fn mappings(log: &Serial, id: &str) -> Result, String> { + let head = format!("iommu: domain{id} maps "); + records(log) + .filter_map(|m| m.strip_prefix(head.as_str())) + .map(|rest| { + let (phys, at) = rest.split_once(" at ").ok_or_else(|| format!("{rest:?}"))?; + let (start, end) = span(phys).ok_or_else(|| format!("{rest:?}"))?; + let at = u64::from_str_radix(at.split_whitespace().next().and_then(|a| a.strip_prefix("0x")).ok_or_else(|| format!("{rest:?}"))?, 16) + .map_err(|e| format!("{rest:?}: {e}"))?; + Ok((start, end, at)) + }) + .collect() +} + +/// Whether domain `id`'s record says it maps each pool at identity: a kernel +/// driver's. +pub fn at_identity(log: &Serial, id: &str) -> bool { + let head = format!("iommu: domain{id} "); + records(log).any(|m| m.starts_with(head.as_str()) && m.contains(" maps each pool at identity")) +} + +/// No domain reaches a root-bridge window the `acpi` claim's holder declared: +/// a bridge may route a request in one before the unit sees it. A claimed +/// function's domain hands out addresses outside every window; a kernel +/// driver's maps each pool at its own physical address, which lies outside +/// every window too. +pub fn clear_of_host_bridges(log: &Serial) -> Result<(), String> { + let windows = declared(log)?; + let inside = |(floor, ceiling): (u64, u64)| windows.iter().copied().find(|(base, end)| *end > floor && *base < ceiling); + let (mut claimed, mut identity) = (0, 0); for record in records(log).filter(|m| m.starts_with("iommu: domain")) { - let Some((_, span)) = record.split_once(" addresses from ") else { continue }; - let (floor, ceiling) = span + let id = record["iommu: domain".len()..].split_whitespace().next().unwrap_or_default(); + if record.contains(" maps each pool at identity") { + for (start, end, at) in mappings(log, id)? { + if at != start { + return Err(format!("domain{id} is a kernel driver's and maps {start:#x}..{end:#x} at {at:#x}")); + } + if let Some((base, end)) = inside((start, end)) { + return Err(format!("domain{id} maps a pool inside the root-bridge window {base:#x}..{end:#x}")); + } + } + identity += 1; + continue; + } + let Some((_, addresses)) = record.split_once(" addresses from ") else { continue }; + let (floor, ceiling) = addresses .split_once(" to ") - .and_then(|(from, to)| range(&format!("{from}..{to}"))) + .and_then(|(from, to)| span(&format!("{from}..{to}"))) .ok_or_else(|| format!("an unreadable domain record: {record:?}"))?; - if let Some((base, end)) = windows.iter().find(|(base, end)| *end > floor && *base < ceiling) { - return Err(format!( - "{record:?} hands out addresses inside the root-bridge window {base:#x}..{end:#x}" - )); + if let Some((base, end)) = inside((floor, ceiling)) { + return Err(format!("{record:?} hands out addresses inside the root-bridge window {base:#x}..{end:#x}")); } - domains += 1; + claimed += 1; } - if domains == 0 { + if claimed + identity == 0 { return Err(format!("no domain was made on this boot:\n{}", log.text())); } - eprintln!(" [claims] {domains} domain(s), none reaching a root-bridge window"); + eprintln!(" [claims] {claimed} claimed domain(s) and {identity} kernel driver's, none reaching a root-bridge window"); Ok(()) } diff --git a/tests/guest/rust-tests/src/bin/acpi_mediated.rs b/tests/guest/rust-tests/src/bin/acpi_mediated.rs index 377457be53f..34e6a47dff6 100644 --- a/tests/guest/rust-tests/src/bin/acpi_mediated.rs +++ b/tests/guest/rust-tests/src/bin/acpi_mediated.rs @@ -526,17 +526,17 @@ fn configuration(holder: &Holder, info: &AcpiInfo) { assert_eq!(holder.write(Space::PciConfig, host_bridge(0x44), Width::Byte, held), Ok(()), "acpi: 0x44 by address"); assert_eq!(holder.write(Space::SystemMemory, ecam + 0x44, Width::Byte, held), Ok(()), "acpi: 0x44 through ECAM"); assert_eq!(holder.read(Space::PciConfig, host_bridge(0x44), Width::Byte), Ok(held)); - // The NVMe controller diskserver holds is its driver's whole. Each write - // below is of the value the register holds, so one a broken fence let - // through would be red here and nowhere else. - const NVME: u64 = 0x0010_1b36; - let nvme = (0..32).find(|&device| holder.read(Space::PciConfig, pci_address(0, 0, device, 0, 0), Width::DWord) == Ok(NVME)).expect("acpi: no NVMe controller on this guest's bus 0"); + // The xHCI controller this kernel drives is its driver's whole. Each + // write below is of the value the register holds, so one a broken fence + // let through would be red here and nowhere else. + const XHCI: u64 = 0x0194_1033; + let xhci = (0..32).find(|&device| holder.read(Space::PciConfig, pci_address(0, 0, device, 0, 0), Width::DWord) == Ok(XHCI)).expect("acpi: no NEC xHCI controller on this guest's bus 0"); for offset in [0x44u16, 0x100] { - let at = pci_address(0, 0, nvme, 0, offset); + let at = pci_address(0, 0, xhci, 0, offset); let value = holder.read(Space::PciConfig, at, Width::DWord).expect("acpi: a held function reads"); - assert_eq!(holder.write(Space::PciConfig, at, Width::DWord, value), Err(Refused::HeldFunction), "acpi: the NVMe controller's {offset:#x}"); - let through = ecam + (u64::from(nvme) << 15 | u64::from(offset)); - assert_eq!(holder.write(Space::SystemMemory, through, Width::DWord, value), Err(Refused::HeldFunction), "acpi: the NVMe controller's {offset:#x} through ECAM"); + assert_eq!(holder.write(Space::PciConfig, at, Width::DWord, value), Err(Refused::HeldFunction), "acpi: the xHCI controller's {offset:#x}"); + let through = ecam + (u64::from(xhci) << 15 | u64::from(offset)); + assert_eq!(holder.write(Space::SystemMemory, through, Width::DWord, value), Err(Refused::HeldFunction), "acpi: the xHCI controller's {offset:#x} through ECAM"); } // ICH9's LPC bridge places the ACPI and TCO blocks the kernel declared at 40h. let pmbase = pci_address(0, 0, 0x1F, 0, 0x40); @@ -547,7 +547,7 @@ fn configuration(holder: &Holder, info: &AcpiInfo) { } /// What [`configuration`] says after the host bridge's identity. -const CONFIGURATION_SAID: &str = "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the NVMe controller diskserver holds HeldFunction, and the host bridge's 0x44 was written"; +const CONFIGURATION_SAID: &str = "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the xHCI controller this kernel drives HeldFunction, and the host bridge's 0x44 was written"; fn lock(holder: &Holder, info: &AcpiInfo) { let fadt = holder.table(info.rsdp, b"FACP"); diff --git a/tests/images/acpicase.toml b/tests/images/acpicase.toml index b7fdf1134bf..a1af3a81ac2 100644 --- a/tests/images/acpicase.toml +++ b/tests/images/acpicase.toml @@ -1,8 +1,10 @@ # The ACPI server's death, judged on the T14 by `acpi_server_death`: the server # is built into the image and started by nothing but the one job, which holds # the claim it hands it. A boot that ends itself, as `tests/images/jobcase.toml` does. +# Nothing here claims a PCI function: the claim's holders declare none of the +# root bridges' memory, so every PCI claim on this boot is refused. [boot] -start = ["logkeeper", "diskserver", "fileserver", "test-runner"] +start = ["logkeeper", "fileserver", "test-runner"] [programs.logkeeper] service = true @@ -29,19 +31,9 @@ args = ["test_rs_acpi_mediated", "reboot"] [symlinks] "bin/reboot" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.diskserver] -service = true -restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. +# The file servers: the log and the running slot's volume on the disk the +# kernel drives, one process each. No block service: its NVMe claim would wait +# on a declaration no holder here makes. [programs.fileserver] restart = true roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/images/clipboardcase.toml b/tests/images/clipboardcase.toml index a4964678451..3441e5ffe71 100644 --- a/tests/images/clipboardcase.toml +++ b/tests/images/clipboardcase.toml @@ -9,7 +9,7 @@ assets = ["assets"] [boot] -start = ["logkeeper", "compositor", "test-runner"] +start = ["acpiserver", "logkeeper", "compositor", "test-runner"] # `every_boot_config_runs_logkeeper` is what refuses a boot config without it. [programs.logkeeper] @@ -28,3 +28,10 @@ drives = ["pci:07:80:00/1af4:1043"] receives = ["clipboard"] [programs.test-runner] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/images/drivescase.toml b/tests/images/drivescase.toml index e8ed7433d85..ef6a22b6950 100644 --- a/tests/images/drivescase.toml +++ b/tests/images/drivescase.toml @@ -3,7 +3,7 @@ # its own until the job that judges them has run. [boot] -start = ["logkeeper", "test-runner"] +start = ["acpiserver", "logkeeper", "test-runner"] # `every_boot_config_runs_logkeeper` is what refuses a boot config without it. [programs.logkeeper] @@ -19,3 +19,10 @@ drives = ["pci:00:ff:00/1234:11e8"] # none without it. [programs.test-runner] syscap = ["inventory", "dup"] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/images/jobcase.toml b/tests/images/jobcase.toml index 252c685e28f..1ab70a0b627 100644 --- a/tests/images/jobcase.toml +++ b/tests/images/jobcase.toml @@ -2,7 +2,7 @@ # job list is the manifest's own and the last thing it does is hand the machine # back to firmware. This is the shape a T14 boot has. [boot] -start = ["logkeeper", "diskserver", "fileserver", "test-runner"] +start = ["acpiserver", "logkeeper", "diskserver", "fileserver", "test-runner"] [programs.logkeeper] service = true @@ -36,3 +36,10 @@ devices = ["pci:01:08:02/1b36:0010"] restart = true roles = ["data", "log", "boot"] receives = ["block"] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/images/latencycase.toml b/tests/images/latencycase.toml index cb4496bb0f2..384c2e81d8b 100644 --- a/tests/images/latencycase.toml +++ b/tests/images/latencycase.toml @@ -11,7 +11,7 @@ # `power` because the job list is what ends the boot: there is no host on the # other end of the console on the machine this config exists for. [boot] -start = ["logkeeper", "diskserver", "fileserver", "test-runner"] +start = ["acpiserver", "logkeeper", "diskserver", "fileserver", "test-runner"] [programs.logkeeper] service = true @@ -44,3 +44,10 @@ devices = ["pci:01:08:02/1b36:0010"] restart = true roles = ["data", "log", "boot"] receives = ["block"] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index f20c0c24aee..410baeeb49f 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -9,7 +9,7 @@ assets = ["assets"] [boot] -start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "sshserver", "test-runner", "ps2server"] +start = ["acpiserver", "logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "sshserver", "test-runner", "ps2server"] # **Every image that carries a `TOYOS-LOG` partition runs this**, and every # image does. The kernel keeps the record ring and writes no file at all, so a @@ -80,3 +80,10 @@ devices = ["pci:01:08:02/1b36:0010"] restart = true roles = ["data", "log", "boot"] receives = ["block"] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/images/metaldevicecase.toml b/tests/images/metaldevicecase.toml index 594c145fe4b..84ca7391afa 100644 --- a/tests/images/metaldevicecase.toml +++ b/tests/images/metaldevicecase.toml @@ -12,7 +12,7 @@ # and output-path choice are the only exercise of `toyos-hda` that hardware can # give. [boot] -start = ["logkeeper", "diskserver", "fileserver", "soundserver", "test-runner"] +start = ["acpiserver", "logkeeper", "diskserver", "fileserver", "soundserver", "test-runner"] [programs.logkeeper] service = true @@ -69,3 +69,10 @@ devices = ["pci:01:08:02/1b36:0010"] restart = true roles = ["data", "log", "boot"] receives = ["block"] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/images/netcase.toml b/tests/images/netcase.toml index 556f9a2a698..945f9bd554e 100644 --- a/tests/images/netcase.toml +++ b/tests/images/netcase.toml @@ -9,7 +9,7 @@ # announce itself. [boot] -start = ["logkeeper", "diskserver", "fileserver", "netstack", "soundserver", "test-runner"] +start = ["acpiserver", "logkeeper", "diskserver", "fileserver", "netstack", "soundserver", "test-runner"] # `every_boot_config_runs_logkeeper` is what refuses a boot config without it: the # kernel keeps the record ring and writes no file, so such a boot's `/log` is @@ -51,3 +51,10 @@ devices = ["pci:01:08:02/1b36:0010"] restart = true roles = ["data", "log", "boot"] receives = ["block"] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/images/proctreecase.toml b/tests/images/proctreecase.toml index d5cf2de0250..41d3321d935 100644 --- a/tests/images/proctreecase.toml +++ b/tests/images/proctreecase.toml @@ -15,7 +15,7 @@ # login session the shell opens is answered. [boot] -start = ["logkeeper", "diskserver", "fileserver", "test-runner"] +start = ["acpiserver", "logkeeper", "diskserver", "fileserver", "test-runner"] [apps] folder = "read-write" @@ -64,3 +64,10 @@ devices = ["pci:01:08:02/1b36:0010"] restart = true roles = ["data", "log", "boot"] receives = ["block"] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/images/undeclaredcase.toml b/tests/images/undeclaredcase.toml new file mode 100644 index 00000000000..74feae3da22 --- /dev/null +++ b/tests/images/undeclaredcase.toml @@ -0,0 +1,14 @@ +# A boot no `acpi` claim holder declares on: two `edu` a `drives` row claims, +# and no acpiserver. `a_claim_waits_out_an_undeclared_machine` judges the +# first claim waiting its deadline out and both refused by name. +[boot] +start = ["logkeeper", "test-runner"] + +[programs.logkeeper] +service = true +syscap = ["logread"] + +[programs.pciholder] +drives = ["pci:00:ff:00/1234:11e8"] + +[programs.test-runner] diff --git a/tests/images/virtclaimcase.toml b/tests/images/virtclaimcase.toml index 518a31548f1..133a1a46f2a 100644 --- a/tests/images/virtclaimcase.toml +++ b/tests/images/virtclaimcase.toml @@ -4,7 +4,7 @@ # IORT routes past the SMMUv3; `test_rs_claim_bar_fault` faults in edu's BAR # and names it as a syscall's buffer. [boot] -start = ["logkeeper", "test-runner"] +start = ["acpiserver", "logkeeper", "test-runner"] [programs.logkeeper] service = true @@ -18,3 +18,10 @@ syscap = ["logread"] receives = ["power"] args = ["--bound-ms=240000", "test_rs_claim_lpi", "test_rs_claim_unrouted", "test_rs_claim_bar_fault"] syscap = ["device", "inventory", "dup"] + +# Started before every PCI claimant: it declares the memory the root bridges +# decode, which every claim of a PCI function waits for. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] diff --git a/tests/toyos.rs b/tests/toyos.rs index dc51b32f3de..6033a341c54 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -357,6 +357,13 @@ const MACHINE_TESTS: &[&str] = &[ // spawns, each process holding its own — which no host build has. No // bench has two identical functions the kernel leaves to a claim. "a_drives_row_holds_one_process_per_function", + // A PCI claim on a boot whose `acpi` claim holder never declares the root + // bridges' memory: the kernel's wait for it and its deadline, on QEMU + // x86-64's q35 with two `edu` (`Profile::HeadlessTwoEdu`). The state + // machine is `toyos-pci`'s, host-tested; the wait on its watch and the + // refusal at its end are a claimant's thread in a booted kernel. No bench + // boots without its ACPI server. + "a_claim_waits_out_an_undeclared_machine", // Whether QEMU's virtio functions negotiated `VIRTIO_F_ACCESS_PLATFORM` // behind its emulated VT-d unit and without one: no shipped machine has a // virtio function, so only a QEMU machine can be asked. @@ -465,8 +472,7 @@ const MACHINE_TESTS: &[&str] = &[ // display resize reaching the guest's mode: no metal machine has a // virtio-gpu, and the host tests drive `toyos-virtio-gpu` against a model // written from this design's own reading of §5.7, so only QEMU's device - // says it was read right. x86-64 q35 alone, `Profile::HeadlessVirtioGpu`: - // an AArch64 `virt` boot hands over no PCI claim yet. A boot of its own: + // says it was read right. x86-64 q35, `Profile::HeadlessVirtioGpu`. A boot of its own: // its `-vnc` display changes the display configuration the clipboard // test's premise, `-display none`, rests on. "the_compositor_drives_the_virtio_gpu", @@ -1978,7 +1984,7 @@ const ACPI_MEDIATED_SAID: [&str; 10] = [ "acpi: the FACS read through as type 10, its write was refused FacsWrite, and the memory after it was written and put back", "acpi: COM1, the CMOS index, the 8259 and the configuration mechanism were refused KernelPort; the i8042's row ClaimedPort; PM1a_CNT read and refused its write ReadOnlyPort; the POST port was written", "acpi: ACPI_ENABLE and ACPI_DISABLE were refused SMI_CMD as KernelCommand and SCI_EN stayed set, a write wider than a byte was refused CommandSpan, every firmware call made was read back from the port and counted on the boot processor, and a storm of them was refused CommandRate", - "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the NVMe controller diskserver holds HeldFunction, and the host bridge's 0x44 was written", + "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the xHCI controller this kernel drives HeldFunction, and the host bridge's 0x44 was written", "acpi: the lock a dead holder left taken read free; it was taken and given back, given back with GBL_RLS where the firmware had asked, and found pending while the firmware owned it", "acpi: a sleep type wider than three bits was refused InvalidArgument, the next holder's replaced a dead one's, and a second under one claim was refused AlreadyExists", ]; @@ -2034,6 +2040,9 @@ fn acpi_mediated_access(cpus: u32) -> Result<(), String> { said.must_be_clean()?; said.must_say("acpi: the Global Lock is the FACS's at ")?; said.must_say("acpi: the Global Lock given back for a holder that left it taken (its claim is gone)")?; + // The keeper's claim went without declaring the root bridges' memory, so + // none will be held this boot. + said.must_say("pcidev: no declaration of the root bridges' memory will be held: the acpi claim ended without declaring")?; // Whose range registers passed the unlisted read, and how this // hypervisor's second CPU holds its own beside them. let mtrr = ["mtrr: the boot processor's range registers: ", "mtrr: cpu1's range registers are "]; @@ -3747,6 +3756,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "hosted_clang_hello" => common::hostedclang::hosted_clang_hello(), "usbd_drives_the_spare" => usbd_drives_the_spare(test_config), "a_drives_row_holds_one_process_per_function" => a_drives_row_holds_one_process_per_function(), + "a_claim_waits_out_an_undeclared_machine" => a_claim_waits_out_an_undeclared_machine(), "usb_keyboard_rollover" => usb_keyboard_rollover(test_config), "vector_state_survives_a_switch" => vector_state_survives_a_switch(test_config), "the_opening_exchange_with_qemus_agent_completes" => the_opening_exchange_with_qemus_agent_completes(), @@ -3950,6 +3960,42 @@ fn usbd_drives_the_spare(test_config: &Path) -> Result<(), String> { Ok(()) } +/// A boot with no ACPI server: nothing declares the root bridges' memory, so +/// the first claim of an `edu` waits on the declaration's watch until its +/// deadline, and that claim and the second are each refused by that name, +/// before either function is touched; nothing is handed over. +fn a_claim_waits_out_an_undeclared_machine() -> Result<(), String> { + const WAITED: &str = "pcidev: a claim waited "; + const REFUSED: &str = "NOT HANDED OVER — no declaration of the memory the root bridges decode is held (nothing was declared before a claim's deadline)"; + const DEADLINE: Duration = Duration::from_secs(30); + let options = BootOptions { profile: qemu::Profile::HeadlessTwoEdu, ..Default::default() }; + let config = compile::repo_root().join("tests/images/undeclaredcase.toml"); + let mut qemu = QemuInstance::boot_with_options(&config, &[], &[], options); + // The guest is quiet for the whole of the wait, which the deadline bounds: + // a ceiling past it, not a quiet guest's verdict. + let waited = qemu.drain_until(DEADLINE * 3, |l| l.contains(REFUSED)); + let mut console = format!("{}\n{waited}", qemu.boot_log()); + await_guest(&mut qemu, &mut console, "both claims refused", |t| t.matches(REFUSED).count() >= 2) + .map_err(|e| format!("{e}\n{console}"))?; + let said = serial::Serial::named("the undeclared boot", console.clone()); + said.must_not_say(" [1234:11e8] handed over")?; + said.must_not_say("pcidev: the acpi claim declared")?; + let waited = said.must_say(WAITED)?; + let took = waited.split_once(WAITED).and_then(|(_, rest)| rest.split(" for ").next()).unwrap_or_default().to_string(); + let millis: u64 = took.strip_suffix("ms").and_then(|ms| ms.parse().ok()).ok_or_else(|| format!("{waited:?} names no duration in ms"))?; + if u128::from(millis) < DEADLINE.as_millis() { + return Err(format!("the claim waited {took}, short of its {DEADLINE:?} deadline:\n{console}")); + } + if said.text().matches(WAITED).count() != 1 { + return Err(format!("a claim after the deadline waited too:\n{console}")); + } + eprintln!(" [undeclared] {}", waited.trim()); + for line in said.text().lines().filter(|l| l.contains(REFUSED)) { + eprintln!(" [undeclared] {}", line.trim()); + } + Ok(()) +} + /// A `drives` row's processes on a q35 with two `edu`, which the kernel drives /// neither of: the kernel hands each function over once, two `pciholder`s /// each say they hold one of the two, and the job reads both still held, @@ -3992,8 +4038,9 @@ fn a_drives_row_holds_one_process_per_function() -> Result<(), String> { } /// Every AMD-Vi unit translating, every xHCI controller the kernel drives -/// moved to a domain of its own, the boot stick bound through one, and no -/// DMA refused. +/// moved to a domain of its own that maps exactly its one pool, at the pool's +/// own physical address, and nothing else; the boot stick bound through one, +/// and no DMA refused. fn amdvi_translates(kernel: &serial::Serial) -> Result<(), String> { let text = kernel.text(); let units: Vec<&str> = @@ -4011,7 +4058,18 @@ fn amdvi_translates(kernel: &serial::Serial) -> Result<(), String> { return Err(format!("the kernel drove no xHCI controller:\n{text}")); } for at in &controllers { - kernel.must_say(&format!("iommu: {at} moves to domain"))?; + let moved = format!("iommu: {at} moves to domain"); + let id = text + .lines() + .find_map(|l| l.split_once(moved.as_str())?.1.split_whitespace().next()) + .ok_or_else(|| format!("{at} was moved to no domain:\n{text}"))?; + if !claims::at_identity(kernel, id) { + return Err(format!("{at}'s domain{id} is no kernel driver's, mapping at identity:\n{text}")); + } + match claims::mappings(kernel, id)?.as_slice() { + [(start, end, at_)] if at_ == start => eprintln!(" [amdvi] {at}: domain{id} maps its pool {start:#x}..{end:#x} at identity, and nothing else"), + other => return Err(format!("{at}'s domain{id} maps {other:x?}, not its one pool at identity")), + } } kernel.must_say("usb-storage: 1 device(s)")?; for unit in units { @@ -5508,6 +5566,14 @@ fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { return Err(format!("the kernel keeps {kept:?}, where this bench's \\_S5 is {:?}", bench.s5)); } eprintln!(" [acpi] {}; {}", handed.trim(), kept.trim()); + // The root bridges' memory declared out of the loaded namespace, and what + // that cost: the load's time, and each claim's wait for the declaration. + eprintln!(" [acpi] {}", log.must_say("acpiserver: the root bridges' memory declared: ")?.trim()); + eprintln!(" [acpi] {}", log.must_say(" tables loaded in ")?.trim()); + eprintln!(" [acpi] {}", kernel.must_say(claims::DECLARED)?.trim()); + for waited in kernel.text().lines().filter(|l| l.contains("pcidev: a claim waited ")) { + eprintln!(" [acpi] {}", waited.trim()); + } let others = number_between(kernel.text(), "SMP: ", " of ")? - 1; let compared: Vec<&str> = kernel.text().lines().filter(|l| l.contains("mtrr: cpu") && l.contains("'s range registers are ")).collect(); if compared.len() as u64 != others || compared.iter().any(|l| l.contains("are on and not the boot processor's")) { From d84d8c34517abcb536f014e2416bd3472952909b Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 15 Oct 2026 00:47:46 +0200 Subject: [PATCH 3/9] Fail-closed judge reads a wait under a millisecond Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- tests/toyos.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/toyos.rs b/tests/toyos.rs index 6033a341c54..a724d20f91e 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -3982,7 +3982,13 @@ fn a_claim_waits_out_an_undeclared_machine() -> Result<(), String> { said.must_not_say("pcidev: the acpi claim declared")?; let waited = said.must_say(WAITED)?; let took = waited.split_once(WAITED).and_then(|(_, rest)| rest.split(" for ").next()).unwrap_or_default().to_string(); - let millis: u64 = took.strip_suffix("ms").and_then(|ms| ms.parse().ok()).ok_or_else(|| format!("{waited:?} names no duration in ms"))?; + // The kernel writes a wait under a millisecond in ns. + let millis: u64 = match (took.strip_suffix("ms"), took.strip_suffix("ns")) { + (Some(ms), _) => ms.parse().ok(), + (None, Some(ns)) => ns.parse::().ok().map(|_| 0), + (None, None) => None, + } + .ok_or_else(|| format!("{waited:?} names no duration"))?; if u128::from(millis) < DEADLINE.as_millis() { return Err(format!("the claim waited {took}, short of its {DEADLINE:?} deadline:\n{console}")); } From 401b07222125bd316d8f1903a17d8c06dfcfd06f Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 15 Oct 2026 00:53:54 +0200 Subject: [PATCH 4/9] The declaration is per root bridge: segment, buses, _CRS windows and the _OSC grant Ruled for lifecycle stage 2, which arms hot-plug ports under a granting root: acpiserver negotiates each root's _OSC under the PCI host bridge UUID (PCI Firmware Specification 3.3 section 4.5) in the pass that reads its _CRS, and the kernel keeps the records and says each grant. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- images/system.toml | 4 +- kernel/src/pcidev/mod.rs | 50 +++++- kernel/src/syscall/device.rs | 4 +- lib/acpi/src/lib.rs | 2 +- lib/acpi/src/resource.rs | 47 ++++-- lib/acpi/tests/resource.rs | 22 ++- sdk/abi/src/acpi.rs | 139 ++++++++++++++--- sdk/abi/src/syscall.rs | 2 +- sdk/toyos/src/device.rs | 2 +- system/acpiserver/src/apertures.rs | 239 ++++++++++++++++++++--------- system/acpiserver/src/host.rs | 17 +- system/acpiserver/src/main.rs | 6 +- system/acpiserver/src/osc.rs | 56 +++++-- tests/toyos.rs | 8 + 14 files changed, 453 insertions(+), 145 deletions(-) diff --git a/images/system.toml b/images/system.toml index 603aaab27bf..217be9680e0 100644 --- a/images/system.toml +++ b/images/system.toml @@ -62,7 +62,9 @@ serves = ["log"] # firmware's own access: the kernel makes the AML's reads and writes for it, # inside the fences of `toyos_userbound::firmware`. `power` is how a press # stops the machine: through the supervisor, which has `logkeeper` make the -# log whole. +# log whole. It declares the memory the PCI root bridges decode out of each +# root's `_CRS`, which every PCI claim waits for, so the supervisor starts it +# before every other row; an image that claims a PCI function starts it. [programs.acpiserver] service = true devices = ["acpi"] diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 60ae491c706..d9c7499e572 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -394,6 +394,8 @@ struct Machine { /// windows is never touched, and a function is handed over on no other /// state. apertures: aperture::Held>, + /// The root bridges the declaration was made of, kept with it. + roots: Vec, /// Windows this module has cut, as `(requester, BAR index, at, span)`. /// /// **A window belongs to the BAR it was cut for, not to the claim that @@ -410,6 +412,30 @@ struct Machine { resetting: Vec<(u16, Resetting, Kept)>, } +/// One PCI root bridge as the `acpi` claim's holder declared it. +pub struct RootBridge { + segment: u16, + first_bus: u8, + windows: Vec, + osc: toyos_abi::acpi::Osc, +} + +impl RootBridge { + /// Every bridge `declared` carries, or `None` where a record is none the + /// ABI names: a count past its array, or an `_OSC` word it does not. + pub fn decode(declared: &toyos_abi::acpi::Apertures) -> Option> { + declared + .bridges()? + .iter() + .map(|bridge| { + (bridge.first_bus <= bridge.last_bus).then_some(())?; + let windows = bridge.windows()?.to_vec(); + Some(Self { segment: bridge.segment, first_bus: bridge.first_bus, windows, osc: bridge.osc()? }) + }) + .collect() + } +} + /// Posted once, when [`Machine::apertures`] leaves `Unknown`. static DECLARATION: Watch = Watch::new(); @@ -431,6 +457,7 @@ static MACHINE: Lock = Lock::new(Machine { map: Vec::new(), trying: Vec::new(), apertures: aperture::Held::Unknown, + roots: Vec::new(), windows: Vec::new(), resetting: Vec::new(), }); @@ -650,17 +677,30 @@ pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry]) { machine.high = high; } -/// Hold `windows` as the root bridges' memory: the `acpi` claim holder's -/// declaration, from `Unknown` alone. A window over memory firmware's map -/// gives the kernel is refused with the rest ([`aperture::check`]). -pub fn declare(windows: &[RootBridgeWindow]) -> Result<(), aperture::Declined> { +/// Hold `roots` and their windows, together, as the root bridges' memory: +/// the `acpi` claim holder's declaration, from `Unknown` alone. A window over +/// memory firmware's map gives the kernel, in any root, is refused with the +/// rest ([`aperture::check`]). Each root's `_OSC` grant is said. +pub fn declare(roots: Vec) -> Result<(), aperture::Declined> { let memory: Vec<(u64, u64)> = crate::mm::firmware_map() .iter() .filter(|entry| toyos_bootmap::is_read_as_memory(entry.uefi_type)) .map(|entry| (entry.start, entry.end)) .collect(); + let windows: Vec = roots.iter().flat_map(|root| root.windows.iter().copied()).collect(); let mut machine = MACHINE.lock(); - let declared = machine.apertures.declare(windows.to_vec(), &memory); + let declared = machine.apertures.declare(windows, &memory); + if declared != Err(aperture::Declined::Late) { + machine.roots = roots; + for root in &machine.roots { + let granted = match root.osc { + toyos_abi::acpi::Osc::Absent => "no _OSC", + toyos_abi::acpi::Osc::Granted(granted) if granted & toyos_abi::acpi::NATIVE_HOT_PLUG != 0 => "native hot-plug granted", + toyos_abi::acpi::Osc::Granted(_) | toyos_abi::acpi::Osc::Refused => "not granted", + }; + log!("pci: root {:04x}:{:02x} _OSC: {granted}", root.segment, root.first_bus); + } + } match (&declared, machine.apertures.answer()) { (Ok(()), Some(Ok(declaration))) => account_for(declaration.windows(), &machine.decoded), (Err(aperture::Declined::Refused(why)), _) => { diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 1faac7338db..56d382102c0 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -179,8 +179,8 @@ pub(super) fn sys_acpi(ctx: &SyscallContext, handle: RawHandle, op: u64, at: u64 (ops::LOCK_RELEASE, _) => crate::arch::acpi_mode::lock_release(row).map(|()| 0), (ops::S5, _) => crate::arch::acpi_mode::s5(row, at).map(|()| 0), (ops::DECLARE, _) => { - let windows = declared.as_ref().and_then(|d| d.windows()).ok_or(SyscallError::InvalidArgument)?; - match crate::pcidev::declare(windows) { + let bridges = declared.as_ref().and_then(crate::pcidev::RootBridge::decode).ok_or(SyscallError::InvalidArgument)?; + match crate::pcidev::declare(bridges) { Ok(()) => Ok(0), Err(toyos_pci::aperture::Declined::Refused(_)) => Err(SyscallError::InvalidArgument), Err(toyos_pci::aperture::Declined::Late) => Err(SyscallError::AlreadyExists), diff --git a/lib/acpi/src/lib.rs b/lib/acpi/src/lib.rs index 626f7d48bc9..6b0b205edf7 100644 --- a/lib/acpi/src/lib.rs +++ b/lib/acpi/src/lib.rs @@ -47,7 +47,7 @@ pub use madt::{ }; pub use gtdt::{gtdt, Gtdt, TimerInterrupt, GTDT_NEEDED}; pub use mcfg::{ecam_allocations, AllocationRefused, Allocations, ConfigRegister, EcamWindow, SEGMENT_GROUP}; -pub use resource::{io_ports, memory_windows, ResourceError, MAX_LIST_BYTES}; +pub use resource::{io_ports, root_bridge, ResourceError, RootResources, MAX_LIST_BYTES}; pub use spcr::{spcr, Gas, SerialInterface, Spcr, GAS_SYSTEM_MEMORY, SPCR_NEEDED}; /// Physical memory, as this decoder reads it. diff --git a/lib/acpi/src/resource.rs b/lib/acpi/src/resource.rs index aca772b03ae..265f6b9994d 100644 --- a/lib/acpi/src/resource.rs +++ b/lib/acpi/src/resource.rs @@ -1,5 +1,5 @@ -//! ACPI resource descriptors (ACPI 6.5 §6.4), decoded into the memory -//! windows a PCI root bridge's `_CRS` says it forwards ([`memory_windows`]), +//! ACPI resource descriptors (ACPI 6.5 §6.4), decoded into the bus range and +//! memory windows a PCI root bridge's `_CRS` says it forwards ([`root_bridge`]), //! and into the I/O ports a device names its registers by ([`io_ports`]). //! //! Input is firmware-supplied and untrusted: no path panics, the walk @@ -96,6 +96,9 @@ pub enum ResourceError { /// stepped over: a root bridge's `_CRS` that names one says something /// about its memory this decoder does not read. Consumed { min: u64 }, + /// A bus number range past bus 0xff, or a second one: a root's buses + /// are one run of one segment's. + Buses { min: u64, max: u64 }, /// More memory windows, or I/O runs, than the caller has room for. TooMany { room: usize }, /// An I/O Port Descriptor naming a range of bases the OS chooses among, @@ -129,6 +132,7 @@ impl core::fmt::Display for ResourceError { Self::TooMany { room } => { write!(f, "more memory windows or I/O runs than the {room} there is room for") } + Self::Buses { min, max } => write!(f, "the bus range {min:#x}..={max:#x} is not the root's one run of buses"), Self::Relocatable { min, max } => write!(f, "the I/O run is relocatable between {min:#x} and {max:#x}"), Self::NoPorts { port } => write!(f, "the I/O run at {port:#x} is no ports long"), } @@ -177,13 +181,22 @@ fn field(phys: P, at: u64, offset: usize, width: usize) -> u64 { (0..width).fold(0u64, |v, i| v | u64::from(phys.byte(at + (offset + i) as u64)) << (8 * i)) } -/// Every memory window the root bridge's `_CRS` list at `at` says it -/// forwards, written into `out`, and how many. +/// What a root bridge's `_CRS` produces: its bus range, where it names one, +/// and how many memory windows [`root_bridge`] wrote. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct RootResources { + pub buses: Option<(u8, u8)>, + pub windows: usize, +} + +/// The bus range and every memory window the root bridge's `_CRS` list at +/// `at` says it forwards, the windows written into `out`. /// /// **A window is a memory range an Address Space Descriptor produces** -/// (ACPI 6.5 §6.4.3.5). Its bus numbers and I/O ranges, in those descriptors -/// and in the small I/O Port and Fixed I/O Port descriptors (§6.4.2.5, -/// §6.4.2.6), are stepped over; every other descriptor is refused by name, +/// (ACPI 6.5 §6.4.3.5); its buses are the one bus number range one produces, +/// inside bus 0xff. Its I/O ranges, in those descriptors and in the small I/O +/// Port and Fixed I/O Port descriptors (§6.4.2.5, §6.4.2.6), are stepped +/// over; every other descriptor is refused by name, /// as is a memory range whose two accounts of its extent disagree, that the /// bridge consumes, or that a non-zero `_TRA` translates. A range of no /// length is none. @@ -191,9 +204,10 @@ fn field(phys: P, at: u64, offset: usize, width: usize) -> u64 { /// **A refusal carries no windows at all**: a caller handed the ones decoded /// before the refusal would be holding an aperture with a hole in it, and would /// place a BAR in the hole. -pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) -> Result { +pub fn root_bridge(phys: P, at: u64, out: &mut [RootBridgeWindow]) -> Result { let mut offset = 0usize; let mut found = 0usize; + let mut buses = None; loop { if offset >= MAX_LIST_BYTES { return Err(ResourceError::Unterminated); @@ -202,7 +216,7 @@ pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) - let head = at + offset as u64; offset += item.len; let layout = match item.tag { - END_TAG => return Ok(found), + END_TAG => return Ok(RootResources { buses, windows: found }), IO_PORT | FIXED_IO_PORT => continue, tag => Layout::of(tag).ok_or(ResourceError::UnknownTag { tag })?, }; @@ -210,19 +224,24 @@ pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) - return Err(ResourceError::Short { tag: item.tag, whole: item.len, needed: layout.bytes() }); } let kind = phys.byte(head + RESOURCE_TYPE as u64); - match kind { - TYPE_MEMORY => {} - TYPE_IO | TYPE_BUS => continue, - kind => return Err(ResourceError::UnknownResourceType { kind }), + if !matches!(kind, TYPE_MEMORY | TYPE_IO | TYPE_BUS) { + return Err(ResourceError::UnknownResourceType { kind }); } let read = |n| field(phys, head, layout.field(n), layout.width); let (min, max, translation, length) = (read(1), read(2), read(3), read(4)); - if length == 0 { + if kind == TYPE_IO || length == 0 { continue; } if min.checked_add(length - 1) != Some(max) { return Err(ResourceError::Inconsistent { min, max, length }); } + if kind == TYPE_BUS { + buses = match (buses, u8::try_from(min), u8::try_from(max)) { + (None, Ok(first), Ok(last)) => Some((first, last)), + _ => return Err(ResourceError::Buses { min, max }), + }; + continue; + } if translation != 0 { return Err(ResourceError::Translated { min, offset: translation }); } diff --git a/lib/acpi/tests/resource.rs b/lib/acpi/tests/resource.rs index c1e013632a9..04fdf962812 100644 --- a/lib/acpi/tests/resource.rs +++ b/lib/acpi/tests/resource.rs @@ -5,7 +5,7 @@ mod common; use common::{address_space, qword_descriptor, resource_list as list, Machine, BUS, IO, MEMORY}; use toyos_abi::acpi::RootBridgeWindow; -use toyos_acpi::{io_ports, memory_windows, ResourceError, MAX_LIST_BYTES}; +use toyos_acpi::{io_ports, root_bridge, ResourceError, RootResources, MAX_LIST_BYTES}; /// Where a firmware pool allocation sits in the crafted machines below. const AT: u64 = 0x7f00_1234; @@ -22,7 +22,7 @@ fn qword(kind: u8, min: u64, length: u64, translation: u64) -> Vec { fn windows(bytes: &[u8], room: usize) -> Result, ResourceError> { let regions: &[(u64, &[u8])] = &[(AT, bytes)]; let mut out = vec![RootBridgeWindow::default(); room]; - let count = memory_windows(Machine { regions }, AT, &mut out)?; + let count = root_bridge(Machine { regions }, AT, &mut out)?.windows; out.truncate(count); Ok(out) } @@ -155,6 +155,24 @@ fn every_width_of_address_space_descriptor_is_read() { assert_eq!(windows(&list(&[extended]), 8), Ok(vec![RootBridgeWindow { base: 0xc000_0000, length: 0x1000_0000 }])); } +/// The root's buses are its one bus number range, in a descriptor of any +/// width; a second range, and one past bus 0xff, are refused by name, and a +/// list with none names none. +#[test] +fn a_roots_buses_are_its_one_bus_range() { + let read = |bytes: &[u8]| { + let regions: &[(u64, &[u8])] = &[(AT, bytes)]; + root_bridge(Machine { regions }, AT, &mut [RootBridgeWindow::default(); 8]) + }; + let bus = |min, length| address_space(0x88, 2, BUS, [0, min, min + length - 1, 0, length]); + let memory = qword(MEMORY, 0x8000_0000, 0x1000_0000, 0); + assert_eq!(read(&list(&[bus(0, 0x100), memory.clone()])), Ok(RootResources { buses: Some((0, 0xff)), windows: 1 })); + assert_eq!(read(&list(&[qword(BUS, 0x80, 0x40, 0)])), Ok(RootResources { buses: Some((0x80, 0xbf)), windows: 0 })); + assert_eq!(read(&list(&[memory])), Ok(RootResources { buses: None, windows: 1 })); + assert_eq!(read(&list(&[bus(0, 0x10), bus(0x20, 0x10)])), Err(ResourceError::Buses { min: 0x20, max: 0x2f })); + assert_eq!(read(&list(&[bus(0xf0, 0x20)])), Err(ResourceError::Buses { min: 0xf0, max: 0x10f })); +} + /// q35's `_CRS` carries the configuration ports beside its ranges as a small /// I/O Port Descriptor: stepped over with the I/O ranges, and what follows it /// still read. diff --git a/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index 94caeea8306..7d7ba0d93d8 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -30,11 +30,12 @@ //! until a holder has supplied it [`crate::syscall::SYS_SHUTDOWN`] is refused //! [`crate::syscall::SyscallError::NotSupported`]. //! -//! **The memory the PCI root bridges decode is the holder's to declare, once** -//! ([`op::DECLARE`], [`Apertures`]): each root's `_CRS` is AML. Until a -//! declaration is held the kernel hands over no PCI function, and a claim -//! made meanwhile waits for it; a declaration the kernel refuses, or a claim -//! that ends without one, leaves every PCI claim refused for the boot. +//! **The PCI root bridges are the holder's to declare, once** ([`op::DECLARE`], +//! [`Apertures`]): each root's memory windows from its `_CRS`, and what its +//! `_OSC` granted, are AML. Until a declaration is held the kernel hands over +//! no PCI function, and a claim made meanwhile waits for it; a declaration +//! the kernel refuses, or a claim that ends without one, leaves every PCI +//! claim refused for the boot. //! //! **A hardware-reduced machine** ([`HARDWARE_REDUCED`]; ACPI 6.5 §4.1) has //! no fixed hardware: its claim names no block and raises no SCI, and its @@ -103,7 +104,7 @@ pub mod op { /// control to power the machine off. Once under a claim, and kept until /// the next claim's holder supplies its own. pub const S5: u64 = 3; - /// Declare the memory the PCI root bridges decode: an + /// Declare the PCI root bridges, their memory and their `_OSC`: an /// [`Apertures`](super::Apertures), in through the caller's struct. Once /// a boot, from the claim's holder. pub const DECLARE: u64 = 4; @@ -351,30 +352,108 @@ impl RootBridgeWindow { } } -/// The most windows one declaration carries. -pub const MAX_WINDOWS: usize = 32; +/// The most root bridges one declaration carries. +pub const MAX_BRIDGES: usize = 16; +/// The most memory windows one root bridge's record carries. +pub const MAX_BRIDGE_WINDOWS: usize = 8; + +/// The PCI host bridge `_OSC` Control field's bits ToyOS asks for (PCI +/// Firmware Specification 3.3, §4.5.1, Table 4-5): native PCI Express +/// hot-plug, and the PCI Express capability structure. +pub const NATIVE_HOT_PLUG: u32 = 1 << 0; +pub const CAPABILITY_STRUCTURE: u32 = 1 << 4; + +/// What a root bridge's `_OSC` said. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Osc { + /// The root has no `_OSC`. + Absent, + /// Its evaluation, or its answer, was refused: nothing is granted. + Refused, + /// It was negotiated, and granted these Control bits. + Granted(u32), +} + +/// [`HostBridge::osc`]'s words. +const OSC_ABSENT: u32 = 0; +const OSC_REFUSED: u32 = 1; +const OSC_ANSWERED: u32 = 2; crate::user_safe! { - /// What [`op::DECLARE`] carries: the first `count` of `windows`, every - /// root bridge's together. + /// One PCI root bridge: its segment, the buses below it, the memory + /// windows its `_CRS` produces, and what its `_OSC` granted. + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub struct HostBridge { + pub segment: u16, + pub first_bus: u8, + pub last_bus: u8, + pub osc: u32, + pub granted: u32, + pub window_count: u32, + pub windows: [RootBridgeWindow; MAX_BRIDGE_WINDOWS], + } +} + +impl HostBridge { + /// One root's record, or `None` past [`MAX_BRIDGE_WINDOWS`] or over a bus + /// range that ends before it starts. + pub fn new(segment: u16, buses: (u8, u8), windows: &[RootBridgeWindow], osc: Osc) -> Option { + let (osc, granted) = match osc { + Osc::Absent => (OSC_ABSENT, 0), + Osc::Refused => (OSC_REFUSED, 0), + Osc::Granted(granted) => (OSC_ANSWERED, granted), + }; + let mut bridge = Self { + segment, + first_bus: buses.0, + last_bus: buses.1, + osc, + granted, + window_count: u32::try_from(windows.len()).ok()?, + windows: [RootBridgeWindow::default(); MAX_BRIDGE_WINDOWS], + }; + bridge.windows.get_mut(..windows.len())?.copy_from_slice(windows); + (buses.0 <= buses.1).then_some(bridge) + } + + /// Its windows, or `None` for a count past the array. + pub fn windows(&self) -> Option<&[RootBridgeWindow]> { + self.windows.get(..usize::try_from(self.window_count).ok()?) + } + + /// What its `_OSC` said, or `None` for a word no [`Osc`] is, or a grant + /// beside no answer. + pub fn osc(&self) -> Option { + match (self.osc, self.granted) { + (OSC_ABSENT, 0) => Some(Osc::Absent), + (OSC_REFUSED, 0) => Some(Osc::Refused), + (OSC_ANSWERED, granted) => Some(Osc::Granted(granted)), + _ => None, + } + } +} + +crate::user_safe! { + /// What [`op::DECLARE`] carries: the first `count` of `bridges`. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Apertures { pub count: u64, - pub windows: [RootBridgeWindow; MAX_WINDOWS], + pub bridges: [HostBridge; MAX_BRIDGES], } } impl Apertures { - /// `windows` as a declaration, or `None` past [`MAX_WINDOWS`]. - pub fn new(windows: &[RootBridgeWindow]) -> Option { - let mut declared = Self { count: windows.len() as u64, windows: [RootBridgeWindow::default(); MAX_WINDOWS] }; - declared.windows.get_mut(..windows.len())?.copy_from_slice(windows); + /// `bridges` as a declaration, or `None` past [`MAX_BRIDGES`]. + pub fn new(bridges: &[HostBridge]) -> Option { + let empty = HostBridge::new(0, (0, 0), &[], Osc::Absent).expect("an empty record"); + let mut declared = Self { count: bridges.len() as u64, bridges: [empty; MAX_BRIDGES] }; + declared.bridges.get_mut(..bridges.len())?.copy_from_slice(bridges); Some(declared) } - /// The windows declared, or `None` for a count past the array. - pub fn windows(&self) -> Option<&[RootBridgeWindow]> { - self.windows.get(..usize::try_from(self.count).ok()?) + /// The bridges declared, or `None` for a count past the array. + pub fn bridges(&self) -> Option<&[HostBridge]> { + self.bridges.get(..usize::try_from(self.count).ok()?) } } @@ -403,14 +482,24 @@ mod tests { assert!(!whole.holds(2, u64::MAX)); } - /// A list that fits is carried whole and read back as itself; one past the - /// array is no declaration, and a count past it reads back as none. + /// A record that fits is carried whole and read back as itself; one past + /// its array is none, and a count or an `_OSC` word past what the ABI + /// names reads back as none. #[test] - fn a_declaration_carries_its_windows_and_no_count_past_them() { + fn a_declaration_carries_its_bridges_and_nothing_past_them() { let w = RootBridgeWindow { base: 0x1000_0000, length: 0x2eff_0000 }; - let declared = Apertures::new(&[w]).expect("one window fits"); - assert_eq!(declared.windows(), Some(&[w][..])); - assert_eq!(Apertures::new(&[w; MAX_WINDOWS + 1]), None); - assert_eq!(Apertures { count: MAX_WINDOWS as u64 + 1, ..declared }.windows(), None); + let root = HostBridge::new(0, (0, 0xff), &[w], Osc::Granted(NATIVE_HOT_PLUG)).expect("one window fits"); + assert_eq!(root.windows(), Some(&[w][..])); + assert_eq!(root.osc(), Some(Osc::Granted(NATIVE_HOT_PLUG))); + assert_eq!(HostBridge::new(0, (0, 0xff), &[w; MAX_BRIDGE_WINDOWS + 1], Osc::Absent), None); + assert_eq!(HostBridge::new(0, (2, 1), &[w], Osc::Absent), None); + assert_eq!(HostBridge { window_count: MAX_BRIDGE_WINDOWS as u32 + 1, ..root }.windows(), None); + assert_eq!(HostBridge { osc: 3, ..root }.osc(), None); + assert_eq!(HostBridge { osc: OSC_ABSENT, granted: 1, ..root }.osc(), None); + + let declared = Apertures::new(&[root]).expect("one bridge fits"); + assert_eq!(declared.bridges(), Some(&[root][..])); + assert_eq!(Apertures::new(&[root; MAX_BRIDGES + 1]), None); + assert_eq!(Apertures { count: MAX_BRIDGES as u64 + 1, ..declared }.bridges(), None); } } diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 294a83641f5..63374a7a2ff 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -2111,7 +2111,7 @@ pub fn acpi_s5(claim: RawHandle, slp_typ_a: u64) -> Result<(), SyscallError> { check_unit(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::S5, slp_typ_a, 0)) } -/// Declare the memory the PCI root bridges decode ([`crate::acpi::op::DECLARE`]). +/// Declare the PCI root bridges ([`crate::acpi::op::DECLARE`]). /// `Ok` is the declaration held; [`SyscallError::InvalidArgument`] one the /// kernel refused, said in its log by name and leaving every PCI claim refused; /// [`SyscallError::AlreadyExists`] a declaration made after the first, or after diff --git a/sdk/toyos/src/device.rs b/sdk/toyos/src/device.rs index d38ae5c25c7..142cf481bfc 100644 --- a/sdk/toyos/src/device.rs +++ b/sdk/toyos/src/device.rs @@ -234,7 +234,7 @@ impl AcpiDev { syscall::acpi_s5(self.as_handle(), slp_typ_a) } - /// Declare the memory the PCI root bridges decode + /// Declare the PCI root bridges /// ([`syscall::acpi_declare`]). pub fn declare(&self, declared: &toyos_abi::acpi::Apertures) -> Result<(), SyscallError> { syscall::acpi_declare(self.as_handle(), declared) diff --git a/system/acpiserver/src/apertures.rs b/system/acpiserver/src/apertures.rs index e1c57d152cf..8b8e2a64387 100644 --- a/system/acpiserver/src/apertures.rs +++ b/system/acpiserver/src/apertures.rs @@ -1,29 +1,33 @@ -//! The memory the PCI root bridges decode, declared to the kernel once -//! ([`Kernel::declare`]): every PCI claim on the machine waits for it, so it +//! The PCI root bridges, declared to the kernel once ([`Kernel::declare`]): +//! each one's segment, buses and memory windows, and what its `_OSC` +//! granted. Every PCI claim on the machine waits for the declaration, so it //! is read as soon as the namespace is initialised (ACPI 6.5 §6.5.1). //! //! **A root is a PCI host bridge by its `_HID` or a `_CID`** (PNP0A03 or //! PNP0A08, §6.1.5, §6.1.2) **below no other one**; a root whose `_STA` says -//! it is not present decodes nothing and is not read. Its windows are what -//! `toyos_acpi::memory_windows` reads of its `_CRS` (§6.2.2). +//! it is not present decodes nothing and is not read. Its segment is its +//! `_SEG` (§6.5.6), 0 without one; its buses and windows are what +//! `toyos_acpi::root_bridge` reads of its `_CRS` (§6.2.2); its `_OSC` is +//! negotiated once, query then commit (`crate::osc::negotiate_root`). //! -//! **The declaration is every root's windows together, or none at all**: a -//! root whose `_STA` or `_CRS` is refused, a list the decoder refuses, more -//! windows than one declaration carries, and a namespace with no root are -//! each said by name and declared as no window, which the kernel refuses — -//! and with it every PCI claim, rather than one placed by a list with a hole -//! in it. A machine with no namespace declares the same. - -use toyos_abi::acpi::{RootBridgeWindow, MAX_WINDOWS}; +//! **The declaration is every root together, or none at all**: a root whose +//! `_STA`, `_SEG` or `_CRS` is refused, a list the decoder refuses or that +//! names no bus range, more roots or windows than a declaration carries, and +//! a namespace with no root are each said by name and declared as no root, +//! which the kernel refuses — and with it every PCI claim, rather than one +//! placed by a list with a hole in it. A machine with no namespace declares +//! the same. An `_OSC` refused is that root's grant refused, and nothing +//! else's. + +use toyos_abi::acpi::{HostBridge, Osc, RootBridgeWindow, MAX_BRIDGES, MAX_BRIDGE_WINDOWS}; use toyos_aml::{Kind, Value}; use crate::aml::{kind, Aml}; use crate::host::{Declared, Kernel, Stopping, OWN}; use crate::init::{evaluate, host_bridge, Census, Names, PRESENT}; -/// Every root bridge's windows, in walk order, or why there is no -/// declaration. -pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) -> Result, String> { +/// Every root bridge, in walk order, or why there is no declaration. +pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) -> Result, String> { let devices = names.objects.iter().filter(|named| named.kind == Kind::Device).map(|named| named.path.as_str()); let mut bridges: Vec<&str> = Vec::new(); for device in devices { @@ -34,42 +38,77 @@ pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) if bridges.is_empty() { return Err("the namespace names no PCI host bridge".into()); } - let mut windows = Vec::new(); + let mut roots = Vec::new(); for root in bridges { if !present(aml, names, root, census)? { println!("{OWN}{root} is not present, so it decodes nothing"); continue; } + if roots.len() == MAX_BRIDGES { + return Err(format!("the namespace names more than the {MAX_BRIDGES} root bridges a declaration carries")); + } + let segment = segment(aml, names, root)?; let path = format!("{root}._CRS"); let crs = match aml.interpreter.evaluate(&mut aml.host, &path, &[]) { Ok(Value::Buffer(crs)) => crs, Ok(_) => return Err(said(&path, "a root bridge's _CRS is no buffer")), Err(why) => return Err(said(&path, &format!("a root bridge's _CRS was refused: {}", kind(&why)))), }; - let mut out = [RootBridgeWindow::default(); MAX_WINDOWS]; - let room = MAX_WINDOWS - windows.len(); - match toyos_acpi::memory_windows(&crs[..], 0, &mut out[..room]) { - Ok(count) => windows.extend_from_slice(&out[..count]), + let mut windows = [RootBridgeWindow::default(); MAX_BRIDGE_WINDOWS]; + let found = match toyos_acpi::root_bridge(&crs[..], 0, &mut windows) { + Ok(found) => found, Err(why) => return Err(said(&path, &format!("a root bridge's _CRS is no list of windows: {why}"))), + }; + let buses = found.buses.ok_or_else(|| said(&path, "a root bridge's _CRS names no bus range"))?; + let osc = osc(aml, names, root); + roots.push(HostBridge::new(segment, buses, &windows[..found.windows], osc).expect("the decoder bounded both")); + } + Ok(roots) +} + +/// What `root`'s `_OSC` granted, said under [`OWN`] where it was refused. +fn osc(aml: &mut Aml<'_, K>, names: &Names, root: &str) -> Osc { + let path = format!("{root}._OSC"); + if !names.paths.contains(&path) { + return Osc::Absent; + } + match crate::osc::negotiate_root(aml, &path) { + Ok(granted) => Osc::Granted(granted), + Err(why) => { + println!("{OWN}{path} was refused: {why}"); + Osc::Refused } } - Ok(windows) } -/// Declare what [`read`] answers, or no window where it refused, saying -/// which and what the kernel did with it. -pub fn declare(kernel: &K, read: Result, String>) -> Result<(), Stopping> { - let windows = match read { - Ok(windows) => windows, +/// `root`'s segment: its `_SEG`, 0 where it has none (ACPI 6.5 §6.5.6). +fn segment(aml: &mut Aml<'_, K>, names: &Names, root: &str) -> Result { + let path = format!("{root}._SEG"); + if !names.paths.contains(&path) { + return Ok(0); + } + match aml.interpreter.evaluate(&mut aml.host, &path, &[]) { + Ok(Value::Integer(segment)) => u16::try_from(segment).map_err(|_| said(&path, "a root bridge's _SEG is past 0xffff")), + Ok(_) => Err(said(&path, "a root bridge's _SEG is no integer")), + Err(why) => Err(said(&path, &format!("a root bridge's _SEG was refused: {}", kind(&why)))), + } +} + +/// Declare what [`read`] answers, or no root where it refused, saying which +/// and what the kernel did with it. +pub fn declare(kernel: &K, read: Result, String>) -> Result<(), Stopping> { + let roots = match read { + Ok(roots) => roots, Err(why) => { - println!("acpiserver: no window declared, so every PCI claim is refused: {why}"); + println!("acpiserver: no root bridge declared, so every PCI claim is refused: {why}"); Vec::new() } }; - let listed: Vec = windows.iter().map(|w| format!("mem {:#x}..{:#x}", w.base, w.end())).collect(); - match kernel.declare(&windows)? { + let listed: Vec = + roots.iter().flat_map(|root| root.windows().expect("made by `HostBridge::new`")).map(|w| format!("mem {:#x}..{:#x}", w.base, w.end())).collect(); + match kernel.declare(&roots)? { Declared::Held => println!("acpiserver: the root bridges' memory declared: {}", listed.join(", ")), - Declared::Refused if windows.is_empty() => {} + Declared::Refused if roots.is_empty() => {} Declared::Refused => println!("acpiserver: the kernel refused the root bridges' memory as declared ({}); its log says why", listed.join(", ")), Declared::Late => println!("acpiserver: the kernel already holds an answer for the root bridges' memory, so nothing is declared"), } @@ -104,59 +143,80 @@ fn said(path: &str, why: &str) -> String { mod tests { use super::*; use crate::aml::load; + use crate::aml::tests::{sealed, CRAFTED_RSDP}; + use crate::common::{buffer, cat, def_name, device, int, method, ret, string}; use crate::host::tests::Scripted; + use crate::osc::ROOT_CONTROL; const Q35_DSDT: &[u8] = include_bytes!("../../../lib/acpi/fixtures/qemu-11.1.1/dsdt.bin"); const VIRT_DSDT: &[u8] = include_bytes!("../../../lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin"); - /// The windows a DSDT's roots declare, read as the server reads them. - fn declared(dsdt: &[u8]) -> Result, String> { + /// The roots a DSDT declares, read as the server reads them. + fn declared(dsdt: &[u8]) -> Result, String> { let kernel = crate::aml::tests::crafted(dsdt, &[]); - let (_, kept) = load(&kernel, crate::aml::tests::CRAFTED_RSDP); + let (_, kept) = load(&kernel, CRAFTED_RSDP); let mut aml = kept.expect("the DSDT loads"); let names = Names::read(&aml.interpreter).expect("its namespace walks"); read(&mut aml, &names, &mut Census::default()) } + fn windows(root: &HostBridge) -> Vec { + root.windows().expect("a record `HostBridge::new` made").to_vec() + } + /// QEMU's `virt` (the measured case): its one root, `\_SB.PCI0` - /// (PNP0A08), declares its 32-bit and 64-bit memory windows and nothing - /// of its bus numbers, its I/O window translated to 0x3eff0000, ECAM or - /// flash. + /// (PNP0A08) on segment 0, declares buses 0 to 0xff and its 32-bit and + /// 64-bit memory windows, and nothing of its I/O window translated to + /// 0x3eff0000, ECAM or flash; its `_OSC` grants what was asked. #[test] - fn virts_root_declares_its_two_memory_windows() { + fn virts_root_declares_its_buses_windows_and_grant() { + let roots = declared(VIRT_DSDT).expect("virt's root"); + let [root] = roots.as_slice() else { panic!("{roots:x?}") }; + assert_eq!((root.segment, root.first_bus, root.last_bus), (0, 0, 0xff)); assert_eq!( - declared(VIRT_DSDT), - Ok(vec![ + windows(root), + [ RootBridgeWindow { base: 0x1000_0000, length: 0x2eff_0000 }, RootBridgeWindow { base: 0x80_0000_0000, length: 0x80_0000_0000 }, - ]) + ] ); + assert_eq!(root.osc(), Some(Osc::Granted(VIRT_GRANTED))); } - /// q35: one root whose `_CRS` names the VGA window, two 32-bit windows - /// and the 64-bit window above memory, and steps over its configuration - /// ports and I/O ranges. + /// q35: one root on buses 0 to 0xff whose `_CRS` names the VGA window, + /// two 32-bit windows and the 64-bit window above memory, and steps over + /// its configuration ports and I/O ranges. #[test] - fn q35s_root_declares_its_memory_windows() { + fn q35s_root_declares_its_buses_windows_and_grant() { + let roots = declared(Q35_DSDT).expect("q35's root"); + let [root] = roots.as_slice() else { panic!("{roots:x?}") }; + assert_eq!((root.segment, root.first_bus, root.last_bus), (0, 0, 0xff)); assert_eq!( - declared(Q35_DSDT), - Ok(vec![ + windows(root), + [ RootBridgeWindow { base: 0xa_0000, length: 0x2_0000 }, RootBridgeWindow { base: 0x8000_0000, length: 0x6000_0000 }, RootBridgeWindow { base: 0xf000_0000, length: 0xec0_0000 }, RootBridgeWindow { base: 0xc0_0000_0000, length: 0x8_0000_0000 }, - ]) + ] ); + assert_eq!(root.osc(), Some(Osc::Granted(Q35_GRANTED))); } + /// What each QEMU's root `_OSC` grants of [`ROOT_CONTROL`]: `virt` both, + /// q35 the capability structure and not native hot-plug, which its + /// firmware keeps for its own ACPI hot-plug. + const VIRT_GRANTED: u32 = ROOT_CONTROL; + const Q35_GRANTED: u32 = toyos_abi::acpi::CAPABILITY_STRUCTURE; + /// No byte of either `_CRS` as evaluated panics the decoder or runs it /// away: each takes each of its 255 other values in turn, and both arms, - /// windows and refusal, are reached. + /// a root and a refusal, are reached. #[test] fn no_single_byte_mutation_of_a_roots_crs_panics_or_runs_away() { for dsdt in [Q35_DSDT, VIRT_DSDT] { let kernel = crate::aml::tests::crafted(dsdt, &[]); - let (_, kept) = load(&kernel, crate::aml::tests::CRAFTED_RSDP); + let (_, kept) = load(&kernel, CRAFTED_RSDP); let mut aml = kept.expect("the DSDT loads"); let Ok(Value::Buffer(crs)) = aml.interpreter.evaluate(&mut aml.host, "\\_SB_.PCI0._CRS", &[]) else { panic!("PCI0._CRS is a buffer"); @@ -166,9 +226,9 @@ mod tests { for value in (0..=255u8).filter(|&v| v != crs[offset]) { let mut mutated = crs.clone(); mutated[offset] = value; - let mut out = [RootBridgeWindow::default(); MAX_WINDOWS]; - match toyos_acpi::memory_windows(&mutated[..], 0, &mut out) { - Ok(count) => assert!(out[..count].iter().all(|w| w.length != 0), "byte {offset} as {value:#04x}"), + let mut out = [RootBridgeWindow::default(); MAX_BRIDGE_WINDOWS]; + match toyos_acpi::root_bridge(&mutated[..], 0, &mut out) { + Ok(found) => assert!(out[..found.windows].iter().all(|w| w.length != 0), "byte {offset} as {value:#04x}"), Err(_) => refused += 1, } mutations += 1; @@ -178,26 +238,65 @@ mod tests { } } - /// A root that is not a list of windows, one whose `_CRS` is refused, and - /// a namespace with none: no declaration, each said by name; and what the - /// server declares then is no window at all. + /// A root's `_CRS`: buses 0 to 0xff and one window at 0x80000000. + fn crs() -> Vec { + let bus = [0x88, 0x0D, 0x00, 0x02, 0x0C, 0x00, 0, 0, 0, 0, 0xFF, 0, 0, 0, 0x00, 0x01]; + let mut memory = vec![0x87, 0x17, 0x00, 0x00, 0x0C, 0x03]; + for field in [0u32, 0x8000_0000, 0x8fff_ffff, 0, 0x1000_0000] { + memory.extend_from_slice(&field.to_le_bytes()); + } + let list = cat(&[&bus, &memory, &[0x79, 0x00]]); + buffer(&int(list.len() as u64), &list) + } + + /// `\_SB.PCI0`, a PNP0A08 whose `_CRS` is `crs` and which carries `rest`. + fn root(crs: &[u8], rest: &[u8]) -> Vec { + let body = cat(&[&def_name("_HID", &string("PNP0A08")), &def_name("_CRS", crs), rest]); + sealed(b"DSDT", &crate::common::scope("\\_SB", &device("PCI0", &body))) + } + + /// A root's `_OSC` returning the answer `answer` to both the query and + /// the commit. + fn osc(answer: &[u8]) -> Vec { + method("_OSC", 4, &ret(answer)) + } + + /// The three DWORDs as a Buffer. + fn dwords(words: [u32; 3]) -> Vec { + let bytes: Vec = words.iter().flat_map(|w| w.to_le_bytes()).collect(); + buffer(&int(12), &bytes) + } + + /// What a root's `_OSC` grants, read off a namespace of that one root: + /// a firmware that echoes the request grants it; one that answers + /// failure, or masks every Control bit, grants nothing; a root with no + /// `_OSC` says so; and an answer that is no buffer of three DWORDs is + /// refused by name, its root still declared. + #[test] + fn a_roots_osc_is_negotiated_once_and_its_grant_read() { + let granted = |rest: &[u8]| declared(&root(&crs(), rest)).map(|roots| roots[0].osc()); + // Return (Arg3): the query's DWORDs back, then the commit's. + assert_eq!(granted(&osc(&[0x6B])), Ok(Some(Osc::Granted(ROOT_CONTROL)))); + assert_eq!(granted(&osc(&dwords([1 << 1, 0, ROOT_CONTROL]))), Ok(Some(Osc::Granted(0)))); + assert_eq!(granted(&osc(&dwords([0, 0, 0]))), Ok(Some(Osc::Granted(0)))); + // Control bits granted that were never asked for are dropped. + assert_eq!(granted(&osc(&dwords([0, 0, u32::MAX]))), Ok(Some(Osc::Granted(ROOT_CONTROL)))); + assert_eq!(granted(&[]), Ok(Some(Osc::Absent))); + assert_eq!(granted(&osc(&buffer(&int(4), &[0; 4]))), Ok(Some(Osc::Refused))); + assert_eq!(granted(&osc(&int(5))), Ok(Some(Osc::Refused))); + } + + /// A root that is not a list of windows, one whose `_CRS` is refused or + /// names no bus range, and a namespace with none: no declaration, each + /// said by name; and what the server declares then is no root at all. #[test] fn a_root_that_declares_nothing_usable_refuses_the_whole_declaration() { - use crate::aml::tests::{cat, sealed}; - // Device (PCI0) { Name (_HID, "PNP0A08") Name (_CRS, Buffer { IRQ }) } - let device = |crs: &[u8]| { - let body = cat(&[&[0x08], b"_HID", &[0x0D], b"PNP0A08", &[0x00], &[0x08], b"_CRS", crs]); - let mut d = vec![0x5B, 0x82, (body.len() + 5) as u8]; - d.extend_from_slice(b"PCI0"); - d.extend_from_slice(&body); - d - }; - let irq = [0x11, 0x08, 0x0A, 0x05, 0x22, 0x01, 0x00, 0x79, 0x00]; - let refused = declared(&sealed(b"DSDT", &device(&irq))); - assert_eq!(refused, Err("a root bridge's _CRS is no list of windows: descriptor tag 0x22 is not decoded here".into())); - let integer = declared(&sealed(b"DSDT", &device(&[0x0A, 0x05]))); - assert_eq!(integer, Err("a root bridge's _CRS is no buffer".into())); - assert_eq!(declared(&sealed(b"DSDT", &[0x08, b'A', b'A', b'A', b'A', 0x00])), Err("the namespace names no PCI host bridge".into())); + let irq = buffer(&int(5), &[0x22, 0x01, 0x00, 0x79, 0x00]); + assert_eq!(declared(&root(&irq, &[])), Err("a root bridge's _CRS is no list of windows: descriptor tag 0x22 is not decoded here".into())); + assert_eq!(declared(&root(&int(5), &[])), Err("a root bridge's _CRS is no buffer".into())); + let no_bus = buffer(&int(2), &[0x79, 0x00]); + assert_eq!(declared(&root(&no_bus, &[])), Err("a root bridge's _CRS names no bus range".into())); + assert_eq!(declared(&sealed(b"DSDT", &def_name("AAAA", &int(0)))), Err("the namespace names no PCI host bridge".into())); let kernel = Scripted::default(); declare(&kernel, Err("the namespace names no PCI host bridge".into())).expect("not stopping"); diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index c513f7fac43..200067db8ce 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -92,7 +92,7 @@ use std::collections::BTreeMap; use std::fmt; use std::time::{Duration, Instant}; -use toyos_abi::acpi::{pci_address, Access, Refused, RootBridgeWindow, Space, Width, UNLISTED}; +use toyos_abi::acpi::{pci_address, Access, HostBridge, Refused, Space, Width, UNLISTED}; use toyos_aml::{Address, Denied, Host, RuntimeLoad, Transfer}; use crate::devices::Ec; @@ -150,9 +150,8 @@ pub trait Kernel { /// The machine is hardware-reduced (`toyos_abi::acpi::HARDWARE_REDUCED`): /// no fixed hardware, and a power-off that writes no sleep type. fn hardware_reduced(&self) -> bool; - /// Declare the memory the PCI root bridges decode, once - /// ([`crate::apertures`]). - fn declare(&self, windows: &[RootBridgeWindow]) -> Result; + /// Declare the PCI root bridges, once ([`crate::apertures`]). + fn declare(&self, roots: &[HostBridge]) -> Result; /// Waits until `until`, serving the SCI as `sci::await_sleep` does for /// the fixed power button where the claim serves it and the controller's /// GPE `ec_gpe`: what latched and was taken. @@ -783,8 +782,8 @@ pub mod tests { pub handed: Cell>, /// The machine is hardware-reduced. pub reduced: bool, - /// The windows it was declared, once. - pub declared: RefCell>>, + /// The roots it was declared, once. + pub declared: RefCell>>, /// What each Sleep's wait takes, in turn; nothing once it runs out. /// Each served set a Sleep was asked to serve. pub sleeps: RefCell>, @@ -915,14 +914,14 @@ pub mod tests { self.reduced } - fn declare(&self, windows: &[RootBridgeWindow]) -> Result { + fn declare(&self, roots: &[HostBridge]) -> Result { self.stopping()?; let mut declared = self.declared.borrow_mut(); if declared.is_some() { return Ok(Declared::Late); } - *declared = Some(windows.to_vec()); - Ok(if windows.is_empty() { Declared::Refused } else { Declared::Held }) + *declared = Some(roots.to_vec()); + Ok(if roots.is_empty() { Declared::Refused } else { Declared::Held }) } fn latched(&self) -> Option { diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 5df62a2cfb1..00aa571f0c0 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -95,7 +95,7 @@ use toyos::ioport::{in16, in8, out16, out8}; use toyos::poller::{Poller, READABLE}; use toyos::power::{self, Stop}; use toyos::AcpiDev; -use toyos_abi::acpi::{Access, AcpiInfo, Apertures, Block, RootBridgeWindow, FIXED_POWER_BUTTON, HARDWARE_REDUCED}; +use toyos_abi::acpi::{Access, AcpiInfo, Apertures, Block, HostBridge, FIXED_POWER_BUTTON, HARDWARE_REDUCED}; use toyos_abi::syscall::{DeviceType, SyscallError}; use acpiserver_api::{CONTROLLER_NONE, CONTROLLER_SERVED}; @@ -275,8 +275,8 @@ impl Kernel for Claim<'_> { self.info.flags & HARDWARE_REDUCED != 0 } - fn declare(&self, windows: &[RootBridgeWindow]) -> Result { - let declared = Apertures::new(windows).expect("acpiserver: the windows were read into a declaration's room"); + fn declare(&self, roots: &[HostBridge]) -> Result { + let declared = Apertures::new(roots).expect("acpiserver: the roots were read into a declaration's room"); match self.dev.declare(&declared) { Err(SyscallError::InvalidArgument) => Ok(Declared::Refused), Err(SyscallError::AlreadyExists) => Ok(Declared::Late), diff --git a/system/acpiserver/src/osc.rs b/system/acpiserver/src/osc.rs index b81936421ea..02726c0ba11 100644 --- a/system/acpiserver/src/osc.rs +++ b/system/acpiserver/src/osc.rs @@ -1,7 +1,9 @@ //! `_OSC` negotiated (ACPI 6.5 §6.2.11; Intel 302223-007 §2.2): `\_SB._OSC` //! under the platform-wide UUID (§6.2.11.2), then, on an Intel processor, //! each processor's `_OSC` under Intel's, each a query with the Query flag -//! and then a commit of what the query granted (§6.2.11.1.1, §6.2.11.1.3). +//! and then a commit of what the query granted (§6.2.11.1.1, §6.2.11.1.3); +//! and each PCI root bridge's under the PCI host bridge UUID ([`negotiate_root`], +//! PCI Firmware Specification 3.3 §4.5), by `crate::apertures`. //! //! **What ToyOS claims is one declaration a UUID** ([`PLATFORM`], //! [`PROCESSOR`]), each bit set only where ToyOS does what the bit promises, @@ -113,16 +115,15 @@ impl Answered { } } -/// One evaluation of `path` with the two DWORDs, its answer's two DWORDs. -fn evaluate(aml: &mut Aml<'_, K>, path: &str, uuid: &[u8; 16], first: u32, second: u32) -> Result<[u32; 2], String> { - let capabilities = [first.to_le_bytes(), second.to_le_bytes()].concat(); - let args = [Value::Buffer(uuid.to_vec()), Value::Integer(REVISION), Value::Integer(2), Value::Buffer(capabilities)]; +/// One evaluation of `path` with `N` DWORDs, its answer's `N` DWORDs. +fn evaluate(aml: &mut Aml<'_, K>, path: &str, uuid: &[u8; 16], dwords: [u32; N]) -> Result<[u32; N], String> { + let capabilities: Vec = dwords.iter().flat_map(|dword| dword.to_le_bytes()).collect(); + let args = [Value::Buffer(uuid.to_vec()), Value::Integer(REVISION), Value::Integer(N as u64), Value::Buffer(capabilities)]; match aml.interpreter.evaluate(&mut aml.host, path, &args) { - Ok(Value::Buffer(answer)) if answer.len() == 8 => { - let dword = |at: usize| u32::from_le_bytes(answer[at..at + 4].try_into().expect("four bytes")); - Ok([dword(0), dword(4)]) + Ok(Value::Buffer(answer)) if answer.len() == 4 * N => { + Ok(core::array::from_fn(|i| u32::from_le_bytes(answer[4 * i..4 * i + 4].try_into().expect("four bytes")))) } - Ok(Value::Buffer(answer)) => Err(format!("its answer is {} bytes, where 8 were passed (§6.2.11)", answer.len())), + Ok(Value::Buffer(answer)) => Err(format!("its answer is {} bytes, where {} were passed (§6.2.11)", answer.len(), 4 * N)), Ok(_) => Err("its answer is no Buffer (§6.2.11)".into()), Err(why) => { println!("{OWN}{path} was refused {why:x?}"); @@ -134,15 +135,48 @@ fn evaluate(aml: &mut Aml<'_, K>, path: &str, uuid: &[u8; 16], first: /// `path`'s `_OSC` negotiated under `claim`: the query, and the commit of /// what it granted that `claim` asked for. pub fn negotiate(aml: &mut Aml<'_, K>, path: &str, claim: &Claim) -> Result { - let [query, offered] = evaluate(aml, path, &claim.uuid, QUERY, claim.capabilities)?; + let [query, offered] = evaluate(aml, path, &claim.uuid, [QUERY, claim.capabilities])?; let offered = offered & claim.capabilities; if query & (FAILURE | UNRECOGNIZED_UUID) != 0 { return Ok(Answered { query, offered, commit: None }); } - let [commit, granted] = evaluate(aml, path, &claim.uuid, 0, offered)?; + let [commit, granted] = evaluate(aml, path, &claim.uuid, [0, offered])?; Ok(Answered { query, offered, commit: Some((commit, granted & offered)) }) } +/// The PCI host bridge UUID (PCI Firmware Specification 3.3 §4.5.1). +const HOST_BRIDGE: [u8; 16] = uuid(b"33DB4D5B-1FF7-401C-9657-7441C03DD766"); + +/// The Support field ToyOS states for a root (§4.5.1, Table 4-4): extended +/// configuration space, which the kernel reaches through ECAM (bit 0), and +/// MSI, which every claim is armed with (bit 4). Nothing else it does. +const ROOT_SUPPORT: u32 = 1 << 0 | 1 << 4; + +/// The Control field ToyOS asks of a root (Table 4-5): native PCI Express +/// hot-plug, and the PCI Express capability structure it needs with it. +pub const ROOT_CONTROL: u32 = toyos_abi::acpi::NATIVE_HOT_PLUG | toyos_abi::acpi::CAPABILITY_STRUCTURE; + +/// A root's `_OSC` at `path` negotiated (§4.5.1): the query, and the commit +/// of the Control bits it offered of [`ROOT_CONTROL`]; what is granted is +/// what the commit answered of those, and nothing where either answered +/// failure or an unrecognised UUID. An answer that is no buffer of the three +/// DWORDs passed refuses the negotiation by name. +pub fn negotiate_root(aml: &mut Aml<'_, K>, path: &str) -> Result { + const REFUSED: u32 = FAILURE | UNRECOGNIZED_UUID | UNRECOGNIZED_REVISION; + let [query, _, offered] = evaluate(aml, path, &HOST_BRIDGE, [QUERY, ROOT_SUPPORT, ROOT_CONTROL])?; + let offered = offered & ROOT_CONTROL; + if query & REFUSED != 0 { + println!("{OWN}{path}'s query answered {}", status(query)); + return Ok(0); + } + let [commit, _, granted] = evaluate(aml, path, &HOST_BRIDGE, [0, ROOT_SUPPORT, offered])?; + if commit & REFUSED != 0 { + println!("{OWN}{path}'s commit answered {}", status(commit)); + return Ok(0); + } + Ok(granted & offered) +} + /// What the negotiation answered, for the line the server says. #[derive(Default)] pub struct Negotiated { diff --git a/tests/toyos.rs b/tests/toyos.rs index a724d20f91e..b4a97bacae4 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2624,6 +2624,11 @@ serial: {serial}")); }; eprintln!(" [virt] {armed}"); + // The root bridge the ACPI server declared, with what its `_OSC` granted. + let Some(root) = line("pci: root 0000:00 _OSC: native hot-plug granted") else { + return Err(format!("virt's root was not declared with native hot-plug granted\nserial:\n{serial}")); + }; + eprintln!(" [virt] {root}"); let Some(handed) = line(&format!("{EDU} handed over, ")) else { return Err(format!("edu was never handed over serial: @@ -4023,6 +4028,9 @@ fn a_drives_row_holds_one_process_per_function() -> Result<(), String> { .map_err(|e| format!("{e}\n{console}"))?; let said = serial::Serial::named("the drives boot", console.clone()); said.must_be_clean()?; + // The root bridge the ACPI server declared, with what q35's `_OSC` + // granted: not native hot-plug, which its firmware keeps. + eprintln!(" [drives] {}", said.must_say("pci: root 0000:00 _OSC: not granted")?.trim()); let mut handed: Vec<&str> = console.lines().filter_map(|l| l.split_once("pcidev: PCI ")?.1.split_once(HANDED).map(|(at, _)| at)).collect(); let mut holding: Vec<&str> = From 0a67b018e4b642f8cdbb4b0ecffe439a6e257e83 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 15 Oct 2026 06:39:30 +0200 Subject: [PATCH 5/9] One window list across roots, bounded by the declaration; a refused declaration fails every metal boot The AMD bench's root has more memory windows than the per-root bound of 8, so at 4f5142fea its declaration was refused, every claim on it would have been, and no row saw it: its boot claims nothing and its row judges kernel-driven domains. The windows are now one list of 64 across every root; acpiserver says each root's count and its _CRS bytes under OWN, so the bench's next boot records them. Every metal boot now fails on the kernel's refused-declaration record, and the AMD row requires the declaration. The host bridge _OSC tests gain QEMU q35's masked shape. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- kernel/src/pcidev/mod.rs | 5 +- sdk/abi/src/acpi.rs | 141 +++++++++++++++++------------ system/acpiserver/src/apertures.rs | 94 ++++++++++++++----- system/acpiserver/src/host.rs | 8 +- system/acpiserver/src/main.rs | 9 +- system/acpiserver/src/osc.rs | 4 +- tests/common/claims.rs | 3 + tests/common/metal.rs | 11 +++ tests/toyos.rs | 2 + 9 files changed, 188 insertions(+), 89 deletions(-) diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index cb701efce36..9c67558c87c 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -421,14 +421,15 @@ pub struct RootBridge { impl RootBridge { /// Every bridge `declared` carries, or `None` where a record is none the - /// ABI names: a count past its array, or an `_OSC` word it does not. + /// ABI names: a count or a run of windows past its array, or an `_OSC` + /// word it does not. pub fn decode(declared: &toyos_abi::acpi::Apertures) -> Option> { declared .bridges()? .iter() .map(|bridge| { (bridge.first_bus <= bridge.last_bus).then_some(())?; - let windows = bridge.windows()?.to_vec(); + let windows = declared.windows_of(bridge)?.to_vec(); Some(Self { segment: bridge.segment, first_bus: bridge.first_bus, windows, osc: bridge.osc()? }) }) .collect() diff --git a/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index 7d7ba0d93d8..18f2008c13f 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -354,8 +354,11 @@ impl RootBridgeWindow { /// The most root bridges one declaration carries. pub const MAX_BRIDGES: usize = 16; -/// The most memory windows one root bridge's record carries. -pub const MAX_BRIDGE_WINDOWS: usize = 8; +/// The most memory windows one declaration carries, every root's together. +/// Measured: QEMU's `virt` declares 2, q35 4, the ThinkPad T14 3; the AMD +/// bench's one root more than 8, the per-root bound that refused it. Four +/// times what any machine in reach needs is a bound no firmware should meet. +pub const MAX_WINDOWS: usize = 64; /// The PCI host bridge `_OSC` Control field's bits ToyOS asks for (PCI /// Firmware Specification 3.3, §4.5.1, Table 4-5): native PCI Express @@ -380,8 +383,8 @@ const OSC_REFUSED: u32 = 1; const OSC_ANSWERED: u32 = 2; crate::user_safe! { - /// One PCI root bridge: its segment, the buses below it, the memory - /// windows its `_CRS` produces, and what its `_OSC` granted. + /// One PCI root bridge: its segment, the buses below it, what its `_OSC` + /// granted, and the run of [`Apertures::windows`] its `_CRS` produces. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct HostBridge { pub segment: u16, @@ -389,38 +392,12 @@ crate::user_safe! { pub last_bus: u8, pub osc: u32, pub granted: u32, + pub first_window: u32, pub window_count: u32, - pub windows: [RootBridgeWindow; MAX_BRIDGE_WINDOWS], } } impl HostBridge { - /// One root's record, or `None` past [`MAX_BRIDGE_WINDOWS`] or over a bus - /// range that ends before it starts. - pub fn new(segment: u16, buses: (u8, u8), windows: &[RootBridgeWindow], osc: Osc) -> Option { - let (osc, granted) = match osc { - Osc::Absent => (OSC_ABSENT, 0), - Osc::Refused => (OSC_REFUSED, 0), - Osc::Granted(granted) => (OSC_ANSWERED, granted), - }; - let mut bridge = Self { - segment, - first_bus: buses.0, - last_bus: buses.1, - osc, - granted, - window_count: u32::try_from(windows.len()).ok()?, - windows: [RootBridgeWindow::default(); MAX_BRIDGE_WINDOWS], - }; - bridge.windows.get_mut(..windows.len())?.copy_from_slice(windows); - (buses.0 <= buses.1).then_some(bridge) - } - - /// Its windows, or `None` for a count past the array. - pub fn windows(&self) -> Option<&[RootBridgeWindow]> { - self.windows.get(..usize::try_from(self.window_count).ok()?) - } - /// What its `_OSC` said, or `None` for a word no [`Osc`] is, or a grant /// beside no answer. pub fn osc(&self) -> Option { @@ -434,26 +411,62 @@ impl HostBridge { } crate::user_safe! { - /// What [`op::DECLARE`] carries: the first `count` of `bridges`. + /// What [`op::DECLARE`] carries: the first `bridge_count` of `bridges`, + /// each naming its run of the first `window_count` of `windows`. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Apertures { - pub count: u64, + pub bridge_count: u32, + pub window_count: u32, pub bridges: [HostBridge; MAX_BRIDGES], + pub windows: [RootBridgeWindow; MAX_WINDOWS], + } +} + +impl Default for Apertures { + fn default() -> Self { + let none = HostBridge { segment: 0, first_bus: 0, last_bus: 0, osc: OSC_ABSENT, granted: 0, first_window: 0, window_count: 0 }; + Self { bridge_count: 0, window_count: 0, bridges: [none; MAX_BRIDGES], windows: [RootBridgeWindow::default(); MAX_WINDOWS] } } } impl Apertures { - /// `bridges` as a declaration, or `None` past [`MAX_BRIDGES`]. - pub fn new(bridges: &[HostBridge]) -> Option { - let empty = HostBridge::new(0, (0, 0), &[], Osc::Absent).expect("an empty record"); - let mut declared = Self { count: bridges.len() as u64, bridges: [empty; MAX_BRIDGES] }; - declared.bridges.get_mut(..bridges.len())?.copy_from_slice(bridges); - Some(declared) + /// Add one root, or `None` where it does not fit: past [`MAX_BRIDGES`] + /// or [`MAX_WINDOWS`], or over a bus range that ends before it starts. + pub fn push(&mut self, segment: u16, buses: (u8, u8), windows: &[RootBridgeWindow], osc: Osc) -> Option<()> { + let (osc, granted) = match osc { + Osc::Absent => (OSC_ABSENT, 0), + Osc::Refused => (OSC_REFUSED, 0), + Osc::Granted(granted) => (OSC_ANSWERED, granted), + }; + (buses.0 <= buses.1).then_some(())?; + let first = self.window_count as usize; + let end = first.checked_add(windows.len())?; + let slot = self.bridges.get_mut(self.bridge_count as usize)?; + self.windows.get_mut(first..end)?.copy_from_slice(windows); + *slot = HostBridge { + segment, + first_bus: buses.0, + last_bus: buses.1, + osc, + granted, + first_window: first as u32, + window_count: windows.len() as u32, + }; + self.bridge_count += 1; + self.window_count = end as u32; + Some(()) } /// The bridges declared, or `None` for a count past the array. pub fn bridges(&self) -> Option<&[HostBridge]> { - self.bridges.get(..usize::try_from(self.count).ok()?) + self.bridges.get(..self.bridge_count as usize) + } + + /// `bridge`'s windows, or `None` for a run past the windows declared. + pub fn windows_of(&self, bridge: &HostBridge) -> Option<&[RootBridgeWindow]> { + let first = bridge.first_window as usize; + let end = first.checked_add(bridge.window_count as usize)?; + self.windows.get(..self.window_count as usize)?.get(first..end) } } @@ -482,24 +495,38 @@ mod tests { assert!(!whole.holds(2, u64::MAX)); } - /// A record that fits is carried whole and read back as itself; one past - /// its array is none, and a count or an `_OSC` word past what the ABI - /// names reads back as none. + /// Roots that fit are carried whole, each with its own run of the shared + /// windows; one past either array is none; a count, a run or an `_OSC` + /// word past what the ABI names reads back as none. #[test] fn a_declaration_carries_its_bridges_and_nothing_past_them() { - let w = RootBridgeWindow { base: 0x1000_0000, length: 0x2eff_0000 }; - let root = HostBridge::new(0, (0, 0xff), &[w], Osc::Granted(NATIVE_HOT_PLUG)).expect("one window fits"); - assert_eq!(root.windows(), Some(&[w][..])); - assert_eq!(root.osc(), Some(Osc::Granted(NATIVE_HOT_PLUG))); - assert_eq!(HostBridge::new(0, (0, 0xff), &[w; MAX_BRIDGE_WINDOWS + 1], Osc::Absent), None); - assert_eq!(HostBridge::new(0, (2, 1), &[w], Osc::Absent), None); - assert_eq!(HostBridge { window_count: MAX_BRIDGE_WINDOWS as u32 + 1, ..root }.windows(), None); - assert_eq!(HostBridge { osc: 3, ..root }.osc(), None); - assert_eq!(HostBridge { osc: OSC_ABSENT, granted: 1, ..root }.osc(), None); - - let declared = Apertures::new(&[root]).expect("one bridge fits"); - assert_eq!(declared.bridges(), Some(&[root][..])); - assert_eq!(Apertures::new(&[root; MAX_BRIDGES + 1]), None); - assert_eq!(Apertures { count: MAX_BRIDGES as u64 + 1, ..declared }.bridges(), None); + let w = |base| RootBridgeWindow { base, length: 0x1000 }; + let mut declared = Apertures::default(); + declared.push(0, (0, 0x7f), &[w(0x1000), w(0x3000)], Osc::Granted(NATIVE_HOT_PLUG)).expect("fits"); + declared.push(0, (0x80, 0xff), &[w(0x5000)], Osc::Absent).expect("fits"); + let bridges = declared.bridges().expect("two bridges").to_vec(); + assert_eq!(bridges.len(), 2); + assert_eq!(declared.windows_of(&bridges[0]), Some(&[w(0x1000), w(0x3000)][..])); + assert_eq!(declared.windows_of(&bridges[1]), Some(&[w(0x5000)][..])); + assert_eq!(bridges[0].osc(), Some(Osc::Granted(NATIVE_HOT_PLUG))); + assert_eq!(bridges[1].osc(), Some(Osc::Absent)); + assert_eq!(declared.push(0, (2, 1), &[], Osc::Absent), None); + + // One root may carry every window: the bound is the declaration's. + let mut wide = Apertures::default(); + assert_eq!(wide.push(0, (0, 0xff), &[w(0); MAX_WINDOWS], Osc::Absent), Some(())); + assert_eq!(wide.push(0, (0, 0xff), &[w(0)], Osc::Absent), None); + let mut many = Apertures::default(); + for _ in 0..MAX_BRIDGES { + many.push(0, (0, 0), &[], Osc::Absent).expect("fits"); + } + assert_eq!(many.push(0, (0, 0), &[], Osc::Absent), None); + + let first = bridges[0]; + assert_eq!(declared.windows_of(&HostBridge { window_count: 3, ..first }), Some(&[w(0x1000), w(0x3000), w(0x5000)][..])); + assert_eq!(declared.windows_of(&HostBridge { window_count: 4, ..first }), None); + assert_eq!(HostBridge { osc: 3, ..first }.osc(), None); + assert_eq!(HostBridge { osc: OSC_ABSENT, granted: 1, ..first }.osc(), None); + assert_eq!(Apertures { bridge_count: MAX_BRIDGES as u32 + 1, ..declared }.bridges(), None); } } diff --git a/system/acpiserver/src/apertures.rs b/system/acpiserver/src/apertures.rs index 8b8e2a64387..3bae7585d4c 100644 --- a/system/acpiserver/src/apertures.rs +++ b/system/acpiserver/src/apertures.rs @@ -19,15 +19,24 @@ //! the same. An `_OSC` refused is that root's grant refused, and nothing //! else's. -use toyos_abi::acpi::{HostBridge, Osc, RootBridgeWindow, MAX_BRIDGES, MAX_BRIDGE_WINDOWS}; +use toyos_abi::acpi::{Osc, RootBridgeWindow, MAX_BRIDGES, MAX_WINDOWS}; use toyos_aml::{Kind, Value}; use crate::aml::{kind, Aml}; use crate::host::{Declared, Kernel, Stopping, OWN}; use crate::init::{evaluate, host_bridge, Census, Names, PRESENT}; +/// One PCI root bridge, as it is declared. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Root { + pub segment: u16, + pub buses: (u8, u8), + pub windows: Vec, + pub osc: Osc, +} + /// Every root bridge, in walk order, or why there is no declaration. -pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) -> Result, String> { +pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) -> Result, String> { let devices = names.objects.iter().filter(|named| named.kind == Kind::Device).map(|named| named.path.as_str()); let mut bridges: Vec<&str> = Vec::new(); for device in devices { @@ -38,7 +47,8 @@ pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) if bridges.is_empty() { return Err("the namespace names no PCI host bridge".into()); } - let mut roots = Vec::new(); + let mut roots: Vec = Vec::new(); + let mut room = MAX_WINDOWS; for root in bridges { if !present(aml, names, root, census)? { println!("{OWN}{root} is not present, so it decodes nothing"); @@ -54,15 +64,20 @@ pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) Ok(_) => return Err(said(&path, "a root bridge's _CRS is no buffer")), Err(why) => return Err(said(&path, &format!("a root bridge's _CRS was refused: {}", kind(&why)))), }; - let mut windows = [RootBridgeWindow::default(); MAX_BRIDGE_WINDOWS]; - let found = match toyos_acpi::root_bridge(&crs[..], 0, &mut windows) { + let bytes: Vec = crs.iter().map(|b| format!("{b:02x}")).collect(); + println!("{OWN}{path}: {}", bytes.join(" ")); + let mut windows = [RootBridgeWindow::default(); MAX_WINDOWS]; + let found = match toyos_acpi::root_bridge(&crs[..], 0, &mut windows[..room]) { Ok(found) => found, Err(why) => return Err(said(&path, &format!("a root bridge's _CRS is no list of windows: {why}"))), }; let buses = found.buses.ok_or_else(|| said(&path, "a root bridge's _CRS names no bus range"))?; + room -= found.windows; let osc = osc(aml, names, root); - roots.push(HostBridge::new(segment, buses, &windows[..found.windows], osc).expect("the decoder bounded both")); + roots.push(Root { segment, buses, windows: windows[..found.windows].to_vec(), osc }); } + let counts: Vec = roots.iter().map(|root| root.windows.len().to_string()).collect(); + println!("acpiserver: {} root bridge(s), with {} memory window(s)", roots.len(), counts.join(" + ")); Ok(roots) } @@ -96,7 +111,7 @@ fn segment(aml: &mut Aml<'_, K>, names: &Names, root: &str) -> Result /// Declare what [`read`] answers, or no root where it refused, saying which /// and what the kernel did with it. -pub fn declare(kernel: &K, read: Result, String>) -> Result<(), Stopping> { +pub fn declare(kernel: &K, read: Result, String>) -> Result<(), Stopping> { let roots = match read { Ok(roots) => roots, Err(why) => { @@ -105,7 +120,7 @@ pub fn declare(kernel: &K, read: Result, String>) -> } }; let listed: Vec = - roots.iter().flat_map(|root| root.windows().expect("made by `HostBridge::new`")).map(|w| format!("mem {:#x}..{:#x}", w.base, w.end())).collect(); + roots.iter().flat_map(|root| &root.windows).map(|w| format!("mem {:#x}..{:#x}", w.base, w.end())).collect(); match kernel.declare(&roots)? { Declared::Held => println!("acpiserver: the root bridges' memory declared: {}", listed.join(", ")), Declared::Refused if roots.is_empty() => {} @@ -152,7 +167,7 @@ mod tests { const VIRT_DSDT: &[u8] = include_bytes!("../../../lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin"); /// The roots a DSDT declares, read as the server reads them. - fn declared(dsdt: &[u8]) -> Result, String> { + fn declared(dsdt: &[u8]) -> Result, String> { let kernel = crate::aml::tests::crafted(dsdt, &[]); let (_, kept) = load(&kernel, CRAFTED_RSDP); let mut aml = kept.expect("the DSDT loads"); @@ -160,8 +175,8 @@ mod tests { read(&mut aml, &names, &mut Census::default()) } - fn windows(root: &HostBridge) -> Vec { - root.windows().expect("a record `HostBridge::new` made").to_vec() + fn windows(root: &Root) -> Vec { + root.windows.clone() } /// QEMU's `virt` (the measured case): its one root, `\_SB.PCI0` @@ -172,7 +187,7 @@ mod tests { fn virts_root_declares_its_buses_windows_and_grant() { let roots = declared(VIRT_DSDT).expect("virt's root"); let [root] = roots.as_slice() else { panic!("{roots:x?}") }; - assert_eq!((root.segment, root.first_bus, root.last_bus), (0, 0, 0xff)); + assert_eq!((root.segment, root.buses), (0, (0, 0xff))); assert_eq!( windows(root), [ @@ -180,7 +195,7 @@ mod tests { RootBridgeWindow { base: 0x80_0000_0000, length: 0x80_0000_0000 }, ] ); - assert_eq!(root.osc(), Some(Osc::Granted(VIRT_GRANTED))); + assert_eq!(root.osc, Osc::Granted(VIRT_GRANTED)); } /// q35: one root on buses 0 to 0xff whose `_CRS` names the VGA window, @@ -190,7 +205,7 @@ mod tests { fn q35s_root_declares_its_buses_windows_and_grant() { let roots = declared(Q35_DSDT).expect("q35's root"); let [root] = roots.as_slice() else { panic!("{roots:x?}") }; - assert_eq!((root.segment, root.first_bus, root.last_bus), (0, 0, 0xff)); + assert_eq!((root.segment, root.buses), (0, (0, 0xff))); assert_eq!( windows(root), [ @@ -200,7 +215,7 @@ mod tests { RootBridgeWindow { base: 0xc0_0000_0000, length: 0x8_0000_0000 }, ] ); - assert_eq!(root.osc(), Some(Osc::Granted(Q35_GRANTED))); + assert_eq!(root.osc, Osc::Granted(Q35_GRANTED)); } /// What each QEMU's root `_OSC` grants of [`ROOT_CONTROL`]: `virt` both, @@ -226,7 +241,7 @@ mod tests { for value in (0..=255u8).filter(|&v| v != crs[offset]) { let mut mutated = crs.clone(); mutated[offset] = value; - let mut out = [RootBridgeWindow::default(); MAX_BRIDGE_WINDOWS]; + let mut out = [RootBridgeWindow::default(); MAX_WINDOWS]; match toyos_acpi::root_bridge(&mutated[..], 0, &mut out) { Ok(found) => assert!(out[..found.windows].iter().all(|w| w.length != 0), "byte {offset} as {value:#04x}"), Err(_) => refused += 1, @@ -240,15 +255,42 @@ mod tests { /// A root's `_CRS`: buses 0 to 0xff and one window at 0x80000000. fn crs() -> Vec { + crs_of(&[(0x8000_0000, 0x1000_0000)]) + } + + /// A root's `_CRS`: buses 0 to 0xff and a DWord window at each + /// `(base, length)`. + fn crs_of(windows: &[(u32, u32)]) -> Vec { let bus = [0x88, 0x0D, 0x00, 0x02, 0x0C, 0x00, 0, 0, 0, 0, 0xFF, 0, 0, 0, 0x00, 0x01]; - let mut memory = vec![0x87, 0x17, 0x00, 0x00, 0x0C, 0x03]; - for field in [0u32, 0x8000_0000, 0x8fff_ffff, 0, 0x1000_0000] { - memory.extend_from_slice(&field.to_le_bytes()); + let mut list = bus.to_vec(); + for &(base, length) in windows { + list.extend_from_slice(&[0x87, 0x17, 0x00, 0x00, 0x0C, 0x03]); + for field in [0u32, base, base + (length - 1), 0, length] { + list.extend_from_slice(&field.to_le_bytes()); + } } - let list = cat(&[&bus, &memory, &[0x79, 0x00]]); + list.extend_from_slice(&[0x79, 0x00]); buffer(&int(list.len() as u64), &list) } + /// A root with many windows is declared whole: the bound is the + /// declaration's, not a root's. The legacy windows a laptop's firmware + /// lists 16 KiB each from 0xc0000, a low and a high window, 18 in all, + /// past the per-root bound of 8 that refused the AMD bench's root; and + /// a root past the declaration's own bound is refused by name. + #[test] + fn a_root_with_many_windows_is_declared_and_only_an_absurd_one_refused() { + let legacy = (0..16u32).map(|i| (0xc_0000 + i * 0x4000, 0x4000)); + let many: Vec<(u32, u32)> = legacy.chain([(0xa_0000, 0x2_0000), (0xd000_0000, 0x2000_0000)]).collect(); + let roots = declared(&root(&crs_of(&many), &[])).expect("eighteen windows"); + assert_eq!(roots[0].windows.len(), 18); + let absurd: Vec<(u32, u32)> = (0..MAX_WINDOWS as u32 + 1).map(|i| (0x1000_0000 + i * 0x1000, 0x1000)).collect(); + assert_eq!( + declared(&root(&crs_of(&absurd), &[])), + Err(format!("a root bridge's _CRS is no list of windows: more memory windows or I/O runs than the {MAX_WINDOWS} there is room for")) + ); + } + /// `\_SB.PCI0`, a PNP0A08 whose `_CRS` is `crs` and which carries `rest`. fn root(crs: &[u8], rest: &[u8]) -> Vec { let body = cat(&[&def_name("_HID", &string("PNP0A08")), &def_name("_CRS", crs), rest]); @@ -274,13 +316,23 @@ mod tests { /// refused by name, its root still declared. #[test] fn a_roots_osc_is_negotiated_once_and_its_grant_read() { - let granted = |rest: &[u8]| declared(&root(&crs(), rest)).map(|roots| roots[0].osc()); + let granted = |rest: &[u8]| declared(&root(&crs(), rest)).map(|roots| Some(roots[0].osc)); // Return (Arg3): the query's DWORDs back, then the commit's. assert_eq!(granted(&osc(&[0x6B])), Ok(Some(Osc::Granted(ROOT_CONTROL)))); assert_eq!(granted(&osc(&dwords([1 << 1, 0, ROOT_CONTROL]))), Ok(Some(Osc::Granted(0)))); assert_eq!(granted(&osc(&dwords([0, 0, 0]))), Ok(Some(Osc::Granted(0)))); // Control bits granted that were never asked for are dropped. assert_eq!(granted(&osc(&dwords([0, 0, u32::MAX]))), Ok(Some(Osc::Granted(ROOT_CONTROL)))); + // QEMU q35's own shape: the Control DWORD masked by the firmware, + // 0x1F with `acpi-pci-hotplug-with-bridge-support=off`, 0x1E with + // its default, which keeps native hot-plug. The grant is what the + // commit answered. + let masked = |mask: u8| { + let ctrl = cat(&[&[0x8A, 0x6B, 0x0A, 0x08], b"CTRL", &[0x7B], b"CTRL", &[0x0A, mask], b"CTRL"]); + method("_OSC", 4, &cat(&[&ctrl, &ret(&[0x6B])])) + }; + assert_eq!(granted(&masked(0x1F)), Ok(Some(Osc::Granted(ROOT_CONTROL)))); + assert_eq!(granted(&masked(0x1E)), Ok(Some(Osc::Granted(toyos_abi::acpi::CAPABILITY_STRUCTURE)))); assert_eq!(granted(&[]), Ok(Some(Osc::Absent))); assert_eq!(granted(&osc(&buffer(&int(4), &[0; 4]))), Ok(Some(Osc::Refused))); assert_eq!(granted(&osc(&int(5))), Ok(Some(Osc::Refused))); diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 200067db8ce..04de60c5710 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -92,7 +92,7 @@ use std::collections::BTreeMap; use std::fmt; use std::time::{Duration, Instant}; -use toyos_abi::acpi::{pci_address, Access, HostBridge, Refused, Space, Width, UNLISTED}; +use toyos_abi::acpi::{pci_address, Access, Refused, Space, Width, UNLISTED}; use toyos_aml::{Address, Denied, Host, RuntimeLoad, Transfer}; use crate::devices::Ec; @@ -151,7 +151,7 @@ pub trait Kernel { /// no fixed hardware, and a power-off that writes no sleep type. fn hardware_reduced(&self) -> bool; /// Declare the PCI root bridges, once ([`crate::apertures`]). - fn declare(&self, roots: &[HostBridge]) -> Result; + fn declare(&self, roots: &[crate::apertures::Root]) -> Result; /// Waits until `until`, serving the SCI as `sci::await_sleep` does for /// the fixed power button where the claim serves it and the controller's /// GPE `ec_gpe`: what latched and was taken. @@ -783,7 +783,7 @@ pub mod tests { /// The machine is hardware-reduced. pub reduced: bool, /// The roots it was declared, once. - pub declared: RefCell>>, + pub declared: RefCell>>, /// What each Sleep's wait takes, in turn; nothing once it runs out. /// Each served set a Sleep was asked to serve. pub sleeps: RefCell>, @@ -914,7 +914,7 @@ pub mod tests { self.reduced } - fn declare(&self, roots: &[HostBridge]) -> Result { + fn declare(&self, roots: &[crate::apertures::Root]) -> Result { self.stopping()?; let mut declared = self.declared.borrow_mut(); if declared.is_some() { diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 00aa571f0c0..635763e2f52 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -95,7 +95,7 @@ use toyos::ioport::{in16, in8, out16, out8}; use toyos::poller::{Poller, READABLE}; use toyos::power::{self, Stop}; use toyos::AcpiDev; -use toyos_abi::acpi::{Access, AcpiInfo, Apertures, Block, HostBridge, FIXED_POWER_BUTTON, HARDWARE_REDUCED}; +use toyos_abi::acpi::{Access, AcpiInfo, Apertures, Block, FIXED_POWER_BUTTON, HARDWARE_REDUCED}; use toyos_abi::syscall::{DeviceType, SyscallError}; use acpiserver_api::{CONTROLLER_NONE, CONTROLLER_SERVED}; @@ -275,8 +275,11 @@ impl Kernel for Claim<'_> { self.info.flags & HARDWARE_REDUCED != 0 } - fn declare(&self, roots: &[HostBridge]) -> Result { - let declared = Apertures::new(roots).expect("acpiserver: the roots were read into a declaration's room"); + fn declare(&self, roots: &[apertures::Root]) -> Result { + let mut declared = Apertures::default(); + for root in roots { + declared.push(root.segment, root.buses, &root.windows, root.osc).expect("acpiserver: the roots were read into a declaration's room"); + } match self.dev.declare(&declared) { Err(SyscallError::InvalidArgument) => Ok(Declared::Refused), Err(SyscallError::AlreadyExists) => Ok(Declared::Late), diff --git a/system/acpiserver/src/osc.rs b/system/acpiserver/src/osc.rs index 02726c0ba11..1ac3f7de1f2 100644 --- a/system/acpiserver/src/osc.rs +++ b/system/acpiserver/src/osc.rs @@ -80,8 +80,8 @@ const UNRECOGNIZED_UUID: u32 = 1 << 2; const UNRECOGNIZED_REVISION: u32 = 1 << 3; const MASKED: u32 = 1 << 4; -/// The revision of both UUIDs' capabilities buffer (§6.2.11.2; 302223-007 -/// §2.2). +/// The revision of every UUID's capabilities buffer here (§6.2.11.2; +/// 302223-007 §2.2; PCI Firmware Specification 3.3 §4.5.1). const REVISION: u64 = 1; /// A status DWORD's bits by name. diff --git a/tests/common/claims.rs b/tests/common/claims.rs index 2b8d52bb7a1..98b34c2efef 100644 --- a/tests/common/claims.rs +++ b/tests/common/claims.rs @@ -125,6 +125,9 @@ pub fn reuses_its_entry(log: &Serial) -> Result<(), String> { Ok(()) } +/// The kernel's record of a declaration it refused. +pub const REFUSED_DECLARATION: &str = "pcidev: the root bridges' memory is refused as declared"; + /// What the `acpi` claim's holder declared the root bridges decode. pub const DECLARED: &str = "pcidev: the acpi claim declared root bridge memory: "; diff --git a/tests/common/metal.rs b/tests/common/metal.rs index 7daf0664dca..f007ea7c3b8 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -274,6 +274,16 @@ impl Readback { bootlog::one_clock(&self.loader, &self.log).map_err(|why| format!("{}: {why}", self.label)) } + /// No declaration of the PCI root bridges this boot made was refused: a + /// refused one leaves every claim refused, which a row that claims + /// nothing would never see. + pub fn declaration_not_refused(&self) -> Result<(), String> { + match self.kernel.lines().find(|line| line.contains(super::claims::REFUSED_DECLARATION)) { + Some(line) => Err(format!("{}: the kernel refused the root bridges' declaration: {}", self.label, line.trim())), + None => Ok(()), + } + } + /// When `Boot: complete` was written, on the same clock. pub fn complete_record_ms(&self) -> Option { self.kernel.lines().find(|line| line.contains(bootlog::COMPLETE)).and_then(bootlog::record_millis) @@ -1156,6 +1166,7 @@ pub fn judge_readbacks( findings.extend(back.deadline_on_time().err()); findings.extend(back.lockup_on_time().err()); findings.extend(back.one_clock().err()); + findings.extend(back.declaration_not_refused().err()); for why in &findings { eprintln!(" FAIL {why}"); } diff --git a/tests/toyos.rs b/tests/toyos.rs index 79687303566..c6e7b37593a 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -4292,6 +4292,8 @@ fn amdvi_translates(kernel: &serial::Serial) -> Result<(), String> { } } kernel.must_say("usb-storage: 1 device(s)")?; + // The bench's root bridges declared and held, which a claim on it needs. + eprintln!(" [amdvi] {}", kernel.must_say(claims::DECLARED)?.trim()); for unit in units { eprintln!(" [amdvi] {}", unit.trim()); } From 71bce126ff4cc12ac19318c4d98ac556f5128cdc Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 15 Oct 2026 06:53:17 +0200 Subject: [PATCH 6/9] Review round 1: a malformed record refused by name, the root _OSC queried only, the no-acpi-row loss recorded - A declaration whose records the ABI does not name (a count or run past its array, an unnamed _OSC word, a reversed bus range) or whose roots share a bus on one segment is refused through Held like any other refusal: logged, posted, and the answer (toyos_pci::aperture::records). - The root _OSC is a query alone and its offer is recorded; the hot-plug service that acts on native hot-plug control (#934) commits it, so the osc module's condition holds. - issues/an-x86-machine-with-no-acpi-row-hands-over-no-pci-claim.md. - window.rs: Addresses::map's contract stated per arm. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- ...ith-no-acpi-row-hands-over-no-pci-claim.md | 30 ++++++ kernel/pci/src/aperture.rs | 75 ++++++++++++++- kernel/src/iommu/window.rs | 3 +- kernel/src/pcidev/mod.rs | 70 +++++++------- kernel/src/syscall/device.rs | 4 +- sdk/abi/src/acpi.rs | 14 +-- system/acpiserver/src/apertures.rs | 93 ++++++++++++------- system/acpiserver/src/osc.rs | 24 ++--- tests/toyos.rs | 10 +- 9 files changed, 223 insertions(+), 100 deletions(-) create mode 100644 issues/an-x86-machine-with-no-acpi-row-hands-over-no-pci-claim.md diff --git a/issues/an-x86-machine-with-no-acpi-row-hands-over-no-pci-claim.md b/issues/an-x86-machine-with-no-acpi-row-hands-over-no-pci-claim.md new file mode 100644 index 00000000000..14e6bc74fa8 --- /dev/null +++ b/issues/an-x86-machine-with-no-acpi-row-hands-over-no-pci-claim.md @@ -0,0 +1,30 @@ +--- +status: open +kind: defect +opened: 2026-10-15 +--- + +# An x86 machine with no ACPI row hands over no PCI claim + +Every PCI claim waits for the holder of the `acpi` claim to declare the root +bridges (`kernel/src/pcidev`'s `declare`, made by `/system/bin/acpiserver`), +and on x86-64 that claim exists only over a row `acpi_mode::init` +(`kernel/src/arch/x86_64/acpi_mode.rs`) fills. It fills none where the FADT is +unusable, its fixed hardware is refused (`HW_REDUCED_ACPI` among the +refusals), no PM1a control block is declared, its `SMI_CMD` was not declared, +no I/O APIC carries the SCI, or the SCI's line is another row's. On such a +machine acpiserver gets no claim and declares nothing, so the first PCI claim +waits its 30 s deadline and every claim of the boot is refused, the block +service's NVMe among them. Before #935 the loader's window list handed those +claims over. + +Measured: none of the machines in reach is such a machine. q35, the +ThinkPad T14 and the AMD bench each fill the row (their `acpi: the ACPI row:` +lines); a hardware-reduced x86 guest (Cloud Hypervisor, QEMU's `microvm`) +would not, as `issues/a-hardware-reduced-x86-machine-has-no-power-off-and-no-power-button.md` +records for its power-off. + +**Owner**: the PCI apertures track (#916, #935). + +**Exit**: such a machine declares its root bridges through a path that needs +no `acpi` claim row, or the gap is measured absent on every supported machine. diff --git a/kernel/pci/src/aperture.rs b/kernel/pci/src/aperture.rs index eedf9fded2e..3ec229d67c0 100644 --- a/kernel/pci/src/aperture.rs +++ b/kernel/pci/src/aperture.rs @@ -5,7 +5,7 @@ //! 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::acpi::RootBridgeWindow; +use toyos_abi::acpi::{Apertures, RootBridgeWindow}; /// Where an extent stands against the memory windows the root bridges decode. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -32,6 +32,12 @@ pub enum Refused { /// is what lets a device address equal to a RAM address go unrouted /// peer-to-peer, so a declaration saying otherwise is wrong about one. Memory(RootBridgeWindow), + /// A root's record is none the ABI names: a count or a run of windows + /// past its array, an `_OSC` word it does not name, or a bus range that + /// ends before it starts. + Record, + /// Two roots of one segment name one bus. + Buses { segment: u16, first: u8, last: u8 }, } impl core::fmt::Display for Refused { @@ -48,6 +54,10 @@ impl core::fmt::Display for Refused { b.end() ), Self::Memory(w) => write!(f, "its window {:#x}..{:#x} lies over memory", w.base, w.end()), + Self::Record => write!(f, "a root's record is none the ABI names"), + Self::Buses { segment, first, last } => { + write!(f, "two of its roots on segment {segment:#06x} share buses {first:#04x}..={last:#04x}") + } } } } @@ -144,6 +154,16 @@ impl> Held { answer } + /// Refuse a declaration whose records are none ([`records`]), from + /// [`Self::Unknown`] alone; the refusal is the answer, as `declare`'s is. + pub fn refuse(&mut self, why: Refused) -> Result<(), Declined> { + if !matches!(self, Self::Unknown) { + return Err(Declined::Late); + } + *self = Self::Withheld(Withheld::Refused(why)); + Err(Declined::Refused(why)) + } + /// Nothing will be declared, for `why`: an answer where there was none, /// and nothing changed where there was one. Whether it was the answer. pub fn withhold(&mut self, why: Withheld) -> bool { @@ -164,6 +184,26 @@ impl> Held { } } +/// Whether every root `declared` carries is one the ABI names, and no two +/// roots of one segment share a bus: what a declaration's windows are read +/// out of only once this holds. +pub fn records(declared: &Apertures) -> Result<(), Refused> { + let bridges = declared.bridges().ok_or(Refused::Record)?; + for (at, bridge) in bridges.iter().enumerate() { + let named = bridge.first_bus <= bridge.last_bus && bridge.osc().is_some() && declared.windows_of(bridge).is_some(); + if !named { + return Err(Refused::Record); + } + let shares = |other: &&toyos_abi::acpi::HostBridge| { + other.segment == bridge.segment && other.first_bus <= bridge.last_bus && bridge.first_bus <= other.last_bus + }; + if bridges[..at].iter().any(|other| shares(&other)) { + return Err(Refused::Buses { segment: bridge.segment, first: bridge.first_bus, last: bridge.last_bus }); + } + } + Ok(()) +} + /// Where `base .. end` stands against `windows`. /// /// **An empty window list decodes nothing.** A machine whose firmware would not @@ -293,4 +333,37 @@ mod tests { assert_eq!(ended.answer().map(|a| a.err()), Some(Some(why))); } } + + /// Each malformed record is refused by name before a window is read: a + /// count past the array, a run of windows past those declared, an + /// `_OSC` word the ABI does not name, a reversed bus range, and two roots + /// of one segment sharing a bus; roots on two segments may share bus + /// numbers. A refused record is the answer, and later declarations late. + #[test] + fn a_malformed_record_is_refused_by_name_and_is_the_answer() { + use toyos_abi::acpi::{HostBridge, Osc}; + let w = RootBridgeWindow { base: 0x8000_0000, length: 0x1000_0000 }; + let mut good = Apertures::default(); + good.push(0, (0, 0x7f), &[w], Osc::Absent).expect("fits"); + assert_eq!(records(&good), Ok(())); + let first = good.bridges[0]; + let with = |bridge: HostBridge| Apertures { bridges: [bridge; toyos_abi::acpi::MAX_BRIDGES], ..good }; + assert_eq!(records(&Apertures { bridge_count: u32::MAX, ..good }), Err(Refused::Record)); + assert_eq!(records(&with(HostBridge { window_count: 2, ..first })), Err(Refused::Record)); + assert_eq!(records(&with(HostBridge { osc: 7, ..first })), Err(Refused::Record)); + assert_eq!(records(&with(HostBridge { first_bus: 0x80, last_bus: 0x7f, ..first })), Err(Refused::Record)); + + let mut shared = good; + shared.push(0, (0x40, 0xff), &[], Osc::Absent).expect("fits"); + assert_eq!(records(&shared), Err(Refused::Buses { segment: 0, first: 0x40, last: 0xff })); + let mut apart = good; + apart.push(1, (0, 0x7f), &[], Osc::Absent).expect("fits"); + assert_eq!(records(&apart), Ok(())); + + let mut held = Held::<[RootBridgeWindow; 1]>::Unknown; + assert_eq!(held.refuse(Refused::Record), Err(Declined::Refused(Refused::Record))); + assert_eq!(held.answer().map(|a| a.err()), Some(Some(Withheld::Refused(Refused::Record)))); + assert_eq!(held.declare([w], &[]), Err(Declined::Late)); + assert_eq!(held.refuse(Refused::Record), Err(Declined::Late)); + } } diff --git a/kernel/src/iommu/window.rs b/kernel/src/iommu/window.rs index f2970a51b47..7c49f8582cb 100644 --- a/kernel/src/iommu/window.rs +++ b/kernel/src/iommu/window.rs @@ -84,7 +84,8 @@ impl Addresses { Ok(Self::Identity { translatable }) } - /// Where `bytes` at `phys` goes in this domain, never handed out before. + /// Where `bytes` at `phys` goes in this domain: `phys` itself in a kernel + /// driver's, and in a claimed function's room never handed out before. pub fn map(&mut self, phys: u64, bytes: u64) -> Result { match self { Self::Identity { translatable } => match phys.checked_add(bytes.next_multiple_of(PAGE_2M)) { diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 9c67558c87c..d077cb37e64 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -412,30 +412,12 @@ struct Machine { } /// One PCI root bridge as the `acpi` claim's holder declared it. -pub struct RootBridge { +struct RootBridge { segment: u16, - first_bus: u8, - windows: Vec, + buses: (u8, u8), osc: toyos_abi::acpi::Osc, } -impl RootBridge { - /// Every bridge `declared` carries, or `None` where a record is none the - /// ABI names: a count or a run of windows past its array, or an `_OSC` - /// word it does not. - pub fn decode(declared: &toyos_abi::acpi::Apertures) -> Option> { - declared - .bridges()? - .iter() - .map(|bridge| { - (bridge.first_bus <= bridge.last_bus).then_some(())?; - let windows = declared.windows_of(bridge)?.to_vec(); - Some(Self { segment: bridge.segment, first_bus: bridge.first_bus, windows, osc: bridge.osc()? }) - }) - .collect() - } -} - /// Posted once, when [`Machine::apertures`] leaves `Unknown`. static DECLARATION: Watch = Watch::new(); @@ -677,30 +659,44 @@ pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry]) { machine.high = high; } -/// Hold `roots` and their windows, together, as the root bridges' memory: -/// the `acpi` claim holder's declaration, from `Unknown` alone. A window over -/// memory firmware's map gives the kernel, in any root, is refused with the -/// rest ([`aperture::check`]). Each root's `_OSC` grant is said. -pub fn declare(roots: Vec) -> Result<(), aperture::Declined> { +/// Hold the roots `declared` carries and their windows, together, as the +/// root bridges' memory: the `acpi` claim holder's declaration, from +/// `Unknown` alone. A record the ABI does not name ([`aperture::records`]), +/// and a window over memory firmware's map gives the kernel in any root +/// ([`aperture::check`]), refuse the whole of it, and the refusal is the +/// answer. Each root's `_OSC` offer is said. +pub fn declare(declared: &toyos_abi::acpi::Apertures) -> Result<(), aperture::Declined> { let memory: Vec<(u64, u64)> = crate::mm::firmware_map() .iter() .filter(|entry| toyos_bootmap::is_read_as_memory(entry.uefi_type)) .map(|entry| (entry.start, entry.end)) .collect(); - let windows: Vec = roots.iter().flat_map(|root| root.windows.iter().copied()).collect(); let mut machine = MACHINE.lock(); - let declared = machine.apertures.declare(windows, &memory); - if declared != Err(aperture::Declined::Late) { - machine.roots = roots; - for root in &machine.roots { - let granted = match root.osc { - toyos_abi::acpi::Osc::Absent => "no _OSC", - toyos_abi::acpi::Osc::Granted(granted) if granted & toyos_abi::acpi::NATIVE_HOT_PLUG != 0 => "native hot-plug granted", - toyos_abi::acpi::Osc::Granted(_) | toyos_abi::acpi::Osc::Refused => "not granted", - }; - log!("pci: root {:04x}:{:02x} _OSC: {granted}", root.segment, root.first_bus); + let answer = match aperture::records(declared) { + Err(why) => machine.apertures.refuse(why), + Ok(()) => { + let bridges = declared.bridges().expect("`records` read every bridge"); + let windows: Vec = + bridges.iter().flat_map(|bridge| declared.windows_of(bridge).expect("`records` read every run")).copied().collect(); + let answer = machine.apertures.declare(windows, &memory); + if answer != Err(aperture::Declined::Late) { + machine.roots = bridges + .iter() + .map(|b| RootBridge { segment: b.segment, buses: (b.first_bus, b.last_bus), osc: b.osc().expect("`records` read every word") }) + .collect(); + for root in &machine.roots { + let offered = match root.osc { + toyos_abi::acpi::Osc::Absent => "no _OSC", + toyos_abi::acpi::Osc::Offered(offered) if offered & toyos_abi::acpi::NATIVE_HOT_PLUG != 0 => "native hot-plug offered", + toyos_abi::acpi::Osc::Offered(_) | toyos_abi::acpi::Osc::Refused => "not offered", + }; + log!("pci: root {:04x}:{:02x} _OSC: {offered}, buses {:#04x}..={:#04x}", root.segment, root.buses.0, root.buses.0, root.buses.1); + } + } + answer } - } + }; + let declared = answer; match (&declared, machine.apertures.answer()) { (Ok(()), Some(Ok(declaration))) => account_for(declaration.windows(), &machine.decoded), (Err(aperture::Declined::Refused(why)), _) => { diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 56d382102c0..ef035d7e7c7 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -179,8 +179,8 @@ pub(super) fn sys_acpi(ctx: &SyscallContext, handle: RawHandle, op: u64, at: u64 (ops::LOCK_RELEASE, _) => crate::arch::acpi_mode::lock_release(row).map(|()| 0), (ops::S5, _) => crate::arch::acpi_mode::s5(row, at).map(|()| 0), (ops::DECLARE, _) => { - let bridges = declared.as_ref().and_then(crate::pcidev::RootBridge::decode).ok_or(SyscallError::InvalidArgument)?; - match crate::pcidev::declare(bridges) { + let declared = declared.as_ref().ok_or(SyscallError::InvalidArgument)?; + match crate::pcidev::declare(declared) { Ok(()) => Ok(0), Err(toyos_pci::aperture::Declined::Refused(_)) => Err(SyscallError::InvalidArgument), Err(toyos_pci::aperture::Declined::Late) => Err(SyscallError::AlreadyExists), diff --git a/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index 18f2008c13f..88a41005b36 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -373,8 +373,10 @@ pub enum Osc { Absent, /// Its evaluation, or its answer, was refused: nothing is granted. Refused, - /// It was negotiated, and granted these Control bits. - Granted(u32), + /// Its query offered these Control bits: what a commit would be + /// granted. Stage B makes no commit; the hot-plug service that acts on + /// the control makes it. + Offered(u32), } /// [`HostBridge::osc`]'s words. @@ -404,7 +406,7 @@ impl HostBridge { match (self.osc, self.granted) { (OSC_ABSENT, 0) => Some(Osc::Absent), (OSC_REFUSED, 0) => Some(Osc::Refused), - (OSC_ANSWERED, granted) => Some(Osc::Granted(granted)), + (OSC_ANSWERED, granted) => Some(Osc::Offered(granted)), _ => None, } } @@ -436,7 +438,7 @@ impl Apertures { let (osc, granted) = match osc { Osc::Absent => (OSC_ABSENT, 0), Osc::Refused => (OSC_REFUSED, 0), - Osc::Granted(granted) => (OSC_ANSWERED, granted), + Osc::Offered(granted) => (OSC_ANSWERED, granted), }; (buses.0 <= buses.1).then_some(())?; let first = self.window_count as usize; @@ -502,13 +504,13 @@ mod tests { fn a_declaration_carries_its_bridges_and_nothing_past_them() { let w = |base| RootBridgeWindow { base, length: 0x1000 }; let mut declared = Apertures::default(); - declared.push(0, (0, 0x7f), &[w(0x1000), w(0x3000)], Osc::Granted(NATIVE_HOT_PLUG)).expect("fits"); + declared.push(0, (0, 0x7f), &[w(0x1000), w(0x3000)], Osc::Offered(NATIVE_HOT_PLUG)).expect("fits"); declared.push(0, (0x80, 0xff), &[w(0x5000)], Osc::Absent).expect("fits"); let bridges = declared.bridges().expect("two bridges").to_vec(); assert_eq!(bridges.len(), 2); assert_eq!(declared.windows_of(&bridges[0]), Some(&[w(0x1000), w(0x3000)][..])); assert_eq!(declared.windows_of(&bridges[1]), Some(&[w(0x5000)][..])); - assert_eq!(bridges[0].osc(), Some(Osc::Granted(NATIVE_HOT_PLUG))); + assert_eq!(bridges[0].osc(), Some(Osc::Offered(NATIVE_HOT_PLUG))); assert_eq!(bridges[1].osc(), Some(Osc::Absent)); assert_eq!(declared.push(0, (2, 1), &[], Osc::Absent), None); diff --git a/system/acpiserver/src/apertures.rs b/system/acpiserver/src/apertures.rs index 3bae7585d4c..c92d1d496ce 100644 --- a/system/acpiserver/src/apertures.rs +++ b/system/acpiserver/src/apertures.rs @@ -1,6 +1,6 @@ //! The PCI root bridges, declared to the kernel once ([`Kernel::declare`]): //! each one's segment, buses and memory windows, and what its `_OSC` -//! granted. Every PCI claim on the machine waits for the declaration, so it +//! offers. Every PCI claim on the machine waits for the declaration, so it //! is read as soon as the namespace is initialised (ACPI 6.5 §6.5.1). //! //! **A root is a PCI host bridge by its `_HID` or a `_CID`** (PNP0A03 or @@ -8,7 +8,7 @@ //! it is not present decodes nothing and is not read. Its segment is its //! `_SEG` (§6.5.6), 0 without one; its buses and windows are what //! `toyos_acpi::root_bridge` reads of its `_CRS` (§6.2.2); its `_OSC` is -//! negotiated once, query then commit (`crate::osc::negotiate_root`). +//! queried, and what it offers recorded, with no commit (`crate::osc::query_root`). //! //! **The declaration is every root together, or none at all**: a root whose //! `_STA`, `_SEG` or `_CRS` is refused, a list the decoder refuses or that @@ -16,7 +16,7 @@ //! a namespace with no root are each said by name and declared as no root, //! which the kernel refuses — and with it every PCI claim, rather than one //! placed by a list with a hole in it. A machine with no namespace declares -//! the same. An `_OSC` refused is that root's grant refused, and nothing +//! the same. An `_OSC` refused is that root's offer refused, and nothing //! else's. use toyos_abi::acpi::{Osc, RootBridgeWindow, MAX_BRIDGES, MAX_WINDOWS}; @@ -81,14 +81,14 @@ pub fn read(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) Ok(roots) } -/// What `root`'s `_OSC` granted, said under [`OWN`] where it was refused. +/// What `root`'s `_OSC` offers, said under [`OWN`] where it was refused. fn osc(aml: &mut Aml<'_, K>, names: &Names, root: &str) -> Osc { let path = format!("{root}._OSC"); if !names.paths.contains(&path) { return Osc::Absent; } - match crate::osc::negotiate_root(aml, &path) { - Ok(granted) => Osc::Granted(granted), + match crate::osc::query_root(aml, &path) { + Ok(granted) => Osc::Offered(granted), Err(why) => { println!("{OWN}{path} was refused: {why}"); Osc::Refused @@ -195,7 +195,7 @@ mod tests { RootBridgeWindow { base: 0x80_0000_0000, length: 0x80_0000_0000 }, ] ); - assert_eq!(root.osc, Osc::Granted(VIRT_GRANTED)); + assert_eq!(root.osc, Osc::Offered(VIRT_OFFERED)); } /// q35: one root on buses 0 to 0xff whose `_CRS` names the VGA window, @@ -215,14 +215,14 @@ mod tests { RootBridgeWindow { base: 0xc0_0000_0000, length: 0x8_0000_0000 }, ] ); - assert_eq!(root.osc, Osc::Granted(Q35_GRANTED)); + assert_eq!(root.osc, Osc::Offered(Q35_OFFERED)); } - /// What each QEMU's root `_OSC` grants of [`ROOT_CONTROL`]: `virt` both, - /// q35 the capability structure and not native hot-plug, which its + /// What each QEMU's root `_OSC` query offers of [`ROOT_CONTROL`]: `virt` + /// both, q35 the capability structure and not native hot-plug, which its /// firmware keeps for its own ACPI hot-plug. - const VIRT_GRANTED: u32 = ROOT_CONTROL; - const Q35_GRANTED: u32 = toyos_abi::acpi::CAPABILITY_STRUCTURE; + const VIRT_OFFERED: u32 = ROOT_CONTROL; + const Q35_OFFERED: u32 = toyos_abi::acpi::CAPABILITY_STRUCTURE; /// No byte of either `_CRS` as evaluated panics the decoder or runs it /// away: each takes each of its 255 other values in turn, and both arms, @@ -297,8 +297,7 @@ mod tests { sealed(b"DSDT", &crate::common::scope("\\_SB", &device("PCI0", &body))) } - /// A root's `_OSC` returning the answer `answer` to both the query and - /// the commit. + /// A root's `_OSC` returning `answer`. fn osc(answer: &[u8]) -> Vec { method("_OSC", 4, &ret(answer)) } @@ -309,33 +308,59 @@ mod tests { buffer(&int(12), &bytes) } - /// What a root's `_OSC` grants, read off a namespace of that one root: - /// a firmware that echoes the request grants it; one that answers - /// failure, or masks every Control bit, grants nothing; a root with no - /// `_OSC` says so; and an answer that is no buffer of three DWORDs is - /// refused by name, its root still declared. + /// What a root's `_OSC` offers, read off a namespace of that one root: a + /// firmware that echoes the request offers it; one that answers failure, + /// or masks every Control bit, offers nothing; Control bits offered that + /// were never asked for are dropped; a root with no `_OSC` says so; and + /// an answer that is no buffer of three DWORDs is refused by name, its + /// root still declared. #[test] - fn a_roots_osc_is_negotiated_once_and_its_grant_read() { - let granted = |rest: &[u8]| declared(&root(&crs(), rest)).map(|roots| Some(roots[0].osc)); - // Return (Arg3): the query's DWORDs back, then the commit's. - assert_eq!(granted(&osc(&[0x6B])), Ok(Some(Osc::Granted(ROOT_CONTROL)))); - assert_eq!(granted(&osc(&dwords([1 << 1, 0, ROOT_CONTROL]))), Ok(Some(Osc::Granted(0)))); - assert_eq!(granted(&osc(&dwords([0, 0, 0]))), Ok(Some(Osc::Granted(0)))); - // Control bits granted that were never asked for are dropped. - assert_eq!(granted(&osc(&dwords([0, 0, u32::MAX]))), Ok(Some(Osc::Granted(ROOT_CONTROL)))); + fn a_roots_osc_is_queried_and_its_offer_read() { + let offered = |rest: &[u8]| declared(&root(&crs(), rest)).map(|roots| roots[0].osc); + // Return (Arg3): the query's DWORDs back. + assert_eq!(offered(&osc(&[0x6B])), Ok(Osc::Offered(ROOT_CONTROL))); + assert_eq!(offered(&osc(&dwords([1 << 1, 0, ROOT_CONTROL]))), Ok(Osc::Offered(0))); + assert_eq!(offered(&osc(&dwords([1 << 3, 0, ROOT_CONTROL]))), Ok(Osc::Offered(0))); + assert_eq!(offered(&osc(&dwords([0, 0, 0]))), Ok(Osc::Offered(0))); + assert_eq!(offered(&osc(&dwords([0, 0, u32::MAX]))), Ok(Osc::Offered(ROOT_CONTROL))); // QEMU q35's own shape: the Control DWORD masked by the firmware, // 0x1F with `acpi-pci-hotplug-with-bridge-support=off`, 0x1E with - // its default, which keeps native hot-plug. The grant is what the - // commit answered. + // its default, which keeps native hot-plug. let masked = |mask: u8| { let ctrl = cat(&[&[0x8A, 0x6B, 0x0A, 0x08], b"CTRL", &[0x7B], b"CTRL", &[0x0A, mask], b"CTRL"]); method("_OSC", 4, &cat(&[&ctrl, &ret(&[0x6B])])) }; - assert_eq!(granted(&masked(0x1F)), Ok(Some(Osc::Granted(ROOT_CONTROL)))); - assert_eq!(granted(&masked(0x1E)), Ok(Some(Osc::Granted(toyos_abi::acpi::CAPABILITY_STRUCTURE)))); - assert_eq!(granted(&[]), Ok(Some(Osc::Absent))); - assert_eq!(granted(&osc(&buffer(&int(4), &[0; 4]))), Ok(Some(Osc::Refused))); - assert_eq!(granted(&osc(&int(5))), Ok(Some(Osc::Refused))); + assert_eq!(offered(&masked(0x1F)), Ok(Osc::Offered(ROOT_CONTROL))); + assert_eq!(offered(&masked(0x1E)), Ok(Osc::Offered(toyos_abi::acpi::CAPABILITY_STRUCTURE))); + assert_eq!(offered(&[]), Ok(Osc::Absent)); + assert_eq!(offered(&osc(&buffer(&int(4), &[0; 4]))), Ok(Osc::Refused)); + assert_eq!(offered(&osc(&int(5))), Ok(Osc::Refused)); + } + + /// The `_OSC` is evaluated once, with the Query flag set: nothing is + /// committed, so the firmware keeps what it offered. The firmware here + /// counts its calls and keeps the first DWORD it was passed. + #[test] + fn a_roots_osc_is_never_committed() { + let body = cat(&[ + &[0x75], + b"CNT_", + &[0x8A, 0x6B, 0x00], + b"STS0", + &[0x70], + b"STS0", + b"QFLG", + &ret(&[0x6B]), + ]); + let rest = cat(&[&def_name("CNT_", &int(0)), &def_name("QFLG", &int(0xFF)), &method("_OSC", 4, &body)]); + let kernel = crate::aml::tests::crafted(&root(&crs(), &rest), &[]); + let (_, kept) = load(&kernel, CRAFTED_RSDP); + let mut aml = kept.expect("the DSDT loads"); + let names = Names::read(&aml.interpreter).expect("its namespace walks"); + assert_eq!(read(&mut aml, &names, &mut Census::default()).map(|roots| roots[0].osc), Ok(Osc::Offered(ROOT_CONTROL))); + let integer = |aml: &mut Aml<'_, Scripted>, path| aml.interpreter.evaluate(&mut aml.host, path, &[]); + assert!(matches!(integer(&mut aml, "\\_SB_.PCI0.CNT_"), Ok(Value::Integer(1)))); + assert!(matches!(integer(&mut aml, "\\_SB_.PCI0.QFLG"), Ok(Value::Integer(1)))); } /// A root that is not a list of windows, one whose `_CRS` is refused or diff --git a/system/acpiserver/src/osc.rs b/system/acpiserver/src/osc.rs index 1ac3f7de1f2..f9c954665a2 100644 --- a/system/acpiserver/src/osc.rs +++ b/system/acpiserver/src/osc.rs @@ -2,7 +2,8 @@ //! under the platform-wide UUID (§6.2.11.2), then, on an Intel processor, //! each processor's `_OSC` under Intel's, each a query with the Query flag //! and then a commit of what the query granted (§6.2.11.1.1, §6.2.11.1.3); -//! and each PCI root bridge's under the PCI host bridge UUID ([`negotiate_root`], +//! and each PCI root bridge's under the PCI host bridge UUID, a query alone, +//! since nothing yet does what its Control bits promise ([`query_root`], //! PCI Firmware Specification 3.3 §4.5), by `crate::apertures`. //! //! **What ToyOS claims is one declaration a UUID** ([`PLATFORM`], @@ -156,25 +157,20 @@ const ROOT_SUPPORT: u32 = 1 << 0 | 1 << 4; /// hot-plug, and the PCI Express capability structure it needs with it. pub const ROOT_CONTROL: u32 = toyos_abi::acpi::NATIVE_HOT_PLUG | toyos_abi::acpi::CAPABILITY_STRUCTURE; -/// A root's `_OSC` at `path` negotiated (§4.5.1): the query, and the commit -/// of the Control bits it offered of [`ROOT_CONTROL`]; what is granted is -/// what the commit answered of those, and nothing where either answered -/// failure or an unrecognised UUID. An answer that is no buffer of the three -/// DWORDs passed refuses the negotiation by name. -pub fn negotiate_root(aml: &mut Aml<'_, K>, path: &str) -> Result { +/// What a root's `_OSC` at `path` offers of [`ROOT_CONTROL`] (§4.5.1): its +/// query alone, and nothing where it answered failure, an unrecognised UUID +/// or revision. **No commit is made**: committing native hot-plug control +/// hands the firmware's hot-plug to ToyOS, and the service that acts on it +/// commits. An answer that is no buffer of the three DWORDs passed refuses +/// the query by name. +pub fn query_root(aml: &mut Aml<'_, K>, path: &str) -> Result { const REFUSED: u32 = FAILURE | UNRECOGNIZED_UUID | UNRECOGNIZED_REVISION; let [query, _, offered] = evaluate(aml, path, &HOST_BRIDGE, [QUERY, ROOT_SUPPORT, ROOT_CONTROL])?; - let offered = offered & ROOT_CONTROL; if query & REFUSED != 0 { println!("{OWN}{path}'s query answered {}", status(query)); return Ok(0); } - let [commit, _, granted] = evaluate(aml, path, &HOST_BRIDGE, [0, ROOT_SUPPORT, offered])?; - if commit & REFUSED != 0 { - println!("{OWN}{path}'s commit answered {}", status(commit)); - return Ok(0); - } - Ok(granted & offered) + Ok(offered & ROOT_CONTROL) } /// What the negotiation answered, for the line the server says. diff --git a/tests/toyos.rs b/tests/toyos.rs index c6e7b37593a..0172803d792 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2705,9 +2705,9 @@ serial: {serial}")); }; eprintln!(" [virt] {armed}"); - // The root bridge the ACPI server declared, with what its `_OSC` granted. - let Some(root) = line("pci: root 0000:00 _OSC: native hot-plug granted") else { - return Err(format!("virt's root was not declared with native hot-plug granted\nserial:\n{serial}")); + // The root bridge the ACPI server declared, with what its `_OSC` offered. + let Some(root) = line("pci: root 0000:00 _OSC: native hot-plug offered, buses 0x00..=0xff") else { + return Err(format!("virt's root was not declared with native hot-plug offered\nserial:\n{serial}")); }; eprintln!(" [virt] {root}"); let Some(handed) = line(&format!("{EDU} handed over, ")) else { @@ -4235,8 +4235,8 @@ fn a_drives_row_holds_one_process_per_function() -> Result<(), String> { let said = serial::Serial::named("the drives boot", console.clone()); said.must_be_clean()?; // The root bridge the ACPI server declared, with what q35's `_OSC` - // granted: not native hot-plug, which its firmware keeps. - eprintln!(" [drives] {}", said.must_say("pci: root 0000:00 _OSC: not granted")?.trim()); + // offered: not native hot-plug, which its firmware keeps. + eprintln!(" [drives] {}", said.must_say("pci: root 0000:00 _OSC: not offered, buses 0x00..=0xff")?.trim()); let mut handed: Vec<&str> = console.lines().filter_map(|l| l.split_once("pcidev: PCI ")?.1.split_once(HANDED).map(|(at, _)| at)).collect(); let mut holding: Vec<&str> = From cf3147628d17693ff6ac9b03fca7ea4ab2f9e75f Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 15 Oct 2026 07:06:04 +0200 Subject: [PATCH 7/9] Option 1 removed: no identity domain for a kernel driver Owner ruling: the kernel stops driving PCI functions (step 10) before this lands, so no kernel driver needs a domain before the declaration. The window rule is the one rule again, for every domain: above the CPU where the unit has room, and otherwise clear of the declared windows, with no domain where nothing is declared. A claim passes its declaration; a kernel driver, which comes up before any declaration, passes none. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- kernel/pci/src/aperture.rs | 5 +- kernel/src/arch/aarch64/smmu/domain.rs | 16 ++--- kernel/src/arch/aarch64/smmu/selftest.rs | 4 +- kernel/src/arch/x86_64/amdvi/domain.rs | 11 +-- kernel/src/arch/x86_64/amdvi/selftest.rs | 4 +- kernel/src/arch/x86_64/iommu_unit.rs | 7 +- kernel/src/arch/x86_64/vtd/domain.rs | 11 +-- kernel/src/iommu/domains.rs | 77 ++++++++------------ kernel/src/iommu/mod.rs | 38 +++++----- kernel/src/iommu/window.rs | 90 ++++++++---------------- kernel/src/pcidev/mod.rs | 2 +- tests/checks/claims.rs | 14 ---- tests/common/claims.rs | 55 +++------------ tests/toyos.rs | 18 +---- 14 files changed, 114 insertions(+), 238 deletions(-) diff --git a/kernel/pci/src/aperture.rs b/kernel/pci/src/aperture.rs index 3ec229d67c0..9e40629ccb3 100644 --- a/kernel/pci/src/aperture.rs +++ b/kernel/pci/src/aperture.rs @@ -28,9 +28,8 @@ pub enum Refused { /// Two windows name one address: whichever is wrong, the declaration does /// not say what the bridges decode. Overlap(RootBridgeWindow, RootBridgeWindow), - /// A window over memory: a root bridge's window is MMIO and never RAM, which - /// is what lets a device address equal to a RAM address go unrouted - /// peer-to-peer, so a declaration saying otherwise is wrong about one. + /// A window over memory: a root bridge's window is MMIO and never RAM, so a + /// declaration saying otherwise is wrong about one. Memory(RootBridgeWindow), /// A root's record is none the ABI names: a count or a run of windows /// past its array, an `_OSC` word it does not name, or a bus range that diff --git a/kernel/src/arch/aarch64/smmu/domain.rs b/kernel/src/arch/aarch64/smmu/domain.rs index f8625649bbc..d3f03d95c53 100644 --- a/kernel/src/arch/aarch64/smmu/domain.rs +++ b/kernel/src/arch/aarch64/smmu/domain.rs @@ -20,8 +20,7 @@ use toyos_smmu::table::{entry, next, plan, Access, Entry, Leaf, INPUT_BITS}; use toyos_smmu::Asid; use super::{window, Live, UNIT}; -use crate::iommu::domains::{self, Placement}; -use crate::iommu::window::Addresses; +use crate::iommu::domains; use crate::iommu::{DomainId, IommuError, Iova, StreamId}; use crate::log; use crate::mm::PAGE_SIZE; @@ -98,14 +97,15 @@ pub fn unclaimable(_function: StreamId) -> Option<&'static str> { None } -/// A new domain placed as `placement` says: refused, with no id spent, where -/// it would not hold what was asked. -pub fn create(placement: Placement) -> Result<(DomainId, Option), IommuError> { +/// A new domain, with `room` bytes of it handed out first, clear of the root +/// bridges' declared `windows`, `None` where none are held: refused, with no +/// id spent, where it would have less than that. +pub fn create(room: u64, windows: Option<&[toyos_abi::acpi::RootBridgeWindow]>) -> Result<(DomainId, Iova), IommuError> { let doorbell = super::super::irqchip::its::doorbell_page(); let mut held = UNIT.lock(); let live = held.as_mut().ok_or(IommuError::NoUnit)?; let ceiling = if live.unit.asid(0x100).is_some() { 1 << 16 } else { 1 << 8 }; - let next = live.domains.prepare(INPUT_BITS as u8, ceiling, placement)?; + let next = live.domains.prepare(INPUT_BITS as u8, ceiling, room, windows)?; let asid = live.unit.asid(next.id).expect("SMMU: an id below the unit's ASIDs"); let root = Phys::new(live.memory.alloc(PAGE_SIZE)).expect("SMMU: a table below the unit's output size"); let context = Phys::new(live.memory.alloc(64)).expect("SMMU: a descriptor below the unit's output size"); @@ -117,9 +117,7 @@ pub fn create(placement: Placement) -> Result<(DomainId, Option), IommuErr } descriptor.write_u64(0, words[0]); if let Some(page) = doorbell { - if let Addresses::Window(window) = &next.addresses { - assert!(page < window.floor(), "SMMU: the doorbell at {page:#x} is inside the addresses a domain hands out"); - } + assert!(page < next.addresses.floor(), "SMMU: the doorbell at {page:#x} is inside the addresses a domain hands out"); live.put(root, page, Leaf::Doorbell(Phys::new(page).expect("SMMU: a 4 KiB page below 2^48"))); } let what = alloc::format!( diff --git a/kernel/src/arch/aarch64/smmu/selftest.rs b/kernel/src/arch/aarch64/smmu/selftest.rs index 0771a1b800f..7d13b19d917 100644 --- a/kernel/src/arch/aarch64/smmu/selftest.rs +++ b/kernel/src/arch/aarch64/smmu/selftest.rs @@ -59,9 +59,7 @@ pub(super) fn run(devices: &[PciDevice]) { verdict(aborted, REFUSED) ); - // At boot, before any declaration: on its guest the unit translates past - // the CPU's addresses, where no window bounds a claimed function's domain. - let (space, at) = OwnSpace::create(PAGE_2M, &[]).expect("smmu-selftest: a domain on the armed unit"); + let (space, at) = OwnSpace::create(PAGE_2M, None).expect("smmu-selftest: a domain on the armed unit"); space.place(at, phys, PAGE_2M).expect("smmu-selftest: the domain's own first leaf"); space.attach(device.bus, device.dev, device.func); let landed = write(regs, at + 0x40, phys + 0x40); diff --git a/kernel/src/arch/x86_64/amdvi/domain.rs b/kernel/src/arch/x86_64/amdvi/domain.rs index 0cd1bad476e..600e6c28cb5 100644 --- a/kernel/src/arch/x86_64/amdvi/domain.rs +++ b/kernel/src/arch/x86_64/amdvi/domain.rs @@ -15,7 +15,7 @@ use alloc::vec::Vec; -use crate::iommu::domains::{self, Domains, Placement}; +use crate::iommu::domains::{self, Domains}; use crate::iommu::{DomainId, IommuError, Iova, StreamId}; use crate::log; use crate::sync::Lock; @@ -110,13 +110,14 @@ pub fn untranslated(stream: StreamId) -> Option<&'static str> { } } -/// A new domain placed as `placement` says: refused, with no id spent, where -/// it would not hold what was asked. -pub fn create(placement: Placement) -> Result<(DomainId, Option), IommuError> { +/// A new domain, with `room` bytes of it handed out first, clear of the root +/// bridges' declared `windows`, `None` where none are held: refused, with no +/// id spent, where it would have less than that. +pub fn create(room: u64, windows: Option<&[toyos_abi::acpi::RootBridgeWindow]>) -> 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, placement)?; + let next = amd.domains.prepare(9 * ROOT_LEVEL + 12, 1 << 16, room, windows)?; let space = Space { root: TABLES.lock().alloc() }; Ok(amd.domains.add(next, space, alloc::format!("root={:#x} mode={ROOT_LEVEL}", space.root.get()))) } diff --git a/kernel/src/arch/x86_64/amdvi/selftest.rs b/kernel/src/arch/x86_64/amdvi/selftest.rs index 161cb175875..6c89eaab734 100644 --- a/kernel/src/arch/x86_64/amdvi/selftest.rs +++ b/kernel/src/arch/x86_64/amdvi/selftest.rs @@ -41,9 +41,7 @@ pub fn run(devices: &[PciDevice]) { let (on_own, on_identity) = (StreamId::pci(moved.bus, moved.dev, moved.func), StreamId::pci(kept.bus, kept.dev, kept.func)); - // At boot, before any declaration: on its guest the unit translates past - // the CPU's addresses, where no window bounds a claimed function's domain. - let (space, at) = OwnSpace::create(PAGE_2M, &[]).expect("amdvi-selftest: a domain on the programmed unit"); + let (space, at) = OwnSpace::create(PAGE_2M, None).expect("amdvi-selftest: a domain on the programmed unit"); space.place(at, phys, PAGE_2M).expect("amdvi-selftest: the domain's own first leaf"); space.attach(moved.bus, moved.dev, moved.func); let landed = write(moved_regs, at + 0x40, phys + 0x40); diff --git a/kernel/src/arch/x86_64/iommu_unit.rs b/kernel/src/arch/x86_64/iommu_unit.rs index 4c7e002b5c0..7e38795f0af 100644 --- a/kernel/src/arch/x86_64/iommu_unit.rs +++ b/kernel/src/arch/x86_64/iommu_unit.rs @@ -131,13 +131,12 @@ pub(super) fn yn(v: bool) -> char { pub mod domain { use super::{backend, Backend}; use crate::arch::x86_64::{amdvi, vtd}; - use crate::iommu::domains::Placement; use crate::iommu::{DomainId, IommuError, Iova, StreamId}; - pub fn create(placement: Placement) -> Result<(DomainId, Option), IommuError> { + pub fn create(room: u64, windows: Option<&[toyos_abi::acpi::RootBridgeWindow]>) -> Result<(DomainId, Iova), IommuError> { match backend() { - Backend::Vtd => vtd::domain::create(placement), - Backend::AmdVi => amdvi::domain::create(placement), + Backend::Vtd => vtd::domain::create(room, windows), + Backend::AmdVi => amdvi::domain::create(room, windows), Backend::None => Err(IommuError::NoUnit), } } diff --git a/kernel/src/arch/x86_64/vtd/domain.rs b/kernel/src/arch/x86_64/vtd/domain.rs index 276d3ceed58..566d1aadd88 100644 --- a/kernel/src/arch/x86_64/vtd/domain.rs +++ b/kernel/src/arch/x86_64/vtd/domain.rs @@ -18,7 +18,7 @@ use crate::log; use alloc::vec::Vec; -use crate::iommu::domains::{self, Domains, Placement}; +use crate::iommu::domains::{self, Domains}; use crate::iommu::{AddressWidth, DomainId, IommuError, Iova, StreamId}; use crate::sync::Lock; @@ -59,16 +59,17 @@ pub fn unit_agrees(width: AddressWidth, ceiling: u32, mgaw: u8) { }; } -/// A new domain placed as `placement` says: refused, with no id spent, where -/// it would not hold what was asked. -pub fn create(placement: Placement) -> Result<(DomainId, Option), IommuError> { +/// A new domain, with `room` bytes of it handed out first, clear of the root +/// bridges' declared `windows`, `None` where none are held: refused, with no +/// id spent, where it would have less than that. +pub fn create(room: u64, windows: Option<&[toyos_abi::acpi::RootBridgeWindow]>) -> Result<(DomainId, Iova), IommuError> { let mut held = DOMAINS.lock(); let (width, ceiling, mgaw) = match held.agreement { Agreement::None => return Err(IommuError::NoUnit), Agreement::Split => return Err(IommuError::WidthsDisagree), Agreement::One(width, ceiling, mgaw) => (width, ceiling, mgaw), }; - let next = held.domains.prepare(table::translatable_bits(width, mgaw), ceiling, placement)?; + let next = held.domains.prepare(table::translatable_bits(width, mgaw), ceiling, room, windows)?; let space = Space { root: TABLES.lock().alloc(), width }; let what = alloc::format!("root={:#x} aw={} mgaw={mgaw}", space.root.phys(), width.bits()); Ok(held.domains.add(next, space, what)) diff --git a/kernel/src/iommu/domains.rs b/kernel/src/iommu/domains.rs index 39fbdf203ba..562d0b00eaa 100644 --- a/kernel/src/iommu/domains.rs +++ b/kernel/src/iommu/domains.rs @@ -7,7 +7,7 @@ use alloc::vec::Vec; use toyos_abi::acpi::RootBridgeWindow; -use crate::iommu::window::{Addresses, Window}; +use crate::iommu::window::Window; use crate::iommu::{DomainId, IommuError, Iova}; use crate::mm::PAGE_2M; @@ -17,27 +17,18 @@ pub struct Domains { first: u16, /// By id minus `first`; never shrinks, since a released id would name a /// domain some unit may still have cached. - live: Vec<(S, Addresses)>, - /// The regions firmware reserved for a device, as `(start, end)`, which no - /// claimed function's addresses reach ([`Window::new`]). + live: Vec<(S, Window)>, + /// The regions firmware reserved for a device, as `(start, end)`, which + /// no domain's addresses reach beside the declared windows + /// ([`Window::new`]). reserved: Vec<(u64, u64)>, } -/// Where a new domain's addresses come from ([`Addresses`]). -#[derive(Clone, Copy)] -pub enum Placement<'a> { - /// A kernel driver's: at identity. - Identity, - /// A claimed function's: `room` bytes handed out first, clear of the - /// declared `windows`. - Claimed { room: u64, windows: &'a [RootBridgeWindow] }, -} - /// A domain [`Domains::prepare`] admitted, not yet made. pub struct Next { pub id: u16, - pub addresses: Addresses, - first: Option, + pub addresses: Window, + first: Iova, } impl Domains { @@ -45,45 +36,34 @@ impl Domains { Self { first, live: Vec::new(), reserved: Vec::new() } } - /// Before any domain is made: every claimed function's is built clear of these. + /// Before any domain is made: every one is built clear of these. pub fn avoid(&mut self, reserved: Vec<(u64, u64)>) { self.reserved = reserved; } - /// The next domain's id and its addresses over `translatable` bits, placed - /// as `placement` says; refused, with no id spent, where ids reach - /// `ceiling` or the addresses would not hold what was asked. - pub fn prepare(&self, translatable: u8, ceiling: u32, placement: Placement) -> Result { + /// The next domain's id and its window over `translatable` bits, with + /// `room` bytes handed out first, clear of the root bridges' declared + /// `windows`, `None` where no declaration is held; refused, with no id + /// spent, where ids reach `ceiling` or the window would hold less. + pub fn prepare(&self, translatable: u8, ceiling: u32, room: u64, windows: Option<&[RootBridgeWindow]>) -> Result { let id = u16::try_from(usize::from(self.first) + self.live.len()) .ok() .filter(|id| u32::from(*id) < ceiling) .ok_or(IommuError::DomainsExhausted(ceiling))?; - let (addresses, first) = match placement { - Placement::Identity => (Addresses::identity(translatable)?, None), - Placement::Claimed { room, windows } => { - let reserved: Vec<(u64, u64)> = - windows.iter().map(|w| (w.base, w.end())).chain(self.reserved.iter().copied()).collect(); - let (window, first) = Window::new(translatable, &reserved, room)?; - (Addresses::Window(window), Some(first)) - } - }; + let reserved: Option> = + windows.map(|windows| windows.iter().map(|w| (w.base, w.end())).chain(self.reserved.iter().copied()).collect()); + let (addresses, first) = Window::new(translatable, reserved.as_deref(), room)?; Ok(Next { id, addresses, first }) } - /// `next`, made over `space`, which `what` describes in its record; and - /// the room a claimed placement asked for. - pub fn add(&mut self, next: Next, space: S, what: impl core::fmt::Display) -> (DomainId, Option) { - match &next.addresses { - Addresses::Identity { translatable } => { - log!("iommu: domain{} {what} maps each pool at identity, below {:#x}", next.id, 1u64 << translatable) - } - Addresses::Window(window) => log!( - "iommu: domain{} {what} addresses from {:#x} to {:#x}", - next.id, - window.floor(), - window.ceiling() - ), - } + /// `next`, made over `space`, which `what` describes in its record. + pub fn add(&mut self, next: Next, space: S, what: impl core::fmt::Display) -> (DomainId, Iova) { + log!( + "iommu: domain{} {what} addresses from {:#x} to {:#x}", + next.id, + next.addresses.floor(), + next.addresses.ceiling() + ); self.live.push((space, next.addresses)); (DomainId::new(next.id), next.first) } @@ -97,8 +77,8 @@ impl Domains { pub fn map(&mut self, id: DomainId, phys: u64, bytes: u64) -> Result<(S, Iova), IommuError> { aligned(phys)?; // A `DomainId` only `add` mints, so the index is always in range. - let (space, addresses) = &mut self.live[usize::from(id.raw() - self.first)]; - let at = addresses.map(phys, bytes)?; + let (space, window) = &mut self.live[usize::from(id.raw() - self.first)]; + let at = window.reserve(bytes).ok_or(IommuError::AddressesExhausted(window.ceiling()))?; log!("iommu: domain{} maps {phys:#x}..{:#x} at {:#x}", id.raw(), phys + bytes.next_multiple_of(PAGE_2M), at.raw()); Ok((*space, at)) } @@ -108,9 +88,8 @@ impl Domains { /// since [`Self::map`] may yet hand it out. pub fn place(&self, id: DomainId, at: Iova, phys: u64, bytes: u64) -> Result { aligned(phys)?; - let (space, addresses) = &self.live[usize::from(id.raw() - self.first)]; - let handed_out = matches!(addresses, Addresses::Window(window) if window.handed_out(at, bytes)); - assert!(handed_out, "iommu: domain{} never handed out {:#x}+{bytes:#x}", id.raw(), at.raw()); + let (space, window) = &self.live[usize::from(id.raw() - self.first)]; + assert!(window.handed_out(at, bytes), "iommu: domain{} never handed out {:#x}+{bytes:#x}", id.raw(), at.raw()); Ok(*space) } } diff --git a/kernel/src/iommu/mod.rs b/kernel/src/iommu/mod.rs index 77030249be4..a695ee146ca 100644 --- a/kernel/src/iommu/mod.rs +++ b/kernel/src/iommu/mod.rs @@ -121,9 +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 }, - /// Memory reaches `top`, past what the units translate, so a kernel - /// driver's pool may have no device address equal to its own. - MemoryPastTranslation { translatable: u8, 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 }, @@ -150,10 +151,11 @@ 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::MemoryPastTranslation { translatable, top } => write!( + Self::Undeclared { translatable, cpu } => write!( f, - "this machine's units translate {translatable} bits and its memory reaches {top:#x}, \ - so a pool may have no device address equal to its own" + "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, @@ -177,12 +179,12 @@ pub enum DeviceSpace { } impl DeviceSpace { - /// One for whichever of `functions` is attached to it, mapping each pool - /// at its own physical address ([`window::Addresses::Identity`]), or the - /// machine's own with the reason: the machine's own where any of them - /// keeps it, so the one attached reaches what its device addresses name. - /// For a driver **in this kernel**, whose addresses are the kernel's - /// either way and which no claim ever takes over. + /// One for whichever of `functions` is attached to it, or the machine's + /// own with the reason: the machine's own where any of them keeps it, so + /// the one attached reaches what its device addresses name, and where the + /// unit has no room above the CPU's addresses, since a driver in this + /// kernel comes up before the root bridges are declared. For a driver + /// **in this kernel**, whose addresses are the kernel's either way. pub fn create(functions: &[crate::drivers::pci::PciDevice]) -> Self { for function in functions { let stream = StreamId::pci(function.bus, function.dev, function.func); @@ -191,7 +193,7 @@ impl DeviceSpace { return Self::Untranslated; } } - match unit::domain::create(domains::Placement::Identity) { + match unit::domain::create(0, None) { Ok((id, _)) => Self::Own(id), Err(why) => { log!("iommu: no domain of its own for a device: {why}"); @@ -239,11 +241,11 @@ pub struct OwnSpace(DomainId); impl OwnSpace { /// One with `room` bytes of it handed out at the address answered, clear - /// of the root bridges' declared `windows`, or the reason there is none: a - /// machine with no unit and a machine out of domains are both refusals. - pub fn create(room: u64, windows: &[toyos_abi::acpi::RootBridgeWindow]) -> Result<(Self, u64), IommuError> { - let (id, at) = unit::domain::create(domains::Placement::Claimed { room, windows })?; - Ok((Self(id), at.expect("iommu: a claimed placement hands out its room").raw())) + /// of the root bridges' declared `windows`, `None` where none are held, + /// or the reason there is none: a machine with no unit and a machine out + /// of domains are both refusals. + pub fn create(room: u64, windows: Option<&[toyos_abi::acpi::RootBridgeWindow]>) -> Result<(Self, u64), IommuError> { + unit::domain::create(room, windows).map(|(id, at)| (Self(id), at.raw())) } /// [`DeviceSpace::map`]. diff --git a/kernel/src/iommu/window.rs b/kernel/src/iommu/window.rs index 7c49f8582cb..4392aeec9e5 100644 --- a/kernel/src/iommu/window.rs +++ b/kernel/src/iommu/window.rs @@ -1,23 +1,18 @@ //! A domain's device addresses — where they start, where they end and which //! of them are handed out — for every backend. //! -//! **A kernel driver's domain maps each pool at its own physical address** -//! ([`Addresses::Identity`]): a root bridge's window is MMIO and never RAM -//! (`toyos_pci::aperture::check` refuses a declaration saying otherwise), so a -//! device address equal to a RAM address is never routed peer-to-peer before -//! the unit sees it (PCIe Base §2.4), and no declaration is needed. The -//! function is never released to a claim, so no later holder inherits an -//! address it was aimed at. -//! -//! **A claimed function's domain hands its addresses out above all memory** -//! ([`Window`]), so a descriptor a holder left behind names nothing the next -//! holder's domain maps, and clear of every declared root-bridge window. Where -//! the unit translates past every address the CPU issues, they lie above every -//! such address. Where it translates no more bits than the CPU's physical -//! addresses, whatever the backend, they start a quarter of the way up and end -//! under the first declared window or region firmware reserved for a device -//! (an RMRR or IVMD) over that start. An address is handed out once, unmapped -//! or not: a device holding a stale one would reach whatever took its place. +//! They lie above all memory, so a descriptor still carrying one names +//! nothing a domain maps and faults rather than landing on a page, and clear +//! of every root-bridge window: a bridge may route a window peer-to-peer +//! before the unit sees the request (PCIe Base §2.4). Where the unit +//! translates past every address the CPU issues, they lie above every such +//! address, and nothing needs declaring. Where it translates no more bits +//! than the CPU's physical addresses, whatever the backend, they start a +//! quarter of the way up and end under the first declared window or region +//! firmware reserved for a device (an RMRR or IVMD) over that start, and with +//! no declaration accepted there is no domain. An address is handed out once, +//! unmapped or not: a device holding a stale one would reach whatever took +//! its place. use crate::iommu::{IommuError, Iova}; use crate::mm::PAGE_2M; @@ -54,46 +49,15 @@ const fn ceiling(translatable: u8, floor: u64, reserved: &[(u64, u64)]) -> u64 { /// Where a domain over `translatable` bits begins and ends on a CPU issuing /// `cpu`-bit physical addresses, `reserved` the declared root-bridge windows -/// and firmware's reserved regions. -const fn bounds(translatable: u8, cpu: u8, reserved: &[(u64, u64)]) -> (u64, u64) { +/// and firmware's reserved regions, or `None` where no declaration is held. +const fn bounds(translatable: u8, cpu: u8, reserved: Option<&[(u64, u64)]>) -> Result<(u64, u64), IommuError> { if translatable > cpu { - return (1 << cpu, 1 << translatable); + return Ok((1 << cpu, 1 << translatable)); } let floor = first_address(translatable); - (floor, ceiling(translatable, floor, reserved)) -} - -/// Where one domain's device addresses come from. -#[derive(Clone, Copy)] -pub enum Addresses { - /// A kernel driver's: each mapping at its own physical address, below - /// `1 << translatable`. - Identity { translatable: u8 }, - /// A claimed function's. - Window(Window), -} - -impl Addresses { - /// A kernel driver's, refused where memory reaches past what the unit - /// translates: a pool there would have no address equal to its own. - pub fn identity(translatable: u8) -> Result { - let top = crate::mm::pmm::top(); - if top > 1 << translatable { - return Err(IommuError::MemoryPastTranslation { translatable, top }); - } - Ok(Self::Identity { translatable }) - } - - /// Where `bytes` at `phys` goes in this domain: `phys` itself in a kernel - /// driver's, and in a claimed function's room never handed out before. - pub fn map(&mut self, phys: u64, bytes: u64) -> Result { - match self { - Self::Identity { translatable } => match phys.checked_add(bytes.next_multiple_of(PAGE_2M)) { - Some(end) if end <= 1 << *translatable => Ok(Iova::translated(phys)), - _ => Err(IommuError::MemoryPastTranslation { translatable: *translatable, top: phys.saturating_add(bytes) }), - }, - Self::Window(window) => window.reserve(bytes).ok_or(IommuError::AddressesExhausted(window.ceiling())), - } + match reserved { + Some(reserved) => Ok((floor, ceiling(translatable, floor, reserved))), + None => Err(IommuError::Undeclared { translatable, cpu }), } } @@ -101,8 +65,8 @@ impl Window { /// A new domain's window over `translatable` bits ([`bounds`]), with /// `room` bytes of it handed out first; refused where it would start at or /// below memory or hold less than that, and at least one leaf. - pub fn new(translatable: u8, reserved: &[(u64, u64)], room: u64) -> Result<(Self, Iova), IommuError> { - let (floor, ceiling) = bounds(translatable, crate::arch::cpu::physical_address_bits(), reserved); + pub fn new(translatable: u8, reserved: Option<&[(u64, u64)]>, room: u64) -> Result<(Self, Iova), IommuError> { + let (floor, ceiling) = bounds(translatable, crate::arch::cpu::physical_address_bits(), reserved)?; let top = crate::mm::pmm::top(); if floor <= top { return Err(IommuError::WindowBelowMemory { translatable, floor, top }); @@ -209,14 +173,16 @@ const _: () = { }; /// [`bounds`] on each kind of machine: a unit wider than the CPU's addresses -/// places above them, whatever is declared; a unit no wider places under its -/// first declared window. +/// places above them, declared or not; a unit no wider places under its +/// first declared window, and with nothing declared nowhere. const _: () = { const T14: [(u64, u64); 1] = [(0x40_0000_0000, 0x60_3DC0_0000)]; - assert!(matches!(bounds(48, 40, &[]), (0x100_0000_0000, 0x1_0000_0000_0000))); - assert!(matches!(bounds(48, 40, &T14), (0x100_0000_0000, 0x1_0000_0000_0000))); - assert!(matches!(bounds(39, 39, &T14), (0x20_0000_0000, 0x40_0000_0000))); + assert!(matches!(bounds(48, 40, None), Ok((0x100_0000_0000, 0x1_0000_0000_0000)))); + assert!(matches!(bounds(48, 40, Some(&T14)), Ok((0x100_0000_0000, 0x1_0000_0000_0000)))); + assert!(matches!(bounds(39, 39, Some(&T14)), Ok((0x20_0000_0000, 0x40_0000_0000)))); + assert!(matches!(bounds(39, 39, None), Err(IommuError::Undeclared { translatable: 39, cpu: 39 }))); + assert!(matches!(bounds(39, 46, None), Err(IommuError::Undeclared { .. }))); // The AMD bench: 48-bit host tables on a CPU of 48, all its windows below // the quarter floor, so the whole of the upper quarter. - assert!(matches!(bounds(48, 48, &[(0x4_5020_0000, 0xfc_9000_0000)]), (0x4000_0000_0000, 0x1_0000_0000_0000))); + assert!(matches!(bounds(48, 48, Some(&[(0x4_5020_0000, 0xfc_9000_0000)])), Ok((0x4000_0000_0000, 0x1_0000_0000_0000)))); }; diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index d077cb37e64..9de4cef86fd 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -1141,7 +1141,7 @@ fn function_space(number: usize, windows: &[RootBridgeWindow]) -> Result Ok(space), None => { - let (space, lend) = OwnSpace::create(LEND_WINDOW, windows)?; + let (space, lend) = OwnSpace::create(LEND_WINDOW, Some(windows))?; *held = Some(Space { space, lend }); Ok(Space { space, lend }) } diff --git a/tests/checks/claims.rs b/tests/checks/claims.rs index 343308841ec..3b1b97565e0 100644 --- a/tests/checks/claims.rs +++ b/tests/checks/claims.rs @@ -72,23 +72,9 @@ pub fn one_entry_per_slot() { } /// The recorded domain reaches into `0x4000000000..`; capped there, it passes. -/// A kernel driver's domain passes where each pool is at its own address -/// outside every window, and reds where one is not. pub fn domains_end_below_the_windows() { let why = claims::clear_of_host_bridges(&log(RECORDED)).expect_err("the recorded domain"); assert!(why.contains("inside the root-bridge window 0x4000000000..0x603dc00000"), "{why}"); let capped = RECORDED.replace("to 0x8000000000", "to 0x4000000000"); assert_eq!(claims::clear_of_host_bridges(&log(&capped)), Ok(())); - - let identity = |maps: &str| { - format!( - "{capped}[2026-09-29 11:54:39 0.260 cpu0 kernel] iommu: domain3 root=0xa32000 aw=48 mgaw=39 maps each pool at identity, below 0x8000000000\n\ - [2026-09-29 11:54:39 0.261 cpu0 kernel] iommu: domain3 maps {maps}\n" - ) - }; - assert_eq!(claims::clear_of_host_bridges(&log(&identity("0x4c00000..0x4e00000 at 0x4c00000"))), Ok(())); - let moved = claims::clear_of_host_bridges(&log(&identity("0x4c00000..0x4e00000 at 0x2000000000"))).expect_err("a pool off identity"); - assert!(moved.contains("maps 0x4c00000..0x4e00000 at 0x2000000000"), "{moved}"); - let inside = claims::clear_of_host_bridges(&log(&identity("0xa2000000..0xa2200000 at 0xa2000000"))).expect_err("a pool in a window"); - assert!(inside.contains("inside the root-bridge window 0xa2000000..0xbd000000"), "{inside}"); } diff --git a/tests/common/claims.rs b/tests/common/claims.rs index 98b34c2efef..fb5630fba83 100644 --- a/tests/common/claims.rs +++ b/tests/common/claims.rs @@ -149,66 +149,27 @@ fn declared(log: &Serial) -> Result, String> { .collect() } -/// Each mapping domain `id`'s records say it made, as `(physical start, -/// physical end, device address)`. -pub fn mappings(log: &Serial, id: &str) -> Result, String> { - let head = format!("iommu: domain{id} maps "); - records(log) - .filter_map(|m| m.strip_prefix(head.as_str())) - .map(|rest| { - let (phys, at) = rest.split_once(" at ").ok_or_else(|| format!("{rest:?}"))?; - let (start, end) = span(phys).ok_or_else(|| format!("{rest:?}"))?; - let at = u64::from_str_radix(at.split_whitespace().next().and_then(|a| a.strip_prefix("0x")).ok_or_else(|| format!("{rest:?}"))?, 16) - .map_err(|e| format!("{rest:?}: {e}"))?; - Ok((start, end, at)) - }) - .collect() -} - -/// Whether domain `id`'s record says it maps each pool at identity: a kernel -/// driver's. -pub fn at_identity(log: &Serial, id: &str) -> bool { - let head = format!("iommu: domain{id} "); - records(log).any(|m| m.starts_with(head.as_str()) && m.contains(" maps each pool at identity")) -} - -/// No domain reaches a root-bridge window the `acpi` claim's holder declared: -/// a bridge may route a request in one before the unit sees it. A claimed -/// function's domain hands out addresses outside every window; a kernel -/// driver's maps each pool at its own physical address, which lies outside -/// every window too. +/// Every domain's addresses lie outside every root-bridge window the `acpi` +/// claim's holder declared: a bridge may route a request in one before the +/// unit sees it. pub fn clear_of_host_bridges(log: &Serial) -> Result<(), String> { let windows = declared(log)?; - let inside = |(floor, ceiling): (u64, u64)| windows.iter().copied().find(|(base, end)| *end > floor && *base < ceiling); - let (mut claimed, mut identity) = (0, 0); + let mut domains = 0; for record in records(log).filter(|m| m.starts_with("iommu: domain")) { - let id = record["iommu: domain".len()..].split_whitespace().next().unwrap_or_default(); - if record.contains(" maps each pool at identity") { - for (start, end, at) in mappings(log, id)? { - if at != start { - return Err(format!("domain{id} is a kernel driver's and maps {start:#x}..{end:#x} at {at:#x}")); - } - if let Some((base, end)) = inside((start, end)) { - return Err(format!("domain{id} maps a pool inside the root-bridge window {base:#x}..{end:#x}")); - } - } - identity += 1; - continue; - } let Some((_, addresses)) = record.split_once(" addresses from ") else { continue }; let (floor, ceiling) = addresses .split_once(" to ") .and_then(|(from, to)| span(&format!("{from}..{to}"))) .ok_or_else(|| format!("an unreadable domain record: {record:?}"))?; - if let Some((base, end)) = inside((floor, ceiling)) { + if let Some((base, end)) = windows.iter().find(|(base, end)| *end > floor && *base < ceiling) { return Err(format!("{record:?} hands out addresses inside the root-bridge window {base:#x}..{end:#x}")); } - claimed += 1; + domains += 1; } - if claimed + identity == 0 { + if domains == 0 { return Err(format!("no domain was made on this boot:\n{}", log.text())); } - eprintln!(" [claims] {claimed} claimed domain(s) and {identity} kernel driver's, none reaching a root-bridge window"); + eprintln!(" [claims] {domains} domain(s), none reaching a root-bridge window"); Ok(()) } diff --git a/tests/toyos.rs b/tests/toyos.rs index 0172803d792..c5e27e9a0ee 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -4258,9 +4258,8 @@ fn a_drives_row_holds_one_process_per_function() -> Result<(), String> { } /// Every AMD-Vi unit translating, every xHCI controller the kernel drives -/// moved to a domain of its own that maps exactly its one pool, at the pool's -/// own physical address, and nothing else; the boot stick bound through one, -/// and no DMA refused. +/// moved to a domain of its own, the boot stick bound through one, the root +/// bridges declared, and no DMA refused. fn amdvi_translates(kernel: &serial::Serial) -> Result<(), String> { let text = kernel.text(); let units: Vec<&str> = @@ -4278,18 +4277,7 @@ fn amdvi_translates(kernel: &serial::Serial) -> Result<(), String> { return Err(format!("the kernel drove no xHCI controller:\n{text}")); } for at in &controllers { - let moved = format!("iommu: {at} moves to domain"); - let id = text - .lines() - .find_map(|l| l.split_once(moved.as_str())?.1.split_whitespace().next()) - .ok_or_else(|| format!("{at} was moved to no domain:\n{text}"))?; - if !claims::at_identity(kernel, id) { - return Err(format!("{at}'s domain{id} is no kernel driver's, mapping at identity:\n{text}")); - } - match claims::mappings(kernel, id)?.as_slice() { - [(start, end, at_)] if at_ == start => eprintln!(" [amdvi] {at}: domain{id} maps its pool {start:#x}..{end:#x} at identity, and nothing else"), - other => return Err(format!("{at}'s domain{id} maps {other:x?}, not its one pool at identity")), - } + kernel.must_say(&format!("iommu: {at} moves to domain"))?; } kernel.must_say("usb-storage: 1 device(s)")?; // The bench's root bridges declared and held, which a claim on it needs. From de8e9883afd10de7d7a4d5686e2e005ea270729c Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 15 Oct 2026 07:45:33 +0200 Subject: [PATCH 8/9] virt_claim_lpi: VirtIts has two roots, so its root 0 ends at bus 7 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- tests/toyos.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/toyos.rs b/tests/toyos.rs index e410875efb4..cff33ea48d5 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2752,7 +2752,7 @@ serial: }; eprintln!(" [virt] {armed}"); // The root bridge the ACPI server declared, with what its `_OSC` offered. - let Some(root) = line("pci: root 0000:00 _OSC: native hot-plug offered, buses 0x00..=0xff") else { + let Some(root) = line("pci: root 0000:00 _OSC: native hot-plug offered, buses ") else { return Err(format!("virt's root was not declared with native hot-plug offered\nserial:\n{serial}")); }; eprintln!(" [virt] {root}"); From a18b42b8894efcff4c930c4a8cbb995bf7dea748 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 15 Oct 2026 07:49:50 +0200 Subject: [PATCH 9/9] The per-root record carries the _OSC commit beside the offer So #934 commits through this ABI unchanged: a root's record holds what its query offered and what a commit (Query clear) was granted, none in stage B. A commit beside no offer, or of bits not offered, is a record the ABI does not name, refused by name with the declaration. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- kernel/pci/src/aperture.rs | 20 +++++--- kernel/src/pcidev/mod.rs | 23 +++++++-- sdk/abi/src/acpi.rs | 90 +++++++++++++++++++++++++++-------- system/acpiserver/src/main.rs | 2 +- 4 files changed, 106 insertions(+), 29 deletions(-) diff --git a/kernel/pci/src/aperture.rs b/kernel/pci/src/aperture.rs index 9e40629ccb3..0917589890f 100644 --- a/kernel/pci/src/aperture.rs +++ b/kernel/pci/src/aperture.rs @@ -32,8 +32,9 @@ pub enum Refused { /// declaration saying otherwise is wrong about one. Memory(RootBridgeWindow), /// A root's record is none the ABI names: a count or a run of windows - /// past its array, an `_OSC` word it does not name, or a bus range that - /// ends before it starts. + /// past its array, an `_OSC` word it does not name, a commit beside no + /// offer or of more than it offered, or a bus range that ends before it + /// starts. Record, /// Two roots of one segment name one bus. Buses { segment: u16, first: u8, last: u8 }, @@ -189,7 +190,7 @@ impl> Held { pub fn records(declared: &Apertures) -> Result<(), Refused> { let bridges = declared.bridges().ok_or(Refused::Record)?; for (at, bridge) in bridges.iter().enumerate() { - let named = bridge.first_bus <= bridge.last_bus && bridge.osc().is_some() && declared.windows_of(bridge).is_some(); + let named = bridge.first_bus <= bridge.last_bus && bridge.commit().is_some() && declared.windows_of(bridge).is_some(); if !named { return Err(Refused::Record); } @@ -343,7 +344,7 @@ mod tests { use toyos_abi::acpi::{HostBridge, Osc}; let w = RootBridgeWindow { base: 0x8000_0000, length: 0x1000_0000 }; let mut good = Apertures::default(); - good.push(0, (0, 0x7f), &[w], Osc::Absent).expect("fits"); + good.push(0, (0, 0x7f), &[w], Osc::Absent, None).expect("fits"); assert_eq!(records(&good), Ok(())); let first = good.bridges[0]; let with = |bridge: HostBridge| Apertures { bridges: [bridge; toyos_abi::acpi::MAX_BRIDGES], ..good }; @@ -351,12 +352,19 @@ mod tests { assert_eq!(records(&with(HostBridge { window_count: 2, ..first })), Err(Refused::Record)); assert_eq!(records(&with(HostBridge { osc: 7, ..first })), Err(Refused::Record)); assert_eq!(records(&with(HostBridge { first_bus: 0x80, last_bus: 0x7f, ..first })), Err(Refused::Record)); + // A commit with no offer beside it. + let mut uncommitted = Apertures::default(); + uncommitted.push(0, (0, 0x7f), &[w], Osc::Absent, Some(toyos_abi::acpi::NATIVE_HOT_PLUG)).expect("fits"); + assert_eq!(records(&uncommitted), Err(Refused::Record)); + let mut offered = Apertures::default(); + offered.push(0, (0, 0x7f), &[w], Osc::Offered(toyos_abi::acpi::NATIVE_HOT_PLUG), None).expect("fits"); + assert_eq!(records(&offered), Ok(())); let mut shared = good; - shared.push(0, (0x40, 0xff), &[], Osc::Absent).expect("fits"); + shared.push(0, (0x40, 0xff), &[], Osc::Absent, None).expect("fits"); assert_eq!(records(&shared), Err(Refused::Buses { segment: 0, first: 0x40, last: 0xff })); let mut apart = good; - apart.push(1, (0, 0x7f), &[], Osc::Absent).expect("fits"); + apart.push(1, (0, 0x7f), &[], Osc::Absent, None).expect("fits"); assert_eq!(records(&apart), Ok(())); let mut held = Held::<[RootBridgeWindow; 1]>::Unknown; diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 9de4cef86fd..701e3c431cc 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -411,11 +411,13 @@ struct Machine { resetting: Vec<(u16, Resetting, Kept)>, } -/// One PCI root bridge as the `acpi` claim's holder declared it. +/// One PCI root bridge as the `acpi` claim's holder declared it: what its +/// `_OSC` query offered, and what a commit was granted where one was made. struct RootBridge { segment: u16, buses: (u8, u8), osc: toyos_abi::acpi::Osc, + committed: Option, } /// Posted once, when [`Machine::apertures`] leaves `Unknown`. @@ -682,7 +684,12 @@ pub fn declare(declared: &toyos_abi::acpi::Apertures) -> Result<(), aperture::De if answer != Err(aperture::Declined::Late) { machine.roots = bridges .iter() - .map(|b| RootBridge { segment: b.segment, buses: (b.first_bus, b.last_bus), osc: b.osc().expect("`records` read every word") }) + .map(|b| RootBridge { + segment: b.segment, + buses: (b.first_bus, b.last_bus), + osc: b.osc().expect("`records` read every word"), + committed: b.commit().expect("`records` read every word"), + }) .collect(); for root in &machine.roots { let offered = match root.osc { @@ -690,7 +697,17 @@ pub fn declare(declared: &toyos_abi::acpi::Apertures) -> Result<(), aperture::De toyos_abi::acpi::Osc::Offered(offered) if offered & toyos_abi::acpi::NATIVE_HOT_PLUG != 0 => "native hot-plug offered", toyos_abi::acpi::Osc::Offered(_) | toyos_abi::acpi::Osc::Refused => "not offered", }; - log!("pci: root {:04x}:{:02x} _OSC: {offered}, buses {:#04x}..={:#04x}", root.segment, root.buses.0, root.buses.0, root.buses.1); + let committed = match root.committed { + None => alloc::string::String::from("no commit"), + Some(committed) => alloc::format!("committed {committed:#x}"), + }; + log!( + "pci: root {:04x}:{:02x} _OSC: {offered}, buses {:#04x}..={:#04x}, {committed}", + root.segment, + root.buses.0, + root.buses.0, + root.buses.1 + ); } } answer diff --git a/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index 88a41005b36..905fddda050 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -384,16 +384,23 @@ const OSC_ABSENT: u32 = 0; const OSC_REFUSED: u32 = 1; const OSC_ANSWERED: u32 = 2; +/// [`HostBridge::commit`]'s words. +const COMMIT_NONE: u32 = 0; +const COMMIT_MADE: u32 = 1; + crate::user_safe! { /// One PCI root bridge: its segment, the buses below it, what its `_OSC` - /// granted, and the run of [`Apertures::windows`] its `_CRS` produces. + /// query offered and what a commit was granted, and the run of + /// [`Apertures::windows`] its `_CRS` produces. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct HostBridge { pub segment: u16, pub first_bus: u8, pub last_bus: u8, pub osc: u32, - pub granted: u32, + pub offered: u32, + pub commit: u32, + pub committed: u32, pub first_window: u32, pub window_count: u32, } @@ -403,10 +410,21 @@ impl HostBridge { /// What its `_OSC` said, or `None` for a word no [`Osc`] is, or a grant /// beside no answer. pub fn osc(&self) -> Option { - match (self.osc, self.granted) { + match (self.osc, self.offered) { (OSC_ABSENT, 0) => Some(Osc::Absent), (OSC_REFUSED, 0) => Some(Osc::Refused), - (OSC_ANSWERED, granted) => Some(Osc::Offered(granted)), + (OSC_ANSWERED, offered) => Some(Osc::Offered(offered)), + _ => None, + } + } + + /// The Control bits a commit (Query clear) was granted, `Some(None)` + /// where none was made; `None` for a word the ABI does not name, a + /// commit beside no offer, or bits granted that were not offered. + pub fn commit(&self) -> Option> { + match (self.commit, self.osc()?) { + (COMMIT_NONE, _) if self.committed == 0 => Some(None), + (COMMIT_MADE, Osc::Offered(offered)) if self.committed & !offered == 0 => Some(Some(self.committed)), _ => None, } } @@ -426,19 +444,34 @@ crate::user_safe! { impl Default for Apertures { fn default() -> Self { - let none = HostBridge { segment: 0, first_bus: 0, last_bus: 0, osc: OSC_ABSENT, granted: 0, first_window: 0, window_count: 0 }; + let none = HostBridge { + segment: 0, + first_bus: 0, + last_bus: 0, + osc: OSC_ABSENT, + offered: 0, + commit: COMMIT_NONE, + committed: 0, + first_window: 0, + window_count: 0, + }; Self { bridge_count: 0, window_count: 0, bridges: [none; MAX_BRIDGES], windows: [RootBridgeWindow::default(); MAX_WINDOWS] } } } impl Apertures { - /// Add one root, or `None` where it does not fit: past [`MAX_BRIDGES`] - /// or [`MAX_WINDOWS`], or over a bus range that ends before it starts. - pub fn push(&mut self, segment: u16, buses: (u8, u8), windows: &[RootBridgeWindow], osc: Osc) -> Option<()> { - let (osc, granted) = match osc { + /// Add one root, `committed` what a commit was granted where one was + /// made, or `None` where it does not fit: past [`MAX_BRIDGES`] or + /// [`MAX_WINDOWS`], or over a bus range that ends before it starts. + pub fn push(&mut self, segment: u16, buses: (u8, u8), windows: &[RootBridgeWindow], osc: Osc, committed: Option) -> Option<()> { + let (osc, offered) = match osc { Osc::Absent => (OSC_ABSENT, 0), Osc::Refused => (OSC_REFUSED, 0), - Osc::Offered(granted) => (OSC_ANSWERED, granted), + Osc::Offered(offered) => (OSC_ANSWERED, offered), + }; + let (commit, committed) = match committed { + None => (COMMIT_NONE, 0), + Some(committed) => (COMMIT_MADE, committed), }; (buses.0 <= buses.1).then_some(())?; let first = self.window_count as usize; @@ -450,7 +483,9 @@ impl Apertures { first_bus: buses.0, last_bus: buses.1, osc, - granted, + offered, + commit, + committed, first_window: first as u32, window_count: windows.len() as u32, }; @@ -504,31 +539,48 @@ mod tests { fn a_declaration_carries_its_bridges_and_nothing_past_them() { let w = |base| RootBridgeWindow { base, length: 0x1000 }; let mut declared = Apertures::default(); - declared.push(0, (0, 0x7f), &[w(0x1000), w(0x3000)], Osc::Offered(NATIVE_HOT_PLUG)).expect("fits"); - declared.push(0, (0x80, 0xff), &[w(0x5000)], Osc::Absent).expect("fits"); + declared.push(0, (0, 0x7f), &[w(0x1000), w(0x3000)], Osc::Offered(NATIVE_HOT_PLUG), None).expect("fits"); + declared.push(0, (0x80, 0xff), &[w(0x5000)], Osc::Absent, None).expect("fits"); let bridges = declared.bridges().expect("two bridges").to_vec(); assert_eq!(bridges.len(), 2); assert_eq!(declared.windows_of(&bridges[0]), Some(&[w(0x1000), w(0x3000)][..])); assert_eq!(declared.windows_of(&bridges[1]), Some(&[w(0x5000)][..])); assert_eq!(bridges[0].osc(), Some(Osc::Offered(NATIVE_HOT_PLUG))); assert_eq!(bridges[1].osc(), Some(Osc::Absent)); - assert_eq!(declared.push(0, (2, 1), &[], Osc::Absent), None); + assert_eq!(declared.push(0, (2, 1), &[], Osc::Absent, None), None); // One root may carry every window: the bound is the declaration's. let mut wide = Apertures::default(); - assert_eq!(wide.push(0, (0, 0xff), &[w(0); MAX_WINDOWS], Osc::Absent), Some(())); - assert_eq!(wide.push(0, (0, 0xff), &[w(0)], Osc::Absent), None); + assert_eq!(wide.push(0, (0, 0xff), &[w(0); MAX_WINDOWS], Osc::Absent, None), Some(())); + assert_eq!(wide.push(0, (0, 0xff), &[w(0)], Osc::Absent, None), None); let mut many = Apertures::default(); for _ in 0..MAX_BRIDGES { - many.push(0, (0, 0), &[], Osc::Absent).expect("fits"); + many.push(0, (0, 0), &[], Osc::Absent, None).expect("fits"); } - assert_eq!(many.push(0, (0, 0), &[], Osc::Absent), None); + assert_eq!(many.push(0, (0, 0), &[], Osc::Absent, None), None); let first = bridges[0]; assert_eq!(declared.windows_of(&HostBridge { window_count: 3, ..first }), Some(&[w(0x1000), w(0x3000), w(0x5000)][..])); assert_eq!(declared.windows_of(&HostBridge { window_count: 4, ..first }), None); assert_eq!(HostBridge { osc: 3, ..first }.osc(), None); - assert_eq!(HostBridge { osc: OSC_ABSENT, granted: 1, ..first }.osc(), None); + assert_eq!(HostBridge { osc: OSC_ABSENT, offered: 1, ..first }.osc(), None); + + // An offer with no commit, stage B's record, decodes; a commit decodes + // beside an offer of what it was granted; a commit beside no offer, + // granting what was not offered, or under a word the ABI does not + // name, is none. + assert_eq!(first.commit(), Some(None)); + let mut committed = Apertures::default(); + committed.push(0, (0, 0xff), &[], Osc::Offered(NATIVE_HOT_PLUG), Some(NATIVE_HOT_PLUG)).expect("fits"); + assert_eq!(committed.bridges[0].commit(), Some(Some(NATIVE_HOT_PLUG))); + let mut unoffered = Apertures::default(); + unoffered.push(0, (0, 0xff), &[], Osc::Absent, Some(NATIVE_HOT_PLUG)).expect("fits"); + assert_eq!(unoffered.bridges[0].commit(), None); + let mut beyond = Apertures::default(); + beyond.push(0, (0, 0xff), &[], Osc::Offered(CAPABILITY_STRUCTURE), Some(NATIVE_HOT_PLUG)).expect("fits"); + assert_eq!(beyond.bridges[0].commit(), None); + assert_eq!(HostBridge { commit: 2, ..first }.commit(), None); + assert_eq!(HostBridge { committed: 1, ..first }.commit(), None); assert_eq!(Apertures { bridge_count: MAX_BRIDGES as u32 + 1, ..declared }.bridges(), None); } } diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 635763e2f52..95f9b30ce13 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -278,7 +278,7 @@ impl Kernel for Claim<'_> { fn declare(&self, roots: &[apertures::Root]) -> Result { let mut declared = Apertures::default(); for root in roots { - declared.push(root.segment, root.buses, &root.windows, root.osc).expect("acpiserver: the roots were read into a declaration's room"); + declared.push(root.segment, root.buses, &root.windows, root.osc, None).expect("acpiserver: the roots were read into a declaration's room"); } match self.dev.declare(&declared) { Err(SyscallError::InvalidArgument) => Ok(Declared::Refused),