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)
PoC verification
Evidence pack
findings/poc/DF-4494 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- ENOMEM mid-parse (rs780_dpm.c:828-832):
kzalloc(struct igp_ps)failure kfreesrdev->pm.dpm.psand returns β without NULLing the pointer, leakingps_priv[0..i-1], and leavingboot_ps(already set to&ps[k]by an earlier state at :741-742) pointing into the freed array.num_psis still 0. * Probe continues (swallowed error):rs780_dpm_enable():608 dereferences the danglingboot_psβ use-after-free read; the freed slot's contents become theigp_ps *that :82 derefs. * Teardown:rs780_dpm_fini():959kfree(rdev->pm.dpm.ps)β double free of the kmalloc'd state array. - Missing IntegratedSystemInfo header (:933-934):
rs780_dpm_init()callsradeon_dpm_fini(rdev)itself (freeingps_priv[],ps,privβ all left dangling) and returns-EINVAL, which radeon_pm.c:1448 discards. The very next statement inrs780_dpm_enable()βrs780_get_pm_mode_parameters():57pi->crtc_id = 0β is a use-after-free WRITE into the freedigp_power_info; the subsequent boot_ps walk reads freed memory again. - Unknown igp_info crev β₯ 3 (:914-916): returns
-EINVALwith 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 FREEthen 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).
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 freedigp_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 NULLsdpm.ps,boot_ps,uvd_ps(single, consistent cleanup; teardownkfree(NULL)) rs780_dpm_init()no longer callsradeon_dpm_fini()itself β the swallowed error leaves a valid-or-NULL state, teardown owns the freers780_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
- 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. - 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
fixedharness_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
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).
No comments yet.