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)
PoC verification
Evidence pack
findings/poc/DF-5064 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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, ...)):
- Stale live entry points into freed module memory. The
/dev/dri/cardN/renderDNcdevs (created bydrm_sysfs_minor_alloc, drm_sysfs.c:97, withdrm_cdevswowned by the still-loaded drm.ko) keep working:drm_openβdrm_minor_acquirefinds the never-unregistered minor indrm_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 everydev->driver->*anddev->dev_privatedereference on the leaked device. - Unbounded leak per load/unload cycle: the whole
drm_devicegraph (drm_device + minors +drm_sysfs_minor_allockdev + vblank etc.) is never freed (drm_dev_putdrops the only primary reference; release (i915_driver_release/drm_dev_fini) never runs). - For i915 the driver itself is never torn down at all: i915's
struct drm_driverhas no.unloadmember on DragonFly (i915_drv.c:3279-3313), sodrm_device_detachskips the entirei915_driver_unload()path β GPU MMIO mappings/IRQs are left live while the module goes away. lockuninit()onstruct_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.
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)
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.sys/dev/drm/drm_dragonfly.c:251-295βdrm_device_detach(): sysctl cleanup,kfree(dev->agp), optionaldev->driver->unload,pci_disable_busmaster,lockuninitΓ3,drm_fini_pdev(&dev->pdev). Nodrm_dev_unregister(), nodrm_dev_put(), nodrm_dev_fini().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.)sys/dev/drm/drm_sysfs.c:97β/dev/dri/cardNcdev created bydrm_sysfs_minor_alloc()viamake_dev(&drm_cdevsw, ...); the only removal path isdrm_minor_unregister()(drm_drv.c:236-258), reachable solely throughdrm_dev_unregister()(drm_drv.c:941-970) β never called at detach.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 returnsERR_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.sys/dev/drm/i915/i915_drv.c:3279-3313β i915'sstruct drm_driverhas no.unloadβdrm_dragonfly.c:279skips driver teardown entirely; MMIO/IRQ stay live acrosskldunload i915kms.- Reference lifecycle:
drm_dev_init(drm_drv.c:538-560) takes the primary ref (kref_init); onlydrm_dev_putβdrm_dev_release(drm_drv.c:744-756) β.release/drm_dev_finifrees it. Nothing on the newbus path drops it β leak per load/unload cycle. drm_fini_pdevgrep: 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β noclass=display;/dev/driabsent). No runtime execution possible. - Fix compile-validation: patched
drm_dragonfly.cbuilt in the guest's/usr/src/sys/dev/drmβ fullmakerc=0,drm.koandi915.kolinked (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_testableFix 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
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).
No comments yet.