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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3899 Β· 5 files
FileTypeDescriptionSize
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 paths ibx_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.md
↓ download raw

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

  1. 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 the WARN_ON, and the subtraction at :107 yields num_shared_dpll. On ICL, num_shared_dpll == 7 (the icl_plls[] table at :3206-3215 has 7 entries + terminator; dev_priv->shared_dplls is declared struct intel_shared_dpll shared_dplls[I915_NUM_PLLS] with I915_NUM_PLLS == 7 at i915_drv.h:1793 / intel_dpll_mgr.h:137). id == 7 is out of bounds for both dev_priv->shared_dplls[] and intel_atomic_state.shared_dpll[7].

  2. 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.)

  1. 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 pass pipe_config->shared_dpll. - pipe_config->shared_dpll is assigned ONLY from: a. intel_get_shared_dpll() β†’ platform get_dpll hooks, 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 via intel_find_shared_dpll ranges 0..num-1 or by_id with in-range ids); b. register-readback paths in intel_display.c that all WARN_ON range- check the register-derived id BEFORE intel_get_shared_dpll_by_id: :9386 (BXT SKL_DPLL0..SKL_DPLL2), :9407 (ICL combo {0,1}), :9411-9421 (ICL TC ports fixed MG ids 3..6), :9464 (SKL SKL_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 the pll < shared_dplls clause 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.

  2. Why no guest run. The QEMU audit guest has no Intel display hardware; intel_shared_dpll_init() never installs a dpll_mgr (intel_dpll_mgr.c:3249-3252), intel_get_shared_dpll() returns NULL at :3295, and intel_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 as untested to 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_testable
baseline no→ patch + rebuild →patched clean

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

Confirmed kernel references

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).