Nine defects in the kernel's high-risk areas, each closed with a measured control - #306
Merged
Conversation
…t literal The five naked entry stubs were the last GS accesses in the kernel spelling a displacement as a number: eight in `arch::syscall`'s entry (the stack switch, its three diagnostic stores and the preempt bracket), four in the Ring 0 timer's re-arm and its need_resched/fire-count stores, and two each in `device_irq`, `common_entry` and the shootdown entry. Each now takes the matching `arch::percpu::OFF_*` as a `const` operand, which a naked stub accepts exactly as an ordinary `asm!` does — `arch::idt::nmi` had been doing it since it was written. `OFF_KERNEL_RSP` is new, for the one field no Rust access named. With no literal left, the 20 `const _: () = assert!(OFF_x == N)` protect nothing and are deleted with the 27 `// offset N` field comments — a third copy of the same numbers. The hazard those existed for was never the reorder they catch: it was a field edit made *with* the asserts updated, which left the 18 asm sites reading the wrong bytes with no diagnostic at all. `_pad200` goes too. It was reserved because "dropping these 8 bytes shifts all of them" — every field below it was reached by a literal — and now nothing is. Its removal is what proves the unification rather than asserting it. Negative control, measured: this whole change reverted onto its base, with `_pad200` deleted and the 20 asserts updated to the shifted offsets — the careful edit the old shape invites — reds. The syscall entry's `lock add dword ptr gs:[240]` then lands on `last_seen_ring0_fires`, and init dies in boot with `scheduler entered while a lock is held: preempt depth 4294966880, baseline 0` at `syscall_entry+0x88`. `ALONE syscall_cost: red again, the same failure both times`. Independent oracle, the emitted machine code rather than the source: the 33 `gs:` instructions across `syscall_entry`, `common_entry`, `timer_entry`, `tlb_flush_entry` and the six `device_irq` stubs, disassembled before and after with llvm-objdump, are the same instructions in the same order with every displacement moved by exactly the 8 bytes `_pad200` occupied — 0xf0 to 0xe8 for the preempt count, 0xd8 to 0xd0 for `syscall_rip`, 0x10 and 0x18 unmoved because they sit above the padding. No instruction changed shape, so the `const` operand really is the immediate-displacement form the literal was. Green: syscall_cost, irq_census_conservation, sched_stress, futex_wake_counts, cargo test --lib. Closes issues/design-debt/percpu-asm-contract-is-unbound.md's first half; the two PerCpu riders that entry says must not precede this follow.
The two riders the entry made wait for the unification, now that a field edit
moves nothing but the field.
`lapic_id` had zero readers: written once and never read, while every other
`lapic_id` in the kernel is the *parameter* of `alloc_percpu`, `init_bsp` or
`alloc_ap`. The field goes, and with it the parameter of the two functions that
carried it only to store it — `init_bsp` keeps its own, which its identity log
line prints, and `smp`'s AP loop still names the lapic id it sent the
INIT-SIPI-SIPI to.
`alloc_percpu` set 8 of the struct's fields and left the other 14 to
`alloc_zeroed`. It is one `ptr::write(PerCpu { .. })` now, so a field added
tomorrow does not compile until somebody says what it starts as — which is the
question `current_tid`/`current_pid` answer with `u32::MAX` and a zeroed
allocation would have answered with thread 0 of process 0. `init_tss_descriptor`
stays after the write, because a TSS descriptor holds the address the block was
allocated at.
Green: sched_stress (an SMP boot, so every AP takes this path),
irq_census_conservation, syscall_cost, cargo test --lib.
Closes issues/design-debt/percpu-asm-contract-is-unbound.md. No citation of the
slug or the path exists anywhere in the tree, and `src/redlist.rs` has no row
sourced to it. What the entry was for now lives where it is enforced: the
`OFF_*` block's own header in `arch/percpu.rs`, which says every access spells
a constant and none spells a number.
The panic handler recovered — killed the current process and rejoined the
scheduler — where `syscall_rip() != 0 && current_tid().is_some()`. Nothing ever
cleared `syscall_rip`, so on any CPU that had served one syscall the first
clause was permanently true, and every panic in IRQ context with a task current
read as that task's syscall panic.
`PerCpu::syscall_task` is what the question actually needs: the pid and tid of
the task this CPU entered a syscall for, or a sentinel. `syscall_handler`
brackets the dispatch with it, and `in_syscall()` holds only while the recorded
identity is the one this CPU is running. Pid *and* tid, because a tid is
per-process and `Tid(0)` is the main thread of every process on the machine.
It is deliberately not a guard type: a kernel panic does not unwind, and the
panic handler must find the bracket still open. A thread that leaves through
`SYS_EXIT` and never returns leaves an identity no live task has, which reads
false the moment anything else runs.
The predicate errs one way and it is the safe one — a syscall that parked and
resumed on a CPU that finished somebody else's in between answers false, so a
panic there halts and reports rather than recovering. The `false` direction
costs a report; the `true` direction kills an innocent process. Making that
case answer true means saving and restoring the word across a context switch as
`preempt_count` is, which is `hw::KernelHw::switch`'s to do and not this.
The crash report's `Syscall:` block moves to the same predicate. That is the
second half of the entry: the block was printed off the stale word and then
`user_backtrace` walked an equally stale `syscall_rbp` through the *current*
address space.
Negative control, measured — a `panic!` staged in `timer_handler`, five Ring 3
timer fires in, on both arms of the same session:
base: the process is killed and the machine runs on — `FAIL
rs::allocator_stress: exit code -1` — and the report carries
`Syscall: num=63 user_rip=0x1000003a036` with a user backtrace into
`dlmalloc::malloc+0x776`: a syscall that had already returned, named
as the context of a timer interrupt.
fixed: `PANIC ... md2 control` with no `Syscall:` block, and every CPU
halted — "the guest went quiet because every CPU is halted".
Independent oracle: the recorded real failure this entry was reopened on
(2026-08-20, a 12-wide `bootable.img` boot-storm capture of a kernel death),
where the same stale block printed `Syscall: num=90 user_rip=0x1000003d458`
and the backtrace off `syscall_rbp` faulted — `FAULT rip=... cr2=0x0 ...
RECURSIVE` — losing the rest of the report. The control arm above reproduces
its first half from a staged panic rather than from a boot storm.
Green: syscall_cost, allocator_stress, handle_kill_policy,
cargo test --lib.
`src/prose-ledger` gains three rows deliberately: `arch/percpu.rs` 501 -> 535
(this field, its four accessors, and the whole-struct initialiser two commits
back, which is this pull request's), `arch/idt/exceptions.rs` 192 -> 195 and
`arch/syscall/gate.rs` 90 -> 95.
Closes issues/panic-path/syscall-rip-never-cleared.md. Its two citations went
with it: `arch/idt/exceptions.rs` and `sched/kthread.rs`, both of which now
state the predicate rather than the defect. No `src/redlist.rs` row is sourced
to it.
…efore it acknowledges `apic::enable_x2apic` writes `0xFF` into the spurious-interrupt vector field on the BSP and on every AP, and the IDT left slot 0xFF `IdtEntry::EMPTY` — `P = 0`. A vector the CPU can deliver through a non-present gate is a contributory fault and the CPU escalates to `#DF`, which halts the machine: the rule `9bd7a9e` wrote above `idt_vectors!` for the range Intel names, on the one vector the platform names instead. `arch/idt/spurious.rs` is the gate. It is `ring0` — it reaches no task, touches no preempt count and reschedules nothing — and it does not log, for `arch/idt/nmi.rs`'s reason: it can arrive inside the log's own commit bracket. What it does is the census's single `add` and then the difficult part, which is the acknowledgment: a genuine spurious interrupt sets no ISR bit (SDM Vol. 3A §11.9), so an unconditional `eoi()` would clear an unrelated interrupt's bit and lose it, while the same vector reached by an IPI does go through the IRR and does need one. The handler reads the in-service register and acknowledges only what is in service. `irq_census::Source::Spurious` is where the count lands, because the handler cannot say anything itself. A non-zero column on a machine that staged nothing is an interrupt-routing defect, and this is its only witness. The gate is exercised on every run rather than shipped unentered. Nothing on this host raises the vector by itself — the SDM's classic condition needs a task-priority register this kernel never writes, and every device here is MSI or MSI-X — so `lapic-spurious-selftest` raises it deliberately and reports three things: that the delivery arrived and was counted, that the vector is no longer in service afterwards, and that the CPU went on taking interrupts. Two negative controls, each measured on its own arm of the same session: the gate removed, the delivery still staged: the guest never reaches `===READY===` and the harness reports `the console carried: nothing at all` — the halted machine with no name on it, which is exactly the defect. the gate kept, the acknowledgment removed: `LAPIC: spurious selftest FAILED — vector 0xff is still in service, so nothing below priority 0xF can be delivered on cpu0 again`, from the kernel's own arm. Independent oracle: Intel SDM Vol. 3A — §11.9 for what a spurious interrupt is and that it needs no EOI, §11.8.4 for an in-service bit blocking every lower priority, and §6.14's contributory-fault table for the escalation a `P = 0` gate produces. The tree's own recorded failure is the same mechanism on the exception range: `9bd7a9e`'s `div` by zero took the whole guest down before every Intel-named vector had a gate. `lapic_spurious_vector` is a new registered name, `Sched::Parallel`, `Tier::Fast`, committed to `tests/test-durations` with the `UNMEASURED` marker — the one measured run it buys is by design, and the price verdict lands on the run that measures it. Green: lapic_spurious_vector, irq_census_conservation (whose host-side `SOURCES` gains the same column), cargo test --lib. `src/prose-ledger`: `arch/idt/spurious.rs` enters at 50; `arch/idt/mod.rs` 223 -> 235, `irq_census.rs` 117 -> 125, `main.rs` 328 -> 329, `actuator.rs` 436 -> 442, `arch/apic.rs` 251 -> 263, `tests/toyos.rs` 4777 -> 4794. The dated column is untouched. Closes issues/kernel/the-lapic-spurious-vector-has-no-gate.md. Its one citation, in `a-double-fault-on-cpu-1-under-a-wide-suite.md`, is updated in this commit: that reading is closed and was never the claim. The entry's last paragraph — the other 235 empty slots — is not this vector's defect and is filed as issues/kernel/an-unclaimed-vector-halts-the-machine-with-no-name.md, with the control above as its evidence. No `src/redlist.rs` row is sourced to either.
…s and a hope
`bot` took `(data_phys: u64, data_len: u32)` and asserted `data_len <=
MSC_DATA_LEN` — 32 KiB — while four of its five call sites pointed at
`MSC_SCRATCH`, which is 64 bytes. The bound was in the right place with the
wrong operand: nothing related the length the device was told to move to the
buffer it was told to move it into.
The pair is one `DataPhase = Option<Dma<'static>>` now. The CBW's
`dCBWDataTransferLength` is the region's own `size()`, so a command cannot name
a length its destination does not have, and `None` is a command with no data
phase rather than a zero somebody has to read as one. `MSC_DATA_LEN` is still
asserted and now says the only thing it can: this driver rings at most 32 KiB
per transfer.
The two scratch users take a 64-byte region and narrow it — `Dma::subview` is
what refuses `read_scratch`'s `want` at the buffer, in the type that owns the
bound, rather than against a constant declared somewhere else.
Negative control, measured, both arms in one session: an INQUIRY staged to ask
for a 4,096-byte data phase into the 64-byte scratch.
base: `usb_storage_gate` **green**. The transfer was programmed, the stick
bound, the volume mounted, and nothing anywhere said a word.
fixed: `DMA: 4096 byte(s) at 0x0 run past a region of 0x40, in the region at
0xffff800001612080` — refused at the buffer, before anything reached a
ring.
Independent oracle, USB Mass Storage Bulk-Only Transport 1.0: §5.1 makes
`dCBWDataTransferLength` the number of bytes the *host* expects to transfer, and
§6.7.2 licenses the device to send exactly that many before the CSW. So a host
that names a length its buffer does not have has authorised the overflow; the
device declining to use it is the device's choice, not the driver's bound. That
is also why the base arm is green — QEMU's stick answers INQUIRY with 36 bytes
and short-packets — and why no test in this tree could have caught it.
Green: usb_storage_gate, usb_storage_shapes, usb_transport_break (all nightly
tier), usb_short_read, usb_storage_write_error, cargo test --lib.
`src/prose-ledger` raises `drivers/xhci/wait/msc.rs` 548 -> 561 deliberately.
Closes issues/filesystem/bot-length-assertion-binds-another-buffer.md — the USB
storage type-safety audit's F6. No citation of the slug or the path exists
elsewhere in the tree, and no `src/redlist.rs` row is sourced to it.
…t is pulled
A Device Slot is the controller's from the moment Enable Slot answers, and only
Disable Slot gives one back. The unplug half was closed: `teardown_port`
disables the slot a port carries. The half that stayed open is a device that is
refused and *stays plugged in* — a hub, a camera, a fingerprint reader, a disk
with no bulk pair, one the pool has no block for, one whose bring-up fails —
which kept its slot for the life of the boot, on eleven paths.
`device::refuse` is the one exit those paths take now: it marks the port
attached with no slot and submits Disable Slot with `AfterSlot::Refused`. Two
things it deliberately does not do. The port stays *attached*, which is
`let_go`'s answer one stage earlier — a port that read as unattached would
enumerate the same refused device again on every debounce. And the pool blocks
stay with the port until the unplug, because the port still has a device in it
and `teardown_port` is where they are released; only the slot is a resource the
controller is short of.
Where a class driver is the one refusing, it has to say so, so `msc::bind` and
`bind_hid` answer `bool`: SET_CONFIGURATION failing, Configure Endpoint failing
for a bulk pair or for a HID interrupt endpoint, a pointer past
`PointerSource::claim`'s table, and a stick that never becomes ready are
refusals that used to end in the same `finish` a bound device does.
Gate and negative control are the same test, `xhci_slot_exhaustion`, which
stages a controller clamped to one device block on a six-device bus. It gains
one assertion — that every refused device's slot is given back — and that
assertion is measured in both directions in one session:
base: `FAIL xhci_slot_exhaustion: 0 slot(s) disabled for 5 refused
device(s)` — the test that describes the leak was its largest producer.
fixed: green, 5 for 5.
Independent oracle, xHCI 1.2: §4.6.4 makes Disable Slot legal from any slot
state, which is what lets a refusal issue it against a device still in its port
and is why no path here has to reason about what state the device reached;
§4.5.1 and the Enable Slot semantics in §4.6.3 make the slot allocated at the
command's completion whatever becomes of the device. Every path between a
successful Enable Slot and a bound device was enumerated by the USB storage
type-safety audit (F12) rather than by this change, and the count is the same
eleven.
Green: xhci_slot_exhaustion, xhci_many_devices, xhci_full_speed_device,
xhci_hotplug, usb_storage_gate, usb_refused_disk_first, cargo test --lib.
`src/prose-ledger` raises four rows deliberately: `xhci/device.rs` 262 -> 279,
`xhci/mod.rs` 948 -> 956, `xhci/wait/msc.rs` 561 -> 562, `tests/toyos.rs`
4794 -> 4799.
Closes issues/hardware/xhci-slot-never-given-back.md. No citation of the slug or
the path exists elsewhere in the tree and no `src/redlist.rs` row is sourced to
it.
… stall one There was no `restart_endpoint` for a control endpoint, so a stall halted EP0 for good. Two places reach that state and both are now answered. **The enumeration.** QEMU's `usb-wacom-tablet` stalls SET_PROTOCOL on every boot of the full-speed bus, and the driver binds it anyway — that tolerance is deliberate and stays. What did not stay is going on with EP0 halted: `toyos_xhci::enumerate` learns `Stalled` from the act and answers with two commands of its own, Reset Endpoint and Set TR Dequeue Pointer on DCI 1. Both, because Reset Endpoint alone leaves the controller's dequeue pointer on the TRB that stalled and the next control transfer re-runs it. No packet goes out: the device clears its own half on the next SETUP. **Bulk-Only Reset Recovery.** If the class request itself stalls, EP0 is halted and the two CLEAR_FEATUREs behind it are control transfers that can no longer run — so a disk that broke once could never be recovered. `control_transfer` now recovers EP0 before it reports a stall, which is the only place that knows one happened, and every blocking caller inherits it. `run_recovery` is the loop `quiesce_endpoint` had, lifted so EP0 reaches the same `Recovery` sequence as the bulk and interrupt endpoints; `restart_control_endpoint` is the entry point that drops the device half. `EP0_DCI` is declared once. Every device's EP0 ring is at `DEV_EP0_RING` inside its own device block, which is what lets a caller supply the block and nothing else. Gate and negative control are the same test, `xhci_full_speed_device`, which boots the bus the stall really happens on. It gains two assertions — that exactly one SET_PROTOCOL stalled, and that EP0 ran again after it — measured in both directions in one session: base: `FAIL xhci_full_speed_device: EP0 was left halted behind the stall` fixed: green, and the driver says `EP0 on port 6 runs again after the stall`. The sequence half is gated where it is decided: two host tests in `toyos-xhci/src/enumerate.rs` assert that a stalled SET_PROTOCOL produces exactly `ResetEp0`, `SetEp0Dequeue`, `ConfigureEndpoint` in that order, and that a device which answered pays for neither. 41 pass in that crate. Independent oracle, two specifications rather than this tree's reasoning: USB 2.0 §9.4.5 does not define the Halt feature for the default control pipe and §8.5.3.4 has the device clear the condition on the next SETUP — which is why there is no CLEAR_FEATURE here and why sending one would be a request over the endpoint that is halted. xHCI 1.2 §4.6.8 puts a stalled endpoint in Halted and names Reset Endpoint as what leaves it, and §4.6.10 is why Set TR Dequeue Pointer has to follow. Green: xhci_full_speed_device, xhci_slot_exhaustion, xhci_hid_break, usb_short_read, usb_transport_break, usb_storage_gate, `cargo test` in toyos-xhci, cargo test --lib. `src/prose-ledger` raises five rows deliberately: `xhci/device.rs` 279 -> 289, `xhci/mod.rs` 956 -> 959, `xhci/wait/mod.rs` 219 -> 243, `tests/common/usb.rs` 948 -> 954 (its dated column is untouched at 4), `toyos-xhci/src/enumerate.rs` 106 -> 126. Closes issues/filesystem/control-stall-halts-ep0.md. No citation of the slug or the path exists elsewhere in the tree, and no `src/redlist.rs` row is sourced to it.
The entry was filed about the panic console, whose cached framebuffer address a
driver frees under it, and that half had already moved into `gpu::set_resolution`
— it blinds the console across the window and rearms it if the driver refused.
What the entry says next is the half that was still open: "the pattern is simply
unguarded for anything that caches the address", and two things do.
`device::set_framebuffer_info` holds the description the *next* framebuffer
claim is answered with — its regions and its geometry — and it was written once,
at registration, by a driver that then changed the mode without it. Its own
comment says the second consumer out loud: the absolute pointer's per-axis scale
"is a function of the screen and has to follow a mode change", and nothing made
it. So after a successful resize the next claimant is handed the regions of the
buffer that is no longer scanned out, at the geometry before last, and a tablet
maps its coordinates onto a screen that is not there.
`gpu::set_resolution` calls it now, which is what owning the invalidation means:
a caller doing it for itself is a caller that has to know the driver freed
something. `gpu::screen` is the one constructor of that description, so
registration and a mode change cannot describe the same screen differently —
`register_gpu` was the second copy and is now three lines.
Negative control, measured, both arms in one session. No configuration the
harness boots can resize at all — measured, and it is why nothing here caught
this: `md2 probe: the resize answered Err(NotSupported)`, because every guest
takes the UEFI GOP path and GOP cannot change mode after boot services exit,
while virtio-gpu — the one driver that can — is on no profile in
`tests/common/qemu.rs`. So the control stages a GOP that accepts a mode change
and asks the registry what the next claimant would be told:
base: `the resize answered Ok((800, 600)) and the registry says
Some((2048, 2048))`
fixed: `the resize answered Ok((800, 600)) and the registry says
Some((800, 600))`
Independent oracle, an in-tree differential rather than an argument: the two
paths that answer "what is on this screen" are the resize's own return value,
which the compositor reads (`userland/compositor/src/session.rs`), and the
device registry, which every later claim reads. They are answers to one question
and the control above is them disagreeing.
**No committed gate, and that is a property of the harness rather than a
choice**: a successful resize is unreachable from any guest it can boot, which
the first measurement above establishes. Filed as
issues/diagnostics/no-guest-can-change-the-display-mode.md so the gap is tracked
rather than implied.
Green: diskless_boot, screen_log_absent, metal_sim_input, cargo test --lib.
`src/prose-ledger` raises `kernel/src/gpu.rs` 19 -> 33 deliberately.
Closes issues/design-debt/set-resolution-frees-a-live-framebuffer.md. No
citation of the slug or the path exists elsewhere in the tree, and no
`src/redlist.rs` row is sourced to it.
`toyos-abi`'s `futex_wait` says "Returns 0 on wake, 1 on timeout" and the kernel could produce only the first: both arms of `process::futex_wait` returned 0, and under them `scheduler::futex_wait` returned a bare `true` for everything — `completion::wait_until` answers `Ok(())` for a satisfied predicate and for an expired deadline alike. So a caller could not tell a timeout from a wake, and it broke silently, because the honest answer and the wrong one were the same number. The two are told apart by the word itself, which is what the wait was armed on: after `wait_until` returns, the predicate runs once more. A word that no longer holds `expected` is the wake this wait was for — or a writer that got there first, or the frame going away — and a word that still holds it is a wait nothing it was armed for has ended, which leaves the caller's own deadline. `scheduler::futex_wait` answers a named `FutexEnd` rather than the `bool` it had, which claimed "it parked": a question no caller asks, answered `true` whether or not it had. This is the entry's second option, and its own words about it are right — "the word may have changed and changed back". That reads as a wake, which is also what the ABI answers a caller who was woken and found the word back where it was: a `futex_wait` return is never proof of anything but that the caller must look again. It costs one re-evaluation of a predicate the wait has already run at least once, against `completion::wait_until` growing an outcome and eleven call sites having to say what they do with it. The gate is the third question in `futex_wake_counts`, which already owns this syscall pair, so no new name is registered. Three assertions, because any one of them passes on a kernel answering a constant: a timed wait nobody wakes answers 1, a wait whose word never matched answers 0, and a wait that is woken answers 0. Only the first is timed; the woken arm waits forever and is woken by the test itself, so no margin decides a verdict. Negative control, measured, this kernel change reverted under the same test: `FAIL rs::futex_wake_counts: a futex wait nobody woke answered 0, wanted the timeout`. Independent oracle: the ABI's own line, written before this and never met — `toyos-abi/src/syscall.rs`, "Returns 0 on wake, 1 on timeout" — and behind it POSIX's `pthread_cond_timedwait`, whose `ETIMEDOUT` is the caller this exists for and which cannot be built on a primitive that answers one number. Green: futex_wake_counts, abuse_kernel_addr, sched_stress, cargo test --lib. `src/prose-ledger` raises three rows deliberately: `scheduler.rs` 642 -> 659, `process.rs` 920 -> 922, `futex_wake_counts.rs` 152 -> 167. Closes two entries, which are one defect seen twice: issues/kernel/futex-wait-cannot-report-a-timeout.md and issues/kernel/futex-wait-never-returns-its-timeout-code.md. The second says so itself — it is the doc-comment half of the first. No citation of either slug or path exists elsewhere in the tree, and no `src/redlist.rs` row is sourced to either.
`MetalXhciBoth` exists to put both pointers on the same xHCI slot id of their
own controller, because a slot id was once used as a machine-wide name for a
device and is not one. It balanced the boot stick on the first controller with a
`usb-hub` on the second — and a hub is walked past, so what it contributed was
a *leaked* slot. The commit before last gives that slot back at the refusal, so
the second controller's devices moved down one and the premise went with them:
FAIL xhci_two_controllers: the two pointers are on slots 3 and 2, so a
slot-keyed merge would not have collided and this test proves nothing
That is the test declining to certify rather than a defect it found, and the
staging is what is wrong. A second keyboard on the second controller balances
the stick with a device that *binds*, which is the only kind whose slot survives
its enumeration. The hub stays, walked past as before — and now also exercising
the refusal path that gives a slot back on a bus where something else is
enumerated behind it.
Measured, alone, three arms in one session: on `16c05999` (this branch's base)
both pointers land on slot 3 and the test is green; on the branch with the
old device list they land on 3 and 2 and it reds; with the list rebalanced they
are on slot 3 again and merge as sources 1 and 2.
The machine-wide totals the test asserts move with the device list: 5 HID
devices and three keyboards.
`src/prose-ledger` raises `tests/common/qemu.rs` 1588 -> 1591 deliberately.
`syscall_window_nmi` reds in the wide phase of this branch's `cargo test` — at 1,505 s against a committed 6,825 ms, on a host carrying 92 guests and a second worktree's suite — and its isolated re-run in the same session is green in 5 s with the storm reported in full. `--known-red` answered NOT ON THE LIST, so the next agent to meet it would have read it as theirs. The row is `Instrument::DevHostLoaded`, `Finding::Seen`, no rate: one sighting has no denominator, and this tree does not write a number it did not measure. What the entry beside it says is what would settle it. It is not this branch's: the syscall entry's displacements changed *spelling* and are byte-identical machine code, and the two per-CPU stores the panic path's bracket adds are not a 220x wall stretch. The same tip runs the test green alone.
…reason Placed by the orchestrator with the slot fix that revealed it: MetalXhciBoth balanced its buses with a leaked hub slot, so the fix moved both pointers and the test declined to certify. A staging device must own a resource that survives its own enumeration.
The UNMEASURED marker bought hosted run 33077119849 (guest partition green); it measured lapic_spurious_vector at 6829ms, under the Fast commitment, so the declared tier stands. The rest of the profile is unchanged.
Japabu
marked this pull request as ready for review
August 27, 2026 14:30
Japabu
enabled auto-merge
August 27, 2026 14:30
Both sides raised tests/common/qemu.rs and tests/toyos.rs; the merged file exceeds either arm, so the rows re-record the measured union (1607, 4848), verified by the gate below.
The merge of main brought the reshaped placement signature beside this branch's call and the union borrowed a generic argument the callee takes by value; CI clippy refused it, and this time the kernel clippy ran locally before the push.
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nine tracker entries closed, out of a batch of 32 read for value. Every one is
in an area the workflow calls high-risk — the syscall entry, the panic path,
interrupt delivery, three USB drivers, the display, the futex pair — so each
names its negative control and its independent oracle below, and every control
was measured on both arms in one session rather than asserted.
Three entries are filed rather than fixed, and one existing entry loses a
citation it no longer has.
What landed
arch/percpu: everygs:displacement is derived —design-debt/percpu-asm-contract-is-unbound. The five naked entry stubs werethe last GS accesses spelling a displacement as a number; each takes the
matching
OFF_*as aconstoperand now, the 20const _: () = assert!andthe 27
// offset Ncomments go with the last literal, and_pad200— reservedbecause "dropping these 8 bytes shifts all of them" — goes too. Its two riders
follow in the next commit:
lapic_idhad no readers, andalloc_percpuwritesthe whole struct so a new field cannot default to whatever zero means for it.
careful edit the old shape invites. Init dies in boot with
scheduler entered while a lock is held: preempt depth 4294966880,ALONE: red again.gs:instructions across the stubs are thesame instructions with every displacement moved by exactly the 8 bytes the
padding occupied — the machine code, not the source, saying the
constoperand is the immediate the literal was.
The panic path asks whether this task is in a syscall —
panic-path/syscall-rip-never-cleared.syscall_ripwas never cleared, so onany CPU that had served one syscall the recovery predicate was permanently
true and every IRQ-context panic with a task current killed that task's
process.
PerCpu::syscall_taskrecords the pid and tid of the task the bracketwas entered for;
in_syscall()holds only while that is the task running.panic!staged intimer_handler. On base the process is killedand the machine runs on, and the report carries
Syscall: num=63 user_rip=…with a user backtrace intodlmalloc::malloc— a syscall that hadalready returned. With the fix, no
Syscall:block and every CPU halted.12-wide boot-storm capture), where the same stale block's
user_backtracefaulted and took the rest of the report with it.
Vector 0xFF has a gate, and it asks before it acknowledges —
kernel/the-lapic-spurious-vector-has-no-gate.enable_x2apicwrites 0xFF intothe SVR on every CPU and the IDT left the slot
P = 0. The handler counts thedelivery in the interrupt census — it may not log, for
idt::nmi's reason — andEOIs only what the in-service register says is in service.
never reaches
===READY===andthe console carried: nothing at all; withthe gate kept and the acknowledgment removed, the self-test says
vector 0xff is still in service, so nothing below priority 0xF can be delivered again.needs no EOI), §11.8.4 (an in-service bit blocks every lower priority),
§6.14's contributory-fault table for the escalation a
P = 0gate produces.lapic_spurious_vectoris a new registered name, Fast, committed totests/test-durationswith theUNMEASUREDmarker.A BOT command names the region its data phase lands in —
filesystem/bot-length-assertion-binds-another-buffer.botasserteddata_len <= MSC_DATA_LEN(32 KiB) while four of five call sites pointed at a64-byte scratch. The pair is one
Option<Dma<'static>>; the CBW's length is theregion's own size.
On base
usb_storage_gateis green — the transfer is programmed andnothing says a word. With the fix:
DMA: 4096 byte(s) at 0x0 run past a region of 0x40.device is licensed to send
dCBWDataTransferLengthbytes, so a host thatnames a length its buffer does not have has authorised the overflow. It is
also why the base arm is green: QEMU's stick answers with 36 bytes.
A refused device gives its slot back where it is refused —
hardware/xhci-slot-never-given-back. Eleven paths kept a Device Slot for adevice still in its port.
device::refusedisables it and leaves the portattached, so nothing re-enumerates what it just refused;
msc::bindandbind_hidanswerboolso a class driver's refusal is one too.xhci_slot_exhaustiongains the assertion, and on base it reads0 slot(s) disabled for 5 refused device(s)— the test that describes theleak was its largest producer.
what lets a refusal issue it against a device still in its port) and §4.6.3.
A stalled control transfer leaves EP0 running —
filesystem/control-stall-halts-ep0. Two paths reach a halted EP0: thetolerated SET_PROTOCOL stall during enumeration, answered by two commands the
pure sequence now owes, and Bulk-Only Reset Recovery, where the CLEAR_FEATUREs
behind a stalled class request could no longer run.
control_transferrecoversEP0 before it reports a stall.
xhci_full_speed_device— the bus where QEMU's tablet reallystalls — gains the assertion, and on base reads
EP0 was left halted behind the stall. The sequence half is gated where it is decided: two host tests intoyos-xhci.the device clears it on the next SETUP) with xHCI 1.2 §4.6.8 and §4.6.10
(Reset Endpoint leaves Halted, Set TR Dequeue is why it must follow).
set_resolutionreplaces every description of the mode it changed —design-debt/set-resolution-frees-a-live-framebuffer. The device registryanswered the next framebuffer claim with the regions and geometry of the mode
before last, and the absolute pointer's per-axis scale — whose own comment says
it "has to follow a mode change" — followed nothing.
the resize answered Ok((800, 600)) and the registry says Some((2048, 2048)). Fixed:Some((800, 600)).compositor reads, against the registry every later claim reads. Two answers to
one question, and the control is them disagreeing.
profile attaches a virtio-gpu, so every guest takes the GOP path and cannot
resize at all — measured, and filed.
SYS_FUTEX_WAITanswers the timeout its ABI documents —kernel/futex-wait-cannot-report-a-timeoutandkernel/futex-wait-never-returns-its-timeout-code, one defect seen twice. Thepredicate runs once more after the wait: a word that still holds
expectedis await nothing it was armed for ended.
a futex wait nobody woke answered 0, wanted the timeout.on wake, 1 on timeout" — and POSIX's
pthread_cond_timedwait, which cannot bebuilt on a primitive that answers one number.
futex_wake_counts, so no new name isregistered: three assertions, because any one alone passes on a kernel
answering a constant.
Two things this branch had to answer for
xhci_two_controllersreds on the slot fix, and the staging is what waswrong.
MetalXhciBothbalanced the boot stick with ausb-hub— a devicethat is walked past, so what it contributed was a leaked slot. Rebalanced with a
device that binds; measured on three arms (base green on slot 3, branch red on
3 and 2, rebalanced green on slot 3 again).
syscall_window_nmireds at 1,505 s against a committed 6,825 ms in a92-guest run sharing the host with another worktree's suite, and is green alone
in 5 s in the same session.
--known-redsaid NOT ON THE LIST; it now carries aDevHostLoadedrow with no rate, sourced to a new entry that says what wouldsettle it.
exit_wait_stormreds in the same phase and was already on the list.Filed rather than fixed
kernel/an-unclaimed-vector-halts-the-machine-with-no-name— the other 235P = 0slots, with the spurious gate's own control as its evidence.diagnostics/no-guest-can-change-the-display-mode— no harness profile canreach a successful resize.
build/syscall-window-nmi-reds-under-a-shared-host— the red above.Gates
cargo test --libgreen (180).cargo test --workspace --exclude toyos-buildgreen: 1,081 passed, 0 failed.
cargo test(Fast tier, 288 names) twice: thefirst run at 92 guests beside another worktree's suite is 285 passed and the
three reds above — one this branch's and fixed, two contention. The second, on
the tip with the staging rebalanced and nothing else on the host, is 288
passed, 0 failed in 108 s.
src/prose-ledgergains deliberate raises in the same commits as the prose thatearned them, and one new row for
arch/idt/spurious.rs. The dated column isuntouched throughout.