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

Divide-by-zero family in DCE8 display watermark math: dce_v8_0_latency_watermark divides by available_bandwidth and disp_clk before any guard (DF-3828 DCE11 / DF-3857 DCE10 twin)

Summary

dce_v8_0_latency_watermark() computes the three divides before its only guard (num_heads==0 - dead anyway via the caller gate). With DPM disabled, yclk/sclk derive from pm.current clocks seeded from unguarded VBIOS ulDefaultEngineClock/ulDefaultMemoryClock (assigned even on parse failure); one zero term collapses available_bandwidth to 0 and a mode set takes #DE -> kernel panic. VERIFIED: exact-replica harness on the guest traps SIGFPE 3/3, healthy-clock control returns normally. Unprivileged local DoS on any Bonaire/Kaveri/Kabini/Mullins in that state - one mode set panics at every mode change. DCE8 parts default to the legacy watermark path (DC not default for them), so this is the live path. Guard-reorder fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3878 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 5.1 KB ↓ raw
wm_dce8_div0.c β€” 5.9 KB view raw
build.sh β€” 191 B view raw
run.sh β€” 135 B view raw
build.log β€” 8 B view raw
run.log β€” 282 B view raw
run.2.log β€” 180 B view raw
dmesg.txt β€” 378 B view raw
env.txt β€” 531 B view raw
fix.diff β€” 2.5 KB view raw
verdict.json β€” 5.8 KB view raw

DF-3878 β€” Divide-by-zero family in DCE8 display watermark math (dce_v8_0_latency_watermark)

File: sys/dev/drm/amd/amdgpu/dce_v8_0.c β€” pass 2 (GLM 5.3) Twins: DF-3828 (DCE11, dce_v11_0.c:908-918), DF-3857 (DCE10, dce_v10_0.c:882-892), DF-3665 (evergreen family).

BUILD scp wm_dce8_div0.c dfbsd-maxx:/tmp/ vm.sh run_root 'cc -O2 -Wall -o /tmp/wm_dce8 /tmp/wm_dce8_div0.c'

RUN (unprivileged) vm.sh run_user '/tmp/wm_dce8; echo exit=$?' # affected: zero clocks vm.sh run_user '/tmp/wm_dce8 12000000 5000000 148500' # healthy control

EXPECTED healthy-clock control -> prints watermark value, no trap zero-clock (affected) -> SIGFPE (exit=136), dmesg "exited on signal 8" 3/3 runs trap; kernel path itself cannot run on this guest (0 AMD PCI devices, no /dev/dri β€” env.txt) β€” same hardware gate as DF-3857/3828.

WHAT IT PROVES Exact integer-level replica of dce_v8_0_available_bandwidth() (dce_v8_0.c:642-768) + dce_v8_0_latency_watermark() (:812-860): with DPM disabled and pm.current_mclk/current_sclk == 0 (VBIOS zero defaults, amdgpu_atombios.c:678/680 assigned unguarded at :718-719), available_bandwidth() collapses to 0 and the divides at :817-818 take the #DE trap β€” before the function's only guard (num_heads==0, :827, itself dead via the caller gate at :959).

WHICH WATERMARK CODE RUNS ON DCE8 CONFIG_DRM_AMD_DC is defined (sys/dev/drm/kconfig.h) but CIK takes DC only when forced: BONAIRE/KAVERI/KABINI/MULLINS -> return amdgpu_dc > 0 (amdgpu_device.c:2304-2315) and amdgpu_dc defaults to -1, so the default display path on those parts is this legacy file (dce_v8_1/8_2/8_3/8_5 ip blocks, cik.c:2022-2081) β€” dce_v8_0_latency_watermark is live. CHIP_HAWAII defaults to DC (amdgpu_dc != 0) and runs the DF-3869 dce_calcs family instead.

VERDICT.md
↓ download raw

DF-3878 VERDICT β€” divide-by-zero in dce_v8_0_latency_watermark (DCE8)

Status: not_reproduced (kernel path) / arithmetic primitive reproduced 3/3 β€” source-certain. Same hardware gate as DF-3857 (DCE10) and DF-3828 (DCE11): this guest has no AMD ASIC (pciconf vendor 0x1002 count = 0, no /dev/dri β€” env.txt), so the kernel instruction cannot execute here. What is proven:

Root cause (path:line into this tree)

  • dce_v8_0_latency_watermark() divides by available_bandwidth at dce_v8_0.c:817 ((512*8*1000)/available_bandwidth) and :818 ((128*4*1000)/available_bandwidth), and by wm->disp_clk at :819 (40000000/wm->disp_clk) β€” all BEFORE the function's only guard (if (wm->num_heads == 0) return 0;, :827).
  • The num_heads guard is dead anyway: the sole caller chain dce_v8_0_crtc_do_set_base :1973 β†’ dce_v8_0_bandwidth_update :1082-1100 β†’ dce_v8_0_program_watermarks :948 gates on amdgpu_crtc->base.enabled && num_heads && mode (:959).
  • Zero divisor traced end-to-end: with !adev->pm.dpm_enabled (:967), wm_high.yclk = adev->pm.current_mclk * 10 (:973) and wm_high.sclk = adev->pm.current_sclk * 10 (:974). pm.current_mclk/current_sclk are seeded from VBIOS ulDefaultMemoryClock/ulDefaultEngineClock with NO zero-guard (amdgpu_atombios.c:678/680 β€” neighboring fields at :667/:694/:708 are clamped, these two are not) and are assigned EVEN ON PARSE FAILURE (amdgpu_atombios.c:718-719). One zero term collapses dce_v8_0_available_bandwidth (:760-768, = min of :642/:700/:729 terms) to 0 β†’ #DE β†’ kernel panic. Identical seeding to DF-3828/3857; the atombios parser is asic-agnostic.
  • The wm->disp_clk == 0 vector (mode->clock) is KILLED in this tree: drm_mode_convert_umode β†’ drm_mode_validate_driver (drm_modes.c:1745) β†’ drm_mode_validate_basic (drm_modes.c:1114) rejects clock==0 with MODE_CLOCK_LOW on the SETCRTC path; probed modes pass the same check in drm_probe_helper.c:368-375.
  • Second entry: amdgpu_pm_compute_clocks β†’ amdgpu_display_bandwidth_update (amdgpu_pm.c:2141) β†’ same function on every DPM clock recompute.

Reproduction

  • wm_dce8_div0.c β€” exact replica of the fixed20_12-truncated bandwidth math and the three divides. Affected inputs (zero clocks): SIGFPE, exit=136, 3/3 runs, dmesg pid NNNN (wm_dce8), uid 1001: exited on signal 8 (run.log). Healthy-clock control (1200000/500000 kHz) returns 2297 without trapping.
  • In-kernel effect: the identical div instruction raises #DE in kernel mode β†’ panic during an unprivileged DRM mode set on any Bonaire/Kaveri/ Kabini/Mullins in the zero-clock state β†’ local DoS. No memory-safety impact (pure div0).

Pass-2 negatives checked and killed (dce_v10/v11 template)

  • hpd arrays: AMDGPU_HPD_NONE = 0xff (amdgpu_mode.h:92) so every hpd >= num_hpd guard (:224, :248, :276, :318, :2942, :3133) rejects it; hpd_offsets[6] vs num_hpd=6 OK.
  • audio: pin[AMDGPU_MAX_AFMT_BLOCKS=9] (amdgpu_mode.h:261) vs num_pins<=7 (:1374-1383) OK; afmt[9] vs num_dig<=7 (KAVERI, cik.c:2023) OK; dig_offsets[7] covers it (:71-79).
  • crtc: crtc_offsets[6] vs num_crtc<=6 (:385-405); vblank_get_counter (:144-150) lacks a crtc < 0 check but every caller passes validated unsigned pipe (amdgpu_kms.c:1129-1156 via amdgpu_get_vblank_counter_kms which guards pipe >= num_crtc, amdgpu_kms.c:1109-1113); get_scanoutpos (:198-208) checks both bounds itself.
  • ISR indices: dce_v8_0_crtc_irq (:3013-3018) indexes interrupt_status_offsets[6] with src_id-1 β€” bounded by the registration contract (sw_init :2645-2649 registers only 1..num_crtc; dispatch looks up client.sources[src_id], amdgpu_irq.c:426-431). Same for hpd_irq (:3126-3139, entry->src_data[0] checked at :3133 BEFORE use).
  • pageflip_irq crtcs[crtc_id] read-before-check (:3082-3085): latent ordering defect (Linux moved the check first upstream), but crtc_id is derived from src_id∈{8..18} (registered :2651-2655) β†’ in [0,5]; not attacker-reachable β€” hardening note only, consistent with the dce10 pass-2 treatment.
  • dfixed_const(wm->yclk) u32 overflow for large clocks (drm_fixed.h:35) degrades watermark math on ALL dce versions β€” correctness, not memory safety; shared drm_fixed.h, out of this file's scope.
  • LUT loop (:2030-2035) reads 256 entries; gamma_size forced 256 (:2584 drm_mode_crtc_set_gamma_size(&base,256)) β†’ exact.
  • cursor_set2 (:2248-2331): GEM handle via drm_gem_object_lookup with file_priv; width/height upper-bounded (:2268-2272); width==0 causes only a harmless (u32)-1 register field. No priv issue.

Fix

Guard the divides (mirror of the num_heads guard) and zero-guard the VBIOS default-clock seeding β€” see fix.diff (git-apply-clean against sys/).

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 (moving the num_heads guard before them, adding disp_clk and available_bandwidth checks) plus the VBIOS zero-clock seeding, and is git-apply-clean against the read-only sys/ tree; a patched-kernel build cannot be exercised on this guest (no AMD ASIC, amdgpu never attaches), so hardware fix validation is not_testable here

fix.diff; VERDICT.md; env.txt (hardware gate)
↓ fix.diffper-fix-DF-3878

Confirmed kernel references

Detail

Exploit chain

unprivileged user with /dev/dri/cardX access -> drm_mode_setcrtc -> dce_v8_0_crtc_do_set_base (:1973) -> dce_v8_0_bandwidth_update (:1082) -> dce_v8_0_program_watermarks (:948, gate at :959 passes for any enabled crtc with num_heads>=1) -> dce_v8_0_latency_watermark -> (51281000)/available_bandwidth with available_bandwidth==0 -> #DE -> kernel panic. Precondition: pm.current_sclk or current_mclk == 0 (VBIOS zero default clocks or failed firmware-info parse with DPM off). Memory-safety impact: none (pure div0 DoS).

Evidence (decisive lines)

run.log (healthy control returns 2297, zero-clock input traps SIGFPE exit=136); run.2.log (3/3 stress traps); dmesg.txt ('pid NNNN (wm_dce8), uid 1001: exited on signal 8' 6x); env.txt (no AMD ASIC on guest); fix.diff (git-apply-clean); VERDICT.md (full chain + the dce10/11-template negative notes: hpd 0xff guards, audio pin[9]/num_pins<=7, afmt[9]/num_dig<=7, crtc_offsets[6], ISR src-id registration contract, pageflip read-before-check hardening note, LUT 256==gamma_size, cursor bounds)

PoC changes

Authored from scratch, modeled on the DF-3857 pack (no seed provided by orchestrator). Inputs via argv to defeat constant folding; dce_v8_0 line numbers and CIK dram-channel/bandwidth constants (dce_v8_0.c:642-768) adapted from this file; DC-vs-legacy boundary for DCE8 parts verified in-tree.

Verified recommended fix

Bail out of dce_v8_0_latency_watermark when num_heads/disp_clk/available_bandwidth is 0 (mirroring and moving the existing guard before the divides), and zero-guard ulDefaultEngineClock/ulDefaultMemoryClock seeding in amdgpu_atombios_get_clock_info

Verdict

Source-certain divide-by-zero in the DCE8 display watermark math, the direct DF-3828 (DCE11) / DF-3857 (DCE10) twin: dce_v8_0_latency_watermark() divides by available_bandwidth at dce_v8_0.c:817-818 and by wm->disp_clk at :819, all before the function's only guard (num_heads==0, :827, itself dead via the caller gate at :959) and with no divisor validation anywhere on the caller chain (mode set -> dce_v8_0_crtc_do_set_base :1973 -> dce_v8_0_bandwidth_update :1082 -> dce_v8_0_program_watermarks :948, plus the DPM recompute path amdgpu_pm.c:2141). Zero inputs traced end-to-end: with DPM disabled, wm.yclk/wm.sclk come from adev->pm.current_mclk/current_sclk (:973-974), seeded from VBIOS ulDefaultEngineClock/ulDefaultMemoryClock with no zero-guard (amdgpu_atombios.c:678/680) and assigned even on parse failure (:718-719) - identical to the DF-3828/3857 seeding; one zero term collapses dce_v8_0_available_bandwidth (:760-768) to 0 and the divide takes #DE -> kernel panic. The mode->clock==0 secondary vector is killed in this tree by drm_mode_validate_basic (drm_modes.c:1114) via drm_mode_convert_umode -> drm_mode_validate_driver (:1745). Which watermark function runs on DCE8 parts: verified that BONAIRE/KAVERI/KABINI/MULLINS take the legacy path by default (amdgpu_device.c:2304-2315 'return amdgpu_dc > 0' with amdgpu_dc defaulting to -1), so dce_v8_0_latency_watermark IS live on those parts; CHIP_HAWAII defaults to DC and runs the DF-3869 dce_calcs family instead. Arithmetic primitive proven on the guest: exact replica of the bandwidth/watermark math traps SIGFPE (exit=136, signal 8 = #DE, dmesg-confirmed) 3/3 stress runs while the healthy-clock control returns normally (run.log, run.2.log, dmesg.txt). Kernel-path reproduction impossible on this guest - 0 AMD PCI devices, no /dev/dri (env.txt), amdgpu never binds - hence not_reproduced for the kernel reach; impact ceiling on Bonaire/Kaveri/Kabini/Mullins hardware in the zero-clock state: local DoS via panic by an unprivileged DRM client mode set.