Heap OOB write in get_dcefclk_voltage_dependency_table pioneer path: allocation capped at min(ucNumEntries+1, 8) but copy loop runs raw ucNumEntries β up to 6916 bytes of VBIOS-controlled records past a 228-byte kernel heap object
Summary
On the pioneer Vega10 SKU (0x6863 rev0, last DCEFCLK entry <90000) the destination kzalloc is sized for min(count+1, 8) 28-byte records (228B max) but the copy loop iterates the raw firmware u8 count. A crafted VBIOS with count>8 writes (count-8)*28 bytes past the object - 6916 bytes at count=255 - a linear heap overflow repeating a record with an arbitrary u32 + byte every 28 bytes. Parse runs at probe AND at every pp_table sysfs reload. HARNESS-REPRODUCED with faithful struct layouts + canary guard (count=9 smashes first record; count=255 writes through byte 7144; device/revision negative controls and the patched loop leave the guard intact). Bound-the-loop fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4395 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dcefclk_oflow.c | β | 9.6 KB | view raw | |
| build.sh | β | 114 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.log | β | 291 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| env.txt | β | 417 B | view raw | |
| fix.diff | β | 492 B | view raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-4395 VERDICT β vega10 dcefclk pioneer-path heap OOB write
Status: reproduced (harness) | impact: corruption | confidence: certain
Root cause (path:line)
get_dcefclk_voltage_dependency_table() in
sys/dev/drm/amd/powerplay/hwmgr/vega10_processpptables.c:
- Line 709 asserts only
ucNumEntries != 0β a u8 the VBIOS image fully controls (up to 255); nothing validates it against the image size (that systemic gap is DF-4396). - Lines 718-726 β the pioneer workaround: when
adev->pdev->device == 0x6863 && rev_id == 0andentries[ucNumEntries-1].ulClk < 90000,num_entries = min(ucNumEntries + 1, NUM_DSPCLK_LEVELS=8). - Lines 729-733 β the destination
kzallocis sized by num_entries (4 + 28 * num_entries;phm_ppt_v1_clock_voltage_dependency_recordis 28 bytes β hwmgr_ppt.h:31-45). - Lines 740-745 β the copy loop runs
for (i = 0; i < clk_dep_table->ucNumEntries; i++)β the raw firmware count, not the allocation count.
=> For ucNumEntries >= 9 on the pioneer SKU the loop writes
(ucNumEntries - 8) * 28 bytes past the object. Worst case
ucNumEntries = 255: allocation 228 bytes, writes through byte 7144,
6916 bytes past β a linear heap overflow repeating a 28-byte record
whose first 4 bytes are an arbitrary VBIOS u32 (ulClk) and whose 5th
byte is an arbitrary ucVddInd (only those two fields are written; the
rest of each record keeps the kzalloc zeros, i.e. mostly-zero spray with
a controlled dword every 28 bytes β classic heap-corruption primitive
into the DragonFly kernel heap, kmalloc-backed kzalloc).
Trigger / threat model
Parse runs at vega10_pp_tables_initialize() (powerplay init, probe and
every pp_table sysfs reload). Attacker controls the table bytes via:
1. a crafted/flashed VBIOS ROM image (supply chain / re-flashed card /
passthrough ROM), or
2. root writing /sys/class/drm/card0/device/pp_table
(amdgpu_pm.c:1007 DEVICE_ATTR pp_table S_IWUSR).
Device gate: PCI device id 0x6863, revision 0 ("pioneer" Vega10 SKU). The parse-side count itself is unconstrained on that SKU.
Reproduction (this pack)
Lifted-copy harness dcefclk_oflow.c (same approach the audit used for
DF-1179 in the same driver family): the allocation decision and copy
loop are replicated byte-for-byte with the real struct layouts
(ATOM_Vega10_CLK_Dependency_Record = 8 bytes,
phm_ppt_v1_clock_voltage_dependency_record = 28 bytes), the kzalloc is
emulated by a canary-guarded calloc. Run on the audit guest
(cc 8.3, DragonFly 6.5-DEVELOPMENT):
ucNumEntries=6β guard intact (workaround allocates 7, copy 6+1)ucNumEntries=9β 28 bytes past the object (guard smashed)ucNumEntries=255β writes through byte 7144 of the 228-byte object (6916 bytes past), first OOB record showsclk=0x3f0(the VBIOS-chosen u32 landing in kernel heap memory)- negative control A (device 0x687f) and B (revision 1): guards intact β proves the overflow is the pioneer-cap interaction, not the copy loop in general (non-pioneer paths alloc == copy count).
- PATCHED loop (fix.diff bound): guard intact with
ucNumEntries=255.
The in-kernel path cannot run on the guest (no AMD GPU β run.log hardware gate), so this is a harness-level reproduction of the exact overflow arithmetic of the kernel code, per the project's DF-1179 precedent.
Fix
fix.diff (git apply --check OK): bound the copy loop by num_entries
β identical to upstream's current code:
- for (i = 0; i < clk_dep_table->ucNumEntries; i++) {
+ for (i = 0; i < num_entries; i++) {
Excess entries are dropped (same semantics upstream). The image-side
count validation (reading 255 entries from an image that doesn't hold
them) is covered by the DF-4396 central layer, whose unit test includes
this path (dcefclk: pioneer fw=255 -> alloc 8, copy 8).
Fix validation: kernel-level not_testable (no ASIC on guest); harness patched-control proves the fixed bound stops the overflow; the DF-4396 patched parser (which subsumes this fix) syntax-validates with 0 errors on the guest kernel toolchain.
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 ASIC). fix.diff is git-apply-able; the harness PATCHED control (loop bounded by num_entries) shows the overflow is gone for the same crafted input, and the DF-4396 central fix (which subsumes this one-liner) syntax-validizes with 0 errors on the guest kernel toolchain (build.log).
fix.diff; run.log PATCHED control; findings/poc/DF-4396/build.log (0 syntax errors)
Confirmed kernel references
- sys/dev/drm/amd/powerplay/hwmgr/vega10_processpptables.c:695
- sys/dev/drm/amd/powerplay/hwmgr/vega10_processpptables.c:721
- sys/dev/drm/amd/powerplay/hwmgr/vega10_processpptables.c:729
- sys/dev/drm/amd/powerplay/hwmgr/vega10_processpptables.c:740
- sys/dev/drm/amd/powerplay/hwmgr/vega10_pptable.h:181
- sys/dev/drm/amd/powerplay/hwmgr/hwmgr_ppt.h:31
- sys/dev/drm/amd/amdgpu/amdgpu_pm.c:1007
Detail
Evidence (decisive lines)
run.log (guard smashed at n=9: 5+ bytes; n=255: writes through byte 7144 of a 228-byte object, first OOB record clk=0x3f0; PATCHED control intact); dcefclk_oflow.c (verbatim replica of :718-750); fix.diff (git apply --check OK)
PoC changes
authored fresh this run (pass 2); no seed PoC existed
Verified recommended fix
bound the dcefclk copy loop by num_entries (fix.diff, one line)
Verdict
Heap OOB write in get_dcefclk_voltage_dependency_table (vega10_processpptables.c:695-755): on the pioneer SKU (PCI 0x6863 rev 0, last entry <90000) the destination kzalloc is sized min(ucNumEntries+1, 8) records (28 B each = 228 B max) but the copy loop at :740 iterates the raw firmware ucNumEntries β with a crafted VBIOS table of ucNumEntries>8 the loop writes (ucNumEntries-8)*28 = up to 6916 bytes of VBIOS-controlled records (arbitrary u32 ulClk + byte ucVddInd every 28 bytes) linearly past the kernel heap object. Reproduced on the audit guest with a faithful lifted-copy harness (DF-1179 precedent for the same driver family): guard smashed from ucNumEntries=9 onward, 6916 B past at 255; negative controls (different device id / revision != 0) and the one-line patched loop leave the guard intact. In-kernel path hardware-gated (no AMD GPU on the guest, run.log).
No comments yet.