⬢ DragonFlyBSD Kernel Audit
DF-4740 / verdict.json
← back to finding ↓ download raw
{
  "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"
}