diff --git a/Cargo.lock b/Cargo.lock index 89524a21729..50ae6d742cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2668,6 +2668,10 @@ dependencies = [ [[package]] name = "toyos-userbound" version = "0.1.0" +dependencies = [ + "toyos-abi", + "toyos-bootmap", +] [[package]] name = "toyos-wallclock" diff --git a/issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.md b/issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.md new file mode 100644 index 00000000000..57047f0f1ac --- /dev/null +++ b/issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.md @@ -0,0 +1,162 @@ +--- +status: open +kind: defect +opened: 2026-10-07 +--- + +# The `acpi` claim's holder reaches every port and firmware range the kernel did not declare + +The kernel reads and writes for the holder of the `acpi` claim whatever the +firmware's AML names (`SYS_ACPI`, `kernel/src/arch/x86_64/acpi_mode.rs`), and +decides each access by what its address is +(`toyos-userbound/src/firmware.rs`). What that decision passes is wider than +what any one machine's AML needs, because nothing tells the kernel which +regions a machine's tables define until the holder's interpreter has loaded +them: + +- **Every port the kernel did not declare and no other row names**, both + ways. An I/O BAR of a PCI function is such a port, a kernel driver's and a + claim holder's alike: the kernel records the memory BARs it decodes and no + I/O BAR. So is every chipset register the firmware never mentioned. +- **All ACPI NVS and reserved memory**, both ways, outside the FACS and outside + every page a device the kernel knows of decodes in (a window it mapped, a + function's memory BAR): the firmware's own state, which its SMI handlers + read and trust, and any device register firmware typed reserved that is no + BAR and that the kernel maps nothing of. +- **A function's configuration space, to read**, anywhere the MCFG's window + reaches. No configuration write is made: each is refused `ConfigWrite` + until a machine's AML makes one, and the arm that then passes it is designed + against that write. + +What the kernel's declarations do not follow: + +- **A declared block that moves.** `PM1a_CNT`, the TCO block and `SMI_CMD` are + declared by the port numbers the tables gave at boot. The registers that + place those blocks are in configuration space, which the holder cannot + write, but a chipset reaches them a second way wherever it mirrors them in + memory firmware typed reserved or behind an undeclared index and data pair: + a write there that moved a block would leave its registers on ports nothing + declared, read-only no longer. +- **A port the firmware traps.** An access to an undeclared port that raises an + SMI is a stay in SMM for as long as the firmware's handler takes, made with + the mediation's spinlock held; the kernel neither bounds it nor counts it, + where it counts the SMI its own `SMI_CMD` write raises. +- **A machine whose FADT names no `SMI_CMD`.** Nothing is declared there, so + the chipset's software-SMI port is a port like any other and the holder + writes it; the write is refused by name only where the FADT names the port. + +So a bug in `/system/bin/acpiserver`, or AML it runs, can reach those; the +kernel bounds where, and not what. The owner's ruling on the server reading +and writing firmware-owned memory, ports and PCI configuration space through a +kernel-checked call, as the orchestrator's brief for this slice records it: +"just do it properly the first time i dont know what it is but no workarounds +as always and do the work whatever that is". That the surface so built is +address-bounded and no narrower is this change's design, not his ruling, and +`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md` owns +this beside +`issues/the-acpi-servers-holder-drives-the-embedded-controller-unfiltered.md`. + +What the T14's tables need was measured without the machine, on a model whose +reads answer zero: at load, reads of six ACPI NVS pages and of four functions' +configuration space, and no write; across its initialisation and query +methods, memory writes in three pages, port writes to two ports nothing +declared and to `SMI_CMD`, and no configuration write. Which UEFI types the +real bases fall in is unread. + +Where that machine's devices decode is read: by its firmware's map as two +recorded boots give it (ToyOS's own, whose `pcidev` lists what the map, the +BARs and the bridges' forwarded ranges leave free, and Linux's print of the +same map), none of the 21 memory BARs of its 24 functions is at an address the +map lists, 16 of them past the end of the direct map; nor, by Linux's print, +is the I/O APIC, the HPET or any of the four DMA remapping units. There a BAR +and a kernel-driven window answered `MemoryType` or `Unmapped` before the +kernel's record refused them, and the record is what refuses them on a machine +whose firmware lists such a range as reserved. + +**The holder reads runtime-services data whole** (the orchestrator's ruling, +not the owner's). The T14's firmware keeps the FADT and every definition +block its XSDT lists in `EfiRuntimeServicesData`, read on that machine: the +server's first load there was refused `MemoryType` in type 6 for all 30 such +entries, where UEFI 2.10 §2.3.4 has tables in ACPI reclaim or NVS memory. A +read there passes now, as in the other three firmware types; a write stays +refused `TableWrite` until a machine's AML is measured making one, and +`EfiRuntimeServicesCode` stays refused both ways. No memory the kernel hands +out has the type, so no kernel or process memory is reached by it. What it +costs is that the holder reads whatever else a firmware keeps in +runtime-services data, which nothing here has listed: its variable store's +working copy and its services' own state are candidates, unread. + +**The holder reads a register at an address the firmware's map does not +list** (the orchestrator's ruling, not the owner's). With its tables readable, +the T14's load was refused one table of 14 for one byte read at an address +the map lists nowhere, below 4 GiB, outside the ECAM window, in no page the +kernel drives and no function's BAR: by its place, the chipset's own register +space. Such a read passes now where the address is inside the direct map and +the boot processor's range registers, as the kernel read them at boot, type +it uncacheable (`acpi_mode::uncached`, decided by `kernel::mtrr`); a write +stays refused, range registers that are off type nothing, so there every +such read is refused, and on a machine where any CPU's range registers are +on and are not the boot processor's every such read is refused +`RangeRegistersDiffer`. + +What keeps kernel and process memory out of that read is not the range +registers. The allocator hands out only memory the firmware's map lists as +usable (`toyos_bootmap::is_usable_type`), so memory the map does not list +holds nothing ToyOS put there, whatever it is; and an access is refused where +any usable range of the map holds a byte of it, whichever range lists that +byte first. Three things are not checked: + +- **The range registers are not the effective type everywhere.** A processor + that types RAM from 4 GiB to its top of memory write-back by a + configuration bit outside the range registers (AMD's `SYSCFG` and `TOM2`) + answers the registers' default type for that RAM, which is uncacheable on + such firmware: an unlisted range of RAM above 4 GiB inside the direct map + is read there as a register would be. Nothing reads that bit. What such a + read reaches is RAM the map left out, which the allocator never handed + out. +- **A CPU whose range registers are off reads a register uncached, and one + whose are on and differ stops the read; nothing makes them the boot + processor's.** The read is made on whichever CPU the call runs on. Each + CPU reads its own range registers as it comes up and the kernel says how + they stand beside the boot processor's (`arch::mtrr::compare`, decided by + `kernel::mtrr::beside`): the same words; off, where every read that CPU + makes is uncached by the architecture, so a register is still read once; + or on and not the same, where what the boot processor's say of an address + is not known of that CPU's read, and every unlisted read on the machine is + refused `RangeRegistersDiffer`. Measured, three readings of the kernel's + lines: on the T14's `acpi_tables_loaded` boot the boot processor has 10 + variable pairs and each of its seven other CPUs has the boot processor's + words; on a QEMU 11.1.1 q35 guest under KVM the boot processor reads + `IA32_MTRR_DEF_TYPE` as 0xc06 with 8 variable pairs and the second CPU has + the boot processor's words; on a QEMU 11.1.1 q35 guest under TCG the boot + processor reads the same and the second CPU reads `IA32_MTRR_DEF_TYPE` as + 0, off. So the on-and-different arm has run on no machine, and only TCG's + second CPU is not the same. Firmware is to leave them the + same (Intel SDM Vol. 3A, "MTRR Considerations in MP Systems"), and a + kernel that programmed every other CPU's from the boot processor's would + make them so and the refusal unreachable; this kernel programs no range + register. Two things are open: + - **Who owns the other CPUs' range registers**, firmware as now or the + kernel. Owner: the owner, whose decision it is. **Exit**: he rules, and + the kernel either programs them and asserts them as it does a control + register, or this bullet records that it never will. + - **Why the second CPU of the guest under TCG has them off is unread**: + whether that guest's firmware programs only the boot processor's, or the + emulation resets them when the CPU is started, where under KVM the same + QEMU version leaves them the same. Whether the two guests ran the same + firmware build is unread too. Owner: the stage, + `issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`. + **Exit**: the firmware's source or the emulator's read for that CPU, + and this bullet says which of the two it is. +- **The fixed range registers are not read**, so an unlisted address below + 1 MiB is refused whole, a register there included + (`firmware::FIXED_RANGE_END`). +- **A read of a register can have an effect in the device** — a status bit + cleared, a FIFO advanced — that the kernel cannot know: it bounds where + the holder reads and not what reading does there. On the T14 one such read + is measured, at load, in one page; what the initialisation and query + methods read there is unread. + +**Exit**: an access is passed only inside a region the machine's loaded tables +define, checked by something other than the holder; or the owner rules the +address-only bound is the one ToyOS keeps. diff --git a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md index 21f71d903b3..a28dfebec60 100644 --- a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md +++ b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md @@ -10,8 +10,9 @@ The T14's firmware hands the machine over in legacy mode, which interrupts every CPU every 2.2 s (`issues/the-t14s-firmware-interrupts-every-cpu-every-2-2-s-under-toyos.md`). -Nothing in the tree evaluates the AML in a machine's DSDT or SSDTs. What -ToyOS takes from them it takes another way: `toyos-acpi/src/dsdt.rs` finds +The ACPI server loads a machine's DSDT and SSDTs and evaluates `\_S5`, and +nothing else of their AML yet. What ToyOS takes from them it takes another +way: `toyos-acpi/src/dsdt.rs` finds `\_S5_` by a byte scan, and the loader asks UEFI for the root bridges' windows `_CRS` would name (`bootloader/src/rootbridge.rs`). Nothing reads `_CST`, which names a CPU's C-states. @@ -136,7 +137,8 @@ written. **Stage: the interpreter** (the orchestrator's placement of "The AML stage closes it"). ToyOS's own AML interpreter, written from the specification, run by the ACPI server, the battery first: `userland/acpiserver/aml`, pure and -host-tested, beside the server, which does not link it yet. It owns +host-tested, beside the server, which loads the tables with it +(`userland/acpiserver/src/aml.rs`). It owns `issues/the-t14s-power-button-event-came-up-to-17-s-after-ec-query-0x28.md`. **Exit**: a host test loads QEMU 11.1.1's DSDT (`toyos-acpi/fixtures/qemu-11.1.1/dsdt.bin`) and evaluates `\_S5` to the @@ -231,6 +233,28 @@ otherwise pay to find again: tables load under the meter as it stands, the arena's free slots after the last read with them. +What the server's load of the tables, on the T14 and through the kernel's +mediated access, leaves open: + +- **The server waits for no release of the Global Lock** (the orchestrator's + ruling, not the owner's). A take that finds the firmware holding the lock + leaves it the request, as ACPI 6.5 §5.2.10.1 has it, and is then denied by + name and counted in the server's ledger; the access under it is not made, + and the table or method that asked is refused. The wait that section + describes, for the SCI the firmware raises with `GBL_STS`, is not in the + tree: no tier reached it, and the T14's load took the lock 241 times and + found the firmware holding it in none. Owner: this stage. **Exit**: a + machine's log carries the denial, `the Global Lock: the firmware holds it`, + which the `acpi_tables_loaded` row reds on as on every refusal; the wait + comes back with the test that reaches its port sequence. +- **A press during the load waits for it.** The server arms the power button + and then loads the tables before it serves an SCI, so a press in that time + latches and is served when the load ends: 77 ms on the T14, measured once, + and bounded only by what the interpreter lets each table sleep, 10 s. + Owner: this stage. **Exit**: the slice that keeps the namespace serves the + SCI while a table loads, or the `acpi_tables_loaded` row holds the load's + time on the T14 under a bound the owner names. + The press issue's measurement of 2026-10-07 found the three presses it lost changing nothing its scout read, with the button's event enabled and no SMI taken, and its hypothesis is that the controller wants the firmware's diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index e00d6a63bb7..2844d0ed557 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -180,6 +180,10 @@ version = "0.1.0" [[package]] name = "toyos-userbound" version = "0.1.0" +dependencies = [ + "toyos-abi", + "toyos-bootmap", +] [[package]] name = "toyos-wallclock" diff --git a/kernel/pure/lib.rs b/kernel/pure/lib.rs index 67a1c47742a..86f6f211fda 100644 --- a/kernel/pure/lib.rs +++ b/kernel/pure/lib.rs @@ -1,6 +1,7 @@ //! What the kernel decides without touching the machine: the scheduler core -//! ([`sched`]), the process and thread lifecycle ([`proclife`]) and which PCID an -//! address space is handed ([`pcid`]). The kernel binary links it; the host runs +//! ([`sched`]), the process and thread lifecycle ([`proclife`]), which PCID an +//! address space is handed ([`pcid`]) and what type the range registers give a +//! range ([`mtrr`]). The kernel binary links it; the host runs //! its tests, because none of it reads a register, a clock or a kernel lock. #![no_std] @@ -10,6 +11,7 @@ extern crate alloc; #[cfg(test)] extern crate std; +pub mod mtrr; pub mod pcid; pub mod proclife; pub mod sched; diff --git a/kernel/pure/mtrr.rs b/kernel/pure/mtrr.rs new file mode 100644 index 00000000000..da5dc942e0c --- /dev/null +++ b/kernel/pure/mtrr.rs @@ -0,0 +1,346 @@ +//! What memory type the processor's variable range registers give a physical +//! range, decided from the words read off them: `IA32_MTRR_DEF_TYPE` and each +//! `IA32_MTRR_PHYSBASEn`, `IA32_MTRR_PHYSMASKn` pair. +//! +//! The fixed range registers, which type the first 1 MiB, are not among +//! them: an answer for a range below 1 MiB is the variable registers' alone. +//! Nor is anything outside the range registers that types memory, a +//! processor's own configuration bit for RAM above 4 GiB among them. +//! +//! A range has one type or it has none ([`Unknown`]): picking one where the +//! registers give two would be inventing an answer firmware never gave. +//! +//! The registers are each CPU's own. [`beside`] says how one CPU's stand +//! beside the boot processor's, where firmware wrote what it typed the +//! machine's memory. + +#![forbid(unsafe_code)] + +/// Bit 11 of `IA32_MTRR_DEF_TYPE`: clear means the whole address space is UC. +const DEF_TYPE_ENABLE: u64 = 1 << 11; +/// Bit 11 of an `IA32_MTRR_PHYSMASK`. +const PHYSMASK_VALID: u64 = 1 << 11; +/// Physical address bits of a PHYSBASE/PHYSMASK: 4 KiB-aligned, masked to the +/// 52-bit architectural ceiling, never narrower than a CPU's real width. +const PHYS_MASK: u64 = 0x000F_FFFF_FFFF_F000; + +/// A memory type in the MTRRs' architectural encoding, matching the MSR values. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum MemoryType { + Uncacheable, + WriteCombining, + WriteThrough, + WriteProtected, + WriteBack, +} + +impl MemoryType { + fn from_encoding(raw: u8) -> Option { + match raw { + 0x00 => Some(Self::Uncacheable), + 0x01 => Some(Self::WriteCombining), + 0x04 => Some(Self::WriteThrough), + 0x05 => Some(Self::WriteProtected), + 0x06 => Some(Self::WriteBack), + _ => None, + } + } + + pub fn name(self) -> &'static str { + match self { + Self::Uncacheable => "UC", + Self::WriteCombining => "WC", + Self::WriteThrough => "WT", + Self::WriteProtected => "WP", + Self::WriteBack => "WB", + } + } +} + +/// Why a range has no single answer. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Unknown { + /// A register holds an encoding the architecture does not define. + ReservedEncoding, + /// Overlapping MTRRs whose types the architecture leaves undefined. + Conflicting, + /// Part of the range is covered and part is not. + PartiallyCovered, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Effective { + Known(MemoryType), + Unknown(Unknown), + /// MTRRs are off, so the whole address space is UC by architecture. + MtrrsDisabled, +} + +impl Effective { + pub fn name(&self) -> &'static str { + match self { + Self::Known(t) => t.name(), + Self::MtrrsDisabled => "UC (MTRRs disabled)", + Self::Unknown(Unknown::ReservedEncoding) => "unknown (reserved MTRR encoding)", + Self::Unknown(Unknown::Conflicting) => "unknown (overlapping MTRRs disagree)", + Self::Unknown(Unknown::PartiallyCovered) => "unknown (range only partly covered)", + } + } + + /// Whether the range registers type the range uncacheable, which is what + /// tells a register from RAM. Registers that are off type nothing: all + /// RAM is uncacheable under them too, so that answer tells neither. + pub fn typed_uncacheable(&self) -> bool { + matches!(self, Self::Known(MemoryType::Uncacheable)) + } +} + +/// Effective type of a WC-PAT page over range `mtrr`: WC wins even over an +/// MTRR's UC (SDM Vol. 3A Table 11-7); `None` only when `mtrr` has no single +/// answer. +pub fn effective_under_wc(mtrr: &Effective) -> Option { + match mtrr { + Effective::Known(_) | Effective::MtrrsDisabled => Some(MemoryType::WriteCombining), + Effective::Unknown(_) => None, + } +} + +/// Two MTRRs over one address: UC beats anything, WT beats WB, else undefined. +fn combine(a: MemoryType, b: MemoryType) -> Option { + use MemoryType::{Uncacheable, WriteBack, WriteThrough}; + match (a, b) { + (x, y) if x == y => Some(x), + (Uncacheable, _) | (_, Uncacheable) => Some(Uncacheable), + (WriteThrough, WriteBack) | (WriteBack, WriteThrough) => Some(WriteThrough), + _ => None, + } +} + +/// The memory type of `first..=last` under the default-type word `def_type` +/// and `pairs`, each variable register's `(PHYSBASE, PHYSMASK)`. +pub fn range_type(def_type: u64, pairs: impl IntoIterator, first: u64, last: u64) -> Effective { + if def_type & DEF_TYPE_ENABLE == 0 { + return Effective::MtrrsDisabled; + } + let Some(default) = MemoryType::from_encoding(def_type as u8) else { + return Effective::Unknown(Unknown::ReservedEncoding); + }; + + let mut covering: Option = None; + for (base, mask) in pairs { + if mask & PHYSMASK_VALID == 0 { + continue; + } + // A PHYSMASK's contiguous high bits size the region: from PHYSBASE + // under the mask to every address below the mask's lowest set bit + // above it. A mask with no address bit matches every address. + let phys_mask = mask & PHYS_MASK; + let region_first = base & phys_mask; + let region_last = region_first | phys_mask.isolate_lowest_one().wrapping_sub(1); + if region_last < first || region_first > last { + continue; + } + if region_first > first || region_last < last { + return Effective::Unknown(Unknown::PartiallyCovered); + } + let Some(t) = MemoryType::from_encoding(base as u8) else { + return Effective::Unknown(Unknown::ReservedEncoding); + }; + covering = Some(match covering { + None => t, + Some(prev) => match combine(prev, t) { + Some(merged) => merged, + None => return Effective::Unknown(Unknown::Conflicting), + }, + }); + } + Effective::Known(covering.unwrap_or(default)) +} + +/// How a CPU's range registers stand beside the boot processor's. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Beside { + /// Word for word the boot processor's: this CPU reads every range as the + /// type [`range_type`] gives it under those. + Same, + /// Not the boot processor's, and off: this CPU reads every range + /// uncached, whatever firmware typed it. + Off, + /// Not the boot processor's, and on: nothing the boot processor's say of + /// a range is known of a read this CPU makes there. + Different, +} + +/// One CPU's registers, `other`, beside the boot processor's: each the +/// default-type word and every variable register's `(PHYSBASE, PHYSMASK)`. +/// The words whole, a register whose valid bit is clear included: two +/// snapshots that type every range alike and are not the same words are +/// [`Beside::Different`], which refuses and never passes. +pub fn beside(boot: (u64, &[(u64, u64)]), other: (u64, &[(u64, u64)])) -> Beside { + if other == boot { + Beside::Same + } else if other.0 & DEF_TYPE_ENABLE == 0 { + Beside::Off + } else { + Beside::Different + } +} + +#[cfg(test)] +mod tests { + use super::MemoryType::{Uncacheable as UC, WriteBack as WB, WriteCombining as WC, WriteProtected as WP, WriteThrough as WT}; + use super::*; + + const ON: u64 = DEF_TYPE_ENABLE; + const GIB: u64 = 1 << 30; + const MIB: u64 = 1 << 20; + + /// A valid pair typing `size` bytes at `base`, a power of two of them, on + /// a processor of 39 physical address bits. + fn pair(base: u64, size: u64, encoding: u64) -> (u64, u64) { + assert!(size.is_power_of_two() && base.is_multiple_of(size)); + (base | encoding, !(size - 1) & ((1 << 39) - 1) & PHYS_MASK | PHYSMASK_VALID) + } + + fn known(def_type: u64, pairs: &[(u64, u64)], first: u64, last: u64) -> Effective { + range_type(def_type, pairs.iter().copied(), first, last) + } + + #[test] + fn a_range_no_register_covers_has_the_default_type() { + for (encoding, ty) in [(0, UC), (1, WC), (4, WT), (5, WP), (6, WB)] { + assert_eq!(known(ON | encoding, &[], 0x10_0000, 0x10_0fff), Effective::Known(ty)); + // A pair elsewhere, and one whose valid bit is clear over the range itself. + let pairs = [pair(GIB, GIB, 0), (2 * GIB, !(GIB - 1) & PHYS_MASK)]; + assert_eq!(known(ON | encoding, &pairs, 2 * GIB, 2 * GIB + 7), Effective::Known(ty)); + } + } + + #[test] + fn a_default_or_a_covering_register_of_no_defined_encoding_is_unknown() { + for encoding in [2u64, 3, 7, 0xFF] { + assert_eq!(known(ON | encoding, &[], 0, 7), Effective::Unknown(Unknown::ReservedEncoding), "default {encoding}"); + assert_eq!( + known(ON, &[pair(GIB, GIB, encoding)], GIB, GIB + 7), + Effective::Unknown(Unknown::ReservedEncoding), + "a register's {encoding}" + ); + // The same register over another range decides nothing of this one. + assert_eq!(known(ON, &[pair(GIB, GIB, encoding)], 2 * GIB, 2 * GIB + 7), Effective::Known(UC)); + } + } + + #[test] + fn a_register_types_its_region_to_the_last_byte_and_no_further() { + // Write-back by default, and the 256 MiB under 4 GiB uncacheable. + let hole = [pair(4 * GIB - 256 * MIB, 256 * MIB, 0)]; + let (start, end) = (4 * GIB - 256 * MIB, 4 * GIB); + assert_eq!(known(ON | 6, &hole, start, start), Effective::Known(UC)); + assert_eq!(known(ON | 6, &hole, end - 8, end - 1), Effective::Known(UC)); + assert_eq!(known(ON | 6, &hole, start, end - 1), Effective::Known(UC)); + assert_eq!(known(ON | 6, &hole, start - 1, start - 1), Effective::Known(WB)); + assert_eq!(known(ON | 6, &hole, end, end + 7), Effective::Known(WB)); + // One byte out of it, by either end, is two types and so none. + assert_eq!(known(ON | 6, &hole, start - 1, start), Effective::Unknown(Unknown::PartiallyCovered)); + assert_eq!(known(ON | 6, &hole, end - 1, end), Effective::Unknown(Unknown::PartiallyCovered)); + assert_eq!(known(ON | 6, &hole, start - 8, end + 7), Effective::Unknown(Unknown::PartiallyCovered)); + } + + #[test] + fn two_registers_over_one_range_answer_as_the_architecture_orders_them() { + let over = |a: u64, b: u64| known(ON | 6, &[pair(GIB, GIB, a), pair(GIB, 256 * MIB, b)], GIB, GIB + 7); + // Uncacheable over anything, whichever register says it. + for other in [1, 4, 5, 6] { + assert_eq!(over(0, other), Effective::Known(UC)); + assert_eq!(over(other, 0), Effective::Known(UC)); + } + assert_eq!(over(4, 6), Effective::Known(WT)); + assert_eq!(over(6, 4), Effective::Known(WT)); + for same in [(0, UC), (1, WC), (4, WT), (5, WP), (6, WB)] { + assert_eq!(over(same.0, same.0), Effective::Known(same.1)); + } + // Every other pair of types is undefined. + for (a, b) in [(1, 4), (1, 5), (1, 6), (4, 5), (5, 6)] { + assert_eq!(over(a, b), Effective::Unknown(Unknown::Conflicting), "{a} over {b}"); + assert_eq!(over(b, a), Effective::Unknown(Unknown::Conflicting), "{b} over {a}"); + } + // A third register that is uncacheable does not undo a conflict the first two made. + let three = [pair(GIB, GIB, 1), pair(GIB, GIB, 6), pair(GIB, GIB, 0)]; + assert_eq!(known(ON | 6, &three, GIB, GIB + 7), Effective::Unknown(Unknown::Conflicting)); + // Past the smaller register the larger decides alone. + assert_eq!(known(ON | 6, &[pair(GIB, GIB, 1), pair(GIB, 256 * MIB, 5)], GIB + 256 * MIB, GIB + 256 * MIB + 7), Effective::Known(WC)); + } + + #[test] + fn registers_that_are_off_type_nothing_uncacheable() { + // The enable bit clear, whatever the default type and the pairs say. + for def_type in [0u64, 6, 0xFF, 1 << 10] { + let off = known(def_type, &[pair(GIB, GIB, 0)], GIB, GIB + 7); + assert_eq!(off, Effective::MtrrsDisabled); + assert!(!off.typed_uncacheable(), "registers that are off were read as typing a range uncacheable"); + } + assert!(Effective::Known(UC).typed_uncacheable()); + for ty in [WC, WT, WP, WB] { + assert!(!Effective::Known(ty).typed_uncacheable(), "{ty:?}"); + } + for unknown in [Unknown::ReservedEncoding, Unknown::Conflicting, Unknown::PartiallyCovered] { + assert!(!Effective::Unknown(unknown).typed_uncacheable(), "{unknown:?}"); + } + } + + #[test] + fn a_mask_with_no_address_bit_and_a_region_at_the_top_decide_without_overflow() { + // A valid mask of no address bit matches every address. + let all = [(0, PHYSMASK_VALID)]; + assert_eq!(known(ON | 6, &all, 0, u64::MAX), Effective::Known(UC)); + assert_eq!(known(ON | 6, &all, u64::MAX, u64::MAX), Effective::Known(UC)); + // The last 4 KiB the registers can name, and the byte after it. + let top = [(PHYS_MASK, PHYS_MASK | PHYSMASK_VALID)]; + assert_eq!(known(ON | 6, &top, PHYS_MASK, PHYS_MASK | 0xFFF), Effective::Known(UC)); + assert_eq!(known(ON | 6, &top, (PHYS_MASK | 0xFFF) + 1, u64::MAX), Effective::Known(WB)); + } + + /// Firmware's registers on a machine of write-back RAM and an uncacheable + /// hole under 4 GiB. + const BOOT: (u64, [(u64, u64); 2]) = (ON | 6, [(0xC000_0000, 0x7F_C000_0800), (0, 0)]); + + #[test] + fn registers_that_are_word_for_word_the_boot_processors_are_the_same() { + assert_eq!(beside((BOOT.0, &BOOT.1), (BOOT.0, &BOOT.1)), Beside::Same); + // Off on both, the same words: the same, and typing nothing. + assert_eq!(beside((6, &BOOT.1), (6, &BOOT.1)), Beside::Same); + assert_eq!(beside((ON, &[]), (ON, &[])), Beside::Same); + } + + #[test] + fn registers_that_are_off_and_not_the_boot_processors_are_off() { + // As a CPU comes out of reset: every word zero. + assert_eq!(beside((BOOT.0, &BOOT.1), (0, &[(0, 0); 2])), Beside::Off); + // The enable bit alone clear, every other word the boot processor's. + assert_eq!(beside((BOOT.0, &BOOT.1), (6, &BOOT.1)), Beside::Off); + // Off on both and not the same words. + assert_eq!(beside((6, &BOOT.1), (0, &BOOT.1)), Beside::Off); + } + + #[test] + fn registers_that_are_on_and_not_the_boot_processors_are_different() { + let boot = (BOOT.0, &BOOT.1[..]); + // Another default type; the fixed registers' enable bit; a pair's + // base, its type, its mask and its valid bit; a pair more, and one fewer. + let others: [(u64, &[(u64, u64)]); 8] = [ + (ON, &BOOT.1), + (ON | 6 | 1 << 10, &BOOT.1), + (ON | 6, &[(0x8000_0000, 0x7F_C000_0800), (0, 0)]), + (ON | 6, &[(0xC000_0006, 0x7F_C000_0800), (0, 0)]), + (ON | 6, &[(0xC000_0000, 0x7F_8000_0800), (0, 0)]), + (ON | 6, &[(0xC000_0000, 0x7F_C000_0000), (0, 0)]), + (ON | 6, &[(0xC000_0000, 0x7F_C000_0800), (0, 0), (0, 0)]), + (ON | 6, &[(0xC000_0000, 0x7F_C000_0800)]), + ]; + for other in others { + assert_eq!(beside(boot, other), Beside::Different, "{other:x?}"); + } + // On where the boot processor's are off. + assert_eq!(beside((6, &BOOT.1), boot), Beside::Different); + } +} diff --git a/kernel/src/arch/aarch64/acpi_mode.rs b/kernel/src/arch/aarch64/acpi_mode.rs index 46eef7140af..eca660cf4f1 100644 --- a/kernel/src/arch/aarch64/acpi_mode.rs +++ b/kernel/src/arch/aarch64/acpi_mode.rs @@ -13,3 +13,21 @@ pub fn claim() -> Result<(usize, AcpiInfo), ClaimError> { pub fn release(_row: usize) { unreachable!("AArch64 has no ACPI row") } + +/// Never called, as [`release`]: the three are reached only with a claim. +pub fn access(_row: usize, _request: &mut toyos_abi::acpi::Access) -> Result<(), toyos_abi::syscall::SyscallError> { + unreachable!("AArch64 has no ACPI row") +} + +pub fn lock_take() -> Result { + unreachable!("AArch64 has no ACPI row") +} + +pub fn lock_release() -> Result<(), toyos_abi::syscall::SyscallError> { + unreachable!("AArch64 has no ACPI row") +} + +#[cfg(feature = "test-actuators")] +pub fn debug_firmware_lock(_act: u64) -> u64 { + toyos_abi::syscall::SyscallError::NotSupported.to_u64() +} diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index fb9ac1bec61..7735c082585 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -26,6 +26,35 @@ //! //! The row is the FADT's PM1a event and GPE0 blocks and the ECDT's two //! ports, filled once at boot; the SCI is its one line, level. +//! +//! **What the firmware's AML names outside the row, this kernel reads and +//! writes for the claim's holder, one access at a time** ([`access`]): memory +//! through the direct map and a port both ways, a function's configuration +//! space through ECAM to read, each only with the witness +//! `toyos_userbound::firmware` answered for it. Firmware's memory is typed by +//! firmware's own map and by its MTRRs: the direct map's leaves select the +//! PAT's write-back entry, under which the range registers decide (Intel SDM +//! Vol. 3A, Table 12-7), so a register window the firmware reserved is read as +//! the firmware typed it. A register at an address the firmware's map does +//! not list is read the same way, only where the boot processor's registers, +//! read once at boot, type it uncacheable, and only on a machine none of +//! whose CPUs holds registers that are on and not those (`mtrr::compare`): +//! the read is made on whichever CPU the call runs on. +//! +//! **The firmware's Global Lock is taken and given back here** (ACPI 6.5 +//! §5.2.10.1), by compare-and-exchange on the FACS's lock word; a release the +//! firmware asked for meanwhile is signalled by `GBL_RLS` in `PM1a_CNT` +//! (§4.8.3.2). A lock its holder left taken goes back with the claim, and +//! before the power-off, so SMM never waits on a process that is gone. A +//! machine whose FADT names no FACS has no lock, and every take is answered +//! taken; one whose FACS this kernel refuses has a lock nothing here can +//! take, and every take is refused ([`GlobalLock`]). +//! +//! **Nothing is done for the holder once the stop has begun**, as nothing is +//! written to `SMI_CMD`: an access and a lock exchange are each made under +//! [`HOLDER`], from the decision to the last instruction, and refused there +//! once the stop has begun; the power-off takes that lock before it owns the +//! hardware ([`settle`]), which waits out the one in flight. use alloc::boxed::Box; use alloc::format; @@ -33,8 +62,12 @@ use alloc::string::String; use alloc::vec; use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; -use toyos_abi::acpi::{AcpiInfo, Block, FIXED_POWER_BUTTON}; +use core::sync::atomic::AtomicU32; + +use toyos_abi::acpi::{Access, AcpiInfo, Block, Refused, Space, Width, FIXED_POWER_BUTTON}; +use toyos_abi::syscall::SyscallError; use toyos_acpi::{Ec, FixedHardware, LegacyMode, PowerButton}; +use toyos_userbound::firmware::{self, Ecam, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt}; use toyos_userbound::Ports; use super::pio::{self, Declared, TakenBack}; @@ -43,6 +76,7 @@ use super::{cpu, smi_cmd}; use crate::device::ClaimError; use crate::isa::{self, Function}; use crate::log; +use crate::sync::{Lock, LockGuard}; use crate::time::{Deadline, Duration}; /// `isa`'s row for the fixed hardware. @@ -68,8 +102,70 @@ struct Hardware { legacy: Option, /// Or why it is none a holder can be handed. ec: Result, + rsdp: u64, + /// The window configuration space is reached through, as the MCFG bounds it. + ecam: Option, + lock: GlobalLock, } +/// The firmware's Global Lock, as this machine's FADT has it. +#[derive(Clone, Copy)] +enum GlobalLock { + /// The FADT names no FACS: the machine has no lock, and a take is + /// answered taken. + Absent, + /// The FADT names a FACS this kernel exchanges no word in: a holder told + /// it had the lock would hold one that excludes nothing, so a take is + /// refused. + Refused, + At(Facs), +} + +impl GlobalLock { + fn facs(self) -> Option { + match self { + Self::At(facs) => Some(facs), + Self::Absent | Self::Refused => None, + } + } +} + +/// The FACS, as `(start, end)`, and its lock word. +#[derive(Clone, Copy)] +struct Facs { + span: (u64, u64), + word: LockWordAt, +} + +/// What this kernel does for the claim's holder, and for whom. +struct Holder { + /// A claim exists: nothing is done for a holder that is gone. + claimed: bool, + /// The holder took the Global Lock and has not given it back. + locked: bool, +} + +/// Held across everything this kernel does for the claim's holder, from the +/// decision to the last instruction of the act: each mediated access, and +/// each change of the lock word. +static HOLDER: Lock = Lock::new(Holder { claimed: false, locked: false }); + +/// The right to act for the claim's holder, held across the act; none once +/// the claim is gone or the stop has begun. A claim that exists is the one +/// the caller's process bound: `isa::claim_row` mints no next one while that +/// process has a thread left, one inside this call included. +fn acting() -> Result, SyscallError> { + let holder = HOLDER.lock(); + if !holder.claimed || crate::quiesce::begun() { + return Err(SyscallError::Gone); + } + Ok(holder) +} + +/// PM1 control's `GBL_RLS` (ACPI 6.5 §4.8.3.2): written by the OS to tell the +/// firmware the Global Lock it asked for is free. +const GBL_RLS: u16 = 1 << 2; + /// Written once, by [`init`]. static HARDWARE: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); @@ -77,6 +173,16 @@ static HARDWARE: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); /// the release writes `ACPI_DISABLE`. static ENABLED: AtomicBool = AtomicBool::new(false); +/// Wait out whatever is being done for the claim's holder, and give back a +/// Global Lock it was stopped holding: the stop has begun, so no access or +/// take follows. +pub fn settle(_taken: &TakenBack) { + let mut holder = HOLDER.lock(); + if let Some(hardware) = hardware() { + give_back(hardware, &mut holder, "the machine is stopping"); + } +} + fn hardware() -> Option<&'static Hardware> { let at = HARDWARE.load(Ordering::Acquire); // SAFETY: `init` stored a leaked `Box` once, and nothing frees it. @@ -140,10 +246,55 @@ pub fn init(rsdp_addr: u64) { if cpu::inw(control.port(0)) & SCI_EN != 0 { "ACPI" } else { "legacy" }, ); isa::fill(ROW, Function { name: "the ACPI fixed hardware", runs, irqs: vec![], wires: vec![sci] }); - let was = HARDWARE.swap(Box::into_raw(Box::new(Hardware { fixed, control, legacy, ec })), Ordering::Release); + let (ecam, lock) = (ecam(rsdp_addr), global_lock(&fadt)); + let hardware = Hardware { fixed, control, legacy, ec, rsdp: rsdp_addr, ecam, lock }; + let was = HARDWARE.swap(Box::into_raw(Box::new(hardware)), Ordering::Release); assert!(was.is_null(), "acpi: init ran twice"); } +/// The ECAM window as the MCFG's first allocation bounds it (PCI Firmware +/// Specification 3.3, Table 4-3: the segment group at +8 of the entry, the +/// first and last bus at +10 and +11). +fn ecam(rsdp_addr: u64) -> Option { + let (mcfg, base) = toyos_acpi::ecam_base(crate::drivers::acpi::direct_phys(), rsdp_addr).ok()?; + let entry = toyos_acpi::MCFG_FIRST_ENTRY; + let (segment, first_bus, last_bus) = (mcfg.u16_at(entry + 8)?, mcfg.byte(entry + 10)?, mcfg.byte(entry + 11)?); + if first_bus > last_bus { + log!("acpi: the MCFG's window ends at bus {last_bus:#x}, before its first, {first_bus:#x}: no configuration access is mediated"); + return None; + } + Some(Ecam { base, segment, first_bus, last_bus }) +} + +/// The Global Lock of the FACS the FADT names, said by name where there is +/// none or it is none this kernel takes: a FACS that does not decode, and one +/// whose lock word is not in memory the firmware's map gives the firmware. +fn global_lock(fadt: &toyos_acpi::Table

) -> GlobalLock { + let refused = |why: core::fmt::Arguments| { + log!("acpi: a Global Lock this kernel cannot take — {why}: every take is refused"); + GlobalLock::Refused + }; + let facs = match toyos_acpi::facs(fadt.phys(), fadt) { + Ok(facs) => facs, + Err(toyos_acpi::FacsRefused::Absent) => { + log!("acpi: no Global Lock — the FADT names no FACS: every take is answered taken"); + return GlobalLock::Absent; + } + Err(why) => return refused(format_args!("the FADT's FACS is none this kernel reads ({why:?})")), + }; + let map = crate::mm::firmware_map(); + let word = match firmware::lock_word(map, crate::mm::direct_map_end().get(), facs.base + toyos_acpi::FACS_GLOBAL_LOCK) { + Ok(word) => word, + Err(why) => return refused(format_args!("the lock word of the FADT's FACS at {:#x} is none this kernel exchanges ({why:?})", facs.base)), + }; + log!( + "acpi: the Global Lock is the FACS's at {:#x}, in memory the firmware's map types {}", + word.at(), + firmware::type_word(map, word.at()) + ); + GlobalLock::At(Facs { span: (facs.base, facs.base + u64::from(facs.len)), word }) +} + fn run(block: Block) -> Ports { Ports::new(block.port, block.len).expect("`fixed_hardware` bounded every block by the port space") } @@ -165,7 +316,10 @@ pub fn claim() -> Result<(usize, AcpiInfo), ClaimError> { let hardware = hardware().ok_or(ClaimError::Absent)?; let row = isa::claim_row(ROW)?; match enter(hardware) { - Ok(()) => Ok((row, info(hardware))), + Ok(()) => { + HOLDER.lock().claimed = true; + Ok((row, info(hardware))) + } Err(refused) => { isa::release(row); Err(refused) @@ -179,12 +333,14 @@ fn info(hardware: &Hardware) -> AcpiInfo { Err(_) => (Block::NONE, Block::NONE, 0), }; AcpiInfo { + rsdp: hardware.rsdp, pm1_event: hardware.fixed.pm1a_event, gpe0: hardware.fixed.gpe0, ec_command, ec_data, ec_gpe, flags: if hardware.fixed.power_button == PowerButton::Fixed { FIXED_POWER_BUTTON } else { 0 }, + reserved: 0, } } @@ -250,8 +406,14 @@ fn enter(hardware: &Hardware) -> Result<(), ClaimError> { /// mint took it out of it, and then the row released, so no claimant finds /// `SCI_EN` set by a holder whose disable is still to come. pub fn release(row: usize) { + let hardware = hardware().expect("a claimed row has its hardware"); + { + let mut holder = HOLDER.lock(); + holder.claimed = false; + give_back(hardware, &mut holder, "its claim is gone"); + } if ENABLED.load(Ordering::Relaxed) { - leave(hardware().expect("a claimed row has its hardware")); + leave(hardware); } isa::release(row); } @@ -331,3 +493,278 @@ pub fn quiet(taken: &TakenBack) { } } } + +/// The FACS's lock word. One `AtomicU32` and nothing else of the page: the +/// firmware's SMI handlers change it under this kernel. +fn lock_word(word: LockWordAt) -> &'static AtomicU32 { + let at = crate::mm::DirectMap::from_phys(word.at()); + // SAFETY: the policy passed the word: on a dword boundary, all four bytes + // inside the direct map and in memory the firmware's map gives the + // firmware, so it is mapped for the machine's life and no Rust object. + unsafe { &*at.as_ptr::() } +} + +/// Try the Global Lock for the claim's holder: `Ok(true)` taken, +/// `Ok(false)` where the firmware owns it, with the pending bit left set for +/// the firmware's release to answer with `GBL_STS`, and `NotSupported` on a +/// machine whose lock this kernel cannot take. +pub fn lock_take() -> Result { + let hardware = hardware().expect("a claimed row has its hardware"); + let mut holder = acting()?; + if holder.locked { + return Err(SyscallError::AlreadyExists); + } + let taken = match hardware.lock { + GlobalLock::Absent => true, + GlobalLock::Refused => return Err(SyscallError::NotSupported), + GlobalLock::At(facs) => { + let word = lock_word(facs.word); + let mut read = word.load(Ordering::Acquire); + loop { + let (new, acquired) = toyos_acpi::acquire(read); + match word.compare_exchange(read, new, Ordering::AcqRel, Ordering::Acquire) { + Ok(_) => break acquired, + Err(now) => read = now, + } + } + } + }; + holder.locked = taken; + Ok(taken) +} + +/// Give the Global Lock back for the claim's holder. +pub fn lock_release() -> Result<(), SyscallError> { + let hardware = hardware().expect("a claimed row has its hardware"); + let mut holder = acting()?; + if !holder.locked { + return Err(SyscallError::InvalidArgument); + } + give_back(hardware, &mut holder, ""); + Ok(()) +} + +/// Clear the lock word's owner where the claim's holder holds it, and tell +/// the firmware where it asked meanwhile. `orphaned` says why the holder did +/// not give it back itself, for the log; empty where it did. +fn give_back(hardware: &Hardware, holder: &mut Holder, orphaned: &str) { + if !core::mem::take(&mut holder.locked) { + return; + } + let signalled = hardware.lock.facs().is_some_and(|facs| { + let word = lock_word(facs.word); + let mut read = word.load(Ordering::Acquire); + let signal = loop { + let (new, signal) = toyos_acpi::release(read); + match word.compare_exchange(read, new, Ordering::AcqRel, Ordering::Acquire) { + Ok(_) => break signal, + Err(now) => read = now, + } + }; + if signal { + let control = hardware.control.port(0); + // SAFETY: the PM1a control block, declared; `GBL_RLS` over the + // register as it reads, whose `SLP_EN` reads clear (§4.8.3.2). + unsafe { cpu::outw(control, cpu::inw(control) | GBL_RLS) }; + } + signal + }); + if !orphaned.is_empty() { + log!( + "acpi: the Global Lock given back for a holder that left it taken ({orphaned}){}", + if signalled { ", and the firmware, which asked for it meanwhile, told by GBL_RLS" } else { "" } + ); + } +} + +/// `debug_action::ACPI_FIRMWARE_LOCK`: the firmware's side of the lock word. +#[cfg(feature = "test-actuators")] +pub fn debug_firmware_lock(act: u64) -> u64 { + use toyos_abi::syscall::debug_action::{FIRMWARE_ASKS, FIRMWARE_FREES, FIRMWARE_OWNS}; + let Some(facs) = hardware().and_then(|hardware| hardware.lock.facs()) else { return SyscallError::NotSupported.to_u64() }; + let word = lock_word(facs.word); + let was = match act { + FIRMWARE_FREES => word.fetch_and(!(toyos_acpi::OWNED | toyos_acpi::PENDING), Ordering::AcqRel), + FIRMWARE_OWNS => word.fetch_or(toyos_acpi::OWNED, Ordering::AcqRel), + FIRMWARE_ASKS => word.fetch_or(toyos_acpi::PENDING, Ordering::AcqRel), + _ => return SyscallError::InvalidArgument.to_u64(), + }; + u64::from(was) +} + +/// Bytes at an address whatever its alignment, moved by one instruction. +#[repr(C, packed)] +struct Unaligned(T); + +fn read_memory(passed: &MemoryAt) -> u64 { + let at = crate::mm::DirectMap::from_phys(passed.at()); + // SAFETY: the policy passed the range as the firmware's, inside the direct + // map and in no memory this kernel hands out: mapped for the machine's + // life, and no Rust object. + unsafe { + match passed.width() { + Width::Byte => u64::from(at.as_ptr::().read_volatile()), + Width::Word => u64::from(at.as_ptr::>().read_volatile().0), + Width::DWord => u64::from(at.as_ptr::>().read_volatile().0), + Width::QWord => at.as_ptr::>().read_volatile().0, + } + } +} + +fn write_memory(passed: &MemoryAt, value: u64) { + let at = crate::mm::DirectMap::from_phys(passed.at()); + // SAFETY: as `read_memory`, and the policy passed the write: the firmware's + // own reserved or non-volatile memory, outside its tables and the FACS. + unsafe { + match passed.width() { + Width::Byte => at.as_mut_ptr::().write_volatile(value as u8), + Width::Word => at.as_mut_ptr::>().write_volatile(Unaligned(value as u16)), + Width::DWord => at.as_mut_ptr::>().write_volatile(Unaligned(value as u32)), + Width::QWord => at.as_mut_ptr::>().write_volatile(Unaligned(value)), + } + } +} + +fn read_port(passed: &PortAt) -> u64 { + let port = pio::mediated(passed); + match passed.width() { + Width::Byte => u64::from(cpu::inb(port)), + Width::Word => u64::from(cpu::inw(port)), + Width::DWord => u64::from(cpu::inl(port)), + Width::QWord => unreachable!("the policy passes no qword port access"), + } +} + +fn write_port(passed: &PortAt, value: u64) { + let port = pio::mediated(passed); + // SAFETY: the policy passed a write to every port of the span: none this + // kernel declared and keeps, and none another claim's row names. + unsafe { + match passed.width() { + Width::Byte => cpu::outb(port, value as u8), + Width::Word => cpu::outw(port, value as u16), + Width::DWord => cpu::outl(port, value as u32), + Width::QWord => unreachable!("the policy passes no qword port access"), + } + } +} + +/// One configuration access, decided and, where it is a read, made through +/// the window the MCFG names, which the policy bounded the function by. +fn config(hardware: &Hardware, _acting: &Holder, segment: u16, function: PciFunction, offset: u16, width: Width, write: bool) -> Result { + let at = firmware::config(hardware.ecam, segment, function, offset, width, write)?; + let PciFunction { bus, device, function } = at.function(); + let space = crate::drivers::pci::function_window(bus, device, function).expect("a machine with a claimable ACPI row enumerated its PCI functions"); + let offset = u64::from(at.offset()); + 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"), + }) +} + +/// Whether a read of `len` bytes at `at` through the direct map is uncached: +/// its leaves select the PAT's write-back entry, under which the range +/// registers decide (Intel SDM Vol. 3A, Table 12-7). The boot processor's +/// decide it, as read at boot, so the answer is one whichever CPU asks: a +/// CPU whose own are off answers nothing of what firmware typed the range, +/// and reads it uncached all the same. +fn uncached(at: u64, len: u64) -> bool { + let (def_type, pairs) = super::mtrr::boot(); + kernel::mtrr::range_type(def_type, pairs.iter().copied(), at, at + (len - 1)).typed_uncacheable() +} + +/// One memory access, decided and made; the type firmware's map gives its +/// first byte goes back with either. The records of what devices decode are +/// read under their own locks and let go before the access: a window mapped +/// after the decision is one the access was made a moment before. +fn memory(hardware: &Hardware, acting: &Holder, 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| { + crate::mm::paging::with_driven_windows(|driven| { + let memory = Memory { + map, + mapped_end: crate::mm::direct_map_end().get(), + ecam: hardware.ecam, + devices: driven.iter().copied().chain(bars), + facs: hardware.lock.facs().map(|facs| facs.span), + uncached, + registers_differ: super::mtrr::any_differs(), + }; + memory.decide(at, width, write.is_some()) + }) + }); + match (verdict, write) { + (MemoryVerdict::Through(passed), None) => Ok(read_memory(&passed)), + (MemoryVerdict::Through(passed), Some(value)) => { + write_memory(&passed, value); + Ok(0) + } + (MemoryVerdict::AsConfig(function, offset), _) => { + let segment = hardware.ecam.expect("the policy answered a configuration access from an ECAM window").segment; + config(hardware, acting, segment, function, offset, width, write.is_some()) + } + (MemoryVerdict::Refused(refused), _) => Err(refused), + } +} + +/// One port access, decided and made. +fn port(row: usize, _acting: &Holder, address: u64, width: Width, write: Option) -> Result { + let port = u16::try_from(address).map_err(|_| Refused::PortSpan)?; + let passed = firmware::port(|port| pio::standing(port, row), port, width, write.is_some())?; + match write { + None => Ok(read_port(&passed)), + Some(value) => { + write_port(&passed, value); + Ok(0) + } + } +} + +/// Make the access `request` names for the holder of `row`'s claim, or +/// refuse it by name: a read's value, the refusal and the memory type are +/// written back into it. `Err` is a request that names no space, width or +/// direction, a value wider than its width, or a reserved byte that is not +/// zero; and `Gone` once the claim is gone or the stop has begun. +pub fn access(row: usize, 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); + } + let acting = acting()?; + request.memory_type = toyos_abi::acpi::UNLISTED; + let made = match space { + Space::SystemMemory => memory(hardware, &acting, request, width, write), + Space::SystemIo => port(row, &acting, request.address, width, write), + Space::PciConfig => { + // `toyos_abi::acpi::pci_address`: nothing above the segment group. + 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, &acting, (at >> 32) as u16, function, at as u16, width, write.is_some()), + _ => 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(()) +} diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 23c12e2a735..1f32790d475 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -140,6 +140,8 @@ pub fn platform_devices(rsdp_addr: u64) { /// Every other CPU, running. pub fn start_other_cpus(platform: &Platform, args: &KernelArgs) { + // Before the first of them starts: each compares its own to these. + super::mtrr::init(); super::smp::boot_aps(&platform.madt, args.boot_pml4_addr); } diff --git a/kernel/src/arch/x86_64/cpu.rs b/kernel/src/arch/x86_64/cpu.rs index 5a287b846c6..bff62883f2f 100644 --- a/kernel/src/arch/x86_64/cpu.rs +++ b/kernel/src/arch/x86_64/cpu.rs @@ -383,6 +383,23 @@ pub fn inw(port: Port) -> u16 { value } +/// # Safety +/// `outb`'s contract, thirty-two bits wide. +#[inline] +pub unsafe fn outl(port: Port, value: u32) { + asm!("out dx, eax", in("dx") port.number(), in("eax") value); +} + +#[inline] +pub fn inl(port: Port) -> u32 { + let value: u32; + // SAFETY: as `inb` — one instruction into the declared output, no memory operand. + unsafe { + asm!("in eax, dx", out("eax") value, in("dx") port.number()); + } + value +} + /// One I/O bus cycle of delay, for a device that needs one between two commands. #[inline] pub fn io_wait() { diff --git a/kernel/src/arch/x86_64/i8042/mod.rs b/kernel/src/arch/x86_64/i8042/mod.rs index 9027151c7b6..1e2b247e1f2 100644 --- a/kernel/src/arch/x86_64/i8042/mod.rs +++ b/kernel/src/arch/x86_64/i8042/mod.rs @@ -970,7 +970,7 @@ pub fn init(rsdp_addr: u64) { // Declared before the first access, and for the rest of the boot: a // controller this probe has touched is no process's to claim. for (run, port) in [(&DATA, 0x60), (&STATUS, 0x64)] { - match pio::declare("the i8042", toyos_userbound::Ports::one(port)) { + match pio::declare("the i8042", toyos_userbound::Ports::one(port), toyos_userbound::Mediated::Kept) { Ok(declared) => run.set(declared), Err(why) => { log!("i8042: port {port:#x} not declared ({why:?}) — left unprobed"); diff --git a/kernel/src/arch/x86_64/mtrr.rs b/kernel/src/arch/x86_64/mtrr.rs index a7abe1011ea..8261b30035f 100644 --- a/kernel/src/arch/x86_64/mtrr.rs +++ b/kernel/src/arch/x86_64/mtrr.rs @@ -1,152 +1,110 @@ -//! What memory type firmware gave a physical range. +//! What memory type firmware gave a physical range: the range registers, +//! read here and decided by [`kernel::mtrr`]. //! //! Read-only: firmware owns these registers, the kernel programs none. A //! mapping with [`CachePolicy::Normal`](crate::mm::policy::CachePolicy) //! selects PAT entry 0 (WB), so what this module reports is the effective //! type; the exception is [`effective_under_wc`], where WC outvotes the MTRR //! instead of deferring to it. +//! +//! They are the registers of the CPU that reads them. Firmware is to leave +//! every processor's the same (Intel SDM Vol. 3A, "MTRR Considerations in MP +//! Systems"), and one that did not is said and never refused a boot: the +//! boot processor's are kept as it read them before any other CPU started +//! ([`init`]), and every other CPU reads its own as it comes up and says how +//! they stand beside those ([`compare`]). What a CPU whose registers are on +//! and not the boot processor's costs the machine is its caller's to say +//! ([`any_differs`]). + +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; + +pub use kernel::mtrr::{effective_under_wc, Effective}; +use kernel::mtrr::Beside; use crate::arch::cpu; +use crate::log; const IA32_MTRRCAP: u32 = 0xFE; const IA32_MTRR_DEF_TYPE: u32 = 0x2FF; const IA32_MTRR_PHYSBASE0: u32 = 0x200; -/// Bit 11 of `IA32_MTRR_DEF_TYPE`: clear means the whole address space is UC. -const DEF_TYPE_ENABLE: u64 = 1 << 11; -/// Bit 11 of an `IA32_MTRR_PHYSMASK`. -const PHYSMASK_VALID: u64 = 1 << 11; -/// Physical address bits of a PHYSBASE/PHYSMASK: 4 KiB-aligned, masked to the -/// 52-bit architectural ceiling, never narrower than a CPU's real width. -const PHYS_MASK: u64 = 0x000F_FFFF_FFFF_F000; -/// A memory type in the MTRRs' architectural encoding, matching the MSR values. -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum MemoryType { - Uncacheable, - WriteCombining, - WriteThrough, - WriteProtected, - WriteBack, -} +/// A CPU's default-type word and each variable register's +/// `(PHYSBASE, PHYSMASK)`. +type Registers = (u64, Vec<(u64, u64)>); -impl MemoryType { - fn from_encoding(raw: u8) -> Option { - match raw { - 0x00 => Some(Self::Uncacheable), - 0x01 => Some(Self::WriteCombining), - 0x04 => Some(Self::WriteThrough), - 0x05 => Some(Self::WriteProtected), - 0x06 => Some(Self::WriteBack), - _ => None, - } - } +/// The boot processor's. Written once, by [`init`]. +static BOOT: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); - pub fn name(self) -> &'static str { - match self { - Self::Uncacheable => "UC", - Self::WriteCombining => "WC", - Self::WriteThrough => "WT", - Self::WriteProtected => "WP", - Self::WriteBack => "WB", - } - } -} +/// Some CPU's registers are on and not the boot processor's. +static DIFFERS: AtomicBool = AtomicBool::new(false); -/// Why a range has no single answer, reported rather than resolved: picking one -/// type here would be inventing an answer firmware never gave. -pub enum Unknown { - /// A variable MTRR holds an encoding the architecture does not define. - ReservedEncoding, - /// Overlapping MTRRs whose types the architecture leaves undefined. - Conflicting, - /// Part of the range is covered and part is not. - PartiallyCovered, +/// This CPU's default-type word and each variable register's +/// `(PHYSBASE, PHYSMASK)`. +fn registers() -> Registers { + let pairs = (0..(cpu::rdmsr(IA32_MTRRCAP) & 0xFF) as u32) + .map(|i| (cpu::rdmsr(IA32_MTRR_PHYSBASE0 + i * 2), cpu::rdmsr(IA32_MTRR_PHYSBASE0 + i * 2 + 1))) + .collect(); + (cpu::rdmsr(IA32_MTRR_DEF_TYPE), pairs) } -pub enum Effective { - Known(MemoryType), - Unknown(Unknown), - /// MTRRs are off, so the whole address space is UC by architecture. - MtrrsDisabled, +/// Keep the boot processor's registers as firmware handed them over. On the +/// boot processor, before any other CPU starts. +pub fn init() { + let boot = registers(); + log!("mtrr: the boot processor's range registers: IA32_MTRR_DEF_TYPE {:#x} and {} variable pairs", boot.0, boot.1.len()); + let was = BOOT.swap(Box::into_raw(Box::new(boot)), Ordering::Release); + assert!(was.is_null(), "mtrr: init ran twice"); } -impl Effective { - pub fn name(&self) -> &'static str { - match self { - Self::Known(t) => t.name(), - Self::MtrrsDisabled => "UC (MTRRs disabled)", - Self::Unknown(Unknown::ReservedEncoding) => "unknown (reserved MTRR encoding)", - Self::Unknown(Unknown::Conflicting) => "unknown (overlapping MTRRs disagree)", - Self::Unknown(Unknown::PartiallyCovered) => "unknown (range only partly covered)", - } - } +/// The boot processor's registers, as [`init`] read them. +pub fn boot() -> (u64, &'static [(u64, u64)]) { + let at = BOOT.load(Ordering::Acquire); + assert!(!at.is_null(), "mtrr: the boot processor's range registers were asked for before they were read"); + // SAFETY: `init` stored a leaked `Box` once, and nothing frees or writes it. + let (def_type, pairs) = unsafe { &*at }; + (*def_type, pairs) } -/// Effective type of a WC-PAT page over range `mtrr`: WC wins even over an -/// MTRR's UC (SDM Vol. 3A Table 11-7); `None` only when `mtrr` has no single -/// answer. -pub fn effective_under_wc(mtrr: &Effective) -> Option { - match mtrr { - Effective::Known(_) | Effective::MtrrsDisabled => Some(MemoryType::WriteCombining), - Effective::Unknown(_) => None, +/// Read this CPU's registers and say how they stand beside the boot +/// processor's. On every other CPU, as it comes up. +pub fn compare(cpu_id: u32) { + let (boot, (def_type, pairs)) = (boot(), registers()); + match kernel::mtrr::beside(boot, (def_type, &pairs)) { + Beside::Same => log!("mtrr: cpu{cpu_id}'s range registers are the boot processor's"), + Beside::Off => log!( + "mtrr: cpu{cpu_id}'s range registers are off (IA32_MTRR_DEF_TYPE {def_type:#x}) and not the boot processor's: \ + every read this CPU makes is uncached" + ), + Beside::Different => { + DIFFERS.store(true, Ordering::Release); + let pair = pairs.iter().zip(boot.1).position(|(own, boots)| own != boots); + log!( + "mtrr: cpu{cpu_id}'s range registers are on and not the boot processor's: IA32_MTRR_DEF_TYPE {def_type:#x} \ + beside {:#x}, {} variable pairs beside {}, the first that is not the same {}", + boot.0, + pairs.len(), + boot.1.len(), + match pair { + Some(n) => alloc::format!("pair {n}, {:#x}/{:#x} beside {:#x}/{:#x}", pairs[n].0, pairs[n].1, boot.1[n].0, boot.1[n].1), + None => "none".into(), + }, + ); + } } } -/// Two MTRRs over one address: UC beats anything, WT beats WB, else undefined. -fn combine(a: MemoryType, b: MemoryType) -> Option { - use MemoryType::{Uncacheable, WriteBack, WriteThrough}; - match (a, b) { - (x, y) if x == y => Some(x), - (Uncacheable, _) | (_, Uncacheable) => Some(Uncacheable), - (WriteThrough, WriteBack) | (WriteBack, WriteThrough) => Some(WriteThrough), - _ => None, - } +/// Whether any CPU that came up holds registers that are on and not the boot +/// processor's: on such a machine what the boot processor's say of a range +/// is not known of a read another CPU makes there. +pub fn any_differs() -> bool { + DIFFERS.load(Ordering::Acquire) } -/// The memory type firmware gave `[base, base + size)`; fixed MTRRs (first -/// 1 MiB) are not consulted. +/// The memory type firmware gave `[base, base + size)` on this CPU; fixed +/// MTRRs (first 1 MiB) are not consulted. pub fn range_type(base: u64, size: u64) -> Effective { - let def_type = cpu::rdmsr(IA32_MTRR_DEF_TYPE); - if def_type & DEF_TYPE_ENABLE == 0 { - return Effective::MtrrsDisabled; - } - let default = match MemoryType::from_encoding(def_type as u8) { - Some(t) => t, - None => return Effective::Unknown(Unknown::ReservedEncoding), - }; - - let end = base + size; - let mut covering: Option = None; - for i in 0..(cpu::rdmsr(IA32_MTRRCAP) & 0xFF) as u32 { - let mask = cpu::rdmsr(IA32_MTRR_PHYSBASE0 + i * 2 + 1); - if mask & PHYSMASK_VALID == 0 { - continue; - } - // A PHYSMASK's contiguous high bits size the region: PHYSBASE plus - // 1 << its lowest set bit. - let phys_mask = mask & PHYS_MASK; - let base_msr = cpu::rdmsr(IA32_MTRR_PHYSBASE0 + i * 2); - let region_start = base_msr & phys_mask; - let region_end = region_start + (1u64 << phys_mask.trailing_zeros()); - if region_end <= base || region_start >= end { - continue; - } - if region_start > base || region_end < end { - // Not uniform under this MTRR; picking a winner would be - // inventing an answer. - return Effective::Unknown(Unknown::PartiallyCovered); - } - let t = match MemoryType::from_encoding(base_msr as u8) { - Some(t) => t, - None => return Effective::Unknown(Unknown::ReservedEncoding), - }; - covering = Some(match covering { - None => t, - Some(prev) => match combine(prev, t) { - Some(merged) => merged, - None => return Effective::Unknown(Unknown::Conflicting), - }, - }); - } - Effective::Known(covering.unwrap_or(default)) + let (def_type, pairs) = registers(); + kernel::mtrr::range_type(def_type, pairs, base, base + (size - 1)) } diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs index a91ddcd1557..38525436e6e 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -862,11 +862,31 @@ pub fn load_kernel_flush() { unsafe { kernel_root().load_flush() }; } +/// Every window [`map_mmio`] mapped, as `(start, end)`: what this kernel +/// drives a device through, and so what it refuses the `acpi` claim's holder +/// (`arch::acpi_mode`), who is refused every page one lies in. Its only writer +/// is the only maker of such a window. One entry a distinct window and none +/// removed: a boot's drivers map theirs once, and past boot only `pcidev` +/// maps, a BAR where firmware put it and at each address it tries it at, and +/// one it has placed it does not try again. +static DRIVEN: Lock> = Lock::new(Vec::new()); + +/// Run `f` over every window this kernel mapped to drive a device. +pub fn with_driven_windows(f: impl FnOnce(&[(u64, u64)]) -> T) -> T { + f(&DRIVEN.lock()) +} + /// Free function (not a method): the lock and the shootdown are separate /// statements. Not optional — `map_2m` may change memory type under a /// sibling's stale entry, which is SDM Vol. 3A §11.12.4 undefined behaviour. pub fn map_mmio(phys: u64, size: u64, policy: MmioPolicy) -> crate::mm::Mmio { let mmio = kernel().lock().map_mmio(phys, size, policy.cache()); + { + let mut driven = DRIVEN.lock(); + if !driven.contains(&(phys, phys + size)) { + driven.push((phys, phys + size)); + } + } crate::arch::tlb::shootdown(crate::invalidation::Origin::Mmio); // Read back off the table and logged beside firmware's MTRR verdict: the // boot's own evidence that no register window trusts firmware. diff --git a/kernel/src/arch/x86_64/pio.rs b/kernel/src/arch/x86_64/pio.rs index cca1019b2fe..05b014183fc 100644 --- a/kernel/src/arch/x86_64/pio.rs +++ b/kernel/src/arch/x86_64/pio.rs @@ -8,12 +8,18 @@ //! [`declare`] what a probe or a firmware table names at boot, each refused //! where it shares a port with a run declared before it. `crate::isa` asks //! [`holder`] before it opens a row. +//! +//! **Every declaration says what its ports answer the `acpi` claim's holder** +//! ([`Mediated`]), whose AML may name any port: [`standing`] is what +//! `toyos_userbound::firmware::port` decides an access by, and [`mediated`] +//! the one other maker of a [`Port`], from that decision's witness. use alloc::format; use alloc::string::String; use core::sync::atomic::{AtomicU32, Ordering}; -use toyos_userbound::{PortAccess, Ports, Reserved, Undeclared}; +use toyos_userbound::firmware::{PortAt, Standing}; +use toyos_userbound::{Mediated, PortAccess, Ports, Reserved, Undeclared}; use super::ioapic::{self, Gsi, IsaLine, Trigger}; use crate::log; @@ -67,14 +73,17 @@ pub const CMOS: Declared = Declared::fixed(0x70, 2); /// ECAM: it is the kernel's all the same, and a grant of it would be a way /// round every claim. `CONFIG_ADDRESS` is a dword at 0xCF8, so its first port /// alone keeps it refused, and 0xCF9 free for a reset register. -const FIXED: &[(&str, Declared)] = &[ - ("COM1", COM1), - ("the 8259 pair", PIC_PRIMARY), - ("the 8259 pair", PIC_SECONDARY), - ("the POST port", POST), - ("the CMOS RTC", CMOS), - ("the PCI configuration mechanism", Declared::fixed(0xCF8, 1)), - ("the PCI configuration mechanism", Declared::fixed(0xCFC, 4)), +const FIXED: &[(&str, Declared, Mediated)] = &[ + ("COM1", COM1, Mediated::Kept), + ("the 8259 pair", PIC_PRIMARY, Mediated::Kept), + ("the 8259 pair", PIC_SECONDARY, Mediated::Kept), + // A write nothing decodes costs the kernel nothing whoever makes it. + ("the POST port", POST, Mediated::Open), + // Index and data: a holder's access between the kernel's two would move + // the index under it. + ("the CMOS RTC", CMOS, Mediated::Kept), + ("the PCI configuration mechanism", Declared::fixed(0xCF8, 1), Mediated::Kept), + ("the PCI configuration mechanism", Declared::fixed(0xCFC, 4), Mediated::Kept), ]; const _: () = { @@ -95,18 +104,37 @@ static RUNTIME: Lock> = Lock::new(Reserved::new()); /// Declare `ports` to `holder`, refused where another holder has one of them. /// Boot's alone: a run declared once a process could hold a grant would be a /// port the grant was not checked against. -pub fn declare(holder: &'static str, ports: Ports) -> Result { +pub fn declare(holder: &'static str, ports: Ports, mediated: Mediated) -> Result { assert!(!crate::smp::is_ready(), "pio: {holder} declared ports after userland could hold a grant"); - if let Some(&(first, _)) = FIXED.iter().find(|(_, fixed)| fixed.0.overlaps(ports)) { + if let Some(&(first, ..)) = FIXED.iter().find(|(_, fixed, _)| fixed.0.overlaps(ports)) { return Err(Undeclared::Clash(first)); } - RUNTIME.lock().declare(holder, ports)?; + RUNTIME.lock().declare(holder, ports, mediated)?; Ok(Declared(ports)) } /// Who holds a port of `ports`, if this kernel declared one. pub fn holder(ports: Ports) -> Option<&'static str> { - FIXED.iter().find(|(_, fixed)| fixed.0.overlaps(ports)).map(|&(name, _)| name).or_else(|| RUNTIME.lock().holder(ports)) + FIXED.iter().find(|(_, fixed, _)| fixed.0.overlaps(ports)).map(|&(name, ..)| name).or_else(|| RUNTIME.lock().holder(ports)) +} + +/// What `port` is to the holder of `row`'s claim: declared, another row's, or +/// free. Its own row's ports are free: it holds them already. +pub fn standing(port: u16, row: usize) -> Standing { + let one = Ports::one(port); + if let Some(&(.., mediated)) = FIXED.iter().find(|(_, fixed, _)| fixed.0.overlaps(one)) { + return Standing::Declared(mediated); + } + if let Some(mediated) = RUNTIME.lock().mediated(port) { + return Standing::Declared(mediated); + } + let others = (0..crate::isa::MAX_ROWS).filter(|&other| other != row).filter_map(crate::isa::runs); + if others.flatten().any(|run| run.overlaps(one)) { Standing::Row } else { Standing::Free } +} + +/// The port an access the mediation policy passed begins at. +pub fn mediated(passed: &PortAt) -> Port { + Port(passed.port()) } /// Every row's ports, the kernel's again: the power-off's, and nobody else's. diff --git a/kernel/src/arch/x86_64/power.rs b/kernel/src/arch/x86_64/power.rs index d1d86746d9d..0eed43f9429 100644 --- a/kernel/src/arch/x86_64/power.rs +++ b/kernel/src/arch/x86_64/power.rs @@ -10,7 +10,7 @@ use core::sync::atomic::{AtomicU8, Ordering}; use toyos_acpi::{Reset, Table, TableError, S5, SDT_HEADER_LEN, SDT_REVISION}; -use toyos_userbound::Ports; +use toyos_userbound::{Mediated, Ports}; use super::cpu; use super::pio::{self, Declared, Slot}; @@ -48,7 +48,7 @@ pub fn init_reset(rsdp_addr: u64) { } }; match toyos_acpi::reset_register(&fadt) { - Reset::Port { port, value } => match pio::declare("the reset register", Ports::one(port)) { + Reset::Port { port, value } => match pio::declare("the reset register", Ports::one(port), Mediated::Kept) { Ok(declared) => { RESET_VALUE.store(value, Ordering::Relaxed); RESET.set(declared); @@ -79,7 +79,7 @@ pub fn init_off(rsdp_addr: u64) { }; let pm1a = block.port; let run = Ports::new(pm1a, block.len).expect("pm1a_control bounded the block by the port space"); - match pio::declare("the PM1a control block", run) { + match pio::declare("the PM1a control block", run, Mediated::ReadOnly) { Ok(declared) => PM1A_CNT.set(declared), Err(why) => { log!("ACPI: PM1a control block {pm1a:#x} not declared ({why:?}) — no soft-off"); @@ -171,6 +171,7 @@ pub fn off(stopping: crate::quiesce::Stopping) -> ! { let control = control.port(0); let taken = pio::take_back(&stopping); super::smi_cmd::settle(&taken); + super::acpi_mode::settle(&taken); let held = cpu::inw(control); if held & SCI_EN != 0 { super::acpi_mode::quiet(&taken); diff --git a/kernel/src/arch/x86_64/smi_cmd.rs b/kernel/src/arch/x86_64/smi_cmd.rs index 564cb12c989..1143372f9ce 100644 --- a/kernel/src/arch/x86_64/smi_cmd.rs +++ b/kernel/src/arch/x86_64/smi_cmd.rs @@ -29,7 +29,7 @@ use core::fmt; use alloc::string::String; use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering::Relaxed}; -use toyos_userbound::{Ports, Undeclared}; +use toyos_userbound::{Mediated, Ports, Undeclared}; use super::pio::{self, Slot, TakenBack}; use super::{apic, cpu, percpu, IrqGuard}; @@ -81,9 +81,11 @@ impl fmt::Display for Written { } } -/// Declare the port the FADT names. Boot's. +/// Declare the port the FADT names. Boot's. The `acpi` claim's holder reads it +/// and never writes it: a write is a command to the firmware, and [`write`] +/// makes every one. pub fn declare(port: u16) -> Result<(), Undeclared> { - PORT.set(pio::declare("SMI_CMD", Ports::one(port))?); + PORT.set(pio::declare("SMI_CMD", Ports::one(port), Mediated::ReadOnly)?); Ok(()) } diff --git a/kernel/src/arch/x86_64/smp.rs b/kernel/src/arch/x86_64/smp.rs index 3313af5966d..9e2712141df 100644 --- a/kernel/src/arch/x86_64/smp.rs +++ b/kernel/src/arch/x86_64/smp.rs @@ -292,6 +292,8 @@ extern "C" fn ap_entry() -> ! { syscall::init(); // Calibration is a one-time BSP measurement; nothing left for an AP to do here. apic::init_ap(); + // Before the echo, so every CPU the roster counts has said its line. + crate::arch::mtrr::compare(percpu::cpu_id()); // Echo this attempt's token, so the BSP counts this AP for its own attempt. ROSTER.echo(percpu::ap_token()); diff --git a/kernel/src/arch/x86_64/watchdog.rs b/kernel/src/arch/x86_64/watchdog.rs index 6f19a6b67b7..82839593dc5 100644 --- a/kernel/src/arch/x86_64/watchdog.rs +++ b/kernel/src/arch/x86_64/watchdog.rs @@ -67,7 +67,7 @@ pub fn init(devices: &[PciDevice]) { }; // ICH9 and every PCH since: a 32-byte block. - let block = match toyos_userbound::Ports::new(port, 0x20).map(|run| pio::declare("the TCO watchdog", run)) { + let block = match toyos_userbound::Ports::new(port, 0x20).map(|run| pio::declare("the TCO watchdog", run, toyos_userbound::Mediated::ReadOnly)) { Some(Ok(block)) => block, refused => { log!("watchdog: the TCO block at {port:#x} is not this kernel's to drive ({:?}) — not armed", refused.map(|r| r.err())); diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index caddc3743d2..665088e21bf 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -536,12 +536,22 @@ impl<'a> Iterator for CapabilityIter<'a> { } } +/// The window [`enumerate`] walked. +static ECAM: crate::sync::Lock> = crate::sync::Lock::new(None); + +/// The configuration space of the function at this address, whether or not +/// one answers there: an absent function reads all ones. +pub fn function_window(bus: u8, dev: u8, func: u8) -> Option { + (*ECAM.lock()).map(|ecam| PciDevice::new(&ecam, bus, dev, func).mmio) +} + /// The most functions [`enumerate`] will hand back; the rest are logged, not enumerated. const MAX_DEVICES: usize = 256; /// Every PCIe function ECAM decodes, in bus/device/function order; drivers must select all matches, not the first. pub fn enumerate(ecam: &crate::mm::Mmio) -> Vec { log!("PCI: Enumerating devices..."); + *ECAM.lock() = Some(*ecam); let mut found: Vec = Vec::new(); 'scan: for bus in 0..=255u16 { diff --git a/kernel/src/main.rs b/kernel/src/main.rs index d9efe43ded9..5edbe022899 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -372,6 +372,9 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { // ROOT's image, `LoaderData` like the black box's page. Empty on a // boot the loader handed none. root_image, + // Firmware's memory map as the loader copied it, which `mm` keeps + // (`mm::firmware_map`): `LoaderData` too. + mm::Region { start: kernel_args.memory_map_addr, end: kernel_args.memory_map_addr + kernel_args.memory_map_size }, ]; // A region the loader did not allocate withholds memory nothing uses, so one the firmware map does not hold as `LoaderData` is refused. // Block 1: the ELF region (`kernel_elf_addr`+`kernel_elf_size`) is not page-aligned. @@ -394,8 +397,8 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { // name, rather than indexing it, means a region added to `loader` fails // to compile here instead of compiling and being silently dropped from // what `mm::init` withholds. - let [image, elf, black_box, root] = loader; - let reserved = [image, elf, black_box, root, arch::boot::reserved()]; + let [image, elf, black_box, root, map] = loader; + let reserved = [image, elf, black_box, root, map, arch::boot::reserved()]; // The last point before the first hash container (`mm::init`'s address // space), and not earlier: seeding fails only by panicking, and a panic diff --git a/kernel/src/mm/mod.rs b/kernel/src/mm/mod.rs index 609b9fd3ae5..162d4e54868 100644 --- a/kernel/src/mm/mod.rs +++ b/kernel/src/mm/mod.rs @@ -153,10 +153,29 @@ impl core::fmt::Debug for DirectMap { } } +/// Firmware's memory map, set once by [`init`]: where the loader left it, +/// which `reserved` withholds from the allocator. +static FIRMWARE_MAP: (core::sync::atomic::AtomicPtr, core::sync::atomic::AtomicUsize) = + (core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()), core::sync::atomic::AtomicUsize::new(0)); + +/// Firmware's memory map as the loader handed it over; empty before [`init`]. +pub fn firmware_map() -> &'static [MemoryMapEntry] { + use core::sync::atomic::Ordering::Acquire; + let at = FIRMWARE_MAP.0.load(Acquire); + if at.is_null() { + return &[]; + } + // SAFETY: `init` stored a `'static` slice's pointer before its length, and + // nothing writes or frees that memory. + unsafe { core::slice::from_raw_parts(at, FIRMWARE_MAP.1.load(Acquire)) } +} + /// Call once at boot, in order: pmm (physical pages) → paging (direct map) → /// alloc (heap) → every kernel root slot, before the first user space copies -/// them. -pub fn init(memory_map: &[MemoryMapEntry], reserved: &[Region]) { +/// them. `reserved` holds `memory_map`'s own memory. +pub fn init(memory_map: &'static [MemoryMapEntry], reserved: &[Region]) { + FIRMWARE_MAP.1.store(memory_map.len(), core::sync::atomic::Ordering::Release); + FIRMWARE_MAP.0.store(memory_map.as_ptr().cast_mut(), core::sync::atomic::Ordering::Release); alloc::init_early(); pmm::init(memory_map, reserved); DIRECT_MAP_END.set(paging::init(memory_map, crate::drivers::panic_console::scanout())); diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 1590f7a8533..c0ab7cba7ad 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -403,6 +403,18 @@ pub fn inventory() -> Vec { out } +/// Every range a memory BAR of an enumerated function decoded as firmware +/// left it, as `(start, end)`. What the `acpi` claim's holder is refused +/// (`toyos_userbound::firmware`), whoever drives the function. A window this +/// module cut for a BAR since is not among them: [`publish`] cuts only at +/// addresses firmware's map does not list, which the mediation refuses by +/// that. `f` runs under [`MACHINE`]; the mediation takes `paging`'s record of +/// windows inside it, and nothing holding that record's lock takes this one. +pub fn with_bar_memory(f: impl FnOnce(&mut dyn Iterator) -> T) -> T { + let machine = MACHINE.lock(); + f(&mut machine.decoded.iter().map(|&(_, start, end)| (start, end))) +} + /// The PCI segment group every enumerated function is on. pub fn segment() -> u16 { MACHINE.lock().segment diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index da73a9691cd..231a0b1c2fd 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -114,6 +114,38 @@ pub(super) fn sys_device_reg(handle: RawHandle, offset: u64, width: u64, value: } } +/// One operation on the `acpi` claim (`toyos_abi::acpi::op`), for the process +/// the claim's first read bound: a handle moved on to another answers +/// `PermissionDenied`, as its ports answer nothing there. +pub(super) fn sys_acpi(ctx: &SyscallContext, handle: RawHandle, op: u64, at: u64) -> u64 { + use toyos_abi::acpi::op as ops; + let held = process::with_process_data(|data| { + data.handles + .get::(handle, Rights::WRITE) + .map(|claim| (claim.class(), claim.isa_row())) + }); + let row = match held { + Ok((device::DeviceType::Acpi, Some(row))) => row, + Ok((class, _)) => { + return crate::object::HandleError::WrongType { held: class.class_name(), wanted: "an acpi claim" }.refuse() + } + Err(e) => return e.refuse(), + }; + if !crate::isa::bound_to(row, process::current_process()) { + return SyscallError::PermissionDenied.to_u64(); + } + let done = match op { + ops::ACCESS => ctx.copy_in::(UserAddr::new(at)).and_then(|mut request| { + crate::arch::acpi_mode::access(row, &mut request)?; + ctx.copy_out(UserAddr::new(at), &request).map(|()| 0) + }), + ops::LOCK_TAKE => crate::arch::acpi_mode::lock_take().map(|taken| if taken { ops::TAKEN } else { ops::PENDING }), + ops::LOCK_RELEASE => crate::arch::acpi_mode::lock_release().map(|()| 0), + _ => Err(SyscallError::InvalidArgument), + }; + done.unwrap_or_else(|e| e.to_u64()) +} + /// Mints a device claim, gated on a `SysCap` carrying [`Rights::DEVICE`]. /// /// `selector` says which device where the class alone does not — a PCI diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index c3dacf6690c..5b84a922472 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -23,7 +23,7 @@ use super::HANDLE_LEN; #[cfg(feature = "test-actuators")] use super::debug::{canary, debug_heap_alloc, ring0_timer_in_syscall, FATAL_HALT_NONCE, LOCK_ACROSS_SWITCH}; use super::device::{ - holds_claim, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, + holds_claim, sys_acpi, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, sys_device_dma_unmap, sys_device_reg, sys_gpu_reset_scanout, sys_partition_transfer, Transfer, }; @@ -641,6 +641,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> }, // An interrupt inside a syscall's body, which nothing a guest does puts there on demand. DA::RING0_TIMER_IN_SYSCALL => ring0_timer_in_syscall(), + DA::ACPI_FIRMWARE_LOCK => crate::arch::acpi_mode::debug_firmware_lock(a2), _ => SyscallError::InvalidArgument.to_u64(), }, SYS_SCHED_INFO => match ctx.copy_out(UserAddr::new(a1), &sys_sched_info()) { @@ -669,6 +670,9 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> process::set_current_thread_name(&name[..len]); 0 }, + // Gated by the claim handle alone, as a PCI function's calls are: the + // kernel decides each access by what its address is. + SYS_ACPI => sys_acpi(&ctx, RawHandle(a1 as u32), a2, a3), SYS_DEVICE_REG_READ => sys_device_reg(RawHandle(a1 as u32), a2, a3, None), SYS_DEVICE_REG_WRITE => sys_device_reg(RawHandle(a1 as u32), a2, a3, Some(a4)), _ => SyscallError::InvalidArgument.to_u64(), diff --git a/tests/acpicase/system.toml b/tests/acpicase/system.toml index 2d6213adf51..c9d4da54c6b 100644 --- a/tests/acpicase/system.toml +++ b/tests/acpicase/system.toml @@ -14,10 +14,13 @@ syscap = ["logread"] # `dup` because test-runner hands a job its capability as a duplicate and # hands none without it; `power` because the job list ends the machine, # asking the supervisor. +# +# `args` as it stands is the guest's: `acpi_mediated_access` boots this file +# with its probe on ROOT. A metal row's job list replaces it. [programs.test-runner] receives = ["power"] syscap = ["device", "dup"] -args = ["reboot"] +args = ["test_rs_acpi_mediated", "reboot"] [programs.toybox] diff --git a/tests/common/isa.rs b/tests/common/isa.rs index b433593b95e..423161ca4b9 100644 --- a/tests/common/isa.rs +++ b/tests/common/isa.rs @@ -6,7 +6,7 @@ use super::serial::Serial; /// The kernel's line under `i8042-withheld`: the premise of every row that /// needs the claim granted. -const WITHHELD: &str = "i8042: withheld, left unprobed for a claim"; +pub const WITHHELD: &str = "i8042: withheld, left unprobed for a claim"; /// How often `needle` is in `log`, which must be `want`. fn said(log: &Serial, needle: &str, want: usize) -> Result<(), String> { diff --git a/tests/common/power.rs b/tests/common/power.rs index f555a857d8b..b9fbc7f2f6f 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -544,12 +544,55 @@ const ACPI_ARMED: &str = "acpiserver: armed: power button served, embedded contr const ACPI_PRESSED: &str = "acpiserver: the power button was pressed, on SCI 1 of this boot; asking the supervisor to power off"; +/// What `/system/bin/acpiserver` says of each definition block, after +/// `acpiserver: `: its place, how many there are, which it is, and what +/// became of it. +const ACPI_TABLE: &str = "acpiserver: table "; + +/// What it says of `\_S5`, ahead of the two values. +const ACPI_S5: &str = "acpiserver: \\_S5 evaluated: "; + +/// The number after `SLP_TYPa=` on a line. +fn slp_typ_a(line: &str) -> Result { + line.split_once("SLP_TYPa=") + .and_then(|(_, after)| after.split(|c: char| !c.is_ascii_digit()).next()) + .and_then(|digits| digits.parse().ok()) + .ok_or_else(|| format!("no SLP_TYPa on {line:?}")) +} + +/// The server loaded every definition block it found, the DSDT first, each +/// said on a line in its place; and the `SLP_TYPa` its `\_S5` evaluates to +/// is the one the kernel's byte scan of the DSDT decoded (`kernel`'s +/// [`SOFT_OFF_DECODED`] line): the interpreter against the scan, over bytes +/// one read through the mediated access and the other through the direct +/// map. Answers how many blocks there were. +pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial) -> Result { + let said: Vec<&str> = log.text().lines().filter_map(|line| line.split_once(ACPI_TABLE).map(|(_, said)| said.trim())).collect(); + let Some(count) = said.first().and_then(|first| first.split_once(" of ")?.1.split_once(' ')?.0.parse::().ok()) else { + return Err(format!("the server said nothing of a first table ({said:?}):\n{}", log.text())); + }; + let expected: Vec = + (1..=count).map(|place| format!("{place} of {count} ({}) loaded", if place == 1 { "DSDT" } else { "SSDT" })).collect(); + if said != expected { + return Err(format!("the server's tables are {said:#?}, where {count} loaded ones are {expected:#?}")); + } + let (evaluated, decoded) = (log.must_say(ACPI_S5)?, kernel.must_say(SOFT_OFF_DECODED)?); + if slp_typ_a(evaluated)? != slp_typ_a(decoded)? { + return Err(format!("the server's \\_S5 is not the kernel's: {:?} beside {:?}", evaluated.trim(), decoded.trim())); + } + eprintln!(" [power] {count} table(s) loaded; {} beside {}", evaluated.trim(), decoded.trim()); + Ok(count) +} + /// A press of q35's power button stops the machine through ToyOS's own path: /// the kernel put the machine in ACPI mode for the server's claim, the press /// arrives as the server's first SCI and the only one, since the kernel masks /// the level line until the server has served it, and the server has the /// supervisor stop the machine, which QEMU reports as the guest's own -/// power-off. +/// power-off. The press is sent as soon as the server has armed, which is +/// before it loads the machine's tables: the press is latched across the +/// load, whose lines and `\_S5` are read here, on the one firmware's tables a +/// guest has. pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { let options = BootOptions { qmp: true, ..Default::default() }; let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); @@ -572,6 +615,9 @@ pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { return Err(format!("the supervisor's last word after the press is not a power-off:\n{}", after.text())); } after.must_be_clean()?; + let whole = serial::Serial::named("the boot and the press", console); + acpi_tables_loaded(&whole, &whole)?; + whole.must_say_after(ACPI_S5, ACPI_PRESSED)?; eprintln!(" [power] the press: {ACPI_PRESSED}"); Ok(()) } diff --git a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs new file mode 100644 index 00000000000..f520ba72839 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs @@ -0,0 +1,391 @@ +//! The `acpi` claim's mediated access, asked as its holder asks: what the +//! kernel reads and writes for it, what it refuses and by which name, and the +//! firmware's Global Lock taken, found owned and given back. +//! +//! Run on a boot that starts no ACPI server (`tests/acpicase`) and whose +//! kernel leaves the i8042 to a claim (`i8042-withheld`), so its row is +//! another claim's; on a guest: every write here that must be refused is one +//! a kernel that made it would make for real — to RAM, to the tables, to +//! COM1, to `PM1a_CNT`, to a function's configuration space — and the +//! firmware's side of the lock is staged on the FACS itself, which only a +//! machine whose firmware is not using it can take. Each address is found as +//! a holder finds it, from the RSDP the claim's description names. +//! +//! First a child is handed the claim, takes the lock and exits with it: the +//! claim binds to one process for that process's life, so the parent claims +//! only after, and reads the lock word free. +//! +//! Last it takes the lock once more and asks for the power-off holding it: a +//! stop ends no process, so the kernel finds a live holder's lock taken and +//! gives it back before the power-off owns the hardware, which +//! `acpi_mediated_access` reads in the kernel's own line. + +use std::os::toyos::process::CommandExt; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; + +use toyos::endow::{Endowments, SYSCAP_LABEL}; +use toyos::syscap::SysCap; +use toyos::{AsHandle, Device}; +use toyos_abi::acpi::{pci_address, Access, AcpiInfo, Refused, Space, Width, UNLISTED}; +use toyos_abi::syscall::{self, debug_action, DeviceType, SyscallError}; +use toyos_abi::RawHandle; + +const SELF_PATH: &str = "/system/bin/test_rs_acpi_mediated"; +const CLAIM_LABEL: &str = "acpi-claim"; + +/// `EFI_MEMORY_TYPE`s: what the kernel hands out as RAM, and ACPI's two. +const USABLE: [u8; 5] = [1, 2, 3, 4, 7]; +const ACPI_RECLAIM: u8 = 9; +const ACPI_NVS: u8 = 10; + +/// The liveness ceiling on a dead holder's claim coming back, as `isa_row`'s. +const RELEASED: Duration = Duration::from_secs(5); + +struct Holder(Device); + +impl Holder { + fn handle(&self) -> RawHandle { + self.0.as_handle() + } + + fn ask(&self, mut access: Access) -> (Result, u8) { + let made = syscall::acpi_access(self.handle(), &mut access).expect("acpi: a well-formed access on a bound claim"); + (made, access.memory_type) + } + + fn read(&self, space: Space, address: u64, width: Width) -> Result { + self.ask(Access::read(space, address, width)).0 + } + + fn write(&self, space: Space, address: u64, width: Width, value: u64) -> Result<(), Refused> { + self.ask(Access::write(space, address, width, value)).0.map(drop) + } + + fn memory(&self, address: u64, width: Width) -> u64 { + self.read(Space::SystemMemory, address, width).unwrap_or_else(|why| panic!("acpi: a read of the firmware's own memory was refused: {why:?}")) + } + + /// The first table the XSDT lists under `signature`. + fn table(&self, rsdp: u64, signature: &[u8; 4]) -> u64 { + // ACPI 6.5 Table 5.3: `XsdtAddress` at 24; Table 5.4: a table's length at 4, and the XSDT's entries from 36. + let xsdt = self.memory(rsdp + 24, Width::QWord); + let entries = (self.memory(xsdt + 4, Width::DWord) - 36) / 8; + (0..entries) + .map(|i| self.memory(xsdt + 36 + i * 8, Width::QWord)) + .find(|&table| self.memory(table, Width::DWord) == u64::from(u32::from_le_bytes(*signature))) + .unwrap_or_else(|| panic!("acpi: the XSDT lists no {:?}", String::from_utf8_lossy(signature))) + } +} + +fn main() { + match std::env::args().nth(1).as_deref() { + None => probe(), + Some("keeper") => keeper(), + other => panic!("acpi: unknown role {other:?}"), + } +} + +fn claim(cap: &SysCap) -> Device { + let by = Instant::now() + RELEASED; + loop { + match cap.claim(DeviceType::Acpi) { + Ok(claim) => return claim, + Err(SyscallError::AlreadyExists) => {} + Err(other) => panic!("acpi: the fixed hardware's claim answered {other:?}"), + } + assert!(Instant::now() < by, "acpi: the claim never came back in {RELEASED:?}"); + std::thread::yield_now(); + } +} + +/// Read the claim's description, which binds it to this process. +fn bind(claim: &Device) -> AcpiInfo { + let mut bytes = [0u8; size_of::()]; + let n = claim.read(&mut bytes).expect("acpi: the claim's first read is its description"); + assert_eq!(n, bytes.len(), "acpi: a description of {n} bytes"); + // SAFETY: `AcpiInfo` is `repr(C)` integers with no padding, so every bit pattern of its size is one. + unsafe { core::ptr::read_unaligned(bytes.as_ptr().cast()) } +} + +fn probe() { + let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("test-runner endows a device-minting capability"); + + // A holder that dies with the lock: the kernel gives it back with the claim. + let kept = Command::new(SELF_PATH) + .arg("keeper") + .endow(CLAIM_LABEL, claim(&cap).into_raw().0) + .stdout(Stdio::piped()) + .output() + .expect("acpi: spawn the keeper"); + let said = String::from_utf8_lossy(&kept.stdout); + assert!(kept.status.success() && said.contains(KEPT), "acpi: the keeper ended {:?} having said {said:?}", kept.status); + + let holder = Holder(claim(&cap)); + + // Unbound, the claim answers nothing: its first read is what makes this + // process its holder. + let mut post = Access::write(Space::SystemIo, 0x80, Width::Byte, 0); + assert_eq!(syscall::acpi_access(holder.handle(), &mut post), Err(SyscallError::PermissionDenied)); + assert_eq!(syscall::acpi_lock_take(holder.handle()), Err(SyscallError::PermissionDenied)); + let info = bind(&holder.0); + println!("acpi: an unbound claim was refused its access and the lock"); + + // A request that names no space, width or direction, a value wider than + // its width, and a reserved byte that is set. + let io = Access::read(Space::SystemIo, 0x80, Width::Byte); + for malformed in [ + Access { space: 3, ..io }, + Access { width: 3, ..io }, + Access { width: 0, ..io }, + Access { write: 2, ..io }, + Access { write: 1, value: 0x100, ..io }, + Access { reserved: [0, 0, 1], ..io }, + ] { + let mut asked = malformed; + assert_eq!(syscall::acpi_access(holder.handle(), &mut asked), Err(SyscallError::InvalidArgument), "{malformed:?}"); + assert_eq!(asked, malformed, "acpi: a refused request was written to"); + } + + memory(&holder, &info); + ports(&holder, &info); + configuration(&holder, &info); + lock(&holder, &info); + + assert_eq!(syscall::acpi_lock_take(holder.handle()), Ok(true), "acpi: the lock, for the power-off to find"); + println!("{HELD_INTO_THE_STOP}"); + let refused = toyos::power::stop(toyos::power::Stop::Shutdown); + panic!("acpi: the power-off was refused: {refused:?}"); +} + +/// What the probe says once every arm above has passed, holding the lock. +const HELD_INTO_THE_STOP: &str = "acpi: holding the Global Lock, and asking for the power-off with it"; + +fn memory(holder: &Holder, info: &AcpiInfo) { + // RAM: the megabyte's first page, which a guest's firmware hands over as memory. + for width in [Width::Byte, Width::QWord] { + let (read, ty) = holder.ask(Access::read(Space::SystemMemory, 0x10_0000, width)); + assert_eq!(read, Err(Refused::UsableMemory), "acpi: RAM was read"); + assert!(USABLE.contains(&ty), "acpi: RAM answered memory type {ty}"); + assert_eq!(holder.write(Space::SystemMemory, 0x10_0000, width, 0), Err(Refused::UsableMemory), "acpi: RAM was written"); + } + // This program's own stack is RAM somewhere, and what it holds is still its own. + let canary = std::hint::black_box(0x5AFE_C0DE_5AFE_C0DEu64); + assert_eq!(canary, 0x5AFE_C0DE_5AFE_C0DE); + println!("acpi: RAM was refused both ways as UsableMemory"); + + // The tables: read, with the bytes firmware put there, and never written. + let (signature, ty) = holder.ask(Access::read(Space::SystemMemory, info.rsdp, Width::QWord)); + assert_eq!(signature, Ok(u64::from_le_bytes(*b"RSD PTR ")), "acpi: the RSDP's signature"); + assert_eq!(ty, ACPI_RECLAIM, "acpi: this firmware keeps its RSDP in memory type {ty}"); + assert_eq!(holder.write(Space::SystemMemory, info.rsdp, Width::Byte, b'X'.into()), Err(Refused::TableWrite)); + assert_eq!(holder.memory(info.rsdp, Width::Byte), u64::from(b'R'), "acpi: a refused write reached the RSDP"); + // An unaligned word and dword of it are one access each. + assert_eq!(holder.memory(info.rsdp + 1, Width::Word), u64::from(u16::from_le_bytes(*b"SD"))); + assert_eq!(holder.memory(info.rsdp + 1, Width::DWord), u64::from(u32::from_le_bytes(*b"SD P"))); + println!("acpi: the RSDP read through as type {ty} and its write was refused TableWrite"); + + // An address firmware's map does not list, between the PCI hole's start + // and the ECAM window, where this guest's firmware has the boot processor's range registers + // type everything from the top of low RAM to 4 GiB uncacheable: a + // register's address, read and never written. And one the map does not + // list below 1 MiB, the legacy video hole, which the kernel calls no + // register: the fixed range registers decide there, and it reads none. + let (hole, ty) = holder.ask(Access::read(Space::SystemMemory, 0xD000_0000, Width::DWord)); + assert!(hole.is_ok() && ty == UNLISTED, "acpi: an unlisted address the range registers type uncacheable answered {hole:?}, type {ty}"); + let (hole, ty) = holder.ask(Access::write(Space::SystemMemory, 0xD000_0000, Width::DWord, 0)); + assert_eq!((hole, ty), (Err(Refused::MemoryType), UNLISTED), "acpi: an unlisted address was written"); + let (hole, ty) = holder.ask(Access::read(Space::SystemMemory, 0xA_0000, Width::DWord)); + assert_eq!((hole, ty), (Err(Refused::UnlistedCached), UNLISTED), "acpi: an unlisted address below 1 MiB"); + // The local APIC; the I/O APIC, which the kernel drives through its + // first 0x20 bytes, at its first register, at the EOI register a chipset + // keeps at 0x40 and at its page's last dword; and the HPET. Each is a + // device's by the kernel's own record of it: this firmware's map lists + // none of them, and an address it does not list answers `MemoryType`. + for device in [0xFEE0_0000u64, 0xFEC0_0000, 0xFEC0_0040, 0xFEC0_0FFC, 0xFED0_0000] { + assert_eq!(holder.read(Space::SystemMemory, device, Width::DWord), Err(Refused::DeviceMemory), "acpi: {device:#x} was read"); + assert_eq!(holder.write(Space::SystemMemory, device, Width::DWord, 0), Err(Refused::DeviceMemory), "acpi: {device:#x} was written"); + } + // A memory BAR of a function no kernel driver maps: the network card, + // which is a claim's and which nothing in this boot claims, so its BAR + // stays where firmware put it. + let bar = nic_bar(holder); + assert_eq!(holder.read(Space::SystemMemory, bar, Width::DWord), Err(Refused::DeviceMemory), "acpi: a function's BAR at {bar:#x} was read"); + assert_eq!(holder.write(Space::SystemMemory, bar + 0x14, Width::DWord, 0), Err(Refused::DeviceMemory), "acpi: a function's BAR was written"); + assert_eq!(holder.read(Space::SystemMemory, u64::MAX, Width::Word), Err(Refused::Unmapped)); + println!("acpi: an unlisted register was read and refused its write MemoryType, an unlisted address below 1 MiB was refused UnlistedCached, and the interrupt controllers, the HPET and a function's BAR DeviceMemory"); + + // Non-volatile memory, both ways: the FACS, and the bytes after it. + let fadt = holder.table(info.rsdp, b"FACP"); + // Table 5.9: `FIRMWARE_CTRL` at 36, `X_FIRMWARE_CTRL` at 132. + let facs = match holder.memory(fadt + 132, Width::QWord) { + 0 => holder.memory(fadt + 36, Width::DWord), + wide => wide, + }; + let (signature, ty) = holder.ask(Access::read(Space::SystemMemory, facs, Width::DWord)); + assert_eq!(signature, Ok(u64::from(u32::from_le_bytes(*b"FACS")))); + assert_eq!(ty, ACPI_NVS, "acpi: this firmware keeps its FACS in memory type {ty}"); + let len = holder.memory(facs + 4, Width::DWord); + for (at, width) in [(facs + 16, Width::DWord), (facs, Width::Byte), (facs + len - 1, Width::Byte), (facs - 4, Width::QWord)] { + assert_eq!(holder.write(Space::SystemMemory, at, width, 0), Err(Refused::FacsWrite), "acpi: a write at {at:#x}"); + } + let beside = facs + len; + let was = holder.memory(beside, Width::QWord); + assert_eq!(holder.write(Space::SystemMemory, beside, Width::QWord, !was), Ok(()), "acpi: a write to non-volatile memory"); + assert_eq!(holder.memory(beside, Width::QWord), !was, "acpi: the write did not land"); + assert_eq!(holder.write(Space::SystemMemory, beside, Width::QWord, was), Ok(())); + println!("acpi: the FACS read through as type {ty}, its write was refused FacsWrite, and the memory after it was written and put back"); +} + +/// Where the first memory BAR of the guest's network card is, read from its +/// configuration space as a holder reads it (PCI Local Bus 3.0 §6.2.5.1: bit +/// 0 clear is memory, and bits 2:1 of `10b` a 64-bit address whose high half +/// is the next register). +fn nic_bar(holder: &Holder) -> u64 { + // virtio-net as a modern device (virtio 1.2 §4.1.2: device id 0x1040 + 1). + const NIC: u64 = 0x1041_1af4; + let config = |device: u8, offset: u16| holder.read(Space::PciConfig, pci_address(0, 0, device, 0, offset), Width::DWord).expect("acpi: a configuration read on bus 0"); + let device = (0..32).find(|&device| config(device, 0) == NIC).expect("acpi: no virtio network card on this guest's bus 0"); + (0..6) + .map(|slot| (config(device, 0x10 + slot * 4), slot)) + .find(|&(low, _)| low != 0 && low & 1 == 0) + .map(|(low, slot)| { + let high = if low >> 1 & 3 == 2 { config(device, 0x14 + slot * 4) } else { 0 }; + high << 32 | low & !0xF + }) + .expect("acpi: the network card has no memory BAR") +} + +fn ports(holder: &Holder, info: &AcpiInfo) { + // COM1 and the CMOS index: the kernel's, both ways. + for port in [0x3F8u64, 0x3FD, 0x70, 0x20, 0xCF8] { + assert_eq!(holder.read(Space::SystemIo, port, Width::Byte), Err(Refused::KernelPort), "acpi: port {port:#x} was read"); + assert_eq!(holder.write(Space::SystemIo, port, Width::Byte, b'!'.into()), Err(Refused::KernelPort), "acpi: port {port:#x} was written"); + } + assert_eq!(holder.write(Space::SystemIo, 0x3F7, Width::Word, 0), Err(Refused::KernelPort), "acpi: a word that ends on COM1"); + assert_eq!(holder.read(Space::SystemIo, 0xFFFF, Width::Word), Err(Refused::PortSpan)); + assert_eq!(holder.read(Space::SystemIo, 0x1_0000, Width::Byte), Err(Refused::PortSpan)); + assert_eq!(holder.read(Space::SystemIo, 0x80, Width::QWord), Err(Refused::PortSpan)); + + // The i8042's row, which another claim is for: this boot's kernel leaves + // the controller unprobed, so nothing declared its ports. + for port in [0x60u64, 0x64] { + assert_eq!(holder.read(Space::SystemIo, port, Width::Byte), Err(Refused::ClaimedPort), "acpi: port {port:#x} was read"); + assert_eq!(holder.write(Space::SystemIo, port, Width::Byte, 0), Err(Refused::ClaimedPort), "acpi: port {port:#x} was written"); + } + assert_eq!(holder.read(Space::SystemIo, 0x5F, Width::Word), Err(Refused::ClaimedPort), "acpi: a word that ends on the i8042's data port"); + + // The POST port, which the kernel declared and opens. + assert_eq!(holder.write(Space::SystemIo, 0x80, Width::Byte, 0x5A), Ok(()), "acpi: the POST port"); + + // `PM1a_CNT` and `SMI_CMD`, as the FADT names them (Table 5.9, at 64 and 48): read, never written. + let fadt = holder.table(info.rsdp, b"FACP"); + let control = holder.memory(fadt + 64, Width::DWord); + let held = holder.read(Space::SystemIo, control, Width::Word).expect("acpi: PM1a_CNT reads"); + assert_eq!(held & 1, 1, "acpi: PM1a_CNT reads {held:#06x}, SCI_EN clear, on a machine a claim put in ACPI mode"); + assert_eq!(holder.write(Space::SystemIo, control, Width::Word, held), Err(Refused::ReadOnlyPort), "acpi: PM1a_CNT was written"); + let smi_cmd = holder.memory(fadt + 48, Width::DWord); + assert_ne!(smi_cmd, 0, "acpi: this firmware names no SMI_CMD"); + holder.read(Space::SystemIo, smi_cmd, Width::Byte).expect("acpi: SMI_CMD reads"); + assert_eq!(holder.write(Space::SystemIo, smi_cmd, Width::Byte, 0), Err(Refused::ReadOnlyPort), "acpi: SMI_CMD was written"); + + // The claim's own event block, which nothing declared, and a dword of it. + let status = u64::from(info.pm1_event.port); + holder.read(Space::SystemIo, status, Width::Word).expect("acpi: the claim's own PM1 status"); + holder.read(Space::SystemIo, status, Width::DWord).expect("acpi: the claim's own PM1 event block as a dword"); + println!("acpi: COM1, the CMOS index, the 8259 and the configuration mechanism were refused KernelPort; the i8042's row ClaimedPort; PM1a_CNT and SMI_CMD read and refused their write ReadOnlyPort; the POST port was written"); +} + +fn configuration(holder: &Holder, info: &AcpiInfo) { + let host_bridge = |offset| pci_address(0, 0, 0, 0, offset); + let id = holder.read(Space::PciConfig, host_bridge(0), Width::DWord).expect("acpi: the host bridge's identity"); + assert!(id as u16 != 0xFFFF, "acpi: no function answers at 00:00.0 ({id:#010x})"); + assert_eq!(holder.read(Space::PciConfig, host_bridge(0), Width::Word), Ok(id & 0xFFFF)); + assert_eq!(holder.read(Space::PciConfig, host_bridge(2), Width::Word), Ok(id >> 16)); + assert_eq!(holder.read(Space::PciConfig, host_bridge(3), Width::Byte), Ok(id >> 24)); + // No function answers on the last bus's last device: all ones, not a refusal. + assert_eq!(holder.read(Space::PciConfig, pci_address(0, 0xFF, 31, 7, 0), Width::DWord), Ok(0xFFFF_FFFF)); + assert_eq!(holder.read(Space::PciConfig, host_bridge(0), Width::QWord), Err(Refused::ConfigSpan)); + assert_eq!(holder.read(Space::PciConfig, host_bridge(2), Width::DWord), Err(Refused::ConfigSpan)); + assert_eq!(holder.read(Space::PciConfig, host_bridge(0x1000), Width::Byte), Err(Refused::ConfigSpan)); + assert_eq!(holder.read(Space::PciConfig, pci_address(1, 0, 0, 0, 0), Width::DWord), Err(Refused::ConfigUnreachable)); + assert_eq!(holder.read(Space::PciConfig, 1 << 48, Width::DWord), Err(Refused::ConfigUnreachable)); + + // The same register through the ECAM window is the same access: the + // MCFG's first allocation names the window's base at 44. + let ecam = holder.memory(holder.table(info.rsdp, b"MCFG") + 44, Width::QWord); + assert_eq!(holder.read(Space::SystemMemory, ecam, Width::DWord), Ok(id), "acpi: the host bridge through ECAM"); + assert_eq!(holder.read(Space::SystemMemory, ecam, Width::QWord), Err(Refused::ConfigSpan)); + + // No write, by a function's address or through the ECAM window: the + // header, a register past it with the value it holds, extended space, a + // function nothing answers at, and a shape no read has. + let held = holder.read(Space::PciConfig, host_bridge(0x44), Width::Byte).expect("acpi: a register past the header"); + for (offset, width, value) in [(0x04, Width::Word, 0), (0x10, Width::DWord, 0), (0x3C, Width::Byte, 0), (0x44, Width::Byte, held), (0x44, Width::Byte, !held & 0xFF), (0x100, Width::DWord, 0)] { + assert_eq!(holder.write(Space::PciConfig, host_bridge(offset), width, value), Err(Refused::ConfigWrite), "acpi: {offset:#x} by address"); + assert_eq!(holder.write(Space::SystemMemory, ecam + u64::from(offset), width, value), Err(Refused::ConfigWrite), "acpi: {offset:#x} through ECAM"); + } + assert_eq!(holder.read(Space::PciConfig, host_bridge(0x44), Width::Byte), Ok(held), "acpi: a refused write reached the host bridge"); + assert_eq!(holder.write(Space::PciConfig, pci_address(0, 0xFF, 31, 7, 0x44), Width::Byte, 0), Err(Refused::ConfigWrite)); + assert_eq!(holder.write(Space::PciConfig, host_bridge(2), Width::DWord, 0), Err(Refused::ConfigWrite)); + println!("acpi: the host bridge read as {id:#010x} by its address and through ECAM, and every write to configuration space was refused ConfigWrite"); +} + +fn lock(holder: &Holder, info: &AcpiInfo) { + let fadt = holder.table(info.rsdp, b"FACP"); + let facs = match holder.memory(fadt + 132, Width::QWord) { + 0 => holder.memory(fadt + 36, Width::DWord), + wide => wide, + }; + let word = |holder: &Holder| holder.memory(facs + 16, Width::DWord); + const PENDING: u64 = 1; + const OWNED: u64 = 2; + + assert_eq!(word(holder), 0, "acpi: the keeper's lock outlived its claim"); + assert_eq!(syscall::acpi_lock_release(holder.handle()), Err(SyscallError::InvalidArgument), "acpi: a lock nobody took was given back"); + assert_eq!(syscall::acpi_lock_take(holder.handle()), Ok(true)); + assert_eq!(word(holder), OWNED); + assert_eq!(syscall::acpi_lock_take(holder.handle()), Err(SyscallError::AlreadyExists), "acpi: a second take"); + assert_eq!(syscall::acpi_lock_release(holder.handle()), Ok(())); + assert_eq!(word(holder), 0); + + // The firmware asks while the holder has it: the release clears the + // word and tells the firmware by `GBL_RLS`, bit 2 of `PM1a_CNT` (ACPI 6.5 + // §4.8.3.2), which this guest's model keeps as written where a chipset + // reads it back clear. + const GBL_RLS: u64 = 1 << 2; + // Table 5.9: `PM1a_CNT_BLK` at 64. + let control = holder.memory(fadt + 64, Width::DWord); + let told = |holder: &Holder| holder.read(Space::SystemIo, control, Width::Word).expect("acpi: PM1a_CNT reads") & GBL_RLS; + assert_eq!(syscall::acpi_lock_take(holder.handle()), Ok(true)); + assert_eq!(syscall::debug_with(debug_action::ACPI_FIRMWARE_LOCK, debug_action::FIRMWARE_ASKS), OWNED); + assert_eq!(word(holder), OWNED | PENDING); + assert_eq!(told(holder), 0, "acpi: GBL_RLS reads set before any release owed it"); + assert_eq!(syscall::acpi_lock_release(holder.handle()), Ok(())); + assert_eq!(word(holder), 0); + assert_eq!(told(holder), GBL_RLS, "acpi: a release the firmware had asked for wrote no GBL_RLS"); + + // The firmware owns it: the take is refused and leaves its request. + assert_eq!(syscall::debug_with(debug_action::ACPI_FIRMWARE_LOCK, debug_action::FIRMWARE_OWNS), 0); + assert_eq!(syscall::acpi_lock_take(holder.handle()), Ok(false), "acpi: a lock the firmware owns was taken"); + assert_eq!(word(holder), OWNED | PENDING); + assert_eq!(syscall::acpi_lock_take(holder.handle()), Ok(false)); + assert_eq!(syscall::acpi_lock_release(holder.handle()), Err(SyscallError::InvalidArgument), "acpi: the firmware's lock was given back"); + assert_eq!(word(holder), OWNED | PENDING, "acpi: a refused release changed the word"); + // The firmware lets go, and the next take has it; the stale request goes with the take. + assert_eq!(syscall::debug_with(debug_action::ACPI_FIRMWARE_LOCK, debug_action::FIRMWARE_FREES), OWNED | PENDING); + assert_eq!(syscall::acpi_lock_take(holder.handle()), Ok(true)); + assert_eq!(word(holder), OWNED); + assert_eq!(syscall::acpi_lock_release(holder.handle()), Ok(())); + println!("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"); +} + +/// What the keeper says once it holds the lock. +const KEPT: &str = "keeper: holding the Global Lock, and leaving with it"; + +fn keeper() { + let claim: Device = Endowments::get().take(CLAIM_LABEL).expect("acpi: the keeper is endowed the claim"); + bind(&claim); + assert_eq!(syscall::acpi_lock_take(claim.as_handle()), Ok(true), "keeper: the lock"); + println!("{KEPT}"); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 996ed86de37..91c55f30b34 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -165,6 +165,11 @@ const RUST_SKIP: &[&str] = &[ // It holds the boot open to near the runner's bound, and asserts nothing: // the `acpi_server_events` metal row runs it. "acpi_hold", + // It claims the fixed hardware itself, which needs a boot that starts no + // server, and stages the firmware's side of the Global Lock and finds the + // i8042's row another claim's, which need the test kernel and its + // `i8042-withheld`: `acpi_mediated_access` runs it on tests/acpicase. + "acpi_mediated", // It powers the machine off: `machine_shutdown_short_stop` runs it. "stop_short", ]; @@ -259,6 +264,13 @@ const MACHINE_TESTS: &[&str] = &[ // The press itself: QEMU raises the fixed power-button event on demand, // and nothing presses the T14's button but a hand. "acpi_power_button", + // What the kernel reads and writes for the `acpi` claim's holder and what + // it refuses. A red here is a write the kernel made — to RAM, to the + // firmware's tables, to COM1, to `PM1a_CNT`, to a function's configuration + // space — and the take that finds the Global Lock owned and the release + // that owes `GBL_RLS` need the FACS's word staged as only an idle firmware + // allows: neither is done to the T14, which nothing powers on again. + "acpi_mediated_access", // The power-off after a stop that left a thread running, in ACPI mode: it // ends the machine, so only one QEMU reports stopping can be asked, and // the T14 hands over in legacy mode, where no holder means no quieting. @@ -456,6 +468,16 @@ const METAL: &[(&str, metal::Metal)] = &[ judge: |b| acpi_events_on_metal(b[0]), }, ), + ( + // The server's load of the T14's own definition blocks, through the + // kernel's mediated access: `acpi_tables_on_metal` says what is read. + // The same boot as `acpi_server_events`. + "acpi_tables_loaded", + metal::Metal { + arms: &[metal::once("testcases-hold", "tests/testcases", &[], &["test_rs_acpi_hold"])], + judge: |b| acpi_tables_on_metal(b[0]), + }, + ), ( // The server killed: the kernel writes `ACPI_DISABLE` as its claim goes, // and `SCI_EN` reads clear after it. @@ -1678,6 +1700,66 @@ fn virt_job(profile: qemu::Profile, job: &str, said: &str) -> Result<(), String> bootlog::one_clock(&serial, &serial).map_err(|why| format!("{why}\nserial:\n{serial}")) } +/// What `acpi_mediated` says, an arm a line, once the kernel answered each as +/// its policy says. +const ACPI_MEDIATED_SAID: [&str; 8] = [ + "acpi: an unbound claim was refused its access and the lock", + "acpi: RAM was refused both ways as UsableMemory", + "acpi: the RSDP read through as type 9 and its write was refused TableWrite", + "acpi: an unlisted register was read and refused its write MemoryType, an unlisted address below 1 MiB was refused UnlistedCached, and the interrupt controllers, the HPET and a function's BAR DeviceMemory", + "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 and SMI_CMD read and refused their write ReadOnlyPort; the POST port was written", + "by its address and through ECAM, and every write to configuration space was refused ConfigWrite", + "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", +]; + +/// Boot `tests/acpicase`, whose one job is `test_rs_acpi_mediated`, on the +/// test kernel, and judge the job and what the kernel said beside it: the +/// lock found at boot, given back for the holder that died with it, and +/// given back for the probe itself, which asks for the power-off holding it +/// once every arm has passed. The probe does not come back from that, so its +/// verdict is its last line and the kernel's; a probe that ends instead is +/// one whose arm failed. +fn acpi_mediated_access() -> Result<(), String> { + const JOB: &str = "test_rs_acpi_mediated"; + const HELD_INTO_THE_STOP: &str = "acpi: holding the Global Lock, and asking for the power-off with it"; + const GIVEN_BACK_AT_THE_STOP: &str = "acpi: the Global Lock given back for a holder that left it taken (the machine is stopping)"; + let case = compile::repo_root().join("tests/acpicase"); + let mut qemu = QemuInstance::boot_with_options( + &case, + &[], + &[], + BootOptions { + // The test kernel, for the Global Lock's actuator too. + kernel_params: &["i8042-withheld"], + ready_marker: "acpi: the ACPI row: ", + extra_root_files: vec![suite_bin(toyos_build::arch::Arch::X86_64, "acpi_mediated")], + ..Default::default() + }, + ); + let mut console = format!("{}\n", qemu.boot_log()); + let ended = format!("===TEST_END {JOB} "); + await_guest(&mut qemu, &mut console, "the probe's power-off to give the lock back", |said| { + said.contains(GIVEN_BACK_AT_THE_STOP) || said.contains(&ended) + })?; + let said = serial::Serial::named("the probe's boot", console); + said.must_be_clean()?; + said.must_say(isa::WITHHELD)?; + 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)")?; + // Whose range registers passed the unlisted read, and how this + // hypervisor's second CPU holds its own beside them. + for line in ["mtrr: the boot processor's range registers: ", "mtrr: cpu1's range registers are "] { + eprintln!(" [acpi] {}", said.must_say(line)?.trim()); + } + for line in ACPI_MEDIATED_SAID { + eprintln!(" [acpi] {}", said.must_say(line)?.trim()); + } + said.must_say(HELD_INTO_THE_STOP)?; + eprintln!(" [acpi] {}", said.must_say_after(HELD_INTO_THE_STOP, GIVEN_BACK_AT_THE_STOP)?.trim()); + Ok(()) +} + /// A `mask-windows` boot's windows: `common::irqcensus::windows`'s verdict, /// with `cpus` CPUs reporting. fn mask_windows(capture: &str, cpus: u32) -> Result<(), String> { @@ -2712,6 +2794,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config), "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), + "acpi_mediated_access" => acpi_mediated_access(), "machine_shutdown_short_stop" => power::machine_shutdown_short_stop(test_config), other => Err(format!("unknown machine test {other}")), } @@ -3753,15 +3836,85 @@ fn acpi_events_on_metal(back: &metal::Readback) -> Result<(), String> { Ok(()) } +/// How many definition blocks the T14 has: Linux on the same machine says +/// `14 ACPI AML tables successfully acquired and loaded`. +const T14_DEFINITION_BLOCKS: usize = 14; + +/// The server's load of the T14's tables, every access the kernel's to make +/// for it: all of the machine's definition blocks, as many as Linux loads +/// there, each fetched through `SYS_ACPI`, summing to zero as its firmware +/// sealed it, and loaded; `\_S5` evaluating to the `SLP_TYPa` the kernel's +/// own scan of the DSDT decoded; and nothing refused, so no bridge answered +/// what the interpreter refuses, no address was `Unmapped`, and no access +/// the load makes is one the policy keeps from it. The load's AML read +/// memory, read configuration space and took the Global Lock, which is the +/// real lock word exchanged and given back each time. Every other CPU of the +/// machine said how its range registers stand beside the boot processor's, +/// which typed the load's unlisted read a register's, and none holds registers +/// that are on and not those. What it prints beside +/// that is the first measurement of each: the load's time, the reads by +/// address space, the takes that found the firmware holding the lock, and +/// the pages of memory by the type the firmware's map gives them. +fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { + let (log, kernel) = (back.log(), back.kernel()); + let lines: Vec<&str> = log + .text() + .lines() + .filter(|l| toyos_logstream::program_line(l).is_some_and(|said| said.tag == "acpiserver")) + .collect(); + if let Some(fired) = lines.iter().find(|l| l.contains("panicked")) { + return Err(format!("the server died: {fired}")); + } + let blocks = power::acpi_tables_loaded(&log, &kernel)?; + if blocks != T14_DEFINITION_BLOCKS { + return Err(format!("the server found {blocks} definition blocks where Linux loads {T14_DEFINITION_BLOCKS}")); + } + 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")) { + return Err(format!( + "{others} other CPUs came up, and their range registers beside the boot processor's are {compared:#?}" + )); + } + let off = compared.iter().filter(|l| l.contains("range registers are off")).count(); + eprintln!(" [acpi] {others} other CPUs' range registers compared with the boot processor's: {off} off, none on and different"); + let refused: Vec<&&str> = lines.iter().filter(|l| l.contains("acpiserver: refused") || l.contains(" refused: ")).collect(); + if !refused.is_empty() { + return Err(format!("the server refused something of this machine's AML: {refused:#?}")); + } + let took = log.must_say(&format!("acpiserver: {blocks} of {blocks} tables loaded in "))?; + let bytes = log.must_say("acpiserver: the tables' bytes took ")?; + let aml = log.must_say("acpiserver: the tables' AML read SystemMemory ")?; + let memory = number_between(aml, "AML read SystemMemory ", " times, SystemIO ")?; + let config = number_between(aml, " and PCI_Config ", ", its memory in pages: ")?; + let takes = number_between(aml, "; took the Global Lock ", " times, ")?; + if memory == 0 || config == 0 || takes == 0 { + return Err(format!("this machine's tables read memory and configuration space and take the Global Lock as they load, and the server's did not: {aml}")); + } + for line in [took, bytes, aml] { + eprintln!(" [acpi] {}", line.trim()); + } + Ok(()) +} + /// The server's death on the T14: the kernel put the machine in ACPI mode for /// the job's claim, and when the killed server's claim went it wrote /// `ACPI_DISABLE` (the FADT's 0xf1) and read `SCI_EN` clear: the firmware has /// the buttons again. Each write was made on the boot processor, and the /// enable was asked from another CPU: the job claims from a thread it found -/// off the boot processor, so that write is the one that crossed. +/// off the boot processor, so that write is the one that crossed. And the +/// Global Lock this machine's firmware keeps: the kernel read the FACS its +/// FADT names through the direct map and found the lock word in ACPI NVS +/// memory (type 10) by the firmware's own map, which is where Linux's print +/// of the same map puts it. fn acpi_death_on_metal(back: &metal::Readback) -> Result<(), String> { back.job_passed("test_rs_acpi_release")?; let kernel = back.kernel(); + let lock = kernel.must_say("acpi: the Global Lock is the FACS's at ")?.trim(); + if !lock.ends_with(", in memory the firmware's map types 10") { + return Err(format!("the Global Lock's word is not in ACPI NVS memory: {lock}")); + } + eprintln!(" [acpi] {lock}"); let enabled = kernel.must_say("acpi: ACPI mode: ACPI_ENABLE 0xf0 written to SMI_CMD 0xb2 ")?; // The kernel says this only of a `PM1a_CNT` it read with `SCI_EN` clear. let left = kernel.must_say("acpi: legacy mode again: ACPI_DISABLE 0xf1 written to SMI_CMD 0xb2 ")?; diff --git a/toyos-abi/src/acpi.rs b/toyos-abi/src/acpi.rs index 24d65de557f..0ad0cd054ec 100644 --- a/toyos-abi/src/acpi.rs +++ b/toyos-abi/src/acpi.rs @@ -9,6 +9,15 @@ //! **The SCI is a level line, masked by the kernel each time it is taken.** The //! holder clears the status bits behind it and then acknowledges the claim //! ([`ACK`]); a line acknowledged with a status bit still set is taken again. +//! +//! **What the firmware's AML addresses outside those blocks the holder reaches +//! one access at a time, through the kernel** ([`Access`]): firmware-owned +//! memory and a port both ways, the memory its tables are in and a register +//! at an address firmware lists nowhere to read, a function's configuration space to read. The +//! kernel decides each +//! by what the address is and answers a refusal by name ([`Refused`]); nothing +//! is mapped and no port opened. The firmware's Global Lock is taken and given +//! back the same way ([`op::LOCK_TAKE`]), and goes back with the claim. crate::user_safe! { /// A run of ports a register block occupies; `len` 0 is no block. @@ -37,6 +46,9 @@ crate::user_safe! { /// The claim's description. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct AcpiInfo { + /// Where the loader found the RSDP: the root of the tables the holder + /// reads through [`Access`]. + pub rsdp: u64, /// The PM1a event block, its status half then its enable half. pub pm1_event: Block, /// The GPE0 block, its status half then its enable half. @@ -48,6 +60,7 @@ crate::user_safe! { /// The GPE the embedded controller raises, inside [`Self::gpe0`]. pub ec_gpe: u16, pub flags: u16, + pub reserved: u32, } } @@ -59,3 +72,190 @@ impl AcpiInfo { /// The word a holder writes to its claim to have the SCI unmasked. pub const ACK: u32 = 1; + +/// What [`crate::syscall::SYS_ACPI`] does on the claim. +pub mod op { + /// One [`Access`](super::Access), in and out through the caller's struct. + pub const ACCESS: u64 = 0; + /// Try the firmware's Global Lock (ACPI 6.5 §5.2.10.1): answers + /// [`TAKEN`], or [`PENDING`] where the firmware owns it, with the pending + /// bit left set so the firmware raises `GBL_STS` when it lets go. + pub const LOCK_TAKE: u64 = 1; + /// Give the Global Lock back, signalling the firmware where it asked + /// meanwhile. + pub const LOCK_RELEASE: u64 = 2; + + pub const TAKEN: u64 = 0; + pub const PENDING: u64 = 1; +} + +/// The address space an [`Access`] names, by its ACPI number (ACPI 6.5 Table +/// 5.1). +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Space { + SystemMemory = 0, + SystemIo = 1, + /// `address` is [`pci_address`]'s. + PciConfig = 2, +} + +impl Space { + pub const fn from_raw(raw: u8) -> Option { + match raw { + 0 => Some(Self::SystemMemory), + 1 => Some(Self::SystemIo), + 2 => Some(Self::PciConfig), + _ => None, + } + } +} + +/// How many bytes one access moves. +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Width { + Byte = 1, + Word = 2, + DWord = 4, + QWord = 8, +} + +impl Width { + pub const fn from_raw(raw: u8) -> Option { + match raw { + 1 => Some(Self::Byte), + 2 => Some(Self::Word), + 4 => Some(Self::DWord), + 8 => Some(Self::QWord), + _ => None, + } + } + + pub const fn bytes(self) -> u64 { + self as u64 + } + + /// The widest value an access of this width carries. + pub const fn max_value(self) -> u64 { + match self { + Self::QWord => u64::MAX, + narrower => (1 << (8 * narrower as u64)) - 1, + } + } +} + +/// A PCI_Config [`Access::address`]: the function, and the byte offset into +/// its 4096 bytes of configuration space. +pub const fn pci_address(segment: u16, bus: u8, device: u8, function: u8, offset: u16) -> u64 { + (segment as u64) << 32 | (bus as u64) << 24 | (device as u64 & 0x1F) << 19 | (function as u64 & 7) << 16 | offset as u64 +} + +/// Why the kernel did not make an access. Each is one row of the policy, so a +/// holder's log tells them apart. +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Refused { + /// Memory the kernel hands out as RAM: its own, and every process's. + UsableMemory = 1, + /// A write to memory a firmware keeps its tables in: ACPI reclaim, and + /// runtime-services data. + TableWrite = 2, + /// Memory of a type the kernel passes no access to, or a write at an + /// address firmware's map does not list; [`Access::memory_type`] says + /// which. + MemoryType = 3, + /// Past the end of what the kernel maps. + Unmapped = 4, + /// Memory a device decodes: a window the kernel drives one through, a PCI + /// function's memory BAR whoever drives it, or the local APIC's. + DeviceMemory = 5, + /// A write to the FACS, whose Global Lock is the kernel's to change. + FacsWrite = 6, + /// An access whose first and last byte the map types differently. + Straddles = 7, + /// A port the kernel declared and keeps to itself. + KernelPort = 8, + /// A write to a port the kernel declared and lets be read. + ReadOnlyPort = 9, + /// A port of a function another claim is for. + ClaimedPort = 10, + /// No port access is this wide, or it runs past port 0xFFFF. + PortSpan = 11, + /// A segment group or bus the kernel's configuration window does not hold. + ConfigUnreachable = 12, + /// A configuration access wider than a dword, across a dword boundary, or + /// past the function's 4096 bytes. + ConfigSpan = 13, + /// A write to configuration space, by its address or through the ECAM + /// window: the kernel makes none for the holder. + ConfigWrite = 14, + /// A read at an address firmware's map does not list, which the + /// processor's range registers do not type uncacheable: no register. + UnlistedCached = 15, + /// A read at an address firmware's map does not list, on a machine where + /// some CPU's range registers are on and are not the boot processor's: + /// what types the address a register is not what every CPU reads it + /// under. + RangeRegistersDiffer = 16, +} + +impl Refused { + pub const fn from_raw(raw: u8) -> Option { + Some(match raw { + 1 => Self::UsableMemory, + 2 => Self::TableWrite, + 3 => Self::MemoryType, + 4 => Self::Unmapped, + 5 => Self::DeviceMemory, + 6 => Self::FacsWrite, + 7 => Self::Straddles, + 8 => Self::KernelPort, + 9 => Self::ReadOnlyPort, + 10 => Self::ClaimedPort, + 11 => Self::PortSpan, + 12 => Self::ConfigUnreachable, + 13 => Self::ConfigSpan, + 14 => Self::ConfigWrite, + 15 => Self::UnlistedCached, + 16 => Self::RangeRegistersDiffer, + _ => return None, + }) + } +} + +/// [`Access::memory_type`] for an address firmware's map does not list, and +/// for an access that is not to memory. +pub const UNLISTED: u8 = 0xFF; + +crate::user_safe! { + /// One access the holder asks the kernel to make, and its answer. + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub struct Access { + pub address: u64, + /// What to write, or what was read. + pub value: u64, + /// A [`Space`]. + pub space: u8, + /// A [`Width`]. + pub width: u8, + /// 0 to read, 1 to write. + pub write: u8, + /// Out: 0 where the access was made, else a [`Refused`]. + pub refused: u8, + /// Out, for memory: the UEFI memory type firmware's map gives the first + /// byte (`EFI_MEMORY_TYPE`), or [`UNLISTED`]. + pub memory_type: u8, + pub reserved: [u8; 3], + } +} + +impl Access { + pub const fn read(space: Space, address: u64, width: Width) -> Self { + Self { address, value: 0, space: space as u8, width: width as u8, write: 0, refused: 0, memory_type: UNLISTED, reserved: [0; 3] } + } + + pub const fn write(space: Space, address: u64, width: Width, value: u64) -> Self { + Self { address, value, space: space as u8, width: width as u8, write: 1, refused: 0, memory_type: UNLISTED, reserved: [0; 3] } + } +} diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 3e3260d780e..fc1e4989003 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -282,19 +282,24 @@ pub const SYS_PORT_BADGE: u64 = 126; /// [`Rights::TRACE`]: crate::handle::Rights::TRACE pub const SYS_TRACE_READ: u64 = 127; +/// One operation on the `acpi` claim ([`crate::acpi::op`]): an access the +/// kernel mediates, or the firmware's Global Lock. See [`acpi_access`] and +/// [`acpi_lock_take`]. +pub const SYS_ACPI: u64 = 128; + /// Bins in the per-process syscall profile — one for every number this ABI /// issues, and one at the end for every number it does not. /// /// **The profile's parts sum to its total, and that is the whole requirement.** /// A bin array narrower than the ABI reaches drops calls out of the line while /// the total goes on counting them. -pub const SYSCALL_PROFILE_BINS: usize = 129; +pub const SYSCALL_PROFILE_BINS: usize = 130; /// Where a number this ABI does not issue is counted. Merging is a degradation /// a reader can see in the line; dropping is one nobody can. pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1; -const _: () = assert!(SYS_TRACE_READ < SYSCALL_PROFILE_OTHER as u64); +const _: () = assert!(SYS_ACPI < SYSCALL_PROFILE_OTHER as u64); pub const WNOHANG: u64 = 1; @@ -849,6 +854,17 @@ pub mod debug_action { /// and re-armed a quantum, [`RING0_FIRE_NEVER`] if none came within /// 100 ms, and [`RING0_FIRE_OTHER_SPAN`] if it re-armed something else. pub const RING0_TIMER_IN_SYSCALL: u64 = 26; + /// Play the firmware's side of the Global Lock (ACPI 6.5 §5.2.10.1) on the + /// FACS's lock word: [`FIRMWARE_OWNS`] takes it, as an SMI handler would, + /// [`FIRMWARE_ASKS`] sets the pending bit under an owner, as a handler that + /// found it taken would, and [`FIRMWARE_FREES`] clears both. Answers the + /// word as it was, or `NotSupported` on a machine with no FACS. No guest's + /// firmware touches the lock on demand; the take that finds it owned and + /// the release that owes a signal are the shipped paths. + pub const ACPI_FIRMWARE_LOCK: u64 = 27; + pub const FIRMWARE_FREES: u64 = 0; + pub const FIRMWARE_OWNS: u64 = 1; + pub const FIRMWARE_ASKS: u64 = 2; pub const RING0_FIRE_REARMED: u64 = 0; pub const RING0_FIRE_NEVER: u64 = 1; pub const RING0_FIRE_OTHER_SPAN: u64 = 2; @@ -2082,6 +2098,40 @@ pub fn trace_read( .map(|n| n as usize) } +/// Have the kernel make one access on the `acpi` claim `claim`, or refuse it +/// by name. `Ok` is what a read answered, and what a write wrote. +/// +/// The claim must have been read once, which binds it to the caller: one that +/// was not is refused [`SyscallError::PermissionDenied`], and an access that +/// names no space or width [`SyscallError::InvalidArgument`]. +pub fn acpi_access( + claim: RawHandle, + access: &mut crate::acpi::Access, +) -> Result, SyscallError> { + check_unit(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::ACCESS, access as *mut crate::acpi::Access as u64, 0))?; + Ok(match access.refused { + 0 => Ok(access.value), + word => Err(crate::acpi::Refused::from_raw(word).expect("the kernel answers a refusal this ABI names")), + }) +} + +/// Try the firmware's Global Lock for the `acpi` claim's holder: `Ok(true)` +/// where it is now held, `Ok(false)` where the firmware owns it and will raise +/// `GBL_STS` on letting go. A lock the claim already holds is refused +/// [`SyscallError::AlreadyExists`], and every take +/// [`SyscallError::NotSupported`] on a machine whose FADT names a FACS the +/// kernel exchanges no lock word in; one that names no FACS has no lock, and +/// a take is answered taken. +pub fn acpi_lock_take(claim: RawHandle) -> Result { + check(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::LOCK_TAKE, 0, 0)).map(|word| word == crate::acpi::op::TAKEN) +} + +/// Give the Global Lock back. One the claim does not hold is refused +/// [`SyscallError::InvalidArgument`]. +pub fn acpi_lock_release(claim: RawHandle) -> Result<(), SyscallError> { + check_unit(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::LOCK_RELEASE, 0, 0)) +} + /// Sleep for the given number of nanoseconds. pub fn nanosleep(nanos: u64) { syscall(SYS_NANOSLEEP, nanos, 0, 0, 0); diff --git a/toyos-acpi/src/facs.rs b/toyos-acpi/src/facs.rs new file mode 100644 index 00000000000..23f2ccf1f8b --- /dev/null +++ b/toyos-acpi/src/facs.rs @@ -0,0 +1,87 @@ +//! The FACS (ACPI 6.5 §5.2.10, Table 5.13), which the FADT points at and no +//! XSDT lists, and the Global Lock in it (§5.2.10.1, Table 5.16). +//! +//! The lock is one dword both the operating system and the firmware's SMI +//! handlers change by compare-and-exchange. [`acquire`] and [`release`] are +//! the two transitions §5.2.10.1 gives as code, each from the word read to the +//! word to exchange it for. + +use crate::fadt::{FADT_FIRMWARE_CTRL, FADT_X_FIRMWARE_CTRL}; +use crate::{Phys, Table, MAX_TABLE_LEN, SDT_REVISION}; + +/// Table 5.13: `Length` at 4, the Global Lock at 16, in a structure of 64 +/// bytes or more; §5.2.10 aligns it on a 64-byte boundary. +const FACS_LENGTH: u64 = 4; +pub const FACS_GLOBAL_LOCK: u64 = 16; +const FACS_MIN_LEN: u32 = 64; +const FACS_ALIGN: u64 = 64; + +/// Table 5.16. +pub const PENDING: u32 = 1 << 0; +pub const OWNED: u32 = 1 << 1; + +/// Where the FACS is, and the length it declares. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Facs { + pub base: u64, + pub len: u32, +} + +/// Why a machine's FACS is none this decoder hands out. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum FacsRefused { + /// The FADT names none: both address fields are zero. + Absent, + /// Its first 64 bytes are not all readable. + Unmapped(u64), + /// Not on the 64-byte boundary §5.2.10 gives it: an address a firmware + /// that kept the specification did not write. + Misaligned(u64), + Signature, + Length(u32), +} + +/// The FACS the FADT names: `X_FIRMWARE_CTRL` where a revision that has it +/// holds a non-zero one, else `FIRMWARE_CTRL` (Table 5.9). +pub fn facs(phys: P, fadt: &Table

) -> Result { + let wide = match fadt.byte(SDT_REVISION) { + Some(r) if r >= 2 => fadt.u64_at(FADT_X_FIRMWARE_CTRL).filter(|a| *a != 0), + _ => None, + }; + let base = wide.unwrap_or_else(|| u64::from(fadt.u32_at(FADT_FIRMWARE_CTRL).unwrap_or(0))); + if base == 0 { + return Err(FacsRefused::Absent); + } + if !base.is_multiple_of(FACS_ALIGN) { + return Err(FacsRefused::Misaligned(base)); + } + if !phys.readable(base, FACS_MIN_LEN as usize) { + return Err(FacsRefused::Unmapped(base)); + } + if crate::bytes4(phys, base) != *b"FACS" { + return Err(FacsRefused::Signature); + } + let len = crate::u32le(phys, base + FACS_LENGTH); + if len < FACS_MIN_LEN || len as usize > MAX_TABLE_LEN { + return Err(FacsRefused::Length(len)); + } + Ok(Facs { base, len }) +} + +/// §5.2.10.1's `AcquireGlobalLock`: the word to exchange `word` for, and +/// whether that exchange takes the lock. Where it does not, the new word +/// carries the pending bit, and the owner signals its release. Decided by the +/// owner bit read, where the sequence's `cmp dl, 3` reads the low byte whole: +/// the two agree while bits 2 to 7 are zero, as Table 5.16 reserves them. +pub const fn acquire(word: u32) -> (u32, bool) { + let owned = word & OWNED != 0; + let new = word & !PENDING | OWNED | if owned { PENDING } else { 0 }; + (new, !owned) +} + +/// §5.2.10.1's `ReleaseGlobalLock`: the word to exchange `word` for, and +/// whether the other side asked while the lock was held and is owed the +/// release's signal. +pub const fn release(word: u32) -> (u32, bool) { + (word & !(PENDING | OWNED), word & PENDING != 0) +} diff --git a/toyos-acpi/src/fadt.rs b/toyos-acpi/src/fadt.rs index ed77ba361e6..5f4408c78ad 100644 --- a/toyos-acpi/src/fadt.rs +++ b/toyos-acpi/src/fadt.rs @@ -5,6 +5,7 @@ use core::num::NonZeroU8; use crate::{find_table, Phys, Table, TableError, SDT_REVISION}; /// Table 5.9 offsets, from the start of the table. +pub(crate) const FADT_FIRMWARE_CTRL: usize = 36; pub const FADT_DSDT: usize = 40; pub const FADT_PM1A_CNT_BLK: usize = 64; const FADT_CENTURY: usize = 108; @@ -15,6 +16,7 @@ const FADT_RESET_REG: usize = 116; const FADT_RESET_VALUE: usize = 128; const FADT_ARM_BOOT_ARCH: usize = 129; const FADT_MINOR_VERSION: usize = 131; +pub(crate) const FADT_X_FIRMWARE_CTRL: usize = 132; pub const FADT_X_DSDT: usize = 140; /// `ARM_BOOT_ARCH`'s two flags. diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index 0f59f9ff4b8..ece5e687ddc 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -16,6 +16,7 @@ mod dsdt; mod ecdt; +mod facs; mod fadt; mod gtdt; mod madt; @@ -26,12 +27,14 @@ use toyos_bootmap::DirectMapEnd; pub use dsdt::{s5_slp_typ, S5}; pub use ecdt::{ecdt, Ec, EcRefused, Register, ECDT_NEEDED}; +pub use facs::{acquire, facs, release, Facs, FacsRefused, FACS_GLOBAL_LOCK, OWNED, PENDING}; pub use fadt::{ century_of, dsdt_address, fixed_hardware, iapc_boot_arch, pm1a_control, psci, reset_register, rtc_century, Century, Field, FixedHardware, FixedRefused, LegacyMode, PowerButton, Psci, Reset, CMOS_RAM, FADT_FOR_FIXED_HARDWARE, FADT_FOR_RESET, FADT_PM1A_CNT_BLK, FADT_X_DSDT, }; +use fadt::FADT_DSDT; pub use madt::{ isa_line, madt_entries, sci_line, Gicc, IoApicEntry, Line, MadtEntries, MadtEntry, MadtHalt, Polarity, SourceOverride, Trigger, MADT_ENTRIES, @@ -282,10 +285,7 @@ pub fn find_table( needed: usize, ) -> Result, TableError> { let xsdt = xsdt(phys, rsdp_addr)?; - // `Table::open` guarantees len >= SDT_HEADER_LEN, so this subtraction is total. - let entry_count = (xsdt.len - SDT_HEADER_LEN) / 8; - - for i in 0..entry_count { + for i in 0..entries(&xsdt) { let Some(at) = xsdt.u64_at(SDT_HEADER_LEN + i * 8) else { break }; match Table::open(phys, at, signature, needed) { // An entry pointing at nothing is one entry skipped, not the end of the walk. @@ -296,6 +296,73 @@ pub fn find_table( Err(TableError::Absent) } +/// How many 8-byte entries an XSDT holds. +fn entries(xsdt: &Table

) -> usize { + // `Table::open` guarantees len >= SDT_HEADER_LEN, so this subtraction is total. + (xsdt.len - SDT_HEADER_LEN) / 8 +} + +/// A machine's definition blocks in the order they are loaded (ACPI 6.5 +/// §5.2.11.1, §5.2.11.2): the DSDT the FADT names, then every SSDT the XSDT +/// lists, in the XSDT's order. See [`definition_blocks`]. +pub struct DefinitionBlocks

{ + xsdt: Table

, + dsdt: bool, + next: usize, +} + +/// Every definition block of the machine whose RSDP is at `rsdp_addr`, each +/// validated or answered as the refusal that says why it is not: the walk +/// goes on past a refused one, which is its caller's to rule on. The first +/// item is always the DSDT's. An entry of another signature and a null entry +/// are passed over; an entry whose header the reader cannot reach is answered +/// [`TableError::Unmapped`], since nothing says it is no SSDT. The same holds +/// of the FADT: a DSDT whose FADT was not found among entries of which one +/// could not be read is refused as that entry, not as [`TableError::Absent`], +/// which is said only where every entry was read and none is the FADT. +pub fn definition_blocks(phys: P, rsdp_addr: u64) -> Result, TableError> { + Ok(DefinitionBlocks { xsdt: xsdt(phys, rsdp_addr)?, dsdt: false, next: 0 }) +} + +impl DefinitionBlocks

{ + /// The first FADT the XSDT lists, long enough to name a DSDT. + fn fadt(&self) -> Result, TableError> { + let mut unread = None; + for i in 0..entries(&self.xsdt) { + let Some(at) = self.xsdt.u64_at(SDT_HEADER_LEN + i * 8) else { break }; + match Table::open(self.xsdt.phys, at, b"FACP", FADT_DSDT + 4) { + Err(TableError::Absent) => {} + Err(TableError::Unmapped { .. }) if at == 0 => {} + Err(unmapped @ TableError::Unmapped { .. }) => { + unread.get_or_insert(unmapped); + } + found => return found, + } + } + Err(unread.unwrap_or(TableError::Absent)) + } +} + +impl Iterator for DefinitionBlocks

{ + type Item = Result, TableError>; + + fn next(&mut self) -> Option { + if !core::mem::replace(&mut self.dsdt, true) { + return Some(self.fadt().and_then(|fadt| Table::open(self.xsdt.phys, dsdt_address(&fadt), b"DSDT", SDT_HEADER_LEN))); + } + while self.next < entries(&self.xsdt) { + let at = self.xsdt.u64_at(SDT_HEADER_LEN + self.next * 8)?; + self.next += 1; + match Table::open(self.xsdt.phys, at, b"SSDT", SDT_HEADER_LEN) { + Err(TableError::Absent) => continue, + Err(TableError::Unmapped { .. }) if at == 0 => continue, + block => return Some(block), + } + } + None + } +} + /// PCI Firmware Specification 3.3, Table 4-3: the first allocation structure /// sits one 8-byte reserved field past the header, and its base address is the /// ECAM window's. diff --git a/toyos-acpi/tests/corpus.rs b/toyos-acpi/tests/corpus.rs index 2886495771f..4bfa4668828 100644 --- a/toyos-acpi/tests/corpus.rs +++ b/toyos-acpi/tests/corpus.rs @@ -12,7 +12,7 @@ use common::{declare_len, entry, madt, rsdp, sdt, t14_root_bridge, xsdt, Machine use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ - dsdt_address, ecam_base, ecdt, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, + definition_blocks, dsdt_address, ecam_base, ecdt, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, s5_slp_typ, sci_line, Century, EcRefused, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, Register, Reset, SourceOverride, Table, TableError, Trigger, ECDT_NEEDED, @@ -396,6 +396,10 @@ fn no_single_byte_mutation_of_a_real_table_panics_or_runs_away() { let _ = hpet_base(m, rsdp_at); let _ = rtc_century(m, rsdp_at); let _ = iapc_boot_arch(m, rsdp_at); + if let Ok(blocks) = definition_blocks(m, rsdp_at) { + // The DSDT, and at most an item an 8-byte entry of the largest XSDT. + assert!(blocks.count() <= 1 + MAX_TABLE_LEN / 8, "{which}[{offset}]={value:#04x}: the block walk is not ending"); + } if let Ok(t) = find_table(m, rsdp_at, b"FACP", 36) { let _ = reset_register(&t); let _ = psci(&t); @@ -435,6 +439,129 @@ fn no_single_byte_mutation_of_a_real_table_panics_or_runs_away() { assert!(halts[1] > 0, "no resealed mutation halted a walk, so that arm is untested here"); } +/// A FADT of the ACPI 1.0 length naming its DSDT at `dsdt`. +fn facp_naming(dsdt: u32) -> Vec { + let mut body = vec![0u8; 116 - 36]; + body[40 - 36..44 - 36].copy_from_slice(&dsdt.to_le_bytes()); + sdt(b"FACP", 1, &body) +} + +/// The DSDT first, wherever the XSDT lists the FADT, then the SSDTs in the +/// XSDT's order and nothing else it lists; a refused block is answered in +/// its place and the walk goes on past it. +#[test] +fn the_definition_blocks_are_the_dsdt_and_then_each_ssdt_in_the_xsdts_order() { + const DSDT_AT: u64 = 0x10_0000; + let at = |n: u64| TABLE_AT + n * 0x1000; + let head = rsdp(XSDT_AT, 2, 36); + let fadt = facp_naming(DSDT_AT as u32); + let dsdt = sdt(b"DSDT", 2, &[1]); + let (first, second, third) = (sdt(b"SSDT", 2, &[1]), sdt(b"SSDT", 2, &[2, 2]), sdt(b"SSDT", 2, &[3, 3, 3])); + let mut broken = sdt(b"SSDT", 2, &[4]); + broken[9] = broken[9].wrapping_add(1); + let mut long = sdt(b"SSDT", 2, &[5]); + declare_len(&mut long, 0x2000); + let hpet = sdt(b"HPET", 1, &[0u8; 20]); + // An SSDT ahead of the FADT, a null entry, a table that is no definition + // block, an SSDT that does not sum, an entry no region holds, an SSDT + // longer than what holds it, and a DSDT the XSDT lists itself, which is + // not where a DSDT is named. + let root = xsdt(&[at(1), 0, at(0), at(6), at(2), at(3), 0xdead_0000, at(4), DSDT_AT, at(5)]); + let regions: &[(u64, &[u8])] = &[ + (RSDP_AT, &head), + (XSDT_AT, &root), + (at(0), &fadt), + (at(1), &first), + (at(2), &second), + (at(3), &broken), + (at(4), &long), + (at(5), &third), + (at(6), &hpet), + (DSDT_AT, &dsdt), + ]; + let blocks: Vec> = definition_blocks(Machine { regions }, RSDP_AT) + .expect("the XSDT") + .map(|block| block.map(|table| (table.base(), table.len()))) + .collect(); + assert_eq!( + blocks, + [ + Ok((DSDT_AT, 37)), + Ok((at(1), 37)), + Ok((at(2), 38)), + Err(TableError::Checksum), + Err(TableError::Unmapped { at: 0xdead_0000, len: 36 }), + Err(TableError::Unmapped { at: at(4), len: 0x2000 }), + Ok((at(5), 39)), + ] + ); +} + +/// The DSDT's item is the first whatever became of it: a machine whose XSDT +/// lists no FADT, and one whose FADT names no DSDT, each say so there, and +/// the SSDTs follow all the same. +#[test] +fn a_dsdt_that_cannot_be_found_is_the_first_block_refused() { + let head = rsdp(XSDT_AT, 2, 36); + let ssdt = sdt(b"SSDT", 2, &[1]); + let walk = |regions: &[(u64, &[u8])]| -> Vec> { + definition_blocks(Machine { regions }, RSDP_AT).expect("the XSDT").map(|block| block.map(|table| table.base())).collect() + }; + + let root = xsdt(&[TABLE_AT]); + assert_eq!(walk(&[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &ssdt)]), [Err(TableError::Absent), Ok(TABLE_AT)]); + + let fadt = facp_naming(0); + let root = xsdt(&[TABLE_AT + 0x1000, TABLE_AT]); + assert_eq!( + walk(&[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &ssdt), (TABLE_AT + 0x1000, &fadt)]), + [Err(TableError::Unmapped { at: 0, len: 36 }), Ok(TABLE_AT)] + ); + + // The FADT names a table that is there and is no DSDT. + let fadt = facp_naming(TABLE_AT as u32); + let root = xsdt(&[TABLE_AT + 0x1000]); + assert_eq!(walk(&[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &ssdt), (TABLE_AT + 0x1000, &fadt)]), [Err(TableError::Absent)]); + + assert_eq!(definition_blocks(Machine { regions: &[] }, RSDP_AT).err(), Some(TableError::BadRsdp)); +} + +/// A machine whose reader reaches its RSDP, its XSDT and one table that is +/// no definition block, and none of the memory its FADT, DSDT and SSDTs are +/// in. The DSDT is refused as the first entry that could not be read, which +/// may be the FADT: `Absent` there would say the firmware names no DSDT, +/// where what happened is that its tables' memory was not read. Each other +/// unread entry follows in its place, and the table that was read and is no +/// SSDT is passed over. +#[test] +fn a_dsdt_whose_fadt_could_not_be_read_is_refused_as_unread_and_not_as_absent() { + let at = |n: u64| TABLE_AT + n * 0x1000; + let head = rsdp(XSDT_AT, 2, 36); + let hpet = sdt(b"HPET", 1, &[0u8; 20]); + let root = xsdt(&[at(0), at(1), at(2), 0, at(3)]); + let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (at(2), &hpet)]; + let blocks: Vec> = + definition_blocks(Machine { regions }, RSDP_AT).expect("the XSDT").map(|block| block.err()).collect(); + let unread = |n| Some(TableError::Unmapped { at: at(n), len: 36 }); + assert_eq!(blocks, [unread(0), unread(0), unread(1), unread(3)]); + + // Every entry read, and none a FADT: that is `Absent`. + let root = xsdt(&[at(2), 0]); + let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (at(2), &hpet)]; + let blocks: Vec> = + definition_blocks(Machine { regions }, RSDP_AT).expect("the XSDT").map(|block| block.err()).collect(); + assert_eq!(blocks, [Some(TableError::Absent)]); + + // A FADT that is read wins over an entry before it that was not. + let fadt = facp_naming(at(4) as u32); + let dsdt = sdt(b"DSDT", 2, &[1]); + let root = xsdt(&[at(0), at(1)]); + let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (at(1), &fadt), (at(4), &dsdt)]; + let blocks: Vec> = + definition_blocks(Machine { regions }, RSDP_AT).expect("the XSDT").map(|block| block.map(|table| table.base())).collect(); + assert_eq!(blocks, [Ok(at(4)), Err(TableError::Unmapped { at: at(0), len: 36 })]); +} + /// **Stated as a test, so extending the decoder reds the statement.** The XSDT /// walk deliberately returns the first signature match's verdict rather than /// trying a second table of the same name. diff --git a/toyos-acpi/tests/facs.rs b/toyos-acpi/tests/facs.rs new file mode 100644 index 00000000000..5b59db36e31 --- /dev/null +++ b/toyos-acpi/tests/facs.rs @@ -0,0 +1,109 @@ +//! The FACS the FADT names, and the Global Lock's two transitions held to +//! ACPI 6.5 §5.2.10.1's own instruction sequences, executed step by step. + +mod common; + +use common::{reseal, Machine}; +use toyos_acpi::{acquire, facs, release, Facs, FacsRefused, Table, OWNED, PENDING, SDT_HEADER_LEN}; + +const FADT_AT: u64 = 0x7fb7_9000; +const QEMU_FADT: &[u8] = include_bytes!("../fixtures/qemu-11.1.1/facp.bin"); +/// Table 5.9: `FIRMWARE_CTRL` and `X_FIRMWARE_CTRL`. +const FIRMWARE_CTRL: usize = 36; +const X_FIRMWARE_CTRL: usize = 132; + +fn fadt_naming(narrow: u32, wide: u64) -> Vec { + let mut fadt = QEMU_FADT.to_vec(); + fadt[FIRMWARE_CTRL..FIRMWARE_CTRL + 4].copy_from_slice(&narrow.to_le_bytes()); + fadt[X_FIRMWARE_CTRL..X_FIRMWARE_CTRL + 8].copy_from_slice(&wide.to_le_bytes()); + reseal(&mut fadt); + fadt +} + +fn a_facs(len: u32) -> Vec { + let mut bytes = vec![0u8; 64]; + bytes[..4].copy_from_slice(b"FACS"); + bytes[4..8].copy_from_slice(&len.to_le_bytes()); + bytes +} + +fn decoded(fadt: &[u8], at: u64, structure: &[u8]) -> Result { + let regions = [(FADT_AT, fadt), (at, structure)]; + let m = Machine { regions: ®ions }; + let fadt = Table::open(m, FADT_AT, b"FACP", SDT_HEADER_LEN).expect("the crafted FADT"); + facs(m, &fadt) +} + +#[test] +fn the_wide_address_wins_where_it_is_not_zero_and_the_narrow_one_serves_where_it_is() { + assert!(QEMU_FADT[8] >= 2 && QEMU_FADT.len() >= X_FIRMWARE_CTRL + 8, "the fixture is a revision with X_FIRMWARE_CTRL"); + let structure = a_facs(64); + assert_eq!(decoded(&fadt_naming(0x1000, 0x2_0000_0040), 0x2_0000_0040, &structure), Ok(Facs { base: 0x2_0000_0040, len: 64 })); + assert_eq!(decoded(&fadt_naming(0x1000, 0), 0x1000, &structure), Ok(Facs { base: 0x1000, len: 64 })); + // The narrow address is not read where the wide one names another place. + assert_eq!(decoded(&fadt_naming(0x1000, 0x2_0000_0040), 0x1000, &structure), Err(FacsRefused::Unmapped(0x2_0000_0040))); + assert_eq!(decoded(&fadt_naming(0, 0), 0x1000, &structure), Err(FacsRefused::Absent)); +} + +#[test] +fn a_structure_that_is_no_facs_is_refused_by_name() { + let fadt = fadt_naming(0x1000, 0); + let mut unsigned = a_facs(64); + unsigned[..4].copy_from_slice(b"FACP"); + assert_eq!(decoded(&fadt, 0x1000, &unsigned), Err(FacsRefused::Signature)); + assert_eq!(decoded(&fadt, 0x1000, &a_facs(63)), Err(FacsRefused::Length(63))); + assert_eq!(decoded(&fadt, 0x1000, &a_facs(u32::MAX)), Err(FacsRefused::Length(u32::MAX))); + assert_eq!(decoded(&fadt, 0x1000, &a_facs(64)[..63]), Err(FacsRefused::Unmapped(0x1000))); +} + +/// §5.2.10 aligns the FACS on a 64-byte boundary: every base that is not on +/// one is refused, by either address field, and the boundary itself is not. +#[test] +fn a_facs_off_its_sixty_four_byte_boundary_is_refused() { + for off in [1u32, 2, 4, 8, 16, 32, 60, 63] { + let at = 0x1000 + off; + assert_eq!(decoded(&fadt_naming(at, 0), u64::from(at), &a_facs(64)), Err(FacsRefused::Misaligned(u64::from(at))), "narrow, {off} off"); + let wide = 0x2_0000_0000 + u64::from(off); + assert_eq!(decoded(&fadt_naming(0x1000, wide), wide, &a_facs(64)), Err(FacsRefused::Misaligned(wide)), "wide, {off} off"); + } + for at in [0x40u32, 0x1000, 0x1040, 0xFFFF_FFC0] { + assert_eq!(decoded(&fadt_naming(at, 0), u64::from(at), &a_facs(64)), Ok(Facs { base: u64::from(at), len: 64 }), "{at:#x}"); + } +} + +/// `AcquireGlobalLock`, an instruction a line: `and edx, not 1`, `bts edx, 1`, +/// `adc edx, 0`, then `cmp dl, 3` and `sbb eax, eax`. +fn spec_acquire(word: u32) -> (u32, bool) { + let mut edx = word & !1; + let carry = edx >> 1 & 1; + edx |= 1 << 1; + edx = edx.wrapping_add(carry); + let below = (edx as u8) < 3; + (edx, below) +} + +/// `ReleaseGlobalLock`: `and edx, not 03h`, then `and eax, 1` of the word read. +fn spec_release(word: u32) -> (u32, bool) { + (word & !3, word & 1 != 0) +} + +#[test] +fn both_transitions_are_the_specifications_own_sequences() { + // Every state of the two bits, under reserved bits clear, set and mixed + // above the low byte: `cmp dl, 3` reads that byte whole, so the sequence + // itself answers for a word whose bits 2 to 7 are zero, as Table 5.16 + // reserves them. + for reserved in [0u32, !0xFF, 0xA5A5_A500] { + for state in 0..4u32 { + let word = reserved | state; + assert_eq!(acquire(word), spec_acquire(word), "acquire from {word:#x}"); + assert_eq!(release(word), spec_release(word), "release from {word:#x}"); + } + } + assert_eq!(acquire(0), (OWNED, true)); + assert_eq!(acquire(OWNED), (OWNED | PENDING, false), "a lock the firmware owns is marked pending, not taken"); + assert_eq!(acquire(OWNED | PENDING), (OWNED | PENDING, false)); + assert_eq!(acquire(PENDING), (OWNED, true), "a stale pending bit is cleared by whoever takes a free lock"); + assert_eq!(release(OWNED), (0, false)); + assert_eq!(release(OWNED | PENDING), (0, true), "the firmware asked while it was held, and is owed the signal"); +} diff --git a/toyos-acpi/tests/fixtures.rs b/toyos-acpi/tests/fixtures.rs index 043a9177c33..f2ca1855149 100644 --- a/toyos-acpi/tests/fixtures.rs +++ b/toyos-acpi/tests/fixtures.rs @@ -9,7 +9,7 @@ use common::{t14_root_bridge, Machine, OVMF_ROOT_BRIDGE}; use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ - century_of, dsdt_address, ecam_base, find_table, fixed_hardware, hpet_base, iapc_boot_arch, + century_of, definition_blocks, dsdt_address, ecam_base, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, s5_slp_typ, sci_line, Century, FixedHardware, IoApicEntry, LegacyMode, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, SourceOverride, Table, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, @@ -113,6 +113,24 @@ fn the_fadt_names_the_power_block_and_the_dsdt() { assert_eq!(dsdt_address(&fadt), 0x7fb7_a000); } +/// QEMU publishes one definition block, the DSDT its FADT names, and no SSDT: +/// the walk answers it whole and ends. The DSDT is that of a later boot of +/// the same QEMU, the same 8500 bytes its `SOURCE` says this boot named here. +#[test] +fn the_definition_blocks_of_qemu_are_its_dsdt_alone() { + const DSDT: &[u8] = include_bytes!("../fixtures/qemu-11.1.1/dsdt.bin"); + let mut regions = REGIONS.to_vec(); + regions.push((0x7fb7_a000, DSDT)); + let blocks: Vec<_> = definition_blocks(Machine { regions: ®ions }, RSDP).expect("the XSDT").collect(); + let [Ok(dsdt)] = blocks.as_slice() else { panic!("{} definition blocks, or a refused one", blocks.len()) }; + assert_eq!((dsdt.base(), dsdt.len()), (0x7fb7_a000, 8500)); + assert_eq!((0..4).map(|i| dsdt.byte(i).unwrap()).collect::>(), b"DSDT"); + + // The boot these tables are of kept its DSDT where this machine holds nothing. + let blocks: Vec<_> = definition_blocks(machine(), RSDP).expect("the XSDT").map(|block| block.err()).collect(); + assert_eq!(blocks, [Some(TableError::Unmapped { at: 0x7fb7_a000, len: 36 })]); +} + /// `ACPI: PM1a=0x604 SLP_TYPa=0`, off the DSDT of the boot that logged it /// (`fixtures/qemu-11.1.1/SOURCE`): `\_S5_`'s package, found past `\_S4_`'s, /// whose first element is 2. diff --git a/toyos-userbound/Cargo.toml b/toyos-userbound/Cargo.toml index c067012f3fb..dc24dd17a49 100644 --- a/toyos-userbound/Cargo.toml +++ b/toyos-userbound/Cargo.toml @@ -16,3 +16,10 @@ description = "Every decision the kernel makes about the user/kernel boundary, p version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" + +[dependencies] +# The `acpi` claim's access and refusal words, and firmware's memory map +# entry: one declaration each, read by the policy and by the kernel. +toyos-abi = { path = "../toyos-abi" } +# Which memory types the kernel hands out as RAM. +toyos-bootmap = { path = "../toyos-bootmap" } diff --git a/toyos-userbound/src/firmware.rs b/toyos-userbound/src/firmware.rs new file mode 100644 index 00000000000..67ed7691d7f --- /dev/null +++ b/toyos-userbound/src/firmware.rs @@ -0,0 +1,397 @@ +//! What the kernel reads and writes for the holder of the `acpi` claim, whose +//! interpreter runs the firmware's AML: one access at a time, to memory, a +//! port or a function's configuration space, each decided here by what the +//! address is. +//! +//! **An access is made only through a witness this module answered** +//! ([`MemoryAt`], [`PortAt`], [`ConfigAt`], and [`LockWordAt`] for the +//! compare-and-exchange on the FACS): none has another constructor, so a +//! refusal cannot reach an accessor. +//! +//! **Memory is the firmware's or it is refused.** The UEFI memory map types +//! every range (UEFI 2.10 §7.2, `EFI_MEMORY_TYPE`): what the kernel hands out +//! as RAM is refused whole, ACPI NVS and reserved memory pass both ways, ACPI +//! reclaim memory, where the tables are, is read and never written, and every +//! other type, and an address the map does not list, is refused with its +//! type, for whoever reads the holder's log to rule on. +//! +//! **Runtime-services data is read and never written, as the tables' +//! memory is, because on some machines it is.** UEFI 2.10 §2.3.4 has "ACPI +//! Tables loaded at boot time ... contained in memory of type +//! EfiACPIReclaimMemory (recommended) or EfiACPIMemoryNVS", and a firmware +//! that keeps every table its XSDT lists in `EfiRuntimeServicesData` exists +//! all the same; the kernel's own reader reads them there. No memory the +//! kernel hands out carries the type (`toyos_bootmap::is_usable_type`). What +//! the holder reads there beside the tables is whatever else that firmware +//! keeps in it. Runtime-services code is refused both ways. +//! +//! **An address the map does not list is read where it is a register, and +//! never written.** A chipset keeps registers at addresses its firmware lists +//! nowhere, and a machine's AML reads them as it loads. Such a read passes at +//! or above [`FIXED_RANGE_END`] where the kernel maps the address and the +//! processor's range registers type it uncacheable ([`Memory::uncached`]): +//! that is what makes a read of a register one read of it, on a machine +//! whose CPUs all read it under those registers; where some CPU's are on +//! and are not those ([`Memory::registers_differ`]) no such read passes. +//! It is not what +//! keeps RAM out, since the range registers are not the effective type +//! everywhere: the allocator hands out only memory the map lists as usable +//! ([`toyos_bootmap::is_usable_type`]), so memory the map does not list holds +//! nothing ToyOS put there. The read may have an effect in the device that +//! nothing here knows of. Inside that, every page a device the kernel knows +//! of decodes in is refused, whatever firmware types it and whoever drives +//! the device, and an address in the ECAM window is a configuration access +//! and is decided as one. +//! +//! **What the allocator hands out is refused wherever the map lists it.** A +//! map's ranges may overlap, and the allocator takes every usable one: an +//! access is refused where any usable range holds a byte of it, whatever +//! another range types the same byte. +//! +//! **A port is the kernel's to answer where the kernel declared it** +//! ([`crate::port::Mediated`]), another claim's where a row names it, and +//! passes otherwise. +//! +//! **Configuration space is read and never written.** + +use toyos_abi::acpi::{Refused, Width, UNLISTED}; +use toyos_abi::boot::MemoryMapEntry; + +use crate::port::{Mediated, IO_PORTS}; +use crate::span::PAGE_4K; + +/// `EfiReservedMemoryType`, `EfiRuntimeServicesData`, `EfiACPIReclaimMemory` +/// and `EfiACPIMemoryNVS`. +const EFI_RESERVED: u32 = 0; +const EFI_RUNTIME_DATA: u32 = 6; +const EFI_ACPI_RECLAIM: u32 = 9; +const EFI_ACPI_NVS: u32 = 10; + +/// The local APIC's registers and the window every interrupt message is +/// addressed to (Intel SDM Vol. 3A §11.4.1 and §11.11.1): the kernel's whether +/// or not it maps them. +const LOCAL_APIC: (u64, u64) = (0xFEE0_0000, 0xFEF0_0000); + +/// One past what the processor's fixed range registers type (Intel SDM +/// Vol. 3A, "Fixed Range MTRRs"). Below it they decide whether a read is +/// cached and the kernel reads none of them, so no address the map does not +/// list is a register's there. +pub const FIXED_RANGE_END: u64 = 0x10_0000; + +/// Bytes of configuration space a function has. +const CONFIG_BYTES: u16 = 0x1000; + +/// The window configuration space is reached through, as the MCFG names it +/// (PCI Firmware Specification 3.3, Table 4-3): `base` is bus 0's, whatever +/// the first bus the window decodes. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Ecam { + pub base: u64, + pub segment: u16, + pub first_bus: u8, + pub last_bus: u8, +} + +impl Ecam { + fn holds(&self, at: u64) -> bool { + // Saturating: the base is firmware's word. + let start = self.base.saturating_add(u64::from(self.first_bus) << 20); + let end = self.base.saturating_add((u64::from(self.last_bus) + 1) << 20); + (start..end).contains(&at) + } +} + +/// One PCI function on the segment group the kernel enumerated. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Function { + pub bus: u8, + pub device: u8, + pub function: u8, +} + +/// What the kernel knows of the machine's memory. `D` yields every range a +/// device decodes that the kernel keeps a record of, as `(start, end)`: each +/// window it mapped to drive one, and each memory BAR of each function. +#[derive(Clone)] +pub struct Memory<'a, D> { + /// Firmware's map, as the loader handed it over. + pub map: &'a [MemoryMapEntry], + /// One past the last byte the kernel maps. + pub mapped_end: u64, + pub ecam: Option, + pub devices: D, + /// The FACS, as `(start, end)`. + pub facs: Option<(u64, u64)>, + /// Whether the processor reads `len` bytes at an address uncached, + /// whatever maps them: asked only of an address the map does not list, at + /// or above [`FIXED_RANGE_END`]. + pub uncached: fn(u64, u64) -> bool, + /// Some CPU's range registers are on and are not the ones `uncached` + /// answers from: what it answers is not known of a read that CPU makes. + pub registers_differ: bool, +} + +/// A memory access the policy passed. +/// +/// ```compile_fail,E0451 +/// let _ = toyos_userbound::firmware::MemoryAt { at: 0x1000, width: toyos_abi::acpi::Width::Byte }; +/// ``` +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct MemoryAt { + at: u64, + width: Width, +} + +impl MemoryAt { + pub const fn at(&self) -> u64 { + self.at + } + + pub const fn width(&self) -> Width { + self.width + } +} + +/// What a memory access is. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum MemoryVerdict { + Through(MemoryAt), + /// In the ECAM window: this function's configuration space at this + /// offset, still to be decided by [`config`]. + AsConfig(Function, u16), + Refused(Refused), +} + +fn overlaps(first: u64, last: u64, (start, end): (u64, u64)) -> bool { + first < end && start <= last +} + +/// `range` grown to the pages it lies in. A device decodes whole pages at +/// least, whatever span of them its driver asked to be mapped: an I/O APIC is +/// driven through its first 0x20 bytes, and a chipset's keeps an EOI register +/// further into the same page. +fn pages((start, end): (u64, u64)) -> (u64, u64) { + (start & !(PAGE_4K - 1), end.saturating_add(PAGE_4K - 1) & !(PAGE_4K - 1)) +} + +/// The UEFI type of the range of `map` holding `at`, or `None` where it lists +/// none. +fn type_of(map: &[MemoryMapEntry], at: u64) -> Option { + map.iter().find(|entry| (entry.start..entry.end).contains(&at)).map(|entry| entry.uefi_type) +} + +/// [`type_of`] as [`toyos_abi::acpi::Access::memory_type`] carries it. +pub fn type_word(map: &[MemoryMapEntry], at: u64) -> u8 { + type_of(map, at).and_then(|ty| u8::try_from(ty).ok()).filter(|&ty| ty != UNLISTED).unwrap_or(UNLISTED) +} + +/// The type of a range of `map` the allocator hands out that holds a byte of +/// `first..=last`, whichever range [`type_of`] finds first there. +fn usable(map: &[MemoryMapEntry], first: u64, last: u64) -> Option { + map.iter() + .find(|entry| toyos_bootmap::is_usable_type(entry.uefi_type) && overlaps(first, last, (entry.start, entry.end))) + .map(|entry| entry.uefi_type) +} + +impl> Memory<'_, D> { + pub fn decide(self, at: u64, width: Width, write: bool) -> MemoryVerdict { + use MemoryVerdict::Refused as No; + let Some(last) = at.checked_add(width.bytes() - 1) else { return No(Refused::Unmapped) }; + if let Some(ecam) = self.ecam { + match (ecam.holds(at), ecam.holds(last)) { + (true, true) => { + let offset = at - ecam.base; + let function = + Function { bus: (offset >> 20) as u8, device: (offset >> 15 & 0x1F) as u8, function: (offset >> 12 & 7) as u8 }; + return MemoryVerdict::AsConfig(function, (offset & 0xFFF) as u16); + } + (false, false) => {} + _ => return No(Refused::Straddles), + } + } + if overlaps(at, last, LOCAL_APIC) || self.devices.into_iter().any(|device| overlaps(at, last, pages(device))) { + return No(Refused::DeviceMemory); + } + let ty = type_of(self.map, at); + if type_of(self.map, last) != ty { + return No(Refused::Straddles); + } + if usable(self.map, at, last).is_some() { + return No(Refused::UsableMemory); + } + match ty { + Some(EFI_ACPI_RECLAIM | EFI_RUNTIME_DATA) if write => return No(Refused::TableWrite), + Some(EFI_RESERVED | EFI_ACPI_NVS | EFI_ACPI_RECLAIM | EFI_RUNTIME_DATA) => {} + None if !write => {} + Some(_) | None => return No(Refused::MemoryType), + } + if last >= self.mapped_end { + return No(Refused::Unmapped); + } + if ty.is_none() && self.registers_differ { + return No(Refused::RangeRegistersDiffer); + } + if ty.is_none() && (at < FIXED_RANGE_END || !(self.uncached)(at, width.bytes())) { + return No(Refused::UnlistedCached); + } + if write && self.facs.is_some_and(|facs| overlaps(at, last, facs)) { + return No(Refused::FacsWrite); + } + MemoryVerdict::Through(MemoryAt { at, width }) + } +} + +/// The Global Lock's word, where the kernel may exchange it. +/// +/// ```compile_fail,E0451 +/// let _ = toyos_userbound::firmware::LockWordAt { at: 0x1000 }; +/// ``` +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct LockWordAt { + at: u64, +} + +impl LockWordAt { + pub const fn at(&self) -> u64 { + self.at + } +} + +/// Why the lock word a FACS names is none the kernel exchanges. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum NoLockWord { + /// A dword not on a dword boundary is no operand of an atomic exchange. + Misaligned, + /// One of its four bytes is in memory of this type, or `None` in memory + /// firmware's map does not list: not firmware's own to keep a lock in, so + /// an exchange there would write RAM, the tables or a device. + Type(Option), + /// Past the end of what the kernel maps. + Unmapped, +} + +/// Decide whether the dword at `at` is one the kernel exchanges as the Global +/// Lock: all four bytes in memory firmware keeps as its own and a holder may +/// write, ACPI NVS or reserved, and inside what the kernel maps. Each byte is +/// typed, not the first of the structure: a FACS at the end of a firmware +/// range puts the word in whatever follows. +pub fn lock_word(map: &[MemoryMapEntry], mapped_end: u64, at: u64) -> Result { + if !at.is_multiple_of(4) { + return Err(NoLockWord::Misaligned); + } + // No overflow: `at` is on a dword boundary. + let last = at + 3; + if let Some(handed_out) = usable(map, at, last) { + return Err(NoLockWord::Type(Some(handed_out))); + } + for byte in at..=last { + match type_of(map, byte) { + Some(EFI_RESERVED | EFI_ACPI_NVS) => {} + other => return Err(NoLockWord::Type(other)), + } + } + if last >= mapped_end { + return Err(NoLockWord::Unmapped); + } + Ok(LockWordAt { at }) +} + +/// What the kernel says of one port. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Standing { + /// Nothing declared it and no row names it. + Free, + /// The kernel declared it, with this answer. + Declared(Mediated), + /// A row another claim is for names it. + Row, +} + +/// A port access the policy passed. +/// +/// ```compile_fail,E0451 +/// let _ = toyos_userbound::firmware::PortAt { port: 0x3F8, width: toyos_abi::acpi::Width::Byte }; +/// ``` +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct PortAt { + port: u16, + width: Width, +} + +impl PortAt { + pub const fn port(&self) -> u16 { + self.port + } + + pub const fn width(&self) -> Width { + self.width + } +} + +/// Decide an access of `width` at `port`: every port it spans is asked of +/// `standing`. +pub fn port(standing: impl Fn(u16) -> Standing, port: u16, width: Width, write: bool) -> Result { + if width == Width::QWord || port as usize + width.bytes() as usize > IO_PORTS { + return Err(Refused::PortSpan); + } + for port in port..=port + (width.bytes() as u16 - 1) { + match standing(port) { + Standing::Free | Standing::Declared(Mediated::Open) => {} + Standing::Declared(Mediated::ReadOnly) if !write => {} + Standing::Declared(Mediated::ReadOnly) => return Err(Refused::ReadOnlyPort), + Standing::Declared(Mediated::Kept) => return Err(Refused::KernelPort), + Standing::Row => return Err(Refused::ClaimedPort), + } + } + Ok(PortAt { port, width }) +} + +/// A configuration read the policy passed. +/// +/// ```compile_fail,E0451 +/// fn forged(function: toyos_userbound::firmware::Function) { +/// let _ = toyos_userbound::firmware::ConfigAt { function, offset: 0, width: toyos_abi::acpi::Width::Byte }; +/// } +/// ``` +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct ConfigAt { + function: Function, + offset: u16, + width: Width, +} + +impl ConfigAt { + pub const fn function(&self) -> Function { + self.function + } + + pub const fn offset(&self) -> u16 { + self.offset + } + + pub const fn width(&self) -> Width { + self.width + } +} + +/// Decide a configuration access. A write is refused, whatever it names: a +/// function's configuration space holds what the kernel reads its own +/// configuration from and what moves the ports it declared. A read is held to +/// its shape: a function the window reaches, and at most a dword that crosses +/// no dword boundary, the unit a configuration register is defined in. +pub fn config(ecam: Option, segment: u16, function: Function, offset: u16, width: Width, write: bool) -> Result { + if write { + return Err(Refused::ConfigWrite); + } + let reached = ecam.is_some_and(|ecam| { + ecam.segment == segment && (ecam.first_bus..=ecam.last_bus).contains(&function.bus) + }); + if !reached || function.device > 31 || function.function > 7 { + return Err(Refused::ConfigUnreachable); + } + let bytes = width.bytes() as u16; + if width == Width::QWord || offset % 4 + bytes > 4 || offset >= CONFIG_BYTES { + return Err(Refused::ConfigSpan); + } + Ok(ConfigAt { function, offset, width }) +} diff --git a/toyos-userbound/src/lib.rs b/toyos-userbound/src/lib.rs index bf63df0e219..180642a32f4 100644 --- a/toyos-userbound/src/lib.rs +++ b/toyos-userbound/src/lib.rs @@ -8,14 +8,16 @@ //! asked for be placed at all, and where does it go? **After a trap**: which //! side did the frame come from? **Before an `in` or `out`**: which ports does //! this CPU open to the process running on it, and which may no grant reach? +//! **Before an access the kernel makes for the `acpi` claim's holder**: is the +//! address the firmware's to have touched? //! //! [`span`] answers the first, [`segment`] the second, [`place`] the third, -//! [`fault`] the fourth and [`port`] the fifth. +//! [`fault`] the fourth, [`port`] the fifth and [`firmware`] the sixth. //! -//! Pure. No I/O, no allocation, no `unsafe`, nothing read from a device and -//! nothing named outside this crate. The kernel is the only caller — +//! Pure. No I/O, no allocation, no `unsafe`, nothing read from a device. The +//! kernel is the only caller — //! `user_ptr.rs`, `mm/`, `syscall/`, `loader/`, `arch/x86_64/percpu.rs`, -//! `arch/x86_64/pio.rs` and `arch/x86_64/idt/exceptions.rs` — and this is a +//! `arch/x86_64/pio.rs`, `arch/x86_64/acpi_mode.rs` and `arch/x86_64/idt/exceptions.rs` — and this is a //! crate rather than files inside it so that the boundary table below runs on //! the host in milliseconds instead of in a boot. //! @@ -28,6 +30,7 @@ #![forbid(unsafe_code)] pub mod fault; +pub mod firmware; pub mod place; pub mod port; pub mod segment; @@ -35,7 +38,7 @@ pub mod span; pub use fault::Ring; pub use place::{PageSpan, Window}; -pub use port::{port_access, IoBitmap, PortAccess, Ports, Reserved, Undeclared, IO_PORTS}; +pub use port::{port_access, IoBitmap, Mediated, PortAccess, Ports, Reserved, Undeclared, IO_PORTS}; pub use segment::{pieces, segments, Pinned, Pins, Segment}; pub use span::{ align_2m_checked, in_user_half, is_user_addr, is_user_object, rebase_base, Access, PAGE_2M, diff --git a/toyos-userbound/src/port.rs b/toyos-userbound/src/port.rs index abb64a5383e..cd59b17f466 100644 --- a/toyos-userbound/src/port.rs +++ b/toyos-userbound/src/port.rs @@ -108,11 +108,24 @@ impl IoBitmap { } } +/// What a declared run answers the `acpi` claim's holder, whose AML may name +/// any port ([`crate::firmware::port`]). Every declaration says, so none +/// passes by default. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Mediated { + /// Refused both ways. + Kept, + /// Read for the holder, never written. + ReadOnly, + /// Read and written for the holder. + Open, +} + /// The ports no grant reaches, each run named by what holds it: the one /// declaration of every port the kernel drives, read by whatever decides a /// grant. pub struct Reserved { - runs: [Option<(&'static str, Ports)>; N], + runs: [Option<(&'static str, Ports, Mediated)>; N], } /// Why a run was not declared. @@ -136,18 +149,23 @@ impl Reserved { } /// Reserve `ports` for `holder`, refused where another holder has one of them. - pub fn declare(&mut self, holder: &'static str, ports: Ports) -> Result<(), Undeclared> { + pub fn declare(&mut self, holder: &'static str, ports: Ports, mediated: Mediated) -> Result<(), Undeclared> { if let Some(first) = self.holder(ports) { return Err(Undeclared::Clash(first)); } let slot = self.runs.iter_mut().find(|slot| slot.is_none()).ok_or(Undeclared::Full)?; - *slot = Some((holder, ports)); + *slot = Some((holder, ports, mediated)); Ok(()) } /// Who holds a port of `ports`, if anyone does. pub fn holder(&self, ports: Ports) -> Option<&'static str> { - self.runs.iter().flatten().find(|(_, held)| held.overlaps(ports)).map(|&(name, _)| name) + self.runs.iter().flatten().find(|(_, held, _)| held.overlaps(ports)).map(|&(name, ..)| name) + } + + /// What the run holding `port` answers a mediated access, if one does. + pub fn mediated(&self, port: u16) -> Option { + self.runs.iter().flatten().find(|(_, held, _)| held.overlaps(Ports::one(port))).map(|&(.., mediated)| mediated) } } @@ -313,8 +331,11 @@ mod tests { #[test] fn a_declared_run_refuses_every_run_that_shares_a_port_with_it() { let mut reserved = Reserved::<4>::new(); - reserved.declare("the PM1a control block", run(0x1804, 2)).expect("the first run"); - reserved.declare("SMI_CMD", Ports::one(0xB2)).expect("a disjoint run"); + reserved.declare("the PM1a control block", run(0x1804, 2), Mediated::ReadOnly).expect("the first run"); + reserved.declare("SMI_CMD", Ports::one(0xB2), Mediated::Kept).expect("a disjoint run"); + assert_eq!(reserved.mediated(0x1805), Some(Mediated::ReadOnly)); + assert_eq!(reserved.mediated(0xB2), Some(Mediated::Kept)); + assert_eq!(reserved.mediated(0x1806), None); // Either end of the block, a run that covers it, and a run beside it. assert_eq!(reserved.holder(Ports::one(0x1804)), Some("the PM1a control block")); assert_eq!(reserved.holder(Ports::one(0x1805)), Some("the PM1a control block")); @@ -323,14 +344,14 @@ mod tests { assert_eq!(reserved.holder(Ports::one(0x1806)), None); assert_eq!(reserved.holder(run(0xB0, 3)), Some("SMI_CMD")); // A second holder of a declared port is refused naming the first. - assert_eq!(reserved.declare("the GPE0 block", run(0x1805, 1)), Err(Undeclared::Clash("the PM1a control block"))); + assert_eq!(reserved.declare("the GPE0 block", run(0x1805, 1), Mediated::Open), Err(Undeclared::Clash("the PM1a control block"))); assert_eq!(reserved.holder(run(0x1806, 0x10)), None, "a refused declaration reserved nothing"); } #[test] fn a_full_declaration_refuses_one_more_run() { let mut reserved = Reserved::<1>::new(); - reserved.declare("COM1", run(0x3F8, 8)).expect("the one slot"); - assert_eq!(reserved.declare("the RTC", run(0x70, 2)), Err(Undeclared::Full)); + reserved.declare("COM1", run(0x3F8, 8), Mediated::Kept).expect("the one slot"); + assert_eq!(reserved.declare("the RTC", run(0x70, 2), Mediated::Kept), Err(Undeclared::Full)); } } diff --git a/toyos-userbound/tests/firmware.rs b/toyos-userbound/tests/firmware.rs new file mode 100644 index 00000000000..fd2f37893e1 --- /dev/null +++ b/toyos-userbound/tests/firmware.rs @@ -0,0 +1,574 @@ +//! The `acpi` claim's mediated access, row by row: what passes, what each +//! refusal is called, and that the edges of every range fall on the right +//! side. + +use toyos_abi::acpi::{Refused, Width}; +use toyos_abi::boot::MemoryMapEntry; +use toyos_userbound::firmware::{config, lock_word, port, type_word, Ecam, Function, Memory, MemoryVerdict, NoLockWord, Standing, FIXED_RANGE_END}; +use toyos_userbound::Mediated; + +const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { + MemoryMapEntry { uefi_type, start, end } +} + +const GIB: u64 = 1 << 30; + +/// Descriptors of the map QEMU 11.1.1's edk2 hands a 2 GiB q35 guest +/// (`toyos-bootmap/tests/direct_map.rs` holds it whole), with an ACPI reclaim +/// range where that boot's tables were: RAM, ACPI NVS beside RAM, the ECAM +/// window typed reserved, and a reserved range above everything mapped. +const Q35: [MemoryMapEntry; 9] = [ + e(7, 0x0, 0x87000), + e(4, 0x87000, 0x88000), + e(7, 0x100000, 0x800000), + e(10, 0x800000, 0x808000), + e(7, 0x808000, 0x80b000), + e(9, 0x7fb74000, 0x7fb7f000), + e(10, 0x7ff60000, 0x80000000), + e(0, 0xe0000000, 0xf0000000), + e(0, 0xfd00000000, 0x10000000000), +]; +const Q35_ECAM: Ecam = Ecam { base: 0xe000_0000, segment: 0, first_bus: 0, last_bus: 0xFF }; +/// The I/O APIC and the HPET, as the kernel maps them on q35. +const Q35_DRIVEN: &[(u64, u64)] = &[(0xfec0_0000, 0xfec0_0020), (0xfed0_0000, 0xfed0_1000)]; +const Q35_FACS: (u64, u64) = (0x7ff7_7000, 0x7ff7_7040); + +/// The ranges devices decode, as a test lists them. +type Devices = std::iter::Copied>; +type Mem = Memory<'static, Devices>; + +fn devices(decoded: &'static [(u64, u64)]) -> Devices { + decoded.iter().copied() +} + +/// A machine of one map and nothing else: no ECAM window, no FACS. +fn bare(map: &'static [MemoryMapEntry], decoded: &'static [(u64, u64)]) -> Mem { + Memory { map, mapped_end: 4 * GIB, ecam: None, devices: devices(decoded), facs: None, uncached: registers, registers_differ: false } +} + +/// What the range registers type uncacheable on these machines: the hole +/// under 4 GiB from [`REGISTERS`] up, where a chipset keeps its registers. +const REGISTERS: u64 = 0xfe00_0000; + +fn registers(at: u64, len: u64) -> bool { + at >= REGISTERS && at + len <= 4 * GIB +} + +fn q35() -> Mem { + Memory { map: &Q35, mapped_end: 4 * GIB, ecam: Some(Q35_ECAM), devices: devices(Q35_DRIVEN), facs: Some(Q35_FACS), uncached: registers, registers_differ: false } +} + +/// A map shaped as a laptop's is, at addresses of this test's own: RAM, a +/// reserved range, ACPI NVS, ACPI reclaim, a page of RAM after them, runtime +/// services data, a memory-mapped I/O range, and the ECAM window typed as +/// memory-mapped I/O. +const LAPTOP: [MemoryMapEntry; 9] = [ + e(7, 0x0, 0x9f000), + e(0, 0x9f000, 0x100000), + e(7, 0x100000, 0x7000_0000), + e(0, 0x7000_0000, 0x7400_0000), + e(10, 0x7400_0000, 0x7480_0000), + e(9, 0x7480_0000, 0x7490_0000), + e(7, 0x7490_0000, 0x7490_1000), + e(6, 0x7490_1000, 0x74a0_0000), + e(11, 0xc000_0000, 0xd000_0000), +]; + +fn laptop() -> Mem { + let ecam = Ecam { base: 0xc000_0000, segment: 0, first_bus: 0, last_bus: 0xFF }; + Memory { map: &LAPTOP, mapped_end: 4 * GIB, ecam: Some(ecam), devices: devices(&[]), facs: Some((0x7400_0040, 0x7400_0080)), uncached: registers, registers_differ: false } +} + +fn passes(memory: &Mem, at: u64, width: Width, write: bool) -> bool { + match memory.clone().decide(at, width, write) { + MemoryVerdict::Through(witness) => { + assert_eq!((witness.at(), witness.width()), (at, width), "the witness names another access"); + true + } + _ => false, + } +} + +fn refused(memory: &Mem, at: u64, width: Width, write: bool) -> Refused { + match memory.clone().decide(at, width, write) { + MemoryVerdict::Refused(why) => why, + other => panic!("{at:#x} {width:?} write={write} was not refused: {other:?}"), + } +} + +#[test] +fn ram_is_refused_both_ways_whatever_usable_type_it_is() { + let q35 = q35(); + for write in [false, true] { + for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { + // Conventional memory, boot services data, and the last byte of each. + for at in [0x0, 0x100000, 0x87000, 0x808000, 0x800000 - width.bytes()] { + assert_eq!(refused(&q35, at, width, write), Refused::UsableMemory, "{at:#x} {width:?}"); + } + } + } + assert_eq!(type_word(&Q35, 0x100000), 7); + assert_eq!(type_word(&Q35, 0x87000), 4); +} + +#[test] +fn nvs_and_reserved_memory_pass_both_ways_to_their_last_byte_and_no_further() { + let (q35, laptop) = (q35(), laptop()); + for write in [false, true] { + assert!(passes(&q35, 0x800000, Width::QWord, write)); + assert!(passes(&q35, 0x808000 - 8, Width::QWord, write)); + assert!(passes(&q35, 0x7ff60000, Width::Byte, write)); + assert!(passes(&laptop, 0x9f000, Width::DWord, write), "reserved memory below 1 MiB"); + assert!(passes(&laptop, 0x7000_0000, Width::Word, write)); + // One byte out of the range is in RAM, whichever end it leaves by. + assert_eq!(refused(&q35, 0x808000 - 7, Width::QWord, write), Refused::Straddles); + assert_eq!(refused(&q35, 0x800000 - 1, Width::Word, write), Refused::Straddles); + assert_eq!(refused(&q35, 0x808000, Width::Byte, write), Refused::UsableMemory); + } + assert_eq!(type_word(&Q35, 0x800000), 10); + assert_eq!(type_word(&LAPTOP, 0x7000_0000), 0); +} + +#[test] +fn the_tables_memory_is_read_and_never_written() { + let (q35, laptop) = (q35(), laptop()); + for memory in [&q35, &laptop] { + let reclaim = memory.map.iter().find(|entry| entry.uefi_type == 9).expect("a reclaim range"); + assert!(passes(memory, reclaim.start, Width::QWord, false)); + assert!(passes(memory, reclaim.end - 1, Width::Byte, false)); + assert_eq!(refused(memory, reclaim.start, Width::QWord, true), Refused::TableWrite); + assert_eq!(refused(memory, reclaim.end - 1, Width::Byte, true), Refused::TableWrite); + } + // Reclaim beside NVS: a read across the two is still two types. + assert_eq!(refused(&laptop, 0x7480_0000 - 2, Width::DWord, false), Refused::Straddles); +} + +/// A firmware that keeps the tables its XSDT lists in runtime-services data, +/// as one real machine's does: read to its last byte, never written, and a +/// read that leaves it for the RAM beside it is refused. +#[test] +fn runtime_services_data_is_read_as_the_tables_memory_is_and_never_written() { + let laptop = laptop(); + let data = LAPTOP.iter().find(|entry| entry.uefi_type == 6).expect("a runtime-services data range"); + for width in [Width::Byte, Width::QWord] { + assert!(passes(&laptop, data.start, width, false)); + assert!(passes(&laptop, data.end - width.bytes(), width, false)); + assert_eq!(refused(&laptop, data.start, width, true), Refused::TableWrite); + assert_eq!(refused(&laptop, data.end - width.bytes(), width, true), Refused::TableWrite); + } + assert_eq!(refused(&laptop, data.start - 1, Width::Word, false), Refused::Straddles); + assert_eq!(refused(&laptop, data.start - 1, Width::Byte, false), Refused::UsableMemory); + assert_eq!(type_word(&LAPTOP, data.start), 6); + // A device's page inside it is still a device's. + let decoding = bare(&LAPTOP, &[(0x7495_0000, 0x7495_0100)]); + assert_eq!(refused(&decoding, 0x7495_0800, Width::Byte, false), Refused::DeviceMemory); + assert!(passes(&decoding, 0x7495_1000, Width::Byte, false)); +} + +/// A chipset's registers at an address the firmware's map lists nowhere, as +/// one real machine's AML reads them while it loads: read, in each width, +/// where the kernel maps the address and the range registers type it +/// uncacheable, and never written. What the kernel drives or a function +/// decodes there is a device's still, and the ECAM window a configuration +/// access. +#[test] +fn an_unlisted_register_is_read_where_it_is_uncached_and_never_written() { + let laptop = laptop(); + const AT: u64 = REGISTERS + 0x12_3000; + assert_eq!(type_word(&LAPTOP, AT), toyos_abi::acpi::UNLISTED); + for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { + assert!(passes(&laptop, AT + 0x110, width, false), "{width:?}"); + assert_eq!(refused(&laptop, AT + 0x110, width, true), Refused::MemoryType, "{width:?}"); + } + // The last byte the range registers type so, and the first they do not. + assert!(passes(&laptop, REGISTERS, Width::Byte, false)); + assert_eq!(refused(&laptop, REGISTERS - 1, Width::Byte, false), Refused::UnlistedCached); + assert_eq!(refused(&laptop, REGISTERS - 1, Width::Word, false), Refused::UnlistedCached, "a read that begins outside it"); + assert!(passes(&laptop, 4 * GIB - 9, Width::QWord, false)); + // Past what the kernel maps there is nothing to read it through. + assert_eq!(refused(&laptop, 4 * GIB - 4, Width::QWord, false), Refused::Unmapped); + let low = Memory { mapped_end: AT + 0x110, ..bare(&LAPTOP, &[]) }; + assert_eq!(refused(&low, AT + 0x110, Width::Byte, false), Refused::Unmapped); + assert!(passes(&low, AT + 0x10f, Width::Byte, false)); + // A page the kernel knows a device decodes in. + let decoding = bare(&LAPTOP, &[(AT, AT + 0x20)]); + assert_eq!(refused(&decoding, AT + 0x110, Width::Byte, false), Refused::DeviceMemory); + assert_eq!(refused(&q35(), 0xfee0_0000, Width::DWord, false), Refused::DeviceMemory, "the local APIC"); + // Listed memory is decided by its type, whatever the range registers say of it. + const TYPED: [MemoryMapEntry; 3] = [e(7, REGISTERS, REGISTERS + 0x1000), e(11, REGISTERS + 0x1000, REGISTERS + 0x2000), e(0, REGISTERS + 0x2000, REGISTERS + 0x3000)]; + let typed = bare(&TYPED, &[]); + assert_eq!(refused(&typed, REGISTERS, Width::Byte, false), Refused::UsableMemory); + assert_eq!(refused(&typed, REGISTERS + 0x1000, Width::Byte, false), Refused::MemoryType); + assert!(passes(&typed, REGISTERS + 0x2000, Width::Byte, true)); + // A read across listed memory and a hole is two things. + assert_eq!(refused(&typed, REGISTERS + 0x3000 - 1, Width::Word, false), Refused::Straddles); +} + +/// The range registers the cache check answers from are the boot processor's. +/// Where some CPU's are on and are not those, no address the map does not +/// list is read, a register's and one below 1 MiB alike; listed memory, which +/// its type decides, and every other refusal are as they were. +#[test] +fn no_unlisted_address_is_read_where_a_cpus_range_registers_differ() { + let agreeing = laptop(); + let differing = Memory { registers_differ: true, ..laptop() }; + const AT: u64 = REGISTERS + 0x12_3110; + for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { + assert!(passes(&agreeing, AT, width, false), "{width:?}"); + assert_eq!(refused(&differing, AT, width, false), Refused::RangeRegistersDiffer, "{width:?}"); + assert_eq!(refused(&differing, AT, width, true), Refused::MemoryType, "{width:?}"); + } + assert_eq!(refused(&differing, REGISTERS - 1, Width::Byte, false), Refused::RangeRegistersDiffer); + const HOLE: &[MemoryMapEntry] = &[e(7, 0x0, 0xa_0000)]; + let low = Memory { registers_differ: true, ..bare(HOLE, &[]) }; + assert_eq!(refused(&low, 0xa_0000, Width::Byte, false), Refused::RangeRegistersDiffer); + // What the map lists, a device decodes or the kernel does not map. + assert!(passes(&differing, 0x7000_0000, Width::DWord, true), "reserved memory"); + assert!(passes(&differing, 0x7400_0000, Width::DWord, false), "ACPI NVS"); + assert_eq!(refused(&differing, 0x10_0000, Width::Byte, false), Refused::UsableMemory); + assert_eq!(refused(&differing, 4 * GIB - 4, Width::QWord, false), Refused::Unmapped); + let decoding = Memory { registers_differ: true, ..bare(&LAPTOP, &[(AT, AT + 0x20)]) }; + assert_eq!(refused(&decoding, AT, Width::Byte, false), Refused::DeviceMemory); +} + +/// Below 1 MiB the fixed range registers decide what is cached, and the +/// kernel reads none of them: an unlisted address there is refused whatever +/// the cache check answers, to the last byte below the bound. +#[test] +fn an_unlisted_address_below_1_mib_is_refused_whatever_the_range_registers_answer() { + // RAM, and a hole from the legacy video memory up that the map never lists. + const HOLE: &[MemoryMapEntry] = &[e(7, 0x0, 0xa_0000)]; + let memory = Memory { uncached: |_, _| true, ..bare(HOLE, &[]) }; + for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { + for at in [0xa_0000, 0xc_0000, FIXED_RANGE_END - 8, FIXED_RANGE_END - width.bytes()] { + assert_eq!(refused(&memory, at, width, false), Refused::UnlistedCached, "{at:#x} {width:?}"); + assert_eq!(refused(&memory, at, width, true), Refused::MemoryType, "{at:#x} {width:?}"); + } + assert!(passes(&memory, FIXED_RANGE_END, width, false), "{width:?} at the bound"); + } + assert_eq!(refused(&memory, FIXED_RANGE_END - 1, Width::Byte, false), Refused::UnlistedCached); + assert_eq!(refused(&memory, FIXED_RANGE_END - 1, Width::Word, false), Refused::UnlistedCached, "a read that begins below the bound"); + // Listed memory below the bound is decided by its type, as anywhere. + assert!(passes(&laptop(), 0x9f000, Width::DWord, false)); +} + +/// A map that lists a range twice, or two ranges over one another: the +/// allocator takes every usable range, so a byte any usable range holds is +/// RAM, whichever range lists it first. +#[test] +fn memory_any_usable_range_holds_is_refused_whatever_lists_it_first() { + for firmware in [0, 6, 9, 10] { + for handed_out in [1, 2, 3, 4, 7] { + // The same range under both types, the firmware's first. + let twice = [e(firmware, 0x1000, 0x3000), e(handed_out, 0x1000, 0x3000)]; + // A usable range over the firmware range's second page and beyond. + let over = [e(firmware, 0x1000, 0x3000), e(handed_out, 0x2000, 0x4000)]; + for write in [false, true] { + let twice = Memory { map: &twice, mapped_end: 4 * GIB, ecam: None, devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; + let over = Memory { map: &over, mapped_end: 4 * GIB, ecam: None, devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; + let why = MemoryVerdict::Refused(Refused::UsableMemory); + assert_eq!(twice.clone().decide(0x1000, Width::Byte, write), why, "type {firmware} listed before {handed_out}"); + assert_eq!(twice.decide(0x2ff8, Width::QWord, write), why); + assert_eq!(over.clone().decide(0x2000, Width::Byte, write), why, "type {firmware} under {handed_out}"); + assert_eq!(over.clone().decide(0x1ffc, Width::QWord, write), why, "an access whose last bytes a usable range holds"); + assert_eq!(over.clone().decide(0x2fff, Width::Byte, write), why); + // The page no usable range holds is the firmware's still. + let alone = over.decide(0x1ff8, Width::QWord, write); + match (firmware, write) { + (6 | 9, true) => assert_eq!(alone, MemoryVerdict::Refused(Refused::TableWrite)), + _ => assert!(matches!(alone, MemoryVerdict::Through(_)), "type {firmware} write={write}: {alone:?}"), + } + } + } + } + // The lock word is exchanged in no byte a usable range holds either. + assert_eq!(lock_word(&[e(10, 0x1000, 0x2000), e(4, 0x1000, 0x2000)], 4 * GIB, 0x1010), Err(NoLockWord::Type(Some(4)))); + assert_eq!(lock_word(&[e(10, 0x1000, 0x2000), e(7, 0x1013, 0x2000)], 4 * GIB, 0x1010), Err(NoLockWord::Type(Some(7)))); + assert!(lock_word(&[e(10, 0x1000, 0x2000), e(7, 0x1014, 0x2000)], 4 * GIB, 0x1010).is_ok()); +} + +/// Runtime-services code is the firmware's to execute and nobody's to read +/// through this claim: refused both ways, with its type. +#[test] +fn runtime_services_code_is_refused_both_ways() { + const CODE: [MemoryMapEntry; 2] = [e(6, 0x7490_1000, 0x74a0_0000), e(5, 0x74a0_0000, 0x74b0_0000)]; + let memory = bare(&CODE, &[]); + for write in [false, true] { + assert_eq!(refused(&memory, 0x74a0_0000, Width::Byte, write), Refused::MemoryType); + assert_eq!(refused(&memory, 0x74b0_0000 - 8, Width::QWord, write), Refused::MemoryType); + } + assert_eq!(type_word(&CODE, 0x74a0_0000), 5); + // Data beside code: a read across the two is two types. + assert_eq!(refused(&memory, 0x74a0_0000 - 4, Width::QWord, false), Refused::Straddles); +} + +#[test] +fn every_other_type_and_an_unlisted_address_is_refused_with_its_type() { + let laptop = laptop(); + // A hole the map does not list, which no register is in: its write is + // refused by the map, and its read by the range registers. + assert_eq!(refused(&laptop, 0x8000_0000, Width::Byte, true), Refused::MemoryType); + assert_eq!(refused(&laptop, 0x8000_0000, Width::Byte, false), Refused::UnlistedCached); + assert_eq!(type_word(&LAPTOP, 0x8000_0000), toyos_abi::acpi::UNLISTED); + // Every type but the four the policy names, as the only range of a map. + for ty in (0..=0x20u32).chain([0x7000_0000, 0x8000_0000, u32::MAX]) { + let map = [e(ty, 0x1000, 0x2000)]; + let memory = Memory { map: &map, mapped_end: 4 * GIB, ecam: None, devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; + let read = memory.clone().decide(0x1000, Width::Byte, false); + let write = memory.decide(0x1000, Width::Byte, true); + let through = |verdict| matches!(verdict, MemoryVerdict::Through(_)); + match ty { + 0 | 10 => assert!(through(read) && through(write), "type {ty}"), + 6 | 9 => assert!(through(read) && write == MemoryVerdict::Refused(Refused::TableWrite), "type {ty}"), + 1 | 2 | 3 | 4 | 7 => assert!(read == write && read == MemoryVerdict::Refused(Refused::UsableMemory), "type {ty}"), + _ => assert!(read == write && read == MemoryVerdict::Refused(Refused::MemoryType), "type {ty}"), + } + } +} + +#[test] +fn memory_past_what_the_kernel_maps_is_refused() { + let q35 = q35(); + assert_eq!(refused(&q35, 0xfd_0000_0000, Width::Byte, false), Refused::Unmapped); + const ACROSS_THE_END: &[MemoryMapEntry] = &[e(0, 4 * GIB - 0x1000, 4 * GIB + 0x1000)]; + let memory = bare(ACROSS_THE_END, &[]); + assert!(passes(&memory, 4 * GIB - 8, Width::QWord, true)); + assert_eq!(refused(&memory, 4 * GIB - 7, Width::QWord, false), Refused::Unmapped); + assert_eq!(refused(&memory, 4 * GIB, Width::Byte, false), Refused::Unmapped); + // An access that would wrap the address space names no memory. + assert_eq!(refused(&q35, u64::MAX, Width::Word, false), Refused::Unmapped); + assert_eq!(refused(&q35, u64::MAX - 6, Width::QWord, true), Refused::Unmapped); +} + +/// The I/O APIC, the HPET and the local APIC, typed reserved by this map. +const DEVICES_RESERVED: &[MemoryMapEntry] = &[e(0, 0xfe00_0000, 0xff00_0000)]; + +#[test] +fn every_page_a_window_the_kernel_drives_lies_in_is_refused_inside_any_type() { + let memory = bare(DEVICES_RESERVED, Q35_DRIVEN); + for write in [false, true] { + assert_eq!(refused(&memory, 0xfec0_0000, Width::DWord, write), Refused::DeviceMemory); + assert_eq!(refused(&memory, 0xfec0_001f, Width::Byte, write), Refused::DeviceMemory); + // The I/O APIC is mapped as 0x20 bytes; its EOI register is at 0x40 of + // the page, and the page is the device's to its last byte. + assert_eq!(refused(&memory, 0xfec0_0020, Width::DWord, write), Refused::DeviceMemory, "the dword after the mapped bytes"); + assert_eq!(refused(&memory, 0xfec0_0040, Width::DWord, write), Refused::DeviceMemory, "the EOI register"); + assert_eq!(refused(&memory, 0xfec0_0fff, Width::Byte, write), Refused::DeviceMemory, "the page's last byte"); + assert_eq!(refused(&memory, 0xfec0_0000 - 1, Width::Word, write), Refused::DeviceMemory, "a word that ends in the page"); + assert_eq!(refused(&memory, 0xfec0_0ffd, Width::QWord, write), Refused::DeviceMemory, "a qword that begins in it"); + assert_eq!(refused(&memory, 0xfed0_0ff8, Width::QWord, write), Refused::DeviceMemory); + assert_eq!(refused(&memory, 0xfed0_0000 - 1, Width::Word, write), Refused::DeviceMemory); + assert_eq!(refused(&memory, 0xfee0_0000, Width::DWord, write), Refused::DeviceMemory); + assert_eq!(refused(&memory, 0xfeef_ffff, Width::Byte, write), Refused::DeviceMemory); + assert_eq!(refused(&memory, 0xfee0_0000 - 4, Width::QWord, write), Refused::DeviceMemory); + // The pages either side of each device are the firmware's. + assert!(passes(&memory, 0xfec0_0000 - 8, Width::QWord, write)); + assert!(passes(&memory, 0xfec0_1000, Width::Byte, write)); + assert!(passes(&memory, 0xfed0_0000 - 1, Width::Byte, write)); + assert!(passes(&memory, 0xfed0_1000, Width::Byte, write)); + assert!(passes(&memory, 0xfee0_0000 - 8, Width::QWord, write)); + assert!(passes(&memory, 0xfef0_0000, Width::Byte, write)); + } + // A window that begins and ends inside pages takes each page it touches. + let memory = bare(DEVICES_RESERVED, &[(0xfe40_0ff0, 0xfe40_1010)]); + assert_eq!(refused(&memory, 0xfe40_0000, Width::Byte, true), Refused::DeviceMemory); + assert_eq!(refused(&memory, 0xfe40_1fff, Width::Byte, true), Refused::DeviceMemory); + assert!(passes(&memory, 0xfe40_0000 - 1, Width::Byte, true)); + assert!(passes(&memory, 0xfe40_2000, Width::Byte, true)); +} + +#[test] +fn a_memory_bar_is_refused_where_firmware_types_its_range_as_its_own() { + // A 16 KiB BAR, and a BAR that answered no size and is recorded as one + // byte, in memory this map types reserved. + const BARS: &[(u64, u64)] = &[(0xfe10_0000, 0xfe10_4000), (0xfe20_0000, 0xfe20_0001)]; + // The same 16 KiB BAR in ACPI NVS, and one above everything mapped. + const NVS: &[MemoryMapEntry] = &[e(10, 0x7400_0000, 0x7480_0000), e(0, 0x40_0000_0000, 0x40_1000_0000)]; + const NVS_BARS: &[(u64, u64)] = &[(0x7410_0000, 0x7410_4000), (0x40_0000_0000, 0x40_0100_0000)]; + let reserved = bare(DEVICES_RESERVED, BARS); + let nvs = bare(NVS, NVS_BARS); + for write in [false, true] { + assert_eq!(refused(&reserved, 0xfe10_0000, Width::DWord, write), Refused::DeviceMemory); + assert_eq!(refused(&reserved, 0xfe10_3fff, Width::Byte, write), Refused::DeviceMemory); + assert_eq!(refused(&reserved, 0xfe10_0000 - 4, Width::QWord, write), Refused::DeviceMemory); + assert!(passes(&reserved, 0xfe10_4000, Width::Byte, write), "the byte after the BAR"); + assert!(passes(&reserved, 0xfe10_0000 - 8, Width::QWord, write), "the qword before it"); + assert_eq!(refused(&reserved, 0xfe20_0000, Width::Byte, write), Refused::DeviceMemory); + assert_eq!(refused(&reserved, 0xfe20_0fff, Width::Byte, write), Refused::DeviceMemory, "the page of a BAR of unknown size"); + assert!(passes(&reserved, 0xfe20_1000, Width::Byte, write)); + assert_eq!(refused(&nvs, 0x7410_0000, Width::QWord, write), Refused::DeviceMemory); + assert!(passes(&nvs, 0x7410_4000, Width::QWord, write)); + // A device's memory is refused as a device's even where nothing maps it. + assert_eq!(refused(&nvs, 0x40_0000_0000, Width::DWord, write), Refused::DeviceMemory); + assert_eq!(refused(&nvs, 0x40_0100_0000, Width::DWord, write), Refused::Unmapped); + } + // With no record of the BAR the same addresses pass: the record is what refuses them. + assert!(passes(&bare(DEVICES_RESERVED, &[]), 0xfe10_0000, Width::DWord, true)); +} + +#[test] +fn the_facs_is_read_and_its_bytes_are_never_written() { + let q35 = q35(); + assert!(passes(&q35, Q35_FACS.0 + 16, Width::DWord, false), "the lock word reads"); + assert_eq!(refused(&q35, Q35_FACS.0 + 16, Width::DWord, true), Refused::FacsWrite); + assert_eq!(refused(&q35, Q35_FACS.0 - 4, Width::QWord, true), Refused::FacsWrite, "a write that ends in it"); + assert_eq!(refused(&q35, Q35_FACS.1 - 1, Width::Byte, true), Refused::FacsWrite); + assert!(passes(&q35, Q35_FACS.1, Width::Byte, true), "the byte after it is plain NVS"); + assert!(passes(&q35, Q35_FACS.0 - 8, Width::QWord, true)); +} + +#[test] +fn an_address_in_the_ecam_window_is_a_configuration_access_whatever_the_map_types_it() { + // Typed reserved on q35 and memory-mapped I/O on the laptop's shape. + for memory in [q35(), laptop()] { + let base = memory.ecam.expect("an ECAM window").base; + for write in [false, true] { + assert_eq!( + memory.clone().decide(base + (3 << 20 | 0x1c << 15 | 5 << 12 | 0x48), Width::DWord, write), + MemoryVerdict::AsConfig(Function { bus: 3, device: 0x1c, function: 5 }, 0x48) + ); + assert_eq!( + memory.clone().decide(base + (0xFF << 20 | 0x1f << 15 | 7 << 12 | 0xFFF), Width::Byte, write), + MemoryVerdict::AsConfig(Function { bus: 0xFF, device: 0x1f, function: 7 }, 0xFFF), + "the window's last byte" + ); + assert_eq!(refused(&memory, base - 1, Width::Word, write), Refused::Straddles); + assert_eq!(refused(&memory, base + (0x100 << 20) - 1, Width::Word, write), Refused::Straddles); + } + } + // A window that begins at a later bus holds nothing below it. + const WINDOW: &[MemoryMapEntry] = &[e(0, 0xe000_0000, 0xf000_0000)]; + let ecam = Ecam { base: 0xe000_0000, segment: 0, first_bus: 0x10, last_bus: 0x1F }; + let memory = Memory { ecam: Some(ecam), ..bare(WINDOW, &[]) }; + assert!(passes(&memory, 0xe000_0000, Width::Byte, false), "below the first bus is plain reserved memory"); + assert_eq!(memory.clone().decide(0xe100_0000, Width::Byte, false), MemoryVerdict::AsConfig(Function { bus: 0x10, device: 0, function: 0 }, 0)); + assert!(passes(&memory, 0xe200_0000, Width::Byte, false), "past the last bus too"); + // A base firmware put at the top of the address space decides without overflow. + let ecam = Ecam { base: u64::MAX - 0xFFF, segment: 0, first_bus: 0, last_bus: 0xFF }; + let memory = Memory { ecam: Some(ecam), ..bare(WINDOW, &[]) }; + assert!(passes(&memory, 0xe000_0000, Width::Byte, false)); +} + +#[test] +fn the_lock_word_is_exchanged_only_where_all_four_bytes_are_the_firmwares_own() { + // The FACS of the q35 boot and of the laptop's shape, both in ACPI NVS. + assert_eq!(lock_word(&Q35, 4 * GIB, Q35_FACS.0 + 16).map(|word| word.at()), Ok(Q35_FACS.0 + 16)); + assert_eq!(lock_word(&LAPTOP, 4 * GIB, 0x7400_0050).map(|word| word.at()), Ok(0x7400_0050)); + assert!(lock_word(&LAPTOP, 4 * GIB, 0x7000_0010).is_ok(), "reserved memory is the firmware's too"); + + // A firmware range that ends inside the word, at its last byte, and before + // it: what follows is RAM, and a word with one byte there is refused. + for (end, verdict) in [(0x1014, Ok(0x1010)), (0x1013, Err(NoLockWord::Type(Some(7)))), (0x1011, Err(NoLockWord::Type(Some(7)))), (0x1010, Err(NoLockWord::Type(Some(7))))] { + let map = [e(10, 0x1000, end), e(7, end, 0x2000)]; + assert_eq!(lock_word(&map, 4 * GIB, 0x1010).map(|word| word.at()), verdict, "the firmware's range ends at {end:#x}"); + } + // The same where nothing is listed after the range, and where the word's + // first byte is in RAM and its last in the firmware's. + assert_eq!(lock_word(&[e(10, 0x1000, 0x1012)], 4 * GIB, 0x1010), Err(NoLockWord::Type(None))); + assert_eq!(lock_word(&[e(7, 0x1000, 0x1012), e(10, 0x1012, 0x2000)], 4 * GIB, 0x1010), Err(NoLockWord::Type(Some(7)))); + assert_eq!(lock_word(&[], 4 * GIB, 0x1010), Err(NoLockWord::Type(None))); + + // Only ACPI NVS and reserved memory: not the tables' memory, which is + // never written, nor any other type. + for ty in (0..=0x20u32).chain([0x7000_0000, u32::MAX]) { + let verdict = lock_word(&[e(ty, 0x1000, 0x2000)], 4 * GIB, 0x1010); + match ty { + 0 | 10 => assert!(verdict.is_ok(), "type {ty}"), + _ => assert_eq!(verdict, Err(NoLockWord::Type(Some(ty)))), + } + } + + // A dword off its boundary, in the firmware's own memory. + for off in 1..4 { + assert_eq!(lock_word(&Q35, 4 * GIB, Q35_FACS.0 + 16 + off), Err(NoLockWord::Misaligned)); + } + // The last word the kernel maps, and the first it does not. + let map = [e(10, 4 * GIB - 0x1000, 4 * GIB + 0x1000)]; + assert!(lock_word(&map, 4 * GIB, 4 * GIB - 4).is_ok()); + assert_eq!(lock_word(&map, 4 * GIB, 4 * GIB), Err(NoLockWord::Unmapped)); + assert_eq!(lock_word(&[e(10, u64::MAX - 0xFFF, u64::MAX)], 4 * GIB, u64::MAX - 3), Err(NoLockWord::Type(None)), "the address space's last dword"); +} + +/// The kernel's declarations as q35 boots with them, and the i8042's row. +fn standing(port: u16) -> Standing { + match port { + 0x3F8..=0x3FF | 0x20..=0x21 | 0xA0..=0xA1 | 0x70..=0x71 | 0xCF8 | 0xCFC..=0xCFF | 0xCF9 => Standing::Declared(Mediated::Kept), + 0x80 => Standing::Declared(Mediated::Open), + 0xB2 | 0x604..=0x605 | 0x660..=0x67F => Standing::Declared(Mediated::ReadOnly), + 0x60 | 0x64 => Standing::Row, + _ => Standing::Free, + } +} + +#[test] +fn a_port_answers_as_its_declaration_says() { + for write in [false, true] { + for (at, width) in [(0x3F8, Width::Byte), (0x3FF, Width::Byte), (0x20, Width::Word), (0x70, Width::Byte), (0xCF8, Width::DWord), (0xCF9, Width::Byte)] { + assert_eq!(port(standing, at, width, write), Err(Refused::KernelPort), "{at:#x}"); + } + assert_eq!(port(standing, 0x60, Width::Byte, write), Err(Refused::ClaimedPort)); + assert_eq!(port(standing, 0x64, Width::Byte, write), Err(Refused::ClaimedPort)); + // The POST port, and ports nothing declared. + for (at, width) in [(0x80, Width::Byte), (0x72, Width::Word), (0x1800, Width::DWord), (0xFFFC, Width::DWord), (0xFFFF, Width::Byte)] { + let witness = port(standing, at, width, write).expect("a free port"); + assert_eq!((witness.port(), witness.width()), (at, width)); + } + } + // SMI_CMD, the PM1a control block and the TCO block: read, never written. + for (at, width) in [(0xB2, Width::Byte), (0x604, Width::Word), (0x605, Width::Byte), (0x660, Width::DWord)] { + assert!(port(standing, at, width, false).is_ok(), "{at:#x} reads"); + assert_eq!(port(standing, at, width, true), Err(Refused::ReadOnlyPort), "{at:#x}"); + } +} + +#[test] +fn a_wide_access_is_held_to_every_port_it_spans() { + // A word that begins on a free port and ends on a kept one, and the same + // into a row, a read-only run and out of the port space. + assert_eq!(port(standing, 0x3F7, Width::Word, false), Err(Refused::KernelPort)); + assert_eq!(port(standing, 0x1D, Width::DWord, false), Err(Refused::KernelPort)); + assert_eq!(port(standing, 0x5F, Width::Word, true), Err(Refused::ClaimedPort)); + assert_eq!(port(standing, 0xB1, Width::Word, true), Err(Refused::ReadOnlyPort)); + assert!(port(standing, 0xB1, Width::Word, false).is_ok()); + assert_eq!(port(standing, 0xFFFF, Width::Word, false), Err(Refused::PortSpan)); + assert_eq!(port(standing, 0xFFFD, Width::DWord, true), Err(Refused::PortSpan)); + assert_eq!(port(standing, 0x1800, Width::QWord, false), Err(Refused::PortSpan), "no port access is a qword"); +} + +const HOST_BRIDGE: Function = Function { bus: 0, device: 0, function: 0 }; + +#[test] +fn a_configuration_read_is_held_to_the_window_and_to_one_register() { + let ecam = Some(Ecam { base: 0xe000_0000, segment: 0, first_bus: 0, last_bus: 0x7F }); + for (offset, width) in [(0, Width::DWord), (0xE, Width::Byte), (0x19, Width::Byte), (0x4A, Width::Word), (0xFFC, Width::DWord), (0xFFF, Width::Byte)] { + let at = config(ecam, 0, HOST_BRIDGE, offset, width, false).expect("one register of a reachable function"); + assert_eq!((at.function(), at.offset(), at.width()), (HOST_BRIDGE, offset, width)); + } + let last = Function { bus: 0x7F, device: 31, function: 7 }; + assert!(config(ecam, 0, last, 0, Width::DWord, false).is_ok()); + for (offset, width) in [(0, Width::QWord), (1, Width::DWord), (3, Width::Word), (0xFFE, Width::DWord), (0x1000, Width::Byte), (u16::MAX, Width::Byte)] { + assert_eq!(config(ecam, 0, HOST_BRIDGE, offset, width, false), Err(Refused::ConfigSpan), "{offset:#x} {width:?}"); + } + assert_eq!(config(ecam, 1, HOST_BRIDGE, 0, Width::DWord, false), Err(Refused::ConfigUnreachable), "another segment group"); + assert_eq!(config(ecam, 0, Function { bus: 0x80, device: 0, function: 0 }, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); + assert_eq!(config(ecam, 0, Function { bus: 0, device: 32, function: 0 }, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); + assert_eq!(config(ecam, 0, Function { bus: 0, device: 0, function: 8 }, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); + assert_eq!(config(None, 0, HOST_BRIDGE, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); +} + +#[test] +fn every_configuration_write_is_refused_by_one_name() { + let ecam = Some(Ecam { base: 0xe000_0000, segment: 0, first_bus: 0, last_bus: 0x7F }); + // The header, a capability's place, the registers past them, extended + // space: every register a read reaches. + for offset in (0..0x1000u16).step_by(4) { + for (at, width) in [(offset, Width::DWord), (offset + 2, Width::Word), (offset + 3, Width::Byte)] { + assert!(config(ecam, 0, HOST_BRIDGE, at, width, false).is_ok(), "{at:#x} {width:?} reads"); + assert_eq!(config(ecam, 0, HOST_BRIDGE, at, width, true), Err(Refused::ConfigWrite), "{at:#x} {width:?}"); + } + } + // And what no read reaches is a write all the same. + assert_eq!(config(ecam, 0, HOST_BRIDGE, 0, Width::QWord, true), Err(Refused::ConfigWrite)); + assert_eq!(config(ecam, 1, HOST_BRIDGE, 0, Width::DWord, true), Err(Refused::ConfigWrite)); + assert_eq!(config(None, 0, HOST_BRIDGE, 0x44, Width::Byte, true), Err(Refused::ConfigWrite)); +} diff --git a/toyos/src/device.rs b/toyos/src/device.rs index 1e2138064de..6808df252a8 100644 --- a/toyos/src/device.rs +++ b/toyos/src/device.rs @@ -212,6 +212,24 @@ impl AcpiDev { pub fn ack(&self) -> Result<(), SyscallError> { syscall::write(self.0.0.0, &toyos_abi::acpi::ACK.to_ne_bytes()).map(|_| ()) } + + /// Have the kernel make one access outside the claim's own ports, or + /// refuse it by name; beside either, the UEFI type firmware's map gives a + /// memory address ([`toyos_abi::acpi::Access::memory_type`]). + pub fn access(&self, mut access: toyos_abi::acpi::Access) -> Result<(Result, u8), SyscallError> { + let made = syscall::acpi_access(self.as_handle(), &mut access)?; + Ok((made, access.memory_type)) + } + + /// Try the firmware's Global Lock ([`syscall::acpi_lock_take`]): `false` + /// where the firmware owns it and will raise `GBL_STS` on letting go. + pub fn lock_take(&self) -> Result { + syscall::acpi_lock_take(self.as_handle()) + } + + pub fn lock_release(&self) -> Result<(), SyscallError> { + syscall::acpi_lock_release(self.as_handle()) + } } impl AsHandle for AcpiDev { diff --git a/userland/Cargo.lock b/userland/Cargo.lock index d0dfba9d56a..0d33d2167ab 100644 --- a/userland/Cargo.lock +++ b/userland/Cargo.lock @@ -8,6 +8,8 @@ version = "0.1.0" dependencies = [ "toyos", "toyos-abi", + "toyos-acpi", + "toyos-aml", ] [[package]] diff --git a/userland/acpiserver/Cargo.toml b/userland/acpiserver/Cargo.toml index 29bceb4e376..faa28e18881 100644 --- a/userland/acpiserver/Cargo.toml +++ b/userland/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." +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." version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" @@ -8,6 +8,9 @@ license = "MIT OR Apache-2.0" [dependencies] toyos = { path = "../../toyos" } toyos-abi = { path = "../../toyos-abi" } +# The tables' decode, which the kernel reads its own of by. +toyos-acpi = { path = "../../toyos-acpi" } +toyos-aml = { path = "aml" } [package.metadata.toyos.host] exempt.owns = "ToyOS's ACPI fixed hardware, claimed from the kernel with the SCI it raises" diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs index b3cd8f8486b..28353ce1ec2 100644 --- a/userland/acpiserver/aml/src/field.rs +++ b/userland/acpiserver/aml/src/field.rs @@ -233,8 +233,10 @@ impl Machine<'_> { // the low seven bits of the Header Type: a function that is // absent answers all ones, and any other layout a byte of // something else. - if register(self, 0x0E)? & 0x7F != 0x01 { - return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it")); + let header_type = register(self, 0x0E)?; + let refused = |secondary| Error::Bridge { segment, bus, device: b.device, function: b.function, header_type, secondary }; + if header_type & 0x7F != 0x01 { + return Err(refused(None)); } let answered = register(self, 0x19)?; // §6.5.4: the region is ready once its bridge has a bus number. @@ -242,7 +244,7 @@ impl Machine<'_> { // above the bus the bridge is on: any other answer would address // a device that is not below this bridge. if answered <= bus { - return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it")); + return Err(refused(Some(answered))); } bus = answered; } diff --git a/userland/acpiserver/aml/src/lib.rs b/userland/acpiserver/aml/src/lib.rs index 5b080074f1a..b5233010f94 100644 --- a/userland/acpiserver/aml/src/lib.rs +++ b/userland/acpiserver/aml/src/lib.rs @@ -133,6 +133,12 @@ pub enum Error { Unsupported(&'static str), /// The [`Host`] refused an access. Host(String), + /// A function between a PCI_Config region's device and its host bridge + /// names no bus below it (§6.5.4), by the registers it answered: a + /// Header Type that is no PCI-to-PCI bridge's, after which its Secondary + /// Bus Number was not asked, or a Secondary Bus Number not above `bus`, + /// the bus the function is on. + Bridge { segment: u16, bus: u8, device: u8, function: u8, header_type: u8, secondary: Option }, } /// A refusal by the [`Host`], saying why. diff --git a/userland/acpiserver/aml/tests/regions.rs b/userland/acpiserver/aml/tests/regions.rs index f83bc015c1f..cbc0d7b298b 100644 --- a/userland/acpiserver/aml/tests/regions.rs +++ b/userland/acpiserver/aml/tests/regions.rs @@ -230,7 +230,7 @@ fn a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus() { // bridge's own names no bus below it, and nothing is accessed there. for unset in [0x00, 0x40, 0x3F] { let (m, v) = read(&below(&endpoint), &[(upper(header), &[0x01]), (upper(secondary), &[unset])], ven); - assert!(matches!(v, Err(Error::Rule(_))), "{unset:#x}: {v:?}"); + assert_eq!(v, Err(Error::Bridge { segment: 0, bus: 0x40, device: 3, function: 1, header_type: 0x01, secondary: Some(unset) })); assert_eq!(m.accesses(), vec![Event::Read(upper(header), Access::Byte), Event::Read(upper(secondary), Access::Byte)], "{unset:#x}"); } @@ -239,7 +239,7 @@ fn a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus() { // one that is absent all ones, which is above every bus. Neither is asked. for (layout, at_0x19) in [(0x00, 0x45), (0xFF, 0xFF), (0x02, 0x45), (0x80, 0x45)] { let (m, v) = read(&below(&endpoint), &[(upper(header), &[layout]), (upper(secondary), &[at_0x19])], ven); - assert!(matches!(v, Err(Error::Rule(_))), "{layout:#x}: {v:?}"); + assert_eq!(v, Err(Error::Bridge { segment: 0, bus: 0x40, device: 3, function: 1, header_type: layout, secondary: None })); assert_eq!(m.accesses(), vec![Event::Read(upper(header), Access::Byte)], "{layout:#x}"); } diff --git a/userland/acpiserver/src/aml.rs b/userland/acpiserver/src/aml.rs index 9444dc90b4b..253ae8b1b8a 100644 --- a/userland/acpiserver/src/aml.rs +++ b/userland/acpiserver/src/aml.rs @@ -1,8 +1,441 @@ -//! Everything only the machine's AML can answer. Stage 1 interprets no AML, -//! so it answers what an empty namespace does: no embedded-controller query -//! is served. +//! Everything only the machine's AML can answer: its definition blocks +//! loaded into one namespace ([`toyos_aml`]), and what this server asks of it. +//! +//! [`load`] fetches the DSDT and every SSDT through the kernel +//! ([`crate::tables`]), loads each in the order the firmware lists them, and +//! says one line a table: loaded, or refused and why. **A refused SSDT is +//! said loudly and the load goes on** (the owner's ruling: "Go on, say it +//! loudly"). **A refused DSDT leaves no namespace**: no SSDT is loaded onto +//! nothing, and the server serves the power button as it does without AML. +//! A machine that is stopping ends the load in one line, and that is no +//! refusal. +//! +//! Every read of a table's bytes the kernel refused is counted by the +//! kernel's name for it and the memory type, in one line, whether or not the +//! load reached that table. +//! +//! Then `\_S5` is evaluated (ACPI 6.5, "\_Sx (System States)") and said, for +//! whoever holds it against the kernel's own decode of the same package, its +//! `ACPI: PM1a=` line. Nothing is evaluated after it yet, so the namespace is +//! not kept: no embedded-controller query is served. +//! +//! A table's line says its place, whether it is the DSDT or an SSDT, and a +//! refusal by its kind; what the firmware chose — a name, an offset, an +//! address, a bridge's registers — is on a line under [`OWN`]. + +use std::time::Instant; + +use toyos_acpi::TableError; +use toyos_aml::{Error, Host, Interpreter, Value}; + +use crate::host::{Firmware, Kernel, Refusal, OWN}; +use crate::tables::Tables; /// An embedded-controller query, taken off the controller, run once the /// drain that took it has ended: a query's method may itself talk to the /// controller. pub fn query(_q: u8) {} + +/// What became of the machine's definition blocks: the load's verdict, which +/// its lines say and nothing in the server acts on yet. Whoever evaluates a +/// method after the load asks `blocks` whether there is a namespace, and the +/// power-off through the server writes `s5`. +#[derive(Default, Debug, PartialEq, Eq)] +pub struct Loaded { + /// Each block in the order it was loaded, the DSDT first; `Err` is the + /// kind of its refusal. + pub blocks: Vec>, + /// `\_S5`'s `SLP_TYPa` and `SLP_TYPb`. + pub s5: Option<(u64, u64)>, +} + +/// What a refused evaluation or load is called in a line anyone may quote. +fn kind(why: &Error) -> String { + match why { + Error::Malformed { why, .. } => format!("malformed AML: {why}"), + Error::NotFound(_) => "a name that does not resolve".into(), + Error::Exists(_) => "a name defined twice".into(), + Error::Type(why) => format!("a type its operator refuses: {why}"), + Error::Rule(why) | Error::Table(why) | Error::Bound(why) => (*why).into(), + Error::Fatal { .. } => "the firmware executed Fatal".into(), + Error::Unsupported(why) => format!("not carried by this interpreter: {why}"), + Error::Host(why) => format!("denied by this server: {why}"), + Error::Bridge { .. } => BRIDGE.into(), + } +} + +/// The refusal the interpreter makes of a bridge's own registers. +const BRIDGE: &str = "a bridge above a PCI_Config region answered no bus below it"; + +/// What a table refused before any of it ran is called; `refused` is why the +/// kernel read none of the range a [`TableError::Unmapped`] names. +fn unread(why: &TableError, refused: impl Fn(u64) -> Option) -> String { + match why { + TableError::BadRsdp => "the RSDP does not check".into(), + TableError::NoXsdt => "the RSDP names no XSDT".into(), + TableError::Absent => "nothing names it, or what is named is another table".into(), + TableError::Length { .. } => "it declares a length no table has".into(), + TableError::Checksum => "its bytes do not sum to zero".into(), + TableError::Unmapped { at, .. } => match refused(*at) { + Some(refusal) => format!("its bytes could not be read: {refusal}"), + None => "its bytes are at no address a table has".into(), + }, + } +} + +const STOPPING: &str = "acpiserver: the machine is stopping, so the tables' load ends here"; + +/// Load the machine's definition blocks from the RSDP at `rsdp`, and +/// evaluate `\_S5`. +pub fn load(kernel: &K, rsdp: u64) -> Loaded { + let began = Instant::now(); + let mut loaded = Loaded::default(); + let tables = Tables::new(kernel); + let blocks: Vec<_> = match toyos_acpi::definition_blocks(&tables, rsdp) { + Ok(blocks) => blocks.collect(), + Err(_) if tables.stopping.get() => { + println!("{STOPPING}"); + return loaded; + } + Err(why) => { + println!( + "acpiserver: no namespace: the XSDT was not read ({}{}), so no table is; the power button is served, and nothing of this machine's AML", + unread(&why, |at| tables.refused(at)), + tables.last_refused().map_or(String::new(), |refusal| format!("; the last read refused was {refusal}")) + ); + println!("{OWN}that was {why:x?}"); + return loaded; + } + }; + if tables.stopping.get() { + println!("{STOPPING}"); + return loaded; + } + + let mut host = Firmware::new(kernel); + let mut interpreter = Interpreter::new(); + let count = blocks.len(); + for (place, block) in blocks.iter().enumerate() { + let name = if place == 0 { "DSDT" } else { "SSDT" }; + let place = place + 1; + let done = match block { + Ok(table) => interpreter.load(&mut host, table).map_err(|why| { + println!("{OWN}table {place} was refused {why:x?}"); + kind(&why) + }), + Err(why) => { + println!("{OWN}table {place} was not read: {why:x?}"); + Err(unread(why, |at| tables.refused(at))) + } + }; + if host.stopping { + println!("{STOPPING}"); + return loaded; + } + match &done { + Ok(()) => println!("acpiserver: table {place} of {count} ({name}) loaded"), + Err(kind) => println!("acpiserver: table {place} of {count} ({name}) refused: {kind}"), + } + loaded.blocks.push(done); + if place == 1 && loaded.blocks[0].is_err() { + println!( + "acpiserver: no namespace: the DSDT was refused, so the {} SSDT(s) after it are not loaded; the power button is served, and nothing of this machine's AML", + count - 1 + ); + break; + } + } + + println!( + "acpiserver: {} of {count} tables loaded in {} ms", + loaded.blocks.iter().filter(|block| block.is_ok()).count(), + began.elapsed().as_millis() + ); + println!( + "acpiserver: the tables' bytes took {} reads of firmware memory, in pages: {}", + tables.reads.get(), + tables.pages.borrow().by_type() + ); + let refusals = tables.refusals(); + if !refusals.is_empty() { + println!("acpiserver: reads of the tables' bytes the kernel refused: {}", refusals.counts()); + } + println!( + "acpiserver: the tables' AML read SystemMemory {} times, SystemIO {} and PCI_Config {}, its memory in pages: {}; took the Global Lock {} times, {} of them from the firmware; and ran Notify {} times", + host.reads[0], + host.reads[1], + host.reads[2], + host.pages.by_type(), + host.takes, + host.contended, + host.notifies + ); + + if loaded.blocks.first().is_some_and(|dsdt| dsdt.is_ok()) { + match s5(&mut interpreter, &mut host) { + Ok((a, b)) => { + println!("acpiserver: \\_S5 evaluated: SLP_TYPa={a} SLP_TYPb={b}"); + loaded.s5 = Some((a, b)); + } + Err(_) if host.stopping => println!("{STOPPING}"), + Err(kind) => println!("acpiserver: \\_S5 refused: {kind}"), + } + } + if !host.refused.is_empty() { + println!("acpiserver: refused so far, of accesses: {}", host.refused.counts()); + } + loaded +} + +/// `\_S5`'s first two elements: what `SLP_TYPa` and `SLP_TYPb` are written +/// for soft-off. +fn s5(interpreter: &mut Interpreter, host: &mut dyn Host) -> Result<(u64, u64), String> { + match interpreter.evaluate(host, "\\_S5", &[]) { + Ok(Value::Package(elements)) => match elements.as_slice() { + [Value::Integer(a), Value::Integer(b), ..] => Ok((*a, *b)), + _ => Err("a package that does not begin with two integers".into()), + }, + Ok(_) => Err("no package".into()), + Err(why) => { + println!("{OWN}\\_S5 was refused {why:x?}"); + Err(kind(&why)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::host::tests::Scripted; + use crate::host::{Take, HELD}; + + /// QEMU's own tables where its guest held them + /// (`toyos-acpi/fixtures/qemu-11.1.1/SOURCE`), as ACPI reclaim memory. + const RSDP: u64 = 0x7fb7_e014; + const DSDT: u64 = 0x7fb7_a000; + const QEMU: &[(u64, &[u8])] = &[ + (RSDP, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/rsdp.bin")), + (0x7fb7_d0e8, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/xsdt.bin")), + (0x7fb7_9000, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/facp.bin")), + (0x7fb7_8000, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/apic.bin")), + (0x7fb7_7000, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/hpet.bin")), + (0x7fb7_6000, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/mcfg.bin")), + (0x7fb7_5000, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/dmar.bin")), + (0x7fb7_4000, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/waet.bin")), + (DSDT, include_bytes!("../../../toyos-acpi/fixtures/qemu-11.1.1/dsdt.bin")), + ]; + + fn machine(tables: &[(u64, &[u8])]) -> Scripted { + Scripted { memory: tables.iter().map(|&(at, bytes)| (at, 9, bytes.to_vec())).collect(), ..Default::default() } + } + + /// What every guest boot does, against the kernel's stand-in: QEMU's + /// DSDT is fetched, loads asking the machine for nothing, and its `\_S5` + /// is the `SLP_TYPa=0` the kernel logged on the boot the tables are of. + #[test] + fn qemus_tables_load_and_s5_is_what_its_kernel_decoded() { + let kernel = machine(QEMU); + assert_eq!(load(&kernel, RSDP), Loaded { blocks: vec![Ok(())], s5: Some((0, 0)) }); + assert!(kernel.asked.borrow().iter().all(|access| access.write == 0 && access.space == 0)); + assert!(!kernel.held.get()); + } + + // The builders below are `toyos-aml`'s test encodings of §20.2, the few a + // definition block here needs. + + fn sealed(signature: &[u8; 4], body: &[u8]) -> Vec { + let mut table = vec![0u8; 36]; + table[..4].copy_from_slice(signature); + table[4..8].copy_from_slice(&(36 + body.len() as u32).to_le_bytes()); + table[8] = 2; + table.extend_from_slice(body); + table[9] = 0u8.wrapping_sub(table.iter().fold(0u8, |sum, &byte| sum.wrapping_add(byte))); + table + } + + fn cat(parts: &[&[u8]]) -> Vec { + parts.concat() + } + + /// `Name (, )` for a four-character name and a byte. + fn name(name: &[u8; 4], value: u8) -> Vec { + cat(&[&[0x08], name, &[0x0A, value]]) + } + + /// `Name (_S5_, Package (2) { a, b })`. + fn s5_package(a: u8, b: u8) -> Vec { + cat(&[&[0x08], b"_S5_", &[0x12, 0x06, 0x02, 0x0A, a, 0x0A, b]]) + } + + /// A machine whose XSDT lists a FADT naming `dsdt` and then `ssdts`. + fn crafted(dsdt: &[u8], ssdts: &[&[u8]]) -> Scripted { + const AT: u64 = 0x10_0000; + let at = |n: usize| AT + 0x1_0000 * n as u64; + let mut fadt_body = vec![0u8; 116 - 36]; + fadt_body[40 - 36..44 - 36].copy_from_slice(&(at(2) as u32).to_le_bytes()); + let mut fadt = sealed(b"FACP", &fadt_body); + fadt[8] = 1; + fadt[9] = 0; + fadt[9] = 0u8.wrapping_sub(fadt.iter().fold(0u8, |sum, &byte| sum.wrapping_add(byte))); + let mut entries = at(1).to_le_bytes().to_vec(); + for n in 0..ssdts.len() { + entries.extend_from_slice(&at(3 + n).to_le_bytes()); + } + let xsdt = sealed(b"XSDT", &entries); + let mut rsdp = vec![0u8; 36]; + rsdp[..8].copy_from_slice(b"RSD PTR "); + rsdp[15] = 2; + rsdp[20..24].copy_from_slice(&36u32.to_le_bytes()); + rsdp[24..32].copy_from_slice(&at(0).to_le_bytes()); + rsdp[8] = 0u8.wrapping_sub(rsdp[..20].iter().fold(0u8, |sum, &byte| sum.wrapping_add(byte))); + rsdp[32] = 0u8.wrapping_sub(rsdp.iter().fold(0u8, |sum, &byte| sum.wrapping_add(byte))); + let mut memory = vec![(AT - 0x1000, 9, rsdp), (at(0), 9, xsdt), (at(1), 9, fadt), (at(2), 9, dsdt.to_vec())]; + memory.extend(ssdts.iter().enumerate().map(|(n, ssdt)| (at(3 + n), 9, ssdt.to_vec()))); + Scripted { memory, ..Default::default() } + } + + const CRAFTED_RSDP: u64 = 0x10_0000 - 0x1000; + + #[test] + fn a_refused_ssdt_is_one_block_refused_and_the_rest_load() { + let dsdt = sealed(b"DSDT", &cat(&[&name(b"AAAA", 1), &s5_package(5, 7)])); + let collides = sealed(b"SSDT", &name(b"AAAA", 2)); + let mut unsummed = sealed(b"SSDT", &name(b"BBBB", 3)); + unsummed[9] = unsummed[9].wrapping_add(1); + let good = sealed(b"SSDT", &name(b"CCCC", 4)); + let kernel = crafted(&dsdt, &[&collides, &unsummed, &good]); + assert_eq!( + load(&kernel, CRAFTED_RSDP), + Loaded { + blocks: vec![Ok(()), Err("a name defined twice".into()), Err("its bytes do not sum to zero".into()), Ok(())], + s5: Some((5, 7)), + } + ); + } + + #[test] + fn a_refused_dsdt_leaves_no_namespace_and_loads_no_ssdt() { + // A definition block that ends inside a Name. + let dsdt = sealed(b"DSDT", &[0x08, b'A']); + let ssdt = sealed(b"SSDT", &name(b"CCCC", 4)); + let kernel = crafted(&dsdt, &[&ssdt, &ssdt]); + let loaded = load(&kernel, CRAFTED_RSDP); + assert_eq!(loaded.blocks.len(), 1, "an SSDT was loaded onto no DSDT: {loaded:?}"); + assert!(matches!(&loaded.blocks[0], Err(kind) if kind.starts_with("malformed AML: ")), "{loaded:?}"); + assert_eq!(loaded.s5, None); + } + + #[test] + fn tables_the_kernel_will_not_read_are_refused_by_its_name_for_it() { + // No RSDP where the claim says: RAM, to the stand-in. + assert_eq!(load(&machine(QEMU), 0x1000), Loaded::default()); + + // The DSDT's own bytes are not the firmware's to read. + let kernel = machine(&QEMU[..QEMU.len() - 1]); + let ram = "its bytes could not be read: a SystemMemory read the kernel refused UsableMemory, in memory of type 7"; + assert_eq!(load(&kernel, RSDP), Loaded { blocks: vec![Err(ram.into())], s5: None }); + } + + /// The shape the first load on the real machine had: its RSDP and XSDT + /// in memory the kernel reads, and its FADT, DSDT and SSDTs in memory of + /// a type the kernel passed no read of then, here one it still does not. The DSDT's line says that, by + /// the kernel's name for the refusal and the type, and not that nothing + /// names a DSDT. + #[test] + fn tables_in_memory_of_a_type_the_kernel_keeps_are_refused_by_that_name_and_type() { + let dsdt = sealed(b"DSDT", &s5_package(5, 0)); + let ssdt = sealed(b"SSDT", &name(b"CCCC", 4)); + let mut kernel = crafted(&dsdt, &[&ssdt, &ssdt]); + // What the XSDT lists moves into kept memory: the RSDP and the XSDT stay. + let listed = kernel.memory.split_off(2); + kernel.kept = listed.iter().map(|(at, _, bytes)| (*at, at + bytes.len() as u64, 5)).collect(); + let kept = "its bytes could not be read: a SystemMemory read the kernel refused MemoryType, in memory of type 5"; + assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Err(kept.into())], s5: None }); + } + + #[test] + fn an_s5_that_is_no_package_of_two_integers_is_refused_by_kind() { + let absent = sealed(b"DSDT", &name(b"AAAA", 1)); + assert_eq!(load(&crafted(&absent, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None }); + let integer = sealed(b"DSDT", &name(b"_S5_", 5)); + assert_eq!(load(&crafted(&integer, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None }); + let short = sealed(b"DSDT", &cat(&[&[0x08], b"_S5_", &[0x12, 0x04, 0x01, 0x0A, 0x05]])); + assert_eq!(load(&crafted(&short, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None }); + } + + #[test] + fn a_machine_that_stops_mid_load_ends_it_with_nothing_refused() { + for answered in [0, 3, 40] { + let kernel = machine(QEMU); + kernel.stops_after.set(Some(answered)); + assert_eq!(load(&kernel, RSDP), Loaded::default(), "after {answered} reads"); + } + } + + #[test] + fn every_refusals_kind_is_free_of_what_the_firmware_chose() { + let chosen = "\\_SB_.PCI0.XYZW"; + for why in [ + Error::NotFound(chosen.into()), + Error::Exists(chosen.into()), + Error::Malformed { at: 0x1234, why: "a NameString" }, + Error::Fatal { kind: 1, code: 0x1234, arg: 0x5678 }, + Error::Bridge { segment: 0, bus: 0x12, device: 0x1c, function: 5, header_type: 0x81, secondary: Some(0x34) }, + ] { + let said = kind(&why); + assert!(!said.contains("XYZW") && !said.chars().any(|c| c.is_ascii_digit()), "{why:?} is said as {said:?}"); + } + assert_eq!(kind(&Error::Bridge { segment: 0, bus: 0, device: 0, function: 0, header_type: 0, secondary: None }), BRIDGE); + assert_eq!(kind(&Error::Host("a write to SystemIO".into())), "denied by this server: a write to SystemIO"); + let unlisted = Refusal::Kernel { space: toyos_abi::acpi::Space::SystemMemory, refused: toyos_abi::acpi::Refused::MemoryType, memory_type: toyos_abi::acpi::UNLISTED }; + assert_eq!( + unread(&TableError::Unmapped { at: 0xdead_0000, len: 36 }, |at| (at == 0xdead_0000).then_some(unlisted)), + "its bytes could not be read: a SystemMemory read the kernel refused MemoryType, in unlisted firmware memory" + ); + } + + /// A Lock field's access takes the Global Lock through the kernel and + /// gives it back; one the firmware holds is the load's refusal, by name, + /// and the field is not read without it. + #[test] + fn a_lock_field_read_at_load_takes_the_lock_and_a_held_lock_refuses_the_table() { + const NVS: u64 = 0x7700_0000; + // OperationRegion (REGN, SystemMemory, 0x77000000, 4); Field (REGN, + // ByteAcc, Lock, Preserve) { FLDA, 8 }; Name (COPY, 0); Store (FLDA, COPY). + let body = cat(&[ + &[0x5B, 0x80], + b"REGN", + &[0x00, 0x0C, 0x00, 0x00, 0x00, 0x77, 0x0A, 0x04], + &[0x5B, 0x81, 0x0B], + b"REGN", + &[0x11], + b"FLDA", + &[0x08], + &[0x08], + b"COPY", + &[0x00], + &[0x70], + b"FLDA", + b"COPY", + &s5_package(5, 0), + ]); + let dsdt = sealed(b"DSDT", &body); + let with_nvs = || { + let mut kernel = crafted(&dsdt, &[]); + kernel.memory.push((NVS, 10, vec![0x42; 4])); + kernel + }; + let field = toyos_abi::acpi::Access::read(toyos_abi::acpi::Space::SystemMemory, NVS, toyos_abi::acpi::Width::Byte); + + let kernel = with_nvs(); + assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((5, 0)) }); + assert!(!kernel.held.get(), "the load ended holding the Global Lock"); + assert_eq!(kernel.asked.borrow().last(), Some(&field)); + + let kernel = with_nvs(); + kernel.takes.borrow_mut().push_back(Take::Pending); + let loaded = load(&kernel, CRAFTED_RSDP); + assert_eq!(loaded, Loaded { blocks: vec![Err(format!("denied by this server: {HELD}"))], s5: None }); + assert_ne!(kernel.asked.borrow().last(), Some(&field), "the field was read without the lock"); + } +} diff --git a/userland/acpiserver/src/host.rs b/userland/acpiserver/src/host.rs new file mode 100644 index 00000000000..a2827d905b4 --- /dev/null +++ b/userland/acpiserver/src/host.rs @@ -0,0 +1,493 @@ +//! What the machine's AML asks of the machine, answered through the kernel: +//! [`Firmware`] is the interpreter's [`Host`] over the `acpi` claim's +//! mediated access ([`toyos_abi::acpi`]), and [`Kernel`] is that access as +//! this server asks for it, so a host test answers in the kernel's place. +//! +//! **Nothing is written.** Loading a machine's tables writes nothing, and +//! this server evaluates nothing that does yet: a write in any space is +//! denied by name, and so is every access to the embedded controller's +//! space, which no transaction serves for AML yet. SystemCMOS never arrives: +//! the interpreter refuses that space itself. A read of memory, of a port or +//! of a function's configuration space is the kernel's to make or refuse, and +//! a refusal it names is denied under that name. +//! +//! **The Global Lock is the kernel's to exchange** (ACPI 6.5 §5.2.10.1). A +//! take that finds the firmware holding it is denied by name ([`HELD`]) and +//! counted: this server waits for no release of the firmware's yet. +//! +//! **What a line says.** A refusal is said the first time it is seen and +//! counted after ([`Ledger`]), as its address space, the kernel's name for +//! the refusal and the memory type: a line that can be quoted anywhere. Its +//! address, its function and any name the firmware chose are the machine's +//! own and go on a line of their own, under [`OWN`]. + +use std::collections::BTreeMap; +use std::fmt; +use std::time::{Duration, Instant}; + +use toyos_abi::acpi::{pci_address, Access, Refused, Space, Width, UNLISTED}; +use toyos_aml::{Address, Denied, Host}; + +use crate::ledger::Ledger; + +/// What opens a line that carries an address, a PCI function or a name the +/// firmware chose: this machine's own, and quoted in no record. +pub const OWN: &str = "acpiserver: (this machine's own, quoted in no record) "; + +/// The denial of a take that found the firmware holding the Global Lock. +pub const HELD: &str = "the Global Lock: the firmware holds it, and this server waits for no release yet"; + +/// The kernel does nothing more for this claim: the machine is stopping. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Stopping; + +/// What the kernel answered one access: the value read, or its refusal, and +/// the UEFI type firmware's map gives a memory address. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Answer { + pub made: Result, + pub memory_type: u8, +} + +/// What the kernel answered a take of the Global Lock. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Take { + Taken, + /// The firmware holds it, and was left the request. + Pending, + /// The machine's FACS is one the kernel exchanges no lock word in. + Unusable, +} + +/// The `acpi` claim's mediated access, as this server asks for it. +pub trait Kernel { + fn access(&self, access: Access) -> Result; + fn lock_take(&self) -> Result; + fn lock_release(&self) -> Result<(), Stopping>; +} + +/// Why a read was not made. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Refusal { + Kernel { space: Space, refused: Refused, memory_type: u8 }, + Stopping, +} + +fn space_name(space: Space) -> &'static str { + match space { + Space::SystemMemory => "SystemMemory", + Space::SystemIo => "SystemIO", + Space::PciConfig => "PCI_Config", + } +} + +/// What memory of a UEFI type is called: an address firmware's map does not +/// list has no type, and a name of its own. +fn type_name(memory_type: u8) -> String { + match memory_type { + UNLISTED => "unlisted firmware memory".into(), + listed => format!("memory of type {listed}"), + } +} + +impl fmt::Display for Refusal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Kernel { space: Space::SystemMemory, refused, memory_type } => { + write!(f, "a SystemMemory read the kernel refused {refused:?}, in {}", type_name(*memory_type)) + } + Self::Kernel { space, refused, .. } => write!(f, "a {} read the kernel refused {refused:?}", space_name(*space)), + Self::Stopping => f.write_str("the machine is stopping, and the kernel reads nothing more for this server"), + } + } +} + +/// One read through the kernel: its value and, for memory, the type of what +/// was read. +pub fn read(kernel: &impl Kernel, space: Space, address: u64, width: Width) -> Result<(u64, u8), Refusal> { + let Answer { made, memory_type } = kernel.access(Access::read(space, address, width)).map_err(|Stopping| Refusal::Stopping)?; + match made { + Ok(value) => Ok((value, memory_type)), + Err(refused) => Err(Refusal::Kernel { space, refused, memory_type }), + } +} + +/// The distinct 4 KiB pages of memory read, each with the UEFI type +/// firmware's map gives it. +#[derive(Default)] +pub struct Pages(BTreeMap); + +impl Pages { + pub fn read(&mut self, address: u64, memory_type: u8) { + self.0.insert(address >> 12, memory_type); + } + + /// How many pages of each type, and nothing of where they are. + pub fn by_type(&self) -> String { + let mut types: BTreeMap = BTreeMap::new(); + for &memory_type in self.0.values() { + *types.entry(memory_type).or_insert(0) += 1; + } + if types.is_empty() { + return "none".into(); + } + types.iter().map(|(&ty, pages)| format!("{pages} of {}", type_name(ty).trim_start_matches("memory of "))).collect::>().join(", ") + } +} + +/// The interpreter's host on one machine, and what it was asked. +pub struct Firmware<'k, K> { + kernel: &'k K, + /// The zero of [`Host::timer`]. + began: Instant, + /// Reads made, by [`Space`]. + pub reads: [u64; 3], + pub pages: Pages, + /// Takes of the Global Lock, and how many found the firmware holding it. + pub takes: u64, + pub contended: u64, + pub notifies: u64, + pub refused: Ledger, + notified: Ledger, + /// The kernel answered that the machine is stopping. + pub stopping: bool, +} + +impl<'k, K: Kernel> Firmware<'k, K> { + pub fn new(kernel: &'k K) -> Self { + Firmware { + kernel, + began: Instant::now(), + reads: [0; 3], + pages: Pages::default(), + takes: 0, + contended: 0, + notifies: 0, + refused: Ledger::default(), + notified: Ledger::default(), + stopping: false, + } + } + + /// Deny by the name `what`, said the first time with `own`, the machine's + /// own detail of it. + fn deny(&mut self, what: String, own: fmt::Arguments) -> Denied { + if self.refused.see(&what) { + println!("acpiserver: refused for the first time: {what}"); + println!("{OWN}that was {own}"); + } + Denied(what) + } + + fn stopped(&mut self) -> Denied { + self.stopping = true; + Denied(Refusal::Stopping.to_string()) + } +} + +fn wide(width: toyos_aml::Access) -> Width { + match width { + toyos_aml::Access::Byte => Width::Byte, + toyos_aml::Access::Word => Width::Word, + toyos_aml::Access::DWord => Width::DWord, + toyos_aml::Access::QWord => Width::QWord, + } +} + +const NO_CONTROLLER: &str = "EmbeddedControl: this server runs no controller transaction for AML yet"; + +impl Host for Firmware<'_, K> { + fn read(&mut self, at: Address, width: toyos_aml::Access) -> Result { + let (space, address) = match at { + Address::Memory(address) => (Space::SystemMemory, address), + Address::Io(port) => (Space::SystemIo, u64::from(port)), + Address::PciConfig { segment, bus, device, function, offset } => (Space::PciConfig, pci_address(segment, bus, device, function, offset)), + Address::EmbeddedControl(_) => return Err(self.deny(format!("a read of {NO_CONTROLLER}"), format_args!("{at:x?}"))), + }; + match read(self.kernel, space, address, wide(width)) { + Ok((value, memory_type)) => { + self.reads[space as usize] += 1; + if space == Space::SystemMemory { + self.pages.read(address, memory_type); + } + Ok(value) + } + Err(Refusal::Stopping) => Err(self.stopped()), + Err(refusal) => Err(self.deny(refusal.to_string(), format_args!("{width:?} at {at:x?}"))), + } + } + + fn write(&mut self, at: Address, width: toyos_aml::Access, value: u64) -> Result<(), Denied> { + let what = match at { + Address::Memory(_) => "a write to SystemMemory: this server writes nothing for AML yet".into(), + Address::Io(_) => "a write to SystemIO: this server writes nothing for AML yet".into(), + Address::PciConfig { .. } => "a write to PCI_Config: this server writes nothing for AML yet".into(), + Address::EmbeddedControl(_) => format!("a write to {NO_CONTROLLER}"), + }; + Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) + } + + fn sleep(&mut self, ms: u64) { + // The firmware's own delay (§19.6.125), bounded by the interpreter in + // what one evaluation may ask for. + std::thread::sleep(Duration::from_millis(ms)); + } + + fn stall(&mut self, us: u64) { + let until = Instant::now() + Duration::from_micros(us); + while Instant::now() < until { + std::hint::spin_loop(); + } + } + + fn timer(&mut self) -> u64 { + (self.began.elapsed().as_nanos() / 100) as u64 + } + + fn notify(&mut self, object: &str, value: u64) { + self.notifies += 1; + if self.notified.see(&format!("{object} {value:#x}")) { + println!("{OWN}Notify({object}, {value:#x}) for the first time; nothing serves a Notify yet"); + } + } + + fn global_lock(&mut self, take: bool) -> Result<(), Denied> { + if !take { + return self.kernel.lock_release().map_err(|Stopping| self.stopped()); + } + self.takes += 1; + match self.kernel.lock_take() { + Err(Stopping) => Err(self.stopped()), + Ok(Take::Taken) => Ok(()), + Ok(Take::Unusable) => { + Err(self.deny("the Global Lock: the kernel exchanges no lock word in this machine's FACS".into(), format_args!("a take"))) + } + Ok(Take::Pending) => { + self.contended += 1; + Err(self.deny(HELD.into(), format_args!("a take the firmware was left the request for"))) + } + } + } +} + +#[cfg(test)] +pub mod tests { + use std::cell::{Cell, RefCell}; + use std::collections::VecDeque; + + use super::*; + + /// A kernel that answers reads from bytes at addresses, each range with a + /// memory type, refuses `kept` ranges as the real one refuses memory of a + /// type it passes no read of and every other address as it refuses RAM, + /// and answers the Global Lock from a script. + #[derive(Default)] + pub struct Scripted { + pub memory: Vec<(u64, u8, Vec)>, + /// `(start, end, memory type)`. + pub kept: Vec<(u64, u64, u8)>, + pub ports: Vec<(u64, u64)>, + pub config: Vec<(u64, u64)>, + pub asked: RefCell>, + /// What each take answers, in turn; `Taken` once it runs out. + pub takes: RefCell>, + pub held: Cell, + /// Accesses and lock exchanges answered before the machine stops. + pub stops_after: Cell>, + } + + impl Scripted { + fn stopping(&self) -> Result<(), Stopping> { + match self.stops_after.get() { + Some(0) => Err(Stopping), + Some(left) => { + self.stops_after.set(Some(left - 1)); + Ok(()) + } + None => Ok(()), + } + } + } + + impl Kernel for Scripted { + fn access(&self, access: Access) -> Result { + self.stopping()?; + self.asked.borrow_mut().push(access); + assert_eq!(access.write, 0, "this server asks the kernel for no write"); + let width = Width::from_raw(access.width).expect("a width").bytes(); + let listed = |values: &[(u64, u64)]| values.iter().find(|(at, _)| *at == access.address).map(|&(_, value)| value); + Ok(match Space::from_raw(access.space).expect("a space") { + Space::SystemMemory => { + let held = self.memory.iter().find(|(base, _, bytes)| { + access.address >= *base && access.address + width <= base + bytes.len() as u64 + }); + match held { + Some((base, memory_type, bytes)) => { + let from = (access.address - base) as usize; + let value = bytes[from..from + width as usize].iter().rev().fold(0u64, |value, &byte| value << 8 | u64::from(byte)); + Answer { made: Ok(value), memory_type: *memory_type } + } + None => match self.kept.iter().find(|(start, end, _)| (*start..*end).contains(&access.address)) { + Some(&(.., memory_type)) => Answer { made: Err(Refused::MemoryType), memory_type }, + None => Answer { made: Err(Refused::UsableMemory), memory_type: 7 }, + }, + } + } + Space::SystemIo => Answer { made: listed(&self.ports).ok_or(Refused::KernelPort), memory_type: UNLISTED }, + Space::PciConfig => Answer { made: listed(&self.config).ok_or(Refused::ConfigUnreachable), memory_type: UNLISTED }, + }) + } + + fn lock_take(&self) -> Result { + self.stopping()?; + let take = self.takes.borrow_mut().pop_front().unwrap_or(Take::Taken); + if take == Take::Taken { + assert!(!self.held.replace(true), "a lock already held was taken"); + } + Ok(take) + } + + fn lock_release(&self) -> Result<(), Stopping> { + self.stopping()?; + assert!(self.held.replace(false), "a lock nobody held was given back"); + Ok(()) + } + } + + const NVS: u64 = 0x7700_0000; + + fn machine() -> Scripted { + Scripted { + memory: vec![(NVS, 10, vec![0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), (NVS + 0x2000, 0, vec![0xAB; 4])], + ports: vec![(0xB2, 0x5A)], + config: vec![(pci_address(0, 0, 0x1F, 3, 0x40), 0x1234)], + ..Default::default() + } + } + + #[test] + fn a_read_is_the_kernels_in_each_space_and_counted_by_space_and_page() { + let kernel = machine(); + let mut host = Firmware::new(&kernel); + assert_eq!(host.read(Address::Memory(NVS), toyos_aml::Access::QWord), Ok(0x8877_6655_4433_2211)); + assert_eq!(host.read(Address::Memory(NVS + 2), toyos_aml::Access::Word), Ok(0x4433)); + assert_eq!(host.read(Address::Memory(NVS + 0x2000), toyos_aml::Access::Byte), Ok(0xAB)); + assert_eq!(host.read(Address::Io(0xB2), toyos_aml::Access::Byte), Ok(0x5A)); + let function = Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 3, offset: 0x40 }; + assert_eq!(host.read(function, toyos_aml::Access::Word), Ok(0x1234)); + assert_eq!( + *kernel.asked.borrow(), + [ + Access::read(Space::SystemMemory, NVS, Width::QWord), + Access::read(Space::SystemMemory, NVS + 2, Width::Word), + Access::read(Space::SystemMemory, NVS + 0x2000, Width::Byte), + Access::read(Space::SystemIo, 0xB2, Width::Byte), + Access::read(Space::PciConfig, 0x00FB_0040, Width::Word), + ] + ); + assert_eq!(host.reads, [3, 1, 1]); + assert_eq!(host.pages.by_type(), "1 of type 0, 1 of type 10", "two reads of one page are one page"); + assert!(host.refused.is_empty()); + } + + #[test] + fn a_page_the_firmware_lists_nowhere_is_counted_under_a_name_of_its_own() { + let mut pages = Pages::default(); + pages.read(0xfe00_0110, UNLISTED); + pages.read(0xfe00_0ff0, UNLISTED); + pages.read(NVS, 10); + assert_eq!(pages.by_type(), "1 of type 10, 1 of unlisted firmware memory"); + } + + #[test] + fn a_read_the_kernel_refuses_is_denied_under_the_kernels_name_and_counted() { + let kernel = machine(); + let mut host = Firmware::new(&kernel); + let ram = "a SystemMemory read the kernel refused UsableMemory, in memory of type 7"; + for _ in 0..3 { + assert_eq!(host.read(Address::Memory(0x10_0000), toyos_aml::Access::DWord), Err(Denied(ram.into()))); + } + // A read that runs off the end of what firmware holds is refused whole. + assert_eq!(host.read(Address::Memory(NVS + 4), toyos_aml::Access::QWord), Err(Denied(ram.into()))); + let port = "a SystemIO read the kernel refused KernelPort"; + assert_eq!(host.read(Address::Io(0x70), toyos_aml::Access::Byte), Err(Denied(port.into()))); + let function = Address::PciConfig { segment: 1, bus: 0, device: 0, function: 0, offset: 0 }; + let config = "a PCI_Config read the kernel refused ConfigUnreachable"; + assert_eq!(host.read(function, toyos_aml::Access::DWord), Err(Denied(config.into()))); + assert_eq!(host.refused.counts(), format!("{config} x1; {port} x1; {ram} x4")); + assert_eq!(host.reads, [0, 0, 0], "a refused read is no read made"); + assert_eq!(host.pages.by_type(), "none"); + } + + #[test] + fn nothing_is_written_and_the_controllers_space_is_not_reached() { + let kernel = machine(); + let mut host = Firmware::new(&kernel); + let function = Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 3, offset: 0x40 }; + for (at, space) in [(Address::Memory(NVS), "SystemMemory"), (Address::Io(0xB2), "SystemIO"), (function, "PCI_Config")] { + let denied = host.write(at, toyos_aml::Access::Byte, 0).expect_err("a write was made"); + assert_eq!(denied.0, format!("a write to {space}: this server writes nothing for AML yet")); + } + let controller = Address::EmbeddedControl(0x38); + assert_eq!(host.write(controller, toyos_aml::Access::Byte, 1), Err(Denied(format!("a write to {NO_CONTROLLER}")))); + assert_eq!(host.read(controller, toyos_aml::Access::Byte), Err(Denied(format!("a read of {NO_CONTROLLER}")))); + assert!(kernel.asked.borrow().is_empty(), "the kernel was asked for an access this server denies itself"); + assert_eq!(host.reads, [0, 0, 0]); + } + + #[test] + fn the_lock_is_taken_and_given_back_through_the_kernel() { + let kernel = machine(); + let mut host = Firmware::new(&kernel); + assert_eq!(host.global_lock(true), Ok(())); + assert!(kernel.held.get()); + assert_eq!(host.global_lock(false), Ok(())); + assert!(!kernel.held.get()); + assert_eq!((host.takes, host.contended), (1, 0)); + assert!(host.refused.is_empty()); + } + + #[test] + fn a_lock_the_firmware_holds_is_denied_by_name_and_counted() { + let kernel = machine(); + kernel.takes.borrow_mut().extend([Take::Pending, Take::Pending, Take::Taken]); + let mut host = Firmware::new(&kernel); + for _ in 0..2 { + assert_eq!(host.global_lock(true), Err(Denied(HELD.into()))); + assert!(!kernel.held.get(), "a lock the firmware holds was reported taken"); + } + assert_eq!(kernel.takes.borrow().len(), 1, "a denied take asked the kernel again"); + assert_eq!(host.refused.counts(), format!("{HELD} x2")); + // The firmware let go: the next take is the kernel's answer again. + assert_eq!(host.global_lock(true), Ok(())); + assert_eq!(host.global_lock(false), Ok(())); + assert_eq!((host.takes, host.contended), (3, 2)); + } + + #[test] + fn a_lock_the_kernel_cannot_take_is_denied_and_never_reported_held() { + let kernel = machine(); + kernel.takes.borrow_mut().push_back(Take::Unusable); + let mut host = Firmware::new(&kernel); + assert_eq!( + host.global_lock(true), + Err(Denied("the Global Lock: the kernel exchanges no lock word in this machine's FACS".into())) + ); + assert_eq!(host.contended, 0); + } + + #[test] + fn a_stopping_machine_denies_everything_and_is_no_refusal_of_the_firmwares() { + let kernel = machine(); + kernel.stops_after.set(Some(1)); + let mut host = Firmware::new(&kernel); + assert_eq!(host.read(Address::Memory(NVS), toyos_aml::Access::Byte), Ok(0x11)); + assert!(!host.stopping); + let stopping = Denied(Refusal::Stopping.to_string()); + assert_eq!(host.read(Address::Memory(NVS), toyos_aml::Access::Byte), Err(stopping.clone())); + assert!(host.stopping); + assert_eq!(host.global_lock(true), Err(stopping.clone())); + assert_eq!(host.global_lock(false), Err(stopping)); + assert!(host.refused.is_empty(), "the stop was counted as something this machine's firmware was refused"); + } +} diff --git a/userland/acpiserver/src/ledger.rs b/userland/acpiserver/src/ledger.rs new file mode 100644 index 00000000000..f58af60af6f --- /dev/null +++ b/userland/acpiserver/src/ledger.rs @@ -0,0 +1,45 @@ +//! What the server refuses, each said the first time it is seen and counted +//! after: a machine whose firmware asks the same refused thing a thousand +//! times says so in one line and a number. + +use std::collections::BTreeMap; + +/// Every distinct thing seen, and how often. +#[derive(Default)] +pub struct Ledger(BTreeMap); + +impl Ledger { + /// Count `what`; `true` the first time it is seen, which is when its + /// caller says it. + pub fn see(&mut self, what: &str) -> bool { + let count = self.0.entry(what.to_owned()).or_insert(0); + *count += 1; + *count == 1 + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Each thing seen with its count, in the order of their text. + pub fn counts(&self) -> String { + self.0.iter().map(|(what, count)| format!("{what} x{count}")).collect::>().join("; ") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_thing_is_new_once_and_counted_every_time() { + let mut ledger = Ledger::default(); + assert!(ledger.is_empty()); + assert!(ledger.see("a write to SystemIO")); + assert!(!ledger.see("a write to SystemIO")); + assert!(ledger.see("a read of RAM"), "another thing is new whatever was seen before it"); + assert!(!ledger.see("a write to SystemIO")); + assert!(!ledger.is_empty()); + assert_eq!(ledger.counts(), "a read of RAM x1; a write to SystemIO x3"); + } +} diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index f2207adbdd3..b3e0042d84d 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -13,6 +13,12 @@ //! acknowledged: a press after the clear latches and is served, one //! before it is lost. //! +//! **Then the machine's tables are loaded** ([`aml::load`]), through the +//! kernel's mediated access ([`Claim`]): after the arming, so a press during +//! the load latches and is served when it ends. A table refused, and a DSDT +//! refused, are each said and survived; the power button is served either +//! way. +//! //! **Each SCI** is read off both blocks ([`sci::events`]): a press stops the //! machine through the supervisor, and the controller's GPE drains the //! controller of every query waiting, which are then run, one by one, as @@ -26,7 +32,10 @@ mod aml; mod ec; +mod host; +mod ledger; mod sci; +mod tables; use std::collections::{BTreeMap, VecDeque}; use std::time::{Duration, Instant}; @@ -36,10 +45,11 @@ use toyos::ioport::{in16, in8, out16, out8}; use toyos::poller::{Poller, READABLE}; use toyos::power::{self, Stop}; use toyos::AcpiDev; -use toyos_abi::acpi::{AcpiInfo, Block, FIXED_POWER_BUTTON}; +use toyos_abi::acpi::{Access, AcpiInfo, Block, FIXED_POWER_BUTTON}; use toyos_abi::syscall::{DeviceType, SyscallError}; use ec::{Do, Transaction, Wait}; +use host::{Answer, Kernel, Stopping, Take}; use sci::{Event, Served, Unserved, PM1_STATUS, PWRBTN}; /// The most a controller is waited for at one step of a transaction: one that @@ -86,9 +96,44 @@ fn main() { empty: 0, }; server.arm(); + aml::load(&Claim(&server.dev), server.info.rsdp); server.serve(); } +/// The claim as the tables' fetch and their AML ask it for what lies outside +/// its own ports. +struct Claim<'a>(&'a AcpiDev); + +impl Claim<'_> { + /// The kernel's answer; a stopping machine's is the caller's to carry, + /// and any other refusal of a call this server formed is this server's + /// defect. + fn answered(asked: &str, answer: Result) -> Result { + match answer { + Ok(answer) => Ok(answer), + Err(SyscallError::Gone) => Err(Stopping), + Err(other) => panic!("acpiserver: the kernel answered {asked} {other:?}"), + } + } +} + +impl Kernel for Claim<'_> { + fn access(&self, access: Access) -> Result { + Self::answered("a mediated access", self.0.access(access)).map(|(made, memory_type)| Answer { made, memory_type }) + } + + fn lock_take(&self) -> Result { + match self.0.lock_take() { + Err(SyscallError::NotSupported) => Ok(Take::Unusable), + answer => Self::answered("a take of the Global Lock", answer).map(|taken| if taken { Take::Taken } else { Take::Pending }), + } + } + + fn lock_release(&self) -> Result<(), Stopping> { + Self::answered("the Global Lock's release", self.0.lock_release()) + } +} + /// Each byte of a status-and-enable block: its status port and its enable port. fn bytes(block: Block) -> impl Iterator { (0..block.len / 2).map(move |i| (block.port + i, block.enable() + i)) @@ -224,7 +269,7 @@ impl Server { *count += 1; self.counted += 1; if *count == 1 { - println!("acpiserver: embedded controller query {q:#04x} taken for the first time, served by nothing: stage 1 runs no AML"); + println!("acpiserver: embedded controller query {q:#04x} taken for the first time, served by nothing: no query's method is evaluated yet"); } } } diff --git a/userland/acpiserver/src/tables.rs b/userland/acpiserver/src/tables.rs new file mode 100644 index 00000000000..5db9c25807c --- /dev/null +++ b/userland/acpiserver/src/tables.rs @@ -0,0 +1,182 @@ +//! The machine's tables, fetched through the kernel into this process: +//! [`Tables`] is the physical memory [`toyos_acpi`] decodes from, read one +//! mediated access at a time ([`crate::host::Kernel`]) and kept, so a table +//! is checked and loaded from bytes this server holds. +//! +//! [`Phys::readable`] is where a range is fetched, whole or not at all, and +//! [`Phys::byte`] reads what was fetched: the decoder asks for every range +//! before it reads one, which is the trait's contract. A range the kernel +//! refuses is not readable, and why is kept by its address for whoever logs +//! the table ([`Tables::refused`]). Address zero is no table's. + +use std::cell::{Cell, RefCell}; + +use toyos_abi::acpi::{Space, Width}; +use toyos_acpi::Phys; + +use crate::host::{self, Kernel, Pages, Refusal}; +use crate::ledger::Ledger; + +pub struct Tables<'k, K> { + kernel: &'k K, + /// Every range fetched, at its address. + held: RefCell)>>, + pub reads: Cell, + pub pages: RefCell, + /// Each range that was not readable, by where it begins, and why. + refused: RefCell>, + /// The kernel answered that the machine is stopping. + pub stopping: Cell, +} + +impl<'k, K: Kernel> Tables<'k, K> { + pub fn new(kernel: &'k K) -> Self { + Tables { + kernel, + held: RefCell::new(Vec::new()), + reads: Cell::new(0), + pages: RefCell::new(Pages::default()), + refused: RefCell::new(Vec::new()), + stopping: Cell::new(false), + } + } + + /// Why the range that begins at `phys` was not readable, the last time + /// it was asked for. + pub fn refused(&self, phys: u64) -> Option { + self.refused.borrow().iter().rev().find(|(at, _)| *at == phys).map(|&(_, why)| why) + } + + /// Every refusal of a range, counted by what it says. + pub fn refusals(&self) -> Ledger { + let mut ledger = Ledger::default(); + for (_, why) in self.refused.borrow().iter() { + ledger.see(&why.to_string()); + } + ledger + } + + /// Why the last range that was not readable was not. + pub fn last_refused(&self) -> Option { + self.refused.borrow().last().map(|&(_, why)| why) + } + + /// `len` bytes at `phys`, in qwords and then in bytes, so no read reaches + /// past the range asked for into memory of another type. + fn fetch(&self, phys: u64, len: usize) -> Result, Refusal> { + let mut bytes = Vec::with_capacity(len); + while bytes.len() < len { + let width = if len - bytes.len() >= 8 { Width::QWord } else { Width::Byte }; + let at = phys + bytes.len() as u64; + let (value, memory_type) = host::read(self.kernel, Space::SystemMemory, at, width)?; + self.reads.set(self.reads.get() + 1); + self.pages.borrow_mut().read(at, memory_type); + bytes.extend_from_slice(&value.to_le_bytes()[..width.bytes() as usize]); + } + Ok(bytes) + } +} + +impl Phys for &Tables<'_, K> { + fn readable(self, phys: u64, len: usize) -> bool { + let Some(end) = phys.checked_add(len as u64) else { return false }; + if phys == 0 { + return false; + } + if self.held.borrow().iter().any(|(base, bytes)| *base <= phys && end <= base + bytes.len() as u64) { + return true; + } + match self.fetch(phys, len) { + Ok(bytes) => { + self.held.borrow_mut().push((phys, bytes)); + true + } + Err(refusal) => { + self.stopping.set(self.stopping.get() || refusal == Refusal::Stopping); + self.refused.borrow_mut().push((phys, refusal)); + false + } + } + } + + fn byte(self, phys: u64) -> u8 { + let held = self.held.borrow(); + // The newest first: a table's whole range is fetched after its header's. + let byte = held.iter().rev().find_map(|(base, bytes)| phys.checked_sub(*base).and_then(|at| bytes.get(at as usize))); + *byte.expect("acpiserver: the table decoder read a byte it never asked for, against `toyos_acpi::Phys`'s contract") + } +} + +#[cfg(test)] +mod tests { + use toyos_abi::acpi::{Access, Refused}; + + use super::*; + use crate::host::tests::Scripted; + + const AT: u64 = 0x7fb0_0000; + const KEPT: u64 = 0x7000_0000; + + fn machine() -> Scripted { + Scripted { memory: vec![(AT, 9, (0..=40u8).collect())], kept: vec![(KEPT, KEPT + 0x1000, 5)], ..Default::default() } + } + + #[test] + fn a_range_is_fetched_whole_in_qwords_and_then_bytes_and_kept() { + let kernel = machine(); + let tables = &Tables::new(&kernel); + assert!(tables.readable(AT + 1, 19)); + let read = |at, width| Access::read(Space::SystemMemory, at, width); + assert_eq!( + *kernel.asked.borrow(), + [read(AT + 1, Width::QWord), read(AT + 9, Width::QWord), read(AT + 17, Width::Byte), read(AT + 18, Width::Byte), read(AT + 19, Width::Byte)], + "a qword that would have reached past the range was asked for" + ); + assert_eq!((0..19).map(|i| tables.byte(AT + 1 + i)).collect::>(), (1..=19u8).collect::>()); + assert_eq!(tables.reads.get(), 5); + assert_eq!(tables.pages.borrow().by_type(), "1 of type 9"); + + // What is held is not fetched again; what reaches past it is. + assert!(tables.readable(AT + 4, 8)); + assert_eq!(kernel.asked.borrow().len(), 5); + assert!(tables.readable(AT + 4, 20)); + assert_eq!(kernel.asked.borrow().len(), 5 + 2 + 4); + assert_eq!(tables.byte(AT + 23), 23); + assert_eq!(tables.last_refused(), None); + } + + #[test] + fn a_range_the_kernel_refuses_any_byte_of_is_not_readable_and_says_why() { + let kernel = machine(); + let tables = &Tables::new(&kernel); + // The last byte is past what the firmware holds here. + assert!(!tables.readable(AT + 32, 10)); + let ram = Refusal::Kernel { space: Space::SystemMemory, refused: Refused::UsableMemory, memory_type: 7 }; + assert_eq!(tables.refused(AT + 32), Some(ram)); + assert!(tables.readable(AT + 32, 9)); + // Memory of a type the kernel passes no read of, refused under that + // name and type, each range by its own address. + assert!(!tables.readable(KEPT + 8, 36)); + let kept = Refusal::Kernel { space: Space::SystemMemory, refused: Refused::MemoryType, memory_type: 5 }; + assert_eq!((tables.refused(KEPT + 8), tables.refused(AT + 32), tables.refused(AT)), (Some(kept), Some(ram), None)); + assert_eq!(tables.last_refused(), Some(kept)); + assert_eq!(tables.refusals().counts(), format!("{kept} x1; {ram} x1")); + assert!(!tables.stopping.get()); + + // Address zero and a range that wraps are no table's, and the kernel is not asked. + let asked = kernel.asked.borrow().len(); + assert!(!tables.readable(0, 36)); + assert!(!tables.readable(u64::MAX - 3, 36)); + assert_eq!(kernel.asked.borrow().len(), asked); + } + + #[test] + fn a_stopping_machine_reads_no_table() { + let kernel = machine(); + kernel.stops_after.set(Some(2)); + let tables = &Tables::new(&kernel); + assert!(!tables.readable(AT, 36)); + assert!(tables.stopping.get()); + assert_eq!(tables.refused(AT), Some(Refusal::Stopping)); + } +}