DF-4927 / verdict.json
{ "finding_id": "DF-4927", "status": "inconclusive", "reproduced": 0, "impact": "none", "confidence": "certain", "verdict": "Kernel path could not be exercised: the QEMU/KVM audit guest has no radeon GPU, so radeon_sa.c is unreachable there (status inconclusive rather than not_reproduced - it was never runnable). The bug itself is certain from source and upstream history: DragonFly carries pre-2022 radeon_sa code - it HAS the 2016 fence-ref fix (ref loop at radeon_sa.c:352-353, upstream f6ff4f67cdf8) but LACKS the 2022 fences[i]=NULL initialization in radeon_sa_bo_next_hole (upstream 0381ac3ca2e7, whose commit message describes this exact UAF). A fence borrowed uncounted at sa.c:278 is freed by the SAME thread inside the same locked loop (try_free/next_hole removal once the GPU signals it; flist fences hold refcount 1 - radeon_ib.c:95-98), and the dangling fences[] entry then reaches radeon_fence_ref (UAF write, :352-353), radeon_fence_wait_any (UAF read, :356) and radeon_fence_unref (double kfree via dma_fence_release, :357-358, linux_fence.c:43-51). A line-faithful userspace port of sa.c:142-374 run on the guest as uid=1001 reproduces the full event chain deterministically (100/100, exit 42), and adding exactly the upstream one-liner eliminates all events (100/100, exit 0) - see run.log/run.2.log/run.3.log.", "exploit_chain": "Kernel (analysis, requires radeon HW): unprivileged DRI client exhausts the shared ring_tmp_bo IB pool with multi-ring workloads -> radeon_sa_bo_new blocking loop borrows a pending fence into fences[i] (sa.c:278) -> GPU signals it between two inner-loop scans -> same thread frees it via try_free/next_hole (sa.c:166/306 -> :150) -> radeon_fence_ref() kref_gets freed M_DRM slab memory at :353 (freelist-metadata corruption or victim-object field corruption if reused) -> radeon_fence_unref() drives refcount to 0 again at :358 -> second kfree of the same chunk (double free) -> slab-groom to victim object / function pointer -> code exec as uid 0. Not executable on this guest (no GPU). PoC-executed portion: deterministic borrow->kfree->UAF-ref->UAF-wait->double-free sequence on the ported allocator logic, plus fix validation on the same port.", "evidence": "run.log: [fence F1 refcount 0 -> kfree] followed by *** UAF-REF ... at radeon_sa_bo_new:352 fence_ref, *** UAF-WAIT ... :356 fence_wait_any, *** DOUBLE-FREE ... :358 fence_unref, exit=42. run.2.log (fixed): same kfree timeline, zero events, exit=0. run.3.log: 100/100 vs 100/100. VERDICT.md: full path:line trace + upstream commit quotes. fix.diff: exact port of 0381ac3ca2e7 (applies clean, zero offset).", "kernel_refs": [ "sys/dev/drm/radeon/radeon_sa.c:278", "sys/dev/drm/radeon/radeon_sa.c:270-272", "sys/dev/drm/radeon/radeon_sa.c:335-338", "sys/dev/drm/radeon/radeon_sa.c:150", "sys/dev/drm/radeon/radeon_sa.c:166", "sys/dev/drm/radeon/radeon_sa.c:306", "sys/dev/drm/radeon/radeon_sa.c:352-353", "sys/dev/drm/radeon/radeon_sa.c:356", "sys/dev/drm/radeon/radeon_sa.c:357-358", "sys/dev/drm/linux_fence.c:43-51", "sys/dev/drm/radeon/radeon_fence.c:699-715", "sys/dev/drm/radeon/radeon_ib.c:95-98" ], "poc_changes": "Original finding sketch did not exist (pass-2 finding discovered in this run); PoC authored from scratch as a line-faithful port of sa.c:142-374 with: minimal list.h equivalent, locks dropped (bug is single-threaded), dma_fence_get/put modeled by refcount+dead-flag. Two fidelity fixes during bring-up: (1) main initially kept an extra reference on the target fence so it never died - fixed to mirror radeon_ib_frees handover (refcount 1 on flist); (2) the ported radeon_fence_ref lacked dma_fence_gets NULL guard and crashed on NULL fences[] entries - restored the guard. GPU signaling modeled as flip-on-3rd-check, placing the signal exactly between the borrow scan and the next scan.", "attempts": 3, "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": 480, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o rsa_base radeon_sa_uaf.c && cc -O2 -Wall -DFIXED -o rsa_fixed radeon_sa_uaf.c", "run_cmd": "./rsa_base; ./rsa_fixed (expect 42 then 0)", "code_hash": "0e596eb67c8a83dcf0e2e5728b6fb444772888d966aae97fe949f8d1134e19a6", "notes": "Guest has no radeon hardware: kernel-level reproduction impossible on this system (radeon module binds only to radeon PCs; QEMU exposes none). Assessment on radeon-equipped systems: High severity kernel heap UAF/double-free reachable by unprivileged users with DRI render access under sustained multi-ring IB pressure. Upstream fixed this exact pattern in 2022 (radeon 0381ac3ca2e7, amdgpu 6a15f3ff19a8); DragonFlys radeon_sa.c predates it. DF-1987 (BUG_ON panic) and DF-1988 (fini without lock) are separate, already-known findings and were not re-reported.", "recommended_fix": "Port upstream Linux 0381ac3ca2e7: add fences[i] = NULL; at the top of the per-ring scan in radeon_sa_bo_next_hole() and drop the redundant fences[] reset in radeon_sa_bo_new().", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Validated at logic level: the -DFIXED build differs from baseline by EXACTLY the upstream one-liner (fences[i] = NULL in next_hole) and produces zero dangling-deref events where baseline produces the full UAF-ref/UAF-wait/double-free chain (100/100 runs each way). Kernel-level single-fix rebuild not performed: without a radeon GPU the modified code path cannot be entered on the audit guest, so a kernel build would only prove compilation; fix.diff applies to sys/dev/drm/radeon/radeon_sa.c cleanly at zero offset (verified with patch --dry-run).", "fix_evidence": "run.log (baseline: 3 events, exit 42) vs run.2.log (fixed: 0 events, exit 0); run.3.log stress 100x; fix.diff hunks at radeon_sa.c:267 and :332." } |