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

boot_ps NULL dereference in kv_dpm_disable (kv_update_current_ps(adev, NULL) -> kv_get_ps(NULL)) when the VBIOS power table has no BOOT-classified state β€” panic on suspend/detach

Summary

kv_parse_pplib_non_clock_info only assigns boot_ps for a BOOT-classified state; nothing validates one exists after parsing. A crafted VBIOS with states but none BOOT-classified leaves boot_ps NULL while DPM inits and enables normally (the NULL flows into current_ps/requested_ps). On the next suspend or detach, kv_dpm_disable calls kv_update_current_ps(adev, NULL) -> kv_get_ps(NULL) reads rps->ps_priv at NULL+40 -> panic. amdgpu kv_dpm instance of the boot_ps family (DF-3586/3610/3646/3623/3754). VERIFIED: verbatim NULL+40 harness (offsetof proof + SIGSEGV crash mode).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3889 Β· 11 files
FileTypeDescriptionSize
boot_ps_null.c β€” 2.6 KB view raw
build.sh β€” 95 B view raw
run.sh β€” 116 B view raw
build.log β€” 107 B view raw
run.log β€” 184 B view raw
run.2.log β€” 221 B view raw
env.txt β€” 561 B view raw
fix.diff β€” 613 B view raw
VERDICT.md β€” 3.3 KB ↓ raw
README.md β€” 1.9 KB ↓ raw
verdict.json β€” 2.7 KB view raw

DF-3889 β€” boot_ps NULL dereference in kv_dpm_disable when the VBIOS power table has no BOOT-classified state

File: sys/dev/drm/amd/amdgpu/kv_dpm.c:1400 (sink: kv_update_current_ps(adev, adev->pm.dpm.boot_ps)), :3026 (unvalidated NULL assignment in sw_init) Class: CWE-476 NULL-pointer dereference (boot_ps family: DF-3586/3610/3623/3646/3754 twins β€” this is the amdgpu kv_dpm instance, unfixed) Verdict: inconclusive at runtime on this guest (no AMD GPU); source-certain; harness proves the verbatim NULL+40 deref β†’ SIGSEGV.

Build (on the DragonFly guest, as root)

mkdir -p /tmp/DF-3889 && cp boot_ps_null.c /tmp/DF-3889/h.c
cd /tmp/DF-3889 && cc -Wall -O0 -o t h.c

Run

./t          # accounting β€” dereference site and offset (ps_priv at +40)
./t crash    # verbatim kv_get_ps(NULL) -> SIGSEGV (exit 139)

Expected

  • accounting: offsetof(struct amdgpu_ps, ps_priv) = 40; kv_dpm_disable (:1400) -> kv_update_current_ps(adev, NULL) -> kv_get_ps(NULL) (:371-373): rps->ps_priv reads *(void**)(NULL + 40).
  • crash: Segmentation fault, exit=139 (kernel equivalent: page fault in kernel mode β†’ panic).

Kernel trigger

kv_parse_pplib_non_clock_info (kv_dpm.c:2672-2675) only assigns adev->pm.dpm.boot_ps for a state with ATOM_PPLIB_CLASSIFICATION_BOOT. A VBIOS power table without such a state leaves boot_ps == NULL while DPM otherwise inits and runs (num_ps > 0, dpm_enabled = true). Then:

  • suspend/resume or driver detach β†’ kv_dpm_hw_fini/kv_dpm_suspend β†’ kv_dpm_disable β†’ kv_update_current_ps(adev, NULL) (kv_dpm.c:1400) β†’ kv_get_ps(NULL) (kv_dpm.c:371-373) β†’ rps->ps_priv at NULL+40 β†’ panic;
  • kv_dpm_sw_init (:3026) and kv_dpm_suspend (:3096) also propagate the NULL into current_ps/requested_ps (benign assignments here, but the same unvalidated assumption).
VERDICT.md
↓ download raw

DF-3889 β€” verdict narrative

Finding

boot_ps is set only inside kv_parse_pplib_non_clock_info (kv_dpm.c:2672-2675):

if (rps->class & ATOM_PPLIB_CLASSIFICATION_BOOT) {
        adev->pm.dpm.boot_ps = rps;
        kv_patch_boot_state(adev, ps);
}

Nothing validates that a BOOT-classified state exists after parsing. With a crafted (or simply mis-built) VBIOS whose PowerPlayInfo state array contains states but none with the BOOT classification bit, adev->pm.dpm.boot_ps stays NULL (amdgpu_device is kzalloc'd), and kv_dpm_init succeeds β€” num_ps > 0, enable_dpm = true, dpm_enabled = true after kv_dpm_enable.

Sinks:

  1. kv_dpm_disable :1400 β€” kv_update_current_ps(adev, adev->pm.dpm.boot_ps) β†’ kv_get_ps(rps) at :371-373 does struct kv_ps *ps = rps->ps_priv; β€” a read at NULL + offsetof(struct amdgpu_ps, ps_priv) (= 40) β†’ page fault β†’ panic. Reached from kv_dpm_hw_fini (:3074-3085, driver detach / GPU fini) and kv_dpm_suspend (:3087-3100, every S3 suspend) whenever dpm_enabled.
  2. kv_dpm_sw_init :3026 β€” current_ps = requested_ps = boot_ps assigns NULLs into the framework (benign by itself; amdgpu_dpm_change_power_state_locked handles a NULL current_ps in kv_check_state_equal via the cps == NULL check at :3250-3259, and amdgpu_dpm_print_power_state is only called with amdgpu_dpm == 1, non-default).
  3. kv_dpm_suspend :3096 β€” same NULL assignment.

This is the amdgpu kv_dpm.c instance of the boot_ps family filed for si_dpm/ci_dpm/ni_dpm (DF-3586, DF-3623, DF-3646, DF-3754); pass 1 on this file did not cover it (DF-1268/1269 are different bugs).

Root cause (kernel references)

What was verified, and how

  1. Source-level certainty. Full-file pass-2 audit; the amdgpu_pm.c caller paths (amdgpu_dpm_change_power_state_locked :1895-1945, kv_check_state_equal NULL handling :3250-3259, print gated on amdgpu_dpm == 1, default -1 per amdgpu_drv.c:92) traced to confirm :1400 is the live sink.
  2. Harness on the guest (DragonFly 6.5-DEVELOPMENT x86_64, base cc 8.3): boot_ps_null.c replicates kv_get_ps/kv_update_current_ps verbatim with a 1:1 struct amdgpu_ps; crash mode executes the NULL+40 read β†’ SIGSEGV exit=139 (kernel equivalent: page fault in kernel mode β†’ panic).
  3. Hardware absence. Guest is virtio-only; the in-kernel trigger needs a KV APU with a crafted VBIOS. Recorded inconclusive, impact none on this guest; source + harness certain.

Fix validation

fix.diff (a) refuses init when no BOOT state was parsed (kv_dpm_init returns -EINVAL, matching the family's fix pattern) and (b) NULL-guards the :1400 call. Not built on the guest (no AMD hardware) β†’ fix_status: not_testable.

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 guest).

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

Confirmed kernel references

Detail

Exploit chain

crafted-VBIOS power table without BOOT classification -> dpm_enabled with boot_ps==NULL -> S3 suspend / driver detach -> kv_dpm_disable -> kv_get_ps(NULL) -> kernel page fault -> panic (DoS).

Evidence (decisive lines)

run.log: 'offsetof(struct amdgpu_ps, ps_priv) = 40', 'kv_dpm_disable (:1400) ... reads *(void**)(NULL + 40)'; run.2.log: crash mode + Segmentation fault exit=139; VERDICT.md; fix.diff

PoC changes

No seed (pass-2 self-authored); single guest build+run cycle succeeded.

Verified recommended fix

Fail kv_dpm_init when the parsed power table has no BOOT-classified state, and NULL-guard the kv_update_current_ps call in kv_dpm_disable (:1400).

Verdict

Source-certain NULL-pointer dereference (boot_ps family, DF-3586/3623/3646/3754 amdgpu-kv instance); runtime-inconclusive on this guest (no AMD GPU). kv_parse_pplib_non_clock_info (kv_dpm.c:2672-2675) only assigns adev->pm.dpm.boot_ps for an ATOM_PPLIB_CLASSIFICATION_BOOT state and nothing validates one exists; a VBIOS power table without it leaves boot_ps NULL while DPM otherwise inits and enables. On the next suspend or driver detach, kv_dpm_hw_fini/kv_dpm_suspend -> kv_dpm_disable (:1400) calls kv_update_current_ps(adev, NULL) -> kv_get_ps(NULL) (:371-373) reads rps->ps_priv at NULL+40 -> page fault -> panic. sw_init (:3026) and suspend (:3096) also propagate the NULL into current_ps/requested_ps. Harness executes the verbatim NULL+40 deref -> SIGSEGV exit=139 (kernel equivalent: panic).