Skip to content

fix(x86_64): find the IRQs of virtio-mmio devices on QEMU microvm with ACPI - #2777

Merged
stlankes merged 2 commits into
hermit-os:mainfrom
martijnhoekstra:fix/microvm-mmio-irq
Nov 7, 2026
Merged

stlankes merged 2 commits into
hermit-os:mainfrom
martijnhoekstra:fix/microvm-mmio-irq

Conversation

@martijnhoekstra

@martijnhoekstra martijnhoekstra commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2776. Hermit's CI boots microvm with acpi=off, where this already works. The issue also describes taking only the first commit.

Without virtio_mmio.device= arguments, Hermit probes QEMU microvm's virtio-mmio transports and assumes transport n uses IRQ 5 + n. QEMU only numbers them that way with acpi=off. With ACPI and ioapic2=off, transport n uses GSI 16 + n. With the second IOAPIC, the default, there are 24 transports on GSIs 24 to 47. With ACPI on, QEMU puts nothing on the command line. Only the DSDT says which GSI each transport uses: each one is an LNRO0005 device with a memory range and an interrupt.

With -machine microvm,ioapic2=off, the NIC is transport 7 at 0xfeb00e00 and raises GSI 23, but Hermit installs its handler on IRQ 12. The interrupt arrives at vector 55. Hermit only sends IRQs 0 to 15 to handle_interrupt, so vector 55 goes to the exception handler, and the core stops before the application starts:

[INFO  virtio    ] Virtio interrupt handler at line 12
[ERROR interrupts] Exception 55

Two commits:

  1. IRQs 0 to 79 (vectors 32 to 111) now go to handle_interrupt. The vectors from 112 up stay with the local APIC. handle_interrupt already acknowledges and counts an IRQ that has no handler.
  2. Without command-line devices, Hermit loads the DSDT and SSDTs with the acpi crate's AML interpreter, collects the LNRO0005 devices, and takes the address and interrupt from their _CRS. It only probes the fixed layout if there are no ACPI tables, as with acpi=off. On machines whose ACPI tables list no virtio-mmio devices, it no longer reads the microvm addresses. This links the AML interpreter into x86_64 builds with acpi and without pci. The echo application below grew by 227 KB.

Tested on main with hermit-loader-x86_64-multiboot v0.5.7 and a TCP and UDP echo application built without pci, in QEMU 7.2:

  • microvm with default options, under KVM and TCG: Hermit finds the NIC at 0xfeb02e00 on GSI 47, and the echo test passes.
  • microvm,ioapic2=off (KVM): Hermit installs the handler on IRQ 23, and the echo test passes.
  • microvm,acpi=off (KVM): probing still finds the NIC on IRQ 12, and the echo test passes.
  • q35 with virtio-net-pci (KVM): the echo test passes.
  • q35 (KVM) with the build without pci: Hermit boots and finds no network device, as on main, but no longer probes the 8 microvm addresses.
  • cargo test --lib, the integration tests and clippy (x86_64, aarch64, riscv64, and x86_64 without pci, with and without acpi) pass.

The DSDT describes the virtio-mmio interrupts as level-triggered and active high, but Hermit still programs every IOAPIC line edge-triggered, as before. That works on QEMU's IOAPIC under both KVM and TCG. I haven't changed the trigger mode.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark Results

Details
Benchmark Current: ec8716a Previous: 2e23902 Performance Ratio
startup_benchmark Build Time 90.85 s 80.34 s 1.13 ❗
startup_benchmark File Size 0.80 MB 0.80 MB 1.00 ❗
Startup Time - 1 core 0.75 s (±0.01 s) 0.75 s (±0.02 s) 1.01
Startup Time - 2 cores 0.75 s (±0.02 s) 0.74 s (±0.02 s) 1.01
Startup Time - 4 cores 0.75 s (±0.02 s) 0.74 s (±0.02 s) 1.01
multithreaded_benchmark Build Time 90.91 s 82.11 s 1.11 ❗
multithreaded_benchmark File Size 0.89 MB 0.86 MB 1.04 ❗
Multithreaded Pi Efficiency - 2 Threads 89.60 % (±8.21 %) 85.89 % (±6.61 %) 1.04
Multithreaded Pi Efficiency - 4 Threads 45.17 % (±2.66 %) 43.43 % (±2.56 %) 1.04
Multithreaded Pi Efficiency - 8 Threads 26.24 % (±1.69 %) 25.76 % (±1.53 %) 1.02
micro_benchmarks Build Time 89.62 s 80.40 s 1.11 ❗
micro_benchmarks File Size 0.89 MB 0.86 MB 1.04 ❗
Scheduling time - 1 thread 68.03 ticks (±2.18 ticks) 62.65 ticks (±4.06 ticks) 1.09 ❗
Scheduling time - 2 threads 37.60 ticks (±4.22 ticks) 34.08 ticks (±4.10 ticks) 1.10
Micro - Time for syscall (getpid) 3.56 ticks (±0.61 ticks) 3.45 ticks (±0.58 ticks) 1.03
Memcpy speed - (built_in) block size 4096 80102.15 MByte/s (±55565.32 MByte/s) 82448.38 MByte/s (±56997.13 MByte/s) 0.97
Memcpy speed - (built_in) block size 1048576 30119.46 MByte/s (±24412.89 MByte/s) 30585.98 MByte/s (±24707.84 MByte/s) 0.98
Memcpy speed - (built_in) block size 16777216 25904.73 MByte/s (±21446.54 MByte/s) 26340.06 MByte/s (±21720.96 MByte/s) 0.98
Memset speed - (built_in) block size 4096 80660.25 MByte/s (±55918.38 MByte/s) 82292.76 MByte/s (±56891.50 MByte/s) 0.98
Memset speed - (built_in) block size 1048576 30798.10 MByte/s (±24811.49 MByte/s) 31323.85 MByte/s (±25145.86 MByte/s) 0.98
Memset speed - (built_in) block size 16777216 26591.01 MByte/s (±21853.16 MByte/s) 27104.68 MByte/s (±22209.94 MByte/s) 0.98
Memcpy speed - (rust) block size 4096 72867.95 MByte/s (±51085.29 MByte/s) 74097.96 MByte/s (±51811.44 MByte/s) 0.98
Memcpy speed - (rust) block size 1048576 29789.86 MByte/s (±24275.06 MByte/s) 30361.60 MByte/s (±24602.37 MByte/s) 0.98
Memcpy speed - (rust) block size 16777216 25480.44 MByte/s (±21197.49 MByte/s) 27625.34 MByte/s (±22806.88 MByte/s) 0.92
Memset speed - (rust) block size 4096 73042.78 MByte/s (±51199.32 MByte/s) 74373.47 MByte/s (±51976.48 MByte/s) 0.98
Memset speed - (rust) block size 1048576 30506.73 MByte/s (±24677.12 MByte/s) 31110.89 MByte/s (±25033.24 MByte/s) 0.98
Memset speed - (rust) block size 16777216 26167.62 MByte/s (±21610.22 MByte/s) 28386.93 MByte/s (±23265.03 MByte/s) 0.92
alloc_benchmarks Build Time 82.61 s 74.76 s 1.11 ❗
alloc_benchmarks File Size 0.87 MB 0.87 MB 1.00 ❗
Allocations - Allocation success 91.35 % 91.31 % 1.00 ❗
Allocations - Deallocation success 100.00 % 100.00 % 1
Allocations - Pre-fail Allocations 61.61 % 61.44 % 1.00 ❗
Allocations - Average Allocation time 9214.02 Ticks (±87.52 Ticks) 5860.58 Ticks (±98.43 Ticks) 1.57 ❗
Allocations - Average Allocation time (no fail) 9628.61 Ticks (±94.32 Ticks) 6554.81 Ticks (±92.86 Ticks) 1.47 ❗
Allocations - Average Deallocation time 1388.79 Ticks (±338.16 Ticks) 1805.01 Ticks (±250.35 Ticks) 0.77
mutex_benchmark Build Time 82.56 s 79.82 s 1.03 ❗
mutex_benchmark File Size 0.89 MB 0.86 MB 1.04 ❗
Mutex Stress Test Average Time per Iteration - 1 Threads 12.26 ns (±0.48 ns) 12.10 ns (±0.41 ns) 1.01
Mutex Stress Test Average Time per Iteration - 2 Threads 93.70 ns (±6.51 ns) 40.26 ns (±1.68 ns) 2.33 ❗

This comment was automatically generated by workflow using github-action-benchmark.

@stlankes

stlankes commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

@martijnhoekstra Can you rebase your PR and test it?

The IOAPIC routes GSI n to vector 32 + n, but only IRQs 0 to 15 went
to handle_interrupt. Any other IRQ hit the exception handler, which
stops the core. On QEMU's microvm with ioapic2=off, the virtio-mmio
NIC raises IRQ 23, and Hermit stops with "Exception 55" before the
application starts.

Send IRQs 0 to 79 to handle_interrupt. The vectors from 112 up stay
with the local APIC.

Assisted-by: Claude:claude-opus-5-5
Without virtio_mmio.device= arguments, Hermit probes QEMU microvm's
transports and assumes transport n uses IRQ 5 + n. That only holds
with acpi=off. With ACPI, QEMU uses other GSIs and describes each
transport in the DSDT as an LNRO0005 device.

Read those devices with the acpi crate's AML interpreter. Probe only
if there are no ACPI tables.

Assisted-by: Claude:claude-opus-5-5
@martijnhoekstra

Copy link
Copy Markdown
Contributor Author

Rebased onto main and retested. With #2770, #2771 and #2775 on main, -machine microvm with default options now gets a working network under KVM and TCG. I also changed it to probe only when there are no ACPI tables, so builds without pci on q35 no longer read the microvm addresses. The test list in the description is updated.

@stlankes
stlankes self-requested a review November 7, 2026 22:47
@stlankes
stlankes added this pull request to the merge queue Nov 7, 2026
Merged via the queue into hermit-os:main with commit 0b8328c Nov 7, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

x86_64: no network on QEMU microvm with ACPI on

2 participants