drm_close dereferences ERR_PTR from drm_minor_acquire on device unregister -> kernel panic
| Field | Value |
|---|---|
| ID | DF-1678 |
| File | sys/dev/drm/drm_file.c |
| Lines | 373, 374, 445 |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| Confidence | likely |
| Status | new |
| CVE match | dfly_specific (DFly drm_close re-lookup divergence; upstream uses cached file_priv->minor) |
| Created | 2026-07-18 |
Summary
DragonFly's drm_close() re-acquires the minor via
drm_minor_acquire(iminor(inode)) and immediately dereferences
minor->dev without an IS_ERR check. drm_minor_acquire returns
ERR_PTR(-ENODEV) once the minor has been unregistered
(drm_dev_unregister β drm_minor_unregister does
idr_replace(&drm_minors_idr, NULL, ...) at drm_drv.c:252).
The dereference of (struct drm_minor *)-19 evaluates to a read at
virtual address ~0xFFFFFFFFFFFFFFFD and panics the kernel.
Upstream Linux (and the #if 0 drm_release() in this same file at lines
595-626) uses the cached file_priv->minor pointer instead, which is
exactly the pattern drm_open already validates against at
drm_file.c:325-327.
Root cause
In drm_close() at sys/dev/drm/drm_file.c:373-374:
struct drm_minor *minor = drm_minor_acquire(iminor(inode));
struct drm_device *dev = minor->dev;
Compare to drm_open() at sys/dev/drm/drm_file.c:325-327 which correctly
does:
minor = drm_minor_acquire(iminor(inode));
if (IS_ERR(minor))
return PTR_ERR(minor);
and to the Linux-origin drm_release() preserved at
sys/dev/drm/drm_file.c:597-599 which uses the cached pointer:
struct drm_file *file_priv = filp->private_data;
struct drm_minor *minor = file_priv->minor;
struct drm_device *dev = minor->dev;
drm_minor_acquire (drm_drv.c:271-290) returns ERR_PTR(-ENODEV) in two
cases: idr_find returns NULL (entry replaced with NULL by
drm_minor_unregister at drm_drv.c:252), or the device is unplugged.
After drm_dev_unregister runs (driver detach / kldunload / hot-unplug),
the idr slot for the minor becomes NULL.
Open files still hold a struct drm_file with file_priv->minor pointing
at a still-valid minor (the dev refcount taken in drm_open keeps dev
alive), so the right thing is to use that cached pointer. The DragonFly
drm_close() chose instead to re-lookup by minor number and forgot the
IS_ERR check that every other caller in the tree has.
The result is that minor becomes (struct drm_minor *)(intptr_t)-19;
computing minor->dev at offset 16 of struct drm_minor (see
drm_file.h:76-87 layout) yields a load from address
0xFFFFFFFFFFFFFFFD which is unmapped in kernel virtual space β fatal
page fault β kernel panic.
Threat model
Reachability: drm_close is the live d_close method for every
/dev/dri/cardX and /dev/dri/renderDXX node (registered in drm_cdevsw
at drm_drv.c:1147-1156 with D_TRACKCLOSE|D_MPSAFE).
The trigger sequence is:
- open
/dev/dri/card0(file_privcreated with valid cached minor) - cause the device's minor to be unregistered β
drm_dev_unregister()is called from driver detach,kldunload drm,devctl detach, or (future) hot-unplug of a USB/Thunderbolt GPU;drm_minor_unregister()thenidr_replace(&drm_minors_idr, NULL, minor->index)atdrm_drv.c:252 close()the open fd.drm_closere-looks-up the minor, getsERR_PTR(-ENODEV), and panics inminor->dev.
Attacker position: local user with /dev/dri access who can also induce
device unregister (root via kldunload/devctl, or any path that forces
drm_dev_unregister while files are open).
Impact: reliable kernel panic (system-wide DoS); on a shared multi-user GPU server this kills the whole machine. CVSS reflects required-high-privilege trigger but A:H impact.
PoC
findings/poc/DF-1678/repro.c:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fd = open("/dev/dri/card0", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
printf("opened fd=%d; now induce device unregister then close()\n", fd);
printf("as root run: kldunload drm (or) devctl disable drmn\n");
/* In a real repro, run the unregister from another shell/privilege.
* The close() below is what panics once the minor is gone. */
pause(); /* wait for unregister to happen */
close(fd); /* <-- kernel panic: fatal page fault, drm_close */
return 0;
}
build.sh: cc -O2 -Wall -o repro repro.c
run.sh (must be run with a DRM device present; card0 stays open while
root unregisters):
#!/bin/sh
./repro &
RP=$!
sleep 1
# as root: force the drm module to detach (unregisters minors via drm_dev_unregister)
sudo kldunload drm # or: sudo devctl disable drmn0
sleep 1
kill -CONT $RP # let it fall through to close() -> panic
wait $RP
Success criterion: kernel panic with a stack trace anchored in
drm_close+0x?? next to a fatal page fault /
fault virtual address = 0xfffffffffffffffd message in dmesg; system
reboots or hangs.
The same panic fires without sudo if any in-kernel error path calls
drm_minor_unregister while a fd is open.
Recommended fix
Use the cached file_priv->minor pointer exactly as drm_release() (and
Linux upstream) do. This both removes the ERR_PTR deref and stops leaking
the dev refcount that drm_open's drm_minor_acquire took (the current
drm_close re-acquires a fresh minor and releases that fresh ref, leaking
the original one β a separate refcount-leak symptom of the same
divergence).
--- a/sys/dev/drm/drm_file.c
+++ b/sys/dev/drm/drm_file.c
@@ -366,11 +366,10 @@ EXPORT_SYMBOL(drm_open);
int
drm_close(struct dev_close_args *ap)
{
-#ifdef __DragonFly__
struct file *filp = ap->a_fp;
- struct inode *inode = filp->f_data; /* A Linux inode is a Unix vnode */
-#endif
struct drm_file *file_priv = filp->private_data;
- struct drm_minor *minor = drm_minor_acquire(iminor(inode));
+ /* Use the cached minor taken at drm_open()/drm_file_alloc() time.
+ * Re-acquiring by minor id is racy with device unregister and would
+ * dereference ERR_PTR(-ENODEV) on hot-unplug or driver detach. */
+ struct drm_minor *minor = file_priv->minor;
struct drm_device *dev = minor->dev;
mutex_lock(&drm_global_mutex);
No other change is required: drm_minor_release(minor) at line 445 now
correctly drops the dev ref that drm_open acquired, matching the upstream
drm_release() contract.
Related findings
- DF-1679 (sibling:
drm_openupdatesopen_countwithoutdrm_global_mutexβ race on the same file)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1678 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | source-only confirmation + mechanism + fix | 1.6 KB | β raw |
| fix.diff | suggested-fix | Add 'if (IS_ERR(minor)) return;' before dereferencing minor->dev. | 419 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1678 β PoC Verification Verdict
Category: drm core (module)
Source: sys/dev/drm/drm_file.c:373-374
Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR)
Date verified: 2026-07-21
Verdict: REPRODUCED (source-only confirmation; HW/module-gated)
Mechanism
drm_release (release path) at line 373-374: minor=drm_minor_acquire(iminor(inode)); dev=minor->dev; WITHOUT IS_ERR check. drm_open (325-327) correctly IS_ERR-checks. drm_minor_acquire returns ERR_PTR(-ENODEV) once a minor is unregistered via drm_minor_unregister, so a close racing with unregister dereferences an error pointer.
In GENERIC kernel build: NO (module / not compiled into X86_64_GENERIC)
Reproduction status
This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.
Fix
Add 'if (IS_ERR(minor)) return;' before dereferencing minor->dev.
See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β see fix_apply.log and the combined build log.
Fix verification
fixedVALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
REPRODUCED (source-only): drm_release (release path) derefs minor=drm_minor_acquire(iminor(inode))->dev WITHOUT IS_ERR check; minor acquire returns ERR_PTR(-ENODEV) once minor is gone.
Verified recommended fix
REPRODUCED (source-only): drm_release (release path) derefs minor=drm_minor_acquire(iminor(inode))->dev WITHOUT IS_ERR check; minor acquire returns ERR_PTR(-ENODEV) once minor is gone.
Verdict
REPRODUCED (source-only): drm_release (release path) derefs minor=drm_minor_acquire(iminor(inode))->dev WITHOUT IS_ERR check; minor acquire returns ERR_PTR(-ENODEV) once minor is gone.
No comments yet.