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

vega20_get_sclk_od/get_mclk_od: count-1 underflow wild read (+34 GB) and DIV_ROUND_UP divide-by-zero on zero golden clock via world-readable pp_sclk_od/pp_mclk_od (DF-3686/DF-3707 twins)

Summary

vega20_get_sclk_od and get_mclk_od index dpm_levels[count - 1] with unsigned count and then DIV_ROUND_UP(value*100, golden_value) with no guards. Case A (div0): with the DPM SMU feature disabled, the single golden level is seeded from the raw VBIOS bootup clock copied with no zero check - a crafted/blank VBIOS value 0 makes golden_value 0 -> #DE panic on first read of the world-readable sysfs. Case B (wild read): after a failed SMU re-enable on resume (no cleanup), golden_dpm_table stays the memset-zero copy -> count==0 -> dpm_levels[0xFFFFFFFF] = read at ~32 GiB past the kzalloc'd backend. Guest-verified arithmetic: SIGFPE 3/3 for case A; case B's verbatim wild load confirmed executing in guest objdump. Unprivileged local DoS (panic) plus wild kernel OOB read on Vega20 - any local user can 'cat pp_sclk_od'; preconditions VBIOS-gated or a degraded post-resume DPM state.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3858 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 5.7 KB ↓ raw
od_div0.c β€” 4.2 KB view raw
build.sh β€” 115 B view raw
run.sh β€” 128 B view raw
run.log β€” 527 B view raw
run.2.log β€” 1.4 KB view raw
env.txt β€” 467 B view raw
fix.diff β€” 1.7 KB view raw
verdict.json β€” 5.0 KB view raw
manifest.json β€” 1.3 KB view raw

DF-3858 β€” vega20_get_sclk_od/get_mclk_od: count-1 underflow wild read + divide-by-zero on zero golden clock (DF-3686/DF-3707 twins)

What

sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:1318-1381:

int value        = sclk_table->dpm_levels[sclk_table->count - 1].value;        /* :1326 */
int golden_value = golden_sclk_table->dpm_levels[golden_sclk_table->count - 1].value; /* :1327-28 */
value -= golden_value;
value = DIV_ROUND_UP(value * 100, golden_value);                               /* :1332  <-- #DE */

(twin at :1372-1378 for memclk.)

Case A β€” divide-by-zero (DF-3707 twin). When the DPM_GFXCLK (resp. DPM_UCLK) SMU feature is not enabled, vega20_setup_gfxclk_dpm_table() takes the boot-clock branch (:544-547): count = 1; dpm_levels[0].value = data->vbios_boot_state.gfx_clock / 100; β€” the raw VBIOS bootup_sclk_in10khz copied with no zero check (ppatomfwctrl.c:523/:556 β†’ vega20_hwmgr.c:745). A crafted/blank VBIOS value 0 β‡’ golden_value == 0 β‡’ DIV_ROUND_UP(x, 0) β‡’ #DE panic.

Case B β€” unsigned count-1 underflow (DF-3686 twin). If DPM (re-)enable aborts inside vega20_setup_default_dpm_tables() (any SMU message failure, PP_ASSERT at :594/:613/... returns early after the memset of the whole dpm_table at :588), dpm_table/golden_dpm_table stay all-zero (count == 0). Boot-time failure tears the device down (hwmgr_hw_init β†’ err2 frees the backend, probe fails) β€” but the resume path has no cleanup (hwmgr_resume, hwmgr.c:308-330: returns the error with the backend intact and pm_en untouched), and pm_en was set at powerplay-create time (amd_powerplay.c:51), so after a failed SMU re-enable on resume the world-readable sysfs node still dispatches into get_sclk_od β‡’ dpm_levels[(uint32_t)0 - 1] = dpm_levels[0xFFFFFFFF] β‡’ read at +34,359,738,360 bytes (~32 GiB) past dpm_levels[0] inside the kzalloc'd struct vega20_hwmgr backend β€” a wild kernel OOB read; if it lands on a zero, case A's divide then panics.

Reach (world-readable)

/sys/class/drm/card0/device/pp_sclk_od and pp_mclk_od are S_IRUGO (amdgpu_pm.c:1019-1022) β†’ amdgpu_get_pp_sclk_od (:794, no other check) β†’ pp_dpm_get_sclk_od (amd_powerplay.c:757, only !hwmgr || !pm_en guard) β†’ vega20_get_sclk_od. Any local user can cat it.

Reproduce (arithmetic-level; kernel reach hardware-gated)

build.sh    # cc -O2 -Wall -o /tmp/od_div0 /tmp/od_div0.c
run.sh      # /tmp/od_div0 0   (case A)   /tmp/od_div0 1   (case B)

Expected (observed on the audit guest, run.log):

case A: ... DIV_ROUND_UP(x, 0)
Floating point exception (core dumped)        # exit=136 (SIGFPE = #DE), 3/3 runs
case B: golden gfx_table.count = 0 -> index (uint32_t)0-1 = 4294967295
        effective read offset = 34359738360 bytes past dpm_levels[0] (elem size 8)

Disassembly on the guest confirmed the case-B load mov 0x401ef8(,%rax,8),%ecx with rax = 0xFFFFFFFF really executes (+34 GB address); in this userland process it happened to land on an unrelated mapped page (returned garbage 0) β€” in the kernel the backend is a kmalloc object and a +34 GB landing is near-certainly unmapped (page-fault panic) or feeds garbage into the case-A divide.

Impact

Local DoS (panic) and/or wild OOB read via an unprivileged sysfs read, on a Vega20 (Radeon VII / MI50 / MI60) in the affected states (crafted VBIOS zero boot clock; or DPM bring-up failure). Hardware/VBIOS-gated.

VERDICT.md
↓ download raw

DF-3858 VERDICT β€” vega20_get_sclk_od/get_mclk_od count-1 underflow + div0

Status: not_reproduced (kernel reach hardware-gated) β€” both arithmetic facts proven on the guest (SIGFPE 3/3; wild +34 GiB index proven in disassembly), source chain traced end-to-end.

Root cause (path:line, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c

unless noted)

vega20_get_sclk_od() :1318-1335 (mem twin :1364-1381):

  • :1326 sclk_table->dpm_levels[sclk_table->count - 1].value
  • :1327-1328 golden_sclk_table->dpm_levels[golden_sclk_table->count - 1].value
  • :1332 value = DIV_ROUND_UP(value * 100, golden_value);

No zero-count and no zero-divisor guard.

Case A β€” golden_value == 0 (DF-3707 twin)

  1. vega20_setup_gfxclk_dpm_table() :538-547: when data->smu_features[GNLD_DPM_GFXCLK].enabled is false, dpm_table->count = 1; dpm_table->dpm_levels[0].value = data->vbios_boot_state.gfx_clock / 100; (:545-546).
  2. data->vbios_boot_state.gfx_clock = boot_up_values.ulGfxClk (vega20_hwmgr.c:745), where ulGfxClk = fw_info->bootup_sclk_in10khz (ppatomfwctrl.c:523 and :556) β€” raw VBIOS u32, no zero check anywhere on this path.
  3. vega20_setup_default_dpm_tables() :713-715 memcpy's the whole table into golden_dpm_table, so the golden top (and only) value is 0.
  4. First cat pp_sclk_od after boot β‡’ DIV_ROUND_UP(0*100, 0) β‡’ #DE β‡’ panic. (Values in the enabled path are protected: vega20_get_dpm_frequency_by_index() :499-501 asserts *clk != 0 β€” the developer bounded the SMU path but not the VBIOS boot-clock path.)

Case B β€” count == 0 wild read (DF-3686 twin)

  1. vega20_setup_default_dpm_tables() :588 memset(&data->dpm_table, 0, sizeof(data->dpm_table)); each sub-table is filled only if its SMU feature is enabled and every SMU message succeeds (:592-711, PP_ASSERT at :594/:541/:562/... return early).
  2. Boot-time failure is torn down: hwmgr_hw_init (hwmgr.c:246-256) goes err2 β†’ backend_fini (kfree backend) β†’ pp_hw_init (amd_powerplay.c:118-130) returns the error β†’ the powerplay IP block init fails β†’ the device never probes (no sysfs). The live degraded state is resume: hwmgr_resume (hwmgr.c:308-330) calls phm_enable_dynamic_state_management with no cleanup on failure (no backend free, pm_en untouched), leaving the driver attached with the memset-zero dpm tables whenever SMU re-enable fails after suspend (flaky SMU / VF passthrough states).
  3. hwmgr->pm_en was set at powerplay-create time (amd_powerplay.c:51) and is not cleared on the resume failure; the sysfs nodes exist for the lifetime of the device (amdgpu_device.c:2664, amdgpu_pm.c:1019-1022) and pp_dpm_get_sclk_od (amd_powerplay.c:757-763) checks only !hwmgr || !pm_en β€” so a world-readable cat pp_sclk_od still dispatches into vega20_get_sclk_od.
  4. golden_dpm_table.count == 0 β‡’ count - 1 == 0xFFFFFFFF (uint32 count, vega20_hwmgr.h:156-159) β‡’ dpm_levels[0xFFFFFFFF].value: element size 8 β‡’ effective read offset 34,359,738,360 bytes past dpm_levels[0] in the kzalloc'd struct vega20_hwmgr backend (vega20_hwmgr_backend_init() :383).
  5. Wild read outcome: unmapped β‡’ page-fault panic; mapped garbage β‡’ value - garbage then divided by garbage β€” and if the garbage golden value is 0, case A's #DE. (DF-1253 covers the analogous underflow inside vega20_apply_clocks_adjust_rules() :3095-3104 with the mclk_latency_table.count loop; this finding is the distinct get_sclk_od/get_mclk_od sink, checklist item (c) "OD table getters, get_max_sclk/mclk consumers".)

Why vega20_get_profiling_clk_mask/force_clock_level are NOT affected

:2229-2231 (count - 1 into a mask) and :2249/:2276 (soft_max_level >= count reject) were traced: even with count==0 the mask path shifts by 0xFFFFFFFF (x86 shl masks to 31) β‡’ soft_max_level = 31 β‰₯ count β‡’ -EINVAL. No OOB.

What ran on the Guest

od_div0.c replicates the exact struct layout (vega20_hwmgr.h:140-171) and the verbatim get_sclk_od body.

  • Case A (count=1, value=0): SIGFPE, exit=136, 3/3 runs (run.log Γ—2, run.2.log), dmesg pid NNNN (od_div0), uid 0: exited on signal 8.
  • Case B (count=0): prints index 4294967295 and effective offset 34359738360 bytes; the verbatim access executed (objdump on the guest: mov 0x401ef8(,%rax,8),%ecx with rax=0xFFFFFFFF) and landed on an unrelated mapped page in this userland process (returned 0) β€” in-kernel the same address relative to a kmalloc'd backend is a wild read.

Why not reproduced

Audit guest has 0 AMD PCI devices, no /dev/dri (env.txt); vega20_hwmgr code cannot execute. Kernel reach needs a Vega20 board in state A (crafted VBIOS zero boot clock with GFXCLK DPM feature disabled) or state B (failed DPM bring-up). Impact ceiling: unprivileged local DoS via panic + wild OOB read.

Fix validation

fix.diff makes vega20_get_sclk_od/get_mclk_od return 0 when either table count is 0 or the golden top value is 0 (mirroring the DF-3707 fix), and zero-guards the VBIOS boot-up clocks in vega20_init_smc_table (floor to existing smu_features-independent 10 kHz minimum like other consumers). git-apply-clean against the read-only sys/ tree; a patched kernel cannot be exercised on this guest (no Vega20 ASIC), so fix validation is not_testable.

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

fix.diff adds the count/golden-value guards to both OD getters and is git-apply-clean against the read-only sys/ tree; no Vega20 ASIC on this guest (amdgpu never binds), so a patched-kernel run cannot be exercised - not_testable

fix.diff; VERDICT.md; env.txt
↓ fix.diffper-fix-DF-3858

Confirmed kernel references

Detail

Exploit chain

unprivileged local user: cat /sys/class/drm/card0/device/pp_sclk_od -> amdgpu_get_pp_sclk_od (amdgpu_pm.c:794) -> pp_dpm_get_sclk_od (amd_powerplay.c:757) -> vega20_get_sclk_od -> (A) DIV_ROUND_UP by golden_value==0 -> #DE panic, or (B) dpm_levels[0xFFFFFFFF] wild read at +34.4 GB -> page fault/panic or garbage into the divide. Preconditions: (A) VBIOS bootup_sclk_in10khz==0 with DPM_GFXCLK disabled; (B) failed SMU re-enable on resume leaving memset-zero dpm tables.

Evidence (decisive lines)

run.log (case A SIGFPE exit=136 x2, case B index/offset printout); run.2.log (3rd trap + dmesg 'exited on signal 8' + objdump of the verbatim wild load); VERDICT.md (full chain incl. resume-path reachability correction and killed profiling-mask variants); fix.diff (git-apply-clean); env.txt (no AMD ASIC)

PoC changes

Authored from scratch. One build fix: missing for the struct replica (guest cc 8.3).

Verified recommended fix

Guard vega20_get_sclk_od/get_mclk_od: return 0 when either table count is 0 or the golden top value is 0 (and zero-floor the VBIOS boot-up clocks in vega20_init_smc_table)

Verdict

Source-traced dual defect in vega20_get_sclk_od/vega20_get_mclk_od (vega20_hwmgr.c:1318-1381), the DF-3686/DF-3707 twins: (A) DIV_ROUND_UP(value*100, golden_value) at :1332/:1378 divides by zero when the golden top clock is 0 - reachable with the DPM_GFXCLK/DPM_UCLK SMU feature disabled, where setup_gfxclk/memclk_dpm_table seeds dpm_levels[0].value from the raw VBIOS bootup_sclk/mclk_in10khz copied with no zero check (ppatomfwctrl.c:523/:556 -> vega20_hwmgr.c:745-746, :546/:567, golden copy at :714); (B) dpm_levels[count-1] with count==0 wraps to 0xFFFFFFFF - a wild read 34,359,738,360 bytes (~32 GiB) past dpm_levels[0] in the kzalloc'd vega20_hwmgr backend - reachable after a failed SMU re-enable on resume (hwmgr.c:308-330 has no cleanup, pm_en stays set from amd_powerplay.c:51) with the sysfs node still dispatching. Trigger is the world-readable sysfs pp_sclk_od/pp_mclk_od (S_IRUGO, amdgpu_pm.c:1019-1022 -> pp_dpm_get_sclk_od amd_powerplay.c:757, only !hwmgr||!pm_en guard). Guest evidence: case A traps SIGFPE (exit=136, signal 8 = the kernel #DE class) 3/3 runs; case B's verbatim access with rax=0xFFFFFFFF confirmed in guest objdump (mov 0x401ef8(,%rax,8),%ecx) computing the +34 GiB address. Kernel reach not reproducible here: 0 AMD PCI devices, no /dev/dri (env.txt). Distinct from known DF-1253 (the apply_clocks_adjust_rules mclk_latency_table loop), DF-1254 (OD8 arrays), DF-1255 (setup_single_dpm_table write). Impact ceiling on Vega20 hardware in the affected states: unprivileged local DoS via panic + wild OOB read.