drm_open() leaks one drm_device kref per successful open: the success-path drm_minor_acquire() is never balanced on any live path, so drm_dev_release()/drm_dev_fini() can never run and the whole drm_device leaks at unload
Summary
drm_open takes a minor ref and keeps it on success - Linux balances it in drm_release via file_priv->minor, but on DF the live release (drm_close) only balances its OWN close-time acquire; the alternative balance points (drm_release #ifdef, the unregistered dtor) are dead code. Net +1 drm_device kref per open/close cycle: dev->ref never returns to base so drm_dev_release never fires and the entire drm_device + driver-private memory leaks at unload (plus theoretical int-wrap after ~2^31 cycles). Save-and-release-the-open-minor fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4533 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | β | 2.5 KB | view raw | |
| static_proof.sh | β | 1.2 KB | view raw | |
| static_proof.log | β | 2.0 KB | view raw | |
| fix.diff | β | 768 B | view raw | |
| fix_build.log | β | 1.2 KB | view raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| verdict.json | β | 3.3 KB | view raw |
DF-4533 VERDICT
Classification: REAL BUG, inconclusive at runtime (hardware-gated),
source-certain.
Root cause chain
sys/dev/drm/drm_file.c:325βdrm_open()acquires the minor (drm_minor_acquire()βdrm_dev_get(),drm_drv.c:271-290).drm_file.c:343-358β on failure,err_undodrops it (:357). On success the function returns with the reference deliberately held β this mirrors Linux, where the file owns the reference anddrm_release()(the fops->release) drops it viafile_priv->minor.- On DragonFly the release fop equivalent is
drm_close(): -drm_file.c:373β acquires a fresh minor reference (this is the DF-1678-era safety acquire), -drm_file.c:445β releases that fresh reference. The open-held reference from step 2 is never released: net +1 dev kref per open/close cycle. - The two other would-be balances are dead:
-
drm_release()(drm_file.c:595-604) returns 0 immediately on__DragonFly__before any release; -drm_cdevpriv_dtor()(drm_drv.c:1194-1236) is never registered (nodevfs_set_cdevpriv()caller insys/dev/drmβ mechanically proven bystatic_proof.sh).
Consequence
dev->refgrows without bound;drm_dev_release()(drm_drv.c:737-747) never fires, so at driver unload the entirestruct drm_device(and the driver-private memory hanging off it) leaks instead of being finalized bydrm_dev_fini()(drm_drv.c:666-692).- Any video-group user can grow the count by cycling
open()/close()(trigger.c); ~2^31 cycles would wrap the int-based atomic counter (theoretical, hours of continuous cycling).
Severity: Low β bounded real-world impact (one-time dev leak at unload + refcount growth); no memory-safety consequence demonstrated. It sits squarely in the DF-1678 family ("drm_open/release vs unload β the minor refcount protocol"), which pass 2 was asked to extend.
What was run
| Step | Result |
|---|---|
static_proof.sh on the repo |
PASS β all four acquire/release sites enumerated; drm_release() dead on DFly; dtor unregistered |
trigger.c built on guest (-Wall -Wextra -O2) |
clean, RC=0 |
trigger 1000 on guest |
hardware-gated: /dev/dri/card0: No such file or directory |
fix.diff applies to pristine sys/dev/drm/drm_file.c |
verified (patch --dry-run + apply on scratch copy) |
patched drm_file.c compiled with exact kernel CFLAGS |
RC=0 (fix_build.log, object /tmp/drm_file_4533.o) |
| guest source restored | md5 matches repo |
Fix
fix.diff: save file_priv->minor into open_minor at drm_close() entry
(before the kfree at :430) and drm_minor_release(open_minor) at the tail,
next to the existing release of the close's own acquire. Keeps the DF-1678
safety acquire intact while restoring the Linux refcount pairing.
Same-anchor note: DF-4532's fix.diff anchors at the same tail β merge by hand
if both are applied.
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_testablePatch compiles clean (RC=0) with exact X86_64_GENERIC kernel CFLAGS on the guest; applies cleanly to the pristine tree. Runtime validation needs a DRM device (absent here); the fix restores the Linux acquire/release pairing by construction while keeping the DF-1678-era close-time safety acquire.
findings/poc/DF-4533/fix.diff; findings/poc/DF-4533/fix_build.log
Confirmed kernel references
Detail
Evidence (decisive lines)
static_proof.log (PASS); VERDICT.md (full acquire/release ledger); fix.diff (applies clean); fix_build.log (patched drm_file.c compiles RC=0 with exact kernel CFLAGS); run evidence in DF-4531/run.log (hardware-gated trigger output on the shared guest)
PoC changes
Wrote from scratch: trigger.c (open/close cycle driver) and static_proof.sh (tree-level proof of the imbalance).
Verified recommended fix
In drm_close(), save file_priv->minor before kfree(file_priv) and drm_minor_release() it at the tail (balancing drm_open's success-path acquire) - see fix.diff.
Verdict
REAL BUG, runtime-gated on absent hardware. drm_open() takes a minor reference at drm_file.c:325 (drm_minor_acquire -> drm_dev_get) and, mirroring Linux, keeps it on SUCCESS - the open file owns it and Linux's drm_release() drops it via file_priv->minor. On DragonFly the live release path is drm_close(), which releases ONLY its own fresh acquire (drm_file.c:373 acquire / :445 release); the open-held reference is never dropped anywhere: drm_release() is dead on DragonFly (returns at :604) and drm_cdevpriv_dtor() (drm_drv.c:1194) is never registered (no devfs_set_cdevpriv caller - mechanically proven). Net +1 drm_device kref per open/close cycle: dev->ref never returns to base, drm_dev_release()/drm_dev_fini() (drm_drv.c:737/666) can never run, so the whole drm_device leaks at driver unload, and the unbounded int atomic counter can in principle be wrapped by ~2^31 cycles (trigger.c). Low severity (bounded leak, no memory-safety consequence); extends the DF-1678 'open/release vs unload minor refcount protocol' family as requested by pass 2.
No comments yet.