dc_sink retain/release contract violated by DM forced-connector paths: emulated_link_detect strands references (permanent ~1KB dc_sink leak per forced-detect cycle) and update_connector_after_detect branch A releases a borrowed dc_sink (premature free with dangling link->local_sink, or kref_put UAF write + dc_sink_free double-free on freed slab after EDID-change detect)
Summary
Two DM paths break the dc_sink_retain/dc_sink_release ownership (dc_sink.c:68-83): emulated_link_detect retains prev_sink and never releases on ANY exit + overwrites link->local_sink without dropping the link ref (two stranded ~1KB sinks per forced-detect cycle, every S3/HPD); update_connector_after_detect branch A releases a BORROWED aconnector->dc_sink - premature free with dangling local_sink, or on EDID-change cycles dc_link_detect already freed it so :993 is kref_put on freed memory ending in dc_sink_free double-free of a groomable slab block. Root/boot video= or drm.edid_firmware= gate + physical monitor swap. Guest has no AMD GPU: ledger_model.c models the exact op sequence and reproduces both (leak: 9 live after 8 cycles; UAF: release-on-freed + double-free; fixed: 1 live, 0 UAF). Full pairing diff in fix.diff.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5218 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 8.8 KB | β raw | |
| ledger_model.c | β | 8.2 KB | view raw | |
| build.sh | β | 112 B | view raw | |
| run.sh | β | 132 B | view raw | |
| build.log | β | 312 B | view raw | |
| run.log | β | 772 B | view raw | |
| env.txt | β | 1.3 KB | view raw | |
| fix.diff | β | 3.0 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 6.1 KB | view raw |
DF-5218 evidence pack β dc_sink refcount ledger violations in DM forced/EDID-override paths
Finding: dc_sink_retain/dc_sink_release (sys/dev/drm/amd/display/dc/core/dc_sink.c:68-83)
are misused by two amdgpu_dm paths that run for boot-forced connectors
(video= force / EDID-firmware override):
- Leak β
emulated_link_detect()(amdgpu_dm.c:741-815) retainsprev_sink(amdgpu_dm.c:754) and never releases it on ANY exit path, and overwriteslink->local_sink(amdgpu_dm.c:810) without dropping the link's reference. Every forced-detect cycle (each S3 resume / HPD with no display) permanently strands one heapstruct dc_sink(~1 KB). - UAF / double-free β
amdgpu_dm_update_connector_after_detect()branch A (amdgpu_dm.c:983-1003) callsdc_sink_release(aconnector->dc_sink)(amdgpu_dm.c:993) on a pointer DM holds borrowed (assigned without retain at amdgpu_dm.c:995, :1044, :3278-3280). The dropped reference is the link's: on a still-live sink this is a refcount undercount (premature free whilelink->local_sinkstill points at it); on an EDID-change cycle the sink has already been freed bydc_link_detect(dc_link.c:902) before the update runs, so :993 executeskref_put()on freed memory (UAF write) and, when the poisoned count reaches 0,dc_sink_free()on the freed object (double free / arbitrary-free of a reused slab block).
Reproduce
The kernel path needs an AMD DC GPU (Tonga/Fiji for DCE10, or any DC ASIC) β the audit QEMU guest has none (see env.txt), so the runtime trigger cannot be executed there. Two artifacts make the finding verifiable anyway:
- ledger_model.c β an executable model of the exact operation sequence (verbatim ledger from the cited lines), with instrumented refcounts and use-after-free detection. Builds and runs anywhere.
- VERDICT.md β line-by-line source proof of both ledger violations and the
reachability chain (who sets
connector->force, which ASICs, which detect cycles).
./build.sh # cc -O2 -Wall -o ledger_model ledger_model.c ./run.sh # bug-leak | bug-uaf | fixed
Expected (observed on the DFly guest, cc 8.3, see run.log):
[leak-scenario] live dc_sink objects after 8 emulated detects: 9 [leak-scenario] BUG: LEAK REPRODUCED (stranded dc_sink per cycle) [uaf-scenario] stale sink freed before the :993 release: YES (dc_link.c:902 freed it) [uaf-scenario] retain/release on freed objects (kref UAF writes): 1 [uaf-scenario] double-free attempts (dc_sink_free on freed): 1 [uaf-scenario] BUG: UAF REPRODUCED (dc_sink_release on freed dc_sink) [leak-scenario] live dc_sink objects after 8 emulated detects: 1 [leak-scenario] FIXED: exactly 1 live sink (correct)
Real-world trigger (hardware present)
- Boot with a forced connector and EDID override, e.g.
video=DP-1:1920x1080e(force-digital) ordrm.edid_firmware=DP-1:edid/your-edid.binβconnector->forceis set from the command line indrm_connector_get_cmdline_mode()(sys/dev/drm/drm_connector.c:137-140);dc_em_sinkis then created on the firstmode_validcall (amdgpu_dm.c:3320-3322 ->handle_edid_mgmt->create_eml_sink). - Leak: leave the connector headless and suspend/resume (or HPD-cycle)
repeatedly β one stranded
struct dc_sinkperemulated_link_detect. - UAF: with a display attached, cause two detect cycles whose EDIDs
differ (replug a different monitor / KVM switch): the second
dc_link_detectfrees the old sink at dc_link.c:902, then amdgpu_dm.c:993 releases the freed object.
Fix
fix.diff (never applied to sys/): emulated_link_detect adopts the
link_disconnect_sink-style transfer + release-on-all-exits pairing; branch A
stops releasing/retaining borrowed references. The model's fixed mode
demonstrates the corrected ledger.
DF-5218 β VERDICT
Status: inconclusive (runtime) / proven at source level (ledger model
reproduces both violations).
Impact class (if hardware present): kernel heap leak + kref UAF write /
double-free. Reproduced on guest: no β the path requires an AMD DC GPU; the
guest has only the QEMU emulated VGA (vendor 0x1234), so amdgpu binds zero
devices and dc_sink_create() never executes (proven in env.txt).
1. What was audited
sys/dev/drm/amd/display/dc/core/dc_sink.c (108 LOC) β the DC core sink
lifecycle: construct/destruct, dc_sink_create/dc_sink_retain/
dc_sink_release/dc_sink_free. The file itself is a correct, thin
kzalloc/construct/kref wrapper:
dc_sink_create(dc_sink.c:85-104): kzalloc -> construct ->kref_init(refcount = 1, creator's).construct(dc_sink.c:42-62) only fails wheninit_params->link == NULL(dc_sink.c:47-48) and in that case nothing has been written, so thekfreeon the failure path (dc_sink.c:100) needs no destruct. File-local verdict: clean.destruct(dc_sink.c:34-40) freesdc_container_idβ dead-but-safe: a tree-wide search showsdc_container_idis only ever assigned NULL (dc_sink.c:38, :55); no code path allocates it. No double-free clone hazard either: the "logically cloneable up to refcount" comment (dc.h:713-716) is vestigial β no code memcpys astruct dc_sink.- No locking, no usercopy, no ioctl entry β the entire attack surface is the retain/release API consumed by DC/DM on display events.
The pass-2 finding is therefore an API-contract violation by its callers (the sink-lifecycle interplay this file audit was scoped to hunt):
2. Violation A β permanent sink leak in emulated_link_detect
emulated_link_detect() (amdgpu_dm.c:741-826) runs for boot-forced connectors
when nothing is physically attached (callers: amdgpu_dm.c:869 resume,
:1092 HPD, :1219 HPD-RX, :1886 init β all gated on
aconnector->base.force && new_connection_type == dc_connection_none).
Ledger (verbatim from source):
751: prev_sink = link->local_sink; /* borrowed */
753: if (prev_sink != NULL)
754: dc_sink_retain(prev_sink); /* +1 temp ref */
... /* default: ... return; (:798) β temp ref LEAKED */
804: sink = dc_sink_create(&sink_init_data); /* +1 = link's ref */
805: if (!sink) { ... return; (:807) } β temp ref LEAKED */
810: link->local_sink = sink; /* old link ref STRANDED */
812: dm_helpers_read_local_edid(...) /* DF-1837's overflow path */
/* function end β temp ref LEAKED (no dc_sink_release anywhere) */
Consequences: on every call with a prior local_sink, two references to
the old struct dc_sink are stranded (the link's, overwritten at :810 without
release, and the temp from :754, never released on any exit). The object
(β1 KB: embedded dc_edid.raw_edid[512] + edid_caps + features_3d[],
dc.h:696-717) can never reach refcount 0. Forced headless connector +
repeated S3 resume β unbounded kernel heap growth.
Contrast with the correct sibling dc_link_detect() (dc_link.c:642-902),
which retains prev_sink, calls link_disconnect_sink() (dc_link.c:492-500,
releases the link ref) and releases the temp ref on every exit path
(:696, :710, :724, :735, :759, :791, :902).
3. Violation B β release of a borrowed ref β premature free / UAF
amdgpu_dm_update_connector_after_detect() branch A (amdgpu_dm.c:975-1005)
runs for the same forced connectors once dc_em_sink exists
(gate :974-975; dc_em_sink created in create_eml_sink, amdgpu_dm.c:3271,
on the first mode_valid call β amdgpu_dm.c:3320-3322).
DM's convention (everywhere except this branch): aconnector->dc_sink is a
borrowed pointer β assigned with no dc_sink_retain at amdgpu_dm.c:995,
:1001, :1044, :3278-3280. Branch A nevertheless does:
992: if (aconnector->dc_sink != aconnector->dc_em_sink) 993: dc_sink_release(aconnector->dc_sink); /* ref DM never took */
Two states make this a memory-safety bug:
- Old sink still is
link->local_sink(refcount 1 = link's): :993 drops the link's reference β refcount 0 β sink freed whilelink->local_sinkstill points at it β use-after-free on every later touch (nextdc_link_detectβlink_disconnect_sinkβdc_sink_release, stream commits, EDID ioctls). - EDID-change cycle (the deterministic one): a later
dc_link_detectwith a differing EDID frees the old sink itself (retain :644 βlink_disconnect_sink:647 β create new :755 β release prev :902 β refcount 0 β freed). DM still holds the stale borrowed pointer, and the following branch-A update executesdc_sink_release()on the freed object:kref_put()(atomic decrement β UAF write into a freed slab block) and, when the corrupted count reaches 0,dc_sink_free()βdestruct()+kfree()on the freed block β a double free that hands the block back to the allocator while it may already be reused (arbitrary-free of a live slab victim; sink-sized kmalloc zone, groomable timing between the free at dc_link.c:902 and the release at amdgpu_dm.c:993).
Note the accidental interaction: Violation A's stranded references keep the
old emulated sinks over-refcounted, which masks Violation B on the
headless path β the undercount UAF surfaces on the display-attached
forced-connector path instead (where emulated_link_detect never runs).
Both ledgers are proven in ledger_model.c (bug-leak, bug-uaf modes),
whose output matches the by-hand accounting above (run.log).
4. Reachability chain (who can trigger this)
connector->forceis set only from the kernel command line (drm_connector_get_cmdline_mode, sys/dev/drm/drm_connector.c:137-140 βvideo=<conn>:...e[d]), fromdrm.edid_firmware=(sys/dev/drm/drm_edid.c:1606-1625), or by the driver itself (create_eml_sinksets DRM_FORCE_OFF). β the configuration gate is boot-time/root privilege.- The DC sink machinery itself requires an amdgpu-bound AMD ASIC with DC enabled (DCE_VERSION_10_0 = Tonga/Fiji, dc_resource.c:71-76; DCE 11.x/12.x likewise) β specific hardware.
- Given (1)+(2): the leak repeats on every suspend/resume or HPD cycle of the forced headless connector; the UAF fires on any two detect cycles with differing EDIDs (monitor/KVM swap β a physical action that needs no privilege beyond the configured connector).
Severity assessment: Medium β genuine kernel heap corruption (memcorrupt class), but gated behind privileged boot configuration and AMD hardware, and the UAF trigger additionally needs a physical EDID change. The leak variant is a slow local DoS only.
5. Why not reproduced on the guest
- Guest: DragonFly 6.5-DEVELOPMENT x86_64 (uname in env.txt); the only VGA
device is
vgapci0@pci0:0:2:0 chip=0x11111234(QEMU virtual VGA, vendor 0x1234).kldload amdgpusucceeds but binds zero devices β dmesg shows only[drm] amdgpu kernel modesetting enabled.and no probe output;dc_create/dc_sink_createnever run. No KVM device-assignment of an AMD GPU is available in this environment, and QEMU has no AMD DC GPU model. - The ledger model (
ledger_model.c) was therefore built and executed on the guest (cc 8.3 [DragonFly]) and reproduces both violations exactly (run.log): 9 live sinks after 8 cycles (leak), and one release-on-freed + one double-free attempt in the EDID-change cycle (UAF), with thefixedmode returning to exactly 1 live sink and zero UAF hits.
Attempts: 2 (guest hardware-gate probe incl. amdgpu kldload; ledger-model
build+run). Guest was reset clean after probing (vm.sh reset with-src);
no kernel changes were made.
6. Fix validation
fix.diff implements: (a) emulated_link_detect adopts the
link_disconnect_sink-style reference transfer with goto out +
release-on-all-exits; (b) branch A drops the unowned dc_sink_release
(amdgpu_dm.c:993) and its counterpart retain (:1003), leaving
aconnector->dc_sink uniformly borrowed as in the normal path. A patched
kernel cannot be exercised on this guest (no AMD GPU β the changed code
never executes β the baseline itself does not run), so
fix_status = not_testable; the model's fixed mode (same pairing
semantics) demonstrates the corrected ledger returns to zero leaks / zero
UAF hits. fix.diff was authored against the read-only sys/ tree and was
never applied there.
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.diff authored against read-only sys/ (never applied). Baseline cannot run on this guest (no AMD GPU β the amdgpu driver never initializes, so neither baseline nor patched kernel exercises the changed lines); a patched-kernel build would prove nothing about behavior. The corrected pairing semantics are demonstrated by ledger_model.c's 'fixed' mode: exactly 1 live sink after 8 cycles and zero UAF hits, versus the buggy ledger's 9 live sinks and 1 double-free attempt.
fix.diff; run.log (fixed-mode output); VERDICT.md section 6.
Confirmed kernel references
- sys/dev/drm/amd/display/dc/core/dc_sink.c:68
- sys/dev/drm/amd/display/dc/core/dc_sink.c:85
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:741
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:754
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:810
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:993
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:995
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:1044
- sys/dev/drm/amd/display/dc/core/dc_link.c:492
- sys/dev/drm/amd/display/dc/core/dc_link.c:902
Detail
Exploit chain
With an AMD DC GPU present and a boot-forced connector: (a) leak β leave forced connector headless, suspend/resume repeatedly; each emulated_link_detect strands one dc_sink (heap exhaustion DoS). (b) UAF β attach display, cause two detect cycles with differing EDIDs (monitor/KVM swap); dc_link_detect frees the old sink at dc_link.c:902, then amdgpu_dm.c:993 kref_puts the freed object; if the slab block was reused in the window between the free and the release, the decrement corrupts the victim object and a resulting 0-count triggers dc_sink_free (destruct + kfree) on it β arbitrary-free primitive on a ~1KB kmalloc zone, groomable toward slab corruption; on this stock guest no uid0 chain was attempted because the driver never initializes.
Evidence (decisive lines)
VERDICT.md (full line-cited ledger proof + reachability chain + hardware gate); run.log (guest: ledger_model bug-leak -> 9 live sinks; bug-uaf -> 'stale sink freed before the :993 release: YES', 1 UAF write, 1 double-free attempt; fixed -> 1 live sink, 0 UAF); env.txt (uname, pciconf chip=0x11111234 QEMU VGA, kldload amdgpu binding zero devices); fix.diff (pairing fix); ledger_model.c (code_hash below).
PoC changes
No seed PoC existed. Authored ledger_model.c: a userspace model of the verbatim dc_sink/dc_link/amdgpu_dm operation sequence with instrumented refcounts and freed-object detection (the kernel path itself is hardware-gated); plus build.sh/run.sh; first host build attempt failed (audit host has no cc) β final build/run performed on the DFly guest via base64 transport.
Verified recommended fix
emulated_link_detect: mirror dc_link_detect's pairing (release the link ref via a link_disconnect_sink-style transfer, drop the temp ref on every exit); update_connector_after_detect branch A: stop releasing/retaining the borrowed aconnector->dc_sink (fix.diff).
Verdict
Source-level proven, runtime hardware-gated. The dc_sink lifecycle file itself (dc_sink.c:34-104) is correct; its retain/release contract (dc_sink.c:68-83) is violated by two DM forced-connector paths: (1) emulated_link_detect (amdgpu_dm.c:741-826) retains prev_sink at :754 with no release on any exit and overwrites link->local_sink at :810 without dropping the link ref β one permanently stranded struct dc_sink (~1KB) per forced-detect cycle (S3 resume/HPD of a headless forced connector): unbounded kernel heap leak; (2) update_connector_after_detect branch A releases a BORROWED aconnector->dc_sink at amdgpu_dm.c:993 (never retained on store at :995/:1044/:3278) β on a still-live local_sink this drops the link's only ref (premature free with link->local_sink dangling), and on an EDID-change cycle the sink is already freed by dc_link_detect (dc_link.c:902) so :993 is dc_sink_release on freed memory: kref_put UAF write and dc_sink_free double-free of a groomable ~1KB slab block. Preconditions: boot-time connector force (video=/drm.edid_firmware=, drm_connector.c:137-140) + AMD DC GPU (DCE10=Tonga/Fiji, dc_resource.c:71-76) + (UAF) physical EDID change. The QEMU guest has only the QEMU virtual VGA (vendor 0x1234); kldload amdgpu binds zero devices and dc_sink_create never executes (env.txt), so no runtime reproduction was possible; an executable model of the exact ledger (ledger_model.c) was built and run on the guest (cc 8.3) and reproduces both violations (leak: 9 live sinks after 8 cycles; UAF: 1 release-on-freed + 1 double-free attempt) with the fixed pairing returning to 1 live sink / 0 UAF (run.log).
No comments yet.