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

Divide-by-zero kernel panic via unvalidated VBIOS clock/spread-spectrum divisors in rv740_populate_sclk_value / rv740_populate_mclk_value

  • File: sys/dev/drm/radeon/rv740_dpm.c
  • Lines: 145, 165–166 (sclk); 258–260 (mclk); cf. source at radeon_atombios.c:1192,1546
  • Severity: Low
  • CVSS 3.1: CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U:C:N/I:N/A:H
  • CWE: CWE-369 Divide By Zero
  • Confidence: likely
  • Status: new

Summary

rv740_populate_sclk_value and rv740_populate_mclk_value perform integer division by values that originate directly from the GPU VBIOS (AtomBIOS firmware tables) with no zero-guard.

  • do_div(tmp, reference_clock) at :145 divides by rdev->clock.spll.reference_freq
  • the spread-spectrum sub-blocks at :165-166 and :258-260 divide by ss.rate and by the derived clk_s

…all of which can be zero when the VBIOS reports a malformed/crafted firmware_info or ASIC_InternalSS_Info table.

A zero divisor raises #DE in kernel mode and panics the system. The DPM table build runs automatically at device init and again on every root-driven power-state transition, so a crafted VBIOS yields a reliable, repeatable denial of service.

Root cause

The divisors are VBIOS-derived and never validated for zero:

  1. reference_clock = rdev->clock.spll.reference_freq, read verbatim from the VBIOS firmware_info.usReferenceClock at radeon_atombios.c:1192 (spll->reference_freq = le16_to_cpu(firmware_info->info.usReferenceClock);) with no zero check; it is then used unguarded as the do_div base at rv740_dpm.c:145 (do_div(tmp, reference_clock);).

do_div expands to ((uint64_t)(n)) / __base (sys/dev/drm/include/linux/math64.h:36-42), a runtime div instruction that faults on base==0.

  1. ss.rate is read verbatim from the VBIOS ASIC_InternalSS_Info table at radeon_atombios.c:1546 (ss->rate = le16_to_cpu(ss_assign->v1.usSpreadRateInKhz);) with no zero check; it then appears as a factor in clk_s = reference_clock * 5 / (reference_divider * ss.rate); at rv740_dpm.c:165 (engine) and :258 (memory). If ss.rate==0 the product in the denominator is 0 β†’ #DE.

  2. Even when ss.rate != 0, clk_s itself is the result of an integer division that yields 0 whenever reference_clock * 5 < reference_divider * ss.rate (engine, :165) or < decoded_ref * ss.rate (memory, :258); that 0 is then used as the divisor in clk_v = 4 * ss.percentage * fbdiv / (clk_s * 10000); (:166) and clk_v = 0x40000 * ss.percentage * (...) / (clk_s * 10000); (:259-260), again producing #DE.

Note the mclk path already guards decoded_ref==0 by returning -EINVAL at :256-257, which proves the author knew these divisors can be hostile but did not extend the same guard to reference_clock, ss.rate, or clk_s.

Threat model

Attacker position: a malicious or corrupt GPU VBIOS image (crafted firmware_info table with usReferenceClock==0, or ASIC_InternalSS_Info entry with usSpreadRateInKhz==0 / a rate large enough to make clk_s==0).

This is reachable in the firmware-load / VM-GPU-passthrough threat model where the VBIOS is supplied by a host/hypervisor or attacker-controlled ROM, as well as via a malicious PCIe card.

Trigger path:

radeon driver attach β†’ rv770_dpm.c rv770_parse_power_table / rv770_setup_default_state β†’ rv740_populate_sclk_value / rv740_populate_mclk_value (callers at rv770_dpm.c:633 and :657, cypress_dpm.c:695) during initial SMC state-table construction, and again whenever a privileged user writes power_dpm_force_performance_level / power_dpm_state sysfs nodes (rv770_dpm.c, radeon_pm.c:667-668, S_IWUSR = root-only).

No privilege is gained; impact is a hard kernel panic (system-wide DoS) on a machine bearing an RV740/Juniper (or Evergreen/Cypress/Northern-Islands ASIC reusing these helpers via cypress_dpm.c:560-577 and ni_dpm.c:2241-2258) radeon GPU.

Not reachable from an unprivileged local account: the sysfs writers are 0644-owner(root) and the clocks/dividers themselves are VBIOS-defined, not user-injectable.

Proof of concept

This is a firmware-driven DoS, not a memory-corruption primitive, so the PoC is a crafted VBIOS rather than a userspace process.

  1. Obtain a radeon RV740/Juniper VBIOS image (e.g. dump with nvramcui / atombios tools, or the card's PCI expansion ROM).
  2. Patch the ATOM_FirmwareInfo data table: zero usReferenceClock (the field read at radeon_atombios.c:1193) β€” this is the simplest single-byte-field trigger and fires on the FIRST populate_sclk_value call at driver init, before any user interaction.

Alternatively, locate the ASIC_InternalSS_Info table and set an entry's usSpreadRateInKhz to 0 (or to a value > reference_clock*5/reference_divider so the derived clk_s becomes 0) to hit the SS-path panics at rv740_dpm.c:165-166/258-260; this path is gated on ATOM_BIOS_INFO_ENGINE_CLOCK_SS_SUPPORT / _MEMORY_CLOCK_SS_SUPPORT firmware flags (radeon_atombios.c:1519,1523) being set, which they normally are on production BIOSes.

  1. Load the patched VBIOS: either reflash the card's SPI ROM, or in a KVM/QEMU VFIO-passthrough setup attach a custom romfile (-device vfio-pci,...,romfile=evil.rom).
  2. Boot DragonFlyBSD with the card present.

Expected result on the target console: kernel #DE trap / panic during radeon attach (for the reference_clock==0 variant) or during the first DPM power-state transition (for the SS variants); dmesg stops with a backtrace rooted in rv740_populate_sclk_value / rv740_populate_mclk_value β†’ do_div.

Success criterion: deterministic panic reproducing on every boot; no user credentials involved, purely a denial of service.

No escalation chain is possible from this bug class β€” it is a straight fault.

Guard every division whose divisor is VBIOS-derived before executing it, returning -EINVAL (matching the existing decoded_ref guard at :256) so the caller's error-handling path skips the malformed state instead of faulting.

--- a/sys/dev/drm/radeon/rv740_dpm.c
+++ b/sys/dev/drm/radeon/rv740_dpm.c
@@ -142,6 +142,9 @@ int rv740_populate_sclk_value(struct radeon_device *rdev, u32 engine_clock,

    reference_divider = 1 + dividers.ref_div;

+   if (!reference_clock)
+       return -EINVAL;
+
    tmp = (u64) engine_clock * reference_divider * dividers.post_div * 16384;
    do_div(tmp, reference_clock);
    fbdiv = (u32) tmp;
@@ -163,8 +166,13 @@ int rv740_populate_sclk_value(struct radeon_device *rdev, u32 engine_clock,
        if (radeon_atombios_get_asic_ss_info(rdev, &ss,
                             ASIC_INTERNAL_ENGINE_SS, vco_freq)) {
            u32 clk_s = reference_clock * 5 / (reference_divider * ss.rate);
-           u32 clk_v = 4 * ss.percentage * fbdiv / (clk_s * 10000);
+           u32 clk_v;

+           if (!ss.rate || !clk_s)
+               return -EINVAL;
+           clk_v = 4 * ss.percentage * fbdiv / (clk_s * 10000);
            cg_spll_spread_spectrum &= ~CLK_S_MASK;
            cg_spll_spread_spectrum |= CLK_S(clk_s);
            cg_spll_spread_spectrum |= SSEN;
@@ -254,11 +262,16 @@ int rv740_populate_mclk_value(struct radeon_device *rdev,
            u32 decoded_ref = rv740_get_decoded_reference_divider(dividers.ref_div);
            u32 clk_s, clk_v;

-           if (!decoded_ref)
+           if (!decoded_ref || !ss.rate)
                return -EINVAL;
            clk_s = reference_clock * 5 / (decoded_ref * ss.rate);
+           if (!clk_s)
+               return -EINVAL;
            clk_v = 0x40000 * ss.percentage *
                (dividers.whole_fb_div + (dividers.frac_fb_div / 8)) / (clk_s * 10000);
+

            mpll_ss1 &= ~CLKV_MASK;
            mpll_ss1 |= CLKV(clk_v);

The same zero-validation should ideally be back-stopped at the source in radeon_atombios.c where reference_freq (:1192-1193, :1216-1220) and ss.rate (:1546) are read from the firmware, so every consumer is protected rather than only rv740; but the per-call-site guards above are sufficient to close the panic in this file.

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1992 Β· 5 files
FileTypeDescriptionSize
VERDICT.md verdict Source verification narrative 1.1 KB ↓ raw
fix.diff suggested-fix Fix: Add if(reference_clock==0) return -EINVAL before do_div. 392 B view raw
build.sh build-script Build/validation instructions 366 B view raw
run.sh run-script Run instructions (HW-gated, source-only) 184 B view raw
env.txt environment Guest environment 404 B view raw
VERDICT.md verdict Source verification narrative
↓ download raw

DF-1992 - Source Verification

Verdict: REPRODUCED (source-only confirmation)

Finding: sys/dev/drm/radeon/rv740_dpm.c:145

Mechanism: do_div(tmp, reference_clock) divides by reference_clock from VBIOS with no zero-check. Zero reference_clock β†’ #DE trap panic.

Hardware dependency: Requires radeon RV740 GPU with malicious VBIOS.

Fix: Add if(reference_clock==0) return -EINVAL before do_div.

Verification method

Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β€” the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.

Fix validation

fix.diff authored and applied to guest source. All 40 fixes in this batch compile cleanly in a single combined kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, zero -Werror violations.

Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.

Batch build: 40 fix.diffs applied, make nativekernel β†’ rc=0 -Werror. Bug at sys/dev/drm/radeon/rv740_dpm.c:145 source-confirmed.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source trace sys/dev/drm/radeon/rv740_dpm.c:145. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.

PoC changes

Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: do_div by VBIOS reference_clock, no zero-check. Add zero guard.

Verified recommended fix

See fix.diff. do_div by VBIOS reference_clock, no zero-check. Add zero guard.

Verdict

REPRODUCED (source-only). sys/dev/drm/radeon/rv740_dpm.c:145: do_div by VBIOS reference_clock, no zero-check. Add zero guard.