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

boot_ps NULL dereference panic in kv_dpm_enable/kv_dpm_disable (plus kv_dpm_get_sclk ps_priv-NULL window) when VBIOS power table has no BOOT-classified state

Summary

kv_parse_pplib_non_clock_info only assigns boot_ps for a BOOT-classified state; a VBIOS power table without one parses successfully with boot_ps==NULL, and radeon_pm_init_dpm DISCARDS kv_dpm_init()'s return value so DPM enables anyway. enable/disable call kv_update_current_ps(rdev, NULL) -> kv_get_ps(NULL) reads rps->ps_priv at NULL+64 -> panic. Family sink: kv_dpm_get_sclk dereferences the requested ps_priv which stays NULL when pick_power_state returned NULL; next dce8_bandwidth_update faults. VERIFIED: verbatim-expression harnesses on the guest - both children SIGSEGV as predicted (NULL+64). Guaranteed boot/suspend/detach panic on KV/BT/ML APU hardware with crafted VBIOS. amdgpu twin DF-3889.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3953 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
trigger_df3953.c β€” 5.5 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 116 B view raw
build.log β€” 249 B view raw
run.log β€” 2.2 KB view raw
env.txt β€” 725 B view raw
fix.diff β€” 1.8 KB view raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 3.5 KB view raw

DF-3953 β€” radeon kv_dpm boot_ps NULL dereference (kv_dpm_enable / kv_dpm_disable)

File: sys/dev/drm/radeon/kv_dpm.c (pass 2, GLM 5.3) Severity: Medium Β· CWE: CWE-476 (NULL pointer dereference) Β· Confidence: certain (source) Status: inconclusive on this guest (no AMD GPU β€” hardware-gated), arithmetic-simulation harness verified.

What

rdev->pm.dpm.boot_ps stays NULL when the VBIOS PowerPlayInfo table parses successfully but contains no state classified ATOM_PPLIB_CLASSIFICATION_BOOT. Nothing validates its existence, and radeon_pm_init_dpm() (radeon_pm.c:1448) discards kv_dpm_init()'s return value, so DPM enable/disable run regardless:

  • kv_dpm_enable() end: kv_update_current_ps(rdev, rdev->pm.dpm.boot_ps) β€” kv_dpm.c:1277
  • kv_dpm_disable() end (suspend via radeon_pm_suspend_dpm radeon_pm.c:1260, detach via radeon_pm_fini_dpm radeon_pm.c:1694): same call β€” kv_dpm.c:1326
  • Family sink: kv_dpm_get_sclk() kv_dpm.c:2881 dereferences pi->requested_rps.ps_priv, which stays NULL whenever radeon_dpm_pick_power_state() returned NULL (radeon_pm.c:1051) and compute_clocks bailed (radeon_pm.c:1075-1078); the next dce8_bandwidth_update (cik.c:9287/9327) faults through levels[0].

All dereference rps->ps_priv at NULL + 64 via kv_get_ps(NULL) (kv_dpm.c:239-244) β†’ kernel page fault on the NULL page β†’ panic.

Reproduce

cd findings/poc/DF-3953
./build.sh          # cc -O2 -Wall -o trigger_df3953 trigger_df3953.c
./run.sh            # analysis mode (exit 0), then crash mode

Expected (crash mode): both children die SIGSEGV sig=11 reproducing the verbatim NULL + offsetof(struct radeon_ps, ps_priv) access; final line RESULT: BOTH SINKS FAULT AS PREDICTED (rc=0). Kernel equivalent: panic at boot / S3 suspend / driver detach on a KV APU whose VBIOS power table lacks a BOOT-classified state.

Threat model

Attacker controls the GPU VBIOS image (physical replacement, rom flashing as root, or firmware supply chain). Result: guaranteed boot/suspend/detach panic (local DoS of the machine). No unprivileged-local or remote vector. Same family as amdgpu DF-3889 (Medium), si_dpm DF-3586, ci_dpm DF-3623, ni_dpm DF-3754 β€” not covered by pass-1 DF-1305/1306/1307 on this file.

Fix

Guard boot_ps in kv_dpm.c (fix.diff) and stop discarding kv_dpm_init()'s return value in radeon_pm.c:1448 (noted in fix.diff header; radeon_pm.c is outside this file's audited scope).

VERDICT.md
↓ download raw

DF-3953 VERDICT

Verdict: INCONCLUSIVE at kernel level (no AMD GPU on guest) β€” source-certain NULL-page dereference; userland arithmetic simulation of both sinks faults exactly as predicted (SIGSEGV at NULL+64).

How the bug was established

  1. boot_ps is conditionally assigned. kv_parse_pplib_non_clock_info() (kv_dpm.c:2598-2601) sets rdev->pm.dpm.boot_ps = rps only when rps->class & ATOM_PPLIB_CLASSIFICATION_BOOT. A PowerPlayInfo table whose states carry other classifications parses successfully (kv_parse_power_table returns 0 at kv_dpm.c:2714) with boot_ps == NULL.

  2. The error channel that should catch this is dead. radeon_pm_init_dpm() (radeon_pm.c:1448) calls the radeon_dpm_init macro β€” rdev->asic->dpm.init(rdev) = kv_dpm_init() β€” without examining its return value, then unconditionally calls radeon_dpm_setup_asic() and radeon_dpm_enable() (= kv_dpm_enable).

  3. Sinks. On functioning hardware the SMC calls at the start of kv_dpm_enable() succeed, so the function reaches its last statement (kv_dpm.c:1277): kv_update_current_ps(rdev, rdev->pm.dpm.boot_ps) β†’ kv_get_ps(NULL) (kv_dpm.c:239-244) loads rps->ps_priv from NULL + 64 β†’ kernel page fault β†’ panic. Identically at kv_dpm.c:1326 in kv_dpm_disable(), reached from suspend (radeon_pm.c:1260) and from radeon_pm_fini_dpm on detach (radeon_pm.c:1694, gated only by the legacy num_power_states > 1 counter), and again on resume through radeon_pm_resume_dpm β†’ radeon_dpm_enable (radeon_pm.c:1321-1322).

  4. Second sink of the same NULL family (verified in harness child2). kv_dpm_get_sclk() (kv_dpm.c:2878-2887) dereferences kv_get_ps(&pi->requested_rps) β€” ps_priv remains NULL until the first successful kv_dpm_pre_set_power_state. If radeon_dpm_pick_power_state() finds no match it returns NULL (radeon_pm.c:1051) and radeon_pm_compute_clocks_dpm returns early without setting it (radeon_pm.c:1075-1078); the next modeset's dce8_bandwidth_update (cik.c:9287/9327, gated only on dpm_enabled) calls radeon_dpm_get_sclk β†’ requested_state->levels[0] with requested_state == NULL β†’ panic.

What the harness proves (and does not prove)

  • Proves: the exact index/offset arithmetic (offsetof(struct radeon_ps, ps_priv) = 64), that the expressions as written in kv_dpm.c fault on a NULL rps (children die SIGSEGV at the predicted addresses), and the call-chain wiring (every hop verified in source with line cites).
  • Does not prove: an on-target kernel panic β€” that requires KV/BT/ML APU hardware with a crafted VBIOS. The QEMU guest has only a virtio VGA device (pciconf -lv: vgapci0 1af4:1111) and no radeon.ko loaded.

Why not uid0

NULL-page read at a fixed small offset; the faulting value is never used as a write target or control flow beyond the copy in kv_update_current_ps (pi->current_rps = *rps also faults first, at NULL+0). Impact ceiling: panic/DoS. Not exploitable for privilege escalation.

Fix validation

fix.diff authored against the read-only sys/ tree (guards in kv_dpm.c + note for radeon_pm.c:1448). Not built/tested: no AMD hardware on the guest (fix_status: not_testable). The guard is trivially sufficient by inspection: both NULL sinks receive boot_ps only after the check.

Family context

amdgpu kv_dpm.c twin = DF-3889 (Medium, same verdict shape). radeon-wide twins: DF-3586 (si_dpm), DF-3623 (ci_dpm), DF-3754 (ni_dpm). Distinct from pass-1 DF-1305/1306/1307 (different indices, different sinks).

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored against the read-only sys/ tree after source+harness verification; not built - no AMD hardware on the guest.

findings/poc/DF-3953/fix.diff
↓ fix.diffper-fix-DF-3953

Confirmed kernel references

Detail

Exploit chain

crafted-VBIOS power table without BOOT classification -> DPM enabled with boot_ps==NULL -> boot/suspend/resume/detach -> kv_update_current_ps(NULL) -> NULL+64 page fault -> panic (DoS). No uid0 path: NULL-page read only, value never used as write target.

Evidence (decisive lines)

run.log: 'offsetof(struct radeon_ps, ps_priv) = 64', 'child1 ... SIGNALLED sig=11', 'child2 (kv_dpm_get_sclk :2881): SIGNALLED sig=11', 'RESULT: BOTH SINKS FAULT AS PREDICTED (rc=0)'; VERDICT.md traces every hop; fix.diff

PoC changes

No seed (pass-2 self-authored). Two fix cycles: nested comment broke the first build; first crash run had child1's fault optimized away (dead store) - made the loaded pointer volatile so the faulting load is observable.

Verified recommended fix

Guard boot_ps in kv_dpm_enable/kv_dpm_disable and NULL-check requested_state in kv_dpm_get_sclk (fix.diff); additionally honor kv_dpm_init()'s return value at radeon_pm.c:1448.

Verdict

Source-certain NULL-pointer dereference (radeon-kv boot_ps family; amdgpu twin is DF-3889). kv_parse_pplib_non_clock_info (kv_dpm.c:2598-2601) only assigns rdev->pm.dpm.boot_ps for an ATOM_PPLIB_CLASSIFICATION_BOOT state; nothing validates one exists, and radeon_pm.c:1448 DISCARDS kv_dpm_init()'s return value so DPM is enabled regardless. kv_dpm_enable (:1277) and kv_dpm_disable (:1326, suspend radeon_pm.c:1260 / fini radeon_pm.c:1694 / resume radeon_pm.c:1321-1322) call kv_update_current_ps(rdev, NULL) -> kv_get_ps(NULL) (:239-244) reads rps->ps_priv at NULL+64 -> page fault -> panic. Additional sink: kv_dpm_get_sclk (:2881) dereferences pi->requested_rps.ps_priv which stays NULL when radeon_dpm_pick_power_state returned NULL (radeon_pm.c:1051) and compute_clocks bailed (:1075-1078); next dce8_bandwidth_update (cik.c:9287/9327) faults. Harness executes the verbatim NULL+64 deref in children -> SIGSEGV sig=11 both sinks (kernel equivalent: panic). Runtime-inconclusive on this guest: virtio VGA only, no radeon hardware.