# 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.
