From: Dave Jiang <dave.jiang@intel.com>
To: "Ananyev, Konstantin" <konstantin.ananyev@intel.com>,
"vkoul@kernel.org" <vkoul@kernel.org>
Cc: "dmaengine@vger.kernel.org" <dmaengine@vger.kernel.org>
Subject: Re: [PATCH] dmaengine: idxd: fix submission race window
Date: Fri, 25 Jun 2021 11:07:23 -0700 [thread overview]
Message-ID: <7443580a-1eaa-bbbc-0e87-6b2c779a9654@intel.com> (raw)
In-Reply-To: <DM6PR11MB4491DA436F12DF0A973939179A069@DM6PR11MB4491.namprd11.prod.outlook.com>
On 6/25/2021 10:50 AM, Ananyev, Konstantin wrote:
>>>> Konstantin observed that when descriptors are submitted, the descriptor is
>>>> added to the pending list after the submission. This creates a race window
>>>> with the slight possibility that the descriptor can complete before it
>>>> gets added to the pending list and this window would cause the completion
>>>> handler to miss processing the descriptor.
>>>>
>>>> To address the issue, the addition of the descriptor to the pending list
>>>> must be done before it gets submitted to the hardware. However, submitting
>>>> to swq with ENQCMDS instruction can cause a failure with the condition of
>>>> either wq is full or wq is not "active".
>>>>
>>>> With the descriptor allocation being the gate to the wq capacity, it is not
>>>> possible to hit a retry with ENQCMDS submission to the swq. The only
>>>> possible failure can happen is when wq is no longer "active" due to hw
>>>> error and therefore we are moving towards taking down the portal. Given
>>>> this is a rare condition and there's no longer concern over I/O
>>>> performance, the driver can walk the completion lists in order to retrieve
>>>> and abort the descriptor.
>>>>
>>>> The error path will set the descriptor to aborted status. It will take the
>>>> work list lock to prevent further processing of worklist. It will do a
>>>> delete_all on the pending llist to retrieve all descriptors on the pending
>>>> llist. The delete_all action does not require a lock. It will walk through
>>>> the acquired llist to find the aborted descriptor while add all remaining
>>>> descriptors to the work list since it holds the lock. If it does not find
>>>> the aborted descriptor on the llist, it will walk through the work
>>>> list. And if it still does not find the descriptor, then it means the
>>>> interrupt handler has removed the desc from the llist but is pending on the work
>>>> list lock and will process it once the error path releases the lock.
>>>>
>>>> Fixes: eb15e7154fbf ("dmaengine: idxd: add interrupt handle request and release support")
>>>> Reported-by: Konstantin Ananyev <konstantin.ananyev@intel.com>
>>>> Signed-off-by: Dave Jiang <dave.jiang@intel.com>
>>>> ---
>>>> drivers/dma/idxd/idxd.h | 14 ++++++++
>>>> drivers/dma/idxd/irq.c | 36 +++++++++++----------
>>>> drivers/dma/idxd/submit.c | 77 ++++++++++++++++++++++++++++++++++++++++-----
>>>> 3 files changed, 101 insertions(+), 26 deletions(-)
>>>>
>>>> diff --git a/drivers/dma/idxd/idxd.h b/drivers/dma/idxd/idxd.h
>>>> index 1f0991dec679..0f27374eae4b 100644
>>>> --- a/drivers/dma/idxd/idxd.h
>>>> +++ b/drivers/dma/idxd/idxd.h
>>>> @@ -294,6 +294,14 @@ struct idxd_desc {
>>>> struct idxd_wq *wq;
>>>> };
>>>>
>>>> +/*
>>>> + * This is software defined error for the completion status. We overload the error code
>>>> + * that will never appear in completion status and only SWERR register.
>>>> + */
>>>> +enum idxd_completion_status {
>>>> + IDXD_COMP_DESC_ABORT = 0xff,
>>>> +};
>>>> +
>>>> #define confdev_to_idxd(dev) container_of(dev, struct idxd_device, conf_dev)
>>>> #define confdev_to_wq(dev) container_of(dev, struct idxd_wq, conf_dev)
>>>>
>>>> @@ -480,4 +488,10 @@ static inline void perfmon_init(void) {}
>>>> static inline void perfmon_exit(void) {}
>>>> #endif
>>>>
>>>> +static inline void complete_desc(struct idxd_desc *desc, enum idxd_complete_type reason)
>>>> +{
>>>> + idxd_dma_complete_txd(desc, reason);
>>>> + idxd_free_desc(desc->wq, desc);
>>>> +}
>>>> +
>>>> #endif
>>>> diff --git a/drivers/dma/idxd/irq.c b/drivers/dma/idxd/irq.c
>>>> index 7a2cf0512501..8fd52937537a 100644
>>>> --- a/drivers/dma/idxd/irq.c
>>>> +++ b/drivers/dma/idxd/irq.c
>>>> @@ -245,12 +245,6 @@ static inline bool match_fault(struct idxd_desc *desc, u64 fault_addr)
>>>> return false;
>>>> }
>>>>
>>>> -static inline void complete_desc(struct idxd_desc *desc, enum idxd_complete_type reason)
>>>> -{
>>>> - idxd_dma_complete_txd(desc, reason);
>>>> - idxd_free_desc(desc->wq, desc);
>>>> -}
>>>> -
>>>> static int irq_process_pending_llist(struct idxd_irq_entry *irq_entry,
>>>> enum irq_work_type wtype,
>>>> int *processed, u64 data)
>>>> @@ -258,8 +252,7 @@ static int irq_process_pending_llist(struct idxd_irq_entry *irq_entry,
>>>> struct idxd_desc *desc, *t;
>>>> struct llist_node *head;
>>>> int queued = 0;
>>>> - unsigned long flags;
>>>> - enum idxd_complete_type reason;
>>>> + enum idxd_complete_type reason = IDXD_COMPLETE_NORMAL;
>>>>
>>>> *processed = 0;
>>>> head = llist_del_all(&irq_entry->pending_llist);
>>>> @@ -272,16 +265,20 @@ static int irq_process_pending_llist(struct idxd_irq_entry *irq_entry,
>>>> reason = IDXD_COMPLETE_DEV_FAIL;
>>>>
>>>> llist_for_each_entry_safe(desc, t, head, llnode) {
>>>> - if (desc->completion->status) {
>>>> - if ((desc->completion->status & DSA_COMP_STATUS_MASK) != DSA_COMP_SUCCESS)
>>>> + u8 status = desc->completion->status & DSA_COMP_STATUS_MASK;
>>>> +
>>>> + if (status) {
>>>> + if (status == IDXD_COMP_DESC_ABORT)
>>>> + reason = IDXD_COMPLETE_ABORT;
>>>> + else if (status != DSA_COMP_SUCCESS)
>>>> match_fault(desc, data);
>>>> complete_desc(desc, reason);
>>> reason was overwritten above for one particular desc, but not restored.
>>> So all futher descs will be completed with = IDXD_COMPLETE_ABORT.
>>> I think we need either to set it up on each iteration, or:
>>> if (status == IDXD_COMP_DESC_ABORT)
>>> complete_desc(desc, IDXD_COMPLETE_ABORT);
>>> else {
>>> if (status != DSA_COMP_SUCCESS)
>>> match_fault(desc, data);
>>> complete_desc(desc, reason);
>>> }
>> Ah right. I'll fix that.
>>
>>
>>>> (*processed)++;
>>>> } else {
>>>> - spin_lock_irqsave(&irq_entry->list_lock, flags);
>>>> + spin_lock_bh(&irq_entry->list_lock);
>>>> list_add_tail(&desc->list,
>>>> &irq_entry->work_list);
>>>> - spin_unlock_irqrestore(&irq_entry->list_lock, flags);
>>>> + spin_unlock_bh(&irq_entry->list_lock);
>>>> queued++;
>>>> }
>>>> }
>>>> @@ -295,10 +292,9 @@ static int irq_process_work_list(struct idxd_irq_entry *irq_entry,
>>>> int *processed, u64 data)
>>>> {
>>>> int queued = 0;
>>>> - unsigned long flags;
>>>> LIST_HEAD(flist);
>>>> struct idxd_desc *desc, *n;
>>>> - enum idxd_complete_type reason;
>>>> + enum idxd_complete_type reason = IDXD_COMPLETE_NORMAL;
>>>>
>>>> *processed = 0;
>>>> if (wtype == IRQ_WORK_NORMAL)
>>>> @@ -310,9 +306,9 @@ static int irq_process_work_list(struct idxd_irq_entry *irq_entry,
>>>> * This lock protects list corruption from access of list outside of the irq handler
>>>> * thread.
>>>> */
>>>> - spin_lock_irqsave(&irq_entry->list_lock, flags);
>>>> + spin_lock_bh(&irq_entry->list_lock);
>>> I am not really an expert in kenel sipn lock flavours...
>>> Can you explain to me why we can now replace _irqsave_ with _bh_?
>> Actually I think we can do the callbacks directly now that we are using
>> spin_lock_bh(). Honestly I don't think we ever needed to call
>> spin_lock_irqsave(). It disables interrupts and therefore the driver was
>> doing the callbacks outside of the lock. But since we are running
>> threaded interrupt and we don't actually touch the list in the hard
>> interrupt handler, there's no need to disable interrupt. spin_lock_bh()
>> disables software interrupts and that should cover the thread interrupt
>> handler.
> Ok, thanks for explanation.
> About calling complete_txd() while holding the lock - agree, it seems allowable.
> Though both dma_descriptor_unmap() and user-provided callbacks might be
> time consuming. If execute them holding the list_lock - it means normal irq
> handler wouldn't be able to progress till they are completed.
> Probably not that big deal, but IMO still better to do complete()
> outside the lock section, after all it seems easy enough to do.
Fair enough. I'll leave the lock alone and deal with it as is. I'll
create a different patch to deal with the lock stuff after.
>
>>
>>>> if (list_empty(&irq_entry->work_list)) {
>>>> - spin_unlock_irqrestore(&irq_entry->list_lock, flags);
>>>> + spin_unlock_bh(&irq_entry->list_lock);
>>>> return 0;
>>>> }
>>>>
>>>> @@ -326,10 +322,14 @@ static int irq_process_work_list(struct idxd_irq_entry *irq_entry,
>>>> }
>>>> }
>>>>
>>>> - spin_unlock_irqrestore(&irq_entry->list_lock, flags);
>>>> + spin_unlock_bh(&irq_entry->list_lock);
>>>>
>>>> list_for_each_entry(desc, &flist, list) {
>>>> - if ((desc->completion->status & DSA_COMP_STATUS_MASK) != DSA_COMP_SUCCESS)
>>>> + u8 status = desc->completion->status & DSA_COMP_STATUS_MASK;
>>>> +
>>>> + if (desc->completion->status == IDXD_COMP_DESC_ABORT)
>>>> + reason = IDXD_COMPLETE_ABORT;
>>>> + else if (status != DSA_COMP_SUCCESS)
>>>> match_fault(desc, data);
>>>> complete_desc(desc, reason);
>>> Same comment as above for pending list.
>>>
>>>> }
>>>> diff --git a/drivers/dma/idxd/submit.c b/drivers/dma/idxd/submit.c
>>>> index 736def129fa8..d99a3aeb038c 100644
>>>> --- a/drivers/dma/idxd/submit.c
>>>> +++ b/drivers/dma/idxd/submit.c
>>>> @@ -88,9 +88,66 @@ void idxd_free_desc(struct idxd_wq *wq, struct idxd_desc *desc)
>>>> sbitmap_queue_clear(&wq->sbq, desc->id, cpu);
>>>> }
>>>>
>>>> +static void list_abort_desc(struct idxd_wq *wq, struct idxd_irq_entry *ie,
>>>> + struct idxd_desc *desc)
>>>> +{
>>>> + struct idxd_desc *d, *n;
>>>> +
>>>> + lockdep_assert_held(&ie->list_lock);
>>>> + list_for_each_entry_safe(d, n, &ie->work_list, list) {
>>>> + if (d == desc) {
>>>> + list_del(&d->list);
>>>> + complete_desc(desc, IDXD_COMPLETE_ABORT);
>>>> + return;
>>>> + }
>>>> + }
>>>> +
>>>> + /*
>>>> + * At this point, the desc needs to be aborted is held by the completion
>>>> + * handler where it has taken it off the pending list but has not added to the
>>>> + * work list. It will be cleaned up by the interrupt handler when it sees the
>>>> + * IDXD_COMP_DESC_ABORT for completion status.
>>>> + */
>>>> +}
>>>> +
>>>> +static void llist_abort_desc(struct idxd_wq *wq, struct idxd_irq_entry *ie,
>>>> + struct idxd_desc *desc)
>>>> +{
>>>> + struct idxd_desc *d, *t;
>>>> + struct llist_node *head;
>>>> + bool found = false;
>>>> +
>>>> + desc->completion->status = IDXD_COMP_DESC_ABORT;
>>>> + /*
>>>> + * Grab the list lock so it will block the irq thread handler. This allows the
>>>> + * abort code to locate the descriptor need to be aborted.
>>>> + */
>>>> + spin_lock_bh(&ie->list_lock);
>>>> + head = llist_del_all(&ie->pending_llist);
>>>> + if (!head) {
>>>> + list_abort_desc(wq, ie, desc);
>>>> + spin_unlock_bh(&ie->list_lock);
>>>> + return;
>>>> + }
>>>> +
>>>> + llist_for_each_entry_safe(d, t, head, llnode) {
>>>> + if (d == desc) {
>>>> + complete_desc(desc, IDXD_COMPLETE_ABORT);
>>>> + found = true;
>>>> + continue;
>>>> + }
>>>> + list_add_tail(&desc->list, &ie->work_list);
>>>> + }
>>>> +
>>>> + if (!found)
>>>> + list_abort_desc(wq, ie, desc);
>>>> + spin_unlock_bh(&ie->list_lock);
>>>> +}
>>> There is one thing that worries me in that approach:
>>> From what I see, right now idxd calls complete_desc() without list_lock held.
>>> I think it is better to keep things like that.
>>> I suppose it should be easy enough to re-arrange your code a bit:
>>> 1) make list_abort_desc() not to call complete_desc(), but simply
>>> return desc if found or null otherwise.
>>> 2) In llist_abort_desc() - grab the lock and try to find the desc in one of the lists.
>>> If found in the work_list, then remove it from the llist, but don't call complete() yet.
>>> Then release the lock and do complete().
>>>
>>> Something like that, to be more specific:
>>>
>>> static void llist_abort_desc(struct idxd_wq *wq, struct idxd_irq_entry *ie,
>>> struct idxd_desc *desc)
>>> {
>>> struct idxd_desc *d, *found, *t;
>>> struct llist_node *head;
>>>
>>> desc->completion->status = IDXD_COMP_DESC_ABORT;
>>> found = NULL;
>>>
>>> spin_lock_bh(&ie->list_lock);
>>> head = llist_del_all(&ie->pending_llist);
>>> if (head != NULL) {
>>> llist_for_each_entry_safe(d, t, head, llnode) {
>>> if (d == desc) {
>>> found = desc;
>>> continue;
>>> }
>>> list_add_tail(&desc->list, &ie->work_list);
>>> }
>>>
>>> If (found == NULL)
>>> found = list_abort_desc(wq, ie, desc);
>>>
>>> spin_unlock_bh(&ie->list_lock);
>>>
>>> if (found != NULL)
>>> complete_desc(found, IDXD_COMPLETE_ABORT);
>>> }
>>>
>>>
>>>> +
>>>> int idxd_submit_desc(struct idxd_wq *wq, struct idxd_desc *desc)
>>>> {
>>>> struct idxd_device *idxd = wq->idxd;
>>>> + struct idxd_irq_entry *ie = NULL;
>>>> void __iomem *portal;
>>>> int rc;
>>>>
>>>> @@ -108,6 +165,16 @@ int idxd_submit_desc(struct idxd_wq *wq, struct idxd_desc *desc)
>>>> * even on UP because the recipient is a device.
>>>> */
>>>> wmb();
>>>> +
>>>> + /*
>>>> + * Pending the descriptor to the lockless list for the irq_entry
>>>> + * that we designated the descriptor to.
>>>> + */
>>>> + if (desc->hw->flags & IDXD_OP_FLAG_RCI) {
>>>> + ie = &idxd->irq_entries[desc->vector];
>>>> + llist_add(&desc->llnode, &ie->pending_llist);
>>>> + }
>>>> +
>>>> if (wq_dedicated(wq)) {
>>>> iosubmit_cmds512(portal, desc->hw, 1);
>>>> } else {
>>>> @@ -120,18 +187,12 @@ int idxd_submit_desc(struct idxd_wq *wq, struct idxd_desc *desc)
>>>> rc = enqcmds(portal, desc->hw);
>>>> if (rc < 0) {
>>>> percpu_ref_put(&wq->wq_active);
>>>> + if (ie)
>>>> + llist_abort_desc(wq, ie, desc);
>>>> return rc;
>>>> }
>>>> }
>>>>
>>>> percpu_ref_put(&wq->wq_active);
>>>> -
>>>> - /*
>>>> - * Pending the descriptor to the lockless list for the irq_entry
>>>> - * that we designated the descriptor to.
>>>> - */
>>>> - if (desc->hw->flags & IDXD_OP_FLAG_RCI)
>>>> - llist_add(&desc->llnode, &idxd->irq_entries[desc->vector].pending_llist);
>>>> -
>>>> return 0;
>>>> }
>>>>
prev parent reply other threads:[~2021-06-25 18:07 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-06-24 19:13 [PATCH] dmaengine: idxd: fix submission race window Dave Jiang
2021-06-25 14:37 ` Ananyev, Konstantin
2021-06-25 15:49 ` Dave Jiang
2021-06-25 17:50 ` Ananyev, Konstantin
2021-06-25 18:07 ` Dave Jiang [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=7443580a-1eaa-bbbc-0e87-6b2c779a9654@intel.com \
--to=dave.jiang@intel.com \
--cc=dmaengine@vger.kernel.org \
--cc=konstantin.ananyev@intel.com \
--cc=vkoul@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