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

Divide-by-zero family in DCE11 display watermark math: dce_v11_0_latency_watermark divides by available_bandwidth and disp_clk before any guard (DF-3665 twin)

Summary

dce_v11_0_latency_watermark() computes (512*8*1000)/available_bandwidth, (128*4*1000)/available_bandwidth and 40000000/wm->disp_clk - all before the function's only guard (num_heads==0) and with no divisor validation on any caller path. When the memory/engine clocks read as zero, available_bandwidth() collapses to 0 and the mode-set path takes an integer #DE in kernel context -> panic. The clocks are seeded from the VBIOS ulDefaultEngineClock/ulDefaultMemoryClock fields with no zero-guard (every neighboring clock field in the same parser gets one) and are assigned even when the firmware-info parse fails, leaving 0. Secondary vectors: wm->disp_clk = mode->clock and the div_u64 by mode->clock. VERIFIED: arithmetic-level PoC executed on the guest - zero-clock input traps SIGFPE (signal 8 = #DE, confirmed in dmesg) 3/3 runs while the healthy-clock control returns normally. Local DoS on any machine with a Carrizo/Stoney/Polaris GPU whose clocks read as zero: an unprivileged user with /dev/dri/card0 access issues a mode-set and the kernel panics. Hardware-gated kernel reach on this guest; primitive + trigger chain fully traced; fix guards validated git-apply-clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3828 Β· 12 files
FileTypeDescriptionSize
README.md β€” 4.2 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
wm_div0.c β€” 4.5 KB view raw
build.sh β€” 98 B view raw
run.sh β€” 107 B view raw
build.log β€” 9 B view raw
run.log β€” 497 B view raw
run.2.log β€” 477 B view raw
env.txt β€” 873 B view raw
fix.diff β€” 2.7 KB view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 4.2 KB view raw

DF-3828 β€” amdgpu DCE11: divide-by-zero family in the display watermark math (dce_v11_0_latency_watermark + callers)

What

sys/dev/drm/amd/amdgpu/dce_v11_0.c, dce_v11_0_latency_watermark() (lines 903-951) β€” the DF-3665 (evergreen latency_watermark) family twin:

907 u32 available_bandwidth = dce_v11_0_available_bandwidth(wm);
908 u32 worst_chunk_return_time = (512 * 8 * 1000) / available_bandwidth;   /* DIV0 */
909 u32 cursor_line_pair_return_time = (128 * 4 * 1000) / available_bandwidth; /* DIV0 */
910 u32 dc_latency = 40000000 / wm->disp_clk;                               /* DIV0 */
...
918 if (wm->num_heads == 0)     /* guard placed AFTER the divides */
919     return 0;

All three divides execute before the only guard in the function, and none of the three divisors is validated anywhere on the path.

Zero-input sources (traced)

dce_v11_0_program_watermarks() (1039-1161) fills the struct:

  • 1058-1066: !adev->pm.dpm_enabled β†’ wm.yclk = adev->pm.current_mclk * 10, wm.sclk = adev->pm.current_sclk * 10. pm.current_sclk/current_mclk are seeded from amdgpu_atombios_get_clock_info() (sys/dev/drm/amd/amdgpu/amdgpu_atombios.c:718-719) straight from the VBIOS ulDefaultEngineClock/ulDefaultMemoryClock (:677-680) without any zero-guard β€” note every neighbouring clock field in that parser does get a guard (:610, :642, :669, :694, :708), these two do not, and the assignment at :718 even runs when the firmware-info parse fails with ret != 0, leaving both clocks 0.
  • With yclk == 0 (or sclk == 0), dce_v11_0_available_bandwidth() (851-859) = min(dram, data_return, dmif) = 0 β†’ line 908/909 divide by zero.
  • 1068: wm.disp_clk = mode->clock β€” a zero pixel clock reaches line 910 (secondary vector; the primary realistic one is the zero sclk/mclk).

Trigger (local user β†’ kernel panic)

dce_v11_0_crtc_do_set_base() β†’ dce_v11_0_bandwidth_update() (dce_v11_0.c:2106) β†’ dce_v11_0_program_watermarks() (1186) β†’ dce_v11_0_latency_watermark() (1085 / 1124). A mode-set ioctl from any DRM client (user with /dev/dri/card0 access β€” console/X users) on a system where the clocks read as zero (VBIOS default-clock fields zero + DPM off) takes #DE in kernel context β†’ panic. Sibling divides by wm->num_heads at 967/987 are safe only because the caller's guard at 1050 requires num_heads != 0; dce_v11_0_check_latency_hiding() divides wm->lb_size / wm->src_width (1004) β€” same family, requires a zero-width mode.

Impact

Kernel divide-by-zero (#DE) β†’ immediate panic of a machine with an AMD Carrizo/Stoney/Polaris/VEGAM GPU (DCE11/11.2): local denial of service by an unprivileged user with graphics-device access. Hardware-gated (AMD ASIC required). Severity Medium (local DoS, unusual-config clock-zero precondition).

Reproduce (arithmetic-level replica β€” no AMD GPU required)

scp wm_div0.c dfbsd-maxx:/tmp/
ssh dfbsd 'cc -O2 -Wall -o /tmp/wm_div0 /tmp/wm_div0.c'
ssh dfbsd '/tmp/wm_div0; echo exit=$?'

Expected (see run.log, run.2.log):

healthy clocks: watermark math returns 2297 (no trap)
affected clocks (current_mclk=0, current_sclk=0): entering latency_watermark()...
Floating point exception (core dumped)
exit=136        <- SIGFPE = #DE, the exact kernel fault class

The replica copies the arithmetic of dce_v11_0_dram_bandwidth/data_return_bandwidth/dmif_request_bandwidth β†’ available_bandwidth β†’ the three divides at :908-:910 with inputs taken from argv (so the compiler cannot fold the trap away). Userspace #DE = SIGFPE; in kernel mode the identical div instruction is an unrecoverable

DE β†’ panic.

Kernel-path verification status

The audit guest has no AMD display ASIC (pciconf -l: bochs VGA 1234:1111 only; no /dev/dri, no drm/amdgpu modules β€” env.txt), so dce_v11_0_* never executes in-kernel there: verdict not_reproduced for the kernel reachability; the divide-by-zero primitive itself is proven on-guest (SIGFPE, 2 runs) and certain at source level.

Fix

fix.diff β€” validate available_bandwidth and wm->disp_clk before dividing (and move the num_heads check first), plus harden the mode->clock divides at 1051/1053 and add the missing zero-guards for default_sclk/default_mclk in the VBIOS clock parser.

VERDICT.md
↓ download raw

DF-3828 VERDICT

Bottom line

**Source-certain divide-by-zero family in the DCE11 watermark math; arithmetic-level reproduction on the guest succeeded (SIGFPE = the exact

DE fault class, 2/2 runs); kernel-path reproduction impossible here (no

AMD ASIC). Verdict not_reproduced for the kernel reach with the primitive proven and the trigger chain fully traced.**

The defect

dce_v11_0_latency_watermark() β€” sys/dev/drm/amd/amdgpu/dce_v11_0.c:

  • :908 (512 * 8 * 1000) / available_bandwidth β€” divides by dce_v11_0_available_bandwidth(wm) (:851-859) = min(dram_bw, data_return_bw, dmif_bw); each term is a fixed-point product that truncates to 0 when its clock input is 0.
  • :909 (128 * 4 * 1000) / available_bandwidth β€” same divisor.
  • :910 40000000 / wm->disp_clk β€” wm->disp_clk = mode->clock (program_watermarks :1068).
  • :918 if (wm->num_heads == 0) return 0; β€” the only guard, placed after the divides (mirrors the DF-3665 evergreen pattern).

No caller validates the divisors: dce_v11_0_program_watermarks (:1085, :1124) guards only enabled && num_heads && mode (:1050).

Zero-clock provenance (kernel refs)

  • dce_v11_0_program_watermarks:1064-1065 (DPM-disabled path): wm.yclk = adev->pm.current_mclk * 10; wm.sclk = adev->pm.current_sclk * 10.
  • amdgpu_atombios_get_clock_info β€” sys/dev/drm/amd/amdgpu/amdgpu_atombios.c: default_sclk/default_mclk taken verbatim from the VBIOS firmware-info table at :677-680 without the zero-guard that every neighboring clock field gets (:610, :642, :669, :694, :708), and unconditionally assigned to pm.current_sclk/current_mclk at :718-719 even when the table parse failed (ret != 0), leaving both 0.
  • Consequence: on a board whose VBIOS reports zero default engine/memory clocks (or after a failed clock-info parse) with power management disabled, the very first watermark computation divides by zero.

Trigger chain (unprivileged local user)

drm_mode_setcrtc (any DRM client with /dev/dri/card0, e.g. an X/wayland user; no DRM-Master privilege needed beyond the fd) β†’ dce_v11_0_crtc_do_set_base β†’ dce_v11_0_bandwidth_update (dce_v11_0.c:2106) β†’ dce_v11_0_program_watermarks (:1186) β†’ dce_v11_0_latency_watermark (:1085/:1124) β†’ div by 0 β†’ #DE β†’ panic. Also reached from dce_v11_0_crtc_dpms β†’ amdgpu_pm_compute_clocks bandwidth updates on DPMS transitions.

What was run on the guest

wm_div0.c (argv-parameterized, so clang cannot fold the trap β€” the first version did fold it into ud2/SIGILL, which is itself proof the divide is reachable, but the shipped version traps the honest way):

  • run.log, run.2.log: healthy-clock control returns 2297; zero-clock input β†’ Floating point exception (core dumped), exit 136 (signal 8 = SIGFPE = #DE), confirmed by dmesg: pid ... (wm_div0) ... exited on signal 8.
  • env.txt: guest PCI bus = i440FX + bochs VGA (1234:1111) + virtio; no AMD display controller; no /dev/dri; no drm/amdgpu kld β†’ the kernel path cannot attach on this guest.

Classification per honest-status table

Guest reachable + target device absent β†’ not_reproduced, reproduced=0, impact=none (nothing kernel-level observed). Realistic impact ceiling on equipped hardware: local DoS via kernel panic (Medium). Confidence: certain (source trace + on-guest arithmetic proof of the fault class).

Negative results (classes hunted and killed in this file)

  • IV-ring OOB in dce_v11_0_crtc_irq (:3332-3333, no bounds check on crtc = entry->src_id - 1) and dce_v11_0_pageflip_irq (:3239-3242, array load before the bounds check): killed β€” amdgpu_irq_dispatch (amdgpu_irq.c:414-432) only invokes handlers for registered (client_id, src_id) pairs and validates src_id < AMDGPU_MAX_IRQ_SRC_ID; crtc_irq is registered only for src_id ∈ {1..num_crtc} (:2870) and pageflip_irq for src_id ∈ {8..18} (:2875), so the handler-side indices stay within crtcs[6] (AMDGPU_MAX_CRTCS=6, amdgpu_mode.h:65). Hardening nit only.
  • Audio pin arrays: audio.pin[AMDGPU_MAX_AFMT_BLOCKS=9] (amdgpu_mode.h:261) vs num_pins ≀ 8 and pin_offsets[8] (:1426-1436) β€” in-bounds; afmt dig->dig_encoder ≀ 6 < afmt[9] β€” in-bounds; dce_v11_0_audio_enable NULL-checks the pin (:1419).
  • dce_v11_0_is_display_hung crtc_status[6] vs num_crtc ≀ 6 β€” in-bounds; vga_control_regs[6] indexed by crtc_id ≀ 5 β€” in-bounds.
  • num_heads divides at :967/:987 β€” protected by caller guard :1050.
  • Suspend/resume: dce_v11_0_suspend/resume (:3003-3032) are plain hw_fini/hw_init wrappers with saved backlight; no lifetime issue found.

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 guards all three divides plus the mode->clock divides and the VBIOS zero-clock seeding. A patched-kernel build cannot be exercised on this guest: amdgpu never attaches without an AMD ASIC, so there is no observable kernel behavior difference - hardware fix validation is not_testable here.

fix.diff; VERDICT.md
↓ fix.diffper-fix-DF-3828

Confirmed kernel references

Detail

Exploit chain

unprivileged user opens /dev/dri/card0 (graphics/console user) -> drm_mode_setcrtc on any connector -> dce_v11_0_crtc_do_set_base -> dce_v11_0_bandwidth_update (dce_v11_0.c:2106) -> dce_v11_0_program_watermarks -> dce_v11_0_latency_watermark -> div by available_bandwidth==0 -> #DE -> panic. Precondition: pm.current_sclk or current_mclk == 0 (VBIOS zero default clocks / failed clock-info parse with DPM off).

Evidence (decisive lines)

run.log + run.2.log (SIGFPE exit=136 twice, healthy control clean); VERDICT.md (full chain incl. amdgpu_atombios.c zero-guard omission and the killed IV-ring OOB theories); env.txt (no AMD ASIC on guest)

PoC changes

Authored from scratch (no seed). v1 constant-folded the zero divisor into ud2/SIGILL under -O2; inputs moved to argv so the trap is the genuine #DE -> SIGFPE (signal 8), matching the kernel fault class.

Verified recommended fix

Validate available_bandwidth and wm->disp_clk non-zero before the divides in dce_v11_0_latency_watermark (bail to 0 like the num_heads==0 path), guard mode->clock in program_watermarks, and add the missing zero-guards for default_sclk/default_mclk in amdgpu_atombios_get_clock_info.

Verdict

Source-certain divide-by-zero family in the DCE11 display watermark math: dce_v11_0_latency_watermark() divides by available_bandwidth at dce_v11_0.c:908-909 and by wm->disp_clk at :910, all before the function's only guard (num_heads==0, :918) and with no divisor validation on any caller path. Zero inputs traced end-to-end: amdgpu_atombios_get_clock_info() copies VBIOS ulDefaultEngineClock/ulDefaultMemoryClock into default_sclk/default_mclk with no zero-guard (amdgpu_atombios.c:677-680 - unlike neighboring fields at :610/:642/:669/:694/:708) and assigns them to pm.current_sclk/current_mclk even on parse failure (:718-719); with DPM disabled, dce_v11_0_program_watermarks (:1064-1065) feeds them straight into the bandwidth terms, which collapse to 0, and a mode-set ioctl (dce_v11_0.c:2106 call chain) takes #DE -> kernel panic on any Carrizo/Stoney/Polaris/VEGAM machine in that state: local DoS by an unprivileged DRM client. The arithmetic primitive is proven on the guest: the exact replica of available_bandwidth()+latency_watermark() traps with SIGFPE (signal 8 = #DE, exit 136) in 2/2 runs while the healthy-clock control returns normally (run.log, run.2.log, dmesg in VERDICT.md). Kernel-path reproduction is impossible on this guest - it has no AMD display ASIC (bochs VGA only, no /dev/dri, env.txt) - hence not_reproduced for the kernel reach; impact ceiling on equipped hardware: dos via panic.