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)
PoC verification
Evidence pack
findings/poc/DF-2207 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
- Confirm the types of
npagesandPAGE_SIZEat the cited path:line. - Confirm the multiplication is fed into a wider unsigned argument.
- Author
fix.diff(compute byte size once assize_t). - Phase 8 β apply all 5 batched fixes and rebuild
drm.kowith-Werror.
Source trace (confirmed)
sys/dev/drm/include/asm/io.h:114βint npages;insidestruct iomap.sys/cpu/x86_64/include/param.h:78β#define PAGE_SIZE (1<<PAGE_SHIFT). The literal1isint, soPAGE_SIZEhas typeint.sys/dev/drm/linux_iomapping.c:52βimp->npages = size / PAGE_SIZE;stores the page count into anint(narrowing).sizeitself isunsigned long(__ioremap_commonarg), 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 fornpages >= 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 intovm_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β computebyte_size = (size_t)imp->npages * PAGE_SIZE;once and use it for bothpaddr_endandpmap_unmapdev. No semantic change for any in-range mapping.build_baseline.logβ unpatcheddrm.kobuild, rc=0,-Werror.build_patched.logβ patcheddrm.korebuild, 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)
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, flagsDRM_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 subsequentDRM_IOCTL_RM_MAP(DRM_AUTH) tears it down viaiounmap(). 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
fixeddrm.ko baseline+patched rc=0 -Werror
drm.ko baseline+patched rc=0 -Werror
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.
No comments yet.