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

VBIOS-supplied table offsets never validated against firmware image size

  • File: sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
  • Lines: 61–62, 162–163, 227–228, 263–264, 299–300, 350–351, 875–876, 910–911, 917–921, 1029–1030, 1279–1284, 1291–1293, 1305–1307, 1313–1315, 1321–1323, 1329–1331, 1348–1350, 1360–1361, 1473–1475, 1485–1487, 1492–1493, 1512–1515, 1571–1574
  • Severity: High
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
  • CWE: CWE-787 Out-of-bounds Write
  • Confidence: certain

Summary

Throughout processpptables.c, VBIOS-supplied USHORT offsets (usExtendendedHeaderOffset, usStateArrayOffset, usClockInfoArrayOffset, usNonClockInfoArrayOffset, usVCETableOffset, usUVDTableOffset, usSAMUTableOffset, usACPTableOffset, usPowerTuneTableOffset, usSclkVddgfxTableOffset, usVddcDependencyOnSCLKOffset, usCACLeakageTableOffset, usPPMTableOffset, usVddcPhaseShedLimitsTableOffset, etc.) are added to (unsigned long)powerplay_table and the result is immediately dereferenced as a typed pointer. No offset is ever checked to lie within [0, hwmgr->soft_pp_table_size) nor, for compound walks (lines 75–148), to keep the cumulative pointer inside the image. A crafted VBIOS can set any of these to 0xFFFF, pointing 65535 bytes past the table base.

Root cause

Every ... = (... *)(((unsigned long)powerplay_table) + le16_to_cpu(...offset)) site lacks bounds. Representative examples:

  • Extended-header deref at processpptables.c:59-62 (and duplicated at 160-163, 225-228, 261-264, 297-300, 348-351, 1485-1487, 1029-1030).
  • StateArray/ClockInfoArray/NonClockInfoArray derefs at processpptables.c:875-876, 910-911, 917-921.
  • Per-sub-table derefs at processpptables.c:1230-1235 (VCE), 1245-1250 (UVD), 1259-1261 (SAMU), 1270-1272 (ACP), 1279-1284 / 1291-1293 (PowerTune), 1305-1307 / 1313-1315 / 1321-1323 / 1329-1331 / 1348-1350 (POWERPLAYTABLE4 dependency offsets), 1360-1361 (sclk_vdd_gfx), 1473-1475 (CAC leakage), 1492-1493 (PPM), 1512-1515 (phase shed), 1571-1574 (VCE clock info array and clock info array).

The cumulative table walker at processpptables.c:75-148 (get_vce_clock_info_array_offset/_size, get_vce_clock_voltage_limit_table_offset/_size, get_vce_state_table_offset) blindly chains offsets and p->ucNumEntries * sizeof(...) sizes β€” each term VBIOS-controlled β€” without ever checking that the running total still lies inside soft_pp_table_size.

usExtendendedHeaderOffset is even compared only with > 0 (processpptables.c:58, 159, 224, 260, 296, 347) β€” a non-zero value of any size passes.

Threat

Same local/VBIOS position as DF-1468/1469. A single malicious USHORT offset (e.g. usExtendendedHeaderOffset = 0xFFFF in a v3+ table) makes the extended-header pointer land ~64 KB past the PowerPlay table base. The kernel then reads usSize, usVCETableOffset, etc. from arbitrary kernel memory, dereferences those values as further offsets, and so on β€” a single bad offset cascades into arbitrary-read-then-corrupt chains.

Because the walker at lines 75–148 sums multiple VBIOS-supplied sizes, the cumulative offset can wrap modulo 2^64 only at extreme UCHAR products, but the practical case (offset near image end + small counts) is trivially reachable and lands reads/writes in adjacent kernel pages.

Exploit / PoC

Same atom-table mutator as DF-1468. Forge a PowerPlayInfo atom table of real size ~256 bytes; set usTableSize = sizeof(ATOM_PPLIB_POWERPLAYTABLE3) to pass the version check at processpptables.c:53-54, then set usExtendendedHeaderOffset = 0xFFFF. On driver attach, get_vce_table_offset (line 62) computes powerplay_table + 0xFFFF and dereferences extended_header->usSize (line 63) from kernel memory 64 KB past the table.

Expected observable: kernel page fault at smu_atom_.../get_vce_table_offset during amdgpu init, or β€” with the adjacent 64 KB mapped β€” chained derefs producing clearly garbled vce_table_offset values that drive a secondary fault deeper in init_clock_voltage_dependency.

Centralize all VBIOS pointer arithmetic behind one helper that rejects OOB offsets:

--- a/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
+++ b/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
@@ -46,6 +46,21 @@

 #define NUM_BITS_CLOCK_INFO_ARRAY_INDEX 6

+/*
+ * Resolve a VBIOS-supplied USHORT offset relative to the PowerPlay table
+ * base. Returns NULL if (offset, want) runs outside soft_pp_table_size,
+ * which every caller MUST check before dereferencing.
+ */
+static const void *pp_table_ptr(const struct pp_hwmgr *hwmgr,
+               const ATOM_PPLIB_POWERPLAYTABLE *tbl,
+               uint16_t offset, size_t want)
+{
+   unsigned long base = (unsigned long)hwmgr->soft_pp_table;
+   unsigned long p    = base + offset;
+   if (offset >= hwmgr->soft_pp_table_size ||
+       want > hwmgr->soft_pp_table_size - offset)
+       return NULL;
+   return (const void *)p;
+}

Replace every (((unsigned long)powerplay_table) + le16_to_cpu(off)) site listed in root_cause with pp_table_ptr(hwmgr, powerplay_table, le16_to_cpu(off), sizeof(*target)), abort the containing init_* function with -EINVAL when it returns NULL. This single refactor closes DF-1468, DF-1469, and DF-1470 simultaneously.

  • DF-1468 (sibling): unbounded per-record index into clock-info arrays.
  • DF-1469 (sibling): inflated ucNumEntries count OOB.
  • Part of the recurring "VBIOS offset/index OOB" family.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1470 Β· 12 files
FileTypeDescriptionSize
harness.c trigger-source marker-redzone replica of processpptables USHORT-offset OOB deref chain 6.1 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 961 B view raw
fix.diff suggested-fix add pp_offset_in_table helper validating offset vs soft_pp_table_size; apply at extended-header site (mirror at ~50 sites) 1.5 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.3 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-1470 β€” VBIOS table offsets never validated vs firmware size (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_vce_table_offset, lines 55-66:

if (powerplay_table3->usExtendendedHeaderOffset > 0) {
    const ATOM_PPLIB_EXTENDEDHEADER *extended_header =
        (const ATOM_PPLIB_EXTENDEDHEADER *)
        (((unsigned long)powerplay_table3) +
         le16_to_cpu(powerplay_table3->usExtendendedHeaderOffset));  /* :60 OOB ptr */
    if (le16_to_cpu(extended_header->usSize) >= ...)                 /* :63 OOB read #1 */
        vce_table_offset = le16_to_cpu(extended_header->usVCETableOffset); /* :65 OOB read #2 */
}

Throughout the file, VBIOS USHORT offsets (usExtendendedHeaderOffset, usStateArrayOffset, usClockInfoArrayOffset, usVCETableOffset, ...) are added to the powerplay_table base and immediately dereferenced as typed pointers with no check that the offset lies within [0, soft_pp_table_size). A single malicious USHORT (e.g. 0xFFFF) makes the pointer land ~64 KB past the table -> chained derefs cascade into arbitrary-read-then-corrupt. Affects ~50 sites; the compound walker at lines 75-148 sums multiple VBIOS sizes without checking the cumulative total.

Harness proof

table real size         = 16 bytes
marker ext_header at    = offset 16 (just past the table)
marker usVCETableOffset = 0xcafe
In-bounds  usExtendendedHeaderOffset=0 -> vce_offset=0 (OK)
OOB        usExtendendedHeaderOffset=16 (past table) -> vce_offset=0xcafe
RESULT: heap OOB read CONFIRMED (processpptables.c:60-65 pattern)

Fix

fix.diff introduces a pp_offset_in_table(hwmgr, base, offset, need) helper that validates [base+offset, base+offset+need) lies within [soft_pp_table, soft_pp_table+soft_pp_table_size), and applies it at the representative get_vce_table_offset extended-header site. The same helper must be applied at every offset-deref site (~50 places).

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-1468 and DF-1469; amdgpu.ko built under -Werror with 0 errors; processpptables.o (12504 bytes) produced, amdgpu.ko linked. The helper compiles into the module. Runtime before/after not possible (no AMD GPU HW).

baseline (harness): OOB usExtendendedHeaderOffset=16 -> vce_offset=0xcafe (redzone); in-bounds vce_offset=0
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 (USHORT offset deref returns adjacent marker bytes, cascading). Realistic runtime impact with amdgpu HW + crafted VBIOS is arbitrary kernel read + corruption across ~50 sites. Evidence pack: findings/poc/DF-1470/ (harness.c).

Evidence (decisive lines)

table real size         = 16 bytes
marker ext_header at    = offset 16 (just past the table)
marker usVCETableOffset = 0xcafe
In-bounds  usExtendendedHeaderOffset=0 -> vce_offset=0 (OK)
OOB        usExtendendedHeaderOffset=16 (past table) -> vce_offset=0xcafe
Worst case usExtendendedHeaderOffset=0xFFFF -> deref at base+65535, ~64KB past the powerplay table
RESULT: heap OOB read CONFIRMED (processpptables.c:60-65 pattern)
RUN_EXIT=0

PoC changes

Authored harness.c (rewritten from guard-page to marker-redzone), build.sh, run.sh, fix.diff (introduce pp_offset_in_table helper validating offset vs soft_pp_table_size, apply at extended-header site), VERDICT.md, manifest.json. fix.diff regenerated via copy+edit+diff.

Verified recommended fix

Introduce a pp_offset_in_table(hwmgr, base, offset, need) helper validating [base+offset, base+offset+need) lies within [soft_pp_table, soft_pp_table+soft_pp_table_size), and apply it at the representative get_vce_table_offset extended-header site (:58-66); mirror at the ~50 other offset-deref sites. Supersedes the finding proposal by providing a reusable helper. Full diff in findings/poc/DF-1470/fix.diff.

Verdict

REPRODUCED. Throughout processpptables.c, VBIOS USHORT offsets (usExtendendedHeaderOffset/usStateArrayOffset/usClockInfoArrayOffset/usVCETableOffset/...) are added to the powerplay_table base and deref'd as typed pointers with NO check that the offset lies within [0, soft_pp_table_size) (representative site :55-66). A single malicious USHORT (e.g. 0xFFFF) -> pointer ~64KB past table -> cascading arbitrary-read-then-corrupt. Confirmed by marker-redzone harness: offset past the table returns the redzone marker (vce_offset=0xcafe from a fake ext_header just past the table). soft_pp_table_size (:844) tracked but never used.