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

NULL-pointer deref of intel_connector->encoder in HPD storm re-enable and poll-init workers

Field Value
ID DF-1832
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-476 NULL Pointer Dereference
File sys/dev/drm/i915/intel_hotplug.c
Lines 233, 566
Area dev/drm/i915 (HPD interrupt/workqueue)
Confidence likely
Discovered 2026-07-20
Reported pending
Known CVE none
CVE match unchecked

Summary

intel_hpd_irq_storm_reenable_work() and i915_hpd_poll_init_work() dereference intel_connector->encoder->hpd_pin without first checking that encoder is non-NULL, unlike the sibling iterators in the same file (intel_hpd_irq_storm_disable at lines 179-181 and i915_hotplug_work_func at line 359). Non-MST connectors briefly have encoder == NULL between drm_connector_init() (which adds them to dev->mode_config.connector_list under connector_list_lock) and intel_connector_attach_encoder(). If either worker iterates the list during that window it takes a kernel NULL page fault β†’ local DoS panic.

Root cause

Two sites in intel_hotplug.c lack the NULL guard that the file's other two iterators implement.

Site 1 β€” intel_hpd_irq_storm_reenable_work (intel_hotplug.c:227-233):

drm_for_each_connector_iter(connector, &conn_iter) {
    struct intel_connector *intel_connector = to_intel_connector(connector);
    /* Don't check MST ports, they don't have pins */
    if (!intel_connector->mst_port &&
        intel_connector->encoder->hpd_pin == pin) {   /* <-- NULL deref */

The MST filter excludes MST connectors (which never get intel_connector->encoder set via intel_connector_attach_encoder), but does NOT exclude non-MST connectors that are mid-init. The worker runs without dev->mode_config.mutex (only irq_lock is held, line 217) so it can race against connector registration.

Site 2 β€” i915_hpd_poll_init_work (intel_hotplug.c:555-566):

if (intel_connector->mst_port)
    continue;
if (!connector->polled && I915_HAS_HOTPLUG(dev_priv) &&
    intel_connector->encoder->hpd_pin > HPD_NONE) {   /* <-- NULL deref */

This one holds mode_config.mutex (line 550), but drm_connector_init / intel_connector_attach_encoder (e.g. in intel_hdmi_init_connector, intel_hdmi.c:2365 vs 2396) do NOT acquire mode_config.mutex, so the race window still exists.

The correct pattern, used by the file's other two iterators:

/* intel_hpd_irq_storm_disable, intel_hotplug.c:179-181 */
intel_encoder = intel_connector->encoder;
if (!intel_encoder)
    continue;
pin = intel_encoder->hpd_pin;
/* i915_hotplug_work_func, intel_hotplug.c:359 */
if (!intel_connector->encoder)
    continue;

Threat model & preconditions

  • Attacker position: local user with physical access to the GPU's HPD signals (via USB-C dock / DP / HDMI adapter), OR root (to manipulate the HPD storm threshold via debugfs).
  • Privileges gained or impact: kernel panic / local DoS. Page 0 is not mappable on DFly; the deref only reads an enum, not a function pointer, so no code-exec primitive was identified.
  • Required config or capabilities: device i915 KMS loaded, Intel gen4+ GPU with has_hotplug=1, plus a window in which a non-MST connector is on connector_list with encoder == NULL (driver reload, MST topology reprobe racing a non-MST port, or runtime suspend/resume of the connector init path).
  • Reachability:
  • storm_reenable worker: scheduled HPD_STORM_REENABLE_DELAY = 2*60*1000 ms after a HPD storm is detected (intel_hotplug.c:114, 202). Triggerable by a faulty/malicious DP/HDMI/USB-C adapter, or via root writing to /sys/kernel/debug/dri/0/i915_hpd_storm_ctl to drop the threshold then issuing DRM ioctls that produce HPD pulses.
  • poll_init worker: no storm precondition. Scheduled by intel_hpd_init (line 526) and intel_hpd_poll_init (line 614); runtime suspend/resume cycles exercise this path. Race is narrower because mode_config.mutex is held.

Proof of concept

PoC source: findings/poc/DF-1832/hpd_panic.c

Build & run

cc hpd_panic.c -o hpd_panic
# Drop the HPD storm threshold (requires root) and hammer DRM ioctls to
# generate HPD detect pulses. Then reload i915 inside the 2-minute
# HPD_STORM_REENABLE_DELAY window so the reenable worker races against
# intel_hdmi_init_connector.
sudo sh -c 'echo 1 > /sys/kernel/debug/dri/0/i915_hpd_storm_ctl'
sudo ./hpd_panic &
sudo kldunload i915 && kldload i915

Expected output

Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x<small offset>
instruction pointer      = 0x..<intel_hpd_irq_storm_reenable_work+...>
...
calltrap() at 0x...
intel_hpd_irq_storm_reenable_work+0x... at 0x...
process_one_work+0x... at 0x...

Impact

Local denial of service via kernel panic on DFly systems running i915 KMS. Triggerable by physical-access attackers (faulty USB-C dock), or root with debugfs write access to i915_hpd_storm_ctl. No confidentiality or integrity impact demonstrated; the dereference only reads an enum offset.

Add the same NULL-encoder guard used by intel_hpd_irq_storm_disable and i915_hotplug_work_func. Two hunks, both pure defensive additions; no behavioural change for fully-initialised connectors.

--- a/sys/dev/drm/i915/intel_hotplug.c
+++ b/sys/dev/drm/i915/intel_hotplug.c
@@ -227,6 +227,9 @@ static void intel_hpd_irq_storm_reenable_work(struct work_struct *work)
        drm_connector_list_iter_begin(dev, &conn_iter);
        drm_for_each_connector_iter(connector, &conn_iter) {
            struct intel_connector *intel_connector = to_intel_connector(connector);
+
+           if (!intel_connector->encoder)
+               continue;
            /* Don't check MST ports, they don't have pins */
            if (!intel_connector->mst_port &&
                intel_connector->encoder->hpd_pin == pin) {
--- a/sys/dev/drm/i915/intel_hotplug.c
+++ b/sys/dev/drm/i915/intel_hotplug.c
@@ -560,6 +560,9 @@ static void i915_hpd_poll_init_work(struct work_struct *work)
        if (intel_connector->mst_port)
            continue;

+       if (!intel_connector->encoder)
+           continue;
+
        if (!connector->polled && I915_HAS_HOTPLUG(dev_priv) &&
            intel_connector->encoder->hpd_pin > HPD_NONE) {

References

  • Sibling NULL-guard pattern in same file: intel_hotplug.c:179-181, 359.
  • Window source: drm_connector_init adds to connector_list (drm_connector.c:257-260) before intel_connector_attach_encoder runs (intel_display.c:16224-16228).
  • Upstream Linux has the same code shape; this is a long-standing latent defect shared with the i915 driver.

Timeline

  • 2026-07-20 Discovered during automated audit.
  • 2026-07-20 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1832 Β· 5 files
FileTypeDescriptionSize
VERDICT.md verdict Source verification narrative 1.2 KB ↓ raw
fix.diff suggested-fix Fix: Add intel_connector->encoder NULL check before dereferencing hpd_pin. 728 B view raw
build.sh build-script Build/validation instructions 366 B view raw
run.sh run-script Run instructions (HW-gated, source-only) 184 B view raw
env.txt environment Guest environment 404 B view raw
VERDICT.md verdict Source verification narrative
↓ download raw

DF-1832 - Source Verification

Verdict: REPRODUCED (source-only confirmation)

Finding: sys/dev/drm/i915/intel_hotplug.c:233,566

Mechanism: intel_hpd_irq_storm_reenable_work (L233) and i915_hpd_poll_init_work (L566) deref intel_connector->encoder->hpd_pin without NULL guard that sibling functions implement. NULL page fault if encoder==NULL during connector init window.

Hardware dependency: Requires i915 GPU with hotplug storm or poll-init race.

Fix: Add intel_connector->encoder NULL check before dereferencing hpd_pin.

Verification method

Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β€” the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.

Fix validation

fix.diff authored and applied to guest source. All 40 fixes in this batch compile cleanly in a single combined kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, zero -Werror violations.

Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.

Batch build: 40 fix.diffs applied, make nativekernel β†’ rc=0 -Werror. Bug at sys/dev/drm/i915/intel_hotplug.c:233,566 source-confirmed.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source trace sys/dev/drm/i915/intel_hotplug.c:233,566. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.

PoC changes

Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: HPD reenable/poll-init deref encoder without NULL guard. Add encoder NULL check.

Verified recommended fix

See fix.diff. HPD reenable/poll-init deref encoder without NULL guard. Add encoder NULL check.

Verdict

REPRODUCED (source-only). sys/dev/drm/i915/intel_hotplug.c:233,566: HPD reenable/poll-init deref encoder without NULL guard. Add encoder NULL check.