# DF-5004 VERDICT — REPRODUCED (function-level, deterministic)

**Finding:** `amdgpu_pll_compute()` divides by zero when *derived* dividers
under-run to zero — `fb_div == 0` (small nonzero `target_clock`) or
`ref_div == 0` (`post_div ≥ 129` via `AMDGPU_PLL_USE_POST_DIV`) — trapping at
`amdgpu_pll.c:56` / `amdgpu_pll.c:203` (and `:51` via `gcd(0,0)==0` for the
overlap). This is **distinct from DF-2037**, whose sinks are
`amdgpu_pll.c:172/:178` (`target_clock == 0`) and `:88/:91/:242`
(`post_div == 0`): in every case here `target_clock != 0` and `post_div != 0`.

## How it was reproduced

`pll_harness.c` contains verbatim copies of
`sys/dev/drm/amd/amdgpu/amdgpu_pll.c:44-251`
(`amdgpu_pll_reduce_ratio`, `amdgpu_pll_get_fb_ref_div`, `amdgpu_pll_compute`),
with `gcd64()` matching `sys/dev/drm/include/linux/gcd.h` exactly
(`gcd(0,0)==0`, `gcd(x,0)==x`) and `DIV_ROUND_UP`/`DIV_ROUND_CLOSEST`
matching `sys/dev/drm/include/linux/kernel.h`. PLL limits are the ones the
driver installs for every pixel PLL (`amdgpu_atombios.c:618-625`):
min/max_post_div = 2/127, min/max_ref_div = 2/1023, min/max_feedback_div =
4/4095, reference_freq = 2700 (27 MHz), pll_out_min = 64800 (the driver's
own `==0` fixup floor at `amdgpu_atombios.c:610-611`).

Built and executed on the audit QEMU guest
(`DragonFly dfbsd 6.5-DEVELOPMENT #0 ... x86_64`, see `env.txt`):

| case    | input                                   | result | sink                          |
|---------|------------------------------------------|--------|-------------------------------|
| control | freq=148500 (148.5 MHz mode)             | exit 0, dot=14850 fb=880 ref=2 post=80 | — (sane) |
| tc5     | freq=55 → tc=5, flags=0                  | SIGFPE | `amdgpu_pll.c:56` (`DIV_ROUND_UP(fb_div_min, fb_div=0)`) |
| ref0    | reference_freq=0, freq=60000             | SIGFPE | `amdgpu_pll.c:63` (`DIV_ROUND_UP(post_div_min, den=0)`) — producer filed as DF-5005 |
| pd200   | USE_POST_DIV, pll->post_div=200          | SIGFPE | `amdgpu_pll.c:203` (`(ref*fb_div)/(0*post_div)` = 0/0) |

`gdb_lines.txt` (gdb -batch, `-g -O0` build) pins each trap to the exact
statement inside the verbatim copy: harness:107==amdgpu_pll.c:56,
harness:114==amdgpu_pll.c:63, harness:228==amdgpu_pll.c:203, with the frame
arguments showing the zeros (`nom=0`, `den_min=11` for ref0 — i.e.
post_div_min=11 exactly as derived from ceil(64800/6000)).

Determinism: 3/3 runs identical (`run.2.log`).

## Arithmetic derivation (why the zeros are forced, not contrived)

*tc5*: `post_div_min = ceil(64800/5) = 12960` is never clamped down to
`max_post_div` (amdgpu_pll.c:172-176 only clamps *up* to `min_post_div`), the
search loop at :199 is empty (`12960 > 127`), `post_div = post_div_max = 127`
(:196), `ref_div_max = min(128/127, 1023) = 1` (:88), so
`fb_div = DIV_ROUND_CLOSEST(nom·1·127, den) = round(127·tc/ref) = 0` for
`tc < ref/254` (tc=5, ref=2700), then
`amdgpu_pll_reduce_ratio(0, 1, 4, 2)` at :221 divides
`DIV_ROUND_UP(4, 0)` at :56.

*pd200*: `post_div_min = post_div_max = pll->post_div = 200`
(:154-155, from `atombios_crtc.c:441-442` firmware `ucPostDiv`),
`ref_div_max = min(128/200, 1023) = 0` (:88),
`ref_div = min(max(x,1u), 0) = 0` (:91), `fb_div = 0` (:92), and the first
loop iteration computes `(reference_freq * 0) / (0 * 200)` at :203.

## In-kernel reachability

Same chain DF-2037 established: DRM-master/VT user modeset with attacker
chosen dot clock → `dce_v*_pick_pll` → `amdgpu_atombios_crtc_pll`
(atombios_crtc.c:812,:854) → `amdgpu_pll_compute(adev->clock.ppll[0..2], adjusted_clock, ...)`.
The `tc∈[1,10]` window (dot clock 0.10–1.09 MHz) sits one decade above
DF-2037's `freq<10` window; the `pd200` window additionally requires the
VBIOS AdjustDisplayPll command to return a post divider > 128. In-kernel a
`div` by zero raises #DE → kernel trap → panic: local DoS. No memory
corruption, no escalation path — the primitive is a trap only.

Guest caveat: the audit guest has no AMD GPU (`env.txt`: only
`vgapci0: Red Hat/virtio` = bochs std VGA), so amdgpu cannot attach and an
in-kernel modeset repro is impossible *on this guest*; the function-level
proof above is on the verbatim code and the in-kernel division is the same
instruction on the same values. DF-2037's pass-1 verification was accepted at
the same ("source-only") standard; this pack is strictly stronger
(executable, gdb line-pinned, 3× determinism, fix-validated).

## Fix validation (function level)

`fix.diff` adds three guards (reduce_ratio zero-floors; `post_div` floor and
`ref_div_max ≥ 1` in get_fb_ref_div). Applied 1:1 to the harness copy
(`pll_harness_fixed.c`, built on guest — `fix_run.log`):

| case    | baseline | patched |
|---------|----------|---------|
| control | dot=14850 fb=880 ref=2 post=80 | **identical** |
| tc5     | SIGFPE   | completes (degenerate, defined output) |
| pd200   | SIGFPE   | completes (dot=5994) |
| ref0    | SIGFPE   | completes (also needs DF-5005's producer fix for a *sane* clock) |

Kernel-level rebuild validation is not possible on this guest (no AMD GPU to
drive the modeset path), recorded as such in `verdict.json:fix_verdict`.

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