> On Hyper-V the MSI retarget hypercall is asynchronous. When a CPU is taken
> offline, chip_data_update() frees the outgoing CPU's vector inline because the
> deferred cleanup-vector mechanism is unavailable for an offline CPU. A device
> interrupt raised inside the stop_machine window can be posted to the
> outgoing CPU's old vIRR after that inline free, causing two problems:
>
> 1) The completion it carries is lost, as nothing drains the old vector
> once it is freed. If it was the last in-flight completion on an
> otherwise idle queue, the command times out and the controller is
> reset. This is the functional bug.
>
> 2) When the outgoing CPU re-enables interrupts during teardown, the late
> delivery finds VECTOR_UNUSED and reevaluate_vector() logs "No irq
> handler" - harmless but noisy.
>
> The native MSI path (msi_set_affinity()) handles this race with
> VECTOR_RETRIGGERED + irq_retrigger, but the Hyper-V MSI chip carries
> IRQCHIP_MOVE_DEFERRED and reaches the forced-migration else-branch in
> chip_data_update() instead, so it never gets that protection.
>
> Mirror that protection in the forced-migration path:
>
> - Issue __apic_send_IPI(newcpu, newvec) after installing the new mapping
> so a raced completion is drained on the new target. The retarget is
> asynchronous, so the outgoing IRR is not authoritative and the IPI is
> unconditional; a spurious or duplicate ISR is harmless to the MSI
> completion-draining handlers (NVMe, netdev), which find an empty queue.
> - Mark the freed slot VECTOR_RETRIGGERED so the late stray is absorbed by
> reevaluate_vector() rather than logged. The write is unconditional
> because apic_free_vector() leaves the slot pointing at this irq's stale
> desc, not an unused entry (unlike msi_set_affinity()).
>
> The migration runs under stop_machine with interrupts disabled on all CPUs,
> so any raced CQE is visible before the new CPU handles the retrigger.
>
> The guard is restricted to edge MSI vectors on Hyper-V: it requires
> X86_HYPER_MS_HYPERV, an external vector, a non-level trigger, and an
> attached msi_desc. Only the MSI retarget hypercall is asynchronous, so this is
> the sole path with the race; edge IOAPIC lines retarget synchronously via the
> RTE and IR-remapped interrupts via the IRTE, and neither is touched.
> Narrowing to MSI also keeps the unconditional retrigger confined to
> completion-draining handlers, which tolerate a phantom ISR, rather than
> arbitrary edge handlers. The slot marking and completion drain must happen
> where the vector is freed and the new mapping is installed, which is why the
> guard lives in chip_data_update() rather than the Hyper-V MSI chip, which
> cannot reach vector_irq[].
>
> Verified on a 64-CPU Azure VM: zero "No irq handler" events across 320 CPU
> hotplug passes with concurrent NVMe I/O, compared to ~460 per 5 minutes
> before the fix. No NVMe timeouts or controller resets observed.
>
> Fixes: e84cf6aa501c5 ("x86/apic/vector: Handle vector release on CPU unplug
> correctly")
> Cc: [email protected]
> Assisted-by: GitHub-Copilot:claude-opus-4.8
> Signed-off-by: Naman Jain <[email protected]>
I think this patch is correct in that it retriggers interrupts unconditional on
Hyper-V. This is needed because Hyper-V does MSI retarget asynchronously,
unlike all other platforms.
As an alternative, this can also be fixed in fixup_irqs(). IRQ layer already
has this logic in fixup_irqs() for dealing with a lost interrupt when
programming the chip for retargeting, calling chip->irq_retrigger()
unconditionally for Hyper-V in fixup_irqs() is another option.
Thanks,
Long
> ---
> arch/x86/kernel/apic/vector.c | 48 +++++++++++++++++++++++++++++++++++
> 1 file changed, 48 insertions(+)
>
> diff --git a/arch/x86/kernel/apic/vector.c b/arch/x86/kernel/apic/vector.c
> index
> bddc544653999..3577e9ca23ba6 100644
> --- a/arch/x86/kernel/apic/vector.c
> +++ b/arch/x86/kernel/apic/vector.c
> @@ -20,6 +20,7 @@
> #include <asm/i8259.h>
> #include <asm/desc.h>
> #include <asm/irq_remapping.h>
> +#include <asm/hypervisor.h>
>
> #include <asm/trace/irq_vectors.h>
>
> @@ -152,6 +153,7 @@ static void chip_data_update(struct irq_data *irqd,
> unsigned int newvec, unsigne
> struct apic_chip_data *apicd = apic_chip_data(irqd);
> struct irq_desc *desc = irq_data_to_desc(irqd);
> bool managed = irqd_affinity_is_managed(irqd);
> + bool hv_retrigger = false;
>
> lockdep_assert_held(&vector_lock);
>
> @@ -181,7 +183,46 @@ static void chip_data_update(struct irq_data *irqd,
> unsigned int newvec, unsigne
> apicd->prev_cpu = apicd->cpu;
> WARN_ON_ONCE(apicd->cpu == newcpu);
> } else {
> + /*
> + * The outgoing CPU cannot use the deferred cleanup-vector
> + * mechanism, so its vector is freed inline below. On Hyper-V
> the
> + * MSI retarget hypercall is asynchronous, so an interrupt
> raised
> + * inside the stop_machine window can be posted to the
> outgoing
> + * CPU's old vIRR after the free. Two complementary steps
> handle
> + * that (see also the retrigger at the end of the function):
> + *
> + * - Retrigger on the new target so a raced completion is
> drained
> + * there rather than lost. The retarget is asynchronous, so
> the
> + * outgoing IRR is not authoritative and the IPI is issued
> + * unconditionally; a spurious ISR is harmless to
> + * completion-draining handlers (they find an empty queue).
> + * - Mark the freed slot VECTOR_RETRIGGERED so a late stray
> is
> + * absorbed by reevaluate_vector() instead of logging "No irq
> + * handler" while the CPU still takes interrupts during
> + * teardown; __setup_vector_irq() resets it on re-online.
> + *
> + * This mirrors msi_set_affinity()'s protection, which the
> Hyper-V
> + * MSI chip bypasses via IRQCHIP_MOVE_DEFERRED. The
> guard is
> + * restricted to edge MSI vectors on Hyper-V (msi_desc
> present):
> + * only the MSI retarget hypercall is asynchronous, so edge
> IOAPIC
> + * lines (retargeted synchronously via the RTE) and level-
> triggered
> + * lines never see this race and must not be force-injected.
> + */
> + if (hypervisor_is_type(X86_HYPER_MS_HYPERV) &&
> + apicd->vector >= FIRST_EXTERNAL_VECTOR &&
> + !irqd_is_level_type(irqd) && irq_data_get_msi_desc(irqd))
> + hv_retrigger = true;
> apic_free_vector(apicd->cpu, apicd->vector, managed);
> + /*
> + * apic_free_vector() releases the matrix bit but leaves the
> + * outgoing CPU's vector_irq[] slot pointing at the stale desc,
> so
> + * the marker is written unconditionally here. (Unlike
> + * msi_set_affinity(), which marks a genuinely unused slot and
> + * therefore guards with IS_ERR_OR_NULL, the slot here still
> holds
> + * this irq's old desc.)
> + */
> + if (hv_retrigger)
> + per_cpu(vector_irq, apicd->cpu)[apicd->vector] =
> VECTOR_RETRIGGERED;
> }
>
> setnew:
> @@ -190,6 +231,13 @@ static void chip_data_update(struct irq_data *irqd,
> unsigned int newvec, unsigne
> BUG_ON(!IS_ERR_OR_NULL(per_cpu(vector_irq, newcpu)[newvec]));
> per_cpu(vector_irq, newcpu)[newvec] = desc;
> apic_update_irq_cfg(irqd, newvec, newcpu);
> + /*
> + * Drain any completion that raced onto the freed vIRR by retriggering
> + * on the new target (see the else-branch above). Issued after the new
> + * mapping is installed so the handler is present when it is serviced.
> + */
> + if (hv_retrigger)
> + __apic_send_IPI(newcpu, newvec);
> }
>
> static void vector_assign_managed_shutdown(struct irq_data *irqd)
> --
> 2.43.0
>