Skip to content

machine-arm: pcie-root-port under the SMMUv3 with per-bus stream IDs - #336

Merged
tamnd merged 1 commit into
mainfrom
smmu-root-port
Oct 14, 2026
Merged

tamnd merged 1 commit into
mainfrom
smmu-root-port

Conversation

@tamnd

@tamnd tamnd commented Oct 14, 2026

Copy link
Copy Markdown
Owner

What

-device pcie-root-port on Arm virt, with PCI functions behind it under the SMMUv3, as QEMU 11.1 does it.

  • The SMMU now covers the functions behind root ports. Each function's IOMMU region works out its stream ID at each translation from the number its bus has then, as smmu_get_sid() does with PCI_BUILD_BDF(pci_bus_num(bus), devfn). Before this the stream ID was fixed when the function was plugged. SmmuV3::device_ops_with takes the stream ID as a closure; device_ops(sid) still works and calls it.
  • The root port itself gets an IOMMU region too, as pci_init_bus_master() gives it one in QEMU.
  • The IORT root complex mapping covers the root bus and the buses behind the bridges (iort_host_bridges() with pci_bus_range()).
  • As in virt_acpi_build_update(), the ACPI tables, loader and RSDP are built again on the first fw_cfg read after each reset, so the IORT has the bus numbers the firmware gave the root ports. A reset clears the flag, as virt_acpi_build_reset() does.
  • The DSDT nests the functions behind a cold plugged root port in the port's device (build_pci_bridge_aml()).
  • -device pcie-root-port takes id, bus=pcie.0, addr, chassis, slot, port, hotplug and multifunction. PCI types take bus=<root port id>. The other QEMU properties of the type fail with "is not supported by ruvm yet".

The shared notifier API is not touched. The only change in hw-iommu is device_ops_with in smmuv3.rs.

Comparison with QEMU 11.1

Alpine linux-virt 6.12.110 with -M virt,iommu=smmuv3,gic-version=3 -cpu max -m 1G -smp 1 -device pcie-root-port,id=rp1,chassis=1 -device virtio-blk-pci,bus=rp1,drive=d0,disable-legacy=on,iommu_platform=on, booted both with -kernel Image (device tree) and with edk2 and -kernel vmlinuz-virt (ACPI). The initramfs prints the SMMU and IOMMU dmesg lines, the PCI functions with their IOMMU groups, the IORT, the md5 of the 16 MiB disk, then writes a sector, drops the caches, reads it back, and prints the md5 again.

Results, ruvm against QEMU 11.1:

  • SMMU probe lines match in both boots: ias 44-bit, oas 44-bit (features 0x00088705), 262144 cmdq entries, 131072 evtq entries.
  • The IORT read from /sys/firmware/acpi/tables/IORT is the same 0xec bytes on both, with the root complex mapping IDs 0 to 0x1ff (buses 0 and 1) to the SMMU.
  • IOMMU groups: with the device tree the root port and the disk behind it share group 0 on both. With ACPI the disk gets a group of its own on both.
  • The disk reads md5 3cf64d06... before, the sector written through the SMMU reads back, and the md5 after is b6fb6854... on both, in the guest and on the host image.
  • The one difference is that QEMU adds its default NIC at 00:01.0, so its root port is 00:02.0 and its group numbers count one more. ruvm has no default NIC on virt.

A first ruvm run with -smp 2 stalled in multi_cpu_stop with soft lockups after the SMMU probe, on a server with a load of about 25 on 6 cores. With -smp 1 the same point took about 45 s and then went on. I did not check whether main does the same.

Differences from QEMU, not done here

  • pcie-root-port without an id fails. QEMU names its bus pcie.<n> with an n that depends on the other buses, so nothing can be plugged behind it by name anyway.
  • A root port behind a root port, and the other root port properties (the reserve sizes, x-speed, acpi-index and so on), fail with "not supported by ruvm yet".
  • Root ports together with virtio-iommu fail in either order, since the virtio-iommu endpoints are only set up for the root bus. That is a follow-up.
  • pci-bridge and pcie-pci-bridge are not ported, so the requester ID aliasing of pci_device_get_iommu_bus_devfn() is not needed yet.
  • sbsa-ref does not take pcie-root-port yet.
  • Accelerated nested mode (accel=on, CMDQV) needs host SMMU hardware we do not have, so it comes last. Notifiers and replay will build on the IOMMU notifier API from hw-vfio, intel-iommu: DMA through the vIOMMU for vfio-pci #330.

Tests

  • smmu_stream_id_behind_a_root_port: with the stream table entry of stream 0 bypassing and the entry of 0x100 aborting, the MSI of a function plugged while its bus was 0 is dropped once the guest numbers the bus 1, and gets through with the SMMU off.
  • iort_behind_a_root_port: the IORT maps 0x200 IDs to the SMMU with the bus numbered 1. Read through fw_cfg it keeps the numbers of its first read until the next reset, then has 0x400 for buses 1 to 3. The DSDT has S00 inside S10.
  • root_port_and_virtio_iommu, and planner tests for the properties and errors.
  • On this commit rebased onto 31dd93b, on server2: cargo test -p ruvm-hw-iommu -p ruvm-machine-arm -p ruvm-system -p ruvm-hw-pci -p ruvm-firmware -p ruvm-mem, cargo test -p ruvm-system --lib, the linux-user x86 suite, clippy -D warnings, cargo check --workspace --all-targets, the xtasks and the Windows check all pass.

@tamnd tamnd added kind/feature It should do something it does not. area/machine Machine types, boards, ACPI, device trees, firmware. area/devices Device models that are not virtio. labels Oct 14, 2026
@tamnd
tamnd merged commit d8ef5ac into main Oct 14, 2026
@tamnd
tamnd deleted the smmu-root-port branch October 14, 2026 14:24
tamnd added a commit that referenced this pull request Oct 14, 2026
Part of #9.

vfio-pci now maps the BARs of the device into the guest, like
`vfio_region_mmap()` in QEMU. Reads and writes to a mapped BAR go to the
device memory without a pread or pwrite per access. While INTx is
pending the mmaps are off, so the guest's next BAR access traps and
serves as the EOI, and a timer on the virtual clock turns them back on
once INTx is quiet, as `vfio_intx_mmap_enable()` does.

What is in it:

- ruvm-mem: a RAM device region type for host device memory, made with
`memory_region_init_ram_device_ptr()`, that the vfio listener maps for
DMA like RAM.
- ruvm-sys: the region info capabilities (sparse mmap areas and
`MSIX_MAPPABLE`), `VFIO_DEVICE_FEATURE_DMA_BUF`, and
`HostMemory::map_device()`, which reserves the whole region and maps
each area into it.
- vfio-pci: the sparse mmap areas as `vfio_setup_region_sparse_mmaps()`
reads them, with size 0 areas dropped, the areas sorted, and QEMU's
error for overlapping areas. Each area becomes a "NAME BAR n mmaps[i]"
subregion of the trapped BAR, then the dma-buf probe runs with QEMU's
two warnings. A failed mmap prints "Failed to mmap NAME BAR n.
Performance may be slow" and the BAR stays trapped. Without
`MSIX_MAPPABLE` the pages of the MSI-X table are cut out of the mmaps,
as `vfio_pci_fixup_msix_region()` does. INTx turns the mmaps off and
arms the timer, the timer rearms while INTx is pending, and turning INTx
off deletes the timer and turns the mmaps on. The container maps the RAM
device sections for DMA, and a failure there is a warning with QEMU's
peer to peer hint instead of an error.
- system: `x-no-mmap` and `x-intx-mmap-timeout-ms` now do what they do
in QEMU, and the virtual clock is passed to realize for the timer.

Where it differs from QEMU:

- Under TCG, ruvm reads and writes a RAM device region through its IO
callbacks, not through the TLB. There are no KVM, HVF or WHPX slots for
it yet.
- `vfio_sub_page_bar_update_mapping()` is not ported, so a BAR smaller
than a host page stays trapped.
- The page sizes for the DMA map check come from the host page size, as
`VFIO_IOMMU_GET_INFO` is not ported.
- `x-msix-relocation` is not ported.

Checked:

On server2, on 9596e67, the branch with main up to #336 merged in:

- clippy with `-D warnings` and `--all-targets` for ruvm-mem,
ruvm-hw-iommu, ruvm-sys, ruvm-hw-pci, ruvm-hw-vfio, ruvm-machine-x86,
ruvm-system and ruvm-machine-arm.
- clippy for x86_64-pc-windows-gnu and aarch64-apple-darwin on the same
crates.
- tests of ruvm-mem, ruvm-hw-iommu, ruvm-sys, ruvm-hw-pci and
ruvm-hw-vfio, and the ruvm-machine-x86, ruvm-system and ruvm-machine-arm
libs. New tests cover the sparse mmap areas, the MSI-X table cut and the
INTx timer on a manual clock.
- xtask unsafe-audit, provenance, layers and style.

The same checks without ruvm-machine-arm, the release build and the L1
compare below ran on 7b2159e, the commit before that merge. The merge
brings in #335 and #336, which touch only Arm files.

- An L1 compare on the 7b2159e release build. L1 is QEMU 11.1 with
`intel-iommu,caching-mode=on` and an e1000e at 00:04.0 bound to
vfio-pci. L2 is run with `-device vfio-pci,host=0000:00:04.0,addr=2`
under qtest, as QEMU 11.1 and ruvm, each with and without
`x-no-mmap=on`, and then with `x-intx-mmap-timeout-ms=0`. Each batch
waits for all of its answers, and the syscall counts are the syscr
deltas of the L2 process from /proc.

Part A, with `-S`, where the virtual clock does not move. qtest places
the BARs, reads and writes BAR0, BAR1, the IO BAR and the MSI-X BAR,
sends a frame by DMA, programs the IOAPIC and raises INTx.

| Step | QEMU 11.1 | ruvm | QEMU x-no-mmap | ruvm x-no-mmap |
| --- | --- | --- | --- | --- |
| mmaps of the device fd | BAR0 0x20000, BAR1 0x20000, BAR3 0x4000 |
same | none | none |
| 300 STATUS reads | 10 | 5 | 310 | 305 |
| 300 STATUS reads after INTx | 310 | 305 | 310 | 305 |

All 71 answers are the same in the four runs. Both print "VFIO dma-buf
not supported in kernel, using mmap fallback, P2P DMA will not work", as
the L1 kernel has no dma-buf for vfio. ruvm prints it without QEMU's
`-device vfio-pci,host=0000:00:04.0,addr=2:` prefix. The MSI-X BAR is
mapped whole in both, since the kernel reports `MSIX_MAPPABLE`.

Part B, in a guest that runs, so the timer can fire. SeaBIOS places BAR0
at 0xfeba0000 in all four runs. qtest raises INTx, waits 3 seconds, does
one STATUS read, then reads ICR, masks the interrupt and does 300 reads,
waits 3 seconds and does 300 reads more.

| Step | QEMU 11.1 | ruvm | QEMU timeout 0 | ruvm timeout 0 |
| --- | --- | --- | --- | --- |
| 300 reads before INTx | 16 | 6 | 16 | 6 |
| one read 3 s after INTx | 5, trapped | 3, trapped | 4, trapped | 3,
trapped |
| ICR and 300 reads | 319 | 307 | 318 | 307 |
| 300 reads 3 s later | 14 | 5 | 314 | 305 |

The INTx step itself is two writes at 4, 2, 3 and 2, so the one read
adds a pread in every run. ICR reads 0x80000001 in all four. The mmaps
are off while INTx is pending, back on after the timeout, and stay off
with a timeout of 0, in QEMU and in ruvm. How many of the 300 reads
right after the EOI trap depends on when the next timer tick lands, and
was between 74 and 319 over the debug runs for both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/devices Device models that are not virtio. area/machine Machine types, boards, ACPI, device trees, firmware. kind/feature It should do something it does not.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant