# DF-3885 — `drm_pick_crtcs()` exponential recursion: (CRTCs+1)^connectors DoS via connector-count inflation

**File:** `sys/dev/drm/drm_fb_helper.c:2422-2495` (pass 2; caller `drm_setup_crtcs()` :2602)
**Class:** algorithmic complexity DoS (CWE-407) → unkillable KMS worker holding `fb_helper->lock`
**Severity:** Medium / confidence: likely (hardware-assisted trigger; arithmetic guest-verified)

## What

`drm_pick_crtcs()` assigns CRTCs to connectors with an exhaustive recursive
search. For connector index `n` it recurses **once unconditionally** and then
**once per candidate CRTC**:

```c
2439: best_crtcs[n] = NULL;
2440: best_score = drm_pick_crtcs(fb_helper, best_crtcs, modes, n+1, ...);   /* unconditional */
...
2461: for (c = 0; c < fb_helper->crtc_count; c++) {
...
2483:     score = my_score + drm_pick_crtcs(fb_helper, crtcs, modes, n + 1, ...);  /* per CRTC */
```

When every connector carries a mode (`modes[n] != NULL` — the normal case for
any connected display), the call count is `(crtcs + 1)^(connectors)`. There is
no budget, no memoization, no depth cap.

`drm_fb_helper_add_one_connector()` (`:140-175`) grows `connector_count`
without limit: it `krealloc`s `connector_info` for every connector that
appears. On MST-capable drivers the malicious-hub topology enumerates
connectors freely (each LINK_ADDRESS ACK lists up to 15 ports per branch,
chained — see DF-3881's unbounded-depth twin; `intel_dp_mst.c:497` and
`radeon_fb.c:440` / `amdgpu_dm_mst_types.c:414` add each one). A hostile hub
therefore sets `connector_count` to 20-40+ **with all connectors mode-carrying
(the hub fabricates EDIDs/modes)**.

The whole search runs inside `drm_fb_helper_hotplug_event()` →
`drm_setup_crtcs()` while **`fb_helper->lock` is held** (`:2855`, `:2565`):
the kms hotplug worker pegs a CPU for days (5^20 ≈ 9.5e13 calls on a 4-pipe
i915 at ~2.4ns/call of pure model recursion — kernel calls are far heavier),
and every other fbdev entry point that takes the lock
(`drm_fb_helper_restore_fbdev_mode_unlocked` ← `lastclose`,
`drm_fb_helper_set_par`, `drm_fb_helper_blank`, …) blocks forever behind it.
Console switching and DRM close paths then wedge: system-wide DoS.

## Measured (guest, `pick_crtcs_model.c`, cc -O2)

```
connectors= 8 crtcs=4 calls=488281         time=   2.719 ms
connectors=12 crtcs=4 calls=305175781      time= 747.599 ms     (x5 per connector)
```
Extrapolated: 20 connectors ≈ 9.5e13 calls ≈ days of kernel CPU with the lock
held. (The model's 15-connector run alone outran the SSH timeout — see
VERDICT.md.)

## Threat model / reachability

Physical: malicious DP hub/dock (same actor as DF-1264/65/3881/3886/3882) —
it both inflates the connector count (fake topology) and supplies modes (fake
EDIDs), then triggers any HPD cycle. No host privilege required.
No *software-only* unprivileged trigger was found on this tree: reaching
`drm_setup_crtcs` requires a connector add/remove event, which requires the
DRM device (absent on this guest).

Also noted (same loop, logic-only): `drm_target_preferred()` uses
`BIT_ULL(fb_helper->connector_count)` (`:2342`) which silently wraps for
counts ≥ 64 — mis-configuration picking, not memory unsafety.

## Fix

`fix.diff` — thread a `budget` (default 2^20 invocations) through the
recursion; when exhausted, keep the best configuration found so far instead
of continuing the exhaustive search. Validated to apply cleanly to the
pristine tree.
