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

AI (Vega20-class) mailbox RCV interrupt enabled before flr_work is initialized β€” host-timed IDH_FLR_NOTIFICATION executes schedule_work() on a zeroed work_struct β†’ NULL function-pointer call β†’ kernel panic (DF-4755 twin)

Summary

The exact DF-4755 ordering defect in the AI generation: amdgpu_irq_get at :351 writes VALID_INT_EN=1 nine lines before INIT_WORK at :360, and amdgpu_sriov_runtime is already true at late_init - a host-timed FLR in the window schedules the zeroed work and the shim calls func==NULL (linux_workqueue.c:89). Window re-opens on every resume. INIT_WORK-first fix. SR-IOV hw-gated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4975 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 4.7 KB ↓ raw
fix.diff β€” 806 B view raw
verdict.json β€” 3.2 KB view raw
manifest.json β€” 1020 B view raw
env.txt β€” 1.1 KB view raw

DF-4975 β€” AI (Vega20-class) mailbox RCV interrupt enabled before flr_work is initialized (DF-4755 twin)

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

What

xgpu_ai_mailbox_get_irq() enables the BIF mailbox RCV interrupt source (amdgpu_irq_get at mxgpu_ai.c:351 β†’ xgpu_ai_set_mailbox_rcv_irq mxgpu_ai.c:273-285 β†’ VALID_INT_EN=1 in BIF_BX_PF0_MAILBOX_INT_CNTL) before INIT_WORK(&adev->virt.flr_work, ...) at mxgpu_ai.c:360.

Any IDH_FLR_NOTIFICATION delivered by the host/PF inside that window is handled by xgpu_ai_mailbox_rcv_irq (mxgpu_ai.c:287-310), which β€” because amdgpu_sriov_runtime(adev) is already true at this point (proof below) β€” executes schedule_work(&adev->virt.flr_work) (mxgpu_ai.c:296) on a zeroed (kzalloc'd, never INIT_WORK'ed) work_struct.

Under DragonFly's DRM workqueue shim (sys/dev/drm/include/linux/workqueue.h:47-56, sys/dev/drm/linux_workqueue.c:120-140), queue_work() on a zeroed work has on_queue == false, so the work is STAILQ-inserted (the NULL link pointers are simply overwritten) and later executed by the worker at linux_workqueue.c:89 via work->func(work) where func == NULL β†’ call through NULL function pointer β†’ kernel panic.

Structurally identical to DF-4755 (mxgpu_vi.c:600-609), which pass 2 filed for the VI generation. This is the AI-generation twin; mxgpu_ai.c had no prior finding of this class.

Why the schedule_work path is live during the window

xgpu_ai_mailbox_rcv_irq only schedules when amdgpu_sriov_runtime(adev) (AMDGPU_SRIOV_CAPS_RUNTIME) is set. Boot order proves it is set before late_init β†’ get_irq runs:

  1. amdgpu_device_init() β†’ amdgpu_virt_request_full_gpu(adev, true) (amdgpu_device.c:1497) clears RUNTIME (amdgpu_virt.c:259).
  2. End of amdgpu_device_init() β†’ amdgpu_virt_release_full_gpu(adev, true) (amdgpu_device.c:1696) sets RUNTIME (amdgpu_virt.c:282).
  3. Only later, amdgpu_device_ip_late_init() (amdgpu_device.c:2700) β†’ soc15_common_late_init (soc15.c:800-804) β†’ xgpu_ai_mailbox_get_irq() β†’ irq_get(rcv) at mxgpu_ai.c:351 … INIT_WORK at mxgpu_ai.c:360.

adev itself is kzalloc'd (amdgpu_kms.c:180), so flr_work is all-zero until line 360. The race window is the irq enable β†’ INIT_WORK distance (two register write round trips + a second amdgpu_irq_get), into which a host-timed (or merely unlucky) FLR notification lands.

The same window re-opens on resume: amdgpu_device_resume() re-runs amdgpu_device_ip_late_init() (amdgpu_device.c:2936) β†’ get_irq() again.

Threat model

SR-IOV VF guest only (Vega20-class AI asic). The trigger actor is the host/PF (trusted boundary), so this is host-timed DoS/panic β€” same rating basis as DF-4755: Low. Impact when it fires: guest kernel panic (NULL func call in a workqueue worker thread).

Initialize the work before enabling the interrupt source:

 int xgpu_ai_mailbox_get_irq(struct amdgpu_device *adev)
 {
    int r;

+   INIT_WORK(&adev->virt.flr_work, xgpu_ai_mailbox_flr_work);
+
    r = amdgpu_irq_get(adev, &adev->virt.rcv_irq, 0);
    if (r)
        return r;
    r = amdgpu_irq_get(adev, &adev->virt.ack_irq, 0);
    if (r) {
        amdgpu_irq_put(adev, &adev->virt.rcv_irq, 0);
        return r;
    }
-
-   INIT_WORK(&adev->virt.flr_work, xgpu_ai_mailbox_flr_work);

    return 0;
 }

Reproduce / verification status

Not runnable on the audit guest: requires an AI-generation (Vega20-class) ASIC in SR-IOV VF mode; the QEMU guest has only a QEMU std VGA device (see env.txt). Verification is by complete code-order trace (VERDICT.md).

VERDICT.md
↓ download raw

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

status: untested | reproduced: 0 | impact: none (on this guest) | code-path confidence: certain / manifestation: likely

Why this is a real defect (line-by-line)

  1. Enabling before init. xgpu_ai_mailbox_get_irq(): - mxgpu_ai.c:351 amdgpu_irq_get(adev, &adev->virt.rcv_irq, 0) β€” first enable of the source (refcount 0β†’1) calls amdgpu_irq_update() (amdgpu_irq.c:511-525) which invokes the .set callback xgpu_ai_set_mailbox_rcv_irq (mxgpu_ai.c:273-285) β†’ writes VALID_INT_EN=1 to mmBIF_BX_PF0_MAILBOX_INT_CNTL (mxgpu_ai.c:280-282). From this instruction onward the device can raise the mailbox RCV irq. - mxgpu_ai.c:360 INIT_WORK(&adev->virt.flr_work, xgpu_ai_mailbox_flr_work) β€” the work is only now given a func. Between 351 and 360 it is a zeroed blob (adev is kzalloc'd, amdgpu_kms.c:180-183; no other initializer exists β€” INIT_WORK for flr_work appears exactly once in the tree, at mxgpu_ai.c:360).

  2. The ISR path is live in that window. xgpu_ai_mailbox_rcv_irq (mxgpu_ai.c:287-310): peeks RCV_DW0 (line 291); for IDH_FLR_NOTIFICATION (line 294) checks amdgpu_sriov_runtime(adev) (line 295) then schedule_work(&adev->virt.flr_work) (line 296). - RUNTIME bit proven set before late_init: amdgpu_device_init() ends with amdgpu_virt_release_full_gpu(adev, true) at amdgpu_device.c:1696, which sets AMDGPU_SRIOV_CAPS_RUNTIME (amdgpu_virt.c:282). amdgpu_device_ip_late_init() (amdgpu_device.c:2700) β†’ soc15_common_late_init (soc15.c:800-804) β†’ get_irq() runs strictly after. So line 295's gate is open. - In this port .process handlers run in hard-IRQ context (same basis as DF-4756's "hard-IRQ .process handler"); schedule_work from hardirq is supported and intended.

  3. What a zeroed work does under the DFly shim. struct work_struct (sys/dev/drm/include/linux/workqueue.h:47-56): ws_entries (STAILQ), func, worker, on_queue, running, canceled. Zeroed β‡’ func == NULL, on_queue == false. queue_work() (sys/dev/drm/linux_workqueue.c:120-140): sees on_queue == false β†’ STAILQ_INSERT_TAIL (overwrites the NULL link pointers β€” no immediate list corruption) β†’ marks on_queue. Worker process_all_work() (linux_workqueue.c:66-100): dequeues, sets running = true, drops worker_lock, then line 89 work->func(work) β†’ indirect call through NULL β‡’ kernel panic. This is exactly the mechanism DF-4755 documented for the VI twin.

  4. Host control of the trigger. The PF writes RCV_DW0 and sets RCV_MSG_VALID; the VF cannot block that. The window is short (two register round-trips) but re-opens on every resume (amdgpu_device_resume re-runs amdgpu_device_ip_late_init, amdgpu_device.c:2936). A host that performs an FLR around VM resume has a realistic shot at the window; an unlucky benign host can hit it too. Consequence: guest kernel panic (availability only; no control over the called pointer β€” it is deterministically NULL).

Why not executed on the guest (honest boundary)

The audit QEMU guest (DragonFly dfbsd 6.5-DEVELOPMENT #0 ... X86_64_GENERIC) has: - no AMD GPU at all β€” the only display device is QEMU std VGA (vgapci0@pci0:0:2.0 ... chip=0x1111234, see env.txt); - no SR-IOV virtual function (it is not an SR-IOV guest); - no drm/amdgpu compiled into the running kernel (sysctl kern.conftxt matches zero drm|ath devices).

The code path additionally requires a host/PF to time an FLR notification into the probe window. Running the guest cannot produce this by any local action; classification is therefore untested on this hardware rather than not_reproduced. The defect itself is proven by the static trace above (every hop cited path:line).

Exploit chain

None β€” the called pointer is NULL by construction (kzalloc), so the outcome is a deterministic panic, not controlled execution. Impact ceiling: host-timed guest DoS at probe/resume. Rated Low, mirroring DF-4755.

Fix validation

fix_status: not_testable β€” fix.diff (INIT_WORK moved above both amdgpu_irq_get calls) is authored but cannot be built+exercised on this guest for the same hardware-absence reason. The change removes the window by construction: the work is fully initialized before the interrupt source can deliver anything.

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 moves INIT_WORK before the interrupt-source enable, removing the window by construction; cannot be build-validated on this guest (no AMD hardware, amdgpu not compiled in), hence not_testable.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4975/VERDICT.md (hop-by-hop trace); findings/poc/DF-4975/env.txt (guest hardware absence); decisive refs: mxgpu_ai.c:351 vs mxgpu_ai.c:360; mxgpu_ai.c:287-310; amdgpu_virt.c:282; amdgpu_device.c:1696 vs :2700; amdgpu_kms.c:180; linux_workqueue.c:89

PoC changes

No runnable PoC exists for this class: requires Vega20-class SR-IOV VF plus a PF timing an FLR notification into the probe window. Pack carries the code-order proof instead.

Verified recommended fix

Move INIT_WORK(&adev->virt.flr_work, ...) above both amdgpu_irq_get() calls in xgpu_ai_mailbox_get_irq() (see fix.diff).

Verdict

Real init-order race, proven by full code trace but not executable on the audit guest (no AMD GPU / no SR-IOV VF / amdgpu not compiled in). xgpu_ai_mailbox_get_irq() enables the mailbox RCV interrupt source (mxgpu_ai.c:351 amdgpu_irq_get -> xgpu_ai_set_mailbox_rcv_irq VALID_INT_EN=1) before INIT_WORK(&adev->virt.flr_work) at mxgpu_ai.c:360; a host/PF-timed IDH_FLR_NOTIFICATION in the window hits xgpu_ai_mailbox_rcv_irq:296 schedule_work() on a kzalloc-zeroed work_struct, and the DFly workqueue shim executes work->func(work) with func==NULL (linux_workqueue.c:89) -> kernel panic. RUNTIME caps bit is proven set before late_init (amdgpu_device.c:1696 release_full_gpu sets it via amdgpu_virt.c:282; amdgpu_device_ip_late_init at amdgpu_device.c:2700 runs later), so the schedule_work gate is open. Twin of DF-4755 (mxgpu_vi.c:600-609); mxgpu_ai.c had no prior finding of this class. Window also re-opens on resume (amdgpu_device.c:2936 re-runs late_init).