DF-4510 / verdict.json
{ "finding_id": "DF-4510", "status": "inconclusive", "reproduced": 0, "impact": "none", "confidence": "likely", "verdict": "Race-certain by construction, untestable on this guest. drm_syncobj_fence_get() (drm_syncobj.h:120-130) loads syncobj->fence and takes a reference via dma_fence_get_rcu_safe() (dma-fence.h:189-199) which on this port is a bare load + unconditional kref_get, under rcu_read_lock() that is only preempt_disable() (rcupdate.h:46-56). drm_syncobj_replace_fence() (drm_syncobj.c:170-196) swaps the pointer under syncobj->lock and then dma_fence_put()s the old fence OUTSIDE the lock (:195); for a syncobj holding only a SIGNAL-created stub fence that put is the last reference and frees synchronously via dma_fence_release->kfree (linux_fence.c:43-51; stub ops have no ->release). A concurrent waiter on another CPU that loaded the pointer before the swap executes kref_get on freed memory (UAF write) and then uses the freed fence \u2014 dma_fence_is_signaled dispatches fence->ops and dma_fence_add_callback writes its cb_list \u2014 a heap UAF (read+write) type-confusable through slab grooming; all syncobj ioctls are DRM_UNLOCKED|DRM_RENDER_ALLOW and i915/amdgpu set DRIVER_SYNCOBJ, so this is unprivileged on real desktop hardware. Could not be exercised: the guest has no DRM device at all (only QEMU virtio-gpu hardware, unsupported by this port; no /dev/dri \u2014 run.log), so the racing ioctls cannot be issued; trigger program builds clean on the guest (build.log) and runs unmodified on any i915/amdgpu box with 2+ cores. Narrow load->inc window makes practical exploitation likely rather than certain, but the window is attacker-repeatable indefinitely at ioctl speed. Same no-op-RCU family as DF-4329/DF-1881/DF-4506, distinct file and object; unrelated to the known DF-1666 fd leak on this file.", "exploit_chain": "unpriv render-node fd -> SYNCOBJ_CREATE(SIGNALED) once -> thread A: SYNCOBJ_SIGNAL loop (each call frees the previous stub fence via replace_fences unlocked put) while thread B: SYNCOBJ_WAIT(0) loop (fence_get load+inc) -> B interleaves load/kfree/inc -> kref_get on freed kmalloc slot + use of freed dma_fence -> groom the freed bucket with controlled data -> fake dma_fence ops pointer -> dma_fence_is_signaled/fence_cb list write primitives -> code exec on this unhardened target", "evidence": "VERDICT.md (line-accurate race proof + exploitation sketch); syncobj_race.c + run.log (builds clean; device-absent diagnosis on guest); fix.diff (git-apply clean; two halves: kref_get_unless_zero+retry and kfree_rcu deferred fence free)", "kernel_refs": [ "sys/dev/drm/drm_syncobj.c:170-216", "sys/dev/drm/drm_syncobj.c:195", "sys/dev/drm/drm_syncobj.c:242", "sys/dev/drm/drm_syncobj.c:715", "sys/dev/drm/include/drm/drm_syncobj.h:120-130", "sys/dev/drm/include/linux/dma-fence.h:97-110", "sys/dev/drm/include/linux/dma-fence.h:189-199", "sys/dev/drm/include/linux/rcupdate.h:46-56", "sys/dev/drm/linux_fence.c:43-51", "sys/dev/drm/drm_ioctl.c:685-698" ], "poc_changes": "authored fresh: self-contained uapi structs + ioctl numbers taken from sys/dev/drm/include/uapi/drm/drm.h (0xBF/0xC3/0xC5), two racing pthreads, device-absent diagnosis path", "attempts": 1, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 15, "guest_dirty": 0, "build_cmd": "cc -O2 -pthread -o /tmp/syncobj_race /root/poc/DF-4510/syncobj_race.c", "run_cmd": "/tmp/syncobj_race 5", "code_hash": "ccbb9e85f3d5446fb315e9542d4025f07bd7bebbfd23a2370fbb5fafb267e3b4", "fix_status": "not_testable", "fix_baseline_reproduced": 0, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff (kref_get_unless_zero + retry in dma_fence_get_rcu/_rcu_safe, deferred kfree_rcu(fence, rcu) in dma_fence_release/dma_fence_free \u2014 the ports __kfree_rcu delay-line gives >=1s grace, ample for the few-instruction window) is git-apply clean against sys/; baseline/patched comparison impossible on the guest because no DRM device exists to run the trigger against a patched kernel.", "fix_evidence": "git apply --check findings/poc/DF-4510/fix.diff -> OK; kref_get_unless_zero verified present in sys/dev/drm/include/linux/kref.h:85; struct dma_fence.rcu member verified in sys/dev/drm/include/linux/dma-fence.h:45; __kfree_rcu delay-line verified in sys/dev/drm/linux_rcu.c:80-110,162-184", "notes": "Cross-references (family, not duplicates): DF-4329 (i915_request RCU no-op), DF-1881 (reservation dma_fence_get_rcu unconditionally kref_get), DF-4506 (linux_idr grow kfree), DF-4362 (fd shim leak \u2014 different bug on this file). Known and NOT re-reported on this file: DF-1666 fd leak via drm_syncobj_get_fd -ENOSYS-after-get_unused_fd_flags and via export_sync_files no-op put_unused_fd (both instances re-confirmed but covered). Also killed for this file: idr usage is fully serialized under file_private->syncobj_table_lock (find/alloc/remove all take it) so the DF-4506 idr_grow race does NOT apply here; drm_syncobj_fence_get_or_add_callbacks WARN_ON overwrite leaks one fence ref only in an equally-tight corner (upstream-inherited, folded into notes); no timeline/pt/transfer code exists in this port of drm_syncobj.c.", "recommended_fix": "dma_fence_get_rcu(_safe): kref_get_unless_zero + retry loop, and free fences via kfree_rcu (dma_fence_release default path and dma_fence_free) so the ports RCU delay-line covers lockless readers" } |