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

hpd_lock pairing asymmetry in handle_hpd_rx_irq(): link-type flip inside handler strands the lock (permanent hang) or releases an unheld lock

Summary

handle_hpd_rx_irq() takes aconnector->hpd_lock iff dc_link->type != mst_branch on ENTRY but releases it iff the condition holds on EXIT, re-reading the field after dc_link_detect() which rewrites it in-handler: single->mst_branch (branch discovered from sink DPCD) and mst_branch->single (sink-controlled topology-start failure). A stranded lock permanently wedges handle_hpd_irq work items, dm_suspend/dm_resume, and amdgpu_dm_irq_fini's flush_work; the reverse direction runs lockmgr LK_RELEASE on a never-held lock. Crafted/defective DisplayPort dock or sink (DF-3626-class sink-controlled condition): persistent unresolvable DoS or lock-state corruption/panic - physical or supply-chain vector. On-guest harness prints the exact entry-x-exit pairing matrix showing the two unbalanced cells. Fix: cache the locking decision at entry.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3654 Β· 11 files
FileTypeDescriptionSize
harness.c β€” 2.7 KB view raw
build.sh β€” 88 B view raw
run.sh β€” 90 B view raw
build.log β€” 9 B view raw
run.log β€” 845 B view raw
env.txt β€” 323 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
fix.diff β€” 1.2 KB view raw
manifest.json β€” 988 B view raw
verdict.json β€” 3.8 KB view raw
README.md β€” 2.6 KB ↓ raw

DF-3654 β€” hpd_lock pairing asymmetry in handle_hpd_rx_irq() (sink-triggered permanent hang / unlock-without-hold)

File: sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c Lines: 1209-1210 (conditional lock), 1232 (in-handler dc_link_detect), 1251-1254 (re-evaluated conditional unlock) Severity: Medium (crafted-sink / malicious-dock local DoS; panic variant) Β· CWE-667 Β· confidence: likely

What

handle_hpd_rx_irq() decides whether to take aconnector->hpd_lock by reading dc_link->type on entry, but decides whether to release it by re-reading dc_link->type on exit. Between the two reads, the handler itself calls dc_link_detect() (amdgpu_dm.c:1232), and detection mutates link->type:

  • single β†’ mst_branch β€” detect_dp() when an MST branch is discovered (sys/dev/drm/amd/display/dc/core/dc_link.c:528);
  • mst_branch β†’ single β€” when dm_helpers_dp_mst_start_top_mgr() fails (sink-controlled sideband failure) at dc_link.c:569.

The two unbalanced combinations:

entry type exit type effect
single mst_branch lock stranded forever
mst_branch single mutex_unlock() (lockmgr LK_RELEASE) without hold

A stranded hpd_lock permanently wedges: every later handle_hpd_irq() for the connector (amdgpu_dm.c:1083) blocks inside its work item; dm_suspend() / dm_resume() block forever (amdgpu_dm.c:864, 878) β€” the machine can no longer suspend or resume; amdgpu_dm_irq_fini()'s flush_work() hangs (amdgpu_dm_irq.c:334) so driver teardown deadlocks.

Threat model

The type flip is driven by data the sink supplies over DDC/AUX sideband (DisplayPort branch capability bits, MST sideband responses). A malicious or defective DP dock/hub β€” the DF-3626-class "sink-controlled condition" β€” that causes the first HPD-RX short pulse handling to run dc_link_detect() and discover the branch strands the lock; one that makes topology start fail after the link is already classified MST gets an unbalanced release.

Harness

harness.c prints the exact entryΓ—exit decision matrix computed the way the kernel computes it (entry read at 1209, exit read at 1251), demonstrating the two unbalanced cells and their code-level causes. The audit guest is virtio-only (no AMD GPU β€” env.txt), so the kernel handler itself cannot run.

./build.sh && ./run.sh

Expected decisive output (run.log): the matrix with STRANDED LOCK and UNLOCK WITHOUT HOLD rows.

Contents

harness.c Β· build.sh Β· run.sh Β· build.log Β· run.log Β· env.txt Β· VERDICT.md Β· fix.diff Β· manifest.json Β· verdict.json

VERDICT.md
↓ download raw

DF-3654 β€” VERDICT

Status: inconclusive (no AMD GPU on the audit guest; the lock-pairing defect is source-certain and modeled on-guest; the in-handler type flip is source-proven, its hardware realization is timing-dependent).

Root cause

handle_hpd_rx_irq() (sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:1195-1255):

1201   bool is_mst_root_connector = aconnector->mst_mgr.mst_state;
...
1209   if (dc_link->type != dc_connection_mst_branch)
1210           mutex_lock(&aconnector->hpd_lock);            /* ENTRY read */
...
1212   if (dc_link_handle_hpd_rx_irq(dc_link, NULL, NULL) && !is_mst_root_connector) {
...
1232           } else if (dc_link_detect(dc_link, DETECT_REASON_HPDRX)) {
...
1247   if ((dc_link->cur_link_settings.lane_count != LANE_COUNT_UNKNOWN) ||
1248       (dc_link->type == dc_connection_mst_branch))
1249           dm_handle_hpd_rx_irq(aconnector);
1250
1251   if (dc_link->type != dc_connection_mst_branch) {       /* EXIT read */
1252           drm_dp_cec_irq(&aconnector->dm_dp_aux.aux);
1253           mutex_unlock(&aconnector->hpd_lock);
1254   }

dc_link_detect() runs between the two reads of dc_link->type and can rewrite it:

  • sys/dev/drm/amd/display/dc/core/dc_link.c:526-528 β€” when is_mst_supported(link) (DPCD branch capability, sink-supplied), link->type = dc_connection_mst_branch.
  • dc_link.c:565-571 β€” when dm_helpers_dp_mst_start_top_mgr() fails (DisplayPort sideband/topology failure β€” sink-controlled), link->type is reset to dc_connection_single.

Consequences:

  1. entry=single, exit=mst_branch (branch discovered during this handler): hpd_lock (a lockmgr lock initialized LK_CANRECURSE, amdgpu_dm.c:3940) is never released. Every subsequent handle_hpd_irq() for this connector blocks forever at amdgpu_dm.c:1083 inside its per-source work item (amdgpu_dm_irq.c:311 INIT_WORK); dm_suspend()/dm_resume() block at amdgpu_dm.c:864/878 (suspend/resume permanently broken); amdgpu_dm_irq_fini() hangs in flush_work() (amdgpu_dm_irq.c:334) (driver teardown deadlocks). Persistent, unresolvable DoS.
  2. entry=mst_branch, exit=single (topology start failed in-handler; entry requires mst_state==false with type still mst_branch β€” post-MST-teardown state): mutex_unlock() on a lock this thread never acquired. In the DragonFly DRM shim mutex_unlock β†’ lockmgr(lock, LK_RELEASE) (sys/dev/drm/include/linux/mutex.h:39) β€” releasing an unheld lockmgr lock corrupts the lock state / panics under DIAGNOSTIC.

Why "likely" and not "certain"

The pairing defect is unambiguous in source (two different reads of a field mutated in between; no cached decision). What cannot be proven on this guest is the hardware sequence: direction (1) needs the branch discovery to happen inside an HPD-RX short-pulse handler rather than the long-HPD path (handle_hpd_irq, which locks unconditionally and is symmetric). That is plausible for a dock that renegotiates without toggling HPD long, or a malicious sink that drives short pulses β€” exactly the DF-3626 lesson (sink-controlled condition) β€” but it is device-timing-dependent.

Verification (Phase V)

  • Guest is virtio-only (env.txt: no AMD PCI device, no /dev/dri, no drm klds) β†’ handler cannot execute.
  • harness.c computes the entryΓ—exit matrix with the kernel's exact conditionals; run.log shows the two unbalanced cells: entry=single exit=mst_branch β†’ STRANDED LOCK and entry=mst_branch exit=single β†’ UNLOCK WITHOUT HOLD, each annotated with the dc_link.c line that performs the in-handler flip.

Fix

Cache the entry decision (bool lock_taken = dc_link->type != dc_connection_mst_branch;) and gate both lock and unlock on the cached value β€” see fix.diff (mirrors the upstream-style fix; applies cleanly per git apply --check). fix_status: not_testable (hardware-gated).

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

Hardware-gated: requires an amdgpu-backed DP connector and a sink that flips the link classification inside one HPD-RX handling. fix.diff makes lock/unlock symmetric on a cached decision; git apply --check verified against sys/; not applied (read-only audit target).

fix.diff in this pack
↓ fix.diffper-fix-DF-3654

Confirmed kernel references

Detail

Exploit chain

Malicious/defective DisplayPort dock or sink (physical or supply-chain): presents MST branch caps so an HPD-RX short pulse causes dc_link_detect() inside handle_hpd_rx_irq() to flip link->type single->mst_branch while hpd_lock is held -> lock stranded -> all later HPD processing for the connector, suspend/resume, and driver teardown hang permanently. Alternate direction: make topology start fail after the link is classified MST to trigger unlock-without-hold -> lockmgr state corruption/panic.

Evidence (decisive lines)

run.log (pairing matrix: STRANDED LOCK and UNLOCK WITHOUT HOLD cells with dc_link.c:528/569 annotations); VERDICT.md (full source trace incl. lock shim mapping); fix.diff (cached lock decision; git apply --check clean); env.txt (guest virtio-only)

PoC changes

no seeded PoC for pass-2; harness written fresh β€” enumerates the entry x exit decision matrix exactly as the kernel computes the two conditionals

Verified recommended fix

Cache the lock decision at handler entry (bool hpd_lock_taken = dc_link->type != dc_connection_mst_branch) and gate lock and unlock on the cached value.

Verdict

Source-certain locking defect, hardware-gated runtime. handle_hpd_rx_irq() takes aconnector->hpd_lock iff dc_link->type != dc_connection_mst_branch on ENTRY (amdgpu_dm.c:1209-1210) but releases it iff the same condition holds on EXIT (1251-1254), re-reading the field after dc_link_detect() (1232) which can rewrite it in-handler: single->mst_branch at dc_link.c:528 (branch discovered from sink DPCD) and mst_branch->single at dc_link.c:569 (dm_helpers_dp_mst_start_top_mgr failure, sink-controlled sideband). Direction 1 strands hpd_lock permanently: later handle_hpd_irq() work items for the connector block forever (amdgpu_dm.c:1083), dm_suspend/dm_resume hang (864/878), amdgpu_dm_irq_fini flush_work() hangs (amdgpu_dm_irq.c:334) β€” persistent DoS. Direction 2 executes mutex_unlock() (= lockmgr LK_RELEASE) on a never-held lock (sys/dev/drm/include/linux/mutex.h:39) β€” lock corruption/panic. The pairing matrix was demonstrated on-guest by the harness (run.log); the kernel handler itself cannot run on the virtio-only guest (env.txt: no AMD GPU). 'likely' rather than 'certain' only because the in-handler type-flip hardware sequence (short-pulse HPD-RX that runs detection discovering/losing the branch) is device-timing-dependent β€” the DF-3626 sink-controlled-condition class.