β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3885

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3885 Β· 11 files
FileTypeDescriptionSize
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.md
↓ download raw

VERDICT β€” DF-3885

status: inconclusive (kernel path hardware-gated) / impact assessed: dos / confidence: likely

Why the blowup is real

  1. 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 whenever modes[n] != NULL for all n. There is no call budget anywhere.
  2. drm_setup_crtcs() calls it at :2602 with fb_helper->lock held (lockdep_assert at :2565), from drm_fb_helper_hotplug_event() (:2870) and __drm_fb_helper_initial_config_and_unlock (:2717).
  3. 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.
  4. Lock consequence: fb_helper->lock stays 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 for crtc_count > 1 at :2474), and i915's intel_fb_initial_config bails 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3885

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.