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

Unbounded sink-driven go_again retry loop in intel_dp_check_mst_status livelocks the DP hotplug workqueue (MST HPD DoS)

Summary

intel_dp_check_mst_status() re-reads the sink's ESI interrupt block and jumps back to 'go_again' whenever drm_dp_mst_hpd_irq() reports the event handled. 'handled' is set exclusively from sink-supplied bytes (esi[0] sink-count != cached, esi[1] & DOWN_REP_MSG_RDY, esi[1] & UP_REQ_MSG_RDY). The clear-write of esi[1..3] back to DP_SINK_COUNT_ESI+1 is a request the sink may ignore. A malicious MST-capable branch device therefore keeps the loop spinning forever in the hotplug dp_wq kthread - permanent livelock, the intel_display_power_get() reference taken at :5684 is never released (display power well wedged, runtime PM stuck), all subsequent DP/HDMI hotplug processing on all ports starves, one core pegged. enable_dp_mst defaults to true; code ships in stock /boot/kernel/i915.ko (nm: intel_dp_check_mst_status at 0xcbd80). Attacker with brief physical access plugs an evil MST dock/monitor (or flashes a real dock's branch firmware). Hardware-gated: virtio-only guest cannot execute the path; source-certain.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3626 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.8 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
fix.diff β€” 1.3 KB view raw
verdict.json β€” 3.7 KB view raw
manifest.json β€” 855 B view raw
env.txt β€” 245 B view raw

DF-3626 β€” Unbounded sink-driven retry loop in intel_dp_check_mst_status (MST HPD livelock)

  • File: sys/dev/drm/i915/intel_dp.c:4294-4323 (loop body; label go_again at 4294)
  • Class: CWE-835 (loop with unreachable exit condition driven by untrusted data)
  • Severity: Medium (kernel-thread livelock / display-subsystem DoS; attacker = malicious MST-capable DisplayPort branch device β€” "evil dock" / "evil monitor", physical plug-in)
  • Status on this audit's QEMU guest: inconclusive β€” the guest is virtio-only (vgapci0@pci0:0:2:0 chip=0x11111234), the i915 driver can never PCI-probe-attach, so no DP aux/MST path can execute. Source-level trace below is line-accurate and the vulnerable function is present in the shipped /boot/kernel/i915.ko (nm β†’ 00000000000cbd80 t intel_dp_check_mst_status; strings contain got esi %3ph / got esi2 %3ph from the loop body).

What the code does

intel_dp_hpd_pulse() (intel_dp.c:5656) is invoked for short HPD pulses from intel_hotplug.c dig_port_work on dev_priv->hotplug.dp_wq (intel_hotplug.c:496-497). For an MST-active port it calls intel_dp_check_mst_status() (intel_dp.c:5687), which:

  1. reads the 14-byte ESI block from the sink (intel_dp_get_sink_irq_esi, intel_dp.c:4293);
  2. drm_dp_mst_hpd_irq() (drm_dp_mst_topology.c:2450) sets *handled = true whenever esi[0] & 0x3f != mgr->sink_count, or esi[1] & DP_DOWN_REP_MSG_RDY, or esi[1] & DP_UP_REQ_MSG_RDY;
  3. if handled, the driver writes esi[1..3] back to DP_SINK_COUNT_ESI+1 to clear the interrupt bits (intel_dp.c:4309-4317), re-reads the ESI, and β€” if the read succeeds β€” goto go_again (intel_dp.c:4319-4323).

There is no iteration bound on the go_again loop. A branch device that simply ignores the clear-write and keeps returning the same ESI payload (or toggles the 6-bit sink-count field each read) makes handled true on every pass β†’ infinite loop.

Consequences (source-level)

  • The dp_wq kthread spins forever inside intel_dp_check_mst_status() (each iteration performs aux reads/writes + drm_dp_mst_handle_down_rep() sideband processing β€” continuous work, not a sleeping stall).
  • intel_display_power_get(dev_priv, intel_dp->aux_power_domain) taken by the caller at intel_dp.c:5684 is never released β†’ the AUX/PPS display power well refcount is leaked permanently β†’ runtime PM of the display block wedged.
  • All subsequent hotplug events queued on dev_priv->hotplug.dp_wq (every DP/HDMI hotplug on all ports) starve β†’ display subsystem unusable until reboot.
  • CPU: one core pegged in the workqueue thread.

Trigger (attacker behavior spec β€” no host-side "program" exists for this class)

A malicious DP sink is hardware. The minimal evil device implements:

  1. DPCD 0x000 = rev 0x12, DP_MSTM_CAP (0x0011) = DP_MST_CAP|UPSTREAM_IS_SRC, downstream-port-present bit set β†’ i915 enters MST mode (intel_dp_can_mst(), intel_dp.c:4036).
  2. Assert a short HPD pulse.
  3. Answer every native AUX read of DP_SINK_COUNT_ESI (0x2002..0x200f) with esi[1] |= DP_DOWN_REP_MSG_RDY (0x02) and never clear it regardless of any write to 0x2003-0x2005.

Same effect with less hardware: alternate the low 6 bits of esi[0] (sink count) between two values on successive reads.

This is implementable today on an FPGA DP source/sink board or by patching a cheap USB-C MST dock's branch firmware β€” the standard "evil dock" threat model for DP parsing bugs.

Fix

Bound the number of go_again bounces (see fix.diff): after N handled re-reads (e.g. 8), break out and let the normal HPD/reprobe machinery take over. Also drm_kms_helper_hotplug_event() so userspace sees the churn.

Expected post-fix behavior

The loop terminates after at most N iterations; the worst a malicious sink achieves is N wasted aux transactions per HPD pulse.

VERDICT.md
↓ download raw

VERDICT β€” DF-3626: intel_dp_check_mst_status() unbounded go_again loop

verdict: inconclusive (runtime) / certain (source-level) β€” reproduced=0, impact=none (nothing demonstrable on this guest), expected impact on real HSW+ hardware with an attached hostile MST branch device: DoS (kthread livelock + permanent power-well reference + display hotplug starvation).

Why runtime verification was not possible

The audit guest is QEMU/KVM with a virtio display device:

$ pciconf -l | grep -i -E "vgapci|display"
vgapci0@pci0:0:2:0:  class=0x030000 card=0x11001af4 chip=0x11111234 rev=0x02 hdr=0x00
$ kldstat | grep drm      β†’ (nothing)
$ ls /dev/dri             β†’ NO_DEV_DRI

chip 0x1111:1234 is the QEMU/virtio display adapter. i915.ko's PCI ID table (i915_pciids.h β†’ i915_pci_register) matches only Intel vendor 0x8086 IDs, so the driver never attaches; intel_dp_hpd_pulse() can never run. No amount of setup inside the guest substitutes for Intel display hardware plus a hostile DP branch device on the physical connector.

Proof the vulnerable code ships in the default install

$ nm /boot/kernel/i915.ko | grep intel_dp_check_mst_status
00000000000cbd80 t intel_dp_check_mst_status
$ strings /boot/kernel/i915.ko | grep -E "got esi"
got esi %3ph
got esi2 %3ph          <- the re-read branch inside the go_again loop

i915.ko is built and shipped in /boot/kernel of the stock DragonFly 6.5-DEVELOPMENT install booted here, and enable_dp_mst defaults to true (sys/dev/drm/i915/i915_params.h:69).

Line-accurate path trace (this tree)

step where note
1 sys/dev/drm/i915/i915_irq.c β†’ intel_hpd_irq_handler() platform irq handlers call it for each short pulse
2 sys/dev/drm/i915/intel_hotplug.c:496-497 queue_work(dev_priv->hotplug.dp_wq, &dev_priv->hotplug.dig_port_work)
3 sys/dev/drm/i915/intel_hotplug.c:313 dig_port->hpd_pulse(dig_port, long_hpd) β†’ intel_dp_hpd_pulse
4 intel_dp.c:5684 intel_display_power_get() β€” ref never dropped if we never return
5 intel_dp.c:5687 intel_dp_check_mst_status(intel_dp) (is_mst == true)
6 intel_dp.c:4293 intel_dp_get_sink_irq_esi() β€” 14-byte sink read, attacker data
7 drm_dp_mst_topology.c:2450-2473 handled = true on sink-count change / DOWN_REP_MSG_RDY / UP_REQ_MSG_RDY β€” all sink-controlled bits
8 intel_dp.c:4309-4317 clear-write of esi[1..3] to DP_SINK_COUNT_ESI+1 β€” a request; sink may ignore
9 intel_dp.c:4319-4322 re-read succeeds β†’ goto go_again β€” no bound anywhere in the function

The only exits are: ESI read failure (intel_dp_get_sink_irq_esi != 14 bytes) or handled == false. Both are fully under the sink's control.

Note also the esi[10] sub-analysis: drm_dp_channel_eq_ok(&esi[10], …) (drm_dp_helper.c:60-77) only indexes link_status[0..2] β†’ esi[10..12], in bounds of the 14-byte array β€” the memory-safety reads are fine; the defect is purely the unbounded control loop.

Exploit chain

Not a memory-corruption primitive; no escalation chain. Chain is: evil dock β†’ MST handshake β†’ 1 short HPD β†’ infinite go_again β†’ dp_wq kthread livelock + leaked display power-well reference + all-port hotplug starvation β†’ display subsystem dead until reboot (DoS).

Fix validation

fix.diff bounds the loop to 8 bounces and schedules a hotplug event on exhaustion. Not buildable/testable on this guest (no Intel GPU to modeset, and the path needs hostile HW); fix_status: not_testable.

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 bounds the go_again loop to 8 bounces and emits a hotplug event + DRM_ERROR on exhaustion; could not be build-validated on this guest because the target path requires Intel display hardware (no i915 attach on virtio guest) and a hostile sink to drive it.

findings/poc/DF-3626/fix.diff
↓ fix.diffper-fix-DF-3626

Confirmed kernel references

Detail

Exploit chain

evil DP branch device -> advertise DPCD rev>=0x12 + DP_MSTM_CAP(MST) -> i915 enters MST -> device asserts one short HPD -> kernel reads ESI (0x2002..0x200f) -> device always returns esi[1]|DP_DOWN_REP_MSG_RDY and ignores the clear-writes -> intel_dp_check_mst_status() loops 'go_again' forever -> dp_wq kthread busy-spins, display power-well refcount leaked, all-port HPD dead until reboot (DoS). No memory-corruption primitive, no escalation path.

Evidence (decisive lines)

VERDICT.md (full path trace table + guest hardware proof); README.md (loop anatomy, attacker spec, consequences); env.txt (guest pci/kld state); fix.diff (bounded-bounce patch). Decisive lines: intel_dp.c:4294 (go_again label), 4306-4322 (handled->clear->re-read->goto), drm_dp_mst_topology.c:2455-2470 (handled set from sink bits only).

PoC changes

No seed PoC existed; this is a hardware-in-the-loop bug (malicious DP sink). The evidence pack documents the exact evil-sink DPCD behavior spec instead of a runnable host program, plus proof the vulnerable code ships in the default-install kernel module.

Verified recommended fix

Bound the go_again bounce count (e.g. 8) in intel_dp_check_mst_status(); on exhaustion schedule a full hotplug reprobe instead of looping.

Verdict

Source-level certain, runtime inconclusive: intel_dp_check_mst_status() (sys/dev/drm/i915/intel_dp.c:4294-4323) retries the sink-driven ESI handshake via 'goto go_again' with no iteration bound; drm_dp_mst_hpd_irq() (sys/dev/drm/drm_dp_mst_topology.c:2450-2473) returns handled=true whenever the sink keeps DP_DOWN_REP_MSG_RDY/UP_REQ_MSG_RDY set or toggles the 6-bit sink count, so a malicious MST branch device livelocks the dp_wq hotplug kthread forever, leaks the intel_display_power_get() reference taken at intel_dp.c:5684, and starves all display hotplug handling until reboot. Could not be exercised at runtime because the audit guest is virtio-only (vgapci chip 0x1111:1234): i915 never PCI-attaches and /dev/dri does not exist; however the vulnerable function ships in the stock /boot/kernel/i915.ko (nm: 00000000000cbd80 t intel_dp_check_mst_status; loop debug strings present) and enable_dp_mst defaults to true (i915_params.h:69). Expected impact on real HSW+ hardware: kernel DoS via evil dock/monitor (physical-access vector).