From 3605706ef624829fefda0addee52414a2e2733f2 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:50:48 +0200 Subject: [PATCH 1/6] A reset register that is SMI_CMD, a machine with no ECDT and a control-method power button each leave ACPI mode to the server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An AMD laptop's FADT names port 0xb0 as both its reset register (value 0xfb) and SMI_CMD, has no ECDT, and makes its power button a control method device. The kernel refused it ACPI mode three ways; it is now claimed, and the server serves what only its AML names. The shared port is declared once, as SMI_CMD, before the IDT, with the reset register: `power::init_reset` declares both FADT roles, and where they are one port the reset register is SMI_CMD's declaration and the reset's value is a sixth kept byte (`SmiCmd::named`, `KeptCommands`), so no write the acpi holder asks of the port resets the machine. The reset's `out` stays a plain write from whichever CPU ends the machine: Table 5.9 asks the boot processor of a command to SMI_CMD and nothing of the reset. The ECDT stopgap is deleted, as the owner's "Stopgap, delete later" asked the day the controller is read from the DSDT's own device. The kernel reads no ECDT, its row is PM1a events and GPE0, and `AcpiInfo` loses the controller's fields. The server finds PNP0C09 in the namespace its load keeps (`devices.rs`): `_STA`, `_CRS`'s two fixed I/O ports through the new `toyos_acpi::io_ports` (data first, command second, ACPI 6.5 §12.11), and `_GPE` held to GPE0. Its ports are no row's, so the server reaches them through the kernel's mediated access, as any port AML names. A control-method power button is served as §4.8.2.2.1.2 has it: where the FADT says the button is a control method device, the server finds every present PNP0C0C, runs each controller query's `_Qxx`, and a Notify of 0x80 to a button is a press on the existing power-off path. To let a query's method reach its Notify the host now passes a port write to the kernel, whose policy decides it; memory and configuration space stay unwritten. On a machine whose button is the fixed one no query's method runs, as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...r-off-after-the-kernels-own-acpi-enable.md | 29 +- ...ine-in-acpi-mode-and-interprets-its-aml.md | 52 ++- kernel/src/arch/x86_64/acpi_mode.rs | 85 +--- kernel/src/arch/x86_64/power.rs | 61 ++- kernel/src/arch/x86_64/smi_cmd.rs | 15 +- tests/common/power.rs | 6 +- .../src/bin/counters_metal.rs | 21 +- tests/toyos.rs | 19 +- toyos-abi/src/acpi.rs | 21 +- toyos-acpi/src/ecdt.rs | 70 --- toyos-acpi/src/fadt.rs | 15 +- toyos-acpi/src/lib.rs | 18 +- toyos-acpi/src/resource.rs | 67 ++- toyos-acpi/tests/amd_laptop.rs | 87 ++++ toyos-acpi/tests/corpus.rs | 54 +-- toyos-acpi/tests/resource.rs | 58 ++- toyos-acpi/tests/thinkpad_t14.rs | 27 +- toyos-userbound/src/port.rs | 6 +- toyos-userbound/tests/firmware.rs | 27 +- toyos/src/device.rs | 2 +- userland/acpiserver-api/src/lib.rs | 6 + userland/acpiserver/src/aml.rs | 85 ++-- userland/acpiserver/src/devices.rs | 400 ++++++++++++++++++ userland/acpiserver/src/host.rs | 95 ++++- userland/acpiserver/src/main.rs | 222 ++++++---- 25 files changed, 1101 insertions(+), 447 deletions(-) delete mode 100644 toyos-acpi/src/ecdt.rs create mode 100644 toyos-acpi/tests/amd_laptop.rs create mode 100644 userland/acpiserver/src/devices.rs diff --git a/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md b/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md index d1f3c268b42..d5f830274ea 100644 --- a/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md +++ b/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md @@ -43,24 +43,21 @@ the one config whose claim a job mints, `tests/acpicase`, runs its job list unprompted. Such a test reads QEMU's model of the ICH9 and never the T14's firmware. -**The mint refuses q35 in legacy mode**, so the test this exit names cannot -be built yet. Measured on QEMU 11.1.1's q35 under OVMF and TCG, on -`tests/acpicase` with the monitor's `o /b 0xb2 3` sent before the job -claimed: the kernel logged `acpi: this machine stays in legacy mode — its -firmware serves an embedded controller no holder could (the ECDT is unusable: -Absent)` and the claim answered `NotSupported`. q35 has no ECDT, and -`acpi_mode::enter` writes `ACPI_ENABLE` only for a machine whose embedded -controller the ECDT names, the stopgap -`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md` -records. That write also landed by a race, between the kernel's ACPI row line -and the job's claim: `tests/acpicase` still runs its job list unprompted. +**The mint refused q35 in legacy mode until the ECDT stopgap was deleted.** +Measured on QEMU 11.1.1's q35 under OVMF and TCG, on `tests/acpicase` with +the monitor's `o /b 0xb2 3` sent before the job claimed: the kernel logged +`acpi: this machine stays in legacy mode — its firmware serves an embedded +controller no holder could (the ECDT is unusable: Absent)` and the claim +answered `NotSupported`. The kernel reads no ECDT now, and `acpi_mode::enter` +writes `ACPI_ENABLE` for q35 as for any machine in legacy mode; nothing has +read it do so. What the test still waits on is a job that waits to be asked: +the monitor's write landed by a race, between the kernel's ACPI row line and +the job's claim, and `tests/acpicase` still runs its job list unprompted. Owned by the stage "the interpreter" of -`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`, whose -deletion of the ECDT stopgap is what lets a machine with no embedded -controller be put in ACPI mode by the kernel. Moving it there is the -placement of the power-off's slice, which found the refusal; it passed that -slice's review and is not the owner's. +`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`. +Moving it there is the placement of the power-off's slice, which found the +refusal; it passed that slice's review and is not the owner's. **Exit** (the orchestrator's placement, not the owner's; that it waits on the stopgap's deletion and on a job that waits to be asked is that slice's): once the ECDT 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 4e10af18689..593a1c74028 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 @@ -124,16 +124,21 @@ in ACPI mode; that no T14 row reads it is Moving it out of this exit is the orchestrator's placement, not the owner's. **Ruled** (owner, 2026-10-04, "Stopgap, delete later"): "Stage 1 uses the -extra table so the T14 switches to ACPI mode now." Stage 1 reads the -embedded controller from the ECDT, and that path is a stopgap: it is deleted -the day the interpreter reads the controller from the DSDT's own device. A -machine without an ECDT stays in legacy mode until then; such a machine keeps -its firmware interrupts, and this stage's exit cannot be met on it. +extra table so the T14 switches to ACPI mode now." Stage 1 read the +embedded controller from the ECDT as that stopgap, to be deleted the day the +interpreter reads the controller from the DSDT's own device. It is deleted: +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 +(`userland/acpiserver/src/devices.rs`); the kernel reads no ECDT. -Stage 1's design, not a ruling: a machine whose power button is a control -method device stays in legacy mode too, refused by name; and a machine its -firmware hands over in ACPI mode is served whatever it has, since nothing is -written. +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 +query method makes of it (ACPI 6.5 §4.8.2.2.1.2), which the server runs for +every query only on such a machine; a button raised by a GPE's own `_Lxx` +or `_Exx` is not served, since the server enables no GPE but the +controller's. A machine its firmware hands over in ACPI mode is served +whatever it has, since nothing is written. **Stage: the interpreter** (the orchestrator's placement of "The AML stage closes it"). ToyOS's own AML interpreter, written from the specification, run @@ -283,16 +288,14 @@ mediated access, leaves open: 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 machine with no holder of the `acpi` claim has no power-off.** The - kernel reads no AML, so a machine whose claim it refuses, one in legacy - mode with no ECDT or with a control-method power button, has nobody to - evaluate `\_S5`: `SYS_SHUTDOWN` is refused there, where the kernel's own - scan of the DSDT powered such a machine off before. Neither the T14 nor q35 - is one: the T14 has an ECDT and a fixed button, and OVMF hands q35 over in - ACPI mode. Owner: this stage. **Exit**: the ECDT stopgap is deleted, so a - machine is claimed for what its DSDT names; what a machine with a - control-method button does for a power-off is ruled with that button's - device. +- **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 + server's design makes the button's Notify a press, which stops the + machine through the one path. What such a machine does for a power-off + was to be "ruled with that button's device", and the owner has not ruled + it. Owner: this stage. **Exit**: the owner rules it, and the server does + what he rules. Two more machines have no power-off, and that is the ruled state and no weakness with an exit. A boot whose config starts no server: "Power-off @@ -324,9 +327,14 @@ 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. The server's AML makes none yet: its -host denies every write AML asks for and passes none to the kernel -(`userland/acpiserver/src/host.rs`). +stores a byte to `SMI_CMD`, leaves open. The server's host passes the +kernel a write to a port and to nothing else +(`userland/acpiserver/src/host.rs`), and the AML that writes after the load +is a controller query's method, run only on a machine whose power button is +a control method device; the T14's button is the fixed one. On an AMD +laptop's tables, outside the tree, every query method the controller +defines wrote port 0x80 and no other port, with each controller byte read +as 0 and again as 0xFF: no call. - **What a call does there is the firmware's**, and the kernel bounds who, when, where, which byte and how often: diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index 893c82976f2..e21922e73b8 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -13,19 +13,16 @@ //! disable again. Neither is written once the stop has begun: `smi_cmd` //! refuses it. //! -//! **A machine stays in legacy mode where its firmware serves something no -//! holder could**: an embedded controller the ECDT does not name, or a power -//! button that is a control method device, which only AML serves. Both are -//! refused at the mint, by name; a machine the firmware handed over in ACPI -//! mode is claimed whatever it has, since nothing is written. +//! **What only the firmware's AML names is the holder's to find**: the +//! embedded controller is a device of the DSDT, and a power button that is a +//! control method device is served by the AML that notifies it. This kernel +//! reads no AML, so neither keeps a machine in legacy mode; the controller's +//! ports are the holder's to reach as [`access`] reaches any port no row +//! names, and no table this kernel reads names them. //! -//! **The ECDT is a stopgap**: the embedded controller is read from it until the -//! interpreter reads the controller's own device from the DSDT, and then this -//! path and `toyos_acpi::ecdt` go -//! (`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`). -//! -//! The row is the FADT's PM1a event and GPE0 blocks and the ECDT's two -//! ports, filled once at boot; the SCI is its one line, level. +//! The row is the FADT's PM1a event and GPE0 blocks, filled once at boot; +//! the SCI is its one line, level. `SMI_CMD` is declared before it, with the +//! reset register (`power::init_reset`). //! //! **What the firmware's AML names outside the row, this kernel reads and //! writes for the claim's holder, one access at a time** ([`access`]): memory @@ -82,7 +79,7 @@ use core::sync::atomic::AtomicU32; use toyos_abi::acpi::{Access, AcpiInfo, Block, Refused, Space, Width, FIXED_POWER_BUTTON}; use toyos_abi::syscall::SyscallError; -use toyos_acpi::{Ec, FixedHardware, LegacyMode, PowerButton}; +use toyos_acpi::{FixedHardware, LegacyMode, PowerButton}; use toyos_userbound::firmware::{ self, CallRate, Ecam, FirmwareCall, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict, }; @@ -116,8 +113,6 @@ const HANDBACK: Duration = Duration::from_millis(100); struct Hardware { fixed: FixedHardware, control: Declared, - /// Or why it is none a holder can be handed. - ec: Result, rsdp: u64, /// The window configuration space is reached through, as the MCFG bounds it. ecam: Option, @@ -222,9 +217,9 @@ fn hardware() -> Option<&'static Hardware> { (!at.is_null()).then(|| unsafe { &*at }) } -/// Decode the fixed hardware, declare `SMI_CMD`, and fill the row; or say by -/// name why this machine has none. After the i8042's row, whose lines the -/// SCI may not share. +/// Decode the fixed hardware and fill the row; or say by name why this +/// machine has none. After the i8042's row, whose lines the SCI may not +/// share. pub fn init(rsdp_addr: u64) { let fadt = match toyos_acpi::find_table( crate::drivers::acpi::direct_phys(), @@ -242,17 +237,9 @@ pub fn init(rsdp_addr: u64) { let Some(control) = super::power::pm1a_control() else { return log!("acpi: no ACPI row — no PM1a control block declared"); }; - let facs = toyos_acpi::facs(fadt.phys(), &fadt); - // A FACS this kernel cannot read leaves `S4BIOS_F` unread, and the byte kept. - let s4bios = match facs { - Ok(facs) => facs.s4bios, - Err(toyos_acpi::FacsRefused::Absent) => false, - Err(_) => true, - }; - if let Some(Err(why)) = fixed.smi_cmd.map(|named| smi_cmd::declare(named.port, named.named(s4bios))) { - return log!("acpi: no ACPI row — SMI_CMD not declared: {why:?}"); + if fixed.smi_cmd.is_some() && smi_cmd::declared().is_none() { + return log!("acpi: no ACPI row — the FADT's SMI_CMD was not declared"); } - let ec = embedded_controller(rsdp_addr, fixed.gpe0); let Some(sci) = super::ioapic::sci(fixed.sci_int) else { return log!("acpi: no ACPI row — no I/O APIC carries the SCI"); }; @@ -264,11 +251,8 @@ pub fn init(rsdp_addr: u64) { if fixed.gpe0.len != 0 { runs.push(run(fixed.gpe0)); } - if let Ok(ec) = &ec { - runs.extend([Ports::one(ec.command), Ports::one(ec.data)]); - } log!( - "acpi: the ACPI row: PM1a events {:#x}+{}, GPE0 {:#x}+{}, SCI {}, {}, embedded controller {}; the firmware handed over in {} mode", + "acpi: the ACPI row: PM1a events {:#x}+{}, GPE0 {:#x}+{}, SCI {}, {}; the firmware handed over in {} mode", fixed.pm1a_event.port, fixed.pm1a_event.len, fixed.gpe0.port, @@ -278,15 +262,11 @@ pub fn init(rsdp_addr: u64) { PowerButton::Fixed => "the fixed-hardware power button", PowerButton::ControlMethod => "a control-method power button", }, - match &ec { - Ok(ec) => format!("at {:#x}/{:#x} on GPE {:#x}", ec.command, ec.data, ec.gpe), - Err(why) => format!("none ({why})"), - }, if cpu::inw(control.port(0)) & SCI_EN != 0 { "ACPI" } else { "legacy" }, ); isa::fill(ROW, Function { name: "the ACPI fixed hardware", runs, irqs: vec![], wires: vec![sci] }); - let (ecam, lock) = (ecam(rsdp_addr), global_lock(facs)); - let hardware = Hardware { fixed, control, ec, rsdp: rsdp_addr, ecam, lock }; + let (ecam, lock) = (ecam(rsdp_addr), global_lock(toyos_acpi::facs(fadt.phys(), &fadt))); + let hardware = Hardware { fixed, control, rsdp: rsdp_addr, ecam, lock }; let was = HARDWARE.swap(Box::into_raw(Box::new(hardware)), Ordering::Release); assert!(was.is_null(), "acpi: init ran twice"); } @@ -338,17 +318,6 @@ fn run(block: Block) -> Ports { Ports::new(block.port, block.len).expect("`fixed_hardware` bounded every block by the port space") } -fn embedded_controller(rsdp_addr: u64, gpe0: Block) -> Result { - let ecdt = toyos_acpi::find_table(crate::drivers::acpi::direct_phys(), rsdp_addr, b"ECDT", toyos_acpi::ECDT_NEEDED) - .map_err(|e| format!("the ECDT is unusable: {e:?}"))?; - let ec = toyos_acpi::ecdt(&ecdt).map_err(|refused| format!("the ECDT names none: {refused:?}"))?; - // §5.2.9: a GPE block's status half holds eight GPEs a byte. - if u16::from(ec.gpe) >= gpe0.len / 2 * 8 { - return Err(format!("the ECDT puts it on GPE {:#x}, outside GPE0's {}", ec.gpe, gpe0.len / 2 * 8)); - } - Ok(ec) -} - /// The row, claimed, with the machine in ACPI mode; or the refusal that /// leaves it in legacy mode, said by name. pub fn claim() -> Result<(isa::Row, AcpiInfo), ClaimError> { @@ -360,19 +329,12 @@ pub fn claim() -> Result<(isa::Row, AcpiInfo), ClaimError> { } fn info(hardware: &Hardware) -> AcpiInfo { - let (ec_command, ec_data, ec_gpe) = match &hardware.ec { - Ok(ec) => (Block { port: ec.command, len: 1 }, Block { port: ec.data, len: 1 }, u16::from(ec.gpe)), - Err(_) => (Block::NONE, Block::NONE, 0), - }; AcpiInfo { rsdp: hardware.rsdp, pm1_event: hardware.fixed.pm1a_event, gpe0: hardware.fixed.gpe0, - ec_command, - ec_data, - ec_gpe, flags: if hardware.fixed.power_button == PowerButton::Fixed { FIXED_POWER_BUTTON } else { 0 }, - reserved: 0, + reserved: [0; 3], } } @@ -404,15 +366,6 @@ fn enter(hardware: &Hardware) -> Result<(), ClaimError> { return refuse("the FADT names no SMI_CMD, ACPI_ENABLE and ACPI_DISABLE to leave it and come back with"); }; let enable = legacy.acpi_enable.get(); - if let Err(why) = &hardware.ec { - return refuse(&format!( - "its firmware serves an embedded controller no holder could ({why})" - )); - } - if hardware.fixed.power_button == PowerButton::ControlMethod { - return refuse("its power button is a control method device, which only AML serves"); - } - let Some(write) = smi_cmd::write(enable) else { return refuse("the machine is stopping"); }; diff --git a/kernel/src/arch/x86_64/power.rs b/kernel/src/arch/x86_64/power.rs index 8c71f215615..0900c3868d9 100644 --- a/kernel/src/arch/x86_64/power.rs +++ b/kernel/src/arch/x86_64/power.rs @@ -1,6 +1,8 @@ //! Reset and power-off through the FADT: its reset register, and S5 soft-off //! through the PM1a control block with the `SLP_TYPa` the holder of the `acpi` -//! claim supplied. +//! claim supplied. The reset register is declared with `SMI_CMD`, the other +//! port the FADT has a write command, since the two may be one port +//! ([`init_reset`]). //! //! **This kernel reads no AML, so it knows no sleep type of its own.** `\_S5` //! is the firmware's AML to evaluate, and the claim's holder does @@ -40,7 +42,13 @@ const UNSUPPLIED: u8 = u8::MAX; static RESET: Slot = Slot::empty(); static RESET_VALUE: AtomicU8 = AtomicU8::new(0); -/// Record the FADT's reset register, or say by name why this machine has none. +/// Declare the FADT's two ports a write commands, `SMI_CMD` and the reset +/// register, and record the reset register; or say by name why this machine +/// has either not. +/// +/// **One port may be both** (an AMD laptop's 0xb0 is): it is declared once, +/// as `SMI_CMD`, whose kept bytes then hold the reset's value too +/// (`SmiCmd::named`), and the reset register is that declaration. /// /// Before `percpu::init_bsp` loads the IDT: from then on every panic can be /// reported, and a panic that can be reported but not ended is a machine that @@ -48,23 +56,43 @@ static RESET_VALUE: AtomicU8 = AtomicU8::new(0); /// refused — a table walk on a machine that has already failed once is how a /// panic becomes a triple fault. pub fn init_reset(rsdp_addr: u64) { - let fadt = match toyos_acpi::find_table(direct_phys(), rsdp_addr, b"FACP", toyos_acpi::FADT_FOR_RESET) { + let fadt = match toyos_acpi::find_table(direct_phys(), rsdp_addr, b"FACP", toyos_acpi::FADT_FOR_FIXED_HARDWARE) { Ok(table) => table, Err(e) => { - log!("ACPI: FADT unusable: {e:?} — no reboot, a panic will hold the panel"); + log!("ACPI: FADT unusable: {e:?} — no reboot, a panic will hold the panel, and no SMI_CMD"); return; } }; - match toyos_acpi::reset_register(&fadt) { - Reset::Port { port, value } => match pio::declare("the reset register", Ports::one(port), Mediated::Kept) { - Ok(declared) => { - RESET_VALUE.store(value, Ordering::Relaxed); - RESET.set(declared); - log!("ACPI: reset register SystemIO {port:#x} <- {value:#04x}"); - } - Err(why) => log!("ACPI: reset register {port:#x} not declared ({why:?}) — no reboot"), - }, - other => log!("ACPI: no reset register this kernel writes ({other:?}) — no reboot"), + let reset = toyos_acpi::reset_register(&fadt); + // Fixed hardware this kernel does not serve has no ACPI row, and so no use for `SMI_CMD`. + let smi_cmd = toyos_acpi::fixed_hardware(&fadt).ok().and_then(|fixed| fixed.smi_cmd); + if let Some(named) = smi_cmd { + // A FACS this kernel cannot read leaves `S4BIOS_F` unread, and the byte kept. + let s4bios = match toyos_acpi::facs(fadt.phys(), &fadt) { + Ok(facs) => facs.s4bios, + Err(toyos_acpi::FacsRefused::Absent) => false, + Err(_) => true, + }; + if let Err(why) = super::smi_cmd::declare(named.port, named.named(s4bios, reset)) { + log!("ACPI: SMI_CMD {:#x} not declared ({why:?}) — no ACPI row", named.port); + } + } + let Reset::Port { port, value } = reset else { + return log!("ACPI: no reset register this kernel writes ({reset:?}) — no reboot"); + }; + let shared = smi_cmd.is_some_and(|named| named.port == port); + let declared = if shared { + super::smi_cmd::declared().ok_or_else(|| "SMI_CMD, which it is, was not declared".into()) + } else { + pio::declare("the reset register", Ports::one(port), Mediated::Kept).map_err(|why| alloc::format!("{why:?}")) + }; + match declared { + Ok(declared) => { + RESET_VALUE.store(value, Ordering::Relaxed); + RESET.set(declared); + log!("ACPI: reset register SystemIO {port:#x} <- {value:#04x}{}", if shared { ", which is SMI_CMD too" } else { "" }); + } + Err(why) => log!("ACPI: reset register {port:#x} not declared ({why}) — no reboot"), } } @@ -116,7 +144,10 @@ pub fn pm1a_control() -> Option { } /// Write the reset register and nothing else: no lock, nothing but the port -/// the FADT named. A machine with no reset register halts. +/// the FADT named. A machine with no reset register halts. Where the register +/// is `SMI_CMD` this `out` is no write of `smi_cmd`'s: Table 5.9 asks the boot +/// processor of a command to `SMI_CMD` and nothing of the reset, which is +/// made from whichever CPU ends the machine. // No fallback: 0xCF9, the keyboard controller and anything else are written only where a table named them. pub fn reset() -> ! { if let Some(reset) = RESET.get() { diff --git a/kernel/src/arch/x86_64/smi_cmd.rs b/kernel/src/arch/x86_64/smi_cmd.rs index e0aea0018e2..3f17585892f 100644 --- a/kernel/src/arch/x86_64/smi_cmd.rs +++ b/kernel/src/arch/x86_64/smi_cmd.rs @@ -30,7 +30,9 @@ //! `issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md`'s. //! //! The port is this module's alone, so the `out` in [`answer`] is the only -//! one the kernel can make to it, and [`answer`] reads which CPU it is on +//! command the kernel can make to it, the reset's `out` aside where the +//! FADT's reset register is this port (`power::reset`), which ends the +//! machine; and [`answer`] reads which CPU it is on //! with interrupts closed beside that `out`: no caller's state decides it. //! It is counted there too, with the time it held the boot processor //! ([`counted`]), whoever asked for it. @@ -38,7 +40,7 @@ //! **The `acpi` claim's holder writes no byte here: it asks for one**, and the //! declaration says which bytes are never written for it //! (`toyos_userbound::Mediated::Command`): those the FADT gives a meaning, -//! which are this kernel's own commands. +//! which are this kernel's own commands and its reset's value. //! //! **None is made once the stop has begun**: the power-off waits out a write //! in flight ([`settle`]) and then owns the hardware, and an SMI it did not @@ -50,7 +52,7 @@ use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering::{Acquire, Rel use toyos_userbound::{KeptCommands, Mediated, Ports, Undeclared}; -use super::pio::{self, Slot, TakenBack}; +use super::pio::{self, Declared, Slot, TakenBack}; use super::{apic, cpu, percpu, IrqGuard}; use crate::shootdown::Shootdown; use crate::sync::Lock; @@ -113,11 +115,16 @@ impl fmt::Display for Written { /// Declare the port the FADT names, and the values it names for it. Boot's. /// The `acpi` claim's holder reads it and never writes it: a write is a /// command to the firmware, and [`write`] makes every one. -pub fn declare(port: u16, named: [Option; 5]) -> Result<(), Undeclared> { +pub fn declare(port: u16, named: [Option; 6]) -> Result<(), Undeclared> { PORT.set(pio::declare("SMI_CMD", Ports::one(port), Mediated::Command(KeptCommands(named)))?); Ok(()) } +/// The port, where [`declare`] declared one. +pub fn declared() -> Option { + PORT.get() +} + /// How many writes this CPU has made and the nanoseconds they held it, on /// the boot processor of a machine that names the port; `None` on every /// other CPU, which makes none. diff --git a/tests/common/power.rs b/tests/common/power.rs index 7ffbbea66a0..7701db0d170 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -676,8 +676,8 @@ pub fn usb_reset_on_metal(arms: &[&super::metal::Readback]) -> Result<(), String } /// What `/system/bin/acpiserver` says once it serves q35's power button, -/// whole: q35 has no embedded controller. -const ACPI_ARMED: &str = "acpiserver: armed: power button served, embedded controller none"; +/// whole, before it loads the tables. +const ACPI_ARMED: &str = "acpiserver: armed: power button served"; /// The server's line for the press, naming the SCI it came on. const ACPI_PRESSED: &str = @@ -757,6 +757,8 @@ pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { let whole = serial::Serial::named("the boot and the press", console); acpi_tables_loaded(&whole, &whole, Q35_S5_SUPPLIED)?; whole.must_say_after(ACPI_S5_HANDED, ACPI_PRESSED)?; + // QEMU's DSDT names no embedded controller, which the server finds there. + whole.must_say_after(&format!("{}no PNP0C09 device is present", acpiserver_api::CONTROLLER_NONE), ACPI_PRESSED)?; eprintln!(" [power] the press: {ACPI_PRESSED}"); Ok(()) } diff --git a/tests/toyos-rust-tests/src/bin/counters_metal.rs b/tests/toyos-rust-tests/src/bin/counters_metal.rs index fc58389d0c0..15ce754a0d9 100644 --- a/tests/toyos-rust-tests/src/bin/counters_metal.rs +++ b/tests/toyos-rust-tests/src/bin/counters_metal.rs @@ -12,7 +12,7 @@ //! through the `/log` fileserver on that fileserver's CPU. //! //! **`idle0` waits for the ACPI server's first lines** ([`acpi_said`]): -//! `acpiserver` logs its arming, and a query number the first time the +//! `acpiserver` logs whether it serves an embedded controller, and a query number the first time the //! embedded controller raises it, and the kernel the claim's first interrupt, //! at times the machine chooses. A query number first raised later, on a quiet //! machine, is the `counters` row's to refuse. @@ -63,8 +63,8 @@ const LOADED: Duration = Duration::from_secs(20); /// at its write budget (`userland/logkeeper/src/policy.rs`, 5 s). const SETTLE_BOUND: Duration = Duration::from_secs(10); -/// How long [`acpi_said`] waits for the server to arm and the controller's -/// first query and SCI to come. +/// How long [`acpi_said`] waits for the server to say its controller and the +/// controller's first query and SCI to come. const ACPI_BOUND: Duration = Duration::from_secs(10); /// What this binary's own children are asked to do: exit at once. @@ -168,16 +168,19 @@ fn print(phase: &str, read: &Read) { } } -/// Wait until `acpiserver` has armed and, where it serves an embedded -/// controller, the kernel has logged the claim's first interrupt and the server -/// its first query. +/// Wait until `acpiserver` has said whether it serves an embedded controller +/// and, where it does, the kernel has logged the claim's first interrupt and +/// the server its first query. fn acpi_said(log: &mut Log) { let mut armed: Option = None; let (mut interrupt, mut query) = (false, false); - log.until("the ACPI server armed, and its controller's first SCI and query", ACPI_BOUND, |line| { + log.until("the ACPI server's controller, and its first SCI and query", ACPI_BOUND, |line| { if let Some(said) = program_line(line).filter(|said| said.tag == "acpiserver") { - if let Some(rest) = said.text.strip_prefix("acpiserver: armed: ") { - armed = Some(!rest.ends_with("embedded controller none")); + if said.text.starts_with(acpiserver_api::CONTROLLER_SERVED) { + armed = Some(true); + } + if said.text.starts_with(acpiserver_api::CONTROLLER_NONE) { + armed = Some(false); } query |= said.text.starts_with("acpiserver: embedded controller query ") && said.text.contains(" taken for the first time"); diff --git a/tests/toyos.rs b/tests/toyos.rs index 5dc34455148..d72fc4fb666 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5210,11 +5210,12 @@ fn counters_on_metal(back: &metal::Readback) -> Result<(), String> { Ok(()) } -/// The T14's ACPI row as the kernel filled it from the machine's FACP, ECDT -/// and APIC (Linux on the same machine: `EC_CMD/EC_SC=0x66, EC_DATA=0x62`, -/// `GPE=0x6e`, `INT_SRC_OVR (bus 0 bus_irq 9 global_irq 9 high level)`), the -/// server armed on it, at least one embedded-controller query taken and a -/// count of them logged, and no guard of the server's fired. +/// The T14's ACPI row as the kernel filled it from the machine's FACP and +/// APIC (Linux on the same machine: `INT_SRC_OVR (bus 0 bus_irq 9 global_irq +/// 9 high level)`), the server armed on it, the embedded controller it found +/// in the DSDT the one Linux used (`EC_CMD/EC_SC=0x66, EC_DATA=0x62`, +/// `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_hold")?; let (log, kernel) = (back.log(), back.kernel()); @@ -5223,12 +5224,10 @@ fn acpi_events_on_metal(back: &metal::Readback) -> Result<(), String> { eprintln!(" [acpi] {}", enabled.trim()); kernel.must_say( "acpi: the ACPI row: PM1a events 0x1800+4, GPE0 0x1860+32, SCI gsi 9 level/high, the \ - fixed-hardware power button, embedded controller at 0x66/0x62 on GPE 0x6e; the firmware \ - handed over in legacy mode", - )?; - log.must_say( - "acpiserver: armed: power button served, embedded controller on GPE 0x6e at 0x66/0x62", + fixed-hardware power button; the firmware handed over in legacy mode", )?; + log.must_say("acpiserver: armed: power button served")?; + log.must_say(&format!("{}0x6e at 0x66/0x62", acpiserver_api::CONTROLLER_SERVED))?; let lines: Vec<&str> = log .text() .lines() diff --git a/toyos-abi/src/acpi.rs b/toyos-abi/src/acpi.rs index d4ce49f49a7..43a2cb48173 100644 --- a/toyos-abi/src/acpi.rs +++ b/toyos-abi/src/acpi.rs @@ -3,8 +3,8 @@ //! //! The kernel puts the machine in ACPI mode when it mints the claim and back //! in the mode its firmware handed over when the claim goes; the holder gets -//! the register blocks the FADT and the ECDT name, as ports it may `in` and -//! `out`, and the SCI as records on its own claim handle. +//! the register blocks the FADT names, as ports it may `in` and `out`, and +//! the SCI as records on its own claim handle. //! //! **The SCI is a level line, masked by the kernel each time it is taken.** The //! holder clears the status bits behind it and then acknowledges the claim @@ -64,20 +64,8 @@ crate::user_safe! { pub pm1_event: Block, /// The GPE0 block, its status half then its enable half. pub gpe0: Block, - /// The embedded controller's command/status and data ports, as the ECDT - /// names them; `len` 0 where the machine named none. - pub ec_command: Block, - pub ec_data: Block, - /// The GPE the embedded controller raises, inside [`Self::gpe0`]. - pub ec_gpe: u16, pub flags: u16, - pub reserved: u32, - } -} - -impl AcpiInfo { - pub fn has_ec(&self) -> bool { - self.ec_command.len != 0 + pub reserved: [u16; 3], } } @@ -216,7 +204,8 @@ pub enum Refused { 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`, - /// `CST_CNT`): the kernel's to write, or nobody's. + /// `CST_CNT`, and `RESET_VALUE` where the reset register is `SMI_CMD`): + /// the kernel's to write, or nobody's. KernelCommand = 17, /// A write that reaches `SMI_CMD` and is not one byte to it. CommandSpan = 18, diff --git a/toyos-acpi/src/ecdt.rs b/toyos-acpi/src/ecdt.rs deleted file mode 100644 index f430c3a62a8..00000000000 --- a/toyos-acpi/src/ecdt.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! The ECDT: ACPI 6.5 §5.2.16, Table 5.88 — the embedded controller's ports -//! and its GPE, named for an OS that reaches the controller before any AML. - -use crate::{Phys, Table, SDT_HEADER_LEN}; - -/// Table 5.88: `EC_CONTROL` and `EC_DATA` are Generic Address Structures, -/// then `UID` (4) and `GPE_BIT` (1). -const EC_CONTROL: usize = SDT_HEADER_LEN; -const EC_DATA: usize = EC_CONTROL + 12; -const GPE_BIT: usize = EC_DATA + 12 + 4; - -/// The bytes [`ecdt`] reads to the end of. -pub const ECDT_NEEDED: usize = GPE_BIT + 1; - -const SPACE_SYSTEM_IO: u8 = 1; - -/// The embedded controller as the ECDT names it. -#[derive(Clone, Copy, 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, -} - -/// Which register an [`EcRefused`] is about. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Register { - Command, - Data, -} - -/// Why the ECDT names no controller this kernel hands out. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum EcRefused { - /// The table ends before `GPE_BIT`. - Short { len: usize }, - /// Not in the System I/O space: an MMIO controller is not served. - NotSystemIo { register: Register, space: u8 }, - /// Not one byte wide at bit 0. - Width { register: Register, bit_width: u8, bit_offset: u8 }, - /// Zero, or past the 16-bit port space. - Address { register: Register, address: u64 }, -} - -/// 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 }), - } - }; - Ok(Ec { - command: port(Register::Command, EC_CONTROL)?, - data: port(Register::Data, EC_DATA)?, - gpe: table.byte(GPE_BIT).ok_or(short)?, - }) -} diff --git a/toyos-acpi/src/fadt.rs b/toyos-acpi/src/fadt.rs index f6cef2936c5..e1fc086ec90 100644 --- a/toyos-acpi/src/fadt.rs +++ b/toyos-acpi/src/fadt.rs @@ -23,10 +23,6 @@ const FADT_X_DSDT: usize = 140; const PSCI_COMPLIANT: u16 = 1 << 0; const PSCI_USE_HVC: u16 = 1 << 1; -/// The bytes [`reset_register`] reads to the end of, so a caller opening the -/// table for that decode alone asks for what it needs and nothing after it. -pub const FADT_FOR_RESET: usize = FADT_RESET_VALUE + size_of::(); - /// Fixed feature flags bit 10, `RESET_REG_SUP` (Table 5.10); then the address space IDs of Table 5.1. const RESET_REG_SUP: u32 = 1 << 10; const SPACE_SYSTEM_MEMORY: u8 = 0; @@ -263,8 +259,11 @@ impl SmiCmd { /// The five values, each `None` where the tables name none: `S4BIOS_REQ` /// where `s4bios`, the FACS's `S4BIOS_F`, is clear, whatever the field - /// holds, and each other where its field is zero. - pub fn named(&self, s4bios: bool) -> [Option; 5] { + /// holds, and each other where its field is zero; then `RESET_VALUE` + /// where `reset`, the FADT's reset register, is this very port, a write + /// of which resets the machine (Table 5.9, `RESET_REG`) and is a sixth + /// meaning. + pub fn named(&self, s4bios: bool, reset: Reset) -> [Option; 6] { let nonzero = |value: u8| (value != 0).then_some(value); [ nonzero(self.acpi_enable), @@ -272,6 +271,10 @@ impl SmiCmd { s4bios.then_some(self.s4bios_req), nonzero(self.pstate_cnt), nonzero(self.cst_cnt), + match reset { + Reset::Port { port, value } if port == self.port => Some(value), + _ => None, + }, ] } } diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index 794c4089665..af4ea9465a7 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -15,7 +15,6 @@ #![no_std] #![forbid(unsafe_code)] -mod ecdt; mod facs; mod fadt; mod gtdt; @@ -26,13 +25,12 @@ mod spcr; use toyos_bootmap::DirectMapEnd; -pub use ecdt::{ecdt, Ec, EcRefused, Register, ECDT_NEEDED}; pub use facs::{acquire, facs, release, Facs, FacsRefused, FACS_GLOBAL_LOCK, OWNED, PENDING}; pub use fadt::{ century_of, dsdt_address, fixed_hardware, iapc_boot_arch, pm1a_control, psci, reset_register, rtc_century, Century, Field, FixedHardware, FixedRefused, LegacyMode, PowerButton, Psci, Reset, SmiCmd, CMOS_RAM, - FADT_FOR_FIXED_HARDWARE, FADT_FOR_RESET, FADT_PM1A_CNT_BLK, + FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, }; use fadt::FADT_DSDT; pub use iort::{iort, Iort, IortRefused, ItsDevice, Node, Route, Smmuv3}; @@ -41,7 +39,7 @@ pub use madt::{ Polarity, SourceOverride, Trigger, MADT_ENTRIES, }; pub use gtdt::{gtdt, Gtdt, TimerInterrupt, GTDT_NEEDED}; -pub use resource::{memory_windows, ResourceError, MAX_LIST_BYTES}; +pub use resource::{io_ports, memory_windows, ResourceError, MAX_LIST_BYTES}; pub use spcr::{spcr, Gas, SerialInterface, Spcr, GAS_SYSTEM_MEMORY, SPCR_NEEDED}; /// Physical memory, as this decoder reads it. @@ -56,6 +54,18 @@ pub trait Phys: Copy { fn byte(self, phys: u64) -> u8; } +/// Bytes an evaluation handed over, `_CRS`'s buffer among them, read as if +/// they began at address 0. +impl Phys for &[u8] { + fn readable(self, phys: u64, len: usize) -> bool { + usize::try_from(phys).ok().and_then(|at| at.checked_add(len)).is_some_and(|end| end <= self.len()) + } + + fn byte(self, phys: u64) -> u8 { + self[phys as usize] + } +} + /// Physical memory as a kernel reads it through its direct map, one byte at a /// time. /// diff --git a/toyos-acpi/src/resource.rs b/toyos-acpi/src/resource.rs index 26273cce3bc..15bb21f8760 100644 --- a/toyos-acpi/src/resource.rs +++ b/toyos-acpi/src/resource.rs @@ -1,5 +1,6 @@ //! ACPI resource descriptors, decoded into the memory windows a PCI root -//! bridge decodes. +//! bridge decodes, and into the I/O ports a device names its registers by +//! ([`io_ports`]). //! //! The bytes are the ones ACPI 6.5 §6.4.3.5 defines and firmware emits in two //! places for the same bridge: `_CRS` in the DSDT, which is what Linux reads, @@ -71,8 +72,13 @@ pub enum ResourceError { /// A memory range the bridge consumes rather than forwards — its own /// registers, not a range anything behind it decodes. Consumed { min: u64 }, - /// More memory windows than the caller has room for. + /// More memory windows, or I/O runs, than the caller has room for. TooMany { room: usize }, + /// An I/O Port Descriptor naming a range of bases the OS chooses among, + /// not one fixed run. + Relocatable { min: u16, max: u16 }, + /// An I/O run of no ports. + NoPorts { port: u16 }, } impl core::fmt::Display for ResourceError { @@ -97,8 +103,10 @@ impl core::fmt::Display for ResourceError { write!(f, "the range at {min:#x} is consumed by the bridge, not forwarded") } Self::TooMany { room } => { - write!(f, "more memory windows than the {room} there is room for") + write!(f, "more memory windows or I/O runs than the {room} there is room for") } + Self::Relocatable { min, max } => write!(f, "the I/O run is relocatable between {min:#x} and {max:#x}"), + Self::NoPorts { port } => write!(f, "the I/O run at {port:#x} is no ports long"), } } } @@ -208,3 +216,56 @@ pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) - offset = through; } } + +/// ACPI 6.5 §6.4.2.5: the I/O Port Descriptor, a small item of seven bytes: +/// its decode, its range's minimum and maximum base, alignment and length. +const IO_PORT: u8 = 0x47; +/// ACPI 6.5 §6.4.2.6: the Fixed Location I/O Port Descriptor, a small item of +/// three bytes: a 10-bit base and a length. +const FIXED_IO_PORT: u8 = 0x4B; + +/// The first port of each I/O run the list at `at` names, in its order, +/// written into `out`, and how many: what a device that names its own +/// registers by `_CRS` and by nothing else names them with (ACPI 6.5 §12.11, +/// the embedded controller's). +/// +/// **Only a run firmware fixed is a port**: an I/O Port Descriptor whose +/// minimum and maximum base differ names a range the OS chooses within, and +/// is refused, as is a run of no ports and any descriptor but these two. +pub fn io_ports(phys: P, at: u64, out: &mut [u16]) -> Result { + let mut offset = 0usize; + let mut found = 0usize; + loop { + if offset >= MAX_LIST_BYTES { + return Err(ResourceError::Unterminated); + } + let item = item(phys, at, offset)?; + let head = at + offset as u64; + let byte = |i: u64| phys.byte(head + i); + let (min, max, length) = match item.tag { + END_TAG => return Ok(found), + IO_PORT => { + let base = |i| u16::from(byte(i)) | u16::from(byte(i + 1)) << 8; + (base(2), base(4), byte(7)) + } + FIXED_IO_PORT => { + let base = (u16::from(byte(1)) | u16::from(byte(2)) << 8) & 0x3FF; + (base, base, byte(3)) + } + tag => return Err(ResourceError::UnknownTag { tag }), + }; + if min != max { + return Err(ResourceError::Relocatable { min, max }); + } + if length == 0 { + return Err(ResourceError::NoPorts { port: min }); + } + let room = out.len(); + match out.get_mut(found) { + Some(slot) => *slot = min, + None => return Err(ResourceError::TooMany { room }), + } + found += 1; + offset += item.len; + } +} diff --git a/toyos-acpi/tests/amd_laptop.rs b/toyos-acpi/tests/amd_laptop.rs new file mode 100644 index 00000000000..1ddeec20e01 --- /dev/null +++ b/toyos-acpi/tests/amd_laptop.rs @@ -0,0 +1,87 @@ +//! An AMD laptop's fixed hardware and reset register, decoded from an extract +//! of its FACP: the machine whose reset register is its `SMI_CMD`, and whose +//! power button is a control method device. +//! +//! **An extract, never the table**: the FACP here is laid out from the fields +//! `fixed_hardware`, `pm1a_control` and `reset_register` read, at their +//! offsets and with the machine's own bytes, and zeros everywhere else, then +//! sealed. The whole table was checked against these fields outside the tree. + +mod common; + +use common::{sdt, Machine}; +use toyos_abi::acpi::Block; +use toyos_acpi::{ + fixed_hardware, pm1a_control, reset_register, FixedHardware, PowerButton, Reset, SmiCmd, Table, FADT_FOR_FIXED_HARDWARE, +}; + +/// The FACP's fields: (offset, the machine's bytes there). Revision 5, 268 +/// bytes long; a Generic Address Structure is space, bit width, bit offset, +/// access size, then the address. +const FACP_LEN: usize = 268; +const FACP_REVISION: u8 = 5; +const FACP_FIELDS: &[(usize, &[u8])] = &[ + (46, &[0x09, 0x00]), + (48, &[0xb0, 0x00, 0x00, 0x00]), + (52, &[0xa0, 0xa1, 0x00, 0x00]), + (56, &[0x00, 0x04, 0x00, 0x00]), + (64, &[0x04, 0x04, 0x00, 0x00]), + (80, &[0x20, 0x04, 0x00, 0x00]), + (88, &[0x04, 0x02]), + (92, &[0x08, 0x00]), + (112, &[0xbd, 0xc5, 0x20, 0x00]), + (116, &[0x01, 0x08, 0x00, 0x01, 0xb0, 0, 0, 0, 0, 0, 0, 0]), + (128, &[0xfb]), + (148, &[0x01, 0x20, 0x00, 0x03, 0x00, 0x04, 0, 0, 0, 0, 0, 0]), + (172, &[0x01, 0x10, 0x00, 0x02, 0x04, 0x04, 0, 0, 0, 0, 0, 0]), + (220, &[0x01, 0x40, 0x00, 0x04, 0x20, 0x04, 0, 0, 0, 0, 0, 0]), +]; + +const AT: u64 = 0x9000_0000; + +fn fadt() -> Table> { + let mut body = vec![0u8; FACP_LEN - 36]; + for &(at, bytes) in FACP_FIELDS { + body[at - 36..at - 36 + bytes.len()].copy_from_slice(bytes); + } + let bytes: &'static [u8] = Box::leak(sdt(b"FACP", FACP_REVISION, &body).into_boxed_slice()); + let regions: &'static [(u64, &[u8])] = Box::leak(Box::new([(AT, bytes)])); + Table::open(Machine { regions }, AT, b"FACP", FADT_FOR_FIXED_HARDWARE).expect("a sealed extract") +} + +/// `SMI_CMD` 0xb0 with `ACPI_ENABLE` 0xa0 and `ACPI_DISABLE` 0xa1, PM1a +/// events at 0x400, control at 0x404, eight bytes of GPE0 at 0x420, and +/// flags 0x0020c5bd, whose bit 4 makes the power button a control method +/// device. +#[test] +fn the_fadt_names_a_control_method_power_button() { + let fadt = fadt(); + assert_eq!( + fixed_hardware(&fadt), + Ok(FixedHardware { + sci_int: 9, + smi_cmd: Some(SmiCmd { port: 0xb0, acpi_enable: 0xa0, acpi_disable: 0xa1, s4bios_req: 0, pstate_cnt: 0, cst_cnt: 0 }), + pm1a_event: Block { port: 0x400, len: 4 }, + gpe0: Block { port: 0x420, len: 8 }, + power_button: PowerButton::ControlMethod, + }) + ); + assert_eq!(pm1a_control(&fadt), Ok(Block { port: 0x404, len: 2 })); +} + +/// The reset register is SystemIO 0xb0 with `RESET_VALUE` 0xfb: `SMI_CMD`'s +/// own port. So 0xfb is a sixth value the tables give that port a meaning, +/// and is among the bytes kept from whoever asks for a write there; a reset +/// register anywhere else adds none. +#[test] +fn the_reset_register_is_smi_cmd_and_its_value_is_one_the_tables_name() { + let fadt = fadt(); + let reset = reset_register(&fadt); + assert_eq!(reset, Reset::Port { port: 0xb0, value: 0xfb }); + let smi_cmd = fixed_hardware(&fadt).expect("the extract decodes").smi_cmd.expect("a port"); + assert_eq!(smi_cmd.named(false, reset), [Some(0xa0), Some(0xa1), None, None, None, Some(0xfb)]); + assert_eq!(smi_cmd.named(false, Reset::Port { port: 0xcf9, value: 0xfb }), [Some(0xa0), Some(0xa1), None, None, None, None]); + for elsewhere in [Reset::Absent, Reset::Unsupported, Reset::SystemMemory, Reset::Address(0xb0_0000)] { + assert_eq!(smi_cmd.named(false, elsewhere)[5], None, "{elsewhere:?}"); + } +} diff --git a/toyos-acpi/tests/corpus.rs b/toyos-acpi/tests/corpus.rs index 1c17895b268..fc894888df2 100644 --- a/toyos-acpi/tests/corpus.rs +++ b/toyos-acpi/tests/corpus.rs @@ -12,10 +12,10 @@ 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_base, ecdt, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, + definition_blocks, dsdt_address, ecam_base, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, - EcRefused, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, - Register, Reset, SourceOverride, Table, TableError, Trigger, ECDT_NEEDED, + Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, + Reset, SourceOverride, Table, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, MAX_TABLE_LEN, }; @@ -862,8 +862,8 @@ fn the_values_the_fadt_names_for_smi_cmd_are_each_read_from_their_own_byte() { .smi_cmd .expect("a port"); assert_eq!(all, SmiCmd { port: 0xb2, acpi_enable: 0xa0, acpi_disable: 0xa1, s4bios_req: 0xa2, pstate_cnt: 0xa3, cst_cnt: 0xa4 }); - assert_eq!(all.named(true), [Some(0xa0), Some(0xa1), Some(0xa2), Some(0xa3), Some(0xa4)]); - assert_eq!(all.named(false), [Some(0xa0), Some(0xa1), None, Some(0xa3), Some(0xa4)]); + assert_eq!(all.named(true, Reset::Absent), [Some(0xa0), Some(0xa1), Some(0xa2), Some(0xa3), Some(0xa4), None]); + assert_eq!(all.named(false, Reset::Absent), [Some(0xa0), Some(0xa1), None, Some(0xa3), Some(0xa4), None]); for (at, named) in [(52, 0), (53, 1), (54, 2), (55, 3), (95, 4)] { let one = fixed(|t| { t[52..56].fill(0); @@ -873,16 +873,16 @@ fn the_values_the_fadt_names_for_smi_cmd_are_each_read_from_their_own_byte() { .expect("a FADT naming one") .smi_cmd .expect("a port"); - let mut want = [None; 5]; + let mut want = [None; 6]; want[named] = Some(0x5a); // With the flag set `S4BIOS_REQ` names the byte it holds, a zero where it holds one. let mut flagged = want; flagged[2] = Some(if named == 2 { 0x5a } else { 0 }); - assert_eq!(one.named(true), flagged, "the byte at {at}, S4BIOS_F set"); + assert_eq!(one.named(true, Reset::Absent), flagged, "the byte at {at}, S4BIOS_F set"); if named == 2 { want[2] = None; } - assert_eq!(one.named(false), want, "the byte at {at}, S4BIOS_F clear"); + assert_eq!(one.named(false, Reset::Absent), want, "the byte at {at}, S4BIOS_F clear"); } } @@ -903,44 +903,6 @@ fn a_revision_1_fadt_is_read_at_its_32_bit_fields_alone() { assert_eq!(fixed_hardware(&fadt).map(|f| f.pm1a_event), Ok(Block { port: 0x600, len: 4 })); } -fn ec_table(edit: impl FnOnce(&mut [u8])) -> Result { - let mut body = vec![0u8; ECDT_NEEDED - 36]; - body[..12].copy_from_slice(&[1, 8, 0, 0, 0x66, 0, 0, 0, 0, 0, 0, 0]); - body[12..24].copy_from_slice(&[1, 8, 0, 0, 0x62, 0, 0, 0, 0, 0, 0, 0]); - body[28] = 0x6e; - edit(&mut body); - let t = sdt(b"ECDT", 1, &body); - let regions: &[(u64, &[u8])] = &[(TABLE_AT, &t)]; - ecdt(&Table::open(Machine { regions }, TABLE_AT, b"ECDT", 36).expect("ECDT")) -} - -#[test] -fn an_ecdt_naming_no_port_controller_is_refused_by_name() { - assert_eq!(ec_table(|_| {}), Ok(toyos_acpi::Ec { command: 0x66, data: 0x62, gpe: 0x6e })); - assert_eq!( - ec_table(|b| b[0] = 0), - Err(EcRefused::NotSystemIo { register: Register::Command, space: 0 }), - "a controller in memory" - ); - assert_eq!( - ec_table(|b| b[13] = 16), - Err(EcRefused::Width { register: Register::Data, bit_width: 16, bit_offset: 0 }) - ); - assert_eq!( - ec_table(|b| b[4] = 0), - Err(EcRefused::Address { register: Register::Command, address: 0 }), - "an ECDT of zeros, which some firmware publishes" - ); - assert_eq!( - ec_table(|b| b[16 + 2] = 1), - Err(EcRefused::Address { register: Register::Data, address: 0x1_0062 }) - ); - let short = sdt(b"ECDT", 1, &[1, 8, 0, 0, 0x66, 0, 0, 0, 0, 0, 0, 0]); - let regions: &[(u64, &[u8])] = &[(TABLE_AT, &short)]; - let table = Table::open(Machine { regions }, TABLE_AT, b"ECDT", 36).expect("ECDT"); - assert_eq!(ecdt(&table), Err(EcRefused::Short { len: 48 })); -} - /// Table 5.9: the OS treats the SCI as level and active low. Where no override /// names it, and where one names it conforming, that is what it is; where one /// says otherwise, what it says. diff --git a/toyos-acpi/tests/resource.rs b/toyos-acpi/tests/resource.rs index 2a32804ce75..5143196b739 100644 --- a/toyos-acpi/tests/resource.rs +++ b/toyos-acpi/tests/resource.rs @@ -5,7 +5,7 @@ mod common; use common::{qword_descriptor, resource_list as list, Machine, BUS, IO, MEMORY}; use toyos_abi::boot::RootBridgeWindow; -use toyos_acpi::{memory_windows, ResourceError, MAX_LIST_BYTES}; +use toyos_acpi::{io_ports, memory_windows, ResourceError, MAX_LIST_BYTES}; /// Where a firmware pool allocation sits in the crafted machines below. const AT: u64 = 0x7f00_1234; @@ -193,3 +193,59 @@ fn a_list_that_runs_off_the_end_of_what_can_be_read_is_refused() { bytes.truncate(40); assert_eq!(windows(&bytes, 8), Err(ResourceError::Unreadable { at: AT, len: 46 })); } + +/// An I/O Port Descriptor (ACPI 6.5 §6.4.2.5): 16-bit decode, a run of `len` +/// ports whose base is anywhere from `min` to `max`. +fn io(min: u16, max: u16, len: u8) -> Vec { + let [min_lo, min_hi] = min.to_le_bytes(); + let [max_lo, max_hi] = max.to_le_bytes(); + vec![0x47, 0x01, min_lo, min_hi, max_lo, max_hi, 0x00, len] +} + +/// A Fixed Location I/O Port Descriptor (§6.4.2.6). +fn fixed_io(base: u16, len: u8) -> Vec { + let [lo, hi] = base.to_le_bytes(); + vec![0x4B, lo, hi, len] +} + +/// The first port of every run `bytes` names, read from [`AT`]. +fn ports(bytes: &[u8], room: usize) -> Result, ResourceError> { + let regions: &[(u64, &[u8])] = &[(AT, bytes)]; + let mut out = vec![0; room]; + let count = io_ports(Machine { regions }, AT, &mut out)?; + out.truncate(count); + Ok(out) +} + +/// An AMD laptop's embedded controller, as its `_CRS` names it, decoded: +/// two I/O Port Descriptors of one port each, 0x62 then 0x66, each fixed by +/// a minimum equal to its maximum. ACPI 6.5 §12.11 puts the data register +/// first and the command/status register second; the order is the list's. +#[test] +fn an_amd_laptops_controller_is_two_fixed_ports_in_the_lists_order() { + let crs = list(&[io(0x62, 0x62, 1), io(0x66, 0x66, 1)]); + assert_eq!(ports(&crs, 2), Ok(vec![0x62, 0x66])); + // The same list as an evaluation hands the buffer over, from address 0. + let mut out = [0; 2]; + assert_eq!(io_ports(&crs[..], 0, &mut out), Ok(2)); + assert_eq!(out, [0x62, 0x66]); + // The fixed form names the same two, its base in ten bits. + assert_eq!(ports(&list(&[fixed_io(0x62, 1), fixed_io(0xFC66, 1)]), 2), Ok(vec![0x62, 0x66])); +} + +/// A run the OS would choose within, a run of no ports, any other descriptor +/// and more runs than there is room for are each refused by name, never +/// stepped over. +#[test] +fn an_io_run_that_is_no_fixed_port_is_refused_by_name() { + assert_eq!(ports(&list(&[io(0x62, 0x6A, 1)]), 2), Err(ResourceError::Relocatable { min: 0x62, max: 0x6A })); + assert_eq!(ports(&list(&[io(0x62, 0x62, 0)]), 2), Err(ResourceError::NoPorts { port: 0x62 })); + assert_eq!(ports(&list(&[fixed_io(0x66, 0)]), 2), Err(ResourceError::NoPorts { port: 0x66 })); + // IRQ (§6.4.2.1) beside the two ports. + assert_eq!(ports(&list(&[io(0x62, 0x62, 1), vec![0x22, 0x01, 0x00]]), 2), Err(ResourceError::UnknownTag { tag: 0x22 })); + let three = list(&[io(0x62, 0x62, 1), io(0x66, 0x66, 1), io(0x68, 0x68, 1)]); + assert_eq!(ports(&three, 2), Err(ResourceError::TooMany { room: 2 })); + assert_eq!(ports(&[0x47, 0x01, 0x62], 2), Err(ResourceError::Unreadable { at: AT, len: 8 })); + let unterminated = io(0x62, 0x62, 1).repeat(MAX_LIST_BYTES / 8 + 1); + assert_eq!(ports(&unterminated, MAX_LIST_BYTES), Err(ResourceError::Unterminated)); +} diff --git a/toyos-acpi/tests/thinkpad_t14.rs b/toyos-acpi/tests/thinkpad_t14.rs index 7f05a10bdb7..3d4237a68ff 100644 --- a/toyos-acpi/tests/thinkpad_t14.rs +++ b/toyos-acpi/tests/thinkpad_t14.rs @@ -1,9 +1,9 @@ -//! The T14's fixed hardware, embedded controller and SCI, decoded from an -//! extract of its FACP, ECDT and APIC and held against what Linux read on the +//! The T14's fixed hardware and SCI, decoded from an +//! extract of its FACP and APIC and held against what Linux read on the //! same machine (`fixtures/thinkpad-t14/SOURCE`). //! //! **An extract, never the tables**: each table here is laid out from the -//! fields `fixed_hardware`, `pm1a_control`, `ecdt` and `sci_line` read, at their offsets and +//! fields `fixed_hardware`, `pm1a_control` and `sci_line` read, at their offsets and //! with the machine's own bytes, and zeros everywhere else, then sealed. The //! whole tables are checked against these fields outside the tree. @@ -12,8 +12,8 @@ mod common; use common::{entry, madt, sdt, Machine}; use toyos_abi::acpi::Block; use toyos_acpi::{ - ecdt, fixed_hardware, madt_entries, pm1a_control, sci_line, Ec, FixedHardware, Line, MadtEntry, - Polarity, PowerButton, SmiCmd, SourceOverride, Table, Trigger, ECDT_NEEDED, FADT_FOR_FIXED_HARDWARE, + fixed_hardware, madt_entries, pm1a_control, sci_line, FixedHardware, Line, MadtEntry, + Polarity, PowerButton, SmiCmd, SourceOverride, Table, Trigger, FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, }; @@ -41,15 +41,6 @@ const FACP_FIELDS: &[(usize, &[u8])] = &[ (232, &[0x01, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0, 0, 0]), ]; -/// The ECDT's `EC_CONTROL`, `EC_DATA`, `UID` and `GPE_BIT`. -const ECDT_REVISION: u8 = 1; -const ECDT_FIELDS: &[(usize, &[u8])] = &[ - (36, &[0x01, 0x08, 0x00, 0x00, 0x66, 0, 0, 0, 0, 0, 0, 0]), - (48, &[0x01, 0x08, 0x00, 0x00, 0x62, 0, 0, 0, 0, 0, 0, 0]), - (60, &[0x00, 0x00, 0x00, 0x00]), - (64, &[0x6e]), -]; - /// The APIC's two interrupt source overrides, whole. const APIC_OVERRIDES: &[[u8; 10]] = &[ [0x02, 0x0a, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00], @@ -97,14 +88,6 @@ fn the_t14s_fadt_names_the_blocks_linux_served_its_sci_through() { assert_eq!(pm1a_control(&fadt), Ok(Block { port: 0x1804, len: 2 })); } -/// Linux: `ACPI: EC: EC_CMD/EC_SC=0x66, EC_DATA=0x62` and `ACPI: EC: GPE=0x6e`. -#[test] -fn the_t14s_ecdt_names_the_controller_linux_used() { - let bytes = laid_out(b"ECDT", ECDT_REVISION, ECDT_NEEDED, ECDT_FIELDS); - let table = open(bytes, b"ECDT", ECDT_NEEDED); - assert_eq!(ecdt(&table), Ok(Ec { command: 0x66, data: 0x62, gpe: 0x6e })); -} - /// Linux: `ACPI: INT_SRC_OVR (bus 0 bus_irq 9 global_irq 9 high level)`. #[test] fn the_t14s_madt_names_its_sci_level_and_active_high() { diff --git a/toyos-userbound/src/port.rs b/toyos-userbound/src/port.rs index d66740e3736..bfc27410789 100644 --- a/toyos-userbound/src/port.rs +++ b/toyos-userbound/src/port.rs @@ -125,9 +125,11 @@ pub enum Mediated { } /// The bytes a port that commands the firmware takes from the kernel alone: -/// those the machine's tables give a meaning, `None` where they name none. +/// those the machine's tables give a meaning, `None` where they name none: +/// the FADT's five for `SMI_CMD`, and its reset register's value where that +/// register is the same port, so no write asked of it resets the machine. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct KeptCommands(pub [Option; 5]); +pub struct KeptCommands(pub [Option; 6]); impl KeptCommands { pub fn holds(self, value: u8) -> bool { diff --git a/toyos-userbound/tests/firmware.rs b/toyos-userbound/tests/firmware.rs index aec6232f63a..d68fd236187 100644 --- a/toyos-userbound/tests/firmware.rs +++ b/toyos-userbound/tests/firmware.rs @@ -493,8 +493,8 @@ fn the_lock_word_is_exchanged_only_where_all_four_bytes_are_the_firmwares_own() } /// What a crafted FADT names for `SMI_CMD`: `ACPI_ENABLE`, `ACPI_DISABLE`, -/// `S4BIOS_REQ` and `CST_CNT`, and no `PSTATE_CNT`. -const KEPT: KeptCommands = KeptCommands([Some(0xF0), Some(0xF1), Some(0xF2), None, Some(0x85)]); +/// `S4BIOS_REQ` and `CST_CNT`, no `PSTATE_CNT`, and a reset register elsewhere. +const KEPT: KeptCommands = KeptCommands([Some(0xF0), Some(0xF1), Some(0xF2), None, Some(0x85), None]); /// The kernel's declarations as q35 boots with them, with [`KEPT`] for /// `SMI_CMD`'s, and the i8042's row. @@ -560,16 +560,35 @@ fn a_byte_for_smi_cmd_is_a_firmware_call_unless_the_fadt_names_it() { } } // A FADT that names none keeps none. - let unnamed = |_| Standing::Declared(Mediated::Command(KeptCommands([None; 5]))); + let unnamed = |_| Standing::Declared(Mediated::Command(KeptCommands([None; 6]))); for value in [0u64, 0xF0, 0xFF] { assert!(matches!(port(unnamed, 0xB2, Width::Byte, Some(value)), PortVerdict::FirmwareCall(call) if u64::from(call.value()) == value)); } // And one that names zero keeps zero. - let zero = |_| Standing::Declared(Mediated::Command(KeptCommands([None, None, Some(0), None, None]))); + let zero = |_| Standing::Declared(Mediated::Command(KeptCommands([None, None, Some(0), None, None, None]))); assert_eq!(port(zero, 0xB2, Width::Byte, Some(0)), refused_port(Refused::KernelCommand)); assert!(matches!(port(zero, 0xB2, Width::Byte, Some(1)), PortVerdict::FirmwareCall(_))); } +/// An AMD laptop's `SMI_CMD` is its reset register too, 0xb0 with +/// `RESET_VALUE` 0xfb beside `ACPI_ENABLE` 0xa0 and `ACPI_DISABLE` 0xa1: the +/// one declaration keeps the reset's byte with the FADT's own, so no write +/// asked of the port resets the machine, and every other byte is a call. +#[test] +fn a_smi_cmd_that_is_the_reset_register_keeps_the_resets_byte() { + let shared = |port| match port { + 0xB0 => Standing::Declared(Mediated::Command(KeptCommands([Some(0xA0), Some(0xA1), None, None, None, Some(0xFB)]))), + _ => Standing::Free, + }; + for kept in [0xA0u64, 0xA1, 0xFB] { + assert_eq!(port(shared, 0xB0, Width::Byte, Some(kept)), refused_port(Refused::KernelCommand), "{kept:#04x}"); + } + for called in [0x00u64, 0x28, 0xFA, 0xFC, 0xFF] { + assert!(matches!(port(shared, 0xB0, Width::Byte, Some(called)), PortVerdict::FirmwareCall(call) if u64::from(call.value()) == called), "{called:#04x}"); + } + assert_eq!(through(port(shared, 0xB0, Width::Byte, None)), Some((0xB0, Width::Byte))); +} + /// A command is one byte to the one port: a wider write that reaches it, /// from below or from it, is refused, whatever byte would land there, and so /// is a value no byte holds. diff --git a/toyos/src/device.rs b/toyos/src/device.rs index 7c69e4f03be..3919726d0dc 100644 --- a/toyos/src/device.rs +++ b/toyos/src/device.rs @@ -191,7 +191,7 @@ fn irq_record(dev: &Device) -> Result { + pub interpreter: Interpreter, + pub host: Firmware<'k, K>, +} /// What became of the machine's definition blocks: the load's verdict, which /// its lines say. Whoever evaluates a method after the load asks `blocks` @@ -56,7 +59,7 @@ pub struct Loaded { } /// What a refused evaluation or load is called in a line anyone may quote. -fn kind(why: &Error) -> String { +pub fn kind(why: &Error) -> String { match why { Error::Malformed { why, .. } => format!("malformed AML: {why}"), Error::NotFound(_) => "a name that does not resolve".into(), @@ -97,8 +100,16 @@ pub const NO_S5_HANDED: &str = "acpiserver: no \\_S5 was handed to the kernel, which powers off only on one an ACPI server supplied and refuses a shutdown without one"; /// Load the machine's definition blocks from the RSDP at `rsdp`, and -/// evaluate `\_S5`. -pub fn load(kernel: &K, rsdp: u64) -> Loaded { +/// 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); + 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; + (loaded, kept.then_some(Aml { interpreter, host })) +} + +fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Firmware<'_, K>, rsdp: u64) -> Loaded { let began = Instant::now(); let mut loaded = Loaded::default(); let tables = Tables::new(kernel); @@ -124,14 +135,12 @@ pub fn load(kernel: &K, rsdp: u64) -> Loaded { return loaded; } - let mut host = Firmware::new(kernel); - let mut interpreter = Interpreter::new(); let count = blocks.len(); for (place, block) in blocks.iter().enumerate() { let name = if place == 0 { "DSDT" } else { "SSDT" }; let place = place + 1; let done = match block { - Ok(table) => interpreter.load(&mut host, table).map_err(|why| { + Ok(table) => interpreter.load(host, table).map_err(|why| { println!("{OWN}table {place} was refused {why:x?}"); kind(&why) }), @@ -184,7 +193,7 @@ pub fn load(kernel: &K, rsdp: u64) -> Loaded { ); if loaded.blocks.first().is_some_and(|dsdt| dsdt.is_ok()) { - match s5(&mut interpreter, &mut host) { + match s5(interpreter, host) { Ok((a, b)) => { println!("acpiserver: \\_S5 evaluated: SLP_TYPa={a} SLP_TYPb={b}"); loaded.s5 = Some((a, b)); @@ -233,11 +242,15 @@ fn s5(interpreter: &mut Interpreter, host: &mut dyn Host) -> Result<(u64, u64), } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use crate::host::tests::Scripted; use crate::host::{Take, HELD}; + fn load_only(kernel: &K, rsdp: u64) -> Loaded { + load(kernel, rsdp).0 + } + /// QEMU's own tables where its guest held them /// (`toyos-acpi/fixtures/qemu-11.1.1/SOURCE`), as ACPI reclaim memory. const RSDP: u64 = 0x7fb7_e014; @@ -264,7 +277,7 @@ mod tests { #[test] fn qemus_tables_load_and_s5_is_what_its_kernel_decoded() { let kernel = machine(QEMU); - assert_eq!(load(&kernel, RSDP), Loaded { blocks: vec![Ok(())], s5: Some((0, 0)), handed: true }); + assert_eq!(load_only(&kernel, RSDP), Loaded { blocks: vec![Ok(())], s5: Some((0, 0)), handed: true }); assert!(kernel.asked.borrow().iter().all(|access| access.write == 0 && access.space == 0)); assert!(!kernel.held.get()); assert_eq!(kernel.handed.get(), Some(0)); @@ -273,7 +286,7 @@ mod tests { // The builders below are `toyos-aml`'s test encodings of §20.2, the few a // definition block here needs. - fn sealed(signature: &[u8; 4], body: &[u8]) -> Vec { + pub fn sealed(signature: &[u8; 4], body: &[u8]) -> Vec { let mut table = vec![0u8; 36]; table[..4].copy_from_slice(signature); table[4..8].copy_from_slice(&(36 + body.len() as u32).to_le_bytes()); @@ -283,22 +296,22 @@ mod tests { table } - fn cat(parts: &[&[u8]]) -> Vec { + pub fn cat(parts: &[&[u8]]) -> Vec { parts.concat() } /// `Name (, )` for a four-character name and a byte. - fn name(name: &[u8; 4], value: u8) -> Vec { + pub fn name(name: &[u8; 4], value: u8) -> Vec { cat(&[&[0x08], name, &[0x0A, value]]) } /// `Name (_S5_, Package (2) { a, b })`. - fn s5_package(a: u8, b: u8) -> Vec { + pub fn s5_package(a: u8, b: u8) -> Vec { cat(&[&[0x08], b"_S5_", &[0x12, 0x06, 0x02, 0x0A, a, 0x0A, b]]) } /// A machine whose XSDT lists a FADT naming `dsdt` and then `ssdts`. - fn crafted(dsdt: &[u8], ssdts: &[&[u8]]) -> Scripted { + pub fn crafted(dsdt: &[u8], ssdts: &[&[u8]]) -> Scripted { const AT: u64 = 0x10_0000; let at = |n: usize| AT + 0x1_0000 * n as u64; let mut fadt_body = vec![0u8; 116 - 36]; @@ -324,7 +337,7 @@ mod tests { Scripted { memory, ..Default::default() } } - const CRAFTED_RSDP: u64 = 0x10_0000 - 0x1000; + pub const CRAFTED_RSDP: u64 = 0x10_0000 - 0x1000; #[test] fn a_refused_ssdt_is_one_block_refused_and_the_rest_load() { @@ -335,7 +348,7 @@ mod tests { let good = sealed(b"SSDT", &name(b"CCCC", 4)); let kernel = crafted(&dsdt, &[&collides, &unsummed, &good]); assert_eq!( - load(&kernel, CRAFTED_RSDP), + load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(()), Err("a name defined twice".into()), Err("its bytes do not sum to zero".into()), Ok(())], s5: Some((5, 7)), @@ -350,7 +363,7 @@ mod tests { let dsdt = sealed(b"DSDT", &[0x08, b'A']); let ssdt = sealed(b"SSDT", &name(b"CCCC", 4)); let kernel = crafted(&dsdt, &[&ssdt, &ssdt]); - let loaded = load(&kernel, CRAFTED_RSDP); + let loaded = load_only(&kernel, CRAFTED_RSDP); assert_eq!(loaded.blocks.len(), 1, "an SSDT was loaded onto no DSDT: {loaded:?}"); assert!(matches!(&loaded.blocks[0], Err(kind) if kind.starts_with("malformed AML: ")), "{loaded:?}"); assert_eq!(loaded.s5, None); @@ -359,12 +372,12 @@ mod tests { #[test] fn tables_the_kernel_will_not_read_are_refused_by_its_name_for_it() { // No RSDP where the claim says: RAM, to the stand-in. - assert_eq!(load(&machine(QEMU), 0x1000), Loaded::default()); + assert_eq!(load_only(&machine(QEMU), 0x1000), Loaded::default()); // The DSDT's own bytes are not the firmware's to read. let kernel = machine(&QEMU[..QEMU.len() - 1]); let ram = "its bytes could not be read: a SystemMemory read the kernel refused UsableMemory, in memory of type 7"; - assert_eq!(load(&kernel, RSDP), Loaded { blocks: vec![Err(ram.into())], s5: None, handed: false }); + assert_eq!(load_only(&kernel, RSDP), Loaded { blocks: vec![Err(ram.into())], s5: None, handed: false }); } /// The shape the first load on the real machine had: its RSDP and XSDT @@ -381,17 +394,17 @@ mod tests { let listed = kernel.memory.split_off(2); kernel.kept = listed.iter().map(|(at, _, bytes)| (*at, at + bytes.len() as u64, 5)).collect(); let kept = "its bytes could not be read: a SystemMemory read the kernel refused MemoryType, in memory of type 5"; - assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Err(kept.into())], s5: None, handed: false }); + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Err(kept.into())], s5: None, handed: false }); } #[test] fn an_s5_that_is_no_package_of_two_integers_is_refused_by_kind() { let absent = sealed(b"DSDT", &name(b"AAAA", 1)); - assert_eq!(load(&crafted(&absent, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); + assert_eq!(load_only(&crafted(&absent, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); let integer = sealed(b"DSDT", &name(b"_S5_", 5)); - assert_eq!(load(&crafted(&integer, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); + assert_eq!(load_only(&crafted(&integer, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); let short = sealed(b"DSDT", &cat(&[&[0x08], b"_S5_", &[0x12, 0x04, 0x01, 0x0A, 0x05]])); - assert_eq!(load(&crafted(&short, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); + assert_eq!(load_only(&crafted(&short, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); } /// `SLP_TYPa` is the package's first element and the only one handed @@ -401,20 +414,20 @@ mod tests { #[test] fn only_an_s5_the_kernel_kept_is_a_power_off() { let kernel = crafted(&sealed(b"DSDT", &s5_package(7, 9)), &[]); - assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((7, 9)), handed: true }); + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((7, 9)), handed: true }); assert_eq!(kernel.handed.get(), Some(7)); let kernel = crafted(&sealed(b"DSDT", &s5_package(8, 0)), &[]); - assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((8, 0)), handed: false }); + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((8, 0)), handed: false }); assert_eq!(kernel.handed.get(), None); for dsdt in [sealed(b"DSDT", &name(b"AAAA", 1)), sealed(b"DSDT", &[0x08, b'A'])] { let kernel = crafted(&dsdt, &[]); - assert!(!load(&kernel, CRAFTED_RSDP).handed); + assert!(!load_only(&kernel, CRAFTED_RSDP).handed); assert_eq!(kernel.handed.get(), None); } let kernel = machine(QEMU); - assert!(!load(&kernel, 0x1000).handed); + assert!(!load_only(&kernel, 0x1000).handed); assert_eq!(kernel.handed.get(), None); } @@ -423,7 +436,7 @@ mod tests { for answered in [0, 3, 40] { let kernel = machine(QEMU); kernel.stops_after.set(Some(answered)); - assert_eq!(load(&kernel, RSDP), Loaded::default(), "after {answered} reads"); + assert_eq!(load_only(&kernel, RSDP), Loaded::default(), "after {answered} reads"); } } @@ -483,13 +496,13 @@ mod tests { let field = toyos_abi::acpi::Access::read(toyos_abi::acpi::Space::SystemMemory, NVS, toyos_abi::acpi::Width::Byte); let kernel = with_nvs(); - assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((5, 0)), handed: true }); + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((5, 0)), handed: true }); assert!(!kernel.held.get(), "the load ended holding the Global Lock"); assert_eq!(kernel.asked.borrow().last(), Some(&field)); let kernel = with_nvs(); kernel.takes.borrow_mut().push_back(Take::Pending); - let loaded = load(&kernel, CRAFTED_RSDP); + let loaded = load_only(&kernel, CRAFTED_RSDP); assert_eq!(loaded, Loaded { blocks: vec![Err(format!("denied by this server: {HELD}"))], s5: None, handed: false }); assert_ne!(kernel.asked.borrow().last(), Some(&field), "the field was read without the lock"); } diff --git a/userland/acpiserver/src/devices.rs b/userland/acpiserver/src/devices.rs new file mode 100644 index 00000000000..3337a608feb --- /dev/null +++ b/userland/acpiserver/src/devices.rs @@ -0,0 +1,400 @@ +//! The devices this server serves that only the machine's namespace names: +//! the embedded controller (ACPI 6.5 §12.11), whose ports and GPE no table the +//! kernel reads gives, and a power button that is a control method device +//! (§4.8.2.2.1.2), which the AML that notifies it is the only account of. +//! +//! **A device is one whose `_HID` names it** (§6.1.5): an EISA ID's integer +//! or a string, PNP0C09 for the controller and PNP0C0C for the button, and +//! **present where its `_STA` says so** (§6.3.7), bit 0, a device with none +//! being present. +//! +//! **The controller is its `_CRS`'s two fixed ports and its `_GPE`'s +//! integer**: §12.11 puts the data register first and the command/status +//! register second, and a `_GPE` that is a package names a GPE block device, +//! which this server does not serve. The GPE is held to the FADT's GPE0 +//! block, eight a byte of its status half. A machine with more than one +//! controller present has none served. Whatever keeps a controller from +//! being served is said once, by kind; what the firmware chose — a path, a +//! number — goes under [`OWN`]. +//! +//! **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. + +use std::collections::BTreeSet; + +use toyos_abi::acpi::Block; +use acpiserver_api::CONTROLLER_NONE; +use toyos_aml::{Host, Interpreter, Kind, Value}; + +use crate::aml::kind; +use crate::host::OWN; + +/// The embedded controller, as its device names it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Ec { + pub path: String, + pub command: u16, + pub data: u16, + pub gpe: u16, + /// The queries its device defines a method for. + pub queries: BTreeSet, +} + +/// What was found. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Found { + pub ec: Option, + /// Every control-method power button present, by path. + pub buttons: Vec, +} + +const EC: &str = "PNP0C09"; +const POWER_BUTTON: &str = "PNP0C0C"; + +/// Find the controller, and the buttons where `control_method`, 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 { + 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 mut controllers = Vec::new(); + for device in devices { + let hid = format!("{device}._HID"); + if !named.contains(&hid) { + continue; + } + let id = match interpreter.evaluate(host, &hid, &[]) { + Ok(value) => id(&value), + Err(why) => { + println!("{OWN}{hid} was refused {why:x?}"); + println!("acpiserver: a device's _HID did not evaluate, so it is no device this server serves: {}", kind(&why)); + continue; + } + }; + let wanted = match id.as_deref() { + Some(EC) => true, + Some(POWER_BUTTON) => control_method, + _ => false, + }; + if !wanted || !present(interpreter, host, &named, &device) { + continue; + } + match id.as_deref() { + Some(EC) => controllers.push(device), + _ => { + println!("{OWN}{device} is the control-method power button"); + found.buttons.push(device); + } + } + } + found.ec = match controllers.as_slice() { + [] => { + println!("{CONTROLLER_NONE}no PNP0C09 device is present"); + None + } + [device] => match controller(interpreter, host, device, gpe0, &named) { + Ok(ec) => { + println!("{OWN}{device} is the embedded controller"); + Some(ec) + } + Err(why) => { + println!("{CONTROLLER_NONE}its device {why}"); + None + } + }, + more => { + println!("{CONTROLLER_NONE}{} PNP0C09 devices are present, and this server serves one", more.len()); + None + } + }; + found +} + +/// What became of an embedded-controller query's method. +#[derive(Debug, PartialEq, Eq)] +pub enum Queried { + Ran, + /// The controller's device defines none for the query. + Absent, + /// By the kind of its refusal. + Refused(String), +} + +/// Run the method the controller's device defines for query `q` (§12.11, +/// `_Qxx`, the number in two hex digits): its Notifies are the host's. +pub fn query(interpreter: &mut Interpreter, host: &mut dyn Host, ec: &Ec, q: u8) -> Queried { + if !ec.queries.contains(&q) { + return Queried::Absent; + } + match interpreter.evaluate(host, &format!("{}._Q{q:02X}", ec.path), &[]) { + Ok(_) => Queried::Ran, + Err(why) => { + println!("{OWN}{}._Q{q:02X} was refused {why:x?}", ec.path); + Queried::Refused(kind(&why)) + } + } +} + +/// 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()); + } + named.insert(walk.path().to_string()); + } + Ok((devices, named)) +} + +/// 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 { + match value { + Value::String(text) => String::from_utf8(text.clone()).ok(), + &Value::Integer(compressed) => { + let [vendor_hi, vendor_lo, product_hi, product_lo] = u32::try_from(compressed).ok()?.to_le_bytes(); + let vendor = u16::from_be_bytes([vendor_hi, vendor_lo]); + let letter = |shift: u16| char::from(b'@' + (vendor >> shift & 0x1F) as u8); + Some(format!("{}{}{}{product_hi:02X}{product_lo:02X}", letter(10), letter(5), letter(0))) + } + _ => None, + } +} + +/// Whether `device` is present: bit 0 of its `_STA`, which it is where it has +/// none; one whose `_STA` does not evaluate to an integer is said and is not. +fn present(interpreter: &mut Interpreter, host: &mut dyn Host, named: &BTreeSet, device: &str) -> bool { + let sta = format!("{device}._STA"); + if !named.contains(&sta) { + return true; + } + match interpreter.evaluate(host, &sta, &[]) { + Ok(Value::Integer(status)) => status & 1 != 0, + Ok(_) => { + println!("{OWN}{sta} is no integer"); + println!("acpiserver: a device's _STA is no integer, so it is taken as absent"); + false + } + Err(why) => { + println!("{OWN}{sta} was refused {why:x?}"); + println!("acpiserver: a device's _STA did not evaluate, so it is taken as absent: {}", kind(&why)); + false + } + } +} + +/// The controller `device` names, or why it names none this server serves. +fn controller(interpreter: &mut Interpreter, host: &mut dyn Host, device: &str, gpe0: Block, named: &BTreeSet) -> Result { + let crs = match interpreter.evaluate(host, &format!("{device}._CRS"), &[]) { + Ok(Value::Buffer(list)) => list, + Ok(_) => return Err("names its resources by no buffer".into()), + Err(why) => return Err(format!("names no resources: {}", kind(&why))), + }; + let mut ports = [0u16; 2]; + match toyos_acpi::io_ports(&crs[..], 0, &mut ports) { + Ok(2) => {} + Ok(n) => return Err(format!("names {n} I/O run(s), not the two of §12.11")), + Err(why) => return Err(format!("names its ports by a list this server does not read: {why}")), + } + let [data, command] = ports; + // §5.2.9: a GPE block's status half holds eight GPEs a byte. + let gpes = u64::from(gpe0.len / 2 * 8); + let gpe = match interpreter.evaluate(host, &format!("{device}._GPE"), &[]) { + Ok(Value::Integer(gpe)) if gpe < gpes => gpe as u16, + Ok(Value::Integer(gpe)) => { + println!("{OWN}{device}._GPE is {gpe:#x}"); + return Err(format!("raises a GPE outside GPE0's {gpes}")); + } + Ok(Value::Package(_)) => return Err("raises its GPE in a GPE block device, which this server does not serve".into()), + Ok(_) => return Err("names its GPE by no integer".into()), + 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 }) +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::aml::tests::{cat, crafted, sealed, CRAFTED_RSDP}; + use crate::aml::{load, Aml}; + + /// GPE0 as an AMD laptop's FADT names it: eight bytes, 32 GPEs. + pub const GPE0: Block = Block { port: 0x420, len: 8 }; + + /// A NameString of segments from the root: `\` and a MultiNamePrefix. + pub fn path(segments: &[&[u8; 4]]) -> Vec { + let mut out = vec![b'\\']; + match segments.len() { + 1 => {} + 2 => out.push(0x2E), + n => out.extend_from_slice(&[0x2F, n as u8]), + } + segments.iter().for_each(|segment| out.extend_from_slice(*segment)); + out + } + + /// A PkgLength of one to four bytes, for `len` bytes after it (§20.2.4). + pub fn pkg(body: &[u8]) -> Vec { + let mut out = match body.len() + 1 { + n if n < 0x40 => vec![n as u8], + _ => { + let n = body.len() + 2; + assert!(n < 0x1000, "a test package past two length bytes"); + vec![0x40 | (n & 0xF) as u8, (n >> 4) as u8] + } + }; + out.extend_from_slice(body); + out + } + + /// `Device () { }`. + pub fn device(at: &[&[u8; 4]], body: &[u8]) -> Vec { + cat(&[&[0x5B, 0x82], &pkg(&cat(&[&path(at), body]))]) + } + + /// `Name (, )`, the form `EisaId` and `_STA` take. + pub fn dword(name: &[u8; 4], value: u32) -> Vec { + cat(&[&[0x08], name, &[0x0C], &value.to_le_bytes()]) + } + + /// `Name (, )`. + pub fn byte(name: &[u8; 4], value: u8) -> Vec { + cat(&[&[0x08], name, &[0x0A, value]]) + } + + /// `Name (_CRS, ResourceTemplate () { IO (Decode16, p, p, 0, 1) ... })`. + pub fn crs(ports: &[u16]) -> Vec { + let mut list: Vec = ports.iter().flat_map(|p| { let [lo, hi] = p.to_le_bytes(); [0x47, 0x01, lo, hi, lo, hi, 0x00, 0x01] }).collect(); + list.extend_from_slice(&[0x79, 0x00]); + let buffer = cat(&[&[0x0A, list.len() as u8], &list]); + cat(&[&[0x08], b"_CRS", &[0x11], &pkg(&buffer)]) + } + + /// `EisaId ("PNP0C09")` and `EisaId ("PNP0C0C")`, as an AMD laptop's DSDT + /// stores them. + pub const PNP0C09: u32 = 0x090C_D041; + pub const PNP0C0C: u32 = 0x0C0C_D041; + + /// An AMD laptop's controller and power button, decoded: the controller + /// at `\_SB.PCI0.LPC0.EC` with `_HID` PNP0C09, an `_STA` of 0x0F, its + /// `_CRS` 0x62 then 0x66 and `_GPE` 3; and the button at `\_SB.PWRB` + /// with `_HID` PNP0C0C and an `_STA` of 0x0B. + pub fn laptop(extra_ec: &[u8]) -> Vec { + cat(&[ + &device(&[b"_SB_", b"PCI0"], &[]), + &device(&[b"_SB_", b"PCI0", b"LPC0"], &[]), + &device(&[b"_SB_", b"PCI0", b"LPC0", b"EC__"], &cat(&[&dword(b"_HID", PNP0C09), &byte(b"_STA", 0x0F), &crs(&[0x62, 0x66]), &byte(b"_GPE", 3), extra_ec])), + &device(&[b"_SB_", b"PWRB"], &cat(&[&dword(b"_HID", PNP0C0C), &byte(b"_STA", 0x0B)])), + ]) + } + + pub const EC_PATH: &str = "\\_SB_.PCI0.LPC0.EC__"; + pub const PWRB: &str = "\\_SB_.PWRB"; + + fn found(dsdt: &[u8], control_method: bool) -> Found { + 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) + } + + #[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() }; + assert_eq!(found(&laptop(&[]), true), Found { ec: Some(ec.clone()), buttons: vec![PWRB.into()] }); + assert_eq!(found(&laptop(&[]), false), Found { ec: Some(ec), buttons: vec![] }, "a fixed button's machine looks for none"); + } + + /// An AMD laptop's `_Q28`, decoded: its query's number stored to the POST + /// port as a word, then `Notify (\_SB.PWRB, 0x80)`; beside it `_Q2A`, + /// whose Notify is the lid's. + pub fn queries() -> Vec { + let region = cat(&[&[0x5B, 0x80], b"P80R", &[0x01, 0x0A, 0x80, 0x0A, 0x02]]); + let field = cat(&[&[0x5B, 0x81], &pkg(&cat(&[b"P80R", &[0x02], b"P80H", &[0x10]]))]); + let method = |name: &[u8; 4], q: u8, notified: &[u8]| { + let store = cat(&[&[0x70, 0x0A, q], b"P80H"]); + let notify = cat(&[&[0x86], notified, &[0x0A, 0x80]]); + cat(&[&[0x14], &pkg(&cat(&[name, &[0x00], &store, ¬ify]))]) + }; + cat(&[®ion, &field, &method(b"_Q28", 0x28, &path(&[b"_SB_", b"PWRB"])), &method(b"_Q2A", 0x2A, &path(&[b"_SB_", b"LID_"]))]) + } + + /// The laptop's DSDT with [`queries`] in its controller, and the lid. + pub fn laptop_with_queries() -> Vec { + cat(&[&device(&[b"_SB_", b"LID_"], &dword(b"_HID", 0x0D0C_D041)), &laptop(&queries())]) + } + + /// Query 0x28's method runs, writes its word to the POST port through the + /// kernel and notifies the button, which the host counts as a press; 0x2A's + /// notifies the lid, which is none; and a query the controller defines no + /// method for runs nothing. + #[test] + fn query_0x28_on_an_amd_laptop_is_a_press_of_its_power_button() { + let mut kernel = crafted(&sealed(b"DSDT", &laptop_with_queries()), &[]); + 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 ec = found.ec.expect("the controller"); + assert_eq!(ec.queries, BTreeSet::from([0x28, 0x2A])); + host.buttons = found.buttons; + assert_eq!(query(&mut interpreter, &mut host, &ec, 0x2A), Queried::Ran); + assert_eq!(host.presses, 0, "the lid's Notify is no press"); + assert_eq!(query(&mut interpreter, &mut host, &ec, 0x28), Queried::Ran); + assert_eq!(host.presses, 1); + let wrote = toyos_abi::acpi::Access::write(toyos_abi::acpi::Space::SystemIo, 0x80, toyos_abi::acpi::Width::Word, 0x28); + assert_eq!(kernel.asked.borrow().iter().filter(|access| access.write == 1).last(), Some(&wrote)); + assert_eq!(query(&mut interpreter, &mut host, &ec, 0x29), Queried::Absent); + assert_eq!(host.presses, 1); + } + + /// A port the kernel refuses ends the method before its Notify: no press, + /// and the refusal by its kind. + #[test] + fn a_query_whose_write_the_kernel_refuses_is_no_press() { + 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); + 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())); + assert_eq!(host.presses, 0); + } + + #[test] + fn an_eisa_id_is_its_three_letters_and_four_digits() { + assert_eq!(id(&Value::Integer(u64::from(PNP0C09))).as_deref(), Some("PNP0C09")); + assert_eq!(id(&Value::Integer(u64::from(PNP0C0C))).as_deref(), Some("PNP0C0C")); + assert_eq!(id(&Value::String(b"PNP0C09".to_vec())).as_deref(), Some("PNP0C09")); + assert_eq!(id(&Value::Integer(1 << 32)), None); + assert_eq!(id(&Value::Buffer(b"PNP0C09".to_vec())), None); + } + + /// Absent devices are not served, and a controller whose resources or + /// GPE are none this server serves is none, by name. + #[test] + fn a_controller_this_server_cannot_serve_is_none() { + let absent = cat(&[&device(&[b"_SB_", b"EC__"], &cat(&[&dword(b"_HID", PNP0C09), &byte(b"_STA", 0x0E), &crs(&[0x62, 0x66]), &byte(b"_GPE", 3)]))]); + assert_eq!(found(&absent, true), Found::default()); + let ec = |crs_ports: &[u16], gpe: u8| cat(&[&device(&[b"_SB_", b"EC__"], &cat(&[&dword(b"_HID", PNP0C09), &crs(crs_ports), &byte(b"_GPE", gpe)]))]); + assert_eq!(found(&ec(&[0x62, 0x66], 32), true).ec, None, "GPE 32 is past an eight-byte GPE0"); + assert_eq!(found(&ec(&[0x62, 0x66], 31), true).ec.map(|ec| ec.gpe), Some(31)); + assert_eq!(found(&ec(&[0x62], 3), true).ec, None, "one port"); + assert_eq!(found(&ec(&[0x62, 0x66, 0x68], 3), true).ec, None, "three ports"); + let two = cat(&[&ec(&[0x62, 0x66], 3), &device(&[b"_SB_", b"EC2_"], &cat(&[&dword(b"_HID", PNP0C09), &crs(&[0x68, 0x6C]), &byte(b"_GPE", 4)]))]); + assert_eq!(found(&two, true).ec, None, "two controllers present"); + let no_button = cat(&[&device(&[b"_SB_", b"PWRB"], &cat(&[&dword(b"_HID", PNP0C0C), &byte(b"_STA", 0)]))]); + assert_eq!(found(&no_button, true).buttons, Vec::::new(), "a button whose _STA says absent"); + } +} diff --git a/userland/acpiserver/src/host.rs b/userland/acpiserver/src/host.rs index a76ddab746a..27fd328282e 100644 --- a/userland/acpiserver/src/host.rs +++ b/userland/acpiserver/src/host.rs @@ -3,18 +3,24 @@ //! mediated access ([`toyos_abi::acpi`]), and [`Kernel`] is that access as //! this server asks for it, so a host test answers in the kernel's place. //! -//! **Nothing is written.** Loading a machine's tables writes nothing, and -//! this server evaluates nothing that does yet: a write in any space is -//! denied by name, and so is every access to the embedded controller's -//! space, which no transaction serves for AML yet. SystemCMOS never arrives: -//! the interpreter refuses that space itself. A read of memory, of a port or -//! of a function's configuration space is the kernel's to make or refuse, and -//! a refusal it names is denied under that name. +//! **A port is the only thing written.** A read of memory, of a port or of a +//! function's configuration space, and a write to a port, is the kernel's to +//! make or refuse, and a refusal it names is denied under that name: the +//! kernel keeps every port it drives, and makes a byte for `SMI_CMD` a call +//! into the firmware or refuses it. A write to memory or to configuration +//! space is denied by name, and so is every access to the embedded +//! controller's space, which no transaction serves for AML yet. SystemCMOS +//! never arrives: the interpreter refuses that space itself. //! //! **The Global Lock is the kernel's to exchange** (ACPI 6.5 §5.2.10.1). A //! take that finds the firmware holding it is denied by name ([`HELD`]) and //! counted: this server waits for no release of the firmware's yet. //! +//! **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. +//! //! **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. //! @@ -156,6 +162,10 @@ pub struct Firmware<'k, K> { pub notifies: u64, pub refused: Ledger, notified: Ledger, + /// The paths of the control-method power buttons this server serves. + pub buttons: Vec, + /// Presses of one of them notified and not yet served. + pub presses: u64, /// The kernel answered that the machine is stopping. pub stopping: bool, } @@ -172,6 +182,8 @@ impl<'k, K: Kernel> Firmware<'k, K> { notifies: 0, refused: Ledger::default(), notified: Ledger::default(), + buttons: Vec::new(), + presses: 0, stopping: false, } } @@ -226,9 +238,18 @@ impl Host for Firmware<'_, K> { fn write(&mut self, at: Address, width: toyos_aml::Access, value: u64) -> Result<(), Denied> { let what = match at { - Address::Memory(_) => "a write to SystemMemory: this server writes nothing for AML yet".into(), - Address::Io(_) => "a write to SystemIO: this server writes nothing for AML yet".into(), - Address::PciConfig { .. } => "a write to PCI_Config: this server writes nothing for AML yet".into(), + Address::Io(port) => { + 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?}"), + )), + }; + } + Address::Memory(_) => "a write to SystemMemory: this server writes no memory for AML".into(), + Address::PciConfig { .. } => "a write to PCI_Config: this server writes no configuration space for AML".into(), Address::EmbeddedControl(_) => format!("a write to {NO_CONTROLLER}"), }; Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) @@ -253,8 +274,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 { + self.presses += 1; + } if self.notified.see(&format!("{object} {value:#x}")) { - println!("{OWN}Notify({object}, {value:#x}) for the first time; nothing serves a Notify yet"); + 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}"); } } @@ -322,7 +349,7 @@ pub mod tests { fn access(&self, access: Access) -> Result { self.stopping()?; self.asked.borrow_mut().push(access); - assert_eq!(access.write, 0, "this server asks the kernel for no write"); + assert!(access.write == 0 || access.space == Space::SystemIo as u8, "this server asks the kernel for no write but a port's"); 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") { @@ -439,22 +466,56 @@ pub mod tests { assert_eq!(host.pages.by_type(), "none"); } + /// A port is written as the kernel answers, its refusal under its name; + /// memory and configuration space are never written, and the + /// controller's space is not reached, none of them asked of the kernel. #[test] - fn nothing_is_written_and_the_controllers_space_is_not_reached() { + fn only_a_port_is_written_and_the_controllers_space_is_not_reached() { let kernel = machine(); let mut host = Firmware::new(&kernel); + // An AMD laptop's `_Q28` writes its query's number to the POST port as a word. + assert_eq!(host.write(Address::Io(0xB2), toyos_aml::Access::Word, 0x28), Ok(())); + let refused = host.write(Address::Io(0x70), toyos_aml::Access::Byte, 1); + assert_eq!(refused, Err(Denied("a SystemIO write the kernel refused KernelPort".into()))); + assert_eq!( + *kernel.asked.borrow(), + [ + Access::write(Space::SystemIo, 0xB2, Width::Word, 0x28), + Access::write(Space::SystemIo, 0x70, Width::Byte, 1), + ] + ); let function = Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 3, offset: 0x40 }; - for (at, space) in [(Address::Memory(NVS), "SystemMemory"), (Address::Io(0xB2), "SystemIO"), (function, "PCI_Config")] { - let denied = host.write(at, toyos_aml::Access::Byte, 0).expect_err("a write was made"); - assert_eq!(denied.0, format!("a write to {space}: this server writes nothing for AML yet")); + 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, toyos_aml::Access::Byte, 0), Err(Denied(denied.into()))); } let controller = Address::EmbeddedControl(0x38); assert_eq!(host.write(controller, toyos_aml::Access::Byte, 1), Err(Denied(format!("a write to {NO_CONTROLLER}")))); assert_eq!(host.read(controller, toyos_aml::Access::Byte), Err(Denied(format!("a read of {NO_CONTROLLER}")))); - assert!(kernel.asked.borrow().is_empty(), "the kernel was asked for an access this server denies itself"); + assert_eq!(kernel.asked.borrow().len(), 2, "the kernel was asked for an access this server denies itself"); assert_eq!(host.reads, [0, 0, 0]); } + /// A Notify of 0x80 to a button the server serves is a press, and to + /// anything else, or of another value, is none. + #[test] + fn only_a_notify_of_a_press_to_a_served_button_is_a_press() { + let kernel = machine(); + let mut host = Firmware::new(&kernel); + host.notify("\\_SB_.PWRB", 0x80); + assert_eq!(host.presses, 0, "a press of a button the server does not serve yet"); + host.buttons.push("\\_SB_.PWRB".into()); + for (object, value) in [("\\_SB_.PWRB", 0x02), ("\\_SB_.SLPB", 0x80), ("\\_SB_.PWRB_", 0x80), ("\\_SB_.LID_", 0x80)] { + host.notify(object, value); + } + assert_eq!(host.presses, 0); + host.notify("\\_SB_.PWRB", 0x80); + host.notify("\\_SB_.PWRB", 0x80); + assert_eq!((host.presses, host.notifies), (2, 7)); + } + #[test] fn the_lock_is_taken_and_given_back_through_the_kernel() { let kernel = machine(); diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index 958f777578c..605de3fc575 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -8,10 +8,9 @@ //! again. //! //! **At start** every fixed and general-purpose event is disabled and its -//! status cleared, then the power button and the embedded controller's GPE -//! are enabled, the controller's backlog is drained, and the SCI -//! acknowledged: a press after the clear latches and is served, one -//! before it is lost. +//! status cleared, then the fixed power button is enabled where the FADT +//! says the machine has one, and the SCI acknowledged: a press after the +//! clear latches and is served, one before it is lost. //! //! **Then the machine's tables are loaded** ([`aml::load`]), through the //! kernel's mediated access ([`Claim`]): after the arming, so a press during @@ -20,11 +19,20 @@ //! way. The load hands the kernel `\_S5`'s sleep type, without which the //! kernel refuses every power-off. //! +//! **Then the embedded controller is found in the namespace and armed** +//! ([`devices::find`]): 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`]). 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 each query is run as its +//! method ([`devices::query`]): a Notify of a press is a press. Elsewhere no +//! query's method runs. +//! //! **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 -//! controller of every query waiting, which are then run, one by one, as -//! [`aml::query`] says, after the drain that took them. An event this server +//! controller of every query waiting, which are then run, one by one, after +//! the drain that took them. An event this server //! never enabled, a controller that does not answer, more queries in one //! drain than [`QUERIES`], and [`sci::EMPTY_SCIS`] SCIs in a row that carried //! nothing are each a panic naming the registers. @@ -33,6 +41,7 @@ //! of every one at [`COUNTS`] intervals, never a line per event. mod aml; +mod devices; mod ec; mod host; mod ledger; @@ -47,9 +56,12 @@ use toyos::ioport::{in16, in8, out16, out8}; use toyos::poller::{Poller, READABLE}; use toyos::power::{self, Stop}; use toyos::AcpiDev; -use toyos_abi::acpi::{Access, AcpiInfo, Block, FIXED_POWER_BUTTON}; +use toyos_abi::acpi::{Access, AcpiInfo, Block, Space, Width, FIXED_POWER_BUTTON}; use toyos_abi::syscall::{DeviceType, SyscallError}; +use acpiserver_api::{CONTROLLER_NONE, CONTROLLER_SERVED}; +use aml::Aml; +use devices::{Ec, Queried}; use ec::{Do, Transaction, Wait}; use host::{Answer, Kernel, Stopping, Take}; use sci::{Event, Served, Unserved, Unstopped, PM1_STATUS, PWRBTN}; @@ -62,10 +74,14 @@ const QUERIES: usize = 32; /// How often the log is told the queries' counts, where they moved. const COUNTS: Duration = Duration::from_secs(30); -struct Server { - dev: AcpiDev, +struct Server<'a> { + dev: &'a AcpiDev, info: AcpiInfo, served: Served, + /// The controller the namespace names, if it names one this server serves. + ec: Option, + /// The namespace the load kept. + aml: Option>>, /// Taken off the controller by the drain, run after it. queued: VecDeque, /// Every query number taken, and how often. @@ -84,14 +100,14 @@ fn main() { return; }; let info = dev.describe().expect("acpiserver: the claim's first read is its description"); - let served = Served { - power_button: info.flags & FIXED_POWER_BUTTON != 0, - ec_gpe: info.has_ec().then_some(info.ec_gpe), - }; + let served = Served { power_button: info.flags & FIXED_POWER_BUTTON != 0, ec_gpe: None }; + let claim = Claim(&dev); let mut server = Server { - dev, + dev: &dev, info, served, + ec: None, + aml: None, queued: VecDeque::new(), counts: BTreeMap::new(), counted: 0, @@ -101,10 +117,12 @@ fn main() { power_off: false, }; server.arm(); - server.power_off = aml::load(&Claim(&server.dev), server.info.rsdp).handed; + let (loaded, kept) = aml::load(&claim, server.info.rsdp); + server.power_off = loaded.handed; + server.aml = kept; + server.arm_controller(); server.serve(); } - /// The claim as the tables' fetch and their AML ask it for what lies outside /// its own ports. struct Claim<'a>(&'a AcpiDev); @@ -146,12 +164,28 @@ impl Kernel for Claim<'_> { } } +impl Claim<'_> { + /// A byte of one of the controller's ports, which no row holds: `None` + /// once the machine is stopping, and a refusal is the kernel's keeping a + /// port [`Server::arm_controller`] found it would reach. + fn port_in(&self, port: u16) -> Option { + let answer = self.access(Access::read(Space::SystemIo, u64::from(port), Width::Byte)).ok()?; + Some(answer.made.unwrap_or_else(|refused| panic!("acpiserver: the kernel refused a read of the embedded controller's port {port:#x}: {refused:?}")) as u8) + } + + fn port_out(&self, port: u16, byte: u8) -> Option<()> { + let answer = self.access(Access::write(Space::SystemIo, u64::from(port), Width::Byte, u64::from(byte))).ok()?; + answer.made.unwrap_or_else(|refused| panic!("acpiserver: the kernel refused a write of the embedded controller's port {port:#x}: {refused:?}")); + Some(()) + } +} + /// Each byte of a status-and-enable block: its status port and its enable port. fn bytes(block: Block) -> impl Iterator { (0..block.len / 2).map(move |i| (block.port + i, block.enable() + i)) } -impl Server { +impl Server<'_> { fn arm(&mut self) { let pm1 = self.info.pm1_event; out16(pm1.enable(), 0); @@ -163,25 +197,49 @@ impl Server { if self.served.power_button { out16(pm1.enable(), PWRBTN); } - if let Some(n) = self.served.ec_gpe { - let (_, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the kernel bounded every GPE by the block"); - out8(enable, in8(enable) | 1 << (n % 8)); - } - if self.info.has_ec() { - self.drain(); - self.run_queued(); - } self.dev.ack().expect("acpiserver: the claim's acknowledgement"); println!( - "acpiserver: armed: power button {}, embedded controller {}", - if self.served.power_button { "served" } else { "not the fixed one, so not served" }, - match self.served.ec_gpe { - Some(gpe) => format!("on GPE {gpe:#x} at {:#x}/{:#x}", self.info.ec_command.port, self.info.ec_data.port), - None => "none".into(), - }, + "acpiserver: armed: power button {}", + if self.served.power_button { "served" } else { "a control method device, served once the namespace names it" }, ); } + /// Find the controller, and the buttons where they are control method + /// devices, in the namespace the load kept; enable the controller's GPE + /// and drain its backlog. + fn arm_controller(&mut self) { + let Some(aml) = &mut self.aml else { + if !self.served.power_button { + println!("acpiserver: no control-method power button served: no namespace was kept"); + } + return println!("{CONTROLLER_NONE}no namespace was kept"); + }; + let found = devices::find(&mut aml.interpreter, &mut aml.host, 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 Some(ec) = found.ec else { return }; + let claim = Claim(self.dev); + // Its status register, which a read leaves as it was; the data + // register's read takes a byte, and a refusal of it is the panic of + // `Claim::port_in`. + match claim.access(Access::read(Space::SystemIo, u64::from(ec.command), Width::Byte)) { + Err(Stopping) => return, + Ok(Answer { made: Err(refused), .. }) => return println!("{CONTROLLER_NONE}the kernel keeps its status port from this server, {refused:?}"), + Ok(Answer { made: Ok(_), .. }) => {} + } + let n = ec.gpe; + let (status, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); + out8(status, 1 << (n % 8)); + out8(enable, in8(enable) | 1 << (n % 8)); + println!("{CONTROLLER_SERVED}{n:#x} at {:#x}/{:#x}", ec.command, ec.data); + self.served.ec_gpe = Some(n); + self.ec = Some(ec); + self.drain(); + self.run_queued(); + } + fn serve(&mut self) -> ! { let poller = Poller::new(1); let mut next_count = Instant::now() + COUNTS; @@ -190,7 +248,7 @@ impl Server { let mut watching = false; loop { if !watching { - poller.watch(&self.dev, READABLE, 0); + poller.watch(self.dev, READABLE, 0); watching = true; } let wait = next_count.saturating_duration_since(Instant::now()); @@ -240,8 +298,8 @@ impl Server { Event::Ec => { // The controller's GPE is an edge: cleared before the drain, // so an event the drain does not see raises it again. - let n = self.info.ec_gpe; - let (status, _) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the kernel bounded every GPE by the block"); + let n = self.served.ec_gpe.expect("an event of the controller's GPE is one this server enabled"); + let (status, _) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); out8(status, 1 << (n % 8)); self.drain(); } @@ -263,63 +321,48 @@ impl Server { /// Take every query the controller has waiting off it, queued for after. fn drain(&mut self) { + let Some(ec) = &self.ec else { return }; + let (claim, command, data) = (Claim(self.dev), ec.command, ec.data); let mut taken = 0; - while in8(self.info.ec_command.port) & ec::SCI_EVT != 0 { - let q = self.transact(Transaction::query()); + while let Some(status) = claim.port_in(command) + && status & ec::SCI_EVT != 0 + { + let Some(q) = transact(&claim, command, data, Transaction::query()) else { return }; if q == 0 { break; } taken += 1; - assert!( - taken <= QUERIES, - "acpiserver: the embedded controller had more than {QUERIES} queries waiting at once (EC_SC {:#04x})", - in8(self.info.ec_command.port) - ); + assert!(taken <= QUERIES, "acpiserver: the embedded controller had more than {QUERIES} queries waiting at once (EC_SC {status:#04x})"); self.queued.push_back(q); } } + /// Each query the drain took, run as its method where the buttons are + /// control method devices, and counted. fn run_queued(&mut self) { while let Some(q) = self.queued.pop_front() { - aml::query(q); + let served = match (&mut self.aml, &self.ec) { + (Some(aml), Some(ec)) if !aml.host.buttons.is_empty() => { + let queried = devices::query(&mut aml.interpreter, &mut aml.host, ec, q); + let presses = std::mem::take(&mut aml.host.presses); + Some((queried, presses)) + } + _ => None, + }; let count = self.counts.entry(q).or_insert(0); *count += 1; self.counted += 1; if *count == 1 { - println!("acpiserver: embedded controller query {q:#04x} taken for the first time, served by nothing: no query's method is evaluated yet"); + let said = match &served { + None => "served by nothing: no query's method runs on a machine whose power button is the fixed one".into(), + Some((Queried::Ran, _)) => "its method ran".into(), + Some((Queried::Absent, _)) => "the controller defines no method for it".into(), + Some((Queried::Refused(why), _)) => format!("its method did not finish: {why}"), + }; + println!("acpiserver: embedded controller query {q:#04x} taken for the first time; {said}"); } - } - } - - /// One transaction, waiting on the controller at each step for at most - /// [`EC_STEP`]. - fn transact(&self, mut tx: Transaction) -> u8 { - let (command, data) = (self.info.ec_command.port, self.info.ec_data.port); - let mut waiting: Option<(Wait, Instant)> = None; - loop { - let status = in8(command); - match tx.step(status) { - Do::Wait(wait) => { - let since = match waiting { - Some((was, since)) if was == wait => since, - _ => Instant::now(), - }; - assert!( - since.elapsed() < EC_STEP, - "acpiserver: the embedded controller kept {wait:?} unmet for {EC_STEP:?} (EC_SC {status:#04x})" - ); - waiting = Some((wait, since)); - std::thread::yield_now(); - } - Do::WriteCommand(byte) => { - waiting = None; - out8(command, byte); - } - Do::ReadData => { - waiting = None; - tx.read(in8(data)); - } - Do::Done(byte) => return byte, + if served.is_some_and(|(_, presses)| presses != 0) { + self.press(); } } } @@ -333,3 +376,32 @@ impl Server { println!("acpiserver: {} SCIs; {}{}", self.scis, acpiserver_api::QUERIES_COUNTED, counts.join(", ")); } } + +/// One transaction on the controller at `command` and `data`, waiting on it at +/// each step for at most [`EC_STEP`]; `None` once the machine is stopping. +fn transact(claim: &Claim, command: u16, data: u16, mut tx: Transaction) -> Option { + let mut waiting: Option<(Wait, Instant)> = None; + loop { + let status = claim.port_in(command)?; + match tx.step(status) { + Do::Wait(wait) => { + let since = match waiting { + Some((was, since)) if was == wait => since, + _ => Instant::now(), + }; + assert!(since.elapsed() < EC_STEP, "acpiserver: the embedded controller kept {wait:?} unmet for {EC_STEP:?} (EC_SC {status:#04x})"); + waiting = Some((wait, since)); + std::thread::yield_now(); + } + Do::WriteCommand(byte) => { + waiting = None; + claim.port_out(command, byte)?; + } + Do::ReadData => { + waiting = None; + tx.read(claim.port_in(data)?); + } + Do::Done(byte) => return Some(byte), + } + } +} From 4259cf8148d1305699c4307378881b15434e977a Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 15:43:24 +0200 Subject: [PATCH 2/6] The track says what the laptop's queries wrote exactly: no port but 0x80 Of 204 out-of-tree evaluations of its 102 query methods, 142 wrote nothing; none wrote a port but 0x80. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md | 6 +++--- 1 file changed, 3 insertions(+), 3 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 593a1c74028..36d77995b98 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 @@ -332,9 +332,9 @@ kernel a write to a port and to nothing else (`userland/acpiserver/src/host.rs`), and the AML that writes after the load is a controller query's method, run only on a machine whose power button is a control method device; the T14's button is the fixed one. On an AMD -laptop's tables, outside the tree, every query method the controller -defines wrote port 0x80 and no other port, with each controller byte read -as 0 and again as 0xFF: no call. +laptop's tables, outside the tree, no query method the controller defines +wrote a port but 0x80, with each controller byte read as 0 and again as +0xFF: no call. - **What a call does there is the firmware's**, and the kernel bounds who, when, where, which byte and how often: From 3d26c03ec59c8f3ae80eb99b671e45591e5db8c0 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 16:18:56 +0200 Subject: [PATCH 3/6] AML's port writes reach the kernel only for the three registers a machine's AML was measured to write, and a controller port the kernel refuses ends the controller by name Review of #845, round 1. Measured out of the tree (the server's own devices.rs on an AMD laptop's tables, nothing cleared, six read arms): its load writes the CMOS upper bank's index 0x72 and the FCH PM index 0xCD6, a byte each, and its DSDT is refused without either; its find writes nothing; its 102 queries write only the POST port's word. The host forwards exactly those three (port, width) pairs and denies every other port write by name without asking the kernel, so no reset-capable port (0xCF9, 0x92), SMI_CMD byte or data register is reached from AML. The first forwarded write to each is said once. The per-query decision is run_query, host-tested under both kinds of power button: on a fixed-button machine no query's method runs and nothing is written. The q35 no-PNP0C09 reading moves from the guest's acpi_power_button to the host test that loads QEMU's own DSDT. A controller port the kernel refuses no longer panics the server: the probe's refusal and one met later each say CONTROLLER_NONE and why, and a later one disables the controller's GPE. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- tests/common/power.rs | 2 - userland/acpiserver/src/aml.rs | 11 ++- userland/acpiserver/src/host.rs | 80 ++++++++++++----- userland/acpiserver/src/main.rs | 150 +++++++++++++++++++++++--------- 4 files changed, 175 insertions(+), 68 deletions(-) diff --git a/tests/common/power.rs b/tests/common/power.rs index 7701db0d170..07baa051bf8 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -757,8 +757,6 @@ pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { let whole = serial::Serial::named("the boot and the press", console); acpi_tables_loaded(&whole, &whole, Q35_S5_SUPPLIED)?; whole.must_say_after(ACPI_S5_HANDED, ACPI_PRESSED)?; - // QEMU's DSDT names no embedded controller, which the server finds there. - whole.must_say_after(&format!("{}no PNP0C09 device is present", acpiserver_api::CONTROLLER_NONE), ACPI_PRESSED)?; eprintln!(" [power] the press: {ACPI_PRESSED}"); Ok(()) } diff --git a/userland/acpiserver/src/aml.rs b/userland/acpiserver/src/aml.rs index d9555b95d44..a75b1b00f61 100644 --- a/userland/acpiserver/src/aml.rs +++ b/userland/acpiserver/src/aml.rs @@ -273,11 +273,18 @@ 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. + /// 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. #[test] fn qemus_tables_load_and_s5_is_what_its_kernel_decoded() { let kernel = machine(QEMU); - assert_eq!(load_only(&kernel, RSDP), Loaded { blocks: vec![Ok(())], s5: Some((0, 0)), handed: true }); + 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"); + 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!(kernel.asked.borrow().iter().all(|access| access.write == 0 && access.space == 0)); assert!(!kernel.held.get()); assert_eq!(kernel.handed.get(), Some(0)); diff --git a/userland/acpiserver/src/host.rs b/userland/acpiserver/src/host.rs index 27fd328282e..d9a89e1dcf4 100644 --- a/userland/acpiserver/src/host.rs +++ b/userland/acpiserver/src/host.rs @@ -3,14 +3,19 @@ //! mediated access ([`toyos_abi::acpi`]), and [`Kernel`] is that access as //! this server asks for it, so a host test answers in the kernel's place. //! -//! **A port is the only thing written.** A read of memory, of a port or of a -//! function's configuration space, and a write to a port, is the kernel's to -//! make or refuse, and a refusal it names is denied under that name: the -//! kernel keeps every port it drives, and makes a byte for `SMI_CMD` a call -//! into the firmware or refuses it. A write to memory or to configuration -//! space is denied by name, and so is every access to the embedded -//! controller's space, which no transaction serves for AML yet. SystemCMOS -//! never arrives: the interpreter refuses that space itself. +//! **A read of memory, of a port or of a function's configuration space is +//! the kernel's to make or refuse**, and a refusal it names is denied under +//! that name. +//! +//! **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, and so is +//! every access to the embedded controller's space, which no transaction +//! serves for AML yet. SystemCMOS never arrives: the interpreter refuses that +//! space itself. //! //! **The Global Lock is the kernel's to exchange** (ACPI 6.5 §5.2.10.1). A //! take that finds the firmware holding it is denied by name ([`HELD`]) and @@ -46,6 +51,17 @@ pub const OWN: &str = "acpiserver: (this machine's own, quoted in no record) "; /// The denial of a take that found the firmware holding the Global Lock. pub const HELD: &str = "the Global Lock: the firmware holds it, and this server waits for no release yet"; +/// The 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; @@ -162,6 +178,8 @@ pub struct Firmware<'k, K> { pub notifies: u64, pub refused: Ledger, notified: Ledger, + /// The [`FORWARDED`] writes asked of the kernel, by port and width. + written: Ledger, /// The paths of the control-method power buttons this server serves. pub buttons: Vec, /// Presses of one of them notified and not yet served. @@ -182,6 +200,7 @@ impl<'k, K: Kernel> Firmware<'k, K> { notifies: 0, refused: Ledger::default(), notified: Ledger::default(), + written: Ledger::default(), buttons: Vec::new(), presses: 0, stopping: false, @@ -238,7 +257,11 @@ 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 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(()), @@ -466,36 +489,49 @@ pub mod tests { assert_eq!(host.pages.by_type(), "none"); } - /// A port is written as the kernel answers, its refusal under its name; - /// memory and configuration space are never written, and the - /// controller's space is not reached, none of them asked of the kernel. + /// 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 the controller's space is + /// not reached, none of them asked of the kernel. #[test] - fn only_a_port_is_written_and_the_controllers_space_is_not_reached() { - let kernel = machine(); + fn only_a_forwarded_port_write_is_asked_of_the_kernel() { + use toyos_aml::Access::{Byte, DWord, Word}; + let mut kernel = machine(); + kernel.ports = vec![(0x72, 0), (0x80, 0)]; let mut host = Firmware::new(&kernel); - // An AMD laptop's `_Q28` writes its query's number to the POST port as a word. - assert_eq!(host.write(Address::Io(0xB2), toyos_aml::Access::Word, 0x28), Ok(())); - let refused = host.write(Address::Io(0x70), toyos_aml::Access::Byte, 1); + assert_eq!(host.write(Address::Io(0x80), Word, 0x28), Ok(())); + assert_eq!(host.write(Address::Io(0x72), Byte, 0x08), Ok(())); + let refused = host.write(Address::Io(0xCD6), Byte, 0x60); assert_eq!(refused, Err(Denied("a SystemIO write the kernel refused KernelPort".into()))); assert_eq!( *kernel.asked.borrow(), [ - Access::write(Space::SystemIo, 0xB2, Width::Word, 0x28), - Access::write(Space::SystemIo, 0x70, Width::Byte, 1), + Access::write(Space::SystemIo, 0x80, Width::Word, 0x28), + Access::write(Space::SystemIo, 0x72, Width::Byte, 0x08), + 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)] + { + 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, toyos_aml::Access::Byte, 0), Err(Denied(denied.into()))); + assert_eq!(host.write(at, Byte, 0), Err(Denied(denied.into()))); } let controller = Address::EmbeddedControl(0x38); - assert_eq!(host.write(controller, toyos_aml::Access::Byte, 1), Err(Denied(format!("a write to {NO_CONTROLLER}")))); - assert_eq!(host.read(controller, toyos_aml::Access::Byte), Err(Denied(format!("a read of {NO_CONTROLLER}")))); - assert_eq!(kernel.asked.borrow().len(), 2, "the kernel was asked for an access this server denies itself"); + 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!(host.reads, [0, 0, 0]); + assert_eq!(host.written.counts(), "0x72 Byte x1; 0x80 Word x1; 0xcd6 Byte x1"); } /// A Notify of 0x80 to a button the server serves is a press, and to diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index 605de3fc575..698f4a2384b 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -22,11 +22,12 @@ //! **Then the embedded controller is found in the namespace and armed** //! ([`devices::find`]): 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`]). A machine whose DSDT did not load has no controller served. +//! ([`Claim`]), and one it refuses ends the controller, said by name. 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 each query is run as its -//! method ([`devices::query`]): a Notify of a press is a press. Elsewhere no -//! query's method runs. +//! buttons the namespace names are found too, and where one is, each query +//! is run as its method ([`run_query`]): a Notify of a press is a press. +//! Elsewhere no query's method runs. //! //! **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 @@ -164,19 +165,23 @@ impl Kernel for Claim<'_> { } } +/// Why an access to one of the controller's ports was not made. +enum Unmade { + Stopping, + /// The kernel keeps the port from this server, said as it refused it. + Refused(String), +} + impl Claim<'_> { - /// A byte of one of the controller's ports, which no row holds: `None` - /// once the machine is stopping, and a refusal is the kernel's keeping a - /// port [`Server::arm_controller`] found it would reach. - fn port_in(&self, port: u16) -> Option { - let answer = self.access(Access::read(Space::SystemIo, u64::from(port), Width::Byte)).ok()?; - Some(answer.made.unwrap_or_else(|refused| panic!("acpiserver: the kernel refused a read of the embedded controller's port {port:#x}: {refused:?}")) as u8) + /// A byte of one of the controller's ports, which no row holds. + fn port_in(&self, port: u16) -> Result { + let answer = self.access(Access::read(Space::SystemIo, u64::from(port), Width::Byte)).map_err(|Stopping| Unmade::Stopping)?; + answer.made.map(|byte| byte as u8).map_err(|refused| Unmade::Refused(format!("the kernel refused a read of its port {port:#x}, {refused:?}"))) } - fn port_out(&self, port: u16, byte: u8) -> Option<()> { - let answer = self.access(Access::write(Space::SystemIo, u64::from(port), Width::Byte, u64::from(byte))).ok()?; - answer.made.unwrap_or_else(|refused| panic!("acpiserver: the kernel refused a write of the embedded controller's port {port:#x}: {refused:?}")); - Some(()) + fn port_out(&self, port: u16, byte: u8) -> Result<(), Unmade> { + let answer = self.access(Access::write(Space::SystemIo, u64::from(port), Width::Byte, u64::from(byte))).map_err(|Stopping| Unmade::Stopping)?; + answer.made.map(drop).map_err(|refused| Unmade::Refused(format!("the kernel refused a write of its port {port:#x}, {refused:?}"))) } } @@ -220,14 +225,13 @@ impl Server<'_> { } aml.host.buttons = found.buttons; let Some(ec) = found.ec else { return }; - let claim = Claim(self.dev); // Its status register, which a read leaves as it was; the data - // register's read takes a byte, and a refusal of it is the panic of - // `Claim::port_in`. - match claim.access(Access::read(Space::SystemIo, u64::from(ec.command), Width::Byte)) { - Err(Stopping) => return, - Ok(Answer { made: Err(refused), .. }) => return println!("{CONTROLLER_NONE}the kernel keeps its status port from this server, {refused:?}"), - Ok(Answer { made: Ok(_), .. }) => {} + // register's read takes a byte, and a port refused later ends the + // controller then ([`Server::unserve`]). + match Claim(self.dev).port_in(ec.command) { + Err(Unmade::Stopping) => return, + Err(Unmade::Refused(why)) => return println!("{CONTROLLER_NONE}{why}"), + Ok(_) => {} } let n = ec.gpe; let (status, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); @@ -319,17 +323,27 @@ impl Server<'_> { } } - /// Take every query the controller has waiting off it, queued for after. + /// Take every query the controller has waiting off it, queued for after; + /// a port the kernel refuses ends the controller by name. fn drain(&mut self) { - let Some(ec) = &self.ec else { return }; + match self.take_queries() { + Ok(()) | Err(Unmade::Stopping) => {} + Err(Unmade::Refused(why)) => self.unserve(&why), + } + } + + fn take_queries(&mut self) -> Result<(), Unmade> { + let Some(ec) = &self.ec else { return Ok(()) }; let (claim, command, data) = (Claim(self.dev), ec.command, ec.data); let mut taken = 0; - while let Some(status) = claim.port_in(command) - && status & ec::SCI_EVT != 0 - { - let Some(q) = transact(&claim, command, data, Transaction::query()) else { return }; + loop { + let status = claim.port_in(command)?; + if status & ec::SCI_EVT == 0 { + return Ok(()); + } + let q = transact(&claim, command, data, Transaction::query())?; if q == 0 { - break; + return Ok(()); } taken += 1; assert!(taken <= QUERIES, "acpiserver: the embedded controller had more than {QUERIES} queries waiting at once (EC_SC {status:#04x})"); @@ -337,24 +351,28 @@ impl Server<'_> { } } - /// Each query the drain took, run as its method where the buttons are - /// control method devices, and counted. + /// Serve the controller no more: its GPE disabled, what it had waiting + /// dropped, and why said. + fn unserve(&mut self, why: &str) { + let n = self.served.ec_gpe.take().expect("a controller served has its GPE enabled"); + let (_, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); + out8(enable, in8(enable) & !(1 << (n % 8))); + self.ec = None; + self.queued.clear(); + println!("{CONTROLLER_NONE}{why}"); + } + + /// Each query the drain took, run as its method where a control-method + /// power button is served ([`run_query`]), and counted. fn run_queued(&mut self) { while let Some(q) = self.queued.pop_front() { - let served = match (&mut self.aml, &self.ec) { - (Some(aml), Some(ec)) if !aml.host.buttons.is_empty() => { - let queried = devices::query(&mut aml.interpreter, &mut aml.host, ec, q); - let presses = std::mem::take(&mut aml.host.presses); - Some((queried, presses)) - } - _ => None, - }; + let served = run_query(self.aml.as_mut(), self.ec.as_ref(), q); let count = self.counts.entry(q).or_insert(0); *count += 1; self.counted += 1; if *count == 1 { let said = match &served { - None => "served by nothing: no query's method runs on a machine whose power button is the fixed one".into(), + None => UNRUN.into(), Some((Queried::Ran, _)) => "its method ran".into(), Some((Queried::Absent, _)) => "the controller defines no method for it".into(), Some((Queried::Refused(why), _)) => format!("its method did not finish: {why}"), @@ -377,9 +395,25 @@ impl Server<'_> { } } +/// 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"; + +/// Query `q`'s method run, and the presses its Notifies made, where a +/// control-method power button is served; `None` elsewhere. +fn run_query(aml: Option<&mut Aml<'_, K>>, ec: Option<&Ec>, q: u8) -> Option<(Queried, u64)> { + match (aml, ec) { + (Some(aml), Some(ec)) if !aml.host.buttons.is_empty() => { + let queried = devices::query(&mut aml.interpreter, &mut aml.host, ec, q); + let presses = std::mem::take(&mut aml.host.presses); + Some((queried, presses)) + } + _ => None, + } +} + /// One transaction on the controller at `command` and `data`, waiting on it at -/// each step for at most [`EC_STEP`]; `None` once the machine is stopping. -fn transact(claim: &Claim, command: u16, data: u16, mut tx: Transaction) -> Option { +/// each step for at most [`EC_STEP`]. +fn transact(claim: &Claim, command: u16, data: u16, mut tx: Transaction) -> Result { let mut waiting: Option<(Wait, Instant)> = None; loop { let status = claim.port_in(command)?; @@ -401,7 +435,39 @@ fn transact(claim: &Claim, command: u16, data: u16, mut tx: Transaction) -> Opti waiting = None; tx.read(claim.port_in(data)?); } - Do::Done(byte) => return Some(byte), + Do::Done(byte) => return Ok(byte), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aml::load; + use crate::aml::tests::{crafted, sealed, CRAFTED_RSDP}; + use crate::devices::tests::{laptop_with_queries, GPE0}; + + /// An AMD laptop's controller and `_Q28` under both kinds of power + /// button: the method runs, writes the POST port and presses only where + /// the FADT says the button is a control method device; on a machine + /// whose button is the fixed one no query's method runs and nothing is + /// written. + #[test] + fn a_querys_method_runs_only_where_a_control_method_button_is_served() { + for control_method in [false, true] { + let mut kernel = crafted(&sealed(b"DSDT", &laptop_with_queries()), &[]); + 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); + aml.host.buttons = found.buttons; + let ec = found.ec.expect("the controller"); + let served = run_query(Some(&mut aml), Some(&ec), 0x28); + let wrote = kernel.asked.borrow().iter().filter(|access| access.write == 1).count(); + match control_method { + true => assert_eq!((served, wrote), (Some((Queried::Ran, 1)), 1)), + false => assert_eq!((served, wrote), (None, 0), "a query's method ran on a machine whose power button is the fixed one"), + } } } } From d0538cccdbdd0d7afdadea2206d91517b771b904 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 16:22:54 +0200 Subject: [PATCH 4/6] The FACS is read once at boot, the issues say what the server forwards and the dead short press, and the laptop extract records the ruling it is committed under power::init_reset reads the FACS the FADT names once and the boot hands that read to acpi_mode::init, which read it a second time for the Global Lock. The track's firmware-call paragraph said the AML that writes after the load is a query's method; the load and the device search evaluate AML too. It now names FORWARDED and what the out-of-tree measurement of an AMD laptop's load, search and 102 queries found, and records a write outside the three as a weakness with an exit. The dead short press of a control-method button the server does not serve is recorded beside the unruled power-off. The lines issue no longer lists counters_metal's controller literal, which reads acpiserver-api now. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...ines-are-spelled-again-by-their-readers.md | 11 ++--- ...ine-in-acpi-mode-and-interprets-its-aml.md | 44 ++++++++++++++----- kernel/src/arch/aarch64/boot.rs | 2 +- kernel/src/arch/x86_64/acpi_mode.rs | 7 +-- kernel/src/arch/x86_64/boot.rs | 10 +++-- kernel/src/arch/x86_64/power.rs | 23 ++++++---- kernel/src/main.rs | 2 +- toyos-acpi/tests/amd_laptop.rs | 5 +++ 8 files changed, 70 insertions(+), 34 deletions(-) diff --git a/issues/acpiserver-lines-are-spelled-again-by-their-readers.md b/issues/acpiserver-lines-are-spelled-again-by-their-readers.md index b3d5c81b609..99289474d93 100644 --- a/issues/acpiserver-lines-are-spelled-again-by-their-readers.md +++ b/issues/acpiserver-lines-are-spelled-again-by-their-readers.md @@ -6,9 +6,10 @@ opened: 2026-10-08 # `acpiserver`'s lines are spelled again by their readers -`userland/acpiserver-api` holds one of the server's lines, its count of the -embedded controller's queries, and the server, `acpi_hold` and the -`acpi_server_events` judge read it there. Every other line of the server's +`userland/acpiserver-api` holds three of the server's lines, its count of +the embedded controller's queries and its word on whether it serves a +controller, and the server, `acpi_hold`, `counters_metal` and the +`acpi_server_events` judge read them there. Every other line of the server's that a reader matches is still a second spelling, held to the server's by nothing: a change of wording in `userland/acpiserver/src` is found as a red guest test, or on the T14 as a wait that runs to its bound. @@ -18,8 +19,8 @@ guest test, or on the T14 as a wait that runs to its bound. - `tests/toyos.rs`, `acpi_events_on_metal` and `acpi_tables_on_metal`: the armed line, `taken for the first time`, `tables loaded in`, the tables' read counts, `refused`. -- `tests/toyos-rust-tests/src/bin/counters_metal.rs`, `acpi_said`: - `acpiserver: armed: `, `embedded controller none`, the first-query line. +- `tests/toyos-rust-tests/src/bin/counters_metal.rs`, `acpi_said`: the + first-query line. Owner: `userland/acpiserver`. 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 36d77995b98..a69f09e51db 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,10 +135,10 @@ the kernel's mediated access, since no row names them 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 query method makes of it (ACPI 6.5 §4.8.2.2.1.2), which the server runs for -every query only on such a machine; a button raised by a GPE's own `_Lxx` -or `_Exx` is not served, since the server enables no GPE but the -controller's. A machine its firmware hands over in ACPI mode is served -whatever it has, since nothing is written. +every query only on such a machine and only once it found the button; a +button raised by a GPE's own `_Lxx` or `_Exx` is not served, since the +server enables no GPE but the controller's. A machine its firmware hands +over in ACPI mode is served whatever it has, since the mint writes nothing. **Stage: the interpreter** (the orchestrator's placement of "The AML stage closes it"). ToyOS's own AML interpreter, written from the specification, run @@ -296,6 +296,17 @@ mediated access, leaves open: was to be "ruled with that button's device", and the owner has not ruled it. Owner: this stage. **Exit**: the owner rules it, and the server does what he rules. +- **A control-method button the server does not serve is dead in ACPI + mode.** The kernel puts the machine in ACPI mode when it mints the claim, + before the server has read whether it serves the button. Where it then + serves none, because no namespace was kept, no controller is served, no + present PNP0C0C was found or the button's `_Qxx` is refused, a short press + reaches nothing; in legacy mode the firmware served it, and before #845 + the kernel refused such a machine the claim and left it there. The server + says each of those by name. Owner: this stage. **Exit**: the owner rules + between that machine in ACPI mode with its press dead and the claim given + back so the firmware has the button again, and a host test reads the + server doing what he rules. Two more machines have no power-off, and that is the ruled state and no weakness with an exit. A boot whose config starts no server: "Power-off @@ -327,14 +338,23 @@ 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. The server's host passes the -kernel a write to a port and to nothing else -(`userland/acpiserver/src/host.rs`), and the AML that writes after the load -is a controller query's method, run only on a machine whose power button is -a control method device; the T14's button is the fixed one. On an AMD -laptop's tables, outside the tree, no query method the controller defines -wrote a port but 0x80, with each controller byte read as 0 and again as -0xFF: no call. +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 denies every other +write by name (`FORWARDED` in `userland/acpiserver/src/host.rs`), the +index registers 0x72 and 0xCD6, a byte each, and the POST port's word. +Those are what an AMD laptop's AML wrote, measured outside the tree on its +tables with nothing cleared between the load, the device search and all +102 of its query methods (#845): its DSDT writes both indexes at its load +and is refused without either, the search writes nothing, and the queries +write only the POST port. + +- **A port write outside the three ends what made it.** A machine whose + AML writes another port at its load has that table refused by name, and + a method that does has its evaluation refused. The T14's load writes + none: it loaded with every write denied. Owner: this stage. **Exit**: a + machine's reading of a write outside the three, which the list takes or + the owner refuses, and a slice that runs a method which stores to + `SMI_CMD` takes the call with the readings below. - **What a call does there is the firmware's**, and the kernel bounds who, when, where, which byte and how often: diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index afdceef985e..1dd7ac2c97e 100644 --- a/kernel/src/arch/aarch64/boot.rs +++ b/kernel/src/arch/aarch64/boot.rs @@ -365,7 +365,7 @@ pub fn timer() { /// The platform's own devices that are not PCI functions: none this kernel /// drives on an ACPI Arm machine. -pub fn platform_devices(_rsdp_addr: u64) {} +pub fn platform_devices(_platform: &Platform, _rsdp_addr: u64) {} /// Every other CPU, running. pub fn start_other_cpus(platform: &Platform, _args: &KernelArgs) { diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index e21922e73b8..94f710644e3 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -219,8 +219,8 @@ fn hardware() -> Option<&'static Hardware> { /// Decode the fixed hardware and fill the row; or say by name why this /// machine has none. After the i8042's row, whose lines the SCI may not -/// share. -pub fn init(rsdp_addr: u64) { +/// share; `facs` is what `power::init_reset` read of the FACS. +pub fn init(rsdp_addr: u64, facs: Option>) { let fadt = match toyos_acpi::find_table( crate::drivers::acpi::direct_phys(), rsdp_addr, @@ -265,7 +265,8 @@ pub fn init(rsdp_addr: u64) { if cpu::inw(control.port(0)) & SCI_EN != 0 { "ACPI" } else { "legacy" }, ); isa::fill(ROW, Function { name: "the ACPI fixed hardware", runs, irqs: vec![], wires: vec![sci] }); - let (ecam, lock) = (ecam(rsdp_addr), global_lock(toyos_acpi::facs(fadt.phys(), &fadt))); + let facs = facs.expect("power::init_reset read the FACS of the FADT this decoded"); + let (ecam, lock) = (ecam(rsdp_addr), global_lock(facs)); let hardware = Hardware { fixed, control, rsdp: rsdp_addr, ecam, lock }; let was = HARDWARE.swap(Box::into_raw(Box::new(hardware)), Ordering::Release); assert!(was.is_null(), "acpi: init ran twice"); diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 0fefc11be38..f1bf66c3a49 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -76,6 +76,8 @@ pub fn reserved() -> Region { /// What the boot learns bringing interrupts up and hands later steps. pub struct Platform { madt: MadtInfo, + /// The FACS as `power::init_reset` read it, for `acpi_mode::init`. + facs: Option>, } /// Interrupt delivery, this CPU's per-CPU block and the syscall gate. @@ -87,14 +89,14 @@ pub fn interrupts(rsdp_addr: u64) -> Platform { // Off the same tables as the MADT, and before the IDT below makes a panic // reportable: a panic that can be reported but not ended leaves the machine // holding its panel for a hand that may not be in the room. - super::power::init_reset(rsdp_addr); + let facs = super::power::init_reset(rsdp_addr); apic::init(); percpu::init_bsp(apic::id()); super::power::init_control(rsdp_addr); ioapic::init(&madt); idt::enable_interrupts(); super::syscall::init(); - Platform { madt } + Platform { madt, facs } } /// The clock: the TSC, calibrated against the HPET, and the CMOS wall clock. @@ -132,10 +134,10 @@ pub fn timer() { } /// The platform's own devices that are not PCI functions. -pub fn platform_devices(rsdp_addr: u64) { +pub fn platform_devices(platform: &Platform, rsdp_addr: u64) { super::i8042::init(rsdp_addr); super::pio::fill_i8042_row(); - super::acpi_mode::init(rsdp_addr); + super::acpi_mode::init(rsdp_addr, platform.facs); } /// Every other CPU, running. diff --git a/kernel/src/arch/x86_64/power.rs b/kernel/src/arch/x86_64/power.rs index 0900c3868d9..ccf1c79f903 100644 --- a/kernel/src/arch/x86_64/power.rs +++ b/kernel/src/arch/x86_64/power.rs @@ -18,7 +18,7 @@ use core::sync::atomic::{AtomicU8, Ordering}; -use toyos_acpi::Reset; +use toyos_acpi::{Facs, FacsRefused, Reset}; use toyos_userbound::firmware::SleepType; use toyos_userbound::{Mediated, Ports}; @@ -44,7 +44,8 @@ static RESET_VALUE: AtomicU8 = AtomicU8::new(0); /// Declare the FADT's two ports a write commands, `SMI_CMD` and the reset /// register, and record the reset register; or say by name why this machine -/// has either not. +/// has either not. The FACS the FADT names is read here once, and handed to +/// `acpi_mode::init`: `None` where the FADT is unusable. /// /// **One port may be both** (an AMD laptop's 0xb0 is): it is declared once, /// as `SMI_CMD`, whose kept bytes then hold the reset's value too @@ -55,22 +56,28 @@ static RESET_VALUE: AtomicU8 = AtomicU8::new(0); /// still needs a hand. Walking these tables inside the panic handler instead is /// refused — a table walk on a machine that has already failed once is how a /// panic becomes a triple fault. -pub fn init_reset(rsdp_addr: u64) { +pub fn init_reset(rsdp_addr: u64) -> Option> { let fadt = match toyos_acpi::find_table(direct_phys(), rsdp_addr, b"FACP", toyos_acpi::FADT_FOR_FIXED_HARDWARE) { Ok(table) => table, Err(e) => { log!("ACPI: FADT unusable: {e:?} — no reboot, a panic will hold the panel, and no SMI_CMD"); - return; + return None; } }; - let reset = toyos_acpi::reset_register(&fadt); + let facs = toyos_acpi::facs(fadt.phys(), &fadt); + declare(&fadt, facs); + Some(facs) +} + +fn declare(fadt: &toyos_acpi::Table

, facs: Result) { + let reset = toyos_acpi::reset_register(fadt); // Fixed hardware this kernel does not serve has no ACPI row, and so no use for `SMI_CMD`. - let smi_cmd = toyos_acpi::fixed_hardware(&fadt).ok().and_then(|fixed| fixed.smi_cmd); + let smi_cmd = toyos_acpi::fixed_hardware(fadt).ok().and_then(|fixed| fixed.smi_cmd); if let Some(named) = smi_cmd { // A FACS this kernel cannot read leaves `S4BIOS_F` unread, and the byte kept. - let s4bios = match toyos_acpi::facs(fadt.phys(), &fadt) { + let s4bios = match facs { Ok(facs) => facs.s4bios, - Err(toyos_acpi::FacsRefused::Absent) => false, + Err(FacsRefused::Absent) => false, Err(_) => true, }; if let Err(why) = super::smi_cmd::declare(named.port, named.named(s4bios, reset)) { diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 5aef1ee13a4..72862f7bd0e 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -518,7 +518,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { // First in the device phase, after storage: its lines are the diagnostic // boot's answer for a dead keyboard, and a panel shows the log's tail. - arch::boot::platform_devices(kernel_args.rsdp_addr); + arch::boot::platform_devices(&platform, kernel_args.rsdp_addr); #[cfg(feature = "boot-actuators")] arch::boot::interrupt_selftests(); diff --git a/toyos-acpi/tests/amd_laptop.rs b/toyos-acpi/tests/amd_laptop.rs index 1ddeec20e01..df5a77222d1 100644 --- a/toyos-acpi/tests/amd_laptop.rs +++ b/toyos-acpi/tests/amd_laptop.rs @@ -6,6 +6,11 @@ //! `fixed_hardware`, `pm1a_control` and `reset_register` read, at their //! offsets and with the machine's own bytes, and zeros everywhere else, then //! sealed. The whole table was checked against these fields outside the tree. +//! +//! **No field here is an OEM string or identifier**, the condition the +//! orchestrator ruled an extract of this laptop's FACP is committed under: +//! the header's OEM ID, OEM table ID, OEM revision and creator fields are +//! zeros (`common::sdt`), and nothing names the machine but "an AMD laptop". mod common; From 83af0b3994eed97067834bc7c8c300522d1f62e1 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 17:45:30 +0200 Subject: [PATCH 5/6] A controller port the kernel refuses ends the controller under a host test: the drain and its transaction ask any Kernel, and the server reads its row's registers through Ports Round 2's NOTE: `unserve` ran only over a real claim, so dropping its `self.ec = None` or its clear of the controller's GPE enable bit landed unseen. `take_queries`, `transact` and the controller's `port_in` and `port_out` now ask any `Kernel`, as `run_query` already did, and the server's PM1 event and GPE0 registers go through `Ports`, which the machine answers with its own ports and a host test with bytes. The server no longer holds the `AcpiDev`: `arm` and `serve` take it, the only two that need the device itself. `a_controller_port_the_kernel_refuses_ends_the_controller` drives `drain` into a refusal of the status port and, inside a query's transaction, of the data port, and reads the GPE enable byte, the served controller, the queue and that nothing is asked of the kernel again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- userland/acpiserver/src/main.rs | 216 +++++++++++++++++++++++--------- 1 file changed, 158 insertions(+), 58 deletions(-) diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index 698f4a2384b..dac22c025c1 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -75,14 +75,15 @@ const QUERIES: usize = 32; /// How often the log is told the queries' counts, where they moved. const COUNTS: Duration = Duration::from_secs(30); -struct Server<'a> { - dev: &'a AcpiDev, +struct Server<'a, K, P> { + kernel: &'a K, + ports: P, info: AcpiInfo, served: Served, /// The controller the namespace names, if it names one this server serves. ec: Option, /// The namespace the load kept. - aml: Option>>, + aml: Option>, /// Taken off the controller by the drain, run after it. queued: VecDeque, /// Every query number taken, and how often. @@ -101,29 +102,46 @@ fn main() { return; }; let info = dev.describe().expect("acpiserver: the claim's first read is its description"); - let served = Served { power_button: info.flags & FIXED_POWER_BUTTON != 0, ec_gpe: None }; let claim = Claim(&dev); - let mut server = Server { - dev: &dev, - info, - served, - ec: None, - aml: None, - queued: VecDeque::new(), - counts: BTreeMap::new(), - counted: 0, - logged: 0, - scis: 0, - empty: 0, - power_off: false, - }; - server.arm(); + 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.arm_controller(); - server.serve(); + server.serve(&dev); +} + +/// The row's PM1 event and GPE0 registers, which this server reads and writes +/// itself. +trait Ports { + fn in8(&self, port: u16) -> u8; + fn out8(&self, port: u16, byte: u8); + fn in16(&self, port: u16) -> u16; + fn out16(&self, port: u16, word: u16); +} + +/// The machine's own ports, which the claim opened to this process. +struct Machine; + +impl Ports for Machine { + fn in8(&self, port: u16) -> u8 { + in8(port) + } + + fn out8(&self, port: u16, byte: u8) { + out8(port, byte) + } + + fn in16(&self, port: u16) -> u16 { + in16(port) + } + + fn out16(&self, port: u16, word: u16) { + out16(port, word) + } } + /// The claim as the tables' fetch and their AML ask it for what lies outside /// its own ports. struct Claim<'a>(&'a AcpiDev); @@ -172,17 +190,15 @@ enum Unmade { Refused(String), } -impl Claim<'_> { - /// A byte of one of the controller's ports, which no row holds. - fn port_in(&self, port: u16) -> Result { - let answer = self.access(Access::read(Space::SystemIo, u64::from(port), Width::Byte)).map_err(|Stopping| Unmade::Stopping)?; - answer.made.map(|byte| byte as u8).map_err(|refused| Unmade::Refused(format!("the kernel refused a read of its port {port:#x}, {refused:?}"))) - } +/// A byte of one of the controller's ports, which no row holds. +fn port_in(kernel: &impl Kernel, port: u16) -> Result { + let answer = kernel.access(Access::read(Space::SystemIo, u64::from(port), Width::Byte)).map_err(|Stopping| Unmade::Stopping)?; + answer.made.map(|byte| byte as u8).map_err(|refused| Unmade::Refused(format!("the kernel refused a read of its port {port:#x}, {refused:?}"))) +} - fn port_out(&self, port: u16, byte: u8) -> Result<(), Unmade> { - let answer = self.access(Access::write(Space::SystemIo, u64::from(port), Width::Byte, u64::from(byte))).map_err(|Stopping| Unmade::Stopping)?; - answer.made.map(drop).map_err(|refused| Unmade::Refused(format!("the kernel refused a write of its port {port:#x}, {refused:?}"))) - } +fn port_out(kernel: &impl Kernel, port: u16, byte: u8) -> Result<(), Unmade> { + let answer = kernel.access(Access::write(Space::SystemIo, u64::from(port), Width::Byte, u64::from(byte))).map_err(|Stopping| Unmade::Stopping)?; + answer.made.map(drop).map_err(|refused| Unmade::Refused(format!("the kernel refused a write of its port {port:#x}, {refused:?}"))) } /// Each byte of a status-and-enable block: its status port and its enable port. @@ -190,19 +206,37 @@ fn bytes(block: Block) -> impl Iterator { (0..block.len / 2).map(move |i| (block.port + i, block.enable() + i)) } -impl Server<'_> { - fn arm(&mut self) { +impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { + fn new(kernel: &'a K, ports: P, info: AcpiInfo) -> Self { + Server { + kernel, + ports, + info, + served: Served { power_button: info.flags & FIXED_POWER_BUTTON != 0, ec_gpe: None }, + ec: None, + aml: None, + queued: VecDeque::new(), + counts: BTreeMap::new(), + counted: 0, + logged: 0, + scis: 0, + empty: 0, + power_off: false, + } + } + + fn arm(&mut self, dev: &AcpiDev) { let pm1 = self.info.pm1_event; - out16(pm1.enable(), 0); - out16(pm1.port, PM1_STATUS); + self.ports.out16(pm1.enable(), 0); + self.ports.out16(pm1.port, PM1_STATUS); for (status, enable) in bytes(self.info.gpe0) { - out8(enable, 0); - out8(status, 0xFF); + self.ports.out8(enable, 0); + self.ports.out8(status, 0xFF); } if self.served.power_button { - out16(pm1.enable(), PWRBTN); + self.ports.out16(pm1.enable(), PWRBTN); } - self.dev.ack().expect("acpiserver: the claim's acknowledgement"); + dev.ack().expect("acpiserver: the claim's acknowledgement"); println!( "acpiserver: armed: power button {}", if self.served.power_button { "served" } else { "a control method device, served once the namespace names it" }, @@ -228,15 +262,15 @@ impl Server<'_> { // 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`]). - match Claim(self.dev).port_in(ec.command) { + match port_in(self.kernel, ec.command) { Err(Unmade::Stopping) => return, Err(Unmade::Refused(why)) => return println!("{CONTROLLER_NONE}{why}"), Ok(_) => {} } let n = ec.gpe; let (status, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); - out8(status, 1 << (n % 8)); - out8(enable, in8(enable) | 1 << (n % 8)); + self.ports.out8(status, 1 << (n % 8)); + self.ports.out8(enable, self.ports.in8(enable) | 1 << (n % 8)); println!("{CONTROLLER_SERVED}{n:#x} at {:#x}/{:#x}", ec.command, ec.data); self.served.ec_gpe = Some(n); self.ec = Some(ec); @@ -244,7 +278,7 @@ impl Server<'_> { self.run_queued(); } - fn serve(&mut self) -> ! { + fn serve(&mut self, dev: &AcpiDev) -> ! { let poller = Poller::new(1); let mut next_count = Instant::now() + COUNTS; // A watch answers once: one still registered from a wait that timed @@ -252,7 +286,7 @@ impl Server<'_> { let mut watching = false; loop { if !watching { - poller.watch(self.dev, READABLE, 0); + poller.watch(dev, READABLE, 0); watching = true; } let wait = next_count.saturating_duration_since(Instant::now()); @@ -261,21 +295,21 @@ impl Server<'_> { self.log_counts(); next_count = Instant::now() + COUNTS; } - match self.dev.irq() { + match 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.dev.ack().expect("acpiserver: the claim's acknowledgement"); + dev.ack().expect("acpiserver: the claim's acknowledgement"); } } /// Everything one SCI carried, each event served and its status cleared. fn take(&mut self) { let pm1 = self.info.pm1_event; - let gpe0: Vec<(u8, u8)> = bytes(self.info.gpe0).map(|(status, enable)| (in8(status), in8(enable))).collect(); - let events = match sci::events(&self.served, (in16(pm1.port), in16(pm1.enable())), &gpe0) { + let gpe0: Vec<(u8, u8)> = bytes(self.info.gpe0).map(|(status, enable)| (self.ports.in8(status), self.ports.in8(enable))).collect(); + let events = match sci::events(&self.served, (self.ports.in16(pm1.port), self.ports.in16(pm1.enable())), &gpe0) { Ok(events) => events, Err(Unserved::Pm1 { status, enable }) => panic!( "acpiserver: PM1 status {status:#06x} under enable {enable:#06x} carries an event this server never enabled" @@ -288,7 +322,7 @@ impl Server<'_> { self.empty < sci::EMPTY_SCIS, "acpiserver: {} SCIs in a row carried no event this server serves (PM1 {:#06x}, GPE0 {gpe0:x?})", self.empty, - in16(pm1.port), + self.ports.in16(pm1.port), ); return; } @@ -296,7 +330,7 @@ impl Server<'_> { for event in events { match event { Event::PowerButton => { - out16(pm1.port, PWRBTN); + self.ports.out16(pm1.port, PWRBTN); self.press(); } Event::Ec => { @@ -304,7 +338,7 @@ impl Server<'_> { // so an event the drain does not see raises it again. let n = self.served.ec_gpe.expect("an event of the controller's GPE is one this server enabled"); let (status, _) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); - out8(status, 1 << (n % 8)); + self.ports.out8(status, 1 << (n % 8)); self.drain(); } } @@ -334,14 +368,14 @@ impl Server<'_> { fn take_queries(&mut self) -> Result<(), Unmade> { let Some(ec) = &self.ec else { return Ok(()) }; - let (claim, command, data) = (Claim(self.dev), ec.command, ec.data); + let (command, data) = (ec.command, ec.data); let mut taken = 0; loop { - let status = claim.port_in(command)?; + let status = port_in(self.kernel, command)?; if status & ec::SCI_EVT == 0 { return Ok(()); } - let q = transact(&claim, command, data, Transaction::query())?; + let q = transact(self.kernel, command, data, Transaction::query())?; if q == 0 { return Ok(()); } @@ -356,7 +390,7 @@ impl Server<'_> { fn unserve(&mut self, why: &str) { let n = self.served.ec_gpe.take().expect("a controller served has its GPE enabled"); let (_, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); - out8(enable, in8(enable) & !(1 << (n % 8))); + self.ports.out8(enable, self.ports.in8(enable) & !(1 << (n % 8))); self.ec = None; self.queued.clear(); println!("{CONTROLLER_NONE}{why}"); @@ -413,10 +447,10 @@ fn run_query(aml: Option<&mut Aml<'_, K>>, ec: Option<&Ec>, q: u8) -> /// One transaction on the controller at `command` and `data`, waiting on it at /// each step for at most [`EC_STEP`]. -fn transact(claim: &Claim, command: u16, data: u16, mut tx: Transaction) -> Result { +fn transact(kernel: &impl Kernel, command: u16, data: u16, mut tx: Transaction) -> Result { let mut waiting: Option<(Wait, Instant)> = None; loop { - let status = claim.port_in(command)?; + let status = port_in(kernel, command)?; match tx.step(status) { Do::Wait(wait) => { let since = match waiting { @@ -429,11 +463,11 @@ fn transact(claim: &Claim, command: u16, data: u16, mut tx: Transaction) -> Resu } Do::WriteCommand(byte) => { waiting = None; - claim.port_out(command, byte)?; + port_out(kernel, command, byte)?; } Do::ReadData => { waiting = None; - tx.read(claim.port_in(data)?); + tx.read(port_in(kernel, data)?); } Do::Done(byte) => return Ok(byte), } @@ -446,6 +480,72 @@ mod tests { use crate::aml::load; use crate::aml::tests::{crafted, sealed, CRAFTED_RSDP}; use crate::devices::tests::{laptop_with_queries, GPE0}; + use crate::host::tests::Scripted; + use std::cell::RefCell; + + /// The row's registers as bytes a write leaves as written. + #[derive(Default)] + struct Bytes(RefCell>); + + impl Ports for Bytes { + fn in8(&self, port: u16) -> u8 { + *self.0.borrow().get(&port).unwrap_or_else(|| panic!("a read of {port:#x}, which nothing wrote")) + } + + fn out8(&self, port: u16, byte: u8) { + self.0.borrow_mut().insert(port, byte); + } + + fn in16(&self, port: u16) -> u16 { + panic!("a PM1 read of {port:#x}, which no test here makes") + } + + fn out16(&self, port: u16, _: u16) { + panic!("a PM1 write of {port:#x}, which no test here makes") + } + } + + /// A controller port the kernel refuses, its status port or, inside a + /// query's transaction, its data port, ends the controller: its GPE + /// disabled and no other of the block's, what it had waiting dropped, and + /// nothing asked of the kernel for it again. + #[test] + fn a_controller_port_the_kernel_refuses_ends_the_controller() { + let (command, data, gpe) = (0x66, 0x62, 3); + let status = || Access::read(Space::SystemIo, command, Width::Byte); + for (ports, asked) in [ + (vec![], vec![status()]), + ( + // A query waits and its answer is ready, at a data port the kernel keeps. + vec![(command, u64::from(ec::SCI_EVT | ec::OBF))], + vec![ + status(), + status(), + Access::write(Space::SystemIo, command, Width::Byte, 0x84), + status(), + Access::read(Space::SystemIo, data, Width::Byte), + ], + ), + ] { + let kernel = Scripted { ports, ..Default::default() }; + let info = AcpiInfo { rsdp: 0, pm1_event: Block::NONE, gpe0: GPE0, flags: 0, reserved: [0; 3] }; + let mut server = Server::new(&kernel, Bytes::default(), info); + 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() }); + // Taken by an earlier event of the same SCI, and not yet run. + server.queued.push_back(0x28); + server.drain(); + assert_eq!(*kernel.asked.borrow(), asked); + assert_eq!(server.ec, None, "a refused controller is still served"); + assert_eq!(server.served.ec_gpe, None); + assert_eq!(server.ports.in8(enable), 0b1000_0001, "the controller's GPE is left enabled, or another with it disabled"); + assert!(server.queued.is_empty(), "a refused controller's queries are still run"); + server.drain(); + assert_eq!(kernel.asked.borrow().len(), asked.len(), "a refused controller was asked again"); + } + } /// An AMD laptop's controller and `_Q28` under both kinds of power /// button: the method runs, writes the POST port and presses only where From 27d3a8142e14486831047bc43be8c05fcb983fc3 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 19:39:35 +0200 Subject: [PATCH 6/6] No namespace leaves a control-method button dead in ACPI mode, as the owner ruled, under a host test The owner ruled, asked what a machine put in ACPI mode whose server cannot load its tables should do: "Stay in ACPI mode, button dead". The track's bullet records the ruling as its exit, met, and no_namespace_leaves_the_button_and_the_controller_unserved_and_keeps_the_claim holds it: the server serves neither the button nor the controller, enables no GPE, asks the kernel nothing, and returns to serve the claim. The two issue files this branch does not take up are back to main's: a pull request edits only the issue it takes up. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...ines-are-spelled-again-by-their-readers.md | 11 ++++--- ...r-off-after-the-kernels-own-acpi-enable.md | 29 ++++++++++--------- ...ine-in-acpi-mode-and-interprets-its-aml.md | 12 +++++--- userland/acpiserver/src/main.rs | 20 +++++++++++++ 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/issues/acpiserver-lines-are-spelled-again-by-their-readers.md b/issues/acpiserver-lines-are-spelled-again-by-their-readers.md index 99289474d93..b3d5c81b609 100644 --- a/issues/acpiserver-lines-are-spelled-again-by-their-readers.md +++ b/issues/acpiserver-lines-are-spelled-again-by-their-readers.md @@ -6,10 +6,9 @@ opened: 2026-10-08 # `acpiserver`'s lines are spelled again by their readers -`userland/acpiserver-api` holds three of the server's lines, its count of -the embedded controller's queries and its word on whether it serves a -controller, and the server, `acpi_hold`, `counters_metal` and the -`acpi_server_events` judge read them there. Every other line of the server's +`userland/acpiserver-api` holds one of the server's lines, its count of the +embedded controller's queries, and the server, `acpi_hold` and the +`acpi_server_events` judge read it there. Every other line of the server's that a reader matches is still a second spelling, held to the server's by nothing: a change of wording in `userland/acpiserver/src` is found as a red guest test, or on the T14 as a wait that runs to its bound. @@ -19,8 +18,8 @@ guest test, or on the T14 as a wait that runs to its bound. - `tests/toyos.rs`, `acpi_events_on_metal` and `acpi_tables_on_metal`: the armed line, `taken for the first time`, `tables loaded in`, the tables' read counts, `refused`. -- `tests/toyos-rust-tests/src/bin/counters_metal.rs`, `acpi_said`: the - first-query line. +- `tests/toyos-rust-tests/src/bin/counters_metal.rs`, `acpi_said`: + `acpiserver: armed: `, `embedded controller none`, the first-query line. Owner: `userland/acpiserver`. diff --git a/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md b/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md index d5f830274ea..d1f3c268b42 100644 --- a/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md +++ b/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md @@ -43,21 +43,24 @@ the one config whose claim a job mints, `tests/acpicase`, runs its job list unprompted. Such a test reads QEMU's model of the ICH9 and never the T14's firmware. -**The mint refused q35 in legacy mode until the ECDT stopgap was deleted.** -Measured on QEMU 11.1.1's q35 under OVMF and TCG, on `tests/acpicase` with -the monitor's `o /b 0xb2 3` sent before the job claimed: the kernel logged -`acpi: this machine stays in legacy mode — its firmware serves an embedded -controller no holder could (the ECDT is unusable: Absent)` and the claim -answered `NotSupported`. The kernel reads no ECDT now, and `acpi_mode::enter` -writes `ACPI_ENABLE` for q35 as for any machine in legacy mode; nothing has -read it do so. What the test still waits on is a job that waits to be asked: -the monitor's write landed by a race, between the kernel's ACPI row line and -the job's claim, and `tests/acpicase` still runs its job list unprompted. +**The mint refuses q35 in legacy mode**, so the test this exit names cannot +be built yet. Measured on QEMU 11.1.1's q35 under OVMF and TCG, on +`tests/acpicase` with the monitor's `o /b 0xb2 3` sent before the job +claimed: the kernel logged `acpi: this machine stays in legacy mode — its +firmware serves an embedded controller no holder could (the ECDT is unusable: +Absent)` and the claim answered `NotSupported`. q35 has no ECDT, and +`acpi_mode::enter` writes `ACPI_ENABLE` only for a machine whose embedded +controller the ECDT names, the stopgap +`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md` +records. That write also landed by a race, between the kernel's ACPI row line +and the job's claim: `tests/acpicase` still runs its job list unprompted. Owned by the stage "the interpreter" of -`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`. -Moving it there is the placement of the power-off's slice, which found the -refusal; it passed that slice's review and is not the owner's. +`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`, whose +deletion of the ECDT stopgap is what lets a machine with no embedded +controller be put in ACPI mode by the kernel. Moving it there is the +placement of the power-off's slice, which found the refusal; it passed that +slice's review and is not the owner's. **Exit** (the orchestrator's placement, not the owner's; that it waits on the stopgap's deletion and on a job that waits to be asked is that slice's): once the ECDT 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 a69f09e51db..72fced2b417 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 @@ -303,10 +303,14 @@ mediated access, leaves open: present PNP0C0C was found or the button's `_Qxx` is refused, a short press reaches nothing; in legacy mode the firmware served it, and before #845 the kernel refused such a machine the claim and left it there. The server - says each of those by name. Owner: this stage. **Exit**: the owner rules - between that machine in ACPI mode with its press dead and the claim given - back so the firmware has the button again, and a host test reads the - server doing what he rules. + says each of those by name. Owner: this stage. **Exit, met**: the owner + ruled (2026-10-10), asked "When ToyOS switches a machine into ACPI mode but + its ACPI server can't load the firmware's tables (so it can't handle a + power button the firmware reports through ACPI code), what should + happen?": "Stay in ACPI mode, button dead". The host test + `no_namespace_leaves_the_button_and_the_controller_unserved_and_keeps_the_claim` + (`userland/acpiserver/src/main.rs`) reads the server serving neither the + button nor the controller and keeping the claim. Two more machines have no power-off, and that is the ruled state and no weakness with an exit. A boot whose config starts no server: "Power-off diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index dac22c025c1..f441c408e41 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -547,6 +547,26 @@ mod tests { } } + /// 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, + /// enables no GPE, asks the kernel nothing, and goes on to serve the + /// claim rather than end and give it back. + #[test] + fn no_namespace_leaves_the_button_and_the_controller_unserved_and_keeps_the_claim() { + let kernel = Scripted::default(); + let info = AcpiInfo { rsdp: 0, pm1_event: Block::NONE, gpe0: GPE0, flags: 0, reserved: [0; 3] }; + let mut server = Server::new(&kernel, Bytes::default(), info); + for (_, enable) in bytes(GPE0) { + server.ports.out8(enable, 0); + } + server.arm_controller(); + 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"); + assert!(kernel.asked.borrow().is_empty(), "the kernel was asked for an access with no namespace"); + } + /// An AMD laptop's controller and `_Q28` under both kinds of power /// button: the method runs, writes the POST port and presses only where /// the FADT says the button is a control method device; on a machine