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

rs780_dpm: NULL boot_ps / skipped state parse (ucStateEntrySize==1 or no BOOT-classified state) -> NULL-pointer page-fault panic in rs780_dpm_enable and print paths

Summary

boot_ps is only assigned inside the per-state parse when a state carries the BOOT classification, and only inside the 'if (ucStateEntrySize - 1)' block - yet the parse returns success with num_ps set when no BOOT state exists or the stride skips everything. radeon_pm_init_dpm discards the init return and sets current/requested to boot_ps, then calls dpm_enable which derefs NULL->ps_priv. Harness-verified: no-BOOT-class SIGSEGV at exactly offsetof(ps_priv); stride-1 SIGSEGV at offsetof(sclk_low). RS780 twin of DF-4187/4124/4143/4118. Guard+reject-strides fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4493 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
harness.c β€” 48.7 KB view raw
build.sh β€” 65 B view raw
run.sh β€” 121 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 β€” 3.7 KB view raw

DF-4493 β€” rs780_dpm.c: NULL boot_ps / skipped state parse β†’ NULL-deref page-fault panic

What

Two ways to reach a NULL rdev->pm.dpm.boot_ps (or NULL ps_priv) that the DPM enable path dereferences without any check:

  1. No BOOT-classified state (rs780_dpm.c:741-742 sets boot_ps only when rps->class & ATOM_PPLIB_CLASSIFICATION_BOOT). A PowerPlayInfo table whose non-clock infos lack the BOOT bit parses "successfully" (rs780_parse_power_table() returns 0, :843), leaving boot_ps == NULL. radeon_pm_init_dpm() (radeon_pm.c:1448) discards the dpm_init return, sets current_ps = requested_ps = boot_ps (:1449) and calls radeon_dpm_enable() (:1453) β†’ rs780_dpm_enable() fetches boot_ps (:600) and passes it to rs780_initialize_dpm_parameters() (:608) β†’ rs780_initialize_dpm_power_state() does default_state = rs780_get_ps(boot_ps) (:78) β†’ NULL->ps_priv read at address 0x28 β†’ page-fault panic.

  2. ucStateEntrySize == 1: the guard if (power_info->pplib.ucStateEntrySize - 1) (:822) is false, so NO state is parsed at all β€” every ps[i].ps_priv stays NULL and boot_ps is never set β€” yet num_ps = ucNumStates is still assigned (:842) and the function returns success. Any later rs780_get_ps() consumer derefs NULL: rs780_dpm_print_power_state() (:946 ps->sclk_low, address 0x8), rs780_dpm_set_power_state() (:567-568), rs780_dpm_get_sclk() (:965-970), rs780_dpm_force_performance_level() (:1031).

Family: DF-4187 (sumo), DF-4124 (rv6xx), DF-4143 (trinity), DF-4118 (cypress) β€” rs780 is the last sibling with the same shape.

Reproduce

./build.sh
./run.sh            # modes 4493 (no BOOT state) and 4493b (ucStateEntrySize==1)

Expected baseline (run.log):

  • mode 4493: boot_ps=0x0 then *** SIGNAL 11 at address 0x28 ***, exit=99
  • mode 4493b: ps[0].ps_priv=0x0 then *** SIGNAL 11 at address 0x8 ***, exit=99

Expected with harness_fixed: mode 4493 prints DRM_ERROR: no boot power state, refusing to enable DPM (enable guard), mode 4493b rejects the table at parse (invalid state entry size 1), both exit=0.

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): rs780_dpm_enable() refuses to run without a boot power state, and the parse rejects ucStateEntrySize < 2 (an ATOM_PPLIB_STATE is 2 bytes). 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-4493 β€” VERDICT

Finding: NULL boot_ps / never-parsed states (ucStateEntrySize == 1 or missing BOOT classification) β†’ NULL-pointer page-fault panic in the rs780 DPM enable/print paths.

Status: REPRODUCED at logic level (verbatim replica), 2 attempts. Kernel-level trigger hardware-gated (no RS780/RS880 IGP in the QEMU guest); family precedent DF-4187/DF-4124/DF-4143/DF-4118 verified identically.

Reachability chain (all cited)

  1. radeon_pm_init_dpm() β€” radeon_dpm_init(rdev); at radeon_pm.c:1448 is a bare statement: the return value of rs780_dpm_init() is discarded, so parse "success" is not even required β€” but in these two scenarios the parse genuinely returns 0 (rs780_dpm.c:843).
  2. radeon_pm.c:1449 current_ps = requested_ps = boot_ps (NULL).
  3. radeon_pm.c:1453 radeon_dpm_enable(rdev) β†’ rs780_dpm.c:600 boot_ps = rdev->pm.dpm.boot_ps (NULL) β†’ :608 rs780_initialize_dpm_parameters(rdev, NULL) β†’ :78 rs780_get_ps(NULL) β†’ rps->ps_priv load from NULL+40 (0x28).

Variant B (ucStateEntrySize == 1): rs780_dpm.c:822 if (power_info->pplib.ucStateEntrySize - 1) evaluates 1 - 1 == 0 β†’ the entire per-state parse (kzalloc igp_ps, ps_priv attach, boot_ps assignment at :741-742) is skipped, while :842 still sets num_ps = ucNumStates and the function returns 0. Consumers then deref the NULL ps_priv: rs780_dpm_print_power_state() :946 (ps->sclk_low, NULL+8), rs780_dpm_set_power_state() :567-568, rs780_dpm_get_sclk() :965-970, rs780_dpm_force_performance_level() :1031.

Observed (run.log)

===== mode 4493 =====
rs780_parse_power_table() = 0 num_ps=1 boot_ps=0x0
calling rs780_dpm_enable() with boot_ps == NULL ...
*** SIGNAL 11 at address 0x28: SIGSEGV ... (kernel: page-fault PANIC) ***
exit=99

===== mode 4493b =====
rs780_parse_power_table() = 0 num_ps=1 boot_ps=0x0 ps[0].ps_priv=0x0
radeon_dpm_print_power_states() walks 1 states whose ps_priv are NULL ...
*** SIGNAL 11 at address 0x8: SIGSEGV ... ***
exit=99

Address 0x28 is exactly offsetof(struct radeon_ps, ps_priv) (verified in the harness against the kernel layout radeon.h:1342-1357); address 0x8 is offsetof(struct igp_ps, sclk_low) (rs780_dpm.h:52-58). The harness is compiled with -fno-delete-null-pointer-checks β€” the same semantics the kernel relies on β€” so the loads trap instead of being folded away.

Fix validation

fix.diff: (a) rs780_dpm_enable() returns -EINVAL when boot_ps == NULL; (b) parse rejects ucStateEntrySize < 2. Fixed harness:

DRM_ERROR: no boot power state, refusing to enable DPM
rs780_dpm_enable() returned -22 - NO CRASH (fix active)   exit=0
DRM_ERROR: invalid state entry size 1
rs780_parse_power_table() = -22 ... crafted table rejected   exit=0

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

Attempts

  1. First run showed "NO CRASH": the userspace stub's dead outputs let GCC eliminate the NULL loads (UB folding). Fixed by compiling with -fno-delete-null-pointer-checks and feeding the loaded sclk into a volatile sink β€” restoring kernel trap semantics.
  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 4493 returns -EINVAL before any deref ('no boot power state'); 4493b rejected at parse ('invalid state entry size 1'); combined fix builds radeon.ko under -Werror (MAKE_RC=0)

run.log fixed sections for modes 4493/4493b (exit=0, canary_violations=0); radeon_fix_apply.log hunk 1 (enable guard) and hunk 4 (entry-size check); radeon_module_build.log MAKE_RC=0
↓ fix.diffper-fix-DF-4493

Confirmed kernel references

Detail

Exploit chain

crafted VBIOS PowerPlayInfo (no BOOT class, or ucStateEntrySize==1) -> parse returns success with boot_ps/ps_priv NULL -> radeon_pm.c:1448 swallows nothing to swallow (parse succeeded) -> dpm enable runs -> NULL->ps_priv read at 0x28 -> kernel page-fault panic (local DoS on probe / first mode set / radeon_dpm=1 boot print)

Evidence (decisive lines)

run.log '===== mode 4493 =====': 'boot_ps=0x0' then 'SIGNAL 11 at address 0x28', exit=99; '===== mode 4493b =====': 'ps[0].ps_priv=0x0' then 'SIGNAL 11 at address 0x8', exit=99; fixed sections: 'DRM_ERROR: no boot power state, refusing to enable DPM' / 'DRM_ERROR: invalid state entry size 1', exit=0; radeon_module_build.log MAKE_RC=0 under -Werror

PoC changes

wrote harness from scratch (no seed); -fno-delete-null-pointer-checks + volatile clock sink required so the NULL loads trap with kernel semantics instead of being folded by GCC

Verified recommended fix

refuse to enable DPM without a boot power state (boot_ps NULL check in rs780_dpm_enable) and reject ucStateEntrySize < 2 in rs780_parse_power_table, as in fix.diff

Verdict

Reproduced at logic level with the verbatim replica (harness modes 4493/4493b): a PowerPlayInfo table with no BOOT-classified state leaves rdev->pm.dpm.boot_ps NULL (rs780_dpm.c:741-742 is the only assignment), radeon_pm_init_dpm discards the init return (radeon_pm.c:1448) and still calls radeon_dpm_enable (:1453) -> rs780_get_ps(NULL) reads NULL->ps_priv at 0x28 -> SIGSEGV (kernel: page-fault panic); ucStateEntrySize==1 makes the if (ucStateEntrySize - 1) guard (:822) skip every state while num_ps is still set (:842), leaving all ps_priv NULL -> rs780_dpm_print_power_state :946 derefs ps->sclk_low at 0x8 -> SIGSEGV. Fault addresses match the kernel struct layouts exactly (offsetof radeon_ps.ps_priv = 40, igp_ps.sclk_low = 8). RS780 twin of DF-4187 (sumo) / DF-4124 (rv6xx) / DF-4143 (trinity) / DF-4118 (cypress). Kernel-level repro hardware-gated (no RS780 IGP in the QEMU guest). Fix proven: enable refuses NULL boot_ps; parse rejects ucStateEntrySize < 2; radeon.ko builds under -Werror (MAKE_RC=0).