diff --git a/issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md b/issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md deleted file mode 100644 index a25b674591c..00000000000 --- a/issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-11 ---- - -# acpiserver refuses the AMD bench's DSDT for a SystemMemory read in memory-mapped I/O, so its `\_S5` is never handed over - -On the AMD bench's boot at `fb03703a1` (PR #852, the log in -issuecomment-6107031259) the DSDT's load makes one SystemMemory read, a Byte, -at an address the firmware's map types 11, `EfiMemoryMappedIO`. The kernel -refuses it `MemoryType`: `toyos_userbound::firmware`'s `decide` passes memory -of types 0, 6, 9 and 10 and, to read, an address the map does not list, and -refuses every other type. The server says so by name: - -``` -acpiserver: table 1 of 17 (DSDT) refused: denied by this server: a SystemMemory read the kernel refused MemoryType, in memory of type 11 -acpiserver: no namespace: the DSDT was refused, so the 16 SSDT(s) after it are not loaded; the power button is served, and nothing of this machine's AML -acpiserver: 0 of 17 tables loaded in 20 ms -acpiserver: no \_S5 was handed to the kernel, which powers off only on one an ACPI server supplied and refuses a shutdown without one -``` - -So that bench has no power-off, and, its button being a control method -device, no button: the owner's "Stay in ACPI mode, button dead" is the ruled -state for a machine whose namespace was not kept -(`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`). The -address itself the server marks as the machine's own and quoted in no -record, and it is not quoted here. The same load's two SystemIO index writes -passed, as #845 measured them. - -A register a firmware types `EfiMemoryMappedIO` is a device's, which the -kernel's present bound does not reach; what AML may read and write in it, and -checked against what, is the policy owed. It is the case of -`issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.md`, -whose exit asks that an access pass only inside a region the machine's loaded -tables define, which a DSDT refused at its load never defines. - -Owner: the interpreter stage of -`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`; the -widening of the kernel's bound is a security boundary and is ruled before it -is built. - -## Exit condition - -The policy for AML's SystemMemory OperationRegions in memory the firmware -types `EfiMemoryMappedIO` is ruled, `firmware::decide` holds to it with a -host test of each arm, and an AMD bench boot reads all 17 tables loaded and -`\_S5` handed to the kernel; or the ruling refuses such reads, and this bench's -lost power-off is recorded as the ruled state. diff --git a/issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md b/issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md deleted file mode 100644 index e9ca67d37c4..00000000000 --- a/issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-07 ---- - -# QEMU's interrupt links are refused by the AML interpreter - -`system/acpiserver/aml/src/field.rs`'s `pci` finds the host bridge a -PCI_Config region is below as the nearest Device that names a `_BBN`. QEMU -11.1.1's DSDT (`lib/acpi/fixtures/qemu-11.1.1/dsdt.bin`) names none: its -`\_SB.PCI0` is a host bridge by its `_HID` and `_CID` alone, on bus 0. So a -field of the region its ISA bridge declares is refused as `Unsupported`, and -with it every method that reads one. - -Measured by evaluating each method that DSDT defines without an argument, -once, against a host that answers every read with zero: 39 methods, 15 a -value, 24 refused with `a PCI_Config region below no host bridge, which names -a _BBN` — the eight interrupt links' `_STA`, `_CRS` and `_DIS`. The T14's -host bridge names a `_BBN`, and none of its methods is refused this way. - -Nothing uses the interrupt links yet; whoever routes a PCI interrupt through -the interpreter on QEMU does. - -**Exit condition**: a host test in `system/acpiserver/aml/tests/` evaluates -`\_SB.LNKA._CRS` on that fixture to a buffer, the host bridge found by what -the specification names one by (§6.1.5 `_HID`, §6.1.2 `_CID`). diff --git a/issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md b/issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md deleted file mode 100644 index c8e6f6520bd..00000000000 --- a/issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-04 ---- - -# The ACPI server talks to the embedded controller without the global lock - -A machine's AML names whether its firmware also reaches the embedded -controller from SMM: the controller device's `_GLK` returns 1, and then every -transaction is taken under the FACS global lock (ACPI 6.5 §6.5.7, §5.2.10.1). -`/system/bin/acpiserver` evaluates `_GLK` only to leave the controller's -space unattached to AML, and so read no battery, wherever it is not 0 -(`attach`, `system/acpiserver/src/main.rs`), and takes no lock around its -own queries: on such a machine a query of the server's and one of the -firmware's can interleave on the same two ports. The lock itself is there to -take: the interpreter's host takes it for a Lock field or an Acquire of -`\_GL`, waiting out the firmware's hold on its `GBL_STS` -(`system/acpiserver/src/host.rs`); nothing takes it around a controller -transaction. - -On the T14 no table names a `_GLK` at all: a byte search of its DSDT and every -SSDT for the name finds none (the tables captured before its wipe, read -outside the tree). Every other machine is unread. - -Owned by `issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`, -whose interpreter is what can evaluate it. - -**Exit**: the server takes the global lock around each controller -transaction wherever the controller's `_GLK` evaluates to 1, and a test drives -a controller that requires it. 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 new file mode 100644 index 00000000000..d17fccef13a --- /dev/null +++ b/issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.md @@ -0,0 +1,55 @@ +--- +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-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md b/issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md new file mode 100644 index 00000000000..45035d5ab2a --- /dev/null +++ b/issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md @@ -0,0 +1,34 @@ +--- +status: open +kind: defect +opened: 2026-10-12 +--- + +# The AMD bench's SSDT8 scopes a controller no table defines, and is refused whole + +The AMD bench's eighth SSDT ends in `Scope (\_SB.PCI0.LPC0.EC0)`, and no +table of the bench defines `EC0_`: its controller is `EC__`. ACPI 6.5 +§19.6.120 reads "The object referred to by Location must already exist in +the namespace", so the block breaks a rule of the specification, and +`toyos-aml` refuses it whole with `NotFound`, keeping nothing of it. + +**Ruled** (owner, 2026-10-12, on #875 in comment 6121646183): "Refuse the +table whole", by name; SSDT11's two calls into it fail by name too. + +What the refusal costs, measured outside the tree on the bench's dump +(#875's M1, part 7): cut before the `Scope`, the block loads seven more +objects, one for each of its methods, and nothing else changes; SSDT11's +`M009` and `M010` call two of them, `M038` and `M039`, and any evaluation +that reaches either is refused by name. Neither is reached by the server's +start (#875's check outside the tree, `ToyOSOrg/evidence` `pr-875`). + +Owner: the AML track +(`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`). + +## Exit condition + +The bench's firmware ships an SSDT8 whose `Scope` names an object its tables +define, read loaded on the bench's `acpi_tables_loaded` row with the +expectation in `tests/toyos.rs` moved to 17 of 17; or the owner rules that a +block is kept short of a term that breaks a rule, and the interpreter does +so with a host test of 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 new file mode 100644 index 00000000000..4cfb3793bb5 --- /dev/null +++ b/issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md @@ -0,0 +1,68 @@ +--- +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/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 8ad5768f1a0..a6e4d1600e8 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 @@ -1,5 +1,5 @@ --- -status: open +status: assigned kind: track opened: 2026-10-03 --- @@ -12,11 +12,8 @@ every CPU every 2.2 s The ACPI server loads a machine's DSDT and SSDTs, evaluates `\_S5` and hands the kernel its sleep type, which the kernel powers the machine off with and -has from nowhere else. Beyond that it evaluates only the devices it serves: -the walk's `_HID`, `_STA`, `_CRS` and `_GPE`, the embedded controller's -`_GLK` and `_REG`, its query methods where the power button is a control -method device, and the batteries' and AC adapters' `_STA`, `_BIX` or -`_BIF`, `_BST` and `_PSR`. What else +has from nowhere else. It initialises the namespace, negotiates `_OSC`, +serves the runtime GPEs and delivers Notifies (#875). What else ToyOS takes from them it takes another way: the loader asks UEFI for the root bridges' windows `_CRS` would name (`loader/src/rootbridge.rs`). Nothing reads `_CST`, which names a CPU's C-states. @@ -135,6 +132,13 @@ the server finds the PNP0C09 device in the namespace its load kept and takes its ports from `_CRS` and its GPE from `_GPE`, and reaches the ports through the kernel's mediated access, since no row names them (`system/acpiserver/src/devices.rs`); the kernel reads no ECDT. +**Ruled** (owner, 2026-10-12, on #875, against ACPI 6.5 §6.5.4's "OSPM must +make Embedded Controller operation regions, accessed via the Embedded +Controllers described in ECDT, available before executing any control +method"): "follow the spec". ECDT support is permanent: the server attaches +the controller the ECDT names before the first table loads +(`early_controller`, `system/acpiserver/src/aml.rs`), and serves the +namespace's device in its place once it is found. The design, not a ruling: a machine whose power button is a control method device is claimed too, and its button served by the Notify the controller's @@ -208,20 +212,28 @@ otherwise pay to find again: bridge's answer, and one that was goes to the owner with the table refusal. - **The T14's processor objects need `Load`.** Its tables hold eight `Load` - opcodes and one `LoadTable`, none run while a table loads, and Linux lists eight tables - loaded that way; the interpreter refuses both as unsupported. -- **A refused evaluation keeps what it stored**, and nothing gives an - interpreter's 16 MiB back: after one method has filled it, an Integer - still evaluates, a field that fits one among them, and every method that - must hold anything new is refused; so is a String, Buffer or Package - handed to the caller, `\_S5`'s among them, where the bound has no room - for the caller's copy while it is built + opcodes, none run while a table loads, and Linux lists eight tables loaded + that way. `Load` is built (#875): outside the tree, on the T14's dump, + every capability bit loads the eight, by OEM Table ID and length, and + ToyOS's claims load one, of 459 bytes. Owner: S1, below. **Exit**: the + T14's `acpi_tables_loaded` reads both `_OSC`s negotiated and that block + loaded, nothing refused. +- **A method refused at the heap bound ends as any method ends**, as + ruled on #875: everything it made is destroyed on its exit (ACPI 6.5 + §5.5.2.3) and every Mutex it held is given back (§19.6.88), and it is + refused by the bound's name. **Exit, met by #875**: + `a_method_refused_at_the_bound_gives_back_what_it_made` + (`system/acpiserver/aml/tests/heap.rs`) fills the bound through one + method and then evaluates a method that builds a buffer of a mebibyte. + + What a method stores into an object that outlives it stays, since ACPI + 6.5 takes no store back, so an interpreter filled that way stays full: an Integer still + evaluates, a field that fits one among them, and every method that must + hold anything new is refused, as is a String, Buffer or Package handed to + the caller, `\_S5`'s among them (`what_is_held_live_is_bounded_in_sum`, `system/acpiserver/aml/tests/hostile.rs`; - `a_full_interpreter_reads_a_field_that_fits_an_integer`, `system/acpiserver/aml/tests/heap.rs`). - Owner: this stage, which decides - what the server does with an interpreter that is full. **Exit**: a test - fills the budget through one method, and the server then evaluates a - method that builds a buffer. + `a_full_interpreter_reads_a_field_that_fits_an_integer`). That is the + ruled state and no weakness with an exit. - **The interpreter's 16 MiB bounds the heap it holds from one call to the next, not what one call holds while it runs.** The meter counts what an interpreter's allocations ask for (`object::Meter`, @@ -257,11 +269,14 @@ otherwise pay to find again: capacity: by its doubling it is 8,192 slots, 1,301 of them free, and a method that defines more names than are free moves the arena each time it runs, at a step for every 64 bytes of it. Owner: this stage, - which gives the server its memory. **Exit**: the server states its - interpreter's bound in heap bytes, and a host test under a counting - allocator holds the most a load and an evaluation hold while they run to - it; and the arena's capacity after the real machine's last table is - read, not derived. + which gives the server its memory. **Exit, met by #875**: the server + states its interpreter's bounds in heap bytes (`MAX_LIVE` between + evaluations, `MAX_RUNNING` while one runs), and every hostile fill of + `system/acpiserver/aml/tests/heap.rs` holds its peak under the counting + allocator to `MAX_RUNNING`; the static arena's capacity after the T14's + last table and the block its commit loads is read outside the tree on its + dump, 8,192 slots for 6,989 objects, and the server says it on the + machine. - **The T14's tables are not in name order.** The interpreter's walk of the namespace (`Interpreter::walk`, `system/acpiserver/aml/src/lib.rs`) reads siblings as their tables declared them. Of the 267 objects of the @@ -272,14 +287,21 @@ otherwise pay to find again: two of them writing anything, the PCI root bridge's and the embedded controller's; and that controller's device is below that bridge, so under those answers the two run parent before child whichever order - siblings take. What the 45 do under the machine's own answers, and - whether any hangs on a sibling's, nothing has read. + siblings take. Outside the tree on the T14's dump, the server's walk, + which looks for `_INI` methods as §6.5.1 does, reads 12 `_STA` and runs 37 `_INI`, + two refused at the ten-second wait + for an SMI answered by nothing a dump holds (#875); what they do under + the machine's own answers the T14's `acpi_tables_loaded` reads. - **An evaluation may ask to wait ten seconds, and no caller names another limit.** The limit counts the time asked of Sleep, Stall and Wait, a load's too (`MAX_WAIT_US`, `system/acpiserver/aml/src/lib.rs`), and `Interpreter::usage` says what a call asked, the request that was refused - included. The T14's 14 tables asked for none while they loaded, and no - method of them has been read refused at it. Owner: this stage. **Exit**: + included. The T14's 14 tables asked for none while they loaded. Outside + the tree on its dump, the controller's `_INI` and one more are refused at + it, each calling the firmware through `SMI_CMD` and answered by nothing a + dump holds; on the machine the census says the longest wait an + evaluation asked (`system/acpiserver/src/init.rs`), which the T14's + `acpi_tables_loaded` prints. Owner: this stage. **Exit**: the slice that carries a reading of a method refused at ten seconds, on the T14 and by `usage`, brings the limit its caller names with that reading; until one does, ten seconds stands and nothing names more. @@ -287,16 +309,6 @@ otherwise pay to find again: What the server's load of the tables, on the T14 and through the kernel's mediated access, leaves open: -- **The controller's `_REG(3, 1)` runs on a namespace no `_INI` has run in.** - The server tells the controller its space is there as it attaches it to - AML (`attach`, `system/acpiserver/src/main.rs`), before the battery's - read, and runs no `_INI`: the T14's controller `_INI` calls the firmware - through `SMI_CMD`, which the host does not write. The scouts' dry run read that - the T14's `_REG` takes another branch before its `_INI` than after it, - reading and writing controller offset 0x03 where after it touches the - controller not at all; Linux runs it after. Owner: this stage. **Exit**: - the slice that runs the `_STA` and `_INI` walk runs it before `_REG`. - - **A take of the Global Lock that the AML sets no bound on ends the server where the firmware holds the lock past 1 s** (`RELEASE`, `system/acpiserver/src/host.rs`). The bound is this server's guess, not @@ -309,27 +321,24 @@ mediated access, leaves open: after a longer session is still owed. A Lock field and an `Acquire(\_GL, 0xFFFF)` take with no bound. An Acquire with a TimeoutValue waits that long, at most what the evaluation has left of its 10 s, and the - TimeoutValue is charged to it. Owner: this + TimeoutValue is charged to it. The server measures how long a take + waited for the firmware's release, its longest hold as a take saw it, and + the load's line says it beside the contended count (#875). Owner: this stage. **Exit**: a hold measured on the T14, either by a contended take in the count the server logs, which covers the load and the power sources' census alone, or by `ff_gbl_lock` after a long Linux session. That measurement sets the bound, or replaces it with no bound if the firmware never holds the lock. -- **A press during the load waits for it, and a power-off asked during it is - refused.** The server arms the power button - and then loads the tables before it serves an SCI, so a press in that time - latches and is served when the load ends: 77 ms on the T14, measured once, - and bounded only by what the interpreter lets each table sleep, 10 s. The - kernel has `\_S5`'s sleep type only once the load has handed it over, and - until then refuses `SYS_SHUTDOWN` by name, stopping nothing +- **A power-off asked during the load is refused.** A press during the load + is taken by the Sleep it falls in (`sci::await_sleep`), but the kernel has + `\_S5`'s sleep type only once the load has handed it over, and until then + refuses `SYS_SHUTDOWN` by name, stopping nothing (`kernel/src/arch/x86_64/power.rs`); the asker is told and may ask again. - Owner: this stage. **Exit**, of the press's wait: the slice that keeps the - namespace serves the SCI while a table loads. The refusal has no exit and - stays: `\_S5` is evaluated in a namespace the load has built, so no - power-off is made before the load ends, and what bounds the refusal is the - load's time. That time is held on the T14 once the `acpi_tables_loaded` row - reads it under a bound the owner names. + The refusal has no exit and stays: `\_S5` is evaluated in a namespace the + load has built, so no power-off is made before the load ends, and what + bounds the refusal is the load's time. That time is held on the T14 once + the `acpi_tables_loaded` row reads it under a bound the owner names. - **A control-method power button's press powers the machine off unruled.** The claim is no longer refused for a missing ECDT or a control-method button, so such a machine has a server to evaluate `\_S5`, and the @@ -384,24 +393,12 @@ mediated access, leaves open: then supplies a sleep type that is not the machine's and reads it refused. What the firmware call, which the kernel makes for the server where its AML -stores a byte to `SMI_CMD`, leaves open. No AML the server runs makes one -now: its host asks the kernel for three port writes, and for the embedded -controller's transactions where AML reaches its space, and denies every other -write by name (`FORWARDED` in `system/acpiserver/src/host.rs`), the -index registers 0x72 and 0xCD6, a byte each, and the POST port's word. -Those are what an AMD laptop's AML wrote, measured outside the tree on its -tables with nothing cleared between the load, the device search and all -102 of its query methods (#845): its DSDT writes both indexes at its load -and is refused without either, the search writes nothing, and the queries -write only the POST port. - -- **A port write outside the three ends what made it.** A machine whose - AML writes another port at its load has that table refused by name, and - a method that does has its evaluation refused. The T14's load writes - none: it loaded with every write denied. Owner: this stage. **Exit**: a - machine's reading of a write outside the three, which the list takes or - the owner refuses, and a slice that runs a method which stores to - `SMI_CMD` takes the call with the readings below. +stores a byte to `SMI_CMD`, leaves open. The server asks the kernel for +every port write its AML makes, as the owner ruled on #875, and the +kernel refuses the ports of the devices it drives, the 8259's edge/level +control among them (`PROTECTED` in `lib/userbound/src/firmware.rs`). Outside the tree on the +T14's dump, two of its `_INI`s call the firmware through `SMI_CMD`; on +neither bench's dump does AML write a port the kernel refuses. - **What a call does there is the firmware's**, and the kernel bounds who, when, where, which byte and how often: @@ -469,3 +466,71 @@ and a dry run of the firmware's `_INI` for the controller clears that bit in its two variants that answer all ones, the six that answer zero showing no bit, so that slice's first reading on the T14 is the same scout's of that bit, clear after the initialisation has run, before the press. + +**Ruled** (owner, 2026-10-12, in #870's comment 6119715385), asked whether +deep idle states take the interpreter's missing `Load` and `_OSC`/`_PDC` +first or wait for this track: "finish aml then continue working on deep idle +states". What finished is, and the slices below and their order, are #875's +design, not his. + +**Finished** is held to ACPI 6.5, and to Intel's Processor Vendor-Specific +ACPI Interface Specification (302223-007) for a processor's `_OSC` on Intel: + +- `toyos-aml` carries every construct of the grammar (§20.2), and each legacy + one real firmware ships, and `Error::Unsupported` is gone. +- Every space of Table 5.182 reaches the host with its §5.5.2.4 protocol, and + `_REG` announces those the server serves. +- The server does OSPM's side of the protocol. It: + - loads every block, static and loaded at run time; + - initialises the namespace; + - negotiates `\_SB._OSC` and each processor's `_OSC` with only what ToyOS + honours; + - runs every runtime GPE's method; + - delivers every Notify. +- On the T14 and the AMD bench, in the state the owner ruled, ACPI 6.5 + 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; + - 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. + +Not in finished: + +- concurrent method execution, which §5.5.2 permits and does not require; +- the PCI host bridges' `_OSC`; +- sleep states; +- `_CST`, which no part of this track reads: idle CPUs entering the states + it names read it (`issues/idle-cpus-enter-the-c-states-cst-names.md`, + #870); +- `_PDC`, which Intel deprecated in favour of `_OSC` in ACPI 3.0 and which + ACPI 6.5 does not define. + +All six slices land as #875 (`wt/toyos-aml`): the owner accepted its design +with "do the full aml in one pr". A check outside the tree on both benches' +tables and NVS (#875's M1) fixes the order inside it. The +T14's processors' `_OSC` read `\OSYS`, which `\_SB.PC00._INI` writes, so S3's +`_STA`/`_INI` walk runs before S1's `_OSC`. That walk writes `SMI_CMD`, and +memory outside the map, so S2's ruling on those writes comes before both. + +All six are built in #875. What each still waits on is a reading of the +machines: + +- **S1**: the T14's `acpi_tables_loaded` reads both `_OSC`s negotiated and + the one block ToyOS's claims load loaded, nothing refused. +- **S2**: the AMD bench's `acpi_tables_loaded` reads 16 of its 17 tables + loaded and `\_S5` handed, SSDT8 refused as the owner ruled + (`issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md`). +- **S3**: the T14's `acpi_tables_loaded` reads every `_INI` run but the two + the owner ruled refused, held by path, and `_REG(3, 1)` after them. +- **S5**: the T14's held boot reads every runtime GPE that fired served + with its method, none refused. +- **S6**: the full-interpreter bullet above, by the design #875 proposes; + the ten-second and Global Lock bullets, each by its T14 reading. diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index e6ed42d187e..579d3ed6148 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -33,7 +33,8 @@ //! PAT's write-back entry, under which the range registers decide (Intel SDM //! Vol. 3A, Table 12-7), so a register window the firmware reserved is read as //! the firmware typed it. A register at an address the firmware's map does -//! not list is read the same way, only where the boot processor's registers, +//! 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. diff --git a/lib/acpi/src/ecdt.rs b/lib/acpi/src/ecdt.rs new file mode 100644 index 00000000000..f32b12fc736 --- /dev/null +++ b/lib/acpi/src/ecdt.rs @@ -0,0 +1,90 @@ +//! The ECDT (ACPI 6.5 §5.2.15, Table 5.54): the embedded controller's two +//! registers, its GPE and its device's path, for an OS that makes the +//! controller's space "available before executing any control method" +//! (§6.5.4). + +use core::ops::Range; + +use crate::{Phys, Table, SDT_HEADER_LEN}; + +/// Table 5.54: `EC_CONTROL` and `EC_DATA` are Generic Address Structures, +/// then `UID` (4), `GPE_BIT` (1) and `EC_ID`, ASCII up to a NUL. +const EC_CONTROL: usize = SDT_HEADER_LEN; +const EC_DATA: usize = EC_CONTROL + 12; +const GPE_BIT: usize = EC_DATA + 12 + 4; +const EC_ID: usize = GPE_BIT + 1; + +/// The bytes [`ecdt`] reads at least: through `EC_ID`'s NUL, where its path +/// is empty. +pub const ECDT_NEEDED: usize = EC_ID + 1; + +const SPACE_SYSTEM_IO: u8 = 1; + +/// The embedded controller as the ECDT names it. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Ec { + /// The command register when written and the status register when read. + pub command: u16, + pub data: u16, + /// The GPE it raises. + pub gpe: u8, + /// Where `EC_ID`'s path lies in the table, its NUL left out: "a fully + /// qualified reference to the namespace object that is this embedded + /// controller device". + pub id: Range, +} + +/// Which register an [`EcRefused`] is about. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum EcRegister { + Command, + Data, +} + +/// Why the ECDT names no controller this decode hands out. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum EcRefused { + /// The table ends before `EC_ID`. + Short { len: usize }, + /// Not in the System I/O space, the controller's two ports of §12.11. + NotSystemIo { register: EcRegister, space: u8 }, + /// Not one byte wide at bit 0. + Width { register: EcRegister, bit_width: u8, bit_offset: u8 }, + /// Zero, or past the 16-bit port space. + Address { register: EcRegister, address: u64 }, + /// `EC_ID` runs to the table's end without its NUL, or holds a byte + /// that is not printable ASCII. + Id, +} + +/// The controller the ECDT names. +pub fn ecdt(table: &Table

) -> Result { + let short = EcRefused::Short { len: table.len() }; + let port = |register, at: usize| { + let field = |offset| table.byte(at + offset).ok_or(short); + let (space, bit_width, bit_offset) = (field(0)?, field(1)?, field(2)?); + let address = table.u64_at(at + 4).ok_or(short)?; + if space != SPACE_SYSTEM_IO { + return Err(EcRefused::NotSystemIo { register, space }); + } + if (bit_width, bit_offset) != (8, 0) { + return Err(EcRefused::Width { register, bit_width, bit_offset }); + } + match u16::try_from(address) { + Ok(port) if port != 0 => Ok(port), + _ => Err(EcRefused::Address { register, address }), + } + }; + let command = port(EcRegister::Command, EC_CONTROL)?; + let data = port(EcRegister::Data, EC_DATA)?; + let gpe = table.byte(GPE_BIT).ok_or(short)?; + let mut end = EC_ID; + loop { + match table.byte(end) { + Some(0) => break, + Some(0x21..=0x7E) => end += 1, + _ => return Err(EcRefused::Id), + } + } + Ok(Ec { command, data, gpe, id: EC_ID..end }) +} diff --git a/lib/acpi/src/lib.rs b/lib/acpi/src/lib.rs index 9a3f2479589..5941cb22b74 100644 --- a/lib/acpi/src/lib.rs +++ b/lib/acpi/src/lib.rs @@ -15,6 +15,7 @@ #![no_std] #![forbid(unsafe_code)] +mod ecdt; mod facs; mod fadt; mod gtdt; @@ -27,6 +28,7 @@ mod spcr; use toyos_bootmap::DirectMapEnd; +pub use ecdt::{ecdt, Ec, EcRefused, EcRegister, ECDT_NEEDED}; pub use facs::{acquire, facs, release, Facs, FacsRefused, FACS_GLOBAL_LOCK, OWNED, PENDING}; pub use fadt::{ century_of, dsdt_address, fixed_hardware, iapc_boot_arch, pm1a_control, psci, reset_register, @@ -313,6 +315,37 @@ pub fn find_table( Err(TableError::Absent) } +/// The table the XSDT lists whose signature, OEM ID and OEM Table ID +/// (§5.2.6) are these, compared byte for byte, a shorter identifier matching +/// a field it begins whose rest is NULs or spaces (ACPI 6.5 §19.6.77): `None` +/// where none matches. An entry that does not open is passed over. +pub fn find_by_id(phys: P, rsdp_addr: u64, signature: &[u8], oem_id: &[u8], oem_table_id: &[u8]) -> Result>, TableError> { + const OEM_ID: u64 = 10; + const OEM_TABLE_ID: u64 = 16; + let xsdt = xsdt(phys, rsdp_addr)?; + let same = |at: u64, len: u64, id: &[u8]| { + id.len() as u64 <= len + && (0..len).all(|i| { + let byte = phys.byte(at + i); + match id.get(i as usize) { + Some(&want) => byte == want, + None => byte == 0 || byte == b' ', + } + }) + }; + for i in 0..entries(&xsdt) { + let Some(at) = xsdt.u64_at(SDT_HEADER_LEN + i * 8) else { break }; + if at == 0 || !phys.readable(at, SDT_HEADER_LEN) { + continue; + } + if same(at, 4, signature) && same(at + OEM_ID, 6, oem_id) && same(at + OEM_TABLE_ID, 8, oem_table_id) { + let signature: [u8; 4] = core::array::from_fn(|k| phys.byte(at + k as u64)); + return Table::open(phys, at, &signature, SDT_HEADER_LEN).map(Some); + } + } + Ok(None) +} + /// How many 8-byte entries an XSDT holds. fn entries(xsdt: &Table

) -> usize { // `Table::open` guarantees len >= SDT_HEADER_LEN, so this subtraction is total. diff --git a/lib/acpi/tests/corpus.rs b/lib/acpi/tests/corpus.rs index caad92af233..807a7019528 100644 --- a/lib/acpi/tests/corpus.rs +++ b/lib/acpi/tests/corpus.rs @@ -12,7 +12,7 @@ use common::{declare_len, entry, madt, rsdp, sdt, t14_root_bridge, xsdt, Machine use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ - definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, + definition_blocks, dsdt_address, ecam_allocations, find_by_id, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, Reset, SourceOverride, Table, TableError, Trigger, @@ -584,6 +584,33 @@ fn the_xsdt_walk_answers_with_its_first_match_and_tries_no_second() { ); } +/// A table found by its signature, OEM ID and OEM Table ID (ACPI 6.5 +/// §19.6.77): an identifier shorter than its field matches where the rest is +/// NULs or spaces, a longer one or another byte does not, and an entry that +/// does not open is passed over. +#[test] +fn a_table_is_found_by_its_three_identifiers() { + let named = |signature: &[u8; 4], oem: &[u8; 6], table: &[u8; 8]| { + let mut t = sdt(signature, 1, &[0u8; 4]); + t[10..16].copy_from_slice(oem); + t[16..24].copy_from_slice(table); + common::reseal(&mut t); + t + }; + let first = named(b"OEM1", b"TOYOS ", b"TABLE1\0\0"); + let second = named(b"OEM1", b"TOYOS ", b"TABLE2 "); + let head = rsdp(XSDT_AT, 2, 36); + let root = xsdt(&[0x9000_0000, TABLE_AT, TABLE_AT + 0x1000]); + let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &first), (TABLE_AT + 0x1000, &second)]; + let m = Machine { regions }; + let found = |sig: &[u8], oem: &[u8], table: &[u8]| find_by_id(m, RSDP_AT, sig, oem, table).map(|t| t.map(|t| t.base())); + assert_eq!(found(b"OEM1", b"TOYOS", b"TABLE1"), Ok(Some(TABLE_AT))); + assert_eq!(found(b"OEM1", b"TOYOS ", b"TABLE2"), Ok(Some(TABLE_AT + 0x1000))); + assert_eq!(found(b"OEM1", b"TOYO", b"TABLE1"), Ok(None), "a shorter identifier the field does not end in padding after"); + assert_eq!(found(b"OEM1", b"TOYOS", b"TABLE3"), Ok(None)); + assert_eq!(found(b"OEM1", b"TOYOS X", b"TABLE1"), Ok(None), "an identifier longer than its field"); +} + #[test] fn the_shared_reader_refuses_what_it_does_not_hold() { let bytes = [1u8, 2, 3, 4]; diff --git a/lib/acpi/tests/ecdt.rs b/lib/acpi/tests/ecdt.rs new file mode 100644 index 00000000000..22baebe2109 --- /dev/null +++ b/lib/acpi/tests/ecdt.rs @@ -0,0 +1,51 @@ +//! The ECDT's controller (ACPI 6.5 §5.2.15, Table 5.54): both ports, its GPE +//! and its device's path, and each refusal by name. + +mod common; + +use common::{sdt, Machine}; +use toyos_acpi::{ecdt, Ec, EcRefused, EcRegister, Table, ECDT_NEEDED}; + +/// A Generic Address Structure (Table 5.1): space, bit width, bit offset, +/// access size, then the address. +fn gas(space: u8, bit_width: u8, bit_offset: u8, address: u64) -> Vec { + let mut g = vec![space, bit_width, bit_offset, 1]; + g.extend_from_slice(&address.to_le_bytes()); + g +} + +/// An ECDT of `EC_CONTROL`, `EC_DATA`, `UID` 1, `GPE_BIT` and `EC_ID`. +fn table(control: &[u8], data: &[u8], gpe: u8, id: &[u8]) -> Vec { + sdt(b"ECDT", 1, &[control, data, &1u32.to_le_bytes(), &[gpe], id].concat()) +} + +fn decoded(bytes: &[u8]) -> Result { + let m = Machine { regions: &[(0x1000, bytes)] }; + let table = Table::open(m, 0x1000, b"ECDT", ECDT_NEEDED).expect("the crafted ECDT opens"); + ecdt(&table) +} + +#[test] +fn the_ecdt_names_the_controllers_ports_gpe_and_device() { + let bytes = table(&gas(1, 8, 0, 0x66), &gas(1, 8, 0, 0x62), 0x6e, b"\\_SB.PCI0.LPCB.EC\0"); + let ec = decoded(&bytes).expect("a controller"); + assert_eq!((ec.command, ec.data, ec.gpe), (0x66, 0x62, 0x6e)); + assert_eq!(&bytes[ec.id], b"\\_SB.PCI0.LPCB.EC"); +} + +#[test] +fn a_controller_the_ecdt_names_wrongly_is_refused_by_name() { + let (io, id) = (gas(1, 8, 0, 0x62), b"\\_SB.EC0\0".as_slice()); + for (control, data, refused) in [ + (gas(0, 8, 0, 0xfe00_0000), io.clone(), EcRefused::NotSystemIo { register: EcRegister::Command, space: 0 }), + (io.clone(), gas(1, 16, 0, 0x62), EcRefused::Width { register: EcRegister::Data, bit_width: 16, bit_offset: 0 }), + (gas(1, 8, 1, 0x66), io.clone(), EcRefused::Width { register: EcRegister::Command, bit_width: 8, bit_offset: 1 }), + (gas(1, 8, 0, 0), io.clone(), EcRefused::Address { register: EcRegister::Command, address: 0 }), + (io.clone(), gas(1, 8, 0, 0x1_0000), EcRefused::Address { register: EcRegister::Data, address: 0x1_0000 }), + ] { + assert_eq!(decoded(&table(&control, &data, 3, id)), Err(refused)); + } + // A path that runs to the table's end, and one with a byte no name holds. + assert_eq!(decoded(&table(&io, &io, 3, b"\\_SB.EC0")), Err(EcRefused::Id)); + assert_eq!(decoded(&table(&io, &io, 3, b"\\_SB.E C0\0")), Err(EcRefused::Id)); +} diff --git a/lib/acpiserver-api/src/lib.rs b/lib/acpiserver-api/src/lib.rs index fb09b38c7e0..81d8372b173 100644 --- a/lib/acpiserver-api/src/lib.rs +++ b/lib/acpiserver-api/src/lib.rs @@ -14,6 +14,11 @@ pub const QUERIES_COUNTED: &str = "embedded controller queries taken: "; pub const CONTROLLER_SERVED: &str = "acpiserver: embedded controller on GPE "; pub const CONTROLLER_NONE: &str = "acpiserver: embedded controller none: "; +/// The server's census of the batteries and adapters it found, written once +/// at the end of its start where a battery is present, after their first +/// reading. +pub const POWER_SOURCES: &str = "acpiserver: power sources: "; + /// A present battery's static information, once after the controller is /// armed: `battery of `, then its numbers. pub const BATTERY_INFO: &str = "battery information: "; @@ -23,3 +28,7 @@ pub const BATTERY_INFO: &str = "battery information: "; /// remaining and full capacity, its state, rate, voltage and the power it /// moves in mW, then the adapters' state. 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) "; diff --git a/lib/userbound/src/firmware.rs b/lib/userbound/src/firmware.rs index b16215ef70c..dfaf290104b 100644 --- a/lib/userbound/src/firmware.rs +++ b/lib/userbound/src/firmware.rs @@ -11,9 +11,9 @@ //! **Memory is the firmware's or it is refused.** The UEFI memory map types //! every range (UEFI 2.10 §7.2, `EFI_MEMORY_TYPE`): what the kernel hands out //! as RAM is refused whole, ACPI NVS and reserved memory pass both ways, ACPI -//! reclaim memory, where the tables are, is read and never written, and every -//! other type, and an address the map does not list, is refused with its -//! type, for whoever reads the holder's log to rule on. +//! 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. //! //! **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 @@ -25,9 +25,13 @@ //! the holder reads there beside the tables is whatever else that firmware //! keeps in it. Runtime-services code is refused both ways. //! -//! **An address the map does not list is read where it is a register, and -//! never written.** A chipset keeps registers at addresses its firmware lists -//! nowhere, and a machine's AML reads them as it loads. Such a read passes at +//! **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 @@ -49,8 +53,12 @@ //! another range types the same byte. //! //! **A port is the kernel's to answer where the kernel declared it** -//! ([`crate::port::Mediated`]), another claim's where a row names it, and -//! passes otherwise. +//! ([`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. //! //! **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`]). @@ -82,6 +90,8 @@ const EFI_RESERVED: u32 = 0; const EFI_RUNTIME_DATA: u32 = 6; const EFI_ACPI_RECLAIM: u32 = 9; const EFI_ACPI_NVS: u32 = 10; +/// `EfiMemoryMappedIO`. +const EFI_MMIO: u32 = 11; /// The local APIC's registers and the window every interrupt message is /// addressed to (Intel SDM Vol. 3A §11.4.1 and §11.11.1): the kernel's whether @@ -121,8 +131,9 @@ pub struct Memory<'a, D> { /// The FACS, as `(start, end)`. pub facs: Option<(u64, u64)>, /// Whether the processor reads `len` bytes at an address uncached, - /// whatever maps them: asked only of an address the map does not list, at - /// or above [`FIXED_RANGE_END`]. + /// whatever maps them: asked only of a register's read, 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. @@ -218,17 +229,18 @@ 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 if !write => {} + None | Some(EFI_MMIO) if !write => {} Some(_) | None => return No(Refused::MemoryType), } if last >= self.mapped_end { return No(Refused::Unmapped); } - if ty.is_none() && self.registers_differ { + let register = matches!(ty, None | Some(EFI_MMIO)); + if register && self.registers_differ { return No(Refused::RangeRegistersDiffer); } - if ty.is_none() && (at < FIXED_RANGE_END || !(self.uncached)(at, width.bytes())) { - return No(Refused::UnlistedCached); + if register && (at < FIXED_RANGE_END || !(self.uncached)(at, width.bytes())) { + return No(Refused::RegisterCached); } if write && self.facs.is_some_and(|facs| overlaps(at, last, facs)) { return No(Refused::FacsWrite); @@ -350,15 +362,28 @@ pub enum PortVerdict { Refused(Refused), } +/// The ports kept from AML that no declaration of the kernel's holds, each +/// `(first, last)`: the 8259 pair's edge/level control, ELCR1 and ELCR2, +/// which set each of the pair's lines edge- or level-triggered. Intel lists +/// them among the pair's own registers (Intel 332691-003, the 100 Series PCH +/// datasheet, Vol. 2, §28.1, Table 28-1: 4D0h and 4D1h beside 20h-21h and +/// A0h-A1h); the kernel declares and drives the pair, so it is the kernel's +/// whole, though the declaration spans only its command and data ports. +pub const PROTECTED: [(u16, u16); 1] = [(0x4D0, 0x4D1)]; + /// Decide an access of `width` at `port`, a write of `write`: every port it -/// spans is asked of `standing`. +/// spans is asked of `standing` and of [`PROTECTED`]. pub fn port(standing: impl Fn(u16) -> Standing, port: u16, width: Width, write: Option) -> PortVerdict { use PortVerdict::Refused as No; if width == Width::QWord || port as usize + width.bytes() as usize > IO_PORTS { return No(Refused::PortSpan); } + let last = port + (width.bytes() as u16 - 1); + if PROTECTED.iter().any(|&(first, end)| port <= end && first <= last) { + return No(Refused::ProtectedPort); + } let mut commanded: Option = None; - for port in port..=port + (width.bytes() as u16 - 1) { + for port in port..=last { match standing(port) { Standing::Free | Standing::Declared(Mediated::Open) => {} Standing::Declared(Mediated::ReadOnly | Mediated::Command(_)) if write.is_none() => {} diff --git a/lib/userbound/tests/firmware.rs b/lib/userbound/tests/firmware.rs index 508046ee1dd..7c9d8969558 100644 --- a/lib/userbound/tests/firmware.rs +++ b/lib/userbound/tests/firmware.rs @@ -201,8 +201,8 @@ fn an_unlisted_register_is_read_where_it_is_uncached_and_never_written() { } // The last byte the range registers type so, and the first they do not. assert!(passes(&laptop, REGISTERS, Width::Byte, false)); - assert_eq!(refused(&laptop, REGISTERS - 1, Width::Byte, false), Refused::UnlistedCached); - assert_eq!(refused(&laptop, REGISTERS - 1, Width::Word, false), Refused::UnlistedCached, "a read that begins outside it"); + assert_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); @@ -214,7 +214,7 @@ fn an_unlisted_register_is_read_where_it_is_uncached_and_never_written() { assert_eq!(refused(&decoding, AT + 0x110, Width::Byte, false), Refused::DeviceMemory); assert_eq!(refused(&q35(), 0xfee0_0000, Width::DWord, false), Refused::DeviceMemory, "the local APIC"); // Listed memory is decided by its type, whatever the range registers say of it. - const TYPED: [MemoryMapEntry; 3] = [e(7, REGISTERS, REGISTERS + 0x1000), e(11, REGISTERS + 0x1000, REGISTERS + 0x2000), e(0, REGISTERS + 0x2000, REGISTERS + 0x3000)]; + 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); @@ -223,6 +223,51 @@ fn an_unlisted_register_is_read_where_it_is_uncached_and_never_written() { assert_eq!(refused(&typed, REGISTERS + 0x3000 - 1, Width::Word, false), 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 +/// 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. +#[test] +fn memory_mapped_io_is_read_as_a_register_is_and_never_written() { + const AT: u64 = REGISTERS + 0x10_0000; + const MMIO: [MemoryMapEntry; 4] = [ + e(7, 0x0, 0x8000_0000), + e(11, 0x8000_0000, 0x8010_0000), + e(7, AT - 0x1000, AT), + e(11, AT, AT + 0x10_0000), + ]; + 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:?}"); + } + // 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); + // Some CPU's range registers on and not the boot processor's. + let differing = Memory { registers_differ: true, ..bare(&MMIO, &[]) }; + assert_eq!(refused(&differing, AT + 0x110, Width::DWord, false), Refused::RangeRegistersDiffer); + // Past what the kernel maps there is nothing to read it through. + let low = Memory { mapped_end: AT + 0x110, ..bare(&MMIO, &[]) }; + assert_eq!(refused(&low, AT + 0x110, Width::Byte, false), Refused::Unmapped); + // A page a driven window or a function's BAR holds, and the local APIC. + let decoding = bare(&MMIO, &[(AT + 0x2000, AT + 0x2004)]); + assert_eq!(refused(&decoding, AT + 0x2ff8, Width::QWord, false), Refused::DeviceMemory); + assert!(passes(&decoding, AT + 0x3000, Width::QWord, false)); + const APIC: [MemoryMapEntry; 1] = [e(11, 0xfee0_0000, 0xfef0_0000)]; + assert_eq!(refused(&bare(&APIC, &[]), 0xfee0_0030, Width::DWord, false), Refused::DeviceMemory); + // An ECAM window the map types memory-mapped I/O is configuration space. + assert!(matches!(laptop().decide(0xc000_8000, Width::DWord, false), MemoryVerdict::AsConfig(..))); +} + /// 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 @@ -260,13 +305,13 @@ fn an_unlisted_address_below_1_mib_is_refused_whatever_the_range_registers_answe let memory = Memory { uncached: |_, _| true, ..bare(HOLE, &[]) }; for width in [Width::Byte, Width::Word, Width::DWord, Width::QWord] { for at in [0xa_0000, 0xc_0000, FIXED_RANGE_END - 8, FIXED_RANGE_END - width.bytes()] { - assert_eq!(refused(&memory, at, width, false), Refused::UnlistedCached, "{at:#x} {width:?}"); + assert_eq!(refused(&memory, at, width, false), Refused::RegisterCached, "{at:#x} {width:?}"); assert_eq!(refused(&memory, at, width, true), Refused::MemoryType, "{at:#x} {width:?}"); } assert!(passes(&memory, FIXED_RANGE_END, width, false), "{width:?} at the bound"); } - assert_eq!(refused(&memory, FIXED_RANGE_END - 1, Width::Byte, false), Refused::UnlistedCached); - assert_eq!(refused(&memory, FIXED_RANGE_END - 1, Width::Word, false), Refused::UnlistedCached, "a read that begins below the bound"); + 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"); // Listed memory below the bound is decided by its type, as anywhere. assert!(passes(&laptop(), 0x9f000, Width::DWord, false)); } @@ -327,7 +372,7 @@ fn every_other_type_and_an_unlisted_address_is_refused_with_its_type() { // A hole the map does not list, which no register is in: its write is // refused by the map, and its read by the range registers. assert_eq!(refused(&laptop, 0x8000_0000, Width::Byte, true), Refused::MemoryType); - assert_eq!(refused(&laptop, 0x8000_0000, Width::Byte, false), Refused::UnlistedCached); + assert_eq!(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]) { @@ -339,6 +384,8 @@ fn every_other_type_and_an_unlisted_address_is_refused_with_its_type() { match ty { 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)), 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}"), } @@ -585,6 +632,29 @@ fn a_port_answers_as_its_declaration_says() { } } +/// The 8259 pair's edge/level control, which no declaration holds, is +/// refused both ways, an access at either end of it or across into it as +/// well; the ports beside it, the PIT, which the kernel neither declares nor +/// drives, and the PCI interrupt router's index and data, pass. +#[test] +fn the_8259s_edge_level_control_is_refused_though_nothing_declares_it() { + for write in [None, Some(0)] { + for (at, width) in [(0x4D0, Width::Byte), (0x4D1, Width::Byte), (0x4D0, Width::Word), (0x4CE, Width::DWord)] { + assert_eq!(port(standing, at, width, write), refused_port(Refused::ProtectedPort), "{width:?} at {at:#x}"); + } + for (at, width) in [ + (0x40, Width::DWord), + (0x48, Width::DWord), + (0x4CC, Width::DWord), + (0x4D2, Width::Word), + (0xC00, Width::Byte), + (0xC01, Width::Byte), + ] { + assert_eq!(through(port(standing, at, width, write)), Some((at, width)), "{width:?} at {at:#x}"); + } + } +} + /// `SMI_CMD` is read as a port is. A byte written to it is a call into the /// firmware carrying that byte, for every byte but those the FADT gives a /// meaning, zero among the callable where the tables name none with it, and diff --git a/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index 43a2cb48173..fb4fda5c625 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -13,7 +13,8 @@ //! **What the firmware's AML addresses outside those blocks the holder reaches //! one access at a time, through the kernel** ([`Access`]): firmware-owned //! memory and a port both ways, the memory its tables are in and a register -//! at an address firmware lists nowhere to read, a function's configuration space to read. The +//! 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 @@ -194,13 +195,15 @@ pub enum Refused { /// A write to configuration space, by its address or through the ECAM /// window: the kernel makes none for the holder. ConfigWrite = 14, - /// A read at an address firmware's map does not list, which the - /// processor's range registers do not type uncacheable: no register. - UnlistedCached = 15, - /// A read at an address firmware's map does not list, on a machine where - /// some CPU's range registers are on and are not the boot processor's: - /// what types the address a register is not what every CPU reads it - /// under. + /// A register's read, 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 + /// processor's: what types the address a register is not what every CPU + /// reads 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`, @@ -212,6 +215,9 @@ pub enum Refused { /// A byte for `SMI_CMD` past the most the kernel writes for the holder in /// a second. CommandRate = 19, + /// A port the kernel keeps from AML though it declared none of it: + /// `toyos_userbound::firmware::PROTECTED`. + ProtectedPort = 20, } impl Refused { @@ -231,11 +237,12 @@ impl Refused { 12 => Self::ConfigUnreachable, 13 => Self::ConfigSpan, 14 => Self::ConfigWrite, - 15 => Self::UnlistedCached, + 15 => Self::RegisterCached, 16 => Self::RangeRegistersDiffer, 17 => Self::KernelCommand, 18 => Self::CommandSpan, 19 => Self::CommandRate, + 20 => Self::ProtectedPort, _ => return None, }) } diff --git a/sdk/abi/src/arch/aarch64.rs b/sdk/abi/src/arch/aarch64.rs index 33304a1611a..bab478eee86 100644 --- a/sdk/abi/src/arch/aarch64.rs +++ b/sdk/abi/src/arch/aarch64.rs @@ -29,6 +29,11 @@ pub fn counter() -> u64 { now } +/// Whether the processor is Intel's: no Intel processor implements AArch64. +pub fn processor_is_intel() -> bool { + false +} + /// AArch64 has no port space, and no grant names a port on it: each of these /// dies naming the port it was asked for. pub mod ioport { diff --git a/sdk/abi/src/arch/mod.rs b/sdk/abi/src/arch/mod.rs index 3eb4e451eb3..5c1a9f443d9 100644 --- a/sdk/abi/src/arch/mod.rs +++ b/sdk/abi/src/arch/mod.rs @@ -1,6 +1,7 @@ //! What a process reads off its own CPU without a syscall, one module per //! architecture: the counter the clock page describes, the thread's id in -//! its thread control block, and `in` and `out` where there is a port space. +//! its thread control block, `in` and `out` where there is a port space, and +//! whether the processor is Intel's. #[cfg(target_arch = "aarch64")] mod aarch64; @@ -8,6 +9,6 @@ mod aarch64; mod x86_64; #[cfg(target_arch = "aarch64")] -pub use aarch64::{counter, current_tid, ioport, TCB_TID}; +pub use aarch64::{counter, current_tid, ioport, processor_is_intel, TCB_TID}; #[cfg(target_arch = "x86_64")] -pub use x86_64::{counter, current_tid, ioport, TCB_TID}; +pub use x86_64::{counter, current_tid, ioport, processor_is_intel, TCB_TID}; diff --git a/sdk/abi/src/arch/x86_64.rs b/sdk/abi/src/arch/x86_64.rs index 793090ed982..8d8e1b3d94e 100644 --- a/sdk/abi/src/arch/x86_64.rs +++ b/sdk/abi/src/arch/x86_64.rs @@ -38,6 +38,29 @@ pub fn counter() -> u64 { } } +/// Whether the processor is Intel's, by CPUID leaf 0's vendor string, +/// `GenuineIntel` in EBX, EDX and ECX (Intel SDM Vol. 2A, "CPUID"): the +/// vendor whose interface a processor object's `_OSC` speaks under Intel's +/// UUID (Intel 302223-007 §2.2). +pub fn processor_is_intel() -> bool { + let (ebx, ecx, edx): (u64, u32, u32); + // SAFETY: CPUID is unprivileged, and the kernel enables no CPUID + // faulting; RBX, which LLVM reserves, is put back as it was. + unsafe { + core::arch::asm!( + "mov {rbx}, rbx", + "cpuid", + "xchg {rbx}, rbx", + rbx = out(reg) ebx, + inout("eax") 0u32 => _, + inout("ecx") 0u32 => ecx, + out("edx") edx, + options(nomem, nostack, preserves_flags), + ); + } + [ebx as u32, edx, ecx] == [*b"Genu", *b"ineI", *b"ntel"].map(u32::from_le_bytes) +} + /// `in` and `out` from Ring 3: a port this process holds no grant for, which /// its CPU's I/O permission bitmap refuses, faults it. pub mod ioport { diff --git a/sdk/abi/src/lib.rs b/sdk/abi/src/lib.rs index 6aa383ad4c9..51f7fd8cb6b 100644 --- a/sdk/abi/src/lib.rs +++ b/sdk/abi/src/lib.rs @@ -125,4 +125,4 @@ pub mod tcb { const _: () = assert!(AARCH64_BYTES == 16, "AArch64's psABI TCB is two words"); } -pub use arch::{current_tid, ioport, TCB_TID}; +pub use arch::{current_tid, ioport, processor_is_intel, TCB_TID}; diff --git a/system/acpiserver/aml/src/exec.rs b/system/acpiserver/aml/src/exec.rs index 7c8f5c9dfb8..340f134ab21 100644 --- a/system/acpiserver/aml/src/exec.rs +++ b/system/acpiserver/aml/src/exec.rs @@ -10,7 +10,8 @@ //! //! Every evaluation is bounded: in steps, in nesting (terms, invocations and //! field accesses together, each a frame of this walk), in the size of any -//! object, and in time asked to sleep. A bound reached is a refusal. +//! object, in Mutexes held at once, and in time asked to sleep. A bound +//! reached is a refusal. //! //! The bytes of a string or buffer that an operator makes, at a size its //! table chooses, are made after its operands are evaluated, and are let go @@ -25,15 +26,15 @@ use alloc::vec::Vec; use core::cell::{Cell, RefCell}; use core::cmp::Ordering; -use crate::field::{flags, BufField, Field, Kind, Region}; +use crate::field::{attrib, flags, BufField, Connection, Field, Kind, Region}; use crate::name::{text, Path, Seg}; -use crate::namespace::{Namespace, NodeId}; +use crate::namespace::{Arena, Namespace, NodeId}; use crate::object::{ bounded, decimal, fit, hex2, joined, slot_of, to_buf, to_int, to_str, Body, Bytes, Elems, Kept, Meter, Method, Mutex, Object, Ref, Slot, Unresolved, Width, }; use crate::stream::{starts_name, Cursor}; -use crate::{Error, Host, Usage, MAX_DEPTH, MAX_NESTING, MAX_STEPS, MAX_WAIT_US, REVISION, WINDOWS, WORK_PER_STEP}; +use crate::{Error, Host, RuntimeLoad, Usage, MAX_DEPTH, MAX_NESTING, MAX_STEPS, MAX_WAIT_US, REVISION, WINDOWS, WORK_PER_STEP}; pub(crate) struct Machine<'a> { pub(crate) ns: &'a mut Namespace, @@ -51,12 +52,26 @@ pub(crate) struct Machine<'a> { global: u32, } +/// The heap a frame and what runs in it hold beside the meter: its locals' +/// and seven arguments' shared slots and the arguments' vector, a name of +/// 255 segments read, and a PCI_Config walk's devices, 257 of them in a +/// vector that doubles. +pub(crate) const FRAME_HEAP: usize = 15 * (2 * core::mem::size_of::() + core::mem::size_of::>()) + + 7 * core::mem::size_of::() + + 255 * core::mem::size_of::() + + 512 * core::mem::size_of::(); + pub(crate) struct Frame { locals: [Slot; 8], args: Vec, scope: NodeId, table: Bytes, held: usize, + /// Where what this frame defines lives: a table's load's, or its method's. + arena: Arena, + /// The method this frame runs, or the root for a table's load: what a + /// `Load` it makes is said to have been made by. + by: NodeId, } pub(crate) enum Flow { @@ -66,6 +81,13 @@ pub(crate) enum Flow { Return(Object), } +/// Where a definition block `Load` reads comes from. +pub(crate) enum Source { + /// Memory at `at`, `len` bytes long, which the block must lie in. + Memory { at: u64, len: u64 }, + Bytes(Vec), +} + enum Target { None, Debug, @@ -80,13 +102,15 @@ fn slot(o: Object) -> Slot { } impl Frame { - pub(crate) fn new(scope: NodeId, args: Vec, table: Bytes, held: usize) -> Frame { + pub(crate) fn new(scope: NodeId, args: Vec, table: Bytes, held: usize, arena: Arena) -> Frame { Frame { locals: core::array::from_fn(|_| slot(Object::Uninit)), args: args.into_iter().map(slot).collect(), scope, table, held, + arena, + by: scope, } } } @@ -236,9 +260,11 @@ impl<'a> Machine<'a> { self.depth -= 1; } - /// Ends an evaluation: whatever it still holds is let go, and holding - /// anything at its end is itself a refusal (§19.6.88: "the top-level - /// control method cannot exit while still holding ownership of a Mutex"). + /// Ends an evaluation: whatever it still holds is let go, a refused + /// evaluation's too, and holding anything at its end is itself a refusal + /// (§19.6.88: "Ownership of a Mutex must be relinquished before + /// completion of any invocation"; an invocation the interpreter refuses + /// is one it completes, so it relinquishes for it). /// What it took goes to `took`, whichever way it ends. pub(crate) fn finish(mut self, r: Result, took: &mut Usage) -> Result { (took.steps, took.waited_us) = (self.steps, self.waited_us); @@ -301,7 +327,7 @@ impl<'a> Machine<'a> { fn define(&mut self, f: &Frame, p: &Path, o: Object) -> Result { let steps = &mut self.steps; - self.ns.create(f.scope, p, o, &mut || tick(steps)) + self.ns.create(f.arena, f.scope, p, o, &mut || tick(steps)) } fn resolve(&mut self, f: &Frame, p: &Path) -> Result { @@ -312,14 +338,20 @@ impl<'a> Machine<'a> { self.ns.object(id).cloned().ok_or_else(|| Error::NotFound(String::from("an object its method's exit destroyed"))) } - /// The integer a named child of `scope` evaluates to, if it exists. - pub(crate) fn named_int(&mut self, scope: NodeId, seg: Seg) -> Result, Error> { + /// What a named child of `scope` evaluates to, if it exists: a method of + /// no arguments invoked, anything else its value. + pub(crate) fn named_object(&mut self, scope: NodeId, seg: Seg) -> Result, Error> { let Some(id) = self.ns.child(scope, seg) else { return Ok(None) }; let v = match self.node_object(id)? { Object::Method(m) if m.args == 0 => self.invoke(id, m, Vec::new())?, _ => self.node_value(id)?, }; - to_int(&v, self.w).map(Some) + Ok(Some(v)) + } + + /// The integer a named child of `scope` evaluates to, if it exists. + pub(crate) fn named_int(&mut self, scope: NodeId, seg: Seg) -> Result, Error> { + self.named_object(scope, seg)?.map(|v| to_int(&v, self.w)).transpose() } pub(crate) fn evaluate(&mut self, id: NodeId, args: Vec) -> Result { @@ -349,7 +381,7 @@ impl<'a> Machine<'a> { } /// The object a reference refers to (§19.6.30). - fn deref(&mut self, r: &Ref) -> Result { + pub(crate) fn deref(&mut self, r: &Ref) -> Result { match r { Ref::Node(id) => self.node_value(*id), Ref::Slot(s) => Ok(slot_of(s)?.borrow().clone()), @@ -406,11 +438,14 @@ impl<'a> Machine<'a> { } self.levels.push(m.sync); } - let made = self.ns.mark(); - let mut f = Frame::new(node, args, table.clone(), self.held.len()); + let made = self.ns.mark(Arena::Method); + let mut f = Frame::new(node, args, table.clone(), self.held.len(), Arena::Method); let bytes = table.borrow(); let mut c = Cursor::new(&bytes, start, end); let flow = self.term_list(&mut f, &mut c); + // However the method ends, refused at the heap bound among the ways, + // what it defined goes: §5.5.2.3, objects created within a method + // "are destroyed on exit". self.ns.unwind(made); if m.serialized { self.levels.pop(); @@ -504,8 +539,12 @@ impl<'a> Machine<'a> { 0x80 => self.def_region(f, c), 0x81 | 0x86 | 0x87 => self.def_field(f, c), 0x82..=0x85 => self.def_scoped(f, c), - 0x88 => Err(Error::Unsupported("DataTableRegion")), - 0x20 => Err(Error::Unsupported("Load")), + 0x88 => self.def_data_region(f, c), + 0x20 => { + c.byte()?; + c.byte()?; + self.load_op(f, c).map(|_| Flow::Next) + } 0x21 | 0x22 => self.delay(f, c), 0x24 | 0x26 | 0x27 => self.sync_statement(f, c), 0x32 => self.fatal(f, c), @@ -568,6 +607,194 @@ impl<'a> Machine<'a> { Ok(Flow::Next) } + // ---- definition blocks (§5.4.2, §19.6.76) ---------------------------- + + /// A definition block's term list run in the static arena below `root`, + /// in this evaluation's integer width and under its bounds: what it + /// defines lives as long as the namespace, and a refusal takes all of it + /// off again. + pub(crate) fn run_block(&mut self, bytes: Vec, root: NodeId) -> Result<(), Error> { + // Held for as long as a method it defines refers to it. + let table = self.bytes(bytes)?; + let made = self.ns.mark(Arena::Static); + let mut f = Frame::new(root, Vec::new(), table.clone(), self.held.len(), Arena::Static); + let bytes = table.borrow(); + let mut c = Cursor::new(&bytes, toyos_acpi::SDT_HEADER_LEN, bytes.len()); + let r = self.term_list(&mut f, &mut c).and_then(|flow| match flow { + Flow::Next => Ok(()), + _ => Err(Error::Rule("a Return, Break or Continue at definition block level")), + }); + if r.is_err() { + self.ns.unwind(made); + } + r + } + + /// Load (§19.6.76): a definition block from a SystemMemory region, a + /// field unit of one at whole bytes, or a Buffer, loaded at the root. Its + /// Boolean is stored to the target and is the value. + fn load_op(&mut self, f: &mut Frame, c: &mut Cursor<'_>) -> Result { + let p = self.name(c)?; + let t = self.target(f, c)?; + let id = self.resolve(f, &p)?; + let source = match self.node_object(id)? { + Object::Region(r) if r.space == 0 => Source::Memory { at: r.base, len: r.len }, + Object::Field(u) => match &u.kind { + Kind::Region(r) if r.space == 0 && u.bit % 8 == 0 && u.len % 8 == 0 => { + let (offset, len) = (u.bit / 8, u.len / 8); + // The block lies wholly in the field, and the field in its region. + let len = len.min(r.len.saturating_sub(offset)); + Source::Memory { at: r.base.checked_add(offset).ok_or(Error::Rule("a field unit past the end of memory"))?, len } + } + _ => return Err(Error::Type("Load of a field unit that is no whole bytes of a SystemMemory region (§19.6.76)")), + }, + Object::Buf(b) => Source::Bytes(b.borrow().clone()), + _ => return Err(Error::Type("Load of an object that is no SystemMemory region, field unit or Buffer (§19.6.76)")), + }; + let root = self.ns.root(); + let loaded = self.load_block(f.by, source, root)?; + let v = Object::Int(self.w.bool(loaded)); + self.store(f, t, v.clone())?; + Ok(v) + } + + /// A String argument, one of a table's identifiers, no longer than + /// `most` bytes (§19.6.77: "a run-time error is generated"). + fn table_id(&mut self, f: &mut Frame, c: &mut Cursor<'_>, most: usize) -> Result, Error> { + let v = self.arg(f, c)?; + let s = to_str(&v, self.w)?; + if s.len() > most { + return Err(Error::Rule("a table's signature, OEM ID or OEM Table ID longer than its field (§19.6.77)")); + } + Ok(s) + } + + /// DataTableRegion (§19.6.25): a SystemMemory region over the table the + /// XSDT lists with this signature, OEM ID and OEM Table ID, its header + /// to its last byte. + fn def_data_region(&mut self, f: &mut Frame, c: &mut Cursor<'_>) -> Result { + c.byte()?; + c.byte()?; + let p = self.name(c)?; + let ids = [self.table_id(f, c, 4)?, self.table_id(f, c, 6)?, self.table_id(f, c, 8)?]; + let found = self.host.find_table(&ids[0], &ids[1], &ids[2]).map_err(|d| Error::Host(d.0))?; + let (base, len) = found.ok_or(Error::Rule("a DataTableRegion names a table the XSDT does not list (§19.6.25)"))?; + let r = self.meter.hold(Region { space: 0, base, len: len as u64, scope: f.scope })?; + self.define(f, &p, Object::Region(r))?; + Ok(Flow::Next) + } + + /// LoadTable (§19.6.77): the table the XSDT lists with this signature, + /// OEM ID and OEM Table ID loaded below the RootPathString, resolved from + /// the current scope, or the root where it is empty; then, where it + /// loaded and a ParameterPathString is given, ParameterData stored to + /// that object, below the root path unless it begins with `\` or `^`. + /// False where no table matches or the block is refused. + fn load_table(&mut self, f: &mut Frame, c: &mut Cursor<'_>) -> Result { + let ids = [self.table_id(f, c, 4)?, self.table_id(f, c, 6)?, self.table_id(f, c, 8)?]; + let root_text = { + let v = self.arg(f, c)?; + to_str(&v, self.w)? + }; + let parameter_text = { + let v = self.arg(f, c)?; + to_str(&v, self.w)? + }; + let data = self.arg(f, c)?; + let path = |m: &mut Self, text: &[u8]| -> Result { + m.charge(text.len())?; + Path::text(text).ok_or(Error::Rule("a LoadTable path that is no name (§19.6.77)")) + }; + let root = match root_text.as_slice() { + b"" | b"\\" => self.ns.root(), + text => { + let p = path(self, text)?; + self.resolve(f, &p)? + } + }; + let found = match self.host.find_table(&ids[0], &ids[1], &ids[2]) { + Ok(found) => found, + Err(d) => { + let by = self.path_of(f.by, None)?; + self.host.loaded(&RuntimeLoad { by: &by, header: None, refused: Some(&Error::Host(d.0)) }); + return Ok(Object::Int(0)); + } + }; + let Some((at, len)) = found else { return Ok(Object::Int(0)) }; + let loaded = self.load_block(f.by, Source::Memory { at, len: len as u64 }, root)?; + if loaded && !parameter_text.is_empty() { + let mut p = path(self, ¶meter_text)?; + if !(p.root || p.up > 0) { + let base = path(self, &root_text)?; + p = Path { root: base.root || root_text.is_empty(), up: base.up, segs: [base.segs, p.segs].concat() }; + } + let id = self.resolve(f, &p)?; + self.store(f, Target::Node(id), data)?; + } + Ok(Object::Int(self.w.bool(loaded))) + } + + /// A definition block loaded below `root` from `source` by the method + /// `by`, told to the host: `Ok(false)` where the block is refused — + /// malformed, colliding, failing its header, length or checksum, or + /// denied by the host while it loads — and nothing of it kept. A bound + /// of the evaluation, and Fatal, refuse the evaluation instead. + pub(crate) fn load_block(&mut self, by: NodeId, source: Source, root: NodeId) -> Result { + let by = self.path_of(by, None)?; + let mut header = None; + let r = self.block(source, &mut header).and_then(|bytes| self.run_block(bytes, root)); + let refused = r.as_ref().err(); + self.host.loaded(&RuntimeLoad { by: &by, header: header.as_ref().map(|h: &[u8; toyos_acpi::SDT_HEADER_LEN]| &h[..]), refused }); + match r { + Ok(()) => Ok(true), + Err(e @ (Error::Bound(_) | Error::Fatal { .. })) => Err(e), + Err(_) => Ok(false), + } + } + + /// A definition block's bytes, its header first, checked whole before + /// any of it runs (§5.2.6): a length that holds its header and lies + /// wholly in its source, and bytes that sum to zero. + fn block(&mut self, source: Source, header: &mut Option<[u8; toyos_acpi::SDT_HEADER_LEN]>) -> Result, Error> { + const HEADER: usize = toyos_acpi::SDT_HEADER_LEN; + let length = |head: &[u8]| u32::from_le_bytes([head[4], head[5], head[6], head[7]]) as usize; + let bytes = match source { + Source::Bytes(bytes) => { + if bytes.len() < HEADER { + return Err(Error::Table("a definition block shorter than its header (§5.2.6)")); + } + *header = bytes[..HEADER].try_into().ok(); + let declared = length(&bytes); + if declared > bytes.len() { + return Err(Error::Table("a definition block longer than the Buffer it is loaded from (§19.6.76)")); + } + bytes[..declared].to_vec() + } + Source::Memory { at, len } => { + if len < HEADER as u64 { + return Err(Error::Table("a definition block's region shorter than its header (§5.2.6)")); + } + let head = self.host.table(at, HEADER).map_err(|d| Error::Host(d.0))?; + *header = head.get(..HEADER).and_then(|h| h.try_into().ok()); + let declared = length(&head); + if declared as u64 > len { + return Err(Error::Table("a definition block longer than the region it is loaded from (§19.6.76)")); + } + bounded(declared)?; + self.charge(declared)?; + self.host.table(at, declared.max(HEADER)).map_err(|d| Error::Host(d.0))? + } + }; + if bytes.len() < HEADER || length(&bytes) != bytes.len() { + return Err(Error::Table("a definition block whose length does not hold its header, or is not what was read (§5.2.6)")); + } + self.charge(bytes.len())?; + if bytes.iter().fold(0u8, |sum, &b| sum.wrapping_add(b)) != 0 { + return Err(Error::Table("a definition block whose bytes do not sum to zero (§5.2.6)")); + } + Ok(bytes) + } + // ---- named objects (§20.2.5.1, §20.2.5.2) ------------------------------ fn def_alias(&mut self, f: &mut Frame, c: &mut Cursor<'_>) -> Result { @@ -576,7 +803,7 @@ impl<'a> Machine<'a> { let alias = self.name(c)?; let target = self.resolve(f, &source)?; let steps = &mut self.steps; - self.ns.alias(f.scope, &alias, target, &mut || tick(steps))?; + self.ns.alias(f.arena, f.scope, &alias, target, &mut || tick(steps))?; Ok(Flow::Next) } @@ -749,6 +976,8 @@ impl<'a> Machine<'a> { }; let (mut access, lock, update) = flags(l.byte()?); let mut bit = 0u64; + let mut protocol = (0, 0); + let mut connection = None; while !l.done() { self.step()?; match l.peek()? { @@ -758,22 +987,27 @@ impl<'a> Machine<'a> { } 0x01 | 0x03 => { let ext = l.byte()? == 0x03; - access = flags(l.byte()?).0; - l.byte()?; - if ext { - l.byte()?; - } + let access_type = l.byte()?; + access = flags(access_type).0; + let access_attrib = l.byte()?; + let length = if ext { Some(l.byte()?) } else { None }; + protocol = attrib(access_type, access_attrib, length); } 0x02 => { - // ConnectField: the connection of a GeneralPurposeIO or - // GenericSerialBus field, which this interpreter does not - // access. + // ConnectField (§20.2.5.2): a resource descriptor the + // units after it reach a GeneralPurposeIO pin or a + // GenericSerialBus device by. l.byte()?; - if l.peek()? == 0x11 { - self.data_object(f, &mut l)?; + let named = if l.peek()? == 0x11 { + match self.data_object(f, &mut l)? { + Object::Buf(b) => Connection::Bytes(b), + _ => return Err(l.malformed("a ConnectField's BufferData is no Buffer")), + } } else { - self.name(&mut l)?; - } + let p = self.name(&mut l)?; + Connection::Named(self.resolve(f, &p)?) + }; + connection = Some((named, bit)); } _ => { let seg = l.seg()?; @@ -782,7 +1016,7 @@ impl<'a> Machine<'a> { return Err(l.malformed("a field unit of zero bits")); } let to = bit.checked_add(len).filter(|&e| e <= 1 << 62).ok_or(l.malformed("a FieldList overflows"))?; - let unit = self.meter.hold(Field { kind: kind.clone(), bit, len, access, lock, update })?; + let unit = self.meter.hold(Field { kind: kind.clone(), bit, len, access, lock, update, attrib: protocol, connection: connection.clone() })?; self.define(f, &Path { root: false, up: 0, segs: vec![seg] }, Object::Field(unit))?; bit = to; } @@ -892,6 +1126,9 @@ impl<'a> Machine<'a> { /// `false` where `\_GL`'s firmware side held it past `within` /// milliseconds, and nothing was acquired. fn acquire(&mut self, m: &Kept, within: Option) -> Result { + if self.held.len() >= crate::MAX_HELD { + return Err(Error::Bound("more Mutexes held at once than one evaluation holds")); + } // §19.6.88: an Acquire's SyncLevel is equal to or above the current one. if m.held.get() == 0 && self.levels.last().is_some_and(|&l| m.sync < l) { return Err(Error::Rule("Acquire of a Mutex below the current SyncLevel (§19.6.88)")); @@ -1332,9 +1569,17 @@ impl<'a> Machine<'a> { } } + /// Store (§19.6.128): its value is its source's, or, for a field whose + /// BufferAcc write is bidirectional, the response the write became + /// (§5.5.2.4.5.1, §5.5.2.4.7.1). fn store_op(&mut self, f: &mut Frame, c: &mut Cursor<'_>) -> Result { let v = self.arg(f, c)?; let t = self.super_name(f, c)?; + if let Target::Node(id) = t + && let Object::Field(x) = self.node_object(id)? + { + return Ok(self.write_field_answering(&x, v.clone())?.unwrap_or(v)); + } self.store(f, t, v.clone())?; Ok(v) } @@ -1483,8 +1728,8 @@ impl<'a> Machine<'a> { 0x28 => self.bcd_in(f, c), 0x29 => self.bcd_out(f, c), 0x33 => Ok(Object::Int(self.host.timer() & self.w.ones())), - 0x1F => Err(Error::Unsupported("LoadTable")), - 0x20 => Err(Error::Unsupported("Load")), + 0x1F => self.load_table(f, c), + 0x20 => self.load_op(f, c), _ => Err(Error::Malformed { at, why: "not an expression (§20.2.5.4)" }), } } diff --git a/system/acpiserver/aml/src/field.rs b/system/acpiserver/aml/src/field.rs index 9aef5d90791..d64f3058cdf 100644 --- a/system/acpiserver/aml/src/field.rs +++ b/system/acpiserver/aml/src/field.rs @@ -1,9 +1,16 @@ //! Field units over operation regions (§19.6.47, §19.6.63, §19.6.7) and //! buffer fields over buffers (§19.6.18-23, §19.6.62). //! -//! A field is read and written in access units: aligned, of the width its -//! access type names, each one [`Host`](crate::Host) access. A unit the field -//! covers in part is completed by the field's update rule. A Lock field holds +//! A field of a space addressed by offset is read and written in access +//! units: aligned, of the width its access type names, each one +//! [`Host`](crate::Host) access. A unit the field covers in part is +//! completed by the field's update rule. A field of a space whose protocol is +//! command- or connection-based (§5.5.2.4: SMBus, IPMI, GeneralPurposeIO, +//! GenericSerialBus, PlatformRtMechanism, FFixedHW) is one +//! [`Transfer`](crate::Transfer) whole, with the protocol its AccessAs names +//! and the descriptor its Connection names; a BufferAcc field's value is the +//! buffer the host answers, and a store to it answers the response its +//! buffer became (§5.5.2.4.5.1). A Lock field holds //! the Global Lock across the whole access. An IndexField reaches a unit by //! writing its byte offset to the index field and then accessing the data //! field; a BankField writes its bank value to the bank field first. @@ -15,6 +22,7 @@ //! and an Integer stored, without the heap: an interpreter that is full //! still does both. +use alloc::string::String; use alloc::vec; use alloc::vec::Vec; @@ -22,7 +30,7 @@ use crate::exec::Machine; use crate::name::Seg; use crate::namespace::NodeId; use crate::object::{fit, to_buf, to_int, Bytes, Kept, Object}; -use crate::{Address, Error, MAX_BYTES}; +use crate::{Address, Error, Transfer, MAX_BYTES}; pub(crate) struct Region { pub(crate) space: u8, @@ -56,6 +64,46 @@ pub(crate) struct Field { pub(crate) lock: bool, /// UpdateRule (§20.2.5.2): 0 Preserve, 1 WriteAsOnes, 2 WriteAsZeros. pub(crate) update: u8, + /// The protocol the AccessAs before it names (Table 5.184), 0 for none, + /// and the byte count of AttribBytes and the raw protocols. + pub(crate) attrib: (u8, u8), + /// The Connection before it (§19.6.15), and the field's bit at which the + /// units after that connection begin. + pub(crate) connection: Option<(Connection, u64)>, +} + +/// A Connection's resource descriptor (§19.6.15): a buffer the field list +/// holds, or a named object read as a buffer when the field is accessed. +#[derive(Clone)] +pub(crate) enum Connection { + Bytes(Bytes), + Named(NodeId), +} + +/// A space whose protocol is command- or connection-based (§5.5.2.4): its +/// field is accessed whole, through the host. +pub(crate) fn carried(space: u8) -> bool { + matches!(space, 0x04 | 0x07 | 0x08 | 0x09 | 0x0B | 0x7F) +} + +/// A space whose protocol is BufferAcc's: its field's value is a buffer. +fn buffered(space: u8) -> bool { + matches!(space, 0x04 | 0x07 | 0x09 | 0x0B | 0x7F) +} + +/// The AccessAs attribute an AccessField's or ExtendedAccessField's bytes +/// name (§20.2.5.2): the AccessType byte's bits 7:6 name AttribBytes, +/// AttribRawBytes or AttribRawProcessBytes of the AccessAttrib byte's count, +/// or, as 0, the AccessAttrib byte itself; an ExtendedAccessField names its +/// attribute and its count. +pub(crate) fn attrib(access_type: u8, access_attrib: u8, extended: Option) -> (u8, u8) { + match (extended, access_type >> 6) { + (Some(length), _) => (access_attrib, length), + (None, 0) => (access_attrib, 0), + (None, 1) => (0x0B, access_attrib), + (None, 2) => (0x0E, access_attrib), + (None, _) => (0x0F, access_attrib), + } } pub(crate) struct BufField { @@ -102,6 +150,19 @@ pub(crate) fn flags(b: u8) -> (u8, bool, u8) { (b & 0x0F, b & 0x10 != 0, (b >> 5) & 0x3) } +/// The region a field of a carried space is in, or `None` for a field +/// accessed by unit; a BankField or IndexField reaches a carried space by no +/// unit, and is refused. +fn carried_region(f: &Field) -> Result>, Error> { + match &f.kind { + Kind::Region(r) if carried(r.space) => Ok(Some(r.clone())), + Kind::Bank { region, .. } if carried(region.space) => { + Err(Error::Rule("a BankField of a space whose protocol is command- or connection-based (§5.5.2.4)")) + } + _ => Ok(None), + } +} + impl Machine<'_> { /// The bytes of one access unit, by the access type and, for an access /// type of AnyAcc, the narrowest naturally aligned unit that holds the @@ -112,9 +173,9 @@ impl Machine<'_> { Kind::Region(r) | Kind::Bank { region: r, .. } => Some(r), Kind::Index { .. } => None, }; - // Table 19.34: EmbeddedControl, SystemCMOS, GeneralPurposeIO and PCC - // permit byte access only. - let bytes_only = matches!(region.map(|r| r.space), Some(0x03 | 0x05 | 0x08 | 0x0A)); + // Table 19.34: EmbeddedControl, SystemCMOS and PCC permit byte access + // only; GeneralPurposeIO is carried whole. + let bytes_only = matches!(region.map(|r| r.space), Some(0x03 | 0x05 | 0x0A)); let within = |end: u64| region.is_none_or(|r| end <= r.len); let w = match f.access { 0 if bytes_only => 1, @@ -129,7 +190,7 @@ impl Machine<'_> { 2 => 2, 3 => 4, 4 => 8, - 5 => return Err(Error::Unsupported("BufferAcc, which only the SMBus, IPMI and GenericSerialBus spaces use")), + 5 => return Err(Error::Rule("BufferAcc in a space addressed by offset, which takes no buffer (§19.6.47)")), _ => return Err(Error::Rule("an access by a reserved AccessType (§20.2.5.2)")), }; if bytes_only && w != 1 { @@ -140,7 +201,9 @@ impl Machine<'_> { fn address(&mut self, r: &Region, offset: u64, w: u64) -> Result { let past = || Error::Rule("a field access runs past its operation region (§19.6.47)"); - if offset.checked_add(w).ok_or_else(past)? > r.len { + // A PciBarTarget region's length is its BAR's (§5.5.2.4.4.1), which + // the host reads. + if r.space != 0x06 && offset.checked_add(w).ok_or_else(past)? > r.len { return Err(past()); } let at = r.base.checked_add(offset).ok_or_else(past)?; @@ -162,17 +225,28 @@ impl Machine<'_> { let at = u8::try_from(at).ok().filter(|&a| u64::from(a) + w <= 0x100); Ok(Address::EmbeddedControl(at.ok_or(Error::Rule("an EmbeddedControl access past its 256 bytes (§12)"))?)) } - 0x04 => Err(Error::Unsupported("the SMBus address space")), - 0x05 => Err(Error::Unsupported("the SystemCMOS address space")), - 0x06 => Err(Error::Unsupported("the PciBarTarget address space")), - 0x07 => Err(Error::Unsupported("the IPMI address space")), - 0x08 => Err(Error::Unsupported("the GeneralPurposeIO address space")), - 0x09 => Err(Error::Unsupported("the GenericSerialBus address space")), - 0x0A => Err(Error::Unsupported("the PCC address space")), - 0x0B => Err(Error::Unsupported("the PlatformRtMechanism address space")), - 0x7F => Err(Error::Unsupported("the FFixedHW address space")), - 0x0C..=0x7E => Err(Error::Unsupported("a reserved address space (Table 5.182)")), - _ => Err(Error::Unsupported("an OEM-defined address space")), + 0x05 => { + // The RTC/CMOS device's bytes, 256 at the most an extension names. + let at = u8::try_from(at).ok().filter(|&a| u64::from(a) + w <= 0x100); + Ok(Address::SystemCmos(at.ok_or(Error::Rule("a SystemCMOS access past the CMOS's 256 bytes (§5.5.2.4.3)"))?)) + } + 0x06 => { + // §5.5.2.4.4.1: the region's offset is a BAR's in the + // function's configuration space, 0x10 to 0x24. + let bar = u8::try_from(r.base).ok().filter(|b| (0x10..=0x24).contains(b) && b % 4 == 0); + let bar = bar.ok_or(Error::Rule("a PciBarTarget region whose offset is no BAR's (§5.5.2.4.4.1)"))?; + let p = self.pci(r)?; + Ok(Address::PciBarTarget { segment: p.segment, bus: p.bus, device: p.device, function: p.function, bar, offset }) + } + 0x0A => { + // §5.5.2.4.8.2: the region's offset is the subspace's ID. + let subspace = u8::try_from(r.base).map_err(|_| Error::Rule("a PCC region whose subspace ID is past 255 (§5.5.2.4.8.2)"))?; + let offset = u32::try_from(offset).map_err(|_| past())?; + Ok(Address::Pcc { subspace, offset }) + } + 0x80..=0xFF => Ok(Address::Oem { space: r.space, offset: at }), + 0x0C..=0x7E => Err(Error::Rule("a region of a reserved address space (Table 5.182)")), + _ => Err(Error::Rule("a field of a space whose protocol is the host's, accessed by unit")), } } @@ -181,16 +255,21 @@ impl Machine<'_> { /// the high word, function in the low), in the segment group the host /// bridge's `_SEG` names or 0 without one (§6.5.6). The host bridge is /// the nearest device naming a `_BBN`, which is the bus directly below it - /// (§6.5.5); each device between it and the region's is a PCI-to-PCI + /// (§6.5.5), or else whose `_HID` or one of whose `_CID`s (§6.1.5, + /// §6.1.2) names a PCI or PCI Express host bridge, PNP0A03 or PNP0A08, + /// on bus 0, the only root bus a machine that names no `_BBN` has; each + /// device between it and the region's is a PCI-to-PCI /// bridge by its Header Type register, whose Secondary Bus Number /// register is the bus below it (PCI-to-PCI Bridge Architecture /// Specification 1.2, §3.2.5.4). A region declared in the host bridge /// itself addresses the bridge. /// /// §6.5.4 holds a PCI_Config region accessible always only on a root bus - /// naming a `_BBN`, and one below a bridge once "the bridge controller - /// has been programmed with a bus number": a region below anything else - /// is refused, where any bus chosen for it would be another device's. + /// naming a `_BBN`; one on a root bus that names none once its `_REG(2, + /// 1)` has run, which is the caller's to run; and one below a bridge once + /// "the bridge controller has been programmed with a bus number": a + /// region below anything else is refused, where any bus chosen for it + /// would be another device's. /// /// Every access asks again, firmware's methods and the bridges both: /// nothing is kept that a bridge renumbered since would make stale. A @@ -204,7 +283,7 @@ impl Machine<'_> { loop { self.step()?; if matches!(self.ns.object(host), Some(Object::Device)) { - if self.ns.child(host, bbn).is_some() { + if self.ns.child(host, bbn).is_some() || self.host_bridge(host)? { break; } if below.len() > usize::from(u8::MAX) { @@ -213,7 +292,7 @@ impl Machine<'_> { below.push(host); } let above = self.ns.parent(host); - host = above.ok_or(Error::Unsupported("a PCI_Config region below no host bridge, which names a _BBN"))?; + host = above.ok_or(Error::Rule("a PCI_Config region below no host bridge, which names a _BBN or is PNP0A03 or PNP0A08 by its _HID or _CID"))?; } let bus = self.named_int(host, bbn)?.unwrap_or(0); let segment = self.named_int(host, Seg(*b"_SEG"))?.unwrap_or(0); @@ -251,6 +330,32 @@ impl Machine<'_> { self.function(device, segment, bus) } + /// Whether a device is a PCI host bridge by its `_HID` or a `_CID`: an + /// EISA ID's integer or a string, a `_CID` one or a package of them. + fn host_bridge(&mut self, device: NodeId) -> Result { + // EisaId ("PNP0A03") and EisaId ("PNP0A08") as AML stores them. + const IDS: [u64; 2] = [0x030A_D041, 0x080A_D041]; + const TEXT: [&[u8]; 2] = [b"PNP0A03", b"PNP0A08"]; + for seg in [Seg(*b"_HID"), Seg(*b"_CID")] { + let ids = match self.named_object(device, seg)? { + None => continue, + Some(Object::Pkg(p)) => p.borrow().clone(), + Some(id) => vec![id], + }; + for id in ids { + let hit = match self.resolve_lazy(id)? { + Object::Int(id) => IDS.contains(&id), + Object::Str(text) => TEXT.contains(&text.borrow().as_slice()), + _ => false, + }; + if hit { + return Ok(true); + } + } + } + Ok(false) + } + /// The function a device's `_ADR` names on `bus` (§6.1.1). fn function(&mut self, device: NodeId, segment: u16, bus: u8) -> Result { let adr = Seg(*b"_ADR"); @@ -300,11 +405,48 @@ impl Machine<'_> { pub(crate) fn read_field(&mut self, f: &Field) -> Result { self.enter()?; - let r = self.locked(f.lock, |m| m.read_units(f)); + let r = self.locked(f.lock, |m| match carried_region(f)? { + Some(region) => m.transfer(f, ®ion, None), + None => m.read_units(f), + }); self.leave(); r } + /// One [`Transfer`] of a carried field whole: a read where `write` is + /// `None`, else a write of its bytes. A BufferAcc field's value is the + /// buffer the host answered; a GeneralPurposeIO field's, its pins as an + /// Integer or a Buffer as wide as it is (§19.6.47). + fn transfer(&mut self, f: &Field, region: &Region, write: Option<&[u8]>) -> Result { + let connection = match &f.connection { + None => None, + Some((Connection::Bytes(b), at)) => Some((b.clone(), *at)), + Some((Connection::Named(id), at)) => match self.ns.object(*id).cloned() { + Some(Object::Buf(b)) => Some((b, *at)), + Some(Object::BufField(b)) => match self.read_buf_field(&b)? { + Object::Buf(b) => Some((b, *at)), + _ => return Err(Error::Type("a Connection whose descriptor is no Buffer (§19.6.15)")), + }, + Some(_) => return Err(Error::Type("a Connection whose descriptor is no Buffer (§19.6.15)")), + None => return Err(Error::NotFound(String::from("a Connection's descriptor its method's exit destroyed"))), + }, + }; + let held = connection.as_ref().map(|(b, at)| (b.borrow(), *at)); + let at = Transfer { + space: region.space, + region: region.base, + bit: f.bit, + bits: f.len, + attrib: f.attrib.0, + length: f.attrib.1, + connection: held.as_ref().map(|(b, at)| (b.as_slice(), *at)), + }; + let answer = self.host.transfer(&at, write).map_err(|d| Error::Host(d.0))?; + drop(held); + crate::object::bounded(answer.len())?; + if buffered(region.space) { self.new_buf(answer) } else { self.value(fit(answer, bytes_for(f.len)?), f.len) } + } + /// Whether a field's value is a Buffer (§19.6.47): an Integer when it /// fits one, else a Buffer. fn wide(&self, bits: u64) -> bool { @@ -352,6 +494,28 @@ impl Machine<'_> { /// firmware's methods, which may store to the source, and the store is of /// what the source held when it began. pub(crate) fn write_field(&mut self, f: &Field, v: Object) -> Result<(), Error> { + self.write_field_answering(f, v).map(drop) + } + + /// A store to a field, answering the response a carried BufferAcc + /// field's write became, and `None` for any other field. + pub(crate) fn write_field_answering(&mut self, f: &Field, v: Object) -> Result, Error> { + if let Some(region) = carried_region(f)? { + let bytes = match &v { + Object::Int(x) => fit(x.to_le_bytes().to_vec(), bytes_for(f.len)?), + Object::Buf(_) | Object::Str(_) => to_buf(&v, self.w)?, + _ => return Err(Error::Type("a store to a field unit of an object that is not an integer, buffer or string")), + }; + let copy = self.bytes(bytes)?; + self.enter()?; + let r = self.locked(f.lock, |m| m.transfer(f, ®ion, Some(©.borrow()))); + self.leave(); + return r.map(|answer| buffered(region.space).then_some(answer)); + } + self.write_by_units(f, v).map(|()| None) + } + + fn write_by_units(&mut self, f: &Field, v: Object) -> Result<(), Error> { let n = bytes_for(f.len)?; let (int, copy, held); let (source, piece): (&[u8], usize) = match &v { diff --git a/system/acpiserver/aml/src/lib.rs b/system/acpiserver/aml/src/lib.rs index 69fc21fab7c..f34e07545f9 100644 --- a/system/acpiserver/aml/src/lib.rs +++ b/system/acpiserver/aml/src/lib.rs @@ -5,22 +5,39 @@ //! A definition block is firmware's, and so untrusted: whatever its bytes, //! [`Interpreter::load`] and [`Interpreter::evaluate`] return a value or a //! named [`Error`], never panic, and never run unbounded — every evaluation -//! is bounded in steps, nesting, object size and time asked to sleep, and +//! is bounded in steps, nesting, object size, Mutexes held and time asked to +//! sleep, and //! what an interpreter holds of tables, namespace and objects is bounded in //! sum, in bytes of heap ([`MAX_LIVE`]). A load refused leaves the namespace //! without anything that table created, and the interpreter holding what it -//! held before. What the last load or evaluation took of those bounds is -//! [`Interpreter::usage`]'s, a refused one's too. +//! held before. An evaluation refused, at the heap bound as at any other, +//! gives back everything its methods made (§5.5.2.3: they "are destroyed on +//! exit") and every Mutex they held (§19.6.88), and keeps what they stored +//! into an object that outlives them, which ACPI 6.5 never takes back: no +//! store is journalled. What the +//! last load or evaluation took of those bounds is [`Interpreter::usage`]'s, +//! a refused one's too. //! //! [`Interpreter::walk`] reads the namespace an interpreter holds, and runs //! nothing of it: which objects a caller then evaluates, and in which order, //! is the caller's. //! +//! A definition block a method loads (`Load`, §19.6.76) lives as long as +//! the namespace, and what the method itself defined dies at its exit: the +//! namespace keeps the two apart (`namespace`). A block refused loads false +//! and keeps nothing; a bound it reaches refuses the evaluation, whose +//! bounds it shares. Its bytes come through the [`Host`], which reads them +//! only from memory Table 15.6 calls reserved or NVS, and each load is told +//! to it ([`Host::loaded`]). +//! //! The library touches no hardware. An operation region's field is read and -//! written through the [`Host`] the caller passes, in SystemMemory, -//! SystemIO, PCI_Config and EmbeddedControl space; an access in any other -//! space is refused as [`Error::Unsupported`], as are `Load`, `LoadTable` -//! and `DataTableRegion`. Only one invocation runs at a time, so a Mutex is +//! written through the [`Host`] the caller passes, in every address space of +//! Table 5.182: a space addressed by offset as an [`Address`] an access unit +//! at a time, and a space whose protocol is command- or connection-based +//! (§5.5.2.4) as one [`Transfer`] of the field whole. Which spaces it serves +//! is the host's to say, and it denies the rest by name. A table the XSDT +//! lists is found through it too, for `LoadTable` and `DataTableRegion`. +//! Only one invocation runs at a time, so a Mutex is //! never contended and an Event is never signalled by anyone else; `\_GL`'s //! other owner is the firmware, whose side the [`Host`] waits out. //! @@ -49,12 +66,13 @@ mod namespace; mod object; mod stream; +use alloc::boxed::Box; use alloc::rc::Rc; use alloc::string::String; use alloc::vec::Vec; use core::cell::Cell; -use exec::{Frame, Machine}; +use exec::Machine; use name::{Path, Seg}; use namespace::Namespace; use object::{Body, Meter, Method, Mutex, Object, Ref}; @@ -78,6 +96,29 @@ pub(crate) const MAX_ELEMENTS: usize = 1 << 16; /// record, string, buffer and package; what one holds beyond it while it /// runs is `object::Meter`'s to say. pub const MAX_LIVE: usize = 16 << 20; +/// The most heap one load or evaluation holds while it runs: [`MAX_LIVE`], +/// and what it holds beyond the meter's count, each part bounded by another +/// of this interpreter's bounds: +/// - an arena's old slots while a doubling moves it, at most half what the +/// meter admits; +/// - one operator's bytes before the meter holds them, Concatenate's the +/// most at its two operands' copies and their sum, four of the largest +/// object, one level's of them however deep they nest; +/// - a table's or a loaded block's bytes while they are read in, the +/// largest object; +/// - for each frame of the deepest nest, its locals' and arguments' slots, +/// a name of 255 segments and a PCI_Config region's walk of 257 devices; +/// - a record and a level for each Mutex held, at most [`MAX_HELD`]; +/// - a node's path for a Notify, a reference or a refusal, a segment and a +/// separator for each level as deep as the meter admits a node. +pub const MAX_RUNNING: usize = MAX_LIVE + + MAX_LIVE / 2 + + 5 * MAX_BYTES + + MAX_DEPTH as usize * exec::FRAME_HEAP + + MAX_HELD * (core::mem::size_of::>() + 1) * 2 + + namespace::DEEPEST * (2 * core::mem::size_of::() + 5); +/// The most Mutex acquisitions one evaluation holds at once. +pub(crate) const MAX_HELD: usize = 256; /// The bytes of work one step stands for: a step for every this many bytes /// an operation makes, copies, compares or walks. pub(crate) const WORK_PER_STEP: usize = 64; @@ -135,9 +176,6 @@ pub enum Error { Fatal { kind: u8, code: u32, arg: u64 }, /// What this interpreter bounds was exceeded. Bound(&'static str), - /// Something the specification defines that this interpreter does not - /// carry. - Unsupported(&'static str), /// The [`Host`] refused an access. Host(String), /// A function between a PCI_Config region's device and its host bridge @@ -161,7 +199,8 @@ pub enum Access { QWord, } -/// Where an access to an operation region lands (Table 5.182). +/// Where an access to an operation region of a space addressed by offset +/// lands (Table 5.182). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Address { Memory(u64), @@ -169,6 +208,42 @@ pub enum Address { Io(u16), PciConfig { segment: u16, bus: u8, device: u8, function: u8, offset: u16 }, EmbeddedControl(u8), + /// A byte of the RTC/CMOS device's non-volatile memory (§5.5.2.4.3). + SystemCmos(u8), + /// `offset` into the space the BAR at `bar` in the function's + /// configuration space decodes (§5.5.2.4.4): its size, I/O or memory, + /// is the BAR's. + PciBarTarget { segment: u16, bus: u8, device: u8, function: u8, bar: u8, offset: u64 }, + /// `offset` into the shared memory after the signature of PCC subspace + /// `subspace` (§5.5.2.4.8). + Pcc { subspace: u8, offset: u32 }, + /// An OEM-defined space, 0x80 to 0xFF. + Oem { space: u8, offset: u64 }, +} + +/// A field of a space whose protocol is command- or connection-based: +/// SMBus, IPMI, GeneralPurposeIO, GenericSerialBus, PlatformRtMechanism and +/// FFixedHW (§5.5.2.4), accessed whole. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Transfer<'a> { + /// The space (Table 5.182). + pub space: u8, + /// The region's offset: SMBus's and IPMI's device and initial command, + /// GenericSerialBus's initial command, a PCC-like subspace's number. + pub region: u64, + /// The field's place in its region, in bits from its start, and its + /// length: a command value is its byte offset, and a GeneralPurposeIO + /// field's pins are its bits. + pub bit: u64, + pub bits: u64, + /// The protocol its AccessAs names (Table 5.184), 0 where none does, and + /// the byte count an AttribBytes, AttribRawBytes or + /// AttribRawProcessBytes carries. + pub attrib: u8, + pub length: u8, + /// The resource descriptor its Connection names (§19.6.15), and the bit + /// of the region at which the units that connection reaches begin. + pub connection: Option<(&'a [u8], u64)>, } /// What the interpreter asks of the operating system. @@ -193,6 +268,39 @@ pub trait Host { fn global_take_within(&mut self, ms: u16) -> Result; /// Gives back the Global Lock a take took. fn global_release(&mut self) -> Result<(), Denied>; + /// `len` bytes of memory at `at`, of a definition block `Load` reads + /// from a SystemMemory region or a field of one (§19.6.76): "Any table + /// loaded via an operation region must be in memory marked as + /// AddressRangeReserved or AddressRangeNVS", which Table 15.6 maps the + /// firmware's memory types to, and any other is refused. + fn table(&mut self, at: u64, len: usize) -> Result, Denied>; + /// A definition block an evaluation loaded at run time, or refused. + fn loaded(&mut self, load: &RuntimeLoad<'_>); + /// A field of a space whose protocol is the host's, read where `write` + /// is `None` and written with it otherwise: what it answers is the + /// field's value, or the response a write's buffer becomes (§5.5.2.4.5, + /// §5.5.2.4.7), whatever its length. + fn transfer(&mut self, at: &Transfer<'_>, write: Option<&[u8]>) -> Result, Denied>; + /// The table the XSDT lists with this signature, OEM ID and OEM Table ID + /// (§19.6.77, §19.6.25), each compared byte for byte and a shorter one + /// padded: its address and length, `None` where none matches. "Any table + /// referenced by LoadTable must be in memory marked by + /// AddressRangeReserved or AddressRangeNVS", as by a Data Table Region, + /// and one elsewhere is denied. + fn find_table(&mut self, signature: &[u8], oem_id: &[u8], oem_table_id: &[u8]) -> Result, Denied>; +} + +/// A definition block an evaluation loaded at run time (§19.6.76), or tried +/// to. +#[derive(Debug)] +pub struct RuntimeLoad<'a> { + /// The absolute path of the method that loaded it, `\` for a table's + /// load. + pub by: &'a str, + /// Its header (§5.2.6), where it was read. + pub header: Option<&'a [u8]>, + /// Why it was refused, and nothing of it kept. + pub refused: Option<&'a Error>, } /// An object as the caller receives or passes it. @@ -205,6 +313,10 @@ pub enum Value { Package(Vec), /// A reference to the named object at this absolute path. Reference(String), + /// A reference to an unnamed object — a package's element, a byte or + /// field of a buffer, a LocalX or ArgX — as what it referred to when it + /// was handed over. Only AML makes one, so it is refused as an argument. + Unnamed(Box), } /// What a load or an evaluation took of its bounds, a refused one as well. @@ -234,7 +346,8 @@ pub enum Kind { Event, Method, Mutex, - OperationRegion, + /// In this address space (Table 5.182). + OperationRegion { space: u8 }, PowerResource, /// The Processor object ACPI 6.4 deprecated (ACPI 6.3A §19.6.108). Processor, @@ -252,6 +365,10 @@ pub struct Entry { /// below an object follows it at a greater depth, so a caller leaves a /// subtree out by skipping to the next entry no deeper than its top. pub depth: u32, + /// Its place among the objects the interpreter holds, in the order they + /// were defined: an object a load defines after [`Interpreter::defined`] + /// was read is at that place or past it. + pub serial: u32, pub name: [u8; 4], pub kind: Kind, } @@ -275,7 +392,7 @@ impl Iterator for Walk<'_> { type Item = Entry; fn next(&mut self) -> Option { - let (depth, seg, object) = self.0.step()?; + let (depth, serial, seg, object) = self.0.step()?; let kind = match object { Object::Uninit => Kind::Uninitialized, Object::Int(_) => Kind::Integer, @@ -293,9 +410,9 @@ impl Iterator for Walk<'_> { Object::Method(_) => Kind::Method, Object::Mutex(_) => Kind::Mutex, Object::Event(_) => Kind::Event, - Object::Region(_) => Kind::OperationRegion, + Object::Region(r) => Kind::OperationRegion { space: r.space }, }; - Some(Entry { depth, name: seg.0, kind }) + Some(Entry { depth, serial, name: seg.0, kind }) } } @@ -323,7 +440,7 @@ impl Interpreter { let root = ns.root(); let mut put = |name: &[u8; 4], o: Object| { let p = Path { root: true, up: 0, segs: alloc::vec![Seg(*name)] }; - ns.create(root, &p, o, &mut || Ok(())).expect("the root is empty and every predefined name differs"); + ns.create(namespace::Arena::Static, root, &p, o, &mut || Ok(())).expect("the root is empty and every predefined name differs"); }; for scope in [b"_GPE", b"_PR_", b"_SB_", b"_SI_", b"_TZ_"] { put(scope, Object::Scope); @@ -357,22 +474,12 @@ impl Interpreter { (b"SSDT", None) => return Err(Error::Table("an SSDT before the DSDT, whose revision sets every integer's width")), _ => return Err(Error::Table("not a DSDT or SSDT (§5.2.11)")), }; - // Held for as long as a method it defines refers to it. - let table = self.meter.bytes(bytes)?; - let (root, made) = (self.ns.root(), self.ns.mark()); - let mut f = Frame::new(root, Vec::new(), table.clone(), 0); + let root = self.ns.root(); let mut m = Machine::new(&mut self.ns, host, w, self.meter.clone()); - let bytes = table.borrow(); - let mut c = stream::Cursor::new(&bytes, toyos_acpi::SDT_HEADER_LEN, bytes.len()); - let r = m.term_list(&mut f, &mut c).and_then(|flow| match flow { - exec::Flow::Next => Ok(()), - _ => Err(Error::Rule("a Return, Break or Continue at definition block level")), - }); + let r = m.run_block(bytes, root); let r = m.finish(r, &mut self.last); if r.is_ok() { self.width = Some(w); - } else { - self.ns.unwind(made); } r } @@ -405,6 +512,19 @@ impl Interpreter { self.last } + /// How many objects the interpreter holds that the tables defined and it + /// predefined: what a [`Walk`] entry's `serial` is read against. + pub fn defined(&self) -> u32 { + self.ns.defined() + } + + /// The slots the namespace's static arena holds, which the meter holds + /// it at: what the tables' objects have made it, read rather than + /// derived. + pub fn slots(&self) -> usize { + self.ns.slots() + } + /// Every object the loaded tables and this interpreter defined, read-only /// and without evaluating any: each after the object it is in, siblings /// in the order they were declared, a table's after those of the tables @@ -429,6 +549,7 @@ impl Interpreter { self.meter.list(p.iter().map(|e| self.object_of(e, w, depth + 1)).collect::>()?)?, ), Value::Reference(path) => Object::Ref(Ref::Node(self.named(path)?)), + Value::Unnamed(_) => return Err(Error::Type("an argument that is a reference to an unnamed object, which only AML makes")), }) } @@ -485,6 +606,11 @@ fn value_of(m: &mut Machine<'_>, meter: &Meter, handed: &mut usize, o: Object, d hand(meter, handed, path.capacity())?; Value::Reference(path) } - _ => return Err(Error::Unsupported("a reference to an unnamed object, handed to the caller")), + Object::Ref(r) => { + hand(meter, handed, core::mem::size_of::())?; + let referred = m.deref(&r)?; + Value::Unnamed(Box::new(value_of(m, meter, handed, referred, depth + 1)?)) + } + _ => return Err(Error::Type("an object handed to the caller that is no value (§19.6.101)")), }) } diff --git a/system/acpiserver/aml/src/namespace.rs b/system/acpiserver/aml/src/namespace.rs index cf888b4c159..6bc8eb148f9 100644 --- a/system/acpiserver/aml/src/namespace.rs +++ b/system/acpiserver/aml/src/namespace.rs @@ -1,26 +1,36 @@ //! The one namespace every definition block loads into (§5.3). //! -//! Nodes live in an arena in the order they were made, and it is a stack: -//! a method's exit and a refused load take off everything made since they -//! began (§5.5.2.3), which is all theirs, a callee's being gone before its -//! caller makes more. A node's parent and an alias's source are therefore -//! older than it and outlive it. A node is named by its index and by a stamp -//! no other node ever has, so a reference to an object that is gone resolves -//! to nothing rather than to whatever was made at its index since. -//! Nothing here recurses over the tree's depth, which a table chooses, as it -//! does a path's length: every walk pays its caller's [`Toll`] for each scope -//! it climbs and each segment it looks up. +//! Nodes live in two arenas, each in the order its nodes were made and each +//! a stack. **The static arena holds what a definition block defines while +//! it loads** (§5.5.2: "Global objects are those NameSpace objects created at +//! table load time"), at boot or at run time by `Load`; a refused load takes +//! off everything it made. **The method arena holds what a method invocation +//! defines**, and a method's exit takes off everything made since it began +//! (§5.5.2.3), which is all its own, a callee's being gone before its caller +//! makes more. A block a method loads interleaves with the method's own +//! nodes in time, which is why one stack cannot hold both: the method's exit +//! would take the block with it. //! -//! The arena is held against the interpreter's [`Meter`] at its capacity, -//! and each node at [`ENTRY`] beside it, from its creation to its removal: -//! an exit or a refusal leaves the arena the slots it had at the [`Mark`], -//! and a parent whose last child it took no map. The arena therefore moves -//! as often as a method that grows it runs, and a move pays the [`Toll`] for -//! the bytes it copies. +//! A node's parent and an alias's source are older than it or static, and +//! outlive it: a static node is refused below a method's, and so is a static +//! alias of one. A node is named by its arena, its index and a stamp no other +//! node ever has, so a reference to an object that is gone resolves to +//! nothing rather than to whatever was made at its index since. Nothing here +//! recurses over the tree's depth, which a table chooses, as it does a +//! path's length: every walk pays its caller's [`Toll`] for each scope it +//! climbs and each segment it looks up. //! -//! The arena's order is the order its nodes were declared in, and a [`Walk`] -//! reads sibling order from it alone: a parent's map keeps its children by -//! name. +//! Each arena is held against the interpreter's [`Meter`] at its capacity, +//! and each node at [`ENTRY`] beside it, from its creation to its removal: an +//! exit or a refusal leaves its arena the slots it had at the [`Mark`], and +//! a parent whose last child it took no map. An arena therefore moves as +//! often as a method or load that grows it runs, and a move pays the +//! [`Toll`] for the bytes it copies. +//! +//! The static arena's order is the order its nodes were declared in, and a +//! [`Walk`] reads sibling order from it alone: a parent's map keeps its +//! children by name. A walk is read between evaluations, when the method +//! arena is empty. use alloc::collections::BTreeMap; use alloc::rc::Rc; @@ -35,43 +45,80 @@ use crate::{Error, WORK_PER_STEP}; /// part of, which refuses the walk at its bound. pub(crate) type Toll<'a> = &'a mut dyn FnMut() -> Result<(), Error>; -/// The bytes one slot of the arena is held at. +/// The bytes one slot of an arena is held at. const SLOT: usize = core::mem::size_of::(); /// The bytes a node's entry among its parent's children is held at: a whole /// leaf of the map, eleven keys and values beside its link to the tree, /// which a parent of one child allocates for it; a map of more holds fewer /// bytes an entry. -const ENTRY: usize = (core::mem::size_of::() + 4 + 11 * core::mem::size_of::<(Seg, u32)>()).next_multiple_of(8); +const ENTRY: usize = (core::mem::size_of::() + 4 + 11 * core::mem::size_of::<(Seg, Link)>()).next_multiple_of(8); -/// The slots the arena starts with. +/// The slots an arena starts with. const SLOTS: usize = 16; +/// The deepest a node lies: each level a slot and an entry its parent holds +/// it at, against the meter. +pub(crate) const DEEPEST: usize = crate::MAX_LIVE / (SLOT + ENTRY); + +/// Which arena a node is in, and so what ends it. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Arena { + /// A definition block's: it lives as long as the namespace. + Static = 0, + /// A method invocation's: it ends with the invocation. + Method = 1, +} + +/// A node by its place, whatever it holds now: its index, its arena in the +/// top bit, in the four bytes an entry of a parent's map holds it in. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +struct Link(u32); + +/// The bit of a [`Link`] that says the method arena. +const METHOD: u32 = 1 << 31; + +impl Link { + fn new(arena: Arena, index: u32) -> Link { + Link(index | if arena == Arena::Method { METHOD } else { 0 }) + } + + fn arena(self) -> Arena { + if self.0 & METHOD == 0 { Arena::Static } else { Arena::Method } + } + + fn index(self) -> usize { + (self.0 & !METHOD) as usize + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct NodeId { - index: u32, + link: Link, stamp: u64, } -/// How many nodes there are and how many slots hold them: what +/// How many nodes an arena has and how many slots hold them: what /// [`Namespace::unwind`] goes back to. pub(crate) struct Mark { + arena: Arena, nodes: usize, slots: usize, } struct Node { seg: Seg, - parent: Option, - children: BTreeMap, - /// An Alias (§19.6.4) acts exactly as its source, which already exists. - alias: Option, + parent: Option, + children: BTreeMap, + /// An Alias (§19.6.4) acts exactly as its source, which already exists + /// and outlives it. + alias: Option, object: Object, stamp: u64, } pub(crate) struct Namespace { - nodes: Vec, + arenas: [Vec; 2], /// The stamp of the next node made. next: u64, meter: Rc, @@ -79,23 +126,31 @@ pub(crate) struct Namespace { impl Namespace { pub(crate) fn new(meter: Rc) -> Self { - let mut ns = Namespace { nodes: Vec::new(), next: 0, meter }; + let mut ns = Namespace { arenas: [Vec::new(), Vec::new()], next: 0, meter }; let root = Node { seg: Seg(*b"\\___"), parent: None, children: BTreeMap::new(), alias: None, object: Object::Scope, stamp: 0 }; let held = "an empty meter holds the arena's first slots"; - ns.push(root, &mut || Ok(())).expect(held); + ns.push(Arena::Static, root, &mut || Ok(())).expect(held); ns } pub(crate) fn root(&self) -> NodeId { - NodeId { index: 0, stamp: 0 } + NodeId { link: Link::new(Arena::Static, 0), stamp: 0 } + } + + fn at(&self, link: Link) -> &Node { + &self.arenas[link.arena() as usize][link.index()] + } + + fn at_mut(&mut self, link: Link) -> &mut Node { + &mut self.arenas[link.arena() as usize][link.index()] } - fn id(&self, index: u32) -> NodeId { - NodeId { index, stamp: self.nodes[index as usize].stamp } + fn id(&self, link: Link) -> NodeId { + NodeId { link, stamp: self.at(link).stamp } } fn node(&self, id: NodeId) -> Option<&Node> { - self.nodes.get(id.index as usize).filter(|n| n.stamp == id.stamp) + self.arenas[id.link.arena() as usize].get(id.link.index()).filter(|n| n.stamp == id.stamp) } pub(crate) fn object(&self, id: NodeId) -> Option<&Object> { @@ -103,7 +158,7 @@ impl Namespace { } pub(crate) fn set(&mut self, id: NodeId, object: Object) -> Result<(), Error> { - let n = self.nodes.get_mut(id.index as usize).filter(|n| n.stamp == id.stamp); + let n = self.arenas[id.link.arena() as usize].get_mut(id.link.index()).filter(|n| n.stamp == id.stamp); n.map(|n| n.object = object).ok_or(Error::NotFound(String::from("an object its method's exit destroyed"))) } @@ -114,7 +169,7 @@ impl Namespace { /// The object named `seg` directly below `id`, an alias followed. pub(crate) fn child(&self, id: NodeId, seg: Seg) -> Option { let c = *self.node(id)?.children.get(&seg)?; - Some(self.id(self.nodes[c as usize].alias.unwrap_or(c))) + Some(self.id(self.at(c).alias.unwrap_or(c))) } /// The scope a path's segments are walked from: the root, or `scope` @@ -156,10 +211,10 @@ impl Namespace { Ok(Some(at)) } - /// Creates the object a path names: every segment but the last must - /// exist, and the last must not (§5.3: "a name collision ... is - /// considered fatal"). - pub(crate) fn create(&mut self, scope: NodeId, path: &Path, object: Object, toll: Toll<'_>) -> Result { + /// Creates the object a path names, in `arena`: every segment but the + /// last must exist, and the last must not (§5.3: "a name collision ... is + /// considered fatal"); a static object below a method's is refused. + pub(crate) fn create(&mut self, arena: Arena, scope: NodeId, path: &Path, object: Object, toll: Toll<'_>) -> Result { let (last, parents) = path.segs.split_last().ok_or(Error::Rule("a definition names no object"))?; let missing = || Error::NotFound(crate::name::text(path)); let mut at = self.start(scope, path, toll)?.ok_or_else(missing)?; @@ -170,53 +225,75 @@ impl Namespace { if self.node(at).ok_or_else(missing)?.children.contains_key(last) { return Err(Error::Exists(self.path_of(at, Some(*last), toll)?)); } - let node = Node { seg: *last, parent: Some(at.index), children: BTreeMap::new(), alias: None, object, stamp: 0 }; - self.push(node, toll) + if arena == Arena::Static && at.link.arena() == Arena::Method { + return Err(Error::Rule("a definition block defines an object below one a method made, which it would outlive")); + } + let node = Node { seg: *last, parent: Some(at.link), children: BTreeMap::new(), alias: None, object, stamp: 0 }; + self.push(arena, node, toll) } - /// A node at the arena's end and among its parent's children, stamped, + /// A node at its arena's end and among its parent's children, stamped, /// and held against the meter before either grows. An arena that is /// full doubles, which moves it: a step for every [`WORK_PER_STEP`] /// bytes moved, as for any copy. - fn push(&mut self, node: Node, toll: Toll<'_>) -> Result { - let index = u32::try_from(self.nodes.len()).map_err(|_| Error::Bound("the namespace's node count"))?; - if self.nodes.len() == self.nodes.capacity() { - let held = self.nodes.capacity(); + fn push(&mut self, arena: Arena, node: Node, toll: Toll<'_>) -> Result { + let nodes = &self.arenas[arena as usize]; + let index = u32::try_from(nodes.len()).ok().filter(|&i| i < METHOD).ok_or(Error::Bound("the namespace's node count"))?; + if nodes.len() == nodes.capacity() { + let held = nodes.capacity(); for _ in 0..held * SLOT / WORK_PER_STEP { toll()?; } let more = held.max(SLOTS); self.meter.take(more * SLOT)?; - self.nodes.reserve_exact(more); + self.arenas[arena as usize].reserve_exact(more); } + let link = Link::new(arena, index); if let Some(p) = node.parent { self.meter.take(ENTRY)?; - self.nodes[p as usize].children.insert(node.seg, index); + self.at_mut(p).children.insert(node.seg, link); } let stamp = self.next; self.next += 1; - self.nodes.push(Node { stamp, ..node }); - Ok(NodeId { index, stamp }) + self.arenas[arena as usize].push(Node { stamp, ..node }); + Ok(NodeId { link, stamp }) } - pub(crate) fn alias(&mut self, scope: NodeId, path: &Path, target: NodeId, toll: Toll<'_>) -> Result { - let id = self.create(scope, path, Object::Uninit, toll)?; - self.nodes[id.index as usize].alias = Some(target.index); + pub(crate) fn alias(&mut self, arena: Arena, scope: NodeId, path: &Path, target: NodeId, toll: Toll<'_>) -> Result { + if arena == Arena::Static && target.link.arena() == Arena::Method { + return Err(Error::Rule("a definition block makes an Alias of an object a method made, which it would outlive")); + } + let id = self.create(arena, scope, path, Object::Uninit, toll)?; + self.at_mut(id.link).alias = Some(target.link); Ok(id) } - pub(crate) fn mark(&self) -> Mark { - Mark { nodes: self.nodes.len(), slots: self.nodes.capacity() } + /// How many nodes the static arena holds: every one a load made, and the + /// predefined ones. + pub(crate) fn defined(&self) -> u32 { + self.arenas[Arena::Static as usize].len() as u32 + } + + /// The static arena's slots, which the meter holds it at. + pub(crate) fn slots(&self) -> usize { + self.arenas[Arena::Static as usize].capacity() + } + + pub(crate) fn mark(&self, arena: Arena) -> Mark { + let nodes = &self.arenas[arena as usize]; + Mark { arena, nodes: nodes.len(), slots: nodes.capacity() } } - /// Destroys every object made since `mark` was read (§5.5.2.3), newest - /// first, and gives back the slots the arena has grown by since. + /// Destroys every object made in the mark's arena since it was read + /// (§5.5.2.3), newest first, and gives back the slots that arena has + /// grown by since. pub(crate) fn unwind(&mut self, mark: Mark) { - while self.nodes.len() > mark.nodes - && let Some(n) = self.nodes.pop() + let arena = mark.arena as usize; + while self.arenas[arena].len() > mark.nodes + && let Some(n) = self.arenas[arena].pop() { if let Some(p) = n.parent { - let children = &mut self.nodes[p as usize].children; + let children = &mut self.at_mut(p).children; children.remove(&n.seg); // A map keeps a leaf for the last entry it loses, which // nothing is held for. @@ -226,9 +303,9 @@ impl Namespace { self.meter.give(ENTRY); } } - let held = self.nodes.capacity(); - self.nodes.shrink_to(mark.slots); - self.meter.give((held - self.nodes.capacity()) * SLOT); + let held = self.arenas[arena].capacity(); + self.arenas[arena].shrink_to(mark.slots); + self.meter.give((held - self.arenas[arena].capacity()) * SLOT); } /// The absolute path of a node, and of `child` below it when given. @@ -239,21 +316,23 @@ impl Namespace { let Some(p) = n.parent else { break }; toll()?; segs.push(n.seg); - at = self.nodes.get(p as usize); + at = Some(self.at(p)); } segs.reverse(); Ok(crate::name::text(&Path { root: true, up: 0, segs })) } } -/// Every object below the root, each after its parent and siblings in the -/// order they were declared, an Alias left out: its source is met at its own -/// place, and an Alias of an ancestor would otherwise never end. +/// Every object of the static arena below the root, each after its parent +/// and siblings in the order they were declared, an Alias left out: its +/// source is met at its own place, and an Alias of an ancestor would +/// otherwise never end. /// /// It is held against the meter while it lasts, at eight bytes a node and /// five a level of the deepest: both are as many as the tables chose. pub(crate) struct Walk<'a> { - ns: &'a Namespace, + nodes: &'a [Node], + meter: &'a Meter, /// A node's first child and its next sibling, by index; 0, the root's, /// is none. first: Vec, @@ -271,20 +350,22 @@ const fn path_len(depth: usize) -> usize { impl Namespace { pub(crate) fn walk(&self) -> Result, Error> { - let count = self.nodes.len(); + let nodes = &self.arenas[Arena::Static as usize][..]; + let count = nodes.len(); let links = 2 * count * core::mem::size_of::(); self.meter.take(links)?; - let mut walk = Walk { ns: self, first: Vec::new(), next: Vec::new(), at: 0, depth: 1, path: String::new(), held: links }; + let mut walk = Walk { nodes, meter: &self.meter, first: Vec::new(), next: Vec::new(), at: 0, depth: 1, path: String::new(), held: links }; for links in [&mut walk.first, &mut walk.next] { links.reserve_exact(count); links.resize(count, 0); } - // A parent is older than its child, so its depth is known first: - // `next` holds each node's depth until the links are made. + // A parent is older than its child and static as it is, so its depth + // is known first: `next` holds each node's depth until the links are + // made. let mut deepest = 0; - for (i, n) in self.nodes.iter().enumerate() { + for (i, n) in nodes.iter().enumerate() { if let Some(p) = n.parent { - walk.next[i] = walk.next[p as usize] + 1; + walk.next[i] = walk.next[p.index()] + 1; deepest = deepest.max(walk.next[i]); } } @@ -294,11 +375,11 @@ impl Namespace { walk.path.reserve_exact(path); // Newest first, each node goes to the front of its parent's list, // which leaves the oldest there. - for (i, n) in self.nodes.iter().enumerate().rev() { + for (i, n) in nodes.iter().enumerate().rev() { walk.next[i] = 0; if let (Some(p), None) = (n.parent, n.alias) { - walk.next[i] = walk.first[p as usize]; - walk.first[p as usize] = i as u32; + walk.next[i] = walk.first[p.index()]; + walk.first[p.index()] = i as u32; } } walk.at = walk.first[0]; @@ -308,19 +389,19 @@ impl Namespace { impl Drop for Walk<'_> { fn drop(&mut self) { - self.ns.meter.give(self.held); + self.meter.give(self.held); } } impl Walk<'_> { - /// The next object's depth, a child of the root's being 1, its name and - /// the object. - pub(crate) fn step(&mut self) -> Option<(u32, Seg, &Object)> { + /// The next object's depth, a child of the root's being 1, its place in + /// the static arena, its name and the object. + pub(crate) fn step(&mut self) -> Option<(u32, u32, Seg, &Object)> { let i = self.at as usize; if i == 0 { return None; } - let (n, depth) = (&self.ns.nodes[i], self.depth); + let (n, depth) = (&self.nodes[i], self.depth); self.path.truncate(path_len(depth as usize - 1)); self.path.push(if depth == 1 { '\\' } else { '.' }); self.path.extend(n.seg.0.iter().map(|&c| char::from(c))); @@ -332,12 +413,12 @@ impl Walk<'_> { // has one: each node is climbed from once in a whole walk. let mut up = i; while up != 0 && self.next[up] == 0 { - up = self.ns.nodes[up].parent.map_or(0, |p| p as usize); + up = self.nodes[up].parent.map_or(0, |p| p.index()); self.depth -= 1; } self.at = self.next[up]; } - Some((depth, n.seg, &n.object)) + Some((depth, i as u32, n.seg, &n.object)) } /// The absolute path of the object last stepped to. diff --git a/system/acpiserver/aml/tests/common/mod.rs b/system/acpiserver/aml/tests/common/mod.rs index c9e8e54da0d..47257769395 100644 --- a/system/acpiserver/aml/tests/common/mod.rs +++ b/system/acpiserver/aml/tests/common/mod.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; use toyos_acpi::{Phys, Table}; -use toyos_aml::{Access, Address, Denied, Error, Host, Interpreter, Value}; +use toyos_aml::{Access, Address, Denied, Error, Host, Interpreter, RuntimeLoad, Transfer, Value}; /// PkgLength (§20.2.4) of `n` bytes that follow it: the length counts its /// own encoding. @@ -285,6 +285,18 @@ pub fn table(signature: &[u8; 4], revision: u8, body: &[u8]) -> Vec { t } +/// An SSDT of `body` whose OEM Table ID is `id`, padded with spaces. +pub fn ssdt_named(id: &str, body: &[u8]) -> Vec { + let mut t = table(b"SSDT", 2, body); + let mut field = [b' '; 8]; + field[..id.len()].copy_from_slice(id.as_bytes()); + t[16..24].copy_from_slice(&field); + t[9] = 0; + let sum = t.iter().fold(0u8, |s, &b| s.wrapping_add(b)); + t[9] = 0u8.wrapping_sub(sum); + t +} + pub fn dsdt(body: &[u8]) -> Vec { table(b"DSDT", 2, body) } @@ -298,6 +310,17 @@ pub enum Event { Notify(String, u64), GlobalTake(Option), GlobalRelease, + /// A definition block's bytes asked for, at and long. + Table(u64, usize), + /// A run-time load told: the method that made it, the block's OEM Table + /// ID where its header was read, and why it was refused. + Loaded(String, Option<[u8; 8]>, Option), + /// A carried field's transfer: its space, region, bit and bits, its + /// protocol and count, its connection's descriptor and start, and what + /// was written. + Transfer { space: u8, region: u64, bit: u64, bits: u64, attrib: (u8, u8), connection: Option<(Vec, u64)>, write: Option> }, + /// A table asked for by its signature, OEM ID and OEM Table ID. + FindTable(Vec, Vec, Vec), } /// A machine of bytes: every address space a sparse map, every request @@ -309,6 +332,14 @@ pub struct Machine { pub refuse: bool, /// The firmware holds the Global Lock past every bound a take names. pub firmware_holds: bool, + /// Memory Table 15.6 calls neither AddressRangeReserved nor + /// AddressRangeNVS, as `(start, end)`: a block there is refused. + pub reclaim: Vec<(u64, u64)>, + /// What a carried field's transfer answers; an empty one answers what + /// was written, or a zero byte for a read. + pub answer: Vec, + /// The tables the XSDT lists, as the addresses of their bytes in memory. + pub listed: Vec, } fn key(a: Address) -> (u8, u64) { @@ -324,6 +355,12 @@ fn key(a: Address) -> (u8, u64) { | u64::from(offset), ), Address::EmbeddedControl(x) => (3, u64::from(x)), + Address::SystemCmos(x) => (5, u64::from(x)), + Address::PciBarTarget { segment, bus, device, function, bar, offset } => { + (6, (u64::from(segment) << 56) | (u64::from(bus) << 48) | (u64::from(device) << 43) | (u64::from(function) << 40) | (u64::from(bar) << 32) | offset) + } + Address::Pcc { subspace, offset } => (0x0A, (u64::from(subspace) << 32) | u64::from(offset)), + Address::Oem { space, offset } => (space, offset), } } @@ -406,6 +443,63 @@ impl Host for Machine { self.log.push(Event::GlobalRelease); Ok(()) } + + fn table(&mut self, at: u64, len: usize) -> Result, Denied> { + self.log.push(Event::Table(at, len)); + if self.reclaim.iter().any(|&(start, end)| at < end && start < at + len as u64) { + return Err(Denied("a definition block in memory Table 15.6 calls neither reserved nor NVS".into())); + } + Ok(self.peek(Address::Memory(at), len)) + } + + fn loaded(&mut self, load: &RuntimeLoad<'_>) { + let id = load.header.map(|h| h[16..24].try_into().expect("a header's OEM Table ID")); + self.log.push(Event::Loaded(load.by.into(), id, load.refused.cloned())); + } + + fn transfer(&mut self, at: &Transfer<'_>, write: Option<&[u8]>) -> Result, Denied> { + self.log.push(Event::Transfer { + space: at.space, + region: at.region, + bit: at.bit, + bits: at.bits, + attrib: (at.attrib, at.length), + connection: at.connection.map(|(b, from)| (b.to_vec(), from)), + write: write.map(<[u8]>::to_vec), + }); + if self.refuse { + return Err(Denied("refused".into())); + } + Ok(match (self.answer.is_empty(), write) { + (false, _) => self.answer.clone(), + (true, Some(w)) => w.to_vec(), + (true, None) => vec![0], + }) + } + + /// Each listed table's header compared field by field, a shorter + /// identifier padded with NULs or spaces; one in `reclaim` is denied. + fn find_table(&mut self, signature: &[u8], oem_id: &[u8], oem_table_id: &[u8]) -> Result, Denied> { + self.log.push(Event::FindTable(signature.to_vec(), oem_id.to_vec(), oem_table_id.to_vec())); + let same = |field: &[u8], id: &[u8]| field.starts_with(id) && field[id.len()..].iter().all(|&b| b == 0 || b == b' '); + for &at in &self.listed { + let head = self.peek(Address::Memory(at), 36); + if same(&head[..4], signature) && same(&head[10..16], oem_id) && same(&head[16..24], oem_table_id) { + if self.reclaim.iter().any(|&(start, end)| (start..end).contains(&at)) { + return Err(Denied("a table in memory Table 15.6 calls neither reserved nor NVS".into())); + } + return Ok(Some((at, u32::from_le_bytes(head[4..8].try_into().expect("four bytes")) as usize))); + } + } + Ok(None) + } +} + +impl Machine { + /// Every run-time load told, in order. + pub fn loads(&self) -> Vec { + self.log.iter().filter(|e| matches!(e, Event::Loaded(..))).cloned().collect() + } } /// A table's bytes as physical memory at address 0, for @@ -487,4 +581,14 @@ impl Host for Sink { fn global_release(&mut self) -> Result<(), Denied> { Ok(()) } + fn table(&mut self, _: u64, len: usize) -> Result, Denied> { + Ok(vec![0; len]) + } + fn loaded(&mut self, _: &RuntimeLoad<'_>) {} + fn transfer(&mut self, _: &Transfer<'_>, _: Option<&[u8]>) -> Result, Denied> { + Ok(vec![0]) + } + fn find_table(&mut self, _: &[u8], _: &[u8], _: &[u8]) -> Result, Denied> { + Ok(None) + } } diff --git a/system/acpiserver/aml/tests/heap.rs b/system/acpiserver/aml/tests/heap.rs index f4ad8696f8d..3bc08013540 100644 --- a/system/acpiserver/aml/tests/heap.rs +++ b/system/acpiserver/aml/tests/heap.rs @@ -60,6 +60,7 @@ fn heap() -> isize { } const BOUND: isize = MAX_LIVE as isize; +const RUNNING: isize = toyos_aml::MAX_RUNNING as isize; /// The refusal that is this bound's, and not the step bound's. const FULL: &str = "more held live than one interpreter holds"; @@ -91,9 +92,11 @@ impl Filled { Filled { held: HELD.get() - before, peak: PEAK.get() - before, refused } } - /// The harm first: the heap held, then that the bound is what ended the fill. + /// The harm first: the heap held, and the most held while it ran, then + /// that the bound is what ended the fill. fn within_the_bound(&self) { assert!(self.held <= BOUND, "{} bytes held, {} at most on the way", self.held, self.peak); + assert!(self.peak <= RUNNING, "{} bytes held on the way, past the {RUNNING} a running interpreter holds", self.peak); assert_eq!(self.refused, Some(Error::Bound(FULL)), "nothing filled the interpreter"); } } @@ -210,16 +213,16 @@ fn devices_of_one_child_each_fill_it_to_the_bound() { } /// A device nothing is named below holds no map, and none again once a -/// method has named an object below it and exited: 40,000 such devices, two +/// method has named an object below it and exited: 30,000 such devices, two /// evaluations that each name an object below half of them, then empty /// buffers to the bound. #[test] fn a_parent_whose_last_child_is_unwound_holds_no_map() { - let devices: Vec> = (0..40_000).map(|d| device(&seg(d), &[])).collect(); + let devices: Vec> = (0..30_000).map(|d| device(&seg(d), &[])).collect(); let devices = table(b"SSDT", 2, &devices.concat()); - let (fill, fills) = fillers(8, &buffer(&int(0), &[])); + let (fill, fills) = fillers(16, &buffer(&int(0), &[])); let below = |half: usize| { - let names: Vec> = (0..20_000).map(|d| def_name(&format!("\\{}.T", seg(half * 20_000 + d)), &int(0))).collect(); + let names: Vec> = (0..15_000).map(|d| def_name(&format!("\\{}.T", seg(half * 15_000 + d)), &int(0))).collect(); method(&format!("M{half}"), 0, &names.concat()) }; let methods = table(b"SSDT", 2, &cat(&[&below(0), &below(1)])); @@ -235,7 +238,7 @@ fn a_parent_whose_last_child_is_unwound_holds_no_map() { let maps = PEAK.get() - before - held; let refused = fills.iter().find_map(|m| i.evaluate(&mut Sink, m, &[]).err()); Filled::of(before, refused).within_the_bound(); - // The names were made: 20,000 parents each held a map while one ran. + // The names were made: 15,000 parents each held a map while one ran. assert!(maps >= 1 << 20, "{maps} bytes held by an evaluation while it ran"); assert_eq!(kept, held, "bytes held after the two evaluations, and before them"); } @@ -476,3 +479,53 @@ fn a_full_interpreter_reads_a_field_that_fits_an_integer() { assert_eq!(i.evaluate(&mut Sink, "\\NARW", &[]), Ok(Value::Integer(0))); assert_eq!(i.evaluate(&mut Sink, "\\WIDE", &[]), Err(Error::Bound(FULL))); } + +/// One evaluation holds at most 256 Mutex acquisitions at once: the 257th of +/// one Mutex acquired again and again is refused at the bound, and the +/// evaluation holds nothing at its end. +#[test] +fn an_evaluation_holds_at_most_256_mutexes() { + let acquires = |n: usize| { + let acquire = cat(&[&[0x5B, 0x23], &name("MUT0"), &0xFFFFu16.to_le_bytes()]); + cat(&[&[0x5B, 0x01], &name("MUT0"), &[0x00], &method("MAIN", 0, &cat(&[&acquire.repeat(n), &ret(&int(1))]))]) + }; + let (mut i, _) = start(&acquires(256)); + assert_eq!(i.evaluate(&mut Sink, "\\MAIN", &[]), Err(Error::Rule("a method exits holding a Mutex it acquired (§19.6.88)"))); + let (mut i, _) = start(&acquires(257)); + assert_eq!(i.evaluate(&mut Sink, "\\MAIN", &[]), Err(Error::Bound("more Mutexes held at once than one evaluation holds"))); +} + +/// A method refused at the bound ends as any other: what it made, a named object and its +/// locals' buffers, is given back to the byte, the Global Lock it held is +/// let go, so the next Acquire of it takes it from the firmware again, and +/// it is refused by the bound's name; the next method builds a buffer of a +/// mebibyte. What a method stores into an object that outlives it stays +/// (`what_is_held_live_is_bounded_in_sum`). +#[test] +fn a_method_refused_at_the_bound_gives_back_what_it_made() { + let acquire = cat(&[&[0x5B, 0x23], &name("\\_GL"), &0xFFFFu16.to_le_bytes()]); + let body = cat(&[ + &method("MAKE", 0, &ret(&buffer(&int(0x10_0000), &[]))), + &method("TAKE", 0, &cat(&[&acquire, &[0x5B, 0x27], &name("\\_GL"), &ret(&int(1))])), + &method( + "FILL", + 0, + &cat(&[ + &acquire, + &def_name("MINE", &buffer(&int(0x10_0000), &[])), + &store(&var_package(&int(64), &[]), &local(0)), + &store(&int(0), &local(1)), + &while_(&int(1), &cat(&[&store(&buffer(&int(0x10_0000), &[]), &index(&local(0), &local(1), &[0x00])), &increment(&local(1))])), + ]), + ), + ]); + let (mut i, _) = start(&body); + let (live, held) = (i.usage().live, HELD.get()); + assert_eq!(i.evaluate(&mut Sink, "\\FILL", &[]), Err(Error::Bound(FULL))); + assert_eq!((i.usage().live, HELD.get()), (live, held), "what the refused method made is held"); + assert_eq!(i.evaluate(&mut Sink, "\\FILL.MINE", &[]), Err(Error::NotFound("\\FILL.MINE".into()))); + let mut firmware = Machine::default(); + assert_eq!(i.evaluate(&mut firmware, "\\TAKE", &[]), Ok(Value::Integer(1))); + assert_eq!(firmware.log, [Event::GlobalTake(None), Event::GlobalRelease], "the refused method's hold of the Global Lock is kept"); + assert_eq!(i.evaluate(&mut Sink, "\\MAKE", &[]), Ok(Value::Buffer(vec![0; 0x10_0000]))); +} diff --git a/system/acpiserver/aml/tests/hostile.rs b/system/acpiserver/aml/tests/hostile.rs index aaac51f7d98..7fd93a2b693 100644 --- a/system/acpiserver/aml/tests/hostile.rs +++ b/system/acpiserver/aml/tests/hostile.rs @@ -291,6 +291,19 @@ fn iterations(setup: &[u8], scopes: usize, op: &[u8]) -> u64 { } } +/// How often `While (One) { MAKE Increment (\CNT) }` ran in a method that +/// first defines `names`, MAKE defining one name more, before the step bound +/// refused it. +fn crowded(names: &[u8]) -> u64 { + let main = method("MAIN", 0, &cat(&[names, &while_(&int(1), &cat(&[&name("MAKE"), &increment(&name("\\CNT"))]))])); + let (mut i, mut m) = loaded(&cat(&[&def_name("CNT", &int(0)), &method("MAKE", 0, &def_name("TMP", &int(0))), &main])); + assert!(matches!(i.evaluate(&mut m, "\\MAIN", &[]), Err(Error::Bound(_)))); + match i.evaluate(&mut m, "\\CNT", &[]) { + Ok(Value::Integer(n)) => n, + other => panic!("{other:?}"), + } +} + /// The work one step may hide is a table's to size wherever a name is read /// or walked, wherever an operator reads more of an object than it makes and /// wherever a definition moves the namespace's arena to make room: each is @@ -301,13 +314,12 @@ fn a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it() { const STEPS: u64 = 1 << 20; const SCOPES: usize = 200; const BYTES: usize = 1 << 14; - // The arena is full at 1,024 nodes when MAIN runs: the root and the nine - // predefined names, CNT, these 1,011, MAKE and MAIN. MAKE's name is one - // more, and its exit gives the slots back; a node is more than a step's - // worth of bytes to move. + // The method arena is full at 1,024 nodes when the loop runs: MAIN's own + // names. MAKE's name is one more, and its exit gives the slots back; a + // node is more than a step's worth of bytes to move. const NODES: usize = 1024; - let crowd: Vec> = (0..NODES - 13).map(|n| def_name(&format!("X{n:03X}"), &int(0))).collect(); - let crowd = cat(&[&crowd.concat(), &method("MAKE", 0, &def_name("TMP", &int(0)))]); + let crowd: Vec> = (0..NODES).map(|n| def_name(&format!("X{n:03X}"), &int(0))).collect(); + let crowd = crowd.concat(); let cond_ref_of = |n: &[u8]| cat(&[&[0x5B, 0x12], n, &local(0)]); let deep: Vec = (0..SCOPES).map(|d| format!("N{d:03}")).collect(); let deep = format!("\\{}", deep.join(".")); @@ -331,7 +343,7 @@ fn a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it() { per_scope, iterations(&cat(&[&chain, &method("MAKE", 0, &def_name(&format!("{deep}.TMP"), &int(0)))]), 0, &name("MAKE")), ), - ("a definition that moves the arena, which its method's exit moves back", STEPS / NODES as u64, iterations(&crowd, 0, &name("MAKE"))), + ("a definition that moves the method arena, which its method's exit moves back", STEPS / NODES as u64, crowded(&crowd)), ("Notify, which names its device by its path", per_scope, iterations(&[], SCOPES, &cat(&[&[0x86], &name("^"), &int(0x80)]))), ("a long buffer stored to a short one", per_byte, iterations(&cat(&[&big, &small]), 0, &store(&name("BIG"), &name("SMAL")))), ( diff --git a/system/acpiserver/aml/tests/load.rs b/system/acpiserver/aml/tests/load.rs new file mode 100644 index 00000000000..367033ad6dc --- /dev/null +++ b/system/acpiserver/aml/tests/load.rs @@ -0,0 +1,320 @@ +//! Load (§19.6.76): a definition block loaded at run time from a SystemMemory +//! region, a field unit of one or a Buffer, its objects global and the +//! loading method's own gone at its exit (§5.5.2, §5.5.2.3); a refused block +//! false and nothing of it kept, and a bound the evaluation's refusal. + +mod common; + +use common::*; +use toyos_aml::{Address, Error, Interpreter, Kind, Value}; + +/// Where the region the tests load from begins, and how long it is. +const AT: u64 = 0x1_0000; +const LEN: u64 = 0x200; + +/// `OperationRegion (BLK0, SystemMemory, AT, LEN)`. +fn region() -> Vec { + op_region("BLK0", 0x00, &int(AT), &int(LEN)) +} + +/// `Load (, )` (§20.2.5.4). +fn load(source: &str, target: &[u8]) -> Vec { + cat(&[&[0x5B, 0x20], &name(source), target]) +} + +/// The block every test loads: `\LDED` 0x42, and `\LMTH`, which returns +/// 0x43. +fn block() -> Vec { + ssdt_named("LOADED", &cat(&[&def_name("\\LDED", &int(0x42)), &method("\\LMTH", 0, &ret(&int(0x43)))])) +} + +/// The namespace of `dsdt` loaded, with `block` in memory at [`AT`]. +fn machine(dsdt: &[u8], block: &[u8]) -> (Interpreter, Machine) { + let mut m = Machine::default(); + m.poke(Address::Memory(AT), block); + let mut i = Interpreter::new(); + i.load_bytes(&mut m, &common::dsdt(dsdt)).expect("the DSDT loads"); + (i, m) +} + +fn kept(i: &mut Interpreter, m: &mut Machine) -> (Result, Result) { + (i.evaluate(m, "\\LDED", &[]), i.evaluate(m, "\\LMTH", &[])) +} + +fn loaded() -> (Result, Result) { + (Ok(Value::Integer(0x42)), Ok(Value::Integer(0x43))) +} + +/// A method that defines objects of its own, loads, and defines more: the +/// load is true, stored to its target and returned; the block's objects +/// outlive the method, its methods run from its bytes after, and the +/// method's own objects are gone at its exit. +#[test] +fn a_block_a_method_loads_outlives_it_and_the_methods_own_objects_do_not() { + let main = method( + "MAIN", + 0, + &cat(&[&def_name("MINE", &int(5)), &store(&load("BLK0", &local(0)), &name("\\RES")), &def_name("MORE", &int(6)), &ret(&local(0))]), + ); + let (mut i, mut m) = machine(&cat(&[®ion(), &def_name("RES", &int(0)), &main]), &block()); + assert_eq!(i.evaluate(&mut m, "\\MAIN", &[]), Ok(Value::Integer(u64::MAX))); + assert_eq!(i.evaluate(&mut m, "\\RES", &[]), Ok(Value::Integer(u64::MAX))); + assert_eq!(kept(&mut i, &mut m), loaded()); + for gone in ["\\MAIN.MINE", "\\MAIN.MORE"] { + assert_eq!(i.evaluate(&mut m, gone, &[]), Err(Error::NotFound(gone.into()))); + } + let walked: Vec = { + let mut walk = i.walk().expect("the namespace walks"); + let mut paths = Vec::new(); + while walk.next().is_some() { + paths.push(walk.path().to_string()); + } + paths + }; + assert!(walked.contains(&"\\LDED".to_string()) && walked.contains(&"\\LMTH".to_string()), "{walked:?}"); + assert_eq!(m.loads(), [Event::Loaded("\\MAIN".into(), Some(*b"LOADED "), None)]); +} + +/// §19.6.76: a block loads at definition-block level as it does from a +/// method, from the region, from a field unit of it at whole bytes, and from +/// a Buffer, each at the root. +#[test] +fn a_block_loads_from_a_region_a_field_unit_and_a_buffer_at_definition_block_level() { + let bytes = block(); + let sources = [ + cat(&[®ion(), &def_name("RES", &int(0)), &load("BLK0", &name("RES"))]), + cat(&[ + ®ion(), + &def_name("RES", &int(0)), + &field("BLK0", 0x01, &[unit("TABL", 8 * bytes.len())]), + &load("TABL", &name("RES")), + ]), + cat(&[&def_name("BUFF", &buffer(&int(bytes.len() as u64), &bytes)), &def_name("RES", &int(0)), &load("BUFF", &name("RES"))]), + ]; + for dsdt in sources { + let (mut i, mut m) = machine(&dsdt, &bytes); + assert_eq!(i.evaluate(&mut m, "\\RES", &[]), Ok(Value::Integer(u64::MAX))); + assert_eq!(kept(&mut i, &mut m), loaded()); + assert_eq!(m.loads(), [Event::Loaded("\\".into(), Some(*b"LOADED "), None)]); + } + // A field unit's block, read from where the field begins in its region. + let shifted = cat(&[&[0u8; 16], &bytes]); + let dsdt = cat(&[®ion(), &def_name("RES", &int(0)), &field("BLK0", 0x01, &[skip(8 * 16), unit("TABL", 8 * bytes.len())]), &load("TABL", &name("RES"))]); + let (mut i, mut m) = machine(&dsdt, &shifted); + assert_eq!(i.evaluate(&mut m, "\\RES", &[]), Ok(Value::Integer(u64::MAX))); + assert!(m.log.contains(&Event::Table(AT + 16, bytes.len())), "{:?}", m.log); +} + +/// The value a `\MAIN` that loads `block` from the region into `Local0` +/// returns, and what the namespace kept of the block. +fn refused(block: &[u8], prepare: impl FnOnce(&mut Machine)) -> (Result, Machine, Interpreter) { + let main = method("MAIN", 0, &cat(&[&load("BLK0", &local(0)), &ret(&local(0))])); + let (mut i, mut m) = machine(&cat(&[®ion(), &main]), block); + prepare(&mut m); + let r = i.evaluate(&mut m, "\\MAIN", &[]); + (r, m, i) +} + +/// §19.6.76: "A value of zero (false) means the operation failed": a block +/// whose bytes do not sum to zero, whose length runs past its region or +/// does not hold its header, whose AML is malformed or collides with a +/// name, or whose bytes the host refuses, loads false; nothing of it is +/// kept, and the host is told why. +#[test] +fn a_refused_block_is_false_and_keeps_nothing() { + let good = block(); + let mut unsummed = good.clone(); + unsummed[40] ^= 1; + let mut long = good.clone(); + long[4..8].copy_from_slice(&((LEN + 1) as u32).to_le_bytes()); + let mut short = good.clone(); + short[4..8].copy_from_slice(&20u32.to_le_bytes()); + // Ends inside a Name; and defines `\LDED` twice. + let malformed = ssdt_named("LOADED", &cat(&[&def_name("\\LDED", &int(0x42)), &[0x08, b'A']])); + let collides = ssdt_named("LOADED", &cat(&[&def_name("\\LDED", &int(0x42)), &def_name("\\LDED", &int(0x43))])); + let table = |why: &'static str| Some(Error::Table(why)); + for (bytes, why) in [ + (unsummed, table("a definition block whose bytes do not sum to zero (§5.2.6)")), + (long, table("a definition block longer than the region it is loaded from (§19.6.76)")), + (short, table("a definition block whose length does not hold its header, or is not what was read (§5.2.6)")), + (collides, Some(Error::Exists("\\LDED".into()))), + ] { + let (r, m, mut i) = refused(&bytes, |_| ()); + assert_eq!(r, Ok(Value::Integer(0))); + assert_eq!(m.loads(), [Event::Loaded("\\MAIN".into(), Some(*b"LOADED "), why)]); + let mut m = m; + assert_eq!(i.evaluate(&mut m, "\\LDED", &[]), Err(Error::NotFound("\\LDED".into()))); + } + let (r, m, _) = refused(&malformed, |_| ()); + assert_eq!(r, Ok(Value::Integer(0))); + assert!(matches!(&m.loads()[..], [Event::Loaded(_, _, Some(Error::Malformed { .. }))]), "{:?}", m.loads()); + // The host's refusal of a page Table 15.6 calls neither reserved nor NVS. + let (r, m, _) = refused(&good, |m| m.reclaim = vec![(AT, AT + 0x1000)]); + assert_eq!(r, Ok(Value::Integer(0))); + let denied = Error::Host("a definition block in memory Table 15.6 calls neither reserved nor NVS".into()); + assert_eq!(m.loads(), [Event::Loaded("\\MAIN".into(), None, Some(denied))]); +} + +/// A block that defines an object below one the loading method made, or an +/// Alias of one, would outlive it: it is refused, false, and keeps nothing; +/// below an object a table made it loads. +#[test] +fn a_block_that_would_outlive_its_parent_or_source_is_refused() { + let main = method( + "MAIN", + 0, + &cat(&[&device("DEVX", &[]), &def_name("MINE", &int(5)), &load("BLK0", &local(0)), &ret(&local(0))]), + ); + for (body, kept) in [ + (scope("\\MAIN.DEVX", &def_name("LDED", &int(1))), false), + (cat(&[&[0x06], &name("\\MAIN.MINE"), &name("\\LDED")]), false), + (scope("\\DEVS", &def_name("LDED", &int(1))), true), + ] { + let (mut i, mut m) = machine(&cat(&[®ion(), &device("DEVS", &[]), &main]), &ssdt_named("LOADED", &body)); + assert_eq!(i.evaluate(&mut m, "\\MAIN", &[]), Ok(Value::Integer(if kept { u64::MAX } else { 0 }))); + if !kept { + assert!(matches!(&m.loads()[..], [Event::Loaded(_, _, Some(Error::Rule(_)))]), "{:?}", m.loads()); + } + } +} + +/// A bound the block's load reaches is the evaluation's: steps a load takes +/// are the evaluation's own, and buy it no more. +#[test] +fn a_bound_a_block_reaches_refuses_the_evaluation() { + let spins = ssdt_named("LOADED", &while_(&int(1), &[])); + let (r, m, _) = refused(&spins, |_| ()); + assert_eq!(r, Err(Error::Bound("more steps than one evaluation may take"))); + assert!(matches!(&m.loads()[..], [Event::Loaded(_, _, Some(Error::Bound(_)))]), "{:?}", m.loads()); +} + +/// A loaded block's integers are as wide as the DSDT's revision makes every +/// integer (§19.6.29), whatever its own header's revision. +#[test] +fn a_loaded_blocks_integers_are_the_dsdts_width() { + let wide = ssdt_named("LOADED", &def_name("\\WIDE", &int(u64::MAX))); + let mut m = Machine::default(); + m.poke(Address::Memory(AT), &wide); + let mut i = Interpreter::new(); + let narrow = table(b"DSDT", 1, &cat(&[®ion(), &def_name("RES", &int(0)), &load("BLK0", &name("RES"))])); + i.load_bytes(&mut m, &narrow).expect("the DSDT loads"); + assert_eq!(i.evaluate(&mut m, "\\RES", &[]), Ok(Value::Integer(0xFFFF_FFFF))); + assert_eq!(i.evaluate(&mut m, "\\WIDE", &[]), Ok(Value::Integer(0xFFFF_FFFF))); +} + +/// What the walk reads after a run-time load: the block's objects at their +/// place in definition order, past every object the namespace held before. +#[test] +fn a_loaded_blocks_objects_are_walked_past_what_was_defined_before() { + let main = method("MAIN", 0, &load("BLK0", &[0x00])); + let (mut i, mut m) = machine(&cat(&[®ion(), &main]), &block()); + let before = i.defined(); + i.evaluate(&mut m, "\\MAIN", &[]).expect("MAIN runs"); + let mut walk = i.walk().expect("the namespace walks"); + let mut serials = Vec::new(); + while let Some(entry) = walk.next() { + if ["\\LDED", "\\LMTH"].contains(&walk.path()) { + serials.push((entry.serial >= before, entry.kind)); + } + } + assert_eq!(serials, [(true, Kind::Integer), (true, Kind::Method)]); +} + +/// A table the XSDT lists, at [`AT`]: signature `OEM1`, OEM ID `TOYOS `, +/// OEM Table ID `TABLE1`, defining `\_SB.TBLD` and `MYD` below its root. +fn listed(m: &mut Machine) -> Vec { + let mut t = ssdt_named("TABLE1", &cat(&[&def_name("TBLD", &int(0x51)), &def_name("MYD", &int(0))])); + t[..4].copy_from_slice(b"OEM1"); + t[9] = 0; + t[9] = 0u8.wrapping_sub(t.iter().fold(0u8, |s, &b| s.wrapping_add(b))); + m.poke(Address::Memory(AT), &t); + m.listed = vec![AT]; + t +} + +/// `LoadTable (, , , , , )`. +fn load_table(ids: [&str; 3], root: &str, parameter: &str, data: &[u8]) -> Vec { + cat(&[&[0x5B, 0x1F], &string(ids[0]), &string(ids[1]), &string(ids[2]), &string(root), &string(parameter), data]) +} + +fn table_machine(body: &[u8]) -> (Interpreter, Machine) { + let mut m = Machine::default(); + let mut i = Interpreter::new(); + i.load_bytes(&mut m, &common::dsdt(body)).expect("the DSDT loads"); + listed(&mut m); + (i, m) +} + +/// §19.6.77: the table found by its three identifiers, a shorter one padded, +/// loads below the RootPathString; ParameterData is stored to the +/// ParameterPathString below that root once it has, and the value is true. +#[test] +fn load_table_loads_the_listed_table_below_its_root_and_stores_its_parameter() { + let main = method("MAIN", 0, &ret(&load_table(["OEM1", "TOYOS", "TABLE1"], "\\_SB", "MYD", &int(0x77)))); + let (mut i, mut m) = table_machine(&main); + assert_eq!(i.evaluate(&mut m, "\\MAIN", &[]), Ok(Value::Integer(u64::MAX))); + assert_eq!(i.evaluate(&mut m, "\\_SB.TBLD", &[]), Ok(Value::Integer(0x51))); + assert_eq!(i.evaluate(&mut m, "\\_SB.MYD", &[]), Ok(Value::Integer(0x77))); + assert!(m.log.contains(&Event::FindTable(b"OEM1".to_vec(), b"TOYOS".to_vec(), b"TABLE1".to_vec()))); + assert_eq!(m.loads(), [Event::Loaded("\\MAIN".into(), Some(*b"TABLE1 "), None)]); +} + +/// No table of those identifiers loads false and asks nothing more; one +/// whose memory the host denies, as Table 15.6 has it, loads false and +/// keeps nothing; an identifier longer than its field is a run-time error, +/// and so is a parameter path that names nothing once the table loaded. +#[test] +fn load_table_of_no_table_or_denied_memory_is_false_and_a_long_identifier_is_refused() { + let call = |ids: [&str; 3], parameter: &str| method("MAIN", 0, &ret(&load_table(ids, "", parameter, &int(1)))); + let (mut i, mut m) = table_machine(&call(["OEM1", "TOYOS", "TABLE2"], "")); + assert_eq!(i.evaluate(&mut m, "\\MAIN", &[]), Ok(Value::Integer(0))); + assert_eq!(m.loads(), []); + let (mut i, mut m) = table_machine(&call(["OEM1", "TOYOS", "TABLE1"], "")); + m.reclaim = vec![(AT, AT + 0x1000)]; + assert_eq!(i.evaluate(&mut m, "\\MAIN", &[]), Ok(Value::Integer(0))); + assert_eq!(i.evaluate(&mut m, "\\TBLD", &[]), Err(Error::NotFound("\\TBLD".into()))); + assert!(matches!(&m.loads()[..], [Event::Loaded(_, None, Some(Error::Host(_)))]), "{:?}", m.loads()); + for ids in [["OEM12", "TOYOS", "TABLE1"], ["OEM1", "TOYOSXX", "TABLE1"], ["OEM1", "TOYOS", "TABLE123X"]] { + let (mut i, mut m) = table_machine(&call(ids, "")); + assert_eq!(i.evaluate(&mut m, "\\MAIN", &[]), Err(Error::Rule("a table's signature, OEM ID or OEM Table ID longer than its field (§19.6.77)")), "{ids:?}"); + } + let (mut i, mut m) = table_machine(&call(["OEM1", "TOYOS", "TABLE1"], "NONE")); + assert_eq!(i.evaluate(&mut m, "\\MAIN", &[]), Err(Error::NotFound("\\NONE".into()))); +} + +/// §19.6.25: a Data Table Region is a SystemMemory region over the table +/// the XSDT lists, its header to its last byte; one naming no table, or a +/// table in memory the host denies, refuses the definition. +#[test] +fn a_data_table_region_is_the_listed_tables_memory() { + // DataTableRegion (DTR0, "OEM1", "TOYOS", "TABLE1"); Field (DTR0, + // ByteAcc) { SIG, 32, LEN, 32 }. + let region = |table: &str| { + cat(&[ + &[0x5B, 0x88], + &name("DTR0"), + &string("OEM1"), + &string("TOYOS"), + &string(table), + &field("DTR0", 0x01, &[unit("SIG", 32), unit("LEN", 32), skip(8 * 28), unit("LAST", 8)]), + ]) + }; + let (mut i, mut m) = { + let mut m = Machine::default(); + let t = listed(&mut m); + let mut i = Interpreter::new(); + i.load_bytes(&mut m, &common::dsdt(®ion("TABLE1"))).expect("the DSDT loads"); + assert_eq!(i.evaluate(&mut m, "\\LEN", &[]), Ok(Value::Integer(t.len() as u64))); + (i, m) + }; + assert_eq!(i.evaluate(&mut m, "\\SIG", &[]), Ok(Value::Integer(u64::from(u32::from_le_bytes(*b"OEM1"))))); + assert!(m.accesses().contains(&Event::Read(Address::Memory(AT), toyos_aml::Access::Byte))); + let mut m = Machine::default(); + listed(&mut m); + let refused = Interpreter::new().load_bytes(&mut m, &common::dsdt(®ion("TABLE2"))); + assert_eq!(refused, Err(Error::Rule("a DataTableRegion names a table the XSDT does not list (§19.6.25)"))); + let mut m = Machine::default(); + listed(&mut m); + m.reclaim = vec![(AT, AT + 0x1000)]; + assert!(matches!(Interpreter::new().load_bytes(&mut m, &common::dsdt(®ion("TABLE1"))), Err(Error::Host(_)))); +} diff --git a/system/acpiserver/aml/tests/regions.rs b/system/acpiserver/aml/tests/regions.rs index a56e59807df..5c5fae3139a 100644 --- a/system/acpiserver/aml/tests/regions.rs +++ b/system/acpiserver/aml/tests/regions.rs @@ -185,7 +185,79 @@ fn a_pci_config_region_addresses_its_devices_function() { let at = Address::PciConfig { segment: 1, bus: 0x80, device: 0x1F, function: 3, offset: 0x41 }; assert_eq!(m.accesses(), vec![Event::Read(at, Access::Byte)]); let (_, v) = read(&lpc(&[]), &[], "\\_SB.PCI0.ISAB.R41"); - assert!(matches!(v, Err(Error::Unsupported(_)))); + assert_eq!(v, Err(Error::Rule(NO_HOST_BRIDGE))); +} + +/// The refusal of a PCI_Config region no host bridge is above. +const NO_HOST_BRIDGE: &str = "a PCI_Config region below no host bridge, which names a _BBN or is PNP0A03 or PNP0A08 by its _HID or _CID"; + +/// A host bridge that names no `_BBN` is one by its `_HID` or a `_CID` +/// (§6.1.5, §6.1.2), PNP0A03 or PNP0A08 as an EISA ID or a string, a `_CID` +/// alone or in a package, each a name or a method: its bus is 0, the only +/// root bus a machine that names no `_BBN` has. Any other ID names no host +/// bridge, and the region is refused. +#[test] +fn a_host_bridge_without_a_bbn_is_found_by_its_hid_or_cid_on_bus_0() { + let lpc = |bridge: &[u8]| { + scope( + "\\_SB", + &device( + "PCI0", + &cat(&[ + bridge, + &device( + "ISAB", + &cat(&[ + &def_name("_ADR", &int(0x001F_0000)), + &op_region("ISAR", 0x02, &int(0x60), &int(0x04)), + &field("ISAR", BYTE, &[unit("PRQA", 8)]), + ]), + ), + ]), + ), + ) + }; + // EisaId ("PNP0A03"), EisaId ("PNP0A08"), and another device's. + let (pnp0a03, pnp0a08, other) = (0x030A_D041u64, 0x080A_D041u64, 0x090C_D041u64); + let at = Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 0, offset: 0x60 }; + for bridge in [ + def_name("_HID", &int(pnp0a08)), + cat(&[&def_name("_HID", &int(pnp0a08)), &def_name("_CID", &int(pnp0a03))]), + cat(&[&def_name("_HID", &int(other)), &def_name("_CID", &int(pnp0a03))]), + cat(&[&def_name("_HID", &string("ACPI0016")), &def_name("_CID", &package(&[int(other), string("PNP0A08")]))]), + method("_HID", 0, &ret(&string("PNP0A03"))), + ] { + let (m, v) = read(&lpc(&bridge), &[(at, &[0x8B])], "\\_SB.PCI0.ISAB.PRQA"); + assert_eq!(v, Ok(Value::Integer(0x8B))); + assert_eq!(m.accesses(), vec![Event::Read(at, Access::Byte)]); + } + for not_one in [ + def_name("_HID", &int(other)), + cat(&[&def_name("_HID", &string("PNP0A0")), &def_name("_CID", &package(&[string("PNP0A033")]))]), + def_name("_CID", &buffer(&int(4), b"PNP0")), + ] { + let (m, v) = read(&lpc(¬_one), &[(at, &[0x8B])], "\\_SB.PCI0.ISAB.PRQA"); + assert_eq!(v, Err(Error::Rule(NO_HOST_BRIDGE))); + assert_eq!(m.accesses(), vec![]); + } +} + +/// QEMU 11.1.1's q35 DSDT names no `_BBN`: its `\_SB.PCI0` is a host +/// bridge by its `_HID` and `_CID`, so an interrupt link's `_CRS`, which +/// reads the ISA bridge's routing registers, evaluates to its resource +/// template. +#[test] +fn qemus_interrupt_link_reads_its_routing_register_below_a_bridge_named_by_its_hid() { + let dsdt = include_bytes!("../../../../lib/acpi/fixtures/qemu-11.1.1/dsdt.bin"); + let mut m = Machine::default(); + let mut i = Interpreter::new(); + i.load_bytes(&mut m, dsdt).expect("QEMU's DSDT loads"); + // PIRQA routed to IRQ 11, as edk2 leaves it on q35. + m.poke(Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 0, offset: 0x60 }, &[0x0B]); + m.log.clear(); + let crs = i.evaluate(&mut m, "\\_SB.LNKA._CRS", &[]); + assert!(matches!(&crs, Ok(Value::Buffer(_))), "{crs:?}"); + assert!(m.accesses().iter().all(|e| matches!(e, Event::Read(Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 0, .. }, _))), "{:?}", m.accesses()); } /// A device below bridges is on the bus the nearest one's Secondary Bus @@ -273,7 +345,7 @@ fn a_pci_config_region_below_more_bridges_than_buses_is_refused() { assert_eq!(v, Err(Error::Rule("a PCI_Config region below more bridges than there are buses for them"))); assert_eq!(m.accesses(), vec![]); let (m, v) = below(256); - assert_eq!(v, Err(Error::Unsupported("a PCI_Config region below no host bridge, which names a _BBN"))); + assert_eq!(v, Err(Error::Rule(NO_HOST_BRIDGE))); assert_eq!(m.accesses(), vec![]); } @@ -370,16 +442,18 @@ fn a_lock_field_holds_the_global_lock_across_its_access() { } #[test] -fn a_host_refusal_and_a_space_not_carried_are_refused_by_name() { +fn a_host_refusal_and_a_reserved_space_are_refused_by_name() { let (mut i, mut m) = loaded(&memory_region(BYTE, &[unit("A", 8)])); m.refuse = true; assert_eq!(i.evaluate(&mut m, "\\A", &[]), Err(Error::Host("refused".into()))); - let smbus = cat(&[&op_region("SMB0", 0x04, &int(0), &int(0x100)), &field("SMB0", BYTE, &[unit("S", 8)])]); - assert_eq!(read(&smbus, &[], "\\S").1, Err(Error::Unsupported("the SMBus address space"))); + let smbus = cat(&[&op_region("SMB0", 0x04, &int(0), &int(0x100)), &field("SMB0", 0x05, &[unit("S", 8)])]); + let (mut i, mut m) = loaded(&smbus); + m.refuse = true; + assert_eq!(i.evaluate(&mut m, "\\S", &[]), Err(Error::Host("refused".into())), "a carried space the host denies"); // A reserved space or flag loads, and is refused where an access needs // its meaning; reserved bit 7 means nothing and is ignored. let reserved = cat(&[&op_region("RSV", 0x0C, &int(0), &int(1)), &field("RSV", BYTE, &[unit("R", 8)])]); - assert_eq!(read(&reserved, &[], "\\R").1, Err(Error::Unsupported("a reserved address space (Table 5.182)"))); + assert_eq!(read(&reserved, &[], "\\R").1, Err(Error::Rule("a region of a reserved address space (Table 5.182)"))); assert!(matches!(read(&memory_region(0x06, &[unit("A", 8)]), &[], "\\A").1, Err(Error::Rule(_)))); let (m, r) = store_into(&memory_region(BYTE | 0x60, &[unit("A", 4)]), "A", &int(1)); assert!(matches!(r, Err(Error::Rule(_)))); diff --git a/system/acpiserver/aml/tests/spaces.rs b/system/acpiserver/aml/tests/spaces.rs new file mode 100644 index 00000000000..356c5009c4b --- /dev/null +++ b/system/acpiserver/aml/tests/spaces.rs @@ -0,0 +1,209 @@ +//! Every address space of Table 5.182 reaches the host: one addressed by +//! offset an access unit at a time, as an [`Address`], and one whose protocol +//! is command- or connection-based (§5.5.2.4) whole, as a +//! [`toyos_aml::Transfer`] with its AccessAs protocol and its Connection. + +mod common; + +use common::*; +use toyos_aml::{Access, Address, Error, Value}; + +/// FieldFlags (§20.2.5.2). +const BYTE: u8 = 1; +const DWORD: u8 = 3; +const BUFFER: u8 = 5; + +/// `AccessAs (, )` (§20.2.5.2, AccessField). +fn access_as(access_type: u8, attrib: u8) -> Vec { + vec![0x01, access_type, attrib] +} + +/// An ExtendedAccessField: `AccessAs (, ())`. +fn access_as_extended(access_type: u8, attrib: u8, length: u8) -> Vec { + vec![0x03, access_type, attrib, length] +} + +/// `Connection ()` and `Connection ()` (§20.2.5.2, ConnectField). +fn connection(descriptor: &[u8]) -> Vec { + cat(&[&[0x02], &buffer(&int(descriptor.len() as u64), descriptor)]) +} + +fn connection_named(n: &str) -> Vec { + cat(&[&[0x02], &name(n)]) +} + +fn read(body: &[u8], answer: &[u8], path: &str) -> (Machine, Result) { + let (mut i, mut m) = loaded(body); + m.answer = answer.to_vec(); + m.log.clear(); + let r = i.evaluate(&mut m, path, &[]); + (m, r) +} + +fn transfers(m: &Machine) -> Vec { + m.log.iter().filter(|e| matches!(e, Event::Transfer { .. })).cloned().collect() +} + +/// §5.5.2.4.7: a GenericSerialBus field is one transfer with its protocol +/// and its connection, its value the buffer the host answered, and a store +/// to it answers the response its buffer became (`BUFF = (FLD = BUFF)`). +/// An AccessAs names its protocol by its byte, or, by the AccessType byte's +/// bits 7:6, AttribBytes, AttribRawBytes or AttribRawProcessBytes of a +/// count; an ExtendedAccessField names both. +#[test] +fn a_generic_serial_bus_field_is_one_transfer_with_its_protocol_and_connection() { + let descriptor = [0x8E, 0x01, 0x02, 0x03]; + let body = cat(&[ + &op_region("GSB0", 0x09, &int(0x10), &int(0x100)), + &field( + "GSB0", + BUFFER, + &[ + connection(&descriptor), + access_as(BUFFER, 0x08), + unit("WORD", 8), + access_as(BUFFER | 1 << 6, 4), + unit("BYT4", 8), + access_as(BUFFER | 3 << 6, 2), + unit("RAWP", 8), + access_as_extended(BUFFER, 0x0E, 16), + unit("RAWB", 8), + ], + ), + &def_name("BUFF", &buffer(&int(4), &[0, 0, 0x34, 0x12])), + &method("SEND", 0, &ret(&store(&name("BUFF"), &name("WORD")))), + ]); + let answer = [0x00, 0x02, 0xCD, 0xAB]; + for (unit, bit, attrib) in [("\\WORD", 0, (0x08, 0)), ("\\BYT4", 8, (0x0B, 4)), ("\\RAWP", 16, (0x0F, 2)), ("\\RAWB", 24, (0x0E, 16))] { + let (m, v) = read(&body, &answer, unit); + assert_eq!(v, Ok(Value::Buffer(answer.to_vec())), "{unit}"); + let expected = Event::Transfer { space: 0x09, region: 0x10, bit, bits: 8, attrib, connection: Some((descriptor.to_vec(), 0)), write: None }; + assert_eq!(transfers(&m), [expected], "{unit}"); + } + let (m, v) = read(&body, &answer, "\\SEND"); + assert_eq!(v, Ok(Value::Buffer(answer.to_vec())), "the store's value is the response"); + let written = Event::Transfer { space: 0x09, region: 0x10, bit: 0, bits: 8, attrib: (0x08, 0), connection: Some((descriptor.to_vec(), 0)), write: Some(vec![0, 0, 0x34, 0x12]) }; + assert_eq!(transfers(&m), [written]); +} + +/// §5.5.2.4.6: a GeneralPurposeIO field's pins follow its Connection, +/// inline or named, from the bit that connection began at; its value is an +/// Integer of its pins, and a store writes them. +#[test] +fn a_general_purpose_io_field_reaches_its_pins_through_its_connection() { + let (first, second) = ([0x8C, 0x01, 0x0A], [0x8C, 0x01, 0x07]); + let body = cat(&[ + &def_name("GMOD", &buffer(&int(3), &first)), + &op_region("GPO2", 0x08, &int(0), &int(1)), + &field("GPO2", BYTE, &[connection_named("GMOD"), unit("MODE", 2), connection(&second), unit("STAT", 1)]), + &method("SET", 0, &store(&int(3), &name("MODE"))), + ]); + let (m, v) = read(&body, &[0x02], "\\MODE"); + assert_eq!(v, Ok(Value::Integer(2))); + assert_eq!(transfers(&m), [Event::Transfer { space: 0x08, region: 0, bit: 0, bits: 2, attrib: (0, 0), connection: Some((first.to_vec(), 0)), write: None }]); + let (m, v) = read(&body, &[0x01], "\\STAT"); + assert_eq!(v, Ok(Value::Integer(1))); + assert_eq!(transfers(&m), [Event::Transfer { space: 0x08, region: 0, bit: 2, bits: 1, attrib: (0, 0), connection: Some((second.to_vec(), 2)), write: None }]); + let (m, _) = read(&body, &[], "\\SET"); + assert_eq!(transfers(&m), [Event::Transfer { space: 0x08, region: 0, bit: 0, bits: 2, attrib: (0, 0), connection: Some((first.to_vec(), 0)), write: Some(vec![3]) }]); +} + +/// SMBus, IPMI, PlatformRtMechanism and FFixedHW are carried whole, each a +/// BufferAcc field whose value is the host's buffer. +#[test] +fn smbus_ipmi_prm_and_ffh_fields_are_carried_whole() { + for (space, region) in [(0x04, 0x4200), (0x07, 0x3000), (0x0B, 0x1234), (0x7F, 0)] { + let body = cat(&[&op_region("REG0", space, &int(region), &int(0x100)), &field("REG0", BUFFER, &[skip(8 * 0xC1), unit("FLD", 8)])]); + let (m, v) = read(&body, &[0x00, 0x01, 0x5A], "\\FLD"); + assert_eq!(v, Ok(Value::Buffer(vec![0x00, 0x01, 0x5A])), "{space:#x}"); + assert_eq!(transfers(&m), [Event::Transfer { space, region, bit: 8 * 0xC1, bits: 8, attrib: (0, 0), connection: None, write: None }], "{space:#x}"); + } +} + +/// SystemCMOS (§5.5.2.4.3) and PCC (§5.5.2.4.8) a byte at a time, +/// PciBarTarget (§5.5.2.4.4) by the function's BAR at the region's offset, +/// its length the BAR's, and an OEM-defined space by offset: each an +/// [`Address`] the host answers. +#[test] +fn every_space_addressed_by_offset_reaches_the_host() { + let cmos = cat(&[&op_region("CMS0", 0x05, &int(0), &int(0x100)), &field("CMS0", BYTE, &[skip(8 * 0x40), unit("C40", 8)])]); + let (m, v) = read(&cmos, &[], "\\C40"); + assert_eq!((m.accesses(), v), (vec![Event::Read(Address::SystemCmos(0x40), Access::Byte)], Ok(Value::Integer(0)))); + let wide = cat(&[&op_region("CMS0", 0x05, &int(0), &int(0x100)), &field("CMS0", 0x02, &[unit("CW", 16)])]); + assert!(matches!(read(&wide, &[], "\\CW").1, Err(Error::Type(_))), "SystemCMOS permits ByteAcc alone (Table 19.34)"); + + let bar = |offset: u64| { + device( + "PCI0", + &cat(&[ + &def_name("_BBN", &int(0)), + &device("DEV2", &cat(&[&def_name("_ADR", &int(0x0002_0000)), &op_region("PBAR", 0x06, &int(offset), &int(4)), &field("PBAR", DWORD, &[skip(8 * 0x20), unit("REG", 32)])])), + ]), + ) + }; + let (m, v) = read(&bar(0x10), &[], "\\PCI0.DEV2.REG"); + let at = Address::PciBarTarget { segment: 0, bus: 0, device: 2, function: 0, bar: 0x10, offset: 0x20 }; + assert_eq!((m.accesses(), v), (vec![Event::Read(at, Access::DWord)], Ok(Value::Integer(0)))); + for offset in [0x11, 0x28, 0x0C] { + assert_eq!(read(&bar(offset), &[], "\\PCI0.DEV2.REG").1, Err(Error::Rule("a PciBarTarget region whose offset is no BAR's (§5.5.2.4.4.1)")), "{offset:#x}"); + } + + let pcc = cat(&[&op_region("PFRM", 0x0A, &int(2), &int(0x10C)), &field("PFRM", BYTE, &[skip(32), unit("FLGS", 16)])]); + let (m, _) = read(&pcc, &[], "\\FLGS"); + assert_eq!(m.accesses(), [4, 5].map(|offset| Event::Read(Address::Pcc { subspace: 2, offset }, Access::Byte))); + + let oem = cat(&[&op_region("OEM0", 0x80, &int(0x1000), &int(0x10)), &field("OEM0", DWORD, &[unit("O", 32)])]); + let (m, _) = read(&oem, &[], "\\O"); + assert_eq!(m.accesses(), [Event::Read(Address::Oem { space: 0x80, offset: 0x1000 }, Access::DWord)]); +} + +/// A reserved space (Table 5.182), BufferAcc where the space takes no +/// buffer, and a BankField of a carried space break a rule, and nothing +/// reaches the host. +#[test] +fn a_reserved_space_buffer_acc_by_offset_and_a_bank_of_a_carried_space_are_refused() { + let reserved = cat(&[&op_region("RES0", 0x0C, &int(0), &int(4)), &field("RES0", BYTE, &[unit("R", 8)])]); + let memory = cat(&[&op_region("MEM0", 0x00, &int(0x1000), &int(4)), &field("MEM0", BUFFER, &[unit("M", 8)])]); + let bank = cat(&[ + &op_region("MEM0", 0x00, &int(0x1000), &int(4)), + &field("MEM0", BYTE, &[unit("BNK", 8)]), + &op_region("SMB0", 0x04, &int(0), &int(0x100)), + &bank_field("SMB0", "BNK", &int(1), BUFFER, &[unit("B", 8)]), + ]); + for (body, path, why) in [ + (reserved, "\\R", "a region of a reserved address space (Table 5.182)"), + (memory, "\\M", "BufferAcc in a space addressed by offset, which takes no buffer (§19.6.47)"), + (bank, "\\B", "a BankField of a space whose protocol is command- or connection-based (§5.5.2.4)"), + ] { + let (m, v) = read(&body, &[], path); + assert_eq!(v, Err(Error::Rule(why)), "{path}"); + assert_eq!((m.accesses(), transfers(&m)), (vec![], vec![]), "{path}"); + } +} + +/// A reference to an unnamed object handed to the caller is what it referred +/// to: a package's element by Index; a method's LocalX is gone with it; and +/// no caller passes one back. +#[test] +fn a_reference_to_an_unnamed_object_is_handed_over_as_what_it_refers_to() { + let body = cat(&[ + &def_name("PKG", &package(&[int(5), int(7)])), + &method("ELEM", 0, &ret(&index(&name("PKG"), &int(1), &[0x00]))), + &method("LOCL", 0, &cat(&[&store(&int(9), &local(0)), &ret(&ref_of(&local(0)))])), + &method("TAKE", 1, &ret(&arg(0))), + ]); + let (mut i, mut m) = loaded(&body); + assert_eq!(i.evaluate(&mut m, "\\ELEM", &[]), Ok(Value::Unnamed(Box::new(Value::Integer(7))))); + assert_eq!(i.evaluate(&mut m, "\\LOCL", &[]), Err(Error::NotFound("a LocalX or ArgX its method's exit destroyed".into()))); + let passed = Value::Unnamed(Box::new(Value::Integer(7))); + assert_eq!(i.evaluate(&mut m, "\\TAKE", &[passed]), Err(Error::Type("an argument that is a reference to an unnamed object, which only AML makes"))); +} + +/// ACPI 6.5 reserves `0x5B 0x2A`, once Unload (Table 20.2), and a block a +/// Load made has no handle to name: it is malformed, as a statement and as +/// an argument. +#[test] +fn unload_is_a_reserved_opcode() { + assert!(matches!(returns(&[0x5B, 0x2A, 0x60]), Err(Error::Malformed { .. }))); + assert!(matches!(returns(&ret(&[0x5B, 0x2A, 0x60])), Err(Error::Malformed { .. }))); +} diff --git a/system/acpiserver/aml/tests/walk.rs b/system/acpiserver/aml/tests/walk.rs index 3fd282899f6..47f07a0095f 100644 --- a/system/acpiserver/aml/tests/walk.rs +++ b/system/acpiserver/aml/tests/walk.rs @@ -174,7 +174,7 @@ fn every_object_is_walked_as_what_it_is() { assert_eq!(at("\\STR0"), (1, Kind::String)); assert_eq!(at("\\BUF0"), (1, Kind::Buffer)); assert_eq!(at("\\PKG0"), (1, Kind::Package)); - assert_eq!(at("\\REG0"), (1, Kind::OperationRegion)); + assert_eq!(at("\\REG0"), (1, Kind::OperationRegion { space: 0 })); assert_eq!(at("\\FLD0"), (1, Kind::FieldUnit)); assert_eq!(at("\\BFL0"), (1, Kind::BufferField)); assert_eq!(at("\\MTX0"), (1, Kind::Mutex)); diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index 2105fa4ab51..e535c37ee12 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -1,7 +1,9 @@ //! Everything only the machine's AML can answer: its definition blocks //! loaded into one namespace ([`toyos_aml`]), and what this server asks of it. //! -//! [`load`] fetches the DSDT and every SSDT through the kernel +//! [`load`] attaches the controller the ECDT names, which AML may reach +//! before any control method runs (ACPI 6.5 §6.5.4), to the host; then +//! fetches the DSDT and every SSDT through the kernel //! ([`crate::tables`]), loads each in the order the firmware lists them, and //! says one line a table: loaded, or refused and why. **A refused SSDT is //! said loudly and the load goes on** (the owner's ruling: "Go on, say it @@ -23,9 +25,11 @@ //! not hold — and that is said once, loudly ([`NO_S5_HANDED`]); the server //! goes on serving. **A namespace whose DSDT loaded is kept** ([`Aml`]), //! with the host that answered its load, for what the server evaluates in it -//! after: the devices it serves ([`crate::devices`]), the power sources -//! ([`crate::battery`]), and, where the power button is a control method -//! device, each embedded-controller query's method. +//! after: its initialisation ([`crate::init`]), `_OSC` ([`crate::osc`]), the +//! devices it serves ([`crate::devices`]), the power sources +//! ([`crate::battery`]), the runtime GPEs' methods ([`crate::gpe`]), and, +//! where the power button is a control method device, each +//! embedded-controller query's method. //! //! A table's line says its place, whether it is the DSDT or an SSDT, and a //! refusal by its kind; what the firmware chose — a name, an offset, an @@ -36,7 +40,7 @@ use std::time::Instant; use toyos_acpi::TableError; use toyos_aml::{Error, Host, Interpreter, Value}; -use crate::host::{Firmware, Kernel, Refusal, Stopping, OWN}; +use crate::host::{Firmware, Kernel, Refusal, Stopping, Unmade, OWN}; use crate::tables::Tables; /// A machine's namespace, and the host every evaluation in it is answered by. @@ -68,7 +72,6 @@ pub fn kind(why: &Error) -> String { Error::Type(why) => format!("a type its operator refuses: {why}"), Error::Rule(why) | Error::Table(why) | Error::Bound(why) => (*why).into(), Error::Fatal { .. } => "the firmware executed Fatal".into(), - Error::Unsupported(why) => format!("not carried by this interpreter: {why}"), Error::Host(why) => format!("denied by this server: {why}"), Error::Bridge { .. } => BRIDGE.into(), } @@ -104,6 +107,7 @@ pub const NO_S5_HANDED: &str = /// evaluate `\_S5`; the namespace, where its DSDT loaded, is kept. pub fn load(kernel: &K, rsdp: u64) -> (Loaded, Option>) { let mut host = Firmware::new(kernel); + host.rsdp = rsdp; let mut interpreter = Interpreter::new(); let loaded = load_into(kernel, &mut interpreter, &mut host, rsdp); let kept = loaded.blocks.first().is_some_and(|dsdt| dsdt.is_ok()) && !host.stopping; @@ -135,6 +139,7 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi println!("{STOPPING}"); return loaded; } + early_controller(kernel, &tables, rsdp, host); let count = blocks.len(); for (place, block) in blocks.iter().enumerate() { @@ -183,13 +188,14 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi println!("acpiserver: reads of the tables' bytes the kernel refused: {}", refusals.counts()); } println!( - "acpiserver: the tables' AML read SystemMemory {} times, SystemIO {} and PCI_Config {}, its memory in pages: {}; took the Global Lock {} times, {} of them from the firmware; and ran Notify {} times", + "acpiserver: the tables' AML read SystemMemory {} times, SystemIO {} and PCI_Config {}, its memory in pages: {}; took the Global Lock {} times, {} of them from the firmware; the firmware held it {:?} at the longest against a take; and ran Notify {} times", host.reads[0], host.reads[1], host.reads[2], host.pages.by_type(), host.takes, host.contended, + host.longest_hold, host.notifies ); @@ -226,6 +232,45 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi loaded } +/// Attach the embedded controller the ECDT names to `host`, so its space is +/// there "before executing any control method" (ACPI 6.5 §6.5.4, the owner's +/// "follow the spec"): its ports, its device's path, and no Global Lock, +/// whose rule only the device's `_GLK` names, until the namespace's device +/// is found and served in its place. A machine with no ECDT waits for that +/// device; one whose ECDT names no controller this server reaches is said. +fn early_controller(kernel: &K, tables: &Tables<'_, K>, rsdp: u64, host: &mut Firmware<'_, K>) { + let table = match toyos_acpi::find_table(tables, rsdp, b"ECDT", toyos_acpi::ECDT_NEEDED) { + Ok(table) => table, + Err(TableError::Absent) => return, + Err(why) => { + println!("acpiserver: the ECDT was not read, so the controller's space waits for its device: {}", unread(&why, |at| tables.refused(at))); + return; + } + }; + let ec = match toyos_acpi::ecdt(&table) { + Ok(ec) => ec, + Err(why) => { + println!("acpiserver: the ECDT names no controller this server reaches, so its space waits for its device"); + println!("{OWN}that was {why:x?}"); + return; + } + }; + let id: String = ec.id.clone().filter_map(|at| table.byte(at)).map(char::from).collect(); + let Some(path) = crate::devices::canonical(&id) else { + println!("acpiserver: the ECDT's EC_ID is no fully qualified path, so the controller's space waits for its device"); + println!("{OWN}that was {id:?}"); + return; + }; + // Its status register, which a read leaves as it was. + if let Err(Unmade::Refused(why)) = crate::host::port_in(kernel, ec.command) { + println!("acpiserver: the ECDT's embedded controller is not attached: {why}"); + return; + } + println!("acpiserver: the ECDT's embedded controller is attached to AML before any control method runs"); + println!("{OWN}it is {path} at {:#x}/{:#x}, GPE {:#x}", ec.command, ec.data, ec.gpe); + host.ec = Some(crate::devices::Ec { path, command: ec.command, data: ec.data, gpe: ec.gpe.into(), queries: Default::default(), glk: false }); +} + /// `\_S5`'s first two elements: what `SLP_TYPa` and `SLP_TYPb` are written /// for soft-off. fn s5(interpreter: &mut Interpreter, host: &mut dyn Host) -> Result<(u64, u64), String> { @@ -274,19 +319,26 @@ pub mod tests { /// What every guest boot does, against the kernel's stand-in: QEMU's /// DSDT is fetched, loads asking the machine for nothing, and its `\_S5` - /// is the `SLP_TYPa=0` the kernel logged on the boot the tables are of; - /// its namespace names no embedded controller, and no control-method - /// button where one is looked for. + /// is the `SLP_TYPa=0` the kernel logged on the boot the tables are of. + /// It defines no `_INI`, so its initialisation runs none and examines no + /// device. Its namespace names no embedded controller, and no + /// control-method button where one is looked for. #[test] fn qemus_tables_load_and_s5_is_what_its_kernel_decoded() { let kernel = machine(QEMU); let (loaded, kept) = load(&kernel, RSDP); assert_eq!(loaded, Loaded { blocks: vec![Ok(())], s5: Some((0, 0)), handed: true }); - let Aml { mut interpreter, mut host } = kept.expect("QEMU's DSDT loads"); + let mut aml = kept.expect("QEMU's DSDT loads"); + let names = crate::init::Names::read(&aml.interpreter).expect("the namespace reads"); + let mut census = crate::init::Census::default(); + crate::init::initialise(&mut aml, &names, &mut census); + assert_eq!((census.sb_ini, census.ini, census.sta), (None, 0, 0)); + assert!(census.refused.is_empty(), "{}", census.refused.counts()); + let Aml { mut interpreter, mut host } = aml; for control_method in [false, true] { - assert_eq!(crate::devices::find(&mut interpreter, &mut host, crate::devices::tests::GPE0, control_method), crate::devices::Found::default()); + assert_eq!(crate::devices::find(&mut interpreter, &mut host, &names, crate::devices::tests::GPE0, control_method), crate::devices::Found::default()); } - assert!(kernel.asked.borrow().iter().all(|access| access.write == 0 && access.space == 0)); + assert!(kernel.asked.borrow().iter().all(|access| access.write == 0 && access.space != 1), "QEMU's AML wrote or read a port"); assert!(!kernel.held.get()); assert_eq!(kernel.handed.get(), Some(0)); } @@ -320,6 +372,13 @@ pub mod tests { /// A machine whose XSDT lists a FADT naming `dsdt` and then `ssdts`. pub fn crafted(dsdt: &[u8], ssdts: &[&[u8]]) -> Scripted { + crafted_with(dsdt, ssdts, &[]) + } + + /// [`crafted`], its XSDT listing `others` after the SSDTs. + pub fn crafted_with(dsdt: &[u8], ssdts: &[&[u8]], others: &[&[u8]]) -> Scripted { + let tables: Vec<&[u8]> = ssdts.iter().chain(others).copied().collect(); + let ssdts = &tables[..]; const AT: u64 = 0x10_0000; let at = |n: usize| AT + 0x1_0000 * n as u64; let mut fadt_body = vec![0u8; 116 - 36]; @@ -347,6 +406,37 @@ pub mod tests { pub const CRAFTED_RSDP: u64 = 0x10_0000 - 0x1000; + /// An ECDT (ACPI 6.5 Table 5.54) naming a controller at the host tests' + /// two ports, on GPE 3, whose device is `id`. + pub fn ecdt(id: &[u8]) -> Vec { + let gas = |port: u64| cat(&[&[1, 8, 0, 1], &port.to_le_bytes()]); + let mut table = sealed(b"ECDT", &cat(&[&gas(crate::host::tests::EC_COMMAND), &gas(crate::host::tests::EC_DATA), &1u32.to_le_bytes(), &[3], id, &[0]])); + table[8] = 1; + table[9] = 0; + table[9] = 0u8.wrapping_sub(table.iter().fold(0u8, |sum, &byte| sum.wrapping_add(byte))); + table + } + + /// §6.5.4: the ECDT's controller is attached before the DSDT loads, by + /// the path its `EC_ID` names as the namespace writes it, with no Global + /// Lock, whose rule its device's `_GLK` names; a machine with no ECDT, + /// an `EC_ID` that is no path, and a controller whose status port the + /// kernel refuses have none attached. + #[test] + fn the_ecdts_controller_is_attached_before_any_control_method() { + let dsdt = sealed(b"DSDT", &s5_package(5, 0)); + let attached = |kernel: &Scripted| load(kernel, CRAFTED_RSDP).1.expect("the DSDT loads").host.ec; + let mut kernel = crafted_with(&dsdt, &[], &[&ecdt(b"\\_SB.PCI0.LPC0.EC")]); + kernel.ports = vec![(crate::host::tests::EC_COMMAND, 0)]; + let ec = crate::devices::Ec { path: "\\_SB_.PCI0.LPC0.EC__".into(), command: 0x66, data: 0x62, gpe: 3, queries: Default::default(), glk: false }; + assert_eq!(attached(&kernel), Some(ec)); + let mut kernel = crafted_with(&dsdt, &[], &[&ecdt(b"_SB.EC")]); + kernel.ports = vec![(crate::host::tests::EC_COMMAND, 0)]; + assert_eq!(attached(&kernel), None, "an EC_ID that is no fully qualified path"); + assert_eq!(attached(&crafted_with(&dsdt, &[], &[&ecdt(b"\\_SB.EC")])), None, "a status port the kernel refuses"); + assert_eq!(attached(&crafted(&dsdt, &[])), None, "no ECDT"); + } + #[test] fn a_refused_ssdt_is_one_block_refused_and_the_rest_load() { let dsdt = sealed(b"DSDT", &cat(&[&name(b"AAAA", 1), &s5_package(5, 7)])); diff --git a/system/acpiserver/src/battery.rs b/system/acpiserver/src/battery.rs index c1552048b6f..a7a8516c54b 100644 --- a/system/acpiserver/src/battery.rs +++ b/system/acpiserver/src/battery.rs @@ -13,8 +13,10 @@ //! the batteries' and adapters' own methods alone: the controller's `_REG` //! and the walk that named the devices ran before it, and are not theirs. //! -//! **Every [`POLL`]** each battery's `_BST` and each adapter's `_PSR` are -//! evaluated ([`Power::read`]), and a line is said on the first reading and +//! **Every [`POLL`]**, and on a Notify of a status change (§10.2.1), each +//! battery's `_BST` and each adapter's `_PSR` are evaluated +//! ([`Power::read`]); on a battery's Notify of an information change they are +//! found again. A line is said on the first reading and //! wherever a battery's whole percent or state, or the adapter's, moved since //! the line before: a draining battery says a line a percent, never one a //! poll. @@ -358,7 +360,7 @@ impl Power { if present == 0 { return (None, Some(format!("acpiserver: no battery: {found}"))); } - let line = format!("acpiserver: power sources: {found}, the first reading's included; each read every {} s", POLL.as_secs()); + let line = format!("{}{found}, the first reading's included; each read every {} s", acpiserver_api::POWER_SOURCES, POLL.as_secs()); (Some(power), Some(line)) } @@ -555,12 +557,14 @@ pub mod tests { } /// The fixture loaded and its controller found and attached, as the - /// server arms it ([`crate::attach`]). + /// server arms it ([`devices::attach`]). fn armed(kernel: &Scripted) -> (Aml<'_, Scripted>, Found) { let (_, aml) = crate::aml::load(kernel, CRAFTED_RSDP); let mut aml = aml.expect("the fixture's DSDT loads"); - let found = devices::find(&mut aml.interpreter, &mut aml.host, GPE0, false); - crate::attach(&mut aml, found.ec.as_ref().expect("the fixture's controller")).expect("no port of it refused"); + let names = crate::init::Names::read(&aml.interpreter).expect("the namespace reads"); + let found = devices::find(&mut aml.interpreter, &mut aml.host, &names, GPE0, false); + let ec = found.ec.clone().expect("the fixture's controller"); + devices::attach(&mut aml, ec, &names, &mut crate::init::Census::default()).expect("no port of it refused"); (aml, found) } @@ -677,19 +681,27 @@ pub mod tests { } /// §6.5.7: a controller whose `_GLK` is 1 wants the Global Lock around - /// each transaction, which this server does not take: its space is not - /// attached to AML, its `_REG` is not run, and the battery, which says - /// it is present only after, is not read, nor is the controller reached. + /// each transaction: its space is attached, its `_REG` run and its + /// battery read, every transaction AML makes on it under the lock, which + /// the kernel's stand-in holds it to, and the lock given back after each. + /// One whose `_GLK` is absent or 0 is reached without the lock, and one + /// whose `_GLK` is another value is not attached, nor its battery read, + /// nor the controller reached. #[test] - fn a_controller_that_wants_the_global_lock_is_not_attached_and_no_battery_is_read() { - for (glk, attached) in [(Some(1), false), (Some(0), true), (None, true)] { - let kernel = laptop_machine(glk); + fn a_controller_that_wants_the_global_lock_is_reached_under_it_alone() { + for (glk, attached, locked) in [(Some(1), true, true), (Some(0), true, false), (None, true, false), (Some(2), false, false)] { + let mut kernel = laptop_machine(glk); + kernel.lock_rule = locked; let (mut aml, power) = found(&kernel); assert_eq!(power.is_some(), attached, "_GLK {glk:?}"); - assert_eq!(aml.host.ec.is_some(), attached, "_GLK {glk:?}"); + assert_eq!(aml.host.ec.as_ref().map(|ec| ec.glk), attached.then_some(locked), "_GLK {glk:?}"); let ecok = aml.interpreter.evaluate(&mut aml.host, "\\_SB_.EC0_.ECOK", &[]); assert_eq!(ecok, Ok(Value::Integer(u64::from(attached))), "_GLK {glk:?}"); assert_eq!(done(&kernel).is_empty(), !attached, "_GLK {glk:?}: {:x?}", done(&kernel)); + assert!(!kernel.held.get(), "_GLK {glk:?}: the lock was left taken"); + if locked { + assert!(aml.host.takes as usize > done(&kernel).len(), "_GLK {glk:?}: fewer takes than transactions"); + } } } diff --git a/system/acpiserver/src/devices.rs b/system/acpiserver/src/devices.rs index 2e14e0d4afa..3febdb29763 100644 --- a/system/acpiserver/src/devices.rs +++ b/system/acpiserver/src/devices.rs @@ -17,6 +17,12 @@ //! being served is said once, by kind; what the firmware chose — a path, a //! number — goes under [`OWN`]. //! +//! **The controller is attached to AML under the lock rule its `_GLK` +//! names** ([`attach`], §6.5.7): none where it is absent or 0, the Global +//! Lock around each transaction where it is 1, and unattached otherwise; +//! then `_REG(3, 1)` runs in each scope that declares a region of its space, +//! after every `_INI` (§6.5.4). +//! //! **Buttons are looked for only where the FADT says the power button is a //! control method device**; on a machine whose button is the fixed one, a //! PNP0C0C the firmware lists anyway is not another way to press it. @@ -32,8 +38,13 @@ use toyos_abi::acpi::Block; use acpiserver_api::CONTROLLER_NONE; use toyos_aml::{Host, Interpreter, Kind, Value}; +use crate::aml::Aml; +use crate::host::Kernel; +use crate::init::Census; + use crate::aml::kind; use crate::host::OWN; +use crate::init::Names; /// The embedded controller, as its device names it. #[derive(Clone, Debug, PartialEq, Eq)] @@ -44,6 +55,8 @@ pub struct Ec { pub gpe: u16, /// The queries its device defines a method for. pub queries: BTreeSet, + /// Its `_GLK` asks for the Global Lock around each transaction (§6.5.7). + pub glk: bool, } /// What was found. @@ -65,15 +78,10 @@ const ADAPTER: &str = "ACPI0003"; /// Find the controller, the buttons where `control_method`, and every /// battery and adapter, in the namespace `interpreter` holds; `gpe0` is the /// block every GPE is in. -pub fn find(interpreter: &mut Interpreter, host: &mut dyn Host, gpe0: Block, control_method: bool) -> Found { +pub fn find(interpreter: &mut Interpreter, host: &mut dyn Host, names: &Names, gpe0: Block, control_method: bool) -> Found { let mut found = Found::default(); - let (devices, named) = match devices(interpreter) { - Ok(walked) => walked, - Err(why) => { - println!("acpiserver: no device of the namespace is served: its walk was refused, {why}"); - return found; - } - }; + let named = &names.paths; + let devices = names.objects.iter().filter(|object| object.kind == Kind::Device).map(|object| object.path.clone()); let mut controllers = Vec::new(); for device in devices { let hid = format!("{device}._HID"); @@ -101,7 +109,7 @@ pub fn find(interpreter: &mut Interpreter, host: &mut dyn Host, gpe0: Block, con Some(POWER_BUTTON) => control_method, _ => false, }; - if !wanted || !present(interpreter, host, &named, &device) { + if !wanted || !present(interpreter, host, named, &device) { continue; } match id.as_deref() { @@ -117,7 +125,7 @@ pub fn find(interpreter: &mut Interpreter, host: &mut dyn Host, gpe0: Block, con println!("{CONTROLLER_NONE}no PNP0C09 device is present"); None } - [device] => match controller(interpreter, host, device, gpe0, &named) { + [device] => match controller(interpreter, host, device, gpe0, named) { Ok(ec) => { println!("{OWN}{device} is the embedded controller"); Some(ec) @@ -135,6 +143,35 @@ pub fn find(interpreter: &mut Interpreter, host: &mut dyn Host, gpe0: Block, con found } +/// Attach `ec` to the AML host, so its space reaches the controller under +/// the lock rule its `_GLK` names (ACPI 6.5 §6.5.7): none where it is absent +/// or 0, the Global Lock around each transaction where it is 1. Then tell +/// each scope that declares a region of its space that the space is there, +/// by its `_REG` (§6.5.4: Arg0 3 the EmbeddedControl space, Arg1 1 connect), +/// after the namespace's `_INI`s have run. A `_GLK` that is another value, no +/// Integer or refused leaves the space unattached, said. `Err` is why a port +/// of it the kernel refused ended it; `Ok` is the controller as served. +pub fn attach(aml: &mut Aml<'_, K>, mut ec: Ec, names: &Names, census: &mut Census) -> Result { + let glk = format!("{}._GLK", ec.path); + let unattached: Option = match names.paths.contains(&glk).then(|| aml.interpreter.evaluate(&mut aml.host, &glk, &[])) { + None | Some(Ok(Value::Integer(0))) => None, + Some(Ok(Value::Integer(1))) => { + ec.glk = true; + None + } + Some(Ok(Value::Integer(_))) => Some("its _GLK is neither 0 nor 1".into()), + Some(Ok(_)) => Some("its _GLK is no Integer".into()), + Some(Err(why)) => Some(format!("its _GLK was refused, {}", kind(&why))), + }; + if let Some(why) = unattached { + println!("acpiserver: the embedded controller's space is not attached to AML: {why}"); + return Ok(ec); + } + aml.host.ec = Some(ec.clone()); + crate::init::announce(aml, names, crate::init::EMBEDDED_CONTROL, 0, census); + aml.host.ended.take().map_or(Ok(ec), Err) +} + /// What became of an embedded-controller query's method. #[derive(Debug, PartialEq, Eq)] pub enum Queried { @@ -160,23 +197,28 @@ pub fn query(interpreter: &mut Interpreter, host: &mut dyn Host, ec: &Ec, q: u8) } } -/// Every device's path, and every name's, by one walk. -fn devices(interpreter: &Interpreter) -> Result<(Vec, BTreeSet), String> { - let mut walk = interpreter.walk().map_err(|why| kind(&why))?; - let (mut devices, mut named) = (Vec::new(), BTreeSet::new()); - while let Some(entry) = walk.next() { - if entry.kind == Kind::Device { - devices.push(walk.path().to_string()); +/// A fully qualified path as ASL writes it, `\\_SB.PCI0.EC`, as the +/// namespace's walk writes it: each segment padded with `_` to four +/// characters (§5.3). `None` where it is no such path. +pub fn canonical(text: &str) -> Option { + let segments = text.strip_prefix('\\')?; + let mut path = String::new(); + for segment in segments.split('.') { + let lead = segment.bytes().next().is_some_and(|c| c.is_ascii_uppercase() || c == b'_'); + let rest = segment.bytes().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == b'_'); + if !lead || !rest || segment.len() > 4 { + return None; } - named.insert(walk.path().to_string()); + path.push(if path.is_empty() { '\\' } else { '.' }); + path.push_str(&format!("{segment:_<4}")); } - Ok((devices, named)) + Some(path) } /// A `_HID` as the text it names (§6.1.5): a string as it is, and an integer /// as the EISA ID it compresses, three letters of five bits each and four /// hex digits, its bytes in the order AML stores them. -fn id(value: &Value) -> Option { +pub fn id(value: &Value) -> Option { match value { Value::String(text) => String::from_utf8(text.clone()).ok(), &Value::Integer(compressed) => { @@ -238,7 +280,7 @@ fn controller(interpreter: &mut Interpreter, host: &mut dyn Host, device: &str, Err(why) => return Err(format!("names no GPE: {}", kind(&why))), }; let queries = (0..=u8::MAX).filter(|q| named.contains(&format!("{device}._Q{q:02X}"))).collect(); - Ok(Ec { path: device.into(), command, data, gpe, queries }) + Ok(Ec { path: device.into(), command, data, gpe, queries, glk: false }) } #[cfg(test)] @@ -324,12 +366,13 @@ pub mod tests { let kernel = crafted(&sealed(b"DSDT", dsdt), &[]); let (_, kept) = load(&kernel, CRAFTED_RSDP); let Aml { mut interpreter, mut host } = kept.expect("the DSDT loads"); - find(&mut interpreter, &mut host, GPE0, control_method) + let names = crate::init::Names::read(&interpreter).expect("the namespace reads"); + find(&mut interpreter, &mut host, &names, GPE0, control_method) } #[test] fn an_amd_laptops_controller_and_button_are_found_in_its_dsdt() { - let ec = Ec { path: EC_PATH.into(), command: 0x66, data: 0x62, gpe: 3, queries: BTreeSet::new() }; + let ec = Ec { path: EC_PATH.into(), command: 0x66, data: 0x62, gpe: 3, queries: BTreeSet::new(), glk: false }; assert_eq!(found(&laptop(&[]), true), Found { ec: Some(ec.clone()), buttons: vec![PWRB.into()], ..Found::default() }); assert_eq!(found(&laptop(&[]), false), Found { ec: Some(ec), ..Found::default() }, "a fixed button's machine looks for none"); } @@ -363,7 +406,8 @@ pub mod tests { kernel.ports = vec![(0x80, 0)]; let (_, kept) = load(&kernel, CRAFTED_RSDP); let Aml { mut interpreter, mut host } = kept.expect("the DSDT loads"); - let found = find(&mut interpreter, &mut host, GPE0, true); + let names = crate::init::Names::read(&interpreter).expect("the namespace reads"); + let found = find(&mut interpreter, &mut host, &names, GPE0, true); let ec = found.ec.expect("the controller"); assert_eq!(ec.queries, BTreeSet::from([0x28, 0x2A])); host.buttons = found.buttons; @@ -384,7 +428,8 @@ pub mod tests { let kernel = crafted(&sealed(b"DSDT", &laptop_with_queries()), &[]); let (_, kept) = load(&kernel, CRAFTED_RSDP); let Aml { mut interpreter, mut host } = kept.expect("the DSDT loads"); - let found = find(&mut interpreter, &mut host, GPE0, true); + let names = crate::init::Names::read(&interpreter).expect("the namespace reads"); + let found = find(&mut interpreter, &mut host, &names, GPE0, true); host.buttons = found.buttons; let refused = query(&mut interpreter, &mut host, found.ec.as_ref().expect("the controller"), 0x28); assert_eq!(refused, Queried::Refused("denied by this server: a SystemIO write the kernel refused KernelPort".into())); diff --git a/system/acpiserver/src/gpe.rs b/system/acpiserver/src/gpe.rs new file mode 100644 index 00000000000..3df2cfef92d --- /dev/null +++ b/system/acpiserver/src/gpe.rs @@ -0,0 +1,121 @@ +//! The machine's general-purpose events (ACPI 6.5 §5.6.4): which GPE0 inputs +//! this server enables, by the methods `\_GPE` names and the wake events +//! devices' `_PRW` name. +//! +//! **A runtime GPE is one with a `\_GPE._Lxx` or `_Exx`** (§5.6.4.1.1), `xx` +//! its number in GPE0 in two hex digits, an `_L` a level's and an `_E` an +//! edge's, **and no wake event**: §5.6.4.2 keeps wake and runtime events off +//! one input except for the power, lid and sleep buttons and the PCI host +//! bridge's PME, so a GPE the `_PRW` of any other device names is a wake +//! event, which OSPM enables only to arm a device's wake (§5.6.10), and this +//! server arms none. A GPE that names both methods, or that is the embedded +//! controller's, which its driver serves (§5.6.4.1.2), is no runtime GPE +//! either. A `_PRW` that names a GPE block device names no GPE0 input. +//! +//! What is found is said in one line, a count each; the machine's own +//! numbers and paths go under [`OWN`]. + +use std::collections::{BTreeMap, BTreeSet}; + +use toyos_abi::acpi::Block; +use toyos_aml::{Kind, Value}; + +use crate::aml::{kind, Aml}; +use crate::host::{Kernel, OWN}; +use crate::init::Names; +use crate::sci::Trigger; + +/// The devices §5.6.4.2 lets share one GPE between wake and runtime events: +/// the power, lid and sleep buttons, and the PCI host bridge. +const SHARED: [&str; 4] = ["PNP0C0C", "PNP0C0D", "PNP0C0E", "PNP0A03"]; + +/// The runtime GPEs of GPE0 `gpe0`, but the controller's `ec_gpe`, each with +/// its method's trigger. +pub fn runtime(aml: &mut Aml<'_, K>, names: &Names, gpe0: Block, ec_gpe: Option) -> BTreeMap { + // §5.2.9: a GPE block's status half holds eight GPEs a byte; its length is the FADT's byte. + let inputs = gpe0.len / 2 * 8; + let mut methods: BTreeMap> = BTreeMap::new(); + for named in names.objects.iter().filter(|named| named.kind == Kind::Method) { + let Some(name) = named.path.strip_prefix("\\_GPE.") else { continue }; + let (trigger, digits) = match name.as_bytes() { + [b'_', b'L', ..] => (Trigger::Level, &name[2..]), + [b'_', b'E', ..] => (Trigger::Edge, &name[2..]), + _ => continue, + }; + if let Ok(n) = u16::from_str_radix(digits, 16) { + methods.entry(n).or_default().push(trigger); + } + } + let mut wake = BTreeSet::new(); + for named in names.objects.iter().filter(|named| matches!(named.kind, Kind::Device | Kind::Processor | Kind::ThermalZone | Kind::PowerResource)) { + let prw = format!("{}._PRW", named.path); + if !names.paths.contains(&prw) { + continue; + } + let n = match aml.interpreter.evaluate(&mut aml.host, &prw, &[]) { + Ok(Value::Package(p)) => match p.first() { + Some(&Value::Integer(n)) => n, + // A GPE block device's input (§7.3.13), no GPE0 one. + Some(Value::Package(_)) => continue, + _ => { + println!("{OWN}{prw} names its GPE by no Integer or package"); + continue; + } + }, + Ok(_) => { + println!("{OWN}{prw} is no package"); + continue; + } + Err(why) => { + println!("{OWN}{prw} was refused {why:x?}"); + println!("acpiserver: a device's _PRW was refused, so the GPE it names is not known for a wake event: {}", kind(&why)); + continue; + } + }; + if !shares(aml, names, &named.path) { + wake.insert(n); + } + } + let mut runtime = BTreeMap::new(); + let (mut beyond, mut both) = (0, 0); + for (n, triggers) in &methods { + match (triggers.as_slice(), *n < inputs) { + (_, false) => beyond += 1, + ([_, _, ..], true) => { + both += 1; + println!("{OWN}GPE {n:#x} names both an _L and an _E method"); + } + (&[trigger], true) if Some(*n) != ec_gpe && !wake.contains(&u64::from(*n)) => { + runtime.insert(*n, trigger); + } + _ => {} + } + } + println!( + "acpiserver: GPEs: {} with a method in GPE0, {} of them runtime and enabled, {} wake events left disabled, {} past GPE0, {} naming both triggers", + methods.len() - beyond, + runtime.len(), + wake.len(), + beyond, + both + ); + println!("{OWN}the runtime GPEs are {:x?}", runtime.keys().collect::>()); + runtime +} + +/// Whether the device at `path` is one §5.6.4.2 lets share a GPE between +/// wake and runtime events, by its `_HID` or a `_CID`. +fn shares(aml: &mut Aml<'_, K>, names: &Names, path: &str) -> bool { + ["_HID", "_CID"].into_iter().any(|id| { + let at = format!("{path}.{id}"); + if !names.paths.contains(&at) { + return false; + } + let ids = match aml.interpreter.evaluate(&mut aml.host, &at, &[]) { + Ok(Value::Package(ids)) => ids, + Ok(id) => vec![id], + Err(_) => return false, + }; + ids.iter().filter_map(crate::devices::id).any(|id| SHARED.contains(&id.as_str())) + }) +} diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 71086a49a11..a9d6187a8e5 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -7,20 +7,43 @@ //! the kernel's to make or refuse**, and a refusal it names is denied under //! that name. //! -//! **Only the port writes in [`FORWARDED`] are asked of the kernel**, each -//! said the first time it is asked: a write to a register that changes -//! nothing but an index or a POST code. Every other port write is denied by -//! name and never reaches the kernel: a reset-capable port, `SMI_CMD`, a data -//! register. The reset register is the kernel's to keep wherever it is. A -//! write to memory or to configuration space is denied by name. SystemCMOS -//! never arrives: the interpreter refuses that space itself. +//! **A port write is the kernel's to make or refuse, as a read is**, each +//! port and width said the first time it is asked, as the owner ruled on +//! #875. The kernel refuses the ports of the devices it drives +//! (`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. +//! +//! **The spaces this server serves are SystemMemory, SystemIO, PCI_Config +//! and EmbeddedControl.** Every other space of Table 5.182 reaches it and is +//! denied by name: SystemCMOS, PciBarTarget, PCC, the OEM-defined ones, and +//! the command- and connection-based ones the interpreter carries whole +//! (SMBus, IPMI, GeneralPurposeIO, GenericSerialBus, PlatformRtMechanism, +//! FFixedHW), so `_REG` announces none of them. +//! +//! **A table the XSDT lists is found by its three identifiers** +//! ([`Host::find_table`]) through the kernel's reads, and answered only where +//! it lies in memory Table 15.6 calls AddressRangeReserved or +//! AddressRangeNVS, as `LoadTable` and a Data Table Region require. //! //! **The embedded controller's space is its own transactions'** ([`crate::ec`], -//! [`transact`]), a byte each, on the controller the server attached -//! ([`Firmware::ec`]) and through the kernel, which no row holds its ports -//! for. With none attached the space is denied by name. A port of it the -//! kernel refuses detaches the controller and is handed to the server -//! ([`Firmware::ended`]), which serves it no more. +//! [`Firmware::transaction`]), a byte each, on the controller the server +//! attached ([`Firmware::ec`]) and through the kernel, which no row holds its +//! ports for, each under the Global Lock where the controller's `_GLK` asks +//! for it (ACPI 6.5 §6.5.7) and AML does not hold it already. A region is +//! available while its handler is (§6.5.4: "When an operation region +//! handler is unavailable, AML cannot access data fields in that region"), +//! and `_REG` only tells AML so: one attached is reached whether or not its +//! `_REG` has run, and with none attached the space is denied by name where +//! §6.5.4 has writes ignored and reads indeterminate. A port of it the kernel refuses +//! detaches the controller and is handed to the server ([`Firmware::ended`]), +//! which serves it no more. //! //! **The Global Lock's word is the kernel's to exchange** (ACPI 6.5 //! §5.2.10.1). A take that finds the firmware holding it leaves the firmware @@ -33,7 +56,22 @@ //! **A Notify of 0x80 to a control-method power button is a press** (ACPI //! 6.5 §4.8.2.2.1.2), counted in [`Firmware::presses`] for the server to //! serve, once [`Firmware::buttons`] names the button; every other Notify is -//! said the first time and served by nothing. +//! kept, once a value and an object, for the server to deliver after the +//! evaluation that made it ([`Firmware::notified`]). +//! +//! **An evaluation's Sleep serves the SCI to its deadline** +//! ([`Kernel::sleep`], `sci::await_sleep`): a press of the fixed button and +//! the controller's GPE are taken as they latch and kept +//! ([`Firmware::latched`]) for the server to serve after the evaluation, so a +//! press during a table's load is taken then, and powers the machine off once +//! the load has handed `\_S5` over. +//! +//! **A definition block AML loads at run time is read through the kernel +//! only from memory Table 15.6 calls AddressRangeReserved or +//! AddressRangeNVS** ([`Host::table`], ACPI 6.5 §19.6.76), each read's type +//! the kernel's answer; every load is said, its signature and length on a +//! line anyone may quote, its OEM Table ID and the method that loaded it +//! under [`OWN`]. //! //! **The power-off's sleep type is the one thing handed to the kernel** //! ([`Kernel::s5`]): the kernel writes the register, and reads no AML. @@ -49,15 +87,14 @@ use std::fmt; use std::time::{Duration, Instant}; use toyos_abi::acpi::{pci_address, Access, Refused, Space, Width, UNLISTED}; -use toyos_aml::{Address, Denied, Host}; +use toyos_aml::{Address, Denied, Host, RuntimeLoad, Transfer}; use crate::devices::Ec; use crate::ec::{Do, Transaction, Wait}; use crate::ledger::Ledger; +use crate::sci::Latched; -/// 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) "; +pub use acpiserver_api::OWN; /// 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, @@ -68,17 +105,6 @@ pub const RELEASE: Duration = Duration::from_secs(1); /// has not moved in this will not. const EC_STEP: Duration = Duration::from_millis(500); -/// The port writes asked of the kernel, by port and width: the index -/// registers of the CMOS's upper bank and of the AMD FCH's power-management -/// block, a byte each, and the POST port's word. No other: a port write this -/// server has no account of can be a reset (0x92, 0xCF9), a call into the -/// firmware (`SMI_CMD`) or a data register's. -pub const FORWARDED: [(u16, toyos_aml::Access); 3] = - [(0x72, toyos_aml::Access::Byte), (0x80, toyos_aml::Access::Word), (0xCD6, toyos_aml::Access::Byte)]; - -/// The denial of a port write not in [`FORWARDED`]. -pub const UNFORWARDED: &str = "a SystemIO write of a port and width this server asks of the kernel for no AML"; - /// The kernel does nothing more for this claim: the machine is stopping. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Stopping; @@ -101,7 +127,8 @@ pub enum Take { Unusable, } -/// The `acpi` claim's mediated access, as this server asks for it. +/// The `acpi` claim as this server asks it, its mediated access and its SCI, +/// and the power-off it asks of the supervisor. pub trait Kernel { fn access(&self, access: Access) -> Result; fn lock_take(&self) -> Result; @@ -114,6 +141,18 @@ pub trait Kernel { /// once under a claim: `false` where it is wider than the register's /// field, and the kernel kept nothing. fn s5(&self, slp_typ_a: u64) -> Result; + /// Waits until `until`, serving the SCI as `sci::await_sleep` does for + /// the fixed power button where the claim serves it and the controller's + /// GPE `ec_gpe`: what latched and was taken. + fn sleep(&self, until: Instant, ec_gpe: Option) -> Latched; + /// The SCIs the claim latched since its last acknowledgement, without + /// waiting: how many, or `None` where none is. + fn latched(&self) -> Option; + /// Acknowledge the SCI, whose status this server has cleared. + fn ack(&self); + /// Ask `/system/bin/supervisor` to power the machine off + /// (`toyos::power::stop`), which answers only where it did not. + fn power_off(&self) -> toyos::power::Refused; } /// Why an access to one of the controller's ports was not made. @@ -182,6 +221,13 @@ fn space_name(space: Space) -> &'static str { } } +/// Whether memory of a UEFI type is AddressRangeReserved or AddressRangeNVS +/// by Table 15.6: types 0, 5, 6, 8, 11, 12, 13 and 15 up, and 10. An address +/// firmware's map does not list is neither. +fn reserved_or_nvs(memory_type: u8) -> bool { + memory_type != UNLISTED && matches!(memory_type, 0 | 5 | 6 | 8 | 10..=13 | 15..) +} + /// What memory of a UEFI type is called: an address firmware's map does not /// list has no type, and a name of its own. fn type_name(memory_type: u8) -> String { @@ -248,6 +294,10 @@ pub struct Firmware<'k, K> { /// and gives back. pub takes: u64, pub contended: u64, + /// The longest a take waited for the firmware's release, or waited and + /// gave up: the firmware's longest hold of the lock, at least, as far as a + /// take saw it. + pub longest_hold: Duration, pub given_back: u64, pub notifies: u64, /// The controller whose space AML reaches, once the server attached it. @@ -256,15 +306,28 @@ pub struct Firmware<'k, K> { /// for the server to serve it no more. pub ended: Option, pub refused: Ledger, - notified: Ledger, - /// The [`FORWARDED`] writes asked of the kernel, by port and width. + /// The presses said, by button. + said: Ledger, + /// The port writes asked of the kernel, by port and width. written: 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. + pub latched: Latched, + /// Each Notify but a press, once a value and an object, for the server + /// to deliver. + pub notified: std::collections::BTreeSet<(String, u64)>, /// Presses of one of them notified and not yet served. pub presses: u64, /// The kernel answered that the machine is stopping. pub stopping: bool, + /// AML holds the Global Lock, which a transaction it makes runs under. + locked: bool, + /// Definition blocks loaded at run time, and refused. + pub loaded: u64, + pub loads_refused: u64, + /// The RSDP the tables were found from, which `LoadTable` searches again. + pub rsdp: u64, } impl<'k, K: Kernel> Firmware<'k, K> { @@ -276,19 +339,48 @@ impl<'k, K: Kernel> Firmware<'k, K> { pages: Pages::default(), takes: 0, contended: 0, + longest_hold: Duration::ZERO, given_back: 0, notifies: 0, ec: None, ended: None, refused: Ledger::default(), - notified: Ledger::default(), + said: Ledger::default(), written: Ledger::default(), buttons: Vec::new(), + latched: Latched::default(), + notified: Default::default(), presses: 0, stopping: false, + locked: false, + loaded: 0, + loads_refused: 0, + rsdp: 0, } } + /// One transaction on the controller at `command` and `data`, under the + /// Global Lock where its `_GLK` asks for it (`glk`) and AML does not hold + /// it already. + pub fn transaction(&mut self, command: u16, data: u16, glk: bool, tx: Transaction) -> Result { + let lock = glk && !self.locked; + if lock { + match self.global_take() { + Ok(()) => {} + Err(_) if self.stopping => return Err(Unmade::Stopping), + Err(Denied(why)) => return Err(Unmade::Refused(why)), + } + } + let made = transact(self.kernel, command, data, tx); + if lock { + match self.global_release() { + Ok(()) => {} + Err(_) => return Err(Unmade::Stopping), + } + } + made + } + /// Deny by the name `what`, said the first time with `own`, the machine's /// own detail of it. fn deny(&mut self, what: String, own: fmt::Arguments) -> Denied { @@ -299,6 +391,37 @@ impl<'k, K: Kernel> Firmware<'k, K> { Denied(what) } + /// A write the kernel makes or refuses, a refusal denied under its name + /// and, for memory, the type of what it was refused in. + 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: Err(refused), memory_type }) => { + let what = match space { + Space::SystemMemory => format!("a SystemMemory write the kernel refused {refused:?}, in {}", type_name(memory_type)), + space => format!("a {} write the kernel refused {refused:?}", space_name(space)), + }; + Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) + } + } + } + + /// The denial of an access to a space this server serves no AML in. + fn unserved(&mut self, at: Address, width: toyos_aml::Access) -> Denied { + let space = match at { + Address::Memory(_) => "SystemMemory", + Address::Io(_) => "SystemIO", + Address::PciConfig { .. } => "PCI_Config", + Address::EmbeddedControl(_) => "EmbeddedControl", + Address::SystemCmos(_) => "SystemCMOS", + Address::PciBarTarget { .. } => "PciBarTarget", + Address::Pcc { .. } => "PCC", + Address::Oem { .. } => "an OEM-defined space", + }; + self.deny(format!("an access to {space}, which this server serves no AML in"), format_args!("{width:?} at {at:x?}")) + } + fn stopped(&mut self) -> Denied { self.stopping = true; Denied(Refusal::Stopping.to_string()) @@ -309,10 +432,10 @@ impl<'k, K: Kernel> Firmware<'k, K> { /// one of its ports, which detaches it. fn controller(&mut self, what: &str, at: Address, width: toyos_aml::Access, tx: Transaction) -> Result { assert_eq!(width, toyos_aml::Access::Byte, "the interpreter reaches EmbeddedControl a byte at a time (Table 19.34)"); - let Some(ec) = &self.ec else { + let Some((command, data, glk)) = self.ec.as_ref().map(|ec| (ec.command, ec.data, ec.glk)) else { return Err(self.deny(format!("{what} {NO_CONTROLLER}"), format_args!("{at:x?}"))); }; - match transact(self.kernel, ec.command, ec.data, tx) { + match self.transaction(command, data, glk, tx) { Ok(byte) => Ok(byte), Err(Unmade::Stopping) => Err(self.stopped()), Err(Unmade::Refused(why)) => { @@ -330,10 +453,16 @@ impl<'k, K: Kernel> Firmware<'k, K> { fn take(&mut self, until: Instant) -> Result { self.takes += 1; let mut contended = false; + let asked = Instant::now(); loop { match self.kernel.lock_take() { Err(Stopping) => return Err(self.stopped()), - Ok(Take::Taken) => return Ok(true), + Ok(Take::Taken) => { + if contended { + self.longest_hold = self.longest_hold.max(asked.elapsed()); + } + return Ok(true); + } Ok(Take::Unusable) => { let what = "the Global Lock: the kernel exchanges no lock word in this machine's FACS"; return Err(self.deny(what.into(), format_args!("a take"))); @@ -345,6 +474,7 @@ impl<'k, K: Kernel> Firmware<'k, K> { self.contended += 1; } if !self.kernel.released(until) { + self.longest_hold = self.longest_hold.max(asked.elapsed()); return Ok(false); } } @@ -369,6 +499,7 @@ impl Host for Firmware<'_, K> { Address::Io(port) => (Space::SystemIo, u64::from(port)), Address::PciConfig { segment, bus, device, function, offset } => (Space::PciConfig, pci_address(segment, bus, device, function, offset)), Address::EmbeddedControl(address) => return self.controller("a read of", at, width, Transaction::read_at(address)).map(u64::from), + other => return Err(self.unserved(other, width)), }; match read(self.kernel, space, address, wide(width)) { Ok((value, memory_type)) => { @@ -384,35 +515,32 @@ impl Host for Firmware<'_, K> { } fn write(&mut self, at: Address, width: toyos_aml::Access, value: u64) -> Result<(), Denied> { - let what = match at { - Address::Io(port) if !FORWARDED.contains(&(port, width)) => UNFORWARDED.into(), + match at { Address::Io(port) => { if self.written.see(&format!("{port:#x} {width:?}")) { - println!("acpiserver: a SystemIO write of {port:#x}, {width:?}, asked of the kernel for the first time"); + println!("{OWN}a SystemIO write of {port:#x}, {width:?}, asked of the kernel for the first time"); } - return match self.kernel.access(Access::write(Space::SystemIo, u64::from(port), wide(width), value)) { - Err(Stopping) => Err(self.stopped()), - Ok(Answer { made: Ok(_), .. }) => Ok(()), - Ok(Answer { made: Err(refused), .. }) => Err(self.deny( - format!("a SystemIO write the kernel refused {refused:?}"), - format_args!("{width:?} {value:#x} to {at:x?}"), - )), - }; + 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::Memory(_) => "a write to SystemMemory: this server writes no memory for AML".into(), - Address::PciConfig { .. } => "a write to PCI_Config: this server writes no configuration space for AML".into(), Address::EmbeddedControl(address) => { let byte = u8::try_from(value).expect("a byte access writes a byte"); - return self.controller("a write to", at, width, Transaction::write_at(address, byte)).map(drop); + self.controller("a write to", at, width, Transaction::write_at(address, byte)).map(drop) } - }; - Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) + other => Err(self.unserved(other, width)), + } } fn sleep(&mut self, ms: u64) { // The firmware's own delay (§19.6.125), bounded by the interpreter in - // what one evaluation may ask for. - std::thread::sleep(Duration::from_millis(ms)); + // what one evaluation may ask for, the SCI served through it. + let latched = self.kernel.sleep(Instant::now() + Duration::from_millis(ms), self.ec.as_ref().map(|ec| ec.gpe)); + self.latched.presses += latched.presses; + self.latched.ec |= latched.ec; } fn stall(&mut self, us: u64) { @@ -429,32 +557,116 @@ impl Host for Firmware<'_, K> { fn notify(&mut self, object: &str, value: u64) { self.notifies += 1; // §5.6.6's notification values of a power button: 0x80 is a press in S0. - let press = value == 0x80 && self.buttons.iter().any(|button| button == object); - if press { + if value == 0x80 && self.buttons.iter().any(|button| button == object) { self.presses += 1; + if self.said.see(object) { + println!("{OWN}Notify({object}, {value:#x}) for the first time: a press of the power button"); + } + return; } - if self.notified.see(&format!("{object} {value:#x}")) { - let served = if press { "a press of the power button" } else { "served by nothing" }; - println!("{OWN}Notify({object}, {value:#x}) for the first time: {served}"); - } + self.notified.insert((object.to_owned(), value)); } fn global_take(&mut self) -> Result<(), Denied> { if self.take(Instant::now() + RELEASE)? { + self.locked = true; return Ok(()); } panic!("acpiserver: the firmware held the Global Lock {RELEASE:?} after a take left it the pending bit, and raised no GBL_STS"); } fn global_take_within(&mut self, ms: u16) -> Result { - self.take(Instant::now() + Duration::from_millis(ms.into())) + let taken = self.take(Instant::now() + Duration::from_millis(ms.into()))?; + self.locked = taken; + Ok(taken) } fn global_release(&mut self) -> Result<(), Denied> { + self.locked = false; self.kernel.lock_release().map_err(|Stopping| self.stopped())?; self.given_back += 1; Ok(()) } + + fn table(&mut self, at: u64, len: usize) -> Result, Denied> { + let mut bytes = Vec::with_capacity(len); + while bytes.len() < len { + let width = if len - bytes.len() >= 8 { Width::QWord } else { Width::Byte }; + let Some(address) = at.checked_add(bytes.len() as u64) else { + return Err(self.deny("a definition block past the end of memory".into(), format_args!("{at:#x}+{len:#x}"))); + }; + match read(self.kernel, Space::SystemMemory, address, width) { + Ok((value, memory_type)) if reserved_or_nvs(memory_type) => { + self.reads[Space::SystemMemory as usize] += 1; + self.pages.read(address, memory_type); + bytes.extend_from_slice(&value.to_le_bytes()[..width.bytes() as usize]); + } + Ok((_, memory_type)) => { + let what = format!("a definition block in {}, which Table 15.6 calls neither AddressRangeReserved nor AddressRangeNVS", type_name(memory_type)); + return Err(self.deny(what, format_args!("{at:#x}+{len:#x}"))); + } + Err(Refusal::Stopping) => return Err(self.stopped()), + Err(refusal) => return Err(self.deny(refusal.to_string(), format_args!("a definition block at {at:#x}+{len:#x}"))), + } + } + Ok(bytes) + } + + fn transfer(&mut self, at: &Transfer<'_>, _: Option<&[u8]>) -> Result, Denied> { + let space = match at.space { + 0x04 => "SMBus", + 0x07 => "IPMI", + 0x08 => "GeneralPurposeIO", + 0x09 => "GenericSerialBus", + 0x0B => "PlatformRtMechanism", + _ => "FFixedHW", + }; + Err(self.deny(format!("an access to {space}, which this server serves no AML in"), format_args!("{at:x?}"))) + } + + fn find_table(&mut self, signature: &[u8], oem_id: &[u8], oem_table_id: &[u8]) -> Result, Denied> { + let tables = crate::tables::Tables::new(self.kernel); + let found = toyos_acpi::find_by_id(&tables, self.rsdp, signature, oem_id, oem_table_id); + if tables.stopping.get() { + return Err(self.stopped()); + } + let ids = format_args!("{:?} {:?} {:?}", signature.escape_ascii().to_string(), oem_id.escape_ascii().to_string(), oem_table_id.escape_ascii().to_string()); + let table = match found { + Ok(Some(table)) => table, + Ok(None) => return Ok(None), + Err(why) => return Err(self.deny("a table LoadTable or a Data Table Region names: the XSDT was not read".into(), format_args!("{ids}: {why:x?}"))), + }; + match read(self.kernel, Space::SystemMemory, table.base(), Width::Byte) { + Ok((_, memory_type)) if reserved_or_nvs(memory_type) => Ok(Some((table.base(), table.len()))), + Ok((_, memory_type)) => { + let what = format!("a table LoadTable or a Data Table Region names, in {}, which Table 15.6 calls neither AddressRangeReserved nor AddressRangeNVS", type_name(memory_type)); + Err(self.deny(what, format_args!("{ids}"))) + } + Err(Refusal::Stopping) => Err(self.stopped()), + Err(refusal) => Err(self.deny(refusal.to_string(), format_args!("a table {ids}"))), + } + } + + fn loaded(&mut self, load: &RuntimeLoad<'_>) { + let header = load.header.map(|header| { + let text = |bytes: &[u8]| bytes.escape_ascii().to_string(); + (text(&header[..4]), u32::from_le_bytes(header[4..8].try_into().expect("four bytes")), text(&header[16..24])) + }); + let said = header.as_ref().map_or("its header unread".into(), |(signature, length, _)| format!("{signature}, {length} bytes")); + let oem = header.as_ref().map_or(String::new(), |(_, _, oem)| format!(", OEM Table ID {oem:?}")); + match load.refused { + None => { + self.loaded += 1; + println!("acpiserver: a table loaded at run time: {said}"); + println!("{OWN}{} loaded it{oem}", load.by); + } + Some(why) => { + self.loads_refused += 1; + println!("acpiserver: a table loaded at run time was refused: {said}: {}", crate::aml::kind(why)); + println!("{OWN}{} loaded it{oem}, refused {why:x?}", load.by); + } + } + } } #[cfg(test)] @@ -471,13 +683,18 @@ pub mod tests { /// A kernel that answers reads from bytes at addresses, each range with a /// memory type, refuses `kept` ranges as the real one refuses memory of a - /// type it passes no read of and every other address as it refuses RAM, - /// and answers the Global Lock from a script. + /// 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. #[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)>, + /// `(start, end)` of what the kernel drives, refused as device memory. + pub driven: Vec<(u64, u64)>, pub ports: Vec<(u64, u64)>, pub config: Vec<(u64, u64)>, pub asked: RefCell>, @@ -491,12 +708,24 @@ pub mod tests { /// A controller at [`EC_DATA`] and [`EC_COMMAND`], answering its two /// ports before [`Self::ports`] is asked. pub controller: RefCell>, + /// The controller is reached only under the Global Lock, as a `_GLK` + /// of 1 asks: a write to either port or a read of its data, which + /// takes the byte, made without it panics. + pub lock_rule: bool, /// Accesses and lock exchanges answered before the machine stops. pub stops_after: Cell>, /// The `SLP_TYPa` it was handed and kept. pub handed: Cell>, + /// What each Sleep's wait takes, in turn; nothing once it runs out. + /// Each served set a Sleep was asked to serve. + pub sleeps: RefCell>, + pub slept: RefCell>>, } + /// What [`Scripted`] panics with where it is asked to power off: the + /// machine stops there, and nothing after the ask runs. + pub const POWERED_OFF: &str = "the scripted machine powered off"; + impl Scripted { fn stopping(&self) -> Result<(), Stopping> { match self.stops_after.get() { @@ -514,7 +743,7 @@ pub mod tests { fn access(&self, access: Access) -> Result { self.stopping()?; self.asked.borrow_mut().push(access); - assert!(access.write == 0 || access.space == Space::SystemIo as u8, "this server asks the kernel for no write but a port's"); + 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 listed = |values: &[(u64, u64)]| values.iter().find(|(at, _)| *at == access.address).map(|&(_, value)| value); Ok(match Space::from_raw(access.space).expect("a space") { @@ -523,11 +752,26 @@ pub mod tests { access.address >= *base && access.address + width <= base + bytes.len() as u64 }); match held { + // 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 => { + let mut written = self.written.borrow_mut(); + for i in 0..width { + written.insert(access.address + i, (access.value >> (8 * i)) as u8); + } + Answer { made: Ok(0), memory_type: *memory_type } + } Some((base, memory_type, bytes)) => { - let from = (access.address - base) as usize; - let value = bytes[from..from + width as usize].iter().rev().fold(0u64, |value, &byte| value << 8 | u64::from(byte)); + let written = self.written.borrow(); + let value = (0..width).rev().fold(0u64, |value, i| { + let at = access.address + i; + value << 8 | u64::from(written.get(&at).copied().unwrap_or(bytes[(at - base) as usize])) + }); Answer { made: Ok(value), memory_type: *memory_type } } + 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)) { Some(&(.., memory_type)) => Answer { made: Err(Refused::MemoryType), memory_type }, None => Answer { made: Err(Refused::UsableMemory), memory_type: 7 }, @@ -535,6 +779,9 @@ pub mod tests { } } Space::SystemIo if self.controller.borrow().is_some() && [EC_DATA, EC_COMMAND].contains(&access.address) => { + if access.write != 0 || access.address == EC_DATA { + assert!(!self.lock_rule || self.held.get(), "a transaction on a controller whose _GLK asks for the Global Lock, made without it"); + } let mut controller = self.controller.borrow_mut(); let ec = controller.as_mut().expect("just seen"); let read = match (access.address, access.write != 0) { @@ -577,6 +824,11 @@ pub mod tests { self.releases.borrow_mut().pop_front().unwrap_or(false) } + fn sleep(&self, _: Instant, ec_gpe: Option) -> Latched { + self.slept.borrow_mut().push(ec_gpe); + self.sleeps.borrow_mut().pop_front().unwrap_or_default() + } + fn s5(&self, slp_typ_a: u64) -> Result { self.stopping()?; assert!(!self.held.get(), "the sleep type was handed over under the Global Lock"); @@ -587,6 +839,18 @@ pub mod tests { assert_eq!(self.handed.replace(Some(slp_typ_a)), None, "a second sleep type under one claim, which the kernel refuses"); Ok(true) } + + fn latched(&self) -> Option { + None + } + + fn ack(&self) { + panic!("an SCI acknowledged where none latched"); + } + + fn power_off(&self) -> toyos::power::Refused { + panic!("{POWERED_OFF}"); + } } const NVS: u64 = 0x7700_0000; @@ -654,49 +918,120 @@ pub mod tests { assert_eq!(host.pages.by_type(), "none"); } - /// A port write in [`FORWARDED`] is asked of the kernel, and made or - /// refused as it answers; every other port and width, memory and - /// configuration space are denied by name, and so is the controller's - /// space with no controller attached, none of them asked of the kernel. + /// 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. #[test] - fn only_a_forwarded_port_write_is_asked_of_the_kernel() { - use toyos_aml::Access::{Byte, DWord, Word}; + fn every_port_write_is_asked_of_the_kernel_and_made_or_refused_as_it_answers() { + use toyos_aml::Access::{Byte, Word}; let mut kernel = machine(); - kernel.ports = vec![(0x72, 0), (0x80, 0)]; + kernel.ports = vec![(0xC00, 0), (0xC01, 0), (0x80, 0), (0xB2, 0)]; let mut host = Firmware::new(&kernel); - assert_eq!(host.write(Address::Io(0x80), Word, 0x28), Ok(())); - assert_eq!(host.write(Address::Io(0x72), Byte, 0x08), Ok(())); - let refused = host.write(Address::Io(0xCD6), Byte, 0x60); - assert_eq!(refused, Err(Denied("a SystemIO write the kernel refused KernelPort".into()))); + let made = [(0xC00, Byte, 0x02), (0xC01, Byte, 0x0B), (0x80, Word, 0x28), (0xB2, Byte, 0xA0)]; + for (port, width, value) in made { + assert_eq!(host.write(Address::Io(port), width, value), Ok(()), "{port:#x} {width:?}"); + } + let kept = host.write(Address::Io(0xCF9), Byte, 0x06); + assert_eq!(kept, Err(Denied("a SystemIO write the kernel refused KernelPort".into()))); assert_eq!( *kernel.asked.borrow(), [ + Access::write(Space::SystemIo, 0xC00, Width::Byte, 0x02), + Access::write(Space::SystemIo, 0xC01, Width::Byte, 0x0B), Access::write(Space::SystemIo, 0x80, Width::Word, 0x28), - Access::write(Space::SystemIo, 0x72, Width::Byte, 0x08), - Access::write(Space::SystemIo, 0xCD6, Width::Byte, 0x60), + Access::write(Space::SystemIo, 0xB2, Width::Byte, 0xA0), + Access::write(Space::SystemIo, 0xCF9, Width::Byte, 0x06), ] ); - // Reset-capable ports with the values that reset a machine, the - // reset register an AMD laptop shares with `SMI_CMD`, a data - // register, and a forwarded port at a width it is not forwarded at. - for (port, width, value) in - [(0xCF9, Byte, 0x06), (0xCF9, Byte, 0x0E), (0x92, Byte, 0x01), (0xB0, Byte, 0xFB), (0xB2, Byte, 0xA0), (0x73, Byte, 0), (0xCD7, Byte, 0), (0x72, Word, 0), (0x80, Byte, 0), (0x80, DWord, 0)] - { - assert_eq!(host.write(Address::Io(port), width, value), Err(Denied(UNFORWARDED.into())), "{port:#x} {width:?}"); - } let function = Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 3, offset: 0x40 }; - for (at, denied) in [ - (Address::Memory(NVS), "a write to SystemMemory: this server writes no memory for AML"), - (function, "a write to PCI_Config: this server writes no configuration space for AML"), - ] { - assert_eq!(host.write(at, Byte, 0), Err(Denied(denied.into()))); - } + 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()))); 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(), 3, "the kernel was asked for an access this server denies itself"); + assert_eq!(kernel.asked.borrow().len(), 5, "the kernel was asked for an access this server denies itself"); assert_eq!(host.reads, [0, 0, 0]); - assert_eq!(host.written.counts(), "0x72 Byte x1; 0x80 Word x1; 0xcd6 Byte x1"); + 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. + #[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)]; + 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)); + assert_eq!(host.write(Address::Memory(NVS + 0x2000), Byte, 1), Ok(())); + 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"; + for _ in 0..2 { + assert_eq!(host.write(Address::Memory(0xfe00_0010), DWord, 0), Err(Denied(unlisted.into()))); + } + assert_eq!(host.refused.counts(), format!("{unlisted} x2; {tables} x1")); + } + + /// Every space of Table 5.182 this server serves no AML in is denied by + /// name and asks the kernel nothing: SystemCMOS, PciBarTarget, PCC and an + /// OEM-defined space by address, and each carried whole. + #[test] + fn a_space_this_server_serves_no_aml_in_is_denied_by_name() { + use toyos_aml::Access::Byte; + let kernel = machine(); + let mut host = Firmware::new(&kernel); + for (at, space) in [ + (Address::SystemCmos(0x40), "SystemCMOS"), + (Address::PciBarTarget { segment: 0, bus: 0, device: 2, function: 0, bar: 0x10, offset: 0 }, "PciBarTarget"), + (Address::Pcc { subspace: 2, offset: 4 }, "PCC"), + (Address::Oem { space: 0x80, offset: 0 }, "an OEM-defined space"), + ] { + let denied = Err(Denied(format!("an access to {space}, which this server serves no AML in"))); + assert_eq!(host.read(at, Byte).map(drop), denied.clone()); + assert_eq!(host.write(at, Byte, 0), denied); + } + for (space, name) in [(0x04, "SMBus"), (0x07, "IPMI"), (0x08, "GeneralPurposeIO"), (0x09, "GenericSerialBus"), (0x0B, "PlatformRtMechanism"), (0x7F, "FFixedHW")] { + let at = Transfer { space, region: 0, bit: 0, bits: 8, attrib: 0, length: 0, connection: None }; + assert_eq!(host.transfer(&at, None), Err(Denied(format!("an access to {name}, which this server serves no AML in")))); + } + assert!(kernel.asked.borrow().is_empty()); + } + + /// LoadTable's and a Data Table Region's table is found in the XSDT by + /// its three identifiers and answered only in memory Table 15.6 calls + /// reserved or NVS: QEMU's tables are ACPI reclaim memory, and denied. + #[test] + fn a_table_is_found_by_its_identifiers_only_in_reserved_or_nvs_memory() { + use crate::aml::tests::{crafted_with, sealed, CRAFTED_RSDP}; + let mut oem = sealed(b"OEM1", &[0; 4]); + oem[10..16].copy_from_slice(b"TOYOS "); + oem[16..24].copy_from_slice(b"TABLE1 "); + oem[9] = 0; + oem[9] = 0u8.wrapping_sub(oem.iter().fold(0u8, |sum, &b| sum.wrapping_add(b))); + let mut kernel = crafted_with(&sealed(b"DSDT", &[]), &[], &[&oem]); + let listed = kernel.memory.iter().position(|(_, _, bytes)| bytes.starts_with(b"OEM1")).expect("the table"); + let at = kernel.memory[listed].0; + for (memory_type, found) in [(0, true), (10, true), (6, true), (9, false)] { + kernel.memory[listed].1 = memory_type; + let mut host = Firmware::new(&kernel); + host.rsdp = CRAFTED_RSDP; + let answer = host.find_table(b"OEM1", b"TOYOS", b"TABLE1"); + match found { + true => assert_eq!(answer, Ok(Some((at, oem.len()))), "type {memory_type}"), + false => assert_eq!(answer, Err(Denied("a table LoadTable or a Data Table Region names, in memory of type 9, which Table 15.6 calls neither AddressRangeReserved nor AddressRangeNVS".into()))), + } + assert_eq!(host.find_table(b"OEM1", b"TOYOS", b"TABLE2"), Ok(None)); + } } /// The controller's space is its own transactions', one a byte, through @@ -710,7 +1045,7 @@ pub mod tests { space[0xA8] = 0x5A; *kernel.controller.borrow_mut() = Some(Emulated::new(space)); let mut host = Firmware::new(&kernel); - host.ec = Some(Ec { path: "\\_SB_.EC0_".into(), command: EC_COMMAND as u16, data: EC_DATA as u16, gpe: 3, queries: Default::default() }); + host.ec = Some(Ec { path: "\\_SB_.EC0_".into(), command: EC_COMMAND as u16, data: EC_DATA as u16, gpe: 3, queries: Default::default(), glk: false }); assert_eq!(host.read(Address::EmbeddedControl(0xA8), Byte), Ok(0x5A)); assert_eq!(host.write(Address::EmbeddedControl(0x81), Byte, 0x01), Ok(())); assert_eq!(host.read(Address::EmbeddedControl(0x81), Byte), Ok(0x01)); @@ -730,7 +1065,7 @@ pub mod tests { use toyos_aml::Access::Byte; let kernel = machine(); let mut host = Firmware::new(&kernel); - let ec = Ec { path: "\\_SB_.EC0_".into(), command: EC_COMMAND as u16, data: EC_DATA as u16, gpe: 3, queries: Default::default() }; + let ec = Ec { path: "\\_SB_.EC0_".into(), command: EC_COMMAND as u16, data: EC_DATA as u16, gpe: 3, queries: Default::default(), glk: false }; host.ec = Some(ec.clone()); let refused = host.read(Address::EmbeddedControl(0xA8), Byte); assert_eq!(refused, Err(Denied("a read of EmbeddedControl: the kernel refused a port of the controller".into()))); diff --git a/system/acpiserver/src/init.rs b/system/acpiserver/src/init.rs new file mode 100644 index 00000000000..72750de0744 --- /dev/null +++ b/system/acpiserver/src/init.rs @@ -0,0 +1,487 @@ +//! The namespace initialised (ACPI 6.5 §6.5.1, §6.5.4): what OSPM runs of a +//! machine's AML once its tables are loaded, and again over what a table +//! loaded at run time added. +//! +//! **`_REG(2, 1)` first**, in each scope that declares a PCI_Config region +//! not on a root bus naming a `_BBN`: §6.5.4 makes those regions available +//! once the bus is configured, and the kernel reads configuration space from +//! before this server runs, so they are announced before any `_INI` reaches +//! them. A region on a root bus naming a `_BBN` is always available, and its +//! scope's `_REG` is not run. Finding the host bridge of a root bus that +//! names none evaluates its candidates' `_HID` and `_CID`, as the +//! interpreter's own access to such a region does. +//! +//! **Then `\_SB._INI`, unconditionally** ("at the beginning of namespace +//! initialization"), **then each device's `_STA` and `_INI` by Table 6.67, +//! where an `_INI` is defined at or below it**, parent before child: a device +//! its `_STA` says is present has its `_INI` run and its children examined; +//! one functional and not present has its children examined alone; one +//! neither, nothing below it. A device with no `_STA` is both. A Processor +//! object is a device here. After a run-time load, the `_INI`s that load +//! defined are run, by the same table, their ancestors' `_STA` asked again. +//! +//! **The walk looks for `_INI` methods** (§6.5.1: OSPM "will examine each of +//! the children of the device for \_INI methods", and evaluates `_STA` +//! "before evaluating the \_INI object"): a device with no `_INI` at or +//! below it is not examined, and its `_STA` is asked by what serves it, a +//! battery's once the controller is attached ([`crate::battery`]). **A +//! `_STA` refused, or no Integer, says nothing**, of which §6.5.1 is silent: +//! by ToyOS's rule its device's `_INI` is not run, since nothing said it is +//! present, and its children are examined, as Table 6.67's row that runs +//! nothing of a device yet closes nothing below it has them. +//! +//! **EmbeddedControl is announced in each scope that declares a region of +//! it once the controller is attached** ([`announce`], `crate::devices`), +//! after the walk. SystemMemory and SystemIO are always available and +//! announced to nobody, and no other space is served. +//! +//! An evaluation refused is said the first time by kind, its path under +//! [`OWN`], and counted; each device's `_STA` is said under [`OWN`] too, the +//! record a reading of the same machine under another OS is held to. + +use std::collections::{BTreeMap, BTreeSet}; + +use toyos_aml::{Interpreter, Kind, Value}; + +use crate::aml::{kind, Aml}; +use crate::host::{Kernel, OWN}; +use crate::ledger::Ledger; + +/// One object of the namespace, as one walk read it. +pub struct Named { + pub path: String, + pub depth: u32, + pub serial: u32, + pub kind: Kind, +} + +/// The namespace read once, before anything is evaluated in it: every object +/// in walk order, and every path. +pub struct Names { + pub objects: Vec, + pub paths: BTreeSet, +} + +impl Names { + pub fn read(interpreter: &Interpreter) -> Result { + let mut walk = interpreter.walk().map_err(|why| kind(&why))?; + let (mut objects, mut paths) = (Vec::new(), BTreeSet::new()); + while let Some(entry) = walk.next() { + let path = walk.path().to_string(); + paths.insert(path.clone()); + objects.push(Named { path, depth: entry.depth, serial: entry.serial, kind: entry.kind }); + } + Ok(Names { objects, paths }) + } + + fn has(&self, scope: &str, name: &str) -> bool { + self.paths.contains(&format!("{scope}.{name}")) + } + + fn devices(&self) -> BTreeSet<&str> { + self.objects.iter().filter(|named| is_device(named.kind)).map(|named| named.path.as_str()).collect() + } +} + +fn is_device(kind: Kind) -> bool { + matches!(kind, Kind::Device | Kind::Processor) +} + +/// The scope an object's path is in. +fn parent(path: &str) -> &str { + path.rsplit_once('.').map_or("\\", |(scope, _)| scope) +} + +/// `_STA`'s Present and Functional bits (§6.3.7). +const PRESENT: u64 = 1 << 0; +const FUNCTIONAL: u64 = 1 << 3; + +/// What the initialisation ran, and what of it was refused. +#[derive(Default)] +pub struct Census { + /// `\_SB._INI`: absent, run, or refused. + pub sb_ini: Option, + pub sta: u64, + pub ini: u64, + /// `_REG(space, 1)` runs, by space. + pub reg: BTreeMap, + /// Each refusal, by the method's name and its kind. + pub refused: Ledger, + /// The longest time an evaluation asked to wait, refused or not, in µs + /// (`toyos_aml::Usage::waited_us`). + pub longest_wait_us: u64, +} + +impl Census { + /// The line that says what ran, a count each. + pub fn line(&self) -> String { + let sb = match self.sb_ini { + None => "absent", + Some(true) => "run", + Some(false) => "refused", + }; + let reg: Vec = self.reg.iter().map(|(space, n)| format!("_REG({space}, 1) in {n} scope(s)")).collect(); + let refused = if self.refused.is_empty() { "none".to_string() } else { self.refused.counts() }; + format!( + "acpiserver: the namespace initialised: \\_SB._INI {sb}; {} _STA read, {} _INI run, {}; the longest wait an evaluation asked {} µs; evaluations refused, by kind: {refused}", + self.sta, + self.ini, + if reg.is_empty() { "no _REG run".to_string() } else { reg.join(", ") }, + self.longest_wait_us + ) + } + + fn refuse(&mut self, method: &str, path: &str, why: &toyos_aml::Error) { + let what = format!("{method}: {}", kind(why)); + if self.refused.see(&what) { + println!("acpiserver: an evaluation of the initialisation was refused for the first time: {what}"); + } + println!("{OWN}{path} was refused {why:x?}"); + } +} + +/// Evaluate `path`, counted in `census` under `method` where refused. +fn evaluate(aml: &mut Aml<'_, K>, census: &mut Census, method: &str, path: &str, args: &[Value]) -> Option { + let answer = aml.interpreter.evaluate(&mut aml.host, path, args); + census.longest_wait_us = census.longest_wait_us.max(aml.interpreter.usage().waited_us); + match answer { + Ok(value) => Some(value), + Err(_) if aml.host.stopping => None, + Err(why) => { + census.refuse(method, path, &why); + None + } + } +} + +/// Initialise the namespace the load kept: every scope's PCI_Config +/// announced, `\_SB._INI`, then the walk. +pub fn initialise(aml: &mut Aml<'_, K>, names: &Names, census: &mut Census) { + announce_config(aml, names, 0, census); + if names.paths.contains("\\_SB_._INI") { + census.sb_ini = Some(evaluate(aml, census, "\\_SB._INI", "\\_SB_._INI", &[]).is_some()); + } + walk(aml, names, 0, census); +} + +/// What a run-time load added since `since`: its PCI_Config regions +/// announced, its `_INI`s run by Table 6.67, and its EmbeddedControl regions +/// announced where the controller is attached. +pub fn loaded(aml: &mut Aml<'_, K>, names: &Names, since: u32, census: &mut Census) { + announce_config(aml, names, since, census); + walk(aml, names, since, census); + if aml.host.ec.is_some() { + announce(aml, names, EMBEDDED_CONTROL, since, census); + } +} + +/// The EmbeddedControl space's number (Table 5.182). +pub const EMBEDDED_CONTROL: u8 = 3; +const PCI_CONFIG: u8 = 2; + +/// Each scope that declares a region of `space` defined at `since` or after, +/// once, in walk order. +fn scopes(names: &Names, space: u8, since: u32) -> Vec<&str> { + let mut seen = BTreeSet::new(); + names + .objects + .iter() + .filter(|named| named.serial >= since && named.kind == Kind::OperationRegion { space }) + .map(|named| parent(&named.path)) + .filter(|scope| seen.insert(*scope)) + .collect() +} + +/// `_REG(space, 1)` in each scope that declares a region of `space` defined +/// at `since` or after and names a `_REG`. +pub fn announce(aml: &mut Aml<'_, K>, names: &Names, space: u8, since: u32, census: &mut Census) { + for scope in scopes(names, space, since) { + reg(aml, names, space, scope, census); + } +} + +fn reg(aml: &mut Aml<'_, K>, names: &Names, space: u8, scope: &str, census: &mut Census) { + if !names.has(scope, "_REG") || aml.host.stopping { + return; + } + if evaluate(aml, census, "_REG", &format!("{scope}._REG"), &[Value::Integer(space.into()), Value::Integer(1)]).is_some() { + *census.reg.entry(space).or_insert(0) += 1; + } +} + +/// `_REG(2, 1)` in each scope whose PCI_Config regions are not on a root bus +/// naming a `_BBN`, and below a host bridge. +fn announce_config(aml: &mut Aml<'_, K>, names: &Names, since: u32, census: &mut Census) { + let devices = names.devices(); + for scope in scopes(names, PCI_CONFIG, since) { + // The device the region addresses, and each one above it. + let above: Vec<&str> = std::iter::successors(Some(scope), |path| (*path != "\\").then(|| parent(path))) + .filter(|path| devices.contains(path)) + .collect(); + let Some(&device) = above.first() else { continue }; + let mut bridge = None; + for (at, candidate) in above.iter().enumerate() { + if names.has(candidate, "_BBN") { + bridge = Some((at, true)); + break; + } + if host_bridge(aml, names, candidate, census) { + bridge = Some((at, false)); + break; + } + } + match bridge { + // The host bridge itself, or a device on the bus right below it. + Some((at, true)) if at <= 1 => {} + Some(_) => reg(aml, names, PCI_CONFIG, scope, census), + None => println!("{OWN}{device} declares a PCI_Config region below no host bridge"), + } + } +} + +/// Whether `device` is a PCI host bridge by its `_HID` or a `_CID` (§6.1.5, +/// §6.1.2): PNP0A03 or PNP0A08. +fn host_bridge(aml: &mut Aml<'_, K>, names: &Names, device: &str, census: &mut Census) -> bool { + for id in ["_HID", "_CID"] { + if !names.has(device, id) { + continue; + } + let ids = match evaluate(aml, census, id, &format!("{device}.{id}"), &[]) { + Some(Value::Package(ids)) => ids, + Some(id) => vec![id], + None => continue, + }; + if ids.iter().filter_map(crate::devices::id).any(|id| id == "PNP0A03" || id == "PNP0A08") { + return true; + } + } + false +} + +/// Table 6.67 over the namespace: each device examined has its `_STA` read, +/// and each one present whose `_INI` was defined at `since` or after has it +/// run. A device at or below which no `_INI` was defined since is not +/// examined. +fn walk(aml: &mut Aml<'_, K>, names: &Names, since: u32, census: &mut Census) { + // The devices that own an `_INI` to run, and every scope above one. + let ini: BTreeSet<&str> = + names.objects.iter().filter(|named| named.serial >= since && named.path.ends_with("._INI") && named.kind == Kind::Method).map(|named| parent(&named.path)).collect(); + let wanted: BTreeSet<&str> = ini.iter().flat_map(|scope| std::iter::successors(Some(*scope), |path| (*path != "\\").then(|| parent(path)))).collect(); + // The depth below which nothing is examined, where a device said so. + let mut closed: Option = None; + for named in &names.objects { + if closed.is_some_and(|depth| named.depth > depth) { + continue; + } + closed = None; + if !is_device(named.kind) || !wanted.contains(named.path.as_str()) { + continue; + } + if aml.host.stopping { + return; + } + let status = match names.has(&named.path, "_STA") { + false => PRESENT | FUNCTIONAL, + true => { + let path = format!("{}._STA", named.path); + match evaluate(aml, census, "_STA", &path, &[]) { + Some(Value::Integer(status)) => { + census.sta += 1; + println!("{OWN}{path} {status:#x}"); + status + } + Some(_) => { + census.refuse("_STA", &path, &toyos_aml::Error::Type("a _STA that is no Integer (§6.3.7)")); + continue; + } + None => continue, + } + } + }; + if status & (PRESENT | FUNCTIONAL) == 0 { + closed = Some(named.depth); + continue; + } + if status & PRESENT != 0 && ini.contains(named.path.as_str()) { + let path = format!("{}._INI", named.path); + if evaluate(aml, census, "_INI", &path, &[]).is_some() { + census.ini += 1; + } + } + } +} + +/// The interpreter's own test encodings of §20.2. +#[cfg(test)] +#[path = "../aml/tests/common/mod.rs"] +mod common; + +#[cfg(test)] +mod tests { + use super::*; + use crate::aml::tests::{crafted, sealed, CRAFTED_RSDP}; + use crate::host::tests::Scripted; + use toyos_abi::acpi::Space; + + use super::common::{add, arg, cat, def_name, device, field, int, method, name, op_region, ret, scope, store, string, unit}; + + /// The POST port as a word field, `\P80`, which each method below writes + /// its own tag to: what the kernel's stand-in was asked to write there + /// is the order they ran in. + fn post() -> Vec { + // SystemIO; WordAcc (§20.2.5.2). + cat(&[&op_region("P80R", 0x01, &int(0x80), &int(2)), &field("P80R", 0x02, &[unit("P80", 16)])]) + } + + fn tagged(tag: u64) -> Vec { + store(&int(tag), &name("\\P80")) + } + + fn posted(kernel: &Scripted) -> Vec { + kernel.asked.borrow().iter().filter(|access| access.write == 1 && access.space == Space::SystemIo as u8 && access.address == 0x80).map(|access| access.value).collect() + } + + /// The namespace of `dsdt` loaded and initialised, and what that ran. + fn initialised(dsdt: &[u8]) -> (Scripted, Census) { + let mut kernel = crafted(&sealed(b"DSDT", &cat(&[&post(), dsdt])), &[]); + kernel.ports = vec![(0x80, 0)]; + 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); + drop(aml); + (kernel, census) + } + + /// §6.5.1: `\_SB._INI` first, then each device parent before child by + /// Table 6.67: no `_STA` is present and functional; functional and not + /// present runs no `_INI` and examines the children; neither examines + /// nothing below. Each `_STA` asked is counted, and each `_INI` run. + #[test] + fn sb_ini_runs_first_then_each_device_by_table_6_67() { + let dsdt = scope( + "\\_SB", + &cat(&[ + &method("_INI", 0, &tagged(1)), + &device( + "DEVA", + &cat(&[ + &method("_INI", 0, &tagged(2)), + &device("DEVB", &cat(&[&method("_STA", 0, &ret(&int(0x08))), &method("_INI", 0, &tagged(0xBB)), &device("DEVC", &method("_INI", 0, &tagged(3)))])), + &device("DEVD", &cat(&[&def_name("_STA", &int(0)), &method("_INI", 0, &tagged(0xDD)), &device("DEVE", &method("_INI", 0, &tagged(0xEE)))])), + ]), + ), + // Sleep (5) before its tag: the longest wait the census reads. + &device("DEVF", &cat(&[&def_name("_STA", &int(0x0F)), &method("_INI", 0, &cat(&[&[0x5B, 0x22, 0x0A, 0x05], &tagged(4)]))])), + ]), + ); + let (kernel, census) = initialised(&dsdt); + assert_eq!(posted(&kernel), [1, 2, 3, 4]); + assert_eq!((census.sb_ini, census.sta, census.ini), (Some(true), 3, 3)); + assert!(census.refused.is_empty(), "{}", census.refused.counts()); + assert_eq!( + census.line(), + "acpiserver: the namespace initialised: \\_SB._INI run; 3 _STA read, 3 _INI run, no _REG run; the longest wait an evaluation asked 5000 µs; evaluations refused, by kind: none" + ); + } + + /// A refused `_INI` is counted by kind and the walk goes on; a `_STA` + /// that is refused or no Integer is counted, its device's `_INI` is not + /// run, and its children are examined. + #[test] + fn a_refused_ini_or_sta_is_counted_and_the_walk_goes_on() { + let dsdt = scope( + "\\_SB", + &cat(&[ + // A Divide by zero. + &device("DEVA", &method("_INI", 0, &cat(&[&[0x78], &int(1), &int(0), &[0x00, 0x00]]))), + &device("DEVB", &cat(&[&def_name("_STA", &string("ON")), &device("DEVC", &method("_INI", 0, &tagged(0xCC)))])), + &device( + "DEVD", + &cat(&[&method("_STA", 0, &ret(&name("NONE"))), &method("_INI", 0, &tagged(0xDD)), &device("DEVF", &method("_INI", 0, &tagged(0xDF)))]), + ), + &device("DEVE", &method("_INI", 0, &tagged(5))), + ]), + ); + let (kernel, census) = initialised(&dsdt); + assert_eq!(posted(&kernel), [0xCC, 0xDF, 5]); + assert_eq!((census.sb_ini, census.sta, census.ini), (None, 0, 3)); + assert_eq!( + census.refused.counts(), + "_INI: Divide by zero (§19.6.32) x1; _STA: a name that does not resolve x1; _STA: a type its operator refuses: a _STA that is no Integer (§6.3.7) x1" + ); + } + + /// §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 + /// post, and nothing is refused; a device above an `_INI` has its `_STA` + /// asked, and the `_INI` runs. + #[test] + fn a_device_with_no_ini_at_or_below_it_is_not_examined() { + let dsdt = scope( + "\\_SB", + &cat(&[ + &op_region("ERAM", 0x03, &int(0), &int(0xFF)), + &field("ERAM", 0x01, &[unit("BSTA", 8)]), + &device("BAT1", &method("_STA", 0, &ret(&name("\\_SB.BSTA")))), + &device("LONE", &method("_STA", 0, &cat(&[&tagged(0x10), &ret(&int(0x0F))]))), + &device( + "PARE", + &cat(&[&method("_STA", 0, &cat(&[&tagged(0x20), &ret(&int(0x0F))])), &device("KIDD", &method("_INI", 0, &tagged(0x21)))]), + ), + ]), + ); + let (kernel, census) = initialised(&dsdt); + assert_eq!(posted(&kernel), [0x20, 0x21]); + assert_eq!((census.sta, census.ini), (1, 1)); + assert!(census.refused.is_empty(), "{}", census.refused.counts()); + } + + /// §6.5.4: a PCI_Config region on a root bus naming a `_BBN` is always + /// available, and its scope's `_REG` is not run, the bridge's own or a + /// device's on the bus below it; one on a root bus that names none, a + /// host bridge by its `_HID`, and one below a bridge, are announced with + /// `_REG(2, 1)`, before `\_SB._INI` and every device's `_INI`. + #[test] + fn pci_config_is_announced_before_any_ini_where_no_bbn_root_bus_holds_it() { + let region = |n: &str| cat(&[&op_region(n, 0x02, &int(0), &int(4)), &field(n, 0x01, &[unit(&format!("{}F", &n[..3]), 8)])]); + // `_REG` writes its tag plus Arg0 plus Arg1 shifted, so 2, 1 adds 0x12. + let reg = |tag: u64| method("_REG", 2, &store(&add(&add(&int(tag), &arg(0), &[0x00]), &cat(&[&[0x79], &arg(1), &int(4), &[0x00]]), &[0x00]), &name("\\P80"))); + let dsdt = scope( + "\\_SB", + &cat(&[ + &method("_INI", 0, &tagged(0x5B)), + &device( + "PCI0", + &cat(&[ + &def_name("_HID", &int(0x080A_D041)), + ®ion("CFG0"), + ®(0x100), + &device("DEV1", &cat(&[&def_name("_ADR", &int(0x0001_0000)), ®ion("CFG1"), ®(0x200)])), + ]), + ), + &device( + "PCI1", + &cat(&[ + &def_name("_BBN", &int(0x10)), + ®ion("CFGB"), + ®(0xB00), + &method("_INI", 0, &tagged(0x99)), + &device("DEV2", &cat(&[&def_name("_ADR", &int(0x0002_0000)), ®ion("CFG2"), ®(0xC00)])), + &device( + "BRG0", + &cat(&[&def_name("_ADR", &int(0x0003_0000)), &device("DEV3", &cat(&[&def_name("_ADR", &int(0)), ®ion("CFG3"), ®(0x300)]))]), + ), + ]), + ), + ]), + ); + let (kernel, census) = initialised(&dsdt); + assert_eq!(posted(&kernel), [0x112, 0x212, 0x312, 0x5B, 0x99]); + assert_eq!(census.reg, BTreeMap::from([(2, 3)])); + assert!(census.refused.is_empty(), "{}", census.refused.counts()); + assert!(kernel.asked.borrow().iter().all(|access| access.space != Space::PciConfig as u8), "a region was read"); + } +} diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 4860aeecf0a..7bdc5f49ab5 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -17,22 +17,37 @@ //! the load latches and is served when it ends. A table refused, and a DSDT //! refused, are each said and survived; the power button is served either //! way. The load hands the kernel `\_S5`'s sleep type, without which the -//! kernel refuses every power-off. +//! kernel refuses every power-off. A controller the ECDT names is attached to +//! AML before the first table loads. +//! +//! **Then what latched during the load is served** ([`Server::loaded`]), a +//! press taken by a table's Sleep and any SCI the load left latched: a press +//! then powers the machine off on the `\_S5` the load handed over, and waits +//! for no initialisation. +//! +//! **Then the namespace is initialised** ([`init`]): PCI_Config announced +//! where §6.5.4 asks it be, `\_SB._INI`, and the `_STA` and `_INI` of each +//! device at or above an `_INI` by Table 6.67, again over whatever a table +//! loaded meanwhile added. //! //! **Then the embedded controller is found in the namespace and armed** -//! ([`devices::find`]): attached to the AML host, so its space reaches it, -//! and told so by its `_REG` (ACPI 6.5 §6.5.4), where its `_GLK` asks for no -//! Global Lock around its transactions (§6.5.7), which this server takes -//! around none; then its GPE enabled and its backlog drained. Its two -//! ports are no row's, so every access to them is the kernel's to make -//! ([`Claim`]), and one it refuses ends the controller, said by name, the -//! server's own or AML's. A machine whose DSDT did not load has no -//! controller served. +//! ([`devices::find`], [`devices::attach`]): attached to the AML host in the ECDT's place, so its +//! space reaches it, under the Global Lock where its `_GLK` asks for it +//! (ACPI 6.5 §6.5.7), and announced by `_REG(3, 1)` in each scope that +//! declares a region of it (§6.5.4), after every `_INI`; then its GPE +//! enabled and its backlog drained. Its two ports are no row's, so every +//! access to them is the kernel's to make ([`Claim`]), and one it refuses +//! ends the controller, said by name, the server's own or AML's. A machine +//! whose DSDT did not load has no controller served. //! **Where the FADT says the power button is a control method device**, the //! buttons the namespace names are found too, and where one is, each query //! is run as its method ([`run_query`]): a Notify of a press is a press. //! Elsewhere no query's method runs. //! +//! **Then `_OSC` is negotiated** ([`osc`]): `\_SB._OSC`, and each +//! processor's on an Intel processor, query then commit, claiming what ToyOS +//! honours; whatever a table they loaded added is initialised in turn. +//! //! **Each SCI** is read off both blocks ([`sci::events`]): a press stops the //! machine through the supervisor, or is said and dropped on a machine with //! no power-off, and the controller's GPE drains the @@ -57,8 +72,11 @@ mod aml; mod battery; mod devices; mod ec; +mod gpe; mod host; +mod init; mod ledger; +mod osc; mod sci; mod tables; @@ -73,14 +91,14 @@ use toyos::power::{self, Stop}; use toyos::AcpiDev; use toyos_abi::acpi::{Access, AcpiInfo, Block, FIXED_POWER_BUTTON}; use toyos_abi::syscall::{DeviceType, SyscallError}; -use toyos_aml::Value; use acpiserver_api::{CONTROLLER_NONE, CONTROLLER_SERVED}; use aml::Aml; use battery::Power; use devices::{Ec, Queried}; use ec::Transaction; -use host::{port_in, transact, Answer, Kernel, Stopping, Take, Unmade, OWN}; +use host::{port_in, Answer, Kernel, Stopping, Take, Unmade, OWN}; +use init::{Census, Names}; use sci::{Bits, Event, Fixed, Served, Unserved, Unstopped, PM1_STATUS, PWRBTN}; /// The most queries one drain takes before the controller is a storm. @@ -109,6 +127,20 @@ struct Server<'a, K, P> { empty: u32, /// The kernel was handed `\_S5`'s sleep type: a press powers the machine off. power_off: bool, + /// What the namespace's initialisation ran. + census: Census, + /// What `_OSC` answered. + negotiated: osc::Negotiated, + /// The processor is Intel's, whose `_OSC` interface a processor object + /// speaks ([`toyos_abi::processor_is_intel`]). + intel: bool, + /// The batteries and adapters the namespace names, which a Notify of an + /// information change reads again. + sources: (Vec, Vec), + /// Each runtime GPE's methods run, by GPE. + gpe_runs: BTreeMap, + /// Each Notify delivered, by object and value. + delivered: ledger::Ledger, } fn main() { @@ -121,10 +153,15 @@ fn main() { let mut server = Server::new(&claim, Machine, info); server.arm(&dev); let (loaded, kept) = aml::load(&claim, server.info.rsdp); - server.power_off = loaded.handed; - server.aml = kept; - let (batteries, adapters) = server.arm_controller(); + let names = server.loaded(loaded.handed, kept); + let (batteries, adapters) = server.arm_controller(names.as_ref()); + if let Some(names) = server.negotiate(names) { + println!("{}", server.census.line()); + server.arm_gpes(&names); + } + server.sources = (batteries.clone(), adapters.clone()); server.find_power(batteries, adapters); + server.served_aml(); server.serve(); } @@ -213,12 +250,33 @@ impl Kernel for Claim<'_> { sci::await_release(&mut Waiting(self), until) } + fn sleep(&self, until: Instant, ec_gpe: Option) -> sci::Latched { + let served = Served { power_button: self.info.flags & FIXED_POWER_BUTTON != 0, ec_gpe, ..Default::default() }; + sci::await_sleep(&mut Waiting(self), until, &served) + } + fn s5(&self, slp_typ_a: u64) -> Result { match self.dev.s5(slp_typ_a) { Err(SyscallError::InvalidArgument) => Ok(false), answer => Self::answered("the power-off's sleep type", answer).map(|()| true), } } + + fn latched(&self) -> Option { + match self.dev.irq() { + Ok(record) => Some(record.count), + Err(SyscallError::WouldBlock) => None, + Err(other) => panic!("acpiserver: the claim's record answered {other:?}"), + } + } + + fn ack(&self) { + self.dev.ack().expect("acpiserver: the claim's acknowledgement"); + } + + fn power_off(&self) -> power::Refused { + power::stop(Stop::Shutdown) + } } /// The claim's event blocks and SCI for one wait for the firmware's release. @@ -233,6 +291,11 @@ impl Fixed for Waiting<'_, '_> { out16(self.0.info.pm1_event.port, bits); } + fn gpe_clear(&mut self, n: u16) { + let (status, _) = bytes(self.0.info.gpe0).nth(usize::from(n / 8)).expect("a GPE this server serves is in the block"); + out8(status, 1 << (n % 8)); + } + fn enables(&mut self) -> Bits { Bits { pm1: in16(self.0.info.pm1_event.enable()), gpe0: bytes(self.0.info.gpe0).map(|(_, enable)| in8(enable)).collect() } } @@ -256,7 +319,7 @@ impl Fixed for Waiting<'_, '_> { } fn ack(&mut self) { - self.0.dev.ack().expect("acpiserver: the claim's acknowledgement"); + Kernel::ack(self.0); } } @@ -271,7 +334,7 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { kernel, ports, info, - served: Served { power_button: info.flags & FIXED_POWER_BUTTON != 0, ec_gpe: None }, + served: Served { power_button: info.flags & FIXED_POWER_BUTTON != 0, ..Default::default() }, ec: None, aml: None, power: None, @@ -282,6 +345,12 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { scis: 0, empty: 0, power_off: false, + census: Census::default(), + negotiated: osc::Negotiated::default(), + intel: toyos_abi::processor_is_intel(), + sources: (Vec::new(), Vec::new()), + gpe_runs: BTreeMap::new(), + delivered: ledger::Ledger::default(), } } @@ -303,25 +372,113 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { ); } + /// Take what the load kept: what latched during it is served first, so a + /// press then powers the machine off on the `\_S5` it handed over and + /// waits for no initialisation, and then the namespace is initialised + /// ([`Server::initialise`]). + fn loaded(&mut self, handed: bool, kept: Option>) -> Option { + self.power_off = handed; + self.aml = kept; + self.served_aml(); + self.latched(); + self.initialise() + } + + /// Each SCI already latched, served without waiting for one. + fn latched(&mut self) { + let Some(count) = self.kernel.latched() else { return }; + self.scis += u64::from(count); + self.take(); + self.kernel.ack(); + } + + /// Initialise the namespace the load kept ([`init`]), and again whatever + /// a table its initialisation loaded added, until none did; what comes + /// back is the namespace as it then stands. + fn initialise(&mut self) -> Option { + let aml = self.aml.as_mut()?; + let names = match Names::read(&aml.interpreter) { + Ok(names) => names, + Err(why) => { + println!("acpiserver: the namespace is not initialised: its walk was refused, {why}"); + return None; + } + }; + let defined = aml.interpreter.defined(); + init::initialise(aml, &names, &mut self.census); + self.settle(defined, names) + } + + /// Run what each table loaded since `since` added ([`init::loaded`]), + /// until an initialisation loads nothing more; `names` is the namespace + /// as it stood at `since`. + fn settle(&mut self, mut since: u32, mut names: Names) -> Option { + let aml = self.aml.as_mut()?; + while aml.interpreter.defined() > since && !aml.host.stopping { + names = match Names::read(&aml.interpreter) { + Ok(names) => names, + Err(why) => { + println!("acpiserver: what a table loaded at run time added is not initialised: the walk was refused, {why}"); + return None; + } + }; + let added = since; + since = aml.interpreter.defined(); + init::loaded(aml, &names, added, &mut self.census); + } + Some(names) + } + + /// Negotiate `_OSC` ([`osc`]), then initialise what the tables it loaded + /// added; what comes back is the namespace as it then stands, and its + /// size is said, after the machine's last table. + fn negotiate(&mut self, names: Option) -> Option { + let (names, aml) = (names?, self.aml.as_mut()?); + let before = aml.interpreter.defined(); + self.negotiated = osc::negotiate_all(aml, &names, self.intel); + let names = self.settle(before, names)?; + let aml = self.aml.as_ref()?; + println!( + "acpiserver: {} table(s) loaded at run time and {} refused; the namespace holds {} objects in {} slots, and the interpreter {} bytes of heap, of the {} it holds between evaluations and the {} while one runs", + aml.host.loaded, + aml.host.loads_refused, + aml.interpreter.defined(), + aml.interpreter.slots(), + aml.interpreter.usage().live, + toyos_aml::MAX_LIVE, + toyos_aml::MAX_RUNNING + ); + Some(names) + } + /// Find the controller, and the buttons where they are control method /// devices, in the namespace the load kept; attach the controller to the - /// AML host ([`attach`]), enable its GPE and drain its backlog. What + /// AML host ([`devices::attach`]), enable its GPE and drain its backlog. What /// comes back is the namespace's batteries and adapters, by path. - fn arm_controller(&mut self) -> (Vec, Vec) { - let Some(aml) = &mut self.aml else { + fn arm_controller(&mut self, names: Option<&Names>) -> (Vec, Vec) { + let (Some(aml), Some(names)) = (&mut self.aml, names) else { if !self.served.power_button { println!("acpiserver: no control-method power button served: no namespace was kept"); } println!("{CONTROLLER_NONE}no namespace was kept"); return (Vec::new(), Vec::new()); }; - let found = devices::find(&mut aml.interpreter, &mut aml.host, self.info.gpe0, !self.served.power_button); + let found = devices::find(&mut aml.interpreter, &mut aml.host, names, self.info.gpe0, !self.served.power_button); if !self.served.power_button { println!("acpiserver: {} control-method power button(s) served, each by the queries' Notify", found.buttons.len()); } aml.host.buttons = found.buttons; let sources = (found.batteries, found.adapters); - let Some(ec) = found.ec else { return sources }; + let early = aml.host.ec.take(); + let Some(ec) = found.ec else { + if early.is_some() { + println!("{CONTROLLER_NONE}the namespace names no controller it serves, and the ECDT's is detached from AML"); + } + return sources; + }; + if early.is_some_and(|early| (early.path.as_str(), early.command, early.data) != (ec.path.as_str(), ec.command, ec.data)) { + println!("acpiserver: the ECDT's embedded controller is not the namespace's, which is served"); + } // Its status register, which a read leaves as it was; the data // register's read takes a byte, and a port refused later ends the // controller then ([`Server::unserve`]). @@ -333,10 +490,13 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { } Ok(_) => {} } - if let Err(why) = attach(aml, &ec) { - println!("{CONTROLLER_NONE}{why}"); - return sources; - } + let ec = match devices::attach(aml, ec, names, &mut self.census) { + Ok(ec) => ec, + Err(why) => { + println!("{CONTROLLER_NONE}{why}"); + return sources; + } + }; if aml.host.stopping { return sources; } @@ -421,16 +581,140 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { self.ports.out8(status, 1 << (n % 8)); self.drain(); } + Event::Gpe(n) => self.gpe(n), } } self.run_queued(); + self.served_aml(); + } + + /// Enable each runtime GPE the namespace names ([`gpe::runtime`]), its + /// status cleared first. + fn arm_gpes(&mut self, names: &Names) { + let Some(aml) = &mut self.aml else { return }; + let runtime = gpe::runtime(aml, names, self.info.gpe0, self.served.ec_gpe); + for &n in runtime.keys() { + let (status, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("a runtime GPE is in GPE0"); + self.ports.out8(status, 1 << (n % 8)); + self.ports.out8(enable, self.ports.in8(enable) | 1 << (n % 8)); + } + self.served.gpes = runtime; + } + + /// GPE `n` served by its method (ACPI 6.5 §5.6.4): its enable cleared, + /// an edge's status cleared, the method run, a level's status cleared, + /// and its enable set again; a method refused leaves it disabled, said + /// by name, and served no more. + fn gpe(&mut self, n: u16) { + let trigger = self.served.gpes[&n]; + let (status, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("a runtime GPE is in GPE0"); + let bit = 1 << (n % 8); + self.ports.out8(enable, self.ports.in8(enable) & !bit); + if trigger == sci::Trigger::Edge { + self.ports.out8(status, bit); + } + let method = format!("\\_GPE._{}{n:02X}", if trigger == sci::Trigger::Edge { 'E' } else { 'L' }); + let aml = self.aml.as_mut().expect("a GPE is served only in a namespace"); + let ran = aml.interpreter.evaluate(&mut aml.host, &method, &[]); + if trigger == sci::Trigger::Level { + self.ports.out8(status, bit); + } + let runs = self.gpe_runs.entry(n).or_insert(0); + *runs += 1; + match ran { + Ok(_) => { + self.ports.out8(enable, self.ports.in8(enable) | bit); + if *runs == 1 { + println!("acpiserver: GPE {n:#x} taken for the first time; its method ran"); + } + } + Err(_) if aml.host.stopping => {} + Err(why) => { + self.served.gpes.remove(&n); + println!("acpiserver: GPE {n:#x}'s method was refused, so the GPE is served no more: {}", aml::kind(&why)); + println!("{OWN}that was {method}, refused {why:x?}"); + } + } + self.ended(); + } + + /// What the namespace's evaluations left to serve: each press a Sleep + /// took, the controller's queries a Sleep found waiting, and each Notify + /// delivered ([`Server::deliver`]). What these evaluations leave in turn + /// waits for the next SCI or poll. + fn served_aml(&mut self) { + let Some(aml) = &mut self.aml else { return }; + let latched = std::mem::take(&mut aml.host.latched); + let notified = std::mem::take(&mut aml.host.notified); + if latched.ec { + self.drain(); + self.run_queued(); + } + self.deliver(notified); + for _ in 0..latched.presses { + self.press(); + } + } + + /// Each Notify to its owner (ACPI 6.5 §5.6.6): a battery's or adapter's + /// status change (0x80) reads the power sources again, and a battery's + /// information change (0x81) finds them again; `\_SB`'s or a negotiated + /// processor's 0x08 negotiates its `_OSC` again with the same claim + /// (§6.2.11); a processor's performance, idle or throttling change + /// (0x80, 0x81, 0x82, §8.4) is counted, since nothing reads those states + /// yet; anything else is served by nothing and counted. + fn deliver(&mut self, notified: std::collections::BTreeSet<(String, u64)>) { + let (mut status, mut info) = (false, false); + for (object, value) in notified { + let source = self.sources.0.contains(&object) || self.sources.1.contains(&object); + let processor = self.negotiated.processors.iter().any(|(path, _)| *path == object); + let served = match value { + 0x80 if source => { + status = true; + "the power sources read again" + } + 0x81 if self.sources.0.contains(&object) => { + info = true; + "the power sources found again" + } + 0x08 if object == "\\_SB_" || processor => { + self.renegotiate(&object); + "its _OSC negotiated again" + } + 0x80..=0x82 if processor => "counted: nothing reads the processors' performance, idle or throttling states", + _ => "served by nothing", + }; + if self.delivered.see(&format!("{object} {value:#x}")) { + println!("{OWN}Notify({object}, {value:#x}) for the first time: {served}"); + } + } + if info { + let (batteries, adapters) = self.sources.clone(); + self.find_power(batteries, adapters); + } else if status { + self.read_power(); + } + } + + /// `object`'s `_OSC` negotiated again with its claim. + fn renegotiate(&mut self, object: &str) { + let Some(aml) = &mut self.aml else { return }; + let claim = if object == "\\_SB_" { &osc::PLATFORM } else { &osc::PROCESSOR }; + let before = aml.interpreter.defined(); + match osc::negotiate(aml, &format!("{object}._OSC"), claim) { + Ok(answered) => println!("{OWN}{object}._OSC negotiated again: {answered:x?}"), + Err(why) => println!("{OWN}{object}._OSC was refused again: {why}"), + } + if let Ok(names) = Names::read(&aml.interpreter) { + self.settle(before, names); + } } /// A press: the machine stops, and this comes back only where it has no /// power-off, which the load said and this says again. fn press(&mut self) { println!("acpiserver: the power button was pressed, on SCI {} of this boot; asking the supervisor to power off", self.scis); - let refused = power::stop(Stop::Shutdown); + let refused = self.kernel.power_off(); match sci::unstopped(refused, self.power_off) { Unstopped::Dropped => toyos::error!("acpiserver: the press is dropped: {}", aml::NO_S5_HANDED.trim_start_matches("acpiserver: ")), Unstopped::Defect => panic!("acpiserver: the power-off was refused: {refused:?}"), @@ -447,15 +731,15 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { } fn take_queries(&mut self) -> Result<(), Unmade> { - let Some(ec) = &self.ec else { return Ok(()) }; - let (command, data) = (ec.command, ec.data); + let (Some(ec), Some(aml)) = (&self.ec, &mut self.aml) else { return Ok(()) }; + let (command, data, glk) = (ec.command, ec.data, ec.glk); let mut taken = 0; loop { let status = port_in(self.kernel, command)?; if status & ec::SCI_EVT == 0 { return Ok(()); } - let q = transact(self.kernel, command, data, Transaction::query())?; + let q = aml.host.transaction(command, data, glk, Transaction::query())?; if q == 0 { return Ok(()); } @@ -527,48 +811,9 @@ impl<'a, P: Ports> Server<'a, Claim<'a>, P> { self.log_counts(); next_count = Instant::now() + COUNTS; } - match self.kernel.dev.irq() { - Ok(record) => self.scis += u64::from(record.count), - Err(SyscallError::WouldBlock) => continue, - Err(other) => panic!("acpiserver: the claim's record answered {other:?}"), - } - self.take(); - self.kernel.dev.ack().expect("acpiserver: the claim's acknowledgement"); - } - } -} - -/// Attach `ec` to the AML host, so its space reaches the controller, where -/// its `_GLK` asks for no Global Lock around a transaction (ACPI 6.5 §6.5.7): -/// absent or 0. Then tell its `_REG` the space is there (§6.5.4: Arg0 3 the -/// EmbeddedControl space, Arg1 1 connect). `Err` is why a port of it the -/// kernel refused ended it. -fn attach(aml: &mut Aml<'_, K>, ec: &Ec) -> Result<(), String> { - let evaluate = |aml: &mut Aml<'_, K>, method: &str, args: &[Value]| { - let path = format!("{}.{method}", ec.path); - match aml.interpreter.evaluate(&mut aml.host, &path, args) { - Err(toyos_aml::Error::NotFound(named)) if named == path => Ok(None), - answer => answer.map(Some), - } - }; - let unattached: Option = match evaluate(aml, "_GLK", &[]) { - Ok(None | Some(Value::Integer(0))) => None, - Ok(Some(Value::Integer(_))) => Some("its _GLK asks for the Global Lock around each transaction, which this server takes around none".into()), - Ok(Some(_)) => Some("its _GLK is no Integer".into()), - Err(why) => Some(format!("its _GLK was refused, {}", aml::kind(&why))), - }; - if let Some(why) = unattached { - println!("acpiserver: the embedded controller's space is not attached to AML: {why}"); - return Ok(()); - } - aml.host.ec = Some(ec.clone()); - if let Err(why) = evaluate(aml, "_REG", &[Value::Integer(3), Value::Integer(1)]) { - if !aml.host.stopping { - println!("acpiserver: the embedded controller's _REG refused: {}", aml::kind(&why)); - println!("{OWN}that was {}._REG", ec.path); + self.latched(); } } - aml.host.ended.take().map_or(Ok(()), Err) } /// What a query no method runs for is said to be. @@ -587,9 +832,15 @@ fn run_query(aml: Option<&mut Aml<'_, K>>, ec: Option<&Ec>, q: u8) -> } } +/// The interpreter's own test encodings of §20.2. +#[cfg(test)] +#[path = "../aml/tests/common/mod.rs"] +mod common; + #[cfg(test)] mod tests { use super::*; + use toyos_aml::Value; use toyos_abi::acpi::{Space, Width}; use crate::aml::load; use crate::aml::tests::{crafted, sealed, CRAFTED_RSDP}; @@ -597,9 +848,11 @@ mod tests { use crate::host::tests::Scripted; use std::cell::RefCell; - /// The row's registers as bytes a write leaves as written. + /// The row's registers as bytes a write leaves as written, but those in + /// `.1`, status registers, where a one written clears its bit (ACPI 6.5 + /// §4.8.4.1) once something has set them. #[derive(Default)] - struct Bytes(RefCell>); + struct Bytes(RefCell>, std::collections::BTreeSet); impl Ports for Bytes { fn in8(&self, port: u16) -> u8 { @@ -607,15 +860,22 @@ mod tests { } fn out8(&self, port: u16, byte: u8) { - self.0.borrow_mut().insert(port, byte); + let mut bytes = self.0.borrow_mut(); + let written = match bytes.get(&port) { + Some(&was) if self.1.contains(&port) => was & !byte, + _ => byte, + }; + bytes.insert(port, written); } fn in16(&self, port: u16) -> u16 { - panic!("a PM1 read of {port:#x}, which no test here makes") + u16::from_le_bytes([self.in8(port), self.in8(port + 1)]) } - fn out16(&self, port: u16, _: u16) { - panic!("a PM1 write of {port:#x}, which no test here makes") + fn out16(&self, port: u16, word: u16) { + let [lo, hi] = word.to_le_bytes(); + self.out8(port, lo); + self.out8(port + 1, hi); } } @@ -641,13 +901,15 @@ mod tests { ], ), ] { - let kernel = Scripted { ports, ..Default::default() }; - let info = AcpiInfo { rsdp: 0, pm1_event: Block::NONE, gpe0: GPE0, flags: 0, reserved: [0; 3] }; + let kernel = Scripted { ports, ..crafted(&sealed(b"DSDT", &[]), &[]) }; + let info = AcpiInfo { rsdp: CRAFTED_RSDP, pm1_event: Block::NONE, gpe0: GPE0, flags: 0, reserved: [0; 3] }; let mut server = Server::new(&kernel, Bytes::default(), info); + server.aml = load(&kernel, CRAFTED_RSDP).1; + kernel.asked.borrow_mut().clear(); let enable = GPE0.enable(); server.ports.out8(enable, 0b1000_1001); server.served.ec_gpe = Some(gpe); - server.ec = Some(Ec { path: "\\_SB_.EC0_".into(), command: command as u16, data: data as u16, gpe, queries: Default::default() }); + server.ec = Some(Ec { path: "\\_SB_.EC0_".into(), command: command as u16, data: data as u16, gpe, queries: Default::default(), glk: false }); // Taken by an earlier event of the same SCI, and not yet run. server.queued.push_back(0x28); server.drain(); @@ -676,7 +938,8 @@ mod tests { server.ports.out8(enable, 0); } server.aml = load(&kernel, CRAFTED_RSDP).1; - let (batteries, adapters) = server.arm_controller(); + let names = server.initialise(); + let (batteries, adapters) = server.arm_controller(names.as_ref()); assert_eq!(server.served.ec_gpe, Some(3)); assert!(server.aml.as_ref().is_some_and(|aml| aml.host.ec.is_some()), "the served controller is not attached to AML"); server.find_power(batteries, adapters); @@ -719,7 +982,8 @@ mod tests { server.ports.out8(enable, 0); } server.aml = load(&kernel, CRAFTED_RSDP).1; - server.arm_controller(); + let names = server.initialise(); + server.arm_controller(names.as_ref()); assert_eq!(server.counts, BTreeMap::from([(0x28, 1)]), "the query waiting was not drained"); let refused = &server.aml.as_ref().expect("the namespace").host.refused; assert!(refused.is_empty(), "{}", refused.counts()); @@ -727,6 +991,321 @@ mod tests { assert_eq!(done, [(WR, CONNECTED, 1), (0x84, 0, 0), (RD, AC, 0)], "_REG's write, the drain's query, the method's read"); } + /// The track's `_REG` bullet and §6.5.4: the controller's `_REG(3, 1)` + /// runs after every `_INI`, and an `_INI` that writes the controller's + /// space reaches it only where the ECDT named the controller, which is + /// attached before any control method runs; without one it is refused, + /// and the controller is attached by its device after the walk. + #[test] + fn the_controllers_reg_runs_after_every_ini_which_reaches_it_through_the_ecdt() { + use crate::aml::tests::{crafted_with, ecdt}; + use crate::devices::tests::{byte, crs, device, dword, pkg, PNP0C09}; + use crate::ec::tests::Emulated; + // OperationRegion (ECOR, EmbeddedControl, 0, 0x100); Field (ECOR, + // ByteAcc) { Offset (0x10), INIB, 8, REGB, 8 }; Method (_INI) { INIB = + // One }; Method (_REG, 2) { REGB = Arg1 }. + let region = [&[0x5B, 0x80][..], b"ECOR", &[0x03, 0x00, 0x0B, 0x00, 0x01]].concat(); + let field = [&[0x5B, 0x81][..], &pkg(&[&b"ECOR"[..], &[0x01, 0x00, 0x40, 0x08], b"INIB", &[0x08], b"REGB", &[0x08]].concat())].concat(); + let ini = [&[0x14][..], &pkg(&[&b"_INI"[..], &[0x00, 0x70, 0x01], b"INIB"].concat())].concat(); + let reg = [&[0x14][..], &pkg(&[&b"_REG"[..], &[0x02, 0x70, 0x69], b"REGB"].concat())].concat(); + let ec = device(&[b"_SB_", b"EC0_"], &[dword(b"_HID", PNP0C09), crs(&[0x62, 0x66]), byte(b"_GPE", 3), region, field, ini, reg].concat()); + for with_ecdt in [true, false] { + let others: Vec> = if with_ecdt { vec![ecdt(b"\\_SB.EC0")] } else { vec![] }; + let others: Vec<&[u8]> = others.iter().map(Vec::as_slice).collect(); + let kernel = crafted_with(&sealed(b"DSDT", &ec), &[], &others); + *kernel.controller.borrow_mut() = Some(Emulated::new([0; 256])); + let info = AcpiInfo { rsdp: CRAFTED_RSDP, pm1_event: Block::NONE, gpe0: GPE0, flags: FIXED_POWER_BUTTON, reserved: [0; 3] }; + let mut server = Server::new(&kernel, Bytes::default(), info); + for (_, enable) in bytes(GPE0) { + server.ports.out8(enable, 0); + } + server.aml = load(&kernel, CRAFTED_RSDP).1; + let names = server.initialise(); + server.arm_controller(names.as_ref()); + let done = kernel.controller.borrow().as_ref().expect("a controller").done.clone(); + let (wr, inib, regb) = (0x81, 0x10, 0x11); + match with_ecdt { + true => assert_eq!(done, [(wr, inib, 1), (wr, regb, 1)], "the _INI's write, then _REG's"), + false => { + assert_eq!(done, [(wr, regb, 1)], "only _REG's write"); + assert_eq!(server.census.refused.counts(), "_INI: denied by this server: a write to EmbeddedControl: no controller is attached to this server's AML x1"); + } + } + assert_eq!((server.census.ini, server.census.reg.get(&3).copied()), (u64::from(with_ecdt), Some(1))); + assert_eq!(server.served.ec_gpe, Some(3)); + } + } + + /// A platform `_OSC` and an Intel processor's that record what they are + /// passed in firmware memory and offer bit 0 alone to a query; the + /// processor's commit loads a block from reserved memory, whose device's + /// `_INI` sets a byte. + fn osc_machine(block_type: u8) -> Scripted { + use crate::common::{arg, buffer, cat, device, else_, field, if_, int, method, name, op_region, pkg, ret, scope, ssdt_named, store, unit}; + const NVS: u64 = 0x7700_0000; + const BLOCK: u64 = 0x7800_0000; + let block = ssdt_named("CPU0PSD", &device("\\_SB.NEWD", &method("_INI", 0, &store(&int(0x77), &name("\\NEWI"))))); + let dword = |n: &str| unit(n, 32); + let nvs = cat(&[ + &op_region("NVSR", 0x00, &int(NVS), &int(0x30)), + &field("NVSR", 0x03, &["PUU", "PQ1", "PQ2", "PC1", "PC2", "CUU", "CQ1", "CQ2", "CC1", "CC2", "NEWI"].map(dword)), + &op_region("BLKR", 0x00, &int(BLOCK), &int(block.len() as u64)), + ]); + // CreateDWordField (Arg3, n, CDWn) (§19.6.20). + let created = |at: u64, n: &str| cat(&[&[0x8A], &arg(3), &int(at), &name(n)]); + let osc = |p: &str, uuid: &[u8; 16], commit: &[u8]| { + method( + "_OSC", + 4, + &cat(&[ + &created(0, "CDW1"), + &created(4, "CDW2"), + &if_(&cat(&[&[0x93], &arg(0), &buffer(&int(16), uuid)]), &store(&int(1), &name(&format!("\\{p}UU")))), + &if_( + &cat(&[&[0x7B], &name("CDW1"), &int(1), &[0x00]]), + &cat(&[ + &store(&name("CDW1"), &name(&format!("\\{p}Q1"))), + &store(&name("CDW2"), &name(&format!("\\{p}Q2"))), + &store(&int(0x10), &name("CDW1")), + &store(&int(1), &name("CDW2")), + ]), + ), + &else_(&cat(&[&store(&name("CDW1"), &name(&format!("\\{p}C1"))), &store(&name("CDW2"), &name(&format!("\\{p}C2"))), commit])), + &ret(&arg(3)), + ]), + ) + }; + let load = cat(&[&[0x5B, 0x20], &name("\\BLKR"), &[0x60]]); + // Processor (\_PR.CPU0, 0, 0x410, 6) (ACPI 6.3A §19.6.108). + let processor = cat(&[&[0x5B, 0x83], &pkg(&cat(&[&name("\\_PR.CPU0"), &[0x00], &0x410u32.to_le_bytes(), &[0x06], &osc("C", &osc::PROCESSOR.uuid, &load)]))]); + let dsdt = cat(&[&nvs, &scope("\\_SB", &osc("P", &osc::PLATFORM.uuid, &[])), &processor]); + let mut kernel = crafted(&sealed(b"DSDT", &dsdt), &[]); + kernel.memory.push((NVS, 10, vec![0; 0x30])); + kernel.memory.push((BLOCK, block_type, block)); + kernel + } + + /// §6.2.11: `\_SB._OSC` and, on an Intel processor, each processor's are + /// queried with what ToyOS claims, nothing, and committed with what the + /// query granted of it, nothing, though the firmware offered a bit; + /// the block the processor's commit loads from reserved memory loads, and + /// its device's `_INI` runs. On another vendor's processor its `_OSC` is + /// not evaluated, and a block in ACPI reclaim memory, which Table 15.6 + /// calls neither reserved nor NVS, is refused and keeps nothing. + #[test] + fn osc_commits_only_what_toyos_claims_and_what_its_load_adds_is_initialised() { + for (intel, block_type, loaded) in [(true, 0, true), (false, 0, false), (true, 9, false)] { + let kernel = osc_machine(block_type); + let info = AcpiInfo { rsdp: CRAFTED_RSDP, pm1_event: Block::NONE, gpe0: GPE0, flags: FIXED_POWER_BUTTON, reserved: [0; 3] }; + let mut server = Server::new(&kernel, Bytes::default(), info); + server.intel = intel; + server.aml = load(&kernel, CRAFTED_RSDP).1; + let names = server.initialise(); + server.arm_controller(names.as_ref()); + server.negotiate(names).expect("the namespace stands"); + let aml = server.aml.as_mut().expect("the namespace"); + let mut read = |path: &str| match aml.interpreter.evaluate(&mut aml.host, path, &[]) { + Ok(Value::Integer(v)) => v, + other => panic!("{path}: {other:?}"), + }; + let platform: Vec = ["\\PUU", "\\PQ1", "\\PQ2", "\\PC1", "\\PC2"].map(&mut read).to_vec(); + assert_eq!(platform, [1, 1, 0, 0, 0], "the platform's UUID, the query flag and nothing claimed, then the commit of nothing"); + let processor: Vec = ["\\CUU", "\\CQ1", "\\CQ2", "\\CC1", "\\CC2"].map(&mut read).to_vec(); + assert_eq!(processor, if intel { [1, 1, 0, 0, 0] } else { [0; 5] }, "intel {intel}"); + assert_eq!(read("\\NEWI"), if loaded { 0x77 } else { 0 }, "intel {intel}, block in type {block_type}"); + assert_eq!((aml.host.loaded, aml.host.loads_refused), (u64::from(loaded), u64::from(intel && !loaded))); + let answered = osc::Answered { query: 0x10, offered: 0, commit: Some((0, 0)) }; + assert_eq!(server.negotiated.platform, Some(Ok(answered))); + assert_eq!(server.negotiated.processors.len(), usize::from(intel)); + assert_eq!(server.census.ini, u64::from(loaded), "the loaded device's _INI"); + } + } + + /// The POST port as a word field, `\\P80`, and a method `name` that writes + /// `tag` to it: what the kernel's stand-in was asked to write there is + /// which methods ran, in order. + fn posting(name: &str, tag: u64) -> Vec { + use crate::common::{int, method, store}; + method(name, 0, &store(&int(tag), &crate::common::name("\\P80"))) + } + + fn post() -> Vec { + use crate::common::{cat, field, int, op_region, unit}; + cat(&[&op_region("P80R", 0x01, &int(0x80), &int(2)), &field("P80R", 0x02, &[unit("P80", 16)])]) + } + + fn posted(kernel: &Scripted) -> Vec { + kernel.asked.borrow().iter().filter(|access| access.write == 1 && access.space == Space::SystemIo as u8 && access.address == 0x80).map(|access| access.value).collect() + } + + /// A server over `dsdt`, its namespace loaded and initialised, its GPE0 + /// and PM1 registers all clear. + fn served(kernel: &Scripted, flags: u16) -> (Server<'_, Scripted, Bytes>, Option) { + let info = AcpiInfo { rsdp: CRAFTED_RSDP, pm1_event: Block { port: 0x400, len: 4 }, gpe0: GPE0, flags, reserved: [0; 3] }; + let statuses = bytes(GPE0).map(|(status, _)| status).chain([0x400, 0x401]).collect(); + let mut server = Server::new(kernel, Bytes(RefCell::default(), statuses), info); + for port in 0x400..0x404 { + server.ports.0.borrow_mut().insert(port, 0); + } + for (status, enable) in bytes(GPE0) { + server.ports.0.borrow_mut().insert(status, 0); + server.ports.out8(enable, 0); + } + server.aml = load(kernel, CRAFTED_RSDP).1; + let names = server.initialise(); + (server, names) + } + + /// §5.6.4: each `\\_GPE._Lxx` and `_Exx` in GPE0 is a runtime GPE and + /// enabled, but one the `_PRW` of a device other than the buttons and the + /// PCI host bridge names (§5.6.4.2), the controller's, and one past + /// GPE0. On its SCI its method runs, its status is cleared and its enable + /// set again; a method refused leaves it disabled and served no more. + #[test] + fn runtime_gpes_are_enabled_and_served_by_their_methods() { + use crate::common::{cat, def_name, device, int, method, package, scope}; + use crate::devices::tests::PNP0C0C; + let gpe = scope( + "\\_GPE", + &cat(&[ + &posting("_L10", 0x10), + &posting("_E11", 0x11), + // A Divide by zero. + &method("_L12", 0, &[0x78, 0x01, 0x00, 0x00, 0x00]), + &posting("_L13", 0x13), + &posting("_L14", 0x14), + &posting("_L50", 0x50), + &posting("_L03", 0x03), + ]), + ); + let devices = scope( + "\\_SB", + &cat(&[ + &device("WAKE", &cat(&[&def_name("_HID", &int(0x0105_D041)), &def_name("_PRW", &package(&[int(0x13), int(3)]))])), + &device("LID0", &cat(&[&def_name("_HID", &int(u64::from(PNP0C0C))), &def_name("_PRW", &package(&[int(0x14), int(4)]))])), + ]), + ); + let mut kernel = crafted(&sealed(b"DSDT", &cat(&[&post(), &gpe, &devices, &crate::devices::tests::laptop(&[])])), &[]); + kernel.ports = vec![(0x80, 0), (0x66, 0)]; + *kernel.controller.borrow_mut() = Some(crate::ec::tests::Emulated::new([0; 256])); + let (mut server, names) = served(&kernel, FIXED_POWER_BUTTON); + server.arm_controller(names.as_ref()); + let names = server.negotiate(names).expect("the namespace stands"); + server.arm_gpes(&names); + use sci::Trigger::{Edge, Level}; + assert_eq!(server.served.gpes, BTreeMap::from([(0x10, Level), (0x11, Edge), (0x12, Level), (0x14, Level)])); + let enables: Vec = bytes(GPE0).map(|(_, enable)| server.ports.in8(enable)).collect(); + assert_eq!(enables, [0b1000, 0, 0b0001_0111, 0], "the controller's GPE 3 and the runtime GPEs"); + let (status, _) = bytes(GPE0).nth(2).expect("byte 2"); + server.ports.0.borrow_mut().insert(status, 0b0000_0111); + kernel.asked.borrow_mut().clear(); + server.take(); + assert_eq!(posted(&kernel), [0x10, 0x11]); + assert_eq!(server.ports.in8(status), 0, "a status left set"); + assert_eq!(server.served.gpes.keys().copied().collect::>(), [0x10, 0x11, 0x14], "a GPE whose method was refused is still served"); + let (_, enable) = bytes(GPE0).nth(2).expect("byte 2"); + assert_eq!(server.ports.in8(enable), 0b0001_0011, "the refused GPE left enabled, or a served one disabled"); + } + + /// §5.6.6: a battery's status change reads the power sources again, a + /// battery's information change finds them again, `\\_SB`'s 0x08 + /// negotiates its `_OSC` again, and anything else is served by nothing: + /// each said once and nothing evaluated for it. + #[test] + fn a_notify_is_delivered_to_its_owner() { + let kernel = crate::battery::tests::laptop_machine(None); + let (mut server, names) = served(&kernel, FIXED_POWER_BUTTON); + let (batteries, adapters) = server.arm_controller(names.as_ref()); + server.sources = (batteries.clone(), adapters.clone()); + server.find_power(batteries.clone(), adapters); + let done = || kernel.controller.borrow().as_ref().expect("a controller").done.len(); + let before = done(); + server.deliver([(batteries[0].clone(), 0x80)].into()); + assert!(done() > before, "a battery's status change read no battery"); + let before = done(); + server.deliver([("\\_SB_.NONE".to_string(), 0x80), (batteries[0].clone(), 0x02)].into()); + assert_eq!(done(), before, "a Notify nobody owns evaluated something"); + server.deliver([(batteries[0].clone(), 0x81)].into()); + assert!(done() > before, "a battery's information change found no battery"); + assert_eq!(server.delivered.counts().matches(" x1").count(), 4); + + let kernel = osc_machine(0); + let (mut server, names) = served(&kernel, FIXED_POWER_BUTTON); + server.arm_controller(names.as_ref()); + server.negotiate(names).expect("the namespace stands"); + let nvs = || kernel.asked.borrow().iter().filter(|access| access.write == 1 && access.address == 0x7700_0000).count(); + let before = nvs(); + server.deliver([("\\_SB_".to_string(), 0x08)].into()); + assert_eq!(nvs(), before + 2, "the platform's _OSC queried and committed again"); + } + + /// What a Sleep took of the SCI is served after its evaluation: the + /// controller's queries it found waiting are drained and counted, and + /// the Sleep served the controller's GPE. + #[test] + fn what_a_sleep_took_is_served_after_its_evaluation() { + use crate::common::{cat, method}; + let sleeps = method("SLP", 0, &[0x5B, 0x22, 0x01]); + let kernel = crafted(&sealed(b"DSDT", &cat(&[&crate::devices::tests::laptop(&[]), &sleeps])), &[]); + *kernel.controller.borrow_mut() = Some(crate::ec::tests::Emulated::new([0; 256])); + let (mut server, names) = served(&kernel, FIXED_POWER_BUTTON); + server.arm_controller(names.as_ref()); + kernel.controller.borrow_mut().as_mut().expect("a controller").queries.push(0x1C); + kernel.sleeps.borrow_mut().push_back(sci::Latched { presses: 0, ec: true }); + let aml = server.aml.as_mut().expect("the namespace"); + aml.interpreter.evaluate(&mut aml.host, "\\SLP_", &[]).expect("SLP runs"); + assert_eq!(*kernel.slept.borrow(), [Some(3)], "the Sleep did not serve the controller's GPE"); + assert_eq!(server.counts.get(&0x1C), None); + server.served_aml(); + assert_eq!(server.counts.get(&0x1C), Some(&1), "the query a Sleep found waiting was not drained"); + } + + /// A press a table's Sleep took during the load powers the machine off + /// before the namespace is initialised: the `_INI` whose Acquire of the + /// Global Lock has no bound, while the firmware holds it past + /// [`host::RELEASE`], is never reached. + #[test] + fn a_press_latched_during_the_load_powers_off_before_an_ini_that_waits_on_the_firmware() { + use crate::common::{cat, device, method, scope}; + use crate::host::tests::POWERED_OFF; + use crate::host::Take; + // Sleep (One); Device (\_SB.GLI) { Method (_INI) { Acquire (\_GL, 0xFFFF) Release (\_GL) } }. + let unbounded = cat(&[&[0x5B, 0x23], b"\\_GL_", &[0xFF, 0xFF], &[0x5B, 0x27], b"\\_GL_"]); + let dsdt = cat(&[&[0x5B, 0x22, 0x01], &scope("\\_SB", &device("GLI", &method("_INI", 0, &unbounded)))]); + let kernel = Scripted { + sleeps: RefCell::new([sci::Latched { presses: 1, ec: false }].into()), + takes: RefCell::new([Take::Pending].into()), + ..crafted(&sealed(b"DSDT", &dsdt), &[]) + }; + let info = AcpiInfo { rsdp: CRAFTED_RSDP, pm1_event: Block::NONE, gpe0: GPE0, flags: FIXED_POWER_BUTTON, reserved: [0; 3] }; + let mut server = Server::new(&kernel, Bytes::default(), info); + let (loaded, kept) = load(&kernel, CRAFTED_RSDP); + + let stopped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| server.loaded(loaded.handed, kept))); + + let why = stopped.err().map(|payload| payload.downcast_ref::().cloned().unwrap_or_default()); + assert_eq!(why.as_deref(), Some(POWERED_OFF), "the press did not power the machine off first"); + assert_eq!(*kernel.takes.borrow(), [Take::Pending], "the _INI took the Global Lock before the press was served"); + } + + /// §6.2.11.1.3: a commit sends what the query offered of the claim, and a + /// bit claimed that the query did not offer is dropped; what the commit + /// grants is read of what it sent. + #[test] + fn a_commit_sends_only_what_the_query_offered_of_the_claim() { + let kernel = osc_machine(0); + let (mut server, _) = served(&kernel, FIXED_POWER_BUTTON); + let aml = server.aml.as_mut().expect("the namespace"); + let claim = osc::Claim { uuid: osc::PLATFORM.uuid, capabilities: 0b11 }; + let answered = osc::negotiate(aml, "\\_SB_._OSC", &claim); + assert_eq!(answered, Ok(osc::Answered { query: 0x10, offered: 0b01, commit: Some((0, 0b01)) })); + let mut read = |path: &str| match aml.interpreter.evaluate(&mut aml.host, path, &[]) { + Ok(Value::Integer(v)) => v, + other => panic!("{path}: {other:?}"), + }; + assert_eq!((read("\\PQ2"), read("\\PC2")), (0b11, 0b01), "the query asks the claim, the commit sends the offer"); + } + /// The owner's ruling: a machine whose power button is a control method /// device and whose namespace was not kept stays in ACPI mode with the /// button dead. The server serves neither the button nor the controller, @@ -740,7 +1319,8 @@ mod tests { for (_, enable) in bytes(GPE0) { server.ports.out8(enable, 0); } - server.arm_controller(); + let names = server.initialise(); + server.arm_controller(names.as_ref()); assert!(!server.served.power_button, "a control-method button is served as the fixed one"); assert_eq!((server.ec.is_some(), server.served.ec_gpe), (false, None), "a controller is served with no namespace"); assert!(bytes(GPE0).all(|(_, enable)| server.ports.in8(enable) == 0), "a GPE is enabled with no namespace"); @@ -759,7 +1339,8 @@ mod tests { kernel.ports = vec![(0x80, 0)]; let (_, kept) = load(&kernel, CRAFTED_RSDP); let mut aml = kept.expect("the DSDT loads"); - let found = devices::find(&mut aml.interpreter, &mut aml.host, GPE0, control_method); + let names = crate::init::Names::read(&aml.interpreter).expect("the namespace reads"); + let found = devices::find(&mut aml.interpreter, &mut aml.host, &names, GPE0, control_method); aml.host.buttons = found.buttons; let ec = found.ec.expect("the controller"); let served = run_query(Some(&mut aml), Some(&ec), 0x28); diff --git a/system/acpiserver/src/osc.rs b/system/acpiserver/src/osc.rs new file mode 100644 index 00000000000..b81936421ea --- /dev/null +++ b/system/acpiserver/src/osc.rs @@ -0,0 +1,233 @@ +//! `_OSC` negotiated (ACPI 6.5 §6.2.11; Intel 302223-007 §2.2): `\_SB._OSC` +//! under the platform-wide UUID (§6.2.11.2), then, on an Intel processor, +//! each processor's `_OSC` under Intel's, each a query with the Query flag +//! and then a commit of what the query granted (§6.2.11.1.1, §6.2.11.1.3). +//! +//! **What ToyOS claims is one declaration a UUID** ([`PLATFORM`], +//! [`PROCESSOR`]), each bit set only where ToyOS does what the bit promises, +//! the owner's condition on `_OSI` ("the interpreter must then actually +//! support what it claims") held to `_OSC`. +//! +//! **The answer is untrusted**: a buffer of the length passed, or the +//! negotiation is refused by name; its status bits are read by name; a bit +//! granted that was not asked for is dropped, and the commit sends at most +//! what the declaration holds. A query that answers failure or an +//! unrecognised UUID is not committed. The query is made once and not +//! repeated while the platform answers Capabilities Masked, as §6.2.11.1.1 +//! recommends: a firmware that masks every set but one never ends it. +//! +//! What each negotiation answered is said: the platform's on one line, the +//! processors' counted on one, each processor's path under [`OWN`]. + +use toyos_aml::Value; + +use crate::aml::{kind, Aml}; +use crate::host::{Kernel, OWN}; +use crate::init::Names; + +/// A UUID's 16 bytes from its text, `aabbccdd-eeff-gghh-iijj-kkllmmnnoopp`, +/// as ToUUID lays them out (Table 19.41): the first three fields' bytes +/// reversed, the rest in order. +const fn uuid(text: &[u8; 36]) -> [u8; 16] { + const fn digit(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'A'..=b'F' => c - b'A' + 10, + b'a'..=b'f' => c - b'a' + 10, + _ => panic!("a UUID's digit"), + } + } + const ORDER: [usize; 16] = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]; + let mut bytes = [0u8; 16]; + let (mut at, mut i) = (0, 0); + while i < 16 { + if text[at] == b'-' { + at += 1; + } + bytes[ORDER[i]] = digit(text[at]) << 4 | digit(text[at + 1]); + at += 2; + i += 1; + } + bytes +} + +/// One UUID's claim: what OSPM passes in the second DWORD. +pub struct Claim { + pub uuid: [u8; 16], + pub capabilities: u32, +} + +/// The platform-wide capabilities (§6.2.11.2, Table 6.13) ToyOS honours: +/// none. Bit 0, the Processor Aggregator Device: no `ACPI000C` is served. +/// Bits 1 to 22: nothing reads `_PPC`, `_CPC`, `_PRS`, `_PDC` or any object +/// those bits announce support for. +pub const PLATFORM: Claim = Claim { uuid: uuid(b"0811B06E-4A27-44F9-8D60-3CBBC22E7B48"), capabilities: 0 }; + +/// A processor's capabilities on Intel (302223-007 Table 1) ToyOS honours: +/// none. Bit 0: the kernel enables HWP itself +/// (`kernel/src/arch/x86_64/control_regs.rs`), and writes no `IA32_PERF_CTL` +/// through `_PCT`. Bit 12: the kernel takes no HWP interrupt. Bits 8 and 9: +/// the kernel enters no `MWAIT` state but C1's, and reads no `_CST`. +pub const PROCESSOR: Claim = Claim { uuid: uuid(b"4077A616-290C-47BE-9EBD-D87058713953"), capabilities: 0 }; + +/// The first DWORD's bits (§6.2.11): passed, the Query flag; answered, the +/// status. +const QUERY: u32 = 1 << 0; +const FAILURE: u32 = 1 << 1; +const UNRECOGNIZED_UUID: u32 = 1 << 2; +const UNRECOGNIZED_REVISION: u32 = 1 << 3; +const MASKED: u32 = 1 << 4; + +/// The revision of both UUIDs' capabilities buffer (§6.2.11.2; 302223-007 +/// §2.2). +const REVISION: u64 = 1; + +/// A status DWORD's bits by name. +fn status(dword: u32) -> String { + let named = [(FAILURE, "failure"), (UNRECOGNIZED_UUID, "unrecognized UUID"), (UNRECOGNIZED_REVISION, "unrecognized revision"), (MASKED, "capabilities masked")]; + let set: Vec<&str> = named.iter().filter(|(bit, _)| dword & bit != 0).map(|(_, name)| *name).collect(); + let rest = dword & !(QUERY | FAILURE | UNRECOGNIZED_UUID | UNRECOGNIZED_REVISION | MASKED); + match (set.is_empty(), rest) { + (true, 0) => "nothing".into(), + (_, 0) => set.join(", "), + _ => format!("{}{}reserved bits {rest:#x}", set.join(", "), if set.is_empty() { "" } else { ", " }), + } +} + +/// What one `_OSC` answered: the query's status and grant, and the commit's +/// status and grant, where one was made. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Answered { + pub query: u32, + pub offered: u32, + pub commit: Option<(u32, u32)>, +} + +impl Answered { + fn said(&self, asked: u32) -> String { + let commit = match self.commit { + Some((status_word, granted)) => format!("the commit answered {} and granted {granted:#x}", status(status_word)), + None => "no commit was made".into(), + }; + format!("the query answered {} and offered {:#x} of {asked:#x}; {commit}", status(self.query), self.offered) + } +} + +/// One evaluation of `path` with the two DWORDs, its answer's two DWORDs. +fn evaluate(aml: &mut Aml<'_, K>, path: &str, uuid: &[u8; 16], first: u32, second: u32) -> Result<[u32; 2], String> { + let capabilities = [first.to_le_bytes(), second.to_le_bytes()].concat(); + let args = [Value::Buffer(uuid.to_vec()), Value::Integer(REVISION), Value::Integer(2), Value::Buffer(capabilities)]; + match aml.interpreter.evaluate(&mut aml.host, path, &args) { + Ok(Value::Buffer(answer)) if answer.len() == 8 => { + let dword = |at: usize| u32::from_le_bytes(answer[at..at + 4].try_into().expect("four bytes")); + Ok([dword(0), dword(4)]) + } + Ok(Value::Buffer(answer)) => Err(format!("its answer is {} bytes, where 8 were passed (§6.2.11)", answer.len())), + Ok(_) => Err("its answer is no Buffer (§6.2.11)".into()), + Err(why) => { + println!("{OWN}{path} was refused {why:x?}"); + Err(kind(&why)) + } + } +} + +/// `path`'s `_OSC` negotiated under `claim`: the query, and the commit of +/// what it granted that `claim` asked for. +pub fn negotiate(aml: &mut Aml<'_, K>, path: &str, claim: &Claim) -> Result { + let [query, offered] = evaluate(aml, path, &claim.uuid, QUERY, claim.capabilities)?; + let offered = offered & claim.capabilities; + if query & (FAILURE | UNRECOGNIZED_UUID) != 0 { + return Ok(Answered { query, offered, commit: None }); + } + let [commit, granted] = evaluate(aml, path, &claim.uuid, 0, offered)?; + Ok(Answered { query, offered, commit: Some((commit, granted & offered)) }) +} + +/// What the negotiation answered, for the line the server says. +#[derive(Default)] +pub struct Negotiated { + pub platform: Option>, + /// Each processor's, in walk order. + pub processors: Vec<(String, Result)>, +} + +/// `\_SB._OSC`, then each processor's `_OSC` where the processor is Intel's +/// (`intel`): every +/// `Processor` object and every `ACPI0007` device (ACPI 6.5 §8.4) that names +/// one, in the namespace's order. +pub fn negotiate_all(aml: &mut Aml<'_, K>, names: &Names, intel: bool) -> Negotiated { + let mut negotiated = Negotiated::default(); + if names.paths.contains("\\_SB_._OSC") { + let answered = negotiate(aml, "\\_SB_._OSC", &PLATFORM); + match &answered { + Ok(answered) => println!("acpiserver: \\_SB._OSC negotiated: {}", answered.said(PLATFORM.capabilities)), + Err(why) => println!("acpiserver: \\_SB._OSC was refused: {why}"), + } + negotiated.platform = Some(answered); + } + let processors: Vec = names + .objects + .iter() + .filter(|named| match named.kind { + toyos_aml::Kind::Processor => true, + toyos_aml::Kind::Device => processor_device(aml, names, &named.path), + _ => false, + }) + .filter(|named| names.paths.contains(&format!("{}._OSC", named.path))) + .map(|named| named.path.clone()) + .collect(); + if processors.is_empty() || aml.host.stopping { + return negotiated; + } + if !intel { + println!("acpiserver: {} processors' _OSC are not negotiated: the processor is not Intel's, whose interface their UUID names", processors.len()); + return negotiated; + } + for processor in processors { + let path = format!("{processor}._OSC"); + let answered = negotiate(aml, &path, &PROCESSOR); + match &answered { + Ok(answered) => println!("{OWN}{path} negotiated: {}", answered.said(PROCESSOR.capabilities)), + Err(why) => println!("{OWN}{path} was refused: {why}"), + } + negotiated.processors.push((processor, answered)); + if aml.host.stopping { + break; + } + } + let refused = negotiated.processors.iter().filter(|(_, answered)| answered.is_err()).count(); + let granted = negotiated.processors.iter().filter_map(|(_, answered)| answered.as_ref().ok()?.commit).fold(0, |all, (_, granted)| all | granted); + println!( + "acpiserver: the processors' _OSC negotiated, Intel's: {} evaluated, query then commit, {refused} refused; granted {granted:#x} of {:#x}", + negotiated.processors.len(), + PROCESSOR.capabilities + ); + negotiated +} + +/// Whether the device at `path` is a processor by its `_HID` (§8.4): +/// ACPI0007. +fn processor_device(aml: &mut Aml<'_, K>, names: &Names, path: &str) -> bool { + let hid = format!("{path}._HID"); + names.paths.contains(&hid) && aml.interpreter.evaluate(&mut aml.host, &hid, &[]).ok().as_ref().and_then(crate::devices::id).as_deref() == Some("ACPI0007") +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Table 19.41's layout, against the bytes the first DWORD of each UUID + /// and the rest read as. + #[test] + fn a_uuid_is_laid_out_as_to_uuid_lays_it_out() { + assert_eq!(PLATFORM.uuid, [0x6E, 0xB0, 0x11, 0x08, 0x27, 0x4A, 0xF9, 0x44, 0x8D, 0x60, 0x3C, 0xBB, 0xC2, 0x2E, 0x7B, 0x48]); + assert_eq!(PROCESSOR.uuid, [0x16, 0xA6, 0x77, 0x40, 0x0C, 0x29, 0xBE, 0x47, 0x9E, 0xBD, 0xD8, 0x70, 0x58, 0x71, 0x39, 0x53]); + } + + #[test] + fn a_status_is_read_by_name() { + assert_eq!(status(0), "nothing"); + assert_eq!(status(MASKED | QUERY), "capabilities masked"); + assert_eq!(status(FAILURE | UNRECOGNIZED_UUID | 1 << 9), "failure, unrecognized UUID, reserved bits 0x200"); + } +} diff --git a/system/acpiserver/src/sci.rs b/system/acpiserver/src/sci.rs index 5b228cf183a..204e03a582d 100644 --- a/system/acpiserver/src/sci.rs +++ b/system/acpiserver/src/sci.rs @@ -1,8 +1,10 @@ //! What one SCI carried: every event whose status and enable bits are both //! set, read off the PM1 event block and the GPE0 block, and refused by name //! where it is one this server never enabled; what a press is whose -//! power-off came back refused ([`unstopped`]); and the wait for the -//! firmware's release of the Global Lock ([`await_release`]). +//! power-off came back refused ([`unstopped`]); the wait for the firmware's +//! release of the Global Lock ([`await_release`]); and the wait an +//! evaluation's Sleep makes, which takes a press and the controller's GPE as +//! they latch ([`await_sleep`]). use std::time::Instant; @@ -27,12 +29,25 @@ pub enum Event { PowerButton, /// The embedded controller's GPE. Ec, + /// A runtime GPE, whose `\_GPE._Lxx` or `_Exx` serves it. + Gpe(u16), +} + +/// How a GPE's status is cleared around its method (ACPI 6.5 §5.6.4): an +/// edge's before it runs, a level's once it has. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Trigger { + Edge, + Level, } /// What this server enabled, and so serves. +#[derive(Default)] pub struct Served { pub power_button: bool, pub ec_gpe: Option, + /// Each runtime GPE enabled, and its method's trigger. + pub gpes: std::collections::BTreeMap, } /// An event enabled and set that this server never enabled: the firmware, or @@ -61,6 +76,7 @@ pub fn events(served: &Served, pm1: (u16, u16), gpe0: &[(u8, u8)]) -> Result Event::Ec, + n if served.gpes.contains_key(&n) => Event::Gpe(n), n => return Err(Unserved::Gpe(n)), }); } @@ -81,6 +97,8 @@ pub trait Fixed { fn status(&mut self) -> Bits; /// Write `bits` to PM1 status, which clears each one set (Table 4.13). fn pm1_clear(&mut self, bits: u16); + /// Clear GPE `n`'s status, a one written to its bit (§4.8.4.1). + fn gpe_clear(&mut self, n: u16); fn enables(&mut self) -> Bits; fn enable(&mut self, enables: &Bits); /// Waits for the SCI until `until` and takes its record: `false` is the @@ -127,6 +145,55 @@ pub fn await_release(fixed: &mut impl Fixed, until: Instant) -> bool { released } +/// What latched while an evaluation slept, taken off the hardware. +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] +pub struct Latched { + /// Presses of the fixed power button. + pub presses: u32, + /// The controller's GPE: its queries are waiting to be drained. + pub ec: bool, +} + +/// Waits until `until`, an evaluation's Sleep (ACPI 6.5 §19.6.125), serving +/// the SCI meanwhile: a press of the fixed power button where `served` +/// serves it and the controller's GPE where it is served are taken, their +/// status cleared, and handed back; anything else that latches keeps its +/// status for whoever serves it after, and loses its enable until the wait +/// ends, so the level SCI it raised falls. The wait lasts until `until` +/// whatever latches. +pub fn await_sleep(fixed: &mut impl Fixed, until: Instant, served: &Served) -> Latched { + let kept = fixed.enables(); + let mut enabled = kept.clone(); + let mut latched = Latched::default(); + loop { + let status = fixed.status(); + if served.power_button && status.pm1 & enabled.pm1 & PWRBTN != 0 { + fixed.pm1_clear(PWRBTN); + latched.presses += 1; + } + let ec = served.ec_gpe.filter(|&n| { + let (byte, bit) = (usize::from(n / 8), n % 8); + status.gpe0.get(byte).zip(enabled.gpe0.get(byte)).is_some_and(|(s, e)| s & e & 1 << bit != 0) + }); + if let Some(n) = ec { + fixed.gpe_clear(n); + latched.ec = true; + } + let status = fixed.status(); + enabled.pm1 &= !status.pm1; + for (enable, status) in enabled.gpe0.iter_mut().zip(&status.gpe0) { + *enable &= !status; + } + fixed.enable(&enabled); + fixed.ack(); + if !fixed.sci(until) { + break; + } + } + fixed.enable(&kept); + latched +} + /// SCIs in a row that carried no event this server serves, past which the /// line is a storm and not a machine with something to say. pub const EMPTY_SCIS: u32 = 64; @@ -157,7 +224,7 @@ mod tests { use super::*; fn served() -> Served { - Served { power_button: true, ec_gpe: Some(0x6e) } + Served { power_button: true, ec_gpe: Some(0x6e), ..Default::default() } } #[test] @@ -239,6 +306,9 @@ mod tests { fn pm1_clear(&mut self, bits: u16) { self.status &= !bits; } + fn gpe_clear(&mut self, n: u16) { + self.gpe0[usize::from(n / 8)] &= !(1 << (n % 8)); + } fn enables(&mut self) -> Bits { self.enables.clone() } @@ -300,6 +370,28 @@ mod tests { assert_eq!(block.status, PWRBTN); } + /// A Sleep serves the SCI to its deadline: each press and the + /// controller's GPE are taken and cleared as they latch, a runtime GPE + /// that latches keeps its status and loses its enable until the wait + /// ends, and every enable is put back. + #[test] + fn a_sleep_takes_presses_and_the_controllers_gpe_and_leaves_the_rest_latched() { + // The block's controller is GPE 6 (byte 0's 0x40) and a runtime + // GPE is 16 (byte 2's 0x01). + let served = Served { power_button: true, ec_gpe: Some(6), ..Default::default() }; + let mut block = Block::new(&[Set::Pm1(PWRBTN), Set::Gpe(0, 0x40), Set::Gpe(2, 0x01), Set::Pm1(PWRBTN)]); + let kept = block.enables.clone(); + let latched = await_sleep(&mut block, Instant::now(), &served); + assert_eq!(latched, Latched { presses: 2, ec: true }); + assert_eq!((block.status, block.gpe0.clone()), (0, vec![0, 0, 0x01]), "a press or the controller's GPE left latched, or the runtime GPE taken"); + assert_eq!(block.enables, kept, "an enable was not put back"); + assert_eq!(block.sets, 4, "the wait ended before its deadline"); + // A machine whose button is a control method device takes no press. + let mut block = Block::new(&[Set::Pm1(PWRBTN)]); + let latched = await_sleep(&mut block, Instant::now(), &Served { power_button: false, ..served }); + assert_eq!((latched, block.status), (Latched::default(), PWRBTN)); + } + #[test] fn only_a_press_on_a_machine_handed_no_sleep_type_is_dropped() { let no_sleep_type = Refused::Kernel(SyscallError::NotSupported); diff --git a/tests/common/power.rs b/tests/common/power.rs index 61ff0aa316e..dc5cb437d05 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -650,43 +650,3 @@ pub fn usb_reset_on_metal(arms: &[&super::metal::Readback]) -> Result<(), String /// What `/system/bin/acpiserver` says once it serves q35's power button, /// whole, before it loads the tables. const ACPI_ARMED: &str = "acpiserver: armed: power button served"; - -/// What `/system/bin/acpiserver` says of each definition block, after -/// `acpiserver: `: its place, how many there are, which it is, and what -/// became of it. -const ACPI_TABLE: &str = "acpiserver: table "; - -/// What it says once the kernel has kept `\_S5`'s `SLP_TYPa`, ahead of it. -const ACPI_S5_HANDED: &str = "acpiserver: \\_S5 handed to the kernel: "; - -/// The number after `SLP_TYPa=` on a line. -fn slp_typ_a(line: &str) -> Result { - line.split_once("SLP_TYPa=") - .and_then(|(_, after)| after.split(|c: char| !c.is_ascii_digit()).next()) - .and_then(|digits| digits.parse().ok()) - .ok_or_else(|| format!("no SLP_TYPa on {line:?}")) -} - -/// The server loaded every definition block it found, the DSDT first, each -/// said on a line in its place; and the `SLP_TYPa` its `\_S5` evaluates to, -/// which it handed the kernel, and the PM1a control block the kernel says it -/// powers off on with it, are the ones `supplied` holds for this machine: the -/// kernel's line whole to the value, read some way that is not this server's -/// evaluation. Answers how many blocks there were. -pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial, supplied: &str) -> Result { - let said: Vec<&str> = log.text().lines().filter_map(|line| line.split_once(ACPI_TABLE).map(|(_, said)| said.trim())).collect(); - let Some(count) = said.first().and_then(|first| first.split_once(" of ")?.1.split_once(' ')?.0.parse::().ok()) else { - return Err(format!("the server said nothing of a first table ({said:?}):\n{}", log.text())); - }; - let expected: Vec = - (1..=count).map(|place| format!("{place} of {count} ({}) loaded", if place == 1 { "DSDT" } else { "SSDT" })).collect(); - if said != expected { - return Err(format!("the server's tables are {said:#?}, where {count} loaded ones are {expected:#?}")); - } - let (handed, kept) = (log.must_say(ACPI_S5_HANDED)?, kernel.must_say(supplied)?); - if slp_typ_a(handed)? != slp_typ_a(supplied)? { - return Err(format!("the server's \\_S5 is not this machine's: {:?} beside {supplied:?}", handed.trim())); - } - eprintln!(" [power] {count} table(s) loaded; {} beside {}", handed.trim(), kept.trim()); - Ok(count) -} diff --git a/tests/guest/rust-tests/Cargo.toml b/tests/guest/rust-tests/Cargo.toml index 8e4439523d4..1006b8590fc 100644 --- a/tests/guest/rust-tests/Cargo.toml +++ b/tests/guest/rust-tests/Cargo.toml @@ -15,7 +15,7 @@ toyos-inspect = { path = "../../../lib/inspect" } # wait on the log being quiet. toyos-logstream = { path = "../../../lib/logstream" } logkeeper-api = { path = "../../../lib/logkeeper-api" } -# The ACPI server's count line, for `acpi_hold`'s wait on it. +# The ACPI server's lines, for `acpi_hold`'s and `acpi_counted`'s waits on them. acpiserver-api = { path = "../../../lib/acpiserver-api" } # The diary's decoder, for `trace_read` and `trace_flood`. toyos-trace = { path = "../../../lib/trace" } diff --git a/tests/guest/rust-tests/src/bin/acpi_counted.rs b/tests/guest/rust-tests/src/bin/acpi_counted.rs new file mode 100644 index 00000000000..cefea7a8eee --- /dev/null +++ b/tests/guest/rust-tests/src/bin/acpi_counted.rs @@ -0,0 +1,27 @@ +//! Wait until `/system/bin/acpiserver` has logged a count of the embedded +//! controller's queries, and exit non-zero where the runner's own bound comes +//! near first. +//! +//! The `acpi_server_events` metal row runs it among its boot's rows' jobs: its +//! judge reads that line, which the server writes only once one of its count +//! intervals has passed in which the controller raised a query, so it is that +//! row's premise alone and holds no other row's boot. + +use std::time::Duration; + +use toyos_logstream::program_line; + +#[path = "../served_log.rs"] +mod served_log; + +/// How long after boot the wait gives up: the runner's bound less a tenth. +const UNTIL_MS: u64 = toyos_tco::JOB_BOUND_MS - toyos_tco::JOB_BOUND_MS / 10; + +fn main() { + let since_boot = toyos_abi::clock::nanos_since_boot() / 1_000_000; + let bound = Duration::from_millis(UNTIL_MS.saturating_sub(since_boot)); + served_log::Log::open().until("the ACPI server's count of its controller's queries", bound, |line| { + program_line(line).is_some_and(|said| said.tag == "acpiserver" && said.text.contains(acpiserver_api::QUERIES_COUNTED)) + }); + println!("acpi_counted: the ACPI server's count is in the log, and nothing stopped the machine"); +} diff --git a/tests/guest/rust-tests/src/bin/acpi_hold.rs b/tests/guest/rust-tests/src/bin/acpi_hold.rs index f0f0634e9b1..ce0e9032d94 100644 --- a/tests/guest/rust-tests/src/bin/acpi_hold.rs +++ b/tests/guest/rust-tests/src/bin/acpi_hold.rs @@ -1,11 +1,12 @@ -//! Hold the boot open until `/system/bin/acpiserver` has logged a count of the -//! embedded controller's queries, and exit non-zero where the runner's own -//! bound comes near first. +//! Hold the boot open until `/system/bin/acpiserver` has said its power +//! sources, the last line of its start, and exit non-zero where the runner's +//! own bound comes near first. //! -//! The `acpi_server_events` metal row runs it, last in its boot: its judge -//! reads that line, which the server writes only once one of its count -//! intervals has passed, and a boot whose other jobs end sooner does not reach -//! it. +//! The rows on `testcases` that read the server's start end their boot on it: +//! the server writes that line on every machine with a battery once its +//! tables, its namespace's initialisation, its `_OSC` and its first reading +//! are done, where a count of the controller's events waits on an event an +//! idle machine may never raise. use std::time::Duration; @@ -20,8 +21,8 @@ const UNTIL_MS: u64 = toyos_tco::JOB_BOUND_MS - toyos_tco::JOB_BOUND_MS / 10; fn main() { let since_boot = toyos_abi::clock::nanos_since_boot() / 1_000_000; let bound = Duration::from_millis(UNTIL_MS.saturating_sub(since_boot)); - served_log::Log::open().until("the ACPI server's count of its controller's queries", bound, |line| { - program_line(line).is_some_and(|said| said.tag == "acpiserver" && said.text.contains(acpiserver_api::QUERIES_COUNTED)) + served_log::Log::open().until("the ACPI server's power sources", bound, |line| { + program_line(line).is_some_and(|said| said.tag == "acpiserver" && said.text.starts_with(acpiserver_api::POWER_SOURCES)) }); - println!("acpi_hold: the ACPI server's count is in the log, and nothing stopped the machine"); + println!("acpi_hold: the ACPI server's power sources are in the log, and nothing stopped the machine"); } diff --git a/tests/guest/rust-tests/src/bin/acpi_mediated.rs b/tests/guest/rust-tests/src/bin/acpi_mediated.rs index 7f2ebb78af1..70ccf59067f 100644 --- a/tests/guest/rust-tests/src/bin/acpi_mediated.rs +++ b/tests/guest/rust-tests/src/bin/acpi_mediated.rs @@ -251,7 +251,7 @@ fn memory(holder: &Holder, info: &AcpiInfo) { let (hole, ty) = holder.ask(Access::write(Space::SystemMemory, 0xD000_0000, Width::DWord, 0)); assert_eq!((hole, ty), (Err(Refused::MemoryType), UNLISTED), "acpi: an unlisted address was written"); let (hole, ty) = holder.ask(Access::read(Space::SystemMemory, 0xA_0000, Width::DWord)); - assert_eq!((hole, ty), (Err(Refused::UnlistedCached), UNLISTED), "acpi: an unlisted address below 1 MiB"); + assert_eq!((hole, ty), (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 @@ -268,7 +268,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 UnlistedCached, and the interrupt controllers, the HPET and a function's BAR DeviceMemory"); + 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"); // Non-volatile memory, both ways: the FACS, and the bytes after it. let fadt = holder.table(info.rsdp, b"FACP"); diff --git a/tests/toyos.rs b/tests/toyos.rs index 6cd2b717dcb..564a66606b8 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -152,10 +152,14 @@ const RUST_SKIP: &[&str] = &[ // a boot that starts none: the `acpi_server_death` metal row runs it on // tests/images/acpicase.toml. "acpi_release", + // It waits for the ACPI server's power sources, and no guest's machine + // has a battery: the `acpi_tables_loaded`, `acpi_server_events` and + // `battery_read` metal rows end their boot on it. + "acpi_hold", // It waits for the ACPI server's count of its embedded controller's // queries, and no guest's machine has that controller: the // `acpi_server_events` metal row runs it. - "acpi_hold", + "acpi_counted", // It claims the fixed hardware itself, which needs a boot that starts no // server, and stages the firmware's side of the Global Lock and finds the // i8042's row another claim's, which need the test kernel and its @@ -493,11 +497,11 @@ const METAL: &[(&str, metal::Metal)] = &[ ( // The ACPI server on the machine its stage is for: the row the kernel // filled from the T14's own tables, the server armed on it, and the - // embedded controller's events taken and counted, on a boot held open + // embedded controller's events taken and counted, its job waiting // past the server's count interval. "acpi_server_events", metal::Metal { - arms: TESTCASES_HELD, + arms: &[metal::Arm { jobs: &["test_rs_acpi_counted"], ..TESTCASES_HELD[0] }], judge: |b| acpi_events_on_metal(b[0]), }, ), @@ -515,7 +519,8 @@ const METAL: &[(&str, metal::Metal)] = &[ // The T14's battery and AC adapter as the server read them through // its AML and its embedded controller: `battery_on_metal` says what // is held, and prints the readings for Linux's on the same machine. - // The same boot as `acpi_server_events`, held past several polls. + // The same boot as `acpi_server_events`, held to the server's power + // sources, which follow the first reading. "battery_read", metal::Metal { arms: TESTCASES_HELD, @@ -1006,10 +1011,10 @@ const TESTCASES: &[metal::Arm] = &[metal::once( ], )]; -/// The same boot, its rows ended on the hold: the count it waits for comes -/// thirty seconds after the server arms, and a row's job behind it would wait -/// that out too. The members run behind it: the hold gives up at a time since -/// boot, which a list of members before it would spend. +/// The same boot, its rows ended on the hold, which waits for the ACPI +/// server's power sources, the last line of its start. The members run +/// behind it: the hold gives up at a time since boot, which a list of +/// members before it would spend. const TESTCASES_HELD: &[metal::Arm] = &[metal::Arm { last: Some("test_rs_acpi_hold"), ..metal::once("testcases", "tests/images/testcases.toml", &[], &[]) @@ -1776,7 +1781,7 @@ 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 UnlistedCached, and the interrupt controllers, the HPET and a function's BAR DeviceMemory", + "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: 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", @@ -4708,6 +4713,7 @@ fn counters_on_metal(back: &metal::Readback) -> Result<(), String> { /// `GPE=0x6e`), at least one of its queries taken and a count of them /// logged, and no guard of the server's fired. fn acpi_events_on_metal(back: &metal::Readback) -> Result<(), String> { + back.job_passed("test_rs_acpi_counted")?; back.job_passed("test_rs_acpi_hold")?; let (log, kernel) = (back.log(), back.kernel()); let enabled = kernel.must_say("acpi: ACPI mode: ACPI_ENABLE 0xf0 written to SMI_CMD 0xb2 ")?; @@ -4727,7 +4733,13 @@ fn acpi_events_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 firsts: Vec<&&str> = lines.iter().filter(|l| l.contains("taken for the first time")).collect(); + if let Some(refused) = lines.iter().find(|l| l.contains("method was refused, so the GPE is served no more")) { + return Err(format!("a runtime GPE's method was refused: {refused}")); + } + for gpe in lines.iter().filter(|l| l.contains("acpiserver: GPE ") && l.contains(" taken for the first time; its method ran")) { + eprintln!(" [acpi] {}", gpe.trim()); + } + let firsts: Vec<&&str> = lines.iter().filter(|l| l.contains("query") && l.contains("taken for the first time")).collect(); let counts = lines.iter().rfind(|l| l.contains(acpiserver_api::QUERIES_COUNTED)); let (true, Some(counts)) = (!firsts.is_empty(), counts) else { return Err(format!("the server logged {} first sighting(s) and {counts:?} for counts", firsts.len())); @@ -4739,34 +4751,140 @@ fn acpi_events_on_metal(back: &metal::Readback) -> Result<(), String> { Ok(()) } -/// How many definition blocks the T14 has: Linux on the same machine says -/// `14 ACPI AML tables successfully acquired and loaded`. -const T14_DEFINITION_BLOCKS: usize = 14; - -/// The kernel's line for the sleep type its server handed it, whole to the -/// value, on the T14: that machine's PM1a control block, and its `\_S5`'s -/// `SLP_TYPa` as a byte scan of its DSDT reads it, with no interpreter. -const T14_S5_SUPPLIED: &str = "power: S5 is PM1a 0x1804 with SLP_TYPa=7,"; - -/// The server's load of the T14's tables, every access the kernel's to make -/// for it: all of the machine's definition blocks, as many as Linux loads -/// there, each fetched through `SYS_ACPI`, summing to zero as its firmware -/// sealed it, and loaded; `\_S5`'s `SLP_TYPa` handed to the kernel, which -/// says it powers this machine off with that value on its own PM1a block, -/// both as [`T14_S5_SUPPLIED`] holds them, and never does on this row; and -/// nothing refused, so no bridge answered -/// what the interpreter refuses, no address was `Unmapped`, and no access -/// the load makes is one the policy keeps from it. The load's AML read -/// memory, read configuration space and took the Global Lock, which is the -/// real lock word exchanged and given back each time. Every other CPU of the -/// machine said how its range registers stand beside the boot processor's, -/// which typed the load's unlisted read a register's, and none holds registers -/// that are on and not those. What it prints beside -/// that is the first measurement of each: the load's time, the reads by -/// address space, the takes that found the firmware holding the lock, and -/// the pages of memory by the type the firmware's map gives them. +/// What one bench's tables are, as the check outside the tree read them on +/// their dump and the bench's own boots on this row (#875) found them. +struct Bench { + /// The kernel's line for the ACPI row the FADT names, to its SCI. + row: &'static str, + /// How many definition blocks the XSDT and the FADT name. + blocks: usize, + /// Each block refused, by its place and its refusal's kind: a firmware + /// defect the tracker records. + refused: &'static [(usize, &'static str)], + /// The kernel's line for the sleep type the server handed it, whole to + /// the value, read some way that is not this server's evaluation. + s5: &'static str, + /// 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}")); + } + Ok(()) + } +} + +/// 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`). +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)", +}; + +/// 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`). +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", +}; + +/// The server's load of a bench's tables, every access the kernel's to make +/// for it, read in the log up to the server's power sources line, which the +/// boot was held for: each of the machine's definition blocks, fetched through `SYS_ACPI`, summing to zero as its firmware sealed +/// 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` +/// 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 +/// registers that are on and not those. What it prints beside that is the +/// first measurement of each: the load's time, the reads by address space, +/// the takes that found the firmware holding the lock and its longest hold, +/// the pages of memory by type, the longest wait an evaluation asked, the +/// GPEs enabled, the interpreter's heap and the namespace's slots. fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { - let (log, kernel) = (back.log(), back.kernel()); + back.job_passed("test_rs_acpi_hold")?; + let (whole, kernel) = (back.log(), back.kernel()); + let text = whole.text(); + let at = text.find(acpiserver_api::POWER_SOURCES).ok_or("the server never said its power sources, which the hold waited for")?; + let end = text[at..].find('\n').map_or(text.len(), |n| at + n); + let log = serial::Serial::named("the log up to the server's power sources", &text[..end]); let lines: Vec<&str> = log .text() .lines() @@ -4775,10 +4893,26 @@ fn acpi_tables_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 blocks = power::acpi_tables_loaded(&log, &kernel, T14_S5_SUPPLIED)?; - if blocks != T14_DEFINITION_BLOCKS { - return Err(format!("the server found {blocks} definition blocks where Linux loads {T14_DEFINITION_BLOCKS}")); + let bench = [T14, AMD].into_iter().find(|bench| kernel.text().contains(bench.row)).ok_or("no bench of this row's names its ACPI row so")?; + let said: Vec<&str> = lines.iter().filter_map(|l| l.split_once("acpiserver: table ").map(|(_, said)| said.trim())).filter(|said| said.contains(" of ")).collect(); + let expected: Vec = (1..=bench.blocks) + .map(|place| { + let name = if place == 1 { "DSDT" } else { "SSDT" }; + match bench.refused.iter().find(|(at, _)| *at == place) { + Some((_, kind)) => format!("{place} of {} ({name}) refused: {kind}", bench.blocks), + None => format!("{place} of {} ({name}) loaded", bench.blocks), + } + }) + .collect(); + if said != expected { + return Err(format!("the server's tables are {said:#?}, where this bench's are {expected:#?}")); } + let handed = log.must_say("acpiserver: \\_S5 handed to the kernel: ")?; + let kept = kernel.must_say("power: S5 is PM1a ")?; + if !kept.contains(bench.s5) { + return Err(format!("the kernel keeps {kept:?}, where this bench's \\_S5 is {:?}", bench.s5)); + } + eprintln!(" [acpi] {}; {}", handed.trim(), kept.trim()); let others = number_between(kernel.text(), "SMP: ", " of ")? - 1; let compared: Vec<&str> = kernel.text().lines().filter(|l| l.contains("mtrr: cpu") && l.contains("'s range registers are ")).collect(); if compared.len() as u64 != others || compared.iter().any(|l| l.contains("are on and not the boot processor's")) { @@ -4788,20 +4922,54 @@ fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { } let off = compared.iter().filter(|l| l.contains("range registers are off")).count(); eprintln!(" [acpi] {others} other CPUs' range registers compared with the boot processor's: {off} off, none on and different"); - let refused: Vec<&&str> = lines.iter().filter(|l| l.contains("acpiserver: refused") || l.contains(" refused: ")).collect(); + 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)) + .collect(); if !refused.is_empty() { return Err(format!("the server refused something of this machine's AML: {refused:#?}")); } - let took = log.must_say(&format!("acpiserver: {blocks} of {blocks} tables loaded in "))?; + 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 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 !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)); + } + log.must_say("acpiserver: \\_SB._OSC negotiated: ")?; + if bench.loaded.is_empty() { + if let Some(processors) = lines.iter().find(|l| l.contains("processors' _OSC")) { + return Err(format!("this bench's processors name no _OSC: {processors}")); + } + } else { + let processors = log.must_say("acpiserver: the processors' _OSC negotiated, Intel's: ")?; + if !processors.contains(", 0 refused;") { + return Err(format!("a processor's _OSC was refused: {processors}")); + } + eprintln!(" [acpi] {}", processors.trim()); + } + let loads: Vec<&str> = lines.iter().filter_map(|l| l.split_once("acpiserver: a table loaded at run time: ").map(|(_, said)| said.trim())).collect(); + if loads != bench.loaded { + return Err(format!("the tables loaded at run time are {loads:?}, where this bench's claims load {:?}", bench.loaded)); + } + let blocks = bench.blocks - bench.refused.len(); + let took = log.must_say(&format!("acpiserver: {blocks} of {} tables loaded in ", bench.blocks))?; let bytes = log.must_say("acpiserver: the tables' bytes took ")?; let aml = log.must_say("acpiserver: the tables' AML read SystemMemory ")?; let memory = number_between(aml, "AML read SystemMemory ", " times, SystemIO ")?; - let config = number_between(aml, " and PCI_Config ", ", its memory in pages: ")?; let takes = number_between(aml, "; took the Global Lock ", " times, ")?; - if memory == 0 || config == 0 || takes == 0 { - return Err(format!("this machine's tables read memory and configuration space and take the Global Lock as they load, and the server's did not: {aml}")); + if bench.blocks == T14.blocks && (memory == 0 || takes == 0) { + return Err(format!("this machine's tables read memory and take the Global Lock as they load, and the server's did not: {aml}")); } - for line in [took, bytes, aml] { + let size = log.must_say(" table(s) loaded at run time and 0 refused; the namespace holds ")?; + let gpes = log.must_say("acpiserver: GPEs: ")?; + for line in [took, bytes, aml, census, size, gpes] { eprintln!(" [acpi] {}", line.trim()); } Ok(()) @@ -4814,9 +4982,10 @@ 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; every method -/// answered. The census's Global Lock counts are printed and held to -/// nothing: an evaluation gives back the lock it still holds as it ends +/// 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. +/// 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 /// word's exchange is `acpi_tables_on_metal`'s to hold. The lines are printed /// whole: their numbers are what Linux's `/sys/class/power_supply` reads on @@ -4832,10 +5001,15 @@ 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: ")).collect(); + let refused: Vec<&&str> = lines + .iter() + .filter(|l| l.contains("acpiserver: refused") || l.contains(" refused: ")) + .filter(|l| !T14.ruled.says(l)) + .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();