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
@@ -0,0 +1,48 @@
---
status: open
kind: tooling
opened: 2026-10-01
---

# Nothing fails without the timer stop before `CPU_OFF`

`kernel/src/arch/aarch64/power.rs`'s `cpu_off` stops the CPU's timer before
it calls `CPU_OFF`. The reason is DEN0022 §5.5.2: an interrupt that reaches a
core `CPU_OFF` turned off is an erroneous state. That section is where the
requirement comes from. It is not evidence that this kernel meets it, and no
test can fail if the stop is deleted:

- QEMU's PSCI is the only provider any guest reaches, and it delivers nothing
to a vCPU it holds off.
- A second provider, TF-A under QEMU, needs a C cross-toolchain, which is not
a declared host tool.
- No AArch64 metal target exists. By the owner's ruling in
`issues/kernel/toyos-runs-on-arm64.md`, no ARM hardware is a target.
- `SGI_OFF` lands on idle CPUs. An idle CPU with no parked deadline has
already stopped its one-shot (`toyos-sched/src/timer.rs:43`), so deleting
the stop changes nothing even on metal in that case.

The stop serves a wider claim: no interrupt reaches a core `CPU_OFF` turned
off. That claim also covers a kick from a CPU that is not off yet, and the
stop does nothing about kicks.

**Evidence**: the deletion mutation, applied at `94dea677c` and reversed
after the run:

```diff
--- a/kernel/src/arch/aarch64/power.rs
+++ b/kernel/src/arch/aarch64/power.rs
@@ pub(super) fn cpu_off() -> ! {
- irqchip::stop_timer();
```

`cargo test --test toyos-build -- virt_` exited 0 with it applied: `19
passed, 19 total`, the power-off of eight CPUs in `virt_smp`, `virt_el1_smp`
and `virt_off_names_the_cpus_left_on` among them. The patch and the run are
https://github.com/ToyOSOrg/ToyOS/pull/647#issuecomment-5956924013.

**Exit**: an AArch64 metal target where `SGI_OFF` lands on a CPU whose timer
is armed, with the deletion mutation red there.

Owner: `kernel/src/arch/aarch64/power.rs`'s `cpu_off`, under
`issues/kernel/toyos-runs-on-arm64.md`.
7 changes: 1 addition & 6 deletions issues/kernel/toyos-runs-on-arm64.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ bound). Heaviest: `arch::cpu` (57 references), `arch::percpu` (28),

- **Port I/O** (`cpu::inb/outb/inw/outw`): `rtc.rs:203-204`,
`drivers/serial.rs` (16 sites), `drivers/i8042/mod.rs` (12 sites),
`drivers/acpi.rs:325,345` (reset and PM1a soft-off),
`drivers/watchdog.rs:84-158`, `bootloader/src/watchdog.rs:195,204`.
- **APIC / IOAPIC / MSI.** `hw.rs:1-5`: "Everything here is x2APIC, TSC or a
single instruction." The MSI doorbell `0xFEE0_0000` is hardcoded three
Expand Down Expand Up @@ -293,11 +292,7 @@ Each stage names its exit; "measured" means a number from a run.
`toyos_elf::RelocError::TlsDescriptor` for a library).
**Every CPU starts, ahead of small-kernel stage 6 by the owner's word, as
stage 4 did:** it ports no device interrupt, so nothing of the relay.
Owed before the exit holds:
`SYSTEM_RESET`, `SYSTEM_OFF` and `CPU_OFF` behind a reset and power-off
seam that takes x86-64's reset register and PM1a out of
`drivers/acpi.rs`, and the stop shown on eight CPUs ending in that
power-off; the TLS-descriptor resolver;
Owed before the exit holds: the TLS-descriptor resolver;
`issues/kernel/the-crash-evidence-records-x86-fault-registers.md`; the
blocked-task dump's probe of a CPU that ignored its kick
(`sched/dump.rs`'s `probe_silent`), which reaches `irqchip::send_nmi`'s
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
status: open
kind: tooling
opened: 2026-10-02
---

# Nothing judges that an AArch64 panic resets the machine at its bound

Since PSCI gave AArch64 a reset, a panic there after `psci::init` arms
`panic_reboot`'s bound, where it used to hold the panel, and at the bound's
end `reboot_now` calls `SYSTEM_RESET`. No test waits for that reset.
`virt_fatal_halts_the_others_first` holds the arm line, `panic: rebooting in
60 s, timed by`, and ends there; `virt_reboot` judges `SYSTEM_RESET` from the
syscall, not from a panic.

A test is one boot and costs the shipped bound, `toyos_tco::PANIC_BOUND_MS`,
60 s of every suite run, on a suite that takes 45 s. The actuator that
shortened the bound for a judge, `panic-reboot-fast`, went with the x86-64
tests that used it, and AArch64 has no metal row to move this to.

**Evidence**: the measurement, once, at `94dea677c`, as a test added by a
patch and removed after the run: `test-late-panic` on `virt` at EL2, waited to
QEMU's stop. `cargo test --test toyos-build -- virt_scout` exited 0:

```
[kernel 1.557 cpu0] panic: rebooting in 60 s, timed by the calibrated clock
[scout] QEMU's stop reason Some("guest-reset"), 60.0s after the ready marker was read
[scout] said after the ready marker: Ok("\npanic: the bound is over: returning this machine to firmware\n")
[scout] CPU_OFF, SYSTEM_OFF and SYSTEM_RESET calls traced: [(84000009, 0)]
```

The patch is `s0-scout-panic-resets.patch` in
https://github.com/ToyOSOrg/ToyOS/pull/647#issuecomment-5956924013.

**Exit**: that patch's test as a `virt_` row, red when `reboot_now` halts
instead of resetting, once one boot can shorten the bound, or on the owner's
word that the row is worth its 60 s.

Owner: `kernel/src/panic_reboot.rs`, under
`issues/kernel/toyos-runs-on-arm64.md`.
13 changes: 13 additions & 0 deletions kernel/src/actuator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,19 @@ actuators! {
/// Skip the startup for the AP that would be cpu2, so a non-last AP never starts.
smp_skip_ap = "smp-skip-ap";

/// The roster's last two CPUs take the power-off's SGI and halt without
/// `CPU_OFF`, so PSCI answers them on for the whole budget. Judged by
/// `virt_off_names_the_cpus_left_on`.
// PSCI is AArch64's alone, so x86-64 builds an accessor it never reads.
#[allow(dead_code)]
power_off_spares_the_last_two = "power-off-spares-the-last-two";

/// `psci::init` keeps no conduit, so this kernel has no reset: a machine
/// without PSCI. Judged by `virt_reboot_refused_without_psci`.
// PSCI is AArch64's alone, so x86-64 builds an accessor it never reads.
#[allow(dead_code)]
psci_withheld = "psci-withheld";

/// Time the same read loop on every CPU, either side of the `mov cr0` that enables caching.
control_regs_bench = "control-regs-bench";

Expand Down
9 changes: 5 additions & 4 deletions kernel/src/arch/aarch64/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,19 +328,20 @@ pub fn reserved() -> Region {
}

/// What the boot learns bringing interrupts up and hands later steps: the
/// other CPUs the MADT names, and how to start them.
/// other CPUs the MADT names.
pub struct Platform {
gic: super::irqchip::Gic,
psci: Option<super::psci::Conduit>,
}

/// Interrupt delivery: this CPU's per-CPU block, the GIC and the timer's
/// interrupt, and interrupts unmasked. The syscall gate is the vectors' own.
pub fn interrupts(rsdp_addr: u64) -> Platform {
// First, so a panic in anything below has a reset to end its bound with.
super::psci::init(rsdp_addr);
super::percpu::init_bsp();
let gic = super::irqchip::init(rsdp_addr);
super::cpu::enable_interrupts();
Platform { gic, psci: super::psci::init(rsdp_addr) }
Platform { gic }
}

/// The clock: the generic timer's count, at the rate firmware states in
Expand Down Expand Up @@ -368,7 +369,7 @@ pub fn platform_devices(_rsdp_addr: u64) {}

/// Every other CPU, running.
pub fn start_other_cpus(platform: &Platform, _args: &KernelArgs) {
super::smp::start(&platform.gic, platform.psci);
super::smp::start(&platform.gic);
}

/// The interrupt-controller selftests an actuator asks for.
Expand Down
18 changes: 15 additions & 3 deletions kernel/src/arch/aarch64/irqchip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub(super) enum Intid {
Kick = 0,
/// Stops a CPU for good: [`stop_other_cpus`]'s.
Halt,
/// Turns a CPU off for the machine's power-off: [`off_all_but_self`]'s.
Off,
/// What `irq-storm` floods this CPU with.
Storm,
Hda,
Expand All @@ -48,6 +50,7 @@ pub(super) enum Intid {

pub(super) const SGI_KICK: u32 = Intid::Kick as u32;
pub(super) const SGI_HALT: u32 = Intid::Halt as u32;
pub(super) const SGI_OFF: u32 = Intid::Off as u32;
#[cfg(feature = "boot-actuators")]
pub(super) const SGI_STORM: u32 = Intid::Storm as u32;

Expand Down Expand Up @@ -213,7 +216,7 @@ pub fn init_cpu(frame: u64) {
redistributor.write_u32(GICR_ICFGR1, edge);

stop_timer_hardware();
let enabled = 1 << SGI_KICK | 1 << SGI_HALT | 1 << timer;
let enabled = 1 << SGI_KICK | 1 << SGI_HALT | 1 << SGI_OFF | 1 << timer;
#[cfg(feature = "boot-actuators")]
let enabled = enabled | 1 << SGI_STORM;
redistributor.write_u32(GICR_ISENABLER0, enabled);
Expand Down Expand Up @@ -292,13 +295,22 @@ pub fn kick_cpu(cpu: u32) {
sgi(SGI_KICK, crate::smp::hardware_id(cpu));
}

pub fn kick_all_but_self() {
all_but_self(SGI_KICK);
}

/// Raise the power-off's SGI on every other CPU the roster holds.
pub(super) fn off_all_but_self() {
all_but_self(SGI_OFF);
}

// Each by name, not with `IRM`'s broadcast: that reaches an AP which echoed too
// late and halted with its SGIs enabled, where a kick left pending wakes the
// masked `wfi` at once, for good.
pub fn kick_all_but_self() {
fn all_but_self(intid: u32) {
let me = percpu::cpu_id();
for cpu in (0..crate::smp::cpu_count()).filter(|&cpu| cpu != me) {
kick_cpu(cpu);
sgi(intid, crate::smp::hardware_id(cpu));
}
}

Expand Down
3 changes: 3 additions & 0 deletions kernel/src/arch/aarch64/keyboard_controller.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! The platform's own keyboard controller. An Arm machine has none: its
//! keyboards are USB, and the panic panel's key poll reads nothing here.

/// Whether the panic path reads a key here.
pub const PANIC_KEYS: bool = false;

/// No byte ever waits.
pub fn poll_byte() -> Option<(u8, bool)> {
None
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/arch/aarch64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ pub mod irqchip;
pub mod keyboard_controller;
pub mod paging;
pub mod percpu;
pub mod pio;
pub mod pmu;
pub mod power;
pub mod psci;
pub mod rtc;
pub mod smp;
Expand Down
15 changes: 0 additions & 15 deletions kernel/src/arch/aarch64/pio.rs

This file was deleted.

120 changes: 120 additions & 0 deletions kernel/src/arch/aarch64/power.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! Reset and power-off: PSCI's `SYSTEM_RESET` and `SYSTEM_OFF` (Arm DEN0022
//! §5.12, §5.10), through the conduit [`super::psci`] kept. A machine without
//! PSCI has neither, and holds where either is asked for.
//!
//! Every line here goes straight to the UART, each whole under one fatal
//! path's hold of its registers ([`serial::panic_registers`]): nothing drains
//! the log ring after any of these calls. The hold is let go before a halt,
//! since a CPU halted holding it costs every later line the whole bound.

use super::{cpu, irqchip, percpu, psci};
use crate::drivers::serial;
use crate::time::{Cadence, Deadline, Duration, DEAF_CPU};

/// How often [`off`] asks again of a CPU PSCI still answers on: PSCI says a
/// CPU is off only to one that asks.
const ASK_AGAIN: Cadence = Cadence::every(
Duration::from_millis(1),
"one call into firmware per period, and a power-off at most one period after the last CPU is off",
);

pub fn can_reset() -> bool {
psci::conduit().is_some()
}

/// `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() -> ! {
if let Some(psci) = psci::conduit() {
let refusal = psci.system_reset();
let mut uart = serial::panic_registers();
uart.write(b"power: SYSTEM_RESET answered ");
say_code(&mut uart, refusal);
uart.write(b"; holding\n");
}
cpu::halt()
}

/// `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() -> ! {
cpu::disable_interrupts();
let Some(psci) = psci::conduit() else { cpu::halt() };
irqchip::off_all_but_self();
// The tripwire's span without its panic: what keeps a CPU on may be
// firmware's refusal, and firmware's word never panics this kernel.
let deadline = Deadline::at(crate::clock::now() + Duration::from_nanos(DEAF_CPU.nanos()));
let me = percpu::cpu_id();
for cpu in (0..crate::smp::cpu_count()).filter(|&cpu| cpu != me) {
let mpidr = toyos_gicv3::unpacked_affinity(crate::smp::hardware_id(cpu));
loop {
match psci.affinity_info(mpidr) {
Ok(psci::Affinity::Off) => break,
Ok(_) if !deadline.reached(crate::clock::now()) => {
let again = Deadline::at(crate::clock::now() + ASK_AGAIN.duration());
while !again.reached(crate::clock::now()) {
core::hint::spin_loop();
}
}
Ok(_) => {
let mut uart = serial::panic_registers();
uart.write(b"power: cpu");
uart.dec(u64::from(cpu));
uart.write(b" is not off by PSCI's answer inside the budget; SYSTEM_OFF regardless\n");
break;
}
Err(refusal) => {
let mut uart = serial::panic_registers();
uart.write(b"power: cpu");
uart.dec(u64::from(cpu));
uart.write(b"'s AFFINITY_INFO answered ");
say_code(&mut uart, refusal);
uart.write(b"; SYSTEM_OFF regardless\n");
break;
}
}
}
}
let refusal = psci.system_off();
let mut uart = serial::panic_registers();
uart.write(b"power: SYSTEM_OFF answered ");
say_code(&mut uart, refusal);
uart.write(b"; holding\n");
drop(uart);
cpu::halt()
}

/// This CPU's part in [`off`], on the SGI that asked for it: its timer
/// stopped, since a core `CPU_OFF` turned off may take no interrupt (DEN0022
/// §5.5.2), then `CPU_OFF`. The SGI is never ended, as `SGI_HALT` is not, so
/// a refusal leaves this CPU halted with nothing signalled to it, and [`off`]
/// names it still on.
pub(super) fn cpu_off() -> ! {
irqchip::stop_timer();
if crate::actuator::power_off_spares_the_last_two() && percpu::cpu_id() + 2 >= crate::smp::cpu_count() {
cpu::halt()
}
let Some(psci) = psci::conduit() else {
unreachable!("power: SGI_OFF is raised only by `off`, which holds a conduit")
};
let refusal = psci.cpu_off();
let mut uart = serial::panic_registers();
uart.write(b"power: cpu");
uart.dec(u64::from(percpu::cpu_id()));
uart.write(b"'s CPU_OFF answered ");
say_code(&mut uart, refusal);
uart.write(b"\n");
drop(uart);
cpu::halt()
}

/// The code PSCI refused a call with, signed.
fn say_code(uart: &mut serial::PanicUart, code: i32) {
if code < 0 {
uart.write(b"-");
}
uart.dec(u64::from(code.unsigned_abs()));
}
Loading
Loading