drm_sched_job_recovery re-runs completed jobs across its unlocked run_job() window racing drm_sched_job_finish β UAF (job + drm_sched_fence freed, parent write and cb armed inside freed heap, panic on signal)
Summary
Recovery walks ring_mirror_list with job_list_lock dropped around run_job without skipping already-signaled jobs or pinning s_fence; a job that completed just before the reset gets finish()'d inside the window - recovery then writes the parent pointer and arms &s_fence->cb inside the FREED fence, linked into a live fence's cb list: signalling executes process_job through reclaimed memory. REPRODUCED twice on fresh-reset guests (UAF armed confirmed by walking the live cb_list; stage-2 fire = Fatal trap 9 GPF in dma_fence_signal_locked). Production path: TDR on one ring runs recovery for ALL rings (amdgpu_device_gpu_recover) from a context the victim ring's finish_work does not serialize against. Unpriv GPU client privesc-grade primitives (fixed-offset pointer write, list-node+cb->func write into freed heap, ops->release indirect call). Fix (signal-skip + fence pin) validated on rebuilt amdgpu.ko: 10/10 benign, no UAF.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4740 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df4740.c | β | 14.9 KB | view raw | |
| Makefile | β | 328 B | β download | |
| build.sh | β | 98 B | view raw | |
| run.sh | β | 160 B | view raw | |
| build.log | β | 64 B | view raw | |
| run.log | β | 1000 B | view raw | |
| run_fixed.log | β | 2.1 KB | view raw | |
| fix_build.log | β | 1.2 MB | β download | |
| panic.txt | β | 2.8 KB | view raw | |
| env.txt | β | 290 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| verdict.json | β | 5.6 KB | view raw |
Fix verification
fixedfixed: amdgpu.ko rebuilt from patched /usr/src on the guest (fix_build.log, 301 cc steps); re-run of the exact harness shows recovery never re-enters the completed victim (10/10 benign interleave, no 'hold entered', no UAF CONFIRMED), fire returns EPERM, guest stays up (run_fixed.log) β versus baseline UAF CONFIRMED + Fatal trap 9.
fix.diff; fix_build.log; run_fixed.log
Confirmed kernel references
- sys/dev/drm/scheduler/sched_main.c:368
- sys/dev/drm/scheduler/sched_main.c:373
- sys/dev/drm/scheduler/sched_main.c:374
- sys/dev/drm/scheduler/sched_main.c:216
- sys/dev/drm/scheduler/sched_main.c:220
- sys/dev/drm/scheduler/sched_main.c:225
- sys/dev/drm/scheduler/sched_main.c:226
- sys/dev/drm/amd/amdgpu/amdgpu_device.c:3409
Detail
Exploit chain
unprivileged GPU client hangs one ring (TDR) while completing a job on another ring -> amdgpu_device_gpu_recover runs recovery for the victim ring from the hung ring's TDR worker -> victim job's finish_work frees job+s_fence inside recovery's unlocked run_job window -> recovery writes parent ptr + cb.func + cb.node links into freed heap (fixed offsets; slot reclaimable by attacker-groomed same-size objects) -> new parent fence signals -> drm_sched_process_job interprets victim object as drm_sched_fence: kref get/put writes, atomic_dec via s_fence->sched, lockmgr via fence->lock, ops->release indirect call -> forged-fence RIP control on this guest (demonstrated: kernel GPF/panic through the freed cb).
Evidence (decisive lines)
run.log (stage-1: hold entered, free_job INSIDE window, UAF CONFIRMED with cb at +0xa0 in freed s_fence); panic.txt (FIRE then Fatal trap 9 at dma_fence_signal_locked+0xb0, db> wedge); run_fixed.log + fix_build.log (patched amdgpu.ko: 10/10 benign, no UAF, EPERM fire, guest up); df4740.c (harness); VERDICT.md (full trace with path:line)
PoC changes
No seed existed; authored a KLD harness (df4740.c) that drives the real scheduler code with synthetic backend ops, models the amdgpu cross-ring reset flow, gates run_job to open the race window, waits out the drm RCU delay-line, reclaims/identifies the freed fence, and verifies the armed callback by walking the live fence's cb_list. Two compile fixes vs first draft (moduledata_t handler signature, kprintf format) and one semantic fix (first-pass fence recording for both tests).
Verified recommended fix
In drm_sched_job_recovery skip jobs with dma_fence_is_signaled(&s_fence->finished) and dma_fence_get/put(&s_fence->finished) across the unlocked run_job window (plus cancel work_tdr in drm_sched_fini) β fix.diff, fix-validated.
Verdict
Reproduced deterministically on a fresh-reset guest against the unmodified in-kernel scheduler (drm/amdgpu KLDs + a synthetic-backend harness KLD driving the real drm_sched_ / dma_fence_ code). drm_sched_job_recovery drops job_list_lock around run_job() (sched_main.c:368-369) without skipping jobs whose finished fence is already signaled and without pinning the fence: a concurrent drm_sched_job_finish (whose cancel_delayed_work_sync at sched_main.c:216 serializes only against THIS scheduler's work_tdr, not against a reset context originating on another ring β the amdgpu_device_gpu_recover flow, amdgpu_device.c:3409-3442) removes the job, RCU-frees s_fence and calls free_job inside the window; recovery then writes s_fence->parent and arms &s_fence->cb inside the freed allocation (sched_main.c:373-374). Stage-1 proof: the armed cb node (offset +0xa0) was found linked into the live recovery-parent fence's cb_list inside the freed s_fence; the freed block had meanwhile been through the drm RCU delay-line free. Stage-2: signalling the recovery parent executed drm_sched_process_job through the freed callback -> Fatal trap 9 (GPF) in dma_fence_signal_locked+0xb0, guest wedged at db>, vm.sh status down. An unfilled variant first executed silently and queued a second RCU free (delayed double-free). Primitive: fixed-offset heap-pointer write + list-node write + later kref/atomic/lockmgr ops and an ops->release indirect call through attacker-groomable freed heap β RIP-control-grade on this no-SMAP/SMEP/KASLR guest; production weaponization additionally requires a GPU driver instance (amdgpu in-tree; absent from the QEMU guest). fix.diff (skip signaled jobs + pin finished fence across the window) validated: patched amdgpu.ko rebuild, harness re-run -> recovery never re-runs the completed job (no hold entered, 10/10 benign), no UAF armed, fire returns EPERM, guest stays up.
No comments yet.