β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3755

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3755 Β· 13 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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:

  1. Ignored-error amplification. radeon_pm_init_dpm() discards the dpm.init return (radeon_pm.c:1448 β€” radeon_dpm_init(rdev); is a bare statement, macro at radeon.h:2793) and immediately runs radeon_pm.c:1449 current_ps = requested_ps = boot_ps. If a BOOT-classified state was parsed before the failing iteration, boot_ps points INTO the freed array (ni_parse_pplib_non_clock_info assigned &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 chain ni_dpm_enable:3589/3641 -> ni_init_smc_table:1943/1975 -> ni_get_ps(freed)->ps_priv -> ni_ps through the stale ps_priv pointer (the struct ni_ps privs 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.

  2. Leak. num_ps stays 0, so the k already-parsed ps_priv allocations are never freed (ni_dpm_fini's loop runs 0 times).

  3. 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 by dpm_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)

  1. AMD NI GPU + radeon DPM (default on) β€” QEMU guest is virtio-only (env.txt: no 0x1002 PCI device; radeon.ko + radeonkmsfw_* present).
  2. Crafted VBIOS PowerPlayInfo table with >= 2 states (ucNumStates >= 2) so at least one ps_priv kzalloc succeeds, and kernel heap pressure making a later kzalloc(sizeof(struct ni_ps)) fail mid-parse.
  3. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed (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.
↓ fix.diffper-fix-DF-3755

Confirmed kernel references

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.