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

dce_virtual's crtc_irq .set callback invokes the DragonFly hrtimer_cancel emulation, which may sleep (taskqueue_drain) and migrate CPUs, while amdgpu_irq_update holds adev->irq.lock β€” atomic-context contract violation, display-irq stall/lock-protocol hazard

Summary

Every vblank enable/disable runs irq_update holding spin_lock_irqsave(adev->irq.lock) across the .set callback -> hrtimer_cancel, which the DF emulation implements with possible CPU migration and a blocking taskqueue_drain - a sleep while holding the exclusive display-irq lock and the crit section. Linux hrtimer_cancel is atomic-safe; the shim violates the contract (i915 locked contexts call it too). Unpriv DRM user on virtual_display amdgpu hardware: display-path stalls/deadlock exposure. Defer-out-of-callback or make-the-shim-non-blocking fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4643 Β· 4 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
verdict.json β€” 3.0 KB view raw
manifest.json β€” 601 B view raw

DF-4643 β€” dce_virtual .set callback runs the sleeping DragonFly hrtimer_cancel

under adev->irq.lock

  • File: sys/dev/drm/amd/amdgpu/dce_virtual.c:736 (via :744-755), chain into sys/dev/drm/linux_hrtimer.c:121-150
  • Severity: Medium (local DoS / lock-protocol violation; config-gated)
  • Status: untested β€” requires an AMD GPU, kldload amdgpu, and amdgpu.virtual_display=1. The audit guest has no AMD GPU (only QEMU virtual VGA vgapci0) and amdgpu is not in GENERIC.

The chain (all user-triggerable once the driver is up)

Any DRM ioctl that enables/disables a CRTC vblank (page-flip, DRM_IOCTL_WAIT_VBLANK, modeset/DPMS) on a virtual-display GPU:

amdgpu_enable_vblank_kms / amdgpu_disable_vblank_kms   amdgpu_kms.c:1172-1194
  -> amdgpu_irq_get/put(adev, &adev->crtc_irq, idx)    amdgpu_irq.c:499-524
    -> amdgpu_irq_update(adev, src, type)              amdgpu_irq.c:451
       spin_lock_irqsave(&adev->irq.lock)              amdgpu_irq.c:458   <-- crit_enter + lockmgr EXCLUSIVE
       -> src->funcs->set(...)                          amdgpu_irq.c:467
          = dce_virtual_set_crtc_irq_state              dce_virtual.c:744
          -> dce_virtual_set_crtc_vblank_interrupt_state dce_virtual.c:715
             -> hrtimer_cancel(&crtc->vblank_timer)     dce_virtual.c:736
                -> lwkt_setcpu_self(timer->gd)          linux_hrtimer.c:140 (CPU migration)
                -> systimer_del(&timer->st)             linux_hrtimer.c:137/141
                -> taskqueue_drain(tq, &timer->task)    linux_hrtimer.c:144-145
                   -> TQ_SLEEP/ssleep                   subr_taskqueue.c:490-495  (BLOCKS)
       spin_unlock_irqrestore(&adev->irq.lock)          amdgpu_irq.c:468

On Linux, hrtimer_cancel() is atomic-context safe (it spins, never sleeps), which is why the amdgpu code can call it from the .set callback that the irq core invokes under a raw spinlock with IRQs off. The DragonFly emulation (sys/dev/drm/linux_hrtimer.c) violates that contract: cancel may (a) taskqueue_drain β†’ ssleep β€” a blocking sleep while the caller holds adev->irq.lock (a lockmgr lock here) and sits in the crit section entered by spin_lock_irqsave (local_irq_save = crit_enter, see sys/dev/drm/include/linux/irqflags.h:34-37, spinlock.h:75-79); and (b) lwkt_setcpu_self() β€” migrate the thread across CPUs while it holds that lock. Every amdgpu interrupt-state update on all CRTCs serializes behind the sleeping thread; combined with the already-broken cancel state machine (DF-2174: active cleared by the task epilogue makes cancel return without stopping the systimer; DF-2175: hrtimer_start_range_ns re-inits an armed systimer), the disable path can sleep for an unbounded drain and migrate a lock holder, producing long system-wide stalls of the display irq path and compounding the DF-2174 un-cancellable timer.

This is distinct from DF-2174/2175 (both about active-flag/queue corruption inside the emulation): the defect here is the context contract β€” a callback the irq core calls under a lock/irqsave is legally allowed (per Linux semantics every drm driver relies on) to call hrtimer primitives, and the emulation's blocking/migrating implementation of hrtimer_cancel is illegal in that context.

Fix directions

Either (a) in dce_virtual.c: do not touch hrtimers from the .set callback β€” record the desired state and arm/cancel the software vblank timer from a work item (or only from hw_init/hw_fini/SMP-call context), or (b) in linux_hrtimer.c: make hrtimer_cancel non-blocking in atomic context (spin for completion / return -EAGAIN semantics) so the Linux atomic-context contract holds.

VERDICT.md
↓ download raw

DF-4643 β€” VERDICT

Not verified on the guest (untested). The defect is proven by static tracing of the full call chain (see README.md); it cannot be executed on the audit guest because there is no AMD PCI device and amdgpu exists only as a kld module (sys/dev/drm/amd/amd/Makefile, absent from sys/config/X86_64_GENERIC).

What is proven (path:line)

  1. .set is always invoked under the irq lock: amdgpu_irq_update() takes spin_lock_irqsave(&adev->irq.lock, irqflags) (amdgpu_irq.c:458) and calls src->funcs->set(...) (amdgpu_irq.c:467) before releasing (amdgpu_irq.c:468).
  2. dce_virtual's .set is dce_virtual_set_crtc_irq_state (dce_virtual.c:744, registered at :757-760) which forwards to dce_virtual_set_crtc_vblank_interrupt_state (:752) whose disable branch calls hrtimer_cancel (:736).
  3. The emulation's hrtimer_cancel (linux_hrtimer.c:121-150) can: - lwkt_setcpu_self(timer->gd) (line 140) β€” CPU migration of the caller, - systimer_del (137/141) β€” with KKASSERT(gd == mycpu && !SYSTF_IPIRUNNING) (kern_systimer.c:220) reachable only by construction of the emulation, - taskqueue_drain (144-145) β†’ TQ_SLEEP/ssleep (subr_taskqueue.c:490-495) β€” a blocking sleep.
  4. spin_lock_irqsave in the DF drm port = local_irq_save (crit_enter, irqflags.h:58-62/34-37) + lockmgr(LK_EXCLUSIVE) (spinlock.h:75-79) β€” so the caller holds the exclusive display-irq lock and a crit section across the sleeping/migrating cancel.
  5. User reachability once the driver runs: amdgpu_enable_vblank_kms / amdgpu_disable_vblank_kms (amdgpu_kms.c:1172-1194) are the drm_driver vblank hooks; every drmWaitVBlank/page-flip/DPMS cycle drives them. dce_virtual_crtc_dpms (dce_virtual.c:142) calls amdgpu_irq_update directly as well.

Why Medium / dos and not higher

  • Gate: needs an AMD GPU, the amdgpu module loaded by an admin, and the non-default amdgpu.virtual_display=1 β†’ "requires unusual config" (Medium).
  • On DragonFly the lock is a lockmgr lock (sleepable), so this is not the instant hard-freeze it would be on Linux's raw spinlock; the practical impact is a display-irq-path stall (all vblank enable/disable serialized behind a sleeping lock holder) that compounds DF-2174's un-cancellable timer, plus thread-migration of a lock holder β€” a lock-protocol violation with real deadlock exposure under contention, not a demonstrated RCE primitive.

Defer hrtimer arm/cancel out of the .set callback:

--- a/sys/dev/drm/amd/amdgpu/dce_virtual.c
+++ b/sys/dev/drm/amd/amdgpu/dce_virtual.c
@@ -715,6 +715,13 @@ static void dce_virtual_set_crtc_vblank_interrupt_state(struct amdgpu_device *ad
                            enum amdgpu_interrupt_state state)
 {
+   /*
+    * .set runs under adev->irq.lock (spin_lock_irqsave in
+    * amdgpu_irq_update); the DragonFly hrtimer emulation may sleep or
+    * migrate CPUs in hrtimer_cancel(), so arm/cancel must be deferred
+    * to process context.
+    */
    if (crtc >= adev->mode_info.num_crtc || !adev->mode_info.crtcs[crtc]) {
        DRM_DEBUG("invalid crtc %d\n", crtc);
        return;

(concretely: store state in the crtc and let a scheduled work item perform hrtimer_init/start/cancel; alternatively make linux_hrtimer.c's cancel atomic-context-safe system-wide.)

fix.diff was not authored as a standalone applied-and-built artifact because no runtime verification was possible on this guest (fix_status: not_testable).

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

No runtime verification was possible (no AMD hardware); sketch of the local fix is in VERDICT.md.

VERDICT.md fix section.
per-fix-DF-4643

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md/VERDICT.md carry the complete traced chain with path:line for every hop; guest facts (no AMD PCI device, amdgpu absent from GENERIC) recorded in notes.

PoC changes

none β€” no PoC seed; driver not loadable on guest

Verified recommended fix

Defer hrtimer arm/cancel out of the .set callback in dce_virtual.c (record state, act from a work item), or make the DF hrtimer emulation's hrtimer_cancel non-blocking in atomic context.

Verdict

Proven statically, not executable on the audit guest (no AMD GPU; amdgpu is kldload-only). dce_virtual's crtc_irq .set callback (dce_virtual.c:744 -> :715 -> hrtimer_cancel at :736) is always invoked from amdgpu_irq_update under spin_lock_irqsave(&adev->irq.lock) (amdgpu_irq.c:458-468). The DF hrtimer emulation's hrtimer_cancel (linux_hrtimer.c:121-150) may taskqueue_drain -> ssleep (subr_taskqueue.c:490-495) and lwkt_setcpu_self (CPU migration) β€” illegal in the atomic context Linux hrtimer semantics guarantee, so every vblank enable/disable (amdgpu_kms.c:1172-1194, user-triggerable via DRM ioctls) can sleep or migrate while holding the exclusive display-irq lock: display-irq-path stall/deadlock exposure, compounding the known un-cancellable-timer defects DF-2174/DF-2175 (distinct root cause: blocking/migrating cancel vs active-flag corruption). Expected impact dos; gated by AMD hardware + amdgpu.virtual_display=1 -> Medium.