Two command encodings checked against the specification:
- COMPLETION_WAIT set bit 1 (I, interrupt) instead of bit 0 (S, store), so the
IOMMU was never asked to write the sentinel, the poll always exhausted its
spins, and completeAndWait returned without any guarantee the preceding
invalidation had executed — no invalidation barrier has ever existed, on QEMU
or on silicon. The in-code claim that "QEMU's amd-iommu does not implement
the store form" was a misdiagnosis of this bug: with S set, QEMU stores the
sentinel fine, and the amd-iommu cases now run without the warn line. On real
hardware, which fetches commands asynchronously, the missing barrier was an
IOTLB use-after-free window: unmap returned before the invalidation was
confirmed and the caller freed the frames.
- INVALIDATE_IOMMU_PAGES "invalidate everything" used address bits 51:12
all-ones; the architected encoding is bits 62:12 all-ones (the spec's literal
0x7FFF_FFFF_FFFF_F000). Real silicon is free to misread the non-architected
form as a bounded range.
VT-d and AMD-Vi are x86 hardware, but lived in the architecture-neutral
kernel tree and leaked further: the core's public Kind enum named both
vendors, and the ACPI parser read the VT-d version/capability registers
(raw volatile MMIO inside table discovery). Now the vendor backends
live in architecture/x86_64/ behind architecture.iommu — the core hands
over the discovery facts plus an injected environment (frame allocation
+ the log sink, the same pattern enablePaging uses) and receives the
hardware vtable back, so the backends never import kernel internals and
an ARM port supplies its SMMU with no core change. Discovery keeps
table facts only; the live-unit register check moved into VT-d detect
(version reading zero now stays fail-open). The unused kindOf() is
gone. Log shapes the harness pins (iommu online, DANOS-IOMMU-FAULT)
are unchanged; all five IOMMU QEMU cases pass.