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

amdgpu_fence_wait_empty: dead dma_fence_get_rcu bail β€” racing reader resurrects and keeps using an already-released fence (double-free tail empirically blocked by the port's refcount_inc saturation)

Summary

The reader loads a slot under preempt_disable and re-references with dma_fence_get_rcu, which the port implements as unconditional kref_get - the !get_rcu bail is dead code. A preempting fence IRQ dropping the last reference lets the reader take a reference on the already-released fence and wait/put on it. REPRODUCED on guest via a KLD on the port's own primitives (refcount resurrected to 0xC0000000); the hypothesized double-free tail is EMPIRICALLY BLOCKED by refcount_inc saturating zero counts - containment incidental, re-armed by any change to refcount_inc or the delay line. Same DF-4510 family weakened. Unpriv render-node reach on hardware. kref_get_unless_zero header fix (same shape as DF-4510).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4684 Β· 14 files
FileTypeDescriptionSize
df4684.c β€” 10.0 KB view raw
Makefile β€” 178 B ↓ download
build.sh β€” 143 B view raw
run.sh β€” 170 B view raw
build.log β€” 1004 B view raw
run.log β€” 1.0 KB view raw
run.fixed.log β€” 666 B view raw
fix.diff β€” 499 B view raw
VERDICT.md β€” 7.6 KB ↓ raw
README.md β€” 1.6 KB ↓ raw
env.txt β€” 760 B view raw
build_kernel.log β€” 1.6 KB view raw
verdict.json β€” 5.5 KB view raw
manifest.json β€” 1.4 KB view raw

DF-4684 β€” amdgpu_fence_wait_empty dead dma_fence_get_rcu bail (Low, hardening)

  • File: sys/dev/drm/amd/amdgpu/amdgpu_fence.c:295-316 (reader), :244-265 (writer), :631-649 (release/free).
  • Class: lockless fence-get under no-op RCU (DF-4510 family, weakened instance) β€” dead bail path + use of an already-released fence; double-free tail empirically blocked by the port's refcount_inc saturation.
  • Trigger on the audit guest: none needed for the primitive β€” df4684.ko replays the exact code path with the port's own primitives.

Reproduce

# push df4684.c + Makefile to the guest (guest: 6.5-DEVELOPMENT, /usr/src present)
# build:
cd /root/poc/DF-4684 && make -m /usr/share/mk KMODDIR=$PWD   # -> df4684.ko
# baseline (stock headers; drm.ko of the running kernel):
kldload drm
kldload ./df4684.ko
sysctl kern.df4684_run=1      # then: dmesg | tail
# fixed header A/B:
#   patch /usr/src/sys/dev/drm/include/linux/dma-fence.h per fix.diff,
#   rm -f df4684.o df4684.ko && make ... && kldunload df4684; kldload ./df4684.ko
#   sysctl kern.df4684_run=1  # reader bails at :307, single free

Expected

Baseline run.log: refcount resurrected to 3221225472 (0xC0000000 REFCOUNT_SATURATED), RESULT: releases=1 frees=1 β€” the reader used the dead fence and the double-free tail was blocked by saturation. Fixed run.fixed.log: dma_fence_get_rcu(...) = 0 β†’ get_rcu refused the dead fence (FIXED header).

See VERDICT.md for the full line-accurate narrative and fix.diff. panic.txt-style artifacts are not applicable (no crash by design); DF-4685 (same module, mode 2) carries the panic evidence.

VERDICT.md
↓ download raw

DF-4684 β€” amdgpu_fence_wait_empty() lockless dma_fence_get_rcu() resurrects an already-released fence (dead !get_rcu bail; no memory corruption on this port β€” saturation blocks the double-free tail)

What was audited and what the finding IS

sys/dev/drm/amd/amdgpu/amdgpu_fence.c:295-316 (amdgpu_fence_wait_empty) is the amdgpu instance of the DF-4510-family lockless-fence-get pattern. It reads a fence-array slot under rcu_read_lock() and takes a reference with dma_fence_get_rcu():

/* amdgpu_fence.c:304-311 */
ptr = &ring->fence_drv.fences[seq & ring->fence_drv.num_fences_mask];
rcu_read_lock();                                   /* preempt_disable() */
fence = rcu_dereference(*ptr);                     /* :306 load P      */
if (!fence || !dma_fence_get_rcu(fence)) {         /* :307 get P       */
        rcu_read_unlock();
        return 0;
}
rcu_read_unlock();
r = dma_fence_wait(fence, false);                  /* :313 */
dma_fence_put(fence);                              /* :314 */

On Linux both halves are safe: rcu_read_lock() is a real grace-period critical section and dma_fence_get_rcu() is kref_get_unless_zero + NULL-on-failure, so the :307 bail path actually exists. On this port:

The writer it races against is amdgpu_fence_process() (amdgpu_fence.c:244-265, fence IRQ / fallback timer / force-completion / debugfs), which pulls the fence out of the slot and drops the array's reference:

fence = rcu_dereference_protected(*ptr, 1);   /* :252 */
RCU_INIT_POINTER(*ptr, NULL);                 /* :253 */
...
r = dma_fence_signal(fence);                  /* :258 β€” signal BEFORE put */
...
dma_fence_put(fence);                         /* :264 β€” may be the LAST ref */

When the array reference is the last one (the steady state of a completed, unprocessed fence on an idle ring), the :264 put drives the refcount to zero, amdgpu_fence_release() (:646-649) queues call_rcu(&f->rcu, amdgpu_fence_free), and the object is freed by the port's 1-second delay line (linux_rcu.c rcu_timer, delta < hz break).

The race (line-accurate)

On real hardware the fence-completion IRQ preempting the PM/teardown reader between :306 and :307 produces exactly this order:

  1. reader loads P from the slot (:306);
  2. IRQ: amdgpu_fence_process() NULLs the slot, signals P, puts P β€” last ref β†’ call_rcu#1 (free deferred β‰₯1 s);
  3. reader resumes at :307: dma_fence_get_rcu(P) does an unconditional kref_get on the already-released fence.

What the guest run proved (deterministic, this exact code path)

df4684.c is a KLD that replays emit/process/wait_empty/release/free verbatim against the port's own primitives (dma_fence_init/signal/release, call_rcu delay line, kmem_cache shim), scheduling the IRQ interleave by hand. Baseline (run.log, guest 6.5-DEVELOPMENT X86_64_GENERIC, drm.ko stock):

df4684: reader :306 rcu_dereference(slot) = 0xfffff8008f90d100
df4684: writer :258 dma_fence_signal -> 0
df4684: release #1: refcount hit 0 -> call_rcu(&f->rcu=..., free)
df4684: reader :307 dma_fence_get_rcu(...) = <same ptr>, refcount resurrected to 3221225472
df4684: reader :313 dma_fence_wait -> 0 (signaled)
df4684: reader :314 dma_fence_put done, refcount now -1073741825
df4684: [delay-line fired #1] kmem_cache_free(0xfffff8008f90d100)
df4684: RESULT: releases=1 frees=1 β€” ... refcount_inc SATURATION ... BLOCKED ...

3221225472 = 0xC0000000 = REFCOUNT_SATURATED (sys/dev/drm/include/linux/refcount.h:39,53-60): the port's refcount_inc() detects a zero refcount and poisons the count instead of incrementing. The reader's :314 put therefore decrements 0xC0000000 β†’ 0xBFFFFFFF and never re-releases β€” the double-call_rcu / double-kmem_cache_free tail I initially hypothesized is blocked on this port. The run proves it: exactly one release, one delay-line free, and the two post-run allocations come back distinct (no freelist aliasing).

Why the freed-memory variant is also unreachable here (unlike DF-4510): the reader's :306β†’:307 window sits inside preempt_disable(), so it can only be stretched by an interrupt (milliseconds at most), while the object's memory cannot be freed for β‰₯1 s (delay line). The get therefore always lands on mapped, zero-refcount memory β†’ saturation. DF-4510's syncobj stub fences, by contrast, kfree() synchronously at last put, so the load can land on freed-and-reused memory with a nonzero first word β€” that remains a real UAF; this instance does not.

Verdict

  • The upstream safety contract of amdgpu_fence_wait_empty:307 is violated on this port: the bail path is dead code, and a racing reader really does take a "reference" on and keep using a fence whose lifetime already ended (empirically demonstrated).
  • On the current port the consequence is contained to that dead-object use (memory still mapped, fence already signaled so :313 returns immediately) plus refcount poisoning of a dead object β€” no memory corruption. The containment is incidental, resting on refcount_inc saturation semantics; any change to refcount_inc (e.g. adopting Linux semantics where 0 is "use-after-free, saturate" β€” present here β€” or a plain atomic inc) or to the delay-line grace silently re-arms the double-free demonstrated blocked here.
  • Severity Low / hardening; filed because pass-2 was explicitly asked whether the DF-4510 pattern exists in this file (it does, in weakened form) and because the dead check + dead-object use is a real defect worth the one-line fix.

Fix (validated A/B on the guest)

fix.diff = make dma_fence_get_rcu() use kref_get_unless_zero and return NULL on a dead fence (same shape as the DF-4510 header fix; kref_get_unless_zero already exists at include/linux/kref.h:85, and linux_reservation.c:357 already expects this contract).

  • baseline: resurrection to 0xC0000000, releases=1 frees=1 with the reader proceeding on the dead fence (run.log);
  • patched header (module rebuilt against it, same stock drm.ko β€” the inline is compiled into the consumer): reader :307 dma_fence_get_rcu(...) = 0 β†’ get_rcu refused the dead fence (FIXED header) -> wait_empty returns 0, no put β†’ single legitimate free (run.fixed.log).

Full-tree compile validation of the header fix plus the DF-4685 rcu_barrier fix: make -j6 nativekernel KERNCONF=X86_64_GENERIC RC=0 under -Werror (build_kernel.log excerpt in this pack).

Reaching this code on hardware

amdgpu_fence_wait_empty runs at runtime via amdgpu_pm_compute_clocks (amdgpu_pm.c:2133-2146, ACPI events, DPM sysfs, display changes) and at teardown (amdgpu_fence_driver_fini/suspend, amdgpu_fence.c:502,537), on any amdgpu-bound GPU (module amdgpu.ko; render node is unprivileged). The audit guest has no AMD GPU (QEMU virtio-gpu only), so the in-situ IRQ race could not be run; the deterministic KLD replay exercises the identical code path on the port's own primitives.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/B on the guest: baseline (stock headers) resurrects the dead fence (run.log); with fix.diff applied to the guest /usr/src copy and the module rebuilt against it β€” first on the stock drm.ko, then on a fully rebuilt kernel (make -j6 nativekernel KERNCONF=X86_64_GENERIC RC=0 under -Werror, installed and rebooted) β€” dma_fence_get_rcu returns 0, the reader takes the upstream bail ('get_rcu refused the dead fence -> wait_empty returns 0, no put'), and exactly one legitimate free fires (run.fixed.log). No regression: linux_reservation.c consumers already check the get_rcu return value.

run.log vs run.fixed.log; fix.diff (git-apply clean against sys/); build_kernel.log (nativekernel RC=0, 35574-line build log); post-validation md5 restore of /usr/src/sys/dev/drm/include/linux/dma-fence.h = 190f4a4ad15ce6247620a63e98c75327 (pristine)
↓ fix.diffper-fix-DF-4684

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (baseline: 'refcount resurrected to 3221225472', 'RESULT: releases=1 frees=1 ... SATURATION ... BLOCKED'); run.fixed.log (patched header: 'dma_fence_get_rcu(...) = 0' -> 'get_rcu refused the dead fence ... no put'); VERDICT.md line-accurate race + why-saturation-saves analysis; df4684.c the verbatim replica

PoC changes

authored fresh KLD (df4684.c): replicates emit/process/wait_empty/release/free verbatim with the port's primitives; two revisions β€” first proved the double-free tail, then re-instrumented (release/free counters) after discovering refcount_inc saturation blocks it; MODULE_DEPEND(drm) deliberately omitted because DF's linker does not refcount module deps (kern_linker.c:574), which made kldunload tear down drm.ko and wedge in a wait_event

Verified recommended fix

dma_fence_get_rcu(): kref_get_unless_zero + return NULL on a dead fence (dma-fence.h:97-103) β€” same shape as the DF-4510 header fix; linux_reservation.c:357 already expects this contract

Verdict

Deterministically reproduced on the guest that amdgpu_fence_wait_empty's upstream safety bail is dead code on this port: the reader (amdgpu_fence.c:306-307, replicated verbatim in a KLD against the port's own dma_fence/call_rcu/kmem_cache primitives) loads the array slot, the fence-completion IRQ ordering (amdgpu_fence_process :252-264) releases the fence to zero and queues call_rcu, and the reader's dma_fence_get_rcu then takes a 'reference' on the already-released fence β€” the port's dma_fence_get_rcu (dma-fence.h:97-103) is an unconditional kref_get under rcu_read_lock()==preempt_disable (rcupdate.h:46-56), so the !get_rcu bail at :307 can never fire. The hypothesized double-free tail (resurrect->put->second call_rcu on the same rcu_head->kmem_cache_free twice) is EMPIRICALLY BLOCKED on this port: the port's refcount_inc saturates a zero count to REFCOUNT_SATURATED 0xC0000000 (refcount.h:53-60), the reader's put lands at 0xBFFFFFFF, releases=1 frees=1, post-run allocs distinct (run.log). The freed-and-reused-memory variant is unreachable because the reader's window is preempt_disable'd (interrupts only, milliseconds) while the delay line cannot free for >=1s β€” unlike DF-4510's synchronously-kfree'd syncobj stubs. Net: a genuine contract violation (use of a logically-dead fence + dead bail path) with no memory corruption on current port semantics; containment is incidental to refcount_inc saturation, so the one-line header fix is warranted as hardening. In-situ IRQ race not exercisable on this guest (no AMD GPU; QEMU virtio-gpu only); the deterministic KLD replay runs the identical code path.