β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5352

ioapic_abi_legacy_intr_config(): driver-facing ABI panics production kernels when called with a non-legacy IRQ (KKASSERT-only validation, void return)

Summary

bus_config_intr on a non-legacy IRQ (e.g. the driver's own just-allocated MSI-X vector): KKASSERT-only validation compiles out on production kernels, the LEGACY search falls through with cpuid==ncpus, flags written into a wrong-type map, then ioapic_gsi_pin(-1) panics 'no I/O APIC'; INVARIANTS kernels panic at the assert. Reproduced BOTH kernel flavors on the guest via KLD (msix_alloc until irq>=24 then config) + fix validated (silent return, valid config unaffected). Runtime-guard-and-return fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5352 Β· 11 files
FileTypeDescriptionSize
legacy_crash.c β€” 2.9 KB view raw
build.sh β€” 119 B view raw
run.sh β€” 108 B view raw
run_prodkernel.log β€” 609 B view raw
panic_kkassert.txt β€” 1.3 KB view raw
run_invkernel.log β€” 0 B ↓ download
run_fixed.log β€” 582 B view raw
env.txt β€” 988 B view raw
VERDICT.md β€” 4.6 KB ↓ raw
fix.diff β€” 891 B view raw
verdict.json β€” 3.9 KB view raw
VERDICT.md
↓ download raw

DF-5352 VERDICT β€” reproduced (kernel panic from driver-facing legacy-config ABI), Low severity

Bottom line

REPRODUCED on the guest both ways:

  • production-behaviour kernel (X86_64_GENERIC minus options INVARIANTS; stock GENERIC ships without INVARIANTS): a KLD calling MachIntrABI.legacy_intr_config(irq=24, LEVEL, LOW) where irq 24 is an MSI-X map (im_gsi == -1) panics the kernel: panic: ioapic_gsi_search: no I/O APIC (ioapic.c:415-427, reached via ioapic_abi.c:1038 ioapic_gsi_pin(map->im_gsi)).
  • INVARIANTS kernel: the same trigger panics at the debug assert panic: assertion "irq >= 0 && irq < ioapic_abi_legacy_irq_max" failed in ioapic_abi_legacy_intr_config at ioapic_abi.c:999.

Either way a single bad driver call β€” bus_config_intr() on an IRQ that isn't a legacy map, e.g. the MSI/MSI-X vector the driver itself just allocated β€” is promoted to a kernel panic. The function returns void; no error path exists. On production kernels the guard KKASSERTs at ioapic_abi.c:999/:1005 compile to nothing (sys/sys/systm.h:94-122), the search loop at :1000-1004 falls through with cpuid==ncpus, and :1022/:1035 write flag/trigger fields into a wrong-type map before :1038 feeds im_gsi=-1 into the gsi lookup. Fix validated: with runtime range/type checks (early return) the identical kldload returns cleanly, no panic.

Reproduction (baseline)

Production kernel (run_prodkernel.log):

legacy_crash: msix_alloc rc=0 irq=24
legacy_crash: calling legacy_intr_config(irq=24, LEVEL, LOW); production kernel should panic in ioapic_gsi_search (im_gsi == -1)
panic: ioapic_gsi_search: no I/O APIC
ioapic_gsi_search() at ioapic_gsi_search+0x3a 0xffffffff80b7350a
Debugger("panic")

INVARIANTS kernel (panic_kkassert.txt β€” captured 07:58 UTC on the stock X86_64_GENERIC):

legacy_crash: ... irq=24
panic: assertion "irq >= 0 && irq < ioapic_abi_legacy_irq_max" failed in ioapic_abi_legacy_intr_config at /usr/src/sys/platform/pc64/apic/ioapic_abi.c:999

Note on trigger selection: the first attempt used irq 19 and returned without panic β€” on this QEMU guest irqs 16..23 are legacy level/low maps (on some CPU), so ioapic_abi_legacy_intr_config() found a LEGACY map, matched trig/pola, and took the early return at ioapic_abi.c:1024-1025. The module therefore loops msix_alloc until it lands an irq >= 24. This also documents the correct-behavior baseline: valid legacy requests are unaffected.

Impact

DoS/panic from privileged (driver) misuse; no memory corruption on the demonstrated path (the -1 gsi reaches ioapic_gsi_search which panics by design instead of indexing OOB). No unprivileged entry point. Severity Low. Same pattern class as DF-5351: INVARIANTS-only guards on a driver-facing ABI (also cited: ioapic_abi_legacy_intr_cpuid ioapic_abi.c:1121 silently returning cpu0, ioapic_abi_msi_map ioapic_abi.c:1413 passing im_msi_base=-1 to msi_map for non-MSI intrs β€” benign values, no memory unsafety).

Fix validation (fix.diff)

Applied to guest /usr/src, incremental X86_64_NOINV rebuild (#1 08:38:15), reboot, identical PoC (run_fixed.log):

legacy_crash: msix_alloc rc=0 irq=24
legacy_crash: calling legacy_intr_config(irq=24, LEVEL, LOW); ...
legacy_crash: RETURNED WITHOUT PANIC (unexpected)
LC_RC=0, guest up

No panic; correct legacy configurations (irq 19 path) still take the normal route; MSI-X allocations/releases unaffected.

poc_changes

  • DECLARE_MODULE moduledata_t form (see DF-5351 VERDICT).
  • Loop msix allocations until irq >= 24 (irqs 16..23 are legacy on this guest) β€” the naive "first msix irq" trigger hits a real legacy map and returns cleanly.
  • Built with the in-tree module recipe (see build.sh).

Kernel references

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, X86_64_NOINV rebuilt (#1 08:38:15) and rebooted; identical PoC re-run: legacy_intr_config(irq=24, LEVEL, LOW) returns silently, 'RETURNED WITHOUT PANIC', LC_RC=0, guest stays up (run_fixed.log). No panic; valid legacy path (irq 19) still configured normally.

findings/poc/DF-5352/run_fixed.log; fix.diff
↓ fix.diffper-fix-DF-5352

Confirmed kernel references

Detail

Exploit chain

Driver (KLD / any driver calling bus_config_intr() or MachIntrABI.legacy_intr_config()) passes a non-legacy IRQ, e.g. its own MSI-X vector -> KKASSERT guards are no-ops on production kernels -> cpuid exits == ncpus, map = last cpu's non-legacy map -> im_flags/im_trig/im_pola written into wrong-type map -> ioapic_gsi_pin(-1) -> ioapic_gsi_search(-1) -> panic('no I/O APIC'). DoS from privileged misuse; no memory corruption on this path (gsi_search panics by design); no unprivileged entry point.

Evidence (decisive lines)

run_prodkernel.log (production kernel: 'legacy_intr_config(irq=24, LEVEL, LOW)' then 'panic: ioapic_gsi_search: no I/O APIC' + frame ioapic_gsi_search+0x3a); panic_kkassert.txt (INVARIANTS kernel: assert at ioapic_abi.c:999); run_fixed.log (fixed kernel: 'RETURNED WITHOUT PANIC', LC_RC=0, guest up)

PoC changes

DECLARE_MODULE moduledata_t form; loop msix_alloc until irq >= 24 (on this guest irqs 16..23 are legacy level/low maps on some CPU, so the naive first-msix-irq trigger hits the normal early-return path at ioapic_abi.c:1024 and returns cleanly - kept as the correct-behavior baseline); in-tree kmod build recipe

Verified recommended fix

In ioapic_abi_legacy_intr_config(), replace the KKASSERTs with runtime checks: return early when irq is out of [0, ioapic_abi_legacy_irq_max) and when no CPU holds a LEGACY map for that irq (cpuid == ncpus)

Verdict

Reproduced: on a production-behaviour kernel (no INVARIANTS), calling the driver-facing legacy-config ABI with an IRQ that is an MSI-X map (im_gsi=-1) panics the kernel - panic: ioapic_gsi_search: no I/O APIC - because ioapic_abi_legacy_intr_config()'s only validation is KKASSERTs that compile out (ioapic_abi.c:999/:1005); the LEGACY search loop falls through, the wrong-type map is mutated, and ioapic_gsi_pin(-1) panics (ioapic.c:415-427). On the INVARIANTS kernel the same trigger panics at the :999 assert instead. Fix (runtime range/type checks with early return) makes the identical kldload return cleanly with no panic and leaves valid legacy configurations working.