DRM minor teardown leaks the make_dev'd /dev/dri/cardN cdev and the kzalloc'd struct device; stale devfs node keeps si_drv1 dangling at the freed drm_device
Summary
Each drm_minor allocation creates real kernel resources: the sysfs alloc kzallocs a struct device and make_devs the cardN cdev, discarding the returned pointer. Teardown never releases either: drm_minor_free frees only the minor (put_device #if 0'd, no destroy_dev anywhere in sys/dev/drm). After the final dev_put the surviving devfs node's si_drv1 dangles at freed memory. Post-unregister opens fail ENODEV, so this is a per-attach/detach-cycle leak + stale-node hygiene bug, not a user-reachable corruption primitive. Store-and-destroy fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4378 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| chk_drm.c | β | 1.8 KB | view raw | |
| build.sh | β | 78 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 477 B | view raw | |
| run.log | β | 477 B | view raw | |
| env.txt | β | 568 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| manifest.json | β | 868 B | view raw | |
| verdict.json | β | 2.8 KB | view raw |
DF-4378 β drm_drv.c minor teardown leaks the make_dev'd /dev/dri/cardN cdev and minor->kdev; stale node keeps si_drv1 dangling at the freed drm_device
- File:
sys/dev/drm/drm_drv.c(root cause site:drm_minor_free():176-196; companiondrm_sysfs.c:97) - Class: CWE-401/CWE-772 (missing resource release / dangling pointer after free) β Low
- Pass: 2 (GLM 5.3 re-audit); no known findings existed for this file
- Verdict:
not_reproducedβ hardware/module-gated: the audit QEMU guest has no DRM GPU;/dev/driabsent and no drm kld loaded (seerun.log). Code defect certain from source (destroy_devappears nowhere insys/dev/drm).
What a maintainer needs to know
On DragonFly, each drm_minor allocation creates real kernel resources:
drm_dev_init()βdrm_minor_alloc()(drm_drv.c:130) βdrm_sysfs_minor_alloc()(drm_sysfs.c:79) whichkzallocs astruct device(:93) andmake_dev()s the character device/dev/dri/cardN(:97-98), then drops the returnedcdevon the floor (devnodelocal is never stored).
Teardown never releases either:
drm_minor_free()(drm_drv.c:176-196) doesidr_remove+kfree(minor)βput_device(minor->kdev)is#if 0(:186-188), the kzalloc'dstruct deviceleaks, and the cdev is neverdestroy_dev'd (rg destroy_dev sys/dev/drmβ 0 hits).drm_dev_unregister()βdrm_minor_unregister()(:237-260) only NULLs the idr slot anddev_set_drvdata(minor->kdev, NULL); the devfs node persists.- After the final
drm_dev_put()βdrm_dev_fini()(:666-693) βdrm_minor_free(), the surviving/dev/dri/cardNnode'ssi_drv1(set to thedrm_device*at every open, drm_file.c:497) dangles at freed memory. New opens fail safely (drm_minor_acquireβidr_findβ NULL β ENODEV), so the dangling pointer is not directly dereferenceable by users; the practical cost is: 1. a per-attach/detach-cycle leak of one cdev + onestruct deviceper minor (driver reload, GPU hotplug), and 2. a stale/dev/dri/cardNentry that collides with themake_devof the next attachment of the same minor index.
Trigger requires root (kldunload/devctl detach) or physical GPU hotplug β hence Low.
Build / Run / Expected
build.sh # on the DragonFly guest: cc -o chk_drm chk_drm.c run.sh # ./chk_drm
Expected on hardware with a DRM driver: card nodes listed; the leak is observed by
kldunload/kldload cycles (vmstat -m M_DRM growth, /dev/dri entry surviving
unload).
Observed on this guest (run.log): DF-4378: /dev/dri absent β EXIT=2; kldstat
shows only kernel/ehci/xhci; hw.drm sysctl unknown β drm core not even loaded.
Fix
fix.diff β store the cdev in struct drm_minor, destroy_dev() it and
kfree(minor->kdev) in drm_minor_free() (with <sys/conf.h> included before the
function).
DF-4378 VERDICT β not_reproduced (no DRM hardware/module on guest), defect certain from source
Reproduced?
No β the guest has no DRM device at all. chk_drm.c compiled and ran
(BUILD_OK_4378, run.log): /dev/dri is absent, kldstat lists only kernel,
ehci.ko, xhci.ko, and sysctl hw.drm is an unknown OID (the drm core module is not
loaded β QEMU's emulated VGA is handled by the classic vga/vt console stack).
The leak fires only when a DRM driver actually attaches (drm_dev_register β minors β
make_dev) and then detaches; neither exists here.
Why the defect is certain anyway (path:line)
Resource creation (per minor, primary AND render):
sys/dev/drm/drm_drv.c:158βminor->kdev = drm_sysfs_minor_alloc(minor);sys/dev/drm/drm_sysfs.c:93βkdev = kzalloc(sizeof(*kdev), GFP_KERNEL);sys/dev/drm/drm_sysfs.c:97-98βdevnode = make_dev(&drm_cdevsw, minor->index, DRM_DEV_UID, DRM_DEV_GID, DRM_DEV_MODE, "dri/%s", dev_str);β the returned cdev is stored in a local and never saved anywhere.
Resource release that never happens:
sys/dev/drm/drm_drv.c:176-196βdrm_minor_free():put_device(minor->kdev)is compiled out (#if 0, :186-188); the function doesidr_remove+kfree(minor)only. Neither thestruct devicenor the cdev is released.sys/dev/drm/drm_drv.c:237-260βdrm_minor_unregister()NULLs the idr slot anddev_set_drvdata(minor->kdev, NULL); nodestroy_dev.- Whole-tree check:
rg destroy_dev sys/dev/drmβ zero matches. The devfs node outlives the driver forever. sys/dev/drm/drm_file.c:497βkdev->si_drv1 = dev;is written at open time on the cdev; afterdrm_dev_fini()frees thedrm_device(kref 0 βdrm_dev_releaseβdrm_dev_finiβdrm_minor_free), the surviving node'ssi_drv1is a dangling pointer. Users cannot reach it: post-unregister opens fail ENODEV indrm_minor_acquire()(drm_drv.c:271-290, idr slot NULLed underdrm_minor_lock), and mmap requires a successful open first.
Exploitability: not a memory-safety primitive for an unprivileged user β a resource/lifetime hygiene bug (kernel heap leak per driver attach/detach cycle + stale devfs entry + name collision on re-attach of the same minor index). Trigger is privileged (kldunload/devctl) or physical (hotplug) β Low.
What I checked and killed on this file (pass-2 negatives)
drm_minor_acquire/release(drm_drv.c:271-295): the kref is taken underdrm_minor_lockand minors are only freed at dev-refcount 0 indrm_dev_fini, after unregister NULLed the idr slot β no acquire/free race.drm_stub_open(:1015-1053) anddrm_releaseglue:#if 0β dead on this port; real open/close aredrm_open/drm_close(drm_file.c) with balanced acquire/release.drm_cdevpriv_dtor(:1194-1236): dead code β nothing in sys/dev/drm callsdevfs_set_cdevpriv; had it been live, its missingkfree(file_priv)(and the duplicate release sequence vsdrm_close) would double-release. Worth deleting.drm_mmap_single(:1256-1274):dev = kdev->si_drv1looks unchecked-NULL, butsi_drv1is set bydrm_open_helperbefore any fd exists, and an open fd pins thedrm_devicevia the minor acquire ref β not reachable with NULL/dangling.dmi_found(:1281-1339):kgetenv()returns either a kmalloc'd copy (dynamic kenv) or a static pointer;kfreeenv()self-guards onkenv_isdynamic(kern_environment.c:229-251/332-336) β pairing is balanced, no bad free.drm_dev_initerror path (:635-650):drm_minor_freeis NULL-slot-guarded and idempotent; sysctl init/cleanup pairing is correct on every goto path.- idr ranges (PRIMARY 0-63 / RENDER 64-127),
controlD%dnaming,busid_strksnprintf sizing,drm_debug/tunable plumbing: all in-bounds/benign.
Fix validation
fix.diff authored against the read-only tree (adds struct cdev *devnode to
struct drm_minor, stores the make_dev result, and destroys/frees both in
drm_minor_free, with <sys/conf.h> included before the function). Not
applied; fix validation not_testable β the guest cannot attach a DRM driver,
so neither the baseline leak nor the patched behavior can be observed. The diff is
compile-checkable by inspection (cdev_t == struct cdev *; incomplete-type member
declaration is valid in drm_file.h; destroy_dev prototype comes from
<sys/conf.h>).
Guest evidence
run.log: uname, BUILD_OK_4378,/dev/dri absent, EXIT=2, kldstat (no drm),sysctl: unknown oid 'hw.drm'.env.txt: guest identity + absence summary.
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 authored against the read-only tree, never applied; cannot validate on the guest since no DRM driver can attach there (no GPU). Diff is minimal and compile-safe by inspection (incomplete-type cdev pointer in drm_file.h, destroy_dev via sys/conf.h).
fix.diff in this pack; guest absence evidence in run.log/env.txt.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (creation vs release path:line, plus the killed-candidates list incl. dead drm_cdevpriv_dtor and balanced kgetenv/kfreeenv); run.log shows BUILD_OK_4378, /dev/dri absent, kldstat without drm, unknown oid hw.drm.
PoC changes
Probe written from scratch (no seed); added
Verified recommended fix
Store the make_dev() cdev in struct drm_minor and destroy_dev() it plus kfree(minor->kdev) in drm_minor_free() (fix.diff: drm_drv.c + drm_file.h + drm_sysfs.c).
Verdict
drm_minor_free() (drm_drv.c:176-196) kfrees the drm_minor while the resources its allocation created are never released: drm_sysfs_minor_alloc() kzallocs a struct device (drm_sysfs.c:93) and make_dev()s /dev/dri/cardN (drm_sysfs.c:97) storing the cdev nowhere; put_device is #if 0 and destroy_dev appears nowhere in sys/dev/drm. Every driver attach/detach (or module load/unload) cycle leaks one cdev + one struct device per minor, and the stale /dev/dri node keeps si_drv1 (set at open, drm_file.c:497) dangling at the freed drm_device. Not user-triggerable memory corruption (post-unregister opens fail ENODEV via the idr NULL slot); privileged/hotplug-gated resource leak -> Low. Not reproduced on the audit guest: no DRM GPU, drm module not loaded (chk_drm built and ran, /dev/dri absent, exit 2).
No comments yet.