mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "guanghuifeng@linux.alibaba.com" <guanghuifeng@linux.alibaba.com>
To: "Suthikulpanit, Suravee" <suravee.suthikulpanit@amd.com>,
	joro@8bytes.org, vasant.hegde@amd.com, will@kernel.org,
	robin.murphy@arm.com, iommu@lists.linux.dev,
	linux-kernel@vger.kernel.org
Cc: "Shu, Ming" <Ming.Shu@amd.com>,
	"Shukla, Santosh" <Santosh.Shukla@amd.com>
Subject: Re: [PATCH] iommu/amd: Restore three-stage vector cleanup for IR affinity migration
Date: Tue, 20 Oct 2026 18:23:35 +0800	[thread overview]
Message-ID: <f55c7a60-521a-4a46-aae8-60dd9286c003@linux.alibaba.com> (raw)
In-Reply-To: <54db5f22-ba63-433d-b98d-d6dc6b17b27f@amd.com>

Thank you for the detailed review and the hardware clarification.

在 2026/10/20 10:36, Suthikulpanit, Suravee 写道:
> Hi Guanghui,
> 
> On 10/15/2026 11:23 AM, Guanghui Feng wrote:
>> From: Guanghui Feng <guanghui.fgh@linux.alibaba.com>
>>
>> AMD IOMMU specification (Rev 3.10) does not define interrupt message
>> draining semantics for INVALIDATE_INTERRUPT_TABLE + COMPLETION_WAIT.
>> This creates a race between vector_schedule_cleanup() and in-flight MSI
>> messages still transiting the Data Fabric after IRTE modification.
>>
>> Intel VT-d is architecturally safe due to explicit interrupt draining
>> guarantees.  VT-d spec Section 6.5.5 states:
>>
>>    "An interrupt request is considered drained when the interrupt is
>>     accepted by the processor Local xAPIC (for fixed and lowest priority
>>     delivery mode interrupts this means the interrupt is at least
>>     recorded in the Local xAPIC Interrupt Request Register (IRR))."
>>
>>    "Interrupt draining is performed by remapping hardware after Interrupt
>>     Entry Cache (IEC) invalidation requests.  For IEC invalidations
>>     submitted through the queued invalidation interface, interrupt
>>     draining must be completed before the next Invalidation Wait
>>     Descriptor is completed by hardware."
>>
>>    "For index-selective IEC invalidation requests, hardware only
>>     guarantees draining of non-committed interrupt requests referencing
>>     interrupt indexes specified in the invalidation request."
>>
>> Additionally, Section 6.5.2.8 (IEC Invalidate Descriptor) mandates:
>>
>>    "As part of IEC invalidation, hardware must drain interrupt requests
>>     that are already processed by the remapping hardware, but queued
>>     within the Root-Complex to be delivered to the processor."
>>
>> Therefore when qi_flush_iec() + qi_submit_sync() returns (Wait Descriptor
>> reports QI_DONE), all non-committed interrupt messages referencing the
>> invalidated IRTE index are guaranteed to have been delivered to the
>> target LAPIC IRR.  The subsequent vector_schedule_cleanup() is safe
>> because is_vector_pending(prev_vector) will always observe the drained
>> message in IRR, causing the cleanup timer to rearm until the interrupt
>> is properly serviced.
>>
>> In contrast, AMD IOMMU spec Section 2.4.5 defines
>> INVALIDATE_INTERRUPT_TABLE only as:
>>
>>    "instructs the IOMMU to invalidate all cached interrupt information
>>     for the device"
>>
>> with no completion condition, no drain semantics, and no ordering
>> guarantee for previously-remapped interrupt messages.  Section 2.4.11.2
>> ("Invalidation Commands Interaction Requirements") limits the completion
>> guarantee exclusively to DMA transactions:
>>
>>    "Invalidation commands are considered complete only when the IOMMU can
>>     ensure there are no DMA transactions in flight anywhere in the system
>>     fabric that relied on translation cache contents prior to
>>     invalidation."
>>
>> The words "drain", "drained", or "draining" appear zero times in the
>> entire AMD IOMMU specification (Rev 3.10, 48882-PUB, Feb 2025) in
>> connection with interrupt messages.
>>
>> Consequently, iommu_flush_irt_and_complete() returning does NOT guarantee
>> that old (vector, destination) interrupt messages have been delivered to
>> the previous CPU's LAPIC IRR.  The immediate vector_schedule_cleanup()
>> after IRTE modification races with in-flight messages on the Data Fabric:
>> the cleanup timer reads IRR clear (message still in transit), calls
>> free_moved_vector(), then the message arrives at the old CPU hitting
>> VECTOR_UNUSED -- producing "No irq handler for vector" and permanently
>> losing an edge-triggered MSI.
> 
> Please describe the scenario you encountered to reproduce this issue.

We observed this on kernel 5.10 in a virtualization shared-instance
deployment:

- Guest uses VFIO passthrough devices (NVMe, NIC) with IOMMU interrupt
   remapping enabled (non-posted mode, guest_mode=0).
- Interrupts are delivered via KVM irqfd/eventfd software injection
   path, not hardware posted interrupts.
- When a vCPU is migrated to a different physical CPU (scheduler
   balancing or NUMA-aware placement), may adjusts the associated
   device interrupt affinity to follow the vCPU's new pCPU via
   irq_set_affinity -> amd_ir_set_affinity.
- During this affinity migration, the device continues generating MSI
   at high rate.  We observe intermittent "do_IRQ: X.Y No irq handler
   for vector" messages

Note: kernel 5.10 does not contain commit bdc1dad299bb ("x86/vector:
Replace IRQ_MOVE_CLEANUP_VECTOR with a timer callback").  In 5.10,
the cleanup path uses send_cleanup_vector() which sends an IPI
(IRQ_MOVE_CLEANUP_VECTOR) directly to the old CPU.  The IPI arrives
and runs sysvec_irq_move_cleanup() almost immediately -- providing
even less delay than the timer-based approach in newer kernels.
This makes the race window significantly wider on 5.10.

Commit bdc1dad299bb already recognized and partially mitigated this
class of issue by:
   - Replacing the immediate IPI with a per-CPU timer (jiffies+1)
   - Adding is_vector_pending() IRR check before freeing


> 
>> Fix by restoring three-stage cleanup semantics for the normal
>> (guest_mode=0) path:
>>
>>   1. Change amd_ir_chip.irq_ack from apic_ack_irq to apic_ack_edge,
>>      which calls irq_complete_move().  This ensures cleanup is only
>>      scheduled when the new vector first arrives at the new CPU --
>>      providing a natural delay (device interrupt period + PCIe transit +
>>      IOMMU translation + Data Fabric delivery) that far exceeds any
>>      residual in-flight message transit time.
>>
>>   2. Remove the unconditional vector_schedule_cleanup() from
>>      amd_ir_set_affinity() for the IRTE-modified path.
>>
>>   3. For guest_mode=1 (AVIC/guest interrupt virtualization), the IRTE
>>      is not modified and no host-vector interrupt will arrive (interrupts
>>      are delivered to the vAPIC backing page instead).  Retain direct
>>      vector_schedule_cleanup() in this case to prevent move_in_progress
>>      from permanently blocking future affinity changes.
> 
> Please describe the permanently blocking scenario.

When guest_mode=1 (AVIC), irte_ga_set_affinity() does not modify the
IRTE (guarded by "if (!irte->lo.fields_remap.guest_mode)").  However,
the parent domain (apic_set_affinity) has already allocated a new host
vector and set move_in_progress=1.  Since the IRTE still points to
the guest vAPIC backing page, no interrupt will ever arrive at the
host with the new vector, so irq_complete_move() will never fire.

Without vector_schedule_cleanup() in this path, move_in_progress stays
set permanently.  Subsequent calls to assign_vector_locked() check:

     if (apicd->move_in_progress || !hlist_unhashed(&apicd->clist))
         return -EBUSY;

This causes all future affinity changes for this IRQ to fail with
-EBUSY, which irq_move_masked_irq() handles by re-setting
IRQD_SETAFFINITY_PENDING -- but since no interrupt arrives on the
host to trigger __irq_move_irq(), the pending request is never
processed.  The IRQ's affinity becomes permanently stuck.


> 
>> The combination of stage-2 natural delay and stage-3 IRR gating
>> (is_vector_pending + timer rearm) provides robust protection against
>> the AMD-specific lack of interrupt drain guarantee.
> 
> Adding some arbitary delay does not necessary guarantee the delivery 
> before clearing the old vector.
> 
>> Intel VT-d, Hyper-V, and UV platforms are not modified as Intel is
>> architecturally safe per the spec guarantees cited above.
>>
>> Fixes: 5478844b12d3 ("iommu/amd: Move interrupt remapping to its own 
>> file")
> 
> Is this the correct commit? I could not find this commit. Could you 
> please provide link to the commit?
> 
>> Signed-off-by: Guanghui Feng <guanghui.fgh@linux.alibaba.com>
>> ---
>>   drivers/iommu/amd/amd_iommu_types.h |  2 +-
>>   drivers/iommu/amd/iommu.c           | 46 +++++++++++++++++++++--------
>>   2 files changed, 34 insertions(+), 14 deletions(-)
>>
>> diff --git a/drivers/iommu/amd/amd_iommu_types.h b/drivers/iommu/amd/ 
>> amd_iommu_types.h
>> index 8241ef922519..35024e3f5b90 100644
>> --- a/drivers/iommu/amd/amd_iommu_types.h
>> +++ b/drivers/iommu/amd/amd_iommu_types.h
>> @@ -1037,7 +1037,7 @@ struct amd_irte_ops {
>>       void (*prepare)(void *, u32, bool, u8, u32, int);
>>       void (*activate)(struct amd_iommu *iommu, void *, u16, u16);
>>       void (*deactivate)(struct amd_iommu *iommu, void *, u16, u16);
>> -    void (*set_affinity)(struct amd_iommu *iommu, void *, u16, u16, 
>> u8, u32);
>> +    bool (*set_affinity)(struct amd_iommu *iommu, void *, u16, u16, 
>> u8, u32);
>>       void *(*get)(struct irq_remap_table *, int);
>>       void (*set_allocated)(struct irq_remap_table *, int);
>>       bool (*is_allocated)(struct irq_remap_table *, int);
>> diff --git a/drivers/iommu/amd/iommu.c b/drivers/iommu/amd/iommu.c
>> index 56262f6b1f70..60e769384e8b 100644
>> --- a/drivers/iommu/amd/iommu.c
>> +++ b/drivers/iommu/amd/iommu.c
>> @@ -3634,7 +3634,7 @@ static void irte_ga_deactivate(struct amd_iommu 
>> *iommu, void *entry, u16 devid,
>>       modify_irte_ga(iommu, devid, index, irte);
>>   }
>> -static void irte_set_affinity(struct amd_iommu *iommu, void *entry, 
>> u16 devid, u16 index,
>> +static bool irte_set_affinity(struct amd_iommu *iommu, void *entry, 
>> u16 devid, u16 index,
>>                     u8 vector, u32 dest_apicid)
>>   {
>>       union irte *irte = (union irte *) entry;
>> @@ -3642,9 +3642,11 @@ static void irte_set_affinity(struct amd_iommu 
>> *iommu, void *entry, u16 devid, u
>>       irte->fields.vector = vector;
>>       irte->fields.destination = dest_apicid;
>>       modify_irte(iommu, devid, index, irte);
>> +
>> +    return true;
> 
> This blindly return true regardless of the return value from 
> modify_irte_ga.
> 
>>   }
>> -static void irte_ga_set_affinity(struct amd_iommu *iommu, void 
>> *entry, u16 devid, u16 index,
>> +static bool irte_ga_set_affinity(struct amd_iommu *iommu, void 
>> *entry, u16 devid, u16 index,
>>                    u8 vector, u32 dest_apicid)
>>   {
>>       struct irte_ga *irte = (struct irte_ga *) entry;
>> @@ -3656,7 +3658,11 @@ static void irte_ga_set_affinity(struct 
>> amd_iommu *iommu, void *entry, u16 devid
>>           irte->hi.fields.destination =
>>                       APICID_TO_IRTE_DEST_HI(dest_apicid);
>>           modify_irte_ga(iommu, devid, index, irte);
>> +
>> +        return true;
>>       }
>> +
>> +    return false;
> 
> This blindly return true regardless of the return value from 
> modify_irte_ga.
> 
>>   }
>>   #define IRTE_ALLOCATED (~1U)
>> @@ -3933,7 +3939,7 @@ static void irq_remapping_free(struct irq_domain 
>> *domain, unsigned int virq,
>>       irq_domain_free_irqs_common(domain, virq, nr_irqs);
>>   }
>> -static void amd_ir_update_irte(struct irq_data *irqd, struct 
>> amd_iommu *iommu,
>> +static bool amd_ir_update_irte(struct irq_data *irqd, struct 
>> amd_iommu *iommu,
>>                      struct amd_ir_data *ir_data,
>>                      struct irq_2_irte *irte_info,
>>                      struct irq_cfg *cfg);
>> @@ -4153,7 +4159,7 @@ static int amd_ir_set_vcpu_affinity(struct 
>> irq_data *data, void *info)
>>   }
>> -static void amd_ir_update_irte(struct irq_data *irqd, struct 
>> amd_iommu *iommu,
>> +static bool amd_ir_update_irte(struct irq_data *irqd, struct 
>> amd_iommu *iommu,
>>                      struct amd_ir_data *ir_data,
>>                      struct irq_2_irte *irte_info,
>>                      struct irq_cfg *cfg)
>> @@ -4163,9 +4169,9 @@ static void amd_ir_update_irte(struct irq_data 
>> *irqd, struct amd_iommu *iommu,
>>        * Atomically updates the IRTE with the new destination, vector
>>        * and flushes the interrupt entry cache.
>>        */
>> -    iommu->irte_ops->set_affinity(iommu, ir_data->entry, irte_info- 
>> >devid,
>> -                      irte_info->index, cfg->vector,
>> -                      cfg->dest_apicid);
>> +    return iommu->irte_ops->set_affinity(iommu, ir_data->entry, 
>> irte_info->devid,
>> +                         irte_info->index, cfg->vector,
>> +                         cfg->dest_apicid);
>>   }
>>   static int amd_ir_set_affinity(struct irq_data *data,
>> @@ -4176,6 +4182,7 @@ static int amd_ir_set_affinity(struct irq_data 
>> *data,
>>       struct irq_cfg *cfg = irqd_cfg(data);
>>       struct irq_data *parent = data->parent_data;
>>       struct amd_iommu *iommu = ir_data->iommu;
>> +    bool irte_modified;
>>       int ret;
>>       if (!iommu)
>> @@ -4185,13 +4192,26 @@ static int amd_ir_set_affinity(struct irq_data 
>> *data,
>>       if (ret < 0 || ret == IRQ_SET_MASK_OK_DONE)
>>           return ret;
>> -    amd_ir_update_irte(data, iommu, ir_data, irte_info, cfg);
>> +    irte_modified = amd_ir_update_irte(data, iommu, ir_data, 
>> irte_info, cfg);
>> +
>>       /*
>> -     * After this point, all the interrupts will start arriving
>> -     * at the new destination. So, time to cleanup the previous
>> -     * vector allocation.
>> +     * If the IRTE was actually modified (guest_mode == 0), rely on the
>> +     * three-stage cleanup: apic_ack_edge() calls irq_complete_move() 
>> when
>> +     * the new vector first arrives at the new CPU, which then schedules
>> +     * the cleanup of the previous vector allocation.  This avoids a 
>> race
>> +     * where in-flight MSI messages, still on the Data Fabric after the
>> +     * IRT invalidation because the AMD-Vi specification provides no
>> +     * interrupt drain guarantee, arrive after the old vector has been
>> +     * freed.
>> +     *
>> +     * If the IRTE was NOT modified (guest_mode == 1), no in-flight
>> +     * messages exist for the host vector and the new host vector will
>> +     * never receive an interrupt (those go to the vAPIC backing page
>> +     * instead).  Schedule the cleanup directly to prevent
>> +     * move_in_progress from getting permanently stuck.
>>        */
>> -    vector_schedule_cleanup(cfg);
>> +    if (!irte_modified)
>> +        vector_schedule_cleanup(cfg);
> 
> The !irte_modified is really checking guest_mode == 0 before calling 
> vector_schedule_cleanup(). The name is misleading. I also do not see how 
> this resolves the issue instead of calling vector_schedule_cleanup() 
> unconditionally.
> 
>>       return IRQ_SET_MASK_OK_DONE;
>>   }
>> @@ -4205,7 +4225,7 @@ static void ir_compose_msi_msg(struct irq_data 
>> *irq_data, struct msi_msg *msg)
>>   static struct irq_chip amd_ir_chip = {
>>       .name            = "AMD-IR",
>> -    .irq_ack        = apic_ack_irq,
>> +    .irq_ack        = apic_ack_edge,
> 
> The apic_ack_edge() basically calls __vector_schedule_cleanup() before 
> apic_ack_irq() on the new cpu, which should have already been called 
> during irq_set_affinity() and clears the move_in_progress. So, the 
> apic_ack_edge() would not be calling __vector_schedule_cleanup(). I do 
> not see how this would make a different.
> 

I believe there may be a misunderstanding.  The patch has two parts
that work together as a pair:

   Part (1): Change .irq_ack to apic_ack_edge (adds irq_complete_move)
   Part (2): REMOVE the unconditional vector_schedule_cleanup() from
             amd_ir_set_affinity() for the guest_mode=0 path

With part (2), move_in_progress is NOT cleared during
irq_set_affinity() anymore.  It remains set until the new vector
first arrives at the new CPU and apic_ack_edge() -> irq_complete_move()
fires. both changes are required together.

>  From AMD IOMMU hardware standpoint,
> 
> 1. The IOMMU considers the interrupt "done" at the point the write 
> completes at the fabric level (i.e. when the upstream posted write is 
> accepted). There is no round-trip back from the CPU APIC.
> 
> 2. When SW issues an interrupt table invalidation followed by 
> COMPLETION_WAIT, the IOMMU must drain all in-flight interrupt before 
> acknowledging the COMPLETION_WAIT. This ensures any stale IRTE data has 
> been flushed before the invalidation is considered complete from the 
> software's perspective.
> 
> 3. The time to drainthe data fabric should be much less than the 1- 
> jiffie during the vector_schedule_cleanup().
> 
> Regards,
> 
> Suravee

- COMPLETION_WAIT guarantees all in-flight interrupts are drained
   before software proceeds (point 2).
- The drain time is well within 1 jiffy (point 3).
- The timer + is_vector_pending() IRR check from bdc1dad299bb
   provides additional safety margin.

With this understanding, we agree that mainline kernels (containing
bdc1dad299bb) are safe without this patch.

> 
>>       .irq_set_affinity    = amd_ir_set_affinity,
>>       .irq_set_vcpu_affinity    = amd_ir_set_vcpu_affinity,
>>       .irq_compose_msi_msg    = ir_compose_msi_msg,


      reply	other threads:[~2026-10-20 10:23 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-15  4:23 Guanghui Feng
2026-10-20  2:36 ` Suthikulpanit, Suravee
2026-10-20 10:23   ` guanghuifeng [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=f55c7a60-521a-4a46-aae8-60dd9286c003@linux.alibaba.com \
    --to=guanghuifeng@linux.alibaba.com \
    --cc=Ming.Shu@amd.com \
    --cc=Santosh.Shukla@amd.com \
    --cc=iommu@lists.linux.dev \
    --cc=joro@8bytes.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=robin.murphy@arm.com \
    --cc=suravee.suthikulpanit@amd.com \
    --cc=vasant.hegde@amd.com \
    --cc=will@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®