Non-atomic, lock-inconsistent dev->irq_enabled test-and-clear in drm_irq_uninstall() admits double driver->irq_uninstall()+free_irq(), amplified into a double-free/UAF by the unlocked free_irq() lookup in linux_irq.c
Summary
The uninstall flag transition is unlocked while callers hold mutually inconsistent locks (ioctl holds struct_mutex; drm_lastclose holds nothing) - two racing winners both teardown and both kfree(irq_entry) because the shim's lookup runs before irqdata_lock is taken. Latent today: no in-tree DRIVER_LEGACY driver exists and the ioctl is CAP_SYS_ADMIN+LEGACY gated; root module load/unload is single-threaded in practice. Atomic vbl_lock test-and-set + lock-across-lookup-remove shim fix (two-file diff).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5101 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| probe_drm_guest.sh | β | 806 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.sh | β | 93 B | view raw | |
| build.log | β | 247 B | view raw | |
| run.log | β | 762 B | view raw | |
| fix.diff | β | 2.8 KB | view raw |
DF-5101 β drm_irq.c irq_enabled test-and-clear race (hardening)
What this is
An Info-severity hardening finding filed from the pass-2 audit of
sys/dev/drm/drm_irq.c. The PoC for this pack is a reachability probe:
the race has no in-tree unprivileged trigger and this audit guest has no DRM
device at all, so the honest verdict is inconclusive (feature absent on
guest), not reproduced/not_reproduced.
The claim (static, proven by inspection)
drm_irq_uninstall()(sys/dev/drm/drm_irq.c:182-183) does a non-atomic, lock-inconsistent test-and-clear ofdev->irq_enabled.- One caller takes
dev->struct_mutex(root ioctl path,drm_irq.c:257-259), another caller takes no lock at all (drm_legacy_dev_reinit()insys/dev/drm/drm_file.c:541-543runsdrm_irq_uninstall()before takingstruct_mutex), and driver load/unload paths take neither. - If two callers ever race past the flag check, both execute
driver->irq_uninstall()+free_irq(dev->irq, dev). - The DF
free_irq()shim (sys/dev/drm/linux_irq.c:108-141) performs its list lookup without holdingirqdata_lock(the lock is only taken for the list removal after the teardown at lines 129-135 already ran), so two racing callers can both "find" the entry and double-executebus_teardown_intr()/bus_release_resource()/kfree(irq_entry)β a latent double-free/UAF.
Why it is only Info (gating, all verified in-tree)
DRM_IOCTL_CONTROLisDRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY(sys/dev/drm/drm_ioctl.c:623), andDRM_ROOT_ONLYis enforced withcapable(CAP_SYS_ADMIN)atsys/dev/drm/drm_ioctl.c:538β the ioctl uninstall path needs root.- That ioctl path and the
drm_lastcloseβ reinit path both early-out for non-DRIVER_LEGACYdrivers (sys/dev/drm/drm_irq.c:238-239,sys/dev/drm/drm_file.c:570-571). No in-tree driver sets DRIVER_LEGACY: radeon kms_driver (radeon_drv.c:669-672), amdgpu (amdgpu_drv.c:1303-1305) and i915 (i915_drv.c:3284-3285) are all KMS-only. - Remaining callers are module load/unload/resume paths, single-threaded in
practice and root-gated (i915
i915_drv.c:699/intel_display.c:16197; radeonradeon_irq_kms.c:318/339; amdgpuamdgpu_irq.c:274/302).
Reproduce (probe)
./run.sh # or: dfbsd-qemu/vm.sh run_root '...probe commands...'
No build step (build.sh documents that). Expected on the standard audit
snapshot: no /dev/dri or /dev/drm nodes, no DRM modules on disk, only
the QEMU std VGA device (chip 0x11111234) that no DRM driver claims β
neither install nor uninstall of a DRM irq handler is reachable at runtime on
this guest.
VERDICT β DF-5101 (drm_irq.c irq_enabled race, Info hardening)
Bottom line
INCONCLUSIVE β the code defect is real and proven by source inspection, but it cannot be exercised at runtime on any in-tree buildable configuration, and the standard audit guest (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, QEMU std VGA) has no DRM device, no DRM modules on disk, and no /dev/dri or /dev/drm nodes, so no runtime reproduction attempt is possible there.
The defect (path:line)
sys/dev/drm/drm_irq.c:182-183βirq_enabled = dev->irq_enabled; dev->irq_enabled = false;is a non-atomic test-and-clear. Two racing callers ofdrm_irq_uninstall()can both observetrue.- Callers hold inconsistent locks: the root ioctl path holds
dev->struct_mutex(sys/dev/drm/drm_irq.c:257-259), butdrm_legacy_dev_reinit()callsdrm_irq_uninstall()with no lock and before it takesstruct_mutex(sys/dev/drm/drm_file.c:541-552); driver unload paths hold neither. - Amplifier in the DF shim:
free_irq()(sys/dev/drm/linux_irq.c:116-141) does itsirq_listlookup withoutirqdata_lock; the lock is only acquired for theSLIST_REMOVE(lines 137-139) afterbus_teardown_intr/bus_release_resource/pci_release_msi(lines 129-134) have already run unlocked. Two racing winners therefore both teardown and bothkfree(irq_entry)(line 140) β double-free/UAF in the IRQ subsystem. - The same pattern exists on the install side
(
sys/dev/drm/drm_irq.c:116-118, test-then-set, callers unserialized).
Why it cannot fire in-tree (traced, not assumed)
- Unprivileged angle dead:
DRM_IOCTL_CONTROLis registered withDRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY(sys/dev/drm/drm_ioctl.c:623);DRM_ROOT_ONLYβcapable(CAP_SYS_ADMIN)(drm_ioctl.c:538). This is DF-specific hardening; upstream Linux has only DRM_MASTER here. - Legacy angle dead:
drm_legacy_irq_control()returns early unless DRIVER_LEGACY (drm_irq.c:238-239);drm_lastclose()only callsdrm_legacy_dev_reinit()for DRIVER_LEGACY (drm_file.c:570-571). Checked all three DRM drivers in this tree: radeonkms_driver.driver_features= USE_AGP|HAVE_IRQ|IRQ_SHARED|GEM|PRIME| RENDER (+MODESET at init) βsys/dev/drm/radeon/radeon_drv.c:669-672,751; amdgpu = ...|MODESET|... (amdgpu_drv.c:1303-1305); i915 = ...|MODESET|... (i915_drv.c:3284-3285). None sets DRIVER_LEGACY. - Remaining callers are load/unload/resume (root, effectively
single-threaded): i915 load
i915_drv.c:699+ teardownintel_display.c:16197; radeonradeon_irq_kms.c:318,339; amdgpuamdgpu_irq.c:274,302. - Secondary race considered and killed: vblank ioctls observing
dev->irq_enabled == trueduring the install window (flag set atdrm_irq.c:118beforerequest_irqat 130). In-tree, install only runs during driver load/resume before the device is user-visible or under the driver's modeset serialization, and the consequence would be a lost/spurious interrupt at worst (DoS-grade), not memory corruption. Same ordering as upstream Linux of this vintage β not a DF divergence.
Guest verification (honest classification)
Probe (run.log, run 2026-09-27):
* ls /dev/dri /dev/drm* β No such file or directory (both).
* kldstat β only kernel, ehci.ko, xhci.ko.
* /boot/modules β no drm/i915/radeon/amdgpu modules built.
* pciconf -lv β single display device vgapci0@pci0:0:2.0 chip=0x11111234
(QEMU Virtual Video Controller, std VGA) β claimed by no DRM driver.
* dmesg | grep -i drm β empty.
Conclusion: status=inconclusive, reproduced=false, impact=none,
attempts=1, guest_dirty=0. The finding stands as a defense-in-depth fix
(see fix.diff): serialize the flag transitions under dev->vbl_lock in
drm_irq.c, and hold irqdata_lock across the whole find/teardown/remove
sequence in linux_irq.c:free_irq() so the double-free amplifier is closed
even for future legacy/out-of-tree drivers.
Fix note: dev->vbl_lock is initialized in drm_vblank_init()
(drm_vblank.c:432); every in-tree drm_irq_install() user calls
drm_vblank_init() first, so guarding with vbl_lock is safe for them.
A fully general hardening would also initialize the spinlock at device
allocation (drm_drv.c), which is beyond the minimal diff proposed here.
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_testablenot_testable: guest has no DRM device/driver (see run.log), so neither baseline nor patched behavior is observable at runtime; the diff is proven applicable (patch --dry-run clean against sys/dev/drm/{drm_irq.c,linux_irq.c}) and the edits match the intended fix exactly (diff-verified), but kernel-build + runtime A/B validation could not be performed.
findings/poc/DF-5101/fix.diff (applies with patch -p1 --dry-run: 'checking file sys/dev/drm/drm_irq.c' / 'checking file sys/dev/drm/linux_irq.c'; applied content diff-verified against the intended edit in /tmp/opencode/fixtree)
Confirmed kernel references
- sys/dev/drm/drm_irq.c:116-118
- sys/dev/drm/drm_irq.c:182-183
- sys/dev/drm/drm_irq.c:220
- sys/dev/drm/drm_irq.c:236-239
- sys/dev/drm/drm_irq.c:251-259
- sys/dev/drm/drm_file.c:541-543
- sys/dev/drm/drm_file.c:569-571
- sys/dev/drm/drm_ioctl.c:538
- sys/dev/drm/drm_ioctl.c:623
- sys/dev/drm/linux_irq.c:108-141
- sys/dev/drm/radeon/radeon_drv.c:669-672
- sys/dev/drm/amd/amdgpu/amdgpu_drv.c:1303-1305
- sys/dev/drm/i915/i915_drv.c:3284-3285
- sys/dev/drm/i915/i915_drv.c:699
Detail
Evidence (decisive lines)
findings/poc/DF-5101/run.log (guest probe: no /dev/dri, no /dev/drm*, kldstat shows only ehci/xhci, /boot/modules has no drm modules, pciconf shows only QEMU std VGA 0x11111234, dmesg has no drm lines); VERDICT.md traces the full gating chain with path:line; fix.diff dry-run applies cleanly to sys/dev/drm/{drm_irq.c,linux_irq.c}.
PoC changes
No seed PoC existed (pass-2 finding, new). Authored a guest reachability probe (probe_drm_guest.sh / run.sh) instead of a trigger program because the race has no unprivileged in-tree admission path; probe run via vm.sh run_root.
Verified recommended fix
Serialize dev->irq_enabled transitions under dev->vbl_lock in drm_irq_install/drm_irq_uninstall and hold irqdata_lock across the find+teardown+remove sequence in linux_irq.c free_irq() (see fix.diff).
Verdict
Code defect proven by inspection: drm_irq_uninstall() performs a non-atomic test-and-clear of dev->irq_enabled (drm_irq.c:182-183) while its callers hold inconsistent locks (root ioctl path holds dev->struct_mutex at drm_irq.c:257-259, drm_legacy_dev_reinit() calls it unlocked at drm_file.c:541-543, driver unload paths hold neither). If two callers ever race past the flag, both run driver->irq_uninstall() + free_irq(); the DF free_irq() shim looks up irq_list without holding irqdata_lock (linux_irq.c:116-121) so both can win and double-execute bus_teardown_intr()/bus_release_resource()/kfree(irq_entry) (linux_irq.c:129-140) β a latent double-free/UAF. However, no in-tree buildable configuration can trigger it: DRM_IOCTL_CONTROL is DRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY (drm_ioctl.c:623, enforced at 538), and both the ioctl path and the drm_lastclose reinit path are DRIVER_LEGACY-gated (drm_irq.c:238-239, drm_file.c:570-571) while radeon/amdgpu/i915 (the only drm_irq_install users) are all KMS-only. The audit guest additionally has no DRM hardware, modules or device nodes at all (run.log), so no runtime reproduction is possible. Filed as Info hardening with a verified-applicable fix.diff.
No comments yet.