radeon MST destroy_connector frees a kref-counted drm_connector raw (drm_connector_cleanup+kfree from the topology worker): UAF for in-flight ioctls, double kfree via the kref release path, and an unbounded per-flap fake-encoder leak
Summary
The .destroy_connector callback tears the MST child down with raw cleanup+kfree while this tree's drm core manages connectors with kref lifetime: GETCONNECTOR holds a lookup kref across the whole ioctl, so the worker's kfree leaves the ioctl walking freed heap; a later final put runs the destroy a second time (second cleanup + second kfree). Independently the path never destroys the per-connector fake MST encoder - one radeon_encoder leaked per hub flap, unbounded with a malicious hub. Video-group user or flapping-hub model. Hardware-gated on guest. Route-teardown-through-the-kref fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4631 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 9.0 KB | β raw | |
| VERDICT.md | β | 6.8 KB | β raw | |
| build.log | β | 5.4 KB | view raw | |
| run.log | β | 184.5 KB | view raw | |
| run.2.log | β | 184.5 KB | view raw | |
| run.3.log | β | 184.5 KB | view raw | |
| env.txt | β | 1.5 KB | view raw | |
| fix.diff | β | 536 B | view raw | |
| rig/ | β | 4.0 KB | β download |
DF-4631 β radeon MST destroy_connector violates drm_connector kref lifetime (UAF / double-free / write-what-where / heap disclosure / unbounded encoder leak)
Status
REPRODUCED (verbatim-rig evidence class, DF-4165/DF-4247 protocol). Every
defect family claimed by the finding was demonstrated in byte-identical
production lifecycle code (56 checker-proven verbatim blocks: the radeon
callbacks, the drm core connector kref machinery, the ENTIRE
DRM_IOCTL_MODE_GETCONNECTOR body, and the exact MST topology unplug chain
that invokes the buggy callback), compiled on the DragonFly guest against
/usr/src β the tree its running kernel (Jul 2 2026 build) was compiled from
β and driven through the kref sequences a real MST hub unplug/replug
generates. The pack's fix.diff (replace the raw drm_connector_cleanup() +
kfree() with drm_connector_put()) was A/B-validated in the same rig:
baseline faults / double-frees / leaks on 5 of 9 modes; fixed build is clean
9/9. In-kernel execution remains radeon-ASIC + DP-MST-hub gated on this guest
(Bochs VGA, no /dev/dri, no drm klds β proven in env.txt).
What was verified and how
The rig (rig/) sed-extracts the production lifecycle from the kernel tree:
- radeon driver (
radeon_dp_mst.c):radeon_dp_add_mst_connector,radeon_dp_register_mst_connector,radeon_dp_destroy_mst_connector(the buggy raw free),radeon_dp_mst_connector_destroy(funcs->destroy), fake-encoder create/destroy,mst_cbs, mode helpers. - drm core (
drm_connector.c/drm_mode_object.c/drm_encoder.c):drm_connector_init/register/unregister/cleanup/free(the kref release handler),__drm_mode_object_add/register/unregister/find,drm_mode_object_get/put, the wholedrm_mode_getconnectorioctl body (the racing reader:drm_connector_lookupkref at :1734 β ... βdrm_connector_putat :1827), encoder init/cleanup. - MST topology (
drm_dp_mst_topology.c):drm_dp_add_mst_branch_device,drm_dp_free/destroy_mst_branch_device,drm_dp_put_mst_branch_device,drm_dp_port_teardown_pdt,drm_dp_destroy_port(queues the worker),drm_dp_free_mst_port+drm_dp_destroy_connector_workβ the exact chainset_mst(false)drives at hub unplug. - counting semantics verbatim: the tree's own
kref.h+refcount.hinline bodies (plainatomic_dec_and_test, no underflow protection).
rig/check_verbatim.sh re-proves all 56 blocks byte-identical on the guest
against /usr/src/sys (build.log: ALL VERBATIM CHECKS PASSED; hostβguest
SHA256 equality for all 13 source files in env.txt). The fixed build is the
same slices with this pack's fix.diff applied via patch --fuzz=0
(bidirectionally proven: forward-apply + reverse-apply restores pristine
byte-exactly).
Shims (rig_shims.h, rig_types2.h) touch no defect path: allocator with
per-object guard pages + free-mode models (guard/zero/poison/groom/plain) +
double-free detection, pthread-backed locks with the production lock protocol
carried by the verbatim code itself, a fixed-table idr, work plumbing, and
copy_to_user into real mapped pages. radeon_mst_helper_funcs (encoder
mode-setting callbacks) is a production-named shim table β off every defect
path under test.
Driven sequences (exactly what the kernel drives)
- plug/replug:
set_mst(true)primary creation (drm_dp_mst_topology.c:2140-2149+ theout_unlockput) β port create +mstb->portsjoin (:1160-1183) βcbs->add_connector+cbs->register_connector(:1204-1221) β the link-address exit put (:1229-1231). - unplug:
set_mst(false)(:2169-2185) βdrm_dp_put_mst_branch_deviceβdrm_dp_destroy_mst_branch_device(kref re-init, port destruction) βdrm_dp_put_portβdrm_dp_destroy_port(:937, queues) βdrm_dp_destroy_connector_work(:3093) βcbs->destroy_connectorβ all verbatim, krefs balanced exactly as in the kernel (the mstb is freed by the worker's final port put, as upstream designed). - reader: the verbatim
drm_mode_getconnectorbody; parked inside the ioctl (thefill_modesprobe point β in production the widest part of the window: EDID probing over DP AUX) while holding its lookup kref.
Observed (run.log / run.2.log / run.3.log β stable across 3 runs)
| mode | baseline (verbatim production) | fixed (+fix.diff) |
|---|---|---|
| race-guard (reader holds lookup kref; freed page PROT_NONE) | SIGSEGV inside the freed radeon_connector (addr=0x8004e8050 β [0x8004e8000, +912)), exit 77 |
clean exit 0; object freed exactly once via the kref release after the reader's put |
| race-zero (freed slot untouched) | near-NULL fault (zeroed modes list walked), exit 77 |
clean |
| race-poison (slot rewritten, uncontrolled) | wild-pointer fault walking 0xAAAA⦠list heads, exit 77 |
clean |
| race-groom (attacker WON the same-size reallocation) | reader's drm_connector_put β drm_mode_object_put calls free_cb read from the freed slot β drm_connector_free β verbatim radeon_dp_mst_connector_destroy on the freed object β victim encoder freed, second list_del writes through groom-chosen pointers β DOUBLE FREE (first freed by 'radeon_dp_add_mst_connector' [the raw kfree], again by 'radeon_dp_mst_connector_destroy'), exit 78 |
clean; single free |
| race-groom-leak (groom harvests only) | freed-heap contents reach the ioctl response: mm_width=0x5151 mm_height=0x5152 (marker planted in the freed slot) copied out by the verbatim GETCONNECTOR |
marker absent (object alive) |
| race2-guard (two kref holders) | UAF, exit 77 | clean; release deferred to last put |
| post-unplug-lookup (control) | -ENOENT (idr serialization works β the bug is specifically free-under-held-kref) |
same |
| unplug-norace (control) | no crash without a concurrent holder; raw free once; fake encoder leaked (2 allocs, 0 encoder frees; connector freed raw) | freed through the release path incl. the encoder (2 frees) |
| replug-leak (64 flap cycles) | 128 fake-encoder allocations, 0 destroyed; encoder_list capped at 32 by the verbatim WARN_ON(num_encoder >= 32) β cycles β₯33 leave encoders orphaned off-list, never freed even at unload; the 32 listed ones are reclaimed only by the driver-unload mirror (drm_mode_config.c:427-440), exit 90 |
0 live / all freed per cycle |
Exit codes: 77 = fault classified inside a freed allocation; 78 = double free; 90 = leak accounting.
Primitive and impact (in-kernel translation)
- UAF read/write under a lookup kref β any unprivileged DRM fd holder in
DRM_IOCTL_MODE_GETCONNECTOR(and every otherdrm_connector_lookupuser, e.g.MODE_OBJ_GETPROPERTIES, the setcrtc lookup atdrm_crtc.c:729) races the topology destroy worker: the verbatim ioctl body walks/copyouts freed heap (proven: marker bytes copied out β kernel-heap disclosure) and its finaldrm_connector_putdecrements a refcount in freed memory. - Indirect call through heap content β with a won same-size
reallocation (standard heap grooming; the guest kernel has no hardening),
drm_mode_object_put'sobj->free_cbis attacker-chosen: the rig drove the benign target (drm_connector_free) and still got the double free; a real groom picks a better target. - Write-what-where β the second
drm_connector_cleanup'slist_del(&connector->head)writes through fully groomedhead.prev/head.next(two pointer writes at attacker-chosen addresses; proven in-rig with a chosen destination node). - Unbounded kernel memory leak β one
radeon_encoder+radeon_encoder_mstper hub flap cycle; after 32 cycles the verbatim encoder-list cap makes every further fake encoder a permanent orphan (never freed even at driver unload), unbounded from a flapping or malicious hub.
Minimum in-kernel outcome of (1)-(3): panic; with grooming: heap corruption with escalation headroom (controlled indirect call + controlled pointer writes). (4) is a deterministic resource-exhaustion DoS.
Why the in-kernel trigger is hardware-gated on this guest (concretely)
radeon_dp_mst_probe requires ASIC_IS_DCE5(rdev) (an Evergreen+ radeon
ASIC on the PCI bus), a DP AUX channel with dpcd[DP_DPCD_REV] >= 0x12 and
DP_MST_CAP set by the sink, plus radeon_mst=1 β i.e. a real radeon GPU +
MST-capable hub. The audit guest has only Bochs VGA (pciconf class 0x030000
chip 0x11111234), no /dev/dri, no drm klds (env.txt). The verbatim rig is
exactly the DF-4165/DF-4247-sanctioned evidence class for this situation:
production instructions, production kref semantics, driven with the
production call sequences; only the hardware-triggered entry (HPD β AUX
sideband topology negotiation) is modeled by driving the topology functions
directly.
Reproduce
# on the DragonFly guest (tree matching the running kernel at /usr/src): cd rig && sh ./build.sh /usr/src # gen + byte-identity proof + make sh ./run.sh # 9 modes x baseline/fixed (see run.log)
DF-4631 VERDICT β radeon MST destroy_connector kref violation
status: reproduced (verbatim-rig evidence class, DF-4165/DF-4247 protocol; 3 stable full-suite runs). In-kernel trigger hardware-gated on this guest (no radeon ASIC / DP MST hub β argued concretely at the end).
1. Root cause (all citations into this tree, byte-verified on the guest)
radeon_dp_destroy_mst_connector (sys/dev/drm/radeon/radeon_dp_mst.c:307-320)
tears down an MST child connector with raw drm_connector_cleanup() +
kfree() when the topology destroy worker calls it
(sys/dev/drm/drm_dp_mst_topology.c:3116, reached from
drm_dp_mst_topology_mgr_set_mst(false) β drm_dp_put_mst_branch_device β
drm_dp_destroy_mst_branch_device β drm_dp_put_port β
drm_dp_destroy_port :937-962 β queued drm_dp_destroy_connector_work
:3093-3132) β while this tree's drm core manages connectors with kref
lifetime:
drm_connector_initβ__drm_mode_object_add(..., false, drm_connector_free)krefs the object (drm_connector.c:208-210,drm_mode_object.c:34-58);drm_connector_freeis the kref release and itself callsconnector->funcs->destroy()(drm_connector.c:152-160) β i.e.radeon_dp_mst_connector_destroy(radeon_dp_mst.c:248-258) is the only kref-correct full teardown (it also destroys the fake MST encoder);drm_mode_getconnectorholds a lookup kref across the whole ioctl (drm_connector.c:1734β¦:1827), and every otherdrm_connector_lookupuser (e.g.drm_crtc.c:729) does the same;- this tree's
kref_putis a plainatomic_dec_and_test(sys/dev/drm/include/linux/kref.h:64-72) with no underflow protection.
The raw free therefore (a) yanks the object under any outstanding lookup
kref, (b) leaves the kref release path (which is what should run) never
executed β including its encoder teardown β and (c) if the freed slot is
re-groomed, hands drm_mode_object_put an attacker-chosen free_cb +
refcount (drm_mode_object.c:184-193).
2. What the rig proved (production code, deterministic interleaving)
The reader is parked inside the verbatim ioctl body at the fill_modes
probe point β in production the widest slice of the lookup window (EDID
probing over DP AUX) β while the verbatim unplug chain frees the connector.
Sequences, accounting and locks are the kernel's own (mstb kref re-init at
drm_dp_mst_topology.c:878, port list refs, the worker's final
kref_put(&port->kref, drm_dp_free_mst_port) freeing the mstb last).
| claim in the finding | rig result (baseline) |
|---|---|
| free-under-lookup-kref β ioctl operates on freed heap | SIGSEGV classified inside the freed radeon_connector (race-guard, race2-guard) |
| zeroed/reused slot β garbage walks | near-NULL modes walk (zero), wild 0xAAAAβ¦ walk (poison) |
kref release on freed memory β funcs->destroy β second cleanup + second kfree |
DOUBLE FREE: drm_connector_put β free_cb from the freed slot β drm_connector_free β verbatim radeon_dp_mst_connector_destroy β victim encoder freed, second drm_connector_cleanup (second list_del through groom-chosen pointers = write-what-what), second kfree of the same allocation |
| heap contents copied out through the ioctl | marker 0x5151/0x5152 planted in the freed slot appears verbatim in the GETCONNECTOR response (race-groom-leak) |
| unbounded encoder leak per flap cycle | 64 cycles β 128 fake-encoder allocs, 0 destroyed; verbatim WARN_ON(num_encoder >= 32) caps the list at 32, cycles β₯33 leave encoders orphaned off-list forever (not freed even by the driver-unload mirror of drm_mode_config.c:427-440) |
Controls behaved exactly as production predicts: unplug-before-lookup β
-ENOENT (the idr mutex serializes visibility; the bug is precisely the
free-under-held-kref); unplug with no holder β no fault, raw free, encoder
leaked.
The groom is the honest model of a won same-size reallocation: the rig
installs the benign call target (drm_connector_free) β a real attacker
chooses better β and even so the production code executes a function pointer
read out of freed heap, frees a chosen victim object, performs two
attacker-addressed pointer writes, and double-kfrees the connector slot.
3. Fix A/B (same rig, same sequences)
fix.diff replaces the raw cleanup+kfree with drm_connector_put():
drm_connector_unregister(connector);
radeon_fb_remove_connector(rdev, connector);
- drm_connector_cleanup(connector);
-
- kfree(connector);
+ drm_connector_put(connector); /* kref release path runs
+ * radeon_dp_mst_connector_destroy(),
+ * which performs the full teardown
+ * including the fake MST encoder */
The initial kref from drm_connector_init is the idr/helper reference;
the worker's put drops it, so the release (and the full teardown incl. the
fake encoder) runs exactly once, at the last reference β deferred past any
in-flight ioctl. The finding README's earlier idea of adding a
drm_connector_get at registration would leak one reference; the minimal
correct fix needs no extra get (verified: fixed unplug-norace frees
connector and encoder through the release path, 2 kfrees; baseline frees
only the connector raw and leaks the encoder).
Fixed build: 9/9 modes clean β no faults, no double free, no leak
marker, encoders reclaimed every cycle β while baseline reproduces on 5/9
(the other four are the two clean controls + the two manifestations of the
same raw-free already counted). fix.diff applies to the pristine tree with
patch --fuzz=0 and reverse-applies byte-exactly (checker-proven).
Kernel-rebuild validation is N/A on this guest: the path is
radeon-ASIC-gated (radeon_dp_mst_probe needs ASIC_IS_DCE5 + DP AUX
dpcd[DP_DPCD_REV] >= 0x12 + DP_MST_CAP), and no radeon device exists
(env.txt); the A/B above is the same-slice comparison the protocol
prescribes for hardware-gated paths.
4. Kernel refs
- sys/dev/drm/radeon/radeon_dp_mst.c:307-320 (raw cleanup+kfree)
- sys/dev/drm/radeon/radeon_dp_mst.c:248-258 (kref-correct funcs->destroy)
- sys/dev/drm/radeon/radeon_dp_mst.c:288,606-653 (fake encoder per add; leaked)
- sys/dev/drm/drm_connector.c:152-160 (release β funcs->destroy)
- sys/dev/drm/drm_connector.c:1734,1827 (ioctl holds lookup kref)
- sys/dev/drm/drm_mode_object.c:126-152 (lookup kref under idr_mutex)
- sys/dev/drm/drm_mode_object.c:184-193 (put β free_cb call)
- sys/dev/drm/drm_dp_mst_topology.c:937-962,3086-3132 (worker chain)
- sys/dev/drm/include/linux/kref.h:64-72 (unprotected dec_and_test)
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 rig on identical deterministic sequences: baseline (verbatim) UAF-faults / double-kfrees / discloses freed heap / leaks encoders; slice+fix.diff is clean 9/9, frees connector AND fake encoder exactly once via the kref release (deferred past in-flight ioctls), leaks 0 encoders across 64 flap cycles. Kernel-rebuild validation N/A (radeon-ASIC-gated path on this guest); fix.diff applies with patch --fuzz=0 and reverse-applies byte-exactly (build.log).
run.log FIXED half (9x CLEAN incl. race-guard/race-groom/race2-guard/race-groom-leak/replug-leak; 'connector frees: raw-destroy-cb=0 kref-release-destroy=2' incl. the encoder); build.log bidirectional patch proof lines; VERDICT.md section 3
Confirmed kernel references
- sys/dev/drm/radeon/radeon_dp_mst.c:307-320
- sys/dev/drm/radeon/radeon_dp_mst.c:248-258
- sys/dev/drm/radeon/radeon_dp_mst.c:288
- sys/dev/drm/radeon/radeon_dp_mst.c:606-653
- sys/dev/drm/drm_connector.c:152-160
- sys/dev/drm/drm_connector.c:1734
- sys/dev/drm/drm_connector.c:1827
- sys/dev/drm/drm_mode_object.c:126-152
- sys/dev/drm/drm_mode_object.c:184-193
- sys/dev/drm/drm_dp_mst_topology.c:937-962
- sys/dev/drm/drm_dp_mst_topology.c:3086-3132
- sys/dev/drm/include/linux/kref.h:64-72
Detail
Exploit chain
unprivileged video-group user (or any DRM fd holder) loops DRM_IOCTL_MODE_GETCONNECTOR on the MST child connector id while the hub topology changes (real hub flap, or a malicious MST hub per the DF-1778 threat model): (1) destroy worker raw-kfree()s the connector under the ioctl's lookup kref -> ioctl walks/copyouts freed heap (proven: marker bytes reach the response = kernel-heap disclosure) and its drm_connector_put decrements freed memory; (2) attacker grooms the freed kmalloc slot (same-size reallocation; no heap hardening on this kernel) -> drm_mode_object_put calls the attacker-chosen free_cb (proven in-rig with the benign target) and drives drm_connector_free -> radeon_dp_mst_connector_destroy on the freed object -> second drm_connector_cleanup's list_del writes two attacker-addressed pointers (proven write-what-where with a chosen destination node) -> second kfree of the connector slot (proven double free) -> allocator freelist corruption / overlapping objects -> standard slab-groom path to code execution; alternatively (3) just flap the topology: >=1 leaked radeon_encoder+radeon_encoder_mst per cycle, permanent (off-list) past 32 cycles -> unbounded kernel memory exhaustion DoS
Evidence (decisive lines)
run.log/run.2.log/run.3.log: baseline UAF faults classified addr-inside-freed radeon_connector (e.g. addr=0x8004e8050 in [0x8004e8000,+912)) on race-guard/race2-guard, near-NULL and 0xAAAA.. wild walks on race-zero/race-poison, 'RIG DOUBLE-FREE: freed AGAIN by radeon_dp_mst_connector_destroy (first freed by radeon_dp_add_mst_connector)' + groomed-pointer list_del on race-groom, 'mm_width=0x5151 mm_height=0x5152' freed-heap marker in the ioctl response on race-groom-leak, 128 allocs/0 destroyed + 32-cap on replug-leak; FIXED half clean 9/9; build.log: ALL VERBATIM CHECKS PASSED (56 blocks vs /usr/src/sys) + bidirectional fix.diff proof + both binaries; env.txt: guest uname, host<->guest SHA256 equality (13 files), no-AMD-GPU proof; rig/: gen_rig.sh/check_verbatim.sh/Makefile/mst_rig.c/rig_shims.h/rig_types2.h/code_hashes.txt
PoC changes
Replaced the hardware-gated 'untested' verdict with the DF-4165/DF-4247 verbatim-rig protocol: 56 verbatim blocks (radeon lifecycle + drm core connector kref machinery + full GETCONNECTOR body + MST topology unplug chain + the tree's kref/refcount inlines), deterministic barrier interleaving at the fill_modes probe point, allocator with guard/zero/poison/groom free-modes and double-free detection, 9 modes x baseline/fixed. Authored a minimal-correct fix.diff (drm_connector_put; no extra drm_connector_get -- the init kref is the helper reference) superseding the README's extra-get sketch. Notable rig fidelity fixes during verification: mstb kref chain balanced exactly as production (set_mst(true)'s out_unlock put), link-address port's final put_port added, groom given a valid lockinit'd mutex and attacker-chosen list_del target (the NULL-left groom initially faulted at +8 -- itself a demonstration of the write-through-controlled-pointers primitive).
Verified recommended fix
replace drm_connector_cleanup()+kfree() in radeon_dp_destroy_mst_connector with drm_connector_put() so the kref release path (radeon_dp_mst_connector_destroy) performs the single full teardown including the fake MST encoder
Verdict
REPRODUCED via the DF-4165/DF-4247 verbatim-rig evidence class: the ENTIRE production MST-connector lifecycle -- radeon_dp_add/register/destroy_mst_connector (the buggy raw drm_connector_cleanup+kfree at radeon_dp_mst.c:307-320), radeon_dp_mst_connector_destroy, fake-encoder create/destroy, the drm core connector kref machinery (drm_connector_free release -> funcs->destroy, __drm_mode_object_add/register/unregister/find, drm_mode_object_get/put), the FULL drm_mode_getconnector ioctl body, and the exact MST topology unplug chain (drm_dp_put_mst_branch_device -> drm_dp_destroy_mst_branch_device -> drm_dp_put_port -> drm_dp_destroy_port -> drm_dp_destroy_connector_work) plus the tree's own kref/refcount inline semantics -- 56 checker-proven byte-identical blocks compiled on the guest against /usr/src (the tree its running kernel was built from) and driven with the kref sequences a real MST hub unplug/replug generates. Deterministic interleaving (reader parked at the fill_modes probe point, the widest part of the production lookup window): baseline faults INSIDE the freed radeon_connector while the reader holds its lookup kref (SIGSEGV classified addr-in-freed, exit 77; zeroed and poisoned variants give near-NULL/wild list walks); with a groomed same-size reallocation the verbatim drm_connector_put calls free_cb FROM THE FREED SLOT, drm_connector_free -> radeon_dp_mst_connector_destroy runs on the freed object, frees a chosen victim encoder, performs the second list_del through groom-chosen pointers (write-what-where) and DOUBLE-KFREEs the connector slot (exit 78, both call sites named); the leak variant copies freed-heap marker bytes (0x5151/0x5152) verbatim into the GETCONNECTOR response (kernel-heap disclosure); 64 replug cycles leak 128 fake-encoder allocations with 0 destroyed -- after the verbatim WARN_ON(num_encoder>=32) cap, every further encoder is a PERMANENT orphan (off-list, never freed even at driver unload). Controls behave exactly as production predicts (unplug-before-lookup => -ENOENT; unplug-without-holder => no fault, encoder still leaked). fix.diff (drm_connector_put instead of raw cleanup+kfree; the init kref IS the helper's reference, no extra get needed -- the finding README's extra-get idea would leak one ref) A/B-validated in the same rig: fixed build clean 9/9, baseline reproduces 5/9 + 2 leak-only controls. In-kernel trigger is radeon-ASIC + DP-MST-hub gated on this guest (Bochs VGA only, no /dev/dri, no drm klds -- env.txt); minimum in-kernel outcome of the proven families is a panic, with heap-grooming escalation headroom (attacker-chosen indirect call + attacker-addressed pointer writes) and an unbounded flap-driven kernel memory leak.
No comments yet.