# 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:

```c
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.
