Double free of rdev->pm.dpm.ps (plus leak of parsed ps_priv states) on the allocation-failure path of si_parse_power_table β radeon twin of amdgpu DF-3588
Summary
When a ni_ps kzalloc fails mid-parse, si_parse_power_table kfrees rdev->pm.dpm.ps without NULLing it, leaves num_ps==0 (leaking already-parsed ps_priv allocations) and returns -ENOMEM. radeon_pm_init_dpm ignores the init return; on driver unload radeon_pm_fini_dpm calls radeon_dpm_fini UNCONDITIONALLY (no dpm_enabled check), so si_dpm_fini kfrees the dangling pointer again - double free of a heap block that intervening allocations have recycled (live-object free / cross-object UAF). VERIFIED: harness with fault-injected alloc failure - after the buggy free a same-size victim recycles the block, fini's second free releases the live victim, next allocation aliases it and writes through the second owner clobber the first ('>>> DOUBLE FREE of a LIVE object <<<'); fixed-path clean. OOM/fault-injection-gated heap corruption on SI radeon systems at driver unload after failed DPM init.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3611 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 5.9 KB | view raw | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 11 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| fix.diff | β | 862 B | view raw | |
| fix_build.log | β | 1.6 KB | view raw | |
| fix_run.log | β | 262 B | view raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| README.md | β | 1.9 KB | β raw | |
| manifest.json | β | 789 B | view raw | |
| verdict.json | β | 3.4 KB | view raw |
DF-3611 β Double free of rdev->pm.dpm.ps (and leak of parsed ps_priv states) on the allocation-failure path of si_parse_power_table
Severity: Low Confidence: certain Status: reproduced (harness) Impact: dos
What
si_parse_power_table kfrees rdev->pm.dpm.ps at :6855 when a ni_ps kzalloc fails, but leaves the pointer dangling, num_ps==0 (so the earlier ps_priv allocations leak) and returns -ENOMEM. radeon_pm_init_dpm:1448 ignores the init return; on driver unload si.c:6945 -> radeon_pm_fini:1705 calls radeon_dpm_fini UNCONDITIONALLY (no dpm_enabled check) -> si_dpm_fini:7085 kfrees the dangling pointer: double free of a block that the heap has meanwhile recycled -> live-object free / cross-object UAF. Harness replicates the sequence with fault injection: the freed dpm.ps block is recycled by a live victim object; fini's second free releases it; the next allocation aliases it and writes through the second owner clobber the first owner's data (observed marker corruption). OOM-gated (needs the ni_ps kzalloc to fail while later allocs succeed), same trigger class and Low severity as the amdgpu twin.
Reproduce
sh build.sh # cc -O2 -Wall -o harness harness.c sh run.sh # prints [A] buggy-path behaviour, [B] fixed-path behaviour
Expected decisive output (see run.log): the [A] section shows the fault or
corruption the kernel would take; the [B] section shows the behaviour with
fix.diff applied.
No AMD GPU is attached to the audit QEMU guest (virtio-only), so the radeon DPM path cannot be exercised live; the harness replicates the exact kernel expressions with crafted-VBIOS inputs (house methodology, cf. DF-1137..1140).
Fix
fix.diff β validated: git apply --check clean; make si_dpm.o in
sys/dev/drm/radeon with KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC
compiles -Werror clean with all five pass-2 fixes stacked; harness [B]
shows the primitive gone.
DF-3611 β Verdict
Verdict: REPRODUCED (source-level + harness) β double free of a recycled heap block (cross-object UAF), OOM-gated
radeon twin of amdgpu DF-3588.
Bug confirmation
si_dpm.c:6840 rdev->pm.dpm.ps = kzalloc(sizeof(struct radeon_ps) * ucNumEntries);
si_dpm.c:6845 for (i = 0; i < state_array->ucNumEntries; i++) {
...
si_dpm.c:6853 ps = kzalloc(sizeof(struct ni_ps), GFP_KERNEL);
si_dpm.c:6854 if (ps == NULL) {
si_dpm.c:6855 kfree(rdev->pm.dpm.ps); // freed, NOT NULLed,
si_dpm.c:6856 return -ENOMEM; // num_ps still 0,
// ps_priv[0..i-1] LEAK
si_dpm_init propagates the -ENOMEM, but the caller ignores it:
radeon_pm.c:1448 radeon_dpm_init(rdev); // rc discarded
radeon_pm.c:1453 ret = radeon_dpm_enable(rdev); // fails later, ignored path
and teardown frees the same pointer unconditionally:
si.c:6945 radeon_pm_fini(rdev)
radeon_pm.c:1723-1724 pm_method == PM_METHOD_DPM -> radeon_pm_fini_dpm
radeon_pm.c:1705 radeon_dpm_fini(rdev); // no dpm_enabled check
si_dpm.c:7085 kfree(rdev->pm.dpm.ps); // SECOND free
Between init failure and fini, ordinary kernel allocation churn recycles the freed block (same size class). The fini-time kfree then frees whatever object now occupies the block -> next allocation returns it to a second owner -> use-after-free with cross-object type confusion.
Harness confirmation
run.log (fault-injected kzalloc failure at state 2 of 4):
[heap churn] victim object now lives at 0x8004904a0 (SAME block - recycled)
[unload] si_dpm_fini: for(i<num_ps=0) skipped; kfree(dpm.ps=0x8004904a0)
>>> DOUBLE FREE of a LIVE object <<<
next allocation gets same block at 0x8004904a0; write via 2nd owner:
victim->marker = 0x4141414141414141 (owner #1 view)
other->marker = 0x4141414141414141 (owner #2 view)
victim->marker was silently CLOBBERED -> use-after-free
The harness also prints the secondary defect: 2 leaked ni_ps allocations
(the already-parsed states) because num_ps is only set after the loop.
Trigger conditions
- SI GPU + radeon DPM (absent on the audit guest).
kzalloc(sizeof(struct ni_ps))failure mid-parse while the surrounding allocations succeed β OOM / kmalloc fault injection (vm.zone_allocatelimits, KMEM_DEBUG). Low practical likelihood => severity Low, matching the amdgpu twin (DF-3588).
Exploit chain
If triggered under controlled memory pressure, the primitive is a same-size- class cross-object UAF; the freed block sits inside rdev->pm.dpm lifecycle, freeable again at driver unload (or GPU reset -> radeon_pm_fini). No demonstrated uid=0 chain (OOM-gated trigger, unprivileged users do not control it directly).
Fix
fix.diff:
- error path frees the already-parsed ps_priv states, frees dpm.ps and
NULLs it before returning -ENOMEM;
- si_dpm_fini guards on rdev->pm.dpm.ps and re-NULLs after free.
Validation: git apply --check OK; make si_dpm.o -Werror clean with all
five fixes stacked; harness [B] shows the fixed error path (0 leaked bytes,
dangling pointer gone, fini no-op).
fix_status: fixed (compile + harness-level).
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
fixedgit apply --check OK (standalone and stacked with the other four si_dpm.c fixes); targeted module TU compile make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC si_dpm.o => rc=0, -Werror clean (87080 B). Harness fixed-path shows the primitive gone (fix.diff frees the already-parsed ps_priv states, NULLs rdev->pm.dpm.ps on the error path, and guards si_dpm_fini with if (rdev->pm.dpm.ps). git apply --check OK; stacked module TU compile -Werror clean; harness [B] clean.)
fix.diff frees the already-parsed ps_priv states, NULLs rdev->pm.dpm.ps on the error path, and guards si_dpm_fini with `if (rdev->pm.dpm.ps)`. git apply --check OK; stacked module TU compile -Werror clean; harness [B] clean.
Confirmed kernel references
Detail
Exploit chain
kzalloc failure at state i -> kfree(dpm.ps), pointer dangling, privs leaked -> init rc ignored -> heap recycles block -> driver unload -> radeon_dpm_fini unconditional -> kfree(dpm.ps) again -> live object freed -> UAF/type confusion for any same-size-class object
Evidence (decisive lines)
run.log [A]: 'victim object now lives at 0x8004904a0 (SAME block - recycled)' ... '>>> DOUBLE FREE of a LIVE object <<<' ... 'victim->marker was silently CLOBBERED -> use-after-free'; [B]: 'si_dpm_fini: dpm.ps == NULL -> no-op (no double free)'
PoC changes
n/a (pass-2 finding authored with its harness; no seeded PoC to fix)
Verified recommended fix
see fix.diff
Verdict
radeon twin of amdgpu DF-3588. si_parse_power_table kfrees rdev->pm.dpm.ps at :6855 when a ni_ps kzalloc fails, but leaves the pointer dangling, num_ps==0 (so the earlier ps_priv allocations leak) and returns -ENOMEM. radeon_pm_init_dpm:1448 ignores the init return; on driver unload si.c:6945 -> radeon_pm_fini:1705 calls radeon_dpm_fini UNCONDITIONALLY (no dpm_enabled check) -> si_dpm_fini:7085 kfrees the dangling pointer: double free of a block that the heap has meanwhile recycled -> live-object free / cross-object UAF. Harness replicates the sequence with fault injection: the freed dpm.ps block is recycled by a live victim object; fini's second free releases it; the next allocation aliases it and writes through the second owner clobber the first owner's data (observed marker corruption). OOM-gated (needs the ni_ps kzalloc to fail while later allocs succeed), same trigger class and Low severity as the amdgpu twin.
No comments yet.