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

Unchecked requested_clock_mode_index++ in r600 DYNPM downclock (discrete branch) β€” one-past-end OOB heap read of per-state clock_info

Summary

r600_pm_get_dynpm_state's DYNPM_ACTION_DOWNCLOCK discrete-ASIC branch increments requested_clock_mode_index without any comparison to the selected power state's num_clock_modes. On r6xx discrete hardware with PowerPlayInfo frev1/2 BIOS tables every state has exactly one clock mode, so the increment yields index 1 == mode count and clock_info[1] is read past the end of a 1-element kzalloc (DRM_DEBUG_DRIVER arguments are evaluated even with debug printing disabled) and via r600_pm_misc / radeon_set_power_state, whose garbage sclk/mclk/voltage are then programmed to the GPU. Sibling of DF-1196 but a distinct defect: that is the power_state index++ in the IGP/R600 branch vs num_power_states; this is the clock_mode index++ in the discrete branch vs num_clock_modes - different variable, different array dimension, different bounds source. Local user on r6xx/r7xx discrete with dynpm: one-past-end heap read driving GPU clock/voltage programming. On DragonFly the DYNPM method is additionally unreachable today (PM sysfs attrs #if 0) - latent like DF-1196.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3734 Β· 7 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
run.sh β€” 129 B view raw
run.log β€” 227 B view raw
env.txt β€” 231 B view raw
fix.diff β€” 908 B view raw
verdict.json β€” 3.8 KB view raw

DF-3734 β€” unchecked requested_clock_mode_index++ in r600 DYNPM downclock (discrete branch)

File: sys/dev/drm/radeon/r600.c:482-487 Class: kernel OOB heap read (one-past-end) β€” CWE-125 / CWE-1284 Severity: Low Β· Confidence: likely (source-certain defect; trigger gated on DYNPM) Bucket: kernleak (sibling of DF-1196, distinct site β€” see "not a duplicate" below)

Build / Run / Expected

There is no runnable userspace trigger: the bug is an internal index arithmetic defect on the kernel PM worker path and requires a physical r6xx/r7xx GPU with a specific ATOM BIOS PowerPlay table, plus pm_method=dynpm. The guest used for this audit is virtio-only (no AMD PCI device) and its kernel build ships no radeonkms.ko β€” see env.txt. The verification performed is therefore source-level (full trace in VERDICT.md) plus a guest module/hardware presence check (re-run any time):

dfbsd-qemu/vm.sh run_root 'ls /boot/kernel/radeonkms.ko; pciconf -l | grep 1002'

Expected result on real hardware (RV610/RV630/RV635/RV670 etc. with PowerPlayInfo frev1/2 BIOS, β‰₯2 power states, display connected, DYNPM enabled): power_state[req_ps].clock_info[1] is read one element past a 1-element kzalloc β€” at r600.c:516-522 (DRM_DEBUG_DRIVER args, evaluated even with debug printing off) and via r600_pm_misc() at r600.c:771 / radeon_pm.c:181-199 β†’ garbage sclk/mclk/voltage programmed to the GPU.

Trigger conditions (all required)

  1. pm_method == PM_METHOD_DYNPM β€” on DragonFly this is currently unreachable: the only transitions to DYNPM are the sysfs store radeon_set_pm_method (radeon_pm.c:444-449), and all sysfs attribute creation is compiled out (#if 0 at radeon_pm.c:1603-1616 and 1638-1647). The defect is latent in this tree, live in upstream-style configs.
  2. Non-IGP, non-R600 ASIC (RV610..RV770 discrete) β†’ the else branch of r600_pm_get_dynpm_state (r600.c:442-514).
  3. DYNPM_ACTION_DOWNCLOCK with requested_clock_mode_index == 0 (always true when the state's num_clock_modes == 1).
  4. active_crtc_count > 0 and clock_info[0].flags & RADEON_PM_MODE_NO_DISPLAY (set by default on non-default state 0 β€” radeon_atombios.c:2075-2078).
  5. Selected power state built by the frev1/2 parser β†’ every state has num_clock_modes == 1 and a 1-element clock_info allocation (radeon_atombios.c:2127-2133).

Then requested_clock_mode_index++ (r600.c:486) makes the index 1 == the state's mode count β†’ one-past-end.

Why this is NOT a re-report of DF-1196

DF-1196 covers the power_state index increment at r600.c:405 in the IGP/R600 branch (requested_power_state_index++ vs num_power_states). DF-3734 is the clock_mode index increment at r600.c:486 in the discrete-ASIC branch (requested_clock_mode_index++ vs num_clock_modes) β€” different variable, different array (clock_info is a per-state heap allocation, power_state the outer array), different branch, different bounds source.

Fix

fix.diff (bounds-check the increment). Never applied to sys/ β€” authored against the read-only tree.

Artifacts

  • VERDICT.md β€” full source-level proof and reachability analysis
  • run.sh, run.log, env.txt β€” guest module/hardware presence check
  • fix.diff β€” proposed fix
  • manifest.json, verdict.json
VERDICT.md
↓ download raw

VERDICT β€” DF-3734

status: inconclusive (runtime reproduction impossible on this audit guest: virtio-only, no AMD PCI device, no radeonkms.ko in the guest kernel build β€” env.txt). The defect itself is source-certain; the trace below is the evidence.

Root cause

r600_pm_get_dynpm_state(), discrete-ASIC branch (non-IGP, non-R600), DYNPM_ACTION_DOWNCLOCK:

sys/dev/drm/radeon/r600.c:469-487
    case DYNPM_ACTION_DOWNCLOCK:
        if (rdev->pm.requested_power_state_index == rdev->pm.current_power_state_index) {
            if (rdev->pm.current_clock_mode_index == 0) {
                rdev->pm.requested_clock_mode_index = 0;      // <-- can be 0
                rdev->pm.dynpm_can_downclock = false;
            } else
                rdev->pm.requested_clock_mode_index =
                    rdev->pm.current_clock_mode_index - 1;
        } else {
            rdev->pm.requested_clock_mode_index = 0;          // <-- can be 0
            rdev->pm.dynpm_can_downclock = false;
        }
        if ((rdev->pm.active_crtc_count > 0) &&
            (rdev->pm.power_state[rdev->pm.requested_power_state_index].
             clock_info[rdev->pm.requested_clock_mode_index].flags &
             RADEON_PM_MODE_NO_DISPLAY)) {
            rdev->pm.requested_clock_mode_index++;             // :486 UNCHECKED
        }

The increment at r600.c:486 has no comparison against power_state[requested_power_state_index].num_clock_modes.

Why the index can be out of bounds

struct radeon_power_state.clock_info is a per-state heap pointer (radeon.h:1300-1311), sized by the BIOS table parser. For PowerPlayInfo frev 1/2 (r6xx-era BIOSes) the parser gives every state exactly one mode:

sys/dev/drm/radeon/radeon_atombios.c:2126-2133
    if (!rdev->pm.power_state[state_index].clock_info) {
        rdev->pm.power_state[state_index].clock_info =
            kzalloc(sizeof(struct radeon_pm_clock_info), GFP_KERNEL);  // 1 element
    }
    ...
    rdev->pm.power_state[state_index].num_clock_modes = 1;

With num_clock_modes == 1, current_clock_mode_index is necessarily 0, so requested_clock_mode_index is always 0 entering the NO_DISPLAY check. The RADEON_PM_MODE_NO_DISPLAY flag is set by default on state 0's single mode (radeon_atombios.c:2075-2078), and the discrete branch selects requested_power_state_index = 1 (single-head, r600.c:461-462) or the multi-head fallback 0 (r600.c:458-460). If the selected state's mode 0 carries NO_DISPLAY, the ++ yields index 1 == num_clock_modes β€” exactly one past the end of a 1-element allocation.

OOB consumers (the read sites)

  1. r600.c:516-522 β€” DRM_DEBUG_DRIVER("Requested: e: %d m: %d p: %d\n", power_state[req_ps].clock_info[req_cm].sclk, ... .mclk, ...) β€” C function arguments are evaluated even when debug printing is disabled β†’ unconditional OOB heap read.
  2. radeon_pm.c:181-199 (radeon_set_power_state) β€” clock_info[req_cm].sclk and .mclk OOB; the garbage values are clamped only upward (> default_sclk), then programmed via radeon_set_engine_clock() / radeon_set_memory_clock() (radeon_pm.c:222-237).
  3. r600_pm_misc() r600.c:766-783 β€” ps->clock_info[req_cm].voltage OOB; a garbage VOLTAGE_SW voltage is passed to radeon_atom_set_voltage() (r600.c:777-778) β†’ arbitrary voltage index programmed to the GPU.

Impact ceiling: OOB kernel heap read (limited scope, kmalloc slab adjacent data) whose contents drive GPU clock/voltage programming β€” local DoS / hardware misbehavior class. No direct kernel-memory disclosure to userspace (values go to GPU registers), hence Low severity β€” consistent with the pass-1 rating of the sibling DF-1196 (Low).

Reachability analysis (DragonFly-specific)

  • r600_pm_get_dynpm_state is only called via radeon_pm_get_dynpm_state from the PM worker under pm_method == PM_METHOD_DYNPM (radeon_pm.c:1765/1777/1800/1948), all behind radeon_pm_compute_clocks_old's num_power_states < 2 guard (radeon_pm.c:1735).
  • The only write of PM_METHOD_DYNPM is the sysfs store radeon_set_pm_method (radeon_pm.c:444-449), and no sysfs power attributes are created on DragonFly β€” both creation blocks are #if 0'd (radeon_pm.c:1603-1616 for the DPM attrs, 1638-1647 for power_profile/power_method). pm_method is only ever PROFILE (default) or DPM (radeon_pm.c:1522-1588).
  • Therefore on this tree the DYNPM branch is dead code today β€” the defect is latent and becomes live the moment the sysfs knob is re-enabled (a one-line #if 1), or in upstream-derived trees. This is exactly the same reachability posture as the accepted DF-1196 (same entry path), filed as a finding by pass 1 for the same reason.

Guest verification performed

env.txt / run.log: DragonFly 6.5-DEVELOPMENT x86_64 guest; ls /boot/kernel/radeonkms.ko β†’ No such file (driver not built for this guest kernel; only the 224 radeonkmsfw_*.ko firmware stubs exist); pciconf -l | grep 1002 β†’ no AMD device (virtio-only). A runtime trigger is impossible on this guest; no kernel build/fix validation was attempted for the same reason (fix_status: not_testable).

Bounds-check the increment at r600.c:486 (see fix.diff): only increment while requested_clock_mode_index < power_state[requested_power_state_index].num_clock_modes - 1.

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 authored against the read-only sys/ tree; kernel fix build/validation not attempted β€” the guest has no radeon hardware and no radeonkms.ko, so neither baseline nor patched behavior is observable.

findings/poc/DF-3734/fix.diff
↓ fix.diffper-fix-DF-3734

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full source-level trace with path:line); fix.diff (bounds-checked increment); run.log + env.txt (guest has no AMD GPU and no radeonkms.ko β€” 224 radeonkmsfw_*.ko firmware stubs only); decisive lines: r600.c:486 (unchecked ++), radeon.h:1300-1311 (clock_info is per-state heap pointer), radeon_atombios.c:2126-2133 (1-element allocation, num_clock_modes=1), radeon_atombios.c:2075-2078 (NO_DISPLAY on state 0 by default), r600.c:516-522 + r600.c:771 + radeon_pm.c:181-199 (OOB consumers).

PoC changes

No runnable PoC exists for a kernel-internal PM index bug without r6xx hardware; replaced with a source-level proof plus guest module/hardware presence check.

Verified recommended fix

Bounds-check requested_clock_mode_index++ at r600.c:486 against power_state[requested_power_state_index].num_clock_modes (see fix.diff).

Verdict

Source-certain one-past-end OOB heap read: r600.c:486 increments requested_clock_mode_index in the discrete-branch DYNPM downclock path without checking num_clock_modes; with PowerPlayInfo frev1/2 BIOS tables every state has exactly one clock mode (radeon_atombios.c:2127-2133), so the increment yields index 1 == mode count and clock_info[1] is read past a 1-element kzalloc at r600.c:516-522 (DRM_DEBUG_DRIVER args are evaluated even with printing off) and via r600_pm_misc/radeon_set_power_state (r600.c:771, radeon_pm.c:181-199), programming garbage clocks/voltage. Runtime reproduction was impossible on the audit guest: virtio-only (no AMD PCI device) and radeonkms.ko is not built for the guest kernel (env.txt/run.log). On DragonFly the DYNPM method is additionally unreachable today because all PM sysfs attribute creation is compiled out (radeon_pm.c:1603/1638 #if 0), making the defect latent here but identical in reachability posture to accepted sibling DF-1196 (which is the power_state index at :405 in the IGP branch; DF-3734 is the clock_mode index at :486 in the discrete branch β€” different variable, array, and bounds source, not a duplicate).