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

Dangling rdev->pm.dpm.dyn_state entries pointers after error-path kfree in r600_parse_extended_power_table β€” r600_free_extended_power_table at teardown kfrees them again (heap double free; root site shared by all six DPM family callers)

Summary

When a dependency-table parse fails, the error paths kfree the already-parsed tables' entries but never NULL the pointers. Every family caller propagates the error; the unconditional dpm fini at unload/reboot kfrees the same pointers a second time. The shared root site for the six DPM family callers (btc/ni/si/ci/kv/trinity) - family-calibrated with DF-3973 (btc caller-side only)/4023/4117/4125. Attacker model: hostile VBIOS + one induced allocation failure at attach. NULL-each + idempotent-fini fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4371 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
trigger.md β€” 1.9 KB ↓ raw
fix.diff β€” 3.1 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 2.7 KB view raw

DF-4371 β€” r600_parse_extended_power_table error-path kfree leaves dangling

dyn_state entries pointers β†’ double free in r600_free_extended_power_table

  • File: sys/dev/drm/radeon/r600_dpm.c (:939, :950-951, :962-964 dangling kfree sites; :1299-1315 the second free), triggered from every DPM family caller (btc/ni/si/ci/kv/trinity) and completing at driver teardown.
  • Severity: Medium β€” same family/calibration as DF-3973/DF-4023/DF-4117/DF-4125.
  • Class: CWE-416/CWE-459 (double free / dangling pointer on error path).

The bug

r600_parse_clk_voltage_dep_table() returns error only when its kzalloc fails (r600_dpm.c:829-831). When the second/third/fourth table's parse hits that failure, the error paths of r600_parse_extended_power_table() free the already-parsed tables' entries but leave the pointers dangling:

/* r600_dpm.c:936-941 (vddci-on-mclk failure frees vddc-on-sclk) */
if (ret) {
        kfree(rdev->pm.dpm.dyn_state.vddc_dependency_on_sclk.entries);
        return ret;                 /* pointer NOT set to NULL */
}

(same shape at :949-953 freeing two pointers, and :961-966 freeing three).

Every caller (e.g. btc_dpm.c:2577-2579, ni_dpm.c:4075-4077, si_dpm.c:6953-6955, kv_dpm.c:2731-2733, trinity_dpm.c:2001-2003, ci_dpm.c:5776-5779) propagates the error; dpm init fails. At unload or reboot radeon_pm_fini() calls radeon_dpm_fini() unconditionally (radeon_pm.c:1705 β€” no dpm_enabled guard), dispatching to e.g. btc_dpm_fini() (btc_dpm.c:2727) β†’ r600_free_extended_power_table() β†’ kfree() of the same, already-freed pointers β†’ heap double free.

Trigger (needs AMD hardware; documented, not runnable on audit VM)

  1. radeon GPU (BTC/NI/SI/CI/KV/Trinity family) whose VBIOS carries a pplib4+ PowerPlayInfo table with usVddcDependencyOnSCLKOffset != 0 and usVddciDependencyOnMCLKOffset != 0.
  2. Force the second parse's kzalloc to fail (kernel slab fault injection β€” failslab on the drm uma/slab, or genuine memory pressure at attach).
  3. Device attach fails; dpm_enabled = false.
  4. Unload/reboot β†’ double free of vddc_dependency_on_sclk.entries (or the 2/3-pointer variants).

Artifacts

  • trigger.md β€” the exact call chain and a fault-injection runbook
  • fix.diff β€” NULL every pointer after kfree (both in the error paths and in r600_free_extended_power_table, making the teardown free idempotent); verified patch -p1 --dry-run APPLIES_CLEAN
  • VERDICT.md β€” full analysis
VERDICT.md
↓ download raw

DF-4371 VERDICT β€” dangling dyn_state entries β†’ double free (r600_dpm.c error paths)

Bottom line

Certain by static proof; not reproduced at runtime β€” the audit VM has no radeon GPU, and the trigger additionally requires an induced allocation failure during VBIOS parsing. Same defect family as DF-3973 (caller-side btc instance), DF-4023/DF-4117/DF-4125 (ps pointer); this is the root site inside r600_dpm.c shared by all six family callers.

Why the double free is real (path:line)

  1. r600_dpm.c:927-931 β€” vddc_dependency_on_sclk parsed OK (entries = live kzalloc'd pointer).
  2. r600_dpm.c:936-941 β€” the vddci_dependency_on_mclk parse fails (its internal kzalloc at r600_dpm.c:829 returned NULL β†’ -ENOMEM): c if (ret) { kfree(rdev->pm.dpm.dyn_state.vddc_dependency_on_sclk.entries); return ret; } vddc_dependency_on_sclk.entries is now dangling; its count may also be stale from a previous partially-parsed attempt. Two-pointer variant at r600_dpm.c:949-953 (vddc-on-mclk failure), three- pointer variant at r600_dpm.c:961-966 (mvdd failure).
  3. Caller propagates: e.g. btc_dpm.c:2577-2579 ret = r600_parse_extended_power_table(rdev); if (ret) return ret; (identical: ni_dpm.c:4075, si_dpm.c:6953, kv_dpm.c:2731, trinity_dpm.c:2001, ci_dpm.c:5776-5779).
  4. radeon_pm_init dpm path fails β†’ rdev->pm.dpm_enabled = false (radeon_pm.c:1465,1470). Device init failure is not fatal for the driver session; teardown always follows at unload/reboot.
  5. radeon_pm.c:1705 β€” radeon_dpm_fini(rdev); runs unconditionally (no dpm_enabled check; line 1705 sits outside the `if (rdev-

    pm.dpm_enabled)` block that ends at 1704).

  6. β†’ btc_dpm_fini() (btc_dpm.c:2727) / ni_dpm_fini (ni_dpm.c:4276) / si_dpm_fini (si_dpm.c:7088) / kv_dpm_fini (kv_dpm.c:2870) / trinity_dpm_fini (trinity_dpm.c:2091) / ci_dpm.c:5713 β†’ r600_free_extended_power_table() (r600_dpm.c:1299-1315) β†’ kfree(dyn_state->vddc_dependency_on_sclk.entries); β€” second free of the same pointer.

DragonFly kfree() of an already-freed slab object is a double free / free-list corruption (uma zone poisoning may catch it as a panic; on a groomed heap it is a classic UAF-style corruption primitive). Precondition: attacker controls the VBIOS (hostile card / modified ROM / VFCT override via radeon_get_bios ACPI path radeon_bios.c:717) and can force one kzalloc failure at attach (failslab injection or real memory pressure).

Verification attempts on the audit VM

  • Guest: DragonFly 6.5-DEVELOPMENT x86_64, no GPU (pciconf: only QEMU virtio VGA 0x1111:1234), no radeon module loadable, no /dev/dri.
  • The radeon DPM attach path cannot run at all β†’ the parse/failure/fini chain is unreachable at runtime on this VM.
  • Static verification only, therefore status not_reproduced, impact none (on this guest). Confidence in the code defect itself: certain β€” every hop above is a direct citation.

Severity calibration

Identical trigger model and impact class as DF-3973 / DF-4023 / DF-4117 / DF-4125 (all Medium, CVSS:3.1/AV:P/AC:H/PR:L, CWE-416, memcorrupt bucket). Kept consistent: Medium, CVSS:3.1/AV:P/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H.

Fix

fix.diff: (a) NULL each pointer immediately after the error-path kfree (sites :939, :950-951, :962-964); (b) make r600_free_extended_power_table() idempotent by NULLing every pointer it frees β€” this also protects against the caller-side free-then-fini pattern (DF-3973's second path) reaching the same double free. Verified patch -p1 --dry-run β†’ APPLIES_CLEAN on a scratch copy. Runtime fix validation impossible on this VM (no radeon hardware): fix_status: not_testable.

kernel_refs

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.

trigger.md
↓ download raw

DF-4371 trigger (documentation β€” requires radeon hardware)

Runtime reproduction requires (1) an radeon DPM-family GPU whose VBIOS has a pplib4+ PowerPlayInfo table with both usVddcDependencyOnSCLKOffset and usVddciDependencyOnMCLKOffset set, and (2) one forced allocation failure inside the second/third/fourth r600_parse_clk_voltage_dep_table() call.

Call chain

radeon_device_init β†’ radeon_pm_init β†’ radeon_dpm_init β†’ <asic>_dpm_init
  β†’ r600_get_platform_caps            (r600_dpm.c:846)
  β†’ <asic>_parse_power_table
  β†’ r600_parse_extended_power_table   (r600_dpm.c:874)
      parse vddc_on_sclk OK           (:927-931)  entries allocated
      parse vddci_on_mclk FAILS       (:932-941)  kzalloc NULL at :829
          kfree(vddc_on_sclk.entries) (:939)      pointer dangling
          return -ENOMEM
<asic>_dpm_init returns error         (btc_dpm.c:2578 et al.)
radeon_pm_init: dpm_enabled = false   (radeon_pm.c:1470)
... driver unbind / reboot ...
radeon_pm_fini β†’ radeon_dpm_fini      (radeon_pm.c:1705, unconditional)
  β†’ <asic>_dpm_fini β†’ r600_free_extended_power_table (r600_dpm.c:1299)
      kfree(vddc_on_sclk.entries) AGAIN   <-- double free

Fault-injection runbook (on suitable hardware)

  1. Boot a kernel with slab fault injection available (DragonFly: debug sysctls / DEBUGFLAGS, or patch kzalloc call at r600_dpm.c:829 to fail on the 2nd invocation), or attach the card under heavy memory pressure.
  2. Load radeon with dpm=1 (default). Observe radeon: dpm initialization failed in dmesg with the device still present.
  3. kldunload radeon (or reboot). Observe the double-free: DragonFly's malloc flags with M_WAITOK + slab invariant checks panic on free-list corruption; without redzones the corruption is silent β€” check for subsequent unrelated panics/allocator reports.
  4. Expected on patched kernel (fix.diff): clean unload, no second free (pointers NULLed).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored post-verification; applies cleanly (patch -p1 --dry-run) to a scratch copy of r600_dpm.c; runtime before/after validation impossible on this VM (no radeon hardware)

findings/poc/DF-4371/fix.diff; dry-run output 'checking file sys/dev/drm/radeon/r600_dpm.c / DF4371_APPLIES_CLEAN'
↓ fix.diffper-fix-DF-4371

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (path:line proof of both frees and the unconditional fini); trigger.md (fault-injection runbook); fix.diff verified APPLIES_CLEAN (patch -p1 --dry-run, scratch copy)

PoC changes

No seed present; trigger documented as a runbook (hardware + fault injection required β€” no standalone userspace PoC can reach kernel VBIOS parsing)

Verified recommended fix

NULL every freed dyn_state pointer in the error paths and make r600_free_extended_power_table() idempotent by NULLing after each kfree (see fix.diff)

Verdict

Certain by static proof, not reproduced at runtime: the audit VM has no radeon GPU (only QEMU virtio VGA) so the radeon DPM attach path cannot execute, and the trigger additionally needs an induced kzalloc failure during VBIOS parse. The error paths of r600_parse_extended_power_table (r600_dpm.c:939/950-951/962-964) kfree already-parsed dyn_state entries and leave the pointers dangling; every family caller propagates the error; radeon_pm_fini calls radeon_dpm_fini unconditionally (radeon_pm.c:1705) and _dpm_fini -> r600_free_extended_power_table (r600_dpm.c:1299-1315) frees the same pointers a second time -> heap double free at driver teardown. Same family and calibration as DF-3973/4023/4117/4125 (Medium, AV:P/AC:H/PR:L).