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

Unsynchronized aconnector->dc_sink teardown: lockless dm_dp_destroy_mst_connector (destroy work) races mode_config.mutex-held dm_dp_mst_get_modes check-then-set β€” dc_sink leaked with dangling dc_sink->priv, remote-sink slot exhaustion makes the known DF-1966 NULL-deref panic deterministic, plus UAF-read windows

Summary

The destroy work holds NO lock by design while get_modes check-then-sets dc_sink under mode_config.mutex from unpriv GETCONNECTOR - a lost race leaks one dc_sink into remote_sinks[4] with priv dangling at the freed aconnector; after 4 leaks every add_remote_sink fails, making the next monitor connect panic at the DF-1966 site deterministically, plus UAF-read windows in update_freesync_caps/mode_valid. Distinct from DF-4748 (this is the unlocked releaser). Serialize-with-mode_config.mutex fix (cannot deadlock - the only caller invokes lockless).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4910 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 6.3 KB ↓ raw
mst_race_trigger.c β€” 2.9 KB view raw
build.sh β€” 106 B view raw
run.sh β€” 161 B view raw
run.log β€” 162 B view raw
env.txt β€” 347 B view raw
fix.diff β€” 1.2 KB view raw
verdict.json β€” 4.7 KB view raw
manifest.json β€” 1.3 KB view raw

DF-4910 β€” Unsynchronized aconnector->dc_sink teardown race between dm_dp_destroy_mst_connector (unlocked) and dm_dp_mst_get_modes (under mode_config.mutex)

File: sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c Severity: Medium | CWE-362 / CWE-367 | confidence: likely

What to build / run

The kernel-side race requires AMD Display Core hardware with a DisplayPort MST topology (amdgpu DM + a DP hub or MST monitor). The userspace half of the race is mst_race_trigger.c (hammer DRM_IOCTL_MODE_GETCONNECTOR); the other half is an MST topology change (unplug/re-enumeration of the hub), which only hardware can produce.

./build.sh    # cc -o mst_race_trigger mst_race_trigger.c  (on a machine WITH /dev/dri)
./run.sh      # loop: open card, enumerate connectors, hammer GETCONNECTOR
              # while the DP-MST topology is flapped (unplug hub / power-cycle dongle)

Expected result (on AMD MST hardware)

  • run.log on the kernel console after 1..4 successful races shows, on the next monitor connect, the DF-1966 signature: dc_link_add_remote_sink permanently returning NULL (all MAX_SINKS_PER_LINK(4) slots leaked) -> dc_sink->priv = aconnector NULL-deref panic in dm_dp_mst_get_modes (amdgpu_dm_mst_types.c:243).
  • Between races: leaked dc_sink objects pinned in link->remote_sinks[] with ->priv pointing at the freed amdgpu_dm_connector.

Actual result on this audit guest

The audit guest (DragonFly 6.5-DEVELOPMENT, QEMU/KVM) has no AMD PCI display device (only the QEMU virtual VGA vgapci0, chip 0x1234 8086-class), so amdgpu.ko loads but binds nothing: no /dev/dri, no connectors, the dm_dp_mst_* code paths are unreachable at runtime. Recorded honestly in run.log / env.txt. Status: untested (hardware-gated), root cause fully proven by source trace (see VERDICT.md).

VERDICT.md
↓ download raw

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:

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix cannot be validated on this guest: baseline cannot run (no AMD display hardware, no /dev/dri). fix.diff is a minimal, deadlock-audited serialization patch (the cb's only caller invokes it lockless).

fix.diff; call-site proof drm_dp_mst_topology.c:3103-3116 holds no locks when calling cbs->destroy_connector.
↓ fix.diffper-fix-DF-4910

Confirmed kernel references

Detail

Exploit chain

unpriv GETCONNECTOR hammer x MST topology flap (physical replug / hostile DP dongle) -> unlocked destroy skips dc_sink release while probe installs it -> dc_sink leaked with ->priv dangling at freed aconnector; after 4 races every later dc_link_add_remote_sink fails (dc.c:1752) -> next hotplug NULL-deref panic in dm_dp_mst_get_modes (DF-1966 amplifier); UAF-read windows at amdgpu_dm.c:5855/3324 are read-only primitives, no write primitive identified.

Evidence (decisive lines)

VERDICT.md (full line-by-line trace + killed candidates), run.log (guest: NO_AMD_PCI_DEVICE / NO_DEV_DRI, trigger builds and runs, path unreachable), env.txt (uname 6.5-DEVELOPMENT, amdgpu.ko present but unbound), fix.diff (mode_config.mutex serialization).

PoC changes

No seeded PoC existed; wrote mst_race_trigger.c from scratch (DRM ioctl constants embedded since guest lacks installed drm headers).

Verified recommended fix

Hold dev->mode_config.mutex across the aconnector->port/dc_sink teardown in dm_dp_destroy_mst_connector (see fix.diff).

Verdict

Code-proven race: dm_dp_destroy_mst_connector (amdgpu_dm_mst_types.c:386-392) tears down aconnector->port/dc_sink from drm_dp_destroy_connector_work with no mode_config.mutex (drm_dp_mst_topology.c:3103-3116 deliberately lockless), while dm_dp_mst_get_modes (amdgpu_dm_mst_types.c:232-244) check-then-sets the same dc_sink under mode_config.mutex from the unpriv DRM_IOCTL_MODE_GETCONNECTOR path (drm_connector.c:1759-1763). The losing interleaving leaks the dc_sink into link->remote_sinks[] with dc_sink->priv dangling at the kfree'd aconnector, permanently consumes one of MAX_SINKS_PER_LINK(4) slots, and after 4 hits makes the known DF-1966 NULL-deref panic deterministic; secondary UAF-read windows exist in amdgpu_dm_update_freesync_caps (amdgpu_dm.c:5855) and amdgpu_dm_connector_mode_valid (amdgpu_dm.c:3324-3331). NOT runtime-tested: the audit guest has no AMD display device (only the QEMU virtual VGA), amdgpu.ko loads but binds nothing, /dev/dri does not exist β€” recorded in run.log/env.txt. Fix (mode_config.mutex around the teardown) authored in fix.diff but not buildable-into a testable configuration on this guest because the buggy path requires AMD DM hardware with a DP MST topology.