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

VBIOS-supplied indices into clock-info arrays OOB read in processpptables.c

  • File: sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
  • Lines: 1099–1107 (UVD), 1131–1139 (VCE), 1576–1587 (VCE state), 923–935 (v2 state walk)
  • Severity: High
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
  • CWE: CWE-125 Out-of-bounds Read
  • Confidence: certain

Summary

In processpptables.c, AMDGPU PowerPlay table parsers index flexible-length arrays using UCHAR indices taken directly from VBIOS records and never compare those indices against the array's own length field. A crafted VBIOS drives the pointer arbitrarily far past the array β€” and ultimately past the mapped BIOS image β€” yielding OOB reads of kernel memory whose values are then copied into kzalloc'd hwmgr state and dereferenced as clock/voltage pointers.

Root cause

All of the indexed accesses trust the per-record UCHAR index with no bound check against the array's own count field:

  • get_uvd_clock_voltage_limit_table, processpptables.c:1099-1107: &array->entries[table->entries[i].ucUVDClockInfoIndex] β€” array is a UVDClockInfoArray * with real length array->ucNumEntries (pptable.h:628-631), index never compared.
  • get_vce_clock_voltage_limit_table, processpptables.c:1131-1139: &array->entries[table->entries[i].ucVCEClockInfoIndex] β€” same pattern; VCEClockInfoArray.ucNumEntries is the bound (pptable.h:582-585), never checked.
  • get_vce_state_table_entry, processpptables.c:1576-1587: outer i is bounded only by the VBIOS-supplied numEntries, and the inner &vce_clock_info_array->entries[record->ucVCEClockInfoIndex] plus *clock_info = clock_arrays->clockInfo + clockInfoIndex*ucEntrySize where clockInfoIndex (= record->ucClockInfoIndex & 0x3F) is never checked against clock_arrays->ucNumEntries.
  • pp_tables_get_entry (v2 path), processpptables.c:923-935: pstate_entry_v2->nonClockInfoIndex * pnon_clock_arrays->ucEntrySize and pstate_entry_v2->clockInfoIndex[i] * pclock_arrays->ucEntrySize β€” both UCHAR indices unbounded against the corresponding ucNumEntries.
  • pp_tables_get_entry (legacy path), processpptables.c:944-957: same defect for ucNonClockStateIndex and ucClockStateIndices[i].

Enabler: get_powerplay_table (processpptables.c:827-849) stores hwmgr->soft_pp_table_size but it is never consulted anywhere in this file as a bound.

Threat

Local. Precondition: attacker controls the GPU VBIOS atom PowerPlay/FirmwareInfo tables. Two realistic placements:

  1. Host root or physical access flashes a malicious VBIOS.
  2. SR-IOV / virtualization where a malicious or compromised host feeds a guest's amdgpu driver a crafted atom BIOS image (smu_atom_get_data_table at smu_helper.c:660-673 returns bios + data_start straight from the per-device atom_context).

Impact: OOB read past the mapped BIOS region β€” panic if the next page is unmapped, otherwise attacker-influenced kernel bytes are dereferenced as clock/voltage pointers and later followed by hwmgr code, enabling controlled memory corruption. The VCE state entry path additionally hands the OOB-derived pointer back to the caller as *clock_info, broadening the deref surface.

Exploit / PoC

A PoC is a malicious VBIOS atom data table, not a userspace program (this surface has no syscall entry point). Steps:

  1. Dump a reference PowerPlayInfo atom table from a target AMD GPU.
  2. Forge a minimal PowerPlay table whose extended header (ATOM_PPLIB_EXTENDEDHEADER) advertises a VCE table offset; build the VCE table such that VCEClockInfoArray.ucNumEntries = 1 but every ATOM_PPLIB_VCE_Clock_Voltage_Limit_Record.ucVCEClockInfoIndex = 0xFF.
  3. Write the modified BIOS into the GPU ROM (or, for SR-IOV, supply it to the guest atom context).
  4. Bind amdgpu and observe the driver-load path: init_clock_voltage_dependency β†’ get_vce_clock_voltage_limit_table reads array->entries[255] at processpptables.c:1132, landing 255*sizeof(VCEClockInfo) past a 1-entry array β€” typically outside the mapped BIOS region, producing an immediate kernel page-fault/panic (DoS proof) or, if the adjacent page is mapped, dereference of arbitrary kernel bytes as entry->ucEVClkHigh / usEVClkLow etc.

Bounds-check every index against the corresponding array's ucNumEntries and validate the pointer stays within soft_pp_table_size. Pattern:

--- a/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
+++ b/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
@@ -1128,6 +1128,11 @@ static int get_vce_clock_voltage_limit_table(struct pp_hwmgr *hwmgr,
        return -ENOMEM;

    vce_table->count = table->numEntries;
+   if (array->ucNumEntries == 0)
+       return -EINVAL;
    for (i = 0; i < table->numEntries; i++) {
+       if (table->entries[i].ucVCEClockInfoIndex >= array->ucNumEntries)
+           return -EINVAL;
        const VCEClockInfo *entry = &array->entries[table->entries[i].ucVCEClockInfoIndex];

Apply the identical pattern to:

  • get_uvd_clock_voltage_limit_table (processpptables.c:1099-1107)
  • get_vce_state_table_entry (processpptables.c:1576-1587)
  • pp_tables_get_entry v2 path (processpptables.c:923-935)
  • pp_tables_get_entry legacy path (processpptables.c:944-957)
  • DF-1469 (sibling): inflated ucNumEntries count OOB in same file.
  • DF-1470 (sibling): unvalidated VBIOS USHORT table offsets in same file.
  • Part of the recurring "VBIOS offset/index OOB" family (radeon_atombios, radeon_combios, amdgpu_atombios, bios_parser, bios_parser2, intel_bios).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1468 Β· 12 files
FileTypeDescriptionSize
harness.c trigger-source marker-redzone replica of processpptables UVD clock-info index OOB read 5.9 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 789 B view raw
fix.diff suggested-fix add ucUVDClockInfoIndex >= array->ucNumEntries check (mirror at sibling sites) 931 B 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.1 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-1468 β€” VBIOS indices into clock-info arrays OOB read (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_uvd_clock_voltage_limit_table, lines 1099-1107:

for (i = 0; i < table->numEntries; i++) {
    const UVDClockInfo *entry =
        &array->entries[table->entries[i].ucUVDClockInfoIndex];  /* :1101 OOB */
    uvd_table->entries[i].vclk = (entry->ucVClkHigh << 16) | entry->usVClkLow;
    ...
}

A UCHAR index (ucUVDClockInfoIndex / ucVCEClockInfoIndex / ucClockInfoIndex / nonClockInfoIndex) taken directly from the VBIOS is used to index a flex array whose real length is the sibling ucNumEntries field, but the index is never compared to ucNumEntries. A crafted index reads arbitrarily far past the array into adjacent kernel/VBIOS memory. Sites: :1099-1107 (UVD), :1131-1139 (VCE), :1576-1587 (VCE state), :923-935 (v2 state walk). soft_pp_table_size is stored (:844) but never used as a bound. Enabler: malicious VBIOS flash or SR-IOV guest atom context.

Harness proof

Marker-redzone harness (the read returns bytes it was never entitled to):

array->ucNumEntries   = 1 (the real length)
In-bounds  ucUVDClockInfoIndex=0 (< ucNumEntries) -> vclk=0x112233 (OK)
OOB        ucUVDClockInfoIndex=1 (>= ucNumEntries) -> vclk=0xbbbeef
read at entries[1] returned the redzone marker (0xbb/0xbeef): YES -> OOB read proven
RESULT: heap OOB read CONFIRMED (processpptables.c:1101 pattern)

Fix

fix.diff adds if (table->entries[i].ucUVDClockInfoIndex >= array->ucNumEntries) return -EINVAL; before the index deref in the representative UVD site. The same bound check must be mirrored at the sibling VCE / VCE-state / v2-state-walk sites.

Module build validation (Phase 8)

All 8 amdgpu fixes applied (three touch processpptables.c: DF-1468, 1469, 1470 β€” all apply together cleanly); 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-1469 and DF-1470 (three diffs all touch processpptables.c and apply together); 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 ucUVDClockInfoIndex=1 -> vclk=0xbbbeef (redzone marker); in-bounds vclk=0x112233
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 (OOB read past ucNumEntries returns adjacent bytes). Realistic runtime impact with amdgpu HW + crafted VBIOS / SR-IOV atom context is kernel info leak + corruption (read values drive MMIO writes, leaked via sysfs). Evidence pack: findings/poc/DF-1468/ (harness.c).

Evidence (decisive lines)

array->ucNumEntries   = 1 (the real length)
sizeof(UVDClockInfo)  = 6
In-bounds  ucUVDClockInfoIndex=0 (< ucNumEntries) -> vclk=0x112233 (OK)
OOB        ucUVDClockInfoIndex=1 (>= ucNumEntries) -> vclk=0xbbbeef
Worst case ucUVDClockInfoIndex=255 -> kernel reads at byte offset 1530 past entries[0]
read at entries[1] returned the redzone marker (0xbb/0xbeef): YES -> OOB read proven
RESULT: heap OOB read CONFIRMED (processpptables.c:1101 pattern)
RUN_EXIT=0

PoC changes

Authored harness.c (initial guard-page version rewritten to robust marker-redzone approach after the guest's mmap redzone proved non-deterministic), build.sh, run.sh, fix.diff (check ucUVDClockInfoIndex >= array->ucNumEntries), VERDICT.md, manifest.json. fix.diff regenerated via copy+edit+diff.

Verified recommended fix

In get_uvd_clock_voltage_limit_table (processpptables.c:1099-1107), before the index deref add if (table->entries[i].ucUVDClockInfoIndex >= array->ucNumEntries) return -EINVAL;. Mirror the same bound check at the sibling VCE/VCE-state/v2-state-walk sites. Matches finding proposal. Full diff in findings/poc/DF-1468/fix.diff.

Verdict

REPRODUCED. processpptables.c uses VBIOS UCHAR indices (ucUVDClockInfoIndex/ucVCEClockInfoIndex/ucClockInfoIndex/nonClockInfoIndex) to index flex arrays whose real length is the sibling ucNumEntries field, with NO comparison (representative site :1099-1107). A crafted index reads past the array into adjacent kernel/VBIOS memory, deref'd as clock/voltage values. Confirmed by marker-redzone harness: index>=ucNumEntries reads the redzone marker (vclk=0xbbbeef from slot 1 when only 1 real entry). soft_pp_table_size (:844) is available but never used as a bound.