# 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:

```c
/* 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`.
