VYPR
Unrated severityNVD Advisory· Published May 28, 2026

CVE-2026-46135

CVE-2026-46135

Description

In the Linux kernel, the following vulnerability has been resolved:

nvmet-tcp: fix race between ICReq handling and queue teardown

nvmet_tcp_handle_icreq() updates queue->state after sending an Initialization Connection Response (ICResp), but it does so without serializing against target-side queue teardown.

If an NVMe/TCP host sends an Initialization Connection Request (ICReq) and immediately closes the connection, target-side teardown may start in softirq context before io_work drains the already buffered ICReq. In that case, nvmet_tcp_schedule_release_queue() sets queue->state to NVMET_TCP_Q_DISCONNECTING and drops the queue reference under state_lock.

If io_work later processes that ICReq, nvmet_tcp_handle_icreq() can still overwrite the state back to NVMET_TCP_Q_LIVE. That defeats the DISCONNECTING-state guard in nvmet_tcp_schedule_release_queue() and allows a later socket state change to re-enter teardown and issue a second kref_put() on an already released queue.

The ICResp send failure path has the same problem. If teardown has already moved the queue to DISCONNECTING, a send error can still overwrite the state with NVMET_TCP_Q_FAILED, again reopening the window for a second teardown path to drop the queue reference.

Fix this by serializing both post-send state transitions with state_lock and bailing out if teardown has already started.

Use -ESHUTDOWN as an internal sentinel for that bail-out path rather than propagating it as a transport error like -ECONNRESET. Keep nvmet_tcp_socket_error() setting rcv_state to NVMET_TCP_RECV_ERR before honoring that sentinel so receive-side parsing stays quiesced until the existing release path completes.

Affected products

1

Patches

8
67e1aaf93b49

nvmet-tcp: fix race between ICReq handling and queue teardown

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitChaitanya KulkarniFixed in 6.18.30via kernel-cna
1 file changed · +26 1
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index 5c8d17bcc49bd5..3d8810b42e9dc6 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -398,6 +398,19 @@ static void nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (queue->nvme_sq.ctrl)
     		nvmet_ctrl_fatal_error(queue->nvme_sq.ctrl);
    @@ -923,11 +936,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
dcfe4d1f7960

nvmet-tcp: fix race between ICReq handling and queue teardown

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitChaitanya KulkarniFixed in 7.0.7via kernel-cna
2 files changed · +52 2
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index acc71a26733f90..255ebd948dfe1b 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -398,6 +398,19 @@ static void nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (queue->nvme_sq.ctrl)
     		nvmet_ctrl_fatal_error(queue->nvme_sq.ctrl);
    @@ -922,11 +935,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index acc71a26733f90..255ebd948dfe1b 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -398,6 +398,19 @@ static void nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (queue->nvme_sq.ctrl)
     		nvmet_ctrl_fatal_error(queue->nvme_sq.ctrl);
    @@ -922,11 +935,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
5293a8882c54

nvmet-tcp: fix race between ICReq handling and queue teardown

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitChaitanya KulkarniFixed in 7.1-rc2via kernel-cna
2 files changed · +52 2
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index a4c3c62e33f57d..164a564ba3b4e9 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -394,6 +394,19 @@ static int nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_socket_error(struct nvmet_tcp_queue *queue, int status)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (status == -EPIPE || status == -ECONNRESET || !queue->nvme_sq.ctrl)
     		kernel_sock_shutdown(queue->sock, SHUT_RDWR);
    @@ -908,11 +921,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index a4c3c62e33f57d..164a564ba3b4e9 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -394,6 +394,19 @@ static int nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_socket_error(struct nvmet_tcp_queue *queue, int status)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (status == -EPIPE || status == -ECONNRESET || !queue->nvme_sq.ctrl)
     		kernel_sock_shutdown(queue->sock, SHUT_RDWR);
    @@ -908,11 +921,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
49891c8fe0cb

nvmet-tcp: fix race between ICReq handling and queue teardown

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitChaitanya KulkarniFixed in 6.12.88via kernel-cna
1 file changed · +26 1
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index 0ca261cb1823c5..0e921988130577 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -406,6 +406,19 @@ static void nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (queue->nvme_sq.ctrl)
     		nvmet_ctrl_fatal_error(queue->nvme_sq.ctrl);
    @@ -962,11 +975,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
5293a8882c54

nvmet-tcp: fix race between ICReq handling and queue teardown

1 file changed · +26 1
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index a4c3c62e33f57d..164a564ba3b4e9 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -394,6 +394,19 @@ static int nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_socket_error(struct nvmet_tcp_queue *queue, int status)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (status == -EPIPE || status == -ECONNRESET || !queue->nvme_sq.ctrl)
     		kernel_sock_shutdown(queue->sock, SHUT_RDWR);
    @@ -908,11 +921,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
67e1aaf93b49

nvmet-tcp: fix race between ICReq handling and queue teardown

1 file changed · +26 1
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index 5c8d17bcc49bd5..3d8810b42e9dc6 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -398,6 +398,19 @@ static void nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (queue->nvme_sq.ctrl)
     		nvmet_ctrl_fatal_error(queue->nvme_sq.ctrl);
    @@ -923,11 +936,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
dcfe4d1f7960

nvmet-tcp: fix race between ICReq handling and queue teardown

1 file changed · +26 1
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index acc71a26733f90..255ebd948dfe1b 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -398,6 +398,19 @@ static void nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (queue->nvme_sq.ctrl)
     		nvmet_ctrl_fatal_error(queue->nvme_sq.ctrl);
    @@ -922,11 +935,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    
49891c8fe0cb

nvmet-tcp: fix race between ICReq handling and queue teardown

1 file changed · +26 1
  • drivers/nvme/target/tcp.c+26 1 modified
    diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c
    index 0ca261cb1823c5..0e921988130577 100644
    --- a/drivers/nvme/target/tcp.c
    +++ b/drivers/nvme/target/tcp.c
    @@ -406,6 +406,19 @@ static void nvmet_tcp_build_pdu_iovec(struct nvmet_tcp_cmd *cmd)
     
     static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
     {
    +	/*
    +	 * Keep rcv_state at RECV_ERR even for the internal -ESHUTDOWN path.
    +	 * nvmet_tcp_handle_icreq() can return -ESHUTDOWN after the ICReq has
    +	 * already been consumed and queue teardown has started.
    +	 *
    +	 * If nvmet_tcp_data_ready() or nvmet_tcp_write_space() queues
    +	 * nvmet_tcp_io_work() again before nvmet_tcp_release_queue_work()
    +	 * cancels it, the queue must not keep that old receive state.
    +	 * Otherwise the next nvmet_tcp_io_work() run can reach
    +	 * nvmet_tcp_done_recv_pdu() and try to handle the same ICReq again.
    +	 *
    +	 * That is why queue->rcv_state needs to be updated before we return.
    +	 */
     	queue->rcv_state = NVMET_TCP_RECV_ERR;
     	if (queue->nvme_sq.ctrl)
     		nvmet_ctrl_fatal_error(queue->nvme_sq.ctrl);
    @@ -962,11 +975,24 @@ static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue)
     	iov.iov_len = sizeof(*icresp);
     	ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
     	if (ret < 0) {
    +		spin_lock_bh(&queue->state_lock);
    +		if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +			spin_unlock_bh(&queue->state_lock);
    +			return -ESHUTDOWN;
    +		}
     		queue->state = NVMET_TCP_Q_FAILED;
    +		spin_unlock_bh(&queue->state_lock);
     		return ret; /* queue removal will cleanup */
     	}
     
    +	spin_lock_bh(&queue->state_lock);
    +	if (queue->state == NVMET_TCP_Q_DISCONNECTING) {
    +		spin_unlock_bh(&queue->state_lock);
    +		/* Tell nvmet_tcp_socket_error() teardown is in progress. */
    +		return -ESHUTDOWN;
    +	}
     	queue->state = NVMET_TCP_Q_LIVE;
    +	spin_unlock_bh(&queue->state_lock);
     	nvmet_prepare_receive_pdu(queue);
     	return 0;
     }
    -- 
    cgit 1.3-korg
    
    
    

Vulnerability mechanics

Root cause

"Missing serialization of queue->state transitions in nvmet_tcp_handle_icreq() allows a race where teardown sets DISCONNECTING but the ICReq handler overwrites it back to LIVE or FAILED, leading to a double kref_put on the queue."

Attack vector

An NVMe/TCP host sends an Initialization Connection Request (ICReq) and immediately closes the connection. The target-side teardown may start in softirq context before io_work drains the already buffered ICReq. In that case, nvmet_tcp_schedule_release_queue() sets queue->state to NVMET_TCP_Q_DISCONNECTING and drops the queue reference under state_lock. If io_work later processes that ICReq, nvmet_tcp_handle_icreq() can overwrite the state back to NVMET_TCP_Q_LIVE (or NVMET_TCP_Q_FAILED on send error), defeating the DISCONNECTING-state guard and allowing a second teardown path to issue a second kref_put() on an already released queue [patch_id=2898403].

Affected code

The race is in drivers/nvme/target/tcp.c, in the function nvmet_tcp_handle_icreq(). The function updates queue->state after sending ICResp without holding state_lock, and the teardown path in nvmet_tcp_schedule_release_queue() (or nvmet_tcp_fatal_error() in some kernel versions) also manipulates queue->state and queue->rcv_state [patch_id=2898403].

What the fix does

The patch serializes both post-send state transitions in nvmet_tcp_handle_icreq() with state_lock and bails out with -ESHUTDOWN if teardown has already moved the queue to NVMET_TCP_Q_DISCONNECTING [patch_id=2898403]. On the send-failure path, the same check is added before setting NVMET_TCP_Q_FAILED. Additionally, nvmet_tcp_socket_error() (or nvmet_tcp_fatal_error() in backport variants) now always sets queue->rcv_state to NVMET_TCP_RECV_ERR before returning, so that even when -ESHUTDOWN is returned, a subsequent io_work run cannot re-process the already-consumed ICReq [patch_id=2898403].

Preconditions

  • networkThe attacker must be able to establish an NVMe/TCP connection to the target and send an ICReq.
  • inputThe attacker must immediately close the connection after sending the ICReq, triggering teardown in softirq context before io_work processes the ICReq.

Generated on May 28, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.