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

iounmap() walks the global iomap_list without iomap_lock, racing concurrent inserters/removers into UAF read, double-free and NULL-deref panic

Summary

iounmap() does unlocked SLIST_FOREACH_MUTABLE lookup over process-global iomap_list then later still without lock calls pmap_unmapdev() and only acquires iomap_lock for SLIST_REMOVE/kfree tail. List is global (not per-device) every DRM driver shares it. Any two concurrent iounmap() calls (cross-device DRM_IOCTL_RM_MAP DRM_AUTH-only vs driver teardown or double-cleanup) race. Loser either: (a) dereferences kfree()-d saved-next pointer (UAF read kernel heap info leak/wild walk); (b) double-calls pmap_unmapdev/kmem_free on same VA range (double-free); (c) passes entry not present in list to SLIST_REMOVE which walks to NULL derefs NULL panic. SLIST_FOREACH_MUTABLE stashes tmp_imp=SLIST_NEXT(imp) read unlocked and dereferenced next iteration. pmap_unmapdev called BEFORE entry unlinked. Between unlocked lookup (line 68) and locked remove (line 99) entry can be removed+freed by concurrent iounmap. Same-handle double-call: both pass found-check both pmap_unmapdev same VA (double kmem_free) second SLIST_REMOVE walks past list end SLIST_NEXT(NULL) deref panic. List is global across ALL DRM devices so per-device dev->struct_mutex does NOT serialize cross-device iounmap. Impact UAF read freed heap info leak/wild pointer double-free kernel_map VA memory corruption often escalable to arbitrary kernel write via slab grooming or unambiguous panic DoS.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2206 Β· 8 files
FileTypeDescriptionSize
README.md readme claim, source-trace pointers, fix summary 3.6 KB ↓ raw
VERDICT.md verdict full narrative: race window, outcomes, fix 3.2 KB ↓ raw
fix.diff suggested-fix hold iomap_lock from iounmap entry through kfree 1.4 KB view raw
build.sh build-script applies fix.diff + rebuilds drm.ko in guest 812 B view raw
run.sh run-script HW-gated no-op runner 432 B view raw
build_baseline.log build-log unpatched drm.ko build, rc=0, -Werror 130.8 KB view raw
build_patched.log build-log patched drm.ko rebuild, only linux_iomapping.o recompiled, rc=0, -Werror 2.9 KB view raw
env.txt environment uname, cc, kern.version 376 B view raw
README.md readme claim, source-trace pointers, fix summary
↓ download raw

DF-2206 β€” iounmap() walks global iomap_list without lock (High)

Claim

iounmap() in sys/dev/drm/linux_iomapping.c performs an unlocked SLIST_FOREACH_MUTABLE lookup over the process-global iomap_list, then calls pmap_unmapdev() and only later (still without holding the lock during the unmap) acquires iomap_lock purely for the SLIST_REMOVE / kfree tail. Because the list is shared across every DRM device (not per-device), two concurrent iounmap() callers can race and either dereference a kfree()-ed saved-next pointer (UAF read), double-call pmap_unmapdev / SLIST_REMOVE on the same VA (double free), or walk past end-of-list (NULL deref panic).

Verification approach

HW-gated / source-only. iounmap() is exercised only by DRM drivers (radeon, amdgpu, i915) during MMIO teardown, which on this audit guest has no backing GPU. Per the task brief, source-only confirmation is acceptable for HW/module-gated findings. The race is fully visible at the source level and the structural fix is mechanical, so the verification is:

  1. Source-trace the cited path line-by-line in sys/dev/drm/linux_iomapping.c and confirm the lock-acquire / release pattern matches the claim.
  2. Confirm the list is process-global (single SLIST_HEAD at file scope).
  3. Author fix.diff (hold iomap_lock for the entire op).
  4. Phase 8 β€” apply all 5 batched fixes and rebuild drm.ko with -Werror to confirm the patched source compiles cleanly.

Source trace (confirmed)

Race window

Between the unlocked lookup (line 68) and the locked remove (line 99) the entry pointed at by imp (and the tmp_imp = SLIST_NEXT(imp) saved by SLIST_FOREACH_MUTABLE) can be removed and kfree()-ed by a concurrent iounmap() on the same or any other DRM device, because the list is global. Worst observable outcomes: * UAF read of tmp_imp next iteration (kernel heap info leak / wild walk). * Same-handle double iounmap: both pass found, both call pmap_unmapdev on the same VA (double kmem_free of kernel_map), second SLIST_REMOVE walks SLIST_NEXT(NULL) β†’ panic.

Files

  • VERDICT.md β€” full narrative.
  • fix.diff β€” git-apply-able fix: hold iomap_lock from function entry through kfree. iomap_lock is LK_CANRECURSE and the called helpers (pmap_unmapdev, pmap_change_attr, kfree) do not take it, so holding it across teardown is safe.
  • build_baseline.log β€” full unpatched drm.ko build, rc=0, -Werror.
  • build_patched.log β€” full patched drm.ko rebuild (only linux_iomapping.o recompiled), rc=0, -Werror.
  • env.txt β€” guest environment.

Reproduce

./build.sh    # applies fix.diff into in-guest /usr/src and rebuilds drm.ko
./run.sh      # re-runs (here: only a source-level repro is possible)
VERDICT.md verdict full narrative: race window, outcomes, fix
↓ download raw

DF-2206 β€” VERDICT

Verdict: REPRODUCED (source-only, HW-gated).

Class: kernel race / UAF / double-free / NULL deref (memory corruption).

Mechanism

iounmap() in sys/dev/drm/linux_iomapping.c tears down a previously ioremap()-ed MMIO mapping. The list of live mappings (iomap_list, declared at linux_iomapping.c:39) is a process-global singly-linked SLIST shared by every DRM driver in the system; it is not per-device. The lookup of the matching entry is performed by SLIST_FOREACH_MUTABLE without holding the list lock:

/* linux_iomapping.c:68 */
SLIST_FOREACH_MUTABLE(imp, &iomap_list, im_iomaps, tmp_imp) {
    if (imp->pmap_addr == ptr) { found = 1; break; }
}

After the unlocked lookup succeeds, the code uses imp (and computes paddr_end from imp->npages * PAGE_SIZE) at line 80, optionally calls pmap_change_attr at line 90, calls pmap_unmapdev at line 96 β€” and only then, at lines 98-100, acquires iomap_lock to do SLIST_REMOVE, releases the lock, and kfree(imp) at line 102.

Because the list is global, per-device dev->struct_mutex does not serialize cross-device iounmap(). Two concurrent callers therefore race in the window between the unlocked lookup (line 68) and the locked remove (line 99):

Outcome How
UAF read / wild walk caller A's tmp_imp = SLIST_NEXT(imp) saved by SLIST_FOREACH_MUTABLE points at an entry caller B already kfree()-ed.
double free of kernel_map VA both callers pass the unlocked found check for the same pmap_addr, both call pmap_unmapdev on that VA.
NULL deref panic second SLIST_REMOVE for the same handle walks SLIST_NEXT(NULL) past end of list.

Each of these is a kernel memory-corruption primitive (slab-groommable to arbitrary kernel write in principle; unambiguous panic DoS in practice).

Trigger surface / reachability

iounmap() is reached from every DRM driver's MMIO teardown path (amdgpu_device_fini, radeon/i915 fini, etc.) and from DRM_IOCTL_RM_MAP (drm_bufs.c) which is DRM_AUTH-only. Two concurrent teardowns (driver reset + ioctl, or cross-device) race.

On this audit guest there is no GPU, so the race cannot be exercised live; it is confirmed at the source level. The structural fix is mechanical and the patched module compiles cleanly (Phase 8).

Fix

fix.diff holds iomap_lock from function entry through the final kfree. iomap_lock is initialised LK_CANRECURSE (linux_iomapping.c:37) and none of the helpers called during teardown (pmap_unmapdev, pmap_change_attr, kfree) acquire iomap_lock, so holding it across the teardown cannot deadlock. An early-return path (!found) is updated to release the lock before returning.

Phase 8 build validation

Applied fix.diff (plus the four other batched DRM fixes) to the in-guest /usr/src, rebuilt drm.ko with -Werror: * baseline (unpatched) drm.ko: rc=0 (build_baseline.log). * patched drm.ko: rc=0, only linux_iomapping.o recompiled (build_patched.log); no warnings, no errors.

Verdict

REPRODUCED at source level (HW-gated; no live repro possible on guest). The race is real, the primitive is memory corruption, and the fix compiles cleanly under -Werror.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched reproduced

drm.ko baseline+patched rc=0 -Werror

drm.ko baseline+patched rc=0 -Werror
↓ fix.diffmodule build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none (HW-gated)

Evidence (decisive lines)

HW-gated (no GPU). Source-confirmed: iounmap walks global iomap_list without lock -> UAF/double-free race.

Verified recommended fix

HW-gated (no GPU). Source-confirmed: iounmap walks global iomap_list without lock -> UAF/double-free race.

Verdict

HW-gated (no GPU). Source-confirmed: iounmap walks global iomap_list without lock -> UAF/double-free race.