β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4587

ttm_bo_release_mmap drops an unowned OBJT_MGTDEVICE object reference on every BO revocation β€” premature vm_object termination under live mappings plus early ttm_bo_unref (kernel UAF)

Summary

release_mmap calls vm_object_deallocate after a cdev_pager_lookup that takes NO reference - the pager object's only references are the per-mmap map-entry ones (ref_count == N exactly, confirmed from the DF-1728 leak direction). The function runs on EVERY BO revocation: every VRAM-GTT move, every eviction, swapout, and every radeon dpm event for all VRAM GEM BOs. One mmap + one revocation: ref 1->0 -> vm_object_terminate -> dtor -> ttm_bo_unref while mappings are still attached - next fault derefs the freed object, munmap deallocates freed memory ('deallocated too many times' panic), and if the mmap held the last BO kref the BO is destroyed mid-move with its fake offset freed for reuse. Distinct from DF-1728/4444/1729. Video-group user, dumb buffers, no auth needed. Hold-across + delete-the-deallocate fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4587 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 8.8 KB ↓ raw
df4587.c β€” 4.9 KB view raw
build.sh β€” 105 B view raw
run.sh β€” 107 B view raw
build.log β€” 11 B view raw
run.log β€” 248 B view raw
fixbuild.log β€” 4.0 KB view raw
fix.diff β€” 1.2 KB view raw
verdict.json β€” 5.0 KB view raw

DF-4587 β€” ttm_bo_release_mmap unbalanced vm_object_deallocate β†’ premature pager-object termination under live mappings (UAF)

File: sys/dev/drm/ttm/ttm_bo_vm.c (ttm_bo_release_mmap, lines 820–853)

Build

Guest, any user:

cc -O2 -Wall -Wextra -o /tmp/df4587 /root/df4587.c    # BUILD_RC=0 expected

Run

Guest, any user:

/tmp/df4587 /dev/dri/card0

Expected

  • On radeon/amdgpu hardware (the only DFly drivers setting dev->drm_ttm_bdev, radeon_ttm.c:913 / amdgpu_ttm.c:1796): 1. 64 MiB dumb BO created in VRAM, mmap'd, one page faulted β€” the BO's OBJT_MGTDEVICE pager object now has exactly one reference, owned by the map entry (there is no handle-owned reference). 2. VRAM pressure from further dumb BOs evicts the mapped BO (VRAMβ†’GTT) β†’ ttm_bo_handle_move_mem (ttm_bo.c:318-325) β†’ ttm_bo_unmap_virtual_locked β†’ ttm_bo_release_mmap β†’ vm_object_deallocate(vm_obj) drops the mapping's reference: ref_count 1β†’0 β†’ vm_object_terminate β†’ dev_pager_dealloc β†’ ttm_bo_vm_dtor β†’ ttm_bo_unref β€” the pager object is FREED while the user's map entry still points at it, and the BO kref is dropped early. 3. Re-touching the mapping re-faults through the freed vm_object (UAF read of object->type/un_pager.devp.ops, vm_object_pip_add write into freed memory); munmap/exit runs vm_object_deallocate on freed memory ("deallocated too many times" panic or silent corruption). If the mmap held the last BO kref, ttm_bo_release destroys the BO while ttm_bo_handle_move_mem keeps writing it.
  • On the audit QEMU guest (bochs VGA 1111:1234, no /dev/dri, no drm modules): the trigger stops at the first gate β€” open("/dev/dri/card0") == ENOENT β€” exactly like DF-4458's run. The TTM code path is compile-reachable only; runtime is hardware-gated.

Fix validation

fix.diff removes the unbalanced vm_object_deallocate and holds the object across the revocation. Compile-validated on the guest by rebuilding drm.ko (which compiles ttm/ttm_bo_vm.c via .PATH) with the fix β€” see build.log. Behavioral before/after comparison impossible on this guest (no TTM-capable GPU).

VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff removes the unbalanced vm_object_deallocate, adds vm_object_hold/drop around the revocation (also closes the cdev_pager_lookup-without-ref race). Compile-validated on the guest: drm.ko fully rebuilt with the patched ttm_bo_vm.c, -Werror, DRM_BUILD_RC=0 (fixbuild.log); guest /usr/src restored pristine. Behavioral baseline/patched comparison impossible: no TTM-capable GPU on the guest (fix_status not_testable, compile-validated).

fix.diff; fixbuild.log (patched drm.ko full -Werror compile+link, DRM_BUILD_RC=0); VERDICT.md 'Fix validation'
↓ fix.diffper-fix-DF-4587

Confirmed kernel references

Detail

Exploit chain

On radeon/amdgpu hardware: open /dev/dri/card0 -> CREATE_DUMB 64MiB (VRAM) -> MAP_DUMB -> mmap -> fault one page (pager object ref=1, owned by the map entry) -> allocate further 64MiB dumb BOs until VRAM pressure evicts the mapped BO -> ttm_bo_handle_move_mem -> ttm_bo_release_mmap -> premature vm_object_terminate under the live mapping + early ttm_bo_unref -> re-touch the mapping (fault through freed vm_object: UAF; zone reuse enables cross-object page mapping) or munmap (vm_object_deallocate on freed memory). If the GEM handle was closed first, the BO itself is freed mid-move -> in-kernel UAF write.

Evidence (decisive lines)

VERDICT.md (reference-model proof: device_pager.c:109-168, vm_map.c:1614-1615/1323-1369, vm_mmap.c:1377/1485-1494; caller map ttm_bo.c:318-325/737/1830, radeon_pm.c:146-157); run.log (BUILD_RC=0, GATE open ENOENT, RUN_RC=2); fixbuild.log (drm.ko rebuilt with fix.diff, -Werror, DRM_BUILD_RC=0); fix.diff (git apply --check OK)

PoC changes

no seed; complete trigger written from scratch (DragonFly drm UAPI structs inlined), fixed missing sys/mman.h include after first guest compile

Verified recommended fix

Remove the unbalanced vm_object_deallocate from ttm_bo_release_mmap and vm_object_hold/vm_object_drop around vm_object_page_remove so the pager object dies only with its last mapping

Verdict

Code defect certain, runtime GPU-gated. ttm_bo_release_mmap (ttm_bo_vm.c:821-852) calls vm_object_deallocate(vm_obj) on every BO revocation, but cdev_pager_lookup takes no reference and the OBJT_MGTDEVICE pager object's only references are the per-mmap map-entry references (cdev_pager_allocate returns the caller's ref, vm_map_find consumes it as the entry ref; N mmaps => ref_count == N). The function runs on every VRAM/GTT transition (ttm_bo.c:318-325 via ttm_bo_handle_move_mem, evictions ttm_bo.c:737, swapout ttm_bo.c:1830) and on every radeon dpm event (radeon_pm.c:146-157, ALL VRAM GEM bos). First revocation of a once-mapped BO: ref 1->0 -> vm_object_terminate -> dev_pager_dealloc -> ttm_bo_vm_dtor -> ttm_bo_unref β€” the vm_object is freed under the live map entry (next fault derefs freed object at vm_fault.c:2015/2162 and writes vm_object_pip_add into it; munmap/exit deallocates freed memory -> 'deallocated too many times' panic or silent corruption), and if the mmap held the last BO kref the BO is destroyed while ttm_bo_handle_move_mem keeps writing it. Same defect shape as the i915 twins (DF-4444's file) but strictly worse: TTM's pager dtor drops a BO kref. Unprivileged trigger: open /dev/dri/cardX of radeon/amdgpu (the only DFly drivers setting drm_ttm_bdev), CREATE_DUMB + MAP_DUMB + mmap + allocation burst for eviction. Runtime reproduction impossible on the audit guest: no DRM device (QEMU bochs 1111:1234 VGA, no /dev/dri, no drm modules); trigger df4587.c compiled clean on guest cc 8.3 and stopped exactly at the gate (open ENOENT, run.log).