Divide-by-zero (kernel panic) in drm_cvt_mode/drm_gtf_mode_complex via unvalidated cmdline @refresh β simple_strtol value wraps through unsigned int into int vfieldrate
Summary
The cmdline parser parses '@<refresh>'/'-<bpp>' with simple_strtol into unsigned int locals and stores into int fields with no range check. '@2147483648' becomes vrefresh=INT_MIN; vfieldrate wraps the hperiod/hfreq products to 0 mod 2^32: CVT non-RB traps, CVT-RB traps, GTF traps; '@134217728' (in-range!) wraps hfreq_est to 0 and traps. In-kernel #DE -> panic during early connector probe from a video= boot parameter - persistent boot brick until the param is removed. PROVEN on the guest with verbatim extracts of the kernel functions: SIGFPE exit=136 immediately at the faulting lines, -O2 and -O0; dbg prints the wrapped divisors as 0. Root/bootloader-gated. Range-check-at-intake + vfieldrate-bound fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4224 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 15.3 KB | view raw | |
| dbg.c | β | 931 B | view raw | |
| fixdemo.c | β | 829 B | view raw | |
| build.sh | β | 197 B | view raw | |
| run.sh | β | 125 B | view raw | |
| build.log | β | 1022 B | view raw | |
| run.log | β | 2.1 KB | view raw | |
| run.2.log | β | 1.9 KB | view raw | |
| run.O0.log | β | 1.2 KB | view raw | |
| fixdemo.log | β | 295 B | view raw | |
| env.txt | β | 320 B | view raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| fix.diff | β | 2.2 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-4224 β VERDICT
Status: untested (kernel path) / arithmetic PROVEN on guest β see below. Impact ceiling: kernel panic (DoS). No memory-corruption primitive.
Reproduced how far, and why no further
The QEMU guest has no DRM device: pciconf -l shows only
vgapci0 chip=0x11111234 (Bochs std VGA) and /dev/dri does not exist. The
vulnerable code runs inside drm_connector_get_cmdline_mode()
(sys/dev/drm/drm_connector.c:131-132) which is only called when a DRM driver
registers connectors β never on this guest. Booting the guest with
video=640x480M@2147483648 would therefore exercise nothing, so a
boot-param test on this VM is impossible (not merely inconvenient).
What was verified, on the guest itself (DF cc 8.3 [DragonFly], x86-64,
identical integer ABI to the guest kernel):
- Intake semantics β
strtol("2147483648")β long 2147483648 β(unsigned int)0x80000000 β(int)β2147483648 exactly mirrors drm_modes.c:1442/1465 + drm_connector.h:813 (run.log"parse simulation" line). - Verbatim function arithmetic β
harness.cembeds the untouched arithmetic ofdrm_cvt_mode()(drm_modes.c:143-341) anddrm_gtf_mode_complex()(drm_modes.c:365-529). With the attack values: - case 1 (video=640x480M@2147483648): SIGFPE exit=136 immediately after thedrm_modes.c:243marker (both -O2, -O0). - case 2 (video=640x480MR@2147483648): SIGFPE exit=136 at:302(-O2). - case 3 (video=640x479@2147483648): SIGFPE exit=136 at:459(-O0; at -O2 GCC reschedules the two independent divisions and the trap surfaces at :481 β both divisors are 0, seedbg.coutput). - case 4 (video=640x601@134217728, refresh in int range!): SIGFPE exit=136 at:481(hfreq_est wrapped to 0). -dbgprints the actual divisors:den243=0,den459=0,hfreq_est=0. A userspace SIGFPE is the same #DE exception the kernel takes at the same instruction; in-kernel it is an unhandled divide error β panic. - Fix behaviour β
fixdemoshows layer (a) of fix.diff rejecting 2147483648/4294967295/-5 and layer (b) rejecting 134217728, while 60/500 pass.
Chain (on real hardware)
video=640x480M@2147483648 (kernel cmdline / fb_mode_option) β
fb_get_options β drm_connector.c:132 drm_mode_parse_command_line_for_connector
(drm_modes.c:1465 refresh=2^31 unvalidated) β connector probe β
drm_probe_helper.c:162 drm_mode_create_from_cmdline_mode β drm_modes.c:1600
drm_cvt_mode(vrefresh=INT_MIN) β drm_modes.c:243 hperiod = tmp1*2 /
(tmp2*vfieldrate) with tmp2*vfieldrate == 0 mod 2^32 β #DE β panic
during early boot, before console takeover.
Notes
- The
@134217728variant (case 4) defeats any "just cap at INT_MAX" fix because the value is in range; that is why fix.diff adds the vfieldrate sanity bound in cvt/gtf themselves. -<bpp>intake (drm_modes.c:1474) has the same missing range check; huge bpp flows into fbdev allocation math (out of scope for this finding, noted in the audit result).- Attempts: 2 (first harness iteration compiled the inputs as constants, and GCC legitimately deleted the always-UB divisions; inputs were moved to argv so the compiler cannot fold them, exactly as in the kernel where the value is parsed at runtime).
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_testablePatched-kernel boot test impossible on this guest (no DRM hardware to run the parse path). fix.diff logic demonstrated instead: fixdemo (fixdemo.log) shows layer (a) rejecting 2147483648/4294967295/-5 and layer (b) rejecting 134217728, while legitimate 60/500 pass; both trapping sites become unreachable because vfieldrate can no longer wrap.
fixdemo.log (guest run); fix.diff
Confirmed kernel references
Detail
Exploit chain
video=640x480M@2147483648 (kernel cmdline / fb_mode_option) -> fb_get_options -> drm_connector.c:132 drm_mode_parse_command_line_for_connector (refresh unvalidated) -> drm_probe_helper.c:162 drm_mode_create_from_cmdline_mode -> drm_modes.c:1600 drm_cvt_mode(vrefresh=INT_MIN) -> drm_modes.c:243 division by 0 -> #DE -> boot-time kernel panic (persistent until param removed). Variants: MR->:302/:304, GTF odd vdisplay->:459, @134217728->:481.
Evidence (decisive lines)
run.log: marker 'drm_modes.c:243' followed by 'Floating point exception (core dumped)' exit=136 (case1 -O2); run.2.log/run.O0.log for :302/:459/:481; dbg.log 'den243=0', 'den459=0', 'hfreq_est=0'; fixdemo.log shows fix.diff layers rejecting all poisoned refresh values; env.txt documents absence of DRM device.
PoC changes
self-authored; inputs taken from argv after first iteration compiled them as constants and GCC legitimately deleted the always-UB divisions (kernel values arrive at runtime, so the fold was an artifact)
Verified recommended fix
Range-check the simple_strtol results for @refresh/-bpp at drm_modes.c:1465/1474 (reject <0 or >INT_MAX) and add a vfieldrate sanity bound (reject 0 or >1000) at the top of drm_cvt_mode/drm_gtf_mode_complex.
Verdict
Kernel path untestable on this guest (no DRM device: Bochs VGA 0x1234:1111, /dev/dri absent), but the vulnerable arithmetic is proven on the guest's own toolchain: verbatim extracts of drm_cvt_mode()/drm_gtf_mode_complex() fed the exact intake value produced by simple_strtol("2147483648") (u32 0x80000000 -> int INT_MIN, matching drm_modes.c:1465 + drm_connector.h:813) raise SIGFPE (exit=136) at drm_modes.c:243, :302, :459 and :481 for the four trigger variants; dbg.c prints the wrapped divisors as 0. In-kernel the same idiv is #DE -> panic during early connector probe from a video= boot parameter. No memory corruption; boot-cmdline-gated DoS.
No comments yet.