From 9819ecb014b9d31df649869ee0015b5364688616 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 09:55:20 +0200 Subject: [PATCH 01/30] issues: the AML track's finish, and its slices in order The owner's "finish aml then continue working on deep idle states" (#870, comment 6119715385) puts the interpreter ahead of deep idle states. The track now says what finished is, held to ACPI 6.5 and to Intel's 302223-007 for a processor's _OSC, and the six slices that reach it in order, each with its exit: Load and _OSC first, which deep idle waits on. The design, its citations and its roast are #875's body. The track is assigned, S1 held by #875, and its Load bullet now names its owner. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...ine-in-acpi-mode-and-interprets-its-aml.md | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) 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 8756debd7a..7500698f9e 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 --- @@ -189,7 +189,8 @@ otherwise pay to find again: 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. + loaded that way; the interpreter refuses both as unsupported. Owner: S1, + below, for `Load`, and S4 for `LoadTable`. **Exit**: theirs. - **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 @@ -449,3 +450,89 @@ 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: + - every block loads, and no method is refused; + - under Linux's capabilities as the firmware recorded them, the blocks Linux + loaded come out; + - each `_STA`, `_CST` and battery reading equals Linux's on the same machine. + +Not in finished: + +- concurrent method execution, which §5.5.2 permits and does not require; +- the PCI host bridges' `_OSC`; +- sleep states; +- `_PDC`, which Intel deprecated in favour of `_OSC` in ACPI 3.0 and which + ACPI 6.5 does not define. + +The slices come in this order, each its own pull request. S1 is held by #875 +(`wt/toyos-aml`). Before it is built, a check outside the tree reads both +benches' tables, their NVS and Linux's readings, which the orchestrator dumps +from their Ubuntu installs. + +- **S1: `Load`, and `_OSC` negotiated.** + - `Load` per §19.6.76: its objects outlive the method that loaded it, and + its bytes come only from memory Table 15.6 calls reserved or NVS. + - `\_SB._OSC` and each Intel processor's `_OSC`, query then commit. The + capability set holds only what ToyOS honours, which today is nothing. + - Takes up the `Load` bullet above, and the arena's capacity in the heap + bullet. + - **Exit**: + - host tests of `Load` from a region, a field and a buffer; + - outside the tree, under Linux's capabilities as the T14's NVS recorded + them, the blocks Linux lists, byte for byte; + - `acpi_tables_loaded` reads both `_OSC`s evaluated, with nothing + refused. +- **S2: what AML may reach, ruled.** + - The policy for memory the firmware types `EfiMemoryMappedIO`. + - Whether the kernel's bound stays address-only. + - Any write, outside what passes now, that a machine's measured AML makes. + - **Exit**: + `issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md`'s. +- **S3: the namespace initialised.** + - `\_SB._INI`, then the `_STA`/`_INI` walk by Table 6.67 over each block a + load added, then `_REG`. + - A root bridge without `_BBN`, found by `_HID` or `_CID`. + - `_GLK`. + - Takes up: + - the `_REG` bullet above; + - `issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md`; + - `issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md`. + - **Exit**: their exits, and every device's `_STA` on the T14 equal to + Linux's. +- **S4: the grammar whole.** + - `LoadTable`, `DataTableRegion`, and every space's protocol. + - **Exit**: + - `Error::Unsupported` deleted; + - a host test for each construct; + - both benches' tables loaded outside the tree, none refused for a + construct. +- **S5: every event served.** + - Each runtime GPE's `_Lxx` or `_Exx`, and each Notify to its owner. + - The SCI served while a table loads. + - Takes up the press-wait bullet above. + - **Exit**: that bullet's exit, and a held T14 boot reading every enabled + GPE's method run with none refused. +- **S6: the bounds measured.** + - The heap, full-interpreter, ten-second and Global Lock bullets above, + each by its own exit. From dc11b83cc62574082991d1c68c4f3473c51addf0 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 11:31:11 +0200 Subject: [PATCH 02/30] issues: the AML track lands as one pull request, in the order M1 fixed The owner accepted #875's design with "do the full aml in one pr", so the six slices are no longer six pull requests. M1, run on both benches' complete tables, fixes their order inside it. The T14's processors' _OSC read \OSYS, which \_SB.PC00._INI writes; forced to zero, it loses a block under CPPC. So the _STA/_INI walk runs before _OSC. That walk writes SMI_CMD and memory outside the map, so S2's ruling on those writes comes first. What M1 found false in the track: - no processor _OSC records in memory what it was passed, so the capabilities differential is by set, not by a value an OS recorded; - the DSDT's one LoadTable byte pair is data inside a Buffer, not an opcode, so the Load bullet is S1's alone. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...ine-in-acpi-mode-and-interprets-its-aml.md | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) 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 7500698f9e..17574469e5 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 @@ -188,9 +188,9 @@ 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. Owner: S1, - below, for `Load`, and S4 for `LoadTable`. **Exit**: theirs. + opcodes, none run while a table loads, and Linux lists eight tables loaded + that way; the interpreter refuses them as unsupported. Owner: S1, below. + **Exit**: its. - **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 @@ -473,8 +473,8 @@ ACPI Interface Specification (302223-007) for a processor's `_OSC` on Intel: - delivers every Notify. - On the T14 and the AMD bench: - every block loads, and no method is refused; - - under Linux's capabilities as the firmware recorded them, the blocks Linux - loaded come out; + - under every capability bit, the blocks Linux loaded come out: the + capabilities Linux passed a processor are in no memory a dump holds; - each `_STA`, `_CST` and battery reading equals Linux's on the same machine. Not in finished: @@ -485,10 +485,12 @@ Not in finished: - `_PDC`, which Intel deprecated in favour of `_OSC` in ACPI 3.0 and which ACPI 6.5 does not define. -The slices come in this order, each its own pull request. S1 is held by #875 -(`wt/toyos-aml`). Before it is built, a check outside the tree reads both -benches' tables, their NVS and Linux's readings, which the orchestrator dumps -from their Ubuntu installs. +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, NVS and Linux's readings (#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. - **S1: `Load`, and `_OSC` negotiated.** - `Load` per §19.6.76: its objects outlive the method that loaded it, and @@ -499,8 +501,8 @@ from their Ubuntu installs. bullet. - **Exit**: - host tests of `Load` from a region, a field and a buffer; - - outside the tree, under Linux's capabilities as the T14's NVS recorded - them, the blocks Linux lists, byte for byte; + - outside the tree, under every capability bit, the eight blocks Linux + lists, by OEM table ID and length; - `acpi_tables_loaded` reads both `_OSC`s evaluated, with nothing refused. - **S2: what AML may reach, ruled.** From d2006cc654880efd8dad0cd8916381d2a3ee86d4 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 12:07:21 +0200 Subject: [PATCH 03/30] S2: AML reads memory-mapped I/O as a register, and writes SMI_CMD and firmware memory through the kernel The owner's rulings on PR #875 (comments 6120173372 and 6121646183) settle what a machine's AML may reach. Reads. `toyos_userbound::firmware::decide` passes a read of memory the firmware's map types EfiMemoryMappedIO (UEFI type 11) on exactly the terms an unlisted register's read already passes: at or above 1 MiB, inside the direct map, typed uncacheable by the boot processor's range registers, on a machine none of whose CPUs holds registers that are on and differ, and outside every page a device the kernel knows decodes in (a driven window, any function's memory BAR, the local APIC). A write there stays refused MemoryType. The refusal of a register read the range registers would cache is renamed RegisterCached, since it now names a typed register too. The bound stays address-only: the regions a machine's tables define are known only to the holder's interpreter, which the kernel does not run. Writes. The server asks the kernel for a byte to the FADT's SMI_CMD, read through the same `toyos_acpi::fixed_hardware` decode the kernel declares the port by, which the kernel makes as a firmware call or refuses by byte and rate. Every SystemMemory write AML makes goes to the kernel, which passes NVS and reserved memory and refuses the rest by name: a write outside every range the firmware's map lists comes back MemoryType in unlisted memory, is denied and counted. Configuration writes stay denied. Checks: a host test of each arm of the MMIO read (each width, the last byte, cached, below 1 MiB, CPUs differing, unmapped, a device's page, the local APIC, an ECAM window typed MMIO, a read into RAM beside it); the server's port test gains SMI_CMD at a byte and refused at a word, and a memory-write test holds the kernel's made and refused writes and the ledger's count. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- kernel/src/arch/x86_64/acpi_mode.rs | 3 +- lib/userbound/src/firmware.rs | 32 ++-- lib/userbound/tests/firmware.rs | 61 ++++++- sdk/abi/src/acpi.rs | 21 ++- system/acpiserver/src/aml.rs | 5 + system/acpiserver/src/host.rs | 158 +++++++++++++----- .../guest/rust-tests/src/bin/acpi_mediated.rs | 4 +- tests/toyos.rs | 2 +- 8 files changed, 212 insertions(+), 74 deletions(-) diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index e6ed42d187..579d3ed614 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/userbound/src/firmware.rs b/lib/userbound/src/firmware.rs index b16215ef70..35a1a7b860 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 @@ -82,6 +86,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 +127,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 +225,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); diff --git a/lib/userbound/tests/firmware.rs b/lib/userbound/tests/firmware.rs index 508046ee1d..b84406195d 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}"), } diff --git a/sdk/abi/src/acpi.rs b/sdk/abi/src/acpi.rs index 43a2cb4817..8aa1926deb 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`, @@ -231,7 +234,7 @@ 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, diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index 2105fa4ab5..3a85bc3948 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -135,6 +135,11 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi println!("{STOPPING}"); return loaded; } + match toyos_acpi::find_table(&tables, rsdp, b"FACP", toyos_acpi::FADT_FOR_FIXED_HARDWARE).map(|fadt| toyos_acpi::fixed_hardware(&fadt)) { + Ok(Ok(fixed)) => host.smi_cmd = fixed.smi_cmd.map(|smi| smi.port), + Ok(Err(why)) => println!("acpiserver: the FADT's fixed hardware does not decode, so this machine's AML calls no firmware: {why:?}"), + Err(why) => println!("acpiserver: no FADT was read, so this machine's AML calls no firmware: {}", unread(&why, |at| tables.refused(at))), + } let count = blocks.len(); for (place, block) in blocks.iter().enumerate() { diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 71086a49a1..b3f0fe1637 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -7,13 +7,22 @@ //! 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. +//! **Only the port writes in [`FORWARDED`], and a byte to the FADT's +//! `SMI_CMD`, 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, and a call into the firmware, which the kernel makes or refuses by +//! the byte and the rate (`toyos_userbound::firmware`), the owner's "AML may +//! write exactly the SMI command port the FADT declares". Every other port +//! write is denied by name and never reaches the kernel: a reset-capable +//! port, a data register. The reset register is the kernel's to keep wherever +//! it is. +//! +//! **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 embedded controller's space is its own transactions'** ([`crate::ec`], //! [`transact`]), a byte each, on the controller the server attached @@ -257,8 +266,10 @@ pub struct Firmware<'k, K> { pub ended: Option, pub refused: Ledger, notified: Ledger, - /// The [`FORWARDED`] writes asked of the kernel, by port and width. + /// The port writes asked of the kernel, by port and width. written: Ledger, + /// The FADT's `SMI_CMD`, a byte to which is a call into the firmware. + pub smi_cmd: Option, /// The paths of the control-method power buttons this server serves. pub buttons: Vec, /// Presses of one of them notified and not yet served. @@ -283,6 +294,7 @@ impl<'k, K: Kernel> Firmware<'k, K> { refused: Ledger::default(), notified: Ledger::default(), written: Ledger::default(), + smi_cmd: None, buttons: Vec::new(), presses: 0, stopping: false, @@ -299,6 +311,22 @@ 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?}"))) + } + } + } + fn stopped(&mut self) -> Denied { self.stopping = true; Denied(Refusal::Stopping.to_string()) @@ -385,21 +413,16 @@ 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(), + Address::Io(port) if !FORWARDED.contains(&(port, width)) && (Some(port), width) != (self.smi_cmd, toyos_aml::Access::Byte) => { + UNFORWARDED.into() + } 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"); } - 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?}"), - )), - }; + return self.written_through(Space::SystemIo, u64::from(port), width, value, at); } - Address::Memory(_) => "a write to SystemMemory: this server writes no memory for AML".into(), + Address::Memory(address) => return self.written_through(Space::SystemMemory, address, width, value, at), 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"); @@ -471,11 +494,14 @@ 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)>, pub ports: Vec<(u64, u64)>, @@ -514,7 +540,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,9 +549,21 @@ 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 => match self.kept.iter().find(|(start, end, _)| (*start..*end).contains(&access.address)) { @@ -654,18 +692,22 @@ 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. + /// A port write in [`FORWARDED`], and a byte to the FADT's `SMI_CMD` once + /// the load named it, is asked of the kernel, and made or refused as it + /// answers; every other port and width, `SMI_CMD` at another width, + /// configuration space, and the controller's space with no controller + /// attached are denied by name, none of them asked of the kernel. #[test] - fn only_a_forwarded_port_write_is_asked_of_the_kernel() { + fn only_a_forwarded_port_write_or_a_byte_to_smi_cmd_is_asked_of_the_kernel() { use toyos_aml::Access::{Byte, DWord, Word}; let mut kernel = machine(); - kernel.ports = vec![(0x72, 0), (0x80, 0)]; + kernel.ports = vec![(0x72, 0), (0x80, 0), (0xB2, 0)]; let mut host = Firmware::new(&kernel); + assert_eq!(host.write(Address::Io(0xB2), Byte, 0xA0), Err(Denied(UNFORWARDED.into())), "SMI_CMD before the FADT named it"); + host.smi_cmd = Some(0xB2); assert_eq!(host.write(Address::Io(0x80), Word, 0x28), Ok(())); assert_eq!(host.write(Address::Io(0x72), Byte, 0x08), Ok(())); + assert_eq!(host.write(Address::Io(0xB2), Byte, 0xA0), Ok(())); let refused = host.write(Address::Io(0xCD6), Byte, 0x60); assert_eq!(refused, Err(Denied("a SystemIO write the kernel refused KernelPort".into()))); assert_eq!( @@ -673,30 +715,62 @@ pub mod tests { [ Access::write(Space::SystemIo, 0x80, Width::Word, 0x28), Access::write(Space::SystemIo, 0x72, Width::Byte, 0x08), + Access::write(Space::SystemIo, 0xB2, Width::Byte, 0xA0), Access::write(Space::SystemIo, 0xCD6, Width::Byte, 0x60), ] ); // 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)] - { + // reset register an AMD laptop shares with `SMI_CMD`, `SMI_CMD` at a + // width no call is, 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, Word, 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(), 4, "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(), "0x72 Byte x1; 0x80 Word x1; 0xb2 Byte x1; 0xcd6 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")); } /// The controller's space is its own transactions', one a byte, through diff --git a/tests/guest/rust-tests/src/bin/acpi_mediated.rs b/tests/guest/rust-tests/src/bin/acpi_mediated.rs index d5181bdb03..db2b18cc31 100644 --- a/tests/guest/rust-tests/src/bin/acpi_mediated.rs +++ b/tests/guest/rust-tests/src/bin/acpi_mediated.rs @@ -249,7 +249,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 @@ -266,7 +266,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 4bb3bc94a3..60ffa3f94f 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1894,7 +1894,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", From 50abe34b5007e7c14f931b634d763748b9bfc99c Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 12:08:31 +0200 Subject: [PATCH 04/30] S3: the namespace initialised, the ECDT's controller first, and the controller under its _GLK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What OSPM runs of a machine's AML once its tables load (ACPI 6.5 §6.5.1, §6.5.4), built before S1 because the T14's processors' _OSC read \OSYS, which \_SB.PC00._INI writes (#875's M1, K2). In order (`init.rs`): - `_REG(2, 1)` in each scope that declares a PCI_Config region not on a root bus naming a _BBN, which §6.5.4 alone makes always available: the kernel reads configuration space from the start, so these are announced before any _INI reaches them; - \_SB._INI, unconditionally; - every device's _STA and _INI by Table 6.67, parent before child, a Processor object a device; each _STA said under OWN; - again over whatever an _INI loaded, each new _INI by the same table. The embedded controller. The owner ruled "follow the spec" on the ECDT (#875, comment 6120208838): the controller the ECDT names is attached to AML before the first table loads (`toyos_acpi::ecdt`, which now reads EC_ID's path too), and the namespace's PNP0C09 device is served in its place once found. Its `_GLK` is honoured: 1 takes the Global Lock around each transaction, AML's and the server's own queries alike, unless AML already holds it; another value leaves the space unattached, said. Its `_REG(3, 1)` runs in each scope that declares an EmbeddedControl region, after every _INI, as the track's _REG bullet asks. The interpreter finds a PCI root bridge that names no _BBN by its _HID or a _CID, PNP0A03 or PNP0A08 (§6.1.5, §6.1.2), on bus 0; a region below no host bridge is a broken rule, refused by name. Its walk entries carry each object's place in definition order, so the server knows what a load added (`Entry::serial`, `Interpreter::defined`), and a region's space. Two issues close by their exits: QEMU 11.1.1's \_SB.LNKA._CRS evaluates to a buffer on the fixture, and a controller whose _GLK is 1 is driven by a stand-in that refuses a transaction made without the lock. A reading for the orchestrator: on QEMU, \_SB.CPUS.Cxxx._STA writes QEMU's CPU hotplug selector (SystemIO 0xcd8, a dword), which the server writes for no AML, and \_SB.HPET._STA reads the HPET the kernel drives; both are refused by name and their devices taken as absent, pinned in `qemus_tables_load_and_s5_is_what_its_kernel_decoded`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...inks-are-refused-by-the-aml-interpreter.md | 27 -- ...dded-controller-without-the-global-lock.md | 31 -- ...ine-in-acpi-mode-and-interprets-its-aml.md | 20 +- lib/acpi/src/ecdt.rs | 90 ++++ lib/acpi/src/lib.rs | 2 + lib/acpi/tests/ecdt.rs | 51 ++ system/acpiserver/aml/src/exec.rs | 12 +- system/acpiserver/aml/src/field.rs | 43 +- system/acpiserver/aml/src/lib.rs | 19 +- system/acpiserver/aml/src/namespace.rs | 12 +- system/acpiserver/aml/tests/regions.rs | 76 ++- system/acpiserver/aml/tests/walk.rs | 2 +- system/acpiserver/src/aml.rs | 114 ++++- system/acpiserver/src/battery.rs | 28 +- system/acpiserver/src/devices.rs | 56 ++- system/acpiserver/src/host.rs | 63 ++- system/acpiserver/src/init.rs | 441 ++++++++++++++++++ system/acpiserver/src/main.rs | 220 ++++++--- 18 files changed, 1115 insertions(+), 192 deletions(-) delete mode 100644 issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md delete mode 100644 issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md create mode 100644 lib/acpi/src/ecdt.rs create mode 100644 lib/acpi/tests/ecdt.rs create mode 100644 system/acpiserver/src/init.rs 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 e9ca67d37c..0000000000 --- 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 c8e6f6520b..0000000000 --- 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/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 17574469e5..daf51cc0cd 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 @@ -135,6 +135,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 @@ -516,12 +523,13 @@ memory outside the map, so S2's ruling on those writes comes before both. load added, then `_REG`. - A root bridge without `_BBN`, found by `_HID` or `_CID`. - `_GLK`. - - Takes up: - - the `_REG` bullet above; - - `issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md`; - - `issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md`. - - **Exit**: their exits, and every device's `_STA` on the T14 equal to - Linux's. + - Takes up the `_REG` bullet above. QEMU's interrupt links, refused for a + host bridge named by `_HID` alone, and the controller reached without the + Global Lock its `_GLK` asks for, are closed by their host tests + (`qemus_interrupt_link_reads_its_routing_register_below_a_bridge_named_by_its_hid`, + `a_controller_that_wants_the_global_lock_is_reached_under_it_alone`). + - **Exit**: the `_REG` bullet's, and every device's `_STA` on the T14 + equal to Linux's. - **S4: the grammar whole.** - `LoadTable`, `DataTableRegion`, and every space's protocol. - **Exit**: diff --git a/lib/acpi/src/ecdt.rs b/lib/acpi/src/ecdt.rs new file mode 100644 index 0000000000..f32b12fc73 --- /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 9a3f247958..e4ac4c995d 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, diff --git a/lib/acpi/tests/ecdt.rs b/lib/acpi/tests/ecdt.rs new file mode 100644 index 0000000000..22baebe210 --- /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/system/acpiserver/aml/src/exec.rs b/system/acpiserver/aml/src/exec.rs index 7c8f5c9dfb..0f26dc7479 100644 --- a/system/acpiserver/aml/src/exec.rs +++ b/system/acpiserver/aml/src/exec.rs @@ -312,14 +312,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 { diff --git a/system/acpiserver/aml/src/field.rs b/system/acpiserver/aml/src/field.rs index 9aef5d9079..352824e7fa 100644 --- a/system/acpiserver/aml/src/field.rs +++ b/system/acpiserver/aml/src/field.rs @@ -181,16 +181,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 +209,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 +218,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 +256,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"); diff --git a/system/acpiserver/aml/src/lib.rs b/system/acpiserver/aml/src/lib.rs index 69fc21fab7..db11633272 100644 --- a/system/acpiserver/aml/src/lib.rs +++ b/system/acpiserver/aml/src/lib.rs @@ -234,7 +234,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 +253,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 +280,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 +298,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 }) } } @@ -405,6 +410,12 @@ 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() + } + /// 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 diff --git a/system/acpiserver/aml/src/namespace.rs b/system/acpiserver/aml/src/namespace.rs index cf888b4c15..66f8a6e626 100644 --- a/system/acpiserver/aml/src/namespace.rs +++ b/system/acpiserver/aml/src/namespace.rs @@ -205,6 +205,10 @@ impl Namespace { Ok(id) } + pub(crate) fn defined(&self) -> u32 { + self.nodes.len() as u32 + } + pub(crate) fn mark(&self) -> Mark { Mark { nodes: self.nodes.len(), slots: self.nodes.capacity() } } @@ -313,9 +317,9 @@ impl Drop for Walk<'_> { } 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 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; @@ -337,7 +341,7 @@ impl Walk<'_> { } 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/regions.rs b/system/acpiserver/aml/tests/regions.rs index a56e59807d..5c5869fb26 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![]); } diff --git a/system/acpiserver/aml/tests/walk.rs b/system/acpiserver/aml/tests/walk.rs index 3fd282899f..47f07a0095 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 3a85bc3948..b0c6e3ce25 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -1,7 +1,10 @@ //! 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), and the FADT's +//! `SMI_CMD`, a byte to which is a call into the firmware, 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 @@ -36,7 +39,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. @@ -140,6 +143,7 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi Ok(Err(why)) => println!("acpiserver: the FADT's fixed hardware does not decode, so this machine's AML calls no firmware: {why:?}"), Err(why) => println!("acpiserver: no FADT was read, so this machine's AML calls no firmware: {}", unread(&why, |at| tables.refused(at))), } + early_controller(kernel, &tables, rsdp, host); let count = blocks.len(); for (place, block) in blocks.iter().enumerate() { @@ -231,6 +235,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> { @@ -279,19 +322,34 @@ 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. + /// Its initialisation runs no `_INI` and reads each device's `_STA` but + /// the CPUs', whose `_STA` writes QEMU's CPU hotplug selector, a port + /// this server writes for no AML, and the HPET's, which reads the timer + /// the kernel drives. 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 mut kernel = machine(QEMU); + // The ISA bridge's interrupt routing registers, PIRQA-D and PIRQE-H. + kernel.config = [0x60, 0x61, 0x62, 0x63, 0x68, 0x69, 0x6A, 0x6B].map(|offset| (toyos_abi::acpi::pci_address(0, 0, 0x1F, 0, offset), 0x0B)).to_vec(); + kernel.driven = vec![(0xfed0_0000, 0xfed0_1000)]; 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, 14)); + assert_eq!( + census.refused.counts(), + "_STA: denied by this server: a SystemIO write of a port and width this server asks of the kernel for no AML x2; _STA: denied by this server: a SystemMemory read the kernel refused DeviceMemory, in unlisted firmware memory x1" + ); + 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)); } @@ -325,6 +383,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]; @@ -352,6 +417,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 c1552048b6..9c0c30b917 100644 --- a/system/acpiserver/src/battery.rs +++ b/system/acpiserver/src/battery.rs @@ -559,8 +559,10 @@ pub mod tests { 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"); + crate::attach(&mut aml, ec, &names, &mut crate::init::Census::default()).expect("no port of it refused"); (aml, found) } @@ -677,19 +679,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 2e14e0d4af..6009a6560d 100644 --- a/system/acpiserver/src/devices.rs +++ b/system/acpiserver/src/devices.rs @@ -34,6 +34,7 @@ use toyos_aml::{Host, Interpreter, Kind, Value}; 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 +45,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 +68,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 +99,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 +115,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) @@ -160,23 +158,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 +241,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 +327,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 +367,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 +389,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/host.rs b/system/acpiserver/src/host.rs index b3f0fe1637..56597964aa 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -25,11 +25,13 @@ //! space is denied by name. //! //! **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. 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. //! //! **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 @@ -276,6 +278,8 @@ pub struct Firmware<'k, K> { 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, } impl<'k, K: Kernel> Firmware<'k, K> { @@ -298,9 +302,32 @@ impl<'k, K: Kernel> Firmware<'k, K> { buttons: Vec::new(), presses: 0, stopping: false, + locked: false, } } + /// 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 { @@ -337,10 +364,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)) => { @@ -464,16 +491,20 @@ impl Host for Firmware<'_, K> { 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(()) @@ -504,6 +535,8 @@ pub mod tests { 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>, @@ -517,6 +550,10 @@ 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. @@ -566,6 +603,9 @@ pub mod tests { }); 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 }, @@ -573,6 +613,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) { @@ -784,7 +827,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)); @@ -804,7 +847,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 0000000000..52edb2885a --- /dev/null +++ b/system/acpiserver/src/init.rs @@ -0,0 +1,441 @@ +//! 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 every device's `_STA` and `_INI` by Table +//! 6.67**, 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, its +//! ancestors' `_STA` asked again. +//! +//! **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, +} + +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, {}; evaluations refused, by kind: {refused}", + self.sta, + self.ini, + if reg.is_empty() { "no _REG run".to_string() } else { reg.join(", ") } + ) + } + + 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 { + match aml.interpreter.evaluate(&mut aml.host, path, args) { + 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 none of whose descendants defined an `_INI` since is not +/// asked, on a walk after a run-time load. +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) || (since > 0 && !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)")); + 0 + } + None => 0, + } + } + }; + 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)))])), + ]), + ), + &device("DEVF", &cat(&[&def_name("_STA", &int(0x0F)), &method("_INI", 0, &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; 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 takes its device as neither present nor + /// functional, and nothing below it is 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("DEVE", &method("_INI", 0, &tagged(5))), + ]), + ); + let (kernel, census) = initialised(&dsdt); + assert_eq!(posted(&kernel), [5]); + assert_eq!((census.sb_ini, census.sta, census.ini), (None, 0, 1)); + 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.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 4860aeecf0..6da7085f67 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -17,17 +17,22 @@ //! 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 the namespace is initialised** ([`init`]): PCI_Config announced +//! where §6.5.4 asks it be, `\_SB._INI`, and every device's `_STA` and +//! `_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`]): 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. @@ -58,6 +63,7 @@ mod battery; mod devices; mod ec; mod host; +mod init; mod ledger; mod sci; mod tables; @@ -80,7 +86,8 @@ 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}; +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 +116,8 @@ 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, } fn main() { @@ -123,7 +132,11 @@ fn main() { 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.initialise(); + let (batteries, adapters) = server.arm_controller(names.as_ref()); + if names.is_some() { + println!("{}", server.census.line()); + } server.find_power(batteries, adapters); server.serve(); } @@ -282,6 +295,7 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { scis: 0, empty: 0, power_off: false, + census: Census::default(), } } @@ -303,25 +317,71 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { ); } + /// 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) + } + /// 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 /// 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 +393,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 attach(aml, ec, names, &mut self.census) { + Ok(ec) => ec, + Err(why) => { + println!("{CONTROLLER_NONE}{why}"); + return sources; + } + }; if aml.host.stopping { return sources; } @@ -447,15 +510,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(()); } @@ -538,37 +601,33 @@ impl<'a, P: Ports> Server<'a, Claim<'a>, P> { } } -/// 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))), +/// 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. +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, {}", aml::kind(&why))), }; if let Some(why) = unattached { println!("acpiserver: the embedded controller's space is not attached to AML: {why}"); - return Ok(()); + return Ok(ec); } 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); - } - } - aml.host.ended.take().map_or(Ok(()), Err) + init::announce(aml, names, init::EMBEDDED_CONTROL, 0, census); + aml.host.ended.take().map_or(Ok(ec), Err) } /// What a query no method runs for is said to be. @@ -641,13 +700,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 +737,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 +781,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 +790,51 @@ 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)); + } + } + /// 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 +848,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 +868,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); From d9ca8cd38fce55dc7ccaaba31560a5acb643452b Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 12:22:09 +0200 Subject: [PATCH 05/30] S1: Load, its blocks global in a namespace of two arenas, and _OSC negotiated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Load (ACPI 6.5 §19.6.76). `Load (Object, Result) => Boolean`, encoded `LoadOp NameString Target`, from a SystemMemory region, a field unit of one at whole bytes, or a Buffer, at the root, in the DSDT's integer width. The block's header, length and checksum are checked before any of it runs, and it must lie wholly in its source. Its bytes come through a new host call, which the server answers from the kernel's reads only where every read's memory type is AddressRangeReserved or AddressRangeNVS by Table 15.6 (UEFI types 0, 5, 6, 8, 11, 12, 13 and 15 up, and 10; ACPI reclaim, 9, is neither). A refused block (malformed, colliding, failing its header, length or checksum, or denied by the host) loads false and keeps nothing; a bound of the evaluation, or Fatal, refuses the evaluation, whose steps, heap and time the block shares. Each run-time load is told to the host, which says its signature and length, and under OWN its OEM Table ID and the method that loaded it. Two arenas. §5.5.2: "Global objects are those NameSpace objects created at table load time", and a method's own objects die at its exit (§5.5.2.3). A Load inside a method interleaves the block's nodes with the method's, so one stack would unwind both: the namespace now keeps a static arena, for what definition blocks define, and a method arena, each a stack. A NodeId names its arena (the top bit of a four-byte link, so a child map's entry is no wider), a frame defines into its own, and a static object below a method's, or a static Alias of one, is refused. The walk reads the static arena; the server says its slots after the machine's last table, the track's arena bullet read rather than derived. The cost is the design's: a method that defines names grows the method arena's own slots where the single arena often had room. One heap test filled within 1.3 MB of the 16 MiB bound with a method naming 20,000 objects; measured at 13.03 MB held, it now crosses the bound, so it names 15,000 below 30,000 devices for the same claim. The arena-move charge case fills the method arena with the method's own names, since a definition in a method no longer moves the tables' arena. _OSC (§6.2.11; Intel 302223-007 §2.2). After the controller is armed, \_SB._OSC under the platform-wide UUID, then, where the processor is Intel's (CPUID leaf 0, `toyos_abi::processor_is_intel`), each Processor object's and ACPI0007 device's _OSC under Intel's, each a query and then a commit of what the query offered of what was asked. The claim is one declaration a UUID, each zero with each bit's reason (`osc.rs`). The answer is untrusted: its length must be what was passed, its status read by name, an unasked bit dropped; a query answering failure or an unrecognised UUID is not committed, and Capabilities Masked does not repeat the query, since the T14 answers it to every set but one (M1). What a commit loads is initialised by S3's walk in turn. Checks: host tests of Load from each object, from a method and at definition-block level, the method's own objects gone and the block's kept, each refusal false and keeping nothing, a block below a method's object or aliasing one refused, a bound refusing the evaluation, the DSDT's width; and a server test in which the firmware offers every bit and the commit sends none, the processor's commit loads a block from reserved memory whose device's _INI then runs, a non-Intel processor's _OSC is not evaluated, and a block in reclaim memory is refused. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- sdk/abi/src/arch/aarch64.rs | 5 + sdk/abi/src/arch/mod.rs | 7 +- sdk/abi/src/arch/x86_64.rs | 23 +++ sdk/abi/src/lib.rs | 2 +- system/acpiserver/aml/src/exec.rs | 148 ++++++++++++- system/acpiserver/aml/src/lib.rs | 58 ++++-- system/acpiserver/aml/src/namespace.rs | 241 ++++++++++++++-------- system/acpiserver/aml/tests/common/mod.rs | 46 ++++- system/acpiserver/aml/tests/heap.rs | 10 +- system/acpiserver/aml/tests/hostile.rs | 26 ++- system/acpiserver/aml/tests/load.rs | 221 ++++++++++++++++++++ system/acpiserver/src/host.rs | 66 +++++- system/acpiserver/src/main.rs | 123 ++++++++++- system/acpiserver/src/osc.rs | 233 +++++++++++++++++++++ 14 files changed, 1081 insertions(+), 128 deletions(-) create mode 100644 system/acpiserver/aml/tests/load.rs create mode 100644 system/acpiserver/src/osc.rs diff --git a/sdk/abi/src/arch/aarch64.rs b/sdk/abi/src/arch/aarch64.rs index 33304a1611..bab478eee8 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 3eb4e451eb..5c1a9f443d 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 793090ed98..8d8e1b3d94 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 6aa383ad4c..51f7fd8cb6 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 0f26dc7479..7bc40ea46d 100644 --- a/system/acpiserver/aml/src/exec.rs +++ b/system/acpiserver/aml/src/exec.rs @@ -27,13 +27,13 @@ use core::cmp::Ordering; use crate::field::{flags, BufField, 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, @@ -57,6 +57,11 @@ pub(crate) struct Frame { 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 +71,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 +92,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, } } } @@ -301,7 +315,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 { @@ -412,8 +426,8 @@ 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); @@ -511,7 +525,11 @@ impl<'a> Machine<'a> { 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")), + 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), @@ -574,6 +592,118 @@ 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 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 { @@ -582,7 +712,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) } @@ -1490,7 +1620,7 @@ impl<'a> Machine<'a> { 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")), + 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/lib.rs b/system/acpiserver/aml/src/lib.rs index db11633272..634c88e3f5 100644 --- a/system/acpiserver/aml/src/lib.rs +++ b/system/acpiserver/aml/src/lib.rs @@ -16,11 +16,19 @@ //! 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 +//! space is refused as [`Error::Unsupported`], as are `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. //! @@ -54,7 +62,7 @@ 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}; @@ -193,6 +201,27 @@ 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 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. @@ -328,7 +357,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); @@ -362,22 +391,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 } @@ -416,6 +435,13 @@ impl Interpreter { 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 diff --git a/system/acpiserver/aml/src/namespace.rs b/system/acpiserver/aml/src/namespace.rs index 66f8a6e626..1c2cab8ba4 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,76 @@ 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; +/// 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 +122,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 id(&self, index: u32) -> NodeId { - NodeId { index, stamp: self.nodes[index as usize].stamp } + 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, 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 +154,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 +165,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 +207,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,57 +221,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) } + /// How many nodes the static arena holds: every one a load made, and the + /// predefined ones. pub(crate) fn defined(&self) -> u32 { - self.nodes.len() as 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) -> Mark { - Mark { nodes: self.nodes.len(), slots: self.nodes.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. @@ -230,9 +299,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. @@ -243,21 +312,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, @@ -275,20 +346,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]); } } @@ -298,11 +371,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]; @@ -312,19 +385,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 place in - /// the arena, its name and the object. + /// 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))); @@ -336,7 +409,7 @@ 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]; diff --git a/system/acpiserver/aml/tests/common/mod.rs b/system/acpiserver/aml/tests/common/mod.rs index c9e8e54da0..0ded13039e 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, 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,11 @@ 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 machine of bytes: every address space a sparse map, every request @@ -309,6 +326,9 @@ 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)>, } fn key(a: Address) -> (u8, u64) { @@ -406,6 +426,26 @@ 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())); + } +} + +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 +527,8 @@ 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<'_>) {} } diff --git a/system/acpiserver/aml/tests/heap.rs b/system/acpiserver/aml/tests/heap.rs index f4ad8696f8..99a3809786 100644 --- a/system/acpiserver/aml/tests/heap.rs +++ b/system/acpiserver/aml/tests/heap.rs @@ -210,16 +210,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 +235,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"); } diff --git a/system/acpiserver/aml/tests/hostile.rs b/system/acpiserver/aml/tests/hostile.rs index aaac51f7d9..7fd93a2b69 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 0000000000..d8b2acd5cf --- /dev/null +++ b/system/acpiserver/aml/tests/load.rs @@ -0,0 +1,221 @@ +//! 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)]); +} diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 56597964aa..d5beb26608 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -46,6 +46,13 @@ //! serve, once [`Firmware::buttons`] names the button; every other Notify is //! said the first time and served by nothing. //! +//! **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. //! @@ -60,7 +67,7 @@ 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}; use crate::devices::Ec; use crate::ec::{Do, Transaction, Wait}; @@ -193,6 +200,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 { @@ -280,6 +294,9 @@ pub struct Firmware<'k, K> { 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, } impl<'k, K: Kernel> Firmware<'k, K> { @@ -303,6 +320,8 @@ impl<'k, K: Kernel> Firmware<'k, K> { presses: 0, stopping: false, locked: false, + loaded: 0, + loads_refused: 0, } } @@ -509,6 +528,51 @@ impl Host for Firmware<'_, K> { 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 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)] diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 6da7085f67..820d90365a 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -38,6 +38,10 @@ //! 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 @@ -65,6 +69,7 @@ mod ec; mod host; mod init; mod ledger; +mod osc; mod sci; mod tables; @@ -118,6 +123,11 @@ struct Server<'a, K, P> { 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, } fn main() { @@ -134,7 +144,7 @@ fn main() { server.aml = kept; let names = server.initialise(); let (batteries, adapters) = server.arm_controller(names.as_ref()); - if names.is_some() { + if server.negotiate(names).is_some() { println!("{}", server.census.line()); } server.find_power(batteries, adapters); @@ -296,6 +306,8 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { empty: 0, power_off: false, census: Census::default(), + negotiated: osc::Negotiated::default(), + intel: toyos_abi::processor_is_intel(), } } @@ -354,6 +366,25 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { 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", + aml.host.loaded, + aml.host.loads_refused, + aml.interpreter.defined(), + aml.interpreter.slots() + ); + 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 @@ -646,6 +677,11 @@ 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::*; @@ -835,6 +871,91 @@ mod tests { } } + /// A platform `_OSC` and an Intel processor's that record what they are + /// passed in firmware memory and offer every bit 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(0xFFFF_FFFF), &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 every 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 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, diff --git a/system/acpiserver/src/osc.rs b/system/acpiserver/src/osc.rs new file mode 100644 index 0000000000..b81936421e --- /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"); + } +} From ce15a9eba6db0db5767b3a90de3ece4a05620271 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 14:46:09 +0200 Subject: [PATCH 06/30] acpiserver: attach lives beside find and query in devices.rs The controller's attachment is the devices module's, beside the search that finds the controller and the query that runs its methods, so the check outside the tree drives the server's own modules without main.rs's claim and ports. No behaviour changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/src/battery.rs | 4 ++-- system/acpiserver/src/devices.rs | 33 ++++++++++++++++++++++++++++++ system/acpiserver/src/main.rs | 35 +++----------------------------- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/system/acpiserver/src/battery.rs b/system/acpiserver/src/battery.rs index 9c0c30b917..efd213e864 100644 --- a/system/acpiserver/src/battery.rs +++ b/system/acpiserver/src/battery.rs @@ -555,14 +555,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 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"); - crate::attach(&mut aml, ec, &names, &mut crate::init::Census::default()).expect("no port of it refused"); + devices::attach(&mut aml, ec, &names, &mut crate::init::Census::default()).expect("no port of it refused"); (aml, found) } diff --git a/system/acpiserver/src/devices.rs b/system/acpiserver/src/devices.rs index 6009a6560d..66d3fc70ec 100644 --- a/system/acpiserver/src/devices.rs +++ b/system/acpiserver/src/devices.rs @@ -32,6 +32,10 @@ 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; @@ -133,6 +137,35 @@ pub fn find(interpreter: &mut Interpreter, host: &mut dyn Host, names: &Names, g 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 { diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 820d90365a..eb9985cbb6 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -25,7 +25,7 @@ //! `_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 in the ECDT's place, so its +//! ([`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 @@ -387,7 +387,7 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { /// 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, names: Option<&Names>) -> (Vec, Vec) { let (Some(aml), Some(names)) = (&mut self.aml, names) else { @@ -424,7 +424,7 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { } Ok(_) => {} } - let ec = match attach(aml, ec, names, &mut self.census) { + let ec = match devices::attach(aml, ec, names, &mut self.census) { Ok(ec) => ec, Err(why) => { println!("{CONTROLLER_NONE}{why}"); @@ -632,35 +632,6 @@ impl<'a, P: Ports> Server<'a, Claim<'a>, P> { } } -/// 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. -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, {}", aml::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()); - init::announce(aml, names, init::EMBEDDED_CONTROL, 0, census); - aml.host.ended.take().map_or(Ok(ec), Err) -} - /// What a query no method runs for is said to be. const UNRUN: &str = "served by nothing: no query's method runs where no control-method power button is served"; From 800fb477008f11ab2a52b1dc236142b6e6d32e8e Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 14:55:13 +0200 Subject: [PATCH 07/30] S4: the grammar whole: every space reaches the host, LoadTable, DataTableRegion, and Error::Unsupported deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every address space of Table 5.182 now reaches the host (ACPI 6.5 §5.5.2.4), which says which it serves: - by offset, an access unit at a time, as an `Address`: SystemCMOS a byte of the RTC/CMOS device's memory (§5.5.2.4.3); PciBarTarget by the BAR at the region's offset in the function's configuration space, its length the BAR's (§5.5.2.4.4); PCC by subspace and offset into its shared memory (§5.5.2.4.8); and each OEM-defined space; - whole, as a `Transfer`, where the protocol is command- or connection-based: SMBus, IPMI, GeneralPurposeIO, GenericSerialBus, PlatformRtMechanism and FFixedHW. A field keeps the protocol its AccessAs names (Table 5.184; an AccessType byte's bits 7:6 name AttribBytes, AttribRawBytes or AttribRawProcessBytes of a count, an ExtendedAccessField both) and the descriptor its Connection names, inline or by name, with the bit its units begin at. A BufferAcc field's value is the buffer the host answers, and a Store to it answers the response its buffer became, as `BUFF = (FLD = BUFF)` reads it (§5.5.2.4.5.1, §5.5.2.4.7.1); a GeneralPurposeIO field's is its pins. A reserved space, BufferAcc in a space addressed by offset, and a BankField of a carried space break a rule, refused by name. DataTableRegion (§19.6.25) and LoadTable (§19.6.77) find the table the XSDT lists by signature, OEM ID and OEM Table ID through a new host call (`toyos_acpi::find_by_id`, a shorter identifier matching a field padded with NULs or spaces), which the server answers only where the table lies in memory Table 15.6 calls reserved or NVS: QEMU's and most machines' tables are in ACPI reclaim memory, and denied. LoadTable loads below its RootPathString, stores ParameterData once loaded, and is false for no table, a denied one, or a refused block; an identifier longer than its field is a run-time error. Neither bench's tables use either (#875's M1). A reference to an unnamed object handed to the caller is `Value::Unnamed`, what it referred to; a LocalX's is gone with its method. `0x5B 0x2A`, Unload until ACPI 6.5 reserved it, stays malformed. With that nothing of the grammar is refused as not carried, and Error::Unsupported is deleted. The server serves SystemMemory, SystemIO, PCI_Config and EmbeddedControl, and denies every other space by name. Checks: a host test for each construct (tests/spaces.rs, tests/load.rs, toyos-acpi's corpus); the server's denial of each space and its lookup's memory types. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- lib/acpi/src/lib.rs | 31 ++++ lib/acpi/tests/corpus.rs | 29 ++- system/acpiserver/aml/src/exec.rs | 125 +++++++++++-- system/acpiserver/aml/src/field.rs | 175 +++++++++++++++--- system/acpiserver/aml/src/lib.rs | 78 +++++++- system/acpiserver/aml/tests/common/mod.rs | 62 ++++++- system/acpiserver/aml/tests/load.rs | 99 ++++++++++ system/acpiserver/aml/tests/regions.rs | 10 +- system/acpiserver/aml/tests/spaces.rs | 209 ++++++++++++++++++++++ system/acpiserver/src/aml.rs | 2 +- system/acpiserver/src/host.rs | 121 ++++++++++++- 11 files changed, 886 insertions(+), 55 deletions(-) create mode 100644 system/acpiserver/aml/tests/spaces.rs diff --git a/lib/acpi/src/lib.rs b/lib/acpi/src/lib.rs index e4ac4c995d..5941cb22b7 100644 --- a/lib/acpi/src/lib.rs +++ b/lib/acpi/src/lib.rs @@ -315,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 caad92af23..807a701952 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/system/acpiserver/aml/src/exec.rs b/system/acpiserver/aml/src/exec.rs index 7bc40ea46d..fd6b514552 100644 --- a/system/acpiserver/aml/src/exec.rs +++ b/system/acpiserver/aml/src/exec.rs @@ -25,7 +25,7 @@ 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::{Arena, Namespace, NodeId}; use crate::object::{ @@ -369,7 +369,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()), @@ -524,7 +524,7 @@ 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")), + 0x88 => self.def_data_region(f, c), 0x20 => { c.byte()?; c.byte()?; @@ -643,6 +643,82 @@ impl<'a> Machine<'a> { 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 @@ -885,6 +961,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()? { @@ -894,22 +972,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()?; @@ -918,7 +1001,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; } @@ -1468,9 +1551,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) } @@ -1619,7 +1710,7 @@ 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")), + 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 352824e7fa..d64f3058cd 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")), } } @@ -331,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 { @@ -383,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 634c88e3f5..1953c9e5ae 100644 --- a/system/acpiserver/aml/src/lib.rs +++ b/system/acpiserver/aml/src/lib.rs @@ -25,10 +25,13 @@ //! 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 `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. //! @@ -57,6 +60,7 @@ mod namespace; mod object; mod stream; +use alloc::boxed::Box; use alloc::rc::Rc; use alloc::string::String; use alloc::vec::Vec; @@ -143,9 +147,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 @@ -169,7 +170,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), @@ -177,6 +179,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. @@ -209,6 +247,18 @@ pub trait Host { 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 @@ -234,6 +284,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. @@ -466,6 +520,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")), }) } @@ -522,6 +577,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/tests/common/mod.rs b/system/acpiserver/aml/tests/common/mod.rs index 0ded13039e..4725776939 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, RuntimeLoad, 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. @@ -315,6 +315,12 @@ pub enum Event { /// 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 @@ -329,6 +335,11 @@ pub struct Machine { /// 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) { @@ -344,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), } } @@ -439,6 +456,43 @@ impl Host for Machine { 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 { @@ -531,4 +585,10 @@ impl Host for Sink { 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/load.rs b/system/acpiserver/aml/tests/load.rs index d8b2acd5cf..367033ad6d 100644 --- a/system/acpiserver/aml/tests/load.rs +++ b/system/acpiserver/aml/tests/load.rs @@ -219,3 +219,102 @@ fn a_loaded_blocks_objects_are_walked_past_what_was_defined_before() { } 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 5c5869fb26..5c5fae3139 100644 --- a/system/acpiserver/aml/tests/regions.rs +++ b/system/acpiserver/aml/tests/regions.rs @@ -442,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 0000000000..356c5009c4 --- /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/src/aml.rs b/system/acpiserver/src/aml.rs index b0c6e3ce25..3a816abe29 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -71,7 +71,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(), } @@ -107,6 +106,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; diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index d5beb26608..213eadee31 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -24,6 +24,18 @@ //! 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`], //! [`Firmware::transaction`]), a byte each, on the controller the server //! attached ([`Firmware::ec`]) and through the kernel, which no row holds its @@ -67,7 +79,7 @@ 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, RuntimeLoad}; +use toyos_aml::{Address, Denied, Host, RuntimeLoad, Transfer}; use crate::devices::Ec; use crate::ec::{Do, Transaction, Wait}; @@ -297,6 +309,8 @@ pub struct Firmware<'k, K> { /// 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> { @@ -322,6 +336,7 @@ impl<'k, K: Kernel> Firmware<'k, K> { locked: false, loaded: 0, loads_refused: 0, + rsdp: 0, } } @@ -373,6 +388,21 @@ impl<'k, K: Kernel> Firmware<'k, K> { } } + /// 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()) @@ -443,6 +473,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)) => { @@ -474,6 +505,7 @@ impl Host for Firmware<'_, K> { 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); } + other => return Err(self.unserved(other, width)), }; Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) } @@ -553,6 +585,41 @@ impl Host for Firmware<'_, K> { 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(); @@ -880,6 +947,58 @@ pub mod tests { 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 /// the kernel at the attached controller's ports: a read is RD_EC of its /// address, a write WR_EC of its address and value. From b8dbbad4535af5124e7b496b762171b541e0dd40 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 15:10:02 +0200 Subject: [PATCH 08/30] S5: every event served: runtime GPEs, each Notify to its owner, and the SCI through a load's Sleep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPEs (ACPI 6.5 §5.6.4). Each `\_GPE._Lxx` or `_Exx` in GPE0 is a runtime GPE and enabled, but one the controller's (its driver serves it, §5.6.4.1.2), one naming both triggers, and one the `_PRW` of a device other than the power, lid and sleep buttons and the PCI host bridge names: §5.6.4.2 keeps wake events off runtime inputs, and OSPM enables a wake event only to arm a device's wake, which ToyOS does for none (`gpe.rs`). On its SCI a GPE's enable is cleared, an edge's status cleared, its 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. The census says how many GPEs have a method, how many are runtime, and how many wake events stay disabled. Notifies (§5.6.6). The host keeps each Notify but a control-method button's press, once an object and a value, and the server delivers them after each evaluation: a battery's or adapter's 0x80 reads the power sources again, a battery's 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 0x80 to 0x82 is counted since nothing reads its performance, idle or throttling states yet, and anything else is served by nothing; each is said once. The SCI while a table loads. An evaluation's Sleep waits on the SCI to its deadline (`sci::await_sleep`): a press of the fixed button and the controller's GPE are taken as they latch and handed to the server, which serves them after the evaluation, the press powering the machine off once the load has handed `\_S5` over; anything else keeps its status and loses its enable until the Sleep ends. That meets the exit of the track's bullet on a press during the load, which waited out the load's sleeps. `acpi_press_across_a_lock_wait`'s table waited for the press in its load by `While (!PBST) { Sleep (1) }`, reading PM1 status directly, which §5.5.2.4.1 forbids AML; a Sleep now takes that press, so the fixture waits by Stall, which keeps the processor and serves nothing. What the test holds, a press latched across the Global Lock's wait, is unchanged. Checks: host tests of the GPE census and handling, a refused method's GPE disabled, each Notify's delivery, what a Sleep took served after it, and the Sleep's wait taking a press and the controller's GPE and leaving the rest latched. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/src/gpe.rs | 120 +++++++++++++ system/acpiserver/src/host.rs | 54 ++++-- system/acpiserver/src/main.rs | 314 +++++++++++++++++++++++++++++++++- system/acpiserver/src/sci.rs | 98 ++++++++++- tests/common/power.rs | 9 +- 5 files changed, 568 insertions(+), 27 deletions(-) create mode 100644 system/acpiserver/src/gpe.rs diff --git a/system/acpiserver/src/gpe.rs b/system/acpiserver/src/gpe.rs new file mode 100644 index 0000000000..4eca6a9bdb --- /dev/null +++ b/system/acpiserver/src/gpe.rs @@ -0,0 +1,120 @@ +//! 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 { + let inputs = u16::try_from(gpe0.len / 2 * 8).unwrap_or(u16::MAX); + 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 213eadee31..ef80f07aba 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -56,7 +56,15 @@ //! **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 @@ -84,6 +92,7 @@ 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. @@ -144,6 +153,10 @@ 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; } /// Why an access to one of the controller's ports was not made. @@ -293,13 +306,19 @@ pub struct Firmware<'k, K> { /// for the server to serve it no more. pub ended: Option, pub refused: Ledger, - notified: Ledger, + /// The presses said, by button. + said: Ledger, /// The port writes asked of the kernel, by port and width. written: Ledger, /// The FADT's `SMI_CMD`, a byte to which is a call into the firmware. pub smi_cmd: Option, /// 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. @@ -327,10 +346,12 @@ impl<'k, K: Kernel> Firmware<'k, K> { ec: None, ended: None, refused: Ledger::default(), - notified: Ledger::default(), + said: Ledger::default(), written: Ledger::default(), smi_cmd: None, buttons: Vec::new(), + latched: Latched::default(), + notified: Default::default(), presses: 0, stopping: false, locked: false, @@ -512,8 +533,10 @@ impl Host for Firmware<'_, K> { 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) { @@ -530,14 +553,14 @@ 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> { @@ -689,6 +712,10 @@ pub mod tests { 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>>, } impl Scripted { @@ -789,6 +816,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"); diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index eb9985cbb6..6c9a108a37 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -66,6 +66,7 @@ mod aml; mod battery; mod devices; mod ec; +mod gpe; mod host; mod init; mod ledger; @@ -91,7 +92,7 @@ use aml::Aml; use battery::Power; use devices::{Ec, Queried}; use ec::Transaction; -use host::{port_in, Answer, Kernel, Stopping, Take, Unmade}; +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}; @@ -128,6 +129,13 @@ struct Server<'a, K, P> { /// 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() { @@ -144,10 +152,13 @@ fn main() { server.aml = kept; let names = server.initialise(); let (batteries, adapters) = server.arm_controller(names.as_ref()); - if server.negotiate(names).is_some() { + 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(); } @@ -236,6 +247,11 @@ 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), @@ -256,6 +272,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() } } @@ -294,7 +315,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, @@ -308,6 +329,9 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { 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(), } } @@ -515,9 +539,133 @@ 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 @@ -663,9 +811,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 { @@ -673,15 +823,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); } } @@ -927,6 +1084,145 @@ mod tests { } } + /// 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"); + } + /// 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, diff --git a/system/acpiserver/src/sci.rs b/system/acpiserver/src/sci.rs index 5b228cf183..204e03a582 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 6659738ad5..580ac51948 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -783,13 +783,14 @@ const ONE_TAKE_CONTENDED: &str = "; took the Global Lock 1 times, 1 of them from /// `\_SB.GLW` and, as it loads, waits for `PWRBTN_STS` (Table 4.13) in the /// PM1a event block at `pm1`, then Acquires `\_GL` within `ms`, giving it /// back where that took it and Notifying `\_SB.GLW` with 0x80 where it timed -/// out: +/// out. It waits by Stall, which keeps the processor: a Sleep serves the SCI +/// (`sci::await_sleep`) and would take the press it waits for. /// /// ```text /// Device (\_SB.GLW) {} /// OperationRegion (PM1S, SystemIO, pm1, 2) /// Field (PM1S, WordAcc, NoLock, Preserve) { Offset (1), PBST, 1 } -/// While (!PBST) { Sleep (1) } +/// While (!PBST) { Stall (100) } /// If (Acquire (\_GL, ms)) { Notify (\_SB.GLW, 0x80) } Else { Release (\_GL) } /// ``` fn ssdt_acquiring_the_global_lock(pm1: u16, ms: u16) -> Vec { @@ -804,8 +805,8 @@ fn ssdt_acquiring_the_global_lock(pm1: u16, ms: u16) -> Vec { let region = [&[0x5B, 0x80], PM1S, &[0x01, 0x0B], &pm1.to_le_bytes(), &[0x0A, 0x02]].concat(); // WordAcc (2); a ReservedField (0) of 8 bits, then PBST of 1. let field = package(&[0x5B, 0x81], &[PM1S, &[0x02, 0x00, 0x08], PBST, &[0x01]].concat()); - // LNot (0x92) of PBST; Sleep of One (0x01). - let wait = package(&[0xA2], &[&[0x92], PBST, &[0x5B, 0x22, 0x01]].concat()); + // LNot (0x92) of PBST; Stall of a ByteConst 100. + let wait = package(&[0xA2], &[&[0x92], PBST, &[0x5B, 0x21, 0x0A, 0x64]].concat()); let acquire = [&[0x5B, 0x23], GL, &ms.to_le_bytes()].concat(); let notify = [&[0x86], GLW, &[0x0A, 0x80]].concat(); let release = [&[0x5B, 0x27], GL].concat(); From cf4ed8a802cfdaaa3a87928dbe3c7e20922920b9 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 15:13:48 +0200 Subject: [PATCH 09/30] S6: the bounds stated and read: the heap a running interpreter holds, the longest wait, the firmware's longest hold The heap. The interpreter states what one load or evaluation holds while it runs, `MAX_RUNNING`: `MAX_LIVE` and what a run holds beyond the meter's count, each part bounded by another bound of the interpreter and named at the constant (an arena's old slots while it doubles, one operator's bytes before the meter holds them, a table's while it is read in, each frame's slots, name and PCI walk, each Mutex held, a node's path). Mutexes held were bounded only by the steps, about 4.5 MiB of records: an evaluation now holds at most 256 acquisitions at once, refused at the bound as nesting is. Every hostile fill of `tests/heap.rs`, by loads and by methods, holds its peak under the counting allocator to `MAX_RUNNING`. The server says both bounds beside what the interpreter holds after the machine's last table, and the static arena's slots. The ten-second wait. The census says the longest wait an evaluation of the initialisation asked, refused or not (`Usage::waited_us`), the reading the track's bullet asks of the T14. The Global Lock. The host measures how long a take waited for the firmware's release, the firmware's longest hold as a take saw it, and the load's line says it beside the contended count, the reading the track's bullet sets the bound by. What a full interpreter does is a design the accepted one does not hold, and is proposed on the pull request rather than built here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/aml/src/exec.rs | 12 ++++++++++++ system/acpiserver/aml/src/lib.rs | 23 +++++++++++++++++++++++ system/acpiserver/aml/src/namespace.rs | 4 ++++ system/acpiserver/aml/tests/heap.rs | 20 +++++++++++++++++++- system/acpiserver/src/aml.rs | 3 ++- system/acpiserver/src/host.rs | 12 +++++++++++- system/acpiserver/src/init.rs | 17 ++++++++++++----- system/acpiserver/src/main.rs | 7 +++++-- 8 files changed, 88 insertions(+), 10 deletions(-) diff --git a/system/acpiserver/aml/src/exec.rs b/system/acpiserver/aml/src/exec.rs index fd6b514552..2c2a722bd4 100644 --- a/system/acpiserver/aml/src/exec.rs +++ b/system/acpiserver/aml/src/exec.rs @@ -51,6 +51,15 @@ 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, @@ -1111,6 +1120,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)")); diff --git a/system/acpiserver/aml/src/lib.rs b/system/acpiserver/aml/src/lib.rs index 1953c9e5ae..6a16885d2a 100644 --- a/system/acpiserver/aml/src/lib.rs +++ b/system/acpiserver/aml/src/lib.rs @@ -90,6 +90,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; diff --git a/system/acpiserver/aml/src/namespace.rs b/system/acpiserver/aml/src/namespace.rs index 1c2cab8ba4..6bc8eb148f 100644 --- a/system/acpiserver/aml/src/namespace.rs +++ b/system/acpiserver/aml/src/namespace.rs @@ -57,6 +57,10 @@ const ENTRY: usize = (core::mem::size_of::() + 4 + 11 * core::mem::size_o /// 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 { diff --git a/system/acpiserver/aml/tests/heap.rs b/system/acpiserver/aml/tests/heap.rs index 99a3809786..4f6c3fb1ce 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"); } } @@ -476,3 +479,18 @@ 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"))); +} diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index 3a816abe29..c01147d7bf 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -192,13 +192,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, which held it {:?} at the longest; 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 ); diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index ef80f07aba..d48d710898 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -298,6 +298,9 @@ pub struct Firmware<'k, K> { /// and gives back. pub takes: u64, pub contended: u64, + /// The longest a take waited for the firmware's release: the firmware's + /// longest hold of the lock, 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. @@ -341,6 +344,7 @@ 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, @@ -455,10 +459,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"))); diff --git a/system/acpiserver/src/init.rs b/system/acpiserver/src/init.rs index 52edb2885a..a4ee7cc7b7 100644 --- a/system/acpiserver/src/init.rs +++ b/system/acpiserver/src/init.rs @@ -97,6 +97,9 @@ pub struct Census { 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 { @@ -110,10 +113,11 @@ impl Census { 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, {}; evaluations refused, by kind: {refused}", + "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(", ") } + if reg.is_empty() { "no _REG run".to_string() } else { reg.join(", ") }, + self.longest_wait_us ) } @@ -128,7 +132,9 @@ impl Census { /// 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 { - match aml.interpreter.evaluate(&mut aml.host, path, args) { + 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) => { @@ -356,7 +362,8 @@ mod tests { &device("DEVD", &cat(&[&def_name("_STA", &int(0)), &method("_INI", 0, &tagged(0xDD)), &device("DEVE", &method("_INI", 0, &tagged(0xEE)))])), ]), ), - &device("DEVF", &cat(&[&def_name("_STA", &int(0x0F)), &method("_INI", 0, &tagged(4))])), + // 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); @@ -365,7 +372,7 @@ mod tests { 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; evaluations refused, by kind: none" + "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" ); } diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 6c9a108a37..c091a1f07a 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -400,11 +400,14 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { 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", + "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.slots(), + aml.interpreter.usage().live, + toyos_aml::MAX_LIVE, + toyos_aml::MAX_RUNNING ); Some(names) } From 14241dc96e34b59a1d10672faa28b3fbf125331b Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 15:18:08 +0200 Subject: [PATCH 10/30] The acpi_tables_loaded row judges each bench by its own tables, and reads S1, S3 and S5 on the machine `acpi_tables_loaded` now judges the bench whose ACPI row the kernel names, by that bench's expectations (`Bench` in tests/toyos.rs): the T14's 14 blocks, the AMD bench's 17 with SSDT8 refused for the name its `Scope` does not resolve, as the owner ruled, and each `\_S5`. No row names its hardware yet, so one row that reads either bench as itself costs no red on the other. On either bench it reads what #875 built: the namespace initialised with every `_INI` run and none refused but by the owner's ruling on a write outside every range the map lists, and `_REG(3, 1)` after them; `\_SB._OSC` negotiated, and on the T14 each processor's, none refused; the blocks ToyOS's claims load, by signature and length, the T14's one of 459 bytes as the check outside the tree read it; and it prints the census, the GPEs enabled, the longest wait asked, the firmware's longest hold of the Global Lock, the interpreter's heap and the namespace's slots. `acpi_server_events`, on the held boot, reds on a runtime GPE whose method was refused and prints each GPE served. The issues: the track says what the build met and what each stage still waits on, its `_REG` bullet and the press's wait gone with their exits met; the AMD bench's SSDT8 is recorded as the firmware defect the owner ruled it, with its evidence and an exit; and the AMD bench's memory-mapped I/O issue records the rulings that bring its exit to 16 of 17 tables. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...chs-dsdt-for-a-read-in-memory-mapped-io.md | 7 + ...t8-scopes-a-controller-no-table-defines.md | 37 ++++ ...ine-in-acpi-mode-and-interprets-its-aml.md | 155 +++++++--------- tests/toyos.rs | 167 ++++++++++++++---- 4 files changed, 238 insertions(+), 128 deletions(-) create mode 100644 issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md 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 index a25b674591..d5359e3ebb 100644 --- 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 @@ -47,3 +47,10 @@ 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. + +**Ruled** (owner, 2026-10-12, on #875): such a read is a register's, read +as an unlisted one is (comment 6120173372), built in #875 with a host test +of each arm; and SSDT8 is refused whole (comment 6121646183, +`issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md`), so +the boot's reading is 16 of 17 tables loaded, SSDT8 the one refused, and +`\_S5` handed: the bench's `acpi_tables_loaded` reads 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 0000000000..3d7227a8bd --- /dev/null +++ b/issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md @@ -0,0 +1,37 @@ +--- +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.122 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. Linux +on the same boot logs the same defect, `ACPI BIOS Error (bug): Could not +resolve symbol [\_SB.PCI0.LPC0.EC0], AE_NOT_FOUND`, and keeps the rest of +the block. + +**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/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 daf51cc0cd..264e1d2e75 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 @@ -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. @@ -196,8 +193,11 @@ otherwise pay to find again: refusal. - **The T14's processor objects need `Load`.** Its tables hold eight `Load` opcodes, none run while a table loads, and Linux lists eight tables loaded - that way; the interpreter refuses them as unsupported. Owner: S1, below. - **Exit**: its. + 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 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 @@ -207,7 +207,8 @@ otherwise pay to find again: (`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 + what the server does with an interpreter that is full; #875 proposes it + in its body, unbuilt. **Exit**: a test fills the budget through one method, and the server then evaluates a method that builds a buffer. - **The interpreter's 16 MiB bounds the heap it holds from one call to the @@ -245,11 +246,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 @@ -260,14 +264,20 @@ 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 + reads 56 `_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. @@ -275,16 +285,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 @@ -297,27 +297,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 @@ -386,10 +383,13 @@ 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. + none: it loaded with every write denied. A byte to the FADT's `SMI_CMD` + is asked of the kernel since the owner's ruling (#875). Outside the tree + on the AMD bench's dump, its eight interrupt links' `_STA` each write a + byte to port 0xC00, the PCI interrupt router's index, and are refused: + that reading goes to the owner. Owner: this stage. **Exit**: the owner + rules that write in the list or refused for good, and the T14's + `counters` row reads a call `SMI_CMD` took with the readings below. - **What a call does there is the firmware's**, and the kernel bounds who, when, where, which byte and how often: @@ -499,50 +499,21 @@ 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. -- **S1: `Load`, and `_OSC` negotiated.** - - `Load` per §19.6.76: its objects outlive the method that loaded it, and - its bytes come only from memory Table 15.6 calls reserved or NVS. - - `\_SB._OSC` and each Intel processor's `_OSC`, query then commit. The - capability set holds only what ToyOS honours, which today is nothing. - - Takes up the `Load` bullet above, and the arena's capacity in the heap - bullet. - - **Exit**: - - host tests of `Load` from a region, a field and a buffer; - - outside the tree, under every capability bit, the eight blocks Linux - lists, by OEM table ID and length; - - `acpi_tables_loaded` reads both `_OSC`s evaluated, with nothing - refused. -- **S2: what AML may reach, ruled.** - - The policy for memory the firmware types `EfiMemoryMappedIO`. - - Whether the kernel's bound stays address-only. - - Any write, outside what passes now, that a machine's measured AML makes. - - **Exit**: - `issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md`'s. -- **S3: the namespace initialised.** - - `\_SB._INI`, then the `_STA`/`_INI` walk by Table 6.67 over each block a - load added, then `_REG`. - - A root bridge without `_BBN`, found by `_HID` or `_CID`. - - `_GLK`. - - Takes up the `_REG` bullet above. QEMU's interrupt links, refused for a - host bridge named by `_HID` alone, and the controller reached without the - Global Lock its `_GLK` asks for, are closed by their host tests - (`qemus_interrupt_link_reads_its_routing_register_below_a_bridge_named_by_its_hid`, - `a_controller_that_wants_the_global_lock_is_reached_under_it_alone`). - - **Exit**: the `_REG` bullet's, and every device's `_STA` on the T14 - equal to Linux's. -- **S4: the grammar whole.** - - `LoadTable`, `DataTableRegion`, and every space's protocol. - - **Exit**: - - `Error::Unsupported` deleted; - - a host test for each construct; - - both benches' tables loaded outside the tree, none refused for a - construct. -- **S5: every event served.** - - Each runtime GPE's `_Lxx` or `_Exx`, and each Notify to its owner. - - The SCI served while a table loads. - - Takes up the press-wait bullet above. - - **Exit**: that bullet's exit, and a held T14 boot reading every enabled - GPE's method run with none refused. -- **S6: the bounds measured.** - - The heap, full-interpreter, ten-second and Global Lock bullets above, - each by its own exit. +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`), + which meets + `issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md`'s + exit. +- **S3**: the T14's `acpi_tables_loaded` reads every `_INI` run, none refused + but by the owner's ruling, and `_REG(3, 1)` after them; every device's + `_STA` there equal to Linux's, compared outside the tree. +- **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/tests/toyos.rs b/tests/toyos.rs index 60ffa3f94f..83284a7a7a 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5855,7 +5855,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())); @@ -5867,32 +5873,71 @@ 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 a reading of them under another OS and +/// the check outside the tree on their dump (#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: Linux's count. + 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 refusals the owner ruled, which the namespace's + /// initialisation meets on this machine and which are said, not red. + ruled: &'static [&'static str], +} + +/// The T14: Linux says `14 ACPI AML tables successfully acquired and loaded`; +/// 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 a device's `_INI` may write memory outside every range the map lists, +/// which the owner ruled refused and recorded. +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: &["a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory"], +}; + +/// The AMD bench: Linux counts 17 tables; 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`. +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: &[], +}; + +/// The server's load of a bench's tables, every access the kernel's to make +/// for it: each of the machine's definition blocks, as many as Linux names +/// there, 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` run and none refused but by a ruled +/// refusal, and the controller's `_REG(3, 1)` after them; `\_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()); let lines: Vec<&str> = log @@ -5903,10 +5948,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")) { @@ -5916,20 +5977,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 ruled = |l: &&&str| bench.ruled.iter().any(|ruled| l.contains(ruled)); + 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 ") && !ruled(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 "))?; + for line in lines.iter().filter(ruled) { + 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 == "none" || refusals.split("; ").all(|kind| bench.ruled.iter().any(|ruled| kind.contains(ruled)))) { + return Err(format!("the namespace's initialisation refused an evaluation: {census}")); + } + if !census.contains("_REG(3, 1) in 1 scope(s)") { + return Err(format!("the controller's space was not announced after every _INI: {census}")); + } + 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(()) From fb2bcceda8c42b82973d60a73789b8e401cf7f54 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 15:33:59 +0200 Subject: [PATCH 11/30] acpiserver: a commit sends only what the query offered of the claim While ToyOS claims nothing, no test could see a commit send a bit its query did not offer. The `_OSC` fixture's query now offers bit 0 alone, and a claim of two bits commits the one, the firmware's record of the query and the commit read back. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/src/main.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index c091a1f07a..8d2a7068d5 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -1003,7 +1003,7 @@ mod tests { } /// A platform `_OSC` and an Intel processor's that record what they are - /// passed in firmware memory and offer every bit to a query; the + /// 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 { @@ -1033,7 +1033,7 @@ mod tests { &store(&name("CDW1"), &name(&format!("\\{p}Q1"))), &store(&name("CDW2"), &name(&format!("\\{p}Q2"))), &store(&int(0x10), &name("CDW1")), - &store(&int(0xFFFF_FFFF), &name("CDW2")), + &store(&int(1), &name("CDW2")), ]), ), &else_(&cat(&[&store(&name("CDW1"), &name(&format!("\\{p}C1"))), &store(&name("CDW2"), &name(&format!("\\{p}C2"))), commit])), @@ -1053,7 +1053,7 @@ mod tests { /// §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 every bit; + /// 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 @@ -1226,6 +1226,24 @@ mod tests { assert_eq!(server.counts.get(&0x1C), Some(&1), "the query a Sleep found waiting was not drained"); } + /// §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, From 28ef9349c0dd98c3fbd3f57309932ade986c7273 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 15:42:16 +0200 Subject: [PATCH 12/30] acpiserver: the guest build imports what it uses, and GPE0's count is its own type The server's `Value` import is its tests' alone since `attach` moved to devices.rs, and the guest build denies the unused import; GPE0's length is a `u16` already. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/src/gpe.rs | 3 ++- system/acpiserver/src/main.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/system/acpiserver/src/gpe.rs b/system/acpiserver/src/gpe.rs index 4eca6a9bdb..3df2cfef92 100644 --- a/system/acpiserver/src/gpe.rs +++ b/system/acpiserver/src/gpe.rs @@ -32,7 +32,8 @@ 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 { - let inputs = u16::try_from(gpe0.len / 2 * 8).unwrap_or(u16::MAX); + // §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 }; diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index 8d2a7068d5..adc19070b6 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -85,7 +85,6 @@ 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; @@ -807,6 +806,7 @@ 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}; From bac5ccca6cfc82342faadbdb5cc4188f155b3668 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 15:52:04 +0200 Subject: [PATCH 13/30] S5: what latched during the load is served before the namespace is initialised A press latched while the tables load, whether a Sleep took it or it waits latched in PM1, is served as soon as the load has handed `\_S5` over, before the initialisation, whose evaluations may wait seconds: the server serves what the load's Sleeps took, then any SCI already latched, then initialises. The guest suite found this. `acpi_press_across_a_lock_wait` stages the firmware holding the Global Lock for the whole boot. S3's walk runs q35's own `_STA`s, one of which takes the lock with no bound, so the server waited for the walk and ended at `RELEASE` before serving the press latched during the load: red, `STALLED`, the server's exit 101. Served first, the press powers the machine off on the load's `\_S5`, and the five ACPI guest tests pass. The load's line keeps `"... from the firmware;"`, which that test reads, and says the firmware's longest hold apart, a timed-out take counted as a hold at least that long. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/src/aml.rs | 2 +- system/acpiserver/src/host.rs | 6 ++++-- system/acpiserver/src/main.rs | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index c01147d7bf..1e9fc3d185 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -192,7 +192,7 @@ 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, which held it {:?} at the longest; 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], diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index d48d710898..112b64fe86 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -298,8 +298,9 @@ pub struct Firmware<'k, K> { /// and gives back. pub takes: u64, pub contended: u64, - /// The longest a take waited for the firmware's release: the firmware's - /// longest hold of the lock, as far as a take saw it. + /// 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, @@ -480,6 +481,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); } } diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index adc19070b6..ed4741d082 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -20,6 +20,11 @@ //! 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**, 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 every device's `_STA` and //! `_INI` by Table 6.67, again over whatever a table loaded meanwhile added. @@ -149,6 +154,8 @@ fn main() { let (loaded, kept) = aml::load(&claim, server.info.rsdp); server.power_off = loaded.handed; server.aml = kept; + server.served_aml(); + server.latched(); let names = server.initialise(); let (batteries, adapters) = server.arm_controller(names.as_ref()); if let Some(names) = server.negotiate(names) { @@ -758,6 +765,17 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { } impl<'a, P: Ports> Server<'a, Claim<'a>, P> { + /// Each SCI already latched, served without waiting for one. + fn latched(&mut self) { + match self.kernel.dev.irq() { + Ok(record) => self.scis += u64::from(record.count), + Err(SyscallError::WouldBlock) => return, + Err(other) => panic!("acpiserver: the claim's record answered {other:?}"), + } + self.take(); + self.kernel.dev.ack().expect("acpiserver: the claim's acknowledgement"); + } + fn serve(&mut self) -> ! { let mut next_count = Instant::now() + COUNTS; let mut next_read = Instant::now() + battery::POLL; From b38a1381e61e52c9dc7a3714d1671830af72ab0b Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 15:53:23 +0200 Subject: [PATCH 14/30] acpiserver and toyos-aml: the module headers say what the build added devices.rs says the controller's attachment under its `_GLK`, and its `_REG(3, 1)` after every `_INI`; the interpreter's headers name the bound on Mutexes held; aml.rs lists what is evaluated in the kept namespace; the battery's header says a Notify reads or finds the power sources again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/aml/src/exec.rs | 3 ++- system/acpiserver/aml/src/lib.rs | 3 ++- system/acpiserver/src/aml.rs | 8 +++++--- system/acpiserver/src/battery.rs | 6 ++++-- system/acpiserver/src/devices.rs | 6 ++++++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/system/acpiserver/aml/src/exec.rs b/system/acpiserver/aml/src/exec.rs index 2c2a722bd4..b0d9717e2f 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 diff --git a/system/acpiserver/aml/src/lib.rs b/system/acpiserver/aml/src/lib.rs index 6a16885d2a..68d8f3707b 100644 --- a/system/acpiserver/aml/src/lib.rs +++ b/system/acpiserver/aml/src/lib.rs @@ -5,7 +5,8 @@ //! 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 diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index 1e9fc3d185..552c8c5b05 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -26,9 +26,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 diff --git a/system/acpiserver/src/battery.rs b/system/acpiserver/src/battery.rs index efd213e864..07b1a31d89 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. diff --git a/system/acpiserver/src/devices.rs b/system/acpiserver/src/devices.rs index 66d3fc70ec..3febdb2976 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. From 011714fd944dc6c0a43c2d7e3b6354a2ce388fdb Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 17:22:58 +0200 Subject: [PATCH 15/30] acpiserver: the namespace walk examines a device with an _INI at or below it, and moves on past a refused _STA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner's ruling on #875 on an embedded-controller read before its _REG has run. A region whose space a handler serves is reached whether or not _REG has run, and one no handler serves is refused, as the host already did. ACPI 6.5 §6.5.1 has OSPM examine a device's children "for _INI methods" and evaluate _STA "before evaluating the _INI object", so initialisation asks no _STA of a device with no _INI at or below it. Of a _STA that fails §6.5.1 is silent; by ToyOS's rule its device's _INI is not run and its children are examined. The server's first walk asked every device's _STA, so on the AMD bench, which has no ECDT, the battery's _STA read the controller's space before the controller was attached and was refused, and the eight interrupt links' _STA were asked too. Neither has an _INI at or below it. The walk now uses for its first run the rule the walk after a run-time load already used, and a _STA refused or no Integer no longer closes its subtree. On QEMU's DSDT, which defines no _INI, the walk now asks no _STA. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/src/aml.rs | 18 +++------- system/acpiserver/src/host.rs | 6 +++- system/acpiserver/src/init.rs | 68 ++++++++++++++++++++++++++--------- system/acpiserver/src/main.rs | 5 +-- tests/toyos.rs | 4 +-- 5 files changed, 67 insertions(+), 34 deletions(-) diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index 552c8c5b05..b4b3b912c0 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -326,28 +326,20 @@ 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 initialisation runs no `_INI` and reads each device's `_STA` but - /// the CPUs', whose `_STA` writes QEMU's CPU hotplug selector, a port - /// this server writes for no AML, and the HPET's, which reads the timer - /// the kernel drives. Its namespace names no embedded controller, and no + /// 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 mut kernel = machine(QEMU); - // The ISA bridge's interrupt routing registers, PIRQA-D and PIRQE-H. - kernel.config = [0x60, 0x61, 0x62, 0x63, 0x68, 0x69, 0x6A, 0x6B].map(|offset| (toyos_abi::acpi::pci_address(0, 0, 0x1F, 0, offset), 0x0B)).to_vec(); - kernel.driven = vec![(0xfed0_0000, 0xfed0_1000)]; + let kernel = machine(QEMU); let (loaded, kept) = load(&kernel, RSDP); assert_eq!(loaded, Loaded { blocks: vec![Ok(())], s5: Some((0, 0)), handed: true }); 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, 14)); - assert_eq!( - census.refused.counts(), - "_STA: denied by this server: a SystemIO write of a port and width this server asks of the kernel for no AML x2; _STA: denied by this server: a SystemMemory read the kernel refused DeviceMemory, in unlisted firmware memory x1" - ); + 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, &names, crate::devices::tests::GPE0, control_method), crate::devices::Found::default()); diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 112b64fe86..7c1807c2e0 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -41,7 +41,11 @@ //! 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. With none -//! attached the space is denied by name. A port of it the kernel refuses +//! attached the space is denied by name, and one attached is reached whether +//! or not its `_REG` has run: ACPICA refuses a region no handler serves and +//! reaches one a handler does, `_REG` or not (`AcpiEvAddressSpaceDispatch`, +//! `source/components/events/evregion.c`, ACPICA 20260408), the owner's +//! "follow ACPICA". A port of it the kernel refuses //! detaches the controller and is handed to the server ([`Firmware::ended`]), //! which serves it no more. //! diff --git a/system/acpiserver/src/init.rs b/system/acpiserver/src/init.rs index a4ee7cc7b7..92d4b4b447 100644 --- a/system/acpiserver/src/init.rs +++ b/system/acpiserver/src/init.rs @@ -12,13 +12,22 @@ //! interpreter's own access to such a region does. //! //! **Then `\_SB._INI`, unconditionally** ("at the beginning of namespace -//! initialization"), **then every device's `_STA` and `_INI` by Table -//! 6.67**, 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, its -//! ancestors' `_STA` asked again. +//! 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. +//! +//! **Which devices are examined, and a `_STA` refused, are ACPICA's** +//! (`AcpiNsInitializeDevices` and `AcpiNsInitOneDevice`, +//! `source/components/namespace/nsinit.c`, ACPICA 20260408), the owner's +//! "follow ACPICA" for a read of the controller's space before its `_REG`: +//! 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 has its device's `_INI` not run +//! and its children examined. //! //! **EmbeddedControl is announced in each scope that declares a region of //! it once the controller is attached** ([`announce`], `crate::devices`), @@ -250,8 +259,8 @@ fn host_bridge(aml: &mut Aml<'_, K>, names: &Names, device: &str, cen /// 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 none of whose descendants defined an `_INI` since is not -/// asked, on a walk after a run-time load. +/// 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> = @@ -264,7 +273,7 @@ fn walk(aml: &mut Aml<'_, K>, names: &Names, since: u32, census: &mut continue; } closed = None; - if !is_device(named.kind) || (since > 0 && !wanted.contains(named.path.as_str())) { + if !is_device(named.kind) || !wanted.contains(named.path.as_str()) { continue; } if aml.host.stopping { @@ -282,9 +291,9 @@ fn walk(aml: &mut Aml<'_, K>, names: &Names, since: u32, census: &mut } Some(_) => { census.refuse("_STA", &path, &toyos_aml::Error::Type("a _STA that is no Integer (§6.3.7)")); - 0 + continue; } - None => 0, + None => continue, } } }; @@ -377,8 +386,9 @@ mod tests { } /// A refused `_INI` is counted by kind and the walk goes on; a `_STA` - /// that is refused or no Integer takes its device as neither present nor - /// functional, and nothing below it is examined. + /// that is refused or no Integer is counted, its device's `_INI` is not + /// run, and its children are examined, as ACPICA's `AcpiNsInitOneDevice` + /// moves on past a `_STA` that failed. #[test] fn a_refused_ini_or_sta_is_counted_and_the_walk_goes_on() { let dsdt = scope( @@ -392,14 +402,40 @@ mod tests { ]), ); let (kernel, census) = initialised(&dsdt); - assert_eq!(posted(&kernel), [5]); - assert_eq!((census.sb_ini, census.sta, census.ini), (None, 0, 1)); + assert_eq!(posted(&kernel), [0xCC, 5]); + assert_eq!((census.sb_ini, census.sta, census.ini), (None, 0, 2)); 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" ); } + /// As ACPICA's walk: 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 diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index ed4741d082..fb26f5dce4 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -26,8 +26,9 @@ //! initialisation. //! //! **Then the namespace is initialised** ([`init`]): PCI_Config announced -//! where §6.5.4 asks it be, `\_SB._INI`, and every device's `_STA` and -//! `_INI` by Table 6.67, again over whatever a table loaded meanwhile added. +//! 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`], [`devices::attach`]): attached to the AML host in the ECDT's place, so its diff --git a/tests/toyos.rs b/tests/toyos.rs index 83284a7a7a..f2941d3d53 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5928,8 +5928,8 @@ const AMD: Bench = Bench { /// it, and loaded or refused as its [`Bench`] says; `\_S5`'s `SLP_TYPa` /// handed to the kernel, which says it powers this machine off with that /// value, and never does on this row. Then the namespace initialised (ACPI -/// 6.5 §6.5.1): every `_STA` and `_INI` run and none refused but by a ruled -/// refusal, and the controller's `_REG(3, 1)` after them; `\_SB._OSC` +/// 6.5 §6.5.1): every `_STA` and `_INI` the walk runs, none refused but by a +/// ruled refusal, and the controller's `_REG(3, 1)` after them; `\_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 From 15536e2e0b629ac8a461326de4e60343742cff74 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 17:23:13 +0200 Subject: [PATCH 16/30] AML writes every port the kernel does not keep The owner's ruling on #875: AML may write any I/O port but a deny-list of the ports the kernel owns. The server asked the kernel for three port writes and a byte to SMI_CMD and denied every other itself, which refused the AMD bench's interrupt links, whose _STA write the PCI interrupt router's index at 0xC00. Every port write now goes to the kernel, which decides it as it decides a read. The kernel's deny-list is what it declares (COM1, the 8259 pair, the CMOS RTC, the PCI configuration mechanism, the i8042, the reset register, the PM1a control block and the TCO block read-only, and SMI_CMD as a firmware call), and toyos_userbound::firmware::PROTECTED, refused as ProtectedPort: the 8259's edge/level control, a register of the pair the kernel drives that no declaration holds, and the PIT, which the ruling names among the ports the kernel owns. A refused protected port fails the whole access by name: no part of it is made and none is skipped silently. The FADT is no longer read by the server for SMI_CMD: the kernel decides that byte. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- lib/userbound/src/firmware.rs | 25 ++++++-- lib/userbound/tests/firmware.rs | 26 ++++++++ sdk/abi/src/acpi.rs | 4 ++ system/acpiserver/src/aml.rs | 8 +-- system/acpiserver/src/host.rs | 109 +++++++++++--------------------- 5 files changed, 89 insertions(+), 83 deletions(-) diff --git a/lib/userbound/src/firmware.rs b/lib/userbound/src/firmware.rs index 35a1a7b860..f5435ad788 100644 --- a/lib/userbound/src/firmware.rs +++ b/lib/userbound/src/firmware.rs @@ -53,8 +53,10 @@ //! 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: the +//! owner's "Allow, like Linux", whose `acpi_os_write_port` writes whatever +//! ACPICA's port protection passes. //! //! **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`]). @@ -358,15 +360,30 @@ pub enum PortVerdict { Refused(Refused), } +/// The ports kept from AML that no declaration of the kernel's holds, each +/// `(first, last)`, as ACPICA's `AcpiProtectedPorts` writes them +/// (`source/components/hardware/hwvalid.c`, ACPICA 20260408): the 8259 +/// pair's edge/level control, which ACPICA refuses whatever the firmware +/// asks `_OSI`, as it refuses the pair the kernel declares; and the PIT, +/// both of ACPICA's rows for it, which the owner's ruling names among the +/// ports the kernel owns. ACPICA refuses the PIT, and the rest of its table, +/// only to a firmware whose newest `_OSI` is Windows XP's, and the rest is +/// written here as Linux writes it. +pub const PROTECTED: [(u16, u16); 3] = [(0x40, 0x43), (0x48, 0x4B), (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 b84406195d..ff9f556cea 100644 --- a/lib/userbound/tests/firmware.rs +++ b/lib/userbound/tests/firmware.rs @@ -632,6 +632,32 @@ fn a_port_answers_as_its_declaration_says() { } } +/// The ports ACPICA keeps from AML that no declaration holds, the 8259 +/// pair's edge/level control and the PIT, are refused both ways, an access +/// at either end of a run or across into one as well; the ports beside each +/// run, and the PCI interrupt router's index and data, pass. +#[test] +fn a_port_acpica_protects_is_refused_though_nothing_declares_it() { + for write in [None, Some(0)] { + for (at, width) in [ + (0x40, Width::Byte), + (0x43, Width::Byte), + (0x42, Width::Word), + (0x3E, Width::DWord), + (0x48, Width::DWord), + (0x4B, Width::Byte), + (0x4D0, Width::Byte), + (0x4D1, Width::Byte), + (0x4CE, Width::DWord), + ] { + assert_eq!(port(standing, at, width, write), refused_port(Refused::ProtectedPort), "{width:?} at {at:#x}"); + } + for (at, width) in [(0x3C, Width::DWord), (0x44, Width::DWord), (0x4C, Width::Byte), (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 8aa1926deb..fb4fda5c62 100644 --- a/sdk/abi/src/acpi.rs +++ b/sdk/abi/src/acpi.rs @@ -215,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 { @@ -239,6 +242,7 @@ impl Refused { 17 => Self::KernelCommand, 18 => Self::CommandSpan, 19 => Self::CommandRate, + 20 => Self::ProtectedPort, _ => return None, }) } diff --git a/system/acpiserver/src/aml.rs b/system/acpiserver/src/aml.rs index b4b3b912c0..e535c37ee1 100644 --- a/system/acpiserver/src/aml.rs +++ b/system/acpiserver/src/aml.rs @@ -2,8 +2,7 @@ //! loaded into one namespace ([`toyos_aml`]), and what this server asks of it. //! //! [`load`] attaches the controller the ECDT names, which AML may reach -//! before any control method runs (ACPI 6.5 §6.5.4), and the FADT's -//! `SMI_CMD`, a byte to which is a call into the firmware, to the host; then +//! 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 @@ -140,11 +139,6 @@ fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Fi println!("{STOPPING}"); return loaded; } - match toyos_acpi::find_table(&tables, rsdp, b"FACP", toyos_acpi::FADT_FOR_FIXED_HARDWARE).map(|fadt| toyos_acpi::fixed_hardware(&fadt)) { - Ok(Ok(fixed)) => host.smi_cmd = fixed.smi_cmd.map(|smi| smi.port), - Ok(Err(why)) => println!("acpiserver: the FADT's fixed hardware does not decode, so this machine's AML calls no firmware: {why:?}"), - Err(why) => println!("acpiserver: no FADT was read, so this machine's AML calls no firmware: {}", unread(&why, |at| tables.refused(at))), - } early_controller(kernel, &tables, rsdp, host); let count = blocks.len(); diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 7c1807c2e0..32cb576845 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -7,15 +7,12 @@ //! the kernel's to make or refuse**, and a refusal it names is denied under //! that name. //! -//! **Only the port writes in [`FORWARDED`], and a byte to the FADT's -//! `SMI_CMD`, 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, and a call into the firmware, which the kernel makes or refuses by -//! the byte and the rate (`toyos_userbound::firmware`), the owner's "AML may -//! write exactly the SMI command port the FADT declares". Every other port -//! write is denied by name and never reaches the kernel: a reset-capable -//! port, a data register. The reset register is the kernel's to keep wherever -//! it is. +//! **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: the owner's "Allow, like +//! Linux", which writes every port ACPICA's port protection passes. The +//! kernel refuses the ports it owns and those ACPICA keeps from AML +//! (`toyos_userbound::firmware::PROTECTED`), 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 @@ -111,17 +108,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; @@ -318,8 +304,6 @@ pub struct Firmware<'k, K> { said: Ledger, /// The port writes asked of the kernel, by port and width. written: Ledger, - /// The FADT's `SMI_CMD`, a byte to which is a call into the firmware. - pub smi_cmd: Option, /// 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. @@ -357,7 +341,6 @@ impl<'k, K: Kernel> Firmware<'k, K> { refused: Ledger::default(), said: Ledger::default(), written: Ledger::default(), - smi_cmd: None, buttons: Vec::new(), latched: Latched::default(), notified: Default::default(), @@ -526,25 +509,24 @@ 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)) && (Some(port), width) != (self.smi_cmd, toyos_aml::Access::Byte) => { - 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 self.written_through(Space::SystemIo, u64::from(port), width, value, at); + 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(address) => return self.written_through(Space::SystemMemory, address, width, value, at), - 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) } - other => return Err(self.unserved(other, width)), - }; - Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) + other => Err(self.unserved(other, width)), + } } fn sleep(&mut self, ms: u64) { @@ -914,60 +896,43 @@ pub mod tests { assert_eq!(host.pages.by_type(), "none"); } - /// A port write in [`FORWARDED`], and a byte to the FADT's `SMI_CMD` once - /// the load named it, is asked of the kernel, and made or refused as it - /// answers; every other port and width, `SMI_CMD` at another width, - /// configuration space, and the controller's space with no controller - /// attached are denied by name, none of them asked of the kernel. + /// Every port write is asked of the kernel and made or refused as it + /// answers, the owner's "Allow, like Linux": 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_or_a_byte_to_smi_cmd_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), (0xB2, 0)]; + kernel.ports = vec![(0xC00, 0), (0xC01, 0), (0x80, 0), (0xB2, 0)]; let mut host = Firmware::new(&kernel); - assert_eq!(host.write(Address::Io(0xB2), Byte, 0xA0), Err(Denied(UNFORWARDED.into())), "SMI_CMD before the FADT named it"); - host.smi_cmd = Some(0xB2); - assert_eq!(host.write(Address::Io(0x80), Word, 0x28), Ok(())); - assert_eq!(host.write(Address::Io(0x72), Byte, 0x08), Ok(())); - assert_eq!(host.write(Address::Io(0xB2), Byte, 0xA0), 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, 0xB2, Width::Byte, 0xA0), - Access::write(Space::SystemIo, 0xCD6, Width::Byte, 0x60), + 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`, `SMI_CMD` at a - // width no call is, 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, Word, 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 }; 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(), 4, "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; 0xb2 Byte 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 From eb9419edcf5104d439c5bc8be41e3d9c23a4342f Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 17:23:13 +0200 Subject: [PATCH 17/30] toyos-aml: a method refused at the heap bound gives back what it made The owner's ruling on #875: a method the interpreter runs out of memory for has its changes rolled back and fails by name. Ending it as any other method ends meets that: what it created and its locals and arguments go at its exit, and every Mutex it held is released. A store into an object that outlives the method is not undone. The interpreter already did each of these: its method arena is unwound however the term list ends, the frame drops its locals, and finish releases every Mutex. This commit holds it with a test that fills the bound through one method's named object and locals while it holds a Mutex, and reads the heap given back to the byte, the Mutex free, the refusal named, and a method of a mebibyte's buffer evaluated after. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/aml/src/exec.rs | 11 ++++++--- system/acpiserver/aml/src/lib.rs | 8 +++++-- system/acpiserver/aml/tests/heap.rs | 36 +++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/system/acpiserver/aml/src/exec.rs b/system/acpiserver/aml/src/exec.rs index b0d9717e2f..2c1e64b6c2 100644 --- a/system/acpiserver/aml/src/exec.rs +++ b/system/acpiserver/aml/src/exec.rs @@ -260,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, as ACPICA's + /// `AcpiExReleaseAllMutexes` lets go at a refused evaluation's end, 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"). /// 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); @@ -441,6 +443,9 @@ impl<'a> Machine<'a> { 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, as ACPICA's `AcpiDsTerminateControlMethod` + // deletes "everything that the method created" (ACPICA 20260408). self.ns.unwind(made); if m.serialized { self.levels.pop(); diff --git a/system/acpiserver/aml/src/lib.rs b/system/acpiserver/aml/src/lib.rs index 68d8f3707b..ccf2db9521 100644 --- a/system/acpiserver/aml/src/lib.rs +++ b/system/acpiserver/aml/src/lib.rs @@ -10,8 +10,12 @@ //! 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 and every Mutex they held, and +//! keeps what they stored into an object that outlives them: ACPICA's end of +//! a method refused for memory, the owner's "follow Linux/ACPICA". 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, diff --git a/system/acpiserver/aml/tests/heap.rs b/system/acpiserver/aml/tests/heap.rs index 4f6c3fb1ce..adddc21a0e 100644 --- a/system/acpiserver/aml/tests/heap.rs +++ b/system/acpiserver/aml/tests/heap.rs @@ -494,3 +494,39 @@ fn an_evaluation_holds_at_most_256_mutexes() { 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 ACPICA ends one refused for memory +/// (`AE_NO_MEMORY`), as any other: what it made, a named object and its +/// locals' buffers, is given back to the byte, the Mutex it held is released, +/// 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, +/// as ACPICA keeps it (`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("MUT0"), &0xFFFFu16.to_le_bytes()]); + let body = cat(&[ + &[0x5B, 0x01], + &name("MUT0"), + &[0x00], + &method("MAKE", 0, &ret(&buffer(&int(0x10_0000), &[]))), + &method("TAKE", 0, &cat(&[&acquire, &[0x5B, 0x27], &name("MUT0"), &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()))); + assert_eq!(i.evaluate(&mut Sink, "\\TAKE", &[]), Ok(Value::Integer(1)), "the refused method's Mutex is held"); + assert_eq!(i.evaluate(&mut Sink, "\\MAKE", &[]), Ok(Value::Buffer(vec![0; 0x10_0000]))); +} From 7c46e7daca2666dcfd083e7824f19c0517f283e1 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 17:24:11 +0200 Subject: [PATCH 18/30] issues: the track records the three rulings, and a finding the namespace walk turned up The heap bullet's exit is met by a_method_refused_at_the_bound_gives_back_what_it_made, and what a method stores into an object that outlives it staying is the ruled state. The port-write bullet goes with the weakness: every port write is the kernel's to decide. The T14's walk count is the one the check outside the tree read after the walk began examining only a device with an _INI at or below it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...no-thermal-zones-ini-which-acpicas-does.md | 17 ++++++ ...ine-in-acpi-mode-and-interprets-its-aml.md | 59 ++++++++----------- 2 files changed, 41 insertions(+), 35 deletions(-) create mode 100644 issues/the-namespace-walk-runs-no-thermal-zones-ini-which-acpicas-does.md diff --git a/issues/the-namespace-walk-runs-no-thermal-zones-ini-which-acpicas-does.md b/issues/the-namespace-walk-runs-no-thermal-zones-ini-which-acpicas-does.md new file mode 100644 index 0000000000..870c8e9074 --- /dev/null +++ b/issues/the-namespace-walk-runs-no-thermal-zones-ini-which-acpicas-does.md @@ -0,0 +1,17 @@ +--- +status: open +kind: finding +opened: 2026-10-12 +--- + +# The namespace walk runs no thermal zone's `_INI`, which ACPICA's does + +The server's initialisation (`system/acpiserver/src/init.rs`) examines a +Device or Processor object by its `_STA` and runs its `_INI`, and passes a +ThermalZone by: a thermal zone's `_STA` is not asked and its `_INI` is never +run, and nothing below one is examined. ACPICA 20260408's walk examines a +thermal zone as it does a device (`AcpiNsFindIniMethods` and +`AcpiNsInitOneDevice`, `source/components/namespace/nsinit.c`), and #875 +made the walk ACPICA's in which devices it examines. Neither bench's +thermal zone names an `_STA` or an `_INI`, read outside the tree on both +dumps (#875), so neither machine's initialisation differs for 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 264e1d2e75..047106fad7 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 @@ -198,19 +198,22 @@ otherwise pay to find again: 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 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 +- **A method refused at the heap bound ends as ACPICA ends one refused + for memory**, the owner's "follow Linux/ACPICA" (#875): everything it + made and every Mutex it held is given back, 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, as ACPICA + keeps it, 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; #875 proposes it - in its body, unbuilt. **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`, @@ -264,8 +267,9 @@ 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. Outside the tree on the T14's dump, the server's walk - reads 56 `_STA` and runs 37 `_INI`, two refused at the ten-second wait + siblings take. Outside the tree on the T14's dump, the server's walk, + which examines what ACPICA's 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 @@ -369,27 +373,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. A byte to the FADT's `SMI_CMD` - is asked of the kernel since the owner's ruling (#875). Outside the tree - on the AMD bench's dump, its eight interrupt links' `_STA` each write a - byte to port 0xC00, the PCI interrupt router's index, and are refused: - that reading goes to the owner. Owner: this stage. **Exit**: the owner - rules that write in the list or refused for good, and the T14's - `counters` row reads a call `SMI_CMD` took with the readings below. +stores a byte to `SMI_CMD`, leaves open. The server asks the kernel for +every port write its AML makes, the owner's "Allow, like Linux" (#875), and +the kernel refuses the ports it declares and those ACPICA keeps from AML +(`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: From a4111b4ca51b837bd6a468b9394e2e77d7ace739 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 17:25:26 +0200 Subject: [PATCH 19/30] toyos-aml: the refused method's test holds the Global Lock let go, which a reused Mutex could not show A Mutex left held by a refused evaluation is acquired again without a refusal, the interpreter's acquisitions being recursive, so the test's first Mutex could not tell finish's release from none. \_GL can: a hold left behind would take nothing from the firmware at the next Acquire. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/aml/tests/heap.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/system/acpiserver/aml/tests/heap.rs b/system/acpiserver/aml/tests/heap.rs index adddc21a0e..c1593e9632 100644 --- a/system/acpiserver/aml/tests/heap.rs +++ b/system/acpiserver/aml/tests/heap.rs @@ -497,19 +497,17 @@ fn an_evaluation_holds_at_most_256_mutexes() { /// A method refused at the bound ends as ACPICA ends one refused for memory /// (`AE_NO_MEMORY`), as any other: what it made, a named object and its -/// locals' buffers, is given back to the byte, the Mutex it held is released, -/// 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, -/// as ACPICA keeps it (`what_is_held_live_is_bounded_in_sum`). +/// 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, as +/// ACPICA keeps it (`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("MUT0"), &0xFFFFu16.to_le_bytes()]); + let acquire = cat(&[&[0x5B, 0x23], &name("\\_GL"), &0xFFFFu16.to_le_bytes()]); let body = cat(&[ - &[0x5B, 0x01], - &name("MUT0"), - &[0x00], &method("MAKE", 0, &ret(&buffer(&int(0x10_0000), &[]))), - &method("TAKE", 0, &cat(&[&acquire, &[0x5B, 0x27], &name("MUT0"), &ret(&int(1))])), + &method("TAKE", 0, &cat(&[&acquire, &[0x5B, 0x27], &name("\\_GL"), &ret(&int(1))])), &method( "FILL", 0, @@ -527,6 +525,8 @@ fn a_method_refused_at_the_bound_gives_back_what_it_made() { 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()))); - assert_eq!(i.evaluate(&mut Sink, "\\TAKE", &[]), Ok(Value::Integer(1)), "the refused method's Mutex is held"); + 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]))); } From 270e2a854fee9e027637afdaf0fd69f0cce00f8d Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 17:26:34 +0200 Subject: [PATCH 20/30] acpiserver: the walk's test gives the refused _STA's device a child, so a closed subtree reds it The mutant that closed a refused _STA's subtree stayed green: that device had no child for the walk to skip. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/src/init.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/system/acpiserver/src/init.rs b/system/acpiserver/src/init.rs index 92d4b4b447..2d44c5bf37 100644 --- a/system/acpiserver/src/init.rs +++ b/system/acpiserver/src/init.rs @@ -397,13 +397,16 @@ mod tests { // 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( + "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, 5]); - assert_eq!((census.sb_ini, census.sta, census.ini), (None, 0, 2)); + 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" From b3bfc9742d3bc157fff73ca48e9654dbf5bfb674 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 19:58:22 +0200 Subject: [PATCH 21/30] tests: acpi_press_across_a_lock_wait's table declares a device whose _INI takes the Global Lock with no bound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That test held the order bac5ccca6 set, a press latched during the load served before the namespace is initialised, only because the walk then asked q35's own _STAs, one of which takes the lock with no bound. The walk now examines only a device with an _INI at or below it (ACPI 6.5 §6.5.1), and q35's DSDT defines none, so the test no longer met an initialisation that waits on the firmware's hold. The table QEMU is handed now declares \_SB.GLI, whose _INI Acquires \_GL with no bound: a server that initialised before serving the press would end at RELEASE. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- tests/common/power.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/common/power.rs b/tests/common/power.rs index 580ac51948..35b34fa72a 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -784,10 +784,15 @@ const ONE_TAKE_CONTENDED: &str = "; took the Global Lock 1 times, 1 of them from /// PM1a event block at `pm1`, then Acquires `\_GL` within `ms`, giving it /// back where that took it and Notifying `\_SB.GLW` with 0x80 where it timed /// out. It waits by Stall, which keeps the processor: a Sleep serves the SCI -/// (`sci::await_sleep`) and would take the press it waits for. +/// (`sci::await_sleep`) and would take the press it waits for. It declares +/// the device `\_SB.GLI` too, whose `_INI`, run by the namespace's +/// initialisation after the load, Acquires `\_GL` with no bound: a server +/// that initialised before serving the press would end at the firmware's +/// hold. /// /// ```text /// Device (\_SB.GLW) {} +/// Device (\_SB.GLI) { Method (_INI) { Acquire (\_GL, 0xFFFF) Release (\_GL) } } /// OperationRegion (PM1S, SystemIO, pm1, 2) /// Field (PM1S, WordAcc, NoLock, Preserve) { Offset (1), PBST, 1 } /// While (!PBST) { Stall (100) } @@ -797,6 +802,7 @@ fn ssdt_acquiring_the_global_lock(pm1: u16, ms: u16) -> Vec { const GL: &[u8] = b"\\_GL_"; // `\` and a DualNamePrefix (§20.2.2). const GLW: &[u8] = b"\\\x2E_SB_GLW_"; + const GLI: &[u8] = b"\\\x2E_SB_GLI_"; const PM1S: &[u8] = b"PM1S"; const PBST: &[u8] = b"PBST"; // A PkgLength of one byte (§20.2.4): itself and what follows, under 64. @@ -810,8 +816,12 @@ fn ssdt_acquiring_the_global_lock(pm1: u16, ms: u16) -> Vec { let acquire = [&[0x5B, 0x23], GL, &ms.to_le_bytes()].concat(); let notify = [&[0x86], GLW, &[0x0A, 0x80]].concat(); let release = [&[0x5B, 0x27], GL].concat(); + // An Acquire of no bound (0xFFFF, §19.6.2), then a Release, as a method of no arguments (MethodFlags 0). + let unbounded = [&[0x5B, 0x23], GL, &0xFFFFu16.to_le_bytes(), &release].concat(); + let ini = package(&[0x14], &[b"_INI".as_slice(), &[0x00], &unbounded].concat()); let aml = [ package(&[0x5B, 0x82], GLW), + package(&[0x5B, 0x82], &[GLI, &ini].concat()), region, field, wait, @@ -844,7 +854,9 @@ fn hex_after(line: &str, after: &str) -> Result { /// claim; the table QEMU adds waits as it loads for the press, sent once the /// server has armed, and only then Acquires the lock within [`ACQUIRE_MS`]. /// Then the press must power the machine off, after the Acquire said it timed -/// out. +/// out, and before the namespace's initialisation, whose `\_SB.GLI._INI` +/// takes the lock with no bound and would end the server at the firmware's +/// hold. pub fn acpi_press_across_a_lock_wait(probe: (String, Vec)) -> Result<(), String> { let case = super::compile::repo_root().join("tests/images/acpicase.toml"); let mut qemu = QemuInstance::boot_with_options( From 117858e2f45b8dc8b4da71ee4735a99e458c58ce Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 01:50:45 +0200 Subject: [PATCH 22/30] toyos-userbound: PROTECTED keeps the 8259's edge/level control alone, and passes the PIT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PIT is no port the kernel declares or drives, so it comes off the list, as the orchestrator ruled on #875 (6130332818, 6131125468). What stays, ELCR1 and ELCR2, Intel lists among the 8259 pair's own registers (332691-003 Vol. 2, Table 28-1), and the kernel drives the pair. The header cites ACPI 6.5's exclusive access to a region's addresses (§5.5.2.4) for the rule, and no other implementation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- lib/userbound/src/firmware.rs | 24 ++++++++++++------------ lib/userbound/tests/firmware.rs | 29 +++++++++++++---------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/lib/userbound/src/firmware.rs b/lib/userbound/src/firmware.rs index f5435ad788..dfaf290104 100644 --- a/lib/userbound/src/firmware.rs +++ b/lib/userbound/src/firmware.rs @@ -54,9 +54,11 @@ //! //! **A port is the kernel's to answer where the kernel declared it** //! ([`crate::port::Mediated`]), another claim's where a row names it, refused -//! where [`PROTECTED`] names it, and passes otherwise, a write as a read: the -//! owner's "Allow, like Linux", whose `acpi_os_write_port` writes whatever -//! ACPICA's port protection passes. +//! 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`]). @@ -361,15 +363,13 @@ pub enum PortVerdict { } /// The ports kept from AML that no declaration of the kernel's holds, each -/// `(first, last)`, as ACPICA's `AcpiProtectedPorts` writes them -/// (`source/components/hardware/hwvalid.c`, ACPICA 20260408): the 8259 -/// pair's edge/level control, which ACPICA refuses whatever the firmware -/// asks `_OSI`, as it refuses the pair the kernel declares; and the PIT, -/// both of ACPICA's rows for it, which the owner's ruling names among the -/// ports the kernel owns. ACPICA refuses the PIT, and the rest of its table, -/// only to a firmware whose newest `_OSI` is Windows XP's, and the rest is -/// written here as Linux writes it. -pub const PROTECTED: [(u16, u16); 3] = [(0x40, 0x43), (0x48, 0x4B), (0x4D0, 0x4D1)]; +/// `(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` and of [`PROTECTED`]. diff --git a/lib/userbound/tests/firmware.rs b/lib/userbound/tests/firmware.rs index ff9f556cea..7c9d896955 100644 --- a/lib/userbound/tests/firmware.rs +++ b/lib/userbound/tests/firmware.rs @@ -632,27 +632,24 @@ fn a_port_answers_as_its_declaration_says() { } } -/// The ports ACPICA keeps from AML that no declaration holds, the 8259 -/// pair's edge/level control and the PIT, are refused both ways, an access -/// at either end of a run or across into one as well; the ports beside each -/// run, and the PCI interrupt router's index and data, pass. +/// 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 a_port_acpica_protects_is_refused_though_nothing_declares_it() { +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::Byte), - (0x43, Width::Byte), - (0x42, Width::Word), - (0x3E, Width::DWord), + (0x40, Width::DWord), (0x48, Width::DWord), - (0x4B, Width::Byte), - (0x4D0, Width::Byte), - (0x4D1, Width::Byte), - (0x4CE, Width::DWord), + (0x4CC, Width::DWord), + (0x4D2, Width::Word), + (0xC00, Width::Byte), + (0xC01, Width::Byte), ] { - assert_eq!(port(standing, at, width, write), refused_port(Refused::ProtectedPort), "{width:?} at {at:#x}"); - } - for (at, width) in [(0x3C, Width::DWord), (0x44, Width::DWord), (0x4C, Width::Byte), (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}"); } } From f61d38f9497accad9d47c5ba12afa2ac20778cc7 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 01:50:45 +0200 Subject: [PATCH 23/30] acpiserver, toyos-aml: each rule cites ACPI 6.5 or states ToyOS's own, and no other implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By the owner's clean-room ruling (#875, 6131125468): the walk looks for _INI methods as §6.5.1 has it, a refused _STA is ToyOS's own rule where §6.5.1 is silent, a region is available while its handler is (§6.5.4), and a method's objects go at its exit (§5.5.2.3) with its Mutexes (§19.6.88). The thermal-zone finding is deleted: §6.5.1 puts _INI under a device object, so a walk that runs no thermal zone's is no weakness. The SSDT8 issue cites §19.6.120, Scope, and no other OS's log. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...t8-scopes-a-controller-no-table-defines.md | 7 ++---- ...no-thermal-zones-ini-which-acpicas-does.md | 17 -------------- ...ine-in-acpi-mode-and-interprets-its-aml.md | 20 ++++++++--------- system/acpiserver/aml/src/exec.rs | 14 ++++++------ system/acpiserver/aml/src/lib.rs | 7 +++--- system/acpiserver/aml/tests/heap.rs | 7 +++--- system/acpiserver/src/host.rs | 21 +++++++++--------- system/acpiserver/src/init.rs | 22 +++++++++---------- 8 files changed, 47 insertions(+), 68 deletions(-) delete mode 100644 issues/the-namespace-walk-runs-no-thermal-zones-ini-which-acpicas-does.md 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 index 3d7227a8bd..45035d5ab2 100644 --- 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 @@ -8,12 +8,9 @@ opened: 2026-10-12 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.122 reads "The object referred to by Location must already exist in +§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. Linux -on the same boot logs the same defect, `ACPI BIOS Error (bug): Could not -resolve symbol [\_SB.PCI0.LPC0.EC0], AE_NOT_FOUND`, and keeps the rest of -the block. +`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. diff --git a/issues/the-namespace-walk-runs-no-thermal-zones-ini-which-acpicas-does.md b/issues/the-namespace-walk-runs-no-thermal-zones-ini-which-acpicas-does.md deleted file mode 100644 index 870c8e9074..0000000000 --- a/issues/the-namespace-walk-runs-no-thermal-zones-ini-which-acpicas-does.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-10-12 ---- - -# The namespace walk runs no thermal zone's `_INI`, which ACPICA's does - -The server's initialisation (`system/acpiserver/src/init.rs`) examines a -Device or Processor object by its `_STA` and runs its `_INI`, and passes a -ThermalZone by: a thermal zone's `_STA` is not asked and its `_INI` is never -run, and nothing below one is examined. ACPICA 20260408's walk examines a -thermal zone as it does a device (`AcpiNsFindIniMethods` and -`AcpiNsInitOneDevice`, `source/components/namespace/nsinit.c`), and #875 -made the walk ACPICA's in which devices it examines. Neither bench's -thermal zone names an `_STA` or an `_INI`, read outside the tree on both -dumps (#875), so neither machine's initialisation differs for 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 047106fad7..e8c365fc93 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 @@ -198,16 +198,16 @@ otherwise pay to find again: 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 ACPICA ends one refused - for memory**, the owner's "follow Linux/ACPICA" (#875): everything it - made and every Mutex it held is given back, and it is refused by the - bound's name. **Exit, met by #875**: +- **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, as ACPICA - keeps it, so an interpreter filled that way stays full: an Integer still + 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 @@ -268,7 +268,7 @@ otherwise pay to find again: 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. Outside the tree on the T14's dump, the server's walk, - which examines what ACPICA's does, reads 12 `_STA` and runs 37 `_INI`, + 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. @@ -374,9 +374,9 @@ mediated access, leaves open: What the firmware call, which the kernel makes for the server where its AML stores a byte to `SMI_CMD`, leaves open. The server asks the kernel for -every port write its AML makes, the owner's "Allow, like Linux" (#875), and -the kernel refuses the ports it declares and those ACPICA keeps from AML -(`PROTECTED` in `lib/userbound/src/firmware.rs`). Outside the tree on the +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. diff --git a/system/acpiserver/aml/src/exec.rs b/system/acpiserver/aml/src/exec.rs index 2c1e64b6c2..340f134ab2 100644 --- a/system/acpiserver/aml/src/exec.rs +++ b/system/acpiserver/aml/src/exec.rs @@ -260,11 +260,11 @@ impl<'a> Machine<'a> { self.depth -= 1; } - /// Ends an evaluation: whatever it still holds is let go, as ACPICA's - /// `AcpiExReleaseAllMutexes` lets go at a refused evaluation's end, 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); @@ -444,8 +444,8 @@ impl<'a> Machine<'a> { 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, as ACPICA's `AcpiDsTerminateControlMethod` - // deletes "everything that the method created" (ACPICA 20260408). + // 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(); diff --git a/system/acpiserver/aml/src/lib.rs b/system/acpiserver/aml/src/lib.rs index ccf2db9521..f34e07545f 100644 --- a/system/acpiserver/aml/src/lib.rs +++ b/system/acpiserver/aml/src/lib.rs @@ -11,9 +11,10 @@ //! 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. An evaluation refused, at the heap bound as at any other, -//! gives back everything its methods made and every Mutex they held, and -//! keeps what they stored into an object that outlives them: ACPICA's end of -//! a method refused for memory, the owner's "follow Linux/ACPICA". What the +//! 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. //! diff --git a/system/acpiserver/aml/tests/heap.rs b/system/acpiserver/aml/tests/heap.rs index c1593e9632..3bc0801354 100644 --- a/system/acpiserver/aml/tests/heap.rs +++ b/system/acpiserver/aml/tests/heap.rs @@ -495,13 +495,12 @@ fn an_evaluation_holds_at_most_256_mutexes() { 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 ACPICA ends one refused for memory -/// (`AE_NO_MEMORY`), as any other: what it made, a named object and its +/// 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, as -/// ACPICA keeps it (`what_is_held_live_is_bounded_in_sum`). +/// 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()]); diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 32cb576845..ef9275b5a9 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -8,10 +8,9 @@ //! that name. //! //! **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: the owner's "Allow, like -//! Linux", which writes every port ACPICA's port protection passes. The -//! kernel refuses the ports it owns and those ACPICA keeps from AML -//! (`toyos_userbound::firmware::PROTECTED`), and makes a byte to the FADT's +//! 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**: @@ -37,12 +36,12 @@ //! [`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. With none -//! attached the space is denied by name, and one attached is reached whether -//! or not its `_REG` has run: ACPICA refuses a region no handler serves and -//! reaches one a handler does, `_REG` or not (`AcpiEvAddressSpaceDispatch`, -//! `source/components/events/evregion.c`, ACPICA 20260408), the owner's -//! "follow ACPICA". A port of it the kernel refuses +//! 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. //! @@ -897,7 +896,7 @@ pub mod tests { } /// Every port write is asked of the kernel and made or refused as it - /// answers, the owner's "Allow, like Linux": the AMD bench's interrupt + /// 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 diff --git a/system/acpiserver/src/init.rs b/system/acpiserver/src/init.rs index 2d44c5bf37..72750de074 100644 --- a/system/acpiserver/src/init.rs +++ b/system/acpiserver/src/init.rs @@ -20,14 +20,15 @@ //! 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. //! -//! **Which devices are examined, and a `_STA` refused, are ACPICA's** -//! (`AcpiNsInitializeDevices` and `AcpiNsInitOneDevice`, -//! `source/components/namespace/nsinit.c`, ACPICA 20260408), the owner's -//! "follow ACPICA" for a read of the controller's space before its `_REG`: -//! 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 has its device's `_INI` not run -//! and its children examined. +//! **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`), @@ -387,8 +388,7 @@ mod tests { /// 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, as ACPICA's `AcpiNsInitOneDevice` - /// moves on past a `_STA` that failed. + /// run, and its children are examined. #[test] fn a_refused_ini_or_sta_is_counted_and_the_walk_goes_on() { let dsdt = scope( @@ -413,7 +413,7 @@ mod tests { ); } - /// As ACPICA's walk: a device with no `_INI` at or below it is not + /// §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` From 637409f885c6bf1ec1fb8cb5e8da098e2f0237ff Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 01:50:54 +0200 Subject: [PATCH 24/30] tests: acpi_tables_loaded is held to the server's power sources, and the AMD row expects its ruled config write As ruled on #875 (6131125468): - acpi_hold waits on the server's power sources line, which it writes on every machine with a battery at the end of its start, through acpiserver_api::POWER_SOURCES; acpi_tables_loaded's judge reads the log up to it. A count of the controller's events is no bounded event on an idle machine, so acpi_server_events, whose claim it is, waits for it in a job of its own, acpi_counted, and holds no other row. - The AMD bench's row expects the controller's _REG(3, 1) and the adapter's _PSR refused ConfigWrite, as it expects SSDT8 refused; each bench's ruled refusals are now met, not only allowed. The cost is recorded with AMD's PPR 55922 rev 3.06, which documents no register behind the root complex's 0xB8 and 0xBC. - The bench's block counts are the XSDT's and the FADT's, cited as such. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...-for-a-write-to-the-root-complexs-index.md | 55 +++++++++++++ lib/acpiserver-api/src/lib.rs | 5 ++ system/acpiserver/src/battery.rs | 2 +- tests/guest/rust-tests/Cargo.toml | 2 +- .../guest/rust-tests/src/bin/acpi_counted.rs | 27 +++++++ tests/guest/rust-tests/src/bin/acpi_hold.rs | 21 ++--- tests/toyos.rs | 77 ++++++++++++------- 7 files changed, 149 insertions(+), 40 deletions(-) create mode 100644 issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.md create mode 100644 tests/guest/rust-tests/src/bin/acpi_counted.rs 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 0000000000..d2c6015ec0 --- /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 `855eff969` +(`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/lib/acpiserver-api/src/lib.rs b/lib/acpiserver-api/src/lib.rs index fb09b38c7e..cf78b210d8 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: "; diff --git a/system/acpiserver/src/battery.rs b/system/acpiserver/src/battery.rs index 07b1a31d89..a7a8516c54 100644 --- a/system/acpiserver/src/battery.rs +++ b/system/acpiserver/src/battery.rs @@ -360,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)) } diff --git a/tests/guest/rust-tests/Cargo.toml b/tests/guest/rust-tests/Cargo.toml index ced2d9e744..dc8558d432 100644 --- a/tests/guest/rust-tests/Cargo.toml +++ b/tests/guest/rust-tests/Cargo.toml @@ -17,7 +17,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" } # A block service's session, its grants and the partition types, for # `partition_grant` and `update_idle_slot`. 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 0000000000..cefea7a8ee --- /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 f0f0634e9b..ce0e9032d9 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/toyos.rs b/tests/toyos.rs index a17f43e258..41571c820b 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -171,10 +171,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 @@ -620,11 +624,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]), }, ), @@ -642,7 +646,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, @@ -1103,10 +1108,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", &[], &[]) @@ -5838,6 +5843,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 ")?; @@ -5875,12 +5881,12 @@ fn acpi_events_on_metal(back: &metal::Readback) -> Result<(), String> { Ok(()) } -/// What one bench's tables are, as a reading of them under another OS and -/// the check outside the tree on their dump (#875) found 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: Linux's count. + /// 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. @@ -5891,16 +5897,17 @@ struct Bench { /// The blocks the processors' `_OSC` load under ToyOS's claims, by /// signature and length. loaded: &'static [&'static str], - /// The host's refusals the owner ruled, which the namespace's - /// initialisation meets on this machine and which are said, not red. + /// The host's refusals the owner ruled, which this machine's AML meets: + /// each is met, and said, not red. ruled: &'static [&'static str], + /// What the census says of `_REG(3, 1)`, after every `_INI`. + reg: &'static str, } -/// The T14: Linux says `14 ACPI AML tables successfully acquired and loaded`; -/// 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 a device's `_INI` may write memory outside every range the map lists, -/// which the owner ruled refused and recorded. +/// 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` writes memory outside every +/// range the map lists, which the owner ruled refused and recorded. const T14: Bench = Bench { row: "acpi: the ACPI row: PM1a events 0x1800+4, GPE0 0x1860+32, SCI gsi 9 level/high", blocks: 14, @@ -5908,30 +5915,36 @@ const T14: Bench = Bench { s5: "power: S5 is PM1a 0x1804 with SLP_TYPa=7,", loaded: &["SSDT, 459 bytes"], ruled: &["a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory"], + reg: "_REG(3, 1) in 1 scope(s)", }; -/// The AMD bench: Linux counts 17 tables; its SSDT8 ends in a `Scope` of an -/// `EC0` no table defines, refused whole as the owner ruled +/// 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`. +/// 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: &["a SystemMemory write the kernel refused ConfigWrite, in memory of type 11"], + reg: "no _REG run", }; /// The server's load of a bench's tables, every access the kernel's to make -/// for it: each of the machine's definition blocks, as many as Linux names -/// there, fetched through `SYS_ACPI`, summing to zero as its firmware sealed +/// 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, and the controller's `_REG(3, 1)` after them; `\_SB._OSC` +/// 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 @@ -5941,7 +5954,12 @@ const AMD: Bench = Bench { /// 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() @@ -5988,6 +6006,9 @@ fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { if !refused.is_empty() { return Err(format!("the server refused something of this machine's AML: {refused:#?}")); } + if let Some(unmet) = bench.ruled.iter().find(|ruled| !lines.iter().any(|l| l.contains(*ruled))) { + return Err(format!("this bench's AML meets the refusal the owner ruled, and the server never said it: {unmet}")); + } for line in lines.iter().filter(ruled) { eprintln!(" [acpi] a refusal the owner ruled: {}", line.trim()); } @@ -5996,8 +6017,8 @@ fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { if !(refusals == "none" || refusals.split("; ").all(|kind| bench.ruled.iter().any(|ruled| kind.contains(ruled)))) { return Err(format!("the namespace's initialisation refused an evaluation: {census}")); } - if !census.contains("_REG(3, 1) in 1 scope(s)") { - return Err(format!("the controller's space was not announced after every _INI: {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() { From 01523db5d84cab0628210f47d8a4451dd2855836 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 02:50:23 +0200 Subject: [PATCH 25/30] tests: battery_read holds the T14's ruled refusal as acpi_tables_loaded does The T14's round-4 boot at ddc243eaf read battery_read red on "a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory": the refusal the owner ruled on #875 (a memory write outside every range the map describes is refused by name and recorded, not admitted), the one the T14 bench's `ruled` names, and the one acpi_tables_loaded held met on the same boot. battery_on_metal kept its own refusal filter and never consulted the bench's; it now does, so any other refusal still reds it. The write, from the T14's own tables run on the host and read by hand: the embedded controller's _INI calls its hotkey device's wireless set-up, which on a flag the firmware's SMI answers drives one GPIO pad's TX state to 1 through the tables' own PCR helper. It is one byte, 0x01, at SBREG_BAR + Port ID 0x6E + 0xA70: PAD_CFG_DW0 of GPP_A13 in GPIO Community 0, bit 0 GPIOTXSTATE (Intel 500 Series Chipset On-Package PCH datasheet vol. 2, 631120-002, section 27.1.114), inside a 64 KiB window the GPIO controller (INT34C5) lists in its _CRS, and in no range of the firmware's memory map. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- tests/toyos.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/toyos.rs b/tests/toyos.rs index d7aa0440ce..5da3fbab85 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -6103,9 +6103,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 refusals, which `acpi_tables_on_metal` holds met. +/// 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 @@ -6121,7 +6122,11 @@ 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.iter().any(|ruled| l.contains(ruled))) + .collect(); if !refused.is_empty() { return Err(format!("the server refused something of this machine's AML: {refused:#?}")); } From bf6a51068ca8723107784a68889dda4b7f138765 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 02:54:32 +0200 Subject: [PATCH 26/30] issues: the T14's controller and hotkey _INI are refused for writes outside the map The cost of the owner's ruling on the T14, recorded as the AMD bench's is: the controller's _INI stops at a GPIO pad's TX state in the PCH's GPIO Community 0 (a window the GPIO controller's _CRS lists and the firmware's map does not), read on the T14 at ddc243eaf and reproduced on the host by the T14's own tables; the hotkey device's _INI stops at a write the T14's log does not detail, which the host run puts in the processor's MCHBAR. The T14 bench's doc points at it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...-are-refused-for-writes-outside-the-map.md | 64 +++++++++++++++++++ tests/toyos.rs | 6 +- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md 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 0000000000..c93b6a35a1 --- /dev/null +++ b/issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.md @@ -0,0 +1,64 @@ +--- +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. The T14 row +expects the refusal (`T14` in `tests/toyos.rs`). + +**The controller's write**, read on the T14 at #875's `639ce1374` 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/tests/toyos.rs b/tests/toyos.rs index 5da3fbab85..6d7156b794 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5949,8 +5949,10 @@ struct Bench { /// 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` writes memory outside every -/// range the map lists, which the owner ruled refused and recorded. +/// 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, From 5352b0901049e1966b56ffd4a96d9fbe6e2fa33c Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 03:20:27 +0200 Subject: [PATCH 27/30] tests: the T14 bench holds its ruled refusal as the two _INIs by path Round 5's review of #875: the T14 judges excused every line that carried the ruled kind, so acpi_tables_loaded passed a third MemoryType refusal of any path and a census of x2 or x9 alike, and battery_read passed a _BST refused MemoryType. A bench's ruled refusal is now a Ruled: its kind, each line the server says of it by kind, each evaluation it refuses by the path the server says under OWN, and the census's count of it, exactly. Every line carrying the kind but the census is one of those, and each of those is there. battery_read holds the T14's the same way. The judge prints the ruled lines that say a kind, and none of the server's OWN lines. OWN moves to acpiserver-api, which the judge now reads it through. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- lib/acpiserver-api/src/lib.rs | 4 ++ system/acpiserver/src/host.rs | 4 +- tests/toyos.rs | 81 ++++++++++++++++++++++++++++------- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/lib/acpiserver-api/src/lib.rs b/lib/acpiserver-api/src/lib.rs index cf78b210d8..81d8372b17 100644 --- a/lib/acpiserver-api/src/lib.rs +++ b/lib/acpiserver-api/src/lib.rs @@ -28,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/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index ef9275b5a9..5d26576165 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -94,9 +94,7 @@ 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, diff --git a/tests/toyos.rs b/tests/toyos.rs index 1c503e0bd4..6b63ebc91b 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5950,13 +5950,49 @@ struct Bench { /// The blocks the processors' `_OSC` load under ToyOS's claims, by /// signature and length. loaded: &'static [&'static str], - /// The host's refusals the owner ruled, which this machine's AML meets: - /// each is met, and said, not red. - ruled: &'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 @@ -5969,7 +6005,15 @@ const T14: Bench = Bench { refused: &[], s5: "power: S5 is PM1a 0x1804 with SLP_TYPa=7,", loaded: &["SSDT, 459 bytes"], - ruled: &["a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory"], + 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)", }; @@ -5987,7 +6031,16 @@ const AMD: Bench = Bench { refused: &[(9, "a name that does not resolve")], s5: "with SLP_TYPa=5,", loaded: &[], - ruled: &["a SystemMemory write the kernel refused ConfigWrite, in memory of type 11"], + 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", }; @@ -6052,25 +6105,22 @@ 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 ruled = |l: &&&str| bench.ruled.iter().any(|ruled| l.contains(ruled)); 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 ") && !ruled(l)) + .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:#?}")); } - if let Some(unmet) = bench.ruled.iter().find(|ruled| !lines.iter().any(|l| l.contains(*ruled))) { - return Err(format!("this bench's AML meets the refusal the owner ruled, and the server never said it: {unmet}")); - } - for line in lines.iter().filter(ruled) { + 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 == "none" || refusals.split("; ").all(|kind| bench.ruled.iter().any(|ruled| kind.contains(ruled)))) { - return Err(format!("the namespace's initialisation refused an evaluation: {census}")); + 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)); @@ -6116,7 +6166,7 @@ const T14_POWER_SOURCES: &str = "acpiserver: power sources: 1 of 1 control-metho /// The T14's battery as the server read it: the one battery its tables name /// found present once the controller's `_REG` ran, its information read, /// and at least one reading of it with the adapter's state; nothing refused -/// but [`T14`]'s ruled refusals, which `acpi_tables_on_metal` holds met. +/// 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 @@ -6137,11 +6187,12 @@ fn battery_on_metal(back: &metal::Readback) -> Result<(), String> { let refused: Vec<&&str> = lines .iter() .filter(|l| l.contains("acpiserver: refused") || l.contains(" refused: ")) - .filter(|l| !T14.ruled.iter().any(|ruled| l.contains(ruled))) + .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(); From 1b94d83e15edff081c7872809223ea69bef4bb6a Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 03:27:26 +0200 Subject: [PATCH 28/30] acpiserver: the load's latched press is served before initialisation in Server::loaded, and acpi_press_across_a_lock_wait is cut Round 5's review of #875: acpi_press_across_a_lock_wait booted QEMU, launched acpiserver, pressed the button and judged the order of the server's main, an integration test of a userspace program, which reviewer.md's Tests refuses (#890). A module test holds the claim instead. main's order, the load's sleep type and namespace taken, what a Sleep latched during the load served, the SCIs already latched served, then the namespace initialised, moves into Server::loaded, generic over the Kernel. For that, the claim's SCI record, its acknowledgement and the power-off asked of the supervisor are Kernel's: Claim answers them as main did, serve reads the SCI through the same latched, and Scripted answers no SCI and panics with POWERED_OFF where it is asked to power off, as a stop that happens takes the process with it. a_press_latched_during_the_load_powers_off_before_an_ini_that_waits_on_the_firmware: a DSDT whose Sleep takes a press as it loads, and whose \_SB.GLI._INI Acquires \_GL with no bound while the firmware holds it (takes: [Pending], no release), is loaded and handed to Server::loaded; the machine powers off and the take is never asked. Initialising before the latched press reds it: the _INI's take finds the firmware holding the lock past RELEASE. The cut takes what only that test used: acpi_mediated's held arm, BootOptions::acpi_tables and its -acpitable plumbing, and the count acpi_tables_loaded answered. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/acpiserver/src/host.rs | 27 +++- system/acpiserver/src/main.rs | 101 ++++++++---- tests/common/power.rs | 148 +----------------- tests/common/qemu.rs | 17 -- .../guest/rust-tests/src/bin/acpi_mediated.rs | 29 +--- tests/toyos.rs | 11 +- 6 files changed, 104 insertions(+), 229 deletions(-) diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index 5d26576165..a9d6187a8e 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -127,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; @@ -144,6 +145,14 @@ pub trait Kernel { /// 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. @@ -713,6 +722,10 @@ pub mod tests { 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() { @@ -826,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; diff --git a/system/acpiserver/src/main.rs b/system/acpiserver/src/main.rs index fb26f5dce4..7bdc5f49ab 100644 --- a/system/acpiserver/src/main.rs +++ b/system/acpiserver/src/main.rs @@ -20,10 +20,10 @@ //! 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**, 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 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 @@ -153,11 +153,7 @@ 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; - server.served_aml(); - server.latched(); - let names = server.initialise(); + 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()); @@ -265,6 +261,22 @@ impl Kernel for Claim<'_> { 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. @@ -307,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); } } @@ -360,6 +372,26 @@ 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. @@ -682,7 +714,7 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { /// 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:?}"), @@ -766,17 +798,6 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { } impl<'a, P: Ports> Server<'a, Claim<'a>, P> { - /// Each SCI already latched, served without waiting for one. - fn latched(&mut self) { - match self.kernel.dev.irq() { - Ok(record) => self.scis += u64::from(record.count), - Err(SyscallError::WouldBlock) => return, - Err(other) => panic!("acpiserver: the claim's record answered {other:?}"), - } - self.take(); - self.kernel.dev.ack().expect("acpiserver: the claim's acknowledgement"); - } - fn serve(&mut self) -> ! { let mut next_count = Instant::now() + COUNTS; let mut next_read = Instant::now() + battery::POLL; @@ -790,13 +811,7 @@ 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"); + self.latched(); } } } @@ -1245,6 +1260,34 @@ mod tests { 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. diff --git a/tests/common/power.rs b/tests/common/power.rs index 35b34fa72a..3ea40c5afc 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -704,8 +704,8 @@ fn slp_typ_a(line: &str) -> Result { /// 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 { +/// evaluation. +pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial, supplied: &str) -> Result<(), String> { 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())); @@ -720,7 +720,7 @@ pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial, supplie 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) + Ok(()) } /// A press of q35's power button stops the machine through ToyOS's own path: @@ -760,145 +760,3 @@ pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { eprintln!(" [power] the press: {ACPI_PRESSED}"); Ok(()) } - -/// The PM1a event block QEMU's q35 firmware sets, whose status -/// [`ssdt_acquiring_the_global_lock`] reads: the table is built before the -/// boot, which then says the block it found. -const Q35_PM1A_EVENTS: u16 = 0x600; - -/// The Acquire's TimeoutValue, in ms; the wait for the press before it has -/// the rest of the 10 s one evaluation may spend asleep, past which the -/// load refuses the table. -const ACQUIRE_MS: u16 = 2000; - -/// What the server says of the table's Notify, which runs only where the -/// Acquire before it timed out. -const ACQUIRE_TIMED_OUT: &str = "Notify(\\_SB_.GLW_, 0x80) for the first time"; - -/// What it says of the load's takes: one, and it found the firmware holding -/// the lock. -const ONE_TAKE_CONTENDED: &str = "; took the Global Lock 1 times, 1 of them from the firmware;"; - -/// An SSDT (ACPI 6.5 §5.2.11.2) whose definition block declares the device -/// `\_SB.GLW` and, as it loads, waits for `PWRBTN_STS` (Table 4.13) in the -/// PM1a event block at `pm1`, then Acquires `\_GL` within `ms`, giving it -/// back where that took it and Notifying `\_SB.GLW` with 0x80 where it timed -/// out. It waits by Stall, which keeps the processor: a Sleep serves the SCI -/// (`sci::await_sleep`) and would take the press it waits for. It declares -/// the device `\_SB.GLI` too, whose `_INI`, run by the namespace's -/// initialisation after the load, Acquires `\_GL` with no bound: a server -/// that initialised before serving the press would end at the firmware's -/// hold. -/// -/// ```text -/// Device (\_SB.GLW) {} -/// Device (\_SB.GLI) { Method (_INI) { Acquire (\_GL, 0xFFFF) Release (\_GL) } } -/// OperationRegion (PM1S, SystemIO, pm1, 2) -/// Field (PM1S, WordAcc, NoLock, Preserve) { Offset (1), PBST, 1 } -/// While (!PBST) { Stall (100) } -/// If (Acquire (\_GL, ms)) { Notify (\_SB.GLW, 0x80) } Else { Release (\_GL) } -/// ``` -fn ssdt_acquiring_the_global_lock(pm1: u16, ms: u16) -> Vec { - const GL: &[u8] = b"\\_GL_"; - // `\` and a DualNamePrefix (§20.2.2). - const GLW: &[u8] = b"\\\x2E_SB_GLW_"; - const GLI: &[u8] = b"\\\x2E_SB_GLI_"; - const PM1S: &[u8] = b"PM1S"; - const PBST: &[u8] = b"PBST"; - // A PkgLength of one byte (§20.2.4): itself and what follows, under 64. - let package = |op: &[u8], body: &[u8]| [op, &[u8::try_from(body.len() + 1).expect("a short package")], body].concat(); - // SystemIO (1) at a WordConst, of a ByteConst's length. - let region = [&[0x5B, 0x80], PM1S, &[0x01, 0x0B], &pm1.to_le_bytes(), &[0x0A, 0x02]].concat(); - // WordAcc (2); a ReservedField (0) of 8 bits, then PBST of 1. - let field = package(&[0x5B, 0x81], &[PM1S, &[0x02, 0x00, 0x08], PBST, &[0x01]].concat()); - // LNot (0x92) of PBST; Stall of a ByteConst 100. - let wait = package(&[0xA2], &[&[0x92], PBST, &[0x5B, 0x21, 0x0A, 0x64]].concat()); - let acquire = [&[0x5B, 0x23], GL, &ms.to_le_bytes()].concat(); - let notify = [&[0x86], GLW, &[0x0A, 0x80]].concat(); - let release = [&[0x5B, 0x27], GL].concat(); - // An Acquire of no bound (0xFFFF, §19.6.2), then a Release, as a method of no arguments (MethodFlags 0). - let unbounded = [&[0x5B, 0x23], GL, &0xFFFFu16.to_le_bytes(), &release].concat(); - let ini = package(&[0x14], &[b"_INI".as_slice(), &[0x00], &unbounded].concat()); - let aml = [ - package(&[0x5B, 0x82], GLW), - package(&[0x5B, 0x82], &[GLI, &ini].concat()), - region, - field, - wait, - package(&[0xA0], &[acquire, notify].concat()), - package(&[0xA1], &release), - ] - .concat(); - let length = u32::try_from(36 + aml.len()).expect("a short table"); - let mut table = [b"SSDT".as_slice(), &length.to_le_bytes(), &[2, 0], b"TOYOS ", b"GLWAIT ", &1u32.to_le_bytes(), b"TOYS", &1u32.to_le_bytes(), &aml].concat(); - // The byte at 9 makes the whole table sum to zero. - table[9] = table.iter().fold(0u8, |sum, &b| sum.wrapping_sub(b)); - table -} - -/// The number after `after` on a line, in hex. -fn hex_after(line: &str, after: &str) -> Result { - line.split_once(after) - .map(|(_, rest)| rest.trim_start_matches("0x")) - .and_then(|rest| u64::from_str_radix(&rest[..rest.find(|c: char| !c.is_ascii_hexdigit()).unwrap_or(rest.len())], 16).ok()) - .ok_or_else(|| format!("no number after {after:?} on {line:?}")) -} - -/// A press latched before the server's wait for the firmware's release of -/// the Global Lock is served once the wait ends at its Acquire's timeout: the -/// wait clears the press's enable once it finds its status latched, takes -/// the SCI the press raised, and puts every enable back after, so the press's -/// status raises the SCI again for `serve`. `tests/images/acpicase.toml` with the test -/// kernel's actuator for the firmware's side of the lock, which -/// `acpi_mediated`'s `held` arm stages owned before it hands the server the -/// claim; the table QEMU adds waits as it loads for the press, sent once the -/// server has armed, and only then Acquires the lock within [`ACQUIRE_MS`]. -/// Then the press must power the machine off, after the Acquire said it timed -/// out, and before the namespace's initialisation, whose `\_SB.GLI._INI` -/// takes the lock with no bound and would end the server at the firmware's -/// hold. -pub fn acpi_press_across_a_lock_wait(probe: (String, Vec)) -> Result<(), String> { - let case = super::compile::repo_root().join("tests/images/acpicase.toml"); - let mut qemu = QemuInstance::boot_with_options( - &case, - &[], - &[], - BootOptions { - // The test kernel, for the Global Lock's actuator, as `acpi_mediated_access` boots it. - kernel_params: &["i8042-withheld"], - ready_marker: "acpi: the Global Lock is the FACS's at ", - extra_root_files: vec![ - probe, - // The arm's name is the file's: the probe asks whether it is there. - ("share/acpi_mediated_held".to_string(), b"held\n".to_vec()), - ], - acpi_tables: vec![ssdt_acquiring_the_global_lock(Q35_PM1A_EVENTS, ACQUIRE_MS)], - qmp: true, - ..Default::default() - }, - ); - let boot = serial::Serial::named("the boot", qemu.boot_log().to_string()); - let pm1 = hex_after(boot.must_say("acpi: the ACPI row: PM1a events ")?, "PM1a events ")?; - if pm1 != u64::from(Q35_PM1A_EVENTS) { - return Err(format!("the PM1a event block is at {pm1:#x}, where the table QEMU was handed reads {Q35_PM1A_EVENTS:#x}")); - } - // Opened before the press: QMP delivers no event emitted before its - // client connected. - let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); - let mut console = format!("{}\n", qemu.boot_log()); - qemu::await_marker(&mut qemu, &mut console, ACPI_ARMED, "the ACPI server arming")?; - stop.power_button(); - let pressed_at = console.len(); - ended(&mut qemu, &mut stop, &mut console, SHUTTING_DOWN, "guest-shutdown")?; - let after = serial::Serial::named("the press", console[pressed_at..].to_string()); - after.must_say(ONE_TAKE_CONTENDED)?; - after.must_say_after(ACQUIRE_TIMED_OUT, ACPI_PRESSED)?; - after.must_be_clean()?; - let whole = serial::Serial::named("the boot and the press", console); - if acpi_tables_loaded(&whole, &whole, Q35_S5_SUPPLIED)? < 2 { - return Err(format!("the server loaded no table beside the DSDT, so not the one QEMU was handed:\n{}", whole.text())); - } - eprintln!(" [power] {}", whole.must_say(ONE_TAKE_CONTENDED)?.trim()); - eprintln!(" [power] {}", whole.must_say_after(ACQUIRE_TIMED_OUT, ACPI_PRESSED)?.trim()); - Ok(()) -} diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index c68087bb7f..188dec2019 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -1108,10 +1108,6 @@ pub struct BootOptions { /// update's ROOT of this many bytes: a machine that updates itself. /// `None` for every guest whose subject is not the update. pub second_slot: Option, - /// Whole ACPI tables, header and checksum included, that QEMU lists - /// beside its own (`-acpitable`): firmware AML a guest's own tables do - /// not carry. q35's alone. - pub acpi_tables: Vec>, /// The instant the machine's RTC starts at, as QEMU's `-rtc base=` takes /// it, and the host's own UTC with `None`. pub rtc_base: Option<&'static str>, @@ -1153,7 +1149,6 @@ impl Default for BootOptions { extra_root_files: Vec::new(), psci_trace: None, second_slot: None, - acpi_tables: Vec::new(), rtc_base: None, } } @@ -1432,9 +1427,6 @@ impl QemuInstance { }; let sockets = Sockets::new(&options); - for (i, table) in options.acpi_tables.iter().enumerate() { - fs::write(acpi_table(&sockets.dir, i), table).expect("[qemu] write an added ACPI table"); - } let screendump = test_dir.join(format!("screen-{seq}.ppm")); // Per-instance, not a fixed /tmp path: a screen test waits on this @@ -2447,10 +2439,6 @@ fn qemu_command( if options.gdb_stub { qemu.arg("-s"); } - assert!(options.acpi_tables.is_empty() || arch == Arch::X86_64, "an added ACPI table is q35's"); - for i in 0..options.acpi_tables.len() { - qemu.arg("-acpitable").arg(format!("file={}", acpi_table(socket_dir, i).display())); - } if let Some(socket) = qmp_socket { qemu.arg("-qmp") .arg(format!("unix:{},server,nowait", socket.display())); @@ -2482,11 +2470,6 @@ impl Sockets { } } -/// The `i`th of [`BootOptions::acpi_tables`], as a file in a boot's own `dir`. -fn acpi_table(dir: &Path, i: usize) -> PathBuf { - dir.join(format!("acpi-{i}.aml")) -} - /// The QMP socket `options` asks for, named in `dir`. fn qmp_socket(dir: &Path, options: &BootOptions) -> Option { options.qmp.then(|| dir.join("qmp.sock")) diff --git a/tests/guest/rust-tests/src/bin/acpi_mediated.rs b/tests/guest/rust-tests/src/bin/acpi_mediated.rs index de4bfae013..69f3dc4221 100644 --- a/tests/guest/rust-tests/src/bin/acpi_mediated.rs +++ b/tests/guest/rust-tests/src/bin/acpi_mediated.rs @@ -42,18 +42,12 @@ //! [`OUTLIVED`] has a child supply this guest's own sleep type and exit, and //! the parent, which supplied none, ask for the power-off after the child's //! claim and one more have been released. -//! -//! **A press is served across the server's wait for the firmware's release** -//! ([`held`]), on a boot whose ROOT carries [`HELD`] and whose firmware lists -//! a table that takes the Global Lock at its load: the firmware's side of the -//! lock is staged owned before `/system/bin/acpiserver` is handed the claim. -//! The press, the wait's timeout and the power-off are the harness's to read. use std::os::toyos::process::CommandExt; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; -use toyos::endow::{Endowments, DEV_PREFIX, SYSCAP_LABEL}; +use toyos::endow::{Endowments, SYSCAP_LABEL}; use toyos::syscap::SysCap; use toyos::{AsHandle, Device}; use toyos_abi::acpi::{pci_address, Access, AcpiInfo, Refused, Space, Width, UNLISTED}; @@ -71,9 +65,6 @@ const CLAIM_LABEL: &str = "acpi-claim"; /// binary's name and takes no argument. const OUTLIVED: &str = "/system/share/acpi_mediated_outlived"; -/// The file whose presence on ROOT makes this boot [`held`]'s. -const HELD: &str = "/system/share/acpi_mediated_held"; - /// `EFI_MEMORY_TYPE`s: what the kernel hands out as RAM, and ACPI's two. const USABLE: [u8; 5] = [1, 2, 3, 4, 7]; const ACPI_RECLAIM: u8 = 9; @@ -121,7 +112,6 @@ impl Holder { fn main() { match std::env::args().nth(1).as_deref() { None if std::fs::exists(OUTLIVED).expect("acpi: ask ROOT for this boot's arm") => outlived(), - None if std::fs::exists(HELD).expect("acpi: ask ROOT for this boot's arm") => held(), None => probe(), Some("keeper") => keeper(), Some("supplier") => supplier(), @@ -648,20 +638,3 @@ fn outlived() { let refused = toyos::power::stop(toyos::power::Stop::Shutdown); panic!("acpi: the power-off was refused: {refused:?}"); } - -/// What [`held`] says once the server has the claim, with the firmware's -/// side of the lock owned. -const STAGED: &str = "acpi: the firmware's side of the Global Lock staged owned, and the server handed the claim"; - -fn held() { - let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("test-runner endows a device-minting capability"); - let claim = claim(&cap); - assert_eq!(syscall::debug_with(debug_action::ACPI_FIRMWARE_LOCK, debug_action::FIRMWARE_OWNS), 0, "acpi: the lock was not free before the server ran"); - let mut server = Command::new("/system/bin/acpiserver") - .endow(&format!("{DEV_PREFIX}{}", DeviceType::Acpi.class_name()), claim.into_raw().0) - .spawn() - .expect("acpi: spawn the server"); - println!("{STAGED}"); - let ended = server.wait().expect("acpi: wait for the server"); - panic!("acpi: the server ended {ended:?}, where a press powers the machine off"); -} diff --git a/tests/toyos.rs b/tests/toyos.rs index 6b63ebc91b..756df9cbdf 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -193,8 +193,8 @@ const RUST_SKIP: &[&str] = &[ // It claims the fixed hardware itself, which needs a boot that starts no // server, and stages the firmware's side of the Global Lock and finds the // i8042's row another claim's, which need the test kernel and its - // `i8042-withheld`: `acpi_mediated_access` and `acpi_press_across_a_lock_wait` - // run it on tests/images/acpicase.toml. + // `i8042-withheld`: `acpi_mediated_access` runs it on + // tests/images/acpicase.toml. "acpi_mediated", // It powers the machine off: `machine_shutdown_short_stop` runs it. "stop_short", @@ -394,12 +394,6 @@ const MACHINE_TESTS: &[&str] = &[ // by stopping. No host test reaches either, and the T14 is never asked to // power off. "acpi_supply_outlives_holder", - // A press while acpiserver waits for the firmware's release of the Global - // Lock: the firmware's hold is the test kernel's actuator on the FACS's - // word, the table that takes the lock is one QEMU adds, and the press is - // QEMU's on demand. The T14's firmware holds the lock when it will, and - // nothing presses its button but a hand. - "acpi_press_across_a_lock_wait", // The power-off after a stop that left a thread running, in ACPI mode: it // ends the machine, so only one QEMU reports stopping can be asked, and // the T14 hands over in legacy mode, where no holder means no quieting. @@ -3929,7 +3923,6 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "acpi_mediated_access" => acpi_mediated_access(2), "acpi_lock_given_back_on_one_cpu" => acpi_mediated_access(1), "acpi_supply_outlives_holder" => acpi_supply_outlives_holder(), - "acpi_press_across_a_lock_wait" => power::acpi_press_across_a_lock_wait(suite_bin(toyos_build::arch::Arch::X86_64, "acpi_mediated")), "machine_shutdown_short_stop" => power::machine_shutdown_short_stop(test_config), "machine_shutdown_wire_held" => power::machine_shutdown_wire_held(test_config, false), "machine_shutdown_wire_kept" => power::machine_shutdown_wire_held(test_config, true), From 17c2cb89ae5f160153ba43f390d185dab91083e3 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 03:28:17 +0200 Subject: [PATCH 29/30] issues: the track's finish is the ruled state; the T14 issue records the GPIO and MCHBAR ruling; the AMD bench's MMIO read is closed Round 5's review of #875. The track's finish and S3's exit asked every block loaded and no method refused on both benches, and readings equal to Linux's: the first is false under the owner's rulings (SSDT8, the AMD bench's ConfigWrite, the T14's two _INIs), and a reading under another OS is evidence, never an exit's oracle (#902). Both now state the ruled state, ACPI 6.5 deciding. The T14 issue records the owner's ruling on #875 (comment 6135767889): the GPIO pad write and the MCHBAR write stay refused, isolation deciding, and the row expects both _INIs by path. acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md is closed: its exit is met. The policy was ruled (comment 6120173372), firmware::decide holds it with a host test of each arm, and the AMD bench's boot at ddc243eaf (639ce1374 before round 6's reword, the same tree) read 16 of 17 tables loaded, SSDT8 the one refused as ruled, and \_S5 handed, SLP_TYPa=5: acpi_tables_loaded passed, judge exit 0 (https://github.com/ToyOSOrg/evidence/blob/063f2e24002d1f506a59c4440f6da9b300753f7a/round6/metal/amd-judge.log, and judged again at 5352b0901 in round6/judge/fixed-amd.log, exit 0). Two issues name the branch's commits by their hashes after the reword. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...chs-dsdt-for-a-read-in-memory-mapped-io.md | 56 ------------------- ...-for-a-write-to-the-root-complexs-index.md | 2 +- ...-are-refused-for-writes-outside-the-map.md | 10 +++- ...ine-in-acpi-mode-and-interprets-its-aml.md | 29 +++++----- 4 files changed, 24 insertions(+), 73 deletions(-) delete mode 100644 issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md 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 d5359e3ebb..0000000000 --- a/issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md +++ /dev/null @@ -1,56 +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. - -**Ruled** (owner, 2026-10-12, on #875): such a read is a register's, read -as an unlisted one is (comment 6120173372), built in #875 with a host test -of each arm; and SSDT8 is refused whole (comment 6121646183, -`issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.md`), so -the boot's reading is 16 of 17 tables loaded, SSDT8 the one refused, and -`\_S5` handed: the bench's `acpi_tables_loaded` reads 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 index d2c6015ec0..d17fccef13 100644 --- 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 @@ -22,7 +22,7 @@ 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 `855eff969` +**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`): 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 index c93b6a35a1..4cfb3793bb 100644 --- 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 @@ -14,10 +14,14 @@ 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. The T14 row -expects the refusal (`T14` in `tests/toyos.rs`). +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 `639ce1374` and +**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 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 e8c365fc93..f781202069 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 @@ -467,11 +467,18 @@ ACPI Interface Specification (302223-007) for a processor's `_OSC` on Intel: honours; - runs every runtime GPE's method; - delivers every Notify. -- On the T14 and the AMD bench: - - every block loads, and no method is refused; - - under every capability bit, the blocks Linux loaded come out: the - capabilities Linux passed a processor are in no memory a dump holds; - - each `_STA`, `_CST` and battery reading equals Linux's on the same machine. +- 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. Not in finished: @@ -483,7 +490,7 @@ Not in finished: 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, NVS and Linux's readings (#875's M1) fixes the order inside it. The +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. @@ -495,13 +502,9 @@ machines: 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`), - which meets - `issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md`'s - exit. -- **S3**: the T14's `acpi_tables_loaded` reads every `_INI` run, none refused - but by the owner's ruling, and `_REG(3, 1)` after them; every device's - `_STA` there equal to Linux's, compared outside the tree. + (`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; From a785962f4164f8475b767c75789ce8c0fe9445f0 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 03:45:25 +0200 Subject: [PATCH 30/30] issues: the track's finish states the AMD adapter's ruled unknown and names what reads _CST The finish asked every battery reading with no value unknown, which the AMD bench cannot meet under the ruling on its _PSR: its adapter reads `AC unknown`. It now states that exception. `_CST` left the finish in the previous commit without a word on where it went: no part of this track reads it, and the deep idle states track does (#870). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...runs-the-machine-in-acpi-mode-and-interprets-its-aml.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 f781202069..524dffba48 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 @@ -478,13 +478,18 @@ ACPI Interface Specification (302223-007) for a processor's `_OSC` on Intel: 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. + 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.