diff --git a/Cargo.lock b/Cargo.lock index a66297b398..44ff268524 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4532,6 +4532,7 @@ dependencies = [ "toyos-abi", "toyos-acpi", "toyos-bootmap", + "toyos-tco", ] [[package]] diff --git a/images/console.toml b/images/console.toml index 8d54cd8163..d5851a4201 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 a4d35efc5a..d5cc3583bd 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..bf91669db4 --- /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 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-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..402e8ea32a --- /dev/null +++ b/issues/the-t14s-thermal-modes-do-not-move-its-package-power-limits.md @@ -0,0 +1,31 @@ +--- +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 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 +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 +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..d5cc0fc478 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,27 +654,41 @@ 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: +/// 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() @@ -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..16950ac821 100644 --- a/lib/acpiserver-api/src/lib.rs +++ b/lib/acpiserver-api/src/lib.rs @@ -32,3 +32,17 @@ 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 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/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..6469da5d07 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,65 @@ 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, + // which plans to write it; nothing writes it yet + // (`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 +181,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 +272,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 +284,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 +503,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 +530,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/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 a9d6187a8e..f7b9dcca43 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"); @@ -682,19 +720,23 @@ 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, 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)>, /// 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 + /// 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 => { @@ -772,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 }, }, @@ -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.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)); @@ -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/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] diff --git a/tests/guest/rust-tests/src/bin/acpi_mediated.rs b/tests/guest/rust-tests/src/bin/acpi_mediated.rs index bbd9c2e499..05f6bb25ec 100644 --- a/tests/guest/rust-tests/src/bin/acpi_mediated.rs +++ b/tests/guest/rust-tests/src/bin/acpi_mediated.rs @@ -5,7 +5,7 @@ //! Run on a boot that starts no ACPI server (`tests/images/acpicase.toml`), //! where the i8042's 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. @@ -212,6 +212,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"; @@ -241,21 +244,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"); @@ -267,7 +272,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"); @@ -521,20 +526,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 bc9287762d..e116e632e5 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1825,11 +1825,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", ]; @@ -4918,98 +4918,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 @@ -5018,9 +4997,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 @@ -5076,22 +5057,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() { @@ -5133,8 +5125,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 @@ -5152,15 +5143,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();