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

rs690 watermark fixed-point divide-by-zero via pixel-clock 2^20 wrap: dfixed_const(1048576 kHz) == 0 β†’ do_div by zero β†’ kernel panic (trap 18), reachable from an unprivileged DRM master via DRM_IOCTL_MODE_SETCRTC on RS690/RS740/RS780/RS880

Summary

mode->clock is converted with a 32-bit (clock<<12) shift; clock = 2^20 or 2^21 kHz wraps to exactly 0 and the immediately following dfixed_div(1000, 0) executes a 64-bit divq by zero. Every userspace-path filter passes (core rejects only 0 and >INT_MAX; radeon installs no mode_valid), and SETCRTC needs only DRM_MASTER (first opener). The bandwidth update runs on every modeset for RS690/RS740 and (via the asic map) RS780/RS880 mass-market IGPs. VERIFIED on guest: KLD scenario with clock=1048576 -> 'Fatal trap 18 integer divide fault, Stopped at dfixed_div+0xa: divq'; shipped radeon.ko disassembly proves the same runtime div; WITH_FIX build survives all scenarios. Guard-zero-safe-watermark fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4582 Β· 18 files
FileTypeDescriptionSize
rs690_div0_body.h β€” 15.1 KB view raw
rs690_div0.c β€” 1.6 KB view raw
rs690_div0_kmod.c β€” 2.7 KB view raw
Makefile β€” 310 B ↓ download
build.sh β€” 115 B view raw
run.sh β€” 143 B view raw
setcrtc_clock_wrap.c β€” 4.0 KB view raw
build.log β€” 1.4 KB view raw
run.log β€” 1.2 KB view raw
run.2.log β€” 1.7 KB view raw
fix_run.log β€” 1.5 KB view raw
panic.txt β€” 1.2 KB view raw
objdump_radeon_ko.txt β€” 1.2 KB view raw
env.txt β€” 530 B view raw
sha256.txt β€” 334 B view raw
fix.diff β€” 954 B view raw
VERDICT.md β€” 5.2 KB ↓ raw
verdict.json β€” 6.2 KB view raw
VERDICT.md
↓ download raw

DF-4582 VERDICT

Finding: rs690_crtc_bandwidth_compute() divide-by-zero via dfixed_const(mode->clock) 32-bit shift wrap; local user with DRM access on RS690/RS740/RS780/RS880 hardware turns one DRM_IOCTL_MODE_SETCRTC into a kernel panic (integer divide fault, trap 18).

Status: primitive reproduced on guest (kernel + userland + shipped

binary); end-to-end ioctl chain NOT executed (no AMD IGP in the QEMU guest). Overall: inconclusive for the full chain, confidence: likely β€” every hop of the chain is individually proven (see below), only the hardware hop is missing.

The bug, line by line

Reachability chain (each hop verified in this tree)

  1. Local user opens /dev/dri/cardN β‡’ becomes DRM master (MODE_SETCRTC is DRM_MASTER|DRM_UNLOCKED, drm_ioctl.c:656).
  2. drm_mode_setcrtc (drm_crtc.c:655) β†’ drm_mode_convert_umode (drm_modes.c:1691): only clock > INT_MAX rejected (:1696); our 1048576 passes; drm_mode_validate_driver (:1145) β†’ drm_mode_validate_basic (:1106) rejects only clock == 0 (:1114) β€” 1048576 passes; radeon installs no mode_config.funcs->mode_valid (radeon_display.c:1350-1353), so no range check exists. drm_crtc_check_viewport only checks fb geometry.
  3. Mode applied by crtc helpers (encoder mode_fixups in radeon are unconditional return true, e.g. atombios_encoders.c:356); PLL programming is best-effort and does not fail the modeset.
  4. crtc enable β†’ radeon_pm_compute_clocks (atombios_crtc.c:296) β†’ radeon_pm_compute_clocks_old (radeon_pm.c:1729): PROFILE method calls radeon_pm_set_clocks unconditionally (:1753-1755).
  5. radeon_pm_set_clocks β†’ radeon_bandwidth_update (radeon_pm.c:311-312, guarded only by active_crtc_count != 0 β€” true after the modeset).
  6. rs690_bandwidth_update (rs690.c:583) β†’ rs690_crtc_bandwidth_compute (rs690.c:625) with crtc->base.mode = the crafted mode β†’ #DE.

ASICs: .bandwidth_update = &rs690_bandwidth_update for RS690/RS740 (radeon_asic.c:727) and RS780/RS880 (radeon_asic.c:1138).

What was executed on the guest

run result
./rs690_div0 (userland, -O0, verbatim math) SIGFPE Γ—3 scenarios
./rs690_div0 (userland, -O2) survived β€” gcc UB-fused the div chain in this build context (documented; not the source semantics, not what the shipped binary does)
KLD sysctl df4582.run=0 (kernel flags, runtime value) Fatal trap 18: integer divide fault, dfixed_div+0xa: divq, guest down (panic.txt)
objdump /boot/kernel/radeon.ko shipped binary contains the runtime div with the truncating shl $0xc at 0x5ac39-0x5ac61 β‡’ trap reachable in the shipped code, no UB folding
KLD WITH_FIX, scenarios 0/1/2 DF4582-FIX: zero divisor avoided, all survive, clean unload (fix_run.log)

The harness dfixed_div is marked noinline in the KERNEL build so the compiler cannot fuse the inner (by-constant) and outer divisions β€” the shipped radeon.ko build happens not to fuse them either (the disassembly proves a real div by the runtime divisor); noinline reproduces exactly that codegen shape without changing any arithmetic.

Why not "reproduced" overall

The DRM ioctl β†’ modeset β†’ bandwidth-update chain requires an RS690/RS740/ RS780/RS880 IGP; the audit guest's only GPU is virtio-vga and /dev/dri does not exist. The panic primitive, the zero-divisor arithmetic, the missing validation, and the trigger plumbing are all individually demonstrated or source-proven; the single missing hop is the hardware. Hence status: inconclusive, reproduced: 0, impact: panic, confidence: likely (certain for the primitive).

Fix validation

fix.diff adds a zero-divisor guard after the clock conversion (bail with the same safe watermark the disabled-CRTC path uses, rs690.c:283-287). Validated at harness level: baseline kernel-flag build panics (trap 18); WITH_FIX build survives all scenarios and unloads cleanly. A full hardware validation would need an RS690-class board.

Exploitability ceiling

DoS (kernel panic) β€” no memory corruption; the write set of the bug is the divide fault only. On multi-user RS780-era machines (780G/785G chipsets were mass-market) with /dev/dri group access this is a one-syscall local kernel panic from any unprivileged user who can open the card.

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 clean

Harness-level fix validation on the guest kernel: the unpatched build of the same verbatim-function KLD panics with Fatal trap 18 (baseline_reproduced=1); the build carrying the fix.diff guards survives all three scenarios with 'DF4582-FIX: zero divisor avoided ... bailing with safe watermark' and unloads cleanly (patched_reproduced=0). Full-hardware validation was not possible (no RS690-class IGP in the guest); the guard is input-independent (checks the computed fixed-point value, not the raw clock), so it covers every wrap multiple and any other zero-producing input.

fix_run.log (guard messages x3 + clean unload) vs panic.txt/run.log (baseline trap 18); fix.diff (guard after the clock->fixed conversion at rs690.c:328, mirroring the disabled-CRTC safe-watermark bail at rs690.c:283-287).
↓ fix.diffper-fix-DF-4582

Confirmed kernel references

Detail

Exploit chain

open /dev/dri/cardN (first opener => DRM master) -> DRM_IOCTL_MODE_SETCRTC with umode.clock=1048576 kHz, sane h/v timings (passes drm_mode_convert_umode ERANGE + drm_mode_validate_basic clock!=0; radeon has no mode_valid) -> crtc helpers commit mode (encoder fixups unconditional, PLL best-effort) -> atombios_crtc_dpms -> radeon_pm_compute_clocks -> radeon_pm_compute_clocks_old PROFILE -> radeon_pm_set_clocks -> radeon_bandwidth_update (radeon_pm.c:312) -> rs690_bandwidth_update (rs690.c:583; asic map radeon_asic.c:727 RS690/RS740, :1138 RS780/RS880) -> rs690_crtc_bandwidth_compute rs690.c:326-329: dfixed_const(1048576)=(u32)(2^20<<12)=0 -> dfixed_div(1000,0) -> do_div by zero (drm_fixed.h:62-70 + math64.h:36-42) -> integer divide fault -> kernel panic.

Evidence (decisive lines)

panic.txt (Fatal trap 18, Stopped at dfixed_div+0xa: divq %rsi,%eax); run.log (decisive kernel run); run.2.log (-O0 userland SIGFPE x3 + codegen notes); objdump_radeon_ko.txt (SHIPPED binary: 0x5ac41 shl $0xc truncating shift, 0x5ac61 div %rcx runtime zero divisor; 0x5adb9 div %r8 for the DF-4583 sideport sink); fix_run.log (WITH_FIX build survives all scenarios with guard messages, clean unload); rs690_div0_body.h (verbatim extraction, byte-identical function body).

PoC changes

No seed PoC existed (pass-1 file was clean). Authored from scratch: verbatim extraction of rs690_crtc_bandwidth_compute + dfixed/do_div helpers into a standalone header; userland driver (fork per scenario) and DragonFly KLD with runtime sysctl scenario selection (df4582.run). Two build-context fixes were required and are documented: (1) do_div/lower_32_bits must be defined before dfixed_div (include order mirrors the kernel build); (2) dfixed_div is noinline in the KERNEL harness build so gcc cannot UB-fuse the inner by-constant division with the outer runtime division β€” the shipped radeon.ko build emits exactly this unfused shape (see objdump_radeon_ko.txt); a first attempt also lost -DDF4582_FIXED because kmod.mk reassigns CFLAGS (Makefile now appends after the include).

Verified recommended fix

rs690.c: after 'a.full = dfixed_div(a, b)' of the pixel clock, bail with the disabled-CRTC safe watermark when a.full == 0 (dfixed_const 32-bit shift wraps clocks >= 2^20 kHz to zero); same guard class for the VBIOS-derived divisors (see DF-4583).

Verdict

Primitive PROVEN on the guest: the byte-identical rs690_crtc_bandwidth_compute() (verbatim extraction, kernel build flags) executed in the live guest kernel with the attacker-shaped mode->clock=1048576 kHz (2^20) takes 'Fatal trap 18: integer divide fault' at dfixed_div+0xa divq β€” guest down (panic.txt, run.log). The SHIPPED /boot/kernel/radeon.ko disassembly independently confirms the reachable runtime division: 0x5ac39-0x5ac61 loads mode->clock, applies the truncating shl $0xc (dfixed_const), strength-reduces only the inner by-constant division, and emits a real 'div %rcx' whose divisor is exactly zero for clock ≑ 0 (mod 2^20). Every hop of the unprivileged chain is source-proven (SETCRTC needs only DRM master = first opener; drm_mode_convert_umode rejects only clock>INT_MAX; drm_mode_validate_basic only clock==0; radeon sets no mode_config.funcs->mode_valid; bandwidth_update is reached from every modeset via the PROFILE pm path). The single missing hop is hardware: the QEMU guest has no RS690/RS740/RS780/RS880 IGP (virtio-vga only, /dev/dri absent), so the end-to-end ioctl chain could not be executed β€” hence inconclusive rather than reproduced. Ceiling = local kernel panic (DoS), no memory corruption.