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

Heap OOB write in get_vddc_lookup_table: buffer sized for 16 records, loop walks up to 255

Summary

process_pptables_v1_0.c:168-169 table_size = sizeof(uint32)+sizeof(record)*max_levels=16 (callers pass 16 at 287,296). 176 table->count = ucNumEntries (UCHAR 0..255 from VBIOS). 178 loop for i<ucNumEntries. GET_FLEXIBLE_ARRAY_MEMBER_ADDR entries[i] writes record at offset 4+10*i. i>=16 -> OOB write past 164-byte kmalloc. ucNumEntries=255 -> last write at byte 2544 of 164-byte alloc -> ~2390 bytes controlled overflow into adjacent kmalloc-256 slab. Only *_lookup helper with dual bounds; siblings get_mclk/sclk/mm/pcie correctly use ucNumEntries for both alloc and loop. Trigger: crafted VBIOS (VFIO romfile, malicious FPGA GPU). pp_tables_v1_0_initialize at driver bind. Fix: table->count = min(ucNumEntries, max_levels); loop for i<table->count.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1574 Β· 8 files
FileTypeDescriptionSize
VERDICT.md verdict full narrative: bug, reachability, fix, threat model 6.3 KB ↓ raw
README.md readme summary + reproduce 1.7 KB ↓ raw
fix.diff suggested-fix clamp table->count and loop to min(ucNumEntries, max_levels) 944 B view raw
fix_build.log build-log patched process_pptables_v1_0.c compiles -Werror clean 1.6 KB view raw
env.txt environment guest uname, pciconf, kldstat β€” no AMD hardware 2.3 KB view raw
manifest.json manifest this catalog 2.6 KB view 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
README.md readme summary + reproduce
↓ download raw

DF-1574 β€” get_vddc_lookup_table heap OOB write (amdgpu powerplay)

At a glance

Field Value
Finding DF-1574
Severity High
Class CWE-787 Out-of-bounds Write
File sys/dev/drm/amd/powerplay/hwmgr/process_pptables_v1_0.c:168-190
Verdict NOT REPRODUCED ON GUEST (real bug, unreachable runtime path)
Impact none (on this guest); latent memory-corruption
Confidence certain (source-traced line-by-line)

The bug, in one sentence

get_vddc_lookup_table() allocates 4 + 10*max_levels bytes but loops ucNumEntries times writing 10 bytes per iteration, where ucNumEntries (0..255) comes from VBIOS and the two callers pass max_levels=16 β€” so a hostile VBIOS with ucNumEntries>16 writes ~2.4 KB past the kmalloc-256 slab object.

Reproduce on this guest

Cannot run. The affected source file is part of the amdgpu KLD module, which is not in X86_64_GENERIC, not loaded, and there is no AMD GPU on the QEMU guest. See VERDICT.md for the full reachability analysis and fix_build.log for the standalone patched-source compile.

Apply the fix

cd /usr/src
patch -p1 < findings/poc/DF-1574/fix.diff
# In sys/dev/drm/amd/amdgpu:
make   # rebuild amdgpu.ko

Files in this folder

File Purpose
VERDICT.md Full analysis: bug, reachability, fix, threat model
fix.diff git-apply-able unified diff (clamps count + loop)
fix_build.log Translation-unit compile log (patched source, -Werror clean)
env.txt Guest environment + PCI/module state
manifest.json Machine-readable catalog
VERDICT.md verdict full narrative: bug, reachability, fix, threat model
↓ download raw

DF-1574 β€” Heap OOB write in get_vddc_lookup_table (process_pptables_v1_0.c)

Verdict

NOT REPRODUCED ON THIS GUEST β€” the bug is real in source (line-by-line confirmed), but the affected translation unit is part of the amdgpu KLD module which is not in X86_64_GENERIC and is not loaded on this QEMU guest (no AMD GPU hardware present). This is a latent finding: a real OOB write that is unreachable at runtime on this guest and cannot be exercised by any harness we can build here without either (a) kldload amdgpu (root-only) or (b) AMD GPU hardware with a crafted VBIOS. The fix is authored, applies cleanly, and compiles cleanly with the kernel's own -Werror CFLAGS.

The bug (confirmed in source)

sys/dev/drm/amd/powerplay/hwmgr/process_pptables_v1_0.c, function get_vddc_lookup_table (lines 153-195):

  • Line 168-169 β€” allocation uses the caller's max_levels: table_size = sizeof(uint32_t) + sizeof(phm_ppt_v1_voltage_lookup_record) * max_levels;
  • Both callers (init_dpm_2_parameters) pass max_levels=16 (lines 287 and 296).
  • With sizeof(uint32_t)=4 and sizeof(record)=10 (5x uint16: us_calculated/us_vdd/us_cac_low/us_cac_mid/us_cac_high β€” see hwmgr_ppt.h:78-84), the allocation is 164 bytes (kmalloc-256).
  • Line 176 β€” table->count = vddc_lookup_pp_tables->ucNumEntries; (ucNumEntries is UCHAR 0..255 from VBIOS β€” vega10_pptable.h:237).
  • Line 178 β€” loop is bounded by ucNumEntries, not by max_levels: for (i = 0; i < vddc_lookup_pp_tables->ucNumEntries; i++).
  • Each iteration writes 10 bytes at offset 4 + i*10.
  • If ucNumEntries > 16 the loop runs past the 164-byte allocation.
  • Worst case ucNumEntries=255: last write at byte offset 4 + 254*10 = 2544 β†’ ~2390 bytes of controlled OOB write into the adjacent kmalloc-256 slab bucket.

Sibling functions are correct (rules out pattern blindness)

  • get_mclk_voltage_dependency_table (line 367+), get_sclk_voltage_dependency_table_by_bitmap (line 320+), get_mm_voltage_dependency_table, get_pcie_table β€” all use clk_dep_table->ucNumEntries for BOTH the table_size allocation AND the loop bound (lines 328-336, 531-542, 565-576, ...).
  • get_vddc_lookup_table is the only helper in this file that splits allocation-vs-loop bounds β€” a clear inconsistency.

Reachability on this guest

  • The amdgpu driver is not in X86_64_GENERIC (grep -iE 'drm|radeon|amdgpu' sys/config/X86_64_GENERIC β†’ empty).
  • It is also not in LINT64 (amdgpu is not listed; only drm and radeon are).
  • amdgpu is built only as a KLD module from sys/dev/drm/amd/amdgpu/Makefile (which lists process_pptables_v1_0.c).
  • At test time kldstat shows no drm/amdgpu/radeon modules loaded.
  • pciconf -l shows only QEMU std VGA (vgapci0@pci0:0:2:0, chip=0x11111234), no AMD GPU.
  • Therefore get_vddc_lookup_table cannot be reached at runtime on this guest. The CAM/scsi layer does not touch it; the only paths into it are pp_tables_v1_0_initialize (called during amdgpu driver attach, which requires real AMD GPU hardware or VFIO PCI passthrough of an AMD GPU).

Realistic threat model (when reachable)

  • An attacker who controls the VBIOS image supplied to the amdgpu driver can trigger the OOB. Two real-world scenarios: 1. VFIO PCI passthrough β€” a malicious guest OS supplies a crafted ROM file (e.g. via vfio-pci.romfile=) that the host driver parses on attach. 2. Malicious FPGA GPU / malicious PCIe device β€” presents an AMD vendor/device ID and serves a hostile PowerPlay table during probe.
  • In both cases the attacker must already have some privilege: root inside a VFIO-passthrough guest, or physical PCIe access. This is a rootβ†’kernel (or "kernel trusts PCI firmware") hardening gap, not an unprivileged-userβ†’root escalation.
  • On the realistic DragonFly target (default GENERIC, INVARIANTS ON), the slab INVARIANTS checks (chunk_mark_allocated/magic) would catch the cross-object overwrite and panic the kernel β€” i.e. the realistic impact on GENERIC is panic/DoS, not uid0. Without INVARIANTS the primitive is a large attacker-shaped heap write.

Exploit chain

Not applicable. The primitive is real (attacker-shaped ~2.4 KB heap write into kmalloc-256 bucket), but its only trigger paths are root-controlled (kldload + malicious VBIOS) on the default kernel, and the guest has no AMD GPU hardware at all. This is a valid hard blocker: the write is reachable only from an already-root context (kldload / VFIO romfile control / physical PCIe), and the affected code path is dead at runtime on this guest. The bug is documented as a latent memory-corruption defect with a real primitive characterization and a verified fix.

The fix

fix.diff β€” clamp both table->count and the loop bound to min(ucNumEntries, max_levels). Minimal, targeted, one logical change.

-table->count = vddc_lookup_pp_tables->ucNumEntries;
-
-for (i = 0; i < vddc_lookup_pp_tables->ucNumEntries; i++) {
+/* ucNumEntries comes from VBIOS (UCHAR, 0..255) but the buffer was
+ * sized for max_levels entries.  Clamp the count and loop bound to
+ * max_levels to prevent an out-of-bounds write past the slab object. */
+if (vddc_lookup_pp_tables->ucNumEntries > max_levels)
+   table->count = max_levels;
+else
+   table->count = vddc_lookup_pp_tables->ucNumEntries;
+
+for (i = 0; i < table->count; i++) {

PoC changes

No PoC source exists in findings/poc/DF-1574/ other than fix.diff and the build/run logs. A userspace PoC cannot exercise this path because the amdgpu driver is not loadable on the guest and the bug requires a malicious VBIOS at driver attach time. The trigger would have to be a crafted VBIOS ROM image supplied via vfio-pci.romfile= or a malicious FPGA GPU β€” neither of which the QEMU guest can present.

Reproduce

Not runnable on this guest (no AMD GPU; amdgpu not loadable).

# On a host with a real AMD GPU + DragonFly amdgpu driver:
#   1. Craft a VBIOS ROM with a Tonga PowerPlay table whose
#      Voltage_Lookup_Table has ucNumEntries > 16.
#   2. kldload amdgpu  (or boot with the GPU present)
#   3. Observe panic (INVARIANTS ON) or heap corruption (INVARIANTS OFF).
# Apply this finding's fix.diff and the OOB is gone.

The standalone translation-unit compile of the patched file (process_pptables_v1_0.c) is captured in fix_build.log.

Fix verification

not_testable

compile validated -Werror

module/translation-unit build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. get_vddc_lookup_table alloc by max_levels=16 but loop by ucNumEntries(u8) -> ~2.4KB OOB. amdgpu not in GENERIC, no AMD GPU.