# DF-5218 evidence pack — dc_sink refcount ledger violations in DM forced/EDID-override paths

Finding: `dc_sink_retain`/`dc_sink_release` (sys/dev/drm/amd/display/dc/core/dc_sink.c:68-83)
are misused by two amdgpu_dm paths that run for boot-forced connectors
(`video=` force / EDID-firmware override):

1. **Leak** — `emulated_link_detect()` (amdgpu_dm.c:741-815) retains
   `prev_sink` (amdgpu_dm.c:754) and never releases it on ANY exit path, and
   overwrites `link->local_sink` (amdgpu_dm.c:810) without dropping the link's
   reference.  Every forced-detect cycle (each S3 resume / HPD with no display)
   permanently strands one heap `struct dc_sink` (~1 KB).
2. **UAF / double-free** — `amdgpu_dm_update_connector_after_detect()`
   branch A (amdgpu_dm.c:983-1003) calls `dc_sink_release(aconnector->dc_sink)`
   (amdgpu_dm.c:993) on a pointer DM holds **borrowed** (assigned without
   retain at amdgpu_dm.c:995, :1044, :3278-3280).  The dropped reference is the
   link's: on a still-live sink this is a refcount undercount (premature free
   while `link->local_sink` still points at it); on an EDID-change cycle the
   sink has already been freed by `dc_link_detect` (dc_link.c:902) before the
   update runs, so :993 executes `kref_put()` on freed memory (UAF write) and,
   when the poisoned count reaches 0, `dc_sink_free()` on the freed object
   (double free / arbitrary-free of a reused slab block).

## Reproduce

The kernel path needs an AMD DC GPU (Tonga/Fiji for DCE10, or any DC ASIC) —
the audit QEMU guest has none (see env.txt), so the runtime trigger cannot be
executed there.  Two artifacts make the finding verifiable anyway:

* **ledger_model.c** — an executable model of the exact operation sequence
  (verbatim ledger from the cited lines), with instrumented refcounts and
  use-after-free detection.  Builds and runs anywhere.
* **VERDICT.md** — line-by-line source proof of both ledger violations and the
  reachability chain (who sets `connector->force`, which ASICs, which detect
  cycles).

```
./build.sh          # cc -O2 -Wall -o ledger_model ledger_model.c
./run.sh            # bug-leak | bug-uaf | fixed
```

Expected (observed on the DFly guest, cc 8.3, see run.log):

```
[leak-scenario] live dc_sink objects after 8 emulated detects: 9
[leak-scenario] BUG: LEAK REPRODUCED (stranded dc_sink per cycle)
[uaf-scenario] stale sink freed before the :993 release: YES (dc_link.c:902 freed it)
[uaf-scenario] retain/release on freed objects (kref UAF writes): 1
[uaf-scenario] double-free attempts (dc_sink_free on freed): 1
[uaf-scenario] BUG: UAF REPRODUCED (dc_sink_release on freed dc_sink)
[leak-scenario] live dc_sink objects after 8 emulated detects: 1
[leak-scenario] FIXED: exactly 1 live sink (correct)
```

## Real-world trigger (hardware present)

1. Boot with a forced connector and EDID override, e.g.
   `video=DP-1:1920x1080e` (force-digital) or `drm.edid_firmware=DP-1:edid/your-edid.bin`
   — `connector->force` is set from the command line in
   `drm_connector_get_cmdline_mode()` (sys/dev/drm/drm_connector.c:137-140);
   `dc_em_sink` is then created on the first `mode_valid` call
   (amdgpu_dm.c:3320-3322 -> `handle_edid_mgmt` -> `create_eml_sink`).
2. **Leak**: leave the connector headless and suspend/resume (or HPD-cycle)
   repeatedly — one stranded `struct dc_sink` per `emulated_link_detect`.
3. **UAF**: with a display attached, cause two detect cycles whose EDIDs
   differ (replug a different monitor / KVM switch): the second
   `dc_link_detect` frees the old sink at dc_link.c:902, then
   amdgpu_dm.c:993 releases the freed object.

## Fix

`fix.diff` (never applied to sys/): emulated_link_detect adopts the
link_disconnect_sink-style transfer + release-on-all-exits pairing; branch A
stops releasing/retaining borrowed references.  The model's `fixed` mode
demonstrates the corrected ledger.
