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

flr_work never drained at mailbox teardown β€” xgpu_ai_mailbox_put_irq() lacks cancel_work_sync(), so a queued FLR work survives put_irq and uses amdgpu_device after hw_fini/free (UAF at driver detach)

Summary

The teardown hook only refcounts the irq down - nothing ever cancels flr_work (only 4 refs in-tree), so a queued FLR work executes through container_of back to adev, mutex/mmdio peeks, and amdgpu_device_gpu_recover AFTER hw_fini (and past drm_dev_put at detach = UAF; on suspend/recovery it forces an unintended second gpu_recover). cancel_work_sync exists in the shim and is already the idiom at amdgpu_irq.c:279 - a plain omission. One-line drain fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4976 Β· 5 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
fix.diff β€” 695 B view raw
verdict.json β€” 3.4 KB view raw
manifest.json β€” 1.1 KB view raw

DF-4976 β€” flr_work never canceled/flushed at mailbox teardown (put_irq): queued work outlives the device

File: sys/dev/drm/amd/amdgpu/mxgpu_ai.c (pass 2, GLM 5.3)

What

xgpu_ai_mailbox_put_irq() (mxgpu_ai.c:365-369) β€” the designated teardown hook, invoked from soc15_common_hw_fini (soc15.c:843-846) β€” only drops the two irq sources. It never cancel_work_sync()/flush_work()s adev->virt.flr_work, and no other teardown path in the tree does either (the only references to flr_work are mxgpu_ai.c:235/237/296/360 and amdgpu_virt.h:246).

A flr_work that was queued by xgpu_ai_mailbox_rcv_irq (mxgpu_ai.c:296) shortly before teardown survives put_irq:

  • the handler xgpu_ai_mailbox_flr_work (mxgpu_ai.c:235-271) immediately touches the device: mutex_trylock(&adev->lock_reset) (:250), RREG32_NO_KIQ mailbox peeks in a msleep(10) loop for up to 500 ms (AI_MAILBOX_POLL_FLR_TIMEDOUT, mxgpu_ai.h:239/…), then amdgpu_device_gpu_recover(adev, NULL) (:270).

Consequences, in increasing severity:

  1. During GPU recovery / suspend (put_irq runs per IP hw_fini): the surviving work re-enters amdgpu_device_gpu_recover concurrently with the recovery/suspend cycle that disabled its irq β€” the mutex serializes them into an unintended back-to-back double recovery racing the fini/resume state machine.
  2. During driver detach (amdgpu_driver_unload_kms, amdgpu_kms.c:75 β†’ amdgpu_device_fini β†’ per-IP hw_fini β†’ put_irq, then drm_dev_put(dev) amdgpu_drv.c:974 frees dev->dev_private == adev): the queued work dereferences freed memory β€” container_of on the dead flr_work (mxgpu_ai.c:237-238), lock/unlock of a freed struct lock, MMIO reads through freed register mappings, and a call into amdgpu_device_gpu_recover on the freed device. Use-after-free.

The DragonFly shim provides the missing idiom β€” cancel_work_sync exists (sys/dev/drm/include/linux/workqueue.h:131) and is already used in this very subsystem's teardown (amdgpu_irq.c:279/310 cancel_work_sync(&adev->reset_work)), so the omission is plainly a bug, not a shim limitation.

Aggravating note (functional, same root): get_irq runs only in late_init (probe/resume). GPU recovery runs hw_fini→put_irq but never re-runs late_init, so after the first recovery the mailbox RCV irq stays disabled — FLR notifications are then silently lost, which both widens the stale-work hazard window at the next resume and degrades host FLR coordination.

Threat model

SR-IOV VF guest (Vega20-class). Queuing requires an IDH_FLR_NOTIFICATION from the host/PF (mxgpu_ai.c:294-296) β€” host-timed, like DF-4755/4975 β€” landing just before hw_fini/put_irq of a detach, suspend, or recovery. A guest-local user can widen the recovery-path variant by causing GPU job timeouts (any user with /dev/dri access can hang the ring long enough for TDR), which forces the recovery teardown that put_irq's without checking for a pending FLR work. Impact ceiling: use-after-free at detach; race at recovery. Low (host-timed, teardown window, VF-only), consistent with the family's ratings.

 void xgpu_ai_mailbox_put_irq(struct amdgpu_device *adev)
 {
    amdgpu_irq_put(adev, &adev->virt.ack_irq, 0);
    amdgpu_irq_put(adev, &adev->virt.rcv_irq, 0);
+   cancel_work_sync(&adev->virt.flr_work);
 }

Reproduce / verification status

Not runnable on the audit guest (no AMD GPU / SR-IOV; see env.txt in the DF-4975 pack, identical guest). Verification is by code trace (VERDICT.md).

VERDICT.md
↓ download raw

DF-4976 VERDICT β€” VERIFIED-BY-TRACE, NOT EXECUTED (hardware absent)

status: untested | reproduced: 0 | impact: none (on this guest) | confidence: likely

Defect proof (path:line)

  1. The work and its only queueing site. flr_work lives in struct amdgpu_virt (amdgpu_virt.h:246). It is queued at exactly one site: xgpu_ai_mailbox_rcv_irq mxgpu_ai.c:296 (schedule_work(&adev->virt.flr_work)), gated on an IDH_FLR_NOTIFICATION from the PF (mxgpu_ai.c:294) β€” i.e., the host decides when this work appears.

  2. The teardown hook does not drain it. xgpu_ai_mailbox_put_irq (mxgpu_ai.c:365-369): c void xgpu_ai_mailbox_put_irq(struct amdgpu_device *adev) { amdgpu_irq_put(adev, &adev->virt.ack_irq, 0); amdgpu_irq_put(adev, &adev->virt.rcv_irq, 0); } amdgpu_irq_put (amdgpu_irq.c:541-562) only refcount-decrements and, at zero, calls .set(DISABLE) β€” it neither knows about nor waits for any work. A tree-wide grep confirms no cancel_work_sync/flush_work of flr_work anywhere (only mxgpu_ai.c:235/237/296/360 reference it). put_irq is called from soc15_common_hw_fini (soc15.c:843-846) β€” i.e., on every IP teardown: driver detach, suspend, and GPU recovery.

  3. What the surviving work does. xgpu_ai_mailbox_flr_work (mxgpu_ai.c:235-271): - :237-238 container_of back to adev β€” the very object whose lifetime is ending; - :250-252 mutex_trylock(&adev->lock_reset), adev->in_gpu_reset = 1; - :254-260 up to 500 ms of RREG32_NO_KIQ mailbox peeks (AI_MAILBOX_POLL_FLR_TIMEDOUT = 500, mxgpu_ai.h); - :269-270 amdgpu_device_gpu_recover(adev, NULL) β€” full reset attempt.

  4. The free. Driver detach: amdgpu_driver_unload_kms (amdgpu_kms.c:75) β†’ amdgpu_device_fini β†’ per-IP hw_fini β†’ soc15_common_hw_fini β†’ put_irq (soc15.c:845) … and afterwards the drm device is released (drm_dev_put(dev) at amdgpu_drv.c:974 on the probe-error path; the unload path drops the final reference the same way), which frees dev->dev_private β€” the kzalloc'd adev (amdgpu_kms.c:180-183). A flr_work queued before put_irq (the irq that scheduled it is now disabled, but the queue entry persists) can be running or get picked up by a worker at any point after that: lock/unlock on freed struct lock memory, MMIO through stale mappings, amdgpu_device_gpu_recover on freed adev β‡’ use-after-free. Even when adev is not yet freed (recovery/suspend variant), the surviving work triggers an unintended second amdgpu_device_gpu_recover racing the fini/resume cycle (gpu_recover takes adev->lock_reset at amdgpu_device.c:3388+, so the two recoveries serialize into a back-to-back double reset rather than a deadlock).

  5. The fix idiom is available and already used next door. cancel_work_sync is implemented by the DFly shim (sys/dev/drm/include/linux/workqueue.h:131; linux_workqueue.c) and is already used at teardown for adev->reset_work (amdgpu_irq.c:279/310) β€” this is an omission, not a port limitation.

Honest boundary

Same guest as DF-4975: no AMD GPU (only QEMU std VGA chip=0x1111234), no SR-IOV VF, amdgpu not compiled into the running kernel (env.txt in the DF-4975 pack). The UAF additionally needs a host-timed FLR notification just before teardown. Cannot be exercised from this guest β‡’ untested (DB poc enum has no not_testable), with the defect established by the trace above.

Exploit chain

None to uid0. The freed-object handler is not attacker-shaped (fixed function, fixed target object); realistic outcome is a panic or memory corruption during teardown β‡’ availability impact, host-timed. Rated Low.

Fix validation

fix_status: not_testable β€” fix.diff adds cancel_work_sync(&adev->virt.flr_work) to xgpu_ai_mailbox_put_irq; cannot be built/booted on this guest (hardware absent). The change guarantees no flr_work remains queued/running once the mailbox irq sources are torn down, which removes the hazard by construction.

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

fix.diff drains flr_work inside the teardown hook before the irq sources are dropped, removing the surviving-work hazard by construction; cannot be build-validated on this guest (no AMD hardware, amdgpu not compiled in).

findings/poc/DF-4976/fix.diff
↓ fix.diffper-fix-DF-4976

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4976/VERDICT.md (trace); decisive refs: mxgpu_ai.c:365-369 (no cancel), soc15.c:843-846 (put_irq at hw_fini), mxgpu_ai.c:287-296 (queue site), mxgpu_ai.c:235-271 (post-teardown touches), amdgpu_kms.c:180 + amdgpu_drv.c:974 (alloc/free of adev), workqueue.h:131 + amdgpu_irq.c:279 (idiom exists)

PoC changes

No runnable PoC exists: needs a Vega20-class SR-IOV VF whose PF times an FLR notification into a teardown window. Pack carries the lifetime trace instead.

Verified recommended fix

Add cancel_work_sync(&adev->virt.flr_work) to xgpu_ai_mailbox_put_irq() (see fix.diff).

Verdict

Real object-lifetime gap, proven by trace, not executable on the audit guest (no AMD GPU / no SR-IOV VF). xgpu_ai_mailbox_put_irq (mxgpu_ai.c:365-369) β€” called from soc15_common_hw_fini (soc15.c:843-846) on every detach/suspend/gpu-recovery teardown β€” drops the two irq sources but never cancel_work_sync/flush_work's adev->virt.flr_work; no other code drains it either. A flr_work queued by the RCV irq just before teardown (host/PF sends IDH_FLR_NOTIFICATION, mxgpu_ai.c:294-296) survives and then executes xgpu_ai_mailbox_flr_work (mxgpu_ai.c:235-271) against a device being torn down β€” mutex on adev->lock_reset, 500 ms of MMIO peeks, amdgpu_device_gpu_recover(adev) β€” and at driver detach continues past drm_dev_put into use-after-free of the kzalloc'd adev (amdgpu_kms.c:180, freed with dev_private). cancel_work_sync exists in the DFly shim (workqueue.h:131) and is already used for adev->reset_work at amdgpu_irq.c:279/310, so the omission is a genuine bug. Guest-local users can widen the recovery variant by forcing TDR via /dev/dri, but queueing itself is host-timed; rated Low like the family (DF-4755).