scsi: lpfc: Prevent NDLP reference count underflow in dev_loss_tmo callback

Current dev_loss_tmo handling checks whether there has been a previous
call to unregister with SCSI transport.  If so, the NDLP kref count is
decremented a second time in dev_loss_tmo as the final kref release.
However, this can sometimes result in a reference count underflow if
there is also a race to unregister with NVMe transport as well.  Add a
check for NVMe transport registration before decrementing the final
kref.  If NVMe transport is still registered, then the NVMe transport
unregistration is designated as the final kref decrement.

Signed-off-by: Justin Tee <justin.tee@broadcom.com>
Link: https://lore.kernel.org/r/20241031223219.152342-8-justintee8345@gmail.com
Signed-off-by: Martin K. Petersen <martin.petersen@oracle.com>
This commit is contained in:
Justin Tee 2024-10-31 15:32:15 -07:00 committed by Martin K. Petersen
parent eb038363d8
commit 4281f44ea8

View File

@ -161,6 +161,7 @@ lpfc_dev_loss_tmo_callbk(struct fc_rport *rport)
struct lpfc_hba *phba; struct lpfc_hba *phba;
struct lpfc_work_evt *evtp; struct lpfc_work_evt *evtp;
unsigned long iflags; unsigned long iflags;
bool nvme_reg = false;
ndlp = ((struct lpfc_rport_data *)rport->dd_data)->pnode; ndlp = ((struct lpfc_rport_data *)rport->dd_data)->pnode;
if (!ndlp) if (!ndlp)
@ -183,38 +184,49 @@ lpfc_dev_loss_tmo_callbk(struct fc_rport *rport)
/* Don't schedule a worker thread event if the vport is going down. */ /* Don't schedule a worker thread event if the vport is going down. */
if (test_bit(FC_UNLOADING, &vport->load_flag) || if (test_bit(FC_UNLOADING, &vport->load_flag) ||
!test_bit(HBA_SETUP, &phba->hba_flag)) { !test_bit(HBA_SETUP, &phba->hba_flag)) {
spin_lock_irqsave(&ndlp->lock, iflags); spin_lock_irqsave(&ndlp->lock, iflags);
ndlp->rport = NULL; ndlp->rport = NULL;
if (ndlp->fc4_xpt_flags & NVME_XPT_REGD)
nvme_reg = true;
/* The scsi_transport is done with the rport so lpfc cannot /* The scsi_transport is done with the rport so lpfc cannot
* call to unregister. Remove the scsi transport reference * call to unregister.
* and clean up the SCSI transport node details.
*/ */
if (ndlp->fc4_xpt_flags & (NLP_XPT_REGD | SCSI_XPT_REGD)) { if (ndlp->fc4_xpt_flags & SCSI_XPT_REGD) {
ndlp->fc4_xpt_flags &= ~SCSI_XPT_REGD; ndlp->fc4_xpt_flags &= ~SCSI_XPT_REGD;
/* NVME transport-registered rports need the /* If NLP_XPT_REGD was cleared in lpfc_nlp_unreg_node,
* NLP_XPT_REGD flag to complete an unregister. * unregister calls were made to the scsi and nvme
* transports and refcnt was already decremented. Clear
* the NLP_XPT_REGD flag only if the NVME Rport is
* confirmed unregistered.
*/ */
if (!(ndlp->fc4_xpt_flags & NVME_XPT_REGD)) if (!nvme_reg && ndlp->fc4_xpt_flags & NLP_XPT_REGD) {
ndlp->fc4_xpt_flags &= ~NLP_XPT_REGD; ndlp->fc4_xpt_flags &= ~NLP_XPT_REGD;
spin_unlock_irqrestore(&ndlp->lock, iflags);
lpfc_nlp_put(ndlp); /* may free ndlp */
} else {
spin_unlock_irqrestore(&ndlp->lock, iflags);
}
} else {
spin_unlock_irqrestore(&ndlp->lock, iflags); spin_unlock_irqrestore(&ndlp->lock, iflags);
lpfc_nlp_put(ndlp);
spin_lock_irqsave(&ndlp->lock, iflags);
} }
spin_lock_irqsave(&ndlp->lock, iflags);
/* Only 1 thread can drop the initial node reference. If /* Only 1 thread can drop the initial node reference. If
* another thread has set NLP_DROPPED, this thread is done. * another thread has set NLP_DROPPED, this thread is done.
*/ */
if (!(ndlp->fc4_xpt_flags & NVME_XPT_REGD) && if (nvme_reg || (ndlp->nlp_flag & NLP_DROPPED)) {
!(ndlp->nlp_flag & NLP_DROPPED)) {
ndlp->nlp_flag |= NLP_DROPPED;
spin_unlock_irqrestore(&ndlp->lock, iflags); spin_unlock_irqrestore(&ndlp->lock, iflags);
lpfc_nlp_put(ndlp);
return; return;
} }
ndlp->nlp_flag |= NLP_DROPPED;
spin_unlock_irqrestore(&ndlp->lock, iflags); spin_unlock_irqrestore(&ndlp->lock, iflags);
lpfc_nlp_put(ndlp);
return; return;
} }