One-sided OD count clamps: VBIOS counts size the od_feature_capabilities/od_settings_max/min heap allocations downward while vega20_hwmgr consumers index them by fixed driver enums β heap OOB reads at driver init with the leaked u32s printed to the world-readable OD_RANGE sysfs node
Summary
The clamps have no floor: a crafted VBIOS declaring ODSettingCount=1 gets 4-byte arrays while consumers unconditionally index 0..13/0..9 at init - a ~52-byte read window past a small heap object whose adjacent-slab u32s are exfiltrated to unpriv readers via the pp_od_clk_voltage sysfs whenever they pass the OD range gates. Harness-reproduced on guest: 21 OOB reads (farthest +48B) with a planted marker printed as 'SCLK: ... 2779054081Mhz'; floors+bounded-copies fix validated by full amdgpu.ko rebuild -Werror. Crafted-firmware precondition; distinct from DF-1689/1690 (blob reads vs driver-side heap arrays).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4544 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| od_count_oflow.c | β | 15.1 KB | view raw | |
| build.sh | β | 118 B | view raw | |
| run.sh | β | 97 B | view raw | |
| build.log | β | 17 B | view raw | |
| run.log | β | 2.1 KB | view raw | |
| run.2.log | β | 4.3 KB | view raw | |
| fix_run.log | β | 2.1 KB | view raw | |
| fix.diff | β | 2.2 KB | view raw | |
| fix_build.log | β | 8.6 KB | view raw | |
| env.txt | β | 335 B | view raw | |
| README.md | β | 4.1 KB | β raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.6 KB | view raw |
DF-4544 β vega20 OD count clamps are one-sided: VBIOS-sized allocations vs driver-enum indices
Severity: Medium (heap OOB read + filtered heap info leak via sysfs) | Confidence: certain
File: sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:823-846
What
init_powerplay_table_information() clamps the VBIOS-provided
OverDrive8Table.ODFeatureCount / ODSettingCount from above only
(lines 823-833):
od_feature_count = (le32_to_cpu(...ODFeatureCount) >
ATOM_VEGA20_ODFEATURE_COUNT) ? ATOM_VEGA20_ODFEATURE_COUNT : ...;
od_setting_count = (le32_to_cpu(...ODSettingCount) >
ATOM_VEGA20_ODSETTING_COUNT) ? ATOM_VEGA20_ODSETTING_COUNT : ...;
and then sizes the heap copies by those counts
(copy_overdrive_feature_capabilities_array :694-695,
phm_copy_overdrive_settings_limits_array smu_helper.c:73-74). A table
that declares ODSettingCount = 1 therefore gets 4-byte
od_settings_max / od_settings_min allocations and a 1-byte
od_feature_capabilities allocation (ODFeatureCount = 1).
But the consumers in vega20_hwmgr.c index those arrays with the fixed
driver-side enums, unconditionally, at driver init:
vega20_od8_set_feature_capabilitiesreadsod_settings_max/min[OD8_SETTING_*]for indices 1..13 (vega20_hwmgr.c:906-976;OD8_SETTING_IDvega20_hwmgr.h:398-417) andod_feature_capabilities[1..9](:913-975)- the fill loop (vega20_hwmgr.c:1216-1227) copies
od_settings_min/max[i]intood8_settings_array[i].min/max vega20_print_clock_levels(OD_RANGE)(vega20_hwmgr.c:2861-2905) prints those values to the world-readable pp_od_clk_voltage sysfs node.
With ODSettingCount = 1 the first OOB read is
od_settings_max[OD8_SETTING_GFXCLK_FMAX = 1] (vega20_hwmgr.c:907) β
bytes 4..7 past a 4-byte kmalloc object; the farthest consumer index (13)
reads at +52 B. A crafted VBIOS (or malicious vGPU ROM) therefore gets a
heap out-of-bounds read window of up to ~52 bytes past a small kernel
heap object, with the leaked u32s filtered only by the OD range gates and
then exposed to unprivileged userspace through the OD_RANGE sysfs dump.
Distinct from the knowns: DF-1689/DF-1690 are OOB reads of the VBIOS blob
itself (no size validation); this finding is about the driver-side heap
copies being sized by firmware counts that the clamps only bound from
above.
Reproduce (lifted-copy harness, DF-1179/DF-4395 convention)
The in-kernel path needs a Vega20 ASIC with a crafted VBIOS (env.txt shows the guest has only the QEMU std VGA β no AMD GPU), so the harness replicates the exact struct layouts (vega20_pptable.h, pack(1)), the exact clamp logic, the kmalloc-sized arrays with a marker-filled "adjacent slab" tail, and the exact consumer condition sequence.
./build.sh # cc -O2 -Wall (guest cc 8.3) ./run.sh
Expected (run.log):
- CONTROL full counts (10/14) β 0 OOB reads, normal OD_RANGE output
- CRAFTED ODFeatureCount=10 / ODSettingCount=1 β od_settings_max
4-byte object: 21 OOB reads, farthest +48 B past; od_settings_min
16 OOB reads; OD_RANGE prints SCLK: 800Mhz 2779054081Mhz β
2779054081 = 0xA5A50001, the marker planted at the first u32 past
the object β heap leak to sysfs demonstrated
- CRAFTED ODFeatureCount=1 β od_feature_capabilities 1-byte object:
9 OOB reads (indices 1..9)
- PATCHED (fix.diff: allocations floored at the driver counts) β same
crafted input, 0 OOB reads, no markers in the output
Fix validation
fix.diffβgit apply --checkOK; allocations floored at ATOM_VEGA20_ODFEATURE_COUNT / ATOM_VEGA20_ODSETTING_COUNT, copies bounded- full
amdgpu.komodule rebuilt with the patch applied under the real kernel toolchain (-Werror): RC=0 (fix_build.log); guest /usr/src restored to pristine afterwards - harness PATCHED control: 0 OOB reads for the same crafted input
Files
od_count_oflow.cβ the harness (sha256 a55b58d859d8addc4358bea67a58bcba7faff0150d0f38c999bf4a2c3854f1a2)build.sh/run.sh,build.log/run.log/run.2.log/fix_run.logfix.diff,fix_build.log,env.txt,VERDICT.md,manifest.json,verdict.json
VERDICT β DF-4544
Reproduced: YES (lifted-copy harness on the audit guest; in-kernel path hardware-gated)
- Root cause proven (vega20_processpptables.c:823-846): the clamps
min(VBIOS_count, ATOM_VEGA20_ODSETTING_COUNT)are one-sided β they only bound the count from above. The kzalloc calls (copy_overdrive_feature_capabilities_array :695, phm_copy_overdrive_settings_limits_array smu_helper.c:73-75) size the heap arrays by exactly that clamped count. - Consumer indices proven (vega20_hwmgr.c): vega20_od8_set_feature_ capabilities (:906-976) indexes od_settings_max/min at OD8_SETTING_GFXCLK_FMAX=1 first (line 907) and up to index 13; the fill loop (:1216-1227) writes od_settings_min/max[i] into od8_settings_array[i]; print_clock_levels(OD_RANGE) (:2861-2905) prints min/max to the world-readable OD sysfs node. Driver enum has 16 members (vega20_hwmgr.h:398-417); the fill loop guards i via feature_id which is only ever set for 0..13 β hence indices 0..13 vs an allocation that can legally be a single u32.
- Harness result (run.log, deterministic, 3 runs incl. run.2.log):
crafted ODFeatureCount=10/ODSettingCount=1 β od_settings_max = 4-byte
object, 21 OOB reads, farthest +48 B past; od_settings_min 16 OOB reads;
od_feature OOB (idx 1..9) with ODFeatureCount=1 β 9 reads past a 1-byte
object. The u32 at the first OOB slot (0xA5A50001 = 2779054081) passed
the OD gates and was printed as
SCLK: 800Mhz 2779054081Mhzin the replicated OD_RANGE output β the sysfs exfil path is real, gated only by comparisons the attacker's heap contents must satisfy (> 0, >= min, <= 100 for the power-percentage setting). In the kernel the values read are whatever follows the small kmalloc object in its slab. - Controls: full-count table β 0 OOB reads; PATCHED (floors) β 0 OOB reads, no markers printed.
Why not uid=0
Information-disclosure class, not corruption: the primitive is a bounded (few dozen bytes) read window past a small heap object, filtered through range gates, at driver-load time (one-shot β the arrays are parsed once at pptable_init). No write primitive, no repeat trigger without a driver reload. Realistic ceiling: opportunistic heap-content disclosure to an unprivileged sysfs reader on a machine whose VBIOS/ROM was crafted. Threat model requires attacker-controlled firmware (physical flash / vGPU+custom ROM), same precondition as the known DF-1689/DF-1690 family.
Fix validation
git apply --checkOK (generated against pristine sys/; never applied there)- Full amdgpu.ko rebuild with the fix under the real kernel toolchain with -Werror: RC=0 (fix_build.log); /usr/src restored + verified
- Harness PATCHED control: 0 OOB reads for the same crafted input
Kernel refs
sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:823, sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:829, sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:835, sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:839, sys/dev/drm/amd/powerplay/hwmgr/smu_helper.c:73, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.h:398, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:906, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:1216, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2861, sys/dev/drm/amd/powerplay/hwmgr/vega20_pptable.h:76
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_testablePatched kernel not runnable on the guest (no AMD ASIC). fix.diff is git-apply-able, compiles clean in a full amdgpu.ko module build under the real kernel toolchain with -Werror (fix_build.log, RC=0), and the harness PATCHED control shows 0 OOB reads for the same crafted input.
fix.diff; fix_build.log (module rebuild RC=0, 0 errors); run.log PATCHED section (0 OOB reads, no markers printed)
Confirmed kernel references
- sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:823
- sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:829
- sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:835
- sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:839
- sys/dev/drm/amd/powerplay/hwmgr/smu_helper.c:73
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.h:398
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:906
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:1216
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2861
- sys/dev/drm/amd/powerplay/hwmgr/vega20_pptable.h:76
Detail
Exploit chain
crafted VBIOS/vGPU ROM with ODFeatureCount=10, ODSettingCount=1 -> pptable_init allocates 4-byte od_settings_max/min -> vega20_od8_set_feature_capabilities reads indices 1..13 past the object -> values passing the range gates propagate to od8_settings_array -> OD_RANGE sysfs dump prints adjacent-slab u32s to unprivileged readers (read window ~52 B, one-shot at driver load)
Evidence (decisive lines)
run.log (CRAFTED: 21 OOB reads farthest +48 B, 'SCLK: 800Mhz 2779054081Mhz' = marker 0xA5A50001 leaked; CONTROL + PATCHED: 0 OOB); run.2.log (repeat, deterministic); fix_run.log (decisive rerun incl. PATCHED); fix_build.log (full amdgpu.ko rebuild with fix.diff, -Werror, RC=0); fix.diff (git apply --check OK)
PoC changes
authored fresh this run (pass 2); no seed PoC existed
Verified recommended fix
floor the od_* allocations at the driver-enum counts (ATOM_VEGA20_ODFEATURE_COUNT / ATOM_VEGA20_ODSETTING_COUNT) and bound the copies by the table counts (fix.diff)
Verdict
One-sided count clamps in vega20_processpptables.c:823-833: ODFeatureCount/ODSettingCount from the VBIOS size the od_feature_capabilities (1-byte entries) and od_settings_max/min (4-byte entries) heap allocations downward (kzalloc at :695 / smu_helper.c:73-75), while the vega20_hwmgr.c consumers index them with fixed driver enums (OD8_SETTING_ 0..13, ATOM_VEGA20_ODFEATURE_ 0..9) unconditionally at driver init. With a crafted table declaring ODSettingCount=1 the first OOB read is od_settings_max[1] (vega20_hwmgr.c:907), 4 bytes past a 4-byte object; the farthest index (13) reads +52 B. The values are copied into od8_settings_array min/max (:1219-1221) and printed by print_clock_levels(OD_RANGE) (:2861-2905) to the world-readable pp_od_clk_voltage sysfs node. Reproduced on the audit guest with a faithful lifted-copy harness (struct layouts from vega20_pptable.h pack(1), exact clamp+alloc+consumer sequence, marker-filled adjacent-slab emulation): 21 OOB reads up to +48 B, and the first past-object u32 (0xA5A50001) passed the OD gates and appeared in the replicated sysfs output -- a filtered kernel-heap info leak to unprivileged readers. CONTROL (full counts) and PATCHED (allocations floored at driver counts) show 0 OOB reads. In-kernel path hardware-gated (no AMD GPU on the guest, env.txt); crafted-VBIOS threat model shared with knowns DF-1689/DF-1690 but a distinct defect (driver-side heap arrays vs blob OOB reads).
No comments yet.