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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3853 Β· 11 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + hardware reproduction notes
↓ download 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) and calc_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 /= 0 raises #DE on any x86 CPU: divide_error trap 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.

VERDICT.md verdict full narrative with path:line trace
↓ download raw

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:741 p1pll->reference_freq = RBIOS16(pll_info + 0xe);
  • radeon_combios.c:758 spll->reference_freq = RBIOS16(pll_info + 0x1a);
  • radeon_combios.c:773 mpll->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 the radeon_compute_pll_avivo divide 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 initial radeon_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

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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3853

Confirmed kernel references

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.