diff --git a/issues/an-amd-vi-machine-isolates-no-device.md b/issues/an-amd-vi-machine-isolates-no-device.md new file mode 100644 index 00000000000..96d655ac898 --- /dev/null +++ b/issues/an-amd-vi-machine-isolates-no-device.md @@ -0,0 +1,39 @@ +--- +status: assigned +kind: track +opened: 2026-10-10 +--- + +# An AMD-Vi machine isolates no device + +Held by the orchestrator's AMD laptop track: its stage 5, amdvi-translate, builds +translation, and its stage 6, amdvi-intremap, interrupt remapping. + +On a machine whose IVRS describes AMD-Vi units, `kernel/src/arch/x86_64/amdvi/` +switches every unit off, so every device's DMA reaches all of memory and every +message-signalled interrupt is delivered as written. `DeviceSpace::create` +answers `Untranslated`, and a function a process would drive is refused its +claim (`iommu::remapping` is `NotRemapped`), so no userland driver runs there. + +What remains is translation, then interrupt remapping: + +- **Translation.** A device table blocking by default, every one of the 65,536 + entries `V=1, TV=1` with `IR=IW=0` — an entry left `V=0` passes its + requester's DMA untranslated — identity domains for kernel drivers, a + command buffer whose `COMPLETION_WAIT` is waited on under a `Tripwire`, and + the event log feeding `iommu::fault`. +- **Interrupt remapping.** Interrupt remapping tables, the I/O APIC's and the + HPET's requester ids from the IVRS's special entries, and `claim_msi`. + +Constraints a reader would otherwise pay to re-derive: + +- QEMU 11.1.1's `amd-iommu` translates a function only with `dma-remap=on`, + and only once an `INVALIDATE_DEVTAB_ENTRY` names it; it takes a device table + entry of `Mode` 0 as pass-through whatever `IR` and `IW` say, where the + specification blocks it (`hw/i386/amd_iommu.c` at v11.1.1). +- No AMD machine is in reach of the metal suite. + +**Exit:** a guest boot on `Profile::MetalAmdVi` in which a kernel driver's +DMA goes through a domain of its own, a DMA outside it is recorded as a +fault, and a claimed function's message is delivered through its own +remapping entry. diff --git a/issues/the-host-gate-aborts-with-no-message-when-a-destructor-panics-during-unwinding.md b/issues/the-host-gate-aborts-with-no-message-when-a-destructor-panics-during-unwinding.md new file mode 100644 index 00000000000..903f46d2223 --- /dev/null +++ b/issues/the-host-gate-aborts-with-no-message-when-a-destructor-panics-during-unwinding.md @@ -0,0 +1,43 @@ +--- +status: open +kind: tooling +opened: 2026-10-10 +--- + +# The host gate aborts with no message when a destructor panics during unwinding + +`cargo run -- --ci host > 2>&1` at `d242c7f05` (branch `wt/toyos-amdvi`) +exited 134. Its log stops mid-step, at the `failures:` header of the +`mutate-landed-child-retires-nothing` control, with no panic message and no +verdict. The rerun at the same head, nothing changed, exited 0. + +macOS crash reports show five processes aborting within 0.7 s of each other, +from three different binaries, each with +`SIGABRT` from `abort()`. In every one the faulting thread is +`core::panicking::panic_in_cleanup`, a destructor panicking while another +panic unwound, directly under: + +- `toyos_build::ci::host`, called from `ci::dispatch` and `main`: three + `toyos-build` processes, two of them one binary. +- `toyos_build::build::build_test_parts`, under `build_test_image`, + `common::qemu::build_boot_image_with`, `QemuInstance::boot_with_options` + and `virt_selftest`, on a `run_tasks` worker: one guest-suite process. +- `common::lane::Run::exit`, called from the guest suite's `main`: one + guest-suite process. + +Right after the abort the data volume holding every worktree, its `target/` +and the gate's redirected log was 99% full: 926 GiB, 887 GiB used, 12 GiB +available. Eight minutes later it read 82%, 169 GiB available. + +What reading shows, not yet measured: every one of those frames reaches a +`Drop` that writes to stderr (`build::Building`, `lane::Run`), and +`eprintln!` panics when its write fails. A full volume under a redirected +stderr is one way for all five to fail inside the same second. The trigger is +unconfirmed; the system log for that minute was not readable. + +Whatever the trigger, a gate that dies without saying why is a defect of its +own: a destructor reached while unwinding must not panic. + +**Exit:** a host test in which `Building`'s and `Run`'s destructors run during +an unwinding panic with stderr refusing every write, and the process exits non-zero by +its own exit code, never by `SIGABRT`. diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index 736b7f1f37c..da65626226b 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -126,9 +126,11 @@ actuators! { /// Withhold `VIRTIO_F_ACCESS_PLATFORM` from every virtio device but the console, staging a function whose addresses the unit never translates. virtio_no_access_platform = "virtio-no-access-platform"; - /// Leave every IOMMU unit queueing, translating and remapping through - /// tables of its own, as firmware may hand one over, just before this - /// kernel programs it. Judged by `iommu_firmware_left`. + /// Leave every IOMMU unit as firmware may hand one over, just before this + /// kernel takes it: a VT-d unit queueing, translating and remapping + /// through tables of its own, an AMD-Vi unit blocking every device's DMA + /// with its command buffer and event log running. Judged by + /// `iommu_firmware_left` and `amdvi_firmware_left`. iommu_firmware_left = "iommu-firmware-left"; /// Read the DMAR's flags with `INTR_REMAP` clear, as on a platform whose diff --git a/kernel/src/arch/x86_64/amdvi/mod.rs b/kernel/src/arch/x86_64/amdvi/mod.rs new file mode 100644 index 00000000000..71870b046d0 --- /dev/null +++ b/kernel/src/arch/x86_64/amdvi/mod.rs @@ -0,0 +1,351 @@ +//! AMD-Vi, the AMD I/O Virtualization Technology (IOMMU Specification 48882). +//! Every register layout and every AMD name of the unit is at or below this +//! module. +//! +//! Finds every unit the IVRS describes, logs what each was handed over with, +//! and switches off whatever firmware left on, each field confirmed before +//! the next, so DMA reaches memory untranslated. No unit is programmed: no +//! domain is given and no interrupt is remapped on an AMD-Vi machine +//! (`issues/an-amd-vi-machine-isolates-no-device.md`). + +use alloc::format; +use alloc::string::String; + +use crate::iommu::StreamId; +use crate::log; +use crate::mm::Mmio; +use crate::time::{Duration, Tripwire}; + +use super::iommu_unit::{register_window, yn}; +use toyos_acpi::{DeviceEntry, Features, Ivrs, IvrsBlock, IvrsRefused, Phys, Requesters, TableError, Uid, Unit}; + +/// A unit's register window: everything below the performance counters, +/// which nothing here reads. +const REGISTER_WINDOW: u64 = 0x4000; + +const EXTENDED_FEATURES: u64 = 0x0030; +const CONTROL: u64 = 0x0018; +const STATUS: u64 = 0x2020; + +const IOMMU_EN: u64 = 1 << 0; +const EVENT_LOG_EN: u64 = 1 << 2; +const CMD_BUF_EN: u64 = 1 << 12; + +/// `CONTROL` fields firmware may leave on, in Linux's `iommu_disable` order, +/// each with the `STATUS` bit that says it stopped, or `None` where `CONTROL` +/// reading back is the answer: the logs and their interrupts before the unit, +/// which logs nothing once off. +const FIRMWARE_LEFT: [(u64, Option, &str); 8] = [ + (CMD_BUF_EN, Some(1 << 4), "CmdBufEn"), + (1 << 3, None, "EventIntEn"), + (EVENT_LOG_EN, Some(1 << 3), "EventLogEn"), + (1 << 28, Some(1 << 8), "GALogEn"), + (1 << 29, None, "GAIntEn"), + (1 << 13, Some(1 << 7), "PPRLogEn"), + (1 << 14, None, "PPRIntEn"), + (IOMMU_EN, None, "IommuEn"), +]; + +/// How long a `CONTROL` write is given to show before the kernel panics: a +/// unit that will not stop is one whose reach nothing can state. +const COMMAND_TIMEOUT: Tripwire = Tripwire::absurd( + Duration::from_secs(1), + "a unit that does not stop within a second of being told to is one whose reach nothing can state", +); + +/// Every unit the IVRS describes, switched off; `false` where firmware +/// published no IVRS. +pub fn init(rsdp_addr: u64) -> bool { + let ivrs = match toyos_acpi::ivrs(crate::drivers::acpi::direct_phys(), rsdp_addr) { + Ok(ivrs) => ivrs, + Err(IvrsRefused::Table(TableError::Absent)) => return false, + Err(e) => { + log!("iommu: IVRS unusable: {e:?} — no unit is found, and one firmware left on stays on"); + return true; + } + }; + let info = ivrs.info(); + log!( + "iommu: IVRS rev={} ivinfo={:#010x} efr_images={} preboot_dma_protection={} pa={} va={}", + ivrs.revision(), + info.raw, + yn(info.efr_images), + yn(info.dma_remap), + info.physical_bits, + info.virtual_bits, + ); + let (mut units, mut off) = (0usize, 0usize); + for block in ivrs.blocks() { + match block { + IvrsBlock::Unit(unit) => { + describe(units, &unit); + for entry in ivrs.devices(&unit) { + match entry { + Ok(entry) => log!("iommu: amdvi unit{units} serves {}", served(&ivrs, &entry)), + Err(e) => log!("iommu: amdvi unit{units} device entries refused: {e:?}"), + } + } + if let Some(regs) = present(units, &unit) { + #[cfg(feature = "boot-actuators")] + if crate::actuator::iommu_firmware_left() { + leave_on(regs, &ivrs, &unit); + } + switch_off(units, &unit, regs); + off += 1; + } + units += 1; + } + IvrsBlock::Superseded(unit) => log!( + "iommu: IVRS IVHD {:#04x} at +{} describes {} again, which is read at its higher type", + unit.kind, + unit.at, + requester(unit.device) + ), + IvrsBlock::Memory(m) => { + let requesters = match m.requesters { + Requesters::All => String::from("every requester"), + Requesters::One(id) => format!("{}", requester(id)), + Requesters::Range { first, last } => format!("{}..={}", requester(first), requester(last)), + }; + log!( + "iommu: IVRS IVMD seg={} {requesters} {:#018x}+{:#x} unity={} r={} w={} exclusion={}", + m.segment, + m.start, + m.length, + yn(m.flags & 1 != 0), + yn(m.flags & 2 != 0), + yn(m.flags & 4 != 0), + yn(m.flags & 8 != 0), + ); + } + IvrsBlock::Other { kind, at, len } => { + log!("iommu: IVRS block type {kind:#04x} at +{at}, {len} bytes — not used by this kernel") + } + } + } + log!( + "iommu: AMD-Vi isolates no device this boot: {units} unit(s) described, {off} switched off, so DMA \ + behind those reaches memory untranslated" + ); + true +} + +/// A requester id, printed as `pci::enumerate` prints a function. +fn requester(id: u16) -> StreamId { + let [devfn, bus] = id.to_le_bytes(); + StreamId::pci(bus, devfn >> 3, devfn & 7) +} + +fn describe(index: usize, unit: &Unit) { + let image = match unit.features { + Features::Reported(word) => format!("features={word:#010x}"), + Features::Image { attributes, efr, efr2 } => { + format!("attributes={attributes:#010x} efr_image={efr:#018x} efr2_image={efr2:#018x}") + } + }; + log!( + "iommu: amdvi unit{index} @{:#x} seg={} dev={} ivhd={:#04x} flags={:#04x} cap={:#x} info={:#06x} {image}", + unit.base, + unit.segment, + requester(unit.device), + unit.kind, + unit.flags, + unit.capability, + unit.info, + ); +} + +/// What one device entry names, and its DTE setting byte. +fn served(ivrs: &Ivrs, entry: &DeviceEntry) -> String { + let id = requester; + match *entry { + DeviceEntry::All { data } => format!("every requester data={data:#04x}"), + DeviceEntry::Select { id: one, data } => format!("{} data={data:#04x}", id(one)), + DeviceEntry::Range { first, last, data } => format!("{}..={} data={data:#04x}", id(first), id(last)), + DeviceEntry::Alias { id: one, used, data } => format!("{} as {} data={data:#04x}", id(one), id(used)), + DeviceEntry::AliasRange { first, last, used, data } => { + format!("{}..={} as {} data={data:#04x}", id(first), id(last), id(used)) + } + DeviceEntry::Extended { id: one, data, extended } => { + format!("{} data={data:#04x} extended={extended:#010x}", id(one)) + } + DeviceEntry::ExtendedRange { first, last, data, extended } => { + format!("{}..={} data={data:#04x} extended={extended:#010x}", id(first), id(last)) + } + DeviceEntry::Special { handle, used, variety, data } => { + format!("special variety={variety} handle={handle} as {} data={data:#04x}", id(used)) + } + DeviceEntry::Hid { id: one, data, hid, cid, uid } => { + let text = |bytes: &[u8]| String::from_utf8_lossy(bytes).trim_end_matches('\0').into(); + let uid: String = match uid { + Uid::Absent => String::from("none"), + Uid::Integer(value) => format!("{value}"), + Uid::String { at, len } => text(&ivrs.bytes(at, len).collect::>()), + }; + let (hid, cid): (String, String) = (text(&hid), text(&cid)); + format!("acpi {hid:?} cid={cid:?} uid={uid:?} as {} data={data:#04x}", id(one)) + } + DeviceEntry::Other(kind) => format!("an entry of type {kind:#04x} not used by this kernel"), + } +} + +/// The unit's registers, or `None`, said, where its base is refused or its +/// window does not decode. +fn present(index: usize, unit: &Unit) -> Option { + let Some(regs) = register_window(unit.base, REGISTER_WINDOW) else { + log!( + "iommu: amdvi unit{index} register base {:#x} is not a {REGISTER_WINDOW:#x}-aligned physical \ + address — not mapped, and whatever firmware left on stays on", + unit.base + ); + return None; + }; + // A described unit whose window does not decode: firmware bug, or the unit is powered down. + if regs.read_u32(STATUS) == u32::MAX { + log!( + "iommu: amdvi unit{index} @{:#x}: STATUS reads all ones, the unit is described but not present, \ + and whatever firmware left on stays on", + unit.base + ); + return None; + } + Some(regs) +} + +/// Spins until `done`, panicking with what `stuck` says once +/// [`COMMAND_TIMEOUT`] has passed. +fn spin_until(mut done: impl FnMut() -> bool, stuck: impl Fn() -> String) { + let deadline = crate::clock::nanos_since_boot() + COMMAND_TIMEOUT.nanos(); + while !done() { + assert!(crate::clock::nanos_since_boot() < deadline, "{}", stuck()); + core::hint::spin_loop(); + } +} + +/// Every field of [`FIRMWARE_LEFT`] that is on, off, each confirmed before +/// the next. +fn switch_off(index: usize, unit: &Unit, regs: Mmio) { + log!( + "iommu: amdvi unit{index} handed over control={:#018x} status={:#010x} efr={:#018x}", + regs.read_u64(CONTROL), + regs.read_u32(STATUS), + regs.read_u64(EXTENDED_FEATURES), + ); + for (bit, run, what) in FIRMWARE_LEFT { + if regs.read_u64(CONTROL) & bit == 0 { + continue; + } + log!("iommu: amdvi unit{index} was handed over with {what} on; it goes off"); + regs.write_u64(CONTROL, regs.read_u64(CONTROL) & !bit); + let stopped = || match run { + Some(run) => regs.read_u32(STATUS) & run == 0, + None => regs.read_u64(CONTROL) & bit == 0, + }; + spin_until(stopped, || { + format!( + "iommu: amdvi unit{index} never stopped {what}: CONTROL={:#018x} STATUS={:#010x}", + regs.read_u64(CONTROL), + regs.read_u32(STATUS) + ) + }); + } + log!( + "iommu: amdvi unit{index} @{:#x} switched off control={:#018x} status={:#010x}", + unit.base, + regs.read_u64(CONTROL), + regs.read_u32(STATUS) + ); +} + +/// [`crate::actuator::iommu_firmware_left`]: the unit left on as firmware +/// protecting memory before the operating system runs may leave it — every +/// requester's device table entry translating through a table that maps +/// nothing, so every DMA is blocked, with its command buffer and event log +/// running. The entry of every requester the unit's IVHD names is +/// invalidated, which is what a unit reads a changed entry on, and a +/// completion wait says each was. +/// +/// The tables are leaked, as firmware's are its own: the unit reads them +/// until it is switched off, and freed they could read back as an entry that +/// passes DMA untranslated. +#[cfg(feature = "boot-actuators")] +fn leave_on(regs: Mmio, ivrs: &Ivrs, unit: &Unit) { + use crate::mm::{pmm, DirectMap}; + + const DEVICE_TABLE: u64 = 0x0000; + const COMMAND_BASE: u64 = 0x0008; + const EVENT_BASE: u64 = 0x0010; + const COMMAND_HEAD: u64 = 0x2000; + const COMMAND_TAIL: u64 = 0x2008; + const EVENT_HEAD: u64 = 0x2010; + const EVENT_TAIL: u64 = 0x2018; + /// 2^8 entries of 16 bytes, 4 KiB, in bits 59:56 of either base. + const LOG_LEN: u64 = 8 << 56; + const RING: usize = 1 << 8; + const DEVICE_IDS: u64 = 1 << 16; + const ENTRY: u64 = 32; + /// `V`, `TV`, and a four-level table: `Mode` 100b at 11:9; `IR` and `IW` + /// clear. + const BLOCKED: u64 = 1 | (1 << 1) | (4 << 9); + const INVALIDATE_ENTRY: u64 = 2 << 60; + /// The opcode, and `S`: store the second word at the address. + const COMPLETION_WAIT: u64 = (1 << 60) | 1; + const DONE: u64 = 0x600d_f00d; + + let window = |phys: u64, bytes: u64| { + // SAFETY: `phys` is inside a page `pmm::alloc_page` handed out and + // this function leaks, which the direct map covers for the machine's life. + unsafe { Mmio::over_phys(DirectMap::from_phys(phys), bytes) } + }; + let table = pmm::alloc_page().expect("amdvi: no memory for the device table the actuator leaves on"); + let rest = pmm::alloc_page().expect("amdvi: no memory for the logs the actuator leaves on"); + let table_at = table.direct_map().phys(); + let at = rest.direct_map().phys(); + let (commands_at, events_at, root_at, done_at) = (at, at + 0x1000, at + 0x2000, at + 0x3000); + #[expect(clippy::disallowed_methods, reason = "the unit reads them until it is switched off, as firmware's own tables")] + core::mem::forget((table, rest)); + + let entries = window(table_at, DEVICE_IDS * ENTRY); + for id in 0..DEVICE_IDS { + entries.write_u64(id * ENTRY, BLOCKED | root_at); + entries.write_u64(id * ENTRY + 8, 1); + } + regs.write_u64(DEVICE_TABLE, table_at | (DEVICE_IDS * ENTRY / 0x1000 - 1)); + regs.write_u64(COMMAND_BASE, commands_at | LOG_LEN); + regs.write_u64(EVENT_BASE, events_at | LOG_LEN); + for at in [COMMAND_HEAD, COMMAND_TAIL, EVENT_HEAD, EVENT_TAIL] { + regs.write_u64(at, 0); + } + regs.write_u64(CONTROL, regs.read_u64(CONTROL) | CMD_BUF_EN | EVENT_LOG_EN | IOMMU_EN); + + let commands = window(commands_at, RING as u64 * 16); + let mut tail = 0; + let mut push = |low: u64, high: u64| { + // A ring holds one entry fewer than it has. + assert!(tail < (RING as u64 - 1) * 16, "amdvi: the unit's requesters overflow the actuator's {RING}-entry ring"); + commands.write_u64(tail, low); + commands.write_u64(tail + 8, high); + tail += 16; + }; + for entry in ivrs.devices(unit) { + let (first, last) = match entry.expect("amdvi: the actuator's unit has a device list it can walk") { + DeviceEntry::All { .. } => (0, u16::MAX), + DeviceEntry::Select { id, .. } | DeviceEntry::Extended { id, .. } | DeviceEntry::Hid { id, .. } => (id, id), + DeviceEntry::Range { first, last, .. } | DeviceEntry::ExtendedRange { first, last, .. } => (first, last), + DeviceEntry::Alias { used, .. } + | DeviceEntry::AliasRange { used, .. } + | DeviceEntry::Special { used, .. } => (used, used), + DeviceEntry::Other(_) => continue, + }; + for id in first..=last { + push(INVALIDATE_ENTRY | u64::from(id), 0); + } + } + push(COMPLETION_WAIT | done_at, DONE); + regs.write_u64(COMMAND_TAIL, tail); + let done = window(done_at, 8); + spin_until( + || done.read_u64(0) == DONE, + || format!("amdvi: the actuator's completion wait never landed: STATUS={:#010x}", regs.read_u32(STATUS)), + ); +} diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 23ce8b61ea2..616a43dc160 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -39,8 +39,8 @@ pub unsafe extern "C" fn _start(_kernel_args: &KernelArgs) -> ! { /// refusal has a channel to reach. /// The ACPI tables this architecture decodes: the MADT for its CPUs and I/O /// APICs, the FADT for reset, the PM1a control block and the century register, the HPET for -/// the clock, the MCFG for ECAM and the DMAR for the IOMMU. -pub const ACPI_TABLES: &[&[u8; 4]] = &[b"APIC", b"FACP", b"HPET", b"MCFG", b"DMAR"]; +/// the clock, the MCFG for ECAM and the DMAR or IVRS for the IOMMU. +pub const ACPI_TABLES: &[&[u8; 4]] = &[b"APIC", b"FACP", b"HPET", b"MCFG", b"DMAR", b"IVRS"]; pub fn before_panel() { pat::init(); diff --git a/kernel/src/arch/x86_64/iommu_unit.rs b/kernel/src/arch/x86_64/iommu_unit.rs new file mode 100644 index 00000000000..987ba7edeee --- /dev/null +++ b/kernel/src/arch/x86_64/iommu_unit.rs @@ -0,0 +1,54 @@ +//! The x86-64 IOMMU: VT-d where firmware publishes a DMAR, AMD-Vi where it +//! publishes an IVRS. +//! +//! Only VT-d gives a domain or remaps an interrupt. AMD-Vi programs no unit, +//! so on its machines VT-d holds none: every [`domain`] is refused `NoUnit` +//! and no [`interrupt`] is armed +//! (`issues/an-amd-vi-machine-isolates-no-device.md`). + +use crate::drivers::pci::PciDevice; +use crate::log; +use crate::mm::policy::MmioPolicy; +use crate::mm::Mmio; +use toyos_abi::boot::RootBridgeWindow; + +pub use super::vtd::{domain, fault, interrupt}; + +/// x86-64's 52-bit physical-address ceiling: a register base at or above this +/// is not an address at all, and would otherwise wrap `DirectMap::as_ptr`'s +/// unchecked offset into the user half. +const MAX_PHYS: u64 = 1 << 52; + +/// The units of whichever table firmware published. +pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow]) { + if super::vtd::init(rsdp_addr, devices, windows) || super::amdvi::init(rsdp_addr) { + return; + } + // ACPI cannot tell "no IOMMU silicon" from "the IOMMU disabled in + // firmware"; the line names both rather than guess. + log!( + "iommu: no DMAR or IVRS table — this platform has no IOMMU, or it is disabled in firmware setup \ + (look for \"VT-d\", \"AMD-Vi\" or \"IOMMU\")" + ); +} + +/// Firmware's register base, mapped only if aligned to its `len`-byte window +/// and within the physical range — never clamped to fit, since a base in +/// usable RAM would decode as plausible registers until a write lands in +/// somebody's heap. +pub(super) fn register_window(base: u64, len: u64) -> Option { + if base == 0 || !base.is_multiple_of(len) || base >= MAX_PHYS { + return None; + } + Some(crate::mm::paging::map_mmio(base, len, MmioPolicy::Uncacheable)) +} + +/// One character per boolean; `n` is printed rather than omitted, since an +/// absent field would look like a forgotten one. +pub(super) fn yn(v: bool) -> char { + if v { + 'y' + } else { + 'n' + } +} diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index c1fe5528e4c..f7e45b8dbb6 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -6,10 +6,11 @@ //! //! The PC platform's own devices live here too, because no other architecture //! has them: the i8042, the I/O APIC, the CMOS RTC, the chipset's TCO -//! watchdog and VT-d. Generic code reaches each through the concept it serves +//! watchdog, VT-d and AMD-Vi. Generic code reaches each through the concept it serves //! (`keyboard_controller`, `watchdog`, `iommu_unit`, …), never by its name. pub mod acpi_mode; +pub mod amdvi; pub mod apic; pub mod barrier; pub mod boot; @@ -25,6 +26,7 @@ pub mod hpet; pub mod hw; pub mod i8042; pub mod idt; +pub mod iommu_unit; pub mod ioapic; pub mod mtrr; pub mod paging; @@ -45,8 +47,7 @@ pub mod watchdog; pub use apic as irqchip; pub use i8042 as keyboard_controller; pub use idt as trap; -pub use vtd as iommu_unit; -pub use vtd::interrupt as message_unit; +pub use iommu_unit::interrupt as message_unit; pub use apic::MSI_DOORBELL; pub use idt::DriverIrq; diff --git a/kernel/src/arch/x86_64/vtd/mod.rs b/kernel/src/arch/x86_64/vtd/mod.rs index df574044349..975cdd92b13 100644 --- a/kernel/src/arch/x86_64/vtd/mod.rs +++ b/kernel/src/arch/x86_64/vtd/mod.rs @@ -22,11 +22,11 @@ use crate::drivers::acpi::TableError; use crate::drivers::pci::PciDevice; use toyos_abi::boot::RootBridgeWindow; use crate::iommu::{AddressWidth, StreamId}; -use crate::mm::policy::MmioPolicy; use crate::mm::Mmio; use crate::sync::Lock; use crate::time::{Duration, Tripwire}; +use super::iommu_unit::yn; use dmar::{Dmar, Malformed, Scope, Scopes, Structure}; use queue::Queue; use table::{Table, Tables}; @@ -78,11 +78,6 @@ const COMMAND_TIMEOUT: Tripwire = Tripwire::absurd( "a unit half-way through being enabled is a unit whose reach nothing can state", ); -/// x86-64's 52-bit physical-address ceiling: a register base at or above this -/// is not an address at all, and would otherwise wrap `DirectMap::as_ptr`'s -/// unchecked offset into the user half. -const MAX_PHYS: u64 = 1 << 52; - /// Ceiling on units inventoried; a machine past it is told, not silently truncated. pub(super) const MAX_UNITS: usize = 16; @@ -125,23 +120,14 @@ pub(super) fn invalidate_interrupt_entries() { } /// `windows` and every region firmware reserved are what no domain's addresses -/// reach. -pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow]) { +/// reach. `false` where firmware published no DMAR. +pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow]) -> bool { let dmar = match Dmar::open(rsdp_addr) { Ok(dmar) => dmar, - // ACPI cannot distinguish "no VT-d silicon" from "VT-d disabled in - // firmware"; the line names both rather than guess. - Err(TableError::Absent) => { - log!( - "iommu: no DMAR table — this platform has no IOMMU, or VT-d is disabled in \ - firmware setup (look for \"VT-d\" / \"Intel Virtualization Technology for \ - Directed I/O\")" - ); - return; - } + Err(TableError::Absent) => return false, Err(e) => { log!("iommu: DMAR unusable: {e:?} — this machine has no IOMMU the kernel can use"); - return; + return true; } }; @@ -252,6 +238,7 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[RootBridgeWindow]) for (unit, plan) in ready { enable(unit, plan, devices, &mut domains, remap); } + true } /// Whether every interrupt source may be moved to the remappable format, and @@ -388,22 +375,9 @@ impl Unit { } } -/// Firmware's register base, mapped only if 4 KiB-aligned and within the -/// physical range — never clamped to fit, since a base in usable RAM would -/// decode as a plausible capability register until a write lands in -/// somebody's heap. -fn window(base: u64) -> Option { - if base == 0 || !base.is_multiple_of(REGISTER_WINDOW) || base >= MAX_PHYS { - return None; - } - Some( - crate::mm::paging::map_mmio(base, REGISTER_WINDOW, MmioPolicy::Uncacheable), - ) -} - fn describe_unit(index: usize, drhd: &dmar::Drhd) -> Option { let base = drhd.register_base(); - let Some(regs) = window(base) else { + let Some(regs) = super::iommu_unit::register_window(base, REGISTER_WINDOW) else { log!( "iommu: unit{index} register base {base:#x} is not a 4 KiB-aligned physical address \ — not mapped" @@ -747,13 +721,3 @@ impl Capabilities { self.ecap & (1 << 6) != 0 } } - -/// One character per boolean; `n` is printed rather than omitted, since an -/// absent field would look like a forgotten one. -fn yn(v: bool) -> char { - if v { - 'y' - } else { - 'n' - } -} diff --git a/kernel/src/iommu/mod.rs b/kernel/src/iommu/mod.rs index 667d2e086c0..f6afe2a5ecc 100644 --- a/kernel/src/iommu/mod.rs +++ b/kernel/src/iommu/mod.rs @@ -266,8 +266,6 @@ impl core::fmt::Display for StreamId { /// The device list must be the complete enumeration: enabling translation with an unenumerated device left off it can brick the machine's own boot disk. /// /// No domain's addresses reach into one of `windows`, the memory firmware says the root bridges decode. -/// -/// Calls `unit::init` directly rather than through a dispatch, because x86-64 has one backend and the dispatch is not yet a real seam. pub fn init( rsdp_addr: u64, devices: &[crate::drivers::pci::PciDevice], diff --git a/src/licence.rs b/src/licence.rs index b6cdb8ef377..619a7a37a83 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -301,6 +301,15 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "ours: QEMU's own XSDT, captured by the commit that added toyos-acpi", Terms::Spdx("MIT OR Apache-2.0"), ), + // The IVRS QEMU 11.1.1 published to a q35 with its AMD IOMMU, read out + // of guest physical memory with no operating system in the guest: + // `toyos-acpi/tests/ivrs.rs` decodes it. + ( + "toyos-acpi/fixtures/qemu-11.1.1-amdvi/ivrs.bin", + "c6d3f24592e7e69d5f3bcf4cad0a2cda57d93432d9fd6d2eb499fd289e6cc49d", + "ours: QEMU's own IVRS, captured by the commit that decodes it", + Terms::Spdx("MIT OR Apache-2.0"), + ), // The tables QEMU 11.1.1 published to its `virt` machine in both of its // GIC modes, read out of guest physical memory over the monitor with no // operating system in the guest. Firmware output, as the q35 tables above: diff --git a/tests/common/iommu.rs b/tests/common/iommu.rs index 07915f7fa4f..49aefa8175a 100644 --- a/tests/common/iommu.rs +++ b/tests/common/iommu.rs @@ -1,7 +1,8 @@ //! `iommu_virtio_platform`: whether each virtio function QEMU creates behind //! its emulated VT-d unit, and none created without one, negotiated //! `VIRTIO_F_ACCESS_PLATFORM`, and that a process's claim on one is refused -//! where no unit is. +//! where no unit is. `amdvi_firmware_left`: whether an AMD-Vi unit firmware +//! left blocking is switched off before the boot stick is driven. use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; @@ -424,3 +425,69 @@ fn class_function(log: &Serial, class: &str) -> Option { } found } + +/// What the kernel says of the one unit QEMU's IVRS describes, as an 11h over +/// a superseded 10h (`toyos-acpi/tests/ivrs.rs` decodes the same table). +const AMDVI_UNIT: &str = "iommu: amdvi unit0 @0xfed80000 seg=0 dev=00:02.0 ivhd=0x11"; +const AMDVI_SUPERSEDED: &str = "iommu: IVRS IVHD 0x10 at +48 describes 00:02.0 again"; +/// The xHCI the boot stick is on, among the functions the unit serves. +const AMDVI_SERVES_XHCI: &str = "iommu: amdvi unit0 serves 00:03.0 data=0x00"; +const AMDVI_OFF: &str = "iommu: amdvi unit0 @0xfed80000 switched off control=0x0000000000000000"; +const AMDVI_NO_ISOLATION: &str = "iommu: AMD-Vi isolates no device this boot: 1 unit(s) described, 1 switched off"; +/// A kernel driver's domain refused for want of a unit that translates. +const NO_DOMAIN: &str = "iommu: no domain of its own for a device: no unit on this machine translates"; +/// The USB gate: the boot stick bound through the controller behind the unit, +/// which only DMA the unit lets through can do. +const STICK_BOUND: &str = "usb-storage: 1 device(s)"; + +/// **A unit an AMD machine's firmware left on is switched off, and the boot +/// stick's DMA reaches memory.** QEMU's `amd-iommu` is the only AMD-Vi unit +/// in reach: the T14 is an Intel machine, and the hand-over's register +/// sequence and the DMA it frees are neither of them a host test's. Two +/// boots: the shipped kernel finds a unit nothing left on and switches +/// nothing; the test kernel's `iommu-firmware-left` leaves the unit blocking +/// every device's DMA with its command buffer and event log running, and +/// each goes off by its own write before the stick is driven. With the +/// switch-off reverted that boot's stick never binds. +pub fn amdvi_firmware_left(test_config: &Path) -> Result<(), String> { + let profile = Profile::MetalAmdVi; + let argv = qemu::profile_argv(&BootOptions { profile, ..Default::default() }); + let devices: Vec<&str> = argv.windows(2).filter(|w| w[0] == "-device").map(|w| w[1].as_str()).collect(); + if devices.first() != Some(&"amd-iommu,intremap=off,dma-remap=on") { + return Err(format!( + "the AMD unit is not the first -device of {devices:?}, so a function ahead of it bypasses it" + )); + } + for (kernel_params, left) in [(&[][..], false), (&["iommu-firmware-left"][..], true)] { + let qemu = QemuInstance::boot_with_options(test_config, &[], &[], BootOptions { profile, kernel_params, ..Default::default() }); + let log = Serial::boot(&qemu); + log.must_be_clean()?; + log.must_say("Boot: complete")?; + log.must_say(STICK_BOUND)?; + log.must_say("iommu: IVRS rev=1 ivinfo=0x00002801 efr_images=y preboot_dma_protection=n pa=40 va=0")?; + log.must_say(AMDVI_UNIT)?; + log.must_say(AMDVI_SUPERSEDED)?; + log.must_say(AMDVI_SERVES_XHCI)?; + log.must_say(AMDVI_OFF)?; + log.must_say(AMDVI_NO_ISOLATION)?; + log.must_say(NO_DOMAIN)?; + log.must_not_say("no DMAR or IVRS table")?; + let handed = log.must_say("iommu: amdvi unit0 handed over control=")?; + if left { + // `CmdBufEn`, `EventLogEn` and `IommuEn`, as the actuator wrote them. + if !handed.contains("control=0x0000000000001005") { + return Err(format!("the actuator's unit was handed over as {handed:?}")); + } + for field in ["CmdBufEn", "EventLogEn", "IommuEn"] { + log.must_say(&format!("iommu: amdvi unit0 was handed over with {field} on; it goes off"))?; + } + for field in ["EventIntEn", "GALogEn", "GAIntEn", "PPRLogEn", "PPRIntEn"] { + log.must_not_say(&format!("with {field} on"))?; + } + } else { + log.must_not_say("was handed over with")?; + } + eprintln!(" [amdvi] left on = {left}: {handed}; the stick bound behind the unit"); + } + Ok(()) +} diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index f539f39b59b..eba753b9345 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -712,6 +712,10 @@ pub enum Profile { /// ===TEST_START=== protocol like any other. [`BootOptions::mute`] takes /// it away for the one test that certifies the T14's literal shape. Metal, + /// [`Profile::Metal`] with QEMU's AMD unit in place of VT-d: the boot + /// stick's controller behind an AMD-Vi unit, which the kernel finds by the + /// IVRS and switches off. + MetalAmdVi, /// QEMU `virt` on AArch64 (GICv3, AAVMF): a GOP from `ramfb`, the boot /// stick on an xHCI, the PL011, a virtio-rng for firmware's /// `EFI_RNG_PROTOCOL`, and nothing else — no NIC, NVMe or IOMMU. The @@ -759,7 +763,8 @@ impl Profile { | Self::HeadlessNoUsb | Self::HeadlessVirtioGpu | Self::HeadlessUsbSpare - | Self::Metal => Arch::X86_64, + | Self::Metal + | Self::MetalAmdVi => Arch::X86_64, } } @@ -809,6 +814,18 @@ pub struct Iommu { /// width QEMU offers and interrupt remapping on. pub const IOMMU_DEFAULT: Iommu = Iommu { aw_bits: 48, intremap: true, eim: false }; +/// Which vendor's unit a machine has. +#[derive(Clone, Copy, PartialEq)] +pub enum Unit { + /// `intel-iommu`, with the capabilities it is staged with. + Vtd(Iommu), + /// `amd-iommu`, interrupt remapping off, which needs no split irqchip, + /// and `dma-remap` on: without it QEMU 11.1.1 passes every function's DMA + /// untranslated whatever the unit is told (`hw/i386/amd_iommu.c:1165` + /// and `:1310` at v11.1.1), and a unit left blocking would block nothing. + AmdVi, +} + /// The controller every profile but [`Profile::MetalUsb`] gets. `nec-usb-xhci` /// registers `MAX(p2, p3)` attachable USB ports over `p2 + p3` port registers — /// the two ranges are two speed-specific views of the same ports, not two sets @@ -886,7 +903,7 @@ struct Shape { /// The unit that decodes this machine's DMA, or its absence. Stated per /// profile because absence is a shape and because the unit's own /// capabilities are what the kernel reads at boot. - iommu: Option, + iommu: Option, /// `virt`'s SMMUv3, which is a machine property rather than a device. smmu: Smmu, /// A virtio-rng, which is firmware's alone: edk2's driver puts @@ -983,7 +1000,7 @@ impl Profile { usb: &["usb-kbd,bus=xhci.0"], blockdevs: &[], storage: Storage::Disk { data_bytes: NVME_SMALL }, - iommu: Some(IOMMU_DEFAULT), + iommu: Some(Unit::Vtd(IOMMU_DEFAULT)), smmu: Smmu::Absent, rng: false, virtio_gpu: false, @@ -1001,11 +1018,12 @@ impl Profile { usb: &[], blockdevs: &[], storage: Storage::Stick { nvme_bytes: NVME_SMALL }, - iommu: Some(IOMMU_DEFAULT), + iommu: Some(Unit::Vtd(IOMMU_DEFAULT)), smmu: Smmu::Absent, rng: false, virtio_gpu: false, }, + Self::MetalAmdVi => Shape { iommu: Some(Unit::AmdVi), ..Self::Metal.shape() }, Self::HeadlessNoIommu => Shape { iommu: None, ..Self::Headless.shape() }, Self::HeadlessE1000e => Shape { nic: Nic::E1000e, ..Self::Headless.shape() }, Self::HeadlessNoUsb => Shape { xhci: &[], usb: &[], ..Self::Headless.shape() }, @@ -1024,7 +1042,7 @@ impl Profile { } /// The unit this profile puts on the machine, or `None`. - pub fn iommu(self) -> Option { + pub fn iommu(self) -> Option { self.shape().iommu } } @@ -2149,17 +2167,18 @@ fn qemu_command( // is the option that does it, and it leaves i8042/ps2-kbd/ps2-mouse alone. qemu.arg("-nodefaults"); - // `kernel-irqchip=split` only when there is a unit: interrupt remapping - // needs the userspace half of the irqchip, and a machine with no unit has - // no reason to be built differently from the one it has always been. + // `kernel-irqchip=split` only for VT-d: its interrupt remapping needs the + // userspace half of the irqchip, AMD-Vi is staged with none, and a machine + // with no unit has no reason to be built differently from the one it has + // always been. let mut machine = match arch { Arch::X86_64 => { assert!(shape.smmu == Smmu::Absent, "a q35 has no SMMUv3"); arch.machine().to_string() } Arch::Aarch64 => { - // The unit a profile declares is VT-d, which `virt` has none of. - assert!(shape.iommu.is_none(), "`virt` has no VT-d"); + // A unit a profile declares is VT-d or AMD-Vi, which `virt` has none of. + assert!(shape.iommu.is_none(), "`virt` has no VT-d or AMD-Vi"); let machine = match options.profile { Profile::VirtEl2 | Profile::VirtEl2NoVhe => { format!("{},gic-version=3,virtualization=on", arch.machine()) @@ -2173,7 +2192,7 @@ fn qemu_command( } } }; - if shape.iommu.is_some() { + if let Some(Unit::Vtd(_)) = shape.iommu { machine.push_str(",kernel-irqchip=split"); } @@ -2208,13 +2227,19 @@ fn qemu_command( // address space unless the unit exists when the function is created, so a // unit emitted after the devices it is meant to decode is a unit that // decodes nothing — the vacuity trap, in its harness-side form. - if let Some(unit) = shape.iommu { - qemu.arg("-device").arg(format!( - "intel-iommu,intremap={},caching-mode=on,aw-bits={},eim={}", - if unit.intremap { "on" } else { "off" }, - unit.aw_bits, - if unit.eim { "on" } else { "off" } - )); + match shape.iommu { + Some(Unit::Vtd(unit)) => { + qemu.arg("-device").arg(format!( + "intel-iommu,intremap={},caching-mode=on,aw-bits={},eim={}", + if unit.intremap { "on" } else { "off" }, + unit.aw_bits, + if unit.eim { "on" } else { "off" } + )); + } + Some(Unit::AmdVi) => { + qemu.arg("-device").arg("amd-iommu,intremap=off,dma-remap=on"); + } + None => {} } // A virtio function reaches memory through `vdev->dma_as`, the machine's own // address space with the unit bypassed, unless it is created with this — the diff --git a/tests/toyos.rs b/tests/toyos.rs index 5774e30e097..4750248eb56 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -325,6 +325,11 @@ const MACHINE_TESTS: &[&str] = &[ // behind its emulated VT-d unit and without one: no shipped machine has a // virtio function, so only a QEMU machine can be asked. "iommu_virtio_platform", + // An AMD-Vi unit left blocking every device, switched off before the boot + // stick is driven behind it: the T14 is an Intel machine, no AMD one is + // in reach, and the register sequence and the DMA it frees are no host + // test's. + "amdvi_firmware_left", // netstack's own state behind a real stack and a peer that ends its // connections: netstack is one binary that owns its NIC, with no host // build, and the T14's peer is the bench's network. @@ -3906,6 +3911,7 @@ fn https_fetch() -> Result<(), String> { fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { match name { "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config), + "amdvi_firmware_left" => common::iommu::amdvi_firmware_left(test_config), "netstack_socket_churn" => netstack_socket_churn(), "netstack_streams" => netstack_streams(qemu::Profile::Headless), "netstack_streams_e1000e" => netstack_streams(qemu::Profile::HeadlessE1000e), diff --git a/toyos-acpi/fixtures/qemu-11.1.1-amdvi/SOURCE b/toyos-acpi/fixtures/qemu-11.1.1-amdvi/SOURCE new file mode 100644 index 00000000000..de2593e186e --- /dev/null +++ b/toyos-acpi/fixtures/qemu-11.1.1-amdvi/SOURCE @@ -0,0 +1,18 @@ +The IVRS QEMU 11.1.1 published to a q35 with its AMD IOMMU, under QEMU's own +edk2-x86_64-code.fd, with no operating system in the guest: the firmware was +left to reach its shell, and the top 16 MiB of low memory was then written out +by the monitor's `pmemsave`. The RSDP was found in it by its signature and both +checksums and the XSDT walked host-side to the IVRS, so no byte here came +through the decoder these fixtures judge. The table sums to zero. + + qemu-system-x86_64 -nodefaults -machine q35 \ + -cpu qemu64,+rdrand,+smap,+fsgsbase,+x2apic,+smep -smp 2 -m 4G \ + -drive if=pflash,format=raw,unit=0,file=edk2-x86_64-code.fd,readonly=on \ + -drive if=pflash,format=raw,unit=1,file= \ + -device amd-iommu,intremap=off,dma-remap=on -device nec-usb-xhci,id=xhci \ + -vga std -device nvme,serial=deadbeef,id=nvme0ctl,msix-exclusive-bar=on + + ivrs.bin 0x7f775000, 176 bytes, revision 1 + +`tests/ivrs.rs` lays it under an RSDP and an XSDT of its own; the boot's +XSDT named FACP, APIC, HPET, MCFG, IVRS, WAET and BGRT. diff --git a/toyos-acpi/fixtures/qemu-11.1.1-amdvi/ivrs.bin b/toyos-acpi/fixtures/qemu-11.1.1-amdvi/ivrs.bin new file mode 100644 index 00000000000..14af2495b00 Binary files /dev/null and b/toyos-acpi/fixtures/qemu-11.1.1-amdvi/ivrs.bin differ diff --git a/toyos-acpi/src/ivrs.rs b/toyos-acpi/src/ivrs.rs new file mode 100644 index 00000000000..02adc06e8c4 --- /dev/null +++ b/toyos-acpi/src/ivrs.rs @@ -0,0 +1,551 @@ +//! The IVRS (signature `IVRS`): AMD's I/O Virtualization Reporting Structure, +//! AMD I/O Virtualization Technology (IOMMU) Specification 48882 §5.2 — which +//! AMD-Vi units the machine has, where each one's registers are, which +//! requesters each one serves, and which memory a requester must keep reaching. +//! +//! [`ivrs`] answers a table whose every block it has walked: a block the table +//! cannot hold or shorter than its type's header, an IVMD whose range runs +//! backwards or past the address space, more IVHDs than it bounds, +//! and two IVHDs of one type naming one unit are its refusals. A unit's device +//! entries are judged by the [`Ivrs::devices`] walk that reads them, so a +//! malformed list refuses that walk and leaves every unit's header readable. +//! +//! Firmware describes a unit once per IVHD type it publishes for it — 10h, +//! 11h, 40h, each a superset of the one before — and [`Ivrs::blocks`] answers +//! each unit, keyed by its segment and requester id, once at the highest of +//! them. A block of any other type is answered by its type alone. + +use crate::{find_table, Phys, Table, TableError, SDT_HEADER_LEN, SDT_REVISION}; + +/// The IVinfo word at 36, eight reserved bytes, and the first block at 48. +const IVINFO: usize = SDT_HEADER_LEN; +const FIRST_BLOCK: usize = SDT_HEADER_LEN + 12; + +/// Every block opens with its type (0), flags (1), length (2..4) and a +/// requester id (4..6). +const BLOCK_HEADER: usize = 6; +/// IVHD 10h's header: capability offset (6), register base (8..16), segment +/// (16), IOMMU info (18), IOMMU Feature Reporting (20..24). 11h and 40h put +/// the IOMMU Attributes at 20 and the two EFR images at 24 and 32. +const IVHD_10_LEN: usize = 24; +const IVHD_11_LEN: usize = 40; +/// IVMD: requester id (4), auxiliary id (6), segment (8), start (16..24), +/// length (24..32). +const IVMD_LEN: usize = 32; + +const IVHD_10: u8 = 0x10; +const IVHD_11: u8 = 0x11; +const IVHD_40: u8 = 0x40; +const IVMD_ALL: u8 = 0x20; +const IVMD_ONE: u8 = 0x21; +const IVMD_RANGE: u8 = 0x22; + +/// The most IVHDs a table may hold: a unit is described at most once per +/// type, and the bound is what keeps the pairwise check of them short. +const MAX_DEFINITIONS: usize = 64; + +/// A device entry's type: bits 7:6 encode its length as 4, 8, 16 or 32 +/// bytes, except F0h's, which carries its own. +const PAD_4: u8 = 0x00; +const ALL: u8 = 0x01; +const SELECT: u8 = 0x02; +const START: u8 = 0x03; +const END: u8 = 0x04; +const PAD_8: u8 = 0x40; +const ALIAS: u8 = 0x42; +const ALIAS_START: u8 = 0x43; +const EXTENDED: u8 = 0x46; +const EXTENDED_START: u8 = 0x47; +const SPECIAL: u8 = 0x48; +const HID: u8 = 0xF0; +/// F0h's fixed part: HID (4..12), CID (12..20), UID format (20) and length +/// (21), the UID from 22. +const HID_FIXED: usize = 22; + +/// Why an IVRS, or one unit's device entries, cannot be used. `at` is an +/// offset in the table. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum IvrsRefused { + Table(TableError), + /// The block at `at` declares a length shorter than its type's header, or + /// one the table cannot hold; a header the table cannot hold ends here + /// with `declared` zero. + Block { at: usize, declared: usize }, + /// More IVHDs than `most`. + Definitions { most: usize }, + /// The IVHDs at `at` and `other` are of one type and name one unit. + Duplicate { at: usize, other: usize }, + /// The IVMD at `at` reaches past the 64-bit address space. + Memory { at: usize, start: u64, length: u64 }, + /// A range of requester ids, an IVMD's or a device entry's, whose last id + /// is below its first. + Range { at: usize, first: u16, last: u16 }, + /// The device entry at `at` runs past the end of its block. + Entry { at: usize }, + /// A device entry type whose length implementations disagree on (the 16- + /// and 32-byte ones), or F0h outside an IVHD 40h, which alone carries it. + EntryType { at: usize, kind: u8 }, + /// An end of range with no start of range open. + Unopened { at: usize }, + /// The start of range at `at` is still open at another start or at the + /// end of its block. + Unclosed { at: usize }, + /// An F0h entry's UID of a format this decoder does not know, or of a + /// length its format does not take. + Uid { at: usize, format: u8, len: u8 }, +} + +impl From for IvrsRefused { + fn from(error: TableError) -> Self { + Self::Table(error) + } +} + +/// The IVinfo word, whole and decoded. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct IvInfo { + pub raw: u32, + /// Bit 0, `EFRSup`: every IVHD 11h and 40h carries its unit's EFR image. + pub efr_images: bool, + /// Bit 1: firmware protected memory from DMA before the operating system + /// ran, so a unit may be handed over translating. + pub dma_remap: bool, + /// Bits 14:8 and 21:15: the physical and virtual address bits a unit + /// translates. + pub physical_bits: u8, + pub virtual_bits: u8, +} + +/// What an IVHD reports of its unit beyond its header's first 20 bytes. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Features { + /// IVHD 10h: the IOMMU Feature Reporting word. + Reported(u32), + /// IVHD 11h and 40h: the IOMMU Attributes and the images of the unit's + /// two Extended Feature Registers. + Image { attributes: u32, efr: u64, efr2: u64 }, +} + +/// One AMD-Vi unit as an IVHD describes it. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Unit { + /// The block's offset in the table, which [`Ivrs::devices`] walks from. + pub at: usize, + /// The IVHD type: 10h, 11h or 40h. + pub kind: u8, + pub flags: u8, + /// The unit's own requester id, as a PCI function of `segment`. + pub device: u16, + /// Where the unit's capability block sits in that function's + /// configuration space. + pub capability: u16, + /// The unit's register window. + pub base: u64, + pub segment: u16, + /// The IOMMU info word: MSI message number (4:0) and UnitID (12:8). + pub info: u16, + pub features: Features, +} + +/// The requester ids an IVMD names. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Requesters { + /// IVMD 20h. + All, + /// IVMD 21h. + One(u16), + /// IVMD 22h, both ends included. + Range { first: u16, last: u16 }, +} + +/// An IVMD: memory its requesters must keep reaching, or never reach. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Ivmd { + pub requesters: Requesters, + /// Unity mapping (0), read (1), write (2), exclusion range (3). + pub flags: u8, + pub segment: u16, + pub start: u64, + /// `start + length` does not wrap: [`ivrs`] refused every one that does. + pub length: u64, +} + +/// One block, in the shape its reader acts on. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum IvrsBlock { + /// The IVHD that describes its unit at the highest type the table holds + /// for it. + Unit(Unit), + /// An IVHD a higher-typed one of the same unit replaces. + Superseded(Unit), + Memory(Ivmd), + /// A type not decoded here, by its type, offset and length. + Other { kind: u8, at: usize, len: usize }, +} + +/// An F0h entry's UID: the ACPI `_UID` of the device it names. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Uid { + Absent, + Integer(u64), + /// `len` bytes at `at` in the table, read by [`Ivrs::bytes`]. + String { at: usize, len: u8 }, +} + +/// One device entry, ranges already paired. `data` is the entry's DTE +/// setting byte; a range's is its start's. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum DeviceEntry { + /// Type 1: every requester id of the unit's segment. + All { data: u8 }, + /// Type 2. + Select { id: u16, data: u8 }, + /// Types 3 and 4. + Range { first: u16, last: u16, data: u8 }, + /// Type 42h: `id`'s transactions reach the unit under `used`. + Alias { id: u16, used: u16, data: u8 }, + /// Types 43h and 4. + AliasRange { first: u16, last: u16, used: u16, data: u8 }, + /// Type 46h, with its extended setting word. + Extended { id: u16, data: u8, extended: u32 }, + /// Types 47h and 4. + ExtendedRange { first: u16, last: u16, data: u8, extended: u32 }, + /// Type 48h: an I/O APIC (`variety` 1) or HPET (2), which no PCI walk + /// reaches, numbered `handle` and reaching the unit under `used`. + Special { handle: u8, used: u16, variety: u8, data: u8 }, + /// Type F0h: an ACPI device, by its `_HID` and `_CID` as eight bytes + /// each and its `_UID`, reaching the unit under `id`. + Hid { id: u16, data: u8, hid: [u8; 8], cid: [u8; 8], uid: Uid }, + /// A 4- or 8-byte type not decoded here, padding excepted. + Other(u8), +} + +/// An IVRS whose every block [`ivrs`] has walked. +#[derive(Clone, Copy)] +pub struct Ivrs

{ + table: Table

, +} + +/// A block's header, bounded by the table. +#[derive(Clone, Copy)] +struct Raw { + at: usize, + kind: u8, + flags: u8, + len: usize, + device: u16, +} + +/// The IVRS at `rsdp_addr`, every block of it checked. +pub fn ivrs(phys: P, rsdp_addr: u64) -> Result, IvrsRefused> { + let ivrs = Ivrs { table: find_table(phys, rsdp_addr, b"IVRS", FIRST_BLOCK)? }; + let mut definitions = 0usize; + for raw in ivrs.raw_blocks() { + let raw = raw?; + match raw.kind { + IVHD_10 | IVHD_11 | IVHD_40 => { + definitions += 1; + if definitions > MAX_DEFINITIONS { + return Err(IvrsRefused::Definitions { most: MAX_DEFINITIONS }); + } + let unit = ivrs.unit(raw).ok_or(IvrsRefused::Block { at: raw.at, declared: raw.len })?; + if let Some(other) = ivrs.units().find(|o| o.at < unit.at && o.kind == unit.kind && same(o, &unit)) { + return Err(IvrsRefused::Duplicate { at: unit.at, other: other.at }); + } + } + IVMD_ALL | IVMD_ONE | IVMD_RANGE => { + let memory = ivrs.memory(raw).ok_or(IvrsRefused::Block { at: raw.at, declared: raw.len })?; + if let Requesters::Range { first, last } = memory.requesters { + if last < first { + return Err(IvrsRefused::Range { at: raw.at, first, last }); + } + } + if memory.start.checked_add(memory.length).is_none() { + return Err(IvrsRefused::Memory { at: raw.at, start: memory.start, length: memory.length }); + } + } + _ => {} + } + } + Ok(ivrs) +} + +fn same(a: &Unit, b: &Unit) -> bool { + (a.segment, a.device) == (b.segment, b.device) +} + +impl Ivrs

{ + pub fn revision(&self) -> u8 { + self.table.byte(SDT_REVISION).unwrap_or(0) + } + + pub fn info(&self) -> IvInfo { + let raw = self.table.u32_at(IVINFO).unwrap_or(0); + IvInfo { + raw, + efr_images: raw & 1 != 0, + dma_remap: raw & (1 << 1) != 0, + physical_bits: ((raw >> 8) & 0x7F) as u8, + virtual_bits: ((raw >> 15) & 0x7F) as u8, + } + } + + /// Every block, in the table's order. + pub fn blocks(&self) -> impl Iterator + '_ { + // `ivrs` walked the same headers and decoded the same blocks. + self.raw_blocks().filter_map(|raw| { + let raw = raw.ok()?; + Some(match raw.kind { + IVHD_10 | IVHD_11 | IVHD_40 => { + let unit = self.unit(raw)?; + if self.units().any(|o| o.kind > unit.kind && same(&o, &unit)) { + IvrsBlock::Superseded(unit) + } else { + IvrsBlock::Unit(unit) + } + } + IVMD_ALL | IVMD_ONE | IVMD_RANGE => IvrsBlock::Memory(self.memory(raw)?), + kind => IvrsBlock::Other { kind, at: raw.at, len: raw.len }, + }) + }) + } + + /// The device entries of the IVHD at `unit.at`, in its order; none where + /// no IVHD is there. + pub fn devices(&self, unit: &Unit) -> DeviceEntries<'_, P> { + let (kind, at, end) = match self.raw(unit.at) { + Ok(raw) if raw.kind == IVHD_10 => (raw.kind, raw.at + IVHD_10_LEN, raw.at + raw.len), + Ok(raw) if matches!(raw.kind, IVHD_11 | IVHD_40) => (raw.kind, raw.at + IVHD_11_LEN, raw.at + raw.len), + _ => (0, 0, 0), + }; + DeviceEntries { ivrs: self, kind, at, end, open: None, done: false } + } + + /// `len` bytes at `at`, as a [`Uid::String`] names them. + pub fn bytes(&self, at: usize, len: u8) -> impl Iterator + '_ { + (0..usize::from(len)).filter_map(move |i| self.table.byte(at.checked_add(i)?)) + } + + fn units(&self) -> impl Iterator + '_ { + self.raw_blocks().filter_map(|raw| self.unit(raw.ok()?)) + } + + /// Walks by each block's own length, which [`Ivrs::raw`] bounds below by + /// the block header and above by the table: at most `len / 6` steps. + fn raw_blocks(&self) -> impl Iterator> + '_ { + let mut at = FIRST_BLOCK; + let mut halted = false; + core::iter::from_fn(move || { + if halted || at >= self.table.len() { + return None; + } + let raw = self.raw(at); + match raw { + Ok(raw) => at += raw.len, + Err(_) => halted = true, + } + Some(raw) + }) + } + + fn raw(&self, at: usize) -> Result { + let t = &self.table; + let header = || Some((t.byte(at)?, t.byte(at + 1)?, t.u16_at(at + 2)?, t.u16_at(at + 4)?)); + let Some((kind, flags, declared, device)) = header() else { + return Err(IvrsRefused::Block { at, declared: 0 }); + }; + let len = usize::from(declared); + let floor = match kind { + IVHD_10 => IVHD_10_LEN, + IVHD_11 | IVHD_40 => IVHD_11_LEN, + IVMD_ALL | IVMD_ONE | IVMD_RANGE => IVMD_LEN, + _ => BLOCK_HEADER, + }; + if len < floor || at + len > t.len() { + return Err(IvrsRefused::Block { at, declared: len }); + } + Ok(Raw { at, kind, flags, len, device }) + } + + /// An IVHD's fields, or `None` for any other block. + fn unit(&self, raw: Raw) -> Option { + let t = &self.table; + let at = raw.at; + let features = match raw.kind { + IVHD_10 => Features::Reported(t.u32_at(at + 20)?), + IVHD_11 | IVHD_40 => { + Features::Image { attributes: t.u32_at(at + 20)?, efr: t.u64_at(at + 24)?, efr2: t.u64_at(at + 32)? } + } + _ => return None, + }; + Some(Unit { + at, + kind: raw.kind, + flags: raw.flags, + device: raw.device, + capability: t.u16_at(at + 6)?, + base: t.u64_at(at + 8)?, + segment: t.u16_at(at + 16)?, + info: t.u16_at(at + 18)?, + features, + }) + } + + /// An IVMD's fields, or `None` for any other block. + fn memory(&self, raw: Raw) -> Option { + let t = &self.table; + let at = raw.at; + let requesters = match raw.kind { + IVMD_ALL => Requesters::All, + IVMD_ONE => Requesters::One(raw.device), + IVMD_RANGE => Requesters::Range { first: raw.device, last: t.u16_at(at + 6)? }, + _ => return None, + }; + Some(Ivmd { + requesters, + flags: raw.flags, + segment: t.u16_at(at + 8)?, + start: t.u64_at(at + 16)?, + length: t.u64_at(at + 24)?, + }) + } +} + +/// A start of range waiting for its end. +#[derive(Clone, Copy)] +struct Start { + at: usize, + kind: u8, + first: u16, + data: u8, + /// An alias start's used id, or an extended start's setting word. + word: u32, +} + +/// One unit's device entries; see [`Ivrs::devices`]. A refusal is its last +/// item. +pub struct DeviceEntries<'a, P> { + ivrs: &'a Ivrs

, + /// The IVHD type the entries are in. + kind: u8, + at: usize, + end: usize, + open: Option, + done: bool, +} + +impl Iterator for DeviceEntries<'_, P> { + type Item = Result; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + let item = self.step(); + if !matches!(item, Some(Ok(_))) { + self.done = true; + } + item + } +} + +impl DeviceEntries<'_, P> { + /// Every step advances by at least four bytes, so a walk is at most a + /// quarter of its block's length long. + fn step(&mut self) -> Option> { + loop { + let at = self.at; + if at >= self.end { + return self.open.take().map(|start| Err(IvrsRefused::Unclosed { at: start.at })); + } + let t = &self.ivrs.table; + let Some(kind) = t.byte(at) else { return Some(Err(IvrsRefused::Entry { at })) }; + let len = match kind { + 0x00..=0x7F => 4 << (kind >> 6), + HID if self.kind != IVHD_40 => return Some(Err(IvrsRefused::EntryType { at, kind })), + HID => match t.byte(at + HID_FIXED - 1) { + Some(uid) if at + HID_FIXED <= self.end => HID_FIXED + usize::from(uid), + _ => return Some(Err(IvrsRefused::Entry { at })), + }, + _ => return Some(Err(IvrsRefused::EntryType { at, kind })), + }; + if at + len > self.end { + return Some(Err(IvrsRefused::Entry { at })); + } + self.at += len; + match self.decode(at, kind, len) { + Ok(Some(device)) => return Some(Ok(device)), + // Padding, or a start of range waiting for its end. + Ok(None) => continue, + Err(refused) => return Some(Err(refused)), + } + } + } + + /// The `len`-byte entry of type `kind` at `at`, which the block holds whole. + fn decode(&mut self, at: usize, kind: u8, len: usize) -> Result, IvrsRefused> { + let t = &self.ivrs.table; + let entry = IvrsRefused::Entry { at }; + let id = t.u16_at(at + 1).ok_or(entry)?; + let data = t.byte(at + 3).ok_or(entry)?; + let word = if len == 8 { t.u32_at(at + 4).ok_or(entry)? } else { 0 }; + let used = (word >> 8) as u16; + Ok(Some(match kind { + PAD_4 | PAD_8 => return Ok(None), + ALL => DeviceEntry::All { data }, + SELECT => DeviceEntry::Select { id, data }, + ALIAS => DeviceEntry::Alias { id, used, data }, + EXTENDED => DeviceEntry::Extended { id, data, extended: word }, + SPECIAL => DeviceEntry::Special { handle: word as u8, used, variety: (word >> 24) as u8, data }, + START | ALIAS_START | EXTENDED_START => { + if let Some(open) = self.open.replace(Start { at, kind, first: id, data, word }) { + return Err(IvrsRefused::Unclosed { at: open.at }); + } + return Ok(None); + } + END => { + let Some(Start { at: start, kind, first, data, word }) = self.open.take() else { + return Err(IvrsRefused::Unopened { at }); + }; + let last = id; + if last < first { + return Err(IvrsRefused::Range { at: start, first, last }); + } + match kind { + START => DeviceEntry::Range { first, last, data }, + ALIAS_START => DeviceEntry::AliasRange { first, last, used: (word >> 8) as u16, data }, + _ => DeviceEntry::ExtendedRange { first, last, data, extended: word }, + } + } + HID => self.hid(at, id, data)?, + other => DeviceEntry::Other(other), + })) + } + + /// An F0h entry the block holds whole. + fn hid(&self, at: usize, id: u16, data: u8) -> Result { + let t = &self.ivrs.table; + let entry = IvrsRefused::Entry { at }; + let eight = |from: usize| -> Option<[u8; 8]> { + let mut bytes = [0u8; 8]; + for (i, b) in bytes.iter_mut().enumerate() { + *b = t.byte(from + i)?; + } + Some(bytes) + }; + let (format, len) = (t.byte(at + 20).ok_or(entry)?, t.byte(at + 21).ok_or(entry)?); + let uid = match (format, len) { + (0, 0) => Uid::Absent, + (1, 1..=8) => { + let mut value = 0u64; + for i in 0..usize::from(len) { + value |= u64::from(t.byte(at + HID_FIXED + i).ok_or(entry)?) << (8 * i); + } + Uid::Integer(value) + } + (2, 1..) => Uid::String { at: at + HID_FIXED, len }, + _ => return Err(IvrsRefused::Uid { at, format, len }), + }; + Ok(DeviceEntry::Hid { id, data, hid: eight(at + 4).ok_or(entry)?, cid: eight(at + 12).ok_or(entry)?, uid }) + } +} diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index 794c4089665..0a412973769 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -7,8 +7,8 @@ //! //! Multi-byte fields are composed from bytes, little-endian, so no firmware //! byte is transmuted into a type. Field offsets cite ACPI 6.5, except MCFG's -//! (PCI Firmware Specification), HPET's (IA-PC HPET Specification), SPCR's (Microsoft's SPCR specification) -//! and the IORT's (Arm DEN 0049). +//! (PCI Firmware Specification), HPET's (IA-PC HPET Specification), SPCR's (Microsoft's SPCR specification), +//! the IORT's (Arm DEN 0049) and the IVRS's (AMD 48882). //! //! `no_std`, no allocation, no `unsafe`. @@ -20,6 +20,7 @@ mod facs; mod fadt; mod gtdt; mod iort; +mod ivrs; mod madt; mod resource; mod spcr; @@ -36,6 +37,9 @@ pub use fadt::{ }; use fadt::FADT_DSDT; pub use iort::{iort, Iort, IortRefused, ItsDevice, Node, Route, Smmuv3}; +pub use ivrs::{ + ivrs, DeviceEntries, DeviceEntry, Features, IvInfo, Ivmd, Ivrs, IvrsBlock, IvrsRefused, Requesters, Uid, Unit, +}; pub use madt::{ isa_line, madt_entries, sci_line, Gicc, IoApicEntry, Line, MadtEntries, MadtEntry, MadtHalt, Polarity, SourceOverride, Trigger, MADT_ENTRIES, diff --git a/toyos-acpi/tests/ivrs.rs b/toyos-acpi/tests/ivrs.rs new file mode 100644 index 00000000000..bbfef252ab5 --- /dev/null +++ b/toyos-acpi/tests/ivrs.rs @@ -0,0 +1,461 @@ +//! The IVRS: QEMU's own, decoded against what `iasl -d` read off the same +//! bytes, and every block, every device entry and every refusal over crafted +//! ones. + +mod common; + +use common::{declare_len, reseal, rsdp, sdt, xsdt, Machine}; +use toyos_acpi::{ + ivrs, DeviceEntry, Features, IvInfo, Ivmd, Ivrs, IvrsBlock, IvrsRefused, Requesters, TableError, Uid, Unit, +}; + +const RSDP_AT: u64 = 0x800; +const XSDT_AT: u64 = 0x1000; +const TABLE_AT: u64 = 0x9000; + +/// QEMU 11.1.1's IVRS for `-device amd-iommu,intremap=off,dma-remap=on` on +/// a q35 with a VGA, an xHCI and an NVMe controller (`SOURCE` beside it). +const QEMU: &[u8] = include_bytes!("../fixtures/qemu-11.1.1-amdvi/ivrs.bin"); + +/// `bytes` at [`TABLE_AT`] under an RSDP and an XSDT naming it, opened. +fn judged(bytes: &[u8], ask: impl FnOnce(&Ivrs>) -> T) -> Result { + let head = rsdp(XSDT_AT, 2, 36); + let root = xsdt(&[TABLE_AT]); + let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, bytes)]; + ivrs(Machine { regions }, RSDP_AT).map(|table| ask(&table)) +} + +fn refusal(bytes: &[u8]) -> Option { + judged(bytes, |_| ()).err() +} + +fn blocks(bytes: &[u8]) -> Vec { + judged(bytes, |t| t.blocks().collect()).expect("a table every block of which holds") +} + +/// Every unit's device entries, each walk to its end or its refusal. +fn entries(bytes: &[u8]) -> Vec>> { + judged(bytes, |t| { + t.blocks() + .filter_map(|b| match b { + IvrsBlock::Unit(unit) => Some(t.devices(&unit).collect()), + _ => None, + }) + .collect() + }) + .expect("a table every block of which holds") +} + +// --- QEMU's --------------------------------------------------------------- + +/// The unit both blocks describe: `amd-iommu` is function 00:02.0, its +/// capability at 0x40 and its registers at `AMDVI_BASE_ADDR`. +fn qemu_unit(at: usize, kind: u8, flags: u8, features: Features) -> Unit { + Unit { at, kind, flags, device: 0x10, capability: 0x40, base: 0xfed8_0000, segment: 0, info: 0, features } +} + +/// `iasl -d`: revision 1, `Virtualization Info : 00002801`, an IVHD 10h at +/// 30h with flags D1 and `Feature Reporting : 00000044`, an IVHD 11h at 68h +/// with flags 11 and `EFR Image : 00000000000029D3`. 10h and 11h name one +/// unit, so the 11h is the unit and the 10h is superseded, whatever order +/// they come in. +#[test] +fn qemus_table_is_one_unit_described_twice_and_answered_once_at_its_higher_type() { + let (revision, info) = judged(QEMU, |t| (t.revision(), t.info())).expect("the IVRS QEMU published"); + assert_eq!(revision, 1); + assert_eq!(info, IvInfo { raw: 0x2801, efr_images: true, dma_remap: false, physical_bits: 40, virtual_bits: 0 }); + assert_eq!( + blocks(QEMU), + [ + IvrsBlock::Superseded(qemu_unit(0x30, 0x10, 0xd1, Features::Reported(0x44))), + IvrsBlock::Unit(qemu_unit(0x68, 0x11, 0x11, Features::Image { attributes: 0, efr: 0x29d3, efr2: 0 })), + ] + ); +} + +/// `Device Entry: Select One Device` eight times in each block: 00:00.0, +/// 00:01.0, the unit's own 00:02.0, the xHCI's 00:03.0, the NVMe +/// controller's 00:04.0, and the ICH9's 00:1f.0, .2 and .3. +#[test] +fn qemus_unit_serves_every_function_of_bus_zero_by_one_select_each() { + let select = |id| Ok(DeviceEntry::Select { id, data: 0 }); + let functions: Vec<_> = [0x00, 0x08, 0x10, 0x18, 0x20, 0xf8, 0xfa, 0xfb].into_iter().map(select).collect(); + assert_eq!(entries(QEMU), [functions.as_slice()]); + let superseded = judged(QEMU, |t| { + let IvrsBlock::Superseded(unit) = t.blocks().next().expect("a first block") else { panic!("not superseded") }; + t.devices(&unit).collect::>() + }); + assert_eq!(superseded, Ok(functions)); +} + +// --- crafted -------------------------------------------------------------- + +/// An IVHD of `kind` over `entries`: unit `device` of segment 0, its +/// capability at 0x40, its registers at `base`. +fn ivhd(kind: u8, device: u16, base: u64, entries: &[Vec]) -> Vec { + let mut b = vec![kind, 0xa5, 0, 0]; + b.extend(device.to_le_bytes()); + b.extend(0x40u16.to_le_bytes()); + b.extend(base.to_le_bytes()); + b.extend(0u16.to_le_bytes()); + b.extend(0x1f03u16.to_le_bytes()); + if kind == 0x10 { + b.extend(0x8000_0044u32.to_le_bytes()); + } else { + b.extend(1u32.to_le_bytes()); + b.extend(0x0123_4567_89ab_cdefu64.to_le_bytes()); + b.extend(2u64.to_le_bytes()); + } + b.extend(entries.concat()); + let len = b.len() as u16; + b[2..4].copy_from_slice(&len.to_le_bytes()); + b +} + +fn ivmd(kind: u8, flags: u8, device: u16, aux: u16, start: u64, length: u64) -> Vec { + let mut b = vec![kind, flags, 32, 0]; + b.extend(device.to_le_bytes()); + b.extend(aux.to_le_bytes()); + b.extend(3u16.to_le_bytes()); + b.extend([0u8; 6]); + b.extend(start.to_le_bytes()); + b.extend(length.to_le_bytes()); + b +} + +/// An IVRS over `blocks`, its IVinfo saying EFR images, pre-boot DMA +/// protection, 48 physical and 64 virtual address bits. +fn table(blocks: &[Vec]) -> Vec { + let mut body = (0x0020_3003u32).to_le_bytes().to_vec(); + body.extend([0u8; 8]); + body.extend(blocks.concat()); + sdt(b"IVRS", 2, &body) +} + +fn e4(kind: u8, id: u16, data: u8) -> Vec { + let [lo, hi] = id.to_le_bytes(); + vec![kind, lo, hi, data] +} + +fn e8(kind: u8, id: u16, data: u8, word: u32) -> Vec { + let mut e = e4(kind, id, data); + e.extend(word.to_le_bytes()); + e +} + +fn hid(id: u16, data: u8, hid: &[u8; 8], cid: &[u8; 8], format: u8, uid: &[u8]) -> Vec { + let mut e = e4(0xf0, id, data); + e.extend(hid); + e.extend(cid); + e.extend([format, uid.len() as u8]); + e.extend(uid); + e +} + +/// Every entry type: each decoded one, padding of both lengths, and a +/// reserved type of each. +fn every_entry() -> Vec> { + vec![ + e4(0x01, 0, 0xd7), + e4(0x00, 0xffff, 0xff), + e4(0x02, 0x0018, 0x01), + e4(0x03, 0x0100, 0x40), + e4(0x04, 0x01ff, 0xee), + e8(0x42, 0x0200, 0x02, 0x0003_0100), + e8(0x43, 0x0300, 0x04, 0x0003_0200), + e4(0x02, 0x00a0, 0x00), + e4(0x04, 0x03ff, 0x00), + e8(0x46, 0x0400, 0x08, 0x8000_0000), + e8(0x47, 0x0500, 0x10, 0x4000_0001), + e4(0x04, 0x05ff, 0x00), + e8(0x40, 0, 0, 0xffff_ffff), + e8(0x48, 0, 0xd7, 0x0100_a021), + e8(0x48, 0, 0x00, 0x0200_a000), + e4(0x05, 0x0600, 0x00), + e8(0x49, 0x0700, 0x00, 0), + hid(0x00a5, 0x40, b"AMDI0010", b"\0\0\0\0\0\0\0\0", 1, &[3]), + hid(0x00a6, 0x00, b"AMDI0020", b"PNP0C50\0", 2, b"\\_SB.I2CA"), + hid(0x00a7, 0x00, b"AMDI0030", b"\0\0\0\0\0\0\0\0", 0, &[]), + ] +} + +/// Units 00:00.2 and 00:01.2: the first at 10h and 40h, the second at 10h +/// alone; and an IVMD of each type and a block of a type not decoded. +fn crafted() -> Vec { + table(&[ + ivhd(0x10, 0x0002, 0xfeb8_0000, &[e4(0x01, 0, 0)]), + ivmd(0x20, 0x09, 0, 0, 0x0009_d000, 0x1000), + ivhd(0x40, 0x0002, 0xfeb8_0000, &every_entry()), + ivhd(0x10, 0x000a, 0xfec0_0000, &[e4(0x02, 0x0008, 0)]), + ivmd(0x21, 0x06, 0x00a0, 0, 0x7f00_0000, 0x10_0000), + ivmd(0x22, 0x01, 0x0100, 0x01ff, 0xfee0_0000, 0x10_0000), + vec![0x30, 0, 8, 0, 0, 0, 0, 0], + ]) +} + +/// The offsets `crafted` puts its blocks at. +const FIRST_AT: usize = 48; +const SECOND_AT: usize = FIRST_AT + 28 + 32; + +fn unit(at: usize, kind: u8, device: u16, base: u64) -> Unit { + let features = if kind == 0x10 { + Features::Reported(0x8000_0044) + } else { + Features::Image { attributes: 1, efr: 0x0123_4567_89ab_cdef, efr2: 2 } + }; + Unit { at, kind, flags: 0xa5, device, capability: 0x40, base, segment: 0, info: 0x1f03, features } +} + +#[test] +fn every_block_type_decodes_and_a_unit_is_answered_once_at_its_highest_type() { + let t = crafted(); + let second_len = 40 + every_entry().concat().len(); + let third_at = SECOND_AT + second_len; + assert_eq!(judged(&t, |t| t.info()).map(|i| (i.efr_images, i.dma_remap, i.physical_bits, i.virtual_bits)), Ok((true, true, 48, 64))); + assert_eq!( + blocks(&t), + [ + IvrsBlock::Superseded(unit(FIRST_AT, 0x10, 0x0002, 0xfeb8_0000)), + IvrsBlock::Memory(Ivmd { requesters: Requesters::All, flags: 0x09, segment: 3, start: 0x9_d000, length: 0x1000 }), + IvrsBlock::Unit(unit(SECOND_AT, 0x40, 0x0002, 0xfeb8_0000)), + IvrsBlock::Unit(unit(third_at, 0x10, 0x000a, 0xfec0_0000)), + IvrsBlock::Memory(Ivmd { + requesters: Requesters::One(0x00a0), + flags: 0x06, + segment: 3, + start: 0x7f00_0000, + length: 0x10_0000 + }), + IvrsBlock::Memory(Ivmd { + requesters: Requesters::Range { first: 0x0100, last: 0x01ff }, + flags: 0x01, + segment: 3, + start: 0xfee0_0000, + length: 0x10_0000 + }), + IvrsBlock::Other { kind: 0x30, at: third_at + 28 + 32 * 2, len: 8 }, + ] + ); +} + +/// The string UID's bytes, where [`Uid::String`] says they are. +const UID_AT: usize = SECOND_AT + 40 + 4 * 9 + 8 * 8 + 23 + 22; + +#[test] +fn every_device_entry_decodes_with_its_range_paired_and_its_padding_dropped() { + let walked = entries(&crafted()); + assert_eq!( + walked[0], + [ + DeviceEntry::All { data: 0xd7 }, + DeviceEntry::Select { id: 0x0018, data: 0x01 }, + DeviceEntry::Range { first: 0x0100, last: 0x01ff, data: 0x40 }, + DeviceEntry::Alias { id: 0x0200, used: 0x0301, data: 0x02 }, + // An entry between a start and its end is its own. + DeviceEntry::Select { id: 0x00a0, data: 0x00 }, + DeviceEntry::AliasRange { first: 0x0300, last: 0x03ff, used: 0x0302, data: 0x04 }, + DeviceEntry::Extended { id: 0x0400, data: 0x08, extended: 0x8000_0000 }, + DeviceEntry::ExtendedRange { first: 0x0500, last: 0x05ff, data: 0x10, extended: 0x4000_0001 }, + DeviceEntry::Special { handle: 0x21, used: 0x00a0, variety: 1, data: 0xd7 }, + DeviceEntry::Special { handle: 0x00, used: 0x00a0, variety: 2, data: 0x00 }, + DeviceEntry::Other(0x05), + DeviceEntry::Other(0x49), + DeviceEntry::Hid { id: 0x00a5, data: 0x40, hid: *b"AMDI0010", cid: [0; 8], uid: Uid::Integer(3) }, + DeviceEntry::Hid { + id: 0x00a6, + data: 0x00, + hid: *b"AMDI0020", + cid: *b"PNP0C50\0", + uid: Uid::String { at: UID_AT, len: 9 } + }, + DeviceEntry::Hid { id: 0x00a7, data: 0x00, hid: *b"AMDI0030", cid: [0; 8], uid: Uid::Absent }, + ] + .map(Ok::<_, IvrsRefused>) + ); + assert_eq!(walked[1], [Ok(DeviceEntry::Select { id: 0x0008, data: 0 })]); + assert_eq!(judged(&crafted(), |t| t.bytes(UID_AT, 9).collect::>()), Ok(b"\\_SB.I2CA".to_vec())); +} + +/// The block at [`SECOND_AT`]'s device entries when it holds `list`, in an +/// IVHD of `kind`. +fn walk(kind: u8, list: &[Vec]) -> Vec> { + entries(&table(&[ivhd(kind, 0x0002, 0xfeb8_0000, list)])).remove(0) +} + +const HEAD_40: usize = FIRST_AT + 40; + +#[test] +fn a_walk_that_meets_a_bad_entry_ends_on_its_refusal() { + let select = Ok(DeviceEntry::Select { id: 8, data: 0 }); + let refused = |list: &[Vec], why| { + let mut full = vec![e4(0x02, 8, 0)]; + full.extend_from_slice(list); + full.push(e4(0x02, 0x10, 0)); + assert_eq!(walk(0x40, &full), [select, Err(why)], "{list:02x?}"); + }; + let at = HEAD_40 + 4; + refused(&[e4(0x04, 0x10, 0)], IvrsRefused::Unopened { at }); + refused(&[e4(0x03, 0x10, 0), e8(0x43, 0x20, 0, 0)], IvrsRefused::Unclosed { at }); + refused(&[e4(0x03, 0x10, 0), e4(0x04, 0x0f, 0)], IvrsRefused::Range { at, first: 0x10, last: 0x0f }); + refused(&[e8(0x80, 0, 0, 0)], IvrsRefused::EntryType { at, kind: 0x80 }); + refused(&[e8(0xc0, 0, 0, 0)], IvrsRefused::EntryType { at, kind: 0xc0 }); + for (format, uid) in [(0u8, &[1u8][..]), (1, &[]), (1, &[0; 9]), (3, &[1])] { + refused(&[hid(0, 0, b"AMDI0010", &[0; 8], format, uid)], IvrsRefused::Uid { at, format, len: uid.len() as u8 }); + } + // F0h is IVHD 40h's alone. + let in_11 = walk(0x11, &[e4(0x02, 8, 0), hid(0, 0, b"AMDI0010", &[0; 8], 0, &[]), e4(0x02, 0x10, 0)]); + assert_eq!(in_11, [select, Err(IvrsRefused::EntryType { at: HEAD_40 + 4, kind: 0xf0 })]); + // A start of range the block ends inside. + assert_eq!(walk(0x40, &[e4(0x02, 8, 0), e4(0x03, 0x10, 0)]), [select, Err(IvrsRefused::Unclosed { at })]); +} + +/// The block ends where its length says, whatever an entry's type says it +/// needs past that. +#[test] +fn an_entry_the_block_cannot_hold_is_refused_not_read_past() { + let short = |list: &[Vec], cut: usize| { + let mut bytes = table(&[ivhd(0x40, 0x0002, 0xfeb8_0000, list)]); + // Shorten the block by `cut` and the table with it, so nothing past the block exists. + let len = u16::from_le_bytes([bytes[FIRST_AT + 2], bytes[FIRST_AT + 3]]) - cut as u16; + bytes[FIRST_AT + 2..FIRST_AT + 4].copy_from_slice(&len.to_le_bytes()); + bytes.truncate(bytes.len() - cut); + let len = bytes.len() as u32; + declare_len(&mut bytes, len); + entries(&bytes).remove(0) + }; + let at = HEAD_40; + assert_eq!(short(&[e8(0x42, 0, 0, 0)], 2), [Err(IvrsRefused::Entry { at })]); + assert_eq!(short(&[e4(0x02, 0, 0)], 1), [Err(IvrsRefused::Entry { at })]); + // The fixed part, and then the UID its length byte asks for. + assert_eq!(short(&[hid(0, 0, b"AMDI0010", &[0; 8], 0, &[])], 1), [Err(IvrsRefused::Entry { at })]); + assert_eq!(short(&[hid(0, 0, b"AMDI0010", &[0; 8], 2, b"ab")], 1), [Err(IvrsRefused::Entry { at })]); +} + +/// A unit's list refused leaves the table, and every other unit's header +/// and list, readable: the header is what switching a unit off needs. +#[test] +fn a_refused_list_refuses_its_walk_and_not_the_table() { + let t = table(&[ivhd(0x40, 0x0002, 0xfeb8_0000, &[e4(0x04, 0, 0)]), ivhd(0x10, 0x000a, 0xfec0_0000, &[e4(0x02, 8, 0)])]); + let walked = entries(&t); + assert_eq!(walked, [vec![Err(IvrsRefused::Unopened { at: HEAD_40 })], vec![Ok(DeviceEntry::Select { id: 8, data: 0 })]]); +} + +#[test] +fn a_block_shorter_than_its_header_or_longer_than_the_table_is_refused() { + let with_len = |block: Vec, len: u16| { + let mut block = block; + block[2..4].copy_from_slice(&len.to_le_bytes()); + table(&[block, ivmd(0x20, 0, 0, 0, 0, 0)]) + }; + let unit = ivhd(0x10, 2, 0xfeb8_0000, &[]); + let ext = ivhd(0x11, 2, 0xfeb8_0000, &[]); + let at = FIRST_AT; + for (block, len) in [(unit.clone(), 23u16), (ext.clone(), 39), (ivmd(0x22, 0, 0, 0, 0, 0), 31), (vec![0x30; 8], 5), (unit, 0)] { + assert_eq!(refusal(&with_len(block, len)), Some(IvrsRefused::Block { at, declared: len.into() }), "{len}"); + } + // Past the table, by one byte and by the whole of the length field. + let total = with_len(ext.clone(), 40).len() - FIRST_AT; + for len in [total as u16 + 1, u16::MAX] { + assert_eq!(refusal(&with_len(ext.clone(), len)), Some(IvrsRefused::Block { at, declared: len.into() })); + } + // Bytes after the last block too few to be a block's header. + for tail in 1..6 { + let mut body = table(&[ivhd(0x10, 2, 0xfeb8_0000, &[])])[36..].to_vec(); + body.extend(vec![0x30; tail]); + let bytes = sdt(b"IVRS", 2, &body); + assert_eq!(refusal(&bytes), Some(IvrsRefused::Block { at: FIRST_AT + 24, declared: 0 }), "{tail}"); + } +} + +#[test] +fn a_table_too_short_for_its_first_block_is_refused_by_length() { + let mut bytes = table(&[]); + declare_len(&mut bytes, 47); + assert_eq!(refusal(&bytes), Some(IvrsRefused::Table(TableError::Length { declared: 47, needed: 48 }))); + // Exactly the header is a table of no unit. + assert_eq!(judged(&table(&[]), |t| t.blocks().count()), Ok(0)); +} + +#[test] +fn an_ivmd_range_running_backwards_or_past_the_address_space_is_refused() { + let at = FIRST_AT; + assert_eq!( + refusal(&table(&[ivmd(0x22, 0, 0x0200, 0x01ff, 0, 0x1000)])), + Some(IvrsRefused::Range { at, first: 0x0200, last: 0x01ff }) + ); + let start = u64::MAX - 0xfff; + assert_eq!( + refusal(&table(&[ivmd(0x20, 0, 0, 0, start, 0x1000)])), + Some(IvrsRefused::Memory { at, start, length: 0x1000 }) + ); + assert_eq!(judged(&table(&[ivmd(0x20, 0, 0, 0, start, 0xfff)]), |t| t.blocks().count()), Ok(1)); +} + +#[test] +fn two_ivhds_of_one_type_naming_one_unit_are_refused_and_two_units_are_not() { + let a = ivhd(0x11, 0x0002, 0xfeb8_0000, &[]); + assert_eq!(refusal(&table(&[a.clone(), a.clone()])), Some(IvrsRefused::Duplicate { at: FIRST_AT + 40, other: FIRST_AT })); + let b = ivhd(0x11, 0x000a, 0xfeb8_0000, &[]); + assert_eq!(blocks(&table(&[a, b])).iter().filter(|b| matches!(b, IvrsBlock::Unit(_))).count(), 2); +} + +#[test] +fn more_ivhds_than_the_bound_are_refused() { + let units = |n: u16| table(&(0..n).map(|i| ivhd(0x10, i, 0xfeb8_0000, &[])).collect::>()); + assert_eq!(judged(&units(64), |t| t.blocks().count()), Ok(64)); + assert_eq!(refusal(&units(65)), Some(IvrsRefused::Definitions { most: 64 })); +} + +/// Firmware's bytes, any of them wrong: every single-byte change to QEMU's +/// table and to the crafted one, resealed, and every length either could +/// declare, is decoded or refused without a panic, and every walk ends +/// within its bound. +#[test] +fn no_corruption_of_an_ivrs_panics_or_runs_away() { + let mut walked = 0usize; + let mut refused = 0usize; + let mut walks_refused = 0usize; + let mut ask = |bytes: &[u8]| { + let _ = judged(bytes, |t| { + let _ = t.info(); + assert!(t.blocks().count() <= bytes.len() / 6); + for block in t.blocks() { + if let IvrsBlock::Unit(unit) | IvrsBlock::Superseded(unit) = block { + let mut seen = 0; + for entry in t.devices(&unit) { + seen += 1; + assert!(seen <= bytes.len() / 4 + 1, "a device walk is not advancing"); + match entry { + Ok(DeviceEntry::Hid { uid: Uid::String { at, len }, .. }) => { + assert_eq!(t.bytes(at, len).count(), usize::from(len)); + } + Ok(_) => {} + Err(_) => walks_refused += 1, + } + } + walked += seen; + } + } + }) + .map_err(|_| refused += 1); + }; + for original in [QEMU.to_vec(), crafted()] { + for at in 36..original.len() { + for value in [0x00, 0x01, 0x04, 0x7f, 0x80, 0xf0, 0xff, original[at].wrapping_add(1), original[at] ^ 0x10] { + let mut bytes = original.clone(); + bytes[at] = value; + reseal(&mut bytes); + ask(&bytes); + } + } + for len in 36..original.len() as u32 { + let mut bytes = original.clone(); + declare_len(&mut bytes, len); + ask(&bytes); + } + } + assert!( + walked > 10_000 && refused > 0 && walks_refused > 0, + "walked {walked} entries, refused {refused} tables and {walks_refused} walks: an arm of the sweep reached nothing" + ); +}