Off-by-one bounds guard in intel_get_shared_dpll_id(): one-past-end pointer passes the WARN_ON and yields id == num_shared_dpll (== 7 == I915_NUM_PLLS on ICL)
Summary
The guard 'pll > &shared_dplls[num_shared_dpll]' should be '>=': a pointer exactly equal to the one-past-end element slips through and the subtraction returns num_shared_dpll. On ICL num_shared_dpll==7==I915_NUM_PLLS, i.e. a straight OOB index. Consumers use the id as a register sub-field/address-macro index (DPLL_CTRL1_*_MASK, ICL/CNL_DPLL_CFGCR0). No reachable exploit path today (exhaustive caller audit: every caller passes an in-array pointer or NULL); the defect negates the guard's sole purpose for future callers - if ever fed a one-past-end pointer it mis-indexes DPLL_CTRL1 sub-fields and computes MMIO offsets outside the CFGCR block, feeding wrong-clock state into readback link math (the DF-3810 family consumes exactly those readbacks). Upstream later rewrote it index-based with >=. Info/defense-in-depth; hardware-gated and trigger-less. All other checklist classes killed: PLL ranges platform-bounded, no readback-derived divisors (DSSM forces ref to {19200,24000,38400}), refcount discipline staged-state-safe, all hw_state constructions memsets.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3899 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| fix.diff | β | 702 B | view raw | |
| verdict.json | β | 3.3 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw |
DF-3899 β Off-by-one bounds guard in intel_get_shared_dpll_id()
File: sys/dev/drm/i915/intel_dpll_mgr.c:99-108
Severity: Info (hardening / defense-in-depth β no reachable exploit path today)
Status: untested (Info-tier hardening finding; hardware-gated display path; no PoC
attempted per Phase V skip rule for Low/Info findings)
What the code says
enum intel_dpll_id
intel_get_shared_dpll_id(struct drm_i915_private *dev_priv,
struct intel_shared_dpll *pll)
{
if (WARN_ON(pll < dev_priv->shared_dplls||
pll > &dev_priv->shared_dplls[dev_priv->num_shared_dpll]))
return -1;
return (enum intel_dpll_id) (pll - dev_priv->shared_dplls);
}
The guard uses > instead of >=: a pointer exactly equal to
&shared_dplls[num_shared_dpll] (the one-past-end element) passes the check and
the function returns id == num_shared_dpll. On ICL num_shared_dpll == 7 ==
I915_NUM_PLLS (icl_plls[], intel_dpll_mgr.c:3206-3215), so the returned id is
a straight out-of-bounds index: callers feed it into register sub-field macros
and address macros, e.g.
skl_ddi_clock_get()βDPLL_CTRL1_HDMI_MODE(pll_id)/DPLL_CTRL1_LINK_RATE_MASK(pll_id)shifts (intel_ddi.c:1617-1625)icl_ddi_clock_get()βicl_calc_dp_combo_pll_link(dev_priv, pll_id)βICL_DPLL_CFGCR0(pll_id)(intel_ddi.c:1537-1543, intel_dpll_mgr.c:2564)cnl_ddi_clock_get()βCNL_DPLL_CFGCR0(pll_id)(intel_ddi.c:1563-1565)
Why it is not exploitable today (honest threat)
Every current caller passes a pointer that was either
- taken from inside the array (
intel_get_shared_dpll_by_id()at intel_dpll_mgr.c:84-89;intel_find_shared_dpll()at :256/:276; the pre-allocated pathsibx_get_dpll:437,bxt_get_dpll:1843,icl_get_dpll:2922), or pipe_config->shared_dpll, which is only ever assigned from those in-array pointers or from range-WARN_ON-guarded register readbacks (intel_display.c:9386-9389, 9407-9427, 9464-9467, 9503).
A NULL pointer is caught by the first clause (pll < shared_dplls). The
boundary case (shared_dplls + num_shared_dpll exactly) can only arise from a
future caller bug β which is precisely the situation this guard exists to
catch, and it silently fails to. The sibling accessor
intel_get_shared_dpll_by_id() has no bounds check at all, so this WARN is the
only line of defense on the id-derivation path.
Upstream (Linux 5.x) later rewrote this function index-based with >=:
long pll_idx = pll - dev_priv->shared_dplls;
if (WARN_ON(pll_idx < 0 || pll_idx >= ARRAY_SIZE(dev_priv->shared_dplls)))
return -1;
Fix
fix.diff in this directory (authored against the read-only sys/ tree; never
applied there).
Reproduce
Not runnable: the function is only reached on Intel display hardware during
modeset/clock-readout, and no existing caller supplies an out-of-range pointer
(that is the point of the guard β to protect against future callers). A QEMU
guest without an Intel GPU cannot instantiate intel_shared_dpll_init()
(dev_priv->dpll_mgr stays NULL and intel_get_shared_dpll() bails at
intel_dpll_mgr.c:3295). Verification is therefore source-level only.
VERDICT β DF-3899
Finding: Off-by-one bounds guard in intel_get_shared_dpll_id()
(sys/dev/drm/i915/intel_dpll_mgr.c:103-107)
Verdict: untested β Info-severity hardening defect, source-level certainty (certain), no runtime reproduction attempted or possible on the audit guest.
What was verified, line by line
-
The defect is real and certain. The guard condition at intel_dpll_mgr.c:103-104 is
pll < dev_priv->shared_dplls || pll > &dev_priv->shared_dplls[dev_priv->num_shared_dpll]. A pointer equal to&shared_dplls[num_shared_dpll](exactly one past the last valid element) satisfies neither disjunct, escapes theWARN_ON, and the subtraction at :107 yieldsnum_shared_dpll. On ICL,num_shared_dpll == 7(theicl_plls[]table at :3206-3215 has 7 entries + terminator;dev_priv->shared_dpllsis declaredstruct intel_shared_dpll shared_dplls[I915_NUM_PLLS]withI915_NUM_PLLS == 7at i915_drv.h:1793 / intel_dpll_mgr.h:137).id == 7is out of bounds for bothdev_priv->shared_dplls[]andintel_atomic_state.shared_dpll[7]. -
What the bad id would do downstream. All three consumers use it as a register sub-field index / address-macro index: -
skl_ddi_clock_get()intel_ddi.c:1617-1625:DPLL_CTRL1_HDMI_MODE(pll_id)/DPLL_CTRL1_LINK_RATE_MASK(pll_id)/_SHIFT(pll_id)β id 7 would shift/mask outside the 4Γ6-bit DPLL_CTRL1 layout; -icl_ddi_clock_get()intel_ddi.c:1537-1543 βicl_calc_dp_combo_pll_link()intel_dpll_mgr.c:2555-2565:ICL_DPLL_CFGCR0(7)/ICL_DPLL_CFGCR1(7)β MMIO read at a computed offset outside the CFGCR block; -cnl_ddi_clock_get()intel_ddi.c:1563-1565:CNL_DPLL_CFGCR0(7)β same.
Consequence class: wrong-clock readback / garbage link rate / MMIO reads at
off-block offsets β at minimum wrong display state, at worst a
state-machine corruption feeding later clock math. (The readback-clock math
consumers skl_calc_wrpll_link / cnl_calc_wrpll_link divide by values
read from those registers β the DF-3810 divide-by-zero family β so a
mis-indexed readback plausibly feeds that family too. All speculative past
the boundary defect itself.)
-
Why it is unreachable today. Exhaustive caller audit of
intel_get_shared_dpll_id(rg across sys/dev/drm/i915): - intel_ddi.c:1537, 1563, 1617 β all passpipe_config->shared_dpll. -pipe_config->shared_dpllis assigned ONLY from: a.intel_get_shared_dpll()β platformget_dpllhooks, every one of which returns&dev_priv->shared_dplls[k]for k < num (ibx:437/443, hsw:811/840, skl:1405/1409, bxt:1843, cnl:2378, icl:2922 viaintel_find_shared_dpllranges 0..num-1 orby_idwith in-range ids); b. register-readback paths in intel_display.c that allWARN_ONrange- check the register-derived id BEFOREintel_get_shared_dpll_by_id: :9386 (BXTSKL_DPLL0..SKL_DPLL2), :9407 (ICL combo{0,1}), :9411-9421 (ICL TC ports fixed MG ids 3..6), :9464 (SKLSKL_DPLL0..3), :9477-9495 (HSW switch on PORT_CLK_SEL constants), :9097-9103 (ILK: pipe<2 / PCH_DPLL_SEL β {0,1}). - NULL is caught by thepll < shared_dpllsclause of the guard itself (pointer comparison; NULL < array base). Therefore no existing path can deliver the one-past-end pointer. The bug negates the guard's purpose for future callers β classic defense-in-depth finding. -
Why no guest run. The QEMU audit guest has no Intel display hardware;
intel_shared_dpll_init()never installs adpll_mgr(intel_dpll_mgr.c:3249-3252),intel_get_shared_dpll()returns NULL at :3295, andintel_get_shared_dpll_id()is never invoked. Runtime reproduction would require an Intel i915 + display + a hypothetical buggy future caller. Info-tier, Phase V skipped per contract (status recorded asuntestedto satisfy the poc_results CHECK constraint).
Fix
One-line, mirrors upstream's eventual index-based rewrite. See fix.diff.
No fix-build validation performed (nothing to observe at runtime on the
guest); fix_status: not_testable with this file as evidence.
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_testablefix.diff mirrors upstream's index-based >= guard; no runtime validation possible on the guest (no Intel display hardware; defect has no reachable trigger with current callers), so fix validation is not_testable rather than fixed
findings/poc/DF-3899/fix.diff
Confirmed kernel references
- sys/dev/drm/i915/intel_dpll_mgr.c:99
- sys/dev/drm/i915/intel_dpll_mgr.c:103
- sys/dev/drm/i915/intel_dpll_mgr.c:107
- sys/dev/drm/i915/intel_dpll_mgr.c:3206
- sys/dev/drm/i915/i915_drv.h:1793
- sys/dev/drm/i915/intel_dpll_mgr.h:137
- sys/dev/drm/i915/intel_ddi.c:1537
- sys/dev/drm/i915/intel_ddi.c:1563
- sys/dev/drm/i915/intel_ddi.c:1617
Detail
Evidence (decisive lines)
findings/poc/DF-3899/VERDICT.md (full caller audit), README.md, fix.diff; decisive lines: intel_dpll_mgr.c:103-107 (guard+subtract), i915_drv.h:1793 + intel_dpll_mgr.h:137 (array size 7 == ICL num_shared_dpll), intel_ddi.c:1617-1625 / 1537-1543 / 1563-1565 (id used as register macro index)
PoC changes
no PoC authored: Info-tier hardening finding, hardware-gated (requires Intel i915 display + a hypothetical out-of-range caller); Phase V skip rule for Low/Info findings applied, status recorded as 'untested'
Verified recommended fix
Rewrite the guard index-based: long pll_idx = pll - dev_priv->shared_dplls; WARN_ON(pll_idx < 0 || pll_idx >= dev_priv->num_shared_dpll); return pll_idx;
Verdict
Off-by-one in the bounds guard of intel_get_shared_dpll_id() (intel_dpll_mgr.c:103-107): the '>' comparison lets an exactly-one-past-end pointer through and the function returns id == num_shared_dpll (== 7 == I915_NUM_PLLS on ICL, an out-of-bounds index). All current callers pass in-array or NULL pointers (NULL is caught by the '<' clause), so the defect is unreachable today; it negates the guard's sole purpose of protecting future callers, whose register-macro consumers (DPLL_CTRL1_*_MASK(pll_id), ICL/CNL_DPLL_CFGCR0(pll_id) at intel_ddi.c:1537/1563/1617) would mis-index. Info hardening; no runtime test possible on the QEMU guest (no Intel display HW, dpll_mgr stays NULL at intel_dpll_mgr.c:3295).
No comments yet.