Divide-by-zero kernel panic in drm_gtf_mode_complex() via unvalidated secondary-GTF parameters from crafted EDID
Summary
drm_mode_std()'s LEVEL_GTF2 branch passes GTF_M/2C/K/2J decoded verbatim from an EDID monitor-range descriptor into drm_gtf_mode_complex(). With r[14]=r[15]=0 (GTF_M=0, so GTF_M_PRIME=0) and r[13]=r[17]=200 (GTF_C_PRIME=100), ideal_duty_cycle becomes exactly 100000 and the blanking computation divides by (100000 - ideal_duty_cycle) == 0 -> x86 #DE -> kernel panic. Deterministic; base-block-only EDID (no extension needed); reachable from any connector EDID probe. Physical/adjacent attacker (same vector as DF-1165: $5 DP dongle, KVM, USB-C dock, or hypervisor host) supplies a checksum-valid 128-byte EDID whose range-limit descriptor declares a secondary GTF curve with degenerate parameters; the next mode probe panics the kernel - reliable repeatable DoS of any DragonFly machine with a DRM GPU. VERIFIED on the guest by compiling a MECHANICAL byte-for-byte extraction of drm_gtf_mode_complex from the audit tree (guest /usr/src md5-identical) driven solely with the EDID-decoded values: SIGFPE 3/3 runs. In-kernel trigger not executable on this virtio-only guest - honestly not_reproduced/impact=panic. The identical formula is still present in upstream Linux v5.2 and v6.6 (no known CVE). Fix: guard ideal_duty_cycle bounds (both call sites tolerate NULL). Also killed this pass: all CTA/DisplayID/VSDB walkers re-derived, drm_do_get_edid repair loop, ELD SAD copies, mode-builder divisions; full-file diff vs upstream v5.2 shows only DFly porting shims.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3682 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| gtf_body.inc | β | 5.9 KB | β download | |
| gtf_kernel_extract.c | β | 2.9 KB | view raw | |
| gtf_fix_test.c | β | 2.1 KB | view raw | |
| gtf_body_fixed.inc | β | 6.2 KB | β download | |
| gtf_div0.c | β | 4.6 KB | view raw | |
| gen_edid.py | β | 3.5 KB | view raw | |
| evil_edid.bin | β | 128 B | β download | |
| build.sh | β | 208 B | view raw | |
| run.sh | β | 112 B | view raw | |
| build.log | β | 43 B | view raw | |
| run.log | β | 570 B | view raw | |
| run.2.log | β | 570 B | view raw | |
| env.txt | β | 431 B | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-3682 β Divide-by-zero (kernel panic) in drm_gtf_mode_complex() via crafted-EDID secondary-GTF parameters
What
sys/dev/drm/drm_modes.c:484-485 (reached from sys/dev/drm/drm_edid.c:2188-2193):
ideal_duty_cycle = GTF_C_PRIME * 1000 - (GTF_M_PRIME * 1000000 / hfreq_est);
hblank = total_active_pixels * ideal_duty_cycle / (100000 - ideal_duty_cycle);
GTF_M, GTF_2C, GTF_K, GTF_2J are copied unvalidated from a
sink-supplied EDID monitor-range descriptor (bytes r[13..17]) by
drm_gtf2_m/2c/k/2j() (drm_edid.c:2026-2055). With
r[14]=r[15]=0βGTF_M = 0βGTF_M_PRIME = 0r[13]=200, r[17]=200βGTF_C_PRIME = ((200-200)*K/256 + 200)/2 = 100
ideal_duty_cycle becomes exactly 100000, the divisor
100000 - ideal_duty_cycle becomes zero, and the kernel takes a
divide error (#DE on x86) β panic. Deterministic, base-block-only EDID.
Reachability (line-level trace)
- Sink supplies crafted EDID over DDC/AUX (
drm_do_get_edid). drm_add_edid_modes(drm_edid.c:4729) βadd_standard_modes(4770).drm_mode_std(drm_edid.c:2095): std-timing pair{208, 0x00}decodes to 1912x1195@60;standard_timing_level()(2061) returnsLEVEL_GTF2because a DTD withr[3]==0xFD && r[10]==0x02exists (revβ₯2, feature bit DRM_EDID_FEATURE_DEFAULT_GTF clear).case LEVEL_GTF2(drm_edid.c:2177-2194): firstdrm_gtf_mode()succeeds;drm_mode_hsync(mode) (~75 kHz) > drm_gtf2_hbreak() (=2, r[12]=1)βdrm_gtf_mode_complex(dev, 1912, 1195, 60, 0, 0, M=0, 2C=200, K=128, 2J=200).drm_modes.c:480-485:C'=100, M'=0 β ideal_duty_cycle == 100000β/ 0β panic.
No DMT hit (1912x1195 is not a DMT mode), no dedup conflict (DTD1 carries
pixel_clock=0 so no detailed modes were added first), bad_std_timing()
does not match β every precondition is satisfiable with a 128-byte
checksum-valid base block (evil_edid.bin).
Reproduce (on the DragonFly guest)
The QEMU guest is virtio-only (/dev/dri absent, no drm kld, no GPU), so
the in-kernel trigger cannot be executed there. What is executed is the
verbatim kernel function: gtf_body.inc is a mechanical byte-for-byte
extraction of drm_gtf_mode_complex() from sys/dev/drm/drm_modes.c
(git 6cc80ee9; guest /usr/src copy md5-identical:
ce51a6174062d43b24b7296cc0b3bc1e). Only struct drm_device /
struct drm_display_mode and drm_mode_create()/drm_mode_set_name() are
stubbed; every arithmetic statement is unmodified.
./dfbsd-qemu/vm.sh run_root 'sh /tmp/build.sh' # cc -O2 both harnesses ./dfbsd-qemu/vm.sh run_root 'sh /tmp/run.sh' # baseline + 3x + fix test
Expected (observed):
baseline exit=136 (128+SIGFPE β integer divide trap; kernel: #DE β panic)
run1..run3 exit=136 (deterministic)
fix test exit=0 ("returned NULL (rejected bogus GTF2 params) -> fix OK")
Files
| file | role |
|---|---|
gtf_body.inc |
verbatim extraction of drm_gtf_mode_complex from the audit tree |
gtf_kernel_extract.c |
harness: EDID byte decoding + verbatim function (baseline trap) |
gtf_fix_test.c / gtf_body_fixed.inc |
same, with the proposed fix guard (no trap) |
gen_edid.py β evil_edid.bin |
the malicious 128-byte EDID base block (valid checksum) |
gtf_div0.c |
independent re-derivation harness (same trap; early confirm) |
build.sh / run.sh |
exact commands run on the guest |
build.log / run.log |
full untrimmed guest output |
env.txt |
guest environment + source-identity proof |
fix.diff |
the fix (guard ideal_duty_cycle), never applied to sys/ |
Fix
See fix.diff β reject degenerate GTF2 parameters before the division
(ideal_duty_cycle <= 0 || ideal_duty_cycle >= 100000 β destroy mode,
return NULL; both call sites already handle NULL).
DF-3682 β VERDICT
Finding: divide-by-zero in drm_gtf_mode_complex() (sys/dev/drm/drm_modes.c:484-485)
via unvalidated secondary-GTF parameters taken from a crafted EDID
(sys/dev/drm/drm_edid.c:2188-2193, parameters decoded by
drm_gtf2_m/2c/k/2j() at drm_edid.c:2026-2055).
Classification: not_reproduced (kernel-level), reproduced=0,
impact=panic, confidence=certain for the arithmetic fault and the
reachability trace.
Was it reproduced?
- In-kernel trigger: NO β impossible on this guest. The QEMU guest is
virtio-only: no
/dev/dri, nodrmkld,/boot/modulesabsent, and this tree has no virtio-gpu driver, so no DRM connector ever callsdrm_get_edid()/drm_add_edid_modes(). The physical attack vector (malicious monitor / DP dongle / KVM feeding DDC) does not exist in the VM. - The fault itself: YES, deterministically, on the verbatim kernel code.
gtf_body.incis a mechanical (pythonstr.index+ brace-matching, no hand-retyping) byte-for-byte extraction ofdrm_gtf_mode_complex()fromsys/dev/drm/drm_modes.cat git6cc80ee9. The guest's/usr/src/sys/dev/drm/drm_modes.cis md5-identical (ce51a6174062d43b24b7296cc0b3bc1e). Compiled with the guest cc at -O2 and fed only values decoded from the crafted EDID bytes (evil_edid.bin: range descriptorr[10]=0x02, r[12]=1, r[13]=200, r[14]=r[15]=0, r[16]=128, r[17]=200, std timing{208, 0x00}), the function takes an integer-divide trap: exit 136 (SIGFPE), 3/3 runs. In kernel context the identical instruction sequence raises #DE β DragonFlytrap()β panic.
Why the arithmetic is airtight
GTF_M = 0(r[15]=r[14]=0) βGTF_M_PRIME = K*0/256 = 0.GTF_2C = GTF_2J = 200βGTF_C_PRIME = ((200-200)*128/256 + 200)/2 = 100.ideal_duty_cycle = 100*1000 - 0 = 100000.- Divisor
100000 - ideal_duty_cycle == 0β #DE. No overflow trickery needed; every intermediate is small and positive.
Reachability trace (why the crafted EDID reaches the call)
drm_do_get_edidaccepts the block: header valid, checksum valid (gen_edid.py enforcessum(edid) & 0xFF == 0), version 1 rev 4.drm_add_edid_modes(drm_edid.c:4729) βadd_detailed_modesfirst: DTD1 is the range descriptor withpixel_clock = r[0..1] = 0, sodo_detailed_mode(2818:if (timing->pixel_clock)) adds nothing β the probed-mode dedup list indrm_mode_std(2142) stays empty.add_standard_modes(4770) βdrm_mode_stdon std timing[0]{208, 0x00}:bad_std_timingno; hsize = 2088+248 = 1912; aspect 00 + revβ₯3 β vsize = 191210/16 = 1195; vrefresh 60.standard_timing_level(2061): rev 4 β₯ 2;features & DEFAULT_GTFis clear (byte 0x1A = 0), so CVT not chosen;drm_gtf2_hbreak()= 2 β 0 (DTD1 r[3]=0xFD, r[10]=0x02, r[12]=1) β LEVEL_GTF2.drm_monitor_supports_rbfalse (r[15]=0 β is_rb bit clear); 1912x1195@60 not in DMT β falls intocase LEVEL_GTF2(2177).drm_gtf_mode(1912,1195,60)OK;drm_mode_hsync(mode) β 75 kHz > 2(2186) βdrm_gtf_mode_complex(..., M=0, 2C=200, K=128, 2J=200)(2188) β panic at drm_modes.c:484-485.
Exploit chain
Not an escalation primitive: deterministic DoS/panic (sink-driven, the DF-1165 physical threat model β a $5 DP dongle, KVM, dock, or a VM host feeding DDC). No userβroot route exists for a divide error.
Fix validation
fix.diffguards the divisor (ideal_duty_cycle <= 0 || >= 100000β destroy mode, return NULL; both call sites βdrm_mode_std(drm_edid.c:2194 β returns NULL β caller 2720 checks) and the exported API β tolerate NULL).- Harness-level validation executed on the guest:
gtf_fix_test.cuses the same verbatim body with the guard applied β no trap, returns NULL, exit 0 (run.log, "fix test" section). - A full fixed-kernel build could not be meaningfully validated on this
guest because the baseline kernel trigger itself is untestable there
(no DRM device) β
fix_status: not_testableat kernel level; the guard's effect is proven at the extracted-function level.
Upstream-shared status
The identical formula (same unguarded division, same unvalidated GTF2
parameters) exists in upstream Linux v5.2 and is still present in v6.6
(drivers/gpu/drm/drm_modes.c β verified against
raw.githubusercontent.com/torvalds/linux). No CVE known to exist for it.
This tree's drm_edid.c/drm_modes.c are upstream-verbatim in this region
(full-file diff against v5.2 shows only DFly porting shims).
Artifacts/evidence
run.logβ decisive: baseline exit 136, 3/3, fix test exit 0.build.logβ full untrimmed compiler output.evil_edid.bin+gen_edid.pyβ the malicious sink payload.env.txtβ guest uname, /dev/dri absence, md5 source-identity proof.
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_testableKernel-level fix validation impossible on this guest (baseline in-kernel trigger itself untestable: no DRM device). Guard logic validated at the extracted-function level: gtf_fix_test.c (same verbatim body + fix.diff guard) returns NULL with no trap, exit 0, while the unguarded baseline traps 3/3.
findings/poc/DF-3682/run.log ('fix test' section), findings/poc/DF-3682/gtf_body_fixed.inc, findings/poc/DF-3682/fix.diff
Confirmed kernel references
Detail
Exploit chain
crafted EDID base block (range descriptor flags=0x02 with degenerate GTF2 params + ordinary standard timing) -> DDC/AUX read -> drm_do_get_edid -> drm_add_edid_modes -> add_standard_modes -> drm_mode_std LEVEL_GTF2 -> drm_gtf_mode_complex -> hblank division by zero -> kernel panic. No escalation route (pure DoS).
Evidence (decisive lines)
run.log: baseline exit=136 3/3 (SIGFPE) + fix test exit=0; gtf_body.inc = verbatim extraction (md5-proven identical source on guest); evil_edid.bin/gen_edid.py = malicious sink payload with valid checksum; env.txt = guest has no /dev/dri, no drm kld; fix.diff = divisor guard validated at extracted-function level (gtf_fix_test.c)
PoC changes
No runnable kernel path exists on the guest (no DRM device); PoC pivoted to a mechanical verbatim-extraction harness compiled from the tree's own drm_gtf_mode_complex body, driven purely by EDID-decoded values; fix validated on the same extracted body.
Verified recommended fix
Guard the blanking computation: reject ideal_duty_cycle <= 0 || >= 100000 in drm_gtf_mode_complex (destroy mode, return NULL); both call sites already handle NULL.
Verdict
Divide-by-zero in drm_gtf_mode_complex (sys/dev/drm/drm_modes.c:484-485): GTF_M/GTF_2C/GTF_K/GTF_2J are taken unvalidated from a sink-supplied EDID range-limit descriptor (drm_edid.c:2188 via drm_gtf2_*, drm_edid.c:2026-2055). With M=0 (r[15]=r[14]=0) and 2C=2J=200 (r[13]=r[17]=200), C'=100 and M'=0 make ideal_duty_cycle exactly 100000, so the divisor (100000 - ideal_duty_cycle) is zero -> kernel #DE -> panic. The in-kernel trigger could not be executed on this guest (virtio-only: no /dev/dri, no drm kld, no GPU, no virtio-gpu driver in the tree), so the run is classified not_reproduced; however the fault itself is proven deterministically by compiling a MECHANICAL byte-for-byte extraction of the kernel function from the audit tree (guest /usr/src copy md5-identical) and feeding it only values decoded from the crafted 128-byte checksum-valid EDID (evil_edid.bin): SIGFPE exit 136, 3/3 runs. Reachability of the call in-kernel is proven by line-level trace (README.md/VERDICT.md steps 1-6): base-block-only EDID, no extensions needed, sink-driven physical vector identical to DF-1165's threat model. The identical formula is still present in upstream Linux v6.6 (no known CVE).
No comments yet.