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:1920gatesintel_dp_aux_init_backlight_funcs); the panel must advertiseDP_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) andDP_EDP_BACKLIGHT_FREQ_AUX_SET_CAP(intel_dp_aux_backlight.c:207); the platform VBT must have a non-zerobacklight.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]withDP_EDP_TCON_BACKLIGHT_ADJUSTMENT_CAP | DP_EDP_BACKLIGHT_AUX_ENABLE_CAPsetedp_dpcd[2]withDP_EDP_BACKLIGHT_BRIGHTNESS_AUX_SET_CAP | DP_EDP_BACKLIGHT_FREQ_AUX_SET_CAPset andDP_EDP_BACKLIGHT_BRIGHTNESS_PWM_PIN_CAPclearDP_EDP_PWMGEN_BIT_COUNT_CAP_MIN = 0x00DP_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.
Recommended fix
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
sys/dev/drm/include/drm/drm_dp_helper.h:743βDP_EDP_PWMGEN_BIT_COUNT_MASK = 0x1F.sys/dev/drm/i915/intel_panel.c:1920β gatesintel_dp_aux_init_backlight_funcstoDRM_MODE_CONNECTOR_eDP.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2095 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix | 481 B | view raw |
| VERDICT.md | verdict | source-trace confirmation | 567 B | β 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<
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
fixedChanged pn to int; batch build rc=0.
Changed pn to int; batch 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.
No comments yet.