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

Divide-by-zero (#DE kernel panic) in dpp1_dscl_calc_lb_num_partitions: UABI-legal off-CRTC plane yields recout.width==0 (line_size==0 β†’ 816/0 at :439, also :440/:441) β€” DCN10 commit-path twin of DF-4373

Summary

The LB partition math divides lb_memory_size by width-derived divisors with no zero check; a plane positioned flush off the right edge clamps recout.width to exactly 0 while viewport.width stays >=16 in SOURCE pixels - the only degeneracy guard tests viewport, validation is format-only, and the DCN10 taps check computes min(viewport,recout) but never reads it (the dead variable where the DCE sibling crashes in DF-4373). One UABI-legal atomic commit from an unpriv session user on a Raven GPU panics during update_dchubp_dpp. Second zero source: underscan insets. Trigger replays the exact producer arithmetic and dies SIGFPE 3/3; no AMD GPU on guest so kernel path unreachable (not_reproduced). Guard-the-divisors fix (git-apply-verified).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4696 Β· 12 files
FileTypeDescriptionSize
trigger_df4696.c β€” 8.1 KB view raw
build.sh β€” 144 B view raw
run.sh β€” 116 B view raw
build.log β€” 117 B view raw
build_first_attempt.log β€” 972 B view raw
run.log β€” 882 B view raw
run.2.log β€” 1.7 KB view raw
env.txt β€” 613 B view raw
fix.diff β€” 1.3 KB view raw
VERDICT.md β€” 6.6 KB ↓ raw
manifest.json β€” 1.8 KB view raw
verdict.json β€” 7.6 KB view raw
VERDICT.md
↓ download raw

DF-4696 VERDICT β€” divide-by-zero at dcn10_dpp_dscl.c:439-441

Status: not_reproduced on the audit guest β€” target hardware absent (no AMD GPU under QEMU). The exploit chain is proven statically link by link, and the exact arithmetic (producer formulas + faulting division) was executed on the guest as a userland twin: the attack geometry makes the divisor exactly 0 and the division faults (SIGFPE, exit 136), the control geometry does not.

Reproduction attempt (honest)

  • Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC), QEMU std VGA only; /dev/dri does not exist; no vendor-0x1002 PCI device (env.txt). dcn10_dpp_dscl.c compiles into the amdgpu DC module, which cannot attach β€” the kernel path is unreachable at runtime on this guest. Same situation as DF-4373 (the DCE sibling of this bug family).
  • Probe + arithmetic twin built with base cc 8.3 and run 3Γ—: control case completes (num_part_y clamped to 64); attack case dies with SIGFPE every run (run.log, run.2.log). Intermediate values match the static chain exactly: viewport.width=640 recout.x=640 recout.width=0 line_size=0 memory_line_size_y=0 β†’ 816/0.
  • PoC-code note (poc_changes): the first harness version let gcc8 exploit the C UB of 816/0 with a constant numerator and silently replaced the division (no fault β€” build_first_attempt.log / run_raw log). The kernel's divisor is a runtime value, which compiles to a real idiv and faults (cf. DF-4582, an integer-divide panic reproduced on this guest as trap 18). The harness now hides the divisor behind an asm barrier, forcing the real divide instruction β€” SIGFPE is the faithful twin of the kernel #DE.
  1. UABI reachability. Modesetting needs DRM master; the session user of any seat/desktop holds it (no root). The faulting path runs during the atomic commit (program-front-end), reached from amdgpu_dm_atomic_check-validated, dc_validate-accepted state.

  2. Plane rects are never clipped to the CRTC. fill_rects_from_plane_state (amdgpu_dm.c:2210-2240) copies raw crtc_x/y/w/h, rejects only zero dimensions, sets clip_rect = dst_rect. dm_plane_atomic_check (amdgpu_dm.c:3585-3603) only calls fill_rects + dc_validate_plane ("validates pixel format only", dc_resource.c:2891-2900); dcn10_validate_plane (dcn10_resource.c:1131-1138) checks video src width only. Nothing anywhere rejects an off-CRTC plane position.

  3. The only degeneracy guard tests the wrong rectangle. resource_build_scaling_params (dc_resource.c:1104) returns failure only when viewport.height < 16 || viewport.width < 16 (dc_resource.c:1125-1127) β€” and viewport.width is in SOURCE pixels (dc_resource.c:543: clip.width * src.width / dst.width). A plane with a 640-wide source shown at CRTC_W=1 keeps viewport.width = 640: the guard passes. recout is computed after the guard (dc_resource.c:1129) and is never checked.

  4. recout clamps to exactly zero. calculate_recout (dc_resource.c:615-706): with stream dst = [0,640) and plane CRTC_X=640, CRTC_W=1: recout.x = 0 + (640-0)*640/640 = 640; recout.width = 1*640/640 = 1; clamp at :639-643: recout.width = (0+640) - 640 = 0. Exactly zero, signed ints, no underflow. (Second zero source, no positioning trick needed: underscan hborder insets stream->dst while src stays full mode β€” recout.width = clip.w * dst.w / src.w = 0 for a small plane.)

  5. DCN10 validation has no guard here. dpp_get_optimal_number_of_taps (dcn10_dpp.c:141-210) computes pixel_width = min(viewport.w, recout.w) at :148-151 β€” and never reads it; it returns true unconditionally (non-FP16). This dead variable is exactly where the DCE sibling (DF-4373) crashes, and its deadness here is why DCN10 validation accepts the config. validate_bandwidth (dcn_calcs.c) consumes viewport.width (β‰₯16) and copies recout.width into bw variables but never divides by it on this geometry.

  6. Commit path reaches the sink unconditionally. Full update: dcn10_update_dchubp_dpp (dcn10_hw_sequencer.c:2101-2104, flags full_update/scaling/position) β†’ update_scaler (:2029-2041, sets lb depth 30BPP) β†’ dpp1_dscl_set_scaler_manual_scale (dscl.c:647): memcmp first-program passes; format XRGB8888 (β‰ FP16) so no DSCL_MODE_DSCL_BYPASS early return (:684-685); ratios β‰  1 β†’ not 444_BYPASS either; dpp1_dscl_find_lb_memory_config (:463, dc->debug.use_max_lb false β€” dc is kzalloc'd, dc.c:729-732, no later assignment) β†’ first call dscl_calc_lb_num_partitions(scl_data, LB_MEMORY_CONFIG_1, ...) (:476) β†’ 816 / 0 at :439 β†’ #DE β†’ kernel panic.

  7. Sibling precedent. DF-4373 (dce_transform.c) is the same producer chain crashing in the DCE validation path; DF-4582 proved this guest does panic on integer divide-by-zero in kernel code (trap 18) when the path is reachable.

Impact

Local DoS (kernel panic) by an unprivileged session user (DRM master) on DragonFly machines with a DCN1.0 (Raven) AMD GPU driven by the in-tree amdgpu DC. Not memory corruption; no privilege escalation avenue β€” integer #DE is a trap, not a controllable primitive.

Fix validation

Not possible on this guest: no AMD GPU, so the amdgpu module cannot attach and neither baseline nor patched kernel can exercise the path (fix_status = not_testable). fix.diff guards the three divisors (<= 0, which also covers the lb_bpc == -1 unsupported-depth path) and returns zero partitions; dpp1_dscl_is_lb_conf_valid then fails every configuration (vtaps >= 1 vs num_partitions 0), so the caller falls through to the default LB config with the existing ASSERT warning instead of dividing. No other caller of dscl_calc_lb_num_partitions exists (grep: dcn10_dpp.c:535 registration, internal calls only).

Artifacts

  • trigger_df4696.c β€” device probe + arithmetic twin (control/attack)
  • build.sh / run.sh β€” exact commands
  • build.log (final), build_first_attempt.log (UB-pitfall record), run.log, run.2.log (3 runs, SIGFPE every time)
  • env.txt β€” guest uname, compiler, GPU absence evidence
  • fix.diff β€” divisor guard, authored against the read-only tree
  • manifest.json / verdict.json

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 cannot be exercised on the audit guest: no AMD GPU means the amdgpu DC module (which contains dcn10_dpp_dscl.c) never attaches, so neither baseline nor patched kernel can run the path. fix.diff is authored against the read-only tree; it guards all three divisors (<=0) before the divisions at :439-441 and returns zero partitions, which makes dpp1_dscl_is_lb_conf_valid fail every LB config (vtaps>=1 vs 0 partitions) so dpp1_dscl_find_lb_memory_config falls through to the default config with the existing ASSERT warning instead of faulting. The userland twin of the guarded arithmetic returns 0 partitions for the attack geometry with no fault (code-reviewed by hand; not executed as a kernel).

fix.diff (divisor guard, git-apply-able); VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-4696

Confirmed kernel references

Detail

Exploit chain

unprivileged session user (DRM master) -> DRM_IOCTL_MODE_ATOMIC commit, plane: src 640x480 XRGB8888, CRTC_X=640 (== mode width), CRTC_W=1, CRTC_H=480 on a 640x480 mode -> amdgpu_dm fill_rects_from_plane_state copies raw rects, clip=dst (amdgpu_dm.c:2210-2240) -> dm_plane_atomic_check + dc_validate_plane format-only (amdgpu_dm.c:3585-3603, dc_resource.c:2891-2900, dcn10_resource.c:1131-1138) -> resource_build_scaling_params: viewport.width=1640/1=640 passes the >=16 guard (dc_resource.c:543,1125-1127); calculate_recout: recout.x=640, recout.width=(0+640)-640=0 exactly (dc_resource.c:639-643) -> dpp_get_optimal_number_of_taps returns true (pixel_width computed and never used, dcn10_dpp.c:148-151) -> validation DC_OK -> commit: dcn10_update_dchubp_dpp (dcn10_hw_sequencer.c:2101-2104) -> update_scaler (:2029-2041) -> dpp1_dscl_set_scaler_manual_scale (dscl.c:647, memcmp passes, not DSCL_BYPASS) -> dpp1_dscl_find_lb_memory_config (:463, use_max_lb=false) -> dpp1_dscl_calc_lb_num_partitions (:476) -> line_size=min(640,0)=0, memory_line_size_y=(010+71)/72=0 (:411-416) -> 816/0 at :439 -> integer #DE in kernel mode -> kernel panic (guest userland twin: SIGFPE exit 136, 3/3 runs)

Evidence (decisive lines)

run.log/run.2.log: attack case prints 'viewport.width=640 recout.x=640 recout.width=0 line_size=0 memory_line_size_y=0' then 'Floating point exception' exit 136; control case completes with clamped num_part_y=64; env.txt: no /dev/dri, no vendor 0x1002 PCI, guest uname; build_first_attempt.log: the UB-pitfall record (gcc8 elides a constant-numerator x/0 - kernel uses runtime divisor => real idiv => #DE, cf. DF-4582 trap-18 panic on this same guest); VERDICT.md has the link-by-link static chain with path:line

PoC changes

Seed was authored fresh for this batch (no prior pack). Two fixes during verification: (1) a malformed dead-variable line failed to compile (build_first_attempt.log) - replaced with a faithful mirror of the kernel's dead pixel_width (dcn10_dpp.c:148-151); (2) the naive division replica let gcc8 exploit the C UB of a constant-numerator x/0 and silently avoided the fault - fixed by hiding the divisor behind an asm barrier so a real divide instruction executes, making SIGFPE the faithful twin of the kernel #DE (kernel divisor is a runtime value => real idiv => fault; cf. DF-4582).

Verified recommended fix

In dpp1_dscl_calc_lb_num_partitions, return num_part_y=num_part_c=0 without dividing when memory_line_size_y/c/a <= 0 (degenerate viewport/recout or unsupported depth); callers' dpp1_dscl_is_lb_conf_valid then rejects the config instead of #DE-panicking.

Verdict

Divide-by-zero at sys/dev/drm/amd/display/dc/dcn10/dcn10_dpp_dscl.c:439 (lb_memory_size / memory_line_size_y; same at :440/:441) in dpp1_dscl_calc_lb_num_partitions, called from dpp1_dscl_find_lb_memory_config (:476) on the atomic-commit program path (update_scaler, dcn10_hw_sequencer.c:2029-2041 -> dpp1_dscl_set_scaler_manual_scale dscl.c:647). line_size = min(viewport.width, recout.width) becomes exactly 0 because calculate_recout clamps recout.width to (dst.x+dst.width)-recout.x == 0 for a plane positioned flush off the right edge of the stream window (dc_resource.c:639-643) while viewport.width stays >= 16 in SOURCE pixels (dc_resource.c:543), so the only degeneracy guard (dc_resource.c:1125-1127) passes; amdgpu_dm never clips plane rects to the CRTC (amdgpu_dm.c:2210-2240), plane validation is format-only (amdgpu_dm.c:3585-3603, dc_resource.c:2891-2900, dcn10_resource.c:1131-1138), and the DCN10 taps check computes pixel_width=min(viewport,recout) but never reads it (dcn10_dpp.c:148-151) - the config validates DC_OK and the commit faults with integer #DE -> kernel panic. Could not be executed on the audit QEMU guest because it has no AMD GPU (QEMU std VGA 0x1234:1111, no /dev/dri, no 0x1002 PCI device): the amdgpu DC module cannot attach, so the kernel path is unreachable at runtime on this guest (same constraint as DF-4373/DF-4582 family members DF-4373). The full producer+sink arithmetic was instead executed on the guest as a userland twin with the kernel's integer types: the attack geometry yields line_size=0, memory_line_size_y=0 and the exact division from :439 faults with SIGFPE (exit 136) on 3/3 runs while the control geometry completes - the static chain is certain, the runtime trigger needs DCN1.0 (Raven) hardware. Local DoS (kernel panic) by an unprivileged session user (DRM master) on real DCN1.0 hardware; not memory corruption, no privesc avenue.