# 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()`:

```diff
 	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.
