# DF-2206 — VERDICT

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

**Class:** kernel race / UAF / double-free / NULL deref (memory corruption).

## Mechanism

`iounmap()` in `sys/dev/drm/linux_iomapping.c` tears down a previously
`ioremap()`-ed MMIO mapping.  The list of live mappings (`iomap_list`,
declared at `linux_iomapping.c:39`) is a **process-global** singly-linked
SLIST shared by **every** DRM driver in the system; it is *not* per-device.
The lookup of the matching entry is performed by `SLIST_FOREACH_MUTABLE`
without holding the list lock:

```c
/* linux_iomapping.c:68 */
SLIST_FOREACH_MUTABLE(imp, &iomap_list, im_iomaps, tmp_imp) {
    if (imp->pmap_addr == ptr) { found = 1; break; }
}
```

After the unlocked lookup succeeds, the code uses `imp` (and computes
`paddr_end` from `imp->npages * PAGE_SIZE`) at line 80, optionally calls
`pmap_change_attr` at line 90, calls `pmap_unmapdev` at line 96 — and only
*then*, at lines 98-100, acquires `iomap_lock` to do `SLIST_REMOVE`, releases
the lock, and `kfree(imp)` at line 102.

Because the list is global, per-device `dev->struct_mutex` does **not**
serialize cross-device `iounmap()`.  Two concurrent callers therefore race
in the window between the unlocked lookup (line 68) and the locked remove
(line 99):

| Outcome | How |
|---|---|
| UAF read / wild walk | caller A's `tmp_imp = SLIST_NEXT(imp)` saved by `SLIST_FOREACH_MUTABLE` points at an entry caller B already `kfree()`-ed. |
| double free of `kernel_map` VA | both callers pass the unlocked `found` check for the *same* `pmap_addr`, both call `pmap_unmapdev` on that VA. |
| NULL deref panic | second `SLIST_REMOVE` for the same handle walks `SLIST_NEXT(NULL)` past end of list. |

Each of these is a kernel memory-corruption primitive (slab-groommable to
arbitrary kernel write in principle; unambiguous panic DoS in practice).

## Trigger surface / reachability

`iounmap()` is reached from every DRM driver's MMIO teardown path
(`amdgpu_device_fini`, `radeon`/`i915` fini, etc.) and from
`DRM_IOCTL_RM_MAP` (`drm_bufs.c`) which is `DRM_AUTH`-only.  Two
concurrent teardowns (driver reset + ioctl, or cross-device) race.

On **this audit guest** there is no GPU, so the race cannot be exercised
live; it is confirmed at the source level.  The structural fix is mechanical
and the patched module compiles cleanly (Phase 8).

## Fix

`fix.diff` holds `iomap_lock` from function entry through the final
`kfree`.  `iomap_lock` is initialised `LK_CANRECURSE`
(`linux_iomapping.c:37`) and none of the helpers called during teardown
(`pmap_unmapdev`, `pmap_change_attr`, `kfree`) acquire `iomap_lock`, so
holding it across the teardown cannot deadlock.  An early-return path
(`!found`) is updated to release the lock before returning.

## 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 race is real, the primitive is memory corruption, and the fix compiles
cleanly under `-Werror`.
