From 5596d7b7e690c7fec81bd28b008d75b6e50c1f3d Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 20:48:50 +0200 Subject: [PATCH 1/2] acpi: the ACPI server is trusted platform code; the kernel makes the firmware's writes inside bug fences Under the trust model (#907), the holder of the `acpi` claim, granted by name to /system/bin/acpiserver in the signed image's manifest, runs the firmware's AML, and the firmware already holds the machine. A check on its accesses is a bug fence, kept where it is cheap, and never withholds a feature for want of a vendor document. Before this change the kernel refused every write at an address the firmware's map lists nowhere and every configuration write, which refused the T14's controller and hotkey `_INI`s and the AMD bench's controller `_REG(3, 1)` and adapter `_PSR`. Memory. A write passes wherever a read passes: an unlisted address or `EfiMemoryMappedIO` memory is a register where it is at or above 1 MiB, inside the direct map and typed uncacheable by range registers every CPU shares. RAM, the tables, the FACS, the local APIC, every window the kernel drives and every BAR stay refused, now including each BAR window `pcidev` cut since boot, which the old comment wrongly called refused by its type. On the five host bridges Intel 631122 covers (631121-012 Table 15-3), a write overlapping a register the kernel keeps in MCHBAR is refused `KernelRegister`: GFXVTBAR and VTDPVC0BAR (where the VT-d units decode), INTRDIRCTL (interrupt redirection) and PACKAGE_RAPL_LIMIT (the performance track's, by the orchestrator's standing ruling). The kernel reads 00:00.0's identity and MCHBAR at the ACPI row's init. Configuration space. A write passes through ECAM except: anywhere in a function a kernel driver drives or a claim holds, or of class 08h subclass 00h or 06h (`HeldFunction`); any function's header 00h-3Fh, a covered host bridge's 40h-BFh address map, the 500 Series eSPI controller's ESPI_IOD_IOE (COM1's and the i8042's decode), and each register `toyos_tco` names as placing the TCO block (`AddressMap`). `ConfigWrite` is gone. The class rule lives in the policy, which `pcidev::enumerated` feeds. The server. A `KernelRegister` refusal is answered as written, said once per register, and the evaluation goes on: AML has no notion of a refused write, and the T14's `DYTC` records its mode in NVS after the refused PL1 write. Every other refusal still denies. The server says the first write the kernel made into each unlisted or memory-mapped I/O page and into each function, and now passes PCI_Config writes to the kernel. Issues. The two bench issues close: their methods now run. The AMD issue's exit asked for a mediation by an AMD register map; the trust model retires that exit rather than meeting it. The claim holder's issue is taken up and closed: its exit's second arm, a ruling that the address bound is the one ToyOS keeps, is the trust model; its durable line is the policy's header ("a write the AML asks for is made unless a fence refuses it"), and every weakness it carried that is still true moves to the new the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md with the trust model's residuals, ITSS on the sideband first (the orchestrator's ruling 1 on #906). The thermal modes' PL1/PL2 are filed as the-t14s-thermal-modes-do-not-move-its-package-power-limits.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- Cargo.lock | 1 + images/console.toml | 2 +- images/system.toml | 7 +- ...ses-and-the-kernel-bounds-only-the-call.md | 4 +- ...rmware-range-the-kernel-did-not-declare.md | 164 -------- ...-reach-itss-and-registers-no-fence-sees.md | 81 ++++ ...-for-a-write-to-the-root-complexs-index.md | 55 --- ...-are-refused-for-writes-outside-the-map.md | 68 ---- ...es-do-not-move-its-package-power-limits.md | 30 ++ ...ine-in-acpi-mode-and-interprets-its-aml.md | 10 +- kernel/src/arch/x86_64/acpi_mode.rs | 74 ++-- kernel/src/pcidev/mod.rs | 38 +- lib/acpiserver-api/src/lib.rs | 11 + lib/userbound/Cargo.toml | 3 + lib/userbound/src/firmware.rs | 244 +++++++++--- lib/userbound/tests/firmware.rs | 374 ++++++++++++++---- sdk/abi/src/acpi.rs | 42 +- system/acpiserver/src/host.rs | 148 +++++-- system/acpiserver/src/init.rs | 47 +++ .../guest/rust-tests/src/bin/acpi_mediated.rs | 67 +++- tests/toyos.rs | 164 ++++---- 21 files changed, 991 insertions(+), 643 deletions(-) delete mode 100644 issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.md create mode 100644 issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md delete mode 100644 issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.md delete mode 100644 issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md create mode 100644 issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md diff --git a/Cargo.lock b/Cargo.lock index 3285301c15..9cdfdd4a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4509,6 +4509,7 @@ dependencies = [ "toyos-abi", "toyos-acpi", "toyos-bootmap", + "toyos-tco", ] [[package]] diff --git a/images/console.toml b/images/console.toml index 5b39e07a2f..8a8ccca377 100644 --- a/images/console.toml +++ b/images/console.toml @@ -35,7 +35,7 @@ serves = ["log"] # The machine's ACPI fixed hardware, as `images/system.toml` serves it: `shutdown` # powers this machine off with the sleep type this program reads out of the # firmware's tables and hands the kernel, which refuses a power-off without -# one. +# one. The claim hands it the firmware's own access, as there. [programs.acpiserver] service = true devices = ["acpi"] diff --git a/images/system.toml b/images/system.toml index 8949e71ce8..2a172ca2e1 100644 --- a/images/system.toml +++ b/images/system.toml @@ -58,8 +58,11 @@ serves = ["log"] # The machine's ACPI fixed hardware: the kernel puts the machine in ACPI mode # when it mints the `acpi` claim and back when the claim goes, so the SCI, # the power button and the embedded controller's events are this program's -# while it runs and the firmware's otherwise. `power` is how a press stops -# the machine: through the supervisor, which has `logkeeper` make the log whole. +# while it runs and the firmware's otherwise. The claim also hands it the +# firmware's own access: the kernel makes the AML's reads and writes for it, +# inside the fences of `toyos_userbound::firmware`. `power` is how a press +# stops the machine: through the supervisor, which has `logkeeper` make the +# log whole. [programs.acpiserver] service = true devices = ["acpi"] diff --git a/issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md b/issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md index bf7ea7a59d..f976e60481 100644 --- a/issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md +++ b/issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md @@ -12,7 +12,7 @@ the firmware, which the kernel makes for it `toyos_userbound::firmware::port`). The byte selects a handler that runs in system management mode, above the kernel, and reads whatever the holder wrote to firmware's memory before the call, which the mediated access passes both -ways (`issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.md`). +ways (`issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md`). So a bug in `/system/bin/acpiserver`, or AML it runs, reaches whatever the machine's firmware does on any byte with any argument block. @@ -36,7 +36,7 @@ What the kernel bounds: else that raises a firmware interrupt: the chipset's own SMI enable register sits at a port no table names and nothing declared, so the holder writes it as it writes any port - (`issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.md`). + (`issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md`). Eight a second is no bound on the firmware interrupts a holder can ask for. 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 deleted file mode 100644 index f6a68b771e..0000000000 --- a/issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -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 -(`lib/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; a byte for it is the kernel's to write, on the boot processor - and under its bounds, only where the FADT names the port - (`issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md`). - -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/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md b/issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md new file mode 100644 index 0000000000..1904fe255e --- /dev/null +++ b/issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md @@ -0,0 +1,81 @@ +--- +status: open +kind: defect +opened: 2026-10-13 +--- + +# The ACPI server's firmware writes reach ITSS and registers no fence sees + +The kernel makes the firmware's AML's reads and writes for the holder of the +`acpi` claim, `/system/bin/acpiserver`, unless a fence refuses one +(`toyos_userbound::firmware`, `kernel/src/arch/x86_64/acpi_mode.rs`). The +server is trusted platform code, granted by name in the signed image's +manifest, and each fence is a bug fence kept where it is cheap. These are +what the fences do not see: a bug in the server or the AML it runs reaches +them. + +- **The P2SB sideband, the ITSS among it** (Intel 631119-007 Table 109, port + C4h): the 8259 and I/O APIC routing and polarity. Fencing it needs + SBREG_BAR, which the T14's firmware hides behind `P2SBC.HIDE` (631120-002 + §3.1.22): a configuration write to unhide the bridge, open to an SMI taken + between it and the restore. Ruled unfenced on cost (orchestrator, + 2026-10-13, #906). +- **AMD's SMN behind 00:00.0's 0xB8 and 0xBC.** No AMD document held maps + it on Family 17h Model 60h (PPR 55922-A1); on sibling parts it reaches the + IOMMU's, the data fabric's and the memory controllers' registers. +- **An unheld function from 40h**: its MSI and MSI-X capabilities, its PM + D-state, and in extended space SR-IOV and resizable BAR, which move decode + the kernel did not record. +- **The PCH's eSPI controller and SMBus function from 40h, but what places a + declared block**: on the 500 Series, 00:1F.0's generic I/O and memory + ranges (`ESPI_LGIR1`–`4` 84h–93h, `ESPI_LGMR` 98h), its BIOS decode enable + and BIOS control (D8h, DCh) (631120-002 §2.1), and 00:1F.4's host + configuration `HCFG` (40h) and power gating `SMBSM` (80h) (§6.1). + `ESPI_IOD_IOE` and the TCO base and control are fenced. +- **Registers at an address the firmware's map lists nowhere**, below every + BAR and driven window: the PMC's and the FCH's power gating of devices a + driver holds, and the SPI flash controller's registers. +- **A GPIO output pad wired to a held device's line**, until a driver holds + the GPIO controller, whose window is then a claimed one. +- **An Intel host bridge 631122 does not cover**: its PL1 and PL2, interrupt + redirection, VT-d unit bases and address map pass there. +- **A port the kernel did not declare and no row names**, both ways: an I/O + BAR of a PCI function, a kernel driver's or a claim holder's, since the + kernel records memory BARs only; and a chipset register no table names. + A declared block moved through an undeclared index and data pair leaves + its registers on ports nothing declared. +- **A port the firmware traps**: an SMI the access raises is a stay in SMM, + made with the mediation's lock held, which the kernel neither bounds nor + counts; and on a machine whose FADT names no `SMI_CMD`, the chipset's + command port is a port like any other + (`issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md`). +- **The range registers are not the effective type everywhere.** A + processor that types RAM above 4 GiB write-back outside them (AMD's + `SYSCFG` and `TOM2`) answers their default type for it, uncacheable on + such firmware: unlisted RAM above 4 GiB is accessed there as a register. + No usable range holds it, so the allocator never handed it out. +- **Nothing makes every CPU's range registers the boot processor's.** Where + one differs every unlisted access is refused `RangeRegistersDiffer`; on + QEMU 11.1.1 under TCG the second CPU reads them off, which every one of + its accesses then makes uncached. Who owns the other CPUs' range + registers, firmware or the kernel, is the owner's to rule; why that + guest's second CPU has them off is unread. +- **A register's read can act in the device**, a status bit cleared or a + FIFO advanced: the kernel bounds where the holder accesses, not what the + access does there. +- **A claim minted between a decision and its access** is one the access was + made a moment before: the records are read under their own locks and let + go before the access. +- **The firmware locking 5F50h or 5F54h until reset**, which the T14's + `CTCN` writes and the kernel passes. + +Owner: the AML track +(`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`). + +## Exit condition + +Each item is fenced once fencing it is cheap, or the orchestrator rules it +a residual of the trust model and moves it to the policy's module header; +and the owner rules who owns the other CPUs' range registers, the kernel +then programming and asserting them, or this issue recording that it never +will. diff --git a/issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.md b/issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.md deleted file mode 100644 index d17fccef13..0000000000 --- a/issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-13 ---- - -# The AMD bench's controller `_REG` and adapter `_PSR` are refused for a write to the root complex's index - -On the AMD bench the server's evaluation of the embedded controller's -`_REG(3, 1)` and of the AC adapter's `_PSR` are each refused `ConfigWrite`: -both reach AMD's `ALIB` method in SSDT1, whose mailbox (`A011`) first writes -a DWord through a SystemMemory region over the ECAM window to function -00:00.0, offset 0xB8, and then reads and writes offset 0xBC as `BankField`s -selected by the value written there (ACPI 6.5 §19.6.7). The kernel refuses -every configuration write the claim's holder asks -(`toyos_userbound::firmware::config`). - -**Ruled** (owner's orchestrator, 2026-10-12, #875 comment 6131125468): the -write stays refused. 00:00.0 offset 0xB8 is the root complex's index, and -through it AML reaches every register of the SoC, the IOMMU's configuration -among them; passing it, or every write past the standard header of a -function nothing claims, would put the server in the trusted base, against -"isolation is non-negotiable". - -**What it costs**, read on the bench at #875's `b3bfc9742` -(`ToyOSOrg/evidence` `pr-875`, `round3/amd-acpi_tables_loaded.log`, and -the terms by offset in `round3/amd-aml-chain.txt`): - -- `_REG(3, 1)` is refused after it has stored `OKEC` and `BASC` and - notified the battery, at its call of `^VPC0.NDPC`, so the census reads - `no _REG run` and whatever `NDPC` would tell the firmware of the - controller is not told; -- `_PSR` is refused at its `ALIB (One, …)`, so the adapter's state is - absent: the battery reads `AC unknown`; -- each later evaluation that reaches `ALIB` is refused the same way. The - battery's own `_BIX` and `_BST` are read. - -**What AMD documents.** AMD's PPR for Family 17h Model 60h (55922-A1 -rev 3.06) names 00:00.0, vendor 1022h device 1630h, the Root Complex -(§1.8.1, Table 13), the function the bench's kernel enumerates there, and -documents no register at its offsets 0xB8 or 0xBC nor anything the values -the mailbox writes to 0xB8 select: its register namespaces are the cores', -IO, MCA, SBTSI and SMU::THM alone (List of Namespaces; Memory Map - Main -Memory). So no documented register map exists yet to mediate that index by. - -Owner: the AML track -(`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`). - -## Exit condition - -A design the owner accepts that mediates writes through 00:00.0's 0xB8 and -0xBC by a register map AMD documents, built, with the AMD bench's -`acpi_tables_loaded` reading `_REG(3, 1)` run and `_PSR` answered, and the -expectation in `tests/toyos.rs` moved to it. Until then the refusal stays, -and the row expects it. diff --git a/issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md b/issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md deleted file mode 100644 index 4cfb3793bb..0000000000 --- a/issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-13 ---- - -# The T14's controller `_INI` and hotkey `_INI` are refused for writes outside the map - -On the T14 the server's namespace walk has two `_INI`s refused `MemoryType`, -"a SystemMemory write the kernel refused MemoryType, in unlisted firmware -memory": the embedded controller's (`\_SB.PC00.LPCB.EC._INI`) and its -hotkey device's (`EC.HKEY._INI`). The kernel refuses every write the claim's -holder asks at an address the firmware's memory map does not list -(`toyos_userbound::firmware`). - -**Ruled** (owner, 2026-10-12, #875): a memory write outside every range the -map describes is refused by name and recorded, not admitted. Ruled again on -the two writes read below (#875, comment 6135767889): both stay refused -under that ruling, since a write outside the firmware's map reaches a device -another driver may own, the GPIO community for the pad and MCHBAR for the -hotkey device's, so isolation decides. The T14 row expects both `_INI`s -refused by path and the census's count of two (`T14` in `tests/toyos.rs`). - -**The controller's write**, read on the T14 at #875's `ddc243eaf` and -reproduced on the host by the T14's own tables (`ToyOSOrg/evidence` -`pr-875`, `round5/`): `EC._INI` calls `HKEY.WGIN`, which stores what the -firmware's SMI answers for `WGSV(1)` and, where `WIN8` is set and that -answer is not zero, calls `BPWC(1)`; `BPWC` calls `\_SB.SGOV(0x0902000D, 1)` -where the answer has bit 4 set and bit 7 clear. That is byte 0x01 at -0xFD6E0A70: `\SBRG` (SBREG_BAR, 0xFD000000) + Port ID 0x6E << 16 + 0xA70, -which Intel's 500 Series Chipset On-Package PCH datasheet vol. 2 -(631120-002) §27.1.114 names `PAD_CFG_DW0_GPPC_A_13` of GPIO Community 0, -bit 0 `GPIOTXSTATE`, "drive a level '1' to the TX output pad". The window -0xFD6E0000 + 64 KiB is one of four the GPIO controller `\_SB.GPI0` -(`INT34C5`) lists in its `_CRS`, and each holds every pad's configuration -of its community with `PAD_OWN`, `PADCFGLOCK`, `HOSTSW_OWN` and the GPE -group assignment (§27.1.1–27.1.19); `PAD_OWN` 00 is "no read/write -restriction to the Pad Configuration register set during host ownership". - -**The hotkey device's write** is not read on the T14: the server details a -kind's first refusal only. On the host the T14's tables reach it by -`HKEY._INI` → `DYTC` → `SCPF` → `\_SB.PC00.PL1S` → `CTCN`, which writes -bytes at MCHBAR + 0x5F50, 0x5F54, 0x59A0 and 0x59A1, where MCHBAR is what -00:00.0 offset 0x48 holds (the host run assumed 0xFEDC0000). Intel's 11th -Generation Core processor datasheet vol. 2 (631122) is the document for -those offsets; it is fetched and unread, its PDF encrypted against the text -extractor tried. - -**What it costs**: - -- `EC._INI` is refused inside `WGIN`, so the pad stays as the firmware - left it, `WGIN`'s flags miss bit 5, and nothing after `WGIN` runs: on - the host, under the values that run answered, that is a read of the - controller's bytes 0x49 and 0x3A and a write of 0x3A with bit 5 set; -- `HKEY._INI` is refused inside `DYTC`, and whatever comes after its - refused write is not done; -- the battery and the adapter are read regardless (`battery_read`). - -Owner: the AML track -(`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`). - -## Exit condition - -The hotkey device's refused write is read on the T14, and either a design -the owner accepts mediates these writes by Intel's register maps, built, -with the T14's `acpi_tables_loaded` reading both `_INI`s run and the -expectation in `tests/toyos.rs` moved to it, or the refusal stays and the -row expects it. diff --git a/issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md b/issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md new file mode 100644 index 0000000000..d627ad194f --- /dev/null +++ b/issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md @@ -0,0 +1,30 @@ +--- +status: open +kind: defect +opened: 2026-10-13 +--- + +# The T14's thermal modes do not move its package power limits + +The T14's hotkey device's `_INI` reaches Lenovo's thermal mode method +(`HKEY._INI` → `DYTC` → `SCPF` → `\_SB.PC00.PL1S` → `CTCN`), which writes +PL1 and PL2 as bytes of MCHBAR + 59A0h, `PACKAGE_RAPL_LIMIT` (Intel 631122 +§3.3.31). That register is the kernel's: the performance track declares it +(`issues/the-kernel-owns-cpu-performance-state.md`, RAPL), so the kernel +refuses the write `KernelRegister` and the ACPI server answers it as made and +goes on (`toyos_userbound::firmware`, `system/acpiserver/src/host.rs`). The +method's other writes, the cTDP control at 5F50h and the turbo activation +ratio at 5F54h, are made, and `DYTC` records its mode in NVS. + +So whatever mode the firmware selects, its PL1 and PL2 do not follow: the +firmware believes the limits moved, and the package runs under whatever the +kernel or the firmware's boot left in the register. + +Owner: the performance track (the orchestrator). + +## Exit condition + +The performance track gives a firmware mode a way to move the kernel's +declared limits, and a T14 row reads PL1 back at the mode's value after +`DYTC`; or the owner rules that ToyOS does not follow the firmware's thermal +modes, and the T14's row says so. 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 a6e4d1600e..3a1ceaae8a 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 @@ -491,16 +491,10 @@ ACPI Interface Specification (302223-007) for a processor's `_OSC` on Intel: deciding what each reading means: - every block loads but the AMD bench's SSDT8, refused whole (`issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md`); - - no method is refused but the AMD bench's `_REG(3, 1)` and `_PSR` - (`issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.md`) - and the T14's controller and hotkey `_INI` - (`issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md`), - each expected by its bench by name; + - no method is refused; - each block a processor's `_OSC` loads under ToyOS's claims is loaded; - each `_STA` the walk reads is said, and each battery reading is read - whole by §6.3.7 and §10.2.2, with no value unknown but the AMD bench's - adapter, `AC unknown` where its ruled `_PSR` refusal leaves the adapter's - state absent. + whole by §6.3.7 and §10.2.2, with no value unknown. Not in finished: diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index 579d3ed614..53c734df4f 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -26,18 +26,18 @@ //! //! **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 +//! through the direct map, a port, and a function's configuration space +//! through ECAM, 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, or in memory it types memory-mapped I/O, 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. +//! not list, or in memory it types memory-mapped I/O, is accessed 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 access is made on whichever +//! CPU the call runs on. //! //! **A byte the holder's AML stores to `SMI_CMD` is a call into the firmware, //! made here and by nobody else** ([`call`]): the policy answers which byte, @@ -83,7 +83,7 @@ use toyos_abi::acpi::{Access, AcpiInfo, Block, Refused, Space, Width, FIXED_POWE use toyos_abi::syscall::SyscallError; use toyos_acpi::{EcamWindow, FixedHardware, LegacyMode, PowerButton}; use toyos_userbound::firmware::{ - self, CallRate, FirmwareCall, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict, + self, CallRate, FirmwareCall, Function as PciFunction, HostBridge, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict, }; use toyos_userbound::Ports; @@ -118,6 +118,7 @@ struct Hardware { rsdp: u64, /// The windows configuration space is reached through, as the MCFG bounds them. ecam: Vec, + host_bridge: HostBridge, lock: GlobalLock, } @@ -172,8 +173,8 @@ struct Calls { /// decision to the last instruction of the act: each mediated access, and /// each change of the lock word. Taken with the claim's own lock held /// (`object::Held`), and `pcidev`'s machine record and then `paging`'s record -/// of windows under it; nothing holding one of those two takes this or a -/// claim's. +/// of windows, or `pcidev`'s slots, under it; nothing holding one of those +/// takes this or a claim's. static HOLDER: Lock = Lock::new(Holder { locked: false, supplied: false, @@ -269,7 +270,7 @@ pub fn init(rsdp_addr: u64, facs: Option) -> Globa GlobalLock::At(Facs { span: (facs.base, facs.base + u64::from(facs.len)), word }) } +/// What 00:00.0 says it is, and its quadword at 48h, read as two dwords. +fn host_bridge() -> HostBridge { + let Some(space) = crate::drivers::pci::function_window(0, 0, 0) else { return HostBridge::ABSENT }; + let at_48h = u64::from(space.read_u32(0x48)) | u64::from(space.read_u32(0x4C)) << 32; + HostBridge { vendor: space.read_u16(0), device: space.read_u16(2), at_48h } +} + fn run(block: Block) -> Ports { Ports::new(block.port, block.len).expect("`fixed_hardware` bounded every block by the port space") } @@ -610,7 +618,8 @@ fn read_memory(passed: &MemoryAt) -> u64 { 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. + // own memory or a register, outside its tables, the FACS, every device the + // kernel knows decodes and every register it keeps. unsafe { match passed.width() { Width::Byte => at.as_mut_ptr::().write_volatile(value as u8), @@ -645,19 +654,33 @@ fn write_port(passed: &PortAt, value: u64) { } } -/// 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)?; +/// One configuration access, decided and made through the window the MCFG +/// names, which the policy bounded the function by. A write is decided on +/// what `pcidev` records of the function, read under its own locks and let +/// go before the write: a claim minted after the decision is one the write +/// was made a moment before. +fn config(hardware: &Hardware, _acting: &Holder, segment: u16, function: PciFunction, offset: u16, width: Width, write: Option) -> Result { + let at = firmware::config(&hardware.ecam, segment, function, offset, width)?; let PciFunction { bus, device, function } = at.function(); let space = crate::drivers::pci::function_window(bus, device, function).expect("a 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)), + let Some(value) = write else { + let offset = u64::from(at.offset()); + return Ok(match at.width() { + Width::Byte => u64::from(space.read_u8(offset)), + Width::Word => u64::from(space.read_u16(offset)), + Width::DWord => u64::from(space.read_u32(offset)), + Width::QWord => unreachable!("the policy passes no qword configuration access"), + }); + }; + let passed = at.write(crate::pcidev::enumerated(bus, device, function))?.at(); + let offset = u64::from(passed.offset()); + match passed.width() { + Width::Byte => space.write_u8(offset, value as u8), + Width::Word => space.write_u16(offset, value as u16), + Width::DWord => space.write_u32(offset, value as u32), Width::QWord => unreachable!("the policy passes no qword configuration access"), - }) + } + Ok(0) } /// Whether a read of `len` bytes at `at` through the direct map is uncached: @@ -689,6 +712,7 @@ fn memory(hardware: &Hardware, acting: &Holder, request: &mut Access, width: Wid facs: hardware.lock.facs().map(|facs| facs.span), uncached, registers_differ: super::mtrr::any_differs(), + host_bridge: hardware.host_bridge, }; memory.decide(at, width, write.is_some()) }) @@ -700,7 +724,7 @@ fn memory(hardware: &Hardware, acting: &Holder, request: &mut Access, width: Wid Ok(0) } (MemoryVerdict::AsConfig(function, offset), _) => { - config(hardware, acting, toyos_acpi::SEGMENT_GROUP, function, offset, width, write.is_some()) + config(hardware, acting, toyos_acpi::SEGMENT_GROUP, function, offset, width, write) } (MemoryVerdict::Refused(refused), _) => Err(refused), } @@ -782,7 +806,7 @@ pub fn access(row: &isa::Row, request: &mut Access) -> Result<(), SyscallError> 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()).map_err(Unmade::from), + 0 => config(hardware, &acting, (at >> 32) as u16, function, at as u16, width, write).map_err(Unmade::from), _ => Err(Refused::ConfigUnreachable.into()), } } diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index b52173a1dc..b3f286ce24 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -372,9 +372,9 @@ static MACHINE: Lock = Lock::new(Machine { /// `one_claimant_per_device` compares `system.toml` strings, and a host-side /// gate is not the capability boundary. /// -/// **Taken alone**: nothing is held while this is, and it is held across -/// nothing — `reserve` runs after `claim` has dropped `MACHINE`, and `release` -/// takes it once the teardown is over. +/// **Held across nothing**, and taken with nothing of this module's held — +/// `reserve` runs after `claim` has dropped `MACHINE`, and `release` takes it +/// once the teardown is over — so it may be taken under any lock outside it. static SLOTS: Lock<[Slot; MAX_FUNCTIONS]> = Lock::new([Slot::Free; MAX_FUNCTIONS]); /// One claim's hold on a slot, from its reservation to the release its drop @@ -429,16 +429,32 @@ 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. +/// What the kernel enumerated at a function, for the `acpi` claim's holder's +/// configuration write (`toyos_userbound::firmware::ConfigAt::write`): `None` +/// where nothing answered. Held where a kernel driver drives it or a claim +/// holds it. +pub fn enumerated(bus: u8, dev: u8, func: u8) -> Option { + let who = ((bus as u16) << 8) | ((dev as u16) << 3) | func as u16; + let (pci, driven) = { + let machine = MACHINE.lock(); + let pci = *machine.identities.iter().find(|pci| requester_of(pci.at) == who)?; + (pci, machine.kernel_driven.contains(&who)) + }; + let held = driven || SLOTS.lock().contains(&Slot::Held(who)); + Some(toyos_userbound::firmware::Enumerated { vendor: pci.vendor, device: pci.device, class: pci.class, subclass: pci.subclass, held }) +} + +/// Every range a memory BAR of an enumerated function decodes, as `(start, +/// end)`: where firmware left it, and each window this module cut for one +/// since. What the `acpi` claim's holder is refused +/// (`toyos_userbound::firmware`), whoever drives the function. `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))) + let firmware = machine.decoded.iter().map(|&(_, start, end)| (start, end)); + let cut = machine.windows.iter().map(|&(_, _, at, span)| (at, at + span)); + f(&mut firmware.chain(cut)) } /// The PCI segment group every enumerated function is on. diff --git a/lib/acpiserver-api/src/lib.rs b/lib/acpiserver-api/src/lib.rs index 81d8372b17..fe9d47bbbd 100644 --- a/lib/acpiserver-api/src/lib.rs +++ b/lib/acpiserver-api/src/lib.rs @@ -32,3 +32,14 @@ pub const BATTERY_READ: &str = "battery read: "; /// 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 first write to each register the kernel keeps for itself, which the +/// server answered as written: the write itself is the next line, under +/// [`OWN`]. +pub const KEPT_WRITE: &str = "acpiserver: a SystemMemory write the kernel refused KernelRegister was not made, and the evaluation went on"; + +/// The first write the kernel made into each page of memory the firmware's +/// map lists nowhere, then the page's type, and into each function's +/// configuration space: the write itself is the next line, under [`OWN`]. +pub const FIRST_WRITE_IN_PAGE: &str = "acpiserver: a SystemMemory write made for the first time in a page of "; +pub const FIRST_WRITE_TO_FUNCTION: &str = "acpiserver: a PCI_Config write made for the first time to a function"; diff --git a/lib/userbound/Cargo.toml b/lib/userbound/Cargo.toml index 1340dde84d..e3a84bf7d7 100644 --- a/lib/userbound/Cargo.toml +++ b/lib/userbound/Cargo.toml @@ -26,3 +26,6 @@ toyos-bootmap = { path = "../bootmap" } # The ECAM window, as the MCFG decode made it: the one declaration of which # addresses are configuration space, read by the policy and by the kernel. toyos-acpi = { path = "../acpi" } +# Where each chipset the watchdog knows places its TCO block, which a +# configuration write is refused. +toyos-tco = { path = "../tco" } diff --git a/lib/userbound/src/firmware.rs b/lib/userbound/src/firmware.rs index dfaf290104..c8ff17d2fb 100644 --- a/lib/userbound/src/firmware.rs +++ b/lib/userbound/src/firmware.rs @@ -3,62 +3,51 @@ //! 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. +//! **The holder is trusted platform code, and every check here is a bug +//! fence.** The claim is granted by name in the signed image's manifest, and +//! the firmware whose AML it runs already holds the machine: a write the AML +//! asks for is made unless a fence below refuses it. A fence keeps a write +//! off what the kernel owns: RAM, the tables, the FACS, a device the kernel +//! knows decodes, a function a driver holds, a register that places what the +//! kernel enumerated or declared, and a register the kernel keeps. What no +//! fence sees is recorded in +//! `issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md`. //! -//! **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, a -//! register is read as the paragraphs below say, and every other type is -//! refused with its type, for whoever reads the holder's log to rule on. +//! **An access is made only through a witness this module answered** +//! ([`MemoryAt`], [`PortAt`], [`ConfigAt`], [`ConfigWriteAt`], and +//! [`LockWordAt`] for the compare-and-exchange on the FACS): none has another +//! constructor, so a refusal cannot reach an accessor. //! -//! **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. +//! **Memory is typed by the UEFI memory map** (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 and +//! runtime-services data are read and never written, an address the map does +//! not list and memory it types `EfiMemoryMappedIO` are accessed where they +//! are a register, and every other type is refused with its type. +//! Runtime-services data is read because some firmware keeps every table its +//! XSDT lists there (UEFI 2.10 §2.3.4 recommends ACPI reclaim); no memory the +//! kernel hands out carries the type (`toyos_bootmap::is_usable_type`). //! -//! **An address the map does not list, and memory it types -//! `EfiMemoryMappedIO`, is read where it is a register, and never written.** -//! A chipset keeps registers at addresses its firmware lists nowhere, or lists -//! as memory-mapped I/O, and a machine's AML reads them as it loads: the -//! firmware's map declaring a range a device's registers is the bound, the -//! owner's "if thats the best way to do it then yes", since nothing but the -//! holder's interpreter knows which regions the tables define. 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 an ECAM window is a configuration access -//! and is decided as one. +//! **A register is an address the map does not list or types +//! `EfiMemoryMappedIO`, at or above [`FIXED_RANGE_END`], inside what the +//! kernel maps, that the processor's range registers type uncacheable +//! ([`Memory::uncached`]), on a machine none of whose CPUs holds range +//! registers that are on and differ ([`Memory::registers_differ`]).** That +//! makes one access of it one access on any CPU. It does not keep RAM out, +//! since the range registers are not the effective type everywhere: the +//! allocator hands out only memory the map lists as usable, so memory the map +//! does not list holds nothing ToyOS put there. //! -//! **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 +//! **Whatever firmware types it, every page a device the kernel knows of +//! decodes in is refused, and a write to a register the kernel keeps +//! ([`HostBridge`]) is refused [`Refused::KernelRegister`].** An address in an +//! ECAM window is a configuration access and is decided as one. What the +//! allocator hands out is refused wherever the map lists it, even where //! 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, refused -//! where [`PROTECTED`] names it, and passes otherwise, a write as a read, as -//! the owner ruled on #875: ACPI 6.5 has control methods hold "exclusive -//! access to any address accessed via fields declared in Operation Regions" -//! (§5.5.2.4, Operation Regions), which a port of a device the kernel drives -//! cannot give them. +//! where [`PROTECTED`] names it, and passes otherwise, a write as a read. //! //! **A byte written to the port that commands the firmware is no port write: //! it is a call into the firmware, the kernel's to make** ([`FirmwareCall`]). @@ -69,7 +58,10 @@ //! handler has not answered, and each call stops every CPU of the machine //! for as long as that handler takes. //! -//! **Configuration space is read and never written.** +//! **Configuration space is read whole and written past what places the +//! machine** ([`ConfigAt::write`]): never a function a driver holds, an +//! interrupt controller or an IOMMU, any function's header, or a register +//! that places what the kernel enumerated or declared. //! //! **The sleep type of the power-off is the holder's to supply and the //! kernel's to write** ([`SleepType`]): `\_S5`'s `SLP_TYPa` is in the @@ -107,6 +99,64 @@ pub const FIXED_RANGE_END: u64 = 0x10_0000; /// Bytes of configuration space a function has. const CONFIG_BYTES: u16 = 0x1000; +/// One past every function's header, Type 0 and Type 1 alike (PCI Express +/// Base 6.0 §7.5.1): its decode enables and BARs, and a bridge's buses and +/// windows. +const HEADER_END: u16 = 0x40; + +/// The host bridges Intel's 11th Generation Core datasheet, volume 2a +/// (631122-003), covers, by device ID: UP3, UP4, UP3-Refresh, H35 and +/// H35-Refresh (631121-012 Table 15-3). Its registers are where the rows +/// below have them on these and no other host bridge. +const MCH_631122: [u16; 5] = [0x9A02, 0x9A04, 0x9A12, 0x9A14, 0x9A1A]; + +/// Offset 48h's `MCHBAR` (631122 §3.1.14): bits 38:17 the base of a 128 KiB +/// window, bit 0 its enable. +const MCHBAR_BASE: u64 = 0x7F_FFFE_0000; +const MCHBAREN: u64 = 1; + +/// The registers in MCHBAR the kernel keeps, each `(first, end)` as an offset +/// into the window (631122): +const KEPT: [(u64, u64); 4] = [ + // §3.4.2 GFXVTBAR and §3.4.4 VTDPVC0BAR: where the two VT-d units decode. + (0x5400, 0x5408), + (0x5410, 0x5418), + // §3.4.5 INTRDIRCTL: how a lowest-priority MSI picks its CPU. + (0x5418, 0x541C), + // §3.3.31 PACKAGE_RAPL_LIMIT, PL1 and PL2: the performance track's + // (`issues/the-kernel-owns-cpu-performance-state.md`). + (0x59A0, 0x59A8), +]; + +/// What the kernel read of 00:00.0 once it enumerated. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct HostBridge { + pub vendor: u16, + pub device: u16, + /// Its quadword at 48h. + pub at_48h: u64, +} + +impl HostBridge { + /// Nothing answered at 00:00.0. + pub const ABSENT: Self = Self { vendor: 0xFFFF, device: 0xFFFF, at_48h: u64::MAX }; + + fn covered(vendor: u16, device: u16) -> bool { + vendor == 0x8086 && MCH_631122.contains(&device) + } + + /// Where its MCHBAR window is, on a host bridge 631122 covers that + /// decodes one. + fn mchbar(&self) -> Option { + (Self::covered(self.vendor, self.device) && self.at_48h & MCHBAREN != 0).then_some(self.at_48h & MCHBAR_BASE) + } + + /// Whether `first..=last` overlaps a register of [`KEPT`]. + fn keeps(&self, first: u64, last: u64) -> bool { + self.mchbar().is_some_and(|base| KEPT.iter().any(|&(start, end)| overlaps(first, last, (base + start, base + end)))) + } +} + /// One PCI function on the segment group the kernel enumerated. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Function { @@ -130,14 +180,16 @@ pub struct Memory<'a, D> { 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 a register's read, at an address the - /// map does not list or types `EfiMemoryMappedIO`, at or above + /// Whether the processor accesses `len` bytes at an address uncached, + /// whatever maps them: asked only of a register's access, at an address + /// the map does not list or types `EfiMemoryMappedIO`, 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. + /// answers from: what it answers is not known of an access that CPU + /// makes. pub registers_differ: bool, + pub host_bridge: HostBridge, } /// A memory access the policy passed. @@ -219,6 +271,9 @@ impl> Memory<'_, D> { if overlaps(at, last, LOCAL_APIC) || self.devices.into_iter().any(|device| overlaps(at, last, pages(device))) { return No(Refused::DeviceMemory); } + if write && self.host_bridge.keeps(at, last) { + return No(Refused::KernelRegister); + } let ty = type_of(self.map, at); if type_of(self.map, last) != ty { return No(Refused::Straddles); @@ -228,9 +283,8 @@ impl> Memory<'_, D> { } 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 | Some(EFI_MMIO) if !write => {} - Some(_) | None => return No(Refused::MemoryType), + Some(EFI_RESERVED | EFI_ACPI_NVS | EFI_ACPI_RECLAIM | EFI_RUNTIME_DATA | EFI_MMIO) | None => {} + Some(_) => return No(Refused::MemoryType), } if last >= self.mapped_end { return No(Refused::Unmapped); @@ -448,7 +502,8 @@ impl CallRate { } } -/// A configuration read the policy passed. +/// A configuration access the policy passed the shape of: a read, or a +/// write still to be decided by [`ConfigAt::write`]. /// /// ```compile_fail,E0451 /// fn forged(function: toyos_userbound::firmware::Function) { @@ -474,17 +529,76 @@ impl ConfigAt { pub const fn width(&self) -> Width { self.width } + + /// Decide a write here, where the kernel enumerated `enumerated`, or + /// nothing. A write is refused [`Refused::HeldFunction`] anywhere in a + /// function a driver holds, an interrupt controller or an IOMMU (PCI Code + /// and ID Assignment Specification, base class 08h, subclasses 00h and + /// 06h): its configuration is its driver's, or the kernel's whether or + /// not it programs it; and [`Refused::AddressMap`] in any function's + /// header and in a register [`placing`] names. + pub fn write(self, enumerated: Option) -> Result { + if enumerated.is_some_and(|found| found.held || found.class == 0x08 && matches!(found.subclass, 0x00 | 0x06)) { + return Err(Refused::HeldFunction); + } + let last = self.offset + (self.width.bytes() as u16 - 1); + if self.offset < HEADER_END || enumerated.is_some_and(|found| placing(found).any(|(first, end)| self.offset < end && first <= last)) { + return Err(Refused::AddressMap); + } + Ok(ConfigWriteAt { at: self }) + } +} + +/// A configuration write the policy passed. +/// +/// ```compile_fail,E0451 +/// fn forged(at: toyos_userbound::firmware::ConfigAt) { +/// let _ = toyos_userbound::firmware::ConfigWriteAt { at }; +/// } +/// ``` +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct ConfigWriteAt { + at: ConfigAt, } -/// 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: &[EcamWindow], segment: u16, function: Function, offset: u16, width: Width, write: bool) -> Result { - if write { - return Err(Refused::ConfigWrite); +impl ConfigWriteAt { + pub const fn at(&self) -> ConfigAt { + self.at } +} + +/// What the kernel enumerated at one function. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Enumerated { + pub vendor: u16, + pub device: u16, + pub class: u8, + pub subclass: u8, + /// A kernel driver drives it or a claim holds it. + pub held: bool, +} + +/// The registers past the header of a function of this identity that place +/// what the kernel enumerated or declared, each `(first, end)`. +fn placing(found: Enumerated) -> impl Iterator { + let Enumerated { vendor, device, .. } = found; + // 631122 §3.1.13–§3.1.34: where the egress port, MCHBAR, graphics + // memory, the ECAM window, DMIBAR, the legacy ranges and the top of each + // RAM range are. + let host_bridge = HostBridge::covered(vendor, device).then_some((0x40, 0xC0)); + // An eSPI controller of the 500 Series PCH (631119-007 Table 6: + // A080h–A09Fh), whose ESPI_IOD_IOE (631120-002 §2.1.6) enables the decode + // of COM1 and the i8042's ports. + let espi = (vendor == 0x8086 && (0xA080..=0xA09F).contains(&device)).then_some((0x80, 0x84)); + // Where a chipset the watchdog knows places its TCO block, and turns it on. + let tco = toyos_tco::chipset(vendor, device).into_iter().flat_map(|row| [(row.base_reg, row.base_reg + 4), (row.enable.reg, row.enable.reg + 4)]); + host_bridge.into_iter().chain(espi).chain(tco) +} + +/// Decide a configuration access's 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: &[EcamWindow], segment: u16, function: Function, offset: u16, width: Width) -> Result { let Function { bus, device, function: number } = function; let reached = segment == SEGMENT_GROUP && ecam.iter().any(|window| window.offset(bus, device, number).is_some()); if !reached { diff --git a/lib/userbound/tests/firmware.rs b/lib/userbound/tests/firmware.rs index 7c9d896955..0426d4bcdb 100644 --- a/lib/userbound/tests/firmware.rs +++ b/lib/userbound/tests/firmware.rs @@ -5,8 +5,8 @@ use toyos_abi::acpi::{Refused, Width}; use toyos_abi::boot::MemoryMapEntry; use toyos_userbound::firmware::{ - config, lock_word, port, sleep_type, type_word, CallRate, Function, Memory, MemoryVerdict, NoLockWord, PortVerdict, Standing, CALLS, - CALL_PERIOD_NS, FIXED_RANGE_END, + config, lock_word, port, sleep_type, type_word, CallRate, Enumerated, Function, HostBridge, Memory, MemoryVerdict, NoLockWord, + PortVerdict, Standing, CALLS, CALL_PERIOD_NS, FIXED_RANGE_END, }; use toyos_acpi::EcamWindow; use toyos_userbound::{KeptCommands, Mediated}; @@ -52,6 +52,8 @@ const Q35: [MemoryMapEntry; 9] = [ /// 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); +/// q35's MCH, which 631122 does not cover, with a window at 48h all the same. +const Q35_HOST_BRIDGE: HostBridge = HostBridge { vendor: 0x8086, device: 0x29C0, at_48h: 0xfed1_0001 }; /// The ranges devices decode, as a test lists them. type Devices = std::iter::Copied>; @@ -63,7 +65,7 @@ fn devices(decoded: &'static [(u64, u64)]) -> Devices { /// 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: &[], devices: devices(decoded), facs: None, uncached: registers, registers_differ: false } + Memory { map, mapped_end: 4 * GIB, ecam: &[], devices: devices(decoded), facs: None, uncached: registers, registers_differ: false, host_bridge: HostBridge::ABSENT } } /// What the range registers type uncacheable on these machines: the hole @@ -75,7 +77,7 @@ fn registers(at: u64, len: u64) -> bool { } fn q35() -> Mem { - Memory { map: &Q35, mapped_end: 4 * GIB, ecam: ecam(0xe000_0000, 0, 0xFF), devices: devices(Q35_DRIVEN), facs: Some(Q35_FACS), uncached: registers, registers_differ: false } + Memory { map: &Q35, mapped_end: 4 * GIB, ecam: ecam(0xe000_0000, 0, 0xFF), devices: devices(Q35_DRIVEN), facs: Some(Q35_FACS), uncached: registers, registers_differ: false, host_bridge: Q35_HOST_BRIDGE } } /// A map shaped as a laptop's is, at addresses of this test's own: RAM, a @@ -95,7 +97,7 @@ const LAPTOP: [MemoryMapEntry; 9] = [ ]; fn laptop() -> Mem { - Memory { map: &LAPTOP, mapped_end: 4 * GIB, ecam: ecam(0xc000_0000, 0, 0xFF), devices: devices(&[]), facs: Some((0x7400_0040, 0x7400_0080)), uncached: registers, registers_differ: false } + Memory { map: &LAPTOP, mapped_end: 4 * GIB, ecam: ecam(0xc000_0000, 0, 0xFF), devices: devices(&[]), facs: Some((0x7400_0040, 0x7400_0080)), uncached: registers, registers_differ: false, host_bridge: HostBridge::ABSENT } } fn passes(memory: &Mem, at: u64, width: Width, write: bool) -> bool { @@ -184,54 +186,59 @@ fn runtime_services_data_is_read_as_the_tables_memory_is_and_never_written() { 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. +/// An address the map does not list is a register where the range registers +/// type it uncacheable, inside what the kernel maps and at or above 1 MiB, +/// and it is written where it is read: in each width, the T14's pad +/// configuration byte at 0xFD6E0A70 among them. 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() { +fn an_unlisted_register_is_written_where_it_is_read() { 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::RegisterCached); - assert_eq!(refused(&laptop, REGISTERS - 1, Width::Word, false), Refused::RegisterCached, "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"); + let t14 = t14(); + for write in [false, true] { + for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { + assert!(passes(&laptop, AT + 0x110, width, write), "{width:?} write={write}"); + } + assert!(passes(&t14, 0xFD6E_0A70, Width::Byte, write), "the T14's GPPC_A_13 pad"); + // The last byte the range registers type so, and the first they do not. + assert!(passes(&laptop, REGISTERS, Width::Byte, write)); + assert_eq!(refused(&laptop, REGISTERS - 1, Width::Byte, write), Refused::RegisterCached); + assert_eq!(refused(&laptop, REGISTERS - 1, Width::Word, write), Refused::RegisterCached, "an access that begins outside it"); + assert!(passes(&laptop, 4 * GIB - 9, Width::QWord, write)); + // Past what the kernel maps there is nothing to reach it through. + assert_eq!(refused(&laptop, 4 * GIB - 4, Width::QWord, write), Refused::Unmapped); + let low = Memory { mapped_end: AT + 0x110, ..bare(&LAPTOP, &[]) }; + assert_eq!(refused(&low, AT + 0x110, Width::Byte, write), Refused::Unmapped); + assert!(passes(&low, AT + 0x10f, Width::Byte, write)); + // A page the kernel knows a device decodes in, a driven window's and + // a BAR's alike, and the local APIC's. + let decoding = bare(&LAPTOP, &[(AT, AT + 0x20)]); + assert_eq!(refused(&decoding, AT + 0x110, Width::Byte, write), Refused::DeviceMemory); + assert_eq!(refused(&q35(), 0xfee0_0000, Width::DWord, write), Refused::DeviceMemory, "the local APIC"); + assert_eq!(refused(&t14, 0xFED9_0000, Width::DWord, write), Refused::DeviceMemory, "a VT-d unit the kernel drives"); + } // Listed memory is decided by its type, whatever the range registers say of it. const TYPED: [MemoryMapEntry; 3] = [e(7, REGISTERS, REGISTERS + 0x1000), e(5, 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_eq!(refused(&typed, REGISTERS, Width::Byte, true), Refused::UsableMemory); + assert_eq!(refused(&typed, REGISTERS + 0x1000, Width::Byte, true), 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); + // An access across listed memory and a hole is two things. + assert_eq!(refused(&typed, REGISTERS + 0x3000 - 1, Width::Word, true), Refused::Straddles); } /// Memory the firmware's map types `EfiMemoryMappedIO` is a device's -/// registers, as the AMD bench's DSDT reads one while it loads: read, in each -/// width, exactly where an unlisted register is (inside the direct map, at or -/// above 1 MiB, typed uncacheable by the range registers every CPU holds), -/// and never written. A page the kernel knows a device decodes in, the local +/// registers, as the AMD bench's DSDT reads one while it loads: read and +/// written, in each width, exactly where an unlisted register is (inside the +/// direct map, at or above 1 MiB, typed uncacheable by the range registers +/// every CPU holds). A page the kernel knows a device decodes in, the local /// APIC's and an ECAM window typed so are what they are whatever the map -/// says, and a read that leaves the range for RAM is refused. +/// says, and an access that leaves the range for RAM is refused. #[test] -fn memory_mapped_io_is_read_as_a_register_is_and_never_written() { +fn memory_mapped_io_is_accessed_as_a_register_is() { const AT: u64 = REGISTERS + 0x10_0000; const MMIO: [MemoryMapEntry; 4] = [ e(7, 0x0, 0x8000_0000), @@ -241,14 +248,14 @@ fn memory_mapped_io_is_read_as_a_register_is_and_never_written() { ]; let memory = bare(&MMIO, &[]); assert_eq!(type_word(&MMIO, AT), 11); - for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { - assert!(passes(&memory, AT + 0x110, width, false), "{width:?}"); - assert!(passes(&memory, AT + 0x10_0000 - width.bytes(), width, false), "{width:?} at its last byte"); - assert_eq!(refused(&memory, AT + 0x110, width, true), Refused::MemoryType, "{width:?}"); + for write in [false, true] { + for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { + assert!(passes(&memory, AT + 0x110, width, write), "{width:?}"); + assert!(passes(&memory, AT + 0x10_0000 - width.bytes(), width, write), "{width:?} at its last byte"); + } + // Typed memory-mapped I/O the range registers cache is no register. + assert_eq!(refused(&memory, 0x8000_0000, Width::Byte, write), Refused::RegisterCached); } - // Typed memory-mapped I/O the range registers cache is no register. - assert_eq!(refused(&memory, 0x8000_0000, Width::Byte, false), Refused::RegisterCached); - assert_eq!(refused(&memory, 0x8000_0000, Width::Byte, true), Refused::MemoryType); // A read across it and the RAM beside it, from either side. assert_eq!(refused(&memory, AT - 1, Width::Word, false), Refused::Straddles); assert_eq!(refused(&memory, AT - 1, Width::Byte, false), Refused::UsableMemory); @@ -270,17 +277,18 @@ fn memory_mapped_io_is_read_as_a_register_is_and_never_written() { /// 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. +/// list is read or written, 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() { +fn no_unlisted_address_is_accessed_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:?}"); + for write in [false, true] { + for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { + assert!(passes(&agreeing, AT, width, write), "{width:?}"); + assert_eq!(refused(&differing, AT, width, write), Refused::RangeRegistersDiffer, "{width:?}"); + } } assert_eq!(refused(&differing, REGISTERS - 1, Width::Byte, false), Refused::RangeRegistersDiffer); const HOLE: &[MemoryMapEntry] = &[e(7, 0x0, 0xa_0000)]; @@ -306,9 +314,10 @@ fn an_unlisted_address_below_1_mib_is_refused_whatever_the_range_registers_answe 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::RegisterCached, "{at:#x} {width:?}"); - assert_eq!(refused(&memory, at, width, true), Refused::MemoryType, "{at:#x} {width:?}"); + assert_eq!(refused(&memory, at, width, true), Refused::RegisterCached, "{at:#x} {width:?}"); } assert!(passes(&memory, FIXED_RANGE_END, width, false), "{width:?} at the bound"); + assert!(passes(&memory, FIXED_RANGE_END, width, true), "{width:?} at the bound"); } assert_eq!(refused(&memory, FIXED_RANGE_END - 1, Width::Byte, false), Refused::RegisterCached); assert_eq!(refused(&memory, FIXED_RANGE_END - 1, Width::Word, false), Refused::RegisterCached, "a read that begins below the bound"); @@ -328,8 +337,8 @@ fn memory_any_usable_range_holds_is_refused_whatever_lists_it_first() { // 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: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; - let over = Memory { map: &over, mapped_end: 4 * GIB, ecam: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; + let twice = Memory { map: &twice, mapped_end: 4 * GIB, ecam: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false, host_bridge: HostBridge::ABSENT }; + let over = Memory { map: &over, mapped_end: 4 * GIB, ecam: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false, host_bridge: HostBridge::ABSENT }; 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); @@ -369,15 +378,15 @@ fn runtime_services_code_is_refused_both_ways() { #[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); + // A hole the map does not list, which the range registers cache: no + // register is there. + assert_eq!(refused(&laptop, 0x8000_0000, Width::Byte, true), Refused::RegisterCached); assert_eq!(refused(&laptop, 0x8000_0000, Width::Byte, false), Refused::RegisterCached); 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: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false }; + let memory = Memory { map: &map, mapped_end: 4 * GIB, ecam: &[], devices: devices(&[]), facs: None, uncached: registers, registers_differ: false, host_bridge: HostBridge::ABSENT }; let read = memory.clone().decide(0x1000, Width::Byte, false); let write = memory.decide(0x1000, Width::Byte, true); let through = |verdict| matches!(verdict, MemoryVerdict::Through(_)); @@ -385,7 +394,7 @@ fn every_other_type_and_an_unlisted_address_is_refused_with_its_type() { 0 | 10 => assert!(through(read) && through(write), "type {ty}"), 6 | 9 => assert!(through(read) && write == MemoryVerdict::Refused(Refused::TableWrite), "type {ty}"), // Below 1 MiB, where no register is read. - 11 => assert!(read == MemoryVerdict::Refused(Refused::RegisterCached) && write == MemoryVerdict::Refused(Refused::MemoryType)), + 11 => assert!(read == write && read == MemoryVerdict::Refused(Refused::RegisterCached), "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}"), } @@ -517,7 +526,7 @@ fn an_address_in_the_ecam_window_is_a_configuration_access_whatever_the_map_type /// Every window is read for every access, the first no more than a later one: /// an address in the second is a configuration access with its own bus, -/// refused as a write whatever the map types it, and a read reaches its buses. +/// decided as one whatever the map types it, and a read reaches its buses. #[test] fn a_later_window_is_held_as_the_first_is() { // Buses 0x40..=0x7f at their own base, after the laptop's 0..=0x3f; both @@ -532,14 +541,14 @@ fn a_later_window_is_held_as_the_first_is() { assert_eq!(verdict, MemoryVerdict::AsConfig(function, 0x10), "write={write}"); // What the kernel does with that verdict. let MemoryVerdict::AsConfig(function, offset) = verdict else { unreachable!() }; - let made = config(memory.ecam, 0, function, offset, Width::DWord, write).map(|at| (at.function(), at.offset())); - assert_eq!(made, if write { Err(Refused::ConfigWrite) } else { Ok((function, 0x10)) }, "write={write}"); + let made = config(memory.ecam, 0, function, offset, Width::DWord).and_then(|at| if write { at.write(None).map(|passed| passed.at()) } else { Ok(at) }); + assert_eq!(made.map(|at| (at.function(), at.offset())), if write { Err(Refused::AddressMap) } else { Ok((function, 0x10)) }, "write={write}"); } for bus in [0x3f, 0x40, 0x7f] { let function = Function { bus, device: 0, function: 0 }; - assert!(config(memory.ecam, 0, function, 0, Width::DWord, false).is_ok(), "bus {bus:#x}"); + assert!(config(memory.ecam, 0, function, 0, Width::DWord).is_ok(), "bus {bus:#x}"); } - assert_eq!(config(memory.ecam, 0, Function { bus: 0x80, device: 0, function: 0 }, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); + assert_eq!(config(memory.ecam, 0, Function { bus: 0x80, device: 0, function: 0 }, 0, Width::DWord), Err(Refused::ConfigUnreachable)); } #[test] @@ -781,36 +790,227 @@ const HOST_BRIDGE: Function = Function { bus: 0, device: 0, function: 0 }; fn a_configuration_read_is_held_to_the_window_and_to_one_register() { let ecam = ecam(0xe000_0000, 0, 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"); + let at = config(ecam, 0, HOST_BRIDGE, offset, width).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()); + assert!(config(ecam, 0, last, 0, Width::DWord).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, 0, HOST_BRIDGE, offset, width), 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(&[], 0, HOST_BRIDGE, 0, Width::DWord, false), Err(Refused::ConfigUnreachable)); + assert_eq!(config(ecam, 1, HOST_BRIDGE, 0, Width::DWord), Err(Refused::ConfigUnreachable), "another segment group"); + assert_eq!(config(ecam, 0, Function { bus: 0x80, device: 0, function: 0 }, 0, Width::DWord), Err(Refused::ConfigUnreachable)); + assert_eq!(config(ecam, 0, Function { bus: 0, device: 32, function: 0 }, 0, Width::DWord), Err(Refused::ConfigUnreachable)); + assert_eq!(config(ecam, 0, Function { bus: 0, device: 0, function: 8 }, 0, Width::DWord), Err(Refused::ConfigUnreachable)); + assert_eq!(config(&[], 0, HOST_BRIDGE, 0, Width::DWord), Err(Refused::ConfigUnreachable)); } +/// A function nothing claims or drives, of no class the kernel keeps. +const FREE: Enumerated = Enumerated { vendor: 0x1b36, device: 0x0010, class: 0x01, subclass: 0x08, held: false }; + +/// The T14's host bridge (`8086:9A14`, 631121-012 Table 15-3: UP3 4 cores), +/// and the AMD bench's root complex (`1022:1630`). +const T14_HOST_BRIDGE: Enumerated = Enumerated { vendor: 0x8086, device: 0x9A14, class: 0x06, subclass: 0x00, held: false }; +const AMD_ROOT_COMPLEX: Enumerated = Enumerated { vendor: 0x1022, device: 0x1630, class: 0x06, subclass: 0x00, held: false }; + +/// A write of `width` at `offset` to a function the kernel enumerated as +/// `found`, or to one nothing answers at, decided whole: shape, then write. +fn config_write(found: Option, offset: u16, width: Width) -> Result { + let ecam = ecam(0xc000_0000, 0, 0x79); + config(ecam, 0, HOST_BRIDGE, offset, width)?.write(found).map(|passed| passed.at().offset()) +} + +/// Every width of every register in `first..end`, each as one access. +fn every_access(first: u16, end: u16) -> impl Iterator { + (first..end).flat_map(|offset| { + [Width::Byte, Width::Word, Width::DWord].into_iter().filter(move |width| offset % 4 + width.bytes() as u16 <= 4).map(move |width| (offset, width)) + }) +} + +/// 00h–3Fh of every function is its header (PCI Express Base 6.0 §7.5.1), +/// which places its decode, its BARs and a bridge's buses and windows: +/// refused [`Refused::AddressMap`] in each width, on a function nothing +/// claims and on one nothing answers at, by its address and through ECAM. +/// From 40h such a function's write passes. #[test] -fn every_configuration_write_is_refused_by_one_name() { - let ecam = ecam(0xe000_0000, 0, 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:?}"); +fn a_configuration_write_is_refused_in_every_header() { + for found in [Some(FREE), Some(AMD_ROOT_COMPLEX), None] { + for (offset, width) in every_access(0, 0x40) { + assert_eq!(config_write(found, offset, width), Err(Refused::AddressMap), "{found:?} {offset:#x} {width:?}"); + } + assert_eq!(config_write(found, 0x40, Width::Byte), Ok(0x40), "{found:?}"); + assert_eq!(config_write(found, 0x100, Width::DWord), Ok(0x100), "{found:?}"); + assert_eq!(config_write(found, 0xFFC, Width::DWord), Ok(0xFFC), "{found:?}"); + } + // Through ECAM it is the same access. + let laptop = laptop(); + let MemoryVerdict::AsConfig(function, offset) = laptop.decide(0xc000_0000 + (0x1f << 15 | 0x3C), Width::Byte, true) else { panic!("in the window") }; + let ecam = ecam(0xc000_0000, 0, 0xFF); + assert_eq!(config(ecam, 0, function, offset, Width::Byte).and_then(|at| at.write(Some(FREE)).map(drop)), Err(Refused::AddressMap)); +} + +/// A function a kernel driver drives or a claim holds is its driver's whole, +/// and an interrupt controller (base class 08h, subclass 00h) and an IOMMU +/// (08h, 06h) are the kernel's whether or not it programs them (PCI Code and +/// ID Assignment Specification): refused [`Refused::HeldFunction`] past the +/// header and in it. Another system peripheral is not. +#[test] +fn a_held_function_is_refused_whole() { + let held = Enumerated { held: true, ..FREE }; + let interrupt_controller = Enumerated { vendor: 0x8086, device: 0x0001, class: 0x08, subclass: 0x00, held: false }; + let iommu = Enumerated { vendor: 0x1022, device: 0x1631, class: 0x08, subclass: 0x06, held: false }; + for found in [held, interrupt_controller, iommu] { + for (offset, width) in [(0x04, Width::Word), (0x40, Width::Byte), (0xB8, Width::DWord), (0x100, Width::DWord), (0xFFF, Width::Byte)] { + assert_eq!(config_write(Some(found), offset, width), Err(Refused::HeldFunction), "{found:?} {offset:#x}"); + } + } + for subclass in [0x01, 0x02, 0x03, 0x04, 0x05, 0x80] { + let peripheral = Enumerated { class: 0x08, subclass, ..interrupt_controller }; + assert_eq!(config_write(Some(peripheral), 0x40, Width::Byte), Ok(0x40), "08h {subclass:#04x}"); + } + // A read is a read: the policy holds its shape and nothing else. + let ecam = ecam(0xc000_0000, 0, 0x79); + assert!(config(ecam, 0, HOST_BRIDGE, 0x40, Width::Byte).is_ok()); +} + +/// On a host bridge 631122 covers, 40h–BFh is its address map (631122 +/// §3.1.13–§3.1.34): PXPEPBAR 40h, MCHBAR 48h, GGC 50h, DEVEN 54h, PAVPC 58h, +/// DPR 5Ch, PCIEXBAR 60h, DMIBAR 68h, PAM0–6 and LAC 80h–87h, TOM A0h, TOUUD +/// A8h, BDSM B0h, BGSM B4h, TSEGMB B8h and TOLUD BCh. Refused there in each +/// width; from C0h, and on any other host bridge, it passes: the AMD bench's +/// root complex is written at B8h and BCh, where its `ALIB` mailbox is. +#[test] +fn the_host_bridges_address_map_is_refused_where_631122_maps_it() { + for device in [0x9A02, 0x9A04, 0x9A12, 0x9A14, 0x9A1A] { + let covered = Enumerated { device, ..T14_HOST_BRIDGE }; + for (offset, width) in every_access(0x40, 0xC0) { + assert_eq!(config_write(Some(covered), offset, width), Err(Refused::AddressMap), "{device:#x} {offset:#x} {width:?}"); + } + for (offset, width) in [(0xC0, Width::DWord), (0xC8, Width::Word), (0xDC, Width::DWord), (0x100, Width::DWord)] { + assert_eq!(config_write(Some(covered), offset, width), Ok(offset), "{device:#x} {offset:#x}"); + } + } + // The H line, q35's MCH and the AMD bench's root complex. + for other in [Enumerated { device: 0x9A26, ..T14_HOST_BRIDGE }, Enumerated { device: 0x9A36, ..T14_HOST_BRIDGE }, Enumerated { device: 0x29C0, ..T14_HOST_BRIDGE }, AMD_ROOT_COMPLEX] { + for (offset, width) in [(0x48, Width::DWord), (0x60, Width::DWord), (0xB8, Width::DWord), (0xBC, Width::DWord), (0xBC, Width::Byte)] { + assert_eq!(config_write(Some(other), offset, width), Ok(offset), "{other:?} {offset:#x}"); + } + } + // Another vendor's device of a covered number is no covered host bridge. + assert_eq!(config_write(Some(Enumerated { vendor: 0x1022, ..T14_HOST_BRIDGE }), 0x48, Width::DWord), Ok(0x48)); +} + +/// The registers that place a port block the kernel declared: the 500 Series +/// eSPI controller's ESPI_IOD_IOE (631119-007 Table 6, A080h–A09Fh; +/// 631120-002 §2.1.6), which enables COM1's and the i8042's decode, and where +/// each chipset the watchdog knows places and enables its TCO block +/// (`toyos_tco::CHIPSETS`): Tiger Lake-LP's SMBus function at TCOBASE 50h and +/// TCOCTL 54h (631120-002 §6.1.17, §6.1.18), and q35's LPC bridge at 40h. +/// Refused [`Refused::AddressMap`]; the registers beside them pass. +#[test] +fn a_register_that_places_a_declared_port_block_is_refused() { + let espi = |device| Enumerated { vendor: 0x8086, device, class: 0x06, subclass: 0x01, held: false }; + for device in [0xA080, 0xA082, 0xA087, 0xA09F] { + for (offset, width) in every_access(0x80, 0x84) { + assert_eq!(config_write(Some(espi(device)), offset, width), Err(Refused::AddressMap), "{device:#x} {offset:#x} {width:?}"); } + assert_eq!(config_write(Some(espi(device)), 0x84, Width::DWord), Ok(0x84), "LGIR1"); + assert_eq!(config_write(Some(espi(device)), 0x7C, Width::DWord), Ok(0x7C)); + } + for device in [0xA07F, 0xA0A0] { + assert_eq!(config_write(Some(espi(device)), 0x80, Width::DWord), Ok(0x80), "{device:#x}"); + } + let smbus = Enumerated { vendor: 0x8086, device: 0xA0A3, class: 0x0C, subclass: 0x05, held: false }; + for (offset, width) in every_access(0x50, 0x58) { + assert_eq!(config_write(Some(smbus), offset, width), Err(Refused::AddressMap), "{offset:#x} {width:?}"); + } + assert_eq!(config_write(Some(smbus), 0x40, Width::DWord), Ok(0x40), "HOSTC"); + assert_eq!(config_write(Some(smbus), 0x58, Width::DWord), Ok(0x58)); + let q35_lpc = Enumerated { vendor: 0x8086, device: 0x2918, class: 0x06, subclass: 0x01, held: false }; + for (offset, width) in every_access(0x40, 0x44) { + assert_eq!(config_write(Some(q35_lpc), offset, width), Err(Refused::AddressMap), "{offset:#x} {width:?}"); + } + assert_eq!(config_write(Some(q35_lpc), 0x44, Width::DWord), Ok(0x44)); +} + +/// MCHBAR's window as 00:00.0 of the T14 places it, 128 KiB aligned. +const T14_MCHBAR: u64 = 0xFEDC_0000; + +/// The T14's shape: RAM low, the ECAM window at 0xC0000000 for buses +/// 0..=0x79 (`pr-875` `round3/t14-testcases.log` line 160), everything from it +/// to 4 GiB typed uncacheable and listed nowhere, its four VT-d units driven, +/// and its host bridge with MCHBAR on. +fn t14() -> Mem { + const MAP: &[MemoryMapEntry] = &[e(7, 0, 0x8000_0000)]; + const DRIVEN: &[(u64, u64)] = &[(0xFED9_0000, 0xFED9_1000), (0xFED8_5000, 0xFED8_6000), (0xFED8_6000, 0xFED8_7000), (0xFED9_1000, 0xFED9_2000)]; + Memory { + map: MAP, + mapped_end: 4 * GIB, + ecam: ecam(0xc000_0000, 0, 0x79), + devices: devices(DRIVEN), + facs: None, + uncached: |at, len| at >= 0xc000_0000 && at + len <= 4 * GIB, + registers_differ: false, + host_bridge: HostBridge { vendor: 0x8086, device: 0x9A14, at_48h: T14_MCHBAR | 1 }, + } +} + +/// On a host bridge 631122 covers whose MCHBAR is on, a write that overlaps +/// a register the kernel keeps there is refused +/// [`Refused::KernelRegister`], in every width, wherever firmware's map types +/// its page: GFXVTBAR 5400h–5407h and VTDPVC0BAR 5410h–5417h (§3.4.2, +/// §3.4.4), INTRDIRCTL 5418h–541Bh (§3.4.5) and PACKAGE_RAPL_LIMIT +/// 59A0h–59A7h (§3.3.31). Every read passes, as do the T14's writes to the +/// cTDP control at 5F50h and the turbo activation ratio at 5F54h, and the +/// bytes either side of each register. +#[test] +fn a_register_the_kernel_keeps_is_refused_kernelregister() { + const KEPT: [(u64, u64); 4] = [(0x5400, 0x5408), (0x5410, 0x5418), (0x5418, 0x541C), (0x59A0, 0x59A8)]; + // The same machine with MCHBAR's page typed reserved. + const RESERVED: &[MemoryMapEntry] = &[e(7, 0, 0x8000_0000), e(0, T14_MCHBAR, T14_MCHBAR + 0x2_0000)]; + for memory in [t14(), Memory { map: RESERVED, ..t14() }] { + for (start, end) in KEPT { + for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { + // Every access that holds a byte of the register. + for at in (T14_MCHBAR + start + 1).saturating_sub(width.bytes())..T14_MCHBAR + end { + assert_eq!(refused(&memory, at, width, true), Refused::KernelRegister, "{at:#x} {width:?}"); + assert!(passes(&memory, at, width, false), "{at:#x} {width:?} reads"); + } + } + } + for (at, width) in [(0x59A0 - 1, Width::Byte), (0x59A8, Width::Byte), (0x5408, Width::QWord), (0x541C, Width::DWord), (0x53F8, Width::QWord), (0x5F50, Width::Byte), (0x5F54, Width::Byte), (0x5F50, Width::DWord)] { + assert!(passes(&memory, T14_MCHBAR + at, width, true), "{at:#x} {width:?}"); + } + } +} + +/// The kept registers are where 631122 has them only on the host bridges it +/// covers (631121-012 Table 15-3: 9A02h, 9A04h, 9A12h, 9A14h and 9A1Ah), and +/// only where MCHBAREN, bit 0 of 48h, is set (§3.1.14): on the H line, q35's +/// MCH, the AMD bench's root complex, a disabled window or nothing at +/// 00:00.0 the same write passes. +#[test] +fn kept_registers_exist_only_on_the_host_bridges_631122_covers() { + let rapl = T14_MCHBAR + 0x59A0; + for device in [0x9A02, 0x9A04, 0x9A12, 0x9A14, 0x9A1A] { + let memory = Memory { host_bridge: HostBridge { vendor: 0x8086, device, at_48h: T14_MCHBAR | 1 }, ..t14() }; + assert_eq!(refused(&memory, rapl, Width::DWord, true), Refused::KernelRegister, "{device:#x}"); + } + for host_bridge in [ + HostBridge { vendor: 0x8086, device: 0x9A26, at_48h: T14_MCHBAR | 1 }, + HostBridge { vendor: 0x8086, device: 0x9A36, at_48h: T14_MCHBAR | 1 }, + HostBridge { vendor: 0x8086, device: 0x29C0, at_48h: T14_MCHBAR | 1 }, + HostBridge { vendor: 0x1022, device: 0x1630, at_48h: T14_MCHBAR | 1 }, + HostBridge { vendor: 0x1022, device: 0x9A14, at_48h: T14_MCHBAR | 1 }, + HostBridge { vendor: 0x8086, device: 0x9A14, at_48h: T14_MCHBAR }, + HostBridge::ABSENT, + ] { + assert!(passes(&Memory { host_bridge, ..t14() }, rapl, Width::DWord, true), "{host_bridge:?}"); } - // 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(&[], 0, HOST_BRIDGE, 0x44, Width::Byte, true), Err(Refused::ConfigWrite)); + // The window's base is bits 38:17: the low bits beside the enable, and + // anything past bit 38, place nothing. + let noisy = Memory { host_bridge: HostBridge { vendor: 0x8086, device: 0x9A14, at_48h: 1 << 45 | T14_MCHBAR | 0x1_FFFF }, ..t14() }; + assert_eq!(refused(&noisy, rapl, Width::Byte, true), Refused::KernelRegister); } /// `SLP_TYPx` is bits 12:10 of PM1 control and `SLP_EN` bit 13 (ACPI 6.5 diff --git a/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index fb4fda5c62..304d9355ed 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -12,12 +12,11 @@ //! //! **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 or types memory-mapped I/O 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 +//! memory, a register at an address firmware lists nowhere or types +//! memory-mapped I/O, a port and a function's configuration space both ways, +//! and the memory its tables are in 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. //! //! **A byte written to the FADT's `SMI_CMD` is a call into the firmware, which @@ -166,9 +165,8 @@ pub enum Refused { /// 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. + /// Memory of a type the kernel passes no access to; [`Access::memory_type`] + /// says which. MemoryType = 3, /// Past the end of what the kernel maps. Unmapped = 4, @@ -192,18 +190,15 @@ pub enum Refused { /// 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 register's read, at an address firmware's map does not list or in + /// A register's access, at an address firmware's map does not list or in /// memory it types `EfiMemoryMappedIO`, which the processor's range /// registers do not type uncacheable, or below 1 MiB, where the fixed /// range registers the kernel does not read decide: no register. RegisterCached = 15, - /// A register's read, as [`Self::RegisterCached`] names one, on a machine - /// where some CPU's range registers are on and are not the boot + /// A register's access, as [`Self::RegisterCached`] names one, 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. + /// accesses it under. RangeRegistersDiffer = 16, /// A byte for `SMI_CMD` that the FADT gives a meaning (ACPI 6.5 Table /// 5.9: `ACPI_ENABLE`, `ACPI_DISABLE`, `S4BIOS_REQ`, `PSTATE_CNT`, @@ -218,6 +213,17 @@ pub enum Refused { /// A port the kernel keeps from AML though it declared none of it: /// `toyos_userbound::firmware::PROTECTED`. ProtectedPort = 20, + /// A configuration write to a function a kernel driver drives or a claim + /// holds, or to an interrupt controller or an IOMMU. + HeldFunction = 21, + /// A configuration write to a register that places what the kernel + /// enumerated or declared: any function's header, a host bridge's + /// address map, or the decode of a port block the kernel declared. + AddressMap = 22, + /// A memory write to a register the kernel keeps for itself: the + /// package's power limit, or what places the IOMMU or redirects + /// interrupts. + KernelRegister = 23, } impl Refused { @@ -236,13 +242,15 @@ impl Refused { 11 => Self::PortSpan, 12 => Self::ConfigUnreachable, 13 => Self::ConfigSpan, - 14 => Self::ConfigWrite, 15 => Self::RegisterCached, 16 => Self::RangeRegistersDiffer, 17 => Self::KernelCommand, 18 => Self::CommandSpan, 19 => Self::CommandRate, 20 => Self::ProtectedPort, + 21 => Self::HeldFunction, + 22 => Self::AddressMap, + 23 => Self::KernelRegister, _ => return None, }) } diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index a9d6187a8e..8fd8883c0c 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -13,12 +13,18 @@ //! (`toyos_userbound::firmware::PROTECTED` among them), and makes a byte to the FADT's //! `SMI_CMD` a call into the firmware, or refuses it by the byte and the rate. //! -//! **A write to memory is the kernel's to make or refuse, as a read is**: -//! firmware records what it was told in its own NVS, `_OSC`'s capabilities -//! among it, and the kernel passes NVS and reserved memory and refuses every -//! other by name, a write outside every range the firmware's map lists -//! among them, which is counted with every refusal. A write to configuration -//! space is denied by name. +//! **A write to memory or configuration space is the kernel's to make or +//! refuse, as a read is**, and a refusal denies the evaluation, with one +//! exception: a register the kernel keeps for itself +//! ([`Refused::KernelRegister`]) is not written, said the first time per +//! register, and answered as written, so the method goes on. ACPI gives AML no +//! notion of a refused write, and the rest of the firmware's method, its own +//! record of what it set among it, is kept. +//! +//! **The first write the kernel makes into each page of memory firmware's map +//! lists nowhere or types memory-mapped I/O, and into each function's +//! configuration space, is said** with its address, width and value under +//! [`OWN`]: no fence of the kernel's knows what it is. //! //! **The spaces this server serves are SystemMemory, SystemIO, PCI_Config //! and EmbeddedControl.** Every other space of Table 5.182 reaches it and is @@ -95,6 +101,7 @@ use crate::ledger::Ledger; use crate::sci::Latched; pub use acpiserver_api::OWN; +use acpiserver_api::{FIRST_WRITE_IN_PAGE, FIRST_WRITE_TO_FUNCTION, KEPT_WRITE}; /// The most the firmware may hold the Global Lock after a take the AML set no /// bound on left it the pending bit: this server's bound, and no measurement, @@ -310,6 +317,11 @@ pub struct Firmware<'k, K> { said: Ledger, /// The port writes asked of the kernel, by port and width. written: Ledger, + /// The writes the kernel made into a register's page or a function's + /// configuration space, by page and by function. + registers: Ledger, + /// The writes the kernel kept from a register of its own, by register. + pub kept: Ledger, /// The paths of the control-method power buttons this server serves. pub buttons: Vec, /// What a Sleep took of the SCI, for the server to serve. @@ -347,6 +359,8 @@ impl<'k, K: Kernel> Firmware<'k, K> { refused: Ledger::default(), said: Ledger::default(), written: Ledger::default(), + registers: Ledger::default(), + kept: Ledger::default(), buttons: Vec::new(), latched: Latched::default(), notified: Default::default(), @@ -392,11 +406,33 @@ impl<'k, K: Kernel> Firmware<'k, K> { } /// A write the kernel makes or refuses, a refusal denied under its name - /// and, for memory, the type of what it was refused in. + /// and, for memory, the type of what it was refused in; but a register + /// the kernel keeps, which is answered as written. fn written_through(&mut self, space: Space, address: u64, width: toyos_aml::Access, value: u64, at: Address) -> Result<(), Denied> { match self.kernel.access(Access::write(space, address, wide(width), value)) { Err(Stopping) => Err(self.stopped()), - Ok(Answer { made: Ok(_), .. }) => Ok(()), + Ok(Answer { made: Ok(_), memory_type }) => { + let first = match space { + Space::SystemMemory if matches!(memory_type, UNLISTED | EFI_MMIO) => Some((format!("{FIRST_WRITE_IN_PAGE}{}", type_name(memory_type)), address >> 12)), + Space::PciConfig => Some((FIRST_WRITE_TO_FUNCTION.into(), address >> 16)), + _ => None, + }; + if let Some((what, place)) = first { + if self.registers.see(&format!("{space:?} {place:#x}")) { + println!("{what}"); + println!("{OWN}that was {width:?} {value:#x} to {at:x?}"); + } + } + Ok(()) + } + Ok(Answer { made: Err(Refused::KernelRegister), .. }) => { + // Every register the kernel keeps is a qword or narrower, on its own qword. + if self.kept.see(&format!("{:#x}", address & !7)) { + println!("{KEPT_WRITE}"); + println!("{OWN}that was {width:?} {value:#x} to {at:x?}"); + } + Ok(()) + } Ok(Answer { made: Err(refused), memory_type }) => { let what = match space { Space::SystemMemory => format!("a SystemMemory write the kernel refused {refused:?}, in {}", type_name(memory_type)), @@ -492,6 +528,9 @@ fn wide(width: toyos_aml::Access) -> Width { const NO_CONTROLLER: &str = "EmbeddedControl: no controller is attached to this server's AML"; +/// `EfiMemoryMappedIO`. +const EFI_MMIO: u8 = 11; + impl Host for Firmware<'_, K> { fn read(&mut self, at: Address, width: toyos_aml::Access) -> Result { let (space, address) = match at { @@ -523,9 +562,8 @@ impl Host for Firmware<'_, K> { self.written_through(Space::SystemIo, u64::from(port), width, value, at) } Address::Memory(address) => self.written_through(Space::SystemMemory, address, width, value, at), - Address::PciConfig { .. } => { - let what = "a write to PCI_Config: this server writes no configuration space for AML".into(); - Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) + Address::PciConfig { segment, bus, device, function, offset } => { + self.written_through(Space::PciConfig, pci_address(segment, bus, device, function, offset), width, value, at) } Address::EmbeddedControl(address) => { let byte = u8::try_from(value).expect("a byte access writes a byte"); @@ -684,8 +722,9 @@ pub mod tests { /// 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 access to and every other address as it refuses RAM, - /// writes memory where the real one does, which later reads see, and - /// answers the Global Lock from a script. + /// writes memory where the real one does, which later reads see, but a + /// register it `keeps`, writes a listed function's configuration space, + /// and answers the Global Lock from a script. #[derive(Default)] pub struct Scripted { pub memory: Vec<(u64, u8, Vec)>, @@ -695,6 +734,9 @@ pub mod tests { pub kept: Vec<(u64, u64, u8)>, /// `(start, end)` of what the kernel drives, refused as device memory. pub driven: Vec<(u64, u64)>, + /// `(start, end)` of registers the kernel keeps, whose write is + /// refused `KernelRegister`. + pub keeps: Vec<(u64, u64)>, pub ports: Vec<(u64, u64)>, pub config: Vec<(u64, u64)>, pub asked: RefCell>, @@ -743,8 +785,8 @@ pub mod tests { fn access(&self, access: Access) -> Result { self.stopping()?; self.asked.borrow_mut().push(access); - assert!(access.write == 0 || access.space != Space::PciConfig as u8, "this server asks the kernel for no configuration write"); let width = Width::from_raw(access.width).expect("a width").bytes(); + let kept = self.keeps.iter().any(|&(start, end)| access.address < end && start < access.address + width); 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 => { @@ -752,6 +794,7 @@ pub mod tests { access.address >= *base && access.address + width <= base + bytes.len() as u64 }); match held { + _ if access.write != 0 && kept => Answer { made: Err(Refused::KernelRegister), memory_type: UNLISTED }, // ACPI reclaim and runtime-services data, as the real one refuses them. Some((_, memory_type @ (6 | 9), _)) if access.write != 0 => Answer { made: Err(Refused::TableWrite), memory_type: *memory_type }, Some((_, memory_type, _)) if access.write != 0 => { @@ -799,6 +842,7 @@ pub mod tests { Answer { made: Ok(u64::from(read)), memory_type: UNLISTED } } Space::SystemIo => Answer { made: listed(&self.ports).ok_or(Refused::KernelPort), memory_type: UNLISTED }, + Space::PciConfig if access.write != 0 => Answer { made: listed(&self.config).map(|_| 0).ok_or(Refused::ConfigUnreachable), memory_type: UNLISTED }, Space::PciConfig => Answer { made: listed(&self.config).ok_or(Refused::ConfigUnreachable), memory_type: UNLISTED }, }) } @@ -919,11 +963,11 @@ pub mod tests { } /// Every port write is asked of the kernel and made or refused as it - /// answers, as the owner ruled on #875: the AMD bench's interrupt - /// router's index and data, the POST port's word and a byte to - /// `SMI_CMD`, made, and a port the kernel keeps, denied under its name - /// for the refusal. Configuration space, and the controller's space with - /// no controller attached, are denied by name and ask the kernel nothing. + /// answers: the AMD bench's interrupt router's index and data, the POST + /// port's word and a byte to `SMI_CMD`, made, and a port the kernel + /// keeps, denied under its name for the refusal. A configuration write is + /// asked as one; the controller's space with no controller attached is + /// denied by name and asks the kernel nothing. #[test] fn every_port_write_is_asked_of_the_kernel_and_made_or_refused_as_it_answers() { use toyos_aml::Access::{Byte, Word}; @@ -947,27 +991,26 @@ pub mod tests { ] ); let function = Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 3, offset: 0x40 }; - let config = "a write to PCI_Config: this server writes no configuration space for AML"; - assert_eq!(host.write(function, Byte, 0), Err(Denied(config.into()))); + assert_eq!(host.write(function, Byte, 0x5A), Ok(())); + assert_eq!(kernel.asked.borrow()[5], Access::write(Space::PciConfig, 0x00FB_0040, Width::Byte, 0x5A)); let controller = Address::EmbeddedControl(0x38); assert_eq!(host.write(controller, Byte, 1), Err(Denied(format!("a write to {NO_CONTROLLER}")))); assert_eq!(host.read(controller, Byte), Err(Denied(format!("a read of {NO_CONTROLLER}")))); - assert_eq!(kernel.asked.borrow().len(), 5, "the kernel was asked for an access this server denies itself"); + assert_eq!(kernel.asked.borrow().len(), 6, "the kernel was asked for an access this server denies itself"); assert_eq!(host.reads, [0, 0, 0]); assert_eq!(host.written.counts(), "0x80 Word x1; 0xb2 Byte x1; 0xc00 Byte x1; 0xc01 Byte x1; 0xcf9 Byte x1"); } /// A write to memory is the kernel's to make or refuse: NVS and reserved /// memory are written, which a later read sees, and a write the kernel - /// refuses, to the tables' memory or to memory the firmware's map lists - /// nowhere, is denied under the kernel's name for it and the type, and - /// counted. + /// refuses, to the tables' memory or to runtime-services code, is denied + /// under the kernel's name for it and the type, and counted. #[test] fn a_memory_write_is_the_kernels_to_make_or_refuse() { use toyos_aml::Access::{Byte, DWord, QWord}; let mut kernel = machine(); kernel.memory.push((0x7fb0_0000, 9, vec![0; 8])); - kernel.kept = vec![(0xfe00_0000, 0xfe00_1000, UNLISTED)]; + kernel.kept = vec![(0xfe00_0000, 0xfe00_1000, 5)]; let mut host = Firmware::new(&kernel); assert_eq!(host.write(Address::Memory(NVS + 4), DWord, 0xdead_beef), Ok(())); assert_eq!(host.read(Address::Memory(NVS), QWord), Ok(0xdead_beef_4433_2211)); @@ -975,11 +1018,58 @@ pub mod tests { assert_eq!(host.read(Address::Memory(NVS + 0x2000), Byte), Ok(1)); let tables = "a SystemMemory write the kernel refused TableWrite, in memory of type 9"; assert_eq!(host.write(Address::Memory(0x7fb0_0000), Byte, 1), Err(Denied(tables.into()))); - let unlisted = "a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory"; + let code = "a SystemMemory write the kernel refused MemoryType, in memory of type 5"; for _ in 0..2 { - assert_eq!(host.write(Address::Memory(0xfe00_0010), DWord, 0), Err(Denied(unlisted.into()))); + assert_eq!(host.write(Address::Memory(0xfe00_0010), DWord, 0), Err(Denied(code.into()))); + } + assert_eq!(host.refused.counts(), format!("{code} x2; {tables} x1")); + assert!(host.registers.is_empty(), "a write to firmware memory or a refused one counted as a register's"); + } + + /// A write the kernel refuses `KernelRegister` is answered as made and + /// denies nothing, said once per register, a register's being its + /// aligned qword: the T14's two bytes of PL1 at 59A0h and 59A1h are one. + /// Every other refusal still denies ([`a_memory_write_is_the_kernels_to_make_or_refuse`]). + #[test] + fn a_write_to_a_register_the_kernel_keeps_is_answered_as_made_and_said_once_a_register() { + use toyos_aml::Access::{Byte, DWord}; + let mut kernel = machine(); + kernel.keeps = vec![(0xfedc_59a0, 0xfedc_59a8), (0xfedc_5418, 0xfedc_541c)]; + let mut host = Firmware::new(&kernel); + for (at, width) in [(0xfedc_59a0, Byte), (0xfedc_59a1, Byte), (0xfedc_59a0, Byte), (0xfedc_59a4, DWord), (0xfedc_5418, DWord)] { + assert_eq!(host.write(Address::Memory(at), width, 0x5A), Ok(()), "{at:#x}"); + } + assert_eq!(kernel.asked.borrow().len(), 5, "each write was asked of the kernel"); + assert_eq!(host.kept.counts(), "0xfedc5418 x1; 0xfedc59a0 x4"); + assert!(host.refused.is_empty() && host.registers.is_empty()); + } + + /// The first write the kernel makes into each 4 KiB page firmware's map + /// lists nowhere or types memory-mapped I/O, and into each function's + /// configuration space, is said once: a later write there is counted, and + /// a write to NVS or reserved memory is none of them. + #[test] + fn the_first_write_into_each_page_and_function_is_said_once() { + use toyos_aml::Access::{Byte, DWord}; + let mut kernel = machine(); + kernel.memory.extend([(0xfd6e_0000, UNLISTED, vec![0; 0x2000]), (0xf800_0000, 11, vec![0; 0x100])]); + kernel.config.push((pci_address(0, 0, 0x1F, 3, 0x44), 0)); + let mut host = Firmware::new(&kernel); + let writes = [ + Address::Memory(0xfd6e_0a70), + Address::Memory(0xfd6e_0a74), + Address::Memory(0xfd6e_1000), + Address::Memory(0xf800_00b8), + Address::Memory(0xf800_00bc), + Address::Memory(NVS), + Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 3, offset: 0x40 }, + Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 3, offset: 0x44 }, + ]; + for at in writes { + assert_eq!(host.write(at, Byte, 1), Ok(()), "{at:x?}"); } - assert_eq!(host.refused.counts(), format!("{unlisted} x2; {tables} x1")); + assert_eq!(host.write(Address::Memory(0xf800_00b8), DWord, 0x3b1_0564), Ok(())); + assert_eq!(host.registers.counts(), "PciConfig 0xfb x2; SystemMemory 0xf8000 x3; SystemMemory 0xfd6e0 x2; SystemMemory 0xfd6e1 x1"); } /// Every space of Table 5.182 this server serves no AML in is denied by diff --git a/system/acpiserver/src/init.rs b/system/acpiserver/src/init.rs index 72750de074..9795bd518f 100644 --- a/system/acpiserver/src/init.rs +++ b/system/acpiserver/src/init.rs @@ -413,6 +413,53 @@ mod tests { ); } + /// A register the kernel keeps for itself is not written, and the + /// evaluation goes on: as the T14's `HKEY._INI` writes PL1's two bytes at + /// MCHBAR + 59A0h and then records its mode in NVS, this `_INI` runs and + /// its record is made, and the register is said once. Every other + /// refusal still denies the evaluation: an `_INI` that writes a page the + /// kernel drives is refused, and what follows its write is not made. + #[test] + fn a_write_the_kernel_keeps_is_not_made_and_the_evaluation_goes_on() { + const PL1: u64 = 0xFEDC_59A0; + const DRIVEN: u64 = 0xFED9_0000; + const NVS: u64 = 0x7700_0000; + let dsdt = cat(&[ + &post(), + &op_region("MCHB", 0x00, &int(PL1), &int(2)), + // ByteAcc (§20.2.5.2). + &field("MCHB", 0x01, &[unit("PL1L", 8), unit("PL1H", 8)]), + &op_region("VTDR", 0x00, &int(DRIVEN), &int(1)), + &field("VTDR", 0x01, &[unit("VTD0", 8)]), + &op_region("NVSR", 0x00, &int(NVS), &int(2)), + &field("NVSR", 0x01, &[unit("MODE", 8), unit("LAST", 8)]), + &scope( + "\\_SB", + &cat(&[ + &device("HKEY", &method("_INI", 0, &cat(&[&store(&int(0x5A), &name("\\PL1L")), &store(&int(0xA5), &name("\\PL1H")), &store(&int(3), &name("\\MODE"))]))), + &device("VTDU", &method("_INI", 0, &cat(&[&store(&int(1), &name("\\VTD0")), &store(&int(4), &name("\\LAST"))]))), + ]), + ), + ]); + let mut kernel = crafted(&sealed(b"DSDT", &dsdt), &[]); + kernel.ports = vec![(0x80, 0)]; + kernel.memory.extend([(NVS, 10, vec![0; 2]), (PL1, toyos_abi::acpi::UNLISTED, vec![0; 8])]); + kernel.keeps = vec![(PL1, PL1 + 8)]; + kernel.driven = vec![(DRIVEN, DRIVEN + 0x1000)]; + let (_, aml) = crate::aml::load(&kernel, CRAFTED_RSDP); + let mut aml = aml.expect("the DSDT loads"); + let names = Names::read(&aml.interpreter).expect("the namespace reads"); + let mut census = Census::default(); + initialise(&mut aml, &names, &mut census); + assert_eq!(census.ini, 1, "{}", census.refused.counts()); + assert_eq!(census.refused.counts(), "_INI: denied by this server: a SystemMemory write the kernel refused DeviceMemory, in unlisted firmware memory x1"); + let written = kernel.written.borrow(); + assert_eq!(written.get(&NVS), Some(&3), "the method's record after the kept write"); + assert_eq!(written.get(&(NVS + 1)), None, "a write after a refused one"); + assert!(!written.contains_key(&PL1) && !written.contains_key(&(PL1 + 1)), "a register the kernel keeps was written"); + assert_eq!(aml.host.kept.counts(), "0xfedc59a0 x2"); + } + /// §6.5.1's walk looks for `_INI` methods: a device with no `_INI` at or below it is not /// examined, so a battery's `_STA` that reads the controller's space /// before any controller is attached is not asked, nor one that would diff --git a/tests/guest/rust-tests/src/bin/acpi_mediated.rs b/tests/guest/rust-tests/src/bin/acpi_mediated.rs index 70ccf59067..6943cc4d5b 100644 --- a/tests/guest/rust-tests/src/bin/acpi_mediated.rs +++ b/tests/guest/rust-tests/src/bin/acpi_mediated.rs @@ -6,7 +6,7 @@ //! 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 +//! COM1, to `PM1a_CNT`, to a function's header or a held function — 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. @@ -213,6 +213,9 @@ fn probe() { panic!("acpi: the power-off was refused: {refused:?}"); } +/// What [`memory`] says of the addresses firmware's map does not list. +const UNLISTED_SAID: &str = "acpi: an unlisted register was read and written, an unlisted address below 1 MiB was refused RegisterCached both ways, and the interrupt controllers, the HPET and a function's BAR DeviceMemory"; + /// 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"; @@ -242,21 +245,23 @@ fn memory(holder: &Holder, info: &AcpiInfo) { // 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. + // type everything from the top of low RAM to 4 GiB uncacheable, and no + // device decodes: a register's address, read and 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::RegisterCached), UNLISTED), "acpi: an unlisted address below 1 MiB"); + assert_eq!((hole, ty), (Ok(0), UNLISTED), "acpi: an unlisted register was not written"); + for access in [Access::read(Space::SystemMemory, 0xA_0000, Width::DWord), Access::write(Space::SystemMemory, 0xA_0000, Width::DWord, 0)] { + assert_eq!(holder.ask(access), (Err(Refused::RegisterCached), 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`. + // none of them, and an address it does not list is a register's. 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"); @@ -268,7 +273,7 @@ fn memory(holder: &Holder, info: &AcpiInfo) { 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 RegisterCached, and the interrupt controllers, the HPET and a function's BAR DeviceMemory"); + println!("{UNLISTED_SAID}"); // Non-volatile memory, both ways: the FACS, and the bytes after it. let fadt = holder.table(info.rsdp, b"FACP"); @@ -522,20 +527,42 @@ fn configuration(holder: &Holder, info: &AcpiInfo) { 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. + // Every function's header is refused AddressMap, by a function's + // address and through the ECAM window. + for (offset, width) in [(0x04, Width::Word), (0x10, Width::DWord), (0x3C, Width::Byte)] { + let value = holder.read(Space::PciConfig, host_bridge(offset), width).expect("acpi: the host bridge's header reads"); + assert_eq!(holder.write(Space::PciConfig, host_bridge(offset), width, value), Err(Refused::AddressMap), "acpi: {offset:#x} by address"); + assert_eq!(holder.write(Space::SystemMemory, ecam + u64::from(offset), width, value), Err(Refused::AddressMap), "acpi: {offset:#x} through ECAM"); + } + // Past it q35's MCH, no host bridge whose address map the kernel knows, + // is written, with the value its register holds, both ways. 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.write(Space::PciConfig, host_bridge(0x44), Width::Byte, held), Ok(()), "acpi: 0x44 by address"); + assert_eq!(holder.write(Space::SystemMemory, ecam + 0x44, Width::Byte, held), Ok(()), "acpi: 0x44 through ECAM"); + assert_eq!(holder.read(Space::PciConfig, host_bridge(0x44), Width::Byte), Ok(held)); + // The NVMe controller diskserver holds is its driver's whole. Each write + // below is of the value the register holds, so one a broken fence let + // through would be red here and nowhere else. + const NVME: u64 = 0x0010_1b36; + let nvme = (0..32).find(|&device| holder.read(Space::PciConfig, pci_address(0, 0, device, 0, 0), Width::DWord) == Ok(NVME)).expect("acpi: no NVMe controller on this guest's bus 0"); + for offset in [0x44u16, 0x100] { + let at = pci_address(0, 0, nvme, 0, offset); + let value = holder.read(Space::PciConfig, at, Width::DWord).expect("acpi: a held function reads"); + assert_eq!(holder.write(Space::PciConfig, at, Width::DWord, value), Err(Refused::HeldFunction), "acpi: the NVMe controller's {offset:#x}"); + let through = ecam + (u64::from(nvme) << 15 | u64::from(offset)); + assert_eq!(holder.write(Space::SystemMemory, through, Width::DWord, value), Err(Refused::HeldFunction), "acpi: the NVMe controller's {offset:#x} through ECAM"); } - assert_eq!(holder.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"); + // ICH9's LPC bridge places the ACPI and TCO blocks the kernel declared at 40h. + let pmbase = pci_address(0, 0, 0x1F, 0, 0x40); + let value = holder.read(Space::PciConfig, pmbase, Width::DWord).expect("acpi: the LPC bridge's PMBASE reads"); + assert_eq!(holder.write(Space::PciConfig, pmbase, Width::DWord, value), Err(Refused::AddressMap), "acpi: the LPC bridge's PMBASE"); + assert_eq!(holder.write(Space::PciConfig, host_bridge(2), Width::DWord, 0), Err(Refused::ConfigSpan)); + println!("acpi: the host bridge read as {id:#010x} {CONFIGURATION_SAID}"); } +/// What [`configuration`] says after the host bridge's identity. +const CONFIGURATION_SAID: &str = "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the NVMe controller diskserver holds HeldFunction, and the host bridge's 0x44 was written"; + fn lock(holder: &Holder, info: &AcpiInfo) { let fadt = holder.table(info.rsdp, b"FACP"); let facs = match holder.memory(fadt + 132, Width::QWord) { diff --git a/tests/toyos.rs b/tests/toyos.rs index bb7953417c..d86447e218 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1792,11 +1792,11 @@ const ACPI_MEDIATED_SAID: [&str; 10] = [ "acpi: an unbound claim was refused its access, the lock and the power-off's sleep type", "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 RegisterCached, and the interrupt controllers, the HPET and a function's BAR DeviceMemory", + "acpi: an unlisted register was read and written, an unlisted address below 1 MiB was refused RegisterCached both ways, 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 read and refused its write ReadOnlyPort; the POST port was written", "acpi: ACPI_ENABLE and ACPI_DISABLE were refused SMI_CMD as KernelCommand and SCI_EN stayed set, a write wider than a byte was refused CommandSpan, every firmware call made was read back from the port and counted on the boot processor, and a storm of them was refused CommandRate", - "by its address and through ECAM, and every write to configuration space was refused ConfigWrite", + "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the NVMe controller diskserver holds HeldFunction, and the host bridge's 0x44 was written", "acpi: the lock a dead holder left taken read free; it was taken and given back, given back with GBL_RLS where the firmware had asked, and found pending while the firmware owned it", "acpi: a sleep type wider than three bits was refused InvalidArgument, the next holder's replaced a dead one's, and a second under one claim was refused AlreadyExists", ]; @@ -4890,98 +4890,77 @@ struct Bench { /// The blocks the processors' `_OSC` load under ToyOS's claims, by /// signature and length. loaded: &'static [&'static str], - /// The host's refusal the owner ruled, which this machine's AML meets: it - /// is met, and said, not red. - ruled: Ruled, - /// What the census says of `_REG(3, 1)`, after every `_INI`. - reg: &'static str, -} - -/// A refusal the owner ruled, which a bench's AML meets. -struct Ruled { - /// Its kind, as the server says it. - kind: &'static str, - /// Each line the server says of it by kind, whole. - said: &'static [&'static str], - /// Each evaluation it refuses, by the path the server says under - /// [`acpiserver_api::OWN`]. - paths: &'static [&'static str], - /// What the namespace's census counts of it. - census: &'static str, -} - -impl Ruled { - /// `line` is one the server says of this refusal: one of [`Self::said`], - /// or one of [`Self::paths`] refused under it. - fn says(&self, line: &str) -> bool { - self.said.iter().any(|said| line.ends_with(said)) - || self.paths.iter().any(|path| line.ends_with(&format!("{}{path} was refused Host({:?})", acpiserver_api::OWN, self.kind))) - } - - /// Of the server's `lines`, each that carries this refusal's kind but the - /// census is one it says of it, and each it says of it is there. - fn held(&self, lines: &[&str]) -> Result<(), String> { - let stray: Vec<&&str> = lines.iter().filter(|l| l.contains(self.kind) && !l.contains("acpiserver: the namespace initialised: ") && !self.says(l)).collect(); - if !stray.is_empty() { - return Err(format!("the server said the refusal the owner ruled of what he did not rule: {stray:#?}")); - } - let owned = self.paths.iter().map(|path| format!("{}{path} was refused Host({:?})", acpiserver_api::OWN, self.kind)); - if let Some(unmet) = self.said.iter().map(|said| said.to_string()).chain(owned).find(|said| !lines.iter().any(|l| l.ends_with(said.as_str()))) { - return Err(format!("this bench's AML meets the refusal the owner ruled, and the server never said: {unmet}")); + /// Each register the kernel keeps that this machine's AML writes: the + /// server says it once, and its evaluation goes on. + kept: &'static [At], + /// Pages of registers this machine's AML writes, each of which the + /// server says the kernel's first write into. + first_written: &'static [At], +} + +/// Where a write the server says lands: at an address, or at an offset into +/// the 128 KiB MCHBAR window (631122 §3.1.14), wherever 00:00.0 places it. +#[derive(Clone, Copy, Debug)] +enum At { + Address(u64), + Mchbar(u64), +} + +impl At { + fn holds(self, address: u64) -> bool { + match self { + At::Address(at) => address == at, + At::Mchbar(offset) => address & 0x1_FFFF == offset, } - Ok(()) } } +/// The address of each write the server said under `head`, which it details +/// on the next line, under [`acpiserver_api::OWN`], as `... to Memory()`. +fn said_writes(lines: &[&str], head: &str) -> Result, String> { + lines + .iter() + .enumerate() + .filter(|(_, l)| l.contains(head)) + .map(|(i, l)| { + let own = lines.get(i + 1).filter(|next| next.contains(acpiserver_api::OWN)).ok_or_else(|| format!("{l:?} is followed by no line of the machine's own"))?; + u64::from_str_radix(field_between(own, " to Memory(", ")")?, 16).map_err(|_| format!("{own:?} names no address")) + }) + .collect() +} + /// The T14: 14 blocks; its `\_S5`'s `SLP_TYPa` read by a byte scan of its /// DSDT, with no interpreter; the first processor's commit loads one block, -/// of 459 bytes; and the controller's `_INI` and its hotkey device's write -/// memory outside every range the map lists, which the owner ruled refused -/// and recorded -/// (`issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md`). +/// of 459 bytes. Its controller's `_INI` writes its GPIO pad +/// `PAD_CFG_DW0_GPPC_A_13` at 0xFD6E0A70 (631120-002 §27.1.114), and its +/// hotkey device's `_INI` writes MCHBAR's cTDP control and turbo activation +/// ratio, at 5F50h and 5F54h, and PL1 at 59A0h, which the kernel keeps +/// (`issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md`). const T14: Bench = Bench { row: "acpi: the ACPI row: PM1a events 0x1800+4, GPE0 0x1860+32, SCI gsi 9 level/high", blocks: 14, refused: &[], s5: "power: S5 is PM1a 0x1804 with SLP_TYPa=7,", loaded: &["SSDT, 459 bytes"], - ruled: Ruled { - kind: "a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory", - said: &[ - "acpiserver: refused for the first time: a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory", - "acpiserver: an evaluation of the initialisation was refused for the first time: _INI: denied by this server: a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory", - ], - paths: &["\\_SB_.PC00.LPCB.EC__._INI", "\\_SB_.PC00.LPCB.EC__.HKEY._INI"], - census: "_INI: denied by this server: a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory x2", - }, - reg: "_REG(3, 1) in 1 scope(s)", + kept: &[At::Mchbar(0x59A0)], + first_written: &[At::Address(0xFD6E_0000), At::Mchbar(0x5000)], }; /// The AMD bench: 17 blocks; its SSDT8 ends in a `Scope` of an `EC0` no /// table defines, refused whole as the owner ruled /// (`issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md`); /// its `\_S5`'s `SLP_TYPa` as the check outside the tree evaluated it; no -/// processor names an `_OSC`; and the controller's `_REG(3, 1)` and the -/// adapter's `_PSR` each write the root complex's index, a configuration -/// write the owner ruled refused -/// (`issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.md`). +/// processor names an `_OSC`. Its controller's `_REG(3, 1)` and its +/// adapter's `_PSR` each reach `ALIB`, whose mailbox writes the root +/// complex's 0xB8 and 0xBC through ECAM, in 00:00.0's page at 0xF8000000. const AMD: Bench = Bench { row: "acpi: the ACPI row: PM1a events 0x400+4, GPE0 0x420+8, SCI gsi 9 level/low", blocks: 17, refused: &[(9, "a name that does not resolve")], s5: "with SLP_TYPa=5,", loaded: &[], - ruled: Ruled { - kind: "a SystemMemory write the kernel refused ConfigWrite, in memory of type 11", - said: &[ - "acpiserver: refused for the first time: a SystemMemory write the kernel refused ConfigWrite, in memory of type 11", - "acpiserver: an evaluation of the initialisation was refused for the first time: _REG: denied by this server: a SystemMemory write the kernel refused ConfigWrite, in memory of type 11", - "acpiserver: a method read for the power sources refused: _PSR: denied by this server: a SystemMemory write the kernel refused ConfigWrite, in memory of type 11", - ], - paths: &["\\_SB_.PCI0.LPC0.EC__._REG"], - census: "_REG: denied by this server: a SystemMemory write the kernel refused ConfigWrite, in memory of type 11 x1", - }, - reg: "no _REG run", + kept: &[], + first_written: &[At::Address(0xF800_0000)], }; /// The server's load of a bench's tables, every access the kernel's to make @@ -4990,9 +4969,11 @@ const AMD: Bench = Bench { /// it, and loaded or refused as its [`Bench`] says; `\_S5`'s `SLP_TYPa` /// handed to the kernel, which says it powers this machine off with that /// value, and never does on this row. Then the namespace initialised (ACPI -/// 6.5 §6.5.1): every `_STA` and `_INI` the walk runs, none refused but by a -/// ruled refusal, each of which is met, and the controller's `_REG(3, 1)` -/// after them as the bench says; `\_SB._OSC` +/// 6.5 §6.5.1): every `_STA` and `_INI` the walk runs, none refused, and the +/// controller's `_REG(3, 1)` after them; each register the kernel keeps that +/// the bench's AML writes said once, and each page of registers it writes +/// said with the kernel's first write into it; every battery reading names +/// the adapter online or offline; `\_SB._OSC` /// negotiated, and on an Intel machine each processor's, none refused; every /// block they load loaded, none refused. Every other CPU of the machine said /// how its range registers stand beside the boot processor's, and none holds @@ -5048,22 +5029,33 @@ fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { let refused: Vec<&&str> = lines .iter() .filter(|l| l.contains("acpiserver: refused") || l.contains(" refused: ") || l.contains("was refused for the first time")) - .filter(|l| !l.contains("acpiserver: table ") && !bench.ruled.says(l)) + .filter(|l| !l.contains("acpiserver: table ")) .collect(); if !refused.is_empty() { return Err(format!("the server refused something of this machine's AML: {refused:#?}")); } - bench.ruled.held(&lines)?; - for line in lines.iter().filter(|l| bench.ruled.said.iter().any(|said| l.ends_with(said))) { - eprintln!(" [acpi] a refusal the owner ruled: {}", line.trim()); + let kept = said_writes(&lines, acpiserver_api::KEPT_WRITE)?; + let unmatched = |expected: &[At], said: &[u64]| expected.iter().copied().filter(|at| !said.iter().any(|&address| at.holds(address))).collect::>(); + if kept.len() != bench.kept.len() || !unmatched(bench.kept, &kept).is_empty() { + return Err(format!("the server said the kernel kept writes to {kept:#x?}, where this bench's AML writes {:x?}", bench.kept)); + } + let pages: Vec = said_writes(&lines, acpiserver_api::FIRST_WRITE_IN_PAGE)?.iter().map(|address| address & !0xFFF).collect(); + let missing = unmatched(bench.first_written, &pages); + if !missing.is_empty() { + return Err(format!("the server said no first write into {missing:x?}; it said {pages:#x?}")); } + eprintln!(" [acpi] writes the kernel kept: {kept:#x?}; pages first written: {pages:#x?}"); let census = log.must_say("acpiserver: the namespace initialised: ")?; let refusals = census.split_once("evaluations refused, by kind: ").map_or("", |(_, kinds)| kinds.trim()); - if refusals != bench.ruled.census { - return Err(format!("the namespace's initialisation refused other than {:?}: {census}", bench.ruled.census)); + if refusals != "none" { + return Err(format!("the namespace's initialisation refused an evaluation: {census}")); } - if !census.contains(&format!("_INI run, {}; ", bench.reg)) { - return Err(format!("the census says other than {:?} of the controller's _REG(3, 1): {census}", bench.reg)); + if !census.contains("_INI run, _REG(3, 1) in 1 scope(s); ") { + return Err(format!("the census says the controller's _REG(3, 1) ran in no one scope: {census}")); + } + let readings: Vec<&&str> = lines.iter().filter(|l| l.contains(acpiserver_api::BATTERY_READ)).collect(); + if readings.is_empty() || readings.iter().any(|l| !(l.ends_with("; AC online") || l.ends_with("; AC offline"))) { + return Err(format!("a battery reading names the adapter neither online nor offline: {readings:#?}")); } log.must_say("acpiserver: \\_SB._OSC negotiated: ")?; if bench.loaded.is_empty() { @@ -5105,8 +5097,7 @@ const T14_POWER_SOURCES: &str = "acpiserver: power sources: 1 of 1 control-metho /// The T14's battery as the server read it: the one battery its tables name /// found present once the controller's `_REG` ran, its information read, -/// and at least one reading of it with the adapter's state; nothing refused -/// but [`T14`]'s ruled refusal, of its two `_INI`s by path and nothing else. +/// and at least one reading of it with the adapter's state; nothing refused. /// The census's Global Lock counts are printed and held to nothing: an /// evaluation gives back the lock it still holds as it ends /// (`finish`, `system/acpiserver/aml/src/exec.rs`), and the real lock @@ -5124,15 +5115,10 @@ fn battery_on_metal(back: &metal::Readback) -> Result<(), String> { if let Some(fired) = lines.iter().find(|l| l.contains("panicked")) { return Err(format!("the server died: {fired}")); } - let refused: Vec<&&str> = lines - .iter() - .filter(|l| l.contains("acpiserver: refused") || l.contains(" refused: ")) - .filter(|l| !T14.ruled.says(l)) - .collect(); + 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:#?}")); } - T14.ruled.held(&lines)?; let found = log.must_say(T14_POWER_SOURCES)?; let info = log.must_say(&format!("acpiserver: {}battery 1 of 1 from ", acpiserver_api::BATTERY_INFO))?; let read: Vec<&&str> = lines.iter().filter(|l| l.contains(acpiserver_api::BATTERY_READ)).collect(); From b4b6e5665bc1fadd817a443ad908c4944ecf4dbc Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 21:48:10 +0200 Subject: [PATCH 2/2] acpi: review round 1: the residuals issue's exit removes a ruled residual, and the contracts say what the code does - The residuals issue's exit no longer sends a ruled residual into the policy's module header, which would rebuild the inventory this change moved out of it: an item is fenced once cheap, or ruled an accepted residual and removed from the list with the ruling in its commit. The range-register question stays. - FIRST_WRITE_IN_PAGE's doc names memory-mapped I/O pages too, as the server says them; FIRST_WRITE_TO_FUNCTION has its own doc. - acpi_mode's `uncached` answers accesses, not reads. - 59A0h: the performance track plans to write it and nothing writes it yet, in the KEPT row and in the thermal-mode issue. - The scripted kernel's `kept` ranges, refused MemoryType, are `typed_refused`, apart from `keeps` and the server's `kept` ledger. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...re-writes-reach-itss-and-registers-no-fence-sees.md | 10 +++++----- ...ermal-modes-do-not-move-its-package-power-limits.md | 7 ++++--- kernel/src/arch/x86_64/acpi_mode.rs | 4 ++-- lib/acpiserver-api/src/lib.rs | 7 +++++-- lib/userbound/src/firmware.rs | 3 ++- system/acpiserver/src/aml.rs | 2 +- system/acpiserver/src/host.rs | 8 ++++---- system/acpiserver/src/tables.rs | 2 +- 8 files changed, 24 insertions(+), 19 deletions(-) diff --git a/issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md b/issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md index 1904fe255e..bf91669db4 100644 --- a/issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md +++ b/issues/the-acpi-servers-firmware-writes-reach-itss-and-registers-no-fence-sees.md @@ -74,8 +74,8 @@ Owner: the AML track ## Exit condition -Each item is fenced once fencing it is cheap, or the orchestrator rules it -a residual of the trust model and moves it to the policy's module header; -and the owner rules who owns the other CPUs' range registers, the kernel -then programming and asserting them, or this issue recording that it never -will. +Each item is fenced once fencing it is cheap, or ruled an accepted residual +of the trust model and removed from this list, with the ruling in the commit +that removes it; and the owner rules who owns the other CPUs' range +registers, the kernel then programming and asserting them, or this issue +recording that it never will. diff --git a/issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md b/issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md index d627ad194f..402e8ea32a 100644 --- a/issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md +++ b/issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md @@ -9,8 +9,9 @@ opened: 2026-10-13 The T14's hotkey device's `_INI` reaches Lenovo's thermal mode method (`HKEY._INI` → `DYTC` → `SCPF` → `\_SB.PC00.PL1S` → `CTCN`), which writes PL1 and PL2 as bytes of MCHBAR + 59A0h, `PACKAGE_RAPL_LIMIT` (Intel 631122 -§3.3.31). That register is the kernel's: the performance track declares it -(`issues/the-kernel-owns-cpu-performance-state.md`, RAPL), so the kernel +§3.3.31). That register is the kernel's: the performance track plans to write it, +though nothing writes it yet (`issues/the-kernel-owns-cpu-performance-state.md`, +RAPL), so the kernel refuses the write `KernelRegister` and the ACPI server answers it as made and goes on (`toyos_userbound::firmware`, `system/acpiserver/src/host.rs`). The method's other writes, the cTDP control at 5F50h and the turbo activation @@ -18,7 +19,7 @@ ratio at 5F54h, are made, and `DYTC` records its mode in NVS. So whatever mode the firmware selects, its PL1 and PL2 do not follow: the firmware believes the limits moved, and the package runs under whatever the -kernel or the firmware's boot left in the register. +firmware's boot left in the register. Owner: the performance track (the orchestrator). diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index 53c734df4f..d5cc0fc478 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -683,12 +683,12 @@ fn config(hardware: &Hardware, _acting: &Holder, segment: u16, function: PciFunc Ok(0) } -/// Whether a read of `len` bytes at `at` through the direct map is uncached: +/// Whether an access 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. +/// and accesses 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() diff --git a/lib/acpiserver-api/src/lib.rs b/lib/acpiserver-api/src/lib.rs index fe9d47bbbd..16950ac821 100644 --- a/lib/acpiserver-api/src/lib.rs +++ b/lib/acpiserver-api/src/lib.rs @@ -39,7 +39,10 @@ pub const OWN: &str = "acpiserver: (this machine's own, quoted in no record) "; pub const KEPT_WRITE: &str = "acpiserver: a SystemMemory write the kernel refused KernelRegister was not made, and the evaluation went on"; /// The first write the kernel made into each page of memory the firmware's -/// map lists nowhere, then the page's type, and into each function's -/// configuration space: the write itself is the next line, under [`OWN`]. +/// map lists nowhere or types memory-mapped I/O, then the page's type: the +/// write itself is the next line, under [`OWN`]. pub const FIRST_WRITE_IN_PAGE: &str = "acpiserver: a SystemMemory write made for the first time in a page of "; + +/// The first write the kernel made into each function's configuration +/// space: the write itself is the next line, under [`OWN`]. pub const FIRST_WRITE_TO_FUNCTION: &str = "acpiserver: a PCI_Config write made for the first time to a function"; diff --git a/lib/userbound/src/firmware.rs b/lib/userbound/src/firmware.rs index c8ff17d2fb..6469da5d07 100644 --- a/lib/userbound/src/firmware.rs +++ b/lib/userbound/src/firmware.rs @@ -123,7 +123,8 @@ const KEPT: [(u64, u64); 4] = [ (0x5410, 0x5418), // §3.4.5 INTRDIRCTL: how a lowest-priority MSI picks its CPU. (0x5418, 0x541C), - // §3.3.31 PACKAGE_RAPL_LIMIT, PL1 and PL2: the performance track's + // §3.3.31 PACKAGE_RAPL_LIMIT, PL1 and PL2: the performance track's, + // which plans to write it; nothing writes it yet // (`issues/the-kernel-owns-cpu-performance-state.md`). (0x59A0, 0x59A8), ]; diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index e535c37ee1..49d25f71e0 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -490,7 +490,7 @@ pub mod tests { 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(); + kernel.typed_refused = 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_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Err(kept.into())], s5: None, handed: false }); } diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 8fd8883c0c..f7b9dcca43 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -720,7 +720,7 @@ pub mod tests { pub const EC_COMMAND: u64 = 0x66; /// 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 + /// memory type, refuses `typed_refused` ranges as the real one refuses memory of a /// type it passes no access to and every other address as it refuses RAM, /// writes memory where the real one does, which later reads see, but a /// register it `keeps`, writes a listed function's configuration space, @@ -731,7 +731,7 @@ pub mod tests { /// The bytes of `memory` a write changed, by address. pub written: RefCell>, /// `(start, end, memory type)`. - pub kept: Vec<(u64, u64, u8)>, + pub typed_refused: Vec<(u64, u64, u8)>, /// `(start, end)` of what the kernel drives, refused as device memory. pub driven: Vec<(u64, u64)>, /// `(start, end)` of registers the kernel keeps, whose write is @@ -815,7 +815,7 @@ pub mod tests { None if self.driven.iter().any(|(start, end)| (*start..*end).contains(&access.address)) => { Answer { made: Err(Refused::DeviceMemory), memory_type: UNLISTED } } - None => match self.kept.iter().find(|(start, end, _)| (*start..*end).contains(&access.address)) { + None => match self.typed_refused.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 }, }, @@ -1010,7 +1010,7 @@ pub mod tests { use toyos_aml::Access::{Byte, DWord, QWord}; let mut kernel = machine(); kernel.memory.push((0x7fb0_0000, 9, vec![0; 8])); - kernel.kept = vec![(0xfe00_0000, 0xfe00_1000, 5)]; + kernel.typed_refused = vec![(0xfe00_0000, 0xfe00_1000, 5)]; let mut host = Firmware::new(&kernel); assert_eq!(host.write(Address::Memory(NVS + 4), DWord, 0xdead_beef), Ok(())); assert_eq!(host.read(Address::Memory(NVS), QWord), Ok(0xdead_beef_4433_2211)); diff --git a/system/acpiserver/src/tables.rs b/system/acpiserver/src/tables.rs index 5db9c25807..3553b756d0 100644 --- a/system/acpiserver/src/tables.rs +++ b/system/acpiserver/src/tables.rs @@ -118,7 +118,7 @@ mod tests { const KEPT: u64 = 0x7000_0000; fn machine() -> Scripted { - Scripted { memory: vec![(AT, 9, (0..=40u8).collect())], kept: vec![(KEPT, KEPT + 0x1000, 5)], ..Default::default() } + Scripted { memory: vec![(AT, 9, (0..=40u8).collect())], typed_refused: vec![(KEPT, KEPT + 0x1000, 5)], ..Default::default() } } #[test]