Dangling rdev->pm.dpm.ps after alloc-failure kfree in ni_parse_power_table: ignored-error UAF publication, ps_priv leak, and teardown double free (use-after-free kfree of a live foreign object)
Summary
When a per-state kzalloc fails at iteration k, ni_parse_power_table kfrees rdev->pm.dpm.ps without NULLing it and returns -ENOMEM before num_ps is assigned. radeon_pm_init_dpm IGNORES the dpm.init return and immediately publishes current_ps=requested_ps=boot_ps - which points INTO the freed array when a BOOT state was parsed before the failure - then runs radeon_dpm_enable through the UAF read chain. ni.c swallows radeon_pm_init's error so the driver loads broken; the k parsed ps_priv states leak permanently, and teardown (unguarded by dpm_enabled) runs ni_dpm_fini's kfree a SECOND time on the dangling pointer - releasing whichever object has reused the chunk (size attacker-chosen via ucNumStates -> kmalloc bucket). HARNESS with DragonFly's own allocator and failure injection: dangling publication; SAME-chunk slab reuse with attacker-planted pointer consumed by ni_dpm_enable; teardown double free proven as silent bin corruption (chunk handed to two live allocations; live victim freed while in use); fixed path clean. Local attacker with crafted multi-state VBIOS plus heap-pressure timing: UAF read chain, permanent leak, and double free of a live foreign object - kernel heap corruption with privesc potential. Dual precondition keeps it Medium, matching DF-3645. Fix compile-validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3755 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 12.7 KB | view raw | |
| build.sh | β | 88 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 26 B | view raw | |
| run.log | β | 2.0 KB | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_build.log | β | 1.4 KB | view raw | |
| fix_run.log | β | 323 B | view raw | |
| env.txt | β | 779 B | view raw | |
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 6.3 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.9 KB | view raw |
DF-3755 β dangling rdev->pm.dpm.ps + double free + UAF on the alloc-failure path of ni_parse_power_table
File: sys/dev/drm/radeon/ni_dpm.c (pass 2)
Family: radeon twin of si DF-3611 / ci DF-3645 (amdgpu DF-3588 / DF-3624)
Build
cc -O2 -Wall -o harness harness.c
(No AMD NI GPU on the QEMU audit guest β virtio-only β so verification is a userspace logic harness replicating the exact lifetime sequence with malloc/free and the same allocator-reuse semantics kmalloc has; see env.txt.)
Run
./harness
Expected output (decisive lines)
[A] ... current_ps = requested_ps = boot_ps = 0x... (DANGLING: ps array freed at ni_dpm.c:4022)
num_ps = 0 (never set -> ps_priv[0..4] leak permanently)
[B] ... victim alloc (384 bytes) = 0x... (reuse: SAME chunk)
ni_dpm_enable consumed reused-chunk data: mclk=0xdeadbee
[C] ... post-corruption allocations: x1=0x... x2=0x... -> ALIAS (same chunk handed out twice)
[C2] ... victim freed while live; next allocation overlays it
[D] ... rc=-ENOMEM propagated; dpm.ps = NULL; ps_privs freed; no double free, no leak
[A] the parse failure frees the state array and the (ignored) init error
lets radeon_pm_init_dpm publish the dangling pointer; [B] the UAF read
through boot_ps after slab reuse; [C]/[C2] the teardown double free
proven as silent allocator corruption (live alias / victim freed while
live); [D] the fixed path.
Fix
fix.diff β on the kzalloc-failure path free the already-parsed ps_priv
states, NULL dpm.ps/boot_ps/uvd_ps (kills dangling + double free +
leak), and make ni_dpm_fini NULL-safe. Validated: applies cleanly (alone
and stacked with DF-3754's fix), compiles -Werror clean as a kernel
object (make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC ni_dpm.o,
see fix_build.log), harness [D] shows the corrected behavior.
DF-3755 β Verdict
Verdict: REPRODUCED (source-level + harness) β dangling-pointer UAF + teardown double free + ps_priv leak; kernel-heap corruption potential, escalation not demonstrated
radeon (Northern Islands) twin of si DF-3611 / ci DF-3645 (amdgpu DF-3588 / DF-3624).
Bug confirmation
ni_parse_power_table() frees the state array when a per-state kzalloc
fails, and leaves every published pointer dangling:
ni_dpm.c:4020 ps = kzalloc(sizeof(struct ni_ps), GFP_KERNEL);
ni_dpm.c:4021 if (ps == NULL) {
ni_dpm.c:4022 kfree(rdev->pm.dpm.ps); // freed, NOT nulled
ni_dpm.c:4023 return -ENOMEM; // num_ps never set
ni_dpm.c:4041 rdev->pm.dpm.num_ps = ...; // only on success
Three consequences:
-
Ignored-error amplification.
radeon_pm_init_dpm()discards thedpm.initreturn (radeon_pm.c:1448 βradeon_dpm_init(rdev);is a bare statement, macro at radeon.h:2793) and immediately runs radeon_pm.c:1449current_ps = requested_ps = boot_ps. If a BOOT-classified state was parsed before the failing iteration,boot_pspoints INTO the freed array (ni_parse_pplib_non_clock_infoassigned&rdev->pm.dpm.ps[i]at ni_dpm.c:3914-3915), so the published current/requested/boot pointers all dangle.radeon_dpm_enable()(radeon_pm.c:1453) then executes the UAF read chainni_dpm_enable:3589/3641 -> ni_init_smc_table:1943/1975 -> ni_get_ps(freed)->ps_priv -> ni_ps through the stale ps_priv pointer(thestruct ni_psprivs themselves stay allocated, so the read chain usually returns stale-but-mapped data β until the slab is reused, then attacker-influenced garbage drives the DPM state machine).radeon_pm_init's error is swallowed a second time by the asic init (ni.c:2432 radeon_pm_init(rdev);β value ignored), so the driver finishes loading in the broken state. -
Leak.
num_psstays 0, so the k already-parsedps_privallocations are never freed (ni_dpm_fini's loop runs 0 times). -
Double free at teardown. Driver removal/shutdown runs
radeon_device_fini -> radeon_fini -> ni.c:2489 radeon_pm_fini -> radeon_pm.c:1705 radeon_dpm_fini(NOT guarded bydpm_enabled) ->ni_dpm_fini:
c
ni_dpm.c:4270 for (i = 0; i < rdev->pm.dpm.num_ps; i++) // 0 iterations
ni_dpm.c:4273 kfree(rdev->pm.dpm.ps); // SECOND free
Between the first free (driver load) and the second (teardown) the slab
object can be reallocated by any kernel subsystem, so this kfree
releases a live foreign object β classic exploitable double-free shape.
The chunk size is attacker-influenced:
kzalloc(sizeof(struct radeon_ps) * ucNumStates) with ucNumStates
a VBIOS byte (2..255 -> ~96 B..~12 KB, selectable kmalloc bucket).
Harness confirmation
run.log decisive lines:
[A] current_ps = requested_ps = boot_ps = 0x8004a3120 (DANGLING: ps array freed at ni_dpm.c:4022)
num_ps = 0 (never set -> ps_priv[0..4] leak permanently)
[B] victim alloc (384 bytes) = 0x8004a30c0 (reuse: SAME chunk)
ni_dpm_enable consumed reused-chunk data: mclk=0xdeadbee
[C] post-corruption allocations: x1=0x8004a30c0 x2=0x8004a30c0 -> ALIAS (same chunk handed out twice)
[C2] victim = 0x8004a30c0 == freed dpm.ps = 0x8004a30c0: REUSED
after kfree(victim): next malloc returns 0x8004a30c0 == victim: YES (double-alloc)
[D] rc=-ENOMEM propagated; dpm.ps = NULL; ps_privs freed; no double free, no leak
The harness replicates the exact lifetime sequence (kzalloc-failure injection at parse iteration 5 of a BOOT-bearing 8-state table) and proves each consequence with DragonFly's own allocator: the dangling publication [A], attacker-controlled data flowing through the UAF read after slab reuse [B], the teardown double free as silent bin corruption β the same chunk handed out twice to live allocations [C] and a live victim freed while in use [C2] β and the fully corrected behavior with fix.diff [D].
Trigger conditions (not met on this guest)
- AMD NI GPU + radeon DPM (default on) β QEMU guest is virtio-only (env.txt: no 0x1002 PCI device; radeon.ko + radeonkmsfw_* present).
- Crafted VBIOS PowerPlayInfo table with >= 2 states (ucNumStates >= 2)
so at least one
ps_privkzalloc succeeds, and kernel heap pressure making a laterkzalloc(sizeof(struct ni_ps))fail mid-parse. - For the double free: driver unload/reboot after the failed load.
The kzalloc-failure requirement is what keeps this at Medium (family precedent: si twin DF-3611 Low without the ignored-init multiplier; ci twin DF-3645 Medium with it β ni has the same multiplier as ci: radeon_pm.c:1448 + ni.c:2432).
Exploit chain
Demonstrated to the allocator-corruption boundary (live-alias double
allocation / foreign-object release) in the harness. A kernel-heap
exploit chain would additionally need: real-HW trigger timing (heap
pressure at exactly the right kzalloc), a reclaiming victim object in the
chosen sizeof(struct radeon_ps)*ucNumStates bucket, and a follow-up
allocation pattern β plausible on real hardware but NOT demonstrated
here (no NI GPU on the audit guest). Realistic impact ceiling: kernel
heap corruption -> potential local privilege escalation; realistic
likelihood: low (dual requirement of crafted vBIOS + timed allocation
failure).
Fix validation
fix.diff:
- parse error path frees the already-parsed ps_priv states (kills the
leak), then NULLs dpm.ps / boot_ps / uvd_ps (kills the dangling
publication AND the double free);
- ni_dpm_fini becomes NULL-safe and re-clears the pointers.
patch -p1 clean against pristine source alone and stacked with
DF-3754's fix; compiles -Werror clean as a kernel object
(make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC ni_dpm.o,
fix_build.log); harness [D] shows the corrected lifetime. Guest
/usr/src restored to pristine afterwards (verified by md5 + grep).
fix_status: fixed (compile + harness-level); live HW test impossible on
this guest.
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
fixedfixed (compile + harness level): fix.diff applies clean alone and stacked with DF-3754's 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_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.
Confirmed kernel references
- 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
Detail
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 (decisive lines)
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)
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.
Verified 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).
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) β boot_ps points INTO the freed array when a BOOT state was parsed before the failing iteration β 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_init's 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 β 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 DragonFly's 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 β plausible on real HW, not demonstrated here.
No comments yet.