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)
PoC verification
Evidence pack
findings/poc/DF-3954 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 fromkv_dpm_set_power_state()(:1866/:1895) whenever such a state is picked as requestedkv_dpm_get_sclk()β kv_dpm.c:2886 β reached fromdce8_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).
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
-
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;β callskv_parse_pplib_clock_info()(which setsps->num_levels = index + 1, kv_dpm.c:2620) only when at least one valid entry exists. WithucNumDPMLevels == 0or all-invalid indexes,kstays 0 andnum_levelskeeps its kzalloc value 0. -
The type makes it a wild read, not an off-by-one.
struct kv_ps(kv_dpm.h:79-87) hasu32 num_levelsAFTERstruct 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. -
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].sclkon every power state change of such a state; kv_dpm_get_sclk (kv_dpm.c:2886) evaluates the same forpi->requested_rpsonce that state was picked β andradeon_dpm_pick_power_state(radeon_pm.c:950-1051) matches purely onps->class/class2bits from the VBIOS, never onnum_levels. -
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, sonum_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_testablefix.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
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.
No comments yet.