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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4748 Β· 22 files
FileTypeDescriptionSize
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):

  1. 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)
  1. Run (all scenarios, baseline then fixed, 3x deterministic):
ssh dfbsd 'sh /root/df4748/rig/run.sh'
  1. 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 (the mst_port member read at amdgpu_dm_helpers.c:199); - baseline disable-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 in dm_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 verdict
  • verify.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.

VERDICT.md
↓ download raw

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_table and 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_connector
  • dc_sink.c:34-104, dc.c:1750-1847, dc_stream.c:38-157 β€” the whole dc_sink/dc remote-sink/dc_stream refcount machinery
  • dc_link.c:2322-2372/2374-2452/2454-2530/2630-2642 β€” update_mst_stream_alloc_table, allocate_mst_payload, deallocate_mst_payload, core_link_disable_stream
  • drm_dp_mst_topology.c payload 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-160 drm_connector_free, drm_mode_object.c get/put, drm_connector.h drm_connector_put, tree kref/refcount bodies, fixpt31_32.c math)

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)

  1. Set, never cleared. Sole assignment in the amd display tree: amdgpu_dm_mst_types.c:243 β€” dc_sink->priv = aconnector;
  2. Deref sites. amdgpu_dm_helpers.c:197/289/317 β€” aconnector = stream->sink->priv;; the guard at :199 (!aconnector || !aconnector->mst_port) is value-only.
  3. 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), then drm_connector_put β†’ drm_connector_free β†’ dm_dp_mst_connector_destroy (mst_types.c:182, kfree(amdgpu_dm_connector)). sink->priv is never nulled. (Ref correction vs the original filing: the MST-child free site is dm_dp_mst_connector_destroy at mst_types.c:193, not amdgpu_dm.c:3187 β€” that one is the non-MST connector destroy; both are raw kfrees, conclusion unchanged.)
  4. The sink outlives the connector. The enabled dc_stream_state holds its own sink ref (dc_stream.c:66 construct β†’ dc_sink_retain; released in destruct, dc_stream.c:112). After the unplug destroy the refcount is exactly 1 = the stream's (rig-asserted).
  5. 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 authored mst_port/port steer 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->priv in dm_dp_destroy_mst_connector before the refs drop; the helpers already early-return on NULL priv (:199).
  • Real kernel module build: on the guest, pristine amdgpu.ko built (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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (same kernel; amdgpu.ko module rebuilt with fix, not loaded -- no AMD ASIC on guest)

Confirmed kernel references

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.