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
42 changes: 42 additions & 0 deletions issues/the-power-offs-give-back-line-is-in-no-black-box-tail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
status: open
kind: defect
opened: 2026-10-08
---

# What the power-off's settle logs is committed after the black box's tail is sealed

`quiesce` (`kernel/src/syscall/machine.rs`) says the boot's last word, drains
the console and seals the newest records onto the black box's page
(`log::seal_tail`) before `power::shutdown` is entered. `shutdown` then calls
`arch::power::settle`, whose `acpi_mode::settle` logs `acpi: the Global Lock
given back for a holder that left it taken (the machine is stopping)` where a
holder was stopped holding the lock. That record is committed below the seal:
it is on the console, which `shutdown` drains after it, and in no page tail.
On the console it follows `Shutting down.`, which `quiesce` calls the boot's
last word.

So on a machine with no serial port no reader keeps the line: `/log`'s writer
was stopped with the rest of userland, `flush_final` has no wire to drain to,
and the page was sealed without it. The T14 is such a machine, and one where
the kernel writes `ACPI_ENABLE` itself and a holder can be stopped holding
the lock.

What that costs today is narrow. A power-off that takes leaves nothing for a
next loader to read: S5 keeps no memory context by ACPI's definition (the
specification's, not a reading of the T14's DIMMs), and the page is DRAM. The
tail is read after a power-off only where the write did not take: `S5 did not
take`, a panic and then a reset.

Not moved in #767, which put the settle above the console's drain: the seal
is `quiesce`'s, shared by the reboot, which settles nothing; it stands above
`xhci::seal_shut`, which must be the last thing `quiesce` does, and the
settle needs the `Stopping` and runs below it. Sealing after the settle, or
settling above the last word, is a second change of the stop's order.

Owner: the `acpi` claim's author (#749).

**Exit**: no record is committed to the ring below `log::seal_tail` on either
end, read in `quiesce`, `power::shutdown` and `power::reboot`; and
`kernel/src/power.rs`'s header sentence then names the page beside the
console.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
status: open
status: assigned
kind: defect
opened: 2026-10-08
---
Expand Down Expand Up @@ -55,3 +55,26 @@ Owner: the `acpi` claim's author (#749).
**Exit**: `acpi_mediated_access` green in the nightly's `tcg / suite`, with the
give-back's line on the wire before `SLP_EN` by construction and not by
`klogd` winning; and a stall of this test carrying what the guest said.

## What landed, and what is still owed

Everything above is `main` before #767 and is kept as it was written. #767
split the architecture's power-off in two: `arch::power::settle` says the
give-back's line, `power::shutdown` drains the console after it, and
`arch::power::off` takes the value only `settle` makes and logs nothing. The
exit's second and third clauses are met by that pull request: the order is
`kernel/src/power.rs`'s `shutdown`, and the test's stall appends what the
guest said since its boot.

The reading above has since been run. On one CPU no other CPU runs `klogd`
beside the power-off, and the line never arrived with the split reverted,
three boots of three, each capture ending at `Shutting down.`; with it the
line arrived, three of three. `acpi_lock_given_back_on_one_cpu`
(`tests/toyos.rs`) is that boot.

Its first clause is not met: no nightly has run `tcg / suite` on a `main`
that carries #767.

Assigned: the orchestrator, at #767's landing. He reads `acpi_mediated_access`
in the first nightly `tcg / suite` on a `main` that carries it, and deletes
this file on a green; a stall there prints the guest's words, and goes here.
10 changes: 9 additions & 1 deletion kernel/src/arch/aarch64/power.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,20 @@ pub fn reset() -> ! {
cpu::halt()
}

/// What [`off`] takes: this machine hands out nothing a power-off takes back.
pub struct Settled(());

/// Nothing to settle, and so nothing said.
pub fn settle(_stopping: crate::quiesce::Stopping) -> Settled {
Settled(())
}

/// `SYSTEM_OFF`, once every other CPU the roster holds has turned itself off
/// with `CPU_OFF` and PSCI answers it off: the caller puts every core in a
/// known state first, and this is DEN0022 §5.10.3's own way to. The budget
/// for all of them is [`DEAF_CPU`]'s span from the SGI: a CPU PSCI still
/// answers on at its end is named, and the machine powers off regardless.
pub fn off(_stopping: crate::quiesce::Stopping) -> ! {
pub fn off(_settled: Settled) -> ! {
cpu::disable_interrupts();
let Some(psci) = psci::conduit() else { cpu::halt() };
irqchip::off_all_but_self();
Expand Down
23 changes: 18 additions & 5 deletions kernel/src/arch/x86_64/power.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use toyos_acpi::{Reset, Table, TableError, S5, SDT_HEADER_LEN, SDT_REVISION};
use toyos_userbound::{Mediated, Ports};

use super::cpu;
use super::pio::{self, Declared, Slot};
use super::pio::{self, Declared, Slot, TakenBack};
use crate::drivers::acpi::direct_phys;
use crate::log;
use crate::time::{Deadline, Duration, Tripwire};
Expand Down Expand Up @@ -153,8 +153,24 @@ const S5_TAKES: Tripwire = Tripwire::absurd(
this kernel two seconds later never entered it",
);

/// The hardware taken back for the power-off, with everything [`settle`]
/// says said: what [`off`] takes, so the log's last drain stands between.
pub struct Settled(TakenBack);

/// Take the hardware back from whoever was handed it, waiting out a write to
/// `SMI_CMD` and an act for the `acpi` claim's holder in flight, and give
/// back a Global Lock that holder was stopped holding.
pub fn settle(stopping: crate::quiesce::Stopping) -> Settled {
let taken = pio::take_back(&stopping);
super::smi_cmd::settle(&taken);
super::acpi_mode::settle(&taken);
Settled(taken)
}

/// Enter S5, or halt on a machine whose tables named no soft-off.
///
/// **Nothing here logs**: the log's last drain is behind it (`power::shutdown`).
///
/// 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
/// (`acpi_mode::quiet`), so no event pending at the write wakes it again;
Expand All @@ -166,12 +182,9 @@ const S5_TAKES: Tripwire = Tripwire::absurd(
/// the panel shows it, and the black box carries it through the panic's reset,
/// 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) -> ! {
pub fn off(Settled(taken): Settled) -> ! {
let (Some(control), true) = (PM1A_CNT.get(), SOFT_OFF.load(Ordering::Acquire)) else { cpu::halt() };
let control = control.port(0);
let taken = pio::take_back(&stopping);
super::smi_cmd::settle(&taken);
super::acpi_mode::settle(&taken);
let held = cpu::inw(control);
if held & SCI_EN != 0 {
super::acpi_mode::quiet(&taken);
Expand Down
16 changes: 14 additions & 2 deletions kernel/src/power.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
//! than of whoever asked for one, and what a third caller gets without knowing
//! it is owed. Resets this kernel does not perform — a TCO or firmware
//! watchdog, a triple fault, power loss — are outside it and always will be.
//!
//! **Nothing an end says is logged after the console's last drain.**
//! `serial::flush_final` is the log ring's last reader for the console: a
//! record committed past it reaches the wire only if `klogd`, on another CPU,
//! beats the register write. What an end says through the ring it says above
//! the flush (`arch::power::settle`); `arch::power::off` and
//! `arch::power::reset` log nothing, and a panic in either drains for itself.
//! The black box's page is another reader and an earlier one: its tail is
//! sealed before either end is entered (`log::seal_tail`), so what `settle`
//! says is on the console and in no page.

use crate::drivers::{serial, xhci::stop};

Expand Down Expand Up @@ -49,11 +59,13 @@ pub fn reset_now() -> ! {

/// Power the machine off, or halt on one that offers no power-off.
pub fn shutdown(stopping: crate::quiesce::Stopping) -> ! {
// Last chance: nothing drains the log ring after this point.
// Above the flush, because it logs.
let settled = crate::arch::power::settle(stopping);
// Nothing drains the log ring after this point, and nothing below logs.
serial::flush_final();
// A power-off takes VBUS with it on a machine whose ports are not
// always-on and takes nothing on one whose are, so the devices are handed
// back here for the same reason as at a reboot.
stop::before_reset();
crate::arch::power::off(stopping)
crate::arch::power::off(settled)
}
20 changes: 15 additions & 5 deletions tests/toyos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ 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",
// The same boot on one CPU, where the power-off's CPU is the only one
// there is: what it logs reaches the console by the stop's own drain or
// not at all, since no other CPU runs `klogd` beside it.
"acpi_lock_given_back_on_one_cpu",
// 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.
Expand Down Expand Up @@ -1727,8 +1731,9 @@ const ACPI_MEDIATED_SAID: [&str; 8] = [
/// 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
/// 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> {
/// one whose arm failed. On `cpus` CPUs: the second one's range registers are
/// read where there is one.
fn acpi_mediated_access(cpus: u32) -> 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)";
Expand All @@ -1741,6 +1746,7 @@ fn acpi_mediated_access() -> Result<(), String> {
// The test kernel, for the Global Lock's actuator too.
kernel_params: &["i8042-withheld"],
ready_marker: "acpi: the ACPI row: ",
smp: cpus,
extra_root_files: vec![suite_bin(toyos_build::arch::Arch::X86_64, "acpi_mediated")],
..Default::default()
},
Expand All @@ -1749,15 +1755,18 @@ fn acpi_mediated_access() -> Result<(), String> {
let ended = format!("===TEST_END {JOB} ");
await_guest(&mut qemu, &mut console, "the probe's power-off to give the lock back", |said| {
said.contains(GIVEN_BACK_AT_THE_STOP) || said.contains(&ended)
})?;
})
// The probe's whole run: a wait that ends on neither line has no other account of it.
.map_err(|why| format!("{why}\nsince its boot the guest said:\n{}", &console[qemu.boot_log().len()..]))?;
let said = serial::Serial::named("the probe's boot", console);
said.must_be_clean()?;
said.must_say(isa::WITHHELD)?;
said.must_say("acpi: the Global Lock is the FACS's at ")?;
said.must_say("acpi: the Global Lock given back for a holder that left it taken (its claim is gone)")?;
// Whose range registers passed the unlisted read, and how this
// hypervisor's second CPU holds its own beside them.
for line in ["mtrr: the boot processor's range registers: ", "mtrr: cpu1's range registers are "] {
let mtrr = ["mtrr: the boot processor's range registers: ", "mtrr: cpu1's range registers are "];
for line in mtrr.iter().take(cpus as usize) {
eprintln!(" [acpi] {}", said.must_say(line)?.trim());
}
for line in ACPI_MEDIATED_SAID {
Expand Down Expand Up @@ -2802,7 +2811,8 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
"nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
"machine_shutdown" => power::machine_shutdown(test_config),
"acpi_power_button" => power::acpi_power_button(test_config),
"acpi_mediated_access" => acpi_mediated_access(),
"acpi_mediated_access" => acpi_mediated_access(2),
"acpi_lock_given_back_on_one_cpu" => acpi_mediated_access(1),
"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}")),
Expand Down
Loading