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

Unvalidated hint IRQ passed to machintr_legacy_intr_cpuid(): deterministic boot-time kernel panic (KKASSERT) from one loader.conf line; OOB ioapic_irqmaps[cpuid][irq] read on non-INVARIANTS kernels

Summary

The hint IRQ is checked only for start>0 before being fed to the interrupt ABI: hint.lm.0.irq=0x10000 panics 1/1 boots at isa_probe_children (KKASSERT in ioapic_abi_legacy_intr_cpuid, full backtrace captured) - and on non-INVARIANTS kernels the compiled-out assert becomes an OOB read of ioapic_irqmaps at a hint-chosen 31-bit index. Loader.conf/root-gated (runtime kenv injection cannot re-trigger - identify early-returns on rescan); robustness boot DoS. start<16 bound fix validated on a rebuilt kernel (same hint boots cleanly).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5646 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
trigger.hint β€” 24 B ↓ download
build.sh β€” 86 B view raw
run.sh β€” 160 B view raw
run.log β€” 1.7 KB view raw
panic.txt β€” 1.7 KB view raw
env.txt β€” 454 B view raw
fix.diff β€” 973 B view raw
verdict.json β€” 3.3 KB view raw

DF-5646 β€” unvalidated hint.<dev>.N.irq reaches machintr_legacy_intr_cpuid() β†’ boot-time kernel panic (KKASSERT / OOB index on release kernels)

Where

sys/bus/isa/isahint.c:73-76 (isahint_add_device)

Build

Nothing to compile β€” pure loader-configuration trigger.

Run (the exact repro)

# on the guest, as root:
printf 'hint.lm.0.irq="0x10000"\n' > /boot/loader.conf.local
shutdown -r now

Expected (observed 2026-10-09, guest dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC, INVARIANTS)

Kernel panics during ISA bus configuration, before userland:

panic: assertion "irq >= 0 && irq < ioapic_abi_legacy_irq_max" failed in ioapic_abi_legacy_intr_cpuid at /usr/src/sys/platform/pc64/apic/ioapic_abi.c:1127
cpuid = 0
Trace beginning at frame 0xffffffff817adb00
ioapic_abi_legacy_intr_cpuid() at ioapic_abi_legacy_intr_cpuid+0xed
isahint_add_device() at isahint_add_device+0x193
isahint_identify() at isahint_identify+0x125
bus_generic_probe_hack() at bus_generic_probe_hack+0x75
isa_probe_children() at isa_probe_children+0x1c
Debugger("panic")
db>

The machine is dead at the db> prompt β€” permanent boot-time DoS from one loader.conf line. vm.sh reset with-src to recover.

Threat model

hint.* values come from /boot/loader.conf* (root/loader-stage write). This is a robustness/validation bug in the hint→resource plumbing: any typo'd or hostile large irq hint bricks the machine at boot. On kernels built without INVARIANTS the KKASSERT is compiled out and the same input becomes an out-of-bounds read ioapic_irqmaps[cpuid][irq] (ioapic_abi.c:1129-1131) with an attacker-chosen 31-bit index (silent for small values, page-fault panic for large ones).

VERDICT.md
↓ download raw

DF-5646 β€” VERDICT

Status: reproduced (boot-time kernel panic, deterministic, 1/1 boots) Impact: panic = permanent boot DoS from one loader.conf line (root/loader threat model β€” Low severity per rubric "already-privileged user", but it is a hard kernel panic reached from documented configuration syntax)

Root cause chain (all live-verified)

  1. /boot/loader.conf.local sets hint.lm.0.irq="0x10000". The loader puts it into the static kernel environment (kenv).

  2. At SI_SUB_CONFIGURE, isa_probe_children(isa1) (isa_common.c:432) calls bus_generic_probe_hack() β†’ DEVICE_IDENTIFY for every driver of the "isa" devclass β†’ isahint_identify() (isahint.c:89), which walks the config(8)-generated devtab (ioconf.c: devtab_count = 19, lm0 has at="isa") and calls isahint_add_device(parent, "lm", 0).

  3. isahint_add_device fetches the irq hint: isahint.c:73 resource_int_value("lm", 0, "irq", &start) β†’ resource_kenv() (subr_bus.c:2315-2323) finds hint.lm.0.irq (kenv is checked before the compiled devtab) β†’ strtol("0x10000") β†’ 65536. The ONLY validation is start > 0.

  4. isahint.c:74-75 passes the raw value straight into the interrupt ABI: bus_set_resource(child, SYS_RES_IRQ, 0, start, 1, machintr_legacy_intr_cpuid(start)) β€” the cpuid argument is evaluated BEFORE any range check exists anywhere.

  5. On IOAPIC systems ioapic_abi_legacy_intr_cpuid() (ioapic_abi.c:1122) opens with KKASSERT(irq >= 0 && irq < ioapic_abi_legacy_irq_max) (ioapic_abi.c:1126-1127). 65536 β‰₯ legacy_irq_max(=16 or 24) β†’ panic.

Live evidence

  • findings/poc/DF-5646/panic.txt: the full panic + backtrace captured from the serial console (vm.sh log). The stack is exactly the chain above: isa_probe_children β†’ bus_generic_probe_hack β†’ isahint_identify β†’ isahint_add_device β†’ ioapic_abi_legacy_intr_cpuid β†’ KKASSERT panic.
  • Guest state after: wedged in ddb (db>), ssh dead β€” a hard brick until out-of-band reset. Recovered with vm.sh reset with-src.

Why the "small" variants matter (release kernels)

options INVARIANTS (X86_64_GENERIC:56) compiles KKASSERT in; stock GENERIC has it. A kernel built without INVARIANTS executes the loop at ioapic_abi.c:1129-1131 instead: map = &ioapic_irqmaps[cpuid][irq] with ioapic_irqmaps[MAXCPU][IOAPIC_HWI_VECTORS] (ioapic_abi.c:469) β€” an out-of-bounds read at a hint-controlled 31-bit index: silent for small values (reads adjacent kernel data, result only steers a cpuid pick), guaranteed page-fault panic for large values. Same root cause, same one-line fix in isahint.c (validate before calling the ABI).

Sibling call sites with the same pattern but driver-supplied (not hint-supplied) values: sys/bus/isa/x86_64/isa.c:100, sys/kern/subr_bus.c:3308 β€” those are not changed by this fix.

Fix

fix.diff (shared with DF-5647's pack): require start > 0 && start < 16 (legacy ISA IRQ space) before touching the ABI. Validated by rebuild + re-run: with the patched kernel the same hint.lm.0.irq="0x10000" boot comes up cleanly (see fix_evidence).

Classification notes

  • Not reachable by unprivileged users: kenv hints are loader-stage (or root kenv(1), and isahint_identify early-returns on rescan β€” runtime kenv injection does not re-run it).
  • Not a memory-corruption primitive on INVARIANTS kernels (assert fires first); on release kernels it is an OOB read only.
  • Honest severity: Low (boot DoS / wild read, gated on loader.conf write = root). CVSS reflects that.

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, kernel rebuilt (nativekernel, BUILD_OK, uname -v '#1: Fri Oct 9 05:53:50 UTC 2026'), rebooted with the exact same hint.lm.0.irq=0x10000 armed alongside the DF-5647 poison set: no panic, guest reached ssh, dmesg clean. Baseline panic / patched boot both captured.

run.fixed.log (current-boot dmesg segment of the patched kernel: no panic lines, devices probe normally); build.log (kernel build + installkernel + BUILD_OK tail)
↓ fix.diffper-fix-DF-5646

Confirmed kernel references

Detail

Exploit chain

loader.conf.local hint.lm.0.irq=0x10000 -> loader static kenv -> resource_kenv (subr_bus.c:2315, consulted before compiled devtab) -> isahint_identify walks devtab (lm0 at=isa, ioconf.c) -> isahint_add_device: resource_int_value(irq) returns 65536, only check start>0 -> machintr_legacy_intr_cpuid(65536) -> ioapic_abi KKASSERT panic at boot; no unprivileged path (identify cannot be re-triggered at runtime).

Evidence (decisive lines)

panic.txt (full panic + backtrace: isa_probe_children -> bus_generic_probe_hack -> isahint_identify -> isahint_add_device -> ioapic_abi_legacy_intr_cpuid); run.fixed.log (patched kernel boots clean with the same hint armed); env.txt; VERDICT.md

PoC changes

None needed β€” the seeded one-line hint trigger works verbatim (value 0x10000 chosen to exceed ioapic_abi_legacy_irq_max while parsing cleanly via strtol).

Verified recommended fix

Validate the hint irq against the legacy ISA IRQ space before calling the interrupt ABI: require start > 0 && start < 16 at isahint.c:73.

Verdict

One loader.conf hint line (hint.lm.0.irq="0x10000") panics the kernel at boot: isahint.c:73-75 passes the unvalidated kenv irq straight into machintr_legacy_intr_cpuid(), and ioapic_abi_legacy_intr_cpuid() (ioapic_abi.c:1126-1127) KKASSERTs irq < ioapic_abi_legacy_irq_max during isa_probe_children. Deterministic 1/1 boots, machine dead in ddb before userland. On kernels without INVARIANTS the same input is an OOB read of ioapic_irqmaps[cpuid][irq] (ioapic_abi.c:1129-1131) with a hint-chosen 31-bit index. Root/loader-gated, hence Low severity, but a hard boot-time kernel panic from documented configuration syntax.