Heap OOB read via unbounded vddInd/vddciInd/mvddInd into 4-8 entry voltage lookup tables
Summary
vega10_patch_voltage_dependency_tables_with_lookup_table (:635-681), vega10_populate_single_display_type (:1854-1863), vega10_get_clock_by_type_with_voltage (:4246-4251): vddInd/vddciInd/mvddInd (u8 from VBIOS records, 0-255) used to index vddc_lookup_table (8 slots), vddci_lookup_table (4 slots), vddmem_lookup_table (4 slots) with NO bounds check. OOB read of ~1KB past 4-8 entry tables into adjacent kernel heap. Read values (us_vdd) propagate to SMC and sysfs -> info leak. Fix: bounds-check against table->count.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1181 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace transcription of the vddInd/vddciInd/mvddInd indexing flaw | 4.9 KB | view raw |
| build.sh | build-script | cc -O2 -o harness harness.c | 101 B | view raw |
| run.sh | run-script | ./harness | 67 B | view raw |
| README.md | readme | human-readable summary + reproduce | 2.6 KB | β raw |
| VERDICT.md | verdict | detailed mechanism, source trace, why-not-reproduced | 3.6 KB | β raw |
| fix.diff | suggested-fix | add 7 bounds checks (vddInd/vddciInd/mvddInd/vddcInd < *_lookup_table->count) at all 3 functions | 2.7 KB | view raw |
| run.log | run-log | harness stdout showing OOB reads at vddci[100], vddmem[255] etc | 656 B | view raw |
| fix_build.log | fix-build-log | nativekernel build log showing vega10_hwmgr.c compiles cleanly with fix applied (rc=0, -Werror) | 5.6 MB | β download |
| fix_run.log | fix-run-log | harness on patched kernel (no regression) | 128 B | view raw |
| env.txt | environment | uname, cc version, pciconf | 947 B | view 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 |
DF-1181 β vega10 VBIOS vddInd/vddciInd/mvddInd OOB read
Bug (confirmed in source)
sys/dev/drm/amd/powerplay/hwmgr/vega10_hwmgr.c β three call sites read
the per-entry vddInd / vddciInd / mvddInd fields as uint8_t
(0-255 range, supplied by the VBIOS) and use them to index into
fixed-size voltage lookup tables with no bounds check against the
table's count or its max_levels allocation:
vega10_patch_voltage_dependency_tables_with_lookup_table()lines 659, 666, 672, 675, 678 β five index sites in one function.vega10_populate_single_display_type()lines 1856-1857.vega10_get_clock_by_type_with_voltage()lines 4248-4249.
Allocation (in sys/dev/drm/amd/powerplay/hwmgr/vega10_processpptables.c):
// get_vddc_lookup_table() at lines 1026-1034
table_size = sizeof(uint32_t) + sizeof(phm_ppt_v1_voltage_lookup_record) * max_levels;
table = kzalloc(table_size, GFP_KERNEL);
table->count = vddc_lookup_pp_tables->ucNumEntries; // from VBIOS u8
// callers (lines 1108-1127) pass:
// vddc_lookup_table max_levels = 8
// vddmem_lookup_table max_levels = 4
// vddci_lookup_table max_levels = 4
A malicious VBIOS that supplies vddciInd = 100 therefore indexes
vddci_lookup_table->entries[100], reading
100 * sizeof(phm_ppt_v1_voltage_lookup_record) (= 100 Γ 10 = 1000 bytes)
past the 4-entry (40-byte) allocation. Read values (us_vdd) propagate
into the SMC firmware and into the sysfs power-state tables β an info
leak of adjacent kernel heap, plus corrupted power-management state.
Trigger model
- Requires
amdgpuattached to a real AMD Vega10 GPU (powerplay hwmgr init path). Not present on this guest. - CVSS
AV:L/AC:L/PR:L/UI:N/S:U:C:H/I:N/A:Hβ the analyst'sPR:Lassumes an unprivileged user could supply a crafted VBIOS / PowerPlay table via sysfs; on this code the table is parsed from the device's own firmware at attach time, so in practice the trigger isPR:H(root to flash malicious firmware). The bug class is real either way.
On this guest
Unreachable β no AMD GPU; amdgpu.ko is present but never attaches.
harness.c is a userspace transcription of the indexing logic; it
builds, runs, and prints which indices would OOB-read, but produces no
kernel effect.
Reproduce
ssh dfbsd-maxx "cd poc/DF-1181 && ./build.sh && ./run.sh"
Recommended fix
Bounds-check every vddInd / vddciInd / mvddInd / vddcInd against
the corresponding *_lookup_table->count before indexing; return
-EINVAL (or -1 for the void-return-styled helpers) on violation.
Full patch in fix.diff. Matches the finding proposal.
DF-1181 β VERDICT
Status: NOT REPRODUCED (HW-gated; bug confirmed in source) Impact: none (cannot trigger on this guest) Confidence: certain (line-by-line source trace) Class: heap OOB read (CWE-125) β latent
Mechanism (cited)
The powerplay Vega10 hwmgr patches VBIOS dependency tables by reading
per-entry voltage indices and looking them up in fixed-size allocation
tables. All index fields are uint8_t from the VBIOS:
vega10_hwmgr.c:658-680 (inside
vega10_patch_voltage_dependency_tables_with_lookup_table):
for (entry_id = 0; entry_id < vdt->count; entry_id++) {
voltage_id = vdt->entries[entry_id].vddInd; // u8
vdt->entries[entry_id].vddc =
table_info->vddc_lookup_table->entries[voltage_id].us_vdd; // no check
}
...
for (entry_id = 0; entry_id < mm_table->count; ++entry_id) {
voltage_id = mm_table->entries[entry_id].vddcInd; // u8
mm_table->entries[entry_id].vddc =
table_info->vddc_lookup_table->entries[voltage_id].us_vdd; // no check
}
for (entry_id = 0; entry_id < mclk_table->count; ++entry_id) {
voltage_id = mclk_table->entries[entry_id].vddInd; // u8
mclk_table->entries[entry_id].vddc =
table_info->vddc_lookup_table->entries[voltage_id].us_vdd;
voltage_id = mclk_table->entries[entry_id].vddciInd; // u8
mclk_table->entries[entry_id].vddci =
table_info->vddci_lookup_table->entries[voltage_id].us_vdd;
voltage_id = mclk_table->entries[entry_id].mvddInd; // u8
mclk_table->entries[entry_id].mvdd =
table_info->vddmem_lookup_table->entries[voltage_id].us_vdd;
}
vega10_hwmgr.c:1856-1857 (vega10_populate_single_display_type):
vddc = table_info->vddc_lookup_table->
entries[dep_table->entries[i].vddInd].us_vdd; // no check
vega10_hwmgr.c:4248-4249 (vega10_get_clock_by_type_with_voltage):
clocks->data[i].voltage_in_mv = (uint32_t)(table_info->vddc_lookup_table->
entries[dep_table->entries[i].vddInd].us_vdd); // no check
Allocation sizing in vega10_processpptables.c:1014-1043 (get_vddc_lookup_table):
table_size = sizeof(uint32_t) +
sizeof(phm_ppt_v1_voltage_lookup_record) * max_levels;
table = kzalloc(table_size, GFP_KERNEL);
table->count = vddc_lookup_pp_tables->ucNumEntries;
Callers at vega10_processpptables.c:1108-1127 pass max_levels = 8 for
vddc, 4 for vddmem, 4 for vddci. With vddciInd = 100 from the VBIOS
the kernel reads ~1000 bytes past the 4-entry (40-byte) allocation β
CWE-125 heap OOB read. Read values flow back to the SMC firmware and
into sysfs.
Why it cannot be reproduced on this guest
- No AMD GPU on the PCI bus (
pciconf -l: vgapci0 is QEMU stdvga vendor 0x1234, not AMD). amdgpu,radeon,drm,powerplayare not inX86_64_GENERIC; the powerplay module never attaches, so the Vega10 hwmgr init path that callsvega10_patch_voltage_dependency_tables_with_lookup_tableis never taken.- Even with hardware present the trigger requires a malicious VBIOS (root or physical access to flash).
Latent / hardware-gated bug; confirmed real by source trace,
unreachable on the audit guest, not an unprivileged-local-to-root
escalation. harness.c is a userspace transcription of the indexing
logic; it builds, runs, and prints which indices would OOB-read, but
produces no kernel effect.
Fix
fix.diff adds if (index >= table->count) return -EINVAL/-1; before
every vddInd/vddciInd/mvddInd/vddcInd index site (3 functions,
7 sites). Validated as applies + compiles only β the bug path
cannot be exercised without AMD hardware, so fix_status: not_testable.
Fix verification
not_testablecompile+harness validated
kernel build rc=0 + harness
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source+harness. vega10 unbounded vddInd/vddciInd/mvddInd indices -> heap OOB read. amdgpu not in GENERIC.
No comments yet.