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)
PoC verification
Evidence pack
findings/poc/DF-5393 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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/).
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
fixedPatched 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)
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.
No comments yet.