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

Dangling rdev->pm.dpm.ps after kfree-without-NULL in trinity_parse_power_table (:1781) + swallowed trinity_dpm_init error -> double free in trinity_dpm_fini (:2086-2089) and UAF via boot_ps at enable; ps_priv[0..i-1] leaked β€” Trinity twin of DF-4023/DF-3973/DF-4117/DF-4125

Summary

On per-state kzalloc failure the parser kfrees dpm.ps without NULLing and leaks the attached ps_priv states; num_ps only set on success so fini cannot free them. The init error propagates but radeon_pm.c:1448 discards it and keeps the device alive; if a BOOT state was parsed before the failure, boot_ps points into the freed array and enable dereferences freed memory. At unload radeon_dpm_fini runs unconditionally -> fini kfrees the dangling ps again -> heap double-free/freelist corruption. Unwind + idempotent-fini fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4144 Β· 19 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
verdict.json β€” 5.7 KB view raw
manifest.json β€” 1.2 KB view raw
fix.diff β€” 1.6 KB view raw
build.log β€” 8.6 KB view raw
run.log β€” 12.5 KB view raw
run.2.log β€” 12.5 KB view raw
env.txt β€” 1.0 KB view raw
rig/gen_rig.sh β€” 15.5 KB view raw
rig/check_verbatim.sh β€” 3.0 KB view raw
rig/Makefile β€” 690 B ↓ download
rig/build.sh β€” 502 B view raw
rig/run.sh β€” 834 B view raw
rig/trinity_slice_verbatim.c β€” 54.8 KB view raw
rig/trinity_rig.c β€” 35.2 KB view raw
rig/rig_shims.h β€” 7.8 KB view raw
rig/radeon_structs_verbatim.h β€” 7.1 KB view raw
rig/atom_defs_verbatim.h β€” 20.9 KB view raw

DF-4144 β€” Dangling rdev->pm.dpm.ps after kfree-without-NULL at trinity_parse_power_table:1781 β†’ double free in trinity_dpm_fini:2089, UAF via boot_ps at enable, ps_priv[0..i-1] leak

  • File: sys/dev/drm/radeon/trinity_dpm.c
  • Class: kernel heap double-free / use-after-free / leak (CWE-415/CWE-416)
  • Verified via the verbatim-code rig (rig/): production slice byte-identical to the guest's /usr/src/sys (68 blocks, build.log); driven with a crafted Trinity DPM image and a deterministic fault-injecting allocator that fails the 2nd per-state kzalloc(sizeof(struct sumo_ps)), poisons freed allocations (PROT_NONE) and aborts on double free.
  • Hardware gating: no AMD ASIC on the audit guest (env.txt) β€” kernel-context consequence of the demonstrated primitives is heap corruption / panic at driver unload (reboot or module unload) under the crafted-VBIOS / memory-pressure threat model.

What the rig demonstrates (baseline build = production code)

mode result
4144-doublefree state0 (BOOT) parses, state1 kzalloc fails β†’ verbatim parser kfrees dpm.ps at :1781 WITHOUT NULLing (num_ps still 0, ps_priv[0] still attached, boot_ps still points into the array); the rig's allocator confirms the pointer is freed while rdev still holds it; the unload path (radeon_pm.c:1705 radeon_dpm_fini, unconditional) runs the verbatim trinity_dpm_fini β†’ kfree(rdev->pm.dpm.ps) at :2089 frees it a SECOND time β†’ RESULT: DOUBLEFREE (rc=89); the leak report shows state 0's ps_priv (128 B) never freed because num_ps==0 hides it from fini
4144-uaf same injection; instead of unload, the rig simulates radeon_pm.c:1448 discarding the init error and :1453 calling enable β†’ the verbatim trinity_dpm_enable reads boot_ps->ps_priv at :1129 β†’ RESULT: FAULT ... USE-AFTER-FREE in freed allocation ... (+40 bytes in) β€” the +40 is exactly offsetof(radeon_ps, ps_priv)

Fixed build (this finding's fix.diff, A/B on the identical image)

The parser error paths unwind (ps_priv[0..i-1] freed BEFORE dpm.ps is freed, then dpm.ps/boot_ps/uvd_ps NULLed), and trinity_dpm_fini is idempotent (ps=NULL, num_ps=0, priv=NULL after frees). Fixed run: inject β†’ init -ENOMEM β†’ fini clean, no double free, zero leaked allocations, and enable returns -EINVAL (the DF-4143 guard sees the NULLed boot_ps) β€” no UAF.

Build / Run (on the DragonFly guest)

cd rig && sh ./build.sh /usr/src DF-4144     # verbatim + fix-lines checks, cc
cd rig && sh ./run.sh                        # 14 modes x both builds

Verdict

Reproduced (verbatim-rig): the exact production code performs a kernel-equivalent double free at unload and a UAF read through the dangling boot_ps at enable; fix.diff (unwind + idempotent fini) removes both and the leak. See VERDICT.md, verdict.json.

VERDICT.md
↓ download raw

DF-4144 β€” VERDICT

REPRODUCED via the verbatim-code rig on the DragonFly guest (same tree fidelity proof as DF-4143: 68 blocks identical to /usr/src/sys).

Root cause (confirmed)

trinity_parse_power_table (trinity_dpm.c:1734-1821):

  • On per-state kzalloc(sizeof(struct sumo_ps)) failure (:1780) the parser executes kfree(rdev->pm.dpm.ps); return -ENOMEM; (:1781-1782) WITHOUT NULLing rdev->pm.dpm.ps, WITHOUT freeing the already-attached ps_priv[0..i-1], and with num_ps unset (:1806 runs only on success). If a BOOT-classified state was parsed before the failure, boot_ps (set at :1705) points into the freed array.
  • trinity_dpm_init propagates the error, but radeon_pm_init_dpm discards it (radeon_dpm_init(rdev); radeon_pm.c:1448) and keeps the device alive.
  • At unload radeon_pm_fini_dpm calls radeon_dpm_fini(rdev) UNCONDITIONALLY (radeon_pm.c:1705) β†’ trinity_dpm_fini (:2080-2092): the num_ps loop frees nothing (num_ps==0), then kfree(rdev->pm.dpm.ps) at :2089 frees the already-freed pointer β†’ double free; ps_priv[0] is leaked.

Rig observations (baseline, decisive lines)

  • 4144-doublefree: RIG: init rc=-12; dpm.ps=0x80047e000 (kfreed at :1781, pointer NOT NULLed), num_ps=0, boot_ps=0x80047e000 (points INTO the freed array); RIG: allocator confirms dpm.ps ... is FREED while rdev still holds it; after the verbatim fini: RESULT: DOUBLEFREE ptr=0x80047e000 (allocation of 96 bytes freed a 2nd time) (96 = 2 Γ— sizeof(struct radeon_ps)=48, both verbatim-verified). Allocator leak report shows the 128-byte ps_priv[0] allocation live at exit (leaked).
  • 4144-uaf: with the swallow simulated (radeon_pm.c:1448-1453), the verbatim enable's trinity_update_current_ps(rdev, boot_ps) (:1129) reads boot_ps->ps_priv: RESULT: FAULT addr=0x80047e028 USE-AFTER-FREE in freed allocation 0x80047e000..0x80047e060 (+40 bytes in, alloc size 96) β€” +40 is exactly offsetof(struct radeon_ps, ps_priv), i.e. the literal rps->ps_priv read of trinity_get_ps over freed heap.

In kernel context: the double free corrupts the slab freelist (panic or worse on the next allocation of that bucket), and the UAF read feeds pi->current_ps = *new_ps β€” a 92-byte copy from freed memory. Both trigger under the crafted-VBIOS / memory-pressure (kzalloc failure) threat model; the injector models the kernel's genuine allocation-failure path.

Fix validation (A/B, identical image + identical injection)

fix.diff: (1) error unwinds at the three failure sites β€” free ps_priv[i-1..0] while dpm.ps is still valid, then NULL dpm.ps, boot_ps, uvd_ps after the production kfree; (2) idempotent fini (dpm.ps = NULL; num_ps = 0; priv = NULL; after the frees). Fixed run: RESULT: OK (fixed) unwind + idempotent fini, no double free, no leak (allocator report: zero live allocations), and the follow-on enable returns -EINVAL (DF-4143 guard + NULLed boot_ps) β€” no UAF. The checker proves the APPLY_DF4144_FIX lines are exactly the fix.diff additions (build.log, 27 lines). Kernel-rebuild validation not performed (path executes only on AMD ASICs, absent from the guest); validated in the rig per the established protocol. fix.diff authored against pristine sys/, never applied.

Honest limitations

  • kzalloc-failure injection models memory pressure; on the target hardware the same code path is reachable with a large crafted ucNumEntries (255 Γ— 128 B per state) at attach.
  • The num_ps==0 leak-report is rig-allocator evidence; in-kernel the leak is 128 B per parsed state per failed probe (re-probe loops make it cumulative).

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

A/B validated in the verbatim rig on the identical crafted image and kzalloc injection: baseline aborts with DOUBLEFREE (rc=89) / UAF fault (rc=88) and leaks 128 B; fixed build unwinds cleanly (allocator: zero live), fini is idempotent, and enable returns -EINVAL via the NULLed boot_ps -- no fault of any class. Kernel-rebuild validation not performed (path executes only on AMD ASICs, absent from this guest); per the verbatim-rig protocol the fixed slice was validated in the same rig.

run.log FIXED sections (RESULT: OK (fixed) unwind + idempotent fini, no double free, no leak / no UAF: dangling boot_ps cleared); build.log ('DF-4144 fix lines == fix.diff additions (27 lines)'); fix.diff
↓ fix.diffper-fix-DF-4144

Confirmed kernel references

Detail

Exploit chain

Crafted VBIOS with a BOOT-classified state followed by enough states to hit a per-state kzalloc failure (255 ucNumEntries x 128 B makes that reachable at attach without global pressure) -> trinity_parse_power_table:1781 kfrees dpm.ps (dangling pointer kept; ps_priv[0..i-1] leaked; boot_ps -> freed array) -> radeon_pm.c:1448 swallows -ENOMEM -> unload: radeon_pm.c:1705 radeon_dpm_fini -> trinity_dpm_fini:2089 kfree of the dangling pointer = heap double free (slab freelist corruption -> panic or worse on the next bucket allocation); alternatively enable (radeon_pm.c:1453) -> trinity_dpm_enable:1129 reads boot_ps->ps_priv from freed memory = use-after-free read feeding pi->current_ps copy. No unprivileged-userland trigger: attacker data is the GPU ROM; the kzalloc failure is kernel memory pressure modeled by the rig injector.

Evidence (decisive lines)

run.log (mode 4144-doublefree: RESULT: DOUBLEFREE ptr=... 96 bytes freed a 2nd time, rc=89; mode 4144-uaf: RESULT: FAULT addr=...e028 USE-AFTER-FREE ... (+40 bytes in); FIXED: no double free, no leak, enable -EINVAL), run.2.log (stability), build.log (68x identical + 'DF-4144 fix lines == fix.diff additions (27 lines)'), rig/ (sources; trinity_rig.c carries the guarded allocator with fault injection), VERDICT.md, env.txt

PoC changes

Seed pack had no runnable PoC (status untested, hardware-gated). Built the verbatim rig from scratch (shared with DF-4143/4145/4146); the novel piece for this finding is the deterministic fault-injecting guarded allocator (fails the nth kzalloc of a chosen size, poisons freed pages PROT_NONE, aborts with rc=89 on double free, reports live allocations for leak evidence) plus the radeon_pm.c:1448/1705 swallow/unload simulations.

Verified recommended fix

Unwind all trinity_parse_power_table error paths (free ps_priv[0..i-1], then NULL dpm.ps/boot_ps/uvd_ps after the production kfree) and make trinity_dpm_fini idempotent (ps=NULL, num_ps=0, priv=NULL) -- fix.diff, A/B validated.

Verdict

REPRODUCED via the verbatim-code rig (68 blocks byte-identical to the guest's /usr/src/sys; same fidelity proof as DF-4143). Baseline production code with the 2nd per-state kzalloc fault-injected (the kernel's genuine allocation-failure path): the verbatim trinity_parse_power_table kfrees rdev->pm.dpm.ps at :1781 without NULLing it, without freeing ps_priv[0..i-1], and with num_ps unset; boot_ps (set at :1705 for the BOOT state parsed before the failure) points into the freed array and the error is swallowed per radeon_pm.c:1448. The unconditional unload path (radeon_pm.c:1705) then runs the verbatim trinity_dpm_fini whose kfree(rdev->pm.dpm.ps) at :2089 frees the pointer a second time -- the rig allocator aborts with RESULT: DOUBLEFREE (96-byte allocation freed twice), and reports state 0's ps_priv (128 B) leaked (num_ps==0 hides it from fini's loop). Driving enable instead (the swallowed-error chain): the verbatim trinity_dpm_enable reads boot_ps->ps_priv at :1129 and faults at freed_dpm.ps+40 -- +40 is exactly offsetof(struct radeon_ps, ps_priv), a literal use-after-free read that in-kernel feeds a 92-byte struct copy from freed heap. Gating: no AMD GPU on the audit guest (env.txt); in-kernel consequence is slab freelist corruption (double free) / UAF at driver unload or enable, under the crafted-VBIOS or memory-pressure threat model. fix.diff (error-path unwinding + idempotent fini) A/B-validated on the identical image and injection: no double free, zero leaked allocations, enable returns -EINVAL via the NULLed boot_ps.