[PATCH v2 1/3] nvme-multipath: implement service-time iopolicy

sashiko-bot at kernel.org sashiko-bot at kernel.org
Mon Oct 19 01:14:50 PDT 2026


Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] nvme-multipath: in_flight_size leak in nvme_mpath_end_request() on failover
- [Medium] nvme-multipath: in_flight_size underflow wraps in nvme_st_compare_load()
--

commit 211891d7cca0785689865f3a7650f5305abaef90
Author: Guixin Liu <kanie at linux.alibaba.com>

nvme-multipath: implement service-time iopolicy

This commit adds a service-time multipath I/O policy that selects paths
based on the ratio of in-flight bytes to a configurable relative throughput
weight, exposing in_flight_bytes and relative_throughput sysfs attributes.

> diff --git a/drivers/nvme/host/multipath.c b/drivers/nvme/host/multipath.c
> index 75dbb58286a32..3901654d8226b 100644
> --- a/drivers/nvme/host/multipath.c
> +++ b/drivers/nvme/host/multipath.c
[ ... ]
> @@ -179,11 +180,16 @@ void nvme_mpath_start_request(struct request *rq)
>  {
>  	struct nvme_ns *ns = rq->q->queuedata;
>  	struct gendisk *disk = ns->head->disk;
> +	int iopolicy = READ_ONCE(ns->head->subsys->iopolicy);
>  
> -	if ((READ_ONCE(ns->head->subsys->iopolicy) == NVME_IOPOLICY_QD) &&
> +	if (iopolicy == NVME_IOPOLICY_QD &&
>  	    !(nvme_req(rq)->flags & NVME_MPATH_CNT_ACTIVE)) {
>  		atomic_inc(&ns->ctrl->nr_active);
>  		nvme_req(rq)->flags |= NVME_MPATH_CNT_ACTIVE;
> +	} else if (iopolicy == NVME_IOPOLICY_ST &&
> +		   !(nvme_req(rq)->flags & NVME_MPATH_CNT_ACTIVE_BYTES)) {
> +		atomic64_add(blk_rq_bytes(rq), &ns->ctrl->in_flight_size);
> +		nvme_req(rq)->flags |= NVME_MPATH_CNT_ACTIVE_BYTES;
>  	}
>  
>  	if (!blk_queue_io_stat(disk->queue) ||
> @@ -205,6 +211,10 @@ void nvme_mpath_end_request(struct request *rq)
>  
>  	if (nvme_req(rq)->flags & NVME_MPATH_CNT_ACTIVE)
>  		atomic_dec_if_positive(&ns->ctrl->nr_active);
> +	if (nvme_req(rq)->flags & NVME_MPATH_CNT_ACTIVE_BYTES) {
> +		atomic64_sub(blk_rq_bytes(rq), &ns->ctrl->in_flight_size);
> +		nvme_req(rq)->flags &= ~NVME_MPATH_CNT_ACTIVE_BYTES;
> +	}

[Severity: Medium]
Can this leak bytes in ns->ctrl->in_flight_size when a request fails over to
another path?

When a request fails with a path error (such as NVME_SC_ANA_TRANSITION,
NVME_SC_ANA_INACCESSIBLE, NVME_SC_ANA_PERSISTENT_LOSS,
NVME_SC_HOST_PATH_ERROR, or NVME_SC_HOST_ABORTED_CMD) or on a dying queue,
__nvme_complete_rq() calls nvme_failover_req():

drivers/nvme/host/multipath.c:nvme_failover_req() {
	...
	spin_lock_irqsave(&ns->head->requeue_lock, flags);
	for (bio = req->bio; bio; bio = bio->bi_next)
		bio_set_dev(bio, ns->head->disk->part0);
	blk_steal_bios(&ns->head->requeue_list, req);
	spin_unlock_irqrestore(&ns->head->requeue_lock, flags);

	nvme_req(req)->status = 0;
	nvme_end_req(req);
}

blk_steal_bios() detaches all bios from the request and sets
req->__data_len = 0 before nvme_end_req() -> __nvme_end_req() ->
nvme_mpath_end_request() is called:

block/blk-mq.c:blk_steal_bios() {
	...
	rq->bio = NULL;
	rq->biotail = NULL;
	rq->__data_len = 0;
}

When nvme_mpath_end_request() subsequently calls blk_rq_bytes(rq), it reads
rq->__data_len as 0 and subtracts 0 from ns->ctrl->in_flight_size while
clearing NVME_MPATH_CNT_ACTIVE_BYTES. Whenever failover occurs without a
controller reset (for example during an ANA state transition, or on a
multipath controller without NVME_CTRL_CMIC_ANA where
nvme_mpath_init_identify() returns early), those bytes remain permanently
counted in ns->ctrl->in_flight_size, skewing path selection in
nvme_service_time_path() and leaving in_flight_bytes non-zero when idle.

>  
>  	if (!(nvme_req(rq)->flags & NVME_MPATH_IO_STATS))
>  		return;
> @@ -464,6 +474,109 @@ static struct nvme_ns *nvme_queue_depth_path(struct nvme_ns_head *head)
>  	return best_opt ? best_opt : best_nonopt;
>  }
>  
> +#define ST_MAX_RELATIVE_THROUGHPUT	100
> +#define ST_MAX_RELATIVE_THROUGHPUT_SHIFT	7
> +#define ST_MAX_INFLIGHT_SIZE \
> +	((size_t)-1 >> ST_MAX_RELATIVE_THROUGHPUT_SHIFT)
> +
> +/*
> + * Compare estimated service time of two paths.
> + * Returns negative if ns1 is better, positive if ns2 is better, 0 if equal.
> + *
> + * Service time = in_flight_size / relative_throughput
> + * Cross-multiply to avoid division.
> + */
> +static int nvme_st_compare_load(struct nvme_ns *ns1, struct nvme_ns *ns2)
> +{
> +	size_t sz1, sz2, st1, st2;
> +	unsigned int tp1 = ns1->ctrl->relative_throughput;
> +	unsigned int tp2 = ns2->ctrl->relative_throughput;
> +
> +	sz1 = atomic64_read(&ns1->ctrl->in_flight_size);
> +	sz2 = atomic64_read(&ns2->ctrl->in_flight_size);

[Severity: Medium]
What happens here if ns->ctrl->in_flight_size underflows below zero after a
controller reset?

When a request is started under NVME_IOPOLICY_ST, nvme_setup_cmd() calls
nvme_clear_nvme_request() (which sets RQF_DONTPREP), and
nvme_mpath_start_request() adds blk_rq_bytes(rq) to ns->ctrl->in_flight_size
and sets NVME_MPATH_CNT_ACTIVE_BYTES.

If that request is then placed onto q->requeue_list in MQ_RQ_IDLE state
without calling nvme_mpath_end_request() (either via nvme_retry_req() ->
blk_mq_requeue_request(), or when a transport queue_rq callback such as
nvme_rdma_queue_rq(), nvme_fc_start_fcp_op(), or nvme_loop_queue_rq()
returns BLK_STS_RESOURCE after nvme_start_request()) and a controller reset
occurs:

1. nvme_cancel_tagset() -> nvme_cancel_request() skips the request because
   blk_mq_rq_state(req) != MQ_RQ_IN_FLIGHT.
2. nvme_mpath_init_identify() resets ctrl->in_flight_size to 0 via
   atomic64_set(&ctrl->in_flight_size, 0).
3. When the queue is unquiesced and the request is re-dispatched,
   nvme_setup_cmd() preserves nvme_req(req)->flags because RQF_DONTPREP is
   set, and nvme_mpath_start_request() skips adding blk_rq_bytes(rq) because
   NVME_MPATH_CNT_ACTIVE_BYTES is already set.
4. When the request completes, nvme_mpath_end_request() unconditionally
   executes atomic64_sub(blk_rq_bytes(rq), &ns->ctrl->in_flight_size),
   driving ctrl->in_flight_size negative (unlike ctrl->nr_active, which uses
   atomic_dec_if_positive()).

In nvme_st_compare_load(), the negative s64 returned by atomic64_read() is
assigned to the unsigned size_t variables sz1 and sz2, wrapping to a large
positive value near SIZE_MAX and causing nvme_service_time_path() to treat
the path as heavily overloaded until enough new I/O leaks or resets it.
Should in_flight_size be prevented from going negative or clamped when read?

[ ... ]
> @@ -1453,6 +1610,7 @@ int nvme_mpath_init_identify(struct nvme_ctrl *ctrl, struct nvme_id_ctrl *id)
>  
>  	/* initialize this in the identify path to cover controller resets */
>  	atomic_set(&ctrl->nr_active, 0);
> +	atomic64_set(&ctrl->in_flight_size, 0);
>  
>  	if (!ctrl->max_namespaces ||
>  	    ctrl->max_namespaces > le32_to_cpu(id->nn)) {
[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261019075607.808947-1-kanie@linux.alibaba.com?part=1



More information about the Linux-nvme mailing list