# VERDICT — DF-4910

**status: untested** (runtime requires AMD DM hardware + DP MST topology; the
audit guest has no AMD display device — proven in env.txt/run.log).
**reproduced: 0** | **impact: none (untested)** — assessed impact if hardware
present: kernel object-lifetime corruption (leak + dangling `dc_sink->priv` +
deterministic DF-1966 panic amplifier + UAF-read windows). **confidence in the
bug: likely** (race proven line-by-line; interleaving requires a topology
event inside a ~microsecond window, achievable by physical replug automation
or a hostile DP device that flaps the topology).

## Root cause (code-proven)

`dm_dp_mst_get_modes()` runs with `dev->mode_config.mutex` held — guaranteed
by `drm_probe_helper.c:400` (`WARN_ON(!mutex_is_locked(...))`), reached from
`drm_mode_getconnector` (`drm_connector.c:1759-1763`, unprivileged ioctl,
connector ref held at 1734) or the output-poll work. It executes a
check-then-set on shared connector state:

- `sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:232` `if (!aconnector->dc_sink)`
- `:237-241` `dc_sink = dc_link_add_remote_sink(...)` — adds the sink into
  `link->remote_sinks[]` and takes the array's reference
  (`sys/dev/drm/amd/display/dc/core/dc.c:1797-1800` via
  `link_add_remote_sink_helper` `dc_sink_retain` at dc.c:1757)
- `:243` `dc_sink->priv = aconnector;`
- `:244` `aconnector->dc_sink = dc_sink;`

The teardown runs in the topology manager's `drm_dp_destroy_connector_work`
(`sys/dev/drm/drm_dp_mst_topology.c:3093-3132`), which — by design, to avoid
AB-BA (`:3098-3102` comment) — calls the driver callback **holding neither
`mode_config.mutex` nor `mgr->lock` nor `connector->mutex`**:

- `amdgpu_dm_mst_types.c:386` `aconnector->port = NULL;`
- `:387` `if (aconnector->dc_sink) {`
- `:389-390` `dc_link_remove_remote_sink(...)` + `dc_sink_release(...)` —
  drops both references (`dc.c:1834` array ref; caller ref) -> sink freed
- `:391` `aconnector->dc_sink = NULL; }`

Two unsynchronized actors on the same field. Losing interleaving:

1. ioctl thread passes the `:232` NULL check;
2. destroy work reads `dc_sink == NULL` at `:387`, skips the release, unregisters
   and `drm_connector_put()`s (`:394-397`);
3. ioctl thread stores the freshly created sink at `:243-244`;
4. ioctl drops its connector reference -> refcount 0 ->
   `dm_dp_mst_connector_destroy` (`:182-197`) kfrees the aconnector — **it
   never releases `dc_sink`** (only `dm_dp_destroy_mst_connector` does).

Result: the sink stays pinned in `link->remote_sinks[]` forever (until
`dc_destroy` at unload, `sys/dev/drm/amd/display/dc/core/dc_link.c:90-93`),
its `->priv` back-pointer dangles at freed memory, and one of the four
`MAX_SINKS_PER_LINK` slots (`sys/dev/drm/amd/display/dc/dc.h:45`) is
permanently consumed. After 4 successful races, every later
`dc_link_add_remote_sink` fails at `dc.c:1752-1755`, which makes the *known*
DF-1966 NULL-deref (`amdgpu_dm_mst_types.c:243`) fire deterministically on the
next monitor connect — a reliable local kernel panic with hardware assist.

Secondary windows inside the same race: `amdgpu_dm_update_freesync_caps`
(`amdgpu_dm.c:5845` NULL-recheck, `:5855` deref of the concurrently-freed
sink) and `amdgpu_dm_connector_mode_valid` (`amdgpu_dm.c:3324`,
`create_stream_for_sink` at `:3331` retaining a sink that the destroy work is
releasing) — use-after-free reads. The dangling `dc_sink->priv` is the same
back-pointer family as DF-4748 (there the window is an enabled stream; here
the window is created by the unlocked teardown itself).

## What I checked and killed (pass-2 depth)

- `dm_dp_mst_detect` passing `aconnector->port == NULL` after `:386`:
  SAFE — `drm_dp_mst_detect_port` (`drm_dp_mst_topology.c:2492`) and
  `drm_dp_mst_get_edid` (`:2558`) both funnel through
  `drm_dp_get_validated_port_ref` (`:1023-1031`), which never dereferences the
  unverified pointer (address comparison only). NULL is never in the port
  list -> returns disconnected.
- `best_encoder` vs encoder free: SAFE — callers hold a connector reference
  through the DRM atomic state; the fake encoder is only freed by
  `dm_dp_mst_connector_destroy` at refcount 0.
- EDID sizing at `:240` `(extensions + 1) * EDID_LENGTH`: SAFE —
  `drm_do_get_edid` (`drm_edid.c:1648-1683`) truncates `edid[0x7e]` to the
  number of *valid* extensions and allocates exactly `(valid+1)*128`;
  EDIDs >512 bytes are then rejected by `dc_link_add_remote_sink`
  (`dc.c:1774`) -> the known DF-1966 crash, not an OOB read.
- Unload path: `amdgpu_dm_fini` -> `drm_mode_config_cleanup` ->
  `dm_dp_mst_connector_destroy` with `dc_sink` still set would leak, but
  `dc_destroy` later releases `remote_sinks[]` (`dc_link.c:90-93`) — benign.
- `drm_connector_attach_encoder` return ignored (`:347`): cosmetic.
- Refcount double-release: impossible — only the destroy work ever releases
  (`get_modes` only adds); the defect is the *missing* release in the losing
  interleaving (leak), not a double free.

## Exploit chain (assessed, not executed)

unpriv local user on an AMD-DM desktop + ability to perturb the DP topology
(hostile/flaky MST dongle, relay-driven replug):
`GETCONNECTOR hammer` × `topology flap` -> leak 4 sinks -> next hotplug hits
`dc.c:1752` cap -> `amdgpu_dm_mst_types.c:243` NULL-deref panic (DoS).
UAF-read windows (`amdgpu_dm.c:5855`, `:3324`) and the dangling `->priv` are
read-side primitives; no write primitive identified in this interleaving.

## Fix

Serialize the teardown with the probe path. The callback's only caller is the
destroy work (`drm_dp_mst_topology.c:3116`) which holds no locks when
invoking it, so acquiring `mode_config.mutex` there cannot deadlock — see
`fix.diff`.

## Guest / environment

- `DragonFly dfbsd 6.5-DEVELOPMENT #0 ... x86_64` (env.txt)
- Guest has NO AMD PCI display device (only QEMU virtual VGA chip 0x1234);
  `amdgpu.ko` loads and binds nothing; no `/dev/dri` exists. Runtime
  verification of an amdgpu-DM path is impossible on this guest. Guest was
  reset clean after probing (`vm.sh reset with-src`).

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