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

__drm_helper_disable_unused_functions() NULLs crtc->primary->fb without dropping its framebuffer reference when invoked outside __drm_mode_set_config_internal β€” fb + GEM object reference leak per disabled unused CRTC

Summary

The one-ref-per-fb invariant is established by the wrapper's old_fb get/put dance; drm_helper_resume_force_mode and the exported disable_unused_functions (radeon/amdgpu resume/fbdev-restore paths) invoke the helper wrapper-less - each NULLed pointer loses its reference, pinning the framebuffer + GEM bo forever. Unbounded across suspend/resume cycles. Put-and-clear-old_fb-at-NULL-site fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4471 Β· 3 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 889 B ↓ raw
fix.diff β€” 900 B view raw

DF-4471 β€” wrapper-less __drm_helper_disable_unused_functions NULLs crtc->primary->fb without dropping its reference

File: sys/dev/drm/drm_crtc_helper.c (pass 2; upstream v4.15-identical).

No runnable PoC β€” pure reference-accounting defect, hardware-gated the same way as DF-4469/DF-4470 (needs radeon/amdgpu + a mode configuration with a temporarily-unused CRTC), and the observable effect (fb/GEM object pinned past destroy) needs kernel introspection to see. Verified by code trace:

Reference protocol

"crtc->primary->fb owns exactly one framebuffer reference" β€” established
by __drm_mode_set_config_internal (drm_crtc.c:474-498): before set_config
every plane->old_fb is snapshotted; after it, get(new plane->fb) /
put(old_fb).  Any pointer the helper leaves behind is balanced there.

The defect

sys/dev/drm/drm_crtc_helper.c:181-191
__drm_helper_disable_unused_functions():
    if (!crtc->enabled) {
            ...
            crtc->primary->fb = NULL;     /* line 189: NO put() */
    }

Inside set_config the wrapper's put(old_fb) balances the lost pointer
(get is skipped for the NULL).  But two in-tree callers invoke the
function WITHOUT the wrapper:

  * drm_helper_resume_force_mode()   β€” drm_crtc_helper.c:983
  * drm_helper_disable_unused_functions() (exported) β€” drm_crtc_helper.c:219
    (used by drm_fb_helper.c, radeon_device.c, radeon_fb.c,
    amdgpu_device.c, amdgpu_fb.c β€” resume + fbdev-restore paths)

Each such call drops the plane's fb pointer and leaks its reference:
the drm_framebuffer and its GEM bo can never be freed (memory leak;
DRM_IOCTL_MODE_RMFB of that fb id will also keep reporting it alive).
Repeated suspend/resume cycles (laptop lid events) repeat the leak.

Severity: Low (leak only, safe direction, needs the unused-CRTC state and driver-initiated paths; an unprivileged console user can at most influence vt/fbcon restore frequency).

Fix

Track and drop the reference at the NULL assignment site:
--- a/sys/dev/drm/drm_crtc_helper.c
+++ b/sys/dev/drm/drm_crtc_helper.c
@@ -184,9 +184,11 @@ static void __drm_helper_disable_unused_functions(struct drm_device *dev)
            if (crtc_funcs->disable)
                (*crtc_funcs->disable)(crtc);
            else
                (*crtc_funcs->dpms)(crtc, DRM_MODE_DPMS_OFF);
+           if (crtc->primary->fb)
+               drm_framebuffer_put(crtc->primary->fb);
            crtc->primary->fb = NULL;
        }
    }
 }
…and remove the matching put from the set_config wrapper accounting, or
(simpler policy fix) make the wrapper tolerate the changed refs by
snapshotting counts rather than pointers.
VERDICT.md
↓ download raw

DF-4471 β€” __drm_helper_disable_unused_functions drops primary->fb without its reference

See README.md (contains the full writeup; no separate VERDICT narrative is warranted for this Low/leak-only, code-proven accounting defect β€” the reference protocol derivation and the balanced-callers analysis are there).

Verification: not attempted on the guest (Low severity, leak-only, and the guest has no DRM device). Status: untested. Fix: fix.diff.

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 (Low/leak-only, hardware-gated); fix.diff drops the reference at the NULL site and clears old_fb so the in-wrapper accounting stays balanced (single put)

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

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (protocol derivation + caller inventory), fix.diff

Verified recommended fix

drm_framebuffer_put(crtc->primary->fb) before NULLing it in __drm_helper_disable_unused_functions and clear old_fb so __drm_mode_set_config_internal does not double-put.

Verdict

Reference-accounting defect: __drm_helper_disable_unused_functions() executes crtc->primary->fb = NULL (drm_crtc_helper.c:189) without drm_framebuffer_put(). Inside set_config the wrapper's get(new)/put(old_fb) dance (drm_crtc.c:474-498) absorbs it, but drm_helper_resume_force_mode() (drm_crtc_helper.c:983) and the exported drm_helper_disable_unused_functions() (drm_crtc_helper.c:219, used by drm_fb_helper.c / radeon_device.c / amdgpu_device.c resume+fbdev paths) invoke it wrapper-less, leaking one framebuffer reference (fb + GEM bo pinned forever, RMFB keeps reporting it) per disabled unused CRTC per call. Leak-only, safe direction, hardware-gated the same as DF-4469/4470 - verification not attempted (Low severity).