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

Zero-level power state: OOB read performance_levels[-1] and integer underflow memset overflow in SMC state upload

Summary

si_parse_power_table can produce power state with performance_level_count=0 (BIOS state with ucNumDPMLevels=0 or all clockInfoIndex out of range). si_convert_power_state_to_smc only guards count>MAX not count==0, reads performance_levels[count-1]=levels[-1] OOB. si_upload_sw_state computes state_size=(0-1)*sizeof(level) underflows to ~SIZE_MAX, memset(smc_state,0,~SIZE_MAX) overflows heap. Also si_dpm_get_sclk/mclk read levels[count-1] from sysfs paths. Attacker: crafted VBIOS via VFIO romfile. Fix: reject count==0 in parser and consumers.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1128 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness: OOB read of performance_levels[-1] when count==0 5.0 KB view raw
fix.diff suggested-fix reject count==0 in si_convert_power_state_to_smc + sclk/mclk getters 1.7 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 240 B view raw
run.sh run-script ./harness 91 B view raw
run.log run-log decisive harness run, full output 1.2 KB view raw
fix_build.log build-log amdgpu.ko rebuilt cleanly with fix applied (3,741,128 bytes) 23.8 KB view raw
env.txt environment uname, cc version, kldstat 278 B view raw
VERDICT.md verdict full narrative: OOB read confirmed, state_size underflow claim refuted 6.0 KB ↓ raw
README.md readme human-facing summary 2.1 KB ↓ 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 human-facing summary
↓ download raw

DF-1128 β€” amdgpu/si_dpm.c zero performance_level_count OOB read

TL;DR

  • Status: REPRODUCED (source + harness). Primitive is an OOB read of state->performance_levels[count-1] when performance_level_count == 0, confirmed by source trace + a userspace math harness that mirrors the kernel code.
  • Impact: leak / panic (read primitive; not write-capable). The same pattern is reachable from sysfs via si_dpm_get_sclk/si_dpm_get_mclk which can leak the OOB-read value to userspace.
  • Caveat on the finding summary: the state_size underflow half of the summary is incorrect β€” with the real struct layout it evaluates to 4, not ~SIZE_MAX. See VERDICT.md Β§"Finding-summary accuracy note".

Why no live trigger on this guest

The bug is in amdgpu.ko (AMD Southern Islands DPM). The QEMU audit guest has no AMD GPU (pciconf -l shows no ATI/AMD display device), so amdgpu.ko is never kldloaded at boot. The trigger requires SI-class hardware plus a crafted VBIOS whose ATOM_PPLIB power state has ucNumDPMLevels==0 (or all clockInfoIndex values out of range).

Files

  • harness.c β€” userspace logic harness mirroring si_convert_power_state_to_smc and the sclk/mclk getters. Demonstrates the OOB read primitive.
  • fix.diff β€” git-apply-able unified diff against sys/dev/drm/amd/amdgpu/si_dpm.c. Rejects count==0 in si_convert_power_state_to_smc, si_upload_sw_state, and the sclk/mclk getters.
  • run.log β€” full output of the harness run.
  • env.txt β€” guest environment.

Reproduce

./build.sh && ./run.sh

Expected: harness prints the OOB read of performance_levels[-1] (returns 0xcccccccc from the simulated pre-object backing buffer), confirms the finding's state_size underflow claim is wrong (real value is 4), and demonstrates that the same count-1 pattern is used by the sysfs-readable si_dpm_get_sclk/si_dpm_get_mclk.

Fix validation

The fix.diff was applied to the in-guest /usr/src tree and amdgpu.ko was rebuilt with make from /usr/src/sys/dev/drm/amd. The full module linked successfully (3.74 MB amdgpu.ko). See VERDICT.md for the build log excerpt.

VERDICT.md verdict full narrative: OOB read confirmed, state_size underflow claim refuted
↓ download raw

DF-1128 β€” Verdict

Verdict: REPRODUCED (source-level + harness) β€” leak / panic class, no escalation chain (read-only primitive)

Bug confirmation

si_parse_power_table (amdgpu/si_dpm.c:7210-7300) iterates over power_state->v2.ucNumDPMLevels (a BIOS-controlled u8) and calls si_parse_pplib_clock_info for each in-range clockInfoIndex. The only place that sets ps->performance_level_count is line 7141 inside si_parse_pplib_clock_info. So if the BIOS state has ucNumDPMLevels==0 OR all clockInfoIndex values are >= ucNumEntries, the inner loop body never runs and performance_level_count stays at its kzalloc-initialized 0.

That count then reaches three consumers with no count==0 guard:

  1. si_convert_power_state_to_smc at amdgpu/si_dpm.c:5662-5665: c if (state->performance_level_count > SISLANDS_MAX_HARDWARE_POWERLEVELS) return -EINVAL; threshold = state->performance_levels[state->performance_level_count-1].sclk * 100 / 100; The guard only rejects count > MAX. With count == 0, the array index (u16)0 - 1 is computed in int as -1, so the access is performance_levels[-1] β€” a 4-byte OOB read of the sclk field before the start of the performance_levels array (i.e. into the preceding slab object / slab metadata).

  2. si_dpm_get_sclk at amdgpu/si_dpm.c:7876 (sysfs path): c return requested_state->performance_levels[requested_state->performance_level_count - 1].sclk; Same OOB read; the result is returned to userspace as the requested sclk via radeon_pm_info-style debugfs/sysfs. β†’ info leak of adjacent slab data.

  3. si_dpm_get_mclk at amdgpu/si_dpm.c:7888: identical OOB pattern for mclk.

A sibling guard at line 2405 (si_populate_power_containment_values) already does the right thing:

if (state->performance_level_count == 0)
    return -EINVAL;

So the fix is well-precedented inside the same file; the three sites above simply missed it.

Harness confirmation

harness.c mirrors the kernel arithmetic. With performance_level_count == 0 and a backing buffer initialised to 0xcc..., the harness reads threshold = 0xcccccccc from performance_levels[-1] β€” exactly the OOB read the source trace predicts. Output captured in run.log.

Finding-summary accuracy note

The finding summary also claims:

si_upload_sw_state computes state_size=(0-1)*sizeof(level) underflows to ~SIZE_MAX, memset(smc_state,0,~SIZE_MAX) overflows heap.

This is incorrect for the actual struct layout. With the real definitions in sislands_smc.h: - sizeof(struct SISLANDS_SMC_SWSTATE) == 4 (header) + sizeof(LEVEL) - sizeof(struct SISLANDS_SMC_HW_PERFORMANCE_LEVEL) == sizeof(LEVEL)

So:

state_size = sizeof(SWSTATE) + ((count - 1) * sizeof(LEVEL))
           = (4 + sizeof(LEVEL)) + (-1 * sizeof(LEVEL))
           = 4   (after unsigned wrap mod 2^64 then truncation to u32)

The harness confirms this: state_size = 0x00000004 (4 bytes). The memset(smc_state, 0, 4) therefore only writes 4 bytes β€” not a heap overflow. The realistic primitive is the OOB read at line 5665 and the matching sysfs-readable OOB reads at 7876/7888.

The OOB read is the actionable bug. This finding is therefore downgraded internally from "integer underflow memset heap overflow" to "missing zero count guard β†’ OOB read / info leak". The fix.diff still closes all three sites.

Exploit chain

This is a read-only primitive (4-byte OOB read before the slab object backing struct si_ps, plus sysfs exfiltration via si_dpm_get_sclk / si_dpm_get_mclk). No write capability β†’ no privilege-escalation chain exists for this bug on its own. The realistic impact ceiling is:

  • Info leak of 4 bytes of adjacent kernel heap (slab metadata or neighbouring object) readable via the si_dpm_get_sclk(!low) sysfs path. Repeated calls with different kzalloc bucket grooming could harvest slab layout / pointer bits β€” useful as a KASLR-defeat helper on systems where KASLR is enabled (DragonFly disables it by default, so this is mostly informational on default installs).
  • Panic if the read crosses into an unmapped page (rare; depends on slab layout).

Trigger conditions (not met on this guest)

  1. AMD Southern Islands GPU present (PCI vendor 1002, SI family).
  2. amdgpu.ko loaded (default on systems with the hardware).
  3. Crafted VBIOS whose ATOM_PPLIB_POWERPLAYTABLE produces a state with performance_level_count == 0. Reachable via VFIO GPU passthrough with a reflashed ROM, or an emulated KVM GPU with a custom ROM.

The QEMU audit guest has no AMD GPU; amdgpu.ko is present in /boot/kernel/ but never loaded. The bug is therefore confirmed at the source + harness level, not via a live runtime trigger. This matches the "latent bug, harness-confirmed primitive" outcome documented for similar driver-only findings.

Fix

fix.diff rejects count == 0 in: - si_convert_power_state_to_smc (line 5662 β€” guards the line 5665 OOB read) - si_upload_sw_state (line 5738 β€” defense-in-depth, since the line 5665 guard now fires earlier; still worth keeping for robustness) - si_dpm_get_sclk / si_dpm_get_mclk (lines 7876, 7888 β€” guards the sysfs-readable OOB read)

The fix is minimal and mirrors the existing guard at line 2405.

Fix validation

  1. git apply --check (well, patch -p1 --check since /usr/src isn't a git repo on the guest) β€” clean apply, all 4 hunks.
  2. cd /usr/src/sys/dev/drm/amd && make with the diff applied β€” amdgpu.ko built cleanly (rc=0, 3,741,128 bytes).
  3. Reverted with patch -R -p1.

Since the bug cannot be triggered live on the guest (no AMD GPU), the "behaviour comparison" half of fix-validation is done at the harness level: the harness includes both the buggy and the fixed code paths side-by-side; the buggy path performs the OOB read, the fixed path returns the -EINVAL equivalent. See run.log for the side-by-side output.

fix_status: fixed (compiles cleanly, harness confirms the patched code path rejects the bad input).

Fix verification

fixed

validated

radeon.ko build rc=0 + harness before/after
↓ fix.diffn/a (module-level)

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source+harness. si_convert_power_state_to_smc count=0 -> performance_levels[-1] OOB read. No AMD GPU.