# DF-4469 VERDICT — racy array sizing vs live connector list in drm_crtc_helper_set_config

## Status: INCONCLUSIVE (race proven at code level; guest has no DRM device)

The audit guest runs QEMU std VGA — `vgapci0: chip=0x11111234` with no KMS
driver bound (no `/dev/dri`), and this DragonFly drm tree has no driver for
the QEMU-emulated VGA, so the ioctl layer of this finding cannot even be
entered on the guest.  The race itself is proven by code trace below.

## Root cause (path:line)

`drm_crtc_helper_set_config` runs under `mode_config.mutex` (taken by
`drm_mode_setcrtc`, drm_crtc.c:601) — but the connector list it snapshots is
NOT protected by that mutex:

1. `sys/dev/drm/drm_crtc_helper.c:571-576`
   `save_connector_encoders = kcalloc(dev->mode_config.num_connector, ...)`
   — `num_connector` is a plain counter.
2. `sys/dev/drm/drm_connector.c:256-260`
   `spin_lock_irq(&config->connector_list_lock); list_add_tail(...);
    config->num_connector++; spin_unlock_irq(...)` — connector ADDITION only
   takes the spinlock, never `mode_config.mutex`.
3. The addition path is live in-tree for radeon DP MST:
   `sys/dev/drm/drm_dp_mst_topology.c:1205` (`cbs->add_connector`, topology
   work context) → `sys/dev/radeon/radeon_dp_mst.c:267-286`
   `radeon_dp_add_mst_connector` → `drm_connector_init` — no mode_config.mutex
   anywhere on that path (destruction is likewise deferred to
   `destroy_connector_work` precisely because the mutex may be held,
   drm_dp_mst_topology.c:950-960).
4. `sys/dev/drm/drm_crtc_helper.c:588-592`
   ```
   drm_for_each_connector_iter(connector, &conn_iter)
       save_connector_encoders[count++] = connector->encoder;
   ```
   The iterator (`drm_connector_list_iter_next`, drm_connector.c:623-650)
   walks the LIVE list under the spinlock and WILL visit a connector added
   after step 1 → **heap OOB write of one pointer** (value = the fresh
   connector's `encoder`, i.e. NULL for a new kzalloc'd MST connector) one
   slot past the kcalloc'd array.  With `num_connector` at a kmalloc bucket
   boundary (8, 16, 32, ...) the slot lands in the adjacent object.
5. `sys/dev/drm/drm_crtc_helper.c:766-770` (fail path)
   ```
   drm_for_each_connector_iter(connector, &conn_iter)
       connector->encoder = save_connector_encoders[count++];
   ```
   reads the same out-of-bounds slot and **stores adjacent heap data into the
   new connector's `->encoder`** — a forged-pointer injection.  Later walks
   dereference it: `drm_crtc_helper.c:663-664`
   `connector->encoder->crtc = NULL` (write through the forged pointer),
   `drm_crtc_helper.c:843` (`connector->encoder->crtc` read),
   `drm_crtc_helper.c:108-114` (pointer compare, benign).
   Any subsequent `drm_mode_setcrtc`/fail path then corrupts memory through
   that pointer.

## Exploit chain (conceptual, on radeon + DP MST)

1. Local unprivileged user becomes DRM master (first opener on a headless
   system, `DRM_IOCTL_SET_MASTER`).
2. Spray objects of the `num_connector+1` pointers' kmalloc size class to
   place an attacker-interesting victim immediately after the future array.
3. Run `DRM_IOCTL_MODE_SETCRTC` in a tight loop while a partner replugs the
   MST hub: each replug re-runs link-address enumeration → connector add.
4. On hit: NULL-write into the adjacent object (step 4 above) and, on a
   subsequent failing setcrtc, OOB-read → forged `connector->encoder` →
   NULL-write at `*(forged + offsetof(drm_encoder, crtc))`.
5. Primitive: deterministic kernel NULL write at (almost) attacker-chosen
   address via heap-groomed adjacent data → escalation path standard for
   NULL/pointer writes (target refcount/flags fields), DoS trivially.

## Why Medium, not High

Memory corruption class is real, but the trigger needs (a) radeon-class GPU
with DP MST in legacy-helper mode, (b) physical-layer MST hotplug events
during a microsecond-scale window, (c) DRM master. No software-only trigger
exists in-tree (MST adds only run from topology work fed by HPD interrupts).

## Fix (systemic, matches upstream-hardening direction)

Snapshot the connector list length consistently, e.g. size by iterating first
or take `mode_config.mutex` in `drm_connector_init`:

```diff
--- a/sys/dev/drm/drm_connector.c
+++ b/sys/dev/drm/drm_connector.c
@@ -253,9 +253,11 @@ int drm_connector_init(struct drm_device *dev,
 	/* We should add connectors at the end to avoid upsetting the connector
 	 * index too much. */
+	drm_modeset_lock_all(dev);
 	spin_lock_irq(&config->connector_list_lock);
 	list_add_tail(&connector->head, &config->connector_list);
 	config->num_connector++;
 	spin_unlock_irq(&config->connector_list_lock);
+	drm_modeset_unlock_all(dev);
```

…or, minimally inside drm_crtc_helper_set_config, clamp the fill loop:

```diff
--- a/sys/dev/drm/drm_crtc_helper.c
+++ b/sys/dev/drm/drm_crtc_helper.c
@@ -588,7 +588,8 @@ int drm_crtc_helper_set_config(struct drm_mode_set *set,
 	count = 0;
 	drm_connector_list_iter_begin(dev, &conn_iter);
 	drm_for_each_connector_iter(connector, &conn_iter)
+		if (count < dev->mode_config.num_connector)
 		save_connector_encoders[count++] = connector->encoder;
 	drm_connector_list_iter_end(&conn_iter);
```
(the fail-path loop needs the symmetric guard).  The lock fix is the correct
one; the clamp is defense-in-depth.

## Attempts

- Guest build of setcrtc_dup.c: build.log.
- Guest run: open("/dev/dri/card0") = ENOENT (no DRM device) → run.log.
- Upstream cross-check: Linux v4.15..master still sizes with `num_connector`
  and still adds connectors under the spinlock only — bug inherited, not
  fixed upstream (checked drm_connector.c @ master 2026-09).

## 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.
