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)
PoC verification
Evidence pack
findings/poc/DF-4582 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
sys/dev/drm/radeon/rs690.c:326-329c a.full = dfixed_const(mode->clock); /* (u32)(clock << 12) */ b.full = dfixed_const(1000); a.full = dfixed_div(a, b); /* MHz-ish; == 0 when wrapped */ pclk.full = dfixed_div(b, a); /* divisor == 0 -> #DE */sys/dev/drm/include/drm/drm_fixed.h:35dfixed_const(A)=(u32)((A)<<12)β clock = 1048576 (2^20 kHz) β2^20 << 12 = 2^32 β‘ 0 (mod 2^32).sys/dev/drm/include/drm/drm_fixed.h:62-70dfixed_divβdo_div(tmp, B.full)βsys/dev/drm/include/linux/math64.h:36-42__rem = n % __base; n /= __baseβ__base==0βdivqby zero.
Reachability chain (each hop verified in this tree)
- Local user opens /dev/dri/cardN β becomes DRM master (MODE_SETCRTC is
DRM_MASTER|DRM_UNLOCKED, drm_ioctl.c:656). drm_mode_setcrtc(drm_crtc.c:655) βdrm_mode_convert_umode(drm_modes.c:1691): onlyclock > INT_MAXrejected (:1696); our 1048576 passes;drm_mode_validate_driver(:1145) βdrm_mode_validate_basic(:1106) rejects onlyclock == 0(:1114) β 1048576 passes; radeon installs nomode_config.funcs->mode_valid(radeon_display.c:1350-1353), so no range check exists.drm_crtc_check_viewportonly checks fb geometry.- 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. - crtc enable β
radeon_pm_compute_clocks(atombios_crtc.c:296) βradeon_pm_compute_clocks_old(radeon_pm.c:1729): PROFILE method callsradeon_pm_set_clocksunconditionally (:1753-1755). radeon_pm_set_clocksβradeon_bandwidth_update(radeon_pm.c:311-312, guarded only byactive_crtc_count != 0β true after the modeset).rs690_bandwidth_update(rs690.c:583) βrs690_crtc_bandwidth_compute(rs690.c:625) withcrtc->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
fixedHarness-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).
Confirmed kernel references
- sys/dev/drm/radeon/rs690.c:326-329
- sys/dev/drm/radeon/rs690.c:583-648
- sys/dev/drm/include/drm/drm_fixed.h:35
- sys/dev/drm/include/drm/drm_fixed.h:62-70
- sys/dev/drm/include/linux/math64.h:36-42
- sys/dev/drm/drm_crtc.c:655
- sys/dev/drm/drm_modes.c:1114
- sys/dev/drm/drm_modes.c:1696
- sys/dev/drm/radeon/radeon_display.c:1350-1353
- sys/dev/drm/drm_ioctl.c:656
- sys/dev/drm/radeon/radeon_pm.c:311-312
- sys/dev/drm/radeon/radeon_pm.c:1729-1755
- sys/dev/drm/radeon/atombios_crtc.c:296
- sys/dev/drm/radeon/radeon_asic.c:727
- sys/dev/drm/radeon/radeon_asic.c:1138
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.
No comments yet.