amdgpu_device_gpu_recover dereferences the timed-out job across the multi-second GPU reset although the fence-completion path can already have kfree'd it (UAF, concurrent-reinit family)
Summary
drm_sched_job_timedout hands the timed-out job to amdgpu_device_gpu_recover holding NO reference. If that job's fences complete just before/during recovery (hang-resolves-at-timeout window; force_completion actively signals outstanding fences inside the window), drm_sched_job_finish -> amdgpu_job_free_cb kfrees the job, while gpu_recover keeps reading job->base.sched once per ring and again after the multi-second reset. Freed-slab reuse forges the sched pointer comparison and can steer job recovery onto arbitrary rings. Gated by amdgpu_gpu_recovery=1 (default off on bare metal) plus the timing race; inherited verbatim from Linux 4.20. Structural UAF proven from source; PoC compiles on guest.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3862 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 3.0 KB | β raw | |
| tdr_race.c | β | 1.5 KB | view raw | |
| build.sh | β | 130 B | view raw | |
| run.sh | β | 163 B | view raw | |
| build.log | β | 76 B | view raw | |
| run.log | β | 210 B | view raw | |
| env.txt | β | 483 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 2.8 KB | view raw |
DF-3862 β amdgpu_device_gpu_recover() dereferences the timed-out job that the fence-completion path can already have freed (UAF, concurrent-reinit family)
File: sys/dev/drm/amd/amdgpu/amdgpu_device.c β amdgpu_device_gpu_recover() (3388-3464)
Root cause
Lifetime chain (all in this tree):
drm_sched_job_timedout()(sys/dev/drm/scheduler/sched_main.c:250-289) picksjob = list_first_entry_or_null(&sched->ring_mirror_list, ...)underjob_list_lock, drops the lock, then callssched->ops->timedout_job(job)(line 271). No reference of any kind is taken on the job.amdgpu_job_timedout()(amdgpu_job.c:31-48) forwards toamdgpu_device_gpu_recover(adev, job)(amdgpu_job.c:47).- Meanwhile, if the job's hardware fence was signaled just before/while the
TDR fires (GPU hung but this job completed; or
amdgpu_fence_process/force_completionraced), the normal completion chain runs asynchronously:drm_sched_process_jobβ signalss_fence->finishedβfinish_cbβdrm_sched_job_finish(sched_main.c:203-227) βlist_del_init(&s_job->node)+sched->ops->free_job(s_job)=amdgpu_job_free_cbβkfree(job)(amdgpu_job.c:110-120). amdgpu_device_gpu_recover()keeps dereferencingjob: - loop 1, amdgpu_device.c:3414if (job && job->base.sched != &ring->sched)β executed for EVERY ring i (up to 17 iterations) across the parked scheduler set-up; - amdgpu_device.c:3417drm_sched_hw_job_reset(..., job ? &job->base : NULL); - loop 2 after the multi-secondamdgpu_device_reset()(MMIO waits, PSP handshake, VRAM recovery): amdgpu_device.c:3438if ((!job || job->base.sched == &ring->sched) && !r).
Between (3) and each of these reads there is no lock that protects the
job object (job_list_lock only guards list membership, and the job is
already off the list once finish ran). β use-after-free read; a groomed
reuse of the freed struct amdgpu_job slab additionally steers which
rings get drm_sched_job_recovery() and feeds a forged
job->base.sched pointer comparison.
Note amdgpu_fence_driver_force_completion() (amdgpu_fence.c:582-586) is
called at amdgpu_device.c:3420 inside this window β it force-signals every
outstanding fence on the ring, actively pushing in-flight jobs toward the
free path above (for jobs whose parent callback detached in
drm_sched_hw_job_reset the chain is severed; for a job that completed a
moment earlier the chain is already running).
Preconditions
amdgpu_gpu_recovery=1module parameter (default-1= recovery disabled on bare metal; enabled for SR-IOV VFs) β amdgpu_device.c:3363-3377.- A GPU hang (lockup_timeout, default 10 s) plus completion of the timed-out job racing the TDR β the classic hang-just-as-job-finished window.
Code is inherited verbatim from Linux ~4.20; upstream later reworked scheduler/timeout lifetime handling (5.x).
Trigger
tdr_race.c β renders the race: submits an endless IB to the compute ring
(hang), times a concurrently-completing job, and loops. Needs the module
parameter amdgpu.gpu_recovery=1 on hardware. Deterministic wins are not
guaranteed (timing race); a DEBUG_MALLOC/invariant kernel would flag the
kfree'd-slab read.
Fix
fix.diff snapshots job->base.sched once at entry (before any fence can
be force-completed) and stops dereferencing job in both loops. The fully
complete fix additionally needs scheduler-side job refcounting (as upstream
did later); this patch removes the reachable post-reset dereferences in this
file.
DF-3862 VERDICT β not_reproduced at runtime (hardware-gated), speculative-likely race, source-traced
Reproduction attempt
Guest has no AMD GPU (see env.txt): no render node, amdgpu attaches to nothing, jobs can never be submitted, no TDR can fire. The race is therefore untestable here: status = not_reproduced, reproduced = 0.
Source trace establishing the lifetime gap (path:line)
- TDR picks the victim without holding any job reference:
sys/dev/drm/scheduler/sched_main.c:258-271 β
job = list_first_entry_or_null(&sched->ring_mirror_list, ...)underjob_list_lock;lockmgr(&sched->job_list_lock, LK_RELEASE);at :268;sched->ops->timedout_job(job);at :271. - amdgpu_job.c:31-48 β
amdgpu_job_timedout()βamdgpu_device_gpu_recover(ring->adev, job)at :47. - Free chain: dma_fence_signal on the hw fence β
drm_sched_process_job(sched_main.c, s_fence->cb) β signalss_fence->finishedβdrm_sched_job_finish_cb(sched_main.c:229-235) βschedule_work(&job->finish_work)βdrm_sched_job_finish(sched_main.c:203-227):list_del_init(&s_job->node)at :220 thensched->ops->free_job(s_job);at :226 βamdgpu_job_free_cb(amdgpu_job.c:110-120) βkfree(job);at :119. - Dangling dereferences in amdgpu_device.c:3414 (
job->base.schedread for every ring), 3417 (&job->base), 3438 (job->base.schedagain after the multi-secondamdgpu_device_reset()). - Ammunition inside the window:
amdgpu_fence_driver_force_completion(ring)at amdgpu_device.c:3420 β amdgpu_fence.c:582-586 force-processes every outstanding fence β for any job whose parent callback was NOT detached indrm_sched_hw_job_reset(i.e. it completed a moment earlier, callback already firing) the free chain (3) runs to completion during the reset.
Why speculative and not "certain": the deref requires the specific interleaving where the timed-out job itself completes concurrently. The window is real and the code holds no protection, but I could not execute it (no GPU) β filing confidence "speculative" for exploitation, with the structural UAF demonstrated from source.
Preconditions (honest gating)
amdgpu_gpu_recovery=1(root module param; default off on bare metal; on for SR-IOV VF) β amdgpu_device.c:3363-3377 (should_recover_gpu).- GPU hang + racing completion of the same job. Impact if hit: UAF read on a
freed heap object; with slab grooming,
drm_sched_job_recovery()can be steered onto additional rings via a forgedjob->base.schedpointer β Medium.
Fix validation
fix.diff (job_sched snapshot) is git apply --check clean. Building the
patched kernel here cannot exercise the path (no AMD GPU): fix_status =
not_testable.
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_testablefix.diff replaces both job derefs with the snapshot; git apply --check clean; cannot exercise TDR path without AMD hardware
findings/poc/DF-3862/fix.diff
Confirmed kernel references
Detail
Exploit chain
hang GPU with amdgpu.gpu_recovery=1 while the timed-out job completes concurrently -> kfree(job) via finish_work during amdgpu_device_reset -> UAF read at :3438; groomed slab reuse forges job->base.sched and steers drm_sched_job_recovery onto arbitrary rings
Evidence (decisive lines)
VERDICT.md (full lifetime trace path:line); build.log/run.log (trigger compiles RC=0 on guest, hardware gate); env.txt
PoC changes
authored from scratch (skeleton; CS submission left as documented TODO since no hardware)
Verified recommended fix
Snapshot job->base.sched into a local at gpu_recover entry and use it in both loops (fix.diff); longer-term add job refcounting to drm_sched timeout path
Verdict
Structural UAF, timing-gated: drm_sched_job_timedout (sched_main.c:266-271) hands the timed-out job to amdgpu_job_timedout -> amdgpu_device_gpu_recover with NO reference held; the async completion chain (drm_sched_process_job -> drm_sched_job_finish sched_main.c:203-227 -> amdgpu_job_free_cb amdgpu_job.c:110-120 -> kfree(job)) can free it while gpu_recover still dereferences job->base.sched at amdgpu_device.c:3414 (every ring) and again at :3438 after the multi-second amdgpu_device_reset; amdgpu_fence_driver_force_completion at :3420 actively pushes jobs toward that free path inside the window. Gated by amdgpu_gpu_recovery=1 (default off on bare metal) plus a hang-vs-completion race; Linux 4.20-era code inherited verbatim. Trigger compiled and ran on the guest: no AMD GPU, no /dev/dri (env.txt) -> TDR path unreachable; honest not_reproduced.
No comments yet.