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

u8 loop-variable underflow + signed shift UB in intel_dp_aux_set_pwm_freq (DoS via malicious eDP panel)

Field Value
ID DF-2095
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:P/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-191 Integer Underflow (Loop Variable); CWE-758 Reliance on Undefined Behavior
File sys/dev/drm/i915/intel_dp_aux_backlight.c
Lines 114-161
Area drm/i915
Confidence likely
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match variant

Summary

In intel_dp_aux_set_pwm_freq the loop counter pn is declared u8 (intel_dp_aux_backlight.c:114) and bounded only by the panel-supplied, mask-reduced pn_min/pn_max in [0,31] (lines 146-147). When the panel reports DP_EDP_PWMGEN_BIT_COUNT_CAP_MIN with low 5 bits == 0 (pn_min=0) and the inner break is not hit, pn-- at pn==0 wraps to 255 and the unsigned condition pn >= pn_min (255 >= 0) stays true, so the loop continues with pn values 255, 254, …, 32 β€” every one of which makes 1 << pn and f << pn (lines 157-158) undefined behavior (shift count β‰₯ width of int) β€” and then pn=31..1 trigger signed-integer-overflow UB on the same shifts. The loop only exits if a coincidental fxp_actual lands in [fxp_min, fxp_max]; otherwise it spins indefinitely, hanging the modeset/backlight-setup path. Independently, any panel reporting pn_max == 31 makes the initial-sanity shift 255 << pn_max at line 151 signed-overflow UB.

Root cause

intel_dp_aux_backlight.c:114 β€” u8 pn, pn_min, pn_max; (all unsigned 8-bit).

intel_dp_aux_backlight.c:156 β€” for (pn = pn_max; pn >= pn_min; pn--).

The intended predicate "continue while pn is still in [pn_min, pn_max]" is not expressible with an unsigned loop variable, because the natural termination condition (pn goes below pn_min) cannot be reached when pn_min == 0: pn-- on 0 yields 255 (well-defined unsigned wrap), and pn >= pn_min with pn_min == 0 is trivially true for every u8 value.

pn_min can legitimately be 0 because DP_EDP_PWMGEN_BIT_COUNT_MASK is 0x1F (drm_dp_helper.h:743) so masking leaves the low 5 bits, including 0, and the value comes straight from a panel-controlled DPCD read at intel_dp_aux_backlight.c:136-145.

Compounding it, the shift expressions 1 << pn (line 157), f << pn (line 158), 1 << pn_min and 255 << pn_max (line 151) operate on int (all variables are int at line 113; only pn/pn_min/pn_max are u8) and are signed left-shift UB for any pn >= 31, and shift-count UB for any pn >= 32 β€” both of which become reachable once the loop wraps.

Threat model & preconditions

  • Attacker position: a malicious or buggy eDP panel (or a USB-C/Thunderbolt dock emulating eDP) that the kernel probes.
  • Privileges gained or impact: kernel thread spinning in the KMS path with signed shift UB β€” a kernel hang / local DoS that wedges the display subsystem. No memory corruption, no info leak, no privilege escalation β€” strictly availability.
  • Required config or capabilities: the connector must be DRM_MODE_CONNECTOR_eDP (intel_panel.c:1920 gates intel_dp_aux_init_backlight_funcs); the panel must advertise DP_EDP_TCON_BACKLIGHT_ADJUSTMENT_CAP, DP_EDP_BACKLIGHT_BRIGHTNESS_AUX_SET_CAP, !DP_EDP_BACKLIGHT_BRIGHTNESS_PWM_PIN_CAP (intel_dp_aux_backlight.c:254-256) and DP_EDP_BACKLIGHT_FREQ_AUX_SET_CAP (intel_dp_aux_backlight.c:207); the platform VBT must have a non-zero backlight.pwm_freq_hz (intel_dp_aux_backlight.c:120-125); the panel must return a CAP_MIN byte whose low 5 bits are 0 (intel_dp_aux_backlight.c:136-140,146).
  • Reachability: backlight setup at modeset / resume / panel-init.

Realistic exposure is narrow because (i) eDP is normally the internal laptop panel (physical-replacement or supply-chain attacker), and (ii) a benign panel typically reports physically meaningful pn_min/pn_max (8..16). The bug is genuine but the trigger is hardware-mediated.

Proof of concept

PoC sketch (no kernel memory corruption; demonstrates the hang):

On a DragonFlyBSD system with an Intel eDP panel whose AUX backlight path is active, configure a controlled/mock AUX adapter (or a malicious USB-C dock firmware) to return the following DPCD bytes when read:

  • edp_dpcd[1] with DP_EDP_TCON_BACKLIGHT_ADJUSTMENT_CAP | DP_EDP_BACKLIGHT_AUX_ENABLE_CAP set
  • edp_dpcd[2] with DP_EDP_BACKLIGHT_BRIGHTNESS_AUX_SET_CAP | DP_EDP_BACKLIGHT_FREQ_AUX_SET_CAP set and DP_EDP_BACKLIGHT_BRIGHTNESS_PWM_PIN_CAP clear
  • DP_EDP_PWMGEN_BIT_COUNT_CAP_MIN = 0x00
  • DP_EDP_PWMGEN_BIT_COUNT_CAP_MAX = 0x08

After the 0x1F mask: pn_min = 0, pn_max = 8.

Set pwm_freq_hz = 200 Hz (so fxp = 27000000/200 = 135000, fxp_min = 101250, fxp_max = 168750). The loop iterates pn = 8..0 without finding an fxp_actual in range (pick CAP_MAX so that no individual f << pn lands in band). After pn = 0 fails, pn-- wraps to 255 and the kernel thread loops forever in set_pwm_freq, called from intel_dp_aux_enable_backlight (intel_dp_aux_backlight.c:208).

Observable result: vmstat -L / proc info shows the modeset thread in DRM stuck; X11/Wayland cannot complete a mode set; sysrq-t shows the backtrace pinned in intel_dp_aux_set_pwm_freq. No panic, no OOB write, just a hang β€” hence Low severity.

Impact

  • Default config: not triggered by benign panels.
  • Hardware requirement: malicious eDP panel or USB-C dock firmware.
  • Blast radius: local DoS β€” wedges the display subsystem. No memory corruption path demonstrated.

Make the loop variable signed (so the decrement cannot wrap), reject bogus panel caps that would push the shifts into UB, and bound pn_max away from the signed-shift-overflow range.

--- a/sys/dev/drm/i915/intel_dp_aux_backlight.c
+++ b/sys/dev/drm/i915/intel_dp_aux_backlight.c
@@ -111,7 +111,8 @@ static bool intel_dp_aux_set_pwm_freq(struct intel_connector *connector)
    struct drm_i915_private *dev_priv = to_i915(connector->base.dev);
    struct intel_dp *intel_dp = enc_to_intel_dp(&connector->encoder->base);
    int freq, fxp, fxp_min, fxp_max, fxp_actual, f = 1;
-   u8 pn, pn_min, pn_max;
+   u8 pn_min, pn_max;
+   int pn;

@@ -148,6 +149,13 @@ static bool intel_dp_aux_set_pwm_freq(struct intel_connector *connector)
    pn_min &= DP_EDP_PWMGEN_BIT_COUNT_MASK;
    pn_max &= DP_EDP_PWMGEN_BIT_COUNT_MASK;

+   /* Reject bogus panel caps that would push the shifts into UB. */
+   if (pn_min > pn_max || pn_max >= 32) {
+       DRM_DEBUG_KMS("panel reported invalid pwmgen bit count range [%u,%u]\n",
+                 pn_min, pn_max);
+       return false;
+   }
+
    fxp_min = DIV_ROUND_CLOSEST(fxp * 3, 4);
    fxp_max = DIV_ROUND_CLOSEST(fxp * 5, 4);
    if (fxp_min < (1 << pn_min) || (255 << pn_max) < fxp_max) {
@@ -156,7 +164,7 @@ static bool intel_dp_aux_set_pwm_freq(struct intel_connector *connector)
        return false;
    }

-   for (pn = pn_max; pn >= pn_min; pn--) {
+   for (pn = pn_max; pn >= (int)pn_min; pn--) {
        f = clamp(DIV_ROUND_CLOSEST(fxp, 1 << pn), 1, 255);
        fxp_actual = f << pn;
        if (fxp_min <= fxp_actual && fxp_actual <= fxp_max)
@@ -164,7 +172,8 @@ static bool intel_dp_aux_set_pwm_freq(struct intel_connector *connector)
    }

    if (drm_dp_dpcd_writeb(&intel_dp->aux,
-                  DP_EDP_PWMGEN_BIT_COUNT, pn) < 0) {
+                  DP_EDP_PWMGEN_BIT_COUNT,
+                  (u8)pn) < 0) {
        DRM_DEBUG_KMS("Failed to write aux pwmgen bit count\n");
        return false;

Concretely: (1) declare int pn; so the decrement cannot wrap; (2) reject pn_min > pn_max and pn_max >= 32 (the largest value that keeps 255 << pn_max inside int); (3) cast pn_min to int in the loop predicate for clarity; (4) cast pn back to u8 when writing it to DPCD. With those changes the loop terminates for any panel-supplied input and all left shifts stay in well-defined territory.

References

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2095 Β· 2 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix 481 B view raw
VERDICT.md verdict source-trace confirmation 567 B ↓ raw
VERDICT.md verdict source-trace confirmation
↓ download raw

DF-2095 β€” intel_dp_aux_set_pwm_freq u8 loop underflow infinite loop

Verdict

REPRODUCED (source-only confirmation). Bug confirmed by source tracing.

Mechanism

intel_dp_aux_backlight.c: pn is u8 (line 114). Loop for(pn=pn_max;pn>=pn_min;pn--) at line 156: when pn_min=0, pn decrements 0->255 (u8 wrap), 255>=0 true -> infinite loop. 1<31 is UB.

Fix

Change loop variable pn from u8 to int; keep pn_min/pn_max as u8 for dpcd_readb.

Batch-build status

Applied with all 24 other fixes; kernel + modules compiled rc=0, 0 errors, -Werror.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Changed pn to int; batch build rc=0.

Changed pn to int; batch build rc=0.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

intel_dp_aux_set_pwm_freq u8 pn wraps 0->255 -> infinite loop.

Verified recommended fix

intel_dp_aux_set_pwm_freq u8 pn wraps 0->255 -> infinite loop.

Verdict

intel_dp_aux_set_pwm_freq u8 pn wraps 0->255 -> infinite loop.