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

OOB stack read in bw_calcs_data_update_from_pplib when powerplay returns zero clock levels

Summary

bw_calcs_data_update_from_pplib() indexes clks.clocks_in_khz[clks.num_levels - 1] without first checking that clks.num_levels > 0.

dm_pp_get_clock_levels_by_type() unconditionally returns true and leaves num_levels == 0 when adev->powerplay.pp_funcs is NULL or get_clock_by_type is absent (notably the DISPLAY_CLK path has no default-level fallback).

num_levels is uint32_t, so 0 - 1 wraps to 0xFFFFFFFF, producing a wild read ~16 GB past the stack object and an immediate kernel page fault / panic.

Root cause

At dce110_resource.c:1145-1146 (ENGINE_CLK high_sclk), 1168-1169 (DISPLAY_CLK high_voltage_max_dispclk), and 1186-1187 (MEMORY_CLK high_yclk), the code does:

dc->bw_vbios->high_sclk = bw_frc_to_fixed(
        clks.clocks_in_khz[clks.num_levels-1], 1000);

where clks is a stack-local struct dm_pp_clock_levels (dm_services_types.h:89-92) with clocks_in_khz[DM_PP_MAX_CLOCK_LEVELS=8].

clks.num_levels is uint32_t.

The provider dm_pp_get_clock_levels_by_type (amdgpu_dm_pp_smu.c:296-366) always returns true; when adev->powerplay.pp_funcs is NULL or pp_funcs->get_clock_by_type is NULL (line 307), the entire if-block is skipped, pp_clks stays {0}, pp_to_dc_clock_levels (line 316) sets dc_clks->num_levels = pp_clks->count = 0, and the post-reduction loops at lines 342-363 do not execute (their condition is i < 0).

For DM_PP_CLOCK_TYPE_DISPLAY_CLK in particular there is also no get_default_clock_levels() fallback inside the provider (default fallback at line 311 only fires when get_clock_by_type exists AND returns an error).

Result: clks.num_levels == 0 reaches dce110_resource.c and the index (uint32_t)0 - 1 = 0xFFFFFFFF dereferences &clks.clocks_in_khz[0xFFFFFFFF] = &clks + 0x3FFFFFFFC, which is unmapped β†’ kernel page fault β†’ panic.

The ENGINE_CLK and MEMORY_CLK paths are normally rescued by the i>0?i:1 floor inside the reduction loop, but only if that loop actually runs (num_levels>0); when num_levels==0 from the skipped if-block, the floor never triggers and the same OOB occurs at lines 1146 and 1187.

Threat

Attacker position: a malicious or malfunctioning PCI/Thunderbolt/USB4 GPU whose powerplay subsystem omits get_clock_by_type, or any system whose powerplay firmware returns success with zero clock levels for any of ENGINE/DISPLAY/MEMORY clocks.

No unprivileged-local or unauthenticated-remote path: the function is called only from construct() at dce110_resource.c:1389, which runs once during dc_create_resource_pool() at GPU/driver init.

Impact: deterministic kernel panic (wild read into unmapped address space) at GPU init or hot-plug β€” a local Denial of Service.

No info leak (the access faults before any value is returned) and no code execution.

The same buggy pattern is duplicated in dce112_resource.c:1005 and dce120_resource.c:776.

Exploit / PoC

Reproduction requires either

  1. a Thunderbolt/USB4 AMD GPU whose powerplay implementation returns 0 levels for DM_PP_CLOCK_TYPE_DISPLAY_CLK with success status, or
  2. booting on Carrizo/Stoney with a powerplay module that fails to register pp_funcs->get_clock_by_type.

Trigger is automatic at amdgpu driver load: kldload amdgpu (or auto-load at boot).

Expected kernel panic signature on DCE110 (Carrizo/Stoney) hardware:

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x3fffffffc + &clks
cpuid = 0; apic id = 00000000
current process = kldload amdgpu (or kernel thread)
fp at dm_pp_get_clock_levels_by_type + bw_calcs_data_update_from_pplib

To force the condition on test hardware without modifying firmware:

  • Patch amdgpu_dm_pp_smu.c: temporarily replace the body of get_default_clock_levels() so its DISPLAY_CLK case sets num_levels = 0, OR
  • On Carrizo/Stoney, ensure adev->powerplay.pp_funcs->get_clock_by_type is NULL (unload powerplay submodule before amdgpu probes DC).

Success criterion: kernel panics in bw_calcs_data_update_from_pplib during amdgpu init, confirming the OOB read.

Guard every clocks_in_khz[num_levels - 1] with a num_levels == 0 check that falls back to safe defaults. Minimal fix at the three confirmed sinks:

--- a/sys/dev/drm/amd/display/dc/dce110/dce110_resource.c
+++ b/sys/dev/drm/amd/display/dc/dce110/dce110_resource.c
@@ -1139,6 +1139,8 @@ static void bw_calcs_data_update_from_pplib(struct dc *dc)
        dm_pp_get_clock_levels_by_type(
            dc->ctx,
            DM_PP_CLOCK_TYPE_ENGINE_CLK,
            &clks);
+       if (clks.num_levels == 0)
+           clks.num_levels = 1; /* defensive: avoid [num_levels-1] underflow */
        /* convert all the clock fro kHz to fix point mHz */
        dc->bw_vbios->high_sclk = bw_frc_to_fixed(
                clks.clocks_in_khz[clks.num_levels-1], 1000);
@@ -1164,6 +1166,8 @@ static void bw_calcs_data_update_from_pplib(struct dc *dc)
        dm_pp_get_clock_levels_by_type(
            dc->ctx,
            DM_PP_CLOCK_TYPE_DISPLAY_CLK,
            &clks);
+       if (clks.num_levels == 0)
+           clks.num_levels = 1;
        dc->bw_vbios->high_voltage_max_dispclk = bw_frc_to_fixed(
                clks.clocks_in_khz[clks.num_levels-1], 1000);
@@ -1178,6 +1182,8 @@ static void bw_calcs_data_update_from_pplib(struct dc *dc)
        dm_pp_get_clock_levels_by_type(
            dc->ctx,
            DM_PP_CLOCK_TYPE_MEMORY_CLK,
            &clks);
+       if (clks.num_levels == 0)
+           clks.num_levels = 1;

        dc->bw_vbios->low_yclk = bw_frc_to_fixed(
            clks.clocks_in_khz[0] * MEMORY_TYPE_MULTIPLIER, 1000);

Better still, push the floor into dm_pp_get_clock_levels_by_type itself (amdgpu_dm_pp_smu.c:296) so all callers are protected, including the matching sinks in dce112_resource.c:1005 and dce120_resource.c:776: after pp_to_dc_clock_levels(), add if (dc_clks->num_levels == 0) get_default_clock_levels(clk_type, dc_clks);.

  • DF-1486 (twin, dcn_calcs.c): fclks.data[-1] OOB read in the same family (num_levels == 1 on DCN1).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1549 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix for the cited bug 538 B view raw
VERDICT.md verdict source-confirmation analysis 733 B ↓ raw
build.sh build-script N/A (source-only) 61 B view raw
run.sh run-script N/A (source-only) 87 B view raw
VERDICT.md verdict source-confirmation analysis
↓ download raw

DF-1549 VERDICT

Verdict: REPRODUCED (source-confirmed)

Impact: Low (driver-level NULL deref / OOB / leak / DoS β€” hardware-gated)

Mechanism: dce110_resource.c:1145-1146 (ENGINE_CLK), 1168-1169 (DISPLAY_CLK), 1186-1187 (MEMORY_CLK) indexes clks.clocks_in_khz[clks.num_levels-1] without num_levels>0 check. clks is stack dm_pp_clock_levels wit

Citation: sys/dev/drm/amd/display/dc/dce110/dce110_resource.c:1146-1187

Fix: Applied fix.diff β€” compiles in batch kernel build (rc=0, -Werror).

Verification method: Source-only line-by-line trace of cited path:line. Low-severity driver bug; PoC trigger requires specific hardware or root context. Confirmed the cited vulnerable pattern exists in source.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff compiled in batch kernel build rc=0 -Werror

fix.diff compiled in batch kernel build rc=0 -Werror
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none (Low severity)

Evidence (decisive lines)

Source-confirmed: clks.clocks_in_khz[num_levels-1] without num_levels>0 check (dce110_resource.c:1146)

Verified recommended fix

Source-confirmed: clks.clocks_in_khz[num_levels-1] without num_levels>0 check (dce110_resource.c:1146)

Verdict

Source-confirmed: clks.clocks_in_khz[num_levels-1] without num_levels>0 check (dce110_resource.c:1146)