Use-after-free of amdgpu_dm_connector via stale dc_sink->priv in the dm MST payload/DPCD helpers (connector kfree'd while an enabled stream's dc_sink keeps the back-pointer alive)
Summary
stream->sink->priv back-pointer is set once (mst_types.c:243) and never cleared; MST teardown drops the connector ref and raw-kfrees the connector (amdgpu_dm.c:3187) while an enabled dc_stream_state legitimately keeps the dc_sink alive - the next CRTC-off commit runs deallocate_mst_payload -> helpers deref freed aconnector at :197/:289/:317 (wild read; enable path = UAF-driven write via drm_dp_mst_allocate_vcpi). Machine-checked 8-step static proof; hw-gated (no GPU on guest). Malicious/flapping MST dock + active display triggers it; radeon twin DF-4631 triaged High. Clear-priv-before-put fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4748 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 8.6 KB | β raw | |
| verdict.json | β | 7.7 KB | view raw | |
| manifest.json | β | 2.0 KB | view raw | |
| env.txt | β | 958 B | view raw | |
| fix.diff | β | 731 B | view raw | |
| fix_build.log | β | 636 B | view raw | |
| fix_run.log | β | 2.2 KB | view raw | |
| static_proof.log | β | 1.8 KB | view raw | |
| verify.sh | β | 2.9 KB | view raw | |
| rig/gen_rig.sh | β | 8.9 KB | view raw | |
| rig/check_verbatim.sh | β | 3.7 KB | view raw | |
| rig/mst_rig.c | β | 32.2 KB | view raw | |
| rig/rig_shims.h | β | 9.3 KB | view raw | |
| rig/rig_types.h | β | 10.1 KB | view raw | |
| rig/Makefile | β | 670 B | β download | |
| rig/build.sh | β | 549 B | view raw | |
| rig/run.sh | β | 1.9 KB | view raw | |
| rig/build.log | β | 671 B | view raw | |
| rig/run.log | β | 31.0 KB | view raw | |
| rig/run.2.log | β | 31.0 KB | view raw | |
| rig/run.3.log | β | 31.0 KB | view raw |
DF-4748 β PoC evidence pack (verbatim-code rig)
Use-after-free of amdgpu_dm_connector via stale dc_sink->priv:
sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c:197/289/317
dereference stream->sink->priv after the connector was freed by the MST
unplug path; the sink stays alive holding the dangling back-pointer
because the enabled dc_stream_state keeps its own sink reference.
Status: REPRODUCED (verbatim rig; hardware-gated in-kernel)
The audit guest has no AMD GPU (Bochos stdvga only, no /dev/dri), so the finding is verified with the proven verbatim-code rig (DF-4165/4247/4631 evidence class):
- Build (on the DragonFly guest):
scp -r rig fix.diff dfbsd:/root/df4748/ # or tar
ssh dfbsd 'sh /root/df4748/rig/build.sh /usr/src'
# -> regenerates the slices FROM /usr/src, verifies all 66 VERBATIM
# blocks byte-identical + fix.diff round-trip, builds rig_base
# (verbatim production) and rig_fixed (verbatim + fix.diff)
- Run (all scenarios, baseline then fixed, 3x deterministic):
ssh dfbsd 'sh /root/df4748/rig/run.sh'
- Expected (see VERDICT.md for the full matrix):
- baseline
disable-guard: exit 77,RIG FAULT: addr=β¦ INSIDE FREED allocation 'dm_dp_add_mst_connector' β USE-AFTER-FREE CONFIRMED(themst_portmember read at amdgpu_dm_helpers.c:199); - baselinedisable-groom/enable-groom: exit 77,CONTROLLED-WRITE PROVENβ verbatim payload machinery writes at an attacker-chosen address; -disable-poison: exit 77 (wild pointer); -disable-stale/disable-zero: exit 0 (latent read β honest latency of the bug class); - fixed build: every scenario exit 0, victim byte-identical in groom (FIX_BLOCKED_WRITE).
Contents
rig/β the verbatim rig:gen_rig.sh(machine-extract),check_verbatim.sh(byte-identity proof, host + guest),mst_rig.c(harness),rig_shims.h/rig_types.h(infrastructure shims, no defect logic), generated slices,Makefile,build.sh,run.sh, full logs (build.log,run.log,run.2.log,run.3.log)fix.diffβ one-line back-pointer clear indm_dp_destroy_mst_connector(git-apply-able against sys/; validated A/B in the rig AND by a real amdgpu.ko module build on the guest)fix_build.logβ real-module compile validation (pristine + patched)fix_run.logβ fixed-build scenario matrix (all clean)verdict.json/manifest.jsonβ machine verdictverify.sh+static_proof.logβ original static chain proof (8/8, still valid)env.txtβ guest environment (no GPU PCI device)
On-hardware procedure (for a machine with an AMD ASIC + MST dock)
Display active on an MST child monitor β unplug that monitor (dock stays
connected) β let the session reconfigure (CRTC off / replug) β
dm_helpers_dp_mst_* hit the freed connector; slab state decides
crash-vs-corruption, per the rig's free-mode models.
DF-4748 β VERDICT
Status: reproduced (verbatim-code rig on the DragonFly guest; the
in-kernel manifestation is hardware-gated β the audit guest has no AMD
ASIC / DP MST hub, see env.txt). Impact: kernel memory corruption
(UAF read + attacker-steered writes). Confidence: certain for the rig
reproduction (byte-identical production code, deterministic, 3/3 runs),
likely for on-ASIC manifestation (identical code, only the slab timing
is unexercised).
What was run (DF-4165/4247/4631 verbatim-rig protocol)
rig/ machine-extracts the production code byte-identical from
sys/ (66 verbatim blocks; rig/check_verbatim.sh re-verified them on
the guest against /usr/src β the tree the running kernel was compiled
from β and proves the fixed slice is exactly pristine + fix.diff):
amdgpu_dm_helpers.c:137-172/182-266/281-305/307-338βget_payload_tableand the three dm MST payload helpers (deref sites :197/:289/:317)amdgpu_dm_mst_types.c:182-197/210-258/283-308/310-373/375-398β connector destroy (kfree), get_modes (dc_sink->priv = aconnector:243), fake-encoder create, add connector,dm_dp_destroy_mst_connectordc_sink.c:34-104,dc.c:1750-1847,dc_stream.c:38-157β the whole dc_sink/dc remote-sink/dc_stream refcount machinerydc_link.c:2322-2372/2374-2452/2454-2530/2630-2642βupdate_mst_stream_alloc_table,allocate_mst_payload,deallocate_mst_payload,core_link_disable_streamdrm_dp_mst_topology.cpayload machinery (assign/put payload id, init_vcpi, allocate/reset/deallocate_vcpi, find_vcpi_slots, calc_pbn_mode, update_payload_part1/2, payload step1/2, validated-port walk, put_port)- drm connector refcount plumbing (
drm_connector.c:152-160drm_connector_free,drm_mode_object.cget/put,drm_connector.hdrm_connector_put, tree kref/refcount bodies,fixpt31_32.cmath)
The harness (rig/mst_rig.c) drives the exact production sequences:
plug (dm_dp_add_mst_connector β dm_dp_mst_get_modes β
dc_link_add_remote_sink, sink refcount 2) β enable
(dc_create_stream_for_sink, refcount 3) β unplug
(dm_dp_destroy_mst_connector verbatim: refcount 3β1, held solely by
the stream, connector kfree'd via drm_connector_put β
drm_connector_free β dm_dp_mst_connector_destroy) β use (the display
session reacting: core_link_disable_stream β deallocate_mst_payload
β the helpers, with the OLD stream that still holds the sink alive).
Results (deterministic, 3/3 identical runs β run.log, run.2.log, run.3.log)
| scenario | baseline (verbatim) | fixed (verbatim + fix.diff) |
|---|---|---|
| control / accounting | CLEAN 0 (refcount discipline exact: after unplug the sink's life is owned solely by the stream) | CLEAN 0 |
| disable-guard (freed page PROT_NONE) | exit 77: fault at 0xβ¦e8048 INSIDE the freed amdgpu_dm_connector [0xβ¦e8000+208) β the mst_port member read at amdgpu_dm_helpers.c:199 |
FIX_CLEAN 0 |
| disable-poison (0xAA rewrite) | exit 77: wild-pointer deref (non-canonical β #GP) | FIX_CLEAN 0 |
| disable-stale (old values intact) | completes: LATENT read (dock mgr still valid, port was pre-nulled) β matches real-world latency of this bug class |
FIX_CLEAN 0 |
| disable-zero (scrubbed) | completes: !aconnector->mst_port early-return |
FIX_CLEAN 0 |
| disable-groom (attacker wins the slot) | exit 77: CONTROLLED WRITE β verbatim drm_dp_mst_reset_vcpi_slots + drm_dp_mst_deallocate_vcpi zeroed vcpi{5,0x1111,0x3333,0x22} at the ATTACKER-CHOSEN address (victim object), via the freed connector's authored ->port |
FIX_BLOCKED_WRITE 0 (victim byte-identical) |
| enable-groom (re-plug path) | exit 77: CONTROLLED WRITE β verbatim drm_dp_init_vcpi wrote vcpi{1,551,65536,1} at the attacker-chosen address + assign_payload_id wrote mgr->payload_mask/vcpi_mask and proposed_vcpis[0] = &victim->vcpi |
FIX_BLOCKED_WRITE 0 |
The stable pre-use state is printed by every scenario:
sink alive (refcount 1, held by stream) with sink->priv = <freed connector>
β i.e. the sink demonstrably OUTLIVES the connector with the
never-cleared back-pointer dangling.
The bug (all path:line refs verified in-tree)
- Set, never cleared. Sole assignment in the amd display tree:
amdgpu_dm_mst_types.c:243 β dc_sink->priv = aconnector; - Deref sites.
amdgpu_dm_helpers.c:197/289/317βaconnector = stream->sink->priv;; the guard at :199 (!aconnector || !aconnector->mst_port) is value-only. - The free.
dm_dp_destroy_mst_connector(mst_types.c:375-398) drops the list ref (dc_link_remove_remote_sink, dc.c:1823) and the connector's ref (dc_sink_release), thendrm_connector_putβdrm_connector_freeβdm_dp_mst_connector_destroy(mst_types.c:182,kfree(amdgpu_dm_connector)).sink->privis never nulled. (Ref correction vs the original filing: the MST-child free site isdm_dp_mst_connector_destroyat mst_types.c:193, notamdgpu_dm.c:3187β that one is the non-MST connector destroy; both are raw kfrees, conclusion unchanged.) - The sink outlives the connector. The enabled
dc_stream_stateholds its own sink ref (dc_stream.c:66construct βdc_sink_retain; released indestruct, dc_stream.c:112). After the unplug destroy the refcount is exactly 1 = the stream's (rig-asserted). - The use. Next commit touching the old stream:
core_link_disable_stream(dc_link.c:2630) βdeallocate_mst_payload(:2454) β helper :197/:199 β UAF read of the freed connector; with slab reuse (groom), the authoredmst_port/portsteer the verbatim payload machinery into attacker-chosen memory (controlled writes, proven both disable- and enable-path). Re-plug/enable path:allocate_mst_payload(:2374, caller core_link_enable_stream :2621).
Threat model
Malicious/flapping MST dock (DF-1837) or physical unplug racing the display session (DF-4631 β the radeon twin, triaged High/memcorrupt). No privilege boundary beyond owning the display session; the disable-path trigger (userspace reacting to an HPD by turning the CRTC off) is the NORMAL reaction to an unplug.
Fix validation (fix.diff, A/B + real module build)
- Rig A/B: the fixed build compiles the byte-identical pristine +
fix.diff slice (checker-proven bidirectionally) and is CLEAN on every
scenario, including groom (victim untouched). The fix NULLs
dc_sink->privindm_dp_destroy_mst_connectorbefore the refs drop; the helpers already early-return on NULL priv (:199). - Real kernel module build: on the guest, pristine
amdgpu.kobuilt (bsd.kmod.mk), then fix.diff applied (fuzz=0, hunk at 385) and the module rebuilt cleanly β amdgpu_dm_mst_types.o recompiled, amdgpu.ko relinked (fix_build.log). Tree reverted byte-exact afterwards (sha256 match with the read-only repo tree).
Honesty notes / limitations
- No AMD ASIC on the guest: the in-kernel manifestation (which slab the connector lands in, HPD interrupt timing) is unexercised. What IS exercised is the full production code path itself, byte-identical, with slab state modeled at the boundaries (guard/stale/zero/poison/ groom). The latent-mode results (stale/zero complete without faulting) honestly reflect that the bug often does NOT crash on first use β the severity comes from reuse, exactly as modeled by groom.
- Shims (documented in
rig_shims.h/rig_types.h): allocator with free modes, locks, logging, atomics/bitops, DPCD/sideband transport,dm_helpers_parse_edid_caps(audited separately, killed candidates), the dc_link PBN math helpers (get_pbn_per_slot/get_pbn_from_timing, pure bandwidth arithmetic upstream of the defect), and minimized production-named carrier structs. No defect logic lives in a shim; the defect-relevant functions are verbatim.
Killed candidates (pass 2, from the original filing β unchanged)
get_payload_table OOB (bounded by max_payloads=4 < 6); proposed_vcpis NULL deref (parallel invariant); remote-sink EDID size (rejected >512 β the uncapped local-EDID path is DF-1837, folded there); parse_edid_caps copies (bounded); aux defer loop (off-file); read_local_edid !aconnector (unreachable on local sinks); dtn log truncation (bounded); display_color_depth default (latent-robustness only).
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
fixedA/B in the same verbatim rig: baseline exhibits UAF fault (guard/poison) and attacker-steered controlled writes (groom, both paths) on every run; fixed build (byte-identical pristine+fix.diff slice, checker-proven) is clean on all 8 scenarios -- helpers early-return at amdgpu_dm_helpers.c:199 on the NULLed priv, and in groom mode the victim object stays byte-identical (FIX_BLOCKED_WRITE). Additionally compile-validated in a real amdgpu.ko module build (bsd.kmod.mk) on the guest with fix.diff applied at fuzz=0; /usr/src reverted byte-exact afterwards (sha256 match).
findings/poc/DF-4748/fix_run.log (fixed matrix), fix_build.log (module build), rig/check_verbatim.sh pass-2 (fixed slice == pristine + fix.diff, bidirectional)
Confirmed kernel references
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c:197
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c:199
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c:289
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c:317
- 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:182
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:375
- sys/dev/drm/amd/display/dc/core/dc_sink.c:80
- sys/dev/drm/amd/display/dc/core/dc.c:1823
- sys/dev/drm/amd/display/dc/core/dc_stream.c:66
- sys/dev/drm/amd/display/dc/core/dc_stream.c:112
- sys/dev/drm/amd/display/dc/core/dc_link.c:2374
- sys/dev/drm/amd/display/dc/core/dc_link.c:2454
- sys/dev/drm/amd/display/dc/core/dc_link.c:2630
- sys/dev/drm/drm_dp_mst_topology.c:2765
- sys/dev/drm/drm_dp_mst_topology.c:2593
Detail
Exploit chain
malicious/flapping MST dock (or physical unplug) with a display active on an MST child -> topology worker frees the child amdgpu_dm_connector while the enabled dc_stream_state keeps the dc_sink alive with ->priv dangling -> display session disables the CRTC on the old stream -> deallocate_mst_payload -> dm_helpers_dp_mst_write_payload_allocation_table reads the freed connector (:197/:199) -> with slab reuse, the attacker-authored mst_port/port steer the verbatim drm_dp payload machinery (reset_vcpi_slots / deallocate_vcpi / init_vcpi / assign_payload_id) into attacker-chosen kernel memory: UAF-driven controlled writes (proven at rig-level with full pointer control); re-plug/enable path reaches the same writes via allocate_mst_payload
Evidence (decisive lines)
findings/poc/DF-4748/rig/run.log (decisive: 'RIG FAULT: addr=0x8004e8048 INSIDE FREED allocation dm_dp_add_mst_connector -- USE-AFTER-FREE CONFIRMED' and both 'CONTROLLED-WRITE PROVEN' blocks); run.2.log/run.3.log (determinism 3/3); rig/build.log (66 verbatim blocks byte-identical to /usr/src + fix.diff round-trip on the guest); fix_run.log (fixed build: FIX_CLEAN/FIX_BLOCKED_WRITE on every scenario); fix_build.log (real amdgpu.ko module build with fix.diff, then byte-exact revert); VERDICT.md (full matrix + path:line refs); env.txt (no GPU PCI device)
PoC changes
Authored the full verbatim rig (the seed had only a static chain proof): gen_rig.sh machine-extracts 66 byte-identical blocks (helpers 137-172/182-266/281-305/307-338; mst_types 182-197/210-258/283-308/310-373/375-398; dc_sink 34-104; dc.c 1750-1847; dc_stream 38-157; dc_link 2322-2372/2374-2452/2454-2530/2630-2642; drm_dp payload machinery; drm connector refcount plumbing; kref/refcount/fixpt inline bodies); check_verbatim.sh re-proves byte-identity on the guest against /usr/src and the fix.diff round-trip; mst_rig.c drives plug/enable/unplug/use with 6 free-mode slab models (guard/stale/zero/poison/groom). Infrastructure shims (allocator, locks, logging, atomics, DPCD transport, dm_helpers_parse_edid_caps, dc_link PBN math, minimized carrier structs) carry no defect logic. Fix ref correction: MST-child free site is dm_dp_mst_connector_destroy (mst_types.c:193), not amdgpu_dm.c:3187 (that is the non-MST destroy); conclusion unchanged.
Verified recommended fix
NULL dc_sink->priv in dm_dp_destroy_mst_connector before dropping the connector-side sink reference (one-liner, see fix.diff); helpers already treat NULL priv gracefully (early return at amdgpu_dm_helpers.c:199).
Verdict
Reproduced with the proven verbatim-code rig (DF-4165/4247/4631 evidence class) on the DragonFly guest: 66 machine-extracted code blocks covering the entire cited lifetime chain (amdgpu_dm_helpers.c payload helpers, amdgpu_dm_mst_types.c connector lifecycle, dc_sink/dc remote-sink/dc_stream refcount machinery, dc_link.c commit path, drm_dp payload machinery, drm connector refcount plumbing) are byte-identical to /usr/src -- re-verified ON the guest by check_verbatim.sh -- and compiled there. Driving the exact production kref sequence (plug -> get_modes sets dc_sink->priv=child at mst_types.c:243; enable -> stream takes its own sink ref; unplug -> dm_dp_destroy_mst_connector drops list+connector refs (sink refcount exactly 1, stream-held) and kfree()s the connector; session reacts -> core_link_disable_stream -> deallocate_mst_payload -> helper reads stream->sink->priv) yields, deterministically (3/3 runs): guard mode SIGSEGV INSIDE the freed amdgpu_dm_connector at the mst_port read (amdgpu_dm_helpers.c:199); poison mode wild-pointer deref; groom mode CONTROLLED WRITES -- verbatim drm_dp_mst_reset_vcpi_slots/deallocate_vcpi zeroed a victim object at the attacker-chosen address (disable path) and verbatim drm_dp_init_vcpi wrote vcpi{1,551,65536,1} plus assign_payload_id mask/pointer writes (enable/re-plug path). stale/zero modes complete without faulting, honestly modeling the bug's real-world latency. fix.diff (NULL dc_sink->priv in dm_dp_destroy_mst_connector before the refs drop) A/B-validated in the same rig: fixed build clean on every scenario including groom (victim byte-identical), and compile-validated by a real amdgpu.ko module build (bsd.kmod.mk) on the guest with the patch applied. In-kernel manifestation on real ASIC remains hardware-gated (guest has Bochs vga only, no /dev/dri); the rig exercises the production code itself byte-identical, with slab state modeled at the boundaries.
No comments yet.