Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,21 @@ in ACPI mode; that no T14 row reads it is
Moving it out of this exit is the orchestrator's placement, not the owner's.

**Ruled** (owner, 2026-10-04, "Stopgap, delete later"): "Stage 1 uses the
extra table so the T14 switches to ACPI mode now." Stage 1 reads the
embedded controller from the ECDT, and that path is a stopgap: it is deleted
the day the interpreter reads the controller from the DSDT's own device. A
machine without an ECDT stays in legacy mode until then; such a machine keeps
its firmware interrupts, and this stage's exit cannot be met on it.
extra table so the T14 switches to ACPI mode now." Stage 1 read the
embedded controller from the ECDT as that stopgap, to be deleted the day the
interpreter reads the controller from the DSDT's own device. It is deleted:
the server finds the PNP0C09 device in the namespace its load kept and takes
its ports from `_CRS` and its GPE from `_GPE`, and reaches the ports through
the kernel's mediated access, since no row names them
(`userland/acpiserver/src/devices.rs`); the kernel reads no ECDT.

Stage 1's design, not a ruling: a machine whose power button is a control
method device stays in legacy mode too, refused by name; and a machine its
firmware hands over in ACPI mode is served whatever it has, since nothing is
written.
The design, not a ruling: a machine whose power button is a control method
device is claimed too, and its button served by the Notify the controller's
query method makes of it (ACPI 6.5 §4.8.2.2.1.2), which the server runs for
every query only on such a machine and only once it found the button; a
button raised by a GPE's own `_Lxx` or `_Exx` is not served, since the
server enables no GPE but the controller's. A machine its firmware hands
over in ACPI mode is served whatever it has, since the mint writes nothing.

**Stage: the interpreter** (the orchestrator's placement of "The AML stage
closes it"). ToyOS's own AML interpreter, written from the specification, run
Expand Down Expand Up @@ -283,16 +288,29 @@ mediated access, leaves open:
power-off is made before the load ends, and what bounds the refusal is the
load's time. That time is held on the T14 once the `acpi_tables_loaded` row
reads it under a bound the owner names.
- **A machine with no holder of the `acpi` claim has no power-off.** The
kernel reads no AML, so a machine whose claim it refuses, one in legacy
mode with no ECDT or with a control-method power button, has nobody to
evaluate `\_S5`: `SYS_SHUTDOWN` is refused there, where the kernel's own
scan of the DSDT powered such a machine off before. Neither the T14 nor q35
is one: the T14 has an ECDT and a fixed button, and OVMF hands q35 over in
ACPI mode. Owner: this stage. **Exit**: the ECDT stopgap is deleted, so a
machine is claimed for what its DSDT names; what a machine with a
control-method button does for a power-off is ruled with that button's
device.
- **A control-method power button's press powers the machine off unruled.**
The claim is no longer refused for a missing ECDT or a control-method
button, so such a machine has a server to evaluate `\_S5`, and the
server's design makes the button's Notify a press, which stops the
machine through the one path. What such a machine does for a power-off
was to be "ruled with that button's device", and the owner has not ruled
it. Owner: this stage. **Exit**: the owner rules it, and the server does
what he rules.
- **A control-method button the server does not serve is dead in ACPI
mode.** The kernel puts the machine in ACPI mode when it mints the claim,
before the server has read whether it serves the button. Where it then
serves none, because no namespace was kept, no controller is served, no
present PNP0C0C was found or the button's `_Qxx` is refused, a short press
reaches nothing; in legacy mode the firmware served it, and before #845
the kernel refused such a machine the claim and left it there. The server
says each of those by name. Owner: this stage. **Exit, met**: the owner
ruled (2026-10-10), asked "When ToyOS switches a machine into ACPI mode but
its ACPI server can't load the firmware's tables (so it can't handle a
power button the firmware reports through ACPI code), what should
happen?": "Stay in ACPI mode, button dead". The host test
`no_namespace_leaves_the_button_and_the_controller_unserved_and_keeps_the_claim`
(`userland/acpiserver/src/main.rs`) reads the server serving neither the
button nor the controller and keeping the claim.

Two more machines have no power-off, and that is the ruled state and no
weakness with an exit. A boot whose config starts no server: "Power-off
Expand Down Expand Up @@ -324,9 +342,23 @@ mediated access, leaves open:
then supplies a sleep type that is not the machine's and reads it refused.

What the firmware call, which the kernel makes for the server where its AML
stores a byte to `SMI_CMD`, leaves open. The server's AML makes none yet: its
host denies every write AML asks for and passes none to the kernel
(`userland/acpiserver/src/host.rs`).
stores a byte to `SMI_CMD`, leaves open. No AML the server runs makes one
now: its host asks the kernel for three port writes and denies every other
write by name (`FORWARDED` in `userland/acpiserver/src/host.rs`), the
index registers 0x72 and 0xCD6, a byte each, and the POST port's word.
Those are what an AMD laptop's AML wrote, measured outside the tree on its
tables with nothing cleared between the load, the device search and all
102 of its query methods (#845): its DSDT writes both indexes at its load
and is refused without either, the search writes nothing, and the queries
write only the POST port.

- **A port write outside the three ends what made it.** A machine whose
AML writes another port at its load has that table refused by name, and
a method that does has its evaluation refused. The T14's load writes
none: it loaded with every write denied. Owner: this stage. **Exit**: a
machine's reading of a write outside the three, which the list takes or
the owner refuses, and a slice that runs a method which stores to
`SMI_CMD` takes the call with the readings below.

- **What a call does there is the firmware's**, and the kernel bounds who,
when, where, which byte and how often:
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/arch/aarch64/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ pub fn timer() {

/// The platform's own devices that are not PCI functions: none this kernel
/// drives on an ACPI Arm machine.
pub fn platform_devices(_rsdp_addr: u64) {}
pub fn platform_devices(_platform: &Platform, _rsdp_addr: u64) {}

/// Every other CPU, running.
pub fn start_other_cpus(platform: &Platform, _args: &KernelArgs) {
Expand Down
86 changes: 20 additions & 66 deletions kernel/src/arch/x86_64/acpi_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,16 @@
//! disable again. Neither is written once the stop has begun: `smi_cmd`
//! refuses it.
//!
//! **A machine stays in legacy mode where its firmware serves something no
//! holder could**: an embedded controller the ECDT does not name, or a power
//! button that is a control method device, which only AML serves. Both are
//! refused at the mint, by name; a machine the firmware handed over in ACPI
//! mode is claimed whatever it has, since nothing is written.
//! **What only the firmware's AML names is the holder's to find**: the
//! embedded controller is a device of the DSDT, and a power button that is a
//! control method device is served by the AML that notifies it. This kernel
//! reads no AML, so neither keeps a machine in legacy mode; the controller's
//! ports are the holder's to reach as [`access`] reaches any port no row
//! names, and no table this kernel reads names them.
//!
//! **The ECDT is a stopgap**: the embedded controller is read from it until the
//! interpreter reads the controller's own device from the DSDT, and then this
//! path and `toyos_acpi::ecdt` go
//! (`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`).
//!
//! The row is the FADT's PM1a event and GPE0 blocks and the ECDT's two
//! ports, filled once at boot; the SCI is its one line, level.
//! The row is the FADT's PM1a event and GPE0 blocks, filled once at boot;
//! the SCI is its one line, level. `SMI_CMD` is declared before it, with the
//! reset register (`power::init_reset`).
//!
//! **What the firmware's AML names outside the row, this kernel reads and
//! writes for the claim's holder, one access at a time** ([`access`]): memory
Expand Down Expand Up @@ -83,7 +80,7 @@ use core::sync::atomic::AtomicU32;

use toyos_abi::acpi::{Access, AcpiInfo, Block, Refused, Space, Width, FIXED_POWER_BUTTON};
use toyos_abi::syscall::SyscallError;
use toyos_acpi::{Ec, EcamWindow, FixedHardware, LegacyMode, PowerButton};
use toyos_acpi::{EcamWindow, FixedHardware, LegacyMode, PowerButton};
use toyos_userbound::firmware::{
self, CallRate, FirmwareCall, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict,
};
Expand Down Expand Up @@ -117,8 +114,6 @@ const HANDBACK: Duration = Duration::from_millis(100);
struct Hardware {
fixed: FixedHardware,
control: Declared,
/// Or why it is none a holder can be handed.
ec: Result<Ec, String>,
rsdp: u64,
/// The windows configuration space is reached through, as the MCFG bounds them.
ecam: Vec<EcamWindow>,
Expand Down Expand Up @@ -223,10 +218,10 @@ fn hardware() -> Option<&'static Hardware> {
(!at.is_null()).then(|| unsafe { &*at })
}

/// Decode the fixed hardware, declare `SMI_CMD`, and fill the row; or say by
/// name why this machine has none. After the i8042's row, whose lines the
/// SCI may not share.
pub fn init(rsdp_addr: u64) {
/// Decode the fixed hardware and fill the row; or say by name why this
/// machine has none. After the i8042's row, whose lines the SCI may not
/// share; `facs` is what `power::init_reset` read of the FACS.
pub fn init(rsdp_addr: u64, facs: Option<Result<toyos_acpi::Facs, toyos_acpi::FacsRefused>>) {
let fadt = match toyos_acpi::find_table(
crate::drivers::acpi::direct_phys(),
rsdp_addr,
Expand All @@ -243,17 +238,9 @@ pub fn init(rsdp_addr: u64) {
let Some(control) = super::power::pm1a_control() else {
return log!("acpi: no ACPI row — no PM1a control block declared");
};
let facs = toyos_acpi::facs(fadt.phys(), &fadt);
// A FACS this kernel cannot read leaves `S4BIOS_F` unread, and the byte kept.
let s4bios = match facs {
Ok(facs) => facs.s4bios,
Err(toyos_acpi::FacsRefused::Absent) => false,
Err(_) => true,
};
if let Some(Err(why)) = fixed.smi_cmd.map(|named| smi_cmd::declare(named.port, named.named(s4bios))) {
return log!("acpi: no ACPI row — SMI_CMD not declared: {why:?}");
if fixed.smi_cmd.is_some() && smi_cmd::declared().is_none() {
return log!("acpi: no ACPI row — the FADT's SMI_CMD was not declared");
}
let ec = embedded_controller(rsdp_addr, fixed.gpe0);
let Some(sci) = super::ioapic::sci(fixed.sci_int) else {
return log!("acpi: no ACPI row — no I/O APIC carries the SCI");
};
Expand All @@ -265,11 +252,8 @@ pub fn init(rsdp_addr: u64) {
if fixed.gpe0.len != 0 {
runs.push(run(fixed.gpe0));
}
if let Ok(ec) = &ec {
runs.extend([Ports::one(ec.command), Ports::one(ec.data)]);
}
log!(
"acpi: the ACPI row: PM1a events {:#x}+{}, GPE0 {:#x}+{}, SCI {}, {}, embedded controller {}; the firmware handed over in {} mode",
"acpi: the ACPI row: PM1a events {:#x}+{}, GPE0 {:#x}+{}, SCI {}, {}; the firmware handed over in {} mode",
fixed.pm1a_event.port,
fixed.pm1a_event.len,
fixed.gpe0.port,
Expand All @@ -279,15 +263,12 @@ pub fn init(rsdp_addr: u64) {
PowerButton::Fixed => "the fixed-hardware power button",
PowerButton::ControlMethod => "a control-method power button",
},
match &ec {
Ok(ec) => format!("at {:#x}/{:#x} on GPE {:#x}", ec.command, ec.data, ec.gpe),
Err(why) => format!("none ({why})"),
},
if cpu::inw(control.port(0)) & SCI_EN != 0 { "ACPI" } else { "legacy" },
);
isa::fill(ROW, Function { name: "the ACPI fixed hardware", runs, irqs: vec![], wires: vec![sci] });
let facs = facs.expect("power::init_reset read the FACS of the FADT this decoded");
let (ecam, lock) = (crate::drivers::pci::windows(), global_lock(facs));
let hardware = Hardware { fixed, control, ec, rsdp: rsdp_addr, ecam, lock };
let hardware = Hardware { fixed, control, rsdp: rsdp_addr, ecam, lock };
let was = HARDWARE.swap(Box::into_raw(Box::new(hardware)), Ordering::Release);
assert!(was.is_null(), "acpi: init ran twice");
}
Expand Down Expand Up @@ -326,17 +307,6 @@ fn run(block: Block) -> Ports {
Ports::new(block.port, block.len).expect("`fixed_hardware` bounded every block by the port space")
}

fn embedded_controller(rsdp_addr: u64, gpe0: Block) -> Result<Ec, String> {
let ecdt = toyos_acpi::find_table(crate::drivers::acpi::direct_phys(), rsdp_addr, b"ECDT", toyos_acpi::ECDT_NEEDED)
.map_err(|e| format!("the ECDT is unusable: {e:?}"))?;
let ec = toyos_acpi::ecdt(&ecdt).map_err(|refused| format!("the ECDT names none: {refused:?}"))?;
// §5.2.9: a GPE block's status half holds eight GPEs a byte.
if u16::from(ec.gpe) >= gpe0.len / 2 * 8 {
return Err(format!("the ECDT puts it on GPE {:#x}, outside GPE0's {}", ec.gpe, gpe0.len / 2 * 8));
}
Ok(ec)
}

/// The row, claimed, with the machine in ACPI mode; or the refusal that
/// leaves it in legacy mode, said by name.
pub fn claim() -> Result<(isa::Row, AcpiInfo), ClaimError> {
Expand All @@ -348,19 +318,12 @@ pub fn claim() -> Result<(isa::Row, AcpiInfo), ClaimError> {
}

fn info(hardware: &Hardware) -> AcpiInfo {
let (ec_command, ec_data, ec_gpe) = match &hardware.ec {
Ok(ec) => (Block { port: ec.command, len: 1 }, Block { port: ec.data, len: 1 }, u16::from(ec.gpe)),
Err(_) => (Block::NONE, Block::NONE, 0),
};
AcpiInfo {
rsdp: hardware.rsdp,
pm1_event: hardware.fixed.pm1a_event,
gpe0: hardware.fixed.gpe0,
ec_command,
ec_data,
ec_gpe,
flags: if hardware.fixed.power_button == PowerButton::Fixed { FIXED_POWER_BUTTON } else { 0 },
reserved: 0,
reserved: [0; 3],
}
}

Expand Down Expand Up @@ -392,15 +355,6 @@ fn enter(hardware: &Hardware) -> Result<(), ClaimError> {
return refuse("the FADT names no SMI_CMD, ACPI_ENABLE and ACPI_DISABLE to leave it and come back with");
};
let enable = legacy.acpi_enable.get();
if let Err(why) = &hardware.ec {
return refuse(&format!(
"its firmware serves an embedded controller no holder could ({why})"
));
}
if hardware.fixed.power_button == PowerButton::ControlMethod {
return refuse("its power button is a control method device, which only AML serves");
}

let Some(write) = smi_cmd::write(enable) else {
return refuse("the machine is stopping");
};
Expand Down
10 changes: 6 additions & 4 deletions kernel/src/arch/x86_64/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ pub fn reserved() -> Region {
/// What the boot learns bringing interrupts up and hands later steps.
pub struct Platform {
madt: MadtInfo,
/// The FACS as `power::init_reset` read it, for `acpi_mode::init`.
facs: Option<Result<toyos_acpi::Facs, toyos_acpi::FacsRefused>>,
}

/// Interrupt delivery, this CPU's per-CPU block and the syscall gate.
Expand All @@ -87,14 +89,14 @@ pub fn interrupts(rsdp_addr: u64) -> Platform {
// Off the same tables as the MADT, and before the IDT below makes a panic
// reportable: a panic that can be reported but not ended leaves the machine
// holding its panel for a hand that may not be in the room.
super::power::init_reset(rsdp_addr);
let facs = super::power::init_reset(rsdp_addr);
apic::init();
percpu::init_bsp(apic::id());
super::power::init_control(rsdp_addr);
ioapic::init(&madt);
idt::enable_interrupts();
super::syscall::init();
Platform { madt }
Platform { madt, facs }
}

/// The clock: the TSC, calibrated against the HPET, and the CMOS wall clock.
Expand Down Expand Up @@ -132,10 +134,10 @@ pub fn timer() {
}

/// The platform's own devices that are not PCI functions.
pub fn platform_devices(rsdp_addr: u64) {
pub fn platform_devices(platform: &Platform, rsdp_addr: u64) {
super::i8042::init(rsdp_addr);
super::pio::fill_i8042_row();
super::acpi_mode::init(rsdp_addr);
super::acpi_mode::init(rsdp_addr, platform.facs);
}

/// Every other CPU, running.
Expand Down
Loading
Loading