DF-4740 / verdict.json
{ "finding_id": "DF-4740", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "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 schedulers work_tdr, not against a reset context originating on another ring \u2014 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 fences 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 \u2014 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.", "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 rings TDR worker -> victim jobs finish_work frees job+s_fence inside recoverys 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": "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)", "kernel_refs": [ "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" ], "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 fences 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).", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 2100, "guest_dirty": 0, "build_cmd": "make in /tmp/df4740 (guest, bsd.kmod.mk; see build.sh) after kldload amdgpu", "run_cmd": "kldload /tmp/df4740/df4740.ko; sysctl hw.df4740_test=1; sysctl hw.df4740_fire=1 (see run.sh)", "code_hash": "751649c068ff459041fad8be03c35fc9ad6136287ec4b1c6dac9d5152b0635de", "notes": "Baseline runs started from vm.sh reset with-src (fresh guest) every time; race caught on attempt 1 in both baseline runs (fully deterministic under the harness). Stage-2 fire re-fills the freed block with 0xde to stand in for allocator reuse (what happens within microseconds on a live system); an earlier no-refill fire showed the silent delayed double-free path. Guest reset with-src performed at end of run.", "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) \u2014 fix.diff, fix-validated.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fixed: 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) \u2014 versus baseline UAF CONFIRMED + Fatal trap 9.", "fix_evidence": "fix.diff; fix_build.log; run_fixed.log" } |