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

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)

No comments yet.

PoC verification

Evidence pack

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

VERDICT.md
↓ download raw

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

  1. SI GPU + radeon DPM (absent on the audit guest).
  2. kzalloc(sizeof(struct ni_ps)) failure mid-parse while the surrounding allocations succeed β€” OOM / kmalloc fault injection (vm.zone_allocate limits, 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

fixed
baseline reproduced→ patch + rebuild →patched clean

git 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.
↓ fix.diffper-fix-DF-3611

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.