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

amdgpu_cs_wait_all_fences reads fence->error after the final dma_fence_put (use-after-free read, value returned as ioctl errno)

Summary

The wait_all loop drops the fence reference and then dereferences fence->error; when the ioctl's reference is the last one (context fence slot recycled after 32 further submissions), this reads freed memory and returns the value as the ioctl's errno. The sibling amdgpu_cs_wait_ioctl has the correct order, proving the local defect. RCU-deferred free narrows but does not eliminate the window. 4-byte heap-content oracle; read-only. Reorder-put fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4265 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
wait_all_fences_uaf.c β€” 5.1 KB view raw
build.sh β€” 65 B view raw
run.sh β€” 89 B view raw
build.log β€” 782 B view raw
run.log β€” 869 B view raw
env.txt β€” 516 B view raw
kmod_baseline_build.log β€” 4.0 KB view raw
fix_build.log β€” 4.0 KB view raw
fix.diff β€” 516 B view raw
verdict.json β€” 2.9 KB view raw

DF-4265 β€” amdgpu_cs_wait_all_fences reads fence->error after the final dma_fence_put (UAF read)

File: sys/dev/drm/amd/amdgpu/amdgpu_cs.c (pass 2) Severity: Low (use-after-free read; value returned as ioctl errno) β€” CWE-416 Confidence: likely (defect certain; runtime manifestation narrowed by RCU-deferred freeing on this tree)

Build

cc -O2 -Wall -o wait_all_fences_uaf wait_all_fences_uaf.c

Run (as any user on amdgpu hardware)

./wait_all_fences_uaf

Submits 33 minimal GFX CS on one context (so the context fence slot for seq 1 β€” replaced only after amdgpu_sched_jobs=32 further submissions, amdgpu_ctx.c:449-461 β€” is recycled while the wait ioctl holds its own reference), then calls DRM_IOCTL_AMDGPU_WAIT_FENCES with wait_all=1 on seq 1.

Root cause (path:line)

amdgpu_cs_wait_all_fences() (amdgpu_cs.c:1481-1516):

r = dma_fence_wait_timeout(fence, true, timeout);
dma_fence_put(fence);              /* :1501  may drop the LAST ref   */
if (r < 0) return r;
if (r == 0) break;
if (fence->error)                  /* :1508  READ AFTER PUT          */
        return fence->error;

If the reference obtained from amdgpu_cs_get_fence() (amdgpu_ctx.c:489) is the last one (context slot replaced, scheduler/job refs dropped), :1501 frees the fence and :1508 reads freed memory. Compare the correct ordering in the sibling amdgpu_cs_wait_ioctl() (:1367-1370: read fence->error before the put).

On this tree the free is deferred: drm_sched_fence_release_finished β†’ dma_fence_put(scheduled) β†’ drm_sched_fence_release_scheduled β†’ call_rcu(&fence->finished.rcu, drm_sched_fence_free) (sys/dev/drm/scheduler/sched_fence.c:107-113), so a plain single-threaded run reads pre-free memory; manifestation needs the RCU grace period (or preemption) between :1501 and :1508, and the stale value is returned to userspace as the ioctl errno β€” a 4-byte heap oracle (whether the slot got reallocated by a concurrently-submitted job, and the new fence's error code). Impact ceiling: info oracle + theoretical crash; kept Low.

Guest verification (this audit)

Hardware-gated like DF-4262..64 (no AMD GPU): trigger builds (build.log), run exits at open (run.log); baseline + patched in-tree compile rc=0 with -Werror (kmod_baseline_build.log, fix_build.log β€” reorder so the put happens after the error read, all exit paths covered).

Fix

fix.diff: restructure the loop body so fence->error is read before dma_fence_put() and every exit path (r<0 return, r==0 break, error return, loop continue) puts the reference exactly once.

VERDICT.md
↓ download raw

DF-4265 evidence pack β€” verdict

Reproduced? NOT at runtime β€” hardware-gated (no AMD GPU on the audit guest). Defect certain at code level; realistic manifestation is a narrow race (RCU-deferred free), hence confidence=likely for end-to-end impact.

Proof chain

  1. amdgpu_cs_wait_fences_ioctl() (amdgpu_cs.c:1590-1622) with wait_all=1 reaches amdgpu_cs_wait_all_fences() (:1481-1516).
  2. fence = amdgpu_cs_get_fence(...) (:1494) returns a referenced fence: amdgpu_ctx_get_fence() does dma_fence_get(centity->fences[seq & (amdgpu_sched_jobs-1)]) (amdgpu_ctx.c:489) β€” our own reference on top of the context array's.
  3. Body of the loop (:1500-1509): r = dma_fence_wait_timeout(fence, true, timeout); dma_fence_put(fence); / :1501 / ... if (fence->error) / :1508 β€” read AFTER put / return fence->error;
  4. The context array reference is dropped when the slot is overwritten by a later submission: amdgpu_ctx_add_fence() (amdgpu_ctx.c:440-464) replaces centity->fences[seq & 31] and dma_fence_put(other) (:461). After 32 further submissions on the entity (plus normal scheduler teardown of the finished job), the reference dropped at :1501 can be the final one.
  5. Free path on this tree: dma_fence_put β†’ kref β†’ drm_sched_fence_release_finished (sched_fence.c:122-127) β†’ put of the embedded scheduled fence β†’ drm_sched_fence_release_scheduled (sched_fence.c:107-113) β†’ call_rcu(..., drm_sched_fence_free) β€” the slab object is returned to sched_fence_slab only after an RCU grace period. Between :1501 and :1508 there are no RCU read-side critical sections holding it, so the read is a genuine use-after-free in the C sense; practical manifestation requires the grace period to elapse inside the window (preemption + idle machine makes this possible but rare). If freed+reallocated (attacker submits jobs concurrently β€” drm_sched_fence_create allocates from the same slab), :1508 reads the NEW object's error field and returns it as the ioctl's errno β†’ 4-byte heap oracle. If freed and not reused, the read touches slab memory (typically non-fatal).
  6. The correct ordering exists in the sibling ioctl amdgpu_cs_wait_ioctl() (:1367-1371: error read precedes the put) β€” this is a local defect of wait_all_fences only.

Guest verification performed

  • trigger compiles (build.log); runs to the hardware gate (run.log)
  • baseline + patched (reordered put) in-tree compile rc=0 with -Werror (kmod_baseline_build.log / fix_build.log; fix hunk at :1498+)

Exploit chain: read-only oracle; no write primitive; no escalation path.

Recommended fix: read fence->error before dma_fence_put(fence), covering all exit paths (fix.diff).

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

Reordering fix.diff applied on the guest stacked with the other amdgpu_cs.c fixes (hunk @1498) and compiled in-tree with -Werror rc=0 (fix_build.log). Runtime comparison impossible (no AMD GPU); compile-only validation.

fix_build.log rc=0; guest patch output 'Hunk #1 succeeded at 1498 (offset -15 lines)'.
↓ fix.diffper-fix-DF-4265

Confirmed kernel references

Detail

Exploit chain

Read-only oracle on drm_sched_fence slab reuse; no write primitive.

Evidence (decisive lines)

wait_all_fences_uaf.c (33 GFX submissions then WAIT_FENCES all on seq 1); run.log (hardware gate); kmod_baseline_build.log + fix_build.log; VERDICT.md has the full refcount/rcu analysis.

PoC changes

Trigger written from scratch; SCHED_JOBS=32 matches amdgpu_sched_jobs default (amdgpu_drv.c:108) so the slot holding seq 1 is recycled by the 33rd submission.

Verified recommended fix

Read fence->error before dma_fence_put() and put the reference on every exit path (r<0, r==0 break, error return, loop continue).

Verdict

Genuine use-after-free READ ordering defect in amdgpu_cs_wait_all_fences(): dma_fence_put(fence) at amdgpu_cs.c:1501 precedes the fence->error read at :1508; when our reference is the last one (context slot replaced after 32 further submissions - amdgpu_ctx.c:449-461 - and scheduler refs dropped) the read touches freed memory. On this tree the free is RCU-deferred (sched_fence.c:107-113 call_rcu), so single-threaded manifestation is unlikely and the practical ceiling is a 4-byte heap oracle returned as the ioctl errno (plus theoretical crash) - hence Low severity / confidence likely for end-to-end impact while the code defect itself is certain (the sibling amdgpu_cs_wait_ioctl at :1367-1370 has the correct order). Runtime reproduction hardware-gated (no AMD GPU on guest); fix (reordered put covering all exit paths) compiles in-tree with -Werror.