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)
PoC verification
Evidence pack
findings/poc/DF-5004 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- 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=5with 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 coverstc==0, i.e.freq<10; the windowfreq β [10,109]is one decade above it and was NOT covered.) - ref_div β 0, panic at amdgpu_pll.c:203
AMDGPU_PLL_USE_POST_DIVwithpll->post_div β₯ 129(settable from the AdjustDisplayPll firmware outputucPostDiv, a u8, atatombios_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/0divide by zero. (Variant: an unclampedpost_div_min > 128combined withAMDGPU_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).
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
fixedfix.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
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.
No comments yet.