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

Divide-by-zero in dce_v10_0_latency_watermark: divides by available_bandwidth/disp_clk before any guard (DF-3828 DCE11 twin)

Summary

dce_v10_0_latency_watermark() computes the three divides before the function's only guard (num_heads==0, itself dead via the caller gate) and with no divisor validation on any caller path. When the memory/engine clocks read as zero, available_bandwidth() collapses to 0 and a mode-set takes an integer #DE -> panic. Clocks are seeded from VBIOS ulDefaultEngineClock/ulDefaultMemoryClock with no zero-guard and assigned even on parse failure - the exact seeding DF-3828 proved for DCE11 (asic-agnostic parser, applies to Tonga/Fiji). Arithmetic primitive VERIFIED on guest: exact replica traps SIGFPE 3/3 runs, healthy-clock control returns normally. Local unprivileged DoS (kernel panic) on any Tonga/Fiji machine whose default clocks read as 0: an unprivileged DRM client issues a mode set and the kernel takes #DE.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3857 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 5.3 KB ↓ raw
wm_div0.c β€” 5.1 KB view raw
build.sh β€” 115 B view raw
run.sh β€” 124 B view raw
build.log β€” 184 B view raw
run.log β€” 586 B view raw
run.2.log β€” 608 B view raw
env.txt β€” 467 B view raw
fix.diff β€” 1.6 KB view raw
verdict.json β€” 4.7 KB view raw
manifest.json β€” 1.3 KB view raw

DF-3857 β€” Divide-by-zero in dce_v10_0_latency_watermark (DCE10, DF-3828 twin)

What

sys/dev/drm/amd/amdgpu/dce_v10_0.c:882-884 β€” dce_v10_0_latency_watermark() computes

u32 worst_chunk_return_time      = (512 * 8 * 1000) / available_bandwidth;  /* :882 */
u32 cursor_line_pair_return_time = (128 * 4 * 1000) / available_bandwidth;  /* :883 */
u32 dc_latency                   = 40000000 / wm->disp_clk;                 /* :884 */

all three before the function's only guard (if (wm->num_heads == 0) return 0;, :892). When available_bandwidth collapses to 0 the first divide takes an integer #DE in kernel context β†’ panic. The num_heads==0 guard is additionally dead through this file's only caller: dce_v10_0_program_watermarks() gates the call on amdgpu_crtc->base.enabled && num_heads && mode (:1024).

Zero-clock source: with DPM disabled, wm.yclk/wm.sclk = adev->pm.current_mclk/current_sclk * 10 (:1038-1039, :1077-1078); pm.current_* are seeded from the VBIOS ulDefaultEngineClock/ ulDefaultMemoryClock with no zero-guard (amdgpu_atombios.c:677-680) and are assigned even when the firmware-info parse fails (:718-719) β€” the exact seeding DF-3828 proved for DCE11; the parser is asic-agnostic (Tonga/Fiji). With yclk == 0 (or sclk == 0) the min() in dce_v10_0_available_bandwidth() (:825-833) is 0.

The mode->clock == 0 secondary vector is already blocked at the DRM layer in this tree (drm_mode_validate_basic(), sys/dev/drm/drm_modes.c:1114 returns MODE_CLOCK_LOW), so wm->disp_clk != 0 for any mode that reaches program_watermarks β€” the live vector is the zero memory/engine clock.

Reproduce (arithmetic-level; kernel reach is hardware-gated)

build.sh   # cc -O2 -Wall -o /tmp/wm_div0 /tmp/wm_div0.c
run.sh     # /tmp/wm_div0; echo exit=$?

Expected (observed on the audit guest, run.log):

healthy clocks: watermark math returns 2297 (no trap)
affected clocks (current_mclk=0, current_sclk=0): entering latency_watermark()...
exit=136                      # 128+8 -> SIGFPE = #DE, the kernel panic class

Control (/tmp/wm_div0 6000000 3000000 148500) returns normally. run.2.log: dmesg shows pid NNNN (wm_div0), uid 0: exited on signal 8 3/3 runs.

Impact

Local DoS (kernel panic) on any Tonga/Fiji machine whose default clocks read as 0 (crafted/blank VBIOS or failed firmware-info parse with DPM off): an unprivileged DRM client issues a mode set β†’ dce_v10_0_crtc_do_set_base β†’ dce_v10_0_bandwidth_update (:2064) β†’ program_watermarks β†’ latency_watermark β†’ #DE β†’ panic.

Kernel reach on this guest

Not possible: the audit guest has 0 AMD PCI devices (pciconf shows no vendor=0x1002), no /dev/dri (env.txt) β€” amdgpu never binds. Hence not_reproduced for the kernel path; the arithmetic primitive and the full call chain are proven at source level and the trap class is demonstrated.

VERDICT.md
↓ download raw

DF-3857 VERDICT β€” dce_v10_0_latency_watermark divide-by-zero (DF-3828 twin)

Status: not_reproduced (kernel reach hardware-gated) β€” arithmetic primitive proven on guest, source chain certain.

Root cause (path:line, all in sys/dev/drm/amd/amdgpu/ unless noted)

dce_v10_0_latency_watermark() divides by available_bandwidth at dce_v10_0.c:882-883 and by wm->disp_clk at :884, before the only guard if (wm->num_heads == 0) return 0; at :892, with no divisor validation on any caller path:

  1. dce_v10_0_crtc_do_set_base() :2064 β†’ dce_v10_0_bandwidth_update() :1145-1163 β†’ dce_v10_0_program_watermarks() :1013-1135 β†’ dce_v10_0_latency_watermark() :877. Triggered by any mode set (unprivileged DRM master via DRM_IOCTL_MODE_SETCRTC / page-flip base update) and by amdgpu_pm_compute_clocks DPMS transitions (:2510).
  2. dce_v10_0_program_watermarks :1032-1040 / :1071-1079: with adev->pm.dpm_enabled == false, wm.yclk = pm.current_mclk * 10, wm.sclk = pm.current_sclk * 10.
  3. amdgpu_atombios_get_clock_info() (amdgpu_atombios.c:677-680) copies VBIOS ulDefaultEngineClock/ulDefaultMemoryClock into adev->clock.default_sclk/default_mclk with no zero-guard (every neighbouring field does get one β€” see DF-3828's audit), and :718-719 assigns them to pm.current_sclk/current_mclk even when the parse fails.
  4. dce_v10_0_available_bandwidth() :825-833 = min(dram_bw, data_ret, dmif_req); dram_bw = trunc(ch*4 * yclk/1000 * 0.7) (:707-725) is 0 when yclk==0; data_ret is 0 when sclk==0 (:765-783). One zero term zeroes the min.
  5. :882 (512*8*1000) / 0 β†’ integer #DE β†’ kernel panic.

Guard placement: the num_heads==0 early-return at :892 is dead through this file's only caller (gate at :1024 includes num_heads), so even a perfect num_heads guard could not have prevented the zero-bandwidth divide β€” the missing check is on the divisors, exactly the DF-3828/DF-3665 family defect.

Secondary vectors examined and killed: - mode->clock == 0 (div_u64 divides at :1025-1027, wm->disp_clk at :884): blocked pre-driver by drm_mode_validate_basic() (sys/dev/drm/drm_modes.c:1114-1115, MODE_CLOCK_LOW) in the drm_mode_setcrtc/drm_mode_convert_umode path (drm_crtc.c:655, drm_modes.c:1750). No user-supplied 0-clock mode reaches DCE10. - wm->src_width == 0 divide in dce_v10_0_check_latency_hiding() :978: same validation rejects hdisplay==0 (drm_modes.c:1117). - HPD/audio/crtc array bounds (dce_v11 template): hpd_offsets[6] uses are guarded (:279, :303, :331, :383, :3010, :3162); interrupt_status_offsets[6] / crtc_offsets[7] uses (:3206-3207, :3113-3114) are bounded by the amdgpu_irq dispatch contract (only client/src_id pairs registered via amdgpu_irq_add_id reach .process; amdgpu_irq.c:356, :414-432); num_pins=7/num_dig=7 fit pin[AMDGPU_MAX_AFMT_BLOCKS=9]/afmt[9] (amdgpu_mode.h:261,329); crtc_status[6] (:403) fits fixed num_crtc=6 (:452-465). The read-before-check at dce_v10_0_pageflip_irq :3114 vs :3116 is latent-only (registered pageflip src_ids 8..18 β†’ crtc_id 0..5). - dce_v10_0_cursor_set2 width==0 β†’ CUR_SIZE gets 0xFFFFFFFF (:2330): register cosmetics only, no memory-safety impact.

What ran on the guest

wm_div0.c replicates the exact fixed-point truncation sequence of dce_v10_0_dram_bandwidth/data_return_bandwidth/dmif_request_bandwidth/ available_bandwidth/latency_watermark with argv-controlled clocks (so the compiler cannot fold the divide).

Results (build cc 8.3 [DragonFly], -O2 -Wall): - Zero-clock input: SIGFPE, exit=136 β€” 3/3 runs (run.log Γ—2, run.2.log), dmesg: pid NNNN (wm_div0), uid 0: exited on signal 8. - Healthy-clock control (6000000/3000000 kHz): returns 2297, no trap.

Signal 8 in userspace is the same #DE fault the kernel takes on the same instruction; in-kernel this is an unrecoverable panic (kernel #DE has no fixup here).

Why not reproduced

The audit guest is QEMU/KVM with virtio + bochs VGA: pciconf -lv lists 0 devices with vendor=0x1002 and there is no /dev/dri (env.txt), so amdgpu.ko never attaches and the DCE10 kernel path cannot execute. Kernel reach requires a Tonga/Fiji dGPU in the affected state (zero default clocks from VBIOS with DPM off). Impact ceiling on equipped hardware: local DoS via panic, triggered by an unprivileged DRM client's mode set.

Fix validation

fix.diff guards the three divides (bail to the num_heads==0-style 0 return), adds the available_bandwidth==0 check to the sibling callers, and adds zero-guards for default_sclk/default_mclk in amdgpu_atombios_get_clock_info (upstream-root fix mirroring DF-3828). A patched-kernel build cannot be exercised on this guest (no AMD ASIC β‡’ amdgpu never binds β‡’ no observable behavior difference), so fix validation is not_testable here; the diff is git-apply-clean against the read-only sys/ tree.

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 both divides 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, no observable kernel behavior difference), so hardware fix validation is not_testable here

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

Confirmed kernel references

Detail

Exploit chain

unprivileged user with /dev/dri/card0 access -> drm_mode_setcrtc -> dce_v10_0_crtc_do_set_base (dce_v10_0.c:1822) -> dce_v10_0_bandwidth_update (:2064/:1145) -> dce_v10_0_program_watermarks -> dce_v10_0_latency_watermark -> divide by 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 (SIGFPE exit=136 twice + healthy control); run.2.log (3rd trap + dmesg 'exited on signal 8' 3/3 + guest /tmp cleanup); VERDICT.md (full chain incl. killed mode->clock/src_width vectors and the dce_v11-template bounds negatives); env.txt (no AMD ASIC on guest); fix.diff (git-apply-clean)

PoC changes

Authored from scratch, modeled on the DF-3828 pack (no seed provided by orchestrator). Inputs via argv to defeat constant folding; dce_v10_0 line numbers and dram-channel math (cik_get_number_of_dram_channels family) adapted from this file.

Verified recommended fix

Bail out of dce_v10_0_latency_watermark when available_bandwidth or wm->disp_clk is 0 (mirroring the num_heads==0 return), and zero-guard ulDefaultEngineClock/ulDefaultMemoryClock seeding in amdgpu_atombios_get_clock_info

Verdict

Source-certain divide-by-zero in the DCE10 display watermark math, the direct DF-3828 (DCE11) twin: dce_v10_0_latency_watermark() divides by available_bandwidth at dce_v10_0.c:882-883 and by wm->disp_clk at :884, all before the function's only guard (num_heads==0, :892, itself dead via the caller gate at :1024) and with no divisor validation anywhere on the caller chain (mode set -> dce_v10_0_crtc_do_set_base :2064 -> dce_v10_0_bandwidth_update :1145 -> program_watermarks :1013). Zero inputs traced end-to-end: with DPM disabled, wm.yclk/wm.sclk come from adev->pm.current_mclk/current_sclk (:1038-1039, :1077-1078), seeded from VBIOS ulDefaultEngineClock/ulDefaultMemoryClock with no zero-guard (amdgpu_atombios.c:677-680) and assigned even on parse failure (:718-719) - identical to the DF-3828 seeding; one zero term collapses dce_v10_0_available_bandwidth (:825-833) 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, MODE_CLOCK_LOW). Arithmetic primitive proven on the guest: exact replica of the bandwidth/watermark math traps SIGFPE (exit=136, signal 8 = #DE, dmesg-confirmed) 3/3 runs while the healthy-clock control returns normally. Kernel-path reproduction impossible on this guest - 0 AMD PCI devices (vendor=0x1002 count 0), no /dev/dri (env.txt), amdgpu never binds - hence not_reproduced for the kernel reach; impact ceiling on Tonga/Fiji hardware in the affected state: local DoS via panic by an unprivileged DRM client mode set.