Skip to content

The machine's stop keeps its gate and waits for no thread - #1241

Merged
Japabu merged 2 commits into
mainfrom
stop-gate-without-wait
Nov 11, 2026
Merged

Japabu merged 2 commits into
mainfrom
stop-gate-without-wait

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

#1218 has merged. This PR is one commit, 954b57d, whose parent dd0ceb7 is #1218's commit on main. GitHub's compare reads it one commit ahead of main, and it merges cleanly onto main e558cc1. The PR's commit list may still show dd0ceb7 from its last sync.

Problem

The kernel's stop did two jobs. It closed Ring 3 to every thread but the one that asked. It also swept the process table and waited, inside a 2 s budget, for every other thread to park, band or exit, then logged a stop: record of the count.

The wait did two things:

  • For a thread parked when the stop began, the sweep's mark meant a later wake put it in the stopped band unrun, so it never ran the rest of its syscall.
  • For a thread running in a syscall, it gave the thread a budget to reach its boundary, which the thread could outlast.

It cost these:

  • a SeqCst fence on every park, band and exit;
  • the sticky STOP mark with three routes into the band;
  • a watch Gate with its own loom model;
  • an actuator;
  • a record parsed by the harness.

Neither of its effects is something the machine's end needs. What has to finish is flushed, synced and ended before the supervisor asks.

Result

  • The gate stays, the wait goes. quiesce::stop names its caller, demands a pass of every other CPU and returns once each has begun one.
    • A pass runs in Ring 0, and whatever that CPU then returns to Ring 3 reads the stop begun and is banded at the boundary.
    • A CPU that begins no pass within DEAF_CPU is wedged, and the kernel panics.
    • This is shared code (Doorbell::demand_pass, driver::pass_on_every_other_cpu), so AArch64 and the reboot path get the same acknowledgement from every CPU.
    • It replaces the user-half shootdown that pio::take_back ran for the same reason, so the Stop shootdown origin is removed.
  • What now runs after the stop begins. A thread inside a syscall, or parked in one and woken later, runs on to its boundary, and what it logs may follow the last word. The stop's module header says so.
  • Removed:
    • the sweep, its watch and budget;
    • note_progress and its fence on every park;
    • Gate, with its loom model and its gate-fence-off control;
    • TaskShared's STOP / stop_if_blocked / stop_pending / mark_stop;
    • CpuSched::place's stop arm;
    • the stop: record and its parser (toyos-quiesce keeps the supervisor's bounds);
    • the stop-budget-spent actuator.
  • The stopped band stays. A task banded at its boundary still needs a container (I1). Its only route in is now dispose_stop. A banded task's word reads Ready on its own CPU, so no wake claims it. A retire falls to handle_retire's home arm and leaves it there.
  • Manual: the services page now says that once the supervisor asks, no program but the asking thread runs its own code again. It also says the kernel waits for no program.
  • Issues:
    • The question record is closed. The Ring 3 gate is the part the supervisor cannot provide, and the stop's header says so. The inventory now says the design keeps the stop's gate.
    • Six records about the removed wait, record, staging and stop routes are deleted.
    • One aarch64 defect met on the way is recorded (Risks).

Scheduler functions changed (wakeperf lands first)

CpuSched::place, CpuSched::keep_stopped, CpuSched::handle_retire (a comment), SchedPass::dispose_stop, invariants::check_cpu, TaskShared::at_safe_point (doc only), Doorbell::begin_pass, new Doorbell::demand_pass / pass_demanded, driver::pass, driver::pass_block, driver::ring_all_but_self → pass_on_every_other_cpu.

Lines

This PR's commit against its base dd0ceb7:

Area Lines
kernel/src (11 files) +100 −250
kernel/pure (5 files, tests included) +79 −228
kernel/loom and kernel/Cargo.toml +33 −86
lib/quiesce +4 −269
tests +42 −60
Whole commit (41 files) +312 −1217

Tests

  • machine_shutdown_with_a_thread_spinning replaces machine_shutdown_short_stop, which judged the record's shortfall that no longer exists.
    • Setup: q35 with two CPUs and a thread spinning in Ring 3 when the stop begins, judged by a clean ACPI power-off.
    • It claims only that the stop completes. It is the one row where a CPU must answer the demand while running a thread in Ring 3.
    • After the gate, Ring 3 has no observable output, so the row cannot see a thread that ran past it. The loom model below carries the ordering claim.
  • The new loom model a_pass_that_answers_a_demand_reads_what_its_demander_wrote checks that a CPU whose pass answered the demand reads the gate open. It reds under the existing doorbell-kick-relaxed control, which is now registered for it in build/ci.rs.
  • The unit test a_demanded_pass_is_answered_by_the_next_pass_and_nothing_else covers the doorbell's new bit.
  • a_retire_for_a_stopped_task_leaves_it_in_the_band replaces the unit tests of the removed mark.
  • virt_smp no longer reads the record's CPU count.
  • The USB ordering row now orders usbd's end before the kernel's last word.

Evidence

Evidence branch: https://github.com/ToyOSOrg/evidence/tree/c88f1fa7da95900f3611da7bd9de9be3d12e7d25

Reused, and why it still holds

  • Guest rows. Run 43030668596 at 8502f16 passed 15 of 15:

    • the stop: machine_shutdown_with_a_thread_spinning, a_clean_stop_proves_its_image, the_log_lands_on_the_boot_stick;
    • ACPI: acpi_mediated_access, acpi_lock_given_back_on_one_cpu, acpi_supply_outlives_holder, acpi_press_is_one_sci;
    • session_lifecycle;
    • aarch64: virt_smp, virt_el1_smp, virt_reboot, virt_reboot_refused_without_psci, virt_off_names_the_cpus_left_on, virt_fatal_halts_the_others_first, virt_mask_windows.

    The aarch64 rows ran because the change is in shared scheduler code.

  • Metal. The T14 run at 5524159 (below), whose code equals 8502f16's outside issues/.

Neither changes for this head. git range-diff of 5524159 against this head shows four kinds of change in code:

None of these is on the stop path. The rebased head's compilation is covered by round 2's clippy shapes below.

First: did anything depend on the wait?

An experiment kernel kept the gate, swept nothing and waited for nothing (1314b4bcb, 3725bfcc8). Every stop row that built passed on it, in runs 43024839173 and 43026246044. Two rows were red:

  • machine_shutdown_short_stop, by design: the record always read zero.
  • virt_mask_windows (Risks).

Host checks, round 1 (141a667)

The code is the same as 5524159. Logs.

Check Result
kernel sched 199 pass
loom_sleep 4 pass
loom_watch 11 pass
doorbell-kick-relaxed control reds with "a pass answered the demand and read the gate closed after it"
kernel-sim pass
toyos-checks (the harness, including the changed record readers) 44 pass
toyos-quiesce builds; no tests remain
clippy, 12 shapes exit 0
cargo test --lib (the build system) 384 pass, 1 red: the recorded macOS defect issues/the-llvm-environment-test-breaks-git-on-macos.md, in build/llvm.rs and build/sysroot.rs, which this PR does not touch

Host checks, round 2 (this head, after the rebase)

At 954b57d. Logs.

Check Result
cargo test -p kernel --lib --features sched-check sched:: exit 0, 199 pass
cargo test -p kernel-loom --test loom_sleep exit 0, 4 pass
Control: --features doorbell-kick-relaxed --test loom_sleep -- --exact a_pass_that_answers_a_demand_reads_what_its_demander_wrote exit 101, with its registered message
cargo test -p kernel-sim exit 0, 69 pass
Clippy in CI's 10 kernel shapes, -D warnings with the adopted lints: the library with and without its controls; x86_64-unknown-none and aarch64-unknown-none-softfloat, each plain, with boot-actuators,test-actuators, with boot-actuators and with its instruments exit 0 in every shape, no warning, the kernel checked in each
cargo test --lib (the build system's issue gates read this round's records) exit 101: 384 pass, and the same one macOS llvm red

Not run, and why: the full --ci host. This change touches no build step, no other host member, crypto or the apps. The merge queue runs the full host checks.

Metal: the T14 at 5524159, green

Two boots ran on the T14 only: metalcase at 18:19Z and benchcase at 18:26Z. No AMD bench run was made. Readbacks, judge and guard.

Driver verdicts and boot facts:

  • toyos-metal read "passed" for both boots. That verdict requires:
    • Boot: complete;
    • no job past its bound;
    • the supervisor's stop line ("the machine stops, and logkeeper makes the log whole first (Reboot)") as the volume log's last line;
    • logkeeper's stop mark on the volume;
    • the FAT32 check;
    • the machine answering ssh again, after 69 s and 243 s.
  • Both build lines read supervisor: build 55241591965afdae0033da2adc5fb30104c5c48b clean.

Offline judge. Run from a clean detached checkout at 5524159 with cargo test --test toyos-build -- --metal --metal-readback <dir> machine_reboot boot:benchcase: exit 0, 5 passed, 0 failed (judge.log).

Row Result
osbench PASS: all 16 groups ran to their end on the NVMe
clock_holds_over_minutes PASS
i219_leaves_ulp_unarmed PASS
i219_carries_netstacks_link PASS
machine_reboot PASS: reset register SystemIO 0xcf9 <- 0x06 decoded

All 17 jobs of benchcase (16 osbench groups and test_rs_lan_lease) and metalcase's job ended with exit 0. The 4 timed numbers on this machine are within the 2x guard of their records: 0 past their record.

osbench sched, ipc and tail against the four ToyOS T14 boots of the 2026-11-11 scoreboard at ad5db5a (osbench-guard.txt). One boot against four, with main moved in between, so this is a guard and not a measurement of the change. Of 27 rows:

  • pingpong.futex.cross_core is past 2x the recorded median: 4,685 ns against 257.

  • Four rows are 1–8 % worse than every recorded boot:

    • wake.storm.call.w4: 1.08x;
    • wake.storm.call.w16: 1.05x;
    • ipc.connect_accept: 1.01x;
    • tail's cpu.clock: 4,189 against 4,190 MHz.

    The recorded spread of the two wake.storm.call rows is 10 % and 8 %. This change adds no work to the wake or call path: it removes a load from CpuSched::place and a fence from the park, band and exit paths, and begin_pass clears one more bit in the same instruction. The sibling rows go the other way: wake.storm.call.w1 0.93x, wake.storm.drain.w1 0.93x, pingpong.futex.same_cpu 0.95x. Investigated by that reading and not by a paired session; a paired base-and-head session would settle it.

  • The other 22 rows are at 0.93x to 1.10x of the recorded median, within or below the recorded range.

What the T14 readback cannot show. The kernel writes its census and last word (Rebooting.) after logkeeper's flush, to the UART alone, and the T14 bench captures no UART. A sched: cpuN began no pass panic in the stop would also land there. So its absence from the volume log establishes nothing, and that panic's own reboot after 5 s would also bring the machine back. The loader reads no crash record (issues/a-crash-record-survives-a-reset-outside-ram.md), so "no panic record at the next loader pass" is not observable either. What the T14 does show:

  • the stop asked;
  • the log made whole and stop-marked;
  • the reset and return.

The guest rows above, whose console is read, show the stop completing and its last word, including with a thread spinning in Ring 3 on another CPU.

Risks

  • A thread may run kernel code after the stop begins. A thread inside a syscall, or parked in one and woken after the stop begins, runs the rest of its syscall in Ring 0 before it is banded.
    • Before, the sweep's mark sent a parked thread to the band unrun. A running one could outlast the budget.
    • Mediated ACPI port access and SMI_CMD writes are refused under their locks once the stop has begun.
    • On the power-off path, settle waits out an act already in flight.
    • The reboot path runs no settle, so an act in flight can overlap the reset write. This PR does not change that.
  • An exit: record can follow the last word. It would come from a straggler's teardown: a process whose last thread was already in process::leave or its teardown when the stop began. That record can reach the UART after the last word. The aarch64 PSCI rows (ended_through_psci) red on any line after the last word, so a future red there with an exit: line is this design's residual. It was not seen in the 15/15 run or in either experiment run.
  • No power-off on metal. No metal bench runs a power-off row.
  • virt_mask_windows (aarch64) red, cause unknown. It was red at main 422915e (43027396712) and on both experiment runs, which branched from 422915e. It was green at 34cb4c9 (43026252506) and at 8502f16. Every red ran 422915e's code and every green later code, and 34cb4c9 adds kernel changes (A move names the rights it delivers: a ring and a manifest's SysCap stay where they were given #1204). So whether it is intermittent or was fixed in between is not established. It is recorded, with the repeat that would tell, as issues/virt-mask-windows-finds-a-mask-with-no-hook-in-a-lower-el-data-abort.md.

Review round 2

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1241 at 5524159

Architecture. The split is right.

  • The gate stays. It is the one part no program can provide: no thread but the caller re-enters Ring 3 while the kernel runs its last sequence.
  • The rest goes:
    • the wait;
    • the STOP mark;
    • the SeqCst fence on every park, band and exit;
    • Gate;
    • the actuator;
    • the record and its parser.
  • Size. Net −916 lines; kernel/src is +88 −240.
  • The acknowledgement moves. It used to be an x86-only user-half shootdown on the power-off path. It is now the shared doorbell demand (demand_pass / pass_on_every_other_cpu). The reboot path and AArch64 now get the same acknowledgement from Ring 0 that only the x86 power-off had before.
  • Order is a type fact. take_back and take_for_the_stop both take &Stopping.
  • toyos-quiesce keeps only the bounds the supervisor shares with drivers and logkeeper. That is a real shared subject.

The two claims

  1. "The old wait only shortened the window and never closed it." This is true for a thread executing in a syscall: the 2-quantum budget ran out into the record's shortfall. It is false for a parked thread. stop_if_blocked marked it, and CpuSched::place's stop arm banded it unrun on any later wake, adopt or retire. So a sweep that ended with running == 0 left no thread but the caller able to run kernel code. This PR reopens that window: a parked thread woken after the stop finishes the rest of its syscall in Ring 0. The PR body's Risks section says so correctly. The commit message ("It established nothing the supervisor's orderly stop does not") and the new module header (blocker 1) do not.

  2. "ACPI port access and SMI_CMD writes are refused under their own locks once the stop begins." Verified on x86:

    • acpi_mode::acting reads begun() under HOLDER.
    • smi_cmd::write reads it under WRITING.
    • arch::power::settle takes both locks, so on the power-off path an act in flight is waited out and no new one follows. The lock's release/acquire carries the write to CALLER.

    One gap: the reboot path runs no settle, because power::reboot drops Stopping. An act already in flight can therefore overlap the reset write. This PR does not change that, and the act takes microseconds while the census and UART drain before the reset take milliseconds.

What else a straggling thread can do after the stop

A straggler here is a thread already inside a syscall when the stop begins, or one parked then and woken later. Once stop() returns, no thread is in Ring 3, so no new syscall can start.

  • Disk. The kernel has no storage driver. The disk drivers ended before the ask (DRIVERS_MS), and stop_before_reset clears bus mastering on every held function.

  • Device programming. These calls have no begun() check and are not serialized with pcidev::stop_before_reset, which by design takes no lock:

    • a PCI claim or release, whose FLR or D3hot settle spins for up to about 100 ms and then restores the BARs and Device Control;
    • dma_start, which sets Bus Master Enable;
    • DMA map and unmap;
    • MSI arming;
    • slot actions.

    I found no syscall that parks and then enables mastering. The claim path leaves Bus Master Enable off, and only dma_start, a short syscall, sets it. A straggler would have to be inside dma_start at the instant of stop_before_reset, which runs milliseconds after the stop began. I found no concrete harm. What remains is the kind of risk the design accepts.

  • The log. A straggler's kernel record committed after the last word but before the final drain reaches the UART after the last word.

    • Where it comes from. The producer that matters is the exit: record from teardown_bookkeeping (process.rs:723). It comes from a process whose last thread was already in process::leave or the teardown when the stop began. A killed thread still inside its syscall writes nothing: Stop outranks Exit, so it is banded first. The old sweep waited for such a teardown; the new stop does not.
    • Which rows red on it. The aarch64 PSCI rows check this through ended_through_psci (tests/toyos.rs:4171–4197), which reds on any line after the last word other than the expected PSCI lines.
    • How likely it is. It was not seen in the 15 of 15 run or in either experiment run. It is unlikely, given the supervisor's flush, sync and driver waits before the ask. It is now a legitimate outcome of the design that those rows report as a failure (suggestion 6).

The kept stopped band

Keep it.

  • How it works now. With the mark gone, the band has one way in (dispose_stop) and no way out. A wake cannot claim a Ready word. A retire falls to handle_retire's home arm and is consumed. Steals and placement read only rq.
  • Why the alternatives are worse:
    • A park with no deadline that a kill cannot end puts the task in parked. handle_retire claims it there, place sends a killed task to dying, and commit refuses to park a killed task again.
    • dying dispatches its tasks.
    • Dropping the task breaks invariant I1 and the ownership of its stack.
  • Why it is not a change to the decision. The brief listed the band for removal because it assumed the band served the sweep's marks. In fact the band is how the gate represents a stopped thread. Keeping it is an implementation finding, not a change to the decision.

The loom model and its control

  • What the model isolates. a_pass_that_answers_a_demand_reads_what_its_demander_wrote checks the one ordering the gate adds. The gate is relaxed on both sides. Only demand_pass's release and begin_pass's acquire can make the gate's write visible to a pass that answers the demand.
  • The negative control. It is registered in CONTROLS as its own --exact run under doorbell-kick-relaxed. In the evidence it reds with its own message (control-doorbell.log, exit 101).
  • What else covers the bit. The unit test checks that only begin_pass clears the demand bit, and that a ring or an armed sleep does not.
  • The rest is structural. Every return to Ring 3 passes leave_user_if_due, and exit_to_user checks again after every do_preempt. That holds by inspection.
  • Sufficiency. This is enough. The Gate model and its gate-fence-off control went with what they checked.

Overlap and landing

The virt_mask_windows record

I checked it against the logs:

Run Revision checked out Result
43027396712 422915e red: DOUBLE PANIC, data abort from a lower EL
43026252506 34cb4c9 green
both experiment runs 1314b4b and 3725bfc, whose parent chain starts at 422915e red, the same panic
43030668596 8502f16 green

The record's two claims about revisions are correct. It leaves out one fact: 34cb4c9 descends from 422915e and adds kernel changes (#1204: syscall/ipc, object/handle, loader/start.rs). Every red ran on 422915e's code and every green on later code. So the evidence cannot tell an intermittent defect from one fixed in between. The PR body's "pre-existing flake on main" is unsupported (suggestion 7).

Evidence

  • Guest run. The guest log checks out 8502f16… and reports "15 passed, 15 total". git diff 8502f16d7 552415919 -- . ':!issues' is empty.
  • Host checks. They ran at 141a667, which differs from the head only in the virt_mask_windows record. The doorbell control reds with its registered message.
  • The one host failure. The llvm failure in the build-system tests is the recorded macOS defect, in files this PR does not touch.
  • The new row. machine_shutdown_with_a_thread_spinning claims only that the stop completes. It is the one row where a CPU must answer the demand while running a thread in Ring 3.

BLOCKERS

  1. kernel/src/quiesce.rs:7–9: "a parked one stays parked" is false. This is the module's central contract. A thread parked when the stop begins can be woken by a deadline, a device interrupt or another straggler's post. It then runs the rest of its syscall in Ring 0 and is banded only at its boundary. The PR's own Risks section says so, and the mark that used to keep parked threads parked is what this PR removes. A reader trusting the header would rule out exactly the straggler cases analysed above. Replace it with something like: "A thread inside a syscall, or parked in one and woken later, runs on to the boundary, and what it logs may follow the last word."
  2. kernel/src/process.rs:779: "…so a machine stop waits for every record and release here" is now false. The stop waits for no thread, and this PR deleted the note_progress below this line that made the clause true. Delete the clause.
  3. kernel/src/log/console.rs:18–20: "A holder the stop did not stop within its budget, which the stop's record names, may still write over it." is stale. There is no budget and no record any more. stop() now returns only after every other CPU has passed through Ring 0, so the UART's holder, which writes from Ring 3, cannot write over the stop's wire. Delete the sentence.
  4. issues/the-kernel-is-small-interrupts-post-and-threads-wait.md:361 is stale. The inventory row covering firmware tables, ACPI mode, reset and power-off, the machine's stop and what the loader named reads "the rest below". That pointed at the stop's design-change bullet, which this PR deletes, and the loader row below is marked Done. The design now keeps the stop's gate (the services page). Change the cell to say so, for example "yes for the tables, the acpi row, power and the stop's gate".

SUGGESTIONS

  1. kernel/pure/sched/cpu.rs:1244–1247. The comment on handle_retire's home arm says a word naming this CPU "means an Adopt is on its way here". For a banded task it means the band, and this PR's "a retire finds it at home and leaves it there" rests on exactly this arm. The gap predates this PR, but adding one clause would make the reliance visible.
  2. kernel/src/scheduler.rs:141. "if either mark it can carry" is out of date: the stop is no longer a mark on the thread.
  3. kernel/src/sched/driver.rs:722–723. "whatever a CPU returns to Ring 3 after its own read what this caller wrote" is garbled. Suggested: "a thread a CPU returns to Ring 3 after that pass reads what the caller wrote before the call".
  4. kernel/src/arch/x86_64/pio.rs:143. "Take every row's ports back" no longer matches the code: the function now only mints the proof.
  5. kernel/src/power.rs:44. "which waits on userland's stop" should now say it waits for every other CPU's pass. The conclusion still holds: a CPU stuck with interrupts masked blocks the stop, which then panics after DEAF_CPU.
  6. The log case above. If blocker 1's wording does not cover it, add it to the Risks. Then a future aarch64 PSCI-row red with an exit: line after the last word is recognized as this design's residual rather than chased.
  7. The virt_mask_windows record. Add that 34cb4c9 descends from 422915e with kernel changes, and that the first step is a repeat at 422915e and at current main.

For the orchestrator: claims in the PR body and commit message

  • "pre-existing flake" is unsupported (see the virt_mask_windows section above).
  • "Two boots, for the T14 and the AMD bench": only the T14 run is queued. The body should say which boots actually ran.
  • The whole PR is +289 −1205 at the head, not +286.
  • The commit message's "It established nothing the supervisor's orderly stop does not" should be corrected along with blocker 1.

What the T14 readback must show

  • The build line names 5524159 and says it is clean.
  • metalcase / machine_reboot:
    • the supervisor's stop line and logkeeper's stop mark are on the volume;
    • Rebooting. is on the console;
    • the machine comes back, and the next loader pass finds no panic record.
    • In particular, there is no sched: cpuN began no pass in … of being kicked for one. This is the first run of the new acknowledgement on metal, across all the T14's CPUs, on a reboot path that had no acknowledgement before.
  • benchcase:
    • all 16 osbench groups and test_rs_lan_lease pass;
    • the boot ends in a clean reboot with the stop mark;
    • the sched, ipc and tail groups are within the existing 2× guard of the T14's recorded values.
    • If any of those groups is slower than its recorded value, investigate it, since this PR removes a fence from the park path. A missing gain is not a rejection.

SEND BACK. The four blockers are text fixes, plus the rebase described above; no code change is required. Once they are fixed, the verdict is LAND, pending the T14 readback above.

Japabu and others added 2 commits November 11, 2026 18:39
…request does

The record asked which of two readings of the design holds: a parent asks
its children to quit over its own protocol and the kernel keeps only the
kill, or the kernel's walk of the subtree is the mechanism the interrupt,
hang-up and terminate events need. The orchestrator decided the second.

Why the walk stays: whether a process listens and whether it is claimed
for its end are checked and acted on as one act under one lock, which the
process-lifecycle model checks, so no read comes between them; a hung or
deaf process in the middle of a tree cannot shield what is below it, since
the walk reaches each process itself; and the userland alternative needs a
listener in every process and a relay in three runtimes to carry a request
down (the orchestrator's analysis; not re-derived here).

The authority page now states it: a holder of a child's handle may ask its
subtree to quit with a reason, a listener receives it as an event, one that
never listened or has not taken the last request is ended, and the kernel
never asks on its own. "Quitting gracefully is the parent's job" is
replaced by that last clause, which is the part of it that was true.

The sessions page read as if a sign-out offered every app the chance to
finish, and as if a second request ended a process that ignored the first.
What happens: the first ask ends at once every app that never listened; a
second request ends a process that has not yet taken the first and asks
again one that has taken it; a process that took a request and carries on
is ended only by a force.

issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md is
deleted and its one citation, in the kernel inventory's list of unresolved
design changes, with it. The durable rule already stands at the site that
owns it, kernel/proclife/src/quit.rs's header.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The kernel's stop did two things: it closed Ring 3 to every thread but its
caller, and it swept the process table, marked parked threads and waited
inside a budget until every other thread had parked, banded or exited,
then wrote a record of the count. The first is the kernel's alone: its
last sequence takes ports back, settles what programs left in flight and
cuts power, and no program can vouch that nothing runs in Ring 3 across
it. The second kept a thread parked when the stop began from running the
rest of its syscall, since a wake put a marked thread in the band unrun,
and gave a thread running in a syscall a budget to reach its boundary
that it could outlast. Neither is something the machine's end needs: what
had to finish is flushed, synced and ended before the supervisor asks. A
thread inside a syscall, or parked in one and woken later, now runs on to
its boundary, and what it logs may follow the last word.

The gate stays. `quiesce::stop` names its caller, then demands a pass of
every other CPU and returns once each has begun one (`Doorbell::
demand_pass`, `driver::pass_on_every_other_cpu`, shared by both
architectures): a pass runs in Ring 0, and whatever a CPU returns to
Ring 3 after it reads the stop begun and is banded at that boundary. A
CPU that begins no pass inside `DEAF_CPU` is wedged, a panic. This
answer from Ring 0 replaces the user-half shootdown `pio::take_back` ran
for the same reason, and the `Stop` shootdown origin goes with it.

Removed: the sweep, its watch and budget, `note_progress` and the
`SeqCst` fence it put on every park, band and exit; the watch `Gate` and
its loom model and control; `TaskShared`'s `STOP` mark with
`stop_if_blocked`, `stop_pending` and `mark_stop`, and
`CpuSched::place`'s arm that banded a woken or adopted stopped task (a
banded task's word reads `Ready` on its own CPU, so no wake claims it and
a retire finds it at home and leaves it); the `stop:` record and its
parser in `toyos-quiesce`, which keeps the supervisor's bounds; the
`stop-budget-spent` actuator. The band itself stays: a task stopped at
its boundary still needs a container.

Scheduler functions changed: `CpuSched::place`, `keep_stopped`,
`handle_retire` (a comment), `SchedPass::dispose_stop`,
`invariants::check_cpu`, `Doorbell::begin_pass` (and new `demand_pass`,
`pass_demanded`), `driver::pass`, `driver::pass_block`,
`driver::ring_all_but_self` (now `pass_on_every_other_cpu`).

Tests: `machine_shutdown_short_stop` judged the record's shortfall, which
no longer exists; it becomes `machine_shutdown_with_a_thread_spinning`,
two CPUs with a spinner in Ring 3 when the stop begins, judged by a clean
power-off. `virt_smp` no longer reads a record's CPU count, and the USB
ordering row orders usbd's end before the kernel's last word. The new
loom model `a_pass_that_answers_a_demand_reads_what_its_demander_wrote`
reds under the existing `doorbell-kick-relaxed` control, and
`a_retire_for_a_stopped_task_leaves_it_in_the_band` replaces the unit
tests of the removed mark.

The question record asked whether the kernel's sweep held anything the
supervisor's stop does not, on the premise that the design names only the
power-off as the kernel's. The design never named the sweep, but the
Ring 3 gate is not the supervisor's to provide; that is what the
record's exit wanted written at the stop's header, and it is. The record
is deleted, with six records about the removed wait, record, staging and
stop routes; the services page says the kernel waits for no program once
the stop is asked. An unrelated aarch64 defect met on the way,
`virt_mask_windows` red on main itself, is recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up review of #1241 at 954b57d

Scope. This round checks only the top commit 954b57d against 5524159: the four blockers, the rebase onto #1218 (dd0ceb7, which sits on main 67713cb), suggestions 1–7, and the PR body and commit message. It reopens nothing settled in the previous review.

Architecture. Unchanged since the previous round, and still right. The gate stays and the wait goes. The shared doorbell demand is the Ring 0 acknowledgement, and the band is the stopped thread's container.

How the rebase was checked

  • I replayed 5524159 onto dd0ceb7 with git merge-tree --merge-base=b56693152 dd0ceb7c4 552415919. It conflicts in exactly two files: kernel/src/invalidation.rs and issues/the-kernel-is-small-interrupts-post-and-threads-wait.md.
  • Outside those two conflicts, the diff from that replay to 954b57d is only the comment edits listed below and the virt_mask_windows record. This PR's own code is unchanged.
  • Every kernel stack sits above unmapped memory, and an overflow names its stack #1211's check_stack lines come from the base. The replay picks them up automatically, and the range-diff shows them only as context.

Blockers from the previous round: all resolved

  1. kernel/src/quiesce.rs:7–10. The header now says that a thread inside a syscall, or parked in one and woken later, runs on to the boundary, and that what it logs may follow the last word. "A parked one stays parked" is gone.
  2. kernel/src/process.rs:748. The false clause is deleted. What remains ("in the process until the last table section marks it") is true, and the next line depends on it.
  3. kernel/src/log/console.rs:16–19. The stale budget and record sentence is deleted.
  4. issues/the-kernel-is-small-interrupts-post-and-threads-wait.md:361. The cell now reads "yes for the tables, the acpi row, power and the stop's gate; what the loader named has left (Where a boot came from is the supervisor's to read, not the kernel's #1210)". This matches line 23 and the Done row at 394.

Rebase resolution: correct

Suggestions 1–7: all taken, all accurate

  1. kernel/pure/sched/cpu.rs:1249–1250. Accurate. dispose_stop calls preempt(self.cpu.id, …), which moves the task's word from Running(cpu) to Ready(cpu) (task.rs:1083). home_of then names this CPU, and the home arm leaves the task in the band.

  2. kernel/src/scheduler.rs:143–146. Accurate.

  3. kernel/src/sched/driver.rs:725–728. Accurate, and it now reads cleanly.

  4. kernel/src/arch/x86_64/pio.rs:143–145. It now matches the code, which only mints the proof.

  5. kernel/src/power.rs:44–47. Accurate. A CPU masked inside a BackendGuard begins no pass, so the stop would wait for it until DEAF_CPU.

  6. The exit: note in Risks. Accurate.

    • process::leave is called only from leave_user_if_due's Exit arm (scheduler.rs:166), and Stop outranks Exit there. So the only possible source is a thread that was already past that check, in leave or the teardown, when the stop began.
    • ended_through_psci (tests/toyos.rs:4181–4205) rejects any non-blank line after the last word other than the expected PSCI lines.
  7. The virt_mask_windows record. Its ancestry facts check out:

    The added first step and the second exit are sound.

PR body and commit message

Each correction requested last round has been made:

  • "pre-existing flake" is gone.
  • The boots are stated truthfully: only the T14's metalcase and benchcase are queued, none has run, and no AMD run is queued.
  • The commit message's claim about the wait is corrected and matches blocker 1.

Line counts against dd0ceb7 match the table exactly:

Area Files Lines
kernel/src 11 +100 −250
kernel/pure 5 +79 −228
loom and kernel/Cargo.toml 4 +33 −86
lib/quiesce 2 +4 −269
tests 4 +42 −60
whole commit 41 +312 −1217

Three small inaccuracies remain, none blocking. They are for the orchestrator.

  1. Evidence-reuse paragraph. It credits the range-diff with showing "OwnedAlloc in process.rs". It does not: the range-diff shows only check_stack as context. Every kernel stack sits above unmapped memory, and an overflow names its stack #1211 also changes more of driver.rs (the canary's removal, paint_stack, the idle-loop and trampoline actuators), and none of that is on the stop path either. The merge-tree replay above supports a more exact sentence: "replaying 5524159 onto dd0ceb7 differs from this head only in the two conflict resolutions, comments and one issue record."
  2. Round 2's clippy row. It says "CI's 10 kernel shapes". build/clippy.rs has 12 kernel shapes, including the two x86-64 heap-band shapes. Round 2 ran 10 of them and round 1 ran all 12. The code delta since round 1 is comments plus a COUNT change that no feature changes, so the 10 shapes are enough. Only the wording is wrong.
  3. Commit message. "virt_mask_windows red on main itself" is true at 422915e, but later main was green. "red at main 422915e" would avoid suggesting that current main is red.

Re-verification evidence (d730bba37, host-954b57d85/)

Every log names rev 954b57d.

Check Result
kernel-sched exit 0, 199 pass
loom exit 0, 4 pass, including the new model
control-doorbell exit 101, panics with the message build/ci.rs registers for it
kernel-sim exit 0; the binaries add up to 69
clippy exit 0 in all 10 shapes; the kernel is checked in each
build-system 384 pass, 1 red: the recorded macOS llvm test, in files this PR does not touch

This is enough for a rebase whose only code delta is comments and a constant.

Metal (pending)

The T14 readback staged at 5524159 must show what the previous review listed:

  • a clean build line naming 5524159;
  • the supervisor's stop line and logkeeper's stop mark on the volume;
  • Rebooting. and the machine's return;
  • no sched: cpuN began no pass panic;
  • all 16 osbench groups and test_rs_lan_lease passing, with sched, ipc and tail inside the 2× guard. Investigate any slowdown against the recorded values.

BLOCKERS

None.

LAND, pending the T14 metalcase and benchcase readback above.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator: follow-up review r2 was LAND pending the T14 readback. The judge at 5524159 (machine_reboot boot:benchcase) exits 0 with 5 of 5 passed: all 16 osbench groups, i219_carries_netstacks_link (lease) and machine_reboot (reset 0xcf9). Both build lines read 5524159 clean, and the stop line and stop mark are on the volume. Rebooting. and the census go to the UART only, which the T14 does not capture, so the stop's end on hardware rests on the guest rows that read the console (15/15). The four rows 1–8% off lie within their recorded spread on paths this change does not touch; pingpong.futex.cross_core is the known bimodal row. Queueing.

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 18:43
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 4280c31 Nov 11, 2026
10 checks passed
@Japabu
Japabu deleted the stop-gate-without-wait branch November 11, 2026 18:57
Japabu added a commit that referenced this pull request Nov 11, 2026
…cks' spawn records in main's form

Main moved under the branch in three places this branch also changes:
#1241 renamed `machine_shutdown_short_stop` to
`machine_shutdown_with_a_thread_spinning`, which is now that name's
`MACHINE_TESTS` entry; #1238 changed `virt_user_mode`'s arm, which this branch
moved into a function and which now carries the change; and #1238 changed the
kernel's spawn record to `spawn: pid=N (total=Xms) <path>` and the
supervisor's to `started logkeeper pid=N`, which the one-clock check's planted
log now writes beside the T14's first record.

#1238's readers no longer read the older form, so the two bench readbacks
this branch carries as fixtures, written before it, read as a boot with no
record of logkeeper's spawn and a job with no spawn record. Their three
lines each are rewritten in the form the kernel and the supervisor now write,
with the same pids and times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…cks' spawn records in main's form

Main moved under the branch in three places this branch also changes:
#1241 renamed `machine_shutdown_short_stop` to
`machine_shutdown_with_a_thread_spinning`, which is now that name's
`MACHINE_TESTS` entry; #1238 changed `virt_user_mode`'s arm, which this branch
moved into a function and which now carries the change; and #1238 changed the
kernel's spawn record to `spawn: pid=N (total=Xms) <path>` and the
supervisor's to `started logkeeper pid=N`, which the one-clock check's planted
log now writes beside the T14's first record.

#1238's readers no longer read the older form, so the two bench readbacks
this branch carries as fixtures, written before it, read as a boot with no
record of logkeeper's spawn and a job with no spawn record. Their three
lines each are rewritten in the form the kernel and the supervisor now write,
with the same pids and times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…o longer uses

#1241 deleted the watch's gate model and this change deleted the
two-submitter model. Those were `loom_watch`'s two uses of `CPU1`, so after
both the import is unused, and `cargo clippy -p kernel-loom --all-targets`
refuses it. The rebase's other resolutions are in the commits it replays:
`resolve` and `ThisProcess` look up through #1246's `with_handles`, and the
watch imports `poll::Via` beside main's `Arc` and `CellLock`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant