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

NULL-pointer dereference of rdev->pm.dpm.boot_ps (and ps_priv) when the crafted-VBIOS power table has no usable BOOT state β€” panic at default-enabled DPM

Summary

boot_ps is assigned only inside ni_parse_pplib_non_clock_info, which is only reached inside the 'if (ucStateEntrySize - 1)' block of ni_parse_power_table; nothing validates that a boot state was found before DPM enable. Crafted-VBIOS shapes (ucNumStates==0; ucStateEntrySize==1 - which also leaves every ps[i].ps_priv NULL; or no state classified BOOT) leave boot_ps NULL while the parse returns 0. radeon_pm_init_dpm ignores the dpm.init rc, publishes current_ps=requested_ps=boot_ps (NULL), and calls radeon_dpm_enable: the chain faults at ni_get_ps(NULL)->ps_priv = read at NULL+0x28 -> unrecoverable page fault during driver load. The ucStateEntrySize==1 shape additionally faults on the first power-state change. HARNESS: verbatim parse walk + consumer call graph - children SIGSEGV at 0x28 and 0x0; fixed-path -EINVAL. Local attacker with control of the GPU's video BIOS (reflash, passthrough custom ROM, supply-chain) on any BARTS/TURKS/CAICOS/CAYMAN/ARUBA card: deterministic kernel panic at driver load (DPM default-enabled). ni twin of DF-3610/3646/amdgpu DF-3586. Fix compile-validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3754 Β· 13 files
FileTypeDescriptionSize
harness.c β€” 12.1 KB view raw
build.sh β€” 88 B view raw
run.sh β€” 90 B view raw
build.log β€” 26 B view raw
run.log β€” 1.0 KB view raw
fix.diff β€” 405 B view raw
fix_build.log β€” 1.4 KB view raw
fix_run.log β€” 138 B view raw
env.txt β€” 779 B view raw
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 4.7 KB ↓ raw
manifest.json β€” 1020 B view raw
verdict.json β€” 4.1 KB view raw

DF-3754 β€” NULL-pointer dereference of rdev->pm.dpm.boot_ps / ps_priv in ni DPM (crafted VBIOS)

File: sys/dev/drm/radeon/ni_dpm.c (pass 2) Family: radeon twin of si DF-3610 / ci DF-3646 (amdgpu DF-3586 / DF-3623)

Build

cc -O2 -Wall -o harness harness.c

(No AMD NI GPU on the QEMU audit guest β€” virtio-only β€” so verification is a userspace logic harness that executes the exact kernel call graph with verbatim structs and offsets; see env.txt for the hardware/module check.)

Run

./harness

Expected output

[A] ... child died from signal 11: read at NULL+0x28
    >>> kernel: page-fault at 0x28 -> panic during DPM enable <<<
[A2] ... child died from signal 11: read at NULL+0x0
    >>> kernel: page-fault at 0 -> panic on first state change <<<
[B] Fixed path ... rc=-EINVAL (DPM enable refused, no dereference)

[A] proves the boot_ps==NULL dereference chain (ni_dpm_enable -> ni_init_smc_table -> ni_populate_smc_initial_state(NULL) -> ni_get_ps(NULL)->ps_priv, fault address 0x28 = offsetof(struct radeon_ps, ps_priv) on LP64). [A2] proves the ps_priv==NULL variant (ni_update_requested_ps: *new_ps with new_ps==NULL, fault address 0x0). [B] shows the fix.diff guard rejecting the configuration instead.

Fix

fix.diff β€” refuse to enable DPM without a boot state (mirrors the si DF-3610 fix). Validated: applies cleanly to pristine source (alone and stacked with DF-3755's fix), compiles -Werror clean as a kernel object (make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC ni_dpm.o, see fix_build.log), and the harness [B] path shows enable refused with no fault.

VERDICT.md
↓ download raw

DF-3754 β€” Verdict

Verdict: REPRODUCED (source-level + harness) β€” NULL-deref panic, no escalation (deterministic NULL, no attacker-controlled offset)

radeon (Northern Islands) twin of si DF-3610 / ci DF-3646.

Bug confirmation

rdev->pm.dpm.boot_ps is assigned in exactly one place on the ni parse path β€” ni_parse_pplib_non_clock_info():

ni_dpm.c:3914   if (rps->class & ATOM_PPLIB_CLASSIFICATION_BOOT)
ni_dpm.c:3915       rdev->pm.dpm.boot_ps = rps;

That function is only reachable inside the per-state block of ni_parse_power_table() guarded by if (power_info->pplib.ucStateEntrySize - 1) (ni_dpm.c:4018). ni_parse_power_table() never verifies that a boot state was found and returns 0 as long as the header parses. Crafted-VBIOS shapes that leave boot_ps == NULL while parsing "succeeds":

  1. ucNumStates == 0 β€” the state loop is a no-op, num_ps = 0;
  2. ucStateEntrySize == 1 β€” the whole per-state block is skipped, so rdev->pm.dpm.ps[i].ps_priv also stays NULL for every state and the non-clock info (BOOT classification) is never parsed;
  3. ucNumStates >= 1 but no state carries ATOM_PPLIB_CLASSIFICATION_BOOT (0x0008) in usClassification.

The consumer chain has no guard anywhere:

radeon_pm.c:1448     radeon_dpm_init(rdev);                 // return IGNORED
radeon_pm.c:1449     current_ps = requested_ps = boot_ps;   // NULL ok'd
radeon_pm.c:1453     ret = radeon_dpm_enable(rdev);
ni_dpm.c:3589        struct radeon_ps *boot_ps = rdev->pm.dpm.boot_ps;
ni_dpm.c:3641        ret = ni_init_smc_table(rdev);
ni_dpm.c:1943          radeon_boot_state = rdev->pm.dpm.boot_ps;   // NULL
ni_dpm.c:1975          ni_populate_smc_initial_state(rdev, NULL, table);
ni_dpm.c:1683            struct ni_ps *initial_state = ni_get_ps(NULL);
ni_dpm.c:733-735         return rps->ps_priv;    // read at NULL+0x28 -> panic

offsetof(struct radeon_ps, ps_priv) == 0x28 (radeon.h:1342-1356, LP64). Later NULL derefs on the same pointer: ni_dpm.c:3657 (ni_populate_mc_reg_table(rdev, boot_ps)), ni_dpm.c:3673 (ni_populate_smc_tdp_limits), ni_dpm.c:3698 (ni_update_current_ps), ni_dpm_disable:3738. The ps_priv-NULL variant (shape 2) additionally faults on the first power-state change: ni_dpm_pre_set_power_state:3771 -> ni_update_requested_ps:3576/3581 (ni_pi->requested_ps = *new_ps with new_ps == NULL, fault at 0x0), and in ni_dpm_print_power_state:4282.

DPM is default-enabled for the NI asics (BARTS/TURKS/CAICOS/CAYMAN/ARUBA, radeon_pm.c:1546-1572: radeon_dpm = -1 auto -> PM_METHOD_DPM whenever rlc/smc firmware is present β€” radeonkmsfw_* modules ship in /boot/kernel).

Harness confirmation

run.log:

[A] ... child died from signal 11: read at NULL+0x28
    >>> kernel: page-fault at 0x28 -> panic during DPM enable <<<
[A2] ... child died from signal 11: read at NULL+0x0
    >>> kernel: page-fault at 0 -> panic on first state change <<<
[B] Fixed path ... rc=-EINVAL (DPM enable refused, no dereference)

The harness builds a synthetic VBIOS PowerPlayInfo table, runs a verbatim replica of ni_parse_power_table's walk (including the ucStateEntrySize - 1 promotion quirk) over it, then executes the real consumer call graph; the forked children take SIGSEGV at exactly the predicted fault addresses (0x28 and 0x0) β€” the in-kernel equivalent is an unrecoverable page fault / panic during DPM enable.

Trigger conditions (not met on this guest)

  1. AMD NI (Barts/Turks/Caicos/Cayman/Aruba) GPU with radeon DPM β€” the audit QEMU guest is virtio-only (no 0x1002 PCI device; verified in env.txt).
  2. radeon.ko loaded and DPM enabled (default for NI).
  3. Crafted VBIOS (reflash / KVM passthrough with custom ROM / supply-chain).

Exploit chain

None β€” the faulting pointer is fully deterministic NULL; there is no attacker-controlled offset or content. Pure boot-time DoS (panic) with a malicious VBIOS. Same conclusion as the si/ci twins.

Fix validation

fix.diff (guard in ni_dpm_enable, mirrors si DF-3610's fix): git apply/patch -p1 clean against pristine source β€” alone and stacked with DF-3755's fix; compiles -Werror clean as a kernel object (make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC ni_dpm.o, full CFLAGS in fix_build.log); harness [B] shows enable refused, no fault.

fix_status: fixed (compile + harness-level); live HW boot test impossible on this guest.

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

fixed (compile + harness level): fix.diff applies clean alone and stacked with DF-3755's fix, compiles -Werror clean as a kernel object (make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC ni_dpm.o), and the harness fixed-path returns -EINVAL with no dereference. Live-HW boot test impossible on the virtio-only guest.

fix_build.log (patched ni_dpm.c -Werror compile, guard present at :3593); fix_run.log ([B] rc=-EINVAL, no fault); guest /usr/src restored to pristine and verified by md5 after validation.
↓ fix.diffper-fix-DF-3754

Confirmed kernel references

Detail

Exploit chain

None possible: deterministic NULL dereference, no attacker-controlled offset or content; boot-time DoS (panic) with a crafted VBIOS only.

Evidence (decisive lines)

run.log [A]/[A2] (child SIGSEGV at 0x28 and 0x0 = the exact kernel fault addresses), harness.c (verbatim ni_parse_power_table walk incl. the ucStateEntrySize-1 promotion quirk + consumer call graph), fix_run.log ([B] fixed path refuses), fix_build.log (patched ni_dpm.o -Werror clean), env.txt (virtio-only guest, radeon.ko+fw present, no 0x1002 PCI dev)

PoC changes

No seed existed; harness written from scratch (verbatim-replica methodology of DF-3610). Two iterations: missing errno.h include; unused-var cleanup via faithful clock_info use.

Verified recommended fix

Guard ni_dpm_enable: refuse DPM enable when rdev->pm.dpm.boot_ps == NULL (return -EINVAL); see fix.diff.

Verdict

Source-certain NULL-deref, ni twin of DF-3610 (si) / DF-3646 (ci). rdev->pm.dpm.boot_ps is set only in ni_parse_pplib_non_clock_info (ni_dpm.c:3914-3915) behind the ucStateEntrySize!=1 guard (ni_dpm.c:4018); crafted VBIOS shapes (ucNumStates==0, ucStateEntrySize==1, or no ATOM_PPLIB_CLASSIFICATION_BOOT state) leave it NULL while ni_parse_power_table returns 0. radeon_pm_init_dpm ignores the dpm.init rc (radeon_pm.c:1448), publishes current_ps=requested_ps=boot_ps (NULL, :1449) and calls radeon_dpm_enable (:1453) -> ni_dpm_enable:3589/3641 -> ni_init_smc_table:1943/1975 -> ni_populate_smc_initial_state:1683 -> ni_get_ps(NULL)->ps_priv = read at NULL+0x28 -> unrecoverable page fault / panic at default-enabled DPM (BARTS/TURKS/CAICOS/CAYMAN/ARUBA). The ucStateEntrySize==1 shape also leaves ps_priv NULL for all states -> first power-state change faults in ni_update_requested_ps (ni_dpm.c:3576/3581, *NULL read). Verified via a userspace logic harness executing the verbatim call graph over a synthetic VBIOS: children SIGSEGV at exactly 0x28 and 0x0 (run.log); no AMD GPU exists on the virtio-only audit guest (env.txt), so the kernel-context panic itself is hardware-gated. No escalation: the pointer is deterministic NULL. fix.diff (ni_dpm_enable boot_ps guard, mirrors si fix) applies clean, compiles -Werror clean as ni_dpm.o in the real kernel tree, and the harness fixed-path refuses with -EINVAL.