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

Duplicate connector IDs in DRM_IOCTL_MODE_SETCRTC leak one drm_connector reference per duplicate slot per successful modeset β€” per-slot get vs per-connector binding, surplus dropped only on the fail path

Summary

SETCRTC copies user connector IDs without de-dup; the get loop takes one ref per ARRAY SLOT for unbound connectors, but the dev-list traversal establishes exactly one binding per connector and the surplus put loop exists only under fail:. An unbound connector listed k>=2 times leaks k-1 refs per successful call; once destroyed (MST unplug) its refcount never reaches zero. All 8 combinations traced - only unbound-dup-success is imbalanced. Reject-duplicates-at-ioctl fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4470 Β· 8 files
FileTypeDescriptionSize
setcrtc_dup.c β€” 8.3 KB view raw
build.sh β€” 89 B view raw
run.sh β€” 118 B view raw
build.log β€” 100 B view raw
run.log β€” 188 B view raw
env.txt β€” 411 B view raw
VERDICT.md β€” 3.0 KB ↓ raw
fix.diff β€” 656 B view raw
VERDICT.md
↓ download raw

DF-4470 VERDICT β€” duplicate connector IDs leak one reference per successful modeset

Status: INCONCLUSIVE (arithmetic certain; guest lacks DRM device)

Root cause (path:line)

  1. sys/dev/drm/drm_crtc.c:721-741 β€” drm_mode_setcrtc copies user connector IDs into connector_set[] with per-ID lookup and no de-duplication; set->num_connectors = crtc_req->count_connectors (line 749).
  2. sys/dev/drm/drm_crtc_helper.c:626-630 c for (ro = 0; ro < set->num_connectors; ro++) { if (set->connectors[ro]->encoder) continue; drm_connector_get(set->connectors[ro]); /* once per SLOT */ }
  3. Binding happens once per (dev-list) connector β€” not per array slot β€” at drm_crtc_helper.c:635-666 (connector->encoder = new_encoder). The reference protocol is "one reference while the encoder is bound" (comment at drm_crtc_helper.c:468, drop at drm_crtc_helper.c:469).
  4. The surplus drop exists ONLY on the failure path (drm_crtc_helper.c:775-779); the success return at lines 755-757 has no put loop.

β‡’ an unbound connector listed kβ‰₯2 times in set_connectors receives k gets and 1 binding: +(kβˆ’1) leaked references per successful setcrtc. A bound connector duplicated is balanced (get skipped, put skipped).

Impact: the kref on drm_connector never again reaches 0 after the connector is destroyed (DP MST unplug path, drm_connector_unregister+cleanup) β€” the connector object (plus its name string, property blobs) is leaked permanently. Bounded-per-call (no per-reference allocation), unbounded in call count; requires DRM master (local unprivileged user on a headless system). Upstream v4.15-identical; Linux master still carries the same get/put asymmetry (checked 2026-09).

The fail path is balanced for duplicates: k gets (step 2) vs k puts (lines 775-779 iterate the array), verified by tracing all four bound/unbound Γ— dup/no-dup Γ— success/fail combinations β€” only unbound-dup-success leaks.

Fix

De-duplicate in drm_mode_setcrtc (drm_crtc.c:721-741):

--- a/sys/dev/drm/drm_crtc.c
+++ b/sys/dev/drm/drm_crtc.c
@@ -729,6 +729,11 @@ int drm_mode_setcrtc(struct drm_device *dev, void *data,
            ret = -ENOENT;
            goto out;
        }
+       for (j = 0; j < i; j++) {
+           if (connector_set[j] == connector) {
+               ret = -EINVAL;  /* duplicate connector id */
+               goto out;
+           }
+       }
        DRM_DEBUG_KMS("[CONNECTOR:%d:%s]\n",
            connector->base.id,
            connector->name);

(and declare int j;) β€” or make the get loop in drm_crtc_helper.c idempotent per connector.

Attempts

  • Guest build: build.log (same binary as DF-4469).
  • Guest run: no /dev/dri β†’ run.log (ENOENT).

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

not validated on guest (no DRM device); fix.diff rejects duplicate connector ids at drm_mode_setcrtc with -EINVAL before any reference is taken

findings/poc/DF-4470/fix.diff
↓ fix.diffper-fix-DF-4470

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (get/put arithmetic over all combinations), setcrtc_dup.c (dup-mode harness), build.log/run.log (guest ENOENT), fix.diff (de-dup in drm_mode_setcrtc)

PoC changes

shared harness with DF-4469; dup variant added full fb+mode setup (disable-only setcrtc never reaches the get loop)

Verified recommended fix

Reject duplicate connector IDs in drm_mode_setcrtc (drm_crtc.c) or make the get loop idempotent per connector.

Verdict

drm_mode_setcrtc copies user connector IDs without de-duplication (drm_crtc.c:721-741); drm_crtc_helper_set_config takes one drm_connector_get() per ARRAY SLOT for unbound connectors (drm_crtc_helper.c:626-630) but establishes exactly one binding per connector (drm_crtc_helper.c:657-666) and the surplus-drop loop exists only on the fail path (drm_crtc_helper.c:775-779) - the success return (755-757) never drops it. An unbound connector listed k>=2 times therefore leaks k-1 references per successful modeset; after the connector is destroyed (MST unplug) its refcount never reaches zero and the object leaks permanently. All 8 bound/unbound x dup/no-dup x success/fail combinations traced - only unbound-dup-success is imbalanced. Not reproducible on the audit guest (no DRM device; /dev/dri absent - open returns ENOENT); arithmetic is fully code-proven. Upstream v4.15-identical and still present in current Linux.