Skip to contentSkip to conversation

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

PATCH Applied Chaitanya Kulkarni 4 participants 5 messages Apr 8 – Apr 9 1 message not in the archive

Subsystems (3)
lore
Share & tools
lore

Chaitanya Kulkarni

author maintainer
#0 lore raw

PATCH 1/1 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.

Fixes c46a6465bac2 ("nvmet-tcp: add NVMe over TCP target driver")

---

Hi Shivam,

This patch is different than the one I posted.

Posted patch :-

		iov.iov_len = sizeof(*icresp);
		ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len);
		if (ret < 0) {
	-		queue->state = NVMET_TCP_Q_FAILED;
	+		spin_lock_bh(&queue->state_lock);
	+		if (queue->state != NVMET_TCP_Q_DISCONNECTING)
	+			queue->state = NVMET_TCP_Q_FAILED;
	+		spin_unlock_bh(&queue->state_lock);
			return ret; /* queue removal will cleanup */
		}

This patch :-

		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 */
		}

It will be great if you can provide tested-by tag on this patch so we can merge this fix as well.

-ck

---
 drivers/nvme/target/tcp.c | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
drivers/nvme/target/tcp.c +28 -0
index 69e971b179ae..98b2ce9a70ca 100644
@@ -407,7 +407,22 @@ static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
 
 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 == -ESHUTDOWN)
+		return;
 	if (status == -EPIPE || status == -ECONNRESET)
 		kernel_sock_shutdown(queue->sock, SHUT_RDWR);
 	else
@@ -922,11 +937,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 already in progress. */
+		return -ESHUTDOWN;
+	}
 	queue->state = NVMET_TCP_Q_LIVE;
+	spin_unlock_bh(&queue->state_lock);
 	nvmet_prepare_receive_pdu(queue);
 	return 0;
 }
Signature · 1 line
2.39.5

A message that is not in the archive (sent off-list or not yet received): BN0PR01MB7007C5B4178BCD8619FC5D91C85B2@BN0PR01MB7007.prod.exchangelabs.com

Tested-by
#2 lore raw

_______________________________________

Quoted 138 lines

From: Chaitanya Kulkarni <[email protected]>
Sent: 08 April 2026 03:51
To: Shivam Kumar
Cc: [email protected]; [email protected]; [email protected]; [email protected]; Chaitanya Kulkarni; [email protected]
Subject: [PATCH] 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.

Fixes c46a6465bac2 ("nvmet-tcp: add NVMe over TCP target driver")

---

Hi Shivam,

This patch is different than the one I posted.

Posted patch :-

iov.iov_len = sizeof(*icresp);
ret = kernel_sendmsg(queue->sock, &msg, &iov, 1, iov.iov_len); if (ret < 0) {
- queue->state = NVMET_TCP_Q_FAILED;
+ spin_lock_bh(&queue->state_lock);
+ if (queue->state != NVMET_TCP_Q_DISCONNECTING)
+ queue->state = NVMET_TCP_Q_FAILED;
+ spin_unlock_bh(&queue->state_lock);
return ret; /* queue removal will cleanup */
}

This patch :-

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 */
}

It will be great if you can provide tested-by tag on this patch so we can merge this fix as well.

(quoted diff) +0 -2
-ck
 
---
 drivers/nvme/target/tcp.c | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
drivers/nvme/target/tcp.c +28 -1
index 69e971b179ae..98b2ce9a70ca 100644
@@ -407,7 +407,22 @@ static void nvmet_tcp_fatal_error(struct nvmet_tcp_queue *queue)
 
 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 == -ESHUTDOWN)
+               return;
        if (status == -EPIPE || status == -ECONNRESET)
                kernel_sock_shutdown(queue->sock, SHUT_RDWR);
        else
@@ -922,11 +937,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 already in progress. */
+               return -ESHUTDOWN;
+       }
        queue->state = NVMET_TCP_Q_LIVE;
+       spin_unlock_bh(&queue->state_lock);
        nvmet_prepare_receive_pdu(queue);
        return 0;
 }
--
Quoted 2 lines

2.39.5

Tested this updated patch - the handle_icreq race is fixed.

maintainer
#3 lore raw
In reply to Chaitanya Kulkarni
Quoted 17 lines · Chaitanya Kulkarni

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.

Quoted 7 lines

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.

This looks okay to me. Will give this a couple days then queue it up if no issues reported.

#4 lore raw
In reply to Chaitanya Kulkarni

drivers/nvme/target/tcp.c static int nvmet_tcp_handle_icreq(struct nvmet_tcp_queue *queue) · line 953

Quoted 1 line · Chaitanya Kulkarni
drivers/nvme/target/tcp.c +1 -0
+		/* Tell nvmet_tcp_socket_error() teardown is already in progress. */

Overly long line.

Otherwise looks good.

maintainer Applied
#5 lore raw
In reply to Chaitanya Kulkarni
Quoted 3 lines · Chaitanya Kulkarni

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.

Thanks, applied to nvme-7.1 with the overly long line fixed up when applying.