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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3665 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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)

  1. The sink. sys/dev/drm/radeon/evergreen.c:2048-2049: (512 * 8 * 1000) / available_bandwidth and (128 * 4 * 1000) / available_bandwidth are unconditional u32 divisions executed at the top of evergreen_latency_watermark() β€” the num_heads == 0 early-return sits at evergreen.c:2057, after the divisions.

  2. Call chain to the sink. Any mode set on an enabled CRTC ends in atombios_crtc.c:1448 (radeon_bandwidth_update(rdev) inside evergreen_crtc_do_set_base) β†’ evergreen.c:2303 evergreen_bandwidth_update() β†’ evergreen.c:2323 evergreen_program_watermarks() β†’ evergreen.c:2216/2218 evergreen_latency_watermark(&wm_high/&wm_low). PM clock changes reach the same code via radeon_pm.c:312/1093/1112/1142.

  3. available_bandwidth can be zero β€” three ways (verbatim-math simulation in df3665.c, executed on the guest; see run.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.

  1. The trap. In-kernel, u32 / 0 on 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_SETCRTC user 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3665

Confirmed kernel references

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.