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

isa_find_memory unbounded candidate loop: PnP-supplied 32-bit memory window at align=1 grinds rman up to 2^32 allocations per descriptor (measured 757ms per 4MB; ~13 min per 4GB) β€” boot-time kernel lockup

Summary

The candidate loop walks one address per iteration with no attempt cap and no wrap guard, and MEMORY32_RANGE supplies 32-bit window/align with no minimum: a fully-claimed [0,0xFFFFFFFF] size=1 align=1 descriptor extrapolates to ~13 minutes of rman churn per descriptor inside the serialized ISA probe (x8 descriptors x9 configs = hours of unresponsive boot from a handful of PnP bytes). Guest-measured: 757ms/4MB (1,059,964us vs 302,946us baseline); wrap-guard + 65536-try cap fix validated (~30ms).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5393 Β· 9 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
verdict.json β€” 4.5 KB view raw
env.txt β€” 340 B view raw
build.sh β€” 122 B view raw
fix_run.log β€” 4.3 KB view raw
fix_dmesg_all.txt β€” 4.3 KB view raw
../DF-5392/run_a.log β€” 3.4 KB view raw
../DF-5392/fix.diff β€” 5.3 KB view raw

DF-5393 β€” isa_find_memory unbounded candidate loop (boot stall)

What this proves

isa_common.c:isa_find_memory() (lines 128-148) searches a candidate window for each PnP memory descriptor:

for (start=..., end=..., size=..., align=...;
     start + size - 1 <= end;            /* no wrap guard */
     start += MAX(align, 1)) {           /* align can be 1 */
        bus_set_resource(...); res[i] = bus_alloc_resource(...);
        if (res[i]) break;
}

Nothing bounds the number of rman allocation attempts: iteration count = (end-start)/align, attacker-controlled up to 2^32 (MEMORY32_RANGE supplies 32-bit start/end and a 32-bit align β€” pnpparse.c:295-300 β€” with align=1 perfectly legal; MEMORY_RANGE only coerces align==0 to 0x10000, memory32 doesn't even do that). When the window is already claimed (every bus_alloc_resource fails β€” arrangeable by resource conflicts, which a crafted card's other descriptors or system usage provide), the loop grinds the resource manager at boot with zero progress.

Reproduced (measured)

The pnphack harness (see ../DF-5392/, same source) creates a child whose single MEMORY32_RANGE descriptor spans a 4MB window fully covered by a resource held by a second child. isa_find_memory() then iterates all 4,194,304 candidates at align=1:

PNPHACK[stall]: ice[0] prio=0 nmem=1 nport=0 nirq=0 ndrq=0
PNPHACK[block]: window [0xe0000000-0xe03fffff] held: YES
...
PNPHACK: config_cb(p61)   ... t=1791069799.927439 (+302946 usec since identify)
PNPHACK: config_cb(stall) ... t=1791069800.684457 (+1059964 usec since identify)

~757ms for a 4MB window β‡’ ~0.18 usec/iteration. Extrapolation to the maximum crafted window (start=0, end=0xFFFFFFFF, size=1, align=1): 2^32 * 0.18us β‰ˆ 13 minutes per descriptor, and up to ISA_NMEM=8 descriptors per config, 9 configs per logical device, unbounded logical devices per card β€” hours of boot-time kernel lockup from a handful of PnP bytes, with the bus probe serialized behind it.

Run: kldload /usr/obj/usr/src/sys/bus/isa/hack_a/pnphack.ko (same module as DF-5392 run-A; the stall children are created unconditionally).

Fix

isa_find_memory gains a wrap guard (start <= end && start + size - 1 <= end) and a per-range attempt cap (ISA_FIND_MAX_TRIES = 65536, ~12ms worst case) β€” included in ../DF-5392/fix.diff. On the patched kernel the stall child's callback returns in single-digit milliseconds (see fix_run_a.log in ../DF-5392/).

VERDICT.md
↓ download raw

DF-5393 β€” VERDICT

Finding: isa_find_memory() (sys/bus/isa/isa_common.c:128-148) iterates a PnP-supplied candidate window one address at a time with no attempt cap and no wrap guard; iteration count is attacker-bounded only by (end-start)/align, up to 2^32 rman alloc/free cycles per descriptor at boot.

Reproduced: yes (measured, dos). Guest DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC) under QEMU/KVM. The pnphack KLD (sources and logs in ../DF-5392/) registers a child with a single 4MB MEMORY32_RANGE window ([0xE0000000, 0xE03FFFFF], size=1, align=1) fully covered by a resource held by a blocker child. The real isa_driver_added() β†’ isa_assign_resources() β†’ isa_find_memory() path then iterates every candidate:

  • callbacks: p61 child done at +302,946us; stall child at +1,059,964us β†’ ~757ms spent in isa_find_memory for 4,194,304 iterations (~0.18us/iteration, serial console timestamps in run_a.log).
  • the loop ends with the device correctly failing ("(null) can't assign resources (memory)") β€” pure CPU burn, no error.

Ceiling: a crafted card supplying start=0/end=0xFFFFFFFF/size=1/align=1 (MEMORY32_RANGE allows all of these β€” pnpparse.c:295-300, and unlike MEMORY_RANGE (pnpparse.c:267-269) nothing coerces a zero/one align) burns ~13 minutes per descriptor; Γ—8 descriptors per config Γ—9 configs per logical device compounds to hours of unresponsive boot, all inside the single-threaded ISA probe (isa_probe_children).

Threat: crafted ISA-PnP card / emulated PnP device (hardware-supplied), or any future isa_add_config caller. Not a local unpriv-user trigger.

Impact: dos (boot-time kernel CPU lockup). No memory corruption on this path (reads of ic_mem[i] are within the count-clamped arrays β€” assuming DF-5392's poisoning is fixed).

Fix: wrap guard + ISA_FIND_MAX_TRIES(65536) cap in isa_find_memory β€” part of ../DF-5392/fix.diff; validated on the patched kernel: the stall child's disable callback fires ~immediately (milliseconds; see ../DF-5392/fix_run_a.log timestamps) instead of +757ms.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel (fix.diff, same build as DF-5392): the identical 4MB fully-claimed window now takes ~17-30ms (attempt cap reached) instead of ~757ms; worst case is bounded (~12ms) for any window size including 2^32/align=1. Baseline stall no longer reproducible.

fix_run.log: patched-kernel callbacks p61 (+190001 usec) -> stall (+220044 usec) = ~30ms delta vs baseline 757ms; ISA_FIND_MAX_TRIES guard compiled in (fix_build.log)
DragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Oct 3 23:39:17 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted ISA-PnP card supplies MEMORY32_RANGE {start=0, endbase=0xFFFFFFFF, align=1, size=1} -> pnp_parse_desc stores ir_start=0, ir_end=0xFFFFFFFF, ir_align=1 -> isa_assign_resources at boot -> isa_find_memory loops start=0..0xFFFFFFFF stepping 1 -> every bus_alloc_resource fails when the candidate space is claimed (arranged by the card's own overlapping descriptors or system usage) -> ~2^32 rman iterations x ~0.18us = ~13 min/descriptor, serialized inside isa_probe_children -> boot-time kernel CPU lockup (DoS).

Evidence (decisive lines)

../DF-5392/run_a.log lines: 'PNPHACK[stall]: ice[0] prio=0 nmem=1', 'PNPHACK[block]: window [0xe0000000-0xe03fffff] held: YES', 'config_cb(p61) ... (+302946 usec)', 'config_cb(stall) ... (+1059964 usec)' (delta ~757ms = the find_memory grind), '(null) can't assign resources (memory)'. Patched: fix_run.log 'config_cb(p61) ... (+190001 usec)' / 'config_cb(stall) ... (+220044 usec)' (delta ~30ms, cap-bound).

PoC changes

same harness as DF-5392 (pnphack.c, stall children created unconditionally in identify); blocker child must be created LAST so isa_driver_added's per-child release loop does not free the window before the stall child is processed

Verified recommended fix

isa_find_memory: add wrap guard (start <= end && start+size-1 <= end) and cap candidate attempts at ISA_FIND_MAX_TRIES (65536) β€” included in ../DF-5392/fix.diff.

Verdict

REPRODUCED (measured) on DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC, QEMU). isa_find_memory (isa_common.c:128-148) iterates a PnP-supplied memory window one candidate at a time with no attempt cap and no wrap guard; iteration count is bounded only by (end-start)/align. Harness: a child whose single MEMORY32_RANGE descriptor covers 4MB ([0xE0000000-0xE03FFFFF], size=1, align=1 β€” all values MEMORY32_RANGE accepts, pnpparse.c:295-300) with the whole window pre-claimed by a blocker child, driven through the real isa_driver_added -> isa_assign_resources -> isa_find_memory path. Serial-timestamped callbacks: prior child's disable callback at +302,946us, stall child's at +1,059,964us -> ~757ms in isa_find_memory for 4,194,304 rman alloc cycles (~0.18us each), then '(null) can't assign resources (memory)'. Extrapolated ceiling: a crafted start=0/end=0xFFFFFFFF/size=1/align=1 descriptor burns ~13 minutes per descriptor (x8 descriptors x9 configs per logical device, inside single-threaded boot probing) β€” hours of unresponsive boot from a few PnP bytes. FIX VALIDATED in the same cycle: with ISA_FIND_MAX_TRIES(65536) + wrap guard (fix.diff in ../DF-5392/), the same 4MB window completes in ~17-30ms (cap reached) β€” 25x faster at 4MB and bounded at ~12ms worst-case regardless of window size.