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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5101 Β· 8 files
FileTypeDescriptionSize
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 of dev->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() in sys/dev/drm/drm_file.c:541-543 runs drm_irq_uninstall() before taking struct_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 holding irqdata_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-execute bus_teardown_intr() / bus_release_resource() / kfree(irq_entry) β€” a latent double-free/UAF.

Why it is only Info (gating, all verified in-tree)

  1. DRM_IOCTL_CONTROL is DRM_AUTH|DRM_MASTER|DRM_ROOT_ONLY (sys/dev/drm/drm_ioctl.c:623), and DRM_ROOT_ONLY is enforced with capable(CAP_SYS_ADMIN) at sys/dev/drm/drm_ioctl.c:538 β†’ the ioctl uninstall path needs root.
  2. That ioctl path and the drm_lastclose β†’ reinit path both early-out for non-DRIVER_LEGACY drivers (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.
  3. Remaining callers are module load/unload/resume paths, single-threaded in practice and root-gated (i915 i915_drv.c:699 / intel_display.c:16197; radeon radeon_irq_kms.c:318/339; amdgpu amdgpu_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.md
↓ download raw

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)

  1. 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 of drm_irq_uninstall() can both observe true.
  2. Callers hold inconsistent locks: the root ioctl path holds dev->struct_mutex (sys/dev/drm/drm_irq.c:257-259), but drm_legacy_dev_reinit() calls drm_irq_uninstall() with no lock and before it takes struct_mutex (sys/dev/drm/drm_file.c:541-552); driver unload paths hold neither.
  3. Amplifier in the DF shim: free_irq() (sys/dev/drm/linux_irq.c:116-141) does its irq_list lookup without irqdata_lock; the lock is only acquired for the SLIST_REMOVE (lines 137-139) after bus_teardown_intr / bus_release_resource / pci_release_msi (lines 129-134) have already run unlocked. Two racing winners therefore both teardown and both kfree(irq_entry) (line 140) β†’ double-free/UAF in the IRQ subsystem.
  4. 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_CONTROL is registered with DRM_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 calls drm_legacy_dev_reinit() for DRIVER_LEGACY (drm_file.c:570-571). Checked all three DRM drivers in this tree: radeon kms_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 + teardown intel_display.c:16197; radeon radeon_irq_kms.c:318,339; amdgpu amdgpu_irq.c:274,302.
  • Secondary race considered and killed: vblank ioctls observing dev->irq_enabled == true during the install window (flag set at drm_irq.c:118 before request_irq at 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_testable
baseline no→ patch + rebuild →patched clean

not_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)
↓ fix.diffper-fix-DF-5101

Confirmed kernel references

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.