diff --git a/build/build.rs b/build/build.rs index d63b8d112c4..8d5802f9ed9 100644 --- a/build/build.rs +++ b/build/build.rs @@ -3106,6 +3106,7 @@ mod tests { "tests/images/proctreecase.toml", "tests/images/scalecase.toml", "tests/images/testcases.toml", + "tests/images/undeclaredcase.toml", "tests/images/virtclaimcase.toml", "tests/images/virtjobcase.toml", "tests/images/virtpaniccase.toml", 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/images/system.toml b/images/system.toml index f333a3a1f32..d526db66483 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/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/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/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/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..0917589890f 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::{Apertures, RootBridgeWindow}; /// Where an extent stands against the memory windows the root bridges decode. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -28,6 +28,16 @@ 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, 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, 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 }, } impl core::fmt::Display for Refused { @@ -43,13 +53,18 @@ 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()), + 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}") + } } } } /// 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 +74,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 +90,120 @@ 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 + } + + /// 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 { + 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)), + } + } +} + +/// 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.commit().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 @@ -131,7 +257,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 +267,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 +302,75 @@ 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))); + } + } + + /// 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, 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 }; + 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)); + // 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, 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, None).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/pci/src/placement.rs b/kernel/pci/src/placement.rs index 72e6eb3a6b5..ef2468b6acf 100644 --- a/kernel/pci/src/placement.rs +++ b/kernel/pci/src/placement.rs @@ -4,11 +4,11 @@ //! [`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::boot::RootBridgeWindow; +use toyos_abi::acpi::RootBridgeWindow; use crate::bridge::{Bridge, Window}; 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 710c7127b05..d3f03d95c53 100644 --- a/kernel/src/arch/aarch64/smmu/domain.rs +++ b/kernel/src/arch/aarch64/smmu/domain.rs @@ -97,14 +97,15 @@ pub fn unclaimable(_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, 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, room)?; + 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"); diff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs index 56862339320..62bdca70b4a 100644 --- a/kernel/src/arch/aarch64/smmu/mod.rs +++ b/kernel/src/arch/aarch64/smmu/mod.rs @@ -210,7 +210,7 @@ pub fn drivers_settled() {} /// 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)) => { @@ -271,8 +271,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); @@ -329,7 +328,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)); @@ -404,7 +402,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..7d13b19d917 100644 --- a/kernel/src/arch/aarch64/smmu/selftest.rs +++ b/kernel/src/arch/aarch64/smmu/selftest.rs @@ -59,7 +59,7 @@ 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"); + 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 7d1934032b1..600e6c28cb5 100644 --- a/kernel/src/arch/x86_64/amdvi/domain.rs +++ b/kernel/src/arch/x86_64/amdvi/domain.rs @@ -59,7 +59,7 @@ static DOMAINS: Lock> = Lock::new(None); /// 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>, + reserved: Vec<(u64, u64)>, settings: Vec, kept: Vec, translated: Vec<(u16, u16)>, @@ -110,13 +110,14 @@ 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, 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, room)?; + 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/mod.rs b/kernel/src/arch/x86_64/amdvi/mod.rs index a9a96b6db53..bc6b090c360 100644 --- a/kernel/src/arch/x86_64/amdvi/mod.rs +++ b/kernel/src/arch/x86_64/amdvi/mod.rs @@ -31,7 +31,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::{interrupts_blocked, Dte, Setting, DEVICE_IDS, ENTRY_BYTES}; use toyos_amdvi::queue::{Command, ENTRIES}; use toyos_amdvi::reg::{self, control, status, Features, SmiFilter}; @@ -142,9 +141,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, @@ -312,10 +311,7 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeW remap }; let identity = build(devices, &settings, remap.is_some()); - // 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(); let mut ids: Vec = platform.iter().map(|&(id, _)| id).collect(); ids.sort_unstable(); ids.dedup(); diff --git a/kernel/src/arch/x86_64/amdvi/selftest.rs b/kernel/src/arch/x86_64/amdvi/selftest.rs index 96fe0d7b873..6c89eaab734 100644 --- a/kernel/src/arch/x86_64/amdvi/selftest.rs +++ b/kernel/src/arch/x86_64/amdvi/selftest.rs @@ -41,7 +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)); - 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 291a2a0d883..7e38795f0af 100644 --- a/kernel/src/arch/x86_64/iommu_unit.rs +++ b/kernel/src/arch/x86_64/iommu_unit.rs @@ -8,7 +8,6 @@ use crate::drivers::pci::PciDevice; use crate::log; use crate::mm::policy::MmioPolicy; use crate::mm::Mmio; -use toyos_abi::boot::RootBridgeWindow; /// Which backend [`init`] found: none until it has, and then fixed. #[derive(Clone, Copy, PartialEq, Eq)] @@ -35,10 +34,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 @@ -134,10 +133,10 @@ pub mod domain { use crate::arch::x86_64::{amdvi, vtd}; use crate::iommu::{DomainId, IommuError, Iova, StreamId}; - pub fn create(room: u64) -> Result<(DomainId, Iova), IommuError> { + pub fn create(room: u64, windows: Option<&[toyos_abi::acpi::RootBridgeWindow]>) -> Result<(DomainId, Iova), IommuError> { match backend() { - Backend::Vtd => vtd::domain::create(room), - Backend::AmdVi => amdvi::domain::create(room), + 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 d31fe481c9a..566d1aadd88 100644 --- a/kernel/src/arch/x86_64/vtd/domain.rs +++ b/kernel/src/arch/x86_64/vtd/domain.rs @@ -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,17 @@ 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, 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, room)?; + 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/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..562d0b00eaa 100644 --- a/kernel/src/iommu/domains.rs +++ b/kernel/src/iommu/domains.rs @@ -5,6 +5,8 @@ use alloc::vec::Vec; +use toyos_abi::acpi::RootBridgeWindow; + use crate::iommu::window::Window; use crate::iommu::{DomainId, IommuError, Iova}; use crate::mm::PAGE_2M; @@ -16,10 +18,10 @@ pub struct Domains { /// 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>, + /// 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)>, } /// A domain [`Domains::prepare`] admitted, not yet made. @@ -31,23 +33,26 @@ pub struct Next { 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>) { + 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 { + /// `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) = Window::new(translatable, self.reserved.as_deref(), room)?; + 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 }) } diff --git a/kernel/src/iommu/mod.rs b/kernel/src/iommu/mod.rs index 1a4993a2764..a695ee146ca 100644 --- a/kernel/src/iommu/mod.rs +++ b/kernel/src/iommu/mod.rs @@ -181,7 +181,9 @@ 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 + /// 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 { @@ -191,7 +193,7 @@ impl DeviceSpace { return Self::Untranslated; } } - match unit::domain::create(0) { + match unit::domain::create(0, None) { Ok((id, _)) => Self::Own(id), Err(why) => { log!("iommu: no domain of its own for a device: {why}"); @@ -238,11 +240,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`, `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`]. @@ -287,15 +290,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); } /// Once, after every kernel driver has taken its functions from firmware and diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 7fe02d9bba1..80aa5ab740f 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 ca2d3069e9d..701e3c431cc 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -30,13 +30,18 @@ //! **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. Behind a bridge an address is offered only where every -//! bridge on the path forwards it ([`placement::behind_bridges`]); where none -//! of that is free the BAR stays where firmware put it and is mapped to -//! nobody, since its page there is not its own. +//! 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. +//! Behind a bridge an address is offered only where every bridge on the path +//! forwards it ([`placement::behind_bridges`]); where none of that is free the +//! BAR stays where firmware put it and is mapped to nobody, since its page +//! there is not its own. //! //! **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 @@ -125,7 +130,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}; @@ -141,7 +147,7 @@ use crate::mm::{align_2m, DirectMap, 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 @@ -382,8 +388,13 @@ struct Machine { /// claim until the probe settles. trying: 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>, + /// 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 @@ -400,19 +411,24 @@ 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), +/// 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`. +static DECLARATION: Watch = Watch::new(); + +/// 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 { functions: Vec::new(), identities: Vec::new(), @@ -424,7 +440,8 @@ static MACHINE: Lock = Lock::new(Machine { bridges: Vec::new(), map: Vec::new(), trying: Vec::new(), - apertures: Apertures::Unknown, + apertures: aperture::Held::Unknown, + roots: Vec::new(), windows: Vec::new(), resetting: Vec::new(), }); @@ -551,7 +568,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 bridges: Vec = devices.iter().filter_map(PciDevice::bridge).collect(); let map: Vec = maps.iter().map(|entry| Window { start: entry.start, end: entry.end }).collect(); @@ -590,16 +607,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) @@ -648,22 +655,116 @@ pub fn publish(devices: &[PciDevice], segment: u16, maps: &[MemoryMapEntry], fir .collect(); machine.segment = segment; machine.decoded = decoded; - machine.apertures = apertures; machine.bridges = bridges; machine.map = map; 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 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 mut machine = MACHINE.lock(); + 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"), + committed: b.commit().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", + }; + 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 + } + }; + let declared = answer; + 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); + if declared != Err(aperture::Declined::Late) { + 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(); } } -/// Say what memory firmware declared the root bridges decode, and which +/// 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 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 @@ -676,7 +777,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 { @@ -693,10 +794,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(crate::iommu::NotRemapped), /// This machine remaps and translates, and not for this function. NotIsolated(&'static str), @@ -739,14 +838,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!( - 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!( + Self::Withheld(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(crate::iommu::NotRemapped::Unremapped) => write!( @@ -859,15 +953,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); } }; @@ -943,7 +1032,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)?; // Decode must be on for a BAR to answer, and off across each move. pci.enable_memory_space(); @@ -1062,13 +1151,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, Some(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..ef035d7e7c7 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 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), + } + } _ => Err(SyscallError::InvalidArgument), } }); 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 ed8d1f5d4e2..00000000000 Binary files a/lib/acpi/fixtures/ovmf-pure-efi/root-bridge-0.bin and /dev/null differ 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 00000000000..a51a869cfbc Binary files /dev/null and b/lib/acpi/fixtures/qemu-11.1.1-virt/dsdt.bin differ 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 18ee16392b3..265f6b9994d 100644 --- a/lib/acpi/src/resource.rs +++ b/lib/acpi/src/resource.rs @@ -1,47 +1,69 @@ -//! ACPI resource descriptors, decoded into the memory windows a PCI root -//! bridge decodes, and into the I/O ports a device names its registers by -//! ([`io_ports`]). -//! -//! The bytes are the ones ACPI 6.5 §6.4.3.5 defines and firmware emits in two -//! places for the same bridge: `_CRS` in the DSDT, and -//! `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL::Configuration` (UEFI 2.10 §14.2), -//! which is what the bootloader reads and this decodes. What a window is, and -//! what an address outside every one of them is, is -//! [`toyos_abi::boot::KernelArgs::root_bridge_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 //! terminates on every input, and every refusal is a [`ResourceError`]. use crate::Phys; -use toyos_abi::boot::RootBridgeWindow; +use toyos_abi::acpi::RootBridgeWindow; -/// The furthest a descriptor list is walked. The protocol answers with a -/// pointer and no length, so this is what bounds the walk of a list whose End -/// Tag is missing, and it is what a reader's [`Phys::readable`] bounds itself -/// by. -pub const MAX_LIST_BYTES: usize = 1024; +/// The furthest a descriptor list is walked: what bounds the walk of a list +/// whose End Tag is missing, beside its reader's own [`Phys::readable`]. +pub const MAX_LIST_BYTES: usize = 4096; /// ACPI 6.5 §6.4.2.9: the small item that ends every resource list, over one /// checksum byte. const END_TAG: u8 = 0x79; -/// ACPI 6.5 §6.4.3.5.1: the QWORD Address Space Descriptor. +/// ACPI 6.5 §6.4.3.5: the four Address Space Descriptors, each a large item +/// whose fields from the resource type to the length have one order and +/// differ in width ([`Layout`]). const QWORD_ADDRESS_SPACE: u8 = 0x8A; -/// Its fields, from the descriptor's first byte. +const DWORD_ADDRESS_SPACE: u8 = 0x87; +const WORD_ADDRESS_SPACE: u8 = 0x88; +const EXTENDED_ADDRESS_SPACE: u8 = 0x8B; +/// The fields every one of them carries at the same place. const RESOURCE_TYPE: usize = 3; const GENERAL_FLAGS: usize = 4; -const QWORD_MINIMUM: usize = 14; -const QWORD_MAXIMUM: usize = 22; -const QWORD_TRANSLATION: usize = 30; -const QWORD_LENGTH: usize = 38; -const QWORD_BYTES: usize = QWORD_LENGTH + 8; + +/// Where one Address Space Descriptor keeps its granularity, minimum, +/// maximum, translation offset and length, each `width` bytes wide and in +/// that order from `first` (ACPI 6.5 Tables 6.47, 6.48, 6.49 and 6.50). +struct Layout { + first: usize, + width: usize, +} + +impl Layout { + const fn of(tag: u8) -> 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,13 +87,18 @@ 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 }, + /// 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, @@ -105,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"), } @@ -148,72 +176,81 @@ 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. +/// 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 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. /// /// **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); } 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); - } - if item.tag != QWORD_ADDRESS_SPACE { - return Err(ResourceError::UnknownTag { tag: item.tag }); - } - if item.len < QWORD_BYTES { - return Err(ResourceError::Short { tag: item.tag, whole: item.len, needed: QWORD_BYTES }); + offset += item.len; + let layout = match item.tag { + END_TAG => return Ok(RootResources { buses, windows: 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() }); } let kind = phys.byte(head + RESOURCE_TYPE as u64); if !matches!(kind, TYPE_MEMORY | TYPE_IO | TYPE_BUS) { return Err(ResourceError::UnknownResourceType { kind }); } - 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; - } + 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 kind == TYPE_IO || length == 0 { + continue; } - offset = through; + 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 }); + } + if phys.byte(head + GENERAL_FLAGS as u64) & CONSUMER != 0 { + return Err(ResourceError::Consumed { min }); + } + 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 ef121f11d95..04fdf962812 100644 --- a/lib/acpi/tests/resource.rs +++ b/lib/acpi/tests/resource.rs @@ -3,9 +3,9 @@ mod common; -use common::{qword_descriptor, resource_list as list, Machine, BUS, IO, MEMORY}; -use toyos_abi::boot::RootBridgeWindow; -use toyos_acpi::{io_ports, memory_windows, ResourceError, MAX_LIST_BYTES}; +use common::{address_space, qword_descriptor, resource_list as list, Machine, BUS, IO, MEMORY}; +use toyos_abi::acpi::RootBridgeWindow; +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) } @@ -90,9 +90,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)]); @@ -126,16 +126,87 @@ 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 }])); +} + +/// 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. +#[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/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/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index 304d9355ed7..905fddda050 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -29,6 +29,18 @@ //! ([`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 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 +//! 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 +65,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 +104,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 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; pub const TAKEN: u64 = 0; pub const PENDING: u64 = 1; @@ -224,6 +244,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 +277,8 @@ impl Refused { 21 => Self::HeldFunction, 22 => Self::AddressMap, 23 => Self::KernelRegister, + 24 => Self::NoRegisters, + 25 => Self::NoPorts, _ => return None, }) } @@ -291,3 +319,268 @@ 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 root bridges one declaration carries. +pub const MAX_BRIDGES: usize = 16; +/// 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 +/// 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, + /// 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. +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` + /// 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 offered: u32, + pub commit: u32, + pub committed: u32, + pub first_window: u32, + pub window_count: u32, + } +} + +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.offered) { + (OSC_ABSENT, 0) => Some(Osc::Absent), + (OSC_REFUSED, 0) => Some(Osc::Refused), + (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, + } + } +} + +crate::user_safe! { + /// 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 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, + 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, `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(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; + 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, + offered, + commit, + committed, + 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(..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) + } +} + +#[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)); + } + + /// 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 = |base| RootBridgeWindow { base, length: 0x1000 }; + let mut declared = Apertures::default(); + 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), 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, 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, None).expect("fits"); + } + 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, 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/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 6af98d26441..1615f9e8dbe 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 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 +/// 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..142cf481bfc 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 PCI root bridges + /// ([`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..a6452f7b5ed 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,19 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi host.notifies ); - 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}"); 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"), Err(Stopping) => { println!("{STOPPING}"); return loaded; @@ -223,7 +225,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 && !kernel.hardware_reduced() { toyos::error!("{NO_S5_HANDED}"); } if !host.refused.is_empty() { @@ -529,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/apertures.rs b/system/acpiserver/src/apertures.rs new file mode 100644 index 00000000000..c92d1d496ce --- /dev/null +++ b/system/acpiserver/src/apertures.rs @@ -0,0 +1,382 @@ +//! The PCI root bridges, declared to the kernel once ([`Kernel::declare`]): +//! each one's segment, buses and memory windows, and what its `_OSC` +//! 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 +//! 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 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 +//! 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 +//! 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 offer refused, and nothing +//! else's. + +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> { + 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 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"); + 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 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(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) +} + +/// 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::query_root(aml, &path) { + Ok(granted) => Osc::Offered(granted), + Err(why) => { + println!("{OWN}{path} was refused: {why}"); + Osc::Refused + } + } +} + +/// `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 root bridge declared, so every PCI claim is refused: {why}"); + Vec::new() + } + }; + let listed: Vec = + 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() => {} + 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::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 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, 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: &Root) -> Vec { + root.windows.clone() + } + + /// QEMU's `virt` (the measured case): its one root, `\_SB.PCI0` + /// (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_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.buses), (0, (0, 0xff))); + assert_eq!( + windows(root), + [ + RootBridgeWindow { base: 0x1000_0000, length: 0x2eff_0000 }, + RootBridgeWindow { base: 0x80_0000_0000, length: 0x80_0000_0000 }, + ] + ); + assert_eq!(root.osc, Osc::Offered(VIRT_OFFERED)); + } + + /// 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_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.buses), (0, (0, 0xff))); + assert_eq!( + 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, Osc::Offered(Q35_OFFERED)); + } + + /// 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_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, + /// 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, 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::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; + } + } + assert!(refused > 0 && refused < mutations, "{refused} of {mutations} refused"); + } + } + + /// 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 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()); + } + } + 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]); + sealed(b"DSDT", &crate::common::scope("\\_SB", &device("PCI0", &body))) + } + + /// A root's `_OSC` returning `answer`. + 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` 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_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. + 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!(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 + /// 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() { + 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"); + assert_eq!(*kernel.declared.borrow(), Some(Vec::new())); + } +} diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index f7b9dcca432..04de60c5710 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -145,9 +145,13 @@ 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; + /// 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 PCI root bridges, once ([`crate::apertures`]). + 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. @@ -162,6 +166,24 @@ 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, +} + +/// 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,10 @@ 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 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>, @@ -873,15 +899,29 @@ 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 hardware_reduced(&self) -> bool { + self.reduced + } + + fn declare(&self, roots: &[crate::apertures::Root]) -> Result { + self.stopping()?; + let mut declared = self.declared.borrow_mut(); + if declared.is_some() { + return Ok(Declared::Late); + } + *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/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..95f9b30ce13 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, 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,26 @@ 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), + 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, roots: &[apertures::Root]) -> Result { + let mut declared = Apertures::default(); + for root in roots { + 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), + Err(SyscallError::AlreadyExists) => Ok(Declared::Late), + answer => Self::answered("the root bridges' memory", answer).map(|()| Declared::Held), } } @@ -392,6 +417,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. diff --git a/system/acpiserver/src/osc.rs b/system/acpiserver/src/osc.rs index b81936421ea..f9c954665a2 100644 --- a/system/acpiserver/src/osc.rs +++ b/system/acpiserver/src/osc.rs @@ -1,7 +1,10 @@ //! `_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, 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`], //! [`PROCESSOR`]), each bit set only where ToyOS does what the bit promises, @@ -78,8 +81,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. @@ -113,16 +116,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 +136,43 @@ 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; + +/// 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])?; + if query & REFUSED != 0 { + println!("{OWN}{path}'s query answered {}", status(query)); + return Ok(0); + } + Ok(offered & ROOT_CONTROL) +} + /// What the negotiation answered, for the line the server says. #[derive(Default)] pub struct Negotiated { 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..3b1b97565e0 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 diff --git a/tests/common/claims.rs b/tests/common/claims.rs index 2968025c17f..fb5630fba83 100644 --- a/tests/common/claims.rs +++ b/tests/common/claims.rs @@ -125,34 +125,44 @@ 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: "; +/// 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: "; + +/// `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::, _>>()?; + .map(|w| w.strip_prefix("mem ").and_then(span).ok_or_else(|| format!("{w:?} in {declared:?}"))) + .collect() +} + +/// 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 mut domains = 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 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}" - )); + return Err(format!("{record:?} hands out addresses inside the root-bridge window {base:#x}..{end:#x}")); } domains += 1; } 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/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 7a29eab3b85..4aac77f8d79 100644 --- a/tests/images/clipboardcase.toml +++ b/tests/images/clipboardcase.toml @@ -13,7 +13,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] @@ -33,3 +33,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 87ea36cbf31..76f26325ac2 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 d5c1c78e27e..cff33ea48d5 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -392,6 +392,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. @@ -514,8 +521,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", @@ -2105,7 +2111,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", ]; @@ -2161,6 +2167,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 "]; @@ -2742,6 +2751,11 @@ serial: {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 ") 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 { return Err(format!("edu was never handed over serial: @@ -3886,6 +3900,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { claims::NOT_REMAPPED, ), "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(), @@ -4191,6 +4206,48 @@ 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(); + // 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}")); + } + 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 machine behind QEMU's AMD unit, which `said` describes, refuses the /// spare's claim for `why`, before a vector is armed or a BAR moved. fn usbd_refused_on_amdvi(test_config: &Path, profile: qemu::Profile, said: &str, why: &str) -> Result<(), String> { @@ -4224,6 +4281,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` + // 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> = @@ -4245,8 +4305,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, 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> = @@ -4267,6 +4327,8 @@ fn amdvi_translates(kernel: &serial::Serial) -> Result<(), String> { 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. + eprintln!(" [amdvi] {}", kernel.must_say(claims::DECLARED)?.trim()); for unit in units { eprintln!(" [amdvi] {}", unit.trim()); } @@ -5854,6 +5916,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")) {