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

Heap buffer overflow via unbounded ucStateEntrySize in power table parsing

Summary

ni_parse_power_table() at ni_dpm.c:4030: loop uses VBIOS-controlled ucStateEntrySize (u8, 0-255) as bound, passes loop index j directly to ni_parse_pplib_clock_info() which indexes performance_levels[NISLANDS_MAX_SMC_PERFORMANCE_LEVELS_PER_SWSTATE=16] at :3927. No bounds check before call. ucStateEntrySize>=18 overflows heap kzalloc(sizeof(struct ni_ps)~324B) with attacker-controlled clock/voltage values. Sibling of DF-1136/DF-1141/DF-1166/DF-1179 VBIOS dpm_levels overflow pattern. Requires crafted VBIOS (physical access or privileged flash). Fix: check j>=NISLANDS_MAX_SMC_PERFORMANCE_LEVELS_PER_SWSTATE before call.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1203 Β· 9 files
FileTypeDescriptionSize
VERDICT.md verdict full path:line trace + reachability analysis 3.8 KB ↓ raw
README.md readme claim, verdict, threat model, fix 2.2 KB ↓ raw
fix.diff suggested-fix cap loop at NISLANDS_MAX_SMC_PERFORMANCE_LEVELS_PER_SWSTATE (16) 679 B view raw
run.sh reachability-probe shell probe: is radeon parser reachable on this guest? 816 B view raw
build.sh build-noop no userspace build (input is GPU VBIOS, not a syscall) 307 B view raw
env.txt environment guest uname, cc, device topology 718 B view raw
fix_build.log build-log compile-validation: kernel+module build with fix applied, rc=0, no errors 139.5 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme claim, verdict, threat model, fix
↓ download raw

DF-1203 β€” Heap overflow via unbounded ucStateEntrySize (radeon ni_dpm.c)

Claim

ni_parse_power_table() (sys/dev/drm/radeon/ni_dpm.c:4030) loops for (j = 0; j < ucStateEntrySize - 1; j++) driven by a VBIOS-supplied u8 ucStateEntrySize (0-255) and passes the loop index j straight into ni_parse_pplib_clock_info(... index=j), which indexes ps->performance_levels[index] at ni_dpm.c:3927. That array has exactly NISLANDS_MAX_SMC_PERFORMANCE_LEVELS_PER_SWSTATE = 16 slots (nislands_smc.h:28). The backing object is a heap kzalloc(sizeof(struct ni_ps)) (ni_dpm.c:4020). A VBIOS with ucStateEntrySize >= 17 therefore writes attacker-chosen clock/voltage fields past the end of the struct ni_ps heap object.

Verdict

NOT TESTABLE on this audit guest (real bug, traced line-by-line in sys/, fix authored and compile-validated). See VERDICT.md.

Why not reproduced here

  • radeon is not in the default kernel config (sys/config/X86_64_GENERIC has no device radeon); it is optional radeon drm in sys/conf/files (a loadable KLD, present only in LINT64).
  • The guest has no AMD radeon GPU (pciconf shows only the QEMU stdvga 0x11111234 at pci0:0:2:0), so radeon.ko cannot attach and ni_dpm_init() / ni_parse_power_table() never run.
  • kldload of radeon.ko requires root (forbidden precondition) and still needs an AMD GPU to call the parser.

Trigger (threat model)

A malicious or buggy GPU VBIOS AtomPowerTable with pplib.ucStateEntrySize set >= 17. Parsed on any system that loads the radeon DRM module against an AMD Northern Islands (Cayman/Barts/Turks/Antilles) GPU. Impact: kernel heap overflow of sizeof(struct rv7xx_pl) per extra slot, content = GPU-supplied clock/voltage/flags β€” corruption / DoS on default GENERIC; primitive-only on unhardened builds.

Reproduce (on appropriate hardware)

On a host with an AMD NI-class GPU + radeon DRM loaded, a crafted VBIOS image flashed/supplied to the GPU triggers the overflow during DPM init. No userspace syscall reaches this path; the input is the GPU firmware table, so there is no runnable userspace PoC.

Fix

fix.diff bounds the loop index to NISLANDS_MAX_SMC_PERFORMANCE_LEVELS_PER_SWSTATE, matching the existing post-check at ni_dpm.c:2641.

VERDICT.md verdict full path:line trace + reachability analysis
↓ download raw

DF-1203 β€” VERDICT

Finding: Heap buffer overflow via unbounded ucStateEntrySize in radeon power-table parsing (sys/dev/drm/radeon/ni_dpm.c). Status: NOT TESTABLE on this audit guest. Confidence (bug is real): certain. Impact ceiling: kernel heap overflow (corruption / DoS); device/firmware-controlled. Fix: authored in fix.diff, applies clean, compile-validated against the radeon KLD source (see fix_build.log).

Mechanism (confirmed line-by-line in sys/)

  1. ni_dpm_init() β†’ ni_parse_power_table() (ni_dpm.c:3993) reads the AtomPowerTable from the GPU VBIOS.
  2. ni_dpm.c:4020 β€” ps = kzalloc(sizeof(struct ni_ps), GFP_KERNEL) allocates one power-state object. struct ni_ps (ni_dpm.h:172) holds performance_levels[NISLANDS_MAX_SMC_PERFORMANCE_LEVELS_PER_SWSTATE], and that constant is 16 (nislands_smc.h:28).
  3. ni_dpm.c:4030 β€” for (j = 0; j < (power_info->pplib.ucStateEntrySize - 1); j++). ucStateEntrySize is a u8 taken verbatim from the VBIOS (0-255). j is int (ni_dpm.c:3990), so no underflow when ucStateEntrySize == 0.
  4. ni_dpm.c:4035-4037 β€” calls ni_parse_pplib_clock_info(rdev, &ps[i], j, clock_info).
  5. ni_dpm.c:3927 β€” struct rv7xx_pl *pl = &ps->performance_levels[index]; with index == j. No bounds check.
  6. For ucStateEntrySize >= 17, j reaches 16 and performance_levels[16] overflows the heap object. Each extra slot writes sizeof(struct rv7xx_pl) of VBIOS-controlled sclk/mclk/vddc/vddci/flags (ni_dpm.c:3931-3938).

The same code already defends against over-count later at ni_dpm.c:2641 (if (state->performance_level_count > NISLANDS_MAX_SMC_PERFORMANCE_LEVELS_PER_SWSTATE)), confirming 16 is the intended bound β€” it is simply never enforced at the parsing loop.

Why it is NOT TESTABLE on this guest

  • sys/config/X86_64_GENERIC contains no device radeon; radeon is optional radeon drm in sys/conf/files (loadable module, in LINT64 only).
  • pciconf -lv on the guest shows only vgapci0@pci0:0:2:0 chip 0x11111234 (QEMU Standard VGA) β€” no AMD GPU, so radeon.ko cannot attach and the DPM/power-table parser is dead code at runtime here.
  • The input that drives the bug (the AtomPowerTable) lives in the GPU VBIOS, not in any userspace syscall argument; there is no unprivileged path to supply it, and kldload radeon is a root-only action that still requires the matching hardware.

This is the valid "dead/unreachable at runtime on this guest" case from the procedure. The bug is genuine (path:line confirmed above); it is simply latent on a guest lacking the device. Per the realism test, the trigger precondition ("a radeon GPU with a malicious/crafted VBIOS") is a plausible real-world threat (malicious peripheral / tampered firmware), not an unprivileged-user escalation.

Exploit chain

None developed: the primitive is reachable only via GPU firmware on a system with the specific hardware and the radeon module loaded. There is no unprivileged local path to it on this guest, so there is no unpriv→root chain to build here. Documented impact ceiling: heap corruption / DoS from a malicious VBIOS.

Fix

fix.diff caps the loop index at the array bound:

for (j = 0;
     j < (power_info->pplib.ucStateEntrySize - 1) &&
     j < NISLANDS_MAX_SMC_PERFORMANCE_LEVELS_PER_SWSTATE;
     j++) {

Minimal, targeted at the root cause, and consistent with the existing 16-slot invariant. Supersedes the finding proposal's intent with an exact, line-accurate diff.

Build / run on this guest

./build.sh && ./run.sh runs a reachability probe (checks for an AMD radeon GPU + the radeon module). On this guest it reports "no AMD GPU / radeon not loaded β†’ parser unreachable"; the bug itself is confirmed by the source trace above and the compile-validated fix.diff.

Fix verification

not_testable

compile validated -Werror

module/kernel build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. ni_parse_power_table ucStateEntrySize u8 unbounded vs performance_levels[16]. radeon not in GENERIC.