Linux-NVME Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Martin Belanger" <martin.nvme.dev@gmail.com>
Cc: linux-nvme@lists.infradead.org
Subject: Re: [PATCH RFC net-next v3 4/5] tls: send a KeyUpdate at the record limit and on request
Date: Sat, 31 Oct 2026 11:18:23 +0000	[thread overview]
Message-ID: <sashiko-outbox-191917@kernel.org> (raw)
In-Reply-To: <20261030-keyupdate-rfc-v3-4-8e1e32226e51@gmail.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] tls: KeyUpdate state races when sk_stream_wait_memory() drops lock_sock()
- [Medium] tls: premature tls_tx_maybe_update_key() call in tls_sw_sendmsg()
- [Medium] tls: tls_tx_key_update() ignores send errors and clears sk_err when copied > 0
--

commit 2c49d2f944d8bdc66426d465f54f8eb74134a56b
Author: Martin Belanger <martin.belanger@dell.com>

tls: send a KeyUpdate at the record limit and on request

This commit sends a TLS 1.3 KeyUpdate handshake record and rotates the TX key
when the key reaches 2^24 records or when replying to a peer KeyUpdate with
update_requested. It also adds the TLS_TX_KEY_UPDATE socket option to allow
applications to trigger a KeyUpdate on demand.

> diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
> index cc0177320e7e6..f46cfbd95a6fc 100644
> --- a/net/tls/tls_sw.c
> +++ b/net/tls/tls_sw.c

[ ... ]

> @@ -805,6 +808,16 @@ static int tls_sw_sendmsg_locked(struct sock *sk, struct msghdr *msg,
>  			goto send_end;
>  		}
>  
> +		/* Check before each new record: one call can write many
> +		 * records, and a KeyUpdate reply must go before the next
> +		 * one. The KeyUpdate itself is a handshake record.
> +		 */
> +		if (!ctx->open_rec && record_type == TLS_RECORD_TYPE_DATA) {
> +			ret = tls_tx_maybe_update_key(sk, msg->msg_flags);
> +			if (ret)
> +				goto send_end;
> +		}
> +
>  		if (ctx->open_rec)
>  			rec = ctx->open_rec;
>  		else
> @@ -1005,6 +1018,153 @@ static int tls_sw_sendmsg_locked(struct sock *sk, struct msghdr *msg,
>  	return copied > 0 ? copied : ret;
>  }
>  
> +/* RFC 9846 section 5.5 allows 2^24.5 full-size records per AES-GCM key.
> + * Send the KeyUpdate a little earlier.
> + */
> +#define TLS_KEYUPDATE_TX_RECORDS	BIT_ULL(24)
> +
> +/* Section 4.7.3: the epoch must not exceed 2^48-1. The application
> + * traffic secrets start at epoch 3.
> + */
> +#define TLS_KEYUPDATE_MAX		(BIT_ULL(48) - 4)
> +
> +/* Send a KeyUpdate with the current key, then install the next key.
> + * Called with tx_lock held and the socket locked.
> + */
> +static int tls_tx_key_update(struct sock *sk, u8 request, int flags)
> +{
> +	u8 body[TLS_KEYUPDATE_MSG_SIZE] = {
> +		TLS_HANDSHAKE_KEYUPDATE, 0, 0, 1, request
> +	};
> +	struct tls_context *tls_ctx = tls_get_ctx(sk);
> +	struct tls_sw_context_tx *ctx = tls_sw_ctx_tx(tls_ctx);
> +	u8 record_type = TLS_RECORD_TYPE_HANDSHAKE;
> +	union {
> +		struct cmsghdr cmsg;
> +		u8 buf[CMSG_SPACE(sizeof(record_type))];
> +	} ctrl = {};
> +	struct msghdr msg = {};
> +	struct cmsghdr *cmsg;
> +	struct kvec iov;
> +	int ret;
> +
> +	msg.msg_control = &ctrl;
> +	msg.msg_controllen = sizeof(ctrl);
> +	msg.msg_flags = flags & (MSG_DONTWAIT | MSG_NOSIGNAL);
> +
> +	cmsg = CMSG_FIRSTHDR(&msg);
> +	cmsg->cmsg_level = SOL_TLS;
> +	cmsg->cmsg_type = TLS_SET_RECORD_TYPE;
> +	cmsg->cmsg_len = CMSG_LEN(sizeof(record_type));
> +	memcpy(CMSG_DATA(cmsg), &record_type, sizeof(record_type));
> +
> +	iov.iov_base = body;
> +	iov.iov_len = sizeof(body);
> +	iov_iter_kvec(&msg.msg_iter, ITER_SOURCE, &iov, 1, sizeof(body));
> +
> +	/* A control record closes the open record first, so all data
> +	 * written so far goes out with the current key, ahead of the
> +	 * KeyUpdate.
> +	 */
> +	ret = tls_sw_sendmsg_locked(sk, &msg, sizeof(body));
> +	if (ret < 0)
> +		return ret;
> +
> +	/* Nothing may be encrypted with the next key before the KeyUpdate
> +	 * is encrypted with the current one.
> +	 */
> +	ret = tls_encrypt_async_wait(ctx);
> +	if (!ret)
> +		ret = tls_keyupdate_rekey(sk, 1);

[Severity: Medium]
As reported by Sashiko on v2 [1], what happens in tls_tx_key_update() when
transmitting the KeyUpdate record fails in tls_tx_records(), for example
with -EPIPE on a write-shutdown socket or -ECONNRESET?

Inside tls_sw_sendmsg_locked(), the 5-byte KeyUpdate body is copied into
msg_pl and copied is incremented to 5 before bpf_exec_tx_verdict() calls
tls_push_record() -> tls_tx_records() -> tls_push_sg() ->
tcp_sendmsg_locked().

When tcp_sendmsg_locked() returns an error such as -EPIPE, tls_tx_records()
calls tls_err_abort(sk, rc) to set sk->sk_err. However, bpf_exec_tx_verdict()
only rolls back *copied when sk->sk_err == EBADMSG:

net/tls/tls_sw.c:bpf_exec_tx_verdict() {
	err = tls_push_record(sk, flags, record_type);
	if (err && err != -EINPROGRESS && sk->sk_err == EBADMSG) {
		*copied -= sk_msg_free(sk, msg);
		tls_free_open_rec(sk);
		err = -sk->sk_err;
	}
	return err;
}

At the end of tls_sw_sendmsg_locked(), sk_stream_error(sk, msg->msg_flags,
ret) calls sock_error(sk) on -EPIPE, which atomically clears sk->sk_err back
to 0, and then tls_sw_sendmsg_locked() discards ret because copied > 0:

net/tls/tls_sw.c:tls_sw_sendmsg_locked() {
end:
	ret = sk_stream_error(sk, msg->msg_flags, ret);
	return copied > 0 ? copied : ret;
}

Because tls_sw_sendmsg_locked() returns 5 rather than a negative errno,
wouldn't tls_tx_key_update() treat the failed transmission as a success,
rotate the TX key via tls_keyupdate_rekey(sk, 1), update tx_key_updates and
tx_key_update_reply or tx_key_update_asked, and return 0 with the unsent
KeyUpdate record still sitting on ctx->tx_list and sk->sk_err cleared to 0?

> +	if (ret) {
> +		/* The peer expects the next key: the connection is lost */
> +		TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSTXREKEYERROR);
> +		tls_err_abort(sk, ret);
> +		return ret;
> +	}
> +
> +	TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSTXREKEYOK);
> +	tls_ctx->tx_key_updates++;
> +	if (request == TLS_KEY_UPDATE_NOT_REQUESTED)
> +		tls_ctx->tx_key_update_reply = false;
> +
> +	return 0;
> +}
> +
> +/* Send a KeyUpdate if one is due: in reply to the peer's
> + * update_requested (RFC 9846 section 4.7.3), or before the key reaches
> + * its record limit (section 5.5). ChaCha20-Poly1305 has no record limit:
> + * the sequence number would wrap first.
> + */
> +static int tls_tx_maybe_update_key(struct sock *sk, int flags)
> +{
> +	struct tls_context *ctx = tls_get_ctx(sk);
> +	bool limit;
> +
> +	if (likely(!ctx->tx_secret.len))
> +		return 0;
> +
> +	limit = ctx->prot_info.cipher_type != TLS_CIPHER_CHACHA20_POLY1305 &&
> +		get_unaligned_be64(ctx->tx.rec_seq) >= TLS_KEYUPDATE_TX_RECORDS;
> +	if (!limit && !ctx->tx_key_update_reply)
> +		return 0;
> +
> +	if (ctx->tx_key_updates >= TLS_KEYUPDATE_MAX) {
> +		/* Ignore update_requested. At the record limit the
> +		 * connection must end.
> +		 */
> +		ctx->tx_key_update_reply = false;
> +		return limit ? -EKEYEXPIRED : 0;
> +	}
> +
> +	return tls_tx_key_update(sk, TLS_KEY_UPDATE_NOT_REQUESTED, flags);
> +}
> +
> +/**
> + * tls_sw_key_update - send a KeyUpdate now, for TLS_TX_KEY_UPDATE
> + * @sk: socket
> + * @request: non-zero to also ask the peer to update its key
> + *
> + * Called with tx_lock held and the socket locked.
> + *
> + * Return: 0 on success, or a negative errno.
> + */
> +int tls_sw_key_update(struct sock *sk, int request)
> +{
> +	struct tls_context *ctx = tls_get_ctx(sk);
> +	int ret;
> +
> +	if (!ctx->tx_secret.len)
> +		return -EINVAL;
> +
> +	if (request) {
> +		/* Only the kernel sees the answer */
> +		if (!ctx->rx_secret.len)
> +			return -EINVAL;
> +		/* Section 4.7.3: wait for the answer to the previous one */
> +		if (ctx->tx_key_update_asked)
> +			return -EBUSY;
> +	}
> +
> +	if (ctx->tx_key_updates >= TLS_KEYUPDATE_MAX - 1)
> +		return -EKEYEXPIRED;
> +
> +	/* A reply must carry update_not_requested */
> +	if (request && ctx->tx_key_update_reply) {
> +		ret = tls_tx_key_update(sk, TLS_KEY_UPDATE_NOT_REQUESTED,
> +					MSG_NOSIGNAL);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	ret = tls_tx_key_update(sk, request ? TLS_KEY_UPDATE_REQUESTED :
> +				TLS_KEY_UPDATE_NOT_REQUESTED, MSG_NOSIGNAL);
> +	if (!ret && request)
> +		ctx->tx_key_update_asked = true;
> +
> +	return ret;
> +}

[Severity: High]
As reported by Sashiko on v2 [2], can the KeyUpdate state in struct
tls_context race when tls_sw_sendmsg_locked() drops lock_sock(sk) inside
sk_stream_wait_memory()?

While tls_sw_key_update() and tls_sw_sendmsg() hold both ctx->tx_lock and
lock_sock(sk), do_tls_setsockopt_conf() (in net/tls/tls_main.c) and
tls_keyupdate_rx() (in net/tls/tls_keyupdate.c) only acquire lock_sock(sk).
When the socket send buffer is full, tls_sw_sendmsg_locked() (at
wait_for_sndbuf or wait_for_memory, or via tls_push_sg() ->
tcp_sendmsg_locked()) calls sk_stream_wait_memory(), which releases
lock_sock(sk) while sleeping:

1. In tls_sw_key_update() and tls_tx_maybe_update_key(), ctx->tx_secret.len,
   ctx->rx_secret.len, ctx->tx_key_update_asked, and ctx->tx_key_update_reply
   are checked before calling tls_tx_key_update() -> tls_sw_sendmsg_locked().
   If sk_stream_wait_memory() drops lock_sock(sk) and a concurrent
   setsockopt(SOL_TLS, TLS_TX) runs do_tls_setsockopt_conf() and zeroes
   ctx->tx_secret, tls_tx_key_update() will wake up, finish sending the
   KeyUpdate record, and call tls_keyupdate_rekey(sk, 1), which fails with
   -EINVAL on !secret->len and aborts the connection via tls_err_abort().

2. Similarly, if a concurrent setsockopt(SOL_TLS, TLS_RX) runs
   do_tls_setsockopt_conf() while tls_sw_key_update(sk, 1) is sleeping in
   sk_stream_wait_memory(), it clears ctx->rx_secret and resets
   ctx->tx_key_update_asked = false. When tls_sw_key_update(sk, 1) wakes up,
   it sends KeyUpdate(TLS_KEY_UPDATE_REQUESTED) and sets
   ctx->tx_key_update_asked = true without a registered rx_secret, leaving
   tx_key_update_asked permanently stuck because user space will consume the
   peer's reply.

3. If tls_keyupdate_rx() processes a peer KeyUpdate with
   TLS_KEY_UPDATE_REQUESTED while lock_sock(sk) is dropped in
   sk_stream_wait_memory(), ctx->tx_key_update_reply becomes true after:
   - tls_sw_key_update(sk, 1) already checked if (request &&
     ctx->tx_key_update_reply), causing it to send
     KeyUpdate(TLS_KEY_UPDATE_REQUESTED) without first sending the required
     KeyUpdate(TLS_KEY_UPDATE_NOT_REQUESTED) reply.
   - tls_sw_sendmsg_locked() already checked if (!ctx->open_rec &&
     record_type == TLS_RECORD_TYPE_DATA) and allocated ctx->open_rec =
     tls_get_rec(sk), so waking up from wait_for_sndbuf jumps to
     alloc_encrypted and sends the Application Data record before the
     KeyUpdate reply.

>  
>  int tls_sw_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
>  {
>  	struct tls_context *tls_ctx = tls_get_ctx(sk);
> @@ -1019,7 +1179,9 @@ int tls_sw_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
>  	if (ret)
>  		return ret;
>  	lock_sock(sk);
> -	ret = tls_sw_sendmsg_locked(sk, msg, size);
> +	ret = tls_tx_maybe_update_key(sk, msg->msg_flags);
> +	if (!ret)
> +		ret = tls_sw_sendmsg_locked(sk, msg, size);

[Severity: Medium]
Should tls_sw_sendmsg() really call tls_tx_maybe_update_key() unconditionally
before tls_sw_sendmsg_locked() validates msg, parses control messages, and
checks msg_data_left(msg)?

Inside tls_sw_sendmsg_locked(), the KeyUpdate check in the loop is guarded by
!ctx->open_rec && record_type == TLS_RECORD_TYPE_DATA after tls_process_cmsg()
has parsed any control message and after msg_data_left(msg) and sk->sk_err
have been checked.

By calling tls_tx_maybe_update_key() at the top of tls_sw_sendmsg() before
entering tls_sw_sendmsg_locked():

1. If the caller is sending a non-Application-Data control record via
   TLS_SET_RECORD_TYPE (such as a TLS Alert record for close_notify),
   tls_tx_maybe_update_key() runs before tls_process_cmsg() sets record_type,
   sending a KeyUpdate before an Alert record or failing with -EKEYEXPIRED at
   TLS_KEYUPDATE_MAX and blocking the Alert from being sent.

2. If the sendmsg() call has size == 0, invalid flags (MSG_MORE | MSG_EOR),
   or a malformed control message that tls_sw_sendmsg_locked() would reject
   with -EINVAL, tls_tx_maybe_update_key() still flushes any open MSG_MORE
   record, transmits a KeyUpdate on the wire, and rotates the TX key.

3. If sk->sk_err is already set on the socket when tx_key_updates >=
   TLS_KEYUPDATE_MAX and the record limit is reached,
   tls_tx_maybe_update_key() returns -EKEYEXPIRED before
   tls_sw_sendmsg_locked() can report -sk->sk_err.

[1] https://lore.kernel.org/r/20261028-keyupdate-rfc-v2-4-eb5143b4c487@gmail.com
[2] https://lore.kernel.org/r/20261028-keyupdate-rfc-v2-4-eb5143b4c487@gmail.com

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030-keyupdate-rfc-v3-0-8e1e32226e51@gmail.com?part=4


  reply	other threads:[~2026-10-31 11:18 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-30 11:18 [PATCH RFC net-next v3 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 2/5] tls: add socket options to register the traffic secrets Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 3/5] tls: update the RX key when a KeyUpdate is received Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 4/5] tls: send a KeyUpdate at the record limit and on request Martin Belanger via B4 Relay
2026-10-31 11:18   ` sashiko-bot [this message]
2026-10-30 11:18 ` [PATCH RFC net-next v3 5/5] selftests/tls: add KeyUpdate tests Martin Belanger via B4 Relay

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-191917@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=linux-nvme@lists.infradead.org \
    --cc=martin.nvme.dev@gmail.com \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox