# DF-4444 — DF-specific `drm_vma_node_unmap` is a structural no-op: user CPU PTEs mapping the GGTT aperture are never revoked on VMA unbind/evict → stale mappings survive into reallocated aperture space (cross-object / cross-user GPU memory read+write)

File: `sys/dev/drm/i915/i915_vma.c:857-890` (called at `i915_vma.c:906-907`).

## Verdict

**Status: inconclusive at runtime (guest has no Intel GPU — the only VGA device is
QEMU virtio-gpu 0x1234:1111; `i915.ko` exists in `/boot/kernel` but can never
attach). The static proof below is certain: the revocation hook is mechanically
a no-op, and it is the *only* revocation mechanism in the entire DF drm tree.**

Severity (static): **High** — unprivileged local user with DRI device access
keeps valid CPU PTEs into the physical GTT aperture window after the owning
VMA is unbound; when the aperture range is re-allocated to another object
(other process / the X server's framebuffers), the stale PTE reads and writes
*that* object's memory. Confidentiality + integrity break between GPU clients,
up to cross-user. Requires Intel graphics hardware (all i915 generations that
support `I915_MMAP_GTT`, i.e. all of them) and GGTT pressure to evict — both
standard on any real desktop using i915.

## Root cause, line by line

Upstream (Linux v4.19, `i915_vma.c`) revokes user mappings with
`unmap_mapping_range(drm.anon_inode->i_mapping, offset, vma->size, 1)` —
a pmap-level zap of every user PTE mapping the VMA's GGTT window.
DragonFly replaced this with a local `drm_vma_node_unmap`
(`sys/dev/drm/i915/i915_vma.c:857-890`) whose every step is inert:

1. `devobj = cdev_pager_lookup(obj)` — returns the `OBJT_MGTDEVICE`
   vm_object created at mmap time (`sys/dev/drm/drm_gem.c:1099`), with a
   reference taken (`sys/vm/vm_pager.c:359-374` `vm_pager_object_lookup`
   does `vm_object_reference_locked`). Fine so far.

2. `vm_object_page_remove(devobj, 0, 0, false)` — iterates only pages
   **resident in the object's rb-tree**. For `OBJT_MGTDEVICE`, pages are
   never inserted into the object: `sys/dev/drm/i915/i915_gem.c:57`
   `#undef USE_INSERT`, the `vm_page_insert()` path at
   `i915_gem.c:2405-2425` is `#if 0` ("NO LONGER USED"), and
   `sys/vm/vm_fault.c:2027-2033` documents the design:
   *"For OBJT_MGTDEVICE the pages are not indexed in the VM object at all
   but instead directly entered into the pmap"* (`vm_fault.c:2170-2174`
   returns the busied fictitious page straight to the caller for
   `pmap_enter`). ⇒ **no resident pages → nothing removed.**

3. `for (i = 0; i < page_count; i++) vm_page_lookup_busy_wait(devobj, i, ...)`
   — same object-rb-tree lookup of the same nonexistent pages ⇒ **all NULL,
   `continue` every iteration. Dead loop.** (Even if a page were found,
   `cdev_pager_free_page` (`sys/vm/device_pager.c:184-194`) only removes the
   page from the object; it does not walk user pmaps — although its
   `pmap_page_protect(m, VM_PROT_NONE)` call would incidentally do the right
   thing *if* the page were ever in the object.)

4. `vm_object_deallocate(devobj)` — only drops the reference taken by (1).

Net effect: the function removes **zero pmap entries**, while the caller
`i915_vma_revoke_mmap` proceeds to `i915_vma_unset_userfault(vma)` and
decrements `obj->userfault_count` (`i915_vma.c:915-917`) — the kernel now
*believes* user mappings were revoked. Grep proof of absence of any other
revocation: `rg 'pmap_remove|pmap_qremove|pmap_page_protect' sys/dev/drm/`
returns **zero** hits in i915/*, drm core, or linux_vm shims.

## Attack chain (requires real Intel GPU)

1. Unprivileged user opens `/dev/dri/renderD128` (no DRM auth needed for
   render-node GEM), creates object A, `I915_MMAP_GTT`s it and touches it:
   `i915_gem_fault` (`i915_gem.c:2229`) pins A `PIN_MAPPABLE` into the GGTT,
   hands out fictitious aperture pages (`i915_gem.c:2418-2428`), and marks
   the VMA `USERFAULT` (`i915_gem.c:2440`). CPU PTEs → physical aperture
   window `gmadr + A.node.start`.
2. User creates GGTT pressure (GTT-maps ~mappable_end worth of 1MiB objects).
   The MM evicts the inactive A: `i915_gem_evict_*` →
   `i915_vma_unbind` (`i915_vma.c:1071`) → `i915_vma_revoke_mmap`
   (`i915_vma.c:1146`) → no-op unmap (above). A's GTT PTEs cleared, A's drm_mm
   node freed, but the attacker's CPU PTEs remain valid.
3. Victim object V (any other client's — e.g. root's scanout framebuffer,
   another user's GTT-mapped buffer) is bound; the drm_mm allocator reuses
   A's freed range for it (attacker shapes size/alignment to make reuse
   deterministic).
4. Attacker reads A's stale mapping → V's contents; writes → V's memory.
   No fault ever occurs (PTE valid), so no revalidation happens.

Secondary reachable revoke sites with the same no-op: fence-steal
`i915_gem_fence_reg.c:248` (tiling/fence revocation), GPU-reset
`i915_gem_revoke_fences` `i915_gem_fence_reg.c:459`, and the object-free
twin `__i915_gem_object_release_mmap` (`i915_gem.c:2149-2165`, identical
DF no-op → stale mappings even survive object free until munmap/exit).

## What was and was not verified

- VERIFIED (static, certain): every step above, cited to lines; full diff
  against Linux v4.19 `i915_vma.c` shows this `#ifdef __DragonFly__` block is
  the only functional divergence in the file (135-line diff, mechanical
  elsewhere).
- VERIFIED (guest): trigger compiles against DF userland (`build.log`) and
  runs to its exact hardware gate (`run.log`: "no DRI device"; guest PCI
  shows only `vgapci0 chip=0x11111234` virtio-gpu, `ls /dev/dri` → ENOENT,
  `env.txt`).
- NOT verified (impossible on this guest): the runtime cross-object
  read/write on real Intel hardware.

## Fix

See `fix.diff`: zap the VMA's aperture window at pmap level using the same
fictitious-page + `pmap_page_protect()` idiom `cdev_pager_free_page` itself
uses (`device_pager.c:188`). Also applies (analogously, per-GGTT-VMA walk of
`obj->vma_list`) to the twin in `i915_gem.c:2149`.

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