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)
PoC verification
Evidence pack
findings/poc/DF-4910 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.logon the kernel console after 1..4 successful races shows, on the next monitor connect, the DF-1966 signature:dc_link_add_remote_sinkpermanently returning NULL (allMAX_SINKS_PER_LINK(4) slots leaked) ->dc_sink->priv = aconnectorNULL-deref panic indm_dp_mst_get_modes(amdgpu_dm_mst_types.c:243).- Between races: leaked
dc_sinkobjects pinned inlink->remote_sinks[]with->privpointing at the freedamdgpu_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 β 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:232if (!aconnector->dc_sink):237-241dc_sink = dc_link_add_remote_sink(...)β adds the sink intolink->remote_sinks[]and takes the array's reference (sys/dev/drm/amd/display/dc/core/dc.c:1797-1800vialink_add_remote_sink_helperdc_sink_retainat dc.c:1757):243dc_sink->priv = aconnector;:244aconnector->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:386aconnector->port = NULL;:387if (aconnector->dc_sink) {:389-390dc_link_remove_remote_sink(...)+dc_sink_release(...)β drops both references (dc.c:1834array ref; caller ref) -> sink freed:391aconnector->dc_sink = NULL; }
Two unsynchronized actors on the same field. Losing interleaving:
- ioctl thread passes the
:232NULL check; - destroy work reads
dc_sink == NULLat:387, skips the release, unregisters anddrm_connector_put()s (:394-397); - ioctl thread stores the freshly created sink at
:243-244; - ioctl drops its connector reference -> refcount 0 ->
dm_dp_mst_connector_destroy(:182-197) kfrees the aconnector β it never releasesdc_sink(onlydm_dp_destroy_mst_connectordoes).
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_detectpassingaconnector->port == NULLafter:386: SAFE βdrm_dp_mst_detect_port(drm_dp_mst_topology.c:2492) anddrm_dp_mst_get_edid(:2558) both funnel throughdrm_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_encodervs encoder free: SAFE β callers hold a connector reference through the DRM atomic state; the fake encoder is only freed bydm_dp_mst_connector_destroyat refcount 0.- EDID sizing at
:240(extensions + 1) * EDID_LENGTH: SAFE βdrm_do_get_edid(drm_edid.c:1648-1683) truncatesedid[0x7e]to the number of valid extensions and allocates exactly(valid+1)*128; EDIDs >512 bytes are then rejected bydc_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_destroywithdc_sinkstill set would leak, butdc_destroylater releasesremote_sinks[](dc_link.c:90-93) β benign. drm_connector_attach_encoderreturn ignored (:347): cosmetic.- Refcount double-release: impossible β only the destroy work ever releases
(
get_modesonly 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.koloads and binds nothing; no/dev/driexists. 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_testableFix 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.
Confirmed kernel references
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:232
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:243
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:244
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:386
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:387
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:389
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:390
- sys/dev/drm/drm_dp_mst_topology.c:3093
- sys/dev/drm/drm_dp_mst_topology.c:3116
- sys/dev/drm/drm_probe_helper.c:400
- sys/dev/drm/drm_connector.c:1759
- sys/dev/drm/amd/display/dc/core/dc.c:1752
- sys/dev/drm/amd/display/dc/core/dc.c:1757
- sys/dev/drm/amd/display/dc/core/dc.c:1834
- sys/dev/drm/amd/display/dc/dc.h:45
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:3324
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:5845
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:5855
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.
No comments yet.