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

hwmgr->num_vce_state_tables stored unclamped (up to 255) over vce_states[6] β€” fill loop silently skipped, count left as a loaded gun for any consumer

Summary

The count is stored BEFORE the AMD_MAX_VCE_LEVELS guard, which only skips the fill loop - a crafted VBIOS leaves a stored count up to 255 over the 6-entry array with zero entries populated. The consumer trusts the stored count as the array bound; today coincidentally fenced by the ioctl-side constant in the only caller loop. The amdgpu DPM path clamps the same count, proving the intended invariant. Clamp-and-fill fix (case 7 of the DF-4388 unit test, PASS).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4389 Β· 9 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.2 KB ↓ raw
fix.diff β€” 1000 B view raw
build.sh β€” 65 B view raw
run.sh β€” 89 B view raw
run.log β€” 34 B view raw
env.txt β€” 478 B view raw
manifest.json β€” 735 B view raw
verdict.json β€” 2.2 KB view raw

DF-4389 β€” hwmgr->num_vce_state_tables stored unclamped (up to 255) against vce_states[6]; fill loop silently skipped

hwmgr->num_vce_state_tables = i = ppt_get_num_of_vce_state_table_entries_v1_0(hwmgr);
if ((i != 0) && (i <= AMD_MAX_VCE_LEVELS)) {        /* guard covers FILL only */
    for (j = 0; j < i; j++)
        ppt_get_vce_state_table_entry_v1_0(hwmgr, j, &(hwmgr->vce_states[j]), NULL, &flags);
}
  • i comes straight from the VBIOS VCE-state-table ucNumEntries (UCHAR, 0..255 β€” and when usVCEStateTableOffset==0, from whatever byte the PowerPlay header alias yields; DF-1576 notes the missing zero-check).
  • hwmgr->vce_states[AMD_MAX_VCE_LEVELS] = 6 entries (sys/dev/drm/amd/powerplay/inc/hwmgr.h:723, sys/dev/drm/amd/include/kgd_pp_interface.h:74).
  • The count is stored unconditionally; a VBIOS with ucNumEntries > 6 leaves num_vce_state_tables up to 255 while the array holds at most 6 (zeroed or stale) entries.
  • Consumer: pp_dpm_get_vce_clock_state (sys/dev/drm/amd/powerplay/amd_powerplay.c:868-869): if (idx < hwmgr->num_vce_state_tables) return &hwmgr->vce_states[idx]; β€” currently reached only from AMDGPU_INFO_VCE_CLOCK_TABLE (amdgpu_kms.c:775) whose loop is bounded by AMDGPU_VCE_CLOCK_TABLE_ENTRIES = 6 (amdgpu_drm.h:962), so today the OOB read is fenced by coincidence: any future consumer trusting the stored count (or a raised ioctl bound) reads up to 249 entries past vce_states[] inside struct pp_hwmgr and copies the fields to userspace.
  • Sibling evidence the clamp is the intended semantics: the amdgpu DPM path clamps the very same firmware count β€” sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:580-581: numEntries > AMD_MAX_VCE_LEVELS ? AMD_MAX_VCE_LEVELS : numEntries.

Build / run

Hardware-gated on the audit guest (no AMD GPU β€” run.log); the clamp arithmetic is case 7 of ../DF-4388/v1_clamp_test.c (255 β†’ 6, PASS on the guest) and the fix rides in DF-4388's fix.diff hunk; a standalone minimal fix.diff is included here.

Expected

With the fix: num_vce_state_tables never exceeds 6 and the first 6 VCE states are populated instead of none.

VERDICT.md
↓ download raw

DF-4389 VERDICT

Trace

  1. ppt_get_num_of_vce_state_table_entries_v1_0 (:1210-1223) returns the VCE state table's ucNumEntries (UCHAR). No offset-zero check (DF-1576), so usVCEStateTableOffset==0 yields an aliased header byte.
  2. get_powerplay_table_entry_v1_0 (:1328) stores that value into hwmgr->num_vce_state_tables before the i <= AMD_MAX_VCE_LEVELS guard, which only protects the fill loop (:1330-1333). Result for ucNumEntries > 6: stored count up to 255, zero entries populated.
  3. Destination: hwmgr->vce_states[AMD_MAX_VCE_LEVELS=6] (hwmgr.h:723, kgd_pp_interface.h:74).
  4. Consumer pp_dpm_get_vce_clock_state (amd_powerplay.c:868-869) trusts the stored count as the exclusive bound for vce_states[idx].

Impact today: bounded only because the sole caller path (AMDGPU_INFO_VCE_CLOCK_TABLE, amdgpu_kms.c:775-784) iterates i < AMDGPU_VCE_CLOCK_TABLE_ENTRIES = 6 β€” a constant that happens to equal the array size. With that coincidence the observable defect is a misreported VCE clock table (zeros) on hostile VBIOS; without it, an OOB read of struct pp_hwmgr tail β†’ userspace info leak. The amdgpu DPM path (amdgpu_dpm.c:580-581) clamps this same firmware count, showing the intended invariant.

Verification (Phase V)

Hardware-gated (no AMD GPU on guest, run.log). Demonstrated: - clamp arithmetic (255 β†’ 6) is case 7 of DF-4388's unit test β€” PASS on the guest (../DF-4388/unit_test_guest.log). - fix.diff (1 hunk, standalone) git apply --check OK; identical hunk is part of DF-4388's compile-validated process_pptables_v1_0_fixed.c (guest cc -fsyntax-only, 0 errors).

Bottom line

Certain (code-proven), currently impact-fenced by a coincidental equal constant in the only consumer β†’ Low (latent OOB / wrong-data, hardening). Fixed centrally in DF-4388's diff; standalone hunk provided.

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 no→ patch + rebuild →patched clean

not_testable at runtime (no AMD ASIC on guest); standalone fix.diff git-apply OK and the identical hunk is inside DF-4388's compile-validated patched file (guest cc, 0 errors).

fix.diff; ../DF-4388/fix_build.log; ../DF-4388/unit_test_guest.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md trace (path:line); ../DF-4388/unit_test_guest.log case 7 (255->6 PASS); fix.diff git apply --check OK; integrated fixed file compile-validated in DF-4388 (0 errors)

PoC changes

n/a - pass-2 finding authored fresh

Verified recommended fix

clamp num_vce_state_tables to AMD_MAX_VCE_LEVELS before storing (fix.diff; also in DF-4388 fix.diff)

Verdict

process_pptables_v1_0.c:1328 stores the firmware VCE-state count (UCHAR, up to 255; garbage when usVCEStateTableOffset==0) into hwmgr->num_vce_state_tables BEFORE the AMD_MAX_VCE_LEVELS guard, which only skips the fill - so a crafted VBIOS leaves a count of up to 255 over a 6-entry hwmgr->vce_states[] (hwmgr.h:723). Today the sole consumer path is coincidentally fenced (AMDGPU_INFO_VCE_CLOCK_TABLE loops i<6, amdgpu_kms.c:775); any consumer trusting the stored count OOB-reads struct pp_hwmgr into userspace. amdgpu_dpm.c:580 clamps the same count, showing the intended invariant. Low/latent: clamp added in DF-4388 fix.diff; standalone hunit provided. Hardware-gated on guest.