Divide-by-zero (#DE panic) in evergreen_latency_watermark(): zero available_bandwidth from tiny user mode clocks, u32 fixed-point wrap of mclk/sclk, or zero VBIOS clocks
Summary
evergreen_latency_watermark() unconditionally divides by evergreen_available_bandwidth() (a min() of three u32 fixed-point products). The divisor is zero in three reachable states: (a) a DRM-master-submitted mode with pixel clock <= ~38 kHz - drm_mode_validate_basic() only rejects clock==0 and radeon registers no mode_valid hook, so dmif bandwidth truncates to 0; (b) mclk/sclk in the u32 fixed20_12 dfixed_const() wrap windows - any clock >= 104857.6 kHz wraps (exhaustive u32-semantics scan: 351 killer mclk values incl. exactly 524288/1048576/1572864 kHz and 156 killer sclk values in 0..4 GHz); (c) VBIOS FirmwareInfo reporting zero clocks. The num_heads==0 early-return sits AFTER the divisions. VERIFIED by verbatim-math reproduction on the audit guest: 9/9 SIGFPE on the executed division (divisor pipe-fed so GCC cannot fold) for clock=25/1/38 kHz, mclk wrap values, and mclk/sclk=0. Any local user holding DRM master panics the kernel with one DRM_IOCTL_MODE_SETCRTC carrying a 1..38 kHz user mode; VBIOS-wrap boards panic on first mode set or DPM transition. Kernel #DE panic = reliable local DoS; no corruption/escalation. Fix: guard the divisors (deeper fix: 64-bit fixed-point).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3665 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df3665.c | β | 8.0 KB | view raw | |
| build.sh | β | 86 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.log | β | 22 B | view raw | |
| run.log | β | 2.8 KB | view raw | |
| env.txt | β | 596 B | view raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| fix.diff | β | 1.5 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-3665 VERDICT β divide-by-zero in evergreen_latency_watermark()
Reproduced? (what "reproduced" means here)
The arithmetic root cause and the divide-by-zero itself are fully proven; the kernel
path cannot be executed on this audit guest because it has no radeon hardware. The
guest is virtio-only (env.txt: vgapci0 ... chip=0x11111234 virtio-gpu, no
/dev/dri, no drm kmods loaded), so sys/dev/drm/radeon/evergreen.c code paths are
unreachable in the running kernel. Per the audit convention for hardware-absent
findings, the machine verdict is inconclusive (reproduced=0) with source-level
certainty documented below.
The proof chain (all path:line verified against this tree)
-
The sink.
sys/dev/drm/radeon/evergreen.c:2048-2049:(512 * 8 * 1000) / available_bandwidthand(128 * 4 * 1000) / available_bandwidthare unconditional u32 divisions executed at the top ofevergreen_latency_watermark()β thenum_heads == 0early-return sits atevergreen.c:2057, after the divisions. -
Call chain to the sink. Any mode set on an enabled CRTC ends in
atombios_crtc.c:1448(radeon_bandwidth_update(rdev)insideevergreen_crtc_do_set_base) βevergreen.c:2303 evergreen_bandwidth_update()βevergreen.c:2323 evergreen_program_watermarks()βevergreen.c:2216/2218evergreen_latency_watermark(&wm_high/&wm_low). PM clock changes reach the same code viaradeon_pm.c:312/1093/1112/1142. -
available_bandwidthcan be zero β three ways (verbatim-math simulation indf3665.c, executed on the guest; seerun.log):
a. User-controlled mode clock 1..38 kHz. radeon_display.c:1350
radeon_mode_funcs has no .mode_valid, so drm_mode_convert_umode()
(drm_modes.c, calls drm_mode_validate_driver β drm_mode_validate_basic
at drm_modes.c:1103-1130) only rejects clock == 0 β 25 kHz passes.
wm.disp_clk = mode->clock (evergreen.c:2172) β
evergreen_dmif_request_bandwidth() (evergreen.c:1989-2007) computes
trunc(32 * (25/1000) * 0.8) = 0 β min() at evergreen.c:2016 β 0.
b. u32 fixed-point wrap. drm_fixed.h:32-42: fixed20_12 is a u32;
dfixed_const(A) = (u32)(A << 12). Inputs are wm->yclk = mclk*10 /
wm->sclk = sclk*10 (evergreen.c:2163-2169, 2190-2196), so any clock
β₯ 104,857.6 kHz wraps. Exhaustive scan (0..4 GHz, step 1 kHz, u32 semantics):
351 mclk values zero evergreen_dram_bandwidth() β including exactly
524288, 1048576, 1572864 kHz and the kΒ·2^20/10 wrap bands (419431 kHz shown)
β and 156 sclk values zero evergreen_data_return_bandwidth(). These
clocks are VBIOS/DPM-controlled (rdev->pm.current_mclk/sclk,
radeon_dpm_get_mclk/sclk).
c. VBIOS zero clocks (FirmwareInfo reporting mclk/sclk = 0, the DF-1708
class) trivially zero the bandwidth.
- The trap. In-kernel,
u32 / 0on x86-64 raises #DE β kernel panic. The PoC executes the exact division (divisor fed through a pipe so the compiler cannot fold it) in forked children: 9/9 divisor-zero scenarios died with SIGFPE (run.log), the userspace manifestation of the same fault. The three control scenarios (typical HD5870/HD5770/sumo clocks) returned 2851/1900/487 β nonzero, no trap β demonstrating the boundary is exactly where the math says it is (clock = 39 kHz β 1; clock = 38 kHz β 0).
Why not "reproduced"/panic on the guest
The guest kernel would panic at evergreen.c:2048 on the first bandwidth_update
after such a mode set β but there is no Evergreen GPU in the QEMU guest (virtio
display only), so /dev/dri/radeon cannot exist and the ioctl path cannot run.
Building a patched kernel to "validate the fix" is likewise not testable without the
device; fix.diff is validated by inspection (it moves the divisions behind a
zero-guard, mirroring the arithmetic the PoC demonstrates).
Threat model
- Local user with DRM-master rights (console user, X session, or any local user when
no other master holds the device) submits a
DRM_IOCTL_MODE_SETCRTCuser mode with clock β€ 38 kHz β immediate kernel panic (path 3a needs no unusual hardware state at all beyond an Evergreen-class radeon). - Conversely, unlucky-but-realistic VBIOS/DPM memory or engine clock values (524.288 MHz, 1048.576 MHz, ...) panic on the first mode set at boot or on the first DPM level change (paths 3b/3c).
Impact: reliable local denial of service (kernel panic). No memory corruption, no privilege escalation. Severity Medium, consistent with the audit's precedent for VBIOS-divisor panics (DF-1386, DF-1650, DF-1137).
Fix
Guard the divisor (and while there, guard wm->disp_clk for line 2050) β see
fix.diff:
u32 mc_latency = 2000; /* 2000 ns. */
u32 available_bandwidth = evergreen_available_bandwidth(wm);
+ if (!available_bandwidth || !wm->disp_clk)
+ return 0;
u32 worst_chunk_return_time = (512 * 8 * 1000) / available_bandwidth;
Returning 0 makes the watermark "no latency hiding required", which is the safe
fallback the function already uses for num_heads == 0. A deeper fix (out of scope
for a minimal security patch) would keep the fixed-point math in 64-bit so the
dfixed_const() wrap stops corrupting every watermark above 1 GHz inputs.
Cross-references
- Same-family audit findings: DF-1386 (cypress burst time), DF-1650 (rs780 drm_mode_vrefresh), DF-1431 (radeon pll sub-10 kHz clock).
- The u32 fixed20_12 wrap additionally makes all DCE4/5 watermark values garbage for mclk β₯ 104.86 MHz β a correctness issue upstream shares; only the zero-window values are security-relevant.
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_testablefix.diff adds the zero-guard before the divisions; cannot be kernel-tested on this guest (no radeon device to reach evergreen_bandwidth_update), but the guard provably makes the zero-divisor paths return early - the exact condition the PoC demonstrates traps without it.
fix.diff; VERDICT.md 'Fix' section; PoC controls show nonzero-divisor behavior unchanged
Confirmed kernel references
- sys/dev/drm/radeon/evergreen.c:2048
- sys/dev/drm/radeon/evergreen.c:2049
- sys/dev/drm/radeon/evergreen.c:2016
- sys/dev/drm/radeon/evergreen.c:1989
- sys/dev/drm/radeon/evergreen.c:2163
- sys/dev/drm/include/drm/drm_fixed.h:35
- sys/dev/drm/drm_modes.c:1114
- sys/dev/drm/radeon/radeon_display.c:1350
- sys/dev/drm/radeon/atombios_crtc.c:1448
Detail
Evidence (decisive lines)
run.log: every 'available_bandwidth=0 ... child killed by SIGFPE' line (9 scenarios incl. clock=25 kHz and mclk=524288 kHz; controls at top return 2851/1900/487); df3665.c: verbatim port of drm_fixed.h u32 fixed-point + evergreen.c:1929-2017 bandwidth functions; VERDICT.md: full path:line proof chain atombios_crtc.c:1448 -> evergreen.c:2303/2323/2216/2048; fix.diff: zero-guard patch
PoC changes
Seed sketch was rewritten from scratch: initial draft inlined dfixed_div(dfixed_const(7), ...) which does not type-check (kernel assigns to fixed20_12 temps first) - fixed to mirror kernel statements; the first SIGFPE demo was silently dead-code-eliminated by GCC 8.3 (unused division result), the second was value-range-folded (comparison provably false) - fixed by feeding the divisor through a pipe and deriving the child exit code from the quotient bits, forcing a real idiv; added control scenarios and the 39 kHz boundary case.
Verified recommended fix
Guard the divisors in evergreen_latency_watermark(): if (!available_bandwidth || !wm->disp_clk) return 0; before the two divisions at evergreen.c:2048-2050
Verdict
Kernel-path trigger is hardware-bound (guest is virtio-only: no radeon device, no /dev/dri, no drm kmods), so the panic itself cannot be executed here; the root cause and the divide are fully proven. evergreen_latency_watermark() (evergreen.c:2048-2049) divides by evergreen_available_bandwidth(), which verbatim-math simulation on the guest shows is ZERO for (a) a DRM-master user mode with pixel clock <= 38 kHz - drm_mode_validate_basic() only rejects clock==0 and radeon registers no mode_valid hook (radeon_display.c:1350); (b) mclk/sclk in the u32 fixed20_12 dfixed_const() wrap windows (exactly 524288/1048576/1572864 kHz and k*2^20/10 bands; 351 mclk and 156 sclk killer values in 0..4 GHz); (c) VBIOS-reported zero clocks. The exact evergreen.c:2048 division was executed in forked children (divisor fed via pipe so GCC cannot fold it) and 9/9 divisor-zero scenarios died with SIGFPE - the userspace face of the kernel #DE panic. Controls (typical clocks) returned nonzero bandwidth and did not trap, confirming the boundary (clock 38 kHz -> 0, 39 kHz -> 1). Impact ceiling: reliable local kernel-panic DoS via DRM_IOCTL_MODE_SETCRTC by any DRM master, or boot/first-modeset panic on unlucky VBIOS DPM clocks. No corruption, no escalation.
No comments yet.