# 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.

## Why the kernel bug is real (static chain, every link cited)

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.
