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

num_levels==0 (VBIOS state with no valid clock entries) -> levels[0xFFFFFFFF] wild read ~48 GiB out in kv_set_valid_clock_range / kv_dpm_get_sclk

Summary

kv_parse_power_table leaves kzalloc'd ps->num_levels == 0 for a state with ucNumDPMLevels==0 or all clockInfoIndex >= ucNumEntries; struct kv_ps has u32 num_levels with levels[] as FIRST member, so levels[num_levels-1] = object + 0xBFFFFFFF4 (~48 GiB). pick_power_state matches class only, never num_levels. VERIFIED: harness prints displacement 0xbfffffff4 (48.00 GiB); child executing the verbatim expression dies SIGSEGV. Boot/mode-set panic on KV-family APU with crafted VBIOS; load-only. amdgpu twin DF-3888.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3954 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
trigger_df3954.c β€” 4.9 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 116 B view raw
build.log β€” 89 B view raw
run.log β€” 1.5 KB view raw
env.txt β€” 725 B view raw
fix.diff β€” 1.5 KB view raw
manifest.json β€” 1.6 KB view raw
verdict.json β€” 3.2 KB view raw

DF-3954 β€” radeon kv_dpm num_levels==0 β†’ levels[0xFFFFFFFF] wild read (~48 GiB)

File: sys/dev/drm/radeon/kv_dpm.c (pass 2, GLM 5.3) Severity: Medium Β· CWE: CWE-191/CWE-125 (integer underflow β†’ OOB read) Confidence: certain (source) Β· Status: inconclusive on guest (no AMD GPU); arithmetic-simulation harness verified.

What

kv_parse_power_table() (kv_dpm.c:2681-2694) advances k only for clock entries whose clockInfoIndex < clock_info_array->ucNumEntries and only while k < SUMO_MAX_HARDWARE_POWERLEVELS. A VBIOS power state with ucNumDPMLevels == 0 β€” or with every clockInfoIndex out of range (continue) β€” never calls kv_parse_pplib_clock_info(), so the kzalloc'd struct kv_ps keeps num_levels == 0 (u32, kv_dpm.h:82; levels[] is the FIRST member).

Consumers index levels[num_levels - 1] with u32 arithmetic β†’ 0 - 1 = 0xFFFFFFFF β†’ levels + 0xFFFFFFFF Γ— sizeof(struct kv_pl)=12 = +0xBFFFFFFF4 (~48 GiB) past the heap object:

  • kv_set_valid_clock_range() β€” kv_dpm.c:1721 (if-path) and :1745-1747 (else-path) β€” reached from kv_dpm_set_power_state() (:1866/:1895) whenever such a state is picked as requested
  • kv_dpm_get_sclk() β€” kv_dpm.c:2886 β€” reached from dce8_bandwidth_update (cik.c:9287/9327) after that state became requested

A zero-level state is pickable: radeon_dpm_pick_power_state() (radeon_pm.c:950-1051) matches on ps->class only, never num_levels.

Reproduce

cd findings/poc/DF-3954
./build.sh          # cc -O2 -Wall -o trigger_df3954 trigger_df3954.c
./run.sh

Expected (crash mode): num_levels(u32) = 0 => index = 0xffffffff => displacement = 0xbfffffff4 (48.00 GiB), child SIGSEGV sig=11, RESULT: WILD READ FAULTS AS PREDICTED. Kernel equivalent: wild kernel read ~48 GiB past kv_ps β†’ page fault β†’ panic.

Threat model

Crafted VBIOS on a KV/BT/ML APU (physical/root/supply-chain). Boot- or mode-set-time panic (DoS). No unprivileged-local vector in radeon (unlike the amdgpu twin DF-3888, which had an info-ioctl caller). The wild read is a load only β€” the value feeds clock comparisons, no leak channel.

Fix

Reject zero-level states at parse time and guard the consumers (fix.diff).

VERDICT.md
↓ download raw

DF-3954 VERDICT

Verdict: INCONCLUSIVE at kernel level (no AMD GPU on guest) β€” source-certain u32-index underflow producing a wild read at +0xBFFFFFFF4 (~48 GiB); the verbatim expression faults in the userland simulation exactly as predicted.

Root cause, traced

  1. Zero-level states parse successfully. kv_parse_power_table (kv_dpm.c:2665-2699): for each state, the inner loop for (j = 0; j < power_state->v2.ucNumDPMLevels; j++) β€” clock_array_index = idx[j]; if (clock_array_index >= clock_info_array->ucNumEntries) continue; if (k >= SUMO_MAX_HARDWARE_POWERLEVELS) break; β€” calls kv_parse_pplib_clock_info() (which sets ps->num_levels = index + 1, kv_dpm.c:2620) only when at least one valid entry exists. With ucNumDPMLevels == 0 or all-invalid indexes, k stays 0 and num_levels keeps its kzalloc value 0.

  2. The type makes it a wild read, not an off-by-one. struct kv_ps (kv_dpm.h:79-87) has u32 num_levels AFTER struct kv_pl levels[SUMO_MAX_HARDWARE_POWERLEVELS] (levels is the FIRST member). levels[num_levels - 1] computes the index in u32: 0xFFFFFFFF, then widens to pointer arithmetic: 0xFFFFFFFF Γ— 12 = 0xBFFFFFFF4 β‰ˆ 48 GiB β€” far outside any mapping.

  3. Sinks. kv_set_valid_clock_range (kv_dpm.c:1721 in the vddc-table branch, :1745-1747 in the sclk_voltage_mapping branch) evaluates new_ps->levels[new_ps->num_levels - 1].sclk on every power state change of such a state; kv_dpm_get_sclk (kv_dpm.c:2886) evaluates the same for pi->requested_rps once that state was picked β€” and radeon_dpm_pick_power_state (radeon_pm.c:950-1051) matches purely on ps->class/class2 bits from the VBIOS, never on num_levels.

  4. Reachability ordering. kv_dpm_get_sclk runs from dce8_bandwidth_update (cik.c:9287/9327, KV is DCE8) gated only on pm_method == PM_METHOD_DPM && dpm_enabled; the requested-state copy (kv_update_requested_ps) has already happened by then via the first successful compute_clocks, so num_levels == 0 (not ps_priv NULL) is what faults β€” the DF-3953 NULL window and this bug are distinct failure modes of the same parse weakness.

Harness

Replicates struct kv_pl/struct kv_ps layouts (with _Static_assert( sizeof(struct kv_pl) == 12)), sets num_levels = 0 exactly as the parse does, and executes the verbatim ps->levels[ps->num_levels - 1].sclk against a guard-page-backed object: child dies SIGSEGV; analysis mode prints the kernel-equivalent displacement 0xbfffffff4 (48.00 GiB).

Why Medium and not higher

Wild load feeding clock comparisons; no write primitive, no direct info leak channel, hardware+VBIOS-gated trigger, boot/mode-set-time panic (DoS). Same class/severity precedent as pass-1 DF-1306/1307 (VBIOS-index OOB reads, Medium) and amdgpu twin DF-3888 (Medium).

Fix validation

fix.diff: (a) kv_parse_power_table skips-and-logs states that produced k == 0, (b) belt-and-braces num_levels == 0 early-outs in kv_set_valid_clock_range and kv_dpm_get_sclk. Not built on the guest (no AMD GPU) β€” fix_status: not_testable; guards are trivially sufficient by inspection.

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; not built - no AMD hardware on the guest. Guards eliminate both sinks by construction.

findings/poc/DF-3954/fix.diff
↓ fix.diffper-fix-DF-3954

Confirmed kernel references

Detail

Exploit chain

crafted-VBIOS state with zero usable clock entries -> picked by class match -> kv_set_valid_clock_range / kv_dpm_get_sclk -> wild read ~48 GiB past kv_ps heap object -> kernel page fault -> panic (DoS). Load-only primitive; value feeds clock comparisons, no leak channel, no write.

Evidence (decisive lines)

run.log: 'num_levels(u32) = 0  =>  index = 0xffffffff  =>  displacement = 0xbfffffff4 (48.00 GiB)', 'child: SIGNALLED sig=11 -> predicted wild read at +0xbfffffff4 confirmed faulting', 'RESULT: WILD READ FAULTS AS PREDICTED'; VERDICT.md full trace; fix.diff

PoC changes

No seed (pass-2 self-authored). One fix cycle: first analysis print used 64-bit unsigned math (0xff..f4) instead of the kernel's u32 index math; corrected to (size_t)(u32)0-1 * 12 = 0xbfffffff4.

Verified recommended fix

Skip zero-clock-entry states in kv_parse_power_table and add num_levels==0 early-outs in kv_set_valid_clock_range and kv_dpm_get_sclk (fix.diff).

Verdict

Source-certain u32-index underflow wild read (amdgpu twin DF-3888; amdgpu's had an info-ioctl caller, radeon's does not - trigger is boot/mode-set with crafted VBIOS). kv_parse_power_table (kv_dpm.c:2681-2694) leaves ps->num_levels==0 for a state with ucNumDPMLevels==0 or all clockInfoIndex >= ucNumEntries ('continue'); struct kv_ps (kv_dpm.h:79-87) has u32 num_levels and levels[] as FIRST member, so 'levels[num_levels - 1]' = levels[0xFFFFFFFF] = object + 0xFFFFFFFF*12 = +0xBFFFFFFF4 (~48 GiB). Sinks: kv_set_valid_clock_range :1721/:1745-1747 (from kv_dpm_set_power_state :1866/:1895 once pick_power_state - which never checks num_levels - selects the state) and kv_dpm_get_sclk :2886 (from dce8_bandwidth_update cik.c:9287/9327). Harness executes the verbatim expression against a guard-page object: SIGSEGV sig=11, displacement 0xbfffffff4 printed. Runtime-inconclusive: no AMD GPU on guest.