[PATCH] scsi: sd: Mark disk dead on host removal to avoid I/O hang

胡连勤 <[email protected]> Fri, 31 Jul 2026 03:45:02 +0000
Newsgroups gmane.linux.block,gmane.linux.kernel,gmane.linux.scsi
Message-ID <TYUPR06MB62172E412054140753D5E782D2C82@TYUPR06MB6217.apcprd06.prod.outlook.com>
When a USB mass storage device with a mounted filesystem (e.g. exFAT)
enters runtime suspend (autosuspend), physical removal triggers
scsi_remove_host() which invokes sd_remove() -> del_gendisk() ->
blk_report_disk_dead().  This triggers sync_filesystem() to write
back dirty data, submits bio and blocks in __bio_queue_enter()
waiting for I/O completion that can never happen since the device
is already gone, leading to a hung task.

del_gendisk() unconditionally calls blk_report_disk_dead(disk, false)
when GD_DEAD is not yet set, which always attempts to sync the
filesystem.  For surprise removal where the device cannot handle I/O,
this sync is futile and hangs forever.  Additionally, SCSI disks
created via blk_mq_alloc_disk_for_queue() do not have GD_OWNS_QUEUE
set, so __blk_mark_disk_dead() (called by del_gendisk()) never sets
QUEUE_FLAG_DYING, and blk_queue_enter() remains blocked waiting for
I/O that will never complete.

Set QUEUE_FLAG_DYING unconditionally in blk_mark_disk_dead(), so
that any in-flight I/O from other threads gets -ENODEV immediately
from blk_queue_enter() regardless of GD_OWNS_QUEUE ownership.

Add scsi_host_in_cancel() helper and call blk_mark_disk_dead() in
sd_remove() when the SCSI host is in SHOST_CANCEL or
SHOST_CANCEL_RECOVERY state (set by scsi_remove_host() before
removing devices), so that del_gendisk() sees GD_DEAD already set
and skips the sync.  For orderly removal (e.g. echo 1 >
/sys/.../delete), the host remains SHOST_RUNNING and sync proceeds
normally.  This matches the NVMe behavior where
nvme_mark_namespaces_dead() is called only on surprise removal
(NVME_CTRL_DEAD).

Call trace on USB storage surprise removal:
INFO: task "kworker/1:0":31 blocked for more than 147 seconds.
Call trace:
 __switch_to+0x198/0x380
 __schedule+0x548/0xfbc
 schedule+0x4c/0x118
 __bio_queue_enter+0xb8/0x174
 blk_mq_submit_bio+0x640/0x7e4
 __submit_bio+0x1a4/0x314
 __submit_bio_noacct_mq+0x38/0x8c
 submit_bio_noacct+0x678/0x750
 submit_bio+0xa8/0x1c8
 submit_bh_wbc+0x148/0x1b4
 __sync_dirty_buffer+0x120/0x1f8
 exfat_sync_fs+0xa4/0xe0
 sync_filesystem+0xa8/0xdc
 fs_bdev_mark_dead+0x30/0x88
 bdev_mark_dead+0x54/0xc4
 blk_report_disk_dead+0x8c/0xd4
 del_gendisk+0xa0/0x2f8
 sd_remove+0x30/0x60
 device_release_driver_internal+0x1c4/0x2bc
 device_release_driver+0x18/0x28
 bus_remove_device+0x158/0x170
 device_del+0x1c8/0x320
 __scsi_remove_device+0x9c/0x184
 scsi_forget_host+0x50/0x70
 scsi_remove_host+0x88/0x18c
 usb_stor_disconnect+0x68/0xf4
 usb_unbind_interface+0x13c/0x340
 device_release_driver_internal+0x1c4/0x2bc
 device_release_driver+0x18/0x28
 bus_remove_device+0x158/0x170
 device_del+0x1c8/0x320
 usb_disable_device+0x84/0x190
 usb_disconnect+0xe8/0x338
 hub_event+0xbd8/0x19ac
 process_scheduled_works+0x200/0x9d8
 worker_thread+0x154/0x3b0

Fixes: 6f8191fdf41d ("block: simplify disk shutdown")
Cc: [email protected]
Signed-off-by: Lianqin Hu <[email protected]>
---
 block/genhd.c            |  1 +
 drivers/scsi/sd.c        | 10 ++++++++++
 include/scsi/scsi_host.h |  6 ++++++
 3 files changed, 17 insertions(+)

diff --git a/block/genhd.c b/block/genhd.c
index df2c3c69b467..7b089e2d52c4 100644
--- a/block/genhd.c
+++ b/block/genhd.c
@@ -681,6 +681,7 @@ static bool __blk_mark_disk_dead(struct gendisk *disk)
  */
 void blk_mark_disk_dead(struct gendisk *disk)
 {
+	blk_queue_flag_set(QUEUE_FLAG_DYING, disk->queue);
 	__blk_mark_disk_dead(disk);
 	blk_report_disk_dead(disk, true);
 }
diff --git a/drivers/scsi/sd.c b/drivers/scsi/sd.c
index 599e75f33334..fb5086465a23 100644
--- a/drivers/scsi/sd.c
+++ b/drivers/scsi/sd.c
@@ -4257,6 +4257,16 @@ static void sd_remove(struct scsi_device *sdp)
 
 	scsi_autopm_get_device(sdkp->device);
 
+	/*
+	 * If the host is being torn down (e.g. USB surprise removal),
+	 * the device cannot handle I/O anymore.
+	 * Mark the disk dead before del_gendisk() so that new I/O is
+	 * rejected immediately and sync_filesystem() in del_gendisk()
+	 * is skipped, avoiding a hang waiting for I/O that can never
+	 * complete.
+	 */
+	if (scsi_host_in_cancel(sdkp->device->host))
+		blk_mark_disk_dead(sdkp->disk);
 	device_del(&sdkp->disk_dev);
 	del_gendisk(sdkp->disk);
 	if (!sdkp->suspended)
diff --git a/include/scsi/scsi_host.h b/include/scsi/scsi_host.h
index f6b286fa59f2..03523ad3284d 100644
--- a/include/scsi/scsi_host.h
+++ b/include/scsi/scsi_host.h
@@ -796,6 +796,12 @@ static inline int scsi_host_in_recovery(struct Scsi_Host *shost)
 		shost->tmf_in_progress;
 }
 
+static inline bool scsi_host_in_cancel(struct Scsi_Host *shost)
+{
+	return shost->shost_state == SHOST_CANCEL ||
+		shost->shost_state == SHOST_CANCEL_RECOVERY;
+}
+
 extern int scsi_queue_work(struct Scsi_Host *, struct work_struct *);
 extern void scsi_flush_work(struct Scsi_Host *);
 
-- 
2.39.0