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)
PoC verification
Evidence pack
findings/poc/DF-3828 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_mclkare seeded fromamdgpu_atombios_get_clock_info()(sys/dev/drm/amd/amdgpu/amdgpu_atombios.c:718-719) straight from the VBIOSulDefaultEngineClock/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 withret != 0, leaving both clocks 0. - With
yclk == 0(orsclk == 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.
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 bydce_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_mclktaken 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 topm.current_sclk/current_mclkat :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 bydmesg: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 oncrtc = entry->src_id - 1) anddce_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 validatessrc_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 withincrtcs[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) vsnum_pinsβ€ 8 andpin_offsets[8](:1426-1436) β in-bounds; afmtdig->dig_encoderβ€ 6 <afmt[9]β in-bounds;dce_v11_0_audio_enableNULL-checks the pin (:1419). dce_v11_0_is_display_hungcrtc_status[6]vsnum_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_testablefix.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
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.
No comments yet.