drm_open() calls device_busy() on every successful open but no live path ever calls device_unbusy(): drm device and its PCI parent wedged in DS_BUSY forever, permanently blocking device_detach()/kldunload
Summary
drm_open ends every successful open with device_busy (forcing DS_BUSY and recursively busying the PCI parent); drm_close never balances it and the only unbusy sits in the never-registered cdevpriv dtor. After the FIRST open of the card node the driver can never detach or unload again, even with zero open fds. Unpriv video-group user permanently wedges the privileged detach lifecycle. Balance-at-close fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4532 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.sh | β | 1.5 KB | view raw | |
| static_proof.sh | β | 1.3 KB | view raw | |
| static_proof.log | β | 652 B | view raw | |
| fix.diff | β | 393 B | view raw | |
| fix_build.log | β | 1.2 KB | view raw | |
| VERDICT.md | β | 3.0 KB | β raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-4532 VERDICT
Classification: REAL BUG, inconclusive at runtime (hardware-gated),
source-certain.
Root cause chain
sys/dev/drm/drm_file.c:351βdevice_busy(dev->dev->bsddev);on every successfuldrm_open(). Unconditional; per-open.- The release side of the pair never runs on this port:
-
drm_close()(drm_file.c:366-448) contains nodevice_unbusy(). -drm_release()(drm_file.c:595-604) returns 0 immediately on__DragonFly__β dead. -drm_cdevpriv_dtor()(drm_drv.c:1194-1236) β the FreeBSD drm2 code this was ported from released the busy count in exactly this destructor β is never registered: zerodevfs_set_cdevpriv()callers exist insys/dev/drm(mechanically verified bystatic_proof.sh). sys/kern/subr_bus.c:1844-1853βdevice_busy():dev->busy++,dev->state = DS_BUSY(recursively busiesdev->parent).sys/kern/subr_bus.c:2129-2134βdevice_detach():if (dev->state == DS_BUSY) return EBUSY;
Net: one open β device + PCI parent stuck DS_BUSY with a never-decreasing
busy count β kldunload/detach of the drm driver fails forever, even after
all fds are closed. An unprivileged video-group user can wedge this state
with a single open("/dev/dri/card0"); only root notices (unload fails).
Severity rationale: Low
The memory effect is 4 bytes of counter per open; the operational effect
(blocks driver detach/hot-replace, e.g. recovering a wedged GPU by module
reload) requires root to exercise. No memory-safety consequence found (the
counter is not used for any bounds decision; device_unbusy() being absent
means the state merely pins DS_BUSY).
What was run
| Step | Result |
|---|---|
static_proof.sh on the repo |
PASS β busy/unbusy sites enumerated; dtor provably unregistered; no unbusy in drm_file.c |
trigger.sh on guest |
hardware-gated (/dev/dri/card0 absent β QEMU std VGA, no drm driver) |
fix.diff applies to pristine sys/dev/drm/drm_file.c |
verified (patch --dry-run + apply on scratch copy) |
patched drm_file.c compiled with exact kernel CFLAGS (X86_64_GENERIC) |
RC=0 (fix_build.log) |
guest /usr/src/sys/dev/drm/drm_file.c restored |
md5 matches repo |
Fix
fix.diff: balance the device_busy() with device_unbusy(dev->dev->bsddev)
at the tail of drm_close(), after drm_minor_release(). Placement after
the last use of dev is deliberate; it runs exactly once per close
(= once per successful open). Note: DF-4533's fix.diff anchors at the same
line β merge by hand if both are applied together.
fix_status: not_testable (no DRM device on the guest; compile-validated only).
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_testablePatch compiles clean (RC=0) with the exact X86_64_GENERIC kernel CFLAGS on the guest; applies cleanly to the pristine tree. Runtime validation needs a DRM device (absent on this guest); the fix restores the busy/unbusy pairing by construction.
findings/poc/DF-4532/fix.diff; findings/poc/DF-4532/fix_build.log
Confirmed kernel references
Detail
Evidence (decisive lines)
static_proof.log (PASS: all busy/unbusy sites, dtor-unregistered proof, detach-refuses-DS_BUSY excerpt); VERDICT.md; fix.diff (applies clean); fix_build.log (patched drm_file.c compiles RC=0 with exact kernel CFLAGS)
PoC changes
Wrote from scratch: trigger.sh (baseline/unload/open/reload cycle) and static_proof.sh (mechanical tree-level proof).
Verified recommended fix
Add device_unbusy(dev->dev->bsddev) at the tail of drm_close(), balancing drm_open()'s device_busy() (see fix.diff).
Verdict
REAL BUG, runtime-gated on absent hardware. drm_open() calls device_busy(dev->dev->bsddev) on every successful open (drm_file.c:351) but no live path ever calls device_unbusy(): drm_close() (drm_file.c:366-448) has none, drm_release() is dead on DragonFly (returns at :604), and the only device_unbusy() in sys/dev/drm (drm_drv.c:1230) sits in drm_cdevpriv_dtor() which is never registered (zero devfs_set_cdevpriv() callers in the tree - mechanically proven by static_proof.sh). device_busy() increments dev->busy and forces DS_BUSY (subr_bus.c:1844-1853, recursively busying the PCI parent); device_detach() refuses DS_BUSY devices (subr_bus.c:2129-2134). Net: one open of /dev/dri/cardN by any video-group user wedges the drm device (and its PCI parent) in DS_BUSY forever - kldunload/detach fails EBUSY even with zero fds open. Impact bounded to the privileged detach/hot-replace lifecycle (root-only to observe), hence Low; no memory-safety consequence (the count guards no bounds decision).
No comments yet.