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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4532 Β· 7 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

DF-4532 VERDICT

Classification: REAL BUG, inconclusive at runtime (hardware-gated), source-certain.

Root cause chain

  1. sys/dev/drm/drm_file.c:351 β€” device_busy(dev->dev->bsddev); on every successful drm_open(). Unconditional; per-open.
  2. The release side of the pair never runs on this port: - drm_close() (drm_file.c:366-448) contains no device_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: zero devfs_set_cdevpriv() callers exist in sys/dev/drm (mechanically verified by static_proof.sh).
  3. sys/kern/subr_bus.c:1844-1853 β€” device_busy(): dev->busy++, dev->state = DS_BUSY (recursively busies dev->parent).
  4. 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_testable
baseline no→ patch + rebuild →patched clean

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

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