# 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`
