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)
PoC verification
Evidence pack
findings/poc/DF-5222 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_listunderiomap_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:
- the
struct iomapentry is leaked fromiomap_listforever (kernel-memory leak + stale SLIST node whosepmap_addrpoints at freed KVA), - the PAT-attribute restore for RAM-backed mappings (linux_iomapping.c:88-91) never happens,
- the device mapping's KVA is returned to
kernel_mapwhile 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.
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 β linuxkpiioremap()(asm/io.h:129) β__ioremap_common()β sys/dev/drm/linux_iomapping.c:41-59.__ioremap_commonkmallocsstruct iomap(:50), maps viapmap_mapdev_attr()(:53), inserts into the globaliomap_listunderiomap_lock(:54-56).drm_legacy_ioremapfree()β sys/dev/drm/drm_memory.c:76-83 β callspmap_unmapdev((vm_offset_t)map->handle, map->size)at :81, bypassingiounmap()(linux_iomapping.c:61-103), which is the ONLY code that removes the list entry (:99), frees thestruct 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)
- 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. - The live MMIO paths don't use them.
drm_addmap_core()maps registers withioremap()/ioremap_wc()directly (drm_bufs.c:219-222) and unwinds withiounmap()(drm_bufs.c:341,358);drm_legacy_rmmap()frees withiounmap()(drm_bufs.c:524) β bookkeeping-consistent. - The only user-reachable ioctl in the area is root-gated.
DRM_IOCTL_ADD_MAPβdrm_legacy_addmap_ioctlis registeredDRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY(drm_ioctl.c:590). - Triggering the mismatch therefore requires root to write and load a kmod
calling the wrappers β root attacking itself.
pmap_unmapdevitself 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_commonpanics viaBUG_ONon 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-branchagp_remapstub (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_testablefix.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.
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.
No comments yet.