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)
PoC verification
Evidence pack
findings/poc/DF-4144 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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-statekzalloc(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.
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 executeskfree(rdev->pm.dpm.ps); return -ENOMEM;(:1781-1782) WITHOUT NULLingrdev->pm.dpm.ps, WITHOUT freeing the already-attachedps_priv[0..i-1], and withnum_psunset (: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_initpropagates the error, butradeon_pm_init_dpmdiscards it (radeon_dpm_init(rdev);radeon_pm.c:1448) and keeps the device alive.- At unload
radeon_pm_fini_dpmcallsradeon_dpm_fini(rdev)UNCONDITIONALLY (radeon_pm.c:1705) βtrinity_dpm_fini(:2080-2092): the num_ps loop frees nothing (num_ps==0), thenkfree(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-byteps_priv[0]allocation live at exit (leaked).4144-uaf: with the swallow simulated (radeon_pm.c:1448-1453), the verbatim enable'strinity_update_current_ps(rdev, boot_ps)(:1129) readsboot_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 literalrps->ps_privread 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==0leak-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
fixedA/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
Confirmed kernel references
- sys/dev/drm/radeon/trinity_dpm.c:1779
- sys/dev/drm/radeon/trinity_dpm.c:1781
- sys/dev/drm/radeon/trinity_dpm.c:1806
- sys/dev/drm/radeon/trinity_dpm.c:2086
- sys/dev/drm/radeon/trinity_dpm.c:2089
- sys/dev/drm/radeon/trinity_dpm.c:1705
- sys/dev/drm/radeon/trinity_dpm.c:1129
- sys/dev/drm/radeon/radeon_pm.c:1448
- sys/dev/drm/radeon/radeon_pm.c:1705
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.
No comments yet.