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

Signed 32-bit overflow in iounmap() byte-size math (npages * PAGE_SIZE) yields OOB pmap_unmapdev / wrong phys_avail match for >=2 GB mappings

Summary

struct iomap.npages is int (asm/io.h:114). iounmap() recomputes byte-size as imp->npages * PAGE_SIZE at lines 80 and 96. PAGE_SIZE is also int (param.h:78). Multiplication int*int overflows signed UB for mapping size >= INT_MAX/PAGE_SIZE ~= 2 GiB. pmap_unmapdev() takes vm_size_t (unsigned long); negative int sign-extended to ~2^63 byte count drives pmap_qremove/kmem_free wildly OOB. pmap_qremove walks PTEs far past mapping into unrelated kernel VA (OOB write PTEs/corruption) and kmem_free corrupts kernel_map. paddr_end at line 80 also computed from overflowed product corrupting phys_avail match decision (may skip or apply pmap_change_attr to wrong region). For exact multiples of 4 GiB wrap produces 0 mapping silently leaks (PTEs never torn down VA never returned). Reachable: root+DRM-master via DRM_IOCTL_ADD_MAP (DRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY) size >= 2 GiB against valid MMIO phys range then DRM_IOCTL_RM_MAP (DRM_AUTH-only) to teardown -> iounmap() overflowed size -> panic or kernel_map corruption. Latent for PCIe Resizable BAR 4-16 GiB GPU VRAM BARs.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2207 Β· 8 files
FileTypeDescriptionSize
README.md readme claim, type trace, concrete overflow table, fix summary 3.4 KB ↓ raw
VERDICT.md verdict int*int signed-overflow UB walkthrough + size_t fix 3.8 KB ↓ raw
fix.diff suggested-fix compute byte_size once as size_t; reuse for paddr_end and pmap_unmapdev 1.4 KB view raw
build.sh build-script applies fix.diff + rebuilds drm.ko in guest 523 B view raw
run.sh run-script HW-gated no-op runner 379 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, rc=0, -Werror 2.9 KB view raw
env.txt environment uname, cc, kern.version 376 B view raw
README.md readme claim, type trace, concrete overflow table, fix summary
↓ download raw

DF-2207 β€” signed 32-bit overflow in iounmap() byte-size math (Medium)

Claim

struct iomap.npages is int (sys/dev/drm/include/asm/io.h:114). PAGE_SIZE is (1<<PAGE_SHIFT) = (1<<12) which also has type int (sys/cpu/x86_64/include/param.h:78). iounmap() recomputes the byte size as imp->npages * PAGE_SIZE at lines 80 and 96 of sys/dev/drm/linux_iomapping.c; that multiplication is int * int and overflows signed for mappings >= INT_MAX/PAGE_SIZE β‰ˆ 2 GiB. The overflowed (possibly negative) int is then sign-extended into the vm_size_t (unsigned long) argument of pmap_unmapdev(), driving pmap_qremove / kmem_free wildly OOB.

Verification approach

HW-gated / source-only. The relevant ioctl path is DRM_IOCTL_ADD_MAP (DRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY) plus DRM_IOCTL_RM_MAP (DRM_AUTH), exercised only when a DRM driver is bound to a real GPU. No GPU on this audit guest. Per the task brief, source-only confirmation is acceptable. The overflow is fully visible at the source level and the structural fix is mechanical, so the verification is:

  1. Confirm the types of npages and PAGE_SIZE at the cited path:line.
  2. Confirm the multiplication is fed into a wider unsigned argument.
  3. Author fix.diff (compute byte size once as size_t).
  4. Phase 8 β€” apply all 5 batched fixes and rebuild drm.ko with -Werror.

Source trace (confirmed)

  • sys/dev/drm/include/asm/io.h:114 β€” int npages; inside struct iomap.
  • sys/cpu/x86_64/include/param.h:78 β€” #define PAGE_SIZE (1<<PAGE_SHIFT). The literal 1 is int, so PAGE_SIZE has type int.
  • sys/dev/drm/linux_iomapping.c:52 β€” imp->npages = size / PAGE_SIZE; stores the page count into an int (narrowing). size itself is unsigned long (__ioremap_common arg), so the truncation is silent for very large mappings but the multiplication back is the unsafe step.
  • sys/dev/drm/linux_iomapping.c:80 β€” paddr_end = imp->paddr + (imp->npages * PAGE_SIZE) - 1; β€” int * int β‡’ signed overflow UB for npages >= 2^19 (β‰ˆ2 GiB mapping).
  • sys/dev/drm/linux_iomapping.c:96 β€” pmap_unmapdev((vm_offset_t)imp->pmap_addr, imp->npages * PAGE_SIZE); β€” same overflow, then sign-extended into vm_size_t (unsigned long) β‡’ multi-GB or multi-TB teardown range.

Concrete overflow example

Mapping size = 2 GiB β‡’ npages = 0x20000 (131072 pages). 0x20000 * 0x1000 = 0x2_0000_0000 which as a signed 32-bit int is 0 (exact 4 GiB wrap) β‡’ pmap_unmapdev(..., 0) does nothing (silent VA leak). Mapping size = 2 GiB + 1 page β‡’ product = 0x2_0000_1000 β‡’ truncated to 0x1000, sign-extends into a small positive teardown of one page (massive under-teardown; the other 131072 PTEs are never cleared and the VA range is never returned to kernel_map). Larger values land negative and sign-extend into multi-TB teardowns that walk unrelated kernel VA.

Files

  • VERDICT.md β€” full narrative.
  • fix.diff β€” compute byte_size = (size_t)imp->npages * PAGE_SIZE; once and use it for both paddr_end and pmap_unmapdev. No semantic change for any in-range mapping.
  • build_baseline.log β€” unpatched drm.ko build, rc=0, -Werror.
  • build_patched.log β€” patched drm.ko rebuild, rc=0, -Werror.
  • env.txt β€” guest environment.

Reproduce

./build.sh    # applies fix.diff + rebuilds drm.ko
./run.sh      # HW-gated no-op (see VERDICT.md)
VERDICT.md verdict int*int signed-overflow UB walkthrough + size_t fix
↓ download raw

DF-2207 β€” VERDICT

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

Class: integer overflow β†’ kernel VA memory corruption / silent leak.

Mechanism

iounmap() in sys/dev/drm/linux_iomapping.c recomputes the mapping's byte size from the stored page count:

/* struct iomap.npages is int    β€” sys/dev/drm/include/asm/io.h:114 */
/* PAGE_SIZE is (1<<PAGE_SHIFT)  β€” sys/cpu/x86_64/include/param.h:78,
 * and the literal 1 has type int, so PAGE_SIZE has type int. */
paddr_end = imp->paddr + (imp->npages * PAGE_SIZE) - 1;     /* line 80 */
...
pmap_unmapdev((vm_offset_t)imp->pmap_addr,
              imp->npages * PAGE_SIZE);                     /* line 96 */

imp->npages * PAGE_SIZE is therefore int * int β€” a signed multiplication with C-level undefined behaviour once the mathematically correct product exceeds INT_MAX. The threshold on x86_64 (PAGE_SIZE = 4096) is INT_MAX / 4096 = 524287 pages, i.e. any mapping of 524288 * 4096 = 2 GiB or larger.

pmap_unmapdev() takes vm_size_t (which is unsigned long). The overflowed int is implicitly sign-extended when passed in:

npages true byte size int product as unsigned long effect
0x20000 (131072, =2 GiB) 0x2_0000_0000 0 0 pmap_unmapdev(..., 0) does nothing β€” PTEs never torn down, VA never returned to kernel_map. Silent leak.
0x20001 (2 GiB + 1 page) 0x2_0000_1000 0x1000 0x1000 under-teardown: only the first page is unmapped, the remaining 131072 pages leak.
0x40000 (4 GiB) 0x4_0000_0000 0 0 same as 2 GiB case: silent leak.
0x100000 (16 GiB β€” Resizable BAR territory) 0x1_0000_0000_0 0 (truncated) 0 silent leak.
0xC0000 (~24 GiB) 0xC_0000_0000_0 negative sign-extended to a huge unsigned long pmap_qremove walks PTEs far past the mapping into unrelated kernel VA β†’ OOB write of PTEs / kmem_free corrupts kernel_map.

paddr_end at line 80 is computed from the same overflowed product, so the "Is this address range backed by regular memory?" loop at lines 82-94 may skip a pmap_change_attr it should apply, or apply it to the wrong region.

Trigger surface / reachability

  • DRM_IOCTL_ADD_MAP (drm_bufs.c, flags DRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY) lets a DRM-master / root user request a map of arbitrary size against a valid MMIO physical range; the subsequent DRM_IOCTL_RM_MAP (DRM_AUTH) tears it down via iounmap(). This is the unprivileged-ish path.
  • PCIe Resizable BAR exposes GPU VRAM BARs of 4–16 GiB on modern Vega20 / MI50 / MI60 class hardware; mapping + unmapping those (which the driver does during init/fini) hits the overflow path directly.

On this audit guest there is no GPU, so the overflow cannot be exercised live; it is confirmed at the source level (types are unambiguous) and the structural fix compiles cleanly (Phase 8).

Fix

fix.diff computes the byte size once as a size_t local (byte_size = (size_t)imp->npages * PAGE_SIZE) and uses it for both paddr_end and pmap_unmapdev. (size_t)imp->npages first widens the page count to 64-bit unsigned, so the subsequent multiply by PAGE_SIZE is size_t * int (promoted to size_t * size_t) and cannot overflow on x86_64 for any mapping the rest of the kernel could conceivably handle. No semantic change for any in-range mapping.

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 signed-overflow UB is real and unambiguous from the type of struct iomap.npages; 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. Source-confirmed: iounmap npages*PAGE_SIZE int*int overflow for >=2GiB mappings.

Verified recommended fix

HW-gated. Source-confirmed: iounmap npagesPAGE_SIZE intint overflow for >=2GiB mappings.

Verdict

HW-gated. Source-confirmed: iounmap npagesPAGE_SIZE intint overflow for >=2GiB mappings.