Re: SCSI EH wakeup deadlock from deferred_qc

Damien Le Moal <[email protected]> Thu, 9 Jul 2026 08:44:33 +0900
Newsgroups org.kernel.vger.linux-ide
Organization Western Digital Research
Message-ID <[email protected]>
On 7/9/26 04:36, Igor Pylypiv wrote:
> Hi Damien,
> 
> I've stumbled upon an issue where SCSI EH didn't run upon command timeout
> but ran ~10 or so seconds later. It seems like a recent regression from
> the introduction of deffered_qc.
> 
> When an active command times out while a non-NCQ command is waiting in
> deferred_qc, SCSI EH fails to wake up. Recovery stalls until the deferred
> command's own timer expires.
> 
> When an NCQ command times out, scsi_timeout() calls scsi_eh_scmd_add(),
> incrementing shost->host_failed (1). However, when scsi_eh_wakeup() checks
> whether to wake the EH thread, scsi_host_busy(shost) counts 2 active
> commands (1 timed-out + 1 in deferred_qc).
> 
> Because busy (2) != host_failed (1), scsi_eh_wakeup() refuses to wake
> the EH thread, deadlocking error recovery until the deferred command
> times out on its own.

Igor,

Can you try the attached diff ?
I will test on my end, but I need to hack something to trigger a timeout :)


-- 
Damien Le Moal
Western Digital Research
ata-timeout.diff (text/x-patch, 1.9 KB)
diff --git a/drivers/ata/libata-scsi.c b/drivers/ata/libata-scsi.c
index 5868526301a2..13a8d2312f41 100644
--- a/drivers/ata/libata-scsi.c
+++ b/drivers/ata/libata-scsi.c
@@ -1730,6 +1730,28 @@ static void ata_scsi_schedule_deferred_qc(struct ata_link *link)
 		queue_work(system_highpri_wq, &link->deferred_qc_work);
 }
 
+enum scsi_timeout_action ata_scsi_eh_timed_out(struct scsi_cmnd *scmd)
+{
+	struct ata_port *ap = ata_shost_to_port(scmd->device->host);
+	unsigned long flags;
+
+	/*
+	 * We had a timeout, either for an NCQ command or for one deferred
+	 * queued command. Either way, we must release all deferred queued
+	 * command so that scsi EH can trigger.
+	 */
+	spin_lock_irqsave(ap->lock, flags);
+	ata_scsi_requeue_deferred_qc(ap);
+	spin_unlock_irqrestore(ap->lock, flags);
+
+	/*
+	 * Let scsi_timeout() know that it must continue with handling the
+	 * timeout as we in fact did not do much here.
+	 */
+	return SCSI_EH_NOT_HANDLED;
+}
+EXPORT_SYMBOL_GPL(ata_scsi_eh_timed_out);
+
 static void ata_scsi_qc_complete(struct ata_queued_cmd *qc)
 {
 	struct ata_link *link = qc->dev->link;
diff --git a/include/linux/libata.h b/include/linux/libata.h
index 96e626d6a7ca..327da43d7496 100644
--- a/include/linux/libata.h
+++ b/include/linux/libata.h
@@ -1153,6 +1153,7 @@ extern int ata_scsi_ioctl(struct scsi_device *dev, unsigned int cmd,
 #endif
 extern enum scsi_qc_status ata_scsi_queuecmd(struct Scsi_Host *h,
 					     struct scsi_cmnd *cmd);
+enum scsi_timeout_action ata_scsi_eh_timed_out(struct scsi_cmnd *cmd);
 #if IS_REACHABLE(CONFIG_ATA)
 bool ata_scsi_dma_need_drain(struct request *rq);
 #else
@@ -1464,6 +1465,7 @@ extern const struct attribute_group *ata_common_sdev_groups[];
 	.ioctl			= ata_scsi_ioctl,		\
 	ATA_SCSI_COMPAT_IOCTL					\
 	.queuecommand		= ata_scsi_queuecmd,		\
+	.eh_timed_out		= ata_scsi_eh_timed_out,	\
 	.dma_need_drain		= ata_scsi_dma_need_drain,	\
 	.this_id		= ATA_SHT_THIS_ID,		\
 	.emulated		= ATA_SHT_EMULATED,		\