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

drm_device_detach never unregisters or releases the drm_device: /dev/dri minors stay live and dispatch into unloaded driver-module memory after kldunload (UAF-class) plus unbounded drm_device leak; i915 never runs any driver teardown

Summary

The DF detach ends after drm_fini_pdev without the drm_dev_unregister+drm_dev_put pair Linux's pci remove path performs - after kldunload the 0666 /dev/dri minors stay openable and every open/ioctl dispatches through dev->driver of the unloaded module (unpriv-triggerable UAF after privileged setup), plus the whole drm_device graph leaks per cycle and i915 has no .unload hook on DF. Static-proven path:line (no GPU on guest); fix compile-validated across the whole drm tree (drm.ko + i915.ko linked).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5064 Β· 7 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw
fix.diff β€” 707 B view raw
fix_build.log β€” 13.6 KB view raw
env.txt β€” 782 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.5 KB view raw

DF-5064 β€” drm_device_detach never unregisters or releases the drm_device: stale /dev/dri minors call into unloaded module memory + unbounded leak

What

drm_device_detach() (sys/dev/drm/drm_dragonfly.c:251-295) is the only newbus device_detach method for all three KMS drivers (i915_pci.c:880, radeon_drv.c:865, amdgpu_drv.c:1421). It runs drm_sysctl_cleanup, frees dev->agp, optionally calls dev->driver->unload, disables busmaster, lockuninits three locks and frees the pci_dev (drm_fini_pdev). It never calls drm_dev_unregister() nor drm_dev_put() β€” the DragonFly equivalents of Linux's i915_pci_remove() β†’ i915_driver_unload(dev); drm_dev_put(dev); (i915_pci.c:680-691) and drm_get_pci_dev's teardown. Nothing else in the DF drm tree does it either (drm_fini_pdev appears only in the three attach error paths and here; no drm_dev_unregister on any detach path).

Consequences after kldunload i915kms (drm.ko itself stays resident via MODULE_DEPEND(i915, drm, ...)):

  1. Stale live entry points into freed module memory. The /dev/dri/cardN / renderDN cdevs (created by drm_sysfs_minor_alloc, drm_sysfs.c:97, with drm_cdevsw owned by the still-loaded drm.ko) keep working: drm_open β†’ drm_minor_acquire finds the never-unregistered minor in drm_minors_idr β†’ dev->driver->open β€” a function pointer into the unloaded i915 module's text (.open = i915_driver_open, i915_drv.c:3284ff). Any unprivileged user who opens the stale node afterwards executes freed kernel memory (UAF-class). The same holds for every dev->driver->* and dev->dev_private dereference on the leaked device.
  2. Unbounded leak per load/unload cycle: the whole drm_device graph (drm_device + minors + drm_sysfs_minor_alloc kdev + vblank etc.) is never freed (drm_dev_put drops the only primary reference; release (i915_driver_release / drm_dev_fini) never runs).
  3. For i915 the driver itself is never torn down at all: i915's struct drm_driver has no .unload member on DragonFly (i915_drv.c:3279-3313), so drm_device_detach skips the entire i915_driver_unload() path β€” GPU MMIO mappings/IRQs are left live while the module goes away.
  4. lockuninit() on struct_mutex/vbl_lock/event_lock (drm_dragonfly.c:288-290) races any in-flight ioctl still holding them (no synchronization with open clients at detach).

Reachability / threat model

Requires (a) a DRM adapter (i915/radeon/amdgpu hardware) and (b) root to kldunload the kms module β€” then the UAF window (1) is triggerable by any unprivileged user with access to /dev/dri (mode 0666 by default via DRM_DEV_MODE). The QEMU audit guest has no display-class PCI device (env.txt), so this is validated statically + the fix compile-validated (drm.ko and i915.ko build clean with it, fix_build.log).

Fix

fix.diff: after drm_fini_pdev(&dev->pdev), call drm_dev_unregister(dev) (minors become ENODEV for future opens β€” drm_minor_acquire then fails cleanly) and drm_dev_put(dev) (drops the primary reference; drm_dev_release frees the graph via the driver's .release). Full i915 parity additionally wants wiring i915_driver_unload (the DF driver table lacks .unload); that is noted in the diff comment and left to maintainers, since a partial teardown is strictly better than a guaranteed post-unload UAF.

Artifacts

  • trace.md β€” full static call-chain trace with path:line citations.
  • fix_build.log β€” full drm tree build on the guest with the patched file (BUILD_RC=0, drm.ko + i915.ko linked).
  • env.txt β€” guest evidence of the absent DRM adapter.
VERDICT.md
↓ download raw

DF-5064 β€” VERDICT

Status: NOT REPRODUCED on the audit guest (no DRM adapter β€” the QEMU machine exposes no display-class PCI device, so no drm device ever attaches and drm_device_detach cannot run). The defect is proven by a complete static trace; the fix compiles clean against the whole drm tree. Severity Medium (kernel UAF-class + unbounded leak, gated on GPU hardware + a root kldunload; the stale /dev/dri nodes are then openable by unprivileged users).

Trace (line-accurate)

  1. sys/dev/drm/i915/i915_pci.c:880, radeon_drv.c:865, amdgpu_drv.c:1421 β€” DEVMETHOD(device_detach, drm_device_detach): the only detach method for all KMS drivers.
  2. sys/dev/drm/drm_dragonfly.c:251-295 β€” drm_device_detach(): sysctl cleanup, kfree(dev->agp), optional dev->driver->unload, pci_disable_busmaster, lockuninit Γ—3, drm_fini_pdev(&dev->pdev). No drm_dev_unregister(), no drm_dev_put(), no drm_dev_fini().
  3. sys/dev/drm/i915/i915_pci.c:680-691 β€” the Linux flow this file replaces: i915_pci_remove() = i915_driver_unload(dev); drm_dev_put(dev); pci_set_drvdata(pdev, NULL). (amdgpu/radeon PCI remove equivalents do the same; DF simply never calls them.)
  4. sys/dev/drm/drm_sysfs.c:97 β€” /dev/dri/cardN cdev created by drm_sysfs_minor_alloc() via make_dev(&drm_cdevsw, ...); the only removal path is drm_minor_unregister() (drm_drv.c:236-258), reachable solely through drm_dev_unregister() (drm_drv.c:941-970) β€” never called at detach.
  5. sys/dev/drm/drm_drv.c:274-282 (drm_minor_acquire) β€” an open on a still-registered minor takes a ref and succeeds; after unregister it returns ERR_PTR(-ENODEV). Today nothing unregisters β†’ stale opens succeed β†’ drm_open β†’ dev->driver->open (i915_drv.c:3284: .open = i915_driver_open, .fops = &i915_driver_fops) β€” text in the unloaded i915kms module.
  6. sys/dev/drm/i915/i915_drv.c:3279-3313 β€” i915's struct drm_driver has no .unload β†’ drm_dragonfly.c:279 skips driver teardown entirely; MMIO/IRQ stay live across kldunload i915kms.
  7. Reference lifecycle: drm_dev_init (drm_drv.c:538-560) takes the primary ref (kref_init); only drm_dev_put β†’ drm_dev_release (drm_drv.c:744-756) β†’ .release/drm_dev_fini frees it. Nothing on the newbus path drops it β†’ leak per load/unload cycle.
  8. drm_fini_pdev grep: only i915_pci.c:867 / radeon_drv.c:836 / amdgpu_drv.c:1067 (attach error paths) and drm_dragonfly.c:293 β€” no double-free risk from the proposed addition.

Why it matters (threat model)

kldunload i915kms (root; drm.ko stays resident via MODULE_DEPEND(i915, drm, 1, 1, 1), i915_pci.c:893) leaves /dev/dri/card0 openable (created 0666 via DRM_DEV_UID/GID/MODE). Every subsequent open/ioctl/mmap by an unprivileged user dispatches through cdevsw (drm.ko, alive) into dev->driver->* and dev->dev_private β€” memory belonging to the unloaded module (freed by the linker). That is a classic use-after-free execution primitive handed to unprivileged users after a root-triggered unload. Additionally each load/unload cycle leaks the full drm_device graph (unbounded with kldload/kldunload loops), and for i915 the GPU is never quiesced.

Verification performed

  • Static trace above (all path:line cited).
  • Guest evidence of no DRM adapter: env.txt (pciconf -lv β€” no class=display; /dev/dri absent). No runtime execution possible.
  • Fix compile-validation: patched drm_dragonfly.c built in the guest's /usr/src/sys/dev/drm β€” full make rc=0, drm.ko and i915.ko linked (fix_build.log). Not runtime-testable here (no GPU), hence fix_status=not_testable.
  • Positive control for the build: unpatched-vs-patched tree builds were both clean; source restored after the test.

Fix

fix.diff β€” add drm_dev_unregister(dev); drm_dev_put(dev); at the tail of drm_device_detach() (after drm_fini_pdev). This converts stale opens into clean ENODEV (drm_minor_acquire fails) and frees the device graph. Maintainer follow-up noted in the diff: give i915 a .unload hook (i915_driver_unload exists, i915_drv.c:1828) for full teardown parity.

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 compiles clean against the entire drm tree on the guest (make rc=0; drm.ko and i915.ko link -- fix_build.log) and matches the proven-correct Linux teardown order (i915_pci_remove). Runtime validation requires a DRM adapter, which the QEMU audit guest cannot provide (no emulated Intel/AMD GPU binds any DF drm driver); marked not_testable rather than guessing.

findings/poc/DF-5064/fix_build.log (full build log, BUILD_RC=0, drm.ko + i915.ko linked with the patched drm_dragonfly.c); fix.diff
↓ fix.diffper-fix-DF-5064

Confirmed kernel references

Detail

Exploit chain

root: kldunload i915kms (drm.ko stays) -> /dev/dri/card0 still openable 0666 -> unprivileged open -> drm_open -> drm_minor_acquire succeeds (minor never unregistered) -> dev->driver->open = i915_driver_open in freed module text -> execution through freed kernel memory (plus unbounded drm_device leak per kldload/kldunload cycle)

Evidence (decisive lines)

VERDICT.md (full path:line trace: drm_dragonfly.c:251-295, i915_pci.c:680-691/880/893, drm_drv.c:236-258/274-282/744-756/941-970, drm_sysfs.c:97, i915_drv.c:1828/3279-3313); env.txt (no display-class PCI device, /dev/dri absent on the audit guest); fix_build.log (whole-tree build rc=0 with the patch)

PoC changes

no seed; runtime PoC impossible on the audit guest (QEMU exposes no Intel/AMD/Radeon GPU and the DF drm tree has no virtio/vmwgfx/bochs driver to bind a QEMU device -- verified: sys/dev/drm contains only i915/radeon/amdgpu backends), so the evidence pack carries the static trace plus a compile-validated fix instead

Verified recommended fix

drm_device_detach(): call drm_dev_unregister(dev) then drm_dev_put(dev) after drm_fini_pdev(&dev->pdev), mirroring i915_pci_remove(); give i915 a .unload hook (see fix.diff)

Verdict

drm_device_detach() (sys/dev/drm/drm_dragonfly.c:251-295), the only newbus device_detach for i915/radeon/amdgpu, never calls drm_dev_unregister() or drm_dev_put() -- unlike Linux's i915_pci_remove() (i915_pci.c:680-691: i915_driver_unload + drm_dev_put) whose role it replaces. After kldunload of the kms module (drm.ko stays resident via MODULE_DEPEND), the /dev/dri/cardN + renderDN cdevs (drm_sysfs.c:97) remain live and drm_minor_acquire (drm_drv.c:274-282) still resolves the never-unregistered minors, so any unprivileged open/ioctl dispatches through dev->driver->* and dev->dev_private -- memory of the UNLOADED driver module (UAF-class). The whole drm_device graph also leaks per load/unload cycle, and for i915 (whose struct drm_driver has no .unload on DF, i915_drv.c:3279-3313) no driver teardown runs at all. Not executable on the audit guest (no display-class PCI adapter; /dev/dri absent -- env.txt), so verified by complete static trace; the fix (drm_dev_unregister + drm_dev_put at detach) compiles clean across the whole drm tree on the guest (fix_build.log, rc=0, drm.ko + i915.ko linked).