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

drm_legacy_ioremapfree() bypasses linuxkpi iounmap() bookkeeping, permanently orphaning iomap_list entries for any kmod user of the exported wrapper pair

Summary

drm_legacy_ioremap() registers every mapping in the global iomap_list via __ioremap_common, but the free wrapper unmaps with raw pmap_unmapdev instead of iounmap - the only code that removes the list entry, frees the struct iomap, and restores PAT attrs for RAM-backed ranges. Zero in-tree callers (exported symbols only); live MMIO paths use ioremap/iounmap directly. Root-loaded bespoke kmod required - defense-in-depth. One-line iounmap fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5222 Β· 5 files
FileTypeDescriptionSize
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 3.8 KB ↓ raw
fix.diff β€” 370 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 3.4 KB view raw

DF-5222 β€” drm_legacy_ioremapfree bypasses linuxkpi iounmap() bookkeeping

File: sys/dev/drm/drm_memory.c (pass-2 GLM 5.3 audit) Severity: Info (hardening / defense-in-depth). Not attacker-reachable in-tree.

What

drm_legacy_ioremap() / drm_legacy_ioremap_wc() (sys/dev/drm/drm_memory.c:64-74) map MMIO via the linuxkpi ioremap() family, which is implemented by __ioremap_common() (sys/dev/drm/linux_iomapping.c:41-59):

  • kmallocs a struct iomap (linux_iomapping.c:50),
  • maps with pmap_mapdev_attr() (:53),
  • registers the mapping in the global iomap_list under iomap_lock (:54-56).

The only code that ever removes an entry from iomap_list, frees the struct iomap, and restores PAT attributes for RAM-backed ranges is iounmap() (linux_iomapping.c:61-103).

drm_legacy_ioremapfree() (drm_memory.c:76-83) instead calls pmap_unmapdev((vm_offset_t)map->handle, map->size) directly (drm_memory.c:81), skipping iounmap() entirely. Consequence for any caller pairing the two exported wrappers:

  1. the struct iomap entry is leaked from iomap_list forever (kernel-memory leak + stale SLIST node whose pmap_addr points at freed KVA),
  2. the PAT-attribute restore for RAM-backed mappings (linux_iomapping.c:88-91) never happens,
  3. the device mapping's KVA is returned to kernel_map while a stale node still names it β€” a bookkeeping inconsistency that any future mixed ioremapfree/iounmap use turns into wrong-range unmap bookkeeping.

Why Info (not higher)

The whole wrapper family is orphaned in-tree: a whole-tree search finds zero callers of drm_legacy_ioremap, drm_legacy_ioremap_wc or drm_legacy_ioremapfree (only the definitions at drm_memory.c:64-83 and the prototypes in include/drm/drm_legacy.h:191-193). The DRM core paths that DO map device memory use ioremap()/ioremap_wc()/iounmap() directly:

  • drm_bufs.c:219-222 (drm_addmap_core _DRM_REGISTERS),
  • drm_bufs.c:341, 358 (error unwinds),
  • drm_bufs.c:524 (drm_legacy_rmmap).

The only user-controlled entry into this area, DRM_IOCTL_ADD_MAP, is gated DRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY (drm_ioctl.c:590) β€” root only β€” and does not go through these wrappers anyway. Reaching the mismatch therefore requires root to load a hand-written kmod that calls the exported wrappers β€” i.e. root attacking itself. No unprivileged route exists.

Reproduce (conceptual β€” intentionally NOT run)

PoC execution was skipped per audit contract (Low/Info hardening finding). A kmod proof would be: kldload a module that calls drm_legacy_ioremap(&map, dev) with a page-aligned MMIO range, then drm_legacy_ioremapfree(&map), then walk iomap_list via ddb β€” the entry remains. fix.diff removes the hazard by routing the free through iounmap().

Fix

One line β€” see fix.diff: replace the direct pmap_unmapdev() call with iounmap(map->handle). iounmap is already declared for this file via drmP.h:43 β†’ linux/io.h:32 β†’ asm/io.h:146.

VERDICT.md
↓ download raw

DF-5222 β€” VERDICT

Finding: exported drm_legacy_ioremapfree() unmaps device memory with a raw pmap_unmapdev() instead of the linuxkpi iounmap(), orphaning the struct iomap bookkeeping entry that drm_legacy_ioremap() created.

Verdict: confirmed at code level; untested at runtime (by design). Info severity β€” hardening/defense-in-depth; there is no attacker-reachable path in this tree, so no PoC was executed (contract: PoCs are required for Critical/High and memcorrupt/privesc/hammer2 buckets; Info hardening is exempt). Guest was not booted for this finding.

Root cause (line-accurate)

  • drm_legacy_ioremap() β€” sys/dev/drm/drm_memory.c:64-68 β†’ linuxkpi ioremap() (asm/io.h:129) β†’ __ioremap_common() β€” sys/dev/drm/linux_iomapping.c:41-59. __ioremap_common kmallocs struct iomap (:50), maps via pmap_mapdev_attr() (:53), inserts into the global iomap_list under iomap_lock (:54-56).
  • drm_legacy_ioremapfree() β€” sys/dev/drm/drm_memory.c:76-83 β€” calls pmap_unmapdev((vm_offset_t)map->handle, map->size) at :81, bypassing iounmap() (linux_iomapping.c:61-103), which is the ONLY code that removes the list entry (:99), frees the struct iomap (:102), and restores PAT attributes for RAM-backed ranges (:88-91).
  • Net effect for any wrapper user: permanent struct iomap + SLIST-node leak per map, stale node naming freed KVA, PAT restore skipped.

Reachability analysis (why this is Info, not higher)

  1. Zero in-tree callers. Whole-sys/ search: the only references to the three wrappers are their definitions (drm_memory.c:64,70,76) and prototypes (include/drm/drm_legacy.h:191-193). They are pure exported dead API.
  2. The live MMIO paths don't use them. drm_addmap_core() maps registers with ioremap()/ioremap_wc() directly (drm_bufs.c:219-222) and unwinds with iounmap() (drm_bufs.c:341,358); drm_legacy_rmmap() frees with iounmap() (drm_bufs.c:524) β€” bookkeeping-consistent.
  3. The only user-reachable ioctl in the area is root-gated. DRM_IOCTL_ADD_MAP β†’ drm_legacy_addmap_ioctl is registered DRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY (drm_ioctl.c:590).
  4. Triggering the mismatch therefore requires root to write and load a kmod calling the wrappers β€” root attacking itself. pmap_unmapdev itself rounds the size up (sys/platform/pc64/x86_64/pmap.c:6227-6229), so no partial-unmap hazard exists either; the defect is purely the leaked/stale bookkeeping.

What else was checked in drm_memory.c (pass-2 negative evidence)

  • No size math, arithmetic, or copyin/copyout exists in the file β€” the wrappers are pass-throughs; drm_get_max_iomem() is #if 0 (drm_memory.c:85-97).
  • __ioremap_common panics via BUG_ON on non-page-aligned offset/size (linux_iomapping.c:47-48); its only in-tree caller with user-influenced parameters (drm_addmap_core) pre-validates page alignment (drm_bufs.c:176-179) β€” no path feeds unaligned values.
  • The #else-branch agp_remap stub (drm_memory.c:56-60) is unused dead code.
  • No page allocators, DMA helpers, or zeroing logic remain in this file (upstream's drm_alloc_pages family lives elsewhere now) β€” the file has been reduced to the three ioremap wrappers.

Fix

fix.diff: route the free through iounmap(map->handle). Declaration already visible via drmP.h:43 β†’ linux/io.h:32 β†’ asm/io.h:146. Not runtime-validated (no reproduction to validate against; fix_status=not_testable).

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

not_testable

fix.diff is a one-line reroute of the free path through iounmap(); declaration already visible via drmP.h:43 -> linux/io.h:32 -> asm/io.h:146. Not runtime-validated because there is no reproduction to validate against (finding is Info with no in-tree trigger); fix_status=not_testable accordingly.

findings/poc/DF-5222/fix.diff; include chain sys/dev/drm/include/drm/drmP.h:43 -> include/linux/io.h:32 -> include/asm/io.h:146.
↓ fix.diffper-fix-DF-5222

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (root-cause trace with path:line + reachability analysis); README.md (mechanism + why Info); fix.diff (one-line fix: iounmap(map->handle)). No run logs β€” PoC intentionally not executed for an Info-severity finding with no unprivileged path; guest not booted.

PoC changes

No seed PoC existed (finding produced by this pass-2 audit). PoC intentionally not authored/run: Info hardening finding, exempt from Phase V per contract; impact ceiling is root-self-harm via bespoke kmod.

Verified recommended fix

In drm_legacy_ioremapfree() replace the direct pmap_unmapdev() call with iounmap(map->handle) so the linuxkpi iomap_list bookkeeping stays balanced (see fix.diff).

Verdict

Confirmed at code level, untested at runtime by design (Info hardening; PoC exempt per contract). Exported drm_legacy_ioremapfree() (sys/dev/drm/drm_memory.c:76-83) frees device mappings with a raw pmap_unmapdev() (:81) instead of the linuxkpi iounmap(), so any kmod pairing it with drm_legacy_ioremap()/_wc() permanently orphans the struct iomap entry that __ioremap_common() registered in iomap_list (sys/dev/drm/linux_iomapping.c:50-56): leaked kernel memory + stale SLIST node naming freed KVA + skipped PAT restore (linux_iomapping.c:88-91,99,102). NOT attacker-reachable: zero in-tree callers exist (whole-tree search β€” only drm_memory.c:64-83 and drm_legacy.h:191-193); the live MMIO paths use ioremap/iounmap directly (drm_bufs.c:219-222,341,358,524) with consistent bookkeeping, and the only user-reachable ioctl in the area is DRM_IOCTL_ADD_MAP gated DRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY (drm_ioctl.c:590). Triggering requires root to load a bespoke kmod β€” root attacking itself.