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

num_levels==0 count-1 underflow: kv_dpm_get_sclk (:2938) and kv_set_valid_clock_range (:1791/:1817) read levels[0xFFFFFFFF] ~48 GB out (panic; unprivileged ioctl trigger)

Summary

kv_parse_power_table sets ps->num_levels only for valid clock entries; a VBIOS state with ucNumDPMLevels==0 or all-invalid clockInfoIndex[] keeps num_levels==0 - parse-legal, no error. kv_dpm_get_sclk then evaluates levels[num_levels-1] = levels + ~48 GB - wild kernel read -> page fault -> panic. Reachable UNPRIVILEGED once such a state is requested: AMDGPU_INFO ioctl (DRM_AUTH|DRM_RENDER_ALLOW) and every DCE8 modeset. Same underflow in kv_set_valid_clock_range on the first power-state transition. Count-1 family twin of DF-3686/DF-3858. VERIFIED: harness prints levels[4294967295] + 51.5GB offset; crash mode SIGSEGV on the verbatim read.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3888 Β· 11 files
FileTypeDescriptionSize
num_levels_underflow.c β€” 3.4 KB view raw
build.sh β€” 95 B view raw
run.sh β€” 116 B view raw
build.log β€” 107 B view raw
run.log β€” 280 B view raw
run.2.log β€” 69 B view raw
env.txt β€” 561 B view raw
fix.diff β€” 933 B view raw
VERDICT.md β€” 4.1 KB ↓ raw
README.md β€” 1.7 KB ↓ raw
verdict.json β€” 3.0 KB view raw

DF-3888 β€” num_levels == 0 β†’ levels[num_levels - 1] wild read (+~48 GB) in kv_dpm_get_sclk / kv_set_valid_clock_range

File: sys/dev/drm/amd/amdgpu/kv_dpm.c:2938 (get_sclk), :1791/:1817 (set_valid_clock_range) Class: CWE-191 unsigned count-1 underflow β†’ wild OOB read (count-1 family: DF-3686/DF-3858 twins) Verdict: inconclusive at runtime on this guest (no AMD GPU); source-certain; harness proves the index/offset and SIGSEGV on the verbatim access.

Build (on the DragonFly guest, as root)

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

Run

./t          # accounting β€” index and byte offset
./t crash    # verbatim read -> SIGSEGV (exit 139)

Expected

  • accounting: num_levels = 0; levels[num_levels - 1] = levels[4294967295]; byte offset 4294967295 * 12 = 51539607540 (~51.5 GB) past levels β€” far outside the 5-entry array (60 bytes) inside kv_power_info.
  • crash: Segmentation fault, exit=139 (kernel equivalent: page fault in kernel mode β†’ panic).

Kernel trigger

A parsed power state whose ucNumDPMLevels == 0 (or all clockInfoIndex entries >= ucNumEntries, continue at kv_dpm.c:2758-2759) keeps ps->num_levels == 0 from kzalloc. Sinks, once such a state becomes the requested state:

  • kv_dpm_get_sclk(handle, false) :2938 β€” unprivileged via the AMDGPU_INFO ioctl (AMDGPU_INFO_DEV_INFO, amdgpu_kms.c:700, DRM_AUTH|DRM_RENDER_ALLOW) and display modeset (dce_v8_0.c:971);
  • kv_set_valid_clock_range :1791 / :1817 (else-branch), on the first power state transition to it (needs check_state_equal to differ, kv_dpm.c:3261).
VERDICT.md
↓ download raw

DF-3888 β€” verdict narrative

Finding

kv_parse_power_table only sets ps->num_levels inside kv_parse_pplib_clock_info (kv_dpm.c:2694: ps->num_levels = index + 1), which is called only for valid clock entries β€” the state loop continues on clock_array_index >= clock_info_array->ucNumEntries (:2758-2759) and breaks at k >= SUMO_MAX_HARDWARE_POWERLEVELS (:2760). A VBIOS state with ucNumDPMLevels == 0, or whose clockInfoIndex[] entries are all out of range, therefore leaves the kzalloc-zeroed struct kv_ps with num_levels == 0 β€” parse-legal, no error.

Use sites then compute levels[num_levels - 1] with unsigned num_levels (u32):

  • kv_dpm_get_sclk (kv_dpm.c:2929-2939): return requested_state->levels[requested_state->num_levels - 1].sclk; (:2938) β†’ levels[0xFFFFFFFF] β†’ levels + 51,539,607,540 bytes (~48 GB) β€” wild read. requested_state is kv_get_ps(&pi->requested_rps); after any power-state selection that lands on a 0-level state, pi->requested_ps.num_levels == 0.
  • kv_set_valid_clock_range (:1772-1832): new_ps->levels[new_ps->num_levels - 1].sclk at :1791 (vddc-table branch) and :1817 (sumo-table branch) β€” same wild read, plus the comparison chain at :1797-1798/:1822-1826 reads table->entries[highest_valid] with a highest_valid derived from the poisoned loop.

Impact: page fault in kernel mode β†’ panic (the ~48 GB offset is never mapped); not a leak (value unusable). DoS.

Trigger reach: on a machine whose (crafted) VBIOS contains such a state, the read is reachable unprivileged: the AMDGPU_INFO ioctl (AMDGPU_INFO_DEV_INFO β†’ amdgpu_dpm_get_sclk(adev, false), amdgpu_kms.c:700, DRM_IOCTL_DEF_DRV(AMDGPU_INFO, ..., DRM_AUTH|DRM_RENDER_ALLOW) :1207) β€” i.e. any user holding the render node; also every DCE8 modeset (dce_v8_0.c:971). The kv_set_valid_clock_range sink additionally needs the state to survive kv_check_state_equal (kv_dpm.c:3261: differs from current β†’ first transition).

This is the count-1-underflow family already filed for the powerplay hwmgrs (DF-3686 smu7, DF-3858 vega20); kv_dpm.c is the legacy-DPM twin, unfixed here.

Root cause (kernel references)

What was verified, and how

  1. Source-level certainty. Full-file pass-2 audit; struct geometry (struct kv_pl = 12 bytes, levels[5]) from kv_dpm.h; caller chain verified in amdgpu_pm.c (amdgpu_dpm_change_power_state_locked :1895-1945 β†’ pre_set β†’ check_state_equal β†’ set_power_state) and amdgpu_kms.c (ioctl flags :1207).
  2. Harness on the guest (DragonFly 6.5-DEVELOPMENT x86_64, base cc 8.3): num_levels_underflow.c replicates kv_dpm_get_sclk verbatim with 1:1 layouts. Accounting mode prints index 4294967295, offset 51539607540 bytes (~51.5 GB); crash mode executes the verbatim read against a single-page mapping β†’ SIGSEGV exit=139 (kernel equivalent: page fault in kernel mode β†’ panic).
  3. Hardware absence. Guest is virtio-only; no AMD GPU, amdgpu not loaded. The in-kernel trigger needs a Kaveri/Kabini/Mullins APU with a crafted VBIOS. Recorded inconclusive, impact none on this guest (per pass-2 spawn instructions), source + harness: certain.

Fix validation

fix.diff guards num_levels == 0 (and NULL priv) in kv_dpm_get_sclk and adds an early return in kv_set_valid_clock_range. 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-3888/fix.diff
↓ fix.diffper-fix-DF-3888

Confirmed kernel references

Detail

Exploit chain

crafted-VBIOS state with 0 valid clock levels becomes requested_ps -> unprivileged AMDGPU_INFO ioctl -> amdgpu_dpm_get_sclk(false) -> kv_dpm_get_sclk reads levels[0xFFFFFFFF] (+48 GB) -> kernel page fault -> panic (DoS). Wild-read value is not exfiltrable (offset unmapped).

Evidence (decisive lines)

run.log: 'num_levels = 0', 'levels[4294967295]', 'levels + 51539607540 bytes (~51.5 GB)'; 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

Guard num_levels==0 (and NULL ps_priv) in kv_dpm_get_sclk (kv_dpm.c:2935) and early-return in kv_set_valid_clock_range (:1781).

Verdict

Source-certain unsigned count-1 underflow (DF-3686/DF-3858 family, legacy-DPM twin); runtime-inconclusive on this guest (no AMD GPU). kv_parse_power_table only sets ps->num_levels for valid clock entries (kv_dpm.c:2694, loop :2756-2769 with continue at :2758-2759), so a VBIOS state with ucNumDPMLevels==0 or all-invalid clockInfoIndex[] leaves num_levels==0 from kzalloc. kv_dpm_get_sclk (:2938) then reads levels[num_levels-1] = levels[0xFFFFFFFF] = levels + 51,539,607,540 bytes (~48 GB) - a wild kernel read -> page fault -> panic; reachable UNPRIVILEGED on such a machine via the AMDGPU_INFO ioctl AMDGPU_INFO_DEV_INFO (amdgpu_kms.c:700, DRM_AUTH|DRM_RENDER_ALLOW) and every DCE8 modeset (dce_v8_0.c:971). Same underflow at kv_set_valid_clock_range :1791/:1817 on the first transition to such a state. Harness prints index 4294967295 / offset 51539607540 and crash mode SIGSEGVs on the verbatim read (kernel equivalent: panic).