# DF-4521 VERDICT

**Status: reproduced** (drm_mm core defect, live, with the real kernel code)
**Impact: dos (demonstrated) + kernel-state corruption (demonstrated); full
i915 container_of/unbind chain hardware-gated on this guest.**

## What was run

A kld (`dfmm_poc.ko`) embedding the unmodified guest kernel source
`/usr/src/sys/dev/drm/drm_mm.c` plus the real `drm_mm_for_each_node_in_range`
macro from `/usr/src/sys/dev/drm/include/drm/drm_mm.h`, driven over a 4 GiB
allocator (`drm_mm_init(mm, 0, 1<<32)`) holding:

- `top`: reserved node `[0xfffff000, 0x100000000)` — a vma ending exactly at
  the top of the address space (`vm->total`), and
- `mid`: reserved node `[0x10000000, 0x10001000)`.

Scenario 1 reproduces, verbatim, the iteration `i915_gem_evict_for_node()`
performs for a softpinned target ending at the top of the GTT with
`check_color`: `start = target.start - 4K`, `end = target.end + 4K`
(i915_gem_evict.c:292-296), i.e. walk `[0xffffe000, 0x100001000)`.

## Result (run.log, dmesg.txt)

```
visit 1: start=fffff000 ... allocated=1          <- real node (eviction target)
visit 2: start=100000000 size=ffffffff00000000 allocated=0  <== HEAD SENTINEL
visit 3..8: same nodes repeat
RESULT: visited=8 head_sentinel_yielded=1 non_terminating=1
```

Both defects of the finding are shown by the real kernel code:

1. **Sentinel leakage**: the loop body receives `&mm->head_node`
   (`allocated=false`, `start=0x100000000 == mm end`). In i915 the body does
   `container_of(node, i915_vma, node)` (i915_gem_evict.c:318) — a fabricated
   `i915_vma*` pointing into `struct i915_address_space` — while the only
   guard, `GEM_BUG_ON(!node->allocated)` (:317), is `((void)(expr))` in
   release builds (i915_gem.h:60, linux/bug.h:45).
2. **Non-termination**: `list_next_entry(head_node)` is the first node of the
   circular `node_list`, so the walk restarts forever. In i915 this livelocks
   with `struct_mutex` held (every GPU path blocks) = reliable local DoS.

## Corruption arm (corrupt.log)

Scenario 2 performs what the i915 body eventually does with the fabricated
vma: `i915_vma_unbind()` → `i915_vma_remove()` →
`drm_mm_remove_node(&vma->node)` (i915_vma.c:687-695), i.e.
`drm_mm_remove_node(&mm->head_node)` on the yielded sentinel. Observed with
the real `drm_mm.c`:

- the kernel survives (no panic — the writes land in the embedded drm_mm),
- **interval tree wiped**: `drm_mm_interval_tree_remove()` rb-erases a node
  that was never inserted; the DFly shim's plain `RB_REMOVE`
  (include/linux/rbtree.h:101-102) sees parent==NULL/children==NULL and sets
  the tree root to NULL. Probe: a range query over `[0x10000000,0x10001000)`
  — exactly the `mid` node — returns **zero** nodes. All overlap checks that
  use `drm_mm_for_each_node_in_range` are now blind → overlapping placements
  → GTT aliasing of unrelated objects (memory-confidentiality/integrity
  impact at GPU level).
- **underflowed hole**: after `list_del(head)`, `add_hole(prev_node)`
  computes `hole_size = next->start - prev_end` across the wrap, producing a
  permanent bogus hole `[0x100000000, 0x10000000)` of size
  `0xffffffff10000000` in `holes_size`/`holes_addr`/`hole_stack`.

In the real driver the eviction-list path would additionally double-pin /
double-unbind real vmas on the wrap-around passes (list re-adds), which is
plain kernel list corruption; that arm requires the actual i915 objects and
is hardware-gated here.

## Why the i915 chain is real (source-traced, no hardware needed)

- softpin acceptance: i915_gem_execbuffer.c:461-463 (alignment/canonical
  only, no gen gate) and :614-615 (`EXEC_OBJECT_PINNED` → user offset |
  `PIN_OFFSET_FIXED`);
- fixed-offset placement: i915_gem_gtt.c:3949-3975 (`i915_gem_gtt_reserve`:
  `drm_mm_reserve_node` then `i915_gem_evict_for_node`);
- the +4K: i915_gem_evict.c:292-296 (`check_color` =
  `vm->mm.color_adjust != NULL`, set at i915_gem_gtt.c:3638-3639 for
  `!HAS_LLC && !USES_PPGTT` — e.g. gen5/gen6-era, or any no-LLC gen8+ part
  with `i915_enable_ppgtt=0`);
- nothing reserves the top page of the GGTT in this tree
  (i915_gem_gtt.c:3620-3660 has no top reservation), so a target ending at
  `vm.total` is reachable and yields `end = vm.total + 4K > head.start`.

Bottom-of-GTT is guarded (`if (start)` before `start -= 4K`), so only the
top case exists. Colors: if `target->color == 0` the body `continue`s on the
sentinel and the loop still wraps (livelock); otherwise the fake vma is
processed into the eviction list (corruption).

## Fix validation

`fix.diff` (2 hunks): (1) `drm_mm.h` — the iterator now stops at the
sentinel: `node__ != &(mm__)->head_node && node__->start < (end__)`;
(2) `i915_gem_evict.c` — clamp `end` to `vm->total` after the guard-page
expansion. Applied to the guest's `/usr/src/sys`, kld rebuilt from the same
real sources, same PoC re-run:

```
run.fixed.log: RESULT: visited=1 head_sentinel_yielded=0 non_terminating=0
```

Baseline bad behavior GONE; in-range walks are unaffected (the sentinel stop
coincides with the old `head.start >= end` stop for all in-range queries).

## Honest limits

- No Intel GPU on the guest (`vgapci0: chip=0x11111234`, Bochs), so the
  end-to-end unprivileged-ioctl trigger could not be executed; the drm_mm
  primitives (sentinel yield, non-termination, tree wipe, underflow hole)
  were executed live with the real kernel code, and every i915 hop is
  cited to source lines above.
- No panic occurred; `impact=dos` reflects the demonstrated livelock plus
  demonstrated kernel-state corruption (see corrupt.log for the latter).
- Behavior is inherited from Linux-4.12-era code (line-identical upstream of
  that era); the finding stands for this tree regardless.

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