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

rs780_dpm: swallowed-init lifetime bugs β€” dangling rdev->pm.dpm.ps after kfree-without-NULL and self-radeon_dpm_fini in rs780_dpm_init -> double free in rs780_dpm_fini and UAF read+write in rs780_dpm_enable

Summary

Three inconsistent error exits are only safe if the caller aborts, but radeon_pm.c discards the init return and teardown calls fini unconditionally: ENOMEM kfrees dpm.ps without NULLing while boot_ps points into it (enable UAF-reads it; fini double-frees it); missing IntegratedSystemInfo makes init call radeon_dpm_fini itself then return an error the caller swallows, so get_pm_mode_parameters write-after-frees the freed igp_power_info; crev>=3 exits with no cleanup. Arm (1) needs no crafted VBIOS - memory pressure at probe on stock firmware suffices. Harness-verified: double-free detected, UAF read, and 8-byte write-after-free all demonstrated; fixed modes exit 0. RS780 twin of DF-4125/4188/4144/4117/3973. Single-owner cleanup fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4494 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw
harness.c β€” 48.7 KB view raw
build.sh β€” 65 B view raw
run.sh β€” 128 B view raw
build.log β€” 2.0 KB view raw
run.log β€” 7.0 KB view raw
env.txt β€” 390 B view raw
fix.diff β€” 4.5 KB view raw
radeon_fix_apply.log β€” 435 B view raw
radeon_module_build.log β€” 139.5 KB view raw
verdict.json β€” 4.6 KB view raw

DF-4494 β€” rs780_dpm.c: swallowed-init lifetime bugs β†’ double free in

rs780_dpm_fini / UAF read+write in rs780_dpm_enable

What

Three inconsistent error exits, all harmless only if the caller aborts β€” but radeon_pm_init_dpm() discards the rs780_dpm_init() return (radeon_pm.c:1448 is a bare statement) and keeps going, and teardown (radeon_pm_fini() β†’ radeon_dpm_fini(), radeon_pm.c:1705) then runs over the wreckage:

  1. ENOMEM mid-parse (rs780_dpm.c:828-832): kzalloc(struct igp_ps) failure kfrees rdev->pm.dpm.ps and returns β€” without NULLing the pointer, leaking ps_priv[0..i-1], and leaving boot_ps (already set to &ps[k] by an earlier state at :741-742) pointing into the freed array. num_ps is still 0. * Probe continues (swallowed error): rs780_dpm_enable() :608 dereferences the dangling boot_ps β†’ use-after-free read; the freed slot's contents become the igp_ps * that :82 derefs. * Teardown: rs780_dpm_fini() :959 kfree(rdev->pm.dpm.ps) β†’ double free of the kmalloc'd state array.
  2. Missing IntegratedSystemInfo header (:933-934): rs780_dpm_init() calls radeon_dpm_fini(rdev) itself (freeing ps_priv[], ps, priv β€” all left dangling) and returns -EINVAL, which radeon_pm.c:1448 discards. The very next statement in rs780_dpm_enable() β€” rs780_get_pm_mode_parameters() :57 pi->crtc_id = 0 β€” is a use-after-free WRITE into the freed igp_power_info; the subsequent boot_ps walk reads freed memory again.
  3. Unknown igp_info crev β‰₯ 3 (:914-916): returns -EINVAL with no cleanup at all β€” inconsistent with (2), memory-safe only by accident.

The ENOMEM trigger needs no crafted VBIOS: any RS780 system that hits memory pressure exactly at the per-state kzalloc during probe gets the dangling-pointer state on stock firmware.

Family: DF-4125 (rv6xx), DF-4188 (sumo), DF-4144 (trinity), DF-4117 (cypress), DF-3973 (btc).

Reproduce

./build.sh
./run.sh            # modes 4494 (double free), 4494u (UAF read), 4494w (UAF write)

Expected baseline (run.log):

  • mode 4494: *** CANARY ALLOCATOR: DOUBLE FREE of ... (size 96) DETECTED ***, exit=98
  • mode 4494u: boot_ps ... points into a FREED allocation -> USE AFTER FREE then SIGBUS (non-canonical pointer loaded from freed memory), exit=99
  • mode 4494w: freed object igp_power_info ... has 8 bytes != 0xb0 -> object WRITTEN AFTER FREE (UAF WRITE) then SIGBUS, exit=99

Expected with harness_fixed: all three modes exit=0 with zero canary violations (pointers NULLed after cleanup; init no longer self-finalizes; enable guard).

Kernel-level reproduction is hardware-gated (no RS780 IGP in the QEMU guest); the harness is a verbatim replica β€” see VERDICT.md.

Fix

fix.diff (in this directory): the ENOMEM path frees the already-parsed ps_priv[], kfrees and NULLs dpm.ps/boot_ps/uvd_ps; rs780_dpm_init() no longer calls radeon_dpm_fini() on its own error path (teardown owns the single free); rs780_dpm_enable() refuses a NULL boot_ps. Validated: radeon.ko builds under -Werror after applying the fix to the guest's /usr/src (radeon_module_build.log, MAKE_RC=0).

VERDICT.md
↓ download raw

DF-4494 β€” VERDICT

Finding: inconsistent error-exit lifetimes in rs780_parse_power_table()/rs780_dpm_init(), turned into live double-free / UAF read+write by radeon_pm_init_dpm() discarding the init return (radeon_pm.c:1448) and by teardown calling rs780_dpm_fini() unconditionally (radeon_pm.c:1705).

Status: REPRODUCED at logic level (verbatim replica), 2 attempts. Kernel-level trigger hardware-gated (no RS780/RS880 IGP in the QEMU guest); family rows DF-4125/DF-4188/DF-4144/DF-4117/DF-3973 verified the same way.

Observed (run.log, canary allocator with 0xB0 free-fill)

mode 4494 β€” dangling dpm.ps β†’ double free

kzalloc(struct igp_ps) failed at state #1 (allocation #6). Parse kfrees dpm.ps (rs780_dpm.c:830) and returns -ENOMEM; the pointer is not NULLed and boot_ps still points at &ps[0]:

rs780_parse_power_table() = -12 (ENOMEM injected at state 1)
rdev->pm.dpm.ps is NOT (dangling pointer kept)NULL after the failed parse
rdev->pm.dpm.boot_ps = 0x8004d1d00 (points into the freed array)
driver unload -> radeon_pm_fini -> rs780_dpm_fini ...
*** CANARY ALLOCATOR: DOUBLE FREE of 0x8004d1d00 (size 96) DETECTED ***
exit=98

(size 96 = sizeof(struct radeon_ps) Γ— ucNumStates(2) β€” the exact allocation rs780_dpm.c:807-808 made.)

mode 4494u β€” UAF read at probe

Same dangling state, then the swallowed-error continue: rs780_dpm_enable() :608 β†’ rs780_initialize_dpm_power_state() :78 loads ps[0].ps_priv from the freed array (0xB0 free-fill), :82 derefs 0xB0B0B0B0B0B0B0B0 β†’ non-canonical β†’ SIGBUS:

*** CANARY: boot_ps (&dpm.ps[0]) ... points into a FREED allocation -> USE AFTER FREE ***
*** SIGNAL 10 at address 0x0: abort ***      exit=99

mode 4494w β€” UAF write via the self-fini path

Successful parse + missing IntegratedSystemInfo header β†’ rs780_dpm_init() frees everything itself (:933) and returns -EINVAL, which radeon_pm.c:1448 discards. The first field-write of the continued probe (rs780_get_pm_mode_parameters() :57-58 pi->crtc_id = 0; pi->refresh_rate = 60) lands in the freed igp_power_info:

*** CANARY: freed object igp_power_info (dpm.priv) (0x8004c1500, 32 bytes) has 8 bytes != 0xb0 -> object WRITTEN AFTER FREE (UAF WRITE) ***
*** SIGNAL 10 at address 0x0: abort ***      exit=99

Why the kernel behaves the same

  • radeon.h:2793 #define radeon_dpm_init(rdev) rdev->asic->dpm.init((rdev)) β€” radeon_pm.c:1448 calls it as a statement; no error path exists.
  • radeon_pm.c:1705 radeon_dpm_fini(rdev); runs at teardown regardless.
  • The kernel kfree() of the dangling pointer at :959 is the classic double free into the DRM slab; the freed igp_power_info / radeon_ps[] sit in small-object slabs that are trivially re-groomed between probe failure and module unload β€” the write-after-free at :57 corrupts whatever now occupies the slot (memcorrupt bucket).

The ENOMEM arm requires no crafted VBIOS β€” ordinary memory pressure at the per-state kzalloc during probe on stock RS780 firmware is enough (same reasoning as family row DF-4188, PR:L).

Fix validation

fix.diff:

  • ENOMEM path: frees ps_priv[0..i-1], kfrees and NULLs dpm.ps, boot_ps, uvd_ps (single, consistent cleanup; teardown kfree(NULL))
  • rs780_dpm_init() no longer calls radeon_dpm_fini() itself β€” the swallowed error leaves a valid-or-NULL state, teardown owns the free
  • rs780_dpm_enable() refuses NULL boot_ps (defense in depth)

Fixed harness (run.log):

rdev->pm.dpm.ps is NULL after the failed parse / boot_ps = 0x0
rs780_dpm_fini() returned; canary_violations so far = 0      exit=0
DRM_ERROR: no boot power state, refusing to enable DPM       exit=0   (4494u)
DRM_ERROR: No integrated system info for your GPU
rs780_dpm_enable() returned 0 ... canary_violations = 0      exit=0   (4494w)

Applied to the guest /usr/src: radeon_fix_apply.log (7 hunks, Plan A), radeon_module_build.log β€” radeon.ko links under -Werror, MAKE_RC=0. Guest source reverted (md5 verified), guest reset with-src afterwards.

Attempts

  1. First run: enable-arm "NO CRASH" (dead stub outputs let GCC fold the wild loads). Fixed with -fno-delete-null-pointer-checks + volatile clock sink.
  2. Decisive run in run.log.

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

harness_fixed 4494: 'dpm.ps is NULL after the failed parse', fini clean, exit=0; 4494u: enable guard fires, exit=0; 4494w: init no longer self-finalizes ('canary_violations = 0'), exit=0; combined fix builds radeon.ko under -Werror (MAKE_RC=0)

run.log fixed sections for modes 4494/4494u/4494w (all exit=0); radeon_fix_apply.log hunks 1, 7, 8; radeon_module_build.log MAKE_RC=0
↓ fix.diffper-fix-DF-4494

Confirmed kernel references

Detail

Exploit chain

memory pressure (or crafted table) at rs780_parse_power_table state kzalloc -> dpm.ps freed but dangling + boot_ps pointing into it -> radeon_pm.c:1448 swallows the error -> probe continues: UAF read of freed ps[0].ps_priv in rs780_dpm_enable (freed-slot contents become the igp_ps pointer dereferenced at :82) -> module unload: rs780_dpm_fini double-frees the array into the DRM slab; variant: missing igp_info header -> init self-finalizes -> continued probe performs a deterministic write-after-free into the freed igp_power_info slot (groomable small-object slab) before faulting

Evidence (decisive lines)

run.log '===== mode 4494 =====': 'DOUBLE FREE of 0x8004d1d00 (size 96) DETECTED', exit=98; '===== mode 4494u =====': 'boot_ps ... points into a FREED allocation -> USE AFTER FREE' then SIGNAL 10, exit=99; '===== mode 4494w =====': 'freed object igp_power_info ... has 8 bytes != 0xb0 -> object WRITTEN AFTER FREE (UAF WRITE)' then SIGNAL 10; all fixed modes exit=0 with canary_violations=0; radeon_module_build.log MAKE_RC=0 under -Werror

PoC changes

wrote harness from scratch (no seed); canary allocator extended with 0xB0 free-fill + freed-object write detection so the UAF write is observable; -fno-delete-null-pointer-checks for kernel trap semantics

Verified recommended fix

NULL dpm.ps/boot_ps/uvd_ps after freeing on every parse error path (freeing prior ps_priv entries), and stop rs780_dpm_init from calling radeon_dpm_fini on its own error path (teardown owns the single free), plus a NULL-boot_ps guard in rs780_dpm_enable β€” as in fix.diff

Verdict

Reproduced at logic level with the verbatim replica (harness modes 4494/4494u/4494w): the ENOMEM path of rs780_parse_power_table kfrees rdev->pm.dpm.ps without NULLing it (:828-832) while boot_ps already points into the array, and radeon_pm.c:1448 discards the error β€” teardown rs780_dpm_fini (:959) kfrees the dangling pointer => DOUBLE FREE of the sizeof(radeon_ps)*ucNumStates allocation (canary: 'DOUBLE FREE of 0x8004d1d00 (size 96) DETECTED', exit=98), and the continued probe's rs780_dpm_enable (:608->:78->:82) reads ps[0].ps_priv from the freed array then derefs the 0xB0 free-fill => SIGBUS/UAF read; separately, the missing-IntegratedSystemInfo exit of rs780_dpm_init calls radeon_dpm_fini itself (:933) and returns -EINVAL that radeon_pm.c:1448 discards, so rs780_get_pm_mode_parameters (:57-58) WRITES pi->crtc_id/refresh_rate into the freed igp_power_info (canary: '8 bytes != 0xb0 -> object WRITTEN AFTER FREE', then SIGBUS). The ENOMEM arm needs no crafted VBIOS (memory pressure at the per-state kzalloc on stock firmware), same PR:L reasoning as family row DF-4188 (sumo twin; also DF-4125 rv6xx / DF-4144 trinity / DF-4117 cypress / DF-3973 btc). Kernel-level repro hardware-gated (no RS780 IGP in the QEMU guest). Fix proven: cleanup NULLs dpm.ps/boot_ps/uvd_ps, init no longer self-finalizes, enable guards NULL boot_ps; radeon.ko builds under -Werror (MAKE_RC=0).