Unchecked BIOS sysclk reference frequency (0) reaches calc_eng_mem_clock division β kernel divide-by-zero panic (local DoS) on PM transitions
Summary
radeon_combios_get_clock_info() loads p1pll/spll/mpll reference_freq raw from the PLL info table with no validation. The only post-parse sanitization anywhere clamps reference_div only - reference_freq is never checked, even though the fallback path in the same function knows good defaults. calc_eng_mem_clock() guards its ref_div fallback but then executes req_clock /= (2 * spll->reference_freq) - with the BIOS word == 0 this is a 32-bit integer division by zero: #DE -> kernel panic on every PM engine-clock change of pre-AVIVO ASICs. HARNESS replicates the kernel statements verbatim and dies with SIGFPE (exit 136). Crafted VBIOS on a pre-AVIVO board with a mobility powerplay table: the first PM transition panics the kernel. Repeatable local DoS. Parse-side zero-sanitize + defense-in-depth sink guard fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3853 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | verbatim copy of the kernel arithmetic (combios clock-info load + calc_eng_mem_clock) over a crafted PLL table; dies with SIGFPE | 3.7 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 65 B | view raw |
| run.sh | run-script | ./harness (crash-tolerant wrapper for the SIGFPE/SIGSEGV harnesses) | 89 B | view raw |
| build.log | build-log | full compiler output (guest) | 65 B | view raw |
| run.log | run-log | full runtime output (guest) - decisive | 299 B | view raw |
| fix.diff | suggested-fix | COMBIOS sysclk reference_freq = 0 -> divide by zero in calc_eng_mem_clock (kernel #DE panic) - git/patch-applyable vs sys/ | 1.1 KB | view raw |
| fix_apply.log | apply-log | patch --dry-run -p1 -d /usr/src on the guest: clean apply | 633 B | view raw |
| VERDICT.md | verdict | full narrative with path:line trace | 4.4 KB | β raw |
| README.md | readme | build/run/expected + hardware reproduction notes | 1.8 KB | β raw |
| env.txt | environment | guest uname + toolchain + 'dmesg radeon lines: 0' | 210 B | view raw |
| verdict.json | verdict-json | machine verdict (persist_poc.py input) | 3.8 KB | view raw |
DF-3853 β COMBIOS sysclk reference_freq = 0 β divide-by-zero in calc_eng_mem_clock
Verdict (one line)
CONFIRMED REAL by source trace; harness replicates the exact kernel
arithmetic and dies with SIGFPE (the userspace image of #DE). Kernel-side
panic NOT observable on the audit guest (no radeon GPU) β inconclusive.
Build / run
./build.sh # cc -O2 -Wall -o harness harness.c ./run.sh # runs the harness, records the SIGFPE
Expected output
== DF-3853: crafted COMBIOS PLL table, sysclk reference_freq = 0 == [combios] spll->reference_freq=0 spll->reference_div=12 (no validation in parser ...) [pm] radeon_legacy_set_engine_clock -> calc_eng_mem_clock: [calc] about to execute: req_clock /= (2 * 0) <- denominator is ZERO harness exit=136 (136=128+8 SIGFPE on BSD => in-kernel: divide_error panic)
sh: ... Segmentation fault/Abort style shell message may or may not print
depending on the shell; the decisive evidence is the SIGFPE exit code 138.
What the harness is / is not
- IS: the verbatim arithmetic of
radeon_combios_get_clock_info()(radeon_combios.c:758-759 load) andcalc_eng_mem_clock()(radeon_clocks.c:344-385) driven by a crafted PLL table in which the sys-clock reference-frequency word (pll_info+0x1a) is zero. - IS NOT: a kernel run β the guest has no radeon device. In the kernel the
same
uint32 /= 0raises #DE on any x86 CPU:divide_errortrap in kernel context = panic.
Reproduce on target hardware
Crafted COMBIOS with a PLL info table whose +0x1a word is 0 (harness bytes),
on any pre-AVIVO mobility board (powerplay table needed so PM clock changes
happen: radeon_pm.c:224 radeon_set_engine_clock -> radeon_legacy_set_engine_clock
-> calc_eng_mem_clock). First PM transition (resume, profile change, dynpm
reclock) panics the box.
DF-3853 β VERDICT
Status: inconclusive (arithmetic proven identical to kernel code; harness SIGFPE == kernel #DE == panic; no radeon hardware on audit guest)
Full narrative
Where the bug is
radeon_combios_get_clock_info() (sys/dev/drm/radeon/radeon_combios.c:725-784)
loads PLL reference frequencies straight from the VBIOS with no validation:
radeon_combios.c:741p1pll->reference_freq = RBIOS16(pll_info + 0xe);radeon_combios.c:758spll->reference_freq = RBIOS16(pll_info + 0x1a);radeon_combios.c:773mpll->reference_freq = RBIOS16(pll_info + 0x26);
The only post-parse sanitization anywhere is in radeon_get_clock_info()
(radeon_clocks.c:194-216), and it clamps reference_div only
(if (spll->reference_div < 2) ...); reference_freq is never checked β
note that the fallback path in the same function knows good defaults
(radeon_clocks.c:229+: IGP 1432 / discrete 2700), the parse path just
doesn't apply them.
The sink
calc_eng_mem_clock() (sys/dev/drm/radeon/radeon_clocks.c:344-385) divides
by the BIOS-supplied sys-clock reference frequency, unguarded:
int ref_div = spll->reference_div;
if (!ref_div) ref_div = RREG32_PLL(...) & MASK; /* ref_div is handled */
...
req_clock /= (2 * spll->reference_freq); /* :377 β NOT handled */
ref_div got a zero-guard; reference_freq did not. A crafted COMBIOS PLL
table with the word at pll_info+0x1a equal to zero makes this a 32-bit
integer division by zero: #DE on x86 β divide_error β kernel panic.
(Second, weaker order: req_clock /= ref_div at :382 if the register
fallback also reads 0.)
Reachability
- Parse:
radeon_clocks.c:186-189β COMBIOS-format image (pre-AVIVO chips; AVIVO chips reject non-atom BIOSes, which is why theradeon_compute_pll_avivodivide path is not reachable from this parser). - Execution: PM clock switch β
radeon_pm.c:224 radeon_set_engine_clock()βradeon_legacy_set_engine_clock(registered for r100..rv4xx/rs4xx, radeon_asic.c:253,321,417,485,621) βcalc_eng_mem_clock. Triggers:radeon_pm_set_clocks()from profile/dynpm transitions, resume (radeon_pm.c:507), and the initialradeon_pm_compute_clocks()at load (radeon_pm.c:464) β the latter requires β₯2 power states, which the same crafted BIOS provides via the combios POWERPLAY table on a mobility board. - Attacker model: same crafted-VBIOS precondition as DF-1250/1251/1252 (malicious card / flashed ROM / VFCT-injected image). Impact once the image is in place: local DoS (kernel panic), repeatedly, at boot/PM events β no user privilege needed after the hardware is in the machine.
Why the harness is faithful
harness.c copies the kernel statements verbatim (commented with
path:line). The only stub is RREG32_PLL(), stubbed to return a plausible
nonzero divider so that the single zero denominator is exactly the
BIOS-controlled reference_freq. Userspace uint32 /= 0 raises SIGFPE β
the same fault class the kernel would take as #DE.
Why inconclusive
The audit guest is virtio-only; no radeon device β the kernel path cannot run there. Source-level certainty is complete: every statement between the BIOS byte and the division is cited and replicated.
Fix
fix.diff (two files):
1. radeon_combios.c: after the mpll block, sanitize zero reference
frequencies to the known-good defaults (2700) for p1/spll/mpll.
2. radeon_clocks.c (defense in depth): guard !spll->reference_freq and
!ref_div inside calc_eng_mem_clock() before either division.
Apply-verified against the pristine tree (patch --dry-run clean,
fix_apply.log). Runtime fix validation impossible on this guest:
fix_status = not_testable.
kernel_refs
- sys/dev/drm/radeon/radeon_combios.c:741,758,773 (unchecked loads)
- sys/dev/drm/radeon/radeon_clocks.c:186-189 (dispatch)
- sys/dev/drm/radeon/radeon_clocks.c:194-216 (clamp block β div only)
- sys/dev/drm/radeon/radeon_clocks.c:344-385 (calc_eng_mem_clock, :377 divide)
- sys/dev/drm/radeon/radeon_pm.c:224,464,507 (triggers)
- sys/dev/drm/radeon/radeon_asic.c:253 (asic callback)
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
not_testablefix.diff (radeon_combios.c + radeon_clocks.c defense-in-depth) applies cleanly to the guest's pristine /usr/src (fix_apply.log, hunks at 784 and 356); runtime fix validation impossible on this guest (no hardware)
fix.diff; fix_apply.log
Confirmed kernel references
- sys/dev/drm/radeon/radeon_combios.c:741
- sys/dev/drm/radeon/radeon_combios.c:758
- sys/dev/drm/radeon/radeon_combios.c:773
- sys/dev/drm/radeon/radeon_clocks.c:186-189
- sys/dev/drm/radeon/radeon_clocks.c:194-216
- sys/dev/drm/radeon/radeon_clocks.c:344-385
- sys/dev/drm/radeon/radeon_pm.c:224
- sys/dev/drm/radeon/radeon_asic.c:253
Detail
Exploit chain
crafted VBIOS (PLL table word +0x1a = 0, plus a powerplay table on a mobility board so >=2 power states exist) -> driver load parses clocks -> any PM clock transition (resume, profile/dynpm change, initial radeon_pm_compute_clocks) -> calc_eng_mem_clock divides by 2*0 -> divide_error trap in kernel context -> panic
Evidence (decisive lines)
run.log decisive lines: '[combios] spll->reference_freq=0 ...' then 'Floating point exception (core dumped)', exit=136; build.log clean; VERDICT.md has the full path:line chain; fix_apply.log proves the two-file fix.diff applies to pristine /usr/src
PoC changes
harness written from scratch (no seed); kernel arithmetic copied verbatim with path:line comments; only stub is RREG32_PLL returning a plausible nonzero so the single zero denominator is the BIOS-controlled reference_freq
Verified recommended fix
Sanitize zero reference frequencies to known-good defaults (2700) after the combios PLL parse, and guard !spll->reference_freq / !ref_div inside calc_eng_mem_clock() (fix.diff, two files)
Verdict
Source-certain divide-by-zero: radeon_combios_get_clock_info() loads spll->reference_freq = RBIOS16(pll_info+0x1a) with no validation (radeon_combios.c:758; p1pll:741 and mpll:773 equally unchecked - the only post-parse clamp, radeon_clocks.c:194-216, fixes reference_div only, never *_freq). calc_eng_mem_clock() then executes req_clock /= (2 * spll->reference_freq) (radeon_clocks.c:377) on every PM engine-clock change of pre-AVIVO asics (radeon_pm.c:224 -> radeon_legacy_set_engine_clock, radeon_asic.c:253+). A crafted COMBIOS with word 0 there makes this a 32-bit integer division by zero: #DE on x86 -> divide_error -> kernel panic. The harness copies the kernel statements verbatim and dies with SIGFPE (exit 136 = 128+8 on DragonFly; run.log: 'Floating point exception (core dumped)'), the userspace image of the same fault. Kernel-side panic not observable: audit guest virtio-only, 0 radeon devices. On target hw this is a repeatable local DoS (boot/resume/PM-transition panic) under the same crafted-VBIOS precondition as DF-1250/1251/1252.
No comments yet.