Skip to content

A claimed function is armed on the mechanism it publishes, and the arming says which case it refused - #443

Open
Japabu wants to merge 17 commits into
mainfrom
msi-claim
Open

Japabu wants to merge 17 commits into
mainfrom
msi-claim

Conversation

@Japabu

@Japabu Japabu commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

A PCI function handed to a userland driver was armed on exactly one interrupt
mechanism: pcidev::bring_up called enable_msix(...).ok_or(Refusal::NoMsix)?,
so a function publishing no MSI-X capability was refused by name and its holder
never ran. Every function this project had handed to a process so far was a
virtio one and every one of those has MSI-X, so the refusal had only ever been
reached by virtio_net_no_msix's deliberate vectors=0.

The ThinkPad T14's onboard NIC is not one of those. Booted on the bench (run 28,
tip=220305b4), this kernel wrote, 1.349 s in, pcidev: PCI 00:1f.6 NOT HANDED OVER — its MSI-X could not be armed, and a claim with no interrupt is a driver that would never be told anything; netd found no endowment and exited, and the
bench's one cable carried no ToyOS.

The owner's direction of 2026-09-08 is the rule this implements: the LAN
uses the most modern interrupt mechanism the device offers and never INTx.
MSI-X first wherever a function has it; MSI only where it has none — the T14's
I219 publishes no MSI-X capability, so there MSI is the most modern there is.
INTx is never armed, and nothing in this branch adds a path that could arm one.

What the kernel does now

bring_up is one match over what the arming answered, and it walks no
capability list of its own:

let armed = match pci.enable_msix(VECTORS[slot]) {
    Ok(entry) => Armed::Msix(entry),
    Err(Unarmed::Unusable) => return Err(Refusal::MsixUnusable),
    Err(Unarmed::Blocked) => return Err(Refusal::NoInterrupt),
    Err(Unarmed::NoTable(NoCapability::Truncated)) => return Err(Refusal::CapsTruncated),
    Err(Unarmed::NoTable(NoCapability::Absent)) => {
        pci.enable_msi(VECTORS[slot]).then_some(Armed::Msi).ok_or(Refusal::NoInterrupt)?
    }
};

enable_msix answers Result<Mmio, Unarmed> and splits the three ways it can
fail where each is known, so no caller re-derives it and no refusal asserts a
reason false on the path that raised it:

  • Unusable — a table this kernel could not reach: Msix::decode failed, the
    BIR is not a memory BAR, or table_address failed. Refusal::MsixUnusable
    says the function publishes MSI-X, this kernel could not arm it, and MSI is
    not a fallback for a function that has a table.
  • Blocked — the unit refuses this function's message (iommu::remap_msi).
    enable_msi calls the same private message() with the same arguments and
    fails identically, so Refusal::NoInterrupt is exactly what happened.
  • NoTable(Absent) — a walk that reached the capability list's terminator and
    found no MSI-X. This is the only case that reaches the MSI arm.
  • NoTable(Truncated) — a walk that stopped at a link the PCI spec forbids.

Truncated is the security case, and it was the branch's own hole. The walk
is caps::CapWalk, which ends at a misaligned pointer, one below the standard
header, or one already visited. Until cad60ec6 a function whose MSI capability
preceded such a link and whose MSI-X capability followed it read as absent,
was armed on MSI, and msix_bar — the same blind walk — named no BAR, so
place_bars withheld none and the MSI-X table's BAR went to the holder with
everything else. A holder that can write that table points the device's message
at any address the LAPIC decodes. On the base that same function is refused
NoMsix and reaches no holder, so the regression was this branch's and is
closed in it.

PciDevice::capability(id) is the one spelling of "find this function's
capability by id". It replaced six copies of
capabilities().find(|c| c.id() == …), three of them in drivers/pci.rs;
grep -rn '\.capability(' kernel/src counts 8 callers today.

What the reviews changed

1. The truncation refusal is classified where the walk is already driven.
The branch had priced two holes as one. Arming a claimed function on MSI needs
silicon or a boot config; the Truncated/Absent split needs neither.
kernel/src/drivers/virtio.rs's cap_selftest already drove the real walk over
a cyclic list, a misaligned link, a below-header link and a forbidden head
through PciDevice::over_config, so each case now also reads back what the
walk's end made of a capability, and prints its own verdict line,
pci cap split. pci_capability_walk is Tier::Fast, so that half is on the
per-pull-request gate and cost no new registration at all.

2. The arming's own answer is read back, not only capability's. bring_up
matches on enable_msix, so each walk case now asserts device.enable_msix(0)
as well as device.capability(msix::CAP_ID). No layout there publishes an MSI-X
capability the walk reaches, so the call returns at its lookup and touches no
MMIO.

3. A case that both finds a capability and truncates, and holds a real table
past the forbidden link.
Every layout published only PCI_CAP_ID_VENDOR, so
Ok(_) => "found" was unreachable and the split only ever separated absent from
truncated. The row is now
&[(0x40, msi::CAP_ID, 0x43), (0x44, msix::CAP_ID, 0)] — a capability the walk
reaches, a link the spec forbids, and an MSI-X capability past it. With only the
first tuple, capability(msix::CAP_ID) answered Truncated over a config space
holding no MSI-X capability anywhere, so the case could not tell a walk stopped
at the forbidden link from one that reached the end of a list with no table;
with the second, truncated means "there is one and it was not read", which is
the discrimination the refusal exists for. CASES stays 15 and SPLITS 13: the
row joins a case rather than adding one.

4. MSI-X first is asserted where a function actually publishes both.
tests/e1000case/system.toml hands netd pci:8086:10d3 — QEMU's e1000e,
which publishes MSI at 0xd0 and MSI-X at 0xa0 (read off the device model over
0xcf8/0xcfc at .github/qemu-version's 11.1.0) — and https_tls13_e1000e is
Tier::Fast. iommu::armed_on_msix takes the vendor:device the boot config
declares (Bench::claims) and requires msix address= for the slot that claim
is on while refusing msi address= there. Point 17 is where that slot stopped
being whichever one the guest printed.

5. A vacuous assertion is gone, for the second time.
pci_function_is_exclusive asserted must_not_say(MSI_ARMED) on 00:03.0,
whose capability list is MSI-X at 0x98 and five vendor capabilities — no id 0x05
anywhere — so no mutation of this kernel can print that line. 94341ce1 had
deleted the identical assertion from tests/common/iommu.rs for that reason and
it returned one file over. Its must_say(MSIX_ARMED) neighbour goes too: on a
function whose only armable mechanism is MSI-X a hand-over is the msix line,
and the handovers != 1 refusal above already requires one. MSI_ARMED is
deleted, and the msix address= line has one spelling — faults::msix_armed(),
read by the arm that requires it and by every judge that requires its absence
(point 17, which is where the second spelling of it was finally closed).

6. The refusals the deleted register carried are recorded again. 76fb456d
deleted pcidev's "What is read back, and what is not", and the two issue files
this branch filed recorded only the MSI arm and the configuration write.
issues/kernel/a-claims-own-refusals-are-read-by-nothing.md records the rest:
ClaimError::Ambiguous, KernelDriven and Exhausted, every window refusal,
and every bound SYS_DEVICE_BAR_MAP and SYS_DEVICE_DMA_ALLOC check.

7. kind: defect over a body saying nothing has ever run the write.
issues/hardware/toyos-i219-refuses-a-part-outside-msi-x-mode-at-ivar.md is a
finding; issues/README.md gives defect as "real, reproducible".

8. The kernel-driven fallback says once why it is safe.
hda::arm_interrupt and xhci::wait::boot::arm_interrupt still take MSI on
whatever a truncated walk reached. PciDevice::enable_msi's own doc is where
that is said, because it is the call both of them make: a driver in this kernel
hands no BAR of its function to a holder, so an MSI-X table past a forbidden
link is one nobody but this kernel could reach. Neither site restates it.

9. The truncation refusal has a judge, and the branch no longer carries a
privilege path a one-field mutation cannot see.
Round 3 reported that
reverting bring_up's two NoTable arms left every tier green, and the tracker
priced the alternative at "a boot config carrying such a function and a test
binary to hold it, plus that boot's tier row and CI price". That price is wrong
twice: the arm under test is a refusal, so no holder is needed, and the
kernel's own actuator machinery reaches inside this arming path already.
pcidev-caps-truncated is one actuators! row; StagedCaps in
kernel/src/drivers/pci.rs holds one requester id across the hand-over's own
walks and CapabilityIter::next ends that function's list one byte off dword
alignment after its MSI capability, leaving every other walk in the machine
reading the list the device published. pci_claim_caps_truncated
(Tier::Fast, tests/common/faults.rs) boots tests/e1000case under it and
reads faults::refused_claim — the refusal on the known slot by its own reason,
neither msi address= nor msix address= for that function, no hand-over of
[8086:10d3], no BAR moved, init's refusal line — then netd's exit,
Boot: complete and a clean console. Mutation 1 below is the negative control
it was added with.

10. The vacuous assertion is deleted a third time, by ruling.
iommu::armed_on_msix was called from tls13_judge for both benches, so
must_not_say("PCI 00:03.0: msi address=") ran on every https_tls13
1af4:1041 publishes no id 0x05, so that arm is green on every implementation
of this kernel. It is the identical arm 94341ce1 deleted and eea8c7f7
deleted again, reinstated one call site further out. The orchestrator's round-4
ruling is that it is deleted and stays deleted. There is no must_not_say of an
msi address= in tests/common/https.rs at all now: tls13_judge answers the
boot console, and the https_tls13_e1000e registration — the one bench whose
claimed function publishes both mechanisms — makes the assertion.

11. Bench::claims is read against the file it names. It restated
tests/e1000case/system.toml's devices row in Rust, which is the drift
tests/common/devices.rs's the_config_runs_exactly_these_jobs exists to
prevent. every_bench_claims_what_its_config_declares follows that pattern: it
reads each bench's system.toml with toml::from_str, the parser
load_audio_baseline already reads tests/audio-baseline.toml with, indexes
programs.netd.devicesnetcase declares the same function twice, once for
netd and once for the test binary that asks for a second claim — and asserts the
constant against that row.

What round 4 changed

12. One refused-claim judge, where there were three copies.
faults::claim_caps_truncated had been written beside two arms that already did
the job: virtio_net_no_msix and iommu::no_unit_is_no_claim each asserted the
refusal line, the reason, no hand-over, no BAR, init's line. refused_claim(log, claims, why) is that judge and all three call it, so a kernel that answered a
refusal by logging it and handing the function over anyway is red wherever the
refusal is reached. What stays at a caller is that arm's own: the argv check and
the audio function for the crippled NIC, the actuator's parameter for the
truncated list, and — on the machine with no unit — that nothing at all was
handed over, which is more than the claim's own refusal says.

13. The refused function's address is the harness's, never the guest's.
The judge read the BDF out of the console and then asserted about that, which
is an assertion about whichever function the kernel happened to name. It knows
the profile's slot (00:03.0), refuses a console that refused any other
function, and anchors each line to it: pcidev: PCI 00:03.0 NOT HANDED OVER — <reason> is one anchored match rather than two unanchored ones. The control
below shows it working — the base kernel refuses the same function with a
different reason and the anchored line reds on it.

14. One spelling of each thing the judge reads. NO_FUNCTION was declared
in kernel/src/drivers/pci.rs and again in kernel/src/iommu/vtd/fault.rs; it
is declared once now, where a function's address is a triple, and the fault
handler reads it through the import it already had. NETD_EXITS was declared
twice and is declared once. 1af4:1041 was written in three places and is read
from https::VIRTIO.claims, the constant point 11 holds against the committed
devices row. The two lines a hand-over spends are built by bar_moved and
msix_armed, so the arm requiring them and the arm requiring their absence
cannot drift apart. What this round missed is that iommu::armed_on_msix spelled
msix address= a third time, which is point 17.

15. Prose deleted, not moved. The escalation story — read the early end as a
terminator, MSI armed on the guess, msix_bar withholding no BAR — was told at
pcidev/mod.rs's header, at drivers/pci.rs's StagedCaps and again in the
judge's doc. The header keeps it and the other two are gone.

16. A compromise on the ordinary path is recorded. place_bars's
Some(index) == table_bar mutated to (Some(index) == table_bar && false) hands
the MSI-X table's own BAR to the holder of every function that publishes a table,
and https_tls13, https_tls13_e1000e, pci_function_is_exclusive and
userdev_dma_fault are each green on it — mutation 5 below, with the spelling
that builds and the bare false that does not. It is pre-existing:
place_bars's line is dc38a054's own, byte for byte, and this branch neither
wrote nor moved it. It is not fixed here — it is a bullet in
issues/kernel/a-claims-own-refusals-are-read-by-nothing.md, which this branch
created for exactly that list. A refused claim is not the arm that would cover
it; the refusal spends no BAR at all, so what is unread is the hand-over that
succeeds.

What round 5 changed

17. The MSI-X judge asserts the harness's address too. Point 13 applied
round 4's ruling to refused_claim and stopped one file over.
iommu::armed_on_msix still read the BDF out of the guest's own
[{claims}] handed over on slot line and then asserted msix address= and
msi address= for that, which is an assertion about whichever function the
kernel named — and it is the same third spelling of msix address= point 14
said there was one of. It reads faults::CLAIMED_AT now, asserts the printed
address equal to it, and spends faults::msix_armed() and the new
faults::msi_armed(); the older mechanism's line gets the constructor the newer
one already had, so neither is written in two places. Mutation 4 below is what
that buys: a kernel that arms MSI on the claimed function and names another
one in its hand-over line passed the old judge and reds on this one.

18. A text scan that cannot parse a line refuses instead of dropping it.
refused_claim's walk over NOT HANDED OVER lines was two filter_maps, so a
line carrying the marker without the pcidev: PCI prefix left the list
silently. The empty list already reds; what did not was a partial miss — one
refusal the scan can read on CLAIMED_AT and a second it cannot read at all,
which is exactly the console "refused any other function" exists to catch. A
text scan closes only the spellings it matches, so the unmatched one is now the
finding: faults::functions_named is that walk for both judges and answers an
Err naming the line and the marker.

19. Prose corrected where it is evidence. The issue file no longer counts
the refusals that are read back — this branch's own judge moved that count, so
it lists them instead. Its place_bars bullet spells the mutation in the form
that builds and names the four tests measured green on it rather than claiming
every tier. CLAIMED_AT's doc said "the slot every profile in this suite puts
the function netd claims on"; no profile puts it anywhere — the slot is QEMU's
assignment by -device order, git grep 'addr=' -- tests/common/qemu.rs prints
nothing, and Profile::Metal carries no such function at all — so it says what
is true of the constant: a wrong one reds and never passes. And the control
below cited toyos.rs:18383, which dc38a054's merge had moved onto a doc
comment; the refusal is assert_fast_profile_label at tests/toyos.rs:18399-18405.

Retracted by name

History is not rewritten here, so these are withdrawn rather than edited.

  • 6d59a093's message and this body said PciDevice::capability replaced
    "seven copies of capabilities().find(|c| c.id() == …) — the four in
    drivers/pci.rs". Both halves are false.
    git grep -n 'capabilities()\.find' 8261079e -- kernel toyos-pci userland
    prints 6 lines, 3 of them in kernel/src/drivers/pci.rs; the other
    three are hda::power_up, pcidev::reset and pcidev::msix_bar. The seventh
    caller, disable_msi, is new code on this branch and replaced no copy.
  • a2be5f84's message says "The five remaining readers of capability" and
    then names six.
  • a2be5f84's message justified deleting userland/netd/src/i219.rs's BAR
    clause with "which this branch falsifies — the T14's 00:1f.6 publishes no
    MSI-X, so nothing is withheld from that claim". False. That clause is
    about code which runs on tests/e1000case too, where the claimed 8086:10d3
    does publish MSI-X with its table in BAR 3 — so the kernel does withhold that
    BAR and does report 0 bytes for it. The invariant returns as one clause at the
    site.
  • This body, 68ba8df6's message and the round-2 handoff all said
    pci_function_is_exclusive's MSI-X-first assertion "cannot be shown by a
    mutation: no function this harness hands to a claim publishes both
    mechanisms". False. tests/e1000case hands one to a claim on the fast
    tier, and mutation 2 below is run against it.
  • eea8c7f7's message and this body offered "-D dead-code refuses a kernel in
    which Refusal::CapsTruncated is never constructed" as what stood against the
    unwitnessed privilege arm. It is not a guard. The lint fires only on a
    mutation that keeps the variant while deleting its constructor; it fires on
    the mutation, never on a regression that reached main; and an #[allow]
    or any later arm that constructs the variant removes it silently. The whole
    change reverted — the arms collapsed and the variant deleted, which is the
    form CLAUDE.md's mutation rule asks for — builds clean, exit 0, and is
    mutation 1 below.
  • issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md priced a
    guest arm at "a boot config carrying such a function and a test binary to hold
    it". Refuted by this tree, and the file is corrected: the shape is staged
    on a function an existing boot config already hands to a claim, and a refusal
    needs no holder. What the file still tracks is the MSI arm that succeeds,
    which nothing in any tier reaches.
  • This body's earlier counts were measured against ad61ed08 and then
    8261079e. Each measurement below names the head it was taken at, because
    this branch has had two since: every green row, mutations 1, 4 and 5 and the
    size numbers were run at 348ac77f, which this head (f0073d33) is one doc
    comment and no code away from; the negative control and mutations 2 and 3 are
    d82dff33's, the head before the merge of 9f91b581, and nothing that moved
    between the two is on a path any of them touches.

The two checks

Negative control — anchored by name to dc38a054, never to a relative
expression a merge moves. For every path the control reverts, dc38a054, the
earlier anchors 8261079e and ad61ed08, and main's tip as merged in this
head (9f91b581) all hold the same bytes:
git diff --name-only ad61ed08 dc38a054 -- <those paths> prints nothing, and
git diff --name-only dc38a054 9f91b581 prints .claude/agents/reviewer.md and
src/durations.rs — neither a reverted path nor a judge — so re-anchoring moves
no byte of what is reverted. It reverts the 16 non-judge paths this branch's
own commits touch and keeps the four judges —
tests/common/faults.rs, tests/common/https.rs, tests/common/iommu.rs,
tests/toyos.rs — plus tests/test-durations, which is not a judge but is the
price row tests/toyos.rs's registration requires (reverting it too makes the
harness refuse the run by name in assert_fast_profile_label,
tests/toyos.rs:18399-18405, instead of running either arm).
kernel/src/iommu/vtd/fault.rs joins the list this round: it reads
drivers/pci.rs's one NO_FUNCTION now, so a control that reverted the
declaration without it would not compile.

B=dc38a054
git checkout $B -- kernel/src/actuator.rs kernel/src/drivers/hda.rs \
  kernel/src/drivers/pci.rs kernel/src/drivers/virtio.rs \
  kernel/src/drivers/virtio_sound.rs kernel/src/drivers/xhci/wait/boot.rs \
  kernel/src/iommu/vtd/fault.rs kernel/src/pcidev/mod.rs \
  toyos-pci/src/caps.rs toyos-pci/src/msi.rs \
  userland/netd/src/i219.rs \
  issues/kernel/a-claimed-function-must-have-msi-x-and-the-i219-may-not.md
git rm -q issues/hardware/toyos-i219-refuses-a-part-outside-msi-x-mode-at-ivar.md \
  issues/kernel/a-claims-own-refusals-are-read-by-nothing.md \
  issues/kernel/nothing-asserts-that-a-claim-answers-no-configuration-write.md \
  issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md
git diff --name-only $B          # the judges and the price row, or it is not a control

printed exactly

tests/common/faults.rs
tests/common/https.rs
tests/common/iommu.rs
tests/test-durations
tests/toyos.rs

Run at d82dff33, the control kernel builds — exit 0 — and two judges go
red
, each the command's own exit status:

FAIL pci_capability_walk: not every crafted capability layout was answered: [kernel 0.128 cpu0] virtio: pci cap selftest 14/14
test result: FAILED. 0 passed, 1 failed, 0 stale or expired expected-failure entries, 0 invalidated, 1 total (12.9s)

exit 1 — and its ALONE line reads "red again on a DIFFERENT failure",
which is the harness comparing two strings that carry the kernel timestamp
(0.128 and 0.105): one assertion, twice, under two clocks. And

FAIL virtio_net_no_msix: "pcidev: PCI 00:03.0 NOT HANDED OVER — neither its MSI-X nor its MSI could be armed" never reached the boot console:
[kernel 0.339 cpu0] pcidev: PCI 00:03.0 NOT HANDED OVER — its MSI-X could not be armed, and a claim with no interrupt is a driver that would never be told anything
ALONE virtio_net_no_msix: red again, the same failure both times — the defect is real.

exit 1. That is the anchored match earning its place: the base refuses the
same function on the same slot, and the judge reds on the reason rather than
passing on the words NOT HANDED OVER appearing somewhere. Restore:
git reset --hard HEAD, git status --porcelain empty, git rev-parse HEAD
unchanged at d82dff33.

What the control does not discriminate, stated rather than implied.
pci_claim_caps_truncated cannot run on it at all, and no kernel is what
refuses it: the actuator and the staging it needs are part of the reverted
change, so the harness refuses the parameter before a guest is started —
tests/common/qemu.rs:2648, "pcidev-caps-truncated" is a kernel_params and the kernel declares no such actuator or parameter, exit 1 with nothing booted. Its negative control is mutation 1, which is the
same revert of the same decision with the instrument left in place.
https_tls13_e1000e stays green on the reverted base, exit 0: the e1000e
publishes MSI-X either way, so the base arms MSI-X too. That arm is a guard on
the owner's direction, shown by mutation 2, not a judge of this diff.
Refusal::MsixUnusable, Unarmed::Blocked, Armed::Msi and both disable_msi
sites likewise revert with every arm green, because nothing in any tier arms a
claimed function on MSI successfully. That is what
issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md still
tracks, still a weakness, with the bench as its exit condition.

A mutation is a measurement only once the mutated tree is shown to build, so
each below quotes its build's exit status before its judge's — and mutation 5
is there because one spelling of it does not build at all. The build
is cargo build --target x86_64-unknown-none --features boot-actuators,test-actuators in kernel/, with target/debug on PATH for
toyos-ld — the same target and features src/clippy.rs's third shape and the
harness's own kernel build use.

Mutation 1 — the privilege decision, and it reds (re-run at this head).
bring_up's two NoTable arms replaced by one falling through to enable_msi,
with Refusal::CapsTruncated and its Display arm deleted — git grep -c CapsTruncated -- kernel matches no file — so the whole change is reverted,
which is also why it builds. Build exit 0, then
cargo test --test toyos-build -- pci_claim_caps_truncated exit 1:

FAIL pci_claim_caps_truncated: the claim this judges is the one on 00:03.0; this console refused []:
[kernel 0.338 cpu0] PCI 00:03.0: msi address=0xfee00098 data=0x00000000
[kernel 0.344 cpu0] pcidev: PCI 00:03.0 BAR 3 (0x4000 bytes) moved to 0xc0600000
[kernel 0.344 cpu0] pcidev: PCI 00:03.0 [8086:10d3] handed over on slot 0, vector 0x28
ALONE pci_claim_caps_truncated: red again, the same failure both times — the defect is real.
test result: FAILED. 0 passed, 1 failed, 0 stale or expired expected-failure entries, 0 invalidated, 1 total (6.5s)

The judge names what it judges and what the console did instead — refused []
is a boot in which no claim was refused at all — and the three lines above are
the hand-over that happened in its place.

BAR 3 is the BAR that function's MSI-X table register names (0xa4 = 0x00000003, BIR 3, offset 0, read off the device model), and the mutated kernel
moved it and handed it to netd with everything else. That is the escalation the
two arms stand between.

Mutation 2 — the ordering (at d82dff33). bring_up tries enable_msi
first and falls back to enable_msix, nothing else touched. Build exit 0,
then
cargo test --test toyos-build -- https_tls13_e1000e exit 1:

[kernel 0.673 cpu0] PCI 00:03.0: msi address=0xfee00098 data=0x00000000
FAIL https_tls13_e1000e: "PCI 00:03.0: msix address=" never reached the boot console:
ALONE https_tls13_e1000e: red again, the same failure both times — the defect is real.
test result: FAILED. 0 passed, 1 failed, 0 stale or expired expected-failure entries, 0 invalidated, 1 total (17.1s)

Every fetch, refusal and differential arm below it still passed — the card works
on MSI, which is why nothing but this assertion sees the order.

Mutation 3 — the arming's own answer (at d82dff33). enable_msix's
map_err(Unarmed::NoTable) replaced by one reporting every miss as Absent.
Build exit 0, then cargo test --test toyos-build -- pci_capability_walk
exit 1:

[kernel 0.075 cpu0] virtio: pci cap selftest FAILED on a list that cycles on itself: the arming reads absent, want truncated
[kernel 0.075 cpu0] virtio: pci cap selftest FAILED on a link that is not dword-aligned: the arming reads absent, want truncated
[kernel 0.076 cpu0] virtio: pci cap selftest FAILED on a link below the standard header: the arming reads absent, want truncated
[kernel 0.076 cpu0] virtio: pci cap selftest FAILED on a head the spec forbids: the arming reads absent, want truncated
[kernel 0.076 cpu0] virtio: pci cap selftest FAILED on a capability reached before a link the spec forbids: the arming reads absent, want truncated
[kernel 0.077 cpu0] virtio: pci cap split 8/13
[kernel 0.077 cpu0] virtio: pci cap selftest 15/15

with 15/15 green beside it: two counts, and only the one that read the
decision moves.

Mutation 4 — the judge's own address, and the partial fix it used to pass.
bring_up arms MSI first (mutation 2's defect) and the hand-over line prints
pci.dev + 1, so the kernel takes the older mechanism on the claimed function
and names a different one in the line the old judge read its address out of.
Against the judge as point 4 shipped it this was green: it found 00:04.0's
msix address= and called an MSI-armed 00:03.0 MSI-X-first. Build exit 0,
then cargo test --test toyos-build -- https_tls13_e1000e exit 1:

FAIL https_tls13_e1000e: this is an assertion about the claim on 00:03.0; 8086:10d3 was handed over on ["00:04.0"]:
[kernel 0.092 cpu0] PCI 00:01.0: msix address=0xfee00018 data=0x00000000
[kernel 0.243 cpu0] PCI 00:04.0: msix address=0xfee00078 data=0x00000000
[kernel 0.323 cpu0] PCI 00:03.0: msi address=0xfee00098 data=0x00000000
[kernel 0.331 cpu0] pcidev: PCI 00:04.0 [8086:10d3] handed over on slot 0, vector 0x28
ALONE https_tls13_e1000e: red again, the same failure both times — the defect is real.
test result: FAILED. 0 passed, 1 failed, 0 stale or expired expected-failure entries, 0 invalidated, 1 total (10.8s)

The capture is the whole argument: three msix address= lines are on that
console and one of them belongs to the claim. A judge that takes its address
from the guest picks whichever the guest offers it; this one names 00:03.0
before reading anything, and the msi address= on that slot is what it would
have gone on to refuse.

Mutation 5 — the recorded compromise, and it stays green (point 16).
place_bars's Some(index) == table_bar mutated to
(Some(index) == table_bar && false): build exit 0, then https_tls13 and
https_tls13_e1000e exit 0, pci_function_is_exclusive exit 0,
userdev_dma_fault exit 0. The bare false the tracker used to spell it
with does not compile — error: unused variable: table_bar,
-D unused-variables implied by -D warnings, build exit 101 — which is why
the issue file now carries the form that builds. This is the one mutation here
run to show that nothing reds.

Independent oracles, two, neither of them another agent.

  1. Linux's own reading of the same function. /proc/interrupts on the T14
    names its NIC's interrupt IR-PCI-MSI-0000:00:1f.6 … enp0s31f6 and
    /sys/bus/pci/devices/0000:00:1f.6/msi_irqs/162 reads mode=msi. A driver
    nobody here wrote, on the same silicon, saying that function is an MSI part.
  2. The PCI capability layout, as toyos-pci's tests encode it. PCI §6.7 fixes
    a capability pointer as dword-aligned and above the 64-byte standard header,
    which is what makes an early-ending walk a fact about the list and not
    about the function. An implementation that reads a misaligned pointer, a
    below-header pointer or a cycle as a clean end is red at
    a_pointer_that_is_not_dword_aligned_is_refused,
    a_pointer_below_the_standard_header_is_refused and
    a_pointer_to_a_visited_link_ends_the_walk; one that calls the terminator an
    early end is red at the_terminator_ends_the_walk and
    a_forward_chain_is_followed_to_its_end.

Green

Every row is the line the command printed in /Users/jan/Dev/jan/toyos-msi, with
the exit status of the command itself and never a pipeline's, measured at this
head.

command printed exit
cargo test --test toyos-build -- pci_claim_caps_truncated PASS pci_claim_caps_truncated (2s) / test result: ok. 1 passed, 1 total (4.9s) 0
cargo test --test toyos-build -- pci_capability_walk PASS pci_capability_walk (2s) / test result: ok. 1 passed, 1 total (3.3s) 0
cargo test --test toyos-build -- https_tls13 (the filter is a prefix, so it selects both bench names) PASS https_tls13 (4s) / PASS https_tls13_e1000e (4s) / test result: ok. 2 passed, 2 total (6.0s) 0
cargo test --test toyos-build -- virtio_net_no_msix PASS virtio_net_no_msix (2s) / test result: ok. 1 passed, 1 total (2.2s) 0
cargo test --test toyos-build -- pci_function_is_exclusive PASS pci_function_is_exclusive (2s) / test result: ok. 1 passed, 1 total (3.4s) 0
cargo test --test toyos-build -- --nightly iommu_virtio_platform (the arm whose no-unit judge round 4 moved) PASS iommu_virtio_platform (6s) / test result: ok. 1 passed, 1 total (7.6s) 0
cargo test --lib test result: ok. 308 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out 0
cargo test --workspace --exclude toyos-build 139 test result: ok lines, 1367 passed, 0 failed, no FAILED line 0
the five cargo clippy shapes src/clippy.rs declares, run one by one as the host job runs them no warnings 0 each
the negative control above (at d82dff33) control kernel build 0; FAIL pci_capability_walk and FAIL virtio_net_no_msix 1 each, which is the result it is run for
mutations 1 and 4 (this head), 2 and 3 (d82dff33) build 0 then judge 1, each above 1 each, which is the result they are run for
mutation 5 (this head) build 0, then four names at 0 — that is the compromise point 16 records, not a guard 0, which is the finding

The price row is no longer a marker. pci_claim_caps_truncated entered
tests/test-durations as an UNMEASURED marker with shards=none, which is
the documented two-cycle cost of a new registration: the marker buys the name
one measured CI run and the next commit carries that price. Run 35100393637,
twelve hosted shards at a75b3828, priced it at 2583 ms, and its
test-durations-merged artifact carries the row; that run's durations job
reds by name on the committed marker — "committed UNMEASURED profile marker(s)
are provisional and may not land: pci_claim_caps_truncated" — and says to
replace it with the measured value. pci_claim_caps_truncated 2583 shards=12
is now the row. 2583 ms is under FAST_COMMIT_MS (8000), so Tier::Fast is the
tier its price gives it and no registration moves. That one row is taken and no
other: every other name in the merged artifact is this run's shard luck, which
src/durations.rs measures at 1.28x p10-p90.

Two reds in an earlier battery were a host going down. At 23ef7817
virtio_net_no_msix and pci_function_is_exclusive each failed with Boot timed out waiting for ===READY===; the console carried: nothing at all — no guest byte
at all, twice, in the minutes before the development host rebooted into a new OS
build and wiped /tmp. Both are exit 0 in the table above, which is measured
after that reboot, as is every other row in it.

Size

git diff --shortstat origin/main HEAD is 21 files changed, 712 insertions(+), 198 deletions(-); across kernel toyos-pci tests userland the --numstat sums
to +575 / −155. Nothing outside kernel/, toyos-pci/, tests/,
userland/netd/ and issues/ is touched by this branch's own commits: no Cargo
manifest, no NOTICE, no sourcegate row, no src/tiers.rs relegation row, no
CLAUDE.md, and no syscall added, changed or retired. What it does add to the
harness is one registered name, one actuators! row and that name's measured
price row.

🤖 Generated with Claude Code

https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK

`pcidev::bring_up` armed exactly one mechanism — `enable_msix(...).ok_or(Refusal::NoMsix)?` — so a
function that publishes no MSI-X capability was refused by name and its holder
never ran. Every function this project had handed to a process so far was a
virtio one and every one of those has MSI-X, so the refusal had only ever been
reached by `virtio_net_no_msix`'s deliberate `vectors=0`.

The ThinkPad T14's onboard NIC is not one of those. Measured on the machine,
not assumed: `/proc/interrupts` names its interrupt
`IR-PCI-MSI-0000:00:1f.6 ... enp0s31f6` and
`/sys/bus/pci/devices/0000:00:1f.6/msi_irqs/162` reads `mode=msi`, so Linux
drives that function on MSI. Flashed and booted (run 28), this kernel wrote
`pcidev: PCI 00:1f.6 NOT HANDED OVER — its MSI-X could not be armed`, netd
found no endowment and exited, and the bench's one cable stayed dark.

`bring_up` now arms MSI-X and falls back to MSI, and `Bound` holds whichever it
got. **The driver above the boundary cannot tell which one it is and does not
have to**: both deliver the same vector into the same `Interrupt`, and the claim
answers the same handle either way. What differs is where the message lives, and
therefore what a hand-over back has to write to silence it — `Armed::silence`
masks an MSI-X table entry or clears MSI Enable, and `Armed::undo` puts the
capability itself back off for a hand-over that armed a vector and was then
refused.

**MSI is not the weaker mechanism here, and the security argument is the same
one.** MSI-X's table is kept out of what the holder maps because a holder that
could rewrite it could point the device's message at any address the LAPIC
decodes. An MSI function's message is in its own config space, which `pcidev`
keeps: `config_read` is read-only and there is no writing counterpart. So MSI
needs no BAR withheld — the same rule reaching a different register file.

`Refusal::NoMsix` becomes `NoInterrupt` and says both mechanisms, because that
is now what it means. `PciDevice::disable_msi` is new and is `disable_msix`'s
counterpart: it sets the per-vector mask where the capability implements one and
clears MSI Enable, which every function has. `toyos_pci::msi` grows `disabled`
and the `MASKED`/`UNMASKED` mask values, host-tested — `disabled` deliberately
does not restore the Multiple Message Enable field an arming zeroed, or a
function would come back armed for as many vectors as it can raise.

The hand-over record now names the mechanism (`vector 0x28 on MSI-X`): it is the
first thing a machine that never heard from its device is asked, and it is not
something the driver above the boundary can see.

The two checks.

- **Negative control.** Run 28 on the T14 is this change reverted whole, on the
  base the granted claim will be measured against: the same `tests/lancase`
  image on this machine's own I219 with `bring_up` arming MSI-X alone. It
  refused the claim by name at 1.349 s, netd exited `code=0` at 2.268 s, and
  nothing answered on the cable for the twenty seconds the boot stayed up.
- **Independent oracles, two.** Linux's own reading of the same function, above
  — a driver nobody here wrote, saying that function is an MSI part. And the
  capability's register layout, which is the PCI spec's and which
  `toyos-pci/src/msi.rs`'s tests encode: the offsets of the address, data and
  mask registers all move with the 64-bit address bit, and writing a vector at
  the wrong one of them lands in whatever capability comes next in the list.
  Beside them, this kernel already produces the shape MSI must reproduce —
  `PCI 00:1f.3: msi address=0xfee00098 data=0x00000000` for the T14's HDA.

Green: `cargo test -p toyos-pci` (41), `cargo test --lib` (296),
`cargo run -- --clippy` (all five invocations), and the four guest arms that
read this module — `virtio_net_no_msix`, `iommu_virtio_platform`,
`pci_function_is_exclusive` and `userdev_dma_fault`. The MSI arm itself is
exercised on the T14 alone: no device QEMU models that a process may claim
publishes MSI without MSI-X, so there is no guest that can take that branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGQ2H2aCwd1jfiNmjsiUvz
@Japabu
Japabu enabled auto-merge September 8, 2026 15:32
Japabu added a commit that referenced this pull request Sep 8, 2026
The green arm cannot be measured until the kernel grants netd the I219's
function, and that is #443. Merged here so the image the T14 runs carries it;
when #443 lands, merging main into this branch is the same commits again.
@Japabu
Japabu disabled auto-merge September 8, 2026 15:38
Japabu added a commit that referenced this pull request Sep 8, 2026
The green arm's claim stops at the 32-bit window, and the branch that says what
the machine has left below 4 GiB is stacked on #443. Merged here so the image
the T14 runs carries the survey; when both land, merging main is the same
commits again.
…t does

The fallback this branch added chose by what an arming answered, so a
function that publishes MSI-X and whose table this kernel could not decode
fell through to MSI and was handed over with that table and its PBA inside a
BAR `place_bars` did not withhold: `msix_bar` answers `None` on a decode
failure, so nothing is kept back, while `enable_msix` answers `None` on the
same input. On the base that function was refused and never reached a holder.

The choice is now what the function's capability list publishes and never what
an arming answered. `toyos_pci::mechanism` is that rule, pure and host-tested:
a function publishing MSI-X is armed on MSI-X or refused `MsixUnusable`, MSI is
armed only where there is no table in a BAR at all, and neither is
`NoInterrupt`. `enable_msix` succeeding implies `Msix::decode` succeeded
implies `msix_bar` named the BAR, so the table's BAR is withheld on every path
that arms MSI-X.

`disable_msi` is the enable bit alone. Its per-vector mask write had no
specification behind its order and was the opposite of the one independent
implementation this branch cites — Linux's `pci_msi_shutdown` clears MSI Enable
and then *unmasks* — and leaving the Mask bit set owes a message on the
set-to-clear transition a later arming makes of it with the Pending bit set
(PCIe 7.7.1.7). What is left is the one decision `Msi::disabled` makes, which
`disabling_clears_the_enable_bit_and_nothing_else` gates on the host; its
fixture now carries Multiple Message Enable set, so the partial implementation
that cleared that field too is red where it used to pass.

`netd`'s `config_space_is_bounded` now attempts a configuration write and
refuses a claim that answers one. That refusal —
`RegTarget::PciConfig(_) => Err(NotSupported)` — is the whole of why an MSI
function's message may stay in configuration space with no BAR withheld for it,
and nothing asserted it.

Deleted: `Armed`'s three one-caller methods and the `arm` free function, whose
bodies are one `match` each at their sites; `msi::MASKED`/`UNMASKED` and the
test that restated their declarations; `iommu.rs`'s `MSI_ARMED` `must_not_say`,
which no implementation of that module could red — 00:03.0 publishes MSI-X, so
that arm never reaches the MSI branch.

`issues/kernel/a-claimed-function-must-have-msi-x-and-the-i219-may-not.md` is
renamed and cut to the half that still stands: `toyos-i219` refuses a part
outside MSI-X mode at `IVAR`, and the T14's `00:1f.6` is measured to be one.
The kernel half the slug claimed is refuted by this branch. No citation to
either the slug or the path exists anywhere else in the tree (`git grep`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGQ2H2aCwd1jfiNmjsiUvz
@Japabu Japabu changed the title A claimed function is armed on MSI where it has no MSI-X A claimed function is armed on the mechanism it publishes, and MSI is not a fallback Sep 8, 2026
Japabu and others added 2 commits September 8, 2026 18:58
…re tracked

`toyos_pci::mechanism` was a new pure-crate rule with one caller, a four-row
truth table its two tests transcribed row for row, and a doc that told the
kernel's hand-over story inside a crate that knows nothing of hand-overs. It is
deleted whole: `toyos-pci/src/lib.rs` is byte-identical to origin/main again,
and `bring_up` asks the capability list directly.

The rule that MSI is not a fall-back is a rule about hand-over and is stated
once, in the module whose subject is a function driven by a process
(`kernel/src/pcidev/mod.rs`). The kernel's own xHCI and HDA drivers arm MSI-X
or fall back to MSI, and that is not the same choice: nothing is handed over
there, so no BAR carrying an MSI-X table reaches a holder and there is no rule
to contradict.

Also deleted, because nothing consumed them: the `on {armed}` discriminant on
the hand-over line, which restates what `report_message` printed off the
device's own registers one line earlier; netd's configuration-space write
probe, which ran on the one claim that is armed on MSI-X and never on the one
armed on MSI, and which had to reach past `PciDev` — a typed handle with no
write method at all — into `toyos_abi::syscall` to make the call; and the second
assertion in `disabling_clears_the_enable_bit_and_nothing_else`, which the first
already implies.

Two weaknesses this branch leaves standing are now recorded rather than carried
in a pull request body:

  issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md
  issues/kernel/nothing-asserts-that-a-claim-answers-no-configuration-write.md

`grep -rn 'disable_msi\b\|enable_msi\b' kernel/` gives five sites: hda.rs:666
and xhci/wait/boot.rs:104 arm and never disarm, and pcidev/mod.rs:495, :520 and
:756 are this branch's, reached by no tier. Each file carries its owner and the
exit condition that closes it.

Prose deleted at the sites the review named, including two pieces of
pre-existing prose in files this branch edits: `pcidev`'s "What is read back,
and what is not" register of tests, which no gate held and which went stale
every time a test moved, and five of the six lines on `BAR_MOVED`/`MSIX_ARMED`.

TWO SENTENCES OF 8dec3ec ARE RETRACTED. History is not rewritten here, so they
are withdrawn by name instead:

  "nothing answered on the cable for the twenty seconds the boot stayed up" is
  false. /Users/jan/.claude/jobs/2280e09e/tmp/t14-run28/lancase.log:12 reads
  "100.92.92.12 answered a ping 64 s into the window, after 5 s of silence — so
  something on this cable was up while Ubuntu was not".

  "Run 28 on the T14 is this change reverted whole, on the base the granted
  claim will be measured against" is false. Run 28 is tip=220305b4
  (t14-run28/lancase.log:1) and run 29 is tip=24625c6b
  (t14-run29/lancase.log:1), whose parent 938957a run 28 does not carry, so the
  pair reverts two changes and is not a negative control.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGQ2H2aCwd1jfiNmjsiUvz
…iving it

`enable_msix` collapsed four outcomes into one `None`, so `bring_up` had to ask
the capability list again to tell "publishes no table" from "publishes one this
kernel could not arm" — and the refusal it raised then asserted a reason that was
false on three of the paths it fired on. `Refusal::MsixUnusable` said the table
"is in a BAR nothing here can name to withhold", which holds only where
`Msix::decode` failed: after a successful decode `msix_bar` answers `Some(bir)`
and `place_bars` does withhold that BAR. On the `message()` path it was worse
than false — `enable_msi` calls the same `message()` and fails identically, so
the honest refusal there is that neither mechanism could be armed, and the
console had already printed `not armed — {why}` one line above the contradiction.

`enable_msix` now answers `Result<Mmio, Unarmed>` (`drivers/pci.rs:33-44`):
`Absent` is a function publishing no MSI-X, `Unusable` a table this kernel could
not reach, `Blocked` a message the unit refuses — which is the same message MSI
would carry. `bring_up` is one match over it (`pcidev/mod.rs:489-496`) with no
capability walk of its own: `Unusable` refuses `MsixUnusable`, `Blocked` refuses
`NoInterrupt`, `Absent` tries MSI. The `else if publishes(msi::CAP_ID)` arm is
gone with the closure: `enable_msi` already opens by looking the capability up
and answering false without one, so the arm decided nothing and no log line on
any machine moved with it. `virtio_net_no_msix` is the test that reaches the
`Absent` arm and reads that false back.

`MsixUnusable` no longer claims what withholding happened; it says the function
publishes MSI-X, this kernel could not arm it, and MSI is not a fallback for a
function that has a table.

`PciDevice::capability(id)` (`drivers/pci.rs:363-366`) is the one spelling of
"find this function's capability by id". It replaces seven copies of
`capabilities().find(|c| c.id() == …)` — the four in `drivers/pci.rs`, `msix_bar`
and `reset` in `pcidev/mod.rs`, and `power_up` in `drivers/hda.rs`.

Three callers move from `Option` to `Result` and decide nothing new:
`hda::arm_interrupt`, `virtio_sound::arm_interrupt` and `xhci::wait::boot::
arm_interrupt` read `.is_ok()`/`.is_err()` where they read `.is_some()`/
`.is_none()`.

Two numbers in 76fb456's message and body are corrected here, since history is
not rewritten. Its message attributed "five sites" to
`grep -rn 'disable_msi\b\|enable_msi\b' kernel/`; that command prints seven
lines — the two definitions plus five call sites — and the enumeration that
followed it was of the call sites only. Its body called
`tests/common/iommu.rs` byte-identical to `origin/main`, which
`git diff origin/main HEAD --shortstat -- tests/common/iommu.rs` refutes at
`1 insertion(+), 6 deletions(-)`; `toyos-pci/src/lib.rs` and
`userland/netd/src/virtio_net.rs` are byte-identical and `tests/common/iommu.rs`
is not. The body also cited the choice at `mod.rs:487-496`, which was the comment
above it and one line short of the block.

Prose deleted rather than rewritten: `Armed::Msi`'s doc, which restated the
module header's own MSI clause; the unbacked "no device QEMU models…" sentence
and the session-local "run 29" locator in
`issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md`; the
rebuttal of the dropped `open` probe in
`issues/kernel/nothing-asserts-that-a-claim-answers-no-configuration-write.md`;
and, pre-existing in files this branch edits, the "The refusal moved with the
driver" chronology in `tests/common/faults.rs` and `enable_msix`'s two-meaning
`None` clause. That tracker's own claim moved with the code: `enable_msi` from
`bring_up` is now reached by `virtio_net_no_msix`, and what nothing reaches is a
successful arming and everything past it.

Green in this worktree: `cargo test -p toyos-pci` 40 passed exit 0;
`cargo test --lib` 295 passed 1 ignored exit 0;
`cargo test --workspace --exclude toyos-build` 138 suites 1347 passed exit 0;
all five `src/clippy.rs` shapes exit 0, no warning. Every
`cargo test --test toyos-build` is still refused at `src/toolchain.rs:1382`
(exit 101) while `/Users/jan/Dev/jan/toyos-aperture` holds the sysroot;
`--claim-sysroot` was not passed and `main` was not merged in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu Japabu changed the title A claimed function is armed on the mechanism it publishes, and MSI is not a fallback A claimed function is armed on the mechanism it publishes, and the arming says which case it refused Sep 8, 2026
Japabu and others added 2 commits September 13, 2026 14:11
… walk reached

`bring_up` armed MSI on `Unarmed::Absent`, and `Absent` meant only "the walk
yielded no MSI-X capability". The walk is `caps::CapWalk`, which ends at a
misaligned pointer, one below the standard header, or one already visited
(`toyos-pci/src/caps.rs`). So a function whose MSI capability precedes such a
link and whose MSI-X capability follows it read as `Absent`, was armed on MSI,
and `msix_bar` — the same blind walk, one line above — named no BAR, so
`place_bars` withheld none and the MSI-X table's BAR went to the holder with
everything else. A holder that can write that table points the device's message
at any address the LAPIC decodes. On origin/main that function was refused
`NoMsix` and reached no holder, so the hole is this branch's.

An early end is now a distinct answer, and the refusal is by name.

- `CapWalk::truncated()` says whether the walk ended at the list's terminator or
  at a link the spec forbids. The decision stays in the pure crate; the kernel
  reads it.
- `PciDevice::capability(id)` answers `Result<Capability, NoCapability>`:
  `Absent` is a walk that reached the terminator and found nothing under the id,
  `Truncated` a walk that stopped early, so what lies past that link was never
  read.
- `enable_msix` carries it out as `Unarmed::NoTable(NoCapability)`, and
  `bring_up` refuses `Refusal::CapsTruncated` — "its capability list ends at a
  link the PCI spec forbids, so whether it holds an MSI-X table in a BAR was
  never read, and MSI is not armed on a guess". Only `NoTable(Absent)` reaches
  the MSI arm.

The five remaining readers of `capability` — `disable_msix`, `enable_msi`,
`disable_msi`, `hda::power_up`, `pcidev::reset` and `msix_bar` — take the same
answer and decide nothing new; a truncated walk leaves each of them where a
missing capability did. `msix_bar` is the one that matters, and what makes it
safe is that `bring_up` refuses the function before `place_bars` ever reads what
it answered.

The host tests that see a partial implementation are `toyos-pci`'s four walk
tests, which now assert the flag as well as the step: an implementation that
treats a misaligned pointer, a below-header pointer or a cycle as a clean end is
red at `a_pointer_that_is_not_dword_aligned_is_refused`,
`a_pointer_below_the_standard_header_is_refused` and
`a_pointer_to_a_visited_link_ends_the_walk`, and one that reports the
terminator as an early end is red at `the_terminator_ends_the_walk` and
`a_forward_chain_is_followed_to_its_end`.

The module header at `kernel/src/pcidev/mod.rs` stated the bound as an absolute
— "a function that publishes MSI-X is armed on MSI-X or refused" — which held
only over what the walk reached. It now states it over the walk.

Deleted, because nothing reads them: `#[derive(Debug, Clone, Copy, PartialEq,
Eq)]` on `Unarmed`, whose five traits nothing in the tree formats, clones,
copies or compares.

Prose deleted rather than rewritten, at the sites the review named: `Unarmed`'s
"Named rather than collapsed into one `None`", which is the argument
`Refusal`'s own doc already makes, by reference to a `None` that no longer
exists; `capability`'s first sentence, which restated its signature;
`bring_up`'s clause naming which test reads which refusal, which the same
commit deleted ten lines of elsewhere; and, pre-existing in files this branch
edits, the three-implementations investigation story above
`tests/common/faults.rs`'s `virtio_net_no_msix` and the reason under
`userland/netd/src/i219.rs`'s BAR search, which this branch falsifies — the
T14's `00:1f.6` publishes no MSI-X, so nothing is withheld from that claim.

Green in this worktree: `cargo test -p toyos-pci` 40 passed exit 0;
`cargo test --lib` 295 passed 1 ignored exit 0;
`cargo test --workspace --exclude toyos-build` 138 suites 1347 passed 0 failed
exit 0; all five `src/clippy.rs` shapes exit 0 with no warning. Every
`cargo test --test toyos-build` is still refused at `src/toolchain.rs:1382`
(exit 101) while `/Users/jan/Dev/jan/toyos-aperture` holds the sysroot;
`--claim-sysroot` was not passed and `main` was not merged in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…reaches

`Refusal::CapsTruncated` and `NoCapability::Truncated` are reached by no test in
any tier, for the same reason the MSI arm is: the guest has no function that
publishes one. The walk's half of the decision is host-tested in
`toyos-pci/src/caps.rs` and a partial implementation of it is red there; the
kernel's refusal arm is not, and the file now says so rather than describing a
tree one commit older.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu and others added 6 commits September 13, 2026 20:48
`main` moved four times under this branch (#449, #451, #454, #455). One
conflict, in `kernel/src/pcidev/mod.rs`'s module header.

#449 appended a sentence to the header's "What is read back, and what is not"
register of tests; `76fb456d` on this branch deleted that register whole,
because no gate held it and it went stale every time a test moved, and recorded
the holes it had been carrying as
`issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md` and
`issues/kernel/nothing-asserts-that-a-claim-answers-no-configuration-write.md`.

The deletion stands. The register names `NoMsix`, the refusal this branch
replaces with `NoInterrupt`, `MsixUnusable` and `CapsTruncated`, so keeping it
would leave the header naming a variant no longer in the file.

#449's sentence is accounted for and not dropped with it. Two of the three
things it said stand elsewhere in #449's own commit: that `account_for` records
on every boot and refuses nothing on it is the header paragraph #449 added
above, and that the record is the whole of what the kernel does about an
un-forwarded window is
`issues/kernel/a-bar-is-placed-in-a-window-firmware-names-no-part-of.md`. The
third — that nothing here reads those records back, that the judge is a boot of
the machine, and that `toyos_pci::aperture` is where the decision behind them is
exercised — had no other home, and is moved to `account_for`'s own doc, which is
the contract for what those records are.

`kernel/src/drivers/pci.rs` merged without conflict and carries both sides:
#449's `PciDevice::bar_slots` and its use in `assigned_bars`, and this branch's
`NoCapability`, `Unarmed`, `PciDevice::capability` and `PciDevice::disable_msi`.
`publish` and `place_bars` bound their BAR walks by `bar_slots()` as #449 left
them; `bring_up` walks no capability list of its own, and a list that ends at a
link the spec forbids still reaches `Refusal::CapsTruncated` and never the MSI
arm.

The pull request body's negative control was anchored to `$(git rev-parse
HEAD^2)`, an expression this merge moves. It is re-anchored by name to
`ad61ed08`, the `main` this merge brings in; the merge changes two of the paths
that control reverts, so the body carries a fresh run of it against that base.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
The branch declared its new MSI path unexercised and filed it as one tracked
weakness. It was two, and only one of them needs silicon.

**The truncation classification needs no claimable function at all.**
`PciDevice::capability`'s `Truncated`/`Absent` split is what decides whether a
malformed capability list falls through to the MSI arm, and
`kernel/src/drivers/virtio.rs`'s `cap_selftest` already drove the real walk over
a cyclic list, a misaligned link, a below-header link and a forbidden head
through `PciDevice::over_config`. Each of those five walk cases now also reads
back what the walk's *end* made of a capability none of them publishes: a walk
that reached the terminator answers `Absent`, one that stopped early answers
`Truncated`. The verdict is its own line, `pci cap split 5/5`, and
`pci_capability_walk` — `Tier::Fast`, so the per-pull-request gate runs it —
refuses a boot that does not print it. No actuator, no boot config, no tier row.

**The mutation it is for, run.** `capability`'s `None if walk.walk.truncated()`
guard replaced by `None if false`, so every miss answers `Absent` — a kernel
reading a list that ended early as one that named nothing. The gate went red at
four named cases with `virtio: pci cap split 1/5`, exit 1, while
`virtio: pci cap selftest 14/14` stayed green beside it: the case count that
existed before could not see this defect, and the split is what sees it.

**MSI-X first is now asserted on the gate that runs.** The only arm requiring a
claimed function to be armed on MSI-X was `iommu_virtio_platform`, which is
`Tier::Nightly` and which `.github/workflows/ci.yml` selects only on a schedule
or a dispatch, so the owner's ordering was unguarded per pull request.
`pci_function_is_exclusive` (`Tier::Fast`) now requires the hand-over to spend
`MSIX_ARMED` and refuses `MSI_ARMED` on the same function, and
`xhci_second_controller` (`Tier::Fast`) requires both of its controllers —
neither of them the `msix=off` one — to report `xHCI: MSI-X enabled`. The second
is the one a producible mutation reds: `nec-usb-xhci` publishes both mechanisms,
so an `arm_interrupt` taking MSI first prints the other line. The claimed-function
arm cannot be mutated that way, because no function this harness hands to a claim
publishes both, and the pull request body says so rather than implying otherwise.

**The arming stays owed, and its tracker now prices only that.**
`issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md` bundled the
classification into the cost of the arming. It now names what remains — the
`bring_up` refusal arm, `disable_msi` from either hand-back site,
`Refusal::MsixUnusable` and `Unarmed::Blocked` — keeps the bench as its exit
condition, and prices the guest alternative honestly: it needs a claimable
function publishing MSI and no MSI-X, which nothing this harness hands to a claim
is, so hiding a capability is not enough by itself.

**The refusals the deleted register carried are recorded again.** `76fb456d`
deleted `pcidev`'s "What is read back, and what is not" and the two issue files
this branch filed recorded only the MSI arm and the configuration write.
`issues/kernel/a-claims-own-refusals-are-read-by-nothing.md` records the rest:
`ClaimError::Ambiguous`, `KernelDriven` and `Exhausted`, every window refusal,
and every bound `SYS_DEVICE_BAR_MAP` and `SYS_DEVICE_DMA_ALLOC` check.

**Two false counts, withdrawn by name.** History is not rewritten here.

- `6d59a093`'s message and the pull request body say `PciDevice::capability`
  replaced "seven copies of `capabilities().find(|c| c.id() == …)` — the four in
  `drivers/pci.rs`". Both halves are false.
  `git grep -n 'capabilities()\.find' ad61ed0 -- kernel toyos-pci userland`
  prints **six** lines, **three** of them in `kernel/src/drivers/pci.rs`
  (`enable_msix`, `disable_msix`, `enable_msi`); the other three are
  `hda::power_up`, `pcidev::reset` and `pcidev::msix_bar`. The seventh caller,
  `disable_msi`, is new code on this branch and replaced no copy.
- `a2be5f84`'s message says "The five remaining readers of `capability`" and then
  names six of them. With this commit's self-test reader,
  `grep -rn '\.capability(' kernel/src` counts eight callers.

`issues/hardware/toyos-i219-refuses-a-part-outside-msi-x-mode-at-ivar.md` was
`kind: defect` over a body saying no hand-over has reached the driver and nothing
has run the write. `issues/README.md` gives `defect` as "real, reproducible", so
it is a `finding`.

Prose deleted rather than rewritten, at the sites the review named: the `pcidev`
header sentence restating `Refusal::CapsTruncated`'s own Display string, and
`disable_msi`'s "the counterpart of `Self::disable_msix`", which restates the
item's name. The `pci-cap-selftest` actuator's doc carried a case count already
one behind the code and that this commit moves again; the count is gone rather
than corrected.

One clause each at `hda::arm_interrupt` and `xhci::wait::boot::arm_interrupt`,
which this branch left taking MSI on whatever a truncated walk reached: both
functions are this kernel's own and hand no BAR to a holder, so an MSI-X table
past the forbidden link is one nobody but this kernel could reach.

Green in this worktree, each the command's own exit status:
`cargo run -- --build-only` exit 0;
`cargo test --test toyos-build -- pci_capability_walk` `PASS` exit 0;
`cargo test --test toyos-build -- pci_function_is_exclusive` `PASS` exit 0;
`cargo test --test toyos-build -- xhci_second_controller` `PASS` exit 0;
`cargo test --test toyos-build -- virtio_net_no_msix` `PASS` exit 0;
`cargo test --lib` 307 passed, 1 ignored, exit 0;
`cargo test --workspace --exclude toyos-build` 1367 passed, 0 failed, exit 0;
`cargo run -- --clippy` `clippy: 5 invocations clean` exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Round 2 closed "MSI-X first is unguarded" with an assertion that guards nothing,
and justified the gap with a sentence about this harness that is false.

**The vacuous assertion is deleted, for the second time and the same reason.**
`tests/toyos.rs`'s `pci_function_is_exclusive` asserted `must_not_say(MSI_ARMED)`
on `00:03.0`. That function is QEMU's `virtio-net-pci-non-transitional`, whose
capability list is MSI-X at 0x98 and five vendor capabilities — no id 0x05
anywhere — so `enable_msi` returns at its lookup and no mutation of this kernel
can put `PCI 00:03.0: msi address=` on that console. `94341ce1` deleted the
identical assertion from `tests/common/iommu.rs` on this branch for exactly that
reason; it came back one file over without an answer, and goes again. Its
`must_say(MSIX_ARMED)` neighbour goes with it: on a function whose only armable
mechanism is MSI-X, a hand-over *is* the msix line, and the `handovers != 1`
refusal four lines above already requires one. `MSI_ARMED` is deleted and
`MSIX_ARMED` is private again, its one reader being the module it lives in.

**The real assertion has a home, and it is on the fast tier.**
`tests/e1000case/system.toml` hands netd `pci:8086:10d3` — QEMU's `e1000e`,
which publishes MSI at 0xd0 *and* MSI-X at 0xa0 — and `https_tls13_e1000e` is
`Tier::Fast`, so every pull request already boots a claimed function that
publishes both. `iommu::armed_on_msix` takes the `vendor:device` the boot config
declares, finds the BDF that function was handed over on, and requires
`msix address=` for it and refuses `msi address=`. The id comes from the new
`Bench::claims` field rather than from whichever function the guest reports, so
the assertion is about the function the harness declared.

**The mutation that reds it, run.** `bring_up`'s match reordered to try
`enable_msi` first, nothing else touched:

```
[kernel 0.324 cpu0] PCI 00:03.0: msi address=0xfee00098 data=0x00000000
[kernel 0.329 cpu0] pcidev: PCI 00:03.0 [8086:10d3] handed over on slot 0, vector 0x28
FAIL https_tls13_e1000e: "PCI 00:03.0: msix address=" never reached the boot console
ALONE https_tls13_e1000e: red again, the same failure both times — the defect is real.
test result: FAILED. 0 passed, 1 failed, 1 total (6.5s)
```

exit **1**. Every fetch, refusal and differential arm below it still passed:
the card works on MSI, which is why nothing but this assertion sees the order.

**The arming's own answer is read back, not just `capability`'s.** `bring_up`
matches on `enable_msix`, so that is what the self-test now asserts: each walk
case reads `device.enable_msix(0)` as well as `device.capability(msix::CAP_ID)`.
No layout there publishes MSI-X, so the call returns at its lookup and touches
no MMIO. Mutation, run: `enable_msix`'s `map_err(Unarmed::NoTable)` replaced by
one reporting every miss as `Absent` —

```
virtio: pci cap selftest FAILED on a list that cycles on itself: the arming reads absent, want truncated
  (and the same on the misaligned link, the below-header link, the forbidden head
   and the capability reached before a forbidden link)
virtio: pci cap split 8/13
virtio: pci cap selftest 15/15
test result: FAILED. 0 passed, 1 failed, 1 total (4.5s)
```

exit **1**, with `15/15` green beside it.

**A case that both finds and truncates.** Every layout published only
`PCI_CAP_ID_VENDOR`, so `Ok(_) => "found"` was unreachable and the split only
ever separated absent from truncated. One row adds the shape this branch exists
to refuse — a capability the walk reaches, then a link the spec forbids:
`(0x40, msi::CAP_ID, 0x43)`, where `capability(msi::CAP_ID)` must answer found
and `capability(msix::CAP_ID)` truncated. `CASES` is 15 and `SPLITS` 13.

**The hole that remains is measured now, not asserted.** `bring_up`'s two
`NoTable` arms replaced by one falling through to `enable_msi` — the whole
privilege fix reverted — leaves `pci_capability_walk`, `virtio_net_no_msix`,
`https_tls13_e1000e` and `pci_function_is_exclusive` all **green**, exit 0 each.
Reaching it needs a *claimed* function whose capability list ends early, which
nothing this harness hands to a claim has;
`issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md` says so in
those words. One thing does stand against it without a test: `-D dead-code`
refuses a kernel in which `Refusal::CapsTruncated` is never constructed, so that
mutation does not build until the lint is suppressed.

Prose deleted rather than rewritten, at the sites the review named:
`pcidev`'s `account_for` coverage narration, which is both a claim about what
reads it back and one another landing moves; `pci_cap_selftest`'s "Fourteen
crafted PCI capability layouts … the five walks among them", two counts this
commit moves; the four lines narrating the two deleted assertions; the three
narrating `xhci_second_controller`'s MSI-X count, which the refusal below them
states; `MSI_ARMED`'s doc, which stated a discrimination the constant could not
carry; and `cap_selftest`'s `SPLITS` doc, positional narration of the rows above.

**Withdrawn by name. History is not rewritten here.**

- The pull request body, `68ba8df6`'s message and the round-2 handoff all say
  `pci_function_is_exclusive`'s assertion "cannot be shown by a mutation: no
  function this harness hands to a claim publishes both mechanisms". False.
  `tests/e1000case` hands one to a claim on the fast tier, and the mutation
  above is run against it.
- `a2be5f84`'s message justified deleting `userland/netd/src/i219.rs`'s BAR
  clause with "which this branch falsifies — the T14's `00:1f.6` publishes no
  MSI-X, so nothing is withheld from that claim". False: the clause is about
  code that runs on `tests/e1000case` too, where the claimed `8086:10d3` does
  publish MSI-X with its table in BAR 3, so the kernel does withhold that BAR
  and does report 0 bytes for it. The invariant returns as one clause at the
  site.
- The body's "Retracted by name" section says
  `git diff --name-status 835cf8e HEAD | wc -l` is **50** and names "eleven
  `src/*.rs`". At `68ba8df6` that command printed **53** and
  `git diff --name-only 835cf8e HEAD -- 'src/*.rs' | wc -l` printed **14**; 50
  is the previous head `88aedad9`'s number. The body carries this head's.

Green in this worktree, each the command's own exit status:
`cargo run -- --build-only` exit 0;
`cargo test --test toyos-build -- pci_capability_walk` `PASS` exit 0;
`cargo test --test toyos-build -- https_tls13_e1000e` `PASS` exit 0;
`cargo test --test toyos-build -- pci_function_is_exclusive` `PASS` exit 0;
`cargo test --test toyos-build -- xhci_second_controller` `PASS` exit 0;
`cargo test --test toyos-build -- virtio_net_no_msix` `PASS` exit 0;
`cargo test --test toyos-build -- --nightly iommu_virtio_platform` `PASS` exit 0;
`cargo test --lib` 307 passed, 1 ignored, exit 0;
`cargo test --workspace --exclude toyos-build` 1367 passed, 0 failed, exit 0;
`cargo run -- --clippy` `clippy: 5 invocations clean` exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Round 3 left the privilege half of this branch unwitnessed and said so: reverting
`bring_up`'s two `NoTable` arms left every tier green, and the tracker priced the
alternative at "a boot config carrying such a function and a test binary to hold
it". That price is refuted by the tree, and a branch that adds a privilege path
does not land with the mutation that would red it missing.

**The shape is staged on a function an existing boot config already claims.**
`pcidev-caps-truncated` is one `actuators!` row; `StagedCaps` in
`kernel/src/drivers/pci.rs` holds one requester id across the hand-over's own
walks, and `CapabilityIter::next` ends that function's list one byte off dword
alignment after its MSI capability. Every other walk in the machine reads the
list the device published — the static is `NO_FUNCTION` until a claim arms it and
again when the guard drops. `tests/e1000case`'s `8086:10d3` publishes MSI at 0xc8
→ 0xd0 and MSI-X at 0xa0, so the staged walk yields MSI, never reaches the MSI-X
capability, and `bring_up` refuses `CapsTruncated`. No holder is needed: the arm
under test is a refusal, and `pci_claim_caps_truncated` reads it off the console
of a boot where netd's claim was refused.

**What the judge asserts is the privilege decision and not the log line.** The
refusal by its own reason; no `msi address=` for that function, which is the
arming on a list this kernel had no right to trust; no hand-over of
`[8086:10d3]`; no BAR moved, so the BAR the MSI-X table lives in is still where
firmware put it; init's own refusal line; netd's exit; and the machine otherwise
whole.

**The vacuous assertion is deleted a third time, and by ruling.**
`iommu::armed_on_msix` was called from `tls13_judge` for both benches, so
`must_not_say("PCI 00:03.0: msi address=")` ran on every `https_tls13`. QEMU's
`virtio-net-pci-non-transitional` publishes MSI-X at 0x98 and five vendor
capabilities and no id 0x05, so `enable_msi` returns at its lookup and no
implementation of this kernel can print that line. `94341ce1` deleted the
identical arm from `tests/common/iommu.rs` and `eea8c7f7` deleted it from
`pci_function_is_exclusive`; it came back a third time one call site further out.
The orchestrator's round-4 ruling is that it is deleted and stays deleted. It is
not in `tests/common/https.rs` at all now: `tls13_judge` answers the boot console
and the `https_tls13_e1000e` registration — the bench whose claimed function
publishes both mechanisms — makes the assertion.

**`Bench::claims` is read against the file it names.** It restated
`tests/e1000case/system.toml`'s `devices` row in Rust, which is the drift
`tests/common/devices.rs`'s `the_config_runs_exactly_these_jobs` exists to
prevent. `every_bench_claims_what_its_config_declares` follows that pattern:
it reads each bench's `system.toml`, takes `[programs.netd]`'s own row, and
asserts the constant against it. `tests/netcase` declares the same function
twice — once for netd and once for the test binary that asks for a second claim —
so the section matters.

**The crafted layout is the case its name claims.** `(0x40, msi::CAP_ID, 0x43)`
alone put no MSI-X capability anywhere in that config space, so
`capability(msix::CAP_ID)` answered `Truncated` over a list that had nothing to
find: the case could not tell a walk stopped at the forbidden link from one that
reached the end of a list with no table. `(0x44, msix::CAP_ID, 0)` is the other
half of the shape the body describes — a capability the walk reaches, a link the
spec forbids, and a table past it — so `truncated` now means "there is one and it
was not read". `CASES` stays 15 and `SPLITS` 13: the row joins a case rather than
adding one.

**The `-D dead-code` claim is withdrawn.** Round 3's body and `eea8c7f7`'s message
offered "`-D dead-code` refuses a kernel in which `Refusal::CapsTruncated` is
never constructed" as what stood against the hole without a test. It is not a
guard: the lint fires on the mutation that keeps the variant, never on a
regression that reached `main`, and a mutation that deletes the variant with the
arms — which is the whole change reverted, and the mutation run below — builds
clean.

Prose deleted rather than rewritten, at the sites the review named:
`tests/toyos.rs`'s "fourteen crafted layouts" and the six-item list after it, a
count this branch moved; `tests/common/https.rs`'s "QEMU's `e1000e` publishes MSI
at 0xd0 and MSI-X at 0xa0", a measurement's provenance and two offsets a
`.github/qemu-version` bump moves; `Bench::claims`'s restatement of
`armed_on_msix`'s own doc; `cap_selftest`'s "The shape the claim path exists to
refuse", which restated the case name on the line below it; and the narrating
halves of the two comments at the `reached` and `enable_msix` reads, the
load-bearing clause of each kept.

**Mutation 1 — the privilege decision, and it reds now.** `bring_up`'s two
`NoTable` arms replaced by one falling through to `enable_msi`, with
`Refusal::CapsTruncated` and its `Display` arm deleted: the whole change
reverted, which is also why it builds. `cargo build --target x86_64-unknown-none
--features boot-actuators,test-actuators` in `kernel/` exit **0**, then
`cargo test --test toyos-build -- pci_claim_caps_truncated` exit **1**:

```
FAIL pci_claim_caps_truncated: "NOT HANDED OVER" never reached the boot console:
[kernel 0.345 cpu0] PCI 00:03.0: msi address=0xfee00098 data=0x00000000
[kernel 0.354 cpu0] pcidev: PCI 00:03.0 BAR 3 (0x4000 bytes) moved to 0xc0600000
[kernel 0.354 cpu0] pcidev: PCI 00:03.0 [8086:10d3] handed over on slot 0, vector 0x28
ALONE pci_claim_caps_truncated: red again, the same failure both times — the defect is real.
test result: FAILED. 0 passed, 1 failed, 0 stale or expired expected-failure entries, 0 invalidated, 1 total (7.9s)
```

BAR 3 is the BAR that function's MSI-X table register names, and the mutated
kernel moved it and gave it to netd with everything else.

**Mutation 2 — the ordering, re-run at this head.** `bring_up` tries
`enable_msi` first and falls back to `enable_msix`, nothing else touched. Build
exit **0**, `cargo test --test toyos-build -- https_tls13_e1000e` exit **1**:

```
[kernel 0.354 cpu0] PCI 00:03.0: msi address=0xfee00098 data=0x00000000
FAIL https_tls13_e1000e: "PCI 00:03.0: msix address=" never reached the boot console
ALONE https_tls13_e1000e: red again, the same failure both times — the defect is real.
test result: FAILED. 0 passed, 1 failed, 0 stale or expired expected-failure entries, 0 invalidated, 1 total (12.8s)
```

**Mutation 3 — the arming's own answer, re-run at this head.** `enable_msix`'s
`map_err(Unarmed::NoTable)` replaced by one reporting every miss as `Absent`.
Build exit **0**, `cargo test --test toyos-build -- pci_capability_walk` exit
**1**:

```
virtio: pci cap selftest FAILED on a list that cycles on itself: the arming reads absent, want truncated
  (and the same on the misaligned link, the below-header link, the forbidden head
   and the capability reached before a forbidden link)
virtio: pci cap split 8/13
virtio: pci cap selftest 15/15
```

Green in this worktree, each the command's own exit status:
`cargo test --test toyos-build -- pci_claim_caps_truncated` `PASS` exit 0;
`cargo test --test toyos-build -- pci_capability_walk` exit 0;
`cargo test --test toyos-build -- https_tls13` exit 0;
`cargo test --test toyos-build -- virtio_net_no_msix` exit 0;
`cargo test --test toyos-build -- pci_function_is_exclusive` exit 0;
`cargo test --test toyos-build -- https_tls13_e1000e` exit 0 (`PASS` in 4 s; its
first run in this session hit the 300 s fetch budget while three other checkouts
held all twelve guest slots, and the harness's own alone re-run was green);
`cargo test --lib` 308 passed, 0 failed, 1 ignored, exit 0;
`cargo test --workspace --exclude toyos-build` 139 `test result: ok` lines,
1367 passed, 0 failed, exit 0;
the five `cargo clippy` shapes `src/clippy.rs` declares, run as the `host` job
runs them, exit 0 each.

`pci_claim_caps_truncated` enters `tests/test-durations` as an `UNMEASURED`
marker with `shards=none`, which is the one red this commit knowingly carries:
`--merge-durations` refuses a committed marker, so the next commit takes the
price CI measures and assigns the final tier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu and others added 5 commits September 16, 2026 17:34
…reads

Round 4 found the branch had added logic beside logic that already did the
job, and prose beside prose that already said it. Both are deleted rather
than rewritten.

**One refused-claim judge.** `faults::claim_caps_truncated` was a third copy
of the shape `virtio_net_no_msix` and `iommu::no_unit_is_no_claim` already
had: refusal line, reason, no hand-over, no BAR, init's line, netd's exit.
`faults::refused_claim(log, claims, why)` is that judge, and the three arms
call it. What is left at each caller is what is that arm's own — the argv
check and the audio function for the crippled NIC, the actuator's boot
parameter for the truncated list, and, for the machine with no unit, that
*nothing at all* was handed over, which is more than the claim's own refusal
says.

**The address is the harness's, never the guest's.** The judge read the
refused function out of the console and then asserted about *that*, which is
an assertion about whichever function the kernel happened to name. It now
knows the profile's slot (`CLAIMED_AT`), refuses a console that refused any
other function, and anchors every line to it: `pcidev: PCI 00:03.0 NOT HANDED
OVER — <reason>` is one match rather than two unanchored ones. `msi address=`
and `msix address=` are both required absent now, where each arm had asked for
one of them.

**One spelling of each thing.** `NO_FUNCTION` was declared in
`drivers/pci.rs` and again in `iommu/vtd/fault.rs`; it is declared once, where
a function's address is a triple, and the fault handler reads it through the
import it already had. `NETD_EXITS` was declared twice and is now declared
once, beside the wait that uses it. `1af4:1041` was written in three places
and is now read from `https::VIRTIO.claims`, the constant
`every_bench_claims_what_its_config_declares` holds against the committed
`devices` row. The two lines a hand-over spends are built by `bar_moved` and
`msix_armed`, so the arm that requires them and the arm that requires their
absence cannot drift apart.

**The bench's row is read with the parser the harness has.**
`every_bench_claims_what_its_config_declares` hand-rolled a TOML section and
list scan, copied from `devices.rs`. It reads `toml::from_str` into a
`toml::Value` and indexes `programs.netd.devices`, as `load_audio_baseline`
does; a device row that is not a string panics by name rather than being
dropped.

**Prose deleted, not moved.** The escalation story — read the early end as a
terminator, MSI is armed on the guess, `msix_bar` withholds no BAR — was told
at `pcidev/mod.rs`'s header, at `drivers/pci.rs`'s `StagedCaps`, and again in
the judge's doc. The header keeps it; the other two are gone. The three-line
reason at `hda::arm_interrupt` and `xhci::wait::boot::arm_interrupt` is one
paragraph on `PciDevice::enable_msi`, the call both of them make.

**A compromise on the ordinary path is recorded.**
`place_bars`'s `Some(index) == table_bar` mutated to `false` hands the MSI-X
table's own BAR to the holder of every function that publishes a table, and
every tier stays green on base and branch alike. It is pre-existing and is not
fixed here; it is a bullet in
`issues/kernel/a-claims-own-refusals-are-read-by-nothing.md`, which this branch
created for exactly that list.

**The marker is replaced by what CI measured.** Run 35100393637's twelve
hosted shards priced `pci_claim_caps_truncated` at 2583 ms and its
`test-durations-merged` artifact carries the row; the `durations` job reds on
the committed marker and says so by name. 2583 ms is under `FAST_COMMIT_MS`
(8000), so `Tier::Fast` stands and no registration moves. That one row is
taken and no other: the profile's other names are this run's shard luck, which
`src/durations.rs` measures at 1.28x p10-p90.

Measured in /Users/jan/Dev/jan/toyos-msi, each the command's own exit:
`pci_claim_caps_truncated` 0, `pci_capability_walk` 0, `virtio_net_no_msix` 0,
`pci_function_is_exclusive` 0, `https_tls13` + `https_tls13_e1000e` 0,
`iommu_virtio_platform` (--nightly) 0; `cargo test --lib` 0;
`cargo test --workspace --exclude toyos-build` 0; the five `src/clippy.rs`
shapes 0 each. M1 re-run over the consolidated judge — both `NoTable` arms
falling through to `enable_msi`, `Refusal::CapsTruncated` and its `Display`
arm deleted, `grep -c CapsTruncated` 0 — builds exit 0 and reds exit 1, now on
the refusal itself ("this console refused []") with `msi address=`,
`BAR 3 … moved` and `[8086:10d3] handed over` in the evidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
… refuses

Round 4's ruling — the address a judge asserts about is the harness's own and
never the guest's — was applied to `refused_claim` and not one file over.
`iommu::armed_on_msix` still read the BDF out of the guest's `handed over on
slot` line and then asserted `msix address=` and `msi address=` for *that*, so a
kernel that armed MSI on the claimed function and named a different one in its
hand-over line passed: the judge found the other function's msix line and called
an MSI-armed `00:03.0` MSI-X-first. It now reads `faults::CLAIMED_AT`, asserts
the printed address equal to it, and spends the spellings `faults::msix_armed()`
and `faults::msi_armed()` — the `msi address=` line gets the constructor
`msix address=` already had, so neither is written twice.

`refused_claim`'s walk over `NOT HANDED OVER` lines was two `filter_map`s, so a
line carrying the marker without the `pcidev: PCI ` prefix was dropped rather
than red — a text scan closes only the spellings it matches, and a kernel that
renamed the prefix would have handed the judge an empty list. `functions_named`
is that walk for both judges and such a line is an `Err`.

Prose, by correction where it is evidence: the issue's opening no longer counts
the refusals that are read back, because this branch's own judge moved that
count; its `place_bars` bullet spells the mutation in the form that builds,
`(Some(index) == table_bar && false)` — a bare `false` is `unused variable:
table_bar`, kernel build exit 101 — and names the four tests measured green on
it rather than claiming every tier; and `CLAIMED_AT`'s doc says what is true of
the constant instead of "every profile in this suite", no profile placing that
function anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
`functions_named`'s doc said a renamed prefix would hand its caller an empty
list, which `refused_claim` already reds on. The case that passed is the partial
one: a console carrying one refusal this walk can parse and a second it cannot,
where the judge of "and no other function" is answered about the subset.

Comment only; no code moved, so the round's measurements stand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
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