⬢ DragonFlyBSD Kernel Audit
← triage · dashboard
DF-3869

Zero-divisor (#DE kernel panic) in calculate_bandwidth: exact-zero divisor at dce_calcs.c:839 from UABI-legal degenerate modes, plus u32 division-by-zero at :1945

Summary

bw_div's ASSERT(denominator!=0) compiles to WARN_ON in release and bw_frc_to_fixed then executes div64_u64_rem(n,0) -> x86-64 divq #0 -> panic. An unprivileged user-mode DRM mode blob (1x16, htotal=3, clock=25MHz) plus plane (src 128x4 -> dst 2x3) passes drm_mode_validate_basic, satisfies the viewport>=16 gate, and makes the :839 divisor EXACTLY 0 (3 + (64-256)/64 = 0, all integers exact in 24.24 fixed point). Secondary sink: min_stutter_dmif_buffer_size (u32) truncates from fixed-point in (0,1) to 0 and :1945 divides by it. VERIFIED: kernel-verbatim fixed-point PoC on the guest - 'DIVISOR == 0 CONFIRMED' + two SIGFPE children. Local unprivileged DoS (kernel panic) on any DCE110/DCE112 DC part (Carrizo/Stoney/Polaris/Vega10 with amdgpu DC enabled): one atomic modeset with a crafted mode blob panics during dc_validate_global_state. Guard + hardened bw_frc_to_fixed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3869 · 10 files
FileTypeDescriptionSize
README.md — 2.6 KB ↓ raw
VERDICT.md — 4.2 KB ↓ raw
dce839_zero_div.c — 11.5 KB view raw
build.sh — 98 B view raw
run.sh — 98 B view raw
run.log — 1.1 KB view raw
run.1.log — 1.1 KB view raw
fix.diff — 2.7 KB view raw
manifest.json — 1.1 KB view raw
verdict.json — 2.3 KB view raw
README.md
↓ download raw

DF-3869 — Zero-divisor (#DE panic) family in dce_calcs calculate_bandwidth()

File: sys/dev/drm/amd/display/dc/calcs/dce_calcs.c Sinks: line 839 (horizontal_blank_and_chunk_granularity_factor outer bw_div) and line 1945 (num_stutter_bursts u32 /) Family: DF-3665 (evergreen watermark div0) / DF-3828 / DF-3857 — same class, DCE display-core bandwidth calculator. Severity: Medium (local DoS, kernel panic) — hardware-gated to DCE110/DCE112 AMD DC parts (Carrizo/Stoney/Polaris/Vega10 with amdgpu.dc=1).

Build & run (works on any UNIX; verified on the audit guest)

cc -O2 -o dce839_zero_div dce839_zero_div.c
./dce839_zero_div

Expected output

The harness replays, with kernel-verbatim fixed-point code (copied from bw_fixed.c, bw_fixed.h, fixpt31_32.c, fixed31_32.h, dc_resource.c), the exact attacker input chain for an unprivileged-supplied DRM mode blob (1x16, htotal=3, clock=25 MHz) and plane state (FB 128x4, src 128x4, dst 2x3):

  1. the mode passes drm_mode_validate_basic() (drm_modes.c:1103) — nothing in the DRM core forbids htotal=3/hdisplay=1;
  2. resource_build_scaling_params succeeds (viewport 64x21 ≥ 16), so bw_calcs() (dce_calcs.c:2985) runs populate_initial_data + calculate_bandwidth;
  3. hsr = 64 (exact in 24.24), vsr ≤ 4/3 → the branch guards at dce_calcs.c:804/820 hold and line 839 executes;
  4. inner sum h_total + (src_width−256)/hsr = 3 + (64−256)/64 = exactly 0 → the outer bw_div(h_total, 0) divisor is 0;
  5. in-kernel this reaches bw_frc_to_fixed(n, 0) (bw_fixed.c:57) whose ASSERT(denominator != 0) is compiled to WARN_ON in release builds (os_types.h:76), then div64_u64_rem(n, 0, …) (math64.h:76, plain C %) → x86-64 divq with divisor 0 → #DE → trap_fatal → kernel panic.

The harness children perform the same divisions in userland and die with SIGFPE (the #DE analog). Guest stays up: this is a userland arithmetic proof; the kernel path itself needs DCE hardware the guest does not have.

Secondary sink (mechanism-demonstrated, not end-to-end proven)

dce_calcs.c:1939 stores bw_fixed_to_int(stutter_dmif_buffer_size[i]) into uint32_t min_stutter_dmif_buffer_size (dce_calcs.h:359); any value in (0,1) truncates to 0 and line 1945 total_bytes_requested / min_stutter_dmif_buffer_size (u32/u32 → divl #0 → #DE panic). Same degenerate-mode family; the full formula chain was not replayed end-to-end, so this sink is likely rather than certain.

Fixes

fix.diff in this directory guards both sinks (and hardens bw_frc_to_fixed).

VERDICT.md
↓ download raw

DF-3869 VERDICT

Status: inconclusive (hardware-gated) — the guest has no AMD GPU (virtio-only QEMU), so the in-kernel path cannot be executed here. The arithmetic that produces the zero divisor was reproduced exactly, on the guest, with kernel-verbatim fixed-point code (run.log).

What was proven (run.log, guest uname in file)

  • bw_calcs() is reachable from unprivileged mode-set on DCE110/112 DC: DRM_IOCTL_MODE_ATOMIC with a user mode blob → drm_atomic_set_mode_prop_for_crtc → drm_mode_convert_umode → drm_mode_validate_basic (drm_modes.c:1103-1120: only clock≠0 and hdisplay≤hsync_start≤hsync_end≤htotal) → amdgpu_dm fill_stream_properties_from_drm_display_mode (amdgpu_dm.c:2678,2689 — h_total = crtc_htotal, pix_clk_khz = crtc_clock, no zero/degeneracy checks) → dc_validate_global_state (dc_resource.c:2079) → resource_build_scaling_params_for_context (dc_resource.c:2133) → dce110_validate_bandwidth/dce112 (dce110_resource.c:857-870) → bw_calcs (dce_calcs.c:2985).
  • Constructed input: mode 1x16 htotal=3 clock=25 MHz; plane src 128x4 → dst 2x3. All DRM-core checks pass (harness step [1]).
  • viewport 64x21 ≥ 16 → resource_build_scaling_params returns true (dc_resource.c:1125) so bw_calcs is reached (harness step [2]).
  • hsr = 64 exactly (24.24 word 1073741824), vsr = 22369600 ≤ frc(4/3) (harness step [3]); branch guards at dce_calcs.c:804 (vsr>1 → prefetch=0) and :817-834 (lb_lines_mid = 4/3 ∉ {2,4}) hold → the else at :838 runs.
  • dce_calcs.c:839 divisor = bw_div(h_total + (src_width−256)/hsr, 2) = bw_div(3<<24 + (−3<<24), 2) = 0 EXACTLY (harness steps [5][6]).
  • In-kernel consequence: bw_div(x, 0) → bw_frc_to_fixed(x, 0) (bw_fixed.h:121-124, bw_fixed.c:57): ASSERT(denominator != 0) is only WARN_ON in release (os_types.h:72-77), then div64_u64_rem(n, 0, &rem) (math64.h:76-80, plain C %) → CPU #DE in kernel → trap_fatal → panic. Userland analogs both fired SIGFPE (harness steps [7][9]).

Why the other big divisors are NOT zero (negative results with weight)

  • pixel_rate/h_total can never be 0: drm core drm_mode_validate_basic rejects clock==0 (MODE_CLOCK_LOW) and hdisplay==0 with htotal≥hsync_end (drm_modes.c:1114-1120); pix_clk_khz*=2 for frame packing keeps ≠0.
  • hsr/vsr can never be 0 on the reachable path: amdgpu_dm fill_rects_from_plane_state rejects zero src/dst extents (amdgpu_dm.c:2439-2459); calculate_scaling_ratios truncates ratios to 19 fractional bits (dc_resource.c:747-753) which could zero a ratio < 2^-19, but the viewport ≥ 16 gate (dc_resource.c:1125) with clip ≤ 16384 bounds ratio ≥ 16/16384 = 2^-10 > 2^-19. Killed.
  • OOB in populate_initial_data: max write index = num_displays+4 ≤ 9 (≤6 streams) < maximum_number_of_surfaces = 12 (dce_calcs.h:233); watermark[9] reads in-bounds. Killed.
  • No VBIOS mem_latency/num_states walk exists in this file (that is dcn_calcs.c); all vbios/dceip data here are compile-time constants except asic_id.vram_width (→ DF-3870).
  • No recursion; single kzalloc'd bw_calcs_data; deterministic.

Sink 1945 (secondary, likely not certain)

min_stutter_dmif_buffer_size (u32, dce_calcs.h:359) is set at :1939 via truncating bw_fixed_to_int(); a stutter_dmif_buffer_size in (0,1) yields 0 and :1945 divides u32/u32 → divl #0 → #DE. Degenerate timings shrink the product below 1; full-chain emulation not performed (the formula depends on the entire watermark model), so recorded as likely, same root cause family.

Bottom line

Source-certain: an unprivileged user with a DRM master/_modeset on a DCE display can drive calculate_bandwidth() into an exact-integer zero divisor at dce_calcs.c:839 → kernel #DE panic. Hardware-gated (no AMD GPU on guest), hence inconclusive runtime verdict; arithmetic proven exactly on the guest.

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 authored against read-only sys/; cannot build DCE kernel path on guest (no AMD HW)

fix.diff
↓ fix.diffper-fix-DF-3869

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log steps [5][6] divisor==0; [7]/[9] SIGFPE children; VERDICT.md reachability chain and killed negatives

PoC changes

harness written fresh (no seed): verbatim copies of bw_frc_to_fixed/dc_fixpt_truncate/fixed31_32_to_bw_fixed + the line-839 expression; fixed DCE of the faulting divisions by storing into volatile

Verified recommended fix

Guard zero denominators at dce_calcs.c:839 and :1945; harden bw_frc_to_fixed against divisor 0 (see fix.diff)

Verdict

Hardware-gated (guest has no AMD GPU): in-kernel path not executable here, but the zero divisor at dce_calcs.c:839 was proven EXACTLY on the guest with kernel-verbatim fixed-point arithmetic (run.log: divisor==0, both #DE analogs SIGFPE). An unprivileged DRM mode blob (1x16 htotal=3) + plane (src 128x4 -> dst 2x3) passes drm_mode_validate_basic, reaches bw_calcs on DCE110/112, and makes the line-839 outer bw_div divide by exactly 0 -> div64_u64_rem(n,0) -> kernel #DE panic. Secondary sink 1945 (u32 truncation -> 0 -> divl) is likely, same family.