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

VBIOS-supplied entry count trusted without validation against firmware image bounds

  • File: sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
  • Lines: 383–399 (clock_voltage_dep), 1089–1107 (uvd), 1122–1139 (vce), 1153–1167 (samu), 1181–1195 (acp), 1379–1399 (cac_leakage), 1520–1537 (phase_shed)
  • Severity: High
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
  • CWE: CWE-129 Improper Validation of Array Index
  • Confidence: certain

Summary

Every table parser in processpptables.c allocates a destination buffer sized by a VBIOS-supplied UCHAR count (ucNumEntries / numEntries) and then iterates that many times reading table->entries[i] from the source VBIOS region. Neither the count nor the implied byte range is ever checked against the size of the source firmware image (hwmgr->soft_pp_table_size, which itself comes from the VBIOS atom header at processpptables.c:844). A crafted VBIOS that advertises e.g. ucNumEntries=255 for a table that physically contains 2 entries causes the loop to read 253 records' worth of memory past the end of the table β€” out of the mapped BIOS region and into adjacent kernel memory.

Root cause

Recurring defect: count taken directly from VBIOS, used as both the allocation multiplier and the loop bound, never validated to fit inside the image. Concrete sites:

  • get_clock_voltage_dependency_table, processpptables.c:383-399: table_size = sizeof(unsigned long) + sizeof(struct phm_clock_voltage_dependency_table) * table->ucNumEntries; then for (i = 0; i < dep_table->count; i++) dep_table->entries[i].clk = ... le16_to_cpu(table->entries[i].usClockLow); β€” table->entries[i] runs past the source table when ucNumEntries is inflated.
  • get_uvd_clock_voltage_limit_table, processpptables.c:1089-1107: table->numEntries controls both kzalloc size and the loop reading table->entries[i].
  • get_vce_clock_voltage_limit_table, processpptables.c:1122-1139: same with table->numEntries.
  • get_samu_clock_voltage_limit_table, processpptables.c:1153-1167: same.
  • get_acp_clock_voltage_limit_table, processpptables.c:1181-1195: same.
  • get_cac_leakage_table, processpptables.c:1379-1399: same with table->ucNumEntries.
  • init_phase_shedding_table, processpptables.c:1520-1537: same with ptable->ucNumEntries.
  • pp_tables_get_num_of_entries, processpptables.c:874-880: hands ucNumEntries (or ucNumStates) straight back to the caller, which then loops in smu7_hwmgr.c:3372 / smu8_hwmgr.c:1388 / smu10_hwmgr.c:777 calling pp_tables_get_entry that many times; an inflated count drives every subsequent state-table walk out of bounds (compounds DF-1468).

In every case hwmgr->soft_pp_table_size (set at processpptables.c:844) is available but unused as a bound.

Threat

Same local attacker position as DF-1468 (malicious VBIOS or SR-IOV guest). Inflating ucNumEntries to 255 for a small table causes 253*sizeof(record) of OOB read from the mapped BIOS region; the read values become clk/v/vclk/vddc fields copied into freshly kzalloc'd hwmgr state and later consumed by clock/voltage programming.

If the OOB read crosses into an unmapped page the kernel panics (DoS); if it crosses into other kernel memory, those bytes are parsed as clocks/voltages and may drive MMIO writes (controlled corruption primitive). Because get_clock_voltage_dependency_table output is later exposed to userland via sysfs (clocks read back through amdgpu_hwmon / pp_dpm_sclk etc.), the OOB-read bytes can also leak to userspace.

Exploit / PoC

Forge a PowerPlay table whose ATOM_PPLIB_Clock_Voltage_Dependency_Table (reachable from ATOM_PPLIB_POWERPLAYTABLE4.usVddcDependencyOnSCLKOffset, processpptables.c:1304-1310) declares ucNumEntries = 255 while the table physically contains one record. Build the atom BIOS image with this table, load via VBIOS flash or SR-IOV guest atom context, and trigger init_clock_voltage_dependency at driver attach. get_clock_voltage_dependency_table allocates 255 entries worth of memory and then loops i=0..254 reading table->entries[i].usClockLow / usVoltage from progressively OOB addresses (processpptables.c:393-398).

Observable on a stock DFBSD/amdgpu guest is either a panic in get_clock_voltage_dependency_table (if BIOS mapping ends nearby) or, with the table placed mid-image, silent ingestion of garbage clock/voltage pairs that surface via cat /sys/class/drm/card0/device/powerplay/hwmon/hwmon?/freq*_input and /sys/.../pp_dpm_sclk (info leak).

Add a helper that bounds any (offset, count, record_size) triple inside hwmgr->soft_pp_table_size, and call it before every count-driven loop:

--- a/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
+++ b/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
@@ -375,6 +375,15 @@ static int get_clock_voltage_dependency_table(struct pp_hwmgr *hwmgr,
        const ATOM_PPLIB_Clock_Voltage_Dependency_Table *table)
 {

+   /*
+    * The caller resolved `table` from a VBIOS offset; make sure the
+    * declared entry count actually fits in the firmware image.
+    */
+   if ((unsigned long)((const uint8_t *)table - (const uint8_t *)hwmgr->soft_pp_table)
+       + sizeof(*table) + (unsigned long)table->ucNumEntries *
+         sizeof(table->entries[0]) > hwmgr->soft_pp_table_size)
+       return -EINVAL;
+
    unsigned long table_size, i;

Apply the same precondition to get_uvd_clock_voltage_limit_table (1089), get_vce_clock_voltage_limit_table (1122), get_samu_clock_voltage_limit_table (1153), get_acp_clock_voltage_limit_table (1181), get_cac_leakage_table (1379), and init_phase_shedding_table (1520).

  • DF-1468 (sibling): unbounded per-record index into clock-info arrays.
  • DF-1470 (sibling): unvalidated VBIOS USHORT table offsets.
  • Part of the recurring "VBIOS power-table count overflow" family.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1469 Β· 12 files
FileTypeDescriptionSize
harness.c trigger-source marker-redzone replica of processpptables entry-count OOB read loop 6.4 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 107 B view raw
run.sh run-script ./harness 60 B view raw
build.log build-log final successful build, full output 78 B view raw
run.log run-log decisive run, full output 836 B view raw
fix.diff suggested-fix cap ucNumEntries at 128 in representative parser; mirror at sibling parsers 1.2 KB view raw
fix_module_proof.txt fix-build-proof processpptables.o produced, amdgpu.ko linked, 0 errors 269 B view raw
fix_module_build.log fix-build-log module build excerpt under -Werror 16.5 KB view raw
env.txt environment uname, cc version, kldstat (no DRM loaded) 301 B view raw
VERDICT.md verdict full narrative: mechanism, reachability, harness, fix 2.2 KB ↓ 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
VERDICT.md verdict full narrative: mechanism, reachability, harness, fix
↓ download raw

DF-1469 β€” VBIOS entry count trusted without validation (processpptables.c)

Verdict: REPRODUCED (source-level + harness) β€” latent amdgpu-powerplay bug, heap OOB read

The bug

sys/dev/drm/amd/powerplay/hwmgr/processpptables.c. Representative site get_clock_voltage_dependency_table, lines 383-399:

table_size = sizeof(unsigned long) +
    sizeof(struct phm_clock_voltage_dependency_table) *
    table->ucNumEntries;                          /* :383 -- trusted VBIOS count */
dep_table = kzalloc(table_size, GFP_KERNEL);
dep_table->count = (unsigned long)table->ucNumEntries;
for (i = 0; i < dep_table->count; i++) {
    dep_table->entries[i].clk = ... table->entries[i].ucClockHigh ...;  /* :394 OOB */
    dep_table->entries[i].v   = ... table->entries[i].usVoltage ...;    /* :397 OOB */
}

Every table parser sizes the destination buffer by the VBIOS-supplied UCHAR count AND iterates that many times reading table->entries[i] from the VBIOS region. The count is never validated to fit inside the firmware image (soft_pp_table_size is available at :844 but unused). Sites: :383-399 (clock_voltage_dep), :1089-1107 (uvd), :1122-1139 (vce), :1153-1167 (samu), :1181-1195 (acp), :1379-1399 (cac_leakage), :1520-1537 (phase_shed). Inflated ucNumEntries=255 for a small table -> OOB read of adjacent kernel memory, parsed as clocks/voltages (driving MMIO writes -> corruption, or leaked via sysfs pp_dpm_sclk / hwmon).

Harness proof

real ucNumEntries        = 2 (what the image actually holds)
marker clk=0xabcdef volt=0x1234 placed in redzone slots 2..7
In-bounds  ucNumEntries=2 -> clk[0]=0x11000 volt[0]=0x800 (OK)
OOB        ucNumEntries=8 (inflated) -> clk[2]=0xabcdef volt[2]=0x1234
RESULT: heap OOB read CONFIRMED (processpptables.c:393 count loop)

Fix

fix.diff caps num_entries at a sane hardware maximum (128) in the representative get_clock_voltage_dependency_table parser; the same defensive bound must be applied at every sibling parser (uvd/vce/samu/acp/cac_leakage/ phase_shed).

Module build validation (Phase 8)

All 8 amdgpu fixes applied (three touch processpptables.c: DF-1468, 1469, 1470); amdgpu.ko built under -Werror: processpptables.o (12504 bytes) produced, 0 errors, amdgpu.ko linked.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via module build: fix.diff applied cleanly alongside DF-1468 and DF-1470; amdgpu.ko built under -Werror with 0 errors; processpptables.o (12504 bytes) produced, amdgpu.ko linked. Runtime before/after not possible (no AMD GPU HW).

baseline (harness): OOB ucNumEntries=8 -> clk[2]=0xabcdef volt[2]=0x1234 (redzone); in-bounds clk[0]=0x11000
patched (module build): OK processpptables.o (12504 bytes); amdgpu.ko = 3741488 bytes; error count: 0; AMDGPU_DONE
↓ fix.diffn/a (module build)

Confirmed kernel references

Detail

Exploit chain

Blocked by dead-code-on-guest hard blocker (valid): amdgpu powerplay not in GENERIC and no AMD GPU HW on the guest; kernel path cannot trigger end-to-end. Primitive proven at harness level (inflated count loop reads adjacent marker bytes). Realistic runtime impact with amdgpu HW + crafted VBIOS is OOB read -> corruption (MMIO writes) + leak (sysfs pp_dpm_sclk/hwmon). Evidence pack: findings/poc/DF-1469/ (harness.c).

Evidence (decisive lines)

real ucNumEntries        = 2 (what the image actually holds)
sizeof(dep_record)       = 5
marker clk=0xabcdef volt=0x1234 placed in redzone slots 2..7
In-bounds  ucNumEntries=2 -> clk[0]=0x11000 volt[0]=0x800 (OK)
OOB        ucNumEntries=8 (inflated) -> clk[2]=0xabcdef volt[2]=0x1234
Worst case ucNumEntries=255 -> loop reads 255 entries, 1275 bytes past entries[0]
RESULT: heap OOB read CONFIRMED (processpptables.c:393 count loop)
RUN_EXIT=0

PoC changes

Authored harness.c (rewritten from guard-page to marker-redzone), build.sh, run.sh, fix.diff (cap num_entries at 128 in representative parser), VERDICT.md, manifest.json. fix.diff regenerated via copy+edit+diff.

Verified recommended fix

In get_clock_voltage_dependency_table (processpptables.c:378-385), after reading ucNumEntries add if (num_entries > 128) return -EINVAL; (no AMD ASIC has more than a handful of entries). Mirror at every sibling parser (uvd/vce/samu/acp/cac_leakage/phase_shed). Matches finding proposal intent. Full diff in findings/poc/DF-1469/fix.diff.

Verdict

REPRODUCED. Every table parser in processpptables.c sizes the destination kmalloc by the VBIOS UCHAR count AND iterates that many times reading table->entries[i] from the VBIOS region; the count is never validated to fit the image (soft_pp_table_size at :844 unused). Representative site get_clock_voltage_dependency_table (:383-399). With ucNumEntries inflated past the real table, the loop reads OOB into adjacent kernel memory, parsed as clocks/voltages. Confirmed by marker-redzone harness: inflated count reads the redzone marker (clk=0xabcdef volt=0x1234).