DF-3755 / verdict.json
{ "finding_id": "DF-3755", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "Source-certain lifetime bug, ni twin of DF-3611 (si) / DF-3645 (ci). ni_parse_power_table kfrees rdev->pm.dpm.ps at ni_dpm.c:4022 when a per-state kzalloc fails but leaves the pointer dangling and returns before num_ps is set (:4041). radeon_pm_init_dpm IGNORES the dpm.init rc (radeon_pm.c:1448) and immediately publishes current_ps=requested_ps=boot_ps (radeon_pm.c:1449) \u2014 boot_ps points INTO the freed array when a BOOT state was parsed before the failing iteration \u2014 then runs radeon_dpm_enable (:1453) through the UAF read chain (ni_dpm_enable:3589/3641 -> ni_init_smc_table:1943/1975 -> ni_get_ps(freed)->ps_priv). ni.c:2432 swallows radeon_pm_inits error, so the driver finishes loading broken; the k parsed ps_priv states leak (num_ps==0), and teardown (ni.c:2489 -> radeon_pm.c:1705 -> ni_dpm_fini:4273) kfrees the dangling pointer a SECOND time \u2014 a use-after-free free() that releases whichever foreign object has since reused the attacker-size-influenced chunk (sizeof(struct radeon_ps)*ucNumStates, ucNumStates a VBIOS byte). Harness proves all legs with DragonFlys own allocator: dangling publication [A], slab-reuse UAF with attacker-planted data consumed by ni_dpm_enable [B], teardown double free as silent bin corruption (same chunk handed to two live allocations) [C] and a live victim freed while in use [C2], corrected behavior with fix.diff [D]. No AMD GPU on the virtio-only guest (env.txt), so the kernel-context corruption is hardware-gated; exploitation beyond the demonstrated double-alloc primitive would additionally need timed heap pressure at load and victim grooming \u2014 plausible on real HW, not demonstrated here.", "exploit_chain": "Demonstrated: crafted multi-state VBIOS + kzalloc failure at parse iteration k -> dangled boot_ps/current_ps/requested_ps -> (i) UAF read chain feeding reused-chunk (attacker-influenced) data into the DPM state machine, (ii) teardown kfree of the dangling pointer releasing a live foreign object in an attacker-chosen size bucket, (iii) double-alloc alias of one chunk to two live allocations. NOT demonstrated (hardware-gated on this guest): slab groom -> victim object -> arbitrary write -> uid=0. Trigger needs crafted VBIOS AND timed allocation failure, hence Medium not High.", "evidence": "run.log [A] (DANGLING assignment + num_ps=0 leak), [B] (SAME-chunk reuse, mclk=0xdeadbee consumed), [C] (x1==x2 ALIAS: same chunk handed out twice), [C2] (victim freed while live, double-alloc), [D] (fixed path); harness.c (failure-injected verbatim lifetime sequence); fix_build.log (patched ni_dpm.o -Werror clean); env.txt (virtio-only guest)", "kernel_refs": [ "sys/dev/drm/radeon/ni_dpm.c:4020", "sys/dev/drm/radeon/ni_dpm.c:4022", "sys/dev/drm/radeon/ni_dpm.c:4041", "sys/dev/drm/radeon/ni_dpm.c:3914", "sys/dev/drm/radeon/ni_dpm.c:4270", "sys/dev/drm/radeon/ni_dpm.c:4273", "sys/dev/drm/radeon/radeon_pm.c:1448", "sys/dev/drm/radeon/radeon.h:2793", "sys/dev/drm/radeon/ni.c:2432", "sys/dev/drm/radeon/ni.c:2489" ], "poc_changes": "No seed existed; harness written from scratch. Iterations: errno.h include; failure-injection flag initially not wired into the replica (fixed); stdio flush before fork/exit so child proof output is captured.", "attempts": 4, "guest_uname": "DragonFly dfbsd 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": 75, "guest_dirty": 1, "build_cmd": "cc -O2 -Wall -o harness harness.c", "run_cmd": "./harness", "code_hash": "ba173e571cf64cad60750ef65f4becb6726363f614aea33fceb7fa4bf1b20bdc", "notes": "Pass-2 finding for sys/dev/drm/radeon/ni_dpm.c (Dillon bucket memcorrupt). Ignored-init multiplier identical to ci DF-3645 (Medium): radeon_pm.c:1448 + ni.c:2432. The shared radeon_pm.c ignore-init bug is documented here as multiplier; the per-file fix (NULL the pointers, unwind ps_privs) is sufficient to kill the ni instance.", "recommended_fix": "On the kzalloc-failure path of ni_parse_power_table: free already-parsed ps_priv states, then NULL rdev->pm.dpm.{ps,boot_ps,uvd_ps} before returning -ENOMEM; make ni_dpm_fini NULL-safe and clear the pointers (see fix.diff).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fixed (compile + harness level): fix.diff applies clean alone and stacked with DF-3754s fix, compiles -Werror clean as a kernel object (make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC ni_dpm.o), and harness [D] shows the corrected lifetime (no dangling, no leak, no double free). Live-HW test impossible on the virtio-only guest.", "fix_evidence": "fix_build.log (patched ni_dpm.c -Werror compile, NULL-ing present at :4032/:4287); fix_run.log ([D] clean unwind); guest /usr/src restored to pristine and verified by md5 after validation." } |