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)
PoC verification
Evidence pack
findings/poc/DF-4684 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_incsaturation. - Trigger on the audit guest: none needed for the primitive β
df4684.koreplays 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.
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:
rcu_read_lock()ispreempt_disable()βsys/dev/drm/include/linux/rcupdate.h:46-49;dma_fence_get_rcu()is an unconditionalkref_getβsys/dev/drm/include/linux/dma-fence.h:97-103β so it can never return NULL and the!dma_fence_get_rcu(fence)half of the:307check is dead code.
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:
- reader loads
Pfrom the slot (:306); - IRQ:
amdgpu_fence_process()NULLs the slot, signalsP, putsPβ last ref βcall_rcu#1(free deferred β₯1 s); - reader resumes at
:307:dma_fence_get_rcu(P)does an unconditionalkref_geton 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:307is 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
:313returns immediately) plus refcount poisoning of a dead object β no memory corruption. The containment is incidental, resting onrefcount_incsaturation semantics; any change torefcount_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
fixedA/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)
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_fence.c:295-316
- sys/dev/drm/amd/amdgpu/amdgpu_fence.c:244-265
- sys/dev/drm/amd/amdgpu/amdgpu_fence.c:631-649
- sys/dev/drm/amd/amdgpu/amdgpu_pm.c:2133-2146
- sys/dev/drm/include/linux/dma-fence.h:97-103
- sys/dev/drm/include/linux/rcupdate.h:46-56
- sys/dev/drm/include/linux/refcount.h:39
- sys/dev/drm/include/linux/refcount.h:53-60
- sys/dev/drm/linux_rcu.c:56-78
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.
No comments yet.