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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4395 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 == 0 and entries[ucNumEntries-1].ulClk < 90000, num_entries = min(ucNumEntries + 1, NUM_DSPCLK_LEVELS=8).
  • Lines 729-733 β€” the destination kzalloc is sized by num_entries (4 + 28 * num_entries; phm_ppt_v1_clock_voltage_dependency_record is 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 shows clk=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_testable
baseline reproduced→ patch + rebuild →patched clean

Patched 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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 x86_64 (syntax validation only)

Confirmed kernel references

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).