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

amdgpu_pll_compute() divide-by-zero via degenerate derived dividers: fb_div under-runs to 0 (DIV_ROUND_UP(nom_min,0) at :56) and ref_div forced to 0 by post_div>=129 (0/0 at :203) β€” distinct sinks and windows from DF-2037

Summary

Beyond DF-2037's tc/post_div==0: small nonzero tc (0.10-1.09MHz window) makes post_div_min=12960 unclamped -> empty loop -> fb_div rounds to 0 -> DIV_ROUND_UP(nom_min,0) at :56; and USE_POST_DIV with firmware post_div>=129 zeroes ref_div -> 0/0 at :203. VERBATIM harness on guest: SIGFPE 3/3 both modes, gdb line-pinned to :56/:203, control clean, fix validated bit-identical. Unpriv DRM-master modeset (sink a) / AdjustDisplayPll output (sink b). Floor-the-degenerates fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5004 Β· 14 files
FileTypeDescriptionSize
pll_harness.c β€” 10.7 KB view raw
build.sh β€” 99 B view raw
run.sh β€” 147 B view raw
build.log β€” 313 B view raw
run.log β€” 772 B view raw
run.2.log β€” 222 B view raw
gdb_functions.txt β€” 370 B view raw
gdb_lines.txt β€” 1.6 KB view raw
fix_run.log β€” 614 B view raw
env.txt β€” 1.1 KB view raw
fix.diff β€” 1.4 KB view raw
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
verdict.json β€” 3.8 KB view raw

DF-5004 β€” amdgpu_pll_compute() divide-by-zero via degenerate derived dividers (fb_div==0 / ref_div==0)

New pass-2 sinks in sys/dev/drm/amd/amdgpu/amdgpu_pll.c, distinct from the known DF-2037 (target_clock==0 / post_div==0). Here target_clock and post_div are NONZERO; the derived quantities under-run to zero and are then used as divisors:

  1. fb_div β†’ 0, panic at amdgpu_pll.c:56 Small-but-nonzero target_clock (freq/10 ∈ [1, ~ref_freq/254], e.g. freq=55 β†’ tc=5 with the standard 27 MHz reference): post_div_min = ceil(64800/5) = 12960 > post_div_max=127 β†’ empty search loop β†’ post_div = 127 β†’ ref_div_max = min(128/127,1023) = 1 β†’ fb_div = DIV_ROUND_CLOSEST(nom*1*127, den) = 0 β†’ amdgpu_pll_reduce_ratio(fb_div=0, ref_div=1, 4, 2): gcd(0,1)=1 β†’ nom=0 β†’ DIV_ROUND_UP(4, 0) divide by zero. (DF-2037 only covers tc==0, i.e. freq<10; the window freq ∈ [10,109] is one decade above it and was NOT covered.)
  2. ref_div β†’ 0, panic at amdgpu_pll.c:203 AMDGPU_PLL_USE_POST_DIV with pll->post_div β‰₯ 129 (settable from the AdjustDisplayPll firmware output ucPostDiv, a u8, at atombios_crtc.c:441-442): ref_div_max = min(128/post_div,1023) = 0 β†’ ref_div = min(max(...,1u), 0) = 0 β†’ in the diff loop (reference_freq*fb_div) / (ref_div*post_div) = 0/0 divide by zero. (Variant: an unclamped post_div_min > 128 combined with AMDGPU_PLL_PREFER_MINM_OVER_MAXP β€” flag is never set by amdgpu callers today, noted as dead route.)

In the kernel these are divu/idiv by zero β†’ #DE trap β†’ panic. Same call path and same attacker as DF-2037: a DRM-master/VT user issues a modeset with a degenerate dot clock; the mode clock reaches amdgpu_pll_compute() unvalidated via atombios_crtc.c:854 (amdgpu_crtc->adjusted_clock, guarded only against == 0).

Reproduce

Run on the DragonFly guest (or any Unix with a C compiler):

./build.sh          # cc -g -O0 -o pll_harness pll_harness.c
./run.sh            # control / tc5 / ref0 / pd200, one process each

Expected: control completes with sane dividers (dot=14850 fb=880 ref=2 post=80); tc5, ref0, pd200 each die with SIGFPE (integer divide by zero). gdb_lines.txt pins the trapping source lines inside the verbatim function copies:

tc5   β†’ pll_harness.c:107  == amdgpu_pll.c:56  (DIV_ROUND_UP(nom_min, *nom=0))
ref0  β†’ pll_harness.c:114  == amdgpu_pll.c:63  (DIV_ROUND_UP(den_min, *den=0))
                                  [that sink is filed separately as DF-5005]
pd200 β†’ pll_harness.c:228  == amdgpu_pll.c:203 ((ref_freq*fb_div)/(ref_div*post_div) = 0/0)

The harness reproduces amdgpu_pll_reduce_ratio / amdgpu_pll_get_fb_ref_div / amdgpu_pll_compute verbatim (sys/dev/drm/amd/amdgpu/amdgpu_pll.c:44-251) with the tree's own gcd64() and DIV_ROUND_* semantics, and the PLL limits programmed for every pixel PLL at amdgpu_atombios.c:618-625.

Why no in-kernel run on the guest

The audit QEMU guest has no AMD GPU (only a bochs/QEMU std VGA β€” see env.txt), so amdgpu never attaches and the DRM modeset path cannot be driven in-kernel there. The divide-by-zero is a pure arithmetic property of the (verbatim-copied) function and is proven deterministically in userland; in-kernel it is the same division on the same values (trap 0 β†’ panic). DF-2037 (pass 1) was verified the same way ("source-only"), this pack is strictly stronger: executable + gdb line-level + 3Γ— determinism + fix validation (fix_run.log: patched functions complete all cases, control output bit-identical).

VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched reproduced

fix.diff guards applied 1:1 to the verbatim harness copy (pll_harness_fixed.c, built+run on guest): baseline SIGFPE cases tc5/pd200 now complete with defined output, control case output bit-identical (dot=14850 fb=880 ref=2 post=80) β€” see fix_run.log. Kernel-level rebuild validation not possible on this guest (no AMD GPU to drive the modeset path); validated at function level only.

fix_run.log (guest, -g -O0): control/tc5/ref0/pd200 all exit 0, control unchanged vs baseline
↓ fix.diffper-fix-DF-5004

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log + run.2.log (SIGFPE 3/3 on tc5/pd200, control sane); gdb_lines.txt (traps at harness:107/:228 == amdgpu_pll.c:56/:203 with nom=0 in frame args); fix_run.log (patched functions: all cases complete, control bit-identical); env.txt (guest has no AMD GPU)

PoC changes

no seed existed; harness authored at verification time (verbatim function copies + tree-exact gcd64/DIV_ROUND_* semantics + amdgpu_atombios.c:618-625 firmware PLL limits)

Verified recommended fix

amdgpu_pll.c: floor gcd result and post-reduce nom/den to >=1 in amdgpu_pll_reduce_ratio; force ref_div_max >= 1 and post_div >= 1 in amdgpu_pll_get_fb_ref_div (fix.diff)

Verdict

REPRODUCED (function-level, deterministic, on-guest): verbatim copies of amdgpu_pll.c:44-251 divide by zero for attacker-representable modeset inputs beyond DF-2037's target_clock==0/post_div==0 windows β€” (a) target_clock in [1, ref/254] (freq 10..109, non-FRAC, std 27MHz ref) forces fb_div=DIV_ROUND_CLOSEST(nom1127,den)=0 (post_div_min=ceil(vco_min/tc) is never clamped to max_post_div, loop empty, ref_div_max=128/127=1), and amdgpu_pll_reduce_ratio then executes DIV_ROUND_UP(fb_div_min, 0) at amdgpu_pll.c:56; (b) AMDGPU_PLL_USE_POST_DIV with pll->post_div>=129 (firmware AdjustDisplayPll ucPostDiv, atombios_crtc.c:441-442) forces ref_div_max=min(128/post_div,1023)=0 -> ref_div=0 -> 0/0 in the diff loop at amdgpu_pll.c:203. Guest run: SIGFPE on tc5/pd200 3/3 runs, control computes sane dividers, gdb pins harness:107==amdgpu_pll.c:56 and harness:228==amdgpu_pll.c:203. Kernel equivalent is div-by-zero trap #DE -> panic on the DRM-master modeset path (atombios_crtc.c:854), i.e. local DoS, no corruption/escalation. In-kernel repro impossible on this guest (no AMD GPU, env.txt) β€” same verification standard as DF-2037 (pass-1 'source-only'), but executable and gdb-pinned here.