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)
PoC verification
Evidence pack
findings/poc/DF-5646 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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)
-
/boot/loader.conf.localsetshint.lm.0.irq="0x10000". The loader puts it into the static kernel environment (kenv). -
At
SI_SUB_CONFIGURE,isa_probe_children(isa1)(isa_common.c:432) callsbus_generic_probe_hack()βDEVICE_IDENTIFYfor every driver of the "isa" devclass βisahint_identify()(isahint.c:89), which walks the config(8)-generated devtab (ioconf.c:devtab_count = 19,lm0hasat="isa") and callsisahint_add_device(parent, "lm", 0). -
isahint_add_devicefetches the irq hint: isahint.c:73resource_int_value("lm", 0, "irq", &start)βresource_kenv()(subr_bus.c:2315-2323) findshint.lm.0.irq(kenv is checked before the compiled devtab) βstrtol("0x10000")β 65536. The ONLY validation isstart > 0. -
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. -
On IOAPIC systems
ioapic_abi_legacy_intr_cpuid()(ioapic_abi.c:1122) opens withKKASSERT(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 withvm.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), andisahint_identifyearly-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
fixedfix.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)
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.
No comments yet.