From 3b4b738a4b6d0cd1b26bad10b42d99ccf9352ac5 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 12:03:12 +0200 Subject: [PATCH 1/5] Power-off goes through the ACPI server: the kernel's S5 byte scan is deleted and the claim's holder supplies the sleep type The owner's ruling "Yes, one path": the kernel reads no AML, so it no longer scans the DSDT for `\_S5_`. `acpiserver` evaluates `\_S5` after loading the tables and hands the kernel its `SLP_TYPa` through a new operation on the `acpi` claim, `op::S5` (K4). The kernel keeps it for the machine's life and writes it at the power-off; `SYS_SHUTDOWN` is refused `NotSupported`, by name and before anything is stopped, until a holder has supplied one. A word wider than the register's three bits is refused and nothing of it kept (`toyos_userbound::firmware::SleepType`, the only way a value reaches the write). His ruling "Go on, say it loudly": a server whose `\_S5` was not handed over, for whatever reason, says at error severity that the machine has no power-off and goes on serving; a press there is said and dropped, where it panicked the server before. Deleted: `toyos-acpi/src/dsdt.rs` with its tests, the byte-scan comparison in the interpreter's namespace test, the harness's `SOFT_OFF_DECODED` and the `machine_soft_off_decoded` metal row, whose reading `acpi_tables_loaded` now takes from the server's hand-over line beside the kernel's. Config sweep: of the x86-64 configs, `system.toml`, `tests/testcases` and `console` name `shutdown`; `console` gains `acpiserver`. Every other config ends in `reboot` or is AArch64, whose power-off is PSCI's. The monitor test the design names is not built: q35 in legacy mode is refused its claim for want of an ECDT (measured; the issue carries it and moves to the interpreter stage, which deletes that stopgap). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- console/system.toml | 16 ++- ...r-off-after-the-kernels-own-acpi-enable.md | 34 +++-- ...ine-in-acpi-mode-and-interprets-its-aml.md | 48 ++++--- kernel/src/arch/aarch64/acpi_mode.rs | 6 +- kernel/src/arch/aarch64/power.rs | 5 + kernel/src/arch/x86_64/acpi_mode.rs | 15 ++- kernel/src/arch/x86_64/boot.rs | 4 +- kernel/src/arch/x86_64/power.rs | 117 +++++++----------- kernel/src/power.rs | 7 +- kernel/src/syscall/device.rs | 1 + kernel/src/syscall/machine.rs | 5 + tests/common/power.rs | 67 +++++----- .../toyos-rust-tests/src/bin/acpi_mediated.rs | 28 ++++- tests/toyos.rs | 32 +++-- toyos-abi/src/acpi.rs | 9 ++ toyos-abi/src/syscall.rs | 14 ++- toyos-acpi/src/dsdt.rs | 48 ------- toyos-acpi/src/fadt.rs | 2 +- toyos-acpi/src/lib.rs | 4 +- toyos-acpi/tests/corpus.rs | 69 +---------- toyos-acpi/tests/fixtures.rs | 17 +-- toyos-userbound/src/firmware.rs | 45 +++++++ toyos-userbound/tests/firmware.rs | 23 +++- toyos/src/device.rs | 6 + userland/acpiserver/aml/tests/namespace.rs | 5 +- userland/acpiserver/src/aml.rs | 86 ++++++++++--- userland/acpiserver/src/host.rs | 19 +++ userland/acpiserver/src/main.rs | 30 ++++- 28 files changed, 448 insertions(+), 314 deletions(-) delete mode 100644 toyos-acpi/src/dsdt.rs diff --git a/console/system.toml b/console/system.toml index 65be9a1a62b..2af4c827ea1 100644 --- a/console/system.toml +++ b/console/system.toml @@ -9,14 +9,15 @@ # records and every program's output — so nothing is lost with the checkpoint, # and puts a prompt underneath. # -# Deliberately three programs. Every extra one is something that can fail +# Deliberately few programs. Every extra one is something that can fail # between the kernel and a prompt, on the machine where a failure costs a -# reflash — and there is no daemon here that a shell needs. +# reflash — and the one daemon here that a shell needs is the one its +# `shutdown` needs. assets = ["assets"] [boot] -start = ["logkeeper", "diskserver", "fileserver", "console"] +start = ["logkeeper", "diskserver", "fileserver", "console", "acpiserver"] # `/system/bin/console` owns the screen and provides the surface its shell and the # shell's children inherit. No compositor here, so nothing receives one. @@ -31,6 +32,15 @@ service = true syscap = ["logread"] serves = ["log"] +# The machine's ACPI fixed hardware, as `system.toml` serves it: `shutdown` +# powers this machine off with the sleep type this program reads out of the +# firmware's tables and hands the kernel, which refuses a power-off without +# one. +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] + [programs.console] provides = ["surface"] receives = ["log"] 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 0c085ed9c2e..e5a586285b3 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,14 +43,28 @@ 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. -Owned by the stage "power-off through the server" of -`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`, which -rewrites the power-off this exit's test reds on. The orchestrator's -placement, not the owner's. - -**Exit** (the orchestrator's placement, not the owner's; not built in stage 1 -of `issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`): -a QEMU guest test whose harness clears `SCI_EN` through the monitor before -the claim is minted, so that the kernel writes `ACPI_ENABLE` itself, ends in -ToyOS's power-off, and is red where `SLP_EN` does not take after that enable. +**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`, whose +deletion of the ECDT stopgap is what lets a machine with no embedded +controller be put in ACPI mode by the kernel. The orchestrator's placement, +not the owner's; moved there from the power-off's slice, which found the +refusal. + +**Exit** (the orchestrator's placement, not the owner's): once the ECDT +stopgap is deleted, a QEMU guest test whose harness clears `SCI_EN` through +the monitor before the claim is minted, on a boot whose job waits to be +asked, so that the kernel writes `ACPI_ENABLE` itself, ends in ToyOS's +power-off, and is red where `SLP_EN` does not take after that enable. The T14's own firmware on this path stays unread by any row. 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 2ba30d45d93..9bd8054b99f 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 @@ -10,12 +10,12 @@ The T14's firmware hands the machine over in legacy mode, which interrupts every CPU every 2.2 s (`issues/the-t14s-firmware-interrupts-every-cpu-every-2-2-s-under-toyos.md`). -The ACPI server loads a machine's DSDT and SSDTs and evaluates `\_S5`, and -nothing else of their AML yet. What ToyOS takes from them it takes another -way: `toyos-acpi/src/dsdt.rs` finds -`\_S5_` by a byte scan, and the loader asks UEFI for the root bridges' -windows `_CRS` would name (`bootloader/src/rootbridge.rs`). Nothing reads -`_CST`, which names a CPU's C-states. +The ACPI server loads a machine's DSDT and SSDTs, evaluates `\_S5` and hands +the kernel its sleep type, which the kernel powers the machine off with and +has from nowhere else; it evaluates nothing else of their AML yet. What else +ToyOS takes from them it takes another way: the loader asks UEFI for the root +bridges' windows `_CRS` would name (`bootloader/src/rootbridge.rs`). Nothing +reads `_CST`, which names a CPU's C-states. **Ruled** (owner, 2026-10-03, on the scout in #681's comment 5966817345): the switch to ACPI mode lands as stage 1, together with a userland server that @@ -169,8 +169,7 @@ otherwise pay to find again: are refused for a function above their region that is not there. That reading was taken after Linux enumerated the buses, and Linux may number a bridge the firmware left unnumbered: it does not show what the firmware - leaves at boot, and nothing here does. Owner: the power-off stage, which - puts the interpreter in the server. **Exit**: on the T14 the server logs + leaves at boot, and nothing here does. Owner: this stage. **Exit**: on the T14 the server logs the load result of each of the 14 tables and a T14 row reads all 14 there; a table refused for a bridge's answer is brought to the owner with that bridge's Header Type and bus registers as the firmware left them, @@ -189,7 +188,7 @@ otherwise pay to find again: for the caller's copy while it is built (`what_is_held_live_is_bounded_in_sum`, `userland/acpiserver/aml/tests/hostile.rs`; `a_full_interpreter_reads_a_field_that_fits_an_integer`, `userland/acpiserver/aml/tests/heap.rs`). - Owner: the power-off stage, which decides + Owner: this stage, which decides what the server does with an interpreter that is full. **Exit**: a test fills the budget through one method, and the server then evaluates a method that builds a buffer. @@ -226,7 +225,7 @@ otherwise pay to find again: is owed before the server links the crate, and with it the slots the arena has free after the last table, since a method that defines more names than that moves the arena each time it runs, at a step for every - 64 bytes of it. Owner: the power-off stage, which gives the server its + 64 bytes of it. Owner: this stage, which gives the server its memory. **Exit**: the server states its interpreter's bound in heap bytes, and a host test under a counting allocator holds the most a load and an evaluation hold while they run to it; and the real machine's @@ -247,13 +246,30 @@ mediated access, leaves open: machine's log carries the denial, `the Global Lock: the firmware holds it`, which the `acpi_tables_loaded` row reds on as on every refusal; the wait comes back with the test that reaches its port sequence. -- **A press during the load waits for it.** The server arms the power button +- **A press during the load waits for it, and a power-off asked during it is + refused.** The server arms the power button and then loads the tables before it serves an SCI, so a press in that time latches and is served when the load ends: 77 ms on the T14, measured once, - and bounded only by what the interpreter lets each table sleep, 10 s. + and bounded only by what the interpreter lets each table sleep, 10 s. The + kernel has `\_S5`'s sleep type only once the load has handed it over, and + until then refuses `SYS_SHUTDOWN` by name, stopping nothing + (`kernel/src/arch/x86_64/power.rs`); the asker is told and may ask again. Owner: this stage. **Exit**: the slice that keeps the namespace serves the SCI while a table loads, or the `acpi_tables_loaded` row holds the load's - time on the T14 under a bound the owner names. + time on the T14 under a bound the owner names; that bound is the refusal's + too. +- **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, and a boot whose + config starts no server, 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. So is a machine whose DSDT the server refuses, which + the server says at error severity and goes on serving (the owner's "Go on, + say it loudly"). 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. The press issue's measurement of 2026-10-07 found the three presses it lost changing nothing its scout read, with the button's event enabled and no SMI @@ -266,9 +282,3 @@ and a dry run of the firmware's `_INI` for the controller clears that bit in its two variants that answer all ones, the six that answer zero showing no bit, so that slice's first reading on the T14 is the same scout's of that bit, clear after the initialisation has run, before the press. - -**Stage: power-off through the server** (the orchestrator's placement of "Yes, -one path"). The ACPI server evaluates `\_S5` and powers the machine off. -Blocked on the interpreter's evaluation of `\_S5`. **Exit**: the kernel's `\_S5_` reader, `toyos-acpi/src/dsdt.rs`, and its caller -in `kernel/src/arch/x86_64/power.rs` are deleted, and a test that powers off -through a broken server is red. diff --git a/kernel/src/arch/aarch64/acpi_mode.rs b/kernel/src/arch/aarch64/acpi_mode.rs index eca660cf4f1..1cb764b0169 100644 --- a/kernel/src/arch/aarch64/acpi_mode.rs +++ b/kernel/src/arch/aarch64/acpi_mode.rs @@ -14,7 +14,7 @@ pub fn release(_row: usize) { unreachable!("AArch64 has no ACPI row") } -/// Never called, as [`release`]: the three are reached only with a claim. +/// Never called, as [`release`]: the four are reached only with a claim. pub fn access(_row: usize, _request: &mut toyos_abi::acpi::Access) -> Result<(), toyos_abi::syscall::SyscallError> { unreachable!("AArch64 has no ACPI row") } @@ -27,6 +27,10 @@ pub fn lock_release() -> Result<(), toyos_abi::syscall::SyscallError> { unreachable!("AArch64 has no ACPI row") } +pub fn s5(_word: u64) -> Result<(), toyos_abi::syscall::SyscallError> { + unreachable!("AArch64 has no ACPI row") +} + #[cfg(feature = "test-actuators")] pub fn debug_firmware_lock(_act: u64) -> u64 { toyos_abi::syscall::SyscallError::NotSupported.to_u64() diff --git a/kernel/src/arch/aarch64/power.rs b/kernel/src/arch/aarch64/power.rs index 477042084df..cc7fd8e0e5a 100644 --- a/kernel/src/arch/aarch64/power.rs +++ b/kernel/src/arch/aarch64/power.rs @@ -22,6 +22,11 @@ pub fn can_reset() -> bool { psci::conduit().is_some() } +/// Never a reason: `SYSTEM_OFF` takes nothing a process supplies. +pub fn off_refused() -> Option<&'static str> { + None +} + /// `SYSTEM_RESET`, which asks nothing of the other CPUs (DEN0022 §5.12.2). A /// wedge calls this: the call takes no lock, and only its refusal is said. pub fn reset() -> ! { diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index 7735c082585..429bf35c7d0 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -50,8 +50,11 @@ //! taken; one whose FACS this kernel refuses has a lock nothing here can //! take, and every take is refused ([`GlobalLock`]). //! +//! **The power-off's sleep type is the holder's to supply** ([`s5`]): `\_S5` +//! is AML, and this kernel reads none. +//! //! **Nothing is done for the holder once the stop has begun**, as nothing is -//! written to `SMI_CMD`: an access and a lock exchange are each made under +//! written to `SMI_CMD`: an access, a lock exchange and a sleep type taken are each made under //! [`HOLDER`], from the decision to the last instruction, and refused there //! once the stop has begun; the power-off takes that lock before it owns the //! hardware ([`settle`]), which waits out the one in flight. @@ -544,6 +547,16 @@ pub fn lock_release() -> Result<(), SyscallError> { Ok(()) } +/// Take the `SLP_TYPa` of the machine's `\_S5` from the claim's holder, for +/// the power-off (`power::supply`): `InvalidArgument` is a word wider than +/// the register's field, of which nothing is kept. +pub fn s5(word: u64) -> Result<(), SyscallError> { + let slp_typ = firmware::sleep_type(word).ok_or(SyscallError::InvalidArgument)?; + let _acting = acting()?; + super::power::supply(slp_typ); + Ok(()) +} + /// Clear the lock word's owner where the claim's holder holds it, and tell /// the firmware where it asked meanwhile. `orphaned` says why the holder did /// not give it back itself, for the log; empty where it did. diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 1f32790d475..0fefc11be38 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -38,7 +38,7 @@ pub unsafe extern "C" fn _start(_kernel_args: &KernelArgs) -> ! { /// logs nothing: the read-back is `pat::check`, in [`after_console`], where a /// refusal has a channel to reach. /// The ACPI tables this architecture decodes: the MADT for its CPUs and I/O -/// APICs, the FADT for reset, soft-off and the century register, the HPET for +/// APICs, the FADT for reset, the PM1a control block and the century register, the HPET for /// the clock, the MCFG for ECAM and the DMAR for the IOMMU. pub const ACPI_TABLES: &[&[u8; 4]] = &[b"APIC", b"FACP", b"HPET", b"MCFG", b"DMAR"]; @@ -90,7 +90,7 @@ pub fn interrupts(rsdp_addr: u64) -> Platform { super::power::init_reset(rsdp_addr); apic::init(); percpu::init_bsp(apic::id()); - super::power::init_off(rsdp_addr); + super::power::init_control(rsdp_addr); ioapic::init(&madt); idt::enable_interrupts(); super::syscall::init(); diff --git a/kernel/src/arch/x86_64/power.rs b/kernel/src/arch/x86_64/power.rs index 0eed43f9429..2a97ed38a34 100644 --- a/kernel/src/arch/x86_64/power.rs +++ b/kernel/src/arch/x86_64/power.rs @@ -1,15 +1,22 @@ //! Reset and power-off through the FADT: its reset register, and S5 soft-off -//! through the PM1a control block with the `SLP_TYPa` the DSDT's `\_S5_` -//! package names. +//! through the PM1a control block with the `SLP_TYPa` the holder of the `acpi` +//! claim supplied. +//! +//! **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 +//! (`acpi_mode::s5`); until one has, [`supplied`] is false and a shutdown is +//! refused before anything is stopped. What a holder supplied is kept for the +//! machine's life: it is a fact of the machine's tables and not of the +//! process that read them. //! //! All input is firmware-supplied and untrusted: a table that does not decode -//! is a machine with no reboot or no soft-off, said by name, never a panic. +//! is a machine with no reboot or no PM1a control block, said by name, never +//! a panic. -use core::mem::size_of; use core::sync::atomic::{AtomicU8, Ordering}; -use toyos_acpi::{Reset, Table, TableError, S5, SDT_HEADER_LEN, SDT_REVISION}; - +use toyos_acpi::Reset; +use toyos_userbound::firmware::SleepType; use toyos_userbound::{Mediated, Ports}; use super::cpu; @@ -18,16 +25,16 @@ use crate::drivers::acpi::direct_phys; use crate::log; use crate::time::{Deadline, Duration, Tripwire}; -/// PM1 control (ACPI 6.5 Table 4.16): `SCI_EN`, `SLP_TYP` and `SLP_EN`. +/// PM1 control (ACPI 6.5 Table 4.16): `SCI_EN` and `SLP_EN`. pub const SCI_EN: u16 = 1 << 0; -const SLP_TYP: u16 = 0b111 << 10; const SLP_EN: u16 = 1 << 13; -/// Declared whether or not soft-off decodes: `SCI_EN` is read through it too. +/// `SCI_EN` is read through it too. static PM1A_CNT: Slot = Slot::empty(); -/// `\_S5_`'s `SLP_TYPa`, shifted into place; 0 until the DSDT named one. -static SLP_TYPA: AtomicU8 = AtomicU8::new(0); -static SOFT_OFF: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); +/// `\_S5`'s `SLP_TYPa` as the `acpi` claim's holder supplied it, or +/// [`UNSUPPLIED`], which is no [`SleepType`]. +static SLP_TYPA: AtomicU8 = AtomicU8::new(UNSUPPLIED); +const UNSUPPLIED: u8 = u8::MAX; static RESET: Slot = Slot::empty(); static RESET_VALUE: AtomicU8 = AtomicU8::new(0); @@ -60,70 +67,41 @@ pub fn init_reset(rsdp_addr: u64) { } } -/// Record S5 soft-off, or say by name why this machine has none; it keeps -/// booting either way. After the IDT, so a fault in the DSDT walk is reported. -pub fn init_off(rsdp_addr: u64) { - const FADT_FOR_X_DSDT: usize = toyos_acpi::FADT_X_DSDT + size_of::(); - +/// Declare the FADT's PM1a control block, or say by name why this machine +/// has none; it keeps booting either way. After the IDT, so a fault in the +/// table walk is reported. +pub fn init_control(rsdp_addr: u64) { 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 soft-off, shutdown will halt instead"); - return; - } + Err(e) => return log!("ACPI: FADT unusable: {e:?} — no PM1a control block, so no ACPI row and no power-off"), }; - let block = match toyos_acpi::pm1a_control(&fadt) { Ok(block) => block, - Err(refused) => return log!("ACPI: no PM1a control block this kernel writes ({refused:?}) — no soft-off"), + Err(refused) => return log!("ACPI: no PM1a control block this kernel writes ({refused:?}) — no ACPI row and no power-off"), }; - let pm1a = block.port; - let run = Ports::new(pm1a, block.len).expect("pm1a_control bounded the block by the port space"); + let run = Ports::new(block.port, block.len).expect("pm1a_control bounded the block by the port space"); match pio::declare("the PM1a control block", run, Mediated::ReadOnly) { Ok(declared) => PM1A_CNT.set(declared), - Err(why) => { - log!("ACPI: PM1a control block {pm1a:#x} not declared ({why:?}) — no soft-off"); - return; - } - } - - // Prefer X_DSDT over DSDT; a revision claiming 2.0 doesn't prove the field is present, so the length is checked rather than trusting the revision alone. - let dsdt_addr = toyos_acpi::dsdt_address(&fadt); - if dsdt_addr == 0 { - log!( - "ACPI: FADT names no DSDT (rev {:?}, needs {FADT_FOR_X_DSDT} bytes for X_DSDT) — no soft-off", - fadt.byte(SDT_REVISION) - ); - return; + Err(why) => log!("ACPI: PM1a control block {:#x} not declared ({why:?}) — no ACPI row and no power-off", block.port), } +} - let dsdt = match Table::open(direct_phys(), dsdt_addr, b"DSDT", SDT_HEADER_LEN) { - Ok(table) => table, - Err(TableError::Unmapped { .. }) => { - log!("ACPI: FADT points the DSDT at {dsdt_addr:#x}, which is not an address — no soft-off"); - return; - } - Err(e) => { - log!("ACPI: DSDT at {dsdt_addr:#x} unusable: {e:?} — no soft-off"); - return; - } - }; +/// Take `\_S5`'s `SLP_TYPa` from the `acpi` claim's holder. A later one +/// replaces it: the last holder to have read the tables is believed. +pub fn supply(slp_typ: SleepType) { + let control = PM1A_CNT.get().expect("an acpi claim exists only over a declared PM1a control block"); + SLP_TYPA.store(slp_typ.get(), Ordering::Release); + log!("power: S5 is PM1a {:#x} with SLP_TYPa={}, as the acpi claim's holder supplied it", control.ports().first(), slp_typ.get()); +} - let slp_typ = match toyos_acpi::s5_slp_typ(&dsdt) { - S5::SlpTyp(slp_typ) => slp_typ, - S5::Absent => { - log!("ACPI: no \\_S5_ package in the DSDT — no soft-off"); - return; - } - S5::Wide(byte) => { - log!("ACPI: the DSDT's \\_S5_ names SLP_TYPa {byte:#x}, wider than its three bits — no soft-off"); - return; - } - }; +/// What a holder supplied, if one has. +fn sleep_type() -> Option { + toyos_userbound::firmware::sleep_type(u64::from(SLP_TYPA.load(Ordering::Acquire))) +} - SLP_TYPA.store(slp_typ, Ordering::Relaxed); - SOFT_OFF.store(true, Ordering::Release); - log!("ACPI: PM1a={pm1a:#x} SLP_TYPa={slp_typ}"); +/// Why this machine has no power-off this kernel performs, where it has none. +pub fn off_refused() -> Option<&'static str> { + sleep_type().is_none().then_some("no ACPI server supplied S5") } pub fn can_reset() -> bool { @@ -153,7 +131,8 @@ const S5_TAKES: Tripwire = Tripwire::absurd( this kernel two seconds later never entered it", ); -/// Enter S5, or halt on a machine whose tables named no soft-off. +/// Enter S5. The caller asked [`off_refused`] before it stopped the machine: +/// no sleep type here is this kernel's defect, and a panic. /// /// ACPI 6.5 §16.1.6's order: on a machine in ACPI mode, which is the OS's /// to put to sleep, every event is disabled and every status cleared first @@ -167,8 +146,8 @@ const S5_TAKES: Tripwire = Tripwire::absurd( /// where a halt would leave a machine that is on, silent, and indistinguishable /// from one the power left. pub fn off(stopping: crate::quiesce::Stopping) -> ! { - let (Some(control), true) = (PM1A_CNT.get(), SOFT_OFF.load(Ordering::Acquire)) else { cpu::halt() }; - let control = control.port(0); + let slp_typ = sleep_type().expect("power: the machine was stopped for a power-off with no SLP_TYPa: the acpi claim's holder supplies it, and a shutdown without one is refused before the stop"); + let control = PM1A_CNT.get().expect("power: a sleep type was supplied over no PM1a control block").port(0); let taken = pio::take_back(&stopping); super::smi_cmd::settle(&taken); super::acpi_mode::settle(&taken); @@ -176,9 +155,9 @@ pub fn off(stopping: crate::quiesce::Stopping) -> ! { if held & SCI_EN != 0 { super::acpi_mode::quiet(&taken); } - let typed = held & !(SLP_TYP | SLP_EN) | u16::from(SLP_TYPA.load(Ordering::Relaxed)) << 10; + let typed = slp_typ.in_control(held & !SLP_EN); let smis_before = super::counters::read().smi; - // SAFETY: the block `init_off` declared and the `SLP_TYPa` the DSDT's `\_S5_` names, both decoded before `SOFT_OFF` was set. + // SAFETY: the block `init_control` declared, and a `SLP_TYPa` the field holds: what it does is the firmware's, as its `\_S5` names it. unsafe { cpu::outw(control, typed); cpu::outw(control, typed | SLP_EN); diff --git a/kernel/src/power.rs b/kernel/src/power.rs index cfca578edf2..d4cea1875bc 100644 --- a/kernel/src/power.rs +++ b/kernel/src/power.rs @@ -16,6 +16,11 @@ pub fn can_reboot() -> bool { crate::arch::power::can_reset() } +/// Why this machine has no power-off this kernel performs, where it has none. +pub fn shutdown_refused() -> Option<&'static str> { + crate::arch::power::off_refused() +} + /// Return the machine to firmware. pub fn reboot() -> ! { serial::flush_final(); @@ -47,7 +52,7 @@ pub fn reset_now() -> ! { crate::arch::power::reset() } -/// Power the machine off, or halt on one that offers no power-off. +/// Power the machine off. The caller asked [`shutdown_refused`] first. pub fn shutdown(stopping: crate::quiesce::Stopping) -> ! { // Last chance: nothing drains the log ring after this point. serial::flush_final(); diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 231a0b1c2fd..d0d0714524f 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -141,6 +141,7 @@ pub(super) fn sys_acpi(ctx: &SyscallContext, handle: RawHandle, op: u64, at: u64 }), ops::LOCK_TAKE => crate::arch::acpi_mode::lock_take().map(|taken| if taken { ops::TAKEN } else { ops::PENDING }), ops::LOCK_RELEASE => crate::arch::acpi_mode::lock_release().map(|()| 0), + ops::S5 => crate::arch::acpi_mode::s5(at).map(|()| 0), _ => Err(SyscallError::InvalidArgument), }; done.unwrap_or_else(|e| e.to_u64()) diff --git a/kernel/src/syscall/machine.rs b/kernel/src/syscall/machine.rs index c0efc356003..9b752fb2af0 100644 --- a/kernel/src/syscall/machine.rs +++ b/kernel/src/syscall/machine.rs @@ -134,10 +134,15 @@ fn quiesce(last: &str) -> Result { } /// Powers the machine off; requires a `SysCap` carrying [`Rights::POWER`]. Returns only when refused. +// The power-off is demanded before anything is torn down, as the reset is below. pub(super) fn sys_shutdown(syscap: RawHandle) -> u64 { if let Err(e) = demand_syscap(syscap, Rights::POWER) { return e.refuse(); } + if let Some(why) = power::shutdown_refused() { + log!("shutdown: {why} — refused"); + return SyscallError::NotSupported.to_u64(); + } match quiesce("Shutting down.") { Ok(stopping) => power::shutdown(stopping), Err(e) => e.to_u64(), diff --git a/tests/common/power.rs b/tests/common/power.rs index b9fbc7f2f6f..37806f96f6b 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -11,9 +11,14 @@ use super::serial; /// `kernel/src/syscall/machine.rs`. pub const SHUTTING_DOWN: &str = "Shutting down."; -/// What the kernel logs once it has decoded S5 soft-off, ahead of the PM1a -/// control block's port and the `SLP_TYPa` the DSDT's `\_S5_` names. -const SOFT_OFF_DECODED: &str = "ACPI: PM1a="; +/// What the kernel logs once the `acpi` claim's holder has supplied the +/// power-off's sleep type, ahead of the PM1a control block's port and the +/// `SLP_TYPa` (`kernel/src/arch/x86_64/power.rs`). +const S5_SUPPLIED: &str = "power: S5 is PM1a "; + +/// That line on q35, whole to the value: QEMU's PM1a control block, and the +/// `SLP_TYPa` its DSDT's `\_S5` names, which is the one its ICH9 powers off on. +const Q35_S5_SUPPLIED: &str = "power: S5 is PM1a 0x604 with SLP_TYPa=0,"; /// Wait for the boot's `last` word on a guest asked to end, then for QEMU to /// stop for `reason` and exit; `console` gains everything said on the way. @@ -40,17 +45,21 @@ pub fn ended( } /// A process holding `POWER` runs `shutdown` and the machine powers off: the -/// boot decoded q35's PM1a block and its `\_S5_`, and QEMU stops for -/// `guest-shutdown`, which neither a reset nor a halt is. +/// kernel declared q35's PM1a block, `acpiserver` evaluated the DSDT's `\_S5` +/// and handed the kernel its `SLP_TYPa`, which the kernel has from nowhere +/// else, and QEMU stops for `guest-shutdown`, which neither a reset nor a +/// halt is. pub fn machine_shutdown(test_config: &Path) -> Result<(), String> { let options = BootOptions { qmp: true, ..Default::default() }; let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); let boot = serial::Serial::boot(&qemu); boot.must_be_clean()?; - // The values this kernel read out of q35's tables, so a decode it got - // wrong fails here and not as a machine that stayed up. - boot.must_say(&format!("{SOFT_OFF_DECODED}0x604 SLP_TYPa=0"))?; + // The values, so a wrong one fails here and not as a machine that stayed + // up; and the event the shutdown waits on, since one asked before it is + // refused. + let mut supplied = boot.text().to_string(); + qemu::await_marker(&mut qemu, &mut supplied, Q35_S5_SUPPLIED, "the ACPI server to hand the kernel \\_S5")?; let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); @@ -64,7 +73,8 @@ pub fn machine_shutdown(test_config: &Path) -> Result<(), String> { } /// A stop that ends with a userland thread still running is followed by a -/// power-off all the same: q35 hands over in ACPI mode, so the power-off +/// power-off all the same, with the sleep type `acpiserver` handed the +/// kernel: q35 hands over in ACPI mode, so the power-off /// quiets the events `acpiserver` enabled, and QEMU stops for /// `guest-shutdown`. One CPU and `stop-budget-spent`, so the stop's one sweep /// finds `stop_short`'s spinner queued behind the stop's caller. @@ -85,9 +95,10 @@ pub fn machine_shutdown_short_stop(test_config: &Path) -> Result<(), String> { let boot = serial::Serial::boot(&qemu); boot.must_be_clean()?; let mut console = boot.text().to_string(); - qemu::await_marker(&mut qemu, &mut console, ACPI_ARMED, "the ACPI server arming")?; - serial::Serial::named("boot", console.clone()) - .must_say("acpi: the firmware handed this machine over in ACPI mode, so nothing is written")?; + qemu::await_marker(&mut qemu, &mut console, Q35_S5_SUPPLIED, "the ACPI server to hand the kernel \\_S5")?; + let armed = serial::Serial::named("boot", console.clone()); + armed.must_say(ACPI_ARMED)?; + armed.must_say("acpi: the firmware handed this machine over in ACPI mode, so nothing is written")?; let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); writeln!(qemu.stdin_mut(), "run test_rs_stop_short").expect("write to QEMU stdin"); @@ -108,14 +119,6 @@ pub fn machine_shutdown_short_stop(test_config: &Path) -> Result<(), String> { Ok(()) } -/// The kernel decoded S5 soft-off out of this machine's FADT and DSDT. Every -/// other branch of `arch::power::init_off` says `no soft-off` and not this. -pub fn soft_off_decoded(kernel: &serial::Serial) -> Result<(), String> { - let line = kernel.must_say(SOFT_OFF_DECODED)?; - eprintln!(" [power] {}", line.trim()); - Ok(()) -} - /// The kernel's read-back above its own arm, in /// `kernel/src/arch/x86_64/watchdog.rs`: whole clauses, one per branch. const ARMED_ON_ARRIVAL: &str = "so the bootloader had already armed the timer"; @@ -549,8 +552,8 @@ const ACPI_PRESSED: &str = /// became of it. const ACPI_TABLE: &str = "acpiserver: table "; -/// What it says of `\_S5`, ahead of the two values. -const ACPI_S5: &str = "acpiserver: \\_S5 evaluated: "; +/// What it says once the kernel has kept `\_S5`'s `SLP_TYPa`, ahead of it. +const ACPI_S5_HANDED: &str = "acpiserver: \\_S5 handed to the kernel: "; /// The number after `SLP_TYPa=` on a line. fn slp_typ_a(line: &str) -> Result { @@ -562,10 +565,8 @@ fn slp_typ_a(line: &str) -> Result { /// The server loaded every definition block it found, the DSDT first, each /// said on a line in its place; and the `SLP_TYPa` its `\_S5` evaluates to -/// is the one the kernel's byte scan of the DSDT decoded (`kernel`'s -/// [`SOFT_OFF_DECODED`] line): the interpreter against the scan, over bytes -/// one read through the mediated access and the other through the direct -/// map. Answers how many blocks there were. +/// is the one it handed the kernel and the kernel says it powers off with +/// (`kernel`'s [`S5_SUPPLIED`] line). Answers how many blocks there were. pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial) -> Result { let said: Vec<&str> = log.text().lines().filter_map(|line| line.split_once(ACPI_TABLE).map(|(_, said)| said.trim())).collect(); let Some(count) = said.first().and_then(|first| first.split_once(" of ")?.1.split_once(' ')?.0.parse::().ok()) else { @@ -576,11 +577,11 @@ pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial) -> Resu if said != expected { return Err(format!("the server's tables are {said:#?}, where {count} loaded ones are {expected:#?}")); } - let (evaluated, decoded) = (log.must_say(ACPI_S5)?, kernel.must_say(SOFT_OFF_DECODED)?); - if slp_typ_a(evaluated)? != slp_typ_a(decoded)? { - return Err(format!("the server's \\_S5 is not the kernel's: {:?} beside {:?}", evaluated.trim(), decoded.trim())); + let (handed, supplied) = (log.must_say(ACPI_S5_HANDED)?, kernel.must_say(S5_SUPPLIED)?); + if slp_typ_a(handed)? != slp_typ_a(supplied)? { + return Err(format!("the server's \\_S5 is not what the kernel powers off with: {:?} beside {:?}", handed.trim(), supplied.trim())); } - eprintln!(" [power] {count} table(s) loaded; {} beside {}", evaluated.trim(), decoded.trim()); + eprintln!(" [power] {count} table(s) loaded; {} beside {}", handed.trim(), supplied.trim()); Ok(count) } @@ -592,7 +593,8 @@ pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial) -> Resu /// power-off. The press is sent as soon as the server has armed, which is /// before it loads the machine's tables: the press is latched across the /// load, whose lines and `\_S5` are read here, on the one firmware's tables a -/// guest has. +/// guest has, and powers the machine off with the sleep type the load handed +/// the kernel. pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { let options = BootOptions { qmp: true, ..Default::default() }; let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); @@ -617,7 +619,8 @@ pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { after.must_be_clean()?; let whole = serial::Serial::named("the boot and the press", console); acpi_tables_loaded(&whole, &whole)?; - whole.must_say_after(ACPI_S5, ACPI_PRESSED)?; + whole.must_say(Q35_S5_SUPPLIED)?; + whole.must_say_after(ACPI_S5_HANDED, ACPI_PRESSED)?; eprintln!(" [power] the press: {ACPI_PRESSED}"); Ok(()) } diff --git a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs index f520ba72839..ddb14edb24d 100644 --- a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs +++ b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs @@ -15,6 +15,11 @@ //! claim binds to one process for that process's life, so the parent claims //! only after, and reads the lock word free. //! +//! Then the power-off, which is the kernel's with the sleep type a holder +//! supplies: on this boot nothing has, so a shutdown is refused and the +//! machine goes on; a word the register's field does not hold is refused and +//! changes nothing; and this guest's own is kept. +//! //! Last it takes the lock once more and asks for the power-off holding it: a //! stop ends no process, so the kernel finds a live holder's lock taken and //! gives it back before the power-off owns the hardware, which @@ -128,8 +133,9 @@ fn probe() { let mut post = Access::write(Space::SystemIo, 0x80, Width::Byte, 0); assert_eq!(syscall::acpi_access(holder.handle(), &mut post), Err(SyscallError::PermissionDenied)); assert_eq!(syscall::acpi_lock_take(holder.handle()), Err(SyscallError::PermissionDenied)); + assert_eq!(syscall::acpi_s5(holder.handle(), Q35_SLP_TYP_A), Err(SyscallError::PermissionDenied)); let info = bind(&holder.0); - println!("acpi: an unbound claim was refused its access and the lock"); + println!("acpi: an unbound claim was refused its access, the lock and the power-off's sleep type"); // A request that names no space, width or direction, a value wider than // its width, and a reserved byte that is set. @@ -151,6 +157,7 @@ fn probe() { ports(&holder, &info); configuration(&holder, &info); lock(&holder, &info); + sleep_type(&holder); assert_eq!(syscall::acpi_lock_take(holder.handle()), Ok(true), "acpi: the lock, for the power-off to find"); println!("{HELD_INTO_THE_STOP}"); @@ -380,6 +387,25 @@ fn lock(holder: &Holder, info: &AcpiInfo) { println!("acpi: the lock a dead holder left taken read free; it was taken and given back, given back with GBL_RLS where the firmware had asked, and found pending while the firmware owned it"); } +/// What this guest's `\_S5` names for `SLP_TYPa` (ACPI 6.5 §7.4.2), as +/// `acpiserver` evaluates it on the boots that start one: QEMU's ICH9 powers +/// off on it. +const Q35_SLP_TYP_A: u64 = 0; + +fn sleep_type(holder: &Holder) { + let stop = || toyos::power::stop(toyos::power::Stop::Shutdown); + let unsupplied = toyos::power::Refused::Kernel(SyscallError::NotSupported); + // No server ran on this boot, and the unbound claim's word above was not kept. + assert_eq!(stop(), unsupplied, "acpi: a power-off with no sleep type supplied"); + // `SLP_TYPx` is three bits (Table 4.16): bit 3 would land on `SLP_EN`. + for wide in [8u64, 0x100, 1 << 32 | 5, u64::MAX] { + assert_eq!(syscall::acpi_s5(holder.handle(), wide), Err(SyscallError::InvalidArgument), "acpi: {wide:#x} as a sleep type"); + } + assert_eq!(stop(), unsupplied, "acpi: a refused sleep type was kept"); + assert_eq!(syscall::acpi_s5(holder.handle(), Q35_SLP_TYP_A), Ok(())); + println!("acpi: the power-off was refused NotSupported until a sleep type was supplied, and a sleep type wider than three bits InvalidArgument"); +} + /// What the keeper says once it holds the lock. const KEPT: &str = "keeper: holding the Global Lock, and leaving with it"; diff --git a/tests/toyos.rs b/tests/toyos.rs index 91c55f30b34..b18e5c8fea8 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -259,7 +259,8 @@ const MACHINE_TESTS: &[&str] = &[ "nested_nmi_is_loud", // The power-off itself: the metal loop reaches the T14 over `ssh` and has // no way to turn it back on, so only a machine QEMU reports stopping can - // be asked. `machine_soft_off_decoded` reads the T14's own decode. + // be asked. `acpi_tables_loaded` reads the sleep type the T14's server + // handed its kernel. "machine_shutdown", // The press itself: QEMU raises the fixed power-button event on demand, // and nothing presses the T14's button but a hand. @@ -717,13 +718,6 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), - ( - // This machine's own PM1a block and `\_S5_`, which are not q35's. The - // write is `machine_shutdown`'s under QEMU: a T14 that powered itself - // off never answers the loop again. - "machine_soft_off_decoded", - metal::Metal { arms: JOBCASE, judge: |b| power::soft_off_decoded(&b[0].kernel()) }, - ), // ---- one image: tests/proctreecase ---- ( // A parent's end takes its children down. The guest carries every @@ -1702,8 +1696,8 @@ fn virt_job(profile: qemu::Profile, job: &str, said: &str) -> Result<(), String> /// What `acpi_mediated` says, an arm a line, once the kernel answered each as /// its policy says. -const ACPI_MEDIATED_SAID: [&str; 8] = [ - "acpi: an unbound claim was refused its access and the lock", +const ACPI_MEDIATED_SAID: [&str; 9] = [ + "acpi: an unbound claim was refused its access, the lock and the power-off's sleep type", "acpi: RAM was refused both ways as UsableMemory", "acpi: the RSDP read through as type 9 and its write was refused TableWrite", "acpi: an unlisted register was read and refused its write MemoryType, an unlisted address below 1 MiB was refused UnlistedCached, and the interrupt controllers, the HPET and a function's BAR DeviceMemory", @@ -1711,19 +1705,23 @@ const ACPI_MEDIATED_SAID: [&str; 8] = [ "acpi: COM1, the CMOS index, the 8259 and the configuration mechanism were refused KernelPort; the i8042's row ClaimedPort; PM1a_CNT and SMI_CMD read and refused their write ReadOnlyPort; the POST port was written", "by its address and through ECAM, and every write to configuration space was refused ConfigWrite", "acpi: the lock a dead holder left taken read free; it was taken and given back, given back with GBL_RLS where the firmware had asked, and found pending while the firmware owned it", + "acpi: the power-off was refused NotSupported until a sleep type was supplied, and a sleep type wider than three bits InvalidArgument", ]; /// Boot `tests/acpicase`, whose one job is `test_rs_acpi_mediated`, on the /// test kernel, and judge the job and what the kernel said beside it: the /// lock found at boot, given back for the holder that died with it, and /// given back for the probe itself, which asks for the power-off holding it -/// once every arm has passed. The probe does not come back from that, so its +/// once every arm has passed; and the power-off, which this boot's kernel +/// refuses by name, twice, until the probe supplies the sleep type no server +/// did, and stops nothing for. The probe does not come back from that, so its /// verdict is its last line and the kernel's; a probe that ends instead is /// one whose arm failed. fn acpi_mediated_access() -> Result<(), String> { const JOB: &str = "test_rs_acpi_mediated"; const HELD_INTO_THE_STOP: &str = "acpi: holding the Global Lock, and asking for the power-off with it"; const GIVEN_BACK_AT_THE_STOP: &str = "acpi: the Global Lock given back for a holder that left it taken (the machine is stopping)"; + const NO_S5: &str = "shutdown: no ACPI server supplied S5 — refused"; let case = compile::repo_root().join("tests/acpicase"); let mut qemu = QemuInstance::boot_with_options( &case, @@ -1755,6 +1753,13 @@ fn acpi_mediated_access() -> Result<(), String> { for line in ACPI_MEDIATED_SAID { eprintln!(" [acpi] {}", said.must_say(line)?.trim()); } + // Two shutdowns refused by the kernel's own name for it, each before + // anything was stopped: the probe ran on and said every line above. + let refused = said.text().lines().filter(|line| line.contains(NO_S5)).count(); + if refused != 2 { + return Err(format!("the kernel said {NO_S5:?} {refused} times, where the probe asked twice with none supplied:\n{}", said.text())); + } + eprintln!(" [acpi] {}", said.must_say_after(NO_S5, "power: S5 is PM1a 0x604 with SLP_TYPa=0, as the acpi claim's holder supplied it")?.trim()); said.must_say(HELD_INTO_THE_STOP)?; eprintln!(" [acpi] {}", said.must_say_after(HELD_INTO_THE_STOP, GIVEN_BACK_AT_THE_STOP)?.trim()); Ok(()) @@ -3843,8 +3848,9 @@ const T14_DEFINITION_BLOCKS: usize = 14; /// The server's load of the T14's tables, every access the kernel's to make /// for it: all of the machine's definition blocks, as many as Linux loads /// there, each fetched through `SYS_ACPI`, summing to zero as its firmware -/// sealed it, and loaded; `\_S5` evaluating to the `SLP_TYPa` the kernel's -/// own scan of the DSDT decoded; and nothing refused, so no bridge answered +/// sealed it, and loaded; `\_S5`'s `SLP_TYPa` handed to the kernel, which +/// says it powers this machine off with that value on its own PM1a block, +/// and never does on this row; and nothing refused, so no bridge answered /// what the interpreter refuses, no address was `Unmapped`, and no access /// the load makes is one the policy keeps from it. The load's AML read /// memory, read configuration space and took the Global Lock, which is the diff --git a/toyos-abi/src/acpi.rs b/toyos-abi/src/acpi.rs index 0ad0cd054ec..1848bd3759b 100644 --- a/toyos-abi/src/acpi.rs +++ b/toyos-abi/src/acpi.rs @@ -18,6 +18,11 @@ //! by what the address is and answers a refusal by name ([`Refused`]); nothing //! is mapped and no port opened. The firmware's Global Lock is taken and given //! back the same way ([`op::LOCK_TAKE`]), and goes back with the claim. +//! +//! **The power-off is the kernel's, with the sleep type the holder supplies** +//! ([`op::S5`]): what `\_S5` evaluates to is in the firmware's AML, and +//! until a holder has supplied it [`crate::syscall::SYS_SHUTDOWN`] is refused +//! [`crate::syscall::SyscallError::NotSupported`]. crate::user_safe! { /// A run of ports a register block occupies; `len` 0 is no block. @@ -84,6 +89,10 @@ pub mod op { /// Give the Global Lock back, signalling the firmware where it asked /// meanwhile. pub const LOCK_RELEASE: u64 = 2; + /// Supply the `SLP_TYPa` the machine's `\_S5` evaluates to (ACPI 6.5 + /// §7.4.2), in the call's third word: what the kernel writes to PM1a + /// control to power the machine off, kept for the machine's life. + pub const S5: u64 = 3; pub const TAKEN: u64 = 0; pub const PENDING: u64 = 1; diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index fc1e4989003..f162ef2acb7 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -283,8 +283,8 @@ pub const SYS_PORT_BADGE: u64 = 126; pub const SYS_TRACE_READ: u64 = 127; /// One operation on the `acpi` claim ([`crate::acpi::op`]): an access the -/// kernel mediates, or the firmware's Global Lock. See [`acpi_access`] and -/// [`acpi_lock_take`]. +/// kernel mediates, the firmware's Global Lock, or the power-off's sleep type. +/// See [`acpi_access`], [`acpi_lock_take`] and [`acpi_s5`]. pub const SYS_ACPI: u64 = 128; /// Bins in the per-process syscall profile — one for every number this ABI @@ -1217,6 +1217,9 @@ pub unsafe fn gpu_set_resolution( /// **The only way this comes back is refused.** A shutdown that happened has no /// caller left to answer, so the return type is the refusal alone; the loop is /// what says so, asking again rather than reporting a success that did not happen. +/// [`SyscallError::NotSupported`] is a machine whose `acpi` claim no holder +/// has supplied the power-off's sleep type on ([`acpi_s5`]): nothing was +/// stopped. pub fn shutdown(syscap: RawHandle) -> SyscallError { loop { if let Err(e) = check_unit(syscall(SYS_SHUTDOWN, syscap.0 as u64, 0, 0, 0)) { @@ -2132,6 +2135,13 @@ pub fn acpi_lock_release(claim: RawHandle) -> Result<(), SyscallError> { check_unit(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::LOCK_RELEASE, 0, 0)) } +/// Supply the `SLP_TYPa` of the machine's `\_S5` for the kernel's power-off +/// ([`crate::acpi::op::S5`]). One wider than the register's three bits is +/// refused [`SyscallError::InvalidArgument`], and nothing is kept of it. +pub fn acpi_s5(claim: RawHandle, slp_typ_a: u64) -> Result<(), SyscallError> { + check_unit(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::S5, slp_typ_a, 0)) +} + /// Sleep for the given number of nanoseconds. pub fn nanosleep(nanos: u64) { syscall(SYS_NANOSLEEP, nanos, 0, 0, 0); diff --git a/toyos-acpi/src/dsdt.rs b/toyos-acpi/src/dsdt.rs deleted file mode 100644 index 5ac3506317f..00000000000 --- a/toyos-acpi/src/dsdt.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! The DSDT (signature `DSDT`), read for one value by a byte scan, not an AML -//! interpreter: the first element of its `\_S5_` package (ACPI 6.5, "\_Sx"). - -use crate::{Phys, Table, SDT_HEADER_LEN}; - -/// AML's `PackageOp` and `BytePrefix` (ACPI 6.5, "AML Grammar Definition"). -const PACKAGE_OP: u8 = 0x12; -const BYTE_PREFIX: u8 = 0x0A; - -/// The widest `SLP_TYPx` the PM1 control register holds: three bits, 12:10. -const SLP_TYP_MAX: u8 = 7; - -/// What a DSDT says to write to `SLP_TYPa` for S5 soft-off. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum S5 { - SlpTyp(u8), - /// No `_S5_` followed by `PackageOp` whose first element lies inside the - /// table. - Absent, - /// A first element wider than `SLP_TYPx`, which shifted into place would - /// set `SLP_EN` and reserved bits. - Wide(u8), -} - -/// The first element of the first `\_S5_` package in `dsdt`, read inside the -/// table's declared length: a `BytePrefix` constant, or the element's own -/// byte, which is `ZeroOp` and `OneOp`'s value. -pub fn s5_slp_typ(dsdt: &Table

) -> S5 { - for at in SDT_HEADER_LEN..dsdt.len() { - if (0..4).any(|i| dsdt.byte(at + i) != Some(b"_S5_"[i])) || dsdt.byte(at + 4) != Some(PACKAGE_OP) { - continue; - } - // `PkgLength`'s lead byte counts the bytes after it in bits 7:6 - // ("Package Length Encoding"); `NumElements` follows it. - let Some(lead) = dsdt.byte(at + 5) else { return S5::Absent }; - let element = at + 5 + 1 + usize::from(lead >> 6) + 1; - let value = match dsdt.byte(element) { - Some(BYTE_PREFIX) => dsdt.byte(element + 1), - byte => byte, - }; - return match value { - Some(value) if value <= SLP_TYP_MAX => S5::SlpTyp(value), - Some(value) => S5::Wide(value), - None => S5::Absent, - }; - } - S5::Absent -} diff --git a/toyos-acpi/src/fadt.rs b/toyos-acpi/src/fadt.rs index 5f4408c78ad..62f8b81560b 100644 --- a/toyos-acpi/src/fadt.rs +++ b/toyos-acpi/src/fadt.rs @@ -17,7 +17,7 @@ const FADT_RESET_VALUE: usize = 128; const FADT_ARM_BOOT_ARCH: usize = 129; const FADT_MINOR_VERSION: usize = 131; pub(crate) const FADT_X_FIRMWARE_CTRL: usize = 132; -pub const FADT_X_DSDT: usize = 140; +const FADT_X_DSDT: usize = 140; /// `ARM_BOOT_ARCH`'s two flags. const PSCI_COMPLIANT: u16 = 1 << 0; diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index ece5e687ddc..18ea34589f2 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -14,7 +14,6 @@ #![no_std] #![forbid(unsafe_code)] -mod dsdt; mod ecdt; mod facs; mod fadt; @@ -25,14 +24,13 @@ mod spcr; use toyos_bootmap::DirectMapEnd; -pub use dsdt::{s5_slp_typ, S5}; 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, CMOS_RAM, - FADT_FOR_FIXED_HARDWARE, FADT_FOR_RESET, FADT_PM1A_CNT_BLK, FADT_X_DSDT, + FADT_FOR_FIXED_HARDWARE, FADT_FOR_RESET, FADT_PM1A_CNT_BLK, }; use fadt::FADT_DSDT; pub use madt::{ diff --git a/toyos-acpi/tests/corpus.rs b/toyos-acpi/tests/corpus.rs index 4bfa4668828..e8a98c78455 100644 --- a/toyos-acpi/tests/corpus.rs +++ b/toyos-acpi/tests/corpus.rs @@ -13,10 +13,10 @@ 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, - madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, s5_slp_typ, sci_line, Century, + 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, Register, Reset, SourceOverride, Table, TableError, Trigger, ECDT_NEEDED, - FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, MAX_TABLE_LEN, S5, + FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, MAX_TABLE_LEN, }; const RSDP_AT: u64 = 0x1_0000; @@ -734,71 +734,6 @@ fn no_single_byte_mutation_of_a_firmwares_descriptor_list_panics_or_runs_away() assert!(refused > 0 && refused < mutations, "{refused} of {mutations} refused"); } -/// `dsdt` opened as the kernel opens one, and its `\_S5_` read. -fn s5_of(dsdt: &[u8]) -> S5 { - let regions: &[(u64, &[u8])] = &[(TABLE_AT, dsdt)]; - let dsdt = Table::open(Machine { regions }, TABLE_AT, b"DSDT", 36).expect("DSDT"); - s5_slp_typ(&dsdt) -} - -/// `Name (_S5_, Package (4) { first, ... })`: `NameOp`, the name, -/// `PackageOp`, a one-byte `PkgLength`, `NumElements`, then `first` and three -/// `ZeroOp`s. -fn s5_package(first: &[u8]) -> Vec { - let mut aml = vec![0x08, b'_', b'S', b'5', b'_', 0x12, (2 + first.len() + 3) as u8, 0x04]; - aml.extend_from_slice(first); - aml.extend_from_slice(&[0x00; 3]); - aml -} - -/// `ZeroOp`, `OneOp` and a `BytePrefix` constant are each the value they -/// encode, and `\_S4_`'s package before it is not `\_S5_`'s. -#[test] -fn s5s_first_element_is_read_whichever_constant_encodes_it() { - let s4 = [0x08, b'_', b'S', b'4', b'_', 0x12, 0x08, 0x04, 0x0A, 0x06, 0x0A, 0x06, 0x00, 0x00]; - for (first, want) in [(&[0x00][..], 0), (&[0x01], 1), (&[0x0A, 0x07], 7)] { - let aml = [&s4[..], &s5_package(first)].concat(); - assert_eq!(s5_of(&sdt(b"DSDT", 2, &aml)), S5::SlpTyp(want), "{first:x?}"); - } -} - -/// A `PkgLength` whose lead byte says one more follows: the element is past -/// both, where reading it one byte early would answer `NumElements`, 4. -#[test] -fn a_two_byte_package_length_is_stepped_over() { - let aml = [0x08, b'_', b'S', b'5', b'_', 0x12, 0x48, 0x00, 0x04, 0x0A, 0x05, 0x00, 0x00, 0x00]; - assert_eq!(s5_of(&sdt(b"DSDT", 2, &aml)), S5::SlpTyp(5)); -} - -/// Three bits hold `SLP_TYPx`: a wider constant, and an encoding this scan -/// does not read as a byte (`WordPrefix`), are refused with the byte, never -/// shifted into `SLP_EN`. -#[test] -fn an_s5_value_wider_than_slp_typ_is_refused_with_it() { - assert_eq!(s5_of(&sdt(b"DSDT", 2, &s5_package(&[0x0A, 0x08]))), S5::Wide(8)); - assert_eq!(s5_of(&sdt(b"DSDT", 2, &s5_package(&[0x0B, 0x05, 0x00]))), S5::Wide(0x0B)); -} - -/// No `_S5_`, an `_S5_` that is not a package, and a package the table ends -/// inside are all no soft-off. -#[test] -fn an_s5_package_that_is_not_there_to_its_first_element_is_absent() { - assert_eq!(s5_of(&sdt(b"DSDT", 2, &[0u8; 16])), S5::Absent); - let method = [0x14, 0x07, b'_', b'S', b'5', b'_', 0x00, 0xA3]; - assert_eq!(s5_of(&sdt(b"DSDT", 2, &method)), S5::Absent); - let cut = &s5_package(&[0x0A, 0x05])[..9]; - assert_eq!(s5_of(&sdt(b"DSDT", 2, cut)), S5::Absent); -} - -/// The scan stops at the declared length: a package in bytes past it, which -/// the machine holds and the table does not, is not read. -#[test] -fn an_s5_package_past_the_declared_length_is_not_read() { - let mut dsdt = sdt(b"DSDT", 2, &s5_package(&[0x01])); - declare_len(&mut dsdt, 36); - assert_eq!(s5_of(&dsdt), S5::Absent); -} - /// A revision-6 FADT of 276 bytes naming q35's fixed hardware in both forms, /// for a test to break one field of. fn crafted(edit: impl FnOnce(&mut [u8])) -> Vec { diff --git a/toyos-acpi/tests/fixtures.rs b/toyos-acpi/tests/fixtures.rs index f2ca1855149..1bac0168534 100644 --- a/toyos-acpi/tests/fixtures.rs +++ b/toyos-acpi/tests/fixtures.rs @@ -10,10 +10,10 @@ use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ century_of, 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, s5_slp_typ, sci_line, + isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, FixedHardware, IoApicEntry, LegacyMode, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, - SourceOverride, Table, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, - MADT_ENTRIES, S5, + SourceOverride, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, + MADT_ENTRIES, }; /// Where each table sat in that guest's physical memory. The XSDT's entries @@ -131,17 +131,6 @@ fn the_definition_blocks_of_qemu_are_its_dsdt_alone() { assert_eq!(blocks, [Some(TableError::Unmapped { at: 0x7fb7_a000, len: 36 })]); } -/// `ACPI: PM1a=0x604 SLP_TYPa=0`, off the DSDT of the boot that logged it -/// (`fixtures/qemu-11.1.1/SOURCE`): `\_S5_`'s package, found past `\_S4_`'s, -/// whose first element is 2. -#[test] -fn the_dsdt_names_the_sleep_type_that_powers_qemu_off() { - const DSDT: u64 = 0x7f77_a000; - let regions: &[(u64, &[u8])] = &[(DSDT, include_bytes!("../fixtures/qemu-11.1.1/dsdt.bin"))]; - let dsdt = Table::open(Machine { regions }, DSDT, b"DSDT", 36).expect("DSDT"); - assert_eq!(s5_slp_typ(&dsdt), S5::SlpTyp(0)); -} - /// `ACPI: reset register SystemIO 0xcf9 <- 0x0f`, against QEMU 11.1.1's own /// source: `hw/i386/acpi-build.c:222-226` publishes `AML_AS_SYSTEM_IO`, /// `bit_width` 8, `ICH9_RST_CNT_IOPORT` (`include/hw/southbridge/ich9.h:122`, diff --git a/toyos-userbound/src/firmware.rs b/toyos-userbound/src/firmware.rs index 67ed7691d7f..830379db5fb 100644 --- a/toyos-userbound/src/firmware.rs +++ b/toyos-userbound/src/firmware.rs @@ -53,6 +53,12 @@ //! passes otherwise. //! //! **Configuration space is read and never written.** +//! +//! **The sleep type of the power-off is the holder's to supply and the +//! kernel's to write** ([`SleepType`]): `\_S5`'s `SLP_TYPa` is in the +//! firmware's AML, which only the holder evaluates, and it is written to a +//! register the holder may not write. A word wider than the register's field +//! is refused, never masked. use toyos_abi::acpi::{Refused, Width, UNLISTED}; use toyos_abi::boot::MemoryMapEntry; @@ -395,3 +401,42 @@ pub fn config(ecam: Option, segment: u16, function: Function, offset: u16, } Ok(ConfigAt { function, offset, width }) } + +/// A `SLP_TYPx` the PM1 control register holds (ACPI 6.5 Table 4.16: three +/// bits, 12:10), as the claim's holder supplied it for the power-off. +/// +/// ```compile_fail,E0451 +/// let _ = toyos_userbound::firmware::SleepType { slp_typ: 5 }; +/// ``` +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct SleepType { + slp_typ: u8, +} + +impl SleepType { + /// The widest value the field holds. + const MAX: u64 = 7; + const SHIFT: u16 = 10; + /// PM1 control's `SLP_TYPx` field. + pub const FIELD: u16 = (Self::MAX as u16) << Self::SHIFT; + + pub const fn get(self) -> u8 { + self.slp_typ + } + + /// `control` with its `SLP_TYPx` field holding this type and every other + /// bit as it was. + pub const fn in_control(self, control: u16) -> u16 { + control & !Self::FIELD | (self.slp_typ as u16) << Self::SHIFT + } +} + +/// The sleep type `word` names, or none where it is wider than the field: +/// shifted into place such a word would set `SLP_EN` and the reserved bits +/// above it. +pub const fn sleep_type(word: u64) -> Option { + if word > SleepType::MAX { + return None; + } + Some(SleepType { slp_typ: word as u8 }) +} diff --git a/toyos-userbound/tests/firmware.rs b/toyos-userbound/tests/firmware.rs index fd2f37893e1..8c2c7c35acc 100644 --- a/toyos-userbound/tests/firmware.rs +++ b/toyos-userbound/tests/firmware.rs @@ -4,7 +4,7 @@ use toyos_abi::acpi::{Refused, Width}; use toyos_abi::boot::MemoryMapEntry; -use toyos_userbound::firmware::{config, lock_word, port, type_word, Ecam, Function, Memory, MemoryVerdict, NoLockWord, Standing, FIXED_RANGE_END}; +use toyos_userbound::firmware::{config, lock_word, port, sleep_type, type_word, Ecam, Function, Memory, MemoryVerdict, NoLockWord, Standing, FIXED_RANGE_END}; use toyos_userbound::Mediated; const fn e(uefi_type: u32, start: u64, end: u64) -> MemoryMapEntry { @@ -572,3 +572,24 @@ fn every_configuration_write_is_refused_by_one_name() { assert_eq!(config(ecam, 1, HOST_BRIDGE, 0, Width::DWord, true), Err(Refused::ConfigWrite)); assert_eq!(config(None, 0, HOST_BRIDGE, 0x44, Width::Byte, true), Err(Refused::ConfigWrite)); } + +/// `SLP_TYPx` is bits 12:10 of PM1 control and `SLP_EN` bit 13 (ACPI 6.5 +/// Table 4.16): each of the eight types lands in the field and nowhere else, +/// whatever the register held, and a word one wider, which shifted would set +/// `SLP_EN`, names none. +#[test] +fn a_sleep_type_is_three_bits_and_lands_only_in_its_field() { + const SLP_EN: u16 = 1 << 13; + for word in 0..=7u64 { + let slp_typ = sleep_type(word).unwrap_or_else(|| panic!("{word} is a sleep type")); + assert_eq!(u64::from(slp_typ.get()), word); + for held in [0u16, 0x0001, 0x1C00, 0xFFFF, SLP_EN | 0x0401] { + let typed = slp_typ.in_control(held); + assert_eq!(u64::from(typed >> 10 & 7), word, "{word} over {held:#06x}"); + assert_eq!(typed & !0x1C00, held & !0x1C00, "{word} over {held:#06x} moved a bit outside SLP_TYPx"); + } + } + for wide in [8, 9, 0xFF, 0x100, 1 << 10, 7 << 10, 1 << 32 | 5, u64::MAX] { + assert_eq!(sleep_type(wide), None, "{wide:#x}"); + } +} diff --git a/toyos/src/device.rs b/toyos/src/device.rs index 6808df252a8..7c69e4f03be 100644 --- a/toyos/src/device.rs +++ b/toyos/src/device.rs @@ -230,6 +230,12 @@ impl AcpiDev { pub fn lock_release(&self) -> Result<(), SyscallError> { syscall::acpi_lock_release(self.as_handle()) } + + /// Supply `\_S5`'s `SLP_TYPa` for the kernel's power-off + /// ([`syscall::acpi_s5`]). + pub fn s5(&self, slp_typ_a: u64) -> Result<(), SyscallError> { + syscall::acpi_s5(self.as_handle(), slp_typ_a) + } } impl AsHandle for AcpiDev { diff --git a/userland/acpiserver/aml/tests/namespace.rs b/userland/acpiserver/aml/tests/namespace.rs index 8c67472cb78..430ac0dc65a 100644 --- a/userland/acpiserver/aml/tests/namespace.rs +++ b/userland/acpiserver/aml/tests/namespace.rs @@ -200,8 +200,7 @@ fn a_processor_parses_opens_a_scope_and_takes_notify() { const QEMU_DSDT: &[u8] = include_bytes!("../../../../toyos-acpi/fixtures/qemu-11.1.1/dsdt.bin"); /// QEMU 11.1.1's DSDT, as `toyos-acpi/fixtures/qemu-11.1.1/SOURCE` records -/// it: a boot of it logged `ACPI: PM1a=0x604 SLP_TYPa=0`, which the kernel -/// read by `toyos_acpi::s5_slp_typ`'s byte scan. +/// it: QEMU powers its q35 off on `SLP_TYPa` 0. #[test] fn qemus_dsdt_loads_and_its_s5_is_what_its_boot_logged() { let mut m = Machine::default(); @@ -209,8 +208,6 @@ fn qemus_dsdt_loads_and_its_s5_is_what_its_boot_logged() { i.load_bytes(&mut m, QEMU_DSDT).unwrap(); let zero = Value::Integer(0); assert_eq!(i.evaluate(&mut m, "\\_S5", &[]), Ok(Value::Package(vec![zero.clone(), zero.clone(), zero.clone(), zero]))); - let scanned = toyos_acpi::s5_slp_typ(&toyos_acpi::Table::open(Image(QEMU_DSDT), 0, b"DSDT", 0).unwrap()); - assert_eq!(scanned, toyos_acpi::S5::SlpTyp(0)); assert_eq!(m.log, vec![]); } diff --git a/userland/acpiserver/src/aml.rs b/userland/acpiserver/src/aml.rs index 253ae8b1b8a..b09b4c5c6d3 100644 --- a/userland/acpiserver/src/aml.rs +++ b/userland/acpiserver/src/aml.rs @@ -14,9 +14,13 @@ //! kernel's name for it and the memory type, in one line, whether or not the //! load reached that table. //! -//! Then `\_S5` is evaluated (ACPI 6.5, "\_Sx (System States)") and said, for -//! whoever holds it against the kernel's own decode of the same package, its -//! `ACPI: PM1a=` line. Nothing is evaluated after it yet, so the namespace is +//! Then `\_S5` is evaluated (ACPI 6.5 §7.4.2, "\_Sx (System States)") and +//! its `SLP_TYPa` handed to the kernel, which powers the machine off with it +//! and has no other source of it. **A machine whose `\_S5` was not handed +//! over has no power-off**, whatever kept it — tables unread, a DSDT refused, +//! an `\_S5` that is no package of two integers, a value the register does +//! not hold — and that is said once, loudly ([`NO_POWER_OFF`]); the server +//! goes on serving. Nothing is evaluated after it yet, so the namespace is //! not kept: no embedded-controller query is served. //! //! A table's line says its place, whether it is the DSDT or an SSDT, and a @@ -28,7 +32,7 @@ use std::time::Instant; use toyos_acpi::TableError; use toyos_aml::{Error, Host, Interpreter, Value}; -use crate::host::{Firmware, Kernel, Refusal, OWN}; +use crate::host::{Firmware, Kernel, Refusal, Stopping, OWN}; use crate::tables::Tables; /// An embedded-controller query, taken off the controller, run once the @@ -37,9 +41,8 @@ use crate::tables::Tables; pub fn query(_q: u8) {} /// What became of the machine's definition blocks: the load's verdict, which -/// its lines say and nothing in the server acts on yet. Whoever evaluates a -/// method after the load asks `blocks` whether there is a namespace, and the -/// power-off through the server writes `s5`. +/// its lines say. Whoever evaluates a method after the load asks `blocks` +/// whether there is a namespace. #[derive(Default, Debug, PartialEq, Eq)] pub struct Loaded { /// Each block in the order it was loaded, the DSDT first; `Err` is the @@ -47,6 +50,8 @@ pub struct Loaded { pub blocks: Vec>, /// `\_S5`'s `SLP_TYPa` and `SLP_TYPb`. pub s5: Option<(u64, u64)>, + /// The kernel kept that `SLP_TYPa`: this machine has a power-off. + pub handed: bool, } /// What a refused evaluation or load is called in a line anyone may quote. @@ -85,6 +90,11 @@ fn unread(why: &TableError, refused: impl Fn(u64) -> Option) -> String const STOPPING: &str = "acpiserver: the machine is stopping, so the tables' load ends here"; +/// What is said, at error severity, of a machine whose `\_S5` the kernel was +/// not handed. +pub const NO_POWER_OFF: &str = + "acpiserver: this machine has no power-off: no \\_S5 was handed to the kernel, which 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 { @@ -104,6 +114,7 @@ pub fn load(kernel: &K, rsdp: u64) -> Loaded { tables.last_refused().map_or(String::new(), |refusal| format!("; the last read refused was {refusal}")) ); println!("{OWN}that was {why:x?}"); + toyos::error!("{NO_POWER_OFF}"); return loaded; } }; @@ -176,11 +187,28 @@ pub fn load(kernel: &K, rsdp: u64) -> Loaded { Ok((a, b)) => { println!("acpiserver: \\_S5 evaluated: SLP_TYPa={a} SLP_TYPb={b}"); loaded.s5 = Some((a, b)); + match kernel.s5(a) { + Ok(true) => { + println!("acpiserver: \\_S5 handed to the kernel: SLP_TYPa={a}"); + loaded.handed = true; + } + Ok(false) => println!("acpiserver: \\_S5 refused: the kernel keeps no SLP_TYPa wider than the register's three bits"), + Err(Stopping) => { + println!("{STOPPING}"); + return loaded; + } + } + } + Err(_) if host.stopping => { + println!("{STOPPING}"); + return loaded; } - Err(_) if host.stopping => println!("{STOPPING}"), Err(kind) => println!("acpiserver: \\_S5 refused: {kind}"), } } + if !loaded.handed { + toyos::error!("{NO_POWER_OFF}"); + } if !host.refused.is_empty() { println!("acpiserver: refused so far, of accesses: {}", host.refused.counts()); } @@ -235,9 +263,10 @@ 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)) }); + assert_eq!(load(&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)); } // The builders below are `toyos-aml`'s test encodings of §20.2, the few a @@ -309,6 +338,7 @@ mod tests { Loaded { blocks: vec![Ok(()), Err("a name defined twice".into()), Err("its bytes do not sum to zero".into()), Ok(())], s5: Some((5, 7)), + handed: true, } ); } @@ -333,7 +363,7 @@ mod tests { // 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 }); + assert_eq!(load(&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 @@ -350,17 +380,41 @@ 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 }); + assert_eq!(load(&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 }); + assert_eq!(load(&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 }); + assert_eq!(load(&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 }); + assert_eq!(load(&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 + /// over; one the register's three bits do not hold is the kernel's to + /// refuse, and leaves the machine without a power-off, as every `\_S5` + /// that was not evaluated does. + #[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!(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!(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_eq!(kernel.handed.get(), None); + } + let kernel = machine(QEMU); + assert!(!load(&kernel, 0x1000).handed); + assert_eq!(kernel.handed.get(), None); } #[test] @@ -428,14 +482,14 @@ 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)) }); + assert_eq!(load(&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); - assert_eq!(loaded, Loaded { blocks: vec![Err(format!("denied by this server: {HELD}"))], s5: None }); + 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/host.rs b/userland/acpiserver/src/host.rs index a2827d905b4..b5be8b27839 100644 --- a/userland/acpiserver/src/host.rs +++ b/userland/acpiserver/src/host.rs @@ -15,6 +15,9 @@ //! 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. //! +//! **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. +//! //! **What a line says.** A refusal is said the first time it is seen and //! counted after ([`Ledger`]), as its address space, the kernel's name for //! the refusal and the memory type: a line that can be quoted anywhere. Its @@ -64,6 +67,9 @@ pub trait Kernel { fn access(&self, access: Access) -> Result; fn lock_take(&self) -> Result; fn lock_release(&self) -> Result<(), Stopping>; + /// Hand the kernel `\_S5`'s `SLP_TYPa` for its power-off: `false` where + /// it is wider than the register's field, and the kernel kept nothing. + fn s5(&self, slp_typ_a: u64) -> Result; } /// Why a read was not made. @@ -294,6 +300,8 @@ pub mod tests { pub held: Cell, /// Accesses and lock exchanges answered before the machine stops. pub stops_after: Cell>, + /// The `SLP_TYPa` it was handed and kept. + pub handed: Cell>, } impl Scripted { @@ -352,6 +360,17 @@ pub mod tests { assert!(self.held.replace(false), "a lock nobody held was given back"); Ok(()) } + + fn s5(&self, slp_typ_a: u64) -> Result { + self.stopping()?; + assert!(!self.held.get(), "the sleep type was handed over under the Global Lock"); + // ACPI 6.5 Table 4.16: `SLP_TYPx` is three bits. + if slp_typ_a > 7 { + return Ok(false); + } + self.handed.set(Some(slp_typ_a)); + Ok(true) + } } const NVS: u64 = 0x7700_0000; diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index b3e0042d84d..9a86aea0527 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -17,10 +17,12 @@ //! kernel's mediated access ([`Claim`]): after the arming, so a press during //! the load latches and is served when it ends. A table refused, and a DSDT //! refused, are each said and survived; the power button is served either -//! way. +//! way. The load hands the kernel `\_S5`'s sleep type, without which the +//! kernel refuses every power-off. //! //! **Each SCI** is read off both blocks ([`sci::events`]): a press stops the -//! machine through the supervisor, and the controller's GPE drains 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 //! never enabled, a controller that does not answer, more queries in one @@ -72,6 +74,8 @@ struct Server { logged: u64, scis: u64, empty: u32, + /// The kernel was handed `\_S5`'s sleep type: a press powers the machine off. + power_off: bool, } fn main() { @@ -94,9 +98,10 @@ fn main() { logged: 0, scis: 0, empty: 0, + power_off: false, }; server.arm(); - aml::load(&Claim(&server.dev), server.info.rsdp); + server.power_off = aml::load(&Claim(&server.dev), server.info.rsdp).handed; server.serve(); } @@ -132,6 +137,13 @@ impl Kernel for Claim<'_> { fn lock_release(&self) -> Result<(), Stopping> { Self::answered("the Global Lock's release", self.0.lock_release()) } + + fn s5(&self, slp_typ_a: u64) -> Result { + match self.0.s5(slp_typ_a) { + Err(SyscallError::InvalidArgument) => Ok(false), + answer => Self::answered("the power-off's sleep type", answer).map(|()| true), + } + } } /// Each byte of a status-and-enable block: its status port and its enable port. @@ -238,10 +250,16 @@ impl Server { self.run_queued(); } - fn press(&mut self) -> ! { + /// A press: the machine stops, and this comes back only where it has no + /// power-off, which the load said and this says again. + fn press(&mut self) { println!("acpiserver: the power button was pressed, on SCI {} of this boot; asking the supervisor to power off", self.scis); - let refused = power::stop(Stop::Shutdown); - panic!("acpiserver: the power-off was refused: {refused:?}"); + match power::stop(Stop::Shutdown) { + power::Refused::Kernel(SyscallError::NotSupported) if !self.power_off => { + toyos::error!("acpiserver: the press is dropped: {}", aml::NO_POWER_OFF.trim_start_matches("acpiserver: ")); + } + refused => panic!("acpiserver: the power-off was refused: {refused:?}"), + } } /// Take every query the controller has waiting off it, queued for after. From 22f7125ceec01d32b59b997d7f1a9b71e3a7d5c1 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 12:20:46 +0200 Subject: [PATCH 2/5] Answer review round 1: one sleep type under a claim, the T14's value pinned in its row, and the dropped press decided where a host test reads it A holder supplied `\_S5`'s sleep type as often as it liked and each call wrote a kernel record. `acpi_mode::s5` now refuses a second under one claim (`AlreadyExists`), and `release` clears the flag, so "replaceable" means by the next claim's holder and the log carries one line a claim. The guest probe reads both: its keeper supplies a sleep type q35 does not power off on and is refused a second; the probe, the next holder, supplies q35's over it and is refused a second in its turn; the harness reads exactly those two kernel lines and that QEMU stopped the guest for `guest-shutdown`, which it does on the probe's value alone. The probe's first refused shutdown moves ahead of the keeper, since the keeper's value outlives it. The `acpi_tables_loaded` row compared the server's line with the kernel's echo of the same word. It now takes the kernel's line whole to the value, as `Q35_S5_SUPPLIED` holds q35's, and the T14's is `T14_S5_SUPPLIED`: PM1a control at 0x1804 and SLP_TYPa 7, which the kernel's byte scan of that machine's DSDT logged on the boot #749 landed on (`ACPI: PM1a=0x1804 SLP_TYPa=7`). The row also reds on the server's word that the machine has no power-off. What a press is whose power-off came back refused was a match arm in `Server::press` that no test reached. It is `sci::unstopped`, pure, with a host test: only the kernel's `NotSupported` on a machine this server handed no sleep type drops the press. The track records what the claim's holder gains, the sleep type a power-off enters, with its owner and exit; says which machines without a power-off are the ruled state and which a weakness with an exit; says the refusal during the load stays; and gives "Go on, say it loudly" the breadth it was ruled with. The T14 power-off issue names the re-owning as this slice's placement. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...r-off-after-the-kernels-own-acpi-enable.md | 9 +-- ...ine-in-acpi-mode-and-interprets-its-aml.md | 56 ++++++++++++++----- kernel/src/arch/x86_64/acpi_mode.rs | 18 ++++-- kernel/src/arch/x86_64/power.rs | 12 ++-- tests/common/power.rs | 32 ++++++----- .../toyos-rust-tests/src/bin/acpi_mediated.rs | 45 ++++++++++----- tests/toyos.rs | 51 ++++++++++++----- toyos-abi/src/acpi.rs | 3 +- toyos-abi/src/syscall.rs | 3 +- userland/acpiserver/src/host.rs | 7 ++- userland/acpiserver/src/main.rs | 11 ++-- userland/acpiserver/src/sci.rs | 45 ++++++++++++++- 12 files changed, 210 insertions(+), 82 deletions(-) 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 e5a586285b3..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 @@ -58,11 +58,12 @@ 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`, whose deletion of the ECDT stopgap is what lets a machine with no embedded -controller be put in ACPI mode by the kernel. The orchestrator's placement, -not the owner's; moved there from the power-off's slice, which found the -refusal. +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): once the ECDT +**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 stopgap is deleted, a QEMU guest test whose harness clears `SCI_EN` through the monitor before the claim is minted, on a boot whose job waits to be asked, so that the kernel writes `ACPI_ENABLE` itself, ends in ToyOS's 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 9bd8054b99f..bb3b2f04b2d 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 @@ -254,22 +254,50 @@ mediated access, leaves open: kernel has `\_S5`'s sleep type only once the load has handed it over, and until then refuses `SYS_SHUTDOWN` by name, stopping nothing (`kernel/src/arch/x86_64/power.rs`); the asker is told and may ask again. - Owner: this stage. **Exit**: the slice that keeps the namespace serves the - SCI while a table loads, or the `acpi_tables_loaded` row holds the load's - time on the T14 under a bound the owner names; that bound is the refusal's - too. + Owner: this stage. **Exit**, of the press's wait: the slice that keeps the + namespace serves the SCI while a table loads. The refusal has no exit and + stays: `\_S5` is evaluated in a namespace the load has built, so no + power-off is made before the load ends, and what bounds the refusal is the + load's time. That time is held on the T14 once the `acpi_tables_loaded` row + reads it under a bound the owner names. - **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, and a boot whose - config starts no server, 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. So is a machine whose DSDT the server refuses, which - the server says at error severity and goes on serving (the owner's "Go on, - say it loudly"). 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. + 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. + + 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 + always goes through the ACPI server" is the owner's "Yes, one path". A + machine whose DSDT the server refuses: the owner's "Go on, say it loudly" + (2026-10-07) was given for a refused table, and the option it chose, as + the orchestrator's record of the session holds it, logs a refused SSDT and + carries on, and after a refused DSDT still serves the button and offers no + power-off. That the server says the same, once and at error severity, of a + machine whose tables it could not read at all or whose `\_S5` is no + package of two integers or names a value the register does not hold, and + that a press on any of them is said and dropped, is the slice's design and + not his ruling. +- **The claim's holder chooses the sleep type a power-off enters.** The + kernel writes to `PM1a_CNT` the `SLP_TYPa` the holder supplied + (`acpi_mode::s5`), and has nothing of its own to hold it against. So a + holder that is wrong or hostile decides which of the field's eight values a + power-off asked by a holder of `POWER` writes: S1, S3 or S4 where the + chipset maps them, or a value it maps to nothing, which is the kernel's + `S5 did not take` panic two seconds after the write. Before, only the + firmware's bytes chose. The holder cannot cause the write, set `SLP_EN` or + reach a bit outside 12:10 (`toyos_userbound::firmware::SleepType`), and + supplies one value under a claim. What it supplied outlives it, so a + machine whose server died still powers off: the slice's choice, which the + owner may overrule for a value withdrawn with its claim. Owner: this + stage. **Exit**: it stays while "one path" stands, which leaves the kernel + no second reading; it goes when the owner rules one in, and a guest test + then supplies a sleep type that is not the machine's and reads it refused. The press issue's measurement of 2026-10-07 found the three presses it lost changing nothing its scout read, with the button's event enabled and no SMI diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index 429bf35c7d0..1542faa5fe4 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -50,8 +50,8 @@ //! taken; one whose FACS this kernel refuses has a lock nothing here can //! take, and every take is refused ([`GlobalLock`]). //! -//! **The power-off's sleep type is the holder's to supply** ([`s5`]): `\_S5` -//! is AML, and this kernel reads none. +//! **The power-off's sleep type is the holder's to supply** ([`s5`]), once +//! under a claim: `\_S5` is AML, and this kernel reads none. //! //! **Nothing is done for the holder once the stop has begun**, as nothing is //! written to `SMI_CMD`: an access, a lock exchange and a sleep type taken are each made under @@ -146,12 +146,14 @@ struct Holder { claimed: bool, /// The holder took the Global Lock and has not given it back. locked: bool, + /// The holder supplied the power-off's sleep type: it supplies no second. + supplied: bool, } /// Held across everything this kernel does for the claim's holder, from the /// decision to the last instruction of the act: each mediated access, and /// each change of the lock word. -static HOLDER: Lock = Lock::new(Holder { claimed: false, locked: false }); +static HOLDER: Lock = Lock::new(Holder { claimed: false, locked: false, supplied: false }); /// The right to act for the claim's holder, held across the act; none once /// the claim is gone or the stop has begun. A claim that exists is the one @@ -413,6 +415,7 @@ pub fn release(row: usize) { { let mut holder = HOLDER.lock(); holder.claimed = false; + holder.supplied = false; give_back(hardware, &mut holder, "its claim is gone"); } if ENABLED.load(Ordering::Relaxed) { @@ -549,10 +552,15 @@ pub fn lock_release() -> Result<(), SyscallError> { /// Take the `SLP_TYPa` of the machine's `\_S5` from the claim's holder, for /// the power-off (`power::supply`): `InvalidArgument` is a word wider than -/// the register's field, of which nothing is kept. +/// the register's field, and `AlreadyExists` a second one under this claim; +/// nothing is kept of either. pub fn s5(word: u64) -> Result<(), SyscallError> { let slp_typ = firmware::sleep_type(word).ok_or(SyscallError::InvalidArgument)?; - let _acting = acting()?; + let mut holder = acting()?; + if holder.supplied { + return Err(SyscallError::AlreadyExists); + } + holder.supplied = true; super::power::supply(slp_typ); Ok(()) } diff --git a/kernel/src/arch/x86_64/power.rs b/kernel/src/arch/x86_64/power.rs index 2a97ed38a34..c9abd15ecac 100644 --- a/kernel/src/arch/x86_64/power.rs +++ b/kernel/src/arch/x86_64/power.rs @@ -5,9 +5,10 @@ //! **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 //! (`acpi_mode::s5`); until one has, [`supplied`] is false and a shutdown is -//! refused before anything is stopped. What a holder supplied is kept for the -//! machine's life: it is a fact of the machine's tables and not of the -//! process that read them. +//! refused before anything is stopped. A holder supplies it once, and what +//! it supplied outlives it, until the next claim's holder supplies its own: +//! it is a fact of the machine's tables and not of the process that read +//! them. //! //! All input is firmware-supplied and untrusted: a table that does not decode //! is a machine with no reboot or no PM1a control block, said by name, never @@ -86,8 +87,9 @@ pub fn init_control(rsdp_addr: u64) { } } -/// Take `\_S5`'s `SLP_TYPa` from the `acpi` claim's holder. A later one -/// replaces it: the last holder to have read the tables is believed. +/// Take `\_S5`'s `SLP_TYPa` from the `acpi` claim's holder, which +/// `acpi_mode::s5` lets supply one: the next claim's holder replaces it, the +/// last to have read the tables being the one believed. pub fn supply(slp_typ: SleepType) { let control = PM1A_CNT.get().expect("an acpi claim exists only over a declared PM1a control block"); SLP_TYPA.store(slp_typ.get(), Ordering::Release); diff --git a/tests/common/power.rs b/tests/common/power.rs index 37806f96f6b..9bb0c84f82d 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -12,12 +12,9 @@ use super::serial; pub const SHUTTING_DOWN: &str = "Shutting down."; /// What the kernel logs once the `acpi` claim's holder has supplied the -/// power-off's sleep type, ahead of the PM1a control block's port and the -/// `SLP_TYPa` (`kernel/src/arch/x86_64/power.rs`). -const S5_SUPPLIED: &str = "power: S5 is PM1a "; - -/// That line on q35, whole to the value: QEMU's PM1a control block, and the -/// `SLP_TYPa` its DSDT's `\_S5` names, which is the one its ICH9 powers off on. +/// power-off's sleep type (`kernel/src/arch/x86_64/power.rs`), whole to the +/// value, on q35: QEMU's PM1a control block, and the `SLP_TYPa` its DSDT's +/// `\_S5` names, which is the one its ICH9 powers off on. const Q35_S5_SUPPLIED: &str = "power: S5 is PM1a 0x604 with SLP_TYPa=0,"; /// Wait for the boot's `last` word on a guest asked to end, then for QEMU to @@ -555,6 +552,10 @@ const ACPI_TABLE: &str = "acpiserver: table "; /// What it says once the kernel has kept `\_S5`'s `SLP_TYPa`, ahead of it. const ACPI_S5_HANDED: &str = "acpiserver: \\_S5 handed to the kernel: "; +/// What it says, at error severity, of a machine whose `\_S5` the kernel was +/// not handed, and again of a press it drops there. +pub const ACPI_NO_POWER_OFF: &str = "this machine has no power-off"; + /// The number after `SLP_TYPa=` on a line. fn slp_typ_a(line: &str) -> Result { line.split_once("SLP_TYPa=") @@ -564,10 +565,12 @@ fn slp_typ_a(line: &str) -> Result { } /// The server loaded every definition block it found, the DSDT first, each -/// said on a line in its place; and the `SLP_TYPa` its `\_S5` evaluates to -/// is the one it handed the kernel and the kernel says it powers off with -/// (`kernel`'s [`S5_SUPPLIED`] line). Answers how many blocks there were. -pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial) -> Result { +/// said on a line in its place; and the `SLP_TYPa` its `\_S5` evaluates to, +/// which it handed the kernel, and the PM1a control block the kernel says it +/// powers off on with it, are the ones `supplied` holds for this machine: the +/// kernel's line whole to the value, read some way that is not this server's +/// evaluation. Answers how many blocks there were. +pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial, supplied: &str) -> Result { let said: Vec<&str> = log.text().lines().filter_map(|line| line.split_once(ACPI_TABLE).map(|(_, said)| said.trim())).collect(); let Some(count) = said.first().and_then(|first| first.split_once(" of ")?.1.split_once(' ')?.0.parse::().ok()) else { return Err(format!("the server said nothing of a first table ({said:?}):\n{}", log.text())); @@ -577,11 +580,11 @@ pub fn acpi_tables_loaded(log: &serial::Serial, kernel: &serial::Serial) -> Resu if said != expected { return Err(format!("the server's tables are {said:#?}, where {count} loaded ones are {expected:#?}")); } - let (handed, supplied) = (log.must_say(ACPI_S5_HANDED)?, kernel.must_say(S5_SUPPLIED)?); + let (handed, kept) = (log.must_say(ACPI_S5_HANDED)?, kernel.must_say(supplied)?); if slp_typ_a(handed)? != slp_typ_a(supplied)? { - return Err(format!("the server's \\_S5 is not what the kernel powers off with: {:?} beside {:?}", handed.trim(), supplied.trim())); + return Err(format!("the server's \\_S5 is not this machine's: {:?} beside {supplied:?}", handed.trim())); } - eprintln!(" [power] {count} table(s) loaded; {} beside {}", handed.trim(), supplied.trim()); + eprintln!(" [power] {count} table(s) loaded; {} beside {}", handed.trim(), kept.trim()); Ok(count) } @@ -618,8 +621,7 @@ pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { } after.must_be_clean()?; let whole = serial::Serial::named("the boot and the press", console); - acpi_tables_loaded(&whole, &whole)?; - whole.must_say(Q35_S5_SUPPLIED)?; + acpi_tables_loaded(&whole, &whole, Q35_S5_SUPPLIED)?; whole.must_say_after(ACPI_S5_HANDED, ACPI_PRESSED)?; eprintln!(" [power] the press: {ACPI_PRESSED}"); Ok(()) diff --git a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs index ddb14edb24d..160228891bf 100644 --- a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs +++ b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs @@ -11,19 +11,24 @@ //! machine whose firmware is not using it can take. Each address is found as //! a holder finds it, from the RSDP the claim's description names. //! -//! First a child is handed the claim, takes the lock and exits with it: the -//! claim binds to one process for that process's life, so the parent claims -//! only after, and reads the lock word free. +//! The power-off is the kernel's with the sleep type a holder supplies, once +//! under its claim. On this boot nothing has, so first a shutdown is refused +//! and the machine goes on. //! -//! Then the power-off, which is the kernel's with the sleep type a holder -//! supplies: on this boot nothing has, so a shutdown is refused and the -//! machine goes on; a word the register's field does not hold is refused and -//! changes nothing; and this guest's own is kept. +//! Then a child is handed the claim, supplies a sleep type this guest does +//! not power off on, is refused a second, takes the lock and exits with it: +//! the claim binds to one process for that process's life, so the parent +//! claims only after, and reads the lock word free. +//! +//! The parent, the next holder, is refused a word the register's field does +//! not hold, supplies this guest's own over the child's, and is refused a +//! second in its turn. //! //! Last it takes the lock once more and asks for the power-off holding it: a //! stop ends no process, so the kernel finds a live holder's lock taken and //! gives it back before the power-off owns the hardware, which -//! `acpi_mediated_access` reads in the kernel's own line. +//! `acpi_mediated_access` reads in the kernel's own line; and the guest +//! powers off, which it does on the parent's sleep type and not the child's. use std::os::toyos::process::CommandExt; use std::process::{Command, Stdio}; @@ -116,6 +121,13 @@ fn bind(claim: &Device) -> AcpiInfo { fn probe() { let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("test-runner endows a device-minting capability"); + // No server ran on this boot, and nothing has held the claim. + assert_eq!( + toyos::power::stop(toyos::power::Stop::Shutdown), + toyos::power::Refused::Kernel(SyscallError::NotSupported), + "acpi: a power-off with no sleep type supplied" + ); + // A holder that dies with the lock: the kernel gives it back with the claim. let kept = Command::new(SELF_PATH) .arg("keeper") @@ -392,18 +404,21 @@ fn lock(holder: &Holder, info: &AcpiInfo) { /// off on it. const Q35_SLP_TYP_A: u64 = 0; +/// A sleep type the register holds and QEMU's ICH9 does nothing on: a +/// power-off that entered it would still be running. +const NO_SLEEP_OF_Q35S: u64 = 5; + fn sleep_type(holder: &Holder) { - let stop = || toyos::power::stop(toyos::power::Stop::Shutdown); - let unsupplied = toyos::power::Refused::Kernel(SyscallError::NotSupported); - // No server ran on this boot, and the unbound claim's word above was not kept. - assert_eq!(stop(), unsupplied, "acpi: a power-off with no sleep type supplied"); // `SLP_TYPx` is three bits (Table 4.16): bit 3 would land on `SLP_EN`. for wide in [8u64, 0x100, 1 << 32 | 5, u64::MAX] { assert_eq!(syscall::acpi_s5(holder.handle(), wide), Err(SyscallError::InvalidArgument), "acpi: {wide:#x} as a sleep type"); } - assert_eq!(stop(), unsupplied, "acpi: a refused sleep type was kept"); + // The keeper's went with its claim, so this holder supplies its own. assert_eq!(syscall::acpi_s5(holder.handle(), Q35_SLP_TYP_A), Ok(())); - println!("acpi: the power-off was refused NotSupported until a sleep type was supplied, and a sleep type wider than three bits InvalidArgument"); + for second in [NO_SLEEP_OF_Q35S, Q35_SLP_TYP_A] { + assert_eq!(syscall::acpi_s5(holder.handle(), second), Err(SyscallError::AlreadyExists), "acpi: a second sleep type under one claim"); + } + println!("acpi: a sleep type wider than three bits was refused InvalidArgument, the next holder's replaced a dead one's, and a second under one claim was refused AlreadyExists"); } /// What the keeper says once it holds the lock. @@ -412,6 +427,8 @@ const KEPT: &str = "keeper: holding the Global Lock, and leaving with it"; fn keeper() { let claim: Device = Endowments::get().take(CLAIM_LABEL).expect("acpi: the keeper is endowed the claim"); bind(&claim); + assert_eq!(syscall::acpi_s5(claim.as_handle(), NO_SLEEP_OF_Q35S), Ok(()), "keeper: a sleep type"); + assert_eq!(syscall::acpi_s5(claim.as_handle(), Q35_SLP_TYP_A), Err(SyscallError::AlreadyExists), "keeper: a second sleep type"); assert_eq!(syscall::acpi_lock_take(claim.as_handle()), Ok(true), "keeper: the lock"); println!("{KEPT}"); } diff --git a/tests/toyos.rs b/tests/toyos.rs index b18e5c8fea8..a2a26e9ac0c 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1705,7 +1705,7 @@ const ACPI_MEDIATED_SAID: [&str; 9] = [ "acpi: COM1, the CMOS index, the 8259 and the configuration mechanism were refused KernelPort; the i8042's row ClaimedPort; PM1a_CNT and SMI_CMD read and refused their write ReadOnlyPort; the POST port was written", "by its address and through ECAM, and every write to configuration space was refused ConfigWrite", "acpi: the lock a dead holder left taken read free; it was taken and given back, given back with GBL_RLS where the firmware had asked, and found pending while the firmware owned it", - "acpi: the power-off was refused NotSupported until a sleep type was supplied, and a sleep type wider than three bits InvalidArgument", + "acpi: a sleep type wider than three bits was refused InvalidArgument, the next holder's replaced a dead one's, and a second under one claim was refused AlreadyExists", ]; /// Boot `tests/acpicase`, whose one job is `test_rs_acpi_mediated`, on the @@ -1713,15 +1713,18 @@ const ACPI_MEDIATED_SAID: [&str; 9] = [ /// lock found at boot, given back for the holder that died with it, and /// given back for the probe itself, which asks for the power-off holding it /// once every arm has passed; and the power-off, which this boot's kernel -/// refuses by name, twice, until the probe supplies the sleep type no server -/// did, and stops nothing for. The probe does not come back from that, so its -/// verdict is its last line and the kernel's; a probe that ends instead is -/// one whose arm failed. +/// refuses by name and stops nothing for while no holder has supplied its +/// sleep type, keeps one sleep type of under each claim, and makes on the +/// probe's and not on the dead holder's before it, which QEMU's ICH9 answers +/// by stopping the guest only for the probe's. The probe does not come back +/// from that, so its verdict is its last line, the kernel's and QEMU's; a +/// probe that ends instead is one whose arm failed. fn acpi_mediated_access() -> Result<(), String> { const JOB: &str = "test_rs_acpi_mediated"; const HELD_INTO_THE_STOP: &str = "acpi: holding the Global Lock, and asking for the power-off with it"; const GIVEN_BACK_AT_THE_STOP: &str = "acpi: the Global Lock given back for a holder that left it taken (the machine is stopping)"; const NO_S5: &str = "shutdown: no ACPI server supplied S5 — refused"; + const S5_SUPPLIED: &str = "power: S5 is PM1a "; let case = compile::repo_root().join("tests/acpicase"); let mut qemu = QemuInstance::boot_with_options( &case, @@ -1732,9 +1735,13 @@ fn acpi_mediated_access() -> Result<(), String> { kernel_params: &["i8042-withheld"], ready_marker: "acpi: the ACPI row: ", extra_root_files: vec![suite_bin(toyos_build::arch::Arch::X86_64, "acpi_mediated")], + qmp: true, ..Default::default() }, ); + // Opened before the probe asks: QMP delivers no event emitted before its + // client connected. + let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); let mut console = format!("{}\n", qemu.boot_log()); let ended = format!("===TEST_END {JOB} "); await_guest(&mut qemu, &mut console, "the probe's power-off to give the lock back", |said| { @@ -1753,15 +1760,23 @@ fn acpi_mediated_access() -> Result<(), String> { for line in ACPI_MEDIATED_SAID { eprintln!(" [acpi] {}", said.must_say(line)?.trim()); } - // Two shutdowns refused by the kernel's own name for it, each before - // anything was stopped: the probe ran on and said every line above. - let refused = said.text().lines().filter(|line| line.contains(NO_S5)).count(); - if refused != 2 { - return Err(format!("the kernel said {NO_S5:?} {refused} times, where the probe asked twice with none supplied:\n{}", said.text())); + // The shutdown refused by the kernel's own name for it, before anything + // was stopped: the probe ran on and said every line above. + eprintln!(" [acpi] {}", said.must_say(NO_S5)?.trim()); + // One line a claim, the dead holder's and then the probe's: no refused + // word was kept, and no second one under either claim. + let supplied: Vec<&str> = said.text().lines().filter_map(|line| Some(line.split_once(S5_SUPPLIED)?.1.trim())).collect(); + if supplied != ["0x604 with SLP_TYPa=5, as the acpi claim's holder supplied it", "0x604 with SLP_TYPa=0, as the acpi claim's holder supplied it"] { + return Err(format!("the kernel kept {supplied:#?}, where the keeper supplied 5 and the probe 0, once each:\n{}", said.text())); } - eprintln!(" [acpi] {}", said.must_say_after(NO_S5, "power: S5 is PM1a 0x604 with SLP_TYPa=0, as the acpi claim's holder supplied it")?.trim()); + said.must_say_after(NO_S5, S5_SUPPLIED)?; said.must_say(HELD_INTO_THE_STOP)?; eprintln!(" [acpi] {}", said.must_say_after(HELD_INTO_THE_STOP, GIVEN_BACK_AT_THE_STOP)?.trim()); + let stopped = stop.reason(); + if stopped.as_deref() != Some("guest-shutdown") { + return Err(format!("QEMU stopped this guest for {stopped:?}, not for the power-off on the probe's sleep type:\n{}", said.text())); + } + eprintln!(" [acpi] QEMU stopped the guest for guest-shutdown, on the probe's sleep type and not the keeper's"); Ok(()) } @@ -3845,12 +3860,19 @@ fn acpi_events_on_metal(back: &metal::Readback) -> Result<(), String> { /// `14 ACPI AML tables successfully acquired and loaded`. const T14_DEFINITION_BLOCKS: usize = 14; +/// The kernel's line for the sleep type its server handed it, whole to the +/// value, on the T14: that machine's PM1a control block, and its `\_S5`'s +/// `SLP_TYPa` as a byte scan of its DSDT reads it, with no interpreter. +const T14_S5_SUPPLIED: &str = "power: S5 is PM1a 0x1804 with SLP_TYPa=7,"; + /// The server's load of the T14's tables, every access the kernel's to make /// for it: all of the machine's definition blocks, as many as Linux loads /// there, each fetched through `SYS_ACPI`, summing to zero as its firmware /// sealed it, and loaded; `\_S5`'s `SLP_TYPa` handed to the kernel, which /// says it powers this machine off with that value on its own PM1a block, -/// and never does on this row; and nothing refused, so no bridge answered +/// both as [`T14_S5_SUPPLIED`] holds them, and never does on this row; no +/// word of the server's that this machine has no power-off; and nothing +/// refused, so no bridge answered /// what the interpreter refuses, no address was `Unmapped`, and no access /// the load makes is one the policy keeps from it. The load's AML read /// memory, read configuration space and took the Global Lock, which is the @@ -3871,7 +3893,10 @@ fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { if let Some(fired) = lines.iter().find(|l| l.contains("panicked")) { return Err(format!("the server died: {fired}")); } - let blocks = power::acpi_tables_loaded(&log, &kernel)?; + let blocks = power::acpi_tables_loaded(&log, &kernel, T14_S5_SUPPLIED)?; + if let Some(none) = lines.iter().find(|l| l.contains(power::ACPI_NO_POWER_OFF)) { + return Err(format!("the server handed the kernel no sleep type: {none}")); + } if blocks != T14_DEFINITION_BLOCKS { return Err(format!("the server found {blocks} definition blocks where Linux loads {T14_DEFINITION_BLOCKS}")); } diff --git a/toyos-abi/src/acpi.rs b/toyos-abi/src/acpi.rs index 1848bd3759b..b5cee977e3d 100644 --- a/toyos-abi/src/acpi.rs +++ b/toyos-abi/src/acpi.rs @@ -91,7 +91,8 @@ pub mod op { pub const LOCK_RELEASE: u64 = 2; /// Supply the `SLP_TYPa` the machine's `\_S5` evaluates to (ACPI 6.5 /// §7.4.2), in the call's third word: what the kernel writes to PM1a - /// control to power the machine off, kept for the machine's life. + /// control to power the machine off. Once under a claim, and kept until + /// the next claim's holder supplies its own. pub const S5: u64 = 3; pub const TAKEN: u64 = 0; diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 974a4ea7c70..46eb827ce98 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -2139,7 +2139,8 @@ pub fn acpi_lock_release(claim: RawHandle) -> Result<(), SyscallError> { /// Supply the `SLP_TYPa` of the machine's `\_S5` for the kernel's power-off /// ([`crate::acpi::op::S5`]). One wider than the register's three bits is -/// refused [`SyscallError::InvalidArgument`], and nothing is kept of it. +/// refused [`SyscallError::InvalidArgument`] and a second under one claim +/// [`SyscallError::AlreadyExists`], and nothing is kept of either. pub fn acpi_s5(claim: RawHandle, slp_typ_a: u64) -> Result<(), SyscallError> { check_unit(syscall(SYS_ACPI, claim.0 as u64, crate::acpi::op::S5, slp_typ_a, 0)) } diff --git a/userland/acpiserver/src/host.rs b/userland/acpiserver/src/host.rs index b5be8b27839..a76ddab746a 100644 --- a/userland/acpiserver/src/host.rs +++ b/userland/acpiserver/src/host.rs @@ -67,8 +67,9 @@ pub trait Kernel { fn access(&self, access: Access) -> Result; fn lock_take(&self) -> Result; fn lock_release(&self) -> Result<(), Stopping>; - /// Hand the kernel `\_S5`'s `SLP_TYPa` for its power-off: `false` where - /// it is wider than the register's field, and the kernel kept nothing. + /// Hand the kernel `\_S5`'s `SLP_TYPa` for its power-off, which it takes + /// once under a claim: `false` where it is wider than the register's + /// field, and the kernel kept nothing. fn s5(&self, slp_typ_a: u64) -> Result; } @@ -368,7 +369,7 @@ pub mod tests { if slp_typ_a > 7 { return Ok(false); } - self.handed.set(Some(slp_typ_a)); + assert_eq!(self.handed.replace(Some(slp_typ_a)), None, "a second sleep type under one claim, which the kernel refuses"); Ok(true) } } diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index 9a86aea0527..111d4c2443d 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -52,7 +52,7 @@ use toyos_abi::syscall::{DeviceType, SyscallError}; use ec::{Do, Transaction, Wait}; use host::{Answer, Kernel, Stopping, Take}; -use sci::{Event, Served, Unserved, PM1_STATUS, PWRBTN}; +use sci::{Event, Served, Unserved, Unstopped, PM1_STATUS, PWRBTN}; /// The most a controller is waited for at one step of a transaction: one that /// has not moved in this will not. @@ -254,11 +254,10 @@ impl Server { /// power-off, which the load said and this says again. fn press(&mut self) { println!("acpiserver: the power button was pressed, on SCI {} of this boot; asking the supervisor to power off", self.scis); - match power::stop(Stop::Shutdown) { - power::Refused::Kernel(SyscallError::NotSupported) if !self.power_off => { - toyos::error!("acpiserver: the press is dropped: {}", aml::NO_POWER_OFF.trim_start_matches("acpiserver: ")); - } - refused => panic!("acpiserver: the power-off was refused: {refused:?}"), + let refused = power::stop(Stop::Shutdown); + match sci::unstopped(refused, self.power_off) { + Unstopped::Dropped => toyos::error!("acpiserver: the press is dropped: {}", aml::NO_POWER_OFF.trim_start_matches("acpiserver: ")), + Unstopped::Defect => panic!("acpiserver: the power-off was refused: {refused:?}"), } } diff --git a/userland/acpiserver/src/sci.rs b/userland/acpiserver/src/sci.rs index 675c796a6c6..c1d0a30cf14 100644 --- a/userland/acpiserver/src/sci.rs +++ b/userland/acpiserver/src/sci.rs @@ -1,6 +1,10 @@ //! What one SCI carried: every event whose status and enable bits are both //! set, read off the PM1 event block and the GPE0 block, and refused by name -//! where it is one this server never enabled. +//! where it is one this server never enabled; and what a press is whose +//! power-off came back refused ([`unstopped`]). + +use toyos::power::Refused; +use toyos_abi::syscall::SyscallError; /// PM1 status and enable (ACPI 6.5 Tables 4.13, 4.14): the power button. pub const PWRBTN: u16 = 1 << 8; @@ -60,6 +64,27 @@ pub fn events(served: &Served, pm1: (u16, u16), gpe0: &[(u8, u8)]) -> Result Unstopped { + match refused { + Refused::Kernel(SyscallError::NotSupported) if !power_off => Unstopped::Dropped, + Refused::Kernel(_) | Refused::NotEndowed | Refused::Unanswered => Unstopped::Defect, + } +} + #[cfg(test)] mod tests { use super::*; @@ -102,4 +127,22 @@ mod tests { "a power button that is not the fixed one is never enabled here" ); } + + #[test] + fn only_a_press_on_a_machine_handed_no_sleep_type_is_dropped() { + let no_sleep_type = Refused::Kernel(SyscallError::NotSupported); + assert_eq!(unstopped(no_sleep_type, false), Unstopped::Dropped); + assert_eq!(unstopped(no_sleep_type, true), Unstopped::Defect, "the kernel lost the sleep type this server handed it"); + for refused in [ + Refused::Kernel(SyscallError::PermissionDenied), + Refused::Kernel(SyscallError::Gone), + Refused::Kernel(SyscallError::InvalidArgument), + Refused::NotEndowed, + Refused::Unanswered, + ] { + for power_off in [false, true] { + assert_eq!(unstopped(refused, power_off), Unstopped::Defect, "{refused:?} with power_off {power_off}"); + } + } + } } From 10a85ae4947f60cf718783cd60c6af464fd1f4cd Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 12:56:29 +0200 Subject: [PATCH 3/5] Answer review round 2: a power-off on a dead holder's sleep type is a test, and the power-off reads it after the last call in flight "What the holder supplied outlives it" was claimed by power.rs's header, the track and the pull request, and no test could fail on it. The guest test acpi_supply_outlives_holder boots tests/acpicase on the shipping kernel with acpi_mediated's new arm staged: a child is handed the claim, supplies q35's own sleep type and exits; the parent claims the row once more, which it can only once the child's claim was released, lets that claim go having supplied nothing, and asks for the power-off holding no claim. The harness reads exactly one kernel supply line and QEMU's guest-shutdown. A job is a binary's name and takes no argument, so the arm is chosen by a file the harness stages on ROOT. power::off read the sleep type before acpi_mode::settle had waited out the holder's call in flight, so a supply that passed acting() as the stop began was logged as kept and not written. It is read after. The T14 row's check for the server's no-power-off line is deleted with its constant: aml::load says that line only where it did not say the hand-over, which the row has just required. The server's line for a load that handed no \_S5 over said the machine has no power-off, which is false where an earlier holder's value stands; the line and aml.rs's header now say what the kernel has. Filed: no harness path boots console/, so its shutdown rests on a row of its config nothing reads (tooling, the orchestrator's). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...no-harness-path-boots-the-console-image.md | 30 ++++++++ kernel/src/arch/x86_64/power.rs | 7 +- tests/acpicase/system.toml | 5 +- tests/common/power.rs | 4 - .../toyos-rust-tests/src/bin/acpi_mediated.rs | 46 +++++++++++- tests/toyos.rs | 74 +++++++++++++++++-- userland/acpiserver/src/aml.rs | 15 ++-- userland/acpiserver/src/main.rs | 2 +- 8 files changed, 157 insertions(+), 26 deletions(-) create mode 100644 issues/no-harness-path-boots-the-console-image.md diff --git a/issues/no-harness-path-boots-the-console-image.md b/issues/no-harness-path-boots-the-console-image.md new file mode 100644 index 00000000000..4037d499b21 --- /dev/null +++ b/issues/no-harness-path-boots-the-console-image.md @@ -0,0 +1,30 @@ +--- +status: open +kind: tooling +opened: 2026-10-08 +--- + +# No harness path boots `console/`, so its `shutdown` rests on a row of its config that nothing reads + +`console/system.toml` starts `acpiserver` because its shell's `shutdown` is +the only way to turn that machine off, and the kernel refuses a power-off +until the `acpi` claim's holder has handed it `\_S5`'s sleep type. No guest +test and no metal row boots that config: deleting `acpiserver` from its +`[boot] start` reds nothing, and the image then refuses every `shutdown`. + +What holds today is a boot made by hand, once, at `52ef7a10f`: `cargo run -- +--console-boot --build-only` exit 0 with `bin/acpiserver` on ROOT, and `cargo +run -- --console-boot` whose console carried `acpiserver: \_S5 handed to the +kernel: SLP_TYPa=0` and `power: S5 is PM1a 0x604 with SLP_TYPa=0, as the acpi +claim's holder supplied it`, with no `panicked` line. Its `shutdown` was not +typed: the shell is on the framebuffer. The rest of the path, from a `shutdown` asked to +QEMU's `guest-shutdown`, is `machine_shutdown`'s on `tests/testcases`. + +## Owner + +The orchestrator. + +## Exit condition + +A harness boot of `console/` that runs its `shutdown` and reads QEMU's +`guest-shutdown`. diff --git a/kernel/src/arch/x86_64/power.rs b/kernel/src/arch/x86_64/power.rs index c9abd15ecac..bbfc0d19638 100644 --- a/kernel/src/arch/x86_64/power.rs +++ b/kernel/src/arch/x86_64/power.rs @@ -4,7 +4,7 @@ //! //! **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 -//! (`acpi_mode::s5`); until one has, [`supplied`] is false and a shutdown is +//! (`acpi_mode::s5`); until one has, [`off_refused`] says so and a shutdown is //! refused before anything is stopped. A holder supplies it once, and what //! it supplied outlives it, until the next claim's holder supplies its own: //! it is a fact of the machine's tables and not of the process that read @@ -148,11 +148,12 @@ const S5_TAKES: Tripwire = Tripwire::absurd( /// where a halt would leave a machine that is on, silent, and indistinguishable /// from one the power left. pub fn off(stopping: crate::quiesce::Stopping) -> ! { - let slp_typ = sleep_type().expect("power: the machine was stopped for a power-off with no SLP_TYPa: the acpi claim's holder supplies it, and a shutdown without one is refused before the stop"); - let control = PM1A_CNT.get().expect("power: a sleep type was supplied over no PM1a control block").port(0); let taken = pio::take_back(&stopping); super::smi_cmd::settle(&taken); super::acpi_mode::settle(&taken); + // Read once no holder's call is in flight: the one a supply logged last. + let slp_typ = sleep_type().expect("power: the machine was stopped for a power-off with no SLP_TYPa: the acpi claim's holder supplies it, and a shutdown without one is refused before the stop"); + let control = PM1A_CNT.get().expect("power: a sleep type was supplied over no PM1a control block").port(0); let held = cpu::inw(control); if held & SCI_EN != 0 { super::acpi_mode::quiet(&taken); diff --git a/tests/acpicase/system.toml b/tests/acpicase/system.toml index c9d4da54c6b..bba53a2a5d9 100644 --- a/tests/acpicase/system.toml +++ b/tests/acpicase/system.toml @@ -15,8 +15,9 @@ syscap = ["logread"] # hands none without it; `power` because the job list ends the machine, # asking the supervisor. # -# `args` as it stands is the guest's: `acpi_mediated_access` boots this file -# with its probe on ROOT. A metal row's job list replaces it. +# `args` as it stands is the guest's: `acpi_mediated_access` and +# `acpi_supply_outlives_holder` boot this file with their probe on ROOT. A +# metal row's job list replaces it. [programs.test-runner] receives = ["power"] syscap = ["device", "dup"] diff --git a/tests/common/power.rs b/tests/common/power.rs index f587add4f8d..8eed7cf96b0 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -552,10 +552,6 @@ const ACPI_TABLE: &str = "acpiserver: table "; /// What it says once the kernel has kept `\_S5`'s `SLP_TYPa`, ahead of it. const ACPI_S5_HANDED: &str = "acpiserver: \\_S5 handed to the kernel: "; -/// What it says, at error severity, of a machine whose `\_S5` the kernel was -/// not handed, and again of a press it drops there. -pub const ACPI_NO_POWER_OFF: &str = "this machine has no power-off"; - /// The number after `SLP_TYPa=` on a line. fn slp_typ_a(line: &str) -> Result { line.split_once("SLP_TYPa=") diff --git a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs index 160228891bf..acbe19e3dba 100644 --- a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs +++ b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs @@ -29,6 +29,12 @@ //! gives it back before the power-off owns the hardware, which //! `acpi_mediated_access` reads in the kernel's own line; and the guest //! powers off, which it does on the parent's sleep type and not the child's. +//! +//! **What a holder supplied stands once it is gone** ([`outlived`]), on a +//! boot of its own, since a power-off ends one: the boot whose ROOT carries +//! [`OUTLIVED`] has a child supply this guest's own sleep type and exit, and +//! the parent, which supplied none, ask for the power-off after the child's +//! claim and one more have been released. use std::os::toyos::process::CommandExt; use std::process::{Command, Stdio}; @@ -44,6 +50,10 @@ use toyos_abi::RawHandle; const SELF_PATH: &str = "/system/bin/test_rs_acpi_mediated"; const CLAIM_LABEL: &str = "acpi-claim"; +/// The file whose presence on ROOT makes this boot [`outlived`]'s: a job is a +/// binary's name and takes no argument. +const OUTLIVED: &str = "/system/share/acpi_mediated_outlived"; + /// `EFI_MEMORY_TYPE`s: what the kernel hands out as RAM, and ACPI's two. const USABLE: [u8; 5] = [1, 2, 3, 4, 7]; const ACPI_RECLAIM: u8 = 9; @@ -90,8 +100,10 @@ impl Holder { fn main() { match std::env::args().nth(1).as_deref() { + None if std::fs::exists(OUTLIVED).expect("acpi: ask ROOT for this boot's arm") => outlived(), None => probe(), Some("keeper") => keeper(), + Some("supplier") => supplier(), other => panic!("acpi: unknown role {other:?}"), } } @@ -413,7 +425,7 @@ fn sleep_type(holder: &Holder) { for wide in [8u64, 0x100, 1 << 32 | 5, u64::MAX] { assert_eq!(syscall::acpi_s5(holder.handle(), wide), Err(SyscallError::InvalidArgument), "acpi: {wide:#x} as a sleep type"); } - // The keeper's went with its claim, so this holder supplies its own. + // The keeper's turn went with its claim, so this holder supplies its own. assert_eq!(syscall::acpi_s5(holder.handle(), Q35_SLP_TYP_A), Ok(())); for second in [NO_SLEEP_OF_Q35S, Q35_SLP_TYP_A] { assert_eq!(syscall::acpi_s5(holder.handle(), second), Err(SyscallError::AlreadyExists), "acpi: a second sleep type under one claim"); @@ -432,3 +444,35 @@ fn keeper() { assert_eq!(syscall::acpi_lock_take(claim.as_handle()), Ok(true), "keeper: the lock"); println!("{KEPT}"); } + +/// What the supplier says once the kernel has its sleep type. +const SUPPLIED: &str = "supplier: this guest's own sleep type supplied, and leaving"; + +fn supplier() { + let claim: Device = Endowments::get().take(CLAIM_LABEL).expect("acpi: the supplier is endowed the claim"); + bind(&claim); + assert_eq!(syscall::acpi_s5(claim.as_handle(), Q35_SLP_TYP_A), Ok(()), "supplier: this guest's sleep type"); + println!("{SUPPLIED}"); +} + +/// What [`outlived`] says before it asks, holding no claim. +const ASKED_WITH_NO_HOLDER: &str = "acpi: asking for the power-off with no claim held, on what a holder that is gone supplied"; + +fn outlived() { + let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("test-runner endows a device-minting capability"); + let left = Command::new(SELF_PATH) + .arg("supplier") + .endow(CLAIM_LABEL, claim(&cap).into_raw().0) + .stdout(Stdio::piped()) + .output() + .expect("acpi: spawn the supplier"); + let said = String::from_utf8_lossy(&left.stdout); + assert!(left.status.success() && said.contains(SUPPLIED), "acpi: the supplier ended {:?} having said {said:?}", left.status); + // The row is claimable again only once the supplier's claim was + // released; and this claim's own release, which supplied nothing, has + // returned before the power-off is asked for. + drop(claim(&cap)); + println!("{ASKED_WITH_NO_HOLDER}"); + let refused = toyos::power::stop(toyos::power::Stop::Shutdown); + panic!("acpi: the power-off was refused: {refused:?}"); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index f1cf3c5348a..a88ed37fada 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -275,6 +275,11 @@ const MACHINE_TESTS: &[&str] = &[ // that owes `GBL_RLS` need the FACS's word staged as only an idle firmware // allows: neither is done to the T14, which nothing powers on again. "acpi_mediated_access", + // A power-off on the sleep type of a holder that is gone: the kernel's + // static across a claim's release, and a machine that answers the write + // by stopping. No host test reaches either, and the T14 is never asked to + // power off. + "acpi_supply_outlives_holder", // The power-off after a stop that left a thread running, in ACPI mode: it // ends the machine, so only one QEMU reports stopping can be asked, and // the T14 hands over in legacy mode, where no holder means no quieting. @@ -1732,7 +1737,6 @@ fn acpi_mediated_access() -> Result<(), String> { const HELD_INTO_THE_STOP: &str = "acpi: holding the Global Lock, and asking for the power-off with it"; const GIVEN_BACK_AT_THE_STOP: &str = "acpi: the Global Lock given back for a holder that left it taken (the machine is stopping)"; const NO_S5: &str = "shutdown: no ACPI server supplied S5 — refused"; - const S5_SUPPLIED: &str = "power: S5 is PM1a "; let case = compile::repo_root().join("tests/acpicase"); let mut qemu = QemuInstance::boot_with_options( &case, @@ -1773,7 +1777,7 @@ fn acpi_mediated_access() -> Result<(), String> { eprintln!(" [acpi] {}", said.must_say(NO_S5)?.trim()); // One line a claim, the dead holder's and then the probe's: no refused // word was kept, and no second one under either claim. - let supplied: Vec<&str> = said.text().lines().filter_map(|line| Some(line.split_once(S5_SUPPLIED)?.1.trim())).collect(); + let supplied = s5_supplied(&said); if supplied != ["0x604 with SLP_TYPa=5, as the acpi claim's holder supplied it", "0x604 with SLP_TYPa=0, as the acpi claim's holder supplied it"] { return Err(format!("the kernel kept {supplied:#?}, where the keeper supplied 5 and the probe 0, once each:\n{}", said.text())); } @@ -1788,6 +1792,63 @@ fn acpi_mediated_access() -> Result<(), String> { Ok(()) } +/// The head of the kernel's line for a sleep type it kept. +const S5_SUPPLIED: &str = "power: S5 is PM1a "; + +/// What the kernel said after [`S5_SUPPLIED`], a line a sleep type it kept. +fn s5_supplied(said: &serial::Serial) -> Vec<&str> { + said.text().lines().filter_map(|line| Some(line.split_once(S5_SUPPLIED)?.1.trim())).collect() +} + +/// Boot `tests/acpicase` on the shipping kernel with `acpi_mediated`'s +/// `outlived` arm staged: a holder supplies q35's own sleep type and exits, +/// its claim and one more that supplied nothing are released, and the probe, +/// holding no claim, asks for the power-off. The kernel kept one sleep type, +/// and QEMU's ICH9 stops the guest on it: what a holder supplied stands once +/// the holder is gone. A probe that ends instead was refused its power-off. +fn acpi_supply_outlives_holder() -> Result<(), String> { + const JOB: &str = "test_rs_acpi_mediated"; + const ASKED: &str = "acpi: asking for the power-off with no claim held, on what a holder that is gone supplied"; + let case = compile::repo_root().join("tests/acpicase"); + let mut qemu = QemuInstance::boot_with_options( + &case, + &[], + &[], + BootOptions { + ready_marker: "acpi: the ACPI row: ", + extra_root_files: vec![ + suite_bin(toyos_build::arch::Arch::X86_64, "acpi_mediated"), + // The arm's name is the file's: `acpi_mediated` asks whether it is there. + ("share/acpi_mediated_outlived".to_string(), b"outlived\n".to_vec()), + ], + qmp: true, + ..Default::default() + }, + ); + // Opened before the probe asks: QMP delivers no event emitted before its + // client connected. + let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); + let mut console = format!("{}\n", qemu.boot_log()); + let ended = format!("===TEST_END {JOB} "); + await_guest(&mut qemu, &mut console, "the power-off on a sleep type its holder left behind", |said| { + said.contains(power::SHUTTING_DOWN) || said.contains(&ended) + })?; + let said = serial::Serial::named("the supplier's boot", console); + said.must_be_clean()?; + let supplied = s5_supplied(&said); + if supplied != ["0x604 with SLP_TYPa=0, as the acpi claim's holder supplied it"] { + return Err(format!("the kernel kept {supplied:#?}, where one holder supplied 0, once:\n{}", said.text())); + } + said.must_say(ASKED)?; + eprintln!(" [acpi] {}", said.must_say_after(ASKED, power::SHUTTING_DOWN)?.trim()); + let stopped = stop.reason(); + if stopped.as_deref() != Some("guest-shutdown") { + return Err(format!("QEMU stopped this guest for {stopped:?}, not for the power-off on the sleep type a dead holder supplied:\n{}", said.text())); + } + eprintln!(" [acpi] QEMU stopped the guest for guest-shutdown, with no claim held, on the sleep type a holder that is gone supplied"); + Ok(()) +} + /// A `mask-windows` boot's windows: `common::irqcensus::windows`'s verdict, /// with `cpus` CPUs reporting. fn mask_windows(capture: &str, cpus: u32) -> Result<(), String> { @@ -2823,6 +2884,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), "acpi_mediated_access" => acpi_mediated_access(), + "acpi_supply_outlives_holder" => acpi_supply_outlives_holder(), "machine_shutdown_short_stop" => power::machine_shutdown_short_stop(test_config), "bar_map_again" => bar_map_again(test_config), other => Err(format!("unknown machine test {other}")), @@ -3910,9 +3972,8 @@ const T14_S5_SUPPLIED: &str = "power: S5 is PM1a 0x1804 with SLP_TYPa=7,"; /// there, each fetched through `SYS_ACPI`, summing to zero as its firmware /// sealed it, and loaded; `\_S5`'s `SLP_TYPa` handed to the kernel, which /// says it powers this machine off with that value on its own PM1a block, -/// both as [`T14_S5_SUPPLIED`] holds them, and never does on this row; no -/// word of the server's that this machine has no power-off; and nothing -/// refused, so no bridge answered +/// both as [`T14_S5_SUPPLIED`] holds them, and never does on this row; and +/// nothing refused, so no bridge answered /// what the interpreter refuses, no address was `Unmapped`, and no access /// the load makes is one the policy keeps from it. The load's AML read /// memory, read configuration space and took the Global Lock, which is the @@ -3934,9 +3995,6 @@ fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { return Err(format!("the server died: {fired}")); } let blocks = power::acpi_tables_loaded(&log, &kernel, T14_S5_SUPPLIED)?; - if let Some(none) = lines.iter().find(|l| l.contains(power::ACPI_NO_POWER_OFF)) { - return Err(format!("the server handed the kernel no sleep type: {none}")); - } if blocks != T14_DEFINITION_BLOCKS { return Err(format!("the server found {blocks} definition blocks where Linux loads {T14_DEFINITION_BLOCKS}")); } diff --git a/userland/acpiserver/src/aml.rs b/userland/acpiserver/src/aml.rs index b09b4c5c6d3..9d6e2f57814 100644 --- a/userland/acpiserver/src/aml.rs +++ b/userland/acpiserver/src/aml.rs @@ -16,10 +16,11 @@ //! //! Then `\_S5` is evaluated (ACPI 6.5 §7.4.2, "\_Sx (System States)") and //! its `SLP_TYPa` handed to the kernel, which powers the machine off with it -//! and has no other source of it. **A machine whose `\_S5` was not handed -//! over has no power-off**, whatever kept it — tables unread, a DSDT refused, +//! and has no other source of it. **A server that handed none over leaves +//! the kernel what an earlier holder of the claim supplied, and no power-off +//! where none did**, whatever kept it — tables unread, a DSDT refused, //! an `\_S5` that is no package of two integers, a value the register does -//! not hold — and that is said once, loudly ([`NO_POWER_OFF`]); the server +//! not hold — and that is said once, loudly ([`NO_S5_HANDED`]); the server //! goes on serving. Nothing is evaluated after it yet, so the namespace is //! not kept: no embedded-controller query is served. //! @@ -92,8 +93,8 @@ const STOPPING: &str = "acpiserver: the machine is stopping, so the tables' load /// What is said, at error severity, of a machine whose `\_S5` the kernel was /// not handed. -pub const NO_POWER_OFF: &str = - "acpiserver: this machine has no power-off: no \\_S5 was handed to the kernel, which refuses a shutdown without one"; +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`. @@ -114,7 +115,7 @@ pub fn load(kernel: &K, rsdp: u64) -> Loaded { tables.last_refused().map_or(String::new(), |refusal| format!("; the last read refused was {refusal}")) ); println!("{OWN}that was {why:x?}"); - toyos::error!("{NO_POWER_OFF}"); + toyos::error!("{NO_S5_HANDED}"); return loaded; } }; @@ -207,7 +208,7 @@ pub fn load(kernel: &K, rsdp: u64) -> Loaded { } } if !loaded.handed { - toyos::error!("{NO_POWER_OFF}"); + toyos::error!("{NO_S5_HANDED}"); } if !host.refused.is_empty() { println!("acpiserver: refused so far, of accesses: {}", host.refused.counts()); diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index 111d4c2443d..720b9dba827 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -256,7 +256,7 @@ impl Server { println!("acpiserver: the power button was pressed, on SCI {} of this boot; asking the supervisor to power off", self.scis); let refused = power::stop(Stop::Shutdown); match sci::unstopped(refused, self.power_off) { - Unstopped::Dropped => toyos::error!("acpiserver: the press is dropped: {}", aml::NO_POWER_OFF.trim_start_matches("acpiserver: ")), + Unstopped::Dropped => toyos::error!("acpiserver: the press is dropped: {}", aml::NO_S5_HANDED.trim_start_matches("acpiserver: ")), Unstopped::Defect => panic!("acpiserver: the power-off was refused: {refused:?}"), } } From 11031d4c79e12e7996074ef9d1adb3eb1a3afa01 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 13:10:48 +0200 Subject: [PATCH 4/5] The console image's issue records its three hand boots, and that none is made again Booted by hand three times on 2026-10-08, in the build requests at 22f7125ce, 52ef7a10f and 10a85ae49, each showing both hand-over lines and each opening QEMU's window on the development machine, which the owner forbids. Until a headless harness path exists the image's build is its only measurement. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...no-harness-path-boots-the-console-image.md | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/issues/no-harness-path-boots-the-console-image.md b/issues/no-harness-path-boots-the-console-image.md index 4037d499b21..4e3bfbcd6c7 100644 --- a/issues/no-harness-path-boots-the-console-image.md +++ b/issues/no-harness-path-boots-the-console-image.md @@ -12,13 +12,19 @@ until the `acpi` claim's holder has handed it `\_S5`'s sleep type. No guest test and no metal row boots that config: deleting `acpiserver` from its `[boot] start` reds nothing, and the image then refuses every `shutdown`. -What holds today is a boot made by hand, once, at `52ef7a10f`: `cargo run -- ---console-boot --build-only` exit 0 with `bin/acpiserver` on ROOT, and `cargo -run -- --console-boot` whose console carried `acpiserver: \_S5 handed to the -kernel: SLP_TYPa=0` and `power: S5 is PM1a 0x604 with SLP_TYPa=0, as the acpi -claim's holder supplied it`, with no `panicked` line. Its `shutdown` was not -typed: the shell is on the framebuffer. The rest of the path, from a `shutdown` asked to -QEMU's `guest-shutdown`, is `machine_shutdown`'s on `tests/testcases`. +What holds today is the image's build (`cargo run -- --console-boot +--build-only` exit 0 with `bin/acpiserver` on ROOT) and three boots made by +hand on 2026-10-08, in the build requests at `22f7125ce`, `52ef7a10f` and +`10a85ae49`: `cargo run -- --console-boot`, QEMU ended once the hand-over was +said. Each console carried `acpiserver: \_S5 handed to the kernel: +SLP_TYPa=0` and `power: S5 is PM1a 0x604 with SLP_TYPa=0, as the acpi claim's +holder supplied it`; the logs of the last two are kept, and the first is the +orchestrator's count from its own run log. Each opened QEMU's window on the +development machine, which the owner forbids: no such boot is made again +until a headless harness path exists, so until then nothing but the build +holds this image. Its `shutdown` was never typed: the shell is on the +framebuffer. The rest of the path, from a `shutdown` asked to QEMU's +`guest-shutdown`, is `machine_shutdown`'s on `tests/testcases`. ## Owner From 779330742a901a6d8f2a27e800de9aa1999484c8 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 14:24:37 +0200 Subject: [PATCH 5/5] The track issue carries the owner's ruling on a sleep type that outlives its holder Asked whether the supplied value survives the server dying or is withdrawn with its claim, the owner chose "Keep it". The issue said the choice was the slice's and his to overrule. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...s-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md index f2a4ff27689..b768251212e 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 @@ -315,8 +315,9 @@ mediated access, leaves open: firmware's bytes chose. The holder cannot cause the write, set `SLP_EN` or reach a bit outside 12:10 (`toyos_userbound::firmware::SleepType`), and supplies one value under a claim. What it supplied outlives it, so a - machine whose server died still powers off: the slice's choice, which the - owner may overrule for a value withdrawn with its claim. Owner: this + machine whose server died still powers off: the owner's "Keep it" + (2026-10-08), asked whether the supplied value survives the server dying + or is withdrawn with its claim. Owner: this stage. **Exit**: it stays while "one path" stands, which leaves the kernel no second reading; it goes when the owner rules one in, and a guest test then supplies a sleep type that is not the machine's and reads it refused.