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

One-sided OD count clamps: VBIOS counts size the od_feature_capabilities/od_settings_max/min heap allocations downward while vega20_hwmgr consumers index them by fixed driver enums β€” heap OOB reads at driver init with the leaked u32s printed to the world-readable OD_RANGE sysfs node

Summary

The clamps have no floor: a crafted VBIOS declaring ODSettingCount=1 gets 4-byte arrays while consumers unconditionally index 0..13/0..9 at init - a ~52-byte read window past a small heap object whose adjacent-slab u32s are exfiltrated to unpriv readers via the pp_od_clk_voltage sysfs whenever they pass the OD range gates. Harness-reproduced on guest: 21 OOB reads (farthest +48B) with a planted marker printed as 'SCLK: ... 2779054081Mhz'; floors+bounded-copies fix validated by full amdgpu.ko rebuild -Werror. Crafted-firmware precondition; distinct from DF-1689/1690 (blob reads vs driver-side heap arrays).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4544 Β· 14 files
FileTypeDescriptionSize
od_count_oflow.c β€” 15.1 KB view raw
build.sh β€” 118 B view raw
run.sh β€” 97 B view raw
build.log β€” 17 B view raw
run.log β€” 2.1 KB view raw
run.2.log β€” 4.3 KB view raw
fix_run.log β€” 2.1 KB view raw
fix.diff β€” 2.2 KB view raw
fix_build.log β€” 8.6 KB view raw
env.txt β€” 335 B view raw
README.md β€” 4.1 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 4.6 KB view raw

DF-4544 β€” vega20 OD count clamps are one-sided: VBIOS-sized allocations vs driver-enum indices

Severity: Medium (heap OOB read + filtered heap info leak via sysfs) | Confidence: certain File: sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:823-846

What

init_powerplay_table_information() clamps the VBIOS-provided OverDrive8Table.ODFeatureCount / ODSettingCount from above only (lines 823-833):

od_feature_count = (le32_to_cpu(...ODFeatureCount) >
            ATOM_VEGA20_ODFEATURE_COUNT) ? ATOM_VEGA20_ODFEATURE_COUNT : ...;
od_setting_count = (le32_to_cpu(...ODSettingCount) >
            ATOM_VEGA20_ODSETTING_COUNT) ? ATOM_VEGA20_ODSETTING_COUNT : ...;

and then sizes the heap copies by those counts (copy_overdrive_feature_capabilities_array :694-695, phm_copy_overdrive_settings_limits_array smu_helper.c:73-74). A table that declares ODSettingCount = 1 therefore gets 4-byte od_settings_max / od_settings_min allocations and a 1-byte od_feature_capabilities allocation (ODFeatureCount = 1).

But the consumers in vega20_hwmgr.c index those arrays with the fixed driver-side enums, unconditionally, at driver init:

  • vega20_od8_set_feature_capabilities reads od_settings_max/min[OD8_SETTING_*] for indices 1..13 (vega20_hwmgr.c:906-976; OD8_SETTING_ID vega20_hwmgr.h:398-417) and od_feature_capabilities[1..9] (:913-975)
  • the fill loop (vega20_hwmgr.c:1216-1227) copies od_settings_min/max[i] into od8_settings_array[i].min/max
  • vega20_print_clock_levels(OD_RANGE) (vega20_hwmgr.c:2861-2905) prints those values to the world-readable pp_od_clk_voltage sysfs node.

With ODSettingCount = 1 the first OOB read is od_settings_max[OD8_SETTING_GFXCLK_FMAX = 1] (vega20_hwmgr.c:907) β€” bytes 4..7 past a 4-byte kmalloc object; the farthest consumer index (13) reads at +52 B. A crafted VBIOS (or malicious vGPU ROM) therefore gets a heap out-of-bounds read window of up to ~52 bytes past a small kernel heap object, with the leaked u32s filtered only by the OD range gates and then exposed to unprivileged userspace through the OD_RANGE sysfs dump. Distinct from the knowns: DF-1689/DF-1690 are OOB reads of the VBIOS blob itself (no size validation); this finding is about the driver-side heap copies being sized by firmware counts that the clamps only bound from above.

Reproduce (lifted-copy harness, DF-1179/DF-4395 convention)

The in-kernel path needs a Vega20 ASIC with a crafted VBIOS (env.txt shows the guest has only the QEMU std VGA β€” no AMD GPU), so the harness replicates the exact struct layouts (vega20_pptable.h, pack(1)), the exact clamp logic, the kmalloc-sized arrays with a marker-filled "adjacent slab" tail, and the exact consumer condition sequence.

./build.sh     # cc -O2 -Wall (guest cc 8.3)
./run.sh

Expected (run.log): - CONTROL full counts (10/14) β†’ 0 OOB reads, normal OD_RANGE output - CRAFTED ODFeatureCount=10 / ODSettingCount=1 β†’ od_settings_max 4-byte object: 21 OOB reads, farthest +48 B past; od_settings_min 16 OOB reads; OD_RANGE prints SCLK: 800Mhz 2779054081Mhz β€” 2779054081 = 0xA5A50001, the marker planted at the first u32 past the object β†’ heap leak to sysfs demonstrated - CRAFTED ODFeatureCount=1 β†’ od_feature_capabilities 1-byte object: 9 OOB reads (indices 1..9) - PATCHED (fix.diff: allocations floored at the driver counts) β†’ same crafted input, 0 OOB reads, no markers in the output

Fix validation

  • fix.diff β€” git apply --check OK; allocations floored at ATOM_VEGA20_ODFEATURE_COUNT / ATOM_VEGA20_ODSETTING_COUNT, copies bounded
  • full amdgpu.ko module rebuilt with the patch applied under the real kernel toolchain (-Werror): RC=0 (fix_build.log); guest /usr/src restored to pristine afterwards
  • harness PATCHED control: 0 OOB reads for the same crafted input

Files

  • od_count_oflow.c β€” the harness (sha256 a55b58d859d8addc4358bea67a58bcba7faff0150d0f38c999bf4a2c3854f1a2)
  • build.sh / run.sh, build.log / run.log / run.2.log / fix_run.log
  • fix.diff, fix_build.log, env.txt, VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

VERDICT β€” DF-4544

Reproduced: YES (lifted-copy harness on the audit guest; in-kernel path hardware-gated)

  • Root cause proven (vega20_processpptables.c:823-846): the clamps min(VBIOS_count, ATOM_VEGA20_ODSETTING_COUNT) are one-sided β€” they only bound the count from above. The kzalloc calls (copy_overdrive_feature_capabilities_array :695, phm_copy_overdrive_settings_limits_array smu_helper.c:73-75) size the heap arrays by exactly that clamped count.
  • Consumer indices proven (vega20_hwmgr.c): vega20_od8_set_feature_ capabilities (:906-976) indexes od_settings_max/min at OD8_SETTING_GFXCLK_FMAX=1 first (line 907) and up to index 13; the fill loop (:1216-1227) writes od_settings_min/max[i] into od8_settings_array[i]; print_clock_levels(OD_RANGE) (:2861-2905) prints min/max to the world-readable OD sysfs node. Driver enum has 16 members (vega20_hwmgr.h:398-417); the fill loop guards i via feature_id which is only ever set for 0..13 β€” hence indices 0..13 vs an allocation that can legally be a single u32.
  • Harness result (run.log, deterministic, 3 runs incl. run.2.log): crafted ODFeatureCount=10/ODSettingCount=1 β†’ od_settings_max = 4-byte object, 21 OOB reads, farthest +48 B past; od_settings_min 16 OOB reads; od_feature OOB (idx 1..9) with ODFeatureCount=1 β†’ 9 reads past a 1-byte object. The u32 at the first OOB slot (0xA5A50001 = 2779054081) passed the OD gates and was printed as SCLK: 800Mhz 2779054081Mhz in the replicated OD_RANGE output β€” the sysfs exfil path is real, gated only by comparisons the attacker's heap contents must satisfy (> 0, >= min, <= 100 for the power-percentage setting). In the kernel the values read are whatever follows the small kmalloc object in its slab.
  • Controls: full-count table β†’ 0 OOB reads; PATCHED (floors) β†’ 0 OOB reads, no markers printed.

Why not uid=0

Information-disclosure class, not corruption: the primitive is a bounded (few dozen bytes) read window past a small heap object, filtered through range gates, at driver-load time (one-shot β€” the arrays are parsed once at pptable_init). No write primitive, no repeat trigger without a driver reload. Realistic ceiling: opportunistic heap-content disclosure to an unprivileged sysfs reader on a machine whose VBIOS/ROM was crafted. Threat model requires attacker-controlled firmware (physical flash / vGPU+custom ROM), same precondition as the known DF-1689/DF-1690 family.

Fix validation

  • git apply --check OK (generated against pristine sys/; never applied there)
  • Full amdgpu.ko rebuild with the fix under the real kernel toolchain with -Werror: RC=0 (fix_build.log); /usr/src restored + verified
  • Harness PATCHED control: 0 OOB reads for the same crafted input

Kernel refs

sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:823, sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:829, sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:835, sys/dev/drm/amd/powerplay/hwmgr/vega20_processpptables.c:839, sys/dev/drm/amd/powerplay/hwmgr/smu_helper.c:73, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.h:398, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:906, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:1216, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2861, sys/dev/drm/amd/powerplay/hwmgr/vega20_pptable.h:76

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 AMD ASIC). fix.diff is git-apply-able, compiles clean in a full amdgpu.ko module build under the real kernel toolchain with -Werror (fix_build.log, RC=0), and the harness PATCHED control shows 0 OOB reads for the same crafted input.

fix.diff; fix_build.log (module rebuild RC=0, 0 errors); run.log PATCHED section (0 OOB reads, no markers printed)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 x86_64 (amdgpu.ko toolchain validation)

Confirmed kernel references

Detail

Exploit chain

crafted VBIOS/vGPU ROM with ODFeatureCount=10, ODSettingCount=1 -> pptable_init allocates 4-byte od_settings_max/min -> vega20_od8_set_feature_capabilities reads indices 1..13 past the object -> values passing the range gates propagate to od8_settings_array -> OD_RANGE sysfs dump prints adjacent-slab u32s to unprivileged readers (read window ~52 B, one-shot at driver load)

Evidence (decisive lines)

run.log (CRAFTED: 21 OOB reads farthest +48 B, 'SCLK: 800Mhz 2779054081Mhz' = marker 0xA5A50001 leaked; CONTROL + PATCHED: 0 OOB); run.2.log (repeat, deterministic); fix_run.log (decisive rerun incl. PATCHED); fix_build.log (full amdgpu.ko rebuild with fix.diff, -Werror, RC=0); fix.diff (git apply --check OK)

PoC changes

authored fresh this run (pass 2); no seed PoC existed

Verified recommended fix

floor the od_* allocations at the driver-enum counts (ATOM_VEGA20_ODFEATURE_COUNT / ATOM_VEGA20_ODSETTING_COUNT) and bound the copies by the table counts (fix.diff)

Verdict

One-sided count clamps in vega20_processpptables.c:823-833: ODFeatureCount/ODSettingCount from the VBIOS size the od_feature_capabilities (1-byte entries) and od_settings_max/min (4-byte entries) heap allocations downward (kzalloc at :695 / smu_helper.c:73-75), while the vega20_hwmgr.c consumers index them with fixed driver enums (OD8_SETTING_ 0..13, ATOM_VEGA20_ODFEATURE_ 0..9) unconditionally at driver init. With a crafted table declaring ODSettingCount=1 the first OOB read is od_settings_max[1] (vega20_hwmgr.c:907), 4 bytes past a 4-byte object; the farthest index (13) reads +52 B. The values are copied into od8_settings_array min/max (:1219-1221) and printed by print_clock_levels(OD_RANGE) (:2861-2905) to the world-readable pp_od_clk_voltage sysfs node. Reproduced on the audit guest with a faithful lifted-copy harness (struct layouts from vega20_pptable.h pack(1), exact clamp+alloc+consumer sequence, marker-filled adjacent-slab emulation): 21 OOB reads up to +48 B, and the first past-object u32 (0xA5A50001) passed the OD gates and appeared in the replicated sysfs output -- a filtered kernel-heap info leak to unprivileged readers. CONTROL (full counts) and PATCHED (allocations floored at driver counts) show 0 OOB reads. In-kernel path hardware-gated (no AMD GPU on the guest, env.txt); crafted-VBIOS threat model shared with knowns DF-1689/DF-1690 but a distinct defect (driver-side heap arrays vs blob OOB reads).