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

amdgpu_atombios_get_clock_info() accepts usReferenceClock==0 with no fixup (unlike every sibling field) β†’ ppll.reference_freq==0 guarantees amdgpu_pll_compute divide-by-zero at first modeset (the rs400/r100 'reference_freq==0' lead, confirmed)

Summary

The same function fixups every neighbouring degenerate field (pll_out_min->64800, max_pixel_clock->40000, ...) but copies usReferenceClock verbatim - den=0 in pll_compute traps at :63 on the first modeset with any nonzero clock; atombios_crtc.c:875 divides by reference_freq/100 too. Guest harness: SIGFPE 3/3, gdb-pinned, control clean. Producer-side zero->2700 fixup.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5005 Β· 12 files
FileTypeDescriptionSize
pll_harness.c β€” 10.7 KB view raw
pll_harness_fixed.c β€” 10.8 KB view raw
build.sh β€” 99 B view raw
run.sh β€” 120 B view raw
build.log β€” 161 B view raw
run.log β€” 295 B view raw
gdb_lines.txt β€” 1.6 KB view raw
env.txt β€” 1.1 KB view raw
fix.diff β€” 1.4 KB view raw
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
verdict.json β€” 3.2 KB view raw

DF-5005 β€” amdgpu_atombios_get_clock_info() accepts usReferenceClock==0 β†’ guaranteed amdgpu_pll_compute div0

sys/dev/drm/amd/amdgpu/amdgpu_atombios.c:592-593:

ppll->reference_freq = le16_to_cpu(firmware_info->info.usReferenceClock);

is copied from the VBIOS FirmwareInfo table without any zero check, even though the same function fixes up every neighbouring field that can be 0: pll_out_min β†’ 64800 (:610-611), lcd_pll_out_min/max β†’ pll_out_* (:603-608), spll/mpll pll_out_min β†’ 64800 (:642-643, :669-670), max_pixel_clock β†’ 40000 (:708-709). reference_freq is the odd one out β€” this is exactly the "latent reference_freq==0 div0" lead from the rs400/r100 family.

The value is installed into ppll[0] and cloned into every ppll[1..] (:628-629); every pixel-clock programming path then feeds it to amdgpu_pll_compute() (atombios_crtc.c:835-854 β†’ amdgpu_pll.c:187 den = pll->reference_freq). With reference_freq == 0:

  • amdgpu_pll_reduce_ratio(): gcd(nom, 0) = nom β†’ den = 0; the denominator-minimum fixup then executes DIV_ROUND_UP(den_min, 0) β†’ divide by zero panic at amdgpu_pll.c:63 on the first modeset with any nonzero clock;
  • (if the mode clock is also 0: gcd(0,0) = 0 β†’ *nom /= 0 at :51);
  • sibling sink: spread-spectrum step-size math divides by pll->reference_freq / 100 (atombios_crtc.c:875-879) β€” reference_freq < 100 gives another zero divisor on the same path.

Same for the spll/mpll copies (:632-633, :659-660: usCoreReferenceClock, usMemoryReferenceClock) which feed the DPM PLL calculators.

Reproduce

Same harness as DF-5004 (verbatim amdgpu_pll.c function copies):

cc -g -O0 -Wall -o pll_harness pll_harness.c
./pll_harness ref0      # SIGFPE at amdgpu_pll.c:63 (gdb_lines.txt)
./pll_harness control   # sanity: 27 MHz reference completes fine

gdb pins the trap to pll_harness.c:114 == amdgpu_pll.c:63 (tmp = DIV_ROUND_UP(den_min, *den) with den == 0, frame shows nom_min=4, den_min=11 β€” den_min is post_div_min=ceil(64800/6000)=11, proving the zero is the reference frequency, not the mode clock).

Threat model

The zero comes from the VBIOS/ATOM firmware image (corrupt or malicious VBIOS, or a driver bug leaving the field unset when get_clock_info is skipped), not from runtime user input β€” unlike DF-2037/DF-5004. Impact is a deterministic kernel panic at first display configuration (firmware robustness / secure-boot supply-chain angle), hence Low.

Fix

fix.diff β€” give reference_freq the same ==0 fixup its sibling fields already get (27 MHz default for ppll; spll/mpll similarly), plus harden the consumer (amdgpu_pll.c guards, DF-5004's fix.diff).

VERDICT.md
↓ download raw

DF-5005 VERDICT β€” REPRODUCED (function-level, deterministic); firmware-gated producer

Finding: amdgpu_atombios_get_clock_info() (sys/dev/drm/amd/amdgpu/amdgpu_atombios.c:592-593) copies usReferenceClock from the VBIOS FirmwareInfo table into ppll->reference_freq (and clones it to all ppll[1..3] at :628-629) with no zero check, unlike every sibling field in the same function that gets an explicit ==0 fixup (pll_out_min :610-611, lcd_pll_out_min/max :603-608, spll->pll_out_min :642-643, mpll->pll_out_min :669-670, max_pixel_clock :708-709). This answers the rs400/r100 audit lead: yes, reference_freq can be 0 from the fw-info parse and it reaches amdgpu_pll_compute (atombios_crtc.c:835-854 picks ppll[0..2] and passes the adjusted clock; amdgpu_pll.c:187 sets den = pll->reference_freq).

Reproduction (guest)

Same harness as DF-5004 β€” verbatim copies of amdgpu_pll.c:44-251; the ref0 case sets reference_freq = 0 with a normal 600 MHz-class mode clock (freq=60000) and the stock amdgpu_atombios.c:618-625 PLL limits:

case ref0 : freq=60000 ref=0 ... -> TRAPPED signal 8 (SIGFPE)
case control (ref=2700)          -> COMPLETED dot=14850 fb=880 ref=2 post=80

gdb (gdb_lines.txt): #0 amdgpu_pll_reduce_ratio (nom=..., den=..., nom_min=4, den_min=11) at pll_harness.c:114 β€” pll_harness.c:114 is the verbatim tmp = DIV_ROUND_UP(den_min, *den); i.e. amdgpu_pll.c:63, and den_min=11 is post_div_min = ceil(64800/6000) = 11, proving the zero divisor is the reference frequency (the mode clock is nonzero: 6000).

Determinism: 3/3 in the DF-5004 stress runs (pll_harness ref0 exit 42 every time), plus the dedicated run in run.log.

Why it panics

den = 0 β†’ gcd(nom, 0) = nom (sys/dev/drm/include/linux/gcd.h: b == 0 β†’ return a) β†’ after the division den = 0 survives β†’ the denominator-minimum fixup runs DIV_ROUND_UP(den_min, 0) β†’ #DE trap β†’ kernel panic at the first display configuration after boot/resume. If the mode clock is also 0 the first gcd itself is gcd(0,0) = 0 and *nom /= 0 traps at amdgpu_pll.c:51. Sibling sink: atombios_crtc.c:875-879 divides the spread-spectrum step size by pll->reference_freq / 100, so any firmware value < 100 is a second zero divisor on the same call chain.

Impact / threat

The trigger is a corrupt or malicious VBIOS/ATOM image (or a code path that leaves the field unset), not runtime attacker input β€” hence Low severity (deterministic panic = firmware-robustness / supply-chain issue; no memory corruption, no escalation). The consumer-side hardening is DF-5004's fix.diff; the producer fix is this finding's fix.diff (same ==0 fixup pattern the file already uses for its neighbours; 27 MHz default for ppll/spll, 100 MHz for mpll).

Fix validation

Producer-level: applying fix.diff replaces the 0 with 2700 β€” exactly the control input, which completes with sane dividers (run.log). Consumer guards (DF-5004 fix.diff) additionally make the unfixed producer complete without trapping (see DF-5004 fix_run.log, ref0 case). Kernel-level rebuild validation not possible on this guest (no AMD GPU β€” env.txt).

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

producer fix maps reference_freq 0 -> 2700, which is exactly the control input that completes with sane dividers (run.log); consumer guards (DF-5004 fix.diff) additionally complete the unfixed-producer case (DF-5004 fix_run.log ref0). Kernel-level rebuild validation not possible on this guest (no AMD GPU).

run.log control case; DF-5004/fix_run.log ref0 case
↓ fix.diffper-fix-DF-5005

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (ref0 SIGFPE, control completes dot=14850 fb=880 ref=2 post=80); gdb_lines.txt (#0 amdgpu_pll_reduce_ratio ... at pll_harness.c:114, den_min=11); DF-5004/run.2.log (3/3 determinism); env.txt (no AMD GPU on guest)

PoC changes

no seed; harness shared with DF-5004 (verbatim amdgpu_pll.c copies, ref0 case exercises this producer)

Verified recommended fix

amdgpu_atombios.c: if (ppll->reference_freq == 0) ppll->reference_freq = 2700; (and equivalents for spll/mpll) β€” mirrors the existing ==0 fixups in the same function

Verdict

REPRODUCED (function-level, deterministic, on-guest): amdgpu_atombios.c:592-593 installs usReferenceClock from the VBIOS into ppll->reference_freq (cloned to all ppll[1..] at :628-629) with NO zero check even though the same function fixups pll_out_min (:610-611), lcd_pll_out_* (:603-608), spll/mpll pll_out_min (:642/:669) and max_pixel_clock (:708-709). With reference_freq==0, amdgpu_pll_compute (reached from atombios_crtc.c:854 with ppll[0..2]) sets den=0 (amdgpu_pll.c:187); gcd(nom,0)=nom leaves den=0 and DIV_ROUND_UP(den_min,0) traps at amdgpu_pll.c:63 β€” guest harness SIGFPE 3/3, gdb pins harness:114==amdgpu_pll.c:63 with den_min=11 (post_div_min for the 600MHz input), proving the zero divisor is the reference clock. gcd(0,0)=0 additionally traps :51 if the mode clock is also 0; atombios_crtc.c:875-879 (ss step size / (ref_freq/100)) is a sibling zero-divisor for ref<100. Gated on corrupt/malicious VBIOS (or skipped get_clock_info) rather than runtime user input -> Low severity, deterministic panic at first modeset, no corruption/escalation.