Skip to content

Stage 6 step 2, built: the longest interrupts-off and preemption-off windows per CPU, and a held window the T14 reads back - #649

Merged
Japabu merged 17 commits into
mainfrom
wt/toyos-sk6b
Oct 2, 2026
Merged

Japabu merged 17 commits into
mainfrom
wt/toyos-sk6b

Conversation

@Japabu

@Japabu Japabu commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Stage 6 step 2 of the small-kernel track (issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md): the longest interrupts-off and preemption-off windows per CPU, printed beside the IRQ census, fed by each architecture's masking primitives and entries.

This lands the instrument and leaves the step's comparison open. The track's step 2 now says:

  • Built, and the exit's first half met: the instrument reads back a window of known length on the T14, and the row's judge refuses a boot that reads it back shorter than it was held or at more than twice that.
  • Exit, open: neither window is longer with step 1 (Stage 6 step 1: a device's handler posts its watch; irq_ring's UserDev and Audio go #634) than with it reverted on the tree that carries the instrument. That is the median of at least five interleaved boots an arm, each read from the load's own report with a report taken as the load starts, against a tolerance stated before measuring, under a load in which a handler step 1 changed posts into a watch that reaches a ring with parked threads. ring_park_herd is not that load, and this pull request does not attempt the comparison.

"Step 1 reverted" is one patch, posted on this pull request (comment 5957681949): sha256 110d0aeb…7628, 23 files under kernel/ and toyos-sched/src/. git apply --check at this head: EXIT=0.

This head is b6635359a: the gates, the patch check above and this head's staging are at it (logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-named/logs/). Every other measurement is at 0aa8d4c88 unless said (logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r6/logs/). git diff --stat 0aa8d4c88 b6635359a is one space in tests/toyos.rs's virt_smp, main's line restored, and three issue files, and this head stages the kernel 0aa8d4c88's boot ran (The images the T14 booted).

The T14's readings

Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71). Every boot and the judgings at their own heads were run by the orchestrator.

Three boots at 0aa8d4c88: its kernel, which is this head's, and two mutated kernels (comment 5960575031)

Each boot: cargo run --bin toyos-metal -- --image … --readback … --fat32-check, EXIT=0, every job (idle_span, pwd, ring_park_herd) exit 0, each image's sha256 checked against the request before it was flashed, loader.log's Kernel: … bytes the request's. Each judging: cargo test --test toyos-build -- --metal --metal-readback 649-r6/<boot> mask_windows from the clean worktree at 0aa8d4c88 (649-r6/judge-{A,B,C}.log).

boot kernel booted the kernel's own windows: held line exit the judge's lines git status after
1-head 3125888 bytes cpu1 ns=50000068 0 [windows] held irqs_off_ns=50080844 preempt_off_ns=50080384; herd irqs_off_ns=1980239 preempt_off_ns=1980156, PASS mask_windows empty
2-report-halved 3124904 bytes cpu1 ns=50000026 1 FAIL mask_windows: cpu1 held both windows for 50000026 ns, and its reports before the load's read back irqs_off_ns=25042021 preempt_off_ns=25041779: a window was reported shorter than it was held empty
3-idle-halt-counted 3124664 bytes cpu1 ns=50000030 1 FAIL mask_windows: cpu1 held both windows for 50000030 ns, and its reports before the load's read back irqs_off_ns=50076547 preempt_off_ns=476714793, past the ceiling of 100000060: a window was reported at more than twice what was held empty

The herd's own report, printed and not judged (649-r6/<boot>/windowscase/kernel.log:416-431), against 1,032,292, 2 × 519,358 and 1,175,943 ns at 8b73eba69:

  • 1-head: 1,980,239 ns, the line of cpu7, which spawned the herd. The other seven read 969,922 to 1,044,783.
  • 2-report-halved: all eight CPUs, the holder among them, printed 5,014,552 to 5,075,290 ns at half, 10.03 to 10.15 ms as held, beside tlb: shootdowns=282 wait=131633us max=10038us.
  • 3-idle-halt-counted: 4,725,822 ns, cpu0's line. The other seven read 544,283 to 1,018,355.

Nothing names what any of the three holds, and each is in the issue it answers (Decisions).

The earlier head's boots, and 0aa8d4c88's judge over them, follow. 0aa8d4c88's image differs from 8b73eba69's by the job list (no echo), the hold's loop, and #642 and #647, which the merge of dd8738302 put in it.

Three boots at 8b73eba69: the head and two mutated kernels (comment 5959415453)

Each boot: cargo run --bin toyos-metal -- --image … --readback … --fat32-check, EXIT=0, passed, every job exit 0, each image's sha256 checked against the request before it was flashed.

boot built from kernel booted the kernel's own windows: held line
1-head 8b73eba69 3112640 bytes cpu1 ns=50000029
2-report-halved + mutation a: every reported span halved 3114216 bytes cpu1 ns=50000001
3-idle-halt-counted + mutation b: the idle halt counted as a preemption-off window 3113400 bytes cpu1 ns=50000069

Judged at 0aa8d4c88, from the committed tree, which has no windows.* row: cargo test --test toyos-build -- --metal --metal-readback 649-r5/<boot> mask_windows (readbacks-summary.txt, readback-r5-*.log).

readback exit the judge's lines git status after
1-head 0 [windows] held irqs_off_ns=50075527 preempt_off_ns=50074983; herd irqs_off_ns=1032292 preempt_off_ns=1029656, PASS mask_windows empty
2-report-halved 1 FAIL mask_windows: cpu1 held both windows for 50000001 ns, and its reports before the load's read back irqs_off_ns=25039186 preempt_off_ns=25038975: a window was reported shorter than it was held empty
3-idle-halt-counted 1 FAIL mask_windows: cpu1 held both windows for 50000069 ns, and its reports before the load's read back irqs_off_ns=50076898 preempt_off_ns=486343787, past the ceiling of 100000138: a window was reported at more than twice what was held empty

What the head's boot read, per report (649-r5/1-head/windowscase/kernel.log):

  • The hold. cpu1 held for 50,000,029 ns and its next report read 50,075,527 ns of interrupts off and 50,074,983 of preemption off: the hold and the rest of that SYS_EXIT.
  • The herd's own report: 934,218 to 1,032,292 ns of interrupts off on the eight CPUs.
  • cpu0's 6.5 ms window is in the first report, 6,533,222 ns, and cpu0 reads at most 943,904 after it.
  • The spawning CPU. cpu7, which test-runner spawns from, reads 1,408,843 and 1,499,808 ns in the two reports that span the spawn of a toybox applet.
  • The stop's report, kept in part by the black box, reads cpu7 11,522,834 and cpu4 11,645,411 ns.

The last three are filed (Decisions).

Eight boots at 72f16e39a: step 1 in and reverted (comment 5957994650)

  • Each boot: cargo run --bin toyos-metal -- --image … --readback … --fat32-check, EXIT=0, test_rs_ring_park_herd exit=0.
  • Four images, each booted twice. Base is 72f16e39a with step 1 reverted by the patch above.
  • Judged at that head: EXIT=0 and PASS mask_windows each. That head printed one report a boot, at the herd's exit, so each line spans from its CPU's start_here.

Nanoseconds, the longest line of each boot's one report:

boot arm image irqs_off preempt_off CPUs over 9 ms
1 base 0b9d4e75… 6551791 6551707 0 of 8
2 head 7d313d04… 6540754 6540671 0 of 8
3 base cd44d176… 6549549 6549484 0 of 8
4 head 38d91fb8… 9651846 9643939 8 of 8
5 base 0b9d4e75… 6540326 6540262 0 of 8
6 head 7d313d04… 6589053 6588992 0 of 8
7 base cd44d176… 9260193 9260124 1 of 8
8 head 38d91fb8… 6587342 6587281 0 of 8
  • The x86-64 hooks hold on the machine: eight boots, eight CPUs each, one or two NMIs on cpu0 in each, and no refusal.
  • They say nothing about step 1: the reading is not the herd's, no handler step 1 changed ran (all 64 census lines read userdev=0 sound=0 dmafault=0 hda=0), the herd's walks are masked on both arms (issues/kernel/syscall-preemption-is-incidental.md), and the longest of four samples is a coin toss when one boot in four carries a long event on either arm.
  • 0aa8d4c88's judge refuses all eight: EXIT=1 each, FAIL mask_windows: the load's report is the boot's first: none was taken as the load started (readbacks-summary.txt). The tree was clean after each.

Decisions

  • The load's reading is its own report alone. A report carries what closed since the report before, and the load's exit prints one before the process is torn down. So it spans the teardown of the job before, the runner's spawn of the load, and the load up to its exit.
    • It is refused unless a report before it emptied every CPU's record.
    • The report after the load's exit is not read. At 8b73eba69 it was, and in all three boots it was set by cpu7's spawn of the next job: 1,499,808, 2 × 748,872 and 1,479,558 ns, against 1,032,292, 2 × 519,358 and 1,175,943 in the herd's own.
    • The row's jobs are idle_span, pwd and the herd: three exits, three reports.
  • A window of known length, bounded both ways. windows::hold_once: the boot's first SYS_EXIT on a mask-windows kernel stays in the syscall until cpu::counter says toyos_sched::windows::HELD_NS (50 ms) have passed, and prints windows: held cpuK ns=<span>.
    • The judge requires the holding CPU's reports between the hold and the load's own to carry a window of each kind no shorter than that span and none past metaltimings::ceiling of it, twice the span. The capture carries the span, so the bound needs no record.
    • The span itself must be at least HELD_NS. The loop compares the nanoseconds it prints: at the T14's 413,479 fs a tick, counter_ticks(HELD_NS) is 120,925,125 ticks and converts back to 49,999,999 ns.
    • What it holds reaches a report through record, take and nanos_of_ticks, like every other window.
  • The idle rule has a control on the machine. idle_span is half a second asleep, and its exit is the boot's first. The CPU that holds has just come out of that halt, so a kernel that counts the halt reports about 500 ms on that CPU where the hold is 50.
  • No windows.* row in the machine's record. The judge prints the held pair and the herd's pair and records neither.
    • Twice one boot's longest window is no ceiling for the next: the herd's own report read 1,980,239, 2 × 5,075,290 and 4,725,822 ns in the three boots, and two of the eight earlier reports, which spanned the herd, read 9,260,193 and 9,603,617 to 9,651,846. The reading that enters the tree is the exit's median.
    • boot.windowscase.{complete_ms,panel_max_us,panel_us} are committed, 1152, 3825 and 21453, from 8b73eba69's head boot: every boot of the row measures them, and a judging on a tree without them would leave it dirty.
  • A Ring 0 exception stops the record. Every Ring 0 exception ends the machine, so trap_dispatch's hook stops that CPU's record on a Ring 0 frame, as refuse does, and the fault's report is the one that is read.
  • No actuator and no staged build. Nothing arms the hold and no build code knows of it: it is hold_once and one call in SYS_EXIT. virt_mask_windows boots mask-windows alone, on tests/virtsmpcase with eight CPUs, because tests/virtjobcase runs debug_refused, which needs SYS_DEBUG.
  • build::MASK_WINDOWS_KERNEL is mask-windows, declared once and read by the suite's build list, the QEMU test and the metal arm.
  • The device stubs' leaving hook asks toyos_userbound::Ring, which idt/mod.rs says is the RPL bits' only reader.
  • Issues.
    • The herd's defect (issues/kernel/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md) no longer says its walks ran with interrupts open before Stage 6 step 1: a device's handler posts its watch; irq_ring's UserDev and Audio go #634. It carries six boots' readings at N = 256, 1.03 to 10.15 ms from the herd's own report, and its exit is the window not growing with N: two sizes on one kernel, each the median of at least five boots, the tail beside it.
    • Three defects are filed with their numbers and their readbacks: issues/kernel/cpu0-holds-interrupts-and-preemption-off-for-6-5-ms-on-the-t14.md, issues/kernel/some-t14-boots-carry-an-interrupts-off-window-of-9-ms-or-more.md and issues/kernel/the-cpu-that-spawns-a-toybox-applet-reads-1-4-ms-of-interrupts-and-preemption-off-on-the-t14.md.
      • The second carries 0aa8d4c88's two: its all-CPU reading a second time in fourteen boots, inside the herd's own report with the shootdown line's max=10038us beside it and nmi=1, and cpu7's 11,492,028 ns in 1-head's first report, before the first job's exit.
      • The third carries cpu7's 1,980,239 ns in the herd's report, 468,787 past the 1,511,452 that boot's pwd report reads for an applet's spawn.
    • issues/build/the-control-registers-verdicts-doc-comment-sits-on-the-census-verdict.md, found on the way.

The instrument, as it stands

  • A kernel build, mask-windows, and not the shipping kernel. It reads the counter at every masking transition and every preempt-count crossing of zero. The shipping kernel carries none of it (Gates).
  • The record is toyos_sched::windows::Windows: sans-IO, one per CPU. Each transition takes the caller's counter reading only when it needs one, and answers Unseen for a transition that could not have followed the state it finds. kernel/src/windows.rs is the glue.
  • One clock. Stamps are cpu::counter, converted with clock's one period.
  • The line. Beside each CPU's census line: windows: cpuN irqs_off_ns=… preempt_off_ns=…, the longest window that CPU closed since the report before. Printed at every process exit, at SYS_SHUTDOWN and in the dump.
  • Interrupts-off: fed where IF/DAIF.I changes.
    • Both architectures: IrqGuard, enable_interrupts/disable_interrupts, the idle halt.
    • x86-64: the SYSCALL entry's Rust half, every gate stub, and the exception dispatch.
    • AArch64: the vector dispatch.
    • A return to user mode opens them at scheduler::exit_to_user's end.
  • A switch moves nothing. This build switches inside an IrqGuard, so every saved context holds interrupts masked.
  • Preemption-off: fed at the count's accessors, and at the x86-64 stubs that raise it inline. Two rules: a scheduler pass ends the window it ran in and starts the next, and the idle halt inside a pass is no window.
  • Every hook checks the state it finds. A transition no hook saw panics at the next hook on that CPU.
  • NMIs are not windows. Nothing masks them, and they run no hook.
  • A CPU is tracked from enter_idle_loop.
  • The load: test_rs_ring_park_herd. 256 threads park in inbox_submit on one ring; the main thread completes one OP_NOP into it 64 times.
  • Harness. TEST_SUITE_KERNEL_BUILDS gains mask-windows, the third kernel a suite run builds. roster.rs holds 1024 entries and refuses a machine with more.

Tests, by tier

  • Host.
    • windows::tests (8) hold the record's semantics: a state machine over a counter.
    • mask_windows_verdict holds both judges to captures: the three-report shape read to its numbers, and what a judge refuses.
  • Metal: mask_windows, x86-64, the shipping kernel plus mask-windows. Every hook's check runs on the machine and a refused transition panics the boot. The judge requires each census line's windows line, every CPU reporting, a held window read back between its span and twice it, and a report before the load's own.
  • QEMU: virt_mask_windows, AArch64, tests/virtsmpcase on eight CPUs.
    • Why QEMU: AArch64 has no metal, and no host runs EL1 code. The vector dispatch's hooks, the DAIF primitives, the WFI halt and the switch are architecture code only a booted AArch64 machine executes, and what refuses a missed hook there is a kernel panic.
    • Its verdict reads no duration: each census line has its CPU's windows line, and each of eight CPUs closed a window of each kind. It does not judge the hold.
  • No x86-64 guest test. The metal row holds the x86-64 hooks.

The checks

Negative controls. Reverting the whole change leaves no mask-windows to build. Each claim has a mutation: a checked patch (git apply --check EXIT=0), applied, built, run by the command named, reversed (git apply -R EXIT=0), with git status --porcelain empty after each batch (mut-*-summary.txt). Patches, runner and probes are in comment 5960408434.

The x86-64 ones have no registered guest test, so they ran in a scratch probe: x86_windows_probe boots the T14 row's kernel and job list on eight CPUs under TCG and reads it with the row's own judge. Unmutated it is EXIT=0. "The record" is a mask-windows: cpuN … panic.

claim mutation where exit red by
the SYSCALL entry opens the window its irqs_masked() deleted (m01) probe 1 the record: "found interrupts masked with no window open"
a guard's drop closes it the drop's hook conditioned on IF set (m02) probe 1 the record: "masked interrupts with a window open"
a return to user mode closes it exit_to_user's hook deleted (m03) probe 1 the same
virt_mask_windows 1 the same
a device interrupt's Ring 0 return closes it the stub's leaving call removed (m04) probe 1 the record: "left a preempt count of zero with a window open"
a switch moves no IF Hw::switch's guard deleted (m05) probe 1 the record: "found interrupts masked with no window open"
the count's raise opens the preempt window preempt_count_up's hook deleted (m06) probe 1 the record: "found the preempt count raised with no window open"
AArch64's SVC entry opens the window EL0_SYNC's irqs_masked() deleted (m11) virt_mask_windows 1 the record, a panic at windows.rs's refuse
a Ring 0 fault reports itself the Ring 0 stop deleted (f01) fault probe 1 the record, in place of the fault: "masked interrupts with a window open"
a report states the span it measured both fields halved (a) probe 1 the judge: read back 26817116 and 26812613 against 50000981 held
the T14, 649-r5/2-report-halved 1 the judge: 25039186 and 25038975 against 50000001 held
the idle halt is no window, in the kernel idle_wait's two hooks deleted (b) probe 1 the judge: read back 52898549 and 505789979 against 50000985 held, past 100001970
the T14, 649-r5/3-idle-halt-counted 1 the judge: 50076898 and 486343787 against 50000069 held, past 100000138

The fault probe, x86_windows_fault_probe, takes a Ring 0 read of 0x10 inside a syscall: unmutated EXIT=0, with the fault's FAULT rip=… on the console.

Host mutations, each EXIT=101 against an unmutated EXIT=0:

claim mutation command a test it reds
a pass ends the window and starts the next scheduled does nothing (h02) cargo test -p toyos-sched --lib windows:: a_pass_ends_the_preemption_off_window_and_starts_the_next
the idle halt is no window, in the record halting/woken do nothing (h03) the same the_idle_halt_inside_a_pass_is_no_window
only a crossing of zero opens one raised acts on any count above zero (h04) the same only_a_crossing_of_zero_opens_or_closes_a_preemption_off_window
the longest is kept, not the last record stores every span (h05) the same an_interrupts_off_window_is_mask_to_unmask_and_the_longest_is_taken_once
an unmask with no window is refused unmasking accepts it (h06) the same every_transition_nothing_reported_before_is_named
the load's reading is its own report read over every report (j01) cargo test --test toyos-checks -- mask_windows_verdict mask_windows_verdict
a load's report needs one before it that refusal removed (j02) the same the same
both kinds are read back one kind at its span is enough (j04) the same the same
the hold is read back before the load the load's report may read it back (j05) the same the same
a read-back is bounded above the ceiling removed (j06) the same the same
both kinds are bounded above one kind under the ceiling is enough (j07) the same the same

Two of those, on the machine's readbacks:

  • j06 on 649-r5/3-idle-halt-counted: EXIT=0, PASS mask_windows, against EXIT=1 unmutated. The ceiling is all that reds mutation b on a tree with no windows.* row.
  • j01 on 649-r5/1-head: EXIT=0, the herd's pair printed as 50075527 and 50074983. Nothing on the machine reds it, since the pair is printed and not judged: its red is the host's.

Independent oracle: real hardware.

  • A maskable interrupt is delivered only with IF set (SDM Vol. 3A §6.8.1). Every gate stub holds the record to that: one that finds a window open is refused. On the T14 real interrupt sources decide when they land: fourteen boots, eight at 72f16e39a, three at 8b73eba69 and three at 0aa8d4c88, two of each three on the mutated kernels, and no refusal.
  • A window of known length, read back on the machine: 50,080,844 and 50,080,384 ns against 50,000,068 held.

Gates

At b6635359a: 649-named/logs/gates-b6635359a-summary.txt, the tree clean before and after.

  • cargo run -- --ci host: EXIT=0, "Host: 66 step(s), all green".
  • cargo test --test toyos-build, the whole guest suite: EXIT=0, "26 passed, 26 total", virt_mask_windows and virt_smp among them, on three kernel builds: "", boot-actuators,test-actuators, mask-windows.
  • cargo run -- --build-only and cargo run -- --arch aarch64 --build-only: EXIT=0 each. Both shipping kernels and both images carry 0 lines matching windows: cpu, 0 matching windows: held, 0 matching mask-windows and 1 matching irq: cpu (grep -c -a).
  • cargo test --test toyos-build -- --list: EXIT=0, mask_windows and virt_mask_windows once each. -- --metal --list mask_windows: EXIT=0, windowscase: 3 job(s).
  • The x86-64 probe under TCG on eight CPUs, at 0aa8d4c88: EXIT=0, twice. One run read Measured { held: (53454662, 53446655), load: (144179372, 101869396) }, the other, with the host's liveness ceilings at 6.35x width, held: (70015957, 69963910), load: (102793430, 92852492).

The images the T14 booted

Three images, each staged at 0aa8d4c88 by cargo test --test toyos-build -- --metal --metal-readback <dir> mask_windows, EXIT=2 ("staged"), the mutations under their patch, applied and reversed around the staging with the tree clean after (649-r6/logs/stage-summary.txt).

boot built from image sha256 kernel in it
1-head 0aa8d4c88 915b0224…eac8 3125888 bytes
2-report-halved 0aa8d4c88 + a-report-halved.patch 57b66b9f…24c3 3124904 bytes
3-idle-halt-counted 0aa8d4c88 + b-idle-halt-counted.patch 8ddca1d3…d9ec 3124664 bytes
  • Each image carries its own kernel. Each staged kernel is found whole in its image, the three differ, and the loader's Kernel: … bytes line says which one booted. The head's kernel, rebuilt from the clean tree after the three stagings, has the same sha256 (ac40f379…28a2).
  • b6635359a stages the kernel 1-head booted. The same staging at this head with no patch, once before the gates and once after: EXIT=2 each, the tree clean after each (649-named/logs/stage-summary.txt, stage2-summary.txt).
    • The staged kernel is 3125888 bytes, sha256 ac40f3799698ff900dcb4bc3d48daa928787e005bde9aee7d34e4e2a70b028a2 both times, cmp against 649-r6/1-head/kernel.staged EXIT=0, and found whole in each image.
    • Each image's parameter line carries root=a2fea9fc0d3982b8d4f4d1a4dbde3ca8, the digest of ROOT's files that 1-head's loader.log printed at its boot, and the staged system-05ff11cc4420f178.toml is 1-head's (diff EXIT=0).
    • An image's own sha256 is no comparator: this head's two stagings are c08168b2…4f02 and f6e68dd2…34ea, 412 differing bytes in 15 sectors (cmp -l, cmp-head-head-2-partitions.txt): 208 in the GPT and its backup, 71 in the EFI System partition, 38 in the slot table, 95 in slot A outside the kernel, and none in the kernel or in ROOT.

Measured

  • kernel/src lines (git show of every .rs, wc -l): 63,062 at stage 6's start (53b1ef7d3), 63,312 at step 1 (815e0899f), 59,225 on main (dd8738302), 59,571 here: 346 more than main, the instrument's.
  • git diff --shortstat origin/main...HEAD: 43 files, +1500 −30.
    • Production: kernel/ +355 −3, toyos-sched +349 of which 109 are the record's tests, src/ +10 −4.
    • Tests: tests/ +498 −6. Issues: +288 −17.

Unsure

  • A window reported up to twice too long passes. The bound is metaltimings::ceiling, the one rule the metal suite has. On the T14 the hold read back 0.16% over its span, so a tighter bound has room, and none is derived here.
  • The load's own report spans the runner's spawn of it. The spawning CPU's line is the report's longest in 1-head, 1,980,239 ns against 1,044,783 at most elsewhere, and past the 1,511,452 that boot's applet spawn reads. It is filed, not removed, and nothing names what it holds beyond the spawn.
  • The loader's bytes are not compared between the image 1-head booted and this head's. That image is gone, and the EFI System partition differs by 71 bytes between two stagings of one tree. What stands for it is the kernel's hash, ROOT's digest, the job list, and a diff from 0aa8d4c88 that touches no source of an image.
  • The hold rests on its site. hold_once masks nothing itself: SYS_EXIT's entry left interrupts masked and the count raised. What says so is the T14's judge.
  • Nothing judges the hold on AArch64. A QEMU verdict reads no duration, so the hold runs there and nothing reads it back.
  • The Ring 0 stop and the x86-64 hooks' controls ran in scratch probes. Nothing in the tree runs them again. On x86-64 a Ring 0 fault is a boot that panics, which the metal loop cannot judge (issues/build/the-metal-loop-cannot-judge-a-boot-that-panics.md).
  • The Ring 0 stop is shown for a fault taken with interrupts masked. One taken with them open is reached by no stager.
  • Every mask-windows boot holds one CPU for 50 ms. A CPU waiting on the holder, in a shootdown say, would read up to that in the same report. The judge keeps it out of the load's reading by order, not out of the first two reports.
  • TCG's readings move with the host. The hold read back at 53 and at 70 ms in two probe runs and the load at 144 and 103 ms. A host loaded further could put the probe's hold past its 100 ms ceiling. It is a preview and no verdict.
  • The issue files cite readbacks by 649-r5/<boot>/… and 649-r6/<boot>/…, scratch directories. The comments they name carry the judged lines and not the logs.
  • The stubs' own edges are outside the windows: the pushes and prologue before an entry's Rust half, and the pops and iretq/sysretq after the leaving hook.
  • This build switches with interrupts masked where the shipping kernel may not, which its windows then include.

🤖 Generated with Claude Code

https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm

Japabu and others added 7 commits September 30, 2026 23:42
…, per CPU, beside the census

`mask-windows` is a kernel build that reads the counter at every masking
transition and every preempt-count crossing of zero, and prints, beside
each CPU's IRQ census line, the longest interrupts-off and preemption-off
window that CPU closed since the report before
(`windows: cpuN irqs_off_ns=… preempt_off_ns=…`).

Fed by each architecture's masking primitives and entries: `IrqGuard`,
`enable_interrupts`/`disable_interrupts` and the halt on both; on x86-64
the syscall entry, every gate stub (device, TLB, timer in both rings,
spurious, unclaimed) and the exception dispatch; on AArch64 the vector
dispatch. A return to user mode opens interrupts at `exit_to_user`'s end,
which every such return runs last. The preempt count's accessors feed its
crossings on both arches, and the x86-64 stubs that raise it inline feed
theirs from their Rust halves. A context switch runs inside an `IrqGuard`
in this build, so no saved context carries interrupts open through
`context_switch`'s flags restore, the one transition no hook can follow.

A scheduler pass ends a preemption-off window and starts the next, and
the idle halt inside a pass is no window: without both the number is the
time since the CPU last returned to user mode, idle time included.

Every hook checks the state it finds: a transition no hook saw panics at
the next hook on that CPU, and every entry is held to what the hardware
says it interrupted (a maskable interrupt only with interrupts open; an
exception's frame carries the flag).

The load `issues/kernel/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md`
names is `test_rs_ring_park_herd`: 256 threads parked in `submit` on one
ring, and the main thread completing into it 64 times, each once the
roster shows all 256 parked again.

`mask_windows` (x86-64, four CPUs) and `virt_mask_windows` (AArch64)
boot `boot-actuators,mask-windows,test-actuators` with `windows-staged`,
which spins 1 ms inside the first process exit, where both windows are
open; a later report of that CPU must carry both at least that long. The
`mask_windows` metal row boots the shipping kernel plus `mask-windows`
on the T14 and records the boot's longest windows and the herd's.

The harness lets a named build that carries the actuators arm one.
roster.rs holds 1024 entries and refuses a machine with more, since the
herd's 257 threads would not fit its 256.

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

Positive control first, then seven refusals: no staged span, a staged
span no later report carries, one carried only by a report before it, a
census without its windows, a CPU that closed no window, one CPU of two,
and a field missing. With the staged check reduced to "some later report
exists" it reds: "a staged span no later report carries was accepted".

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

A 1 ms span is inside what a TCG guest's own windows can reach — the
herd's completion alone walks 256 waiters — so with the spin removed a
later report could still carry it and the negative control would stay
green. 250 ms stands above that and far inside `time::DEAF_CPU`'s 5 s.

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

`toyos_sched::windows::Windows` is one CPU's record: every transition
takes the caller's counter reading (read only when the transition needs
it) and answers `Unseen` for one that could not have followed the state
it finds. The kernel's `windows.rs` is the glue: the per-CPU statics, the
hooks each architecture calls, the report and `windows-staged`.

What no guest can red now reds on the host: a pass ends the
preemption-off window and starts the next, the idle halt inside a pass is
no window, only a crossing of zero opens or closes one (and a raise past
one reads no clock), the longest since the last take is what a take
answers, and each transition nothing reported before is named.

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

At 66530cf both guest arms of the windows' instrument died before a
kernel was built. `mask_windows` and `virt_mask_windows` boot
`MASK_WINDOWS_KERNEL` (`boot-actuators,mask-windows,test-actuators`)
armed with `windows-staged`. `kernel_of` had been widened to let a
named build that carries `boot-actuators` arm an actuator.
`build_test_parts` still required the exact `TEST_KERNEL` for any
parameter outside the kernel's own and the valued ones, so it
panicked: "a boot asking for ["windows-staged"] must boot the test
kernel, not ["boot-actuators", "mask-windows", "test-actuators"]".
Every mutation arm then failed on that same refusal, so none of them
measured anything.

The two sites now ask one predicate, `build::carries_the_actuators`:
the build carries every feature of `TEST_KERNEL`, alone or under an
instrument. `kernel_of`'s `boot-actuators` check becomes the same
predicate, so it can no longer disagree with the image build.
`build_test_parts`' refusal moves into `refuse_an_unarmable_boot` so
a host test can ask it directly. The test is
`a_named_build_over_the_test_kernel_arms_an_actuator`: the windows'
kernel and the test kernel arm `windows-staged`, and the scheduler's
check kernel is refused.

Negative control: restoring the exact-equality rule at the refusal
builds, and the test is red with the guest's own message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…eport says each closed once

At 772bac7, `virt_mask_windows` was red twice. In both runs the report after
the stage carried a preemption-off window longer than the 250 ms spin
(271362000 ns and 292094000 ns), and an interrupts-off window shorter than it
(65031000 ns and 153183000 ns).

The two clocks did not disagree. The spin and the record both read
`cpu::counter` (RDTSC on x86-64, CNTVCT_EL0 after an ISB on AArch64) and
convert with the one period in `clock`. The cause was where the spin ran.
In `virtjobcase` the first process to exit is `preempt`, whose counting
thread never enters the kernel. Its main thread's `SYS_EXIT` is not the last
out. The killed sibling tears the process down at its exit boundary, and
`leave_user_if_due` opens interrupts across that teardown. So the spin ran
with preemption off and interrupts open, and the record said exactly that.

A QEMU verdict asserts order, completion, content and counts, never how
long something took. The guest verdict now reads no duration:

- `toyos_sched::windows::Windows::mark` marks the window of each kind open
  now. A missing open window is refused as `Unseen`. Each mark rides to its
  window's close, and `take_marked` answers each marked close once.
- `windows-staged` marks at the first `SYS_EXIT`, whose entry masked
  interrupts and raised the preempt count, and it spins no longer. It asserts
  both from the CPU first, so a stage moved to where a window is shut is
  refused as that and not as a missed hook. It prints
  `windows: staged cpuK open`. A later report of cpuK prints
  `windows: staged cpuK closed irqs_off=… preempt_off=…` for the marked
  windows that closed since the report before.
- `common::irqcensus::windows` holds a stage to this: after the stage, its
  CPU's reports say each marked window closed exactly once, and no close
  precedes its stage. The census pairing and the every-CPU-closed-both
  checks stay.

Durations are judged on the T14 alone, which arms no actuator. The record's
new mark word costs every close one relaxed load, in the metal build too.

The verdicts move above the control-registers doc block they had been
inserted into. That block's own misplacement onto `irq_census` is main's,
filed as
issues/build/the-control-registers-verdicts-doc-comment-sits-on-the-census-verdict.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Japabu and others added 2 commits October 2, 2026 18:42
Main is 469 commits past the last merge (e754dc8, #588). Six files
conflicted, each resolved by accounting for both sides:

- kernel/src/actuator.rs: main (#639, #660) deleted every actuator in the
  region this branch added `windows-staged` to, `handler-post` among them.
  Main's deletions stand, and `windows-staged` sits after
  `usb-reset-under-load`. Its doc names `virt_mask_windows`, its one judge
  now.
- kernel/src/scheduler.rs: main deleted `dump-in-blocking-pass`'s lines in
  `exit_to_user`; this branch's `irqs_unmasking` hook followed them. The
  hook stays, the actuator's lines go.
- src/clippy.rs: main dropped `fpu-save-nothing` and `user-writable-gsbase`
  from both instrument lists; `mask-windows` is appended to what is left,
  and the two `MASK_WINDOWS_KERNEL` shapes are this branch's.
- src/build.rs: main cut `TEST_SUITE_KERNEL_BUILDS` to the shipping and the
  test kernel; `MASK_WINDOWS_KERNEL` is the third. `carries_the_actuators`
  and `refuse_an_unarmable_boot` merged without conflict.
- tests/common/qemu.rs: main shortened `DECLARED_KERNEL_BUILDS`' doc and
  deleted `kernel_of`'s paragraph on `boot_image`; this branch's sentence
  for the windows' build and `kernel_of`'s rule are kept over main's text.
- tests/toyos.rs: main's file (#660: 21 declared guest tests, `Sched` and
  the shared guest boot gone, `metal::Metal` a struct) taken whole, and
  this branch's additions put back onto it: `ring_park_herd` on
  `RUST_SKIP`, `virt_mask_windows` and its judge `mask_windows`, the
  `mask_windows` metal row with `WINDOWSCASE`, and `windows_on_metal`.

**The x86-64 guest test `mask_windows` does not come back.** Main refuses
one name in two registries (`registered`), and `mask_windows` is the T14's
row; `irq_census_conservation`, the census these lines sit beside, was cut
from the guest suite for its row the same way. Its `MACHINE_TESTS` entry,
its `CARRIES` row and its `run_machine_test` arm, which booted the shared
guest image main deleted, are not carried. What it held is held by:
the x86-64 hooks' checks by the metal row, which boots them under the
herd; the stage by `virt_mask_windows`; the verdict by
`mask_windows_verdict` on the host.

The census of raw masking instructions is unchanged by the merge: every
word of `cli`, `sti`, `popfq`, `iretq`, `sysretq`, `eret`, `daifset`,
`daifclr`, `daif`, `wfi` and `hlt` under `kernel/src`, comments apart, is
the same list at 737803d and here but for main's own two moves: the NMI
gate's `iretq` from `nmi_gate.rs` to `idt/nmi.rs`, and `IrqGuard::unclosed`,
a read of the flags, deleted on both architectures. The clause of the
guard's drop hook that named it goes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Stage 6 step 2's exit compared the two windows at stage 6's first commit
(53b1ef7, the instrument applied as a patch) with those at step 1's head.
Both arms are behind main by the whole guest-suite cut and its
neighbours: their toolchain is in no store, a build at either starts with
an LLVM and compiler rebuild, and their `keystore::sweep` panics on
today's stores and could sweep stores other worktrees use.

The comparison is taken instead as root CLAUDE.md gives a negative
control: on one tree, today's with the instrument, with step 1 (#634) as
it stands and with it reverted. That reads what the exit is for, whether
step 1 lengthened either window, with today's toolchain.

The track's step 2 and the stage's rule for steps 2 to 5 say so, and the
exit of the issue step 2 holds moves with them. The kernel's lines are
still counted against stage 6's first commit.

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

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

The base arm: step 1 (#634, 815e0899f) reverted in what the image is built from, as a patch over this branch's head 72f16e39a. sha256 110d0aebc426b026784a21083c25f8dfb85d0a45a6ce02744fb8e23d2dc87628, 63,825 bytes; git apply --check EXIT=0 at 72f16e39a. It touches 23 files under kernel/ and toyos-sched/src/ and nothing else. How it was made and what checks it: the pull request body, "The base arm".

diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml
index c4b2f45b0..62b59ce64 100644
--- a/kernel/Cargo.toml
+++ b/kernel/Cargo.toml
@@ -53,10 +53,10 @@ log-commit-release-off = []
 # `src/log/registry.rs`'s pointer store and load go `Relaxed`, and
 # `log_publish` reds.
 shard-publish-relaxed = []
-# `src/pcidev/record.rs`'s read-modify-writes become a load and a store, so a
-# message the ISR records between a reader's two halves is lost, and
-# `device_irq` reds. They *are* the record's design, so turning them off reverts
-# the whole of what the model claims.
+# `src/pcidev/record.rs`'s two read-modify-writes become a load and a store, so
+# a message the ISR records between a reader's two halves is lost and a wake can
+# be owed twice, and `device_irq` reds. That pair *is* the record's design, so
+# turning it off reverts the whole of what the model claims.
 device-irq-lossy = []
 # `src/sched/dump_request.rs`'s `take` and `end_report` go `Relaxed`, so the next report
 # is unordered against the last one's writes, and `dump_request` reds.
diff --git a/kernel/src/arch/x86_64/idt/hda.rs b/kernel/src/arch/x86_64/idt/hda.rs
index d75e99ef7..579bebedf 100644
--- a/kernel/src/arch/x86_64/idt/hda.rs
+++ b/kernel/src/arch/x86_64/idt/hda.rs
@@ -1,6 +1,6 @@
 use super::device_irq::device_irq_entry;
 
-// Heap-free, and takes only interrupts-off locks: may interrupt a CPU holding the controller lock (preemption disabled, not interrupts).
+// Lock-free, heap-free: may interrupt a CPU holding the controller lock (preemption disabled, not interrupts).
 extern "sysv64" fn hda_handler() {
     crate::arch::percpu::irq_took!(Hda);
     crate::drivers::hda::isr_complete();
diff --git a/kernel/src/arch/x86_64/idt/user_dev.rs b/kernel/src/arch/x86_64/idt/user_dev.rs
index 9ad2dccbc..18142f94d 100644
--- a/kernel/src/arch/x86_64/idt/user_dev.rs
+++ b/kernel/src/arch/x86_64/idt/user_dev.rs
@@ -6,14 +6,18 @@
 //! wake every user driver in the machine on any of their interrupts, which is
 //! one process learning when another's device is busy.
 //!
-//! Heap-free like every other device entry here: the record is atomics, and
-//! the claim's watch is posted from the handler.
+//! Lock-free and heap-free like every other device entry here: the record is
+//! atomics and the wake happens on the next scheduler pass.
 
 use super::device_irq::device_irq_entry;
+use crate::irq_ring::IrqSource;
 
 fn took(slot: usize) {
     crate::arch::percpu::irq_took!(UserDev);
     crate::pcidev::isr(slot);
+    crate::irq_ring::isr_publish(IrqSource::UserDev, crate::clock::nanos_since_boot());
+    // Force resched now, so `drain_irqs` turns the record into a wake before
+    // the next quantum tick rather than after it.
     crate::preempt::set_need_resched();
     crate::arch::apic::eoi();
 }
diff --git a/kernel/src/arch/x86_64/idt/virtio_sound.rs b/kernel/src/arch/x86_64/idt/virtio_sound.rs
index 6e3744d62..98060824d 100644
--- a/kernel/src/arch/x86_64/idt/virtio_sound.rs
+++ b/kernel/src/arch/x86_64/idt/virtio_sound.rs
@@ -1,6 +1,6 @@
 use super::device_irq::device_irq_entry;
 
-// Heap-free, and takes only interrupts-off locks: may interrupt a CPU holding the controller lock, which disables preemption but not interrupts.
+// Lock-free and heap-free: may interrupt a CPU holding the controller lock, which disables preemption but not interrupts.
 extern "sysv64" fn virtio_sound_handler() {
     crate::arch::percpu::irq_took!(Sound);
     crate::drivers::virtio_sound::isr_complete();
diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs
index d7e401a25..b59cc8303 100644
--- a/kernel/src/arch/x86_64/mod.rs
+++ b/kernel/src/arch/x86_64/mod.rs
@@ -50,8 +50,10 @@ pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::X86_64;
 
 /// Interrupts masked on this CPU for as long as the guard lives, and then put
 /// back as they were — restored, not enabled — so a guard nests inside a region
-/// that is already masked. The one way this kernel masks and restores. `TF` is
-/// always clear in Ring 0, so the guard leaves it alone.
+/// that is already masked. The one way this kernel masks and restores: the
+/// scheduler's pass, a log record's reservation and publication, and the
+/// console backend each hold one. `TF` is always clear in Ring 0, so the guard
+/// leaves it alone.
 ///
 /// Both edges are compiler barriers (no `nomem`): a memory access written
 /// inside the region is emitted inside it.
diff --git a/kernel/src/arch/x86_64/vtd/fault.rs b/kernel/src/arch/x86_64/vtd/fault.rs
index 3de98c0f3..c0188b87c 100644
--- a/kernel/src/arch/x86_64/vtd/fault.rs
+++ b/kernel/src/arch/x86_64/vtd/fault.rs
@@ -1,6 +1,6 @@
 //! Vt-d fault interrupt handling: MSI-delivered, never polled.
 //!
-//! The handler is bounded, allocates nothing; unit and
+//! The handler is bounded, allocates nothing and takes no lock; unit and
 //! function state lives in fixed arrays of atomics, written once before the
 //! mask comes off. Whatever the stream, the same things happen first: Bus
 //! Master Enable cleared on the function that faulted, the first record latched
@@ -36,7 +36,8 @@ const FSTS_OVERFLOW: u32 = 1 << 0;
 // One fault recording register's F bit, in the 32-bit word that carries it.
 const RECORD_FAULT: u32 = 1 << 31;
 
-// Atomics only: each field is written once before the unit's mask comes off.
+// Atomics only: the handler takes no lock; each field is written once before
+// the unit's mask comes off.
 struct FaultUnit {
     // Physical base of the register window; 0 means this slot is unused.
     // vtd::window refuses a base of 0, so no armed unit can collide with the sentinel.
diff --git a/kernel/src/device.rs b/kernel/src/device.rs
index 66935836c..17c7f60d5 100644
--- a/kernel/src/device.rs
+++ b/kernel/src/device.rs
@@ -79,20 +79,7 @@ impl Claim {
 impl Drop for Claim {
     fn drop(&mut self) {
         match self.what {
-            Claimed::Class(class) => {
-                // Before the flag goes: a poll the next holder registers is not this claim's to answer.
-                match class {
-                    DeviceType::HdaAudio | DeviceType::VirtioSound => {
-                        crate::drivers::AUDIO_WATCH.cancel_polls()
-                    }
-                    DeviceType::Keyboard
-                    | DeviceType::Mouse
-                    | DeviceType::Framebuffer
-                    | DeviceType::PciFunction
-                    | DeviceType::Partition => {}
-                }
-                *taken(class).lock() = false;
-            }
+            Claimed::Class(class) => *taken(class).lock() = false,
             // Bus mastering off, then the domain, then the pages: `release`
             // owns that order, and this is where a dying process reaches it.
             Claimed::PciFunction(slot) => crate::pcidev::release(slot),
diff --git a/kernel/src/drivers/hda.rs b/kernel/src/drivers/hda.rs
index d86471b81..8e87fcc85 100644
--- a/kernel/src/drivers/hda.rs
+++ b/kernel/src/drivers/hda.rs
@@ -166,7 +166,7 @@ pub fn isr_complete() {
     }
     ISR.timestamp.store(timestamp, Ordering::Relaxed);
     ISR.mask.fetch_or(mask, Ordering::Release);
-    super::AUDIO_WATCH.post_in_place();
+    crate::irq_ring::isr_publish(crate::irq_ring::IrqSource::Audio, timestamp);
     crate::preempt::set_need_resched();
 }
 
diff --git a/kernel/src/drivers/mod.rs b/kernel/src/drivers/mod.rs
index a4f8cf9f8..d71937ef0 100644
--- a/kernel/src/drivers/mod.rs
+++ b/kernel/src/drivers/mod.rs
@@ -23,4 +23,4 @@ pub use crate::mm::DmaPool;
 
 /// What an audio read and an audio poll wait on: one for both backends, since
 /// at most one binds and the claim's reader cannot know which.
-pub static AUDIO_WATCH: crate::watch::IrqWatch = crate::watch::IrqWatch::new();
+pub static AUDIO_WATCH: crate::watch::Watch = crate::watch::Watch::new();
diff --git a/kernel/src/drivers/virtio_sound.rs b/kernel/src/drivers/virtio_sound.rs
index c5cf09e9d..6ad0df502 100644
--- a/kernel/src/drivers/virtio_sound.rs
+++ b/kernel/src/drivers/virtio_sound.rs
@@ -108,7 +108,8 @@ pub fn isr_complete() {
         return;
     }
     isr_push_completion(mask, timestamp);
-    super::AUDIO_WATCH.post_in_place();
+    crate::irq_ring::isr_publish(crate::irq_ring::IrqSource::Audio, timestamp);
+    // Force a scheduler entry on IRQ return so the record becomes wakes now, not at next tick.
     crate::preempt::set_need_resched();
 }
 
diff --git a/kernel/src/inbox/mod.rs b/kernel/src/inbox/mod.rs
index d2453243f..d7c69cc79 100644
--- a/kernel/src/inbox/mod.rs
+++ b/kernel/src/inbox/mod.rs
@@ -19,18 +19,14 @@
 //! process's ring or say something no object said. A post writes at most one
 //! entry, and a ring holds at most [`MAX_PENDING_WATCHES`] polls.
 //!
-//! **Locks.** What a completion writes, and the page it is written into, sit
-//! behind an [`IrqLock`] of their own, because a device's interrupt handler
-//! posts in place, firing its polls under its list lock; nothing is taken
-//! under it. The rest of a ring, its submissions and its polls, is its
-//! `Lock`'s, which no post reaches. A ring's own watch is an [`IrqWatch`] and
+//! **Locks.** A ring's own lock takes nothing under it and is never taken under
+//! a watch's: a post fires its polls with its list let go. A ring's own watch
 //! holds only threads, because no handle names a ring as a thing to watch.
 
 use alloc::sync::Arc;
 use alloc::vec::Vec;
 use core::sync::atomic::Ordering;
 
-use toyos_sched::sync::CellLock;
 use toyos_sched::task::WaitClass;
 use toyos_sched::watch::{Fire, Ring};
 
@@ -40,7 +36,7 @@ use crate::process::{self, Pid};
 use crate::scheduler;
 use crate::sync::Lock;
 use crate::time::{Deadline, Duration};
-use crate::watch::{IrqLock, IrqWatch};
+use crate::watch::Watch;
 use crate::DirectMap;
 
 use toyos_abi::inbox::{
@@ -66,12 +62,7 @@ impl InboxRef {
 
 impl Drop for InboxRef {
     fn drop(&mut self) {
-        // The page is the completions', so it goes only once no post can reach
-        // them. Both halves are taken out under their locks and let go of
-        // outside them: the unmap flushes.
-        let Some(completions) = self.0.completions.with(Option::take) else {
-            unreachable!("an inbox is torn down by its one reference, once");
-        };
+        // Taken out under the lock and let go of outside it: the unmap flushes.
         let Some(mut state) = self.0.state.lock().take() else {
             unreachable!("an inbox is torn down by its one reference, once");
         };
@@ -79,7 +70,7 @@ impl Drop for InboxRef {
             poll.withdraw();
         }
         // `Unmapped`'s drop flushes; the `Arc` drop after it frees the pages.
-        drop(completions.shm.unmap_from(state.owner_pid));
+        drop(state.shm.unmap_from(state.owner_pid));
     }
 }
 
@@ -202,80 +193,68 @@ const MAX_PENDING_WATCHES: usize = 1024;
 pub struct Inbox {
     /// `None` once the ring's one reference let go of it.
     state: Lock<Option<RingState>>,
-    /// `None` from the moment that reference starts letting go of it.
-    completions: IrqLock<Option<Completions>>,
     /// Threads parked in `submit`; never a poll — see the module header.
-    watch: IrqWatch,
+    watch: Watch,
 }
 
 struct RingState {
-    /// The page's address. The page is [`Completions`]'s, which the teardown
-    /// lets go of only after it has taken this.
     shm_phys: DirectMap,
+    /// A ring's page has no lifetime of its own; it goes with the last handle to the ring.
+    shm: Arc<SharedMemObject>,
     submission_size: u32,
+    completion_size: u32,
+    /// The kernel's own copy of the completion tail, the only one it reads.
+    completion_tail: u32,
     /// Polls still armed as of the last registration, which sweeps the rest.
     pending: Vec<Arc<Poll>>,
     owner_pid: Pid,
 }
 
-// No accessor below returns a Rust reference into a ring's page — the process
-// maps it writable, so only atomics or `read_volatile` are sound here.
+impl RingState {
+    // No accessor below returns a Rust reference into this page — the process
+    // maps it writable, so only atomics or `read_volatile` are sound here.
 
-/// One atomic word of one ring header; never `&RingHeader` — see the block above.
-fn ring_word(page: &DirectMap, ring_off: u64, field_off: usize) -> &core::sync::atomic::AtomicU32 {
-    let ptr = page.as_mut_ptr::<u8>();
-    // SAFETY: offset is in-bounds and 4-aligned within the 2 MiB page, which outlives both of the ring's halves that name it; `AtomicU32` is sound over memory the process also writes.
-    unsafe {
-        core::sync::atomic::AtomicU32::from_ptr(
-            ptr.add(ring_off as usize + field_off) as *mut u32,
-        )
+    /// One atomic word of one ring header; never `&RingHeader` — see the block above.
+    fn ring_word(&self, ring_off: u64, field_off: usize) -> &core::sync::atomic::AtomicU32 {
+        let ptr = self.shm_phys.as_mut_ptr::<u8>();
+        // SAFETY: offset is in-bounds and 4-aligned within the 2 MiB page; `AtomicU32` is sound over memory the process also writes.
+        unsafe {
+            core::sync::atomic::AtomicU32::from_ptr(
+                ptr.add(ring_off as usize + field_off) as *mut u32,
+            )
+        }
     }
-}
 
-impl RingState {
     fn submission_head(&self) -> &core::sync::atomic::AtomicU32 {
-        ring_word(&self.shm_phys, SUBMISSION_RING_OFF, core::mem::offset_of!(RingHeader, head))
+        self.ring_word(SUBMISSION_RING_OFF, core::mem::offset_of!(RingHeader, head))
     }
 
     fn submission_tail(&self) -> &core::sync::atomic::AtomicU32 {
-        ring_word(&self.shm_phys, SUBMISSION_RING_OFF, core::mem::offset_of!(RingHeader, tail))
-    }
-
-    /// One submission entry, copied out by value via `read_volatile` — never a `&Submission`.
-    fn submission_at(&self, index: u32) -> Submission {
-        let ptr = self.shm_phys.as_mut_ptr::<u8>();
-        // SAFETY: `index` is masked by `submission_size` (≤256), keeping the read in-bounds and aligned within the page.
-        unsafe { (ptr.add(SUBMISSIONS_OFF as usize + index as usize * core::mem::size_of::<Submission>()) as *const Submission).read_volatile() }
+        self.ring_word(SUBMISSION_RING_OFF, core::mem::offset_of!(RingHeader, tail))
     }
-}
 
-/// What a poll's completion writes, which a post from an interrupt handler
-/// reaches, and the ring's page, which goes only with these.
-struct Completions {
-    /// A ring's page has no lifetime of its own; it goes with the last handle to the ring.
-    shm: Arc<SharedMemObject>,
-    page: DirectMap,
-    completion_size: u32,
-    /// The kernel's own copy of the completion tail, the only one it reads.
-    completion_tail: u32,
-}
-
-impl Completions {
     fn completion_head(&self) -> &core::sync::atomic::AtomicU32 {
-        ring_word(&self.page, COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, head))
+        self.ring_word(COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, head))
     }
 
     fn completion_tail_word(&self) -> &core::sync::atomic::AtomicU32 {
-        ring_word(&self.page, COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, tail))
+        self.ring_word(COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, tail))
     }
 
     fn completion_dropped(&self) -> &core::sync::atomic::AtomicU32 {
-        ring_word(&self.page, COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, dropped))
+        self.ring_word(COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, dropped))
+    }
+
+    /// One submission entry, copied out by value via `read_volatile` — never a `&Submission`.
+    fn submission_at(&self, index: u32) -> Submission {
+        let ptr = self.shm_phys.as_mut_ptr::<u8>();
+        // SAFETY: `index` is masked by `submission_size` (≤256), keeping the read in-bounds and aligned within the page.
+        unsafe { (ptr.add(SUBMISSIONS_OFF as usize + index as usize * core::mem::size_of::<Submission>()) as *const Submission).read_volatile() }
     }
 
     /// The address of one completion entry — a pointer, never a `&mut` minted from a shared borrow.
     fn completion_at(&self, index: u32) -> *mut Completion {
-        let ptr = self.page.as_mut_ptr::<u8>();
+        let ptr = self.shm_phys.as_mut_ptr::<u8>();
         // SAFETY: `index` is masked by `completion_size` (≤512), keeping the offset inside the page.
         unsafe { ptr.add(COMPLETION_RING_OFF as usize + core::mem::size_of::<RingHeader>() + index as usize * core::mem::size_of::<Completion>()) as *mut Completion }
     }
@@ -290,7 +269,7 @@ impl Completions {
             return;
         }
         let idx = tail & (self.completion_size - 1);
-        // SAFETY: `idx` is masked to ring size; the completions' lock serializes kernel writers.
+        // SAFETY: `idx` is masked to ring size; the ring's lock serializes kernel writers.
         unsafe { self.completion_at(idx).write(Completion { token: user_data, result, flags }) };
         self.completion_tail = tail.wrapping_add(1);
         self.completion_tail_word().store(tail.wrapping_add(1), Ordering::Release);
@@ -313,23 +292,15 @@ impl Inbox {
     /// Post one completion and wake whoever waits in `submit`. A ring already
     /// torn down takes nothing and wakes nobody.
     fn complete(&self, user_data: u64, result: i32) {
-        let posted = self.completions.with(|c| {
-            c.as_mut().map(|c| c.post_completion(user_data, result, 0))
-        });
-        if posted.is_some() {
-            // In place: an interrupt handler's post reaches here through the
-            // poll it fires.
-            self.watch.post_in_place();
+        let posted = self.with_state(|state| state.post_completion(user_data, result, 0));
+        if posted.is_ok() {
+            self.watch.post();
         }
     }
 
     fn with_state<R>(&self, f: impl FnOnce(&mut RingState) -> R) -> Result<R, SyscallError> {
         self.state.lock().as_mut().map(f).ok_or(SyscallError::NotFound)
     }
-
-    fn with_completions<R>(&self, f: impl FnOnce(&Completions) -> R) -> Result<R, SyscallError> {
-        self.completions.with(|c| c.as_ref().map(f)).ok_or(SyscallError::NotFound)
-    }
 }
 
 /// Largest submission ring a process may ask for.
@@ -386,17 +357,14 @@ pub fn create(depth: u32) -> Result<(InboxRef, u64), SyscallError> {
     let inbox = Arc::new(Inbox {
         state: Lock::new(Some(RingState {
             shm_phys,
-            submission_size,
-            pending: Vec::new(),
-            owner_pid: pid,
-        })),
-        completions: IrqLock::new(Some(Completions {
             shm,
-            page: shm_phys,
+            submission_size,
             completion_size,
             completion_tail: 0,
+            pending: Vec::new(),
+            owner_pid: pid,
         })),
-        watch: IrqWatch::new(),
+        watch: Watch::new(),
     });
     Ok((InboxRef(inbox), shm_vaddr))
 }
@@ -423,7 +391,7 @@ pub fn submit(
     }
 
     loop {
-        let (count, dropped) = inbox.with_completions(|c| (c.completion_count(), c.dropped()))?;
+        let (count, dropped) = inbox.with_state(|s| (s.completion_count(), s.dropped()))?;
 
         if count >= min_complete || min_complete == 0 {
             return Ok(count);
@@ -450,7 +418,7 @@ pub fn submit(
             0,
             WaitClass::Io,
             deadline,
-            || inbox.with_completions(|c| c.completion_count()).map_or(true, |n| n >= min_complete),
+            || inbox.with_state(|s| s.completion_count()).map_or(true, |n| n >= min_complete),
         )
         .is_err()
         {
diff --git a/kernel/src/irq_ring.rs b/kernel/src/irq_ring.rs
index a71e5204c..6d2c72165 100644
--- a/kernel/src/irq_ring.rs
+++ b/kernel/src/irq_ring.rs
@@ -10,23 +10,19 @@ use crate::arch::percpu;
 use crate::scheduler::MAX_CPUS;
 
 /// Interrupt sources that drive scheduling; exhaustive, so a new variant requires updating every `match`.
-/// The discriminant is `trace::Kind::IrqDrain`'s top byte, pinned for the reason `Kind` is.
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
 pub enum IrqSource {
-    Xhci = 2,
-    I8042 = 3,
+    Audio,
+    /// Every vector a claimed PCI function delivers on, coalesced into one
+    /// slot: the record only says a pass is owed, and `pcidev` keeps the
+    /// per-slot flag that says whose.
+    UserDev,
+    Xhci,
+    I8042,
 }
 
 impl IrqSource {
-    pub const COUNT: usize = 2;
-
-    /// This source's record in a CPU's slots.
-    const fn slot(self) -> usize {
-        match self {
-            Self::Xhci => 0,
-            Self::I8042 => 1,
-        }
-    }
+    pub const COUNT: usize = 4;
 }
 
 /// 64-byte aligned so two CPUs' slots never share a cache line.
@@ -40,7 +36,7 @@ static SLOTS: [CpuSlots; MAX_CPUS] =
 pub fn isr_publish(source: IrqSource, timestamp_nanos: u64) {
     // MSI-X vectors are configured after clock calibration, so a real IRQ never stamps 0.
     assert!(timestamp_nanos != 0, "irq_ring: zero IRQ timestamp");
-    let slot = &SLOTS[percpu::cpu_id() as usize].0[source.slot()];
+    let slot = &SLOTS[percpu::cpu_id() as usize].0[source as usize];
     // ISRs run with IF=0, so this load-then-store can't interleave with a same-CPU `take`.
     if slot.load(Ordering::Relaxed) == 0 {
         slot.store(timestamp_nanos, Ordering::Relaxed);
@@ -49,7 +45,7 @@ pub fn isr_publish(source: IrqSource, timestamp_nanos: u64) {
 
 /// Consumes the current CPU's pending record for `source`, returning its IRQ-time timestamp.
 pub fn take(source: IrqSource) -> Option<u64> {
-    let slot = &SLOTS[percpu::cpu_id() as usize].0[source.slot()];
+    let slot = &SLOTS[percpu::cpu_id() as usize].0[source as usize];
     // Atomic swap: an interrupting ISR sees either the old record or the cleared slot, never a torn value.
     match slot.swap(0, Ordering::Relaxed) {
         0 => None,
@@ -63,7 +59,7 @@ pub fn take(source: IrqSource) -> Option<u64> {
 
 /// True if `source` has an undrained record on this CPU; non-consuming, unlike [`take`].
 pub fn pending(source: IrqSource) -> bool {
-    SLOTS[percpu::cpu_id() as usize].0[source.slot()].load(Ordering::Relaxed) != 0
+    SLOTS[percpu::cpu_id() as usize].0[source as usize].load(Ordering::Relaxed) != 0
 }
 
 /// True if any IRQ record is undrained on the current CPU; non-consuming.
diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs
index ac7edc38b..20385f95d 100644
--- a/kernel/src/object/ops.rs
+++ b/kernel/src/object/ops.rs
@@ -17,8 +17,7 @@ use crate::time::Deadline;
 use crate::pipe::{self, PipeId};
 use crate::process::PipeMap;
 use crate::user_ptr::{UserBytes, UserBytesMut};
-use crate::inbox::PollEntry;
-use crate::watch::{IrqWatch, Watch};
+use crate::watch::Watch;
 use crate::{device as device_registry, keyboard, mouse};
 
 use super::device::DeviceClaim;
@@ -243,24 +242,14 @@ pub fn pipe_write(object: &KObjectRef) -> Option<(PipeId, WaitClass)> {
 pub enum WatchRef {
     Static(&'static Watch),
     Shared(Arc<Watch>),
-    /// A device's, which its interrupt handler posts.
-    Irq(&'static IrqWatch),
 }
 
-impl WatchRef {
-    pub(crate) fn add_poll(&self, entry: PollEntry) {
+impl core::ops::Deref for WatchRef {
+    type Target = Watch;
+    fn deref(&self) -> &Watch {
         match self {
-            Self::Static(watch) => watch.add_poll(entry),
-            Self::Shared(watch) => watch.add_poll(entry),
-            Self::Irq(watch) => watch.add_poll(entry),
-        }
-    }
-
-    pub fn cancel_polls(&self) {
-        match self {
-            Self::Static(watch) => watch.cancel_polls(),
-            Self::Shared(watch) => watch.cancel_polls(),
-            Self::Irq(watch) => watch.cancel_polls(),
+            Self::Static(watch) => watch,
+            Self::Shared(watch) => watch,
         }
     }
 }
@@ -277,10 +266,10 @@ pub fn read_watch(object: &KObjectRef) -> Option<WatchRef> {
             device_registry::DeviceType::Keyboard => Some(WatchRef::Static(&keyboard::WATCH)),
             device_registry::DeviceType::Mouse => Some(WatchRef::Static(&mouse::WATCH)),
             device_registry::DeviceType::PciFunction => {
-                d.pci_slot().map(|slot| WatchRef::Irq(crate::pcidev::watch(slot)))
+                d.pci_slot().map(|slot| WatchRef::Static(crate::pcidev::watch(slot)))
             }
             device_registry::DeviceType::HdaAudio | device_registry::DeviceType::VirtioSound => {
-                Some(WatchRef::Irq(&crate::drivers::AUDIO_WATCH))
+                Some(WatchRef::Static(&crate::drivers::AUDIO_WATCH))
             }
             device_registry::DeviceType::Framebuffer => None,
             // A partition answers its description and has nothing to wait for.
diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs
index d87b5cd29..f3efd833b 100644
--- a/kernel/src/pcidev/mod.rs
+++ b/kernel/src/pcidev/mod.rs
@@ -117,7 +117,7 @@ use crate::mm::policy::{CachePolicy, MmioPolicy};
 use crate::mm::{align_2m, DirectMap, Mmio, PAGE_2M};
 use crate::object::shm::{Region, SharedMemObject};
 use crate::sync::Lock;
-use crate::watch::IrqWatch;
+use crate::watch::Watch;
 
 /// How many functions this machine can hand out at once.
 ///
@@ -280,7 +280,7 @@ static BOUND: [Lock<Option<Bound>>; MAX_FUNCTIONS] =
 
 /// What a claimed function's poll waits on, one per slot: two processes each driving a
 /// function must not learn when the other's device is busy.
-static WATCHES: [IrqWatch; MAX_FUNCTIONS] = [const { IrqWatch::new() }; MAX_FUNCTIONS];
+static WATCHES: [Watch; MAX_FUNCTIONS] = [const { Watch::new() }; MAX_FUNCTIONS];
 
 /// Every function this machine enumerated, and the two windows a BAR may be
 /// moved into.
@@ -1663,11 +1663,7 @@ pub fn take_record(slot: usize) -> Result<Option<DeviceIrqRecord>, SyscallError>
     if IRQ[slot].faulted() {
         return Err(SyscallError::Io);
     }
-    let taken = IRQ[slot].take();
-    if taken.is_some() && IRQ[slot].take_unannounced() {
-        log!("pcidev: slot {slot} took its first message on vector {:#x}", VECTORS[slot]);
-    }
-    Ok(taken.map(|count| DeviceIrqRecord { count }))
+    Ok(IRQ[slot].take().map(|count| DeviceIrqRecord { count }))
 }
 
 /// Whether a read of the claim answers at once: a message is waiting, or the
@@ -1676,26 +1672,46 @@ pub fn has_irq(slot: usize) -> bool {
     IRQ[slot].armed() || IRQ[slot].faulted()
 }
 
-/// Records one message and posts the claim's watch. Called from the vector's
-/// handler, so it allocates nothing; `record.rs` owns the counting, and
-/// `kernel-loom` models it against a concurrent reader.
+/// Records one message. Called from the vector's ISR, so it takes no lock and
+/// allocates nothing; `record.rs` owns the counting, and `kernel-loom` models
+/// it against a concurrent reader.
 pub fn isr(slot: usize) {
     IRQ[slot].took();
-    WATCHES[slot].post_in_place();
+}
+
+/// Turn every message taken since the last pass into a wake.
+///
+/// On the scheduler pass rather than in the ISR, like every other device in
+/// this kernel: a wake takes the inbox lock and an ISR may not.
+pub fn drain_pending() {
+    for (slot, irq) in IRQ.iter().enumerate() {
+        if !irq.take_pending() {
+            continue;
+        }
+        // A fault's wake is no message.
+        if !irq.faulted() && irq.take_unannounced() {
+            log!(
+                "pcidev: slot {slot} took its first message on vector {:#x}",
+                VECTORS[slot]
+            );
+        }
+        WATCHES[slot].post();
+    }
 }
 
 /// The unit refused this function an access.
 ///
-/// Called from the fault handler: every call the claim answers refuses from
-/// here on, its interrupt read included, and the post wakes whoever waits on
-/// the claim to read that refusal, as a message's does.
+/// Called from the fault handler, which takes no lock: every call the claim
+/// answers refuses from here on, its interrupt read included, and this CPU's
+/// next scheduler pass wakes whoever waits on the claim to read that refusal —
+/// the pass a message earns, posted the way its ISR posts it.
 pub fn note_fault(slot: usize) {
     IRQ[slot].fault();
-    WATCHES[slot].post_in_place();
+    crate::irq_ring::isr_publish(crate::irq_ring::IrqSource::UserDev, crate::clock::nanos_since_boot());
     crate::preempt::set_need_resched();
 }
 
 /// The watch of the function a claim holds at `slot`.
-pub fn watch(slot: usize) -> &'static IrqWatch {
+pub fn watch(slot: usize) -> &'static Watch {
     &WATCHES[slot]
 }
diff --git a/kernel/src/pcidev/record.rs b/kernel/src/pcidev/record.rs
index 72ff55f33..51f27139a 100644
--- a/kernel/src/pcidev/record.rs
+++ b/kernel/src/pcidev/record.rs
@@ -6,16 +6,20 @@
 //! not an ordering, and no guest test in this suite lands on it.
 //!
 //! **Two parties race**: the ISR, on whichever CPU the unit routed the message
-//! to, and the holder reading its record through a syscall on any CPU.
+//! to, and the holder reading its record through a syscall on any CPU. The
+//! scheduler pass that turns a message into a wake runs on the ISR's own CPU
+//! after it, so those two do not interleave.
 //!
-//! **The invariant is that every message is counted exactly once.** The count
-//! is a read-modify-write on both sides and never a load followed by a store:
-//! a reader that loaded a count and then cleared it drops every message the ISR
-//! recorded in between, and a driver that misses one waits for a device that
-//! has already spoken. No ordering carries anything across these words — each
-//! is the whole of what it says — so the orderings here are `Relaxed` and the
-//! model is about the interleaving; the holder reads them after the wake the
-//! claim's watch post owes it, which orders them.
+//! **The invariant is that every message is counted exactly once, and owes
+//! exactly one wake.** Both are read-modify-writes and neither is a load
+//! followed by a store: a reader that loaded a count and then cleared it drops
+//! every message the ISR recorded in between, and a driver that misses one
+//! waits for a device that has already spoken. No ordering carries anything
+//! across these words — each is the whole of what it says — so the orderings
+//! here are `Relaxed` and the model is about the interleaving, **but for one
+//! edge**: a fault arms the same wake a message does, and the pass that takes
+//! that wake has to read the fault, so `pending` is released by [`Interrupt::fault`]
+//! and acquired by [`Interrupt::take_pending`].
 
 #[cfg(not(feature = "loom"))]
 use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
@@ -28,13 +32,16 @@ use loom::sync::atomic::{AtomicBool, AtomicU32, Ordering};
 /// control for and `kernel-loom` is the model of.
 const ORDER: Ordering = Ordering::Relaxed;
 
-/// The negative control: the read-modify-writes become a load and a store,
+/// The negative control: the two read-modify-writes become a load and a store,
 /// which is the whole of what this record's design is. Never on in a kernel
 /// build.
 #[cfg(feature = "device-irq-lossy")]
 macro_rules! take_word {
-    ($word:expr, $empty:expr) => {{
-        let held = $word.load(ORDER);
+    ($word:expr, $empty:expr) => {
+        take_word!($word, $empty, ORDER)
+    };
+    ($word:expr, $empty:expr, $order:expr) => {{
+        let held = $word.load($order);
         $word.store($empty, ORDER);
         held
     }};
@@ -42,7 +49,10 @@ macro_rules! take_word {
 #[cfg(not(feature = "device-irq-lossy"))]
 macro_rules! take_word {
     ($word:expr, $empty:expr) => {
-        $word.swap($empty, ORDER)
+        take_word!($word, $empty, ORDER)
+    };
+    ($word:expr, $empty:expr, $order:expr) => {
+        $word.swap($empty, $order)
     };
 }
 
@@ -62,10 +72,12 @@ macro_rules! bump {
 
 /// What the ISR writes and the claim reads back.
 ///
-/// Atomics only: the handler allocates nothing.
+/// Atomics only: the handler takes no lock and allocates nothing.
 pub struct Interrupt {
     /// Messages since the holder's last read.
     count: AtomicU32,
+    /// Set by the ISR, cleared by the scheduler pass that turns it into a wake.
+    pending: AtomicBool,
     /// The unit refused this function an access. Every call the claim answers
     /// refuses from here on: its bus mastering is gone, so a driver that kept
     /// going would be driving nothing.
@@ -85,6 +97,7 @@ impl Interrupt {
     pub const fn new() -> Self {
         Self {
             count: AtomicU32::new(0),
+            pending: AtomicBool::new(false),
             faulted: AtomicBool::new(false),
             unannounced: AtomicBool::new(true),
         }
@@ -96,6 +109,7 @@ impl Interrupt {
     pub fn new() -> Self {
         Self {
             count: AtomicU32::new(0),
+            pending: AtomicBool::new(false),
             faulted: AtomicBool::new(false),
             unannounced: AtomicBool::new(true),
         }
@@ -108,6 +122,12 @@ impl Interrupt {
     /// two of these, and what it took plus what is left has to be what arrived.
     pub fn took(&self) {
         bump!(self.count);
+        // `swap` and not a store: [`Self::fault`] releases through this same
+        // word, and a plain write landing after that release in `pending`'s
+        // modification order ends the release sequence there — the pass that
+        // later takes the fault's wake would then synchronize with nothing.
+        // An RMW extends the sequence instead, whichever order it lands in.
+        self.pending.swap(true, ORDER);
     }
 
     /// The messages since the last read, or `None` for none.
@@ -127,19 +147,36 @@ impl Interrupt {
         self.count.load(ORDER) != 0
     }
 
+    /// Whether a wake is owed, taken at most once per message. Answers `true`
+    /// for the pass that owes it and `false` for every pass after.
+    ///
+    /// `swap` for the same reason as [`Self::take`]: two passes that both
+    /// loaded `true` would both wake one message's watchers. `Acquire`, so
+    /// the pass that takes a fault's wake reads [`Self::faulted`] set.
+    pub fn take_pending(&self) -> bool {
+        take_word!(self.pending, false, Ordering::Acquire)
+    }
+
     /// Whether this is the first message this slot has taken. Answers `true`
     /// once per claim and `false` ever after, so a caller may log on it.
     ///
-    /// `swap` for [`Self::take`]'s reason: two reads that both loaded `true`
-    /// would both announce one message.
+    /// `swap` for [`Self::take_pending`]'s reason: two passes that both loaded
+    /// `true` would both announce one message.
     pub fn take_unannounced(&self) -> bool {
         take_word!(self.unannounced, false)
     }
 
-    /// The unit refused this function an access. Called from the fault handler:
-    /// every call the claim answers refuses from here on.
+    /// The unit refused this function an access. Called from the fault handler,
+    /// which takes no lock: every call the claim answers refuses from here on,
+    /// and a wake is owed as for a message, because a holder waiting on the
+    /// claim would otherwise wait for a function that can no longer speak.
+    ///
+    /// The wake is a `swap` and not a store: this one races a pass on another
+    /// CPU, and loom 0.7 lets a plain store be lost to a concurrent `swap`, which
+    /// C11 forbids, so a store here is a wake the model cannot show is owed.
     pub fn fault(&self) {
         self.faulted.store(true, ORDER);
+        self.pending.swap(true, Ordering::Release);
     }
 
     pub fn faulted(&self) -> bool {
@@ -150,6 +187,7 @@ impl Interrupt {
     /// up. The holder is not running at either point.
     pub fn clear(&self) {
         self.count.store(0, ORDER);
+        self.pending.store(false, ORDER);
         self.faulted.store(false, ORDER);
         self.unannounced.store(true, ORDER);
     }
diff --git a/kernel/src/sched/driver.rs b/kernel/src/sched/driver.rs
index 800611f60..14a17ad7a 100644
--- a/kernel/src/sched/driver.rs
+++ b/kernel/src/sched/driver.rs
@@ -671,6 +671,18 @@ fn drain_irqs(entered: super::dump::Entered) {
     super::dump::serve_if_owed();
     // Repaints the panel if whoever owns the screen has drawn over the report.
     crate::drivers::panic_console::hold_report();
+
+    if crate::irq_ring::take(crate::irq_ring::IrqSource::UserDev).is_some() {
+        // Which claim it was is the per-slot flag `pcidev` keeps; the record
+        // here says only that a pass is owed, so one function's interrupt does
+        // not wake every user driver in the machine.
+        crate::pcidev::drain_pending();
+    }
+    if crate::irq_ring::take(crate::irq_ring::IrqSource::Audio).is_some() {
+        // Both backends share one watch, so a second would need the parking side
+        // to know which driver bound, which it doesn't.
+        crate::drivers::AUDIO_WATCH.post();
+    }
 }
 
 /// Leave the current stack for this CPU's idle stack and never come back.
diff --git a/kernel/src/sched/payload.rs b/kernel/src/sched/payload.rs
index 34d737df6..523e2a6ed 100644
--- a/kernel/src/sched/payload.rs
+++ b/kernel/src/sched/payload.rs
@@ -8,7 +8,7 @@ use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
 use toyos_sched::fair::{FairShare, ShareState};
 use toyos_sched::hw::Nanos;
 use toyos_sched::msg::Msg;
-use toyos_sched::sync::CellLock;
+use toyos_sched::sync::LeafLock;
 use toyos_sched::task::{SchedPayload, TaskAccounting, TaskShared, WaitClass};
 use toyos_sched::park::WaitTicket;
 
@@ -28,7 +28,7 @@ impl<T> KernelLock<T> {
     }
 }
 
-impl<T: Send> CellLock<T> for KernelLock<T> {
+impl<T: Send> LeafLock<T> for KernelLock<T> {
     fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
         f(&mut self.0.lock())
     }
diff --git a/kernel/src/watch.rs b/kernel/src/watch.rs
index 8697bb067..42c8a0b51 100644
--- a/kernel/src/watch.rs
+++ b/kernel/src/watch.rs
@@ -9,10 +9,9 @@
 //! record of what was posted — a waiter re-reads the object, never the post.
 //!
 //! **A post allocates nothing and may be made under any lock but a poll
-//! ring's** (`crate::inbox`). A watch an interrupt handler posts is an
-//! [`IrqWatch`]: its list sits behind an [`IrqLock`], and its post is made in
-//! place and frees nothing. Every other watch's list lock leaves interrupts
-//! open, and no handler takes it. Registration allocates, in the syscall that registers.
+//! ring's** (`crate::inbox`), so a driver posts from its scheduler-pass drain
+//! and never from its ISR, which publishes to `irq_ring` and nothing else.
+//! Registration allocates, in the syscall that registers.
 //!
 //! A [`Watch`] is a borrowed reference for the whole of a wait: [`Armed`]
 //! holds it, so an object cannot be freed under a thread waiting on it, and
@@ -21,7 +20,6 @@
 use alloc::sync::Arc;
 
 use toyos_sched::hw::Nanos;
-use toyos_sched::sync::CellLock;
 use toyos_sched::task::{Refused, WaitClass, WakeCause, WakeReason};
 use toyos_sched::watch::{Poster, Waiters};
 
@@ -34,64 +32,14 @@ use crate::sched::payload::{KMsg, KShared, KernelLock, TaskHandle};
 use crate::scheduler::Parkable;
 use crate::time::Deadline;
 
-type List = Waiters<KMsg, PollEntry>;
+type Inner = toyos_sched::watch::Watch<KMsg, PollEntry, KernelLock<Waiters<KMsg, PollEntry>>>;
 
-/// What an object holds to be waitable, its list behind `L`.
-pub struct Waitable<L: CellLock<List>>(toyos_sched::watch::Watch<KMsg, PollEntry, L>);
-
-/// A watch no interrupt handler reaches.
-pub type Watch = Waitable<KernelLock<List>>;
-
-/// A watch an interrupt handler posts, and the watch of a ring such a post
-/// completes into. It has no `post`, which frees: [`IrqWatch::post_in_place`]
-/// frees nothing.
-pub type IrqWatch = Waitable<IrqLock<List>>;
-
-/// What an interrupt handler's post takes: an [`IrqWatch`]'s list and a poll
-/// ring's completions. Held with interrupts off, so a handler never finds one
-/// held by the context it interrupted; nothing allocates or frees under it.
-pub struct IrqLock<T>(masked::Masked<T>);
-
-impl<T> IrqLock<T> {
-    pub const fn new(value: T) -> Self {
-        Self(masked::Masked::new(value))
-    }
-}
-
-impl<T: Send> CellLock<T> for IrqLock<T> {
-    fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
-        // Raised first, so the lock's own release never reaches depth zero, and
-        // a pass, with interrupts masked.
-        preempt_off(|_| {
-            let irq = crate::arch::IrqGuard::close();
-            let mut held = self.0.lock(&irq);
-            f(&mut held)
-        })
-    }
-}
-
-mod masked {
-    use crate::arch::IrqGuard;
-    use crate::sync::{Lock, LockGuard};
-
-    /// A lock taken only through a borrow of a closed [`IrqGuard`]: never
-    /// before the mask, and never held past it.
-    pub struct Masked<T>(Lock<T>);
-
-    impl<T> Masked<T> {
-        pub const fn new(value: T) -> Self {
-            Self(Lock::new(value))
-        }
-
-        pub fn lock<'a>(&'a self, _closed: &'a IrqGuard) -> LockGuard<'a, T> {
-            self.0.lock()
-        }
-    }
-}
+/// What an object holds to be waitable.
+pub struct Watch(Inner);
 
 impl Watch {
     pub const fn new() -> Self {
-        Self(toyos_sched::watch::Watch::new(KernelLock::new(Waiters::new())))
+        Self(Inner::new(KernelLock::new(Waiters::new())))
     }
 
     /// Something about the object changed: wake every thread waiting on it and
@@ -134,44 +82,23 @@ impl Watch {
             )
         })
     }
-}
 
-impl IrqWatch {
-    pub const fn new() -> Self {
-        Self(toyos_sched::watch::Watch::new(IrqLock::new(Waiters::new())))
-    }
-
-    /// Something about the object changed: wake every thread waiting on it and
-    /// complete every poll where it stands, freeing nothing. A handler makes it
-    /// with its CPU's preempt count raised, as `device_irq_entry` holds it, so
-    /// this post's own never reaches zero, and a pass, inside the interrupt.
-    pub fn post_in_place(&self) {
-        preempt_off(|p| {
-            let env = Poster { cpus: cpus(), kicker: &HW, preempt: p };
-            self.0.post_in_place(WakeCause::new(WakeReason::Woken), &env);
-        });
-    }
-}
-
-impl<L: CellLock<List>> Waitable<L> {
     /// A poll ring's entry, from `inbox`'s registration and nowhere else.
     pub(crate) fn add_poll(&self, entry: PollEntry) {
         self.0.add_ring(entry);
     }
 
     /// Answer every poll registered here as gone: the source it watched ended.
-    /// A thread's, since it frees what it answered.
     pub fn cancel_polls(&self) {
         self.0.cancel_rings();
     }
-
 }
 
 /// A thread's registration on one watch, held across its wait and ended by
 /// its drop.
 #[must_use = "a registration must outlive the park it was made for"]
-pub struct Armed<'a, L: CellLock<List> = KernelLock<List>> {
-    watch: &'a Waitable<L>,
+pub struct Armed<'a> {
+    watch: &'a Watch,
     shared: Arc<KShared>,
     task: Arc<TaskHandle>,
     /// Wait class for the blocked-time breakdown; the park carries no subject
@@ -179,7 +106,7 @@ pub struct Armed<'a, L: CellLock<List> = KernelLock<List>> {
     class: WaitClass,
 }
 
-impl<L: CellLock<List>> Drop for Armed<'_, L> {
+impl Drop for Armed<'_> {
     fn drop(&mut self) {
         self.watch.0.unregister(&self.shared);
     }
@@ -187,11 +114,7 @@ impl<L: CellLock<List>> Drop for Armed<'_, L> {
 
 /// Register the running task on `watch`; `None` when there is no current task.
 /// Call before reading the condition the wait is for.
-pub fn arm<L: CellLock<List>>(
-    watch: &Waitable<L>,
-    token: u64,
-    class: WaitClass,
-) -> Option<Armed<'_, L>> {
+pub fn arm(watch: &Watch, token: u64, class: WaitClass) -> Option<Armed<'_>> {
     let task = crate::sched::driver::current_handle()?;
     let shared = crate::sched::driver::current_shared()?;
     watch.0.register(&shared, token);
@@ -221,11 +144,7 @@ fn not_revocable() -> ! {
 /// this thread is cancelled. A return is not an answer — the caller re-reads
 /// its condition.
 #[track_caller]
-pub fn wait<L: CellLock<List>>(
-    p: &Parkable,
-    armed: &Armed<'_, L>,
-    deadline: Deadline,
-) -> Result<(), Cancelled> {
+pub fn wait(p: &Parkable, armed: &Armed<'_>, deadline: Deadline) -> Result<(), Cancelled> {
     match wait_inner(p, armed, deadline, Cancel::Answers) {
         Ok(()) => Ok(()),
         Err(Ended::Cancelled) => Err(Cancelled(())),
@@ -235,7 +154,7 @@ pub fn wait<L: CellLock<List>>(
 
 /// The same as [`wait`], for a wait a kill may not end.
 #[track_caller]
-pub fn wait_uncancellable<L: CellLock<List>>(p: &Parkable, armed: &Armed<'_, L>, deadline: Deadline) {
+pub fn wait_uncancellable(p: &Parkable, armed: &Armed<'_>, deadline: Deadline) {
     match wait_inner(p, armed, deadline, Cancel::Ignores) {
         Ok(()) => {}
         Err(Ended::Cancelled) => unreachable!("an uncancellable wait never reports a cancel"),
@@ -249,9 +168,9 @@ pub fn wait_uncancellable<L: CellLock<List>>(p: &Parkable, armed: &Armed<'_, L>,
 /// be waited on again safely, and the caller reads its own condition again to
 /// tell them apart.
 #[track_caller]
-pub fn wait_until<L: CellLock<List>>(
+pub fn wait_until(
     p: &Parkable,
-    watch: &Waitable<L>,
+    watch: &Watch,
     token: u64,
     class: WaitClass,
     deadline: Deadline,
@@ -297,9 +216,9 @@ pub fn wait_uncancellable_until(p: &Parkable, watch: &Watch, token: u64, ready:
 }
 
 #[track_caller]
-fn wait_inner<L: CellLock<List>>(
+fn wait_inner(
     _p: &Parkable,
-    armed: &Armed<'_, L>,
+    armed: &Armed<'_>,
     deadline: Deadline,
     cancel: Cancel,
 ) -> Result<(), Ended> {
diff --git a/toyos-sched/src/cpu.rs b/toyos-sched/src/cpu.rs
index 6700a829a..5be981f47 100644
--- a/toyos-sched/src/cpu.rs
+++ b/toyos-sched/src/cpu.rs
@@ -2401,13 +2401,13 @@ mod tests {
     use crate::fair::{FairShare, ShareState};
     use crate::hw::{Kicker, Machine};
     use crate::mailbox::{mailbox, NoPreempt};
-    use crate::sync::CellLock;
+    use crate::sync::LeafLock;
     use crate::task::{RtState, TaskAccounting, TaskBuilder};
     use std::sync::Mutex;
 
     struct TestLock<T>(Mutex<T>);
 
-    impl<T: Send> CellLock<T> for TestLock<T> {
+    impl<T: Send> LeafLock<T> for TestLock<T> {
         fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
             f(&mut self.0.lock().expect("a test never poisons a lock"))
         }
diff --git a/toyos-sched/src/fair.rs b/toyos-sched/src/fair.rs
index cbcd46030..95aa1256a 100644
--- a/toyos-sched/src/fair.rs
+++ b/toyos-sched/src/fair.rs
@@ -5,7 +5,7 @@
 use core::num::NonZeroU32;
 use core::sync::atomic::{AtomicU64, Ordering};
 
-use crate::sync::CellLock;
+use crate::sync::LeafLock;
 
 /// Clamp for stored lag at the Runnable→NonRunnable transition: how far
 /// behind (entitled catch-up) or ahead (throttled on wake) of the frontier a
@@ -206,13 +206,13 @@ fn vrt_from_lag(frontier: u64, lag: i64) -> u64 {
 }
 
 /// One process's fair-share pot, reached through any thread that owns it. The
-/// cell is supplied by the environment for the reason stated on [`CellLock`]:
+/// cell is supplied by the environment for the reason stated on [`LeafLock`]:
 /// the kernel's is a word-sized spin, the simulator's a mutex.
 pub struct FairShare<L> {
     state: L,
 }
 
-impl<L: CellLock<ShareState>> FairShare<L> {
+impl<L: LeafLock<ShareState>> FairShare<L> {
     pub fn new(state: L) -> Self {
         Self { state }
     }
diff --git a/toyos-sched/src/sync.rs b/toyos-sched/src/sync.rs
index 1a08ca171..b0e1a9827 100644
--- a/toyos-sched/src/sync.rs
+++ b/toyos-sched/src/sync.rs
@@ -20,12 +20,13 @@ pub use loom::sync::Arc;
 
 /// Interior mutability for a small shared cell, supplied by the environment:
 /// a watch's waiter list and the per-process fair share. The kernel's
-/// implementor is never held across a pass or a switch; the simulator and the
-/// loom models supply their own.
+/// implementor is a few-instruction, IRQ-off leaf lock that acquires nothing
+/// beneath it and is never held across a pass or a switch; the simulator and
+/// the loom models supply their own.
 ///
 /// It lives here rather than in one of its users because the core crate may
 /// not implement a lock itself — that would need `unsafe`, which only
 /// `mailbox.rs` is allowed to write.
-pub trait CellLock<T>: Sync {
+pub trait LeafLock<T>: Sync {
     fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R;
 }
diff --git a/toyos-sched/src/task.rs b/toyos-sched/src/task.rs
index 1c490d24c..24f997990 100644
--- a/toyos-sched/src/task.rs
+++ b/toyos-sched/src/task.rs
@@ -13,7 +13,7 @@ use crate::fair::{FairShare, ShareState, QUANTUM_NS};
 use crate::hw::{CpuId, Nanos};
 use crate::mailbox::MailboxNode;
 use crate::msg::Msg;
-use crate::sync::{Arc, AtomicBool, AtomicU64, CellLock, Ordering};
+use crate::sync::{Arc, AtomicBool, AtomicU64, LeafLock, Ordering};
 use crate::park::CommittedTicket;
 
 /// Monotonic, never reused. Stale messages keyed by `TaskKey` are provably
@@ -31,8 +31,8 @@ pub trait SchedPayload: Sized + Send + 'static {
 
     /// The cell the per-process [`FairShare`] lives in. Supplied by the
     /// environment because the core crate may not implement a lock itself
-    /// (see [`CellLock`]).
-    type ShareLock: CellLock<ShareState> + Send;
+    /// (see [`LeafLock`]).
+    type ShareLock: LeafLock<ShareState> + Send;
 }
 
 /// Shorthand for the share type a payload implies.
diff --git a/toyos-sched/src/watch.rs b/toyos-sched/src/watch.rs
index 1b9fcb795..6108818e2 100644
--- a/toyos-sched/src/watch.rs
+++ b/toyos-sched/src/watch.rs
@@ -15,22 +15,16 @@
 //! waiting side does between its registration and its park can be skipped:
 //! `park::prepare` consumes the flag, and the commit consumes the claim.
 //!
-//! **Nothing allocates or frees under the list lock**, because an interrupt
-//! handler's post can interrupt the allocator's holder while another CPU holds
-//! the list waiting for the allocator. A registration takes the entries that
-//! can no longer fire out [`FEW`] at a time, grows a full list in a buffer
-//! allocated with the lock let go, and drops both with it let go.
-//! [`Watch::post`] takes every ring entry out and fires and frees them with the
-//! lock let go; [`Watch::post_in_place`], for a handler, which may not free at
-//! all, fires them where they stand, and later registrations sweep the dead
-//! out, since an entry is one-shot.
+//! **A post allocates nothing.** Every ring entry is one-shot, so a post takes
+//! them all out of the list and fires them with the list lock let go; what it
+//! frees is those entries. A registration sweeps the entries a withdrawal left
+//! behind, so the list never holds more than the polls live at its last post or
+//! registration, plus that one.
 //!
-//! **Lock order.** A post in place fires its rings under the list lock, so
-//! beneath it are each ring's own lock and the watch that ring's submitters
-//! park on, which holds threads and no ring and so nests nothing. The drop of
-//! every entry the list lets go of runs with it let go, because an entry's last
-//! reference may own another watch. So a post may be made under any lock but a
-//! ring's own.
+//! **Lock order.** The list lock is a leaf the environment supplies:
+//! [`Ring::fire`] is always called with it let go, and so is the drop of every
+//! entry the list lets go of, because an entry's last reference may own another
+//! watch. So a post may be made under any lock but a ring's own.
 
 use alloc::vec::Vec;
 use core::marker::PhantomData;
@@ -39,7 +33,7 @@ use crate::cpu::CpuHandles;
 use crate::hw::Kicker;
 use crate::mailbox::{PreemptGuard, SchedMsg};
 use crate::park::{notify, revoke};
-use crate::sync::{fence, Arc, AtomicU32, CellLock, Ordering};
+use crate::sync::{fence, Arc, AtomicU32, LeafLock, Ordering};
 use crate::task::{TaskShared, WakeCause};
 
 /// Why a ring entry is posted.
@@ -56,16 +50,14 @@ pub enum Fire {
 pub trait Ring {
     /// Post this poll's completion. One-shot across every watch the poll is
     /// registered on: an entry that already fired, or whose poll was withdrawn,
-    /// posts nothing. Called with at most the posting watch's list lock held,
-    /// and from an interrupt handler by a post in place: may take only its
-    /// ring's own lock and post only the watch its ring's submitters park on,
-    /// and allocates and frees nothing.
+    /// posts nothing. Called with no watch's list lock held; may take only its
+    /// ring's own lock and post only the watch its ring's submitters park on.
     fn fire(&self, how: Fire);
     /// Whether a fire would still post anything. `false` is permanent.
     fn live(&self) -> bool;
 }
 
-/// Everything a watch holds.
+/// Everything a watch holds, behind the environment's leaf lock.
 pub struct Waiters<M, R> {
     /// In registration order, so a bounded post reaches the longest waiter
     /// first.
@@ -105,16 +97,12 @@ pub struct Poster<'a, M, K, P> {
     pub preempt: &'a P,
 }
 
-/// How many ring entries one section takes out of the list to drop with the
-/// lock let go, on the stack and so allocating nothing.
-pub const FEW: usize = 4;
-
-pub struct Watch<M, R: Ring, L: CellLock<Waiters<M, R>>> {
+pub struct Watch<M, R: Ring, L: LeafLock<Waiters<M, R>>> {
     list: L,
     _msg: PhantomData<fn() -> (M, R)>,
 }
 
-impl<M, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
+impl<M, R: Ring, L: LeafLock<Waiters<M, R>>> Watch<M, R, L> {
     pub const fn new(list: L) -> Self {
         Self {
             list,
@@ -123,7 +111,7 @@ impl<M, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
     }
 }
 
-impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
+impl<M: SchedMsg, R: Ring, L: LeafLock<Waiters<M, R>>> Watch<M, R, L> {
     /// Register the running task for one wait. After this, and before its
     /// condition is read: that order is the whole lost-wake argument. A post
     /// that reached this task during an earlier wait is forgotten here, before
@@ -131,7 +119,14 @@ impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
     pub fn register(&self, task: &Arc<TaskShared<M>>, token: u64) {
         assert!(task.set_waiting(), "a task waits on at most one watch");
         task.forget_posts();
-        self.admit(Waiter { task: task.clone(), token }, |w| &mut w.threads);
+        let dead = self.list.with(|w| {
+            w.threads.push(Waiter {
+                task: task.clone(),
+                token,
+            });
+            sweep(&mut w.rings)
+        });
+        drop(dead);
     }
 
     /// End one wait. Idempotent against [`Self::revoke`], which may have taken
@@ -149,82 +144,28 @@ impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
     /// after this returns and fires the entry itself if the object is already
     /// ready — the ring's half of the same order a thread keeps.
     pub fn add_ring(&self, entry: R) {
-        self.admit(entry, |w| &mut w.rings);
+        let dead = self.list.with(|w| {
+            let dead = sweep(&mut w.rings);
+            w.rings.push(entry);
+            dead
+        });
+        drop(dead);
     }
 
     /// Something changed: wake every registered thread, and fire and let go of
-    /// every ring entry, with the list lock let go.
+    /// every ring entry.
     pub fn post<K: Kicker, P: PreemptGuard>(&self, cause: WakeCause, env: &Poster<'_, M, K, P>) {
-        let mut few: [Option<R>; FEW] = [const { None }; FEW];
-        let many = self.list.with(|w| {
+        let fired = self.list.with(|w| {
             for waiter in &w.threads {
                 notify(&waiter.task, cause, env.cpus, env.kicker, env.preempt);
             }
-            // [`FEW`] leave the list's buffer to the next registration.
-            if w.rings.len() > FEW {
-                return core::mem::take(&mut w.rings);
-            }
-            // Indexed, so an entry with no slot panics rather than being
-            // dropped unfired.
-            for (at, ring) in w.rings.drain(..).enumerate() {
-                few[at] = Some(ring);
-            }
-            Vec::new()
+            core::mem::take(&mut w.rings)
         });
-        for ring in few.iter().flatten().chain(&many) {
+        for ring in &fired {
             ring.fire(Fire::Ready);
         }
     }
 
-    /// [`Self::post`] for a context that may not free, an interrupt handler:
-    /// every ring entry is fired where it stands, under the list lock, so the
-    /// cost is every registration on the watch with that lock held.
-    pub fn post_in_place<K: Kicker, P: PreemptGuard>(
-        &self,
-        cause: WakeCause,
-        env: &Poster<'_, M, K, P>,
-    ) {
-        self.list.with(|w| {
-            for waiter in &w.threads {
-                notify(&waiter.task, cause, env.cpus, env.kicker, env.preempt);
-            }
-            for ring in &w.rings {
-                ring.fire(Fire::Ready);
-            }
-        });
-    }
-
-    /// Put `item` on the list `list` picks, taking out up to [`FEW`] ring
-    /// entries that can no longer fire first. One section while the list has
-    /// room; a full one grows in a buffer allocated with the lock let go, and
-    /// what a section took out is dropped with it let go.
-    fn admit<T>(&self, item: T, list: fn(&mut Waiters<M, R>) -> &mut Vec<T>) {
-        let mut item = item;
-        let mut room = Vec::new();
-        loop {
-            let mut dead: [Option<R>; FEW] = [const { None }; FEW];
-            let full = self.list.with(|w| {
-                take_dead(&mut w.rings, &mut dead);
-                let v = list(w);
-                // Only a buffer that holds the whole list and `item`: another
-                // registration may have outgrown it since it was sized.
-                if v.len() == v.capacity() && room.capacity() > v.len() {
-                    room.append(v);
-                    core::mem::swap(v, &mut room);
-                }
-                if v.len() < v.capacity() {
-                    v.push(item);
-                    return None;
-                }
-                Some((v.capacity(), item))
-            });
-            drop(dead);
-            let Some((seen, back)) = full else { return };
-            item = back;
-            room = Vec::with_capacity((seen * 2).max(FEW));
-        }
-    }
-
     /// Wake at most `limit` threads registered with `token`, in registration
     /// order, and answer how many this post woke. A thread that was not parked
     /// is flagged and spends nothing: its own commit rechecks, and the post
@@ -292,12 +233,6 @@ impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
         }
     }
 
-    /// Run `f` holding the list lock, where a registration holds it, touching
-    /// nothing on the list: an actuator's way to raise an interrupt there.
-    pub fn holding<U>(&self, f: impl FnOnce() -> U) -> U {
-        self.list.with(|_| f())
-    }
-
     /// Registered threads, for a model or a report.
     pub fn threads(&self) -> usize {
         self.list.with(|w| w.threads.len())
@@ -309,22 +244,6 @@ impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
     }
 }
 
-/// Take up to [`FEW`] entries that can no longer fire out of `rings` into
-/// `dead`, and answer how many.
-fn take_dead<R: Ring>(rings: &mut Vec<R>, dead: &mut [Option<R>; FEW]) -> usize {
-    let (mut at, mut taken) = (0, 0);
-    // Order is nothing to a ring entry: every post fires them all.
-    while at < rings.len() && taken < FEW {
-        if rings[at].live() {
-            at += 1;
-        } else {
-            dead[taken] = Some(rings.swap_remove(at));
-            taken += 1;
-        }
-    }
-    taken
-}
-
 /// A word in front of a watch that its posters read to learn whether a post is
 /// owed at all, for a waiter whose condition is a sweep of words the posters
 /// write: the machine's stop, whose posters are every thread's park, band and
@@ -378,9 +297,15 @@ fn gate_fence() {
     }
 }
 
+/// Take out the entries that can no longer fire, for the caller to drop once
+/// the list lock is let go.
+fn sweep<R: Ring>(rings: &mut Vec<R>) -> Vec<R> {
+    rings.extract_if(.., |r| !r.live()).collect()
+}
+
 /// An object that ends with polls still registered answers them: dropping a
 /// watch fires every live ring entry as [`Fire::Gone`].
-impl<M, R: Ring, L: CellLock<Waiters<M, R>>> Drop for Watch<M, R, L> {
+impl<M, R: Ring, L: LeafLock<Waiters<M, R>>> Drop for Watch<M, R, L> {
     fn drop(&mut self) {
         let ended = self.list.with(|w| core::mem::take(&mut w.rings));
         for ring in &ended {
@@ -419,7 +344,7 @@ mod tests {
     }
 
     struct StdLock<T>(Mutex<T>);
-    impl<T: Send> CellLock<T> for StdLock<T> {
+    impl<T: Send> LeafLock<T> for StdLock<T> {
         fn with<U>(&self, f: impl FnOnce(&mut T) -> U) -> U {
             f(&mut self.0.lock().unwrap())
         }
@@ -670,7 +595,7 @@ mod tests {
     }
 
     struct SharedLock(std::sync::Arc<Mutex<Waiters<Msg, Tattler>>>);
-    impl CellLock<Waiters<Msg, Tattler>> for SharedLock {
+    impl LeafLock<Waiters<Msg, Tattler>> for SharedLock {
         fn with<U>(&self, f: impl FnOnce(&mut Waiters<Msg, Tattler>) -> U) -> U {
             f(&mut self.0.lock().unwrap())
         }
@@ -707,49 +632,19 @@ mod tests {
         assert_eq!(w.live_rings(), 1);
     }
 
-    /// A post in place fires where the entry stands and lets go of nothing:
-    /// the entry it fired is still the list's until a registration sweeps it.
-    #[test]
-    fn a_post_in_place_fires_and_drops_nothing() {
-        let (handles, _rx) = cpus();
-        let env = Poster { cpus: &handles, kicker: &NoKick, preempt: &NoPreempt };
-        let w = watch();
-        let poll = Arc::new(Poll::default());
-        w.add_ring(poll.clone());
-        w.post_in_place(woken(), &env);
-        assert_eq!(poll.posts.load(Ordering::Acquire), 1);
-        assert_eq!(Arc::strong_count(&poll), 2, "the post let go of the entry it fired");
-        let t = task(1);
-        w.register(&t, 0);
-        assert_eq!(Arc::strong_count(&poll), 1, "the registration did not sweep it");
-        w.unregister(&t);
-    }
-
-    /// One entry past the [`FEW`] a post takes out on its stack, so a cancel
-    /// or a drop bounded at that many is caught.
     #[test]
     fn cancel_and_drop_answer_every_live_poll_as_gone() {
-        let polls = || -> Vec<_> { (0..=FEW).map(|_| Arc::new(Poll::default())).collect() };
         let w = watch();
-        let cancelled = polls();
-        for poll in &cancelled {
-            w.add_ring(poll.clone());
-        }
+        let cancelled = Arc::new(Poll::default());
+        w.add_ring(cancelled.clone());
         w.cancel_rings();
-        for (at, poll) in cancelled.iter().enumerate() {
-            assert_eq!(poll.state.load(Ordering::Acquire), 2, "poll {at} was not answered as gone");
-            assert_eq!(Arc::strong_count(poll), 1, "the cancel kept poll {at}");
-        }
+        assert_eq!(cancelled.state.load(Ordering::Acquire), 2);
 
-        let dropped = polls();
-        for poll in &dropped {
-            w.add_ring(poll.clone());
-        }
+        let dropped = Arc::new(Poll::default());
+        w.add_ring(dropped.clone());
         drop(w);
-        for (at, poll) in dropped.iter().enumerate() {
-            assert_eq!(poll.state.load(Ordering::Acquire), 2, "dropped poll {at} was not answered as gone");
-            assert_eq!(poll.posts.load(Ordering::Acquire), 1);
-        }
+        assert_eq!(dropped.state.load(Ordering::Acquire), 2);
+        assert_eq!(dropped.posts.load(Ordering::Acquire), 1);
     }
 
     #[test]

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

The scratch probes and the mutation patches, each git apply --check EXIT=0 at 72f16e39a. Their verdicts, with exit codes, are in the pull request body under "The two checks". None of this is in the tree.

x86-probe.patch registers x86_windows_probe: tests/testcases with pwd and test_rs_ring_park_herd as jobs, on four x86-64 CPUs, on boot-actuators,mask-windows,test-actuators armed with windows-staged, judged by mask_windows; with WINDOWS_PROBE=metal in the environment, on mask-windows alone and unarmed, which is the kernel the T14 row boots.

diff --git a/src/build.rs b/src/build.rs
index 1380ca228..68fce4f8e 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1346,8 +1346,8 @@ fn refuse_an_unarmable_boot(own: &[String], features: &[String], params: &[Strin
 }
 
 /// Kernel builds the ordinary test suite is allowed to make.
-pub const TEST_SUITE_KERNEL_BUILDS: [&str; 3] =
-    ["", "boot-actuators,test-actuators", "boot-actuators,mask-windows,test-actuators"];
+pub const TEST_SUITE_KERNEL_BUILDS: [&str; 4] =
+    ["", "boot-actuators,test-actuators", "boot-actuators,mask-windows,test-actuators", "mask-windows"];
 
 /// The windows' instrument over the test kernel, for [`SCHED_CHECK_KERNEL`]'s
 /// reason: one spelling, so one build.
diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs
index 386146128..b9c9afbb4 100644
--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -159,7 +159,7 @@ pub fn boot_census() -> (u32, u32, Vec<String>) {
 /// they measure, and it carries the test kernel's actuators so a stage can mark
 /// its windows. Interactive debug mode is separate: it builds
 /// [`toyos_build::build::DEBUG_KERNEL_BUILD`] and returns before the suite.
-pub const DECLARED_KERNEL_BUILDS: [&str; 3] =
+pub const DECLARED_KERNEL_BUILDS: [&str; 4] =
     toyos_build::build::TEST_SUITE_KERNEL_BUILDS;
 
 /// How many guests the run may have up at once.
diff --git a/tests/toyos.rs b/tests/toyos.rs
index cac71775d..4ec230000 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -174,6 +174,7 @@ const MACHINE_TESTS: &[&str] = &[
     // The nested-NMI report is a raw write to the 16550, which the T14 does not
     // have.
     "nested_nmi_is_loud",
+    "x86_windows_probe",
 ];
 
 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2091,6 +2092,41 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
     match name {
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+        "x86_windows_probe" => {
+            // `WINDOWS_PROBE=metal` boots what the T14 row boots: the instrument
+            // alone, no actuator, no stage.
+            let metal = std::env::var("WINDOWS_PROBE").is_ok_and(|v| v == "metal");
+            let root = compile::repo_root();
+            let herd = qemu::build_toyos_bin(
+                toyos_build::arch::Arch::X86_64,
+                &root.join("tests/toyos-rust-tests"),
+                "ring_park_herd",
+            );
+            let qemu = QemuInstance::boot_with_options(
+                &root.join("tests/windowsprobecase"),
+                &[],
+                &[],
+                BootOptions {
+                    smp: 4,
+                    kernel_features: if metal { &["mask-windows"] } else { toyos_build::build::MASK_WINDOWS_KERNEL },
+                    kernel_params: if metal { &[] } else { &["windows-staged"] },
+                    extra_root_files: vec![("bin/test_rs_ring_park_herd".to_string(), herd)],
+                    ..Default::default()
+                },
+            );
+            let serial = judge_virt_job(qemu, "test_rs_ring_park_herd", "ring_park_herd: 256 threads parked")?;
+            if !metal {
+                return mask_windows(&serial, 4);
+            }
+            let longest = common::irqcensus::windows(&serial)?;
+            if longest.len() != 4 {
+                return Err(format!("{} of 4 CPUs reported windows: {longest:?}", longest.len()));
+            }
+            for most in longest.values() {
+                eprintln!("  [windows] cpu{} irqs_off_ns={} preempt_off_ns={}", most.cpu, most.irqs_off_ns, most.preempt_off_ns);
+            }
+            Ok(())
+        }
         other => Err(format!("unknown machine test {other}")),
     }
 }
diff --git a/tests/windowsprobecase/system.toml b/tests/windowsprobecase/system.toml
new file mode 100644
index 000000000..d4a3ef615
--- /dev/null
+++ b/tests/windowsprobecase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+args = ["--bound-ms=900000", "pwd", "test_rs_ring_park_herd"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]

x86-fault-probe.patch registers x86_windows_fault_probe: one Ring 0 read of an unmapped address through SYS_DEBUG, inside a syscall and so with interrupts masked, on boot-actuators,mask-windows,test-actuators. Green when the console carries the fault's own FAULT rip= report and no mask-windows: refusal.

diff --git a/tests/toyos-rust-tests/src/bin/ring0_read.rs b/tests/toyos-rust-tests/src/bin/ring0_read.rs
new file mode 100644
index 000000000..7365ad2a5
--- /dev/null
+++ b/tests/toyos-rust-tests/src/bin/ring0_read.rs
@@ -0,0 +1,8 @@
+//! Scratch: one Ring 0 read of an unmapped address, inside a syscall and so
+//! with interrupts masked, which ends the machine.
+
+fn main() {
+    println!("ring0_read: asking for a Ring 0 read of 0x10");
+    toyos_abi::syscall::debug_with(toyos_abi::syscall::debug_action::NULL_READ, 0x10);
+    println!("ring0_read: the read returned");
+}
diff --git a/tests/toyos.rs b/tests/toyos.rs
index cac71775d..845e9771f 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -70,6 +70,7 @@ const RUST_SKIP: &[&str] = &[
     // is read off the clock around the syscall. The `tlb_shootdown_waits`
     // metal row runs it on the T14.
     "tlb_shootdown_waits",
+    "ring0_read",
     // Its product is the windows a `mask-windows` kernel reports under it, and
     // its 256 threads' stacks would crowd every member after it in one boot.
     // The `mask_windows` metal row runs it.
@@ -174,6 +175,7 @@ const MACHINE_TESTS: &[&str] = &[
     // The nested-NMI report is a raw write to the 16550, which the T14 does not
     // have.
     "nested_nmi_is_loud",
+    "x86_windows_fault_probe",
 ];
 
 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2091,6 +2093,41 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
     match name {
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+        "x86_windows_fault_probe" => {
+            // A Ring 0 fault taken with interrupts masked, on the instrumented
+            // kernel: the report owed is the fault's, not the record's refusal.
+            let root = compile::repo_root();
+            let read = qemu::build_toyos_bin(
+                toyos_build::arch::Arch::X86_64,
+                &root.join("tests/toyos-rust-tests"),
+                "ring0_read",
+            );
+            let qemu = QemuInstance::boot_with_options(
+                &root.join("tests/windowsfaultcase"),
+                &[],
+                &[],
+                BootOptions {
+                    smp: 4,
+                    kernel_features: toyos_build::build::MASK_WINDOWS_KERNEL,
+                    profile: qemu::Profile::Metal,
+                    ready_marker: "panic: rebooting in",
+                    extra_root_files: vec![("bin/test_rs_ring0_read".to_string(), read)],
+                    ..Default::default()
+                },
+            );
+            let serial = serial::Serial::boot(&qemu);
+            let text = serial.text();
+            if let Some(refused) = text.lines().find(|l| l.contains("mask-windows:")) {
+                return Err(format!("the record refused where a Ring 0 fault was owed its own report: {refused}\n{text}"));
+            }
+            for owed in ["SYS_DEBUG: a Ring 0 read of 0x10", "FAULT rip="] {
+                if !text.contains(owed) {
+                    return Err(format!("{owed:?} not on the console\n{text}"));
+                }
+            }
+            eprintln!("  [windows] a Ring 0 read fault with interrupts masked reported itself");
+            Ok(())
+        }
         other => Err(format!("unknown machine test {other}")),
     }
 }
diff --git a/tests/windowsfaultcase/system.toml b/tests/windowsfaultcase/system.toml
new file mode 100644
index 000000000..dc229891f
--- /dev/null
+++ b/tests/windowsfaultcase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+args = ["--bound-ms=900000", "pwd", "test_rs_ring0_read"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]

m01-syscall-entry-unhooked.patch

diff --git a/kernel/src/arch/x86_64/syscall.rs b/kernel/src/arch/x86_64/syscall.rs
index e7be09402..4e8ba5b0f 100644
--- a/kernel/src/arch/x86_64/syscall.rs
+++ b/kernel/src/arch/x86_64/syscall.rs
@@ -176,7 +176,6 @@ extern "sysv64" fn syscall_handler(num: u64, a1: u64, a2: u64, _: u64, a3: u64,
     // `IA32_FMASK` masked interrupts, and the entry raised the preempt count by one.
     #[cfg(feature = "mask-windows")]
     {
-        crate::windows::irqs_masked();
         crate::windows::preempt_raised();
     }
     #[cfg(feature = "df-witness")]

m02-guard-drop-unhooked.patch

diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs
index d7e401a25..a072656fc 100644
--- a/kernel/src/arch/x86_64/mod.rs
+++ b/kernel/src/arch/x86_64/mod.rs
@@ -82,7 +82,7 @@ impl Drop for IrqGuard {
     fn drop(&mut self) {
         // Only where the restore opens them.
         #[cfg(feature = "mask-windows")]
-        if idt::frame_interrupts_enabled(self.rflags) && !cpu::interrupts_enabled() {
+        if idt::frame_interrupts_enabled(self.rflags) && cpu::interrupts_enabled() {
             crate::windows::irqs_unmasking();
         }
         // SAFETY: the word `close` read out of RFLAGS on this CPU (the guard is

m03-exit-to-user-unhooked.patch

diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs
index 07e9c4ae1..85003c031 100644
--- a/kernel/src/scheduler.rs
+++ b/kernel/src/scheduler.rs
@@ -406,8 +406,6 @@ pub fn exit_to_user() {
         if !crate::preempt::need_resched() {
             // Every caller's next unmask is the return to user mode, or a
             // kernel thread's first.
-            #[cfg(feature = "mask-windows")]
-            crate::windows::irqs_unmasking();
             return;
         }
         assert!(!in_schedule_self(), "exit-to-user inside a scheduler pass");

m04-device-leaving-unhooked.patch

diff --git a/kernel/src/arch/x86_64/idt/device_irq.rs b/kernel/src/arch/x86_64/idt/device_irq.rs
index d3b355e10..6783da8a6 100644
--- a/kernel/src/arch/x86_64/idt/device_irq.rs
+++ b/kernel/src/arch/x86_64/idt/device_irq.rs
@@ -35,7 +35,7 @@ macro_rules! device_irq_entry {
                 #[cfg(feature = "mask-windows")]
                 "mov rdi, [rbp + 88]",
                 #[cfg(feature = "mask-windows")]
-                "call {windows_leaving}",
+                "# {windows_leaving}",
                 "mov rsp, rbp",
                 "lock sub dword ptr gs:[{preempt_count}], 1",
                 "test dword ptr [rsp + 88], 3", // CS = 10 GPRs + RIP above

m05-switch-unguarded.patch

diff --git a/kernel/src/arch/x86_64/hw.rs b/kernel/src/arch/x86_64/hw.rs
index a296670b3..bd2b636da 100644
--- a/kernel/src/arch/x86_64/hw.rs
+++ b/kernel/src/arch/x86_64/hw.rs
@@ -286,8 +286,6 @@ impl Hw for KernelHw {
         let restore = token.restore_ptr();
         // Every context is saved masked, so `context_switch`'s `popfq` never
         // moves `IF`; the resumed context's own guard puts back what it had.
-        #[cfg(feature = "mask-windows")]
-        let _masked = crate::arch::IrqGuard::close();
         // SAFETY: `save`/`restore` are live Box-backed contexts from `SchedPass::finish`, freed only by a later pass; `incoming.thread_pointer` is this kernel's own canonical value for the thread being installed.
         unsafe {
             (*save).thread_pointer = cpu::read_fs_base();

m06-preempt-raise-unhooked.patch

diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs
index 34084fa58..d0ef5729b 100644
--- a/kernel/src/arch/x86_64/percpu.rs
+++ b/kernel/src/arch/x86_64/percpu.rs
@@ -782,8 +782,6 @@ pub fn set_preempt_count(value: u32) {
 #[inline]
 pub fn preempt_count_up() {
     gs::lock_inc_u32::<OFF_PREEMPT_COUNT>();
-    #[cfg(feature = "mask-windows")]
-    crate::windows::preempt_raised();
 }
 
 /// One decrement, atomic against an interrupt on this CPU.

m09-staged-unmarked.patch

diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -139,6 +139,5 @@
         !cpu::interrupts_enabled() && crate::preempt::count() > 0,
         "windows-staged: staged with interrupts open or the preempt count at zero, where no window is open"
     );
-    on(Windows::mark);
     crate::log!("windows: staged cpu{} open", percpu::cpu_id());
 }

m10-exception-flag-ignored.patch

diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs
index e6016116e..0e2de580a 100644
--- a/kernel/src/arch/x86_64/idt/mod.rs
+++ b/kernel/src/arch/x86_64/idt/mod.rs
@@ -390,7 +390,7 @@ extern "sysv64" fn trap_dispatch(frame: *mut TrapFrame) {
 /// count by one.
 #[cfg(feature = "mask-windows")]
 fn windows_entered(frame: &TrapFrame) {
-    if frame_interrupts_enabled(frame.rflags) {
+    if frame_interrupts_enabled(frame.rflags) || frame.rflags != 0 {
         crate::windows::irqs_masked();
     } else {
         crate::windows::irqs_found_masked();

m11-aarch64-svc-unhooked.patch

diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index fe849db4e..e7e0d6917 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -103,8 +103,6 @@ extern "C" fn dispatch(frame: &mut Frame, entry: u64) {
             crate::windows::irqs_unmasking();
         }
         EL0_SYNC => {
-            #[cfg(feature = "mask-windows")]
-            crate::windows::irqs_masked();
             el0_sync(frame);
             crate::scheduler::exit_to_user();
         }

h02-pass-unsplit.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..6de5f7fa6 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -187,9 +187,7 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        let now = now();
-        self.close_preempt(now)?;
-        self.preempt_off.store(now, Ordering::Relaxed);
+        let _ = now;
         Ok(())
     }
 

h03-halt-counted.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..a5fddac5e 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -198,7 +198,8 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        self.close_preempt(now())
+        let _ = now;
+        Ok(())
     }
 
     /// The halt [`Windows::halting`] began has ended, still inside its pass.
@@ -206,7 +207,8 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        self.open_preempt(now)
+        let _ = now;
+        Ok(())
     }
 
     fn open_preempt(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {

h04-crossing-ignored.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..c43fe98c3 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -154,7 +154,7 @@ impl Windows {
 
     /// The preempt count has just been raised by one, to `count`.
     pub fn raised(&self, count: u32, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
-        if count != 1 || !self.tracked() {
+        if count == 0 || !self.tracked() {
             return Ok(());
         }
         self.open_preempt(now)

h05-last-not-longest.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..3fe4d59b8 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -101,9 +101,7 @@ impl Windows {
 
     fn record(longest: &AtomicU64, since: u64, now: u64) {
         let span = now.saturating_sub(since);
-        if span > longest.load(Ordering::Relaxed) {
-            longest.fetch_max(span, Ordering::Relaxed);
-        }
+        longest.store(span, Ordering::Relaxed);
     }
 
     /// The CPU joins the scheduler with interrupts masked and its preempt

h06-unmask-unchecked.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..7e74039fc 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -143,7 +143,7 @@ impl Windows {
     pub fn unmasking(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
         match self.irqs_off.load(Ordering::Relaxed) {
             UNTRACKED => Ok(()),
-            0 => Err(Unseen::AMask),
+            0 => Ok(()),
             since => {
                 Self::record(&self.irqs_longest, since, now());
                 self.irqs_off.store(0, Ordering::Relaxed);

h07-mark-never-carried.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -249,10 +249,7 @@
     /// A window of the kind `marked` names has closed: a mark it carried is
     /// left for the next [`Windows::take_marked`] as `closed`.
     fn carry(&self, marked: u32, closed: u32) {
-        if self.marks.load(Ordering::Relaxed) & marked != 0 {
-            self.marks.fetch_or(closed, Ordering::Relaxed);
-            self.marks.fetch_and(!marked, Ordering::Relaxed);
-        }
+        let _ = (marked, closed);
     }
 
     /// Mark the window of each kind open now, where the caller knows both are.

h08-marked-answered-twice.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -281,7 +281,7 @@
     /// Whether a marked interrupts-off and a marked preemption-off window
     /// closed since the last such take; the next starts from none.
     pub fn take_marked(&self) -> (bool, bool) {
-        let closed = self.marks.fetch_and(!(IRQS_MARK_CLOSED | PREEMPT_MARK_CLOSED), Ordering::Relaxed);
+        let closed = self.marks.load(Ordering::Relaxed);
         (closed & IRQS_MARK_CLOSED != 0, closed & PREEMPT_MARK_CLOSED != 0)
     }
 }

h09-mark-unchecked.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -257,13 +257,8 @@
 
     /// Mark the window of each kind open now, where the caller knows both are.
     pub fn mark(&self) -> Result<(), Unseen> {
-        match self.irqs_off.load(Ordering::Relaxed) {
-            UNTRACKED => return Ok(()),
-            0 => return Err(Unseen::AMask),
-            _ => {}
-        }
-        if self.preempt_off.load(Ordering::Relaxed) == 0 {
-            return Err(Unseen::ARaise);
+        if !self.tracked() {
+            return Ok(());
         }
         self.marks.fetch_or(IRQS_MARKED | PREEMPT_MARKED, Ordering::Relaxed);
         Ok(())

v01-verdict-count-free.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -221,7 +221,7 @@
         }
     }
     if let Some((cpu, irqs, preempt)) = stage {
-        if (irqs, preempt) != (1, 1) {
+        if irqs == 0 || preempt == 0 {
             return Err(format!(
                 "cpu{cpu}'s reports after its stage say its marked interrupts-off window closed \
                  {irqs} time(s) and its marked preemption-off window {preempt}; each closes once"

b01-refusal-exact-equality.patch

diff --git a/src/build.rs b/src/build.rs
index 1380ca228..071cc2c9c 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1339,7 +1339,7 @@ pub fn carries_the_actuators<S: AsRef<str>>(features: &[S]) -> bool {
 /// Refuse a boot arming a parameter its kernel cannot answer: one outside the
 /// kernel's `own` and the valued ones needs [`carries_the_actuators`].
 fn refuse_an_unarmable_boot(own: &[String], features: &[String], params: &[String]) -> Result<(), String> {
-    if params.iter().all(|p| own.contains(p) || is_valued_param(p)) || carries_the_actuators(features) {
+    if params.iter().all(|p| own.contains(p) || is_valued_param(p)) || features.iter().map(String::as_str).eq(TEST_KERNEL.iter().copied()) {
         return Ok(());
     }
     Err(format!("a boot asking for {params:?} must boot a kernel carrying {TEST_KERNEL:?}, not {features:?}"))

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 readings for the step's exit, run and judged by the orchestrator at 72f16e39a: eight boots of windowscase, base and head interleaved. The four staged images were each booted twice (boots 5 to 8 are the images of boots 1 to 4 again, into readback directories of their own). Each boot: cargo run --bin toyos-metal -- --image … --readback … --fat32-check, toyos-metal EXIT=0, test_rs_ring_park_herd exit=0. Each readback judged from the clean, unpatched worktree with cargo test --test toyos-build -- --metal --metal-readback <dir> mask_windows: EXIT=0 and PASS mask_windows on all eight, the record file restored between judgings so each is the judge's first sight of the row. Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71).

Base is this head with step 1 reverted by the posted patch; head is this head. Numbers are nanoseconds, as the judge's [windows] line prints them.

boot arm image boot irqs_off boot preempt_off herd irqs_off herd preempt_off CPUs reporting over 9 ms irqs-off
1-base base 0b9d4e753e11b2ee… 6551791 6551707 6551791 6551707 0 of 8
2-head head 7d313d04f0ba372b… 6540754 6540671 6540754 6540671 0 of 8
3-base base cd44d17699611470… 6549549 6549484 6549549 6549484 0 of 8
4-head head 38d91fb80a801a8b… 9651846 9643939 9651846 9643939 8 of 8
5-base base 0b9d4e753e11b2ee… 6540326 6540262 6540326 6540262 0 of 8
6-head head 7d313d04f0ba372b… 6589053 6588992 6589053 6588992 0 of 8
7-base base cd44d17699611470… 9260193 9260124 9260193 9260124 1 of 8
8-head head 38d91fb80a801a8b… 6587342 6587281 6587342 6587281 0 of 8

What the eight say, read off the table and the per-CPU windows: lines:

  • Six boots, three of each arm, read between 6,540,326 and 6,589,053 ns. One boot of each arm reads over 9 ms: base 9,260,193, head 9,651,846.
  • The two long boots are not alike. In 4-head all eight CPUs report between 9,603,617 and 9,651,846 ns of interrupts off. In 7-base only cpu7 does, at 9,260,193, with cpu0 at 6,590,095 and the other six between 920,455 and 1,404,001. In the six short boots the longest is always cpu0, between 6,540,326 and 6,589,053 (in 2-head the other seven read between 257,884 and 3,733,931).
  • By the rule the round's report gives (met when head's larger sample is no more than base's larger; not met when head's smaller exceeds base's larger; otherwise undecided), four samples an arm leave it undecided: head's larger, 9,651,846, is above base's larger, 9,260,193, and head's smaller, 6,540,754, is below it.
  • Each judging wrote seven rows into tests/metal/lenovo-20w0003amz.toml; the eight diffs are kept as judge-<boot>.record-rows.patch beside the readbacks and none is committed.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #649 at 72f16e39a against origin/main a31eec595. First round: no earlier BLOCKER. Read, not run; the worktree is as found (git status --porcelain --ignore-submodules=none empty before and after). My logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-review/ (t14-lines.txt, revert-check.py, revert-check.out).

Net lines, git diff --shortstat origin/main...72f16e39a: 39 files, +1320 −34. Production: kernel/ +375 −3, toyos-sched +291, src/ +46 −10. Tests: tests/ +432 −12, the record's 136, src/build.rs's 12. Issues +28 −9.

Rulings on the brief's four points

1. The re-based comparison. It answers what the old wording asked. 53b1ef7d3 and 815e0899f were adjacent too, so both wordings compare step 1 with its absence, and a revert on the tree the other arm is measured on is root CLAUDE.md's negative control as written. The re-basing does not rescue the exit: it fails on its load and its statistic, not on its baseline (3).

2. The base arm. Sound as an image source; nothing more is owed for it. Measured here outside the worktree (git archive of kernel and toyos-sched/src at 72f16e39a, 815e0899f and 53b1ef7d3; the patch applied to the first with patch -p1, exit 0):

3. The T14 readings.

What they show:

  • The hooks hold on the machine: eight boots, eight CPUs, one or two NMIs on cpu0 in each, no refusal.
  • cpu0 holds interrupts and preemption off together for 6,540,326 to 6,590,095 ns in the seven boots where it can be seen, on both arms (base 6.540 to 6.590 ms, head 6.541 to 6.589 ms). The two windows differ by 61 to 84 ns, so one section holds both.
  • Two boots in eight carry a window of 9.26 ms or more, one on each arm.

What they do not show is anything about step 1:

  • The reading is not the herd's. Each log holds one report (8 windows: cpu lines), printed at the herd's exit, so it spans from each CPU's start_here. "Boot" and "herd" are the same two numbers in all eight judgings. In every log cpu0 joins at 1.155 or 1.156 and prints nothing until init's first line at 1.161 or 1.162; the herd starts at 1.194 or 1.195. Under TCG the probe's first report, before the herd, already has cpu0 at 39.6 ms (r3-base-x86-probe-metalkernel.log:295).
  • No handler step 1 changed ran. All 64 census lines read userdev=0 sound=0 dmafault=0 hda=0.
  • The herd's walks are masked on both arms. They start in SYS_INBOX_SUBMIT, and kernel/src/arch/x86_64/syscall.rs:32 keeps IF masked for the whole syscall. issues/kernel/syscall-preemption-is-incidental.md has said so since 2026-07-31. "Before Stage 6 step 1: a device's handler posts its watch; irq_ring's UserDev and Audio go #634 every one of these walks ran with interrupts open" is false of the load the exit names.

So the exit is not undecided: this measurement cannot decide it, and four more pairs would not.

Maximum-of-samples is the wrong statistic. With a long event in one boot of four on either arm, "the larger of H is no more than the larger of B" is a coin toss when the arms do not differ, and "not met" needs the smaller of H above the larger of B, which no regression smaller than the 2.7 ms between the short boots and the long ones can produce. Compare the median of at least five interleaved boots an arm, each read from the load's own report with a report taken as the load starts, against a tolerance stated before measuring (the reverted arm's own spread). Report the tail beside it; it is a defect of its own.

What the exits should say:

  • Step 2: the instrument reads back a window of known length on the T14 and its rows are in the machine's record; and the medians above are no longer with step 1 than with it reverted, under a load in which a handler step 1 changed posts into a watch that reaches a ring with parked threads (the defect issue's second bullet). Until then the track says step 2 is built and its exit open.
  • Steps 3 to 5 keep "against the readings step 2 records" once those are in the tree, under the same rule.
  • The defect issue: the window under the herd does not grow with the threads parked, read at two sizes on one kernel. The defect stands on both arms, so "no longer than with step 1 reverted" would close it while it is still true. The sibling's roster poll is itself a masked walk over every thread (45=3619 to 4111 a run, syscall_wall 768 to 847 ms of a 0.9 s run), so that reading has to separate it.

What the long events and the standing window are owed: an issue each, kind: defect, in this pull request (NOTE 1).

4. The round's open points.

  • m10: BLOCKER 4.
  • No window of known length: BLOCKER 3.
  • IA32_FMASK: confirmed by reading, by the tracker and by cpu0's two windows 61 to 84 ns apart; its consequence is ruling 3 and BLOCKER 6.
  • The x86-64 guest test left out of the merge: accepted. It is the tier order, and main cut irq_census_conservation for its row the same way. The probe's evidence is accepted for the mutations whose red is the kernel's own panic. No nightly or CI job executes an x86-64 hook; the next T14 run of the row is the first to see one broken.

BLOCKER

  1. PR body, ## The T14 request, ## Measured, and the sentences "not yet made" (twice), "none yet" and "The T14 has taken none of this" — the body at this head carries no reading from the machine the change targets and says none exists; the eight are in comment 5957994650 — a claim stands only on a measurement in the body, with its command, exit code and log.
  2. tests/toyos.rs:652-655, :2272-2306; kernel/src/process.rs:1068, :1073 — the row's "herd" pair is its "boot" pair in every boot it can produce: one job, so the herd's exit prints the boot's only report; and the census is printed before the teardown begins, so the exit's own window closes after the last report in the log and is never read — suspected green: tests/toyos.rs:2298 herd.values() → longest.values() changes no verdict and no number on any of the eight readbacks; it must turn mask_windows red, which takes a report before the herd (the probe had pwd), one after its exit pass, and a judge that refuses a herd report with none before it.
  3. kernel/src/windows.rs:114-115, kernel/src/hw.rs:82, :85; tests/metal/lenovo-20w0003amz.toml — no test on any machine can fail on a wrong duration or on either of the two rules the preemption-off number rests on, on high-risk code whose every later reading the track compares against — suspected green in every registered test: (a) nanos_of_ticks(irqs) / 2 and the same for preempt; (b) halting() and woken() deleted, which the record refuses nothing of and which makes the number include the idle halt. (a) must turn red a T14 row that holds both windows for a span read off cpu::counter and requires the staged CPU's next report to read no less; (b) must turn mask_windows red against the record's ceiling on an image staged for the T14, which needs the head arm's rows committed (they sit in scratch as judge-*.record-rows.patch).
  4. kernel/src/arch/x86_64/idt/mod.rs:385-392, toyos-sched/src/windows.rs:151, kernel/src/windows.rs:67 — the arm that reads an exception frame's IF is held by no registered test (m10, exit 0 by the implementer's own run), and the body claims it as half of the independent oracle — register the test, or delete the arm: every Ring 0 exception is fatal (exceptions.rs:431, :450), so a Ring 0 entry can stop this CPU's record as refuse does, found_masked goes, and a dying kernel's fault is never displaced by the record's refusal.
  5. toyos-sched/src/windows.rs:37-40, :53, :251-287; kernel/src/windows.rs:117-144; kernel/src/syscall/proc.rs:29-35; kernel/src/actuator.rs:73-75; src/build.rs:1332-1354, :2475-2485; src/clippy.rs:50-52, :103-107, :138-142; tests/common/irqcensus.rs:135-161 and the stage's half of windows; tests/common/qemu.rs:1077-1093; eight of mask_windows_verdict's twelve captures — the stage tests the stage: carry shares no state with record, take or the report's conversion, so m09, h07 to h09 and v01 mutate the mark and nothing of the instrument; it costs a third kernel build a suite run, two clippy shapes, and one load of marks on every close in the mask-windows-alone kernel the T14 measures, which is staging state touched outside its actuator:: guard and outside an actuator-only item — delete it and boot virt_mask_windows on mask-windows alone, the metal row's spelling, or make it BLOCKER 3's known-span control on metal.
  6. issues/kernel/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md:34-35, :38-41; issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md:151-153, :171-176, :188-189 — two issue files this branch changed hold claims the tree refutes: the walks did not run with interrupts open before Stage 6 step 1: a device's handler posts its watch; irq_ring's UserDev and Audio go #634; the reworded exit names a load under which the arms cannot differ; and "the readings step 2 records" are recorded nowhere in the tree — against issues/README.md, where a sentence the tree has refuted is corrected in the commit that finds it; reword as ruling 3 says.
  7. kernel/src/windows.rs:110 — let Some(of) = CPUS.get(cpu as usize) else { return }; — cpu is below cpu_count(), which driver::init asserts is at most MAX_CPUS, so the arm is reached only by a broken invariant and then prints nothing; index it.

NOTE

  1. issues/kernel/ — two defects this round measured are filed nowhere. (a) cpu0's standing window: 6.54 to 6.59 ms on both arms in the seven boots where a longer event does not cover it, both kinds held together, cpu0 silent for the 6 ms before init's first line in all eight. (b) The long events: 4-head, all eight CPUs at 9,603,617 to 9,651,846 ns with tlb: … wait=125911us max=9575us against at most 49119us and 1421us in the other seven; 7-base, cpu7 alone at 9,260,193. By reading, not measured: every CPU waiting out one CPU's masked window inside a shootdown would read like 4-head. Each with the readback's path, the machine and BIOS, and an exit that names the opener and removes or bounds the window. The report cannot name an opener today, so each issue says what will.
  2. tests/toyos.rs:2272 — windows_on_metal has no host test; mask_windows_verdict holds the QEMU judge only, and which report is the herd's is the part that was wrong — captures for one report, a herd report with none before it, and a bracketed one.
  3. src/build.rs:1350, :1354; src/clippy.rs:52 — one build spelled three times; one declaration, if anything of it survives BLOCKER 5.

REMOVE

  1. PR body, ## The merge: "Main was 469 commits ahead. Six files conflicted; the merge commit's message accounts for each."
  2. PR body, ## Gates: "The body at 737803d48 recorded virt_mask_windows red twice at 772bac7da; the mark answers that, and these are its first two measurements."
  3. tests/checks.rs:453 — the eprintln! restating how many captures the test holds.

SEND BACK

Japabu and others added 4 commits October 2, 2026 20:13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…ead back, the load's own report, and no stage

Answers the review of #649 at 72f16e3 (comment 5958368996).

The stage that tested the stage goes (BLOCKER 5). `Windows::mark`,
`take_marked`, `carry` and the mark word shared no state with `record`,
`take` or the report's conversion, so a mutation of the mark mutated
nothing of the instrument. With it go the `windows-staged` actuator, its
arm in `SYS_EXIT`, `carries_the_actuators`, `refuse_an_unarmable_boot`
and its host test, both clippy shapes of the three-feature build, the
harness's widened `kernel_of`, and the staged half of the judge.
`build::MASK_WINDOWS_KERNEL` is now `mask-windows` alone, declared once
and read by the suite's build list, the QEMU test and the metal arm
(NOTE 3).

The known-span control is written fresh and not converted from the
stage. Converting kept the actuator, and an actuator on the T14 needs
the three-feature kernel: a second metal boot, the build predicate, its
test and the clippy shapes all stay. Fresh, it is `windows::hold_once`:
the boot's first `SYS_EXIT` on a `mask-windows` kernel masks interrupts,
raises the preempt count, spins `toyos_sched::windows::HELD_NS` (50 ms)
by `cpu::counter` and prints `windows: held cpuK ns=<span>` from the
same two counter reads. No parameter arms it and no harness code knows
about it; the windows it holds reach a report through `record`, `take`
and `nanos_of_ticks` like every other.

The T14 row reads its durations through one pure function,
`irqcensus::windows_under` (BLOCKER 2, BLOCKER 3, NOTE 2):
- The load's reading is the report its exit printed and the next one,
  which carries the teardown the first cannot. It is refused unless a
  report before them emptied every CPU's record. At 72f16e3 the row
  ran one job, so the herd's report was the boot's only one and carried
  every window since each CPU joined.
- The holding CPU's reports between its hold and the load's start read
  back a window of each kind no shorter than the span the held line
  states, and that span is no shorter than `HELD_NS`.
The row's jobs are `idle_span`, `pwd`, the herd and `echo`. `idle_span`
is new: half a second asleep, and its exit is the boot's first, so the
CPU that holds has just come out of a halt the record must not have
counted. The rows recorded are the held pair and the herd's pair; the
boot's longest goes, since the hold would be it.
`mask_windows_verdict` holds both judges to captures: the four-report
shape read to its numbers, and eleven refusals of `windows_under`.

The arm that read an exception frame's `IF` goes (BLOCKER 4). Every
Ring 0 exception ends the machine, so `trap_dispatch`'s hook stops that
CPU's record on a Ring 0 frame, as `refuse` does, and a dying kernel's
fault is never displaced by a refusal. `Windows::found_masked` goes with
it. The device stubs' leaving hook reads the ring through
`toyos_userbound::Ring`, which the module says is the only reader of the
RPL bits.

`virt_mask_windows` boots `tests/virtsmpcase` on eight CPUs.
`tests/virtjobcase` runs `debug_refused`, which needs `SYS_DEBUG`, and
the instrument's kernel no longer carries the actuators.

`log_cpu` indexes its CPU's record (BLOCKER 7): `cpu` is below
`cpu_count()`, which is at most `MAX_CPUS`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…found say what was read

The review of #649 at 72f16e3 ruled that its eight T14 readings cannot
decide whether stage 6 step 1 lengthened a window (BLOCKER 6, NOTE 1):
each boot printed one report, so the herd's reading was the boot's; no
handler step 1 changed took an interrupt; and the herd's walks start in
`SYS_INBOX_SUBMIT`, which runs masked with step 1 and without it.

The track's step 2 says so: built, exit open, and the exit is the
instrument reading back a known window on the T14 with its rows
recorded, then medians of at least five interleaved boots an arm from
the load's own report, against a tolerance stated first, under a load
that reaches a handler step 1 changed. Steps 3 to 5 measure against the
readings that exit leaves in the tree.

The herd's defect no longer says its walks ran with interrupts open
before #634, which `issues/kernel/syscall-preemption-is-incidental.md`
has refuted since July. Its exit is the window not growing with the
threads parked, read at two sizes on one kernel: the defect stands on
both arms, so a comparison of arms would close it while it is true.

Two defects those eight boots measured are filed with their numbers:
cpu0's 6.5 ms window in every boot, and the windows of 9 ms or more in
some.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
`hold_once` masked interrupts and raised the preempt count around its
spin. Its one caller is `SYS_EXIT`, whose entry did both, so deleting
the two lines changed nothing a boot could show, on either
architecture. What says the hold is inside both windows is the T14's
judge: a hold outside one reads back shorter than it was held.

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

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

The scratch probes and the mutation patches of the round that answers review 5958368996, each git apply --check EXIT=0 at 8b73eba69. Their verdicts, with exit codes, are in the pull request body under "The checks". None of this is in the tree. The patches of comment 5957701335 are 72f16e39a's and no longer describe this branch: m09, m10, h07 to h09, v01 and b01 mutated code this round deleted.

The runner, run.sh <summary> <probe|-> <mutations…> -- <command…>: the unmutated run first, then each mutation checked, applied, run, reversed, and the tree counted clean.

#!/bin/sh
# Each mutation over the committed head: checked, applied, run by the command
# named, reversed in the same step, and the tree shown clean after.
#   run.sh <summary-name> <probe|-> <mutations...> -- <command...>
# `probe` applies x86-probe.patch (the scratch tests, not in the tree) around
# the whole batch; the first run of the batch is the unmutated one.
set -u
W=/Users/jan/Dev/jan/toyos-sk6b
D=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r5
name=$1; probe=$2; shift 2
muts=""
while [ "$1" != "--" ]; do muts="$muts $1"; shift; done
shift
S=$D/logs/mut-$name-summary.txt
cd "$W" || exit 1
: > "$S"
echo "head $(git rev-parse HEAD), tree lines before: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')" >> "$S"
echo "command: $*" >> "$S"
if [ "$probe" = probe ]; then
  git apply --check "$D/x86-probe.patch"; echo "probe check=$?" >> "$S"
  git apply "$D/x86-probe.patch" || exit 1
fi
"$@" > "$D/logs/mut-$name-unmutated.log" 2>&1; echo "unmutated: EXIT=$?" >> "$S"
for m in $muts; do
  p=$(ls "$D"/mutations/"$m"-*.patch)
  git apply --check "$p"; c=$?
  git apply "$p"; a=$?
  "$@" > "$D/logs/mut-$name-$m.log" 2>&1; t=$?
  git apply -R "$p"; r=$?
  why=$(grep -m1 -E 'mask-windows: cpu|FAIL |panicked at|^error' "$D/logs/mut-$name-$m.log" | cut -c1-400)
  echo "$(basename "$p"): check=$c apply=$a test EXIT=$t reverse=$r | $why" >> "$S"
done
if [ "$probe" = probe ]; then
  git apply -R "$D/x86-probe.patch"; echo "probe reversed: $?" >> "$S"
fi
echo "tree lines after: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')" >> "$S"
echo DONE >> "$S"

x86-probe.patch: two scratch machine tests. x86_windows_probe boots the T14 row's kernel (mask-windows alone) and its job list on eight CPUs under TCG and reads it with the row's own judge, irqcensus::windows_under; its durations are a preview of the row and no verdict. x86_windows_fault_probe takes one Ring 0 read fault through SYS_DEBUG's NULL_READ on boot-actuators,mask-windows,test-actuators, a build the patch declares for itself.

diff --git a/src/build.rs b/src/build.rs
index daf0f1fbf..4be899093 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1330,8 +1330,8 @@ fn declared_kernel_features(root: &Path) -> Vec<String> {
 pub const TEST_KERNEL: &[&str] = &["boot-actuators", "test-actuators"];
 
 /// Kernel builds the ordinary test suite is allowed to make.
-pub const TEST_SUITE_KERNEL_BUILDS: [&str; 3] =
-    ["", "boot-actuators,test-actuators", MASK_WINDOWS_KERNEL[0]];
+pub const TEST_SUITE_KERNEL_BUILDS: [&str; 4] =
+    ["", "boot-actuators,test-actuators", MASK_WINDOWS_KERNEL[0], "boot-actuators,mask-windows,test-actuators"];
 
 /// The shipping kernel with the windows' instrument and nothing else, for
 /// [`SCHED_CHECK_KERNEL`]'s reason: one spelling, so one build.
diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs
index 6e2d07f0f..b528bca3e 100644
--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -158,7 +158,7 @@ pub fn boot_census() -> (u32, u32, Vec<String>) {
 /// entry and masking primitive, where a parameter would be a branch on the path
 /// they measure. Interactive debug mode is separate: it builds
 /// [`toyos_build::build::DEBUG_KERNEL_BUILD`] and returns before the suite.
-pub const DECLARED_KERNEL_BUILDS: [&str; 3] =
+pub const DECLARED_KERNEL_BUILDS: [&str; 4] =
     toyos_build::build::TEST_SUITE_KERNEL_BUILDS;
 
 /// How many guests the run may have up at once.
diff --git a/tests/toyos-rust-tests/src/bin/ring0_read.rs b/tests/toyos-rust-tests/src/bin/ring0_read.rs
new file mode 100644
index 000000000..7365ad2a5
--- /dev/null
+++ b/tests/toyos-rust-tests/src/bin/ring0_read.rs
@@ -0,0 +1,8 @@
+//! Scratch: one Ring 0 read of an unmapped address, inside a syscall and so
+//! with interrupts masked, which ends the machine.
+
+fn main() {
+    println!("ring0_read: asking for a Ring 0 read of 0x10");
+    toyos_abi::syscall::debug_with(toyos_abi::syscall::debug_action::NULL_READ, 0x10);
+    println!("ring0_read: the read returned");
+}
diff --git a/tests/toyos.rs b/tests/toyos.rs
index ac2d7441c..19cf32e7d 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -77,6 +77,7 @@ const RUST_SKIP: &[&str] = &[
     // It asserts nothing: it is half a second of an idle machine, which the
     // same row reads the windows across.
     "idle_span",
+    "ring0_read",
     // The C corpus's comparator: a helper reached through one symlink per case,
     // never a test of its own. `shared_metal` stages every name on this list.
     "ccheck",
@@ -177,6 +178,8 @@ const MACHINE_TESTS: &[&str] = &[
     // The nested-NMI report is a raw write to the 16550, which the T14 does not
     // have.
     "nested_nmi_is_loud",
+    "x86_windows_probe",
+    "x86_windows_fault_probe",
 ];
 
 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2095,6 +2098,73 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
     match name {
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+        "x86_windows_probe" => {
+            // Scratch: the T14 row's kernel and job list under TCG, read by the
+            // machine's own judge, whose durations here are a preview and no verdict.
+            const CPUS: u32 = 8;
+            let root = compile::repo_root();
+            let bin = |name: &str| {
+                (
+                    format!("bin/test_rs_{name}"),
+                    qemu::build_toyos_bin(toyos_build::arch::Arch::X86_64, &root.join("tests/toyos-rust-tests"), name),
+                )
+            };
+            let qemu = QemuInstance::boot_with_options(
+                &root.join("tests/windowsprobecase"),
+                &[],
+                &[],
+                BootOptions {
+                    smp: CPUS,
+                    kernel_features: toyos_build::build::MASK_WINDOWS_KERNEL,
+                    extra_root_files: vec![bin("ring_park_herd"), bin("idle_span")],
+                    ..Default::default()
+                },
+            );
+            let serial = judge_virt_job(qemu, "echo", "ring_park_herd: 256 threads parked")?;
+            for line in serial.lines().filter(|l| l.contains("windows: ") || l.contains("exit: ") && l.contains(" pid=")) {
+                eprintln!("  [probe] {line}");
+            }
+            mask_windows(&serial, CPUS)?;
+            let read = common::irqcensus::windows_under(&serial, CPUS, &windows_load_exited())?;
+            eprintln!("  [probe] {read:?}");
+            Ok(())
+        }
+        "x86_windows_fault_probe" => {
+            // Scratch: a Ring 0 fault taken with interrupts masked, on the
+            // instrumented kernel: the report owed is the fault's, not the
+            // record's refusal.
+            let root = compile::repo_root();
+            let read = qemu::build_toyos_bin(
+                toyos_build::arch::Arch::X86_64,
+                &root.join("tests/toyos-rust-tests"),
+                "ring0_read",
+            );
+            let qemu = QemuInstance::boot_with_options(
+                &root.join("tests/windowsfaultcase"),
+                &[],
+                &[],
+                BootOptions {
+                    smp: 4,
+                    kernel_features: &["boot-actuators", "mask-windows", "test-actuators"],
+                    profile: qemu::Profile::Metal,
+                    ready_marker: "panic: rebooting in",
+                    extra_root_files: vec![("bin/test_rs_ring0_read".to_string(), read)],
+                    ..Default::default()
+                },
+            );
+            let serial = serial::Serial::boot(&qemu);
+            let text = serial.text();
+            if let Some(refused) = text.lines().find(|l| l.contains("mask-windows:")) {
+                return Err(format!("the record refused where a Ring 0 fault was owed its own report: {refused}\n{text}"));
+            }
+            for owed in ["SYS_DEBUG: a Ring 0 read of 0x10", "FAULT rip="] {
+                if !text.contains(owed) {
+                    return Err(format!("{owed:?} not on the console\n{text}"));
+                }
+            }
+            eprintln!("  [windows] a Ring 0 read fault with interrupts masked reported itself");
+            Ok(())
+        }
         other => Err(format!("unknown machine test {other}")),
     }
 }
diff --git a/tests/windowsfaultcase/system.toml b/tests/windowsfaultcase/system.toml
new file mode 100644
index 000000000..e05b1267c
--- /dev/null
+++ b/tests/windowsfaultcase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+args = ["--bound-ms=900000", "pwd", "test_rs_ring0_read"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]
diff --git a/tests/windowsprobecase/system.toml b/tests/windowsprobecase/system.toml
new file mode 100644
index 000000000..518bc8396
--- /dev/null
+++ b/tests/windowsprobecase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+args = ["--bound-ms=900000", "test_rs_idle_span", "pwd", "test_rs_ring_park_herd", "echo"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]

a-report-halved.patch

diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..c33ba9c23 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -116,8 +116,8 @@ pub fn log_cpu(cpu: u32) {
     let (irqs, preempt) = CPUS[cpu as usize].take();
     crate::log!(
         "windows: cpu{cpu} irqs_off_ns={} preempt_off_ns={}",
-        crate::clock::nanos_of_ticks(irqs),
-        crate::clock::nanos_of_ticks(preempt),
+        crate::clock::nanos_of_ticks(irqs) / 2,
+        crate::clock::nanos_of_ticks(preempt) / 2,
     );
 }
 

b-idle-halt-counted.patch

diff --git a/kernel/src/hw.rs b/kernel/src/hw.rs
index c40c8af02..049086500 100644
--- a/kernel/src/hw.rs
+++ b/kernel/src/hw.rs
@@ -78,11 +78,7 @@ impl Machine for KernelHw {
         if crate::actuator::diag_tick() {
             irqchip::arm_within(DIAG_TICK_NS);
         }
-        #[cfg(feature = "mask-windows")]
-        crate::windows::halting();
         self.halt();
-        #[cfg(feature = "mask-windows")]
-        crate::windows::woken();
         // **A CPU that is executing has a one-shot armed, and this is where
         // that becomes true again.** `TimerPlan::Stop` left this one at zero
         // before the halt above and only a pass reaching `apply_timer` arms
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..1e270fa09 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -101,16 +101,6 @@ pub fn scheduled() {
     on(|w| w.scheduled(cpu::counter));
 }
 
-/// This CPU halts inside a pass.
-pub fn halting() {
-    on(|w| w.halting(cpu::counter));
-}
-
-/// The halt has ended, still inside its pass.
-pub fn woken() {
-    on(|w| w.woken(cpu::counter));
-}
-
 /// `cpu`'s line, taking its longest windows so the next report starts from none.
 pub fn log_cpu(cpu: u32) {
     let (irqs, preempt) = CPUS[cpu as usize].take();

f01-ring0-fault-not-stopped.patch

diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs
index c7f63b304..c6a50d879 100644
--- a/kernel/src/arch/x86_64/idt/mod.rs
+++ b/kernel/src/arch/x86_64/idt/mod.rs
@@ -386,9 +386,7 @@ extern "sysv64" fn trap_dispatch(frame: *mut TrapFrame) {
 /// place of the fault's report.
 #[cfg(feature = "mask-windows")]
 fn windows_entered(frame: &TrapFrame) {
-    if !toyos_userbound::Ring::of_cs(frame.cs).is_user() {
-        crate::windows::stop_here();
-    }
+    let _ = frame;
     crate::windows::irqs_masked();
     crate::windows::preempt_raised();
 }
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..0781e0a45 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -67,15 +67,6 @@ pub fn irqs_masked() {
     on(|w| w.masked(cpu::counter));
 }
 
-/// This CPU took an exception in the kernel, which ends the machine: nothing
-/// more of it is tracked or checked.
-pub fn stop_here() {
-    on(|w| {
-        w.stop();
-        Ok(())
-    });
-}
-
 /// Interrupts are masked and this CPU is about to open them.
 pub fn irqs_unmasking() {
     on(|w| w.unmasking(cpu::counter));

h02-pass-unsplit.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..6de5f7fa6 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -187,9 +187,7 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        let now = now();
-        self.close_preempt(now)?;
-        self.preempt_off.store(now, Ordering::Relaxed);
+        let _ = now;
         Ok(())
     }
 

h03-halt-counted.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..a5fddac5e 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -198,7 +198,8 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        self.close_preempt(now())
+        let _ = now;
+        Ok(())
     }
 
     /// The halt [`Windows::halting`] began has ended, still inside its pass.
@@ -206,7 +207,8 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        self.open_preempt(now)
+        let _ = now;
+        Ok(())
     }
 
     fn open_preempt(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {

h04-crossing-ignored.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..c43fe98c3 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -154,7 +154,7 @@ impl Windows {
 
     /// The preempt count has just been raised by one, to `count`.
     pub fn raised(&self, count: u32, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
-        if count != 1 || !self.tracked() {
+        if count == 0 || !self.tracked() {
             return Ok(());
         }
         self.open_preempt(now)

h05-last-not-longest.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..3fe4d59b8 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -101,9 +101,7 @@ impl Windows {
 
     fn record(longest: &AtomicU64, since: u64, now: u64) {
         let span = now.saturating_sub(since);
-        if span > longest.load(Ordering::Relaxed) {
-            longest.fetch_max(span, Ordering::Relaxed);
-        }
+        longest.store(span, Ordering::Relaxed);
     }
 
     /// The CPU joins the scheduler with interrupts masked and its preempt

h06-unmask-unchecked.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..7e74039fc 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -143,7 +143,7 @@ impl Windows {
     pub fn unmasking(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
         match self.irqs_off.load(Ordering::Relaxed) {
             UNTRACKED => Ok(()),
-            0 => Err(Unseen::AMask),
+            0 => Ok(()),
             since => {
                 Self::record(&self.irqs_longest, since, now());
                 self.irqs_off.store(0, Ordering::Relaxed);

j01-load-read-over-every-report.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..bbfd0689e 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -278,7 +278,8 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
             held.0, held.1
         ));
     }
-    let load = longest(&mut reports[own].1.iter().chain(&after.1));
+    let _ = after;
+    let load = longest(&mut reports.iter().flat_map(|(_, report)| report));
     Ok(Measured { held, load })
 }
 

j02-no-report-before-the-load-accepted.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..7cdba132e 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -242,7 +242,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
         .iter()
         .rposition(|(at, _)| *at < exited)
         .ok_or_else(|| format!("no report before `{load_exited}`: the load's exit printed none"))?;
-    if own == 0 {
+    if false {
         return Err("the load's report is the boot's first: none was taken as the load started, so it \
                     carries every window since each CPU joined"
             .to_string());

j03-the-report-after-the-exit-unread.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..2775de5d0 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -278,7 +278,8 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
             held.0, held.1
         ));
     }
-    let load = longest(&mut reports[own].1.iter().chain(&after.1));
+    let _ = after;
+    let load = longest(&mut reports[own].1.iter());
     Ok(Measured { held, load })
 }
 

j04-one-kind-read-back-is-enough.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..52086de8c 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -271,7 +271,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
         ));
     }
     let held = longest(&mut read_back);
-    if held.0 < ns || held.1 < ns {
+    if held.0 < ns && held.1 < ns {
         return Err(format!(
             "cpu{cpu} held both windows for {ns} ns, and its reports before the load's read back \
              irqs_off_ns={} preempt_off_ns={}: a window was reported shorter than it was held",

j05-the-loads-report-reads-the-hold-back.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..15315f470 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -259,7 +259,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
     if ns < toyos_sched::windows::HELD_NS {
         return Err(format!("cpu{cpu} held its windows for {ns} ns, and the kernel owes {}", toyos_sched::windows::HELD_NS));
     }
-    let mut read_back = reports[..own]
+    let mut read_back = reports[..]
         .iter()
         .filter(|(at, _)| *at > hold_at)
         .filter_map(|(_, report)| report.get(cpu as usize))

m01-syscall-entry-unhooked.patch

diff --git a/kernel/src/arch/x86_64/syscall.rs b/kernel/src/arch/x86_64/syscall.rs
index e7be09402..4e8ba5b0f 100644
--- a/kernel/src/arch/x86_64/syscall.rs
+++ b/kernel/src/arch/x86_64/syscall.rs
@@ -176,7 +176,6 @@ extern "sysv64" fn syscall_handler(num: u64, a1: u64, a2: u64, _: u64, a3: u64,
     // `IA32_FMASK` masked interrupts, and the entry raised the preempt count by one.
     #[cfg(feature = "mask-windows")]
     {
-        crate::windows::irqs_masked();
         crate::windows::preempt_raised();
     }
     #[cfg(feature = "df-witness")]

m02-guard-drop-unhooked.patch

diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs
index d7e401a25..a072656fc 100644
--- a/kernel/src/arch/x86_64/mod.rs
+++ b/kernel/src/arch/x86_64/mod.rs
@@ -82,7 +82,7 @@ impl Drop for IrqGuard {
     fn drop(&mut self) {
         // Only where the restore opens them.
         #[cfg(feature = "mask-windows")]
-        if idt::frame_interrupts_enabled(self.rflags) && !cpu::interrupts_enabled() {
+        if idt::frame_interrupts_enabled(self.rflags) && cpu::interrupts_enabled() {
             crate::windows::irqs_unmasking();
         }
         // SAFETY: the word `close` read out of RFLAGS on this CPU (the guard is

m03-exit-to-user-unhooked.patch

diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs
index 07e9c4ae1..85003c031 100644
--- a/kernel/src/scheduler.rs
+++ b/kernel/src/scheduler.rs
@@ -406,8 +406,6 @@ pub fn exit_to_user() {
         if !crate::preempt::need_resched() {
             // Every caller's next unmask is the return to user mode, or a
             // kernel thread's first.
-            #[cfg(feature = "mask-windows")]
-            crate::windows::irqs_unmasking();
             return;
         }
         assert!(!in_schedule_self(), "exit-to-user inside a scheduler pass");

m04-device-leaving-unhooked.patch

diff --git a/kernel/src/arch/x86_64/idt/device_irq.rs b/kernel/src/arch/x86_64/idt/device_irq.rs
index d3b355e10..6783da8a6 100644
--- a/kernel/src/arch/x86_64/idt/device_irq.rs
+++ b/kernel/src/arch/x86_64/idt/device_irq.rs
@@ -35,7 +35,7 @@ macro_rules! device_irq_entry {
                 #[cfg(feature = "mask-windows")]
                 "mov rdi, [rbp + 88]",
                 #[cfg(feature = "mask-windows")]
-                "call {windows_leaving}",
+                "# {windows_leaving}",
                 "mov rsp, rbp",
                 "lock sub dword ptr gs:[{preempt_count}], 1",
                 "test dword ptr [rsp + 88], 3", // CS = 10 GPRs + RIP above

m05-switch-unguarded.patch

diff --git a/kernel/src/arch/x86_64/hw.rs b/kernel/src/arch/x86_64/hw.rs
index a296670b3..bd2b636da 100644
--- a/kernel/src/arch/x86_64/hw.rs
+++ b/kernel/src/arch/x86_64/hw.rs
@@ -286,8 +286,6 @@ impl Hw for KernelHw {
         let restore = token.restore_ptr();
         // Every context is saved masked, so `context_switch`'s `popfq` never
         // moves `IF`; the resumed context's own guard puts back what it had.
-        #[cfg(feature = "mask-windows")]
-        let _masked = crate::arch::IrqGuard::close();
         // SAFETY: `save`/`restore` are live Box-backed contexts from `SchedPass::finish`, freed only by a later pass; `incoming.thread_pointer` is this kernel's own canonical value for the thread being installed.
         unsafe {
             (*save).thread_pointer = cpu::read_fs_base();

m06-preempt-raise-unhooked.patch

diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs
index 34084fa58..d0ef5729b 100644
--- a/kernel/src/arch/x86_64/percpu.rs
+++ b/kernel/src/arch/x86_64/percpu.rs
@@ -782,8 +782,6 @@ pub fn set_preempt_count(value: u32) {
 #[inline]
 pub fn preempt_count_up() {
     gs::lock_inc_u32::<OFF_PREEMPT_COUNT>();
-    #[cfg(feature = "mask-windows")]
-    crate::windows::preempt_raised();
 }
 
 /// One decrement, atomic against an interrupt on this CPU.

m11-aarch64-svc-unhooked.patch

diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index fe849db4e..e7e0d6917 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -103,8 +103,6 @@ extern "C" fn dispatch(frame: &mut Frame, entry: u64) {
             crate::windows::irqs_unmasking();
         }
         EL0_SYNC => {
-            #[cfg(feature = "mask-windows")]
-            crate::windows::irqs_masked();
             el0_sync(frame);
             crate::scheduler::exit_to_user();
         }

@Japabu Japabu changed the title Stage 6 step 2: the longest interrupts-off and preemption-off windows per CPU, beside the IRQ census Stage 6 step 2, built: the longest interrupts-off and preemption-off windows per CPU, and a held window for the T14 to read back Oct 2, 2026
@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

T14 evidence at 8b73eba69, run and judged by the orchestrator as a metal mutation loop: the request's three boots in order, each image's sha256 checked against the request before it was flashed (cargo run --bin toyos-metal -- --image … --readback … --fat32-check from the worktree, clean at the head), then judgings A, B, C and D in the request's order with A's seven rows left in the tree for B, C and D. Afterwards the rows were saved as a patch and the worktree restored clean; nothing is committed. Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71).

boot kernel booted (loader.log) loop jobs, exit the kernel's own windows: held line
1-head, no patch 3112640 bytes EXIT=0, passed, 1152 ms idle_span 0, pwd 0, ring_park_herd 0, echo 0 cpu1 ns=50000029
2-report-halved, mutation a 3114216 bytes EXIT=0, passed, 1152 ms the same four, 0 cpu1 ns=50000001
3-idle-halt-counted, mutation b 3113400 bytes EXIT=0, passed, 1153 ms the same four, 0 cpu1 ns=50000069

The three kernel sizes are the request's. Judgings, each cargo test --test toyos-build -- --metal --metal-readback <dir> mask_windows:

readback expected exit the judge's lines
A 1-head, no windows.windowscase.* row in the tree green 0 [windows] held irqs_off_ns=50075527 preempt_off_ns=50074983; herd irqs_off_ns=1499808 preempt_off_ns=1499590, PASS mask_windows; seven rows written
B 2-report-halved red 1 FAIL mask_windows: cpu1 held both windows for 50000001 ns, and its reports before the load's read back irqs_off_ns=25039186 preempt_off_ns=25038975: a window was reported shorter than it was held
C 3-idle-halt-counted, A's rows in the tree red 1 FAIL windows.windowscase.held_preempt_off_ns is 486343787 against 50074983 recorded on this machine, past the ceiling of 100149966; FAIL windows.windowscase.herd_preempt_off_ns is 14149145 against 1499590 recorded on this machine, past the ceiling of 2999180
D 1-head again, judge mutated by j01-load-read-over-every-report.patch (git apply --check, git apply, judge, git apply -R) red 1 FAIL windows.windowscase.herd_irqs_off_ns is 50075527 against 1499808 recorded on this machine, past the ceiling of 2999616; FAIL windows.windowscase.herd_preempt_off_ns is 50074983 against 1499590 recorded on this machine, past the ceiling of 2999180
  • Every arm came out as the request predicted. B, C and D left A's seven rows unchanged (7 added, 0 removed after each).
  • At the head, per CPU ([windows] lines of judging A): cpu0 irqs_off 6,533,222 ns; cpu1 50,075,527 (the held span); cpu2 to cpu7 between 947,018 and 2,327,848. The load's own windows, now read between two reports: 1,499,808 ns interrupts off and 1,499,590 ns preemption off.
  • A's rows are kept as 1-head.record-rows.patch (18 lines) beside the readbacks, for whoever commits them.

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #649 at 8b73eba69 against origin/main b331934c9, round 2. Read, not run; the worktree is as found (git status --porcelain --ignore-submodules=none empty before and after). R5 is /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r5. My logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-review-r2/ (bc-reports.txt, stop-reports.txt, old-windows-lines.txt, summaries.txt).

Net lines, git diff --shortstat origin/main...8b73eba69: 41 files, +1384 −33. Production +607 −7: kernel/ +354 −3, toyos-sched +240, src/ +13 −4. Tests +603 −10: tests/ +494 −10, the record's 109. Issues +174 −16. The growth is the track's step; what goes is in BLOCKER 2 and 3.

The earlier BLOCKERs

  1. CLOSED as filed: the body's first section carries the eight readings at 72f16e39a, with the boot and judge commands and their exits. It recurs for this head: BLOCKER 1.
  2. CLOSED as filed: the herd's pair is no longer the boot's. In 1-head cpu0's 6,533,222 is in the first of four reports (R5/1-head/windowscase/kernel.log:348) and the judge reads herd irqs_off_ns=1499808 (R5/judge-A.log:20); j01 is EXIT=101 on the host (R5/logs/mut-host-judge-summary.txt) and exit 1 on that readback (R5/judge-D.log:25-26); the eight old readbacks are EXIT=1 (R5/logs/old-readbacks-summary.txt). What it reads instead: BLOCKER 2.
  3. (a) CLOSED: R5/judge-B.log:20, exit 1, 25,039,186 read back against 50,000,001 held. (b) OPEN: R5/judge-C.log:21 reads PASS mask_windows, and its exit 1 is two record lines (:24-25) against rows the tree does not have (git grep windowscase 8b73eba69 -- tests/metal finds nothing). BLOCKER 4.
  4. CLOSED: found_masked occurs nowhere at this head; f01 is EXIT=1 against an unmutated EXIT=0 (R5/logs/mut-fault-summary.txt).
  5. CLOSED: windows-staged, take_marked, carries_the_actuators and refuse_an_unarmable_boot occur nowhere; virt_mask_windows passes on mask-windows alone in a suite that is EXIT=0, 22 of 22 (R5/logs/gates-8b73eba69-guest-suite.log:113, :137).
  6. CLOSED: both files at this head say what ruling 3 said.
  7. CLOSED: kernel/src/windows.rs:116 indexes.

The last round's NOTE 1 to 3 and REMOVE 1 to 3 are done.

Rulings

The seven rows.

  • boot.windowscase.complete_ms, panel_max_us, panel_us: a named change, committed from R5/1-head.record-rows.patch. Every boot has them, and a judging on a tree without them leaves it dirty.
  • herd_irqs_off_ns, herd_preempt_off_ns: not a record. BLOCKER 2 says where 1,499,808 comes from, and BLOCKER 3 what its ceiling does on this machine. One boot's maximum, doubled, is the statistic the last round threw out; the reading that enters the tree is the exit's, a median of at least five boots.
  • held_irqs_off_ns, held_preempt_off_ns: sound as numbers from one boot. The kernel fixes the span: three boots held 50,000,029, 50,000,001 and 50,000,069 ns and read back 50,075,527, 2 × 25,039,186 and 50,076,898 of interrupts off, and nothing an unmutated boot of this machine has shown (11,645,411 at most) is near them. Not committed all the same: the record is the wrong keeper. A row holds (b) only against the boot it was taken from, only once it is in the tree, and not after the file is recorded afresh. The capture's own held line holds it on first sight (BLOCKER 4).
  • So no windows.* row enters the record in this pull request, and a judging of the head arm on the committed tree leaves git status empty.

The next head's evidence. BLOCKER 2 and 5 change the image (the job list, the hold's loop), so the three boots are owed again at that head. D is the host's alone.

What the round chose.

  • The control written fresh: accepted. 14 lines and no parameter, against a three-feature kernel, a build predicate and two clippy shapes; 2-report-halved exits 1 on it.
  • virt_mask_windows on tests/virtsmpcase, eight CPUs: accepted. tests/virtjobcase/system.toml runs debug_refused; m03 and m11 are EXIT=1 there (R5/logs/mut-virt-summary.txt).
  • The x86-64 controls in a scratch probe: as the last round ruled.
  • The Ring 0 stop in a scratch probe: accepted. Its loss costs a misnamed panic on an instrument build, and the only tier that reaches it is an x86-64 guest on a kernel carrying the instrument and an actuator, the build the last round's BLOCKER 5 deleted.
  • (b)'s patch deleting the two hooks with their callers: the same mutation.

BLOCKER

  1. PR body, "This branch meets neither"; ## The checks, "Not yet made." and "(b) is green in every test I can run"; ## Measured, "none yet"; ## Unsure, "(b)'s red on the T14 is a prediction" — the body at this head says the machine has read nothing of this head, and its three boots and four judgings are comment 5959415453 — a claim stands on a measurement in the body: each boot and judging with its command, exit and log, at the head it was booted from.
  2. tests/common/irqcensus.rs:207-211, :250-252, :281; tests/toyos.rs:656-657, :664 — the reading named the load's is set by the runner's CPU, in the span that holds its spawn of the next job, in all three boots at this head. The after report's longest is cpu7's, the CPU that spawned echo (R5/1-head/windowscase/kernel.log:435-437, :456): 1,499,808, 2 × 748,872 and 1,479,558. The same CPU reads 1,408,843, 2 × 737,408 and 1,379,642 in the report that spans its spawn of pwd (:388). The herd's own report peaks at 1,032,292, 2 × 519,358 and 1,175,943, and the CPU that ran the herd's exit reads 151,337 and 2 × 76,944 in the after report where it is not also the runner's (:432, :446) — "the load's own windows, 1,499,808" is not the load's, and a load whose windows grew from 1.0 to 1.4 ms moves nothing the judge prints — read the load from its own report alone, which is the track's wording; the refusal at :250, its capture (tests/checks.rs:423, :450), j03 and echo in the job list go, and the doc says what that report spans, the runner's spawn of the load included.
  3. tests/toyos.rs:2287-2288 — every judging writes windows.windowscase.herd_* into the machine's record and asks for it to be committed: a ceiling of twice one boot's maximum, on a reading this machine carried at 9,260,193 and at 9,603,617 to 9,651,846 ns in two of the eight earlier reports that spanned the herd (R5/logs/old-readback-7-base.log:19, old-readback-4-head.log:12-19). Either is past 2,999,616 — a row that reds on a defect this pull request files as open is a flaky row — print the pair and record none.
  4. tests/common/irqcensus.rs:274; kernel/src/hw.rs:82, :85 — the judge refuses a read-back shorter than the hold and none longer, so R5/mutations/b-idle-halt-counted.patch passes every test in this tree — the capture carries the span itself: refuse a read-back past metaltimings::ceiling(ns), the one rule. The patch must then turn mask_windows red on R5/3-idle-halt-counted (486,343,787 against 50,000,069 held) from a tree with no windows.* row, and the bound's own removal must turn mask_windows_verdict red on a capture that reads the hold back at ten times. held_* is then not measured either.
  5. kernel/src/windows.rs:133-139; tests/common/irqcensus.rs:259 — the hold can print one nanosecond under what the judge demands. At the T14's 413,479 fs a tick (R5/1-head/windowscase/kernel.log:35), counter_ticks(HELD_NS) is 120,925,125 ticks and nanos_of_ticks of that is 49,999,999; a loop that leaves at exactly that count prints ns=49999999, and the judge refuses ns < HELD_NS. 2-report-halved printed ns=50000001, three to five ticks from it — a red on the machine with nothing wrong, by rounding — loop until nanos_of_ticks(held) is at least HELD_NS.

NOTE

  1. issues/kernel/cpu0-holds-interrupts-and-preemption-off-for-6-5-ms-on-the-t14.md:46-48, issues/kernel/some-t14-boots-carry-an-interrupts-off-window-of-9-ms-or-more.md:59-61, issues/kernel/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md:40-46 — each says what the new row will tell, and three boots have told it. cpu0's window is in the first report (6,533,222, 2 × 3,273,547, 6,508,165) and cpu0 reads at most 2 × 472,938 after it. The third reading falls after echo's report, on the reboot and stop side: R5/1-head/windowscase/loader.log:57, :63 read cpu7 11,522,834 and cpu4 11,645,411 against 1,499,808 in echo's. The herd's own report reads 1,032,292, 2 × 519,358 and 1,175,943 at most — a record is kept true: the readings and their readback paths go in.
  2. issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md:172-174 — "and its rows are in that machine's record" names rows the ruling takes out; once (b) reds on first sight the exit's first half is met, and the track says so and keeps the comparison as its open exit.
  3. issues/kernel/ — one window this head measured is filed nowhere: the runner's CPU holds interrupts and preemption off together for 1,379,642 to 1,499,808 ns in each report whose span holds one spawn of a toybox applet, six readings in three boots (bc-reports.txt). By reading, not measured, it is the spawn's syscall under the mask issues/kernel/syscall-preemption-is-incidental.md records — file it: it is the floor under every load's reading on that CPU.

REMOVE

  1. src/build.rs:2651-2653 — kernel/Cargo.toml:132-136's reason, said again.
  2. tests/toyos.rs:161-162 — virt_mask_windows' doc (:1431-1433), said again at its row.
  3. PR body, ## What this round changed, per finding: "BLOCKER 1. The section above.", "BLOCKER 7. log_cpu indexes CPUS.", "REMOVE 1 to 3. Deleted.", "The patch the review names for (b) does not build …", and "I chose delete-and-write-fresh over converting the stage, because it is less code:" with its "Converting keeps …" bullet — a review's history, not main's record.
  4. PR body, ## Measured: "Against the reviewed head: kernel/ 21 fewer added lines, …".
  5. PR body: "The merge. origin/main b331934c9 (Nothing ships for tests alone: the five virt probes leave the shipped toybox, and two owner rulings enter reviewer.md #636) is merged, with no conflict."

SEND BACK

Japabu and others added 3 commits October 2, 2026 21:26
#647, #642 and #636's follow-up landed since the last merge. One conflict,
in tests/toyos.rs: #647 made `boot_virt_smp` take a `BootOptions` and
`judge_virt_job` borrow its guest, and this branch had added
`virt_mask_windows` on the old shape. Main's shape is kept whole;
`virt_mask_windows` names its kernel build in the options and lends its
guest like the others. `tests/virtsmpcase` now ends in `shutdown`, which
this test does not wait for: it judges once `unmap_touch` has ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…no windows row is recorded

Review round 2 on #649, at 8b73eba.

The load's reading was its own report and the one after. In all three T14
boots at 8b73eba the second was set by cpu7, the CPU test-runner spawns
from, in the span that held its spawn of `echo`: 1,499,808, 2 x 748,872
and 1,479,558 ns, against 1,032,292, 2 x 519,358 and 1,175,943 in the
herd's own report. So `windows_under` reads the load's report alone, the
refusal of a boot with no report after it goes with its capture, and `echo`
leaves the row's job list: three exits, three reports.

The judge refused a hold read back shorter than it was held and none read
back longer, so a kernel that counts the idle halt as a preemption-off
window passed every test in the tree and was red on the T14 only against a
row recorded from an earlier boot. The capture carries the span itself: a
read-back past `metaltimings::ceiling` of the held line is refused. On the
existing readback of that kernel (486,343,787 ns against 50,000,069 held)
the row is now red on a tree with no `windows.*` row.

No `windows.*` row is recorded. Twice one boot's longest window is a
ceiling this machine has already been past, at 9.2 to 9.7 ms in two of
eight earlier boots, and the held pair is bounded by the capture. The judge
prints both pairs. `boot.windowscase.*`, which every boot of the row
measures, is committed from the head's boot at 8b73eba, so a judging
leaves the tree as it found it.

`hold_once` converted `HELD_NS` to ticks and looped on the tick count. At
the T14's 413,479 fs a tick that count is 120,925,125, which converts back
to 49,999,999 ns: a hold that left at exactly that count would print one
nanosecond under what the judge demands. It loops on the nanoseconds it
prints.

Two comments that said their neighbours again go: `mask-windows`' reason in
`src/build.rs`, which `kernel/Cargo.toml` states, and `virt_mask_windows`'
row comment, which its function's doc states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…ning CPU's window is filed

Each of three issue files said what the row's new reports would tell, and
the T14's three boots at 8b73eba (comment 5959415453 on #649) have told
it: cpu0's 6.5 ms window is in the first report and nothing like it after;
the 10 to 11 ms reading falls after `echo`'s report, on the `reboot` and
stop side; and the herd's own report reads 1.0 to 1.2 ms of interrupts off
at N = 256.

One window those boots measured was filed nowhere: cpu7, which test-runner
spawns from, reads 1,379,642 to 1,499,808 ns of interrupts and preemption
off in each report that spans the spawn of a toybox applet.

The track's step 2: the exit's first half, a window of known length read
back on the T14, is met and no longer rests on rows in the machine's
record. The comparison stays its open exit.

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

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

The scratch probes and the mutation patches of the round that answers review 5959785399, each git apply --check EXIT=0 at 0aa8d4c88. Their verdicts, with exit codes, are in the pull request body under "The checks". None of this is in the tree. The patches of comment 5959292612 are 8b73eba69's: j03 mutated a read this round deleted, j01 to j05 are cut again over the changed judge, j06 and j07 are new, and the kernel and scheduler patches are unchanged and apply at this head.

The runner, run.sh <summary> <probe|-> <mutations…> -- <command…>: the unmutated run first, then each mutation checked, applied, run, reversed, and the tree counted clean.

#!/bin/sh
# Each mutation over the committed head: checked, applied, run by the command
# named, reversed in the same step, and the tree shown clean after.
#   run.sh <summary-name> <probe|-> <mutations...> -- <command...>
# `probe` applies x86-probe.patch (the scratch tests, not in the tree) around
# the whole batch; the first run of the batch is the unmutated one.
set -u
W=/Users/jan/Dev/jan/toyos-sk6b
D=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r6
name=$1; probe=$2; shift 2
muts=""
while [ "$1" != "--" ]; do muts="$muts $1"; shift; done
shift
S=$D/logs/mut-$name-summary.txt
cd "$W" || exit 1
: > "$S"
echo "head $(git rev-parse HEAD), tree lines before: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')" >> "$S"
echo "command: $*" >> "$S"
if [ "$probe" = probe ]; then
  git apply --check "$D/x86-probe.patch"; echo "probe check=$?" >> "$S"
  git apply "$D/x86-probe.patch" || exit 1
fi
"$@" > "$D/logs/mut-$name-unmutated.log" 2>&1; echo "unmutated: EXIT=$?" >> "$S"
for m in $muts; do
  p=$(ls "$D"/mutations/"$m"-*.patch)
  git apply --check "$p"; c=$?
  git apply "$p"; a=$?
  "$@" > "$D/logs/mut-$name-$m.log" 2>&1; t=$?
  git apply -R "$p"; r=$?
  why=$(grep -m1 -E 'mask-windows: cpu|FAIL |panicked at|^error' "$D/logs/mut-$name-$m.log" | cut -c1-400)
  echo "$(basename "$p"): check=$c apply=$a test EXIT=$t reverse=$r | $why" >> "$S"
done
if [ "$probe" = probe ]; then
  git apply -R "$D/x86-probe.patch"; echo "probe reversed: $?" >> "$S"
fi
echo "tree lines after: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')" >> "$S"
echo DONE >> "$S"

x86-probe.patch: two scratch machine tests. x86_windows_probe boots the T14 row's kernel (mask-windows alone) and its job list on eight CPUs under TCG and reads it with the row's own judge, whose durations there are a preview and no verdict. x86_windows_fault_probe takes a Ring 0 read of 0x10 inside a syscall on a kernel carrying the instrument and the actuators. The two system.toml files are tests/testcases/system.toml with a job list.

x86-probe.patch
diff --git a/src/build.rs b/src/build.rs
index 6536f543e..722a16f5d 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1330,8 +1330,8 @@ fn declared_kernel_features(root: &Path) -> Vec<String> {
 pub const TEST_KERNEL: &[&str] = &["boot-actuators", "test-actuators"];
 
 /// Kernel builds the ordinary test suite is allowed to make.
-pub const TEST_SUITE_KERNEL_BUILDS: [&str; 3] =
-    ["", "boot-actuators,test-actuators", MASK_WINDOWS_KERNEL[0]];
+pub const TEST_SUITE_KERNEL_BUILDS: [&str; 4] =
+    ["", "boot-actuators,test-actuators", MASK_WINDOWS_KERNEL[0], "boot-actuators,mask-windows,test-actuators"];
 
 /// The shipping kernel with the windows' instrument and nothing else, for
 /// [`SCHED_CHECK_KERNEL`]'s reason: one spelling, so one build.
diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs
index 4abf78d63..48083798f 100644
--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -158,7 +158,7 @@ pub fn boot_census() -> (u32, u32, Vec<String>) {
 /// entry and masking primitive, where a parameter would be a branch on the path
 /// they measure. Interactive debug mode is separate: it builds
 /// [`toyos_build::build::DEBUG_KERNEL_BUILD`] and returns before the suite.
-pub const DECLARED_KERNEL_BUILDS: [&str; 3] =
+pub const DECLARED_KERNEL_BUILDS: [&str; 4] =
     toyos_build::build::TEST_SUITE_KERNEL_BUILDS;
 
 /// How many guests the run may have up at once.
diff --git a/tests/toyos-rust-tests/src/bin/ring0_read.rs b/tests/toyos-rust-tests/src/bin/ring0_read.rs
new file mode 100644
index 000000000..7365ad2a5
--- /dev/null
+++ b/tests/toyos-rust-tests/src/bin/ring0_read.rs
@@ -0,0 +1,8 @@
+//! Scratch: one Ring 0 read of an unmapped address, inside a syscall and so
+//! with interrupts masked, which ends the machine.
+
+fn main() {
+    println!("ring0_read: asking for a Ring 0 read of 0x10");
+    toyos_abi::syscall::debug_with(toyos_abi::syscall::debug_action::NULL_READ, 0x10);
+    println!("ring0_read: the read returned");
+}
diff --git a/tests/toyos.rs b/tests/toyos.rs
index 14c6e0657..1d2e850f0 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -80,6 +80,7 @@ const RUST_SKIP: &[&str] = &[
     // It asserts nothing: it is half a second of an idle machine, which the
     // same row reads the windows across.
     "idle_span",
+    "ring0_read",
     // The C corpus's comparator: a helper reached through one symlink per case,
     // never a test of its own. `shared_metal` stages every name on this list.
     "ccheck",
@@ -185,6 +186,8 @@ const MACHINE_TESTS: &[&str] = &[
     // no way to turn it back on, so only a machine QEMU reports stopping can
     // be asked. `machine_soft_off_decoded` reads the T14's own decode.
     "machine_shutdown",
+    "x86_windows_probe",
+    "x86_windows_fault_probe",
 ];
 
 /// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2311,6 +2314,73 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
     match name {
         "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
         "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+        "x86_windows_probe" => {
+            // Scratch: the T14 row's kernel and job list under TCG, read by the
+            // machine's own judge, whose durations here are a preview and no verdict.
+            const CPUS: u32 = 8;
+            let root = compile::repo_root();
+            let bin = |name: &str| {
+                (
+                    format!("bin/test_rs_{name}"),
+                    qemu::build_toyos_bin(toyos_build::arch::Arch::X86_64, &root.join("tests/toyos-rust-tests"), name),
+                )
+            };
+            let mut qemu = QemuInstance::boot_with_options(
+                &root.join("tests/windowsprobecase"),
+                &[],
+                &[],
+                BootOptions {
+                    smp: CPUS,
+                    kernel_features: toyos_build::build::MASK_WINDOWS_KERNEL,
+                    extra_root_files: vec![bin("ring_park_herd"), bin("idle_span")],
+                    ..Default::default()
+                },
+            );
+            let serial = judge_virt_job(&mut qemu, WINDOWS_LOAD, "ring_park_herd: 256 threads parked")?;
+            for line in serial.lines().filter(|l| l.contains("windows: ") || l.contains("exit: ") && l.contains(" pid=")) {
+                eprintln!("  [probe] {line}");
+            }
+            mask_windows(&serial, CPUS)?;
+            let read = common::irqcensus::windows_under(&serial, CPUS, &windows_load_exited())?;
+            eprintln!("  [probe] {read:?}");
+            Ok(())
+        }
+        "x86_windows_fault_probe" => {
+            // Scratch: a Ring 0 fault taken with interrupts masked, on the
+            // instrumented kernel: the report owed is the fault's, not the
+            // record's refusal.
+            let root = compile::repo_root();
+            let read = qemu::build_toyos_bin(
+                toyos_build::arch::Arch::X86_64,
+                &root.join("tests/toyos-rust-tests"),
+                "ring0_read",
+            );
+            let qemu = QemuInstance::boot_with_options(
+                &root.join("tests/windowsfaultcase"),
+                &[],
+                &[],
+                BootOptions {
+                    smp: 4,
+                    kernel_features: &["boot-actuators", "mask-windows", "test-actuators"],
+                    profile: qemu::Profile::Metal,
+                    ready_marker: "panic: rebooting in",
+                    extra_root_files: vec![("bin/test_rs_ring0_read".to_string(), read)],
+                    ..Default::default()
+                },
+            );
+            let serial = serial::Serial::boot(&qemu);
+            let text = serial.text();
+            if let Some(refused) = text.lines().find(|l| l.contains("mask-windows:")) {
+                return Err(format!("the record refused where a Ring 0 fault was owed its own report: {refused}\n{text}"));
+            }
+            for owed in ["SYS_DEBUG: a Ring 0 read of 0x10", "FAULT rip="] {
+                if !text.contains(owed) {
+                    return Err(format!("{owed:?} not on the console\n{text}"));
+                }
+            }
+            eprintln!("  [windows] a Ring 0 read fault with interrupts masked reported itself");
+            Ok(())
+        }
         "machine_shutdown" => power::machine_shutdown(test_config),
         other => Err(format!("unknown machine test {other}")),
     }
diff --git a/tests/windowsfaultcase/system.toml b/tests/windowsfaultcase/system.toml
new file mode 100644
index 000000000..e05b1267c
--- /dev/null
+++ b/tests/windowsfaultcase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+args = ["--bound-ms=900000", "pwd", "test_rs_ring0_read"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]
diff --git a/tests/windowsprobecase/system.toml b/tests/windowsprobecase/system.toml
new file mode 100644
index 000000000..9963c4dc9
--- /dev/null
+++ b/tests/windowsprobecase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+args = ["--bound-ms=900000", "test_rs_idle_span", "pwd", "test_rs_ring_park_herd"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]

a-report-halved.patch

diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..c33ba9c23 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -116,8 +116,8 @@ pub fn log_cpu(cpu: u32) {
     let (irqs, preempt) = CPUS[cpu as usize].take();
     crate::log!(
         "windows: cpu{cpu} irqs_off_ns={} preempt_off_ns={}",
-        crate::clock::nanos_of_ticks(irqs),
-        crate::clock::nanos_of_ticks(preempt),
+        crate::clock::nanos_of_ticks(irqs) / 2,
+        crate::clock::nanos_of_ticks(preempt) / 2,
     );
 }
 

b-idle-halt-counted.patch

diff --git a/kernel/src/hw.rs b/kernel/src/hw.rs
index c40c8af02..049086500 100644
--- a/kernel/src/hw.rs
+++ b/kernel/src/hw.rs
@@ -78,11 +78,7 @@ impl Machine for KernelHw {
         if crate::actuator::diag_tick() {
             irqchip::arm_within(DIAG_TICK_NS);
         }
-        #[cfg(feature = "mask-windows")]
-        crate::windows::halting();
         self.halt();
-        #[cfg(feature = "mask-windows")]
-        crate::windows::woken();
         // **A CPU that is executing has a one-shot armed, and this is where
         // that becomes true again.** `TimerPlan::Stop` left this one at zero
         // before the halt above and only a pass reaching `apply_timer` arms
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..1e270fa09 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -101,16 +101,6 @@ pub fn scheduled() {
     on(|w| w.scheduled(cpu::counter));
 }
 
-/// This CPU halts inside a pass.
-pub fn halting() {
-    on(|w| w.halting(cpu::counter));
-}
-
-/// The halt has ended, still inside its pass.
-pub fn woken() {
-    on(|w| w.woken(cpu::counter));
-}
-
 /// `cpu`'s line, taking its longest windows so the next report starts from none.
 pub fn log_cpu(cpu: u32) {
     let (irqs, preempt) = CPUS[cpu as usize].take();

f01-ring0-fault-not-stopped.patch

diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs
index c7f63b304..c6a50d879 100644
--- a/kernel/src/arch/x86_64/idt/mod.rs
+++ b/kernel/src/arch/x86_64/idt/mod.rs
@@ -386,9 +386,7 @@ extern "sysv64" fn trap_dispatch(frame: *mut TrapFrame) {
 /// place of the fault's report.
 #[cfg(feature = "mask-windows")]
 fn windows_entered(frame: &TrapFrame) {
-    if !toyos_userbound::Ring::of_cs(frame.cs).is_user() {
-        crate::windows::stop_here();
-    }
+    let _ = frame;
     crate::windows::irqs_masked();
     crate::windows::preempt_raised();
 }
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..0781e0a45 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -67,15 +67,6 @@ pub fn irqs_masked() {
     on(|w| w.masked(cpu::counter));
 }
 
-/// This CPU took an exception in the kernel, which ends the machine: nothing
-/// more of it is tracked or checked.
-pub fn stop_here() {
-    on(|w| {
-        w.stop();
-        Ok(())
-    });
-}
-
 /// Interrupts are masked and this CPU is about to open them.
 pub fn irqs_unmasking() {
     on(|w| w.unmasking(cpu::counter));

h02-pass-unsplit.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..6de5f7fa6 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -187,9 +187,7 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        let now = now();
-        self.close_preempt(now)?;
-        self.preempt_off.store(now, Ordering::Relaxed);
+        let _ = now;
         Ok(())
     }
 

h03-halt-counted.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..a5fddac5e 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -198,7 +198,8 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        self.close_preempt(now())
+        let _ = now;
+        Ok(())
     }
 
     /// The halt [`Windows::halting`] began has ended, still inside its pass.
@@ -206,7 +207,8 @@ impl Windows {
         if !self.tracked() {
             return Ok(());
         }
-        self.open_preempt(now)
+        let _ = now;
+        Ok(())
     }
 
     fn open_preempt(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {

h04-crossing-ignored.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..c43fe98c3 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -154,7 +154,7 @@ impl Windows {
 
     /// The preempt count has just been raised by one, to `count`.
     pub fn raised(&self, count: u32, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
-        if count != 1 || !self.tracked() {
+        if count == 0 || !self.tracked() {
             return Ok(());
         }
         self.open_preempt(now)

h05-last-not-longest.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..3fe4d59b8 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -101,9 +101,7 @@ impl Windows {
 
     fn record(longest: &AtomicU64, since: u64, now: u64) {
         let span = now.saturating_sub(since);
-        if span > longest.load(Ordering::Relaxed) {
-            longest.fetch_max(span, Ordering::Relaxed);
-        }
+        longest.store(span, Ordering::Relaxed);
     }
 
     /// The CPU joins the scheduler with interrupts masked and its preempt

h06-unmask-unchecked.patch

diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..7e74039fc 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -143,7 +143,7 @@ impl Windows {
     pub fn unmasking(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
         match self.irqs_off.load(Ordering::Relaxed) {
             UNTRACKED => Ok(()),
-            0 => Err(Unseen::AMask),
+            0 => Ok(()),
             since => {
                 Self::record(&self.irqs_longest, since, now());
                 self.irqs_off.store(0, Ordering::Relaxed);

j01-load-read-over-every-report.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..d3e304aff 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -283,7 +283,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
             "{read}, past the ceiling of {ceiling}: a window was reported at more than twice what was held"
         ));
     }
-    let load = longest(&mut reports[own].1.iter());
+    let load = longest(&mut reports.iter().flat_map(|(_, report)| report));
     Ok(Measured { held, load })
 }
 

j02-no-report-before-the-load-accepted.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..9c170505f 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -243,7 +243,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
         .iter()
         .rposition(|(at, _)| *at < exited)
         .ok_or_else(|| format!("no report before `{load_exited}`: the load's exit printed none"))?;
-    if own == 0 {
+    if false {
         return Err("the load's report is the boot's first: none was taken as the load started, so it \
                     carries every window since each CPU joined"
             .to_string());

j04-one-kind-read-back-is-enough.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..fe1e63c57 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -274,7 +274,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
          irqs_off_ns={} preempt_off_ns={}",
         held.0, held.1
     );
-    if held.0 < ns || held.1 < ns {
+    if held.0 < ns && held.1 < ns {
         return Err(format!("{read}: a window was reported shorter than it was held"));
     }
     let ceiling = toyos_build::metaltimings::ceiling(ns);

j05-the-loads-report-reads-the-hold-back.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..e867dc0d3 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -257,7 +257,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
     if ns < toyos_sched::windows::HELD_NS {
         return Err(format!("cpu{cpu} held its windows for {ns} ns, and the kernel owes {}", toyos_sched::windows::HELD_NS));
     }
-    let mut read_back = reports[..own]
+    let mut read_back = reports[..]
         .iter()
         .filter(|(at, _)| *at > hold_at)
         .filter_map(|(_, report)| report.get(cpu as usize))

j06-no-ceiling-on-the-read-back.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..f34d6d745 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -278,7 +278,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
         return Err(format!("{read}: a window was reported shorter than it was held"));
     }
     let ceiling = toyos_build::metaltimings::ceiling(ns);
-    if held.0 > ceiling || held.1 > ceiling {
+    if false {
         return Err(format!(
             "{read}, past the ceiling of {ceiling}: a window was reported at more than twice what was held"
         ));

j07-one-kind-under-the-ceiling-is-enough.patch

diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..402223640 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -278,7 +278,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
         return Err(format!("{read}: a window was reported shorter than it was held"));
     }
     let ceiling = toyos_build::metaltimings::ceiling(ns);
-    if held.0 > ceiling || held.1 > ceiling {
+    if held.0 > ceiling && held.1 > ceiling {
         return Err(format!(
             "{read}, past the ceiling of {ceiling}: a window was reported at more than twice what was held"
         ));

m01-syscall-entry-unhooked.patch

diff --git a/kernel/src/arch/x86_64/syscall.rs b/kernel/src/arch/x86_64/syscall.rs
index e7be09402..4e8ba5b0f 100644
--- a/kernel/src/arch/x86_64/syscall.rs
+++ b/kernel/src/arch/x86_64/syscall.rs
@@ -176,7 +176,6 @@ extern "sysv64" fn syscall_handler(num: u64, a1: u64, a2: u64, _: u64, a3: u64,
     // `IA32_FMASK` masked interrupts, and the entry raised the preempt count by one.
     #[cfg(feature = "mask-windows")]
     {
-        crate::windows::irqs_masked();
         crate::windows::preempt_raised();
     }
     #[cfg(feature = "df-witness")]

m02-guard-drop-unhooked.patch

diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs
index d7e401a25..a072656fc 100644
--- a/kernel/src/arch/x86_64/mod.rs
+++ b/kernel/src/arch/x86_64/mod.rs
@@ -82,7 +82,7 @@ impl Drop for IrqGuard {
     fn drop(&mut self) {
         // Only where the restore opens them.
         #[cfg(feature = "mask-windows")]
-        if idt::frame_interrupts_enabled(self.rflags) && !cpu::interrupts_enabled() {
+        if idt::frame_interrupts_enabled(self.rflags) && cpu::interrupts_enabled() {
             crate::windows::irqs_unmasking();
         }
         // SAFETY: the word `close` read out of RFLAGS on this CPU (the guard is

m03-exit-to-user-unhooked.patch

diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs
index 07e9c4ae1..85003c031 100644
--- a/kernel/src/scheduler.rs
+++ b/kernel/src/scheduler.rs
@@ -406,8 +406,6 @@ pub fn exit_to_user() {
         if !crate::preempt::need_resched() {
             // Every caller's next unmask is the return to user mode, or a
             // kernel thread's first.
-            #[cfg(feature = "mask-windows")]
-            crate::windows::irqs_unmasking();
             return;
         }
         assert!(!in_schedule_self(), "exit-to-user inside a scheduler pass");

m04-device-leaving-unhooked.patch

diff --git a/kernel/src/arch/x86_64/idt/device_irq.rs b/kernel/src/arch/x86_64/idt/device_irq.rs
index d3b355e10..6783da8a6 100644
--- a/kernel/src/arch/x86_64/idt/device_irq.rs
+++ b/kernel/src/arch/x86_64/idt/device_irq.rs
@@ -35,7 +35,7 @@ macro_rules! device_irq_entry {
                 #[cfg(feature = "mask-windows")]
                 "mov rdi, [rbp + 88]",
                 #[cfg(feature = "mask-windows")]
-                "call {windows_leaving}",
+                "# {windows_leaving}",
                 "mov rsp, rbp",
                 "lock sub dword ptr gs:[{preempt_count}], 1",
                 "test dword ptr [rsp + 88], 3", // CS = 10 GPRs + RIP above

m05-switch-unguarded.patch

diff --git a/kernel/src/arch/x86_64/hw.rs b/kernel/src/arch/x86_64/hw.rs
index a296670b3..bd2b636da 100644
--- a/kernel/src/arch/x86_64/hw.rs
+++ b/kernel/src/arch/x86_64/hw.rs
@@ -286,8 +286,6 @@ impl Hw for KernelHw {
         let restore = token.restore_ptr();
         // Every context is saved masked, so `context_switch`'s `popfq` never
         // moves `IF`; the resumed context's own guard puts back what it had.
-        #[cfg(feature = "mask-windows")]
-        let _masked = crate::arch::IrqGuard::close();
         // SAFETY: `save`/`restore` are live Box-backed contexts from `SchedPass::finish`, freed only by a later pass; `incoming.thread_pointer` is this kernel's own canonical value for the thread being installed.
         unsafe {
             (*save).thread_pointer = cpu::read_fs_base();

m06-preempt-raise-unhooked.patch

diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs
index 34084fa58..d0ef5729b 100644
--- a/kernel/src/arch/x86_64/percpu.rs
+++ b/kernel/src/arch/x86_64/percpu.rs
@@ -782,8 +782,6 @@ pub fn set_preempt_count(value: u32) {
 #[inline]
 pub fn preempt_count_up() {
     gs::lock_inc_u32::<OFF_PREEMPT_COUNT>();
-    #[cfg(feature = "mask-windows")]
-    crate::windows::preempt_raised();
 }
 
 /// One decrement, atomic against an interrupt on this CPU.

m11-aarch64-svc-unhooked.patch

diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index fe849db4e..e7e0d6917 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -103,8 +103,6 @@ extern "C" fn dispatch(frame: &mut Frame, entry: u64) {
             crate::windows::irqs_unmasking();
         }
         EL0_SYNC => {
-            #[cfg(feature = "mask-windows")]
-            crate::windows::irqs_masked();
             el0_sync(frame);
             crate::scheduler::exit_to_user();
         }

@Japabu Japabu changed the title Stage 6 step 2, built: the longest interrupts-off and preemption-off windows per CPU, and a held window for the T14 to read back Stage 6 step 2, built: the longest interrupts-off and preemption-off windows per CPU, and a held window the T14 reads back Oct 2, 2026
@Japabu

Japabu commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

T14 at 0aa8d4c88, run by the orchestrator from 649-r6/request.txt: three boots, each image's sha256 checked against the request before it was flashed, each toyos-metal exit 0, loader.log's kernel size the request's (3125888, 3124904, 3124664 bytes), and idle_span, pwd and ring_park_herd each exit=0 in every boot. Then the three judgings, cargo test --test toyos-build -- --metal --metal-readback <dir> mask_windows from the clean worktree at the head; git status --porcelain --ignore-submodules=none printed nothing after each, so no judging wrote a row.

image sha256 the kernel's held line judge
A, head 915b02246f2a06d1019ec4a47ada92346394bd24a8c44665d1f006940324eac8 held cpu1 ns=50000068 exit 0, PASS mask_windows: held irqs_off_ns=50080844 preempt_off_ns=50080384; herd irqs_off_ns=1980239 preempt_off_ns=1980156 (printed, not judged)
B, a-report-halved 57b66b9f8a7e83c18723f5e9cd1a85a14bab9c04cb4c03f068ee04eec84724c3 held cpu1 ns=50000026 exit 1, FAIL mask_windows: cpu1 held both windows for 50000026 ns, and its reports before the load's read back irqs_off_ns=25042021 preempt_off_ns=25041779: a window was reported shorter than it was held
C, b-idle-halt-counted 8ddca1d301a8dfbc11225bc140ea5587a33917617566269a05bfc113ad8bd9ec held cpu1 ns=50000030 exit 1, FAIL mask_windows: cpu1 held both windows for 50000030 ns, and its reports before the load's read back irqs_off_ns=50076547 preempt_off_ns=476714793, past the ceiling of 100000060: a window was reported at more than twice what was held

All three are what the request predicted: A green, B red, C red by the judge and by no row.

Per CPU, the longest each boot reported (irqs_off_ns / preempt_off_ns):

cpu A B (printed at half) C (halts counted)
0 6543821 / 6543757 5025441 / 5024311 6547143 / 527097284
1 50080844 / 50080384 25042021 / 25041779 50076547 / 476714793
2 975372 / 973069 5014552 / 5009820 586745 / 547757986
3 1035232 / 2516805 5075290 / 5070886 544283 / 246666947
4 1672797 / 1672672 5019823 / 5019755 581812 / 547780823
5 969922 / 969767 5022490 / 5020695 557324 / 551819954
6 1221012 / 1220660 5024742 / 5018523 1370675 / 551829245
7 11492028 / 11491962 5069392 / 5067378 8456658 / 526865978

Two readings beside the verdicts, neither judged. In A the herd's own report reads 1,980,239, against 1,032,292 at 8b73eba69. In B all eight CPUs, the holder included, read 5.01 to 5.08 ms at half in the herd's own report, so about 10.0 to 10.2 ms as held (corrected: this first said "every CPU other than the holder"; the table's 25 ms for cpu1 is its longest over the boot): the shape some-t14-boots-carry-an-interrupts-off-window-of-9-ms-or-more.md records, in one boot of this three.

Readbacks and judge logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r6/ (1-head, 2-report-halved, 3-idle-halt-counted, judge-{A,B,C}.log).

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #649 at 0aa8d4c88 against origin/main dd8738302, round 3. Read, not run; the worktree is as found (git status --porcelain --ignore-submodules=none empty before and after). R6 is /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r6, R5 its 649-r5 sibling. My logs: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-review-r3/ (a-reports.txt, bc-reports.txt, r5-reports.txt, loader-logs.txt).

Net lines, git diff --shortstat origin/main...0aa8d4c88: 43 files, +1455 −30. Production +605 −7: kernel/ +355 −3, toyos-sched +240, src/ +10 −4. Tests +608 −7: tests/ +499 −7, the record's 109. Issues +242 −16. The growth is the track's step; nothing more to delete at this head than the REMOVEs.

The earlier BLOCKERs

  1. CLOSED: the body's "Three boots at 0aa8d4c88" carries each boot's command and exit and each judging's command, exit and log (R6/judge-{A,B,C}.log: exit 0, 1, 1), with loader.log's Kernel: 3125888, 3124904 and 3124664 bytes saying which image booted.
  2. CLOSED: tests/common/irqcensus.rs:286 reads reports[own] alone; the refusal, its capture and echo are gone (R6/logs/gates-0aa8d4c88-metal-list.log: windowscase: 3 job(s)). j01 is EXIT=101 against an unmutated EXIT=0 (R6/logs/mut-host-judge-summary.txt), and A's judge prints herd irqs_off_ns=1980239, the longest line of the herd's own report (R6/1-head/windowscase/kernel.log:430).
  3. CLOSED: no boot.measured("windows.…") at this head (git grep 'windows\.windowscase' 0aa8d4c88 finds nothing); git status empty after each of A, B and C (comment 5960575031) and after each of the eleven older readbacks (R6/logs/readbacks-summary.txt).
  4. CLOSED: C is exit 1 by the judge on a tree with no windows.* row, preempt_off_ns=476714793, past the ceiling of 100000060 (R6/judge-C.log); R5/3-idle-halt-counted is exit 1 under this head's judge and EXIT=0 under j06; j06 and j07 are EXIT=101 on mask_windows_verdict.
  5. CLOSED: kernel/src/windows.rs:136-139 loops on the nanoseconds it prints; the three boots print ns=50000068, 50000026 and 50000030.

The last round's NOTE 1 to 3 and REMOVE 1 to 5 are done.

Rulings

The two readings nothing judges. Neither is the instrument's or the judge's: the hold reads back at 50,080,844, 2 × 25,042,021 and 50,076,547 beside them, and B's red is cpu1's read-back alone. Both are evidence the branch's issue files do not carry (NOTE 2 to 4).

  • 1,980,239 is cpu7's line (R6/1-head/windowscase/kernel.log:430), the CPU that spawned the herd (:395). The other seven read 969,922 to 1,044,783 there (:416-428), against 934,218 to 1,032,292 at 8b73eba69: the herd as seven CPUs read it did not move. Nor did an applet's spawn, so SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642 is not it: pwd's report reads cpu7 1,511,452, 2 × 740,441 and 1,360,995 (:388 in each boot), against 1,408,843, 2 × 737,408 and 1,379,642. The 1.98 ms is one line of one boot that nothing names.
  • B's 5 ms lines are all eight CPUs' lines of the herd's own report, the holder's among them: 2 × 5,014,552 to 2 × 5,075,290 (R6/2-report-halved/windowscase/kernel.log:416-430), beside tlb: shootdowns=282 wait=131633us max=10038us (:431) and cpu0's nmi=1 (:415). It is some-t14-boots-…'s first reading a second time in fourteen boots, now placed between pwd's exit and the herd's.
  • C's herd report reads cpu0 4,725,822 (R6/3-idle-halt-counted/windowscase/kernel.log:416) and the other seven 544,283 to 1,018,355.
  • So the load's own report reads 1,980,239, 2 × 5,075,290 and 4,725,822 at this head against 1,032,292, 2 × 519,358 and 1,175,943 at the last. It keeps the boot's start and the next job's spawn out of the load's reading, and none of the machine's own events. The track's open exit already takes a median of five with the tail beside it; the herd defect's exit does not (NOTE 2).

The next head's evidence. The named changes are issue text, the body and one space in tests/toyos.rs: no byte of an image. The three boots stand for that head if its staged kernel is ac40f379…28a2, the one 1-head booted (R6/logs/stage-summary.txt). cargo run -- --ci host and the guest suite are owed at it.

BLOCKER

None.

NOTE

  1. tests/toyos.rs:1483 — let psci =serial.lines()…: the merge's resolution took a space out of a line of AArch64 stage 5: PSCI resets and powers the machine off, behind one reset and power-off seam #647's in virt_smp, the one line of git diff origin/main 0aa8d4c88 -- tests/toyos.rs that is not this branch's — restore main's line.
  2. issues/kernel/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md:40-51, :53-59 — the reading "at one size" is 1.03 to 1.18 ms, and the same report reads 1,980,239, 2 × 5,075,290 and 4,725,822 in this head's three boots; the exit is one reading a size, so which boot carried an event decides "does not grow with N" — add this head's readings with their readback paths, and give the exit the track's statistic: the median of at least five boots a size, the tail beside it.
  3. issues/kernel/some-t14-boots-carry-an-interrupts-off-window-of-9-ms-or-more.md:18-34, :61-70 — the first reading recurred at this head inside the herd's own report, with the shootdown's max beside it again (9575us then, 10038us now) and without nmi=2; and a one-CPU reading now falls before the first job's exit, cpu7's 11,492,028 in A's first report (R6/1-head/windowscase/kernel.log:362; 2,327,848, 2 × 1,831,727 and 3,630,933 at 8b73eba69), where the stop's kept lines read at most 1,470,910 in all three boots (loader-logs.txt) — a record is kept true: both go in.
  4. issues/kernel/the-cpu-that-spawns-a-toybox-applet-reads-1-4-ms-of-interrupts-and-preemption-off-on-the-t14.md:33-36 — "in the herd's cpu7 reads 1019488, 2 × 514392 and 1175943" has a fourth, 1,980,239, past what the same boot's applet spawn reads (1,511,452) — add it, and say the spawn does not account for it.
  5. PR body:26, :28 — "every CPU but the holder" is all eight, and the line does not say they are the herd's own report with max=10038us beside them; "differs from 8b73eba69's in two places" leaves out SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's #642 and AArch64 stage 5: PSCI resets and powers the machine off, behind one reset and power-off seam #647, which the merge put in the image — a record is kept true.
  6. PR body:95, :183-184, :219 — three lines state 8b73eba69's numbers under line 10's "All measurements are at 0aa8d4c88 unless said": "1,032,292 in the head's boot", "eleven boots" and the read-back "at 8b73eba69", "1,175,943 ns against 364,953" — this head's are 1,980,239, fourteen boots, 50,080,844 and 50,080,384 against 50,000,068, and 1,980,239 against 1,044,783.

REMOVE

  1. PR body:184 — "Since that head this branch changed the hold's loop alone … (git diff --stat 8b73eba69 0aa8d4c88 over those files)": an argument by diff where this head's own boot reads the hold back.
  2. PR body:48 — "8b73eba69's own judge read the same three as exit 0, 1 and 1, … Here the third is the judge's own verdict.": a review's history.
  3. PR body:198, :206 and the expected column between them — the request's predictions; what was read is the first section.

LAND AFTER NAMED CHANGES

…'s line is main's again

The named changes of #649's third review (comment 5960868119).

tests/toyos.rs: the merge of dd87383 took a space out of a line of #647's
in `virt_smp`. It is main's line again, so the file's diff against main is
this branch's alone.

The herd's defect: its own report at N = 256 read 1980239, 2 x 5075290 and
4725822 in this head's three T14 boots (comment 5960575031, readbacks
649-r6/), against 1032292, 2 x 519358 and 1175943 at 8b73eba. One reading
a size is decided by which boot carried one of the machine's own events, so
the exit takes the track's statistic: the median of at least five boots a
size, the tail beside it. The roster's syscall counts are said of the
fourteen boots they are the range of.

The 9 ms defect: its first reading recurred in 2-report-halved inside the
herd's own report, all eight CPUs at 2 x 5014552 to 2 x 5075290 with the
shootdown line's max=10038us beside it and nmi=1; and cpu7's line of the
first report read 11492028, 2 x 1308074 and 8456658, where the stop's kept
lines read at most 1396887, 2 x 758960 and 1470910. The exit covers the
one-CPU reading before the first job's exit as well.

The spawning CPU's defect: cpu7's line of the herd's report read 1980239 in
1-head, 468787 past the 1511452 that boot's `pwd` report reads for an
applet's spawn.

No byte of an image moves: the three boots at 0aa8d4c stand for this head
if its staged kernel is the one 1-head booted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
@Japabu
Japabu marked this pull request as ready for review October 2, 2026 20:37
@Japabu
Japabu enabled auto-merge October 2, 2026 20:38
@Japabu
Japabu added this pull request to the merge queue Oct 2, 2026
@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Linux on the same T14, for comparison (the owner asked; run by the orchestrator, not part of this pull request's evidence). Ubuntu 24.04, kernel 6.8.0-142-generic, CONFIG_PREEMPT_DYNAMIC at voluntary, i5-1135G7, 8 CPUs. Ubuntu's kernel has no irqsoff tracer (CONFIG_IRQSOFF_TRACER is not set), so this is rtla timerlat top -q -d 1m --dma-latency 0: a 1 kHz timer per CPU, the interrupt's lateness and the woken thread's lateness. It samples: a window under 1 ms is seen only when a tick falls inside it, so the figures under 1 ms are lower bounds on Linux's longest window; a window of 1 ms or more is always seen.

cpu idle: IRQ max µs idle: thread max µs spawn load: IRQ max µs spawn load: thread max µs
0 7 46 32 88
1 58 571 49 503
2 3 9 16 44
3 13 15 58 64
4 2952 2964 2880 2917
5 99 120 131 168
6 47 50 17 65
7 7 24 29 38

The spawn load is one shell loop per CPU running /bin/true, 85,007 to 85,267 spawns per CPU in the minute. cpu4 lost four samples in each run (#59996) to one event of about 2.9 ms; its cause is not identified. rtla osnoise top -q -d 1m beside it counts hardware noise on every CPU but cpu0, most on cpu5 (1296 idle, 1162 loaded) and cpu4 (130, 193).

Raw output: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/linuxcmp/linux-t14.log.

Merged via the queue into main with commit c59e09e Oct 2, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-sk6b branch October 2, 2026 21:24
Japabu added a commit that referenced this pull request Oct 2, 2026
Main moved during the round: #657 (8f6abbc) and #649 (c59e09e) landed
after b102097 merged 5daab30. No conflict, and no file is changed on
both sides since 5daab30; the `rust` gitlink is the same on both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Japabu added a commit that referenced this pull request Oct 2, 2026
… interrupt keep their own, with owners and exits

Review round 1 of #681 found the T14's log-write file a third record of
the spin `issues/hardware/xhci-waits-are-spins.md` and
`a-disk-operation-can-spin-past-the-tlb-ack-tripwire-before-its-break`
already held, with the cpu0 file's partition claims a fourth.

- xhci-waits-are-spins.md is the one record: the spin, a disk call's
  masked syscall, the tripwire arithmetic (folded from the deleted
  a-disk-operation file), the T14 readings (folded from the deleted
  log-write file), init's partition claims and #649's readings that are
  this by reading. Owner: the small-kernel track's step 10. Exit: the tree
  has no kernel/src/drivers/xhci. Old content dropped as no longer true of
  the tree: the hot-plug enumeration's ~14 ms (it is stepped now) and the
  "I/O thread or asynchronous block layer" choice (the track chose usbd).
- The cpu0 file keeps only the 3.8 ms PCI claim and is renamed
  inits-claim-of-a-pci-function-the-t14-lacks-holds-interrupts-off-for-3-8-ms:
  its 6.5 ms is the spin's. Owner: the same track (stage 5's exit, step 10
  unmasking cpu0's line). Exit: the mask_windows row's first report reads
  cpu0 under 3,817,265 ns after step 10.
- The firmware file drops "and not under Linux" from its slug: Linux's 0
  was read beside rtla timerlat --dma-latency 0 only. It states the Linux
  ACPI values as read (comment 5962768253), shows the stop by the two lock
  gaps instead of a late sample, names the T14 loop as owner of the row,
  and its exit reads the count from init's spawn to the stop's report,
  after any SMI_CMD write the bring-up makes.
- Corrections: the writing CPU acknowledges 130 to 264 ns before each of
  the fourteen long shootdowns ends; the stop's report read two CPUs over
  10 ms in 649-r5/1-head; the walk reads 1.03 to 1.98 ms without the log
  write and the firmware step. The instrument's control bullet is removed.
- a-shutdown-on-a-held-usb-disk's citation of the deleted file follows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
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