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
25 changes: 25 additions & 0 deletions issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ which tests a count where the other tests sixteen times it; a counters read
calls neither, and no function of `counters_read`, `test-runner`,
`supervisor`, `logkeeper`, `toybox`, `kernelprobe` or the loader differs.

The fifth and sixth silent ones, two guests of one run: `virt_smp` and
`virt_mask_windows`, in the whole suite on `wt/toyos-lastword` at
`aa7448db0`, whose diff changes the stop's hold of the console wire and
nothing these guests run before the stop begins.

- **The lines.** `FAIL virt_smp: STALLED: waiting for the job
test_rs_counters_read to end — it went quiet` and `FAIL virt_mask_windows:
STALLED: waiting for the boot's last word — it went quiet`, three seconds
apart on the host's clock; 44 of 46 passed.
- **The load.** 12 wide on the 14-core host, 1-minute load 35.09 when the
run began and 79.44 when it ended; liveness ceilings paid at 1.00x;
workers 346 s building against 752 s testing.
- **The same profile as the second.** `virt_smp`'s last three lines are
`===TEST_END unmap_touch exit=0===` at 11.730, `===TEST_START
test_rs_counters_read===` at 11.731 and the kernel's `spawn:
/system/bin/test_rs_counters_read pid=13` at 11.739 on cpu4.
`virt_mask_windows` said nothing while its second wait ran, as the two
stalls without a console above did; the run kept no PL011 log of it.
- **What is known.** The next whole suite at that commit, at load 79.44
rising to 78.28, passed 46 of 46.
- **Not compared.** These two ran the kernel of `wt/toyos-lastword` at
`aa7448db0`, built by the compiler before the ScalarEvolution fix. The
comparison across the fix above was of `main`'s `tests/virtsmpcase` image;
theirs was not compared, so whether the fault reaches them is not known.

The slow ones, with registers: a probe that captured `info registers -a` over
QMP whenever the read had not ended 3 s after `unmap_touch` (`debug-slow.patch`
in https://github.com/ToyOSOrg/ToyOS/pull/719#issuecomment-5978643928, which
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ leaves a line in the queue at the stop goes red without that and green with it.

The stop drains the queue on the wire right after
`quiesce::stop()`, before `Syncing filesystems...`
(`log::console::drain_for_the_stop`).
(`log::console::take_for_the_stop`).
- The drain disabled as a checked patch: `cargo test --test toyos-build --
--nightly quiesce_stops_the_machine` EXIT=1, `1 line(s) reached the
console after the boot's last word: console: a holder's line, queued once
Expand Down
69 changes: 69 additions & 0 deletions issues/a-woken-klogd-can-wait-seconds-on-an-idle-cpu-under-hvf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
status: open
kind: defect
opened: 2026-10-09
---

# A woken `klogd` can wait seconds on an idle CPU under HVF

A wake that makes `klogd` runnable on an AArch64 CPU under HVF can leave it
undispatched for seconds, until something else wakes that CPU.

**What it cost before the stop held the wire.** A boot asked to stop could
power off without `Shutting down.` on its console. The last word's drain
declined a held wire, and `serial::flush_final` spun on it and gave up.
Measured on 8-CPU `virt` guests under `Profile::Virt`, an Apple-silicon host
of 14 cores, QEMU 11.1.1:

- On the entropy stage's branch at `7522ec61e`, load 28 to 46:
`virt_el1_smp`, `virt_mask_windows` and `virt_off_names_the_cpus_left_on`
side by side with `virt_smp` and `virt_failed_ap_leaves_no_hole`, 131
runs, five reds, each `STALLED: waiting for the boot's last word`; a
sixth in a whole suite at load 41 to 47. Emulated at EL1 the same session,
50 runs, no red. One red's PSCI trace held seven `CPU_OFF` and one
`SYSTEM_OFF`, and its console stopped with `klogd` about 100 ms of guest
clock behind the stop. The probe is `debug-shutdown-regs.patch` in
https://github.com/ToyOSOrg/ToyOS/pull/802#issuecomment-6080912601.
- At `863d73ed3`, with a probe on the stop's two drains (on
https://github.com/ToyOSOrg/ToyOS/pull/805), 2 reds in 21 runs of the same
five, load 23 to 52. A red `virt_mask_windows`: at the decline the wire
was free with one ticket outstanding (`ticket=197 now=196`), its turn
posted to `klogd` and not taken, and the same 2.95 s later when
`flush_final` gave up. A red `virt_el1_smp`: the wire free, `flush_final`
spent 4.16 s.

**The dispatch delay itself.** `virt_reboot_wire_kept` on `Profile::Virt`
(two CPUs), at `863d73ed3` with the stop holding the wire, load 16: the
stop `post_wake`s `klogd` at guest 1.574 s, and `klogd`'s next loop top is at
6.579 s, on cpu1, where each of its tops that boot ran. The stop, on cpu0,
was parked the whole time. `klogd` ran 5 ms after the stop's deadline, when
a probe on that path kicked every CPU. That staging's wait timed out in 6 of
10 runs of that test.

**What it costs now.** The stop holds the console's wire from right after it
stops userland to the machine's end (`log::console::take_for_the_stop`), so
no record waits on `klogd` past it. A `klogd` not dispatched when the stop
asks costs the stop `log::console::LET_GO`, 5 s, and an alert, which reds
every test (`tests/common/serial.rs`, `NEVER_CLEAN`); where it never lets
go, the lines console holders had queued are not on the console, which the
alert says.

**Until it is fixed** these stay emulated (`tests/toyos.rs`):
`virt_mask_windows`, `virt_off_names_the_cpus_left_on`, `virt_reboot`,
`virt_reboot_wire_held` and `virt_reboot_wire_kept` at EL2, and
`virt_el1_smp` on `Profile::VirtTcg`.

**Not known.** Whether the wake reached the idle CPU, whether its sleep
handshake missed it, or whether HVF delivered the kick late: no register
capture of the idle CPU exists.

**Exit**: the cause named from a capture of the idle CPU, and fixed; then
`virt_el1_smp`, `virt_mask_windows`, `virt_off_names_the_cpus_left_on`,
`virt_reboot` and the two `virt_reboot_wire_` tests under `Profile::Virt` on
an Apple host, 300 side-by-side runs at load 30 or more with no red, the
harness's PSCI trace allowed under HVF; they then move to `Profile::Virt`,
`virt_reboot` keeping an EL2 row of its own for `SYSTEM_RESET` through the
SMC conduit, and `Profile::VirtTcg` is deleted.

Owner: the kernel's AArch64 bring-up, `issues/toyos-runs-on-arm64.md`, held
by the orchestrator's next kernel worker.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ opened: 2026-09-27

# The panic lock spin limit is a count its comment calls a second

`PANIC_LOCK_SPIN_LIMIT` in `kernel/src/drivers/serial.rs` bounds two waits:
`panic_flush`'s for `BackendGuard`, and `flush_final`'s for the console wire.
It is 100,000,000 iterations of a `try_lock` and a `pause`, and its comment
`PANIC_LOCK_SPIN_LIMIT` in `kernel/src/drivers/serial.rs` bounds the fatal
path's wait for `BackendGuard` (`panic_registers`, which `panic_flush` and the
stop's write over a kept wire take). It is 100,000,000 iterations of a `try_lock` and a `pause`, and its comment
calls that "~1s of spin". Nothing converts it to time, so the wait lasts
whatever that many `pause`s cost on the CPU running them.

**Evidence:** the code.

**Exit condition:** both waits are bounded by time against the counter the
panic path already times its bound with, and the comment says what is measured.
**Exit condition:** the wait is bounded by time against the counter the panic
path already times its bound with, and the comment says what is measured.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ 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,
was stopped with the rest of userland, the stop's last drain 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.
Expand Down

This file was deleted.

4 changes: 2 additions & 2 deletions issues/toyos-runs-on-arm64.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ Each stage names its exit; "measured" means a number from a run.
answers from a virtio-rng (`kernel/src/random.rs`); `virt_el2_drop`,
`virt_smp` and `virt_jobs_at_el2`, one boot of the job case with the timer
and FP jobs in it, stay emulated at EL2, which HVF gives no guest, and
`virt_reboot` and three more stay emulated at EL2 until
`issues/the-boots-last-word-can-miss-the-console-when-klogd-holds-the-wire.md`
`virt_reboot` and five more stay emulated until
`issues/a-woken-klogd-can-wait-seconds-on-an-idle-cpu-under-hvf.md`
is fixed. Each judges an event, never a rate: no QEMU test measures time.
**Accepted with the move to HVF:** `virt_user_mode`, `virt_irq_storm`,
`virt_timer_floor`, `virt_failed_ap_leaves_no_hole` and
Expand Down
5 changes: 5 additions & 0 deletions kernel/loom/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,11 @@ pub mod scheduler {
#[path = "../../src/sleeplock.rs"]
pub mod sleeplock;

/// The stop's claim on the console wire and `klogd`'s answer, driven over the
/// sleep lock above by `tests/console_handoff.rs`. Atomics and nothing else.
#[path = "../../src/log/handoff.rs"]
pub mod log_handoff;

/// The i8042's interrupt tally. `tests/i8042_tally.rs` is the only model whose
/// subject is a *driver*, and it is here for the reason the others are: the
/// property is "no reader ever sees this pair disagree", which is a claim about
Expand Down
40 changes: 40 additions & 0 deletions kernel/loom/tests/console_handoff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Loom: the stop's claim on the console wire against a holder's letting go
//! (`kernel/src/log/handoff.rs`), over the real `SleepLock`.
//!
//! The holder, `klogd` here, lets the wire go and reads whether the stop
//! asked; the stop asks, and tries the wire. One of them must see the other:
//! the stop's try finds the wire free, or the holder sees the ask, and so
//! posts the release the stop parks on. Where neither does, the stop waits out its whole budget
//! on a wire nobody holds and writes over it. Removing either `SeqCst` fence
//! from `handoff.rs` reds this model with exactly that outcome.

#![cfg(feature = "loom")]

use kernel_loom::log_handoff::Handoff;
use kernel_loom::scheduler::{become_task, TaskId};
use kernel_loom::sleeplock::SleepLock;

const KLOGD: TaskId = TaskId(1, 0);
const STOP: TaskId = TaskId(2, 1);

#[test]
fn the_stop_finds_the_wire_free_or_klogd_sees_the_ask() {
loom::model(|| {
let wire: &'static SleepLock<()> = Box::leak(Box::new(SleepLock::new(())));
let handoff: &'static Handoff = Box::leak(Box::new(Handoff::new()));

become_task(KLOGD);
let held = wire.try_lock().expect("nobody else has the wire yet");
let klogd = loom::thread::spawn(move || {
become_task(KLOGD);
drop(held);
handoff.asked()
});

become_task(STOP);
handoff.ask();
let took = wire.try_lock().is_some();
let saw = klogd.join().unwrap();
assert!(took || saw, "the stop's try found the wire held and klogd never saw the ask: nobody posts");
});
}
14 changes: 14 additions & 0 deletions kernel/src/actuator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ actuators! {
/// it outlasts the stop. Judged by `machine_shutdown_short_stop`.
stop_budget_spent = "stop-budget-spent";

/// Have `klogd` take the console's wire as the stop begins and keep it
/// until the stop asks for it. Judged by `machine_shutdown_wire_held`.
wire_held_across_the_stop = "wire-held-across-the-stop";

/// The same, with `klogd` keeping the wire through the stop's whole
/// `LET_GO`. Judged by `machine_shutdown_wire_kept`.
wire_kept_through_the_stop = "wire-kept-through-the-stop";

/// Have `klogd` hold the console's wire from the boot's last word until the
/// stop has taken the seal, where the stop does not hold it itself. Judged
/// by `machine_shutdown_wire_at_the_seal`.
wire_held_at_the_last_word = "wire-held-at-the-last-word";

/// Make one CPU ignore a kick.
dump_deaf_cpu = "dump-deaf-cpu";

Expand Down Expand Up @@ -189,6 +202,7 @@ const IMPLIES: &[(&str, &[&str])] = &[
// so the control that ends a machine no other bound ends is staged over the
// one that stops this machine ending itself.
("hard-lockup-probe", &["wedge-before-reset"]),
("wire-kept-through-the-stop", &["wire-held-across-the-stop"]),
];

#[cfg(feature = "boot-actuators")]
Expand Down
Loading
Loading