Skip to content

Kernel: a kill never waits on its victim — the last thread out tears its process down - #549

Merged
Japabu merged 30 commits into
mainfrom
wt/toyos-killfix
Sep 28, 2026
Merged

Japabu merged 30 commits into
mainfrom
wt/toyos-killfix

Conversation

@Japabu

@Japabu Japabu commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Two processes that held each other's handle and killed each other at the same time deadlocked the kernel, which then panicked (audit K1). Each killer waited in retire_task on a thread that was itself inside the other kill. With this change a kill never waits on its victim: every thread of a killed process leaves by its own hand, and the last one out tears the process down. No kernel thread is created for it.

What changed, and why

  • A kill claims its victim, posts a retire to every thread, and returns (process::kill_process, scheduler::post_retire).
    • It waits on nothing, so it cannot deadlock on its victim. The killer and wait learn of the end from the process object's exit, which only the teardown publishes.
    • exit claims the same way, and retires the calling thread's siblings.
    • retire_task, its 10 s tripwire and TaskHandle's released flag, accounting copy and release post are deleted.
  • Every thread leaves by its own hand (process::leave).
    • Where: a killed thread leaves at its Ring 3 exit boundary (scheduler::leave_ring3_if_due). Any other thread leaves in exit or thread_exit.
    • What: it leaves its address space, folds its blocked-time accounting into its process's, and marks itself out under the table lock.
  • The exit boundary runs leave with IF set (scheduler::leave_ring3_if_due).
    • kernel_exit_to_user_check is entered with IF clear. A killed process's close_all, address-space drop and records would otherwise all run with interrupts masked on that CPU; a syscall's exit runs the same teardown with IF set.
    • The bracket is the one kernel_exit_to_user_check already puts around do_preempt: set before leave, cleared before the exit pass.
    • The depth is unchanged: 0, BASELINE_IRQ_EXIT, which is do_preempt's own. What IF adds is a nested interrupt, whose Ring 0 return reaches no scheduler entry, and a preempt::enable at depth 0 calling do_preempt, which asserts that same baseline. The teardown mints no Parkable and calls no yield_now: it closes handles by dropping them, and the VFS lock it can reach is a spinlock.
  • The last one out tears the process down, on its own stack, and stays in the process until that is done.
    • proclife::leave answers Last { code, mark } without marking the thread. teardown_bookkeeping marks it with proclife::torn_down, after the teardown's syscalls:, memory: and exit: records and after close_all. quiesce::note_progress posts after that mark.
    • Why: the machine's stop counts a zombie as nothing left to stop. A thread marked before its teardown let the stop return while close_all still dropped files. The shutdown's drain_all and sync_all then ran beside the teardown, and a dropped file's write-back was queued after the drain.
    • teardown's doc states its invariant: it waits on nothing, because it runs on a killed thread whose one cancel may be spent, and at a depth where a park asserts.
  • A thread whose process entry is gone panics (process::current_data, process::process_data). An entry outlives every thread that has not left it, and proclife::leave and route_thread_exit assert the same.
  • The decisions are toyos-proclife's, and the kernel calls them. claim_teardown carries the exit code. retire_set is the set a claim retires. leave answers whether the thread leaving is the last one out, and torn_down marks that one.
  • A join does not collect a thread of a process being torn down (join::collect_zombie). A thread that left keeps its TLS mapped until the last one out. Dropping its entry would return frames its siblings may still run in.
  • CPU time is totalled from the table's live handles at the teardown. Blocked time is folded by each thread as it leaves. ProcessAccounting::child_threads_cpu_ns is deleted.
  • No kernel thread's pid opens (process::process_object), so userland can kill no kernel thread. process_reopen_selftest's process-open-kthread verdict holds it.
  • Shootdown::serve raises flushed with fetch_max. Kernel threads and the idle loop spin in Lock::lock with IF set, so the 0xFE IPI can nest one serve inside another. A store would move flushed backwards, and the initiator would spin to ACK_TIMEOUT's panic.
  • Self-kill takes the ordinary path. The dispatch branch is deleted, and mutual_kill ends with a process killing itself.
  • quiesce-last-* holds its thread until a sweep counts it, and it alone, as running (quiesce::last).
    • hold claims the held thread's id. sweep notes whether it counted that thread as running, and the hold ends only on a sweep that counted exactly one running thread, the held one. It then logs <actuator>: the stop counts quiesce-last alone.
    • Why: the hold used to end on any sweep that counted one running thread. With the last thread out marked a zombie before its teardown, no sweep counts it, and a sweep counting some other thread still in Ring 3 released the hold.
    • quiesce-last-teardown holds the last thread out of a child of quiesce_last between its leaving and its teardown. Its hold is in process::leave, which the exit boundary also reaches at depth 0, where yield_now asserts. There hold refuses by name (scheduler::may_yield): it logs <actuator>: quiesce-last left outside a syscall and is not held and lets the stop go on unheld.
    • The harness (woken_by_the_held_thread) requires the counts … alone line before the stop: record, for both quiesce-last-* boots. For the teardown boot it also requires the held process's exit: test_rs_quiesce_last pid=… code=0 record between the two.
  • kill_while_blocked arm 4 waits for its spinner's exit. The in-guest ENDS_WITHIN deadline and the timed reader are deleted. A spinner the exit boundary misses never ends, and the harness's hang ceiling says so.
  • A ceiling red names the arm. check_rust_result prints the streamed stdout on its error branch, as its exit-code branches do. kill_ends_every_wait prints <wait>: killing before each kill.
  • Issues:
    • issues/kernel/retire-tripwire-is-not-queue-shaped.md closes, because its constant is deleted.
    • Filed: issues/kernel/a-killed-shutdown-caller-panics-on-its-second-drain-backoff.md.
    • Filed: issues/kernel/the-lock-spins-shootdown-poll-says-if-is-clear-and-two-callers-spin-with-it-set.md.
    • Filed: issues/diagnostics/the-quiesce-last-staging-dies-on-ten-seconds-of-guest-clock.md.
    • toyos-sched and issue prose that cited the tripwire is deleted.
  • A refused hold no longer keeps the one hold slot (kernel/src/quiesce.rs, hold). The slot HELD.compare_exchange claims is released back to NOBODY on the may_yield refusal, so the thread the boot stages can still take it.
  • kill_ends_every_wait's sleep arm runs before process-wait and thread-join. Both of those arms also sleep underneath (the waited process's nanosleep, the joined thread's std::thread::sleep), so a sleep mutation would otherwise surface under their name instead of its own.
  • kill_ends_every_wait kills a child only once the kernel's roster shows its main thread parked (spawn, main_thread_parked).
    • The parked in <wait> marker is written before the syscall that parks. A kill landing while the child still returns from that write is honoured at the write's kernel_exit_to_user_check, so the named wait is never entered, and the arm passes whatever the wait does with a kill.
    • That is what the sleep mutation showed at 4d612d6: the sleep arm passed, and the hang came from process-wait, whose waited process runs the same nanosleep(u64::MAX) through the same sys_nanosleep but had a whole second spawn's time to park.
    • spawn now polls the SysCap roster (Rights::ROSTER, which test-runner endows) until the child's main thread is SCHED_BLOCKED, sleeping 10 ms between reads as futex_wake_counts's wait_until_parked does. It has no bound of its own: the harness's ceiling is the test's only clock, and the <wait>: waiting for the roster to show it parked line printed before the poll names where a ceiling red stopped. Between the marker's write and the named wait's syscall the child runs nothing else that parks; its code faults in from /system, the memory image.
    • issues/kernel/a-kill-ends-every-wait-arm-can-pass-without-reaching-its-wait.md closes on its exit condition. tests/testcases/system.toml no longer counts the binaries that read the roster.
  • quiesce_wakes_on_the_last_teardown is disabled, behind the same issue quiesce_wakes_on_the_last_park already is (src/redlist.rs, issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md). Its Fast-tier failure is the same shape: two threads the stop never counted down to alone, one of them the held thread by construction. tests/common/power.rs's gave-up error now appends the boot's whole log, so the next sighting names both.

The model (toyos-proclife)

Every thread's way out is scripted as its own steps: leave; then, for the last one out, free, mark and publish; then post and go.

Laws checked at every state:

  • one claim and one teardown per process;
  • nothing freed or published while any thread but the one tearing it down is still in the process;
  • a teardown in flight keeps its thread in the process (L3);
  • no mapped TLS is dropped under a sibling that is still in the process;
  • no kill or exit waits on another process's teardown.

Laws checked at every leaf:

  • every claimed process published its exit;
  • every killed thread is gone;
  • no waiter is stranded, and no TLS block is orphaned.

A state where nothing can move is a deadlock. KillEachOther reaches 457 states, and every schedule ends. kernel-loom's a_nested_serve_is_not_undone_by_the_one_it_interrupted runs the nested serve on a thread of its own and explores 11 executions at a preemption bound of 2.

Every wait a killed thread can be in

Wait Site A kill
pipe write space kernel/src/syscall/io.rs:72 cancellable, returns
pipe or connection read kernel/src/syscall/io.rs:158 cancellable; kill_while_blocked arms 1–2
virtio-sound, HDA read kernel/src/syscall/io.rs:173, :188 cancellable
keyboard, console read kernel/src/syscall/io.rs:203, :218 cancellable
accept kernel/src/syscall/ipc.rs:309 cancellable; kill_while_blocked arm 3
process wait kernel/src/syscall/proc.rs:76 cancellable; kill_ends_every_wait
thread join kernel/src/syscall/proc.rs:179 cancellable; kill_ends_every_wait
sleep kernel/src/syscall/proc.rs:205 cancellable; kill_ends_every_wait
futex kernel/src/scheduler.rs:474 cancellable; kill_ends_every_wait
poll ring kernel/src/inbox/mod.rs:415 cancellable; kill_ends_every_wait
block retry backoff kernel/src/block.rs:88, from object/ops.rs the kill is read before each park; an attempt in flight is the device's and ends inside block::OPERATION
file-backed fault's block read kernel/src/file_backing.rs:96 read_block_retrying not a park: it retries BudgetExpired for up to block::DEADMAN under the process-data lock without reading the kill bit, and the kill lands when the fault returns
writeback::drain_all backoff kernel/src/block.rs:88, from writeback.rs the cancel is discarded and a second one panics; filed as issues/kernel/a-killed-shutdown-caller-panics-on-its-second-drain-backoff.md
serial WIRE sleep lock kernel/src/sleeplock.rs:100 uncancellable, bounded by its holder; userland reaches it only as the stop's caller
the stop kernel/src/quiesce.rs:189 uncancellable, bounded by quiesce::PARK; the caller is the shutdown
held-thread wait, SS probe, park_forever kernel/src/quiesce.rs:381, arch/x86_64/hw.rs:401, watch.rs actuator builds only; the first two are cancellable
iod, klogd, iod's drain iod.rs:40, :47, log/console.rs:475, writeback.rs:52 kernel threads, which no pid opens and so no kill reaches
spins sync.rs ticket lock; arch/x86_64/tlb.rs shootdown ack; drivers/virtio.rs used ring; disk waits under driver locks not parks: each is bounded by its holder, its tripwire or block::OPERATION, and the kill lands after
Ring 3 the exit boundary kill_while_blocked arm 4, mutual_kill

A thread banded by the machine's stop is never dispatched again: SafePoint::Stop outranks Exit, and the machine is going down.

Host gates, at ae06655; cargo test --lib, --list and --build-only again at 5163e9b, EXIT=0 each (5163e9b changes only the guest test kill_ends_every_wait)

Gate Exit
cargo test --lib EXIT=0, 384 passed
cargo test --workspace --exclude toyos-build EXIT=0
cargo test --manifest-path kernel-loom/Cargo.toml EXIT=0
cargo run -- --clippy EXIT=0, clippy: 10 invocations clean
cargo run -- --ci host EXIT=0, Host: 53 step(s), all green; mutate-last-out-leaves-before-its-teardown: 2 verdicts reached
cargo test --test toyos-build -- --list EXIT=0; builds the guest Rust tests, kill_ends_every_wait among them, and lists it
cargo run -- --build-only EXIT=0

red-wait-sleep.patch (kernel/src/syscall/proc.rs: sys_nanosleep parks in wait_uncancellable until its deadline) applies with git apply --check at ae06655, kernel/ cargo check and cargo check --features boot-actuators exit 0 on the mutant, and git apply -R left the tree clean.

Also at ae06655: toyos-proclife, inside the workspace gate, 32 passed. The kernel/ check variants are inside --clippy: x86_64 and aarch64, each plain, with boot-actuators, and with boot-actuators,test-actuators.

Guest runs, run by the orchestrator, at 4d612d6

Run Exit
Fast tier EXIT=0, 396 of 396 passed
kill_ends_every_wait EXIT=0
red-wait-futex EXIT=1, timed out after 300s, stdout ending on futex: killing
red-wait-poll EXIT=1, timed out after 300s, stdout ending on poll: killing
red-wait-process-wait EXIT=1, timed out after 300s, stdout ending on process-wait: killing
red-wait-thread-join EXIT=1, timed out after 300s, stdout ending on thread-join: killing
red-wait-sleep EXIT=1, timed out after 300s, but stdout ending on process-wait: killing after sleep: a kill ended it: the early kill above

Earlier, at 751e36d: mutual_kill, process_lifecycle, process_reopen_selftest, quiesce_stops_the_machine, quiesce_refuses_a_second_shutdown, and kill_while_blocked with its redlist row lifted, EXIT=0 each.

At 5163e9b: red-wait-sleep (cargo test --test toyos-build -- --nightly kill_ends_every_wait with the patch applied) EXIT=1, timed out after 300s, stdout ending on sleep: killing; the Fast tier EXIT=0, 396 of 396 passed.

Negative controls

Host, in --ci host at ae06655:

  • mutate-last-out-leaves-before-its-teardown: the last one out is marked before its teardown. the_last_one_out_is_in_its_process_until_its_teardown_is_done fails on pid 1 tid 0 is tearing its process down and the table has it out, so a stop would not wait for it.
  • The model's other controls red by their named verdicts: mutate-kill-waits-for-its-victims, mutate-first-out-tears-down, mutate-join-collects-in-a-teardown, mutate-claim-teardown-always-wins and mutate-spawn-skips-the-insert-recheck.

Host, at 751e36d as a checked patch, restored clean:

  • Shootdown::serve stores instead of raising: cargo test --manifest-path kernel-loom/Cargo.toml --test tlb_shootdown EXIT=101. a_nested_serve_is_not_undone_by_the_one_it_interrupted fails on the serve it interrupted published an older generation over it.

Guest, by the orchestrator:

  • The whole change reverted, kernel/, toyos-proclife/ and toyos-sched/ at the merge base with origin/main, tests kept, at 751e36d: mutual_kill EXIT=1, retire_task: task not released after 10000ms naming Running(CpuId(0)) and Running(CpuId(1)), a kernel panic.
  • One wait made uncancellable, one patch per kill_ends_every_wait arm: the futex, poll, process-wait and thread-join arms red on their own <wait>: killing line at 4d612d6; the sleep arm red on its own sleep: killing line at 5163e9b, above.
  • quiesce-last-teardown's hold released on any sweep, not one that counted the held thread, at 751e36d: red-teardown EXIT=1, no quiesce-last-teardown: the stop counts quiesce-last alone line before the stop's record, which reads 5 of 5 … over 2 sweep(s).

Independent oracles

  • The recorded real failure: the base kernel's deadlock under mutual_kill, which the whole-change revert reproduces.
  • toyos-sched-sim's I11, written independently of this change, for the free-after-switch the teardown relies on.
  • The real kernel under QEMU, for every wait class above.

Lines

git diff --shortstat origin/main...HEAD, at 5163e9b:

  • Whole branch: +1402/−1210 over 43 files.
  • Production kernel/src: +219/−340, net −121. toyos/src unchanged.
  • toyos-proclife: +657/−520.
  • Tests: +381/−159.
  • toyos-sched prose: +26/−85.
  • kernel-loom: +30.
  • Issues: +71/−106.
  • src/: +18.

Merge of #564 (the measured schedule), at 4e67c19

Guest runs, run by the orchestrator, at 4e67c19

  • kill_ends_every_wait: EXIT=0
  • mutual_kill: EXIT=0
  • --nightly quiesce_refuses_a_second_shutdown: EXIT=0
  • --weekly process_reopen_selftest: EXIT=0
  • Fast tier: EXIT=0 (233/233)

🤖 Generated with Claude Code

https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j

Japabu and others added 4 commits September 27, 2026 17:12
…ishes it

Two processes holding each other's handle and killing at once deadlocked:
each killer sat in `retire_task` on a thread that was itself inside the
other kill, reached no safe point, and the kernel panicked at the 10 s
tripwire (audit K1). TRANSFER makes it reachable from userland.

`kill_process` now claims, posts every retire (`scheduler::post_retire`)
and hands the rest to `reaper`, a kernel thread no process can kill, which
waits the threads out (`scheduler::await_released`) and runs the same
teardown tail as exit. The reaper stops with userland at a shutdown
(`quiesce::exempt`): it runs userland's teardowns, so a stop waits for one
in progress and stops it when parked, as it did the killer.

A process's end is pollable: its handle's read watch is the object's
watch and it is readable once the exit is published; closing one handle
ends no poll. `toyos::process::Process::watch_end` is the SDK side.

toyos-proclife's model now makes a retire a wait — a thread inside a
scripted op reaches no safe point until it returns — adds the reaper op,
a deadlock check and L6 (every claimed teardown publishes), and the
KillEachOther cases. `mutate-kill-waits-for-its-victims` restores the
base's inline kill and reds them.

Guest tests: `mutual_kill`, and a poll arm in `process_lifecycle`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling is a killer

The audit's K1 names the sibling form too (A2 kills B while B1 kills A).
19 schedules, every one ends; red under mutate-kill-waits-for-its-victims.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the stop names

The reaper stops with userland (quiesce::exempt), so the stop's exact
thread count is one higher. Measured: 11 of 11 before this, and green after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu marked this pull request as ready for review September 27, 2026 15:42
@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review of 61c0a7f: CI is red at this head. Run 36330513554 abi-split concluded FAILURE (exit 1): [sdk] toyos changed and its version did not: toyos/Cargo.toml still says 0.18.0, and the next one is 0.19.0 (the new Process::watch_end). host was SKIPPED behind it, so no CI run exists of the kernel, model or QEMU tests at this head. The previous run, 36328697000 at 8864321, also concluded failure.

NOT READY FOR REVIEW

@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review of PR #549 at 61c0a7f (high-risk: process lifecycle, scheduler)

CI: abi-split is red. The orchestrator has waived it. host runs only on merge_group, so this PR has none of its steps confirmed by CI:

  • root cargo test --lib
  • cargo test --workspace --exclude toyos-build, which includes every toyos-proclife test: two_processes_killing_each_other_both_end, a_kill_chain_of_three_ends, an_exit_whose_sibling_kills_the_process_killing_it, and the six existing tests that now carry Op::reap()
  • every CONTROLS row, the new mutate-kill-waits-for-its-victims included
  • clippy with warnings denied, which the PR body does not claim either
  • the licence judge, the kernel-loom-without-loom step, and the userland host crates

No CI job runs QEMU, so mutual_kill, process_lifecycle, quiesce_stops_the_machine and kill_while_blocked rest only on the PR body.

BLOCKER

  • kernel/src/reaper.rs:45-50 — One FIFO consumer couples unrelated kills.
    • What happens: V2's resource release and exit publish wait behind V1's release (up to the 10 s tripwire per thread) and behind V1's teardown_tail, which can be a File flush on USB. The worst case is the sum over the queue.
    • What it was before: each killer was bounded by its own victim only.
    • Who hits it: init's cut_over (old.kill(); old.wait()), sshd's end, and test-runner's deadline kill all wait behind unrelated teardowns.
    • Measurement missing: the PR body says it is unmeasured. There is also no backpressure: owe never blocks, and the killer no longer pays for the teardown.
    • Needed: a measured V2 wait while V1 has a thread parked in an unanswered block op (blockd --silence-write). Then either have the reaper take an owed kill whose threads are all released instead of the queue head, or record the compromise in issues/ with owner, evidence and exit condition.
  • kernel/src/process.rs:1678-1681 and :1059 — A one-token mutation removes the memory-freeing ordering and no test catches it.
    • The retire: fn(&ThreadSched) seam makes this a one-token patch: retire_threads(threads, main_tid, &process_data, |_| {}) in finish_kill. The teardown then frees a victim's mappings and handles while its threads may still be on a CPU.
    • mutual_kill, kill_while_blocked and the model will all stay green: the model is not the kernel, and the victims leave the kernel within microseconds.
    • Fix: add a fail-fast check at the top of the teardown, assert!(threads.iter().all(|(_, s)| s.handle.released())). Show mutual_kill red under the patch above.
  • kernel/src/object/ops.rs:312 — The claim that closing one Process handle does not cancel another holder's poll is untested.
    • The mutation KObjectRef::Process(_) => true passes its_end_completes_a_poll, because no handle is closed while a poll is armed.
    • Test to add to process_lifecycle.rs: dup the child's handle, watch_end on the dup, close the other handle, and assert that poller.wait(0, 0) stays empty until the release. It must go red under the mutation.

NOTE

  • kernel/src/reaper.rs:26,35 and kernel/src/sched/kthread.rs:118 — REAPER/is is a second registry of kthread identity, and the spawn return type change exists only to feed it. Declare "stops with userland" in the kthread row at spawn, the way OnPanic is declared, and have quiesce::exempt read the row. That deletes REAPER, is, and the console.rs churn.
  • kernel/src/process.rs:1138 and kernel/src/scheduler.rs:506 — retire_task has one caller. Post every sibling's retire first, then retire_threads(.., await_released). That deletes retire_task and the retire parameter, and siblings die concurrently instead of one by one.
  • kernel/src/syscall/dispatch.rs:243 — the self-kill special case lost its stated reason. kill_process no longer awaits, so a self-kill can take the same path: delete the branch and show that a self-kill publishes 137. Otherwise, state the reason that remains.
  • kernel/src/process.rs:1640 / kthread pids — a SysCap MANAGE holder (init) can process_open the reaper's pid and kill it. claim_teardown succeeds, and the reaper pops its own Killed and asserts "cannot retire self": OnPanic::Halt. This hole already exists for klogd/iod/usbd (10 s panic), but the reaper's safety argument rests on "no process can kill it". Refuse kill_process on a kernel task by name, or file it.
  • kernel/src/reaper.rs — the teardown now runs with IF=1 in Lock::lock spins on PROCESS_TABLE/PCID_POOL, where it used to run with IF=0 in a syscall. This puts a userland-triggerable path under audit K2 (a nested shootdown serve makes flushed go backwards).
  • tests/toyos-rust-tests/src/bin/kill_while_blocked.rs:290 — arm 4's separate killer process exists only for the reason this branch deleted. Kill from the observer.
  • toyos-proclife/src/interleave.rs — the model is exhaustive over op-section orders only. in_kernel is set only for kill_by threads. An Exit claimant's own thread retires instantly when posted. A victim thread that is not in a scripted op is never "posted but not yet released". The kernel posts each thread under separate table sections, and the model posts them in one step. None of this is wrong for the KillEachOther scope, but the 19 schedules cover that scope and nothing more.
  • PR body — the QEMU rows were measured with the kernel at 8864321, on base a637f5c, which predates The instrument moves to QEMU 11.1.1, and everything tied to it moves with it #540 (QEMU 11.1.1). Re-run them at 61c0a7f.
  • The toyos_quiesce stop record says "userland thread(s)" and now counts a kernel thread (PR body, "unsure").
  • 61c0a7f git merge -m "Merge origin/main" — the message has no backtick, so nothing was substituted. It does not matter, and nothing is to be rewritten.

REMOVE

  • kernel/src/reaper.rs:5 — "no process can kill it": false (see the NOTE on kthread pids).
  • kernel/src/syscall/dispatch.rs:243 — "Killing yourself is exiting, not killing.": narration with no reason left.
  • kernel/src/sched/kthread.rs:22 — the list of kthread names, rewritten instead of deleted, rots on the next kthread.
  • kernel/src/object/process.rs:32 — rewritten doc line.
  • tests/toyos-rust-tests/src/bin/kill_while_blocked.rs:79-80 — "scheduler::retire_task's own tripwire … the whole of the arm's ability to report": the killer is no longer inside that tripwire.
  • PR body "What I am unsure of" — compromises belong in issues/, not in main's merge record.

Brief questions

  1. Liveness and safety.
    • No deadlock. The reaper holds no lock while parked. A killed thread's release never waits on a teardown: SYS_PROCESS_WAIT, polls and joins are cancellable, and the only uncancellable waits are the ones in quiesce, WIRE and retire.
    • Starvation and unbounded lag: yes (BLOCKER 1).
    • Double free: none; the claim excludes exit, kill and poison.
    • Never freed: only OWED abandoned at shutdown.
    • A panic in the reaper halts the machine; the 10 s tripwire also halts, as before.
  2. Lock order.
    • Killer: PROCESS_TABLE → dlmalloc, then OWED → dlmalloc, with nothing held across.
    • Reaper: the watch lists, then exactly the existing teardown edges (18-21, 4, 3), then publish_exit → Watch → Inbox.state with nothing held.
    • Nothing reaches OWED, so there is no cycle. The only new hazard is the IF=1 context (NOTE).
  3. Model. It is faithful for the claim/post/owe/reap shape. 19 is every interleaving of those three ops' sections, with no reduction applied, so it is exhaustive for that scope only (NOTE).
  4. Shutdown. Yes. A pending teardown is abandoned, whether the reaper is banded while parked or OWED is non-empty. That is equivalent to a frozen userland killer before this change, and a running reaper is waited for until it parks.
  5. Design. Keep the dedicated thread.
    • Tearing down at the victim's last scheduling would make the Ring 3 epilogue parkable on every return path. It would also change toyos-sched's single-retirer protocol, since a claimant would retire siblings that already carry kill bits, and change its loom models.
    • That is more code, and riskier code. The reaper leaves the retire protocol untouched.
    • Fix its head-of-line coupling, and move its quiesce exemption into the kthread row.
  6. Net lines and the merge slip.
    • Overall +639/−141.
    • Production: kernel +138/−39 and toyos +8/−0, so +146/−39 (net +107).
    • src/ci.rs +4. Model +317/−61. Tests +172/−41.
    • The NOTEs above would delete retire_task, the retire parameter, REAPER/is, and the dispatch special case.
    • The -m slip does not matter.

SEND BACK

Japabu and others added 2 commits September 27, 2026 18:35
… asserts it

Answers the review of #549 at 61c0a7f.

The reaper no longer pops its queue in order and waits for the head. Every
thread release now also posts `sched::payload::ANY_RELEASED`, and the
reaper waits on that (or on its own `WORK` when nothing is owed). It takes
the oldest owed kill whose threads are all released, so a slow victim no
longer delays another kill's release. The tripwire is per kill and counts
from when the kill was owed (`scheduler::RETIRE_GIVE_UP`, the same 10 s).
The teardowns still run one at a time on one thread. That is filed as
issues/kernel/the-reaper-runs-every-kills-teardown-one-at-a-time.md.

`fold_retired` (was `retire_threads`) asserts that every thread it folds
is released before any teardown frees what they ran on. Both paths wait
before they reach it: exit through `await_released`, and the reaper
through its choice. Mutation: the reaper takes a kill whose threads are
not all released (`killed.released() || true`). mutual_kill then goes red
with "teardown: a thread of the process is still on the scheduler", both
wide and alone.

Deleted:
- `scheduler::retire_task`. The exit path posts every sibling's retire
  first, then awaits each one, so siblings die concurrently.
- `retire_threads`' `retire` parameter.
- `reaper::REAPER`/`is` and the `kthread::spawn` return change. A kernel
  thread now declares `OnStop::Runs` or `OnStop::Stops` in its row at
  spawn, and `quiesce` reads the row (`kthread::runs_through_the_stop`).
- The self-kill branch in dispatch. A self-kill posts its own retire,
  dies at its Ring 3 boundary, and the reaper publishes 137. mutual_kill
  now checks that.

Also:
- `block::counted` reads the same row, so a block operation the reaper
  opens is counted as the stop's.
- The stop record's thread count loses "userland": it now counts the
  reaper too.
- `Shootdown::serve` raises `flushed` with `fetch_max`, so a serve nested
  inside another cannot move it backwards (audit K2, now reachable from
  the reaper's IF=1 teardown). kernel-loom's
  `a_nested_serve_is_not_undone_by_the_one_it_interrupted` reds under
  `store`.
- process_lifecycle: closing another handle to a running process leaves a
  poll on it empty. It reds under `KObjectRef::Process(_) => true`.
- kill_while_blocked arm 4 kills from the observer. The separate killer
  existed only because a kill used to wait.
- The model's reaper takes a released kill, and its exit posts every
  retire before it waits.
- Stale `retire_task` citations in toyos-sched follow the rename. The sim
  paragraph that said the kernel never batches one process's retires is
  deleted, because it now does.

Filed: issues/kernel/a-manage-holder-can-kill-a-kernel-thread.md.

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

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review of PR #549 at a7457a3, round 2 (high-risk: process lifecycle, scheduler)

CI at a7457a3: abi-split is red (run 36334134842, toyos 0.18.0 not bumped); the orchestrator has waived it because that gate is gone from main. host was SKIPPED (it runs only in merge_group). No CI run confirms any host or QEMU step, so every gate here rests on the PR body's table.

Round-1 BLOCKERs

  • B1, one FIFO consumer: CLOSED. reaper.rs:60 takes the oldest owed kill that is released. The PR body's after-rows discriminate: B was published in 0.53 ms while blockd took 87.7 ms, and before the change B never came before A. The teardowns that still run one at a time are filed. The fix's own claim has no test; see the new BLOCKER below.
  • B2, the unguarded teardown ordering: CLOSED. process.rs:1060 asserts every thread is released. Under killed.released() || true, mutual_kill went EXIT=1 with the assert's message, wide and alone.
  • B3, closing another Process handle: CLOSED. Under Process(_) => true, process_lifecycle went EXIT=1 with a running process's handle completed a poll: [1].

BLOCKER

  • kernel/src/syscall/proc.rs:102 — Userland can kill the reaper or any kernel thread, and the kernel panics. Filed as an issue; it should be fixed here instead.
    • sys_process_open gives out a Process handle for any pid in the table, a kthread's included. SYS_PROCESS_KILL then posts a retire to a thread that has no Ring 3 boundary to die at.
    • Killing iod, usbd or klogd trips the reaper's 10 s assert. Killing the reaper leaves it owing its own teardown to itself, and the same assert follows. Both are OnPanic::Halt.
    • Who can do it: init, the one MANAGE holder. That is still userland panicking the kernel, and this branch adds the kthread whose death the round-1 doc said could not happen.
    • The fix is a few lines: refuse a kernel thread's pid in sys_process_open as NotFound, the way a gone pid is refused. kthread::ROWS already names every kthread, and the issue's own exit condition describes this fix.
    • A test must go red when the refusal is deleted. Then delete issues/kernel/a-manage-holder-can-kill-a-kernel-thread.md.
  • kernel/src/reaper.rs:60 — No test can fail on the round's main claim, "a slow victim holds up no other kill's release".
    • Mutation: replace owed.iter().position(|(_, killed)| killed.released()) with owed.first().filter(|(_, k)| k.released()).map(|_| 0). This is FIFO head-of-line blocking again.
    • Every committed test stays green under it. mutual_kill's victims release within microseconds. The model's take_released is a separate copy of this logic, so no kernel edit can turn it red. The kill-lag measurement was a scratch patch that was never committed.
    • Cheapest fix: move the choice into toyos-proclife as one pure function that next_released and the model's take_released/owes_a_released both call. Then add a law checked at every state: the reaper never stays idle while a released kill is owed. KillEachOther already reaches the state that tells the two apart: kill 1 is at the head and its victim is still in the kernel, while kill 2's victim has already left.
    • The alternative is a guest test that holds victim A unreleased and shows B published first.
    • Either way, it must go red under the patch above.

NOTE

  • Brief Q2, can exit's new shape deadlock? No.
    • Two threads exiting at once: claim_teardown lets exactly one through. The loser returns from release_process into exit_current, and its release is what the claimant waits for.
    • Exit racing a kill: whichever claims second gets Ok and waits on nothing. The posts are the only two calls to retire::begin, and both are behind the claim, so single-retirer holds.
    • No killed thread waits on the reaper. Only the claimant runs await_released, and a claimant is never a victim.
    • release_thread and kill_process never park, so a sibling inside either one reaches its safe point.
  • Brief Q3, is the selection starvation-free? Yes.
    • A released kill waits at most for the released kills owed before it. Kills owed after it never pass it.
    • An unreleased kill blocks nothing. It is bounded by the tripwire, and checking only the head's deadline is enough because the head's is the earliest.
    • The one unbounded-looking term is teardown time, and that is CPU-bound. OpenFileState::drop (object/file.rs:19-25) only queues a write-back and never waits on the device. Round 1 was wrong to say a teardown can sit behind a USB flush.
  • Brief Q4, is the OnStop row the declaration round 1 asked for? Yes.
    • One field is set at spawn beside OnPanic, and quiesce and block::counted both read it.
    • quiesce_stops_the_machine pins Stops for the reaper: with Runs, the count would be 10, not 11.
    • REAPER/is and the spawn return-type change are gone.
  • Brief Q5, the kthread-kill hole: decided above. It is a kernel-crash-from-userland defect that is cheap to close, so it is a BLOCKER to fix here, not an issue.
  • kernel/src/sched/payload.rs:130 — ANY_RELEASED is posted on every thread release in the machine.
    • Cost: one global lock per release, and while a kill is owed, a reaper rescan per release.
    • The reaper only needs releases of killed threads. Post under shared.kill_pending() instead.
    • Then an idle reaper can wait on ANY_RELEASED too, and WORK, the idle branch and the continue re-check (reaper.rs:28,55-66) go away.
    • Either way, deleting the ANY_RELEASED.post() in owe (reaper.rs:42) is a lost wakeup that no test can see.
  • issues/kernel/the-reaper-runs-every-kills-teardown-one-at-a-time.md — the issue's exit condition asks for a stimulus that does not exist: no file close holds a teardown (see Q3). The issue is also status: open with no owner.
  • Net lines:
    • Whole branch: +897/−280. Production (kernel/src, toyos/src): +250/−89, net +161, up from round 1's net +107 despite the four deletions; the reaper grew from about 50 lines to 83.
    • Model, pure crates and loom: +398/−110. Tests: +188/−81. Issues: +57. src/: +4.
    • This round: +334/−215, of which production is +156/−102.
    • Still deletable: the kthread issue file once it is fixed, and WORK with its idle dance (see the ANY_RELEASED NOTE).
  • K2 (fetch_max) is correct. kernel-loom takes shootdown.rs by #[path], so the nested-serve test runs against the kernel's own source.

REMOVE

  • issues/kernel/the-reaper-runs-every-kills-teardown-one-at-a-time.md:23-25 — "No long teardown has been measured; the one PR Kernel: a kill never waits on its victim — the last thread out tears its process down #549's review names is a file's flush on a USB stick" — false: a close never flushes.
  • issues/kernel/the-reaper-runs-every-kills-teardown-one-at-a-time.md:31 — "(a file whose flush the device does not answer)" — the same false premise.
  • issues/kernel/a-manage-holder-can-kill-a-kernel-thread.md:19-22 — "Not run yet … the expected end is" — an unmeasured guess; it goes with the fix.
  • PR body, the "Negative controls" bullet — "The review named the patch … That seam no longer exists, and this mutation is its equivalent" — review chatter in main's record.
  • PR body, the "Blocker 1: …" heading and framing — review-round narrative. The evidence already lives in the issue.

SEND BACK

Japabu and others added 2 commits September 27, 2026 19:01
…hoice is the model's

A kernel thread's pid names no process a handle could hold:
`process::process_object`, the one door from a pid to a `Process` object,
refuses it, so `SYS_PROCESS_OPEN` answers NotFound and a MANAGE holder can no
longer kill iod, usbd, klogd or the reaper into the tripwire's halt. The
control is a second verdict under `process-reopen-selftest`: after the last
kthread is spawned, every row's pid is asked for its object.

The reaper's choice of owed kill is `toyos_proclife::teardown::next_released`,
which the kernel and the model both call; the model's own copy is deleted. The
model's reaper now parks on ANY_RELEASED the way the kernel's does, a kill's
posts, its owe and its return are separate sections, and a new law (L7) holds
at every state: the reaper never sleeps while a released kill is owed. The
explorer skips a state it has reached before, since every law is a property of
the state alone; without that the three-kill chain took 78 s.

ANY_RELEASED is posted only on a killed thread's release. The kill mark and the
release share one word on the TaskHandle, so a release and a kill posted at once
decide by one read-modify-write each: `Hw::release` holds no `TaskShared` whose
kill bit it could read. The idle reaper waits on ANY_RELEASED too, which deletes
`WORK`, the idle branch and the re-check.

The teardown issue is deleted: every handle drop in a teardown either only
queues work (a file's write-back) or is a deferred row released from the zero
queue, so no stimulus can hold one. The kthread issue is deleted with its fix.

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

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review of PR #549 at 1c5ca17, round 3 (high-risk: process lifecycle, scheduler)

CI at 1c5ca17: run 36336743687, host success (--ci host and --ci gate-stage both success). The guest rows rest on the PR body's table.

Round-2 BLOCKERs

  • B1, a kernel thread's pid opens: CLOSED. With the refusal at process.rs:676 deleted, process_reopen_selftest gave EXIT=1 and process-open-kthread: FAIL (4 kernel threads, 4 opened). At the head it gives EXIT=0.
    • The refusal is structural. process_object is the only lookup from a pid to a ProcessObject. Its other callers are the two selftests. loader/mod.rs:648 takes the object of the process it has just spawned. PoisonWake::Process only publishes.
    • No other route exists. SYS_KILL (65) and SYS_WAITPID (26) are retired. Dup, TRANSFER and spawn slots copy handles the caller already holds. A kernel thread's handle table is empty. SYS_PROCESS_STATS takes a handle. No object names a thread.
    • It breaks no kernel use: nothing in the kernel asks process_object for a kthread.
    • The ROWS publish and the table insert happen under one hold of PROCESS_TABLE, and process_object takes that lock, so the check cannot race a spawn. Pids are never reused (IdMap::insert_with), so a row that stays after a Recover thread dies names no later process.
  • B2, no test can fail on "a slow victim holds up no other kill": CLOSED. With next_released taking only the head, cargo test -p toyos-proclife gave EXIT=101 in three tests, each failing L7. The kernel calls the same function.

BLOCKER

  • kernel/src/reaper.rs:36 — owe's ANY_RELEASED.post() has no red, and this round made it load-bearing.
    • Before this round, an idle reaper slept on WORK, and WORK.post() ran on every first kill mutual_kill makes. Deleting it hung the test. Now a killed thread's own release wakes the idle reaper. owe's post is needed only when every victim was released before owe, and nothing reaches that.
    • If the post is lost, the idle reaper sleeps on Deadline::never() and the kill is never published. The tripwire cannot fire either: it only arms for kills the reaper has seen. init's old.kill(); old.wait() would hang without a word.
    • The model's World::owe is a copy, not the kernel's code. toyos-proclife is pure and cannot hold a Watch post, so the post cannot move there. Only a kernel test can turn it red.
    • Patch the red must catch: delete reaper.rs:36.
    • Cheapest red I see:
      • A boot-actuator arm in process::kill_process, between the posts and reaper::owe: for (_, s) in &threads { scheduler::await_released(s) }.
      • A guest test under that arm kills one child that holds no handle back and waits for its exit within the 5 s harness deadline.
      • With the patch applied, the test must time out.
      • mutual_kill cannot be this test. Its second kill's owe wakes the reaper for the first. Its self-kill would also trip await_released's not-self assert.
    • When this is closed, delete the PR body's "Unsure" section.

NOTE

  • kernel/src/sched/payload.rs:134-149 — Q2: the fate word is not a second rule.
    • It has one writer, post_retire, which sets KILLED before claim_retire. TaskShared::mark_kill has no kernel caller. It has one reader, publish_released, deciding whether to post.
    • The two marks disagree only as KILLED without KILL: between the two read-modify-writes, or on a thread that is already released. Either way the cost is one extra post.
    • One home for the mark would need RELEASED inside toyos-sched's state word, or SeqCst on both sides of a two-word Dekker. The one-word design is smaller than either.
    • The ordering holds:
      • Release first: the killer's AcqRel read-modify-write acquires it, then OWED's lock, then the reaper's Acquire load sees RELEASED.
      • Kill first: the release sees KILLED and posts after its read-modify-write. The reaper armed before it checked.
  • kernel/src/sched/payload.rs:137 — The kernel's KILLED gate has only the model's red. if fate & KILLED != 0 → if false should push each mutual_kill round to the reaper's 10 s deadline, past the 5 s harness timeout. Record that measurement next to the model's.
  • kernel/src/sched/payload.rs:148 — TaskHandle::mark_killed sits beside toyos-sched's TaskShared::mark_kill: two kill marks whose names differ by two letters. A call to the wrong one silently skips either the retire or the post.
  • kernel/src/reaper.rs:45 — B2 closes on the shared function. The call site's argument is still a transcription: owed.iter().take(1).map(..) would pass every test. Accepted as the shape round 2 asked for.
  • toyos-proclife/src/interleave.rs:264-277 — Q4: the model is faithful.
    • Posts, owe and return are separate sections, as the kernel's lock sections are.
    • Treating post_retire/retire as atomic is faithful only because of the one-word fate design.
    • The reaper's arm-and-check is one step. That is equivalent, because the kernel arms before it checks.
  • toyos-proclife/src/interleave.rs:403 — The de-duplication is sound. It keys on the whole (World, Vec<Op>), and every field of both derives Hash/Eq, including reaper_asleep, owed, killed and the ops' locals. faults, final_faults and the deadlock test each read only that pair. The trace is used only for the report.
  • kernel/src/sched/kthread.rs:135 — task != CLAIMING is defensive at a site where "the last kernel thread is spawned". A row still in CLAIMING there would drop out of the count unseen. Assert on it, or delete the filter.
  • The teardown issue's deletion is accepted. Round 2 found that its stimulus does not exist. Running the teardowns one at a time is stated in reaper.rs's header.
  • Net lines:
    • Whole branch: +917/−301.
    • Production (kernel/src, toyos/src): +282/−98, net +184. This round: +78/−55, net +23. Of that, open_selftest and its call site are actuator-only.
    • Model, pure crates and loom: +433/−115. Tests: +198/−88. This round deletes two issues (−57).
    • Nothing more to delete that I can name.

REMOVE

  • kernel/src/reaper.rs:7-8 — "Which owed kill it takes is toyos_proclife::teardown::next_released;" — restates the import at line 12 and the call at line 45.
  • PR body, Lines — "reaper.rs: 68 lines, from 83." — a count in main's record that the next edit makes false.

SEND BACK

…ire wait

A kill claims its victim, posts every thread's retire and returns. Each
thread leaves by its own hand (`process::leave`): at its Ring 3 exit
boundary when killed, in `exit` or `thread_exit` otherwise. It leaves its
address space, folds its blocked-time accounting into the process's and
marks itself out under the table lock, and the one whose leaving empties a
claimed process frees the process and publishes its exit on its own stack.
Its kernel stack goes as every stack does, in `Hw::release` on the pass after
the switch. Nobody waits for another thread's release, so a kill cannot
deadlock on its victim by construction.

Deleted: `kernel/src/reaper.rs` and its kernel thread, `ANY_RELEASED`, the
owed queue, `next_released`, the `OnStop` row, `scheduler::await_released`
and `RETIRE_GIVE_UP` with the reaper's tripwire, `TaskHandle`'s released
flag and accounting copy, `ProcessAccounting::child_threads_cpu_ns`, and the
teardown's thread-by-thread fold. `issues/kernel/retire-tripwire-is-not-
queue-shaped.md` closes: its constant is deleted and nothing replaces it.

The decisions are `toyos-proclife`'s and the kernel calls them:
`claim_teardown` now carries the exit code, `retire_set` is the claim's
retire set, and `leave` answers whether the thread leaving is the last one
out. A poisoned thread's death is its leaving, done for it by the idle loop:
a poisoned main thread claims its process and retires the rest, and a
poisoned last thread publishes the claim's code. A join does not collect a
thread of a process being torn down: that thread's TLS is still mapped where
its siblings may run.

The model drops the reaper and scripts every thread's way out as its own
steps. Laws at every state: one claim and one teardown per process, nothing
freed or published while a thread is still in, a stack freed only after the
switch, no mapped TLS dropped under a running sibling, and no kill or exit
waiting on another process's teardown; at every leaf, every claimed process
published and every killed thread gone. New controls:
`mutate-first-out-tears-down` and `mutate-join-collects-in-a-teardown`.

`kill_ends_every_wait` kills a child parked in a futex, a poll ring, a
process wait, a thread join and a sleep, and waits for each to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu Japabu changed the title Kernel: a kill posts and returns, the reaper finishes it; a process end is pollable Kernel: a kill never waits on its victim — the last thread out tears its process down, and a process's end is pollable Sep 27, 2026
@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review of PR #549 at fa1e325, round 4. The design is new (last thread out) and is judged whole. This is high-risk code: the process lifecycle, the scheduler and the quiesce stop.

CI at fa1e325: run 36341735456, host success, with --ci host and --ci gate-stage each success. No PR job runs QEMU, so the guest rows rest on the PR body's table.

Earlier BLOCKERs

  • Round 3 B1, a kernel thread's pid opens: CLOSED.
    • The refusal is at process.rs:672-679 and is unchanged since it was measured red with the refusal deleted, at 1c5ca17.
    • At fa1e325 the body gives process_reopen_selftest EXIT=0 with process-open-kthread: PASS (3 kernel threads, 0 opened).
  • Round 3's owe post: CLOSED by deletion.
    • reaper.rs, owe and ANY_RELEASED are gone: git grep ANY_RELEASED HEAD finds nothing.

BLOCKER

  • kernel/src/process.rs:1043-1050 with kernel/src/quiesce.rs:225-229 — the shutdown's stop no longer waits for a teardown in flight. Data loss.
    • The order at this head: proclife::leave marks the last thread out Zombie and note_progress posts, and only then does teardown run.
    • sweep skips every zombie, so a stop taken while a teardown runs returns early. The shutdown then runs drain_all and sync_all (syscall/machine.rs:110-111) beside that teardown's close_all.
    • An OpenFileState dropped there enqueues its write-back after the drain. Those dirty pages are then not durable at power-off: machine.rs:109 says sync_all misses exactly these pages.
    • The teardown's syscalls:, memory: and exit: records also land after the boot's last word.
    • On main this could not happen:
      • teardown_bookkeeping's mark_all_zombie came after teardown_resources.
      • A kill's teardown ran on the killer, which was still Scheduled.
      • So the sweep counted the thread doing the teardown as running.
    • How it is reached:
      • a single-threaded process inside SYS_EXIT when the stop opens; or
      • the last thread out of a process that was claimed before the stop, still inside its teardown.
    • What the fix must do: count a thread whose process teardown is in flight as running, and post progress only after teardown.
    • The red it needs: a staged boot, in the quiesce-last-* pattern, that holds the last thread out between proclife::leave and teardown_resources until the stop sweeps. The stop must wait for it. At fa1e325 the stop must return first.
    • Also run the quiesce family at the fixed head: quiesce_stops_the_machine, quiesce_last_* and quiesce_twice. None is in the body's guest table, yet this change moves the zombie mark and the note_progress those tests read.
  • kernel/src/scheduler.rs:386,388 — hazard 1's claim has no red.
    • The claim is "the teardown can park, and a park at depth zero asserts".
    • I found no park reachable from leave or teardown. teardown_resources, close_all's drops and publish_exit do not park, and the ZeroHandles hooks are deferred and never take a sleep lock.
    • At depth 0, a guard's preempt::enable only preempts, which the epilogue's own loop already does.
    • Patch: delete crate::preempt::disable(); and crate::preempt::enable_no_resched(); around process::leave(None).
    • I expect kill_while_blocked arm 4 and mutual_kill to stay green under it.
    • Either name the park and show one of those tests red under the patch, or delete both lines and the comment on line 385.
  • kernel/src/scheduler.rs:569-571 and process.rs:724-733 — the kernel's poison changes have no guest measurement.
    • panic_recovery is the one guest test through reap_poisoned, and it is not in the body's table.
    • No guest arm has a poisoned thread that has a sibling.
    • The model cannot see this: Op::Poison does its own posts.
    • Patch: delete for sched in &wake.retire { post_retire(sched); }.
    • The red it needs, as a guest arm:
      • a child parks a second thread in a futex, and its main thread takes SYS_DEBUG action 0;
      • the parent's wait returns -1;
      • under the patch, the arm times out.
    • Also run panic_recovery at the head.
  • kernel/src/process.rs:761-764 and :785-788 — current_data() and process_data() end the thread silently when its entry is gone.
    • The branch's own invariant makes this unreachable. toyos_proclife::teardown::leave asserts that an entry outlives every thread that has not left it, and route_thread_exit and release_thread now expect it.
    • If it is ever reached, the thread exits without leaving. Its claimed process then never tears down, and every wait on it hangs with no report.
    • The branch edited both lines. Fail fast there, with the same panic.

NOTE

  • Hazard 3, the idle loop leaving on a poisoned thread's behalf.
    • The work is bounded: one table section, the retire set, the posts and one publish, for at most MAX_CPUS × SLOTS threads.
    • The idle loop is not a thread the kernel creates, so this fits the ruling.
    • This branch widens it in two ways:
      • A poisoned last thread of a killed process skips teardown_resources. Its handles and address space then go with reap_finished's entry drop on the idle loop, where on main the killer's teardown freed them.
      • A thread poisoned while it held its ProcessData lock now deadlocks its last sibling at leave's process_data.lock() (process.rs:1042). On main, the siblings hit the same lock at their next syscall.
    • Halting on a kernel panic in syscall context would honour "a kernel bug crashes loudly". It would also delete poison.rs, POISONED, PoisonWake and the TLS issue.
    • That choice is the owner's, not this branch's. File it as kind: question.
  • Shootdown::serve's fetch_max: keep it; a present reason holds.
    • Kernel threads run with IF=1 (entry.rs kernel_start: sti), and so does the idle loop (reap_poisoned's PROCESS_TABLE.lock()).
    • A contended Lock::lock there calls tlb::poll → serve_if_owed, and the 0xFE IPI can nest a second serve inside it.
    • The outer store then puts back an older generation, and the initiator spins to ACK_TIMEOUT's panic.
    • This is reachable on main without this branch. sync.rs:140's "this spin runs with IF clear" is false in those contexts. That comment predates this branch.
  • The pollable process end has no production caller.
    • git grep watch_end finds only process_lifecycle.
    • The Process arms in read_watch, has_data and close_ends_polls, the Arc<Watch>, toyos::process::watch_end and its_end_completes_a_poll all exist to serve that one test.
    • This is outside K2's fence. Delete it, or name the caller.
  • The wait audit is missing a row.
    • The missing wait: file_backing.rs:96 read_block_retrying loops on BudgetExpired for up to block::DEADMAN, under the process-data lock, without reading the kill bit.
    • A killed thread that faults on a file-backed page therefore reaches its boundary only after that loop ends. It is bounded, and it is not a park.
    • The kernel has no futex requeue and no IPC reply wait. The fsd client waits in toyos/src are the pipe, poll and futex rows.
  • tests/toyos-rust-tests/src/bin/kill_ends_every_wait.rs:11 — an arm can pass without its wait.
    • The marker is printed before the park, so a kill that lands first ends the child at its syscall exit instead, and the arm passes without the wait under test.
    • Only the sleep arm has a measured red.
    • Fix: make the parent see the child parked, or measure each arm's red.
  • Conflict with Kernel: every kernel panic halts and panic recovery is deleted; delete usbd; open the no-kernel-threads track (K1) #553.
  • The two filed defects: both are real and correctly scoped, and neither is on this path.
    • drain_all: take_cancel asserts at payload.rs:133. The callers are the shutdown syscall and revoke_selftest, which this branch does not change.
    • TLS: an unclaimed poisoned sibling collected by a join. Main is identical.
  • The teardown now runs on a killed thread whose single cancel may already be spent. take_cancel panics on a second one. Nothing in teardown waits cancellably today. That belongs as an invariant on teardown's doc.
  • Q1, kill racing thread_create: nothing escapes.
    • spawn_thread checks admit_thread_insert, then inserts the thread, calls enqueue_new and set_sched, all under one hold of PROCESS_TABLE.
    • claim reads retire_set under that same lock.
    • The model scripts spawn racing an exit, a kill, two spawns and a reap, and mutate-spawn-skips-the-insert-recheck reds.
    • The kernel calls claim_teardown, retire_set, leave, zombify_poisoned, collect_zombie, admit_*, route_thread_exit and finished_pids.
    • What is transcribed rather than called is the order of effects in Op::step and depart_step: thread_exit's post after leave, and the publish outside the lock. A kernel reorder of those does not red the model.
  • Q2, the stack freed after the switch: safe on SMP, and unchanged from main.
    • dispose_dead releases the payload on the CPU that switched away from the task.
    • switch loads the incoming root (hw.rs:450 and :456, idle included), so no CPU keeps a lazy CR3.
    • I11 reds a finalize whose context was never saved.
  • The model's tests.
    • L3 (model.rs:424-428) checks only depart_step's own Switch → Release order. Its teeth test frees a stack by hand. The real check is toyos-sched's I11.
    • interleave.rs:619 assert!(states > 0) asserts nothing.
  • Lines (git diff --shortstat origin/main...HEAD):
    • Whole branch: +1364/−1185.
    • Production (kernel/src, toyos/src): +174/−337, net −163.
    • toyos-proclife: +763/−576. Tests: +313/−82. Issues: +52/−105. toyos-sched: +26/−85. kernel-loom: +26. src/: +10.
    • Still deletable: the pollable end, and the preempt bracket if its park is not shown.

REMOVE

  • PR body, "Deleted" bullet: reaper.rs, ANY_RELEASED, the owed queue, next_released, the OnStop row, await_released, RETIRE_GIVE_UP and "the reaper's 10 s tripwire" do not exist on main (git grep on origin/main finds none of them). Main's record would list deletions it never had.
  • PR body: "The reaper model is deleted." Main has no reaper model.
  • PR body: the "Kept:" framing is relative to this branch's own earlier heads.
  • kernel/src/quiesce.rs:225-226: "A zombie has already written its exit: record and holds no task" is false at this head.
  • tests/toyos-rust-tests/src/bin/kill_while_blocked.rs:211: "The clock starts before the kill and not after it." Its reason was deleted, and the bare assertion is left.
  • issues/kernel/deferred-release-outlives-its-syscall.md:199: "An owner has to be the thread, not the CPU." Its only reason was deleted.

SEND BACK

@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

PR #553's round-5 review corrected its "What #549 must drop" section. Posting the corrected list here since it is a merge instruction for this PR, not part of #553's own record:

Not a drop, but shared ground: tests/toyos-rust-tests/src/bin/process_lifecycle.rs is edited by both branches — #553 deletes "panic recovery" from its module doc.

Japabu and others added 5 commits September 28, 2026 08:07
main's #553 deletes panic recovery: every kernel panic halts. This branch's
poison work goes with it: `toyos-proclife/src/poison.rs`, `Op::Poison` and
its two scripts, `World::poison` and its `poisoned` set, `process::PoisonWake`
and `zombify_poisoned`, the idle loop's poison bank in `reap_finished`, the
TLS issue a poisoned sibling raised, and the "poison path" wording in
`teardown.rs` and `ProcessEntry::teardown_code`.

`kthread::open_selftest` reads `ROWS` as `[AtomicU64]`. `mark_thread_zombie`,
which main kept and this branch's `leave` replaced, goes. main.rs's
`usbd::start()` context is gone with usbd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
The machine's stop counts a zombie as nothing left to stop. The last thread
out was marked a zombie by `proclife::leave`, and `note_progress` posted,
before its teardown ran; a stop taken then returned while that teardown's
`close_all` still dropped files, and the shutdown's `drain_all` and
`sync_all` ran beside it. A dropped file's write-back was queued after the
drain, and its dirty pages were not durable at power-off.

`proclife::leave` now marks every thread but the last one out, and answers
`Last { code, mark }`; `proclife::torn_down` marks that one in
`teardown_bookkeeping`, after its records and releases, and `note_progress`
posts after that mark. The model scripts the mark as its own step, and its
L3 now reads: a teardown in flight keeps its thread in the process. L3's
old sentence, a stack freed only after the switch, checked the model's own
step order; toyos-sched-sim's I11 is that check, and the model's copy and
its hand-run teeth test go.

`mutate-last-out-leaves-before-its-teardown` marks the last one out before
its teardown, as the head before this did; it reds
`the_last_one_out_is_in_its_process_until_its_teardown_is_done` on L3 and
`only_the_thread_that_empties_a_claimed_process_tears_it_down` on its
location assertion. `quiesce-last-teardown` holds the last thread out of a
child of `quiesce_last` between its leaving and its teardown until the stop
counts it alone; `quiesce_wakes_on_the_last_teardown` judges that boot.

The exit boundary calls `process::leave` at its own preempt depth: nothing
the teardown runs parks, and a park there asserts. `current_data` and
`process_data` panic on a missing entry rather than ending the thread
without leaving: an entry outlives every thread that has not left it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Nothing but `process_lifecycle`'s own arm polled it: the `Process` arms of
`read_watch`, `has_data` and `close_ends_polls`, `ProcessObject`'s
`Arc<Watch>`, `toyos::process::Process::watch_end` and
`its_end_completes_a_poll` go back to main's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
A kill returns before its victim ends, so the killer can observe the end
itself: arm 4 kills the spinner and waits for its exit code. The in-guest
`ENDS_WITHIN` deadline and the timed stdout reader go; a spinner the exit
boundary misses never ends, and the harness's hang ceiling says so.

The deferred-release issue loses the sentence whose only reason was
`retire_task`'s park.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
`a_nested_serve_is_not_undone_by_the_one_it_interrupted` ran the nesting as
one written-out schedule, one execution. The nested issue and serve now run
on a spawned thread, so loom places them at every point of the outer serve:
11 executions at a preemption bound of 2, and the model reds when `serve`
stores what it owes instead of raising to it.

Filed: `sync.rs`'s lock spin says it runs with `IF` clear, and kernel
threads and the idle loop spin there with it set, which is where one serve
nests inside another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu Japabu changed the title Kernel: a kill never waits on its victim — the last thread out tears its process down, and a process's end is pollable Kernel: a kill never waits on its victim — the last thread out tears its process down Sep 28, 2026
@Japabu

Japabu commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review of PR #549 at 0c48340, round 5. This is high-risk code: the process lifecycle, the scheduler and the quiesce stop.

CI at 0c48340: run 36388092522 (pull_request, headSha 0c48340) concluded success. Its job host succeeded, including the steps cargo run -- --ci host and cargo run -- --ci gate-stage. No PR job boots QEMU, so every guest verdict below comes from the orchestrator's 549r5-*.log.

Earlier BLOCKERs (round 4, fa1e325)

  • R4-B1, the stop did not wait for a teardown in flight: the code is CLOSED, the red is OPEN.
    • The code: proclife::leave answers Last without marking the thread. torn_down marks it inside teardown_bookkeeping, and note_progress comes after (process.rs:1034-1038).
    • The host control reds: mutate-last-out-leaves-before-its-teardown in --ci host.
    • The guest red this BLOCKER asked for does not exist. 549r5-red-teardown.log reads PASS quiesce_wakes_on_the_last_teardown … over 2 sweep(s), EXIT=0. B1 below carries it.
  • R4-B2, the preempt bracket around leave: CLOSED by deletion. scheduler.rs:383 calls process::leave(None) with no bracket.
  • R4-B3, the poison changes had no guest measurement: CLOSED by deletion. Kernel: every kernel panic halts and panic recovery is deleted; delete usbd; open the no-kernel-threads track (K1) #553, which is this branch's base (1ec6daa), deleted poison. git grep reap_poisoned HEAD -- kernel finds nothing.
  • R4-B4, current_data and process_data exited the thread silently: CLOSED. Both now expect (process.rs:776, :796). git grep 'exit_current(-1)' HEAD -- kernel/src/process.rs exits 1.
  • The six round-4 REMOVEs are gone.

The orchestrator's questions

  • red-whole: the red is named, and the patch reverts the whole change.
    • The panic under src/process.rs:1065:9 reads retire_task: task not released after 10000ms: Running(CpuId(1)) on both CPUs. That is the K1 deadlock.
    • Each backtrace is kill_process → retire_threads → retire_task. retire_task is #[track_caller], which is why the location is in process.rs.
    • The patch is byte-identical to git diff HEAD 1ec6daa9 -- kernel toyos-proclife toyos-sched, index lines aside. That is the whole production change reverted onto the branch's merge base, with the tests kept. toyos/src has no net change.
  • red-teardown: the mutation does what it claims, and the test cannot see it. Under the mutation the held thread is Zombie before its teardown, and sweep skips it (quiesce.rs:224). See B1.
  • red-wait-*: a red that names its wait is possible with no guest clock.
    • When a test hits the ceiling, the harness already puts the streamed program stdout into the TestResult (tests/common/qemu.rs:3654-3661).
    • But check_rust_result's result.error branch prints only kernel_account (tests/toyos.rs:3044-3046).
    • The fix is N2.
  • Fast tier, lan_mdns_answer: confirmed as the defect A boot's sockets are a TempDir under /tmp, reclaimed like all scratch; disable partition_claim_departure and i8042_mouse #560 fixed. 549r5-fast.log:182 reads UNIX socket path '…/lane-10/tap-out-0.sock' is too long / Path must be less than 104 bytes. It is not this branch's.

BLOCKER

  • B1. tests/common/power.rs:391 with kernel/src/quiesce.rs:328: quiesce_wakes_on_the_last_teardown cannot fail on its claim that a stop waits for a teardown in flight. red-teardown survived with EXIT=0.
    • Why the mutant survives:
      • hold releases on the first sweep that counts exactly one running thread (RUNNING != 1). That thread need not be the held one.
      • The harness decides on record.sweeps >= 2. Any thread still in Ring 3 at the first sweep meets that.
      • Under the mutant, the held thread is a zombie that no sweep counts, and the boot passed in 2 sweeps (3 when green).
    • Fix, in the kernel's quiesce::last:
      • record the held thread's id in hold;
      • under boot-actuators, have sweep note whether it counted that thread as running;
      • release hold only on a sweep that counted exactly one running thread and counted the held one, and log "{name}: the stop counts {LAST_THREAD} alone" at that point.
    • Fix, in the harness's woken_by_the_held_thread:
      • require that line before the stop: record, in place of record.sweeps < 2;
      • for quiesce-last-teardown, also require the held process's exit: test_rs_quiesce_last pid=… code=0 record between that line and the stop: record.
    • The measurement it needs:
      • red-teardown-counted-gone.patch → quiesce_wakes_on_the_last_teardown exits non-zero, naming the missing line;
      • all three quiesce_wakes_on_the_last_* are green at the fixed head. The park and exit tests move to the same check.

NOTE

REMOVE

  • tests/common/power.rs:352-354: "a stop that counted it as gone would return at its first sweep". It is false as measured: the mutant returned at its second sweep and passed.
  • kernel/src/process.rs:1026: "teardown_bookkeeping's wake needs that". teardown_bookkeeping wakes nothing; the wake is publish_exit's.
  • kill_ends_every_wait.rs:11-13: the compromise belongs in issues/ (N5), not in the module doc.
  • The PR body's "Negative controls → Guest" predictions ("must red"). The body carries the measured exits, and the red-teardown row measured green.
  • The PR body's "Unsure" section. It is speculation, and its one real item is N5.

SEND BACK

Japabu and others added 4 commits September 28, 2026 11:34
Takes #560, #541, #565, #563, #569 and #570. `src/ci.rs` and
`tests/toyos.rs` merge without conflict; `rust` takes main's pin, 1b236638,
since this branch carries no fork commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…lone

`hold` released on the first sweep that counted one thread running, which
need not be the held one. With the last thread out marked a zombie before its
teardown, no sweep counts it; a sweep that counted some other thread still in
Ring 3 released the hold, and `quiesce_wakes_on_the_last_teardown`, which
judged on `sweeps >= 2`, passed with the teardown unwaited.

`hold` now claims the held thread's id, `sweep` notes whether it counted that
thread as running, and the hold ends only on a sweep that counted exactly one
running thread, the held one. It then logs `<actuator>: the stop counts
quiesce-last alone`. The harness requires that line before the `stop:` record
for all three `quiesce-last-*` boots, and for the teardown boot also the held
process's `exit: test_rs_quiesce_last pid=... code=0` record between the two.

The Teardown hold sits in `process::leave`, which the Ring 3 exit boundary
also reaches, at depth 0, where `yield_now` asserts. `hold` refuses by name
there (`scheduler::may_yield`), logs that the thread left outside a syscall,
and lets the stop go on unheld; the harness then reds on the missing line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…h IF set

`kernel_exit_to_user_check` is entered with IF clear, so `process::leave` at
the exit boundary ran a killed process's whole teardown -- `close_all`, the
address-space drop, the records -- with interrupts masked on that CPU. A
syscall's exit runs the same teardown with IF set. `leave_ring3_if_due` now
sets IF around `leave` and clears it again before the exit pass, the bracket
`kernel_exit_to_user_check` already puts around `do_preempt`.

The preempt depth is unchanged: 0, `BASELINE_IRQ_EXIT`, which is
`do_preempt`'s own. What IF adds is a nested interrupt, which on a Ring 0
return reaches no scheduler entry, and a `preempt::enable` at depth 0 calling
`do_preempt`, which asserts that same baseline. A park asserts at depth 0
with IF either way, and nothing the teardown runs parks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
`check_rust_result` printed only the kernel's account when the harness ended
a test at its ceiling, though the streamed stdout is in the result; it now
prints it, as the exit-code branches do. `kill_ends_every_wait` prints
`<wait>: killing` before each kill, so a wait the kill cannot end is the last
line of that red.

The race by which an arm passes without reaching its wait moves out of the
module doc into `issues/kernel/a-kill-ends-every-wait-arm-can-pass-without-reaching-its-wait.md`,
and `quiesce::last`'s in-guest `STAGED` deadline is filed as
`issues/diagnostics/the-quiesce-last-staging-dies-on-ten-seconds-of-guest-clock.md`.
`teardown`'s doc loses a clause naming a wake `teardown_bookkeeping` does not
make.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu

Japabu commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review of PR #549 at 751e36d, round 6. This is high-risk code: the process lifecycle, the scheduler and the quiesce stop.

CI at 751e36d: run 36405909561, headSha 751e36d, concluded success, and its one job host succeeded. The PR is not a draft. Every guest verdict below comes from the orchestrator's 549r6-*.log.

Earlier findings (round 5, 0c48340)

  • B1, quiesce_wakes_on_the_last_teardown could not fail on its claim: CLOSED.
    • The red: 549r6-red-teardown.log exits 1 on no "quiesce-last-teardown: the stop counts quiesce-last alone" line before the stop's record. Its record is 5 of 5 … over 2 sweep(s).
    • The green: all three quiesce_wakes_on_the_last_* pass by name at 751e36d. Each one's stop: record is 5 of 5 … over 3 sweep(s).
  • N1, merge origin/main: CLOSED by 393a4f2.
  • N2, the ceiling red named no arm: CLOSED in part.
    • check_rust_result now prints the stdout, and each arm prints killing.
    • The sleep arm's red names the wrong arm. See NOTE 1.
  • N3, IF across the teardown at the exit boundary: CLOSED, and correct.
    • The bracket is reachable only from kernel_exit_to_user_check, at depth 0, and scheduler.rs:382 asserts that depth.
    • A nested IRQ enters at depth 1. Its Ring 0 return skips exit_to_user, because idt/mod.rs tests the CS RPL.
    • A preempt::enable that drops to 0 with need_resched set reaches do_preempt, whose baseline is 0.
    • That switch was already reachable with IF clear, because need_resched can be set on entry: leave_ring3_if_due runs before the loop reads it. IF set only makes it more frequent.
    • A thread switched out mid-teardown resumes on incoming.root, which the payload's address_space keeps alive until Hw::release.
    • While it is Ready and unmarked, the sweep counts it as running, so the stop waits for it.
    • The locks it holds with IF set are the ones exit's syscall path already holds with IF set.
  • N4, the hold at the Ring 3 boundary: CLOSED, and sound.
    • may_yield is yield_now's own precondition, count() == blocking_baseline(). At the boundary (depth 0) it is false, and the hold refuses by name instead of asserting.
    • No staged boot reaches the refusal, because the teardown child leaves through SYS_EXIT. It is actuator-only, and it refuses by name.
  • N5 and N6: CLOSED by the two filed issues. Both have an owner and an exit condition. See REMOVE for one false line.
  • N7, kill_while_blocked stays disabled: open, and unchanged. Main's row still points at deferred-release-outlives-its-syscall.md. One more green run with the row lifted is still not a rate.
  • The five round-5 REMOVEs: CLOSED. The power.rs sentence, the process.rs clause, the kill_ends_every_wait module-doc lines and the body's "Unsure" section are all deleted. The "must red" predictions are gone, but "owed" lines replaced them (see REMOVE).

The orchestrator's questions

  • red-whole: a valid negative control.
    • The patch equals git diff HEAD $(git merge-base HEAD origin/main) -- kernel toyos-proclife toyos-sched byte for byte, index lines aside.
    • mutual_kill panics with retire_task: task not released after 10000ms: Running(CpuId(0)) and Running(CpuId(1)). That is K1.
  • Do the wait arms' reds name their own wait? The futex, poll, process-wait and thread-join reds each end on their own <arm>: killing line.
    • The sleep red does not.
    • The sleep mutation breaks no other wait. The process-wait arm's waited process is spawn("sleep", None), parked in nanosleep(u64::MAX).
    • Under the mutation, waited.kill() cannot end it, and waited.wait() (kill_ends_every_wait.rs:54-56) hangs after process-wait: killing and before process-wait: a kill ended it.
    • So the red is the sleep wait, reported under the process-wait arm, and the sleep arm itself is never reached.
    • The thread-join child's parked thread also sleeps, in std::thread::sleep, so that arm would hang under the sleep mutation too. NOTE 1 has the fix.
  • Fast tier, quiesce_wakes_on_the_last_teardown: the record cannot say whose failure it is. The branch owns the red either way.
    • The failure's record: 4 of 6 userland thread(s) stopped … in 2010 ms of a 2010 ms budget over 2 sweep(s).
    • Main's Disable quiesce_stops_the_machine and quiesce_wakes_on_the_last_park behind their filed defects #574 sightings on the park boot (539r7, 555r2, 559): 3 of 5 … 2010 ms … over 2 sweep(s).
    • Both show the same shape: exactly two threads never reached a safe point, and no progress post woke the stop after its first sweep.
    • In both, the held thread is one of the two by construction. It spins in hold's yield_now loop at syscall depth until a sweep counts it alone, and that sweep never comes.
    • That yield loop is main's code. This round changed only its release condition (ALONE for RUNNING != 1), and a stop that is never woken fails the same way under either condition.
    • I found no path by which this branch's teardown changes keep a second thread from its safe point. The held thread holds no lock in hold: the_named_thread drops the table guard, and leave drops its guard before hold.
    • This failing boot counted 6 userland threads, where every green teardown boot counts 5. One thread that is normally gone, most likely the parent's exit thread, was still live at the first sweep.
    • No main run exists for the teardown case, because git grep last_teardown origin/main finds nothing: the test is new on this branch. It has run in two Fast tiers: 549r5 passed and 549r6 failed. The park test passed in both.
    • stopped_boot's gave-up error (tests/common/power.rs:321) is the one error in that function that omits {whole}. So neither this sighting nor main's three names the two threads, and the serial was not kept (target/red-run-serial/toyos-tmp-42615-0 has no quiesce boot).
    • Ruling: most likely Disable quiesce_stops_the_machine and quiesce_wakes_on_the_last_park behind their filed defects #574's defect in the shared staging, and not provable either way.
  • Lines (git diff --shortstat origin/main...HEAD):
    • Whole branch: +1369/−1208 over 41 files.
    • Production kernel/src: +216/−340, net −124.
    • This round: kernel/src +63/−22, net +41. All of it is the actuator-only hold and may_yield, plus the IF bracket (3 lines).
    • toyos-proclife +657/−520, toyos-sched +26/−85, tests +342/−157, kernel-loom +30, src/ +14, issues +84/−106.
    • Production still shrinks. Nothing further in this round's diff is deletable without losing B1's check.

BLOCKER

  • tests/toyos.rs:1013 with src/redlist.rs — quiesce_wakes_on_the_last_teardown is a Fast-tier test this branch adds, and it red in the Fast tier at the head it would land with.
    • The rule: a flaky test is disabled at once.
    • Close it in one of two ways:
    • Either way, append \n{whole} to the gave-up error at tests/common/power.rs:321, as its sibling errors in stopped_boot already do, so the next sighting names the two threads.

NOTE

REMOVE

  • PR body, "At 0c48340, before this round:" and its three bullets: the story of an earlier round.
  • PR body, "At 751e36d: owed. The same set, and the Fast tier.": false now that the runs exist.
  • PR body, "Guest, measured by the orchestrator at 0c48340:": remove the "A teardown counted as gone … This round's quiesce::last change is the fix." bullet. It is a superseded measurement and narration.
  • PR body, "Guest, owed at 751e36d: …": false now. It also says "against origin/main", but the patch reverts onto the merge base.
  • PR body, "The depth-checked entries are Parkable::mint and yield_now … and exit_current (1).": a restatement of the tree that rots.
  • PR body, "An arm can pass without reaching its wait." and "quiesce::last's STAGED is an in-guest deadline.": each restates the issue title beside it.
  • issues/kernel/a-kill-ends-every-wait-arm-can-pass-without-reaching-its-wait.md: remove "The per-arm mutations … each went red when measured, so each arm reached its wait in those runs;". It is false for sleep: that red came from the process-wait arm's waited process.

SEND BACK

…nd sleep leads the wait arms

`quiesce::hold` claimed the one hold slot before checking whether the thread
may yield, and left it claimed on refusal, so the thread a boot actually
stages could never take it. The slot is now released on refusal.

`kill_ends_every_wait`'s `process-wait` and `thread-join` arms both sleep
underneath (the waited process's `nanosleep`, the joined thread's
`std::thread::sleep`), so a mutation that breaks sleep surfaced under one of
their names. `sleep` now runs first among the three.

`stopped_boot`'s gave-up error was the one error in the function that omitted
the boot's whole log, so a sighting of a stop giving up named no threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Japabu and others added 5 commits September 28, 2026 14:18
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…ark's disabled issue

Its Fast-tier sighting on PR #549 at 751e36d is the same shape: two threads
the stop never counts down to, one of them the held thread by construction.
The teardown boot's own extra thread beyond quiesce_last's usual five is the
defect the issue already tracks, not this branch's teardown change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Under the sleep mutation (`sys_nanosleep` parking uncancellably) the sleep
arm still passed: its child exited 137, and the ceiling hang came only from
the process-wait arm, whose waited process runs the same `nanosleep(u64::MAX)`
through the same `sys_nanosleep`. The arm and the mutation were right; the
kill was early. The parent killed on reading the child's `parked in <wait>`
marker, which the child writes before the syscall that parks, so a kill that
lands while the child is still returning from that write is honoured at the
write's `kernel_exit_to_user_check` and the named wait is never entered. The
waited process of the process-wait arm had a whole second spawn's time to
park, and hung as the mutation says it should.

`spawn` now also polls the `SysCap` roster until the child's main thread is
`SCHED_BLOCKED`, under a five-second hang guard. Between the marker's write
and the named wait's syscall the child runs nothing else that parks: its
code is paged from `/system`, which is the memory image. That is the filed
issue's exit condition, so it is closed.

`tests/testcases/system.toml` no longer counts the binaries that read the
roster; this one makes five and the next landing moves it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
A QEMU test's only clock is the harness's hang ceiling, so the five-second
in-guest guard on the roster poll goes. The poll sleeps 10 ms between reads,
as futex_wake_counts' wait_until_parked does, and prints
`<wait>: waiting for the roster to show it parked` before it starts, so a
ceiling red names where it stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu

Japabu commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 8, at 5163e9b

CI at 5163e9b: host COMPLETED SUCCESS. Guest runs by the orchestrator at 5163e9b: the Fast tier EXIT=0, with PASS kill_ends_every_wait (170ms) and PASS mutual_kill; red-wait-sleep EXIT=1. No round-7 review was posted, so this round reviews 751e36d..5163e9b.

Earlier findings (round 6, 751e36d)

  • BLOCKER, quiesce_wakes_on_the_last_teardown red in the Fast tier: CLOSED by option (a).
    • src/redlist.rs:79-82 disables it against the park issue, and the issue carries the 751e36d sighting.
    • tests/common/power.rs:321 appends {whole}.
    • Measurement: the 549r7 run by name says No enabled test matches filter.
  • NOTE 1, the sleep red named the wrong arm: CLOSED.
    • At 4d612d6, with sleep first and no roster poll, the stdout read sleep: a kill ended it and then process-wait: killing (549r7-red-wait-sleep.log:44-46). Reordering the arms alone did not fix it; the kill landed before the park.
    • At 5163e9b the stdout ends on sleep: killing (549r8-red-wait-sleep.log:47-48).
    • Together the two runs show the roster poll decides the outcome.
  • NOTE 3, a refused hold kept the slot: CLOSED (kernel/src/quiesce.rs:343).
  • NOTE 2, the body's guest record: OPEN. See the NOTE below.
  • NOTE 4, merge: OPEN. The branch is 2 first-parent commits behind again (A std build keeps nothing another compiler compiled, and a bootstrap run leaves the fork's lockfiles as it found them #573, Clipboard copied once into a region the compositor made; copy-once is a type, the compositor forbids unsafe code #557); git merge-tree --write-tree HEAD origin/main exits 0.
  • Round-6 REMOVEs: CLOSED. None of them is left in the body, and the issue that carried the false line is deleted.

The orchestrator's questions

  • In-guest time bound: none remains.
    • Two Durations are left in kill_ends_every_wait.rs: the poll interval at :95 and the joined thread's sleep(3600s) loop at :138. Neither is a bound.
    • No Instant or deadline appears in the file.
    • poller.wait(1, u64::MAX, ..) and nanosleep(u64::MAX) are waits the kill must end.
    • Neither tests/toyos.rs nor src/ has a per-test ceiling for kill_ends_every_wait, so the harness ceiling is the only clock. The 10 ms re-read is a poll on a state that has no notification. It is not a flat wait.
  • Does the roster prove the child is parked in the wait under test? Yes, for all five arms. The roster shows a park, and reading the code shows that park is the named one.
    • What the roster shows: sched_state maps both Blocked and Committing to SCHED_BLOCKED (kernel/src/sched/payload.rs:164). Committing is phase 1 of the park handshake, and a kill that claims it makes that same wait's commit refuse to park (toyos-sched/src/park.rs:28). So "BLOCKED" means the thread is inside a park, not merely alive. entry[9] == 0 pins the main thread, which is the one parked in the join arm.
    • Why that park is the named one: after the marker's write returns, nothing else in the child can park.
      • A handle-sink write is the marker's own syscall, and log_stdio::flush on a handle sink does nothing (toyos/src/log/stdio.rs, finish).
      • The std allocator spins (rust/library/std/src/sys/alloc/toyos.rs).
      • A demand-paging fill never parks: it runs under the process-data lock (kernel/src/file_backing.rs:80-89).
      • The kernel's only SleepLock is serial WIRE.
      • Every arm's next call is its named syscall. thread_join is SYS_THREAD_JOIN (std/src/sys/thread/toyos.rs:56), not a futex.
    • The measurement: with the check absent at 4d612d6, the sleep arm passed its mutant. With the check present at 5163e9b, it hangs on its own line.
  • Does red-wait-sleep revert the whole behaviour it controls? Yes.
    • It replaces the one cancellable watch::wait_until in sys_nanosleep with wait_uncancellable re-parked until the deadline. Nothing else ends that park, so a retire's wake just re-parks.
    • sys_nanosleep is unchanged on this branch (git diff origin/main...HEAD -- kernel/src/syscall/proc.rs is empty). So this is a mutation that shows the arm can fail, not the branch's negative control.
    • The branch's negative control is still the whole-change revert measured red at 751e36d. Since then, production has changed only in the actuator-only HELD store.
    • The parent's own 10 ms sleeps still end on their deadline under the mutant: the futex and poll arms finish before the hang.

BLOCKER

None.

NOTE

  • tests/toyos-rust-tests/src/bin/kill_ends_every_wait.rs:110 — the roster buffer holds 256 entries, and the kernel truncates without a word (syscall/machine.rs: if i >= max_entries break). Entries are sorted by pid, and the newest child sorts last. On a crowded boot the child is cut off, and the poll spins to the ceiling under a line that blames the wait. Fix: assert SysinfoHeader::decode(..).entries <= 256 by name.
  • tests/toyos-rust-tests/src/bin/kill_ends_every_wait.rs:94 — a child that dies after its marker, whose entry is gone or has state 3, costs a 300 s ceiling where one roster read could fail by name. Fix: panic in the poll when the pid's main thread is absent or zombie.
  • tests/toyos-rust-tests/src/bin/kill_ends_every_wait.rs:113 — this is the fifth reader that hand-spells the roster entry's offsets 8 and 9. The others are process_lifecycle.rs:258, abuse_thread_name.rs:62, toybox ps.rs:64 and audio_idle_suspend. toyos-abi decodes only the header (SysinfoHeader::decode, "a second spelling is a reader that walks off by a field"). File the missing entry decoder in issues/; it is outside this fence.
  • issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md, Exit — the issue now also disables quiesce_wakes_on_the_last_teardown. That is the only guest check that a stop waits for a teardown in flight (R4-B1, data loss). But neither the Exit list nor "What no enabled guest test checks" names it, so the issue can close with the teardown test still dark. Add both.
  • PR body, "Guest runs … at 4d612d6" — record the 549r8 exits: Fast EXIT=0, and red-wait-sleep EXIT=1 ending on sleep: killing.
  • Merge origin/main (A std build keeps nothing another compiler compiled, and a bootstrap run leaves the fork's lockfiles as it found them #573, Clipboard copied once into a region the compositor made; copy-once is a type, the compositor forbids unsafe code #557) before landing; the merge is clean.
  • Lines: the whole branch is +1402/−1210 over 43 files. This round, merges excluded, is +65/−34, of which kernel/src is +3 (actuator-only). Nothing in this round's diff is deletable.

REMOVE

  • PR body, "Owed at 5163e9b: …" — false now; the runs exist.
  • PR body, "## Unsure" and its bullet — round 5 already removed an Unsure section. This one's content is answered above.
  • PR body, the "A refused hold …" bullet, from "Neither a host test nor a kernel-loom model fits" to "certified by reading, not measurement." — narration of why nothing tests it.
  • PR body, the row "quiesce_wakes_on_the_last_{exit,park,teardown}, by name | EXIT=1: each is disabled …" — it measures nothing.
  • PR body, "The mutation and the arm's sleep were right; the kill was early." — narration.
  • issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md:23-24, "the sixth is the defect this issue is open for, not this PR's teardown change." — an attribution round 6 found could not be proved either way.

LAND AFTER NAMED CHANGES

Japabu and others added 2 commits September 28, 2026 18:20
…ss ceiling, on a header over 256 entries or a main thread gone/zombied; file the missing roster-entry decoder as its own issue; name the teardown claim the quiesce issue leaves dark.

- kill_ends_every_wait.rs asserts the roster header's entry count is at most the 256-entry buffer, and its poll panics by name when the child's main thread is absent from the roster or zombied, rather than spinning to the harness's ceiling.
- issues/design-debt/toyos-abi-decodes-only-the-roster-header-not-its-entries.md: toyos-abi decodes the sysinfo header but not an entry, and four readers hand-spell its offsets 8 and 9 instead.
- issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md: quiesce_wakes_on_the_last_teardown is the only guest check that a stop waits for a teardown in flight, named in both Exit and what no enabled test checks; the unproven sixth-thread attribution is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu
Japabu added this pull request to the merge queue Sep 28, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 28, 2026
#564 deleted `quiesce_wakes_on_the_last_exit`, `quiesce_dump_holds_the_stopped`,
their actuators `quiesce-last-exit` and `quiesce-dump`, `quiesce_last`'s exiting
thread, and collapsed `quiesce::last::Last` to the park. This branch had edited
all of them only to carry its new `quiesce-last-teardown` beside them; those
edits have no purpose without their subjects, so the deletions are taken.

`quiesce-last-teardown` and `quiesce_wakes_on_the_last_teardown` are this
branch's own, not something #564 deleted: they are the guest check that the
stop waits for a teardown in flight (the last one out stays in its process
until `torn_down`). So they stay, and with them `Last` stays an enum of two
(Park, Teardown), `hold` keeps its `last` argument and `sys_nanosleep` passes
`Last::Park`, and `woken_by_the_held_thread` stays a helper with two callers.

- kernel/src/actuator.rs: `quiesce_last_exit` and `quiesce_dump` go (main);
  `quiesce_last_teardown` stays (branch).
- kernel/src/quiesce.rs: `last` keeps the branch's claimed-id hold, `ALONE`
  and the `may_yield` refusal; `Last::Exit` goes.
- kernel/src/syscall/proc.rs: main's `hold()` becomes `hold(Last::Park)`.
- tests/common/power.rs: `quiesce_wakes_on_the_last_exit` and
  `quiesce_dump_holds_the_stopped` go (main); the park and teardown verdicts
  keep the branch's `counts … alone` and teardown `exit:` checks.
- tests/toyos-rust-tests/src/bin/quiesce_last.rs: the exiting thread goes
  (main); the teardown child stays (branch).
- tests/toyos.rs: `quiesce_wakes_on_the_last_teardown` registered Nightly
  beside `quiesce_wakes_on_the_last_park`, which #564 moved to Nightly: same
  binary, same verdict, same disabling issue, and a new row has no catch
  record to put it anywhere else. Its CARRIES row and dispatch arm stay; the
  exit and dump rows go.
- toyos-quiesce `LAST_THREAD`, tests/quiescelastcase/system.toml and the two
  quiesce issues: name the teardown actuator beside the park, not the exit;
  the "in a Fast tier" of the gave-up issue's exit conditions is deleted,
  since both its tests are Nightly now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu

Japabu commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review, merge of #564, at 4e67c19

Scope: git show --remerge-diff 4e67c194 (parents 80276cb, 807f456; base ec0a91a). CI host SUCCESS at 4e67c19; mergeable MERGEABLE. Guest runs by the orchestrator at 4e67c19: Fast 233/233, kill_ends_every_wait, mutual_kill, --nightly quiesce_refuses_a_second_shutdown, --weekly process_reopen_selftest EXIT=0 each.

Accounting: of the 44 files the branch changes against ec0a91a, 38 carry the branch's delta onto 807f456 unchanged (sorted -U0 hunk lines compared file by file). The other 6 are the resolved ones, plus 3 files only the merge edits (kernel/src/syscall/proc.rs, toyos-quiesce/src/lib.rs, tests/quiescelastcase/system.toml). Every #564 hunk in the resolved files is taken: quiesce_last_exit, quiesce_dump, Last::Exit, the exit arm in sys_thread_exit, the exiting thread, quiesce_wakes_on_the_last_exit and quiesce_dump_holds_the_stopped with their rows, their CARRIES entries and their dispatch arms. Nothing #564 deleted comes back. Last, hold(last) and woken_by_the_held_thread each still have two callers because of quiesce_wakes_on_the_last_teardown. The one code edit the merge makes itself, hold(Last::Park) in sys_nanosleep, is exercised by quiesce_refuses_a_second_shutdown, which is green at the head. With Last::Teardown in that call, await_the_held_thread would panic and that test would go red. Placing the teardown row at Nightly follows #564's rules: the author of a row picks its tier, the row sits beside its measured sibling and shares that sibling's redlist issue, and it runs nowhere while it is disabled.

Net against origin/main (git diff --shortstat 807f4561 4e67c194): 47 files, +1521 −1228. Kernel and crates +908 −901. Tests, harness, loom and sim +485 −219. Issues +109 −108. The resolved and merge-only files grow from +144 −57 on the branch to +195 −73 against main: this is the Last enum and the helper that #564 had collapsed and the teardown staging keeps.

BLOCKER

None.

NOTE

  • kernel/src/quiesce.rs:333-346 — the may_yield refusal is reached only through Last::Teardown, and quiesce-last-teardown only through a redlisted test, so no enabled run executes it. The gave-up issue's exit tracks this; the merge did not introduce it.

REMOVE

  • toyos-quiesce/src/lib.rs:70-73 — the list of actuators in LAST_THREAD's doc repeats kernel/src/actuator.rs:119,122 across a crate boundary. It went stale on the branch (it still said park and exit), and the merge rewrote it instead of deleting it.
  • tests/quiescelastcase/system.toml:1-3 — the list of tests that read this file repeats stopped_boot's path in tests/common/power.rs. It went stale on the branch, and the merge rewrote it.
  • tests/toyos-rust-tests/src/bin/quiesce_last.rs:1,3,10-12 — the counts "Two" and "Both" and the list of tests that read it. tests: the measured schedule — Fast is every PR, then Nightly, then Weekly; 15 never-caught tests and the kernel code only they armed deleted #564 moved them and the merge moved them again, and CARRIES already names the readers.
  • tests/toyos.rs:181-182 — "…_park and …_teardown run it" repeats CARRIES rows 1606-1607, and the merge rewrote it.

LAND AFTER NAMED CHANGES

Japabu and others added 2 commits September 28, 2026 19:18
…ad of removing

The merge onto 807f456 rewrote comments that named specific tests or
actuators where a canonical list already exists elsewhere (kernel/src/actuator.rs,
tests/common/power.rs, CARRIES in tests/toyos.rs): toyos-quiesce's LAST_THREAD
doc, quiescelastcase/system.toml's header, quiesce_last.rs's module doc, and
tests/toyos.rs's RUST_SKIP entry all drop their repeated lists rather than
carry them forward again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu
Japabu added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 762e4ba Sep 28, 2026
1 check passed
@Japabu
Japabu deleted the wt/toyos-killfix branch September 28, 2026 17:59
Japabu added a commit that referenced this pull request Sep 28, 2026
Brings in #572 (host QEMU's edk2), #580 and #579 (disabled reds), #549
(a kill never waits on its victim) and #566 (metaltalk redial).

- src/redlist.rs: one row each for user_copy_races_munmap and
  quiesce_leaves_the_volume_whole, which both sides added. main's new rows
  stay (netd_refused_accept, quiesce_wakes_on_the_last_teardown,
  root_chunk_refused_on_a_usb_stick, syscall_window_nmi). The rows for
  tests or issues this branch deleted go (hda_tone, doom_sound_flood,
  latency_wake, sched_check_build), and so does lan_swap, whose issue
  main deleted with swap_netd's and swap_crash_rolls_back's rows.
- The two issue files both sides added take main's text.
- tests/common/power.rs: main's woken_by_the_held_thread, shared by the
  new quiesce_wakes_on_the_last_teardown, without the two clock verdicts
  this branch took off QEMU (stopped_the_machine in stopped_boot, and
  woken_by_its_threads).
- tests/common/qemu.rs: qemu_command takes main's firmware_vars and has
  no audio_wav, so profile_argv passes six paths. The
  too_many_arguments allow goes, because seven parameters do not
  trigger it.
- kill_while_blocked.rs: main's text. After #549 a kill does not park
  in retire_task, so this branch's doc for arm 4 was false. main's arm
  also has no clock.
- tests/toyos.rs check_rust_result: this branch's single-print form,
  which already carries the stdout main added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Japabu added a commit that referenced this pull request Sep 29, 2026
Main's rust pin has not moved since the last merge, so the fork is unchanged.
Every conflict, and how it was resolved:

Modify/delete, main deleted:
- issues/build/a-swaps-redial-races-a-hard-dial-ceiling-against-an-unbounded-guest-gap.md:
  #566 fixed the defect and deleted the issue. This branch had added one sighting to
  it, and a sighting of a fixed defect has no home, so the file stays deleted.
- src/heartbeat.rs: #562 deleted `kernel_heartbeat`'s CPU-mask and gap verdicts with
  the file. This branch had given its done-line table blockd and fsd rows. The table
  goes with the verdict it served.
- tests/doomcase/system.toml: #562 moved the doom audio tests to metal and deleted
  their QEMU config. This branch had added blockd and fsd rows to it. Nothing boots
  it now.

Modify/delete, this branch deleted:
- tests/toyos-rust-tests/src/bin/ftruncate_flush_race.rs,
  tests/toyos-rust-tests/src/bin/quiesce_fsync.rs,
  issues/build/ftruncate-flush-race-reds-intermittently-and-nothing-says-why.md,
  issues/build/quiesce-leaves-the-volume-whole-needs-its-flush-to-close-inside-the-stops-budget.md
  and issues/kernel/a-root-metadata-read-refused-on-budget-is-not-retried.md: main's
  hunks remove timing from them or note its own runs. They are about the kernel FAT
  flush, the stop's kernel sync and the kernel's metadata read, which this branch
  deletes, so they stay deleted.

Content:
- kernel/src/actuator.rs: main's `quiesce_last_teardown` (#549) is kept. The kernel
  FAT actuators `fat_flush_meta_refuse`, `resize_evict_window` and
  `resize_fault_refuse` stay deleted. `process_reopen_selftest` stays where this
  branch has it, with main's doc (#549 also opens every kernel thread's pid).
- src/redlist.rs: both conflicted rows go. `doom_sound_flood` left QEMU with #562,
  and this branch deletes `ftruncate_flush_race`.
- tests/common/gpt.rs: this branch's `device_saying` and decoy `boot` are kept. Main
  drops the `drain_serial` window, so its `qemu` binding is no longer `mut`.
- tests/common/inspect.rs: main's "nothing plays audio" (#562 deleted
  `inspect_plays`) is taken, with this branch's clause on the boot stick.
- tests/common/iommu.rs: main's `panic-reboot-fast` and its wait for the fatal path's
  reset are kept. This branch's `iommu_empty_domain` reads the xHCI's DCBAAP over
  QMP, and QEMU has exited by the time that reset is seen. So `fault_boot` now takes
  a `holding` read, which it runs after the fault line and before it waits for the
  reset, while the fatal path holds its panel. `iommu_context_absent` reads nothing
  there.
- tests/common/origin.rs: main's judgement of `log_ring_keeps_the_owners_slots` is
  taken whole: init says it waited a flush out, or its stop line is missing. That
  drops the millisecond inference between two records, whose record this branch had
  changed from `Syncing filesystems...` to the stop record (#562: no QEMU test
  measures time).
- tests/common/volumes.rs: main's timing edit to `ftruncate_flush_race` goes with
  the test.
- tests/logstallcase/system.toml: main drops `power` and the `shutdown` symlink, since
  the metal row reads `/log` without a stop. This branch's blockd and fsd rows are
  kept, because fsd holds `/log`.
- tests/toyos-rust-tests/src/bin/blockd_io.rs: main's `claim_when_free`, now generic
  and with no deadline, is taken inside this branch's `if let Some(syscap)`. `bench`
  is this branch's blockd-only arm with main's timing removed: no MiB/s, and the
  line says only how many Flushes each run took. The module doc's "timed" goes.
- tests/toyos-rust-tests/src/roster.rs (add/add): both sides wrote one roster
  decoder. Main's is taken whole, because five binaries read it and it has no
  deadline (#562). This branch's copy had a 5 s give-up.
- tests/toyos-rust-tests/src/bin/process_lifecycle.rs: main's is taken whole. This
  branch's only change to it was the move onto its own roster.rs.
- tests/toyos-rust-tests/src/bin/process_stats.rs: main's `refused_calls_are_counted`
  and its roster wait for the held child are kept, and so are this branch's two
  connection arms. The system capability is taken once in `main` and passed to the
  three arms that read the roster, since a second take of the label finds nothing.
  The connection arms now wait on main's `threads_of` for the child's main thread
  to be blocked, with no deadline.
- tests/toyos-rust-tests/src/bin/quiesce_twice.rs: main's `Duration`-only import.
  This branch deletes the owed file, so `File` and `Write` go.
- tests/toyos.rs:
  - RUST_SKIP: main's audio rows are taken. `audio_tone_load` goes, since main
    deleted it. `log_volume_reread` goes, since this branch deletes it.
  - MACHINE_TESTS: `quiesce_leaves_the_volume_whole` stays deleted.
    `quiesce_wakes_on_the_last_teardown` comes from main with main's comment.
    `blockd_serves_nothing` is kept. `hda_tone` and `hda_client_stall` went to metal
    with #562, and `hda_two_live_refused` takes main's comment.
  - CARRIES and dispatch: the same.
  - `nvme_wide_sector`: this branch's blockd arm, which already had no drain window.
- toyos-quiesce/src/lib.rs: this branch's `FILES_MS`, `FLUSH_MS` and `SYNC_MS` are
  kept, with main's `LAST_THREAD` doc, which names both quiesce-last actuators.
- userland/logd/src/policy.rs: this branch deletes the module doc and the
  `LOG_WRITE_BUDGET` paragraphs main edited one line of, so they stay deleted.

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