drm_pick_crtcs exponential recursion: (CRTCs+1)^connectors kernel CPU burn holding fb_helper->lock via hub-inflated connector count
Summary
drm_pick_crtcs recurses once unconditionally and once per candidate CRTC per connector: with mode-carrying connectors the cost is (crtcs+1)^connectors with no budget or cap. connector_count grows unbounded at runtime through the krealloc in add_one_connector fed by MST connector enumeration, and the same malicious hub supplies the modes. The search runs under fb_helper->lock inside drm_setup_crtcs from hotplug/initial-config, so a 20-connector topology on a 4-pipe i915 (~9.5e13 calls) pins a kms worker for days while lastclose, set_par, blank and ioctl block on the same lock - system-wide DoS. GUEST-MEASURED x5-per-connector growth curve (305M calls at 12 connectors; 15-connector run outran a 120s timeout). Budget-threaded fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3885 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| pick_crtcs_model.c | β | 2.2 KB | view raw | |
| build.sh | β | 106 B | view raw | |
| run.sh | β | 99 B | view raw | |
| build.log | β | 345 B | view raw | |
| run.log | β | 500 B | view raw | |
| env.txt | β | 482 B | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| manifest.json | β | 982 B | view raw | |
| verdict.json | β | 3.2 KB | view raw |
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:
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 kreallocs 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.
VERDICT β DF-3885
status: inconclusive (kernel path hardware-gated) / impact assessed: dos / confidence: likely
Why the blowup is real
drm_pick_crtcs()(drm_fb_helper.c:2422-2495) recurses once at :2440 and once per CRTC at :2483-2484 β T(N) = (C+1)Β·T(N-1) with C = crtc_count whenevermodes[n] != NULLfor all n. There is no call budget anywhere.drm_setup_crtcs()calls it at :2602 withfb_helper->lockheld (lockdep_assert at :2565), fromdrm_fb_helper_hotplug_event()(:2870) and__drm_fb_helper_initial_config_and_unlock(:2717).- Connector count is unbounded at runtime:
__drm_fb_helper_add_one_connector()(:140-175) kreallocs and appends on every connector registration. MST drivers register connectors per hub port: intel_dp_mst.c:497, radeon_fb.c:440, amdgpu_dm_mst_types.c:414. Combined with the unbounded topology enumeration in drm_dp_mst_topology.c (see DF-3881 β same root: no depth/count cap on hub claims), a malicious hub reaches 20-40 mode-carrying connectors. - Lock consequence:
fb_helper->lockstays held;lastclose(drm_fb_helper.c:3005-3009),set_par(:1804),blank(:691 β dpms β :673),ioctl(:1585) all block β the DRM master close path and console blanking wedge behind a permanently-running worker.
Measured on the guest
pick_crtcs_model.c (cc -O2, guest cc 8.3): 5^12 = 305,175,781 calls in
747 ms β a clean Γ5 per added connector (run.log). The first version of the
model ran to 15 connectors: 5^15 β 3Γ10^10 calls outran the 120 s SSH
command timeout in optimized userland C β the kernel version does strictly
more work per call (kcalloc at :2444, memcpy, pointer chasing). 20 connectors
β 9.5Γ10^13 calls β days. Model output and the timed-out run both recorded.
Why inconclusive, not reproduced
Demonstrating the kernel behavior needs the DRM device to exist (connector
add + hotplug event). The QEMU guest has no GPU β /dev/dri absent, no drm
kld (env.txt) β so drm_pick_crtcs() cannot be entered at all, let alone
with an inflated connector count. The userspace model proves the complexity
curve, not the kernel lock-hold. Per the honest-status table this is
inconclusive (feature absent on guest) with impact assessed as dos.
Negative results recorded (same area, killed)
- per-CRTC
modeset->connectors[max_conn_count]overflow (:2633+ init at:886-889, i915 max_conn_count=4): unreachable β every MST-capable in-tree driver is multi-CRTC (clone assignment is skipped forcrtc_count > 1at :2474), and i915'sintel_fb_initial_configbails on clone configs (intel_fbdev.c:439-444). Single-CRTC + >4 connectors needs an MST-capable single-pipe part that doesn't exist in this tree. BIT_ULL(connector_count)wrap at :2342: logic-only for counts β₯ 64.- deferred-io fault path, sys/cfb dirty wrappers, setcmap/pan_display/
check_var bodies: compiled out (
#if 0) in this DragonFly port β no attack surface there. - fbdev user copies:
fb_{read,write}wrappers are#if 0'd; the live fbdev glue (register_framebuffer at :2750) has no copyout of kernel memory.
Fix
fix.diff β budget-threaded recursion (2^20 invocations, keep best-so-far on
exhaustion). Applies cleanly to the pristine tree (patch --dry-run).
Kernel-side fix validation impossible on this guest (no GPU):
fix_status: not_testable.
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
not_testablefix.diff (budget-threaded recursion) applies cleanly to the pristine sys/dev/drm/drm_fb_helper.c via patch --dry-run; kernel-side validation impossible on this guest (no GPU), so fix_status=not_testable.
findings/poc/DF-3885/fix.diff
Confirmed kernel references
Detail
Exploit chain
malicious hub enumerates 20+ fake connectors each with fabricated EDID modes -> any HPD cycle -> drm_fb_helper_hotplug_event -> drm_setup_crtcs holds fb_helper->lock across a days-long drm_pick_crtcs search -> all fbdev entry points (lastclose, set_par, blank, ioctl) block -> console/DRM wedge (system DoS).
Evidence (decisive lines)
run.log (measured growth 488281 -> 305175781 calls for 8 -> 12 connectors), build.log (15-connector run outrunning the 120s ssh timeout), VERDICT.md (recurrence derivation + killed adjacent candidates), fix.diff validated clean on pristine tree
PoC changes
no seed; complexity model authored from scratch (capped at 12 connectors after the 15-connector run outran the ssh timeout)
Verified recommended fix
Thread a 2^20-call budget through drm_pick_crtcs and keep the best-so-far assignment when it is exhausted
Verdict
Hardware-gated DoS: drm_pick_crtcs (drm_fb_helper.c:2422-2495) is an exhaustive recursive search with one unconditional plus one-per-CRTC recursion per connector -- (crtcs+1)^connectors calls when all connectors have modes -- with no budget or depth cap, executed under fb_helper->lock from drm_setup_crtcs (:2602, hotplug/initial-config). connector_count grows unbounded via __drm_fb_helper_add_one_connector (:140-175 krealloc) fed by MST port enumeration (intel_dp_mst.c:497 etc.), and the malicious hub also supplies the modes. A 20-connector topology on a 4-pipe i915 is ~9.5e13 calls: the kms worker pins a CPU for days while lastclose/set_par/blank/ioctl block on the same lock. Guest model verified the x5-per-connector curve (305M calls = 748ms at 12 connectors, run.log; the 15-connector run outran a 120s timeout in optimized C). Kernel path not exercisable on this guest (no GPU, /dev/dri absent, env.txt) -- hence inconclusive rather than reproduced.
No comments yet.