* [PATCH v9 01/29] scsi: core: Clear cmd->flags for terminal queuing error
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-09 20:49 ` Bart Van Assche
2026-11-08 20:47 ` [PATCH v9 02/29] libmultipath: Add initial framework John Garry
` (28 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
If a request can't be queued in scsi_queue_rq(), then we error out with
a blk_status_t code. Such a scenario may be the scsi_device has its
transport offline.
For BLK_STS_RESOURCE or BLK_STS_AGAIN, the request will be requeued in the
block layer. For other errors, the block layer ends the request and so
we won't see it again (until recycled).
Normally when a request completes scmd->flags is cleared in
scsi_end_request().
However for the error scenario mentioned above, scsi_end_request() is
not called and so scmd->flags is not cleared. As such, when the request
is recycled we see the old value in scmd->flags.
Fix this issue by clearing scmd->flags in the error scenario mentioned by
calling scsi_cleanup_rq().
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_lib.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
index b52b4c5daa42..36bc995e4e25 100644
--- a/drivers/scsi/scsi_lib.c
+++ b/drivers/scsi/scsi_lib.c
@@ -1994,8 +1994,8 @@ static blk_status_t scsi_queue_rq(struct blk_mq_hw_ctx *hctx,
* we hit an error, as we will never see this command
* again.
*/
- if (req->rq_flags & RQF_DONTPREP)
- scsi_mq_uninit_cmd(cmd);
+ scsi_cleanup_rq(req);
+
scsi_run_queue_async(sdev);
break;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 01/29] scsi: core: Clear cmd->flags for terminal queuing error
2026-11-08 20:47 ` [PATCH v9 01/29] scsi: core: Clear cmd->flags for terminal queuing error John Garry
@ 2026-11-09 20:49 ` Bart Van Assche
0 siblings, 0 replies; 44+ messages in thread
From: Bart Van Assche @ 2026-11-09 20:49 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
On 11/8/26 12:47 PM, John Garry wrote:
> If a request can't be queued in scsi_queue_rq(), then we error out
> with a blk_status_t code. Such a scenario may be the scsi_device has
> its transport offline. For BLK_STS_RESOURCE or BLK_STS_AGAIN, the
> request will be requeued in the block layer.
This is not correct for BLK_STS_AGAIN. Only BLK_STS_RESOURCE and
BLK_STS_DEV_RESOURCE cause the block layer to requeue a request. For
all other status codes, including BLK_STS_AGAIN,
blk_mq_dispatch_rq_list() and blk_mq_try_issue_directly() call
blk_mq_end_request().
> For other errors, the block layer ends the request and so we won't
> see it again (until recycled). Normally when a request completes
> scmd->flags is cleared in scsi_end_request(). However for the error
> scenario mentioned above, scsi_end_request() is not called and so
> scmd->flags is not cleared. As such, when the request is recycled we
> see the old value in scmd->flags.
Please describe the consequences of this bug in the patch description.
As far as I can tell there are at least two:
- SCMD_FAIL_IF_RECOVERING is part of SCMD_PRESERVED_FLAGS and
scsi_execute_cmd() ORs new flags into scmd->flags. If a command
submitted with SCMD_FAIL_IF_RECOVERING is failed with BLK_STS_OFFLINE
because the host is in recovery, the flag leaks into the next request
that is allocated with the same tag. That request will then be failed
instead of requeued if it is submitted while the SCSI error handler
is active.
- If SCMD_INITIALIZED leaks, scsi_init_command() skips
scsi_initialize_rq() for the next filesystem request. Hence,
jiffies_at_alloc and retries are not reinitialized and that request
may be failed prematurely by scsi_cmd_runtime_exceeded().
> Fix this issue by clearing scmd->flags in the error scenario mentioned
> by calling scsi_cleanup_rq().
Please add Fixes: and Cc: stable tags. For the SCMD_FAIL_IF_RECOVERING
leak, I think this should be:
Fixes: 310bcaef6d7e ("scsi: core: Support failing requests while
recovering")
> @@ -1994,8 +1994,8 @@ static blk_status_t scsi_queue_rq(struct
blk_mq_hw_ctx *hctx,
> * we hit an error, as we will never see this command
> * again.
> */
> - if (req->rq_flags & RQF_DONTPREP)
> - scsi_mq_uninit_cmd(cmd);
> + scsi_cleanup_rq(req);
> + scsi_run_queue_async(sdev);
Since the block layer also ends requests for which scsi_queue_rq()
returns BLK_STS_AGAIN, the BLK_STS_AGAIN case has the same issue.
Please convert that case too:
case BLK_STS_AGAIN:
cmd->result = DID_BUS_BUSY << 16;
scsi_cleanup_rq(req);
break;
Although no SCSI driver returns BLK_STS_AGAIN today, I think both
error paths should handle this in the same way.
Thanks,
Bart.
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 02/29] libmultipath: Add initial framework
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
2026-11-08 20:47 ` [PATCH v9 01/29] scsi: core: Clear cmd->flags for terminal queuing error John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 03/29] libmultipath: Add basic gendisk support John Garry
` (27 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry, Hannes Reinecke
Add initial framework for libmultipath. libmultipath is a library for
multipath-capable block drivers, such as NVMe. The main function is to
support path management, path selection, and failover handling.
Basic support to add and remove the head structure - mpath_head - is
included.
This main purpose of this structure is to manage available paths and path
selection. It is quite similar to the multipath functionality in
nvme_ns_head. It also manages the multipath gendisk.
Each path is represented by the mpath_device structure. It should hold a
pointer to the per-path gendisk and also a list element for all siblings
of paths. For NVMe, there would be a mpath_device per nvme_ns.
All the libmultipath code is more or less taken from
drivers/nvme/host/multipath.c, which was originally authored by Christoph
Hellwig <hch@lst.de>.
Reviewed-by: Hannes Reinecke <hare@kernel.org>
Signed-off-by: John Garry <john.garry@linux.dev>
---
include/linux/multipath.h | 28 ++++++++++++++++
lib/Kconfig | 6 ++++
lib/Makefile | 2 ++
lib/multipath.c | 68 +++++++++++++++++++++++++++++++++++++++
4 files changed, 104 insertions(+)
create mode 100644 include/linux/multipath.h
create mode 100644 lib/multipath.c
diff --git a/include/linux/multipath.h b/include/linux/multipath.h
new file mode 100644
index 000000000000..407d985cd31f
--- /dev/null
+++ b/include/linux/multipath.h
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#ifndef _LIBMULTIPATH_H
+#define _LIBMULTIPATH_H
+
+#include <linux/blkdev.h>
+#include <linux/srcu.h>
+
+struct mpath_device {
+ struct list_head siblings;
+ struct gendisk *disk;
+};
+
+struct mpath_head {
+ struct srcu_struct srcu;
+ struct list_head dev_list; /* list of all mpath_devs */
+ struct mutex lock;
+
+ refcount_t refcount;
+
+ struct mpath_device __rcu *current_path[MAX_NUMNODES];
+};
+
+int mpath_get_head(struct mpath_head *mpath_head);
+void mpath_put_head(struct mpath_head *mpath_head);
+int mpath_head_init(struct mpath_head *mpath_head);
+void mpath_head_uninit(struct mpath_head *mpath_head);
+
+#endif // _LIBMULTIPATH_H
diff --git a/lib/Kconfig b/lib/Kconfig
index 1e42f5167c68..3580bab7c2b1 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -630,3 +630,9 @@ config UNION_FIND
config MIN_HEAP
bool
+
+config LIBMULTIPATH
+ bool "MULTIPATH BLOCK DRIVER LIBRARY"
+ depends on BLOCK
+ help
+ If you say yes here then you get a multipath lib for block drivers
diff --git a/lib/Makefile b/lib/Makefile
index a2afe954805c..b4c0da268b67 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -350,3 +350,5 @@ CONTEXT_ANALYSIS_test_context-analysis.o := y
obj-$(CONFIG_CONTEXT_ANALYSIS_TEST) += test_context-analysis.o
subdir-$(CONFIG_FORTIFY_SOURCE) += test_fortify
+
+obj-$(CONFIG_LIBMULTIPATH) += multipath.o
diff --git a/lib/multipath.c b/lib/multipath.c
new file mode 100644
index 000000000000..31ef98980ba4
--- /dev/null
+++ b/lib/multipath.c
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2018 Christoph Hellwig.
+ *
+ * Base on NVMe multipath support in drivers/nvme/host/multipath.c
+ */
+#include <linux/module.h>
+#include <linux/multipath.h>
+#include <linux/wait_bit.h>
+
+static struct workqueue_struct *mpath_wq;
+
+int mpath_get_head(struct mpath_head *mpath_head)
+{
+ if (!refcount_inc_not_zero(&mpath_head->refcount))
+ return -ENXIO;
+ return 0;
+}
+EXPORT_SYMBOL_GPL(mpath_get_head);
+
+void mpath_put_head(struct mpath_head *mpath_head)
+{
+ refcount_t *refcount = &mpath_head->refcount;
+
+ if (refcount_dec_and_test(&mpath_head->refcount))
+ wake_up_var(refcount);
+}
+EXPORT_SYMBOL_GPL(mpath_put_head);
+
+void mpath_head_uninit(struct mpath_head *mpath_head)
+{
+ refcount_t *refcount = &mpath_head->refcount;
+
+ if (!refcount_dec_and_test(refcount))
+ wait_var_event(refcount, !refcount_read(refcount));
+ cleanup_srcu_struct(&mpath_head->srcu);
+}
+EXPORT_SYMBOL_GPL(mpath_head_uninit);
+
+int mpath_head_init(struct mpath_head *mpath_head)
+{
+ memset(mpath_head, 0, sizeof(*mpath_head));
+ INIT_LIST_HEAD(&mpath_head->dev_list);
+ mutex_init(&mpath_head->lock);
+ refcount_set(&mpath_head->refcount, 1);
+
+ return init_srcu_struct(&mpath_head->srcu);
+}
+EXPORT_SYMBOL_GPL(mpath_head_init);
+
+static int __init mpath_init(void)
+{
+ mpath_wq = alloc_workqueue("mpath-wq",
+ WQ_UNBOUND | WQ_MEM_RECLAIM | WQ_SYSFS, 0);
+ if (!mpath_wq)
+ return -ENOMEM;
+ return 0;
+}
+
+static void __exit mpath_exit(void)
+{
+ destroy_workqueue(mpath_wq);
+}
+
+module_init(mpath_init);
+module_exit(mpath_exit);
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("libmultipath");
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 03/29] libmultipath: Add basic gendisk support
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
2026-11-08 20:47 ` [PATCH v9 01/29] scsi: core: Clear cmd->flags for terminal queuing error John Garry
2026-11-08 20:47 ` [PATCH v9 02/29] libmultipath: Add initial framework John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 04/29] libmultipath: Add path selection support John Garry
` (26 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add support to allocate and free a multipath gendisk.
NVMe has almost like-for-like equivalents here:
- mpath_alloc_head_disk() -> nvme_mpath_alloc_disk()
- multipath_partition_scan_work() -> nvme_partition_scan_work()
- mpath_remove_disk() -> nvme_remove_head()
- mpath_device_set_live() -> nvme_mpath_set_live()
struct mpath_head_template is introduced as a method for drivers to
provide custom multipath functionality.
Signed-off-by: John Garry <john.garry@linux.dev>
---
include/linux/multipath.h | 38 ++++++++++++++
lib/multipath.c | 103 ++++++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+)
diff --git a/include/linux/multipath.h b/include/linux/multipath.h
index 407d985cd31f..ca6e02505be4 100644
--- a/include/linux/multipath.h
+++ b/include/linux/multipath.h
@@ -5,11 +5,19 @@
#include <linux/blkdev.h>
#include <linux/srcu.h>
+extern const struct block_device_operations mpath_ops;
+
struct mpath_device {
+ struct mpath_head *mpath_head;
struct list_head siblings;
struct gendisk *disk;
};
+struct mpath_head_template {
+};
+
+#define MPATH_HEAD_DISK_LIVE 0
+
struct mpath_head {
struct srcu_struct srcu;
struct list_head dev_list; /* list of all mpath_devs */
@@ -17,12 +25,42 @@ struct mpath_head {
refcount_t refcount;
+ unsigned long flags;
+ struct gendisk *disk;
+ struct work_struct partition_scan_work;
+ struct device *parent;
+ const struct attribute_group **disk_groups;
+ const struct mpath_head_template *mpdt;
struct mpath_device __rcu *current_path[MAX_NUMNODES];
};
+static inline struct mpath_head *mpath_bd_device_to_head(struct device *dev)
+{
+ return dev_get_drvdata(dev);
+}
+
+static inline struct mpath_head *mpath_gendisk_to_head(struct gendisk *disk)
+{
+ return mpath_bd_device_to_head(disk_to_dev(disk));
+}
+
int mpath_get_head(struct mpath_head *mpath_head);
void mpath_put_head(struct mpath_head *mpath_head);
int mpath_head_init(struct mpath_head *mpath_head);
void mpath_head_uninit(struct mpath_head *mpath_head);
+void mpath_put_disk(struct mpath_head *mpath_head);
+void mpath_remove_disk(struct mpath_head *mpath_head);
+int mpath_alloc_head_disk(struct mpath_head *mpath_head,
+ struct queue_limits *lim, int numa_node);
+void mpath_device_set_live(struct mpath_device *mpath_device);
+
+static inline bool is_mpath_disk(struct gendisk *disk)
+{
+ #if IS_ENABLED(CONFIG_LIBMULTIPATH)
+ return disk->fops == &mpath_ops;
+ #else
+ return false;
+ #endif
+}
#endif // _LIBMULTIPATH_H
diff --git a/lib/multipath.c b/lib/multipath.c
index 31ef98980ba4..93c0ca8b390b 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -37,6 +37,106 @@ void mpath_head_uninit(struct mpath_head *mpath_head)
}
EXPORT_SYMBOL_GPL(mpath_head_uninit);
+static int mpath_bdev_open(struct gendisk *disk, blk_mode_t mode)
+{
+ struct mpath_head *mpath_head = disk->private_data;
+
+ return mpath_get_head(mpath_head);
+}
+
+static void mpath_bdev_release(struct gendisk *disk)
+{
+ struct mpath_head *mpath_head = disk->private_data;
+
+ mpath_put_head(mpath_head);
+}
+
+const struct block_device_operations mpath_ops = {
+ .owner = THIS_MODULE,
+ .open = mpath_bdev_open,
+ .release = mpath_bdev_release,
+};
+EXPORT_SYMBOL_GPL(mpath_ops);
+
+static void multipath_partition_scan_work(struct work_struct *work)
+{
+ struct mpath_head *mpath_head =
+ container_of(work, struct mpath_head, partition_scan_work);
+
+ if (WARN_ON_ONCE(!test_and_clear_bit(GD_SUPPRESS_PART_SCAN,
+ &mpath_head->disk->state)))
+ return;
+
+ mutex_lock(&mpath_head->disk->open_mutex);
+ bdev_disk_changed(mpath_head->disk, false);
+ mutex_unlock(&mpath_head->disk->open_mutex);
+}
+
+void mpath_remove_disk(struct mpath_head *mpath_head)
+{
+ if (test_and_clear_bit(MPATH_HEAD_DISK_LIVE, &mpath_head->flags)) {
+ struct gendisk *disk = mpath_head->disk;
+
+ del_gendisk(disk);
+ }
+}
+EXPORT_SYMBOL_GPL(mpath_remove_disk);
+
+void mpath_put_disk(struct mpath_head *mpath_head)
+{
+ if (!mpath_head->disk)
+ return;
+
+ /* make sure all pending bios are cleaned up */
+ flush_work(&mpath_head->partition_scan_work);
+ put_disk(mpath_head->disk);
+ mpath_head->disk = NULL;
+}
+EXPORT_SYMBOL_GPL(mpath_put_disk);
+
+int mpath_alloc_head_disk(struct mpath_head *mpath_head,
+ struct queue_limits *lim, int numa_node)
+{
+ struct gendisk *disk;
+
+ if (mpath_head->disk)
+ return -EINVAL;
+
+ disk = blk_alloc_disk(lim, numa_node);
+ if (IS_ERR(disk))
+ return PTR_ERR(disk);
+
+ mpath_head->disk = disk;
+ mpath_head->disk->private_data = mpath_head;
+ mpath_head->disk->fops = &mpath_ops;
+
+ set_bit(GD_SUPPRESS_PART_SCAN, &mpath_head->disk->state);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(mpath_alloc_head_disk);
+
+void mpath_device_set_live(struct mpath_device *mpath_device)
+{
+ struct mpath_head *mpath_head = mpath_device->mpath_head;
+ int ret;
+
+ if (!mpath_head->disk)
+ return;
+
+ if (!test_and_set_bit(MPATH_HEAD_DISK_LIVE, &mpath_head->flags)) {
+ dev_set_drvdata(disk_to_dev(mpath_head->disk), mpath_head);
+ ret = device_add_disk(mpath_head->parent, mpath_head->disk,
+ mpath_head->disk_groups);
+ if (ret) {
+ clear_bit(MPATH_HEAD_DISK_LIVE, &mpath_head->flags);
+ return;
+ }
+ queue_work(mpath_wq, &mpath_head->partition_scan_work);
+ }
+}
+EXPORT_SYMBOL_GPL(mpath_device_set_live);
+
int mpath_head_init(struct mpath_head *mpath_head)
{
memset(mpath_head, 0, sizeof(*mpath_head));
@@ -44,6 +144,9 @@ int mpath_head_init(struct mpath_head *mpath_head)
mutex_init(&mpath_head->lock);
refcount_set(&mpath_head->refcount, 1);
+ INIT_WORK(&mpath_head->partition_scan_work,
+ multipath_partition_scan_work);
+
return init_srcu_struct(&mpath_head->srcu);
}
EXPORT_SYMBOL_GPL(mpath_head_init);
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 04/29] libmultipath: Add path selection support
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (2 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 03/29] libmultipath: Add basic gendisk support John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:07 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 05/29] libmultipath: Add bio handling John Garry
` (25 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add code for path selection.
NVMe ANA is abstracted into enum mpath_access_state. The motivation here is
so that SCSI ALUA can be used. Callbacks .is_disabled and .is_optimized
are added to get the path access state.
Path selection modes round-robin, NUMA, and queue-depth are added, same
as NVMe supports.
NVMe has almost like-for-like equivalents here:
- __mpath_find_path() -> __nvme_find_path()
- mpath_find_path() -> nvme_find_path()
and similar for all introduced callee functions.
Functions mpath_set_iopolicy() and mpath_get_iopolicy() are added for
setting default iopolicy.
Signed-off-by: John Garry <john.garry@linux.dev>
---
include/linux/multipath.h | 33 +++++
lib/multipath.c | 251 ++++++++++++++++++++++++++++++++++++++
2 files changed, 284 insertions(+)
diff --git a/include/linux/multipath.h b/include/linux/multipath.h
index ca6e02505be4..9bbab0aa8961 100644
--- a/include/linux/multipath.h
+++ b/include/linux/multipath.h
@@ -7,13 +7,31 @@
extern const struct block_device_operations mpath_ops;
+enum mpath_iopolicy_e {
+ MPATH_IOPOLICY_NUMA,
+ MPATH_IOPOLICY_RR,
+ MPATH_IOPOLICY_QD,
+};
+
+enum mpath_access_state {
+ MPATH_STATE_OPTIMIZED,
+ MPATH_STATE_NONOPTIMIZED,
+ MPATH_STATE_OTHER
+};
+
struct mpath_device {
struct mpath_head *mpath_head;
struct list_head siblings;
struct gendisk *disk;
+ int numa_node;
+ atomic_t *nr_active;
+ enum mpath_access_state access_state;
};
struct mpath_head_template {
+ bool (*is_disabled)(struct mpath_device *);
+ bool (*is_optimized)(struct mpath_device *);
+ const struct attribute_group **device_groups;
};
#define MPATH_HEAD_DISK_LIVE 0
@@ -25,6 +43,7 @@ struct mpath_head {
refcount_t refcount;
+ enum mpath_iopolicy_e *iopolicy;
unsigned long flags;
struct gendisk *disk;
struct work_struct partition_scan_work;
@@ -44,6 +63,14 @@ static inline struct mpath_head *mpath_gendisk_to_head(struct gendisk *disk)
return mpath_bd_device_to_head(disk_to_dev(disk));
}
+static inline enum mpath_iopolicy_e mpath_read_iopolicy(
+ struct mpath_head *mpath_head)
+{
+ return READ_ONCE(*mpath_head->iopolicy);
+}
+void mpath_synchronize(struct mpath_head *mpath_head);
+int mpath_set_iopolicy(const char *str, enum mpath_iopolicy_e *iopolicy);
+int mpath_get_iopolicy(char *buf, int iopolicy);
int mpath_get_head(struct mpath_head *mpath_head);
void mpath_put_head(struct mpath_head *mpath_head);
int mpath_head_init(struct mpath_head *mpath_head);
@@ -63,4 +90,10 @@ static inline bool is_mpath_disk(struct gendisk *disk)
return false;
#endif
}
+
+static inline bool mpath_qd_iopolicy(enum mpath_iopolicy_e *iopolicy)
+{
+ return READ_ONCE(*iopolicy) == MPATH_IOPOLICY_QD;
+}
+
#endif // _LIBMULTIPATH_H
diff --git a/lib/multipath.c b/lib/multipath.c
index 93c0ca8b390b..b8ea8478037e 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -8,8 +8,245 @@
#include <linux/multipath.h>
#include <linux/wait_bit.h>
+static struct mpath_device *mpath_find_path(struct mpath_head *mpath_head);
+
static struct workqueue_struct *mpath_wq;
+static const char * const mpath_iopolicy_names[] = {
+ [MPATH_IOPOLICY_NUMA] = "numa",
+ [MPATH_IOPOLICY_RR] = "round-robin",
+ [MPATH_IOPOLICY_QD] = "queue-depth",
+};
+
+static int mpath_iopolicy_parse(const char *str)
+{
+ return __sysfs_match_string(mpath_iopolicy_names,
+ ARRAY_SIZE(mpath_iopolicy_names), str);
+}
+
+int mpath_set_iopolicy(const char *str, enum mpath_iopolicy_e *iopolicy)
+{
+ int policy;
+
+ if (!str)
+ return -EINVAL;
+ policy = mpath_iopolicy_parse(str);
+ if (policy < 0)
+ return policy;
+ WRITE_ONCE(*iopolicy, policy);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(mpath_set_iopolicy);
+
+int mpath_get_iopolicy(char *buf, int iopolicy)
+{
+ return sprintf(buf, "%s\n", mpath_iopolicy_names[iopolicy]);
+}
+EXPORT_SYMBOL_GPL(mpath_get_iopolicy);
+
+
+void mpath_synchronize(struct mpath_head *mpath_head)
+{
+ synchronize_srcu(&mpath_head->srcu);
+}
+EXPORT_SYMBOL_GPL(mpath_synchronize);
+
+static bool mpath_path_is_disabled(struct mpath_head *mpath_head,
+ struct mpath_device *mpath_device)
+{
+ return mpath_head->mpdt->is_disabled(mpath_device);
+}
+
+static struct mpath_device *__mpath_find_path(struct mpath_head *mpath_head,
+ int node)
+{
+ int found_distance = INT_MAX, fallback_distance = INT_MAX, distance;
+ struct mpath_device *found = NULL, *fallback = NULL, *mpath_device;
+
+ list_for_each_entry_srcu(mpath_device, &mpath_head->dev_list, siblings,
+ srcu_read_lock_held(&mpath_head->srcu)) {
+ if (mpath_path_is_disabled(mpath_head, mpath_device))
+ continue;
+
+ if (mpath_device->numa_node != NUMA_NO_NODE &&
+ (mpath_read_iopolicy(mpath_head) ==
+ MPATH_IOPOLICY_NUMA))
+ distance = node_distance(node,
+ mpath_device->numa_node);
+ else
+ distance = LOCAL_DISTANCE;
+
+ switch(mpath_device->access_state) {
+ case MPATH_STATE_OPTIMIZED:
+ if (distance < found_distance) {
+ found_distance = distance;
+ found = mpath_device;
+ }
+ break;
+ case MPATH_STATE_NONOPTIMIZED:
+ if (distance < fallback_distance) {
+ fallback_distance = distance;
+ fallback = mpath_device;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (!found)
+ found = fallback;
+
+ if (found)
+ rcu_assign_pointer(mpath_head->current_path[node], found);
+
+ return found;
+}
+
+static struct mpath_device *mpath_next_dev(struct mpath_head *mpath_head,
+ struct mpath_device *mpath_dev)
+{
+ mpath_dev = list_next_or_null_rcu(&mpath_head->dev_list,
+ &mpath_dev->siblings, struct mpath_device,
+ siblings);
+
+ if (mpath_dev)
+ return mpath_dev;
+ return list_first_or_null_rcu(&mpath_head->dev_list,
+ struct mpath_device, siblings);
+}
+
+static struct mpath_device *mpath_round_robin_path(
+ struct mpath_head *mpath_head)
+{
+ struct mpath_device *mpath_device, *found = NULL;
+ int node = numa_node_id();
+ enum mpath_access_state access_state_old;
+ struct mpath_device *old =
+ srcu_dereference(mpath_head->current_path[node],
+ &mpath_head->srcu);
+
+ if (unlikely(!old))
+ return __mpath_find_path(mpath_head, node);
+
+ if (list_is_singular(&mpath_head->dev_list)) {
+ if (mpath_path_is_disabled(mpath_head, old))
+ return NULL;
+ return old;
+ }
+
+ for (mpath_device = mpath_next_dev(mpath_head, old);
+ mpath_device && mpath_device != old;
+ mpath_device = mpath_next_dev(mpath_head, mpath_device)) {
+
+ if (mpath_path_is_disabled(mpath_head, mpath_device))
+ continue;
+ if (mpath_device->access_state == MPATH_STATE_OPTIMIZED) {
+ found = mpath_device;
+ goto out;
+ }
+ if (mpath_device->access_state == MPATH_STATE_NONOPTIMIZED)
+ found = mpath_device;
+ }
+
+ /*
+ * The loop above skips the current path for round-robin semantics.
+ * Fall back to the current path if either:
+ * - no other optimized path found and current is optimized,
+ * - no other usable path found and current is usable.
+ */
+ access_state_old = old->access_state;
+ if (!mpath_path_is_disabled(mpath_head, old) &&
+ (access_state_old == MPATH_STATE_OPTIMIZED ||
+ (!found && access_state_old == MPATH_STATE_NONOPTIMIZED)))
+ return old;
+
+ if (!found)
+ return NULL;
+out:
+ rcu_assign_pointer(mpath_head->current_path[node], found);
+
+ return found;
+}
+
+static struct mpath_device *mpath_queue_depth_path(
+ struct mpath_head *mpath_head)
+{
+ struct mpath_device *best_opt = NULL, *mpath_device;
+ struct mpath_device *best_nonopt = NULL;
+ unsigned int min_depth_opt = UINT_MAX, min_depth_nonopt = UINT_MAX;
+ unsigned int depth;
+
+ list_for_each_entry_srcu(mpath_device, &mpath_head->dev_list, siblings,
+ srcu_read_lock_held(&mpath_head->srcu)) {
+
+ if (mpath_path_is_disabled(mpath_head, mpath_device))
+ continue;
+
+ depth = atomic_read(mpath_device->nr_active);
+
+ switch (mpath_device->access_state) {
+ case MPATH_STATE_OPTIMIZED:
+ if (depth < min_depth_opt) {
+ min_depth_opt = depth;
+ best_opt = mpath_device;
+ }
+ break;
+ case MPATH_STATE_NONOPTIMIZED:
+ if (depth < min_depth_nonopt) {
+ min_depth_nonopt = depth;
+ best_nonopt = mpath_device;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (min_depth_opt == 0)
+ return best_opt;
+ }
+
+ return best_opt ? best_opt : best_nonopt;
+}
+
+static inline bool mpath_path_is_optimized(struct mpath_head *mpath_head,
+ struct mpath_device *mpath_device)
+{
+ return mpath_head->mpdt->is_optimized(mpath_device);
+}
+
+static struct mpath_device *mpath_numa_path(struct mpath_head *mpath_head)
+{
+ int node = numa_node_id();
+ struct mpath_device *mpath_device;
+
+ mpath_device = srcu_dereference(mpath_head->current_path[node],
+ &mpath_head->srcu);
+ if (unlikely(!mpath_device))
+ return __mpath_find_path(mpath_head, node);
+ if (unlikely(mpath_path_is_disabled(mpath_head, mpath_device)))
+ return __mpath_find_path(mpath_head, node);
+ if (unlikely(!mpath_path_is_optimized(mpath_head, mpath_device)))
+ return __mpath_find_path(mpath_head, node);
+ return mpath_device;
+}
+
+__maybe_unused
+static struct mpath_device *mpath_find_path(struct mpath_head *mpath_head)
+{
+ enum mpath_iopolicy_e iopolicy = mpath_read_iopolicy(mpath_head);
+
+ switch (iopolicy) {
+ case MPATH_IOPOLICY_QD:
+ return mpath_queue_depth_path(mpath_head);
+ case MPATH_IOPOLICY_RR:
+ return mpath_round_robin_path(mpath_head);
+ default:
+ return mpath_numa_path(mpath_head);
+ }
+}
+
int mpath_get_head(struct mpath_head *mpath_head)
{
if (!refcount_inc_not_zero(&mpath_head->refcount))
@@ -77,6 +314,7 @@ void mpath_remove_disk(struct mpath_head *mpath_head)
if (test_and_clear_bit(MPATH_HEAD_DISK_LIVE, &mpath_head->flags)) {
struct gendisk *disk = mpath_head->disk;
+ mpath_synchronize(mpath_head);
del_gendisk(disk);
}
}
@@ -134,6 +372,19 @@ void mpath_device_set_live(struct mpath_device *mpath_device)
}
queue_work(mpath_wq, &mpath_head->partition_scan_work);
}
+
+ mutex_lock(&mpath_head->lock);
+ if (mpath_path_is_optimized(mpath_head, mpath_device)) {
+ int node, srcu_idx;
+
+ srcu_idx = srcu_read_lock(&mpath_head->srcu);
+ for_each_online_node(node)
+ __mpath_find_path(mpath_head, node);
+ srcu_read_unlock(&mpath_head->srcu, srcu_idx);
+ }
+ mutex_unlock(&mpath_head->lock);
+
+ mpath_synchronize(mpath_head);
}
EXPORT_SYMBOL_GPL(mpath_device_set_live);
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 04/29] libmultipath: Add path selection support
2026-11-08 20:47 ` [PATCH v9 04/29] libmultipath: Add path selection support John Garry
@ 2026-11-08 21:07 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:07 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Medium] libmultipath: inconsistent path optimization check between
is_optimized() and access_state
--
commit 5d198416c184ad80e962a53ac50fbd829ed2c691
Author: John Garry <john.garry@linux.dev>
libmultipath: Add path selection support
This commit adds multipath path selection support for NUMA, round-robin,
and queue-depth I/O policies along with path access state abstractions.
> diff --git a/lib/multipath.c b/lib/multipath.c
> index 93c0ca8b390b1..b8ea8478037ef 100644
> --- a/lib/multipath.c
> +++ b/lib/multipath.c
> @@ -8,8 +8,245 @@
> #include <linux/multipath.h>
> #include <linux/wait_bit.h>
>
> +static struct mpath_device *mpath_find_path(struct mpath_head *mpath_head);
> +
[ ... ]
> +static struct mpath_device *__mpath_find_path(struct mpath_head *mpath_head,
> + int node)
> +{
> + int found_distance = INT_MAX, fallback_distance = INT_MAX, distance;
> + struct mpath_device *found = NULL, *fallback = NULL, *mpath_device;
> +
> + list_for_each_entry_srcu(mpath_device, &mpath_head->dev_list, siblings,
> + srcu_read_lock_held(&mpath_head->srcu)) {
> + if (mpath_path_is_disabled(mpath_head, mpath_device))
> + continue;
> +
> + if (mpath_device->numa_node != NUMA_NO_NODE &&
> + (mpath_read_iopolicy(mpath_head) ==
> + MPATH_IOPOLICY_NUMA))
> + distance = node_distance(node,
> + mpath_device->numa_node);
> + else
> + distance = LOCAL_DISTANCE;
> +
> + switch(mpath_device->access_state) {
> + case MPATH_STATE_OPTIMIZED:
> + if (distance < found_distance) {
> + found_distance = distance;
> + found = mpath_device;
> + }
> + break;
> + case MPATH_STATE_NONOPTIMIZED:
> + if (distance < fallback_distance) {
> + fallback_distance = distance;
> + fallback = mpath_device;
> + }
> + break;
> + default:
> + break;
> + }
> + }
[ ... ]
> +static inline bool mpath_path_is_optimized(struct mpath_head *mpath_head,
> + struct mpath_device *mpath_device)
> +{
> + return mpath_head->mpdt->is_optimized(mpath_device);
> +}
> +
> +static struct mpath_device *mpath_numa_path(struct mpath_head *mpath_head)
> +{
> + int node = numa_node_id();
> + struct mpath_device *mpath_device;
> +
> + mpath_device = srcu_dereference(mpath_head->current_path[node],
> + &mpath_head->srcu);
> + if (unlikely(!mpath_device))
> + return __mpath_find_path(mpath_head, node);
> + if (unlikely(mpath_path_is_disabled(mpath_head, mpath_device)))
> + return __mpath_find_path(mpath_head, node);
> + if (unlikely(!mpath_path_is_optimized(mpath_head, mpath_device)))
> + return __mpath_find_path(mpath_head, node);
> + return mpath_device;
> +}
[Severity: Medium]
Should __mpath_find_path(), mpath_round_robin_path(), and
mpath_queue_depth_path() check mpath_path_is_optimized() (or a callback
returning enum mpath_access_state) instead of reading
mpath_device->access_state directly?
mpath_numa_path() and mpath_device_set_live() call
mpath_path_is_optimized(), which invokes mpath_head->mpdt->is_optimized(),
whereas __mpath_find_path(), mpath_round_robin_path(), and
mpath_queue_depth_path() inspect mpath_device->access_state. If a driver
reports path optimization dynamically through .is_optimized() without
updating mpath_device->access_state (or vice versa), the two checks
disagree: when current_path[node] is non-optimized according to
.is_optimized() but still has access_state set to MPATH_STATE_OPTIMIZED,
mpath_numa_path() falls back to __mpath_find_path() on every I/O, and
__mpath_find_path() treats that non-optimized path as optimized and can
re-select it over a genuinely optimized path at a greater NUMA distance.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=4
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 05/29] libmultipath: Add bio handling
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (3 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 04/29] libmultipath: Add path selection support John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 06/29] libmultipath: Add support for mpath_device management John Garry
` (24 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add support to submit a bio per-path. In addition, for failover, add
support to requeue a failed bio.
NVMe has almost like-for-like equivalents here:
- nvme_available_path() -> mpath_available_path()
- nvme_requeue_work() -> mpath_requeue_work()
- nvme_ns_head_submit_bio() -> mpath_bdev_submit_bio()
For failover, a driver may want to re-submit a bio, so add support to
clone a bio prior to submission.
A bio which is submitted to a per-path device has flag REQ_MPATH set,
same as what is done for NVMe with REQ_NVME_MPATH.
Signed-off-by: John Garry <john.garry@linux.dev>
---
include/linux/multipath.h | 22 ++++++++
lib/multipath.c | 108 +++++++++++++++++++++++++++++++++++++-
2 files changed, 128 insertions(+), 2 deletions(-)
diff --git a/include/linux/multipath.h b/include/linux/multipath.h
index 9bbab0aa8961..374f0531f7e0 100644
--- a/include/linux/multipath.h
+++ b/include/linux/multipath.h
@@ -3,6 +3,7 @@
#define _LIBMULTIPATH_H
#include <linux/blkdev.h>
+#include <linux/blk-mq.h>
#include <linux/srcu.h>
extern const struct block_device_operations mpath_ops;
@@ -29,8 +30,10 @@ struct mpath_device {
};
struct mpath_head_template {
+ bool (*available_path)(struct mpath_device *);
bool (*is_disabled)(struct mpath_device *);
bool (*is_optimized)(struct mpath_device *);
+ struct bio *(*clone_bio)(struct bio *);
const struct attribute_group **device_groups;
};
@@ -44,6 +47,14 @@ struct mpath_head {
refcount_t refcount;
enum mpath_iopolicy_e *iopolicy;
+
+ struct bio_list requeue_list; /* list for requeing bio */
+ spinlock_t requeue_lock;
+ struct work_struct requeue_work; /* work struct for requeue */
+
+ atomic_long_t requeue_no_usable_path_cnt;
+ atomic_long_t fail_no_avail_path_cnt;
+
unsigned long flags;
struct gendisk *disk;
struct work_struct partition_scan_work;
@@ -53,6 +64,13 @@ struct mpath_head {
struct mpath_device __rcu *current_path[MAX_NUMNODES];
};
+#define REQ_MPATH REQ_DRV
+
+static inline bool is_mpath_request(struct request *req)
+{
+ return req->cmd_flags & REQ_MPATH;
+}
+
static inline struct mpath_head *mpath_bd_device_to_head(struct device *dev)
{
return dev_get_drvdata(dev);
@@ -96,4 +114,8 @@ static inline bool mpath_qd_iopolicy(enum mpath_iopolicy_e *iopolicy)
return READ_ONCE(*iopolicy) == MPATH_IOPOLICY_QD;
}
+static inline void mpath_schedule_requeue_work(struct mpath_head *mpath_head)
+{
+ kblockd_schedule_work(&mpath_head->requeue_work);
+}
#endif // _LIBMULTIPATH_H
diff --git a/lib/multipath.c b/lib/multipath.c
index b8ea8478037e..5dc7994f8095 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -7,6 +7,7 @@
#include <linux/module.h>
#include <linux/multipath.h>
#include <linux/wait_bit.h>
+#include <trace/events/block.h>
static struct mpath_device *mpath_find_path(struct mpath_head *mpath_head);
@@ -45,7 +46,6 @@ int mpath_get_iopolicy(char *buf, int iopolicy)
}
EXPORT_SYMBOL_GPL(mpath_get_iopolicy);
-
void mpath_synchronize(struct mpath_head *mpath_head)
{
synchronize_srcu(&mpath_head->srcu);
@@ -232,7 +232,6 @@ static struct mpath_device *mpath_numa_path(struct mpath_head *mpath_head)
return mpath_device;
}
-__maybe_unused
static struct mpath_device *mpath_find_path(struct mpath_head *mpath_head)
{
enum mpath_iopolicy_e iopolicy = mpath_read_iopolicy(mpath_head);
@@ -247,6 +246,81 @@ static struct mpath_device *mpath_find_path(struct mpath_head *mpath_head)
}
}
+static bool mpath_available_path(struct mpath_head *mpath_head)
+{
+ struct mpath_device *mpath_device;
+
+ if (!test_bit(MPATH_HEAD_DISK_LIVE, &mpath_head->flags))
+ return false;
+
+ list_for_each_entry_srcu(mpath_device, &mpath_head->dev_list, siblings,
+ srcu_read_lock_held(&mpath_head->srcu)) {
+ if (mpath_head->mpdt->available_path(mpath_device))
+ return true;
+ }
+
+ return false;
+}
+
+static void mpath_bdev_submit_bio(struct bio *bio)
+{
+ struct mpath_head *mpath_head = bio->bi_bdev->bd_disk->private_data;
+ struct device *dev = mpath_head->parent;
+ struct mpath_device *mpath_device;
+ int srcu_idx;
+
+ /*
+ * The mpath_device might be going away and the bio might be moved to a
+ * different queue in failover, so we need to use the bio_split
+ * pool from the original queue to allocate the bvecs from.
+ */
+ bio = bio_split_to_limits(bio);
+ if (!bio)
+ return;
+
+ srcu_idx = srcu_read_lock(&mpath_head->srcu);
+ mpath_device = mpath_find_path(mpath_head);
+
+ if (likely(mpath_device)) {
+ if (mpath_head->mpdt->clone_bio) {
+ struct bio *orig = bio;
+
+ bio = mpath_head->mpdt->clone_bio(bio);
+ if (!bio) {
+ bio_io_error(orig);
+ goto out;
+ }
+ }
+
+ bio_set_dev(bio, mpath_device->disk->part0);
+ /*
+ * Use BIO_REMAPPED to skip bio_check_eod() when this bio
+ * enters submit_bio_noacct() for the per-path device. The EOD
+ * check already passed on the multipath head.
+ */
+ bio_set_flag(bio, BIO_REMAPPED);
+ bio->bi_opf |= REQ_MPATH;
+ trace_block_bio_remap(bio, disk_devt(mpath_head->disk),
+ bio->bi_iter.bi_sector);
+ submit_bio_noacct(bio);
+ } else if (mpath_available_path(mpath_head)) {
+ dev_warn_ratelimited(dev, "no usable path - requeuing I/O\n");
+
+ spin_lock_irq(&mpath_head->requeue_lock);
+ bio_list_add(&mpath_head->requeue_list, bio);
+ spin_unlock_irq(&mpath_head->requeue_lock);
+ atomic_long_inc(&mpath_head->requeue_no_usable_path_cnt);
+ } else {
+ dev_warn_ratelimited(dev, "no available path - failing I/O\n");
+
+ bio_io_error(bio);
+ atomic_long_inc(&mpath_head->fail_no_avail_path_cnt);
+ }
+
+out:
+ srcu_read_unlock(&mpath_head->srcu, srcu_idx);
+}
+
int mpath_get_head(struct mpath_head *mpath_head)
{
if (!refcount_inc_not_zero(&mpath_head->refcount))
@@ -292,6 +366,7 @@ const struct block_device_operations mpath_ops = {
.owner = THIS_MODULE,
.open = mpath_bdev_open,
.release = mpath_bdev_release,
+ .submit_bio = mpath_bdev_submit_bio,
};
EXPORT_SYMBOL_GPL(mpath_ops);
@@ -309,12 +384,35 @@ static void multipath_partition_scan_work(struct work_struct *work)
mutex_unlock(&mpath_head->disk->open_mutex);
}
+static void mpath_requeue_work(struct work_struct *work)
+{
+ struct mpath_head *mpath_head =
+ container_of(work, struct mpath_head, requeue_work);
+ struct bio *bio, *next;
+
+ spin_lock_irq(&mpath_head->requeue_lock);
+ next = bio_list_get(&mpath_head->requeue_list);
+ spin_unlock_irq(&mpath_head->requeue_lock);
+
+ while ((bio = next) != NULL) {
+ next = bio->bi_next;
+ bio->bi_next = NULL;
+ submit_bio_noacct(bio);
+ }
+}
+
void mpath_remove_disk(struct mpath_head *mpath_head)
{
if (test_and_clear_bit(MPATH_HEAD_DISK_LIVE, &mpath_head->flags)) {
struct gendisk *disk = mpath_head->disk;
+ /*
+ * requeue I/O after MPATH_HEAD_DISK_LIVE has been cleared
+ * to allow multipath to fail all I/O.
+ */
mpath_synchronize(mpath_head);
+ mpath_schedule_requeue_work(mpath_head);
+
del_gendisk(disk);
}
}
@@ -326,6 +424,8 @@ void mpath_put_disk(struct mpath_head *mpath_head)
return;
/* make sure all pending bios are cleaned up */
+ kblockd_schedule_work(&mpath_head->requeue_work);
+ flush_work(&mpath_head->requeue_work);
flush_work(&mpath_head->partition_scan_work);
put_disk(mpath_head->disk);
mpath_head->disk = NULL;
@@ -385,6 +485,7 @@ void mpath_device_set_live(struct mpath_device *mpath_device)
mutex_unlock(&mpath_head->lock);
mpath_synchronize(mpath_head);
+ mpath_schedule_requeue_work(mpath_head);
}
EXPORT_SYMBOL_GPL(mpath_device_set_live);
@@ -397,6 +498,9 @@ int mpath_head_init(struct mpath_head *mpath_head)
INIT_WORK(&mpath_head->partition_scan_work,
multipath_partition_scan_work);
+ INIT_WORK(&mpath_head->requeue_work, mpath_requeue_work);
+ spin_lock_init(&mpath_head->requeue_lock);
+ bio_list_init(&mpath_head->requeue_list);
return init_srcu_struct(&mpath_head->srcu);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 06/29] libmultipath: Add support for mpath_device management
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (4 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 05/29] libmultipath: Add bio handling John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 07/29] libmultipath: Add delayed removal support John Garry
` (23 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add support to add or remove a mpath_device as a path.
NVMe has almost like-for-like equivalents here:
- nvme_mpath_clear_current_path() -> mpath_clear_current_path()
- nvme_mpath_add_sysfs_link() -> mpath_add_sysfs_link()
- nvme_mpath_remove_sysfs_link() -> mpath_remove_sysfs_link()
- nvme_mpath_revalidate_paths() -> mpath_revalidate_paths()
The functionality in mpath_clear_paths() and mpath_synchronize() have the
same pattern which is frequently used in the NVMe code.
Signed-off-by: John Garry <john.garry@linux.dev>
---
include/linux/multipath.h | 12 +++
lib/multipath.c | 154 ++++++++++++++++++++++++++++++++++++++
2 files changed, 166 insertions(+)
diff --git a/include/linux/multipath.h b/include/linux/multipath.h
index 374f0531f7e0..033ee8d54bbe 100644
--- a/include/linux/multipath.h
+++ b/include/linux/multipath.h
@@ -20,10 +20,13 @@ enum mpath_access_state {
MPATH_STATE_OTHER
};
+#define MPATH_DEVICE_SYSFS_ATTR_LINK 0
+
struct mpath_device {
struct mpath_head *mpath_head;
struct list_head siblings;
struct gendisk *disk;
+ unsigned long flags;
int numa_node;
atomic_t *nr_active;
enum mpath_access_state access_state;
@@ -89,6 +92,15 @@ static inline enum mpath_iopolicy_e mpath_read_iopolicy(
void mpath_synchronize(struct mpath_head *mpath_head);
int mpath_set_iopolicy(const char *str, enum mpath_iopolicy_e *iopolicy);
int mpath_get_iopolicy(char *buf, int iopolicy);
+bool mpath_clear_current_path(struct mpath_device *mpath_device);
+int mpath_add_device(struct mpath_device *mpath_device, struct gendisk *disk,
+ int numa_node, atomic_t *nr_active);
+bool mpath_delete_device(struct mpath_device *mpath_device);
+bool mpath_head_devices_empty(struct mpath_head *mpath_head);
+void mpath_clear_paths(struct mpath_head *mpath_head);
+void mpath_revalidate_paths(struct mpath_head *mpath_head);
+void mpath_add_sysfs_link(struct mpath_head *mpath_head);
+void mpath_remove_sysfs_link(struct mpath_device *mpath_device);
int mpath_get_head(struct mpath_head *mpath_head);
void mpath_put_head(struct mpath_head *mpath_head);
int mpath_head_init(struct mpath_head *mpath_head);
diff --git a/lib/multipath.c b/lib/multipath.c
index 5dc7994f8095..fd7821824447 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -52,6 +52,87 @@ void mpath_synchronize(struct mpath_head *mpath_head)
}
EXPORT_SYMBOL_GPL(mpath_synchronize);
+int mpath_add_device(struct mpath_device *mpath_device, struct gendisk *disk,
+ int numa_node, atomic_t *nr_active)
+{
+ struct mpath_head *mpath_head = mpath_device->mpath_head;
+
+ if (!disk || !nr_active)
+ return -EINVAL;
+
+ mpath_device->disk = disk;
+ mpath_device->numa_node = numa_node;
+ mpath_device->nr_active = nr_active;
+ mutex_lock(&mpath_head->lock);
+ list_add_tail_rcu(&mpath_device->siblings, &mpath_head->dev_list);
+ mutex_unlock(&mpath_head->lock);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(mpath_add_device);
+
+bool mpath_delete_device(struct mpath_device *mpath_device)
+{
+ bool empty;
+
+ mutex_lock(&mpath_device->mpath_head->lock);
+ list_del_rcu(&mpath_device->siblings);
+ empty = list_empty(&mpath_device->mpath_head->dev_list);
+ mutex_unlock(&mpath_device->mpath_head->lock);
+
+ mpath_synchronize(mpath_device->mpath_head);
+
+ return empty;
+}
+EXPORT_SYMBOL_GPL(mpath_delete_device);
+
+bool mpath_head_devices_empty(struct mpath_head *mpath_head)
+{
+ bool empty;
+
+ mutex_lock(&mpath_head->lock);
+ empty = list_empty(&mpath_head->dev_list);
+ mutex_unlock(&mpath_head->lock);
+
+ return empty;
+}
+EXPORT_SYMBOL_GPL(mpath_head_devices_empty);
+
+bool mpath_clear_current_path(struct mpath_device *mpath_device)
+{
+ struct mpath_head *mpath_head = mpath_device->mpath_head;
+ bool changed = false;
+ int node;
+
+ for_each_node(node) {
+ if (mpath_device ==
+ rcu_access_pointer(mpath_head->current_path[node])) {
+ rcu_assign_pointer(mpath_head->current_path[node],
+ NULL);
+ changed = true;
+ }
+ }
+
+ return changed;
+}
+EXPORT_SYMBOL_GPL(mpath_clear_current_path);
+
+void mpath_clear_paths(struct mpath_head *mpath_head)
+{
+ int node;
+
+ for_each_node(node)
+ rcu_assign_pointer(mpath_head->current_path[node], NULL);
+}
+EXPORT_SYMBOL_GPL(mpath_clear_paths);
+
+void mpath_revalidate_paths(struct mpath_head *mpath_head)
+{
+ mpath_clear_paths(mpath_head);
+ mpath_schedule_requeue_work(mpath_head);
+}
+EXPORT_SYMBOL_GPL(mpath_revalidate_paths);
+
static bool mpath_path_is_disabled(struct mpath_head *mpath_head,
struct mpath_device *mpath_device)
{
@@ -473,6 +554,8 @@ void mpath_device_set_live(struct mpath_device *mpath_device)
queue_work(mpath_wq, &mpath_head->partition_scan_work);
}
+ mpath_add_sysfs_link(mpath_head);
+
mutex_lock(&mpath_head->lock);
if (mpath_path_is_optimized(mpath_head, mpath_device)) {
int node, srcu_idx;
@@ -489,6 +572,77 @@ void mpath_device_set_live(struct mpath_device *mpath_device)
}
EXPORT_SYMBOL_GPL(mpath_device_set_live);
+void mpath_add_sysfs_link(struct mpath_head *mpath_head)
+{
+ struct device *target;
+ struct device *source;
+ int rc, srcu_idx;
+ struct kobject *mpath_gd_kobj;
+ struct mpath_device *mpath_device;
+
+ /*
+ * Ensure head disk node is already added otherwise we may get invalid
+ * kobj for head disk node
+ */
+ if (!test_bit(GD_ADDED, &mpath_head->disk->state))
+ return;
+
+ mpath_gd_kobj = &disk_to_dev(mpath_head->disk)->kobj;
+ srcu_idx = srcu_read_lock(&mpath_head->srcu);
+
+ list_for_each_entry_srcu(mpath_device, &mpath_head->dev_list, siblings,
+ srcu_read_lock_held(&mpath_head->srcu)) {
+ if (!test_bit(GD_ADDED, &mpath_device->disk->state))
+ continue;
+
+ if (test_and_set_bit(MPATH_DEVICE_SYSFS_ATTR_LINK,
+ &mpath_device->flags))
+ continue;
+
+ target = disk_to_dev(mpath_device->disk);
+ source = disk_to_dev(mpath_head->disk);
+ /*
+ * Create sysfs link from head gendisk kobject @kobj to the
+ * ns path gendisk kobject @target->kobj.
+ */
+ rc = sysfs_add_link_to_group(mpath_gd_kobj, "multipath",
+ &target->kobj, dev_name(target));
+
+ if (unlikely(rc)) {
+ dev_err(disk_to_dev(mpath_head->disk),
+ "failed to create link to %s rc=%d\n",
+ dev_name(target), rc);
+ clear_bit(MPATH_DEVICE_SYSFS_ATTR_LINK,
+ &mpath_device->flags);
+ } else {
+ dev_info(source, "Created multipath sysfs link to %s\n",
+ mpath_device->disk->disk_name);
+ }
+ }
+
+ srcu_read_unlock(&mpath_head->srcu, srcu_idx);
+}
+EXPORT_SYMBOL_GPL(mpath_add_sysfs_link);
+
+void mpath_remove_sysfs_link(struct mpath_device *mpath_device)
+{
+ struct device *target;
+ struct kobject *mpath_gd_kobj;
+ struct mpath_head *mpath_head = mpath_device->mpath_head;
+
+ if (!test_bit(MPATH_DEVICE_SYSFS_ATTR_LINK, &mpath_device->flags))
+ return;
+
+ target = disk_to_dev(mpath_device->disk);
+ mpath_gd_kobj = &disk_to_dev(mpath_head->disk)->kobj;
+
+ sysfs_remove_link_from_group(mpath_gd_kobj, "multipath",
+ dev_name(target));
+
+ clear_bit(MPATH_DEVICE_SYSFS_ATTR_LINK, &mpath_device->flags);
+}
+EXPORT_SYMBOL_GPL(mpath_remove_sysfs_link);
+
int mpath_head_init(struct mpath_head *mpath_head)
{
memset(mpath_head, 0, sizeof(*mpath_head));
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 07/29] libmultipath: Add delayed removal support
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (5 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 06/29] libmultipath: Add support for mpath_device management John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:11 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 08/29] libmultipath: Add sysfs helpers John Garry
` (22 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add support for delayed removal, same as exists for NVMe.
The purpose of this feature is to keep the multipath disk and cdev present
for intermittent periods of no available path.
Helpers mpath_delayed_removal_secs_show() and
mpath_delayed_removal_secs_store() may be used in the driver sysfs code.
The driver is responsible for supplying the removal work callback for
the delayed work.
Signed-off-by: John Garry <john.garry@linux.dev>
---
include/linux/multipath.h | 18 ++++++++
lib/multipath.c | 91 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 108 insertions(+), 1 deletion(-)
diff --git a/include/linux/multipath.h b/include/linux/multipath.h
index 033ee8d54bbe..66b6ad9c2219 100644
--- a/include/linux/multipath.h
+++ b/include/linux/multipath.h
@@ -34,6 +34,7 @@ struct mpath_device {
struct mpath_head_template {
bool (*available_path)(struct mpath_device *);
+ void (*remove_head)(struct mpath_head *);
bool (*is_disabled)(struct mpath_device *);
bool (*is_optimized)(struct mpath_device *);
struct bio *(*clone_bio)(struct bio *);
@@ -41,6 +42,7 @@ struct mpath_head_template {
};
#define MPATH_HEAD_DISK_LIVE 0
+#define MPATH_HEAD_QUEUE_IF_NO_PATH 1
struct mpath_head {
struct srcu_struct srcu;
@@ -58,6 +60,10 @@ struct mpath_head {
atomic_long_t requeue_no_usable_path_cnt;
atomic_long_t fail_no_avail_path_cnt;
+ struct delayed_work remove_work;
+ unsigned int delayed_removal_secs;
+ struct module *drv_module;
+
unsigned long flags;
struct gendisk *disk;
struct work_struct partition_scan_work;
@@ -111,6 +117,11 @@ void mpath_remove_disk(struct mpath_head *mpath_head);
int mpath_alloc_head_disk(struct mpath_head *mpath_head,
struct queue_limits *lim, int numa_node);
void mpath_device_set_live(struct mpath_device *mpath_device);
+bool mpath_can_remove_head(struct mpath_head *mpath_head);
+ssize_t mpath_delayed_removal_secs_show(struct mpath_head *mpath_head,
+ char *buf);
+ssize_t mpath_delayed_removal_secs_store(struct mpath_head *mpath_head,
+ const char *buf, size_t count);
static inline bool is_mpath_disk(struct gendisk *disk)
{
@@ -126,6 +137,13 @@ static inline bool mpath_qd_iopolicy(enum mpath_iopolicy_e *iopolicy)
return READ_ONCE(*iopolicy) == MPATH_IOPOLICY_QD;
}
+static inline bool mpath_head_queue_if_no_path(struct mpath_head *mpath_head)
+{
+ if (test_bit(MPATH_HEAD_QUEUE_IF_NO_PATH, &mpath_head->flags))
+ return true;
+ return false;
+}
+
static inline void mpath_schedule_requeue_work(struct mpath_head *mpath_head)
{
kblockd_schedule_work(&mpath_head->requeue_work);
diff --git a/lib/multipath.c b/lib/multipath.c
index fd7821824447..cdf8fbc5105b 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -67,6 +67,9 @@ int mpath_add_device(struct mpath_device *mpath_device, struct gendisk *disk,
list_add_tail_rcu(&mpath_device->siblings, &mpath_head->dev_list);
mutex_unlock(&mpath_head->lock);
+ if (cancel_delayed_work(&mpath_head->remove_work))
+ module_put(mpath_head->drv_module);
+
return 0;
}
EXPORT_SYMBOL_GPL(mpath_add_device);
@@ -340,7 +343,16 @@ static bool mpath_available_path(struct mpath_head *mpath_head)
return true;
}
- return false;
+ /*
+ * If "mpath_head->delayed_removal_secs" is set (i.e., non-zero), do
+ * not immediately fail I/O. Instead, requeue the I/O for the configured
+ * duration, anticipating that if there's a transient link failure then
+ * it may recover within this time window. This parameter is exported to
+ * userspace via sysfs, and its default value is zero. It is internally
+ * mapped to MPATH_HEAD_QUEUE_IF_NO_PATH. When delayed_removal_secs is
+ * non-zero, this flag is set to true. When zero, the flag is cleared.
+ */
+ return mpath_head_queue_if_no_path(mpath_head);
}
static void mpath_bdev_submit_bio(struct bio *bio)
@@ -482,6 +494,39 @@ static void mpath_requeue_work(struct work_struct *work)
}
}
+bool mpath_can_remove_head(struct mpath_head *mpath_head)
+{
+ unsigned long delay;
+ bool remove = false;
+
+ mutex_lock(&mpath_head->lock);
+ /*
+ * Ensure that no one could remove this module while the head
+ * remove work is pending.
+ */
+ if (mpath_head_queue_if_no_path(mpath_head) &&
+ !check_mul_overflow(mpath_head->delayed_removal_secs, HZ, &delay) &&
+ try_module_get(mpath_head->drv_module)) {
+ mod_delayed_work(mpath_wq, &mpath_head->remove_work, delay);
+ } else {
+ remove = true;
+ }
+
+ mutex_unlock(&mpath_head->lock);
+ return remove;
+}
+EXPORT_SYMBOL_GPL(mpath_can_remove_head);
+
+static void mpath_remove_head_work(struct work_struct *work)
+{
+ struct mpath_head *mpath_head = container_of(to_delayed_work(work),
+ struct mpath_head, remove_work);
+ struct module *drv_module = mpath_head->drv_module;
+
+ mpath_head->mpdt->remove_head(mpath_head);
+ module_put(drv_module);
+}
+
void mpath_remove_disk(struct mpath_head *mpath_head)
{
if (test_and_clear_bit(MPATH_HEAD_DISK_LIVE, &mpath_head->flags)) {
@@ -529,6 +574,9 @@ int mpath_alloc_head_disk(struct mpath_head *mpath_head,
mpath_head->disk->private_data = mpath_head;
mpath_head->disk->fops = &mpath_ops;
+ INIT_DELAYED_WORK(&mpath_head->remove_work, mpath_remove_head_work);
+ mpath_head->delayed_removal_secs = 0;
+
set_bit(GD_SUPPRESS_PART_SCAN, &mpath_head->disk->state);
return 0;
@@ -572,6 +620,47 @@ void mpath_device_set_live(struct mpath_device *mpath_device)
}
EXPORT_SYMBOL_GPL(mpath_device_set_live);
+ssize_t mpath_delayed_removal_secs_show(struct mpath_head *mpath_head,
+ char *buf)
+{
+ int ret;
+
+ mutex_lock(&mpath_head->lock);
+ ret = sysfs_emit(buf, "%u\n", mpath_head->delayed_removal_secs);
+ mutex_unlock(&mpath_head->lock);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(mpath_delayed_removal_secs_show);
+
+ssize_t mpath_delayed_removal_secs_store(struct mpath_head *mpath_head,
+ const char *buf, size_t count)
+{
+ unsigned int sec;
+ ssize_t ret;
+
+ ret = kstrtouint(buf, 0, &sec);
+ if (ret < 0)
+ return ret;
+
+ mutex_lock(&mpath_head->lock);
+ mpath_head->delayed_removal_secs = sec;
+ if (sec)
+ set_bit(MPATH_HEAD_QUEUE_IF_NO_PATH, &mpath_head->flags);
+ else
+ clear_bit(MPATH_HEAD_QUEUE_IF_NO_PATH, &mpath_head->flags);
+ mutex_unlock(&mpath_head->lock);
+
+ /*
+ * Ensure that update to MPATH_HEAD_QUEUE_IF_NO_PATH is seen
+ * by its reader.
+ */
+ mpath_synchronize(mpath_head);
+
+ return count;
+}
+EXPORT_SYMBOL_GPL(mpath_delayed_removal_secs_store);
+
void mpath_add_sysfs_link(struct mpath_head *mpath_head)
{
struct device *target;
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 07/29] libmultipath: Add delayed removal support
2026-11-08 20:47 ` [PATCH v9 07/29] libmultipath: Add delayed removal support John Garry
@ 2026-11-08 21:11 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:11 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 6 potential
issues to consider:
- [High] libmultipath: race between mpath_add_device() and
mpath_can_remove_head()
- [High] libmultipath: UAF from unsynchronized remove_work on head
teardown
- [Medium] libmultipath: drv_module ref leak in mpath_can_remove_head()
- [Medium] libmultipath: missing requeue kick in
mpath_delayed_removal_secs_store()
- [Medium] libmultipath: remove_work initialized in
mpath_alloc_head_disk() instead of mpath_head_init()
- [Low] libmultipath: unchecked HZ overflow in
mpath_delayed_removal_secs_store()
--
commit ac55fad5ec89b5f2112b89a2a217ff2d12ec50d8
Author: John Garry <john.garry@linux.dev>
libmultipath: Add delayed removal support
This commit adds delayed head removal support to libmultipath, keeping the
multipath disk present and requeuing I/O for a configurable duration when all
paths are lost.
> diff --git a/lib/multipath.c b/lib/multipath.c
> index fd78218244472..cdf8fbc5105b3 100644
> --- a/lib/multipath.c
> +++ b/lib/multipath.c
> @@ -67,6 +67,9 @@ int mpath_add_device(struct mpath_device *mpath_device, struct gendisk *disk,
> list_add_tail_rcu(&mpath_device->siblings, &mpath_head->dev_list);
> mutex_unlock(&mpath_head->lock);
>
> + if (cancel_delayed_work(&mpath_head->remove_work))
> + module_put(mpath_head->drv_module);
> +
> return 0;
> }
> EXPORT_SYMBOL_GPL(mpath_add_device);
[ ... ]
> @@ -482,6 +494,39 @@ static void mpath_requeue_work(struct work_struct *work)
> }
> }
>
> +bool mpath_can_remove_head(struct mpath_head *mpath_head)
> +{
> + unsigned long delay;
> + bool remove = false;
> +
> + mutex_lock(&mpath_head->lock);
> + /*
> + * Ensure that no one could remove this module while the head
> + * remove work is pending.
> + */
> + if (mpath_head_queue_if_no_path(mpath_head) &&
> + !check_mul_overflow(mpath_head->delayed_removal_secs, HZ, &delay) &&
> + try_module_get(mpath_head->drv_module)) {
> + mod_delayed_work(mpath_wq, &mpath_head->remove_work, delay);
[Severity: Medium]
Does this leak a reference on mpath_head->drv_module if
mpath_head->remove_work is already pending?
try_module_get() increments the module refcount before mod_delayed_work() is
called. When remove_work is already pending, mod_delayed_work() updates the
existing timer in place and returns true, but neither mpath_remove_head_work()
nor cancel_delayed_work() in mpath_add_device() will run more than once to
drop the extra reference, preventing the driver module from unloading.
> + } else {
> + remove = true;
> + }
> +
> + mutex_unlock(&mpath_head->lock);
> + return remove;
> +}
> +EXPORT_SYMBOL_GPL(mpath_can_remove_head);
[Severity: High]
Can a race between mpath_add_device() and mpath_can_remove_head() remove an
active mpath_head while paths are present on dev_list, or cancel remove_work
after dev_list becomes empty?
mpath_delete_device() drops mpath_head->lock before calling
mpath_synchronize() and before the caller invokes mpath_can_remove_head(),
and neither mpath_can_remove_head() nor mpath_remove_head_work() rechecks
list_empty(&mpath_head->dev_list) under mpath_head->lock:
CPU0 CPU1
mpath_delete_device()
mutex_lock(&mpath_head->lock)
list_del_rcu(&dev1->siblings)
last_path = true
mutex_unlock(&mpath_head->lock)
mpath_add_device()
mutex_lock(&mpath_head->lock)
list_add_tail_rcu(&dev2->siblings,
&mpath_head->dev_list)
mutex_unlock(&mpath_head->lock)
cancel_delayed_work(&remove_work)
mpath_synchronize()
mpath_can_remove_head()
mutex_lock(&mpath_head->lock)
remove = true (or queues remove_work)
mutex_unlock(&mpath_head->lock)
--> removes head with dev2 on dev_list
Conversely, because mpath_add_device() calls cancel_delayed_work() after
releasing mpath_head->lock, if dev2 is deleted on another CPU and
mpath_can_remove_head() queues remove_work before CPU1 runs
cancel_delayed_work(), CPU1 cancels the newly armed remove_work even though
dev_list is now empty.
> +
> +static void mpath_remove_head_work(struct work_struct *work)
> +{
> + struct mpath_head *mpath_head = container_of(to_delayed_work(work),
> + struct mpath_head, remove_work);
> + struct module *drv_module = mpath_head->drv_module;
> +
> + mpath_head->mpdt->remove_head(mpath_head);
> + module_put(drv_module);
> +}
[Severity: High]
Can mpath_remove_head_work() cause a use-after-free on mpath_head during head
teardown or path flapping?
Unlike requeue_work and partition_scan_work, neither mpath_put_disk() nor
mpath_head_uninit() cancels or flushes mpath_head->remove_work, and
mpath_add_device() uses non-synchronous cancel_delayed_work():
CPU0 (mpath_wq) CPU1
mpath_remove_head_work()
[work dequeued; PENDING cleared]
mpath_add_device()
cancel_delayed_work(&remove_work)
--> returns false
mpath_delete_device()
mpath_can_remove_head()
--> returns true (or re-arms timer)
remove_head() frees mpath_head
mpath_head->mpdt->remove_head() <-- use-after-free
Similarly, if remove_work is armed and a subsequent path probe fails and tears
down the head before reaching mpath_add_device(), mpath_head can be freed
while remove_work is still pending in the timer wheel.
> void mpath_remove_disk(struct mpath_head *mpath_head)
> {
> if (test_and_clear_bit(MPATH_HEAD_DISK_LIVE, &mpath_head->flags)) {
> @@ -529,6 +574,9 @@ int mpath_alloc_head_disk(struct mpath_head *mpath_head,
> mpath_head->disk->private_data = mpath_head;
> mpath_head->disk->fops = &mpath_ops;
>
> + INIT_DELAYED_WORK(&mpath_head->remove_work, mpath_remove_head_work);
> + mpath_head->delayed_removal_secs = 0;
> +
> set_bit(GD_SUPPRESS_PART_SCAN, &mpath_head->disk->state);
>
> return 0;
[Severity: Medium]
Should remove_work and delayed_removal_secs be initialized in
mpath_head_init() instead of mpath_alloc_head_disk()?
If mpath_alloc_head_disk() is called again on an existing mpath_head after
mpath_put_disk(), resetting delayed_removal_secs to 0 here without clearing
MPATH_HEAD_QUEUE_IF_NO_PATH leaves the flag out of sync with
delayed_removal_secs. mpath_available_path() then continues returning true
when no paths exist, causing mpath_bdev_submit_bio() to requeue I/O
indefinitely even though delayed_removal_secs is 0. Initializing remove_work
in mpath_head_init() alongside requeue_work and partition_scan_work also
ensures remove_work is initialized before mpath_add_device() can touch it.
[ ... ]
> +ssize_t mpath_delayed_removal_secs_store(struct mpath_head *mpath_head,
> + const char *buf, size_t count)
> +{
> + unsigned int sec;
> + ssize_t ret;
> +
> + ret = kstrtouint(buf, 0, &sec);
> + if (ret < 0)
> + return ret;
> +
> + mutex_lock(&mpath_head->lock);
> + mpath_head->delayed_removal_secs = sec;
> + if (sec)
> + set_bit(MPATH_HEAD_QUEUE_IF_NO_PATH, &mpath_head->flags);
> + else
> + clear_bit(MPATH_HEAD_QUEUE_IF_NO_PATH, &mpath_head->flags);
> + mutex_unlock(&mpath_head->lock);
[Severity: Low]
On 32-bit architectures, can storing a large sec value set
MPATH_HEAD_QUEUE_IF_NO_PATH while mpath_can_remove_head() immediately removes
the head?
mpath_delayed_removal_secs_store() accepts any unsigned int and sets
MPATH_HEAD_QUEUE_IF_NO_PATH without checking whether sec * HZ overflows
unsigned long. When sec > ULONG_MAX / HZ on 32-bit systems, I/O is requeued
on path loss, but check_mul_overflow() in mpath_can_remove_head() fails and
falls back to immediate head removal with zero delay.
> +
> + /*
> + * Ensure that update to MPATH_HEAD_QUEUE_IF_NO_PATH is seen
> + * by its reader.
> + */
> + mpath_synchronize(mpath_head);
> +
> + return count;
> +}
[Severity: Medium]
When delayed_removal_secs is set to 0, does this leave previously queued BIOs
stalled on mpath_head->requeue_list?
Clearing MPATH_HEAD_QUEUE_IF_NO_PATH stops new BIOs from being queued when no
paths are available, but without calling
mpath_schedule_requeue_work(mpath_head) after mpath_synchronize(), BIOs that
were already added to requeue_list while MPATH_HEAD_QUEUE_IF_NO_PATH was set
are not kicked to fail.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=7
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 08/29] libmultipath: Add sysfs helpers
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (6 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 07/29] libmultipath: Add delayed removal support John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:02 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 09/29] libmultipath: Add support for block device IOCTL John Garry
` (21 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add helpers for driver sysfs code for the following functionality:
- get/set iopolicy with mpath_iopolicy_store() and mpath_iopolicy_show()
- show device path per NUMA node
- "multipath" attribute group, equivalent to nvme_ns_mpath_attr_group
- device groups attribute array, similar to nvme_ns_attr_groups but not
containing NVMe members.
Signed-off-by: John Garry <john.garry@linux.dev>
---
include/linux/multipath.h | 5 +++
lib/multipath.c | 94 +++++++++++++++++++++++++++++++++++++++
2 files changed, 99 insertions(+)
diff --git a/include/linux/multipath.h b/include/linux/multipath.h
index 66b6ad9c2219..7a3708ff3725 100644
--- a/include/linux/multipath.h
+++ b/include/linux/multipath.h
@@ -7,6 +7,8 @@
#include <linux/srcu.h>
extern const struct block_device_operations mpath_ops;
+extern const struct attribute_group mpath_attr_group;
+extern const struct attribute_group *mpath_device_groups[];
enum mpath_iopolicy_e {
MPATH_IOPOLICY_NUMA,
@@ -118,6 +120,9 @@ int mpath_alloc_head_disk(struct mpath_head *mpath_head,
struct queue_limits *lim, int numa_node);
void mpath_device_set_live(struct mpath_device *mpath_device);
bool mpath_can_remove_head(struct mpath_head *mpath_head);
+ssize_t mpath_numa_nodes_show(struct mpath_device *mpath_device, char *buf);
+ssize_t mpath_iopolicy_show(enum mpath_iopolicy_e *iopolicy, char *buf);
+bool mpath_iopolicy_store(enum mpath_iopolicy_e *iopolicy, const char *buf);
ssize_t mpath_delayed_removal_secs_show(struct mpath_head *mpath_head,
char *buf);
ssize_t mpath_delayed_removal_secs_store(struct mpath_head *mpath_head,
diff --git a/lib/multipath.c b/lib/multipath.c
index cdf8fbc5105b..ae5a73d570b3 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -620,6 +620,100 @@ void mpath_device_set_live(struct mpath_device *mpath_device)
}
EXPORT_SYMBOL_GPL(mpath_device_set_live);
+static struct attribute dummy_attr = {
+ .name = "dummy",
+};
+
+static struct attribute *mpath_attrs[] = {
+ &dummy_attr,
+ NULL
+};
+
+static bool multipath_sysfs_group_visible(struct kobject *kobj)
+{
+ struct device *dev = container_of(kobj, struct device, kobj);
+ struct gendisk *disk = dev_to_disk(dev);
+
+ return is_mpath_disk(disk);
+}
+DEFINE_SIMPLE_SYSFS_GROUP_VISIBLE(multipath_sysfs)
+
+const struct attribute_group mpath_attr_group = {
+ .name = "multipath",
+ .attrs = mpath_attrs,
+ .is_visible = SYSFS_GROUP_VISIBLE(multipath_sysfs),
+};
+EXPORT_SYMBOL_GPL(mpath_attr_group);
+
+const struct attribute_group *mpath_device_groups[] = {
+ &mpath_attr_group,
+ NULL
+};
+EXPORT_SYMBOL_GPL(mpath_device_groups);
+
+ssize_t mpath_iopolicy_show(enum mpath_iopolicy_e *iopolicy, char *buf)
+{
+ return sysfs_emit(buf, "%s\n",
+ mpath_iopolicy_names[READ_ONCE(*iopolicy)]);
+}
+EXPORT_SYMBOL_GPL(mpath_iopolicy_show);
+
+static void mpath_iopolicy_update(enum mpath_iopolicy_e *iopolicy,
+ int new)
+{
+ int old = READ_ONCE(*iopolicy);
+
+ if (old == new)
+ return;
+
+ WRITE_ONCE(*iopolicy, new);
+
+ pr_info("iopolicy changed from %s to %s\n",
+ mpath_iopolicy_names[old],
+ mpath_iopolicy_names[new]);
+}
+
+bool mpath_iopolicy_store(enum mpath_iopolicy_e *iopolicy, const char *buf)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(mpath_iopolicy_names); i++) {
+ if (sysfs_streq(buf, mpath_iopolicy_names[i])) {
+ mpath_iopolicy_update(iopolicy, i);
+ return true;
+ }
+ }
+
+ return false;
+}
+EXPORT_SYMBOL_GPL(mpath_iopolicy_store);
+
+ssize_t mpath_numa_nodes_show(struct mpath_device *mpath_device, char *buf)
+{
+ struct mpath_head *mpath_head = mpath_device->mpath_head;
+ int node, srcu_idx;
+ nodemask_t numa_nodes;
+ struct mpath_device *current_mpath_dev;
+
+ if (mpath_read_iopolicy(mpath_head) != MPATH_IOPOLICY_NUMA)
+ return 0;
+
+ nodes_clear(numa_nodes);
+
+ srcu_idx = srcu_read_lock(&mpath_head->srcu);
+ for_each_node(node) {
+ current_mpath_dev =
+ srcu_dereference(mpath_head->current_path[node],
+ &mpath_head->srcu);
+ if (current_mpath_dev == mpath_device)
+ node_set(node, numa_nodes);
+ }
+ srcu_read_unlock(&mpath_head->srcu, srcu_idx);
+
+ return sysfs_emit(buf, "%*pbl\n", nodemask_pr_args(&numa_nodes));
+}
+EXPORT_SYMBOL_GPL(mpath_numa_nodes_show);
+
ssize_t mpath_delayed_removal_secs_show(struct mpath_head *mpath_head,
char *buf)
{
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 08/29] libmultipath: Add sysfs helpers
2026-11-08 20:47 ` [PATCH v9 08/29] libmultipath: Add sysfs helpers John Garry
@ 2026-11-08 21:02 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:02 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] libmultipath: duplicate parser and ambiguous return in
mpath_iopolicy_store()
--
commit 12aacc0c8ac98ed4ae873a73979622db40d4f52b
Author: John Garry <john.garry@linux.dev>
libmultipath: Add sysfs helpers
This commit adds sysfs helpers and attribute group definitions for showing
and updating the multipath I/O policy and displaying per-NUMA-node device
paths.
> diff --git a/lib/multipath.c b/lib/multipath.c
> index cdf8fbc5105b3..ae5a73d570b33 100644
> --- a/lib/multipath.c
> +++ b/lib/multipath.c
> @@ -620,6 +620,100 @@ void mpath_device_set_live(struct mpath_device *mpath_device)
[ ... ]
> +static void mpath_iopolicy_update(enum mpath_iopolicy_e *iopolicy,
> + int new)
> +{
> + int old = READ_ONCE(*iopolicy);
> +
> + if (old == new)
> + return;
> +
> + WRITE_ONCE(*iopolicy, new);
> +
> + pr_info("iopolicy changed from %s to %s\n",
> + mpath_iopolicy_names[old],
> + mpath_iopolicy_names[new]);
> +}
> +
> +bool mpath_iopolicy_store(enum mpath_iopolicy_e *iopolicy, const char *buf)
> +{
> + int i;
> +
> + for (i = 0; i < ARRAY_SIZE(mpath_iopolicy_names); i++) {
> + if (sysfs_streq(buf, mpath_iopolicy_names[i])) {
> + mpath_iopolicy_update(iopolicy, i);
> + return true;
> + }
> + }
> +
> + return false;
> +}
> +EXPORT_SYMBOL_GPL(mpath_iopolicy_store);
[Severity: Low]
Can mpath_iopolicy_store() reuse mpath_iopolicy_parse(), and how can callers
tell whether the policy actually changed?
lib/multipath.c already defines mpath_iopolicy_parse() using
__sysfs_match_string() to match against mpath_iopolicy_names[]. Also,
because mpath_iopolicy_store() returns a bool that is true for any valid
policy name even when old == new in mpath_iopolicy_update(), a sysfs store
caller cannot both reject invalid strings with -EINVAL and avoid clearing
cached paths and flushing requeue_work when the existing policy is written
again.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=8
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 09/29] libmultipath: Add support for block device IOCTL
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (7 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 08/29] libmultipath: Add sysfs helpers John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:09 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 10/29] libmultipath: Add mpath_bdev_getgeo() John Garry
` (20 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add mpath_bdev_ioctl() as a multipath block device IOCTL handler. This
handler calls into the mpath_device bdev fops handler.
The .compat_ioctl handler is given the standard handler.
Signed-off-by: John Garry <john.garry@linux.dev>
---
lib/multipath.c | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/lib/multipath.c b/lib/multipath.c
index ae5a73d570b3..d59d7ba81087 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -455,11 +455,53 @@ static void mpath_bdev_release(struct gendisk *disk)
mpath_put_head(mpath_head);
}
+static int mpath_bdev_ioctl(struct block_device *bdev, blk_mode_t mode,
+ unsigned int cmd, unsigned long arg)
+{
+ struct gendisk *disk = bdev->bd_disk;
+ struct mpath_head *mpath_head = mpath_gendisk_to_head(disk);
+ struct mpath_device *mpath_device;
+ int srcu_idx, err;
+
+ /*
+ * This check is duplicated from sd_ioctl() as we don't pass the
+ * partition bdev to fops->ioctl. That is not yet possible as the
+ * per-path disk is hidden and, as such, does not have partitions
+ * scanned.
+ */
+ if (bdev_is_partition(bdev) && !capable(CAP_SYS_RAWIO))
+ return -ENOIOCTLCMD;
+
+ srcu_idx = srcu_read_lock(&mpath_head->srcu);
+ mpath_device = mpath_find_path(mpath_head);
+ if (!mpath_device) {
+ err = -EWOULDBLOCK;
+ goto out_unlock;
+ }
+
+ if (!mpath_device->disk->fops->ioctl) {
+ err = -ENOTTY;
+ goto out_unlock;
+ }
+
+ err = mpath_device->disk->fops->ioctl(
+ mpath_device->disk->part0, mode, cmd, arg);
+out_unlock:
+ srcu_read_unlock(&mpath_head->srcu, srcu_idx);
+ return err;
+}
+
const struct block_device_operations mpath_ops = {
.owner = THIS_MODULE,
.open = mpath_bdev_open,
.release = mpath_bdev_release,
.submit_bio = mpath_bdev_submit_bio,
+ .ioctl = mpath_bdev_ioctl,
+ /*
+ * Both NVMe and SCSI use generic blkdev_compat_ptr_ioctl, so would
+ * avoid their custom compat_ioctl implementation.
+ */
+ .compat_ioctl = blkdev_compat_ptr_ioctl,
};
EXPORT_SYMBOL_GPL(mpath_ops);
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 09/29] libmultipath: Add support for block device IOCTL
2026-11-08 20:47 ` [PATCH v9 09/29] libmultipath: Add support for block device IOCTL John Garry
@ 2026-11-08 21:09 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:09 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [High] libmultipath: deadlock in mpath_bdev_ioctl() holding SRCU lock
across ioctl
--
commit df75a2df3968a2931d7f98024b2fce55905a7703
Author: John Garry <john.garry@linux.dev>
libmultipath: Add support for block device IOCTL
This commit adds mpath_bdev_ioctl() and wires up .ioctl and .compat_ioctl
in mpath_ops to forward block device ioctls to an active path's gendisk.
> diff --git a/lib/multipath.c b/lib/multipath.c
> index ae5a73d570b33..d59d7ba81087f 100644
> --- a/lib/multipath.c
> +++ b/lib/multipath.c
> @@ -455,11 +455,53 @@ static void mpath_bdev_release(struct gendisk *disk)
> mpath_put_head(mpath_head);
> }
>
> +static int mpath_bdev_ioctl(struct block_device *bdev, blk_mode_t mode,
> + unsigned int cmd, unsigned long arg)
> +{
> + struct gendisk *disk = bdev->bd_disk;
> + struct mpath_head *mpath_head = mpath_gendisk_to_head(disk);
> + struct mpath_device *mpath_device;
> + int srcu_idx, err;
> +
> + /*
> + * This check is duplicated from sd_ioctl() as we don't pass the
> + * partition bdev to fops->ioctl. That is not yet possible as the
> + * per-path disk is hidden and, as such, does not have partitions
> + * scanned.
> + */
> + if (bdev_is_partition(bdev) && !capable(CAP_SYS_RAWIO))
> + return -ENOIOCTLCMD;
> +
> + srcu_idx = srcu_read_lock(&mpath_head->srcu);
> + mpath_device = mpath_find_path(mpath_head);
> + if (!mpath_device) {
> + err = -EWOULDBLOCK;
> + goto out_unlock;
> + }
> +
> + if (!mpath_device->disk->fops->ioctl) {
> + err = -ENOTTY;
> + goto out_unlock;
> + }
> +
> + err = mpath_device->disk->fops->ioctl(
> + mpath_device->disk->part0, mode, cmd, arg);
> +out_unlock:
> + srcu_read_unlock(&mpath_head->srcu, srcu_idx);
> + return err;
> +}
[Severity: High]
Can holding mpath_head->srcu across fops->ioctl() self-deadlock when the
underlying ioctl synchronously blocks or removes the path?
For example, when fops->ioctl() dispatches to sd_ioctl() -> scsi_ioctl(), a
host reset ioctl (SG_SCSI_RESET) on ib_srp or a host rescan ioctl on
pqi/hpsa can synchronously block or remove the SCSI device in the same
task:
mpath_bdev_ioctl()
srcu_read_lock(&mpath_head->srcu)
sd_ioctl()
scsi_ioctl()
scsi_ioctl_reset()
...
srp_reset_host()
srp_reconnect_rport()
scsi_block_targets()
...
scsi_device_block()
scsi_mpath_dev_clear_path()
mpath_synchronize()
synchronize_srcu(&mpath_head->srcu) <-- deadlocks
Waiting for the mpath_head->srcu grace period while the same task holds
srcu_read_lock(&mpath_head->srcu) hangs the caller in uninterruptible sleep
and blocks subsequent path removals and state transitions on mpath_head.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=9
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 10/29] libmultipath: Add mpath_bdev_getgeo()
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (8 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 09/29] libmultipath: Add support for block device IOCTL John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 11/29] libmultipath: Add mpath_bdev_get_unique_id() John Garry
` (19 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add mpath_bdev_getgeo() as a multipath block device .getgeo handler.
Here we just redirect into the selected mpath_device disk fops->getgeo
handler.
Signed-off-by: John Garry <john.garry@linux.dev>
---
lib/multipath.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/lib/multipath.c b/lib/multipath.c
index d59d7ba81087..3e2dc7d8b2c2 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -491,6 +491,26 @@ static int mpath_bdev_ioctl(struct block_device *bdev, blk_mode_t mode,
return err;
}
+static int mpath_bdev_getgeo(struct gendisk *disk, struct hd_geometry *geo)
+{
+ struct mpath_head *mpath_head = mpath_gendisk_to_head(disk);
+ int srcu_idx, ret = -EWOULDBLOCK;
+ struct mpath_device *mpath_device;
+
+ srcu_idx = srcu_read_lock(&mpath_head->srcu);
+ mpath_device = mpath_find_path(mpath_head);
+ if (mpath_device) {
+ if (mpath_device->disk->fops->getgeo)
+ ret = mpath_device->disk->fops->getgeo(
+ mpath_device->disk, geo);
+ else
+ ret = -ENOTTY; /* See blkdev_getgeo */
+ }
+ srcu_read_unlock(&mpath_head->srcu, srcu_idx);
+
+ return ret;
+}
+
const struct block_device_operations mpath_ops = {
.owner = THIS_MODULE,
.open = mpath_bdev_open,
@@ -502,6 +522,7 @@ const struct block_device_operations mpath_ops = {
* avoid their custom compat_ioctl implementation.
*/
.compat_ioctl = blkdev_compat_ptr_ioctl,
+ .getgeo = mpath_bdev_getgeo,
};
EXPORT_SYMBOL_GPL(mpath_ops);
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 11/29] libmultipath: Add mpath_bdev_get_unique_id()
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (9 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 10/29] libmultipath: Add mpath_bdev_getgeo() John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 12/29] scsi-multipath: introduce basic SCSI device support John Garry
` (18 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add mpath_bdev_get_unique_id() as a multipath block device .get_unique_id
handler.
Signed-off-by: John Garry <john.garry@linux.dev>
---
lib/multipath.c | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/lib/multipath.c b/lib/multipath.c
index 3e2dc7d8b2c2..b03080afb259 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -455,6 +455,27 @@ static void mpath_bdev_release(struct gendisk *disk)
mpath_put_head(mpath_head);
}
+static int mpath_bdev_get_unique_id(struct gendisk *disk, u8 id[16],
+ enum blk_unique_id type)
+{
+ struct mpath_head *mpath_head = mpath_gendisk_to_head(disk);
+ int srcu_idx, ret = -EWOULDBLOCK;
+ struct mpath_device *mpath_device;
+
+ srcu_idx = srcu_read_lock(&mpath_head->srcu);
+ mpath_device = mpath_find_path(mpath_head);
+ if (mpath_device) {
+ if (mpath_device->disk->fops->get_unique_id)
+ ret = mpath_device->disk->fops->get_unique_id(
+ mpath_device->disk, id, type);
+ else
+ ret = 0; /* referencing __dm_get_unique_id() */
+ }
+ srcu_read_unlock(&mpath_head->srcu, srcu_idx);
+
+ return ret;
+}
+
static int mpath_bdev_ioctl(struct block_device *bdev, blk_mode_t mode,
unsigned int cmd, unsigned long arg)
{
@@ -522,6 +543,7 @@ const struct block_device_operations mpath_ops = {
* avoid their custom compat_ioctl implementation.
*/
.compat_ioctl = blkdev_compat_ptr_ioctl,
+ .get_unique_id = mpath_bdev_get_unique_id,
.getgeo = mpath_bdev_getgeo,
};
EXPORT_SYMBOL_GPL(mpath_ops);
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 12/29] scsi-multipath: introduce basic SCSI device support
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (10 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 11/29] libmultipath: Add mpath_bdev_get_unique_id() John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:02 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 13/29] scsi-multipath: introduce scsi_device head structure John Garry
` (17 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
For a scsi_device to support multipath, introduce structure
scsi_mpath_device to hold multipath-specific details.
Like NS structure for NVME, scsi_mpath_device holds the mpath_device
structure to device management and path selection.
A module param are introduced to enable multipath - the following modes
are available:
- on
- off
- always
SCSI multipath will only be available until the following conditions:
- scsi_multipath enabled and implicit ALUA supported and unique ID
available in VPD page 83.
- scsi_multipath always mode and unique ID available in VPD page 83
The scsi_device structure contains a pointer to scsi_mpath_device; having
this pointer set or unset indicates whether multipath is enabled or
disabled for the scsi_device.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/Kconfig | 10 +++
drivers/scsi/Makefile | 1 +
drivers/scsi/scsi.c | 8 +-
drivers/scsi/scsi_multipath.c | 150 ++++++++++++++++++++++++++++++++++
drivers/scsi/scsi_scan.c | 4 +
drivers/scsi/scsi_sysfs.c | 2 +
include/scsi/scsi_device.h | 2 +
include/scsi/scsi_multipath.h | 55 +++++++++++++
8 files changed, 231 insertions(+), 1 deletion(-)
create mode 100644 drivers/scsi/scsi_multipath.c
create mode 100644 include/scsi/scsi_multipath.h
diff --git a/drivers/scsi/Kconfig b/drivers/scsi/Kconfig
index 90b65bebec70..2491fdf6f563 100644
--- a/drivers/scsi/Kconfig
+++ b/drivers/scsi/Kconfig
@@ -76,6 +76,16 @@ config SCSI_LIB_KUNIT_TEST
If unsure say N.
+config SCSI_MULTIPATH
+ bool "SCSI multipath support (EXPERIMENTAL)"
+ depends on SCSI_MOD
+ select LIBMULTIPATH
+ help
+ This option enables support for native SCSI multipath support for
+ SCSI host.
+
+ If unsure say N.
+
comment "SCSI support type (disk, tape, CD-ROM)"
depends on SCSI
diff --git a/drivers/scsi/Makefile b/drivers/scsi/Makefile
index 5b9328dc3b62..989b2455dcc5 100644
--- a/drivers/scsi/Makefile
+++ b/drivers/scsi/Makefile
@@ -171,6 +171,7 @@ scsi_mod-y += scsi_trace.o scsi_logging.o
scsi_mod-$(CONFIG_PM) += scsi_pm.o
scsi_mod-$(CONFIG_SCSI_DH) += scsi_dh.o
scsi_mod-$(CONFIG_BLK_DEV_BSG) += scsi_bsg.o
+scsi_mod-$(CONFIG_SCSI_MULTIPATH) += scsi_multipath.o
hv_storvsc-y := storvsc_drv.o
diff --git a/drivers/scsi/scsi.c b/drivers/scsi/scsi.c
index 05232ab6366f..685297c84755 100644
--- a/drivers/scsi/scsi.c
+++ b/drivers/scsi/scsi.c
@@ -64,6 +64,7 @@
#include <scsi/scsi_driver.h>
#include <scsi/scsi_eh.h>
#include <scsi/scsi_host.h>
+#include <scsi/scsi_multipath.h>
#include <scsi/scsi_tcq.h>
#include "scsi_priv.h"
@@ -1057,12 +1058,16 @@ static int __init init_scsi(void)
error = scsi_sysfs_register();
if (error)
goto cleanup_sysctl;
+ error = scsi_multipath_init();
+ if (error)
+ goto cleanup_sysfs;
scsi_netlink_init();
printk(KERN_NOTICE "SCSI subsystem initialized\n");
return 0;
-
+cleanup_sysfs:
+ scsi_sysfs_unregister();
cleanup_sysctl:
scsi_exit_sysctl();
cleanup_hosts:
@@ -1081,6 +1086,7 @@ static int __init init_scsi(void)
static void __exit exit_scsi(void)
{
scsi_netlink_exit();
+ scsi_multipath_exit();
scsi_sysfs_unregister();
scsi_exit_sysctl();
scsi_exit_hosts();
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
new file mode 100644
index 000000000000..97f7513b4712
--- /dev/null
+++ b/drivers/scsi/scsi_multipath.c
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 Oracle Corp
+ *
+ */
+
+#include <scsi/scsi_cmnd.h>
+#include <scsi/scsi_driver.h>
+#include <scsi/scsi_proto.h>
+#include <scsi/scsi_host.h>
+#include <scsi/scsi_device.h>
+#include <scsi/scsi_multipath.h>
+
+#include "scsi_priv.h"
+
+#define TPGS_MODE_IMPLICIT 0x1
+
+enum {
+ SCSI_MULTIPATH_OFF,
+ SCSI_MULTIPATH_ON,
+ SCSI_MULTIPATH_ALWAYS,
+};
+
+static const char *scsi_multipath_modes[] = {
+ [SCSI_MULTIPATH_OFF] = "off",
+ [SCSI_MULTIPATH_ON] = "on",
+ [SCSI_MULTIPATH_ALWAYS] = "always",
+};
+
+static int scsi_multipath = SCSI_MULTIPATH_OFF;
+
+static int scsi_multipath_param_set(const char *val, const struct kernel_param *kp)
+{
+ int mode;
+
+ if (!val)
+ return -EINVAL;
+ mode = __sysfs_match_string(scsi_multipath_modes,
+ ARRAY_SIZE(scsi_multipath_modes), val);
+
+ if (mode < 0)
+ return mode;
+ scsi_multipath = mode;
+ return 0;
+}
+
+static int scsi_multipath_param_get(char *buf, const struct kernel_param *kp)
+{
+ return sprintf(buf, "%s\n", scsi_multipath_modes[scsi_multipath]);
+}
+
+static const struct kernel_param_ops multipath_param_ops = {
+ .set = scsi_multipath_param_set,
+ .get = scsi_multipath_param_get,
+};
+
+module_param_cb(multipath, &multipath_param_ops, &scsi_multipath, 0444);
+MODULE_PARM_DESC(multipath, "turn on native multipath support, options: on, off, always");
+
+static int scsi_mpath_unique_lun_id(struct scsi_device *sdev)
+{
+ struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
+ int ret;
+
+ ret = scsi_vpd_lun_id(sdev, scsi_mpath_dev->device_id_str,
+ SCSI_MPATH_DEVICE_ID_LEN);
+ if (ret < 0)
+ return ret;
+ else if (ret == 0)
+ return -EINVAL;
+ else if (ret > SCSI_MPATH_DEVICE_ID_LEN)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int scsi_multipath_sdev_init(struct scsi_device *sdev)
+{
+ struct Scsi_Host *shost = sdev->host;
+ struct scsi_mpath_device *scsi_mpath_dev;
+ struct mpath_device *mpath_device;
+
+ scsi_mpath_dev = kzalloc(sizeof(*scsi_mpath_dev), GFP_KERNEL);
+ if (!scsi_mpath_dev)
+ return -ENOMEM;
+ scsi_mpath_dev->sdev = sdev;
+ sdev->scsi_mpath_dev = scsi_mpath_dev;
+
+ mpath_device = &scsi_mpath_dev->mpath_device;
+ mpath_device->numa_node = dev_to_node(shost->dma_dev);
+ mpath_device->access_state = MPATH_STATE_OPTIMIZED;
+
+ return 0;
+}
+
+static void scsi_multipath_sdev_uninit(struct scsi_device *sdev)
+{
+ kfree(sdev->scsi_mpath_dev);
+ sdev->scsi_mpath_dev = NULL;
+}
+
+int scsi_mpath_dev_alloc(struct scsi_device *sdev)
+{
+ int ret;
+
+ if (scsi_multipath == SCSI_MULTIPATH_OFF)
+ return 0;
+
+ if (!(scsi_device_tpgs(sdev) & TPGS_MODE_IMPLICIT) &&
+ (scsi_multipath != SCSI_MULTIPATH_ALWAYS)) {
+ sdev_printk(KERN_DEBUG, sdev, "IMPLICIT TPGS are required for multipath support\n");
+ return 0;
+ }
+
+ ret = scsi_multipath_sdev_init(sdev);
+ if (ret)
+ return ret;
+
+ ret = scsi_mpath_unique_lun_id(sdev);
+ if (ret < 0)
+ goto out_uninit;
+
+ return 0;
+
+out_uninit:
+ scsi_multipath_sdev_uninit(sdev);
+ return 0;
+}
+
+void scsi_mpath_dev_release(struct scsi_device *sdev)
+{
+ struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
+
+ if (!scsi_mpath_dev)
+ return;
+
+ scsi_multipath_sdev_uninit(sdev);
+}
+
+int __init scsi_multipath_init(void)
+{
+ return 0;
+}
+
+void __exit scsi_multipath_exit(void)
+{
+}
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("scsi_multipath");
diff --git a/drivers/scsi/scsi_scan.c b/drivers/scsi/scsi_scan.c
index 8aae12fd38f3..b89bb1442a9e 100644
--- a/drivers/scsi/scsi_scan.c
+++ b/drivers/scsi/scsi_scan.c
@@ -46,6 +46,7 @@
#include <scsi/scsi_transport.h>
#include <scsi/scsi_dh.h>
#include <scsi/scsi_eh.h>
+#include <scsi/scsi_multipath.h>
#include "scsi_priv.h"
#include "scsi_logging.h"
@@ -1129,6 +1130,9 @@ static int scsi_add_lun(struct scsi_device *sdev, unsigned char *inq_result,
sdev->max_queue_depth = sdev->queue_depth;
WARN_ON_ONCE(sdev->max_queue_depth > sdev->budget_map.depth);
+ if (scsi_mpath_dev_alloc(sdev))
+ return SCSI_SCAN_NO_RESPONSE;
+
/*
* Ok, the device is now all set up, we can
* register it and tell the rest of the kernel
diff --git a/drivers/scsi/scsi_sysfs.c b/drivers/scsi/scsi_sysfs.c
index fe54792ed69e..6a68da42df91 100644
--- a/drivers/scsi/scsi_sysfs.c
+++ b/drivers/scsi/scsi_sysfs.c
@@ -23,6 +23,7 @@
#include <scsi/scsi_transport.h>
#include <scsi/scsi_driver.h>
#include <scsi/scsi_devinfo.h>
+#include <scsi/scsi_multipath.h>
#include "scsi_priv.h"
#include "scsi_logging.h"
@@ -456,6 +457,7 @@ static void scsi_device_dev_release(struct device *dev)
might_sleep();
scsi_dh_release_device(sdev);
+ scsi_mpath_dev_release(sdev);
parent = sdev->sdev_gendev.parent;
diff --git a/include/scsi/scsi_device.h b/include/scsi/scsi_device.h
index e6fd6bc30644..1111fb267154 100644
--- a/include/scsi/scsi_device.h
+++ b/include/scsi/scsi_device.h
@@ -280,6 +280,8 @@ struct scsi_device {
struct device sdev_gendev,
sdev_dev;
+ struct scsi_mpath_device *scsi_mpath_dev;
+
struct work_struct requeue_work;
struct scsi_device_handler *handler;
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
new file mode 100644
index 000000000000..d3d410dafd17
--- /dev/null
+++ b/include/scsi/scsi_multipath.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _SCSI_SCSI_MULTIPATH_H
+#define _SCSI_SCSI_MULTIPATH_H
+
+#include <linux/list.h>
+#include <linux/types.h>
+#include <linux/rcupdate.h>
+#include <linux/workqueue.h>
+#include <linux/mutex.h>
+#include <linux/blk-mq.h>
+#include <linux/multipath.h>
+#include <scsi/scsi.h>
+#include <scsi/scsi_cmnd.h>
+#include <scsi/scsi_dbg.h>
+#include <scsi/scsi_device.h>
+#include <scsi/scsi_devinfo.h>
+#include <scsi/scsi_driver.h>
+
+#ifdef CONFIG_SCSI_MULTIPATH
+#define SCSI_MPATH_DEVICE_ID_LEN 256
+
+struct scsi_mpath_device {
+ struct mpath_device mpath_device;
+ struct scsi_device *sdev;
+
+ char device_id_str[SCSI_MPATH_DEVICE_ID_LEN];
+};
+#define to_scsi_mpath_device(d) \
+ container_of(d, struct scsi_mpath_device, mpath_device)
+
+int scsi_mpath_dev_alloc(struct scsi_device *sdev);
+void scsi_mpath_dev_release(struct scsi_device *sdev);
+int scsi_multipath_init(void);
+void scsi_multipath_exit(void);
+#else /* CONFIG_SCSI_MULTIPATH */
+
+struct scsi_mpath_device {
+};
+
+static inline int scsi_mpath_dev_alloc(struct scsi_device *sdev)
+{
+ return 0;
+}
+static inline void scsi_mpath_dev_release(struct scsi_device *sdev)
+{
+}
+static inline int scsi_multipath_init(void)
+{
+ return 0;
+}
+static inline void scsi_multipath_exit(void)
+{
+}
+#endif /* CONFIG_SCSI_MULTIPATH */
+#endif /* _SCSI_SCSI_MULTIPATH_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 12/29] scsi-multipath: introduce basic SCSI device support
2026-11-08 20:47 ` [PATCH v9 12/29] scsi-multipath: introduce basic SCSI device support John Garry
@ 2026-11-08 21:02 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:02 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Low] scsi: CONFIG_SCSI_MULTIPATH depends on SCSI_MOD instead of SCSI
- [Low] scsi: duplicate MODULE_DESCRIPTION and MODULE_LICENSE in
scsi_multipath.c
- [Low] scsi-multipath: off-by-one truncation check in
scsi_mpath_unique_lun_id()
--
commit 006413118882829d81f9e30417cb6f7346390426
Author: John Garry <john.garry@linux.dev>
scsi-multipath: introduce basic SCSI device support
This commit introduces struct scsi_mpath_device, the scsi_mod.multipath
module parameter, and per-device multipath allocation and release hooks
during SCSI LUN scanning and device teardown.
> diff --git a/drivers/scsi/Kconfig b/drivers/scsi/Kconfig
> index 90b65bebec703..2491fdf6f563e 100644
> --- a/drivers/scsi/Kconfig
> +++ b/drivers/scsi/Kconfig
> @@ -76,6 +76,16 @@ config SCSI_LIB_KUNIT_TEST
>
> If unsure say N.
>
> +config SCSI_MULTIPATH
> + bool "SCSI multipath support (EXPERIMENTAL)"
> + depends on SCSI_MOD
> + select LIBMULTIPATH
> + help
> + This option enables support for native SCSI multipath support for
> + SCSI host.
> +
> + If unsure say N.
> +
[Severity: Low]
Should SCSI_MULTIPATH depend on SCSI instead of SCSI_MOD?
SCSI_MOD defaults to y when SCSI=n and BLOCK=y:
drivers/scsi/Kconfig:
config SCSI_MOD
tristate
default y if SCSI=n || SCSI=y
default m if SCSI=m
depends on BLOCK
When CONFIG_SCSI=n and CONFIG_BLOCK=y, CONFIG_SCSI_MULTIPATH=y can be
enabled, which selects CONFIG_LIBMULTIPATH=y while skipping scsi_mod.o and
drivers/scsi/scsi_multipath.o.
[ ... ]
> diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
> new file mode 100644
> index 0000000000000..97f7513b47122
> --- /dev/null
> +++ b/drivers/scsi/scsi_multipath.c
> @@ -0,0 +1,150 @@
[ ... ]
> +static int scsi_mpath_unique_lun_id(struct scsi_device *sdev)
> +{
> + struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
> + int ret;
> +
> + ret = scsi_vpd_lun_id(sdev, scsi_mpath_dev->device_id_str,
> + SCSI_MPATH_DEVICE_ID_LEN);
> + if (ret < 0)
> + return ret;
> + else if (ret == 0)
> + return -EINVAL;
> + else if (ret > SCSI_MPATH_DEVICE_ID_LEN)
> + return -EINVAL;
> +
> + return 0;
> +}
[Severity: Low]
Should the truncation check test ret >= SCSI_MPATH_DEVICE_ID_LEN instead of
ret > SCSI_MPATH_DEVICE_ID_LEN?
In scsi_vpd_lun_id(), a T10 Vendor ID designator with 252 or more bytes is
clamped to id_len - 4 (252) and formatted with snprintf(id, id_len,
"t10.%*pE", ...), which truncates device_id_str at 255 characters plus the
NUL terminator and returns 256 (SCSI_MPATH_DEVICE_ID_LEN). Testing
ret > SCSI_MPATH_DEVICE_ID_LEN accepts that truncated identifier.
[ ... ]
> +MODULE_LICENSE("GPL");
> +MODULE_DESCRIPTION("scsi_multipath");
[Severity: Low]
Does linking scsi_multipath.o into scsi_mod.o emit duplicate MODULE_LICENSE
and conflicting MODULE_DESCRIPTION entries in scsi_mod's .modinfo section?
drivers/scsi/scsi.c already defines MODULE_DESCRIPTION("SCSI core") and
MODULE_LICENSE("GPL") for scsi_mod.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=12
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 13/29] scsi-multipath: introduce scsi_device head structure
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (11 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 12/29] scsi-multipath: introduce basic SCSI device support John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 14/29] scsi-multipath: provide sysfs link from to scsi_device John Garry
` (16 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Introduce a scsi_device head structure - scsi_mpath_head - to manage
multipathing for a scsi_device. This is similar to nvme_ns_head structure.
There is no reference in scsi_mpath_head to any disk, as this would be
managed by the scsi_disk driver.
A list of scsi_mpath_head structures is managed to lookup for matching
multipathed scsi_device's. Matching is done through the scsi_device
unique id.
A new class for multipathed devices is added, scsi_mpath_device_class.
The purpose of this class is for managing the scsi_mpath_head.dev member.
The naming for the scsi_device structure is in form H:C:I:L,
where H is host, C is channel, I is ID, and L is lun.
However, for a multipathed scsi_device, all the naming members may be
different between member scsi_device's. As such, just use a simple
single-number naming index for each scsi_mpath_head.
The sysfs device folder will have links to the scsi_device's so, it will
be possible to lookup the member scsi_device's.
An example sysfs entry is as follows:
# ls -l /sys/class/scsi_mpath_device/scsi_mpath_device0/
total 0
drwxr-xr-x 2 root root 0 Apr 13 15:48 power
lrwxrwxrwx 1 root root 0 Apr 13 15:48 subsystem -> ../../../../class/scsi_mpath_device
-rw-r--r-- 1 root root 4096 Apr 13 15:48 uevent
-r--r--r-- 1 root root 4096 Apr 13 15:48 vpd_id
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_multipath.c | 205 +++++++++++++++++++++++++++++++++-
drivers/scsi/scsi_sysfs.c | 3 +
include/scsi/scsi_multipath.h | 36 ++++++
3 files changed, 242 insertions(+), 2 deletions(-)
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 97f7513b4712..625a72cb73c3 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -29,6 +29,10 @@ static const char *scsi_multipath_modes[] = {
static int scsi_multipath = SCSI_MULTIPATH_OFF;
+static LIST_HEAD(scsi_mpath_heads_list);
+static DEFINE_MUTEX(scsi_mpath_heads_lock);
+static DEFINE_IDA(scsi_multipath_dev_ida);
+
static int scsi_multipath_param_set(const char *val, const struct kernel_param *kp)
{
int mode;
@@ -74,6 +78,55 @@ static int scsi_mpath_unique_lun_id(struct scsi_device *sdev)
return 0;
}
+static void scsi_mpath_head_release(struct device *dev)
+{
+ struct scsi_mpath_head *scsi_mpath_head =
+ container_of(dev, struct scsi_mpath_head, dev);
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+
+ ida_free(&scsi_multipath_dev_ida, scsi_mpath_head->index);
+ ida_destroy(&scsi_mpath_head->ida);
+ mpath_head_uninit(mpath_head);
+ kfree(scsi_mpath_head);
+}
+
+static ssize_t scsi_mpath_device_vpd_id_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct scsi_mpath_head *scsi_mpath_head =
+ container_of(dev, struct scsi_mpath_head, dev);
+
+ return sysfs_emit(buf, "%s\n", scsi_mpath_head->vpd_id);
+}
+static DEVICE_ATTR(vpd_id, S_IRUGO, scsi_mpath_device_vpd_id_show, NULL);
+
+static struct attribute *scsi_mpath_device_attrs[] = {
+ &dev_attr_vpd_id.attr,
+ NULL
+};
+
+static const struct attribute_group scsi_mpath_device_attrs_group = {
+ .attrs = scsi_mpath_device_attrs,
+};
+
+static bool scsi_multipath_sysfs_group_visible(struct kobject *kobj)
+{
+ return true;
+}
+DEFINE_SIMPLE_SYSFS_GROUP_VISIBLE(scsi_multipath_sysfs)
+
+static const struct attribute_group *scsi_mpath_device_groups[] = {
+ &scsi_mpath_device_attrs_group,
+ NULL
+};
+
+static const struct class scsi_mpath_device_class = {
+ .name = "scsi_mpath_device",
+ .dev_groups = scsi_mpath_device_groups,
+ .dev_release = scsi_mpath_head_release,
+};
+
static int scsi_multipath_sdev_init(struct scsi_device *sdev)
{
struct Scsi_Host *shost = sdev->host;
@@ -93,6 +146,74 @@ static int scsi_multipath_sdev_init(struct scsi_device *sdev)
return 0;
}
+static struct mpath_head_template smpdt = {
+};
+
+static struct scsi_mpath_head *scsi_mpath_alloc_head(char *vpd_id)
+{
+ struct scsi_mpath_head *scsi_mpath_head;
+ int ret;
+
+ scsi_mpath_head = kzalloc(sizeof(*scsi_mpath_head), GFP_KERNEL);
+ if (!scsi_mpath_head)
+ return NULL;
+
+ ida_init(&scsi_mpath_head->ida);
+
+ if (mpath_head_init(&scsi_mpath_head->mpath_head))
+ goto out_free;
+ scsi_mpath_head->mpath_head.mpdt = &smpdt;
+
+ strscpy(scsi_mpath_head->vpd_id, vpd_id,
+ SCSI_MPATH_DEVICE_ID_LEN);
+
+ scsi_mpath_head->index = ida_alloc(&scsi_multipath_dev_ida, GFP_KERNEL);
+ if (scsi_mpath_head->index < 0)
+ goto out_uninit_head;
+ kref_init(&scsi_mpath_head->ref);
+
+ device_initialize(&scsi_mpath_head->dev);
+ scsi_mpath_head->dev.class = &scsi_mpath_device_class;
+ ret = dev_set_name(&scsi_mpath_head->dev, "scsi_mpath_device%d",
+ scsi_mpath_head->index);
+ if (ret) {
+ put_device(&scsi_mpath_head->dev);
+ return NULL;
+ }
+
+ ret = device_add(&scsi_mpath_head->dev);
+ if (ret) {
+ put_device(&scsi_mpath_head->dev);
+ return NULL;
+ }
+
+ return scsi_mpath_head;
+
+out_uninit_head:
+ mpath_head_uninit(&scsi_mpath_head->mpath_head);
+out_free:
+ kfree(scsi_mpath_head);
+ return NULL;
+}
+
+static struct scsi_mpath_head *scsi_mpath_find_head(
+ struct scsi_mpath_device *scsi_mpath_dev)
+{
+ struct scsi_mpath_head *scsi_mpath_head;
+
+ list_for_each_entry(scsi_mpath_head, &scsi_mpath_heads_list, entry) {
+ if (strncmp(scsi_mpath_head->vpd_id,
+ scsi_mpath_dev->device_id_str,
+ SCSI_MPATH_DEVICE_ID_LEN) == 0) {
+ if (scsi_mpath_try_get_head(scsi_mpath_head))
+ continue;
+ return scsi_mpath_head;
+ }
+ }
+
+ return NULL;
+}
+
static void scsi_multipath_sdev_uninit(struct scsi_device *sdev)
{
kfree(sdev->scsi_mpath_dev);
@@ -101,6 +222,7 @@ static void scsi_multipath_sdev_uninit(struct scsi_device *sdev)
int scsi_mpath_dev_alloc(struct scsi_device *sdev)
{
+ struct scsi_mpath_head *scsi_mpath_head;
int ret;
if (scsi_multipath == SCSI_MULTIPATH_OFF)
@@ -120,13 +242,51 @@ int scsi_mpath_dev_alloc(struct scsi_device *sdev)
if (ret < 0)
goto out_uninit;
- return 0;
+ mutex_lock(&scsi_mpath_heads_lock);
+ scsi_mpath_head = scsi_mpath_find_head(sdev->scsi_mpath_dev);
+ if (scsi_mpath_head)
+ goto found;
+ scsi_mpath_head =
+ scsi_mpath_alloc_head(sdev->scsi_mpath_dev->device_id_str);
+ if (!scsi_mpath_head) {
+ sdev_printk(KERN_NOTICE, sdev, "could not allocate multipath head, device multipathing disabled\n");
+ mutex_unlock(&scsi_mpath_heads_lock);
+ goto out_uninit;
+ }
+ list_add_tail(&scsi_mpath_head->entry, &scsi_mpath_heads_list);
+found:
+ mutex_unlock(&scsi_mpath_heads_lock);
+ ret = ida_alloc(&scsi_mpath_head->ida, GFP_KERNEL);
+ if (ret < 0)
+ goto out_put_head;
+ sdev->scsi_mpath_dev->index = ret;
+ sdev->scsi_mpath_dev->scsi_mpath_head = scsi_mpath_head;
+ sdev->scsi_mpath_dev->mpath_device.mpath_head =
+ &scsi_mpath_head->mpath_head;
+ return 0;
+out_put_head:
+ scsi_mpath_put_head(scsi_mpath_head);
out_uninit:
scsi_multipath_sdev_uninit(sdev);
return 0;
}
+static void scsi_mpath_remove_head(struct scsi_mpath_device *scsi_mpath_dev)
+{
+ scsi_mpath_put_head(scsi_mpath_dev->scsi_mpath_head);
+ scsi_mpath_dev->scsi_mpath_head = NULL;
+}
+
+void scsi_mpath_remove_device(struct scsi_mpath_device *scsi_mpath_dev)
+{
+ struct scsi_mpath_head *scsi_mpath_head = scsi_mpath_dev->scsi_mpath_head;
+
+ ida_free(&scsi_mpath_head->ida, scsi_mpath_dev->index);
+
+ scsi_mpath_remove_head(scsi_mpath_dev);
+}
+
void scsi_mpath_dev_release(struct scsi_device *sdev)
{
struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
@@ -137,13 +297,54 @@ void scsi_mpath_dev_release(struct scsi_device *sdev)
scsi_multipath_sdev_uninit(sdev);
}
-int __init scsi_multipath_init(void)
+void scsi_mpath_get_head(struct scsi_mpath_head *scsi_mpath_head)
{
+ kref_get(&scsi_mpath_head->ref);
+}
+EXPORT_SYMBOL_GPL(scsi_mpath_get_head);
+
+int scsi_mpath_try_get_head(struct scsi_mpath_head *scsi_mpath_head)
+{
+ if (!kref_get_unless_zero(&scsi_mpath_head->ref))
+ return -ENXIO;
return 0;
}
+EXPORT_SYMBOL_GPL(scsi_mpath_try_get_head);
+
+static void scsi_mpath_free_head(struct kref *ref)
+{
+ struct scsi_mpath_head *scsi_mpath_head =
+ container_of(ref, struct scsi_mpath_head, ref);
+
+ /*
+ * If we race with scsi_mpath_find_head(), then that function may
+ * find this scsi_mpath_head in the heads list; however we would fail
+ * to take a reference to this scsi_mpath_head and continue the search.
+ * As such, it is safe to call device_unregister (and free
+ * scsi_mpath_head) after we delete this head from the list.
+ */
+ mutex_lock(&scsi_mpath_heads_lock);
+ list_del_init(&scsi_mpath_head->entry);
+ mutex_unlock(&scsi_mpath_heads_lock);
+
+ device_unregister(&scsi_mpath_head->dev);
+}
+
+void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head)
+{
+ kref_put(&scsi_mpath_head->ref, scsi_mpath_free_head);
+}
+EXPORT_SYMBOL_GPL(scsi_mpath_put_head);
+
+int __init scsi_multipath_init(void)
+{
+ return class_register(&scsi_mpath_device_class);
+}
void __exit scsi_multipath_exit(void)
{
+ ida_destroy(&scsi_multipath_dev_ida);
+ class_unregister(&scsi_mpath_device_class);
}
MODULE_LICENSE("GPL");
diff --git a/drivers/scsi/scsi_sysfs.c b/drivers/scsi/scsi_sysfs.c
index 6a68da42df91..5b5de5806385 100644
--- a/drivers/scsi/scsi_sysfs.c
+++ b/drivers/scsi/scsi_sysfs.c
@@ -1526,6 +1526,9 @@ void __scsi_remove_device(struct scsi_device *sdev)
*/
scsi_target_reap(scsi_target(sdev));
+ if (sdev->scsi_mpath_dev)
+ scsi_mpath_remove_device(sdev->scsi_mpath_dev);
+
put_device(dev);
}
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
index d3d410dafd17..50d422798d83 100644
--- a/include/scsi/scsi_multipath.h
+++ b/include/scsi/scsi_multipath.h
@@ -19,12 +19,25 @@
#ifdef CONFIG_SCSI_MULTIPATH
#define SCSI_MPATH_DEVICE_ID_LEN 256
+struct scsi_mpath_head {
+ struct mpath_head mpath_head;
+ char vpd_id[SCSI_MPATH_DEVICE_ID_LEN];
+ struct list_head entry;
+ struct ida ida;
+ struct kref ref;
+ struct device dev;
+ int index;
+};
+
struct scsi_mpath_device {
struct mpath_device mpath_device;
struct scsi_device *sdev;
+ int index;
+ struct scsi_mpath_head *scsi_mpath_head;
char device_id_str[SCSI_MPATH_DEVICE_ID_LEN];
};
+
#define to_scsi_mpath_device(d) \
container_of(d, struct scsi_mpath_device, mpath_device)
@@ -32,8 +45,14 @@ int scsi_mpath_dev_alloc(struct scsi_device *sdev);
void scsi_mpath_dev_release(struct scsi_device *sdev);
int scsi_multipath_init(void);
void scsi_multipath_exit(void);
+void scsi_mpath_remove_device(struct scsi_mpath_device *scsi_mpath_dev);
+void scsi_mpath_get_head(struct scsi_mpath_head *scsi_mpath_head);
+int scsi_mpath_try_get_head(struct scsi_mpath_head *scsi_mpath_head);
+void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head);
#else /* CONFIG_SCSI_MULTIPATH */
+struct scsi_mpath_head {
+};
struct scsi_mpath_device {
};
@@ -51,5 +70,22 @@ static inline int scsi_multipath_init(void)
static inline void scsi_multipath_exit(void)
{
}
+static inline
+void scsi_mpath_remove_device(struct scsi_mpath_device *scsi_mpath_dev)
+{
+}
+static inline
+void scsi_mpath_get_head(struct scsi_mpath_head *scsi_mpath_head)
+{
+}
+static inline
+int scsi_mpath_try_get_head(struct scsi_mpath_head *scsi_mpath_head)
+{
+ return 0;
+}
+static inline
+void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head)
+{
+}
#endif /* CONFIG_SCSI_MULTIPATH */
#endif /* _SCSI_SCSI_MULTIPATH_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 14/29] scsi-multipath: provide sysfs link from to scsi_device
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (12 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 13/29] scsi-multipath: introduce scsi_device head structure John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 15/29] scsi-multipath: support iopolicy John Garry
` (15 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry, Hannes Reinecke
Provide a link in sysfs from a scsi_mpath_device to member scsi_device's.
An example is as follows:
# ls -l /sys/class/scsi_mpath_device/scsi_mpath_device0/multipath/
total 0
lrwxrwxrwx 1 root root 0 Feb 24 12:01 8:0:0:0 -> ../../../../platform/host8/session1/target8:0:0/8:0:0:0
lrwxrwxrwx 1 root root 0 Feb 24 12:01 9:0:0:0 -> ../../../../platform/host9/session2/target9:0:0/9:0:0:0
Reviewed-by: Hannes Reinecke <hare@suse.de>
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_multipath.c | 43 +++++++++++++++++++++++++++++++++++
drivers/scsi/scsi_sysfs.c | 5 ++++
include/scsi/scsi_multipath.h | 8 +++++++
3 files changed, 56 insertions(+)
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 625a72cb73c3..092975e0aca1 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -116,11 +116,54 @@ static bool scsi_multipath_sysfs_group_visible(struct kobject *kobj)
}
DEFINE_SIMPLE_SYSFS_GROUP_VISIBLE(scsi_multipath_sysfs)
+static struct attribute dummy_attr = {
+ .name = "dummy",
+};
+
+static struct attribute *scsi_mpath_attrs[] = {
+ &dummy_attr,
+ NULL
+};
+
+static const struct attribute_group scsi_mpath_attr_group = {
+ .name = "multipath",
+ .attrs = scsi_mpath_attrs,
+ .is_visible = SYSFS_GROUP_VISIBLE(scsi_multipath_sysfs),
+};
+
static const struct attribute_group *scsi_mpath_device_groups[] = {
&scsi_mpath_device_attrs_group,
+ &scsi_mpath_attr_group,
NULL
};
+void scsi_mpath_add_sysfs_link(struct scsi_device *sdev)
+{
+ struct device *target = &sdev->sdev_gendev;
+ struct scsi_mpath_head *scsi_mpath_head =
+ sdev->scsi_mpath_dev->scsi_mpath_head;
+ struct device *source = &scsi_mpath_head->dev;
+ int error;
+
+ error = sysfs_add_link_to_group(&source->kobj, "multipath",
+ &target->kobj, dev_name(target));
+ if (error) {
+ sdev_printk(KERN_INFO, sdev, "Failed to create mpath sysfs link, error=%d\n",
+ error);
+ }
+}
+
+void scsi_mpath_remove_sysfs_link(struct scsi_device *sdev)
+{
+ struct device *target = &sdev->sdev_gendev;
+ struct scsi_mpath_head *scsi_mpath_head =
+ sdev->scsi_mpath_dev->scsi_mpath_head;
+ struct device *source = &scsi_mpath_head->dev;
+
+ sysfs_remove_link_from_group(&source->kobj, "multipath",
+ dev_name(target));
+}
+
static const struct class scsi_mpath_device_class = {
.name = "scsi_mpath_device",
.dev_groups = scsi_mpath_device_groups,
diff --git a/drivers/scsi/scsi_sysfs.c b/drivers/scsi/scsi_sysfs.c
index 5b5de5806385..b5d12b694396 100644
--- a/drivers/scsi/scsi_sysfs.c
+++ b/drivers/scsi/scsi_sysfs.c
@@ -1444,6 +1444,9 @@ int scsi_sysfs_add_sdev(struct scsi_device *sdev)
transport_add_device(&sdev->sdev_gendev);
sdev->is_visible = 1;
+ if (sdev->scsi_mpath_dev)
+ scsi_mpath_add_sysfs_link(sdev);
+
if (IS_ENABLED(CONFIG_BLK_DEV_BSG)) {
sdev->bsg_dev = scsi_bsg_register_queue(sdev);
if (IS_ERR(sdev->bsg_dev)) {
@@ -1496,6 +1499,8 @@ void __scsi_remove_device(struct scsi_device *sdev)
if (IS_ENABLED(CONFIG_BLK_DEV_BSG) && sdev->bsg_dev)
bsg_unregister_queue(sdev->bsg_dev);
+ if (sdev->scsi_mpath_dev)
+ scsi_mpath_remove_sysfs_link(sdev);
device_unregister(&sdev->sdev_dev);
transport_remove_device(dev);
device_del(dev);
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
index 50d422798d83..d224a15ce9c2 100644
--- a/include/scsi/scsi_multipath.h
+++ b/include/scsi/scsi_multipath.h
@@ -49,6 +49,8 @@ void scsi_mpath_remove_device(struct scsi_mpath_device *scsi_mpath_dev);
void scsi_mpath_get_head(struct scsi_mpath_head *scsi_mpath_head);
int scsi_mpath_try_get_head(struct scsi_mpath_head *scsi_mpath_head);
void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head);
+void scsi_mpath_add_sysfs_link(struct scsi_device *sdev);
+void scsi_mpath_remove_sysfs_link(struct scsi_device *sdev);
#else /* CONFIG_SCSI_MULTIPATH */
struct scsi_mpath_head {
@@ -87,5 +89,11 @@ static inline
void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head)
{
}
+static inline void scsi_mpath_add_sysfs_link(struct scsi_device *sdev)
+{
+}
+static inline void scsi_mpath_remove_sysfs_link(struct scsi_device *sdev)
+{
+}
#endif /* CONFIG_SCSI_MULTIPATH */
#endif /* _SCSI_SCSI_MULTIPATH_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 15/29] scsi-multipath: support iopolicy
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (13 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 14/29] scsi-multipath: provide sysfs link from to scsi_device John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 16/29] scsi-multipath: clone each bio John Garry
` (14 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add support to set the multipath iopolicy.
The iopolicy member is per scsi_mpath_head structure.
A module param is added so that the default iopolicy may be set.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_multipath.c | 52 +++++++++++++++++++++++++++++++++++
include/scsi/scsi_multipath.h | 3 ++
2 files changed, 55 insertions(+)
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 092975e0aca1..baf4ca73f8c9 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -61,6 +61,23 @@ static const struct kernel_param_ops multipath_param_ops = {
module_param_cb(multipath, &multipath_param_ops, &scsi_multipath, 0444);
MODULE_PARM_DESC(multipath, "turn on native multipath support, options: on, off, always");
+static enum mpath_iopolicy_e iopolicy = MPATH_IOPOLICY_NUMA;
+
+static int scsi_mpath_set_iopolicy_param(const char *val, const struct kernel_param *kp)
+{
+ return mpath_set_iopolicy(val, &iopolicy);
+}
+
+static int scsi_mpath_get_iopolicy_param(char *buf, const struct kernel_param *kp)
+{
+ return mpath_get_iopolicy(buf, iopolicy);
+}
+
+module_param_call(multipath_iopolicy, scsi_mpath_set_iopolicy_param,
+ scsi_mpath_get_iopolicy_param, &iopolicy, 0644);
+MODULE_PARM_DESC(multipath_iopolicy,
+ "Default multipath I/O policy; 'numa' (default), 'round-robin' or 'queue-depth'");
+
static int scsi_mpath_unique_lun_id(struct scsi_device *sdev)
{
struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
@@ -101,8 +118,41 @@ static ssize_t scsi_mpath_device_vpd_id_show(struct device *dev,
}
static DEVICE_ATTR(vpd_id, S_IRUGO, scsi_mpath_device_vpd_id_show, NULL);
+static ssize_t scsi_mpath_device_iopolicy_store(struct device *dev,
+ struct device_attribute *attr, const char *buf, size_t count)
+{
+ struct scsi_mpath_head *scsi_mpath_head =
+ container_of(dev, struct scsi_mpath_head, dev);
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+
+ if (!mpath_iopolicy_store(&scsi_mpath_head->iopolicy, buf))
+ return -EINVAL;
+
+ mpath_clear_paths(mpath_head);
+ mpath_schedule_requeue_work(mpath_head);
+ /*
+ * Ensure requeue work completes, as this work could run later when
+ * the mpath_head is gone.
+ */
+ flush_work(&mpath_head->requeue_work);
+ return count;
+}
+
+static ssize_t scsi_mpath_device_iopolicy_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct scsi_mpath_head *scsi_mpath_head =
+ container_of(dev, struct scsi_mpath_head, dev);
+
+ return mpath_iopolicy_show(&scsi_mpath_head->iopolicy, buf);
+}
+
+static DEVICE_ATTR(iopolicy, S_IRUGO | S_IWUSR,
+ scsi_mpath_device_iopolicy_show, scsi_mpath_device_iopolicy_store);
+
static struct attribute *scsi_mpath_device_attrs[] = {
&dev_attr_vpd_id.attr,
+ &dev_attr_iopolicy.attr,
NULL
};
@@ -206,6 +256,8 @@ static struct scsi_mpath_head *scsi_mpath_alloc_head(char *vpd_id)
if (mpath_head_init(&scsi_mpath_head->mpath_head))
goto out_free;
scsi_mpath_head->mpath_head.mpdt = &smpdt;
+ scsi_mpath_head->iopolicy = iopolicy;
+ scsi_mpath_head->mpath_head.iopolicy = &scsi_mpath_head->iopolicy;
strscpy(scsi_mpath_head->vpd_id, vpd_id,
SCSI_MPATH_DEVICE_ID_LEN);
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
index d224a15ce9c2..d6f290b84f7f 100644
--- a/include/scsi/scsi_multipath.h
+++ b/include/scsi/scsi_multipath.h
@@ -25,6 +25,7 @@ struct scsi_mpath_head {
struct list_head entry;
struct ida ida;
struct kref ref;
+ enum mpath_iopolicy_e iopolicy;
struct device dev;
int index;
};
@@ -40,6 +41,8 @@ struct scsi_mpath_device {
#define to_scsi_mpath_device(d) \
container_of(d, struct scsi_mpath_device, mpath_device)
+#define to_scsi_mpath_head(d) \
+ container_of(d, struct scsi_mpath_head, mpath_head)
int scsi_mpath_dev_alloc(struct scsi_device *sdev);
void scsi_mpath_dev_release(struct scsi_device *sdev);
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 16/29] scsi-multipath: clone each bio
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (14 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 15/29] scsi-multipath: support iopolicy John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 17/29] scsi-multipath: clear path when device is blocked John Garry
` (13 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
For failover handling, we will take the approach to resubmit each
bio.
However, unlike NVMe, for SCSI there is no guarantee that any bio submitted
is either all or none completed.
As such, for SCSI, for failover handling we will take the approach to
just re-submit the original bio. For this, clone and submit each bio.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_multipath.c | 33 +++++++++++++++++++++++++++++++++
include/scsi/scsi_multipath.h | 1 +
2 files changed, 34 insertions(+)
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index baf4ca73f8c9..8e229357e62c 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -101,6 +101,7 @@ static void scsi_mpath_head_release(struct device *dev)
container_of(dev, struct scsi_mpath_head, dev);
struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+ bioset_exit(&scsi_mpath_head->bio_pool);
ida_free(&scsi_multipath_dev_ida, scsi_mpath_head->index);
ida_destroy(&scsi_mpath_head->ida);
mpath_head_uninit(mpath_head);
@@ -239,7 +240,34 @@ static int scsi_multipath_sdev_init(struct scsi_device *sdev)
return 0;
}
+static void scsi_mpath_clone_end_io(struct bio *clone)
+{
+ struct bio *master_bio = clone->bi_private;
+
+ master_bio->bi_status = clone->bi_status;
+ bio_put(clone);
+ bio_endio(master_bio);
+}
+
+static struct bio *scsi_mpath_clone_bio(struct bio *bio)
+{
+ struct mpath_head *mpath_head = bio->bi_bdev->bd_disk->private_data;
+ struct scsi_mpath_head *scsi_mpath_head = to_scsi_mpath_head(mpath_head);
+ struct bio *clone;
+
+ clone = bio_alloc_clone(bio->bi_bdev, bio, GFP_NOIO,
+ &scsi_mpath_head->bio_pool);
+ if (!clone)
+ return NULL;
+
+ clone->bi_end_io = scsi_mpath_clone_end_io;
+ clone->bi_private = bio;
+
+ return clone;
+}
+
static struct mpath_head_template smpdt = {
+ .clone_bio = scsi_mpath_clone_bio,
};
static struct scsi_mpath_head *scsi_mpath_alloc_head(char *vpd_id)
@@ -265,6 +293,9 @@ static struct scsi_mpath_head *scsi_mpath_alloc_head(char *vpd_id)
scsi_mpath_head->index = ida_alloc(&scsi_multipath_dev_ida, GFP_KERNEL);
if (scsi_mpath_head->index < 0)
goto out_uninit_head;
+ if (bioset_init(&scsi_mpath_head->bio_pool, BIO_POOL_SIZE,
+ 0, BIOSET_PERCPU_CACHE))
+ goto out_ida_free;
kref_init(&scsi_mpath_head->ref);
device_initialize(&scsi_mpath_head->dev);
@@ -284,6 +315,8 @@ static struct scsi_mpath_head *scsi_mpath_alloc_head(char *vpd_id)
return scsi_mpath_head;
+out_ida_free:
+ ida_free(&scsi_multipath_dev_ida, scsi_mpath_head->index);
out_uninit_head:
mpath_head_uninit(&scsi_mpath_head->mpath_head);
out_free:
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
index d6f290b84f7f..d2ef94ebd29a 100644
--- a/include/scsi/scsi_multipath.h
+++ b/include/scsi/scsi_multipath.h
@@ -26,6 +26,7 @@ struct scsi_mpath_head {
struct ida ida;
struct kref ref;
enum mpath_iopolicy_e iopolicy;
+ struct bio_set bio_pool;
struct device dev;
int index;
};
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 17/29] scsi-multipath: clear path when device is blocked
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (15 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 16/29] scsi-multipath: clone each bio John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:06 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 18/29] scsi-multipath: revalidate paths upon device unblock John Garry
` (12 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add scsi_mpath_dev_clear_path() to clear a device path when it becomes
blocked.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_lib.c | 3 +++
drivers/scsi/scsi_multipath.c | 10 ++++++++++
include/scsi/scsi_multipath.h | 5 +++++
3 files changed, 18 insertions(+)
diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
index 36bc995e4e25..1950034f580f 100644
--- a/drivers/scsi/scsi_lib.c
+++ b/drivers/scsi/scsi_lib.c
@@ -35,6 +35,7 @@
#include <scsi/scsi_host.h>
#include <scsi/scsi_transport.h> /* scsi_init_limits() */
#include <scsi/scsi_dh.h>
+#include <scsi/scsi_multipath.h>
#include <trace/events/scsi.h>
@@ -3037,6 +3038,8 @@ static void scsi_device_block(struct scsi_device *sdev, void *data)
scsi_stop_queue(sdev);
mutex_unlock(&sdev->state_mutex);
+ if ((err == 0) && sdev->scsi_mpath_dev)
+ scsi_mpath_dev_clear_path(sdev->scsi_mpath_dev);
WARN_ONCE(err, "%s: failed to block %s in state %d\n",
__func__, dev_name(&sdev->sdev_gendev), state);
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 8e229357e62c..67e9010d5478 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -240,6 +240,16 @@ static int scsi_multipath_sdev_init(struct scsi_device *sdev)
return 0;
}
+void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev)
+{
+ struct mpath_device *mpath_device = &scsi_mpath_dev->mpath_device;
+ struct scsi_mpath_head *scsi_mpath_head = scsi_mpath_dev->scsi_mpath_head;
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+
+ if (mpath_clear_current_path(mpath_device))
+ mpath_synchronize(mpath_head);
+}
+
static void scsi_mpath_clone_end_io(struct bio *clone)
{
struct bio *master_bio = clone->bi_private;
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
index d2ef94ebd29a..2064a3e92274 100644
--- a/include/scsi/scsi_multipath.h
+++ b/include/scsi/scsi_multipath.h
@@ -55,6 +55,7 @@ int scsi_mpath_try_get_head(struct scsi_mpath_head *scsi_mpath_head);
void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head);
void scsi_mpath_add_sysfs_link(struct scsi_device *sdev);
void scsi_mpath_remove_sysfs_link(struct scsi_device *sdev);
+void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev);
#else /* CONFIG_SCSI_MULTIPATH */
struct scsi_mpath_head {
@@ -99,5 +100,9 @@ static inline void scsi_mpath_add_sysfs_link(struct scsi_device *sdev)
static inline void scsi_mpath_remove_sysfs_link(struct scsi_device *sdev)
{
}
+static inline
+void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev)
+{
+}
#endif /* CONFIG_SCSI_MULTIPATH */
#endif /* _SCSI_SCSI_MULTIPATH_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 17/29] scsi-multipath: clear path when device is blocked
2026-11-08 20:47 ` [PATCH v9 17/29] scsi-multipath: clear path when device is blocked John Garry
@ 2026-11-08 21:06 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:06 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [High] scsi: multipath: UAF and NULL deref in scsi_device_block() on
removal race
- [High] scsi: multipath: NULL deref and UAF in scsi_device_block()
during LUN scan
--
commit 6f23ec119648804e57ce4572c175483a5109ea7a
Author: John Garry <john.garry@linux.dev>
scsi-multipath: clear path when device is blocked
This commit introduces scsi_mpath_dev_clear_path() and calls it from
scsi_device_block() to clear the active multipath device path when a SCSI
device is blocked.
> diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
> index 36bc995e4e250..1950034f580f4 100644
> --- a/drivers/scsi/scsi_lib.c
> +++ b/drivers/scsi/scsi_lib.c
[ ... ]
> @@ -3037,6 +3038,8 @@ static void scsi_device_block(struct scsi_device *sdev, void *data)
> scsi_stop_queue(sdev);
>
> mutex_unlock(&sdev->state_mutex);
> + if ((err == 0) && sdev->scsi_mpath_dev)
> + scsi_mpath_dev_clear_path(sdev->scsi_mpath_dev);
>
> WARN_ONCE(err, "%s: failed to block %s in state %d\n",
> __func__, dev_name(&sdev->sdev_gendev), state);
[Severity: High]
Can calling scsi_mpath_dev_clear_path() after dropping sdev->state_mutex
race with __scsi_remove_device() and cause a use-after-free or NULL
pointer dereference on the multipath head?
scsi_block_targets() iterates target devices via starget_for_each_device()
without holding shost->scan_mutex. Once scsi_device_block() unlocks
sdev->state_mutex, __scsi_remove_device() can transition sdev->sdev_state
from SDEV_BLOCK to SDEV_DEL and tear down the multipath head while
starget_for_each_device() still holds a reference to sdev:
CPU0 CPU1
scsi_device_block()
mutex_lock(&sdev->state_mutex)
__scsi_internal_device_block_nowait()
mutex_unlock(&sdev->state_mutex)
__scsi_remove_device()
mutex_lock(&sdev->state_mutex)
sdev->sdev_state = SDEV_DEL
mutex_unlock(&sdev->state_mutex)
...
scsi_mpath_remove_device()
scsi_mpath_remove_head()
scsi_mpath_put_head()
scsi_mpath_dev->scsi_mpath_head = NULL
scsi_mpath_dev_clear_path()
mpath_clear_current_path() <-- use-after-free on mpath_head
mpath_synchronize(mpath_head) <-- NULL deref or use-after-free
Because scsi_mpath_remove_head() drops the reference on scsi_mpath_head
and sets scsi_mpath_dev->scsi_mpath_head to NULL while leaving
mpath_device.mpath_head dangling, mpath_clear_current_path() reads the
freed mpath_device->mpath_head and mpath_synchronize() dereferences a NULL
or freed mpath_head.
> diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
> index 8e229357e62c5..67e9010d54782 100644
> --- a/drivers/scsi/scsi_multipath.c
> +++ b/drivers/scsi/scsi_multipath.c
> @@ -240,6 +240,16 @@ static int scsi_multipath_sdev_init(struct scsi_device *sdev)
> return 0;
> }
>
> +void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev)
> +{
> + struct mpath_device *mpath_device = &scsi_mpath_dev->mpath_device;
> + struct scsi_mpath_head *scsi_mpath_head = scsi_mpath_dev->scsi_mpath_head;
> + struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
> +
> + if (mpath_clear_current_path(mpath_device))
> + mpath_synchronize(mpath_head);
> +}
> +
> static void scsi_mpath_clone_end_io(struct bio *clone)
> {
> struct bio *master_bio = clone->bi_private;
[Severity: High]
What prevents scsi_device_block() from calling scsi_mpath_dev_clear_path()
while scsi_mpath_dev_alloc() is still initializing or freeing
sdev->scsi_mpath_dev during LUN scanning?
sdev is already linked onto starget->devices and set to SDEV_RUNNING in
scsi_add_lun() before scsi_mpath_dev_alloc() runs. Inside
scsi_mpath_dev_alloc(), scsi_multipath_sdev_init() publishes
sdev->scsi_mpath_dev before scsi_mpath_head and mpath_device.mpath_head
are populated, and the error path in scsi_multipath_sdev_uninit() frees
sdev->scsi_mpath_dev before clearing the pointer:
CPU0 CPU1
scsi_add_lun()
scsi_mpath_dev_alloc()
scsi_multipath_sdev_init()
sdev->scsi_mpath_dev = ...
scsi_block_targets()
...
scsi_device_block()
scsi_mpath_dev_clear_path()
mpath_clear_current_path()
mpath_head->current_path[node]
<-- NULL pointer dereference
...
sdev->scsi_mpath_dev->mpath_device.mpath_head = ...
If scsi_mpath_dev_alloc() fails (for example when a LUN has no VPD page 83
device ID) and calls scsi_multipath_sdev_uninit(), a concurrent
scsi_device_block() also risks a use-after-free on sdev->scsi_mpath_dev.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=17
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 18/29] scsi-multipath: revalidate paths upon device unblock
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (16 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 17/29] scsi-multipath: clear path when device is blocked John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:08 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 19/29] scsi-multipath: failover handling John Garry
` (11 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
When a scsi_device is unblocked, we need revalidate paths which means that
we clear paths and requeue any queued bios.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_lib.c | 2 ++
drivers/scsi/scsi_multipath.c | 8 ++++++++
include/scsi/scsi_multipath.h | 5 +++++
3 files changed, 15 insertions(+)
diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
index 1950034f580f..4dbd01484b4a 100644
--- a/drivers/scsi/scsi_lib.c
+++ b/drivers/scsi/scsi_lib.c
@@ -3121,6 +3121,8 @@ static int scsi_internal_device_unblock(struct scsi_device *sdev,
mutex_lock(&sdev->state_mutex);
ret = scsi_internal_device_unblock_nowait(sdev, new_state);
mutex_unlock(&sdev->state_mutex);
+ if ((ret == 0) && sdev->scsi_mpath_dev)
+ scsi_mpath_revalidate_paths(sdev->scsi_mpath_dev);
return ret;
}
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 67e9010d5478..008bcafe73ad 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -240,6 +240,14 @@ static int scsi_multipath_sdev_init(struct scsi_device *sdev)
return 0;
}
+void scsi_mpath_revalidate_paths(struct scsi_mpath_device *scsi_mpath_dev)
+{
+ struct scsi_mpath_head *scsi_mpath_head = scsi_mpath_dev->scsi_mpath_head;
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+
+ mpath_revalidate_paths(mpath_head);
+}
+
void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev)
{
struct mpath_device *mpath_device = &scsi_mpath_dev->mpath_device;
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
index 2064a3e92274..6e50da94fc5d 100644
--- a/include/scsi/scsi_multipath.h
+++ b/include/scsi/scsi_multipath.h
@@ -56,6 +56,7 @@ void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head);
void scsi_mpath_add_sysfs_link(struct scsi_device *sdev);
void scsi_mpath_remove_sysfs_link(struct scsi_device *sdev);
void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev);
+void scsi_mpath_revalidate_paths(struct scsi_mpath_device *scsi_mpath_dev);
#else /* CONFIG_SCSI_MULTIPATH */
struct scsi_mpath_head {
@@ -104,5 +105,9 @@ static inline
void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev)
{
}
+static inline
+void scsi_mpath_revalidate_paths(struct scsi_mpath_device *scsi_mpath_dev)
+{
+}
#endif /* CONFIG_SCSI_MULTIPATH */
#endif /* _SCSI_SCSI_MULTIPATH_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 18/29] scsi-multipath: revalidate paths upon device unblock
2026-11-08 20:47 ` [PATCH v9 18/29] scsi-multipath: revalidate paths upon device unblock John Garry
@ 2026-11-08 21:08 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:08 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [High] scsi: multipath: race in scsi_internal_device_unblock() on
scsi_mpath_head
- [High] scsi: multipath: use-after-free in
scsi_mpath_revalidate_paths() requeue_work
- [Medium] scsi: multipath: race in scsi_device_block() accessing
scsi_mpath_head
- [Medium] scsi: multipath: premature scsi_mpath_dev publish in
scsi_mpath_dev_alloc()
--
commit ed012aa632cd8957f54296618fde6fa312555b38
Author: John Garry <john.garry@linux.dev>
scsi-multipath: revalidate paths upon device unblock
This commit adds scsi_mpath_revalidate_paths() and calls it from
scsi_internal_device_unblock() to clear paths and requeue bios when a
multipath SCSI device is unblocked.
> diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
> index 1950034f580f4..4dbd01484b4a5 100644
> --- a/drivers/scsi/scsi_lib.c
> +++ b/drivers/scsi/scsi_lib.c
> @@ -3121,6 +3121,8 @@ static int scsi_internal_device_unblock(struct scsi_device *sdev,
> mutex_lock(&sdev->state_mutex);
> ret = scsi_internal_device_unblock_nowait(sdev, new_state);
> mutex_unlock(&sdev->state_mutex);
> + if ((ret == 0) && sdev->scsi_mpath_dev)
> + scsi_mpath_revalidate_paths(sdev->scsi_mpath_dev);
>
> return ret;
> }
[Severity: High]
Can scsi_internal_device_unblock() race with __scsi_remove_device() and
dereference a NULL or freed scsi_mpath_head?
Callers such as scsi_target_unblock() and scsi_host_unblock() iterate
devices with shost_for_each_device(), which pins sdev->sdev_gendev (deferring
scsi_device_dev_release() where sdev->scsi_mpath_dev is freed) without
holding shost->scan_mutex. Because sdev->state_mutex is dropped before
calling scsi_mpath_revalidate_paths() (and
scsi_internal_device_unblock_nowait() also returns 0 in SDEV_CANCEL),
__scsi_remove_device() can tear down scsi_mpath_dev->scsi_mpath_head
concurrently:
CPU0 CPU1
scsi_target_unblock()
scsi_internal_device_unblock()
mutex_lock(&sdev->state_mutex)
scsi_internal_device_unblock_nowait()
mutex_unlock(&sdev->state_mutex)
__scsi_remove_device()
...
scsi_mpath_remove_device()
scsi_mpath_remove_head()
scsi_mpath_put_head()
scsi_mpath_dev->scsi_mpath_head = NULL
scsi_mpath_revalidate_paths()
mpath_revalidate_paths(mpath_head) <-- NULL deref or use-after-free
In scsi_mpath_revalidate_paths(), &scsi_mpath_head->mpath_head is at offset
0, so mpath_clear_paths() and mpath_schedule_requeue_work() dereference a
NULL or freed mpath_head.
[Severity: Medium]
Can scsi_internal_device_unblock() also observe a partially initialized or
freed sdev->scsi_mpath_dev during LUN scanning?
In scsi_add_lun(), sdev is already on shost->__devices and starget->devices
and set to SDEV_RUNNING or SDEV_BLOCK before scsi_mpath_dev_alloc() runs:
CPU0 CPU1
scsi_add_lun()
scsi_mpath_dev_alloc()
scsi_multipath_sdev_init()
sdev->scsi_mpath_dev = scsi_mpath_dev
scsi_target_unblock()
scsi_internal_device_unblock()
if (ret == 0 && sdev->scsi_mpath_dev)
scsi_mpath_revalidate_paths()
mpath_revalidate_paths(NULL)
<-- NULL pointer dereference
scsi_mpath_unique_lun_id()
...
sdev->scsi_mpath_dev->scsi_mpath_head = ...
If scsi_mpath_dev_alloc() fails later at out_uninit,
scsi_multipath_sdev_uninit() also frees sdev->scsi_mpath_dev before clearing
the pointer, allowing a concurrent unblock to hit a use-after-free.
> diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
> index 67e9010d54782..008bcafe73ad2 100644
> --- a/drivers/scsi/scsi_multipath.c
> +++ b/drivers/scsi/scsi_multipath.c
> @@ -240,6 +240,14 @@ static int scsi_multipath_sdev_init(struct scsi_device *sdev)
> return 0;
> }
>
> +void scsi_mpath_revalidate_paths(struct scsi_mpath_device *scsi_mpath_dev)
> +{
> + struct scsi_mpath_head *scsi_mpath_head = scsi_mpath_dev->scsi_mpath_head;
> + struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
> +
> + mpath_revalidate_paths(mpath_head);
> +}
[Severity: High]
Can mpath_head->requeue_work queued by scsi_mpath_revalidate_paths() run
after scsi_mpath_head is freed?
Transport target teardown paths such as fc_starget_delete(),
iscsi_remove_session(), and rport_dev_loss_timedout() call
scsi_target_unblock(..., SDEV_TRANSPORT_OFFLINE) immediately followed by
scsi_remove_target():
CPU0 kworker
fc_starget_delete()
scsi_target_unblock()
scsi_internal_device_unblock()
scsi_mpath_revalidate_paths()
mpath_revalidate_paths()
mpath_schedule_requeue_work()
scsi_remove_target()
...
scsi_mpath_remove_device()
scsi_mpath_put_head()
scsi_mpath_head_release()
kfree(scsi_mpath_head)
mpath_requeue_work()
spin_lock_irq(&mpath_head->requeue_lock)
<-- use-after-free
Unlike scsi_mpath_device_iopolicy_store(), which calls
flush_work(&mpath_head->requeue_work) with a comment noting that
requeue_work could otherwise run after mpath_head is gone, neither
scsi_mpath_revalidate_paths() nor scsi_mpath_head_release() /
mpath_head_uninit() flushes or cancels mpath_head->requeue_work.
> +
> void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev)
> {
> struct mpath_device *mpath_device = &scsi_mpath_dev->mpath_device;
[Severity: Medium]
Can scsi_device_block() hit the same race against __scsi_remove_device() or
scsi_mpath_dev_alloc() when calling scsi_mpath_dev_clear_path()?
drivers/scsi/scsi_lib.c:scsi_device_block() {
...
mutex_unlock(&sdev->state_mutex);
if ((err == 0) && sdev->scsi_mpath_dev)
scsi_mpath_dev_clear_path(sdev->scsi_mpath_dev);
...
}
Once scsi_device_block() drops sdev->state_mutex, __scsi_remove_device() can
transition sdev from SDEV_BLOCK directly to SDEV_DEL and run
scsi_mpath_remove_device(), freeing scsi_mpath_head and clearing
scsi_mpath_dev->scsi_mpath_head before scsi_mpath_dev_clear_path()
dereferences it in mpath_synchronize().
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=18
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 19/29] scsi-multipath: failover handling
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (17 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 18/29] scsi-multipath: revalidate paths upon device unblock John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:15 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 20/29] scsi-multipath: provide callbacks for path state John Garry
` (10 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Failover occurs when the scsi_cmnd has failed and it is discovered that the
target scsi_device has transport down.
For a scsi command which suffers failover, requeue the master bio of each
bio attached to its request.
A bio which for which failover occurs is handled in
scsi_mpath_clone_end_io(). Failover is detected for blk_path_error()
occuring, same as how dm-mpath detects this.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_multipath.c | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 008bcafe73ad..4c965650c2bd 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -262,6 +262,20 @@ static void scsi_mpath_clone_end_io(struct bio *clone)
{
struct bio *master_bio = clone->bi_private;
+ if (clone->bi_status && blk_path_error(clone->bi_status)) {
+ struct mpath_head *mpath_head =
+ master_bio->bi_bdev->bd_disk->private_data;
+ unsigned long flags;
+
+ spin_lock_irqsave(&mpath_head->requeue_lock, flags);
+ bio_list_add(&mpath_head->requeue_list, master_bio);
+ spin_unlock_irqrestore(&mpath_head->requeue_lock, flags);
+ bio_put(clone);
+
+ mpath_schedule_requeue_work(mpath_head);
+ return;
+ }
+
master_bio->bi_status = clone->bi_status;
bio_put(clone);
bio_endio(master_bio);
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 19/29] scsi-multipath: failover handling
2026-11-08 20:47 ` [PATCH v9 19/29] scsi-multipath: failover handling John Garry
@ 2026-11-08 21:15 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:15 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [High] scsi-multipath: infinite bio retry loop in
scsi_mpath_clone_end_io()
- [Medium] scsi: multipath: premature master_bio publish in
scsi_mpath_clone_end_io()
- [Low] scsi-multipath: commit message describes scsi_cmnd request
failover
--
commit 8db30403f561c839e4114d28ecdf086eb7458c31
Author: John Garry <john.garry@linux.dev>
scsi-multipath: failover handling
This commit adds failover handling to scsi_mpath_clone_end_io() by
requeueing the master bio onto mpath_head->requeue_list and scheduling
requeue work when a cloned bio fails with blk_path_error().
> Failover occurs when the scsi_cmnd has failed and it is discovered that the
> target scsi_device has transport down.
>
> For a scsi command which suffers failover, requeue the master bio of each
> bio attached to its request.
[Severity: Low]
This isn't a bug, but do these first two paragraphs describe an earlier
request-based design? The patch handles failover in
scsi_mpath_clone_end_io() based on blk_path_error(clone->bi_status) and
does not inspect scsi_cmnd, check whether the scsi_device transport is
down, or iterate over bios attached to a request.
> diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
> index 008bcafe73ad2..4c965650c2bd3 100644
> --- a/drivers/scsi/scsi_multipath.c
> +++ b/drivers/scsi/scsi_multipath.c
> @@ -262,6 +262,20 @@ static void scsi_mpath_clone_end_io(struct bio *clone)
> {
> struct bio *master_bio = clone->bi_private;
>
> + if (clone->bi_status && blk_path_error(clone->bi_status)) {
[Severity: High]
Can requeueing master_bio on every blk_path_error() without checking or
disabling the path cause an infinite bio retry loop?
blk_path_error() returns true for any status other than BLK_STS_NOTSUPP,
BLK_STS_NOSPC, BLK_STS_TARGET, BLK_STS_RESV_CONFLICT, BLK_STS_MEDIUM, and
BLK_STS_PROTECTION. When a command fails with a status such as
BLK_STS_IOERR (for example, from ABORTED_COMMAND or DID_ERROR) while the
scsi_device remains in SDEV_RUNNING, scsi_mpath_is_disabled() stays false
and mpath_head->current_path[node] is not cleared. mpath_requeue_work()
then calls submit_bio_noacct(master_bio) -> mpath_bdev_submit_bio() ->
mpath_find_path(), which repeatedly selects the same active path and
resubmits master_bio in an unbounded loop on kblockd.
> + struct mpath_head *mpath_head =
> + master_bio->bi_bdev->bd_disk->private_data;
> + unsigned long flags;
> +
> + spin_lock_irqsave(&mpath_head->requeue_lock, flags);
> + bio_list_add(&mpath_head->requeue_list, master_bio);
> + spin_unlock_irqrestore(&mpath_head->requeue_lock, flags);
> + bio_put(clone);
> +
> + mpath_schedule_requeue_work(mpath_head);
[Severity: Medium]
Can releasing mpath_head->requeue_lock before bio_put(clone) and
mpath_schedule_requeue_work(mpath_head) cause a use-after-free on
scsi_mpath_head->bio_pool and mpath_head?
Because mpath_bdev_submit_bio() calls submit_bio_noacct(clone) while
current->bio_list is active, mpath_bdev_submit_bio() drops
mpath_head->srcu before __submit_bio_noacct() submits clone to the
underlying queue. If clone fails during queue entry (such as on a dying
queue) or completes asynchronously after request teardown,
scsi_mpath_clone_end_io() runs without mpath_head->srcu or the underlying
q_usage_counter held. Once requeue_lock is dropped, an already-running
mpath_requeue_work() on another CPU can dequeue and complete master_bio
while the last device reference is being dropped:
CPU0 CPU1
scsi_mpath_clone_end_io()
spin_lock_irqsave(&requeue_lock)
bio_list_add(&requeue_list,
master_bio)
spin_unlock_irqrestore(&requeue_lock)
mpath_requeue_work()
bio_list_get(&requeue_list)
submit_bio_noacct(master_bio)
...
bio_io_error(master_bio)
...
scsi_mpath_head_release()
bioset_exit(&bio_pool)
kfree(scsi_mpath_head)
bio_put(clone) <-- use-after-free
mpath_schedule_requeue_work(
mpath_head) <-- use-after-free
Calling bio_put(clone) before acquiring mpath_head->requeue_lock and
calling mpath_schedule_requeue_work(mpath_head) while holding
mpath_head->requeue_lock avoids this race.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=19
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 20/29] scsi-multipath: provide callbacks for path state
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (18 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 19/29] scsi-multipath: failover handling John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:05 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 21/29] scsi-multipath: add scsi_mpath_{start,end}_request() John Garry
` (9 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Provide callbacks for .is_disabled, .is_optimized, and .available_path.
These all use scsi_device.sdev_state and scsi_device.access_state.
Member scsi_device.access_state will be driven by ALUA. Currently
only device handlers support this, and in future we will have core
SCSI support for implicit ALUA (not relying on device handlers).
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_multipath.c | 49 +++++++++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 4c965650c2bd..2f47c39d8293 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -298,7 +298,56 @@ static struct bio *scsi_mpath_clone_bio(struct bio *bio)
return clone;
}
+static bool scsi_mpath_is_disabled(struct mpath_device *mpath_device)
+{
+ struct scsi_mpath_device *scsi_mpath_dev =
+ to_scsi_mpath_device(mpath_device);
+ struct scsi_device *sdev = scsi_mpath_dev->sdev;
+ unsigned char access_state = READ_ONCE(sdev->access_state) &
+ SCSI_ACCESS_STATE_MASK;
+
+ if (READ_ONCE(sdev->sdev_state) != SDEV_RUNNING)
+ return true;
+
+ if (access_state == SCSI_ACCESS_STATE_OPTIMAL ||
+ access_state == SCSI_ACCESS_STATE_ACTIVE)
+ return false;
+
+ return true;
+}
+
+static bool scsi_mpath_is_optimized(struct mpath_device *mpath_device)
+{
+ struct scsi_mpath_device *scsi_mpath_dev =
+ to_scsi_mpath_device(mpath_device);
+ struct scsi_device *sdev = scsi_mpath_dev->sdev;
+ unsigned char access_state = READ_ONCE(sdev->access_state) &
+ SCSI_ACCESS_STATE_MASK;
+
+ if (READ_ONCE(sdev->sdev_state) != SDEV_RUNNING)
+ return false;
+
+ return access_state == SCSI_ACCESS_STATE_OPTIMAL;
+}
+
+static bool scsi_mpath_available_path(struct mpath_device *mpath_device)
+{
+ struct scsi_mpath_device *scsi_mpath_dev =
+ to_scsi_mpath_device(mpath_device);
+ struct scsi_device *sdev = scsi_mpath_dev->sdev;
+ enum scsi_device_state sdev_state = READ_ONCE(sdev->sdev_state);
+
+ if (sdev_state == SDEV_RUNNING || sdev_state == SDEV_QUIESCE ||
+ sdev_state == SDEV_BLOCK || sdev_state == SDEV_CREATED_BLOCK)
+ return true;
+
+ return false;
+}
+
static struct mpath_head_template smpdt = {
+ .is_disabled = scsi_mpath_is_disabled,
+ .is_optimized = scsi_mpath_is_optimized,
+ .available_path = scsi_mpath_available_path,
.clone_bio = scsi_mpath_clone_bio,
};
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 20/29] scsi-multipath: provide callbacks for path state
2026-11-08 20:47 ` [PATCH v9 20/29] scsi-multipath: provide callbacks for path state John Garry
@ 2026-11-08 21:05 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:05 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [High] scsi: scsi_multipath: scsi_mpath_available_path() ignores
sdev->access_state
- [Medium] scsi: scsi_multipath: mpath_device access_state never updated
--
commit 6f83cc4eccb483172140b55837b87755b4421b91
Author: John Garry <john.garry@linux.dev>
scsi-multipath: provide callbacks for path state
This commit implements the .is_disabled, .is_optimized, and .available_path
multipath callbacks for SCSI multipath devices using sdev_state and
access_state.
> diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
> index 4c965650c2bd3..2f47c39d82936 100644
> --- a/drivers/scsi/scsi_multipath.c
> +++ b/drivers/scsi/scsi_multipath.c
> @@ -298,7 +298,56 @@ static struct bio *scsi_mpath_clone_bio(struct bio *bio)
> return clone;
> }
>
> +static bool scsi_mpath_is_disabled(struct mpath_device *mpath_device)
> +{
> + struct scsi_mpath_device *scsi_mpath_dev =
> + to_scsi_mpath_device(mpath_device);
> + struct scsi_device *sdev = scsi_mpath_dev->sdev;
> + unsigned char access_state = READ_ONCE(sdev->access_state) &
> + SCSI_ACCESS_STATE_MASK;
> +
> + if (READ_ONCE(sdev->sdev_state) != SDEV_RUNNING)
> + return true;
> +
> + if (access_state == SCSI_ACCESS_STATE_OPTIMAL ||
> + access_state == SCSI_ACCESS_STATE_ACTIVE)
> + return false;
> +
> + return true;
> +}
> +
> +static bool scsi_mpath_is_optimized(struct mpath_device *mpath_device)
> +{
> + struct scsi_mpath_device *scsi_mpath_dev =
> + to_scsi_mpath_device(mpath_device);
> + struct scsi_device *sdev = scsi_mpath_dev->sdev;
> + unsigned char access_state = READ_ONCE(sdev->access_state) &
> + SCSI_ACCESS_STATE_MASK;
> +
> + if (READ_ONCE(sdev->sdev_state) != SDEV_RUNNING)
> + return false;
> +
> + return access_state == SCSI_ACCESS_STATE_OPTIMAL;
> +}
[Severity: Medium]
Does scsi_mpath_is_optimized() disagree with mpath_device->access_state for
active/non-optimized paths?
scsi_multipath_sdev_init() initializes mpath_device->access_state to
MPATH_STATE_OPTIMIZED, and nothing updates it when sdev->access_state
changes:
lib/multipath.c:__mpath_find_path() {
...
switch(mpath_device->access_state) {
case MPATH_STATE_OPTIMIZED:
if (distance < found_distance) {
found_distance = distance;
found = mpath_device;
}
break;
case MPATH_STATE_NONOPTIMIZED:
...
}
Because __mpath_find_path(), mpath_round_robin_path(), and
mpath_queue_depth_path() select paths using mpath_device->access_state
rather than mpdt->is_optimized(), an active/non-optimized path is still
treated as optimized during path selection. Under the NUMA policy,
mpath_numa_path() then checks scsi_mpath_is_optimized() on every bio, sees
false, and calls __mpath_find_path() to rescan dev_list and pick the same
non-optimized path again on every I/O.
> +
> +static bool scsi_mpath_available_path(struct mpath_device *mpath_device)
> +{
> + struct scsi_mpath_device *scsi_mpath_dev =
> + to_scsi_mpath_device(mpath_device);
> + struct scsi_device *sdev = scsi_mpath_dev->sdev;
> + enum scsi_device_state sdev_state = READ_ONCE(sdev->sdev_state);
> +
> + if (sdev_state == SDEV_RUNNING || sdev_state == SDEV_QUIESCE ||
> + sdev_state == SDEV_BLOCK || sdev_state == SDEV_CREATED_BLOCK)
> + return true;
> +
> + return false;
> +}
[Severity: High]
Should scsi_mpath_available_path() also check sdev->access_state?
When all paths are in SDEV_RUNNING with an unusable ALUA state such as
standby, unavailable, or offline, scsi_mpath_is_disabled() returns true for
every path and mpath_find_path() returns NULL. Because
scsi_mpath_available_path() still returns true for any SDEV_RUNNING device,
mpath_bdev_submit_bio() queues bios onto mpath_head->requeue_list instead
of failing them with bio_io_error(), leaving I/O stuck on requeue_list.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=20
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 21/29] scsi-multipath: add scsi_mpath_{start,end}_request()
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (19 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 20/29] scsi-multipath: provide callbacks for path state John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:28 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 22/29] scsi-multipath: update request for terminal request queuing error John Garry
` (8 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add scsi_mpath_{start,end}_request() to handle updating private multipath
request data, like nvme_mpath_{start,end}_request().
New member Scsi_Host.mpath_nr_active is added. This is required for queue
depth multipath iopolicy.
For NVMe, this count is per controller. The reason is that many NSes may
be connected to a controller, so congestion should be judged at
controller level.
SCSI has no definition of a controller, but SCSI host is a comparable
concept.
Indeed, many SCSI disks may be connected to the same SCSI host, so it
makes sense to count number of active requests at this point. However,
for a transport like iSCSI Initiator over TCP/IP, we have a separate SCSI
host per SCSI device (so there the count would be same at SCSI device
level).
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_lib.c | 8 +++-
drivers/scsi/scsi_multipath.c | 69 +++++++++++++++++++++++++++++++++++
include/scsi/scsi_cmnd.h | 9 ++++-
include/scsi/scsi_host.h | 4 ++
include/scsi/scsi_multipath.h | 11 ++++++
5 files changed, 99 insertions(+), 2 deletions(-)
diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
index 4dbd01484b4a..13562c0ab48b 100644
--- a/drivers/scsi/scsi_lib.c
+++ b/drivers/scsi/scsi_lib.c
@@ -669,7 +669,10 @@ static bool scsi_end_request(struct request *req, blk_status_t error,
struct scsi_device *sdev = cmd->device;
struct request_queue *q = sdev->request_queue;
- if (blk_update_request(req, error, bytes))
+ if (sdev->scsi_mpath_dev && is_mpath_request(req)) {
+ if (scsi_mpath_end_request(req, error, bytes))
+ return true;
+ } else if (blk_update_request(req, error, bytes))
return true;
if (q->limits.features & BLK_FEAT_ADD_RANDOM)
@@ -1947,6 +1950,9 @@ static blk_status_t scsi_queue_rq(struct blk_mq_hw_ctx *hctx,
memset(cmd->sense_buffer, 0, SCSI_SENSE_BUFFERSIZE);
cmd->submitter = SUBMITTED_BY_BLOCK_LAYER;
+ if (sdev->scsi_mpath_dev && is_mpath_request(req))
+ scsi_mpath_start_request(req);
+
blk_mq_start_request(req);
if (blk_mq_is_reserved_rq(req)) {
reason = shost->hostt->queue_reserved_command(shost, cmd);
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 2f47c39d8293..93a4545f1dbf 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -545,6 +545,75 @@ void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head)
}
EXPORT_SYMBOL_GPL(scsi_mpath_put_head);
+void scsi_mpath_start_request(struct request *req)
+{
+ struct scsi_cmnd *scmd = blk_mq_rq_to_pdu(req);
+ struct scsi_device *sdev = scmd->device;
+ struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
+ struct scsi_mpath_head *scsi_mpath_head =
+ scsi_mpath_dev->scsi_mpath_head;
+ struct bio *clone = req->bio, *master;
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+ struct gendisk *disk = mpath_head->disk;
+
+ if (mpath_qd_iopolicy(&scsi_mpath_head->iopolicy) &&
+ !(scmd->flags & SCMD_MPATH_CNT_ACTIVE)) {
+ struct Scsi_Host *shost = sdev->host;
+
+ atomic_inc(&shost->mpath_nr_active);
+ scmd->flags |= SCMD_MPATH_CNT_ACTIVE;
+ }
+
+ if (blk_rq_is_passthrough(req) || !blk_queue_io_stat(disk->queue))
+ return;
+
+ if (!clone) {
+ scmd->flags &= ~SCMD_MPATH_IO_STATS;
+ return;
+ }
+
+ if (scmd->flags & SCMD_MPATH_IO_STATS)
+ return;
+
+ master = clone->bi_private;
+
+ scmd->flags |= SCMD_MPATH_IO_STATS;
+ scmd->start_time = bdev_start_io_acct(master->bi_bdev, req_op(req),
+ jiffies);
+ scmd->start_bytes = blk_rq_bytes(req);
+}
+
+bool scsi_mpath_end_request(struct request *req, blk_status_t error,
+ unsigned int nr_bytes)
+{
+ struct scsi_cmnd *scmd = blk_mq_rq_to_pdu(req);
+ struct scsi_device *sdev = scmd->device;
+ struct block_device *bi_bdev = NULL;
+
+ if (scmd->flags & SCMD_MPATH_IO_STATS) {
+ struct bio *clone = req->bio, *master = clone->bi_private;
+
+ bi_bdev = master->bi_bdev;
+ }
+
+ if (blk_update_request(req, error, nr_bytes))
+ return true;
+
+ if (scmd->flags & SCMD_MPATH_CNT_ACTIVE) {
+ struct Scsi_Host *shost = sdev->host;
+
+ atomic_dec_if_positive(&shost->mpath_nr_active);
+ }
+
+ if (!(scmd->flags & SCMD_MPATH_IO_STATS))
+ return false;
+ bdev_end_io_acct(bi_bdev, req_op(req),
+ scmd->start_bytes >> SECTOR_SHIFT,
+ scmd->start_time);
+
+ return false;
+}
+
int __init scsi_multipath_init(void)
{
return class_register(&scsi_mpath_device_class);
diff --git a/include/scsi/scsi_cmnd.h b/include/scsi/scsi_cmnd.h
index 5a8f7a45dc6d..62ffd33ba7ea 100644
--- a/include/scsi/scsi_cmnd.h
+++ b/include/scsi/scsi_cmnd.h
@@ -58,8 +58,11 @@ struct scsi_pointer {
*/
#define SCMD_FORCE_EH_SUCCESS (1 << 3)
#define SCMD_FAIL_IF_RECOVERING (1 << 4)
+#define SCMD_MPATH_IO_STATS (1 << 5)
+#define SCMD_MPATH_CNT_ACTIVE (1 << 6)
/* flags preserved across unprep / reprep */
-#define SCMD_PRESERVED_FLAGS (SCMD_INITIALIZED | SCMD_FAIL_IF_RECOVERING)
+#define SCMD_PRESERVED_FLAGS (SCMD_INITIALIZED | SCMD_FAIL_IF_RECOVERING | \
+ SCMD_MPATH_IO_STATS | SCMD_MPATH_CNT_ACTIVE)
/* for scmd->state */
#define SCMD_STATE_COMPLETE 0
@@ -146,6 +149,10 @@ struct scsi_cmnd {
* to release this memory. (The memory
* obtained by scsi_malloc is guaranteed
* to be at an address < 16Mb). */
+ #ifdef CONFIG_SCSI_MULTIPATH
+ unsigned long start_time;
+ unsigned int start_bytes;
+ #endif
int result; /* Status code from lower level driver */
};
diff --git a/include/scsi/scsi_host.h b/include/scsi/scsi_host.h
index daaf1247673f..8cbca6b75732 100644
--- a/include/scsi/scsi_host.h
+++ b/include/scsi/scsi_host.h
@@ -756,6 +756,10 @@ struct Scsi_Host {
/* Delay for runtime autosuspend */
int rpm_autosuspend_delay;
+ #ifdef CONFIG_SCSI_MULTIPATH
+ atomic_t mpath_nr_active;
+ #endif
+
/*
* We should ensure that this is aligned, both for better performance
* and also because some compilers (m68k) don't automatically force
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
index 6e50da94fc5d..dca1307c9fa4 100644
--- a/include/scsi/scsi_multipath.h
+++ b/include/scsi/scsi_multipath.h
@@ -57,6 +57,9 @@ void scsi_mpath_add_sysfs_link(struct scsi_device *sdev);
void scsi_mpath_remove_sysfs_link(struct scsi_device *sdev);
void scsi_mpath_dev_clear_path(struct scsi_mpath_device *scsi_mpath_dev);
void scsi_mpath_revalidate_paths(struct scsi_mpath_device *scsi_mpath_dev);
+void scsi_mpath_start_request(struct request *req);
+bool scsi_mpath_end_request(struct request *req, blk_status_t error,
+ unsigned int nr_bytes);
#else /* CONFIG_SCSI_MULTIPATH */
struct scsi_mpath_head {
@@ -109,5 +112,13 @@ static inline
void scsi_mpath_revalidate_paths(struct scsi_mpath_device *scsi_mpath_dev)
{
}
+static inline void scsi_mpath_start_request(struct request *req)
+{
+}
+static inline bool scsi_mpath_end_request(struct request *req, blk_status_t error,
+ unsigned int nr_bytes)
+{
+ return false;
+}
#endif /* CONFIG_SCSI_MULTIPATH */
#endif /* _SCSI_SCSI_MULTIPATH_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 21/29] scsi-multipath: add scsi_mpath_{start,end}_request()
2026-11-08 20:47 ` [PATCH v9 21/29] scsi-multipath: add scsi_mpath_{start,end}_request() John Garry
@ 2026-11-08 21:28 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:28 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] scsi: multipath: premature bio completion in
scsi_mpath_end_request()
- [Medium] scsi-multipath: mismatched bdev I/O accounting in
scsi_mpath_end_request()
--
commit 29255340e39c30a34f5cb1da674871fdd2c627ea
Author: John Garry <john.garry@linux.dev>
scsi-multipath: add scsi_mpath_{start,end}_request()
This commit adds scsi_mpath_start_request() and scsi_mpath_end_request()
to track per-host active multipath requests for the queue-depth I/O policy
and perform block device I/O accounting on multipath requests.
> diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
> index 2f47c39d82936..93a4545f1dbf7 100644
> --- a/drivers/scsi/scsi_multipath.c
> +++ b/drivers/scsi/scsi_multipath.c
> @@ -545,6 +545,75 @@ void scsi_mpath_put_head(struct scsi_mpath_head *scsi_mpath_head)
> }
> EXPORT_SYMBOL_GPL(scsi_mpath_put_head);
>
> +void scsi_mpath_start_request(struct request *req)
> +{
> + struct scsi_cmnd *scmd = blk_mq_rq_to_pdu(req);
> + struct scsi_device *sdev = scmd->device;
> + struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
> + struct scsi_mpath_head *scsi_mpath_head =
> + scsi_mpath_dev->scsi_mpath_head;
> + struct bio *clone = req->bio, *master;
> + struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
> + struct gendisk *disk = mpath_head->disk;
> +
> + if (mpath_qd_iopolicy(&scsi_mpath_head->iopolicy) &&
> + !(scmd->flags & SCMD_MPATH_CNT_ACTIVE)) {
> + struct Scsi_Host *shost = sdev->host;
> +
> + atomic_inc(&shost->mpath_nr_active);
> + scmd->flags |= SCMD_MPATH_CNT_ACTIVE;
> + }
> +
> + if (blk_rq_is_passthrough(req) || !blk_queue_io_stat(disk->queue))
> + return;
> +
> + if (!clone) {
> + scmd->flags &= ~SCMD_MPATH_IO_STATS;
> + return;
> + }
> +
> + if (scmd->flags & SCMD_MPATH_IO_STATS)
> + return;
> +
> + master = clone->bi_private;
> +
> + scmd->flags |= SCMD_MPATH_IO_STATS;
> + scmd->start_time = bdev_start_io_acct(master->bi_bdev, req_op(req),
> + jiffies);
> + scmd->start_bytes = blk_rq_bytes(req);
> +}
> +
> +bool scsi_mpath_end_request(struct request *req, blk_status_t error,
> + unsigned int nr_bytes)
> +{
> + struct scsi_cmnd *scmd = blk_mq_rq_to_pdu(req);
> + struct scsi_device *sdev = scmd->device;
> + struct block_device *bi_bdev = NULL;
> +
> + if (scmd->flags & SCMD_MPATH_IO_STATS) {
> + struct bio *clone = req->bio, *master = clone->bi_private;
> +
> + bi_bdev = master->bi_bdev;
> + }
[Severity: Medium]
Can scsi_mpath_start_request() and scsi_mpath_end_request() account I/O
against different block_device structures when a merged request completes
partially?
scsi_mpath_clone_bio() directs all clone bios to the underlying path
disk's part0 while storing the original bio in clone->bi_private.
Contiguous clone bios from adjacent partitions on mpath_head->disk can
therefore be merged into a single request on sdev->request_queue.
When that request starts, scsi_mpath_start_request() calls
bdev_start_io_acct() on the first bio's master->bi_bdev and sets
SCMD_MPATH_IO_STATS. If a partial completion advances req->bio past the
first bio (returning true from blk_update_request()), the final call to
scsi_mpath_end_request() re-derives bi_bdev from the remaining bio's
master->bi_bdev and calls bdev_end_io_acct() on the second partition,
leaking in_flight on the first partition and underflowing it on the
second.
Accounting against mpath_head->disk->part0 in both functions (or saving
the accounted block_device) would keep the start and end calls paired.
> +
> + if (blk_update_request(req, error, nr_bytes))
> + return true;
> +
> + if (scmd->flags & SCMD_MPATH_CNT_ACTIVE) {
> + struct Scsi_Host *shost = sdev->host;
> +
> + atomic_dec_if_positive(&shost->mpath_nr_active);
> + }
> +
> + if (!(scmd->flags & SCMD_MPATH_IO_STATS))
> + return false;
> + bdev_end_io_acct(bi_bdev, req_op(req),
> + scmd->start_bytes >> SECTOR_SHIFT,
> + scmd->start_time);
> +
> + return false;
> +}
[Severity: Medium]
Does calling blk_update_request() before updating shost->mpath_nr_active
and calling bdev_end_io_acct() complete master_bio too early?
blk_update_request() runs bio_endio(clone) -> scsi_mpath_clone_end_io(),
which either schedules mpath_head->requeue_work on a path error or calls
bio_endio(master_bio). Because mpath_head->disk is bio-based,
__submit_bio() drops its queue reference as soon as
mpath_bdev_submit_bio() returns, so nothing pins bi_bdev once master_bio
completes:
CPU0 CPU1
scsi_mpath_end_request()
blk_update_request()
...
scsi_mpath_clone_end_io()
bio_endio(master_bio)
blkdev_put()
bdev_del_partition()
...
bdev_free_inode()
free_percpu(bdev->bd_stats)
bdev_end_io_acct(bi_bdev, ...)
<-- use-after-free on bi_bdev
In addition, when scsi_mpath_clone_end_io() triggers failover or a
caller immediately submits dependent I/O under MPATH_IOPOLICY_QD,
mpath_queue_depth_path() reads shost->mpath_nr_active before this request
has decremented it.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=21
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 22/29] scsi-multipath: update request for terminal request queuing error
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (20 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 21/29] scsi-multipath: add scsi_mpath_{start,end}_request() John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:09 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 23/29] scsi-multipath: add delayed disk removal support John Garry
` (7 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
If we cannot queue a request in scsi_queue_rq(), then we may error out
with an error code which means that the block layer ends the request (and
does not requeue). Such an error scenario would be scsi_device transport
offline.
In this case, we should update the request to deal with when
SCMD_MPATH_IO_STATS and SCMD_MPATH_IO_STATS flags have been set.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_lib.c | 3 +++
drivers/scsi/scsi_multipath.c | 35 +++++++++++++++++++++++++++++++++++
include/scsi/scsi_multipath.h | 5 +++++
3 files changed, 43 insertions(+)
diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
index 13562c0ab48b..a5a2967d17a2 100644
--- a/drivers/scsi/scsi_lib.c
+++ b/drivers/scsi/scsi_lib.c
@@ -1996,6 +1996,9 @@ static blk_status_t scsi_queue_rq(struct blk_mq_hw_ctx *hctx,
cmd->result = DID_NO_CONNECT << 16;
else
cmd->result = DID_ERROR << 16;
+
+ if (sdev->scsi_mpath_dev && is_mpath_request(req))
+ scsi_mpath_end_request_no_update(req, ret);
/*
* Make sure to release all allocated resources when
* we hit an error, as we will never see this command
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index 93a4545f1dbf..bec6dd94f3be 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -614,6 +614,41 @@ bool scsi_mpath_end_request(struct request *req, blk_status_t error,
return false;
}
+void scsi_mpath_end_request_no_update(struct request *req, blk_status_t error)
+{
+ struct scsi_cmnd *scmd = blk_mq_rq_to_pdu(req);
+ struct scsi_device *sdev = scmd->device;
+ struct bio *clone = req->bio, *master = clone->bi_private;
+ struct block_device *bi_bdev = master->bi_bdev;
+ unsigned int nr_bytes;
+
+ /* Don't generate error logs if we are going to try another path */
+ if (blk_path_error(error))
+ req->rq_flags |= RQF_QUIET;
+
+ if (scmd->flags & SCMD_MPATH_CNT_ACTIVE) {
+ struct Scsi_Host *shost = sdev->host;
+
+ atomic_dec_if_positive(&shost->mpath_nr_active);
+ }
+
+ if (!(scmd->flags & SCMD_MPATH_IO_STATS))
+ return;
+
+ /*
+ * blk_rq_bytes() value should never exceed start_bytes, but check in
+ * case.
+ */
+ if (scmd->start_bytes > blk_rq_bytes(req))
+ nr_bytes = scmd->start_bytes - blk_rq_bytes(req);
+ else
+ nr_bytes = 0;
+
+ bdev_end_io_acct(bi_bdev, req_op(req),
+ nr_bytes >> SECTOR_SHIFT,
+ scmd->start_time);
+}
+
int __init scsi_multipath_init(void)
{
return class_register(&scsi_mpath_device_class);
diff --git a/include/scsi/scsi_multipath.h b/include/scsi/scsi_multipath.h
index dca1307c9fa4..72ad109a9a81 100644
--- a/include/scsi/scsi_multipath.h
+++ b/include/scsi/scsi_multipath.h
@@ -60,6 +60,7 @@ void scsi_mpath_revalidate_paths(struct scsi_mpath_device *scsi_mpath_dev);
void scsi_mpath_start_request(struct request *req);
bool scsi_mpath_end_request(struct request *req, blk_status_t error,
unsigned int nr_bytes);
+void scsi_mpath_end_request_no_update(struct request *req, blk_status_t error);
#else /* CONFIG_SCSI_MULTIPATH */
struct scsi_mpath_head {
@@ -120,5 +121,9 @@ static inline bool scsi_mpath_end_request(struct request *req, blk_status_t erro
{
return false;
}
+static inline void scsi_mpath_end_request_no_update(struct request *req,
+ blk_status_t error)
+{
+}
#endif /* CONFIG_SCSI_MULTIPATH */
#endif /* _SCSI_SCSI_MULTIPATH_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 22/29] scsi-multipath: update request for terminal request queuing error
2026-11-08 20:47 ` [PATCH v9 22/29] scsi-multipath: update request for terminal request queuing error John Garry
@ 2026-11-08 21:09 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:09 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [High] scsi-multipath: NULL dereference on bio-less request in
scsi_mpath_end_request_no_update()
--
commit 6015c0421ae4c77548304e8f223ec4e93492aa00
Author: John Garry <john.garry@linux.dev>
scsi-multipath: update request for terminal request queuing error
This commit adds scsi_mpath_end_request_no_update() and calls it from
scsi_queue_rq() when a multipath request fails with a terminal queueing
error, updating active request counts and I/O statistics.
> diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
> index 93a4545f1dbf7..bec6dd94f3be8 100644
> --- a/drivers/scsi/scsi_multipath.c
> +++ b/drivers/scsi/scsi_multipath.c
> @@ -614,6 +614,41 @@ bool scsi_mpath_end_request(struct request *req, blk_status_t error,
> return false;
> }
>
> +void scsi_mpath_end_request_no_update(struct request *req, blk_status_t error)
> +{
> + struct scsi_cmnd *scmd = blk_mq_rq_to_pdu(req);
> + struct scsi_device *sdev = scmd->device;
> + struct bio *clone = req->bio, *master = clone->bi_private;
> + struct block_device *bi_bdev = master->bi_bdev;
> + unsigned int nr_bytes;
> +
> + /* Don't generate error logs if we are going to try another path */
> + if (blk_path_error(error))
> + req->rq_flags |= RQF_QUIET;
> +
> + if (scmd->flags & SCMD_MPATH_CNT_ACTIVE) {
> + struct Scsi_Host *shost = sdev->host;
> +
> + atomic_dec_if_positive(&shost->mpath_nr_active);
> + }
> +
> + if (!(scmd->flags & SCMD_MPATH_IO_STATS))
> + return;
[Severity: High]
Can clone->bi_private cause a NULL pointer dereference when req->bio is
NULL?
blk_kick_flush() initializes fq->flush_rq with flush_rq->bio set to NULL
and copies REQ_DRV (which REQ_MPATH aliases) into flush_rq->cmd_flags:
block/blk-flush.c:blk_kick_flush() {
...
flush_rq->cmd_flags = REQ_OP_FLUSH | REQ_PREFLUSH;
flush_rq->cmd_flags |= (flags & REQ_DRV) | (flags & REQ_FAILFAST_MASK);
...
}
Unlike scsi_mpath_start_request() and scsi_mpath_end_request(), which
only dereference req->bio when SCMD_MPATH_IO_STATS is set,
scsi_mpath_end_request_no_update() dereferences req->bio unconditionally
at function entry before checking SCMD_MPATH_IO_STATS. If a flush
request hits a terminal queueing error in scsi_queue_rq() (such as when a
path is transport offline), this crashes. Deferring the req->bio and
clone->bi_private dereferences until after the SCMD_MPATH_IO_STATS check
would avoid this.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=22
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 23/29] scsi-multipath: add delayed disk removal support
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (21 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 22/29] scsi-multipath: update request for terminal request queuing error John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 24/29] scsi: sd: add multipath disk class John Garry
` (6 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add support in core code for delayed disk removal support. In this, the
callback calls into the scsi_driver to do the necessary removal work.
The scsi_disk driver (sd) must ensure that the scsi_mpath_device does not
go away while the delayed removal work is active, i.e. it must keep a
reference.
No reference to the scsi_disk multipath structures are kept outside that
driver, so that driver needs to provide a scsi_driver.mpath_remove_head
callback to do the necessary work.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/scsi_multipath.c | 27 +++++++++++++++++++++++++++
include/scsi/scsi_driver.h | 4 ++++
2 files changed, 31 insertions(+)
diff --git a/drivers/scsi/scsi_multipath.c b/drivers/scsi/scsi_multipath.c
index bec6dd94f3be..58b0be88a952 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -344,7 +344,34 @@ static bool scsi_mpath_available_path(struct mpath_device *mpath_device)
return false;
}
+static int scsi_mpath_remove_head_drv(struct device_driver *drv, void *data)
+{
+ struct scsi_mpath_head *scsi_mpath_head = data;
+ struct scsi_driver *scsi_driver = to_scsi_driver(drv);
+
+ if (scsi_driver->mpath_remove_head)
+ scsi_driver->mpath_remove_head(scsi_mpath_head);
+
+ return 0;
+}
+
+static void scsi_mpath_remove_head_work(struct mpath_head *mpath_head)
+{
+ struct scsi_mpath_head *scsi_mpath_head = to_scsi_mpath_head(mpath_head);
+
+ /*
+ * scsi_mpath_remove_head_drv() could drop the final reference to
+ * scsi_mpath_head, meaning that we pass a free'd pointer further
+ * drivers - take a reference so that won't happen.
+ */
+ scsi_mpath_get_head(scsi_mpath_head);
+ bus_for_each_drv(&scsi_bus_type, NULL, scsi_mpath_head,
+ scsi_mpath_remove_head_drv);
+ scsi_mpath_put_head(scsi_mpath_head);
+}
+
static struct mpath_head_template smpdt = {
+ .remove_head = scsi_mpath_remove_head_work,
.is_disabled = scsi_mpath_is_disabled,
.is_optimized = scsi_mpath_is_optimized,
.available_path = scsi_mpath_available_path,
diff --git a/include/scsi/scsi_driver.h b/include/scsi/scsi_driver.h
index 249cea724abd..d92b63d357f2 100644
--- a/include/scsi/scsi_driver.h
+++ b/include/scsi/scsi_driver.h
@@ -8,6 +8,7 @@
struct module;
struct request;
+struct scsi_mpath_head;
struct scsi_driver {
struct device_driver gendrv;
@@ -22,6 +23,9 @@ struct scsi_driver {
int (*done)(struct scsi_cmnd *);
int (*eh_action)(struct scsi_cmnd *, int);
void (*eh_reset)(struct scsi_cmnd *);
+ #ifdef CONFIG_SCSI_MULTIPATH
+ void (*mpath_remove_head)(struct scsi_mpath_head *);
+ #endif
};
#define to_scsi_driver(drv) \
container_of((drv), struct scsi_driver, gendrv)
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 24/29] scsi: sd: add multipath disk class
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (22 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 23/29] scsi-multipath: add delayed disk removal support John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 25/29] scsi: sd: add multipath disk attr groups John Garry
` (5 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add a new class, sd_mpath_disk_class, which is the multipath version of
the scsi_disk class.
Structure sd_mpath_disk is introduced to manage the multipath gendisk.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/sd.c | 43 ++++++++++++++++++++++++++++++++++++++++++-
drivers/scsi/sd.h | 3 +++
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/drivers/scsi/sd.c b/drivers/scsi/sd.c
index dc45df819c42..03617029a552 100644
--- a/drivers/scsi/sd.c
+++ b/drivers/scsi/sd.c
@@ -70,6 +70,7 @@
#include <scsi/scsi_ioctl.h>
#include <scsi/scsicam.h>
#include <scsi/scsi_common.h>
+#include <scsi/scsi_multipath.h>
#include "sd.h"
#include "scsi_priv.h"
@@ -113,6 +114,39 @@ static mempool_t *sd_page_pool;
static mempool_t *sd_large_page_pool;
static atomic_t sd_large_page_pool_users = ATOMIC_INIT(0);
static struct lock_class_key sd_bio_compl_lkclass;
+#ifdef CONFIG_SCSI_MULTIPATH
+struct sd_mpath_disk {
+ struct scsi_mpath_head *scsi_mpath_head;
+};
+
+static void sd_mpath_disk_release(struct device *dev)
+{
+}
+
+static const struct class sd_mpath_disk_class = {
+ .name = "scsi_mpath_disk",
+ .dev_release = sd_mpath_disk_release,
+};
+
+static int sd_mpath_class_register(void)
+{
+ return class_register(&sd_mpath_disk_class);
+}
+
+static void sd_mpath_class_unregister(void)
+{
+ class_unregister(&sd_mpath_disk_class);
+}
+#else /* CONFIG_SCSI_MULTIPATH */
+static int sd_mpath_class_register(void)
+{
+ return 0;
+}
+
+static void sd_mpath_class_unregister(void)
+{
+}
+#endif
static const char *sd_cache_types[] = {
"write through", "none", "write back",
@@ -4474,11 +4508,15 @@ static int __init init_sd(void)
if (err)
goto err_out;
+ err = sd_mpath_class_register();
+ if (err)
+ goto err_out_class;
+
sd_page_pool = mempool_create_page_pool(SD_MEMPOOL_SIZE, 0);
if (!sd_page_pool) {
printk(KERN_ERR "sd: can't init discard page pool\n");
err = -ENOMEM;
- goto err_out_class;
+ goto err_out_mpath_class;
}
err = scsi_register_driver(&sd_template);
@@ -4489,6 +4527,8 @@ static int __init init_sd(void)
err_out_driver:
mempool_destroy(sd_page_pool);
+err_out_mpath_class:
+ sd_mpath_class_unregister();
err_out_class:
class_unregister(&sd_disk_class);
err_out:
@@ -4514,6 +4554,7 @@ static void __exit exit_sd(void)
mempool_destroy(sd_large_page_pool);
class_unregister(&sd_disk_class);
+ sd_mpath_class_unregister();
for (i = 0; i < SD_MAJORS; i++)
unregister_blkdev(sd_major(i), "sd");
diff --git a/drivers/scsi/sd.h b/drivers/scsi/sd.h
index 574af8243016..22438173ea2f 100644
--- a/drivers/scsi/sd.h
+++ b/drivers/scsi/sd.h
@@ -83,6 +83,9 @@ struct zoned_disk_info {
struct scsi_disk {
struct scsi_device *device;
+#ifdef CONFIG_SCSI_MULTIPATH
+ struct sd_mpath_disk *sd_mpath_disk;
+#endif
/*
* disk_dev is used to show attributes in /sys/class/scsi_disk/,
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 25/29] scsi: sd: add multipath disk attr groups
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (23 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 24/29] scsi: sd: add multipath disk class John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 26/29] scsi: sd: support multipath disk John Garry
` (4 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Set multipath disk attr groups, which includes delayed disk removal and
everything from mpath_attr_group.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/sd.c | 36 ++++++++++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/drivers/scsi/sd.c b/drivers/scsi/sd.c
index 03617029a552..fb87b1663ad6 100644
--- a/drivers/scsi/sd.c
+++ b/drivers/scsi/sd.c
@@ -4021,6 +4021,42 @@ static int sd_format_disk_name(char *prefix, int index, char *buf, int buflen)
return 0;
}
+static ssize_t sd_mpath_device_delayed_removal_store(struct device *dev,
+ struct device_attribute *attr, const char *buf, size_t count)
+{
+ struct mpath_head *mpath_head = mpath_bd_device_to_head(dev);
+
+ return mpath_delayed_removal_secs_store(mpath_head, buf, count);
+}
+
+static ssize_t sd_mpath_device_delayed_removal_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct mpath_head *mpath_head = mpath_bd_device_to_head(dev);
+
+ return mpath_delayed_removal_secs_show(mpath_head, buf);
+}
+
+static DEVICE_ATTR(delayed_removal_secs, S_IRUGO | S_IWUSR,
+ sd_mpath_device_delayed_removal_show,
+ sd_mpath_device_delayed_removal_store);
+
+static struct attribute *sd_mpath_disk_attrs[] = {
+ &dev_attr_delayed_removal_secs.attr,
+ NULL
+};
+
+static const struct attribute_group sd_mpath_disk_attr_group = {
+ .attrs = sd_mpath_disk_attrs,
+};
+
+__maybe_unused
+static const struct attribute_group *sd_mpath_disk_attr_groups[] = {
+ &sd_mpath_disk_attr_group,
+ &mpath_attr_group,
+ NULL
+};
+
/**
* sd_probe - called during driver initialization and whenever a
* new scsi device is attached to the system. It is called once
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 26/29] scsi: sd: support multipath disk
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (24 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 25/29] scsi: sd: add multipath disk attr groups John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 21:23 ` sashiko-bot
2026-11-08 20:47 ` [PATCH v9 27/29] scsi: sd: add mpath_dev file John Garry
` (3 subsequent siblings)
29 siblings, 1 reply; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add support to attach a multipath disk.
We still allocate the gendisk per path, and this is required for the
per-path submission. However, those gendisks are marked as hidden. Those
disks are named sdX:Y, where X is the multipath disk index and Y is the
per-path index.
A global list of sd_mpath_disks is kept for matching scsi_device's.
The multipath gendisk has the name and disk->major/minor set to minic a
scsi_disk.
The following is an example of relevant scsi_disk and block sysfs
directories:
$ ls -l /sys/block/ | grep sdc
lrwxrwxrwx 1 root root 0 Feb 24 16:01 sdc -> ../devices/virtual/scsi_mpath_disk/0/sdc
lrwxrwxrwx 1 root root 0 Feb 24 16:01 sdc:0 -> ../devices/platform/host8/session1/target8:0:0/8:0:0:0/block/sdc:0
lrwxrwxrwx 1 root root 0 Feb 24 16:02 sdc:1 -> ../devices/platform/host9/session2/target9:0:0/9:0:0:0/block/sdc:1
$ ls -l /sys/class/scsi_mpath_disk/scsi_mpath_disk0/
total 0
drwxr-xr-x 2 root root 0 Feb 24 16:03 power
drwxr-xr-x 11 root root 0 Feb 24 16:01 sdc
lrwxrwxrwx 1 root root 0 Feb 24 16:01 subsystem -> ../../../../class/scsi_mpath_disk
-rw-r--r-- 1 root root 4096 Feb 24 16:01 uevent
$ ls -l /sys/class/scsi_mpath_disk/scsi_mpath_disk0/sdc/multipath/
total 0
lrwxrwxrwx 1 root root 0 Feb 24 16:20 sdc:0 -> ../../../../../platform/host8/session1/target8:0:0/8:0:0:0/block/sdc:0
lrwxrwxrwx 1 root root 0 Feb 24 16:20 sdc:1 -> ../../../../../platform/host9/session2/target9:0:0/9:0:0:0/block/sdc:1
$ ls -l /dev/sdc*
brw-rw---- 1 root disk 8, 32 Feb 24 16:01 /dev/sdc
brw-rw---- 1 root disk 8, 33 Feb 24 16:01 /dev/sdc1
brw-rw---- 1 root disk 8, 34 Feb 24 16:01 /dev/sdc2
$ lsblk /dev/sdc
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sdc 8:32 0 600M 0 disk
|-sdc1 8:33 0 9M 0 part
`-sdc2 8:34 0 568M 0 part
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/sd.c | 426 +++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 406 insertions(+), 20 deletions(-)
diff --git a/drivers/scsi/sd.c b/drivers/scsi/sd.c
index fb87b1663ad6..ab4ce6c2a8dd 100644
--- a/drivers/scsi/sd.c
+++ b/drivers/scsi/sd.c
@@ -115,12 +115,30 @@ static mempool_t *sd_large_page_pool;
static atomic_t sd_large_page_pool_users = ATOMIC_INIT(0);
static struct lock_class_key sd_bio_compl_lkclass;
#ifdef CONFIG_SCSI_MULTIPATH
+static LIST_HEAD(sd_mpath_disks_list);
+static DEFINE_MUTEX(sd_mpath_disks_lock);
+
struct sd_mpath_disk {
+ struct device dev;
+ int disk_index;
+ int disk_count;
+ struct list_head entry;
struct scsi_mpath_head *scsi_mpath_head;
};
static void sd_mpath_disk_release(struct device *dev)
{
+ struct sd_mpath_disk *sd_mpath_disk =
+ container_of(dev, struct sd_mpath_disk, dev);
+ struct scsi_mpath_head *scsi_mpath_head =
+ sd_mpath_disk->scsi_mpath_head;
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+
+ mpath_put_disk(mpath_head);
+ ida_free(&sd_index_ida, sd_mpath_disk->disk_index);
+ scsi_mpath_put_head(scsi_mpath_head);
+
+ kfree(sd_mpath_disk);
}
static const struct class sd_mpath_disk_class = {
@@ -817,7 +835,8 @@ static void scsi_disk_release(struct device *dev)
{
struct scsi_disk *sdkp = to_scsi_disk(dev);
- ida_free(&sd_index_ida, sdkp->index);
+ if (sdkp->index >= 0)
+ ida_free(&sd_index_ida, sdkp->index);
put_device(&sdkp->device->sdev_gendev);
free_opal_dev(sdkp->opal_dev);
@@ -4021,6 +4040,90 @@ static int sd_format_disk_name(char *prefix, int index, char *buf, int buflen)
return 0;
}
+#ifdef CONFIG_SCSI_MULTIPATH
+static int sd_mpath_revalidate_head(struct scsi_disk *sdkp)
+{
+ struct sd_mpath_disk *sd_mpath_disk = sdkp->sd_mpath_disk;
+ struct scsi_mpath_head *scsi_mpath_head = sd_mpath_disk->scsi_mpath_head;
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+ struct gendisk *disk = mpath_head->disk;
+ struct queue_limits *sdkp_lim = &sdkp->disk->queue->limits;
+ struct queue_limits lim;
+ unsigned int memflags;
+ int ret;
+
+ lim = queue_limits_start_update(disk->queue);
+ memflags = blk_mq_freeze_queue(disk->queue);
+
+ lim.logical_block_size = sdkp_lim->logical_block_size;
+ lim.physical_block_size = sdkp_lim->physical_block_size;
+ lim.io_min = sdkp_lim->io_min;
+ lim.io_opt = sdkp_lim->io_opt;
+
+ queue_limits_stack_bdev(&lim, sdkp->disk->part0, 0,
+ disk->disk_name);
+
+ /* TODO: setup integrity and zoned limits */
+ lim.max_write_streams = sdkp_lim->max_write_streams;
+ lim.write_stream_granularity = sdkp_lim->write_stream_granularity;
+ ret = queue_limits_commit_update(disk->queue, &lim);
+
+ set_capacity_and_notify(disk, get_capacity(sdkp->disk));
+
+ mpath_revalidate_paths(mpath_head);
+
+ blk_mq_unfreeze_queue(disk->queue, memflags);
+
+ return ret;
+}
+
+static int sd_mpath_get_disk(struct sd_mpath_disk *sd_mpath_disk)
+{
+ if (!get_device(&sd_mpath_disk->dev))
+ return -ENXIO;
+ return 0;
+}
+
+static void sd_mpath_put_disk(struct sd_mpath_disk *sd_mpath_disk)
+{
+ put_device(&sd_mpath_disk->dev);
+}
+
+static struct sd_mpath_disk *sd_mpath_find_disk(
+ struct scsi_mpath_head *scsi_mpath_head)
+{
+ struct sd_mpath_disk *sd_mpath_disk;
+ int ret;
+
+ list_for_each_entry(sd_mpath_disk, &sd_mpath_disks_list, entry) {
+ ret = sd_mpath_get_disk(sd_mpath_disk);
+ if (ret)
+ continue;
+
+ if (sd_mpath_disk->scsi_mpath_head == scsi_mpath_head)
+ return sd_mpath_disk;
+
+ sd_mpath_put_disk(sd_mpath_disk);
+ }
+
+ return NULL;
+}
+
+static int sd_mpath_add_disk(struct scsi_disk *sdkp)
+{
+ struct scsi_device *sdp = sdkp->device;
+ struct scsi_mpath_device *scsi_mpath_dev = sdp->scsi_mpath_dev;
+ struct mpath_device *mpath_device = &scsi_mpath_dev->mpath_device;
+ int ret;
+
+ ret = mpath_add_device(mpath_device, sdkp->disk,
+ dev_to_node(sdp->host->dma_dev), &sdp->host->mpath_nr_active);
+ if (ret)
+ return ret;
+ mpath_device_set_live(mpath_device);
+ return 0;
+}
+
static ssize_t sd_mpath_device_delayed_removal_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
@@ -4050,13 +4153,260 @@ static const struct attribute_group sd_mpath_disk_attr_group = {
.attrs = sd_mpath_disk_attrs,
};
-__maybe_unused
static const struct attribute_group *sd_mpath_disk_attr_groups[] = {
&sd_mpath_disk_attr_group,
&mpath_attr_group,
NULL
};
+static int sd_mpath_probe(struct scsi_disk *sdkp)
+{
+ struct scsi_device *sdp = sdkp->device;
+ struct scsi_mpath_device *scsi_mpath_dev = sdp->scsi_mpath_dev;
+ struct device *dma_dev = sdp->host->dma_dev;
+ struct scsi_mpath_head *scsi_mpath_head =
+ scsi_mpath_dev->scsi_mpath_head;
+ struct sd_mpath_disk *sd_mpath_disk;
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+ char disk_name[DISK_NAME_LEN - 2];
+ struct queue_limits lim;
+ struct gendisk *disk;
+ int error;
+
+ /*
+ * sd_mpath_disks_list is kept locked if no disk found.
+ * Otherwise an extra reference is taken.
+ */
+ mutex_lock(&sd_mpath_disks_lock);
+ sd_mpath_disk = sd_mpath_find_disk(scsi_mpath_head);
+ if (sd_mpath_disk) {
+ error = sized_strscpy(disk_name, mpath_head->disk->disk_name,
+ sizeof(disk_name));
+ if (error < 0) {
+ /*
+ * Should not happen as would fail for the same when
+ * allocating the sd_mpath_disk
+ */
+ sd_mpath_put_disk(sd_mpath_disk);
+ mutex_unlock(&sd_mpath_disks_lock);
+ return error;
+ }
+ sd_mpath_disk->disk_count++;
+ mutex_unlock(&sd_mpath_disks_lock);
+
+ goto found;
+ }
+
+ sd_mpath_disk = kzalloc(sizeof(*sd_mpath_disk), GFP_KERNEL);
+ if (!sd_mpath_disk) {
+ error = -ENOMEM;
+ goto out_unlock;
+ }
+
+ sd_mpath_disk->scsi_mpath_head = scsi_mpath_head;
+
+ blk_set_stacking_limits(&lim);
+ lim.dma_alignment = 3;
+ lim.features |= BLK_FEAT_IO_STAT | BLK_FEAT_NOWAIT |
+ BLK_FEAT_POLL | BLK_FEAT_ATOMIC_WRITES;
+
+ error = mpath_alloc_head_disk(mpath_head, &lim,
+ dev_to_node(dma_dev));
+ if (error)
+ goto out_free_disk;
+ disk = mpath_head->disk;
+
+ error = ida_alloc(&sd_index_ida, GFP_KERNEL);
+ if (error < 0) {
+ sdev_printk(KERN_WARNING, sdp, "sd_mpath_probe: memory exhausted.\n");
+ goto out_put_disk;
+ }
+ sd_mpath_disk->disk_index = error;
+ error = sd_format_disk_name("sd", sd_mpath_disk->disk_index,
+ disk->disk_name, DISK_NAME_LEN);
+ if (error)
+ goto out_free_index;
+
+ error = sized_strscpy(disk_name, mpath_head->disk->disk_name,
+ sizeof(disk_name));
+ if (error < 0)
+ goto out_free_index;
+
+ device_initialize(&sd_mpath_disk->dev);
+ sd_mpath_disk->dev.class = &sd_mpath_disk_class;
+
+ /* undone in sd_mpath_disk_release() */
+ scsi_mpath_get_head(scsi_mpath_head);
+
+ error = dev_set_name(&sd_mpath_disk->dev, "scsi_mpath_disk%d",
+ scsi_mpath_head->index);
+ if (error) {
+ put_device(&sd_mpath_disk->dev);
+ goto out_unlock;
+ }
+
+ error = device_add(&sd_mpath_disk->dev);
+ if (error) {
+ put_device(&sd_mpath_disk->dev);
+ goto out_unlock;
+ }
+
+ mpath_head->parent = &sd_mpath_disk->dev;
+ mpath_head->drv_module = THIS_MODULE;
+ mpath_head->disk_groups = sd_mpath_disk_attr_groups;
+
+ list_add_tail(&sd_mpath_disk->entry, &sd_mpath_disks_list);
+ disk->major = sd_major((sd_mpath_disk->disk_index & 0xf0) >> 4);
+ disk->first_minor = ((sd_mpath_disk->disk_index & 0xf) << 4) |
+ (sd_mpath_disk->disk_index & 0xfff00);
+ disk->minors = SD_MINORS;
+
+ sd_mpath_disk->disk_count = 1;
+ mutex_unlock(&sd_mpath_disks_lock);
+found:
+ sdkp->sd_mpath_disk = sd_mpath_disk;
+ sdkp->disk->flags |= GENHD_FL_HIDDEN;
+ snprintf(sdkp->disk->disk_name, DISK_NAME_LEN, "%s:%d",
+ disk_name, scsi_mpath_dev->index);
+
+ sdkp->index = -1;
+ return 0;
+
+out_free_index:
+ ida_free(&sd_index_ida, sd_mpath_disk->disk_index);
+out_put_disk:
+ mpath_put_disk(mpath_head);
+out_free_disk:
+ kfree(sd_mpath_disk);
+out_unlock:
+ mutex_unlock(&sd_mpath_disks_lock);
+ return error;
+}
+
+static void sd_mpath_remove(struct scsi_disk *sdkp)
+{
+ struct sd_mpath_disk *sd_mpath_disk = sdkp->sd_mpath_disk;
+ struct scsi_device *sdp = sdkp->device;
+ struct scsi_mpath_device *scsi_mpath_dev = sdp->scsi_mpath_dev;
+ struct mpath_device *mpath_device = &scsi_mpath_dev->mpath_device;
+ struct scsi_mpath_head *scsi_mpath_head = sd_mpath_disk->scsi_mpath_head;
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+ bool remove = false;
+
+ mpath_synchronize(mpath_head);
+
+ if (mpath_clear_current_path(mpath_device))
+ mpath_synchronize(mpath_head);
+
+ mpath_delete_device(mpath_device);
+
+ mutex_lock(&sd_mpath_disks_lock);
+ sd_mpath_disk->disk_count--;
+ if (!sd_mpath_disk->disk_count && mpath_can_remove_head(mpath_head)) {
+ list_del_init(&sd_mpath_disk->entry);
+ remove = true;
+ }
+ mutex_unlock(&sd_mpath_disks_lock);
+ mpath_remove_sysfs_link(mpath_device);
+ mpath_device->disk = NULL;
+
+ if (remove) {
+ device_del(&sd_mpath_disk->dev);
+ mpath_remove_disk(mpath_head);
+ }
+ sd_mpath_put_disk(sd_mpath_disk);
+}
+
+static void sd_mpath_remove_head(struct scsi_mpath_head *scsi_mpath_head)
+{
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+ struct sd_mpath_disk *sd_mpath_disk;
+ struct device *dev = &scsi_mpath_head->dev;
+
+ mutex_lock(&sd_mpath_disks_lock);
+ sd_mpath_disk = sd_mpath_find_disk(scsi_mpath_head);
+ if (!sd_mpath_disk) {
+ dev_warn(dev, "could not find mpath disk\n");
+ mutex_unlock(&sd_mpath_disks_lock);
+ return;
+ }
+
+ if (sd_mpath_disk->disk_count) {
+ dev_dbg(dev, "non-zero multipath disk count in removal\n");
+ sd_mpath_put_disk(sd_mpath_disk);
+ mutex_unlock(&sd_mpath_disks_lock);
+ return;
+ }
+
+ list_del_init(&sd_mpath_disk->entry);
+ mutex_unlock(&sd_mpath_disks_lock);
+
+ device_del(&sd_mpath_disk->dev);
+ mpath_remove_disk(mpath_head);
+ sd_mpath_put_disk(sd_mpath_disk);
+}
+
+/*
+ * Always calls for a failed probe, so we need to handle that some structures
+ * have not been setup.
+ */
+static void sd_mpath_fail_probe(struct scsi_disk *sdkp)
+{
+ struct sd_mpath_disk *sd_mpath_disk = sdkp->sd_mpath_disk;
+ struct scsi_mpath_device *scsi_mpath_dev;
+ struct mpath_device *mpath_device;
+ struct scsi_device *sdp = sdkp->device;
+ struct scsi_mpath_head *scsi_mpath_head;
+ struct mpath_head *mpath_head;
+ bool remove = false;
+
+ if (!sd_mpath_disk)
+ return;
+
+ scsi_mpath_dev = sdp->scsi_mpath_dev;
+ mpath_device = &scsi_mpath_dev->mpath_device;
+ scsi_mpath_head = sd_mpath_disk->scsi_mpath_head;
+ mpath_head = &scsi_mpath_head->mpath_head;
+
+ mutex_lock(&sd_mpath_disks_lock);
+ sd_mpath_disk->disk_count--;
+ if (!sd_mpath_disk->disk_count) {
+ list_del_init(&sd_mpath_disk->entry);
+ remove = true;
+ }
+ mutex_unlock(&sd_mpath_disks_lock);
+ mpath_device->disk = NULL;
+
+ if (remove) {
+ device_del(&sd_mpath_disk->dev);
+ mpath_remove_disk(mpath_head);
+ }
+ sd_mpath_put_disk(sd_mpath_disk);
+}
+
+#else /* CONFIG_SCSI_MULTIPATH */
+static int sd_mpath_probe(struct scsi_disk *sdkp)
+{
+ return 0;
+}
+static void sd_mpath_remove(struct scsi_disk *sdkp)
+{
+ return;
+}
+static void sd_mpath_fail_probe(struct scsi_disk *sdkp)
+{
+
+}
+static int sd_mpath_revalidate_head(struct scsi_disk *sdkp)
+{
+ return 0;
+}
+static int sd_mpath_add_disk(struct scsi_disk *sdkp)
+{
+ return 0;
+}
+#endif
+
/**
* sd_probe - called during driver initialization and whenever a
* new scsi device is attached to the system. It is called once
@@ -4080,7 +4430,7 @@ static int sd_probe(struct scsi_device *sdp)
struct device *dev = &sdp->sdev_gendev;
struct scsi_disk *sdkp;
struct gendisk *gd;
- int index;
+ int index = -1;
int error;
scsi_autopm_get_device(sdp);
@@ -4109,22 +4459,33 @@ static int sd_probe(struct scsi_device *sdp)
&sd_bio_compl_lkclass);
if (!gd)
goto out_free;
+ sdkp->disk = gd;
+ sdkp->device = sdp;
- index = ida_alloc(&sd_index_ida, GFP_KERNEL);
- if (index < 0) {
- sdev_printk(KERN_WARNING, sdp, "sd_probe: memory exhausted.\n");
- goto out_put;
- }
+ if (sdp->scsi_mpath_dev) {
+ error = sd_mpath_probe(sdkp);
+ if (error)
+ goto out_put;
+ } else {
+ index = ida_alloc(&sd_index_ida, GFP_KERNEL);
+ if (index < 0) {
+ sdev_printk(KERN_WARNING, sdp, "sd_probe: memory exhausted.\n");
+ goto out_put;
+ }
- error = sd_format_disk_name("sd", index, gd->disk_name, DISK_NAME_LEN);
- if (error) {
- sdev_printk(KERN_WARNING, sdp, "SCSI disk (sd) name length exceeded.\n");
- goto out_free_index;
+ error = sd_format_disk_name("sd", index, gd->disk_name,
+ DISK_NAME_LEN);
+ if (error) {
+ sdev_printk(KERN_WARNING, sdp, "SCSI disk (sd) name length exceeded.\n");
+ goto out_free_index;
+ }
+ sdkp->index = index;
+
+ gd->major = sd_major((index & 0xf0) >> 4);
+ gd->first_minor = ((index & 0xf) << 4) | (index & 0xfff00);
+ gd->minors = SD_MINORS;
}
- sdkp->device = sdp;
- sdkp->disk = gd;
- sdkp->index = index;
sdkp->max_retries = SD_MAX_RETRIES;
atomic_set(&sdkp->openers, 0);
atomic_set(&sdkp->device->ioerr_cnt, 0);
@@ -4144,6 +4505,7 @@ static int sd_probe(struct scsi_device *sdp)
error = device_add(&sdkp->disk_dev);
if (error) {
+ sd_mpath_fail_probe(sdkp);
put_device(&sdkp->disk_dev);
put_disk(gd);
goto out;
@@ -4151,10 +4513,6 @@ static int sd_probe(struct scsi_device *sdp)
dev_set_drvdata(dev, sdkp);
- gd->major = sd_major((index & 0xf0) >> 4);
- gd->first_minor = ((index & 0xf) << 4) | (index & 0xfff00);
- gd->minors = SD_MINORS;
-
gd->fops = &sd_fops;
gd->private_data = sdkp;
@@ -4173,6 +4531,7 @@ static int sd_probe(struct scsi_device *sdp)
sd_revalidate_disk(gd);
if (sdp->sector_size > PAGE_SIZE) {
if (sd_large_pool_create()) {
+ sd_mpath_fail_probe(sdkp);
error = -ENOMEM;
device_unregister(&sdkp->disk_dev);
put_disk(gd);
@@ -4180,6 +4539,12 @@ static int sd_probe(struct scsi_device *sdp)
}
}
+ if (sdp->scsi_mpath_dev) {
+ error = sd_mpath_revalidate_head(sdkp);
+ if (error)
+ sdev_printk(KERN_WARNING, sdp, "could not revalidate multipath limits\n");
+ }
+
if (sdp->removable) {
gd->flags |= GENHD_FL_REMOVABLE;
gd->events |= DISK_EVENT_MEDIA_CHANGE;
@@ -4194,6 +4559,7 @@ static int sd_probe(struct scsi_device *sdp)
error = device_add_disk(dev, gd, NULL);
if (error) {
+ sd_mpath_fail_probe(sdkp);
device_unregister(&sdkp->disk_dev);
put_disk(gd);
if (sdp->sector_size > PAGE_SIZE)
@@ -4201,6 +4567,19 @@ static int sd_probe(struct scsi_device *sdp)
goto out;
}
+ if (sdp->scsi_mpath_dev) {
+ error = sd_mpath_add_disk(sdkp);
+ if (error) {
+ sd_mpath_fail_probe(sdkp);
+ device_del(&sdkp->disk_dev);
+ del_gendisk(sdkp->disk);
+
+ put_disk(sdkp->disk);
+ if (sdp->sector_size > PAGE_SIZE)
+ sd_large_pool_destroy();
+ goto out;
+ }
+ }
if (sdkp->security) {
sdkp->opal_dev = init_opal_dev(sdkp, &sd_sec_submit);
if (sdkp->opal_dev)
@@ -4214,7 +4593,8 @@ static int sd_probe(struct scsi_device *sdp)
return 0;
out_free_index:
- ida_free(&sd_index_ida, index);
+ if (index >= 0)
+ ida_free(&sd_index_ida, index);
out_put:
put_disk(gd);
out_free:
@@ -4348,6 +4728,9 @@ static void sd_remove(struct scsi_device *sdp)
if (scsi_autopm_get_device(sdkp->device) < 0)
blk_mark_disk_dead(sdkp->disk);
+ if (sdp->scsi_mpath_dev)
+ sd_mpath_remove(sdkp);
+
device_del(&sdkp->disk_dev);
del_gendisk(sdkp->disk);
if (!sdkp->suspended)
@@ -4514,6 +4897,9 @@ static struct scsi_driver sd_template = {
.resume = sd_resume,
.init_command = sd_init_command,
.uninit_command = sd_uninit_command,
+#ifdef CONFIG_SCSI_MULTIPATH
+ .mpath_remove_head = sd_mpath_remove_head,
+#endif
.done = sd_done,
.eh_action = sd_eh_action,
.eh_reset = sd_eh_reset,
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 26/29] scsi: sd: support multipath disk
2026-11-08 20:47 ` [PATCH v9 26/29] scsi: sd: support multipath disk John Garry
@ 2026-11-08 21:23 ` sashiko-bot
0 siblings, 0 replies; 44+ messages in thread
From: sashiko-bot @ 2026-11-08 21:23 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
Thank you for your contribution! Sashiko AI review found 7 potential
issues to consider:
- [High] scsi: sd: premature sd_mpath_remove() before del_gendisk() in
sd_remove()
- [High] scsi: sd: broken revalidation and state sync in
sd_mpath_revalidate_head()
- [Medium] scsi: sd: clear current_path after mpath_delete_device() in
sd_mpath_remove()
- [Medium] scsi: sd: premature sd_mpath_disk list unlink races with
sd_mpath_probe()
- [Medium] scsi: sd: uncancelled remove_work in sd_mpath_fail_probe()
causes UAF
- [Medium] scsi: sd: parent sd_mpath_disk->dev deleted before child
mpath_head->disk
- [Low] scsi: sd: unsigned sdkp->index makes >= 0 check always true
--
commit a00d9c64612f4369c0c2dd5d2bf64b95ed52be50
Author: John Garry <john.garry@linux.dev>
scsi: sd: support multipath disk
This commit adds support for attaching and managing SCSI multipath disks in
the sd driver, creating a multipath head gendisk while keeping per-path
gendisks hidden.
> diff --git a/drivers/scsi/sd.c b/drivers/scsi/sd.c
> index fb87b1663ad62..ab4ce6c2a8dd2 100644
> --- a/drivers/scsi/sd.c
> +++ b/drivers/scsi/sd.c
[ ... ]
> @@ -817,7 +835,8 @@ static void scsi_disk_release(struct device *dev)
> {
> struct scsi_disk *sdkp = to_scsi_disk(dev);
>
> - ida_free(&sd_index_ida, sdkp->index);
> + if (sdkp->index >= 0)
> + ida_free(&sd_index_ida, sdkp->index);
> put_device(&sdkp->device->sdev_gendev);
> free_opal_dev(sdkp->opal_dev);
>
> @@ -4021,6 +4040,90 @@ static int sd_format_disk_name(char *prefix, int index, char *buf, int buflen)
[Severity: Low]
Is sdkp->index >= 0 always true here?
struct scsi_disk declares index as u32 in drivers/scsi/sd.h, while
sd_mpath_probe() sets sdkp->index = -1. This triggers a -Wtype-limits
warning under W=1 and still calls ida_free(&sd_index_ida, 0xffffffffU) on
multipath per-path disks. Changing struct scsi_disk::index to int matches
sd_mpath_disk::disk_index and local index in sd_probe().
> return 0;
> }
>
> +#ifdef CONFIG_SCSI_MULTIPATH
> +static int sd_mpath_revalidate_head(struct scsi_disk *sdkp)
> +{
> + struct sd_mpath_disk *sd_mpath_disk = sdkp->sd_mpath_disk;
> + struct scsi_mpath_head *scsi_mpath_head = sd_mpath_disk->scsi_mpath_head;
> + struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
> + struct gendisk *disk = mpath_head->disk;
> + struct queue_limits *sdkp_lim = &sdkp->disk->queue->limits;
> + struct queue_limits lim;
> + unsigned int memflags;
> + int ret;
> +
> + lim = queue_limits_start_update(disk->queue);
> + memflags = blk_mq_freeze_queue(disk->queue);
> +
> + lim.logical_block_size = sdkp_lim->logical_block_size;
> + lim.physical_block_size = sdkp_lim->physical_block_size;
> + lim.io_min = sdkp_lim->io_min;
> + lim.io_opt = sdkp_lim->io_opt;
> +
> + queue_limits_stack_bdev(&lim, sdkp->disk->part0, 0,
> + disk->disk_name);
> +
> + /* TODO: setup integrity and zoned limits */
> + lim.max_write_streams = sdkp_lim->max_write_streams;
> + lim.write_stream_granularity = sdkp_lim->write_stream_granularity;
> + ret = queue_limits_commit_update(disk->queue, &lim);
> +
> + set_capacity_and_notify(disk, get_capacity(sdkp->disk));
> +
> + mpath_revalidate_paths(mpath_head);
> +
> + blk_mq_unfreeze_queue(disk->queue, memflags);
> +
> + return ret;
> +}
[Severity: High]
Can probing a standby ALUA path overwrite the multipath head disk capacity
with 0?
On ALUA arrays, standby or unavailable paths succeed INQUIRY VPD page 0x83
and attach to scsi_mpath_head, but fail READ CAPACITY in
sd_revalidate_disk(), leaving get_capacity(sdkp->disk) at 0. Calling
sd_mpath_revalidate_head() unconditionally from sd_probe() sets the live
mpath_head->disk capacity to 0, causing subsequent I/O on the multipath disk
to fail bio_check_eod() with -EIO.
In addition, sd_mpath_revalidate_head() has several other state sync issues:
- It is only called from sd_probe() and never from sd_revalidate_disk() or
sd_rescan(), so LUN resizes or standby-to-active transitions do not update
mpath_head->disk.
- It never calls set_disk_ro(disk, sdkp->write_prot), leaving write-protected
multipath disks openable for writes via mpath_ops.
- When queue_limits_commit_update() fails, sd_mpath_revalidate_head() still
updates capacity and paths, and sd_probe() only logs a warning before
calling sd_mpath_add_disk(sdkp) with uncommitted queue limits.
- For TYPE_ZBC disks, queue_limits_stack_bdev() inherits BLK_FEAT_ZONED onto
mpath_head->disk->queue even though zoned multipath support is not set up.
[ ... ]
> @@ -4050,13 +4153,260 @@ static const struct attribute_group sd_mpath_disk_attr_group = {
[ ... ]
> +static void sd_mpath_remove(struct scsi_disk *sdkp)
> +{
> + struct sd_mpath_disk *sd_mpath_disk = sdkp->sd_mpath_disk;
> + struct scsi_device *sdp = sdkp->device;
> + struct scsi_mpath_device *scsi_mpath_dev = sdp->scsi_mpath_dev;
> + struct mpath_device *mpath_device = &scsi_mpath_dev->mpath_device;
> + struct scsi_mpath_head *scsi_mpath_head = sd_mpath_disk->scsi_mpath_head;
> + struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
> + bool remove = false;
> +
> + mpath_synchronize(mpath_head);
> +
> + if (mpath_clear_current_path(mpath_device))
> + mpath_synchronize(mpath_head);
> +
> + mpath_delete_device(mpath_device);
[Severity: Medium]
Can mpath_device be re-cached into mpath_head->current_path[node] when
mpath_clear_current_path() runs before mpath_delete_device()?
When sd_remove() is triggered by driver unbind or module unload,
sdev->sdev_state remains SDEV_RUNNING, so scsi_mpath_is_disabled() returns
false. Because mpath_device is still on mpath_head->dev_list until
mpath_delete_device() unlinks it, a concurrent path lookup can repopulate
current_path[node]:
CPU0 CPU1
sd_mpath_remove()
mpath_clear_current_path(mpath_device)
mpath_bdev_submit_bio()
mpath_find_path()
__mpath_find_path()
rcu_assign_pointer(
current_path[node],
mpath_device)
mpath_delete_device(mpath_device)
...
mpath_device->disk = NULL;
mpath_bdev_submit_bio()
mpath_find_path()
/* returns cached mpath_device */
mpath_device->disk->part0
<-- NULL pointer dereference
Calling mpath_delete_device(mpath_device) before mpath_clear_current_path()
prevents __mpath_find_path() from finding mpath_device on dev_list after
current_path is cleared.
> +
> + mutex_lock(&sd_mpath_disks_lock);
> + sd_mpath_disk->disk_count--;
> + if (!sd_mpath_disk->disk_count && mpath_can_remove_head(mpath_head)) {
> + list_del_init(&sd_mpath_disk->entry);
> + remove = true;
> + }
> + mutex_unlock(&sd_mpath_disks_lock);
[Severity: Medium]
Can unlinking sd_mpath_disk and dropping sd_mpath_disks_lock before teardown
completes race with a concurrent sd_mpath_probe() for the same
scsi_mpath_head?
In sd_mpath_remove(), sd_mpath_remove_head(), and sd_mpath_fail_probe(),
sd_mpath_disk is removed from sd_mpath_disks_list and sd_mpath_disks_lock is
released before device_del(), mpath_remove_disk(), and sd_mpath_put_disk()
finish tearing down the shared mpath_head:
CPU0 CPU1
sd_mpath_remove()
mutex_lock(&sd_mpath_disks_lock)
list_del_init(&sd_mpath_disk->entry)
mutex_unlock(&sd_mpath_disks_lock)
sd_mpath_probe()
mutex_lock(&sd_mpath_disks_lock)
sd_mpath_find_disk() -> NULL
mpath_alloc_head_disk(mpath_head)
/* fails with -EINVAL, or races
* with old sd_mpath_disk_release()
* calling mpath_put_disk() */
device_del(&sd_mpath_disk->dev)
mpath_remove_disk(mpath_head)
sd_mpath_put_disk(sd_mpath_disk)
Depending on timing, mpath_alloc_head_disk() fails with -EINVAL because
mpath_head->disk is still non-NULL, device_add() fails with -EEXIST on the
duplicate "scsi_mpath_disk%d" name, or a delayed sd_mpath_disk_release()
calls mpath_put_disk(mpath_head) after the new probe has allocated a new
mpath_head->disk.
> + mpath_remove_sysfs_link(mpath_device);
> + mpath_device->disk = NULL;
> +
> + if (remove) {
> + device_del(&sd_mpath_disk->dev);
> + mpath_remove_disk(mpath_head);
> + }
> + sd_mpath_put_disk(sd_mpath_disk);
> +}
[Severity: Medium]
Does calling device_del(&sd_mpath_disk->dev) before
mpath_remove_disk(mpath_head) delete the parent device before its child
gendisk?
sd_mpath_probe() sets mpath_head->parent = &sd_mpath_disk->dev, and
mpath_device_set_live() registers mpath_head->disk as a child of that
device. In sd_mpath_remove(), sd_mpath_remove_head(), and
sd_mpath_fail_probe(), calling device_del(&sd_mpath_disk->dev) first clears
sd_mpath_disk->dev.kobj.parent and tears down the parent sysfs directory
while mpath_head->disk is still registered, causing del_gendisk() inside
mpath_remove_disk() to emit child KOBJ_REMOVE uevents with truncated
DEVPATHs. Also, in sd_mpath_remove(), mpath_remove_sysfs_link(mpath_device)
runs after dropping sd_mpath_disks_lock, so a concurrent last-path removal
or sd_mpath_remove_head() can remove mpath_head->disk while
mpath_remove_sysfs_link() is still accessing disk_to_dev(mpath_head->disk).
[ ... ]
> +static void sd_mpath_fail_probe(struct scsi_disk *sdkp)
> +{
> + struct sd_mpath_disk *sd_mpath_disk = sdkp->sd_mpath_disk;
> + struct scsi_mpath_device *scsi_mpath_dev;
> + struct mpath_device *mpath_device;
> + struct scsi_device *sdp = sdkp->device;
> + struct scsi_mpath_head *scsi_mpath_head;
> + struct mpath_head *mpath_head;
> + bool remove = false;
> +
> + if (!sd_mpath_disk)
> + return;
> +
> + scsi_mpath_dev = sdp->scsi_mpath_dev;
> + mpath_device = &scsi_mpath_dev->mpath_device;
> + scsi_mpath_head = sd_mpath_disk->scsi_mpath_head;
> + mpath_head = &scsi_mpath_head->mpath_head;
> +
> + mutex_lock(&sd_mpath_disks_lock);
> + sd_mpath_disk->disk_count--;
> + if (!sd_mpath_disk->disk_count) {
> + list_del_init(&sd_mpath_disk->entry);
> + remove = true;
> + }
> + mutex_unlock(&sd_mpath_disks_lock);
> + mpath_device->disk = NULL;
> +
> + if (remove) {
> + device_del(&sd_mpath_disk->dev);
> + mpath_remove_disk(mpath_head);
> + }
> + sd_mpath_put_disk(sd_mpath_disk);
> +}
[Severity: Medium]
Can sd_mpath_fail_probe() free scsi_mpath_head while mpath_head->remove_work
is still pending?
When delayed_removal_secs > 0 and the last path is removed in
sd_mpath_remove(), mpath_can_remove_head() schedules mpath_head->remove_work
and leaves sd_mpath_disk on sd_mpath_disks_list with disk_count == 0. If a
new path starts probing before remove_work fires, sd_mpath_probe() increments
disk_count to 1, but remove_work is not cancelled until mpath_add_device() in
sd_mpath_add_disk(). If sd_probe() fails earlier and calls
sd_mpath_fail_probe(), sd_mpath_disk is torn down without cancelling
remove_work:
CPU0 kworker
sd_mpath_remove() (last path)
mpath_can_remove_head()
mod_delayed_work(...,
&mpath_head->remove_work, ...)
sd_probe() (new path)
sd_mpath_probe()
sd_mpath_disk->disk_count++
device_add_disk() -> fails
sd_mpath_fail_probe()
sd_mpath_put_disk()
scsi_mpath_put_head()
scsi_mpath_remove_device()
scsi_mpath_put_head()
kfree(scsi_mpath_head)
mpath_remove_head_work()
sd_mpath_remove_head()
<-- use-after-free
[ ... ]
> @@ -4348,6 +4728,9 @@ static void sd_remove(struct scsi_device *sdp)
> if (scsi_autopm_get_device(sdkp->device) < 0)
> blk_mark_disk_dead(sdkp->disk);
>
> + if (sdp->scsi_mpath_dev)
> + sd_mpath_remove(sdkp);
> +
> device_del(&sdkp->disk_dev);
> del_gendisk(sdkp->disk);
> if (!sdkp->suspended)
[Severity: High]
Can calling sd_mpath_remove() before del_gendisk(sdkp->disk) cause a NULL
pointer dereference or use-after-free while REQ_MPATH requests are still
queued on sdkp->disk->queue?
Cloned multipath bios submitted to sdkp->disk->part0 remain queued or in
flight on sdkp->disk->queue until del_gendisk(sdkp->disk) freezes and drains
that queue. When the last path is removed, sd_mpath_remove() drops the last
reference to sd_mpath_disk->dev, and sd_mpath_disk_release() calls
mpath_put_disk(mpath_head) (setting mpath_head->disk = NULL) and frees
sd_mpath_disk before del_gendisk(sdkp->disk) runs:
CPU0 CPU1
sd_remove()
sd_mpath_remove(sdkp)
mpath_remove_disk(mpath_head)
sd_mpath_put_disk(sd_mpath_disk)
sd_mpath_disk_release()
mpath_put_disk(mpath_head)
mpath_head->disk = NULL;
kfree(sd_mpath_disk)
sd_init_command()
scsi_mpath_start_request(req)
disk = mpath_head->disk;
blk_queue_io_stat(disk->queue)
<-- NULL pointer dereference
del_gendisk(sdkp->disk)
Completing REQ_MPATH requests in scsi_mpath_end_request() or
scsi_mpath_clone_end_io() also access the released mpath_head->disk or
requeue work after mpath_put_disk() has already flushed it. Furthermore,
because __scsi_remove_device() still holds sdev->scsi_mpath_dev's reference
on scsi_mpath_head until after sd_remove() returns, scsi_mpath_put_head() in
sd_mpath_disk_release() does not wait in mpath_head_uninit() when the
multipath disk is still open, freeing sd_mpath_disk while mpath_head->parent
still points to &sd_mpath_disk->dev (used by dev_warn_ratelimited() in
mpath_bdev_submit_bio()).
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261108204733.557995-1-john.garry@linux.dev?part=26
^ permalink raw reply [flat|nested] 44+ messages in thread
* [PATCH v9 27/29] scsi: sd: add mpath_dev file
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (25 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 26/29] scsi: sd: support multipath disk John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 28/29] scsi: sd: add mpath_numa_nodes dev attribute John Garry
` (2 subsequent siblings)
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add a mpath_dev file so that the multipath disk can be looked up from
per-path scsi_disk directory.
The following is an example of this usage:
$ ls -l /dev/sdc
brw-rw---- 1 root disk 8, 32 Feb 24 16:08 /dev/sdc
$ cat /sys/class/scsi_mpath_disk/scsi_mpath_disk0/sdc/multipath/sdc:0/mpath_dev
8:32
This can be used by a util like lsscsi, which would find that the gendisk
for the per-path scsi_disk is missing.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/sd.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 74 insertions(+), 1 deletion(-)
diff --git a/drivers/scsi/sd.c b/drivers/scsi/sd.c
index ab4ce6c2a8dd..8469feb9ba5f 100644
--- a/drivers/scsi/sd.c
+++ b/drivers/scsi/sd.c
@@ -4124,6 +4124,77 @@ static int sd_mpath_add_disk(struct scsi_disk *sdkp)
return 0;
}
+static ssize_t sd_mpath_dev_show(struct device *dev,
+ struct device_attribute *attr, char *page)
+{
+ struct gendisk *gd = dev_to_disk(dev);
+ struct scsi_disk *sdkp = gd->private_data;
+ struct scsi_device *sdev = sdkp->device;
+ struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
+ struct scsi_mpath_head *scsi_mpath_head =
+ scsi_mpath_dev->scsi_mpath_head;
+ struct mpath_head *mpath_head = &scsi_mpath_head->mpath_head;
+ struct sd_mpath_disk *sd_mpath_disk;
+ struct gendisk *disk;
+ char devt_str[13];
+ ssize_t ret = -EINVAL;
+
+ /*
+ * This attr is for the per-path scsi_disk block device. We need to
+ * handle the scenario that the multipath disk has been deleted and
+ * the per-path scsi_disk (and its attr files) is still present.
+ */
+ mutex_lock(&sd_mpath_disks_lock);
+ sd_mpath_disk = sd_mpath_find_disk(scsi_mpath_head);
+ if (!sd_mpath_disk)
+ goto out_unlock;
+ if (!sd_mpath_disk->disk_count)
+ goto out_put_disk;
+
+ disk = mpath_head->disk;
+ /* A reference to this disk is held through sd_mpath_disk */
+ if (!disk || !test_bit(GD_ADDED, &disk->state))
+ goto out_put_disk;
+ format_dev_t(devt_str, disk_to_dev(disk)->devt);
+ ret = sysfs_emit(page, "%s\n", devt_str);
+out_put_disk:
+ sd_mpath_put_disk(sd_mpath_disk);
+out_unlock:
+ mutex_unlock(&sd_mpath_disks_lock);
+ return ret;
+}
+static DEVICE_ATTR(mpath_dev, 0444, sd_mpath_dev_show, NULL);
+
+static struct attribute *sd_mpath_dev_attrs[] = {
+ &dev_attr_mpath_dev.attr,
+ NULL
+};
+
+static umode_t sd_mpath_dev_attr_is_visible(struct kobject *kobj,
+ struct attribute *attr, int i)
+{
+ struct device *dev = kobj_to_dev(kobj);
+ struct gendisk *gd = dev_to_disk(dev);
+ struct scsi_disk *sdkp = gd->private_data;
+ struct scsi_device *sdev = sdkp->device;
+ struct scsi_mpath_device *scsi_mpath_device = sdev->scsi_mpath_dev;
+
+ if (!scsi_mpath_device)
+ return 0;
+
+ return attr->mode;
+}
+
+static const struct attribute_group sd_mpath_dev_attr_group = {
+ .is_visible = sd_mpath_dev_attr_is_visible,
+ .attrs = sd_mpath_dev_attrs,
+};
+
+static const struct attribute_group *sd_mpath_dev_groups[] = {
+ &sd_mpath_dev_attr_group,
+ NULL
+};
+
static ssize_t sd_mpath_device_delayed_removal_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
@@ -4405,6 +4476,8 @@ static int sd_mpath_add_disk(struct scsi_disk *sdkp)
{
return 0;
}
+
+#define sd_mpath_dev_groups NULL
#endif
/**
@@ -4557,7 +4630,7 @@ static int sd_probe(struct scsi_device *sdp)
sdp->host->rpm_autosuspend_delay);
}
- error = device_add_disk(dev, gd, NULL);
+ error = device_add_disk(dev, gd, sd_mpath_dev_groups);
if (error) {
sd_mpath_fail_probe(sdkp);
device_unregister(&sdkp->disk_dev);
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 28/29] scsi: sd: add mpath_numa_nodes dev attribute
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (26 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 27/29] scsi: sd: add mpath_dev file John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-08 20:47 ` [PATCH v9 29/29] scsi: sd: add mpath_queue_depth " John Garry
2026-11-09 20:19 ` [PATCH v9 00/29] Native SCSI Multipath support Laurence Oberman
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add an attribute to show multipath NUMA node per-path (scsi_disk).
The following is an example of reading the file:
$ cat /sys/devices/platform/host8/session1/target8:0:0/8:0:0:0/block/sdc:0/numa_
mpath_numa_nodes
0-3
$ cat /sys/devices/platform/host9/session2/target9:0:0/9:0:0:0/block/sdc:1/numa_
mpath_numa_nodes
$
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/sd.c | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/drivers/scsi/sd.c b/drivers/scsi/sd.c
index 8469feb9ba5f..5ea604c4bbb0 100644
--- a/drivers/scsi/sd.c
+++ b/drivers/scsi/sd.c
@@ -4165,8 +4165,22 @@ static ssize_t sd_mpath_dev_show(struct device *dev,
}
static DEVICE_ATTR(mpath_dev, 0444, sd_mpath_dev_show, NULL);
+static ssize_t sd_mpath_numa_nodes_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct gendisk *gd = dev_to_disk(dev);
+ struct scsi_disk *sdkp = gd->private_data;
+ struct scsi_device *sdev = sdkp->device;
+ struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
+ struct mpath_device *mpath_device = &scsi_mpath_dev->mpath_device;
+
+ return mpath_numa_nodes_show(mpath_device, buf);
+}
+static DEVICE_ATTR(mpath_numa_nodes, 0444, sd_mpath_numa_nodes_show, NULL);
+
static struct attribute *sd_mpath_dev_attrs[] = {
&dev_attr_mpath_dev.attr,
+ &dev_attr_mpath_numa_nodes.attr,
NULL
};
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* [PATCH v9 29/29] scsi: sd: add mpath_queue_depth dev attribute
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (27 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 28/29] scsi: sd: add mpath_numa_nodes dev attribute John Garry
@ 2026-11-08 20:47 ` John Garry
2026-11-09 20:19 ` [PATCH v9 00/29] Native SCSI Multipath support Laurence Oberman
29 siblings, 0 replies; 44+ messages in thread
From: John Garry @ 2026-11-08 20:47 UTC (permalink / raw)
To: james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie, John Garry
Add a queue_depth file so that the multipath dynamic queue depth can be
looked up from per-path gendisk (scsi_disk) directory.
Signed-off-by: John Garry <john.garry@linux.dev>
---
drivers/scsi/sd.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/drivers/scsi/sd.c b/drivers/scsi/sd.c
index 5ea604c4bbb0..29ac1ee806c5 100644
--- a/drivers/scsi/sd.c
+++ b/drivers/scsi/sd.c
@@ -4178,9 +4178,28 @@ static ssize_t sd_mpath_numa_nodes_show(struct device *dev,
}
static DEVICE_ATTR(mpath_numa_nodes, 0444, sd_mpath_numa_nodes_show, NULL);
+static ssize_t sd_mpath_queue_depth_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct gendisk *gd = dev_to_disk(dev);
+ struct scsi_disk *sdkp = gd->private_data;
+ struct scsi_device *sdev = sdkp->device;
+ struct scsi_mpath_device *scsi_mpath_dev = sdev->scsi_mpath_dev;
+ struct scsi_mpath_head *scsi_mpath_head =
+ scsi_mpath_dev->scsi_mpath_head;
+ struct Scsi_Host *shost = sdev->host;
+
+ if (!mpath_qd_iopolicy(&scsi_mpath_head->iopolicy))
+ return 0;
+
+ return sysfs_emit(buf, "%d\n", atomic_read(&shost->mpath_nr_active));
+}
+static DEVICE_ATTR(mpath_queue_depth, 0444, sd_mpath_queue_depth_show, NULL);
+
static struct attribute *sd_mpath_dev_attrs[] = {
&dev_attr_mpath_dev.attr,
&dev_attr_mpath_numa_nodes.attr,
+ &dev_attr_mpath_queue_depth.attr,
NULL
};
--
2.43.0
^ permalink raw reply related [flat|nested] 44+ messages in thread* Re: [PATCH v9 00/29] Native SCSI Multipath support
2026-11-08 20:47 [PATCH v9 00/29] Native SCSI Multipath support John Garry
` (28 preceding siblings ...)
2026-11-08 20:47 ` [PATCH v9 29/29] scsi: sd: add mpath_queue_depth " John Garry
@ 2026-11-09 20:19 ` Laurence Oberman
29 siblings, 0 replies; 44+ messages in thread
From: Laurence Oberman @ 2026-11-09 20:19 UTC (permalink / raw)
To: John Garry, james.bottomley, mkp
Cc: hare, linux-nvme, linux-scsi, snitzer, jmeneghi, bmarzins,
dm-devel, hch, michael.christie
On Sun, 2026-11-08 at 20:47 +0000, John Garry wrote:
> NATIVE SCSI MULTIPATH:
>
> This series introduces native SCSI multipath support. It is intended
> as
> an alternative to dm-mpath.
>
> This support aims to provide a multipath-enabled SCSI block device/
> gendisk.
>
> For a SCSI device to support native multipath, either of the
> following
> conditions must be satisfied:
> a. unique ID in VPD page 83 and ALUA support and scsi_multipath
> modparam
> enabled
> b. unique ID in VPD page 83 and scsi_multipath_always modparam
> enabled
>
> This series relies on reading sdev->access_state to get path
> information.
> This path information would be provided by ALUA. ALUA support which
> does
> not rely on device handlers has already been discussed at
> https://lore.kernel.org/linux-scsi/7755e98f-5619-48ba-bcfc-b64eec930c40@oracle.com/
> and support will be added in the next phase.
>
> New classes of devices are added:
> - scsi_mpath_device
> - scsi_mpath_disk
>
> These are required since a multipath scsi_device has no common scsi
> host.
> An example of the sysfs files and directories for these new classes
> is as
> follows:
>
> $ ls -l /sys/class/scsi_mpath_device/scsi_mpath_device0/
> total 0
> -rw-r--r-- 1 root root 4096 Feb 25 11:59 iopolicy
> drwxr-xr-x 2 root root 0 Feb 25 11:59 multipath
> drwxr-xr-x 2 root root 0 Feb 25 11:59 power
> lrwxrwxrwx 1 root root 0 Feb 25 11:59 subsystem ->
> ../../../../class/scsi_mpath_device
> -rw-r--r-- 1 root root 4096 Feb 25 11:58 uevent
> -r--r--r-- 1 root root 4096 Feb 25 11:59 vpd_id
> $ ls -l /sys/class/scsi_mpath_device/scsi_mpath_device0/multipath/
> total 0
> lrwxrwxrwx 1 root root 0 Feb 25 11:59 8:0:0:0 ->
> ../../../../platform/host8/session1/target8:0:0/8:0:0:0
> lrwxrwxrwx 1 root root 0 Feb 25 11:59 9:0:0:0 ->
> ../../../../platform/host9/session2/target9:0:0/9:0:0:0
> $ cat /sys/class/scsi_mpath_device/scsi_mpath_device0/vpd_id
> naa.600140505200a986f0043c9afa1fd077
> $ cat /sys/class/scsi_mpath_device/scsi_mpath_device0/iopolicy
> numa
> $
>
> $ ls -l /sys/class/scsi_mpath_disk/scsi_mpath_disk0/
> total 0
> drwxr-xr-x 2 root root 0 Feb 25 12:00 power
> drwxr-xr-x 11 root root 0 Feb 25 11:58 sdc
> lrwxrwxrwx 1 root root 0 Feb 25 11:58 subsystem ->
> ../../../../class/scsi_mpath_disk
> -rw-r--r-- 1 root root 4096 Feb 25 11:58 uevent
> $ ls -l /sys/class/scsi_mpath_disk/scsi_mpath_disk0/sdc/multipath/
> total 0
> lrwxrwxrwx 1 root root 0 Feb 25 12:00 sdc:0 ->
> ../../../../../platform/host8/session1/target8:0:0/8:0:0:0/block/sdc:
> 0
> lrwxrwxrwx 1 root root 0 Feb 25 12:00 sdc:1 ->
> ../../../../../platform/host9/session2/target9:0:0/9:0:0:0/block/sdc:
> 1
>
> $ ls -l /dev/sdc
> brw-rw---- 1 root disk 8, 32 Feb 25 11:58 /dev/sdc
>
> The scsi_device and scsi_disk classes otherwise remain unmodified.
> However, the per-path block device is hidden in /dev/. Furthermore,
> multipathed block devices have a new naming scheme, sdX:Y, where
> X is the scsi multipath device index and Y is the path index.
>
> No multipath sg support is added. We still have a per-path sg device.
> Since the SCSI block device is multipath enabled, we can access
> multipathed scsi_ioctl() through that block device.
>
> For failover, we take the approach of cloning bio's and re-submitting
> them
> in full (for failover errors).
>
> LIBMULTIPATH:
>
> This series introduces also libmultipath. It is essentially a
> refactoring
> of NVME multipath support, so we can have a common library to also
> support
> native SCSI multipath.
>
> Much of the code is taken directly from the NVMe multipath code.
> However,
> NVMe specifics are removed. A template structure is provided so the
> driver
> -may provide callbacks for driver specifics, like ANA support for
> NVMe.
>
> Important new structures introduced include:
>
> - mpath_head
> These contain much of the multipath-specific functionality from
> nvme_ns_head, including a pointer to the gendisk structure and
> a path SRCU-based array.
>
> - mpath_device
> This is the per-path structure, and contains much the same
> multipath-specific functionality in nvme_ns
>
> libmultipath provides functionality for path management, path
> selection,
> data path, and failover handling.
>
> Full series also available at
> https://github.com/johnpgarry/linux/tree/scsi-multipath-v7.4-v9
>
> Differences to v8:
> - Rebase
> - Hardening changes advised by sashiko, including "Clear cmd->flags"
> change
>
> Differences to v7:
> - rebase
> - simplify scsi_mpath_clone_end_io() (sashiko)
> - rename bio_list_add_clone() -> bio_list_add_master()
> - rework checks in mpath_alloc_disk_head() (sashiko)
>
> John Garry (29):
> scsi: core: Clear cmd->flags for terminal queuing error
> libmultipath: Add initial framework
> libmultipath: Add basic gendisk support
> libmultipath: Add path selection support
> libmultipath: Add bio handling
> libmultipath: Add support for mpath_device management
> libmultipath: Add delayed removal support
> libmultipath: Add sysfs helpers
> libmultipath: Add support for block device IOCTL
> libmultipath: Add mpath_bdev_getgeo()
> libmultipath: Add mpath_bdev_get_unique_id()
> scsi-multipath: introduce basic SCSI device support
> scsi-multipath: introduce scsi_device head structure
> scsi-multipath: provide sysfs link from to scsi_device
> scsi-multipath: support iopolicy
> scsi-multipath: clone each bio
> scsi-multipath: clear path when device is blocked
> scsi-multipath: revalidate paths upon device unblock
> scsi-multipath: failover handling
> scsi-multipath: provide callbacks for path state
> scsi-multipath: add scsi_mpath_{start,end}_request()
> scsi-multipath: update request for terminal request queuing error
> scsi-multipath: add delayed disk removal support
> scsi: sd: add multipath disk class
> scsi: sd: add multipath disk attr groups
> scsi: sd: support multipath disk
> scsi: sd: add mpath_dev file
> scsi: sd: add mpath_numa_nodes dev attribute
> scsi: sd: add mpath_queue_depth dev attribute
>
> drivers/scsi/Kconfig | 10 +
> drivers/scsi/Makefile | 1 +
> drivers/scsi/scsi.c | 8 +-
> drivers/scsi/scsi_lib.c | 20 +-
> drivers/scsi/scsi_multipath.c | 691 +++++++++++++++++++++++++
> drivers/scsi/scsi_scan.c | 4 +
> drivers/scsi/scsi_sysfs.c | 10 +
> drivers/scsi/sd.c | 611 +++++++++++++++++++++-
> drivers/scsi/sd.h | 3 +
> include/linux/multipath.h | 156 ++++++
> include/scsi/scsi_cmnd.h | 9 +-
> include/scsi/scsi_device.h | 2 +
> include/scsi/scsi_driver.h | 4 +
> include/scsi/scsi_host.h | 4 +
> include/scsi/scsi_multipath.h | 129 +++++
> lib/Kconfig | 6 +
> lib/Makefile | 2 +
> lib/multipath.c | 948
> ++++++++++++++++++++++++++++++++++
> 18 files changed, 2592 insertions(+), 26 deletions(-)
> create mode 100644 drivers/scsi/scsi_multipath.c
> create mode 100644 include/linux/multipath.h
> create mode 100644 include/scsi/scsi_multipath.h
> create mode 100644 lib/multipath.c
>
>
>
On 08/11/2026 20:47, John Garry wrote:
> This series relies on reading sdev->access_state to get path
information.
> This path information would be provided by ALUA. ALUA support which
does
> not rely on device handlers has already been discussed at
>
https://lore.kernel.org/linux-scsi/7755e98f-5619-48ba-bcfc-b64eec930c40@oracle.com/
> and support will be added in the next phase.
Hi John,
I have been testing v9 (your scsi-multipath-v7.4-v9 branch, on
7.4-rc1) on real hardware and found two problems with ALUA arrays,
one in scsi_dh_alua and one in the series itself.
Setup:
- HP P2000 G3 FC array, implicit ALUA, two port groups (one per
controller)
- 2 FC HBAs, 2 target ports each, 30 LUNs with 4 paths per LUN
- scsi_mod.multipath=on, dm-multipath not in use
- Under dm-multipath, the same LUNs show port group 1 at prio 50
(active/optimized) and port group 0 at prio 10
(active/non-optimized)
1. sdev->access_state is wrong after boot (scsi_dh_alua, not this
series)
After boot, every path read active/optimized in
/sys/class/scsi_device/H:C:T:L/device/access_state, while sg_rtpg
reports port group 0 as active/non-optimized. No
"alua: port group XX state ..." message was logged at all.
Since 50759b881e1d ("scsi: device_handler: alua: Call
scsi_device_put() from non-atomic context"), alua_rtpg_queue() only
queues an RTPG if sdev->handler_data is set, but alua_bus_attach()
sets handler_data after alua_initialize() has tried to queue the
initial RTPG. That RTPG is dropped, and access_state stays at its
zeroed default (active/optimized) until something calls
scsi_dh_activate(). dm-multipath hides this through pg_init; native
multipath never calls activate, so the series sees every path as
optimized.
I have sent a separate fix for this to linux-scsi:
https://lore.kernel.org/linux-scsi/20261109201120.1067815-1-loberman@redhat.com/
It is independent of your series, but without it, native multipath
on ALUA arrays has no correct path state.
2. ANO paths are ranked the same as AO paths
With the ALUA state corrected (by writing "activate" to dh_state, or
with the fix above), I/O still went to active/non-optimized paths.
scsi_multipath_sdev_init() sets mpath_device->access_state to
MPATH_STATE_OPTIMIZED and nothing updates it, while
__mpath_find_path(), mpath_round_robin_path() and
mpath_queue_depth_path() rank paths by that field. Only
scsi_mpath_is_optimized()/is_disabled() read sdev->access_state.
With numa, the first path in dev_list wins for every node, and
dev_list order follows sd probe completion, so for about a third of
the LUNs the current path was an ANO path. Example, LUN sdn, reads
completed per path for a 2000 x 1M O_DIRECT dd (:0/:3 AO, :1/:2 ANO
at the time):
sdn:0 0 sdn:1 2000 sdn:2 0 sdn:3 0
Every read went through the non-owning controller. From the code, for
these LUNs the numa recheck of is_optimized() fails on every I/O and
__mpath_find_path() picks the same ANO path again.
With the change below (a template callback so the selectors use the
live state), plus the scsi_dh_alua fix, all 30 LUNs pick an AO
current path at boot, and on LUN sdl (:0/:2 AO, :1/:3 ANO):
numa 2000 0 0 0
round-robin 1000 0 1000 0
queue-depth 18039 0 13961 0 (8 parallel dd, 64k)
No I/O reached the ANO paths under any policy. This is a test patch to
show the problem, not a polished change - feel free to fold it in or
do it differently.
3. Smaller points
- numa with equal NUMA distance uses one path per LUN, so with two AO
paths through two HBAs, half the optimized bandwidth is idle (sdl:2
above under numa). That matches NVMe, but users coming from
dm-multipath service-time will notice. Would round-robin or
queue-depth be a better default for SCSI?
- There are no Documentation/ABI entries for the new sysfs attributes
(scsi_mpath_device vpd_id/iopolicy/multipath, delayed_removal_secs,
mpath_dev, mpath_numa_nodes, mpath_queue_depth), and the
scsi_mod.multipath and multipath_iopolicy parameters are not in
kernel-parameters.txt. I have a draft ABI file if useful.
- There is no userspace tool yet to show heads, paths and ALUA state
(the equivalent of multipath -ll or nvme list-subsys). I used a
small sysfs script for this testing. Ben, is native SCSI multipath
support in multipath-tools planned?
- CONFIG_SCSI_MULTIPATH/CONFIG_LIBMULTIPATH are only enabled in your
branch's x86_64_defconfig, so building from a distro config leaves
them off, and scsi_mod.multipath= is silently ignored. Maybe worth a
note in the cover letter.
I have not tested failover or path loss yet; I will follow up with
that and a Tested-by once done.
Thanks,
Laurence
-- >8 --
From: Laurence Oberman <loberman@redhat.com>
Subject: [PATCH TEST] libmultipath/scsi-multipath: rank paths by live
ALUA access state
mpath_device->access_state is set to MPATH_STATE_OPTIMIZED in
scsi_multipath_sdev_init() and never updated, so the path selectors
rank active/non-optimized paths equal to active/optimized ones.
Add an optional mpath_head_template.access_state() callback, used by
the selectors in place of the cached field when provided, and
implement it for SCSI by mapping the live sdev->access_state.
No change notification is needed: numa rechecks is_optimized() per
I/O and re-runs __mpath_find_path() when the current path is no
longer optimized; round-robin and queue-depth walk the list per I/O.
Signed-off-by: Laurence Oberman <loberman@redhat.com>
---
diff --git a/drivers/scsi/scsi_multipath.c
b/drivers/scsi/scsi_multipath.c
index 58b0be88a..bdae08ecd 100644
--- a/drivers/scsi/scsi_multipath.c
+++ b/drivers/scsi/scsi_multipath.c
@@ -330,6 +330,23 @@ static bool scsi_mpath_is_optimized(struct
mpath_device *mpath_device)
return access_state == SCSI_ACCESS_STATE_OPTIMAL;
}
+static enum mpath_access_state scsi_mpath_access_state(
+ struct mpath_device *mpath_device)
+{
+ struct scsi_mpath_device *scsi_mpath_dev =
+ to_scsi_mpath_device(mpath_device);
+ struct scsi_device *sdev = scsi_mpath_dev->sdev;
+
+ switch (READ_ONCE(sdev->access_state) &
SCSI_ACCESS_STATE_MASK) {
+ case SCSI_ACCESS_STATE_OPTIMAL:
+ return MPATH_STATE_OPTIMIZED;
+ case SCSI_ACCESS_STATE_ACTIVE:
+ return MPATH_STATE_NONOPTIMIZED;
+ default:
+ return MPATH_STATE_OTHER;
+ }
+}
+
static bool scsi_mpath_available_path(struct mpath_device
*mpath_device)
{
struct scsi_mpath_device *scsi_mpath_dev =
@@ -374,6 +391,7 @@ static struct mpath_head_template smpdt = {
.remove_head = scsi_mpath_remove_head_work,
.is_disabled = scsi_mpath_is_disabled,
.is_optimized = scsi_mpath_is_optimized,
+ .access_state = scsi_mpath_access_state,
.available_path = scsi_mpath_available_path,
.clone_bio = scsi_mpath_clone_bio,
};
diff --git a/include/linux/multipath.h b/include/linux/multipath.h
index 7a3708ff3..b47785804 100644
--- a/include/linux/multipath.h
+++ b/include/linux/multipath.h
@@ -39,6 +39,7 @@ struct mpath_head_template {
void (*remove_head)(struct mpath_head *);
bool (*is_disabled)(struct mpath_device *);
bool (*is_optimized)(struct mpath_device *);
+ enum mpath_access_state (*access_state)(struct mpath_device
*);
struct bio *(*clone_bio)(struct bio *);
const struct attribute_group **device_groups;
};
diff --git a/lib/multipath.c b/lib/multipath.c
index b03080afb..3ddb7296a 100644
--- a/lib/multipath.c
+++ b/lib/multipath.c
@@ -142,6 +142,19 @@ static bool mpath_path_is_disabled(struct
mpath_head *mpath_head,
return mpath_head->mpdt->is_disabled(mpath_device);
}
+/*
+ * Use the driver's live view of the path access state if it provides
one,
+ * otherwise fall back to the cached mpath_device->access_state.
+ */
+static enum mpath_access_state mpath_path_access_state(
+ struct mpath_head *mpath_head,
+ struct mpath_device *mpath_device)
+{
+ if (mpath_head->mpdt->access_state)
+ return mpath_head->mpdt->access_state(mpath_device);
+ return mpath_device->access_state;
+}
+
static struct mpath_device *__mpath_find_path(struct mpath_head
*mpath_head,
int node)
{
@@ -161,7 +174,7 @@ static struct mpath_device
*__mpath_find_path(struct mpath_head *mpath_head,
else
distance = LOCAL_DISTANCE;
- switch(mpath_device->access_state) {
+ switch (mpath_path_access_state(mpath_head,
mpath_device)) {
case MPATH_STATE_OPTIMIZED:
if (distance < found_distance) {
found_distance = distance;
@@ -206,7 +219,7 @@ static struct mpath_device *mpath_round_robin_path(
{
struct mpath_device *mpath_device, *found = NULL;
int node = numa_node_id();
- enum mpath_access_state access_state_old;
+ enum mpath_access_state access_state_old, state;
struct mpath_device *old =
srcu_dereference(mpath_head-
>current_path[node],
&mpath_head->srcu);
@@ -226,11 +239,12 @@ static struct mpath_device
*mpath_round_robin_path(
if (mpath_path_is_disabled(mpath_head, mpath_device))
continue;
- if (mpath_device->access_state ==
MPATH_STATE_OPTIMIZED) {
+ state = mpath_path_access_state(mpath_head,
mpath_device);
+ if (state == MPATH_STATE_OPTIMIZED) {
found = mpath_device;
goto out;
}
- if (mpath_device->access_state ==
MPATH_STATE_NONOPTIMIZED)
+ if (state == MPATH_STATE_NONOPTIMIZED)
found = mpath_device;
}
@@ -240,7 +254,7 @@ static struct mpath_device *mpath_round_robin_path(
* - no other optimized path found and current is optimized,
* - no other usable path found and current is usable.
*/
- access_state_old = old->access_state;
+ access_state_old = mpath_path_access_state(mpath_head, old);
if (!mpath_path_is_disabled(mpath_head, old) &&
(access_state_old == MPATH_STATE_OPTIMIZED ||
(!found && access_state_old == MPATH_STATE_NONOPTIMIZED)))
@@ -270,7 +284,7 @@ static struct mpath_device *mpath_queue_depth_path(
depth = atomic_read(mpath_device->nr_active);
- switch (mpath_device->access_state) {
+ switch (mpath_path_access_state(mpath_head,
mpath_device)) {
case MPATH_STATE_OPTIMIZED:
if (depth < min_depth_opt) {
min_depth_opt = depth;
^ permalink raw reply related [flat|nested] 44+ messages in thread