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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4224 Β· 14 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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):

  1. 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).
  2. Verbatim function arithmetic β€” harness.c embeds the untouched arithmetic of drm_cvt_mode() (drm_modes.c:143-341) and drm_gtf_mode_complex() (drm_modes.c:365-529). With the attack values: - case 1 (video=640x480M@2147483648): SIGFPE exit=136 immediately after the drm_modes.c:243 marker (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, see dbg.c output). - case 4 (video=640x601@134217728, refresh in int range!): SIGFPE exit=136 at :481 (hfreq_est wrapped to 0). - dbg prints 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.
  3. Fix behaviour β€” fixdemo shows 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 @134217728 variant (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_testable
baseline no→ patch + rebuild →patched clean

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

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.