Skip to content

VT-d: a moved context entry is invalidated under the domain it held, in one 16-byte write, and GCMD follows §11.4.4.1 - #700

Merged
Japabu merged 11 commits into
mainfrom
wt/toyos-vtdctx
Oct 4, 2026
Merged

Japabu merged 11 commits into
mainfrom
wt/toyos-vtdctx

Conversation

@Japabu

@Japabu Japabu commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

A function's context entry moved onto another domain is now invalidated under the domain it held, and replaced in one 16-byte write; every GCMD write is VT-d §11.4.4.1's recipe, and what firmware left on is switched off one field at a time once the unit's tables are built, just before the unit is programmed.

Spec read: Intel VT-d Rev. 4.1, D51397-016 (sha256 b0c122a5fa927578ff963636dd3abe87aed0a28f73bbf50e74dc54c099bf4aeb). The tree cites Rev. 4.0 (D51397-015); 4.1's revision history changes none of the sections cited below.

What changed, per decision

The invalidation names the old domain id (vtd/table.rs rebind/Displaced, vtd/queue.rs invalidate_context, vtd/mod.rs Live::attach). §6.5.2.1 invalidates a device-selective context-cache entry only where source-id and domain-id match, and §6.2.1 Table 15 has the IOTLB look a translation up by source-id while keeping it under the domain it was made in. attach named the domain the entry was moving to in both descriptors, so the cached old entry and the function's translations through it survived the move: F released from slot 0, G claims slot 0, F re-claimed into slot 1 may still translate through slot 0's domain, which maps G's grants. Now bind reads the entry it replaces, rebind (const) returns the new entry and a Displaced built from the old entry's DID only, and Queue::invalidate_context takes nothing but a Displaced. The IOTLB half also names the old id; nothing is invalidated under the new one, because a translation cached under it for this requester can exist only from an earlier stint there, which that move's own old-id invalidation removed.

A present entry changes in one lock cmpxchg16b (Table::replace_pair). §6.2.2.1 requires a 16-byte atomic write or a sequence whose every state is self-consistent; the two 8-byte stores wrote the DID half first, a present entry naming the new id over the old table. The compare also asserts that the entry replaced is the one whose id is invalidated. No CPUID check is added for CX16: every CPU with a VT-d unit has it, QEMU's qemu64 declares it (target/i386/cpu.c, v9.2.0 l. 2495), and a CPU without it takes #UD at the first move. bind now refuses (panics) a stream with no present entry: bind_identity gives every enumerated function one before TE, and only enumerated functions are attached (pcidev::claim searches MACHINE.functions, the same list iommu::init binds).

GCMD follows §11.4.4.1 (Unit::command): GSTS & 0x96FFFFFF with one field changed. The shadow gcmd started at 0, so the first write (QIE) also cleared any TE, IRE or CFI firmware left — several fields in one write, which the section forbids.

What firmware left on is switched off one field per write, after the unit's tables are built and before its queue is pointed (Unit::hand_over, FIRMWARE_LEFT, called in enable after identity_root and before Queue::new, outside the TABLES lock): translation, compatibility-format pass-through, interrupt remapping, then queued invalidation once IQH == IQT. Kept on, the rest of the sequence would break §6.6 and §6.7 (a live unit's tables may move only onto tables that translate identically) and §6.5.2 (a queue is set up while queued invalidation is off), and CFI would ride into this kernel's IRE. CFI goes before IRE because §11.4.4.2 reports CFIS only while remapping is on. After the build, because a unit firmware left translating passes DMA untranslated from the hand-over until TE is back on; that window remains, and is filed. Differential: Linux v6.16 seeds gcmd from GSTS (dmar.c:1116-1121), disables a firmware queue after IQT == IQH (dmar_disable_qi), and disables pre-enabled TE and IRE outside kdump (iommu.c:2126-2128, irq_remapping.c:589-593). The unit line now carries gsts= as firmware left it.

iommu-firmware-left (boot actuator, #[cfg(feature = "boot-actuators")] leave_on) leaves every unit queueing, translating and remapping, each batch ended by a wait as this kernel's are, and writes CFI unconfirmed. It translates through the identity domain enable itself uses, never an empty root: enable looks the per-width domain up (building it once, logged) before the actuator runs and hands it to leave_on, and both take their root from one function, identity_root, which allocates a root and gives every enumerated function a context entry in that domain. The selftests boot carries it; its row iommu_firmware_left judges that every unit that translates said it switched off translation, queued invalidation and, where its unit line reads ir=y (through tests/common/iommu.rs's unit_fields), remapping; a translating unit no line describes is red. It is in src/metal.rs's FLASHABLE: it writes only unit registers a reset returns and tables in memory the boot owns.

The T14 red at 9be0a8ac8, and what the probe read.

The boot:

  • The selftests boot at 9be0a8ac8 (actuator pointing each unit at an empty root) panicked at 0.156 s, after unit0 logged translating (orch/vtdctx-r2/metal/selftests/loader.log 172–185).
  • It logged unit0 fault recording overflowed, then DMA FAULT owner=kernel unit0 stream=00:02.0 addr=0x000000009c855000 access=read reason=0x06.
  • That is the integrated GPU reading inside rmrr0 (0x9c000000..0xa07fffff, scope 00:02.0).
  • At 25ab31bd5 (actuator translating through an identity domain), the same boot passed.

Round 3's commit message (25ab31bd5) blamed records that overflowed during the empty-root window, and that account is withdrawn. Review round 2 showed why:

  • fault::arm clears every record and FSTS before TE comes back on.
  • An empty root faults 0x01.

What the FSTS probe read (9be0a8ac8 plus probe-fsts-9be0a8ac8.patch; the orchestrator's T14 run, comment 5974618171; orch/vtdctx-r4/metal-probe/selftests/loader.log 177–199):

  • before fault::arm clears: fsts=0x00000003 (PFO, PPF), and the one record is 00:02.0 reading 0x9cb80000 with reason 0x01. These are the empty root's refusals, overflowed.
  • TE confirmed: fsts=0x00000000, and the record is clear (f=n).
  • then unit0 fault recording overflowed, and 00:02.0 reading 0x9cc39000 with reason 0x06.

So the overflow and the 0x06 were both raised through this kernel's identity domain.

Against Rev. 4.1, 0x06 is LGN.3 (Table 26): a read that meets a second-stage entry without R. None of the causes the spec allows applies:

  • The tables. Every entry on that walk grants read (LEAF_PERM, descend), and each line is written back by clflush because ECAP.C=0.
  • The context entry. It is TT=00, AW=2, DID=1 in every arm.
  • Cached state. A unit reporting CM=0 caches no faulted translation (§6.2.4.1). SRTP is followed by global context-cache and IOTLB invalidations, both acknowledged (§6.6).
  • The RMRR. It lies inside the identity domain, mapped read-write.

The same code ran green in two other arms:

  • At 55a5ccb42, the refusal and the hand-over were both logged at 0.153 s, and TE stayed off until 0.155 s.
  • At 25ab31bd5 and 5e8cbb06e, nothing was refused.

What the walk probe read on the T14 (9be0a8ac8 plus probe-walk-9be0a8ac8.patch, the third red of that actuator; orch/vtdctx-r5/metal-walk/selftests/loader.log 135–197, orch/vtdctx-r5/probe-walk-lines.txt):

  • before fault::arm clears: again fsts=0x00000003 and one record, 00:02.0 reading 0x9ccc0000 with reason 0x01.
  • at the fault, 00:02.0 reading 0x9cdb7000 with reason 0x06, raw hi=0xc000000600000010: at=0 pp=0 exe=0 priv=0 t2=0, a plain untranslated read.
  • the unit's walk from RTADDR 0xa18000, each entry read from DRAM after clflush, at TE and at the fault alike: root 0xa19001, context 0xa04001:0x102 (p=1 tt=0 aw=2 did=1), L4 0xa05003, L3 0xa08003, leaf 0x9cc00083, a present 2 MiB read/write entry over 0x9cdb7000.
  • the GPU's PCI STATUS reads 0x0010, rma=0 rta=0, before the actuator, before the hand-over, at TE and at the fault.

What the gap probe read (the walk patch plus 3 ms with TE off after each hand-over, probe-gap-9be0a8ac8.patch; orch/vtdctx-r5/metal-gap/selftests/kernel.log, orch/vtdctx-r5/probe-gap-lines.txt): the same empty-root refusals before fault::arm (fsts=0x00000003, record 0x01), then unit0 held with translation off for 3 ms after the hand-over (line 123), no DMA FAULT line and Boot: complete (1194ms) (line 344); verdict.txt passed.

Conclusion.

  • The unit refused a read the tables in memory grant: the 0x06 is neither in this kernel's tables nor in any cached state VT-d Rev. 4.1 lets the unit keep.
  • It needs both a period in which the unit refuses the GPU's reads and TE back on within microseconds of the hand-over: three boots of the empty-root actuator with no hold faulted, the one with a 3 ms hold did not, and 55a5ccb42 (TE off about 2 ms after its refusals) did not.
  • The only fix that reading names is a timed hold, a flat wait no hardware document mandates that also widens the untranslated window filed below; this branch does not ship one, and the issue's exit refuses it.
  • Reaching it takes a unit left translating that refuses a function's RMRR reads. Firmware does not do that on the T14 (gsts=0x40000000 on all four units), and this head's actuator does not do it.
  • By reading origin/main's enable, not run: its first GCMD write (QIE, from a shadow of 0) clears a firmware-left TE, and TE comes back on after SRTP, IRTA and the global invalidations, as fast as this branch's hand-over does. No boot of main in a refusing state was run.
  • It is filed with the reproduction as its exit: issues/kernel/a-function-refused-under-translation-faults-read-permission-after-the-hand-over.md.

No cheaper tier reaches it. The failure needs a function mastering the bus while the actuator's state is live, and no QEMU guest has one: nothing DMAs between leave_on and the hand-over there, so the hand-over probe was green at 9be0a8ac8 and is green now. A host test over the order sees only the order.

Filed:

  • issues/kernel/a-released-function-still-names-its-slots-domain.md — release leaves the function's entry on its slot's domain, which the slot's next holder is attached to.
  • issues/kernel/a-unit-left-translating-passes-dma-untranslated-while-programmed.md — the window between the hand-over and TE back on.
  • issues/kernel/a-unit-this-kernel-does-not-program-keeps-what-firmware-left-on.md — a unit plan refuses, or past MAX_UNITS, is never handed over.
  • issues/kernel/a-function-refused-under-translation-faults-read-permission-after-the-hand-over.md — the 0x06 above.

Checks (high-risk: security boundary, device driver)

Oracle: the specification, cited in one clause at each changed site; Linux v6.16 as the differential implementation (domain_context_clear_one, iommu.c:1719-1738, reads did off the entry it clears and hands it to intel_context_flush_no_pasid, pasid.c:1120-1150, for both descriptors); QEMU 11.1.1's own descriptor decode, traced; the T14.

Logs are in the orchestrator's scratchpad: round 7's in orch/vtdctx-r7/, round 6's in orch/vtdctx-r6/, round 5's in orch/vtdctx-r5/, round 4's in orch/vtdctx-r4/, round 3's in orch/vtdctx-r3/, round 2's in orch/vtdctx-r2/, round 1's in orch/vtdctx/. Every probe and mutation patch is in a comment below, each applied with git apply --check, run, and reverted in the same script.

At the head, 041014fdf (origin/main 7a6b667ed merged, which brings #703 and with it new crate paths in kernel/Cargo.toml, kernel/pci/Cargo.toml, src/ and the userland the image carries; since 6fdd9c579 the merge and the issue's text):

check command exit what it shows
host suites cargo run -- --ci host 0 Host: 72 step(s), all green (orch/vtdctx-r6/ci-host.log)
image cargo run -- --build-only 0 Boot image: …/target/bootable.img (orch/vtdctx-r7/build-only.log)
guest suite cargo test --test toyos-build 0 26 passed, 26 total, iommu_virtio_platform among them (orch/vtdctx-r7/toyos-build.log)
kernel cd kernel && cargo check 0 (orch/vtdctx-r7/kernel-check.log)
kernel with actuators cd kernel && cargo check --features boot-actuators 0 (orch/vtdctx-r7/kernel-check-actuators.log)

At 6fdd9c579 (orch/vtdctx-r5/), that commit's runs:

check command exit what it shows
host suites cargo run -- --ci host 0 Host: 69 step(s), all green (ci-host.log)
image cargo run -- --build-only 0 (build-only.log)
guest suite cargo test --test toyos-build 0 26 passed, 26 total; every x86 guest boots behind intel-iommu (guest-suite.log)
kernel cd kernel && cargo check 0 (kcheck.log)
kernel with actuators cd kernel && cargo check --features boot-actuators 0 (kcheck-act.log)
staging the selftests boot cargo test --test toyos-build -- --metal --metal-readback orch/vtdctx-r5/metal-head iommu_firmware_left 2 staged: image sha256 d2dfe4d3b632d1f8940606860e4d46f3cc57ca27e996e8379d01602170039552 (metal-stage.log)
that boot on the T14 cargo run --bin toyos-metal -- --image orch/vtdctx-r5/metal-head/selftests/image.img --readback orch/vtdctx-r5/metal-head/selftests --fat32-check, the image's sha256 checked in the flashing command (the orchestrator's run, comment 5974876749) the orchestrator's verdict.txt reads passed; each unit translating gsts=0xc7000000 at 0.155–0.156 s (kernel.log 121–136), no DMA FAULT or overflow line, Boot: complete (1154ms) (line 311)
every row of that boot cargo test --test toyos-build -- --metal --metal-readback orch/vtdctx-r5/metal-head <row>, once for each of input_merge, iommu_firmware_left, lapic_spurious_vector, leak_rollback_selftest, operation_nesting, pci_capability_walk, read_fault_selftests, sysret_ss_reload, xhci_descriptor_walk, xhci_xecp_walk (the orchestrator's, from the clean head) 0, ten times each 1 registration(s), 1 passed, 0 failed, 1 boot(s); iommu_firmware_left: 4 unit(s) programmed after switching off what they were handed over with (judge-t14-*.log)
staging the walk probe worktree detached at 9be0a8ac8, probe-walk-9be0a8ac8.patch applied (comment 5974826699), cd kernel && cargo check --features boot-actuators,test-actuators exit 0, cargo test --test toyos-build -- --metal --metal-readback orch/vtdctx-r5/metal-walk iommu_firmware_left, patch reverted 2 staged: image sha256 e997d5802985c67cf1580c4a02eebc640ebc94c9cf07650755aba20ea8784cbc (walk-stage.log)
the walk boot on the T14 cargo run --bin toyos-metal -- --image orch/vtdctx-r5/metal-walk/selftests/image.img --readback orch/vtdctx-r5/metal-walk/selftests --fat32-check, sha256 checked (the orchestrator's run, comment 5974876749) the orchestrator's; toyos-metal refused verdict.txt: refused, no Boot: complete record; the panic and the readings above (loader.log 135–197)
staging the gap probe the same with probe-gap-9be0a8ac8.patch into orch/vtdctx-r5/metal-gap; back to the head afterwards, tree clean 2 staged: image sha256 3f555e92db15099514a535f9653058d9a4e7a8a0ab3a62561f278f1442c6a2fd (gap-stage.log)
the gap boot on the T14 cargo run --bin toyos-metal -- --image orch/vtdctx-r5/metal-gap/selftests/image.img --readback orch/vtdctx-r5/metal-gap/selftests --fat32-check, sha256 checked (the orchestrator's run, comment 5974876749) the orchestrator's verdict.txt reads passed; the readings above (kernel.log 123, 344)

What they decided: the walk grants read in DRAM, so the tables are not what refuses; a 3 ms TE-off removes the fault, on one boot; the head is green on silicon.

At 5e8cbb06e (orch/vtdctx-r4/), whose vtd/ the head leaves as it was:

check command exit what it shows
hand-over after the build, on QEMU, actuator on hand-over probe: actuator added to declining_is_not_free's boot and the row's judge called on its log 0 one identity domain … covers at 0.094 s, before unit0's hand-over at 0.098–0.102 s, translating at 0.108 s (probe-fix.log 588–592)
before it, the same probe at 401d6f7d5 same 0 hand-over 0.088–0.091 s, identity domain … covers 0.092 s, translating 0.095 s (round 1's judge-fix.log 589–594)
negative control: hand-over collapsed into the base's one GCMD write of 0, at 9be0a8ac8 same probe 1 unit0 translates and never said it was handed over with queued invalidation on (orch/vtdctx-r2/nc3.log)
staging the FSTS probe boot (review round 2, BLOCKER 1) worktree detached at 9be0a8ac8, probe-fsts-9be0a8ac8.patch applied (comment below), cargo test --test toyos-build -- --metal --metal-readback orch/vtdctx-r4/metal-probe iommu_firmware_left, patch reverted, back to the head, tree clean 2 staged: image sha256 b133204ec52429afdbbd6916056809a731c8499b5d2ac3c5647cf681bc8cd02b; logs FSTS, FECTL, GSTS and the one fault record in fault::arm before it clears them, and again once TE is confirmed (probe-stage.log)
that probe boot on the T14 the orchestrator's run, comment 5974618171 the orchestrator's; toyos-metal refused (no Boot: complete) the readings above (orch/vtdctx-r4/metal-probe/selftests/loader.log 177–199)
the selftests boot at 5e8cbb06e on the T14 the orchestrator's run, comment 5974618171; then cargo test --test toyos-build -- --metal --metal-readback orch/vtdctx-r4/metal-head <row> for each of the ten rows 0, ten times each 1 passed, 0 failed; kernel.log 116–121: build at 0.155 s, unit0's hand-over and translating at 0.156 s; no DMA FAULT or overflow line

On the T14, the selftests boot at 25ab31bd5 (the orchestrator's run, comment 5974415448; readback in orch/vtdctx-r3/metal/selftests/), the actuator translating through an identity domain and the hand-over after the build:

check command exit what it shows
the boot cargo run --bin toyos-metal -- --image orch/vtdctx-r3/metal/selftests/image.img --readback orch/vtdctx-r3/metal/selftests --fat32-check, image sha256 1e29e3a2c203be936b2b9a764540aebd82d91239e8e712c69e6a23881843fc95 the orchestrator's verdict.txt reads passed; Boot: complete (1164ms) (kernel.log 311)
every row of that boot cargo test --test toyos-build -- --metal --metal-readback orch/vtdctx-r3/metal <row>, once for each of pci_capability_walk, read_fault_selftests, leak_rollback_selftest, lapic_spurious_vector, xhci_xecp_walk, xhci_descriptor_walk, sysret_ss_reload, input_merge, operation_nesting, iommu_firmware_left 0, ten times each 1 registration(s), 1 passed, 0 failed, 1 boot(s); iommu_firmware_left: 4 unit(s) programmed after switching off what they were handed over with (orch/vtdctx-r3/judge-t14-*.log)
the hand-over on silicon kernel.log 116–136 the identity build at 0.158 s, then each of the four units names translation, compatibility-format pass-through, interrupt remapping and queued invalidation, then translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n … eime=y
no fault grep -n "DMA FAULT|overflow" kernel.log 1 no line

What it decided: the 9be0a8ac8 red does not recur with the hand-over still after the build, so round 1's order is kept; the TE-off gap of the GPU quirk named under Unsure did not fault on this boot. Why 9be0a8ac8 faulted is the FSTS probe's reading, above.

Negative control on silicon: the selftests boot at 9be0a8ac8 (the orchestrator's run; toyos-metal refused): panic at 0.156 s on the 00:02.0 read fault above, all ten rows red (orch/vtdctx-r2/metal/selftests/loader.log 172–185).

At 401d6f7d5, whose table.rs, queue.rs, Live::attach and hand_over later rounds leave as they were:

check command exit what it shows
QEMU trace, fix iommu_virtio_platform with -trace vtd_inv_desc_cc_device -trace vtd_inv_desc_iotlb_domain (probe patch) 0 each of the five context invalidate device 00:0N.00 is followed by iotlb invalidate whole domain 0x1 — the identity domain each entry held
negative control: whole fix reverted onto origin/main, same trace git apply -R of git diff origin/main HEAD -- kernel/ 0 each is followed by the new domain, 0x2…0x6, and the guest is green: no QEMU gate sees this defect
negative control: rebind names domain.id cd kernel && cargo check 101 evaluation panicked: assertion failed: displaced.domain == KERNEL_DOMAIN (table.rs const block)

On the T14, the selftests boot at 55a5ccb42 (the orchestrator's run, toyos-metal exit 0, readback in orch/vtdctx/metal/), the hand-over before the build:

check command exit what it shows
every row of that boot cargo test --test toyos-build -- --metal --metal-readback orch/vtdctx/metal <row>, once for each of the ten rows above 0, ten times each 1 passed, 0 failed, 1 boot(s), the tree clean after each (orch/vtdctx-r2/judge-t14-55a5ccb42.log)
firmware's own state kernel.log 101–110 all four units gsts=0x40000000: only the root table pointer status; firmware leaves nothing on, so on the T14 hand_over runs only under the actuator
CFIS on silicon kernel.log 116–136 silicon reports CFIS after CFI is written with IRE on and EIME off, which QEMU never does (issues/kernel/qemu-passes-compatibility-format-interrupts.md)
a move on silicon kernel.log 206, 227, 299, 311 00:0d.0 moves to domain2, 00:14.0 moves to domain3, 00:1f.3 moves to domain4, through cmpxchg16b and the old-id descriptors; Boot: complete (1152ms)

The T14 is no oracle for the stale-context escape

§6.5.2.1 lets hardware invalidate more coarsely than asked — a unit that drops a device-selective entry by source-id alone never shows the defect — and whether F's old entry is still cached when F next masters the bus is the unit's replacement policy, which nothing here can observe or force. A no-escape on the base kernel would not refute the defect, and a no-escape on the fix would not confirm it, so no row stages it.

Unsure

  • Whether the GPU or the unit carries the refused reads across the hand-over into a 0x06 (filed above). The walk rules out the tables and the GPU's abort bits; the gap shows a TE-off of 3 ms is enough to clear it, on one boot, and does not say which side held it. This GPU's id, 9a49, is in Linux v6.16's quirk_igfx_skip_te_disable list (iommu.c:4502-4504); the hand-over still switches its unit's TE off, and the 25ab31bd5 boot did not fault on that.
  • §6.5.2's other quiescence condition, that the last completed descriptor was a wait, is firmware's and unreadable; a firmware that enabled queued invalidation and never sent one makes QEMU refuse the disable and this kernel panic on the bound. Real silicon's answer is unmeasured: the T14's firmware leaves queued invalidation off.

Growth

git diff --shortstat origin/main...HEAD (merge base 7a6b667ed): 13 files, +457/−127. Production kernel: +259/−126 across vtd/ and actuator.rs (+5); leave_on is compiled only with boot-actuators. Tests and gate: tests/toyos.rs +48, tests/common/iommu.rs +1/−1, src/metal.rs +5. Issues: +144, of which the read-permission fault's is +69.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8

Japabu and others added 2 commits October 3, 2026 23:46
…and GCMD follows §11.4.4.1

Moving a function's context entry onto another domain invalidated the
context cache and the IOTLB under the domain it was moving TO. VT-d Rev.
4.0 §6.5.2.1 invalidates a device-selective context-cache entry only where
both the source-id and the domain-id match, and §6.2.1 has the IOTLB look a
translation up by requester while keeping it under the domain it was made
in. So the entry the unit held for the old domain, and every translation the
function made through it, survived the move: a function released from slot
0, whose slot G then claimed, re-claimed into slot 1 could still translate
through slot 0's domain, which now maps G's grants. QEMU ignores the
domain-id of a device-selective context invalidation (intel_iommu.c,
vtd_process_context_cache_desc), so no guest could see it. Linux reads the
id off the entry it replaces (domain_context_clear_one,
intel_context_flush_no_pasid) and so does this now: `table::bind` reads the
entry, `rebind` decides the new entry and a `Displaced` that only the old
entry's id goes into, and `Queue::invalidate_context` takes nothing else. A
const block holds `rebind` to it: a move off the identity domain onto domain
0xffff is invalidated under domain 1.

The entry itself was rewritten as two 8-byte stores, the domain-id half
first, which leaves a present entry naming the new id over the old table.
§6.2.2.1 asks a present entry to change by a 16-byte atomic write or through
states each self-consistent; it now changes in one `lock cmpxchg16b`, which
also proves the entry replaced is the one whose id is invalidated.

Every GCMD write started from a shadow that began at 0, so the first one
(`QIE`) also dropped any `TE`, `IRE` or `CFI` firmware had left on, several
fields in one write where §11.4.4.1 asks one. `Unit::command` is now the
section's recipe: GSTS & 0x96FFFFFF with one field changed. What firmware left
on is then switched off deliberately, one field per write, before the unit is
pointed anywhere, because §6.6 and §6.7 let a live unit's tables move only
onto tables that translate alike and §6.5.2 sets a queue up only while queued
invalidation is off: translation, compatibility-format pass-through (while
GSTS still reports it, §11.4.4.2), interrupt remapping, then queued
invalidation once its queue is empty. Linux does the same for a handover it
cannot copy (init_dmars, dmar_disable_qi, intel_setup_irq_remapping). The
unit line now carries GSTS as firmware left it.

`iommu-firmware-left` stages that handover — every unit queueing,
translating and remapping through tables of its own, as this kernel leaves
one — and the selftests boot's `iommu_firmware_left` row judges it on the
T14: every unit that translates said it switched each of those off first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
Found while moving context entries: release resets the function and clears
its bus mastering, and leaves its context entry naming the slot's domain,
which the slot's next holder is attached to. Not fixed on this branch.

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

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Probe and mutation patches behind the checks in the body, each applied with git apply --check, run, and reverted in the same command (git status --porcelain --ignore-submodules=none empty after each). Measured at 401d6f7d5.

Trace probe (VTDCTX_TRACE=<dir> cargo test --test toyos-build -- --nocapture iommu_virtio_platform)

--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -2046,5 +2046,11 @@ fn qemu_command(
         (Arch::Aarch64, other) => panic!("`virt` has no `-vga {other}`"),
     }
     qemu.arg("-display").arg("none").arg("-no-reboot");
+    if let Some(dir) = std::env::var_os("VTDCTX_TRACE") {
+        let name = uart_log.file_name().expect("a uart log has a name");
+        qemu.arg("-trace").arg("vtd_inv_desc_iotlb_domain")
+            .arg("-trace").arg("vtd_inv_desc_cc_device")
+            .arg("-D").arg(Path::new(&dir).join(name).with_extension("trace"));
+    }
     if let Some((w, h)) = shape.panel {
         assert_eq!(arch, Arch::X86_64, "a panel is declared through VGA's EDID, and `virt` has no VGA");

Fix arm, exit 0, uart-0.trace:

vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x2
vtd_inv_desc_cc_device context invalidate device 00:01.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x1
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x3
vtd_inv_desc_cc_device context invalidate device 00:05.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x1
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x4
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x4
vtd_inv_desc_cc_device context invalidate device 00:04.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x1
vtd_inv_desc_cc_device context invalidate device 00:02.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x1
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x5
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x5
vtd_inv_desc_cc_device context invalidate device 00:03.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x1
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x6

Negative control — whole fix reverted (git diff origin/main HEAD -- kernel/ | git apply -R, kernel then identical to origin/main), same probe, exit 0, uart-0.trace:

vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x2
vtd_inv_desc_cc_device context invalidate device 00:01.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x2
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x3
vtd_inv_desc_cc_device context invalidate device 00:05.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x3
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x4
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x4
vtd_inv_desc_cc_device context invalidate device 00:04.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x4
vtd_inv_desc_cc_device context invalidate device 00:02.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x5
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x5
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x5
vtd_inv_desc_cc_device context invalidate device 00:03.00
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x6
vtd_inv_desc_iotlb_domain iotlb invalidate whole domain 0x6

Guest's moves, both arms: 00:01.0→domain2, 00:05.0→3, 00:04.0→4, 00:02.0→5, 00:03.0→6.

Negative control: rebind names the new domain (cd kernel && cargo check, exit 101)

--- a/kernel/src/arch/x86_64/vtd/table.rs
+++ b/kernel/src/arch/x86_64/vtd/table.rs
@@ -467,7 +467,7 @@ impl Displaced {
 /// what that leaves the unit holding: the id `old` named, never `domain`'s.
 const fn rebind(old: (u64, u64), requester: u16, domain: &Domain) -> ((u64, u64), Displaced) {
     let new = context_entry(domain.root, domain.id, domain.width);
-    (new, Displaced { requester, domain: (old.1 >> 8) as u16 })
+    (new, Displaced { requester, domain: domain.id })
 }
 
 /// Gives `stream` its first context entry, naming the identity domain.
error[E0080]: evaluation panicked: assertion failed: displaced.domain == KERNEL_DOMAIN && displaced.requester == 0x00F8
   --> src/arch/x86_64/vtd/table.rs:521:5
    |
521 |     assert!(displaced.domain == KERNEL_DOMAIN && displaced.requester == 0x00F8);
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `arch::x86_64::vtd::table::_` failed here

Hand-over probe: the actuator on a QEMU boot, judged by the row's own judge (cargo test --test toyos-build -- --nocapture iommu_virtio_platform, exit 0)

--- a/tests/common/iommu.rs
+++ b/tests/common/iommu.rs
@@ -215,13 +215,14 @@ fn declining_is_not_free(test_config: &Path) -> Result<(), String> {
         &[],
         BootOptions {
             profile: Profile::Headless,
-            kernel_params: &["virtio-no-access-platform"],
+            kernel_params: &["virtio-no-access-platform", "iommu-firmware-left"],
             ..Default::default()
         },
     );
     let log = Serial::boot(&qemu);
     log.must_be_clean()?;
     log.must_say("Boot: complete")?;
+    crate::iommu_firmware_left(log.text())?;
 
     let refused: Vec<&str> = log
         .text()

Negative control: hand-over collapsed into the base's single GCMD write (with the probe above, exit 1)

--- a/kernel/src/arch/x86_64/vtd/mod.rs
+++ b/kernel/src/arch/x86_64/vtd/mod.rs
@@ -359,7 +359,7 @@ impl Unit {
                     self.regs.read_u64(IQH_REG) == self.regs.read_u64(IQT_REG)
                 });
             }
-            self.command(bit, false, what);
+            self.regs.write_u32(GCMD_REG, 0);
         }
     }
 }
FAIL iommu_virtio_platform: unit0 translates and never said it was handed over with queued invalidation on, so the state the actuator left was never switched off by the hand-over:

@Japabu

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

T14 selftests boot at 55a5ccb42, run by the orchestrator.

  • Image sha256 checked before the flash (f64611cf…a0fa); the stick's log partition saved first; toyos-metal exit 0.
  • Judge from the clean head (cargo test --test toyos-build -- --metal --metal-readback <dir> iommu_firmware_left): EXIT=0.
PASS iommu_firmware_left
[metal] 1 passed, 0 failed, 1 boot(s)

What the boot's log shows about the hand-over:

  • Entry state from the firmware, all four units: gsts=0x40000000 (only the root-table-pointer status bit; translation, remapping and queued invalidation off).
  • With the iommu-firmware-left actuator leaving each unit translating, with compatibility-format pass-through, remapping and queued invalidation on, the kernel logs each as "was handed over with … on; it goes off first" and then programs the unit: translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n … eime=y.

As the body says, this boot is no oracle for the stale-context escape itself (§6.5.2.1 lets hardware invalidate more coarsely than asked). No record row was written; the tree is clean.

@Japabu

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1, head 55a5ccb42

Net: 9 files, +283/−84 (+199). Production kernel +215/−84: vtd/ +210/−84, of which leave_on is 24 lines and compiled only with boot-actuators, and the const test is 16 lines; actuator.rs +5. Tests and harness +49: tests/toyos.rs +44, src/metal.rs +5. Issue +19. I accept the growth: the hand-over, the atomic replace and Displaced each carry a spec requirement the base broke, and I found nothing to delete.

Merge with origin/main 4560019a4: git merge-tree --write-tree exit 0. Main moved by #697, which adds two issue files that no part of this diff touches, so nothing needs re-measuring.

Checked and holding:

  • Evidence at 401d6f7d5 covers the head, which adds only an issue file and no host gate reads it. --ci host exit 0 (Host: 67 step(s), all green), guest suite exit 0 (26/26), and both kernel checks exit 0.
  • Both controls hold: the whole fix reverted, traced, names the new domain on QEMU and stays green; rebind naming domain.id exits 101.
  • Spec read against Rev. 4.1: §6.2.2.1, §6.5.2, §6.5.2.1, §6.5.3.3 Table 25, §6.6, §11.4.4.1 (mask 96FFFFFFh) and §11.4.4.2.
  • The Linux differential matches: domain_context_clear_one / intel_context_flush_no_pasid use the old DID, and init_dmars disables pre-enabled TE outside kdump.
  • The T14 log has 00:0d.0 and 00:14.0 moved through bind, with the boot complete.

BLOCKER

  1. kernel/src/arch/x86_64/vtd/mod.rs:508 — unit.hand_over() runs before unit_agrees, the identity-domain build and bind_identity (513–535). On a unit firmware left translating, DMA therefore goes untranslated for the whole table build.
    • T14 kernel.log:116–121: unit0's TE goes off at 0.153 and back on at 0.155, around the 8956-leaf build.
    • QEMU judge-fix.log:590–594: 0.088 → 0.095.
    • The base dropped TE at its first GCMD write, after Queue::new and fault::arm. This branch therefore widens the DMA-isolation window it exists to handle.
    • The choice to switch TE off stands against the spec. §11.4.4.1 lets TE change with DMA in flight. §6.6 requires SRTP before re-enabling, and lets a root pointer move under TES=1 only onto tables that give identical remapping results, which only a copy of firmware's tables could guarantee (Linux kdump's copy_translation_tables, iommu.c:2014). Linux does the same outside kdump (iommu.c:2126–2128).
    • The fix: turn TE off after the tables are built, before Queue::new writes IQA, and outside the TABLES lock so no device wait runs under it.
    • Measure it with the hand-over probe's log, showing unit0's hand-over lines after identity domain … covers.
  2. tests/toyos.rs:809 — iommu-firmware-left joins the SELFTESTS arm, and nine other metal rows are judged on that boot (tests/toyos.rs:597–636, pci_capability_walk … operation_nesting).
    • The T14 readback was judged with the filter iommu_firmware_left alone (judge-t14.log: 1 registration(s), 1 passed). The image those nine rows read changed, and none of them was judged on it.
    • Judge them off the same readback (--metal --metal-readback …/vtdctx/metal <row>, which touches no machine), and put the exit and log in the body.

NOTE

  1. PR body, "The T14" and "Unsure" — the record still says the selftests image is "requested" and that the T14's gsts= is "unknown until the requested run reports".
    • The run is done. All four units show gsts=0x40000000, so on the T14 hand_over runs only under the actuator.
    • Silicon reports CFIS after CFI is written with IRE on and EIME off.
    • 00:0d.0 and 00:14.0 moved with the boot complete.
    • Put the command, exit 0 and these readings in the checks table: a claim stands on a measurement in the body, not in a comment.
  2. PR body "Unsure" item 1 — the remaining untranslated window on a unit firmware hands over translating is a compromise this branch chose, and it is recorded only in the PR body. File it in issues/kernel/ with its evidence and an exit something can read.
  3. kernel/src/arch/x86_64/vtd/mod.rs:173–180 — a unit plan refuses, or one past MAX_UNITS, is never handed over. Whatever firmware left on stays on, walking firmware's tables, while the kernel treats the unit as absent. Nothing records this; file it.
  4. issues/kernel/a-released-function-still-names-its-slots-domain.md:11 — "bind, is reached only from a claim" is false of the tree.
    • The kernel's own drivers reach it through DeviceSpace::attach: drivers/xhci/wait/boot.rs:339, hda.rs:384, virtio_console.rs:213, virtio_sound.rs:306, virtio_gpu.rs:541.
    • The point the line carries is that no release reaches bind; say that instead.
  5. tests/toyos.rs:2811 — the judge reads ir= off the unit line with a substring match. tests/common/iommu.rs:351 unit_fields already parses that line's key=value pairs; make it pub(crate) and use it instead of parsing the same line a second way.

REMOVE

None.

SEND BACK

Japabu and others added 2 commits October 4, 2026 00:16
Answers review round 1 of #700.

`hand_over` ran before the identity domain and the unit's context entries
were built, so a unit firmware left translating passed DMA untranslated for
the whole build: on the T14 under `iommu-firmware-left`, unit0's TE went off
at 0.153 s and came back at 0.155 s around the 8956-leaf build. It now runs
after the build and before `Queue::new` writes IQA (VT-d §6.5.2 points a
queue only with queued invalidation off), outside the TABLES lock. Measured
with the hand-over probe on QEMU 11.1.1: `identity domain ... covers` at
0.082 s, unit0's hand-over at 0.083 s, `translating` at 0.090 s; at
55a5ccb the hand-over was at 0.088 s and the build at 0.092 s.

What stays open is filed: the window that remains while a unit firmware left
translating is programmed, and a unit this kernel does not program keeping
whatever firmware left on.

The iommu_firmware_left judge reads `ir` through `unit_fields`, the parse
tests/common/iommu.rs already has for that line, and refuses a translating
unit no line describes. The released-function issue says what is true of
`bind`: no release reaches it.

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

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Probe behind round 2's BLOCKER 1 measurement: round 1's hand-over probe, unchanged, applied with git apply --check, run, and reverted in the same script (git status --porcelain --ignore-submodules=none showed only the round's own uncommitted edits after it). The tree was 9be0a8ac8's, before its two issue files were written.

--- a/tests/common/iommu.rs
+++ b/tests/common/iommu.rs
@@ -215,13 +215,14 @@ fn declining_is_not_free(test_config: &Path) -> Result<(), String> {
         &[],
         BootOptions {
             profile: Profile::Headless,
-            kernel_params: &["virtio-no-access-platform"],
+            kernel_params: &["virtio-no-access-platform", "iommu-firmware-left"],
             ..Default::default()
         },
     );
     let log = Serial::boot(&qemu);
     log.must_be_clean()?;
     log.must_say("Boot: complete")?;
+    crate::iommu_firmware_left(log.text())?;
 
     let refused: Vec<&str> = log
         .text()

cargo test --test toyos-build -- --nocapture iommu_virtio_platform, exit 0, probe-fix.log lines 589–594: the hand-over follows the build.

22:18:20 [serial 2] [kernel 0.078 cpu0] iommu: DMAR describes 1 units and 0 reserved regions, 0 of them inside the identity domain
22:18:20 [serial 2] [kernel 0.082 cpu0] iommu: identity domain aw=48 covers 0x0..0x180000000 in 3072 2 MiB leaves
22:18:20 [serial 2] [kernel 0.083 cpu0] iommu: unit0 was handed over with translation on; it goes off first
22:18:20 [serial 2] [kernel 0.086 cpu0] iommu: unit0 was handed over with interrupt remapping on; it goes off first
22:18:20 [serial 2] [kernel 0.087 cpu0] iommu: unit0 was handed over with queued invalidation on; it goes off first
22:18:20 [serial 2] [kernel 0.090 cpu0] iommu: unit0 @0xfed90000 translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n irt=0xc10000 irta=0xc10007 eime=n root=0xc0c000 domain=1 aw=48 streams=9

The same probe at 401d6f7d5 (round 1's judge-fix.log, lines 589–594; vtd/mod.rs there is 55a5ccb42's): the hand-over came before the build.

21:50:46 [serial 2] [kernel 0.082 cpu0] iommu: DMAR describes 1 units and 0 reserved regions, 0 of them inside the identity domain
21:50:46 [serial 2] [kernel 0.088 cpu0] iommu: unit0 was handed over with translation on; it goes off first
21:50:46 [serial 2] [kernel 0.091 cpu0] iommu: unit0 was handed over with interrupt remapping on; it goes off first
21:50:46 [serial 2] [kernel 0.091 cpu0] iommu: unit0 was handed over with queued invalidation on; it goes off first
21:50:46 [serial 2] [kernel 0.092 cpu0] iommu: identity domain aw=48 covers 0x0..0x180000000 in 3072 2 MiB leaves
21:50:46 [serial 2] [kernel 0.095 cpu0] iommu: unit0 @0xfed90000 translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n irt=0xc10000 irta=0xc10007 eime=n root=0xc0c000 domain=1 aw=48 streams=9

@Japabu

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Round 1's collapse mutation re-run at 9be0a8ac8, with the hand-over probe in the comment above (git apply --check of both, run, git apply -R of both in the same script; git status --porcelain --ignore-submodules=none empty after):

--- a/kernel/src/arch/x86_64/vtd/mod.rs
+++ b/kernel/src/arch/x86_64/vtd/mod.rs
@@ -359,7 +359,7 @@ impl Unit {
                     self.regs.read_u64(IQH_REG) == self.regs.read_u64(IQT_REG)
                 });
             }
-            self.command(bit, false, what);
+            self.regs.write_u32(GCMD_REG, 0);
         }
     }
 }

cargo test --test toyos-build -- --nocapture iommu_virtio_platform, exit 1, nc3.log:

22:28:16 FAIL iommu_virtio_platform: unit0 translates and never said it was handed over with queued invalidation on, so the state the actuator left was never switched off by the hand-over:
22:28:16 test result: FAILED. 0 passed, 1 failed, 0 invalidated, 1 total (15.5s; workers: 7s building, 8s testing)

@Japabu

Japabu commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

T14 run at 9be0a8ac8 (the selftests boot this round staged): RED — the kernel panics at 0.156 s on a DMA fault from the integrated GPU.

Image sha256 e5f875ce088ff0e7bfe0359468de790dc31a008b5849d8035a102e48b49eafb5 (checked in the command that flashed). Boot came back after 101 s; toyos-metal refused it: "the log partition came back and the log carries no Boot: complete (Nms) record". kernel.log is empty; the loader log's tail (the kernel's console lines it saved):

[0.156 cpu0] iommu: identity domain aw=48 covers 0x0..0x45f800000 in 8956 2 MiB leaves
[0.156 cpu0] iommu: unit0 was handed over with translation on; it goes off first
[0.156 cpu0] iommu: unit0 was handed over with compatibility-format pass-through on; it goes off first
[0.156 cpu0] iommu: unit0 was handed over with interrupt remapping on; it goes off first
[0.156 cpu0] iommu: unit0 was handed over with queued invalidation on; it goes off first
[0.156 cpu0] iommu: unit0 @0xfed90000 translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n irt=0xa1f000 irta=0xa1f807 eime=y root=0xa18000 domain=1 aw=48 streams=24
[0.156 cpu0] iommu: unit1 was handed over with translation on; it goes off first
[0.156 cpu0] iommu: unit1 was handed over with compatibility-format pass-through on; it goes off first
[0.156 cpu0] iommu: unit1 was handed over with interrupt remapping on; it goes off first
[0.156 cpu0] iommu: unit1 was handed over with queued invalidation on; it goes off first
[0.156 cpu0] iommu: unit1 @0xfed85000 translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n irt=0xa1f000 irta=0xa1f807 eime=y root=0xa24000 domain=1 aw=48 streams=24
[0.156 cpu0] iommu: unit0 fault recording overflowed ... earlier faults are lost
[0.156 cpu0] iommu: DMA FAULT owner=kernel unit0 stream=00:02.0 addr=0x000000009c855000 access=read reason=0x06 domain=unrecorded bme=cleared unitfaults=1 streamfaults=1 first=y read-permission
[0.156 cpu0] panic: rebooting in 60 s, timed by the calibrated clock

So with the hand-over moved after the table build (BLOCKER 1's fix), the first thing that happens once unit0 translates is a read fault from 00:02.0 (reason=0x06, read permission, domain=unrecorded). At 55a5ccb42 the same boot passed all ten rows. All ten rows red at this head (each judge: no boot to judge).

Japabu and others added 2 commits October 4, 2026 00:39
… identity domain

The T14's selftests boot at 9be0a8a panicked at 0.156 s on a read fault
from the integrated GPU (00:02.0, reason 0x06, 0x9c855000, inside rmrr0)
once unit0 translated; at 55a5ccb the same boot passed all ten rows.

`leave_on` left each unit translating through an empty root table: every
request from every function behind it faults, the GPU's scanout out of its
reserved region included, which no firmware leaves a unit doing (a reserved
region stays mapped while translation is on). At 55a5ccb that state
lasted from `leave_on` to `hand_over`, microseconds; with the hand-over
after the table build (review round 1, BLOCKER 1) it lasts the 8956-leaf
build, 0.153 s to 0.156 s on the T14, with the GPU faulting the whole time.
The unit's fault records overflowed in it, and the fault that killed the
boot came on the GPU's next scanout read once unit0 translated through
this kernel's tables.

`leave_on` now builds an identity domain of its own and gives every
enumerated function a context entry naming it, as this kernel leaves a unit,
so the state the actuator stages is one firmware may hand over and the
hand-over stays after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
@Japabu Japabu changed the title VT-d: a moved context entry is invalidated under the domain it held, in one 16-byte write, and GCMD follows §11.4.4.1 VT-d: a moved context entry is invalidated under the domain it held, and GCMD follows §11.4.4.1 Oct 3, 2026
@Japabu Japabu changed the title VT-d: a moved context entry is invalidated under the domain it held, and GCMD follows §11.4.4.1 VT-d: a moved context entry is invalidated under the domain it held, in one 16-byte write, and GCMD follows §11.4.4.1 Oct 3, 2026
@Japabu

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

T14 run at 25ab31bd5 (the selftests boot this round staged): GREEN — all ten rows pass.

Image sha256 1e29e3a2c203be936b2b9a764540aebd82d91239e8e712c69e6a23881843fc95 (checked in the command that flashed). Each row judged alone from the clean head with cargo test --test toyos-build -- --metal --metal-readback <dir> <row>; every one EXIT=0, 1 passed, 0 failed: input_merge, iommu_firmware_left, lapic_spurious_vector, leak_rollback_selftest, operation_nesting, pci_capability_walk, read_fault_selftests, sysret_ss_reload, xhci_descriptor_walk, xhci_xecp_walk. So the round-2 red at 9be0a8ac8 (5974187640) is gone with the hand-over still after the build, and the second cause the body names (the GPU's translation-disable quirk) did not show on this boot. IOMMU lines from the kernel log:

[2026-10-03 23:01:08 0.153 cpu0] iommu: unit0 @0xfed90000 seg=0 pci_all=n ver=4.0 cap=0x01c0000c40660462 ecap=0x0000029a00f0505e gsts=0x40000000 aw=48 sagaw=0x04 mgaw=39 nd=256 sps2m=y cm=n psi=n nfr=1 fro=0x400 qi=y ir=
[2026-10-03 23:01:08 0.153 cpu0] iommu: unit1 @0xfed85000 seg=0 pci_all=n ver=1.0 cap=0x00d2008c40660462 ecap=0x0000000000f050da gsts=0x40000000 aw=48 sagaw=0x04 mgaw=39 nd=256 sps2m=y cm=n psi=y nfr=1 fro=0x400 qi=y ir=
[2026-10-03 23:01:08 0.158 cpu0] iommu: unit0 was handed over with translation on; it goes off first
[2026-10-03 23:01:08 0.158 cpu0] iommu: unit0 was handed over with compatibility-format pass-through on; it goes off first
[2026-10-03 23:01:08 0.158 cpu0] iommu: unit0 was handed over with interrupt remapping on; it goes off first
[2026-10-03 23:01:08 0.158 cpu0] iommu: unit0 was handed over with queued invalidation on; it goes off first
[2026-10-03 23:01:08 0.158 cpu0] iommu: unit0 @0xfed90000 translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n irt=0xa37000 irta=0xa37807 eime=y root=0xa30000 domain=1 aw=48 streams=24
[2026-10-03 23:01:08 0.161 cpu0] iommu: unit1 was handed over with translation on; it goes off first
[2026-10-03 23:01:08 0.161 cpu0] iommu: unit1 was handed over with compatibility-format pass-through on; it goes off first
[2026-10-03 23:01:08 0.161 cpu0] iommu: unit1 was handed over with interrupt remapping on; it goes off first
[2026-10-03 23:01:08 0.161 cpu0] iommu: unit1 was handed over with queued invalidation on; it goes off first
[2026-10-03 23:01:08 0.161 cpu0] iommu: unit1 @0xfed85000 translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n irt=0xa37000 irta=0xa37807 eime=y root=0xa54000 domain=1 aw=48 streams=24

@Japabu

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2, head 25ab31bd5

Round 1's BLOCKERs:

  1. CLOSED: the hand-over now comes after the build. On QEMU, vtdctx-r3/probe-fix.log 588–592 (exit 0) logs identity domain … covers at 0.094, then unit0's hand-over at 0.094–0.098, then translating at 0.103. On the T14 at 25ab31bd5, vtdctx-r3/metal/selftests/kernel.log 116–136 logs the build at 0.158 before unit0's hand-over, and all four units are handed over and translating.
  2. CLOSED: all ten rows were judged off the 55a5ccb42 readback (vtdctx-r2/judge-t14-55a5ccb42.log) and off the 25ab31bd5 readback (vtdctx-r3/judge-t14-*.log). Each log reads 1 registration(s), 1 passed, 0 failed, and verdict.txt reads passed.

Round 1's NOTEs 4–7 are answered in 9be0a8ac8.

Net: 12 files, +376/−103. Production kernel is +247/−102, of which leave_on is 35 lines compiled only with boot-actuators. Tests and gate are +54/−1, issues +75. Since round 1, round 2 moves code without growing it, and round 3 adds +14/−3, all in leave_on. Merge with origin/main (42e5fca73) is clean. At the head, --ci host exits 0 (Host: 69 step(s), all green), the guest suite exits 0 (26/26), both kernel checks exit 0 and --build-only exits 0.

BLOCKER

  1. kernel/src/arch/x86_64/vtd/mod.rs:331-343: the round-3 root cause disagrees with the round-2 readback it rests on.
    • The fatal record cannot come from the empty root. It reads reason=0x06 (vtdctx-r2/metal/selftests/loader.log): read permission at a second-level entry, which needs a present root entry and a present context entry. Under leave_on's zeroed root a request faults 0x01, root-entry-not-present (fault.rs:422).
    • Both the record and the overflow came after TE was back on. At 9be0a8ac8, enable runs hand_over (TE off) and then fault::arm. fault::arm clears every record and FSTS, PFO included (fault.rs:180-185), before TE comes back on. So everything the 3 ms empty-root window recorded was wiped, and the panic's overflow and its 0x06 were both raised after unit0 was translating through this kernel's identity domain.
    • That domain grants the read. Its 2 MiB leaf over 0x9c800000 is SL_READ | SL_WRITE (table.rs:35, 198-209), and the log says rmrr0 … inside the identity domain.
    • So commit 25ab31bd5's "the unit's fault records overflowed in it" is false of the code. Neither of the body's two causes ("Unsure") explains a read-permission fault under tables that grant read.
    • Why it blocks. The green at 25ab31bd5 shows the actuator change makes the red go away. It does not show why this kernel's own tables faulted the GPU after a hand-over. On a security boundary, that is a defect that would land unseen. One example: a read the unit holds across TE off and on would fault the same way after any hand-over that follows a faulting period.
    • Measure it (one T14 boot of the selftests image at 9be0a8ac8). Add a probe that logs FSTS and the one record (nfr=1: source, address, reason) in fault::arm before it clears them, and logs FSTS again right after TE is confirmed.
    • Record the result. Put the command, the exit and the lines in the body. Then either explain the 0x06 from them, or file it in issues/kernel/ with this readback and the probe as evidence and an exit something can read.
  2. PR body, checks table ("staging the selftests boot … exit 2, staged, nothing judged"), the line under the 55a5ccb42 table, and "Unsure" item 1: the hardware reading at the head is not in the body.
    • The orchestrator's 25ab31bd5 run is done: verdict.txt reads passed, ten judges exit 0, and kernel.log 116–136 shows the four hand-overs. It appears only in a comment.
    • The body still says the rows "are judged on its readback" and that "the staged run decides between them".
    • Put the run's command, exit and readings in the checks table, and state what the run decided.
  3. kernel/src/arch/x86_64/vtd/mod.rs:336-341: leave_on repeats enable's identity-domain build and its per-device bind_identity loop (mod.rs:528-546), a second copy of a path the tree already has.
    • It also builds a fresh 8956-leaf domain for every unit. On the T14 the build window grows from 0.153→0.156 to 0.153→0.158 (vtdctx-r3/metal/selftests/kernel.log 115–116).
    • Fix: look up domains[domain_slot(width)] before the actuator runs, hand that domain to leave_on, and have both callers use one function that allocates a root and binds every enumerated function to the domain.

NOTE

None.

REMOVE

None.

SEND BACK

… and one root builder

Review round 2, BLOCKER 3: `leave_on` repeated `enable`'s identity-domain
build and its per-function `bind_identity` loop, and built a fresh
8956-leaf domain for every unit, widening the T14's build window from
0.153-0.156 s to 0.153-0.158 s.

`enable` now looks up (or builds) the per-width identity domain before the
actuator runs and hands it to `leave_on`; both take their root table from
one function, `identity_root`, which allocates a root and gives every
enumerated function a context entry in that domain.

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

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Round 4: the FSTS probe for review round 2's BLOCKER 1, probe-fsts-9be0a8ac8.patch (sha256 e795b15304ea6581311cf4c7322e33e909cfdcb466db0ea301371c97bc58f1b7), against 9be0a8ac8. Applied with git apply --check on a detached checkout of 9be0a8ac8, cd kernel && cargo check --features boot-actuators,test-actuators exit 0, image staged with cargo test --test toyos-build -- --metal --metal-readback orch/vtdctx-r4/metal-probe iommu_firmware_left (exit 2, staged; image sha256 b133204ec52429afdbbd6916056809a731c8499b5d2ac3c5647cf681bc8cd02b, iommu-probe: unit found in it), reverted with git apply -R, worktree back at the head and clean. Not run on the T14 yet: listed in orch/vtdctx-r4/request.txt.

diff --git a/kernel/src/arch/x86_64/vtd/fault.rs b/kernel/src/arch/x86_64/vtd/fault.rs
index 3de98c0f3..4f4425fb7 100644
--- a/kernel/src/arch/x86_64/vtd/fault.rs
+++ b/kernel/src/arch/x86_64/vtd/fault.rs
@@ -176,6 +176,7 @@ impl Records {
 /// translation is enabled.
 pub fn arm(index: usize, regs: Mmio, found: Records, vector: u8) {
     let count = found.count;
+    probe(index, regs, found, "fault::arm, before it clears");
 
     // Clear records firmware may have left behind before unmasking.
     for i in 0..count {
@@ -316,6 +317,33 @@ fn drain(index: usize, regs: Mmio, records: u64, count: u32) -> usize {
     seen
 }
 
+/// PROBE (review round 2, BLOCKER 1): FSTS, FECTL, GSTS and every fault
+/// record, raw and decoded, without clearing anything.
+pub fn probe(index: usize, regs: Mmio, found: Records, when: &str) {
+    log!(
+        "iommu-probe: unit{index} {when}: fsts={:#010x} fectl={:#010x} gsts={:#010x} nfr={}",
+        regs.read_u32(FSTS_REG),
+        regs.read_u32(FECTL_REG),
+        regs.read_u32(super::GSTS_REG),
+        found.count
+    );
+    for i in 0..found.count {
+        let record = found.offset + i as u64 * 16;
+        let low = regs.read_u64(record);
+        let high = regs.read_u64(record + 8);
+        log!(
+            "iommu-probe: unit{index} {when}: record{i} lo={low:#018x} hi={high:#018x} f={} \
+             source={} addr={:#018x} access={} reason={:#04x} {}",
+            yn(high & (1u64 << 63) != 0),
+            StreamId::pci((high >> 8) as u8, ((high >> 3) & 0x1F) as u8, (high & 0x7) as u8),
+            low & !0xFFF,
+            if high & (1u64 << 62) != 0 { "read" } else { "write" },
+            ((high >> 32) & 0xFF) as u8,
+            reason_name(((high >> 32) & 0xFF) as u8),
+        );
+    }
+}
+
 fn clear_record(regs: Mmio, record: u64) {
     regs.write_u32(record + 12, RECORD_FAULT);
 }
diff --git a/kernel/src/arch/x86_64/vtd/mod.rs b/kernel/src/arch/x86_64/vtd/mod.rs
index a57385dba..f698b7d2f 100644
--- a/kernel/src/arch/x86_64/vtd/mod.rs
+++ b/kernel/src/arch/x86_64/vtd/mod.rs
@@ -559,6 +559,7 @@ fn enable(
     queue.invalidate_all(unit.regs);
 
     unit.command(TRANSLATION_ENABLE, true, "translation");
+    fault::probe(index, unit.regs, records, "TE confirmed");
     if remap.is_some() {
         unit.command(interrupt::INTERRUPT_REMAPPING_ENABLE, true, "interrupt remapping");
     }

@Japabu

Japabu commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

T14 runs for round 4 (5e8cbb06e).

1. Probe boot (9be0a8ac8 + probe-fsts-9be0a8ac8.patch, image sha256 b133204ec52429afdbbd6916056809a731c8499b5d2ac3c5647cf681bc8cd02b, checked in the command that flashed): panicked as 9be0a8ac8 did; toyos-metal refused (no Boot: complete). Probe lines (kernel.log/loader.log, first ten):

[0.156 cpu0] iommu-probe: unit0 fault::arm, before it clears: fsts=0x00000003 fectl=0xc0000000 gsts=0x41000000 nfr=1
[0.156 cpu0] iommu-probe: unit0 fault::arm, before it clears: record0 lo=0x000000009cb80000 hi=0xc000000100000010 f=y source=00:02.0 addr=0x000000009cb80000 access=read reason=0x01 root-entry-not-present
[0.156 cpu0] iommu-probe: unit0 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[0.156 cpu0] iommu-probe: unit0 TE confirmed: record0 lo=0x000000009cb80000 hi=0x4000000100000010 f=n source=00:02.0 addr=0x000000009cb80000 access=read reason=0x01 root-entry-not-present
[0.156 cpu0] iommu-probe: unit1 fault::arm, before it clears: fsts=0x00000000 fectl=0x80000000 gsts=0x41000000 nfr=1
[0.156 cpu0] iommu-probe: unit1 fault::arm, before it clears: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[0.156 cpu0] iommu-probe: unit1 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[0.156 cpu0] iommu-probe: unit1 TE confirmed: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[0.156 cpu0] iommu-probe: unit2 fault::arm, before it clears: fsts=0x00000000 fectl=0x80000000 gsts=0x41000000 nfr=1
[0.156 cpu0] iommu-probe: unit2 fault::arm, before it clears: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed

Then, from the loader log's saved panic:

Previous boot's panic: 14048 bytes off 0x8000000
[0.156 cpu0] iommu: unit0 @0xfed90000 translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n irt=0xa1f000 irta=0xa1f807 eime=y root=0xa18000 domain=1 aw=48 streams=24
[0.156 cpu0] iommu: unit1 @0xfed85000 translating gsts=0xc7000000 tes=y qies=y ires=y cfis=n irt=0xa1f000 irta=0xa1f807 eime=y root=0xa24000 domain=1 aw=48 streams=24
[0.156 cpu0] iommu: unit0 fault recording overflowed ... earlier faults are lost
[0.156 cpu0] iommu: DMA FAULT owner=kernel unit0 stream=00:02.0 addr=0x000000009cc39000 access=read reason=0x06 domain=unrecorded bme=cleared unitfaults=1 streamfaults=1 first=y read-permission
[0.156 cpu0] panic: rebooting in 60 s, timed by the calibrated clock

So: before fault::arm clears, unit0 holds one record from 00:02.0 with reason 0x01 (root entry not present) and FSTS 0x3 (overflow + pending); after TE is confirmed FSTS is 0 and the record is cleared. The overflow and the fatal reason=0x06 read-permission fault (addr=0x9cc39000) are raised after unit0 and unit1 report translating.

2. Head boot (5e8cbb06e, image sha256 598ed3eee41e6744f24b8c8041f68570790a73e46c377c5c0229deb649a4b015): passed; each of the ten rows judged alone from the clean head, every one EXIT=0 1 passed, 0 failed: input_merge, iommu_firmware_left, lapic_spurious_vector, leak_rollback_selftest, operation_nesting, pci_capability_walk, read_fault_selftests, sysret_ss_reload, xhci_descriptor_walk, xhci_xecp_walk.

Readbacks: orch/vtdctx-r4/metal-probe/selftests/, orch/vtdctx-r4/metal-head/selftests/.

Japabu and others added 2 commits October 4, 2026 01:44
…fter the hand-over

Review round 2's BLOCKER 1 on pull request #700. The T14 probe boot at
9be0a8a (FSTS probe) shows unit0's one record, before fault::arm clears
it, is 00:02.0 reading 0x9cb80000 with reason 0x01 and FSTS=0x3: the empty
root's refusals, overflowed. Once TE is confirmed FSTS is 0 and the record
is clear, so the overflow and the fatal 0x06 at 0x9cc39000 that follow were
raised through this kernel's identity domain, which grants the read.

Against VT-d Rev. 4.1: 0x06 is LGN.3, a read meeting a second-stage entry
without R. Every entry on the walk grants it. The context entry is TT=00,
AW=2, DID=1 in every arm. A CM=0 unit caches no faulted translation
(§6.2.4.1), and the SRTP is followed by acknowledged global context-cache
and IOTLB invalidations (§6.6). Nothing the spec describes carries the
refusal across TE off and on. 55a5ccb (refused only within the hand-over's
millisecond, TE off for 2 ms) and 25ab31b/5e8cbb06e (no refusal) did not
fault.

Firmware leaves the T14's units off, and today's actuator refuses nothing,
so the state is no longer reached. It is filed with the reproduction as its
exit; two probe images at 9be0a8a (raw record, PCI STATUS, the walk as
DRAM holds it, and a 3 ms TE-off arm) are staged for the T14.

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

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Round 5: two probe images for the 0x06 (review round 2, BLOCKER 1), both against 9be0a8ac8. Each was applied with git apply --check on a detached checkout of 9be0a8ac8, checked with cd kernel && cargo check --features boot-actuators,test-actuators (exit 0), and staged with cargo test --test toyos-build -- --metal --metal-readback <dir> iommu_firmware_left (exit 2, staged, nothing judged). Each was then reverted with git apply -R, and the worktree returned to the branch, clean. Neither has run on the T14 yet.

walk (probe-walk-9be0a8ac8.patch, sha256 132701b021764aa3f587cee4291377cf3225e041c90f05bbffa00970752ecac0; image sha256 e997d5802985c67cf1580c4a02eebc640ebc94c9cf07650755aba20ea8784cbc). It contains round 4's FSTS probe, plus:

  • the fatal record raw, with AT, PP, EXE, PRIV, T2 and PV decoded;
  • 00:02.0's PCI COMMAND/STATUS (received master or target abort) before the actuator, before the hand-over, at TE and in the handler before BME is cleared;
  • the unit's walk for the fault from RTADDR down: root, context and each second-stage entry, read from DRAM after clflush, at TE and in the handler.

What it decides:

  • the walk grants read and AT=00, PP=0: the refusal is carried outside the tables;
  • an entry without R: the tables are what changed;
  • AT or PP set: the request was not a plain untranslated read.
diff --git a/kernel/src/arch/x86_64/vtd/fault.rs b/kernel/src/arch/x86_64/vtd/fault.rs
index 3de98c0f3..0f92e1294 100644
--- a/kernel/src/arch/x86_64/vtd/fault.rs
+++ b/kernel/src/arch/x86_64/vtd/fault.rs
@@ -176,6 +176,7 @@ impl Records {
 /// translation is enabled.
 pub fn arm(index: usize, regs: Mmio, found: Records, vector: u8) {
     let count = found.count;
+    probe(index, regs, found, "fault::arm, before it clears");
 
     // Clear records firmware may have left behind before unmasking.
     for i in 0..count {
@@ -279,6 +280,19 @@ fn drain(index: usize, regs: Mmio, records: u64, count: u32) -> usize {
         // First, before anything that can be slow or say no: a function that
         // cannot master the bus raises no second fault, which is the whole of
         // the ceiling on a storm.
+        log!(
+            "iommu-probe: unit{index} drain: fsts={status:#010x} lo={:#018x} hi={high:#018x} \
+             at={} pp={} exe={} priv={} t2={} pv={:#x}",
+            regs.read_u64(record),
+            (high >> 60) & 3,
+            (high >> 31) & 1,
+            (high >> 30) & 1,
+            (high >> 29) & 1,
+            (high >> 28) & 1,
+            (high >> 40) & 0xF_FFFF
+        );
+        probe_config(stream, "drain, before BME is cleared");
+        probe_walk(index, regs, stream, address);
         let stopped = stop(stream);
         let seen_here = note(stream);
         latch(index, stream, address, reason);
@@ -316,6 +330,99 @@ fn drain(index: usize, regs: Mmio, records: u64, count: u32) -> usize {
     seen
 }
 
+/// PROBE (review round 2, BLOCKER 1): FSTS, FECTL, GSTS and every fault
+/// record, raw and decoded, without clearing anything.
+pub fn probe(index: usize, regs: Mmio, found: Records, when: &str) {
+    log!(
+        "iommu-probe: unit{index} {when}: fsts={:#010x} fectl={:#010x} gsts={:#010x} nfr={}",
+        regs.read_u32(FSTS_REG),
+        regs.read_u32(FECTL_REG),
+        regs.read_u32(super::GSTS_REG),
+        found.count
+    );
+    for i in 0..found.count {
+        let record = found.offset + i as u64 * 16;
+        let low = regs.read_u64(record);
+        let high = regs.read_u64(record + 8);
+        log!(
+            "iommu-probe: unit{index} {when}: record{i} lo={low:#018x} hi={high:#018x} f={} \
+             source={} addr={:#018x} access={} reason={:#04x} {}",
+            yn(high & (1u64 << 63) != 0),
+            StreamId::pci((high >> 8) as u8, ((high >> 3) & 0x1F) as u8, (high & 0x7) as u8),
+            low & !0xFFF,
+            if high & (1u64 << 62) != 0 { "read" } else { "write" },
+            ((high >> 32) & 0xFF) as u8,
+            reason_name(((high >> 32) & 0xFF) as u8),
+        );
+    }
+}
+
+/// PROBE (round 5): one 8-byte word as DRAM holds it, the line written back
+/// and invalidated first, so the read is what a non-snooping walk sees.
+fn probe_phys(phys: u64) -> u64 {
+    let ptr = DirectMap::from_phys(phys).as_ptr::<u64>();
+    // SAFETY: probe only; `phys` is a remapping-table word the direct map covers.
+    unsafe {
+        core::arch::asm!("clflush [{a}]", "mfence", a = in(reg) ptr, options(nostack, preserves_flags));
+        core::ptr::read_volatile(ptr)
+    }
+}
+
+const PROBE_ADDR: u64 = 0x000F_FFFF_FFFF_F000;
+
+/// PROBE (round 5): the unit's walk for `stream` at `address`, from the root
+/// RTADDR names, every entry as DRAM holds it.
+pub fn probe_walk(index: usize, regs: Mmio, stream: StreamId, address: u64) {
+    let rta = regs.read_u64(super::RTADDR_REG);
+    let root = rta & PROBE_ADDR;
+    let rlo = probe_phys(root + stream.bus() as u64 * 16);
+    let rhi = probe_phys(root + stream.bus() as u64 * 16 + 8);
+    let ctx = rlo & PROBE_ADDR;
+    let clo = probe_phys(ctx + stream.devfn() as u64 * 16);
+    let chi = probe_phys(ctx + stream.devfn() as u64 * 16 + 8);
+    log!(
+        "iommu-probe: unit{index} walk {stream} @{address:#x}: rtaddr={rta:#x} root[{}]={rlo:#018x}:{rhi:#018x} \
+         ctx[{:#x}]={clo:#018x}:{chi:#018x} p={} tt={} aw={} did={}",
+        stream.bus(),
+        stream.devfn(),
+        clo & 1,
+        (clo >> 2) & 3,
+        chi & 7,
+        (chi >> 8) & 0xFFFF
+    );
+    if rlo & 1 == 0 || clo & 1 == 0 {
+        return;
+    }
+    let mut table = clo & PROBE_ADDR;
+    let mut level = (chi & 7) + 2;
+    while level >= 1 {
+        let slot = (address >> (12 + 9 * (level - 1))) & 0x1FF;
+        let entry = probe_phys(table + slot * 8);
+        log!("iommu-probe: unit{index} walk level{level} table={table:#x}[{slot:#x}]={entry:#018x}");
+        if entry & 3 == 0 || (level == 2 && entry & 0x80 != 0) || level == 1 {
+            return;
+        }
+        table = entry & PROBE_ADDR;
+        level -= 1;
+    }
+}
+
+/// PROBE (round 5): `stream`'s PCI COMMAND and STATUS, read without writing.
+pub fn probe_config(stream: StreamId, when: &str) {
+    let Some(slot) = find(u32::from(stream.requester())) else {
+        log!("iommu-probe: {stream} {when}: not enumerated");
+        return;
+    };
+    let word = window_of(slot.config.load(Ordering::Relaxed), CONFIG_WINDOW).read_u32(4);
+    log!(
+        "iommu-probe: {stream} {when}: command={:#06x} status={:#06x} rma={} rta={}",
+        word & 0xFFFF,
+        word >> 16,
+        (word >> 29) & 1,
+        (word >> 28) & 1
+    );
+}
+
 fn clear_record(regs: Mmio, record: u64) {
     regs.write_u32(record + 12, RECORD_FAULT);
 }
diff --git a/kernel/src/arch/x86_64/vtd/mod.rs b/kernel/src/arch/x86_64/vtd/mod.rs
index a57385dba..a29412e0a 100644
--- a/kernel/src/arch/x86_64/vtd/mod.rs
+++ b/kernel/src/arch/x86_64/vtd/mod.rs
@@ -503,6 +503,7 @@ fn enable(
     let Plan { width, records } = plan;
     #[cfg(feature = "boot-actuators")]
     if crate::actuator::iommu_firmware_left() {
+        fault::probe_config(StreamId::pci(0, 2, 0), "before the actuator");
         unit.leave_on();
     }
     // Before any table is built: what a domain of a driver's own can be is the
@@ -539,6 +540,7 @@ fn enable(
     // untranslated only while it is programmed
     // (`issues/kernel/a-unit-left-translating-passes-dma-untranslated-while-programmed.md`),
     // and before the queue is pointed, which §6.5.2 does only with it off.
+    fault::probe_config(StreamId::pci(0, 2, 0), "before the hand-over");
     unit.hand_over();
     let mut queue = Queue::new(&mut TABLES.lock(), unit.regs);
     // Outside the `TABLES` lock: `interrupt::arm` takes its own lock and
@@ -559,6 +561,11 @@ fn enable(
     queue.invalidate_all(unit.regs);
 
     unit.command(TRANSLATION_ENABLE, true, "translation");
+    fault::probe(index, unit.regs, records, "TE confirmed");
+    if index == 0 {
+        fault::probe_config(StreamId::pci(0, 2, 0), "TE confirmed");
+        fault::probe_walk(index, unit.regs, StreamId::pci(0, 2, 0), 0x9cc39000);
+    }
     if remap.is_some() {
         unit.command(interrupt::INTERRUPT_REMAPPING_ENABLE, true, "interrupt remapping");
     }

gap (probe-gap-9be0a8ac8.patch, sha256 6e24cf52a883385934bdcd8ab0eb62cfedb43fc82d3fadc32afd642c85442bd9; image sha256 3f555e92db15099514a535f9653058d9a4e7a8a0ab3a62561f278f1442c6a2fd). It is the walk patch plus 3 ms with TE off after each hand-over, about the gap 55a5ccb42 had (hand-over at 0.153 s, translating at 0.155 s). It differs from the walk patch only in this hunk:

@@ -539,7 +540,14 @@ fn enable(
     fault::probe_config(StreamId::pci(0, 2, 0), "before the hand-over");
     unit.hand_over();
+    // PROBE (round 5, gap arm): translation held off as long as 55a5ccb42's build held it.
+    let until = crate::clock::nanos_since_boot() + 3_000_000;
+    while crate::clock::nanos_since_boot() < until {
+        core::hint::spin_loop();
+    }
+    log!("iommu-probe: unit{index} held with translation off for 3 ms after the hand-over");

What it decides:

  • green with the walk red: what the refused reads left behind clears given time with TE off;
  • red as well: it does not.

@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

T14 runs for round 5 (6fdd9c579): walk RED (reproduces), gap GREEN, head GREEN.

Each image's sha256 checked in the command that flashed: walk e997d5802985c67cf1580c4a02eebc640ebc94c9cf07650755aba20ea8784cbc, gap 3f555e92db15099514a535f9653058d9a4e7a8a0ab3a62561f278f1442c6a2fd, head d2dfe4d3b632d1f8940606860e4d46f3cc57ca27e996e8379d01602170039552.

1. walk (9be0a8ac8 + probe-walk-9be0a8ac8.patch): panicked; toyos-metal refused. Lines (from the loader log's saved panic):

Previous boot's panic: 14521 bytes off 0x8000000
[0.153 cpu0] iommu-probe: 00:02.0 before the actuator: command=0x0006 status=0x0010 rma=0 rta=0
[0.155 cpu0] iommu-probe: 00:02.0 before the hand-over: command=0x0006 status=0x0010 rma=0 rta=0
[0.155 cpu0] iommu-probe: unit0 fault::arm, before it clears: fsts=0x00000003 fectl=0xc0000000 gsts=0x41000000 nfr=1
[0.155 cpu0] iommu-probe: unit0 fault::arm, before it clears: record0 lo=0x000000009ccc0000 hi=0xc000000100000010 f=y source=00:02.0 addr=0x000000009ccc0000 access=read reason=0x01 root-entry-not-present
[0.155 cpu0] iommu-probe: unit0 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[0.155 cpu0] iommu-probe: unit0 TE confirmed: record0 lo=0x000000009ccc0000 hi=0x4000000100000010 f=n source=00:02.0 addr=0x000000009ccc0000 access=read reason=0x01 root-entry-not-present
[0.155 cpu0] iommu-probe: 00:02.0 TE confirmed: command=0x0006 status=0x0010 rma=0 rta=0
[0.155 cpu0] iommu-probe: unit0 walk 00:02.0 @0x9cc39000: rtaddr=0xa18000 root[0]=0x0000000000a19001:0x0000000000000000 ctx[0x10]=0x0000000000a04001:0x0000000000000102 p=1 tt=0 aw=2 did=1
[0.155 cpu0] iommu-probe: unit0 walk level4 table=0xa04000[0x0]=0x0000000000a05003
[0.155 cpu0] iommu-probe: unit0 walk level3 table=0xa05000[0x2]=0x0000000000a08003
[0.155 cpu0] iommu-probe: unit0 walk level2 table=0xa08000[0xe6]=0x000000009cc00083
[0.155 cpu0] iommu-probe: 00:02.0 before the actuator: command=0x0006 status=0x0010 rma=0 rta=0
[0.155 cpu0] iommu-probe: 00:02.0 before the hand-over: command=0x0006 status=0x0010 rma=0 rta=0
[0.155 cpu0] iommu-probe: unit1 fault::arm, before it clears: fsts=0x00000000 fectl=0x80000000 gsts=0x41000000 nfr=1
[0.155 cpu0] iommu-probe: unit1 fault::arm, before it clears: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[0.155 cpu0] iommu-probe: unit1 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[0.155 cpu0] iommu-probe: unit1 TE confirmed: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[0.155 cpu0] iommu-probe: 00:02.0 before the actuator: command=0x0006 status=0x0010 rma=0 rta=0
[0.155 cpu0] iommu-probe: 00:02.0 before the hand-over: command=0x0006 status=0x0010 rma=0 rta=0
[0.156 cpu0] iommu-probe: unit2 fault::arm, before it clears: fsts=0x00000000 fectl=0x80000000 gsts=0x41000000 nfr=1
[0.156 cpu0] iommu-probe: unit2 fault::arm, before it clears: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[0.156 cpu0] iommu-probe: unit2 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[0.156 cpu0] iommu-probe: unit2 TE confirmed: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[0.156 cpu0] iommu-probe: 00:02.0 before the actuator: command=0x0006 status=0x0010 rma=0 rta=0
[0.156 cpu0] iommu-probe: 00:02.0 before the hand-over: command=0x0006 status=0x0010 rma=0 rta=0
[0.156 cpu0] iommu-probe: unit3 fault::arm, before it clears: fsts=0x00000000 fectl=0x80000000 gsts=0x41000000 nfr=1
[0.156 cpu0] iommu-probe: unit3 fault::arm, before it clears: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[0.156 cpu0] iommu-probe: unit3 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[0.156 cpu0] iommu-probe: unit3 TE confirmed: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[0.156 cpu0] iommu: unit0 fault recording overflowed ... earlier faults are lost
[0.156 cpu0] iommu-probe: unit0 drain: fsts=0x00000003 lo=0x000000009cdb7000 hi=0xc000000600000010 at=0 pp=0 exe=0 priv=0 t2=0 pv=0x0
[0.156 cpu0] iommu-probe: 00:02.0 drain, before BME is cleared: command=0x0006 status=0x0010 rma=0 rta=0
[0.156 cpu0] iommu-probe: unit0 walk 00:02.0 @0x9cdb7000: rtaddr=0xa18000 root[0]=0x0000000000a19001:0x0000000000000000 ctx[0x10]=0x0000000000a04001:0x0000000000000102 p=1 tt=0 aw=2 did=1
[0.156 cpu0] iommu-probe: unit0 walk level4 table=0xa04000[0x0]=0x0000000000a05003
[0.156 cpu0] iommu-probe: unit0 walk level3 table=0xa05000[0x2]=0x0000000000a08003
[0.156 cpu0] iommu-probe: unit0 walk level2 table=0xa08000[0xe6]=0x000000009cc00083
[0.156 cpu0] iommu: DMA FAULT owner=kernel unit0 stream=00:02.0 addr=0x000000009cdb7000 access=read reason=0x06 domain=unrecorded bme=cleared unitfaults=1 streamfaults=1 first=y read-permission
[0.156 cpu0] panic: rebooting in 60 s, timed by the calibrated clock

2. gap (walk + 3 ms with TE off after each hand-over): passed (verdict.txt passed). Probe lines from kernel.log:

[2026-10-04 00:06:46 0.153 cpu0] iommu-probe: 00:02.0 before the actuator: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.156 cpu0] iommu-probe: 00:02.0 before the hand-over: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 held with translation off for 3 ms after the hand-over
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 fault::arm, before it clears: fsts=0x00000003 fectl=0xc0000000 gsts=0x41000000 nfr=1
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 fault::arm, before it clears: record0 lo=0x000000009cc80000 hi=0xc000000100000010 f=y source=00:02.0 addr=0x000000009cc80000 access=read reason=0x01 root-entry-not-present
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 TE confirmed: record0 lo=0x000000009cc80000 hi=0x4000000100000010 f=n source=00:02.0 addr=0x000000009cc80000 access=read reason=0x01 root-entry-not-present
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: 00:02.0 TE confirmed: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 walk 00:02.0 @0x9cc39000: rtaddr=0xa18000 root[0]=0x0000000000a19001:0x0000000000000000 ctx[0x10]=0x0000000000a04001:0x0000000000000102 p=1 tt=0 aw=2 did=1
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 walk level4 table=0xa04000[0x0]=0x0000000000a05003
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 walk level3 table=0xa05000[0x2]=0x0000000000a08003
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: unit0 walk level2 table=0xa08000[0xe6]=0x000000009cc00083
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: 00:02.0 before the actuator: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.159 cpu0] iommu-probe: 00:02.0 before the hand-over: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.162 cpu0] iommu-probe: unit1 held with translation off for 3 ms after the hand-over
[2026-10-04 00:06:46 0.162 cpu0] iommu-probe: unit1 fault::arm, before it clears: fsts=0x00000000 fectl=0x80000000 gsts=0x41000000 nfr=1
[2026-10-04 00:06:46 0.162 cpu0] iommu-probe: unit1 fault::arm, before it clears: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[2026-10-04 00:06:46 0.162 cpu0] iommu-probe: unit1 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[2026-10-04 00:06:46 0.162 cpu0] iommu-probe: unit1 TE confirmed: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[2026-10-04 00:06:46 0.162 cpu0] iommu-probe: 00:02.0 before the actuator: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.162 cpu0] iommu-probe: 00:02.0 before the hand-over: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.165 cpu0] iommu-probe: unit2 held with translation off for 3 ms after the hand-over
[2026-10-04 00:06:46 0.165 cpu0] iommu-probe: unit2 fault::arm, before it clears: fsts=0x00000000 fectl=0x80000000 gsts=0x41000000 nfr=1
[2026-10-04 00:06:46 0.165 cpu0] iommu-probe: unit2 fault::arm, before it clears: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[2026-10-04 00:06:46 0.165 cpu0] iommu-probe: unit2 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[2026-10-04 00:06:46 0.165 cpu0] iommu-probe: unit2 TE confirmed: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[2026-10-04 00:06:46 0.165 cpu0] iommu-probe: 00:02.0 before the actuator: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.165 cpu0] iommu-probe: 00:02.0 before the hand-over: command=0x0006 status=0x0010 rma=0 rta=0
[2026-10-04 00:06:46 0.168 cpu0] iommu-probe: unit3 held with translation off for 3 ms after the hand-over
[2026-10-04 00:06:46 0.168 cpu0] iommu-probe: unit3 fault::arm, before it clears: fsts=0x00000000 fectl=0x80000000 gsts=0x41000000 nfr=1
[2026-10-04 00:06:46 0.168 cpu0] iommu-probe: unit3 fault::arm, before it clears: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed
[2026-10-04 00:06:46 0.168 cpu0] iommu-probe: unit3 TE confirmed: fsts=0x00000000 fectl=0x00000000 gsts=0xc5000000 nfr=1
[2026-10-04 00:06:46 0.168 cpu0] iommu-probe: unit3 TE confirmed: record0 lo=0x0000000000000000 hi=0x0000000000000000 f=n source=00:00.0 addr=0x0000000000000000 access=write reason=0x00 unnamed

3. head (6fdd9c579): passed; each of the ten rows judged alone from the clean head, every one EXIT=0 1 passed, 0 failed: input_merge, iommu_firmware_left, lapic_spurious_vector, leak_rollback_selftest, operation_nesting, pci_capability_walk, read_fault_selftests, sysret_ss_reload, xhci_descriptor_walk, xhci_xecp_walk.

Readbacks: orch/vtdctx-r5/metal-{walk,gap,head}/selftests/.

@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 3, head 6fdd9c579

Round 2's BLOCKERs:

  1. CLOSED, by filing. The 0x06 is filed as issues/kernel/a-function-refused-under-translation-faults-read-permission-after-the-hand-over.md, with the reproduction as its exit. Three T14 readings since round 2 settle what round 2 asked (comment 5974876749; orch/vtdctx-r5/probe-{walk,gap}-lines.txt):
    • Walk, RED, the third reproduction. At the fault, RTADDR 0xa18000 leads to ctx[0x10] p=1 tt=0 aw=2 did=1, then L4 0xa05003, then L3 0xa08003, then leaf 0x9cc00083, read from DRAM after clflush. That leaf is a present 2 MiB read/write entry over 0x9cdb7000. The raw record is hi=0xc000000600000010 with at=0 pp=0 exe=0 priv=0 t2=0, and the GPU's PCI status reads rma=0 rta=0 throughout. The unit therefore refused a read that the tables in memory grant. The fault is neither in this kernel's tables nor in any cached state the spec allows.
    • Gap, GREEN. It has the same empty-root refusals: fsts=0x00000003, record 0x01, before fault::arm. The only change is that TE is held off for 3 ms after hand_over, and no 0x06 follows.
    • Head, GREEN. All ten rows exit 0.
    • What the readings show. With the 55a5ccb42 arm (refusals, then TE off for about 2 ms, green), the fault needs both of these:
      • (a) a period in which the unit refuses the GPU's reads;
      • (b) TE off for only microseconds before it comes back on.
    • Main does (b) too, as the body says. I read origin/main's enable. Its first command is QIE with the shadow at 0, so that write clears a firmware-left TE. Then come SRTP, IRTA and the global invalidations, and then TE, all after the build and in the same span as this branch's hand_over. Both trees do (b). Neither reaches (a) on the T14, because firmware leaves gsts=0x40000000 on all four units and the head's actuator refuses nothing. So "outside this branch" holds, and nothing this branch ships makes it reachable.
    • What the gap reading means for the issue. It names the duration of TE-off as the lever, and the only fix that duration names is a timed hold. That is a flat wait no hardware document mandates. It also widens the untranslated window that issues/kernel/a-unit-left-translating-passes-dma-untranslated-while-programmed.md records. The gap is a reading, not a fix (see NOTE 1).
  2. CLOSED, then reopened at this head as BLOCKER 1 below. The 25ab31bd5 run is in the body's tables (command, exit, verdict.txt, ten judges, kernel.log lines). The head has since moved, and its own run is not in the body.
  3. CLOSED. In 5e8cbb06e, enable takes the per-width domain once through get_or_insert_with before the actuator and hands it to leave_on. Both callers take their root from one function, identity_root. The diff from 25ab31bd5 to 5e8cbb06e is +25/−37 in vtd/mod.rs, and the duplicated build and binding loop are gone.

Net lines, git diff --shortstat origin/main...6fdd9c579: 13 files, +443/−127.

  • Production kernel: about +259/−126 across vtd/ and actuator.rs. leave_on compiles only with boot-actuators.
  • Tests and gate: about +54/−1.
  • Issues: +130.
  • Since round 2's head: production −12 net. The one addition is the +55 issue.

At the head (orch/vtdctx-r5/, gates-head.txt = 6fdd9c579):

  • --ci host exits 0 (Host: 69 step(s), all green).
  • The guest suite exits 0 (26 passed, 26 total).
  • Both kernel checks exit 0.
  • --build-only exits 0.
  • It merges clean with origin/main 7a6b667ed.

BLOCKER

  1. PR body: the checks table at the head, "Conclusion" and "Unsure" item 1. The body still says the head's selftests boot was "staged, nothing judged", and that the walk and gap probes "are staged to split them". The issue's paragraph "Staged on pull request VT-d: a moved context entry is invalidated under the domain it held, in one 16-byte write, and GCMD follows §11.4.4.1 #700, not yet run" says the same. All three boots ran: walk refused, gap passed, head passed. Each sits in comment 5974876749 only, and no claim stands on a measurement outside the body.
    • Add the three runs to the body's table: the flash command, the image sha256, verdict.txt, and for the head the ten judge commands with their exit 0.
    • State what they decided: the walk grants read in DRAM, a 3 ms TE-off removes the fault, and the head is green.
    • Replace the issue's "not yet run" paragraph and its "Ruled out by reading" leaf claim with the walk's measured lines and the gap result. The issue is a record and has to stay true of what it describes.

NOTE

  1. The issue's Exit. "with the hand-over or programming changed to whatever the probe readings name" is now satisfied by a 3 ms hold. That is a flat wait (root CLAUDE.md, "Fail fast"), and it widens the untranslated-DMA window that a-unit-left-translating-passes-dma-untranslated-while-programmed.md files. Make the exit refuse a timed hold. The gap is one green boot against three reds, so state the count.
  2. The body's "By reading, not run … The defect predates this branch". It holds by reading main's enable (above). Keep it labelled as reading, or drop "predates": no boot of main in a refusing state was run.

REMOVE

  1. PR body, the iommu-firmware-left paragraph. Delete the parenthetical "(round 4, review round 2's BLOCKER 3: round 3 had repeated the build and the binding loop in leave_on, one fresh 8956-leaf domain per unit)". It is review history, and main's record does not need it.

SEND BACK

Japabu and others added 2 commits October 4, 2026 02:21
…and its exit refuses a timed hold

The walk probe read the unit's tables from DRAM at the fault: every entry
grants the read, the record is a plain untranslated read and the GPU saw
no abort. The gap probe, 3 ms with TE off after the hand-over, booted
green. That replaces the staged-not-run paragraph and the leaf claim made
by reading. A timed hold is a flat wait no document mandates and widens
the untranslated window already filed, so the exit asks for a hand-over
that waits on an event instead.

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

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 4, head 041014fdf

Round 3's BLOCKER:

  1. CLOSED. The body's tables now carry the walk, gap and head boots at 6fdd9c579: for each, the flash command, the image sha256 and verdict.txt, plus the ten judge commands for the head with exit 0. "What they decided" states the result. In the issue, the "Staged … not yet run" paragraph and the "Ruled out by reading" leaf claim are replaced by the walk's measured lines (root 0xa19001, context 0xa04001:0x102, leaf 0x9cc00083, raw hi=0xc000000600000010, STATUS 0x0010) and by the gap result. I checked them against orch/vtdctx-r2/metal/selftests/loader.log 172–185 and round 3's reading of orch/vtdctx-r5/.

Round 3's NOTEs and REMOVE:

  • NOTE 1 is done. The exit now requires a hand-over that waits on an event, refuses a timed hold by name, and states one green against three reds.
  • NOTE 2 is done. The body labels main's behaviour "By reading … not run", and "predates" is gone.
  • The REMOVE is done. The parenthetical is deleted.

The brief's question: do the image, guest-suite and kernel checks still stand on 6fdd9c579?

What holds:

What does not hold:

  • The body says the head leaves the kernel, tests and gate "as they were". It does not.
    • kernel/Cargo.toml changes all four of its path dependencies on toyos-dma, toyos-gicv3, toyos-pci and toyos-ps2.
    • kernel/pci/Cargo.toml changes its toyos-abi path.
    • src/clippy.rs (+/−80), src/ci.rs and src/userlandhost.rs change.
    • userland/soundserver/src/main.rs, userland/compositor and userland/netstack take new crate paths, and those go into the image the guest suite boots.
  • So the head's image, kernel build and guest image are not the ones measured. The only run at the head is --ci host. It exits 0 (orch/vtdctx-r6/ci-host.log line 7326, Host: 72 step(s), all green; ci-host.exit EXIT=0). It does not cover the image, the guest suite or cargo check --features boot-actuators.
  • Main's merge queue ran Every crate with one consumer moves under it, and the track closes #703's guest suite without this branch's kernel. Nothing has run the combination.
  • Every PR check at this head reads skipping (draft).

BLOCKER

  1. PR body, "At 6fdd9c579 … whose kernel, tests and gate the head leaves as they were".
    • What is wrong: this is false of the tree. The merge changes kernel/Cargo.toml, kernel/pci/Cargo.toml, src/clippy.rs, src/ci.rs, src/userlandhost.rs and the userland the image carries.
    • Why it blocks: no measurement at the head shows the image, the guest suite this change reaches, or the kernel checks green. A claim stands only on a measurement at the head, and re-running is the cheap measurement.
    • What to do: run cargo run -- --build-only, cargo test --test toyos-build, cd kernel && cargo check and cd kernel && cargo check --features boot-actuators at 041014fdf. Put each command, its exit code and its log in the head's table, and delete the "leaves as they were" sentence.
    • The T14 rows can stay on 6fdd9c579, labelled as that commit's.

NOTE

None.

REMOVE

  1. PR body, "## Growth", the sentence "Round 4 against 25ab31bd5: vtd/mod.rs +25/−37, the identity-domain lookup moved ahead of the actuator and the root build shared." It is review-round history of the same kind as round 3's REMOVE, and main's record does not need it.

Net lines, git diff --shortstat origin/main...041014fdf: 13 files, +457/−127.

  • Production kernel: +259/−126 (vtd/, actuator.rs). leave_on compiles only with boot-actuators.
  • Tests and gate: +54/−1.
  • Issues: +144.
  • Since 6fdd9c579: production 0. One issue file changes, +26/−12.

SEND BACK

@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 5, head 041014fdf

Round 4's BLOCKER:

  1. CLOSED. The four gates ran on a clean tree at 041014fdf (orch/vtdctx-r7/head.txt; run.sh runs each in turn and records its exit code):
    • cargo run -- --build-only: EXIT=0. The log rebuilds the moved crates toyos-pci, toyos-gicv3, toyos-ps2 and toyos-dma, then the kernel and the userland servers from their new paths, and ends Boot image: …/target/bootable.img (build-only.log 19–37, 107).
    • cargo test --test toyos-build: EXIT=0, test result: ok. 26 passed, 26 total. PASS iommu_virtio_platform (31s) is among them (toyos-build.log 330, 399).
    • cd kernel && cargo check and cd kernel && cargo check --features boot-actuators: both EXIT=0, and both logs show Checking kernel rather than a cache hit.
    • --ci host at this head: orch/vtdctx-r6/gates-head.txt is 041014fdf, and ci-host.exit is EXIT=0.
    • The head's table in the body carries each of these commands with its exit code and log. The "leaves as they were" sentence is gone. The 6fdd9c579 table is labelled "that commit's runs".

Round 4's REMOVE:

  • Done. The Growth sentence "Round 4 against 25ab31bd5 …" is deleted.

Other claims in the body that I checked:

  • "At 5e8cbb06e …, whose vtd/ the head leaves as it was": git diff 5e8cbb06e 041014fdf -- kernel/src/arch/x86_64/vtd is empty.
  • "At 401d6f7d5, whose table.rs, queue.rs … later rounds leave as they were": the diff of those two files from 401d6f7d5 to the head is empty.
  • The Growth figures match git diff --numstat origin/main...041014fdf:
    • Production kernel: +259/−126.
    • Tests and gate: +54/−1.
    • Issues: +144.

Since round 4 the code is unchanged; only the body was edited.

Net lines, git diff --shortstat origin/main...041014fdf: 13 files, +457/−127.

BLOCKER

None.

NOTE

None.

REMOVE

None.

LAND

@Japabu
Japabu marked this pull request as ready for review October 4, 2026 00:38
@Japabu
Japabu enabled auto-merge October 4, 2026 00:38
@Japabu
Japabu added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 27510ef Oct 4, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-vtdctx branch October 4, 2026 01:25
Japabu added a commit that referenced this pull request Oct 4, 2026
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