radeon_sa_bo_next_hole leaves dangling borrowed fence pointers across inner-loop iterations β UAF write, UAF read, and double kfree in radeon_sa_bo_new (missing upstream fix 0381ac3ca2e7)
Summary
The port has the 2016 fence-ref loop but LACKS the 2022 fences[i]=NULL per-scan init: next_hole borrows an uncounted fence pointer, the same thread frees it under the same lock (flist fences hold exactly one ref - the common case), and a later scan with no fresh borrow ref's/reads/double-kfrees the stale pointer - kernel heap corruption on the standard slab-groom path from sustained unpriv DRI IB traffic. Line-faithful port harness: full chain 100/100 (UAF-REF, UAF-WAIT, DOUBLE-FREE), the exact upstream one-liner kills it 100/100; kernel path inconclusive (no radeon GPU on guest). Port-upstream-0381ac3ca2e7 fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4927 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| radeon_sa_uaf.c | β | 16.9 KB | view raw | |
| build.sh | β | 118 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 468 B | view raw | |
| run.log | β | 824 B | view raw | |
| run.2.log | β | 560 B | view raw | |
| run.3.log | β | 46 B | view raw | |
| env.txt | β | 276 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| VERDICT.md | β | 6.8 KB | β raw | |
| README.md | β | 4.5 KB | β raw | |
| manifest.json | β | 1.8 KB | view raw | |
| verdict.json | β | 5.8 KB | view raw |
DF-4927 β radeon_sa fence-borrow UAF / double-free (pass-2 finding)
File: sys/dev/drm/radeon/radeon_sa.c
Class: kernel heap use-after-free + double-free (memcorrupt), HW-gated
(radeon GPU required at kernel level)
Upstream fix: Linux 0381ac3ca2e7 "drm/radeon: Initialize fences array
entries in radeon_sa_bo_next_hole" (2022; amdgpu twin 6a15f3ff19a8).
DragonFly carries the pre-2022 code: the 2016 fence-ref fix
(f6ff4f67cdf8) is present (the ref loop at radeon_sa.c:352-353) but the
2022 fences[i] = NULL initialization in radeon_sa_bo_next_hole is
missing.
What the bug is
radeon_sa_bo_next_hole() borrows a non-counted pointer into the caller's
fences[] array (radeon_sa.c:278, only when the flist head fence is not
yet signaled). The array is only reset once per outer loop iteration
(radeon_sa.c:335-338); nothing clears fences[i] between inner loop
iterations. If the borrowed fence becomes signaled before a later
next_hole() call, the same thread frees it β via
radeon_sa_bo_try_free() (sa.c:341β166β150) or next_hole() itself
(sa.c:306) calling radeon_sa_bo_remove_locked() β radeon_fence_unref()
β dma_fence_put() β refcount 0 β kfree() (sys/dev/drm/linux_fence.c:43-51;
fences parked on an flist normally hold exactly one reference β see
radeon_ib_free(), sys/dev/drm/radeon/radeon_ib.c:95-98). If ring i
then produces no fresh borrow (its flist is now empty, or its new head is
signaled with tries[i] > 2), the stale pointer survives until the inner
loop exits, and:
radeon_sa.c:352-353radeon_fence_ref(fences[i])β UAF write (kref_get on freed slab memory β corrupts allocator freelist metadata if the chunk was not yet reused),radeon_sa.c:356radeon_fence_wait_any()readsfences[i]->seqβ UAF read,radeon_sa.c:357-358radeon_fence_unref(&fences[i])drops the bogus reference β refcount hits 0 again βdma_fence_releaseβkfree()a second time β double free.
Trigger: any local user with DRI access on a radeon system driving enough
IB traffic that the shared ring_tmp_bo sub-allocator blocks
(radeon_sa_bo_new wait loop) with multi-ring in-flight work β i.e. GPU
workload + timing, no privileges beyond the DRI device. The window is
"fence signals between two inner-loop iterations", which recurs millions
of times under sustained load.
What can be verified on this audit guest
The QEMU guest has no radeon GPU, so the kernel code path cannot be
exercised end-to-end (status inconclusive for the kernel path). What is
verified, deterministically, on the guest:
radeon_sa_uaf.cβ a line-faithful userspace port of the five allocator functions (sa.c:142-374; only list plumbing, locks and the fence object are substituted, see file header). A "GPU simulator" flips the target fence to signaled on its 3rdradeon_fence_signaled()check β i.e. after it was borrowed by onenext_hole()call and before the next iteration inspects it, exactly the window the upstream commit message describes. The model reproduces the full event chain:UAF-REF(352-353) βUAF-WAIT(356) βDOUBLE-FREE(357-358). 100/100 runs, exit 42.- Fix validation on the same port (
-DFIXEDadds exactly the upstream one-linerfences[i] = NULL;): identical fence-death timeline, zero dangling dereferences, 100/100 runs, exit 0. - Source divergence vs. Linux v6.1
radeon_sa.c(fetched during the audit) and the upstream fix commit0381ac3ca2e7.patch(quoted in VERDICT.md).
Reproduce
sh build.sh # cc -O2 -> rsa_base, cc -O2 -DFIXED -> rsa_fixed sh run.sh # expects rsa_base exit 42 (UAF), rsa_fixed exit 0
Decisive output (baseline):
[fence F1 refcount 0 -> kfree] <- same-thread free inside the locked loop *** UAF-REF fence F1 ... at radeon_sa_bo_new:352 fence_ref *** UAF-WAIT fence F1 ... at radeon_sa_bo_new:356 fence_wait_any *** DOUBLE-FREE fence F1 ... at radeon_sa_bo_new:358 fence_unref VERDICT: UAF/double-free REPRODUCED on the ported allocator logic (exit 42)
Files
radeon_sa_uaf.cβ the PoC (port + fence model + GPU simulator)build.sh/run.shbuild.log/run.log(baseline) /run.2.log(fixed) /run.3.log(100Γ stress both ways) β full untrimmed guest outputenv.txtβ guest uname, compiler, userfix.diffβ the kernel fix (upstream port), validated on the port modelVERDICT.md,manifest.json,verdict.json
DF-4927 VERDICT β radeon_sa dangling-fence UAF/double-free
Bottom line
The kernel-level bug is certain from source + upstream history; it could
not be exercised on the audit guest because the guest has no radeon GPU.
The allocator logic was instead extracted verbatim into a userspace port and
run on the guest as uid=1001: the dangling-fence sequence (borrow β same-thread
kfree β ref-after-free β wait-read-after-free β double-free) reproduces
deterministically (100/100), and adding exactly the missing upstream
one-liner eliminates all three events (100/100). Kernel-path status is
therefore inconclusive (HW absent), with confidence: certain on the
code-level claim itself.
Root cause (path:line)
sys/dev/drm/radeon/radeon_sa.c:278βfences[i] = sa_bo->fence;borrows an uncounted pointer (only taken when the fence is not yet signaled).sys/dev/drm/radeon/radeon_sa.c:270-272β ring scancontinues on an empty flist without clearingfences[i]; the array is only reset at the top of the outer loop (:335-338). Upstream Linux v6.1 clears it at the top of every scan (the fix added by commit0381ac3ca2e7).- Fence death on the same thread, still under
wq.lock:radeon_sa_bo_try_free(:341) βradeon_sa_bo_remove_locked(:166) βradeon_fence_unref(:150) βdma_fence_putβkref0 βdma_fence_releaseβkfree(sys/dev/drm/linux_fence.c:43-51, radeon.releaseis NULL βradeon_fence.c:1120); ornext_hole's own removal at:306. Fences on an flist normally have refcount exactly 1 becauseradeon_ib_freedrops the IB's reference immediately afterradeon_sa_bo_freetakes the sa_bo's (sys/dev/drm/radeon/radeon_ib.c:95-98). - Consumption of the dangling pointer:
radeon_sa.c:352-353(ref β UAF write),:356(wait reads->seqβ UAF read),:357-358(unref β refcount 0 again β secondkfreeβ double free).
Fence signaling is asynchronous to wq.lock (GPU IRQ advances
fence_drv[ring].last_seq, radeon_fence.c:340-353/432-447), so a fence
borrowed as "not signaled" in scan k can be removed as "signaled" in scan
k+1 β this is precisely the scenario in the upstream commit message
(quoted in full below).
Upstream evidence
Linux 0381ac3ca2e7 β "drm/radeon: Initialize fences array entries in
radeon_sa_bo_next_hole" (Xiaohui Zhang, 2022-06-07):
The entries were only initialized once in radeon_sa_bo_new. If a fence wasn't signalled yet in the first radeon_sa_bo_next_hole call, but then got signalled before a later radeon_sa_bo_next_hole call, it could destroy the fence but leave its pointer in the array, resulting in use-after-free in radeon_sa_bo_new.
The patch adds fences[i] = NULL; at the top of the per-ring scan and
removes the now-redundant array reset in radeon_sa_bo_new. DragonFly's
copy has neither change; it does have the earlier 2016 fix
(f6ff4f67cdf8, "hold reference to fences in radeon_sa_bo_new") β the ref
loop at radeon_sa.c:352-353 β which is what makes the 2022 bug reach a
radeon_fence_ref() call on the dangling pointer at all.
Verification performed (guest, unprivileged user maxx uid=1001)
build.sh: buildsrsa_base(DF logic as ported) andrsa_fixed(adds exactlyfences[i] = NULL;) with the guest's base gcc 8.3 βbuild.log.run.sh/run.log: baseline prints the kfree of F1 inside the locked loop and then UAF-REF, UAF-WAIT, DOUBLE-FREE at sa.c:352/356/358; exit 42.run.2.log: fixed build β same fence-death timeline, zero events, exit 0.run.3.log: 100Γ repeat each β 100/100 vs 100/100.- PoC port fidelity notes (
poc_changes): locks dropped (bug is single-threaded), linux list.h replaced by an equivalent minimal implementation,dma_fence_get/putmodeled by a refcount with an explicit dead flag (NULL-guard semantics ofdma_fence_getpreserved β that guard was initially missing from the port and caused a spurious NULL-deref crash that was fixed during bring-up; see attempts).
Exploit chain (kernel, on radeon hardware β analysis, not executed)
- Unprivileged user with
/dev/drirender access submits continuous command streams on β₯2 rings (graphics + compute/copy) sized to exhaust the shared IB sub-allocation pool, entering theradeon_sa_bo_newblocking loop. - A fence borrowed by
next_hole()completes on the GPU between two inner-loop scans; the same thread frees theradeon_fence(kmalloc'dM_DRM, ~96 bytes βradeon_fence_emit, radeon_fence.c:131) viatry_free/next_hole, leaving the pointer infences[]. radeon_fence_ref()at sa.c:353 doeskref_geton the freed chunk: +1 to the first word of free slab memory. If the chunk sits on the per-CPU freelist this corrupts freelist metadata; if it was reallocated (fence churn makes reuse likely) it corrupts an arbitrary victim's first field.radeon_fence_unref()at :358 drives the refcount back to 0 βkfree()of the already-free chunk β freelist loop/corruption β cross-cache-style attacks onM_DRMallocations; with heap grooming (fence-sized M_DRM objects are attacker-churnable from DRI) this is a plausible path to controlled overlap β code execution in kernel context (uid=0). On this no-SMAP/SMEP/KASLR guest model the standard slab-groom β victim-object β function-pointer pivot chain applies; none of it could be exercised without the GPU.
Fix
fix.diff ports the upstream fix verbatim (both hunks of 0381ac3ca2e7,
lockmgr context unchanged). Validated on the port model: baseline
reproduced=1, patched reproduced=0. A kernel-level single-fix rebuild
was not performed: without a radeon device the changed code path cannot be
entered on the guest (driver binds only to radeon hardware), so a
kernel-build test would demonstrate nothing beyond compilation β the port
validation is the meaningful one and is recorded as such
(fix_status: fixed, scoped in fix_verdict).
Status classification rationale
- Kernel path: inconclusive (guest lacks the hardware; not
not_reproducedbecause it was never runnable, notskippedbecause we substituted the strongest available verification). - Logic-level: reproduced, deterministic, 100/100.
- Impact field
none= no kernel-level impact was observed on this guest; the assessed impact on radeon-equipped systems is High (kernel heap UAF/double-free driven by unprivileged DRI clients).
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
fixedValidated 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).
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.
Confirmed kernel references
- 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
Detail
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_get's 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 (decisive lines)
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).
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_free's handover (refcount 1 on flist); (2) the ported radeon_fence_ref lacked dma_fence_get's 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.
Verified 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().
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.
No comments yet.