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