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)
PoC verification
Evidence pack
findings/poc/DF-3754 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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":
ucNumStates == 0β the state loop is a no-op,num_ps = 0;ucStateEntrySize == 1β the whole per-state block is skipped, sordev->pm.dpm.ps[i].ps_privalso stays NULL for every state and the non-clock info (BOOT classification) is never parsed;ucNumStates >= 1but no state carriesATOM_PPLIB_CLASSIFICATION_BOOT(0x0008) inusClassification.
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)
- 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).
radeon.koloaded and DPM enabled (default for NI).- 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
fixedfixed (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.
Confirmed kernel references
- sys/dev/drm/radeon/ni_dpm.c:3914
- sys/dev/drm/radeon/ni_dpm.c:4018
- sys/dev/drm/radeon/ni_dpm.c:3589
- sys/dev/drm/radeon/ni_dpm.c:3641
- sys/dev/drm/radeon/ni_dpm.c:1943
- sys/dev/drm/radeon/ni_dpm.c:1975
- sys/dev/drm/radeon/ni_dpm.c:1683
- sys/dev/drm/radeon/ni_dpm.c:733
- sys/dev/drm/radeon/ni_dpm.c:3576
- sys/dev/drm/radeon/radeon_pm.c:1448
- sys/dev/drm/radeon/radeon.h:1355
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.
No comments yet.