[PATCH 2/2] nvme-tcp: skip TLS control records while waiting for the ICResp
sashiko-bot at kernel.org
sashiko-bot at kernel.org
Mon Oct 26 11:24:04 PDT 2026
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] nvme-tcp: missing msg_control in nvme_tcp_skip_tls_record() causes -EIO
- [Medium] nvme-tcp: unhandled EOF in nvme_tcp_recv_icresp() on TLS queues
--
commit e6540f770ca0b1e419e220cc7a58ea6cad9fb1eb
Author: Martin Belanger <martin.belanger at dell.com>
nvme-tcp: skip TLS control records while waiting for the ICResp
This commit updates nvme-tcp connection initialization to skip non-data TLS
records (such as TLS 1.3 NewSessionTicket handshake records) and process
TLS alerts while waiting for the ICResp PDU.
> diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
> index a03476f0f69d5..2d361a9e87101 100644
> --- a/drivers/nvme/host/tcp.c
> +++ b/drivers/nvme/host/tcp.c
[ ... ]
> @@ -1548,12 +1554,138 @@ static void nvme_tcp_free_queue(struct nvme_ctrl *nctrl, int qid)
[ ... ]
> +static int nvme_tcp_skip_tls_record(struct nvme_tcp_queue *queue,
> + struct msghdr *msg, u8 ctype, int len)
> +{
> + u8 buf[NVME_TCP_TLS_REC_CHUNK];
> + int ret;
> +
> + switch (ctype) {
> + case TLS_RECORD_TYPE_HANDSHAKE:
> + break;
> + case TLS_RECORD_TYPE_ALERT:
> + ret = nvme_tcp_recv_tls_alert(queue, msg, len);
> + return ret ? : -ENOTCONN;
> + default:
> + pr_err("queue %d: unhandled TLS record %u\n",
> + nvme_tcp_queue_id(queue), ctype);
> + return -ENOTCONN;
> + }
> +
> + /* the ICResp is in a later record, drain this one first */
> + while (!(msg->msg_flags & MSG_EOR)) {
> + struct kvec iov = {
> + .iov_base = buf,
> + .iov_len = sizeof(buf),
> + };
> + struct msghdr drain = {};
> +
> + ret = kernel_recvmsg(queue->sock, &drain, &iov, 1, sizeof(buf),
> + 0);
> + if (ret <= 0) {
> + pr_warn("queue %d: failed to drain TLS record: %d\n",
> + nvme_tcp_queue_id(queue), ret);
> + return ret ? : -ECONNRESET;
> + }
> + msg->msg_flags = drain.msg_flags;
> + }
[Severity: High]
Will draining a handshake record larger than sizeof(*icresp) (128 bytes) in
drivers/nvme/host/tcp.c:nvme_tcp_skip_tls_record() always fail with -EIO?
When a TLS peer (such as an OpenSSL target sending a TLS 1.3
NewSessionTicket) sends a TLS_RECORD_TYPE_HANDSHAKE record larger than 128
bytes, the initial kernel_recvmsg() in nvme_tcp_recv_icresp() reads the
first 128 bytes and returns without setting MSG_EOR in msg.msg_flags.
In nvme_tcp_skip_tls_record(), the drain msghdr is zero-initialized with
drain.msg_control set to NULL and drain.msg_controllen set to 0 before
calling kernel_recvmsg().
In the kTLS receive path:
kernel_recvmsg()
-> tls_sw_recvmsg()
-> process_rx_list()
-> tls_record_content_type()
-> put_cmsg()
net/core/scm.c:put_cmsg() checks msg->msg_control:
if (!msg->msg_control || msg->msg_controllen < sizeof(struct cmsghdr)) {
msg->msg_flags |= MSG_CTRUNC;
return 0; /* XXX: return error? check spec. */
}
And net/tls/tls_sw.c:tls_record_content_type() rejects non-data records
when MSG_CTRUNC is set:
if (!*control) {
*control = tlm->control;
if (!*control)
return -EBADMSG;
err = put_cmsg(msg, SOL_TLS, TLS_GET_RECORD_TYPE,
sizeof(*control), control);
if (*control != TLS_RECORD_TYPE_DATA) {
if (err || msg->msg_flags & MSG_CTRUNC)
return -EIO;
}
}
Because drain does not provide a control message buffer,
tls_record_content_type() returns -EIO without consuming the rest of the
handshake record, causing nvme_tcp_skip_tls_record() and
nvme_tcp_init_connection() to fail with -EIO.
Should drain.msg_control and drain.msg_controllen be populated with a
cmsg buffer on each iteration of the drain loop?
> +
> + return 0;
> +}
> +
> +/*
> + * Receive the ICResp. Over TLS, skip the records that a peer may send
> + * ahead of it.
> + *
> + * Returns the length received, or a negative errno.
> + */
> +static int nvme_tcp_recv_icresp(struct nvme_tcp_queue *queue,
> + struct nvme_tcp_icresp_pdu *icresp)
> +{
> + char cbuf[CMSG_LEN(sizeof(char))];
> + struct msghdr msg;
> + struct kvec iov;
> + u8 ctype;
> + int i, ret;
> +
> + for (i = 0; i < NVME_TCP_ICRESP_MAX_RECORDS; i++) {
> + memset(&msg, 0, sizeof(msg));
> + memset(cbuf, 0, sizeof(cbuf));
> + iov.iov_base = icresp;
> + iov.iov_len = sizeof(*icresp);
> + if (nvme_tcp_queue_tls(queue)) {
> + msg.msg_control = cbuf;
> + msg.msg_controllen = sizeof(cbuf);
> + }
> + msg.msg_flags = MSG_WAITALL;
> + ret = kernel_recvmsg(queue->sock, &msg, &iov, 1,
> + iov.iov_len, msg.msg_flags);
> + if (ret < 0 || !nvme_tcp_queue_tls(queue))
> + return ret;
[Severity: Medium]
What happens in drivers/nvme/host/tcp.c:nvme_tcp_recv_icresp() if the TLS
peer closes the connection cleanly before sending the ICResp and
kernel_recvmsg() returns 0?
When kernel_recvmsg() returns 0 on EOF over a TLS-enabled queue, the check
above (ret < 0 || !nvme_tcp_queue_tls(queue)) is false, so execution
continues to tls_get_record_type() with the zeroed cbuf.
Since cmsg->cmsg_level is 0, tls_get_record_type() returns 0, and
nvme_tcp_skip_tls_record() hits the default case:
default:
pr_err("queue %d: unhandled TLS record %u\n",
nvme_tcp_queue_id(queue), ctype);
return -ENOTCONN;
This logs a spurious "unhandled TLS record 0" error and returns -ENOTCONN,
bypassing the EOF handling in nvme_tcp_init_connection():
ret = nvme_tcp_recv_icresp(queue, icresp);
if (ret >= 0 && ret < sizeof(*icresp))
ret = -ECONNRESET;
Should the early return condition in nvme_tcp_recv_icresp() check ret <= 0
instead of ret < 0?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261026-nvme-tcp-ctrlrec-fixes-v1-0-ebf6ff7eff34@gmail.com?part=2
More information about the Linux-nvme
mailing list