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
16 changes: 13 additions & 3 deletions console/system.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"]
Expand Down
36 changes: 36 additions & 0 deletions issues/no-harness-path-boots-the-console-image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
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 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

The orchestrator.

## Exit condition

A harness boot of `console/` that runs its `shutdown` and reads QEMU's
`guest-shutdown`.
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,29 @@ 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. Moving it there is the
placement of the power-off's slice, which found the refusal; it passed that
slice's review and is not the owner's.

**Exit** (the orchestrator's placement, not the owner's; that it waits on the
stopgap's deletion and on a job that waits to be asked is that slice's): once the ECDT
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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -227,7 +226,7 @@ otherwise pay to find again:
a counting allocator, in 6,891 nodes. Nothing reads the arena's
capacity: by its doubling it is 8,192 slots, 1,301 of them free, and a
method that defines more names than are free moves the arena each time
it runs, at a step for every 64 bytes of it. Owner: the power-off stage,
it runs, at a step for every 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
Expand Down Expand Up @@ -269,13 +268,59 @@ 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.
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.
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**, 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, 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 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.

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
Expand All @@ -288,9 +333,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.
6 changes: 5 additions & 1 deletion kernel/src/arch/aarch64/acpi_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub fn release() {
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: &crate::isa::Row, _request: &mut toyos_abi::acpi::Access) -> Result<(), toyos_abi::syscall::SyscallError> {
unreachable!("AArch64 has no ACPI row")
}
Expand All @@ -27,6 +27,10 @@ pub fn lock_release(_row: &crate::isa::Row) -> Result<(), toyos_abi::syscall::Sy
unreachable!("AArch64 has no ACPI row")
}

pub fn s5(_row: &crate::isa::Row, _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()
Expand Down
5 changes: 5 additions & 0 deletions kernel/src/arch/aarch64/power.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() -> ! {
Expand Down
32 changes: 29 additions & 3 deletions kernel/src/arch/x86_64/acpi_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`]), 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 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.
Expand Down Expand Up @@ -141,6 +144,8 @@ struct Facs {
struct Holder {
/// 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
Expand All @@ -149,7 +154,7 @@ struct Holder {
/// (`object::Held`), and `pcidev`'s machine record and then `paging`'s record
/// of windows under it; nothing holding one of those two takes this or a
/// claim's.
static HOLDER: Lock<Holder> = Lock::new(Holder { locked: false });
static HOLDER: Lock<Holder> = Lock::new(Holder { locked: false, supplied: false });

/// The right to act for the claim's holder, held across the act; none once
/// the stop has begun. The claim is there for the whole of the act: its row
Expand Down Expand Up @@ -402,7 +407,12 @@ fn enter(hardware: &Hardware) -> Result<(), ClaimError> {
/// disable is still to come.
pub fn release() {
let hardware = hardware().expect("a claimed row has its hardware");
give_back(hardware, &mut HOLDER.lock(), "its claim is gone");
{
let mut holder = HOLDER.lock();
// The next claim's holder supplies its own; this one's stands until then.
holder.supplied = false;
give_back(hardware, &mut holder, "its claim is gone");
}
if ENABLED.load(Ordering::Relaxed) {
leave(hardware);
}
Expand Down Expand Up @@ -534,6 +544,22 @@ pub fn lock_release(row: &isa::Row) -> 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, and `AlreadyExists` a second one under this claim;
/// nothing is kept of either. Its line is said under the claim's lock and
/// [`HOLDER`]: `log::emit` takes no lock.
pub fn s5(row: &isa::Row, word: u64) -> Result<(), SyscallError> {
let slp_typ = firmware::sleep_type(word).ok_or(SyscallError::InvalidArgument)?;
let mut holder = acting(row)?;
if holder.supplied {
return Err(SyscallError::AlreadyExists);
}
holder.supplied = true;
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.
Expand Down
4 changes: 2 additions & 2 deletions kernel/src/arch/x86_64/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];

Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading