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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4631 Β· 9 files
FileTypeDescriptionSize
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 whole drm_mode_getconnector ioctl body (the racing reader: drm_connector_lookup kref at :1734 β†’ ... β†’ drm_connector_put at :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 chain set_mst(false) drives at hub unplug.
  • counting semantics verbatim: the tree's own kref.h + refcount.h inline bodies (plain atomic_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 + the out_unlock put) β†’ port create + mstb->ports join (: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_getconnector body; parked inside the ioctl (the fill_modes probe 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)

  1. UAF read/write under a lookup kref β€” any unprivileged DRM fd holder in DRM_IOCTL_MODE_GETCONNECTOR (and every other drm_connector_lookup user, e.g. MODE_OBJ_GETPROPERTIES, the setcrtc lookup at drm_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 final drm_connector_put decrements a refcount in freed memory.
  2. Indirect call through heap content β€” with a won same-size reallocation (standard heap grooming; the guest kernel has no hardening), drm_mode_object_put's obj->free_cb is attacker-chosen: the rig drove the benign target (drm_connector_free) and still got the double free; a real groom picks a better target.
  3. Write-what-where β€” the second drm_connector_cleanup's list_del(&connector->head) writes through fully groomed head.prev/head.next (two pointer writes at attacker-chosen addresses; proven in-rig with a chosen destination node).
  4. Unbounded kernel memory leak β€” one radeon_encoder + radeon_encoder_mst per 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)
VERDICT.md
↓ download raw

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_free is the kref release and itself calls connector->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_getconnector holds a lookup kref across the whole ioctl (drm_connector.c:1734 … :1827), and every other drm_connector_lookup user (e.g. drm_crtc.c:729) does the same;
  • this tree's kref_put is a plain atomic_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

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 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
↓ fix.diffper-fix-DF-4631

Confirmed kernel references

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.