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)
PoC verification
Evidence pack
findings/poc/DF-4587 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 ofobject->type/un_pager.devp.ops,vm_object_pip_addwrite into freed memory);munmap/exit runsvm_object_deallocateon freed memory ("deallocated too many times" panic or silent corruption). If the mmap held the last BO kref,ttm_bo_releasedestroys the BO whilettm_bo_handle_move_memkeeps 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).
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)
ttm_bo_mmap_single(ttm_bo_vm.c:772-814) βcdev_pager_allocate(bo, OBJT_MGTDEVICE, &ttm_pager_ops, size, nprot, 0, cred).cdev_pager_allocate(sys/vm/device_pager.c:109-168): a new object is created byvm_object_allocate_hold(tp, OFF_TO_IDX(foff+size))withref_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.vm_mmap(sys/vm/vm_mmap.c:1377) hands that object tovm_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_insertadds no reference of its own (vm_map.c:1323-1369 β the only extravm_object_reference_lockedat :1365 is the anonymous coalesce case, not this one). On mmap failurevm_mmapdeallocates 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.)ttm_bo_release_mmapdoescdev_pager_lookup(bo)β no reference taken (device_pager.c:96-106) β thenvm_object_page_remove(the good part: DFly's backing-list scan revokes the PTEs, vm_object.c:1424- 1467) and finallyvm_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)
mmap()a BO once, fault one page (ref_count == 1, the map entry's).- 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_faultdereferences the freed object (type/size at vm_fault.c:2015, pager call at :2162,vm_object_pip_addwrite insidettm_bo_vm_fault_dfly:533) β UAF; zone reuse turns this into cross-object mapping / corruption.munmap/exit instead runsvm_object_deallocateon 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 whilettm_bo_handle_move_memis still writing it (bo->mem = *mem,ttm_tt_bind, LRU ops, ttm_bo.c:327-430) β in-kernel UAF write; the earlydrm_vma_offset_remove(ttm_bo.c:663) also frees the fake offset for reuse by other BOs while the old mapping still exists. - 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.ccompiled 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 tottm_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), impactnoneon this guest; on radeon/amdgpu hardware the impact is kernel UAF / premature BO free (High).
Fix validation
fix.diff: drop the unbalancedvm_object_deallocate, addvm_object_hold/vm_object_droparound 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 --checkOK against the pristine tree.- Compile-validated on the guest: patched
ttm_bo_vm.cstaged into /usr/src anddrm.kofully 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) pluspindex >= object->size(vm_fault.c:2015) bound every fault; the DF-4458 unbounded-growth pattern does not apply to TTM becausedrm_vma_offset_lookup_lockedverifies the whole[start, start+pages)span fits inside the node (drm_vma_manager.c:163-168). - Lookup
start + pagesu64 wrap (drm_vma_manager.c:166): unreachable β the mmap offset arrives pre-shifted (vm_pgoff = *offset >> PAGE_SHIFT, ttm_bo_vm.c:788), sostart β€ 2^52-1and cannot wrap. - In-bo offset decode: mapping at fake offset
node_start + Kmaps BO pages[0,L)instead of[K,K+L)(fault uses object-relative offset, ttm_bo_vm.c:690;*offset = 0at :807) β a correctness regression vs Linux'spage_offsetcomputation (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_opsis never installed on DFly (ttm_fbdev_mmap:476-489 has no caller;amdgpu_bo_fbdev_mmapitself is dead code), andttm_bo_mmap_singleignores the opsttm_bo_mmapinstalled in the throwaway stackvma.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 unboundedgoto retryspin needs an eternally-held reservation, which unprivileged userspace cannot hold indefinitely;mancaptured beforefault_reserve_notifymirrors 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_testablefix.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'
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).
No comments yet.