# DF-4587 — VERDICT

**Finding:** `ttm_bo_release_mmap()` (sys/dev/drm/ttm/ttm_bo_vm.c:820-853)
calls `vm_object_deallocate(vm_obj)` on an OBJT_MGTDEVICE pager object
whose reference it does not own, on **every** BO revocation. The pager
object's only references are the per-mmap references owned by user vm_map
entries, so the first revocation of a once-mapped BO terminates the object
while the mapping is live: the vm_object is freed under a live map entry
(fault/unmap UAF) and `ttm_bo_vm_dtor` → `ttm_bo_unref` fires early
(BO destroyed mid-move when the mmap held the last kref).

**Status:** code defect certain (statically proven end-to-end); runtime
inconclusive on the audit guest — the TTM mmap path is hardware-gated.

## The reference model (proven from source)

1. `ttm_bo_mmap_single` (ttm_bo_vm.c:772-814) → `cdev_pager_allocate(bo,
   OBJT_MGTDEVICE, &ttm_pager_ops, size, nprot, 0, cred)`.
2. `cdev_pager_allocate` (sys/vm/device_pager.c:109-168): a *new* object
   is created by `vm_object_allocate_hold(tp, OFF_TO_IDX(foff+size))` with
   `ref_count == 1` — **there is no additional handle-owned reference**;
   the dev_pager_object_list membership is not a reference. For an
   existing object it adds exactly **+1 for the caller**.
3. `vm_mmap` (sys/vm/vm_mmap.c:1377) hands that object to `vm_map_find`,
   whose contract is *"ref count must be bumped by caller prior to making
   call to account for the new entry"* (sys/vm/vm_map.c:1614-1615);
   `vm_map_insert` adds no reference of its own (vm_map.c:1323-1369 —
   the only extra `vm_object_reference_locked` at :1365 is the anonymous
   coalesce case, not this one). On mmap failure `vm_mmap` deallocates
   the caller's ref itself (vm_mmap.c:1485-1494).
   ⇒ **N live mmaps ⇒ object ref_count == N. Nothing else holds a ref.**
   (DF-1728's "dtor fires exactly once per vm_obj lifetime; N mmaps leak
   N-1 BO krefs" confirms the same model from the leak direction.)
4. `ttm_bo_release_mmap` does `cdev_pager_lookup(bo)` — **no reference
   taken** (device_pager.c:96-106) — then `vm_object_page_remove` (the
   good part: DFly's backing-list scan revokes the PTEs, vm_object.c:1424-
   1467) and finally **`vm_object_deallocate(vm_obj)`** (ttm_bo_vm.c:851).

## Who calls it (attack surface)

`ttm_bo_unmap_virtual[_locked]` → `ttm_bo_release_mmap` is invoked:

* `ttm_bo_handle_move_mem` (ttm_bo.c:307-325) — on **every** BO placement
  change with a PCI/caching transition: all VRAM↔GTT moves, i.e. every
  eviction (`ttm_bo_evict` → :737), every validate that changes domain,
  and the swapout restore (ttm_bo.c:1817). Triggered by ordinary VRAM
  pressure: any local user with /dev/dri/cardX open allocates dumb/GEM
  buffers until the mapped BO is evicted.
* `ttm_bo_swapout` (ttm_bo.c:1830) — memory-pressure swap daemon.
* `radeon_unmap_vram_bos` (radeon_pm.c:146-157) — **every radeon dpm
  power-state transition unmaps ALL VRAM GEM objects.**
* `ttm_mem_io_evict` (ttm_bo_util.c:115-128), `radeon_object.c:641`.

The same defect shape exists in the i915 DF twins
(i915_gem.c:2152-2166, i915_vma.c:863-889) — that is DF-4444's file; this
finding is the TTM instance, which is strictly worse because the pager
dtor (`ttm_bo_vm_dtor`, ttm_bo_vm.c:748-754) drops a **BO kref**.

## Consequence chain (radeon/amdgpu hardware)

1. `mmap()` a BO once, fault one page (ref_count == 1, the map entry's).
2. First eviction/move/dpm event → `vm_object_deallocate` → 1→0 →
   `vm_object_terminate` (vm_object.c:745-863, frees at final drop) →
   `dev_pager_dealloc` → `cdev_pg_dtor` → `ttm_bo_unref(&bo)`:
   * GEM handle still open: BO survives, but the **vm_object is freed
     under the live map entry**. Next fault: `vm_fault` dereferences the
     freed object (type/size at vm_fault.c:2015, pager call at :2162,
     `vm_object_pip_add` write inside `ttm_bo_vm_fault_dfly`:533) — UAF;
     zone reuse turns this into cross-object mapping / corruption.
     `munmap`/exit instead runs `vm_object_deallocate` on freed memory →
     "deallocated too many times" panic (vm_object.c:705-708) or silent
     refcount corruption.
   * GEM handle already closed (mmap holds the last kref): the BO is
     **destroyed while `ttm_bo_handle_move_mem` is still writing it**
     (`bo->mem = *mem`, `ttm_tt_bind`, LRU ops, ttm_bo.c:327-430) —
     in-kernel UAF write; the early `drm_vma_offset_remove`
     (ttm_bo.c:663) also frees the fake offset for reuse by other BOs
     while the old mapping still exists.
3. Multiple mmaps only postpone it: each revocation eats one mapping's
   reference; R ≥ N revocations kill the object early, R < N leave the
   remaining entries with fewer refs than entries (last unmap of *one*
   entry terminates the object under the others).

Unprivileged preconditions: open `/dev/dri/cardX` of a TTM driver
(radeon/amdgpu only on DFly — radeon_ttm.c:913, amdgpu_ttm.c:1796),
CREATE_DUMB + MAP_DUMB + mmap + a second allocation burst. No DRM_AUTH,
no master. (Note: radeon's `verify_access` is additionally stubbed out —
radeon_ttm.c:234-244 — DF-1633 context; not this file's defect.)

## Runtime on the audit guest — hardware gate

* Guest: DragonFly 6.5-DEVELOPMENT, QEMU bochs VGA `1111:1234`,
  **no /dev/dri, no drm.ko/radeon/amdgpu loaded** (run.log).
* `df4587.c` compiled clean (`BUILD_RC=0`, build.log) and ran to the
  gate: `open("/dev/dri/card0") == ENOENT`, RUN_RC=2 (run.log).
* With no TTM device, `drm_mmap_single` (drm_drv.c:1266-1273) never
  dispatches to `ttm_bo_mmap_single`; forcing drm.ko to load without
  hardware wedges the loader (DF-4458 precedent) — not repeated.
* ⇒ status `inconclusive` (code certain, runtime GPU-gated), impact
  `none` *on this guest*; on radeon/amdgpu hardware the impact is
  kernel UAF / premature BO free (High).

## Fix validation

* `fix.diff`: drop the unbalanced `vm_object_deallocate`, add
  `vm_object_hold`/`vm_object_drop` around the revocation (also closes
  the cdev_pager_lookup-without-ref race against a concurrent last
  unmap). PTE revocation itself (`vm_object_page_remove`) is untouched —
  mappings stay valid through BO moves and simply re-fault, which is the
  intended TTM semantic; the object now dies only with its last mapping,
  whose dtor releases the mmap's BO kref.
* `git apply --check` OK against the pristine tree.
* Compile-validated on the guest: patched `ttm_bo_vm.c` staged into
  /usr/src and `drm.ko` fully rebuilt with `-Werror`, `DRM_BUILD_RC=0`
  (fixbuild.log); guest source restored pristine afterwards
  (md5 e1bd78ad3b804a05207e0eebb4447a66 == repo).
* Behavioral baseline/patched comparison impossible on this guest
  (`fix_status: not_testable`).

## Negative results (classes hunted and killed in this file, pass 2)

* Fault bound: `OFF_TO_IDX(offset) >= bo->num_pages` (ttm_bo_vm.c:632)
  plus `pindex >= object->size` (vm_fault.c:2015) bound every fault;
  the DF-4458 unbounded-growth pattern does **not** apply to TTM because
  `drm_vma_offset_lookup_locked` verifies the whole `[start, start+pages)`
  span fits inside the node (drm_vma_manager.c:163-168).
* Lookup `start + pages` u64 wrap (drm_vma_manager.c:166): unreachable —
  the mmap offset arrives pre-shifted (`vm_pgoff = *offset >> PAGE_SHIFT`,
  ttm_bo_vm.c:788), so `start ≤ 2^52-1` and cannot wrap.
* In-bo offset decode: mapping at fake offset `node_start + K` maps BO
  pages `[0,L)` instead of `[K,K+L)` (fault uses object-relative offset,
  ttm_bo_vm.c:690; `*offset = 0` at :807) — a correctness regression vs
  Linux's `page_offset` computation (here #if 0 at :211-214), but no
  security boundary is crossed (vma-node access is per-BO per-filp, so a
  filp granted the node can map any window anyway). Not filed.
* `panic("ttm_bo_vm_fault")` (ttm_bo_vm.c:113): unreachable —
  `ttm_bo_vm_ops` is never installed on DFly (`ttm_fbdev_mmap`:476-489
  has no caller; `amdgpu_bo_fbdev_mmap` itself is dead code), and
  `ttm_bo_mmap_single` ignores the ops `ttm_bo_mmap` installed in the
  throwaway stack `vma`.
* `ttm_bo_vm_access`/`_kmap` (ttm_bo_vm.c:324-399): dead on DFly (no vma
  access_remote_vm machinery); its `(offset+len)` overflow at :370 is
  unreachable.
* Fault-reserve locking: `ttm_bo_reserve(bo, true, true, NULL)` trylock +
  bounded retry (100 × 1-tick sleep, ttm_bo_vm.c:608-619) — an unbounded
  `goto retry` spin needs an eternally-held reservation, which unprivileged
  userspace cannot hold indefinitely; `man` captured before
  `fault_reserve_notify` mirrors upstream Linux.
* `tsleep(&dummy,...)` stack wait-channel (ttm_bo_vm.c:616): timed sleep
  only, no external waker — harmless.

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