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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4378 Β· 11 files
FileTypeDescriptionSize
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; companion drm_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/dri absent and no drm kld loaded (see run.log). Code defect certain from source (destroy_dev appears nowhere in sys/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) which kzallocs a struct device (:93) and make_dev()s the character device /dev/dri/cardN (:97-98), then drops the returned cdev on the floor (devnode local is never stored).

Teardown never releases either:

  • drm_minor_free() (drm_drv.c:176-196) does idr_remove + kfree(minor) β€” put_device(minor->kdev) is #if 0 (:186-188), the kzalloc'd struct device leaks, and the cdev is never destroy_dev'd (rg destroy_dev sys/dev/drm β†’ 0 hits).
  • drm_dev_unregister() β†’ drm_minor_unregister() (:237-260) only NULLs the idr slot and dev_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/cardN node's si_drv1 (set to the drm_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 + one struct device per minor (driver reload, GPU hotplug), and 2. a stale /dev/dri/cardN entry that collides with the make_dev of 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).

VERDICT.md
↓ download raw

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):

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 does idr_remove + kfree(minor) only. Neither the struct device nor the cdev is released.
  • sys/dev/drm/drm_drv.c:237-260 β€” drm_minor_unregister() NULLs the idr slot and dev_set_drvdata(minor->kdev, NULL); no destroy_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; after drm_dev_fini() frees the drm_device (kref 0 β†’ drm_dev_release β†’ drm_dev_fini β†’ drm_minor_free), the surviving node's si_drv1 is a dangling pointer. Users cannot reach it: post-unregister opens fail ENODEV in drm_minor_acquire() (drm_drv.c:271-290, idr slot NULLed under drm_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 under drm_minor_lock and minors are only freed at dev-refcount 0 in drm_dev_fini, after unregister NULLed the idr slot β€” no acquire/free race.
  • drm_stub_open (:1015-1053) and drm_release glue: #if 0 β€” dead on this port; real open/close are drm_open/drm_close (drm_file.c) with balanced acquire/release.
  • drm_cdevpriv_dtor (:1194-1236): dead code β€” nothing in sys/dev/drm calls devfs_set_cdevpriv; had it been live, its missing kfree(file_priv) (and the duplicate release sequence vs drm_close) would double-release. Worth deleting.
  • drm_mmap_single (:1256-1274): dev = kdev->si_drv1 looks unchecked-NULL, but si_drv1 is set by drm_open_helper before any fd exists, and an open fd pins the drm_device via 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 on kenv_isdynamic (kern_environment.c:229-251/332-336) β†’ pairing is balanced, no bad free.
  • drm_dev_init error path (:635-650): drm_minor_free is NULL-slot-guarded and idempotent; sysctl init/cleanup pairing is correct on every goto path.
  • idr ranges (PRIMARY 0-63 / RENDER 64-127), controlD%d naming, busid_str ksnprintf 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_testable
baseline no→ patch + rebuild →patched clean

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

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 after first guest compile failed; guest-side b64decode used for transfer.

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).